ask-web-search 0.3.0 → 0.5.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: fe356e9cdc2a7ee736d51ad93d36dfcaaef72168b0a4b3d98bc19945b919dd4a
4
- data.tar.gz: e334c6903ab48de0a78fee36c07933487dcab49648cc31bbe3adfd46653de01a
3
+ metadata.gz: d8bd1c41d870cf5aa9f13332ba6b2d43fbcf6d6bbebb13e5390f2d9e8c0f6529
4
+ data.tar.gz: b1cc5035874cd122e9d4a8d1c4feec1bc1d146d12842714cbfe4672c43a4dba6
5
5
  SHA512:
6
- metadata.gz: d1fc95c583e7fc95a15ef32b7e1253302c6e2ba1edee41781445c912a1d8631cba839dac044fb94a1cc20d51d219960102cccec5d75eecbf1cef92de0f92e559
7
- data.tar.gz: f56f4d8df11e3de5de981e0f5d182d8491cb12840f12dc4e546986fd9c4f6b82668313cd4270468215f460e5a29ece4ecb0e28bc4c301c88dfd39dc1656ae7e6
6
+ metadata.gz: b80273643aa1f7e4a875832dcef9bfec8a2df3886af9be0f92651f55e8b6873364b59586a9bfa9e6ba5eb95394b686425ca7e7032ad69677ba1fab085fb42498
7
+ data.tar.gz: abae84930f02b3adbe06a136b676a79bb8e3ae2416aa7c94fd94da2d22862672ae35818b92434cb03a163e7a2770f63bfcfd7d10766a447a5f4de97fdf35a57d
@@ -1,5 +1,5 @@
1
1
  module Ask
2
2
  module WebSearch
3
- VERSION = "0.3.0"
3
+ VERSION = "0.5.0"
4
4
  end
5
5
  end
@@ -12,6 +12,16 @@ module Ask
12
12
  # framing — name, parameter schema, result wrapping — lives with the
13
13
  # consumers (the MCP server, the agents) that call this library.
14
14
  module WebSearch
15
+ # Search errors that carry engine diagnostics — the agent needs to
16
+ # know *which* engines failed and *why* (CAPTCHA, timeout, suspended)
17
+ # instead of just "No results found."
18
+ class Error < StandardError; end
19
+
20
+ # SearXNG answered but every engine failed (CAPTCHA, timeout,
21
+ # suspended). The message lists which engines failed and why, plus
22
+ # what the agent can try next.
23
+ class AllEnginesFailedError < Error; end
24
+
15
25
  # Local SearXNG endpoint. Defaults to the standard local instance;
16
26
  # override with SEARXNG_URL or WebSearch.searxng_url=.
17
27
  def self.searxng_url
@@ -22,29 +32,88 @@ module Ask
22
32
  @searxng_url = url
23
33
  end
24
34
 
25
- # Searches +query+ and returns the results as numbered markdown
26
- # ("No results found." when SearXNG found nothing). Raises on
27
- # connection/HTTP failures the caller decides how to surface them.
35
+ # Retry configuration. Set max_retries to 0 to disable retries.
36
+ DEFAULT_MAX_RETRIES = 3
37
+ RETRY_BACKOFF = [0.5, 1.0, 2.0].freeze
38
+
39
+ def self.max_retries
40
+ return @max_retries if defined?(@max_retries)
41
+
42
+ @max_retries = DEFAULT_MAX_RETRIES
43
+ end
44
+
45
+ def self.max_retries=(val)
46
+ @max_retries = val
47
+ end
48
+
49
+ # Searches +query+ and returns the results as numbered markdown.
50
+ # "No results found." only when SearXNG answered cleanly with zero
51
+ # results and no engine failures. When engines failed, raises
52
+ # AllEnginesFailedError with per-engine diagnostics (see
53
+ # #format_engine_error). Raises on connection/HTTP failures — the
54
+ # caller decides how to surface them.
28
55
  def self.search(query)
29
- format_results(search_results(query))
56
+ response = search_raw(query)
57
+ raise AllEnginesFailedError, format_engine_error(query, response) if response[:results].empty? &&
58
+ response[:unresponsive].any?
59
+
60
+ format_results(response[:results])
30
61
  end
31
62
 
32
63
  # The raw result list: { url:, title:, content: } entries from the
33
- # results and infoboxes, deduplicated by url.
64
+ # results and infoboxes, deduplicated by url. Retries up to
65
+ # max_retries times on connection/HTTP failures with exponential
66
+ # backoff. Set max_retries to 0 to disable.
34
67
  def self.search_results(query)
68
+ search_raw(query)[:results]
69
+ end
70
+
71
+ # The full SearXNG response: { results: [...], unresponsive: [[name,
72
+ # reason], ...] }. Same retry logic as #search_results, but preserves
73
+ # the engine diagnostics the caller needs to explain an empty result.
74
+ def self.search_raw(query)
35
75
  uri = URI("#{searxng_url}/search?q=#{URI.encode_www_form_component(query)}&format=json")
36
- http = Net::HTTP.new(uri.host, uri.port)
37
- http.open_timeout = 5
38
- http.read_timeout = 10
39
- req = Net::HTTP::Get.new(uri)
40
- req["User-Agent"] = "ask-web-search/#{Ask::WebSearch::VERSION}"
41
- res = http.request(req)
42
- raise "SearXNG returned #{res.code}: #{res.body}" unless res.code.start_with?("2")
43
-
44
- parse_results(JSON.parse(res.body))
76
+ retries = max_retries || 0
77
+ attempt = 0
78
+ begin
79
+ attempt += 1
80
+ http = Net::HTTP.new(uri.host, uri.port)
81
+ http.open_timeout = 5
82
+ http.read_timeout = 10
83
+ req = Net::HTTP::Get.new(uri)
84
+ req["User-Agent"] = "ask-web-search/#{Ask::WebSearch::VERSION}"
85
+ res = http.request(req)
86
+ raise "SearXNG returned #{res.code}: #{res.body}" unless res.code.start_with?("2")
87
+
88
+ parse_response(JSON.parse(res.body))
89
+ rescue StandardError
90
+ raise if attempt > retries
91
+
92
+ sleep RETRY_BACKOFF[[attempt - 1, RETRY_BACKOFF.size - 1].min]
93
+ retry
94
+ end
45
95
  end
46
96
 
47
- def self.parse_results(data)
97
+ # Formats the engine failure into an actionable message: which engines
98
+ # failed and why, plus what the agent can try next. Keeps the raw
99
+ # SearXNG reason strings (CAPTCHA, timeout, suspended) — the caller
100
+ # can see exactly what happened.
101
+ def self.format_engine_error(query, response)
102
+ lines = ["No search results for #{query.inspect} — all #{response[:unresponsive].size} engine(s) failed:"]
103
+ response[:unresponsive].each do |name, reason|
104
+ lines << "- #{name}: #{reason}"
105
+ end
106
+ lines << "Try: a simpler query, or ask-web-fetch for a known URL."
107
+ lines.join("\n")
108
+ end
109
+ private_class_method :format_engine_error
110
+
111
+ # Parses the full SearXNG JSON response into { results:, unresponsive: }.
112
+ # results are { url:, title:, content: } entries from results and
113
+ # infoboxes, deduplicated by url. unresponsive is the raw
114
+ # [[engine_name, reason], ...] list SearXNG returns for failed
115
+ # engines (e.g. [["duckduckgo", "CAPTCHA"], ["mojeek", "timeout"]]).
116
+ def self.parse_response(data)
48
117
  results = []
49
118
  data.fetch("results", []).each do |r|
50
119
  results << { url: r["url"], title: r["title"], content: r["content"] }
@@ -52,7 +121,15 @@ module Ask
52
121
  data.fetch("infoboxes", []).each do |ib|
53
122
  results << { url: ib["id"], title: ib["infobox"], content: ib["content"] }
54
123
  end
55
- results.uniq { |r| r[:url] }
124
+ {
125
+ results: results.uniq { |r| r[:url] },
126
+ unresponsive: Array(data.fetch("unresponsive_engines", []))
127
+ }
128
+ end
129
+ private_class_method :parse_response
130
+
131
+ def self.parse_results(data)
132
+ parse_response(data)[:results]
56
133
  end
57
134
  private_class_method :parse_results
58
135
 
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.3.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kaka Ruto
@@ -116,7 +116,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
116
116
  - !ruby/object:Gem::Version
117
117
  version: '0'
118
118
  requirements: []
119
- rubygems_version: 4.0.3
119
+ rubygems_version: 4.0.18
120
120
  specification_version: 4
121
121
  summary: Web search library for the ask-rb ecosystem
122
122
  test_files: []