@allansantos-dev/smart-tool 0.9.0 → 0.9.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/CHANGELOG.md +50 -23
- package/NOTICE +32 -32
- package/README.md +117 -116
- package/atomic_io.py +94 -94
- package/bin/smart-tool.mjs +5 -1
- package/client_hooks.py +192 -192
- package/code_graph_java.mjs +49 -49
- package/code_graph_js.cjs +135 -135
- package/duplicates.py +313 -313
- package/embedding_cache.py +99 -99
- package/endpoint_sync.py +179 -179
- package/gateway.py +220 -220
- package/hook_decision.py +96 -96
- package/index_inventory.py +75 -75
- package/indexer.py +878 -878
- package/install.py +113 -11
- package/install_runtime.py +101 -35
- package/integration_metrics.py +82 -82
- package/local_embedder.py +172 -172
- package/model_defaults.py +70 -70
- package/package.json +35 -35
- package/project_insights_ui.py +38 -38
- package/project_monitor.py +204 -204
- package/project_store.py +220 -220
- package/projects_ui.py +146 -146
- package/requirements.txt +8 -8
- package/research_cache.py +171 -171
- package/research_metrics.py +110 -110
- package/router.py +582 -582
- package/secret_store.py +121 -121
- package/setup_ui.py +1011 -986
- package/smart_tool_daemon.py +3604 -3604
- package/version.py +1 -1
- package/web_adapters/browser/fetch_camoufox.py +69 -0
- package/web_adapters/node/ddg-search.mjs +89 -89
- package/web_adapters/node/open-websearch-adapter.mjs +77 -77
- package/web_adapters/node/package.json +12 -12
- package/web_adapters/node/wikipedia-search.mjs +44 -44
- package/web_search_adapters.py +193 -193
- package/web_search_external.py +288 -288
- package/web_search_health.py +88 -88
package/web_search_adapters.py
CHANGED
|
@@ -1,193 +1,193 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Registro único dos adaptadores de motor de busca do `web_search`. Cada adaptador é um
|
|
3
|
-
subprocesso independente (executável + script) que recebe `[query, "--num", n, *extra_args]`
|
|
4
|
-
e devolve `[{title, url, snippet}, ...]` em JSON no stdout — mesmo contrato pra todos, então
|
|
5
|
-
`smart_tool_daemon._handle_web_search` itera a lista sem precisar saber o que cada um faz por
|
|
6
|
-
dentro. Acrescentar/remover/reordenar um motor é só editar `ADAPTERS`; `router.py` monta o
|
|
7
|
-
prompt/schema do modelo de roteamento a partir do campo `hint` de cada entrada, então também
|
|
8
|
-
não precisa mudar por adaptador novo.
|
|
9
|
-
|
|
10
|
-
Três etapas, marcadas por `fan_out` e `fallback_group` (ver ADR 0006):
|
|
11
|
-
|
|
12
|
-
- `fan_out=True` ("duckduckgo", "open-websearch", "wikipedia", "yahoo", "exa", "parallel", "keenable",
|
|
13
|
-
"google"): baratos e rápidos — `_handle_web_search` dispara todos em paralelo (google pelo navegador
|
|
14
|
-
residente). exa/parallel/keenable seguem o padrão de Hermes e OpenCode: plano público sem chave, chave
|
|
15
|
-
opcional do usuário no mesmo endpoint. e combina os resultados
|
|
16
|
-
de quem responder a tempo, pra garantir diversidade de fonte em toda chamada em vez de
|
|
17
|
-
parar no primeiro que responder.
|
|
18
|
-
- `fallback_group="http"` ("tavily", "firecrawl"): APIs com cota gratuita sem chave
|
|
19
|
-
e chave pessoal opcional; entram sob demanda se a cobertura ainda for insuficiente.
|
|
20
|
-
- `fallback_group="browser"` ("camoufox", "startpage"): navegador real, mais lento;
|
|
21
|
-
entra só quando as fontes anteriores ainda forem insuficientes, em sequência.
|
|
22
|
-
- `service_engine`: o tier roda no navegador Camoufox residente (browser_service.py), uma
|
|
23
|
-
instância com abas reaproveitadas para todos os consumidores, em vez de um script por chamada.
|
|
24
|
-
|
|
25
|
-
Sem entrada pra Bing: por períodos ele devolve um SERP genérico da entidade (ou de outro
|
|
26
|
-
assunto), sempre com 200 e sem erro. Em 2026-10-02 um cliente HTTP com TLS de navegador
|
|
27
|
-
recebeu resultados reais (44 de 50); em 2026-10-03 o mesmo cliente, o Camoufox e o Chromium
|
|
28
|
-
receberam a isca. Por não lançar erro, mascararia qualquer fallback depois dele; o índice do
|
|
29
|
-
Bing entra pelo tier yahoo.
|
|
30
|
-
"""
|
|
31
|
-
import os
|
|
32
|
-
import sys
|
|
33
|
-
|
|
34
|
-
PACKAGE_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
35
|
-
NODE_ADAPTER_DIR = os.path.join(PACKAGE_DIR, "web_adapters", "node")
|
|
36
|
-
BROWSER_ADAPTER_DIR = os.path.join(PACKAGE_DIR, "web_adapters", "browser")
|
|
37
|
-
LOCAL_NODE_EXECUTABLE = os.path.join(NODE_ADAPTER_DIR, ".node-runtime", "node.exe")
|
|
38
|
-
NODE_EXECUTABLE = LOCAL_NODE_EXECUTABLE if os.path.isfile(LOCAL_NODE_EXECUTABLE) else "node"
|
|
39
|
-
CAMOUFOX_VENV_PYTHON = os.path.join(BROWSER_ADAPTER_DIR, ".venv", "Scripts", "python.exe")
|
|
40
|
-
BROWSER_SERVICE_SCRIPT = os.path.join(BROWSER_ADAPTER_DIR, "browser_service.py")
|
|
41
|
-
DDG_SEARCH_SCRIPT = os.path.join(NODE_ADAPTER_DIR, "ddg-search.mjs")
|
|
42
|
-
OPEN_WEBSEARCH_ADAPTER_SCRIPT = os.path.join(NODE_ADAPTER_DIR, "open-websearch-adapter.mjs")
|
|
43
|
-
WIKIPEDIA_SEARCH_SCRIPT = os.path.join(NODE_ADAPTER_DIR, "wikipedia-search.mjs")
|
|
44
|
-
EXTERNAL_SEARCH_SCRIPT = os.path.join(PACKAGE_DIR, "web_search_external.py")
|
|
45
|
-
DDGS_SEARCH_SCRIPT = os.path.join(PACKAGE_DIR, "web_search_ddgs.py")
|
|
46
|
-
|
|
47
|
-
FETCH_TIMEOUT_S = 15
|
|
48
|
-
OPEN_WEBSEARCH_TIMEOUT_S = 30
|
|
49
|
-
WIKIPEDIA_TIMEOUT_S = 15
|
|
50
|
-
EXTERNAL_TIMEOUT_S = 18
|
|
51
|
-
DDGS_TIMEOUT_S = 15
|
|
52
|
-
GOOGLE_TIMEOUT_S = 12
|
|
53
|
-
BROWSER_TIMEOUT_S = 60
|
|
54
|
-
# O desafio do Startpage continua caro: duas buscas reais isoladas em 2026-09-29
|
|
55
|
-
# retornaram resultados em 101s e 154s, mesmo com o cookie curto da primeira ainda
|
|
56
|
-
# válido na segunda. Nenhum cookie semanal do Anubis foi emitido; o pré-aquecimento
|
|
57
|
-
# periódico foi removido porque não evitava o custo da busca seguinte. Este tier só
|
|
58
|
-
# roda como último fallback quando as fontes anteriores não cobrem a consulta.
|
|
59
|
-
STARTPAGE_TIMEOUT_S = 240
|
|
60
|
-
|
|
61
|
-
ADAPTERS = [
|
|
62
|
-
{
|
|
63
|
-
"name": "duckduckgo",
|
|
64
|
-
"executable": NODE_EXECUTABLE,
|
|
65
|
-
"script": DDG_SEARCH_SCRIPT,
|
|
66
|
-
"extra_args": [],
|
|
67
|
-
"timeout_s": FETCH_TIMEOUT_S,
|
|
68
|
-
"fan_out": True,
|
|
69
|
-
# Em inglês (não PT-BR): entra literal no system prompt do router_model em
|
|
70
|
-
# router.py, que é contrato com o modelo — ver convenção de idioma do AGENTS.md.
|
|
71
|
-
"hint": "plain fetch against DuckDuckGo HTML — fastest and cheapest, but fails "
|
|
72
|
-
"when DDG serves its JS anti-bot challenge (fetch never executes JS)",
|
|
73
|
-
},
|
|
74
|
-
{
|
|
75
|
-
"name": "open-websearch",
|
|
76
|
-
"executable": NODE_EXECUTABLE,
|
|
77
|
-
"script": OPEN_WEBSEARCH_ADAPTER_SCRIPT,
|
|
78
|
-
"extra_args": ["--engine", "duckduckgo"],
|
|
79
|
-
"timeout_s": OPEN_WEBSEARCH_TIMEOUT_S,
|
|
80
|
-
"fan_out": True,
|
|
81
|
-
"hint": "same DuckDuckGo index, via the open-websearch npm package's own HTTP "
|
|
82
|
-
"client — no browser, cheaper than camoufox; different access "
|
|
83
|
-
"technique than the plain-fetch duckduckgo tier",
|
|
84
|
-
},
|
|
85
|
-
{
|
|
86
|
-
"name": "wikipedia",
|
|
87
|
-
"executable": NODE_EXECUTABLE,
|
|
88
|
-
"script": WIKIPEDIA_SEARCH_SCRIPT,
|
|
89
|
-
"extra_args": [],
|
|
90
|
-
"timeout_s": WIKIPEDIA_TIMEOUT_S,
|
|
91
|
-
"fan_out": True,
|
|
92
|
-
"hint": "official Wikipedia search API, not a search-engine scrape — genuinely "
|
|
93
|
-
"different index (encyclopedic), no anti-bot risk at all, but narrow: "
|
|
94
|
-
"useless for time-sensitive queries (prices, news, weather)",
|
|
95
|
-
},
|
|
96
|
-
{
|
|
97
|
-
"name": "yahoo",
|
|
98
|
-
"executable": sys.executable,
|
|
99
|
-
"script": DDGS_SEARCH_SCRIPT,
|
|
100
|
-
"extra_args": ["--backend", "yahoo"],
|
|
101
|
-
"timeout_s": DDGS_TIMEOUT_S,
|
|
102
|
-
"fan_out": True,
|
|
103
|
-
"hint": "Yahoo SERP (Bing index) via the ddgs library — a different index than the "
|
|
104
|
-
"DuckDuckGo tiers, ~1 s, keyless; intermittently returns nothing",
|
|
105
|
-
},
|
|
106
|
-
{
|
|
107
|
-
"name": "exa",
|
|
108
|
-
"executable": sys.executable,
|
|
109
|
-
"script": EXTERNAL_SEARCH_SCRIPT,
|
|
110
|
-
"extra_args": ["--provider", "exa"],
|
|
111
|
-
"timeout_s": EXTERNAL_TIMEOUT_S,
|
|
112
|
-
"fan_out": True,
|
|
113
|
-
"hint": "Exa neural index via its public MCP (keyless free tier, optional user key) — strong on official docs and primary sources",
|
|
114
|
-
},
|
|
115
|
-
{
|
|
116
|
-
"name": "parallel",
|
|
117
|
-
"executable": sys.executable,
|
|
118
|
-
"script": EXTERNAL_SEARCH_SCRIPT,
|
|
119
|
-
"extra_args": ["--provider", "parallel"],
|
|
120
|
-
"timeout_s": EXTERNAL_TIMEOUT_S,
|
|
121
|
-
"fan_out": True,
|
|
122
|
-
"hint": "Parallel index via its public search MCP (keyless free tier, optional user key) — ranked excerpts, ~1 s",
|
|
123
|
-
},
|
|
124
|
-
{
|
|
125
|
-
"name": "keenable",
|
|
126
|
-
"executable": sys.executable,
|
|
127
|
-
"script": EXTERNAL_SEARCH_SCRIPT,
|
|
128
|
-
"extra_args": ["--provider", "keenable"],
|
|
129
|
-
"timeout_s": EXTERNAL_TIMEOUT_S,
|
|
130
|
-
"fan_out": True,
|
|
131
|
-
"hint": "Keenable independent index (own crawler and ranking) via its public endpoint, optional user key — ~0.3 s",
|
|
132
|
-
},
|
|
133
|
-
{
|
|
134
|
-
"name": "google",
|
|
135
|
-
"service_engine": "google",
|
|
136
|
-
"timeout_s": GOOGLE_TIMEOUT_S,
|
|
137
|
-
"fan_out": True,
|
|
138
|
-
"hint": "Google SERP via the resident Camoufox browser — best index, ~1-2 s once the "
|
|
139
|
-
"browser is open; one search at a time, paused for an hour after a captcha",
|
|
140
|
-
},
|
|
141
|
-
{
|
|
142
|
-
"name": "tavily",
|
|
143
|
-
"executable": sys.executable,
|
|
144
|
-
"script": EXTERNAL_SEARCH_SCRIPT,
|
|
145
|
-
"extra_args": ["--provider", "tavily"],
|
|
146
|
-
"timeout_s": EXTERNAL_TIMEOUT_S,
|
|
147
|
-
"fan_out": False,
|
|
148
|
-
"fallback_group": "http",
|
|
149
|
-
"hint": "independent ranked web search via Tavily; keyless quota is rate-limited, "
|
|
150
|
-
"so use only when existing results are incomplete or irrelevant",
|
|
151
|
-
},
|
|
152
|
-
{
|
|
153
|
-
"name": "firecrawl",
|
|
154
|
-
"executable": sys.executable,
|
|
155
|
-
"script": EXTERNAL_SEARCH_SCRIPT,
|
|
156
|
-
"extra_args": ["--provider", "firecrawl"],
|
|
157
|
-
"timeout_s": EXTERNAL_TIMEOUT_S,
|
|
158
|
-
"fan_out": False,
|
|
159
|
-
"fallback_group": "http",
|
|
160
|
-
"hint": "independent Firecrawl search API with clean snippets; keyless quota is "
|
|
161
|
-
"rate-limited, so use only when cheaper results remain insufficient",
|
|
162
|
-
},
|
|
163
|
-
{
|
|
164
|
-
"name": "camoufox",
|
|
165
|
-
"service_engine": "duckduckgo",
|
|
166
|
-
"timeout_s": BROWSER_TIMEOUT_S,
|
|
167
|
-
"fan_out": False,
|
|
168
|
-
"fallback_group": "browser",
|
|
169
|
-
"hint": "same DuckDuckGo index, via the resident Camoufox browser — solves the "
|
|
170
|
-
"anti-bot challenge the duckduckgo and open-websearch tiers can't",
|
|
171
|
-
},
|
|
172
|
-
{
|
|
173
|
-
"name": "startpage",
|
|
174
|
-
"service_engine": "startpage",
|
|
175
|
-
"timeout_s": STARTPAGE_TIMEOUT_S,
|
|
176
|
-
"fan_out": False,
|
|
177
|
-
"fallback_group": "browser",
|
|
178
|
-
"hint": "different index (Startpage, via the resident Camoufox browser) — the only fallback "
|
|
179
|
-
"that survives DuckDuckGo itself being down, not just the access "
|
|
180
|
-
"technique; subject to burst rate-limiting",
|
|
181
|
-
},
|
|
182
|
-
]
|
|
183
|
-
|
|
184
|
-
# Merge order of the round-robin. Measured 2026-10-03 (docs/spikes/merge_order_spike.py, 40 real queries, same
|
|
185
|
-
# collected results): this order beat the previous random order 16 x 5 (19 ties), SearXNG consensus scoring 14 x 13.
|
|
186
|
-
MERGE_PRIORITY = ["exa", "parallel", "google", "tavily", "firecrawl", "camoufox", "startpage", "keenable", "yahoo",
|
|
187
|
-
"duckduckgo", "open-websearch", "wikipedia", "memoria"]
|
|
188
|
-
DEFAULT_TIER_ORDER = [a["name"] for a in ADAPTERS]
|
|
189
|
-
SPECS = {a["name"]: a for a in ADAPTERS}
|
|
190
|
-
FAN_OUT_TIERS = [a["name"] for a in ADAPTERS if a["fan_out"]]
|
|
191
|
-
FALLBACK_TIERS = [a["name"] for a in ADAPTERS if not a["fan_out"]]
|
|
192
|
-
HTTP_FALLBACK_TIERS = [a["name"] for a in ADAPTERS if a.get("fallback_group") == "http"]
|
|
193
|
-
BROWSER_FALLBACK_TIERS = [a["name"] for a in ADAPTERS if a.get("fallback_group") == "browser"]
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Registro único dos adaptadores de motor de busca do `web_search`. Cada adaptador é um
|
|
3
|
+
subprocesso independente (executável + script) que recebe `[query, "--num", n, *extra_args]`
|
|
4
|
+
e devolve `[{title, url, snippet}, ...]` em JSON no stdout — mesmo contrato pra todos, então
|
|
5
|
+
`smart_tool_daemon._handle_web_search` itera a lista sem precisar saber o que cada um faz por
|
|
6
|
+
dentro. Acrescentar/remover/reordenar um motor é só editar `ADAPTERS`; `router.py` monta o
|
|
7
|
+
prompt/schema do modelo de roteamento a partir do campo `hint` de cada entrada, então também
|
|
8
|
+
não precisa mudar por adaptador novo.
|
|
9
|
+
|
|
10
|
+
Três etapas, marcadas por `fan_out` e `fallback_group` (ver ADR 0006):
|
|
11
|
+
|
|
12
|
+
- `fan_out=True` ("duckduckgo", "open-websearch", "wikipedia", "yahoo", "exa", "parallel", "keenable",
|
|
13
|
+
"google"): baratos e rápidos — `_handle_web_search` dispara todos em paralelo (google pelo navegador
|
|
14
|
+
residente). exa/parallel/keenable seguem o padrão de Hermes e OpenCode: plano público sem chave, chave
|
|
15
|
+
opcional do usuário no mesmo endpoint. e combina os resultados
|
|
16
|
+
de quem responder a tempo, pra garantir diversidade de fonte em toda chamada em vez de
|
|
17
|
+
parar no primeiro que responder.
|
|
18
|
+
- `fallback_group="http"` ("tavily", "firecrawl"): APIs com cota gratuita sem chave
|
|
19
|
+
e chave pessoal opcional; entram sob demanda se a cobertura ainda for insuficiente.
|
|
20
|
+
- `fallback_group="browser"` ("camoufox", "startpage"): navegador real, mais lento;
|
|
21
|
+
entra só quando as fontes anteriores ainda forem insuficientes, em sequência.
|
|
22
|
+
- `service_engine`: o tier roda no navegador Camoufox residente (browser_service.py), uma
|
|
23
|
+
instância com abas reaproveitadas para todos os consumidores, em vez de um script por chamada.
|
|
24
|
+
|
|
25
|
+
Sem entrada pra Bing: por períodos ele devolve um SERP genérico da entidade (ou de outro
|
|
26
|
+
assunto), sempre com 200 e sem erro. Em 2026-10-02 um cliente HTTP com TLS de navegador
|
|
27
|
+
recebeu resultados reais (44 de 50); em 2026-10-03 o mesmo cliente, o Camoufox e o Chromium
|
|
28
|
+
receberam a isca. Por não lançar erro, mascararia qualquer fallback depois dele; o índice do
|
|
29
|
+
Bing entra pelo tier yahoo.
|
|
30
|
+
"""
|
|
31
|
+
import os
|
|
32
|
+
import sys
|
|
33
|
+
|
|
34
|
+
PACKAGE_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
35
|
+
NODE_ADAPTER_DIR = os.path.join(PACKAGE_DIR, "web_adapters", "node")
|
|
36
|
+
BROWSER_ADAPTER_DIR = os.path.join(PACKAGE_DIR, "web_adapters", "browser")
|
|
37
|
+
LOCAL_NODE_EXECUTABLE = os.path.join(NODE_ADAPTER_DIR, ".node-runtime", "node.exe")
|
|
38
|
+
NODE_EXECUTABLE = LOCAL_NODE_EXECUTABLE if os.path.isfile(LOCAL_NODE_EXECUTABLE) else "node"
|
|
39
|
+
CAMOUFOX_VENV_PYTHON = os.path.join(BROWSER_ADAPTER_DIR, ".venv", "Scripts", "python.exe")
|
|
40
|
+
BROWSER_SERVICE_SCRIPT = os.path.join(BROWSER_ADAPTER_DIR, "browser_service.py")
|
|
41
|
+
DDG_SEARCH_SCRIPT = os.path.join(NODE_ADAPTER_DIR, "ddg-search.mjs")
|
|
42
|
+
OPEN_WEBSEARCH_ADAPTER_SCRIPT = os.path.join(NODE_ADAPTER_DIR, "open-websearch-adapter.mjs")
|
|
43
|
+
WIKIPEDIA_SEARCH_SCRIPT = os.path.join(NODE_ADAPTER_DIR, "wikipedia-search.mjs")
|
|
44
|
+
EXTERNAL_SEARCH_SCRIPT = os.path.join(PACKAGE_DIR, "web_search_external.py")
|
|
45
|
+
DDGS_SEARCH_SCRIPT = os.path.join(PACKAGE_DIR, "web_search_ddgs.py")
|
|
46
|
+
|
|
47
|
+
FETCH_TIMEOUT_S = 15
|
|
48
|
+
OPEN_WEBSEARCH_TIMEOUT_S = 30
|
|
49
|
+
WIKIPEDIA_TIMEOUT_S = 15
|
|
50
|
+
EXTERNAL_TIMEOUT_S = 18
|
|
51
|
+
DDGS_TIMEOUT_S = 15
|
|
52
|
+
GOOGLE_TIMEOUT_S = 12
|
|
53
|
+
BROWSER_TIMEOUT_S = 60
|
|
54
|
+
# O desafio do Startpage continua caro: duas buscas reais isoladas em 2026-09-29
|
|
55
|
+
# retornaram resultados em 101s e 154s, mesmo com o cookie curto da primeira ainda
|
|
56
|
+
# válido na segunda. Nenhum cookie semanal do Anubis foi emitido; o pré-aquecimento
|
|
57
|
+
# periódico foi removido porque não evitava o custo da busca seguinte. Este tier só
|
|
58
|
+
# roda como último fallback quando as fontes anteriores não cobrem a consulta.
|
|
59
|
+
STARTPAGE_TIMEOUT_S = 240
|
|
60
|
+
|
|
61
|
+
ADAPTERS = [
|
|
62
|
+
{
|
|
63
|
+
"name": "duckduckgo",
|
|
64
|
+
"executable": NODE_EXECUTABLE,
|
|
65
|
+
"script": DDG_SEARCH_SCRIPT,
|
|
66
|
+
"extra_args": [],
|
|
67
|
+
"timeout_s": FETCH_TIMEOUT_S,
|
|
68
|
+
"fan_out": True,
|
|
69
|
+
# Em inglês (não PT-BR): entra literal no system prompt do router_model em
|
|
70
|
+
# router.py, que é contrato com o modelo — ver convenção de idioma do AGENTS.md.
|
|
71
|
+
"hint": "plain fetch against DuckDuckGo HTML — fastest and cheapest, but fails "
|
|
72
|
+
"when DDG serves its JS anti-bot challenge (fetch never executes JS)",
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"name": "open-websearch",
|
|
76
|
+
"executable": NODE_EXECUTABLE,
|
|
77
|
+
"script": OPEN_WEBSEARCH_ADAPTER_SCRIPT,
|
|
78
|
+
"extra_args": ["--engine", "duckduckgo"],
|
|
79
|
+
"timeout_s": OPEN_WEBSEARCH_TIMEOUT_S,
|
|
80
|
+
"fan_out": True,
|
|
81
|
+
"hint": "same DuckDuckGo index, via the open-websearch npm package's own HTTP "
|
|
82
|
+
"client — no browser, cheaper than camoufox; different access "
|
|
83
|
+
"technique than the plain-fetch duckduckgo tier",
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
"name": "wikipedia",
|
|
87
|
+
"executable": NODE_EXECUTABLE,
|
|
88
|
+
"script": WIKIPEDIA_SEARCH_SCRIPT,
|
|
89
|
+
"extra_args": [],
|
|
90
|
+
"timeout_s": WIKIPEDIA_TIMEOUT_S,
|
|
91
|
+
"fan_out": True,
|
|
92
|
+
"hint": "official Wikipedia search API, not a search-engine scrape — genuinely "
|
|
93
|
+
"different index (encyclopedic), no anti-bot risk at all, but narrow: "
|
|
94
|
+
"useless for time-sensitive queries (prices, news, weather)",
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
"name": "yahoo",
|
|
98
|
+
"executable": sys.executable,
|
|
99
|
+
"script": DDGS_SEARCH_SCRIPT,
|
|
100
|
+
"extra_args": ["--backend", "yahoo"],
|
|
101
|
+
"timeout_s": DDGS_TIMEOUT_S,
|
|
102
|
+
"fan_out": True,
|
|
103
|
+
"hint": "Yahoo SERP (Bing index) via the ddgs library — a different index than the "
|
|
104
|
+
"DuckDuckGo tiers, ~1 s, keyless; intermittently returns nothing",
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
"name": "exa",
|
|
108
|
+
"executable": sys.executable,
|
|
109
|
+
"script": EXTERNAL_SEARCH_SCRIPT,
|
|
110
|
+
"extra_args": ["--provider", "exa"],
|
|
111
|
+
"timeout_s": EXTERNAL_TIMEOUT_S,
|
|
112
|
+
"fan_out": True,
|
|
113
|
+
"hint": "Exa neural index via its public MCP (keyless free tier, optional user key) — strong on official docs and primary sources",
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
"name": "parallel",
|
|
117
|
+
"executable": sys.executable,
|
|
118
|
+
"script": EXTERNAL_SEARCH_SCRIPT,
|
|
119
|
+
"extra_args": ["--provider", "parallel"],
|
|
120
|
+
"timeout_s": EXTERNAL_TIMEOUT_S,
|
|
121
|
+
"fan_out": True,
|
|
122
|
+
"hint": "Parallel index via its public search MCP (keyless free tier, optional user key) — ranked excerpts, ~1 s",
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
"name": "keenable",
|
|
126
|
+
"executable": sys.executable,
|
|
127
|
+
"script": EXTERNAL_SEARCH_SCRIPT,
|
|
128
|
+
"extra_args": ["--provider", "keenable"],
|
|
129
|
+
"timeout_s": EXTERNAL_TIMEOUT_S,
|
|
130
|
+
"fan_out": True,
|
|
131
|
+
"hint": "Keenable independent index (own crawler and ranking) via its public endpoint, optional user key — ~0.3 s",
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
"name": "google",
|
|
135
|
+
"service_engine": "google",
|
|
136
|
+
"timeout_s": GOOGLE_TIMEOUT_S,
|
|
137
|
+
"fan_out": True,
|
|
138
|
+
"hint": "Google SERP via the resident Camoufox browser — best index, ~1-2 s once the "
|
|
139
|
+
"browser is open; one search at a time, paused for an hour after a captcha",
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
"name": "tavily",
|
|
143
|
+
"executable": sys.executable,
|
|
144
|
+
"script": EXTERNAL_SEARCH_SCRIPT,
|
|
145
|
+
"extra_args": ["--provider", "tavily"],
|
|
146
|
+
"timeout_s": EXTERNAL_TIMEOUT_S,
|
|
147
|
+
"fan_out": False,
|
|
148
|
+
"fallback_group": "http",
|
|
149
|
+
"hint": "independent ranked web search via Tavily; keyless quota is rate-limited, "
|
|
150
|
+
"so use only when existing results are incomplete or irrelevant",
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
"name": "firecrawl",
|
|
154
|
+
"executable": sys.executable,
|
|
155
|
+
"script": EXTERNAL_SEARCH_SCRIPT,
|
|
156
|
+
"extra_args": ["--provider", "firecrawl"],
|
|
157
|
+
"timeout_s": EXTERNAL_TIMEOUT_S,
|
|
158
|
+
"fan_out": False,
|
|
159
|
+
"fallback_group": "http",
|
|
160
|
+
"hint": "independent Firecrawl search API with clean snippets; keyless quota is "
|
|
161
|
+
"rate-limited, so use only when cheaper results remain insufficient",
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
"name": "camoufox",
|
|
165
|
+
"service_engine": "duckduckgo",
|
|
166
|
+
"timeout_s": BROWSER_TIMEOUT_S,
|
|
167
|
+
"fan_out": False,
|
|
168
|
+
"fallback_group": "browser",
|
|
169
|
+
"hint": "same DuckDuckGo index, via the resident Camoufox browser — solves the "
|
|
170
|
+
"anti-bot challenge the duckduckgo and open-websearch tiers can't",
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
"name": "startpage",
|
|
174
|
+
"service_engine": "startpage",
|
|
175
|
+
"timeout_s": STARTPAGE_TIMEOUT_S,
|
|
176
|
+
"fan_out": False,
|
|
177
|
+
"fallback_group": "browser",
|
|
178
|
+
"hint": "different index (Startpage, via the resident Camoufox browser) — the only fallback "
|
|
179
|
+
"that survives DuckDuckGo itself being down, not just the access "
|
|
180
|
+
"technique; subject to burst rate-limiting",
|
|
181
|
+
},
|
|
182
|
+
]
|
|
183
|
+
|
|
184
|
+
# Merge order of the round-robin. Measured 2026-10-03 (docs/spikes/merge_order_spike.py, 40 real queries, same
|
|
185
|
+
# collected results): this order beat the previous random order 16 x 5 (19 ties), SearXNG consensus scoring 14 x 13.
|
|
186
|
+
MERGE_PRIORITY = ["exa", "parallel", "google", "tavily", "firecrawl", "camoufox", "startpage", "keenable", "yahoo",
|
|
187
|
+
"duckduckgo", "open-websearch", "wikipedia", "memoria"]
|
|
188
|
+
DEFAULT_TIER_ORDER = [a["name"] for a in ADAPTERS]
|
|
189
|
+
SPECS = {a["name"]: a for a in ADAPTERS}
|
|
190
|
+
FAN_OUT_TIERS = [a["name"] for a in ADAPTERS if a["fan_out"]]
|
|
191
|
+
FALLBACK_TIERS = [a["name"] for a in ADAPTERS if not a["fan_out"]]
|
|
192
|
+
HTTP_FALLBACK_TIERS = [a["name"] for a in ADAPTERS if a.get("fallback_group") == "http"]
|
|
193
|
+
BROWSER_FALLBACK_TIERS = [a["name"] for a in ADAPTERS if a.get("fallback_group") == "browser"]
|