Class: Insika::ToolDefinition
- Inherits:
-
Data
- Object
- Data
- Insika::ToolDefinition
- Defined in:
- lib/insika/tool_definition.rb,
lib/insika/tool_definition.rb
Overview
Definition of a DATA-DEFINED TOOL (no Ruby code): name, description, parameters and an HTTP call. Immutable value object, persisted by ToolStore and materialized at runtime by Tools::DataDefinedTool (one class, N instances — the same pattern as A2ARemote). Phase 5, Step A; parameters migrated to JSON Schema in Phase 7, Step A.
Persisted form (JSON-serializable Hash; ConfigStore stringifies the keys):
{ "name", "description",
"parameters" => <JSON Schema>, # { "type":"object", "properties":{…}, "required":[…] }
"request" => { "method","url","headers"=>{},"query"=>{},"body" },
"response" => { "extract","path" },
"secret_headers" => [ "Authorization", ... ],
"side_effect" => bool, "timeout" => int|nil,
"group" => string|nil, "tags" => [ "b2b", ... ] } # Phase 7/D4/F5 (Step C)
parameters is JSON Schema (the interlingua of OpenAI/Anthropic/MCP; Phase 7/D1):
a nestable object, fed straight into RubyLLM's params_schema (provider-
agnostic). The flat array ([{name,type,required}]) is SUGAR for the simple
case: it is lifted to JSON Schema at build time. The sugar covers scalars and
array:<scalar> — it CANNOT express an array of objects, and says so instead of
guessing an item type. Ingestion validates a safe subset of JSON Schema (R1):
it rejects composition (oneOf/anyOf/allOf/$ref/…) that not every provider supports.
Validation lives HERE (single source): build/from_h raise ValidationError
on malformed input. Name uniqueness and collision with a code tool are NOT
validated here (the value object does not know the registry) — that belongs to the
overlay (Step B). Secrets (credential headers) are the ToolStore's responsibility
(masks/reconciles); the definition itself is agnostic to masking.
Constant Summary collapse
- PARAM_TYPES =
Flat-sugar types: the SCALARS, plus
array:<scalar>for a list. There is no barearray: an array without an item type is an INCOMPLETE declaration, and the engine refuses to guess one (see lift_flat_params). %w[string number integer boolean].freeze
- ARRAY_SUGAR_RE =
/\Aarray:(string|number|integer|boolean)\z/- ARRAY_SUGAR =
PARAM_TYPES.map { |t| "array:#{t}" }.freeze
- HTTP_METHODS =
%w[GET HEAD POST PUT PATCH DELETE].freeze
- IDEMPOTENT =
side_effect default = false
%w[GET HEAD].freeze
- EXTRACTS =
%w[body_raw status json_path].freeze
- NAME_RE =
identifier for the model
/\A[a-z][a-z0-9_]*\z/- PLACEHOLDER_RE =
A
.in the placeholder enables the turn-context namespace{{ctx.*}}(Phase 6/D2), separate from the model's{{param}}. Params follow NAME_RE (no dot) -> a placeholder with a dot can only be a ctx ref. /\{\{\s*([a-zA-Z0-9_.]+)\s*\}\}/- CTX_PREFIX =
Turn-context namespace: values coming from the TURN (not the model), resolved by DataDefinedTool. Closed allowlist (a typo becomes a validation error, not a silently empty header).
"ctx."- CTX_FIELDS =
%w[chat_id store_id agent_id tenant].freeze
- SCHEMA_TYPES =
---- safe subset of JSON Schema (R1) -------------------------------------- Types supported by EVERY provider (OpenAI/Anthropic/Gemini/DeepSeek/Bedrock).
%w[object array string number integer boolean].freeze
- FORBIDDEN_KEYWORDS =
Composition/ref constructs that are NOT universally supported -> a clear error at ingestion time (instead of an opaque failure in the provider).
%w[ oneOf anyOf allOf not $ref if then else patternProperties dependencies dependentSchemas propertyNames unevaluatedProperties $defs definitions ].freeze
- SAY_KEY =
HOW
sayREACHES THE EXECUTOR. RubyLLM'sTool::Haltcarries one value, and that value is the tool's payload (the trace records it, and the model never sees it — the halt ends the loop). So a halt that has something to publish carries BOTH, under keys distinctive enough that a trace reader knows what they are on sight. Unwrapped tools are untouched: nosay, no wrapper. "__insika_halt_say"- PAYLOAD_KEY =
"__insika_halt_payload"- PATH_MISS =
Walks a dotted path. Returns PATH_MISS (not nil) when a segment is absent, so a key whose stored value IS nil stays distinguishable from a missing key.
Object.new.freeze
Instance Attribute Summary collapse
-
#description ⇒ Object
readonly
Returns the value of attribute description.
-
#group ⇒ Object
readonly
Returns the value of attribute group.
-
#halt_when ⇒ Object
readonly
Returns the value of attribute halt_when.
-
#name ⇒ Object
readonly
Returns the value of attribute name.
-
#parameters ⇒ Object
readonly
Returns the value of attribute parameters.
-
#request ⇒ Object
readonly
Returns the value of attribute request.
-
#response ⇒ Object
readonly
Returns the value of attribute response.
-
#secret_headers ⇒ Object
readonly
Returns the value of attribute secret_headers.
-
#side_effect ⇒ Object
readonly
Returns the value of attribute side_effect.
-
#tags ⇒ Object
readonly
Returns the value of attribute tags.
-
#timeout ⇒ Object
readonly
Returns the value of attribute timeout.
Class Method Summary collapse
-
.build(name:, description:, request:, parameters: nil, response: nil, secret_headers: nil, side_effect: nil, timeout: nil, group: nil, tags: nil, halt_when: nil) ⇒ Object
Builds + validates.
-
.from_h(hash) ⇒ Object
Raw Hash (string or symbol keys, from the store/payload) -> ToolDefinition.
-
.halt_say_of(content) ⇒ Object
-> the text to publish, or nil when this halt carries none.
- .wrap_halt(payload, say) ⇒ Object
Instance Method Summary collapse
-
#halt?(body) ⇒ Boolean
-> true when this response ENDS the turn (no further model call).
-
#halt_say(body) ⇒ Object
WHAT THE CUSTOMER GETS WHEN THE MODEL SAID NOTHING FIRST (
halt_when.say). -
#required_params ⇒ Object
Names of the required top-level parameters (DataDefinedTool validates presence before the call).
-
#to_h ⇒ Object
String-keyed Hash for persistence (ConfigStore stringifies again, but we normalize here so the record is stable across backends).
-
#top_level_params ⇒ Object
FLAT view of the top-level properties (name/type/description/required) — for RubyLLM's
#parameters(discovery/tool_search) and the simple authoring UI.
Instance Attribute Details
#description ⇒ Object (readonly)
Returns the value of attribute description
35 36 37 |
# File 'lib/insika/tool_definition.rb', line 35 def description @description end |
#group ⇒ Object (readonly)
Returns the value of attribute group
35 36 37 |
# File 'lib/insika/tool_definition.rb', line 35 def group @group end |
#halt_when ⇒ Object (readonly)
Returns the value of attribute halt_when
35 36 37 |
# File 'lib/insika/tool_definition.rb', line 35 def halt_when @halt_when end |
#name ⇒ Object (readonly)
Returns the value of attribute name
35 36 37 |
# File 'lib/insika/tool_definition.rb', line 35 def name @name end |
#parameters ⇒ Object (readonly)
Returns the value of attribute parameters
35 36 37 |
# File 'lib/insika/tool_definition.rb', line 35 def parameters @parameters end |
#request ⇒ Object (readonly)
Returns the value of attribute request
35 36 37 |
# File 'lib/insika/tool_definition.rb', line 35 def request @request end |
#response ⇒ Object (readonly)
Returns the value of attribute response
35 36 37 |
# File 'lib/insika/tool_definition.rb', line 35 def response @response end |
#secret_headers ⇒ Object (readonly)
Returns the value of attribute secret_headers
35 36 37 |
# File 'lib/insika/tool_definition.rb', line 35 def secret_headers @secret_headers end |
#side_effect ⇒ Object (readonly)
Returns the value of attribute side_effect
35 36 37 |
# File 'lib/insika/tool_definition.rb', line 35 def side_effect @side_effect end |
#tags ⇒ Object (readonly)
Returns the value of attribute tags
35 36 37 |
# File 'lib/insika/tool_definition.rb', line 35 def @tags end |
#timeout ⇒ Object (readonly)
Returns the value of attribute timeout
35 36 37 |
# File 'lib/insika/tool_definition.rb', line 35 def timeout @timeout end |
Class Method Details
.build(name:, description:, request:, parameters: nil, response: nil, secret_headers: nil, side_effect: nil, timeout: nil, group: nil, tags: nil, halt_when: nil) ⇒ Object
Builds + validates. Raises Insika::ValidationError. Accepts keyword args
(already-normalized symbol keys); use from_h for a raw Hash from the store/UI.
parameters accepts JSON Schema (Hash) OR the legacy flat array.
75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 |
# File 'lib/insika/tool_definition.rb', line 75 def self.build(name:, description:, request:, parameters: nil, response: nil, secret_headers: nil, side_effect: nil, timeout: nil, group: nil, tags: nil, halt_when: nil) name = name.to_s raise Insika::ValidationError, "name must match #{NAME_RE.inspect}" unless NAME_RE.match?(name) desc = description.to_s raise Insika::ValidationError, "description is required" if desc.empty? schema = normalize_params(parameters) req = normalize_request(request, top_level_names(schema)) resp = normalize_response(response) method = req[:method] effect = side_effect.nil? ? !IDEMPOTENT.include?(method) : (side_effect ? true : false) new( name: name, description: desc, parameters: schema, request: req, response: resp, secret_headers: Array(secret_headers).map(&:to_s), side_effect: effect, timeout: timeout.nil? ? nil : Integer(timeout), group: normalize_group(group), tags: (), halt_when: normalize_halt_when(halt_when) ) end |
.from_h(hash) ⇒ Object
Raw Hash (string or symbol keys, from the store/payload) -> ToolDefinition.
101 102 103 104 105 106 107 108 109 |
# File 'lib/insika/tool_definition.rb', line 101 def self.from_h(hash) h = deep_symbolize(hash) build( name: h[:name], description: h[:description], parameters: h[:parameters], request: h[:request] || {}, response: h[:response], secret_headers: h[:secret_headers], side_effect: h[:side_effect], timeout: h[:timeout], group: h[:group], tags: h[:tags], halt_when: h[:halt_when] ) end |
.halt_say_of(content) ⇒ Object
-> the text to publish, or nil when this halt carries none.
483 484 485 |
# File 'lib/insika/tool_definition.rb', line 483 def self.halt_say_of(content) content.is_a?(Hash) ? Coercion.presence(content[SAY_KEY]) : nil end |
.wrap_halt(payload, say) ⇒ Object
480 |
# File 'lib/insika/tool_definition.rb', line 480 def self.wrap_halt(payload, say) = { SAY_KEY => say, PAYLOAD_KEY => payload } |
Instance Method Details
#halt?(body) ⇒ Boolean
-> true when this response ENDS the turn (no further model call). body is the
raw response body; a parse failure or a missing path means "does not halt" —
never end a turn on a guess.
423 424 425 426 427 428 429 430 431 432 433 |
# File 'lib/insika/tool_definition.rb', line 423 def halt?(body) return false if halt_when.nil? parsed = JSON.parse(body.to_s) value = dig_path(parsed, halt_when[:json_path]) return false if value == PATH_MISS halt_when[:equals].include?(value.to_s) rescue JSON::ParserError false end |
#halt_say(body) ⇒ Object
WHAT THE CUSTOMER GETS WHEN THE MODEL SAID NOTHING FIRST (halt_when.say).
A halt is worth the model's lead-in ("vou te conectar agora") and nothing after — but the model does not always write one, and then the turn published an EMPTY answer. Measured on a real store: two escalation turns in a row delivered silence, where the same agent without the halt at least said "o time de suporte já está cuidando do seu caso".
The value cannot be guessed. json_path + equals cannot supply it either:
the matched value is by definition one of the equals tokens, so publishing
it would ship "SUBSCRIBED" to a person as often as it ships a sentence. So the
operator names it, in one of two shapes:
"say" => { "json_path" => "tool_result" } # the sentence the backend returned
"say" => { "text" => "CALL_SUPPORT" } # a literal the CHANNEL resolves
The literal form is the one that replaces the usual workaround — forcing the prompt to emit a control token and parsing it downstream. The token then comes from the tool's own contract, deterministically, instead of depending on the model complying with an instruction.
-> String | nil. nil means "publish nothing", which is the pre-existing
behaviour and stays the default for every tool that declares no say.
458 459 460 461 462 463 464 465 466 467 468 469 470 |
# File 'lib/insika/tool_definition.rb', line 458 def halt_say(body) say = halt_when && halt_when[:say] return nil if say.nil? return Coercion.presence(say[:text]) if say[:text] parsed = JSON.parse(body.to_s) value = dig_path(parsed, say[:json_path]) # Only a String is publishable: a hash or a number reaching a customer as the # answer is never what someone meant. value.is_a?(String) ? Coercion.presence(value) : nil rescue JSON::ParserError nil end |
#required_params ⇒ Object
Names of the required top-level parameters (DataDefinedTool validates presence
before the call). Derived from the JSON Schema's required.
502 |
# File 'lib/insika/tool_definition.rb', line 502 def required_params = Array(parameters["required"]).map(&:to_s) |
#to_h ⇒ Object
String-keyed Hash for persistence (ConfigStore stringifies again, but we normalize here so the record is stable across backends).
407 408 409 410 411 412 413 414 415 416 417 418 |
# File 'lib/insika/tool_definition.rb', line 407 def to_h { "name" => name, "description" => description, "parameters" => parameters, "request" => request.transform_keys(&:to_s), "response" => response.transform_keys(&:to_s), "secret_headers" => secret_headers, "side_effect" => side_effect, "timeout" => timeout, "group" => group, "tags" => , "halt_when" => halt_when&.transform_keys(&:to_s) } end |
#top_level_params ⇒ Object
FLAT view of the top-level properties (name/type/description/required) — for
RubyLLM's #parameters (discovery/tool_search) and the simple authoring UI.
The full nested schema goes through params_schema (DataDefinedTool). Symbol-
keyed for compat with callers that already consumed the flat params.
508 509 510 511 512 513 514 515 516 |
# File 'lib/insika/tool_definition.rb', line 508 def top_level_params props = parameters["properties"] || {} required = required_params props.map do |pname, pschema| pschema ||= {} { name: pname.to_s, type: (pschema["type"] || "string").to_s, description: pschema["description"].to_s, required: required.include?(pname.to_s) } end end |