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.
- entari_plugin_hyw/__init__.py +149 -367
- {entari_plugin_hyw-4.0.0rc13.dist-info → entari_plugin_hyw-4.0.0rc15.dist-info}/METADATA +1 -1
- {entari_plugin_hyw-4.0.0rc13.dist-info → entari_plugin_hyw-4.0.0rc15.dist-info}/RECORD +11 -12
- {entari_plugin_hyw-4.0.0rc13.dist-info → entari_plugin_hyw-4.0.0rc15.dist-info}/WHEEL +1 -1
- hyw_core/agent.py +204 -44
- hyw_core/browser_control/assets/card-dist/index.html +35 -31
- hyw_core/browser_control/service.py +43 -0
- hyw_core/definitions.py +57 -12
- hyw_core/search.py +4 -6
- hyw_core/stages/summary.py +45 -18
- entari_plugin_hyw/Untitled-1 +0 -1865
- {entari_plugin_hyw-4.0.0rc13.dist-info → entari_plugin_hyw-4.0.0rc15.dist-info}/top_level.txt +0 -0
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
|
-
|
|
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 =
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
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
|