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

.format_result_for_llm(invocation) ⇒ Object

The content of the tool-call-result message that gets sent back to the model on subsequent turns. Called lazily at message-build time on every turn — overrides can return live state (e.g. a follow-on record's status) rather than a static snapshot. The default is the invocation's raw result jsonb, which is what most tools want.

Parameters:

Returns:

  • (Object)

    anything serializable into the tool-call-result message (string, hash, etc.). Returning nil or "" falls back to the raw result.



161
162
163
# File 'app/models/raif/model_tool.rb', line 161

def format_result_for_llm(invocation)
  invocation.result
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



181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
# File 'app/models/raif/model_tool.rb', line 181

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



213
214
215
# File 'app/models/raif/model_tool.rb', line 213

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_immediate_llm_follow_up?(invocation) ⇒ Boolean

When true, Raif creates a synthetic follow-up entry after this invocation finalizes, prompting the model again immediately so it can react to the tool result in the same conversational turn. Use for tools whose result the model needs to keep reasoning over (search → read → act). Skip for tools whose result is interesting only on a later turn (e.g. queueing a suggestion the user will review later).

Instance-aware so a tool can decide based on what actually happened (e.g. "fire only on partial failure"). Default: false.

Parameters:

Returns:

  • (Boolean)


177
178
179
# File 'app/models/raif/model_tool.rb', line 177

def triggers_immediate_llm_follow_up?(invocation)
  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



262
263
264
# File 'app/models/raif/model_tool.rb', line 262

def tool_arguments_schema
  schema_for_instance(:tool_arguments)
end