diff --git a/README.md b/README.md index 574a014..232b51d 100644 --- a/README.md +++ b/README.md @@ -51,8 +51,7 @@ - `app/tools/schema.py` —— CSV 表结构描述 - `app/llm.py` —— 兼容层(旧的 get_llm),实现已移到 `app/core/llm.py` - `app/eval/` —— 评测:50 题用例、比较器(顺序/答案形态规则见 comparator.py 文件头)、报告 -- `ui/streamlit_app.py` —— 单轮网页界面(工作流版) -- `ui/chat_app.py` —— 多轮对话界面(ReAct 版 + 记忆,支持追问) +- `ui/web/` —— 前端(React + TS),由 `python -m app.server --open` 托管 ## 运行 @@ -87,12 +86,6 @@ python run_eval.py python run_eval.py --oracle python run_eval.py --repeat 3 # 同一配置连跑 3 次,看均值与波动 python run_eval.py --rescore outputs/eval_xxx.json # 不调模型,按当前比较器/标准答案重新打分 - -# 3d. 单轮网页界面(工作流版) -streamlit run ui/streamlit_app.py - -# 3e. 多轮对话界面(ReAct 版,支持记忆与追问) -streamlit run ui/chat_app.py ``` 在代码里调用: diff --git a/app/core/service.py b/app/core/service.py index 55a5819..1f6934d 100644 --- a/app/core/service.py +++ b/app/core/service.py @@ -1,6 +1,6 @@ """统一的分析服务函数 analyze()(B16)。 -所有入口(run.py / run_react.py / run_eval.py / 两个 Streamlit 页面,以及以后的 FastAPI) +所有入口(run.py / run_react.py / run_eval.py / app/server 的 FastAPI 后端) 都只调这一个函数,不再各自装配 schema、字典检索、图和模型。 约定: diff --git a/docs/DEVLOG.md b/docs/DEVLOG.md index 479f41f..a44cb56 100644 --- a/docs/DEVLOG.md +++ b/docs/DEVLOG.md @@ -39,7 +39,7 @@ Python · LangGraph(StateGraph 工作流版 + create_react_agent ReAct 版) | B11 | P2 | eval/report.py:31-34, :40 | 正确率分母只算 executed(虚高);无报错修复率记 100% | 1 | | B12 | P2 | eval/comparator.py _normalize_obj | Series 转 dict 比较忽略顺序(虚高);答案形态不一致(虚低) | 1 | | B13 | P2 | eval/runner.py | 只测工作流版;双重执行;无耗时/token | 3 | -| B14 | P2 | ui/streamlit_app.py | 未接字典 RAG,与 README 不符 | 1(随 Streamlit 下线) | +| B14 | P2 | ui/streamlit_app.py | 未接字典 RAG,与 README 不符 | 1(随 Streamlit 下线,已删 [67]) | | B15 | P2 | tools/csv_io.py | latin-1 兜底静默乱码 | 1 | | B16 | P2 | 4 个入口 | 各自装配流程,无服务层 | 1 | | B17 | P2 | requirements.txt | 未锁版本;无测试、CI、日志框架 | 0 | @@ -1903,6 +1903,16 @@ S 10-21 第二轮: 走查记录:(S 跑完后填) +**走查记录(S,2026-10-23 ~ 10-26,Windows + Firefox 缩放 90%,真 Key)** +- 五条路径全部通过(S 10-26:「其他的没问题」「都是能接受的」)。⑥ 按 10-21 的约定不用复核。⑦ THREAT_MODEL §2.3 S 10-26 确认无异议。 +- 10-23 第一批发现:白屏(Windows .js MIME,#74)和观感 8 条,进了 V2c(#75 / #76)。测试 agent 的基线报告(假模型 ①~⑥ 全过)也一并处理了。 +- 10-26 真 Key 发现,S 定为**可接受、不处理**: + - token 消耗偏高。多表题出 4 张图,没取到数字;单轮预算 130,000 兜底([66])。 + - 「写代码」阶段停不下来。停止只在事件之间检查(service.py:119-121),正在进行的模型调用要等它返回;单次 60 秒,加最多 3 次重试(llm.py:33-34)。这是 F2「不主动断流」的设计。 + - 图表标题选不中、复制不了。标题是画在 ECharts canvas 上的。 + - 会话页不显示模式(标准 / 边想边做),只能从步骤条有没有「理解问题」看出来。 +- 本机环境:ui\web\dist 是用管理员身份解压的,ACL 锁住了,要在管理员 PowerShell 里用 C:\Windows\System32\takeown.exe / icacls.exe 解锁后才能删(PATH 里没有 System32)。 + ## [56] 2026-10-22 · F4 迭代 V2a 方案:按 S 设计稿改版 · commit e4b6ef1 [55] 回填为 b6769cd。 @@ -2204,7 +2214,7 @@ S 10-25 定:§2 上游原文折叠保留;§4 根字号 14。 方案见 docs/roadmap/phase5-frontend.md「V2d」:1 usage 拆分(cache_read / reasoning / estimated,零调用 reported=None,报告导出 6 个字段);2 单轮预算默认 130,000(ReAct 闸门实测最高 63,817 的约 2 倍),调用前判定并预留收尾量,ReAct 走强制收尾、工作流不进 reviewer,缺 usage 按字数估算,卡片显示 tokens;3 finish_reason=length 作为明确错误,本节不设 max_tokens。其余各项记进 plan.md「外部审计待办」。 -## [66] 2026-10-26 · V2d 实现:token 观测与单轮预算 · commit <待提交> +## [66] 2026-10-26 · V2d 实现:token 观测与单轮预算 · commit 2b283b6(PR #78) [65] 回填为 1144c27(PR #77;master tests run 40255033298 绿,这个提交只触发了 tests 工作流)。按 phase5-frontend.md「V2d」§1~3 实现,偏离写在该节「实现与偏离」。 @@ -2214,3 +2224,18 @@ S 10-25 定:§2 上游原文折叠保留;§4 根字号 14。 - 测试设施:ScriptedChatModel tokens_per_call=None 模拟不回 usage;fake_llm 新增 tokens- 场景、MARK_FINALIZE。 - 判定变更:旧断言一条没改(零调用 reported=None 的路径以前没有测试断言)。新增 tests/test_v2d.py 20 条、src/views/v2d.test.tsx 5 条、e2e/f4-budget.spec.ts 1 条(fake 项目按文件名 f[34]* 匹配)。提示词、chart_fixtures 不变。视觉 11、25 两屏:token 文字让 `.acard-time` 遮罩框变宽(每张 1119 像素,只在遮罩框里),基线换成首跑 CI run 40260578380 的 x64 actual;我本地报的「基线不用重取」是错的——本地 aarch64 这 4 屏本来就红,看不出这个差异。 + +## [67] 2026-10-26 · F5 通过,删 Streamlit · commit <待提交> + +[66] 回填为 2b283b6(PR #78;master tests run 40262438449 绿)。F5 走查记录补在 [55] 末尾:五条路径都过了;10-26 的 4 条发现 S 定为可接受,不处理。 + +按 phase5「F5 · 通过与收尾」的清单逐条删除: +- [x] `ui/streamlit_app.py`、`ui/chat_app.py` +- [x] `requirements.txt` 的 `streamlit==1.65.0` +- [x] `tests/test_ui_smoke.py`(3 个测试项:test_page_renders × 2 页 + test_chat_app_turn_goes_through_analyze)。pytest 基线从 872 降到 869,这是删除带来的,不是回归。 +- [x] `packaging/build_sidecar.py` 的 `--exclude-module streamlit` +- [x] README 里的 Streamlit 界面介绍和两条 `streamlit run` +- [x] HANDOVER 第 20 行;DEVLOG B14 标成「已删」 +- 另外改了:app/core/service.py 文件头的入口列表、plan.md 阶段 5 的状态(标成完成)。 + +pyarrow:它只是 streamlit 的传递依赖([18],DEVLOG 第 771 行),所以删掉 streamlit 后,新装的环境里不会再有 pyarrow。pandas 3.0 在没有 pyarrow 时字符串后端改用 python,开发和打包也因此一致了。这样 [18] 留到阶段 6 的「pyarrow 去留」问题就不用再定了,sidecar 体积预计少约 78 MB,以 CI 的 sidecar 工件为准。验证:我用全新 venv 装 requirements-dev.txt,确认没有 streamlit 和 pyarrow,再跑全量 pytest。 diff --git a/docs/HANDOVER.md b/docs/HANDOVER.md index 2ef1cf1..9efdbd6 100644 --- a/docs/HANDOVER.md +++ b/docs/HANDOVER.md @@ -17,12 +17,12 @@ - `app/server`:FastAPI 本地后端(sidecar),SQLite(写法兼容 PG),SSE 推事件,Bearer token,托管前端 dist 并带 CSP。启动契约见 `docs/sidecar-contract.md`(壳必须 `--port 0`)。 - `ui/web`:React 19 + TS 前端(Vite、vitest、Playwright),图表用 ECharts(按需引入)。**前端只通过 HTTP/SSE 和后端通信,不用 Tauri IPC**(S 定,2026-10-12)。 - 外壳:Tauri 2 + PyInstaller 打包的 sidecar,在 GitHub Actions 的 Windows runner 上构建(`packaging/`、`.github/workflows/sidecar.yml`)。 -- Streamlit 旧界面(`ui/streamlit_app.py`、`ui/chat_app.py`)还在,F5 验收后单独 PR 删除(连带面 6 处,清单在 phase5「F5」)。 +- Streamlit 旧界面已在 F5 验收后删除(DEVLOG [67])。 进度:阶段 0–4 完成;**阶段 5(前端)收尾中**,方案是 `docs/roadmap/phase5-frontend.md`。 - F0–F2(#32–#35)、F3 ECharts 六步(#41–#53)、F4 层 1 面板(#57)、F4 迭代 V1(#67)、F4 迭代 V2a(#70,按 S 的设计稿整体重做界面)都已合并。 - 和 F 路径并行的:chart_errors 按执行记账(result.py:117,#55,已完成)、#49 矮画布紧凑几何(#58)、#44 sidecar 契约(#59)、T11 错误文案脱敏(#61)、A1 错题自动复跑(#63)、G1 闸门 oracle 上 CI(#65)、`llm_kind`(含在 #70 里)。 -- F5(S 用真 Key 走五条路径)的清单已合并(#68,DEVLOG [55]),**还没走**,在等 S。 +- F5:S 用真 Key 走完五条路径并通过(2026-10-26,走查记录在 DEVLOG [55]);Streamlit 已删(DEVLOG [67])。阶段 5 完成,下一步阶段 6。 - 最近一步:四屏视觉回归(`ui/web/e2e/f4-visual.spec.ts`,DEVLOG [59])。 基线数字(39c4741 + [59],Linux 沙箱):pytest 837 passed(含闸门 25 条,需先下数据)、vitest 790、e2e 38、lint / tsc / build / `gen_ui_types --check` 全过。 @@ -79,7 +79,7 @@ S 的评审风格:逐条评审,会对照代码和截图核实,按 P0(挡 - 设置页「测试连接」:要后端新接口。 - P1-17:图表警告文案("x 列 '…' 不存在"暴露变量名)在后端 `chart_warnings`,前端改写就是匹配后端文案,要改就改后端。 - P1-16:失效页"重新打开应用"浏览器里做不到,放阶段 6。 -4. **F5**:S 用真 Key 走五条路径(清单在 phase5「F5」),走完把记录填进 DEVLOG [55]。然后单独 PR 删 Streamlit(pytest 基线 814 → 811 是当时的数,现在要按 837 重算)。 +4. **F5**:已通过(2026-10-26),Streamlit 已删,pytest 基线 872 → 869(DEVLOG [67])。 5. **阶段 6** Tauri 打包。代码签名证书要由有实名主体、能付款的一方办(S 没有),选型(大概率 OV 起步)在 phase5「F5」末尾的阶段 6 前置事项里。 ## 6. 环境搭建与运行 diff --git a/docs/roadmap/phase5-frontend.md b/docs/roadmap/phase5-frontend.md index 2404edc..30b7836 100644 --- a/docs/roadmap/phase5-frontend.md +++ b/docs/roadmap/phase5-frontend.md @@ -705,7 +705,7 @@ v2 才可达的字段(link、formatter、aria 等)留到层 2 再测。 估时 1.5 天,一个 PR。 -### F5:S 验收走查(已定,S 10-21 评审通过;DEVLOG [55]) +### F5:S 验收走查(已通过 2026-10-26,走查记录见 DEVLOG [55];Streamlit 已删,DEVLOG [67]) F5 不是回归清单,而是一次真 Key 的手动走查加观感判断,预计一小时内跑完。32 条 e2e 已经覆盖的内容这里不重抄,只列"看什么、什么算过、截图留哪"。启动命令:`python -m app.server --open`。 diff --git a/docs/roadmap/plan.md b/docs/roadmap/plan.md index 922e655..cd85484 100644 --- a/docs/roadmap/plan.md +++ b/docs/roadmap/plan.md @@ -18,7 +18,7 @@ | 3.5 外部验证闸门 | 3 天 | 非自造数据上的 25 题(单表 15:UCI Online Retail 改造的中文 GBK 脏表;多表 10:Olist 8 张表);标准答案由独立脚本在原始数据上算出;最小多表支持(dfs);报告逐题标判对层级、评测集指纹 | 按 docs/roadmap/gate-phase35.md 预登记的阈值与失败分布规则出结论 | M01 M02 M04 M05 M10(部分) M15 | | 4 后端 API | 1–2 周 | 第一步:API Key 进系统钥匙串(keyring);`analyze_stream()` 逐步产出事件,`analyze()` 包一层(M09)。FastAPI:数据集上传(一个数据集可含多个文件,M10)、会话、消息(SSE 流式推送每一步)、设置;SQLite + SQLAlchemy(表结构兼容 PG,预留 user_id);logging + trace id(会话 / 消息 id),runs.jsonl 换成数据库(M08 M14);接口返回 png + 图表数据(M13)。最后:PyInstaller sidecar + 沙箱 worker 在 Actions Windows runner 上的 spike(B35 / M06) | 用 curl 走完"上传 → 提问 → 流式结果 → 历史可查";重启后会话还在;spike 打出的 exe 能在干净 runner 上执行一次沙箱代码 | B18 B35 | | 5 前端 | 2–3 周 | 第一件事(M13,从阶段 4 顺延,含后端三步):图表库定为 ECharts;定 option 格式与白名单、写 builder `chart(kind, df, x=, y=, series=, title=)`;worker 产出 chart_option(与 result 脱钩,B44);接口加 chart_option;改绘图提示词并重跑闸门。之后:React + Vite + TS:数据集面板、对话流(展示计划、代码、执行步骤)、图表、可展开的代码、👍👎 反馈、Key 设置页,分步与验收见 docs/roadmap/phase5-frontend.md(F0–F5) | 不碰命令行就能完整用一遍 | — | -| 6 桌面打包 | 1–2 周 | Tauri 2 外壳(前提见上:前端只走 HTTP)+ PyInstaller 后端 sidecar;Windows 安装包;自动更新;首次启动引导填 Key;依赖许可证审计;决定 pyarrow 去留(打包体积 78.5 MB,开发与打包环境须一致,见 DEVLOG [18]) | 在一台干净的 Windows 上一键安装,可用;对外分发前代码签名、SmartScreen 不拦(M19);对外分发前满足 THREAT_MODEL.md 的 (b) 条件 | — | +| 6 桌面打包 | 1–2 周 | Tauri 2 外壳(前提见上:前端只走 HTTP)+ PyInstaller 后端 sidecar;Windows 安装包;自动更新;首次启动引导填 Key;依赖许可证审计;pyarrow 已随 Streamlit 一起去掉(DEVLOG [67]) | 在一台干净的 Windows 上一键安装,可用;对外分发前代码签名、SmartScreen 不拦(M19);对外分发前满足 THREAT_MODEL.md 的 (b) 条件 | — | | 7 准确率 | 2–4 周 | DuckDB 统一 CSV/Excel/SQL;YAML 语义模型(指标、口径、同义词);枚举值召回;结果合理性校验;反馈沉淀为已验证问答对 | 在阶段 3.5 闸门数据上给出具体目标(闸门结论后定,M20);自造集只作回归 | B05 B09 | | 8 Web 上线(后续) | 另定 | PostgreSQL、登录、任务队列、Docker/E2B 沙箱、Langfuse、服务器部署与 ICP 备案 | 另定 | 多租户 | @@ -30,7 +30,7 @@ | 0–3d | 完成 | 2026-10-09 → 10-10 | DEVLOG [1]–[9] | | 3.5 / 3.6 | 完成,闸门中间区,P0 ⑤⑥ 已修 | 2026-10-12 | DEVLOG [10]–[13] | | 4 | **完成**,三条验收都达到 | 2026-10-12 → 10-13 | DEVLOG [14]–[18];curl 全流程与重启见 [16],跨重启多轮见 [17],干净 runner 上执行沙箱代码见 [18] | -| 5 | **收尾中**:M13 后端三步、F0–F3、F4 层 1、F4 迭代 V1 / V2a、四屏视觉回归已合并;剩 V2b、测试连接、P1-17(方案见 phase5「阶段 5 收尾」)→ F5 真 Key 走查 → 删 Streamlit | 2026-10-13 → 进行中 | DEVLOG [19]–[60];最近合并 #71(1c0b41a,tests run 39891923237) | +| 5 | **完成**:F0–F4、V1 / V2a~V2d、F5 真 Key 走查通过(2026-10-26),Streamlit 已删 | 2026-10-13 → 10-26 | DEVLOG [19]–[67] | **重估(M18)**:阶段 0–4 原估 5–7.5 周,实际 5 个日历日。原因是代码主要由 Hark 写、S 审,**不能**照这个速度外推后面的阶段: - 阶段 0–4 几乎都能在 Linux 沙箱 + CI 里验证;阶段 5–6 的验收是"不碰命令行能用""干净 Windows 一键安装",必须 S 在本机动手看,日历时间取决于 S 的可用时间,不取决于写代码的速度。 diff --git a/packaging/build_sidecar.py b/packaging/build_sidecar.py index 19048ed..3ec3a8c 100644 --- a/packaging/build_sidecar.py +++ b/packaging/build_sidecar.py @@ -28,7 +28,6 @@ "--copy-metadata", "keyring", "--collect-submodules", "keyring.backends", # 用不到、但会被分析器拖进来的大包 - "--exclude-module", "streamlit", "--exclude-module", "tkinter", "--exclude-module", "IPython", "--exclude-module", "pytest", diff --git a/requirements.txt b/requirements.txt index a26e478..c4e0142 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,7 +7,6 @@ pandas==3.0.6 numpy==2.4.6 matplotlib==3.11.2 python-dotenv==1.2.4 -streamlit==1.65.0 rank-bm25==0.2.2 jieba==0.42.1 # 阶段 4:API Key 存系统钥匙串(app/core/keystore.py)。依赖按平台锁定(uv pip compile,3.11/3.12 × Windows/Linux) diff --git a/tests/test_ui_smoke.py b/tests/test_ui_smoke.py deleted file mode 100644 index a299102..0000000 --- a/tests/test_ui_smoke.py +++ /dev/null @@ -1,46 +0,0 @@ -"""两个 Streamlit 页面的冒烟测试:脚本能加载并渲染初始界面(不上传文件、不调模型)。 -Streamlit 计划在阶段 5 下线,这里只保证阶段 1 改走 analyze() 后页面没坏。""" -import os - -import pytest - -from conftest import ROOT - -AppTest = pytest.importorskip("streamlit.testing.v1").AppTest - - -@pytest.mark.parametrize("page", ["streamlit_app.py", "chat_app.py"]) -def test_page_renders(page): - at = AppTest.from_file(os.path.join(ROOT, "ui", page), default_timeout=30).run() - assert not at.exception - assert at.title[0].value == "对话式数据分析 Agent" - - -def test_chat_app_turn_goes_through_analyze(monkeypatch, sample_path): - """多轮页面:一轮提问经 analyze(agent="react", memory=...) 完成并渲染回答。""" - import app.core - from langchain_core.messages import AIMessage - from langgraph.checkpoint.memory import MemorySaver - - from app.core.testing import ScriptedChatModel - - real = app.core.analyze - seen = {} - - def fake_analyze(*a, **k): - seen.update(k) - model = ScriptedChatModel(replies=[AIMessage(content="", tool_calls=[{ - "name": "run_python_on_data", "args": {"code": "result = 1"}, "id": "c1"}]), "答:1"], calls=[]) - return real(*a, chat_model=model, llm_config=app.core.LLMConfig(api_key="t"), **k) - monkeypatch.setattr(app.core, "analyze", fake_analyze) - - at = AppTest.from_file(os.path.join(ROOT, "ui", "chat_app.py"), default_timeout=30) - at.session_state["memory"] = MemorySaver() - at.session_state["df_path"] = sample_path - at.session_state["thread_id"] = "t1" - at.session_state["history"] = [] - at.run() - at.chat_input[0].set_value("总销量").run() - assert not at.exception - assert seen["agent"] == "react" and seen["thread_id"] == "t1" - assert any("答:1" in m.value for m in at.markdown) diff --git a/ui/chat_app.py b/ui/chat_app.py deleted file mode 100644 index ff6a452..0000000 --- a/ui/chat_app.py +++ /dev/null @@ -1,85 +0,0 @@ -"""对话式前端(多轮记忆 + 追问)。 - -基于 ReAct 智能体 + LangGraph MemorySaver:同一个 thread_id 下, -模型能记住前几轮问了什么、得出什么结论,因此可以自然追问 -("那华北呢?""再按产品拆一下")。 -阶段 1 起经 app.core.analyze(agent="react", memory=..., thread_id=...) 调用。 - -运行:streamlit run ui/chat_app.py -""" -import os -import sys -import tempfile -import uuid - -import streamlit as st -from dotenv import load_dotenv - -_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -if _ROOT not in sys.path: - sys.path.insert(0, _ROOT) - -load_dotenv(os.path.join(_ROOT, ".env")) - -from langgraph.checkpoint.memory import MemorySaver - -from app.core import analyze - -st.set_page_config(page_title="对话式数据分析 Agent", layout="wide") -st.title("对话式数据分析 Agent") -st.caption("上传 CSV → 自然语言提问 → 自动写 pandas、执行、画图;支持多轮追问") - -uploaded = st.file_uploader("上传 CSV 数据文件", type=["csv"]) - - -def _reset_session(df_path: str): - """为新上传的文件重建记忆与对话历史。""" - st.session_state.df_path = df_path - st.session_state.memory = MemorySaver() - st.session_state.thread_id = uuid.uuid4().hex - st.session_state.history = [] # [{role, content, charts}] - - -if uploaded is not None: - sig = (uploaded.name, uploaded.size) - if st.session_state.get("file_sig") != sig: - tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".csv") - tmp.write(uploaded.getvalue()) - tmp.close() - st.session_state.file_sig = sig - _reset_session(tmp.name) - -if "memory" not in st.session_state: - st.info("请先上传一个 CSV 文件。") - st.stop() - -# 渲染历史对话(含图表) -for turn in st.session_state.history: - with st.chat_message(turn["role"]): - st.write(turn["content"]) - for c in turn.get("charts", []): - if os.path.exists(c): - st.image(c) - -question = st.chat_input("用自然语言提问,可继续追问…") -if question: - st.session_state.history.append({"role": "user", "content": question}) - with st.chat_message("user"): - st.write(question) - - with st.chat_message("assistant"): - with st.spinner("Agent 正在思考、写代码、执行、画图…"): - res = analyze( - st.session_state.df_path, question, agent="react", - memory=st.session_state.memory, thread_id=st.session_state.thread_id, - log=True, - ) - answer = res.answer or (f"(未能完成:{res.error})" if res.error else "") - st.write(answer) - for c in res.charts: - if os.path.exists(c): - st.image(c) - - st.session_state.history.append( - {"role": "assistant", "content": answer, "charts": list(res.charts)} - ) diff --git a/ui/streamlit_app.py b/ui/streamlit_app.py deleted file mode 100644 index 3d39745..0000000 --- a/ui/streamlit_app.py +++ /dev/null @@ -1,61 +0,0 @@ -"""单轮网页界面(工作流版)。经 app.core.analyze() 调用(B14/B16:与 CLI 同一条路径,自动带数据字典检索)。 -注意:上传的 CSV 落在临时文件里,没有同名 .dict.csv,所以只有本地数据(如 data/sample.csv)才有字典; -Streamlit 界面计划在阶段 5 由 React 前端替代。 - -运行:streamlit run ui/streamlit_app.py -""" -import os -import sys -import tempfile - -import streamlit as st -from dotenv import load_dotenv - -_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -if _ROOT not in sys.path: - sys.path.insert(0, _ROOT) - -load_dotenv(os.path.join(_ROOT, ".env")) - -from app.core import analyze - -st.set_page_config(page_title="对话式数据分析 Agent", layout="wide") -st.title("对话式数据分析 Agent") - -uploaded = st.file_uploader("上传 CSV 数据文件", type=["csv"]) -dict_file = st.file_uploader("(可选)上传数据字典 .dict.csv(两列 field,description)", type=["csv"]) -question = st.text_input("用自然语言提问", value="哪个地区的总销售额最高?画个柱状图。") - -if st.button("开始分析") and uploaded and question: - # 把上传文件落地到临时目录,供 analyze 读取;字典按"同名 .dict.csv"约定放在旁边 - tmpdir = tempfile.mkdtemp(prefix="datachat_") - df_path = os.path.join(tmpdir, "data.csv") - with open(df_path, "wb") as f: - f.write(uploaded.getvalue()) - if dict_file is not None: - with open(os.path.join(tmpdir, "data.dict.csv"), "wb") as f: - f.write(dict_file.getvalue()) - - with st.spinner("Agent 正在规划、写代码、执行、纠错…"): - res = analyze(df_path, question, agent="workflow", log=True) - - if res.error_kind in ("dataset", "llm"): - st.error(res.error) - - st.subheader("最终回答") - st.write(res.answer) - - if res.chart_path and os.path.exists(res.chart_path): - st.image(res.chart_path) - - with st.expander("查看 Agent 的过程(计划 / 代码 / 重试次数 / 耗时)"): - if res.field_notes: - st.markdown("**命中的字段说明**") - st.text("\n".join(f"- {f}:{d}" for f, d in res.field_notes)) - st.markdown("**分析计划**") - st.text(res.plan) - st.markdown("**生成代码**") - st.code(res.code, language="python") - st.markdown(f"**重试次数**:{res.attempts}") - u = res.usage - st.markdown(f"**耗时**:{res.timings.get('total', 0):.1f} 秒;**token**:{u.total_tokens}({u.calls} 次调用)")