Skip to main content
Glama
yzhou7547-cyber

DataPilot MCP Server

README.md
# DataPilot

DataPilot 是一个使用 Python 构建的智能数据分析 Agent 原型。用户上传 CSV 或 Excel
文件后,可以查看数据结构与质量报告、执行只读 DuckDB SQL、用自然语言进行
Text-to-SQL 查询,以及让 LangGraph 把复杂问题拆成多个步骤并生成分析报告和一张 PNG
图表。

当前项目重点展示可验证的分层设计、MCP 工具复用、受控 SQL、LangGraph 工作流和
SQLite Checkpoint。它是一个本地单机原型,不是面向生产环境的完整数据平台。

## 已完成能力

- 读取 `.csv` 和 `.xlsx`,Excel 默认使用第一个工作表;
- 确定性识别 Excel 标题行、多行表头、合并单元格、空行空列和明确汇总行;
- 使用 Pandas 识别字段类型、缺失值、重复行及基础统计;
- 将 DataFrame 注册到进程内 DuckDB,并返回唯一 `dataset_id`;
- 只允许一条 `SELECT` 或 `WITH ... SELECT`,结果最多返回 1000 行;
- 提供 FastAPI 数据集、查询和 Agent API;
- 提供无需 React 的本地浏览器界面,自动完成上传、提问和结果展示;
- 通过 MCP Server 暴露 Schema、Profile、Preview 和只读 SQL 工具;
- 使用 Pydantic Structured Outputs 生成 DuckDB SQL;
- 使用 LangGraph 执行单步骤 Text-to-SQL,并最多修复 SQL 两次;
- 使用父 Planning Graph 将问题拆成 1~4 个独立步骤;
- 从第一个适合绘图的成功步骤生成一张 bar 或 line PNG;
- 使用 `AsyncSqliteSaver` 持久化父图 Checkpoint;
- 通过 `thread_id` 查询最新状态和最多 50 条历史摘要;
- 使用 Mock LLM 完成无网络的全量 pytest 和端到端验收。

## 技术栈

- Python 3.11+
- FastAPI、Pydantic
- Pandas、DuckDB
- MCP Python SDK
- LangChain、LangGraph
- DeepSeek Chat Completions API
- Matplotlib
- AsyncSqliteSaver、SQLite
- pytest

## 技术架构

```mermaid
flowchart TD
    Client["HTTP / MCP 客户端"] --> API["FastAPI API 层"]
    API --> DatasetService["DatasetService"]
    API --> QueryService["QueryService"]
    API --> AgentServices["Graph Services"]

    DatasetService --> Loader["DatasetLoader"]
    Loader --> ExcelDetector["ExcelStructureDetector"]
    DatasetService --> Profiler["DatasetProfiler"]
    DatasetService --> DuckDB["DuckDBService"]
    QueryService --> DuckDB

    AgentServices --> PlanningGraph["Planning Graph"]
    PlanningGraph --> TextToSQLGraph["Text-to-SQL 子图"]
    TextToSQLGraph --> MCPClient["DataPilotMCPClient"]
    MCPClient --> MCPServer["MCP Server"]
    MCPServer --> DatasetService
    MCPServer --> QueryService

    PlanningGraph --> ChartService["ChartService"]
    PlanningGraph --> SQLite["AsyncSqliteSaver"]
```

各层职责:

- API 层:HTTP 参数校验、依赖获取和业务异常到状态码的转换;
- Service 层:组织上传、Profile、查询和 Graph 调用;
- Loader/Profiler/DuckDB/Chart:确定性底层能力,不调用 LLM;
- ExcelStructureDetector:识别工作表结构并生成分析用标准化副本,不修改原文件;
- MCP 层:把已有 Service 暴露成工具,不复制业务逻辑;
- Text-to-SQL 子图:Schema、SQL 生成、查询、修复和回答;
- Planning 父图:规划步骤、逐步调用子图、保存结果、生成图表和报告;
- Checkpointer:按 `thread_id` 保存父图各 super-step 的状态快照。

## 项目结构

```text
app/
├── api/                 # FastAPI 路由
├── graph/               # 单步骤 Text-to-SQL 子图
├── llm/                 # LLM 协议和 DeepSeek 客户端
├── models/              # Pydantic 请求、响应和领域模型
├── planning/            # 多步骤 Planning 父图
├── services/            # 数据、SQL、图表和 Graph Service
├── web/                 # 本地 HTML、CSS、JavaScript 交互页面
├── main.py              # 应用组合根与 lifespan
├── mcp_client.py        # MCP 工具调用封装
└── mcp_server.py        # MCP 工具定义
tests/                   # 单元、API、工作流和项目验收测试
sample_data/             # 可复现演示数据
```

## Excel 结构识别

上传 `.xlsx` 后,`DatasetLoader` 仍然只读取第一个工作表,但不再直接把第一行固定当作
表头。它先调用 `ExcelStructureDetector`,按照确定性规则完成:

- 跳过表头前只有标题内容的行;
- 将最多 3 行的合并式表头组合成唯一字段名;
- 只展开数据区真实存在的纵向合并单元格,例如分组或类别标签;
- 删除全空数据行、全空数据列和标签明确为“合计、总计、小计、汇总、平均”的汇总行;
- 清理字段和文本两侧空格,为重复或空白字段生成稳定名称;
- 返回工作表名、标题、表头位置、清理计数、置信度和 warnings。

原始 Excel 文件不会被修改。CSV 继续沿用原有读取方式。结构结果位于上传响应的
`summary.excel_structure`;置信度为 `low` 时应先核对 Preview。当前规则不会猜测字段的
业务含义,也不会在多个工作表或同一工作表的多个独立表格之间自动选择。

`app/graph/` 和 `app/planning/` 不是重复目录:前者处理一个自然语言查询,后者负责把
复杂请求拆成多个子查询。`TextToSQLService` 是保留并经过测试的线性基线;当前 HTTP
Agent 接口使用的是 `TextToSQLGraphService`。

## LangGraph 工作流

### 单步骤 Text-to-SQL 子图

```mermaid
flowchart LR
    Start["START"] --> Schema["load_schema"]
    Schema -->|成功| Generate["generate_sql"]
    Schema -->|失败| Failed["mark_failed"]
    Generate --> Execute["execute_sql"]
    Execute -->|成功| Answer["generate_answer"]
    Execute -->|失败且未超过 2 次| Repair["repair_sql"]
    Repair --> Execute
    Execute -->|修复耗尽| Failed
    Answer --> End["END"]
    Failed --> End
```

Schema 获取和 SQL 执行必须经过 MCP。LLM 只负责生成结构化 SQL、修复 SQL 和组织自然
语言回答,最终 SQL 仍由 DuckDB 查询层执行安全校验。

### 多步骤 Planning 父图

```mermaid
flowchart LR
    Start["START"] --> Schema["load_schema"]
    Schema --> Plan["create_plan"]
    Plan --> Select["select_next_step"]
    Select --> Child["execute_current_step"]
    Child --> Store["store_step_result"]
    Store -->|仍有步骤| Select
    Store -->|全部完成| Chart["generate_chart"]
    Chart --> Report["synthesize_report"]
    Report --> End["END"]
```

Planner 最多生成 4 个互相独立的自然语言步骤。单个步骤失败不会阻止后续步骤;至少一个
步骤成功时,整体仍可为 `completed`。图表失败只写入 `chart_error`,不会改变步骤状态。

### Checkpoint

父图使用 `AsyncSqliteSaver`,默认数据库为:

```text
.datapilot/checkpoints/checkpoints.sqlite
```

历史固定按最新到最旧返回,最多 50 条。服务重启后可以读取旧 State 和 History,但
不会自动 Resume,也不会重新注册旧 DuckDB 数据集。

## MCP 工具

MCP Streamable HTTP 服务挂载在:

```text
http://127.0.0.1:8000/mcp
```

| 工具 | 输入 | 输出 | 职责 |
|---|---|---|---|
| `get_dataset_schema` | `dataset_id` | 字段和 DuckDB 类型 | 查询 Schema |
| `get_dataset_profile` | `dataset_id` | 数据质量 Profile | 查询确定性质量报告 |
| `preview_dataset` | `dataset_id`, `limit` | 最多 5 行 | 预览数据 |
| `execute_readonly_sql` | `dataset_id`, `sql` | 最多 1000 行 | 执行受控只读 SQL |

MCP 工具只调用 `DatasetService` 和 `QueryService`,不重新实现文件读取、Profile 或 SQL。

## 安装与配置

```bash
python -m pip install -e ".[dev]"
```

环境变量示例见 `.env.example`:

```env
DEEPSEEK_API_KEY=
DEEPSEEK_BASE_URL=https://api.deepseek.com
DEEPSEEK_MODEL=deepseek-v4-flash
CHECKPOINT_DB_PATH=.datapilot/checkpoints/checkpoints.sqlite
LANGGRAPH_STRICT_MSGPACK=true
```

- `DEEPSEEK_API_KEY`:DeepSeek 控制台生成的密钥;未配置时 Agent API 返回 503;
- `DEEPSEEK_BASE_URL`:默认使用 DeepSeek 官方兼容地址;
- `DEEPSEEK_MODEL`:默认使用 `deepseek-v4-flash`,可改为账户可用的模型;
- `CHECKPOINT_DB_PATH`:服务端配置,客户端不能修改;相对路径按启动目录解析;
- `LANGGRAPH_STRICT_MSGPACK`:限制 checkpoint 反序列化类型;项目未启用 pickle fallback。

项目不会自动读取 `.env` 文件。在 VSCode 的 CMD 终端中,应分两行执行:

```cmd
set "DEEPSEEK_API_KEY=你的新密钥"
python -m uvicorn app.main:app --reload
```

PowerShell 使用:

```powershell
$env:DEEPSEEK_API_KEY="你的新密钥"
python -m uvicorn app.main:app --reload
```

密钥必须在启动 Uvicorn 前设置。重启终端后需要重新设置,且不要把真实密钥写入
README、`.env.example`、截图或 Git。

`DeepSeekLLMClient` 使用官方 OpenAI 兼容的 Chat Completions 接口。普通回答读取
`message.content`;SQL 和 AnalysisPlan 等结构化结果使用 JSON Output,再由 Pydantic
执行严格模型校验。项目不再使用 OpenAI Responses API。

启动服务:

```bash
uvicorn app.main:app --reload
```

OpenAPI 文档:`http://127.0.0.1:8000/docs`

本地操作页面:`http://127.0.0.1:8000/`

## 本地操作页面

启动 DataPilot 后,直接在浏览器打开:

```text
http://127.0.0.1:8000/
```

页面支持:

- 点击选择或拖入 CSV/XLSX 文件;
- 自动保存上传响应中的 `dataset_id`,无需手动复制;
- 显示文件名称、行列数、缺失值、重复行和质量警告;
- 展示结构识别后的前 5 行数据;
- 使用“直接查询”调用单步骤 Text-to-SQL;
- 使用“综合分析”调用多步骤 Planning Graph;
- 展示自然语言回答、SQL、查询结果、分析步骤和生成的 PNG 图表;
- 在同一浏览器标签页刷新时恢复当前数据集。

浏览器只保存 `dataset_id`,数据仍由当前 FastAPI 进程管理。服务重启后旧
`dataset_id` 会失效,需要重新上传文件。页面不加载外部前端资源,也不改变现有 API、
MCP 或 Agent 的调用边界。

## API

| 方法 | 路径 | 作用 |
|---|---|---|
| GET | `/health` | 健康检查 |
| GET | `/` | 本地上传和自然语言分析页面 |
| POST | `/api/datasets/upload` | 上传 CSV/XLSX |
| GET | `/api/datasets/{dataset_id}` | 概况和 Profile |
| GET | `/api/datasets/{dataset_id}/schema` | DuckDB Schema |
| GET | `/api/datasets/{dataset_id}/preview` | 最多 5 行预览 |
| POST | `/api/query` | 手动只读 SQL |
| POST | `/api/agent/query` | 单步骤 Text-to-SQL |
| POST | `/api/agent/analyze` | 多步骤分析、报告和图表 |
| GET | `/api/agent/analyze/{thread_id}/state` | 最新状态摘要 |
| GET | `/api/agent/analyze/{thread_id}/history` | Checkpoint 历史摘要 |

## 完整演示流程

项目提供 `sample_data/sales_demo.csv`:

```csv
region,product,sales,profit,sale_date
华东,A,120,20,2026-01-01
华南,B,90,-5,2026-01-02
华东,C,80,10,2026-01-03
华北,D,60,8,2026-01-04
```

以下命令中的 `dataset_xxx` 和 `analysis_xxx` 需要替换为上一步响应中的真实值。

### 1. 上传数据

```bash
curl -X POST http://127.0.0.1:8000/api/datasets/upload \
  -F "file=@sample_data/sales_demo.csv"
```

### 2. 查看 Profile、Schema 和 Preview

```bash
curl http://127.0.0.1:8000/api/datasets/dataset_xxx
curl http://127.0.0.1:8000/api/datasets/dataset_xxx/schema
curl http://127.0.0.1:8000/api/datasets/dataset_xxx/preview
```

### 3. 手动 SQL

```bash
curl -X POST http://127.0.0.1:8000/api/query \
  -H "Content-Type: application/json" \
  -d '{
    "dataset_id": "dataset_xxx",
    "sql": "SELECT region, SUM(sales) AS total_sales FROM dataset_xxx GROUP BY region ORDER BY total_sales DESC"
  }'
```

### 4. 单步骤 Agent

```bash
curl -X POST http://127.0.0.1:8000/api/agent/query \
  -H "Content-Type: application/json" \
  -d '{
    "dataset_id": "dataset_xxx",
    "question": "各地区总销售额分别是多少?"
  }'
```

### 5. 多步骤分析和图表

```bash
curl -X POST http://127.0.0.1:8000/api/agent/analyze \
  -H "Content-Type: application/json" \
  -d '{
    "dataset_id": "dataset_xxx",
    "question": "分析各地区销售表现,并找出利润最低的商品。"
  }'
```

响应包含:

- `thread_id`;
- 规划摘要和每个步骤结果;
- 综合报告;
- `chart` 或 `chart_error`;
- 成功、失败步骤数量和整体状态。

PNG 文件路径位于 `chart.file_path`。当前没有图表下载 API。

### 6. State 和 History

```bash
curl http://127.0.0.1:8000/api/agent/analyze/analysis_xxx/state
curl http://127.0.0.1:8000/api/agent/analyze/analysis_xxx/history
```

### 7. 重启后查询旧 thread

停止并重新启动 Uvicorn,保持 `CHECKPOINT_DB_PATH` 不变,再次调用上面的 State 和
History 接口即可读取旧 checkpoint。旧 `dataset_id` 不会恢复,因为 DatasetService
和 DuckDB 注册仍保存在内存中。

## 测试与验收

```bash
python -m compileall app tests
pytest -q
python -m pip check
```

当前验收结果:

```text
134 passed
No broken requirements found.
```

`tests/test_project_acceptance.py` 使用确定性的 Mock LLM 和模拟销售数据完整验证:上传、
Profile、手动 SQL、单步骤 Agent、多步骤分析、PNG、State、History 和应用重启后读取旧
thread。测试不会发送真实网络请求,也不会写入默认 checkpoint 数据库。

## 当前限制

- 数据集元数据和 DuckDB 注册只在当前进程内,服务重启后不会恢复;
- Excel 当前只分析第一个工作表和其中一个主要表格;低置信度结构需要人工核对 Preview;
- Checkpoint 持久化不等于 Resume,目前不能从中断节点继续;
- SQL 安全采用保守的词法规则,不是完整 SQL AST 沙箱;
- SQLite 适合单机原型,不适合高并发、多实例和高可用部署;
- 没有用户认证、权限隔离、配额、上传大小限制和审计日志;
- 本地页面面向单用户开发演示,不包含登录、会话共享或生产级文件访问控制;
- 没有结构化日志、请求关联 ID、指标或分布式追踪;
- Planner 步骤互相独立,不能消费前一步查询结果;
- 每次分析最多生成一张确定性 bar/line 图;
- 没有图表下载接口、历史清理或 checkpoint 删除;
- 没有 Resume、Interrupt、人工审批、异步 Worker、Redis 或 PostgreSQL;
- DeepSeek API 的延迟、费用、余额和模型可用性取决于外部服务。

更详细的验收结论见 `ACCEPTANCE_REPORT.md`,分阶段开发记录见 `WORKLOG.md`。

Maintenance

ActivitySlowing
ResponsivenessNo issues