Class: Pikuri::Tool

Inherits:
Object
  • Object
show all
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's fetch can be answered from agent A's download within the TTL. See UrlCache for the race and why it's harmless.

Returns:

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. See DECISIONS.md D_keep_calculator.

Sharing: P_stateless — arithmetic over the argument, no I/O, no state. Hand this one constant to every agent in the VM.

Returns:

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.

Returns:

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

Instance Method Summary collapse

Constructor Details

#initialize(name:, description:, parameters:, execute:, trifecta_legs: TrifectaLegs::ASSUMED, wants_context: false) ⇒ Tool

Parameters:

  • name (String)

    function name advertised to the LLM

  • description (String)

    human-readable description used by the LLM to decide when to call the tool

  • parameters (Tool::Parameters)

    declared schema

  • execute (Proc)

    callable invoked with validated keyword arguments, returning a String or RubyLLM::Content (see #execute). Recoverable failures return "Error: <message>" rather than raise.

  • trifecta_legs (Tool::TrifectaLegs) (defaults to: TrifectaLegs::ASSUMED)

    legs this tool contributes. Compute them here rather than at a later resolution pass: construction is wiring time, since every collaborator that decides a leg (the workspace, the sandbox, the confirmer) is already a kwarg of the tool that uses it. Defaults to Pikuri::Tool::TrifectaLegs::ASSUMED — an undeclared tool is presumed hostile on both axes the framework could have determined.

  • wants_context (Boolean) (defaults to: false)

    when true, #run additionally passes a context: keyword carrying this call's ExecuteContext — so execute must declare one. Declared rather than inferred from the Proc, and both halves are mandatory, so either without the other is an ArgumentError on the first call rather than a silent nil.



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

#descriptionString (readonly)

Returns human-readable description used by the LLM to decide when to call the tool.

Returns:

  • (String)

    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

#executeProc (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.

Returns:

  • (Proc)

    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

#nameString (readonly)

Returns function name advertised to the LLM.

Returns:

  • (String)

    function name advertised to the LLM



94
95
96
# File 'lib/pikuri/tool.rb', line 94

def name
  @name
end

#parametersTool::Parameters (readonly)

Returns declared schema; validates incoming arguments and serializes to the JSON Schema shape advertised to the LLM.

Returns:

  • (Tool::Parameters)

    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_legsTool::TrifectaLegs (readonly)

Returns which lethal-trifecta legs this tool contributes, for Pikuri::Trifecta.

Returns:



114
115
116
# File 'lib/pikuri/tool.rb', line 114

def trifecta_legs
  @trifecta_legs
end

#wants_contextBoolean (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.

Returns:

  • (Boolean)

    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.

Parameters:

  • args (Hash)

    raw arguments supplied by the LLM

  • context (ExecuteContext) (defaults to: ExecuteContext.default)

    this invocation's environment, forwarded as a context: keyword only when #wants_context. Never part of args — the LLM cannot supply it, since Pikuri::Tool::Parameters#validate refuses unknown keys. Positional against the house style on purpose: give this method any keyword and Ruby 3 re-reads the bare hash in run('text' => 'hi') as keywords, so args arrives empty at every existing call site.

Returns:

  • (String, RubyLLM::Content)

    tool observation — validation failures always "Error: ..."; success is whatever execute returns.



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.message}"
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.

Parameters:

  • context (ExecuteContext) (defaults to: ExecuteContext.default)

    passed to #run on every call; see the == Cancellation section.

Returns:

  • (Class)

    anonymous RubyLLM::Tool subclass



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