ask-web-search 0.5.0 → 0.6.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 +4 -4
- data/README.md +32 -0
- data/lib/ask/web_search/tool.rb +13 -3
- data/lib/ask/web_search/version.rb +1 -1
- data/lib/ask/web_search.rb +64 -12
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8843934817bb67da278cc649beebc5beb3ed6af85653209511108f72f06b1da5
|
|
4
|
+
data.tar.gz: c2f2ab3b68106eb47806824b7e8d86153cf934159024535be0c7eb3b3bbc4f5b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: df4ec625002df7c953171cea42654a4b4b5ab10fa365cea9ab149991100913d5b8982d1d14d1204a1a596f00c847dc1cb95714309383cb3496913aa7d8137c78
|
|
7
|
+
data.tar.gz: d1212989bbf9cc31e15acd4f43153fff0c57c23038ccb46a4c557955d62d129ed9ecd0cfaf21c92508d1b985cbb5742dd1849cc2dbc235fa6f49b9109ddfd8a8
|
data/README.md
CHANGED
|
@@ -61,6 +61,38 @@ Results are returned as a numbered markdown-like string:
|
|
|
61
61
|
|
|
62
62
|
If no results are found, returns `"No results found."`.
|
|
63
63
|
|
|
64
|
+
## Freshness windows and verticals
|
|
65
|
+
|
|
66
|
+
`search` (and `search_results` / `search_raw`, and both tool framings)
|
|
67
|
+
accept two optional parameters:
|
|
68
|
+
|
|
69
|
+
```ruby
|
|
70
|
+
Ask::WebSearch.search("ruby 4.0 release notes", time_range: "month")
|
|
71
|
+
Ask::WebSearch.search("fed policy", categories: "news")
|
|
72
|
+
Ask::WebSearch.search("retrieval augmented generation", categories: "science")
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
- **`time_range:`** restricts results to a SearXNG freshness window —
|
|
76
|
+
`day`, `week`, `month`, or `year`. Anything else raises `ArgumentError`
|
|
77
|
+
naming the valid values. When a windowed search comes back cleanly
|
|
78
|
+
empty, the result says so — "No results found within the day freshness
|
|
79
|
+
window. Retry with a broader time_range or without one." — instead of a
|
|
80
|
+
bare "No results found.", and engine-failure diagnostics suggest a
|
|
81
|
+
broader window. Caveat: SearXNG delegates date filtering to the engines,
|
|
82
|
+
and as of SearXNG 2026.6.x every date-capable engine returns zero
|
|
83
|
+
results under a date filter, so windows currently fail soft with that
|
|
84
|
+
message. No gem change is needed when the engines are fixed upstream.
|
|
85
|
+
- **`categories:`** scopes the search to a SearXNG category — `news`,
|
|
86
|
+
`science` (research papers), or any category the instance configures.
|
|
87
|
+
Accepts a string or anything Array-able (`[:news, :science]` →
|
|
88
|
+
`news,science`). Values pass through unvalidated: instances configure
|
|
89
|
+
their own category set, and an unknown category degrades to a clean
|
|
90
|
+
empty result rather than a failure. The instance must have vertical
|
|
91
|
+
engines enabled — the `searxng/` compose config in this repository
|
|
92
|
+
enables bing news + google news (news) and arxiv + pubmed (science);
|
|
93
|
+
without them SearXNG silently resolves the request against the general
|
|
94
|
+
engines.
|
|
95
|
+
|
|
64
96
|
## Full documentation
|
|
65
97
|
|
|
66
98
|
The full ask-rb documentation lives at https://ask-rb.github.io/ask-docs.
|
data/lib/ask/web_search/tool.rb
CHANGED
|
@@ -24,13 +24,23 @@ module Ask
|
|
|
24
24
|
params(
|
|
25
25
|
type: "object",
|
|
26
26
|
properties: {
|
|
27
|
-
query: { type: "string", description: "The search query" }
|
|
27
|
+
query: { type: "string", description: "The search query" },
|
|
28
|
+
time_range: {
|
|
29
|
+
type: "string",
|
|
30
|
+
enum: Ask::WebSearch::TIME_RANGES,
|
|
31
|
+
description: "Freshness window: only results from the past day, week, month, or year. Use for recency-sensitive queries (news, prices, releases); omit otherwise."
|
|
32
|
+
},
|
|
33
|
+
categories: {
|
|
34
|
+
type: "string",
|
|
35
|
+
enum: %w[general news science],
|
|
36
|
+
description: "Vertical to search: general web results (default), news, or science (research papers). Use news for current events, science for papers."
|
|
37
|
+
}
|
|
28
38
|
},
|
|
29
39
|
required: ["query"]
|
|
30
40
|
)
|
|
31
41
|
|
|
32
|
-
def execute(query:)
|
|
33
|
-
Ask::Result.ok(data: Ask::WebSearch.search(query))
|
|
42
|
+
def execute(query:, time_range: nil, categories: nil)
|
|
43
|
+
Ask::Result.ok(data: Ask::WebSearch.search(query, time_range: time_range, categories: categories))
|
|
34
44
|
end
|
|
35
45
|
end
|
|
36
46
|
end
|
data/lib/ask/web_search.rb
CHANGED
|
@@ -36,6 +36,12 @@ module Ask
|
|
|
36
36
|
DEFAULT_MAX_RETRIES = 3
|
|
37
37
|
RETRY_BACKOFF = [0.5, 1.0, 2.0].freeze
|
|
38
38
|
|
|
39
|
+
# SearXNG's freshness windows — the valid values of its time_range
|
|
40
|
+
# search parameter. Anything else is rejected up front, so a bad value
|
|
41
|
+
# fails with a message listing these instead of silently searching
|
|
42
|
+
# unfiltered.
|
|
43
|
+
TIME_RANGES = %w[day week month year].freeze
|
|
44
|
+
|
|
39
45
|
def self.max_retries
|
|
40
46
|
return @max_retries if defined?(@max_retries)
|
|
41
47
|
|
|
@@ -52,27 +58,49 @@ module Ask
|
|
|
52
58
|
# AllEnginesFailedError with per-engine diagnostics (see
|
|
53
59
|
# #format_engine_error). Raises on connection/HTTP failures — the
|
|
54
60
|
# caller decides how to surface them.
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
61
|
+
#
|
|
62
|
+
# time_range: restricts results to a freshness window — one of
|
|
63
|
+
# TIME_RANGES (day, week, month, year). With a window set, a clean
|
|
64
|
+
# zero-result search says so ("No results found within the day
|
|
65
|
+
# freshness window...") instead of a bare "No results found.", so the
|
|
66
|
+
# agent widens the window rather than concluding the web is silent.
|
|
67
|
+
# categories: scopes the search to a SearXNG vertical — e.g. "news"
|
|
68
|
+
# or "science" (research papers); instances configure their own set,
|
|
69
|
+
# so values pass through unvalidated. A string or anything Array-able
|
|
70
|
+
# ("news,science" form).
|
|
71
|
+
def self.search(query, time_range: nil, categories: nil)
|
|
72
|
+
time_range = normalize_time_range(time_range)
|
|
73
|
+
categories = normalize_categories(categories)
|
|
74
|
+
response = search_raw(query, time_range: time_range, categories: categories)
|
|
75
|
+
raise AllEnginesFailedError, format_engine_error(query, response, time_range: time_range) if response[:results].empty? &&
|
|
58
76
|
response[:unresponsive].any?
|
|
59
77
|
|
|
60
|
-
format_results(response[:results])
|
|
78
|
+
return format_results(response[:results]) unless response[:results].empty? && time_range
|
|
79
|
+
|
|
80
|
+
"No results found within the #{time_range} freshness window. Retry with a broader time_range or without one."
|
|
61
81
|
end
|
|
62
82
|
|
|
63
83
|
# The raw result list: { url:, title:, content: } entries from the
|
|
64
84
|
# results and infoboxes, deduplicated by url. Retries up to
|
|
65
85
|
# max_retries times on connection/HTTP failures with exponential
|
|
66
|
-
# backoff. Set max_retries to 0 to disable.
|
|
67
|
-
|
|
68
|
-
|
|
86
|
+
# backoff. Set max_retries to 0 to disable. See #search for
|
|
87
|
+
# time_range / categories.
|
|
88
|
+
def self.search_results(query, time_range: nil, categories: nil)
|
|
89
|
+
search_raw(query, time_range: time_range, categories: categories)[:results]
|
|
69
90
|
end
|
|
70
91
|
|
|
71
92
|
# The full SearXNG response: { results: [...], unresponsive: [[name,
|
|
72
93
|
# reason], ...] }. Same retry logic as #search_results, but preserves
|
|
73
94
|
# the engine diagnostics the caller needs to explain an empty result.
|
|
74
|
-
|
|
75
|
-
|
|
95
|
+
# time_range / categories are normalized here — the single point that
|
|
96
|
+
# builds the request — and are idempotent, so callers may pass raw or
|
|
97
|
+
# already-normalized values.
|
|
98
|
+
def self.search_raw(query, time_range: nil, categories: nil)
|
|
99
|
+
params = { q: query, format: "json" }
|
|
100
|
+
params[:time_range] = normalize_time_range(time_range) if time_range
|
|
101
|
+
params[:categories] = normalize_categories(categories) if categories
|
|
102
|
+
uri = URI("#{searxng_url}/search")
|
|
103
|
+
uri.query = URI.encode_www_form(params)
|
|
76
104
|
retries = max_retries || 0
|
|
77
105
|
attempt = 0
|
|
78
106
|
begin
|
|
@@ -97,17 +125,41 @@ module Ask
|
|
|
97
125
|
# Formats the engine failure into an actionable message: which engines
|
|
98
126
|
# failed and why, plus what the agent can try next. Keeps the raw
|
|
99
127
|
# SearXNG reason strings (CAPTCHA, timeout, suspended) — the caller
|
|
100
|
-
# can see exactly what happened.
|
|
101
|
-
|
|
128
|
+
# can see exactly what happened. When a freshness window was set, the
|
|
129
|
+
# hint suggests broadening it.
|
|
130
|
+
def self.format_engine_error(query, response, time_range: nil)
|
|
102
131
|
lines = ["No search results for #{query.inspect} — all #{response[:unresponsive].size} engine(s) failed:"]
|
|
103
132
|
response[:unresponsive].each do |name, reason|
|
|
104
133
|
lines << "- #{name}: #{reason}"
|
|
105
134
|
end
|
|
106
|
-
|
|
135
|
+
hint = "Try: a simpler query, or ask-web-fetch for a known URL."
|
|
136
|
+
hint = "Try: a simpler query, a broader time_range, or ask-web-fetch for a known URL." if time_range
|
|
137
|
+
lines << hint
|
|
107
138
|
lines.join("\n")
|
|
108
139
|
end
|
|
109
140
|
private_class_method :format_engine_error
|
|
110
141
|
|
|
142
|
+
# Validates +time_range+ against TIME_RANGES (nil/blank → nil, symbols
|
|
143
|
+
# accepted). Raises ArgumentError for anything else — the request is
|
|
144
|
+
# never sent.
|
|
145
|
+
def self.normalize_time_range(time_range)
|
|
146
|
+
value = time_range.to_s.strip.downcase
|
|
147
|
+
return nil if value.empty?
|
|
148
|
+
|
|
149
|
+
raise ArgumentError, "invalid time_range #{time_range.inspect} — use one of: #{TIME_RANGES.join(', ')}" unless TIME_RANGES.include?(value)
|
|
150
|
+
|
|
151
|
+
value
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Normalizes +categories+ to SearXNG's comma-separated form —
|
|
155
|
+
# ["news", :science] → "news,science"; nil/blank → nil. Values pass
|
|
156
|
+
# through unvalidated: SearXNG instances configure their own category
|
|
157
|
+
# set, and a bad one degrades to a clean empty result, not a failure.
|
|
158
|
+
def self.normalize_categories(categories)
|
|
159
|
+
value = Array(categories).map { |c| c.to_s.strip.downcase }.reject(&:empty?).uniq.join(",")
|
|
160
|
+
value.empty? ? nil : value
|
|
161
|
+
end
|
|
162
|
+
|
|
111
163
|
# Parses the full SearXNG JSON response into { results:, unresponsive: }.
|
|
112
164
|
# results are { url:, title:, content: } entries from results and
|
|
113
165
|
# infoboxes, deduplicated by url. unresponsive is the raw
|