Class: Insika::Tools::DataDefinedTool
- Inherits:
-
RubyLLM::Tool
- Object
- RubyLLM::Tool
- Insika::Tools::DataDefinedTool
- 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
-
#turn_context ⇒ Object
Turn context: the registry tool does NOT receive TurnState, so the Executor DEPOSITS the turn ids here, per-turn (chat/agent/tenant/store).
Instance Method Summary collapse
- #description ⇒ Object
-
#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. -
#initialize(definition:, http:, egress: Insika::EgressGuard, egress_options: {}, event_stream: nil, turn_context: {}) ⇒ DataDefinedTool
constructor
A new instance of DataDefinedTool.
-
#name ⇒ Object
name/description/parameters per INSTANCE (otherwise the model would see the name derived from the class for every data-tool).
-
#parameters ⇒ Object
FLAT top-level view for discovery (tool_search calls #parameters on the resolved tool).
-
#params_schema ⇒ Object
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).
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 = @event_stream = event_stream @turn_context = symbolize_ctx(turn_context) super() end |
Instance Attribute Details
#turn_context ⇒ Object
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
#description ⇒ Object
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.}" } end |
#name ⇒ Object
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 |
#parameters ⇒ Object
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_schema ⇒ Object
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 |