Module: PlatformSdk::Observability::Langfuse::Recorder
- Defined in:
- lib/platform_sdk/observability/langfuse/recorder.rb
Constant Summary collapse
- MAX_ATTRIBUTE_BYTES =
256KB
262_144- TRUNCATION_SUFFIX =
"…[truncated]"
Class Method Summary collapse
-
.build_attributes(model, input, output, prompt_name, prompt_version, usage, provider = nil) ⇒ Object
rubocop:disable Metrics/ParameterLists.
- .coerce_integer(value) ⇒ Object
-
.coerce_simple(value) ⇒ Object
Pass numerics/booleans through; coerce everything else to plain String.
-
.coerce_string(value) ⇒ Object
rubocop:enable Metrics/ParameterLists.
-
.current_span_active? ⇒ Boolean
rubocop:enable Metrics/ParameterLists.
-
.record_generation(name:, model:, input:, output:, prompt_name: nil, prompt_version: nil, usage: nil, provider: nil) ⇒ Object
rubocop:disable Metrics/ParameterLists.
- .stringify(value) ⇒ Object
- .truncate(str) ⇒ Object
Class Method Details
.build_attributes(model, input, output, prompt_name, prompt_version, usage, provider = nil) ⇒ Object
rubocop:disable Metrics/ParameterLists
29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
# File 'lib/platform_sdk/observability/langfuse/recorder.rb', line 29 def self.build_attributes(model, input, output, prompt_name, prompt_version, usage, provider = nil) attrs = { 'langfuse.observation.type' => 'generation', # `gen_ai.operation.name` is a required attribute in the OTel # GenAI semantic conventions. We only emit chat-style # completions today, so this is a constant. 'gen_ai.operation.name' => 'chat', 'gen_ai.request.model' => coerce_string(model), 'gen_ai.prompt' => stringify(input), 'gen_ai.completion' => stringify(output) } attrs['gen_ai.provider.name'] = coerce_string(provider) if provider if prompt_name && prompt_version attrs['langfuse.observation.prompt.name'] = coerce_string(prompt_name) attrs['langfuse.observation.prompt.version'] = coerce_simple(prompt_version) end if usage.is_a?(Hash) usage = usage.transform_keys(&:to_sym) input_tokens = coerce_integer(usage[:input_tokens]) output_tokens = coerce_integer(usage[:output_tokens]) attrs['gen_ai.usage.input_tokens'] = input_tokens unless input_tokens.nil? attrs['gen_ai.usage.output_tokens'] = output_tokens unless output_tokens.nil? end attrs.compact end |
.coerce_integer(value) ⇒ Object
69 70 71 72 73 74 75 |
# File 'lib/platform_sdk/observability/langfuse/recorder.rb', line 69 def self.coerce_integer(value) return nil if value.nil? Integer(value) rescue ArgumentError, TypeError nil end |
.coerce_simple(value) ⇒ Object
Pass numerics/booleans through; coerce everything else to plain String.
61 62 63 64 65 66 67 |
# File 'lib/platform_sdk/observability/langfuse/recorder.rb', line 61 def self.coerce_simple(value) return nil if value.nil? return value if value.instance_of?(Integer) || value.instance_of?(Float) return value if value.instance_of?(TrueClass) || value.instance_of?(FalseClass) String.new(value.to_s) end |
.coerce_string(value) ⇒ Object
rubocop:enable Metrics/ParameterLists
56 57 58 |
# File 'lib/platform_sdk/observability/langfuse/recorder.rb', line 56 def self.coerce_string(value) Coercions.coerce_string(value) end |
.current_span_active? ⇒ Boolean
rubocop:enable Metrics/ParameterLists
24 25 26 |
# File 'lib/platform_sdk/observability/langfuse/recorder.rb', line 24 def self.current_span_active? OpenTelemetry::Trace.current_span.context.valid? end |
.record_generation(name:, model:, input:, output:, prompt_name: nil, prompt_version: nil, usage: nil, provider: nil) ⇒ Object
rubocop:disable Metrics/ParameterLists
13 14 15 16 17 18 19 20 21 |
# File 'lib/platform_sdk/observability/langfuse/recorder.rb', line 13 def self.record_generation(name:, model:, input:, output:, prompt_name: nil, prompt_version: nil, usage: nil, provider: nil) return unless Langfuse.enabled? return unless current_span_active? tracer = Langfuse.tracer tracer.in_span(name, attributes: build_attributes(model, input, output, prompt_name, prompt_version, usage, provider)) do # span body intentionally empty — attributes carry the data end end |
.stringify(value) ⇒ Object
77 78 79 80 81 82 83 84 85 |
# File 'lib/platform_sdk/observability/langfuse/recorder.rb', line 77 def self.stringify(value) str = value.is_a?(String) ? value : value.to_json truncated = truncate(str) # Force a plain String — value.to_json could be a String subclass # in some odd cases, and value.is_a?(String) lets subclasses through. String.new(truncated) rescue StandardError String.new(truncate(value.to_s)) end |
.truncate(str) ⇒ Object
87 88 89 90 91 92 93 |
# File 'lib/platform_sdk/observability/langfuse/recorder.rb', line 87 def self.truncate(str) return str if str.bytesize <= MAX_ATTRIBUTE_BYTES # Slice on byte boundary then strip any partial multibyte sequence by encoding-coercing. truncated = str.byteslice(0, MAX_ATTRIBUTE_BYTES).scrub('') "#{truncated}#{TRUNCATION_SUFFIX}" end |