entari-plugin-hyw 4.0.0rc7__py3-none-any.whl → 4.0.0rc9__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.
Files changed (114) hide show
  1. entari_plugin_hyw/Untitled-1 +1865 -0
  2. entari_plugin_hyw/__init__.py +726 -394
  3. entari_plugin_hyw/history.py +26 -13
  4. entari_plugin_hyw/misc.py +3 -0
  5. entari_plugin_hyw/search_cache.py +154 -0
  6. {entari_plugin_hyw-4.0.0rc7.dist-info → entari_plugin_hyw-4.0.0rc9.dist-info}/METADATA +3 -1
  7. entari_plugin_hyw-4.0.0rc9.dist-info/RECORD +68 -0
  8. {entari_plugin_hyw-4.0.0rc7.dist-info → entari_plugin_hyw-4.0.0rc9.dist-info}/WHEEL +1 -1
  9. {entari_plugin_hyw-4.0.0rc7.dist-info → entari_plugin_hyw-4.0.0rc9.dist-info}/top_level.txt +1 -0
  10. hyw_core/__init__.py +94 -0
  11. hyw_core/browser_control/__init__.py +65 -0
  12. hyw_core/browser_control/assets/card-dist/index.html +409 -0
  13. hyw_core/browser_control/assets/index.html +5691 -0
  14. hyw_core/browser_control/engines/__init__.py +17 -0
  15. {entari_plugin_hyw/browser → hyw_core/browser_control}/engines/duckduckgo.py +42 -8
  16. {entari_plugin_hyw/browser → hyw_core/browser_control}/engines/google.py +1 -1
  17. {entari_plugin_hyw/browser → hyw_core/browser_control}/manager.py +15 -8
  18. entari_plugin_hyw/render_vue.py → hyw_core/browser_control/renderer.py +29 -14
  19. {entari_plugin_hyw/browser → hyw_core/browser_control}/service.py +340 -112
  20. hyw_core/config.py +154 -0
  21. hyw_core/core.py +322 -0
  22. hyw_core/definitions.py +83 -0
  23. entari_plugin_hyw/modular_pipeline.py → hyw_core/pipeline.py +121 -97
  24. {entari_plugin_hyw → hyw_core}/search.py +19 -14
  25. hyw_core/stages/__init__.py +21 -0
  26. entari_plugin_hyw/stage_base.py → hyw_core/stages/base.py +2 -2
  27. entari_plugin_hyw/stage_summary.py → hyw_core/stages/summary.py +34 -11
  28. entari_plugin_hyw/assets/card-dist/index.html +0 -387
  29. entari_plugin_hyw/browser/__init__.py +0 -10
  30. entari_plugin_hyw/browser/engines/bing.py +0 -95
  31. entari_plugin_hyw/card-ui/.gitignore +0 -24
  32. entari_plugin_hyw/card-ui/README.md +0 -5
  33. entari_plugin_hyw/card-ui/index.html +0 -16
  34. entari_plugin_hyw/card-ui/package-lock.json +0 -2342
  35. entari_plugin_hyw/card-ui/package.json +0 -31
  36. entari_plugin_hyw/card-ui/public/logos/anthropic.svg +0 -1
  37. entari_plugin_hyw/card-ui/public/logos/cerebras.svg +0 -9
  38. entari_plugin_hyw/card-ui/public/logos/deepseek.png +0 -0
  39. entari_plugin_hyw/card-ui/public/logos/gemini.svg +0 -1
  40. entari_plugin_hyw/card-ui/public/logos/google.svg +0 -1
  41. entari_plugin_hyw/card-ui/public/logos/grok.png +0 -0
  42. entari_plugin_hyw/card-ui/public/logos/huggingface.png +0 -0
  43. entari_plugin_hyw/card-ui/public/logos/microsoft.svg +0 -15
  44. entari_plugin_hyw/card-ui/public/logos/minimax.png +0 -0
  45. entari_plugin_hyw/card-ui/public/logos/mistral.png +0 -0
  46. entari_plugin_hyw/card-ui/public/logos/nvida.png +0 -0
  47. entari_plugin_hyw/card-ui/public/logos/openai.svg +0 -1
  48. entari_plugin_hyw/card-ui/public/logos/openrouter.png +0 -0
  49. entari_plugin_hyw/card-ui/public/logos/perplexity.svg +0 -24
  50. entari_plugin_hyw/card-ui/public/logos/qwen.png +0 -0
  51. entari_plugin_hyw/card-ui/public/logos/xai.png +0 -0
  52. entari_plugin_hyw/card-ui/public/logos/xiaomi.png +0 -0
  53. entari_plugin_hyw/card-ui/public/logos/zai.png +0 -0
  54. entari_plugin_hyw/card-ui/public/vite.svg +0 -1
  55. entari_plugin_hyw/card-ui/src/App.vue +0 -787
  56. entari_plugin_hyw/card-ui/src/assets/vue.svg +0 -1
  57. entari_plugin_hyw/card-ui/src/components/HelloWorld.vue +0 -41
  58. entari_plugin_hyw/card-ui/src/components/MarkdownContent.vue +0 -382
  59. entari_plugin_hyw/card-ui/src/components/SectionCard.vue +0 -41
  60. entari_plugin_hyw/card-ui/src/components/StageCard.vue +0 -240
  61. entari_plugin_hyw/card-ui/src/main.ts +0 -5
  62. entari_plugin_hyw/card-ui/src/style.css +0 -29
  63. entari_plugin_hyw/card-ui/src/test_regex.js +0 -103
  64. entari_plugin_hyw/card-ui/src/types.ts +0 -61
  65. entari_plugin_hyw/card-ui/tsconfig.app.json +0 -16
  66. entari_plugin_hyw/card-ui/tsconfig.json +0 -7
  67. entari_plugin_hyw/card-ui/tsconfig.node.json +0 -26
  68. entari_plugin_hyw/card-ui/vite.config.ts +0 -16
  69. entari_plugin_hyw/definitions.py +0 -174
  70. entari_plugin_hyw/stage_instruct.py +0 -355
  71. entari_plugin_hyw/stage_instruct_deepsearch.py +0 -104
  72. entari_plugin_hyw/stage_vision.py +0 -113
  73. entari_plugin_hyw-4.0.0rc7.dist-info/RECORD +0 -102
  74. {entari_plugin_hyw → hyw_core/browser_control}/assets/card-dist/logos/anthropic.svg +0 -0
  75. {entari_plugin_hyw → hyw_core/browser_control}/assets/card-dist/logos/cerebras.svg +0 -0
  76. {entari_plugin_hyw → hyw_core/browser_control}/assets/card-dist/logos/deepseek.png +0 -0
  77. {entari_plugin_hyw → hyw_core/browser_control}/assets/card-dist/logos/gemini.svg +0 -0
  78. {entari_plugin_hyw → hyw_core/browser_control}/assets/card-dist/logos/google.svg +0 -0
  79. {entari_plugin_hyw → hyw_core/browser_control}/assets/card-dist/logos/grok.png +0 -0
  80. {entari_plugin_hyw → hyw_core/browser_control}/assets/card-dist/logos/huggingface.png +0 -0
  81. {entari_plugin_hyw → hyw_core/browser_control}/assets/card-dist/logos/microsoft.svg +0 -0
  82. {entari_plugin_hyw → hyw_core/browser_control}/assets/card-dist/logos/minimax.png +0 -0
  83. {entari_plugin_hyw → hyw_core/browser_control}/assets/card-dist/logos/mistral.png +0 -0
  84. {entari_plugin_hyw → hyw_core/browser_control}/assets/card-dist/logos/nvida.png +0 -0
  85. {entari_plugin_hyw → hyw_core/browser_control}/assets/card-dist/logos/openai.svg +0 -0
  86. {entari_plugin_hyw → hyw_core/browser_control}/assets/card-dist/logos/openrouter.png +0 -0
  87. {entari_plugin_hyw → hyw_core/browser_control}/assets/card-dist/logos/perplexity.svg +0 -0
  88. {entari_plugin_hyw → hyw_core/browser_control}/assets/card-dist/logos/qwen.png +0 -0
  89. {entari_plugin_hyw → hyw_core/browser_control}/assets/card-dist/logos/xai.png +0 -0
  90. {entari_plugin_hyw → hyw_core/browser_control}/assets/card-dist/logos/xiaomi.png +0 -0
  91. {entari_plugin_hyw → hyw_core/browser_control}/assets/card-dist/logos/zai.png +0 -0
  92. {entari_plugin_hyw → hyw_core/browser_control}/assets/card-dist/vite.svg +0 -0
  93. {entari_plugin_hyw/assets/icon → hyw_core/browser_control/assets/logos}/anthropic.svg +0 -0
  94. {entari_plugin_hyw/assets/icon → hyw_core/browser_control/assets/logos}/cerebras.svg +0 -0
  95. {entari_plugin_hyw/assets/icon → hyw_core/browser_control/assets/logos}/deepseek.png +0 -0
  96. {entari_plugin_hyw/assets/icon → hyw_core/browser_control/assets/logos}/gemini.svg +0 -0
  97. {entari_plugin_hyw/assets/icon → hyw_core/browser_control/assets/logos}/google.svg +0 -0
  98. {entari_plugin_hyw/assets/icon → hyw_core/browser_control/assets/logos}/grok.png +0 -0
  99. {entari_plugin_hyw/assets/icon → hyw_core/browser_control/assets/logos}/huggingface.png +0 -0
  100. {entari_plugin_hyw/assets/icon → hyw_core/browser_control/assets/logos}/microsoft.svg +0 -0
  101. {entari_plugin_hyw/assets/icon → hyw_core/browser_control/assets/logos}/minimax.png +0 -0
  102. {entari_plugin_hyw/assets/icon → hyw_core/browser_control/assets/logos}/mistral.png +0 -0
  103. {entari_plugin_hyw/assets/icon → hyw_core/browser_control/assets/logos}/nvida.png +0 -0
  104. {entari_plugin_hyw/assets/icon → hyw_core/browser_control/assets/logos}/openai.svg +0 -0
  105. {entari_plugin_hyw/assets/icon → hyw_core/browser_control/assets/logos}/openrouter.png +0 -0
  106. {entari_plugin_hyw/assets/icon → hyw_core/browser_control/assets/logos}/perplexity.svg +0 -0
  107. {entari_plugin_hyw/assets/icon → hyw_core/browser_control/assets/logos}/qwen.png +0 -0
  108. {entari_plugin_hyw/assets/icon → hyw_core/browser_control/assets/logos}/xai.png +0 -0
  109. {entari_plugin_hyw/assets/icon → hyw_core/browser_control/assets/logos}/xiaomi.png +0 -0
  110. {entari_plugin_hyw/assets/icon → hyw_core/browser_control/assets/logos}/zai.png +0 -0
  111. {entari_plugin_hyw/browser → hyw_core/browser_control}/engines/base.py +0 -0
  112. {entari_plugin_hyw/browser → hyw_core/browser_control}/engines/default.py +0 -0
  113. {entari_plugin_hyw/browser → hyw_core/browser_control}/landing.html +0 -0
  114. {entari_plugin_hyw → hyw_core}/image_cache.py +0 -0
@@ -1,174 +0,0 @@
1
- """
2
- Centralized Definitions
3
-
4
- All global prompts and tool definitions for the pipeline stages.
5
- """
6
-
7
- from typing import Dict, Any
8
-
9
- # =============================================================================
10
- # PROMPTS
11
- # =============================================================================
12
-
13
- INSTRUCT_SP = """你是一个智能 INSTRUCT 助手。
14
- 你的目标是立即, 为用户的查询制定调研计划并执行
15
- > 尽量完成所有搜索和获取, 需要的时候可以并发执行多个同种或不同种工具
16
-
17
- - 用户消息包含典型名词、可能的专有名词组合: 调用 `web_search` 工具(最大3, 推荐1)。
18
- - 提炼消息中出独立的关键词搜索关键词本身, 不添加任何其他辅助搜索词, 搜索效果最好
19
- - 不混合搜索词, 每次只针对搜索一个关键词, 获取其目的方向的最新数据
20
- - 用户消息中出现链接时, 调用 `crawl_page`(最大3) 获取网页全文
21
- - 以下情况调用 `refuse_answer`。
22
- - 明显的鉴证、国家领导人、历史敏感事件隐喻
23
- - 过于露骨的r18+、r18g内容(但不包括 galgame等acg向, 除非其过于违规)
24
-
25
- ## 内容审查
26
- 以下情况调用 `refuse_answer`:
27
- - 内容涉及隐喻政治事件任务
28
- - r18+、r18g(但不包含正常galgame、科普等)
29
-
30
- ## 决定流程
31
- 调用 `set_mode` 决定使用 `fast` 还是 `deepsearch`:
32
- - 只要用户不明确研究就用 `fast`
33
- - 用户提到: 深度搜索、深度研究、deepsearch、整理关于...的资料, 这几种情况时, 选择 `deepsearch`
34
-
35
- ## 重要规则 (CRITICAL RULES):
36
- - 禁止输出任何文本回复:你必须且只能通过工具调用来行动。
37
- - 如果没有工具调用,流程将自动结束。
38
-
39
- ## now
40
- 请快速给出回复.
41
- """
42
-
43
- INSTRUCT_DEEPSEARCH_SP = """你是一个智能 INSTRUCT_DEEPSEARCH 审查助手, 你需要对 INSTRUCT 的输出进行多次信息补充直到信息足够、或达到次数上限(3次)
44
-
45
- - 推荐使用 `crawl_page` 工具查看官方网站、wiki网站(但不推荐维基百科)、权威网站
46
- - crawl_page 永远不使用国内垃圾网站例如 csdn、知乎、等重复搬运二手信息的网站
47
-
48
- ## 重要规则 (CRITICAL RULES):
49
- - 禁止输出任何文本回复:你必须且只能通过工具调用来行动。
50
- - 如果没有必要进一步操作,请不要输出任何内容(空回复),流程将自动进入下一阶段。
51
- """
52
-
53
-
54
- SUMMARY_REPORT_SP = """# 你是一个信息整合专家 (Summary Agent).
55
- 你需要根据用户问题、搜索结果和网页详细内容,生成最终的回答.
56
- 如果用户发送你好或空内容回应你好即可.
57
-
58
- ## 过程要求
59
- - 用户要求的回复语言(包裹在 language 标签内)
60
- ```language
61
- {language}
62
- ```
63
- - 字数控制在600字以内, 百科式风格, 语言严谨不啰嗦.
64
- - 视觉信息: 输入中如果包含自动获取的网页截图,请分析图片中的信息作为参考.
65
- - 注意分辨搜索内容是否和用户问题有直接关系, 避免盲目相信混为一谈.
66
- - 正文格式:
67
- - 先给出一个 `# `大标题约 8-10 个字, 不要有多余废话, 不要直接回答用户的提问.
68
- - 然后紧接着给出一个 <summary>...</summary>, 除了给出一个约 100 字的纯文本简介, 介绍本次输出的长文的清晰、重点概括.
69
- - 随后开始详细二级标题 + markdown 正文, 语言描绘格式丰富多样, 简洁准确可信.
70
- - 请不要给出过长的代码、表格列数等, 只讲重点和准确的数据.
71
- - 不支持渲染: 链接, 图片链接, mermaid
72
- - 支持渲染: 公式, 代码高亮, 只在需要的时候给出.
73
- - 图片链接、链接框架会自动渲染出, 你无需显式给出.
74
- - 引用:
75
- > 重要: 所有正文内容必须基于实际信息, 保证百分百真实度
76
- - 信息来源已按获取顺序编号为 [1], [2], [3]... 但不超过 9 个引用.
77
- - 优先引用优质 fetch 抓取的页面的资源, 但如果抓取到需要登录、需要验证码、需要跳转到其他网站等无法获取的资源, 则不引用此资源
78
- - 正文中直接使用 [1] 格式引用, 只引用对回答有帮助的来源, 只使用官方性较强的 wiki、官方网站、资源站等等, 不使用第三方转载新闻网站.
79
- - 无需给出参考文献列表, 系统会自动生成
80
- """
81
-
82
-
83
- # =============================================================================
84
- # VISION DESCRIPTION PROMPT
85
- # =============================================================================
86
-
87
- VISION_DESCRIPTION_SP = """你是一个图像描述专家。
88
- 根据用户发送的图片和文字,快速描述图片中的内容。
89
-
90
- 要求:
91
- - 客观描述图片中的主要元素、场景、人物、文字等
92
- - 如果图片包含文字,请完整转录
93
- - 如果用户有具体问题,围绕问题描述相关细节
94
- - 描述应该简洁但信息丰富,控制在 300 字以内
95
- - 使用用户的语言回复
96
- """
97
-
98
-
99
- # =============================================================================
100
- # TOOL DEFINITIONS
101
- # =============================================================================
102
-
103
- def get_refuse_answer_tool() -> Dict[str, Any]:
104
- """Tool for refusing to answer inappropriate content."""
105
- return {
106
- "type": "function",
107
- "function": {
108
- "name": "refuse_answer",
109
- "description": "违规内容拒绝回答",
110
- "parameters": {
111
- "type": "object",
112
- "properties": {
113
- "reason": {"type": "string", "description": "拒绝回答的原因(展示给用户)"},
114
- },
115
- "required": ["reason"],
116
- },
117
- },
118
- }
119
-
120
-
121
- def get_web_search_tool() -> Dict[str, Any]:
122
- """Tool for searching the web."""
123
- return {
124
- "type": "function",
125
- "function": {
126
- "name": "web_search",
127
- "description": "网络搜索, 只容许输入正常的字符串查询, 禁止高级搜索",
128
- "parameters": {
129
- "type": "object",
130
- "properties": {"query": {"type": "string"}},
131
- "required": ["query"],
132
- },
133
- },
134
- }
135
-
136
-
137
- def get_crawl_page_tool() -> Dict[str, Any]:
138
- """Tool for crawling a web page."""
139
- return {
140
- "type": "function",
141
- "function": {
142
- "name": "crawl_page",
143
- "description": "抓取网页并返回 Markdown 文本 / 网页截图",
144
- "parameters": {
145
- "type": "object",
146
- "properties": {
147
- "url": {"type": "string"},
148
- },
149
- "required": ["url"],
150
- },
151
- },
152
- }
153
-
154
-
155
- def get_set_mode_tool() -> Dict[str, Any]:
156
- """Tool for setting the pipeline mode (fast or deepsearch)."""
157
- return {
158
- "type": "function",
159
- "function": {
160
- "name": "set_mode",
161
- "description": "设置本次查询的处理模式",
162
- "parameters": {
163
- "type": "object",
164
- "properties": {
165
- "mode": {
166
- "type": "string",
167
- "enum": ["fast", "deepsearch"],
168
- "description": "fast=快速回答 / deepsearch=深度研究"
169
- },
170
- },
171
- "required": ["mode"],
172
- },
173
- },
174
- }
@@ -1,355 +0,0 @@
1
- """
2
- Instruct Stage
3
-
4
- Handles initial task planning and search generation.
5
- Analyze user query and execute initial searches.
6
- """
7
-
8
- import json
9
- import time
10
- import asyncio
11
- from typing import Any, Dict, List, Optional, Tuple, Callable, Awaitable
12
- from loguru import logger
13
- from openai import AsyncOpenAI
14
-
15
- from .stage_base import BaseStage, StageContext, StageResult
16
- from .definitions import (
17
- get_refuse_answer_tool,
18
- get_web_search_tool,
19
- get_crawl_page_tool,
20
- get_set_mode_tool,
21
- INSTRUCT_SP
22
- )
23
-
24
- class InstructStage(BaseStage):
25
- @property
26
- def name(self) -> str:
27
- return "Instruct"
28
-
29
- def __init__(self, config: Any, search_service: Any, client: AsyncOpenAI, send_func: Optional[Callable[[str], Awaitable[None]]] = None):
30
- super().__init__(config, search_service, client)
31
- self.send_func = send_func
32
-
33
- self.refuse_answer_tool = get_refuse_answer_tool()
34
- self.web_search_tool = get_web_search_tool()
35
- self.crawl_page_tool = get_crawl_page_tool()
36
- self.set_mode_tool = get_set_mode_tool()
37
-
38
- async def execute(self, context: StageContext) -> StageResult:
39
- start_time = time.time()
40
-
41
- # --- Round 1: Initial Discovery ---
42
- logger.info("Instruct: Starting Round 1 (Initial Discovery)")
43
-
44
- # Build Round 1 User Message
45
- r1_user_content = self._build_user_message(context)
46
- r1_messages = [
47
- {"role": "system", "content": INSTRUCT_SP},
48
- {"role": "user", "content": r1_user_content}
49
- ]
50
-
51
- # Execute Round 1 LLM
52
- r1_response, r1_usage, r1_tool_calls, r1_content = await self._call_llm(
53
- messages=r1_messages,
54
- tools=[self.refuse_answer_tool, self.web_search_tool, self.crawl_page_tool, self.set_mode_tool],
55
- tool_choice="auto"
56
- )
57
-
58
- if context.should_refuse:
59
- # If refused in Round 1, stop here
60
- return self._build_result(start_time, r1_usage, r1_content, len(r1_tool_calls or []))
61
-
62
- # Execute Round 1 Tools
63
- r1_tool_outputs = []
64
- if r1_tool_calls:
65
- r1_tool_outputs = await self._process_tool_calls(context, r1_tool_calls)
66
-
67
- # --- Context Assembly for Round 2 ---
68
-
69
- # Summarize Round 1 actions for context
70
- r1_summary_text = "## Round 1 Execution Summary\n"
71
- if r1_content:
72
- r1_summary_text += f"Thought: {r1_content}\n"
73
-
74
- if r1_tool_outputs:
75
- r1_summary_text += "Tools Executed & Results:\n"
76
- for output in r1_tool_outputs:
77
- # content here is the tool output (e.g. search results text or crawl preview)
78
- r1_summary_text += f"- Action: {output['name']}\n"
79
- r1_summary_text += f" Result: {output['content']}\n"
80
- else:
81
- r1_summary_text += "No tools were executed in Round 1.\n"
82
-
83
- r2_context_str = f"""User Query: {context.user_input}
84
-
85
- {r1_summary_text}
86
- """
87
- # Save to context for next stage
88
- context.review_context = r2_context_str
89
-
90
- # Update instruct_history for logging/record purposes
91
- context.instruct_history.append({
92
- "role": "assistant",
93
- "content": f"[Round 1 Thought]: {r1_content}\n[Round 1 Actions]: {len(r1_tool_outputs)} tools"
94
- })
95
-
96
- return self._build_result(start_time, r1_usage, r1_content, len(r1_tool_calls or []))
97
-
98
- def _build_user_message(self, context: StageContext) -> Any:
99
- text_prompt = f"User Query: {context.user_input}"
100
- if context.images:
101
- user_content: List[Dict[str, Any]] = [{"type": "text", "text": text_prompt}]
102
- for img_b64 in context.images:
103
- url = f"data:image/jpeg;base64,{img_b64}" if not img_b64.startswith("data:") else img_b64
104
- user_content.append({"type": "image_url", "image_url": {"url": url}})
105
- return user_content
106
- return text_prompt
107
-
108
- async def _call_llm(self, messages, tools, tool_choice="auto"):
109
- model_cfg = self.config.get_model_config("instruct")
110
- client = self._client_for(
111
- api_key=model_cfg.get("api_key"),
112
- base_url=model_cfg.get("base_url")
113
- )
114
- model = model_cfg.get("model_name") or self.config.model_name
115
-
116
- try:
117
- logger.info(f"Instruct: Sending LLM request to {model}...")
118
- response = await client.chat.completions.create(
119
- model=model,
120
- messages=messages,
121
- tools=tools,
122
- tool_choice=tool_choice,
123
- temperature=self.config.temperature,
124
- extra_body=model_cfg.get("extra_body"),
125
- )
126
- except Exception as e:
127
- logger.error(f"InstructStage LLM Error: {e}")
128
- raise e
129
-
130
- usage = {"input_tokens": 0, "output_tokens": 0}
131
- if hasattr(response, "usage") and response.usage:
132
- usage["input_tokens"] = getattr(response.usage, "prompt_tokens", 0) or 0
133
- usage["output_tokens"] = getattr(response.usage, "completion_tokens", 0) or 0
134
-
135
- message = response.choices[0].message
136
- content = message.content or ""
137
- tool_calls = message.tool_calls
138
-
139
- if content:
140
- logger.debug(f"Instruct: Agent Thought -> {content[:100]}...")
141
-
142
- return response, usage, tool_calls, content
143
-
144
- async def _process_tool_calls(self, context: StageContext, tool_calls: List[Any]) -> List[Dict[str, Any]]:
145
- """
146
- Executes tool calls and returns a list of outputs for context building.
147
- Updates context.web_results globally.
148
- """
149
- pending_crawls = [] # List of (url, tool_call_id)
150
- pending_searches = [] # List of (query, tool_call_id)
151
-
152
- results_for_context = []
153
-
154
- for tc in tool_calls:
155
- name = tc.function.name
156
- tc_id = tc.id
157
- try:
158
- args = json.loads(tc.function.arguments)
159
- except json.JSONDecodeError:
160
- results_for_context.append({
161
- "id": tc_id, "name": name, "content": "Error: Invalid JSON arguments"
162
- })
163
- continue
164
-
165
- if name == "refuse_answer":
166
- reason = args.get("reason", "Refused")
167
- logger.warning(f"Instruct: Model Refused Answer. Reason: {reason}")
168
- context.should_refuse = True
169
- context.refuse_reason = reason
170
- results_for_context.append({
171
- "id": tc_id, "name": name, "content": f"Refused: {reason}"
172
- })
173
-
174
- elif name == "web_search":
175
- query = args.get("query")
176
- if query:
177
- logger.info(f"Instruct: Planned search query -> '{query}'")
178
- pending_searches.append((query, tc_id))
179
-
180
- elif name == "crawl_page":
181
- url = args.get("url")
182
- if url:
183
- logger.info(f"Instruct: Planned page crawl -> {url}")
184
- pending_crawls.append((url, tc_id))
185
-
186
- elif name == "set_mode":
187
- mode = args.get("mode", "fast")
188
- if mode in ("fast", "deepsearch"):
189
- context.selected_mode = mode
190
- logger.info(f"Instruct: Mode set to '{mode}'")
191
-
192
- # Notify immediately if deepsearch
193
- if mode == "deepsearch" and self.send_func:
194
- try:
195
- await self.send_func("🔍 正在进行深度研究,可能需要一些时间,请耐心等待...")
196
- except Exception as e:
197
- logger.warning(f"Instruct: Failed to send notification: {e}")
198
-
199
- results_for_context.append({
200
- "id": tc_id, "name": name, "content": f"Mode set to: {mode}"
201
- })
202
- else:
203
- logger.warning(f"Instruct: Invalid mode '{mode}', defaulting to 'fast'")
204
- context.selected_mode = "fast"
205
-
206
- # Execute Batches
207
-
208
- # 1. Crawls
209
- if pending_crawls:
210
- urls = [u for u, _ in pending_crawls]
211
- logger.info(f"Instruct: Executing {len(urls)} crawls via batch...")
212
-
213
- # Start fetch
214
- fetch_task = asyncio.create_task(self.search_service.fetch_pages_batch(urls))
215
-
216
- # Use image capability from context to determine content mode
217
- is_image_mode = getattr(context, "image_input_supported", True)
218
- tab_ids = []
219
- if is_image_mode:
220
- from .render_vue import get_content_renderer
221
- renderer = await get_content_renderer()
222
- loop = asyncio.get_running_loop()
223
- tab_tasks = [
224
- loop.run_in_executor(renderer._executor, renderer._prepare_tab_sync)
225
- for _ in urls
226
- ]
227
- tab_ids = await asyncio.gather(*tab_tasks, return_exceptions=True)
228
- logger.debug(f"Instruct: Prepared {len(tab_ids)} tabs: {tab_ids}")
229
-
230
- crawl_results_list = await fetch_task
231
-
232
- if is_image_mode and tab_ids:
233
- theme_color = getattr(self.config, "theme_color", "#ef4444")
234
- render_tasks = []
235
- valid_pairs = []
236
- MAX_CHARS = 3000
237
- for i, (page_data, tab_id) in enumerate(zip(crawl_results_list, tab_ids)):
238
- if isinstance(tab_id, Exception):
239
- logger.warning(f"Instruct: Skip rendering page {i} due to tab error: {tab_id}")
240
- continue
241
-
242
- # Truncate content to avoid excessive size
243
- content = page_data.get("content", "")
244
- if len(content) > MAX_CHARS:
245
- content = content[:MAX_CHARS] + "\n\n...(content truncated for length)..."
246
- page_data["content"] = content
247
-
248
- if not content:
249
- logger.warning(f"Instruct: Skip rendering page {i} due to empty content")
250
- continue
251
-
252
- valid_pairs.append((i, page_data))
253
- render_tasks.append(
254
- loop.run_in_executor(
255
- renderer._executor,
256
- renderer._render_page_to_b64_sync,
257
- {"title": page_data.get("title", "Page"), "content": content},
258
- tab_id,
259
- theme_color
260
- )
261
- )
262
-
263
- if render_tasks:
264
- logger.debug(f"Instruct: Parallel rendering {len(render_tasks)} pages...")
265
- screenshots = await asyncio.gather(*render_tasks, return_exceptions=True)
266
- logger.debug(f"Instruct: Parallel rendering finished. Results count: {len(screenshots)}")
267
- for j, (orig_idx, page_data) in enumerate(valid_pairs):
268
- if j < len(screenshots) and not isinstance(screenshots[j], Exception):
269
- crawl_results_list[orig_idx]["screenshot_b64"] = screenshots[j]
270
-
271
- for i, (url, tc_id) in enumerate(pending_crawls):
272
- page_data = crawl_results_list[i]
273
- title = page_data.get("title", "Unknown")
274
-
275
- # Update global context
276
- page_item = {
277
- "_id": context.next_id(),
278
- "_type": "page",
279
- "title": page_data.get("title", "Page"),
280
- "url": page_data.get("url", url),
281
- "content": page_data.get("content", ""),
282
- "is_crawled": True,
283
- }
284
- if page_data.get("screenshot_b64"):
285
- page_item["screenshot_b64"] = page_data["screenshot_b64"]
286
- if page_data.get("raw_screenshot_b64"):
287
- page_item["raw_screenshot_b64"] = page_data["raw_screenshot_b64"]
288
- if page_data.get("images"):
289
- page_item["images"] = page_data["images"]
290
-
291
- context.web_results.append(page_item)
292
-
293
- # Output for Context Assembly
294
- content_preview = page_data.get("content", "")[:500]
295
- results_for_context.append({
296
- "id": tc_id,
297
- "name": "crawl_page",
298
- "content": f"Crawled '{title}' ({url}):\n{content_preview}..."
299
- })
300
-
301
- # 2. Searches
302
- if pending_searches:
303
- queries = [q for q, _ in pending_searches]
304
- logger.info(f"Instruct: Executing {len(queries)} searches via batch...")
305
-
306
- search_results_list = await self.search_service.search_batch(queries)
307
-
308
- for i, (query, tc_id) in enumerate(pending_searches):
309
- web_results = search_results_list[i]
310
- visible_results = [r for r in web_results if not r.get("_hidden")]
311
-
312
- # Update global context
313
- total_images = sum(len(item.get("images", []) or []) for item in web_results)
314
- logger.debug(f"Instruct: Search '{query}' returned {len(web_results)} items with {total_images} images total")
315
- for item in web_results:
316
- item["_id"] = context.next_id()
317
- if "type" in item:
318
- item["_type"] = item["type"]
319
- elif "_type" not in item:
320
- item["_type"] = "search"
321
- item["query"] = query
322
- context.web_results.append(item)
323
-
324
- # Output for Context Assembly
325
- summary = f"Found {len(visible_results)} results for '{query}':\n"
326
- for r in visible_results[:5]:
327
- summary += f"- {r.get('title')} ({r.get('url')}): {(r.get('content') or '')[:100]}...\n"
328
-
329
- results_for_context.append({
330
- "id": tc_id,
331
- "name": "web_search",
332
- "content": summary
333
- })
334
-
335
- return results_for_context
336
-
337
- def _build_result(self, start_time, usage, content, tool_calls_count):
338
- model_cfg = self.config.get_model_config("instruct")
339
- model = model_cfg.get("model_name") or self.config.model_name
340
-
341
- trace = {
342
- "stage": "Instruct",
343
- "model": model,
344
- "usage": usage,
345
- "output": content,
346
- "tool_calls": tool_calls_count,
347
- "time": time.time() - start_time,
348
- }
349
-
350
- return StageResult(
351
- success=True,
352
- data={"reasoning": content},
353
- usage=usage,
354
- trace=trace
355
- )
@@ -1,104 +0,0 @@
1
- """
2
- Instruct Deepsearch Stage
3
-
4
- Handles the deepsearch loop: Supplement information until sufficient or max iterations reached.
5
- Inherits from InstructStage to reuse tool execution logic.
6
- """
7
-
8
- import time
9
- from typing import Any, List
10
- from loguru import logger
11
- from openai import AsyncOpenAI
12
-
13
- from .stage_base import StageContext, StageResult
14
- from .stage_instruct import InstructStage
15
- from .definitions import INSTRUCT_DEEPSEARCH_SP
16
-
17
- class InstructDeepsearchStage(InstructStage):
18
- @property
19
- def name(self) -> str:
20
- return "Instruct Deepsearch"
21
-
22
- def __init__(self, config: Any, search_service: Any, client: AsyncOpenAI):
23
- super().__init__(config, search_service, client)
24
- # Inherits tools from InstructStage (web_search, crawl_page)
25
-
26
- async def execute(self, context: StageContext) -> StageResult:
27
- start_time = time.time()
28
- logger.info("Instruct Deepsearch: Starting supplementary research")
29
-
30
- # Check if we have context to review
31
- if not context.review_context:
32
- logger.warning("Instruct Deepsearch: No context found. Skipping.")
33
- return StageResult(
34
- success=True,
35
- data={"reasoning": "Skipped due to missing context.", "should_stop": True}
36
- )
37
-
38
- # Build System Prompt (Clean)
39
- system_prompt = INSTRUCT_DEEPSEARCH_SP
40
-
41
- # Build Messages
42
- # Inject context as a separate user message explaining the background
43
- context_message = f"## 已收集的信息\n\n```context\n{context.review_context}\n```"
44
-
45
- messages = [
46
- {"role": "system", "content": system_prompt},
47
- {"role": "user", "content": context_message},
48
- {"role": "user", "content": self._build_user_message(context)}
49
- ]
50
-
51
- # Call LLM
52
- # We use only web_search and crawl_page tools (no set_mode, no refuse_answer in this stage)
53
- tools = [self.web_search_tool, self.crawl_page_tool]
54
-
55
- response, usage, tool_calls, content = await self._call_llm(
56
- messages=messages,
57
- tools=tools,
58
- tool_choice="auto"
59
- )
60
-
61
- # Check for empty response = signal to stop
62
- should_stop = False
63
- if not tool_calls or len(tool_calls) == 0:
64
- logger.info("Instruct Deepsearch: No tool calls, signaling to stop loop.")
65
- should_stop = True
66
- else:
67
- # Execute Tools
68
- tool_outputs = await self._process_tool_calls(context, tool_calls)
69
-
70
- # Update context for next iteration
71
- iteration_summary = f"\n## Deepsearch Iteration\n"
72
- if content:
73
- iteration_summary += f"Thought: {content}\n"
74
- for output in tool_outputs:
75
- iteration_summary += f"- {output['name']}: {output['content'][:200]}...\n"
76
- context.review_context += iteration_summary
77
-
78
- # Update history
79
- context.instruct_history.append({
80
- "role": "assistant",
81
- "content": f"[Deepsearch]: {content}\n[Actions]: {len(tool_outputs)} tools"
82
- })
83
-
84
- return self._build_result(start_time, usage, content, len(tool_calls or []), should_stop)
85
-
86
- def _build_result(self, start_time, usage, content, tool_calls_count, should_stop=False):
87
- model_cfg = self.config.get_model_config("instruct")
88
- model = model_cfg.get("model_name") or self.config.model_name
89
-
90
- trace = {
91
- "stage": "Instruct Deepsearch",
92
- "model": model,
93
- "usage": usage,
94
- "output": content,
95
- "tool_calls": tool_calls_count,
96
- "time": time.time() - start_time,
97
- }
98
-
99
- return StageResult(
100
- success=True,
101
- data={"reasoning": content, "should_stop": should_stop},
102
- usage=usage,
103
- trace=trace
104
- )