Class: Pikuri::Thunderbird::MailSearch
- Inherits:
-
Pikuri::Tool
- Object
- Pikuri::Tool
- Pikuri::Thunderbird::MailSearch
- Defined in:
- lib/pikuri/thunderbird/mail_search.rb
Overview
The thunderbird_mail_search tool — ranked, deduped search over the
local Thunderbird mailbox via Gloda. Inbound-only (no egress). Its
capability pair is thunderbird_mail_read, which fetches a hit's full
body by the id: handle this tool returns (parent-document retrieval,
like corpus search→read).
Sharing: P_shared_locked — no state of its own, and the Gloda
backend it queries locks; see that class's == Sharing.
Constant Summary collapse
- DEFAULT_LIMIT =
Returns default / max hits returned. The cap protects the host's context budget (a snippet-laden page is dear on a small local model); past it, ranking is moot and narrowing is the right move — which is what header steers the model toward.
15- MAX_LIMIT =
50- DESCRIPTION =
Returns opencode-shape description.
<<~DESC Search the user's local Thunderbird mail (subject, body, sender, recipients, attachment names) and return ranked matches. Usage: - Free-text query and/or exact filters (sender, recipient, subject, date range, folder) — both optional; supply at least one. - Multiple words match broadly (any of them), and the best-matching messages rank first — so extra words widen the net and improve ranking rather than requiring all to appear. - With no query, the filters alone list matches newest-first — e.g. the latest mail from a sender is from="alice@acme.com" with no query. - Reads Thunderbird's own local index — local folders only; it never connects to any mail server. - Returns the best matches with a snippet and a stable id; read a match in full with thunderbird_mail_read. - Spam/Junk/Trash are not searched. DESC
Class Method Summary collapse
-
.run_search(backend:, query:, from:, to:, subject:, after:, before:, folder:, limit:) ⇒ String
Formatted hits, a no-match line, or
"Error: …".
Instance Method Summary collapse
- #initialize(backend:) ⇒ MailSearch constructor
Constructor Details
#initialize(backend:) ⇒ MailSearch
36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
# File 'lib/pikuri/thunderbird/mail_search.rb', line 36 def initialize(backend:) @backend = backend super( name: 'thunderbird_mail_search', description: DESCRIPTION, parameters: Parameters.build { |p| p.optional_string :query, 'Words or a phrase to find, e.g. "invoice from acme". Omit to list by the filters alone (newest first).' p.optional_string :from, 'Only mail whose sender contains this, e.g. "alice@acme.com".' p.optional_string :to, 'Only mail whose recipients contain this, e.g. "me@example.com".' p.optional_string :subject, 'Only mail whose subject contains this, e.g. "receipt".' p.optional_string :after, 'Only mail on/after this date, e.g. "2026-01-01".' p.optional_string :before, 'Only mail on/before this date, e.g. "2026-06-30".' p.optional_string :folder, 'Restrict to one folder by its exact name, e.g. "INBOX".' p.optional_integer :limit, "Max matches (default #{DEFAULT_LIMIT}, max #{MAX_LIMIT}), e.g. 10." }, execute: lambda { |query: nil, from: nil, to: nil, subject: nil, after: nil, before: nil, folder: nil, limit: DEFAULT_LIMIT| MailSearch.run_search(backend: @backend, query:, from:, to:, subject:, after:, before:, folder:, limit:) }, trifecta_legs: Pikuri::Thunderbird::INBOUND_LEGS ) end |
Class Method Details
.run_search(backend:, query:, from:, to:, subject:, after:, before:, folder:, limit:) ⇒ String
Returns formatted hits, a no-match line, or "Error: …".
61 62 63 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 |
# File 'lib/pikuri/thunderbird/mail_search.rb', line 61 def self.run_search(backend:, query:, from:, to:, subject:, after:, before:, folder:, limit:) # Refuse a match-everything call: with no query and no filter this # would dump the whole mailbox newest-first. Every real request has at # least one criterion — steer the model to name one. if [query, from, to, subject, after, before, folder].all? { |v| v.to_s.strip.empty? } return 'Error: give at least one of query, from, to, subject, after, before, or folder to search for.' end limit = limit.to_i.clamp(1, MAX_LIMIT) after_t = DateHelpers.parse_after(after) before_t = DateHelpers.parse_before(before) # Fetch one past the cap so a full page is distinguishable from an # exhausted mailbox: getting limit+1 back means more exist, and the # header must say so — an LLM reads a full page as "that's everything" # and gives a silently incomplete answer. (Heavy Gmail label-dup can # false-negative if the (limit+1)th distinct match falls outside the # backend's over-fetch window; rare, accepted.) hits = backend.search(query:, limit: limit + 1, from:, to:, subject:, after: after_t, before: before_t, folder:) if hits.empty? return query.to_s.strip.empty? ? 'No mail matched those filters.' \ : "No matching mail found for #{query.inspect}." end has_more = hits.size > limit hits = hits.first(limit) [header(shown: hits.size, has_more:), *hits.map { |h| render(h) }].join("\n\n") rescue ArgumentError => e "Error: bad date filter (#{e.})." end |