Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions docs/SCORING.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,62 @@ 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"` and — by Langfuse
convention — the name `"output"`:

```ruby
client.create_score(
name: "output", # Langfuse convention for corrections
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. Corrections appear in the Langfuse UI alongside the original output
with a diff view, and can be read back through the v3 scores API
(`data_type: "CORRECTION"`).

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — the read-back reference now shows the actual API query (GET /api/public/v3/scores?dataType=CORRECTION) instead of the Ruby-style keyword.


### 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
Expand Down
6 changes: 3 additions & 3 deletions lib/langfuse.rb
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,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]
Expand Down Expand Up @@ -266,7 +266,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
#
Expand All @@ -292,7 +292,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
#
Expand Down
14 changes: 11 additions & 3 deletions lib/langfuse/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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)
Expand Down Expand Up @@ -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
#
Expand All @@ -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
#
Expand Down
12 changes: 8 additions & 4 deletions lib/langfuse/evaluation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion lib/langfuse/observations.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
73 changes: 50 additions & 23 deletions lib/langfuse/score_client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -77,6 +77,13 @@ 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)
Expand Down Expand Up @@ -109,7 +116,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
#
Expand Down Expand Up @@ -140,7 +147,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
#
Expand Down Expand Up @@ -242,37 +249,57 @@ def build_score_event(name:, value:, id:, trace_id:, session_id:, observation_id
end
# rubocop:enable Metrics/ParameterLists

# Documented Langfuse limit for TEXT score values.
TEXT_VALUE_LENGTH_RANGE = (1..500)
private_constant :TEXT_VALUE_LENGTH_RANGE

# 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)
when :numeric then normalize_numeric(value)
when :boolean then normalize_boolean(value)
when :categorical then require_string(value, "Categorical")
when :text then normalize_text(value)
# Corrections carry the full corrected output; Langfuse documents no
# length limit for them, so none is enforced here.
when :correction then require_string(value, "Correction")
else raise ArgumentError, "Invalid data_type: #{data_type}"
end
end

def normalize_numeric(value)
raise ArgumentError, "Numeric value must be Numeric, got #{value.class}" unless value.is_a?(Numeric)

value
end

def normalize_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

value
else
raise ArgumentError, "Invalid data_type: #{data_type}"
def normalize_text(value)
require_string(value, "Text")
unless TEXT_VALUE_LENGTH_RANGE.cover?(value.length)
raise ArgumentError, "Text value must contain 1 to 500 characters, got #{value.length}"
end

value
end

def require_string(value, label)
raise ArgumentError, "#{label} value must be a String, got #{value.class}" unless value.is_a?(String)

value
end
# rubocop:enable Metrics/CyclomaticComplexity

# Validate score name
#
Expand Down
16 changes: 15 additions & 1 deletion lib/langfuse/types.rb
Original file line number Diff line number Diff line change
Expand Up @@ -56,19 +56,33 @@ 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<Symbol, String>] 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
#
# @return [Array<Symbol>] 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<Symbol>] 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.
Expand Down
30 changes: 30 additions & 0 deletions spec/langfuse/client_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
10 changes: 10 additions & 0 deletions spec/langfuse/evaluation_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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) }
Expand Down
Loading
Loading