argo-search 1.0.1
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/LICENSE +21 -0
- package/README.md +410 -0
- package/backends/domain_profiles.json +364 -0
- package/backends/engine_registry.yaml +505 -0
- package/backends/quota_profiles.json +395 -0
- package/bin/argo.js +54 -0
- package/config.yaml +554 -0
- package/package.json +43 -0
- package/scripts/adaptive.py +179 -0
- package/scripts/benchmark.py +124 -0
- package/scripts/cache.py +374 -0
- package/scripts/clarify.py +689 -0
- package/scripts/config.py +262 -0
- package/scripts/crawl.py +73 -0
- package/scripts/engines.py +386 -0
- package/scripts/evidence.py +381 -0
- package/scripts/extract.py +69 -0
- package/scripts/fetch.py +118 -0
- package/scripts/health_check.py +437 -0
- package/scripts/health_probe.py +218 -0
- package/scripts/mcp_diag.py +81 -0
- package/scripts/mcp_server.py +488 -0
- package/scripts/query_rewriter.py +278 -0
- package/scripts/quota.py +196 -0
- package/scripts/research.py +499 -0
- package/scripts/route.py +341 -0
- package/scripts/search.py +508 -0
- package/scripts/search_types.py +72 -0
- package/scripts/tfidf_router.py +312 -0
- package/sub-skills/local-search/SKILL.md +104 -0
- package/sub-skills/local-search/config.yaml +328 -0
- package/sub-skills/local-search/engine_registry.py +298 -0
- package/sub-skills/local-search/health_check.py +347 -0
- package/sub-skills/local-search/local_search_adapter.py +56 -0
- package/sub-skills/local-search/parse_maps.yaml +184 -0
- package/sub-skills/local-search/search_v3.py +558 -0
- package/sub-skills/local-search/smart_router.py +215 -0
package/scripts/route.py
ADDED
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
route.py — Unified Search v2 三层路由决策
|
|
4
|
+
|
|
5
|
+
路由策略:
|
|
6
|
+
1. 用户指定引擎 → 直接返回
|
|
7
|
+
2. TF-IDF 语义路由(二元组 + boost + cost + quota)
|
|
8
|
+
3. 正则硬规则匹配(config.yaml domains)
|
|
9
|
+
4. 融合决策:正则 + TF-IDF 验证 → 高置信度
|
|
10
|
+
5. budget 模式:过滤付费引擎
|
|
11
|
+
|
|
12
|
+
每种决策都带 reason 字符串。
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import re
|
|
18
|
+
import time
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
try:
|
|
22
|
+
from config import load_config, get_engines, get_domains, get_cost_factor
|
|
23
|
+
from tfidf_router import semantic_route, get_router
|
|
24
|
+
from quota import get_quota_manager
|
|
25
|
+
except ImportError:
|
|
26
|
+
import sys
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
sys.path.insert(0, str(Path(__file__).parent))
|
|
29
|
+
from config import load_config, get_engines, get_domains, get_cost_factor
|
|
30
|
+
from tfidf_router import semantic_route, get_router
|
|
31
|
+
from quota import get_quota_manager
|
|
32
|
+
|
|
33
|
+
# 自适应学习(可选依赖)
|
|
34
|
+
try:
|
|
35
|
+
from adaptive import get_learner
|
|
36
|
+
_adaptive_learner = get_learner()
|
|
37
|
+
except Exception:
|
|
38
|
+
_adaptive_learner = None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# ── 特征提取 ──────────────────────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
_RE_CHINESE = re.compile(r"[一-鿿]")
|
|
44
|
+
_RE_COMPARE = re.compile(r"\b(vs|versus)\b|(对比|比较|区别|相比|哪个好)", re.I)
|
|
45
|
+
_RE_TECH = re.compile(
|
|
46
|
+
r"\b(api|python|javascript|typescript|code|react|vue|node|rust|go|"
|
|
47
|
+
r"golang|docker|kubernetes|linux|git|sql|error|bug|debug|exception|"
|
|
48
|
+
r"function|class|async|thread|database|algorithm|programming|framework|library)\b|"
|
|
49
|
+
r"(函数|方法|类|库|框架|报错|调试|编程|代码|开发|技术|源码|架构)", re.I)
|
|
50
|
+
_RE_QUESTION = re.compile(
|
|
51
|
+
r"\b(how|what|why|when|where|which|who)\b|"
|
|
52
|
+
r"(怎么|什么|为什么|如何|哪里|哪个|谁|多少|几|吗|呢)", re.I)
|
|
53
|
+
_RE_DEPTH = re.compile(
|
|
54
|
+
r"\b(deep|comprehensive|review|survey|research|paper|thesis)\b|"
|
|
55
|
+
r"(对比分析|深度|全面|详细|深入|系统|完整|综述|研究|探究|详解|论文)", re.I)
|
|
56
|
+
|
|
57
|
+
_ENGINE_NAMES = {
|
|
58
|
+
"anysearch": "AnySearch", "wigolo": "Wigolo", "tavily": "Tavily",
|
|
59
|
+
"zhihu": "知乎", "eastmoney": "东方财富", "byted": "字节搜索",
|
|
60
|
+
"arxiv": "arXiv", "searxng": "SearXNG", "felo": "Felo",
|
|
61
|
+
"bocha": "博查", "openalex": "OpenAlex", "crossref": "Crossref",
|
|
62
|
+
"github": "GitHub", "wikipedia": "Wikipedia", "metaso": "秘塔",
|
|
63
|
+
"wolframalpha": "WolframAlpha", "brave": "Brave",
|
|
64
|
+
"duckduckgo": "DuckDuckGo", "uapi": "UAPI", "semantic_scholar": "Semantic Scholar",
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def extract_features(query: str) -> dict[str, Any]:
|
|
69
|
+
"""提取查询特征向量。"""
|
|
70
|
+
total = len(query)
|
|
71
|
+
chinese = len(_RE_CHINESE.findall(query))
|
|
72
|
+
ratio = chinese / max(total, 1)
|
|
73
|
+
return {
|
|
74
|
+
"chinese_ratio": ratio,
|
|
75
|
+
"english_ratio": 1.0 - ratio,
|
|
76
|
+
"length": total,
|
|
77
|
+
"has_compare": bool(_RE_COMPARE.search(query)),
|
|
78
|
+
"has_technical": bool(_RE_TECH.search(query)),
|
|
79
|
+
"has_question": bool(_RE_QUESTION.search(query)),
|
|
80
|
+
"has_depth_word": bool(_RE_DEPTH.search(query)),
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _feature_labels(features: dict[str, Any]) -> str:
|
|
85
|
+
labels = []
|
|
86
|
+
cr = features.get("chinese_ratio", 0)
|
|
87
|
+
if cr > 0.6:
|
|
88
|
+
labels.append("中文")
|
|
89
|
+
elif cr < 0.1:
|
|
90
|
+
labels.append("英文")
|
|
91
|
+
for key, name in (("has_technical", "技术向"), ("has_compare", "对比分析"),
|
|
92
|
+
("has_depth_word", "深度研究"), ("has_question", "问答型")):
|
|
93
|
+
if features.get(key):
|
|
94
|
+
labels.append(name)
|
|
95
|
+
return " + ".join(labels) if labels else "通用查询"
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
# ── 域匹配 ────────────────────────────────────────────────────────────────────
|
|
99
|
+
|
|
100
|
+
def _compile_domain_patterns(domains: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
101
|
+
compiled = []
|
|
102
|
+
for idx, domain in enumerate(domains):
|
|
103
|
+
patterns = domain.get("patterns", [])
|
|
104
|
+
if isinstance(patterns, str):
|
|
105
|
+
patterns = []
|
|
106
|
+
regexes = []
|
|
107
|
+
for p in patterns:
|
|
108
|
+
try:
|
|
109
|
+
regexes.append(re.compile(p))
|
|
110
|
+
except re.error:
|
|
111
|
+
continue
|
|
112
|
+
compiled.append({**domain, "_idx": idx, "_compiled": regexes})
|
|
113
|
+
return compiled
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def match_domain(query: str, domains: list[dict[str, Any]] | None = None) -> dict[str, Any] | None:
|
|
117
|
+
"""按 config.yaml domains 顺序匹配;无命中返回 catch-all。"""
|
|
118
|
+
if domains is None:
|
|
119
|
+
domains = get_domains()
|
|
120
|
+
compiled = _compile_domain_patterns(domains)
|
|
121
|
+
catch_all: dict[str, Any] | None = None
|
|
122
|
+
for domain in compiled:
|
|
123
|
+
if not domain.get("patterns", []):
|
|
124
|
+
catch_all = domain
|
|
125
|
+
continue
|
|
126
|
+
for regex in domain["_compiled"]:
|
|
127
|
+
if regex.search(query):
|
|
128
|
+
return domain
|
|
129
|
+
return catch_all
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _get_engines_combo(domain: dict[str, Any], enabled: set[str], mode: str = "auto") -> list[str]:
|
|
133
|
+
"""从域配置获取 engines_combo,过滤不可用/付费(budget 模式)。"""
|
|
134
|
+
combo = domain.get("engines_combo", [])
|
|
135
|
+
if combo:
|
|
136
|
+
filtered = [e for e in combo if e in enabled]
|
|
137
|
+
else:
|
|
138
|
+
primary = domain.get("primary", "anysearch")
|
|
139
|
+
fallback = domain.get("fallback")
|
|
140
|
+
engines = [primary]
|
|
141
|
+
if fallback and fallback != primary:
|
|
142
|
+
engines.append(fallback)
|
|
143
|
+
filtered = [e for e in engines if e in enabled]
|
|
144
|
+
|
|
145
|
+
# budget/fast 模式过滤付费引擎
|
|
146
|
+
if mode in ("fast", "budget"):
|
|
147
|
+
quota_mgr = get_quota_manager()
|
|
148
|
+
filtered = [e for e in filtered if quota_mgr.is_available(e, mode=mode)]
|
|
149
|
+
|
|
150
|
+
# fast/budget 模式优先前置 local_search(零成本兜底)
|
|
151
|
+
if mode in ("fast", "budget") and "local_search" in enabled and "local_search" not in filtered:
|
|
152
|
+
filtered.insert(0, "local_search")
|
|
153
|
+
|
|
154
|
+
# fast 模式:只保留免费/本地引擎,最多 2 个,确保速度和零成本
|
|
155
|
+
if mode == "fast":
|
|
156
|
+
from config import get_cost_factor
|
|
157
|
+
filtered = [e for e in filtered if get_cost_factor(e) >= 0.85]
|
|
158
|
+
if "local_search" in filtered:
|
|
159
|
+
filtered.remove("local_search")
|
|
160
|
+
filtered.insert(0, "local_search")
|
|
161
|
+
filtered = filtered[:2]
|
|
162
|
+
|
|
163
|
+
# 自适应学习过滤:排除近期表现差的引擎(成功率 < 30% 或综合评分 < 0.3)
|
|
164
|
+
if _adaptive_learner is not None and len(filtered) > 1:
|
|
165
|
+
original = filtered[:]
|
|
166
|
+
filtered = [e for e in filtered if _adaptive_learner.get_score(e) >= 0.3]
|
|
167
|
+
if not filtered:
|
|
168
|
+
filtered = original
|
|
169
|
+
elif len(filtered) < len(original) and "local_search" in enabled and "local_search" not in filtered:
|
|
170
|
+
filtered.append("local_search")
|
|
171
|
+
|
|
172
|
+
# 健康探针过滤:排除已知不可达的 HTTP 引擎
|
|
173
|
+
try:
|
|
174
|
+
from health_probe import get_engine_status
|
|
175
|
+
healthy = [e for e in filtered if get_engine_status(e).get("available", True)]
|
|
176
|
+
if healthy:
|
|
177
|
+
filtered = healthy
|
|
178
|
+
except Exception:
|
|
179
|
+
pass # 健康探针模块不可用时跳过
|
|
180
|
+
|
|
181
|
+
return filtered
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
# ── 路由主函数 ─────────────────────────────────────────────────────────────────
|
|
185
|
+
|
|
186
|
+
def route_query(query: str, engine_override: str = "auto",
|
|
187
|
+
mode: str = "auto") -> dict[str, Any]:
|
|
188
|
+
"""路由决策主函数。
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
query: 查询词
|
|
192
|
+
engine_override: 用户指定引擎
|
|
193
|
+
mode: 预算模式 (fast/auto/deep/budget)
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
dict: {engine, engines, engines_combo, reason, confidence, domain, ...}
|
|
197
|
+
"""
|
|
198
|
+
start = time.perf_counter()
|
|
199
|
+
|
|
200
|
+
def _done(**kw: Any) -> dict[str, Any]:
|
|
201
|
+
base = {"elapsed_ms": round((time.perf_counter() - start) * 1000, 3)}
|
|
202
|
+
base.update(kw)
|
|
203
|
+
return base
|
|
204
|
+
|
|
205
|
+
if engine_override != "auto":
|
|
206
|
+
return _done(
|
|
207
|
+
engine=engine_override, engines=[engine_override],
|
|
208
|
+
engines_combo=[engine_override],
|
|
209
|
+
reason=f"用户指定: {engine_override}", confidence=1.0,
|
|
210
|
+
features={}, domain=None, parallel=False, mode=mode,
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
features = extract_features(query)
|
|
214
|
+
cfg = load_config()
|
|
215
|
+
enabled = set(get_engines(cfg).keys())
|
|
216
|
+
|
|
217
|
+
# TF-IDF 语义路由
|
|
218
|
+
tfidf_best = None
|
|
219
|
+
tfidf_scores = []
|
|
220
|
+
try:
|
|
221
|
+
tfidf_scores = semantic_route(query, top_k=3)
|
|
222
|
+
if tfidf_scores:
|
|
223
|
+
tfidf_best = tfidf_scores[0][0]
|
|
224
|
+
except Exception:
|
|
225
|
+
pass
|
|
226
|
+
|
|
227
|
+
# 预算模式过滤可用引擎
|
|
228
|
+
quota_mgr = get_quota_manager()
|
|
229
|
+
if mode in ("fast", "budget"):
|
|
230
|
+
enabled = {e for e in enabled if quota_mgr.is_available(e, mode=mode)}
|
|
231
|
+
|
|
232
|
+
# 正则硬规则匹配
|
|
233
|
+
domain = match_domain(query, get_domains(cfg))
|
|
234
|
+
|
|
235
|
+
if domain:
|
|
236
|
+
engines_combo = _get_engines_combo(domain, enabled, mode)
|
|
237
|
+
if not engines_combo:
|
|
238
|
+
# 域内引擎全被过滤,回退
|
|
239
|
+
engines_combo = [e for e in ["local_search", "anysearch", "duckduckgo"] if e in enabled]
|
|
240
|
+
if not engines_combo:
|
|
241
|
+
engines_combo = sorted(enabled)[:2] if enabled else ["anysearch"]
|
|
242
|
+
|
|
243
|
+
# TF-IDF 验证 + catch-all 修复
|
|
244
|
+
tfidf_best_score = tfidf_scores[0][1] if tfidf_scores else 0.0
|
|
245
|
+
is_catch_all = not domain.get("patterns", []) # 无模式 = 兜底域
|
|
246
|
+
|
|
247
|
+
if tfidf_best and tfidf_best in engines_combo:
|
|
248
|
+
confidence = 0.95
|
|
249
|
+
elif tfidf_best and tfidf_best != engines_combo[0]:
|
|
250
|
+
confidence = 0.8
|
|
251
|
+
# catch-all 域 + TF-IDF 高置信度推荐 → 注入推荐引擎到首位
|
|
252
|
+
if is_catch_all and tfidf_best_score > 0.15 and tfidf_best in enabled:
|
|
253
|
+
engines_combo = [tfidf_best] + [e for e in engines_combo if e != tfidf_best]
|
|
254
|
+
confidence = 0.85
|
|
255
|
+
else:
|
|
256
|
+
confidence = 0.9
|
|
257
|
+
# catch-all 域 + TF-IDF 推荐但不在 combo 中 → 前置
|
|
258
|
+
if is_catch_all and tfidf_best and tfidf_best_score > 0.15 and tfidf_best in enabled:
|
|
259
|
+
engines_combo.insert(0, tfidf_best)
|
|
260
|
+
confidence = 0.8
|
|
261
|
+
|
|
262
|
+
parallel = bool(domain.get("parallel", False)) or len(engines_combo) > 2
|
|
263
|
+
# fast 模式强制串行,先 local_search 成功即避免额外 HTTP 开销
|
|
264
|
+
if mode == "fast":
|
|
265
|
+
parallel = False
|
|
266
|
+
|
|
267
|
+
return _done(
|
|
268
|
+
engine=engines_combo[0],
|
|
269
|
+
engines=engines_combo,
|
|
270
|
+
engines_combo=engines_combo,
|
|
271
|
+
engines_fallback=[e for e in enabled if e not in engines_combo],
|
|
272
|
+
reason=(
|
|
273
|
+
f"{_feature_labels(features)} → 命中域 [{domain.get('name', '?')}]"
|
|
274
|
+
+ (f" [TF-IDF→{tfidf_best}]" if tfidf_best else "")
|
|
275
|
+
+ (f" [TF-IDF覆写catch-all]" if is_catch_all and tfidf_best and tfidf_best_score > 0.15 and tfidf_best in engines_combo else "")
|
|
276
|
+
+ f" → {_ENGINE_NAMES.get(engines_combo[0], engines_combo[0])}"
|
|
277
|
+
),
|
|
278
|
+
confidence=confidence, features=features,
|
|
279
|
+
domain=domain.get("name"), parallel=parallel,
|
|
280
|
+
tfidf_scores=[{"engine": n, "score": s} for n, s, _ in tfidf_scores],
|
|
281
|
+
mode=mode,
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
# 正则未命中,用 TF-IDF 结果
|
|
285
|
+
if tfidf_best and tfidf_best in enabled:
|
|
286
|
+
engines_combo = [tfidf_best]
|
|
287
|
+
if "anysearch" in enabled and "anysearch" not in engines_combo:
|
|
288
|
+
engines_combo.append("anysearch")
|
|
289
|
+
engines_combo = [e for e in engines_combo if e in enabled]
|
|
290
|
+
|
|
291
|
+
return _done(
|
|
292
|
+
engine=engines_combo[0],
|
|
293
|
+
engines=engines_combo,
|
|
294
|
+
engines_combo=engines_combo,
|
|
295
|
+
reason=f"TF-IDF 语义路由 → {_ENGINE_NAMES.get(tfidf_best, tfidf_best)} (正则未命中)",
|
|
296
|
+
confidence=0.85, features=features, domain=None,
|
|
297
|
+
parallel=len(engines_combo) > 1,
|
|
298
|
+
tfidf_scores=[{"engine": n, "score": s} for n, s, _ in tfidf_scores],
|
|
299
|
+
mode=mode,
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
# 兜底
|
|
303
|
+
fallback_combo = [e for e in ["local_search", "anysearch", "duckduckgo"] if e in enabled]
|
|
304
|
+
if not fallback_combo:
|
|
305
|
+
fallback_combo = sorted(enabled)[:2] if enabled else ["anysearch"]
|
|
306
|
+
|
|
307
|
+
return _done(
|
|
308
|
+
engine=fallback_combo[0],
|
|
309
|
+
engines=fallback_combo,
|
|
310
|
+
engines_combo=fallback_combo,
|
|
311
|
+
engines_fallback=[],
|
|
312
|
+
reason=f"无匹配域,回退 {_ENGINE_NAMES.get(fallback_combo[0], fallback_combo[0])}",
|
|
313
|
+
confidence=0.3, features=features, domain=None, parallel=False,
|
|
314
|
+
tfidf_scores=[{"engine": n, "score": s} for n, s, _ in tfidf_scores],
|
|
315
|
+
mode=mode,
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
# ── CLI ────────────────────────────────────────────────────────────────────────
|
|
320
|
+
|
|
321
|
+
def _cli():
|
|
322
|
+
import argparse
|
|
323
|
+
parser = argparse.ArgumentParser(description="Unified Search v2 路由器")
|
|
324
|
+
parser.add_argument("query")
|
|
325
|
+
parser.add_argument("--engine", default="auto")
|
|
326
|
+
parser.add_argument("--mode", default="auto", choices=["fast", "auto", "deep", "budget"])
|
|
327
|
+
parser.add_argument("--json", action="store_true")
|
|
328
|
+
args = parser.parse_args()
|
|
329
|
+
decision = route_query(args.query, engine_override=args.engine, mode=args.mode)
|
|
330
|
+
if args.json:
|
|
331
|
+
print(json.dumps(decision, ensure_ascii=False, indent=2))
|
|
332
|
+
else:
|
|
333
|
+
print(f"引擎: {decision['engine']}")
|
|
334
|
+
print(f"组合: {decision.get('engines_combo', decision['engines'])}")
|
|
335
|
+
print(f"原因: {decision['reason']}")
|
|
336
|
+
print(f"置信度: {decision['confidence']:.2f}")
|
|
337
|
+
print(f"耗时: {decision.get('elapsed_ms', 0):.3f} ms")
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
if __name__ == "__main__":
|
|
341
|
+
_cli()
|