entari-plugin-hyw 3.3.1__py3-none-any.whl → 3.3.2__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.

Potentially problematic release.


This version of entari-plugin-hyw might be problematic. Click here for more details.

Files changed (48) hide show
  1. entari_plugin_hyw/__init__.py +763 -309
  2. entari_plugin_hyw/assets/icon/anthropic.svg +1 -0
  3. entari_plugin_hyw/assets/icon/deepseek.png +0 -0
  4. entari_plugin_hyw/assets/icon/gemini.svg +1 -0
  5. entari_plugin_hyw/assets/icon/google.svg +1 -0
  6. entari_plugin_hyw/assets/icon/grok.png +0 -0
  7. entari_plugin_hyw/assets/icon/microsoft.svg +15 -0
  8. entari_plugin_hyw/assets/icon/minimax.png +0 -0
  9. entari_plugin_hyw/assets/icon/mistral.png +0 -0
  10. entari_plugin_hyw/assets/icon/nvida.png +0 -0
  11. entari_plugin_hyw/assets/icon/openai.svg +1 -0
  12. entari_plugin_hyw/assets/icon/openrouter.png +0 -0
  13. entari_plugin_hyw/assets/icon/perplexity.svg +24 -0
  14. entari_plugin_hyw/assets/icon/qwen.png +0 -0
  15. entari_plugin_hyw/assets/icon/xai.png +0 -0
  16. entari_plugin_hyw/assets/icon/zai.png +0 -0
  17. entari_plugin_hyw/assets/libs/highlight.css +10 -0
  18. entari_plugin_hyw/assets/libs/highlight.js +1213 -0
  19. entari_plugin_hyw/assets/libs/katex-auto-render.js +1 -0
  20. entari_plugin_hyw/assets/libs/katex.css +1 -0
  21. entari_plugin_hyw/assets/libs/katex.js +1 -0
  22. entari_plugin_hyw/assets/libs/tailwind.css +1 -0
  23. entari_plugin_hyw/assets/package-lock.json +953 -0
  24. entari_plugin_hyw/assets/package.json +16 -0
  25. entari_plugin_hyw/assets/tailwind.config.js +12 -0
  26. entari_plugin_hyw/assets/tailwind.input.css +235 -0
  27. entari_plugin_hyw/assets/template.html +157 -0
  28. entari_plugin_hyw/assets/template.html.bak +157 -0
  29. entari_plugin_hyw/assets/template.j2 +307 -0
  30. entari_plugin_hyw/core/__init__.py +0 -0
  31. entari_plugin_hyw/core/config.py +35 -0
  32. entari_plugin_hyw/core/history.py +146 -0
  33. entari_plugin_hyw/core/hyw.py +41 -0
  34. entari_plugin_hyw/core/pipeline.py +1065 -0
  35. entari_plugin_hyw/core/render.py +596 -0
  36. entari_plugin_hyw/core/render.py.bak +926 -0
  37. entari_plugin_hyw/utils/__init__.py +2 -0
  38. entari_plugin_hyw/utils/browser.py +40 -0
  39. entari_plugin_hyw/utils/misc.py +93 -0
  40. entari_plugin_hyw/utils/playwright_tool.py +36 -0
  41. entari_plugin_hyw/utils/prompts.py +129 -0
  42. entari_plugin_hyw/utils/search.py +241 -0
  43. {entari_plugin_hyw-3.3.1.dist-info → entari_plugin_hyw-3.3.2.dist-info}/METADATA +20 -28
  44. entari_plugin_hyw-3.3.2.dist-info/RECORD +46 -0
  45. entari_plugin_hyw/hyw_core.py +0 -700
  46. entari_plugin_hyw-3.3.1.dist-info/RECORD +0 -6
  47. {entari_plugin_hyw-3.3.1.dist-info → entari_plugin_hyw-3.3.2.dist-info}/WHEEL +0 -0
  48. {entari_plugin_hyw-3.3.1.dist-info → entari_plugin_hyw-3.3.2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,307 @@
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <link href="https://cdn.jsdelivr.net/npm/remixicon@4.1.0/fonts/remixicon.css" rel="stylesheet"/>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <title>Entari Render</title>
9
+ <!-- @formatter:off -->
10
+ <!-- prettier-ignore -->
11
+ <style>{{ tailwind_css | safe }}</style>
12
+ <style>{{ highlight_css | safe }}</style>
13
+ <script>{{ highlight_js | safe }}</script>
14
+ <style>{{ katex_css | safe }}</style>
15
+ <script>{{ katex_js | safe }}</script>
16
+ <script>{{ katex_auto_render_js | safe }}</script>
17
+ <!-- @formatter:on -->
18
+
19
+ </head>
20
+
21
+ <body class="bg-[#f2f2f2] p-0 box-border m-0 font-sans text-gray-800">
22
+ <div id="main-container" class="w-full max-w-[450px] flex flex-col gap-0 mx-auto bg-[#f2f2f2] p-0 font-sans h-fit pb-8">
23
+
24
+ {# --- MACROS --- #}
25
+
26
+ {% macro icon_container(icon_html, box_class="bg-gray-50 rounded-lg border border-gray-100", size_class="w-10 h-10") %}
27
+ <div class="flex items-center justify-center {{ size_class }} {{ box_class }} shrink-0">
28
+ {{ icon_html | safe }}
29
+ </div>
30
+ {% endmacro %}
31
+
32
+ {% macro card_header(title, icon_html=None, subtitle_html=None, is_plain=False, icon_box_class=None) %}
33
+ {% set container_class = "flex items-center gap-3" if is_plain else "flex items-center gap-3 pb-3 mb-3 border-gray-100 border-b" %}
34
+ {% set default_box_class = icon_box_class if icon_box_class else "bg-gray-50 rounded-lg border border-gray-100" %}
35
+ <div class="{{ container_class }}">
36
+ {{ icon_container(icon_html, box_class=default_box_class) }}
37
+ <div class="flex flex-col min-w-0">
38
+ <div class="text-sm font-bold text-gray-900 uppercase tracking-wide whitespace-nowrap overflow-hidden text-ellipsis">{{ title }}</div>
39
+ {% if subtitle_html and not is_plain %}
40
+ {{ subtitle_html | safe }}
41
+ {% endif %}
42
+ </div>
43
+ </div>
44
+ {% endmacro %}
45
+
46
+ {% macro list_card(icon_html, title_html, subtitle_html=None, link_url=None, right_content_html=None, is_compact=False, icon_box_class="bg-gray-50 rounded-md shrink-0 ring-1 ring-inset ring-black/5") %}
47
+ {% set tag = "a" if link_url else "div" %}
48
+ {% set href_attr = 'href="' ~ link_url ~ '" target="_blank"' if link_url else "" %}
49
+ {% set hover_class = "transition-colors hover:bg-gray-50" if link_url else "" %}
50
+
51
+ {% set padding_class = "p-2.5" if is_compact else "px-4 py-3.5" %}
52
+ {% set align_class = "items-center" if is_compact else "items-start" %}
53
+ {% set icon_size = "w-6 h-6" if is_compact else "w-8 h-8" %}
54
+
55
+ <{{ tag }} {{ href_attr | safe }} class="flex {{ align_class }} gap-3 {{ padding_class }} rounded-lg border border-gray-100 bg-white shadow-sm no-underline text-inherit {{ hover_class }}">
56
+ <div class="flex items-center justify-center {{ icon_size }} {{ icon_box_class }}">
57
+ {{ icon_html | safe }}
58
+ </div>
59
+ <div class="flex flex-col flex-1 min-w-0 gap-0.5">
60
+ <div class="flex items-center gap-2 leading-tight min-w-0">
61
+ {{ title_html | safe }}
62
+ </div>
63
+ {% if subtitle_html %}
64
+ <div>{{ subtitle_html | safe }}</div>
65
+ {% endif %}
66
+ </div>
67
+ {% if right_content_html %}
68
+ <div class="shrink-0 ml-2">{{ right_content_html | safe }}</div>
69
+ {% endif %}
70
+ </{{ tag }}>
71
+ {% endmacro %}
72
+
73
+
74
+ {# --- MAIN CONTENT --- #}
75
+
76
+ <!-- Header: Feature Icons / Badges (Centered) -->
77
+ <!-- Increased top padding from pt-5 to pt-7 for more whitespace -->
78
+ <div class="pt-7 pb-5 px-6 flex justify-between items-center relative z-20">
79
+ <!-- Total Time Badge -->
80
+ <div class="flex items-center gap-1.5 px-3 py-1.5 bg-white/60 backdrop-blur rounded-lg shadow-sm border border-white/50">
81
+ <i class="ri-time-line text-gray-700 font-bold text-[13px]"></i>
82
+ <span class="text-[13px] font-bold text-gray-900 tracking-wide">{{ "%.1f"|format(total_time) }}s</span>
83
+ </div>
84
+ </div>
85
+
86
+ <!-- Response Card (Content First) -->
87
+ <div class="bg-[#f2f2f2] rounded-2xl px-5 pt-0 pb-2 overflow-hidden">
88
+ <div id="markdown-content" class="markdown-body text-[15px] leading-relaxed text-gray-800 [&>*:first-child]:!mt-0">
89
+ {{ content_html | safe }}
90
+ </div>
91
+ </div>
92
+
93
+ <!-- Speculation Card (Optional) -->
94
+ {% if suggestions %}
95
+ <div class="flex flex-col gap-2 bg-[#f2f2f2] rounded-2xl px-5 py-2 overflow-hidden">
96
+ {% set sug_icon %}
97
+ <i class="ri-magic-line text-lg text-pink-600"></i>
98
+ {% endset %}
99
+ {{ card_header("SUGGESTIONS", icon_html=sug_icon, is_plain=True) }}
100
+
101
+ <div class="grid grid-cols-2 gap-2.5">
102
+ {% for sug in suggestions %}
103
+ <div class="flex items-baseline gap-2 text-sm text-gray-600 px-3.5 py-2.5 bg-white/80 backdrop-blur-sm rounded-full shadow-sm hover:shadow transition-shadow cursor-default">
104
+ <span class="text-gray-500 font-mono font-bold text-[13px] whitespace-nowrap">{{ loop.index }}</span>
105
+ <span class="flex-1 text-[13px] text-gray-600 font-medium whitespace-nowrap overflow-hidden text-ellipsis">{{ sug }}</span>
106
+ </div>
107
+ {% endfor %}
108
+ </div>
109
+ </div>
110
+ {% endif %}
111
+
112
+ <!-- Pipeline & Children (Nested) -->
113
+ <div class="bg-[#f2f2f2] rounded-2xl px-5 pt-2 pb-5 overflow-hidden flex flex-col gap-4">
114
+
115
+ {% if stages %}
116
+ {% for stage in stages %}
117
+ <div>
118
+ {# Stage Card #}
119
+ {% set color_class = "bg-white text-gray-700 border-gray-200 shadow-sm" %}
120
+
121
+ {% set icon_box_class = color_class + " rounded-md border shrink-0" %}
122
+
123
+ {% set title_html %}
124
+ <span class="text-[13px] font-bold uppercase text-gray-700 shrink-0">{{ stage.name }}</span>
125
+ <span class="text-[13px] font-medium text-gray-700 truncate min-w-0" title="{{ stage.model }}">{{ stage.model_short }}</span>
126
+ <span class="ml-auto text-[12px] text-gray-400 shrink-0 truncate max-w-[80px]">{{ stage.provider }}</span>
127
+ {% endset %}
128
+
129
+ {% set stats_html %}
130
+ <div class="flex items-center gap-3 text-[12px] text-gray-500 font-mono mt-0.5">
131
+ <span>{{ stage.time_str }}</span><span>{{ stage.cost_str }}</span>
132
+ </div>
133
+ {% endset %}
134
+
135
+ {{ list_card(stage.icon_html, title_html, subtitle_html=stats_html, is_compact=True, icon_box_class=icon_box_class) }}
136
+
137
+ {# Nested Children (Indent & Connect) #}
138
+ {% if stage.references or stage.flow_steps or stage.crawled_pages %}
139
+ <div class="ml-4 pl-4 border-l-2 border-gray-200 mt-2 flex flex-col gap-2">
140
+
141
+ {# References #}
142
+ {% if stage.references %}
143
+ <div class="text-[12px] uppercase font-bold text-blue-600 tracking-wider mb-1 mt-1">Search Results</div>
144
+ {% for ref in stage.references %}
145
+ {% set favicon_url = "https://www.google.com/s2/favicons?domain=" + ref.domain + "&sz=32" %}
146
+
147
+ {% set ref_icon %}
148
+ <img src="{{ favicon_url }}" class="w-3.5 h-3.5 rounded-sm opacity-80 decoration-0">
149
+ {% endset %}
150
+
151
+ {% set ref_icon_box = "bg-white rounded border border-gray-100 w-6 h-6 shrink-0" %}
152
+
153
+ {% set title_html = '<div class="text-[13px] font-medium text-gray-900 truncate underline decoration-gray-300 decoration-1 underline-offset-2 hover:text-black hover:decoration-gray-500 transition-colors">' + ref.title + '</div>' %}
154
+ {% set subtitle_html = '<div class="text-[12px] text-gray-700 truncate">' + ref.domain + '</div>' %}
155
+ {% set right_html = '<div class="flex items-center justify-center min-w-[16px] h-4 px-0.5 text-[10px] font-bold text-blue-600 bg-blue-50 border border-blue-200 rounded">' + (loop.index|string) + '</div>' %}
156
+
157
+ {{ list_card(ref_icon, title_html, subtitle_html=subtitle_html, link_url=ref.url, right_content_html=right_html, is_compact=True, icon_box_class=ref_icon_box) }}
158
+ {% endfor %}
159
+ {% endif %}
160
+
161
+ {# Flow Steps #}
162
+ {% if stage.flow_steps %}
163
+ <div class="text-[12px] uppercase font-bold text-orange-600 tracking-wider mb-1 mt-1">Flow</div>
164
+ {% for step in stage.flow_steps %}
165
+ {% set icon_box_class = "rounded-md border border-gray-100 bg-white text-gray-500 shrink-0" %}
166
+
167
+ {% set title_html = '<div class="text-[13px] font-semibold text-gray-900 underline decoration-gray-300 decoration-1 underline-offset-2 truncate">' + step.description + '</div>' %}
168
+ {% set subtitle_html = '<div class="text-[12px] text-gray-700 leading-tight truncate">' + step.description + '</div>' %}
169
+ {% set right_html = '<div class="flex items-center justify-center min-w-[16px] h-4 px-0.5 text-[10px] font-bold text-orange-700 bg-orange-50 border border-orange-200 rounded">' + ('abcdefghijklmnopqrstuvwxyz'[loop.index0]) + '</div>' %}
170
+
171
+ {{ list_card(step.icon_svg, title_html, subtitle_html=subtitle_html, right_content_html=right_html, is_compact=True, icon_box_class=icon_box_class) }}
172
+ {% endfor %}
173
+ {% endif %}
174
+
175
+ {# Crawled Pages #}
176
+ {% if stage.crawled_pages %}
177
+ <div class="text-[12px] uppercase font-bold text-blue-600 tracking-wider mb-1 mt-1">Fetched Pages</div>
178
+ {% for page in stage.crawled_pages %}
179
+ {% set domain = page.url.split('/')[2] if page.url.split('/')|length > 2 else '' %}
180
+ {% set favicon_url = "https://www.google.com/s2/favicons?domain=" + domain + "&sz=32" %}
181
+
182
+ {% set page_icon %}
183
+ <img src="{{ favicon_url }}" class="w-3.5 h-3.5 rounded-sm opacity-80">
184
+ {% endset %}
185
+
186
+ {% set page_icon_box = "bg-white rounded border border-gray-100 w-6 h-6 shrink-0" %}
187
+
188
+ {% set title_html = '<div class="text-[13px] font-medium text-gray-900 truncate underline decoration-gray-300 decoration-1 underline-offset-2 hover:text-black hover:decoration-gray-500 transition-colors">' + page.title + '</div>' %}
189
+ {% set subtitle_html = '<div class="text-[12px] text-gray-500 truncate">' + domain + '</div>' %}
190
+ {% set right_html = '<div class="flex items-center justify-center min-w-[16px] h-4 px-0.5 text-[10px] font-bold text-orange-600 bg-orange-50 border border-orange-200 rounded">' + (loop.index|string) + '</div>' %}
191
+
192
+ {{ list_card(page_icon, title_html, subtitle_html=subtitle_html, link_url=page.url, right_content_html=right_html, is_compact=True, icon_box_class=page_icon_box) }}
193
+ {% endfor %}
194
+ {% endif %}
195
+
196
+ </div>
197
+ {% endif %}
198
+ </div>
199
+ {% endfor %}
200
+ {% endif %}
201
+
202
+ </div>
203
+
204
+ <script>window.REFERENCES = {{ references_json | safe }};</script>
205
+
206
+ <script>
207
+ document.addEventListener("DOMContentLoaded", () => {
208
+ // Initialize Syntax Highlighting
209
+ if (typeof hljs !== 'undefined') {
210
+ hljs.highlightAll();
211
+ }
212
+
213
+ // Render Math (KaTeX)
214
+ const contentDiv = document.getElementById("markdown-content");
215
+ if(typeof renderMathInElement !== 'undefined') {
216
+ renderMathInElement(contentDiv, {
217
+ delimiters: [
218
+ { left: "$$", right: "$$", display: true },
219
+ { left: "$", right: "$", display: false },
220
+ { left: "\\(", right: "\\)", display: false },
221
+ { left: "\\[", right: "\\]", display: true }
222
+ ],
223
+ throwOnError: false
224
+ });
225
+ }
226
+
227
+ // Process Citations - handles `search:id` (blue) and `page:id` (orange) formats
228
+ function processCitations(rootNode) {
229
+ // 1. Handle citations inside <code> tags (generated by markdown backticks)
230
+ const codeElements = rootNode.querySelectorAll('code');
231
+ codeElements.forEach(code => {
232
+ const text = code.textContent.trim();
233
+ const match = /^(search|page):(\d+)$/i.exec(text);
234
+ if (match) {
235
+ const type = match[1].toLowerCase();
236
+ const id = match[2];
237
+ const isPage = type === "page";
238
+ const colorClass = isPage
239
+ ? "text-orange-600 bg-orange-50 border-orange-200"
240
+ : "text-blue-600 bg-blue-50 border-blue-200";
241
+
242
+ const span = document.createElement("span");
243
+ // Using same badge style
244
+ span.innerHTML = `<span class="inline-flex items-center justify-center min-w-[14px] h-4 px-0.5 text-[9px] font-bold ${colorClass} border rounded align-top -top-0.5 relative mx-0.5 cursor-default" title="${type}:${id}">${id}</span>`;
245
+
246
+ // Replace the <code> element with our badge
247
+ if (code.parentNode) {
248
+ code.parentNode.replaceChild(span.firstElementChild, code);
249
+ }
250
+ }
251
+ });
252
+
253
+ // 2. Handle citations in plain text (fallback for non-backticked ones)
254
+ const walker = document.createTreeWalker(
255
+ rootNode,
256
+ NodeFilter.SHOW_TEXT,
257
+ null,
258
+ false
259
+ );
260
+
261
+ const nodesToReplace = [];
262
+ let node;
263
+ while (node = walker.nextNode()) {
264
+ if (node.parentElement.tagName === "SCRIPT" || node.parentElement.tagName === "STYLE" || node.parentElement.tagName === "A" || node.parentElement.tagName === "CODE") continue;
265
+ // Match search:id or page:id
266
+ if (/(search|page):(\d+)/i.test(node.nodeValue)) {
267
+ nodesToReplace.push(node);
268
+ }
269
+ }
270
+
271
+ nodesToReplace.forEach(textNode => {
272
+ const fragment = document.createDocumentFragment();
273
+ let lastIndex = 0;
274
+ const text = textNode.nodeValue;
275
+ const regex = /`?(search|page):(\d+)`?/gi;
276
+ let match;
277
+
278
+ while ((match = regex.exec(text)) !== null) {
279
+ fragment.appendChild(document.createTextNode(text.substring(lastIndex, match.index)));
280
+
281
+ const type = match[1].toLowerCase();
282
+ const id = match[2];
283
+
284
+ const span = document.createElement("span");
285
+ const isPage = type === "page";
286
+ const colorClass = isPage
287
+ ? "text-orange-600 bg-orange-50 border-orange-200"
288
+ : "text-blue-600 bg-blue-50 border-blue-200";
289
+
290
+ span.innerHTML = `<span class="inline-flex items-center justify-center min-w-[14px] h-4 px-0.5 text-[9px] font-bold ${colorClass} border rounded align-top -top-0.5 relative mx-0.5 cursor-default" title="${type}:${id}">${id}</span>`;
291
+ fragment.appendChild(span.firstElementChild);
292
+
293
+ lastIndex = regex.lastIndex;
294
+ }
295
+
296
+ fragment.appendChild(document.createTextNode(text.substring(lastIndex)));
297
+ if (textNode.parentNode) {
298
+ textNode.parentNode.replaceChild(fragment, textNode);
299
+ }
300
+ });
301
+ }
302
+
303
+ processCitations(contentDiv);
304
+ });
305
+ </script>
306
+ </body>
307
+ </html>
File without changes
@@ -0,0 +1,35 @@
1
+ from dataclasses import dataclass
2
+ from typing import Optional, Dict, Any, List
3
+
4
+ @dataclass
5
+ class HYWConfig:
6
+ api_key: str
7
+ model_name: str
8
+ vision_model_name: Optional[str] = None
9
+ vision_api_key: Optional[str] = None
10
+ vision_base_url: Optional[str] = None
11
+ base_url: str = "https://openrouter.ai/api/v1"
12
+ fusion_mode: bool = False
13
+ save_conversation: bool = False
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
18
+ search_base_url: str = "https://lite.duckduckgo.com/lite/?q={query}"
19
+ image_search_base_url: str = "https://duckduckgo.com/?q={query}&iax=images&ia=images"
20
+ extra_body: Optional[Dict[str, Any]] = None
21
+ temperature: float = 0.4
22
+ max_turns: int = 10
23
+ icon: str = "openai" # logo for primary model
24
+ vision_icon: Optional[str] = None # logo for vision model (falls back to icon when absent)
25
+ instruct_icon: Optional[str] = None # logo for instruct model
26
+ enable_browser_fallback: bool = False
27
+ vision_system_prompt: Optional[str] = None
28
+ intruct_system_prompt: Optional[str] = None
29
+ agent_system_prompt: Optional[str] = None
30
+ input_price: Optional[float] = None # $ per 1M input tokens
31
+ output_price: Optional[float] = None # $ per 1M output tokens
32
+ vision_input_price: Optional[float] = None
33
+ vision_output_price: Optional[float] = None
34
+ intruct_input_price: Optional[float] = None
35
+ intruct_output_price: Optional[float] = None
@@ -0,0 +1,146 @@
1
+ import random
2
+ import string
3
+ from typing import Dict, List, Any, Optional
4
+
5
+ class HistoryManager:
6
+ def __init__(self):
7
+ self._history: Dict[str, List[Dict[str, Any]]] = {}
8
+ self._metadata: Dict[str, Dict[str, Any]] = {}
9
+ self._mapping: Dict[str, str] = {}
10
+ self._context_latest: Dict[str, str] = {}
11
+
12
+ # New: Short code management
13
+ self._short_codes: Dict[str, str] = {} # code -> key
14
+ self._key_to_code: Dict[str, str] = {} # key -> code
15
+ self._context_history: Dict[str, List[str]] = {} # context_id -> list of keys
16
+
17
+ def is_bot_message(self, message_id: str) -> bool:
18
+ """Check if the message ID belongs to a bot message"""
19
+ return message_id in self._history
20
+
21
+ def generate_short_code(self) -> str:
22
+ """Generate a unique 4-digit hex code"""
23
+ while True:
24
+ code = ''.join(random.choices(string.hexdigits.lower(), k=4))
25
+ if code not in self._short_codes:
26
+ return code
27
+
28
+ def get_conversation_id(self, message_id: str) -> Optional[str]:
29
+ return self._mapping.get(message_id)
30
+
31
+ def get_key_by_code(self, code: str) -> Optional[str]:
32
+ return self._short_codes.get(code.lower())
33
+
34
+ def get_code_by_key(self, key: str) -> Optional[str]:
35
+ return self._key_to_code.get(key)
36
+
37
+ def get_history(self, key: str) -> List[Dict[str, Any]]:
38
+ return self._history.get(key, [])
39
+
40
+ def get_metadata(self, key: str) -> Dict[str, Any]:
41
+ return self._metadata.get(key, {})
42
+
43
+ def get_latest_from_context(self, context_id: str) -> Optional[str]:
44
+ return self._context_latest.get(context_id)
45
+
46
+ def list_by_context(self, context_id: str, limit: int = 10) -> List[str]:
47
+ """Return list of keys for a context, most recent first"""
48
+ keys = self._context_history.get(context_id, [])
49
+ return keys[-limit:][::-1]
50
+
51
+ def remember(self, message_id: Optional[str], history: List[Dict[str, Any]], related_ids: List[str], metadata: Optional[Dict[str, Any]] = None, context_id: Optional[str] = None, code: Optional[str] = None):
52
+ if not message_id:
53
+ return
54
+
55
+ key = message_id
56
+ self._history[key] = history
57
+ if metadata:
58
+ self._metadata[key] = metadata
59
+
60
+ self._mapping[key] = key
61
+ for rid in related_ids:
62
+ if rid:
63
+ self._mapping[rid] = key
64
+
65
+ # Generate or use provided short code
66
+ if key not in self._key_to_code:
67
+ if not code:
68
+ code = self.generate_short_code()
69
+ self._short_codes[code] = key
70
+ self._key_to_code[key] = code
71
+
72
+ if context_id:
73
+ self._context_latest[context_id] = key
74
+ if context_id not in self._context_history:
75
+ self._context_history[context_id] = []
76
+ self._context_history[context_id].append(key)
77
+
78
+ def save_to_disk(self, key: str, save_dir: str = "data/conversations"):
79
+ """Save conversation history to disk"""
80
+ import os
81
+ import time
82
+
83
+ if key not in self._history:
84
+ return
85
+
86
+ try:
87
+ os.makedirs(save_dir, exist_ok=True)
88
+ filename = f"{save_dir}/{key}_{int(time.time())}.md"
89
+
90
+ # Formatter
91
+ timestamp = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
92
+ meta = self._metadata.get(key, {})
93
+ model_name = meta.get("model", "unknown")
94
+ code = self._key_to_code.get(key, "N/A")
95
+
96
+ md_content = f"# Conversation Log: {key}\n\n"
97
+ md_content += f"**Time**: {timestamp}\n"
98
+ md_content += f"**Code**: {code}\n"
99
+ md_content += f"**Model**: {model_name}\n"
100
+ md_content += f"**Metadata**: {meta}\n\n"
101
+
102
+ trace_md = meta.get("trace_markdown") if isinstance(meta, dict) else None
103
+ if trace_md:
104
+ md_content += "## Trace\n\n"
105
+ md_content += f"{trace_md}\n\n"
106
+
107
+ md_content += "## History\n\n"
108
+
109
+ for msg in self._history[key]:
110
+ role = msg.get("role", "unknown").upper()
111
+ content = msg.get("content", "")
112
+
113
+ md_content += f"### {role}\n\n"
114
+
115
+ tool_calls = msg.get("tool_calls")
116
+ if tool_calls:
117
+ import json
118
+ try:
119
+ tc_str = json.dumps(tool_calls, ensure_ascii=False, indent=2)
120
+ except:
121
+ tc_str = str(tool_calls)
122
+ md_content += f"**Tool Calls**:\n```json\n{tc_str}\n```\n\n"
123
+
124
+ # Special handling for tool outputs or complex content
125
+ if role == "TOOL":
126
+ # Try to pretty print if it's JSON
127
+ try:
128
+ import json
129
+ # Content might be a JSON string already
130
+ parsed_content = json.loads(content)
131
+ pretty_content = json.dumps(parsed_content, ensure_ascii=False, indent=2)
132
+ md_content += f"**Output**:\n```json\n{pretty_content}\n```\n\n"
133
+ except:
134
+ md_content += f"**Output**:\n```text\n{content}\n```\n\n"
135
+ else:
136
+ if content:
137
+ md_content += f"{content}\n\n"
138
+
139
+ md_content += "---\n\n"
140
+
141
+ with open(filename, "w", encoding="utf-8") as f:
142
+ f.write(md_content)
143
+
144
+ except Exception as e:
145
+ # We can't log easily here without importing logger, but it's fine
146
+ print(f"Failed to save conversation: {e}")
@@ -0,0 +1,41 @@
1
+ from typing import Any, Dict, List, Optional
2
+ from loguru import logger
3
+ from .config import HYWConfig
4
+ from .pipeline import ProcessingPipeline
5
+
6
+ class HYW:
7
+ """
8
+ V2 Core Wrapper (Facade).
9
+ Delegates all logic to ProcessingPipeline.
10
+ Ensures safe lifecycle management.
11
+ """
12
+ def __init__(self, config: HYWConfig):
13
+ self.config = config
14
+ self.pipeline = ProcessingPipeline(config)
15
+ logger.info(f"HYW V2 (Ironclad) initialized - Model: {config.model_name}")
16
+
17
+ async def agent(self, user_input: str, conversation_history: List[Dict] = None, images: List[str] = None,
18
+ selected_model: str = None, selected_vision_model: str = None, local_mode: bool = False) -> Dict[str, Any]:
19
+ """
20
+ Main entry point for the plugin (called by __init__.py).
21
+ """
22
+ # Note: 'images' handling is skipped for V2 initial stability MVP as per user focus on 'search hangs'.
23
+ # We can re-integrate vision later, but for now we focus on Text/Search stability.
24
+
25
+ # Delegate completely to pipeline
26
+ result = await self.pipeline.execute(
27
+ user_input,
28
+ conversation_history or [],
29
+ model_name=selected_model,
30
+ images=images,
31
+ selected_vision_model=selected_vision_model,
32
+ )
33
+ return result
34
+
35
+ async def close(self):
36
+ """Explicit async close method. NO __del__."""
37
+ if self.pipeline:
38
+ await self.pipeline.close()
39
+
40
+ # Legacy Compatibility (optional attributes just to prevent blind attribute errors if referenced externally)
41
+ # in V2 we strongly discourage accessing internal tools directly.