ask-web-search 0.8.0 → 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: 1b83b3aa3b4e051445e316284580fa85a0d478bb5256b60c87e14031e60c6130
4
- data.tar.gz: '0168f632c9d32eb42996d55f197cf2a322ad86f839b7698788c9f0cfc2e8ba87'
3
+ metadata.gz: b1b6fbfe4321cb25a1da21bfcdb45c3a4817c50c27edae5adc429b6383950a2d
4
+ data.tar.gz: fb594f9d216c1a474248f5c2ac2209404fae1566d14c8863bb2e1202c3dcfa5b
5
5
  SHA512:
6
- metadata.gz: 5e5cb4c7817665ca81783898ea4709e7883786f5db18778ec09a7375ef4f2abadecc77c001900a48aea277e3b18859e11a9b2c1f1cc97ecb8ded1fb6e29478f7
7
- data.tar.gz: 889707ff4c5f114ffbd2340cd667de622c281374e20fdee3023280706b8bad91ad3d46b02091274779a2e1ce8ed7acbb5bcad7669cb0f616765af50d6244a282
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.8.0"
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,6 +77,41 @@ module Ask
72
77
  "research_paper" => "research_paper"
73
78
  }.freeze
74
79
 
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
114
+
75
115
  # The TinyFish API key: ask-auth's chain when that gem is present
76
116
  # (env override → ~/.ask/credentials.yml → …), raw ENV otherwise.
77
117
  # nil when nothing resolves.
@@ -83,14 +123,12 @@ module Ask
83
123
  nil
84
124
  end
85
125
 
86
- # True when TinyFish should be the primary backend: a key resolves
87
- # (#tinyfish_api_key) and TINYFISH_SEARCH=0 has not disabled it.
88
- # Read fresh on every call so tests and config reloads don't need
89
- # memo resets.
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.
90
130
  def self.use_tinyfish?
91
- return false if ENV["TINYFISH_SEARCH"] == "0"
92
-
93
- !tinyfish_api_key.to_s.empty?
131
+ backend == :tinyfish
94
132
  end
95
133
 
96
134
  # True when a SearXNG endpoint was configured explicitly (SEARXNG_URL
@@ -151,14 +189,15 @@ module Ask
151
189
  end
152
190
 
153
191
  # The full response: { results: [...], unresponsive: [[name,
154
- # reason], ...] }. Routes to TinyFish when #use_tinyfish?, falling
155
- # back to SearXNG (only when explicitly configured) if TinyFish
156
- # raises; otherwise straight to SearXNG — byte-for-byte the 0.6.x
157
- # behavior for keyless users. Same retry logic either way, but
158
- # preserves the engine diagnostics the caller needs to explain an
159
- # empty result. time_range / categories are normalized by the
160
- # backends that build requests — idempotent, so callers may pass
161
- # 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.
162
201
  def self.search_raw(query, time_range: nil, categories: nil)
163
202
  return searxng_search_raw(query, time_range: time_range, categories: categories) unless use_tinyfish?
164
203
 
@@ -207,6 +246,14 @@ module Ask
207
246
  # catches). time_range maps to recency_minutes; the FIRST
208
247
  # recognized categories value maps to domain_type.
209
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
+
210
257
  params = { query: query }
211
258
  normalized_range = normalize_time_range(time_range)
212
259
  params[:recency_minutes] = TIME_RANGE_TO_MINUTES[normalized_range] if normalized_range
@@ -224,7 +271,7 @@ module Ask
224
271
  http.open_timeout = 5
225
272
  http.read_timeout = 10
226
273
  req = Net::HTTP::Get.new(uri)
227
- req["X-API-Key"] = tinyfish_api_key
274
+ req["X-API-Key"] = key
228
275
  req["User-Agent"] = "ask-web-search/#{Ask::WebSearch::VERSION}"
229
276
  res = http.request(req)
230
277
  raise Error, "TinyFish returned #{res.code}: #{res.body[0, 200]}" unless res.code.start_with?("2")
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.8.0
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kaka Ruto
@@ -93,11 +93,12 @@ dependencies:
93
93
  - - "~>"
94
94
  - !ruby/object:Gem::Version
95
95
  version: '13.0'
96
- description: 'Web search returning results as clean numbered markdown. Two backends:
97
- TinyFish''s hosted API (primary with TINYFISH_API_KEY — no self-hosting) and a local
98
- SearXNG instance (SEARXNG_URL, also the automatic fallback). The capability layer
99
- (WebSearch.search); the native Ask::Tools::WebSearch agent tool is an optional integration
100
- 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.'
101
102
  email:
102
103
  - kaka@myrrlabs.com
103
104
  executables: []