diff --git a/docs/SCORING.md b/docs/SCORING.md index ce0b121..ea06a53 100644 --- a/docs/SCORING.md +++ b/docs/SCORING.md @@ -70,6 +70,65 @@ client.create_score( - Content categories - Quality tiers (low/medium/high) +### Text + +Free-form string notes (1 to 500 characters): + +```ruby +client.create_score( + name: "reviewer_notes", + value: "The response was helpful but could be more concise.", + trace_id: "abc123...", + data_type: :text +) +``` + +**Common use cases:** +- Reviewer notes and qualitative feedback +- Short explanations attached to a trace or observation + +Values must be strings containing 1 to 500 characters; anything else raises +`ArgumentError`. + +### Correction (Corrected Outputs) + +Corrections capture an improved version of an LLM output directly on a trace or +observation. They are scores with `dataType: "CORRECTION"`; Langfuse persists +their name as `"output"`: + +```ruby +client.create_score( + name: "output", # persisted by Langfuse as "output" + value: "The corrected output text", # the full replacement output + trace_id: "abc123...", + observation_id: "def456...", # optional: target an observation + data_type: :correction +) +``` + +**Common use cases:** +- Domain experts documenting what the model should have generated +- Human-in-the-loop review workflows +- Building fine-tuning datasets from corrected outputs + +Values must be strings; there is no length limit. Provide structured +corrections as JSON text when appropriate — the SDK does not serialize objects +for you. A correction must have a `trace_id`; `observation_id` may additionally +target one observation. Session, dataset-run, and score-config associations are +rejected because Langfuse only accepts corrections on traces or observations. +Corrections appear in the Langfuse UI alongside the original output with a diff +view, and can be read back through the v3 scores API +(`GET /api/public/v3/scores?dataType=CORRECTION`). + +### Text/Correction vs. Experiment Evaluations + +Text and correction scores are general scores, not experiment metrics. +`Langfuse::Evaluation` (used by experiment evaluators — see +[EXPERIMENTS.md](EXPERIMENTS.md)) intentionally accepts only `:numeric`, +`:boolean`, and `:categorical` and raises `ArgumentError` for `:text` and +`:correction`, matching the Python SDK. Create corrections and text notes +through `create_score` against the trace or observation instead. + ## Creating Scores ### Client-Level API diff --git a/lib/langfuse.rb b/lib/langfuse.rb index 5e5e28f..2eb3053 100644 --- a/lib/langfuse.rb +++ b/lib/langfuse.rb @@ -57,6 +57,7 @@ class UnauthorizedError < ApiError; end require_relative "langfuse/span_processor" require_relative "langfuse/observations" require_relative "langfuse/trace_id" +require_relative "langfuse/score_value" require_relative "langfuse/score_client" require_relative "langfuse/prompt_renderer" require_relative "langfuse/text_prompt_client" @@ -224,7 +225,7 @@ def propagate_attributes(user_id: nil, session_id: nil, metadata: nil, version: # @param comment [String, nil] Optional comment # @param metadata [Hash, nil] Optional metadata hash # @param environment [String, nil] Optional environment - # @param data_type [Symbol] Data type (:numeric, :boolean, :categorical) + # @param data_type [Symbol] Data type (:numeric, :boolean, :categorical, :text, :correction) # @param dataset_run_id [String, nil] Optional dataset run ID to associate with the score # @param config_id [String, nil] Optional score config ID # @return [void] @@ -266,7 +267,7 @@ def create_score(name:, value:, id: nil, trace_id: nil, session_id: nil, observa # @param value [Numeric, Integer, String] Score value # @param comment [String, nil] Optional comment # @param metadata [Hash, nil] Optional metadata hash - # @param data_type [Symbol] Data type (:numeric, :boolean, :categorical) + # @param data_type [Symbol] Data type (:numeric, :boolean, :categorical, :text, :correction) # @return [void] # @raise [ArgumentError] if no active span or validation fails # @@ -292,7 +293,7 @@ def score_active_observation(name:, value:, comment: nil, metadata: nil, data_ty # @param value [Numeric, Integer, String] Score value # @param comment [String, nil] Optional comment # @param metadata [Hash, nil] Optional metadata hash - # @param data_type [Symbol] Data type (:numeric, :boolean, :categorical) + # @param data_type [Symbol] Data type (:numeric, :boolean, :categorical, :text, :correction) # @return [void] # @raise [ArgumentError] if no active span or validation fails # diff --git a/lib/langfuse/client.rb b/lib/langfuse/client.rb index 1a82d17..9f50de9 100644 --- a/lib/langfuse/client.rb +++ b/lib/langfuse/client.rb @@ -354,7 +354,7 @@ def dataset_run_url(dataset_id:, dataset_run_id:) # @param comment [String, nil] Optional comment # @param metadata [Hash, nil] Optional metadata hash # @param environment [String, nil] Optional environment - # @param data_type [Symbol] Data type (:numeric, :boolean, :categorical) + # @param data_type [Symbol] Data type (:numeric, :boolean, :categorical, :text, :correction) # @param dataset_run_id [String, nil] Optional dataset run ID to associate with the score # @param config_id [String, nil] Optional score config ID # @return [void] @@ -368,6 +368,14 @@ def dataset_run_url(dataset_id:, dataset_run_id:) # # @example Categorical score # client.create_score(name: "category", value: "high", trace_id: "abc123", data_type: :categorical) + # + # @example Text score (1 to 500 characters) + # client.create_score(name: "reviewer_notes", value: "Helpful but verbose", + # trace_id: "abc123", data_type: :text) + # + # @example Corrected output (conventionally named "output") + # client.create_score(name: "output", value: "The corrected output", trace_id: "abc123", + # observation_id: "def456", data_type: :correction) # rubocop:disable Metrics/ParameterLists def create_score(name:, value:, id: nil, trace_id: nil, session_id: nil, observation_id: nil, comment: nil, metadata: nil, environment: nil, data_type: :numeric, dataset_run_id: nil, config_id: nil) @@ -396,7 +404,7 @@ def create_score(name:, value:, id: nil, trace_id: nil, session_id: nil, observa # @param value [Numeric, Integer, String] Score value # @param comment [String, nil] Optional comment # @param metadata [Hash, nil] Optional metadata hash - # @param data_type [Symbol] Data type (:numeric, :boolean, :categorical) + # @param data_type [Symbol] Data type (:numeric, :boolean, :categorical, :text, :correction) # @return [void] # @raise [ArgumentError] if no active span or validation fails # @@ -422,7 +430,7 @@ def score_active_observation(name:, value:, comment: nil, metadata: nil, data_ty # @param value [Numeric, Integer, String] Score value # @param comment [String, nil] Optional comment # @param metadata [Hash, nil] Optional metadata hash - # @param data_type [Symbol] Data type (:numeric, :boolean, :categorical) + # @param data_type [Symbol] Data type (:numeric, :boolean, :categorical, :text, :correction) # @return [void] # @raise [ArgumentError] if no active span or validation fails # diff --git a/lib/langfuse/evaluation.rb b/lib/langfuse/evaluation.rb index f42ec79..d00e8eb 100644 --- a/lib/langfuse/evaluation.rb +++ b/lib/langfuse/evaluation.rb @@ -26,17 +26,21 @@ class Evaluation # @param name [String] Score name (required, must be non-empty) # @param value [Numeric, Boolean, String] Score value (type depends on data_type) # @param comment [String, nil] Optional comment describing the evaluation - # @param data_type [Symbol] One of :numeric, :boolean, or :categorical + # @param data_type [Symbol] One of :numeric, :boolean, or :categorical. + # Experiment evaluations are metrics, so the general-score :text and + # :correction types are rejected here; create those through + # {Client#create_score} instead. # @param config_id [String, nil] Optional score config ID # @param metadata [Hash, nil] Optional metadata hash # @raise [ArgumentError] if name is nil or empty - # @raise [ArgumentError] if data_type is not a valid score data type + # @raise [ArgumentError] if data_type is not an experiment score data type def initialize(name:, value:, comment: nil, data_type: :numeric, config_id: nil, metadata: nil) raise ArgumentError, "name is required" if name.to_s.empty? - unless Types::SCORE_DATA_TYPES.key?(data_type) + unless Types::EXPERIMENT_SCORE_DATA_TYPES.include?(data_type) raise ArgumentError, - "Invalid data_type: #{data_type}. Valid types: #{Types::VALID_SCORE_DATA_TYPES.join(', ')}" + "Invalid data_type: #{data_type}. Experiment evaluations accept: " \ + "#{Types::EXPERIMENT_SCORE_DATA_TYPES.join(', ')}" end validate_value!(value, data_type) diff --git a/lib/langfuse/observations.rb b/lib/langfuse/observations.rb index e5109e0..799ef8c 100644 --- a/lib/langfuse/observations.rb +++ b/lib/langfuse/observations.rb @@ -201,7 +201,7 @@ def current_span # @param value [Numeric, String, Boolean] score value # @param comment [String, nil] optional comment # @param metadata [Hash, nil] optional metadata - # @param data_type [Symbol] one of :numeric, :boolean, :categorical + # @param data_type [Symbol] one of :numeric, :boolean, :categorical, :text, :correction # @return [Hash] created score data from the API def score_trace(name:, value:, comment: nil, metadata: nil, data_type: :numeric) Langfuse.create_score( diff --git a/lib/langfuse/score_client.rb b/lib/langfuse/score_client.rb index 050ed1e..33766a9 100644 --- a/lib/langfuse/score_client.rb +++ b/lib/langfuse/score_client.rb @@ -63,7 +63,7 @@ def initialize(api_client:, config:) # @param comment [String, nil] Optional comment # @param metadata [Hash, nil] Optional metadata hash # @param environment [String, nil] Optional environment - # @param data_type [Symbol] Data type (:numeric, :boolean, :categorical) + # @param data_type [Symbol] Data type (:numeric, :boolean, :categorical, :text, :correction) # @param dataset_run_id [String, nil] Optional dataset run ID to associate with the score # @param config_id [String, nil] Optional score config ID # @return [void] @@ -77,12 +77,20 @@ def initialize(api_client:, config:) # # @example Categorical score # create(name: "category", value: "high", trace_id: "abc123", data_type: :categorical) + # + # @example Text score (1 to 500 characters) + # create(name: "reviewer_notes", value: "Helpful but verbose", trace_id: "abc123", data_type: :text) + # + # @example Corrected output (conventionally named "output") + # create(name: "output", value: "The corrected output", trace_id: "abc123", + # observation_id: "def456", data_type: :correction) # rubocop:disable Metrics/ParameterLists def create(name:, value:, id: nil, trace_id: nil, session_id: nil, observation_id: nil, comment: nil, metadata: nil, environment: nil, data_type: :numeric, dataset_run_id: nil, config_id: nil) validate_name(name) - normalized_value = normalize_value(value, data_type) + normalized_value = ScoreValue.normalize(value, data_type) data_type_str = Types::SCORE_DATA_TYPES[data_type] || raise(ArgumentError, "Invalid data_type: #{data_type}") + validate_correction_subject!(data_type:, trace_id:, session_id:, dataset_run_id:, config_id:) return unless enqueue_trace_linked_score?(trace_id) @@ -109,7 +117,7 @@ def create(name:, value:, id: nil, trace_id: nil, session_id: nil, observation_i # @param value [Numeric, Integer, String] Score value # @param comment [String, nil] Optional comment # @param metadata [Hash, nil] Optional metadata hash - # @param data_type [Symbol] Data type (:numeric, :boolean, :categorical) + # @param data_type [Symbol] Data type (:numeric, :boolean, :categorical, :text, :correction) # @return [void] # @raise [ArgumentError] if no active span or validation fails # @@ -140,7 +148,7 @@ def score_active_observation(name:, value:, comment: nil, metadata: nil, data_ty # @param value [Numeric, Integer, String] Score value # @param comment [String, nil] Optional comment # @param metadata [Hash, nil] Optional metadata hash - # @param data_type [Symbol] Data type (:numeric, :boolean, :categorical) + # @param data_type [Symbol] Data type (:numeric, :boolean, :categorical, :text, :correction) # @return [void] # @raise [ArgumentError] if no active span or validation fails # @@ -214,7 +222,7 @@ def shutdown # @param comment [String, nil] Comment # @param metadata [Hash, nil] Metadata # @param environment [String, nil] Environment - # @param data_type [String] Data type string (NUMERIC, BOOLEAN, CATEGORICAL) + # @param data_type [String] API score data type string # @return [Hash] Event hash # rubocop:disable Metrics/ParameterLists def build_score_event(name:, value:, id:, trace_id:, session_id:, observation_id:, comment:, metadata:, @@ -242,37 +250,13 @@ def build_score_event(name:, value:, id:, trace_id:, session_id:, observation_id end # rubocop:enable Metrics/ParameterLists - # Normalize and validate score value based on data type - # - # @param value [Object] Raw score value - # @param data_type [Symbol] Data type symbol - # @return [Object] Normalized value - # @raise [ArgumentError] if value doesn't match data type - # rubocop:disable Metrics/CyclomaticComplexity - def normalize_value(value, data_type) - case data_type - when :numeric - raise ArgumentError, "Numeric value must be Numeric, got #{value.class}" unless value.is_a?(Numeric) - - value - when :boolean - case value - when true, 1 - 1 - when false, 0 - 0 - else - raise ArgumentError, "Boolean value must be true/false or 0/1, got #{value.inspect}" - end - when :categorical - raise ArgumentError, "Categorical value must be a String, got #{value.class}" unless value.is_a?(String) + def validate_correction_subject!(data_type:, trace_id:, session_id:, dataset_run_id:, config_id:) + return unless data_type == :correction + return if trace_id.is_a?(String) && !trace_id.empty? && !session_id && !dataset_run_id && !config_id - value - else - raise ArgumentError, "Invalid data_type: #{data_type}" - end + raise ArgumentError, + "Correction scores require trace_id and cannot use session_id, dataset_run_id, or config_id" end - # rubocop:enable Metrics/CyclomaticComplexity # Validate score name # diff --git a/lib/langfuse/score_value.rb b/lib/langfuse/score_value.rb new file mode 100644 index 0000000..7cce6d5 --- /dev/null +++ b/lib/langfuse/score_value.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +module Langfuse + # Normalizes score values to the wire representation expected by Langfuse. + # @api private + module ScoreValue + TEXT_LENGTH_RANGE = (1..500) + private_constant :TEXT_LENGTH_RANGE + + # @param value [Object] raw score value + # @param data_type [Symbol] score data type + # @return [Numeric, String] normalized score value + # @raise [ArgumentError] if the value does not match the data type + def self.normalize(value, data_type) + case data_type + when :numeric then numeric(value) + when :boolean then boolean(value) + when :categorical then string(value, "Categorical") + when :text then text(value) + when :correction then string(value, "Correction") + else raise ArgumentError, "Invalid data_type: #{data_type}" + end + end + + def self.numeric(value) + raise ArgumentError, "Numeric value must be Numeric, got #{value.class}" unless value.is_a?(Numeric) + + value + end + private_class_method :numeric + + def self.boolean(value) + case value + when true, 1 then 1 + when false, 0 then 0 + else raise ArgumentError, "Boolean value must be true/false or 0/1, got #{value.inspect}" + end + end + private_class_method :boolean + + def self.text(value) + string(value, "Text") + unless TEXT_LENGTH_RANGE.cover?(value.length) + raise ArgumentError, "Text value must contain 1 to 500 characters, got #{value.length}" + end + + value + end + private_class_method :text + + def self.string(value, label) + raise ArgumentError, "#{label} value must be a String, got #{value.class}" unless value.is_a?(String) + + value + end + private_class_method :string + end +end diff --git a/lib/langfuse/types.rb b/lib/langfuse/types.rb index b5e53ce..9ed41f3 100644 --- a/lib/langfuse/types.rb +++ b/lib/langfuse/types.rb @@ -56,12 +56,17 @@ module Types # - `numeric`: Numeric values (Integer, Float, BigDecimal) # - `boolean`: Boolean values (true/false or 0/1) # - `categorical`: String values for categorical scores + # - `text`: Free-form String values (1 to 500 characters) + # - `correction`: Corrected output String attached to a trace or observation + # (conventionally named "output") # # @return [Hash] Hash mapping symbol keys to API string values SCORE_DATA_TYPES = { numeric: "NUMERIC", boolean: "BOOLEAN", - categorical: "CATEGORICAL" + categorical: "CATEGORICAL", + text: "TEXT", + correction: "CORRECTION" }.freeze # Valid score data type symbols @@ -69,6 +74,15 @@ module Types # @return [Array] Array of valid data type symbols VALID_SCORE_DATA_TYPES = SCORE_DATA_TYPES.keys.freeze + # Score data types accepted by experiment evaluations + # + # Experiment evaluations represent metrics, so this set is intentionally + # narrower than {SCORE_DATA_TYPES}: text notes and corrected outputs are + # general scores, not experiment metrics (matching langfuse-python). + # + # @return [Array] Array of experiment-safe data type symbols + EXPERIMENT_SCORE_DATA_TYPES = %i[numeric boolean categorical].freeze + # Attributes for Langfuse span observations # # Spans are used to track operations, functions, or logical units of work. diff --git a/spec/langfuse/client_spec.rb b/spec/langfuse/client_spec.rb index 6903f0d..df79db1 100644 --- a/spec/langfuse/client_spec.rb +++ b/spec/langfuse/client_spec.rb @@ -1593,6 +1593,36 @@ def self.cache client.create_score(name: "quality", value: 0.85, trace_id: "abc123") end + it "forwards correction scores unchanged" do + score_client = client.instance_variable_get(:@score_client) + expect(score_client).to receive(:create).with( + hash_including( + name: "output", + value: "The corrected output", + trace_id: "abc123", + observation_id: "def456", + data_type: :correction + ) + ) + + client.create_score( + name: "output", + value: "The corrected output", + trace_id: "abc123", + observation_id: "def456", + data_type: :correction + ) + end + + it "forwards text scores unchanged" do + score_client = client.instance_variable_get(:@score_client) + expect(score_client).to receive(:create).with( + hash_including(name: "reviewer_notes", value: "Helpful", data_type: :text) + ) + + client.create_score(name: "reviewer_notes", value: "Helpful", trace_id: "abc123", data_type: :text) + end + it "passes the full score kwarg set to score_client" do score_client = client.instance_variable_get(:@score_client) expect(score_client).to receive(:create).with( diff --git a/spec/langfuse/evaluation_spec.rb b/spec/langfuse/evaluation_spec.rb index e1e031c..e6360a0 100644 --- a/spec/langfuse/evaluation_spec.rb +++ b/spec/langfuse/evaluation_spec.rb @@ -52,6 +52,16 @@ .to raise_error(ArgumentError, /Invalid data_type: invalid/) end + it "rejects the general-score :text type" do + expect { described_class.new(name: "notes", value: "some text", data_type: :text) } + .to raise_error(ArgumentError, /Invalid data_type: text.*numeric, boolean, categorical/) + end + + it "rejects the general-score :correction type" do + expect { described_class.new(name: "output", value: "corrected", data_type: :correction) } + .to raise_error(ArgumentError, /Invalid data_type: correction.*numeric, boolean, categorical/) + end + context "with value type validation" do it "raises ArgumentError for non-numeric value with numeric data_type" do expect { described_class.new(name: "test", value: "hello", data_type: :numeric) } diff --git a/spec/langfuse/score_client_spec.rb b/spec/langfuse/score_client_spec.rb index 32b38b5..ca83056 100644 --- a/spec/langfuse/score_client_spec.rb +++ b/spec/langfuse/score_client_spec.rb @@ -248,6 +248,106 @@ end end + context "with text score" do + it "emits dataType TEXT with the string value" do + expect(api_client).to receive(:send_batch).with(array_including( + hash_including( + body: hash_including( + name: "reviewer_notes", + value: "Helpful but verbose", + dataType: "TEXT" + ) + ) + )) + + score_client.create(name: "reviewer_notes", value: "Helpful but verbose", data_type: :text) + score_client.flush + end + + it "accepts a 500-character value" do + expect(api_client).to receive(:send_batch) + + score_client.create(name: "notes", value: "a" * 500, data_type: :text) + score_client.flush + end + + it "rejects an empty value" do + expect do + score_client.create(name: "notes", value: "", data_type: :text) + end.to raise_error(ArgumentError, /Text value must contain 1 to 500 characters, got 0/) + end + + it "rejects a value longer than 500 characters" do + expect do + score_client.create(name: "notes", value: "a" * 501, data_type: :text) + end.to raise_error(ArgumentError, /Text value must contain 1 to 500 characters, got 501/) + end + + it "rejects non-string values" do + expect do + score_client.create(name: "notes", value: 42, data_type: :text) + end.to raise_error(ArgumentError, /Text value must be a String, got Integer/) + end + end + + context "with correction score" do + it "emits dataType CORRECTION with the caller's name and string value" do + expect(api_client).to receive(:send_batch).with(array_including( + hash_including( + body: hash_including( + name: "output", + value: "The corrected output", + dataType: "CORRECTION", + traceId: "trace-1", + observationId: "obs-1" + ) + ) + )) + + score_client.create(name: "output", value: "The corrected output", + trace_id: "trace-1", observation_id: "obs-1", data_type: :correction) + score_client.flush + end + + it "preserves an arbitrary caller-supplied name without rewriting it" do + expect(api_client).to receive(:send_batch).with(array_including( + hash_including(body: hash_including(name: "custom-name")) + )) + + score_client.create(name: "custom-name", value: "corrected", trace_id: "trace-1", data_type: :correction) + score_client.flush + end + + it "does not enforce a length limit" do + expect(api_client).to receive(:send_batch) + + score_client.create(name: "output", value: "a" * 10_000, trace_id: "trace-1", data_type: :correction) + score_client.flush + end + + it "rejects non-string values" do + expect do + score_client.create(name: "output", value: { text: "corrected" }, data_type: :correction) + end.to raise_error(ArgumentError, /Correction value must be a String, got Hash/) + end + + it "rejects subjects that Langfuse cannot attach a correction to" do + invalid_subjects = [ + {}, + { observation_id: "obs-1" }, + { trace_id: "trace-1", session_id: "session-1" }, + { trace_id: "trace-1", dataset_run_id: "run-1" }, + { trace_id: "trace-1", config_id: "config-1" } + ] + + invalid_subjects.each do |subject| + expect do + score_client.create(name: "output", value: "corrected", data_type: :correction, **subject) + end.to raise_error(ArgumentError, /Correction scores require trace_id/) + end + end + end + context "with validation errors" do it "raises ArgumentError for missing name" do expect do diff --git a/spec/langfuse/score_value_spec.rb b/spec/langfuse/score_value_spec.rb new file mode 100644 index 0000000..015bb17 --- /dev/null +++ b/spec/langfuse/score_value_spec.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +RSpec.describe Langfuse::ScoreValue do + describe ".normalize" do + it "preserves numeric values" do + expect(described_class.normalize(0.75, :numeric)).to eq(0.75) + end + + it "normalizes boolean values to the ingestion representation" do + expect(described_class.normalize(true, :boolean)).to eq(1) + expect(described_class.normalize(false, :boolean)).to eq(0) + end + + it "preserves categorical strings" do + expect(described_class.normalize("high", :categorical)).to eq("high") + end + + it "accepts text values through the 500-character boundary" do + value = "a" * 500 + + expect(described_class.normalize(value, :text)).to equal(value) + end + + it "rejects text outside the documented length range" do + expect { described_class.normalize("", :text) } + .to raise_error(ArgumentError, /1 to 500 characters, got 0/) + expect { described_class.normalize("a" * 501, :text) } + .to raise_error(ArgumentError, /1 to 500 characters, got 501/) + end + + it "accepts correction strings without an invented length limit" do + value = "a" * 10_000 + + expect(described_class.normalize(value, :correction)).to equal(value) + end + + it "rejects values that do not match the requested data type" do + expect { described_class.normalize("no", :numeric) }.to raise_error(ArgumentError, /Numeric/) + expect { described_class.normalize(2, :boolean) }.to raise_error(ArgumentError, /Boolean/) + expect { described_class.normalize(1, :categorical) }.to raise_error(ArgumentError, /Categorical/) + expect { described_class.normalize(1, :text) }.to raise_error(ArgumentError, /Text/) + expect { described_class.normalize(1, :correction) }.to raise_error(ArgumentError, /Correction/) + end + + it "rejects unknown data types" do + expect { described_class.normalize("value", :unknown) } + .to raise_error(ArgumentError, "Invalid data_type: unknown") + end + end +end diff --git a/spec/langfuse/types_spec.rb b/spec/langfuse/types_spec.rb index 2b97cb0..94fb741 100644 --- a/spec/langfuse/types_spec.rb +++ b/spec/langfuse/types_spec.rb @@ -31,6 +31,38 @@ end end + describe "SCORE_DATA_TYPES" do + it "maps all five general score data types to API strings" do + expect(described_class::SCORE_DATA_TYPES).to eq( + numeric: "NUMERIC", + boolean: "BOOLEAN", + categorical: "CATEGORICAL", + text: "TEXT", + correction: "CORRECTION" + ) + end + + it "is frozen" do + expect(described_class::SCORE_DATA_TYPES).to be_frozen + end + end + + describe "EXPERIMENT_SCORE_DATA_TYPES" do + it "contains only metric-style data types" do + expect(described_class::EXPERIMENT_SCORE_DATA_TYPES).to contain_exactly( + :numeric, :boolean, :categorical + ) + end + + it "excludes the general-score text and correction types" do + expect(described_class::EXPERIMENT_SCORE_DATA_TYPES).not_to include(:text, :correction) + end + + it "is frozen" do + expect(described_class::EXPERIMENT_SCORE_DATA_TYPES).to be_frozen + end + end + describe Langfuse::Types::SpanAttributes do describe "#initialize" do it "accepts keyword arguments" do