Class: Pikuri::Tool::Search::Exa
- Inherits:
-
Object
- Object
- Pikuri::Tool::Search::Exa
- Defined in:
- lib/pikuri/tool/search/exa.rb
Overview
Performs an Exa search via the official /search endpoint, returning
hits as Result rows. Split into a thin HTTP fetch (#search) and a pure
parser (.parse) so tests exercise the parser against fixture JSON.
Pikuri::Tool::Search::Engines#search owns the final Markdown rendering.
Constructed with its API key (+Exa.new(api_key:)+); Engines builds one only when an Exa key was configured, so users who never registered don't spend money, then drives it through the same +#search+/+#label+ interface as every provider. pikuri reads no key from the environment (CLAUDE.md "Environment is not a secret store"). Paid; key at https://exa.ai.
Requests type: "auto" (Exa picks neural vs keyword) and +contents: {
highlights: true }+ so each result carries a neural-ranked snippet
populating Result#body — the analog to Brave's description.
Privacy posture
Exa's Privacy Policy uses Query Data to train/fine-tune its models, and
ToS §1.2(c) grants a perpetual and irrevocable, sub-licensable license
over User Input. Business customers under an MSA/DPA get carve-outs; the
default pay-as-you-go key pikuri uses does not. Bottom line: Exa mines
queries to train competing models. If a query would be sensitive in a
training set, don't configure an Exa key — Pikuri::Tool::Search::Engines#providers leaves
Exa out of the cascade unless its key was supplied.
Constant Summary collapse
- ENDPOINT =
Returns Search endpoint (POST, JSON body).
'https://api.exa.ai/search'- DEFAULT_MAX_RESULTS =
Returns default number of results returned, matching DuckDuckGo::DEFAULT_MAX_RESULTS.
10- LIMITER =
Returns Exa is paid and doesn't aggressively throttle, so no minimum interval; the 5-minute cooldown still applies on Pikuri::Tool::Search::Engines::Unavailable so the budget isn't burned on doomed retries.
RateLimiter.new(min_interval: 0.0, cooldown: 300.0)
Class Method Summary collapse
-
.parse(json, max_results: DEFAULT_MAX_RESULTS) ⇒ Array<Result>
Parse an Exa JSON response into Result rows,
bodythe first non-emptyhighlightssnippet.
Instance Method Summary collapse
-
#initialize(api_key:) ⇒ Exa
constructor
A new instance of Exa.
-
#label ⇒ String
Short provider label for Engines logging / fallback messages.
-
#search(query, max_results: DEFAULT_MAX_RESULTS, cancellable: Pikuri::Agent::Control::Cancellable::NEVER) ⇒ Array<Result>
Fetch results for
queryas anArray<Result>.
Constructor Details
#initialize(api_key:) ⇒ Exa
Returns a new instance of Exa.
49 50 51 52 53 |
# File 'lib/pikuri/tool/search/exa.rb', line 49 def initialize(api_key:) raise ArgumentError, 'Exa Search API key is blank' if api_key.to_s.strip.empty? @api_key = api_key end |
Class Method Details
.parse(json, max_results: DEFAULT_MAX_RESULTS) ⇒ Array<Result>
Parse an Exa JSON response into Result rows, body the first
non-empty highlights snippet. A genuine "no results" payload
(+requestId+ present, empty results) returns [] so Pikuri::Tool::Search::Engines#search
renders its no-results stub; any other zero-result shape raises.
111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 |
# File 'lib/pikuri/tool/search/exa.rb', line 111 def self.parse(json, max_results: DEFAULT_MAX_RESULTS) data = JSON.parse(json) results = Array(data['results']).take(max_results).filter_map do |r| href = r['url'].to_s next nil if href.empty? Result.new( url: href, title: clean(r['title']) || href, body: first_highlight(r['highlights']) ) end if results.empty? return [] if genuine_no_results?(data) raise diagnose_empty(data, json) end results end |
Instance Method Details
#label ⇒ String
Returns short provider label for Pikuri::Tool::Search::Engines logging / fallback messages.
57 58 59 |
# File 'lib/pikuri/tool/search/exa.rb', line 57 def label 'Exa' end |
#search(query, max_results: DEFAULT_MAX_RESULTS, cancellable: Pikuri::Agent::Control::Cancellable::NEVER) ⇒ Array<Result>
Fetch results for query as an Array<Result>. Circuit-broken for
5 minutes on rate-limit/unavailable (see LIMITER). The caller
(Pikuri::Tool::Search::Engines#search) normalizes the query and wraps this in a cache.
75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 |
# File 'lib/pikuri/tool/search/exa.rb', line 75 def search(query, max_results: DEFAULT_MAX_RESULTS, cancellable: Pikuri::Agent::Control::Cancellable::NEVER) LIMITER.call(cancellable: cancellable) do response = Faraday.post(ENDPOINT) do |req| req.headers['x-api-key'] = @api_key req.headers['Content-Type'] = 'application/json' req.headers['Accept'] = 'application/json' req.body = JSON.dump( query: query, type: 'auto', numResults: max_results, contents: { highlights: true } ) end unless response.success? if response.status == 429 || response.status >= 500 raise Engines::Unavailable, "HTTP #{response.status}" end raise "Exa Search request failed: #{response.status} #{response.body}" end self.class.parse(response.body, max_results: max_results) end end |