argo-search 2.9.7 → 2.9.9
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 +73 -150
- package/README.es.md +73 -150
- package/README.ja.md +79 -156
- package/README.ko.md +78 -155
- package/README.md +80 -159
- 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 +78 -5
- package/bin/argo.js +33 -2
- package/config.yaml +269 -63
- package/docs/ARGO_INTRO.md +5 -3
- 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/docs/RELEASE_NOTES_v2.9.9.md +78 -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 +7 -2
- package/packages/dsh-plugin/README.md +25 -6
- package/packages/dsh-plugin/dsh/client.js +402 -47
- package/packages/dsh-plugin/dsh/diagnostics.js +97 -0
- package/packages/dsh-plugin/dsh/index.js +135 -1876
- 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 +3 -3
- package/packages/dsh-plugin/scripts/sync-local.sh +70 -0
- package/packages/dsh-plugin/test/client.test.js +177 -2
- package/packages/dsh-plugin/test/diagnostics.test.js +28 -0
- package/packages/dsh-plugin/test/index.test.js +44 -0
- package/scripts/adaptive.py +126 -43
- package/scripts/article_meta.py +131 -0
- package/scripts/bench_tail_ab.py +281 -0
- package/scripts/bounded_run.py +91 -49
- 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/clock_bridge.py +22 -0
- package/scripts/content_boundary.py +46 -0
- package/scripts/content_security.py +119 -4
- package/scripts/content_signals.py +26 -5
- package/scripts/cross_lingual_trace.py +293 -0
- package/scripts/dispatch_result.py +105 -0
- package/scripts/engine_add.py +602 -0
- package/scripts/engine_dispatch.py +58 -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 +58 -22
- package/scripts/fetch_quality.py +13 -0
- package/scripts/fetch_v3.py +28 -3
- package/scripts/gen_routing_snapshot.py +11 -0
- package/scripts/http_client.py +24 -38
- 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 +192 -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/net_proxy.py +27 -0
- package/scripts/output_transparency.py +200 -0
- package/scripts/paid_policy.py +98 -0
- package/scripts/quota.py +158 -13
- package/scripts/recovery.py +7 -3
- 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 +26 -5
- package/scripts/route_domains.py +170 -31
- package/scripts/safe_conn.py +52 -0
- 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 +8 -1
- package/scripts/search_output.py +99 -15
- package/scripts/search_pipeline.py +62 -195
- package/scripts/search_rank.py +6 -0
- package/scripts/search_types.py +107 -9
- package/scripts/semantic_evidence.py +179 -3
- package/scripts/serp_guard.py +138 -7
- package/scripts/serp_kill_log.py +25 -2
- package/scripts/stats_cli.py +13 -1
- package/scripts/tail_completion.py +212 -0
- package/scripts/url_safety.py +126 -3
- package/scripts/usage_log.py +45 -0
- 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/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
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""cross_lingual_trace.py — 跨语言长程搜索与多源任务溯源引擎。
|
|
3
|
+
|
|
4
|
+
架构定位:
|
|
5
|
+
解耦自 research.py 与 wide_research.py。当单语言(如中文或特定小语种)
|
|
6
|
+
信息贫瘠、被低质农场包围,或任务明确要求事实溯源(源头论文、首发漏洞、
|
|
7
|
+
规范草案、最初作者、原始出处)时,自动触发跨语言长程追踪(Pivot & Trace),
|
|
8
|
+
并进行跨语言证据交叉比对(Reconciliation & Disagreement Check)。
|
|
9
|
+
|
|
10
|
+
核心阶段:
|
|
11
|
+
1. 需求与充分性判定(assess_cross_lingual_need):
|
|
12
|
+
检测结果稀疏度、权威源覆盖率以及溯源关键词(起源/出处/最早/first proposed 等)。
|
|
13
|
+
2. 跨语言实体锚点与溯源子查询生成(build_cross_lingual_trace_queries):
|
|
14
|
+
将查询投影至目标语种(以英文为主,或日/德/法垂直语境),生成多阶段溯源子查询。
|
|
15
|
+
3. 跨语言多源证据对账与综合(synthesize_cross_lingual_evidence):
|
|
16
|
+
将跨语言采集回来的结果与原语言结果按一手源等级、时序和互补关系对齐,
|
|
17
|
+
沉淀出 primary_origins(一手源)、complements(互补证据)与 disagreements(口径差异)。
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import re
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# 溯源与跨语言强触发词
|
|
27
|
+
_TRACING_KEYWORDS = (
|
|
28
|
+
"溯源", "起源", "出处", "最初", "源头", "提出者", "发明", "草案",
|
|
29
|
+
"原型", "首发", "首个", "第一篇", "历史沿革", "来龙去脉",
|
|
30
|
+
"origin", "provenance", "root cause", "first proposed", "author of",
|
|
31
|
+
"initial draft", "whitepaper", "specification", "cve-", "rfc "
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
_SOURCE_TYPE_RANKS = {
|
|
35
|
+
"official": 4,
|
|
36
|
+
"academic": 4,
|
|
37
|
+
"standard": 4,
|
|
38
|
+
"repository": 3,
|
|
39
|
+
"reference": 3,
|
|
40
|
+
"news": 2,
|
|
41
|
+
"community": 2,
|
|
42
|
+
"general": 1,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def assess_cross_lingual_need(
|
|
47
|
+
query: str,
|
|
48
|
+
results: list[dict[str, Any]] | None,
|
|
49
|
+
domain: str = "",
|
|
50
|
+
min_sufficient_results: int = 3,
|
|
51
|
+
force_trace: bool = False,
|
|
52
|
+
) -> dict[str, Any]:
|
|
53
|
+
"""评估是否需要启动跨语言长程搜索与任务溯源。
|
|
54
|
+
|
|
55
|
+
触发条件(满足任意一条):
|
|
56
|
+
1. 显式开启 force_trace 或查询包含溯源关键词;
|
|
57
|
+
2. 结果稀疏(有效结果 < min_sufficient_results);
|
|
58
|
+
3. 高置信度/权威一手信源缺失(authority_level >= 3 的结果为 0);
|
|
59
|
+
4. 强跨语言领域(security_search, academic, package_intel, rfc)。
|
|
60
|
+
"""
|
|
61
|
+
q_low = (query or "").lower()
|
|
62
|
+
has_tracing_keyword = any(kw in q_low for kw in _TRACING_KEYWORDS)
|
|
63
|
+
|
|
64
|
+
valid_results = [r for r in (results or []) if isinstance(r, dict)]
|
|
65
|
+
res_count = len(valid_results)
|
|
66
|
+
|
|
67
|
+
high_auth_count = sum(
|
|
68
|
+
1 for r in valid_results
|
|
69
|
+
if float(r.get("authority") or r.get("score") or 0.0) >= 0.7
|
|
70
|
+
or int(r.get("authority_level") or 0) >= 3
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
domain_low = (domain or "").lower()
|
|
74
|
+
is_cross_lingual_domain = any(
|
|
75
|
+
d in domain_low for d in ("security", "academic", "package", "rfc", "macro", "us_stock")
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
is_sparse = res_count < min_sufficient_results
|
|
79
|
+
no_authoritative = (res_count > 0 and high_auth_count == 0)
|
|
80
|
+
|
|
81
|
+
needed = force_trace or has_tracing_keyword or is_sparse or no_authoritative or (
|
|
82
|
+
is_cross_lingual_domain and high_auth_count < 2
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
reasons: list[str] = []
|
|
86
|
+
if force_trace:
|
|
87
|
+
reasons.append("显式指定跨语言长程溯源")
|
|
88
|
+
if has_tracing_keyword:
|
|
89
|
+
reasons.append("命中溯源意图关键词")
|
|
90
|
+
if is_sparse:
|
|
91
|
+
reasons.append(f"原语言结果不足(仅 {res_count} 条,门槛 {min_sufficient_results})")
|
|
92
|
+
if no_authoritative:
|
|
93
|
+
reasons.append("原语言缺乏高置信度一手权威信源")
|
|
94
|
+
if is_cross_lingual_domain:
|
|
95
|
+
reasons.append(f"领域「{domain}」天然具备跨语言溯源需求")
|
|
96
|
+
|
|
97
|
+
target_langs = ["en"]
|
|
98
|
+
if re.search(r"[\u3040-\u309f\u30a0-\u30ff]", query):
|
|
99
|
+
target_langs = ["en", "zh"]
|
|
100
|
+
elif re.search(r"(日企|日本|动漫|acg|丰田|索尼|任天堂)", query, re.I):
|
|
101
|
+
target_langs = ["en", "ja"]
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
"needed": bool(needed),
|
|
105
|
+
"target_langs": target_langs,
|
|
106
|
+
"is_tracing": bool(has_tracing_keyword or force_trace),
|
|
107
|
+
"is_sparse": bool(is_sparse),
|
|
108
|
+
"reasons": reasons,
|
|
109
|
+
"high_auth_count": high_auth_count,
|
|
110
|
+
"result_count": res_count,
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _extract_core_entity(query: str) -> str:
|
|
115
|
+
"""从复杂查询中提取核心溯源实体(英文锚点或关键名词)。"""
|
|
116
|
+
q = (query or "").strip()
|
|
117
|
+
# 优先抽取其中的西文标识(如 CVE-2024-1234, React, Transformer, SQLite 等)
|
|
118
|
+
ascii_matches = re.findall(r"[A-Za-z0-9_\-\.\:\/]{3,}", q)
|
|
119
|
+
if ascii_matches:
|
|
120
|
+
# 过滤掉通用词
|
|
121
|
+
clean_terms = [m for m in ascii_matches if m.lower() not in ("what", "how", "why", "when", "with")]
|
|
122
|
+
if clean_terms:
|
|
123
|
+
return " ".join(clean_terms[:3])
|
|
124
|
+
|
|
125
|
+
# 如果纯 CJK,剥离常见的动词与提问词
|
|
126
|
+
cleaned = re.sub(
|
|
127
|
+
r"(如何|怎么|为什么|溯源|起源|出处|最初|源头|提出者|发明|的来历|是谁|什么时候|最新|评价)",
|
|
128
|
+
" ",
|
|
129
|
+
q,
|
|
130
|
+
).strip()
|
|
131
|
+
words = [w for w in re.split(r"\s+", cleaned) if len(w) >= 2]
|
|
132
|
+
return words[0] if words else q
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def build_cross_lingual_trace_queries(
|
|
136
|
+
query: str,
|
|
137
|
+
target_lang: str = "en",
|
|
138
|
+
focus: str = "comprehensive",
|
|
139
|
+
) -> list[dict[str, str]]:
|
|
140
|
+
"""生成分阶段跨语言长程溯源子查询。
|
|
141
|
+
|
|
142
|
+
返回结构化的子查询列表,标注意图(intent)、策略(strategy)与语言标记。
|
|
143
|
+
"""
|
|
144
|
+
entity = _extract_core_entity(query)
|
|
145
|
+
subs: list[dict[str, str]] = []
|
|
146
|
+
|
|
147
|
+
if target_lang == "en":
|
|
148
|
+
# 阶段 1:一手原始出处与概念提出(Origin & Inception)
|
|
149
|
+
subs.append({
|
|
150
|
+
"query": f"{entity} original paper first proposed inception origin",
|
|
151
|
+
"intent": "原始论文/源头提出者与发布背景溯源",
|
|
152
|
+
"strategy": "origin_trace",
|
|
153
|
+
"lang": "en",
|
|
154
|
+
})
|
|
155
|
+
# 阶段 2:规范、代码仓与官方权威文档(Specification & Implementation)
|
|
156
|
+
subs.append({
|
|
157
|
+
"query": f"{entity} official repository documentation specification RFC",
|
|
158
|
+
"intent": "官方规范、权威仓库与技术标准追踪",
|
|
159
|
+
"strategy": "primary_spec",
|
|
160
|
+
"lang": "en",
|
|
161
|
+
})
|
|
162
|
+
# 阶段 3:演进考证与关键争议(Evolution & Historical Milestone)
|
|
163
|
+
subs.append({
|
|
164
|
+
"query": f"{entity} milestone evolution history benchmark analysis",
|
|
165
|
+
"intent": "历史演进里程碑与事实交叉考证",
|
|
166
|
+
"strategy": "cross_check",
|
|
167
|
+
"lang": "en",
|
|
168
|
+
})
|
|
169
|
+
elif target_lang == "ja":
|
|
170
|
+
subs.append({
|
|
171
|
+
"query": f"{entity} 開発経緯 歴史 初出 仕様",
|
|
172
|
+
"intent": "日文原始出处与开发历史溯源",
|
|
173
|
+
"strategy": "origin_trace",
|
|
174
|
+
"lang": "ja",
|
|
175
|
+
})
|
|
176
|
+
subs.append({
|
|
177
|
+
"query": f"{entity} 公式ドキュメント 仕様書 論文",
|
|
178
|
+
"intent": "日文官方规范与文档追踪",
|
|
179
|
+
"strategy": "primary_spec",
|
|
180
|
+
"lang": "ja",
|
|
181
|
+
})
|
|
182
|
+
else:
|
|
183
|
+
subs.append({
|
|
184
|
+
"query": f"{entity} origin official source history",
|
|
185
|
+
"intent": f"{target_lang} 语种源头与官方文档追踪",
|
|
186
|
+
"strategy": "origin_trace",
|
|
187
|
+
"lang": target_lang,
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
return subs
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _classify_source_nature(url: str, title: str) -> str:
|
|
194
|
+
"""根据 URL 和标题对信源进行性质分类。"""
|
|
195
|
+
u_low = (url or "").lower()
|
|
196
|
+
t_low = (title or "").lower()
|
|
197
|
+
|
|
198
|
+
if any(h in u_low for h in ("arxiv.org", "doi.org", "acm.org", "ieee.org", "biorxiv.org", "semanticscholar.org")):
|
|
199
|
+
return "academic"
|
|
200
|
+
if any(h in u_low for h in ("github.com", "gitlab.com", "bitbucket.org", "git.kernel.org")):
|
|
201
|
+
return "repository"
|
|
202
|
+
if any(h in u_low for h in ("rfc-editor.org", "w3.org", "iso.org", "ecma-international.org", "cve.mitre.org", "nvd.nist.gov")):
|
|
203
|
+
return "standard"
|
|
204
|
+
if any(h in u_low for h in (".gov", ".edu", "europa.eu", "who.int", "un.org")):
|
|
205
|
+
return "official"
|
|
206
|
+
if any(h in u_low for h in ("wikipedia.org", "wikidata.org", "baike.baidu.com")):
|
|
207
|
+
return "reference"
|
|
208
|
+
if any(k in t_low for k in ("official", "announcement", "release note", "white paper", "specification")):
|
|
209
|
+
return "official"
|
|
210
|
+
return "general"
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def synthesize_cross_lingual_evidence(
|
|
214
|
+
primary_results: list[dict[str, Any]],
|
|
215
|
+
cross_results: list[dict[str, Any]],
|
|
216
|
+
query: str,
|
|
217
|
+
) -> dict[str, Any]:
|
|
218
|
+
"""将原语言与跨语言长程检索证据进行交叉对账。
|
|
219
|
+
|
|
220
|
+
产出:
|
|
221
|
+
- primary_origins: 权威一手信源(论文、代码仓、标准机构)
|
|
222
|
+
- complements: 跨语言带来的增量补充事实
|
|
223
|
+
- disagreements: 跨语言比对中发现的时间/口径分歧
|
|
224
|
+
- provenance_chain: 结构化溯源脉络
|
|
225
|
+
"""
|
|
226
|
+
seen_urls: set[str] = set()
|
|
227
|
+
primary_origins: list[dict[str, Any]] = []
|
|
228
|
+
complements: list[dict[str, Any]] = []
|
|
229
|
+
disagreements: list[str] = []
|
|
230
|
+
|
|
231
|
+
all_cross = [r for r in cross_results if isinstance(r, dict)]
|
|
232
|
+
|
|
233
|
+
# 1. 甄选跨语言一手源
|
|
234
|
+
for r in all_cross:
|
|
235
|
+
u = r.get("url") or ""
|
|
236
|
+
if not u or u in seen_urls:
|
|
237
|
+
continue
|
|
238
|
+
seen_urls.add(u)
|
|
239
|
+
t = r.get("title") or ""
|
|
240
|
+
nature = _classify_source_nature(u, t)
|
|
241
|
+
entry = {
|
|
242
|
+
"title": t,
|
|
243
|
+
"url": u,
|
|
244
|
+
"snippet": r.get("snippet") or "",
|
|
245
|
+
"nature": nature,
|
|
246
|
+
"source": r.get("source") or "",
|
|
247
|
+
"authority_rank": _SOURCE_TYPE_RANKS.get(nature, 1),
|
|
248
|
+
}
|
|
249
|
+
if nature in ("academic", "standard", "official", "repository"):
|
|
250
|
+
primary_origins.append(entry)
|
|
251
|
+
else:
|
|
252
|
+
complements.append(entry)
|
|
253
|
+
|
|
254
|
+
# 排序:权威一手源排在最前
|
|
255
|
+
primary_origins.sort(key=lambda x: x["authority_rank"], reverse=True)
|
|
256
|
+
|
|
257
|
+
# 2. 检查年份/时间戳分歧
|
|
258
|
+
year_re = re.compile(r"\b(19\d{2}|20\d{2})\b")
|
|
259
|
+
prim_years = set()
|
|
260
|
+
for r in primary_results:
|
|
261
|
+
text = f"{r.get('title', '')} {r.get('snippet', '')}"
|
|
262
|
+
prim_years.update(year_re.findall(text))
|
|
263
|
+
|
|
264
|
+
cross_years = set()
|
|
265
|
+
for r in all_cross:
|
|
266
|
+
text = f"{r.get('title', '')} {r.get('snippet', '')}"
|
|
267
|
+
cross_years.update(year_re.findall(text))
|
|
268
|
+
|
|
269
|
+
if prim_years and cross_years and not (prim_years & cross_years):
|
|
270
|
+
disagreements.append(
|
|
271
|
+
f"跨语言时间线存在差异:原语言提及时段 {sorted(prim_years)},而外文一手源提及时段 {sorted(cross_years)}"
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
# 3. 构造溯源链条
|
|
275
|
+
provenance_chain: list[dict[str, str]] = []
|
|
276
|
+
for origin in primary_origins[:4]:
|
|
277
|
+
provenance_chain.append({
|
|
278
|
+
"stage": "一手信源定位",
|
|
279
|
+
"source_type": origin["nature"],
|
|
280
|
+
"title": origin["title"][:80],
|
|
281
|
+
"url": origin["url"],
|
|
282
|
+
"evidence": origin["snippet"][:140],
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
return {
|
|
286
|
+
"status": "completed" if (primary_origins or complements) else "no_incremental_evidence",
|
|
287
|
+
"primary_origins": primary_origins[:6],
|
|
288
|
+
"complements_count": len(complements),
|
|
289
|
+
"complements": complements[:6],
|
|
290
|
+
"disagreements": disagreements,
|
|
291
|
+
"provenance_chain": provenance_chain,
|
|
292
|
+
"total_cross_sources": len(all_cross),
|
|
293
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""dispatch_result.py — 引擎编排的产出对象与墙钟记账(2026-10-21 从 engine_dispatch 拆出)。
|
|
3
|
+
|
|
4
|
+
拆出原因:module size 门禁要求 engine_dispatch 只能减不能增,而 2026-10-21 的
|
|
5
|
+
尾等待记账需要同时扩数据类与汇总逻辑。两者都是**纯数据**(无 IO、无调度依赖),
|
|
6
|
+
放在调度器里只会让「调度策略」和「记账口径」继续互相遮挡。
|
|
7
|
+
|
|
8
|
+
记账口径(为什么不是一个数):
|
|
9
|
+
- `useful_ms`:最后一个**贡献结果**的引擎完成时刻(答案从这一刻起已在手里)。
|
|
10
|
+
- `first_sufficient_ms`:早停判据首次成立(结果够用)的时刻;no_early 域没有
|
|
11
|
+
这个时刻,保持 0。
|
|
12
|
+
- `tail_wait_ms`:结果已可交付后仍在等的时间。有早停时刻用它算,没有就用
|
|
13
|
+
「首条结果时刻」算——后者正是 no_early 域白等的来源。
|
|
14
|
+
- `abandoned_engine_ms`:被早停弃置的引擎已花的墙钟**之和**。它是并行资源
|
|
15
|
+
浪费,不等于用户多等的时间(两个引擎并行跑到最后,赢家交结果、输家被弃置,
|
|
16
|
+
用户没有任何额外等待)。`wasted_ms` 保留为它的别名,兼容旧读法。
|
|
17
|
+
|
|
18
|
+
历史教训:旧 `wasted_ms` 口径换过三次,前两版分别是「非 ok 引擎 latency 之和」
|
|
19
|
+
(并行时必然大于墙钟)与 `wall - useful`(有界并发下恒为 0)。两版都让仪表看
|
|
20
|
+
起来正常、却无法指向任何动作。这次拆成两个互不重叠的量,各自对应一个旋钮:
|
|
21
|
+
tail 指向收工时机,abandoned 指向并发/对冲预算。
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
from typing import Any
|
|
27
|
+
|
|
28
|
+
__all__ = ["DispatchResult", "summarize_dispatch"]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class DispatchResult:
|
|
32
|
+
"""编排产出 + 两个钩子(融合后仍要补搜的路径用)。
|
|
33
|
+
|
|
34
|
+
`run_one` / `ingest` 必须外露:macro_data 域的 D6 证据下限补搜在融合之后,
|
|
35
|
+
它要跑单个引擎并把结果并进同一份账(raw_results / engine_outcomes /
|
|
36
|
+
engine_latency / abandoned 计数),另起一份会让 engines_used 与配额记账对不上。
|
|
37
|
+
|
|
38
|
+
`budget_used_ms` / `budget_total_ms`:本次编排的实际墙钟消耗与总预算
|
|
39
|
+
(deep 等无预算模式 total 为 None)。搜索输出把它挂进 timing.budget,
|
|
40
|
+
让「这次离预算上限还有多远」可观测,而不必反推 process_ms。
|
|
41
|
+
|
|
42
|
+
`stranded`:早停弃置且仍存活的引擎 ``[(engine, holder, now_fn), ...]``,
|
|
43
|
+
交 tail_completion 后台收割(学习器记真实成绩 + 合并写 L2 缓存)。主响应
|
|
44
|
+
与 cancelled 记账不受它影响——它改的是「交付被丢弃」这一件事。
|
|
45
|
+
|
|
46
|
+
记账字段的语义与历史见模块 docstring;`wasted_ms` 仅为兼容别名。
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
__slots__ = ("raw_results", "engine_outcomes", "engine_latency",
|
|
50
|
+
"wasted_ms", "early_stopped", "run_one", "ingest",
|
|
51
|
+
"budget_used_ms", "budget_total_ms", "useful_ms",
|
|
52
|
+
"first_sufficient_ms", "tail_wait_ms", "abandoned_engine_ms",
|
|
53
|
+
"stranded")
|
|
54
|
+
|
|
55
|
+
def __init__(self, raw_results: Any, engine_outcomes: Any,
|
|
56
|
+
engine_latency: Any, wasted_ms: int, early_stopped: bool,
|
|
57
|
+
run_one: Any, ingest: Any, budget_used_ms: int | None = None,
|
|
58
|
+
budget_total_ms: int | None = None, useful_ms: int = 0,
|
|
59
|
+
first_sufficient_ms: int = 0, tail_wait_ms: int = 0,
|
|
60
|
+
abandoned_engine_ms: int | None = None,
|
|
61
|
+
stranded: Any = ()) -> None:
|
|
62
|
+
self.raw_results = raw_results
|
|
63
|
+
self.engine_outcomes = engine_outcomes
|
|
64
|
+
self.engine_latency = engine_latency
|
|
65
|
+
self.wasted_ms = wasted_ms
|
|
66
|
+
self.early_stopped = early_stopped
|
|
67
|
+
self.run_one = run_one
|
|
68
|
+
self.ingest = ingest
|
|
69
|
+
self.budget_used_ms = budget_used_ms
|
|
70
|
+
self.budget_total_ms = budget_total_ms
|
|
71
|
+
self.useful_ms = useful_ms
|
|
72
|
+
self.first_sufficient_ms = first_sufficient_ms
|
|
73
|
+
self.tail_wait_ms = tail_wait_ms
|
|
74
|
+
# 旧调用方只传 wasted_ms:别名取同一值,不引入第二个真源。
|
|
75
|
+
self.abandoned_engine_ms = (wasted_ms if abandoned_engine_ms is None
|
|
76
|
+
else abandoned_engine_ms)
|
|
77
|
+
self.stranded = tuple(stranded or ())
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def summarize_dispatch(*, wall_ms: int, contributed_ms: list[float],
|
|
81
|
+
first_result_at: float | None,
|
|
82
|
+
first_sufficient_at: float | None,
|
|
83
|
+
base: float, abandoned_ms: list[float]) -> dict[str, int]:
|
|
84
|
+
"""把调度器的事件时刻折算成对外记账字段(纯函数,便于单测)。
|
|
85
|
+
|
|
86
|
+
`base` 是本次编排的单调钟起点;两个 `*_at` 都是绝对时刻。没有可交付结果
|
|
87
|
+
(全失败)时尾等待记 0:没有东西可交付,那段等待不是「可省的」。
|
|
88
|
+
"""
|
|
89
|
+
useful_ms = int(min(max(contributed_ms), wall_ms)) if contributed_ms else 0
|
|
90
|
+
|
|
91
|
+
def _rel(at: float | None) -> int:
|
|
92
|
+
if at is None:
|
|
93
|
+
return 0
|
|
94
|
+
return int(min(max(at - base, 0.0), wall_ms / 1000.0) * 1000)
|
|
95
|
+
|
|
96
|
+
first_result_ms = _rel(first_result_at)
|
|
97
|
+
first_sufficient_ms = _rel(first_sufficient_at)
|
|
98
|
+
anchor = first_sufficient_ms or first_result_ms
|
|
99
|
+
return {
|
|
100
|
+
"useful_ms": useful_ms,
|
|
101
|
+
"first_result_ms": first_result_ms,
|
|
102
|
+
"first_sufficient_ms": first_sufficient_ms,
|
|
103
|
+
"tail_wait_ms": max(0, wall_ms - anchor) if anchor else 0,
|
|
104
|
+
"abandoned_engine_ms": int(sum(abandoned_ms)),
|
|
105
|
+
}
|