Class: Crystil::Wrappers::RubyLLM
- Inherits:
-
Object
- Object
- Crystil::Wrappers::RubyLLM
- Defined in:
- lib/crystil/wrappers/ruby_llm.rb,
sig/crystil/wrappers/ruby_llm.rbs
Overview
Wrapper for RubyLLM (ruby_llm gem) Supports any provider available via RubyLLM (Anthropic, OpenAI, Google, etc.) Returns raw response and raw formatted query to backend so that the existing extractors can be used.
Instance Method Summary collapse
-
#build_google_query(model_id, messages, tools) ⇒ Hash[Symbol, untyped]
Builds a Google-native query with contents/systemInstruction structure.
-
#build_messages_query(model_id, messages, tools) ⇒ Hash[Symbol, untyped]
Builds an OpenAI-compatible query (used for Anthropic and OpenAI providers).
-
#build_query(provider, model_id, messages, tools) ⇒ Hash[Symbol, untyped]
Dispatches to the appropriate provider-native query builder.
-
#initialize(config, collector, sentinel = nil) ⇒ RubyLLM
constructor
A new instance of RubyLLM.
- #register(client) ⇒ Object
- #validate_client!(client) ⇒ void
- #wrap_ask_method(client) ⇒ void
Constructor Details
#initialize(config, collector, sentinel = nil) ⇒ RubyLLM
Returns a new instance of RubyLLM.
12 13 14 15 16 |
# File 'lib/crystil/wrappers/ruby_llm.rb', line 12 def initialize(config, collector, sentinel = nil) @config = config @collector = collector @sentinel = sentinel end |
Instance Method Details
#build_google_query(model_id, messages, tools) ⇒ Hash[Symbol, untyped]
Builds a Google-native query with contents/systemInstruction structure.
143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 |
# File 'lib/crystil/wrappers/ruby_llm.rb', line 143 def build_google_query(model_id, , tools) system_msgs = .select { |m| m[:role] == "system" } content_msgs = .reject { |m| m[:role] == "system" } query = { model: model_id, contents: content_msgs.map do |m| google_role = m[:role] == "assistant" ? "model" : m[:role] { role: google_role, parts: [{ text: m[:content] }] } end } if system_msgs.any? system_text = system_msgs.map { |m| m[:content] }.join("\n") query[:systemInstruction] = { parts: [{ text: system_text }] } end unless tools.empty? query[:tools] = tools.values.map do |t| { name: t.name, description: t.description, parameters: t.params_schema } end end query end |
#build_messages_query(model_id, messages, tools) ⇒ Hash[Symbol, untyped]
Builds an OpenAI-compatible query (used for Anthropic and OpenAI providers).
132 133 134 135 136 137 138 139 140 |
# File 'lib/crystil/wrappers/ruby_llm.rb', line 132 def (model_id, , tools) query = { model: model_id, messages: } unless tools.empty? query[:tools] = tools.values.map do |t| { name: t.name, description: t.description, parameters: t.params_schema } end end query end |
#build_query(provider, model_id, messages, tools) ⇒ Hash[Symbol, untyped]
Dispatches to the appropriate provider-native query builder.
122 123 124 125 126 127 128 129 |
# File 'lib/crystil/wrappers/ruby_llm.rb', line 122 def build_query(provider, model_id, , tools) if provider == "gemini" build_google_query(model_id, , tools) else # Anthropic and OpenAI have the same query format (model_id, , tools) end end |
#register(client) ⇒ Object
18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
# File 'lib/crystil/wrappers/ruby_llm.rb', line 18 def register(client) validate_client!(client) # Prevent double registration return client if client.instance_variable_defined?(:@crystil_registered) # Store references in the client instance client.instance_variable_set(:@crystil_config, @config) client.instance_variable_set(:@crystil_collector, @collector) client.instance_variable_set(:@crystil_sentinel, @sentinel) client.instance_variable_set(:@crystil_registered, true) # Ask method handles both streaming and non-streaming cases unlike some others. wrap_ask_method(client) client end |
#validate_client!(client) ⇒ void
This method returns an undefined value.
38 39 40 41 42 43 |
# File 'lib/crystil/wrappers/ruby_llm.rb', line 38 def validate_client!(client) return if client.respond_to?(:ask) raise RegistrationError, "Client does not appear to be a valid RubyLLM client (missing ask method)" end |
#wrap_ask_method(client) ⇒ void
This method returns an undefined value.
45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 |
# File 'lib/crystil/wrappers/ruby_llm.rb', line 45 def wrap_ask_method(client) # Capture the wrapper instance in a local variable so it's accessible as a closure # inside the block, where self will no longer refer to it. wrapper = self client.singleton_class.class_eval do include Base alias_method :original_ask, :ask define_method(:ask) do |, with: nil, &block| start_time = Time.now query = {} # Fallback for error analytics if an exception occurs before build_query completes # Version reported is RubyLLM's, not the underlying provider's — RubyLLM is the # meta-wrapper Crystil sees; the native provider gem may not even be loaded. version = defined?(::RubyLLM::VERSION) ? ::RubyLLM::VERSION : nil sentinel = instance_variable_get(:@crystil_sentinel) provider = model.provider # Telemetry invariant: `conversation.client.title` is never nil. # `model.provider` should always return a non-empty string for # well-formed RubyLLM models, but fall back to the meta-wrapper # name (matches JS's pattern of "fallback to the SDK family # name") if it ever isn't. title = case provider when "gemini" then GOOGLE_CLIENT_TITLE when nil, "" then RUBY_LLM_PROVIDER else provider end # Capture existing messages (e.g. system instruction) before the call # adds the new user message to history. existing_msgs = .map { |m| { role: m.role.to_s, content: m.content.to_s } } all_msgs = existing_msgs + [{ role: "user", content: .to_s }] # Build a query in the provider's native format so the backend extractor can parse it. query = wrapper.send(:build_query, provider, model.id, all_msgs, tools) # Pass the actual provider title (e.g. "anthropic", "openai", "google") so the # sentinel backend can route to the correct classifier for this request format. sentinel&.raise_if_irrelevant!(title: title, request: query, version: version) response = original_ask(, with: with, &block) crystil_submit_analytics( method: :ask, args: [], kwargs: query, response: response.raw&.body, start_time: start_time, end_time: Time.now, title: title, provider: RUBY_LLM_PROVIDER, version: version ) response rescue CrystilRequestInterceptedError => e raise e rescue StandardError => e crystil_submit_error_analytics( method: :ask, args: [], kwargs: query, error: e, start_time: start_time, end_time: Time.now, title: title, provider: RUBY_LLM_PROVIDER, version: version ) raise e end end end |