Class: Pikuri::Tool

Inherits:
Object
  • Object
show all
Defined in:
lib/pikuri/tool.rb,
lib/pikuri/tool/bash.rb,
lib/pikuri/tool/edit.rb,
lib/pikuri/tool/glob.rb,
lib/pikuri/tool/grep.rb,
lib/pikuri/tool/read.rb,
lib/pikuri/tool/fetch.rb,
lib/pikuri/tool/skill.rb,
lib/pikuri/tool/write.rb,
lib/pikuri/tool/confirmer.rb,
lib/pikuri/tool/sub_agent.rb,
lib/pikuri/tool/workspace.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/scraper/pdf.rb,
lib/pikuri/tool/scraper/html.rb,
lib/pikuri/tool/search/brave.rb,
lib/pikuri/tool/search/result.rb,
lib/pikuri/tool/skill_catalog.rb,
lib/pikuri/tool/scraper/simple.rb,
lib/pikuri/tool/search/engines.rb,
lib/pikuri/tool/search/duckduckgo.rb,
lib/pikuri/tool/scraper/fetch_error.rb,
lib/pikuri/tool/search/rate_limiter.rb

Overview

Loaded after Tool itself is defined; the class Tool reopening below assumes that order.

Direct Known Subclasses

Bash, Edit, Glob, Grep, Read, Skill, SubAgent, Write

Defined Under Namespace

Modules: Calculator, Fetch, Scraper, Search, WebScrape, WebSearch Classes: Bash, Confirmer, Edit, Glob, Grep, Parameters, Read, Skill, SkillCatalog, SubAgent, Workspace, Write

Constant Summary collapse

FETCH =

Verbatim URL download tool. Thin wrapper over Pikuri::Tool::Fetch.fetch that exposes it to the agent loop in OpenAI tool-call shape. Use for raw textual payloads (JSON APIs, CSV files, robots.txt, source files); use WEB_SCRAPE for rendered web pages or PDFs where readability extraction makes the result usable.

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 or PDFs, use web_scrape — it extracts readable content; fetch returns the raw HTML/PDF 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)
  }
)
CALCULATOR =

Arithmetic-evaluation tool backed by Pikuri::Tool::Calculator.calculate. Accepts Python-flavored operator syntax (+, -, *, /, ** for exponentiation, %, parentheses, decimals) so the model can emit the syntax it already knows.

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.
    - Operators supported: +, -, *, /, ** (exponentiation), %, parentheses, decimal numbers.
    - Decimal results are rounded to 3 places; integer results are exact.
    - Failures (parse error, division by zero) come back as "Error: ..." — read the message and re-call with a corrected expression.
  DESC
  parameters: Parameters.build { |p|
    p.required_string :expression,
                      'Arithmetic expression to evaluate, e.g. ' \
                      '"155 / (58 * 1000.0 / 3600)" or "2**10".'
  },
  execute: ->(expression:) { Calculator.calculate(expression) }
)
WEB_SCRAPE =

Webpage download + Markdown conversion tool. Thin wrapper over Pikuri::Tool::WebScrape.visit that exposes it to the agent loop in OpenAI tool-call shape.

Returns:

new(
  name: 'web_scrape',
  description: <<~DESC,
    Scrapes the rendered webpage, PDF, or text file at the given URL and returns its main content as Markdown.

    Usage:
    - Use for HTML pages or PDFs 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)
  }
)
WEB_SEARCH =

Web-search tool exposed to the agent loop in OpenAI tool-call shape. Calls Search::Engines.search, which cascades through whichever providers are configured (DuckDuckGo always, Brave when its API key is present) in random order, falling back on temporary-unavailability errors. Providers return structured Search::Result rows; Engines.search renders the winning provider’s rows into the smolagents-style Markdown shape the LLM sees, so the format stays stable regardless of which provider ran.

Returns:

new(
  name: 'web_search',
  description: <<~DESC,
    Searches the web for a query and returns the top results as a Markdown list of titles, URLs, and short snippets.

    Usage:
    - Use this to find candidate URLs, then call web_scrape on the most promising one(s) for full content. Snippets alone rarely answer a question.
  DESC
  parameters: Parameters.build { |p|
    p.required_string :query,
                      'The search query, e.g. "BigDecimal precision Ruby".'
    p.optional_integer :max_results,
                       'Maximum number of result entries to return. ' \
                       'Defaults to 10; most providers cap this at 20.'
  },
  execute: ->(query:, max_results: 10) { Search::Engines.search(query, max_results: max_results) }
)

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(name:, description:, parameters:, execute:) ⇒ 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 that returns a String observation. Recoverable failures should be returned as “Error: <message>” Strings rather than raised — see “Error handling convention” above.



74
75
76
77
78
79
# File 'lib/pikuri/tool.rb', line 74

def initialize(name:, description:, parameters:, execute:)
  @name = name
  @description = description
  @parameters = parameters
  @execute = execute
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



53
54
55
# File 'lib/pikuri/tool.rb', line 53

def description
  @description
end

#executeProc (readonly)

Returns callable invoked once arguments have been validated; receives validated keyword arguments and returns a String observation.

Returns:

  • (Proc)

    callable invoked once arguments have been validated; receives validated keyword arguments and returns a String observation



63
64
65
# File 'lib/pikuri/tool.rb', line 63

def execute
  @execute
end

#nameString (readonly)

Returns function name advertised to the LLM.

Returns:

  • (String)

    function name advertised to the LLM



49
50
51
# File 'lib/pikuri/tool.rb', line 49

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



58
59
60
# File 'lib/pikuri/tool.rb', line 58

def parameters
  @parameters
end

Instance Method Details

#run(args) ⇒ String

Validate args against #parameters and forward them as keyword arguments to #execute. Validation failures are caught and rendered as “Error: <message>” Strings so the agent loop can feed them back to the LLM as the next observation; everything else bubbles up.

Parameters:

  • args (Hash)

    raw arguments supplied by the LLM

Returns:

  • (String)

    tool observation, or “Error: …” on validation failure



89
90
91
92
93
94
# File 'lib/pikuri/tool.rb', line 89

def run(args)
  validated = @parameters.validate(args)
  @execute.call(**validated)
rescue Tool::Parameters::ValidationError => e
  "Error: #{e.message}"
end

#to_ruby_llm_toolClass

Build a synthetic RubyLLM::Tool subclass that wraps this Tool. The subclass is what RubyLLM::Chat#with_tool accepts: ruby_llm instantiates it (tool.new) and routes tool calls through instance.call(args), which lands in #execute(**args) and delegates back to #run on this instance.

Returns:

  • (Class)

    anonymous RubyLLM::Tool subclass



103
104
105
106
107
108
109
110
111
112
113
114
115
116
# File 'lib/pikuri/tool.rb', line 103

def to_ruby_llm_tool
  pikuri_tool = self
  schema      = @parameters.to_h
  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) }
  end
end