@shirlytaylor73/smart-search 0.2.0-beta.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.
Files changed (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +188 -0
  3. package/README.zh-CN.md +180 -0
  4. package/npm/bin/smart-search.js +68 -0
  5. package/npm/scripts/postinstall.js +87 -0
  6. package/npm/scripts/resolve-prerelease-version.js +108 -0
  7. package/npm/scripts/set-package-version.js +35 -0
  8. package/npm/scripts/sync-python-version.js +22 -0
  9. package/npm/scripts/test-wrapper-repair.js +137 -0
  10. package/npm/scripts/test.js +76 -0
  11. package/package.json +42 -0
  12. package/pyproject.toml +36 -0
  13. package/skills/smart-search-cli/SKILL.md +41 -0
  14. package/skills/smart-search-cli/agents/openai.yaml +3 -0
  15. package/skills/smart-search-cli/references/cli-contract.md +7 -0
  16. package/skills/smart-search-cli/references/cli-core.md +5 -0
  17. package/skills/smart-search-cli/references/command-patterns.md +12 -0
  18. package/skills/smart-search-cli/references/provider-routing.md +3 -0
  19. package/skills/smart-search-cli/references/regression-release.md +28 -0
  20. package/skills/smart-search-cli/references/setup-config.md +3 -0
  21. package/src/smart_search/__init__.py +1 -0
  22. package/src/smart_search/assets/skills/smart-search-cli/SKILL.md +41 -0
  23. package/src/smart_search/assets/skills/smart-search-cli/agents/openai.yaml +3 -0
  24. package/src/smart_search/assets/skills/smart-search-cli/references/cli-contract.md +7 -0
  25. package/src/smart_search/assets/skills/smart-search-cli/references/cli-core.md +5 -0
  26. package/src/smart_search/assets/skills/smart-search-cli/references/command-patterns.md +12 -0
  27. package/src/smart_search/assets/skills/smart-search-cli/references/provider-routing.md +3 -0
  28. package/src/smart_search/assets/skills/smart-search-cli/references/regression-release.md +28 -0
  29. package/src/smart_search/assets/skills/smart-search-cli/references/setup-config.md +3 -0
  30. package/src/smart_search/cli.py +3118 -0
  31. package/src/smart_search/config.py +809 -0
  32. package/src/smart_search/embedding_presets.py +40 -0
  33. package/src/smart_search/intent_router.py +757 -0
  34. package/src/smart_search/logger.py +43 -0
  35. package/src/smart_search/providers/__init__.py +20 -0
  36. package/src/smart_search/providers/base.py +41 -0
  37. package/src/smart_search/providers/context7.py +141 -0
  38. package/src/smart_search/providers/exa.py +206 -0
  39. package/src/smart_search/providers/jina.py +136 -0
  40. package/src/smart_search/providers/openai_compatible.py +541 -0
  41. package/src/smart_search/providers/xai_responses.py +117 -0
  42. package/src/smart_search/providers/zhipu.py +143 -0
  43. package/src/smart_search/providers/zhipu_mcp.py +230 -0
  44. package/src/smart_search/service.py +3445 -0
  45. package/src/smart_search/skill_installer.py +342 -0
  46. package/src/smart_search/sources.py +429 -0
  47. package/src/smart_search/utils.py +220 -0
@@ -0,0 +1,429 @@
1
+ import ast
2
+ import json
3
+ import re
4
+ import uuid
5
+ from collections import OrderedDict
6
+ from typing import Any
7
+
8
+ import asyncio
9
+
10
+ from .config import config
11
+ from .utils import extract_unique_urls
12
+
13
+
14
+ _MD_LINK_PATTERN = re.compile(r"\[([^\]]+)\]\((https?://[^)]+)\)")
15
+ _INLINE_CITATION_LINK_PATTERN = re.compile(r"\[\[(\d+)\]\]\((https?://[^)]+)\)")
16
+ _SOURCES_HEADING_PATTERN = re.compile(
17
+ r"(?im)^"
18
+ r"(?:#{1,6}\s*)?"
19
+ r"(?:\*\*|__)?\s*"
20
+ r"(sources?|references?|citations?|信源|参考资料|参考|引用|来源列表|来源)"
21
+ r"\s*(?:\*\*|__)?"
22
+ r"(?:\s*[((][^)\n]*[))])?"
23
+ r"\s*[::]?\s*$"
24
+ )
25
+ _SOURCES_FUNCTION_PATTERN = re.compile(
26
+ r"(?im)(^|\n)\s*(sources|source|citations|citation|references|reference|citation_card|source_cards|source_card)\s*\("
27
+ )
28
+
29
+ _THINK_BLOCK_PATTERN = re.compile(r"<think>[\s\S]*?</think>", re.IGNORECASE)
30
+
31
+ _LEADING_POLICY_PATTERNS = [
32
+ re.compile(r"^I\s+cannot\s+(comply|assist|help|provide|fulfill)", re.IGNORECASE),
33
+ re.compile(r"^I\s+(can't|won't|will\s+not|am\s+unable\s+to)\s+(comply|assist|help|provide|fulfill)", re.IGNORECASE),
34
+ re.compile(r"^I('m|\s+am)\s+not\s+able\s+to", re.IGNORECASE),
35
+ re.compile(r"^(Sorry|Apologies|I\s+apologize),?\s+(but\s+)?I\s+(cannot|can't|won't)", re.IGNORECASE),
36
+ re.compile(r"^As\s+an?\s+AI(\s+language\s+model)?", re.IGNORECASE),
37
+ re.compile(r"^我(无法|不能|没有办法)(遵从|遵守|协助|帮助|提供|满足)", re.IGNORECASE),
38
+ re.compile(r"^(抱歉|对不起|很遗憾)[,,]?\s*我(无法|不能)", re.IGNORECASE),
39
+ ]
40
+
41
+ _POLICY_META_KEYWORDS = {
42
+ "policy", "policies", "guideline", "guidelines", "content policy",
43
+ "usage policy", "terms of service", "acceptable use",
44
+ "策略", "政策", "准则", "使用条款", "服务条款",
45
+ }
46
+
47
+ _POLICY_CONTEXT_KEYWORDS = {
48
+ "prompt injection", "jailbreak", "system prompt", "hidden instruction",
49
+ "bypass", "override", "ignore previous",
50
+ "提示注入", "越狱", "系统提示", "隐藏指令",
51
+ }
52
+
53
+
54
+ def _normalize_policy_text(text: str) -> str:
55
+ text = re.sub(r"[*_`#~\[\](){}]", "", text)
56
+ return text.lower().strip()
57
+
58
+
59
+ def _split_paragraphs(text: str) -> list[str]:
60
+ return [p.strip() for p in re.split(r"\n\s*\n", text) if p.strip()]
61
+
62
+
63
+ def _looks_like_policy_block(text: str) -> bool:
64
+ normalized = _normalize_policy_text(text)
65
+ if not normalized:
66
+ return False
67
+
68
+ for pattern in _LEADING_POLICY_PATTERNS:
69
+ if pattern.search(normalized):
70
+ return True
71
+
72
+ meta_count = sum(1 for kw in _POLICY_META_KEYWORDS if kw in normalized)
73
+ ctx_count = sum(1 for kw in _POLICY_CONTEXT_KEYWORDS if kw in normalized)
74
+ if meta_count >= 2 or ctx_count >= 2 or (meta_count >= 1 and ctx_count >= 1):
75
+ return True
76
+
77
+ return False
78
+
79
+
80
+ def sanitize_answer_text(text: str) -> str:
81
+ text = _THINK_BLOCK_PATTERN.sub("", text or "").strip()
82
+ if not text:
83
+ return ""
84
+
85
+ paragraphs = _split_paragraphs(text)
86
+ cleaned: list[str] = []
87
+ leading = True
88
+ for para in paragraphs:
89
+ if leading and _looks_like_policy_block(para):
90
+ continue
91
+ leading = False
92
+ cleaned.append(para)
93
+
94
+ return "\n\n".join(cleaned).strip()
95
+
96
+
97
+ def new_session_id() -> str:
98
+ return uuid.uuid4().hex[:12]
99
+
100
+
101
+ class SourcesCache:
102
+ def __init__(self, max_size: int = 256):
103
+ self._max_size = max_size
104
+ self._lock = asyncio.Lock()
105
+ self._cache: OrderedDict[str, list[dict]] = OrderedDict()
106
+
107
+ async def set(self, session_id: str, sources: list[dict]) -> None:
108
+ async with self._lock:
109
+ self._cache[session_id] = sources
110
+ self._cache.move_to_end(session_id)
111
+ while len(self._cache) > self._max_size:
112
+ self._cache.popitem(last=False)
113
+
114
+ async def get(self, session_id: str) -> list[dict] | None:
115
+ async with self._lock:
116
+ sources = self._cache.get(session_id)
117
+ if sources is None:
118
+ return None
119
+ self._cache.move_to_end(session_id)
120
+ return sources
121
+
122
+
123
+ def merge_sources(*source_lists: list[dict]) -> list[dict]:
124
+ seen: set[str] = set()
125
+ merged: list[dict] = []
126
+ for sources in source_lists:
127
+ for item in sources or []:
128
+ url = (item or {}).get("url")
129
+ if not isinstance(url, str) or not url.strip():
130
+ continue
131
+ url = url.strip()
132
+ if url in seen:
133
+ continue
134
+ seen.add(url)
135
+ merged.append(item)
136
+ return merged
137
+
138
+
139
+ def split_answer_and_sources(text: str) -> tuple[str, list[dict]]:
140
+ raw = (text or "").strip()
141
+ if not raw:
142
+ return "", []
143
+
144
+ if config.output_cleanup_enabled:
145
+ cleaned = sanitize_answer_text(raw)
146
+ if cleaned:
147
+ raw = cleaned
148
+
149
+ inline_sources = _extract_inline_citation_sources(raw)
150
+
151
+ split = _split_function_call_sources(raw)
152
+ if split:
153
+ answer, sources = split
154
+ return answer, merge_sources(sources, inline_sources)
155
+
156
+ split = _split_heading_sources(raw)
157
+ if split:
158
+ answer, sources = split
159
+ return answer, merge_sources(sources, inline_sources)
160
+
161
+ split = _split_details_block_sources(raw)
162
+ if split:
163
+ answer, sources = split
164
+ return answer, merge_sources(sources, inline_sources)
165
+
166
+ split = _split_tail_link_block(raw)
167
+ if split:
168
+ answer, sources = split
169
+ return answer, merge_sources(sources, inline_sources)
170
+
171
+ return raw, inline_sources
172
+
173
+
174
+ def _split_function_call_sources(text: str) -> tuple[str, list[dict]] | None:
175
+ matches = list(_SOURCES_FUNCTION_PATTERN.finditer(text))
176
+ if not matches:
177
+ return None
178
+
179
+ for m in reversed(matches):
180
+ open_paren_idx = m.end() - 1
181
+ extracted = _extract_balanced_call_at_end(text, open_paren_idx)
182
+ if not extracted:
183
+ continue
184
+
185
+ close_paren_idx, args_text = extracted
186
+ sources = _parse_sources_payload(args_text)
187
+ if not sources:
188
+ continue
189
+
190
+ answer = text[: m.start()].rstrip()
191
+ return answer, sources
192
+
193
+ return None
194
+
195
+
196
+ def _extract_balanced_call_at_end(text: str, open_paren_idx: int) -> tuple[int, str] | None:
197
+ if open_paren_idx < 0 or open_paren_idx >= len(text) or text[open_paren_idx] != "(":
198
+ return None
199
+
200
+ depth = 1
201
+ in_string: str | None = None
202
+ escape = False
203
+
204
+ for idx in range(open_paren_idx + 1, len(text)):
205
+ ch = text[idx]
206
+ if in_string:
207
+ if escape:
208
+ escape = False
209
+ continue
210
+ if ch == "\\":
211
+ escape = True
212
+ continue
213
+ if ch == in_string:
214
+ in_string = None
215
+ continue
216
+
217
+ if ch in ("'", '"'):
218
+ in_string = ch
219
+ continue
220
+
221
+ if ch == "(":
222
+ depth += 1
223
+ continue
224
+ if ch == ")":
225
+ depth -= 1
226
+ if depth == 0:
227
+ if text[idx + 1 :].strip():
228
+ return None
229
+ args_text = text[open_paren_idx + 1 : idx]
230
+ return idx, args_text
231
+
232
+ return None
233
+
234
+
235
+ def _split_heading_sources(text: str) -> tuple[str, list[dict]] | None:
236
+ matches = list(_SOURCES_HEADING_PATTERN.finditer(text))
237
+ if not matches:
238
+ return None
239
+
240
+ for m in reversed(matches):
241
+ start = m.start()
242
+ sources_text = text[start:]
243
+ sources = _extract_sources_from_text(sources_text)
244
+ if not sources:
245
+ continue
246
+ answer = text[:start].rstrip()
247
+ return answer, sources
248
+ return None
249
+
250
+
251
+ def _split_tail_link_block(text: str) -> tuple[str, list[dict]] | None:
252
+ lines = text.splitlines()
253
+ if not lines:
254
+ return None
255
+
256
+ idx = len(lines) - 1
257
+ while idx >= 0 and not lines[idx].strip():
258
+ idx -= 1
259
+ if idx < 0:
260
+ return None
261
+
262
+ tail_end = idx
263
+ link_like_count = 0
264
+ while idx >= 0:
265
+ line = lines[idx].strip()
266
+ if not line:
267
+ idx -= 1
268
+ continue
269
+ if not _is_link_only_line(line):
270
+ break
271
+ link_like_count += 1
272
+ idx -= 1
273
+
274
+ tail_start = idx + 1
275
+ if link_like_count < 2:
276
+ return None
277
+
278
+ block_text = "\n".join(lines[tail_start : tail_end + 1])
279
+ sources = _extract_sources_from_text(block_text)
280
+ if not sources:
281
+ return None
282
+
283
+ answer = "\n".join(lines[:tail_start]).rstrip()
284
+ return answer, sources
285
+
286
+
287
+ def _split_details_block_sources(text: str) -> tuple[str, list[dict]] | None:
288
+ lower = text.lower()
289
+ close_idx = lower.rfind("</details>")
290
+ if close_idx == -1:
291
+ return None
292
+ tail = text[close_idx + len("</details>") :].strip()
293
+ if tail:
294
+ return None
295
+
296
+ open_idx = lower.rfind("<details", 0, close_idx)
297
+ if open_idx == -1:
298
+ return None
299
+
300
+ block_text = text[open_idx : close_idx + len("</details>")]
301
+ sources = _extract_sources_from_text(block_text)
302
+ if len(sources) < 2:
303
+ return None
304
+
305
+ answer = text[:open_idx].rstrip()
306
+ return answer, sources
307
+
308
+
309
+ def _is_link_only_line(line: str) -> bool:
310
+ stripped = re.sub(r"^\s*(?:[-*]|\d+\.)\s*", "", line).strip()
311
+ if not stripped:
312
+ return False
313
+ if stripped.startswith(("http://", "https://")):
314
+ return True
315
+ if _MD_LINK_PATTERN.search(stripped):
316
+ return True
317
+ return False
318
+
319
+
320
+ def _parse_sources_payload(payload: str) -> list[dict]:
321
+ payload = (payload or "").strip().rstrip(";")
322
+ if not payload:
323
+ return []
324
+
325
+ data: Any = None
326
+ try:
327
+ data = json.loads(payload)
328
+ except Exception:
329
+ try:
330
+ data = ast.literal_eval(payload)
331
+ except Exception:
332
+ data = None
333
+
334
+ if data is None:
335
+ return _extract_sources_from_text(payload)
336
+
337
+ if isinstance(data, dict):
338
+ for key in ("sources", "citations", "references", "urls"):
339
+ if key in data:
340
+ return _normalize_sources(data[key])
341
+ return _normalize_sources(data)
342
+
343
+ return _normalize_sources(data)
344
+
345
+
346
+ def _normalize_sources(data: Any) -> list[dict]:
347
+ items: list[Any]
348
+ if isinstance(data, (list, tuple)):
349
+ items = list(data)
350
+ elif isinstance(data, dict):
351
+ items = [data]
352
+ else:
353
+ items = [data]
354
+
355
+ normalized: list[dict] = []
356
+ seen: set[str] = set()
357
+
358
+ for item in items:
359
+ if isinstance(item, str):
360
+ for url in extract_unique_urls(item):
361
+ if url not in seen:
362
+ seen.add(url)
363
+ normalized.append({"url": url})
364
+ continue
365
+
366
+ if isinstance(item, (list, tuple)) and len(item) >= 2:
367
+ title, url = item[0], item[1]
368
+ if isinstance(url, str) and url.startswith(("http://", "https://")) and url not in seen:
369
+ seen.add(url)
370
+ out: dict = {"url": url}
371
+ if isinstance(title, str) and title.strip():
372
+ out["title"] = title.strip()
373
+ normalized.append(out)
374
+ continue
375
+
376
+ if isinstance(item, dict):
377
+ url = item.get("url") or item.get("href") or item.get("link")
378
+ if not isinstance(url, str) or not url.startswith(("http://", "https://")):
379
+ continue
380
+ if url in seen:
381
+ continue
382
+ seen.add(url)
383
+ out: dict = {"url": url}
384
+ title = item.get("title") or item.get("name") or item.get("label")
385
+ if isinstance(title, str) and title.strip():
386
+ out["title"] = title.strip()
387
+ desc = item.get("description") or item.get("snippet") or item.get("content")
388
+ if isinstance(desc, str) and desc.strip():
389
+ out["description"] = desc.strip()
390
+ normalized.append(out)
391
+ continue
392
+
393
+ return normalized
394
+
395
+
396
+ def _extract_sources_from_text(text: str) -> list[dict]:
397
+ sources: list[dict] = []
398
+ seen: set[str] = set()
399
+
400
+ for title, url in _MD_LINK_PATTERN.findall(text or ""):
401
+ url = (url or "").strip()
402
+ if not url or url in seen:
403
+ continue
404
+ seen.add(url)
405
+ title = (title or "").strip()
406
+ if title:
407
+ sources.append({"title": title, "url": url})
408
+ else:
409
+ sources.append({"url": url})
410
+
411
+ for url in extract_unique_urls(text or ""):
412
+ if url in seen:
413
+ continue
414
+ seen.add(url)
415
+ sources.append({"url": url})
416
+
417
+ return sources
418
+
419
+
420
+ def _extract_inline_citation_sources(text: str) -> list[dict]:
421
+ sources: list[dict] = []
422
+ seen: set[str] = set()
423
+ for number, url in _INLINE_CITATION_LINK_PATTERN.findall(text or ""):
424
+ url = (url or "").strip()
425
+ if not url or url in seen:
426
+ continue
427
+ seen.add(url)
428
+ sources.append({"title": number, "url": url})
429
+ return sources
@@ -0,0 +1,220 @@
1
+ from typing import List
2
+ import re
3
+ from .providers.base import SearchResult
4
+
5
+ _URL_PATTERN = re.compile(r'https?://[^\s<>"\'`,。、;:!?》)】\)]+')
6
+
7
+
8
+ def extract_unique_urls(text: str) -> list[str]:
9
+ seen: set[str] = set()
10
+ urls: list[str] = []
11
+ for m in _URL_PATTERN.finditer(text):
12
+ url = m.group().rstrip('.,;:!?')
13
+ if url not in seen:
14
+ seen.add(url)
15
+ urls.append(url)
16
+ return urls
17
+
18
+
19
+ def format_extra_sources(tavily_results: list[dict] | None, firecrawl_results: list[dict] | None) -> str:
20
+ sections = []
21
+ idx = 1
22
+ urls = []
23
+ if firecrawl_results:
24
+ lines = ["## Extra Sources [Firecrawl]"]
25
+ for r in firecrawl_results:
26
+ title = r.get("title") or "Untitled"
27
+ url = r.get("url", "")
28
+ if len(url) == 0:
29
+ continue
30
+ if url in urls:
31
+ continue
32
+ urls.append(url)
33
+ desc = r.get("description", "")
34
+ lines.append(f"{idx}. **[{title}]({url})**")
35
+ if desc:
36
+ lines.append(f" {desc}")
37
+ idx += 1
38
+ sections.append("\n".join(lines))
39
+ if tavily_results:
40
+ lines = ["## Extra Sources [Tavily]"]
41
+ for r in tavily_results:
42
+ title = r.get("title") or "Untitled"
43
+ url = r.get("url", "")
44
+ if url in urls:
45
+ continue
46
+ content = r.get("content", "")
47
+ lines.append(f"{idx}. **[{title}]({url})**")
48
+ if content:
49
+ lines.append(f" {content}")
50
+ idx += 1
51
+ sections.append("\n".join(lines))
52
+ return "\n\n".join(sections)
53
+
54
+
55
+ def format_search_results(results: List[SearchResult]) -> str:
56
+ if not results:
57
+ return "No results found."
58
+
59
+ formatted = []
60
+ for i, result in enumerate(results, 1):
61
+ parts = [f"## Result {i}: {result.title}"]
62
+
63
+ if result.url:
64
+ parts.append(f"**URL:** {result.url}")
65
+
66
+ if result.snippet:
67
+ parts.append(f"**Summary:** {result.snippet}")
68
+
69
+ if result.source:
70
+ parts.append(f"**Source:** {result.source}")
71
+
72
+ if result.published_date:
73
+ parts.append(f"**Published:** {result.published_date}")
74
+
75
+ formatted.append("\n".join(parts))
76
+
77
+ return "\n\n---\n\n".join(formatted)
78
+
79
+ fetch_prompt = """
80
+ # Profile: Web Content Fetcher
81
+
82
+ - **Language**: 中文
83
+ - **Role**: 你是一个专业的网页内容抓取和解析专家,获取指定 URL 的网页内容,并将其转换为与原网页高度一致的结构化 Markdown 文本格式。
84
+
85
+ ---
86
+
87
+ ## Workflow
88
+
89
+ ### 1. URL 验证与内容获取
90
+ - 验证 URL 格式有效性,检查可访问性(处理重定向/超时)
91
+ - **关键**:优先识别页面目录/大纲结构(Table of Contents),作为内容抓取的导航索引
92
+ - 全量获取 HTML 内容,确保不遗漏任何章节或动态加载内容
93
+
94
+ ### 2. 智能解析与内容提取
95
+ - **结构优先**:若存在目录/大纲,严格按其层级结构进行内容提取和组织
96
+ - 解析 HTML 文档树,识别所有内容元素:
97
+ - 标题层级(h1-h6)及其嵌套关系
98
+ - 正文段落、文本格式(粗体/斜体/下划线)
99
+ - 列表结构(有序/无序/嵌套)
100
+ - 表格(包含表头/数据行/合并单元格)
101
+ - 代码块(行内代码/多行代码块/语言标识)
102
+ - 引用块、分隔线
103
+ - 图片(src/alt/title 属性)
104
+ - 链接(内部/外部/锚点)
105
+
106
+ ### 3. 内容清理与语义保留
107
+ - 移除非内容标签:`<script>`、`<style>`、`<iframe>`、`<noscript>`
108
+ - 过滤干扰元素:广告模块、追踪代码、社交分享按钮
109
+ - **保留语义信息**:图片 alt/title、链接 href/title、代码语言标识
110
+ - 特殊模块标注:导航栏、侧边栏、页脚用特殊标记保留
111
+
112
+ ---
113
+
114
+ ## Skills
115
+
116
+ ### 1. 内容精准提取与还原
117
+ - **如果存在目录或者大纲,则按照目录或者大纲的结构进行提取**
118
+ - **完整保留原始内容结构**,不遗漏任何信息
119
+ - **准确识别并提取**标题、段落、列表、表格、代码块等所有元素
120
+ - **保持原网页的内容层次和逻辑关系**
121
+ - **精确处理特殊字符**,确保无乱码和格式错误
122
+ - **还原文本内容**,包括换行、缩进、空格等细节
123
+
124
+ ### 2. 结构化组织与呈现
125
+ - **标题层级**:使用 `#`、`##`、`###` 等还原标题层级
126
+ - **目录结构**:使用列表生成 Table of Contents,带锚点链接
127
+ - **内容分区**:使用 `###` 或代码块(` ```section ``` `)明确划分 Section
128
+ - **嵌套结构**:使用缩进列表或引用块(`>`)保持层次关系
129
+ - **辅助模块**:侧边栏、导航等用特殊代码块(` ```sidebar ``` `、` ```nav ``` `)包裹
130
+
131
+ ### 3. 格式转换优化
132
+ - **HTML 转 Markdown**:保持 100% 内容一致性
133
+ - **表格处理**:使用 Markdown 表格语法(`|---|---|`)
134
+ - **代码片段**:用 ` ```语言标识``` ` 包裹,保留原始缩进
135
+ - **图片处理**:转换为 `![alt](url)` 格式,保留所有属性
136
+ - **链接处理**:转换为 `[文本](URL)` 格式,保持完整路径
137
+ - **强调样式**:`<strong>` → `**粗体**`,`<em>` → `*斜体*`
138
+
139
+ ### 4. 内容完整性保障
140
+ - **零删减原则**:不删减任何原网页文本内容
141
+ - **元数据保留**:保留时间戳、作者信息、标签等关键信息
142
+ - **多媒体标注**:视频、音频以链接或占位符标注(`[视频: 标题](URL)`)
143
+ - **动态内容处理**:尽可能抓取完整内容
144
+
145
+ ---
146
+
147
+ ## Rules
148
+
149
+ ### 1. 内容一致性原则(核心)
150
+ - ✅ 返回内容必须与原网页内容**完全一致**,不能有信息缺失
151
+ - ✅ 保持原网页的**所有文本、结构和语义信息**
152
+ - ❌ **不进行**内容摘要、精简、改写或总结
153
+ - ✅ 保留原始的**段落划分、换行、空格**等格式细节
154
+
155
+ ### 2. 格式转换标准
156
+ | HTML | Markdown | 示例 |
157
+ |------|----------|------|
158
+ | `<h1>`-`<h6>` | `#`-`######` | `# 标题` |
159
+ | `<strong>` | `**粗体**` | **粗体** |
160
+ | `<em>` | `*斜体*` | *斜体* |
161
+ | `<a>` | `[文本](url)` | [链接](url) |
162
+ | `<img>` | `![alt](url)` | ![图](url) |
163
+ | `<code>` | `` `代码` `` | `code` |
164
+ | `<pre><code>` | ` ```\n代码\n``` ` | 代码块 |
165
+
166
+ ### 3. 输出质量要求
167
+ - **元数据头部**:
168
+ ```markdown
169
+ ---
170
+ source: [原始URL]
171
+ title: [网页标题]
172
+ fetched_at: [抓取时间]
173
+ ---
174
+ ```
175
+ - **编码标准**:统一使用 UTF-8
176
+ - **可用性**:输出可直接用于文档生成或阅读
177
+
178
+ ---
179
+
180
+ ## Initialization
181
+
182
+ 当接收到 URL 时:
183
+ 1. 按 Workflow 执行抓取和处理
184
+ 2. 返回完整的结构化 Markdown 文档
185
+ """
186
+
187
+
188
+ url_describe_prompt = (
189
+ "Browse the given URL. Return exactly two sections:\n\n"
190
+ "Title: <page title from the page's own <title> tag or top heading; "
191
+ "if missing/generic, craft one using key terms found in the page>\n\n"
192
+ "Extracts: <copy 2-4 verbatim fragments from the page that best represent "
193
+ "its core content. Each fragment must be the author's original words, "
194
+ "wrapped in quotes, separated by ' | '. "
195
+ "Do NOT paraphrase, rephrase, interpret, or describe. "
196
+ "Do NOT write sentences like 'This page discusses...' or 'The author argues...'. "
197
+ "You are a copy-paste machine.>\n\n"
198
+ "Nothing else."
199
+ )
200
+
201
+ rank_sources_prompt = (
202
+ "Given a user query and a numbered source list, output ONLY the source numbers "
203
+ "reordered by relevance to the query (most relevant first). "
204
+ "Format: space-separated integers on a single line (e.g., 14 12 1 3 5). "
205
+ "Include every number exactly once. Nothing else."
206
+ )
207
+
208
+ search_prompt = """You are a helpful research assistant. Answer the user's question thoroughly using web search results.
209
+
210
+ Guidelines:
211
+ - Infer the user's true intent even when the question is vague. Consider multiple angles.
212
+ - Search broadly first (5+ perspectives), then go deep on the 2-3 most relevant ones.
213
+ - Prioritize authoritative sources: official docs, Wikipedia, academic papers, reputable journalism.
214
+ - Search in English first for breadth, switch to Chinese when the topic demands it.
215
+ - Every factual claim should cite its source. More credible sources strengthen the answer.
216
+ - Lead with the most likely answer, then provide supporting analysis.
217
+ - Define technical terms in plain language. Use real-world analogies for complex concepts.
218
+ - Format output in clean Markdown. Use LaTeX for formulas, code blocks for scripts.
219
+ - Be direct and concise. No filler or unnecessary follow-up questions.
220
+ """