חיבור 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