workflow_visualization.py 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. '''
  4. 知识获取工作流可视化
  5. 1. 读取知识获取工作流详细过程数据文件(参考 .cache/9f510b2a8348/execution_record.json)
  6. 2. 文件路径可设置,每个文件表示一个输入信息执行过程,可能有多个。多个文件路径硬编码在代码中,用list表示
  7. 3. 将知识获取工作流用html页面展示出来
  8. 4. HTML 文件输出路径在当前目录下,文件名称为 workflow_visualization_datetime.html
  9. '''
  10. import json
  11. import os
  12. from datetime import datetime
  13. from pathlib import Path
  14. # 硬编码的文件路径列表(相对于项目根目录)
  15. DATA_FILE_PATHS = [
  16. "../.cache/dd8dd68ebf0c/execution_record.json",
  17. "../.cache/70a208f70a7e/execution_record.json",
  18. "../.cache/996e626b9c85/execution_record.json",
  19. # 可以在这里添加更多文件路径
  20. ]
  21. def load_data_files(file_paths):
  22. """读取并解析JSON文件"""
  23. data_list = []
  24. script_dir = Path(__file__).parent
  25. for file_path in file_paths:
  26. # 将相对路径转换为绝对路径
  27. abs_path = (script_dir / file_path).resolve()
  28. if not abs_path.exists():
  29. print(f"警告: 文件不存在: {abs_path}")
  30. continue
  31. try:
  32. with open(abs_path, 'r', encoding='utf-8') as f:
  33. data = json.load(f)
  34. data_list.append(data)
  35. except (json.JSONDecodeError, IOError) as e:
  36. print(f"错误: 读取文件失败 {abs_path}: {e}")
  37. return data_list
  38. def parse_workflow_data(data):
  39. """解析工作流数据,提取关键信息"""
  40. workflow = {
  41. 'input': {},
  42. 'steps': [],
  43. 'output': {}
  44. }
  45. # 提取输入信息
  46. if 'input' in data:
  47. workflow['input'] = {
  48. 'question': data['input'].get('question', ''),
  49. 'post_info': data['input'].get('post_info', ''),
  50. 'persona_info': data['input'].get('persona_info', '')
  51. }
  52. # 提取执行流程(新格式:execution 直接包含各步骤,不再有 modules.function_knowledge)
  53. if 'execution' in data:
  54. execution = data['execution']
  55. # 步骤1: 生成query
  56. if 'generate_query' in execution:
  57. generate_query = execution['generate_query']
  58. workflow['steps'].append({
  59. 'step': 'generate_query',
  60. 'name': '生成查询',
  61. 'query': generate_query.get('query', '') or generate_query.get('response', ''),
  62. 'prompt': generate_query.get('prompt', '')
  63. })
  64. # 步骤2: 选择工具
  65. if 'select_tool' in execution:
  66. select_tool = execution['select_tool']
  67. response = select_tool.get('response', {})
  68. workflow['steps'].append({
  69. 'step': 'select_tool',
  70. 'name': '选择工具',
  71. 'prompt': select_tool.get('prompt', ''),
  72. 'tool_name': response.get('工具名', '') if isinstance(response, dict) else '',
  73. 'tool_id': response.get('工具调用ID', '') if isinstance(response, dict) else '',
  74. 'tool_usage': response.get('使用方法', '') if isinstance(response, dict) else ''
  75. })
  76. # 判断是否选择了工具(如果 response 为空字典或没有工具信息,则没有选择到工具)
  77. has_tool = False
  78. if 'select_tool' in execution:
  79. select_tool = execution['select_tool']
  80. response = select_tool.get('response', {})
  81. if isinstance(response, dict) and response.get('工具名'):
  82. has_tool = True
  83. # 如果选择了工具,执行工具调用流程
  84. if has_tool:
  85. # 步骤3: 提取参数
  86. if 'extract_params' in execution:
  87. extract_params = execution['extract_params']
  88. workflow['steps'].append({
  89. 'step': 'extract_params',
  90. 'name': '提取参数',
  91. 'prompt': extract_params.get('prompt', ''),
  92. 'params': extract_params.get('params', {})
  93. })
  94. # 步骤4: 执行工具(新格式:tool_call 替代 execute_tool)
  95. if 'tool_call' in execution:
  96. tool_call = execution['tool_call']
  97. # 优先使用 result,如果没有则使用 response
  98. result = tool_call.get('result', '')
  99. if not result and tool_call.get('response'):
  100. # 如果 response 是字典,尝试提取其中的 result
  101. response = tool_call.get('response', {})
  102. if isinstance(response, dict):
  103. result = response.get('result', response)
  104. else:
  105. result = response
  106. workflow['steps'].append({
  107. 'step': 'execute_tool',
  108. 'name': '执行工具',
  109. 'response': result or tool_call.get('response', '')
  110. })
  111. # 如果没有选择到工具,进行知识搜索流程
  112. else:
  113. if 'knowledge_search' in execution:
  114. knowledge_search = execution['knowledge_search']
  115. # 步骤3: LLM搜索(大模型+search 渠道的搜索过程)
  116. if 'llm_search' in knowledge_search:
  117. llm_search = knowledge_search['llm_search']
  118. search_results = llm_search.get('search_results', [])
  119. workflow['steps'].append({
  120. 'step': 'llm_search',
  121. 'name': 'LLM搜索',
  122. 'search_results': search_results
  123. })
  124. # 步骤4: 多渠道搜索结果整合
  125. if 'multi_search_merge' in knowledge_search:
  126. multi_search_merge = knowledge_search['multi_search_merge']
  127. workflow['steps'].append({
  128. 'step': 'multi_search_merge',
  129. 'name': '多渠道搜索结果整合',
  130. 'prompt': multi_search_merge.get('prompt', ''),
  131. 'response': multi_search_merge.get('response', ''),
  132. 'sources_count': multi_search_merge.get('sources_count', 0),
  133. 'valid_sources_count': multi_search_merge.get('valid_sources_count', 0)
  134. })
  135. # 步骤5: 发现新工具
  136. if 'extra_tools' in knowledge_search:
  137. extra_tools = knowledge_search['extra_tools']
  138. match_tool_response = extra_tools.get('match_tool_response', {})
  139. selected_tools = match_tool_response.get('selected_tools', [])
  140. workflow['steps'].append({
  141. 'step': 'extra_tools',
  142. 'name': '发现新工具',
  143. 'prompt': extra_tools.get('match_tool_prompt', ''),
  144. 'selected_tools': selected_tools,
  145. 'analysis_summary': match_tool_response.get('analysis_summary', '')
  146. })
  147. # 提取输出信息
  148. if 'output' in data:
  149. workflow['output'] = {
  150. 'result': data['output'].get('result', '')
  151. }
  152. return workflow
  153. def escape_html(text):
  154. """转义HTML特殊字符"""
  155. if not isinstance(text, str):
  156. text = str(text)
  157. return (text.replace('&', '&')
  158. .replace('<', '&lt;')
  159. .replace('>', '&gt;')
  160. .replace('"', '&quot;')
  161. .replace("'", '&#39;'))
  162. def format_json_for_display(obj):
  163. """格式化JSON对象用于显示"""
  164. if isinstance(obj, dict):
  165. return json.dumps(obj, ensure_ascii=False, indent=2)
  166. elif isinstance(obj, str):
  167. try:
  168. parsed = json.loads(obj)
  169. return json.dumps(parsed, ensure_ascii=False, indent=2)
  170. except (json.JSONDecodeError, ValueError):
  171. return obj
  172. return str(obj)
  173. def generate_html(workflows):
  174. """生成HTML页面"""
  175. html = '''<!DOCTYPE html>
  176. <html lang="zh-CN">
  177. <head>
  178. <meta charset="UTF-8">
  179. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  180. <title>知识获取工作流可视化</title>
  181. <style>
  182. * {
  183. margin: 0;
  184. padding: 0;
  185. box-sizing: border-box;
  186. }
  187. body {
  188. font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
  189. background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  190. color: #333;
  191. line-height: 1.6;
  192. min-height: 100vh;
  193. }
  194. .container {
  195. max-width: 1400px;
  196. margin: 0 auto;
  197. padding: 30px 20px;
  198. }
  199. h1 {
  200. text-align: center;
  201. color: white;
  202. margin-bottom: 40px;
  203. font-size: 32px;
  204. font-weight: 600;
  205. text-shadow: 0 2px 10px rgba(0,0,0,0.2);
  206. letter-spacing: 1px;
  207. }
  208. .tabs {
  209. display: flex;
  210. background: white;
  211. border-radius: 12px 12px 0 0;
  212. box-shadow: 0 4px 20px rgba(0,0,0,0.15);
  213. overflow-x: auto;
  214. padding: 5px;
  215. }
  216. .tab {
  217. padding: 16px 28px;
  218. cursor: pointer;
  219. border: none;
  220. background: transparent;
  221. color: #666;
  222. font-size: 14px;
  223. transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
  224. white-space: nowrap;
  225. border-radius: 8px;
  226. margin: 0 4px;
  227. position: relative;
  228. font-weight: 500;
  229. }
  230. .tab:hover {
  231. background: #f0f0f0;
  232. color: #333;
  233. }
  234. .tab.active {
  235. background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  236. color: white;
  237. box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
  238. }
  239. .tab-content {
  240. display: none;
  241. background: white;
  242. padding: 40px;
  243. border-radius: 0 0 12px 12px;
  244. box-shadow: 0 4px 20px rgba(0,0,0,0.15);
  245. margin-bottom: 20px;
  246. animation: fadeIn 0.3s ease-in;
  247. }
  248. @keyframes fadeIn {
  249. from { opacity: 0; transform: translateY(10px); }
  250. to { opacity: 1; transform: translateY(0); }
  251. }
  252. .tab-content.active {
  253. display: block;
  254. }
  255. .input-section {
  256. background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
  257. padding: 28px;
  258. border-radius: 12px;
  259. margin-bottom: 35px;
  260. box-shadow: 0 4px 15px rgba(0,0,0,0.1);
  261. border: 1px solid rgba(255,255,255,0.5);
  262. }
  263. .input-section h3 {
  264. color: #2c3e50;
  265. margin-bottom: 20px;
  266. font-size: 20px;
  267. font-weight: 600;
  268. display: flex;
  269. align-items: center;
  270. gap: 10px;
  271. }
  272. .input-section h3::before {
  273. content: '📋';
  274. font-size: 24px;
  275. }
  276. .input-item {
  277. margin-bottom: 16px;
  278. padding: 12px;
  279. background: rgba(255,255,255,0.7);
  280. border-radius: 8px;
  281. transition: all 0.3s;
  282. }
  283. .input-item:hover {
  284. background: rgba(255,255,255,0.9);
  285. transform: translateX(5px);
  286. }
  287. .input-item strong {
  288. color: #495057;
  289. display: inline-block;
  290. width: 110px;
  291. font-weight: 600;
  292. }
  293. .input-item .placeholder {
  294. color: #999;
  295. font-style: italic;
  296. }
  297. .workflow {
  298. position: relative;
  299. }
  300. .workflow-step {
  301. background: white;
  302. border: 2px solid #e0e0e0;
  303. border-radius: 12px;
  304. margin-bottom: 25px;
  305. transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
  306. overflow: hidden;
  307. box-shadow: 0 2px 8px rgba(0,0,0,0.08);
  308. }
  309. .workflow-step.active {
  310. border-color: #667eea;
  311. box-shadow: 0 8px 24px rgba(102, 126, 234, 0.25);
  312. transform: translateY(-2px);
  313. }
  314. .step-header {
  315. padding: 20px 24px;
  316. background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
  317. cursor: pointer;
  318. display: flex;
  319. justify-content: space-between;
  320. align-items: center;
  321. user-select: none;
  322. transition: all 0.3s;
  323. }
  324. .step-header:hover {
  325. background: linear-gradient(135deg, #e9ecef 0%, #dee2e6 100%);
  326. }
  327. .workflow-step.active .step-header {
  328. background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  329. color: white;
  330. }
  331. .workflow-step.active .step-name {
  332. color: white;
  333. }
  334. .workflow-step.active .step-toggle {
  335. color: white;
  336. }
  337. .step-title {
  338. display: flex;
  339. align-items: center;
  340. gap: 15px;
  341. }
  342. .step-number {
  343. display: inline-flex;
  344. align-items: center;
  345. justify-content: center;
  346. width: 36px;
  347. height: 36px;
  348. background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  349. color: white;
  350. border-radius: 50%;
  351. font-size: 16px;
  352. font-weight: bold;
  353. box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
  354. }
  355. .workflow-step.active .step-number {
  356. background: white;
  357. color: #667eea;
  358. box-shadow: 0 4px 12px rgba(255,255,255,0.3);
  359. }
  360. .step-name {
  361. font-size: 18px;
  362. font-weight: 600;
  363. color: #2c3e50;
  364. }
  365. .step-toggle {
  366. color: #6c757d;
  367. font-size: 20px;
  368. transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
  369. }
  370. .step-toggle.expanded {
  371. transform: rotate(180deg);
  372. }
  373. .step-content {
  374. padding: 0 20px;
  375. max-height: 0;
  376. overflow: hidden;
  377. transition: max-height 0.3s ease-out, padding 0.3s;
  378. }
  379. .step-content.expanded {
  380. max-height: 5000px;
  381. padding: 20px;
  382. }
  383. .step-detail {
  384. margin-bottom: 20px;
  385. }
  386. .step-detail-label {
  387. font-weight: 600;
  388. color: #495057;
  389. margin-bottom: 10px;
  390. display: block;
  391. font-size: 14px;
  392. text-transform: uppercase;
  393. letter-spacing: 0.5px;
  394. }
  395. .step-detail-content {
  396. background: #f8f9fa;
  397. padding: 16px;
  398. border-radius: 8px;
  399. border-left: 4px solid #667eea;
  400. font-size: 14px;
  401. line-height: 1.8;
  402. white-space: pre-wrap;
  403. word-wrap: break-word;
  404. max-height: 400px;
  405. overflow-y: auto;
  406. box-shadow: 0 2px 8px rgba(0,0,0,0.05);
  407. }
  408. .json-content {
  409. font-family: 'SF Mono', 'Monaco', 'Courier New', monospace;
  410. background: #1e1e1e;
  411. color: #d4d4d4;
  412. padding: 20px;
  413. border-radius: 8px;
  414. overflow-x: auto;
  415. border-left: 4px solid #667eea;
  416. box-shadow: 0 4px 12px rgba(0,0,0,0.15);
  417. }
  418. .prompt-toggle-btn {
  419. background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  420. color: white;
  421. border: none;
  422. padding: 10px 20px;
  423. border-radius: 6px;
  424. cursor: pointer;
  425. font-size: 13px;
  426. font-weight: 500;
  427. margin-top: 15px;
  428. transition: all 0.3s;
  429. box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3);
  430. }
  431. .prompt-toggle-btn:hover {
  432. transform: translateY(-2px);
  433. box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
  434. }
  435. .prompt-content {
  436. display: none;
  437. margin-top: 15px;
  438. padding: 16px;
  439. background: #fff3cd;
  440. border-radius: 8px;
  441. border-left: 4px solid #ffc107;
  442. font-size: 13px;
  443. line-height: 1.8;
  444. white-space: pre-wrap;
  445. word-wrap: break-word;
  446. max-height: 500px;
  447. overflow-y: auto;
  448. }
  449. .prompt-content.show {
  450. display: block;
  451. animation: slideDown 0.3s ease-out;
  452. }
  453. @keyframes slideDown {
  454. from {
  455. opacity: 0;
  456. max-height: 0;
  457. }
  458. to {
  459. opacity: 1;
  460. max-height: 500px;
  461. }
  462. }
  463. .output-section {
  464. background: linear-gradient(135deg, #e0f2fe 0%, #bae6fd 100%);
  465. padding: 28px;
  466. border-radius: 12px;
  467. margin-top: 35px;
  468. border-left: 4px solid #0ea5e9;
  469. box-shadow: 0 4px 15px rgba(14, 165, 233, 0.2);
  470. }
  471. .output-section h3 {
  472. color: #0369a1;
  473. margin-bottom: 20px;
  474. font-size: 20px;
  475. font-weight: 600;
  476. display: flex;
  477. align-items: center;
  478. gap: 10px;
  479. }
  480. .output-section h3::before {
  481. content: '✨';
  482. font-size: 24px;
  483. }
  484. .arrow {
  485. text-align: center;
  486. color: #667eea;
  487. font-size: 32px;
  488. margin: -15px 0;
  489. position: relative;
  490. z-index: 1;
  491. filter: drop-shadow(0 2px 4px rgba(102, 126, 234, 0.3));
  492. }
  493. .arrow::before {
  494. content: '↓';
  495. }
  496. @media (max-width: 768px) {
  497. .container {
  498. padding: 10px;
  499. }
  500. .tab {
  501. padding: 12px 15px;
  502. font-size: 13px;
  503. }
  504. .tab-content {
  505. padding: 20px;
  506. }
  507. }
  508. </style>
  509. </head>
  510. <body>
  511. <div class="container">
  512. <h1>知识获取工作流可视化</h1>
  513. <div class="tabs" id="tabs">
  514. '''
  515. # 生成Tab标签
  516. for i, workflow in enumerate(workflows):
  517. question = workflow['input'].get('question', f'问题 {i+1}')
  518. active_class = 'active' if i == 0 else ''
  519. html += f' <button class="tab {active_class}" onclick="switchTab({i})">{escape_html(question)}</button>\n'
  520. html += ' </div>\n'
  521. # 生成Tab内容
  522. for i, workflow in enumerate(workflows):
  523. active_class = 'active' if i == 0 else ''
  524. html += f' <div class="tab-content {active_class}" id="tab-{i}">\n'
  525. # 输入信息
  526. html += ' <div class="input-section">\n'
  527. html += ' <h3>输入信息</h3>\n'
  528. html += f' <div class="input-item"><strong>问题:</strong> {escape_html(workflow["input"].get("question", ""))}</div>\n'
  529. post_info = workflow['input'].get('post_info', '')
  530. post_info_display = escape_html(post_info) if post_info else '<span class="placeholder">(无)</span>'
  531. html += f' <div class="input-item"><strong>帖子信息:</strong> {post_info_display}</div>\n'
  532. persona_info = workflow['input'].get('persona_info', '')
  533. persona_info_display = escape_html(persona_info) if persona_info else '<span class="placeholder">(无)</span>'
  534. html += f' <div class="input-item"><strong>人设信息:</strong> {persona_info_display}</div>\n'
  535. html += ' </div>\n'
  536. # 工作流程
  537. html += ' <div class="workflow">\n'
  538. for j, step in enumerate(workflow['steps']):
  539. step_id = f"step-{i}-{j}"
  540. html += f' <div class="workflow-step" id="{step_id}">\n'
  541. html += ' <div class="step-header" onclick="toggleStep(\'' + step_id + '\')">\n'
  542. html += ' <div class="step-title">\n'
  543. html += f' <span class="step-number">{j+1}</span>\n'
  544. html += f' <span class="step-name">{escape_html(step["name"])}</span>\n'
  545. html += ' </div>\n'
  546. html += ' <span class="step-toggle">▼</span>\n'
  547. html += ' </div>\n'
  548. html += ' <div class="step-content" id="content-' + step_id + '">\n'
  549. # 根据步骤类型显示不同内容(prompt放在最后,默认隐藏)
  550. prompt_id = f"prompt-{step_id}"
  551. if step['step'] == 'generate_query':
  552. if step.get('query'):
  553. html += ' <div class="step-detail">\n'
  554. html += ' <span class="step-detail-label">生成的Query:</span>\n'
  555. html += f' <div class="step-detail-content">{escape_html(step["query"])}</div>\n'
  556. html += ' </div>\n'
  557. elif step['step'] == 'select_tool':
  558. # 判断是否选择了工具
  559. if step.get('tool_name'):
  560. html += ' <div class="step-detail">\n'
  561. html += ' <span class="step-detail-label">工具名称:</span>\n'
  562. html += f' <div class="step-detail-content">{escape_html(step["tool_name"])}</div>\n'
  563. html += ' </div>\n'
  564. if step.get('tool_id'):
  565. html += ' <div class="step-detail">\n'
  566. html += ' <span class="step-detail-label">工具调用ID:</span>\n'
  567. html += f' <div class="step-detail-content">{escape_html(step["tool_id"])}</div>\n'
  568. html += ' </div>\n'
  569. if step.get('tool_usage'):
  570. html += ' <div class="step-detail">\n'
  571. html += ' <span class="step-detail-label">使用方法:</span>\n'
  572. html += f' <div class="step-detail-content">{escape_html(step["tool_usage"])}</div>\n'
  573. html += ' </div>\n'
  574. else:
  575. # 无工具选择时显示提示
  576. html += ' <div class="step-detail">\n'
  577. html += ' <span class="step-detail-label">选择结果:</span>\n'
  578. html += ' <div class="step-detail-content" style="color: #dc3545; font-weight: 500;">无匹配工具</div>\n'
  579. html += ' </div>\n'
  580. elif step['step'] == 'extract_params':
  581. if step.get('params'):
  582. html += ' <div class="step-detail">\n'
  583. html += ' <span class="step-detail-label">提取的参数:</span>\n'
  584. params_str = format_json_for_display(step['params'])
  585. html += f' <div class="step-detail-content json-content">{escape_html(params_str)}</div>\n'
  586. html += ' </div>\n'
  587. elif step['step'] == 'execute_tool':
  588. if step.get('response'):
  589. html += ' <div class="step-detail">\n'
  590. html += ' <span class="step-detail-label">执行结果:</span>\n'
  591. response_str = format_json_for_display(step['response'])
  592. html += f' <div class="step-detail-content json-content">{escape_html(response_str)}</div>\n'
  593. html += ' </div>\n'
  594. elif step['step'] == 'llm_search':
  595. search_results = step.get('search_results', [])
  596. if search_results:
  597. html += ' <div class="step-detail">\n'
  598. html += ' <span class="step-detail-label">搜索结果:</span>\n'
  599. for idx, result in enumerate(search_results, 1):
  600. query = result.get('query', '')
  601. content = result.get('content', '')
  602. html += ' <div style="margin-bottom: 15px; padding: 12px; background: #f0f8ff; border-radius: 6px; border-left: 3px solid #4a90e2;">\n'
  603. html += f' <div style="font-weight: 600; color: #2c3e50; margin-bottom: 8px;">查询 {idx}: {escape_html(query)}</div>\n'
  604. html += f' <div style="color: #555; line-height: 1.6; white-space: pre-wrap;">{escape_html(content)}</div>\n'
  605. html += ' </div>\n'
  606. html += ' </div>\n'
  607. elif step['step'] == 'multi_search_merge':
  608. if step.get('sources_count') is not None:
  609. html += ' <div class="step-detail">\n'
  610. html += ' <span class="step-detail-label">来源统计:</span>\n'
  611. html += f' <div class="step-detail-content">总来源数: {step.get("sources_count", 0)}, 有效来源数: {step.get("valid_sources_count", 0)}</div>\n'
  612. html += ' </div>\n'
  613. if step.get('response'):
  614. html += ' <div class="step-detail">\n'
  615. html += ' <span class="step-detail-label">整合结果:</span>\n'
  616. response_str = format_json_for_display(step['response'])
  617. html += f' <div class="step-detail-content">{escape_html(response_str)}</div>\n'
  618. html += ' </div>\n'
  619. elif step['step'] == 'extra_tools':
  620. selected_tools = step.get('selected_tools', [])
  621. if selected_tools:
  622. html += ' <div class="step-detail">\n'
  623. html += ' <span class="step-detail-label">发现的新工具:</span>\n'
  624. for idx, tool in enumerate(selected_tools, 1):
  625. tool_name = tool.get('tool_name', '')
  626. tool_function = tool.get('function', '')
  627. html += ' <div style="margin-bottom: 15px; padding: 12px; background: #fff3cd; border-radius: 6px; border-left: 3px solid #ffc107;">\n'
  628. html += f' <div style="font-weight: 600; color: #856404; margin-bottom: 6px;">工具 {idx}: {escape_html(tool_name)}</div>\n'
  629. html += f' <div style="color: #856404; line-height: 1.6;">{escape_html(tool_function)}</div>\n'
  630. html += ' </div>\n'
  631. html += ' </div>\n'
  632. else:
  633. html += ' <div class="step-detail">\n'
  634. html += ' <span class="step-detail-label">发现结果:</span>\n'
  635. html += ' <div class="step-detail-content" style="color: #6c757d;">未发现合适的新工具</div>\n'
  636. html += ' </div>\n'
  637. if step.get('analysis_summary'):
  638. html += ' <div class="step-detail">\n'
  639. html += ' <span class="step-detail-label">分析摘要:</span>\n'
  640. html += f' <div class="step-detail-content">{escape_html(step["analysis_summary"])}</div>\n'
  641. html += ' </div>\n'
  642. # Prompt放在最后,默认隐藏(generate_query 步骤不显示 prompt)
  643. if step.get('prompt') and step['step'] != 'generate_query':
  644. html += f' <button class="prompt-toggle-btn" onclick="togglePrompt(\'{prompt_id}\')">显示 Prompt</button>\n'
  645. html += f' <div class="prompt-content" id="{prompt_id}">{escape_html(step["prompt"])}</div>\n'
  646. html += ' </div>\n'
  647. html += ' </div>\n'
  648. # 添加箭头(除了最后一步)
  649. if j < len(workflow['steps']) - 1:
  650. html += ' <div class="arrow"></div>\n'
  651. html += ' </div>\n'
  652. # 输出信息
  653. if workflow['output'].get('result'):
  654. html += ' <div class="output-section">\n'
  655. html += ' <h3>最终输出</h3>\n'
  656. result_str = format_json_for_display(workflow['output']['result'])
  657. html += f' <div class="step-detail-content json-content">{escape_html(result_str)}</div>\n'
  658. html += ' </div>\n'
  659. html += ' </div>\n'
  660. html += ''' </div>
  661. <script>
  662. function switchTab(index) {
  663. // 隐藏所有tab内容
  664. const contents = document.querySelectorAll('.tab-content');
  665. contents.forEach(content => content.classList.remove('active'));
  666. // 移除所有tab的active状态
  667. const tabs = document.querySelectorAll('.tab');
  668. tabs.forEach(tab => tab.classList.remove('active'));
  669. // 显示选中的tab内容
  670. document.getElementById('tab-' + index).classList.add('active');
  671. tabs[index].classList.add('active');
  672. }
  673. function toggleStep(stepId) {
  674. const step = document.getElementById(stepId);
  675. const content = document.getElementById('content-' + stepId);
  676. const toggle = step.querySelector('.step-toggle');
  677. if (content.classList.contains('expanded')) {
  678. content.classList.remove('expanded');
  679. toggle.classList.remove('expanded');
  680. step.classList.remove('active');
  681. } else {
  682. content.classList.add('expanded');
  683. toggle.classList.add('expanded');
  684. step.classList.add('active');
  685. }
  686. }
  687. function togglePrompt(promptId) {
  688. const promptContent = document.getElementById(promptId);
  689. const btn = promptContent.previousElementSibling;
  690. if (promptContent.classList.contains('show')) {
  691. promptContent.classList.remove('show');
  692. btn.textContent = '显示 Prompt';
  693. } else {
  694. promptContent.classList.add('show');
  695. btn.textContent = '隐藏 Prompt';
  696. }
  697. }
  698. // 页面加载时高亮第一个步骤
  699. window.addEventListener('load', function() {
  700. const firstSteps = document.querySelectorAll('.workflow-step');
  701. firstSteps.forEach((step, index) => {
  702. if (index === 0 || index % (firstSteps.length / document.querySelectorAll('.tab-content').length) === 0) {
  703. step.classList.add('active');
  704. const content = step.querySelector('.step-content');
  705. const toggle = step.querySelector('.step-toggle');
  706. if (content) {
  707. content.classList.add('expanded');
  708. toggle.classList.add('expanded');
  709. }
  710. }
  711. });
  712. });
  713. </script>
  714. </body>
  715. </html>'''
  716. return html
  717. def main():
  718. """主函数"""
  719. # 获取当前脚本所在目录
  720. script_dir = Path(__file__).parent
  721. os.chdir(script_dir)
  722. # 读取数据文件
  723. print("正在读取数据文件...")
  724. data_list = load_data_files(DATA_FILE_PATHS)
  725. if not data_list:
  726. print("错误: 没有成功读取任何数据文件")
  727. return
  728. print(f"成功读取 {len(data_list)} 个数据文件")
  729. # 解析工作流数据
  730. print("正在解析工作流数据...")
  731. workflows = [parse_workflow_data(data) for data in data_list]
  732. # 生成HTML
  733. print("正在生成HTML页面...")
  734. html = generate_html(workflows)
  735. # 保存HTML文件
  736. timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
  737. output_filename = f"workflow_visualization_{timestamp}.html"
  738. with open(output_filename, 'w', encoding='utf-8') as f:
  739. f.write(html)
  740. print(f"HTML页面已生成: {output_filename}")
  741. print(f"文件路径: {os.path.abspath(output_filename)}")
  742. if __name__ == '__main__':
  743. main()