@blxzer/cursor-trellis 0.1.2 → 0.2.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 (105) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/README.md +45 -21
  3. package/dist/configurators/workflow.d.ts +1 -1
  4. package/dist/configurators/workflow.d.ts.map +1 -1
  5. package/dist/configurators/workflow.js +20 -9
  6. package/dist/configurators/workflow.js.map +1 -1
  7. package/dist/migrations/manifests/0.1.3.json +9 -0
  8. package/dist/migrations/manifests/0.1.4.json +9 -0
  9. package/dist/templates/cursor/rules/retrieval-routing.mdc +9 -7
  10. package/dist/templates/markdown/agents.md +1 -1
  11. package/dist/templates/markdown/index.d.ts +1 -0
  12. package/dist/templates/markdown/index.d.ts.map +1 -1
  13. package/dist/templates/markdown/index.js +1 -0
  14. package/dist/templates/markdown/index.js.map +1 -1
  15. package/dist/templates/markdown/prompts/run-semantic-slice-12.md.txt +13 -29
  16. package/dist/templates/markdown/spec/guides/code-reuse-thinking-guide.md.txt +0 -49
  17. package/dist/templates/markdown/spec/guides/cross-layer-thinking-guide.md.txt +0 -85
  18. package/dist/templates/markdown/spec/guides/cursor-semantic-compliance.md.txt +7 -21
  19. package/dist/templates/markdown/spec/guides/cursor-subagent-policy.md.txt +6 -10
  20. package/dist/templates/markdown/spec/guides/retrieval-daily-guide.md.txt +17 -18
  21. package/dist/templates/shared-hooks/inject-retrieval-plan.py +42 -23
  22. package/dist/templates/shared-hooks/inject-subagent-context.py +6 -2
  23. package/dist/templates/shared-hooks/session-start.py +17 -9
  24. package/dist/templates/trellis/index.d.ts +8 -4
  25. package/dist/templates/trellis/index.d.ts.map +1 -1
  26. package/dist/templates/trellis/index.js +13 -7
  27. package/dist/templates/trellis/index.js.map +1 -1
  28. package/dist/templates/trellis/scripts/common/codebase_retrieval_router.py +80 -31
  29. package/dist/templates/trellis/scripts/common/config.py +1 -1
  30. package/dist/templates/trellis/scripts/common/cursor_retrieval_env.py +92 -0
  31. package/dist/templates/trellis/scripts/common/parent_orchestration.py +27 -15
  32. package/dist/templates/trellis/scripts/common/retrieval_adapter_metadata.py +100 -9
  33. package/dist/templates/trellis/scripts/common/retrieval_agent_instructions.py +76 -31
  34. package/dist/templates/trellis/scripts/common/retrieval_tool_classification.py +18 -3
  35. package/dist/templates/trellis/scripts/common/semantic_plan_gate.py +19 -0
  36. package/dist/templates/trellis/scripts/common/smart_search_evidence.py +5 -2
  37. package/dist/templates/trellis/scripts/common/smart_search_resolve.py +28 -3
  38. package/dist/templates/trellis/scripts/common/task_gates.py +372 -73
  39. package/dist/templates/trellis/scripts/common/task_map.py +29 -1
  40. package/dist/templates/trellis/scripts/cursor_retrieval_probe.py +396 -0
  41. package/dist/templates/trellis/scripts/cursor_retrieval_probe_prompt.md +300 -0
  42. package/dist/templates/trellis/scripts/retrieval_probe_matrix_template.json +126 -0
  43. package/dist/templates/trellis/workflow.md +9 -53
  44. package/dist/utils/codebase-retrieval-router.d.ts +5 -0
  45. package/dist/utils/codebase-retrieval-router.d.ts.map +1 -1
  46. package/dist/utils/codebase-retrieval-router.js +48 -28
  47. package/dist/utils/codebase-retrieval-router.js.map +1 -1
  48. package/dist/utils/cursor-retrieval-env.d.ts +28 -0
  49. package/dist/utils/cursor-retrieval-env.d.ts.map +1 -0
  50. package/dist/utils/cursor-retrieval-env.js +89 -0
  51. package/dist/utils/cursor-retrieval-env.js.map +1 -0
  52. package/dist/utils/project-capabilities.d.ts.map +1 -1
  53. package/dist/utils/project-capabilities.js +22 -15
  54. package/dist/utils/project-capabilities.js.map +1 -1
  55. package/dist/utils/retrieval-agent-instructions.d.ts.map +1 -1
  56. package/dist/utils/retrieval-agent-instructions.js +37 -21
  57. package/dist/utils/retrieval-agent-instructions.js.map +1 -1
  58. package/dist/utils/retrieval-tool-classification.d.ts +2 -0
  59. package/dist/utils/retrieval-tool-classification.d.ts.map +1 -1
  60. package/dist/utils/retrieval-tool-classification.js +10 -2
  61. package/dist/utils/retrieval-tool-classification.js.map +1 -1
  62. package/dist/utils/semantic-plan-gate.d.ts +8 -0
  63. package/dist/utils/semantic-plan-gate.d.ts.map +1 -0
  64. package/dist/utils/semantic-plan-gate.js +42 -0
  65. package/dist/utils/semantic-plan-gate.js.map +1 -0
  66. package/package.json +5 -43
  67. package/bin/smart-search.js +0 -79
  68. package/dist/templates/trellis/scripts/aggregate_retrieval_telemetry.py +0 -342
  69. package/dist/templates/trellis/scripts/batch_plan_envelope.py +0 -105
  70. package/vendor/smart-search/LICENSE +0 -21
  71. package/vendor/smart-search/README.md +0 -412
  72. package/vendor/smart-search/README.zh-CN.md +0 -430
  73. package/vendor/smart-search/npm/bin/smart-search.js +0 -63
  74. package/vendor/smart-search/npm/scripts/postinstall.js +0 -87
  75. package/vendor/smart-search/npm/scripts/resolve-prerelease-version.js +0 -108
  76. package/vendor/smart-search/npm/scripts/set-package-version.js +0 -35
  77. package/vendor/smart-search/npm/scripts/sync-python-version.js +0 -22
  78. package/vendor/smart-search/npm/scripts/test-wrapper-repair.js +0 -99
  79. package/vendor/smart-search/npm/scripts/test.js +0 -85
  80. package/vendor/smart-search/package.json +0 -42
  81. package/vendor/smart-search/pyproject.toml +0 -37
  82. package/vendor/smart-search/skills/smart-search-cli/SKILL.md +0 -387
  83. package/vendor/smart-search/skills/smart-search-cli/agents/openai.yaml +0 -3
  84. package/vendor/smart-search/skills/smart-search-cli/examples/batch-search.md +0 -98
  85. package/vendor/smart-search/skills/smart-search-cli/examples/evidence-gathering.md +0 -89
  86. package/vendor/smart-search/skills/smart-search-cli/references/cli-contract.md +0 -337
  87. package/vendor/smart-search/src/smart_search/__init__.py +0 -1
  88. package/vendor/smart-search/src/smart_search/assets/skills/smart-search-cli/SKILL.md +0 -387
  89. package/vendor/smart-search/src/smart_search/assets/skills/smart-search-cli/agents/openai.yaml +0 -3
  90. package/vendor/smart-search/src/smart_search/assets/skills/smart-search-cli/examples/batch-search.md +0 -98
  91. package/vendor/smart-search/src/smart_search/assets/skills/smart-search-cli/examples/evidence-gathering.md +0 -89
  92. package/vendor/smart-search/src/smart_search/assets/skills/smart-search-cli/references/cli-contract.md +0 -337
  93. package/vendor/smart-search/src/smart_search/cli.py +0 -2003
  94. package/vendor/smart-search/src/smart_search/config.py +0 -580
  95. package/vendor/smart-search/src/smart_search/logger.py +0 -43
  96. package/vendor/smart-search/src/smart_search/providers/__init__.py +0 -16
  97. package/vendor/smart-search/src/smart_search/providers/base.py +0 -41
  98. package/vendor/smart-search/src/smart_search/providers/context7.py +0 -141
  99. package/vendor/smart-search/src/smart_search/providers/exa.py +0 -206
  100. package/vendor/smart-search/src/smart_search/providers/jina.py +0 -136
  101. package/vendor/smart-search/src/smart_search/providers/openai_compatible.py +0 -370
  102. package/vendor/smart-search/src/smart_search/providers/zhipu.py +0 -143
  103. package/vendor/smart-search/src/smart_search/service.py +0 -2865
  104. package/vendor/smart-search/src/smart_search/sources.py +0 -429
  105. package/vendor/smart-search/src/smart_search/utils.py +0 -220
@@ -1,370 +0,0 @@
1
- import httpx
2
- import json
3
- import logging
4
- from datetime import datetime, timezone
5
- from email.utils import parsedate_to_datetime
6
- from typing import List, Optional
7
- from tenacity import AsyncRetrying, retry_if_exception, stop_after_attempt, wait_random_exponential
8
- from tenacity.wait import wait_base
9
- from .base import BaseSearchProvider, SearchResult
10
- from ..utils import search_prompt, fetch_prompt, url_describe_prompt, rank_sources_prompt
11
- from ..logger import log_info
12
- from ..config import config
13
-
14
- _logger = logging.getLogger(__name__)
15
- _ssl_warning_emitted = False
16
-
17
-
18
- def get_local_time_info() -> str:
19
- try:
20
- local_tz = datetime.now().astimezone().tzinfo
21
- local_now = datetime.now(local_tz)
22
- except Exception:
23
- local_now = datetime.now(timezone.utc)
24
-
25
- weekdays_cn = ["星期一", "星期二", "星期三", "星期四", "星期五", "星期六", "星期日"]
26
- weekday = weekdays_cn[local_now.weekday()]
27
-
28
- return (
29
- f"[Current Time Context]\n"
30
- f"- Date: {local_now.strftime('%Y-%m-%d')} ({weekday})\n"
31
- f"- Time: {local_now.strftime('%H:%M:%S')}\n"
32
- f"- Timezone: {local_now.tzname() or 'Local'}\n"
33
- )
34
-
35
-
36
- RETRYABLE_STATUS_CODES = {408, 429, 500, 502, 503, 504}
37
-
38
-
39
- def _is_retryable_exception(exc) -> bool:
40
- if isinstance(exc, (httpx.TimeoutException, httpx.NetworkError, httpx.ConnectError, httpx.RemoteProtocolError)):
41
- return True
42
- if isinstance(exc, httpx.HTTPStatusError):
43
- return exc.response.status_code in RETRYABLE_STATUS_CODES
44
- return False
45
-
46
-
47
- class _WaitWithRetryAfter(wait_base):
48
-
49
- def __init__(self, multiplier: float, max_wait: int):
50
- self._base_wait = wait_random_exponential(multiplier=multiplier, max=max_wait)
51
- self._protocol_error_base = 3.0
52
-
53
- def __call__(self, retry_state):
54
- if retry_state.outcome and retry_state.outcome.failed:
55
- exc = retry_state.outcome.exception()
56
- if isinstance(exc, httpx.HTTPStatusError) and exc.response.status_code == 429:
57
- retry_after = self._parse_retry_after(exc.response)
58
- if retry_after is not None:
59
- return retry_after
60
- if isinstance(exc, httpx.RemoteProtocolError):
61
- return self._base_wait(retry_state) + self._protocol_error_base
62
- return self._base_wait(retry_state)
63
-
64
- def _parse_retry_after(self, response: httpx.Response) -> Optional[float]:
65
- header = response.headers.get("Retry-After")
66
- if not header:
67
- return None
68
- header = header.strip()
69
-
70
- if header.isdigit():
71
- return float(header)
72
-
73
- try:
74
- retry_dt = parsedate_to_datetime(header)
75
- if retry_dt.tzinfo is None:
76
- retry_dt = retry_dt.replace(tzinfo=timezone.utc)
77
- delay = (retry_dt - datetime.now(timezone.utc)).total_seconds()
78
- return max(0.0, delay)
79
- except (TypeError, ValueError):
80
- return None
81
-
82
-
83
- class OpenAICompatibleSearchProvider(BaseSearchProvider):
84
- def __init__(self, api_url: str, api_key: str, model: str = "grok-4-fast", stream: bool = False):
85
- super().__init__(api_url, api_key)
86
- self.model = model
87
- self.stream = stream
88
-
89
- def get_provider_name(self) -> str:
90
- return "OpenAI-compatible"
91
-
92
- def _build_api_headers(self) -> dict:
93
- return {
94
- "Authorization": f"Bearer {self.api_key}",
95
- "Content-Type": "application/json",
96
- "Accept": "application/json, text/event-stream",
97
- "User-Agent": "smart-search/0.1.0",
98
- }
99
-
100
- def _get_ssl_verify(self) -> bool:
101
- global _ssl_warning_emitted
102
- verify = config.ssl_verify_enabled
103
- if not verify and not _ssl_warning_emitted:
104
- _ssl_warning_emitted = True
105
- _logger.warning("SSL_VERIFY=false: OpenAI-compatible API 请求已禁用 SSL 证书验证,存在安全风险")
106
- return verify
107
-
108
- async def search(self, query: str, platform: str = "", ctx=None) -> List[SearchResult]:
109
- headers = self._build_api_headers()
110
- platform_prompt = ""
111
-
112
- if platform:
113
- platform_prompt = "\n\nYou should search the web for the information you need, and focus on these platform: " + platform + "\n"
114
-
115
- time_context = get_local_time_info() + "\n"
116
-
117
- payload = {
118
- "model": self.model,
119
- "messages": [
120
- {
121
- "role": "system",
122
- "content": search_prompt,
123
- },
124
- {"role": "user", "content": time_context + query + platform_prompt},
125
- ],
126
- "stream": self.stream,
127
- }
128
-
129
- await log_info(ctx, f"platform_prompt: { query + platform_prompt}", config.debug_enabled)
130
-
131
- if self.stream:
132
- return await self._execute_stream_with_retry(headers, payload, ctx)
133
- return await self._execute_completion_with_retry(headers, payload, ctx)
134
-
135
- async def fetch(self, url: str, ctx=None) -> str:
136
- headers = self._build_api_headers()
137
- payload = {
138
- "model": self.model,
139
- "messages": [
140
- {
141
- "role": "system",
142
- "content": fetch_prompt,
143
- },
144
- {"role": "user", "content": url + "\n获取该网页内容并返回其结构化Markdown格式" },
145
- ],
146
- "stream": self.stream,
147
- }
148
- if self.stream:
149
- return await self._execute_stream_with_retry(headers, payload, ctx)
150
- return await self._execute_completion_with_retry(headers, payload, ctx)
151
-
152
- async def _parse_streaming_response(self, response, ctx=None) -> str:
153
- content = ""
154
- full_body_buffer = []
155
-
156
- async for line in response.aiter_lines():
157
- line = line.strip()
158
- if not line:
159
- continue
160
-
161
- full_body_buffer.append(line)
162
-
163
- if line.startswith("data:"):
164
- if line in ("data: [DONE]", "data:[DONE]"):
165
- continue
166
- try:
167
- json_str = line[5:].lstrip()
168
- data = json.loads(json_str)
169
- choices = data.get("choices", [])
170
- if choices and len(choices) > 0:
171
- delta = choices[0].get("delta", {})
172
- if "content" in delta:
173
- content += delta["content"]
174
- except (json.JSONDecodeError, IndexError):
175
- continue
176
-
177
- if not content and full_body_buffer:
178
- try:
179
- full_text = "".join(full_body_buffer)
180
- data = json.loads(full_text)
181
- if "choices" in data and len(data["choices"]) > 0:
182
- message = data["choices"][0].get("message", {})
183
- content = message.get("content", "")
184
- except json.JSONDecodeError:
185
- pass
186
-
187
- await log_info(ctx, f"content: {content}", config.debug_enabled)
188
-
189
- return content
190
-
191
- async def _execute_stream_with_retry(self, headers: dict, payload: dict, ctx=None) -> str:
192
- timeout = httpx.Timeout(connect=6.0, read=120.0, write=10.0, pool=None)
193
-
194
- async with httpx.AsyncClient(timeout=timeout, follow_redirects=True, verify=self._get_ssl_verify()) as client:
195
- async for attempt in AsyncRetrying(
196
- stop=stop_after_attempt(config.retry_max_attempts + 1),
197
- wait=_WaitWithRetryAfter(config.retry_multiplier, config.retry_max_wait),
198
- retry=retry_if_exception(_is_retryable_exception),
199
- reraise=True,
200
- ):
201
- with attempt:
202
- async with client.stream(
203
- "POST",
204
- f"{self.api_url}/chat/completions",
205
- headers=headers,
206
- json=payload,
207
- ) as response:
208
- response.raise_for_status()
209
- return await self._parse_streaming_response(response, ctx)
210
-
211
- async def _parse_completion_response(self, response: httpx.Response, ctx=None) -> str:
212
- """解析非流式 completion 响应,兼容 JSON 和 SSE 文本 fallback"""
213
- content = ""
214
- body_text = response.text or ""
215
- sources: list[dict] = []
216
-
217
- try:
218
- data = response.json()
219
- except Exception:
220
- data = None
221
-
222
- if isinstance(data, dict):
223
- sources = self._extract_citations(data)
224
- choices = data.get("choices", [])
225
- if choices:
226
- message = choices[0].get("message", {})
227
- if isinstance(message, dict):
228
- content = message.get("content", "") or ""
229
- message_citations = self._normalize_citations(message.get("citations"))
230
- if message_citations:
231
- sources = self._merge_citations(sources, message_citations)
232
-
233
- # SSE fallback: 部分中转站即使设置 stream=False 仍可能返回 SSE 格式
234
- if not content and body_text.lstrip().startswith("data:"):
235
- class _LineResponse:
236
- def __init__(self, text: str):
237
- self._lines = text.splitlines()
238
-
239
- async def aiter_lines(self):
240
- for line in self._lines:
241
- yield line
242
-
243
- content = await self._parse_streaming_response(_LineResponse(body_text), ctx)
244
-
245
- if content and sources:
246
- content = f"{content.rstrip()}\n\nsources({json.dumps(sources, ensure_ascii=False)})"
247
-
248
- await log_info(ctx, f"content: {content}", config.debug_enabled)
249
-
250
- return content
251
-
252
- def _extract_citations(self, data: dict) -> list[dict]:
253
- sources = self._normalize_citations(data.get("citations"))
254
- for choice in data.get("choices", []) or []:
255
- if not isinstance(choice, dict):
256
- continue
257
- message = choice.get("message")
258
- if isinstance(message, dict):
259
- sources = self._merge_citations(sources, self._normalize_citations(message.get("citations")))
260
- return sources
261
-
262
- def _normalize_citations(self, citations) -> list[dict]:
263
- if not citations:
264
- return []
265
- if not isinstance(citations, list):
266
- citations = [citations]
267
-
268
- normalized: list[dict] = []
269
- seen: set[str] = set()
270
- for item in citations:
271
- source: dict = {}
272
- if isinstance(item, str):
273
- url = item.strip()
274
- if not url.startswith(("http://", "https://")):
275
- continue
276
- source["url"] = url
277
- elif isinstance(item, dict):
278
- url = item.get("url") or item.get("href") or item.get("link")
279
- if not isinstance(url, str) or not url.startswith(("http://", "https://")):
280
- continue
281
- source["url"] = url
282
- title = item.get("title") or item.get("name") or item.get("label")
283
- if isinstance(title, str) and title.strip():
284
- source["title"] = title.strip()
285
- else:
286
- continue
287
-
288
- if source["url"] in seen:
289
- continue
290
- seen.add(source["url"])
291
- normalized.append(source)
292
- return normalized
293
-
294
- def _merge_citations(self, *source_lists: list[dict]) -> list[dict]:
295
- merged: list[dict] = []
296
- seen: set[str] = set()
297
- for source_list in source_lists:
298
- for item in source_list or []:
299
- url = item.get("url")
300
- if not isinstance(url, str) or not url or url in seen:
301
- continue
302
- seen.add(url)
303
- merged.append(item)
304
- return merged
305
-
306
- async def _execute_completion_with_retry(self, headers: dict, payload: dict, ctx=None) -> str:
307
- """执行带重试机制的非流式 HTTP 请求,兼容上游返回 JSON 或 SSE 文本"""
308
- timeout = httpx.Timeout(connect=6.0, read=120.0, write=10.0, pool=None)
309
-
310
- async with httpx.AsyncClient(timeout=timeout, follow_redirects=True, verify=self._get_ssl_verify()) as client:
311
- async for attempt in AsyncRetrying(
312
- stop=stop_after_attempt(config.retry_max_attempts + 1),
313
- wait=_WaitWithRetryAfter(config.retry_multiplier, config.retry_max_wait),
314
- retry=retry_if_exception(_is_retryable_exception),
315
- reraise=True,
316
- ):
317
- with attempt:
318
- response = await client.post(
319
- f"{self.api_url}/chat/completions",
320
- headers=headers,
321
- json=payload,
322
- )
323
- response.raise_for_status()
324
- return await self._parse_completion_response(response, ctx)
325
-
326
- async def describe_url(self, url: str, ctx=None) -> dict:
327
- headers = self._build_api_headers()
328
- payload = {
329
- "model": self.model,
330
- "messages": [
331
- {"role": "system", "content": url_describe_prompt},
332
- {"role": "user", "content": url},
333
- ],
334
- "stream": False,
335
- }
336
- result = await self._execute_completion_with_retry(headers, payload, ctx)
337
- title, extracts = url, ""
338
- for line in result.strip().splitlines():
339
- if line.startswith("Title:"):
340
- title = line[6:].strip() or url
341
- elif line.startswith("Extracts:"):
342
- extracts = line[9:].strip()
343
- return {"title": title, "extracts": extracts, "url": url}
344
-
345
- async def rank_sources(self, query: str, sources_text: str, total: int, ctx=None) -> list[int]:
346
- """让 OpenAI-compatible 模型按查询相关度对信源排序,返回排序后的序号列表"""
347
- headers = self._build_api_headers()
348
- payload = {
349
- "model": self.model,
350
- "messages": [
351
- {"role": "system", "content": rank_sources_prompt},
352
- {"role": "user", "content": f"Query: {query}\n\n{sources_text}"},
353
- ],
354
- "stream": False,
355
- }
356
- result = await self._execute_completion_with_retry(headers, payload, ctx)
357
- order: list[int] = []
358
- seen: set[int] = set()
359
- for token in result.strip().split():
360
- try:
361
- n = int(token)
362
- if 1 <= n <= total and n not in seen:
363
- seen.add(n)
364
- order.append(n)
365
- except ValueError:
366
- continue
367
- for i in range(1, total + 1):
368
- if i not in seen:
369
- order.append(i)
370
- return order
@@ -1,143 +0,0 @@
1
- import json
2
- import time
3
- from typing import Any
4
-
5
- import httpx
6
- from tenacity import AsyncRetrying, retry_if_exception, stop_after_attempt, wait_random_exponential
7
-
8
- from .base import BaseSearchProvider
9
- from ..config import config
10
- from ..logger import log_info
11
-
12
-
13
- RETRYABLE_STATUS_CODES = {408, 500, 502, 503, 504}
14
-
15
-
16
- def _is_retryable_exception(exc) -> bool:
17
- if isinstance(exc, (httpx.TimeoutException, httpx.NetworkError, httpx.ConnectError)):
18
- return True
19
- if isinstance(exc, httpx.HTTPStatusError):
20
- return exc.response.status_code in RETRYABLE_STATUS_CODES
21
- return False
22
-
23
-
24
- def _normalize_result(item: dict[str, Any]) -> dict[str, Any]:
25
- return {
26
- "title": item.get("title") or "",
27
- "url": item.get("link") or item.get("url") or "",
28
- "description": item.get("content") or "",
29
- "provider": "zhipu",
30
- "source": item.get("media") or "",
31
- "published_date": item.get("publish_date") or "",
32
- "icon": item.get("icon") or "",
33
- "refer": item.get("refer") or "",
34
- }
35
-
36
-
37
- def _error_payload(exc: Exception) -> dict[str, Any]:
38
- if isinstance(exc, httpx.HTTPStatusError):
39
- status_code = exc.response.status_code
40
- if status_code == 429:
41
- error_type = "rate_limited"
42
- elif status_code in {401, 403}:
43
- error_type = "auth_error"
44
- else:
45
- error_type = "network_error"
46
- return {"error_type": error_type, "error": f"HTTP {status_code}: {exc.response.reason_phrase}"}
47
- if isinstance(exc, httpx.TimeoutException):
48
- return {"error_type": "timeout", "error": "request timed out"}
49
- if isinstance(exc, httpx.RequestError):
50
- return {"error_type": "network_error", "error": str(exc)}
51
- return {"error_type": "runtime_error", "error": str(exc)}
52
-
53
-
54
- class ZhipuWebSearchProvider(BaseSearchProvider):
55
- def __init__(
56
- self,
57
- api_url: str,
58
- api_key: str,
59
- search_engine: str = "search_std",
60
- timeout: float = 30.0,
61
- ):
62
- super().__init__(api_url.rstrip("/"), api_key)
63
- self.search_engine = search_engine
64
- self.timeout = timeout
65
-
66
- def get_provider_name(self) -> str:
67
- return "Zhipu Web Search"
68
-
69
- async def search(
70
- self,
71
- query: str,
72
- count: int = 10,
73
- search_engine: str | None = None,
74
- search_intent: bool = True,
75
- search_domain_filter: str = "",
76
- search_recency_filter: str = "noLimit",
77
- content_size: str = "medium",
78
- user_id: str = "",
79
- ctx=None,
80
- ) -> str:
81
- endpoint = f"{self.api_url}/paas/v4/web_search"
82
- headers = {
83
- "Authorization": f"Bearer {self.api_key}",
84
- "Content-Type": "application/json",
85
- "Accept": "application/json",
86
- }
87
- payload: dict[str, Any] = {
88
- "search_query": query[:70],
89
- "search_engine": search_engine or self.search_engine,
90
- "search_intent": search_intent,
91
- "count": count,
92
- "search_recency_filter": search_recency_filter,
93
- "content_size": content_size,
94
- }
95
- if search_domain_filter:
96
- payload["search_domain_filter"] = search_domain_filter
97
- if user_id:
98
- payload["user_id"] = user_id
99
-
100
- await log_info(ctx, f"Zhipu search: {query}", config.debug_enabled)
101
- start_time = time.time()
102
- try:
103
- data = await self._request_with_retry(endpoint, headers, payload)
104
- elapsed_ms = round((time.time() - start_time) * 1000, 2)
105
- results = [_normalize_result(item) for item in data.get("search_result", []) or []]
106
- output = {
107
- "ok": True,
108
- "query": query,
109
- "provider": "zhipu",
110
- "search_engine": payload["search_engine"],
111
- "results": results,
112
- "total": len(results),
113
- "search_intent": data.get("search_intent", []),
114
- "request_id": data.get("request_id", ""),
115
- "elapsed_ms": elapsed_ms,
116
- }
117
- except Exception as e:
118
- elapsed_ms = round((time.time() - start_time) * 1000, 2)
119
- error = _error_payload(e)
120
- output = {
121
- "ok": False,
122
- "query": query,
123
- "provider": "zhipu",
124
- "error_type": error["error_type"],
125
- "error": error["error"],
126
- "elapsed_ms": elapsed_ms,
127
- }
128
- return json.dumps(output, ensure_ascii=False, indent=2)
129
-
130
- async def _request_with_retry(self, endpoint: str, headers: dict, payload: dict) -> dict[str, Any]:
131
- timeout = httpx.Timeout(connect=6.0, read=self.timeout, write=10.0, pool=None)
132
- async with httpx.AsyncClient(timeout=timeout, follow_redirects=True) as client:
133
- async for attempt in AsyncRetrying(
134
- stop=stop_after_attempt(config.retry_max_attempts + 1),
135
- wait=wait_random_exponential(multiplier=config.retry_multiplier, max=config.retry_max_wait),
136
- retry=retry_if_exception(_is_retryable_exception),
137
- reraise=True,
138
- ):
139
- with attempt:
140
- response = await client.post(endpoint, headers=headers, json=payload)
141
- response.raise_for_status()
142
- return response.json()
143
- return {}