Class: Raif::ModelTool

Inherits:
Object
  • Object
show all
Includes:
Concerns::JsonSchemaDefinition
Defined in:
app/models/raif/model_tool.rb

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Concerns::JsonSchemaDefinition

#schema_for_instance

Class Method Details

.description_for_llm(source: nil) ⇒ Object

The description of the tool that will be provided to the model when giving it a list of available tools.

Parameters:

  • source (Object, nil) (defaults to: nil)

    Optional caller context forwarded into any source-aware schema or example blocks.



14
15
16
17
18
19
20
21
22
23
# File 'app/models/raif/model_tool.rb', line 14

def description_for_llm(source: nil)
  <<~DESCRIPTION
    Name: #{tool_name}
    Description: #{tool_description}
    Arguments Schema:
    #{JSON.pretty_generate(tool_arguments_schema_for_source(source))}
    Example Usage:
    #{JSON.pretty_generate(example_model_invocation_for_source(source))}
  DESCRIPTION
end

.example_model_invocation(source: nil, &block) ⇒ Object

Defines or retrieves the tool's example model invocation.

Definition:

- Arity-0 block: the block's return value is the example (evaluated lazily
on first read, then cached).
- Arity-1 block: receives the caller `source` on each read. Not cached,
so the example can reflect per-run context.

Parameters:

  • source (Object, nil) (defaults to: nil)

    Passed into arity-1 example blocks; ignored by arity-0 blocks.



51
52
53
54
55
56
57
58
59
60
61
62
63
64
# File 'app/models/raif/model_tool.rb', line 51

def example_model_invocation(source: nil, &block)
  if block_given?
    @example_model_invocation_block = block
    @example_model_invocation = nil
  elsif @example_model_invocation_block.present?
    if @example_model_invocation_block.arity == 1
      @example_model_invocation_block.call(source)
    else
      @example_model_invocation ||= @example_model_invocation_block.call
    end
  else
    raise NotImplementedError, "#{name}#example_model_invocation is not implemented"
  end
end

.example_model_invocation_for_source(source) ⇒ Object

Analogous backward-compatible entry point for example_model_invocation.



125
126
127
128
129
130
131
# File 'app/models/raif/model_tool.rb', line 125

def example_model_invocation_for_source(source)
  if method_accepts_source_kwarg?(:example_model_invocation)
    example_model_invocation(source: source)
  else
    example_model_invocation
  end
end

.invocation_partial_nameObject



70
71
72
# File 'app/models/raif/model_tool.rb', line 70

def invocation_partial_name
  name.gsub("Raif::ModelTools::", "").underscore
end

.invoke_tool(provider_tool_call_id:, tool_arguments:, source:) ⇒ Object



156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
# File 'app/models/raif/model_tool.rb', line 156

def invoke_tool(provider_tool_call_id:, tool_arguments:, source:)
  prepared_arguments = prepare_tool_arguments_for_source(tool_arguments, source)

  tool_invocation = Raif::ModelToolInvocation.new(
    provider_tool_call_id: provider_tool_call_id,
    source: source,
    tool_type: name,
    tool_arguments: prepared_arguments
  )

  ActiveRecord::Base.transaction do
    tool_invocation.save!
    process_invocation(tool_invocation)
    tool_invocation.completed!
  end

  tool_invocation
rescue StandardError => e
  tool_invocation.failed!
  raise e
end

.prepare_tool_arguments(arguments, source: nil) ⇒ Hash

Prepares tool arguments before validation and invocation. Override in subclasses to add tool-specific argument processing (e.g. type coercion, default injection). The base implementation strips keys not declared in the tool's argument schema, which handles LLMs that hallucinate extra parameters.

Parameters:

  • arguments (Hash)

    The raw tool arguments from the LLM response

  • source (Object, nil) (defaults to: nil)

    The caller invoking the tool (e.g. the agent). Forwarded into the schema lookup so source-aware schemas are evaluated against the same context the model saw when selecting the tool.

Returns:

  • (Hash)

    The prepared arguments ready for validation and processing



188
189
190
# File 'app/models/raif/model_tool.rb', line 188

def prepare_tool_arguments(arguments, source: nil)
  strip_unknown_tool_arguments(arguments, source: source)
end

.prepare_tool_arguments_for_source(arguments, source) ⇒ Object

Analogous backward-compatible entry point for prepare_tool_arguments. Lets subclass overrides with the pre-source-aware signature def self.prepare_tool_arguments(arguments) keep working.



136
137
138
139
140
141
142
# File 'app/models/raif/model_tool.rb', line 136

def prepare_tool_arguments_for_source(arguments, source)
  if method_accepts_source_kwarg?(:prepare_tool_arguments)
    prepare_tool_arguments(arguments, source: source)
  else
    prepare_tool_arguments(arguments)
  end
end

.process_invocation(invocation) ⇒ Object

Raises:

  • (NotImplementedError)


66
67
68
# File 'app/models/raif/model_tool.rb', line 66

def process_invocation(invocation)
  raise NotImplementedError, "#{name}#process_invocation is not implemented"
end

.provider_managed?Boolean

Returns:

  • (Boolean)


144
145
146
# File 'app/models/raif/model_tool.rb', line 144

def provider_managed?
  false
end

.renderable?Boolean

Returns:

  • (Boolean)


148
149
150
# File 'app/models/raif/model_tool.rb', line 148

def renderable?
  true
end

.tool_arguments_schema(dynamic: false, source: nil, &block) ⇒ Object

Defines or retrieves the tool's argument schema.

When defining:

- arity-0 block with `dynamic: false`: static schema, built once at class load.
- arity-0 block with `dynamic: true`: re-evaluated on every read.
- arity-1 block (any `dynamic:` value): source-aware. Re-evaluated on
every read and receives the caller `source` — typically the agent
invoking the tool. Use this to gate fields on per-run context without
reading global state. The `dynamic:` flag is implied for this form
because a source-dependent schema must re-evaluate on each read.

Parameters:

  • dynamic (Boolean) (defaults to: false)

    When true, the schema re-evaluates on each read. Automatically set to true for arity-1 blocks.

  • source (Object, nil) (defaults to: nil)

    Passed into arity-1 schema blocks; ignored for static / arity-0 schemas.



89
90
91
92
93
94
95
96
97
98
99
100
101
# File 'app/models/raif/model_tool.rb', line 89

def tool_arguments_schema(dynamic: false, source: nil, &block)
  if block_given?
    # Arity-1 blocks are inherently source-dependent and must be dynamic.
    # Auto-promote so callers don't have to think about the interaction.
    dynamic = true if block.arity == 1
    json_schema_definition(:tool_arguments, dynamic: dynamic, &block)
  elsif schema_defined?(:tool_arguments)
    schema_for(:tool_arguments, source: source)
  else
    raise NotImplementedError,
      "#{name} must define tool arguments schema via tool_arguments_schema or override #{name}.tool_arguments_schema"
  end
end

.tool_arguments_schema_for_source(source) ⇒ Hash

Backward-compatible entry point for rendering/validating a tool's schema against a caller source. Raif's internals go through this helper (LLM formatters, argument validation, argument stripping) rather than calling tool_arguments_schema(source: …) directly so that subclasses whose overrides predate the source: keyword keep working.

If the tool's tool_arguments_schema accepts source: (the base implementation, or an override that opted in), the helper forwards it. Otherwise the helper falls back to the no-arg form — the schema simply doesn't get per-call context, matching pre-source-aware behavior.

Parameters:

  • source (Object, nil)

    The caller (typically the agent).

Returns:

  • (Hash)

    The tool's JSON schema.



116
117
118
119
120
121
122
# File 'app/models/raif/model_tool.rb', line 116

def tool_arguments_schema_for_source(source)
  if method_accepts_source_kwarg?(:tool_arguments_schema)
    tool_arguments_schema(source: source)
  else
    tool_arguments_schema
  end
end

.tool_description(&block) ⇒ Object



31
32
33
34
35
36
37
38
39
# File 'app/models/raif/model_tool.rb', line 31

def tool_description(&block)
  if block_given?
    @tool_description = block.call
  elsif @tool_description.present?
    @tool_description
  else
    raise NotImplementedError, "#{name}#tool_description is not implemented"
  end
end

.tool_nameObject

The name of the tool as it will be provided to the model & used in the model invocation. Default for something like Raif::ModelTools::WikipediaSearch would be "wikipedia_search"



27
28
29
# File 'app/models/raif/model_tool.rb', line 27

def tool_name
  name.split("::").last.underscore
end

.triggers_observation_to_model?Boolean

Returns:

  • (Boolean)


152
153
154
# File 'app/models/raif/model_tool.rb', line 152

def triggers_observation_to_model?
  false
end

Instance Method Details

#tool_arguments_schemaObject

Instance method to get the tool arguments schema For instance-dependent schemas, builds the schema with this instance as context For class-level schemas, returns the class-level schema



237
238
239
# File 'app/models/raif/model_tool.rb', line 237

def tool_arguments_schema
  schema_for_instance(:tool_arguments)
end