ask-web-search 0.7.1 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 254d229de3235963082784b2300a04a5c6a9fad33df6d733fa511bc51fa59d8e
4
- data.tar.gz: e2fca871ad9b9ce5b7ca3f1b4407514f267bfda66658f2219c1b906812a9f4f1
3
+ metadata.gz: b1b6fbfe4321cb25a1da21bfcdb45c3a4817c50c27edae5adc429b6383950a2d
4
+ data.tar.gz: fb594f9d216c1a474248f5c2ac2209404fae1566d14c8863bb2e1202c3dcfa5b
5
5
  SHA512:
6
- metadata.gz: 4cd05c99d6ff688497fe27bca0215db4d5f68f4607ed07c3880447f44376d9b8294a149483447095130854bf5ad50418edf379b7b408c9655aca5acd48ab03de
7
- data.tar.gz: 4781a15d45be0e99cb27c4185255b06272bb397a21d3403f854c779ecfcca2a5b0181614c23f545831407721223c13089d1bf1bb2c89d0c1a9c9594735566d33
6
+ metadata.gz: b2517586f4020e29f66c2d0130cb315395db80209896ef1203af37f2c92c2b6b59b8b9380bf4646c3217677eb70b4bec22a30ceb92460dc072aba8f8c2f12bee
7
+ data.tar.gz: e273bea7c63d08c06957e9b7cf8562b951a563ad8dc0c6d42258389995f2d057a58743db01c6fc53082fc9f568551ab614e737fb744c5b4ea4a84c6c61466fb7
data/README.md CHANGED
@@ -9,21 +9,9 @@ depends only on ask-tools.
9
9
 
10
10
  ## Backends
11
11
 
12
- Two interchangeable backends, chosen automatically at call time:
12
+ Two interchangeable backends:
13
13
 
14
- 1. **TinyFish (recommended — no self-hosting).** Get a free API key at
15
- [agent.tinyfish.ai](https://agent.tinyfish.ai/api-keys) and export it:
16
-
17
- ```sh
18
- export TINYFISH_API_KEY=...
19
- ```
20
-
21
- That's the whole setup — no Docker, no SearXNG instance. Live
22
- browser-rendered results, freshness windows, and the news /
23
- research-paper verticals all work out of the box.
24
-
25
- 2. **SearXNG (self-hosted).** For users who want queries to stay local,
26
- and as the automatic fallback when TinyFish fails: a running
14
+ 1. **SearXNG (the default — for everyone).** Queries go to a local
27
15
  [SearXNG](https://docs.searxng.org/) instance, default
28
16
  `http://localhost:8888`. Start one with Docker:
29
17
 
@@ -34,14 +22,32 @@ Two interchangeable backends, chosen automatically at call time:
34
22
  Or use the provided `docker-compose.yml` in the `searxng` directory of
35
23
  this repository.
36
24
 
37
- Routing rules:
25
+ 2. **TinyFish (opt-in — for people who'd rather hold an API key than set
26
+ up SearXNG).** Two steps: select the backend, and provide a key.
27
+
28
+ ```sh
29
+ export SEARCH_BACKEND=tinyfish
30
+ ```
31
+
32
+ Get a free key (no credit card) at
33
+ [agent.tinyfish.ai](https://agent.tinyfish.ai/api-keys) and store it —
34
+ either in `~/.ask/credentials.yml` via ask-auth (one line:
35
+ `tinyfish_api_key: <key>`, file is 0600) or `export
36
+ TINYFISH_API_KEY=...`. Selecting tinyfish without a key raises an
37
+ onboarding error that says exactly that; holding a key without
38
+ selecting does nothing — SearXNG stays the default.
39
+
40
+ Selection precedence (first match wins):
41
+
42
+ 1. `TINYFISH_SEARCH=0` — a hard-off that forces SearXNG
43
+ 2. `Ask::WebSearch.backend = :tinyfish | :searxng` (in code)
44
+ 3. `SEARCH_BACKEND=tinyfish | searxng` (env)
45
+ 4. default → SearXNG
38
46
 
39
- - `TINYFISH_API_KEY` set → TinyFish primary; if TinyFish fails **and** a
40
- SearXNG endpoint is explicitly configured, the call falls back to
41
- SearXNG automatically (combined errors surface both failures).
42
- - No key → SearXNG only, exactly as in 0.6.x.
43
- - `TINYFISH_SEARCH=0` disables TinyFish outright (SearXNG only, even
44
- with a key).
47
+ When the tinyfish backend fails (network, rate limit) and a SearXNG
48
+ endpoint is explicitly configured, the call falls back to SearXNG
49
+ automatically — combined errors surface both failures. Invalid backend
50
+ names raise `ArgumentError` listing the valid ones.
45
51
 
46
52
  ## Installation
47
53
 
@@ -52,7 +58,8 @@ gem "ask-web-search"
52
58
  ## Configuration
53
59
 
54
60
  ```sh
55
- export TINYFISH_API_KEY=... # TinyFish (recommended): free key, instant search
61
+ export SEARCH_BACKEND=tinyfish # opt in to TinyFish (default: searxng)
62
+ export TINYFISH_API_KEY=... # TinyFish key (or tinyfish_api_key: in ~/.ask/credentials.yml)
56
63
  export SEARXNG_URL=http://localhost:8888 # SearXNG endpoint (default shown)
57
64
  ```
58
65
 
@@ -1,5 +1,5 @@
1
1
  module Ask
2
2
  module WebSearch
3
- VERSION = "0.7.1"
3
+ VERSION = "0.9.0"
4
4
  end
5
5
  end
@@ -7,13 +7,15 @@ require_relative "web_search/version"
7
7
 
8
8
  module Ask
9
9
  # Searches the web and returns the results as clean, numbered markdown
10
- # for LLM consumption. Two interchangeable backends: TinyFish's hosted
11
- # search API — primary when TINYFISH_API_KEY is set, so newcomers need
12
- # no self-hosted infrastructure — and a local SearXNG instance for
13
- # self-hosters, which is also the automatic fallback when TinyFish
14
- # fails. The capability layer: one entry point (WebSearch.search).
15
- # Tool framing — name, parameter schema, result wrapping — lives with
16
- # the consumers (the MCP server, the agents) that call this library.
10
+ # for LLM consumption. Two interchangeable backends: a local SearXNG
11
+ # instance — the default path for everyone — and TinyFish's hosted
12
+ # search API, strictly opt-in (SEARCH_BACKEND=tinyfish + a free API
13
+ # key) for users who'd rather hold a key than run SearXNG. When the
14
+ # tinyfish backend fails, an explicitly configured SearXNG endpoint is
15
+ # still the automatic fallback. The capability layer: one entry point
16
+ # (WebSearch.search). Tool framing — name, parameter schema, result
17
+ # wrapping — lives with the consumers (the MCP server, the agents)
18
+ # that call this library.
17
19
  module WebSearch
18
20
  # Search errors that carry engine diagnostics — the agent needs to
19
21
  # know *which* engines failed and *why* (CAPTCHA, timeout, suspended)
@@ -46,12 +48,15 @@ module Ask
46
48
  # unfiltered.
47
49
  TIME_RANGES = %w[day week month year].freeze
48
50
 
49
- # TinyFish's hosted search endpoint — the primary backend for anyone
50
- # holding a (free) TINYFISH_API_KEY, so no SearXNG instance is needed.
51
- # The bare host root per the official docs curl example; the
52
- # /client.search.query path seen on the marketing pages 404s.
51
+ # TinyFish's hosted search endpoint — available when the tinyfish
52
+ # backend is selected (SEARCH_BACKEND=tinyfish). The bare host root
53
+ # per the official docs curl example; the /client.search.query path
54
+ # seen on the marketing pages 404s.
53
55
  TINYFISH_SEARCH_URL = "https://api.search.tinyfish.ai"
54
56
 
57
+ # The selectable backends (see .backend for how one is chosen).
58
+ BACKENDS = %w[searxng tinyfish].freeze
59
+
55
60
  # Our time_range → TinyFish's recency_minutes (SearXNG's month/year
56
61
  # are approximate engine filters anyway; these are the equivalents).
57
62
  TIME_RANGE_TO_MINUTES = {
@@ -72,13 +77,58 @@ module Ask
72
77
  "research_paper" => "research_paper"
73
78
  }.freeze
74
79
 
75
- # True when TinyFish should be the primary backend: a key is present
76
- # and TINYFISH_SEARCH=0 has not disabled it. Read fresh on every
77
- # call so tests and config reloads don't need memo resets.
78
- def self.use_tinyfish?
79
- return false if ENV["TINYFISH_SEARCH"] == "0"
80
+ # The search backend — :searxng (the default path for everyone) or
81
+ # :tinyfish (opt-in for users who'd rather hold an API key than run
82
+ # SearXNG). Selection precedence:
83
+ #
84
+ # 1. TINYFISH_SEARCH=0 — a hard-off that forces :searxng
85
+ # 2. backend= setter (Ask::WebSearch.backend = :tinyfish)
86
+ # 3. SEARCH_BACKEND env ("searxng" | "tinyfish")
87
+ # 4. default :searxng
88
+ def self.backend
89
+ return :searxng if ENV["TINYFISH_SEARCH"] == "0"
90
+ return @backend if defined?(@backend) && @backend
91
+
92
+ raw = ENV["SEARCH_BACKEND"]
93
+ return :searxng if raw.to_s.empty?
94
+
95
+ normalize_backend(raw)
96
+ end
97
+
98
+ # Selects the backend in code (:searxng / :tinyfish, or nil to fall
99
+ # back to env/default). Raises ArgumentError for anything else —
100
+ # config-time failure instead of a silently ignored value.
101
+ def self.backend=(name)
102
+ @backend = name.nil? ? nil : normalize_backend(name)
103
+ end
104
+
105
+ def self.normalize_backend(name)
106
+ value = name.to_s.strip.downcase
107
+ unless BACKENDS.include?(value)
108
+ raise ArgumentError, "invalid backend #{name.inspect} — use one of: #{BACKENDS.join(', ')}"
109
+ end
110
+
111
+ value.to_sym
112
+ end
113
+ private_class_method :normalize_backend
80
114
 
81
- !ENV["TINYFISH_API_KEY"].to_s.empty?
115
+ # The TinyFish API key: ask-auth's chain when that gem is present
116
+ # (env override → ~/.ask/credentials.yml → …), raw ENV otherwise.
117
+ # nil when nothing resolves.
118
+ def self.tinyfish_api_key
119
+ return ENV["TINYFISH_API_KEY"] unless defined?(Ask::Auth)
120
+
121
+ Ask::Auth.resolve(:tinyfish_api_key)
122
+ rescue Ask::Auth::MissingCredential
123
+ nil
124
+ end
125
+
126
+ # True when searches should go to TinyFish — i.e. the backend was
127
+ # explicitly selected. Read fresh on every call so tests and config
128
+ # reloads don't need memo resets. Holding a key alone does NOT
129
+ # opt in: SearXNG stays the default path.
130
+ def self.use_tinyfish?
131
+ backend == :tinyfish
82
132
  end
83
133
 
84
134
  # True when a SearXNG endpoint was configured explicitly (SEARXNG_URL
@@ -139,14 +189,15 @@ module Ask
139
189
  end
140
190
 
141
191
  # The full response: { results: [...], unresponsive: [[name,
142
- # reason], ...] }. Routes to TinyFish when #use_tinyfish?, falling
143
- # back to SearXNG (only when explicitly configured) if TinyFish
144
- # raises; otherwise straight to SearXNG — byte-for-byte the 0.6.x
145
- # behavior for keyless users. Same retry logic either way, but
146
- # preserves the engine diagnostics the caller needs to explain an
147
- # empty result. time_range / categories are normalized by the
148
- # backends that build requests — idempotent, so callers may pass
149
- # raw or already-normalized values.
192
+ # reason], ...] }. Routes to TinyFish when #use_tinyfish? (the
193
+ # opt-in tinyfish backend — a keyless selection raises its
194
+ # onboarding error; a transport failure falls back to SearXNG when
195
+ # explicitly configured), otherwise to SearXNG — the default path
196
+ # for everyone. Same retry logic either way, but preserves the
197
+ # engine diagnostics the caller needs to explain an empty result.
198
+ # time_range / categories are normalized by the backends that build
199
+ # requests — idempotent, so callers may pass raw or
200
+ # already-normalized values.
150
201
  def self.search_raw(query, time_range: nil, categories: nil)
151
202
  return searxng_search_raw(query, time_range: time_range, categories: categories) unless use_tinyfish?
152
203
 
@@ -195,6 +246,14 @@ module Ask
195
246
  # catches). time_range maps to recency_minutes; the FIRST
196
247
  # recognized categories value maps to domain_type.
197
248
  def self.tinyfish_search_raw(query, time_range: nil, categories: nil)
249
+ key = tinyfish_api_key
250
+ if key.to_s.empty?
251
+ raise Error, "TinyFish backend selected (SEARCH_BACKEND=tinyfish) but no API key resolves. " \
252
+ "Get a free key at https://agent.tinyfish.ai/api-keys, then add " \
253
+ "`tinyfish_api_key: <key>` to ~/.ask/credentials.yml or export " \
254
+ "TINYFISH_API_KEY — or drop SEARCH_BACKEND to stay on the default SearXNG path."
255
+ end
256
+
198
257
  params = { query: query }
199
258
  normalized_range = normalize_time_range(time_range)
200
259
  params[:recency_minutes] = TIME_RANGE_TO_MINUTES[normalized_range] if normalized_range
@@ -212,7 +271,7 @@ module Ask
212
271
  http.open_timeout = 5
213
272
  http.read_timeout = 10
214
273
  req = Net::HTTP::Get.new(uri)
215
- req["X-API-Key"] = ENV["TINYFISH_API_KEY"]
274
+ req["X-API-Key"] = key
216
275
  req["User-Agent"] = "ask-web-search/#{Ask::WebSearch::VERSION}"
217
276
  res = http.request(req)
218
277
  raise Error, "TinyFish returned #{res.code}: #{res.body[0, 200]}" unless res.code.start_with?("2")
@@ -332,3 +391,15 @@ begin
332
391
  rescue LoadError => e
333
392
  raise unless e.path == "ask-tools"
334
393
  end
394
+
395
+ # ask-auth is an OPTIONAL credential source for the TinyFish key: when
396
+ # present, #tinyfish_api_key resolves through Ask::Auth (env override →
397
+ # ~/.ask/credentials.yml → …) instead of raw ENV alone, so the key can
398
+ # live in one canonical 0600 file instead of every config that spawns a
399
+ # server. Only the ask-auth miss is swallowed; any other LoadError is
400
+ # real.
401
+ begin
402
+ require "ask-auth"
403
+ rescue LoadError => e
404
+ raise unless e.path == "ask-auth"
405
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ask-web-search
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.1
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kaka Ruto
@@ -23,6 +23,20 @@ dependencies:
23
23
  - - ">="
24
24
  - !ruby/object:Gem::Version
25
25
  version: '0.1'
26
+ - !ruby/object:Gem::Dependency
27
+ name: ask-auth
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '0.3'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0.3'
26
40
  - !ruby/object:Gem::Dependency
27
41
  name: vcr
28
42
  requirement: !ruby/object:Gem::Requirement
@@ -79,11 +93,12 @@ dependencies:
79
93
  - - "~>"
80
94
  - !ruby/object:Gem::Version
81
95
  version: '13.0'
82
- description: 'Web search returning results as clean numbered markdown. Two backends:
83
- TinyFish''s hosted API (primary with TINYFISH_API_KEY — no self-hosting) and a local
84
- SearXNG instance (SEARXNG_URL, also the automatic fallback). The capability layer
85
- (WebSearch.search); the native Ask::Tools::WebSearch agent tool is an optional integration
86
- that registers when ask-tools is present.'
96
+ description: 'Web search returning results as clean numbered markdown. Two selectable
97
+ backends: a local SearXNG instance (SEARXNG_URL, the default path for everyone)
98
+ and TinyFish''s hosted API (opt-in: SEARCH_BACKEND=tinyfish plus a TINYFISH_API_KEY
99
+ or ask-auth credentials key). The capability layer (WebSearch.search); the native
100
+ Ask::Tools::WebSearch agent tool is an optional integration that registers when
101
+ ask-tools is present.'
87
102
  email:
88
103
  - kaka@myrrlabs.com
89
104
  executables: []