Class: Insika::Tools::DataDefinedTool

Inherits:
RubyLLM::Tool
  • Object
show all
Defined in:
lib/insika/tools/data_defined_tool.rb

Overview

DATA-DEFINED tool: one class, N instances parameterized by a ToolDefinition (the same pattern as A2ARemote). It makes an HTTP call described in config — no Ruby code per tool. Since it inherits RubyLLM::Tool (pulls in the gem), it is NOT required in lib/insika.rb; the overlay loads it lazily at registration

Contract preserved by duck-typing: it overrides name/description/parameters/ execute; RubyLLM's params_schema derives from #parameters automatically. execute NEVER raises — an error (missing param, blocked egress, HTTP, parse) becomes { error: } to the model, like the other tools.

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(definition:, http:, egress: Insika::EgressGuard, egress_options: {}, event_stream: nil, turn_context: {}) ⇒ DataDefinedTool

Returns a new instance of DataDefinedTool.



19
20
21
22
23
24
25
26
27
28
# File 'lib/insika/tools/data_defined_tool.rb', line 19

def initialize(definition:, http:, egress: Insika::EgressGuard, egress_options: {},
               event_stream: nil, turn_context: {})
  @definition = definition
  @http = http
  @egress = egress
  @egress_options = egress_options
  @event_stream = event_stream
  @turn_context = symbolize_ctx(turn_context)
  super()
end

Instance Attribute Details

#turn_contextObject

Turn context: the registry tool does NOT receive TurnState, so the Executor DEPOSITS the turn ids here, per-turn (chat/agent/tenant/store). They resolve Insika::Tools::DataDefinedTool.{ctx{ctx.*} — SEPARATE from the model's {param} — to emit X-Chat-Id/X-Store-Id/X-Agent-Id. They come from the TURN, never from the model (R2). Reader for testing; the writer is the Executor's injection point.



35
36
37
# File 'lib/insika/tools/data_defined_tool.rb', line 35

def turn_context
  @turn_context
end

Instance Method Details

#descriptionObject



44
# File 'lib/insika/tools/data_defined_tool.rb', line 44

def description = @definition.description

#execute(**kwargs) ⇒ Object

The args are checked against the tool's own JSON Schema BEFORE the request is built: a call the schema does not allow becomes { error: } the model can act on, instead of a wrongly-shaped request that a backend answers 200 to.



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
# File 'lib/insika/tools/data_defined_tool.rb', line 64

def execute(**kwargs)
  if (bad = Insika::SchemaGuard.violation(@definition.parameters, kwargs))
    return { error: bad }
  end

  req = build_request(kwargs)
  reason = @egress.violation(req[:url], **@egress_options)
  return { error: "destination blocked: #{reason}" } if reason

  result = @http.request(**req)
  emit(result[:status])
  payload = extract(result)
  # The RESPONSE says the turn is over (`halt_when`): the backend already
  # answered the customer, so letting the model comment would deliver the
  # message twice. RubyLLM's Tool::Halt ends its loop right here — no second
  # provider call, and the decision is the engine's, not a request in a prompt.
  # Only on a 2xx: an error body that happens to carry the value is a failure,
  # and a failure must reach the model.
  if http_ok?(result) && @definition.halt?(result[:body])
    # `say` (optional) travels WITH the halt so the Executor can publish it when
    # the model wrote no lead-in. Wrapped only when there is one, so every tool
    # that declares no `say` keeps producing exactly the payload it always did.
    say = @definition.halt_say(result[:body])
    return RubyLLM::Tool::Halt.new(say ? Insika::ToolDefinition.wrap_halt(payload, say) : payload)
  end

  payload
rescue StandardError => e
  { error: "HTTP call failed: #{e.message}" }
end

#nameObject

name/description/parameters per INSTANCE (otherwise the model would see the name derived from the class for every data-tool).



43
# File 'lib/insika/tools/data_defined_tool.rb', line 43

def name = @definition.name

#parametersObject

FLAT top-level view for discovery (tool_search calls #parameters on the resolved tool). The real nested schema goes through #params_schema above.



54
55
56
57
58
59
# File 'lib/insika/tools/data_defined_tool.rb', line 54

def parameters
  @parameters ||= @definition.top_level_params.each_with_object({}) do |p, acc|
    sym = p[:name].to_sym
    acc[sym] = RubyLLM::Parameter.new(sym, type: p[:type], desc: p[:description], required: p[:required])
  end
end

#params_schemaObject

FULL (nested) JSON Schema straight into RubyLLM's params_schema — it is what the providers serialize (OpenAI/Anthropic/Gemini/Bedrock prefer params_schema; parameters is just a fallback). Provider-agnostic and the only form that expresses nesting (object/array/enum).,.



50
# File 'lib/insika/tools/data_defined_tool.rb', line 50

def params_schema = @definition.parameters