安全与稳定性: - 移除硬编码 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
2.7 KiB
Python
84 lines
2.7 KiB
Python
"""
|
|
报告生成器 —— 单次分析报告
|
|
"""
|
|
import json
|
|
from typing import Any
|
|
|
|
from core.config import LLM_CONFIG
|
|
from core.utils import get_llm_client, llm_chat
|
|
from layers.explorer import ExplorationStep
|
|
from layers.insights import Insight
|
|
|
|
|
|
REPORT_PROMPT = """你是一个数据分析报告撰写专家。基于以下信息撰写报告。
|
|
|
|
## 用户问题
|
|
{question}
|
|
|
|
## 分析计划
|
|
{plan}
|
|
|
|
## 探索过程
|
|
{exploration}
|
|
|
|
## 主动洞察
|
|
{insights_text}
|
|
|
|
## 可用图表
|
|
{charts_text}
|
|
|
|
## 撰写要求
|
|
1. **开头**:一句话总结核心结论
|
|
2. **核心发现**:按重要性排列,带具体数字
|
|
3. **图表引用**:用 `` 嵌入到相关段落
|
|
4. **深入洞察**:异常、趋势、关联
|
|
5. **建议**:基于数据的行动建议
|
|
6. **审计**:末尾附上所有 SQL
|
|
|
|
中文,专业简报风格。图表自然嵌入对应段落。"""
|
|
|
|
|
|
class ReportGenerator:
|
|
"""报告生成器"""
|
|
|
|
def __init__(self):
|
|
self.client, self.model = get_llm_client(LLM_CONFIG)
|
|
|
|
def generate(self, question: str, plan: dict, steps: list[ExplorationStep],
|
|
insights: list[Insight], charts: list[dict] | None = None) -> str:
|
|
exploration = self._build_exploration(steps)
|
|
insights_text = "\n".join(str(i) for i in insights) if insights else "未检测到异常。"
|
|
charts_text = "\n".join(f"{i}. 标题: {c['title']}, 路径: {c['path']}" for i, c in enumerate(charts or [], 1)) or "无图表。"
|
|
|
|
prompt = REPORT_PROMPT.format(
|
|
question=question,
|
|
plan=json.dumps(plan, ensure_ascii=False, indent=2),
|
|
exploration=exploration,
|
|
insights_text=insights_text,
|
|
charts_text=charts_text,
|
|
)
|
|
|
|
return llm_chat(
|
|
self.client, self.model,
|
|
messages=[
|
|
{"role": "system", "content": "你是专业的数据分析师,撰写清晰、有洞察力的分析报告。"},
|
|
{"role": "user", "content": prompt},
|
|
],
|
|
temperature=0.3, max_tokens=4096,
|
|
)
|
|
|
|
def _build_exploration(self, steps: list[ExplorationStep]) -> str:
|
|
parts = []
|
|
for step in steps:
|
|
if step.action == "done":
|
|
parts.append(f"### 结束\n{step.reasoning}")
|
|
elif step.success:
|
|
parts.append(
|
|
f"### 第 {step.round_num} 轮:{step.purpose}\n"
|
|
f"SQL: `{step.sql}`\n结果 ({step.row_count} 行):\n"
|
|
f"数据: {json.dumps(step.rows, ensure_ascii=False)}"
|
|
)
|
|
else:
|
|
parts.append(f"### 第 {step.round_num} 轮:{step.purpose}\nSQL: `{step.sql}`\n失败: {step.error}")
|
|
return "\n\n".join(parts) if parts else "无探索步骤"
|