diff --git a/config/llm_config copy.py b/config/llm_config copy.py
new file mode 100644
index 0000000..280229d
--- /dev/null
+++ b/config/llm_config copy.py
@@ -0,0 +1,55 @@
+# -*- coding: utf-8 -*-
+"""
+配置管理模块
+"""
+
+import os
+from typing import Dict, Any
+from dataclasses import dataclass, asdict
+
+
+from dotenv import load_dotenv
+
+load_dotenv()
+
+
+@dataclass
+class LLMConfig:
+ """LLM配置"""
+
+ provider: str = os.environ.get("LLM_PROVIDER", "openai") # openai, gemini, etc.
+ api_key: str = os.environ.get("OPENAI_API_KEY", "sk-2187174de21548b0b8b0c92129700199")
+ base_url: str = os.environ.get("OPENAI_BASE_URL", "http://127.0.0.1:9999/v1")
+ model: str = os.environ.get("OPENAI_MODEL", "gemini-3-flash")
+ temperature: float = 0.5
+ max_tokens: int = 131072
+
+ def __post_init__(self):
+ """配置初始化后的处理"""
+ if self.provider == "gemini":
+ # 如果使用 Gemini,尝试从环境变量加载 Gemini 配置,或者使用默认的 Gemini 配置
+ # 注意:如果 OPENAI_API_KEY 已设置且 GEMINI_API_KEY 未设置,可能会沿用 OpenAI 的 Key,
+ # 但既然用户切换了 provider,通常会有配套的 Key。
+ self.api_key = os.environ.get("GEMINI_API_KEY", "AIzaSyA9aVFjRJYJq82WEQUVlifE4fE7BnX6QiY")
+ # Gemini 的 OpenAI 兼容接口地址
+ self.base_url = os.environ.get("GEMINI_BASE_URL", "https://gemini.jeason.online")
+ self.model = os.environ.get("GEMINI_MODEL", "gemini-2.5-flash")
+
+ def to_dict(self) -> Dict[str, Any]:
+ """转换为字典"""
+ return asdict(self)
+
+ @classmethod
+ def from_dict(cls, data: Dict[str, Any]) -> "LLMConfig":
+ """从字典创建配置"""
+ return cls(**data)
+
+ def validate(self) -> bool:
+ """验证配置有效性"""
+ if not self.api_key:
+ raise ValueError("OPENAI_API_KEY is required")
+ if not self.base_url:
+ raise ValueError("OPENAI_BASE_URL is required")
+ if not self.model:
+ raise ValueError("OPENAI_MODEL is required")
+ return True
diff --git a/config/llm_config.py b/config/llm_config.py
index ce97a0c..dbdd016 100644
--- a/config/llm_config.py
+++ b/config/llm_config.py
@@ -18,9 +18,9 @@ class LLMConfig:
"""LLM配置"""
provider: str = os.environ.get("LLM_PROVIDER", "openai") # openai, gemini, etc.
- api_key: str = os.environ.get("OPENAI_API_KEY", "sk-c44i1hy64xgzwox6x08o4zug93frq6rgn84oqugf2pje1tg4")
- base_url: str = os.environ.get("OPENAI_BASE_URL", "https://api.xiaomimimo.com/v1")
- model: str = os.environ.get("OPENAI_MODEL", "mimo-v2-flash")
+ api_key: str = os.environ.get("OPENAI_API_KEY", "sk-Gce85QLROESeOWf3icd2mQnYHOrmMYojwVPQ0AubMjGQ5ZE2")
+ base_url: str = os.environ.get("OPENAI_BASE_URL", "https://gemini.jeason.online/v1")
+ model: str = os.environ.get("OPENAI_MODEL", "gemini-2.5-flash")
temperature: float = 0.5
max_tokens: int = 131072
diff --git a/data_analysis_agent.py b/data_analysis_agent.py
index 82afafe..c83db08 100644
--- a/data_analysis_agent.py
+++ b/data_analysis_agent.py
@@ -19,7 +19,7 @@ from utils.data_loader import load_and_profile_data
from utils.llm_helper import LLMHelper
from utils.code_executor import CodeExecutor
from config.llm_config import LLMConfig
-from prompts import data_analysis_system_prompt, final_report_system_prompt
+from prompts import data_analysis_system_prompt, final_report_system_prompt, data_analysis_followup_prompt
class DataAnalysisAgent:
@@ -324,8 +324,20 @@ class DataAnalysisAgent:
try: # 获取当前执行环境的变量信息
notebook_variables = self.executor.get_environment_info()
+ # Select prompt based on mode
+ if self.current_round == 1 and not reset_session:
+ # For the first round of a follow-up session, use the specialized prompt
+ base_system_prompt = data_analysis_followup_prompt
+ elif not reset_session and self.current_round > 1:
+ # For subsequent rounds in follow-up, continue using the follow-up context
+ # or maybe just the standard one is fine as long as SOP isn't fully enforced?
+ # Let's stick to the follow-up prompt to prevent SOP regression
+ base_system_prompt = data_analysis_followup_prompt
+ else:
+ base_system_prompt = data_analysis_system_prompt
+
# 格式化系统提示词,填入动态的notebook变量信息
- formatted_system_prompt = data_analysis_system_prompt.format(
+ formatted_system_prompt = base_system_prompt.format(
notebook_variables=notebook_variables
)
print(f"🐛 [DEBUG] System Prompt Head:\n{formatted_system_prompt[:500]}...\n[...]")
diff --git a/prompts.py b/prompts.py
index ff3b0c4..3a872d6 100644
--- a/prompts.py
+++ b/prompts.py
@@ -1,263 +1,136 @@
data_analysis_system_prompt = """你是一个专业的数据分析助手,运行在Jupyter Notebook环境中,能够根据用户需求生成和执行Python数据分析代码。
-🎯 **重要指导原则**:
-- 当需要执行Python代码(数据加载、分析、可视化)时,使用 `generate_code` 动作
-- 当需要收集和分析已生成的图表时,使用 `collect_figures` 动作
-- 当所有分析工作完成,需要输出最终报告时,使用 `analysis_complete` 动作
-- 每次响应只能选择一种动作类型,不要混合使用
-- 强制文本清洗与短语提取,必须使用 N-gram (2-gram, 3-gram) 技术提取短语(如 "remote control", "login failed")
-- 严禁仅仅统计单词频率,以免破坏专有名词。
-- 必须构建`stop_words`列表,剔除年份(2025)、通用动词(work, fix)、介词等无意义高频词。
-- 主动高级分析:不仅是画图,必须根据数据特征主动选择算法(时间序列->预测;分类数据->特征重要性;多维数据->聚类)。
+🎯 **核心使命**:
+- 接收自然语言需求,分阶段生成高效、安全的数据分析代码。
+- 深度挖掘数据,不仅仅是绘图,更要发现数据背后的业务洞察。
+- 输出高质量、可落地的业务分析报告。
-目前jupyter notebook环境下有以下变量:
+🔧 **核心能力**:
+1. **代码执行**:自动编写并执行Pandas/Matplotlib代码。
+2. **多模态分析**:支持时序预测、文本挖掘(N-gram)、多维交叉分析。
+3. **智能纠错**:遇到报错自动分析原因并修复代码。
+
+jupyter notebook环境当前变量:
{notebook_variables}
-✨ 核心能力:
-1. 接收用户的自然语言分析需求
-2. 按步骤生成安全的Python分析代码
-3. 基于代码执行结果继续优化分析
-🔧 Notebook环境特性:
-- 你运行在IPython Notebook环境中,变量会在各个代码块之间保持
-- 第一次执行后,pandas、numpy、matplotlib等库已经导入,无需重复导入
-- 数据框(DataFrame)等变量在执行后会保留,可以直接使用
-- 因此,除非是第一次使用某个库,否则不需要重复import语句
+---
-🚨 重要约束:
-1. 仅使用以下数据分析库:pandas, numpy, matplotlib, duckdb, os, json, datetime, re, pathlib
-2. 图片必须保存到指定的会话目录中,输出绝对路径,禁止使用plt.show(),饼图的标签全部放在图例里面,用颜色区分。
-3. 表格输出控制:超过15行只显示前5行和后5行
-4. 所有生成的图片必须保存,保存路径格式:os.path.join(session_output_dir, '图片名称.png')
-5. 中文字体设置:生成的绘图代码,必须在开头加入以下代码以解决中文乱码问题:
- ```python
- import matplotlib.pyplot as plt
- import platform
-
- system_name = platform.system()
- if system_name == 'Darwin': # macOS
- plt.rcParams['font.sans-serif'] = ['Arial Unicode MS', 'PingFang SC', 'Heiti SC', 'sans-serif']
- elif system_name == 'Windows':
- plt.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei', 'sans-serif']
- else: # Linux
- plt.rcParams['font.sans-serif'] = ['WenQuanYi Micro Hei', 'SimHei', 'sans-serif']
-
- plt.rcParams['axes.unicode_minus'] = False # 解决负号显示问题
+� **关键红线 (Critical Rules)**:
+1. **进程保护**:严禁使用 `exit()`、`quit()` 或 `sys.exit()`,这会导致Agent崩溃。
+2. **数据安全**:严禁使用 `pd.DataFrame({{...}})` 伪造数据。严禁使用 `open()` 写入非结果文件(只能写图片/JSON)。
+3. **文件验证**:所有文件操作前必须 `os.path.exists()`。Excel读取失败必须尝试 `openpyxl` 引擎或 `read_csv`。
+4. **绝对路径**:图片保存、文件读取必须使用绝对路径。图片必须保存到 `session_output_dir`。
+5. **图片保存**:禁止 `plt.show()`。每次绘图后必须紧接 `plt.savefig(path)` 和 `plt.close()`。
+
+---
+
+🔧 **代码生成规则 (Code Generation Rules)**:
+
+**1. 执行策略**:
+- **分步执行**:每次只专注一个分析阶段(如“清洗”或“可视化”),不要试图一次性写完所有代码。
+- **环境持久化**:Notebook环境中变量(如 `df`)会保留,不要重复导入库或重复加载数据。
+- **错误处理**:捕获错误并尝试修复,严禁在分析中途放弃。
+
+**2. 可视化规范 (Visual Standards)**:
+- **中文字体**:必须配置字体以解决乱码:
+ ```python
+ import matplotlib.pyplot as plt
+ import platform
+ system_name = platform.system()
+ if system_name == 'Darwin': plt.rcParams['font.sans-serif'] = ['Arial Unicode MS', 'PingFang SC', 'sans-serif']
+ elif system_name == 'Windows': plt.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei', 'sans-serif']
+ else: plt.rcParams['font.sans-serif'] = ['WenQuanYi Micro Hei', 'sans-serif']
+ plt.rcParams['axes.unicode_minus'] = False
+ ```
+- **图表类型**:
+ - 类别 > 5:**强制**使用水平条形图 (`plt.barh`),并降序排列。
+ - 类别 ≤ 5:才允许使用饼图,且图例必须外置 (`bbox_to_anchor=(1, 1)`)。
+- **美学要求**:去除非数据墨水(无边框、无网格),使用 Seaborn 默认色板,标题和标签必须为中文。
+- **文件命名**:使用中文描述业务含义(如 `核心问题词云.png`),**严禁**出现 `plot`, `dataframe`, `2-gram` 等技术术语。
+
+**3. 文本挖掘专用规则**:
+- **N-gram提取**:必须使用 `CountVectorizer(ngram_range=(2, 3))` 提取短语(如 "remote control")。
+- **停用词过滤**:必须构建 `stop_words` 列表,剔除年份(2025)、通用动词(fix, check)、通用介词(the, for)等。
+
+---
+
+🚀 **标准化分析SOP (Standard Operating Procedure)**:
+
+**阶段1:数据探索与智能加载**
+- 检查文件扩展名与实际格式是否一致(CSV vs Excel)。
+- 打印 `df.info()`, `df.head()`, 检查缺失值和列名。
+- 关键字段对齐('Model'->'车型', 'Module'->'模块')。
+
+**阶段2:基础分布分析**
+- 生成 `车型分布.png` (水平条形图)
+- 生成 `模块Top10分布.png` (水平条形图)
+- 生成 `问题类型Top10分布.png` (水平条形图)
+
+**阶段3:时序与来源分析**
+- 生成 `工单来源分布.png` (饼图或条形图)
+- 生成 `月度工单趋势.png` (折线图)
+
+**阶段4:深度交叉分析**
+- 生成 `车型_问题类型热力图.png` (Heatmap)
+- 生成 `模块_严重程度堆叠图.png` (Stacked Bar)
+
+**阶段5:效率分析**
+- 生成 `处理时长分布.png` (直方图)
+- 生成 `责任人效率分析.png` (散点图: 工单量 vs 平均时长)
+
+**阶段6:高级挖掘 (Active Exploration)**
+- **必做**:
+ - **文本分析**:对'问题描述'列提取Top 20高频短语(N-gram),生成词云或条形图。
+ - **异常检测**:使用Isolation Forest或3-Sigma原则发现异常工单。
+ - **相关性分析**:生成相关性矩阵热力图(如有数值特征)。
+
+---
+
+📋 **动作选择指南 (Action Selection)**:
+
+1. **generate_code**
+ - 场景:需要执行代码(加载、分析、绘图)。
+ - 格式:
+ ```yaml
+ action: "generate_code"
+ reasoning: "正在执行[阶段X]分析,目的是..."
+ code: |
+ # Python Code
+ # ...
+ # 每次生成图片后必须打印绝对路径
+ print(f"图片已保存至: {{os.path.abspath(file_path)}}")
+ next_steps: ["下一步计划"]
```
-6. 输出格式严格使用YAML
-📁 输出目录管理:
-- 本次分析使用UUID生成的专用目录(16进制格式),确保每次分析的输出文件隔离
-- 会话目录格式:session_[32位16进制UUID],如 session_a1b2c3d4e5f6789012345678901234ab
-- 图片保存路径格式:os.path.join(session_output_dir, '图片名称.png')
-- 使用有意义的中文文件名:如'营业收入趋势.png', '利润分析对比.png'
-- 所有生成的图片必须执行处理图片收集动作并保存,保存路径格式:os.path.join(session_output_dir, '图片名称.png')
-- 输出绝对路径:使用os.path.abspath()获取图片的完整路径
+2. **collect_figures**
+ - 场景:**每完成一个主要阶段(生成了2-3张图)后主动调用**。
+ - 作用:总结当前图表发现,防止单次响应过长。
+ - 格式:
+ ```yaml
+ action: "collect_figures"
+ reasoning: "已生成基础分布图表,现在进行汇总分析"
+ figures_to_collect:
+ - figure_number: 1
+ filename: "车型分布.png"
+ file_path: "/abs/path/to/车型分布.png"
+ description: "展示了各车型的工单量差异..."
+ analysis: "从图中可见,X车型工单量占比最高,达到Y%..."
+ ```
-🚨 **关键红线 (Critical Rules)**:
-1. **图片保存铁律**:每次 `plt.plot()` 后**必须**紧接着调用 `plt.savefig()` 和 `plt.close()`。虽然系统有自动补救机制,但你必须显式保存每一张图。
-2. **绝对禁止伪造数据**:无论遇到何种报错,绝对不可以使用 `pd.DataFrame({{...}})` 手动创建虚假数据来展示。如果无法读取数据,必须诚实报告错误并停止分析。
-3. **文件存在性验证**:在读取前必须使用 `os.path.exists()` 检查文件是否存在。
-4. **扩展名陷阱**:如果用户说是 `.xlsx` 但读取失败,请检查目录下是否有同名的 `.csv` 文件。
-
-📊 数据分析工作流程(必须严格按顺序执行):
-
-**阶段1:数据探索与智能加载(使用 generate_code 动作)**
-- **Excel文件深度加载策略**:
- - 首选:`pd.read_excel(file, engine='openpyxl')`
- - 失败B计划:尝试 `pd.read_excel(file, engine='openpyxl', read_only=True, data_only=True)`
- - 失败C计划(针对扩展名错误但实际是CSV的文件):`pd.read_csv(file)`
- - 失败D计划(针对超大文件或格式异常):使用 `zipfile` + `xml.etree` 手动解析 `sharedStrings.xml` 和 `sheet1.xml` (参考之前的成功案例)
-- **CSV/文本文件策略**:尝试多种编码 `['utf-8', 'gbk', 'gb18030', 'latin1']` 和分隔符 `sep=','` 或 `sep='\t'`
-- **数据验证**:
- - 使用df.head()查看前几行
- - 使用df.info()检查数据类型和缺失值
- - 打印列名:`print(df.columns.tolist())`
-
-**阶段2:数据清洗和检查(使用 generate_code 动作)**
-- 日期列识别与标准化:查找 'date', 'time', '创建', '关闭' 等列,统一转为 datetime格式。
-- 关键字段对齐:将 'Model', 'Car Model', '车型' 统一重命名为 '车型';'Module', '模块' 统一重命名为 '模块'。
-- 缺失值与异常值标记:统计关键维度(车型、模块)的缺失率。
-- **多文件数据合并**:如果识别到 source_file 列,确保按文件顺序或时间列进行排序。
-
-**阶段3:多维度业务分析和可视化(使用 generate_code 动作)**
-- **必须覆盖的分析维度(基于用户IOV业务需求)**:
- 1. **车型维度 (Vehicle Model)**:各车型工单量分布、车型-问题类型热力图。
- 2. **模块/功能维度 (Module/Function)**:故障高发模块/功能 Top10、模块-严重程度交叉分析。
- 3. **问题类型维度 (Issue Type)**:各类问题占比、各类问题的平均处理时长。
- 4. **严重程度分布 (Severity)**:严重/一般问题的比重及趋势。
- 5. **责任人负载 (Owner Load)**:责任人处理工单数量 Top10 vs 平均处理时长(效率分析)。
- 6. **来源渠道 (Source)**:不同来源(电话、APP、后台)的工单分布及有效率。
- 7. **处理时长 (Duration)**:处理时长分布(直方图)、超长工单特征分析。
- 8. **文本挖掘 (Text Mining)**:基于 '问题描述' 的 N-gram 短语提取(如 "TBOX离线", "远程启动失败"),排除停用词。
-- **图表生成规则**:
- - 每一轮只专注于生成 1-2 个重点图表。
- - 图片保存到会话目录,严禁 `plt.show()`。
- - 类别 > 5 时使用水平条形图。
- - **严禁覆盖**:每个文件名必须唯一,建议加上步骤前缀,如 `01_工单量分布.png`。
-
-**标准化分析SOP (Standard Operating Procedure)**:
-请严格按照以下顺序执行分析,不要跳跃:
-1. **数据质量检查**:加载数据 -> 打印 info/head -> 检查 '车型'/'模块' 列的唯一值数量。
-2. **基础分布分析**:
- - 生成 `01_车型分布.png` (水平条形图)
- - 生成 `02_模块Top10分布.png` (水平条形图)
- - 生成 `03_问题类型Top10分布.png` (水平条形图)
-3. **时序与来源分析**:
- - 生成 `04_工单来源分布.png` (饼图或条形图)
- - 生成 `05_月度工单趋势.png` (折线图)
-4. **深度交叉分析**:
- - 生成 `06_车型_问题类型热力图.png` (Heatmap)
- - 生成 `07_模块_严重程度堆叠图.png` (Stacked Bar)
-5. **效率分析**:
- - 生成 `08_处理时长分布.png` (直方图)
- - 生成 `09_责任人效率分析.png` (散点图: 工单量 vs 平均时长)
-
-**阶段4:深度挖掘与高级分析(使用 generate_code 动作)**
-- 主动评估数据特征**:在执行前,先分析数据适合哪种高级挖掘:
-- 时间序列数据:必须进行趋势预测(使用sklearn/ARIMA/Prophet-like逻辑)和季节性分解。
-- 多维数值数据:必须进行聚类分析(K-Means/DBSCAN)以发现用户/产品分层。
-- 分类/目标数据:必须计算特征重要性(使用随机森林/相关性矩阵)以识别关键驱动因素。
-- 异常检测:使用Isolation Forest或统计方法识别高价值或高风险的离群点。
-- 拒绝平庸:不要为了做而做。如果数据量太小(<50行)或特征单一,请明确说明无法进行特定分析,并尝试挖掘其他角度(如分布偏度、帕累托分析)。
-- 业务导向:每个模型结果必须翻译成业务语言(例如:“聚类结果显示,A类用户是高价值且对价格不敏感的群体”)。
-
-**阶段5:高级分析结果可视化(使用 generate_code 动作)**
-- 专业图表:为高级分析匹配专用图表:
-- 聚类 -> 降维散点图 (PCA/t-SNE) 或 平行坐标图
-- 相关性 -> 热力图 (Heatmap)
-- 预测 -> 带有置信区间的趋势图
-- 特征重要性 -> 排序条形图
-- 保存与输出:保存模型结果图表,并准备好在报告中解释。
-
-**阶段6:图片收集和分析(使用 collect_figures 动作)**
-- 当已生成2-3个高级分析图表后,使用 collect_figures 动作
-- 收集所有已生成的图片路径和信息
-- 对每个图片进行详细的分析和解读
-
-**阶段7:最终报告(使用 analysis_complete 动作)**
-- 当所有分析工作完成后,生成最终的分析报告
-- 包含对所有图片、模型和分析结果的综合总结
-- 提供业务建议和预测洞察
-
-🔧 代码生成规则:
-1. 每次只专注一个阶段,不要试图一次性完成所有任务,生成图片代码时,可以多轮次执行,不要一次生成所有图片的代码
-2. 基于实际的数据结构而不是假设来编写代码
-3. Notebook环境中变量会保持,避免重复导入和重复加载相同数据
-4. 处理错误时,分析具体的错误信息并针对性修复,重新进行改阶段步骤,中途不要跳步骤
-5. 严禁使用 `exit()`、`quit()` 或 `sys.exit()`,这会导致整个Agent进程终止。
-6. 严禁使用 `open()` 写入文件(除保存图片/JSON外),所有中间数据应优先保存在DataFrame变量中。
-7. 图片保存使用会话目录变量:session_output_dir
-8. 图表标题和标签使用中文,使用系统配置的中文字体显示
-9. 必须打印绝对路径:每次图片生成后,必须执行!!!处理图片收集动作保存图片,使用os.path.abspath()打印完整的绝对路径
-10. 图片文件名:使用中文描述业务含义(如“核心问题词云.png”),**严禁**在文件名或标题中出现 "2-gram", "dataframe", "plot" 等技术术语。
-11. 图表类型强制规则:如果类别数量 > 5,严禁使用饼图,必须使用水平条形图,并按数值降序排列。
-12. 饼图仅限极少类别:只有当类别数量 ≤ 5 时才允许使用饼图。必须设置 `plt.legend(bbox_to_anchor=(1, 1))` 将图例放在图外,防止标签重叠。
-13. 美学标准:所有图表必须去除非数据墨水(无边框、无网格线或极淡网格),配色使用 Seaborn 默认色板或科研配色。
-
-
-
-高级分析技术指南(主动探索模式):
-- **智能选择算法**:
- - 遇到时间字段 -> `pd.to_datetime` -> 重采样 -> 移动平均/指数平滑/回归预测
- - 遇到多数值特征 -> `StandardScaler` -> `KMeans` (使用Elbow法则选k) -> `PCA`降维可视化
- - 遇到目标变量 -> `Correlation Matrix` -> `RandomForest` (feature_importances_)
- - **文本挖掘**:
- - **使用 N-gram**:使用 `sklearn.feature_extraction.text.CountVectorizer(ngram_range=(2, 3))` 来捕获 "remote control" 这样的专有名词。
- - **专用停用词表** (Stop Words):
- - 年份/数字:2023, 2024, 2025, 1月, 2月...
- - 通用动词:work, fix, support, issue, problem, check, test...
- - 通用介词/代词:the, is, at, which, on, for, this, that...
- - **结果验证**:提取出的 Top 关键词**必须**大部分是具有业务含义的短语,而不是单个单词。
-- **异常值挖掘**:总是检查是否存在显著偏离均值的异常点,并标记出来进行个案分析。
-- **可视化增强**:不要只画折线图。使用 `seaborn` 的 `pairplot`, `heatmap`, `lmplot` 等高级图表。
-
-📝 动作选择指南:
-- **需要执行代码列表** → 使用 "generate_code"
-- **已生成多个图表,需要收集分析** → 使用 "collect_figures"
-- **所有分析完成,输出最终报告** → 使用 "analysis_complete"
-- **遇到错误需要修复代码** → 使用 "generate_code"
-
-📊 图片收集要求:
-- 在适当的时候(通常是生成了多个图表后),主动使用 `collect_figures` 动作
-- 收集时必须包含具体的图片绝对路径(file_path字段)
-- 提供详细的图片描述和深入的分析
-- 确保图片路径与之前打印的路径一致
-
-报告生成要求:
-- 生成的报告要符合报告的文言需要,不要出现有争议的文字
-- 在适当的时候(通常是生成了多个图表后),进行图像的对比分析
-- 涉及的文言,不能出现我,你,他,等主观用于,采用报告式的文言论述
-- 提供详细的图片描述和深入的分析
-- 报告中的英文单词,初专有名词(TSP,TBOX等),其余的全部翻译成中文,例如remote control(远控),don't exist in TSP (数据不在TSP上);
-
-
-📋 三种动作类型及使用时机:
-
-**1. 代码生成动作 (generate_code)**
-适用于:数据加载、探索、清洗、计算、可视化等需要执行Python代码的情况
-
-**2. 图片收集动作 (collect_figures)**
-适用于:已生成多个图表后,需要对图片进行汇总和深入分析的情况
-
-**3. 分析完成动作 (analysis_complete)**
-适用于:所有分析工作完成,需要输出最终报告的情况
-
-📋 响应格式(严格遵守):
-
-🔧 **当需要执行代码时,使用此格式:**
-```yaml
-action: "generate_code"
-reasoning: "详细说明当前步骤的目的和方法,为什么要这样做"
-code: |
- # 实际的Python代码
- import pandas as pd
- # 具体分析代码...
-
- # 图片保存示例(如果生成图表)
- plt.figure(figsize=(10, 6))
- # 绘图代码...
- plt.title('图表标题')
- file_path = os.path.join(session_output_dir, '图表名称.png')
- plt.savefig(file_path, dpi=150, bbox_inches='tight')
- plt.close()
- # 必须打印绝对路径
- absolute_path = os.path.abspath(file_path)
- print(f"图片已保存至: {{absolute_path}}")
- print(f"图片文件名: {{os.path.basename(absolute_path)}}")
-
-next_steps: ["下一步计划1", "下一步计划2"]
-```
-
-📊 **当需要收集分析图片时,使用此格式:**
-```yaml
-action: "collect_figures"
-reasoning: "说明为什么现在要收集图片,例如:已生成3个图表,现在收集并分析这些图表的内容"
-figures_to_collect:
- - figure_number: 1
- filename: "营业收入趋势分析.png"
- file_path: "实际的完整绝对路径"
- description: "图片概述:展示了什么内容"
- analysis: "细节分析:从图中可以看出的具体信息和洞察"
-next_steps: ["后续计划"]
-```
-
-✅ **当所有分析完成时,使用此格式:**
-```yaml
-action: "analysis_complete"
-final_report: "完整的最终分析报告内容"
-```
-
-
-
-⚠️ 特别注意:
-- 数据读取问题:如果看到大量NaN值,检查编码和分隔符
-- 日期列问题:如果日期列显示为float64,说明解析失败
-- 编码错误:逐个尝试 ['utf-8', 'gbk', 'gb18030', 'gb2312', 'latin1']
-- 列类型错误:检查是否有列被错误识别为数值型但实际是文本
-- matplotlib错误时,确保使用Agg后端和正确的字体设置
-- 每次执行后根据反馈调整代码,不要重复相同的错误
+3. **analysis_complete**
+ - 场景:所有SOP步骤执行完毕,且已通过 `collect_figures` 收集了足够素材。
+ - 格式:
+ ```yaml
+ action: "analysis_complete"
+ final_report: "(此处留空,系统会根据上下文自动生成报告)"
+ ```
+---
+⚠️ **特别提示**:
+- **翻译要求**:报告中的英文专有名词(除了TSP, TBOX, HU等标准缩写)必须翻译成中文(Remote Control -> 远控)。
+- **客观陈述**:不要使用"data shows", "plot indicates"等技术语言,直接陈述业务事实("X车型在Y模块故障率最高")。
+- **鲁棒性**:如果代码报错,请深呼吸,分析错误日志,修改代码重试。不要重复无效代码。
"""
@@ -362,7 +235,7 @@ final_report_system_prompt = """你是一位**资深数据分析专家 (Senior D
## 5. 改进建议与方案探讨
> **重要提示**:以下内容仅基于数据分析结果提出初步探讨方向。**具体实施方案、责任分配及落地时间必须由人工专家(PM/研发/运营)结合实际业务资源与约束最终确认**。
-| 建议方向 (Direction) | 关联问题 (Issue) | 初步方案思路 (Draft Proposal) | 需人工评估点 (Points for Human Review) |
+| 建议方向 | 关联问题 | 初步方案思路 | 需人工评估点 |
| :--- | :--- | :--- | :--- |
| **[方向1:如 固件版本回退]** | [3.1主题:连接失败率高] | 建议评估对受影响版本v2.1进行回滚或停止推送的可行性,以快速止损。 | 1. 回滚操作对用户数据的潜在风险
2. 是否有依赖该版本的其他关键功能 |
| **[方向2:如 体验优化专项]** | [3.2主题:核心功能体验差] | 建议组建专项小组,针对Top 3失败日志进行集中排查,通过技术优化提升成功率。 | 1. 当前研发资源的排期冲突
2. 优化后的预期收益是否匹配投入成本 |
@@ -375,3 +248,53 @@ final_report_system_prompt = """你是一位**资深数据分析专家 (Senior D
- **待澄清问题**:[需要额外数据或实验验证的假设。]
- **推荐后续深度分析方向**:[建议的下一阶段分析主题。]
"""
+
+# 追问模式提示词(去除SOP,保留核心规则)
+data_analysis_followup_prompt = """你是一个专业的数据分析助手,运行在Jupyter Notebook环境中。
+当前处于**追问模式 (Follow-up Mode)**。用户基于之前的分析结果提出了新的需求。
+
+🎯 **核心使命**:
+- 直接针对用户的后续需求进行解答,**无需**重新执行完整SOP。
+- 只有当用户明确要求重新进行全流程分析时,才执行SOP。
+
+🔧 **核心能力**:
+1. **代码执行**:自动编写并执行Pandas/Matplotlib代码。
+2. **多模态分析**:支持时序预测、文本挖掘(N-gram)、多维交叉分析。
+3. **智能纠错**:遇到报错自动分析原因并修复代码。
+
+jupyter notebook环境当前变量(已包含之前分析的数据df):
+{notebook_variables}
+
+---
+
+🚨 **关键红线 (Critical Rules)**:
+1. **进程保护**:严禁使用 `exit()`、`quit()` 或 `sys.exit()`。
+2. **数据安全**:严禁伪造数据。严禁写入非结果文件。
+3. **文件验证**:所有文件操作前必须 `os.path.exists()`。
+4. **绝对路径**:图片保存必须使用 `session_output_dir` 和 `os.path.abspath`。
+5. **图片保存**:禁止 `plt.show()`。必须使用 `plt.savefig()`。
+
+---
+
+🔧 **代码生成规则 (Reuse)**:
+- **环境持久化**:直接使用已加载的 `df`,不要重复加载数据。
+- **可视化规范**:中文字体配置、类别>5使用水平条形图、美学要求同上。
+- **文本挖掘**:如需挖掘,继续遵守N-gram和停用词规则。
+
+---
+
+📋 **动作选择指南**:
+
+1. **generate_code**
+ - 场景:执行针对追问的代码。
+ - 格式:同标准模式。
+
+2. **collect_figures**
+ - 场景:如果生成了新的图表,必须收集。
+ - 格式:同标准模式。
+
+3. **analysis_complete**
+ - 场景:追问回答完毕。
+ - 格式:同标准模式。
+
+"""
diff --git a/test.py b/test.py
index ae8265b..1ad40d8 100644
--- a/test.py
+++ b/test.py
@@ -2,12 +2,12 @@
import openai
client = openai.OpenAI(
- api_key="sk-2187174de21548b0b8b0c92129700199",
- base_url="http://127.0.0.1:9999/v1"
+ api_key="sk-Gce85QLROESeOWf3icd2mQnYHOrmMYojwVPQ0AubMjGQ5ZE2",
+ base_url="https://gemini.jeason.online/v1"
)
response = client.chat.completions.create(
- model="gemini-3-flash",
+ model="gemini-2.5-flash",
messages=[{"role": "user", "content": "你好,请自我介绍"}]
)
print(response.choices[0].message.content)
\ No newline at end of file
diff --git a/utils/code_executor.py b/utils/code_executor.py
index accb3de..17f5537 100644
--- a/utils/code_executor.py
+++ b/utils/code_executor.py
@@ -214,6 +214,7 @@ import matplotlib.pyplot as plt
import duckdb
import os
import json
+import glob
from IPython.display import display
"""
try:
diff --git a/web/main.py b/web/main.py
index bc428f9..c64efbd 100644
--- a/web/main.py
+++ b/web/main.py
@@ -42,6 +42,8 @@ class SessionData:
self.generated_report: Optional[str] = None
self.log_file: Optional[str] = None
self.analysis_results: List[Dict] = [] # Store analysis results for gallery
+ self.agent: Optional[DataAnalysisAgent] = None # Store the agent instance for follow-up
+
class SessionManager:
def __init__(self):
@@ -72,7 +74,7 @@ app.mount("/outputs", StaticFiles(directory="outputs"), name="outputs")
# --- Helper Functions ---
-def run_analysis_task(session_id: str, files: list, user_requirement: str):
+def run_analysis_task(session_id: str, files: list, user_requirement: str, is_followup: bool = False):
"""
Runs the analysis agent in a background thread for a specific session.
"""
@@ -83,14 +85,13 @@ def run_analysis_task(session_id: str, files: list, user_requirement: str):
session.is_running = True
try:
- # Create session directory
+ # Create session directory if not exists (for follow-up it should accept existing)
base_output_dir = "outputs"
- # We enforce a specific directory naming convention or let the util handle it
- # ideally we map session_id to the directory
- # For now, let's use the standard utility but we might lose the direct mapping if not careful
- # Let's trust the return value
- session_output_dir = create_session_output_dir(base_output_dir, user_requirement)
- session.output_dir = session_output_dir
+
+ if not session.output_dir:
+ session.output_dir = create_session_output_dir(base_output_dir, user_requirement)
+
+ session_output_dir = session.output_dir
# Initialize Log capturing
session.log_file = os.path.join(session_output_dir, "process.log")
@@ -125,8 +126,13 @@ def run_analysis_task(session_id: str, files: list, user_requirement: str):
# but try to capture to file?
# Let's just write to the file.
- with open(session.log_file, "w", encoding="utf-8") as f:
- f.write(f"--- Session {session_id} Started ---\n")
+ # Let's just write to the file.
+
+ with open(session.log_file, "a" if is_followup else "w", encoding="utf-8") as f:
+ if is_followup:
+ f.write(f"\n--- Follow-up Session {session_id} Continued ---\n")
+ else:
+ f.write(f"--- Session {session_id} Started ---\n")
# We will create a custom print function that writes to the file
# And monkeypatch builtins.print? No, that's too hacky.
@@ -153,14 +159,30 @@ def run_analysis_task(session_id: str, files: list, user_requirement: str):
sys.stdout = logger # Global hijack!
try:
- llm_config = LLMConfig()
- agent = DataAnalysisAgent(llm_config, force_max_rounds=False, output_dir=base_output_dir)
-
- result = agent.analyze(
- user_input=user_requirement,
- files=files,
- session_output_dir=session_output_dir
- )
+ if not is_followup:
+ llm_config = LLMConfig()
+ agent = DataAnalysisAgent(llm_config, force_max_rounds=False, output_dir=base_output_dir)
+ session.agent = agent
+
+ result = agent.analyze(
+ user_input=user_requirement,
+ files=files,
+ session_output_dir=session_output_dir,
+ reset_session=True
+ )
+ else:
+ agent = session.agent
+ if not agent:
+ print("Error: Agent not initialized for follow-up.")
+ return
+
+ result = agent.analyze(
+ user_input=user_requirement,
+ files=None,
+ session_output_dir=session_output_dir,
+ reset_session=False,
+ max_rounds=10
+ )
session.generated_report = result.get("report_file_path", None)
session.analysis_results = result.get("analysis_results", [])
@@ -185,6 +207,10 @@ def run_analysis_task(session_id: str, files: list, user_requirement: str):
class StartRequest(BaseModel):
requirement: str
+class ChatRequest(BaseModel):
+ session_id: str
+ message: str
+
# --- API Endpoints ---
@app.get("/")
@@ -214,9 +240,21 @@ async def start_analysis(request: StartRequest, background_tasks: BackgroundTask
files = [os.path.abspath(f) for f in files] # Only use absolute paths
- background_tasks.add_task(run_analysis_task, session_id, files, request.requirement)
+ background_tasks.add_task(run_analysis_task, session_id, files, request.requirement, is_followup=False)
return {"status": "started", "session_id": session_id}
+@app.post("/api/chat")
+async def chat_analysis(request: ChatRequest, background_tasks: BackgroundTasks):
+ session = session_manager.get_session(request.session_id)
+ if not session:
+ raise HTTPException(status_code=404, detail="Session not found")
+
+ if session.is_running:
+ raise HTTPException(status_code=400, detail="Analysis already in progress")
+
+ background_tasks.add_task(run_analysis_task, request.session_id, [], request.message, is_followup=True)
+ return {"status": "started"}
+
@app.get("/api/status")
async def get_status(session_id: str = Query(..., description="Session ID")):
session = session_manager.get_session(session_id)
@@ -235,6 +273,27 @@ async def get_status(session_id: str = Query(..., description="Session ID")):
"report_path": session.generated_report
}
+@app.get("/api/export")
+async def export_session(session_id: str = Query(..., description="Session ID")):
+ session = session_manager.get_session(session_id)
+ if not session:
+ raise HTTPException(status_code=404, detail="Session not found")
+
+ if not session.output_dir or not os.path.exists(session.output_dir):
+ raise HTTPException(status_code=404, detail="No data available for export")
+
+ # Create a zip file
+ import shutil
+
+ # We want to zip the contents of session_output_dir
+ # Zip path should be outside to avoid recursive zipping if inside
+ zip_base_name = os.path.join("outputs", f"export_{session_id}")
+
+ # shutil.make_archive expects base_name (without extension) and root_dir
+ archive_path = shutil.make_archive(zip_base_name, 'zip', session.output_dir)
+
+ return FileResponse(archive_path, media_type='application/zip', filename=f"analysis_export_{session_id}.zip")
+
@app.get("/api/report")
async def get_report(session_id: str = Query(..., description="Session ID")):
session = session_manager.get_session(session_id)
@@ -250,8 +309,24 @@ async def get_report(session_id: str = Query(..., description="Session ID")):
# Fix image paths
relative_session_path = os.path.relpath(session.output_dir, os.getcwd())
web_base_path = f"/{relative_session_path}"
+
+ # Robust image path replacement
+ # 1. Replace explicit relative paths ./image.png
content = content.replace("](./", f"]({web_base_path}/")
+ # 2. Replace naked paths that might be generated like ](image.png) but NOT ](http...) or ](/...)
+ import re
+ def replace_link(match):
+ alt = match.group(1)
+ url = match.group(2)
+ if url.startswith("http") or url.startswith("/") or url.startswith("data:"):
+ return match.group(0)
+ # Remove ./ if exists again just in case
+ clean_url = url.lstrip("./")
+ return f""
+
+ content = re.sub(r'!\[(.*?)\]\((.*?)\)', replace_link, content)
+
return {"content": content, "base_path": web_base_path}
@app.get("/api/figures")
diff --git a/web/static/index.html b/web/static/index.html
index 687a624..fcfbd6f 100644
--- a/web/static/index.html
+++ b/web/static/index.html
@@ -10,7 +10,9 @@
-
+
@@ -110,63 +112,74 @@
-
-