@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.
Files changed (41) hide show
  1. package/CHANGELOG.md +50 -23
  2. package/NOTICE +32 -32
  3. package/README.md +117 -116
  4. package/atomic_io.py +94 -94
  5. package/bin/smart-tool.mjs +5 -1
  6. package/client_hooks.py +192 -192
  7. package/code_graph_java.mjs +49 -49
  8. package/code_graph_js.cjs +135 -135
  9. package/duplicates.py +313 -313
  10. package/embedding_cache.py +99 -99
  11. package/endpoint_sync.py +179 -179
  12. package/gateway.py +220 -220
  13. package/hook_decision.py +96 -96
  14. package/index_inventory.py +75 -75
  15. package/indexer.py +878 -878
  16. package/install.py +113 -11
  17. package/install_runtime.py +101 -35
  18. package/integration_metrics.py +82 -82
  19. package/local_embedder.py +172 -172
  20. package/model_defaults.py +70 -70
  21. package/package.json +35 -35
  22. package/project_insights_ui.py +38 -38
  23. package/project_monitor.py +204 -204
  24. package/project_store.py +220 -220
  25. package/projects_ui.py +146 -146
  26. package/requirements.txt +8 -8
  27. package/research_cache.py +171 -171
  28. package/research_metrics.py +110 -110
  29. package/router.py +582 -582
  30. package/secret_store.py +121 -121
  31. package/setup_ui.py +1011 -986
  32. package/smart_tool_daemon.py +3604 -3604
  33. package/version.py +1 -1
  34. package/web_adapters/browser/fetch_camoufox.py +69 -0
  35. package/web_adapters/node/ddg-search.mjs +89 -89
  36. package/web_adapters/node/open-websearch-adapter.mjs +77 -77
  37. package/web_adapters/node/package.json +12 -12
  38. package/web_adapters/node/wikipedia-search.mjs +44 -44
  39. package/web_search_adapters.py +193 -193
  40. package/web_search_external.py +288 -288
  41. package/web_search_health.py +88 -88
@@ -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"]