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
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
research.py — 深度研究工具(wigolo research 理念移植)
|
|
4
|
+
|
|
5
|
+
核心能力:
|
|
6
|
+
1. 问题分解:将复杂查询拆分为 3-5 个子查询
|
|
7
|
+
2. 多源采集:对每个子查询并行执行搜索
|
|
8
|
+
3. 综合报告:合并去重 + 来源标注 + 知识缺口识别
|
|
9
|
+
4. 引用追踪:每个结论可追溯到具体搜索结果
|
|
10
|
+
|
|
11
|
+
用法:
|
|
12
|
+
python3 research.py "CRISPR-Cas9 脱靶效应的 AI 预测方法综述"
|
|
13
|
+
python3 research.py "CVE-2024-6387 生产环境影响评估" --depth deep
|
|
14
|
+
python3 research.py "台积电财报分歧分析" --sub-queries 5
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import argparse
|
|
20
|
+
import json
|
|
21
|
+
import os
|
|
22
|
+
import sys
|
|
23
|
+
import time
|
|
24
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
25
|
+
from typing import Any
|
|
26
|
+
|
|
27
|
+
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
28
|
+
sys.path.insert(0, SCRIPT_DIR)
|
|
29
|
+
|
|
30
|
+
from search import super_search, rrf_merge, deduplicate_by_url
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# ── 交叉引用检测 ──────────────────────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
def detect_cross_references(results: list[dict[str, Any]], min_sources: int = 2,
|
|
36
|
+
min_ngram_len: int = 3) -> list[dict[str, Any]]:
|
|
37
|
+
"""检测多个来源的交叉引用(n-gram 重叠)。
|
|
38
|
+
|
|
39
|
+
如果同一 n-gram 出现在 ≥min_sources 个不同域名的结果中,
|
|
40
|
+
标记为「潜在佐证」。
|
|
41
|
+
"""
|
|
42
|
+
import re
|
|
43
|
+
from urllib.parse import urlparse
|
|
44
|
+
|
|
45
|
+
# 提取所有 snippet 的 n-gram
|
|
46
|
+
ngram_sources: dict[str, set] = {} # ngram -> set of (url, title)
|
|
47
|
+
for r in results:
|
|
48
|
+
url = r.get("url", "")
|
|
49
|
+
domain = urlparse(url).netloc.lower().strip("www.") if url else "unknown"
|
|
50
|
+
text = f"{r.get('title', '')} {r.get('snippet', '')}"
|
|
51
|
+
# 简单分词(中英文混合)
|
|
52
|
+
# 英文按空格分
|
|
53
|
+
en_tokens = re.findall(r"[a-zA-Z]+", text.lower())
|
|
54
|
+
# 中文按字符 bigram/trigram
|
|
55
|
+
cn_chars = re.findall(r"[\u4e00-\u9fff]+", text)
|
|
56
|
+
cn_tokens = []
|
|
57
|
+
for seg in cn_chars:
|
|
58
|
+
for i in range(len(seg) - min_ngram_len + 1):
|
|
59
|
+
cn_tokens.append(seg[i:i + min_ngram_len])
|
|
60
|
+
|
|
61
|
+
all_tokens = en_tokens + cn_tokens
|
|
62
|
+
for n in range(min_ngram_len, min(min_ngram_len + 1, len(all_tokens) + 1)):
|
|
63
|
+
for i in range(len(all_tokens) - n + 1):
|
|
64
|
+
ngram = " ".join(all_tokens[i:i + n])
|
|
65
|
+
if len(ngram) >= 4: # 过滤太短的 ngram
|
|
66
|
+
ngram_sources.setdefault(ngram, set()).add(domain)
|
|
67
|
+
|
|
68
|
+
# 找出被多个来源佐证的 n-gram
|
|
69
|
+
cross_refs = []
|
|
70
|
+
for ngram, domains in ngram_sources.items():
|
|
71
|
+
if len(domains) >= min_sources:
|
|
72
|
+
cross_refs.append({
|
|
73
|
+
"ngram": ngram,
|
|
74
|
+
"source_count": len(domains),
|
|
75
|
+
"domains": sorted(domains),
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
# 按来源数排序,取 top 10
|
|
79
|
+
cross_refs.sort(key=lambda x: x["source_count"], reverse=True)
|
|
80
|
+
return cross_refs[:10]
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
# ── 问题分解 ──────────────────────────────────────────────────────────────────
|
|
84
|
+
|
|
85
|
+
def decompose_query(query: str, num_sub: int = 4) -> list[dict[str, str]]:
|
|
86
|
+
"""将复杂查询分解为子查询。
|
|
87
|
+
|
|
88
|
+
策略:基于关键词特征自动分解,不依赖 LLM。
|
|
89
|
+
"""
|
|
90
|
+
sub_queries = []
|
|
91
|
+
|
|
92
|
+
# 策略 1:中英文混合 → 分语言搜索
|
|
93
|
+
has_chinese = any("\u4e00" <= c <= "\u9fff" for c in query)
|
|
94
|
+
has_english = any(c.isascii() and c.isalpha() for c in query)
|
|
95
|
+
|
|
96
|
+
if has_chinese and has_english:
|
|
97
|
+
# 提取英文核心词
|
|
98
|
+
eng_words = " ".join(w for w in query.split() if w.isascii() and len(w) > 2)
|
|
99
|
+
if eng_words:
|
|
100
|
+
sub_queries.append({
|
|
101
|
+
"query": eng_words,
|
|
102
|
+
"intent": "英文核心概念搜索",
|
|
103
|
+
"strategy": "english_focused"
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
# 策略 2:包含年份/时间 → 补充时效性搜索
|
|
107
|
+
import re
|
|
108
|
+
year_match = re.search(r"20\d{2}", query)
|
|
109
|
+
if year_match:
|
|
110
|
+
year = year_match.group()
|
|
111
|
+
sub_queries.append({
|
|
112
|
+
"query": f"{query} {year} latest update",
|
|
113
|
+
"intent": f"{year}年最新进展",
|
|
114
|
+
"strategy": "temporal"
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
# 策略 3:包含对比词 → 分别搜索各对象
|
|
118
|
+
compare_match = re.search(r"(?:vs| versus |对比|比较|和|与|及)", query, re.I)
|
|
119
|
+
if compare_match:
|
|
120
|
+
parts = re.split(r"(?:vs| versus |对比|比较|和|与|及)", query, flags=re.I)
|
|
121
|
+
for part in parts[:2]:
|
|
122
|
+
part = part.strip()
|
|
123
|
+
if part and len(part) > 2:
|
|
124
|
+
sub_queries.append({
|
|
125
|
+
"query": part,
|
|
126
|
+
"intent": f"独立搜索:{part[:20]}",
|
|
127
|
+
"strategy": "split_compare"
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
# 策略 4:包含「如何/怎么/why」→ 补充教程/方案搜索
|
|
131
|
+
how_match = re.search(r"(?:如何|怎么|how|why|为什么|最佳实践|best practice)", query, re.I)
|
|
132
|
+
if how_match:
|
|
133
|
+
sub_queries.append({
|
|
134
|
+
"query": f"{query} tutorial guide best practices",
|
|
135
|
+
"intent": "教程/最佳实践",
|
|
136
|
+
"strategy": "tutorial"
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
# 策略 5:包含「问题/bug/错误」→ 补充社区讨论搜索
|
|
140
|
+
bug_match = re.search(r"(?:bug|error|问题|报错|故障|issue|panic|crash|exception)", query, re.I)
|
|
141
|
+
if bug_match:
|
|
142
|
+
sub_queries.append({
|
|
143
|
+
"query": f"{query} solution fix workaround community",
|
|
144
|
+
"intent": "社区解决方案",
|
|
145
|
+
"strategy": "community_fix"
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
# 策略 6:包含「论文/学术」→ 补充学术搜索
|
|
149
|
+
academic_match = re.search(r"(?:论文|paper|arxiv|学术|综述|review|survey|研究)", query, re.I)
|
|
150
|
+
if academic_match:
|
|
151
|
+
sub_queries.append({
|
|
152
|
+
"query": f"{query} arxiv semantic scholar 2024 2025",
|
|
153
|
+
"intent": "学术文献补充",
|
|
154
|
+
"strategy": "academic"
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
# 策略 7:包含「安全/CVE」→ 补充安全源
|
|
158
|
+
security_match = re.search(r"(?:CVE|漏洞|vulnerability|security|exploit|PoC)", query, re.I)
|
|
159
|
+
if security_match:
|
|
160
|
+
sub_queries.append({
|
|
161
|
+
"query": f"{query} NVD exploit PoC advisory",
|
|
162
|
+
"intent": "安全数据源补充",
|
|
163
|
+
"strategy": "security"
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
# 策略 8:包含「金融/股票/财报」→ 补充金融源
|
|
167
|
+
finance_match = re.search(r"(?:股价|财报|基金|股票|行情|金融|financial|earnings|stock)", query, re.I)
|
|
168
|
+
if finance_match:
|
|
169
|
+
sub_queries.append({
|
|
170
|
+
"query": f"{query} 东方财富 雪球 研报",
|
|
171
|
+
"intent": "金融数据补充",
|
|
172
|
+
"strategy": "finance"
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
# 确保至少有原始查询
|
|
176
|
+
if not sub_queries:
|
|
177
|
+
sub_queries.append({
|
|
178
|
+
"query": query,
|
|
179
|
+
"intent": "原始查询",
|
|
180
|
+
"strategy": "direct"
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
# 补充通用搜索
|
|
184
|
+
if len(sub_queries) < num_sub:
|
|
185
|
+
sub_queries.append({
|
|
186
|
+
"query": query,
|
|
187
|
+
"intent": "综合搜索",
|
|
188
|
+
"strategy": "general"
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
return _deduplicate_sub_queries(sub_queries[:num_sub])
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _deduplicate_sub_queries(sub_queries: list[dict[str, str]]) -> list[dict[str, str]]:
|
|
195
|
+
"""基于 Jaccard 相似度去重子查询。"""
|
|
196
|
+
import re as _re
|
|
197
|
+
def _tokens(q: str) -> set:
|
|
198
|
+
return set(_re.findall(r'[a-zA-Z]+|[\u4e00-\u9fff]', q.lower()))
|
|
199
|
+
unique = []
|
|
200
|
+
seen_tokens = []
|
|
201
|
+
for sq in sub_queries:
|
|
202
|
+
tokens = _tokens(sq["query"])
|
|
203
|
+
is_dup = False
|
|
204
|
+
for prev in seen_tokens:
|
|
205
|
+
jaccard = len(tokens & prev) / max(len(tokens | prev), 1)
|
|
206
|
+
if jaccard > 0.6:
|
|
207
|
+
is_dup = True
|
|
208
|
+
break
|
|
209
|
+
if not is_dup:
|
|
210
|
+
unique.append(sq)
|
|
211
|
+
seen_tokens.append(tokens)
|
|
212
|
+
return unique
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
# ── 多源采集 ──────────────────────────────────────────────────────────────────
|
|
216
|
+
|
|
217
|
+
def collect_sources(sub_queries: list[dict[str, str]], max_results: int = 5,
|
|
218
|
+
timeout: int = 15, depth: str = "balanced",
|
|
219
|
+
mode: str = "auto") -> dict[str, Any]:
|
|
220
|
+
"""对每个子查询并行执行搜索,返回聚合结果。"""
|
|
221
|
+
all_results = []
|
|
222
|
+
engines_used = set()
|
|
223
|
+
sub_results = []
|
|
224
|
+
t0 = time.time()
|
|
225
|
+
|
|
226
|
+
def _search_one(sq: dict[str, str]) -> dict[str, Any]:
|
|
227
|
+
result = super_search(
|
|
228
|
+
sq["query"], n=max_results, timeout=timeout,
|
|
229
|
+
depth=depth, mode=mode, skip_cache=False
|
|
230
|
+
)
|
|
231
|
+
return {
|
|
232
|
+
"sub_query": sq["query"],
|
|
233
|
+
"intent": sq["intent"],
|
|
234
|
+
"strategy": sq["strategy"],
|
|
235
|
+
"results": result.get("results", []),
|
|
236
|
+
"engines_used": result.get("engines_used", []),
|
|
237
|
+
"elapsed_ms": result.get("elapsed_ms", 0),
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
with ThreadPoolExecutor(max_workers=min(len(sub_queries), 4)) as ex:
|
|
241
|
+
futures = {ex.submit(_search_one, sq): sq for sq in sub_queries}
|
|
242
|
+
all_futures = list(futures.keys())
|
|
243
|
+
try:
|
|
244
|
+
for fut in as_completed(futures, timeout=timeout * 2 + 5):
|
|
245
|
+
try:
|
|
246
|
+
sr = fut.result()
|
|
247
|
+
sub_results.append(sr)
|
|
248
|
+
all_results.extend(sr["results"])
|
|
249
|
+
engines_used.update(sr["engines_used"])
|
|
250
|
+
except Exception as e:
|
|
251
|
+
sq = futures[fut]
|
|
252
|
+
sub_results.append({
|
|
253
|
+
"sub_query": sq["query"],
|
|
254
|
+
"intent": sq["intent"],
|
|
255
|
+
"strategy": sq["strategy"],
|
|
256
|
+
"results": [],
|
|
257
|
+
"engines_used": [],
|
|
258
|
+
"error": str(e),
|
|
259
|
+
"elapsed_ms": 0,
|
|
260
|
+
})
|
|
261
|
+
except Exception:
|
|
262
|
+
# 超时后收集已完成的 futures
|
|
263
|
+
for fut in all_futures:
|
|
264
|
+
if fut.done() and not fut.cancelled():
|
|
265
|
+
try:
|
|
266
|
+
sr = fut.result()
|
|
267
|
+
if sr not in sub_results:
|
|
268
|
+
sub_results.append(sr)
|
|
269
|
+
all_results.extend(sr["results"])
|
|
270
|
+
engines_used.update(sr["engines_used"])
|
|
271
|
+
except Exception:
|
|
272
|
+
pass
|
|
273
|
+
|
|
274
|
+
# RRF 融合
|
|
275
|
+
result_lists = [sr["results"] for sr in sub_results if sr["results"]]
|
|
276
|
+
if len(result_lists) > 1:
|
|
277
|
+
merged = rrf_merge(result_lists)
|
|
278
|
+
elif result_lists:
|
|
279
|
+
merged = deduplicate_by_url(result_lists[0])
|
|
280
|
+
else:
|
|
281
|
+
merged = []
|
|
282
|
+
|
|
283
|
+
elapsed = int((time.time() - t0) * 1000)
|
|
284
|
+
|
|
285
|
+
return {
|
|
286
|
+
"merged_results": merged[:max_results * 3],
|
|
287
|
+
"sub_results": sub_results,
|
|
288
|
+
"engines_used": sorted(engines_used),
|
|
289
|
+
"total_results": len(merged),
|
|
290
|
+
"elapsed_ms": elapsed,
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
# ── 知识缺口识别 ──────────────────────────────────────────────────────────────
|
|
295
|
+
|
|
296
|
+
def identify_gaps(sub_results: list[dict[str, Any]], query: str) -> list[str]:
|
|
297
|
+
"""识别搜索结果中的知识缺口。"""
|
|
298
|
+
gaps = []
|
|
299
|
+
|
|
300
|
+
# 检查是否有子查询完全失败
|
|
301
|
+
for sr in sub_results:
|
|
302
|
+
if not sr["results"]:
|
|
303
|
+
gaps.append(f"子查询「{sr['intent']}」无结果:{sr['sub_query'][:40]}")
|
|
304
|
+
|
|
305
|
+
# 检查是否有子查询结果过少
|
|
306
|
+
for sr in sub_results:
|
|
307
|
+
if sr["results"] and len(sr["results"]) < 2:
|
|
308
|
+
gaps.append(f"子查询「{sr['intent']}」结果稀少(仅 {len(sr['results'])} 条)")
|
|
309
|
+
|
|
310
|
+
# 检查来源多样性
|
|
311
|
+
all_sources = set()
|
|
312
|
+
for sr in sub_results:
|
|
313
|
+
for r in sr["results"]:
|
|
314
|
+
src = r.get("source", "")
|
|
315
|
+
if src:
|
|
316
|
+
all_sources.add(src)
|
|
317
|
+
if len(all_sources) < 3:
|
|
318
|
+
gaps.append(f"来源多样性不足:仅 {len(all_sources)} 个引擎有结果({', '.join(all_sources)})")
|
|
319
|
+
|
|
320
|
+
# 检查时间覆盖
|
|
321
|
+
import re
|
|
322
|
+
year_match = re.search(r"20\d{2}", query)
|
|
323
|
+
if year_match:
|
|
324
|
+
target_year = year_match.group()
|
|
325
|
+
has_recent = False
|
|
326
|
+
for sr in sub_results:
|
|
327
|
+
for r in sr["results"]:
|
|
328
|
+
title = r.get("title", "")
|
|
329
|
+
snippet = r.get("snippet", "")
|
|
330
|
+
if target_year in title or target_year in snippet:
|
|
331
|
+
has_recent = True
|
|
332
|
+
break
|
|
333
|
+
if not has_recent:
|
|
334
|
+
gaps.append(f"未找到 {target_year} 年的直接相关内容")
|
|
335
|
+
|
|
336
|
+
return gaps
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
# ── 综合报告 ──────────────────────────────────────────────────────────────────
|
|
340
|
+
|
|
341
|
+
def synthesize_report(query: str, collection: dict[str, Any],
|
|
342
|
+
gaps: list[str]) -> dict[str, Any]:
|
|
343
|
+
"""生成综合研究报告。"""
|
|
344
|
+
merged = collection["merged_results"]
|
|
345
|
+
sub_results = collection["sub_results"]
|
|
346
|
+
|
|
347
|
+
# 按子查询分组的关键发现
|
|
348
|
+
key_findings = []
|
|
349
|
+
for sr in sub_results:
|
|
350
|
+
if sr["results"]:
|
|
351
|
+
best = sr["results"][0]
|
|
352
|
+
key_findings.append({
|
|
353
|
+
"aspect": sr["intent"],
|
|
354
|
+
"strategy": sr["strategy"],
|
|
355
|
+
"top_result": {
|
|
356
|
+
"title": best.get("title", ""),
|
|
357
|
+
"url": best.get("url", ""),
|
|
358
|
+
"snippet": (best.get("snippet", "") or "")[:200],
|
|
359
|
+
"score": best.get("score", 0),
|
|
360
|
+
"source": best.get("source", ""),
|
|
361
|
+
},
|
|
362
|
+
"result_count": len(sr["results"]),
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
# 来源统计
|
|
366
|
+
source_counts = {}
|
|
367
|
+
for r in merged:
|
|
368
|
+
src = r.get("source", "unknown")
|
|
369
|
+
source_counts[src] = source_counts.get(src, 0) + 1
|
|
370
|
+
|
|
371
|
+
# 引用列表
|
|
372
|
+
citations = []
|
|
373
|
+
for i, r in enumerate(merged[:15]):
|
|
374
|
+
citations.append({
|
|
375
|
+
"id": f"[{i+1}]",
|
|
376
|
+
"title": r.get("title", ""),
|
|
377
|
+
"url": r.get("url", ""),
|
|
378
|
+
"source": r.get("source", ""),
|
|
379
|
+
"score": r.get("score", 0),
|
|
380
|
+
})
|
|
381
|
+
|
|
382
|
+
# 交叉引用检测
|
|
383
|
+
all_results = []
|
|
384
|
+
for sr in sub_results:
|
|
385
|
+
all_results.extend(sr["results"])
|
|
386
|
+
cross_refs = detect_cross_references(all_results)
|
|
387
|
+
|
|
388
|
+
return {
|
|
389
|
+
"query": query,
|
|
390
|
+
"key_findings": key_findings,
|
|
391
|
+
"total_sources": collection["total_results"],
|
|
392
|
+
"engines_used": collection["engines_used"],
|
|
393
|
+
"source_distribution": source_counts,
|
|
394
|
+
"citations": citations,
|
|
395
|
+
"cross_references": cross_refs,
|
|
396
|
+
"gaps": gaps,
|
|
397
|
+
"elapsed_ms": collection["elapsed_ms"],
|
|
398
|
+
"sub_query_count": len(sub_results),
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
# ── 主入口 ────────────────────────────────────────────────────────────────────
|
|
403
|
+
|
|
404
|
+
def deep_research(query: str, num_sub_queries: int = 4, max_results: int = 5,
|
|
405
|
+
timeout: int = 15, depth: str = "balanced",
|
|
406
|
+
mode: str = "auto") -> dict[str, Any]:
|
|
407
|
+
"""执行深度研究。"""
|
|
408
|
+
# 0. 查询改写
|
|
409
|
+
rewrite_result = None
|
|
410
|
+
try:
|
|
411
|
+
from query_rewriter import rewrite_query as do_rewrite
|
|
412
|
+
rewrite_result = do_rewrite(query)
|
|
413
|
+
if rewrite_result["rewritten"] and rewrite_result["confidence"] >= 0.7:
|
|
414
|
+
query = rewrite_result["rewritten"]
|
|
415
|
+
except Exception:
|
|
416
|
+
pass
|
|
417
|
+
|
|
418
|
+
# 1. 问题分解
|
|
419
|
+
sub_queries = decompose_query(query, num_sub_queries)
|
|
420
|
+
|
|
421
|
+
# 2. 多源采集
|
|
422
|
+
collection = collect_sources(sub_queries, max_results, timeout, depth, mode)
|
|
423
|
+
|
|
424
|
+
# 3. 知识缺口
|
|
425
|
+
gaps = identify_gaps(collection["sub_results"], query)
|
|
426
|
+
|
|
427
|
+
# 4. 综合报告
|
|
428
|
+
report = synthesize_report(query, collection, gaps)
|
|
429
|
+
|
|
430
|
+
if rewrite_result and rewrite_result["rewritten"]:
|
|
431
|
+
report["rewritten_query"] = {
|
|
432
|
+
"original": rewrite_result["original"],
|
|
433
|
+
"rewritten": rewrite_result["rewritten"],
|
|
434
|
+
"confidence": rewrite_result["confidence"],
|
|
435
|
+
"reason": rewrite_result["reason"],
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
return report
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
# ── CLI ───────────────────────────────────────────────────────────────────────
|
|
442
|
+
|
|
443
|
+
def main():
|
|
444
|
+
parser = argparse.ArgumentParser(description="深度研究工具")
|
|
445
|
+
parser.add_argument("query", help="研究查询")
|
|
446
|
+
parser.add_argument("--sub-queries", type=int, default=4, help="子查询数量")
|
|
447
|
+
parser.add_argument("-n", "--max-results", type=int, default=5, help="每个子查询最大结果数")
|
|
448
|
+
parser.add_argument("--timeout", type=int, default=15, help="超时秒数")
|
|
449
|
+
parser.add_argument("--depth", choices=["fast", "balanced", "deep"], default="balanced")
|
|
450
|
+
parser.add_argument("--mode", choices=["fast", "auto", "deep", "budget"], default="auto")
|
|
451
|
+
parser.add_argument("--json", action="store_true", help="JSON 输出")
|
|
452
|
+
args = parser.parse_args()
|
|
453
|
+
|
|
454
|
+
report = deep_research(
|
|
455
|
+
args.query, args.sub_queries, args.max_results,
|
|
456
|
+
args.timeout, args.depth, args.mode
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
if args.json:
|
|
460
|
+
print(json.dumps(report, ensure_ascii=False, indent=2))
|
|
461
|
+
else:
|
|
462
|
+
# 人类可读输出
|
|
463
|
+
print(f"\n{'='*60}")
|
|
464
|
+
print(f"深度研究报告:{report['query']}")
|
|
465
|
+
print(f"{'='*60}")
|
|
466
|
+
print(f"子查询数:{report['sub_query_count']} | 引擎:{', '.join(report['engines_used'])}")
|
|
467
|
+
print(f"总结果:{report['total_sources']} | 耗时:{report['elapsed_ms']}ms")
|
|
468
|
+
print()
|
|
469
|
+
|
|
470
|
+
for f in report["key_findings"]:
|
|
471
|
+
print(f"▸ {f['aspect']}")
|
|
472
|
+
print(f" 策略:{f['strategy']} | 结果数:{f['result_count']}")
|
|
473
|
+
if f["top_result"]:
|
|
474
|
+
tr = f["top_result"]
|
|
475
|
+
print(f" 最佳:{tr['title'][:60]}")
|
|
476
|
+
print(f" 来源:{tr['source']} | 分数:{tr['score']}")
|
|
477
|
+
print()
|
|
478
|
+
|
|
479
|
+
if report["citations"]:
|
|
480
|
+
print("── 引用列表 ──")
|
|
481
|
+
for c in report["citations"]:
|
|
482
|
+
print(f" {c['id']} {c['title'][:50]} ({c['source']})")
|
|
483
|
+
print()
|
|
484
|
+
|
|
485
|
+
if report.get("cross_references"):
|
|
486
|
+
print("── 交叉引用(多源佐证)──")
|
|
487
|
+
for cr in report["cross_references"]:
|
|
488
|
+
print(f" 「{cr['ngram'][:30]}」 ← {cr['source_count']} 个来源:{', '.join(cr['domains'][:3])}")
|
|
489
|
+
print()
|
|
490
|
+
|
|
491
|
+
if report["gaps"]:
|
|
492
|
+
print("── 知识缺口 ──")
|
|
493
|
+
for g in report["gaps"]:
|
|
494
|
+
print(f" ⚠ {g}")
|
|
495
|
+
print()
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
if __name__ == "__main__":
|
|
499
|
+
main()
|