Files
chatwoot/lib/integrations/llm_instrumentation_helpers.rb
Aakash BakhleandGitHub eaffad12e7 feat(langfuse): propagate observation metadata for evals (#14634)
# Pull Request Template

## Description

We need to pass on trace level attributes down to the spans inside them
like tool calls, observations, etc.
This way, we can filter observations based on trace level attributes.


## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)

## How Has This Been Tested?

Please describe the tests that you ran to verify your changes. Provide
instructions so we can reproduce. Please also list any relevant details
for your test configuration.

Attributes added to observation metadata for easy filtering
<img width="1327" height="708" alt="image"
src="https://github.com/user-attachments/assets/8f1d1bf8-cde4-481d-a2c2-7920ad2fc52e"
/>

added a `generation_stage` to differentiate llm_calls that call tools vs
those that generate a `final_response`
<img width="1806" height="968" alt="CleanShot 2026-06-03 at 15 11 09@2x"
src="https://github.com/user-attachments/assets/db1fa8e0-7f2d-404b-a719-27a16d400442"
/>


propagated attributes to tool calls for future use
<img width="903" height="517" alt="image"
src="https://github.com/user-attachments/assets/edc61ce8-93db-465c-a66e-043138e2dc15"
/>



## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] Any dependent changes have been merged and published in downstream
modules
2026-06-03 16:45:19 +05:30

107 lines
3.5 KiB
Ruby

# frozen_string_literal: true
module Integrations::LlmInstrumentationHelpers
include Integrations::LlmInstrumentationConstants
include Integrations::LlmInstrumentationContext
include Integrations::LlmInstrumentationCompletionHelpers
def determine_provider(model_name)
return 'openai' if model_name.blank?
model = model_name.to_s.downcase
LlmConstants::PROVIDER_PREFIXES.each do |provider, prefixes|
return provider if prefixes.any? { |prefix| model.start_with?(prefix) }
end
'openai'
end
private
def setup_span_attributes(span, params)
set_request_attributes(span, params)
set_prompt_messages(span, params[:messages])
set_metadata_attributes(span, params)
end
def record_completion(span, result)
if result.respond_to?(:content)
span.set_attribute(ATTR_GEN_AI_COMPLETION_ROLE, result.role.to_s) if result.respond_to?(:role)
span.set_attribute(ATTR_GEN_AI_COMPLETION_CONTENT, result.content.to_s)
elsif result.is_a?(Hash)
set_completion_attributes(span, result)
end
end
def set_request_attributes(span, params)
provider = determine_provider(params[:model])
span.set_attribute(ATTR_GEN_AI_PROVIDER, provider)
span.set_attribute(ATTR_GEN_AI_REQUEST_MODEL, params[:model])
span.set_attribute(ATTR_GEN_AI_REQUEST_TEMPERATURE, params[:temperature]) if params[:temperature]
end
def set_prompt_messages(span, messages)
messages.each_with_index do |msg, idx|
role = msg[:role] || msg['role']
content = msg[:content] || msg['content']
span.set_attribute(format(ATTR_GEN_AI_PROMPT_ROLE, idx), role)
span.set_attribute(format(ATTR_GEN_AI_PROMPT_CONTENT, idx), content.to_s)
end
end
def set_metadata_attributes(span, params)
set_langfuse_attributes(span, current_langfuse_attributes.merge(propagated_langfuse_attributes(params)))
set_langfuse_attributes(span, current_observation_metadata_attributes.merge(propagated_observation_metadata_attributes(params)))
end
def propagated_langfuse_attributes(params)
attrs = {}
session_id = params[:conversation_id].present? ? "#{params[:account_id]}_#{params[:conversation_id]}" : nil
attrs[ATTR_LANGFUSE_USER_ID] = params[:account_id].to_s if params[:account_id]
attrs[ATTR_LANGFUSE_SESSION_ID] = session_id if session_id.present?
attrs[ATTR_LANGFUSE_TAGS] = [params[:feature_name].to_s] if params[:feature_name].present?
return attrs unless params[:metadata].is_a?(Hash)
params[:metadata].each do |key, value|
attrs[format(ATTR_LANGFUSE_METADATA, key)] = value.to_s
end
attrs
end
def propagated_observation_metadata_attributes(params)
attrs = {}
session_id = params[:conversation_id].present? ? "#{params[:account_id]}_#{params[:conversation_id]}" : nil
add_observation_metadata(attrs, 'user_id', params[:account_id])
add_observation_metadata(attrs, 'account_id', params[:account_id])
add_observation_metadata(attrs, 'session_id', session_id)
add_observation_metadata(attrs, 'trace_tags', [params[:feature_name]].to_json)
add_observation_metadata(attrs, 'feature_name', params[:feature_name])
return attrs unless params[:metadata].is_a?(Hash)
params[:metadata].each do |key, value|
add_observation_metadata(attrs, key, value)
end
attrs
end
def add_observation_metadata(attrs, key, value)
return if value.blank?
attrs[format(ATTR_LANGFUSE_OBSERVATION_METADATA, key)] = value.to_s
end
def set_langfuse_attributes(span, attrs)
attrs.each do |key, value|
span.set_attribute(key, value)
end
end
end