outline_agent.py 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727
  1. """
  2. 报告大纲生成Agent (Report Outline Generation Agent)
  3. ===============================================
  4. 此Agent负责根据用户需求和数据样本,生成专业的报告大纲结构。
  5. 核心功能:
  6. 1. 分析用户需求:理解报告目标和关键指标
  7. 2. 数据结构分析:识别可用字段和数据特征
  8. 3. 大纲生成:创建结构化的报告章节和指标需求
  9. 4. 智能推断:自动推断所需字段和计算逻辑
  10. 工作流程:
  11. 1. 接收用户查询和数据样本
  12. 2. 分析数据结构和可用字段
  13. 3. 生成报告标题和章节结构
  14. 4. 定义全局指标需求
  15. 5. 返回结构化的大纲对象
  16. 技术实现:
  17. - 使用LangChain和结构化输出
  18. - 支持异步处理
  19. - 自动字段推断和补全
  20. - 错误处理和默认值提供
  21. 作者: Big Agent Team
  22. 版本: 1.0.0
  23. 创建时间: 2024-12-20
  24. """
  25. from typing import List, Dict, Any
  26. from langchain_openai import ChatOpenAI
  27. from langchain_core.prompts import ChatPromptTemplate
  28. import json
  29. import os
  30. import uuid
  31. import requests
  32. from datetime import datetime
  33. from pydantic import BaseModel, Field
  34. # 数据模型定义(与现有项目兼容)
  35. class MetricRequirement(BaseModel):
  36. """指标需求定义"""
  37. metric_id: str = Field(description="指标唯一标识,如 'total_income_jan'")
  38. metric_name: str = Field(description="指标中文名称")
  39. calculation_logic: str = Field(description="计算逻辑描述")
  40. required_fields: List[str] = Field(description="所需字段")
  41. dependencies: List[str] = Field(default_factory=list, description="依赖的其他指标ID")
  42. class ReportSection(BaseModel):
  43. """报告大纲章节"""
  44. section_id: str = Field(description="章节ID")
  45. title: str = Field(description="章节标题")
  46. description: str = Field(description="章节内容要求")
  47. metrics_needed: List[str] = Field(description="所需指标ID列表")
  48. class ReportOutline(BaseModel):
  49. """完整报告大纲"""
  50. report_title: str = Field(description="报告标题")
  51. sections: List[ReportSection] = Field(description="章节列表")
  52. global_metrics: List[MetricRequirement] = Field(description="全局指标列表")
  53. class OutlineGeneratorAgent:
  54. """大纲生成智能体:将报告需求转化为结构化大纲"""
  55. def __init__(self, api_key: str, base_url: str = "https://api.deepseek.com"):
  56. """
  57. 初始化大纲生成Agent
  58. Args:
  59. api_key: DeepSeek API密钥
  60. base_url: DeepSeek API基础URL
  61. """
  62. self.llm = ChatOpenAI(
  63. model="deepseek-chat",
  64. api_key=api_key,
  65. base_url=base_url,
  66. temperature=0.1
  67. )
  68. # 初始化API调用跟踪
  69. self.api_calls = []
  70. # 获取可用的知识元数据
  71. self.available_knowledge = self._load_available_knowledge()
  72. def _convert_new_format_to_outline(self, new_format_data: Dict[str, Any]) -> Dict[str, Any]:
  73. """将新的JSON格式转换为原来的ReportOutline格式"""
  74. # 转换sections
  75. sections = []
  76. for section_data in new_format_data.get("sections", []):
  77. # 从metrics中提取指标名称
  78. metrics_needed = []
  79. for metric_type in ["calculation_metrics", "statistical_metrics", "analysis_metrics"]:
  80. for metric in section_data.get("metrics", {}).get(metric_type, []):
  81. # 这里可以根据metric_name映射到实际的metric_id
  82. # 暂时使用metric_name作为metric_id
  83. metrics_needed.append(metric.get("metric_name", ""))
  84. section = {
  85. "section_id": section_data.get("section_id", ""),
  86. "title": section_data.get("section_title", ""),
  87. "description": section_data.get("section_description", ""),
  88. "metrics_needed": metrics_needed
  89. }
  90. sections.append(section)
  91. # 生成global_metrics:使用知识ID进行匹配,并强制添加更多农业相关指标
  92. global_metrics = []
  93. used_knowledge_ids = set()
  94. # 首先处理LLM生成的指标
  95. for section in sections:
  96. for metric_name in section["metrics_needed"]:
  97. # 查找对应的指标描述(从原始数据中获取)
  98. metric_description = ""
  99. for section_data in new_format_data.get("sections", []):
  100. for metric_type in ["calculation_metrics", "statistical_metrics", "analysis_metrics"]:
  101. for metric in section_data.get("metrics", {}).get(metric_type, []):
  102. if metric.get("metric_name") == metric_name:
  103. metric_description = metric.get("metric_description", "")
  104. break
  105. if metric_description:
  106. break
  107. if metric_description:
  108. break
  109. # 使用知识ID匹配算法找到最佳匹配
  110. knowledge_id = self._match_metric_to_knowledge(metric_name, metric_description)
  111. # 如果找到匹配的知识ID,使用它作为metric_id
  112. if knowledge_id and knowledge_id not in used_knowledge_ids:
  113. global_metrics.append({
  114. "metric_id": knowledge_id, # 使用知识ID作为metric_id
  115. "metric_name": metric_name,
  116. "calculation_logic": f"使用规则引擎计算{metric_name}: {metric_description}",
  117. "required_fields": ["transactions"], # 规则引擎使用transactions数据
  118. "dependencies": []
  119. })
  120. used_knowledge_ids.add(knowledge_id)
  121. else:
  122. # 如果没有找到匹配的知识ID,生成一个基本的MetricRequirement作为备选
  123. if not any(m.get("metric_id") == metric_name for m in global_metrics):
  124. print(f"⚠️ 指标 '{metric_name}' 未找到匹配的知识ID,使用默认配置")
  125. global_metrics.append({
  126. "metric_id": metric_name,
  127. "metric_name": metric_name,
  128. "calculation_logic": f"计算{metric_name}: {metric_description}",
  129. "required_fields": ["txAmount", "txDirection"],
  130. "dependencies": []
  131. })
  132. # 注意:现在依赖LLM根据提示词生成包含所有必需指标的大纲,不再在代码中强制添加
  133. # 如果LLM没有提供任何指标,则自动补充基础指标
  134. if not global_metrics:
  135. print("⚠️ LLM未提供指标,使用默认基础指标")
  136. available_metrics = self._load_available_metrics()
  137. # 选择前5个基础指标
  138. base_metrics = [m for m in available_metrics if m.get('type') == '基础统计指标'][:5]
  139. for metric in base_metrics:
  140. metric_name = metric['name']
  141. knowledge_id = f"metric-{metric_name}"
  142. if sections: # 确保有章节
  143. sections[0]["metrics_needed"].append(knowledge_id) # 添加到第一个章节
  144. global_metrics.append({
  145. "metric_id": knowledge_id,
  146. "metric_name": metric_name,
  147. "calculation_logic": f"使用规则引擎计算{metric_name}: {metric.get('description', '')}",
  148. "required_fields": ["transactions"],
  149. "dependencies": []
  150. })
  151. print(f"📊 最终生成 {len(global_metrics)} 个指标")
  152. return {
  153. "report_title": new_format_data.get("chapter_title", "流水分析报告"),
  154. "sections": sections,
  155. "global_metrics": global_metrics
  156. }
  157. def create_prompt(self) -> str:
  158. """创建大纲生成提示"""
  159. # 从API动态获取可用的指标列表
  160. available_metrics = self._load_available_metrics()
  161. # 构建指标列表文本
  162. metrics_list_text = "指标名称\t指标类型\t指标描述\n"
  163. for metric in available_metrics:
  164. metrics_list_text += f"{metric['name']}\t{metric.get('type', '计算型指标')}\t{metric.get('description', '')}\n"
  165. # 构建基础提示词
  166. base_prompt = f"""[角色定义]
  167. 你的角色是: 流水分析报告的大纲生成模块。
  168. 你的目标是:
  169. 基于输入的流水分析业务背景信息,
  170. 生成一份可交付、结构清晰、可被程序解析的流水分析报告大纲,
  171. 并以结构化 JSON 的形式,明确每个章节及其下属分析主题所需的分析指标与分析项要求,
  172. 以指导后续分析能力的调用。
  173. [职责边界]
  174. 你只能完成以下事项:
  175. 1.确定流水分析报告应包含的章节结构
  176. 2.明确每个章节下需要覆盖的分析主题
  177. 3.为每个分析主题列出所需的计算指标、统计指标或分析指标
  178. 你不得做以下任何事情:
  179. 1.不得计算任何指标
  180. 2.不得对流水数据进行分析
  181. 3.不得判断交易是否异常或存在风险
  182. 4.不得生成任何分析结论、判断性描述或报告正文
  183. 5.不得决定分析执行顺序或分析方法
  184. 你输出的内容仅是"分析需求清单",而不是"分析结果"。
  185. [可用指标总览]
  186. 系统当前支持 {len(available_metrics)} 个指标。
  187. [重要要求]
  188. 请根据用户需求和可用指标列表,从上述指标中选择最相关的指标。优先选择基础统计指标和时间分析指标,确保报告的完整性和实用性。
  189. [强制要求]
  190. 生成大纲时,请:
  191. 1. 从可用指标中选择合适的指标组合
  192. 2. 确保选择的指标能够满足用户分析需求
  193. 3. 在metrics_needed数组中列出选定的指标名称
  194. 4. 在global_metrics数组中包含对应指标的详细定义
  195. [可选择的指标列表]
  196. {metrics_list_text}
  197. [重要兼容性要求]
  198. 虽然你必须使用上述JSON结构输出,但为了确保与现有系统的兼容性,请在输出中额外包含以下字段:
  199. - 在根级别添加 "report_title": "流水分析报告"
  200. - 在根级别添加 "global_metrics": [] (空数组或根据实际需求填充指标定义)
  201. - 确保输出能被现有系统正确解析和使用
  202. [输出格式要求]
  203. 你必须且只能以 JSON 字符串 形式输出分析大纲,不得输出任何解释性自然语言。
  204. JSON 必须严格遵循以下结构约定:
  205. {{
  206. "chapter_id": "string",
  207. "chapter_title": "string",
  208. "chapter_type": "string",
  209. "sections": [
  210. {{
  211. "section_id": "string",
  212. "section_title": "string",
  213. "section_description": "string",
  214. "metrics_needed": ["string"]
  215. }}
  216. ],
  217. "global_metrics": []
  218. }}"""
  219. return base_prompt
  220. print(f"📊 最终生成 {len(global_metrics)} 个指标")
  221. return {
  222. "report_title": new_format_data.get("chapter_title", "流水分析报告"),
  223. "sections": sections,
  224. "global_metrics": global_metrics
  225. }
  226. async def generate_outline(self, question: str, industry: str, sample_data: List[Dict[str, Any]]) -> ReportOutline:
  227. """异步生成大纲(修复版:自动补全缺失字段)"""
  228. prompt = self.create_prompt()
  229. # 在prompt末尾添加业务背景信息
  230. full_prompt = f"""{prompt}
  231. 【业务背景信息】
  232. 行业:{industry}
  233. 产品类型:经营贷
  234. 客群类型:小微企业"""
  235. messages = [
  236. ("system", "你是一名专业的报告大纲生成专家,必须输出完整、有效的JSON格式,包含所有必需字段。"),
  237. ("user", full_prompt)
  238. ]
  239. # 记录大模型输入
  240. print("========================================")
  241. print("[AGENT] OutlineGeneratorAgent (大纲生成Agent)")
  242. print(f"[KNOWLEDGE_BASE] 已加载 {len(self.available_knowledge)} 个知识元数据")
  243. if self.available_knowledge:
  244. sample_knowledge = self.available_knowledge[:3] # 显示前3个作为示例
  245. print(f"[KNOWLEDGE_SAMPLE] 示例知识: {[k.get('id', '') for k in sample_knowledge]}")
  246. print("[MODEL_INPUT] OutlineGeneratorAgent:")
  247. print(f"[CONTEXT] 基于用户需求和数据样本生成报告大纲")
  248. print(f"Question: {question}")
  249. print(f"Sample data count: {len(sample_data)}")
  250. print("========================================")
  251. # 执行API调用
  252. start_time = datetime.now()
  253. response = await self.llm.ainvoke(messages)
  254. end_time = datetime.now()
  255. # 解析JSON响应
  256. try:
  257. # 从响应中提取JSON内容
  258. content = response.content if hasattr(response, 'content') else str(response)
  259. # 尝试找到JSON部分
  260. json_start = content.find('{')
  261. json_end = content.rfind('}') + 1
  262. if json_start >= 0 and json_end > json_start:
  263. json_str = content[json_start:json_end]
  264. outline_data = json.loads(json_str)
  265. # 转换新的JSON格式为原来的ReportOutline格式
  266. converted_data = self._convert_new_format_to_outline(outline_data)
  267. outline = ReportOutline(**converted_data)
  268. else:
  269. raise ValueError("No JSON found in response")
  270. except Exception as e:
  271. print(f"解析大纲响应失败: {e},使用默认大纲")
  272. # 不在这里创建大纲,在函数末尾统一处理
  273. # 记录API调用结果
  274. call_id = f"api_mll_大纲生成_{'{:.2f}'.format((end_time - start_time).total_seconds())}"
  275. api_call_info = {
  276. "call_id": call_id,
  277. "timestamp": end_time.isoformat(),
  278. "agent": "OutlineGeneratorAgent",
  279. "model": "deepseek-chat",
  280. "request": {
  281. "question": question,
  282. "sample_data_count": len(sample_data),
  283. "prompt": prompt,
  284. "start_time": start_time.isoformat()
  285. },
  286. "response": {
  287. "content": content,
  288. "end_time": end_time.isoformat(),
  289. "duration": (end_time - start_time).total_seconds()
  290. },
  291. "success": True
  292. }
  293. self.api_calls.append(api_call_info)
  294. # 保存API结果到文件
  295. api_results_dir = "api_results"
  296. os.makedirs(api_results_dir, exist_ok=True)
  297. filename = f"{call_id}.json"
  298. filepath = os.path.join(api_results_dir, filename)
  299. try:
  300. with open(filepath, 'w', encoding='utf-8') as f:
  301. json.dump(api_call_info, f, ensure_ascii=False, indent=2)
  302. print(f"[API_RESULT] 保存API结果文件: {filepath}")
  303. except Exception as e:
  304. print(f"[ERROR] 保存API结果文件失败: {filepath}, 错误: {str(e)}")
  305. # 记录大模型输出
  306. print(f"[MODEL_OUTPUT] OutlineGeneratorAgent: {json.dumps(outline.dict() if hasattr(outline, 'dict') else outline, ensure_ascii=False)}")
  307. print("========================================")
  308. # 后处理,补全缺失的section_id和metric_id
  309. outline = self._post_process_outline(outline)
  310. return outline
  311. def _post_process_outline(self, outline: ReportOutline) -> ReportOutline:
  312. """
  313. 后处理大纲,自动补全缺失的必需字段
  314. """
  315. # 为章节补全section_id
  316. for idx, section in enumerate(outline.sections):
  317. if not section.section_id:
  318. section.section_id = f"sec_{idx + 1}"
  319. # 确保metrics_needed是列表
  320. if not isinstance(section.metrics_needed, list):
  321. section.metrics_needed = []
  322. # 为指标补全metric_id和dependencies
  323. for idx, metric in enumerate(outline.global_metrics):
  324. if not metric.metric_id:
  325. metric.metric_id = f"metric_{idx + 1}"
  326. # 确保dependencies是列表
  327. if not isinstance(metric.dependencies, list):
  328. metric.dependencies = []
  329. # 推断required_fields(如果为空)
  330. if not metric.required_fields:
  331. metric.required_fields = self._infer_required_fields(
  332. metric.calculation_logic
  333. )
  334. return outline
  335. def _infer_required_fields(self, logic: str) -> List[str]:
  336. """从计算逻辑推断所需字段"""
  337. field_mapping = {
  338. "收入": ["txAmount", "txDirection"],
  339. "支出": ["txAmount", "txDirection"],
  340. "余额": ["txBalance"],
  341. "对手方": ["txCounterparty"],
  342. "日期": ["txDate"],
  343. "时间": ["txTime", "txDate"],
  344. "摘要": ["txSummary"],
  345. "创建时间": ["createdAt"]
  346. }
  347. fields = []
  348. for keyword, field_list in field_mapping.items():
  349. if keyword in logic:
  350. fields.extend(field_list)
  351. return list(set(fields))
  352. def _load_available_knowledge(self) -> List[Dict[str, Any]]:
  353. """
  354. 从规则引擎获取可用的知识元数据
  355. Returns:
  356. 知识元数据列表,包含id和description
  357. """
  358. try:
  359. url = "http://10.192.72.11:31809/api/rules/getKnowledgeMeta"
  360. headers = {
  361. "Accept": "*/*",
  362. "Accept-Encoding": "gzip, deflate, br",
  363. "Connection": "keep-alive",
  364. "Content-Type": "application/json",
  365. "User-Agent": "PostmanRuntime-ApipostRuntime/1.1.0"
  366. }
  367. response = requests.post(url, headers=headers, json={}, timeout=30)
  368. if response.status_code == 200:
  369. knowledge_meta = response.json()
  370. if isinstance(knowledge_meta, list):
  371. print(f"✅ 成功获取 {len(knowledge_meta)} 个知识元数据")
  372. return knowledge_meta
  373. else:
  374. print(f"⚠️ 知识元数据格式异常: {knowledge_meta}")
  375. return []
  376. else:
  377. print(f"❌ 获取知识元数据失败,状态码: {response.status_code}")
  378. print(f"响应内容: {response.text}")
  379. return []
  380. except Exception as e:
  381. print(f"❌ 获取知识元数据时发生错误: {str(e)}")
  382. return []
  383. def _load_available_metrics(self) -> List[Dict[str, str]]:
  384. """
  385. 从知识库中提取可用的指标列表
  386. Returns:
  387. 指标列表,包含name和description字段
  388. """
  389. knowledge_list = self._load_available_knowledge()
  390. metrics = []
  391. for knowledge in knowledge_list:
  392. knowledge_id = knowledge.get("id", "")
  393. description = knowledge.get("description", "")
  394. # 从知识ID中提取指标名称
  395. if knowledge_id.startswith("metric-"):
  396. metric_name = knowledge_id.replace("metric-", "")
  397. # 从描述中提取更简洁的指标描述
  398. short_description = self._extract_metric_description(description)
  399. metrics.append({
  400. "name": metric_name,
  401. "description": short_description,
  402. "type": self._classify_metric_type(metric_name, description)
  403. })
  404. print(f"✅ 从知识库中提取了 {len(metrics)} 个可用指标")
  405. return metrics
  406. def _extract_metric_description(self, full_description: str) -> str:
  407. """从完整描述中提取简洁的指标描述"""
  408. # 移除"因子概述:"等前缀
  409. description = full_description.replace("因子概述:", "").strip()
  410. # 如果描述太长,取前50个字符
  411. if len(description) > 50:
  412. description = description[:50] + "..."
  413. return description
  414. def _classify_metric_type(self, metric_name: str, description: str) -> str:
  415. """根据指标名称和描述分类指标类型"""
  416. if any(keyword in metric_name for keyword in ["收入", "支出", "金额", "交易笔数"]):
  417. return "基础统计指标"
  418. elif any(keyword in metric_name for keyword in ["时间范围", "时间跨度"]):
  419. return "时间分析指标"
  420. elif any(keyword in metric_name for keyword in ["比例", "占比", "构成"]):
  421. return "结构分析指标"
  422. elif any(keyword in metric_name for keyword in ["排名", "TOP", "前三"]):
  423. return "专项分析指标"
  424. elif any(keyword in metric_name for keyword in ["账户", "数量"]):
  425. return "账户分析指标"
  426. else:
  427. return "其他指标"
  428. def _match_metric_to_knowledge(self, metric_name: str, metric_description: str) -> str:
  429. """
  430. 根据指标名称和描述匹配最合适的知识ID
  431. Args:
  432. metric_name: 指标名称
  433. metric_description: 指标描述
  434. Returns:
  435. 匹配的知识ID,如果没有找到则返回空字符串
  436. """
  437. if not self.available_knowledge:
  438. return ""
  439. # 精确匹配:直接用指标名称匹配知识ID
  440. for knowledge in self.available_knowledge:
  441. knowledge_id = knowledge.get("id", "")
  442. # 去掉前缀匹配,如 "metric-分析账户数量" 匹配 "分析账户数量"
  443. if knowledge_id.startswith("metric-") and knowledge_id.replace("metric-", "") == metric_name:
  444. print(f"🔗 精确匹配指标 '{metric_name}' -> 知识ID: {knowledge_id}")
  445. return knowledge_id
  446. # 扩展匹配:匹配更多的农业相关指标
  447. if "农业" in metric_name:
  448. if "总经营收入" in metric_name:
  449. # 匹配农业总经营收入
  450. for knowledge in self.available_knowledge:
  451. if knowledge.get("id") == "metric-农业总经营收入":
  452. print(f"🔗 扩展匹配指标 '{metric_name}' -> 知识ID: metric-农业总经营收入")
  453. return "metric-农业总经营收入"
  454. if "总经营支出" in metric_name:
  455. # 匹配农业总经营支出
  456. for knowledge in self.available_knowledge:
  457. if knowledge.get("id") == "metric-农业总经营支出":
  458. print(f"🔗 扩展匹配指标 '{metric_name}' -> 知识ID: metric-农业总经营支出")
  459. return "metric-农业总经营支出"
  460. if "交易对手收入排名TOP3" in metric_name or "收入排名" in metric_name:
  461. # 匹配农业交易对手收入TOP3
  462. for knowledge in self.available_knowledge:
  463. if knowledge.get("id") == "metric-农业交易对手经营收入top3":
  464. print(f"🔗 扩展匹配指标 '{metric_name}' -> 知识ID: metric-农业交易对手经营收入top3")
  465. return "metric-农业交易对手经营收入top3"
  466. if "交易对手支出排名TOP3" in metric_name or "支出排名" in metric_name:
  467. # 匹配农业交易对手支出TOP3
  468. for knowledge in self.available_knowledge:
  469. if knowledge.get("id") == "metric-农业交易对手经营支出top3":
  470. print(f"🔗 扩展匹配指标 '{metric_name}' -> 知识ID: metric-农业交易对手经营支出top3")
  471. return "metric-农业交易对手经营支出top3"
  472. # 如果精确匹配失败,使用关键词匹配
  473. keywords = [metric_name]
  474. if metric_description:
  475. # 从描述中提取关键信息
  476. desc_lower = metric_description.lower()
  477. if "收入" in metric_name or "收入" in desc_lower:
  478. keywords.extend(["收入", "总收入", "经营收入"])
  479. if "支出" in metric_name or "支出" in desc_lower:
  480. keywords.extend(["支出", "总支出", "经营支出"])
  481. if "排名" in metric_name or "top" in desc_lower:
  482. keywords.append("排名")
  483. if "比例" in metric_name or "占比" in desc_lower:
  484. keywords.append("比例")
  485. if "时间范围" in metric_name:
  486. keywords.append("时间范围")
  487. if "账户" in metric_name:
  488. keywords.append("账户")
  489. best_match = None
  490. best_score = 0
  491. for knowledge in self.available_knowledge:
  492. knowledge_id = knowledge.get("id", "")
  493. knowledge_desc = knowledge.get("description", "").lower()
  494. # 计算匹配分数
  495. score = 0
  496. for keyword in keywords:
  497. if keyword.lower() in knowledge_desc:
  498. score += 1
  499. # 行业匹配加分
  500. if "黑色金属" in knowledge_desc and "黑色金属" in metric_name:
  501. score += 2
  502. if "农业" in knowledge_desc and "农业" in metric_name:
  503. score += 2
  504. # 直接名称匹配加分
  505. if metric_name.lower() in knowledge_desc:
  506. score += 3
  507. if score > best_score:
  508. best_score = score
  509. best_match = knowledge_id
  510. if best_match and best_score > 0:
  511. print(f"🔗 关键词匹配指标 '{metric_name}' -> 知识ID: {best_match} (匹配分数: {best_score})")
  512. return best_match
  513. print(f"❌ 指标 '{metric_name}' 未找到匹配的知识ID")
  514. return ""
  515. async def generate_report_outline(question: str, industry: str, sample_data: List[Dict[str, Any]], api_key: str, max_retries: int = 3, retry_delay: float = 2.0) -> ReportOutline:
  516. """
  517. 生成报告大纲的主函数,支持重试机制
  518. Args:
  519. question: 用户查询问题
  520. industry: 行业
  521. sample_data: 数据样本
  522. api_key: API密钥
  523. max_retries: 最大重试次数,默认3次
  524. retry_delay: 重试间隔时间(秒),默认2秒
  525. Returns:
  526. 生成的报告大纲
  527. """
  528. import asyncio
  529. import time
  530. agent = OutlineGeneratorAgent(api_key)
  531. print(f"📝 开始生成报告大纲(最多重试 {max_retries} 次)...")
  532. for attempt in range(max_retries):
  533. try:
  534. print(f" 尝试 {attempt + 1}/{max_retries}...")
  535. start_time = time.time()
  536. outline = await agent.generate_outline(question, industry, sample_data)
  537. elapsed_time = time.time() - start_time
  538. print(".2f")
  539. print("\n📝 大纲生成成功:")
  540. print(f" 标题:{outline.report_title}")
  541. print(f" 章节数:{len(outline.sections)}")
  542. print(f" 指标数:{len(outline.global_metrics)}")
  543. return outline
  544. except Exception as e:
  545. elapsed_time = time.time() - start_time if 'start_time' in locals() else 0
  546. print(".2f")
  547. print(f" 错误详情: {str(e)}")
  548. # 如果不是最后一次尝试,等待后重试
  549. if attempt < max_retries - 1:
  550. print(f" ⏳ {retry_delay} 秒后进行第 {attempt + 2} 次重试...")
  551. await asyncio.sleep(retry_delay)
  552. # 增加重试间隔,避免频繁调用
  553. retry_delay = min(retry_delay * 1.5, 10.0) # 最多等待10秒
  554. else:
  555. print(f" ❌ 已达到最大重试次数 ({max_retries}),使用默认结构")
  556. # 所有重试都失败后,使用默认结构
  557. print("⚠️ 所有重试均失败,使用默认大纲结构")
  558. # 获取实际可用的指标来构建默认大纲
  559. available_metrics = self._load_available_metrics()
  560. # 选择一些基础指标作为默认值
  561. default_metric_ids = []
  562. default_global_metrics = []
  563. # 优先选择基础统计指标
  564. base_metrics = [m for m in available_metrics if m.get('type') == '基础统计指标']
  565. if base_metrics:
  566. # 选择前3个基础指标
  567. for metric in base_metrics[:3]:
  568. metric_name = metric['name']
  569. knowledge_id = f"metric-{metric_name}"
  570. default_metric_ids.append(knowledge_id)
  571. default_global_metrics.append(MetricRequirement(
  572. metric_id=knowledge_id,
  573. metric_name=metric_name,
  574. calculation_logic=f"使用规则引擎计算{metric_name}: {metric.get('description', '')}",
  575. required_fields=["transactions"],
  576. dependencies=[]
  577. ))
  578. # 如果基础指标不够,补充其他类型的指标
  579. if len(default_metric_ids) < 3:
  580. other_metrics = [m for m in available_metrics if m.get('type') != '基础统计指标']
  581. for metric in other_metrics[:3-len(default_metric_ids)]:
  582. metric_name = metric['name']
  583. knowledge_id = f"metric-{metric_name}"
  584. default_metric_ids.append(knowledge_id)
  585. default_global_metrics.append(MetricRequirement(
  586. metric_id=knowledge_id,
  587. metric_name=metric_name,
  588. calculation_logic=f"使用规则引擎计算{metric_name}: {metric.get('description', '')}",
  589. required_fields=["transactions"],
  590. dependencies=[]
  591. ))
  592. # 创建使用实际指标的默认大纲
  593. default_outline = ReportOutline(
  594. report_title="默认交易分析报告",
  595. sections=[
  596. ReportSection(
  597. section_id="sec_1",
  598. title="交易概览",
  599. description="基础交易情况分析",
  600. metrics_needed=default_metric_ids
  601. )
  602. ],
  603. global_metrics=default_global_metrics
  604. )
  605. return default_outline