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.
- entari_plugin_hyw/__init__.py +763 -309
- entari_plugin_hyw/assets/icon/anthropic.svg +1 -0
- entari_plugin_hyw/assets/icon/deepseek.png +0 -0
- entari_plugin_hyw/assets/icon/gemini.svg +1 -0
- entari_plugin_hyw/assets/icon/google.svg +1 -0
- entari_plugin_hyw/assets/icon/grok.png +0 -0
- entari_plugin_hyw/assets/icon/microsoft.svg +15 -0
- entari_plugin_hyw/assets/icon/minimax.png +0 -0
- entari_plugin_hyw/assets/icon/mistral.png +0 -0
- entari_plugin_hyw/assets/icon/nvida.png +0 -0
- entari_plugin_hyw/assets/icon/openai.svg +1 -0
- entari_plugin_hyw/assets/icon/openrouter.png +0 -0
- entari_plugin_hyw/assets/icon/perplexity.svg +24 -0
- entari_plugin_hyw/assets/icon/qwen.png +0 -0
- entari_plugin_hyw/assets/icon/xai.png +0 -0
- entari_plugin_hyw/assets/icon/zai.png +0 -0
- entari_plugin_hyw/assets/libs/highlight.css +10 -0
- entari_plugin_hyw/assets/libs/highlight.js +1213 -0
- entari_plugin_hyw/assets/libs/katex-auto-render.js +1 -0
- entari_plugin_hyw/assets/libs/katex.css +1 -0
- entari_plugin_hyw/assets/libs/katex.js +1 -0
- entari_plugin_hyw/assets/libs/tailwind.css +1 -0
- entari_plugin_hyw/assets/package-lock.json +953 -0
- entari_plugin_hyw/assets/package.json +16 -0
- entari_plugin_hyw/assets/tailwind.config.js +12 -0
- entari_plugin_hyw/assets/tailwind.input.css +235 -0
- entari_plugin_hyw/assets/template.html +157 -0
- entari_plugin_hyw/assets/template.html.bak +157 -0
- entari_plugin_hyw/assets/template.j2 +307 -0
- entari_plugin_hyw/core/__init__.py +0 -0
- entari_plugin_hyw/core/config.py +35 -0
- entari_plugin_hyw/core/history.py +146 -0
- entari_plugin_hyw/core/hyw.py +41 -0
- entari_plugin_hyw/core/pipeline.py +1065 -0
- entari_plugin_hyw/core/render.py +596 -0
- entari_plugin_hyw/core/render.py.bak +926 -0
- entari_plugin_hyw/utils/__init__.py +2 -0
- entari_plugin_hyw/utils/browser.py +40 -0
- entari_plugin_hyw/utils/misc.py +93 -0
- entari_plugin_hyw/utils/playwright_tool.py +36 -0
- entari_plugin_hyw/utils/prompts.py +129 -0
- entari_plugin_hyw/utils/search.py +241 -0
- {entari_plugin_hyw-3.3.1.dist-info → entari_plugin_hyw-3.3.2.dist-info}/METADATA +20 -28
- entari_plugin_hyw-3.3.2.dist-info/RECORD +46 -0
- entari_plugin_hyw/hyw_core.py +0 -700
- entari_plugin_hyw-3.3.1.dist-info/RECORD +0 -6
- {entari_plugin_hyw-3.3.1.dist-info → entari_plugin_hyw-3.3.2.dist-info}/WHEEL +0 -0
- {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.
|