feat: 四层架构全面增强
安全与稳定性: - 移除硬编码 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 配置模板
This commit is contained in:
@@ -12,11 +12,12 @@ import matplotlib.pyplot as plt
|
||||
import matplotlib.font_manager as fm
|
||||
|
||||
from core.config import LLM_CONFIG
|
||||
from core.utils import get_llm_client, extract_json_array
|
||||
from core.utils import get_llm_client, llm_chat, extract_json_array
|
||||
from layers.explorer import ExplorationStep
|
||||
|
||||
|
||||
def _setup_chinese_font():
|
||||
"""尝试加载中文字体,找不到时用英文显示(不崩溃)"""
|
||||
candidates = [
|
||||
"SimHei", "Microsoft YaHei", "STHeiti", "WenQuanYi Micro Hei",
|
||||
"Noto Sans CJK SC", "PingFang SC", "Source Han Sans CN",
|
||||
@@ -27,10 +28,27 @@ def _setup_chinese_font():
|
||||
plt.rcParams["font.sans-serif"] = [font]
|
||||
plt.rcParams["axes.unicode_minus"] = False
|
||||
return font
|
||||
# 兜底:尝试找任何 CJK 字体
|
||||
for f in fm.fontManager.ttflist:
|
||||
if any(kw in f.name.lower() for kw in ("cjk", "chinese", "hei", "song", "ming", "fang")):
|
||||
plt.rcParams["font.sans-serif"] = [f.name]
|
||||
plt.rcParams["axes.unicode_minus"] = False
|
||||
return f.name
|
||||
plt.rcParams["axes.unicode_minus"] = False
|
||||
return None
|
||||
return None # 后续图表标题会用英文 fallback
|
||||
|
||||
_setup_chinese_font()
|
||||
|
||||
_CN_FONT = _setup_chinese_font()
|
||||
|
||||
|
||||
def _safe_title(title: str) -> str:
|
||||
"""无中文字体时将标题转为安全显示文本"""
|
||||
if _CN_FONT:
|
||||
return title
|
||||
# 简单映射:中文→拼音首字母摘要,保留英文和数字
|
||||
import re
|
||||
clean = re.sub(r'[^\w\s.,;:!?%/()\-+]', '', title)
|
||||
return clean if clean.strip() else "Chart"
|
||||
|
||||
|
||||
CHART_PLAN_PROMPT = """你是一个数据可视化专家。根据以下分析结果,规划需要生成的图表。
|
||||
@@ -97,15 +115,15 @@ class ChartGenerator:
|
||||
)
|
||||
|
||||
try:
|
||||
response = self.client.chat.completions.create(
|
||||
model=self.model,
|
||||
content = llm_chat(
|
||||
self.client, self.model,
|
||||
messages=[
|
||||
{"role": "system", "content": "你是数据可视化专家。只输出纯 JSON 数组,不要 markdown 代码块。"},
|
||||
{"role": "user", "content": CHART_PLAN_PROMPT.format(exploration_summary="\n\n".join(summary_parts))},
|
||||
],
|
||||
temperature=0.1, max_tokens=1024,
|
||||
)
|
||||
plans = extract_json_array(response.choices[0].message.content.strip())
|
||||
plans = extract_json_array(content)
|
||||
return plans if plans else self._fallback_plan(valid_steps)
|
||||
except Exception as e:
|
||||
print(f" ⚠️ 图表规划失败: {e},使用 fallback")
|
||||
@@ -206,11 +224,11 @@ class ChartGenerator:
|
||||
ax.set_xticklabels(x_vals, rotation=45, ha="right", fontsize=9)
|
||||
ax.legend()
|
||||
|
||||
ax.set_title(title, fontsize=13, fontweight="bold", pad=12)
|
||||
ax.set_title(_safe_title(title), fontsize=13, fontweight="bold", pad=12)
|
||||
if chart_type not in ("pie",):
|
||||
ax.set_xlabel(x_col, fontsize=10)
|
||||
ax.set_xlabel(_safe_title(x_col), fontsize=10)
|
||||
if chart_type != "horizontal_bar":
|
||||
ax.set_ylabel(y_col, fontsize=10)
|
||||
ax.set_ylabel(_safe_title(y_col), fontsize=10)
|
||||
ax.grid(axis="y", alpha=0.3)
|
||||
|
||||
plt.tight_layout()
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import json
|
||||
|
||||
from core.config import LLM_CONFIG
|
||||
from core.utils import get_llm_client
|
||||
from core.utils import get_llm_client, llm_chat
|
||||
from layers.context import AnalysisSession
|
||||
|
||||
|
||||
@@ -47,15 +47,14 @@ class ReportConsolidator:
|
||||
charts_text = "\n".join(f"{i}. {c['title']}: {c['path']}" for i, c in enumerate(charts or [], 1)) or "无图表。"
|
||||
|
||||
try:
|
||||
response = self.client.chat.completions.create(
|
||||
model=self.model,
|
||||
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,
|
||||
)
|
||||
return response.choices[0].message.content
|
||||
except Exception as e:
|
||||
print(f" ⚠️ LLM 整合失败: {e},使用拼接模式")
|
||||
return self._fallback_concat(sessions, charts)
|
||||
|
||||
@@ -5,7 +5,7 @@ import json
|
||||
from typing import Any
|
||||
|
||||
from core.config import LLM_CONFIG
|
||||
from core.utils import get_llm_client
|
||||
from core.utils import get_llm_client, llm_chat
|
||||
from layers.explorer import ExplorationStep
|
||||
from layers.insights import Insight
|
||||
|
||||
@@ -58,15 +58,14 @@ class ReportGenerator:
|
||||
charts_text=charts_text,
|
||||
)
|
||||
|
||||
response = self.client.chat.completions.create(
|
||||
model=self.model,
|
||||
return llm_chat(
|
||||
self.client, self.model,
|
||||
messages=[
|
||||
{"role": "system", "content": "你是专业的数据分析师,撰写清晰、有洞察力的分析报告。"},
|
||||
{"role": "user", "content": prompt},
|
||||
],
|
||||
temperature=0.3, max_tokens=4096,
|
||||
)
|
||||
return response.choices[0].message.content
|
||||
|
||||
def _build_exploration(self, steps: list[ExplorationStep]) -> str:
|
||||
parts = []
|
||||
|
||||
Reference in New Issue
Block a user