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
|
@@ -44,15 +44,112 @@ def top_engine_latency(engine_latency: dict[str, Any] | None,
|
|
|
44
44
|
|
|
45
45
|
|
|
46
46
|
def usage_log_attribution(engine_latency: dict[str, Any] | None,
|
|
47
|
-
top_n: int = DEFAULT_TOP_N
|
|
48
|
-
|
|
47
|
+
top_n: int = DEFAULT_TOP_N,
|
|
48
|
+
engine_outcomes: Iterable[dict[str, Any]] | None = None
|
|
49
|
+
) -> dict[str, Any]:
|
|
50
|
+
"""usage_log query 流的归因字段(慢源 Top-N + 失败类别)。
|
|
49
51
|
|
|
50
52
|
返回 dict 以便调用方 **展开进日志记录**,未来新增归因维度(如
|
|
51
53
|
「等待预算被谁耗掉」「缓存未命中的原因分布」)时只扩这里,
|
|
52
54
|
主管线那一行 `**usage_log_attribution(...)` 不动——归因是会长大的,
|
|
53
55
|
写入点不该跟着长(主管线有模块大小门禁,见模块 docstring)。
|
|
56
|
+
|
|
57
|
+
失败类别只记「哪类」不记条数:errors 已经记了条数,两者互补。
|
|
58
|
+
"""
|
|
59
|
+
out: dict[str, Any] = {
|
|
60
|
+
"engine_latency_top": top_engine_latency(engine_latency, top_n),
|
|
61
|
+
}
|
|
62
|
+
kinds = failure_kinds(engine_outcomes)
|
|
63
|
+
if kinds:
|
|
64
|
+
out["failure_kinds"] = kinds
|
|
65
|
+
return out
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# 合成引擎名(只存在于基准/混沌测试的 fake combo)。它们出现在 engines_used
|
|
69
|
+
# 里就说明这一跑不是真实检索;写进生产 usage_log 会污染慢源排行与总量统计
|
|
70
|
+
# (2026-10-21 实测 "hedge bench" 32 条,dead_primary/fast_backup 进了慢源榜)。
|
|
71
|
+
_SYNTHETIC_ENGINES: frozenset[str] = frozenset({
|
|
72
|
+
"dead_primary", "dead_backup", "fast_backup", "slow_backup",
|
|
73
|
+
"slow_primary", "quick_primary", "fast_primary",
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def is_synthetic_run(engines: Iterable[str] | None) -> bool:
|
|
78
|
+
"""这组引擎是否来自基准/测试的合成组合(有一个即判合成)。"""
|
|
79
|
+
return any(str(e) in _SYNTHETIC_ENGINES for e in (engines or []))
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# 「源被跳过/没资格调用」的状态:空结果时要把它们与「查询没有召回」分开。
|
|
83
|
+
# 只收持续性状态(配额/缺 env/鉴权),不含 timeout/error——后者是本次
|
|
84
|
+
# 真的失败了,原表述已够用。
|
|
85
|
+
SKIPPED_SOURCE_STATUSES: frozenset[str] = frozenset({
|
|
86
|
+
"skipped-quota-exhausted", "skipped-missing-env",
|
|
87
|
+
"quota-exhausted", "auth-failed",
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def skipped_sources(engine_outcomes: Iterable[dict[str, Any]] | None,
|
|
92
|
+
limit: int = 4) -> list[str]:
|
|
93
|
+
"""空结果时被跳过/鉴权失败的引擎名(去重、保序、限量)。"""
|
|
94
|
+
out: list[str] = []
|
|
95
|
+
seen: set[str] = set()
|
|
96
|
+
for o in (engine_outcomes or []):
|
|
97
|
+
if not isinstance(o, dict):
|
|
98
|
+
continue
|
|
99
|
+
if str(o.get("status") or "") not in SKIPPED_SOURCE_STATUSES:
|
|
100
|
+
continue
|
|
101
|
+
name = str(o.get("engine") or "?")
|
|
102
|
+
if name not in seen:
|
|
103
|
+
seen.add(name)
|
|
104
|
+
out.append(name)
|
|
105
|
+
if len(out) >= max(0, int(limit)):
|
|
106
|
+
break
|
|
107
|
+
return out
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def zero_result_recovery_note(recovery: Any,
|
|
111
|
+
engine_outcomes: Iterable[dict[str, Any]] | None
|
|
112
|
+
) -> str | None:
|
|
113
|
+
"""恢复链触发但没救回来时的局限声明;不需要声明时返回 None。
|
|
114
|
+
|
|
115
|
+
「没有召回」与「源被跳过/鉴权失败」处置完全不同:前者改写查询,
|
|
116
|
+
后者看 failures 或补凭证。此前一律写成 no recall,实测 bocha 配额
|
|
117
|
+
耗尽(403 not enough money)被报成「查询无召回」,调用方白跑一轮。
|
|
54
118
|
"""
|
|
55
|
-
|
|
119
|
+
if not isinstance(recovery, dict) or not recovery.get("triggered") \
|
|
120
|
+
or recovery.get("recovered"):
|
|
121
|
+
return None
|
|
122
|
+
note = recovery.get("note") or "no note"
|
|
123
|
+
skipped = skipped_sources(engine_outcomes)
|
|
124
|
+
if skipped:
|
|
125
|
+
return (
|
|
126
|
+
"recovery triggered but no usable strategy ("
|
|
127
|
+
f"{note}); empty results came from skipped/failed sources "
|
|
128
|
+
f"({', '.join(skipped)}) — see failures; this is not necessarily "
|
|
129
|
+
"a no-recall query"
|
|
130
|
+
)
|
|
131
|
+
return (
|
|
132
|
+
"recovery triggered but no usable strategy ("
|
|
133
|
+
f"{note}); empty results likely mean the query had no recall — "
|
|
134
|
+
"try rephrasing"
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def failure_kinds(engine_outcomes: Iterable[dict[str, Any]] | None,
|
|
139
|
+
limit: int = 6) -> list[str]:
|
|
140
|
+
"""本次调用的失败类别(去重、稳定排序),写进 usage_log 供长期归因。
|
|
141
|
+
|
|
142
|
+
errors 只记「几条」,回答不了「哪类失败在反复出现」;类别来自
|
|
143
|
+
output_transparency.build_failures(与 agent 档 failures 同一口径),
|
|
144
|
+
不在日志侧另写一份分类。
|
|
145
|
+
"""
|
|
146
|
+
try:
|
|
147
|
+
from output_transparency import build_failures
|
|
148
|
+
except ImportError: # pragma: no cover - 单文件分发时的降级
|
|
149
|
+
return []
|
|
150
|
+
rows = build_failures({"engine_outcomes": list(engine_outcomes or [])},
|
|
151
|
+
limit=limit)
|
|
152
|
+
return sorted({str(r.get("category") or "unknown") for r in rows})
|
|
56
153
|
|
|
57
154
|
|
|
58
155
|
def aggregate_slow_engines(rows: Iterable[dict[str, Any]],
|
package/scripts/search_cli.py
CHANGED
|
@@ -328,6 +328,27 @@ def _format_engine_summary(summary: dict[str, Any]) -> str:
|
|
|
328
328
|
return "\n".join(lines)
|
|
329
329
|
|
|
330
330
|
|
|
331
|
+
def _merge_verify_timing(timing: dict[str, Any], verify_ms: float) -> None:
|
|
332
|
+
"""把 verify 的墙钟并回 timing 的三个总数,保持互相自洽。
|
|
333
|
+
|
|
334
|
+
why:verify 发生在主搜索快照之后。此前只并回 stages_ms/process_ms,
|
|
335
|
+
elapsed_ms 仍是主搜索的数(实测 1173 vs 10967),调用方最先读的总账
|
|
336
|
+
比它的分项之和还小 8 倍。三个数必须一起走,否则「这次为什么慢」会被
|
|
337
|
+
带偏到主搜索上。
|
|
338
|
+
"""
|
|
339
|
+
total = float(timing.get("stages_ms") or 0) + verify_ms
|
|
340
|
+
timing.setdefault("stages", []).append({
|
|
341
|
+
"stage": "verify",
|
|
342
|
+
"ms": round(verify_ms, 1),
|
|
343
|
+
"pct": round(verify_ms * 100.0 / total, 1) if total > 0 else 0.0,
|
|
344
|
+
})
|
|
345
|
+
timing["stages_ms"] = round(total, 1)
|
|
346
|
+
timing["process_ms"] = round(
|
|
347
|
+
float(timing.get("process_ms") or 0) + verify_ms, 1)
|
|
348
|
+
timing["elapsed_ms"] = round(
|
|
349
|
+
max(float(timing.get("elapsed_ms") or 0), total), 1)
|
|
350
|
+
|
|
351
|
+
|
|
331
352
|
def main():
|
|
332
353
|
# 入口第一件事:钉住 stdout/stderr 编码(实现唯一,见 cli_io)。
|
|
333
354
|
# 放在这里而不是 import 期:本模块被 search.py 与 bin/argo 两条路调用,
|
|
@@ -579,19 +600,11 @@ def main():
|
|
|
579
600
|
# (process_ms 定格处)之后,净增 280–1160ms 完全不进任何账——实测
|
|
580
601
|
# wall 776–1746ms vs process_ms 494–701ms,「这次为什么慢」归因指错
|
|
581
602
|
# 方向。快照后移做不到(verify 是装配后的可选后处理),补一个 stage
|
|
582
|
-
#
|
|
603
|
+
# 并把耗时并回三个总数(stages_ms / process_ms / elapsed_ms),
|
|
604
|
+
# 三者保持自洽(2026-10-22 补 elapsed_ms)。
|
|
583
605
|
if _timing is not None and "timing" in results and args.verify:
|
|
584
606
|
_verify_ms = (time.perf_counter() - _t_verify0) * 1000.0
|
|
585
|
-
|
|
586
|
-
_total = float(_t.get("stages_ms") or 0) + _verify_ms
|
|
587
|
-
_t.setdefault("stages", []).append({
|
|
588
|
-
"stage": "verify",
|
|
589
|
-
"ms": round(_verify_ms, 1),
|
|
590
|
-
"pct": round(_verify_ms * 100.0 / _total, 1) if _total > 0 else 0.0,
|
|
591
|
-
})
|
|
592
|
-
_t["stages_ms"] = round(_total, 1)
|
|
593
|
-
_t["process_ms"] = round(
|
|
594
|
-
float(_t.get("process_ms") or 0) + _verify_ms, 1)
|
|
607
|
+
_merge_verify_timing(results["timing"], _verify_ms)
|
|
595
608
|
|
|
596
609
|
if args.json_output:
|
|
597
610
|
public = {k: v for k, v in results.items() if not k.startswith("_")}
|
package/scripts/search_entry.py
CHANGED
|
@@ -28,7 +28,7 @@ engine_dispatch。
|
|
|
28
28
|
from __future__ import annotations
|
|
29
29
|
|
|
30
30
|
import time
|
|
31
|
-
from
|
|
31
|
+
from search_types import slot_record as dataclass, slot_field as field, slot_replace as replace
|
|
32
32
|
from typing import Any, Callable, Optional
|
|
33
33
|
|
|
34
34
|
from engine_dispatch import run_dispatch
|
|
@@ -361,6 +361,11 @@ def dispatch(req: _SearchRequest, run: _SearchRun, hooks: _SearchHooks) -> _Sear
|
|
|
361
361
|
engine_latency = _dispatch.engine_latency
|
|
362
362
|
wasted_ms = _dispatch.wasted_ms
|
|
363
363
|
useful_ms = _dispatch.useful_ms
|
|
364
|
+
dispatch_tail = {
|
|
365
|
+
"first_sufficient_ms": _dispatch.first_sufficient_ms,
|
|
366
|
+
"tail_wait_ms": _dispatch.tail_wait_ms,
|
|
367
|
+
"abandoned_engine_ms": _dispatch.abandoned_engine_ms,
|
|
368
|
+
}
|
|
364
369
|
early_stopped = _dispatch.early_stopped
|
|
365
370
|
budget_used_ms = _dispatch.budget_used_ms
|
|
366
371
|
budget_total_ms = _dispatch.budget_total_ms
|
|
@@ -378,6 +383,7 @@ def dispatch(req: _SearchRequest, run: _SearchRun, hooks: _SearchHooks) -> _Sear
|
|
|
378
383
|
engine_latency=engine_latency,
|
|
379
384
|
wasted_ms=wasted_ms,
|
|
380
385
|
useful_ms=useful_ms,
|
|
386
|
+
dispatch_tail=dispatch_tail,
|
|
381
387
|
early_stopped=early_stopped,
|
|
382
388
|
budget_used_ms=budget_used_ms,
|
|
383
389
|
budget_total_ms=budget_total_ms,
|
package/scripts/search_output.py
CHANGED
|
@@ -8,10 +8,23 @@
|
|
|
8
8
|
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
|
-
from dataclasses import dataclass
|
|
12
|
-
|
|
13
11
|
from search_rank import filter_results_by_domains
|
|
12
|
+
from search_types import slot_record as dataclass
|
|
14
13
|
from typing import Any
|
|
14
|
+
from output_transparency import ( # noqa: F401
|
|
15
|
+
_AGGREGATOR_HOSTS,
|
|
16
|
+
_FAILURE_META,
|
|
17
|
+
_FAILURE_NON_ERROR,
|
|
18
|
+
_NON_ERROR_OUTCOME,
|
|
19
|
+
_SEVERITY,
|
|
20
|
+
_UNCERTAINTY_HIGH,
|
|
21
|
+
_UNCERTAINTY_LOW,
|
|
22
|
+
_UNCERTAINTY_MEDIUM,
|
|
23
|
+
_hostname,
|
|
24
|
+
build_failures,
|
|
25
|
+
build_uncertainty,
|
|
26
|
+
decorate_compact_payload,
|
|
27
|
+
)
|
|
15
28
|
|
|
16
29
|
|
|
17
30
|
# 本模块的 6 处日志都写在 **except 处理器**里,且分散在 shape_response 的
|
|
@@ -72,6 +85,39 @@ _AGENT_RESULT_FIELDS = (
|
|
|
72
85
|
)
|
|
73
86
|
|
|
74
87
|
|
|
88
|
+
def _slim_verify(v: Any) -> dict[str, Any] | None:
|
|
89
|
+
"""verify 块的 agent 精简版:保留「核验后改了什么」,丢掉逐条细节。
|
|
90
|
+
|
|
91
|
+
why:`--fields agent` 此前把整个 verify 块剥掉,而这是**唯一**能说明
|
|
92
|
+
「花几秒核验换来了什么」的字段。保留 revision_summary 与每条 delta,
|
|
93
|
+
体积约 0.2–0.8 KB;pre/post 绝对分与 title 已在 results[] 可查,不重复。
|
|
94
|
+
"""
|
|
95
|
+
if not isinstance(v, dict):
|
|
96
|
+
return None
|
|
97
|
+
out: dict[str, Any] = {}
|
|
98
|
+
for key in ("revision_summary", "pending", "skipped_cached", "semantic"):
|
|
99
|
+
if v.get(key) is not None:
|
|
100
|
+
out[key] = v[key]
|
|
101
|
+
rows = []
|
|
102
|
+
for r in v.get("verified") or []:
|
|
103
|
+
if not isinstance(r, dict):
|
|
104
|
+
continue
|
|
105
|
+
rows.append({k: r[k] for k in
|
|
106
|
+
("url", "delta", "post_absorption", "fetch_method")
|
|
107
|
+
if r.get(k) is not None})
|
|
108
|
+
if rows:
|
|
109
|
+
out["verified"] = rows
|
|
110
|
+
return out or None
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _slim_reorder(r: Any) -> dict[str, Any] | None:
|
|
114
|
+
"""证据回写排序的 agent 精简版:只留结论(是否重排、动了几处)。"""
|
|
115
|
+
if not isinstance(r, dict):
|
|
116
|
+
return None
|
|
117
|
+
out = {k: r[k] for k in ("reordered", "moved") if r.get(k) is not None}
|
|
118
|
+
return out or None
|
|
119
|
+
|
|
120
|
+
|
|
75
121
|
def _strip_for_agent(payload: dict[str, Any]) -> dict[str, Any]:
|
|
76
122
|
"""--fields agent:输出只留答案内容(P2-2,2026-09-13)。
|
|
77
123
|
|
|
@@ -93,9 +139,33 @@ def _strip_for_agent(payload: dict[str, Any]) -> dict[str, Any]:
|
|
|
93
139
|
# 阶段耗时:默认就带(--no-timing 才没有)。剥掉会让使用者看不到
|
|
94
140
|
# 「这次慢在哪」,也拿不到自己动手优化所需的依据。
|
|
95
141
|
"timing",
|
|
142
|
+
# 综合可信度:一个 level 就能决定「直接引用」还是「先核验」。
|
|
143
|
+
# 剥掉它等于让 agent 自己交叉比对来源数/同站/聚合站/引擎失败——
|
|
144
|
+
# 而那正是它最容易漏掉的一步。
|
|
145
|
+
"meta",
|
|
146
|
+
# 结构化失败清单:类别 + 是否可重试 + 建议动作(≤6 条短字段)。
|
|
147
|
+
# 只有 errors 的整行文本时,agent 知道「坏了」但不知道「该重试、
|
|
148
|
+
# 换源还是去配 key」;这两件事的处置完全不同。
|
|
149
|
+
"failures",
|
|
150
|
+
# 信任级别:网页文本一律是不可信外部数据,随答案一起到达,
|
|
151
|
+
# 避免调用方把摘要里的指令当任务执行(输出层声明,非提示词)。
|
|
152
|
+
"content_trust",
|
|
153
|
+
# 核验产出:verify/evidence_reorder 是**花了墙钟换来的**,且是
|
|
154
|
+
# 「该不该引用这批结果」的直接依据。此前被整块剥掉,调用方付了
|
|
155
|
+
# 几秒核验却只看到 verified_count 一个数(2026-10-22 实测)。
|
|
156
|
+
# 值由下方 _slim_verify/_slim_reorder 压缩,不整块照搬。
|
|
157
|
+
"verify", "evidence_reorder",
|
|
96
158
|
)
|
|
97
159
|
out: dict[str, Any] = {k: payload[k] for k in keep_top
|
|
98
160
|
if payload.get(k) is not None}
|
|
161
|
+
for _key, _slim in (("verify", _slim_verify),
|
|
162
|
+
("evidence_reorder", _slim_reorder)):
|
|
163
|
+
if payload.get(_key) is not None:
|
|
164
|
+
_value = _slim(payload[_key])
|
|
165
|
+
if _value is not None:
|
|
166
|
+
out[_key] = _value
|
|
167
|
+
else:
|
|
168
|
+
out.pop(_key, None)
|
|
99
169
|
slim_results = []
|
|
100
170
|
for r in payload.get("results") or []:
|
|
101
171
|
if not isinstance(r, dict):
|
|
@@ -219,7 +289,8 @@ def format_timing(t: dict[str, Any]) -> str:
|
|
|
219
289
|
f" 引擎并发 墙钟 {d.get('wall_ms')} ms / 引擎合计 "
|
|
220
290
|
f"{d.get('engine_sum_ms')} ms → 并发效率 "
|
|
221
291
|
f"{d.get('parallel_efficiency')}(跑了 {d.get('engines_run')} 个,"
|
|
222
|
-
f"早停={d.get('early_stopped')}
|
|
292
|
+
f"早停={d.get('early_stopped')},尾等待 {d.get('tail_wait_ms')} ms,"
|
|
293
|
+
f"弃置 {d.get('abandoned_engine_ms')} ms)")
|
|
223
294
|
if "overhead_ms" in t:
|
|
224
295
|
lines.append(
|
|
225
296
|
f" 固定开销 {t['overhead_ms']} ms"
|
|
@@ -331,18 +402,6 @@ def format_text_output(results: dict[str, Any]) -> str:
|
|
|
331
402
|
return "\n".join(lines)
|
|
332
403
|
|
|
333
404
|
|
|
334
|
-
# 不算失败的 outcome 状态:这些情况「引擎跑了、没问题」,不该出现在 errors[]
|
|
335
|
-
# cancelled = 早停收工时被主动弃置(结果已够,引擎没失败也没超窗)
|
|
336
|
-
_NON_ERROR_OUTCOME = frozenset({
|
|
337
|
-
"ok", "ok-cached", "partial", "no-results", "no-results-cached",
|
|
338
|
-
"cancelled",
|
|
339
|
-
# 配额耗尽的跳过是**持续状态**,不是本次查询的失败:标记默认持续到周期
|
|
340
|
-
# 边界(一天),若每个查询都把它塞进 errors,调用方会看到一条持续一天的
|
|
341
|
-
# 「错误」并误判本次检索降级。状态本身在 --list-engines 可见。
|
|
342
|
-
"skipped-quota-exhausted",
|
|
343
|
-
})
|
|
344
|
-
|
|
345
|
-
|
|
346
405
|
def _collect_errors(raw_results: dict[str, list[dict[str, Any]]],
|
|
347
406
|
engine_outcomes: list[dict[str, Any]] | None = None
|
|
348
407
|
) -> list[str]:
|
|
@@ -379,6 +438,7 @@ def _collect_errors(raw_results: dict[str, list[dict[str, Any]]],
|
|
|
379
438
|
_add(f"{o.get('engine')}: {detail}")
|
|
380
439
|
return errors
|
|
381
440
|
|
|
441
|
+
|
|
382
442
|
@dataclass(frozen=True)
|
|
383
443
|
class _ShapeContext:
|
|
384
444
|
"""输出成形的全部选项(请求侧只读 + 一个本地检索钩子)。"""
|
|
@@ -603,4 +663,28 @@ def shape_response(ctx: _ShapeContext, result: dict[str, Any]) -> dict[str, Any]
|
|
|
603
663
|
# 待核验 0 条」这类自相矛盾的口径。
|
|
604
664
|
_el["suggested"] = _sug
|
|
605
665
|
_el["pending_count"] = len(_sug)
|
|
666
|
+
# 综合可信度:读的 results 必须与最终输出同源,所以放在收口(与 count
|
|
667
|
+
# 重算同处)——域过滤与本地命中并入都发生在这之前。只读不写结果,
|
|
668
|
+
# 失败时静默跳过(增强失败不得让一次搜索失败)。
|
|
669
|
+
try:
|
|
670
|
+
result["meta"] = {"uncertainty": build_uncertainty(result)}
|
|
671
|
+
except Exception as e:
|
|
672
|
+
_log(f"可信度评估跳过: {type(e).__name__}")
|
|
673
|
+
# 结构化失败清单:与 meta 同一收口,读的 outcome 与漏斗/可信度同源。
|
|
674
|
+
# 失败时静默跳过(增强失败不得让一次搜索失败)。
|
|
675
|
+
try:
|
|
676
|
+
failures = build_failures(result)
|
|
677
|
+
if failures:
|
|
678
|
+
result["failures"] = failures
|
|
679
|
+
except Exception as e:
|
|
680
|
+
_log(f"失败清单跳过: {type(e).__name__}")
|
|
681
|
+
# 信任级别与边界清洗:所有入口(full/agent/MCP)共用同一个收口,
|
|
682
|
+
# 避免 CLI/agent 档带 content_trust 却未剥离网页自带的 </untrusted-web-content>。
|
|
683
|
+
try:
|
|
684
|
+
from content_boundary import (CONTENT_TRUST_UNTRUSTED,
|
|
685
|
+
sanitize_results_boundary)
|
|
686
|
+
sanitize_results_boundary(result.get("results"))
|
|
687
|
+
result["content_trust"] = CONTENT_TRUST_UNTRUSTED
|
|
688
|
+
except Exception:
|
|
689
|
+
pass
|
|
606
690
|
return result
|