entari-plugin-hyw 2.2.5__py3-none-any.whl → 3.5.0rc6__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 (91) hide show
  1. entari_plugin_hyw/__init__.py +371 -315
  2. entari_plugin_hyw/assets/card-dist/index.html +396 -0
  3. entari_plugin_hyw/assets/card-dist/logos/anthropic.svg +1 -0
  4. entari_plugin_hyw/assets/card-dist/logos/cerebras.svg +9 -0
  5. entari_plugin_hyw/assets/card-dist/logos/deepseek.png +0 -0
  6. entari_plugin_hyw/assets/card-dist/logos/gemini.svg +1 -0
  7. entari_plugin_hyw/assets/card-dist/logos/google.svg +1 -0
  8. entari_plugin_hyw/assets/card-dist/logos/grok.png +0 -0
  9. entari_plugin_hyw/assets/card-dist/logos/huggingface.png +0 -0
  10. entari_plugin_hyw/assets/card-dist/logos/microsoft.svg +15 -0
  11. entari_plugin_hyw/assets/card-dist/logos/minimax.png +0 -0
  12. entari_plugin_hyw/assets/card-dist/logos/mistral.png +0 -0
  13. entari_plugin_hyw/assets/card-dist/logos/nvida.png +0 -0
  14. entari_plugin_hyw/assets/card-dist/logos/openai.svg +1 -0
  15. entari_plugin_hyw/assets/card-dist/logos/openrouter.png +0 -0
  16. entari_plugin_hyw/assets/card-dist/logos/perplexity.svg +24 -0
  17. entari_plugin_hyw/assets/card-dist/logos/qwen.png +0 -0
  18. entari_plugin_hyw/assets/card-dist/logos/xai.png +0 -0
  19. entari_plugin_hyw/assets/card-dist/logos/xiaomi.png +0 -0
  20. entari_plugin_hyw/assets/card-dist/logos/zai.png +0 -0
  21. entari_plugin_hyw/assets/card-dist/vite.svg +1 -0
  22. entari_plugin_hyw/assets/icon/anthropic.svg +1 -0
  23. entari_plugin_hyw/assets/icon/cerebras.svg +9 -0
  24. entari_plugin_hyw/assets/icon/deepseek.png +0 -0
  25. entari_plugin_hyw/assets/icon/gemini.svg +1 -0
  26. entari_plugin_hyw/assets/icon/google.svg +1 -0
  27. entari_plugin_hyw/assets/icon/grok.png +0 -0
  28. entari_plugin_hyw/assets/icon/huggingface.png +0 -0
  29. entari_plugin_hyw/assets/icon/microsoft.svg +15 -0
  30. entari_plugin_hyw/assets/icon/minimax.png +0 -0
  31. entari_plugin_hyw/assets/icon/mistral.png +0 -0
  32. entari_plugin_hyw/assets/icon/nvida.png +0 -0
  33. entari_plugin_hyw/assets/icon/openai.svg +1 -0
  34. entari_plugin_hyw/assets/icon/openrouter.png +0 -0
  35. entari_plugin_hyw/assets/icon/perplexity.svg +24 -0
  36. entari_plugin_hyw/assets/icon/qwen.png +0 -0
  37. entari_plugin_hyw/assets/icon/xai.png +0 -0
  38. entari_plugin_hyw/assets/icon/xiaomi.png +0 -0
  39. entari_plugin_hyw/assets/icon/zai.png +0 -0
  40. entari_plugin_hyw/card-ui/.gitignore +24 -0
  41. entari_plugin_hyw/card-ui/README.md +5 -0
  42. entari_plugin_hyw/card-ui/index.html +16 -0
  43. entari_plugin_hyw/card-ui/package-lock.json +2342 -0
  44. entari_plugin_hyw/card-ui/package.json +31 -0
  45. entari_plugin_hyw/card-ui/public/logos/anthropic.svg +1 -0
  46. entari_plugin_hyw/card-ui/public/logos/cerebras.svg +9 -0
  47. entari_plugin_hyw/card-ui/public/logos/deepseek.png +0 -0
  48. entari_plugin_hyw/card-ui/public/logos/gemini.svg +1 -0
  49. entari_plugin_hyw/card-ui/public/logos/google.svg +1 -0
  50. entari_plugin_hyw/card-ui/public/logos/grok.png +0 -0
  51. entari_plugin_hyw/card-ui/public/logos/huggingface.png +0 -0
  52. entari_plugin_hyw/card-ui/public/logos/microsoft.svg +15 -0
  53. entari_plugin_hyw/card-ui/public/logos/minimax.png +0 -0
  54. entari_plugin_hyw/card-ui/public/logos/mistral.png +0 -0
  55. entari_plugin_hyw/card-ui/public/logos/nvida.png +0 -0
  56. entari_plugin_hyw/card-ui/public/logos/openai.svg +1 -0
  57. entari_plugin_hyw/card-ui/public/logos/openrouter.png +0 -0
  58. entari_plugin_hyw/card-ui/public/logos/perplexity.svg +24 -0
  59. entari_plugin_hyw/card-ui/public/logos/qwen.png +0 -0
  60. entari_plugin_hyw/card-ui/public/logos/xai.png +0 -0
  61. entari_plugin_hyw/card-ui/public/logos/xiaomi.png +0 -0
  62. entari_plugin_hyw/card-ui/public/logos/zai.png +0 -0
  63. entari_plugin_hyw/card-ui/public/vite.svg +1 -0
  64. entari_plugin_hyw/card-ui/src/App.vue +412 -0
  65. entari_plugin_hyw/card-ui/src/assets/vue.svg +1 -0
  66. entari_plugin_hyw/card-ui/src/components/HelloWorld.vue +41 -0
  67. entari_plugin_hyw/card-ui/src/components/MarkdownContent.vue +386 -0
  68. entari_plugin_hyw/card-ui/src/components/SectionCard.vue +41 -0
  69. entari_plugin_hyw/card-ui/src/components/StageCard.vue +237 -0
  70. entari_plugin_hyw/card-ui/src/main.ts +5 -0
  71. entari_plugin_hyw/card-ui/src/style.css +29 -0
  72. entari_plugin_hyw/card-ui/src/test_regex.js +103 -0
  73. entari_plugin_hyw/card-ui/src/types.ts +52 -0
  74. entari_plugin_hyw/card-ui/tsconfig.app.json +16 -0
  75. entari_plugin_hyw/card-ui/tsconfig.json +7 -0
  76. entari_plugin_hyw/card-ui/tsconfig.node.json +26 -0
  77. entari_plugin_hyw/card-ui/vite.config.ts +16 -0
  78. entari_plugin_hyw/history.py +170 -0
  79. entari_plugin_hyw/image_cache.py +274 -0
  80. entari_plugin_hyw/misc.py +128 -0
  81. entari_plugin_hyw/pipeline.py +1338 -0
  82. entari_plugin_hyw/prompts.py +108 -0
  83. entari_plugin_hyw/render_vue.py +314 -0
  84. entari_plugin_hyw/search.py +696 -0
  85. entari_plugin_hyw-3.5.0rc6.dist-info/METADATA +116 -0
  86. entari_plugin_hyw-3.5.0rc6.dist-info/RECORD +88 -0
  87. entari_plugin_hyw/hyw_core.py +0 -555
  88. entari_plugin_hyw-2.2.5.dist-info/METADATA +0 -135
  89. entari_plugin_hyw-2.2.5.dist-info/RECORD +0 -6
  90. {entari_plugin_hyw-2.2.5.dist-info → entari_plugin_hyw-3.5.0rc6.dist-info}/WHEEL +0 -0
  91. {entari_plugin_hyw-2.2.5.dist-info → entari_plugin_hyw-3.5.0rc6.dist-info}/top_level.txt +0 -0
@@ -1,555 +0,0 @@
1
- import asyncio
2
- import html
3
- import json
4
- import time
5
- from dataclasses import dataclass, field
6
- from typing import Any, Dict, List, Optional, Union
7
-
8
- import httpx
9
- from loguru import logger
10
- from openai import AsyncOpenAI
11
-
12
- # Try to import Playwright and Trafilatura
13
- try:
14
- import trafilatura
15
- from playwright.async_api import async_playwright, Browser, Playwright
16
- PLAYWRIGHT_AVAILABLE = True
17
- except ImportError:
18
- PLAYWRIGHT_AVAILABLE = False
19
- trafilatura = None
20
- async_playwright = None
21
- Browser = None
22
- Playwright = None
23
-
24
- # --- Constants & Prompts ---
25
-
26
- # --- Constants & Prompts ---
27
-
28
- VISION_EXPERT_SYSTEM_PROMPT = """你是一个专业的图像分析专家。
29
-
30
- [核心任务]
31
- - 请智能分析图片内容,根据图片类型自主选择侧重点。
32
- - **文字优先原则**:如果图片包含清晰的文字(如文档、截图、海报、对话记录等),或者用户的意图明显是获取文字信息,请将 **OCR文字识别** 作为核心任务。
33
- - 必须完整、准确地转录所有可见文字,不要遗漏。
34
- - 视觉描述作为补充,仅需简要说明图片类型(如"这是一张聊天记录截图")。
35
- - 视觉补充:如果图片几乎没有文字,或者文字仅为背景点缀,请重点描述图片的视觉内容(物体、场景、人物、动作、氛围等)。
36
-
37
- [输出格式]
38
- - 直接输出分析结果。
39
- - 如果识别到文字,请使用清晰的格式列出。
40
- """
41
-
42
- BASE_SYSTEM_PROMPT = """你是一个智能AI助手, 你的目的是帮助用户解决问题.
43
- 你拥有强大的思维链能力,在回答前请先进行深度的思考和隐形规划。
44
-
45
- [核心原则 - 必须严格遵守]
46
- - 永远使用中文回答
47
- - 语言简洁、语气客观专业、描述详精练抓重点
48
- - 绝对不允许使用除代码框外的markdown语法(**、*、`、#、-等符号)
49
- - 如果需要给出代码, 请添加到代码框内, 只给出部分代码即可, 尽可能减少回复字数
50
- - 回复带有紧扣结果相关的「补充推测」, 帮助用户进行下一步行动
51
-
52
- [安全审查]
53
- - 禁止讨论政策、国家领导人、政治体制等敏感话题的搜索与验证计划、新闻、历史事件
54
- - 过于敏感的话题, 规划时请谨慎
55
- - 过于色情、暴力、血腥等内容, 请谨慎处理, 避免直接描述
56
-
57
- [搜索与验证原则]
58
- - 避免搜索 x.com 的信息, 尽可能使用权威网站、相关项目官方网站
59
- - 搜索内容指向不同相关项目时, 尝试理解关系, 请避免混为一谈
60
- - 禁止导航到搜索引擎页面, 你可以直接导航到相关官网或权威网站
61
- - 可以同时启动多个工具查看不同页面, 提高效率
62
- - 人名、地名、组织名等关键信息优先验证, 只相信权威网站、相关项目官方网站
63
- - 存在视觉分析专家信息时, 不要尝试通过角色、人物特征进行搜索验证、直接利用视觉分析结果回答. 但如果视觉分析中有文字存在,可以对文字内容进行搜索, 抓住重点补充
64
- - 分步验证思想: 先确认A, 通过A确认B或C. 验证重点:指出需要特别验证的事实、数据或来源.
65
-
66
- [使用以下工具来获取页面和验证信息]
67
- {tools_desc}
68
-
69
- [针对搜索结果的回复要求]
70
- - 根据搜索结果给出准确回答,忽略浏览器广告、自动纠错提示等多余信息
71
- - 减少"根据搜索结果"、"未发现相关信息"等无意义表述
72
- - 由于搜索客观实效性, 避免 `预计` `大概` `可能` 等词汇
73
- - 不能使用 `教程` `怎么办` 等词汇进行搜索, 这些词汇会导致搜索结果偏离主题, 而且非官方信息居多
74
-
75
- [推测]
76
- - 回复推测时也要使得语气平稳、陈述
77
- - 回复推测语句简短, 通常在10个字左右
78
- - 一些合适的推测示例方向: /1 深入研究 /2 了解更多关于 /3 继续深度搜索 /4 解决方案 /1 官方文档的最佳实践 /2 给出实际代码片段 /3 获取页面完整内容...
79
-
80
- [最终回复格式]
81
- [LLM Agent] >>
82
- <纯文本详细解释>
83
- <(如果需要提供代码)```&lt;代码语言>
84
- <代码>
85
- ```>
86
-
87
- [Next?] >>
88
- /1 <回复推测1>
89
- /2 <回复推测2>
90
- /3 <回复推测3>
91
- /4 <回复推测4>
92
- """
93
-
94
- # --- Configuration ---
95
-
96
- @dataclass
97
- class HYWConfig:
98
- api_key: str
99
- model_name: str
100
- base_url: str = "https://openrouter.ai/api/v1"
101
- save_conversation: bool = False
102
- headless: bool = True
103
-
104
- # Browser Tool Configuration
105
- browser_tool: str = "jina" # "jina" or "playwright"
106
- jina_api_key: Optional[str] = None
107
-
108
- vision_model_name: Optional[str] = None
109
- vision_base_url: Optional[str] = None
110
- vision_api_key: Optional[str] = None
111
-
112
- # --- Browser Tool ---
113
-
114
- class BrowserTool:
115
- def __init__(self, config: HYWConfig):
116
- self.config = config
117
- self.playwright: Optional[Any] = None
118
- self.browser: Optional[Any] = None
119
-
120
- if self.config.browser_tool == "playwright" and not PLAYWRIGHT_AVAILABLE:
121
- raise RuntimeError("Browser tool set to 'playwright' but playwright/trafilatura is not installed. Please install with 'pip install entari-plugin-hyw[playwright]' or set browser_tool to 'jina'.")
122
-
123
- if not PLAYWRIGHT_AVAILABLE and self.config.browser_tool != "jina":
124
- logger.warning("Playwright not installed. Local browser navigation disabled.")
125
-
126
- async def navigate(self, url: str) -> str:
127
- """Navigate to a URL and return the page content with fallback mechanism"""
128
-
129
- # Determine primary and secondary methods
130
- can_use_playwright = PLAYWRIGHT_AVAILABLE
131
-
132
- if self.config.browser_tool == "jina":
133
- primary_method = self._navigate_jina
134
- primary_name = "Jina"
135
-
136
- if can_use_playwright:
137
- secondary_method = self._navigate_playwright
138
- secondary_name = "Playwright"
139
- else:
140
- secondary_method = None
141
- secondary_name = None
142
- elif self.config.browser_tool == "playwright":
143
- primary_method = self._navigate_playwright
144
- primary_name = "Playwright"
145
- secondary_method = self._navigate_jina
146
- secondary_name = "Jina"
147
- else:
148
- # Default to Jina if unknown
149
- logger.warning(f"Unknown browser_tool '{self.config.browser_tool}', defaulting to Jina")
150
- primary_method = self._navigate_jina
151
- primary_name = "Jina"
152
- secondary_method = None
153
- secondary_name = None
154
-
155
- # Try primary method
156
- content = await primary_method(url)
157
-
158
- # Check for failure (assuming error messages start with "Error")
159
- if content.startswith("Error") and secondary_method:
160
- logger.warning(f"{primary_name} failed: {content}. Falling back to {secondary_name}...")
161
- content = await secondary_method(url)
162
-
163
- return content
164
-
165
- async def _navigate_jina(self, url: str) -> str:
166
- """Navigate using Jina AI"""
167
- try:
168
- logger.info(f"Jina AI navigating to: {url}")
169
- headers = {}
170
- if self.config.jina_api_key:
171
- headers["Authorization"] = f"Bearer {self.config.jina_api_key}"
172
-
173
- async with httpx.AsyncClient(timeout=30.0) as client:
174
- resp = await client.get(f"https://r.jina.ai/{url}", headers=headers)
175
- if resp.status_code == 200:
176
- content = resp.text
177
- logger.info(f"Successfully fetched {len(content)} chars from {url} via Jina")
178
- return content
179
- else:
180
- return f"Error navigating to {url} via Jina: Status {resp.status_code}"
181
- except Exception as e:
182
- logger.error(f"Jina navigation failed: {e}")
183
- return f"Error navigating to {url} via Jina: {str(e)}"
184
-
185
- async def _ensure_browser(self):
186
- """Ensure Playwright browser is initialized"""
187
- if not PLAYWRIGHT_AVAILABLE:
188
- return
189
-
190
- if self.playwright is None:
191
- self.playwright = await async_playwright().start()
192
-
193
- if self.browser is None:
194
- self.browser = await self.playwright.chromium.launch(
195
- headless=self.config.headless,
196
- args=["--disable-blink-features=AutomationControlled"],
197
- ignore_default_args=["--enable-automation"]
198
- )
199
- logger.info("Playwright browser initialized")
200
-
201
- async def _navigate_playwright(self, url: str) -> str:
202
- """Navigate using Playwright with a fresh context/page"""
203
- if not PLAYWRIGHT_AVAILABLE:
204
- return "Error: Playwright not installed"
205
-
206
- await self._ensure_browser()
207
-
208
- if not self.browser:
209
- return "Error: Browser not initialized"
210
-
211
- context = await self.browser.new_context(
212
- viewport={"width": 1280, "height": 800},
213
- user_agent="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36"
214
- )
215
-
216
- # Inject script to hide navigator.webdriver
217
- await context.add_init_script("""
218
- Object.defineProperty(navigator, 'webdriver', {
219
- get: () => undefined
220
- });
221
- """)
222
-
223
- page = await context.new_page()
224
-
225
- try:
226
- logger.info(f"Playwright navigating to: {url}")
227
- await page.goto(url, wait_until="domcontentloaded", timeout=60000)
228
-
229
- # Wait a bit for dynamic content
230
- try:
231
- await page.wait_for_load_state("networkidle", timeout=5000)
232
- except Exception:
233
- pass
234
-
235
- # Get page content
236
- html_content = await page.content()
237
-
238
- # Use trafilatura for extraction
239
- content = trafilatura.extract(
240
- html_content,
241
- include_links=False,
242
- include_images=False,
243
- include_tables=False,
244
- include_comments=False,
245
- output_format="markdown"
246
- )
247
-
248
- # Fallback
249
- if not content:
250
- content = await page.evaluate("() => document.body.innerText")
251
-
252
- logger.info(f"Successfully fetched {len(content) if content else 0} chars from {url}")
253
- return content if content else "Error: Empty content"
254
-
255
- except Exception as e:
256
- logger.error(f"Playwright navigation failed: {e}")
257
- return f"Error navigating to {url}: {str(e)}"
258
- finally:
259
- await page.close()
260
- await context.close()
261
-
262
- async def close(self):
263
- if self.browser:
264
- await self.browser.close()
265
- if self.playwright:
266
- await self.playwright.stop()
267
-
268
-
269
- # --- Core Class ---
270
-
271
- class HYW:
272
- def __init__(self, config: HYWConfig):
273
- self.config = config
274
- self.client = AsyncOpenAI(base_url=config.base_url, api_key=config.api_key)
275
-
276
- self._init_clients()
277
- self.browser_tool = BrowserTool(config)
278
- self._init_tools()
279
-
280
- logger.info(f"HYW initialized - Save Conversation: {config.save_conversation}, Browser Tool: {config.browser_tool}")
281
-
282
- def _init_clients(self):
283
- # Vision Client
284
- if self.config.vision_base_url:
285
- self.vision_client = AsyncOpenAI(
286
- base_url=self.config.vision_base_url,
287
- api_key=self.config.vision_api_key or self.config.api_key
288
- )
289
- model = self.config.vision_model_name or self.config.model_name
290
- logger.info(f"Vision client created - Endpoint: {self.config.vision_base_url}, Model: {model}")
291
- else:
292
- self.vision_client = self.client
293
- model = self.config.vision_model_name or self.config.model_name
294
- logger.info(f"Vision using main client - Model: {model}")
295
-
296
- def _init_tools(self):
297
- self.tools = []
298
-
299
- self.tools.append({
300
- "type": "function",
301
- "function": {
302
- "name": "browser_navigate",
303
- "description": "Navigate to a URL and return the page content. Use this to search or view pages.",
304
- "parameters": {
305
- "type": "object",
306
- "properties": {
307
- "url": {
308
- "type": "string",
309
- "description": "The URL to navigate to. For searching, use the search engine URL with the query."
310
- }
311
- },
312
- "required": ["url"]
313
- }
314
- }
315
- })
316
-
317
- self.tools_desc = "\n".join([f"- {t['function']['name']}" for t in self.tools])
318
-
319
- async def analyze_images(self, images: List[str]) -> str:
320
- """Analyze images and return description"""
321
- if not images:
322
- return ""
323
-
324
- try:
325
- logger.info(f"Starting image analysis - Count: {len(images)}")
326
-
327
- img_content: List[Dict[str, Any]] = [{'type': 'text', 'text': '请分析这些图片'}]
328
- for img in images:
329
- img_content.append({"type": "image_url", "image_url": {"url": f"data:image/png;base64,{img}"}})
330
-
331
- img_messages = [
332
- {"role": "system", "content": VISION_EXPERT_SYSTEM_PROMPT},
333
- {"role": "user", "content": img_content}
334
- ]
335
-
336
- model = self.config.vision_model_name or self.config.model_name
337
- img_resp = await self.vision_client.chat.completions.create(
338
- model=model,
339
- messages=img_messages
340
- )
341
- if img_resp.choices[0].message.content:
342
- logger.info(f"Image analysis complete")
343
- return img_resp.choices[0].message.content
344
- except Exception as e:
345
- logger.error(f"Image analysis failed: {e}")
346
- return ""
347
- return ""
348
-
349
- async def call_tool(self, tool_call) -> str:
350
- func_name = tool_call.function.name
351
- # Decode HTML entities in arguments before parsing
352
- args_str = html.unescape(tool_call.function.arguments)
353
- try:
354
- args = json.loads(args_str)
355
- except json.JSONDecodeError:
356
- return f"Error: Invalid JSON arguments for tool {func_name}"
357
-
358
- if func_name == "browser_navigate":
359
- if not self.browser_tool:
360
- return "Error: Browser tool is disabled"
361
-
362
- url = args.get("url")
363
- if url:
364
- return await self.browser_tool.navigate(url)
365
- return "Error: Missing URL argument"
366
- return f"Error: Unknown tool {func_name}"
367
-
368
- def _tool_msg(self, tool_call_id: str, content: Any, is_error: bool = False, elapsed_time: Optional[float] = None) -> Dict[str, Any]:
369
- msg_content = f"错误: {content}" if is_error else str(content)
370
- if elapsed_time is not None:
371
- msg_content = f"[已运行: {elapsed_time:.2f}s] {msg_content}"
372
- return {
373
- "role": "tool",
374
- "tool_call_id": tool_call_id,
375
- "content": msg_content
376
- }
377
-
378
- async def _run_tool_isolated(self, tool_call, agent_start_time: float) -> Dict[str, Any]:
379
- tool_start = time.time()
380
- try:
381
- result = await self.call_tool(tool_call)
382
- tool_duration = time.time() - tool_start
383
- total_elapsed = time.time() - agent_start_time
384
- logger.info(f"Tool {tool_call.function.name} finished in {tool_duration:.2f}s (Total since start: {total_elapsed:.2f}s)")
385
- return self._tool_msg(tool_call.id, result, elapsed_time=total_elapsed)
386
- except Exception as e:
387
- total_elapsed = time.time() - agent_start_time
388
- logger.error(f"Tool failed: {e}")
389
- return self._tool_msg(tool_call.id, e, is_error=True, elapsed_time=total_elapsed)
390
-
391
- def _format_extra_content(self, content: Any) -> str:
392
- """Format extra content like reasoning or annotations"""
393
- return str(content)
394
-
395
- def _save_conversation_debug(self, messages: List[Dict[str, Any]]):
396
- """Save conversation history to JSON file for debugging"""
397
- if not self.config.save_conversation:
398
- return
399
-
400
- try:
401
- import os
402
- debug_dir = "saved_conversations"
403
- os.makedirs(debug_dir, exist_ok=True)
404
-
405
- timestamp = int(time.time())
406
- filename = os.path.join(debug_dir, f"conversation_{timestamp}.json")
407
-
408
- with open(filename, "w", encoding="utf-8") as f:
409
- json.dump(messages, f, ensure_ascii=False, indent=2)
410
-
411
- logger.debug(f"Conversation saved to {filename}")
412
- except Exception as e:
413
- logger.warning(f"Failed to save conversation debug: {e}")
414
-
415
- def _append_stats_info(self, content: str, stats: Dict[str, Any], start_time: float) -> str:
416
- current_duration = time.time() - start_time
417
-
418
- if not content or not content.strip():
419
- content = "[ERROR] \n>> 抱歉,获取到的内容可能包含敏感信息,暂时无法显示完整结果。"
420
-
421
- # Build stats parts
422
- vision_duration = stats.get("vision_duration", 0)
423
- if vision_duration > 0:
424
- time_parts = [f"[V:{vision_duration:.2f}s/{current_duration:.2f}s]"]
425
- else:
426
- time_parts = [f"[{current_duration:.2f}s]"]
427
-
428
- # Tools
429
- search_count = stats.get('search_results', 0)
430
- web_count = stats.get('web_pages_opened', 0)
431
- tools_parts = []
432
- if search_count > 0:
433
- tools_parts.append(f"[S:{search_count}]")
434
- if web_count > 0:
435
- tools_parts.append(f"[W:{web_count}]")
436
-
437
- stats_info = f"\n[Stats] :: {' '.join(tools_parts)} {' '.join(time_parts)}"
438
- return content + stats_info
439
-
440
- async def agent(self, user_input: str, conversation_history: Optional[List[Dict[str, Any]]] = None, images: Optional[List[str]] = None) -> Dict[str, Any]:
441
- start_time = time.time()
442
-
443
- stats = {
444
- "llm_calls": 0,
445
- "search_results": 0,
446
- "web_pages_opened": 0,
447
- "total_time": 0.0,
448
- "vision_duration": 0.0
449
- }
450
-
451
- # Vision/OCR Analysis
452
- image_analysis = ""
453
- if images:
454
- v_start = time.time()
455
- image_analysis = await self.analyze_images(images)
456
- stats["vision_duration"] = time.time() - v_start
457
-
458
- system_prompt = BASE_SYSTEM_PROMPT.format(tools_desc=self.tools_desc)
459
- messages: List[Dict[str, Any]] = [{"role": "system", "content": system_prompt}]
460
-
461
- if image_analysis:
462
- messages.append({"role": "system", "content": f"[图片分析报告]\n{image_analysis}"})
463
-
464
- if conversation_history:
465
- messages.extend([m for m in conversation_history if m.get("role") != "system"])
466
-
467
- messages.append({"role": "user", "content": user_input})
468
-
469
- logger.info(f"Processing: {user_input[:50]}...")
470
-
471
- try:
472
- for _ in range(25):
473
- # Retry mechanism for API calls
474
- max_retries = 3
475
- resp = None
476
- last_error = None
477
-
478
- for attempt in range(max_retries):
479
- try:
480
- stats["llm_calls"] += 1
481
- resp = await self.client.chat.completions.create(
482
- model=self.config.model_name,
483
- messages=messages,
484
- tools=self.tools if self.tools else None,
485
- tool_choice="auto" if self.tools else None,
486
- extra_body={"reasoning": {"effort": "low"}}
487
- )
488
- break
489
- except Exception as e:
490
- last_error = e
491
- if attempt < max_retries - 1:
492
- logger.warning(f"API call failed (attempt {attempt + 1}/{max_retries}): {e}")
493
- await asyncio.sleep(2)
494
- else:
495
- logger.error(f"API call failed after {max_retries} attempts: {e}")
496
-
497
- if resp is None:
498
- if last_error:
499
- self._save_conversation_debug(messages)
500
- logger.error(f"Final API failure: {last_error}")
501
- return {
502
- "llm_response": f"抱歉,AI 提供商似乎出现了故障,重试多次后仍然失败。\n错误信息: {str(last_error)}",
503
- "conversation_history": messages,
504
- "stats": stats
505
- }
506
- return {"llm_response": "Error: Failed to get response from LLM", "conversation_history": messages, "stats": stats}
507
-
508
- msg = resp.choices[0].message
509
- msg_dict = msg.model_dump(exclude_none=True)
510
-
511
- # Process reasoning and annotations
512
- annotations = msg_dict.get('annotations')
513
-
514
- # Clean up response dict
515
- for key in ['reasoning_details', 'annotations', 'reasoning']:
516
- msg_dict.pop(key, None)
517
-
518
- # Add system message with search info if available
519
- if annotations:
520
- search_info = self._format_extra_content(annotations)
521
- try:
522
- if isinstance(annotations, list):
523
- stats["search_results"] += len(annotations)
524
- except Exception:
525
- pass
526
-
527
- system_msg = {
528
- "role": "tool",
529
- "content": search_info,
530
- "tool_call_id": "citation"
531
- }
532
- messages.append(system_msg)
533
-
534
- messages.append(msg_dict)
535
-
536
- logger.info(f"LLM Response: content={bool(msg.content)}, tools={bool(msg.tool_calls)}")
537
-
538
- if msg.tool_calls:
539
- stats["web_pages_opened"] += len([tc for tc in msg.tool_calls if tc.function.name == "browser_navigate"])
540
- tasks = [self._run_tool_isolated(tc, start_time) for tc in msg.tool_calls]
541
- results = await asyncio.gather(*tasks)
542
- messages.extend(results)
543
- elif msg.content:
544
- logger.success("Conversation completed")
545
- self._save_conversation_debug(messages)
546
- filtered_history = [m for m in messages if m.get("role") != "system"]
547
- final_response = self._append_stats_info(msg.content, stats, start_time)
548
- return {"llm_response": final_response, "conversation_history": filtered_history, "stats": stats}
549
-
550
- # Max turns reached
551
- self._save_conversation_debug(messages)
552
- final_response = self._append_stats_info("Max turns reached", stats, start_time)
553
- return {"llm_response": final_response, "conversation_history": messages, "stats": stats}
554
- finally:
555
- pass
@@ -1,135 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: entari_plugin_hyw
3
- Version: 2.2.5
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
- Provides-Extra: playwright
23
- Requires-Dist: playwright>=1.56.0; extra == "playwright"
24
- Requires-Dist: trafilatura>=2.0.0; extra == "playwright"
25
- Provides-Extra: dev
26
- Requires-Dist: entari-plugin-server>=0.5.0; extra == "dev"
27
- Requires-Dist: satori-python-adapter-onebot11>=0.2.5; extra == "dev"
28
-
29
- <div align="center">
30
-
31
- # Entari Plugin HYW
32
-
33
- **Entari 智能聊天解释插件**
34
-
35
- [![License](https://img.shields.io/badge/License-MIT-blue.svg?style=flat-square)](https://opensource.org/licenses/MIT) [![PyPI](https://img.shields.io/pypi/v/entari-plugin-hyw?style=flat-square&color=success)](https://pypi.org/project/entari-plugin-hyw/) [![Python](https://img.shields.io/badge/Python-3.10+-blue.svg?style=flat-square&logo=python&logoColor=white)](https://www.python.org/downloads/)
36
-
37
- *IM 环境下的 LLM 智能解释方案*
38
-
39
- </div>
40
-
41
- ## 🎑 效果展示
42
-
43
-
44
-
45
- <div align="center">
46
- <img src="demo.svg" alt="Chat Demo" width="800" style="display: block; margin: 0;">
47
- </div>
48
-
49
- ## ✨ 功能特性
50
- - **关于搜索**:目前推荐使用 OpenRouter 的 `:online` 参数,该参数会优先使用模型提供商的搜索、其次 `exa`(较贵) 进行网页搜索。推荐使用 `x-ai/grok-4.1-fast:online`。
51
- - 给予 `Alconna` 与 `MessageChain` 混合处理, 深度优化触发体验`。
52
- - **网页获取**:支持通过 **Jina AI** 或 **Playwright** 进行实时页面获取。
53
- - **多模态理解**:无缝处理文本与图片。自动对文档/截图进行 OCR 文字识别,对照片进行视觉分析。
54
- - **上下文感知**:维护对话历史记录,支持自然、连续的多轮对话。支持保存对话历史记录搭到本地研究。
55
- - **OneBot 优化**:针对 OneBot 11 协议深度优化,支持解析 JSON 卡片、引用消息等特殊元素。
56
- - `reaction` 表情, 表示任务开始。
57
-
58
-
59
-
60
- ## 📦 安装
61
-
62
- ### 基础安装
63
- ```bash
64
- pip install entari-plugin-hyw
65
- ```
66
-
67
- ### 启用 Playwright 支持
68
- 如果你希望使用 Playwright 进行本地网页渲染(而非仅使用 Jina AI):
69
- ```bash
70
- pip install entari-plugin-hyw[playwright]
71
- playwright install chromium
72
- ```
73
-
74
- ## ⚙️ 配置
75
-
76
- 请在 `entari.yml` 中添加以下配置:
77
-
78
- ```yaml
79
- plugins:
80
- entari_plugin_hyw:
81
- # --- 基础设置 ---
82
- # 触发机器人的命令列表
83
- command_name_list: ["/hyw", "hyw"]
84
-
85
- # 主 LLM 模型配置(必需)(online 模式), 如 x-ai/grok-4.1-fast:online、perplexity/sonar
86
- model_name: "gx-ai/grok-4.1-fast:online"
87
- api_key: "your-api-key"
88
- base_url: "https://dashscope.aliyuncs.com/compatible-mode/v1"
89
-
90
- # --- 浏览器与搜索 ---
91
- # 网页浏览工具: "jina" (默认) 或 "playwright"
92
- browser_tool: "jina"
93
-
94
- # 可选: Jina AI API Key (配置以获得更高限额)(免费方案20/min)
95
- jina_api_key: "jina_..."
96
-
97
- # Playwright 设置
98
- headless: true
99
-
100
- # --- 视觉配置 (可选) ---
101
- # 如果未设置,将回退使用主模型
102
- vision_model_name: "qwen-vl-plus"
103
- vision_api_key: "your-vision-api-key"
104
- vision_base_url: "https://dashscope.aliyuncs.com/compatible-mode/v1"
105
-
106
- # --- 调试 ---
107
- save_conversation: false
108
- ```
109
-
110
- ## 📖 使用方法
111
-
112
- ### 基础指令
113
- 使用配置的命令前缀与机器人交互:
114
-
115
- ```text
116
- hyw 最近LLM有啥新闻, 是不是claude又被秒了
117
- hyw [图片消息] 里面这人写代码怎么我一句都看不懂
118
- hyw https://koishi.chat/ 怎么安装
119
- [回复消息] hyw
120
- [回复消息<[图片消息]>] hyw -t
121
- [回复消息] hyw 补充: 这个rf的意思是github用户RF-Tar-Railt
122
- [回复消息(hyw插件的输出)] /1 详细点描述
123
- [回复消息(hyw插件的输出>] /那谁有多余解释器?
124
- ```
125
-
126
- ### 选项参数
127
- - `-t` / `--text`: 强制纯文本模式(跳过图片分析,节省 Token 或时间)。
128
-
129
- ```text
130
- hyw -t 一大段话。
131
- ```
132
-
133
- ### 引用回复
134
- 支持引用消息进行追问,机器人会自动读取被引用的消息作为上下文:
135
- - **引用 + 命令**:机器人将理解被引用消息的内容(包括图片)通过 `MessageChain` 操作拼接 `Text`、`Image` 与部分 `Custom`。