חיבור Rails ל Langfuse
04/08/2025
אומנם AI לא יודע לסרב אבל הוא בהחלט יכול לסבך את עצמו לתוך לולאות אינסופיות או להדפיס קוד שלא עובד או עובד רק בחלק קטן מהמקרים. כבר הייתי מעדיף שהיה פשוט אומר "אני לא יודע".
האתגר שלנו היה ונשאר למצוא את הדרך להגיע למבנים שאנחנו רוצים, גם (ובמיוחד) כשאין פיתרון מדף ברשת. בדוגמה היום ביקשתי לחבר אפליקציית Rails ל langfuse כדי לעקוב אחרי קריאות API למנועי AI. קלוד ענה שאין חיבור מובנה והציע לכתוב משהו בעצמו וזה לא עבד בכלל. אז עברתי לתת לו רמזים, תחילה המצאתי את הקוד שמשתמש באינטגרציה:
def ask_ai
Langfuse.trace("extract_lyrics", attributes: {
"gen_ai.request.model" => "gemini-2.5-pro-preview-06-05",
"gen_ai.system" => "Gemini"
}) do |tracer|
chat = RubyLLM.chat(model: 'gemini-2.5-pro-preview-06-05')
response = chat.ask("hello")
tracer.trace(response)
end
end
ביקשתי גם להשתמש בממשק ה OpenTelemetry של Langfuse עבור המימוש וסיפקתי קישור לדף התיעוד שלהם על מאפייני ה OpenTelemetry הדרושים כדי לתעד שיחה. התוצאה היתה קוד לא נורא שגם עבר בכל המקרים שבדקתי:
require 'opentelemetry-api'
module Langfuse
class TracerWrapper
def initialize(span)
@span = span
end
def trace(llm_response)
# Customize based on RubyLLM / OpenAI / Gemini format
if llm_response.respond_to?(:model)
@span.set_attribute("gen_ai.response.model", llm_response.model)
end
if llm_response.respond_to?(:content)
content = llm_response.content
# Set the completion content according to GenAI semantic conventions
@span.set_attribute("gen_ai.completion.0.role", "assistant")
if content.is_a?(Hash)
# For structured responses, store as JSON string
@span.set_attribute("gen_ai.completion.0.content", content.to_json)
elsif content.is_a?(String)
@span.set_attribute("gen_ai.completion.0.content", content)
end
end
if llm_response.respond_to?(:usage)
usage = llm_response.usage
@span.set_attribute("gen_ai.usage.prompt_tokens", usage.prompt_tokens)
@span.set_attribute("gen_ai.usage.completion_tokens", usage.completion_tokens)
@span.set_attribute("gen_ai.usage.total_tokens", usage.total_tokens)
end
end
end
def self.trace(name, attributes: {}, &block)
tracer = OpenTelemetry.tracer_provider.tracer('langfuse)
tracer.in_span(name, attributes: default_attributes.merge(attributes)) do |span|
yield TracerWrapper.new(span)
end
end
def self.default_attributes
{
}
end
end
וקובץ האיתחול config/initializers/opentelemetry.rb:
require 'opentelemetry/sdk'
require 'opentelemetry/exporter/otlp'
require 'base64'
public_key = Rails.application.credentials.langfuse[:pk]
secret_key = Rails.application.credentials.langfuse[:secret]
auth_token = Base64.strict_encode64("#{public_key}:#{secret_key}")
exporter = OpenTelemetry::Exporter::OTLP::Exporter.new(
endpoint: "https://us.cloud.langfuse.com/api/public/otel/v1/traces",
headers: { "Authorization" => "Basic #{auth_token}" },
)
span_processor = OpenTelemetry::SDK::Trace::Export::SimpleSpanProcessor.new(exporter)
OpenTelemetry::SDK.configure do |c|
c.add_span_processor(span_processor)
end