Class: Raix::FunctionToolAdapter
- Inherits:
-
Object
- Object
- Raix::FunctionToolAdapter
- Defined in:
- lib/raix/function_tool_adapter.rb
Overview
Adapter to convert Raix function declarations to RubyLLM::Tool instances
Defined Under Namespace
Classes: StopToolCallsRequested, ToolCallsCapReached
Class Method Summary collapse
- .convert_tools_for_ruby_llm(raix_instance) ⇒ Object
- .create_tool_from_function(function_def, instance) ⇒ Object
Class Method Details
.convert_tools_for_ruby_llm(raix_instance) ⇒ Object
80 81 82 83 84 85 86 87 |
# File 'lib/raix/function_tool_adapter.rb', line 80 def self.convert_tools_for_ruby_llm(raix_instance) return [] unless raix_instance.class.respond_to?(:functions) return [] if raix_instance.class.functions.blank? raix_instance.class.functions.map do |function_def| create_tool_from_function(function_def, raix_instance) end end |
.create_tool_from_function(function_def, instance) ⇒ Object
12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 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 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 |
# File 'lib/raix/function_tool_adapter.rb', line 12 def self.create_tool_from_function(function_def, instance) tool_class = Class.new(RubyLLM::Tool) do description function_def[:description] if function_def[:description] # Forward the full JSON-schema parameter dict to RubyLLM rather than # rebuilding it field-by-field via `param(...)`. The per-field path # only carried `type` and `description`, which silently dropped richer # schema like `additionalProperties`, `items`, `enum`, or nested # `properties` — leaving providers (notably Gemini's structured output) # to invent degenerate shapes for `type: object` arguments. if function_def[:parameters].is_a?(Hash) && function_def[:parameters][:properties].present? # RubyLLM's `params(schema)` path forwards the schema verbatim and, unlike the # per-field `param(...)` path, does not inject the OpenAI strict-mode guards. Default # them on so existing tools keep strict behavior, while letting a function declaration # override either by setting it explicitly. params({ additionalProperties: false, strict: true }.merge(function_def[:parameters])) end # Store reference to the instance and function name define_method(:raix_instance) { instance } define_method(:raix_function_name) { function_def[:name] } # Override execute to call the Raix function. # # The max_tool_calls budget is enforced here, inside the generated # wrapper, because RubyLLM drives the tool loop internally # (complete -> handle_tool_calls -> complete) and never yields control # back to Raix between rounds. Counting must therefore happen per tool # invocation: a single model response can pack several parallel tool # calls, all of which RubyLLM executes in one round even after one has # halted, so each wrapper checks the shared counter independently. define_method(:execute) do |**args| if raix_instance.increment_tool_call_count > raix_instance.max_tool_calls # Refuse to run the underlying function and halt the loop. RubyLLM # returns this Halt from chat.complete; ChatCompletion turns it into # a final, tool-less completion. halt(ToolCallsCapReached.new(raix_instance.max_tool_calls)) else result = raix_instance.public_send(raix_function_name, args.with_indifferent_access, nil) # stop_tool_calls_and_respond! sets a flag that only the old (now # unreachable) counting loop used to read. Honor it here so it still # forces a final text response under the RubyLLM backend. if raix_instance.stop_tool_calls_and_respond halt(StopToolCallsRequested.new(result)) else result end end end end # Set a meaningful name for the tool class tool_class.define_singleton_method(:name) do "Raix::GeneratedTool::#{function_def[:name].to_s.camelize}" end tool_instance = tool_class.new # Override the name method to return the original function name # This ensures RubyLLM can match the tool call from the AI tool_instance.define_singleton_method(:name) do function_def[:name].to_s end tool_instance end |