entari-plugin-hyw 4.0.0rc12__tar.gz → 4.0.0rc14__tar.gz

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.

Potentially problematic release.


This version of entari-plugin-hyw might be problematic. Click here for more details.

Files changed (78) hide show
  1. {entari_plugin_hyw-4.0.0rc12/src/entari_plugin_hyw.egg-info → entari_plugin_hyw-4.0.0rc14}/PKG-INFO +1 -1
  2. {entari_plugin_hyw-4.0.0rc12 → entari_plugin_hyw-4.0.0rc14}/pyproject.toml +1 -1
  3. {entari_plugin_hyw-4.0.0rc12 → entari_plugin_hyw-4.0.0rc14/src/entari_plugin_hyw.egg-info}/PKG-INFO +1 -1
  4. {entari_plugin_hyw-4.0.0rc12 → entari_plugin_hyw-4.0.0rc14}/src/hyw_core/agent.py +176 -56
  5. {entari_plugin_hyw-4.0.0rc12 → entari_plugin_hyw-4.0.0rc14}/src/hyw_core/definitions.py +4 -1
  6. {entari_plugin_hyw-4.0.0rc12 → entari_plugin_hyw-4.0.0rc14}/MANIFEST.in +0 -0
  7. {entari_plugin_hyw-4.0.0rc12 → entari_plugin_hyw-4.0.0rc14}/README.md +0 -0
  8. {entari_plugin_hyw-4.0.0rc12 → entari_plugin_hyw-4.0.0rc14}/setup.cfg +0 -0
  9. {entari_plugin_hyw-4.0.0rc12 → entari_plugin_hyw-4.0.0rc14}/src/entari_plugin_hyw/Untitled-1 +0 -0
  10. {entari_plugin_hyw-4.0.0rc12 → entari_plugin_hyw-4.0.0rc14}/src/entari_plugin_hyw/__init__.py +0 -0
  11. {entari_plugin_hyw-4.0.0rc12 → entari_plugin_hyw-4.0.0rc14}/src/entari_plugin_hyw/filters.py +0 -0
  12. {entari_plugin_hyw-4.0.0rc12 → entari_plugin_hyw-4.0.0rc14}/src/entari_plugin_hyw/history.py +0 -0
  13. {entari_plugin_hyw-4.0.0rc12 → entari_plugin_hyw-4.0.0rc14}/src/entari_plugin_hyw/misc.py +0 -0
  14. {entari_plugin_hyw-4.0.0rc12 → entari_plugin_hyw-4.0.0rc14}/src/entari_plugin_hyw/search_cache.py +0 -0
  15. {entari_plugin_hyw-4.0.0rc12 → entari_plugin_hyw-4.0.0rc14}/src/entari_plugin_hyw.egg-info/SOURCES.txt +0 -0
  16. {entari_plugin_hyw-4.0.0rc12 → entari_plugin_hyw-4.0.0rc14}/src/entari_plugin_hyw.egg-info/dependency_links.txt +0 -0
  17. {entari_plugin_hyw-4.0.0rc12 → entari_plugin_hyw-4.0.0rc14}/src/entari_plugin_hyw.egg-info/requires.txt +0 -0
  18. {entari_plugin_hyw-4.0.0rc12 → entari_plugin_hyw-4.0.0rc14}/src/entari_plugin_hyw.egg-info/top_level.txt +0 -0
  19. {entari_plugin_hyw-4.0.0rc12 → entari_plugin_hyw-4.0.0rc14}/src/hyw_core/__init__.py +0 -0
  20. {entari_plugin_hyw-4.0.0rc12 → entari_plugin_hyw-4.0.0rc14}/src/hyw_core/browser_control/__init__.py +0 -0
  21. {entari_plugin_hyw-4.0.0rc12 → entari_plugin_hyw-4.0.0rc14}/src/hyw_core/browser_control/assets/card-dist/index.html +0 -0
  22. {entari_plugin_hyw-4.0.0rc12 → entari_plugin_hyw-4.0.0rc14}/src/hyw_core/browser_control/assets/card-dist/logos/anthropic.svg +0 -0
  23. {entari_plugin_hyw-4.0.0rc12 → entari_plugin_hyw-4.0.0rc14}/src/hyw_core/browser_control/assets/card-dist/logos/cerebras.svg +0 -0
  24. {entari_plugin_hyw-4.0.0rc12 → entari_plugin_hyw-4.0.0rc14}/src/hyw_core/browser_control/assets/card-dist/logos/deepseek.png +0 -0
  25. {entari_plugin_hyw-4.0.0rc12 → entari_plugin_hyw-4.0.0rc14}/src/hyw_core/browser_control/assets/card-dist/logos/gemini.svg +0 -0
  26. {entari_plugin_hyw-4.0.0rc12 → entari_plugin_hyw-4.0.0rc14}/src/hyw_core/browser_control/assets/card-dist/logos/google.svg +0 -0
  27. {entari_plugin_hyw-4.0.0rc12 → entari_plugin_hyw-4.0.0rc14}/src/hyw_core/browser_control/assets/card-dist/logos/grok.png +0 -0
  28. {entari_plugin_hyw-4.0.0rc12 → entari_plugin_hyw-4.0.0rc14}/src/hyw_core/browser_control/assets/card-dist/logos/huggingface.png +0 -0
  29. {entari_plugin_hyw-4.0.0rc12 → entari_plugin_hyw-4.0.0rc14}/src/hyw_core/browser_control/assets/card-dist/logos/microsoft.svg +0 -0
  30. {entari_plugin_hyw-4.0.0rc12 → entari_plugin_hyw-4.0.0rc14}/src/hyw_core/browser_control/assets/card-dist/logos/minimax.png +0 -0
  31. {entari_plugin_hyw-4.0.0rc12 → entari_plugin_hyw-4.0.0rc14}/src/hyw_core/browser_control/assets/card-dist/logos/mistral.png +0 -0
  32. {entari_plugin_hyw-4.0.0rc12 → entari_plugin_hyw-4.0.0rc14}/src/hyw_core/browser_control/assets/card-dist/logos/nvida.png +0 -0
  33. {entari_plugin_hyw-4.0.0rc12 → entari_plugin_hyw-4.0.0rc14}/src/hyw_core/browser_control/assets/card-dist/logos/openai.svg +0 -0
  34. {entari_plugin_hyw-4.0.0rc12 → entari_plugin_hyw-4.0.0rc14}/src/hyw_core/browser_control/assets/card-dist/logos/openrouter.png +0 -0
  35. {entari_plugin_hyw-4.0.0rc12 → entari_plugin_hyw-4.0.0rc14}/src/hyw_core/browser_control/assets/card-dist/logos/perplexity.svg +0 -0
  36. {entari_plugin_hyw-4.0.0rc12 → entari_plugin_hyw-4.0.0rc14}/src/hyw_core/browser_control/assets/card-dist/logos/qwen.png +0 -0
  37. {entari_plugin_hyw-4.0.0rc12 → entari_plugin_hyw-4.0.0rc14}/src/hyw_core/browser_control/assets/card-dist/logos/xai.png +0 -0
  38. {entari_plugin_hyw-4.0.0rc12 → entari_plugin_hyw-4.0.0rc14}/src/hyw_core/browser_control/assets/card-dist/logos/xiaomi.png +0 -0
  39. {entari_plugin_hyw-4.0.0rc12 → entari_plugin_hyw-4.0.0rc14}/src/hyw_core/browser_control/assets/card-dist/logos/zai.png +0 -0
  40. {entari_plugin_hyw-4.0.0rc12 → entari_plugin_hyw-4.0.0rc14}/src/hyw_core/browser_control/assets/card-dist/vite.svg +0 -0
  41. {entari_plugin_hyw-4.0.0rc12 → entari_plugin_hyw-4.0.0rc14}/src/hyw_core/browser_control/assets/index.html +0 -0
  42. {entari_plugin_hyw-4.0.0rc12 → entari_plugin_hyw-4.0.0rc14}/src/hyw_core/browser_control/assets/logos/anthropic.svg +0 -0
  43. {entari_plugin_hyw-4.0.0rc12 → entari_plugin_hyw-4.0.0rc14}/src/hyw_core/browser_control/assets/logos/cerebras.svg +0 -0
  44. {entari_plugin_hyw-4.0.0rc12 → entari_plugin_hyw-4.0.0rc14}/src/hyw_core/browser_control/assets/logos/deepseek.png +0 -0
  45. {entari_plugin_hyw-4.0.0rc12 → entari_plugin_hyw-4.0.0rc14}/src/hyw_core/browser_control/assets/logos/gemini.svg +0 -0
  46. {entari_plugin_hyw-4.0.0rc12 → entari_plugin_hyw-4.0.0rc14}/src/hyw_core/browser_control/assets/logos/google.svg +0 -0
  47. {entari_plugin_hyw-4.0.0rc12 → entari_plugin_hyw-4.0.0rc14}/src/hyw_core/browser_control/assets/logos/grok.png +0 -0
  48. {entari_plugin_hyw-4.0.0rc12 → entari_plugin_hyw-4.0.0rc14}/src/hyw_core/browser_control/assets/logos/huggingface.png +0 -0
  49. {entari_plugin_hyw-4.0.0rc12 → entari_plugin_hyw-4.0.0rc14}/src/hyw_core/browser_control/assets/logos/microsoft.svg +0 -0
  50. {entari_plugin_hyw-4.0.0rc12 → entari_plugin_hyw-4.0.0rc14}/src/hyw_core/browser_control/assets/logos/minimax.png +0 -0
  51. {entari_plugin_hyw-4.0.0rc12 → entari_plugin_hyw-4.0.0rc14}/src/hyw_core/browser_control/assets/logos/mistral.png +0 -0
  52. {entari_plugin_hyw-4.0.0rc12 → entari_plugin_hyw-4.0.0rc14}/src/hyw_core/browser_control/assets/logos/nvida.png +0 -0
  53. {entari_plugin_hyw-4.0.0rc12 → entari_plugin_hyw-4.0.0rc14}/src/hyw_core/browser_control/assets/logos/openai.svg +0 -0
  54. {entari_plugin_hyw-4.0.0rc12 → entari_plugin_hyw-4.0.0rc14}/src/hyw_core/browser_control/assets/logos/openrouter.png +0 -0
  55. {entari_plugin_hyw-4.0.0rc12 → entari_plugin_hyw-4.0.0rc14}/src/hyw_core/browser_control/assets/logos/perplexity.svg +0 -0
  56. {entari_plugin_hyw-4.0.0rc12 → entari_plugin_hyw-4.0.0rc14}/src/hyw_core/browser_control/assets/logos/qwen.png +0 -0
  57. {entari_plugin_hyw-4.0.0rc12 → entari_plugin_hyw-4.0.0rc14}/src/hyw_core/browser_control/assets/logos/xai.png +0 -0
  58. {entari_plugin_hyw-4.0.0rc12 → entari_plugin_hyw-4.0.0rc14}/src/hyw_core/browser_control/assets/logos/xiaomi.png +0 -0
  59. {entari_plugin_hyw-4.0.0rc12 → entari_plugin_hyw-4.0.0rc14}/src/hyw_core/browser_control/assets/logos/zai.png +0 -0
  60. {entari_plugin_hyw-4.0.0rc12 → entari_plugin_hyw-4.0.0rc14}/src/hyw_core/browser_control/engines/__init__.py +0 -0
  61. {entari_plugin_hyw-4.0.0rc12 → entari_plugin_hyw-4.0.0rc14}/src/hyw_core/browser_control/engines/base.py +0 -0
  62. {entari_plugin_hyw-4.0.0rc12 → entari_plugin_hyw-4.0.0rc14}/src/hyw_core/browser_control/engines/default.py +0 -0
  63. {entari_plugin_hyw-4.0.0rc12 → entari_plugin_hyw-4.0.0rc14}/src/hyw_core/browser_control/engines/duckduckgo.py +0 -0
  64. {entari_plugin_hyw-4.0.0rc12 → entari_plugin_hyw-4.0.0rc14}/src/hyw_core/browser_control/landing.html +0 -0
  65. {entari_plugin_hyw-4.0.0rc12 → entari_plugin_hyw-4.0.0rc14}/src/hyw_core/browser_control/manager.py +0 -0
  66. {entari_plugin_hyw-4.0.0rc12 → entari_plugin_hyw-4.0.0rc14}/src/hyw_core/browser_control/renderer.py +0 -0
  67. {entari_plugin_hyw-4.0.0rc12 → entari_plugin_hyw-4.0.0rc14}/src/hyw_core/browser_control/service.py +0 -0
  68. {entari_plugin_hyw-4.0.0rc12 → entari_plugin_hyw-4.0.0rc14}/src/hyw_core/config.py +0 -0
  69. {entari_plugin_hyw-4.0.0rc12 → entari_plugin_hyw-4.0.0rc14}/src/hyw_core/core.py +0 -0
  70. {entari_plugin_hyw-4.0.0rc12 → entari_plugin_hyw-4.0.0rc14}/src/hyw_core/crawling/__init__.py +0 -0
  71. {entari_plugin_hyw-4.0.0rc12 → entari_plugin_hyw-4.0.0rc14}/src/hyw_core/crawling/completeness.py +0 -0
  72. {entari_plugin_hyw-4.0.0rc12 → entari_plugin_hyw-4.0.0rc14}/src/hyw_core/crawling/models.py +0 -0
  73. {entari_plugin_hyw-4.0.0rc12 → entari_plugin_hyw-4.0.0rc14}/src/hyw_core/image_cache.py +0 -0
  74. {entari_plugin_hyw-4.0.0rc12 → entari_plugin_hyw-4.0.0rc14}/src/hyw_core/pipeline.py +0 -0
  75. {entari_plugin_hyw-4.0.0rc12 → entari_plugin_hyw-4.0.0rc14}/src/hyw_core/search.py +0 -0
  76. {entari_plugin_hyw-4.0.0rc12 → entari_plugin_hyw-4.0.0rc14}/src/hyw_core/stages/__init__.py +0 -0
  77. {entari_plugin_hyw-4.0.0rc12 → entari_plugin_hyw-4.0.0rc14}/src/hyw_core/stages/base.py +0 -0
  78. {entari_plugin_hyw-4.0.0rc12 → entari_plugin_hyw-4.0.0rc14}/src/hyw_core/stages/summary.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: entari_plugin_hyw
3
- Version: 4.0.0rc12
3
+ Version: 4.0.0rc14
4
4
  Summary: Use large language models to interpret chat messages
5
5
  Author-email: kumoSleeping <zjr2992@outlook.com>
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "entari_plugin_hyw"
7
- version = "4.0.0-rc12"
7
+ version = "4.0.0-rc14"
8
8
  description = "Use large language models to interpret chat messages"
9
9
  authors = [{name = "kumoSleeping", email = "zjr2992@outlook.com"}]
10
10
  dependencies = [
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: entari_plugin_hyw
3
- Version: 4.0.0rc12
3
+ Version: 4.0.0rc14
4
4
  Summary: Use large language models to interpret chat messages
5
5
  Author-email: kumoSleeping <zjr2992@outlook.com>
6
6
  License: MIT
@@ -2,7 +2,7 @@
2
2
  Agent Pipeline
3
3
 
4
4
  Tool-calling agent that can autonomously use web_tool to search/screenshot.
5
- Maximum 2 tool calls, then forced summary.
5
+ Maximum 2 rounds of tool calls, up to 3 parallel calls per round.
6
6
  """
7
7
 
8
8
  import asyncio
@@ -31,6 +31,9 @@ class AgentSession:
31
31
  messages: List[Dict] = field(default_factory=list) # LLM conversation
32
32
  created_at: float = field(default_factory=time.time)
33
33
 
34
+ # Round tracking (each round can have up to 3 parallel tool calls)
35
+ round_count: int = 0
36
+
34
37
  # Image tracking
35
38
  user_image_count: int = 0 # Number of images from user input
36
39
  total_image_count: int = 0 # Total images including web screenshots
@@ -45,12 +48,13 @@ class AgentSession:
45
48
 
46
49
  @property
47
50
  def call_count(self) -> int:
51
+ """Total number of individual tool calls."""
48
52
  return len(self.tool_calls)
49
53
 
50
54
  @property
51
55
  def should_force_summary(self) -> bool:
52
- """第3次调用时强制总结"""
53
- return self.call_count >= 2
56
+ """Force summary after 2 rounds of tool calls."""
57
+ return self.round_count >= 2
54
58
 
55
59
 
56
60
  def parse_filter_syntax(query: str, max_count: int = 3):
@@ -130,12 +134,15 @@ class AgentPipeline:
130
134
 
131
135
  Flow:
132
136
  1. 用户输入 → LLM (with tools)
133
- 2. If tool_call: execute tool → notify user → loop
134
- 3. If call_count >= 2: force summary on next call
137
+ 2. If tool_call: execute all tools in parallel → notify user with batched message → loop
138
+ 3. If call_count >= 2 rounds: force summary on next call
135
139
  4. Return final content
136
140
  """
137
141
 
138
- MAX_TOOL_CALLS = 2
142
+ MAX_TOOL_ROUNDS = 2 # Maximum rounds of tool calls
143
+ MAX_PARALLEL_TOOLS = 3 # Maximum parallel tool calls per round
144
+ MAX_LLM_RETRIES = 3 # Maximum retries for empty API responses
145
+ LLM_RETRY_DELAY = 1.0 # Delay between retries in seconds
139
146
 
140
147
  def __init__(
141
148
  self,
@@ -183,7 +190,9 @@ class AgentPipeline:
183
190
 
184
191
  # Build initial messages
185
192
  language = getattr(self.config, "language", "Simplified Chinese")
186
- system_prompt = AGENT_SYSTEM_PROMPT + f"\n\n用户要求的语言: {language}"
193
+ from datetime import datetime
194
+ current_time = datetime.now().strftime("%Y-%m-%d %H:%M")
195
+ system_prompt = AGENT_SYSTEM_PROMPT + f"\n\n用户要求的语言: {language}\n当前时间: {current_time}"
187
196
 
188
197
  # Build user content with images if provided
189
198
  user_image_count = len(images) if images else 0
@@ -230,7 +239,7 @@ class AgentPipeline:
230
239
  while True:
231
240
  # Check if we need to force summary (no tools)
232
241
  if session.should_force_summary:
233
- logger.info(f"AgentPipeline: Max tool calls ({self.MAX_TOOL_CALLS}) reached, forcing summary")
242
+ logger.info(f"AgentPipeline: Max tool rounds ({self.MAX_TOOL_ROUNDS}) reached, forcing summary")
234
243
  # Add context message about collected info
235
244
  if context.web_results:
236
245
  context_msg = self._format_web_context(context)
@@ -240,38 +249,88 @@ class AgentPipeline:
240
249
  })
241
250
 
242
251
 
243
- # Final call without tools
244
- response = await client.chat.completions.create(
245
- model=model,
246
- messages=session.messages,
247
- temperature=self.config.temperature,
248
- )
252
+ # Final call without tools (with retry)
253
+ response = None
254
+ for retry in range(self.MAX_LLM_RETRIES):
255
+ try:
256
+ response = await client.chat.completions.create(
257
+ model=model,
258
+ messages=session.messages,
259
+ temperature=self.config.temperature,
260
+ )
261
+
262
+ if response.usage:
263
+ usage_totals["input_tokens"] += response.usage.prompt_tokens or 0
264
+ usage_totals["output_tokens"] += response.usage.completion_tokens or 0
265
+
266
+ # Check for valid response
267
+ if response.choices:
268
+ break # Success, exit retry loop
269
+
270
+ # Empty choices, retry
271
+ logger.warning(f"AgentPipeline: Empty choices in force-summary (attempt {retry + 1}/{self.MAX_LLM_RETRIES}): {response}")
272
+ if retry < self.MAX_LLM_RETRIES - 1:
273
+ await asyncio.sleep(self.LLM_RETRY_DELAY)
274
+ except Exception as e:
275
+ logger.warning(f"AgentPipeline: LLM error (attempt {retry + 1}/{self.MAX_LLM_RETRIES}): {e}")
276
+ if retry < self.MAX_LLM_RETRIES - 1:
277
+ await asyncio.sleep(self.LLM_RETRY_DELAY)
278
+ else:
279
+ return {
280
+ "llm_response": f"Error: {e}",
281
+ "success": False,
282
+ "error": str(e),
283
+ "stats": {"total_time": time.time() - start_time}
284
+ }
249
285
 
250
- if response.usage:
251
- usage_totals["input_tokens"] += response.usage.prompt_tokens or 0
252
- usage_totals["output_tokens"] += response.usage.completion_tokens or 0
286
+ # Final check after all retries
287
+ if not response or not response.choices:
288
+ logger.error(f"AgentPipeline: All retries failed for force-summary")
289
+ return {
290
+ "llm_response": "抱歉,AI 服务返回了空响应,请稍后重试。",
291
+ "success": False,
292
+ "error": "Empty response from API after retries",
293
+ "stats": {"total_time": time.time() - start_time},
294
+ "usage": usage_totals,
295
+ }
253
296
 
254
297
  final_content = response.choices[0].message.content or ""
255
298
  break
256
299
 
257
- # Normal call with tools
300
+ # Normal call with tools (with retry)
258
301
  llm_start = time.time()
259
- try:
260
- response = await client.chat.completions.create(
261
- model=model,
262
- messages=session.messages,
263
- temperature=self.config.temperature,
264
- tools=tools,
265
- tool_choice="auto",
266
- )
267
- except Exception as e:
268
- logger.error(f"AgentPipeline: LLM error: {e}")
269
- return {
270
- "llm_response": f"Error: {e}",
271
- "success": False,
272
- "error": str(e),
273
- "stats": {"total_time": time.time() - start_time}
274
- }
302
+ response = None
303
+
304
+ for retry in range(self.MAX_LLM_RETRIES):
305
+ try:
306
+ response = await client.chat.completions.create(
307
+ model=model,
308
+ messages=session.messages,
309
+ temperature=self.config.temperature,
310
+ tools=tools,
311
+ tool_choice="auto",
312
+ )
313
+
314
+ # Check for valid response
315
+ if response.choices:
316
+ break # Success, exit retry loop
317
+
318
+ # Empty choices, retry
319
+ logger.warning(f"AgentPipeline: Empty choices (attempt {retry + 1}/{self.MAX_LLM_RETRIES}): {response}")
320
+ if retry < self.MAX_LLM_RETRIES - 1:
321
+ await asyncio.sleep(self.LLM_RETRY_DELAY)
322
+ except Exception as e:
323
+ logger.warning(f"AgentPipeline: LLM error (attempt {retry + 1}/{self.MAX_LLM_RETRIES}): {e}")
324
+ if retry < self.MAX_LLM_RETRIES - 1:
325
+ await asyncio.sleep(self.LLM_RETRY_DELAY)
326
+ else:
327
+ logger.error(f"AgentPipeline: All retries failed: {e}")
328
+ return {
329
+ "llm_response": f"Error: {e}",
330
+ "success": False,
331
+ "error": str(e),
332
+ "stats": {"total_time": time.time() - start_time}
333
+ }
275
334
 
276
335
  llm_duration = time.time() - llm_start
277
336
  session.llm_time += llm_duration
@@ -280,6 +339,17 @@ class AgentPipeline:
280
339
  if session.call_count == 0 and session.first_llm_time == 0:
281
340
  session.first_llm_time = llm_duration
282
341
 
342
+ # Final check after all retries
343
+ if not response or not response.choices:
344
+ logger.error(f"AgentPipeline: All retries failed, empty choices")
345
+ return {
346
+ "llm_response": "抱歉,AI 服务返回了空响应,请稍后重试。",
347
+ "success": False,
348
+ "error": "Empty response from API after retries",
349
+ "stats": {"total_time": time.time() - start_time},
350
+ "usage": usage_totals,
351
+ }
352
+
283
353
  if response.usage:
284
354
  usage_totals["input_tokens"] += response.usage.prompt_tokens or 0
285
355
  usage_totals["output_tokens"] += response.usage.completion_tokens or 0
@@ -307,7 +377,12 @@ class AgentPipeline:
307
377
  ]
308
378
  })
309
379
 
310
- # Execute tool calls
380
+ # Execute all tool calls in parallel
381
+ tool_tasks = []
382
+ tool_call_ids = []
383
+ tool_call_names = []
384
+ tool_call_args_list = []
385
+
311
386
  for tool_call in message.tool_calls:
312
387
  tc_id = tool_call.id
313
388
  func_name = tool_call.function.name
@@ -317,17 +392,22 @@ class AgentPipeline:
317
392
  except json.JSONDecodeError:
318
393
  args = {}
319
394
 
320
- logger.info(f"AgentPipeline: Executing tool '{func_name}' with args: {args}")
321
-
395
+ tool_call_ids.append(tc_id)
396
+ tool_call_names.append(func_name)
397
+ tool_call_args_list.append(args)
398
+ logger.info(f"AgentPipeline: Queueing tool '{func_name}' with args: {args}")
399
+
400
+ # Check for refuse_answer first (handle immediately)
401
+ for idx, func_name in enumerate(tool_call_names):
322
402
  if func_name == "refuse_answer":
323
- # Handle refusal
403
+ args = tool_call_args_list[idx]
324
404
  reason = args.get("reason", "Refused")
325
405
  context.should_refuse = True
326
406
  context.refuse_reason = reason
327
407
 
328
408
  session.messages.append({
329
409
  "role": "tool",
330
- "tool_call_id": tc_id,
410
+ "tool_call_id": tool_call_ids[idx],
331
411
  "content": f"已拒绝回答: {reason}"
332
412
  })
333
413
 
@@ -339,23 +419,50 @@ class AgentPipeline:
339
419
  "stats": {"total_time": time.time() - start_time},
340
420
  "usage": usage_totals,
341
421
  }
422
+
423
+ # Execute web_tool calls in parallel
424
+ search_start = time.time()
425
+ tasks_to_run = []
426
+ task_indices = []
427
+
428
+ for idx, func_name in enumerate(tool_call_names):
429
+ if func_name == "web_tool":
430
+ tasks_to_run.append(self._execute_web_tool(tool_call_args_list[idx], context))
431
+ task_indices.append(idx)
432
+
433
+ # Run all web_tool calls in parallel
434
+ if tasks_to_run:
435
+ results = await asyncio.gather(*tasks_to_run, return_exceptions=True)
436
+ else:
437
+ results = []
438
+
439
+ session.search_time += time.time() - search_start
440
+
441
+ # Process results and collect notifications
442
+ notifications = []
443
+ result_map = {} # Map task index to result
444
+
445
+ for i, result in enumerate(results):
446
+ task_idx = task_indices[i]
447
+ if isinstance(result, Exception):
448
+ result_map[task_idx] = {"summary": f"执行失败: {result}", "results": []}
449
+ else:
450
+ result_map[task_idx] = result
451
+
452
+ # Add all tool results to messages and collect notifications
453
+ for idx, func_name in enumerate(tool_call_names):
454
+ tc_id = tool_call_ids[idx]
455
+ args = tool_call_args_list[idx]
342
456
 
343
- elif func_name == "web_tool":
344
- # Execute web tool with time tracking
345
- search_start = time.time()
346
- result = await self._execute_web_tool(args, context)
347
- session.search_time += time.time() - search_start
457
+ if func_name == "web_tool":
458
+ result = result_map.get(idx, {"summary": "未执行", "results": []})
348
459
 
349
460
  # Track tool call
350
461
  session.tool_calls.append({"name": func_name, "args": args})
351
462
  session.tool_results.append(result)
352
463
 
353
- # Send IM notification with search result (NOT "正在搜索...")
354
- if self.send_func:
355
- try:
356
- await self.send_func(f"🔍 {result['summary']}")
357
- except Exception as e:
358
- logger.warning(f"AgentPipeline: Failed to send notification: {e}")
464
+ # Collect notification
465
+ notifications.append(f"🔍 {result['summary']}")
359
466
 
360
467
  # Add tool result to messages
361
468
  result_content = f"搜索完成: {result['summary']}\n\n找到 {len(result.get('results', []))} 个结果"
@@ -368,15 +475,15 @@ class AgentPipeline:
368
475
  # Add image source hint for web screenshots
369
476
  screenshot_count = result.get("screenshot_count", 0)
370
477
  if screenshot_count > 0:
371
- start_idx = session.total_image_count + 1
372
- end_idx = session.total_image_count + screenshot_count
373
- session.total_image_count = end_idx
478
+ start_idx_img = session.total_image_count + 1
479
+ end_idx_img = session.total_image_count + screenshot_count
480
+ session.total_image_count = end_idx_img
374
481
 
375
482
  source_desc = result.get("source_desc", "网页截图")
376
- if start_idx == end_idx:
377
- hint = f"第{start_idx}张图片来自{source_desc},作为查询的参考资料"
483
+ if start_idx_img == end_idx_img:
484
+ hint = f"第{start_idx_img}张图片来自{source_desc},作为查询的参考资料"
378
485
  else:
379
- hint = f"第{start_idx}-{end_idx}张图片来自{source_desc},作为查询的参考资料"
486
+ hint = f"第{start_idx_img}-{end_idx_img}张图片来自{source_desc},作为查询的参考资料"
380
487
  session.messages.append({"role": "system", "content": hint})
381
488
  else:
382
489
  # Unknown tool
@@ -385,6 +492,19 @@ class AgentPipeline:
385
492
  "tool_call_id": tc_id,
386
493
  "content": f"Unknown tool: {func_name}"
387
494
  })
495
+
496
+ # Send batched notification (up to 3 lines)
497
+ if self.send_func and notifications:
498
+ try:
499
+ # Join notifications with newlines, max 3 lines
500
+ notification_msg = "\n".join(notifications[:3])
501
+ await self.send_func(notification_msg)
502
+ except Exception as e:
503
+ logger.warning(f"AgentPipeline: Failed to send notification: {e}")
504
+
505
+ # Increment round count after processing all tool calls in this round
506
+ if tasks_to_run:
507
+ session.round_count += 1
388
508
 
389
509
  # Build final response
390
510
  total_time = time.time() - start_time
@@ -46,6 +46,7 @@ def get_web_tool() -> Dict[str, Any]:
46
46
  "function": {
47
47
  "name": "web_tool",
48
48
  "description": """搜索网页或截图指定URL。用于获取最新信息、查找资料。
49
+ ## 使用方式
49
50
  网页搜索(大部分问题优先使用此方法):
50
51
  直接传入搜索词如 "python async" 会返回搜索结果列表
51
52
 
@@ -84,10 +85,12 @@ AGENT_SYSTEM_PROMPT = """# 你是一个智能助手 (Agent), 你的职责是使
84
85
  最小限度使用自身知识, 尽可能使用 web_tool 获取信息.
85
86
 
86
87
  ## 工具使用指南
88
+ - 并行调用工具
89
+ - 网页搜索: 可以同时调用3次, 其中URL截图消耗较大, 最多同时调用1个
87
90
  - 积极使用 web_tool 获取信息
88
91
  - 搜索时, 关键词保证简单、指向准确、利于传统搜索引擎.
89
92
  - 获取页面截图时, 只使用官方性较强的 wiki、官方网站、资源站等等, 不使用第三方转载新闻网站.
90
- - 最多可调用2次工具, 之后必须给出最终回答
93
+ - 最多可调用2轮工具, 之后必须给出最终回答
91
94
  - 适当时候调用 `refuse_answer`
92
95
  - 对于具体任务, 如果是转述、格式化、翻译等, 请直接给出最终回答, 不再调用工具
93
96