argo-search 2.9.7 → 2.9.8
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/README.en.md +58 -145
- package/README.es.md +58 -145
- package/README.ja.md +64 -151
- package/README.ko.md +63 -150
- package/README.md +65 -154
- package/SKILL.md +3 -3
- package/assets/readme/hero.svg +2 -2
- package/backends/domain_profiles.json +48 -0
- package/backends/engine_registry.yaml +118 -4
- package/backends/quota_profiles.json +92 -12
- package/backends/source_types_cn.json +27 -1
- package/bin/argo +74 -5
- package/bin/argo.js +33 -2
- package/config.yaml +219 -60
- package/docs/ENGINE_CATALOG.md +50 -39
- package/docs/MCP_SETUP.md +3 -1
- package/docs/RELEASE_NOTES_v2.9.8.md +73 -0
- package/engines/specs/cfets.yaml +41 -0
- package/engines/specs/defillama.yaml +38 -0
- package/engines/specs/firecrawl.yaml +1 -1
- package/engines/specs/fxtwitter.yaml +5 -0
- package/engines/specs/gleif.yaml +55 -0
- package/engines/specs/hkex.yaml +40 -0
- package/engines/specs/keenable_free.yaml +36 -0
- package/engines/specs/shfe.yaml +39 -0
- package/engines/specs/stackexchange.yaml +2 -2
- package/engines/specs/tavily_free.yaml +56 -0
- package/engines/specs/yuanbao.yaml +93 -0
- package/mcp/clients.yaml +32 -0
- package/package.json +6 -2
- package/packages/dsh-plugin/README.md +23 -5
- package/packages/dsh-plugin/dsh/client.js +304 -15
- package/packages/dsh-plugin/dsh/diagnostics.js +54 -0
- package/packages/dsh-plugin/dsh/index.js +112 -1875
- package/packages/dsh-plugin/dsh/native-tools.mjs +7 -1
- package/packages/dsh-plugin/dsh/runner.js +666 -0
- package/packages/dsh-plugin/dsh/wide-research.js +878 -0
- package/packages/dsh-plugin/package.json +2 -2
- package/packages/dsh-plugin/scripts/sync-local.sh +70 -0
- package/packages/dsh-plugin/test/client.test.js +175 -1
- package/packages/dsh-plugin/test/diagnostics.test.js +28 -0
- package/packages/dsh-plugin/test/index.test.js +44 -0
- package/scripts/adaptive.py +24 -1
- package/scripts/article_meta.py +131 -0
- package/scripts/budget.py +247 -0
- package/scripts/budget_cli.py +93 -0
- package/scripts/build_release_assets.sh +65 -0
- package/scripts/cache.py +60 -72
- package/scripts/ci_drift_gate.py +90 -0
- package/scripts/content_boundary.py +46 -0
- package/scripts/content_signals.py +26 -5
- package/scripts/cross_lingual_trace.py +293 -0
- package/scripts/dispatch_result.py +98 -0
- package/scripts/engine_dispatch.py +49 -86
- package/scripts/engine_env.py +5 -0
- package/scripts/engine_families.py +6 -0
- package/scripts/engine_policy.py +7 -7
- package/scripts/engines.py +21 -0
- package/scripts/engines_base.py +3 -3
- package/scripts/engines_builders.py +12 -0
- package/scripts/engines_builders_batch10.py +48 -3
- package/scripts/engines_builders_batch14.py +656 -0
- package/scripts/engines_builders_batch15.py +249 -0
- package/scripts/engines_builders_keyless.py +111 -0
- package/scripts/engines_builders_tech.py +0 -1
- package/scripts/evidence.py +31 -3
- package/scripts/evidence_loop.py +54 -22
- package/scripts/fetch_v3.py +25 -0
- package/scripts/gen_routing_snapshot.py +11 -0
- package/scripts/install.ps1 +18 -0
- package/scripts/install.sh +16 -1
- package/scripts/job.py +3 -3
- package/scripts/local_seek.py +41 -5
- package/scripts/macro_countries.py +15 -0
- package/scripts/matrix_search_eval.py +82 -0
- package/scripts/mcp_compact.py +234 -0
- package/scripts/mcp_handlers.py +60 -642
- package/scripts/mcp_launch.sh +10 -8
- package/scripts/mcp_local.py +241 -0
- package/scripts/mcp_server.py +19 -14
- package/scripts/mcp_setup.py +33 -9
- package/scripts/mcp_tools.py +1 -0
- package/scripts/mcp_transport.py +1 -1
- package/scripts/mcp_warm.py +127 -0
- package/scripts/output_transparency.py +200 -0
- package/scripts/paid_policy.py +98 -0
- package/scripts/quota.py +158 -13
- package/scripts/recovery_runner.py +150 -0
- package/scripts/replay_eval.py +18 -0
- package/scripts/research.py +37 -21
- package/scripts/research_expand.py +24 -1
- package/scripts/retry_policy.py +76 -0
- package/scripts/route.py +6 -6
- package/scripts/route_cache.py +18 -0
- package/scripts/route_combo.py +7 -0
- package/scripts/route_domains.py +32 -19
- package/scripts/search.py +15 -27
- package/scripts/search_attribution.py +100 -3
- package/scripts/search_cli.py +24 -11
- package/scripts/search_entry.py +7 -1
- package/scripts/search_output.py +99 -15
- package/scripts/search_pipeline.py +42 -193
- package/scripts/search_types.py +107 -9
- package/scripts/semantic_evidence.py +179 -3
- package/scripts/serp_guard.py +115 -7
- package/scripts/serp_kill_log.py +25 -2
- package/scripts/stats_cli.py +13 -1
- package/scripts/wide_research.py +45 -0
- package/scripts/zhihu_quota.py +139 -0
- package/sub-skills/local-search/bm25.py +27 -3
- package/sub-skills/local-search/cli_runner.py +331 -0
- package/sub-skills/local-search/parsers.py +328 -0
- package/sub-skills/local-search/search_v3.py +31 -674
- package/sub-skills/local-seek/scripts/seek_rank.py +7 -3
- package/assets/readme/settings-keys.png +0 -0
- package/assets/readme/settings-tools.png +0 -0
- package/scripts/image_vision/vision_probe +0 -0
- package/scripts/local_engines.py +0 -321
- package/sub-skills/ego-search/references/browser-runtime.md +0 -43
- package/sub-skills/ego-search/references/install.md +0 -67
- package/sub-skills/ego-search/references/original-ego-upgrade.md +0 -51
- package/sub-skills/ego-search/tests/test_query_normalize.py +0 -68
- package/sub-skills/js-run/tests/fixtures/challenge_like.js +0 -27
- package/sub-skills/js-run/tests/test_jsrun.py +0 -165
- package/sub-skills/local-seek/references/auto-behaviors.md +0 -24
- package/sub-skills/local-seek/references/fd-mdfind-recipes.md +0 -62
- package/sub-skills/local-seek/references/git-integration.md +0 -54
- package/sub-skills/local-seek/references/remote-handoff.md +0 -46
- package/sub-skills/local-seek/references/rg-recipes.md +0 -96
- package/sub-skills/local-seek/references/strategies.md +0 -52
- package/sub-skills/local-seek/references/structural-search.md +0 -59
- package/sub-skills/local-seek/tests/test_content_rank.py +0 -222
- package/sub-skills/local-seek/tests/test_determinism_20261006.py +0 -145
- package/sub-skills/local-seek/tests/test_noise_tier.py +0 -101
- package/sub-skills/local-seek/tests/test_pcre2_probe.py +0 -136
- package/sub-skills/local-seek/tests/test_rank_pinyin.py +0 -77
- package/sub-skills/local-seek/tests/test_regression_20261003.py +0 -169
- package/sub-skills/local-seek/tests/test_regression_20261006.py +0 -146
- package/sub-skills/local-seek/tests/test_seek_degradation.py +0 -111
- package/sub-skills/local-seek/tests/test_seek_fts.py +0 -313
package/scripts/job.py
CHANGED
|
@@ -895,13 +895,13 @@ def _is_quota_exhausted(exc: Exception) -> bool:
|
|
|
895
895
|
的模块静默生效。403/429 各有归属:engine_failure.classify 分 auth /
|
|
896
896
|
blocked / rate_limited,健康问题交熔断器自愈,本函数不接手。
|
|
897
897
|
|
|
898
|
-
|
|
899
|
-
|
|
898
|
+
关键词表真源是 cache_guard 的 `_QUOTA_ERROR_KEYWORDS`(配额分类与自适应
|
|
899
|
+
跳过共用),不另起一份。
|
|
900
900
|
真欠费的文案(`You do not have enough money or package quota`)含
|
|
901
901
|
`quota`,仍然照常命中。
|
|
902
902
|
"""
|
|
903
903
|
try:
|
|
904
|
-
from
|
|
904
|
+
from cache_guard import _QUOTA_ERROR_KEYWORDS
|
|
905
905
|
except ImportError:
|
|
906
906
|
return False
|
|
907
907
|
text = str(exc).lower()
|
package/scripts/local_seek.py
CHANGED
|
@@ -151,6 +151,37 @@ def _seek_disk_cache_put(key: str, results: list[dict[str, Any]]) -> None:
|
|
|
151
151
|
pass
|
|
152
152
|
|
|
153
153
|
|
|
154
|
+
# 本地代码/工程/配置意图正则模式
|
|
155
|
+
_LOCAL_INTENT_PATTERN = re.compile(
|
|
156
|
+
r"(?i)(?:\b(?:def|class|function|fn|import|export|const|var|struct|enum)\b|"
|
|
157
|
+
r"\.(?:py|js|ts|json|yaml|yml|md|rs|go|c|cpp|h|sh|sql)\b|"
|
|
158
|
+
r"(?:readme|config|package\.json|cargo\.toml|dockerfile|makefile)|"
|
|
159
|
+
r"(?:本工程|本项目|代码库|本地文件|架构实现|函数|方法|源码|接口定义))"
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def has_local_code_intent(query: str) -> bool:
|
|
164
|
+
"""是否包含本地工程/代码/配置检索意图。"""
|
|
165
|
+
if not query:
|
|
166
|
+
return False
|
|
167
|
+
return bool(_LOCAL_INTENT_PATTERN.search(query))
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def resolve_include_local(include_local: bool | None, mode: str, query: str = "") -> bool:
|
|
171
|
+
"""include-local 三态解析:True/False 显式,None=自动。
|
|
172
|
+
|
|
173
|
+
自适应感知:除 fast/budget 模式外,当查询具有明显的本地代码/文件特征
|
|
174
|
+
且处于非宽泛根项目目录时,自动开启本地命中。
|
|
175
|
+
"""
|
|
176
|
+
if include_local is not None:
|
|
177
|
+
return bool(include_local)
|
|
178
|
+
if mode in ("fast", "budget"):
|
|
179
|
+
return True
|
|
180
|
+
if has_local_code_intent(query) and _local_seek_dir() is not None:
|
|
181
|
+
return True
|
|
182
|
+
return False
|
|
183
|
+
|
|
184
|
+
|
|
154
185
|
def _is_broad_local_root(path: str) -> bool:
|
|
155
186
|
"""path 是否是「不能当作本地检索范围」的宽泛根。
|
|
156
187
|
|
|
@@ -200,7 +231,8 @@ def _local_seek_dir() -> str | None:
|
|
|
200
231
|
|
|
201
232
|
|
|
202
233
|
def _run_local_seek(query: str, max_n: int = 5,
|
|
203
|
-
search_dir: str | None = None
|
|
234
|
+
search_dir: str | None = None,
|
|
235
|
+
warn: bool = True) -> list[dict[str, Any]]:
|
|
204
236
|
"""本机文件命中(include-local 用):进程内调 seek.run_query,JSON 并入。
|
|
205
237
|
|
|
206
238
|
fast/budget 模式下由 super_search 自动调用(_resolve_include_local),
|
|
@@ -215,10 +247,14 @@ def _run_local_seek(query: str, max_n: int = 5,
|
|
|
215
247
|
# 静默跳过会让用户以为本地没匹配,而事实是根本没查。返回一个
|
|
216
248
|
# 占位「结果」又会污染 results(它只是个说明,不是命中),所以
|
|
217
249
|
# 说明走 stderr:与 CLI 既有的 include-local 异常提示同一条通道。
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
250
|
+
# 只在**显式开启**时提示(warn=False 是 fast/budget 的自动路径):
|
|
251
|
+
# 自动路径在 home 等宽根下每次 search 都会打印,Agent 会把它当成
|
|
252
|
+
# 输出的一部分,属于噪音而非归因。
|
|
253
|
+
if warn:
|
|
254
|
+
sys.stderr.write(
|
|
255
|
+
" [include-local] 当前目录不适合本地检索(home / 根目录过宽,或位于 argo "
|
|
256
|
+
"自身安装树内——只会命中自身源码),已跳过;"
|
|
257
|
+
"要搜本机文件请 cd 到目标目录后重试,或用 local-seek 的 seek.py --path <目录>\n")
|
|
222
258
|
return []
|
|
223
259
|
|
|
224
260
|
# 缓存:同 query+target 在 TTL 内直接命中,避免重复冷启动 seek.py 子进程。
|
|
@@ -66,6 +66,12 @@ _NON_US_COUNTRIES: dict[str, str] = {
|
|
|
66
66
|
name: code for name, code in WORLDBANK_COUNTRIES.items() if code != "USA"
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
# 美国子集:给「统计局/国内」这类中国语境别名做前置排除。
|
|
70
|
+
# 同一张主表派生,避免「美国 统计局」被当成中国口径。
|
|
71
|
+
_US_COUNTRIES: dict[str, str] = {
|
|
72
|
+
name: code for name, code in WORLDBANK_COUNTRIES.items() if code == "USA"
|
|
73
|
+
}
|
|
74
|
+
|
|
69
75
|
|
|
70
76
|
def is_foreign_macro_query(query: str) -> bool:
|
|
71
77
|
"""查询是否明确指向非美国国家/地区。
|
|
@@ -78,5 +84,14 @@ def is_foreign_macro_query(query: str) -> bool:
|
|
|
78
84
|
英文词——实测 `focus on growth`、`consensus 预测`、`business survey`
|
|
79
85
|
全被判成「外国宏观查询」,FRED 因而被无谓地排除。同一个概念(国家词识别)
|
|
80
86
|
在同一模块里有两套行为不同的实现,正是这类漏判的来源。
|
|
87
|
+
|
|
88
|
+
补充(2026-10-23):「国家统计局」「国内」「我国」是明确的中国语境,
|
|
89
|
+
但不在国家名表里。实测「国家统计局 一季度 GDP」因此没让位,FRED 领队
|
|
90
|
+
返回美国 GDP。这几个词只指向中国,直接判为 foreign(非美国)。
|
|
91
|
+
但「美国 统计局」「美国国内 CPI」同时含美国词,必须先排除美国口径。
|
|
81
92
|
"""
|
|
93
|
+
q = query or ""
|
|
94
|
+
if (not match_country(q, _US_COUNTRIES)
|
|
95
|
+
and any(term in q for term in ("国家统计局", "统计局", "国内", "我国"))):
|
|
96
|
+
return True
|
|
82
97
|
return bool(match_country(query, _NON_US_COUNTRIES))
|
|
@@ -254,6 +254,21 @@ ROUTE_MATRIX: list[dict[str, Any]] = [
|
|
|
254
254
|
"domain": "org_entity", "primary": "wikidata", "scenario": "org"},
|
|
255
255
|
{"id": "R_zh_org2", "q": "清华大学 创办年份", "lang": "zh",
|
|
256
256
|
"domain": "org_entity", "primary": "wikidata", "scenario": "org"},
|
|
257
|
+
# 中文企业总部不是地理问题(2026-10-22 修:geo_places 的方位词 unless 增补
|
|
258
|
+
# 「总部|母公司|子公司|注册地|办公地」后落到本域;此前落 geo_places,用 OSM+维基
|
|
259
|
+
# 只拿到广汽/新凯来/五粮液等无关结果)
|
|
260
|
+
{"id": "R_zh_org_hq", "q": "华为 总部在哪", "lang": "zh",
|
|
261
|
+
"domain": "org_entity", "primary": "wikidata", "scenario": "org"},
|
|
262
|
+
|
|
263
|
+
# ── 企业/工商(company_search)──
|
|
264
|
+
# 2026-10-22 补:此前 ROUTE_MATRIX 无本域用例,导致「中国企业股权/工商信息」
|
|
265
|
+
# 被路由到本域、而该域 combo 只有国际法人库(gleif/opencorporates)+ 百科的
|
|
266
|
+
# 问题长期无人发现——实测「长亭科技 股东 持股比例」返回 5 条同名「XX科技
|
|
267
|
+
# 有限公司」干扰。元宝进 combo 第 1 位后(a5a46ad)才具中文交付能力。
|
|
268
|
+
{"id": "R_zh_company_shareholder", "q": "长亭科技 股东 持股比例", "lang": "zh",
|
|
269
|
+
"domain": "company_search", "primary": "opencorporates", "scenario": "company"},
|
|
270
|
+
{"id": "R_zh_company_regcap", "q": "字节跳动 注册资本 变更", "lang": "zh",
|
|
271
|
+
"domain": "company_search", "primary": "opencorporates", "scenario": "company"},
|
|
257
272
|
|
|
258
273
|
# ── 媒体/音乐 ──
|
|
259
274
|
{"id": "R_en_media", "q": "Taylor Swift album", "lang": "en",
|
|
@@ -384,6 +399,73 @@ ROUTE_MATRIX: list[dict[str, Any]] = [
|
|
|
384
399
|
"worldbank", "nbs_stats", "un_comtrade", "endoflife",
|
|
385
400
|
"deps_dev", "openalex", "europepmc"],
|
|
386
401
|
"forbid_cn": True, "scenario": "negative"},
|
|
402
|
+
|
|
403
|
+
# ── 域覆盖(2026-10-22):补 53 个此前零用例的域 ────────────────────────
|
|
404
|
+
# 零用例 = 路由回归无人挡。company_search 零用例,「中国企业股权/工商信息」
|
|
405
|
+
# 被路由到本域却无中文能力的问题因此潜伏 8 天;geo_places 的「在哪」劫持
|
|
406
|
+
# 企业总部查询同理(两者都是靠人工撞出来的)。lang 取路由实测 primary_lang,
|
|
407
|
+
# 与 _route_ok 的 lang 断言对齐;primary 取域声明的主引擎。
|
|
408
|
+
{"id": "R_ai_model", "q": "models.dev claude 价格 上下文", "lang": "en", "domain": "ai_model", "primary": "models_dev", "scenario": "tech"},
|
|
409
|
+
{"id": "R_aviation_weather", "q": "北京首都机场 航空气象 METAR", "lang": "zh", "domain": "aviation_weather", "primary": "aviation_weather", "scenario": "weather"},
|
|
410
|
+
{"id": "R_chinese_tech_deep", "q": "中文 编程 算法 架构 深度对比", "lang": "zh", "domain": "chinese_tech_deep", "primary": "byted", "scenario": "tech"},
|
|
411
|
+
{"id": "R_claim_check", "q": "辟谣 传言 事实核查", "lang": "zh", "domain": "claim_check", "primary": "factcheck_org", "scenario": "factcheck"},
|
|
412
|
+
{"id": "R_cn_encyclopedia", "q": "长征 百度百科 词条解释", "lang": "zh", "domain": "cn_encyclopedia", "primary": "baidu_baike", "scenario": "media"},
|
|
413
|
+
{"id": "R_cn_tech_community", "q": "掘金 前端文章 技术博客", "lang": "zh", "domain": "cn_tech_community", "primary": "juejin", "scenario": "tech"},
|
|
414
|
+
{"id": "R_code_search", "q": "github 代码 实现 用法", "lang": "zh", "domain": "code_search", "primary": "github", "scenario": "code"},
|
|
415
|
+
{"id": "R_crypto_search", "q": "比特币 以太坊 加密货币 价格", "lang": "zh", "domain": "crypto_search", "primary": "coingecko", "scenario": "finance"},
|
|
416
|
+
{"id": "R_dataset_search", "q": "科研数据集 开放数据 下载", "lang": "zh", "domain": "dataset_search", "primary": "datacite", "scenario": "academic"},
|
|
417
|
+
{"id": "R_dictionary_search", "q": "英文单词 词典 意思", "lang": "zh", "domain": "dictionary_search", "primary": "free_dictionary", "scenario": "media"},
|
|
418
|
+
{"id": "R_earth_science", "q": "地震 震级 余震 震中", "lang": "zh", "domain": "earth_science", "primary": "usgs", "scenario": "science"},
|
|
419
|
+
{"id": "R_em_news_search", "q": "东财 全球资讯 市场", "lang": "zh", "domain": "em_news_search", "primary": "em_global_news", "scenario": "news"},
|
|
420
|
+
{"id": "R_energy_grid", "q": "电网 发电结构 发电量", "lang": "zh", "domain": "energy_grid", "primary": "energy_charts", "scenario": "science"},
|
|
421
|
+
{"id": "R_entity_search", "q": "维基数据 知识图谱 实体查询", "lang": "zh", "domain": "entity_search", "primary": "wikidata", "scenario": "media"},
|
|
422
|
+
{"id": "R_fact_check", "q": "长江 长度 是多少", "lang": "zh", "domain": "fact_check", "primary": "wikipedia", "scenario": "factcheck"},
|
|
423
|
+
{"id": "R_futures_market", "q": "沪铜 期货 行情", "lang": "zh", "domain": "futures_market", "primary": "shfe", "scenario": "finance"},
|
|
424
|
+
{"id": "R_fx_query", "q": "人民币 汇率 中间价", "lang": "zh", "domain": "fx_query", "primary": "fx_rate", "scenario": "finance"},
|
|
425
|
+
{"id": "R_game_search", "q": "steam 游戏推荐 好玩的游戏", "lang": "zh", "domain": "game_search", "primary": "steam", "scenario": "media"},
|
|
426
|
+
{"id": "R_global_event", "q": "国际事件 地缘政治 局势", "lang": "zh", "domain": "global_event", "primary": "google_news", "scenario": "news"},
|
|
427
|
+
{"id": "R_hk_filings", "q": "港股 港交所 披露易 公告", "lang": "zh", "domain": "hk_filings", "primary": "hkex", "scenario": "finance"},
|
|
428
|
+
{"id": "R_hot_trending", "q": "微博热搜 今日热搜", "lang": "zh", "domain": "hot_trending", "primary": "baidu_hot", "scenario": "community"},
|
|
429
|
+
{"id": "R_image_search", "q": "雪景 壁纸 图片", "lang": "zh", "domain": "image_search", "primary": "openverse", "scenario": "media"},
|
|
430
|
+
{"id": "R_intl_news_flash", "q": "国际新闻 外媒 国际要闻", "lang": "zh", "domain": "intl_news_flash", "primary": "guardian_rss", "scenario": "news"},
|
|
431
|
+
{"id": "R_japan_law", "q": "日本 法令 民法 条文", "lang": "zh", "domain": "japan_law", "primary": "egov_law", "scenario": "legal"},
|
|
432
|
+
{"id": "R_kor_law", "q": "韩国 法令 民法 条文", "lang": "zh", "domain": "kor_law", "primary": "kor_law", "scenario": "legal"},
|
|
433
|
+
{"id": "R_law_text", "q": "法条 法律条文 全文", "lang": "zh", "domain": "law_text", "primary": "flk_law", "scenario": "legal"},
|
|
434
|
+
{"id": "R_legal", "q": "刑法 司法解释 判例", "lang": "zh", "domain": "legal", "primary": "anysearch", "scenario": "legal"},
|
|
435
|
+
{"id": "R_meme_slang", "q": "网络梗 流行语 黑话", "lang": "zh", "domain": "meme_slang", "primary": "itotii", "scenario": "media"},
|
|
436
|
+
{"id": "R_ml_models", "q": "huggingface 预训练模型 开源模型", "lang": "zh", "domain": "ml_models", "primary": "huggingface", "scenario": "tech"},
|
|
437
|
+
{"id": "R_news_realtime", "q": "最新新闻 今天发生 突发", "lang": "zh", "domain": "news_realtime", "primary": "byted", "scenario": "news"},
|
|
438
|
+
{"id": "R_outbreak_health", "q": "疫情 暴发 流行病 通报", "lang": "zh", "domain": "outbreak_health", "primary": "who_don", "scenario": "science"},
|
|
439
|
+
{"id": "R_patent_search", "q": "专利 权利要求 技术方案", "lang": "zh", "domain": "patent_search", "primary": "google_patents", "scenario": "academic"},
|
|
440
|
+
{"id": "R_prediction_market", "q": "预测市场 事件合约", "lang": "zh", "domain": "prediction_market", "primary": "polymarket", "scenario": "finance"},
|
|
441
|
+
{"id": "R_protein_search", "q": "蛋白质 基因 氨基酸序列", "lang": "zh", "domain": "protein_search", "primary": "uniprot", "scenario": "academic"},
|
|
442
|
+
{"id": "R_redskill_search", "q": "小红书 技能 排行榜", "lang": "zh", "domain": "redskill_search", "primary": "redskill", "scenario": "community"},
|
|
443
|
+
{"id": "R_scholar_search", "q": "google scholar 学术论文 期刊", "lang": "zh", "domain": "scholar_search", "primary": "google_scholar", "scenario": "academic"},
|
|
444
|
+
{"id": "R_sec_filings", "q": "SEC EDGAR 招股书 财报申报", "lang": "zh", "domain": "sec_filings", "primary": "sec_edgar", "scenario": "finance"},
|
|
445
|
+
{"id": "R_semantic_discovery", "q": "discover landscape ecosystem emerging", "lang": "en", "domain": "semantic_discovery", "primary": "exa", "scenario": "tech"},
|
|
446
|
+
{"id": "R_shopping", "q": "购物 评测 推荐 踩坑", "lang": "zh", "domain": "shopping", "primary": "zhihu", "scenario": "media"},
|
|
447
|
+
{"id": "R_skill_search", "q": "agent skills 市场 目录", "lang": "en", "domain": "skill_search", "primary": "skillsmp", "scenario": "community"},
|
|
448
|
+
{"id": "R_species_search", "q": "物种 学名 生物分类 拉丁名", "lang": "zh", "domain": "species_search", "primary": "gbif", "scenario": "academic"},
|
|
449
|
+
{"id": "R_stackoverflow_search", "q": "stackoverflow 编程问题 代码问题", "lang": "zh", "domain": "stackoverflow_search", "primary": "stackoverflow", "scenario": "code"},
|
|
450
|
+
{"id": "R_standards", "q": "国家标准 国标 标准号", "lang": "zh", "domain": "standards", "primary": "std_samr", "scenario": "academic"},
|
|
451
|
+
{"id": "R_tech_deep", "q": "deep dive 架构 实践", "lang": "zh", "domain": "tech_deep", "primary": "arxiv", "scenario": "tech"},
|
|
452
|
+
{"id": "R_us_fundamentals", "q": "营收 净利润 财务数据 苹果", "lang": "zh", "domain": "us_fundamentals", "primary": "sec_companyfacts", "scenario": "finance"},
|
|
453
|
+
{"id": "R_us_legal", "q": "supreme court 判例 precedent", "lang": "en", "domain": "us_legal", "primary": "federal_register", "scenario": "legal"},
|
|
454
|
+
{"id": "R_web_archive", "q": "网页快照 历史网页 wayback", "lang": "zh", "domain": "web_archive", "primary": "wikipedia", "scenario": "general"},
|
|
455
|
+
{"id": "R_web_docs", "q": "mozilla developer fetch 属性", "lang": "en", "domain": "web_docs", "primary": "mdn", "scenario": "code"},
|
|
456
|
+
{"id": "R_wechat_search", "q": "微信公众号 文章 搜狗微信", "lang": "zh", "domain": "wechat_search", "primary": "wechat_sogou", "scenario": "community"},
|
|
457
|
+
{"id": "R_world_news", "q": "noticias 国际 新闻", "lang": "zh", "domain": "world_news", "primary": "anysearch", "scenario": "news"},
|
|
458
|
+
{"id": "R_zhihu_content", "q": "知乎 怎么看待 如何评价", "lang": "zh", "domain": "zhihu_content", "primary": "zhihu", "scenario": "media"},
|
|
459
|
+
{"id": "R_zhihu_hot_list", "q": "知乎热榜 知乎热搜", "lang": "zh", "domain": "zhihu_hot_list", "primary": "zhihu_hot", "scenario": "community"},
|
|
460
|
+
{"id": "R_zhihu_user_data", "q": "我的知乎 我的创作 收藏", "lang": "zh", "domain": "zhihu_user_data", "primary": "zhihu_user", "scenario": "community"},
|
|
461
|
+
|
|
462
|
+
# ── 域冲突让位修复(2026-10-22)────────────────────────────────────────
|
|
463
|
+
# 这三个域的触发词此前被更宽泛的域覆盖,无法通过自动路由命中(因此曾登记在
|
|
464
|
+
# test_route_matrix_coverage 的 _NO_CASE_DEBT 里)。在宽泛侧加 unless 让位后
|
|
465
|
+
# 各补一条钉住;宽泛侧的真查询锁在 test_domain_pattern_precision 的既有用例。
|
|
466
|
+
{"id": "R_jin10_flash", "q": "金十 7x24 快讯", "lang": "zh", "domain": "jin10_flash", "primary": "jin10", "scenario": "news"},
|
|
467
|
+
{"id": "R_v2ex_search", "q": "v2ex 技术社区", "lang": "zh", "domain": "v2ex_search", "primary": "v2ex", "scenario": "code"},
|
|
468
|
+
{"id": "R_wenshu_query", "q": "裁判文书 判决书", "lang": "zh", "domain": "wenshu_query", "primary": "wenshu", "scenario": "legal"},
|
|
387
469
|
]
|
|
388
470
|
|
|
389
471
|
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""mcp_compact.py — MCP 结果载荷裁剪与紧凑压缩协议。
|
|
3
|
+
|
|
4
|
+
拆分自 mcp_handlers.py:纯数据处理与结构级截断,无网络 IO 与外部状态。
|
|
5
|
+
供 MCP 工具执行层(mcp_handlers)、CLI 与外部测试独立复用。
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _trim_snippet(text: Any, n: int = 120) -> str | None:
|
|
15
|
+
"""截断摘要文本,防止超长字符膨胀。"""
|
|
16
|
+
if not text:
|
|
17
|
+
return None
|
|
18
|
+
s = str(text)
|
|
19
|
+
return s if len(s) <= n else s[:n]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _compact_search_result(result: dict[str, Any], summary: bool = False) -> dict[str, Any]:
|
|
23
|
+
"""去掉 MCP 热路径用不到的重字段,缩小首包。"""
|
|
24
|
+
snip_n = 80 if summary else 160
|
|
25
|
+
out: dict[str, Any] = {
|
|
26
|
+
"query": result.get("query"),
|
|
27
|
+
"engine": result.get("engine"),
|
|
28
|
+
"engines_used": result.get("engines_used") or result.get("engines"),
|
|
29
|
+
"count": result.get("count"),
|
|
30
|
+
"elapsed_ms": result.get("elapsed_ms"),
|
|
31
|
+
"cached": result.get("cached"),
|
|
32
|
+
"mode": result.get("mode"),
|
|
33
|
+
"depth": result.get("depth"),
|
|
34
|
+
"execution_tier": result.get("execution_tier"),
|
|
35
|
+
"input_kind": result.get("input_kind"),
|
|
36
|
+
"login_hint": result.get("login_hint"),
|
|
37
|
+
}
|
|
38
|
+
results = []
|
|
39
|
+
for r in result.get("results") or []:
|
|
40
|
+
if not isinstance(r, dict):
|
|
41
|
+
continue
|
|
42
|
+
item = {
|
|
43
|
+
"title": r.get("title"),
|
|
44
|
+
"url": r.get("url"),
|
|
45
|
+
"snippet": _trim_snippet(r.get("snippet"), snip_n),
|
|
46
|
+
"source": r.get("source"),
|
|
47
|
+
"score": r.get("score"),
|
|
48
|
+
}
|
|
49
|
+
# 保留发布时间维度(仅引擎输出该字段时,避免空字段噪音)
|
|
50
|
+
if r.get("published_at"):
|
|
51
|
+
item["published_at"] = r["published_at"]
|
|
52
|
+
# 顶包标记:recovery 替换了引擎原结果时透传,供上层识别结果已被代答
|
|
53
|
+
if r.get("_recovered"):
|
|
54
|
+
item["recovered"] = r["_recovered"]
|
|
55
|
+
# 保留快评,丢掉大块 meta
|
|
56
|
+
for k in ("selection", "absorption", "credibility_fast", "evidence_flags"):
|
|
57
|
+
if k in r:
|
|
58
|
+
item[k] = r[k]
|
|
59
|
+
# 证据完整链路 P0:保留核验信号(仅存在时,避免空字段噪音)
|
|
60
|
+
for k in ("fetch_suggested", "has_fetched_evidence", "post_fetch_absorption"):
|
|
61
|
+
if r.get(k) is not None:
|
|
62
|
+
item[k] = r[k]
|
|
63
|
+
results.append(item)
|
|
64
|
+
out["results"] = results
|
|
65
|
+
# 证据完整链路 P0:顶层门控信号(轻量 4 字段,供 Agent 判断「能否下结论」)
|
|
66
|
+
if result.get("fetch_required") is not None:
|
|
67
|
+
out["fetch_required"] = result["fetch_required"]
|
|
68
|
+
el = result.get("evidence_loop")
|
|
69
|
+
if isinstance(el, dict):
|
|
70
|
+
out["evidence_loop"] = {
|
|
71
|
+
"suggested": el.get("suggested") or [],
|
|
72
|
+
"verified_count": el.get("verified_count") or 0,
|
|
73
|
+
"pending_count": el.get("pending_count") or 0,
|
|
74
|
+
}
|
|
75
|
+
# 引擎可观测性:精简版 engine_outcomes(engine/status/latency,去掉 detail)
|
|
76
|
+
# 让 Agent 能看到「哪个引擎失败、为什么类别」,不占大体积
|
|
77
|
+
outcomes = result.get("engine_outcomes") or []
|
|
78
|
+
if outcomes:
|
|
79
|
+
out["engine_outcomes"] = [
|
|
80
|
+
{
|
|
81
|
+
"engine": o.get("engine"),
|
|
82
|
+
"status": o.get("status"),
|
|
83
|
+
"latency_ms": o.get("latency_ms"),
|
|
84
|
+
"results_count": o.get("results_count"),
|
|
85
|
+
}
|
|
86
|
+
for o in outcomes
|
|
87
|
+
if isinstance(o, dict)
|
|
88
|
+
][:12]
|
|
89
|
+
wasted = result.get("wasted_engine_ms")
|
|
90
|
+
if wasted:
|
|
91
|
+
out["wasted_engine_ms"] = wasted
|
|
92
|
+
if result.get("early_stopped"):
|
|
93
|
+
out["early_stopped"] = True
|
|
94
|
+
if result.get("minhash_removed"):
|
|
95
|
+
out["minhash_removed"] = result.get("minhash_removed")
|
|
96
|
+
from search_output import decorate_compact_payload as _decorate
|
|
97
|
+
_decorate(out, result)
|
|
98
|
+
if result.get("domain_filter"):
|
|
99
|
+
out["domain_filter"] = result["domain_filter"]
|
|
100
|
+
if result.get("rank_method"):
|
|
101
|
+
out["rank_method"] = result.get("rank_method")
|
|
102
|
+
if result.get("reranker"):
|
|
103
|
+
out["reranker"] = result.get("reranker")
|
|
104
|
+
lp = result.get("lang_pref")
|
|
105
|
+
if isinstance(lp, dict):
|
|
106
|
+
out["lang_pref"] = {
|
|
107
|
+
"query_lang": lp.get("query_lang"),
|
|
108
|
+
"system_lang": lp.get("system_lang"),
|
|
109
|
+
"habit_lang": lp.get("habit_lang"),
|
|
110
|
+
"prefer_langs": lp.get("prefer_langs"),
|
|
111
|
+
"engine_lang": lp.get("engine_lang"),
|
|
112
|
+
}
|
|
113
|
+
rec = result.get("recovery")
|
|
114
|
+
if isinstance(rec, dict) and rec.get("triggered"):
|
|
115
|
+
out["recovery"] = {
|
|
116
|
+
"recovered": rec.get("recovered"),
|
|
117
|
+
"level_used": rec.get("level_used"),
|
|
118
|
+
"strategy_used": rec.get("strategy_used"),
|
|
119
|
+
"note": rec.get("note"),
|
|
120
|
+
}
|
|
121
|
+
return out
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _compact_research_result(report: dict[str, Any], summary: bool = False) -> dict[str, Any]:
|
|
125
|
+
"""研究包:只保留 Agent 写作需要的字段,砍掉子查询原始 dumps。"""
|
|
126
|
+
snip_n = 100 if summary else 160
|
|
127
|
+
keys = (
|
|
128
|
+
"query", "query_original", "execution_tier", "topic_profile", "topic_profile_key",
|
|
129
|
+
"discipline", "quality_gates", "report_sections", "source_grades", "disclaimer",
|
|
130
|
+
"academic_discipline", "engines_used", "source_distribution", "elapsed_ms",
|
|
131
|
+
"sub_query_count", "total_sources", "gaps", "mode", "sub_queries",
|
|
132
|
+
"engines_priority", "vertical_engines", "fetch_required",
|
|
133
|
+
"kind", "conclusion_cap", "quality_gate_results", "work_packages",
|
|
134
|
+
"work_package_stages", "protocol", "cross_lingual_trace",
|
|
135
|
+
)
|
|
136
|
+
out: dict[str, Any] = {k: report[k] for k in keys if k in report and report[k] is not None}
|
|
137
|
+
|
|
138
|
+
# 证据完整链路 P0:研究包门控(pending_fetch 截断到 5 条,保留可编程核验信号)
|
|
139
|
+
el = report.get("evidence_loop")
|
|
140
|
+
if isinstance(el, dict):
|
|
141
|
+
out["evidence_loop"] = {
|
|
142
|
+
"high_consequence_domain": el.get("high_consequence_domain"),
|
|
143
|
+
"pending_fetch": (el.get("pending_fetch") or [])[:5],
|
|
144
|
+
"verified_count": el.get("verified_count") or 0,
|
|
145
|
+
"pending_count": el.get("pending_count") or 0,
|
|
146
|
+
}
|
|
147
|
+
if isinstance(report.get("verify"), dict):
|
|
148
|
+
v = report["verify"]
|
|
149
|
+
out["verify"] = {
|
|
150
|
+
"revision_summary": v.get("revision_summary") or {},
|
|
151
|
+
"pending": (v.get("pending") or [])[:5],
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
findings = []
|
|
155
|
+
for kf in report.get("key_findings") or []:
|
|
156
|
+
if not isinstance(kf, dict):
|
|
157
|
+
continue
|
|
158
|
+
top = dict(kf.get("top_result") or {})
|
|
159
|
+
if top.get("snippet"):
|
|
160
|
+
top["snippet"] = _trim_snippet(top.get("snippet"), snip_n)
|
|
161
|
+
findings.append({
|
|
162
|
+
"aspect": kf.get("aspect"),
|
|
163
|
+
"strategy": kf.get("strategy"),
|
|
164
|
+
"top_result": top,
|
|
165
|
+
"result_count": kf.get("result_count"),
|
|
166
|
+
"citation_refs": kf.get("citation_refs"),
|
|
167
|
+
})
|
|
168
|
+
out["key_findings"] = findings
|
|
169
|
+
|
|
170
|
+
sources = []
|
|
171
|
+
for s in report.get("sources") or report.get("citations") or []:
|
|
172
|
+
if not isinstance(s, dict) or not s.get("url"):
|
|
173
|
+
continue
|
|
174
|
+
sources.append({
|
|
175
|
+
"ref": s.get("ref") or s.get("id"),
|
|
176
|
+
"title": s.get("title"),
|
|
177
|
+
"url": s.get("url"),
|
|
178
|
+
"engine": s.get("engine") or s.get("source"),
|
|
179
|
+
"snippet": _trim_snippet(s.get("snippet"), snip_n),
|
|
180
|
+
})
|
|
181
|
+
if summary and len(sources) >= 10:
|
|
182
|
+
break
|
|
183
|
+
out["sources"] = sources
|
|
184
|
+
if not summary:
|
|
185
|
+
out["citations"] = report.get("citations") or sources
|
|
186
|
+
# 交叉引用截断
|
|
187
|
+
crs = report.get("cross_references") or []
|
|
188
|
+
out["cross_references"] = crs[:5]
|
|
189
|
+
# 覆盖度 / 验证记录 / 盲区(科研方法论增强)
|
|
190
|
+
if report.get("coverage_map"):
|
|
191
|
+
out["coverage_map"] = report["coverage_map"]
|
|
192
|
+
if report.get("verification_records"):
|
|
193
|
+
out["verification_records"] = report["verification_records"][:12]
|
|
194
|
+
if report.get("blind_spots"):
|
|
195
|
+
out["blind_spots"] = report["blind_spots"][:8]
|
|
196
|
+
return out
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _cap_extract_output(output: dict[str, Any]) -> dict[str, Any]:
|
|
200
|
+
"""argo_fetch mode=extract 的输出上限(防大页表格撑爆上下文)。
|
|
201
|
+
|
|
202
|
+
结构级裁剪保持 JSON 可解析:表格数量/单元格、metadata 值、jsonld
|
|
203
|
+
条目体积分别设上限,超限截断并打标记。
|
|
204
|
+
"""
|
|
205
|
+
tables = output.get("tables")
|
|
206
|
+
if isinstance(tables, list):
|
|
207
|
+
if len(tables) > 12:
|
|
208
|
+
output["tables"] = tables[:12]
|
|
209
|
+
output["tables_truncated"] = True
|
|
210
|
+
for t in output["tables"]:
|
|
211
|
+
if isinstance(t, list):
|
|
212
|
+
for i, row in enumerate(t):
|
|
213
|
+
if isinstance(row, list):
|
|
214
|
+
t[i] = [str(c)[:300] if c is not None else "" for c in row]
|
|
215
|
+
meta = output.get("metadata")
|
|
216
|
+
if isinstance(meta, dict):
|
|
217
|
+
output["metadata"] = {k: str(v)[:500] for k, v in meta.items()}
|
|
218
|
+
jsonld = output.get("jsonld")
|
|
219
|
+
if isinstance(jsonld, list):
|
|
220
|
+
capped = []
|
|
221
|
+
for entry in jsonld:
|
|
222
|
+
try:
|
|
223
|
+
text = json.dumps(entry, ensure_ascii=False)
|
|
224
|
+
except (TypeError, ValueError):
|
|
225
|
+
continue
|
|
226
|
+
if len(text) > 4000:
|
|
227
|
+
capped.append({"_clipped": True, "preview": text[:4000]})
|
|
228
|
+
else:
|
|
229
|
+
capped.append(entry)
|
|
230
|
+
if len(capped) > 8:
|
|
231
|
+
capped = capped[:8]
|
|
232
|
+
output["jsonld_truncated"] = True
|
|
233
|
+
output["jsonld"] = capped
|
|
234
|
+
return output
|