entari-plugin-hyw 4.0.0rc14__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.

@@ -914,6 +914,49 @@ class ScreenshotService:
914
914
  try: tab.close()
915
915
  except: pass
916
916
 
917
+
918
+ async def execute_script(self, script: str) -> Dict[str, Any]:
919
+ """
920
+ Execute JavaScript in the current active page context.
921
+ This reuses the shared browser instance.
922
+ """
923
+ loop = asyncio.get_running_loop()
924
+ return await loop.run_in_executor(
925
+ self._executor,
926
+ self._execute_script_sync,
927
+ script
928
+ )
929
+
930
+ def _execute_script_sync(self, script: str) -> Dict[str, Any]:
931
+ """Synchronous JS execution."""
932
+ try:
933
+ self._ensure_ready()
934
+ page = self._manager.page
935
+ if not page:
936
+ return {"success": False, "error": "Browser not available"}
937
+
938
+ # Get current active tab or first tab
939
+ # Fix: ChromiumPage object has no attribute 'tabs'
940
+ # We use the page object itself as it represents the active tab controller
941
+ tab = page
942
+ if not tab:
943
+ return {"success": False, "error": "No active tab"}
944
+
945
+ logger.info(f"ScreenshotService: Executing JS on {tab.url}")
946
+
947
+ # Execute JS
948
+ result = tab.run_js(script)
949
+
950
+ return {
951
+ "success": True,
952
+ "result": result,
953
+ "url": tab.url,
954
+ "title": tab.title
955
+ }
956
+ except Exception as e:
957
+ logger.error(f"ScreenshotService: JS execution failed: {e}")
958
+ return {"success": False, "error": str(e)}
959
+
917
960
  async def close(self):
918
961
  self._executor.shutdown(wait=False)
919
962
  logger.info("ScreenshotService: Closed.")
hyw_core/definitions.py CHANGED
@@ -10,17 +10,36 @@ from typing import Dict, Any
10
10
  SUMMARY_REPORT_SP = """# 你是一个总结助手 (Agent), 你的职责是基于搜索工具给出的信息,回答用户的问题或解释用户问题中的关键词。
11
11
  ## 核心原则
12
12
  最小限度使用自身知识, 尽可能使用 web_tool 获取信息.
13
+ 遇到计算、js代码、算法任务, 积极使用 js_executor 工具完成计算任务.
14
+
15
+ ## 抓重点原则
16
+ 搜索结果中往往混杂大量信息,你需要:
17
+ - 主动识别与用户问题最匹配的结果,大胆引用,不要因为信息混在众多结果中就忽略它
18
+ - 即使只有一条结果明确匹配,也要优先使用该结果,而非泛泛而谈
19
+
20
+ ## 图文融合原则
21
+ 当用户同时提供图片和文字时:
22
+ - 先理解用户真正想知道什么(识图?查资料?对比分析?)
23
+ - 图片是"锚点",搜索是"扩展"——围绕图片内容组织搜索信息
24
+ - 行文自然流畅,让图片分析和搜索结果无缝衔接
25
+ - 例如:"图中展示的是 XX(识别结果),这是一款...(搜索扩展)"
13
26
 
14
27
  ## 工具使用指南
15
28
  - 适当时候调用 `refuse_answer`
16
29
 
17
30
  ## 回答格式
31
+ - 字数: 尽可能少, 有多少信息写多少信息, 减少无意义, 足够回答用户问题或解释关键词所需的文字即可
18
32
  - `# ` 大标题约 8-10 个字
19
- - <summary>...</summary> 100 字的概括
33
+ - 必要时可以辅助以 <summary>...</summary> 为格式, 不超过 100 字的概括
20
34
  - 二级标题 + markdown 正文
21
- - 正文使用 [1] 格式引用信息来源, 无需写出源, 系统自动渲染
35
+ - 正文使用 [1] 格式引用信息来源. 如有 js 计算结果, 积极引用. 无需写出源, 系统自动渲染.
22
36
  """
23
37
 
38
+ # Used by SummaryStage for multimodal input with images
39
+ IMAGE_CONTEXT_TEMPLATE = """[System: 图文融合分析指南]
40
+ 用户提供了 {image_count} 张图片,请根据问题类型智能调整回答策略
41
+ 用户问题:"""
42
+
24
43
  def get_refuse_answer_tool() -> Dict[str, Any]:
25
44
  """Tool for refusing to answer inappropriate content."""
26
45
  return {
@@ -45,25 +64,20 @@ def get_web_tool() -> Dict[str, Any]:
45
64
  "type": "function",
46
65
  "function": {
47
66
  "name": "web_tool",
48
- "description": """搜索网页或截图指定URL。用于获取最新信息、查找资料。
67
+ "description": """搜索网页或截图指定URL。用于获取duckduckgo搜索结果或网页内容
49
68
  ## 使用方式
50
69
  网页搜索(大部分问题优先使用此方法):
51
- 直接传入搜索词如 "python async" 会返回搜索结果列表
70
+ 直接传入搜索词如 "python async" 会返回搜索结果列表 搜索词尽可能少且精准, 以利于传统搜索引擎检索
52
71
 
53
72
  网页截图(当用户明确要求截图时使用):
54
73
  传入完整URL如 "https://example.com" 会直接截图该页面
55
-
56
- 网页搜索 + 网页截图(可以预测能直接搜到什么样的结果时使用): (最终截图最多3张)
57
- - 域名过滤: "github=2: python async" → 会搜索 "python async github" 并截图 链接/标题包含 "github" 的前2个结果
58
- - 序号选择: "1,2: minecraft mods" → 会搜索 "minecraft mods" 并截图第1、2个结果
59
- - 多域名: "mcmod=1, github=1: forge mod" → 会搜索 "forge mod mcmod github" 并截图 链接/标题包含 "mcmod" 的前1个结果和 链接/标题包含 "github" 的前1个结果
60
74
  """,
61
75
  "parameters": {
62
76
  "type": "object",
63
77
  "properties": {
64
78
  "query": {
65
79
  "type": "string",
66
- "description": "搜索查询或URL。支持过滤器语法(见描述)"
80
+ "description": "搜索查询或网页获取"
67
81
  }
68
82
  },
69
83
  "required": ["query"]
@@ -72,6 +86,32 @@ def get_web_tool() -> Dict[str, Any]:
72
86
  }
73
87
 
74
88
 
89
+ def get_js_tool() -> Dict[str, Any]:
90
+ """Tool for executing JavaScript in the browser."""
91
+ return {
92
+ "type": "function",
93
+ "function": {
94
+ "name": "js_executor",
95
+ "description": """执行JavaScript代码并返回结果。
96
+ 代码将在当前浏览器页面的上下文中执行。
97
+ 注意:
98
+ 1. 必须使用 `return` 语句返回结果,或者直接作为表达式(如 `1+1`)。
99
+ 2. 严禁使用 `console.log`,其输出无法被捕获,会导致返回 None。
100
+ """,
101
+ "parameters": {
102
+ "type": "object",
103
+ "properties": {
104
+ "script": {
105
+ "type": "string",
106
+ "description": "要执行的JavaScript代码字符串"
107
+ }
108
+ },
109
+ "required": ["script"]
110
+ }
111
+ }
112
+ }
113
+
114
+
75
115
  # =============================================================================
76
116
  # AGENT PROMPTS
77
117
  # =============================================================================
@@ -89,16 +129,21 @@ AGENT_SYSTEM_PROMPT = """# 你是一个智能助手 (Agent), 你的职责是使
89
129
  - 网页搜索: 可以同时调用3次, 其中URL截图消耗较大, 最多同时调用1个
90
130
  - 积极使用 web_tool 获取信息
91
131
  - 搜索时, 关键词保证简单、指向准确、利于传统搜索引擎.
132
+ - 不要尝试通过搜索引擎反推出角色、任务、地点, 搜索引擎没有这个能力
133
+ - 禁止搜索可能导致一切潜在推销广告的内容, 不出现“是什么”、“怎么办”等容易产生广告的内容
134
+ - 禁止搜索任何敏感内容(galgame之类的除外), 禁止搜索政治、成人色情、暴力等内容
92
135
  - 获取页面截图时, 只使用官方性较强的 wiki、官方网站、资源站等等, 不使用第三方转载新闻网站.
93
136
  - 最多可调用2轮工具, 之后必须给出最终回答
94
137
  - 适当时候调用 `refuse_answer`
95
138
  - 对于具体任务, 如果是转述、格式化、翻译等, 请直接给出最终回答, 不再调用工具
139
+ - 遇到计算、js代码、算法任务, 积极使用 js_executor 工具完成计算任务.
96
140
 
97
141
  ## 回答格式
142
+ - 字数: 尽可能少, 有多少获取到的信息、需要解释的内容, 就写多少, 减少无意义输出, 足够完成用户分配给你的任务 / 解释关键词即可.
98
143
  - `# ` 大标题约 8-10 个字
99
- - <summary>...</summary> 100 字的概括
144
+ - 必要时可以辅助以 <summary>...</summary> 为格式, 不超过 100 字的概括
100
145
  - 二级标题 + markdown 正文
101
- - 正文使用 [1] 格式引用信息来源, 无需写出源, 系统自动渲染
146
+ - 正文使用 [1] 格式引用信息来源. 如有 js 计算结果, 积极引用. 无需写出源, 系统自动渲染.
102
147
  """
103
148
 
104
149
 
hyw_core/search.py CHANGED
@@ -160,12 +160,10 @@ class SearchService:
160
160
  service = get_screenshot_service(headless=self._headless)
161
161
  return await service.screenshot_url(url, full_page=full_page)
162
162
 
163
- async def screenshot_with_content(self, url: str, max_content_length: int = 8000) -> Dict[str, Any]:
163
+ async def execute_script(self, script: str) -> Dict[str, Any]:
164
164
  """
165
- Capture screenshot and extract page content.
166
-
167
- Returns:
168
- Dict with screenshot_b64, content (truncated), title, url
165
+ Execute JavaScript in the current page context.
169
166
  """
170
167
  service = get_screenshot_service(headless=self._headless)
171
- return await service.screenshot_with_content(url, max_content_length=max_content_length)
168
+ return await service.execute_script(script)
169
+
@@ -13,7 +13,7 @@ from loguru import logger
13
13
  from openai import AsyncOpenAI
14
14
 
15
15
  from .base import BaseStage, StageContext, StageResult
16
- from ..definitions import SUMMARY_REPORT_SP, get_refuse_answer_tool
16
+ from ..definitions import SUMMARY_REPORT_SP, IMAGE_CONTEXT_TEMPLATE, get_refuse_answer_tool
17
17
 
18
18
 
19
19
  class SummaryStage(BaseStage):
@@ -52,9 +52,9 @@ class SummaryStage(BaseStage):
52
52
  # Build user content
53
53
  user_text = context.user_input or "..."
54
54
  if images:
55
- # Add image context message for multimodal input
56
- image_context = f"[System: The user has provided {len(images)} image(s). Please analyze these images together with the text query to provide a comprehensive response.]"
57
- user_content: List[Dict[str, Any]] = [{"type": "text", "text": f"{image_context}\n\n{user_text}"}]
55
+ # 构建智能图文融合指导
56
+ image_context = IMAGE_CONTEXT_TEMPLATE.format(image_count=len(images))
57
+ user_content: List[Dict[str, Any]] = [{"type": "text", "text": f"{image_context}{user_text}"}]
58
58
  for img_b64 in images:
59
59
  url = f"data:image/jpeg;base64,{img_b64}" if not img_b64.startswith("data:") else img_b64
60
60
  user_content.append({"type": "image_url", "image_url": {"url": url}})
@@ -77,22 +77,49 @@ class SummaryStage(BaseStage):
77
77
 
78
78
  model = model_cfg.model_name or self.config.model_name
79
79
 
80
- try:
81
- response = await client.chat.completions.create(
82
- model=model,
83
- messages=messages,
84
- temperature=self.config.temperature,
85
-
86
- extra_body=getattr(self.config, "summary_extra_body", None),
87
- tools=[refuse_tool],
88
- tool_choice="auto",
89
- )
90
- except Exception as e:
91
- logger.error(f"SummaryStage LLM error: {e}")
80
+ # Retry logic for API calls
81
+ max_retries = 2
82
+ response = None
83
+ last_error = None
84
+
85
+ for attempt in range(max_retries + 1):
86
+ try:
87
+ response = await client.chat.completions.create(
88
+ model=model,
89
+ messages=messages,
90
+ temperature=self.config.temperature,
91
+ extra_body=getattr(self.config, "summary_extra_body", None),
92
+ tools=[refuse_tool],
93
+ tool_choice="auto",
94
+ )
95
+
96
+ # Check for valid response
97
+ if response.choices:
98
+ break # Success, exit retry loop
99
+
100
+ # Empty choices - log and retry
101
+ logger.warning(f"SummaryStage: Empty choices response (attempt {attempt + 1}/{max_retries + 1}). Response: {response}")
102
+ last_error = "Invalid API response: no choices returned"
103
+
104
+ if attempt < max_retries:
105
+ import asyncio
106
+ await asyncio.sleep(1) # Wait 1 second before retry
107
+
108
+ except Exception as e:
109
+ logger.error(f"SummaryStage LLM error (attempt {attempt + 1}/{max_retries + 1}): {e}")
110
+ last_error = str(e)
111
+
112
+ if attempt < max_retries:
113
+ import asyncio
114
+ await asyncio.sleep(1) # Wait 1 second before retry
115
+
116
+ # Check if we got a valid response after retries
117
+ if not response or not response.choices:
118
+ logger.error(f"SummaryStage: All retries exhausted. Last error: {last_error}")
92
119
  return StageResult(
93
120
  success=False,
94
- error=str(e),
95
- data={"content": f"Error generating summary: {e}"}
121
+ error=last_error or "Invalid API response after retries",
122
+ data={"content": f"Error: {last_error or 'API returned invalid response after retries'}"}
96
123
  )
97
124
 
98
125
  usage = {"input_tokens": 0, "output_tokens": 0}