Class: Pikuri::Tool::Search::Brave

Inherits:
Object
  • Object
show all
Defined in:
lib/pikuri/tool/search/brave.rb

Overview

Performs a Brave Search via the official Web Search API, 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. Engines#search owns the final Markdown rendering.

Constructed with its API key (+Brave.new(api_key:)+); Engines builds one only when a Brave key was configured. pikuri reads no key from the environment (CLAUDE.md "Environment is not a secret store"). Key at https://api-dashboard.search.brave.com — the free "Data for Search" tier allows 1 query/sec, ~2k/month.

Privacy posture

Brave's API notice retains Search Query Logs 90 days and collects no identifiers linking a query to a person; it publicly commits not to train on query data, and offers Zero Data Retention — but only on Enterprise, not the free tier pikuri defaults to. Bottom line: the cleanest API-level posture of pikuri's three providers (no training, no IP linkage, capped 90-day retention), but a logged 90-day window on the cheap tier, so not a ZDR substitute for genuinely sensitive queries.

Constant Summary collapse

ENDPOINT =

Returns Web Search endpoint.

Returns:

  • (String)

    Web Search endpoint

'https://api.search.brave.com/res/v1/web/search'
DEFAULT_MAX_RESULTS =

Returns default number of results returned.

Returns:

  • (Integer)

    default number of results returned.

10
LIMITER =

Returns free-tier Brave caps at 1 req/sec; the 5-minute cooldown protects the monthly quota from doomed retries on a 429.

Returns:

  • (RateLimiter)

    free-tier Brave caps at 1 req/sec; the 5-minute cooldown protects the monthly quota from doomed retries on a 429.

RateLimiter.new(min_interval: 1.0, cooldown: 300.0)

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(api_key:) ⇒ Brave

Returns a new instance of Brave.

Parameters:

  • api_key (String)

    Brave Search subscription token. Required and non-blank: pikuri reads no key from the environment — the host supplies it (Engines only constructs a Brave when a key was configured).

Raises:

  • (ArgumentError)

    if api_key is blank



44
45
46
47
48
# File 'lib/pikuri/tool/search/brave.rb', line 44

def initialize(api_key:)
  raise ArgumentError, 'Brave 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 a Brave JSON response into Result rows (++ highlights stripped). A genuine "no results" payload (recognized search shape, empty mixed.main/top/side) returns [] so Engines#search renders its stub; any other zero-result shape raises.

Parameters:

  • json (String)

    response body from ENDPOINT

  • max_results (Integer) (defaults to: DEFAULT_MAX_RESULTS)

    max entries

Returns:

  • (Array<Result>)

    hits, possibly empty on a recognized empty-results payload

Raises:

  • (RuntimeError)

    on an unrecognized zero-result response



99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
# File 'lib/pikuri/tool/search/brave.rb', line 99

def self.parse(json, max_results: DEFAULT_MAX_RESULTS)
  data = JSON.parse(json)
  results = Array(data.dig('web', 'results')).take(max_results).filter_map do |r|
    href = r['url'].to_s
    next nil if href.empty?

    Result.new(
      url: href,
      title: strip_html(r['title']),
      body: strip_html(r['description'])
    )
  end

  if results.empty?
    return [] if genuine_no_results?(data)

    raise diagnose_empty(data, json)
  end

  results
end

Instance Method Details

#labelString

Returns short provider label for Engines.

Returns:

  • (String)

    short provider label for Engines.



51
52
53
# File 'lib/pikuri/tool/search/brave.rb', line 51

def label
  'Brave'
end

#search(query, max_results: DEFAULT_MAX_RESULTS, cancellable: Pikuri::Agent::Control::Cancellable::NEVER) ⇒ Array<Result>

Fetch results for query as an Array<Result>. Throttled to 1/sec and circuit-broken 5 min on rate-limit/quota (see LIMITER). The caller (Engines#search) normalizes the query and wraps this in a cache.

Parameters:

  • query (String)

    search query (already normalized)

  • max_results (Integer) (defaults to: DEFAULT_MAX_RESULTS)

    max entries; Brave's count (1..20)

  • cancellable (Pikuri::Agent::Control::Cancellable) (defaults to: Pikuri::Agent::Control::Cancellable::NEVER)

    makes LIMITER's pacing wait interruptible; see RateLimiter#call.

Returns:

  • (Array<Result>)

    hits, possibly empty when Brave matched nothing

Raises:

  • (Engines::Unavailable)

    on HTTP 429 or 5xx (the cascade falls back), or if LIMITER is in cooldown. Other non-2xx (401/403 bad key) bubble up as RuntimeError.

  • (RuntimeError)

    for non-rate-limit HTTP failures, or a zero-result response that isn't a recognized empty-results payload.



69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
# File 'lib/pikuri/tool/search/brave.rb', line 69

def search(query, max_results: DEFAULT_MAX_RESULTS,
           cancellable: Pikuri::Agent::Control::Cancellable::NEVER)
  LIMITER.call(cancellable: cancellable) do
    response = Faraday.get(
      ENDPOINT,
      { q: query, count: max_results },
      { 'X-Subscription-Token' => @api_key, 'Accept' => 'application/json' }
    )
    unless response.success?
      if response.status == 429 || response.status >= 500
        raise Engines::Unavailable, "HTTP #{response.status}"
      end

      raise "Brave Search request failed: #{response.status} #{response.body}"
    end

    self.class.parse(response.body, max_results: max_results)
  end
end