entari-plugin-hyw 2.2.5__py3-none-any.whl → 3.5.0rc6__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- entari_plugin_hyw/__init__.py +371 -315
- entari_plugin_hyw/assets/card-dist/index.html +396 -0
- entari_plugin_hyw/assets/card-dist/logos/anthropic.svg +1 -0
- entari_plugin_hyw/assets/card-dist/logos/cerebras.svg +9 -0
- entari_plugin_hyw/assets/card-dist/logos/deepseek.png +0 -0
- entari_plugin_hyw/assets/card-dist/logos/gemini.svg +1 -0
- entari_plugin_hyw/assets/card-dist/logos/google.svg +1 -0
- entari_plugin_hyw/assets/card-dist/logos/grok.png +0 -0
- entari_plugin_hyw/assets/card-dist/logos/huggingface.png +0 -0
- entari_plugin_hyw/assets/card-dist/logos/microsoft.svg +15 -0
- entari_plugin_hyw/assets/card-dist/logos/minimax.png +0 -0
- entari_plugin_hyw/assets/card-dist/logos/mistral.png +0 -0
- entari_plugin_hyw/assets/card-dist/logos/nvida.png +0 -0
- entari_plugin_hyw/assets/card-dist/logos/openai.svg +1 -0
- entari_plugin_hyw/assets/card-dist/logos/openrouter.png +0 -0
- entari_plugin_hyw/assets/card-dist/logos/perplexity.svg +24 -0
- entari_plugin_hyw/assets/card-dist/logos/qwen.png +0 -0
- entari_plugin_hyw/assets/card-dist/logos/xai.png +0 -0
- entari_plugin_hyw/assets/card-dist/logos/xiaomi.png +0 -0
- entari_plugin_hyw/assets/card-dist/logos/zai.png +0 -0
- entari_plugin_hyw/assets/card-dist/vite.svg +1 -0
- entari_plugin_hyw/assets/icon/anthropic.svg +1 -0
- entari_plugin_hyw/assets/icon/cerebras.svg +9 -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/huggingface.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/xiaomi.png +0 -0
- entari_plugin_hyw/assets/icon/zai.png +0 -0
- entari_plugin_hyw/card-ui/.gitignore +24 -0
- entari_plugin_hyw/card-ui/README.md +5 -0
- entari_plugin_hyw/card-ui/index.html +16 -0
- entari_plugin_hyw/card-ui/package-lock.json +2342 -0
- entari_plugin_hyw/card-ui/package.json +31 -0
- entari_plugin_hyw/card-ui/public/logos/anthropic.svg +1 -0
- entari_plugin_hyw/card-ui/public/logos/cerebras.svg +9 -0
- entari_plugin_hyw/card-ui/public/logos/deepseek.png +0 -0
- entari_plugin_hyw/card-ui/public/logos/gemini.svg +1 -0
- entari_plugin_hyw/card-ui/public/logos/google.svg +1 -0
- entari_plugin_hyw/card-ui/public/logos/grok.png +0 -0
- entari_plugin_hyw/card-ui/public/logos/huggingface.png +0 -0
- entari_plugin_hyw/card-ui/public/logos/microsoft.svg +15 -0
- entari_plugin_hyw/card-ui/public/logos/minimax.png +0 -0
- entari_plugin_hyw/card-ui/public/logos/mistral.png +0 -0
- entari_plugin_hyw/card-ui/public/logos/nvida.png +0 -0
- entari_plugin_hyw/card-ui/public/logos/openai.svg +1 -0
- entari_plugin_hyw/card-ui/public/logos/openrouter.png +0 -0
- entari_plugin_hyw/card-ui/public/logos/perplexity.svg +24 -0
- entari_plugin_hyw/card-ui/public/logos/qwen.png +0 -0
- entari_plugin_hyw/card-ui/public/logos/xai.png +0 -0
- entari_plugin_hyw/card-ui/public/logos/xiaomi.png +0 -0
- entari_plugin_hyw/card-ui/public/logos/zai.png +0 -0
- entari_plugin_hyw/card-ui/public/vite.svg +1 -0
- entari_plugin_hyw/card-ui/src/App.vue +412 -0
- entari_plugin_hyw/card-ui/src/assets/vue.svg +1 -0
- entari_plugin_hyw/card-ui/src/components/HelloWorld.vue +41 -0
- entari_plugin_hyw/card-ui/src/components/MarkdownContent.vue +386 -0
- entari_plugin_hyw/card-ui/src/components/SectionCard.vue +41 -0
- entari_plugin_hyw/card-ui/src/components/StageCard.vue +237 -0
- entari_plugin_hyw/card-ui/src/main.ts +5 -0
- entari_plugin_hyw/card-ui/src/style.css +29 -0
- entari_plugin_hyw/card-ui/src/test_regex.js +103 -0
- entari_plugin_hyw/card-ui/src/types.ts +52 -0
- entari_plugin_hyw/card-ui/tsconfig.app.json +16 -0
- entari_plugin_hyw/card-ui/tsconfig.json +7 -0
- entari_plugin_hyw/card-ui/tsconfig.node.json +26 -0
- entari_plugin_hyw/card-ui/vite.config.ts +16 -0
- entari_plugin_hyw/history.py +170 -0
- entari_plugin_hyw/image_cache.py +274 -0
- entari_plugin_hyw/misc.py +128 -0
- entari_plugin_hyw/pipeline.py +1338 -0
- entari_plugin_hyw/prompts.py +108 -0
- entari_plugin_hyw/render_vue.py +314 -0
- entari_plugin_hyw/search.py +696 -0
- entari_plugin_hyw-3.5.0rc6.dist-info/METADATA +116 -0
- entari_plugin_hyw-3.5.0rc6.dist-info/RECORD +88 -0
- entari_plugin_hyw/hyw_core.py +0 -555
- entari_plugin_hyw-2.2.5.dist-info/METADATA +0 -135
- entari_plugin_hyw-2.2.5.dist-info/RECORD +0 -6
- {entari_plugin_hyw-2.2.5.dist-info → entari_plugin_hyw-3.5.0rc6.dist-info}/WHEEL +0 -0
- {entari_plugin_hyw-2.2.5.dist-info → entari_plugin_hyw-3.5.0rc6.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
2
|
+
@plugin "daisyui";
|
|
3
|
+
@plugin "@tailwindcss/typography";
|
|
4
|
+
|
|
5
|
+
/* Custom styles */
|
|
6
|
+
body {
|
|
7
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/* Premium reading color scheme - applies to all prose elements */
|
|
11
|
+
.prose {
|
|
12
|
+
color: var(--text-body, #3a3a3c) !important;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.prose h1, .prose h2, .prose h3, .prose h4, .prose h5, .prose h6 {
|
|
16
|
+
color: var(--text-primary, #2c2c2e) !important;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.prose p, .prose li, .prose td, .prose th {
|
|
20
|
+
color: var(--text-body, #3a3a3c) !important;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.prose strong, .prose b {
|
|
24
|
+
color: var(--text-primary, #2c2c2e) !important;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.prose code {
|
|
28
|
+
color: var(--text-body, #3a3a3c) !important;
|
|
29
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
|
|
2
|
+
const stripPrefixBeforeH1 = (text) => {
|
|
3
|
+
const h1Match = text.match(/^#\s+/m)
|
|
4
|
+
if (h1Match && h1Match.index !== undefined) {
|
|
5
|
+
return text.substring(h1Match.index)
|
|
6
|
+
}
|
|
7
|
+
return text
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const dedent = (text) => {
|
|
11
|
+
const lines = text.split('\n')
|
|
12
|
+
// Find minimum indentation of non-empty lines
|
|
13
|
+
let minIndent = Infinity
|
|
14
|
+
for (const line of lines) {
|
|
15
|
+
if (line.trim().length === 0) continue
|
|
16
|
+
const leadingSpace = line.match(/^\s*/)?.[0].length || 0
|
|
17
|
+
if (leadingSpace < minIndent) minIndent = leadingSpace
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (minIndent === Infinity || minIndent === 0) return text
|
|
21
|
+
|
|
22
|
+
return lines.map(line => {
|
|
23
|
+
if (line.trim().length === 0) return ''
|
|
24
|
+
return line.substring(minIndent)
|
|
25
|
+
}).join('\n')
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const parse = (rawMd) => {
|
|
29
|
+
if (!rawMd) return []
|
|
30
|
+
|
|
31
|
+
const md = stripPrefixBeforeH1(rawMd)
|
|
32
|
+
|
|
33
|
+
let content = md.replace(/^#\s+.+$/m, '')
|
|
34
|
+
content = content.replace(/(?:^|\n)\s*(?:#{1,3}|\*\*)\s*(?:References|Citations|Sources)[\s\S]*$/i, '')
|
|
35
|
+
content = content.trim()
|
|
36
|
+
|
|
37
|
+
const sections = []
|
|
38
|
+
|
|
39
|
+
const combinedRegex = /(```[\s\S]*?```|((?:^|\n)\|[^\n]*\|(?:\n\|[^\n]*\|)*)|<summary>[\s\S]*?<\/summary>)/
|
|
40
|
+
|
|
41
|
+
let remaining = content
|
|
42
|
+
|
|
43
|
+
while (remaining) {
|
|
44
|
+
const match = remaining.match(combinedRegex)
|
|
45
|
+
if (!match) {
|
|
46
|
+
if (remaining.trim()) {
|
|
47
|
+
sections.push({ type: 'markdown', content: remaining.trim() })
|
|
48
|
+
}
|
|
49
|
+
break
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const index = match.index
|
|
53
|
+
const matchedStr = match[0]
|
|
54
|
+
const preText = remaining.substring(0, index)
|
|
55
|
+
|
|
56
|
+
if (preText.trim()) {
|
|
57
|
+
sections.push({ type: 'markdown', content: preText.trim() })
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const isCode = matchedStr.startsWith('```')
|
|
61
|
+
const isSummary = matchedStr.startsWith('<summary>')
|
|
62
|
+
const isTable = !isCode && !isSummary && matchedStr.trim().startsWith('|')
|
|
63
|
+
|
|
64
|
+
if (isCode || isTable || isSummary) {
|
|
65
|
+
let language = ''
|
|
66
|
+
let content = matchedStr.trim()
|
|
67
|
+
|
|
68
|
+
if (isCode) {
|
|
69
|
+
const match = matchedStr.match(/^```(\w+)/)
|
|
70
|
+
if (match && match[1]) language = match[1]
|
|
71
|
+
} else if (isSummary) {
|
|
72
|
+
content = content.replace(/^<summary>/, '').replace(/<\/summary>$/, '')
|
|
73
|
+
content = dedent(content)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
sections.push({
|
|
77
|
+
type: 'card',
|
|
78
|
+
title: isCode ? 'Code' : (isSummary ? 'Summary' : 'Table'),
|
|
79
|
+
content: content,
|
|
80
|
+
contentType: isCode ? 'code' : (isSummary ? 'summary' : 'table'),
|
|
81
|
+
language: language
|
|
82
|
+
})
|
|
83
|
+
} else {
|
|
84
|
+
sections.push({ type: 'markdown', content: matchedStr })
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
remaining = remaining.substring(index + matchedStr.length)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return sections
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const test1 = `
|
|
94
|
+
# Title
|
|
95
|
+
|
|
96
|
+
<summary>
|
|
97
|
+
Indented text.
|
|
98
|
+
It might become code block.
|
|
99
|
+
</summary>
|
|
100
|
+
`
|
|
101
|
+
|
|
102
|
+
console.log("\n--- Test 2 (After Fix) ---")
|
|
103
|
+
console.log(JSON.stringify(parse(test1), null, 2))
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// Type definitions for render data
|
|
2
|
+
// Python only passes raw data, all processing happens in frontend
|
|
3
|
+
|
|
4
|
+
export interface Stage {
|
|
5
|
+
name: string
|
|
6
|
+
model: string
|
|
7
|
+
provider: string
|
|
8
|
+
icon_name?: string // Icon identifier (e.g., "google", "openai")
|
|
9
|
+
time: number // Time in seconds (raw number)
|
|
10
|
+
cost: number // Cost in dollars (raw number)
|
|
11
|
+
references?: Reference[]
|
|
12
|
+
image_references?: ImageReference[]
|
|
13
|
+
crawled_pages?: CrawledPage[]
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface Reference {
|
|
17
|
+
title: string
|
|
18
|
+
url: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ImageReference {
|
|
22
|
+
title: string
|
|
23
|
+
url: string
|
|
24
|
+
thumbnail?: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface CrawledPage {
|
|
28
|
+
title: string
|
|
29
|
+
url: string
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface Stats {
|
|
33
|
+
total_time?: number
|
|
34
|
+
vision_duration?: number
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface Flags {
|
|
38
|
+
has_vision: boolean
|
|
39
|
+
has_search: boolean
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Raw data from Python - minimal processing
|
|
43
|
+
export interface RenderData {
|
|
44
|
+
markdown: string // Raw markdown content
|
|
45
|
+
stages: Stage[]
|
|
46
|
+
references: Reference[] // All references for citation
|
|
47
|
+
page_references: Reference[]
|
|
48
|
+
image_references: ImageReference[]
|
|
49
|
+
stats: Stats
|
|
50
|
+
total_time: number
|
|
51
|
+
theme_color?: string // Configurable theme color (hex)
|
|
52
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
|
5
|
+
"types": ["vite/client"],
|
|
6
|
+
|
|
7
|
+
/* Linting */
|
|
8
|
+
"strict": true,
|
|
9
|
+
"noUnusedLocals": true,
|
|
10
|
+
"noUnusedParameters": true,
|
|
11
|
+
"erasableSyntaxOnly": true,
|
|
12
|
+
"noFallthroughCasesInSwitch": true,
|
|
13
|
+
"noUncheckedSideEffectImports": true
|
|
14
|
+
},
|
|
15
|
+
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
|
|
16
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
|
4
|
+
"target": "ES2023",
|
|
5
|
+
"lib": ["ES2023"],
|
|
6
|
+
"module": "ESNext",
|
|
7
|
+
"types": ["node"],
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
|
|
10
|
+
/* Bundler mode */
|
|
11
|
+
"moduleResolution": "bundler",
|
|
12
|
+
"allowImportingTsExtensions": true,
|
|
13
|
+
"verbatimModuleSyntax": true,
|
|
14
|
+
"moduleDetection": "force",
|
|
15
|
+
"noEmit": true,
|
|
16
|
+
|
|
17
|
+
/* Linting */
|
|
18
|
+
"strict": true,
|
|
19
|
+
"noUnusedLocals": true,
|
|
20
|
+
"noUnusedParameters": true,
|
|
21
|
+
"erasableSyntaxOnly": true,
|
|
22
|
+
"noFallthroughCasesInSwitch": true,
|
|
23
|
+
"noUncheckedSideEffectImports": true
|
|
24
|
+
},
|
|
25
|
+
"include": ["vite.config.ts"]
|
|
26
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { defineConfig } from 'vite'
|
|
2
|
+
import vue from '@vitejs/plugin-vue'
|
|
3
|
+
import tailwindcss from '@tailwindcss/vite'
|
|
4
|
+
import { viteSingleFile } from 'vite-plugin-singlefile'
|
|
5
|
+
|
|
6
|
+
export default defineConfig({
|
|
7
|
+
plugins: [
|
|
8
|
+
vue(),
|
|
9
|
+
tailwindcss(),
|
|
10
|
+
viteSingleFile(),
|
|
11
|
+
],
|
|
12
|
+
build: {
|
|
13
|
+
outDir: '../assets/card-dist',
|
|
14
|
+
emptyOutDir: true,
|
|
15
|
+
},
|
|
16
|
+
})
|
|
@@ -0,0 +1,170 @@
|
|
|
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
|
+
import re
|
|
83
|
+
|
|
84
|
+
if key not in self._history:
|
|
85
|
+
return
|
|
86
|
+
|
|
87
|
+
try:
|
|
88
|
+
os.makedirs(save_dir, exist_ok=True)
|
|
89
|
+
|
|
90
|
+
# Extract user's first message (question) for filename
|
|
91
|
+
user_question = ""
|
|
92
|
+
for msg in self._history[key]:
|
|
93
|
+
if msg.get("role") == "user":
|
|
94
|
+
content = msg.get("content", "")
|
|
95
|
+
# Handle content that might be a list (multimodal)
|
|
96
|
+
if isinstance(content, list):
|
|
97
|
+
for item in content:
|
|
98
|
+
if isinstance(item, dict) and item.get("type") == "text":
|
|
99
|
+
user_question = item.get("text", "")
|
|
100
|
+
break
|
|
101
|
+
else:
|
|
102
|
+
user_question = str(content)
|
|
103
|
+
break
|
|
104
|
+
|
|
105
|
+
# Clean and truncate question for filename (10 chars)
|
|
106
|
+
question_part = re.sub(r'[\\/:*?"<>|\n\r\t]', '', user_question)[:10].strip()
|
|
107
|
+
if not question_part:
|
|
108
|
+
question_part = "conversation"
|
|
109
|
+
|
|
110
|
+
# Format: YYYYMMDD_HHMMSS_question.md
|
|
111
|
+
time_str = time.strftime("%Y%m%d_%H%M%S", time.localtime())
|
|
112
|
+
filename = f"{save_dir}/{time_str}_{question_part}.md"
|
|
113
|
+
|
|
114
|
+
# Formatter
|
|
115
|
+
timestamp = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
|
|
116
|
+
meta = self._metadata.get(key, {})
|
|
117
|
+
model_name = meta.get("model", "unknown")
|
|
118
|
+
code = self._key_to_code.get(key, "N/A")
|
|
119
|
+
|
|
120
|
+
md_content = f"# Conversation Log: {key}\n\n"
|
|
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"
|
|
130
|
+
|
|
131
|
+
md_content += "## History\n\n"
|
|
132
|
+
|
|
133
|
+
for msg in self._history[key]:
|
|
134
|
+
role = msg.get("role", "unknown").upper()
|
|
135
|
+
content = msg.get("content", "")
|
|
136
|
+
|
|
137
|
+
md_content += f"### {role}\n\n"
|
|
138
|
+
|
|
139
|
+
tool_calls = msg.get("tool_calls")
|
|
140
|
+
if tool_calls:
|
|
141
|
+
import json
|
|
142
|
+
try:
|
|
143
|
+
tc_str = json.dumps(tool_calls, ensure_ascii=False, indent=2)
|
|
144
|
+
except:
|
|
145
|
+
tc_str = str(tool_calls)
|
|
146
|
+
md_content += f"**Tool Calls**:\n```json\n{tc_str}\n```\n\n"
|
|
147
|
+
|
|
148
|
+
# Special handling for tool outputs or complex content
|
|
149
|
+
if role == "TOOL":
|
|
150
|
+
# Try to pretty print if it's JSON
|
|
151
|
+
try:
|
|
152
|
+
import json
|
|
153
|
+
# Content might be a JSON string already
|
|
154
|
+
parsed_content = json.loads(content)
|
|
155
|
+
pretty_content = json.dumps(parsed_content, ensure_ascii=False, indent=2)
|
|
156
|
+
md_content += f"**Output**:\n```json\n{pretty_content}\n```\n\n"
|
|
157
|
+
except:
|
|
158
|
+
md_content += f"**Output**:\n```text\n{content}\n```\n\n"
|
|
159
|
+
else:
|
|
160
|
+
if content:
|
|
161
|
+
md_content += f"{content}\n\n"
|
|
162
|
+
|
|
163
|
+
md_content += "---\n\n"
|
|
164
|
+
|
|
165
|
+
with open(filename, "w", encoding="utf-8") as f:
|
|
166
|
+
f.write(md_content)
|
|
167
|
+
|
|
168
|
+
except Exception as e:
|
|
169
|
+
# We can't log easily here without importing logger, but it's fine
|
|
170
|
+
print(f"Failed to save conversation: {e}")
|