workflow_visualization.py 41 KB

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