web-search-plus-plugin 1.2.1 → 1.2.2
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.
- package/index.ts +44 -2
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/scripts/search.py +60 -28
package/index.ts
CHANGED
|
@@ -88,10 +88,36 @@ export default function (api: any) {
|
|
|
88
88
|
],
|
|
89
89
|
{
|
|
90
90
|
description:
|
|
91
|
-
"Exa search depth: 'deep' synthesizes across sources (4-12s), 'deep-reasoning' for complex cross-reference analysis (12-50s).
|
|
91
|
+
"Exa search depth: 'deep' synthesizes across sources (4-12s), 'deep-reasoning' for complex cross-reference analysis (12-50s). When provider is auto, depth may be auto-selected based on query complexity.",
|
|
92
92
|
},
|
|
93
93
|
),
|
|
94
94
|
),
|
|
95
|
+
time_range: Type.Optional(
|
|
96
|
+
Type.Union(
|
|
97
|
+
[
|
|
98
|
+
Type.Literal("day"),
|
|
99
|
+
Type.Literal("week"),
|
|
100
|
+
Type.Literal("month"),
|
|
101
|
+
Type.Literal("year"),
|
|
102
|
+
],
|
|
103
|
+
{
|
|
104
|
+
description:
|
|
105
|
+
"Filter results by recency. Applies to Serper (as tbs), Perplexity (as search_recency_filter), Tavily/You.com (as freshness). Useful for news and current events.",
|
|
106
|
+
},
|
|
107
|
+
),
|
|
108
|
+
),
|
|
109
|
+
include_domains: Type.Optional(
|
|
110
|
+
Type.Array(Type.String(), {
|
|
111
|
+
description:
|
|
112
|
+
"Only include results from these domains (e.g. ['arxiv.org', 'github.com']). Supported by Tavily and Exa.",
|
|
113
|
+
}),
|
|
114
|
+
),
|
|
115
|
+
exclude_domains: Type.Optional(
|
|
116
|
+
Type.Array(Type.String(), {
|
|
117
|
+
description:
|
|
118
|
+
"Exclude results from these domains (e.g. ['reddit.com', 'pinterest.com']). Supported by Tavily and Exa.",
|
|
119
|
+
}),
|
|
120
|
+
),
|
|
95
121
|
}),
|
|
96
122
|
async execute(
|
|
97
123
|
_id: string,
|
|
@@ -100,6 +126,9 @@ export default function (api: any) {
|
|
|
100
126
|
provider?: string;
|
|
101
127
|
count?: number;
|
|
102
128
|
depth?: string;
|
|
129
|
+
time_range?: string;
|
|
130
|
+
include_domains?: string[];
|
|
131
|
+
exclude_domains?: string[];
|
|
103
132
|
},
|
|
104
133
|
) {
|
|
105
134
|
const args = [scriptPath, "--query", params.query, "--compact"];
|
|
@@ -119,6 +148,19 @@ export default function (api: any) {
|
|
|
119
148
|
args.push("--exa-depth", params.depth);
|
|
120
149
|
}
|
|
121
150
|
|
|
151
|
+
if (params.time_range) {
|
|
152
|
+
args.push("--time-range", params.time_range);
|
|
153
|
+
args.push("--freshness", params.time_range);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (params.include_domains?.length) {
|
|
157
|
+
args.push("--include-domains", ...params.include_domains);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (params.exclude_domains?.length) {
|
|
161
|
+
args.push("--exclude-domains", ...params.exclude_domains);
|
|
162
|
+
}
|
|
163
|
+
|
|
122
164
|
const envPaths = [
|
|
123
165
|
path.join(PLUGIN_DIR, ".env"),
|
|
124
166
|
path.join(PLUGIN_DIR, "..", "web-search-plus", ".env"),
|
|
@@ -131,7 +173,7 @@ export default function (api: any) {
|
|
|
131
173
|
|
|
132
174
|
try {
|
|
133
175
|
const child = spawnSync("python3", args, {
|
|
134
|
-
timeout:
|
|
176
|
+
timeout: 75000,
|
|
135
177
|
env: childEnv,
|
|
136
178
|
shell: false,
|
|
137
179
|
encoding: "utf8",
|
package/openclaw.plugin.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"id": "web-search-plus-plugin",
|
|
3
3
|
"kind": "skill",
|
|
4
4
|
"name": "Web Search Plus",
|
|
5
|
-
"version": "1.2.
|
|
5
|
+
"version": "1.2.2",
|
|
6
6
|
"description": "Multi-provider web search (Serper/Google, Tavily, Exa/Neural+Deep, Perplexity, You.com, SearXNG) with intelligent auto-routing",
|
|
7
7
|
"configSchema": {
|
|
8
8
|
"type": "object",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "web-search-plus-plugin",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.2",
|
|
4
4
|
"description": "OpenClaw plugin: multi-provider web search (Serper/Google, Tavily, Exa/Neural+Deep, Perplexity, You.com, SearXNG) with intelligent auto-routing",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
package/scripts/search.py
CHANGED
|
@@ -459,17 +459,15 @@ def validate_api_key(provider: str, config: Dict[str, Any] = None) -> str:
|
|
|
459
459
|
],
|
|
460
460
|
"provider": provider
|
|
461
461
|
}
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
462
|
+
raise ProviderConfigError(json.dumps(error_msg))
|
|
463
|
+
|
|
465
464
|
# Validate URL format
|
|
466
465
|
if not key.startswith(("http://", "https://")):
|
|
467
|
-
|
|
466
|
+
raise ProviderConfigError(json.dumps({
|
|
468
467
|
"error": "SearXNG instance URL must start with http:// or https://",
|
|
469
468
|
"provided": key,
|
|
470
469
|
"provider": provider
|
|
471
|
-
}
|
|
472
|
-
sys.exit(1)
|
|
470
|
+
}))
|
|
473
471
|
|
|
474
472
|
return key
|
|
475
473
|
|
|
@@ -500,16 +498,14 @@ def validate_api_key(provider: str, config: Dict[str, Any] = None) -> str:
|
|
|
500
498
|
],
|
|
501
499
|
"provider": provider
|
|
502
500
|
}
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
501
|
+
raise ProviderConfigError(json.dumps(error_msg))
|
|
502
|
+
|
|
506
503
|
if len(key) < 10:
|
|
507
|
-
|
|
504
|
+
raise ProviderConfigError(json.dumps({
|
|
508
505
|
"error": f"API key for {provider} appears invalid (too short)",
|
|
509
506
|
"provider": provider
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
|
|
507
|
+
}))
|
|
508
|
+
|
|
513
509
|
return key
|
|
514
510
|
|
|
515
511
|
|
|
@@ -1370,6 +1366,11 @@ def explain_routing(query: str, config: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
1370
1366
|
|
|
1371
1367
|
|
|
1372
1368
|
|
|
1369
|
+
class ProviderConfigError(Exception):
|
|
1370
|
+
"""Raised when a provider is missing or has an invalid API key/config."""
|
|
1371
|
+
pass
|
|
1372
|
+
|
|
1373
|
+
|
|
1373
1374
|
class ProviderRequestError(Exception):
|
|
1374
1375
|
"""Structured provider error with retry/cooldown metadata."""
|
|
1375
1376
|
|
|
@@ -1437,6 +1438,24 @@ def reset_provider_health(provider: str) -> None:
|
|
|
1437
1438
|
_save_provider_health(state)
|
|
1438
1439
|
|
|
1439
1440
|
|
|
1441
|
+
def _title_from_url(url: str) -> str:
|
|
1442
|
+
"""Derive a readable title from a URL when none is provided."""
|
|
1443
|
+
try:
|
|
1444
|
+
parsed = urlparse(url)
|
|
1445
|
+
domain = parsed.netloc.replace("www.", "")
|
|
1446
|
+
# Use last meaningful path segment as context
|
|
1447
|
+
segments = [s for s in parsed.path.strip("/").split("/") if s]
|
|
1448
|
+
if segments:
|
|
1449
|
+
last = segments[-1].replace("-", " ").replace("_", " ")
|
|
1450
|
+
# Strip file extensions
|
|
1451
|
+
last = re.sub(r'\.\w{2,4}$', '', last)
|
|
1452
|
+
if last:
|
|
1453
|
+
return f"{domain} — {last[:80]}"
|
|
1454
|
+
return domain
|
|
1455
|
+
except Exception:
|
|
1456
|
+
return url[:60]
|
|
1457
|
+
|
|
1458
|
+
|
|
1440
1459
|
def normalize_result_url(url: str) -> str:
|
|
1441
1460
|
if not url:
|
|
1442
1461
|
return ""
|
|
@@ -1759,7 +1778,7 @@ def search_exa(
|
|
|
1759
1778
|
results.append({
|
|
1760
1779
|
"title": f"Exa {exa_depth.replace('-', ' ').title()} Synthesis",
|
|
1761
1780
|
"url": "",
|
|
1762
|
-
"snippet": synthesized_text
|
|
1781
|
+
"snippet": synthesized_text,
|
|
1763
1782
|
"full_synthesis": synthesized_text,
|
|
1764
1783
|
"score": 1.0,
|
|
1765
1784
|
"grounding": grounding_citations[:10],
|
|
@@ -1781,7 +1800,7 @@ def search_exa(
|
|
|
1781
1800
|
"type": "source",
|
|
1782
1801
|
})
|
|
1783
1802
|
|
|
1784
|
-
answer = synthesized_text
|
|
1803
|
+
answer = synthesized_text if synthesized_text else (results[1]["snippet"] if len(results) > 1 else "")
|
|
1785
1804
|
|
|
1786
1805
|
return {
|
|
1787
1806
|
"provider": "exa",
|
|
@@ -1876,13 +1895,17 @@ def search_perplexity(
|
|
|
1876
1895
|
message = choices[0].get("message", {}) if choices else {}
|
|
1877
1896
|
answer = (message.get("content") or "").strip()
|
|
1878
1897
|
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1898
|
+
# Prefer the structured citations array from Perplexity API response
|
|
1899
|
+
api_citations = data.get("citations", [])
|
|
1900
|
+
|
|
1901
|
+
# Fallback: extract URLs from answer text if API doesn't provide citations
|
|
1902
|
+
if not api_citations:
|
|
1903
|
+
api_citations = []
|
|
1904
|
+
seen = set()
|
|
1905
|
+
for u in re.findall(r"https?://[^\s)\]}>\"']+", answer):
|
|
1906
|
+
if u not in seen:
|
|
1907
|
+
seen.add(u)
|
|
1908
|
+
api_citations.append(u)
|
|
1886
1909
|
|
|
1887
1910
|
results = []
|
|
1888
1911
|
|
|
@@ -1897,12 +1920,19 @@ def search_perplexity(
|
|
|
1897
1920
|
"score": 1.0,
|
|
1898
1921
|
})
|
|
1899
1922
|
|
|
1900
|
-
#
|
|
1901
|
-
for i,
|
|
1923
|
+
# Source results from citations
|
|
1924
|
+
for i, citation in enumerate(api_citations[:max_results - 1]):
|
|
1925
|
+
# citations can be plain URL strings or dicts with url/title
|
|
1926
|
+
if isinstance(citation, str):
|
|
1927
|
+
url = citation
|
|
1928
|
+
title = _title_from_url(url)
|
|
1929
|
+
else:
|
|
1930
|
+
url = citation.get("url", "")
|
|
1931
|
+
title = citation.get("title") or _title_from_url(url)
|
|
1902
1932
|
results.append({
|
|
1903
|
-
"title":
|
|
1904
|
-
"url":
|
|
1905
|
-
"snippet": "
|
|
1933
|
+
"title": title,
|
|
1934
|
+
"url": url,
|
|
1935
|
+
"snippet": f"Source cited in Perplexity answer [citation {i+1}]",
|
|
1906
1936
|
"score": round(0.9 - i * 0.1, 3),
|
|
1907
1937
|
})
|
|
1908
1938
|
|
|
@@ -2494,9 +2524,11 @@ Full docs: See README.md and SKILL.md
|
|
|
2494
2524
|
disabled_providers = auto_config.get("disabled_providers", [])
|
|
2495
2525
|
|
|
2496
2526
|
# Start with the selected provider, then try others in priority order
|
|
2527
|
+
# Only include providers that have a configured API key (except the primary,
|
|
2528
|
+
# which gets a clear error if unconfigured and no fallback succeeds)
|
|
2497
2529
|
providers_to_try = [provider]
|
|
2498
2530
|
for p in provider_priority:
|
|
2499
|
-
if p not in providers_to_try and p not in disabled_providers:
|
|
2531
|
+
if p not in providers_to_try and p not in disabled_providers and get_api_key(p, config):
|
|
2500
2532
|
providers_to_try.append(p)
|
|
2501
2533
|
|
|
2502
2534
|
# Skip providers currently in cooldown
|