entari-plugin-hyw 4.0.0rc4__py3-none-any.whl → 4.0.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.
Potentially problematic release.
This version of entari-plugin-hyw might be problematic. Click here for more details.
- entari_plugin_hyw/__init__.py +216 -75
- entari_plugin_hyw/assets/card-dist/index.html +70 -79
- entari_plugin_hyw/browser/__init__.py +10 -0
- entari_plugin_hyw/browser/engines/base.py +13 -0
- entari_plugin_hyw/browser/engines/bing.py +95 -0
- entari_plugin_hyw/browser/engines/duckduckgo.py +137 -0
- entari_plugin_hyw/browser/engines/google.py +155 -0
- entari_plugin_hyw/browser/landing.html +172 -0
- entari_plugin_hyw/browser/manager.py +153 -0
- entari_plugin_hyw/browser/service.py +304 -0
- entari_plugin_hyw/card-ui/src/App.vue +526 -182
- entari_plugin_hyw/card-ui/src/components/MarkdownContent.vue +7 -11
- entari_plugin_hyw/card-ui/src/components/StageCard.vue +33 -30
- entari_plugin_hyw/card-ui/src/types.ts +9 -0
- entari_plugin_hyw/definitions.py +155 -0
- entari_plugin_hyw/history.py +111 -33
- entari_plugin_hyw/misc.py +34 -0
- entari_plugin_hyw/modular_pipeline.py +384 -0
- entari_plugin_hyw/render_vue.py +326 -239
- entari_plugin_hyw/search.py +95 -708
- entari_plugin_hyw/stage_base.py +92 -0
- entari_plugin_hyw/stage_instruct.py +345 -0
- entari_plugin_hyw/stage_instruct_deepsearch.py +104 -0
- entari_plugin_hyw/stage_summary.py +164 -0
- {entari_plugin_hyw-4.0.0rc4.dist-info → entari_plugin_hyw-4.0.0rc6.dist-info}/METADATA +4 -4
- {entari_plugin_hyw-4.0.0rc4.dist-info → entari_plugin_hyw-4.0.0rc6.dist-info}/RECORD +28 -16
- entari_plugin_hyw/pipeline.py +0 -1219
- entari_plugin_hyw/prompts.py +0 -47
- {entari_plugin_hyw-4.0.0rc4.dist-info → entari_plugin_hyw-4.0.0rc6.dist-info}/WHEEL +0 -0
- {entari_plugin_hyw-4.0.0rc4.dist-info → entari_plugin_hyw-4.0.0rc6.dist-info}/top_level.txt +0 -0
|
@@ -56,7 +56,8 @@ const props = defineProps<{
|
|
|
56
56
|
markdown: string
|
|
57
57
|
numSearchRefs?: number
|
|
58
58
|
numPageRefs?: number
|
|
59
|
-
bare?: boolean
|
|
59
|
+
bare?: boolean
|
|
60
|
+
compact?: boolean
|
|
60
61
|
}>()
|
|
61
62
|
|
|
62
63
|
// Configure marked with syntax highlighting
|
|
@@ -251,21 +252,16 @@ const processedHtml = computed(() => {
|
|
|
251
252
|
|
|
252
253
|
<template>
|
|
253
254
|
<div ref="contentRef"
|
|
254
|
-
class="prose prose-slate max-w-none
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
prose-code:bg-gray-100 prose-code:px-1.5 prose-code:py-0.5 prose-code:rounded-none prose-code:text-[0.85em] prose-code:font-mono
|
|
259
|
-
prose-pre:bg-gray-50 prose-pre:border prose-pre:border-gray-200 prose-pre:rounded-none prose-pre:p-0
|
|
260
|
-
prose-img:rounded-none prose-img:my-6 prose-img:max-h-[400px] prose-img:w-auto prose-img:object-contain prose-img:border prose-img:border-gray-200
|
|
261
|
-
prose-ol:list-decimal prose-ol:pl-7 prose-ol:list-outside prose-ol:my-5
|
|
262
|
-
prose-li:my-2.5 prose-li:leading-7
|
|
263
|
-
[&>*:first-child]:!mt-0"
|
|
255
|
+
class="prose prose-slate max-w-none"
|
|
256
|
+
:class="props.compact
|
|
257
|
+
? 'prose-sm !prose-p:my-1 !prose-p:leading-snug !prose-p:text-[13.5px] !prose-li:text-[13.5px] !prose-headings:text-[14px] !prose-headings:mt-2 !prose-headings:mb-1 prose-a:text-blue-600 prose-a:no-underline hover:prose-a:underline [&>*:first-child]:!mt-0'
|
|
258
|
+
: 'prose-lg prose-headings:font-bold prose-headings:mb-3 prose-headings:mt-8 prose-headings:tracking-tight prose-p:leading-7 prose-p:my-4 prose-p:text-[20px] prose-li:text-[20px] prose-a:text-blue-600 prose-a:no-underline hover:prose-a:underline prose-code:bg-gray-100 prose-code:px-1.5 prose-code:py-0.5 prose-code:rounded-none prose-code:text-[0.85em] prose-code:font-mono prose-pre:bg-gray-50 prose-pre:border prose-pre:border-gray-200 prose-pre:rounded-none prose-pre:p-0 prose-img:rounded-none prose-img:my-6 prose-img:max-h-[400px] prose-img:w-auto prose-img:object-contain prose-img:border prose-img:border-gray-200 prose-ol:list-decimal prose-ol:pl-7 prose-ol:list-outside prose-ol:my-5 prose-li:my-2.5 prose-li:leading-7 [&>*:first-child]:!mt-0'"
|
|
264
259
|
style="--prose-headings: var(--text-primary); --prose-body: var(--text-body); --prose-bold: var(--text-primary); --prose-code: var(--text-body)"
|
|
265
260
|
v-html="processedHtml">
|
|
266
261
|
</div>
|
|
267
262
|
</template>
|
|
268
263
|
|
|
264
|
+
|
|
269
265
|
<style>
|
|
270
266
|
/* Highlight.js theme - minimal */
|
|
271
267
|
.hljs {
|
|
@@ -9,6 +9,7 @@ const props = defineProps<{
|
|
|
9
9
|
isLast?: boolean
|
|
10
10
|
prevStageName?: string
|
|
11
11
|
refOffset?: number
|
|
12
|
+
hideRefs?: boolean
|
|
12
13
|
}>()
|
|
13
14
|
|
|
14
15
|
const failedImages = ref<Record<string, boolean>>({})
|
|
@@ -167,28 +168,24 @@ function getModelLogo(model: string): string | undefined {
|
|
|
167
168
|
</div>
|
|
168
169
|
|
|
169
170
|
|
|
170
|
-
<div v-if="stage.
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
<span class="flex-1 text-[18px] font-bold text-gray-700 truncate leading-tight tracking-tight">{{ ref.title }}</span>
|
|
182
|
-
<!-- Square Badge with Shadow -->
|
|
183
|
-
<span class="shrink-0 w-[18px] h-[18px] text-[11px] font-bold flex items-center justify-center" style="background-color: var(--theme-color); color: var(--header-text-color); box-shadow: 0 1px 3px 0 rgba(0,0,0,0.15)">{{ (refOffset || 0) + idx + 1 }}</span>
|
|
171
|
+
<div v-if="stage.description" class="px-5 py-3 text-[14.5px] text-gray-600 bg-gray-50/40 border-y border-gray-100 italic leading-relaxed font-sans">
|
|
172
|
+
<span class="mr-1 text-[var(--theme-color)] not-italic opacity-50">✦</span> {{ stage.description }}
|
|
173
|
+
</div>
|
|
174
|
+
|
|
175
|
+
<div v-if="stage.references?.length || stage.image_references?.length || stage.crawled_pages?.length || stage.tasks?.length" class="bg-white pl-11 relative">
|
|
176
|
+
|
|
177
|
+
<!-- Tasks List -->
|
|
178
|
+
<div v-if="stage.tasks?.length" class="pr-3 py-3 relative z-10 border-b border-gray-50 last:border-0">
|
|
179
|
+
<div v-for="(task, idx) in stage.tasks" :key="idx" class="flex items-start gap-2.5 mb-2 last:mb-0">
|
|
180
|
+
<div class="w-4 h-4 rounded-full border border-[var(--theme-color)] flex items-center justify-center shrink-0 mt-0.5 bg-[var(--theme-color)]/10">
|
|
181
|
+
<Icon icon="mdi:check" class="text-[10px] text-[var(--theme-color)]" />
|
|
184
182
|
</div>
|
|
185
|
-
<
|
|
186
|
-
|
|
187
|
-
</a>
|
|
183
|
+
<span class="text-[15px] text-gray-700 font-medium leading-tight select-text">{{ task }}</span>
|
|
184
|
+
</div>
|
|
188
185
|
</div>
|
|
189
186
|
|
|
190
187
|
<!-- Image Search Results - True Masonry Layout -->
|
|
191
|
-
<div v-if="stage.image_references?.length" class="pr-3 py-3 relative z-10">
|
|
188
|
+
<div v-if="stage.image_references?.length && !hideRefs" class="pr-3 py-3 relative z-10">
|
|
192
189
|
<div class="flex gap-2">
|
|
193
190
|
<!-- Left Column -->
|
|
194
191
|
<div class="flex-1 flex flex-col gap-2">
|
|
@@ -215,20 +212,26 @@ function getModelLogo(model: string): string | undefined {
|
|
|
215
212
|
</div>
|
|
216
213
|
</div>
|
|
217
214
|
|
|
218
|
-
<div v-if="stage.crawled_pages?.length" class="divide-y divide-gray-50 relative z-10">
|
|
219
|
-
<
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
215
|
+
<div v-if="stage.crawled_pages?.length && !hideRefs" class="divide-y divide-gray-50 relative z-10">
|
|
216
|
+
<div v-for="(page, idx) in stage.crawled_pages" :key="idx" class="pr-3 py-3 hover:bg-gray-50 transition-colors group">
|
|
217
|
+
<!-- Header Row: Title & Link -->
|
|
218
|
+
<a :href="page.url" target="_blank" class="flex items-start gap-3 block">
|
|
219
|
+
<img :src="getFavicon(page.url)" class="w-4 h-4 rounded-none shrink-0 object-contain mt-[4px]">
|
|
220
|
+
<div class="flex-1 min-w-0 flex flex-col">
|
|
221
|
+
<div class="flex items-center gap-2">
|
|
222
|
+
<span class="flex-1 text-[18px] font-bold text-gray-700 truncate leading-tight tracking-tight group-hover:text-[var(--theme-color)] transition-colors">{{ page.title }}</span>
|
|
223
|
+
<!-- Square Badge with Shadow -->
|
|
224
|
+
<span class="shrink-0 w-[18px] h-[18px] text-[11px] font-bold flex items-center justify-center" style="background-color: var(--theme-color); color: var(--header-text-color); box-shadow: 0 1px 3px 0 rgba(0,0,0,0.15)">{{ (refOffset || 0) + (stage.references?.length || 0) + idx + 1 }}</span>
|
|
225
|
+
</div>
|
|
226
|
+
<div class="text-[15.5px] font-mono truncate mt-0.5 tracking-tighter" style="color: var(--text-muted)">{{ getDomain(page.url) }}</div>
|
|
228
227
|
</div>
|
|
229
|
-
|
|
228
|
+
</a>
|
|
229
|
+
|
|
230
|
+
<!-- Description Sub-container -->
|
|
231
|
+
<div v-if="page.description" class="ml-7 mt-2.5 p-3 bg-gray-100/80 text-[14px] text-gray-600 leading-snug font-sans border-l-[3px] border-[var(--theme-color)] rounded-r-sm">
|
|
232
|
+
{{ page.description }}
|
|
230
233
|
</div>
|
|
231
|
-
</
|
|
234
|
+
</div>
|
|
232
235
|
</div>
|
|
233
236
|
</div>
|
|
234
237
|
</div>
|
|
@@ -6,16 +6,24 @@ export interface Stage {
|
|
|
6
6
|
model: string
|
|
7
7
|
provider: string
|
|
8
8
|
icon_name?: string // Icon identifier (e.g., "google", "openai")
|
|
9
|
+
icon_config?: string // Config key for icon (e.g. "openai")
|
|
9
10
|
time: number // Time in seconds (raw number)
|
|
10
11
|
cost: number // Cost in dollars (raw number)
|
|
12
|
+
usage?: any // Token usage stats
|
|
11
13
|
references?: Reference[]
|
|
12
14
|
image_references?: ImageReference[]
|
|
13
15
|
crawled_pages?: CrawledPage[]
|
|
16
|
+
description?: string // Brief intro or thought for this stage
|
|
17
|
+
tasks?: string[]
|
|
14
18
|
}
|
|
15
19
|
|
|
16
20
|
export interface Reference {
|
|
17
21
|
title: string
|
|
18
22
|
url: string
|
|
23
|
+
is_fetched?: boolean
|
|
24
|
+
snippet?: string
|
|
25
|
+
type?: string
|
|
26
|
+
images?: string[] // Extracted images (base64)
|
|
19
27
|
}
|
|
20
28
|
|
|
21
29
|
export interface ImageReference {
|
|
@@ -27,6 +35,7 @@ export interface ImageReference {
|
|
|
27
35
|
export interface CrawledPage {
|
|
28
36
|
title: string
|
|
29
37
|
url: string
|
|
38
|
+
description?: string
|
|
30
39
|
}
|
|
31
40
|
|
|
32
41
|
export interface Stats {
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Centralized Definitions
|
|
3
|
+
|
|
4
|
+
All global prompts and tool definitions for the pipeline stages.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Dict, Any
|
|
8
|
+
|
|
9
|
+
# =============================================================================
|
|
10
|
+
# PROMPTS
|
|
11
|
+
# =============================================================================
|
|
12
|
+
|
|
13
|
+
INSTRUCT_SP = """你是一个智能 INSTRUCT 助手。
|
|
14
|
+
你的目标是为用户的查询制定调研计划并执行
|
|
15
|
+
> 尽量完成所有搜索和获取, 需要的时候可以并发执行多个同种或不同种工具
|
|
16
|
+
|
|
17
|
+
- 用户消息包含典型名词、可能的专有名词组合: 调用 `web_search` 工具(最大3)。
|
|
18
|
+
- 提炼消息中出独立的关键词搜索关键词本身, 不添加任何其他辅助搜索词, 搜索效果最好
|
|
19
|
+
- 不混合搜索词, 每次只针对搜索一个关键词, 获取其目的方向的最新数据
|
|
20
|
+
- 用户消息中出现链接时, 调用 `crawl_page`(最大3) 获取网页全文
|
|
21
|
+
- 以下情况调用 `refuse_answer`。
|
|
22
|
+
- 明显的鉴证、国家领导人、历史敏感事件隐喻
|
|
23
|
+
- 过于露骨的r18+、r18g内容(但不包括 galgame等acg向, 除非其过于违规)
|
|
24
|
+
|
|
25
|
+
## 内容审查
|
|
26
|
+
以下情况调用 `refuse_answer`:
|
|
27
|
+
- 内容涉及隐喻政治事件任务
|
|
28
|
+
- r18+、r18g(但不包含正常galgame、科普等)
|
|
29
|
+
|
|
30
|
+
## 决定流程
|
|
31
|
+
调用 `set_mode` 决定使用 `fast` 还是 `deepsearch`:
|
|
32
|
+
- 只要用户不明确研究就用 `fast`
|
|
33
|
+
- 用户提到: 深度搜索、深度研究、deepsearch、整理关于...的资料, 这几种情况时, 选择 `deepsearch`
|
|
34
|
+
|
|
35
|
+
## 重要规则 (CRITICAL RULES):
|
|
36
|
+
- 禁止输出任何文本回复:你必须且只能通过工具调用来行动。
|
|
37
|
+
- 如果没有工具调用,流程将自动结束。
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
INSTRUCT_DEEPSEARCH_SP = """你是一个智能 INSTRUCT_DEEPSEARCH 审查助手, 你需要对 INSTRUCT 的输出进行多次信息补充直到信息足够、或达到次数上限(3次)
|
|
41
|
+
|
|
42
|
+
- 推荐使用 `crawl_page` 工具查看官方网站、wiki网站(但不推荐维基百科)、权威网站
|
|
43
|
+
- crawl_page 永远不使用国内垃圾网站例如 csdn、知乎、等重复搬运二手信息的网站
|
|
44
|
+
|
|
45
|
+
## 重要规则 (CRITICAL RULES):
|
|
46
|
+
- 禁止输出任何文本回复:你必须且只能通过工具调用来行动。
|
|
47
|
+
- 如果没有必要进一步操作,请不要输出任何内容(空回复),流程将自动进入下一阶段。
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
SUMMARY_REPORT_SP = """# 你是一个信息整合专家 (Summary Agent).
|
|
52
|
+
你需要根据用户问题、搜索结果和网页详细内容,生成最终的回答.
|
|
53
|
+
如果用户发送你好或空内容回应你好即可.
|
|
54
|
+
|
|
55
|
+
## 过程要求
|
|
56
|
+
- 用户要求的回复语言(包裹在 language 标签内)
|
|
57
|
+
```language
|
|
58
|
+
{language}
|
|
59
|
+
```
|
|
60
|
+
- 字数控制在600字以内, 百科式风格, 语言严谨不啰嗦.
|
|
61
|
+
- 视觉信息: 输入中如果包含自动获取的网页截图,请分析图片中的信息作为参考.
|
|
62
|
+
- 注意分辨搜索内容是否和用户问题有直接关系, 避免盲目相信混为一谈.
|
|
63
|
+
- 正文格式:
|
|
64
|
+
- 先给出一个 `# `大标题约 8-10 个字, 不要有多余废话, 不要直接回答用户的提问.
|
|
65
|
+
- 然后紧接着给出一个 <summary>...</summary>, 除了给出一个约 100 字的纯文本简介, 介绍本次输出的长文的清晰、重点概括.
|
|
66
|
+
- 随后开始详细二级标题 + markdown 正文, 语言描绘格式丰富多样, 简洁准确可信.
|
|
67
|
+
- 请不要给出过长的代码、表格列数等, 只讲重点和准确的数据.
|
|
68
|
+
- 不支持渲染: 链接, 图片链接, mermaid
|
|
69
|
+
- 支持渲染: 公式, 代码高亮, 只在需要的时候给出.
|
|
70
|
+
- 图片链接、链接框架会自动渲染出, 你无需显式给出.
|
|
71
|
+
- 引用:
|
|
72
|
+
> 重要: 所有正文内容必须基于实际信息, 保证百分百真实度
|
|
73
|
+
- 信息来源已按获取顺序编号为 [1], [2], [3]... 但不超过 9 个引用.
|
|
74
|
+
- 优先引用优质 fetch 抓取的页面的资源, 但如果抓取到需要登录、需要验证码、需要跳转到其他网站等无法获取的资源, 则不引用此资源
|
|
75
|
+
- 正文中直接使用 [1] 格式引用, 只引用对回答有帮助的来源, 只使用官方性较强的 wiki、官方网站、资源站等等, 不使用第三方转载新闻网站.
|
|
76
|
+
- 无需给出参考文献列表, 系统会自动生成
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
# =============================================================================
|
|
81
|
+
# TOOL DEFINITIONS
|
|
82
|
+
# =============================================================================
|
|
83
|
+
|
|
84
|
+
def get_refuse_answer_tool() -> Dict[str, Any]:
|
|
85
|
+
"""Tool for refusing to answer inappropriate content."""
|
|
86
|
+
return {
|
|
87
|
+
"type": "function",
|
|
88
|
+
"function": {
|
|
89
|
+
"name": "refuse_answer",
|
|
90
|
+
"description": "违规内容拒绝回答",
|
|
91
|
+
"parameters": {
|
|
92
|
+
"type": "object",
|
|
93
|
+
"properties": {
|
|
94
|
+
"reason": {"type": "string", "description": "拒绝回答的原因(展示给用户)"},
|
|
95
|
+
},
|
|
96
|
+
"required": ["reason"],
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def get_web_search_tool() -> Dict[str, Any]:
|
|
103
|
+
"""Tool for searching the web."""
|
|
104
|
+
return {
|
|
105
|
+
"type": "function",
|
|
106
|
+
"function": {
|
|
107
|
+
"name": "web_search",
|
|
108
|
+
"description": "网络搜索",
|
|
109
|
+
"parameters": {
|
|
110
|
+
"type": "object",
|
|
111
|
+
"properties": {"query": {"type": "string"}},
|
|
112
|
+
"required": ["query"],
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def get_crawl_page_tool() -> Dict[str, Any]:
|
|
119
|
+
"""Tool for crawling a web page."""
|
|
120
|
+
return {
|
|
121
|
+
"type": "function",
|
|
122
|
+
"function": {
|
|
123
|
+
"name": "crawl_page",
|
|
124
|
+
"description": "抓取网页并返回 Markdown 文本 / 网页截图",
|
|
125
|
+
"parameters": {
|
|
126
|
+
"type": "object",
|
|
127
|
+
"properties": {
|
|
128
|
+
"url": {"type": "string"},
|
|
129
|
+
},
|
|
130
|
+
"required": ["url"],
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def get_set_mode_tool() -> Dict[str, Any]:
|
|
137
|
+
"""Tool for setting the pipeline mode (fast or deepsearch)."""
|
|
138
|
+
return {
|
|
139
|
+
"type": "function",
|
|
140
|
+
"function": {
|
|
141
|
+
"name": "set_mode",
|
|
142
|
+
"description": "设置本次查询的处理模式",
|
|
143
|
+
"parameters": {
|
|
144
|
+
"type": "object",
|
|
145
|
+
"properties": {
|
|
146
|
+
"mode": {
|
|
147
|
+
"type": "string",
|
|
148
|
+
"enum": ["fast", "deepsearch"],
|
|
149
|
+
"description": "fast=快速回答 / deepsearch=深度研究"
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
"required": ["mode"],
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
}
|
entari_plugin_hyw/history.py
CHANGED
|
@@ -75,24 +75,23 @@ class HistoryManager:
|
|
|
75
75
|
self._context_history[context_id] = []
|
|
76
76
|
self._context_history[context_id].append(key)
|
|
77
77
|
|
|
78
|
-
def save_to_disk(self, key: str,
|
|
79
|
-
"""Save conversation history to
|
|
78
|
+
def save_to_disk(self, key: str, save_root: str = "data/conversations", image_path: Optional[str] = None, web_results: Optional[List[Dict]] = None):
|
|
79
|
+
"""Save conversation history to specific folder structure"""
|
|
80
80
|
import os
|
|
81
81
|
import time
|
|
82
82
|
import re
|
|
83
|
+
import shutil
|
|
84
|
+
import json
|
|
83
85
|
|
|
84
86
|
if key not in self._history:
|
|
85
87
|
return
|
|
86
88
|
|
|
87
89
|
try:
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
# Extract user's first message (question) for filename
|
|
90
|
+
# Extract user's first message (question) for folder name
|
|
91
91
|
user_question = ""
|
|
92
92
|
for msg in self._history[key]:
|
|
93
93
|
if msg.get("role") == "user":
|
|
94
94
|
content = msg.get("content", "")
|
|
95
|
-
# Handle content that might be a list (multimodal)
|
|
96
95
|
if isinstance(content, list):
|
|
97
96
|
for item in content:
|
|
98
97
|
if isinstance(item, dict) and item.get("type") == "text":
|
|
@@ -102,31 +101,112 @@ class HistoryManager:
|
|
|
102
101
|
user_question = str(content)
|
|
103
102
|
break
|
|
104
103
|
|
|
105
|
-
# Clean and truncate question
|
|
106
|
-
question_part = re.sub(r'[\\/:*?"<>|\n\r\t]', '', user_question)[:
|
|
104
|
+
# Clean and truncate question
|
|
105
|
+
question_part = re.sub(r'[\\/:*?"<>|\n\r\t]', '', user_question)[:20].strip()
|
|
107
106
|
if not question_part:
|
|
108
107
|
question_part = "conversation"
|
|
109
108
|
|
|
110
|
-
#
|
|
109
|
+
# Create folder: YYYYMMDD_HHMMSS_question
|
|
111
110
|
time_str = time.strftime("%Y%m%d_%H%M%S", time.localtime())
|
|
112
|
-
|
|
111
|
+
folder_name = f"{time_str}_{question_part}"
|
|
112
|
+
folder_path = os.path.join(save_root, folder_name)
|
|
113
|
+
|
|
114
|
+
os.makedirs(folder_path, exist_ok=True)
|
|
113
115
|
|
|
114
|
-
# Formatter
|
|
115
|
-
timestamp = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
|
|
116
116
|
meta = self._metadata.get(key, {})
|
|
117
|
+
|
|
118
|
+
# 1. Save Context/Trace
|
|
119
|
+
trace_md = meta.get("trace_markdown")
|
|
120
|
+
if trace_md:
|
|
121
|
+
with open(os.path.join(folder_path, "context_trace.md"), "w", encoding="utf-8") as f:
|
|
122
|
+
f.write(trace_md)
|
|
123
|
+
|
|
124
|
+
# 2. Save Web Results (Search & Pages)
|
|
125
|
+
if web_results:
|
|
126
|
+
pages_dir = os.path.join(folder_path, "pages")
|
|
127
|
+
os.makedirs(pages_dir, exist_ok=True)
|
|
128
|
+
|
|
129
|
+
search_buffer = [] # Buffer for unfetched search results
|
|
130
|
+
|
|
131
|
+
for i, item in enumerate(web_results):
|
|
132
|
+
item_type = item.get("_type", "unknown")
|
|
133
|
+
title = item.get("title", "Untitled")
|
|
134
|
+
url = item.get("url", "")
|
|
135
|
+
content = item.get("content", "")
|
|
136
|
+
item_id = item.get("_id", i + 1)
|
|
137
|
+
|
|
138
|
+
if not content:
|
|
139
|
+
continue
|
|
140
|
+
|
|
141
|
+
if item_type == "search":
|
|
142
|
+
# Collect search snippets for consolidated file
|
|
143
|
+
search_buffer.append(f"## [{item_id}] {title}\n- **URL**: {url}\n\n{content}\n")
|
|
144
|
+
|
|
145
|
+
elif item_type in ["page", "search_raw_page"]:
|
|
146
|
+
# Save fetched pages/raw search pages individually
|
|
147
|
+
clean_title = re.sub(r'[\\/:*?"<>|\n\r\t]', '', title)[:30].strip() or "page"
|
|
148
|
+
filename = f"{item_id:02d}_{item_type}_{clean_title}.md"
|
|
149
|
+
|
|
150
|
+
# Save screenshot if available
|
|
151
|
+
screenshot_b64 = item.get("screenshot_b64")
|
|
152
|
+
image_ref = ""
|
|
153
|
+
if screenshot_b64:
|
|
154
|
+
try:
|
|
155
|
+
import base64
|
|
156
|
+
img_filename = f"{item_id:02d}_{item_type}_{clean_title}.jpg"
|
|
157
|
+
img_path = os.path.join(pages_dir, img_filename)
|
|
158
|
+
with open(img_path, "wb") as f:
|
|
159
|
+
f.write(base64.b64decode(screenshot_b64))
|
|
160
|
+
image_ref = f"\n### Screenshot\n\n"
|
|
161
|
+
except Exception as e:
|
|
162
|
+
print(f"Failed to save screenshot for {title}: {e}")
|
|
163
|
+
|
|
164
|
+
page_md = f"# [{item_id}] {title}\n\n"
|
|
165
|
+
page_md += f"- **Type**: {item_type}\n"
|
|
166
|
+
page_md += f"- **URL**: {url}\n\n"
|
|
167
|
+
if image_ref:
|
|
168
|
+
page_md += f"{image_ref}\n"
|
|
169
|
+
page_md += f"---\n\n{content}\n"
|
|
170
|
+
|
|
171
|
+
with open(os.path.join(pages_dir, filename), "w", encoding="utf-8") as f:
|
|
172
|
+
f.write(page_md)
|
|
173
|
+
|
|
174
|
+
# Save consolidated search results
|
|
175
|
+
if search_buffer:
|
|
176
|
+
with open(os.path.join(folder_path, "search_results.md"), "w", encoding="utf-8") as f:
|
|
177
|
+
f.write(f"# Search Results\n\nGenerated at {time.strftime('%Y-%m-%d %H:%M:%S')}\n\n" + "\n---\n\n".join(search_buffer))
|
|
178
|
+
|
|
179
|
+
# 3. Save Final Response (MD)
|
|
180
|
+
final_content = ""
|
|
181
|
+
# Find last assistant message
|
|
182
|
+
for msg in reversed(self._history[key]):
|
|
183
|
+
if msg.get("role") == "assistant":
|
|
184
|
+
content = msg.get("content", "")
|
|
185
|
+
if isinstance(content, str):
|
|
186
|
+
final_content = content
|
|
187
|
+
break
|
|
188
|
+
|
|
189
|
+
if final_content:
|
|
190
|
+
with open(os.path.join(folder_path, "final_response.md"), "w", encoding="utf-8") as f:
|
|
191
|
+
f.write(final_content)
|
|
192
|
+
|
|
193
|
+
# Save Output Image (Final Card)
|
|
194
|
+
if image_path and os.path.exists(image_path):
|
|
195
|
+
try:
|
|
196
|
+
dest_img_path = os.path.join(folder_path, "output_card.jpg")
|
|
197
|
+
shutil.copy2(image_path, dest_img_path)
|
|
198
|
+
except Exception as e:
|
|
199
|
+
print(f"Failed to copy output image: {e}")
|
|
200
|
+
|
|
201
|
+
# 4. Save Full Log (Readme style)
|
|
202
|
+
timestamp = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
|
|
117
203
|
model_name = meta.get("model", "unknown")
|
|
118
204
|
code = self._key_to_code.get(key, "N/A")
|
|
119
205
|
|
|
120
|
-
md_content = f"# Conversation Log: {
|
|
121
|
-
md_content += f"**Time**: {timestamp}\n"
|
|
122
|
-
md_content += f"**Code**: {code}\n"
|
|
123
|
-
md_content += f"**Model**: {model_name}\n"
|
|
124
|
-
md_content += f"**Metadata**: {meta}\n\n"
|
|
125
|
-
|
|
126
|
-
trace_md = meta.get("trace_markdown") if isinstance(meta, dict) else None
|
|
127
|
-
if trace_md:
|
|
128
|
-
md_content += "## Trace\n\n"
|
|
129
|
-
md_content += f"{trace_md}\n\n"
|
|
206
|
+
md_content = f"# Conversation Log: {folder_name}\n\n"
|
|
207
|
+
md_content += f"- **Time**: {timestamp}\n"
|
|
208
|
+
md_content += f"- **Code**: {code}\n"
|
|
209
|
+
md_content += f"- **Model**: {model_name}\n\n"
|
|
130
210
|
|
|
131
211
|
md_content += "## History\n\n"
|
|
132
212
|
|
|
@@ -138,33 +218,31 @@ class HistoryManager:
|
|
|
138
218
|
|
|
139
219
|
tool_calls = msg.get("tool_calls")
|
|
140
220
|
if tool_calls:
|
|
141
|
-
import json
|
|
142
221
|
try:
|
|
143
222
|
tc_str = json.dumps(tool_calls, ensure_ascii=False, indent=2)
|
|
144
223
|
except:
|
|
145
224
|
tc_str = str(tool_calls)
|
|
146
225
|
md_content += f"**Tool Calls**:\n```json\n{tc_str}\n```\n\n"
|
|
147
226
|
|
|
148
|
-
# Special handling for tool outputs or complex content
|
|
149
227
|
if role == "TOOL":
|
|
150
|
-
# Try to pretty print if it's JSON
|
|
151
228
|
try:
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
229
|
+
# Try parsing as JSON first
|
|
230
|
+
if isinstance(content, str):
|
|
231
|
+
parsed = json.loads(content)
|
|
232
|
+
pretty = json.dumps(parsed, ensure_ascii=False, indent=2)
|
|
233
|
+
md_content += f"**Output**:\n```json\n{pretty}\n```\n\n"
|
|
234
|
+
else:
|
|
235
|
+
md_content += f"**Output**:\n```text\n{content}\n```\n\n"
|
|
157
236
|
except:
|
|
158
|
-
|
|
237
|
+
md_content += f"**Output**:\n```text\n{content}\n```\n\n"
|
|
159
238
|
else:
|
|
160
239
|
if content:
|
|
161
240
|
md_content += f"{content}\n\n"
|
|
162
241
|
|
|
163
242
|
md_content += "---\n\n"
|
|
164
243
|
|
|
165
|
-
with open(
|
|
244
|
+
with open(os.path.join(folder_path, "full_log.md"), "w", encoding="utf-8") as f:
|
|
166
245
|
f.write(md_content)
|
|
167
246
|
|
|
168
247
|
except Exception as e:
|
|
169
|
-
# We can't log easily here without importing logger, but it's fine
|
|
170
248
|
print(f"Failed to save conversation: {e}")
|
entari_plugin_hyw/misc.py
CHANGED
|
@@ -133,3 +133,37 @@ async def render_refuse_answer(
|
|
|
133
133
|
theme_color=theme_color,
|
|
134
134
|
)
|
|
135
135
|
|
|
136
|
+
|
|
137
|
+
IMAGE_UNSUPPORTED_MARKDOWN = """
|
|
138
|
+
<summary>
|
|
139
|
+
当前模型不支持图片输入,请使用支持视觉能力的模型或仅发送文本。
|
|
140
|
+
</summary>
|
|
141
|
+
"""
|
|
142
|
+
|
|
143
|
+
async def render_image_unsupported(
|
|
144
|
+
renderer,
|
|
145
|
+
output_path: str,
|
|
146
|
+
theme_color: str = "#ef4444",
|
|
147
|
+
tab_id: str = None
|
|
148
|
+
) -> bool:
|
|
149
|
+
"""
|
|
150
|
+
Render a card indicating that the model does not support image input.
|
|
151
|
+
"""
|
|
152
|
+
markdown = f"""
|
|
153
|
+
# 图片输入不支持
|
|
154
|
+
|
|
155
|
+
> 当前选择的模型不支持图片输入。
|
|
156
|
+
> 请切换到支持视觉的模型,或仅发送文本内容。
|
|
157
|
+
"""
|
|
158
|
+
return await renderer.render(
|
|
159
|
+
markdown_content=markdown,
|
|
160
|
+
output_path=output_path,
|
|
161
|
+
stats={},
|
|
162
|
+
references=[],
|
|
163
|
+
page_references=[],
|
|
164
|
+
image_references=[],
|
|
165
|
+
stages_used=[],
|
|
166
|
+
image_timeout=1000,
|
|
167
|
+
theme_color=theme_color,
|
|
168
|
+
tab_id=tab_id
|
|
169
|
+
)
|