Class: Raix::FunctionToolAdapter

Inherits:
Object
  • Object
show all
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

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