Class: Pikuri::Tool
- Inherits:
-
Object
- Object
- Pikuri::Tool
- Defined in:
- lib/pikuri/tool.rb,
lib/pikuri/tool/fetch.rb,
lib/pikuri/tool/scraper.rb,
lib/pikuri/tool/calculator.rb,
lib/pikuri/tool/parameters.rb,
lib/pikuri/tool/search/exa.rb,
lib/pikuri/tool/web_scrape.rb,
lib/pikuri/tool/web_search.rb,
lib/pikuri/tool/search/brave.rb,
lib/pikuri/tool/search/result.rb,
lib/pikuri/tool/trifecta_legs.rb,
lib/pikuri/tool/search/engines.rb,
lib/pikuri/tool/execute_context.rb,
lib/pikuri/tool/search/duckduckgo.rb,
lib/pikuri/tool/search/rate_limiter.rb
Overview
Loaded after Tool itself is defined; the class Tool reopening below
assumes that order.
Defined Under Namespace
Modules: Calculator, Fetch, Scraper, Search, WebScrape, WebSearch Classes: ExecuteContext, Parameters, TrifectaLegs
Constant Summary collapse
- FETCH =
Verbatim URL download tool — a thin wrapper over Pikuri::Tool::Fetch.fetch in OpenAI tool-call shape. Use for raw textual payloads (JSON, CSV,
robots.txt, source files); use WEB_SCRAPE for rendered pages.Sharing:
P_shared_benign— no per-agent state, but Pikuri::Tool::Fetch::CACHE is process-wide (and on disk, so cross-process too): agent B'sfetchcan be answered from agent A's download within the TTL. See UrlCache for the race and why it's harmless. new( name: 'fetch', description: <<~DESC, Downloads the given URL and returns its body verbatim. Usage: - Use for raw textual payloads: JSON APIs, CSV files, robots.txt, sitemaps, source files — anywhere a rendering pass would corrupt the data. - For rendered HTML pages, use web_scrape — it extracts readable content; fetch returns the raw HTML bytes unchanged. - Accepts text/* and common textual application/* types (JSON, XML, JS, XHTML, RSS, Atom). Refuses PDFs, images, and other binaries. DESC parameters: Parameters.build { |p| p.required_string :url, 'Absolute URL to download, including the scheme, ' \ 'e.g. "https://example.com/data.json".' p.optional_integer :max_chars, 'Maximum number of characters of the body to ' \ 'return. Defaults to 5000; hard-capped at ' \ '100000. When the body is longer than this, ' \ 'output is cut and a marker reports the full ' \ 'length.' }, execute: ->(url:, max_chars: Fetch::DEFAULT_MAX_CHARS) { Fetch.fetch(url, max_chars: max_chars) }, # As {WEB_SCRAPE}: attacker-choosable host, attacker-authored response. trifecta_legs: Pikuri::Tool::TrifectaLegs::ASSUMED )
- CALCULATOR =
Arithmetic-evaluation tool backed by Pikuri::Tool::Calculator.calculate. Accepts Python expression syntax (+, -, *, /, //, %, **, unary minus, parentheses, decimals) so the model emits syntax it already knows.
Stays wired even in shells that have
bash: it is the only unconfirmed float arithmetic there, since a shell's own$((…))is integer-only and every interpreter that isn't must stay off the passive allowlist. SeeDECISIONS.mdD_keep_calculator.Sharing:
P_stateless— arithmetic over the argument, no I/O, no state. Hand this one constant to every agent in the VM. new( name: 'calculator', description: <<~DESC, Evaluates a basic arithmetic expression and returns the numeric result. Usage: - Use this for any arithmetic beyond simple mental math — do not eyeball multi-digit work. - Decimal results are rounded to 3 places; integer results are exact. DESC parameters: Parameters.build { |p| p.required_string :expression, 'Arithmetic expression in Python syntax: + - * ' \ '/ (true division), // (floor division), % (modulo), ' \ '** (exponentiation), unary minus, parentheses, ' \ 'decimals. E.g. "155 / (58 * 1000.0 / 3600)" or "2**10".' }, execute: ->(expression:) { Calculator.calculate(expression) }, # No legs: pure arithmetic over the argument, no I/O of any kind. trifecta_legs: Pikuri::Tool::TrifectaLegs::NONE )
- WEB_SCRAPE =
Webpage download + Markdown conversion tool — a thin wrapper over Pikuri::Tool::WebScrape.visit in OpenAI tool-call shape.
Sharing:
P_shared_benign— as FETCH: no per-agent state, but Pikuri::Tool::WebScrape::CACHE is process-wide and on disk, so one agent's scrape answers another's within the TTL. See UrlCache. new( name: 'web_scrape', description: <<~DESC, Scrapes the rendered webpage or text file at the given URL and returns its main content as Markdown. Usage: - Use for HTML pages where you want readable content — readability extraction strips nav, sidebars, and boilerplate. - For raw textual payloads (JSON, CSV, robots.txt, source files), use fetch instead — it returns bytes verbatim, while web_scrape would corrupt them with a Markdown pass. - A Single Page App may return very little or no content. Do NOT retry with a larger max_chars; try a different URL instead. DESC parameters: Parameters.build { |p| p.required_string :url, 'Absolute URL of the webpage to scrape, including ' \ 'the scheme, e.g. "https://example.com/article".' p.optional_integer :max_chars, 'Maximum number of characters of Markdown to ' \ 'return. Defaults to 20000; hard-capped at ' \ '100000. When the page is longer than this, ' \ 'output is cut and a marker reports the full ' \ 'length.' }, execute: ->(url:, max_chars: WebScrape::DEFAULT_MAX_CHARS) { WebScrape.visit(url, max_chars: max_chars) }, # Exactly {TrifectaLegs::ASSUMED}, and the archetype it was named for: the # page is authored by whoever runs the site, and the model picks the URL, # so the request is itself an outbound channel. trifecta_legs: Pikuri::Tool::TrifectaLegs::ASSUMED )
Instance Attribute Summary collapse
-
#description ⇒ String
readonly
Human-readable description used by the LLM to decide when to call the tool.
-
#execute ⇒ Proc
readonly
Callable invoked with validated keyword arguments, returning the observation — usually a
String, or aRubyLLM::Contentwith attachments for multimodal observations (e.g. Workspace::Read on a PNG), whichRubyLLM::Chatturns into the right image/document blocks. -
#name ⇒ String
readonly
Function name advertised to the LLM.
-
#parameters ⇒ Tool::Parameters
readonly
Declared schema; validates incoming arguments and serializes to the JSON Schema shape advertised to the LLM.
-
#trifecta_legs ⇒ Tool::TrifectaLegs
readonly
Which lethal-trifecta legs this tool contributes, for Trifecta.
-
#wants_context ⇒ Boolean
readonly
Whether #run hands
executeacontext:keyword carrying the ExecuteContext for this call.
Instance Method Summary collapse
- #initialize(name:, description:, parameters:, execute:, trifecta_legs: TrifectaLegs::ASSUMED, wants_context: false) ⇒ Tool constructor
-
#run(args, context = ExecuteContext.default) ⇒ String, RubyLLM::Content
Validate
argsagainst #parameters and forward them as kwargs to #execute. -
#to_ruby_llm_tool(context: ExecuteContext.default) ⇒ Class
Build a synthetic
RubyLLM::Toolsubclass wrapping this Tool — whatRubyLLM::Chat#with_toolaccepts.
Constructor Details
#initialize(name:, description:, parameters:, execute:, trifecta_legs: TrifectaLegs::ASSUMED, wants_context: false) ⇒ Tool
135 136 137 138 139 140 141 142 143 |
# File 'lib/pikuri/tool.rb', line 135 def initialize(name:, description:, parameters:, execute:, trifecta_legs: TrifectaLegs::ASSUMED, wants_context: false) @name = name @description = description @parameters = parameters @execute = execute @trifecta_legs = trifecta_legs @wants_context = wants_context end |
Instance Attribute Details
#description ⇒ String (readonly)
Returns human-readable description used by the LLM to decide when to call the tool.
98 99 100 |
# File 'lib/pikuri/tool.rb', line 98 def description @description end |
#execute ⇒ Proc (readonly)
Callable invoked with validated keyword arguments,
returning the observation — usually a String, or a RubyLLM::Content
with attachments for multimodal observations (e.g.
Workspace::Read on a PNG), which RubyLLM::Chat turns into
the right image/document blocks.
110 111 112 |
# File 'lib/pikuri/tool.rb', line 110 def execute @execute end |
#name ⇒ String (readonly)
Returns function name advertised to the LLM.
94 95 96 |
# File 'lib/pikuri/tool.rb', line 94 def name @name end |
#parameters ⇒ Tool::Parameters (readonly)
Returns declared schema; validates incoming arguments and serializes to the JSON Schema shape advertised to the LLM.
103 104 105 |
# File 'lib/pikuri/tool.rb', line 103 def parameters @parameters end |
#trifecta_legs ⇒ Tool::TrifectaLegs (readonly)
Returns which lethal-trifecta legs this tool contributes, for Pikuri::Trifecta.
114 115 116 |
# File 'lib/pikuri/tool.rb', line 114 def trifecta_legs @trifecta_legs end |
#wants_context ⇒ Boolean (readonly)
Returns whether #run hands execute a context: keyword
carrying the ExecuteContext for this call. Declared at construction,
never inferred from the Proc's signature — see the == Cancellation
section.
149 150 151 |
# File 'lib/pikuri/tool.rb', line 149 def wants_context @wants_context end |
Instance Method Details
#run(args, context = ExecuteContext.default) ⇒ String, RubyLLM::Content
Validate args against #parameters and forward them as kwargs to
#execute. Validation failures come back as "Error: <message>" Strings
for the next observation; everything else bubbles up.
165 166 167 168 169 170 171 172 173 174 |
# File 'lib/pikuri/tool.rb', line 165 def run(args, context = ExecuteContext.default) validated = @parameters.validate(args) if @wants_context @execute.call(**validated, context: context) else @execute.call(**validated) end rescue Tool::Parameters::ValidationError => e "Error: #{e.}" end |
#to_ruby_llm_tool(context: ExecuteContext.default) ⇒ Class
Build a synthetic RubyLLM::Tool subclass wrapping this Tool — what
RubyLLM::Chat#with_tool accepts. ruby_llm instantiates it and routes
tool calls through #execute(**args), which delegates to #run.
A wrapper is per agent even when this Tool is shared between several,
which is what lets it close over one agent's cancellation token.
186 187 188 189 190 191 192 193 194 195 196 197 198 199 |
# File 'lib/pikuri/tool.rb', line 186 def to_ruby_llm_tool(context: ExecuteContext.default) pikuri_tool = self schema = @parameters.to_openai tool_name = @name tool_desc = @description Class.new(RubyLLM::Tool) do description(tool_desc) params(schema) define_singleton_method(:name) { tool_name } define_method(:execute) { |**args| pikuri_tool.run(args, context) } end end |