安全与稳定性: - 移除硬编码 API Key,改用 .env + 环境变量 - LLM 调用统一重试机制(指数退避,3 次重试,处理 429/5xx/超时) - 中文字体检测增强(CJK 关键词兜底 + 无字体时英文 fallback) - 缺失 API Key 给出友好提示而非崩溃 分析能力提升: - 异常检测新增 z-score 检测(标准差>2 标记异常) - 新增变异系数 CV 检测(数据波动性) - 新增零值/缺失检测 - 上下文管理器升级为关键词语义匹配(替代简单取最近 2 条) 用户体验: - 报告自动保存为 Markdown(reports/ 目录) - 新增 export 命令导出查询结果为 CSV - 新增 reports 命令查看已保存报告 - CLI 支持 readline 命令历史(方向键翻阅) - CSV 导入工具重写:自动列名映射、容错处理、dry-run 模式 - 新增 .env.example 配置模板
84 lines
3.3 KiB
Python
84 lines
3.3 KiB
Python
"""
|
||
报告整合器 —— 将多次分析结果合并为一份完整报告
|
||
"""
|
||
import json
|
||
|
||
from core.config import LLM_CONFIG
|
||
from core.utils import get_llm_client, llm_chat
|
||
from layers.context import AnalysisSession
|
||
|
||
|
||
CONSOLIDATE_PROMPT = """你是一个高级数据分析总监。下面是你的团队针对同一份数据做的多次分析,请整合为一份完整的综合报告。
|
||
|
||
## 核心问题
|
||
{question}
|
||
|
||
## 各次分析结果
|
||
{sections}
|
||
|
||
## 可用图表
|
||
{charts_text}
|
||
|
||
## 整合要求
|
||
1. **执行摘要**:3-5 句话概括全局结论
|
||
2. **核心发现**:从所有分析中提炼最重要的发现,去重,按重要性排列
|
||
3. **交叉洞察**:不同维度之间的关联
|
||
4. **图表引用**:用 `` 嵌入相关段落
|
||
5. **风险与建议**:按优先级排列
|
||
6. **数据附录**:关键统计数字
|
||
|
||
中文,专业简报风格。先结论后细节。"""
|
||
|
||
|
||
class ReportConsolidator:
|
||
"""报告整合器"""
|
||
|
||
def __init__(self):
|
||
self.client, self.model = get_llm_client(LLM_CONFIG)
|
||
|
||
def consolidate(self, sessions: list[AnalysisSession], question: str = "",
|
||
charts: list[dict] | None = None) -> str:
|
||
if not sessions:
|
||
return "(无分析数据可整合)"
|
||
if not question:
|
||
question = sessions[0].question
|
||
|
||
sections = self._build_sections(sessions)
|
||
charts_text = "\n".join(f"{i}. {c['title']}: {c['path']}" for i, c in enumerate(charts or [], 1)) or "无图表。"
|
||
|
||
try:
|
||
return llm_chat(
|
||
self.client, self.model,
|
||
messages=[
|
||
{"role": "system", "content": "你是高级数据分析总监,整合多维度分析结果。"},
|
||
{"role": "user", "content": CONSOLIDATE_PROMPT.format(question=question, sections=sections, charts_text=charts_text)},
|
||
],
|
||
temperature=0.3, max_tokens=4096,
|
||
)
|
||
except Exception as e:
|
||
print(f" ⚠️ LLM 整合失败: {e},使用拼接模式")
|
||
return self._fallback_concat(sessions, charts)
|
||
|
||
def _build_sections(self, sessions: list[AnalysisSession]) -> str:
|
||
parts = []
|
||
for i, session in enumerate(sessions, 1):
|
||
section = f"### 分析 {i}: {session.question}\n"
|
||
section += f"类型: {session.plan.get('analysis_type', '未知')}\n\n"
|
||
for step in session.steps:
|
||
if not step.success or not step.rows or step.action == "done":
|
||
continue
|
||
section += f"- {step.purpose} ({step.row_count} 行)\n"
|
||
section += f" 数据: {json.dumps(step.rows[:8], ensure_ascii=False)}\n\n"
|
||
if session.insights:
|
||
section += "#### 洞察\n" + "\n".join(f"- {i}" for i in session.insights) + "\n"
|
||
parts.append(section)
|
||
return "\n---\n".join(parts)
|
||
|
||
def _fallback_concat(self, sessions: list[AnalysisSession], charts: list[dict] | None) -> str:
|
||
parts = ["# 综合分析报告\n"]
|
||
for i, s in enumerate(sessions, 1):
|
||
parts.append(f"## 第 {i} 部分: {s.question}\n{s.report}\n")
|
||
if charts:
|
||
parts.append("\n## 可视化\n" + "\n".join(f"![{c['title']}]({c['path']})" for c in charts))
|
||
return "\n".join(parts)
|