安全与稳定性: - 移除硬编码 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 配置模板
74 lines
2.1 KiB
Python
74 lines
2.1 KiB
Python
"""
|
||
Layer 1: 意图规划器
|
||
"""
|
||
import json
|
||
from typing import Any
|
||
|
||
from core.config import LLM_CONFIG
|
||
from core.utils import get_llm_client, llm_chat, extract_json_object
|
||
|
||
PROMPT = """你是一个数据分析规划专家。
|
||
|
||
## 你的任务
|
||
根据用户的分析问题和数据库 Schema,生成一个结构化的分析计划。
|
||
|
||
## 输出格式(严格 JSON)
|
||
```json
|
||
{
|
||
"intent": "一句话描述用户想了解什么",
|
||
"analysis_type": "ranking" | "distribution" | "trend" | "comparison" | "anomaly" | "overview",
|
||
"primary_table": "主要分析的表名",
|
||
"dimensions": ["分组维度列名"],
|
||
"metrics": ["需要聚合的数值列名"],
|
||
"aggregations": ["SUM", "AVG", "COUNT", ...],
|
||
"filters": [{"column": "列名", "condition": "过滤条件(可选)"}],
|
||
"join_needed": false,
|
||
"join_info": {"tables": [], "on": ""},
|
||
"expected_rounds": 3,
|
||
"rationale": "为什么这样规划,需要关注什么"
|
||
}
|
||
```
|
||
|
||
## 分析类型说明
|
||
- ranking: 按某维度排名
|
||
- distribution: 分布/占比
|
||
- trend: 时间趋势
|
||
- comparison: 对比分析
|
||
- anomaly: 异常检测
|
||
- overview: 全局概览
|
||
|
||
## 规划原则
|
||
1. 只选择与问题相关的表和列
|
||
2. 如果需要 JOIN,说明关联条件
|
||
3. 预估需要几轮探索(1-6)
|
||
4. 标注可能的异常关注点
|
||
5. metrics 不要包含 id 列"""
|
||
|
||
|
||
class Planner:
|
||
"""意图规划器"""
|
||
|
||
def __init__(self):
|
||
self.client, self.model = get_llm_client(LLM_CONFIG)
|
||
|
||
def plan(self, question: str, schema_text: str) -> dict[str, Any]:
|
||
content = llm_chat(
|
||
self.client, self.model,
|
||
messages=[
|
||
{"role": "system", "content": PROMPT},
|
||
{"role": "user", "content": f"## Schema\n{schema_text}\n\n## 用户问题\n{question}"},
|
||
],
|
||
temperature=0.1,
|
||
max_tokens=1024,
|
||
)
|
||
plan = extract_json_object(content)
|
||
|
||
if not plan:
|
||
plan = {"intent": content[:100], "analysis_type": "overview"}
|
||
|
||
plan.setdefault("analysis_type", "overview")
|
||
plan.setdefault("expected_rounds", 3)
|
||
plan.setdefault("filters", [])
|
||
plan.setdefault("join_needed", False)
|
||
return plan
|