Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
59 changes: 59 additions & 0 deletions docs/SCORING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 4 additions & 3 deletions lib/langfuse.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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
#
Expand All @@ -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
#
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
52 changes: 18 additions & 34 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,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)

Expand All @@ -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
#
Expand Down Expand Up @@ -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
#
Expand Down Expand Up @@ -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:,
Expand Down Expand Up @@ -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
#
Expand Down
58 changes: 58 additions & 0 deletions lib/langfuse/score_value.rb
Original file line number Diff line number Diff line change
@@ -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
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
Loading
Loading