entari-plugin-hyw 3.3.7__tar.gz → 3.3.8__tar.gz

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.

Files changed (57) hide show
  1. entari_plugin_hyw-3.3.8/PKG-INFO +113 -0
  2. entari_plugin_hyw-3.3.8/README.md +84 -0
  3. {entari_plugin_hyw-3.3.7 → entari_plugin_hyw-3.3.8}/pyproject.toml +1 -1
  4. {entari_plugin_hyw-3.3.7 → entari_plugin_hyw-3.3.8}/src/entari_plugin_hyw/__init__.py +10 -10
  5. entari_plugin_hyw-3.3.8/src/entari_plugin_hyw/assets/icon/huggingface.png +0 -0
  6. entari_plugin_hyw-3.3.8/src/entari_plugin_hyw/assets/icon/xiaomi.png +0 -0
  7. {entari_plugin_hyw-3.3.7 → entari_plugin_hyw-3.3.8}/src/entari_plugin_hyw/core/config.py +8 -8
  8. {entari_plugin_hyw-3.3.7 → entari_plugin_hyw-3.3.8}/src/entari_plugin_hyw/core/pipeline.py +57 -56
  9. entari_plugin_hyw-3.3.8/src/entari_plugin_hyw/utils/prompts.py +140 -0
  10. entari_plugin_hyw-3.3.7/src/entari_plugin_hyw/utils/prompts.py → entari_plugin_hyw-3.3.8/src/entari_plugin_hyw/utils/prompts_cn.py +4 -4
  11. entari_plugin_hyw-3.3.8/src/entari_plugin_hyw.egg-info/PKG-INFO +113 -0
  12. {entari_plugin_hyw-3.3.7 → entari_plugin_hyw-3.3.8}/src/entari_plugin_hyw.egg-info/SOURCES.txt +3 -0
  13. entari_plugin_hyw-3.3.7/PKG-INFO +0 -142
  14. entari_plugin_hyw-3.3.7/README.md +0 -113
  15. entari_plugin_hyw-3.3.7/src/entari_plugin_hyw.egg-info/PKG-INFO +0 -142
  16. {entari_plugin_hyw-3.3.7 → entari_plugin_hyw-3.3.8}/MANIFEST.in +0 -0
  17. {entari_plugin_hyw-3.3.7 → entari_plugin_hyw-3.3.8}/setup.cfg +0 -0
  18. {entari_plugin_hyw-3.3.7 → entari_plugin_hyw-3.3.8}/src/entari_plugin_hyw/assets/icon/anthropic.svg +0 -0
  19. {entari_plugin_hyw-3.3.7 → entari_plugin_hyw-3.3.8}/src/entari_plugin_hyw/assets/icon/deepseek.png +0 -0
  20. {entari_plugin_hyw-3.3.7 → entari_plugin_hyw-3.3.8}/src/entari_plugin_hyw/assets/icon/gemini.svg +0 -0
  21. {entari_plugin_hyw-3.3.7 → entari_plugin_hyw-3.3.8}/src/entari_plugin_hyw/assets/icon/google.svg +0 -0
  22. {entari_plugin_hyw-3.3.7 → entari_plugin_hyw-3.3.8}/src/entari_plugin_hyw/assets/icon/grok.png +0 -0
  23. {entari_plugin_hyw-3.3.7 → entari_plugin_hyw-3.3.8}/src/entari_plugin_hyw/assets/icon/microsoft.svg +0 -0
  24. {entari_plugin_hyw-3.3.7 → entari_plugin_hyw-3.3.8}/src/entari_plugin_hyw/assets/icon/minimax.png +0 -0
  25. {entari_plugin_hyw-3.3.7 → entari_plugin_hyw-3.3.8}/src/entari_plugin_hyw/assets/icon/mistral.png +0 -0
  26. {entari_plugin_hyw-3.3.7 → entari_plugin_hyw-3.3.8}/src/entari_plugin_hyw/assets/icon/nvida.png +0 -0
  27. {entari_plugin_hyw-3.3.7 → entari_plugin_hyw-3.3.8}/src/entari_plugin_hyw/assets/icon/openai.svg +0 -0
  28. {entari_plugin_hyw-3.3.7 → entari_plugin_hyw-3.3.8}/src/entari_plugin_hyw/assets/icon/openrouter.png +0 -0
  29. {entari_plugin_hyw-3.3.7 → entari_plugin_hyw-3.3.8}/src/entari_plugin_hyw/assets/icon/perplexity.svg +0 -0
  30. {entari_plugin_hyw-3.3.7 → entari_plugin_hyw-3.3.8}/src/entari_plugin_hyw/assets/icon/qwen.png +0 -0
  31. {entari_plugin_hyw-3.3.7 → entari_plugin_hyw-3.3.8}/src/entari_plugin_hyw/assets/icon/xai.png +0 -0
  32. {entari_plugin_hyw-3.3.7 → entari_plugin_hyw-3.3.8}/src/entari_plugin_hyw/assets/icon/zai.png +0 -0
  33. {entari_plugin_hyw-3.3.7 → entari_plugin_hyw-3.3.8}/src/entari_plugin_hyw/assets/libs/highlight.css +0 -0
  34. {entari_plugin_hyw-3.3.7 → entari_plugin_hyw-3.3.8}/src/entari_plugin_hyw/assets/libs/highlight.js +0 -0
  35. {entari_plugin_hyw-3.3.7 → entari_plugin_hyw-3.3.8}/src/entari_plugin_hyw/assets/libs/katex-auto-render.js +0 -0
  36. {entari_plugin_hyw-3.3.7 → entari_plugin_hyw-3.3.8}/src/entari_plugin_hyw/assets/libs/katex.css +0 -0
  37. {entari_plugin_hyw-3.3.7 → entari_plugin_hyw-3.3.8}/src/entari_plugin_hyw/assets/libs/katex.js +0 -0
  38. {entari_plugin_hyw-3.3.7 → entari_plugin_hyw-3.3.8}/src/entari_plugin_hyw/assets/libs/tailwind.css +0 -0
  39. {entari_plugin_hyw-3.3.7 → entari_plugin_hyw-3.3.8}/src/entari_plugin_hyw/assets/package-lock.json +0 -0
  40. {entari_plugin_hyw-3.3.7 → entari_plugin_hyw-3.3.8}/src/entari_plugin_hyw/assets/package.json +0 -0
  41. {entari_plugin_hyw-3.3.7 → entari_plugin_hyw-3.3.8}/src/entari_plugin_hyw/assets/tailwind.config.js +0 -0
  42. {entari_plugin_hyw-3.3.7 → entari_plugin_hyw-3.3.8}/src/entari_plugin_hyw/assets/tailwind.input.css +0 -0
  43. {entari_plugin_hyw-3.3.7 → entari_plugin_hyw-3.3.8}/src/entari_plugin_hyw/assets/template.html +0 -0
  44. {entari_plugin_hyw-3.3.7 → entari_plugin_hyw-3.3.8}/src/entari_plugin_hyw/assets/template.html.bak +0 -0
  45. {entari_plugin_hyw-3.3.7 → entari_plugin_hyw-3.3.8}/src/entari_plugin_hyw/assets/template.j2 +0 -0
  46. {entari_plugin_hyw-3.3.7 → entari_plugin_hyw-3.3.8}/src/entari_plugin_hyw/core/__init__.py +0 -0
  47. {entari_plugin_hyw-3.3.7 → entari_plugin_hyw-3.3.8}/src/entari_plugin_hyw/core/history.py +0 -0
  48. {entari_plugin_hyw-3.3.7 → entari_plugin_hyw-3.3.8}/src/entari_plugin_hyw/core/hyw.py +0 -0
  49. {entari_plugin_hyw-3.3.7 → entari_plugin_hyw-3.3.8}/src/entari_plugin_hyw/core/render.py +0 -0
  50. {entari_plugin_hyw-3.3.7 → entari_plugin_hyw-3.3.8}/src/entari_plugin_hyw/utils/__init__.py +0 -0
  51. {entari_plugin_hyw-3.3.7 → entari_plugin_hyw-3.3.8}/src/entari_plugin_hyw/utils/browser.py +0 -0
  52. {entari_plugin_hyw-3.3.7 → entari_plugin_hyw-3.3.8}/src/entari_plugin_hyw/utils/misc.py +0 -0
  53. {entari_plugin_hyw-3.3.7 → entari_plugin_hyw-3.3.8}/src/entari_plugin_hyw/utils/playwright_tool.py +0 -0
  54. {entari_plugin_hyw-3.3.7 → entari_plugin_hyw-3.3.8}/src/entari_plugin_hyw/utils/search.py +0 -0
  55. {entari_plugin_hyw-3.3.7 → entari_plugin_hyw-3.3.8}/src/entari_plugin_hyw.egg-info/dependency_links.txt +0 -0
  56. {entari_plugin_hyw-3.3.7 → entari_plugin_hyw-3.3.8}/src/entari_plugin_hyw.egg-info/requires.txt +0 -0
  57. {entari_plugin_hyw-3.3.7 → entari_plugin_hyw-3.3.8}/src/entari_plugin_hyw.egg-info/top_level.txt +0 -0
@@ -0,0 +1,113 @@
1
+ Metadata-Version: 2.4
2
+ Name: entari_plugin_hyw
3
+ Version: 3.3.8
4
+ Summary: Use large language models to interpret chat messages
5
+ Author-email: kumoSleeping <zjr2992@outlook.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/kumoSleeping/entari-plugin-hyw
8
+ Project-URL: Repository, https://github.com/kumoSleeping/entari-plugin-hyw
9
+ Project-URL: Issue Tracker, https://github.com/kumoSleeping/entari-plugin-hyw/issues
10
+ Keywords: entari,llm,ai,bot,chat
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Requires-Python: >=3.10
18
+ Description-Content-Type: text/markdown
19
+ Requires-Dist: arclet-entari[full]>=0.16.5
20
+ Requires-Dist: openai
21
+ Requires-Dist: httpx
22
+ Requires-Dist: markdown>=3.10
23
+ Requires-Dist: crawl4ai>=0.7.8
24
+ Requires-Dist: jinja2>=3.0
25
+ Requires-Dist: ddgs>=9.10.0
26
+ Provides-Extra: dev
27
+ Requires-Dist: entari-plugin-server>=0.5.0; extra == "dev"
28
+ Requires-Dist: satori-python-adapter-onebot11>=0.2.5; extra == "dev"
29
+
30
+ # Entari Plugin HYW
31
+
32
+ [![PyPI version](https://badge.fury.io/py/entari-plugin-hyw.svg)](https://badge.fury.io/py/entari-plugin-hyw)
33
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
34
+ [![Python Versions](https://img.shields.io/pypi/pyversions/entari-plugin-hyw.svg)](https://pypi.org/project/entari-plugin-hyw/)
35
+
36
+ **English** | [简体中文](docs/README_CN.md)
37
+
38
+ **Entari Plugin HYW** is an advanced agentic chat plugin for the [Entari](https://github.com/entari-org/entari) framework. It leverages Large Language Models (LLMs) to provide intelligent, context-aware, and multi-modal responses within instant messaging environments (OneBot 11, Satori).
39
+
40
+ The plugin implements a three-stage pipeline (**Vision**, **Instruct**, **Agent**) to autonomously decide when to search the web, crawl pages, or analyze images to answer user queries effectively.
41
+
42
+ <p align="center">
43
+ <img src="docs/demo_mockup.svg" width="800" />
44
+ </p>
45
+
46
+ ## Features
47
+
48
+ - 📖 **Agentic Workflow**
49
+ Autonomous decision-making process to search, browse, and reason.
50
+
51
+ - 🎑 **Multi-Modal Support**
52
+ Native support for image analysis using Vision Language Models (VLMs).
53
+
54
+ - 🔍 **Web Search & Crawling**
55
+ Integrated **DuckDuckGo** and **Crawl4AI** for real-time information retrieval.
56
+
57
+ - 🎨 **Rich Rendering**
58
+ Responses are rendered as images containing Markdown, syntax-highlighted code, LaTeX math, and citation badges.
59
+
60
+ - 🔌 **Protocol Support**
61
+ Deep integration with OneBot 11 and Satori protocols, handling reply context and JSON cards perfectly.
62
+
63
+ ## Installation
64
+
65
+ ```bash
66
+ pip install entari-plugin-hyw
67
+ ```
68
+
69
+ ## Configuration
70
+
71
+ Configure the plugin in your `entari.yml`.
72
+
73
+ ### Minimal Configuration
74
+
75
+ ```yaml
76
+ plugins:
77
+ entari_plugin_hyw:
78
+ model_name: google/gemini-3-flash-preview
79
+ api_key: "your-or-api-key-here"
80
+ ```
81
+
82
+ ## Usage
83
+
84
+ ### Commands
85
+
86
+ - **Text Query**
87
+ ```text
88
+ /q What's the latest news on Rust 1.83?
89
+ ```
90
+
91
+ - **Image Analysis**
92
+ *(Send an image with command, or reply to an image)*
93
+ ```text
94
+ /q [Image] Explain this error.
95
+ ```
96
+ - **Quote Query**
97
+ ```text
98
+ [quote: User Message] /q
99
+ ```
100
+
101
+ - **Follow-up**
102
+ *Reply to the bot's message to continue the conversation.*
103
+
104
+ ## Documentation for AI/LLMs
105
+
106
+ - [Instruction Guide (English)](docs/README_LLM_EN.md)
107
+ - [指导手册 (简体中文)](docs/README_LLM_CN.md)
108
+
109
+ ---
110
+
111
+ ## License
112
+
113
+ This project is licensed under the MIT License.
@@ -0,0 +1,84 @@
1
+ # Entari Plugin HYW
2
+
3
+ [![PyPI version](https://badge.fury.io/py/entari-plugin-hyw.svg)](https://badge.fury.io/py/entari-plugin-hyw)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+ [![Python Versions](https://img.shields.io/pypi/pyversions/entari-plugin-hyw.svg)](https://pypi.org/project/entari-plugin-hyw/)
6
+
7
+ **English** | [简体中文](docs/README_CN.md)
8
+
9
+ **Entari Plugin HYW** is an advanced agentic chat plugin for the [Entari](https://github.com/entari-org/entari) framework. It leverages Large Language Models (LLMs) to provide intelligent, context-aware, and multi-modal responses within instant messaging environments (OneBot 11, Satori).
10
+
11
+ The plugin implements a three-stage pipeline (**Vision**, **Instruct**, **Agent**) to autonomously decide when to search the web, crawl pages, or analyze images to answer user queries effectively.
12
+
13
+ <p align="center">
14
+ <img src="docs/demo_mockup.svg" width="800" />
15
+ </p>
16
+
17
+ ## Features
18
+
19
+ - 📖 **Agentic Workflow**
20
+ Autonomous decision-making process to search, browse, and reason.
21
+
22
+ - 🎑 **Multi-Modal Support**
23
+ Native support for image analysis using Vision Language Models (VLMs).
24
+
25
+ - 🔍 **Web Search & Crawling**
26
+ Integrated **DuckDuckGo** and **Crawl4AI** for real-time information retrieval.
27
+
28
+ - 🎨 **Rich Rendering**
29
+ Responses are rendered as images containing Markdown, syntax-highlighted code, LaTeX math, and citation badges.
30
+
31
+ - 🔌 **Protocol Support**
32
+ Deep integration with OneBot 11 and Satori protocols, handling reply context and JSON cards perfectly.
33
+
34
+ ## Installation
35
+
36
+ ```bash
37
+ pip install entari-plugin-hyw
38
+ ```
39
+
40
+ ## Configuration
41
+
42
+ Configure the plugin in your `entari.yml`.
43
+
44
+ ### Minimal Configuration
45
+
46
+ ```yaml
47
+ plugins:
48
+ entari_plugin_hyw:
49
+ model_name: google/gemini-3-flash-preview
50
+ api_key: "your-or-api-key-here"
51
+ ```
52
+
53
+ ## Usage
54
+
55
+ ### Commands
56
+
57
+ - **Text Query**
58
+ ```text
59
+ /q What's the latest news on Rust 1.83?
60
+ ```
61
+
62
+ - **Image Analysis**
63
+ *(Send an image with command, or reply to an image)*
64
+ ```text
65
+ /q [Image] Explain this error.
66
+ ```
67
+ - **Quote Query**
68
+ ```text
69
+ [quote: User Message] /q
70
+ ```
71
+
72
+ - **Follow-up**
73
+ *Reply to the bot's message to continue the conversation.*
74
+
75
+ ## Documentation for AI/LLMs
76
+
77
+ - [Instruction Guide (English)](docs/README_LLM_EN.md)
78
+ - [指导手册 (简体中文)](docs/README_LLM_CN.md)
79
+
80
+ ---
81
+
82
+ ## License
83
+
84
+ This project is licensed under the MIT License.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "entari_plugin_hyw"
7
- version = "3.3.7"
7
+ version = "3.3.8"
8
8
  description = "Use large language models to interpret chat messages"
9
9
  authors = [{name = "kumoSleeping", email = "zjr2992@outlook.com"}]
10
10
  dependencies = [
@@ -58,13 +58,11 @@ class HywConfig(BasicConfModel):
58
58
  base_url: str = "https://openrouter.ai/api/v1"
59
59
  vision_model_name: Optional[str] = None
60
60
  vision_api_key: Optional[str] = None
61
+ language: str = "Simplified Chinese"
61
62
  vision_base_url: Optional[str] = None
62
- vision_system_prompt: Optional[str] = None
63
- intruct_model_name: Optional[str] = None
64
- intruct_api_key: Optional[str] = None
65
- intruct_base_url: Optional[str] = None
66
- intruct_system_prompt: Optional[str] = None
67
- agent_system_prompt: Optional[str] = None
63
+ instruct_model_name: Optional[str] = None
64
+ instruct_api_key: Optional[str] = None
65
+ instruct_base_url: Optional[str] = None
68
66
  search_base_url: str = "https://lite.duckduckgo.com/lite/?q={query}"
69
67
  image_search_base_url: str = "https://duckduckgo.com/?q={query}&iax=images&ia=images"
70
68
  headless: bool = False
@@ -72,8 +70,10 @@ class HywConfig(BasicConfModel):
72
70
  icon: str = "openai"
73
71
  render_timeout_ms: int = 6000
74
72
  extra_body: Optional[Dict[str, Any]] = None
73
+ vision_extra_body: Optional[Dict[str, Any]] = None
74
+ instruct_extra_body: Optional[Dict[str, Any]] = None
75
75
  enable_browser_fallback: bool = False
76
- reaction: bool = True
76
+ reaction: bool = False
77
77
  quote: bool = True
78
78
  temperature: float = 0.4
79
79
  # Billing configuration (price per million tokens)
@@ -83,14 +83,14 @@ class HywConfig(BasicConfModel):
83
83
  vision_input_price: Optional[float] = None
84
84
  vision_output_price: Optional[float] = None
85
85
  # Instruct model pricing overrides (defaults to main model pricing if not set)
86
- intruct_input_price: Optional[float] = None
87
- intruct_output_price: Optional[float] = None
86
+ instruct_input_price: Optional[float] = None
87
+ instruct_output_price: Optional[float] = None
88
88
  # Provider Names
89
89
  search_name: str = "DuckDuckGo"
90
90
  search_provider: str = "crawl4ai" # crawl4ai | httpx | ddgs
91
91
  model_provider: Optional[str] = None
92
92
  vision_model_provider: Optional[str] = None
93
- intruct_model_provider: Optional[str] = None
93
+ instruct_model_provider: Optional[str] = None
94
94
 
95
95
 
96
96
 
@@ -12,27 +12,27 @@ class HYWConfig:
12
12
  fusion_mode: bool = False
13
13
  save_conversation: bool = False
14
14
  headless: bool = True
15
- intruct_model_name: Optional[str] = None
16
- intruct_api_key: Optional[str] = None
17
- intruct_base_url: Optional[str] = None
15
+ instruct_model_name: Optional[str] = None
16
+ instruct_api_key: Optional[str] = None
17
+ instruct_base_url: Optional[str] = None
18
18
  search_base_url: str = "https://lite.duckduckgo.com/lite/?q={query}"
19
19
  image_search_base_url: str = "https://duckduckgo.com/?q={query}&iax=images&ia=images"
20
20
  search_params: Optional[str] = None # e.g. "&kl=cn-zh" for China region
21
21
  search_limit: int = 8
22
22
  extra_body: Optional[Dict[str, Any]] = None
23
+ vision_extra_body: Optional[Dict[str, Any]] = None
24
+ instruct_extra_body: Optional[Dict[str, Any]] = None
23
25
  temperature: float = 0.4
24
26
  max_turns: int = 10
25
27
  icon: str = "openai" # logo for primary model
26
28
  vision_icon: Optional[str] = None # logo for vision model (falls back to icon when absent)
27
29
  instruct_icon: Optional[str] = None # logo for instruct model
28
30
  enable_browser_fallback: bool = False
29
- vision_system_prompt: Optional[str] = None
30
- intruct_system_prompt: Optional[str] = None
31
- agent_system_prompt: Optional[str] = None
31
+ language: str = "Simplified Chinese"
32
32
  input_price: Optional[float] = None # $ per 1M input tokens
33
33
  output_price: Optional[float] = None # $ per 1M output tokens
34
34
  vision_input_price: Optional[float] = None
35
35
  vision_output_price: Optional[float] = None
36
- intruct_input_price: Optional[float] = None
37
- intruct_output_price: Optional[float] = None
36
+ instruct_input_price: Optional[float] = None
37
+ instruct_output_price: Optional[float] = None
38
38
 
@@ -12,14 +12,14 @@ from .config import HYWConfig
12
12
  from ..utils.search import SearchService
13
13
  from ..utils.prompts import (
14
14
  AGENT_SP,
15
- AGENT_SP_INTRUCT_VISION_ADD,
15
+ AGENT_SP_INSTRUCT_VISION_ADD,
16
16
  AGENT_SP_TOOLS_STANDARD_ADD,
17
17
  AGENT_SP_TOOLS_AGENT_ADD,
18
18
  AGENT_SP_SEARCH_ADD,
19
19
  AGENT_SP_PAGE_ADD,
20
20
  AGENT_SP_IMAGE_SEARCH_ADD,
21
- INTRUCT_SP,
22
- INTRUCT_SP_VISION_ADD,
21
+ INSTRUCT_SP,
22
+ INSTRUCT_SP_VISION_ADD,
23
23
  VISION_SP,
24
24
  )
25
25
 
@@ -109,7 +109,7 @@ class ProcessingPipeline:
109
109
  ) -> Dict[str, Any]:
110
110
  """
111
111
  1) Vision: summarize images once (no image persistence).
112
- 2) Intruct: run web_search and decide whether to grant Playwright MCP tools.
112
+ 2) Instruct: run web_search and decide whether to grant Playwright MCP tools.
113
113
  3) Agent: normally no tools; if granted, allow Playwright MCP tools (max 6 rounds; step 5 nudge, step 6 forced).
114
114
  """
115
115
  start_time = time.time()
@@ -133,7 +133,7 @@ class ProcessingPipeline:
133
133
 
134
134
  trace: Dict[str, Any] = {
135
135
  "vision": None,
136
- "intruct": None,
136
+ "instruct": None,
137
137
  "agent": None,
138
138
  }
139
139
 
@@ -150,8 +150,7 @@ class ProcessingPipeline:
150
150
  or getattr(self.config, "vision_model_name", None)
151
151
  or active_model
152
152
  )
153
- vision_prompt_tpl = getattr(self.config, "vision_system_prompt", None) or VISION_SP
154
- vision_prompt = vision_prompt_tpl.format(user_msgs=user_input or "[图片]")
153
+ vision_prompt = VISION_SP.format(user_msgs=user_input or "[图片]")
155
154
  vision_text, vision_usage = await self._run_vision_stage(
156
155
  user_input=user_input,
157
156
  images=images,
@@ -182,10 +181,10 @@ class ProcessingPipeline:
182
181
  "cost": vision_cost
183
182
  }
184
183
 
185
- # Intruct + pre-search
184
+ # Instruct + pre-search
186
185
  instruct_start = time.time()
187
- instruct_model = getattr(self.config, "intruct_model_name", None) or active_model
188
- instruct_text, search_payloads, intruct_trace, intruct_usage, search_time = await self._run_instruct_stage(
186
+ instruct_model = getattr(self.config, "instruct_model_name", None) or active_model
187
+ instruct_text, search_payloads, instruct_trace, instruct_usage, search_time = await self._run_instruct_stage(
189
188
  user_input=user_input,
190
189
  vision_text=vision_text,
191
190
  model=instruct_model,
@@ -194,24 +193,24 @@ class ProcessingPipeline:
194
193
 
195
194
  # Calculate Instruct Cost
196
195
  instruct_cost = 0.0
197
- i_in_price = float(getattr(self.config, "intruct_input_price", None) or getattr(self.config, "input_price", 0.0) or 0.0)
198
- i_out_price = float(getattr(self.config, "intruct_output_price", None) or getattr(self.config, "output_price", 0.0) or 0.0)
196
+ i_in_price = float(getattr(self.config, "instruct_input_price", None) or getattr(self.config, "input_price", 0.0) or 0.0)
197
+ i_out_price = float(getattr(self.config, "instruct_output_price", None) or getattr(self.config, "output_price", 0.0) or 0.0)
199
198
  if i_in_price > 0 or i_out_price > 0:
200
- instruct_cost = (intruct_usage.get("input_tokens", 0) / 1_000_000 * i_in_price) + (intruct_usage.get("output_tokens", 0) / 1_000_000 * i_out_price)
199
+ instruct_cost = (instruct_usage.get("input_tokens", 0) / 1_000_000 * i_in_price) + (instruct_usage.get("output_tokens", 0) / 1_000_000 * i_out_price)
201
200
 
202
201
  # Add instruct usage
203
- usage_totals["input_tokens"] += intruct_usage.get("input_tokens", 0)
204
- usage_totals["output_tokens"] += intruct_usage.get("output_tokens", 0)
202
+ usage_totals["input_tokens"] += instruct_usage.get("input_tokens", 0)
203
+ usage_totals["output_tokens"] += instruct_usage.get("output_tokens", 0)
205
204
 
206
- intruct_trace["time"] = instruct_time
207
- intruct_trace["cost"] = instruct_cost
208
- trace["intruct"] = intruct_trace
205
+ instruct_trace["time"] = instruct_time
206
+ instruct_trace["cost"] = instruct_cost
207
+ trace["instruct"] = instruct_trace
209
208
 
210
209
  # Start agent loop
211
210
  agent_start_time = time.time()
212
211
  current_history.append({"role": "user", "content": user_input or "..."})
213
212
 
214
- mode = intruct_trace.get("mode", self.current_mode).lower()
213
+ mode = instruct_trace.get("mode", self.current_mode).lower()
215
214
  logger.success(f"Instruct Mode: {mode}")
216
215
  self.current_mode = mode
217
216
 
@@ -255,18 +254,17 @@ class ProcessingPipeline:
255
254
  has_image_results = any(r.get("_type") == "image" for r in self.all_web_results)
256
255
 
257
256
  # Build agent system prompt
258
- agent_prompt_tpl = getattr(self.config, "agent_system_prompt", None) or AGENT_SP
259
-
260
257
  mode_desc_text = AGENT_SP_TOOLS_AGENT_ADD.format(tools_desc=tools_desc) if mode == "agent" else AGENT_SP_TOOLS_STANDARD_ADD
261
- system_prompt = agent_prompt_tpl.format(
258
+ system_prompt = AGENT_SP.format(
262
259
  user_msgs=user_msgs_text,
263
260
  mode=mode,
264
- mode_desc=mode_desc_text
261
+ mode_desc=mode_desc_text,
262
+ language=getattr(self.config, "language", "Simplified Chinese")[:128]
265
263
  )
266
264
 
267
265
  # Append vision text if available
268
266
  if vision_text:
269
- system_prompt += AGENT_SP_INTRUCT_VISION_ADD.format(vision_msgs=vision_text)
267
+ system_prompt += AGENT_SP_INSTRUCT_VISION_ADD.format(vision_msgs=vision_text)
270
268
 
271
269
  # Append search results
272
270
  if has_search_results and search_msgs_text:
@@ -299,6 +297,7 @@ class ProcessingPipeline:
299
297
  model=active_model,
300
298
  tools=tools_for_step,
301
299
  tool_choice="auto" if tools_for_step else None,
300
+ extra_body=self.config.extra_body,
302
301
  )
303
302
  step_llm_time = time.time() - step_llm_start
304
303
 
@@ -366,8 +365,8 @@ class ProcessingPipeline:
366
365
  a_in_price = float(getattr(self.config, "input_price", 0.0) or 0.0)
367
366
  a_out_price = float(getattr(self.config, "output_price", 0.0) or 0.0)
368
367
 
369
- agent_input_tokens = usage_totals["input_tokens"] - vision_usage.get("input_tokens", 0) - intruct_usage.get("input_tokens", 0)
370
- agent_output_tokens = usage_totals["output_tokens"] - vision_usage.get("output_tokens", 0) - intruct_usage.get("output_tokens", 0)
368
+ agent_input_tokens = usage_totals["input_tokens"] - vision_usage.get("input_tokens", 0) - instruct_usage.get("input_tokens", 0)
369
+ agent_output_tokens = usage_totals["output_tokens"] - vision_usage.get("output_tokens", 0) - instruct_usage.get("output_tokens", 0)
371
370
 
372
371
  if a_in_price > 0 or a_out_price > 0:
373
372
  agent_cost = (max(0, agent_input_tokens) / 1_000_000 * a_in_price) + (max(0, agent_output_tokens) / 1_000_000 * a_out_price)
@@ -436,14 +435,14 @@ class ProcessingPipeline:
436
435
  "cost": v.get("cost", 0.0)
437
436
  })
438
437
 
439
- if trace.get("intruct"):
440
- i = trace["intruct"]
438
+ if trace.get("instruct"):
439
+ i = trace["instruct"]
441
440
  i_model = i.get("model", "")
442
441
  i_base_url = i.get("base_url", "") or self.config.base_url
443
442
  stages_used.append({
444
443
  "name": "Instruct",
445
444
  "model": i_model,
446
- "icon_config": getattr(self.config, "instruct_icon", None) or getattr(self.config, "intruct_icon", None) or infer_icon(i_model, i_base_url),
445
+ "icon_config": getattr(self.config, "instruct_icon", None) or infer_icon(i_model, i_base_url),
447
446
  "provider": infer_provider(i_base_url),
448
447
  "time": i.get("time", 0),
449
448
  "cost": i.get("cost", 0.0)
@@ -460,9 +459,9 @@ class ProcessingPipeline:
460
459
  })
461
460
 
462
461
  # Add Crawler stage if Instruct used crawl_page
463
- if trace.get("intruct"):
464
- intruct_tool_calls = trace["intruct"].get("tool_calls", [])
465
- crawl_calls = [tc for tc in intruct_tool_calls if tc.get("name") == "crawl_page"]
462
+ if trace.get("instruct"):
463
+ instruct_tool_calls = trace["instruct"].get("tool_calls", [])
464
+ crawl_calls = [tc for tc in instruct_tool_calls if tc.get("name") == "crawl_page"]
466
465
  if crawl_calls:
467
466
  # Build crawled_pages list for UI
468
467
  crawled_pages = []
@@ -828,10 +827,10 @@ class ProcessingPipeline:
828
827
  return f"Unknown tool {name}"
829
828
 
830
829
 
831
- async def _safe_llm_call(self, messages, model, tools=None, tool_choice=None, client: Optional[AsyncOpenAI] = None):
830
+ async def _safe_llm_call(self, messages, model, tools=None, tool_choice=None, client: Optional[AsyncOpenAI] = None, extra_body: Optional[Dict[str, Any]] = None):
832
831
  try:
833
832
  return await asyncio.wait_for(
834
- self._do_llm_request(messages, model, tools, tool_choice, client=client or self.client),
833
+ self._do_llm_request(messages, model, tools, tool_choice, client=client or self.client, extra_body=extra_body),
835
834
  timeout=120.0,
836
835
  )
837
836
  except asyncio.TimeoutError:
@@ -841,7 +840,7 @@ class ProcessingPipeline:
841
840
  logger.error(f"LLM Call Failed: {e}")
842
841
  return type("obj", (object,), {"content": f"Error: Model failure ({e})", "tool_calls": None})(), {"input_tokens": 0, "output_tokens": 0}
843
842
 
844
- async def _do_llm_request(self, messages, model, tools, tool_choice, client: AsyncOpenAI):
843
+ async def _do_llm_request(self, messages, model, tools, tool_choice, client: AsyncOpenAI, extra_body: Optional[Dict[str, Any]] = None):
845
844
  try:
846
845
  payload_debug = json.dumps(messages)
847
846
  logger.info(f"LLM Request Payload Size: {len(payload_debug)} chars")
@@ -856,6 +855,7 @@ class ProcessingPipeline:
856
855
  tools=tools,
857
856
  tool_choice=tool_choice,
858
857
  temperature=self.config.temperature,
858
+ extra_body=extra_body,
859
859
  )
860
860
  logger.info(f"LLM Request RECEIVED after {time.time() - t0:.2f}s")
861
861
 
@@ -880,6 +880,7 @@ class ProcessingPipeline:
880
880
  messages=[{"role": "system", "content": prompt}, {"role": "user", "content": content_payload}],
881
881
  model=model,
882
882
  client=client,
883
+ extra_body=getattr(self.config, "vision_extra_body", None),
883
884
  )
884
885
  return (response.content or "").strip(), usage
885
886
 
@@ -891,15 +892,14 @@ class ProcessingPipeline:
891
892
  tools = [self.web_search_tool, self.image_search_tool, self.set_mode_tool, self.crawl_page_tool]
892
893
  tools_desc = "- internal_web_search: 搜索文本\n- internal_image_search: 搜索图片\n- crawl_page: 获取网页内容\n- set_mode: 设定standard/agent模式"
893
894
 
894
- prompt_tpl = getattr(self.config, "intruct_system_prompt", None) or INTRUCT_SP
895
- prompt = prompt_tpl.format(user_msgs=user_input or "", tools_desc=tools_desc)
895
+ prompt = INSTRUCT_SP.format(user_msgs=user_input or "", tools_desc=tools_desc)
896
896
 
897
897
  if vision_text:
898
- prompt = f"{prompt}\\n\\n{INTRUCT_SP_VISION_ADD.format(vision_msgs=vision_text)}"
898
+ prompt = f"{prompt}\\n\\n{INSTRUCT_SP_VISION_ADD.format(vision_msgs=vision_text)}"
899
899
 
900
900
  client = self._client_for(
901
- api_key=getattr(self.config, "intruct_api_key", None),
902
- base_url=getattr(self.config, "intruct_base_url", None),
901
+ api_key=getattr(self.config, "instruct_api_key", None),
902
+ base_url=getattr(self.config, "instruct_base_url", None),
903
903
  )
904
904
 
905
905
  history: List[Dict[str, Any]] = [
@@ -913,12 +913,13 @@ class ProcessingPipeline:
913
913
  tools=tools,
914
914
  tool_choice="auto",
915
915
  client=client,
916
+ extra_body=getattr(self.config, "instruct_extra_body", None),
916
917
  )
917
918
 
918
919
  search_payloads: List[str] = []
919
- intruct_trace: Dict[str, Any] = {
920
+ instruct_trace: Dict[str, Any] = {
920
921
  "model": model,
921
- "base_url": getattr(self.config, "intruct_base_url", None) or self.config.base_url,
922
+ "base_url": getattr(self.config, "instruct_base_url", None) or self.config.base_url,
922
923
  "prompt": prompt,
923
924
  "user_input": user_input or "",
924
925
  "vision_add": vision_text or "",
@@ -946,8 +947,8 @@ class ProcessingPipeline:
946
947
  history.append(
947
948
  {"tool_call_id": tc.id, "role": "tool", "name": tc.function.name, "content": str(result)}
948
949
  )
949
- intruct_trace["tool_calls"].append(self._tool_call_to_trace(tc))
950
- intruct_trace["tool_results"].append({"name": tc.function.name, "content": str(result)})
950
+ instruct_trace["tool_calls"].append(self._tool_call_to_trace(tc))
951
+ instruct_trace["tool_results"].append({"name": tc.function.name, "content": str(result)})
951
952
 
952
953
  if tc.function.name in ["web_search", "internal_web_search"]:
953
954
  search_payloads.append(str(result))
@@ -959,18 +960,18 @@ class ProcessingPipeline:
959
960
  mode = args.get("mode", mode)
960
961
  mode_reason = args.get("reason", "")
961
962
 
962
- intruct_trace["mode"] = mode
963
+ instruct_trace["mode"] = mode
963
964
  if mode_reason:
964
- intruct_trace["mode_reason"] = mode_reason
965
+ instruct_trace["mode_reason"] = mode_reason
965
966
 
966
- intruct_trace["output"] = ""
967
- intruct_trace["usage"] = usage
968
- return "", search_payloads, intruct_trace, usage, search_time
967
+ instruct_trace["output"] = ""
968
+ instruct_trace["usage"] = usage
969
+ return "", search_payloads, instruct_trace, usage, search_time
969
970
 
970
- intruct_trace["mode"] = mode
971
- intruct_trace["output"] = (response.content or "").strip()
972
- intruct_trace["usage"] = usage
973
- return "", search_payloads, intruct_trace, usage, 0.0
971
+ instruct_trace["mode"] = mode
972
+ instruct_trace["output"] = (response.content or "").strip()
973
+ instruct_trace["usage"] = usage
974
+ return "", search_payloads, instruct_trace, usage, 0.0
974
975
 
975
976
  def _format_search_msgs(self) -> str:
976
977
  """Format search snippets only (not crawled pages)."""
@@ -1050,9 +1051,9 @@ class ProcessingPipeline:
1050
1051
  parts.append(fence("text", v.get("output", "")))
1051
1052
  parts.append("")
1052
1053
 
1053
- if trace.get("intruct"):
1054
- t = trace["intruct"]
1055
- parts.append("## Intruct\n")
1054
+ if trace.get("instruct"):
1055
+ t = trace["instruct"]
1056
+ parts.append("## Instruct\n")
1056
1057
  parts.append(f"- model: `{t.get('model')}`")
1057
1058
  parts.append(f"- base_url: `{t.get('base_url')}`\n")
1058
1059
  parts.append("### Prompt\n")