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.
- entari_plugin_hyw/Untitled-1 +1865 -0
- entari_plugin_hyw/__init__.py +726 -394
- entari_plugin_hyw/history.py +26 -13
- entari_plugin_hyw/misc.py +3 -0
- entari_plugin_hyw/search_cache.py +154 -0
- {entari_plugin_hyw-4.0.0rc7.dist-info → entari_plugin_hyw-4.0.0rc9.dist-info}/METADATA +3 -1
- entari_plugin_hyw-4.0.0rc9.dist-info/RECORD +68 -0
- {entari_plugin_hyw-4.0.0rc7.dist-info → entari_plugin_hyw-4.0.0rc9.dist-info}/WHEEL +1 -1
- {entari_plugin_hyw-4.0.0rc7.dist-info → entari_plugin_hyw-4.0.0rc9.dist-info}/top_level.txt +1 -0
- hyw_core/__init__.py +94 -0
- hyw_core/browser_control/__init__.py +65 -0
- hyw_core/browser_control/assets/card-dist/index.html +409 -0
- hyw_core/browser_control/assets/index.html +5691 -0
- hyw_core/browser_control/engines/__init__.py +17 -0
- {entari_plugin_hyw/browser → hyw_core/browser_control}/engines/duckduckgo.py +42 -8
- {entari_plugin_hyw/browser → hyw_core/browser_control}/engines/google.py +1 -1
- {entari_plugin_hyw/browser → hyw_core/browser_control}/manager.py +15 -8
- entari_plugin_hyw/render_vue.py → hyw_core/browser_control/renderer.py +29 -14
- {entari_plugin_hyw/browser → hyw_core/browser_control}/service.py +340 -112
- hyw_core/config.py +154 -0
- hyw_core/core.py +322 -0
- hyw_core/definitions.py +83 -0
- entari_plugin_hyw/modular_pipeline.py → hyw_core/pipeline.py +121 -97
- {entari_plugin_hyw → hyw_core}/search.py +19 -14
- hyw_core/stages/__init__.py +21 -0
- entari_plugin_hyw/stage_base.py → hyw_core/stages/base.py +2 -2
- entari_plugin_hyw/stage_summary.py → hyw_core/stages/summary.py +34 -11
- entari_plugin_hyw/assets/card-dist/index.html +0 -387
- entari_plugin_hyw/browser/__init__.py +0 -10
- entari_plugin_hyw/browser/engines/bing.py +0 -95
- entari_plugin_hyw/card-ui/.gitignore +0 -24
- entari_plugin_hyw/card-ui/README.md +0 -5
- entari_plugin_hyw/card-ui/index.html +0 -16
- entari_plugin_hyw/card-ui/package-lock.json +0 -2342
- entari_plugin_hyw/card-ui/package.json +0 -31
- entari_plugin_hyw/card-ui/public/logos/anthropic.svg +0 -1
- entari_plugin_hyw/card-ui/public/logos/cerebras.svg +0 -9
- entari_plugin_hyw/card-ui/public/logos/deepseek.png +0 -0
- entari_plugin_hyw/card-ui/public/logos/gemini.svg +0 -1
- entari_plugin_hyw/card-ui/public/logos/google.svg +0 -1
- entari_plugin_hyw/card-ui/public/logos/grok.png +0 -0
- entari_plugin_hyw/card-ui/public/logos/huggingface.png +0 -0
- entari_plugin_hyw/card-ui/public/logos/microsoft.svg +0 -15
- entari_plugin_hyw/card-ui/public/logos/minimax.png +0 -0
- entari_plugin_hyw/card-ui/public/logos/mistral.png +0 -0
- entari_plugin_hyw/card-ui/public/logos/nvida.png +0 -0
- entari_plugin_hyw/card-ui/public/logos/openai.svg +0 -1
- entari_plugin_hyw/card-ui/public/logos/openrouter.png +0 -0
- entari_plugin_hyw/card-ui/public/logos/perplexity.svg +0 -24
- entari_plugin_hyw/card-ui/public/logos/qwen.png +0 -0
- entari_plugin_hyw/card-ui/public/logos/xai.png +0 -0
- entari_plugin_hyw/card-ui/public/logos/xiaomi.png +0 -0
- entari_plugin_hyw/card-ui/public/logos/zai.png +0 -0
- entari_plugin_hyw/card-ui/public/vite.svg +0 -1
- entari_plugin_hyw/card-ui/src/App.vue +0 -787
- entari_plugin_hyw/card-ui/src/assets/vue.svg +0 -1
- entari_plugin_hyw/card-ui/src/components/HelloWorld.vue +0 -41
- entari_plugin_hyw/card-ui/src/components/MarkdownContent.vue +0 -382
- entari_plugin_hyw/card-ui/src/components/SectionCard.vue +0 -41
- entari_plugin_hyw/card-ui/src/components/StageCard.vue +0 -240
- entari_plugin_hyw/card-ui/src/main.ts +0 -5
- entari_plugin_hyw/card-ui/src/style.css +0 -29
- entari_plugin_hyw/card-ui/src/test_regex.js +0 -103
- entari_plugin_hyw/card-ui/src/types.ts +0 -61
- entari_plugin_hyw/card-ui/tsconfig.app.json +0 -16
- entari_plugin_hyw/card-ui/tsconfig.json +0 -7
- entari_plugin_hyw/card-ui/tsconfig.node.json +0 -26
- entari_plugin_hyw/card-ui/vite.config.ts +0 -16
- entari_plugin_hyw/definitions.py +0 -174
- entari_plugin_hyw/stage_instruct.py +0 -355
- entari_plugin_hyw/stage_instruct_deepsearch.py +0 -104
- entari_plugin_hyw/stage_vision.py +0 -113
- entari_plugin_hyw-4.0.0rc7.dist-info/RECORD +0 -102
- {entari_plugin_hyw → hyw_core/browser_control}/assets/card-dist/logos/anthropic.svg +0 -0
- {entari_plugin_hyw → hyw_core/browser_control}/assets/card-dist/logos/cerebras.svg +0 -0
- {entari_plugin_hyw → hyw_core/browser_control}/assets/card-dist/logos/deepseek.png +0 -0
- {entari_plugin_hyw → hyw_core/browser_control}/assets/card-dist/logos/gemini.svg +0 -0
- {entari_plugin_hyw → hyw_core/browser_control}/assets/card-dist/logos/google.svg +0 -0
- {entari_plugin_hyw → hyw_core/browser_control}/assets/card-dist/logos/grok.png +0 -0
- {entari_plugin_hyw → hyw_core/browser_control}/assets/card-dist/logos/huggingface.png +0 -0
- {entari_plugin_hyw → hyw_core/browser_control}/assets/card-dist/logos/microsoft.svg +0 -0
- {entari_plugin_hyw → hyw_core/browser_control}/assets/card-dist/logos/minimax.png +0 -0
- {entari_plugin_hyw → hyw_core/browser_control}/assets/card-dist/logos/mistral.png +0 -0
- {entari_plugin_hyw → hyw_core/browser_control}/assets/card-dist/logos/nvida.png +0 -0
- {entari_plugin_hyw → hyw_core/browser_control}/assets/card-dist/logos/openai.svg +0 -0
- {entari_plugin_hyw → hyw_core/browser_control}/assets/card-dist/logos/openrouter.png +0 -0
- {entari_plugin_hyw → hyw_core/browser_control}/assets/card-dist/logos/perplexity.svg +0 -0
- {entari_plugin_hyw → hyw_core/browser_control}/assets/card-dist/logos/qwen.png +0 -0
- {entari_plugin_hyw → hyw_core/browser_control}/assets/card-dist/logos/xai.png +0 -0
- {entari_plugin_hyw → hyw_core/browser_control}/assets/card-dist/logos/xiaomi.png +0 -0
- {entari_plugin_hyw → hyw_core/browser_control}/assets/card-dist/logos/zai.png +0 -0
- {entari_plugin_hyw → hyw_core/browser_control}/assets/card-dist/vite.svg +0 -0
- {entari_plugin_hyw/assets/icon → hyw_core/browser_control/assets/logos}/anthropic.svg +0 -0
- {entari_plugin_hyw/assets/icon → hyw_core/browser_control/assets/logos}/cerebras.svg +0 -0
- {entari_plugin_hyw/assets/icon → hyw_core/browser_control/assets/logos}/deepseek.png +0 -0
- {entari_plugin_hyw/assets/icon → hyw_core/browser_control/assets/logos}/gemini.svg +0 -0
- {entari_plugin_hyw/assets/icon → hyw_core/browser_control/assets/logos}/google.svg +0 -0
- {entari_plugin_hyw/assets/icon → hyw_core/browser_control/assets/logos}/grok.png +0 -0
- {entari_plugin_hyw/assets/icon → hyw_core/browser_control/assets/logos}/huggingface.png +0 -0
- {entari_plugin_hyw/assets/icon → hyw_core/browser_control/assets/logos}/microsoft.svg +0 -0
- {entari_plugin_hyw/assets/icon → hyw_core/browser_control/assets/logos}/minimax.png +0 -0
- {entari_plugin_hyw/assets/icon → hyw_core/browser_control/assets/logos}/mistral.png +0 -0
- {entari_plugin_hyw/assets/icon → hyw_core/browser_control/assets/logos}/nvida.png +0 -0
- {entari_plugin_hyw/assets/icon → hyw_core/browser_control/assets/logos}/openai.svg +0 -0
- {entari_plugin_hyw/assets/icon → hyw_core/browser_control/assets/logos}/openrouter.png +0 -0
- {entari_plugin_hyw/assets/icon → hyw_core/browser_control/assets/logos}/perplexity.svg +0 -0
- {entari_plugin_hyw/assets/icon → hyw_core/browser_control/assets/logos}/qwen.png +0 -0
- {entari_plugin_hyw/assets/icon → hyw_core/browser_control/assets/logos}/xai.png +0 -0
- {entari_plugin_hyw/assets/icon → hyw_core/browser_control/assets/logos}/xiaomi.png +0 -0
- {entari_plugin_hyw/assets/icon → hyw_core/browser_control/assets/logos}/zai.png +0 -0
- {entari_plugin_hyw/browser → hyw_core/browser_control}/engines/base.py +0 -0
- {entari_plugin_hyw/browser → hyw_core/browser_control}/engines/default.py +0 -0
- {entari_plugin_hyw/browser → hyw_core/browser_control}/landing.html +0 -0
- {entari_plugin_hyw → hyw_core}/image_cache.py +0 -0
entari_plugin_hyw/definitions.py
DELETED
|
@@ -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
|
-
)
|