entari-plugin-hyw 4.0.0rc13__py3-none-any.whl → 4.0.0rc15__py3-none-any.whl

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.

hyw_core/agent.py CHANGED
@@ -15,7 +15,7 @@ from typing import Any, Callable, Awaitable, Dict, List, Optional
15
15
  from loguru import logger
16
16
  from openai import AsyncOpenAI
17
17
 
18
- from .definitions import get_web_tool, get_refuse_answer_tool, AGENT_SYSTEM_PROMPT
18
+ from .definitions import get_web_tool, get_refuse_answer_tool, get_js_tool, AGENT_SYSTEM_PROMPT
19
19
  from .stages.base import StageContext, StageResult
20
20
  from .search import SearchService
21
21
 
@@ -141,6 +141,8 @@ class AgentPipeline:
141
141
 
142
142
  MAX_TOOL_ROUNDS = 2 # Maximum rounds of tool calls
143
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
144
146
 
145
147
  def __init__(
146
148
  self,
@@ -207,8 +209,19 @@ class AgentPipeline:
207
209
 
208
210
  session.messages = [
209
211
  {"role": "system", "content": system_prompt},
210
- {"role": "user", "content": user_content}
211
212
  ]
213
+
214
+ # Add conversation history (previous turns) before current user message
215
+ # This enables continuous conversation context
216
+ if conversation_history:
217
+ for msg in conversation_history:
218
+ role = msg.get("role", "")
219
+ content = msg.get("content", "")
220
+ if role in ("user", "assistant") and content:
221
+ session.messages.append({"role": role, "content": content})
222
+
223
+ # Add current user message
224
+ session.messages.append({"role": "user", "content": user_content})
212
225
 
213
226
  # Add image source hint for user images
214
227
  if user_image_count > 0:
@@ -221,8 +234,9 @@ class AgentPipeline:
221
234
  # Tool definitions
222
235
  web_tool = get_web_tool()
223
236
  refuse_tool = get_refuse_answer_tool()
224
- tools = [web_tool, refuse_tool]
225
-
237
+ js_tool = get_js_tool()
238
+ tools = [web_tool, refuse_tool, js_tool]
239
+
226
240
  usage_totals = {"input_tokens": 0, "output_tokens": 0}
227
241
  final_content = ""
228
242
 
@@ -247,38 +261,88 @@ class AgentPipeline:
247
261
  })
248
262
 
249
263
 
250
- # Final call without tools
251
- response = await client.chat.completions.create(
252
- model=model,
253
- messages=session.messages,
254
- temperature=self.config.temperature,
255
- )
264
+ # Final call without tools (with retry)
265
+ response = None
266
+ for retry in range(self.MAX_LLM_RETRIES):
267
+ try:
268
+ response = await client.chat.completions.create(
269
+ model=model,
270
+ messages=session.messages,
271
+ temperature=self.config.temperature,
272
+ )
273
+
274
+ if response.usage:
275
+ usage_totals["input_tokens"] += response.usage.prompt_tokens or 0
276
+ usage_totals["output_tokens"] += response.usage.completion_tokens or 0
277
+
278
+ # Check for valid response
279
+ if response.choices:
280
+ break # Success, exit retry loop
281
+
282
+ # Empty choices, retry
283
+ logger.warning(f"AgentPipeline: Empty choices in force-summary (attempt {retry + 1}/{self.MAX_LLM_RETRIES}): {response}")
284
+ if retry < self.MAX_LLM_RETRIES - 1:
285
+ await asyncio.sleep(self.LLM_RETRY_DELAY)
286
+ except Exception as e:
287
+ logger.warning(f"AgentPipeline: LLM error (attempt {retry + 1}/{self.MAX_LLM_RETRIES}): {e}")
288
+ if retry < self.MAX_LLM_RETRIES - 1:
289
+ await asyncio.sleep(self.LLM_RETRY_DELAY)
290
+ else:
291
+ return {
292
+ "llm_response": f"Error: {e}",
293
+ "success": False,
294
+ "error": str(e),
295
+ "stats": {"total_time": time.time() - start_time}
296
+ }
256
297
 
257
- if response.usage:
258
- usage_totals["input_tokens"] += response.usage.prompt_tokens or 0
259
- usage_totals["output_tokens"] += response.usage.completion_tokens or 0
298
+ # Final check after all retries
299
+ if not response or not response.choices:
300
+ logger.error(f"AgentPipeline: All retries failed for force-summary")
301
+ return {
302
+ "llm_response": "抱歉,AI 服务返回了空响应,请稍后重试。",
303
+ "success": False,
304
+ "error": "Empty response from API after retries",
305
+ "stats": {"total_time": time.time() - start_time},
306
+ "usage": usage_totals,
307
+ }
260
308
 
261
309
  final_content = response.choices[0].message.content or ""
262
310
  break
263
311
 
264
- # Normal call with tools
312
+ # Normal call with tools (with retry)
265
313
  llm_start = time.time()
266
- try:
267
- response = await client.chat.completions.create(
268
- model=model,
269
- messages=session.messages,
270
- temperature=self.config.temperature,
271
- tools=tools,
272
- tool_choice="auto",
273
- )
274
- except Exception as e:
275
- logger.error(f"AgentPipeline: LLM error: {e}")
276
- return {
277
- "llm_response": f"Error: {e}",
278
- "success": False,
279
- "error": str(e),
280
- "stats": {"total_time": time.time() - start_time}
281
- }
314
+ response = None
315
+
316
+ for retry in range(self.MAX_LLM_RETRIES):
317
+ try:
318
+ response = await client.chat.completions.create(
319
+ model=model,
320
+ messages=session.messages,
321
+ temperature=self.config.temperature,
322
+ tools=tools,
323
+ tool_choice="auto",
324
+ )
325
+
326
+ # Check for valid response
327
+ if response.choices:
328
+ break # Success, exit retry loop
329
+
330
+ # Empty choices, retry
331
+ logger.warning(f"AgentPipeline: Empty choices (attempt {retry + 1}/{self.MAX_LLM_RETRIES}): {response}")
332
+ if retry < self.MAX_LLM_RETRIES - 1:
333
+ await asyncio.sleep(self.LLM_RETRY_DELAY)
334
+ except Exception as e:
335
+ logger.warning(f"AgentPipeline: LLM error (attempt {retry + 1}/{self.MAX_LLM_RETRIES}): {e}")
336
+ if retry < self.MAX_LLM_RETRIES - 1:
337
+ await asyncio.sleep(self.LLM_RETRY_DELAY)
338
+ else:
339
+ logger.error(f"AgentPipeline: All retries failed: {e}")
340
+ return {
341
+ "llm_response": f"Error: {e}",
342
+ "success": False,
343
+ "error": str(e),
344
+ "stats": {"total_time": time.time() - start_time}
345
+ }
282
346
 
283
347
  llm_duration = time.time() - llm_start
284
348
  session.llm_time += llm_duration
@@ -287,6 +351,17 @@ class AgentPipeline:
287
351
  if session.call_count == 0 and session.first_llm_time == 0:
288
352
  session.first_llm_time = llm_duration
289
353
 
354
+ # Final check after all retries
355
+ if not response or not response.choices:
356
+ logger.error(f"AgentPipeline: All retries failed, empty choices")
357
+ return {
358
+ "llm_response": "抱歉,AI 服务返回了空响应,请稍后重试。",
359
+ "success": False,
360
+ "error": "Empty response from API after retries",
361
+ "stats": {"total_time": time.time() - start_time},
362
+ "usage": usage_totals,
363
+ }
364
+
290
365
  if response.usage:
291
366
  usage_totals["input_tokens"] += response.usage.prompt_tokens or 0
292
367
  usage_totals["output_tokens"] += response.usage.completion_tokens or 0
@@ -366,7 +441,10 @@ class AgentPipeline:
366
441
  if func_name == "web_tool":
367
442
  tasks_to_run.append(self._execute_web_tool(tool_call_args_list[idx], context))
368
443
  task_indices.append(idx)
369
-
444
+ elif func_name == "js_executor":
445
+ tasks_to_run.append(self._execute_js_tool(tool_call_args_list[idx], context))
446
+ task_indices.append(idx)
447
+
370
448
  # Run all web_tool calls in parallel
371
449
  if tasks_to_run:
372
450
  results = await asyncio.gather(*tasks_to_run, return_exceptions=True)
@@ -584,6 +662,59 @@ class AgentPipeline:
584
662
  "screenshot_count": 0
585
663
  }
586
664
 
665
+ async def _execute_js_tool(self, args: Dict, context: StageContext) -> Dict[str, Any]:
666
+ """执行 JS 代码工具"""
667
+ script = args.get("script", "")
668
+ if not script:
669
+ return {"summary": "JS执行失败: 代码为空", "results": []}
670
+
671
+ if self.send_func:
672
+ try:
673
+ await self.send_func("💻 正在执行JavaScript代码...")
674
+ except: pass
675
+
676
+ logger.info(f"AgentPipeline: Executing JS script: {script[:50]}...")
677
+ result = await self.search_service.execute_script(script)
678
+
679
+ # 格式化结果
680
+ success = result.get("success", False)
681
+ output = result.get("result", None)
682
+ error = result.get("error", None)
683
+ url = result.get("url", "")
684
+ title = result.get("title", "")
685
+
686
+ # Add to context
687
+ context.web_results.append({
688
+ "_id": context.next_id(),
689
+ "_type": "js_result",
690
+ "url": url,
691
+ "title": title or "JS Execution",
692
+ "script": script,
693
+ "output": str(output) if success else str(error),
694
+ "success": success,
695
+ "content": f"Script: {script}\n\nOutput: {output}" if success else f"Error: {error}"
696
+ })
697
+
698
+ if success:
699
+ summary = f"JS执行成功 (返回: {str(output)[:50]}...)"
700
+ return {
701
+ "summary": summary,
702
+ "results": [{"_type": "js_result", "url": url}],
703
+ "screenshot_count": 0,
704
+ "full_output": str(output), # Return full output for LLM
705
+ "success": True
706
+ }
707
+ else:
708
+ return {
709
+ "summary": f"JS执行失败: {str(error)[:50]}",
710
+ "results": [],
711
+ "screenshot_count": 0,
712
+ "full_output": f"JS Execution Failed: {error}",
713
+ "success": False,
714
+ "error": str(error)
715
+ }
716
+
717
+
587
718
  def _collect_filter_urls(self, filters: List, visible: List[Dict]) -> List[str]:
588
719
  """Collect URLs based on filter specifications."""
589
720
  urls = []
@@ -666,22 +797,51 @@ class AgentPipeline:
666
797
  "cost": instruct_cost,
667
798
  })
668
799
 
669
- # 2. Search Stage (搜索)
800
+ # 2. Search Stage (搜索) / Browser JS Stage
670
801
  if session.tool_calls:
671
- # Collect all search descriptions
802
+ # Collect all search descriptions and check for JS executor calls
672
803
  search_descriptions = []
804
+ js_calls = []
805
+
673
806
  for tc, result in zip(session.tool_calls, session.tool_results):
674
- desc = result.get("summary", "")
675
- if desc:
676
- search_descriptions.append(desc)
677
-
678
- stages.append({
679
- "name": "Search",
680
- "model": "",
681
- "provider": "Web",
682
- "description": " → ".join(search_descriptions) if search_descriptions else "Web Search",
683
- "time": session.search_time,
684
- })
807
+ if tc.get("name") == "js_executor":
808
+ # Collect JS execution info
809
+ js_calls.append({
810
+ "script": tc.get("args", {}).get("script", ""),
811
+ "output": result.get("full_output", result.get("summary", "")),
812
+ "url": result.get("results", [{}])[0].get("url", "") if result.get("results") else "",
813
+ "success": result.get("success", True), # Default to True if not present
814
+ "error": result.get("error", "")
815
+ })
816
+ else:
817
+ desc = result.get("summary", "")
818
+ if desc:
819
+ search_descriptions.append(desc)
820
+
821
+ # Add Search stage if there are search calls
822
+ if search_descriptions:
823
+ stages.append({
824
+ "name": "Search",
825
+ "model": "",
826
+ "provider": "Web",
827
+ "description": " → ".join(search_descriptions),
828
+ "time": session.search_time,
829
+ })
830
+
831
+ # Add Browser JS stage for each JS call
832
+ for js_call in js_calls:
833
+ stages.append({
834
+ "name": "browser_js",
835
+ "model": "",
836
+ "provider": "Browser",
837
+ "description": "JavaScript Execution",
838
+ "script": js_call["script"],
839
+ "output": js_call["output"],
840
+ "url": js_call["url"],
841
+ "success": js_call.get("success"),
842
+ "error": js_call.get("error"),
843
+ "time": 0, # JS execution time is included in search_time
844
+ })
685
845
 
686
846
  # 3. Summary Stage (总结)
687
847
  # Calculate remaining tokens after instruct