workflow_visualization.py 45 KB

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