workflow_visualization.py 46 KB

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