entari-plugin-hyw 3.3.2__tar.gz → 3.5.0rc1__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 (126) hide show
  1. entari_plugin_hyw-3.5.0rc1/PKG-INFO +116 -0
  2. entari_plugin_hyw-3.5.0rc1/README.md +87 -0
  3. {entari_plugin_hyw-3.3.2 → entari_plugin_hyw-3.5.0rc1}/pyproject.toml +2 -1
  4. entari_plugin_hyw-3.5.0rc1/src/entari_plugin_hyw/__init__.py +406 -0
  5. entari_plugin_hyw-3.5.0rc1/src/entari_plugin_hyw/assets/card-dist/index.html +135 -0
  6. entari_plugin_hyw-3.5.0rc1/src/entari_plugin_hyw/assets/card-dist/logos/cerebras.svg +9 -0
  7. entari_plugin_hyw-3.5.0rc1/src/entari_plugin_hyw/assets/card-dist/logos/huggingface.png +0 -0
  8. entari_plugin_hyw-3.5.0rc1/src/entari_plugin_hyw/assets/card-dist/logos/xiaomi.png +0 -0
  9. entari_plugin_hyw-3.5.0rc1/src/entari_plugin_hyw/assets/card-dist/vite.svg +1 -0
  10. entari_plugin_hyw-3.5.0rc1/src/entari_plugin_hyw/assets/icon/anthropic.svg +1 -0
  11. entari_plugin_hyw-3.5.0rc1/src/entari_plugin_hyw/assets/icon/cerebras.svg +9 -0
  12. entari_plugin_hyw-3.5.0rc1/src/entari_plugin_hyw/assets/icon/deepseek.png +0 -0
  13. entari_plugin_hyw-3.5.0rc1/src/entari_plugin_hyw/assets/icon/gemini.svg +1 -0
  14. entari_plugin_hyw-3.5.0rc1/src/entari_plugin_hyw/assets/icon/google.svg +1 -0
  15. entari_plugin_hyw-3.5.0rc1/src/entari_plugin_hyw/assets/icon/grok.png +0 -0
  16. entari_plugin_hyw-3.5.0rc1/src/entari_plugin_hyw/assets/icon/huggingface.png +0 -0
  17. entari_plugin_hyw-3.5.0rc1/src/entari_plugin_hyw/assets/icon/microsoft.svg +15 -0
  18. entari_plugin_hyw-3.5.0rc1/src/entari_plugin_hyw/assets/icon/minimax.png +0 -0
  19. entari_plugin_hyw-3.5.0rc1/src/entari_plugin_hyw/assets/icon/mistral.png +0 -0
  20. entari_plugin_hyw-3.5.0rc1/src/entari_plugin_hyw/assets/icon/nvida.png +0 -0
  21. entari_plugin_hyw-3.5.0rc1/src/entari_plugin_hyw/assets/icon/openai.svg +1 -0
  22. entari_plugin_hyw-3.5.0rc1/src/entari_plugin_hyw/assets/icon/openrouter.png +0 -0
  23. entari_plugin_hyw-3.5.0rc1/src/entari_plugin_hyw/assets/icon/perplexity.svg +24 -0
  24. entari_plugin_hyw-3.5.0rc1/src/entari_plugin_hyw/assets/icon/qwen.png +0 -0
  25. entari_plugin_hyw-3.5.0rc1/src/entari_plugin_hyw/assets/icon/xai.png +0 -0
  26. entari_plugin_hyw-3.5.0rc1/src/entari_plugin_hyw/assets/icon/xiaomi.png +0 -0
  27. entari_plugin_hyw-3.5.0rc1/src/entari_plugin_hyw/assets/icon/zai.png +0 -0
  28. entari_plugin_hyw-3.5.0rc1/src/entari_plugin_hyw/card-ui/.gitignore +24 -0
  29. entari_plugin_hyw-3.5.0rc1/src/entari_plugin_hyw/card-ui/README.md +5 -0
  30. entari_plugin_hyw-3.5.0rc1/src/entari_plugin_hyw/card-ui/index.html +16 -0
  31. entari_plugin_hyw-3.5.0rc1/src/entari_plugin_hyw/card-ui/package-lock.json +2342 -0
  32. entari_plugin_hyw-3.5.0rc1/src/entari_plugin_hyw/card-ui/package.json +31 -0
  33. entari_plugin_hyw-3.5.0rc1/src/entari_plugin_hyw/card-ui/public/logos/anthropic.svg +1 -0
  34. entari_plugin_hyw-3.5.0rc1/src/entari_plugin_hyw/card-ui/public/logos/cerebras.svg +9 -0
  35. entari_plugin_hyw-3.5.0rc1/src/entari_plugin_hyw/card-ui/public/logos/deepseek.png +0 -0
  36. entari_plugin_hyw-3.5.0rc1/src/entari_plugin_hyw/card-ui/public/logos/gemini.svg +1 -0
  37. entari_plugin_hyw-3.5.0rc1/src/entari_plugin_hyw/card-ui/public/logos/google.svg +1 -0
  38. entari_plugin_hyw-3.5.0rc1/src/entari_plugin_hyw/card-ui/public/logos/grok.png +0 -0
  39. entari_plugin_hyw-3.5.0rc1/src/entari_plugin_hyw/card-ui/public/logos/huggingface.png +0 -0
  40. entari_plugin_hyw-3.5.0rc1/src/entari_plugin_hyw/card-ui/public/logos/microsoft.svg +15 -0
  41. entari_plugin_hyw-3.5.0rc1/src/entari_plugin_hyw/card-ui/public/logos/minimax.png +0 -0
  42. entari_plugin_hyw-3.5.0rc1/src/entari_plugin_hyw/card-ui/public/logos/mistral.png +0 -0
  43. entari_plugin_hyw-3.5.0rc1/src/entari_plugin_hyw/card-ui/public/logos/nvida.png +0 -0
  44. entari_plugin_hyw-3.5.0rc1/src/entari_plugin_hyw/card-ui/public/logos/openai.svg +1 -0
  45. entari_plugin_hyw-3.5.0rc1/src/entari_plugin_hyw/card-ui/public/logos/openrouter.png +0 -0
  46. entari_plugin_hyw-3.5.0rc1/src/entari_plugin_hyw/card-ui/public/logos/perplexity.svg +24 -0
  47. entari_plugin_hyw-3.5.0rc1/src/entari_plugin_hyw/card-ui/public/logos/qwen.png +0 -0
  48. entari_plugin_hyw-3.5.0rc1/src/entari_plugin_hyw/card-ui/public/logos/xai.png +0 -0
  49. entari_plugin_hyw-3.5.0rc1/src/entari_plugin_hyw/card-ui/public/logos/xiaomi.png +0 -0
  50. entari_plugin_hyw-3.5.0rc1/src/entari_plugin_hyw/card-ui/public/logos/zai.png +0 -0
  51. entari_plugin_hyw-3.5.0rc1/src/entari_plugin_hyw/card-ui/public/vite.svg +1 -0
  52. entari_plugin_hyw-3.5.0rc1/src/entari_plugin_hyw/card-ui/src/App.vue +216 -0
  53. entari_plugin_hyw-3.5.0rc1/src/entari_plugin_hyw/card-ui/src/assets/vue.svg +1 -0
  54. entari_plugin_hyw-3.5.0rc1/src/entari_plugin_hyw/card-ui/src/components/HelloWorld.vue +41 -0
  55. entari_plugin_hyw-3.5.0rc1/src/entari_plugin_hyw/card-ui/src/components/MarkdownContent.vue +330 -0
  56. entari_plugin_hyw-3.5.0rc1/src/entari_plugin_hyw/card-ui/src/components/SectionCard.vue +41 -0
  57. entari_plugin_hyw-3.5.0rc1/src/entari_plugin_hyw/card-ui/src/components/StageCard.vue +163 -0
  58. entari_plugin_hyw-3.5.0rc1/src/entari_plugin_hyw/card-ui/src/main.ts +5 -0
  59. entari_plugin_hyw-3.5.0rc1/src/entari_plugin_hyw/card-ui/src/style.css +8 -0
  60. entari_plugin_hyw-3.5.0rc1/src/entari_plugin_hyw/card-ui/src/types.ts +51 -0
  61. entari_plugin_hyw-3.5.0rc1/src/entari_plugin_hyw/card-ui/tsconfig.app.json +16 -0
  62. entari_plugin_hyw-3.5.0rc1/src/entari_plugin_hyw/card-ui/tsconfig.json +7 -0
  63. entari_plugin_hyw-3.5.0rc1/src/entari_plugin_hyw/card-ui/tsconfig.node.json +26 -0
  64. entari_plugin_hyw-3.5.0rc1/src/entari_plugin_hyw/card-ui/vite.config.ts +16 -0
  65. {entari_plugin_hyw-3.3.2 → entari_plugin_hyw-3.5.0rc1}/src/entari_plugin_hyw/core/config.py +11 -11
  66. {entari_plugin_hyw-3.3.2 → entari_plugin_hyw-3.5.0rc1}/src/entari_plugin_hyw/core/hyw.py +22 -15
  67. {entari_plugin_hyw-3.3.2 → entari_plugin_hyw-3.5.0rc1}/src/entari_plugin_hyw/core/pipeline.py +249 -183
  68. entari_plugin_hyw-3.5.0rc1/src/entari_plugin_hyw/core/render_vue.py +255 -0
  69. entari_plugin_hyw-3.5.0rc1/src/entari_plugin_hyw/test_output/render_0.jpg +0 -0
  70. entari_plugin_hyw-3.5.0rc1/src/entari_plugin_hyw/test_output/render_1.jpg +0 -0
  71. entari_plugin_hyw-3.5.0rc1/src/entari_plugin_hyw/test_output/render_2.jpg +0 -0
  72. entari_plugin_hyw-3.5.0rc1/src/entari_plugin_hyw/test_output/render_3.jpg +0 -0
  73. entari_plugin_hyw-3.5.0rc1/src/entari_plugin_hyw/test_output/render_4.jpg +0 -0
  74. entari_plugin_hyw-3.5.0rc1/src/entari_plugin_hyw/tests/ui_test_output.jpg +0 -0
  75. entari_plugin_hyw-3.5.0rc1/src/entari_plugin_hyw/tests/verify_ui.py +139 -0
  76. {entari_plugin_hyw-3.3.2 → entari_plugin_hyw-3.5.0rc1}/src/entari_plugin_hyw/utils/misc.py +0 -3
  77. {entari_plugin_hyw-3.3.2 → entari_plugin_hyw-3.5.0rc1}/src/entari_plugin_hyw/utils/prompts.py +24 -32
  78. entari_plugin_hyw-3.5.0rc1/src/entari_plugin_hyw/utils/search.py +479 -0
  79. entari_plugin_hyw-3.5.0rc1/src/entari_plugin_hyw.egg-info/PKG-INFO +116 -0
  80. entari_plugin_hyw-3.5.0rc1/src/entari_plugin_hyw.egg-info/SOURCES.txt +103 -0
  81. {entari_plugin_hyw-3.3.2 → entari_plugin_hyw-3.5.0rc1}/src/entari_plugin_hyw.egg-info/requires.txt +1 -0
  82. entari_plugin_hyw-3.3.2/PKG-INFO +0 -142
  83. entari_plugin_hyw-3.3.2/README.md +0 -114
  84. entari_plugin_hyw-3.3.2/src/entari_plugin_hyw/__init__.py +0 -818
  85. entari_plugin_hyw-3.3.2/src/entari_plugin_hyw/assets/libs/highlight.css +0 -10
  86. entari_plugin_hyw-3.3.2/src/entari_plugin_hyw/assets/libs/highlight.js +0 -1213
  87. entari_plugin_hyw-3.3.2/src/entari_plugin_hyw/assets/libs/katex-auto-render.js +0 -1
  88. entari_plugin_hyw-3.3.2/src/entari_plugin_hyw/assets/libs/katex.css +0 -1
  89. entari_plugin_hyw-3.3.2/src/entari_plugin_hyw/assets/libs/katex.js +0 -1
  90. entari_plugin_hyw-3.3.2/src/entari_plugin_hyw/assets/libs/tailwind.css +0 -1
  91. entari_plugin_hyw-3.3.2/src/entari_plugin_hyw/assets/package-lock.json +0 -953
  92. entari_plugin_hyw-3.3.2/src/entari_plugin_hyw/assets/package.json +0 -16
  93. entari_plugin_hyw-3.3.2/src/entari_plugin_hyw/assets/tailwind.config.js +0 -12
  94. entari_plugin_hyw-3.3.2/src/entari_plugin_hyw/assets/tailwind.input.css +0 -235
  95. entari_plugin_hyw-3.3.2/src/entari_plugin_hyw/assets/template.html +0 -157
  96. entari_plugin_hyw-3.3.2/src/entari_plugin_hyw/assets/template.html.bak +0 -157
  97. entari_plugin_hyw-3.3.2/src/entari_plugin_hyw/assets/template.j2 +0 -307
  98. entari_plugin_hyw-3.3.2/src/entari_plugin_hyw/core/render.py +0 -596
  99. entari_plugin_hyw-3.3.2/src/entari_plugin_hyw/core/render.py.bak +0 -926
  100. entari_plugin_hyw-3.3.2/src/entari_plugin_hyw/utils/search.py +0 -241
  101. entari_plugin_hyw-3.3.2/src/entari_plugin_hyw.egg-info/PKG-INFO +0 -142
  102. entari_plugin_hyw-3.3.2/src/entari_plugin_hyw.egg-info/SOURCES.txt +0 -50
  103. {entari_plugin_hyw-3.3.2 → entari_plugin_hyw-3.5.0rc1}/MANIFEST.in +0 -0
  104. {entari_plugin_hyw-3.3.2 → entari_plugin_hyw-3.5.0rc1}/setup.cfg +0 -0
  105. {entari_plugin_hyw-3.3.2/src/entari_plugin_hyw/assets/icon → entari_plugin_hyw-3.5.0rc1/src/entari_plugin_hyw/assets/card-dist/logos}/anthropic.svg +0 -0
  106. {entari_plugin_hyw-3.3.2/src/entari_plugin_hyw/assets/icon → entari_plugin_hyw-3.5.0rc1/src/entari_plugin_hyw/assets/card-dist/logos}/deepseek.png +0 -0
  107. {entari_plugin_hyw-3.3.2/src/entari_plugin_hyw/assets/icon → entari_plugin_hyw-3.5.0rc1/src/entari_plugin_hyw/assets/card-dist/logos}/gemini.svg +0 -0
  108. {entari_plugin_hyw-3.3.2/src/entari_plugin_hyw/assets/icon → entari_plugin_hyw-3.5.0rc1/src/entari_plugin_hyw/assets/card-dist/logos}/google.svg +0 -0
  109. {entari_plugin_hyw-3.3.2/src/entari_plugin_hyw/assets/icon → entari_plugin_hyw-3.5.0rc1/src/entari_plugin_hyw/assets/card-dist/logos}/grok.png +0 -0
  110. {entari_plugin_hyw-3.3.2/src/entari_plugin_hyw/assets/icon → entari_plugin_hyw-3.5.0rc1/src/entari_plugin_hyw/assets/card-dist/logos}/microsoft.svg +0 -0
  111. {entari_plugin_hyw-3.3.2/src/entari_plugin_hyw/assets/icon → entari_plugin_hyw-3.5.0rc1/src/entari_plugin_hyw/assets/card-dist/logos}/minimax.png +0 -0
  112. {entari_plugin_hyw-3.3.2/src/entari_plugin_hyw/assets/icon → entari_plugin_hyw-3.5.0rc1/src/entari_plugin_hyw/assets/card-dist/logos}/mistral.png +0 -0
  113. {entari_plugin_hyw-3.3.2/src/entari_plugin_hyw/assets/icon → entari_plugin_hyw-3.5.0rc1/src/entari_plugin_hyw/assets/card-dist/logos}/nvida.png +0 -0
  114. {entari_plugin_hyw-3.3.2/src/entari_plugin_hyw/assets/icon → entari_plugin_hyw-3.5.0rc1/src/entari_plugin_hyw/assets/card-dist/logos}/openai.svg +0 -0
  115. {entari_plugin_hyw-3.3.2/src/entari_plugin_hyw/assets/icon → entari_plugin_hyw-3.5.0rc1/src/entari_plugin_hyw/assets/card-dist/logos}/openrouter.png +0 -0
  116. {entari_plugin_hyw-3.3.2/src/entari_plugin_hyw/assets/icon → entari_plugin_hyw-3.5.0rc1/src/entari_plugin_hyw/assets/card-dist/logos}/perplexity.svg +0 -0
  117. {entari_plugin_hyw-3.3.2/src/entari_plugin_hyw/assets/icon → entari_plugin_hyw-3.5.0rc1/src/entari_plugin_hyw/assets/card-dist/logos}/qwen.png +0 -0
  118. {entari_plugin_hyw-3.3.2/src/entari_plugin_hyw/assets/icon → entari_plugin_hyw-3.5.0rc1/src/entari_plugin_hyw/assets/card-dist/logos}/xai.png +0 -0
  119. {entari_plugin_hyw-3.3.2/src/entari_plugin_hyw/assets/icon → entari_plugin_hyw-3.5.0rc1/src/entari_plugin_hyw/assets/card-dist/logos}/zai.png +0 -0
  120. {entari_plugin_hyw-3.3.2 → entari_plugin_hyw-3.5.0rc1}/src/entari_plugin_hyw/core/__init__.py +0 -0
  121. {entari_plugin_hyw-3.3.2 → entari_plugin_hyw-3.5.0rc1}/src/entari_plugin_hyw/core/history.py +0 -0
  122. {entari_plugin_hyw-3.3.2 → entari_plugin_hyw-3.5.0rc1}/src/entari_plugin_hyw/utils/__init__.py +0 -0
  123. {entari_plugin_hyw-3.3.2 → entari_plugin_hyw-3.5.0rc1}/src/entari_plugin_hyw/utils/browser.py +0 -0
  124. {entari_plugin_hyw-3.3.2 → entari_plugin_hyw-3.5.0rc1}/src/entari_plugin_hyw/utils/playwright_tool.py +0 -0
  125. {entari_plugin_hyw-3.3.2 → entari_plugin_hyw-3.5.0rc1}/src/entari_plugin_hyw.egg-info/dependency_links.txt +0 -0
  126. {entari_plugin_hyw-3.3.2 → entari_plugin_hyw-3.5.0rc1}/src/entari_plugin_hyw.egg-info/top_level.txt +0 -0
@@ -0,0 +1,116 @@
1
+ Metadata-Version: 2.4
2
+ Name: entari_plugin_hyw
3
+ Version: 3.5.0rc1
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-2.0-flash-exp
79
+ api_key: "your-or-api-key-here"
80
+ # Rendering Configuration
81
+ render_timeout_ms: 6000 # Browser wait timeout
82
+ render_image_timeout_ms: 3000 # Image load wait timeout
83
+ ```
84
+
85
+ ## Usage
86
+
87
+ ### Commands
88
+
89
+ - **Text Query**
90
+ ```text
91
+ /q What's the latest news on Rust 1.83?
92
+ ```
93
+
94
+ - **Image Analysis**
95
+ *(Send an image with command, or reply to an image)*
96
+ ```text
97
+ /q [Image] Explain this error.
98
+ ```
99
+ - **Quote Query**
100
+ ```text
101
+ [quote: User Message] /q
102
+ ```
103
+
104
+ - **Follow-up**
105
+ *Reply to the bot's message to continue the conversation.*
106
+
107
+ ## Documentation for AI/LLMs
108
+
109
+ - [Instruction Guide (English)](docs/README_LLM_EN.md)
110
+ - [指导手册 (简体中文)](docs/README_LLM_CN.md)
111
+
112
+ ---
113
+
114
+ ## License
115
+
116
+ This project is licensed under the MIT License.
@@ -0,0 +1,87 @@
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-2.0-flash-exp
50
+ api_key: "your-or-api-key-here"
51
+ # Rendering Configuration
52
+ render_timeout_ms: 6000 # Browser wait timeout
53
+ render_image_timeout_ms: 3000 # Image load wait timeout
54
+ ```
55
+
56
+ ## Usage
57
+
58
+ ### Commands
59
+
60
+ - **Text Query**
61
+ ```text
62
+ /q What's the latest news on Rust 1.83?
63
+ ```
64
+
65
+ - **Image Analysis**
66
+ *(Send an image with command, or reply to an image)*
67
+ ```text
68
+ /q [Image] Explain this error.
69
+ ```
70
+ - **Quote Query**
71
+ ```text
72
+ [quote: User Message] /q
73
+ ```
74
+
75
+ - **Follow-up**
76
+ *Reply to the bot's message to continue the conversation.*
77
+
78
+ ## Documentation for AI/LLMs
79
+
80
+ - [Instruction Guide (English)](docs/README_LLM_EN.md)
81
+ - [指导手册 (简体中文)](docs/README_LLM_CN.md)
82
+
83
+ ---
84
+
85
+ ## License
86
+
87
+ 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.2"
7
+ version = "3.5.0-rc1"
8
8
  description = "Use large language models to interpret chat messages"
9
9
  authors = [{name = "kumoSleeping", email = "zjr2992@outlook.com"}]
10
10
  dependencies = [
@@ -14,6 +14,7 @@ dependencies = [
14
14
  "markdown>=3.10",
15
15
  "crawl4ai>=0.7.8",
16
16
  "jinja2>=3.0",
17
+ "ddgs>=9.10.0",
17
18
  ]
18
19
  requires-python = ">=3.10"
19
20
  readme = "README.md"
@@ -0,0 +1,406 @@
1
+ from dataclasses import dataclass, field
2
+ from typing import List, Dict, Any, Optional, Union
3
+ import time
4
+
5
+ from arclet.alconna import Alconna, Args, AllParam, CommandMeta, Option, Arparma, MultiVar, store_true
6
+ from arclet.entari import metadata, listen, Session, plugin_config, BasicConfModel, plugin, command
7
+ from arclet.entari import MessageChain, Text, Image, MessageCreatedEvent, Quote, At
8
+ from satori.element import Custom
9
+ from loguru import logger
10
+ import arclet.letoderea as leto
11
+ from arclet.entari.event.command import CommandReceive
12
+
13
+ from .core.hyw import HYW
14
+ from .core.history import HistoryManager
15
+ from .core.render_vue import ContentRenderer
16
+ from .utils.misc import process_onebot_json, process_images, resolve_model_name
17
+ from arclet.entari.event.lifespan import Cleanup, Startup
18
+
19
+ import os
20
+ import secrets
21
+ import base64
22
+
23
+ import re
24
+
25
+ class _RecentEventDeduper:
26
+ def __init__(self, ttl_seconds: float = 30.0, max_size: int = 2048):
27
+ self.ttl_seconds = ttl_seconds
28
+ self.max_size = max_size
29
+ self._seen: Dict[str, float] = {}
30
+
31
+ def seen_recently(self, key: str) -> bool:
32
+ now = time.time()
33
+ if len(self._seen) > self.max_size:
34
+ self._prune(now)
35
+ ts = self._seen.get(key)
36
+ if ts is None or now - ts > self.ttl_seconds:
37
+ self._seen[key] = now
38
+ return False
39
+ return True
40
+
41
+ def _prune(self, now: float):
42
+ expired = [k for k, ts in self._seen.items() if now - ts > self.ttl_seconds]
43
+ for k in expired:
44
+ self._seen.pop(k, None)
45
+ if len(self._seen) > self.max_size:
46
+ for k, _ in sorted(self._seen.items(), key=lambda kv: kv[1])[: len(self._seen) - self.max_size]:
47
+ self._seen.pop(k, None)
48
+
49
+ _event_deduper = _RecentEventDeduper()
50
+
51
+ @dataclass
52
+ class HywConfig(BasicConfModel):
53
+ admins: List[str] = field(default_factory=list)
54
+ models: List[Dict[str, Any]] = field(default_factory=list)
55
+ question_command: str = "/q"
56
+ model_name: Optional[str] = None
57
+ api_key: Optional[str] = None
58
+ base_url: str = "https://openrouter.ai/api/v1"
59
+ vision_model_name: Optional[str] = None
60
+ vision_api_key: Optional[str] = None
61
+ language: str = "Simplified Chinese"
62
+ vision_base_url: Optional[str] = None
63
+ instruct_model_name: Optional[str] = None
64
+ instruct_api_key: Optional[str] = None
65
+ instruct_base_url: Optional[str] = None
66
+ search_base_url: str = "https://lite.duckduckgo.com/lite/?q={query}"
67
+ image_search_base_url: str = "https://duckduckgo.com/?q={query}&iax=images&ia=images"
68
+ headless: bool = False
69
+ save_conversation: bool = False
70
+ icon: str = "openai"
71
+ render_timeout_ms: int = 6000
72
+ render_image_timeout_ms: int = 3000
73
+ extra_body: Optional[Dict[str, Any]] = None
74
+ vision_extra_body: Optional[Dict[str, Any]] = None
75
+ instruct_extra_body: Optional[Dict[str, Any]] = None
76
+ enable_browser_fallback: bool = False
77
+ reaction: bool = False
78
+ quote: bool = True
79
+ temperature: float = 0.4
80
+ # Billing configuration (price per million tokens)
81
+ input_price: Optional[float] = None # $ per 1M input tokens
82
+ output_price: Optional[float] = None # $ per 1M output tokens
83
+ # Vision model pricing overrides (defaults to main model pricing if not set)
84
+ vision_input_price: Optional[float] = None
85
+ vision_output_price: Optional[float] = None
86
+ # Instruct model pricing overrides (defaults to main model pricing if not set)
87
+ instruct_input_price: Optional[float] = None
88
+ instruct_output_price: Optional[float] = None
89
+ # Provider Names
90
+ search_name: str = "DuckDuckGo"
91
+ search_provider: str = "crawl4ai" # crawl4ai | httpx | ddgs
92
+ model_provider: Optional[str] = None
93
+ vision_model_provider: Optional[str] = None
94
+ instruct_model_provider: Optional[str] = None
95
+
96
+
97
+
98
+ conf = plugin_config(HywConfig)
99
+ history_manager = HistoryManager()
100
+ renderer = ContentRenderer()
101
+ hyw = HYW(config=conf)
102
+
103
+
104
+ @listen(Startup)
105
+ async def _hyw_startup():
106
+ try:
107
+ # Pre-launch browser
108
+ logger.info("HYW: Pre-launching renderer browser...")
109
+ await renderer.start()
110
+ except Exception as e:
111
+ logger.warning(f"HYW: Renderer warm-up failed: {e}")
112
+
113
+ @listen(Cleanup, once=True)
114
+ async def _hyw_cleanup():
115
+ try:
116
+ await hyw.close()
117
+ await renderer.close()
118
+ except Exception as e:
119
+ logger.warning(f"HYW cleanup error: {e}")
120
+
121
+ class GlobalCache:
122
+ models_image_path: Optional[str] = None
123
+
124
+ global_cache = GlobalCache()
125
+
126
+ from satori.exception import ActionFailed
127
+ from satori.adapters.onebot11.reverse import _Connection
128
+
129
+ # Monkeypatch to suppress ActionFailed for get_msg
130
+ original_call_api = _Connection.call_api
131
+
132
+ async def patched_call_api(self, action: str, params: dict = None):
133
+ try:
134
+ return await original_call_api(self, action, params)
135
+ except ActionFailed as e:
136
+ if action == "get_msg":
137
+ logger.warning(f"Suppressed ActionFailed for get_msg: {e}")
138
+ return None
139
+ raise e
140
+
141
+ _Connection.call_api = patched_call_api
142
+
143
+ EMOJI_TO_CODE = {
144
+ "✨": "10024",
145
+ "✅": "10004",
146
+ "❌": "10060"
147
+ }
148
+
149
+ async def react(session: Session, emoji: str):
150
+ if not conf.reaction: return
151
+ try:
152
+ if session.event.login.platform == "onebot":
153
+ code = EMOJI_TO_CODE.get(emoji, "10024")
154
+ # OneBot specific reaction
155
+ await session.account.protocol.call_api(
156
+ "internal/set_group_reaction",
157
+ {
158
+ "group_id": str(session.guild.id),
159
+ "message_id": str(session.event.message.id),
160
+ "code": code,
161
+ "is_add": True
162
+ }
163
+ )
164
+ else:
165
+ # Standard Satori reaction
166
+ await session.reaction_create(emoji=emoji)
167
+ except ActionFailed:
168
+ pass
169
+ except Exception as e:
170
+ logger.warning(f"Reaction failed: {e}")
171
+
172
+ async def process_request(session: Session[MessageCreatedEvent], all_param: Optional[MessageChain] = None,
173
+ selected_model: Optional[str] = None, selected_vision_model: Optional[str] = None,
174
+ conversation_key_override: Optional[str] = None, local_mode: bool = False):
175
+ logger.info(f"Processing request: {all_param}")
176
+ mc = MessageChain(all_param)
177
+ logger.info(f"reply: {session.reply}")
178
+ if session.reply:
179
+ try:
180
+ # Check if reply is from self (the bot)
181
+ # 1. Check by Message ID (reliable for bot's own messages if recorded)
182
+ reply_msg_id = str(session.reply.origin.id) if hasattr(session.reply.origin, 'id') else None
183
+ is_bot = False
184
+
185
+ if reply_msg_id and history_manager.is_bot_message(reply_msg_id):
186
+ is_bot = True
187
+ logger.info(f"Reply target {reply_msg_id} identified as bot message via history")
188
+
189
+ if is_bot:
190
+ logger.info("Reply is from me - ignoring content")
191
+ else:
192
+ logger.info(f"Reply is from user (or unknown) - including content")
193
+ mc.extend(MessageChain(" ") + session.reply.origin.message)
194
+ except Exception as e:
195
+ logger.warning(f"Failed to process reply origin: {e}")
196
+ mc.extend(MessageChain(" ") + session.reply.origin.message)
197
+
198
+ # Filter and reconstruct MessageChain
199
+ filtered_elements = mc.get(Text) + mc.get(Image) + mc.get(Custom)
200
+ mc = MessageChain(filtered_elements)
201
+ logger.info(f"mc: {mc}")
202
+
203
+ text_content = str(mc.get(Text)).strip()
204
+ # Remove HTML image tags from text content to prevent "unreasonable code behavior"
205
+ text_content = re.sub(r'<img[^>]+>', '', text_content, flags=re.IGNORECASE)
206
+
207
+ if not text_content and not mc.get(Image) and not mc.get(Custom):
208
+ return
209
+
210
+ # History & Context
211
+ hist_key = conversation_key_override
212
+ if not hist_key and session.reply and hasattr(session.reply.origin, 'id'):
213
+ hist_key = history_manager.get_conversation_id(str(session.reply.origin.id))
214
+
215
+ hist_payload = history_manager.get_history(hist_key) if hist_key else []
216
+ meta = history_manager.get_metadata(hist_key) if hist_key else {}
217
+ context_id = f"guild_{session.guild.id}" if session.guild else f"user_{session.user.id}"
218
+
219
+ if conf.reaction: await react(session, "✨")
220
+
221
+ try:
222
+ msg_text = str(mc.get(Text)).strip() if mc.get(Text) else ""
223
+ msg_text = re.sub(r'<img[^>]+>', '', msg_text, flags=re.IGNORECASE)
224
+
225
+ # If message is empty but has images, use a placeholder
226
+ if not msg_text and (mc.get(Image) or mc.get(Custom)):
227
+ msg_text = "[图片]"
228
+
229
+ for custom in [e for e in mc if isinstance(e, Custom)]:
230
+ if custom.tag == 'onebot:json':
231
+ if decoded := process_onebot_json(custom.attributes()): msg_text += f"\n{decoded}"
232
+ break
233
+
234
+ # Model Selection (Step 1)
235
+ # Resolve model names from config if they are short names/keywords
236
+ model = selected_model or meta.get("model")
237
+ if model and model != "off":
238
+ resolved, err = resolve_model_name(model, conf.models)
239
+ if resolved:
240
+ model = resolved
241
+ elif err:
242
+ logger.warning(f"Model resolution warning for {model}: {err}")
243
+
244
+ vision_model = selected_vision_model or meta.get("vision_model")
245
+ if vision_model and vision_model != "off":
246
+ resolved_v, err_v = resolve_model_name(vision_model, conf.models)
247
+ if resolved_v:
248
+ vision_model = resolved_v
249
+ elif err_v:
250
+ logger.warning(f"Vision model resolution warning for {vision_model}: {err_v}")
251
+
252
+ images, err = await process_images(mc, vision_model)
253
+
254
+ # Call Agent (Step 1)
255
+ # Sanitize user_input: use extracted text only
256
+ safe_input = msg_text
257
+
258
+ resp = await hyw.agent(safe_input, conversation_history=hist_payload, images=images,
259
+ selected_model=model, selected_vision_model=vision_model, local_mode=local_mode)
260
+
261
+ # Step 1 Results
262
+ step1_vision_model = resp.get("vision_model_used")
263
+ step1_model = resp.get("model_used")
264
+ step1_history = resp.get("conversation_history", [])
265
+ step1_stats = resp.get("stats", {})
266
+
267
+ final_resp = resp
268
+
269
+ # Step 2 (Optional)
270
+
271
+
272
+
273
+ # Extract Response Data
274
+ content = final_resp.get("llm_response", "")
275
+ structured = final_resp.get("structured_response", {})
276
+
277
+ # Render
278
+ import tempfile
279
+ with tempfile.NamedTemporaryFile(suffix=".jpg", delete=False) as tf:
280
+ output_path = tf.name
281
+ model_used = final_resp.get("model_used")
282
+
283
+ # Determine session short code
284
+ if hist_key:
285
+ display_session_id = history_manager.get_code_by_key(hist_key)
286
+ if not display_session_id:
287
+ display_session_id = history_manager.generate_short_code()
288
+ else:
289
+ display_session_id = history_manager.generate_short_code()
290
+
291
+ # Use stats_list if available, otherwise standard stats
292
+ stats_to_render = final_resp.get("stats_list", final_resp.get("stats", {}))
293
+
294
+ render_ok = await renderer.render(
295
+ markdown_content=content,
296
+ output_path=output_path,
297
+ stats=stats_to_render,
298
+ references=structured.get("references", []),
299
+ page_references=structured.get("page_references", []),
300
+ image_references=structured.get("image_references", []),
301
+ stages_used=final_resp.get("stages_used", []),
302
+ image_timeout=conf.render_image_timeout_ms,
303
+ )
304
+
305
+ # Send & Save
306
+ if not render_ok:
307
+ logger.error("Render failed; skipping reply. Check Crawl4AI rendering status.")
308
+ if os.path.exists(output_path):
309
+ try:
310
+ os.remove(output_path)
311
+ except Exception as exc:
312
+ logger.warning(f"Failed to delete render output {output_path}: {exc}")
313
+ sent = None
314
+ else:
315
+ # Convert to base64
316
+ with open(output_path, "rb") as f:
317
+ img_data = base64.b64encode(f.read()).decode()
318
+
319
+ # Build single reply chain (image only now)
320
+ elements = []
321
+ elements.append(Image(src=f'data:image/png;base64,{img_data}'))
322
+
323
+ msg_chain = MessageChain(*elements)
324
+
325
+ if conf.quote:
326
+ msg_chain = MessageChain(Quote(session.event.message.id)) + msg_chain
327
+
328
+ # Use reply_to instead of manual Quote insertion to avoid ActionFailed errors
329
+ sent = await session.send(msg_chain)
330
+
331
+ sent_id = next((str(e.id) for e in sent if hasattr(e, 'id')), None) if sent else None
332
+ msg_id = str(session.event.message.id) if hasattr(session.event, 'message') else str(session.event.id)
333
+ related = [msg_id] + ([str(session.reply.origin.id)] if session.reply and hasattr(session.reply.origin, 'id') else [])
334
+
335
+ history_manager.remember(
336
+ sent_id,
337
+ final_resp.get("conversation_history", []),
338
+ related,
339
+ {
340
+ "model": model_used,
341
+ "trace_markdown": final_resp.get("trace_markdown"),
342
+ },
343
+ context_id,
344
+ code=display_session_id,
345
+ )
346
+
347
+ if conf.save_conversation and sent_id:
348
+ history_manager.save_to_disk(sent_id)
349
+
350
+
351
+ except Exception as e:
352
+ logger.exception(f"Error: {e}")
353
+ err_msg = f"Error: {e}"
354
+ if conf.quote:
355
+ await session.send([Quote(session.event.message.id), err_msg])
356
+ else:
357
+ await session.send(err_msg)
358
+
359
+ # Save conversation on error if response was generated
360
+ if 'resp' in locals() and resp and conf.save_conversation:
361
+ try:
362
+ # Use a temporary ID for error cases
363
+ error_id = f"error_{int(time.time())}_{secrets.token_hex(4)}"
364
+ history_manager.remember(error_id, resp.get("conversation_history", []), [], {"model": model_used if 'model_used' in locals() else "unknown", "error": str(e)}, context_id, code=display_session_id if 'display_session_id' in locals() else None)
365
+ history_manager.save_to_disk(error_id)
366
+ logger.info(f"Saved error conversation to {error_id}")
367
+ except Exception as save_err:
368
+ logger.error(f"Failed to save error conversation: {save_err}")
369
+
370
+
371
+
372
+ # Main Command (Question)
373
+ alc = Alconna(
374
+ conf.question_command,
375
+ Args["all_param;?", AllParam],
376
+ )
377
+
378
+ @command.on(alc)
379
+ async def handle_question_command(session: Session[MessageCreatedEvent], result: Arparma):
380
+ """Handle main Question command"""
381
+ try:
382
+ # logger.info(f"Question Command Triggered. Message: {result}")
383
+ mid = str(session.event.message.id) if getattr(session.event, "message", None) else str(session.event.id)
384
+ dedupe_key = f"{getattr(session.account, 'id', 'account')}:{mid}"
385
+ if _event_deduper.seen_recently(dedupe_key):
386
+ logger.warning(f"Duplicate command event ignored: {dedupe_key}")
387
+ return
388
+ except Exception:
389
+ pass
390
+
391
+ logger.info(f"Question Command Triggered. Message: {session.event.message}")
392
+
393
+ args = result.all_matched_args
394
+ logger.info(f"Matched Args: {args}")
395
+
396
+ # Only all_param is supported now
397
+ # Context ID for history lookup is automatically handled in process_request
398
+
399
+ await process_request(session, args.get("all_param"), selected_model=None, selected_vision_model=None, conversation_key_override=None, local_mode=False)
400
+
401
+ metadata("hyw", author=[{"name": "kumoSleeping", "email": "zjr2992@outlook.com"}], version="3.5.0-rc1", config=HywConfig)
402
+
403
+ @leto.on(CommandReceive)
404
+ async def remove_at(content: MessageChain):
405
+ content = content.lstrip(At)
406
+ return content