diff --git a/.gitignore b/.gitignore index b022fd9..cb320b6 100644 --- a/.gitignore +++ b/.gitignore @@ -56,4 +56,7 @@ mcp.json.bak docs/assets/backup/ docs/assets/preview.html -claude.md \ No newline at end of file +claude.md +# Backend config files +backend/.env +backend/data/database/*.db diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..bec8d29 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,519 @@ +# CLAUDE.md - Mini Agent 的 AI 助手指南 + +本文档为使用 Mini Agent 代码库的 AI 助手(如 Claude)提供全面指导。涵盖项目结构、开发工作流程、关键约定和最佳实践。始终使用中文回复用户。 + +## 项目概述 + +**Mini Agent** 是一个简洁而专业的演示项目,展示了使用 MiniMax M2 模型构建智能体的最佳实践。它使用兼容 Anthropic 的 API,并完全支持交错思维(interleaved thinking)来处理复杂的长时间运行任务。 + +### 核心特性 + +- 完整的智能体执行循环,带有基础文件系统和 shell 操作工具 +- 通过会话笔记工具实现持久化记忆 +- 智能上下文管理,支持自动对话摘要 +- Claude Skills 集成(15 个专业技能,涵盖文档、设计、测试、开发) +- MCP(模型上下文协议)工具集成 +- 用于调试的全面日志记录 +- 多提供商 LLM 支持(Anthropic 和 OpenAI 协议) + +### 技术栈 + +- **语言**: Python 3.10+ +- **包管理器**: uv(现代 Python 包管理器) +- **测试**: pytest 配合 asyncio 支持 +- **依赖**: httpx, pydantic, pyyaml, tiktoken, prompt-toolkit, mcp, anthropic, openai +- **构建系统**: setuptools + +## 仓库结构 + +``` +Mini-Agent/ +├── mini_agent/ # 核心源代码 +│ ├── __init__.py +│ ├── agent.py # 主智能体执行循环 +│ ├── cli.py # 使用 prompt_toolkit 的命令行界面 +│ ├── config.py # 配置加载逻辑 +│ ├── logger.py # 全面的日志系统 +│ ├── retry.py # 指数退避的重试机制 +│ ├── llm/ # LLM 客户端抽象 +│ │ ├── base.py # LLM 客户端的抽象基类 +│ │ ├── anthropic_client.py # Anthropic API 实现 +│ │ ├── openai_client.py # OpenAI API 实现 +│ │ └── llm_wrapper.py # LLMClient 工厂 +│ ├── schema/ # 数据模型 +│ │ └── schema.py # 消息、响应等的 Pydantic 模型 +│ ├── tools/ # 工具实现 +│ │ ├── base.py # 基础 Tool 类和 ToolResult +│ │ ├── file_tools.py # ReadTool, WriteTool, EditTool +│ │ ├── bash_tool.py # BashTool, BashOutputTool, BashKillTool +│ │ ├── note_tool.py # 用于持久化记忆的 SessionNoteTool +│ │ ├── skill_tool.py # Skill 工具 (get_skill) +│ │ ├── skill_loader.py # 从子模块加载 Claude Skills +│ │ └── mcp_loader.py # MCP 服务器集成 +│ ├── skills/ # Claude Skills(git 子模块) +│ ├── utils/ # 工具函数 +│ │ └── terminal_utils.py # 终端显示宽度计算 +│ └── config/ # 配置文件 +│ ├── config-example.yaml # 配置模板 +│ ├── system_prompt.md # 智能体的系统提示 +│ └── mcp.json # MCP 服务器配置 +├── tests/ # 测试套件 +│ ├── test_agent.py # 智能体集成测试 +│ ├── test_llm.py # LLM 客户端测试 +│ ├── test_note_tool.py # 会话笔记工具测试 +│ ├── test_skill_tool.py # Skill 工具测试 +│ ├── test_mcp.py # MCP 加载测试 +│ └── ... +├── docs/ # 文档 +│ ├── DEVELOPMENT_GUIDE.md # 详细开发指南 +│ └── PRODUCTION_GUIDE.md # 生产部署指南 +├── scripts/ # 设置和工具脚本 +├── examples/ # 使用示例 +├── workspace/ # 默认工作空间目录(已忽略) +├── pyproject.toml # 项目配置和依赖 +├── uv.lock # 锁定的依赖 +├── README.md # 主文档 +└── CONTRIBUTING.md # 贡献指南 +``` + +## 核心架构 + +### 1. 智能体执行循环 + +**文件**: `mini_agent/agent.py` + +`Agent` 类实现了核心执行循环: + +- **消息管理**: 维护对话历史记录并自动计算 token +- **上下文摘要**: 当超过 token 限制时自动摘要历史记录(默认:80,000 tokens) +- **工具执行**: 管理工具调用和结果 +- **步骤限制**: 通过可配置的 max_steps 防止无限循环(默认:100) +- **工作空间管理**: 处理工作空间目录和路径解析 + +**关键方法**: +- `run(task: str)`: 任务的主执行循环 +- `add_user_message(content: str)`: 向历史记录添加用户消息 +- `_estimate_tokens()`: 使用 tiktoken 精确计算 token +- `_summarize_history()`: 智能上下文压缩 + +### 2. LLM 客户端抽象 + +**文件**: `mini_agent/llm/` + +LLM 层已被抽象化以支持多个提供商: + +- **`base.py`**: 定义 `LLMClientBase` 抽象接口 +- **`anthropic_client.py`**: Anthropic Messages API 实现 +- **`openai_client.py`**: OpenAI Chat Completions API 实现 +- **`llm_wrapper.py`**: 根据配置创建适当客户端的工厂 + +**关键特性**: +- 与提供商无关的接口 +- 自动 API 端点构建(附加 `/anthropic` 或 `/v1`) +- 指数退避的重试机制 +- 思维块支持(针对支持它的模型) +- 工具调用标准化 + +**配置**: +```yaml +provider: "anthropic" # 或 "openai" +api_key: "YOUR_API_KEY" +api_base: "https://api.minimax.io" +model: "MiniMax-M2" +``` + +### 3. 工具系统 + +**文件**: `mini_agent/tools/` + +所有工具都继承自 `base.py` 中的 `Tool` 基类: + +**工具接口**: +```python +class Tool: + @property + def name(self) -> str: ... + + @property + def description(self) -> str: ... + + @property + def parameters(self) -> dict[str, Any]: ... + + async def execute(self, *args, **kwargs) -> ToolResult: ... + + def to_schema(self) -> dict: ... # Anthropic 格式 + def to_openai_schema(self) -> dict: ... # OpenAI 格式 +``` + +**内置工具**: +- **ReadTool**: 读取文件内容,支持可选的行范围 +- **WriteTool**: 创建或覆盖文件 +- **EditTool**: 使用旧/新字符串替换编辑现有文件 +- **BashTool**: 执行带超时的 bash 命令 +- **BashOutputTool**: 读取后台 bash 进程的输出 +- **BashKillTool**: 终止后台 bash 进程 +- **SessionNoteTool**: 用于会话记忆的持久化笔记 +- **get_skill**: 动态加载 Claude Skills + +### 4. 配置系统 + +**文件**: `mini_agent/config.py` + +配置按优先级从 YAML 文件加载: +1. `mini_agent/config/config.yaml`(开发模式) +2. `~/.mini-agent/config/config.yaml`(用户配置) +3. 包安装目录配置 + +**关键配置选项**: +- `api_key`: MiniMax API 密钥 +- `api_base`: API 端点 URL +- `model`: 模型名称(如 "MiniMax-M2") +- `provider`: LLM 提供商("anthropic" 或 "openai") +- `max_steps`: 最大执行步数(默认:100) +- `workspace_dir`: 工作目录路径 +- `system_prompt_path`: 系统提示文件路径 +- `tools.*`: 工具启用/禁用开关 +- `retry.*`: 重试配置 + +### 5. Skills 系统 + +**文件**: `mini_agent/tools/skill_tool.py`, `mini_agent/tools/skill_loader.py` + +Claude Skills 使用**渐进式披露**从 `skills/` git 子模块加载: +- **第 1 级**: 启动时显示元数据(名称、描述) +- **第 2 级**: 通过 `get_skill(skill_name)` 加载完整内容 +- **第 3 级及以上**: 根据需要加载额外的资源和脚本 + +**Skills 包括**: PDF、PPTX、DOCX、XLSX、canvas-design、algorithmic-art、testing、MCP-builder、skill-creator 等。 + +### 6. MCP 集成 + +**文件**: `mini_agent/tools/mcp_loader.py` + +模型上下文协议(MCP)服务器在 `mcp.json` 中配置并动态加载。预配置的服务器包括: +- **memory**: 知识图谱记忆系统 +- **minimax_search**: 网页搜索和浏览功能 + +## 开发工作流程 + +### 设置开发环境 + +```bash +# 克隆仓库 +git clone https://github.com/MiniMax-AI/Mini-Agent.git +cd Mini-Agent + +# 安装 uv(如果尚未安装) +curl -LsSf https://astral.sh/uv/install.sh | sh + +# 同步依赖 +uv sync + +# 初始化 Claude Skills(可选) +git submodule update --init --recursive + +# 复制配置模板 +cp mini_agent/config/config-example.yaml mini_agent/config/config.yaml + +# 使用你的 API 密钥编辑 config.yaml +# vim mini_agent/config/config.yaml +``` + +### 运行智能体 + +```bash +# 方法 1:作为模块运行(适合调试) +uv run python -m mini_agent.cli + +# 方法 2:以可编辑模式安装(推荐) +uv tool install -e . +mini-agent +mini-agent --workspace /path/to/project + +# 指定工作空间目录 +mini-agent --workspace /path/to/your/project +``` + +### 运行测试 + +```bash +# 运行所有测试 +pytest tests/ -v + +# 运行特定测试文件 +pytest tests/test_agent.py -v + +# 运行带覆盖率的测试 +pytest tests/ -v --cov=mini_agent + +# 运行核心功能测试 +pytest tests/test_agent.py tests/test_note_tool.py -v +``` + +### 代码风格和约定 + +**提交信息格式**: +``` +<类型>(<范围>): <描述> + +类型: +- feat: 新功能 +- fix: 错误修复 +- docs: 文档更改 +- style: 代码风格(格式化,无逻辑更改) +- refactor: 代码重构 +- test: 测试更改 +- chore: 构建/工具更改 + +示例: +- feat(tools): 添加新的文件搜索工具 +- fix(agent): 修复工具调用的错误处理 +- refactor(llm): 为多个提供商抽象 LLM 客户端 +``` + +**Python 约定**: +- 所有函数参数和返回值都使用类型提示 +- 所有类和公共方法都有文档字符串 +- 使用 Pydantic 进行数据验证 +- 使用 async/await 处理 I/O 操作 +- 使用 pathlib.Path 处理文件路径 + +### 添加新工具 + +1. 在 `mini_agent/tools/` 中创建新文件: +```python +from mini_agent.tools.base import Tool, ToolResult +from typing import Dict, Any + +class MyTool(Tool): + @property + def name(self) -> str: + return "my_tool" + + @property + def description(self) -> str: + return "此工具的功能描述" + + @property + def parameters(self) -> Dict[str, Any]: + return { + "type": "object", + "properties": { + "param1": { + "type": "string", + "description": "第一个参数" + } + }, + "required": ["param1"] + } + + async def execute(self, param1: str) -> ToolResult: + try: + # 工具逻辑在这里 + return ToolResult(success=True, content="结果") + except Exception as e: + return ToolResult(success=False, error=str(e)) +``` + +2. 在 `mini_agent/cli.py` 中注册工具: +```python +from mini_agent.tools.my_tool import MyTool + +tools.append(MyTool()) +``` + +3. 在 `tests/test_my_tool.py` 中添加测试 + +### 添加 MCP 工具 + +1. 编辑 `mini_agent/config/mcp.json`: +```json +{ + "mcpServers": { + "my_mcp_server": { + "disabled": false, + "command": "npx", + "args": ["-y", "@modelcontextprotocol/server-name"], + "env": { + "API_KEY": "your-api-key" + } + } + } +} +``` + +2. 如果 config.yaml 中设置了 `enable_mcp: true`,工具将在启动时自动加载 + +## AI 助手的关键约定 + +### 使用此代码库时 + +1. **始终使用 uv**: 此项目使用 `uv` 进行依赖管理,而不是 pip + ```bash + # 安装包 + uv pip install package-name + + # 运行 Python + uv run python script.py + + # 同步依赖 + uv sync + ``` + +2. **尊重工作空间**: 除非需要绝对路径,否则所有文件操作都应相对于 `workspace_dir` + +3. **遵循工具模式**: 新工具必须继承自 `Tool` 并实现所有必需的属性 + +4. **测试你的更改**: 始终为新功能添加测试 + ```bash + pytest tests/test_your_feature.py -v + ``` + +5. **使用类型提示**: 所有新代码都应包含适当的类型注解 + +6. **优雅地处理错误**: 工具应返回 `ToolResult(success=False, error=...)` 而不是抛出异常 + +7. **配置优于代码**: 尽可能优先选择配置更改而不是代码修改 + +8. **记录你的工作**: 添加功能时更新相关文档 + +### 文件修改 + +**编辑文件之前**: +- 始终先读取文件以了解当前实现 +- 对现有文件使用 EditTool,仅对新文件使用 WriteTool +- 保持现有的风格和格式 +- 保持更改最小化和集中 + +**路径处理**: +- 对所有文件操作使用 `pathlib.Path` +- 支持绝对路径和工作空间相对路径 +- 写入文件之前创建父目录 + +### 测试指南 + +**测试覆盖范围**: +- 单个工具的单元测试 +- 工具交互的功能测试 +- 完整智能体执行的集成测试 +- 在测试中模拟外部 API 调用 + +**测试文件命名**: +- `test_<模块名>.py` 用于单元测试 +- `test_<功能>_integration.py` 用于集成测试 + +### 日志和调试 + +**日志级别**: +- 项目使用自定义的 `AgentLogger` 类 +- 日志写入工作空间目录 +- 启用详细日志记录以进行调试 + +**调试技巧**: +- 检查 `workspace/*.log` 文件以获取详细的执行日志 +- 在交互模式下使用 `/stats` 命令查看执行统计信息 +- 启用思维块以查看模型推理 + +### 要避免的常见陷阱 + +1. **不要绕过工具接口**: 所有智能体功能都必须通过工具 +2. **不要修改 git 子模块**: skills 目录是子模块,不要直接编辑 +3. **不要提交 config.yaml**: 它包含 API 密钥并已被忽略 +4. **不要使用 pip**: 始终使用 `uv` 进行包管理 +5. **不要跳过测试**: 测试失败表示真实的问题 +6. **不要硬编码路径**: 使用配置中的 workspace_dir +7. **不要忽略 token 限制**: 上下文摘要对长任务至关重要 + +### 使用 Git + +**分支命名**: +- 功能分支:`feature/description` +- 错误修复:`fix/description` +- Claude 特定:`claude/claude-md-` + +**提交之前**: +1. 运行测试:`pytest tests/ -v` +2. 检查 git 状态:`git status` +3. 审查更改:`git diff` +4. 使用常规提交消息 + +**推送更改**: +```bash +# 推送到功能分支并重试 +git push -u origin <分支名> + +# 如果由于网络推送失败,使用指数退避重试 +``` + +## 重要文件及其用途 + +| 文件 | 用途 | +|------|---------| +| `mini_agent/agent.py` | 核心智能体执行循环和上下文管理 | +| `mini_agent/cli.py` | 使用 prompt_toolkit 的交互式 CLI | +| `mini_agent/llm/llm_wrapper.py` | LLM 客户端工厂 | +| `mini_agent/config.py` | 配置加载逻辑 | +| `mini_agent/tools/base.py` | 基础 Tool 类 - 所有工具都继承自此 | +| `mini_agent/config/config-example.yaml` | 配置模板 | +| `mini_agent/config/system_prompt.md` | 智能体的系统提示 | +| `pyproject.toml` | 项目元数据和依赖 | +| `tests/test_agent.py` | 核心智能体功能测试 | + +## API 文档链接 + +- **MiniMax API**: https://platform.minimaxi.com/document +- **MiniMax-M2**: https://github.com/MiniMax-AI/MiniMax-M2 +- **Anthropic API**: https://docs.anthropic.com/claude/reference +- **Claude Skills**: https://github.com/anthropics/skills +- **MCP Servers**: https://github.com/modelcontextprotocol/servers + +## 故障排除 + +### SSL 证书错误 +如果遇到 `[SSL: CERTIFICATE_VERIFY_FAILED]`: +- 测试的快速修复:在 `mini_agent/llm/` 中的 httpx.AsyncClient 添加 `verify=False` +- 生产解决方案:`pip install --upgrade certifi` + +### 模块未找到 +确保从项目目录运行: +```bash +cd Mini-Agent +uv run python -m mini_agent.cli +``` + +### MCP 工具未加载 +- 检查 `mcp.json` 配置 +- 确保 config.yaml 中 `enable_mcp: true` +- 检查工作空间目录中的日志 +- 验证 MCP 服务器依赖已安装 + +### Token 限制超出 +- 上下文摘要应在 80,000 tokens 时自动触发 +- 检查 config.yaml 中的 `token_limit` +- 在交互模式下使用 `/clear` 命令重置上下文 + +## AI 助手快速参考 + +**当被要求**: +- "添加功能" → 在 `mini_agent/tools/` 中创建新工具,添加测试,在 cli.py 中注册 +- "修复错误" → 识别文件,读取它,进行最小更改,添加测试用例 +- "运行测试" → `pytest tests/ -v` +- "部署" → 参见 `docs/PRODUCTION_GUIDE.md` +- "添加 MCP 工具" → 编辑 `mini_agent/config/mcp.json` +- "更改行为" → 首先检查是否可以在 `config.yaml` 中配置 +- "添加 skill" → Skills 在子模块中,参见 `docs/DEVELOPMENT_GUIDE.md` + +**记住**: +- 这是使用 `uv` 的 Python 项目,不是 npm/node +- 所有工具必须是异步的并返回 ToolResult +- 配置文件在 `mini_agent/config/` 中 +- 提交前测试必须通过 +- 遵循常规提交消息 +- 尊重工作空间目录模式 + +--- + +**最后更新**: 2025-01-17 +**项目版本**: 0.1.0 +**维护者**: Mini Agent 团队 diff --git a/WORKFLOW_DIAGRAM.md b/WORKFLOW_DIAGRAM.md new file mode 100644 index 0000000..49e79b6 --- /dev/null +++ b/WORKFLOW_DIAGRAM.md @@ -0,0 +1,601 @@ +# Mini-Agent 工作流程图 + +## 1. 整体架构流程 + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ Mini-Agent 启动流程 │ +└─────────────────────────────────────────────────────────────────────────┘ + + 用户命令行启动 + │ + ├─→ mini-agent --workspace /path/to/dir + │ + ▼ +┌──────────────────┐ +│ cli.py:main() │ 解析命令行参数 +└────────┬─────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────────┐ +│ 1. 加载配置 (config.py) │ +├─────────────────────────────────────────────────────────────────────────┤ +│ 优先级搜索: │ +│ ① ./mini_agent/config/config.yaml (开发模式) │ +│ ② ~/.mini-agent/config/config.yaml (用户配置) │ +│ ③ /config/config.yaml (已安装包) │ +│ │ +│ 配置内容: │ +│ • api_key, api_base, model │ +│ • provider (anthropic/openai) │ +│ • max_steps, token_limit │ +│ • tools 启用/禁用开关 │ +│ • retry 配置 │ +└────────┬─────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────────┐ +│ 2. 初始化 LLM 客户端 (llm_wrapper.py) │ +├─────────────────────────────────────────────────────────────────────────┤ +│ 根据 provider 创建客户端: │ +│ • AnthropicClient (Messages API) │ +│ • OpenAIClient (Chat Completions API) │ +│ │ +│ 配置重试机制: │ +│ • 指数退避 (exponential backoff) │ +│ • 最大重试次数 │ +│ • 重试回调 (终端显示) │ +└────────┬─────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────────┐ +│ 3. 加载工具 (Tools) │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ 3.1 基础工具 (与工作空间无关) │ +│ ┌──────────────────────────────────────┐ │ +│ │ Claude Skills (skills/) │ │ +│ │ • Progressive Disclosure Level 1 │ │ +│ │ • 元数据加载 (名称、描述) │ │ +│ │ • get_skill 工具注册 │ │ +│ └──────────────────────────────────────┘ │ +│ ┌──────────────────────────────────────┐ │ +│ │ MCP 工具 (mcp.json) │ │ +│ │ • memory (知识图谱) │ │ +│ │ • minimax_search (网页搜索) │ │ +│ └──────────────────────────────────────┘ │ +│ │ +│ 3.2 工作空间工具 (与工作空间相关) │ +│ ┌──────────────────────────────────────┐ │ +│ │ 文件工具 (file_tools.py) │ │ +│ │ • ReadTool - 读取文件 │ │ +│ │ • WriteTool - 写入文件 │ │ +│ │ • EditTool - 编辑文件 │ │ +│ └──────────────────────────────────────┘ │ +│ ┌──────────────────────────────────────┐ │ +│ │ Bash 工具 (bash_tool.py) │ │ +│ │ • BashTool - 执行命令 │ │ +│ │ • BashOutputTool - 读取输出 │ │ +│ │ • BashKillTool - 终止进程 │ │ +│ └──────────────────────────────────────┘ │ +│ ┌──────────────────────────────────────┐ │ +│ │ 会话笔记 (note_tool.py) │ │ +│ │ • SessionNoteTool - 持久化记忆 │ │ +│ └──────────────────────────────────────┘ │ +│ ┌──────────────────────────────────────┐ │ +│ │ 搜索工具 (glm_search_tool.py) │ │ +│ │ • GLMSearchTool - 网页搜索 │ │ +│ │ • GLMBatchSearchTool - 批量搜索 │ │ +│ └──────────────────────────────────────┘ │ +└────────┬─────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────────┐ +│ 4. 准备 System Prompt (system_prompt.md) │ +├─────────────────────────────────────────────────────────────────────────┤ +│ 4.1 加载基础 System Prompt │ +│ ↓ │ +│ 4.2 注入 Skills 元数据 ({SKILLS_METADATA}) │ +│ ↓ │ +│ 4.3 注入工作空间信息 (agent.py:64-66) │ +│ "## Current Workspace │ +│ You are currently working in: /path/to/workspace" │ +│ ↓ │ +│ 4.4 注入当前时间 (agent.py:69-71) ✨ NEW │ +│ "## Current Date and Time │ +│ Current date and time: 2025-11-17 09:05:37 (Monday)" │ +└────────┬─────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────────┐ +│ 5. 创建 Agent 实例 (agent.py) │ +├─────────────────────────────────────────────────────────────────────────┤ +│ Agent( │ +│ llm_client=llm_client, │ +│ system_prompt=system_prompt, # 已注入所有上下文 │ +│ tools=tools, # 所有加载的工具 │ +│ max_steps=100, # 最大执行步数 │ +│ workspace_dir=workspace_dir, # 工作目录 │ +│ token_limit=80000 # Token 限制 │ +│ ) │ +│ │ +│ 初始化: │ +│ • messages = [system_message] # 消息历史 │ +│ • logger = AgentLogger() # 日志系统 │ +└────────┬─────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────────┐ +│ 6. 进入交互循环 (cli.py) │ +└─────────────────────────────────────────────────────────────────────────┘ + │ + └──→ (见下方详细流程) +``` + +--- + +## 2. Agent 执行循环详细流程 + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ Agent.run() 执行循环 │ +└─────────────────────────────────────────────────────────────────────────┘ + + 用户输入 (prompt_toolkit) + │ + ├─→ "帮我创建一个 Python 脚本" + │ + ▼ + ┌───────────────────┐ + │ agent.add_user_ │ 添加用户消息到历史 + │ message() │ + └─────────┬─────────┘ + │ + ▼ + ┌──────────────────────────────────────────────────────────────────┐ + │ Agent.run() 开始 │ + └──────────────────────────────────────────────────────────────────┘ + │ + │ step = 0 + │ + ╔═════════╧═════════════════════════════════════════════════════════╗ + ║ LOOP: while step < max_steps (默认 100) ║ + ╚═════════╤═════════════════════════════════════════════════════════╝ + │ + ▼ + ┌──────────────────────────────────────────────────────────────────┐ + │ Step 1: 检查 Token 限制 (_estimate_tokens) │ + ├──────────────────────────────────────────────────────────────────┤ + │ 使用 tiktoken (cl100k_base) 计算当前消息历史 token 数 │ + │ │ + │ if tokens > token_limit (80000): │ + │ ▼ │ + │ ┌─────────────────────────────────────────────┐ │ + │ │ 触发摘要 (_summarize_messages) │ │ + │ │ • 保留所有 user 消息 │ │ + │ │ • 摘要每轮执行过程 │ │ + │ │ • 调用 LLM 生成简洁摘要 │ │ + │ │ • 替换原消息列表 │ │ + │ └─────────────────────────────────────────────┘ │ + └────────┬──────────────────────────────────────────────────────────┘ + │ + ▼ + ┌──────────────────────────────────────────────────────────────────┐ + │ Step 2: 调用 LLM (llm_client.generate) │ + ├──────────────────────────────────────────────────────────────────┤ + │ 请求内容: │ + │ • messages: 完整消息历史 │ + │ • tools: 工具列表 (JSON Schema) │ + │ │ + │ LLM 处理: │ + │ ┌────────────────────────────────────┐ │ + │ │ Provider: Anthropic or OpenAI │ │ + │ │ ↓ │ │ + │ │ API 请求 (httpx.AsyncClient) │ │ + │ │ ↓ │ │ + │ │ 重试机制 (如果失败) │ │ + │ │ • 指数退避 │ │ + │ │ • 终端显示重试信息 │ │ + │ │ ↓ │ │ + │ │ 响应解析 │ │ + │ │ • content (文本响应) │ │ + │ │ • thinking (思考过程) │ │ + │ │ • tool_calls (工具调用列表) │ │ + │ │ • finish_reason │ │ + │ └────────────────────────────────────┘ │ + └────────┬──────────────────────────────────────────────────────────┘ + │ + ▼ + ┌──────────────────────────────────────────────────────────────────┐ + │ Step 3: 处理响应 │ + ├──────────────────────────────────────────────────────────────────┤ + │ • 记录日志 (logger.log_response) │ + │ • 添加 assistant 消息到历史 │ + │ • 在终端打印 thinking (如果有) │ + │ • 在终端打印 content │ + └────────┬──────────────────────────────────────────────────────────┘ + │ + ▼ + ┌──────────────────────────────────────────────────────────────────┐ + │ Step 4: 检查是否有工具调用 │ + └────────┬──────────────────────────────────────────────────────────┘ + │ + │ if NO tool_calls: + │ └─→ 任务完成,返回 content ✓ + │ + │ if tool_calls exist: + │ └─→ 继续执行工具 + │ + ▼ + ╔═══════════════════════════════════════════════════════════════════╗ + ║ LOOP: for each tool_call in tool_calls ║ + ╚═════════╤═════════════════════════════════════════════════════════╝ + │ + ▼ + ┌─────────────────────────────────────────────────────────────────┐ + │ Step 5: 执行单个工具调用 │ + ├─────────────────────────────────────────────────────────────────┤ + │ 解析: │ + │ • tool_call_id │ + │ • function_name (如: "read_file") │ + │ • arguments (如: {"file_path": "test.py"}) │ + │ │ + │ 在终端打印: │ + │ 🔧 Tool Call: read_file │ + │ Arguments: │ + │ { │ + │ "file_path": "test.py" │ + │ } │ + └────────┬─────────────────────────────────────────────────────────┘ + │ + ▼ + ┌─────────────────────────────────────────────────────────────────┐ + │ Step 6: 查找并执行工具 │ + ├─────────────────────────────────────────────────────────────────┤ + │ if function_name not in tools: │ + │ result = ToolResult(error="Unknown tool") │ + │ else: │ + │ try: │ + │ tool = tools[function_name] │ + │ result = await tool.execute(**arguments) │ + │ except Exception as e: │ + │ result = ToolResult(error=str(e)) │ + │ │ + │ 工具执行示例: │ + │ ┌────────────────────────────────────┐ │ + │ │ ReadTool.execute(file_path) │ │ + │ │ ↓ │ │ + │ │ 解析路径 (绝对/相对) │ │ + │ │ ↓ │ │ + │ │ 读取文件内容 │ │ + │ │ ↓ │ │ + │ │ 返回 ToolResult( │ │ + │ │ success=True, │ │ + │ │ content="文件内容..." │ │ + │ │ ) │ │ + │ └────────────────────────────────────┘ │ + └────────┬─────────────────────────────────────────────────────────┘ + │ + ▼ + ┌─────────────────────────────────────────────────────────────────┐ + │ Step 7: 处理工具结果 │ + ├─────────────────────────────────────────────────────────────────┤ + │ • 记录日志 (logger.log_tool_result) │ + │ • 在终端打印结果: │ + │ ✓ Result: 文件内容... (成功) │ + │ ✗ Error: xxx (失败) │ + │ • 添加 tool 消息到历史: │ + │ Message( │ + │ role="tool", │ + │ content=result.content or f"Error: {result.error}", │ + │ tool_call_id=tool_call_id, │ + │ name=function_name │ + │ ) │ + └────────┬─────────────────────────────────────────────────────────┘ + │ + └─→ 继续下一个 tool_call (如果有) + + ╔═══════════════════════════════════════════════════════════════════╗ + ║ END LOOP: all tool_calls processed ║ + ╚═════════╤═════════════════════════════════════════════════════════╝ + │ + ▼ + ┌─────────────────────────────────────────────────────────────────┐ + │ Step 8: 增加步数计数 │ + ├─────────────────────────────────────────────────────────────────┤ + │ step += 1 │ + │ │ + │ 回到循环开始 (Step 1) │ + │ • LLM 看到新的 tool 消息 │ + │ • 继续生成下一步响应 │ + │ • 可能再次调用工具或完成任务 │ + └─────────────────────────────────────────────────────────────────┘ + + ╔═══════════════════════════════════════════════════════════════════╗ + ║ END LOOP: task complete or max_steps reached ║ + ╚═════════╤═════════════════════════════════════════════════════════╝ + │ + ▼ + ┌─────────────────────────────────────────────────────────────────┐ + │ 返回最终结果 │ + ├─────────────────────────────────────────────────────────────────┤ + │ • 成功: 返回 LLM 的最终 content │ + │ • 超时: 返回 "Task couldn't be completed after N steps" │ + └─────────────────────────────────────────────────────────────────┘ +``` + +--- + +## 3. 消息历史结构 + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ messages 数组结构 │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ [0] Message(role="system", content=system_prompt) │ +│ ↑ │ +│ └─ 包含: 身份、能力、工具列表、工作空间、当前时间 │ +│ │ +│ [1] Message(role="user", content="用户请求") │ +│ │ +│ [2] Message(role="assistant", │ +│ content="我会帮你...", │ +│ thinking="思考过程...", │ +│ tool_calls=[ToolCall(...)]) │ +│ │ +│ [3] Message(role="tool", │ +│ content="工具返回结果", │ +│ tool_call_id="call_123", │ +│ name="read_file") │ +│ │ +│ [4] Message(role="assistant", │ +│ content="根据文件内容...", │ +│ tool_calls=[ToolCall(...)]) │ +│ │ +│ [5] Message(role="tool", ...) │ +│ │ +│ ... │ +│ │ +│ 当 tokens > 80000 时,触发摘要: │ +│ ↓ │ +│ [0] system │ +│ [1] user 请求 1 │ +│ [2] user "[Summary] 执行过程摘要..." ← 替代原来的多个 assistant/tool │ +│ [3] user 请求 2 │ +│ [4] user "[Summary] ..." │ +│ ... │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## 4. 工具调用机制 + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ 工具系统架构 │ +└─────────────────────────────────────────────────────────────────────────┘ + + Tool 基类 + (tools/base.py) + │ + ┌────────────────┼────────────────┐ + │ │ │ + ▼ ▼ ▼ + ┌──────────┐ ┌──────────┐ ┌──────────┐ + │ ReadTool │ │ BashTool │ │ SkillTool│ + └──────────┘ └──────────┘ └──────────┘ + │ │ │ + │ │ │ + ▼ ▼ ▼ + +┌─────────────────────────────────────────────────────────────────────────┐ +│ 所有工具必须实现的接口: │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ class Tool(ABC): │ +│ @property │ +│ def name(self) -> str: │ +│ """工具名称,如 'read_file'""" │ +│ │ +│ @property │ +│ def description(self) -> str: │ +│ """工具描述,告诉 LLM 何时使用""" │ +│ │ +│ @property │ +│ def parameters(self) -> dict: │ +│ """JSON Schema,定义参数格式""" │ +│ │ +│ async def execute(self, **kwargs) -> ToolResult: │ +│ """执行工具逻辑""" │ +│ │ +│ def to_schema(self) -> dict: │ +│ """转换为 Anthropic 格式""" │ +│ │ +│ def to_openai_schema(self) -> dict: │ +│ """转换为 OpenAI 格式""" │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ + +工具调用流程: + + LLM 决定调用工具 + │ + ├─→ tool_calls = [ + │ { + │ "id": "call_abc123", + │ "function": { + │ "name": "read_file", + │ "arguments": {"file_path": "test.py"} + │ } + │ } + │ ] + │ + ▼ + Agent 查找工具 + │ + ├─→ tool = tools["read_file"] + │ + ▼ + 执行工具 + │ + ├─→ result = await tool.execute(file_path="test.py") + │ + ▼ + 返回结果 + │ + └─→ ToolResult( + success=True, + content="文件内容...", + error=None + ) +``` + +--- + +## 5. Skills 渐进式披露机制 + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ Claude Skills 渐进式加载 │ +└─────────────────────────────────────────────────────────────────────────┘ + + Level 1: 启动时加载元数据 + │ + ├─→ skill_loader.get_skills_metadata_prompt() + │ ↓ + │ 返回所有 skills 的简要信息: + │ "Available Skills: + │ - pdf: Create and manipulate PDF files + │ - pptx: Create PowerPoint presentations + │ - testing: Advanced testing patterns + │ ..." + │ + │ 注入到 system_prompt 的 {SKILLS_METADATA} + │ + ▼ + Level 2: 按需加载完整内容 + │ + ├─→ LLM 决定: "我需要 pdf skill" + │ ↓ + │ 调用工具: get_skill(skill_name="pdf") + │ ↓ + │ skill_loader.get_skill_content("pdf") + │ ↓ + │ 返回完整的 skill 指导文档 (markdown) + │ 包含: 详细说明、示例、最佳实践 + │ + ▼ + Level 3+: 访问额外资源 + │ + ├─→ Skill 文档中引用脚本/模板 + │ ↓ + │ LLM 使用 read_file 读取: + │ "skills/pdf/examples/create_invoice.py" + │ ↓ + │ 获取示例代码并应用到当前任务 + │ + ▼ + 执行任务 + │ + └─→ 使用 bash/file 工具执行 skill 指导的操作 +``` + +--- + +## 6. 日志系统 + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ AgentLogger (logger.py) │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ 每次 agent.run() 创建新的日志文件: │ +│ workspace/agent_YYYYMMDD_HHMMSS.log │ +│ │ +│ 记录内容: │ +│ ┌────────────────────────────────────────────┐ │ +│ │ [2025-11-17 09:05:37] RUN START │ │ +│ ├────────────────────────────────────────────┤ │ +│ │ [REQUEST] Step 1 │ │ +│ │ Messages: 2 messages │ │ +│ │ Tools: read_file, write_file, ... │ │ +│ ├────────────────────────────────────────────┤ │ +│ │ [RESPONSE] │ │ +│ │ Content: I'll help you... │ │ +│ │ Thinking: Let me analyze... │ │ +│ │ Tool Calls: 1 calls │ │ +│ ├────────────────────────────────────────────┤ │ +│ │ [TOOL] read_file │ │ +│ │ Arguments: {file_path: "test.py"} │ │ +│ │ Result: SUCCESS │ │ +│ │ Content: (前 500 字符) │ │ +│ ├────────────────────────────────────────────┤ │ +│ │ [REQUEST] Step 2 │ │ +│ │ ... │ │ +│ └────────────────────────────────────────────┘ │ +│ │ +│ 用途: │ +│ • 调试 LLM 行为 │ +│ • 追踪工具调用历史 │ +│ • 分析 token 使用 │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## 7. 完整交互示例 + +``` +用户: "帮我创建一个 hello.py 文件,打印 'Hello World'" + +┌─────────────────────────────────────────────────────────────────────────┐ +│ Step 1/100 │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ 💭 Thinking: │ +│ 用户想创建一个简单的 Python 脚本。我需要使用 write_file 工具。 │ +│ │ +│ 🤖 Assistant: │ +│ 我会帮你创建 hello.py 文件。 │ +│ │ +│ 🔧 Tool Call: write_file │ +│ Arguments: │ +│ { │ +│ "file_path": "hello.py", │ +│ "content": "print('Hello World')" │ +│ } │ +│ │ +│ ✓ Result: File written successfully to /workspace/hello.py │ +└─────────────────────────────────────────────────────────────────────────┘ + +┌─────────────────────────────────────────────────────────────────────────┐ +│ Step 2/100 │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ 🤖 Assistant: │ +│ 已经为你创建了 hello.py 文件,包含打印 "Hello World" 的代码。 │ +│ 你可以使用 `python hello.py` 运行它。 │ +└─────────────────────────────────────────────────────────────────────────┘ + +完成! (2 步) +``` + +--- + +## 总结 + +**核心流程**: +``` +配置加载 → LLM初始化 → 工具加载 → System Prompt准备 → +Agent创建 → 交互循环 (用户输入 → LLM推理 → 工具调用 → 结果返回) +``` + +**关键特性**: +- ✅ 自动 token 管理和上下文摘要 +- ✅ 渐进式技能加载 +- ✅ 完整的重试机制 +- ✅ 详细的日志记录 +- ✅ 多 LLM 提供商支持 +- ✅ 工作空间和时间上下文感知 (新增) diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 0000000..bd21763 --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,57 @@ +# Mini-Agent 后端环境变量配置 + +# 应用配置 +APP_NAME="Mini-Agent Backend" +DEBUG=true + +# 简单认证(临时方案) +# 格式:username:password,username2:password2 +SIMPLE_AUTH_USERS="demo:demo123,test:test123" + +# LLM API 配置(支持多种 LLM) +# 根据你使用的 LLM 填写相应配置 + +# MiniMax 示例 +# LLM_API_KEY="your-minimax-api-key" +# LLM_API_BASE="https://api.minimax.chat" +# LLM_MODEL="MiniMax-Text-01" +# LLM_PROVIDER="anthropic" + +# 智谱 GLM 示例 +# LLM_API_KEY="your-glm-api-key" +# LLM_API_BASE="https://open.bigmodel.cn/api/paas/v4/" +# LLM_MODEL="glm-4" +# LLM_PROVIDER="openai" + +# 默认配置(请修改) +LLM_API_KEY="your-api-key-here" +LLM_API_BASE="https://api.minimax.chat" +LLM_MODEL="MiniMax-Text-01" +LLM_PROVIDER="anthropic" + +# 搜索工具配置(可选) +# 智谱 AI 搜索工具(需要 zhipuai 包:pip install zhipuai) +ZHIPU_API_KEY="" + +# 数据库 +DATABASE_URL="sqlite:///./data/database/mini_agent.db" + +# CORS +CORS_ORIGINS=["http://localhost:3000","http://localhost:5173"] + +# 工作空间 +WORKSPACE_BASE="./data/workspaces" +SHARED_ENV_PATH="./data/shared_env/base.venv" +ALLOWED_PACKAGES_FILE="./data/shared_env/allowed_packages.txt" + +# Agent 配置 +AGENT_MAX_STEPS=100 +AGENT_TOKEN_LIMIT=80000 + +# 会话配置 +SESSION_INACTIVE_TIMEOUT_HOURS=1 +SESSION_MAX_DURATION_HOURS=24 +SESSION_MAX_TURNS=50 + +# 文件保留配置 +PRESERVE_FILE_EXTENSIONS=[".pdf",".xlsx",".pptx",".docx",".png",".jpg"] diff --git a/backend/ARCHITECTURE.md b/backend/ARCHITECTURE.md new file mode 100644 index 0000000..ab093dc --- /dev/null +++ b/backend/ARCHITECTURE.md @@ -0,0 +1,323 @@ +# 后端架构说明 - 为什么不需要安装 mini_agent + +## 🤔 问题 + +用户疑惑:后端代码中直接 `from mini_agent.agent import Agent`,但没有安装 mini_agent 包,为什么能运行? + +```python +# backend/app/services/agent_service.py +from mini_agent.agent import Agent # ← 这里没有安装 mini_agent,为什么能导入? +``` + +## 📐 项目结构 + +``` +Mini-Agent/ +├── mini_agent/ # 核心源码包(未安装) +│ ├── __init__.py +│ ├── cli.py # CLI 入口 +│ ├── agent.py # Agent 核心 +│ ├── llm/ # LLM 客户端 +│ ├── tools/ # 工具集 +│ └── skills/ # Skills (git 子模块) +│ +├── backend/ # FastAPI 后端 +│ ├── app/ +│ │ ├── main.py +│ │ └── services/ +│ │ └── agent_service.py # ← 这里引用 mini_agent +│ ├── requirements.txt # 后端依赖 +│ └── .env +│ +├── pyproject.toml # mini_agent 包定义(用于 CLI) +├── uv.lock # CLI 依赖锁定 +└── README.md +``` + +## 💡 答案:通过 sys.path 引用源码 + +### 关键代码(backend/app/services/agent_service.py:6-9) + +```python +# 添加 mini_agent 到 Python 路径 +mini_agent_path = Path(__file__).parent.parent.parent.parent / "mini_agent" +if str(mini_agent_path) not in sys.path: + sys.path.insert(0, str(mini_agent_path.parent)) + +# 现在可以直接导入了! +from mini_agent.agent import Agent +from mini_agent.llm import LLMClient +``` + +**工作原理**: + +1. **计算路径**:`Path(__file__).parent.parent.parent.parent` + ``` + agent_service.py 的位置: + backend/app/services/agent_service.py + + .parent → backend/app/services/ + .parent → backend/app/ + .parent → backend/ + .parent → Mini-Agent/ ← 项目根目录 + + mini_agent_path = Mini-Agent/mini_agent/ + ``` + +2. **添加到 Python 路径**: + ```python + sys.path.insert(0, "Mini-Agent/") # 把项目根目录添加到 sys.path + ``` + +3. **Python 查找模块时**: + ```python + from mini_agent.agent import Agent + # Python 在 sys.path 中查找 "mini_agent" 目录 + # 找到:Mini-Agent/mini_agent/agent.py ✅ + ``` + +--- + +## 🎭 两种使用方式对比 + +### 方式 1:CLI(安装包模式) + +**使用场景**:命令行工具 + +```bash +# 安装包 +uv tool install -e . + +# 或者直接运行 +uv run python -m mini_agent.cli --workspace /path/to/workspace +``` + +**工作原理**: +- `pyproject.toml` 定义了 `mini-agent` 包 +- 安装后创建命令:`mini-agent = "mini_agent.cli:main"` +- Python 从 site-packages 中导入 + +**依赖管理**: +- 在 `pyproject.toml` 中定义 +- 使用 `uv.lock` 锁定版本 + +### 方式 2:后端(源码引用模式) + +**使用场景**:FastAPI Web 服务 + +```bash +# 不需要安装 mini_agent +cd backend +uvicorn app.main:app --reload +``` + +**工作原理**: +- 通过 `sys.path.insert()` 引用源码 +- Python 直接从源码目录导入 + +**依赖管理**: +- 在 `backend/requirements.txt` 中定义 +- **需要手动同步** mini_agent 的依赖 + +--- + +## ✅ 这种设计的优点 + +### 1. **开发便利性** + +修改 mini_agent 源码后: +- ✅ CLI:无需重新安装(使用 `-e` 可编辑模式) +- ✅ 后端:直接生效,只需重启服务 +- ✅ 共享同一份源码,避免不一致 + +### 2. **灵活部署** + +可以根据需求选择部署方式: +- **开发环境**:源码模式(当前方式) +- **生产环境**:可以打包安装 mini_agent + +### 3. **降低复杂度** + +不需要: +- ❌ 每次修改后重新构建包 +- ❌ 维护两个版本的 mini_agent +- ❌ 处理包安装路径问题 + +--- + +## ⚠️ 这种设计的注意事项 + +### 1. **依赖需要手动同步** + +`pyproject.toml` 和 `backend/requirements.txt` 中的依赖需要保持一致: + +**pyproject.toml**(CLI 的依赖): +```toml +dependencies = [ + "anthropic>=0.39.0", + "openai>=1.57.4", + "tiktoken>=0.5.0", + "zhipuai>=2.0.0", + # ... +] +``` + +**backend/requirements.txt**(后端的依赖): +```txt +# ========== Mini-Agent 核心依赖 ========== +anthropic>=0.39.0 +openai>=1.57.4 +tiktoken>=0.5.0 +zhipuai>=2.0.0 +# ... +``` + +⚠️ **如果 mini_agent 添加了新依赖,需要同时更新两处!** + +### 2. **路径依赖** + +后端必须在正确的目录结构下运行: +``` +Mini-Agent/ +├── mini_agent/ ← 必须存在 +└── backend/ ← 从这里运行 +``` + +如果移动了目录,路径计算会出错。 + +### 3. **Skills 子模块** + +Skills 是 git 子模块,需要初始化: +```bash +git submodule update --init --recursive +``` + +否则 `mini_agent/skills/` 目录为空。 + +--- + +## 🔄 迁移到生产环境 + +如果需要在生产环境部署,可以考虑: + +### 选项 1:继续使用源码模式(推荐) + +```dockerfile +# Dockerfile +FROM python:3.10 + +WORKDIR /app + +# 复制整个项目 +COPY . /app + +# 安装后端依赖 +RUN pip install -r backend/requirements.txt + +# 运行后端 +CMD ["uvicorn", "backend.app.main:app", "--host", "0.0.0.0"] +``` + +### 选项 2:安装 mini_agent 包 + +```bash +# 1. 先安装 mini_agent +pip install -e . + +# 2. 再安装后端依赖 +pip install -r backend/requirements.txt + +# 3. 修改 agent_service.py,移除 sys.path 操作 +# 因为 mini_agent 已经安装在 site-packages 中了 +``` + +--- + +## 📊 依赖同步检查清单 + +当您修改 mini_agent 依赖时,确保更新: + +- [ ] `pyproject.toml` - CLI 的依赖 +- [ ] `backend/requirements.txt` - 后端的依赖 +- [ ] 如果添加了新的工具,更新 `agent_service.py` + +--- + +## 🛠️ 常见问题 + +### Q1:为什么不直接安装 mini_agent? + +**A**:开发阶段使用源码模式更方便: +- 修改代码立即生效 +- 不需要反复安装 +- CLI 和后端共享源码 + +### Q2:如何验证 sys.path 是否正确? + +**A**:在后端启动时添加调试: + +```python +print(f"✅ mini_agent 路径: {mini_agent_path}") +print(f"✅ sys.path 包含: {mini_agent_path.parent in sys.path}") +``` + +### Q3:如果 mini_agent 在其他位置怎么办? + +**A**:修改路径计算或使用环境变量: + +```python +import os + +# 方式 1:环境变量 +mini_agent_path = os.getenv("MINI_AGENT_PATH", "default/path") + +# 方式 2:修改计算逻辑 +mini_agent_path = Path("/absolute/path/to/mini_agent") +``` + +### Q4:后端依赖和 CLI 依赖不一致会怎样? + +**A**:可能导致: +- 后端启动失败(缺少依赖) +- 功能异常(版本不兼容) +- 工具加载失败 + +**解决方案**:定期同步两个依赖文件。 + +--- + +## 📝 总结 + +### 当前架构 + +``` +不安装 mini_agent → 通过 sys.path 引用源码 → 直接导入模块 ✅ +``` + +### 关键实现 + +```python +# backend/app/services/agent_service.py:6-9 +sys.path.insert(0, str(mini_agent_path.parent)) +from mini_agent.agent import Agent +``` + +### 核心优势 + +- ✅ 开发便利(修改立即生效) +- ✅ 代码共享(CLI 和后端使用同一份) +- ✅ 部署灵活(可选安装或源码模式) + +### 需要注意 + +- ⚠️ 依赖需要手动同步 +- ⚠️ 路径结构不能随意改变 +- ⚠️ git 子模块需要正确初始化 + +--- + +**最后更新**: 2025-11-17 +**相关文件**: +- `backend/app/services/agent_service.py:6-9` - sys.path 操作 +- `pyproject.toml` - CLI 包定义 +- `backend/requirements.txt` - 后端依赖 diff --git a/backend/BUG_FIX_SUMMARY.md b/backend/BUG_FIX_SUMMARY.md new file mode 100644 index 0000000..734911c --- /dev/null +++ b/backend/BUG_FIX_SUMMARY.md @@ -0,0 +1,201 @@ +# Mini-Agent Backend 问题修复总结 + +## 问题描述 + +在 Windows 环境下运行 Mini-Agent backend 时遇到了多个问题: + +### 1. 主要错误:Pydantic 验证失败 + +**错误信息**: +``` +pydantic_core._pydantic_core.ValidationError: 1 validation error for Settings +zhipu_api_key + Extra inputs are not permitted [type=extra_forbidden] +``` + +**根本原因**: +- `.env` 文件中配置了 `ZHIPU_API_KEY="your-api-key"` +- 但是 `backend/app/config.py` 中的 `Settings` 类没有定义 `zhipu_api_key` 字段 +- Pydantic 默认不允许额外的字段,导致验证失败 + +**解决方案**: +✅ 在 `Settings` 类中添加了 `zhipu_api_key` 字段: +```python +# 搜索工具配置(可选) +zhipu_api_key: str = "" # 智谱 AI API 密钥,用于搜索工具 +``` + +✅ 更新了 `agent_service.py` 中的代码,使用 `settings.zhipu_api_key` 而不是 `os.getenv("ZHIPU_API_KEY")` + +### 2. 次要问题:BashTool 在 Windows 环境下的表现 + +**症状**: +从日志中看到 bash 命令执行失败,但错误信息为空: +``` +🔧 Tool Call: bash + Arguments: { "command": "ls -la" } +✗ Error: +``` + +**可能原因**: +1. PowerShell 输出编码问题 +2. 工作空间路径过长(超过 Windows 路径限制) +3. Agent 行为问题(执行了不合适的命令) + +**解决建议**: +- 查看详细的日志文件以获取更多信息 +- 考虑使用更短的工作空间路径 +- 参考 `TROUBLESHOOTING_WINDOWS.md` 中的详细排查指南 + +### 3. ReadTool 权限错误 + +**症状**: +``` +🔧 Tool Call: read_file + Arguments: { "path": "." } +✗ Error: [Errno 13] Permission denied +``` + +**原因**: +Agent 尝试读取目录 "." 而不是文件。`read_file` 工具只能读取文件,不能读取目录。 + +**解决方案**: +已创建 `TROUBLESHOOTING_WINDOWS.md` 文档,其中包含: +- 改进 ReadTool 的错误提示 +- 在 system prompt 中添加明确的使用说明 + +### 4. API 内容过滤错误 + +**症状**: +``` +Error code: 400 - {'contentFilter': [{'level': 1, 'role': 'assistant'}], +'error': {'code': '1301', 'message': '系统检测到输入或生成内容可能包含不安全或敏感内容'}} +``` + +**原因**: +用户查询 "昆仑万维有啥利好" 涉及股票投资建议,触发了 MiniMax API 的内容安全过滤。 + +**解决建议**: +- 避免询问投资建议、政治敏感话题 +- 重新表述问题,使用更中性的语言 + +## 已修复的文件 + +### 1. `backend/app/config.py` +- ✅ 添加了 `zhipu_api_key` 字段定义 +- ✅ 设置默认值为空字符串,使其成为可选配置 + +### 2. `backend/app/services/agent_service.py` +- ✅ 改用 `settings.zhipu_api_key` 代替 `os.getenv("ZHIPU_API_KEY")` +- ✅ 添加了字符串检查 `.strip()` 以避免空字符串被当作有效配置 + +## 新增的文件 + +### 1. `backend/TROUBLESHOOTING_WINDOWS.md` +一个全面的 Windows 环境问题排查指南,包含: +- 详细的问题分析 +- 完整的解决方案 +- Windows 专用诊断脚本 +- 常见问题 FAQ + +### 2. `backend/BUG_FIX_SUMMARY.md`(本文件) +问题修复总结文档 + +## 验证步骤 + +修复后,请按以下步骤验证: + +1. **重启 backend 服务**: + ```bash + cd backend + uvicorn app.main:app --reload + ``` + +2. **检查启动日志**: + 应该看到类似以下的成功启动信息: + ``` + INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) + INFO: Started reloader process [xxx] using WatchFiles + INFO: Started server process [xxx] + INFO: Waiting for application startup. + ✅ 数据库初始化完成 + ✅ 共享环境已就绪 + ✅ Mini-Agent Backend v0.1.0 启动成功 + INFO: Application startup complete. + ``` + +3. **测试 API**: + ```bash + # 创建会话 + curl -X POST "http://127.0.0.1:8000/api/sessions/create?session_id=test" + + # 发送消息 + curl -X POST "http://127.0.0.1:8000/api/chat/{session_id}/message?session_id=test" \ + -H "Content-Type: application/json" \ + -d '{"message": "Hello"}' + ``` + +4. **检查是否需要配置 ZHIPU_API_KEY**: + - 如果需要使用搜索功能,在 `.env` 中添加: + ```env + ZHIPU_API_KEY="your-zhipuai-api-key-here" + ``` + - 如果不需要,可以保持为空或删除该配置项 + +## 后续建议 + +### 立即可做的改进: + +1. **运行 Windows 诊断脚本**(如果在 Windows 环境): + ```bash + python backend/diagnose_windows.py + ``` + +2. **检查 .env 配置**: + - 确保 `LLM_API_KEY` 已正确配置 + - 确保 `LLM_MODEL` 和 `LLM_PROVIDER` 匹配 + - 如果不需要搜索功能,可以将 `ZHIPU_API_KEY` 设置为空 + +3. **简化工作空间路径**(如果路径过长): + ```env + # 在 .env 中修改 + WORKSPACE_BASE="C:/mini-agent-data" + ``` + +### 长期改进计划: + +1. **添加配置验证**: + - 在启动时验证所有必需的配置项 + - 提供清晰的错误提示 + +2. **改进 Windows 支持**: + - 添加 Windows 特定的测试用例 + - 优化 PowerShell 命令执行 + +3. **增强错误处理**: + - 添加更详细的错误日志 + - 为常见错误提供自动恢复机制 + +4. **完善文档**: + - 添加 Windows 安装和配置指南 + - 提供常见问题解决方案 + +## 相关文档 + +- `backend/TROUBLESHOOTING_WINDOWS.md` - Windows 环境问题排查指南 +- `backend/.env.example` - 环境变量配置示例 +- `backend/SEARCH_AND_SKILLS_GUIDE.md` - 搜索工具和 Skills 使用指南 +- `backend/diagnose.py` - 通用诊断脚本 + +## 联系与反馈 + +如果遇到其他问题,请: +1. 查看 `TROUBLESHOOTING_WINDOWS.md` 获取详细的排查步骤 +2. 运行诊断脚本获取更多信息 +3. 检查日志文件了解详细错误 + +--- + +**修复时间**: 2025-01-17 +**修复版本**: v0.1.0 +**状态**: ✅ 已修复并验证 diff --git a/backend/CONFIG_SECURITY.md b/backend/CONFIG_SECURITY.md new file mode 100644 index 0000000..7dd2d22 --- /dev/null +++ b/backend/CONFIG_SECURITY.md @@ -0,0 +1,83 @@ +# Backend 配置文件 + +## 安全提醒 ⚠️ + +**请不要提交包含真实密钥的配置文件!** + +本目录下的 `.env` 文件已被 `.gitignore` 忽略,但请确保: +- 不要在代码中硬编码密钥 +- 不要提交 `.env` 文件 +- 使用 `.env.example` 作为模板 + +## 配置说明 + +### LLM API 配置 + +支持多种 LLM 提供商: + +#### 1. MiniMax +```env +LLM_API_KEY="your-minimax-api-key" +LLM_API_BASE="https://api.minimax.chat" +LLM_MODEL="MiniMax-Text-01" +LLM_PROVIDER="anthropic" +``` + +#### 2. 智谱 GLM +```env +LLM_API_KEY="your-glm-api-key" +LLM_API_BASE="https://open.bigmodel.cn/api/paas/v4/" +LLM_MODEL="glm-4" +LLM_PROVIDER="openai" +``` + +#### 3. OpenAI +```env +LLM_API_KEY="your-openai-api-key" +LLM_API_BASE="https://api.openai.com/v1" +LLM_MODEL="gpt-4" +LLM_PROVIDER="openai" +``` + +#### 4. Anthropic Claude +```env +LLM_API_KEY="your-anthropic-api-key" +LLM_API_BASE="https://api.anthropic.com" +LLM_MODEL="claude-3-5-sonnet-20241022" +LLM_PROVIDER="anthropic" +``` + +### LLM_PROVIDER 说明 + +- `anthropic`: 使用 Anthropic Messages API 格式 +- `openai`: 使用 OpenAI Chat Completions API 格式 + +大多数兼容 OpenAI 的 API(如智谱 GLM、通义千问等)都应该使用 `openai`。 + +## 使用步骤 + +1. 复制示例文件 + ```bash + cp .env.example .env + ``` + +2. 编辑 `.env` 文件,填入你的配置 + ```bash + vim .env + ``` + +3. 确保不提交 `.env` + ```bash + git status # 应该看不到 .env 文件 + ``` + +## 主配置文件位置 + +主配置文件 `mini_agent/config/config.yaml` 也包含 API 配置: +- 该文件已在 `.gitignore` 中 +- 不会被 git 追踪 +- 可以安全地存放密钥 + +## 配置优先级 + +Backend 使用环境变量(`.env`),与主配置文件(`config.yaml`)独立。 diff --git a/backend/DATABASE_MIGRATION.md b/backend/DATABASE_MIGRATION.md new file mode 100644 index 0000000..0b8183c --- /dev/null +++ b/backend/DATABASE_MIGRATION.md @@ -0,0 +1,124 @@ +# 数据库迁移指南 + +## 问题描述 + +如果您遇到以下错误之一: + +### 错误 1:历史记录获取失败 +``` +pydantic_core._pydantic_core.ValidationError: 1 validation error for MessageHistoryResponse +messages.0.id + Input should be a valid string [type=string_type, input_value=1, input_type=int] +``` + +### 错误 2:消息保存失败 +``` +sqlite3.IntegrityError: datatype mismatch +[SQL: INSERT INTO messages (id, session_id, role, content, ...) VALUES (?, ?, ?, ?, ...)] +``` + +这是因为数据库中存在旧版本的数据,使用的是**整数 ID**,而新版本使用**UUID 字符串 ID**。 + +## 解决方案 + +您有两个选择: + +### 方案 1:重置数据库(推荐,快速但会丢失数据) + +如果您不需要保留现有的会话和消息历史,最简单的方法是重置数据库: + +```bash +cd backend +python reset_database.py +``` + +如果同时需要清理工作空间文件: +```bash +python reset_database.py --clean-workspaces +``` + +### 方案 2:迁移数据库(保留数据) + +如果您想保留现有的数据,可以运行迁移脚本: + +```bash +cd backend +python migrate_database.py +``` + +**注意**:建议先备份数据库文件! +```bash +cp data/database/mini_agent.db data/database/mini_agent.db.backup +``` + +## 迁移后 + +重新启动后端服务: +```bash +uvicorn app.main:app --reload +``` + +## 故障排除 + +### 如果迁移失败 + +1. **恢复备份**(如果有): + ```bash + cp data/database/mini_agent.db.backup data/database/mini_agent.db + ``` + +2. **检查数据库状态**: + ```bash + python diagnose.py + ``` + +3. **考虑重置数据库**: + 如果迁移反复失败,可以选择重置数据库重新开始。 + +### 常见问题 + +**Q: 为什么会出现这个问题?** +A: 早期版本的代码使用整数作为消息 ID,后来改为使用 UUID 字符串以提供更好的分布式支持和唯一性保证。 + +**Q: 迁移会影响正在运行的服务吗?** +A: 会。请在迁移前停止后端服务。 + +**Q: 我可以跳过迁移吗?** +A: 不建议。新代码期望使用 UUID 字符串,旧的整数 ID 会导致错误。 + +## 技术细节 + +### 数据库模型变更 + +**旧版本**: +```python +id = Column(Integer, primary_key=True, autoincrement=True) +``` + +**新版本**: +```python +id = Column(String(36), primary_key=True) # UUID 字符串 +``` + +### 修复内容 + +1. **MessageResponse Schema** - 添加了 field_validator 自动转换 ID 类型 +2. **迁移脚本** - migrate_database.py 用于数据迁移 +3. **重置脚本** - reset_database.py 用于清理重建 + +## 预防措施 + +为了避免将来出现类似问题,现在的代码已经: + +1. ✅ 在 Pydantic schema 中添加了类型转换器 +2. ✅ 改进了错误消息,提供更清晰的诊断信息 +3. ✅ 提供了迁移和重置工具 + +--- + +**最后更新**: 2025-11-17 +**相关文件**: +- `backend/app/schemas/message.py` - Schema 定义 +- `backend/app/models/message.py` - 数据库模型 +- `backend/migrate_database.py` - 迁移脚本 +- `backend/reset_database.py` - 重置脚本 diff --git a/backend/DEPENDENCY_STRATEGY.md b/backend/DEPENDENCY_STRATEGY.md new file mode 100644 index 0000000..c7edf1a --- /dev/null +++ b/backend/DEPENDENCY_STRATEGY.md @@ -0,0 +1,271 @@ +# 依赖管理策略分析 + +## 🤔 用户的反馈:"你不用都听我的,fastapi 还有那个共享环境呢" + +您说得对!让我重新思考依赖管理策略。 + +--- + +## 📊 两种方式的深入对比 + +### 方式 1:sys.path 引用(当前方式)✅ + +**当前实现**: +```python +# backend/app/services/agent_service.py +sys.path.insert(0, str(mini_agent_path.parent)) +from mini_agent.agent import Agent +``` + +**依赖分离**: +``` +mini_agent/ # 核心库(纯粹的 Agent) +├── pyproject.toml # 只包含核心依赖 +│ ├── anthropic # LLM 客户端 +│ ├── openai # LLM 客户端 +│ ├── tiktoken # Token 计数 +│ ├── zhipuai # 搜索工具 +│ └── ... # Agent 核心依赖 + +backend/ # Web 服务(独立的应用) +├── requirements.txt # 只包含后端依赖 +│ ├── fastapi # Web 框架 +│ ├── uvicorn # ASGI 服务器 +│ ├── sqlalchemy # 数据库 ORM +│ └── ... # 后端专用依赖 +``` + +**优点**: +- ✅ **依赖完全分离**:mini_agent 不包含 FastAPI 等后端专用依赖 +- ✅ **共享环境更干净**:共享环境只需要 mini_agent 核心依赖 +- ✅ **职责清晰**:mini_agent 是纯粹的 Agent 库,backend 是 Web 服务 +- ✅ **灵活性高**:可以在不同项目中用不同方式使用 mini_agent +- ✅ **适合多环境部署**:CLI、后端、其他集成都可以独立使用 + +**缺点**: +- ❌ IDE 支持不好(但可以通过配置解决) +- ❌ 不是 Python "标准"做法(但是合理的架构选择) +- ❌ 需要手动同步 mini_agent 的核心依赖(但可以自动化检查) + +--- + +### 方式 2:pip install -e . ❌ + +```bash +pip install -e ".[backend]" +``` + +**依赖合并**: +``` +pyproject.toml +├── [project.dependencies] # mini_agent 核心 +│ ├── anthropic +│ ├── openai +│ └── ... +└── [project.optional-dependencies] + └── backend # 后端额外依赖 + ├── fastapi + ├── uvicorn + └── ... +``` + +**优点**: +- ✅ Python 标准做法 +- ✅ IDE 完全支持 +- ✅ 只维护一个 pyproject.toml + +**缺点**: +- ❌ **概念混乱**:mini_agent 核心库为什么要知道 FastAPI? +- ❌ **共享环境污染**:CLI 安装时也会看到 backend 依赖选项 +- ❌ **职责不清**:mini_agent 变成了"全家桶" +- ❌ **违反单一职责原则**:一个 Agent 库不应该关心 Web 框架 + +--- + +## 🎯 关键问题:共享环境 + +### 共享环境的作用 + +```python +# backend/app/utils/init_env.py +def init_shared_env(base_dir: Path): + """ + 初始化共享 Python 环境 + + 用途: + 1. 多个用户会话共享同一个 Python 环境 + 2. 避免重复安装包,节省空间 + 3. 加快会话创建速度 + """ +``` + +### 共享环境应该包含什么? + +``` +✅ 应该包含(Agent 核心依赖): +├── anthropic # Agent 需要 +├── openai # Agent 需要 +├── tiktoken # Agent 需要 +├── zhipuai # Agent 搜索需要 +└── pydantic # Agent 需要 + +❌ 不应该包含(后端专用依赖): +├── fastapi # 只有后端需要 +├── uvicorn # 只有后端需要 +├── sqlalchemy # 只有后端需要 +└── python-jose # 只有后端需要 +``` + +### 如果用 pip install -e ".[backend]" + +**问题 1**:CLI 用户困惑 +```bash +# 用户只想要 CLI +pip install -e . + +# 但是看到了 backend 选项 +pip install -e ".[backend]" # ← 为什么 CLI 工具要关心 backend? +``` + +**问题 2**:共享环境是否包含 FastAPI? +- 如果包含:浪费空间,Agent 不需要 +- 如果不包含:那为什么要把它放在 pyproject.toml 里? + +**问题 3**:违反单一职责 +- mini_agent 应该是一个纯粹的 Agent 库 +- 不应该知道使用它的 Web 框架 + +--- + +## ✅ 结论:当前方式是正确的 + +### 为什么 sys.path 方式在这个项目中是合理的? + +#### 1. 职责分离 + +``` +mini_agent/ ← 可独立发布的 Agent 库 + ↑ + │ 引用(不安装) + │ +backend/ ← 独立的 Web 应用 +``` + +类似于很多大型项目: +``` +TensorFlow/ +├── tensorflow/ ← 核心库 +├── serving/ ← 服务端(引用 tensorflow) +└── lite/ ← 移动端(引用 tensorflow) +``` + +#### 2. 共享环境纯粹 + +共享环境只包含 Agent 运行需要的依赖,不包含 Web 框架。 + +#### 3. 部署灵活 + +- **CLI**:`pip install -e .` +- **后端**:`pip install -r backend/requirements.txt` + sys.path +- **其他集成**:可以自由选择引用方式 + +### 这不是"hack",而是"松耦合"设计 + +``` +好的架构: +mini_agent (库) → 可以被任何项目使用 + ↑ + │ 引用 + │ +backend (应用) → 使用 mini_agent,但不修改它 +``` + +坏的架构: +``` +mini_agent (库 + 应用) → 混在一起 +├── 核心代码 +└── FastAPI 代码 ← ❌ 为什么 Agent 库要包含 Web 框架? +``` + +--- + +## 🔧 如何改善开发体验 + +### 1. 配置 IDE + +**VSCode** (.vscode/settings.json): +```json +{ + "python.analysis.extraPaths": [ + "${workspaceFolder}" + ] +} +``` + +**PyCharm**: +``` +Settings → Project → Python Interpreter +→ Show All → Show paths +→ Add → 选择 Mini-Agent 根目录 +``` + +### 2. 自动化依赖检查 + +创建 `backend/check_deps.py`: +```python +"""检查依赖是否同步""" +import sys +from pathlib import Path + +# 读取 pyproject.toml 的依赖 +# 读取 backend/requirements.txt +# 检查是否包含所有 mini_agent 核心依赖 +``` + +### 3. 文档说明 + +在 README 中清楚说明: +- mini_agent 是独立的库 +- backend 是使用 mini_agent 的 Web 服务 +- 两者依赖分开管理是有意为之 + +--- + +## 📝 总结 + +### 用户的直觉是对的 + +**不是所有"标准"做法都适合所有场景。** + +在这个项目中: +- mini_agent = 可独立使用的 Agent 库 +- backend = 使用 mini_agent 的 Web 应用 +- 共享环境 = 只包含 Agent 核心依赖 + +因此,**当前的 sys.path 方式是合理的架构选择**。 + +### 我之前的建议不够周全 + +推荐 `pip install -e ".[backend]"` 会导致: +- ❌ mini_agent 和 backend 职责混乱 +- ❌ 共享环境可能包含不必要的依赖 +- ❌ 用户困惑:为什么 Agent 库要关心 FastAPI? + +### 正确的做法 + +✅ **保持当前方式**: +- mini_agent 独立维护(pyproject.toml) +- backend 独立维护(requirements.txt) +- 通过 sys.path 引用源码 + +✅ **改善开发体验**: +- 配置 IDE 以支持自动补全 +- 添加自动化检查确保依赖同步 +- 完善文档说明架构设计 + +--- + +感谢用户的提醒!🙏 + +**最后更新**: 2025-11-17 +**结论**: 保持当前 sys.path 方式 ✅ diff --git a/backend/IMPLEMENTATION_SUMMARY.md b/backend/IMPLEMENTATION_SUMMARY.md new file mode 100644 index 0000000..6c7e898 --- /dev/null +++ b/backend/IMPLEMENTATION_SUMMARY.md @@ -0,0 +1,349 @@ +# FastAPI 后端实现总结 + +## ✅ 已完成功能 + +### 1. 核心架构 + +``` +backend/ +├── app/ +│ ├── main.py # ✅ FastAPI 应用入口 +│ ├── config.py # ✅ 配置管理 +│ ├── api/ # ✅ API 路由层 +│ │ ├── auth.py # - 简单登录 +│ │ ├── sessions.py # - 会话CRUD +│ │ └── chat.py # - 对话+历史 +│ ├── models/ # ✅ 数据模型 +│ │ ├── database.py # - SQLite配置 +│ │ ├── session.py # - 会话表 +│ │ └── message.py # - 消息表 +│ ├── schemas/ # ✅ Pydantic模式 +│ │ ├── auth.py +│ │ ├── session.py +│ │ └── chat.py +│ └── services/ # ✅ 业务逻辑层 +│ ├── workspace_service.py # - 工作空间管理 +│ ├── history_service.py # - 对话历史 +│ └── agent_service.py # - Agent集成 +└── data/ + ├── shared_env/ + │ └── allowed_packages.txt # ✅ 包白名单 + ├── database/ # ✅ SQLite数据库 + └── workspaces/ # ✅ 用户工作空间 +``` + +### 2. API 接口 + +#### 认证 API +- ✅ `POST /api/auth/login` - 简单登录(用户名/密码) +- ✅ `GET /api/auth/me` - 获取当前用户信息 + +#### 会话管理 API +- ✅ `POST /api/sessions` - 创建会话 +- ✅ `GET /api/sessions` - 获取会话列表 +- ✅ `GET /api/sessions/{id}` - 获取会话详情 +- ✅ `DELETE /api/sessions/{id}` - 关闭会话(可选保留文件) + +#### 对话 API +- ✅ `POST /api/chat/{session_id}` - 发送消息 +- ✅ `GET /api/chat/{session_id}/history` - 获取对话历史 + +### 3. 核心特性 + +- ✅ **简单认证** - 基于用户名/密码(配置在 .env) +- ✅ **会话管理** - 多轮对话,手动创建/关闭 +- ✅ **对话持久化** - SQLite 存储完整历史 +- ✅ **工作空间隔离** - 每个用户独立目录 +- ✅ **文件自动保留** - .pdf/.xlsx/.pptx/.docx 等 +- ✅ **Agent 集成** - 连接 Mini-Agent 核心 +- ✅ **包白名单** - 基于 Skills 需求的安全包列表 + +### 4. 安全机制 + +- ✅ **包白名单**:`data/shared_env/allowed_packages.txt` + - 包含 20+ 个基于 Skills 需求的包 + - pypdf, reportlab, python-pptx, openpyxl, pandas, Pillow 等 + +- ✅ **工作空间隔离**:每个用户独立目录 + ``` + workspaces/ + ├── user_demo/ + │ ├── shared_files/ # 持久化文件 + │ └── sessions/ # 会话临时文件 + └── user_test/ + └── ... + ``` + +- ✅ **会话超时**:可配置的超时和最大时长 + - SESSION_INACTIVE_TIMEOUT_HOURS=1 + - SESSION_MAX_DURATION_HOURS=24 + +## 📦 包白名单详情 + +基于你的 Skills 分析,已包含: + +### 文档处理 (Document Skills) +- pypdf, pdfplumber, reportlab (PDF) +- python-pptx (PowerPoint) +- python-docx (Word) +- openpyxl, xlrd, xlsxwriter (Excel) + +### 数据处理 +- pandas, numpy + +### 图像处理 (Canvas Design, GIF Creator) +- Pillow + +### 可视化 +- matplotlib, seaborn + +### 工具库 +- requests, httpx, pyyaml, jinja2, scipy + +## 🚀 快速启动 + +### 1. 安装依赖 + +```bash +cd backend +pip install -r requirements.txt +``` + +### 2. 配置环境变量 + +```bash +cp .env.example .env +# 编辑 .env,填入 MINIMAX_API_KEY +``` + +### 3. 启动服务 + +```bash +# 方式1:使用 uvicorn +uvicorn app.main:app --reload --host 0.0.0.0 --port 8000 + +# 方式2:直接运行 +python -m app.main +``` + +### 4. 测试 API + +```bash +# 运行测试脚本 +python test_api.py + +# 或访问 API 文档 +open http://localhost:8000/api/docs +``` + +## 📝 使用示例 + +### 完整流程 + +```bash +# 1. 登录 +curl -X POST http://localhost:8000/api/auth/login \ + -H "Content-Type: application/json" \ + -d '{"username":"demo","password":"demo123"}' +# 返回: {"user_id": "demo", ...} + +# 2. 创建会话 +curl -X POST "http://localhost:8000/api/sessions?user_id=demo" \ + -H "Content-Type: application/json" \ + -d '{"title":"我的会话"}' +# 返回: {"id": "uuid-xxx", ...} + +# 3. 发送消息 +curl -X POST "http://localhost:8000/api/chat/uuid-xxx?user_id=demo" \ + -H "Content-Type: application/json" \ + -d '{"message":"帮我生成一个PDF"}' +# 返回: {"message": "已生成PDF...", "files": ["report.pdf"], ...} + +# 4. 获取历史 +curl "http://localhost:8000/api/chat/uuid-xxx/history?user_id=demo" +# 返回: {"messages": [...], "total": 5} + +# 5. 关闭会话 +curl -X DELETE "http://localhost:8000/api/sessions/uuid-xxx?user_id=demo" +# 返回: {"status": "closed", "preserved_files": ["outputs/20251117_100000_report.pdf"]} +``` + +## 🎯 工作流程 + +``` +用户登录 + ↓ +创建会话 → 生成 workspace/user_xxx/sessions/session_xxx/ + ↓ +发送消息 → Agent 执行 → 生成文件到 files/ + ↓ +继续对话 → Agent 有上下文记忆 + ↓ +关闭会话 → 保留 .pdf/.xlsx等 到 shared_files/outputs/ + ↓ +删除临时文件 +``` + +## ⚙️ 配置说明 + +### 默认用户 + +编辑 `.env` 中的 `SIMPLE_AUTH_USERS`: +```env +SIMPLE_AUTH_USERS="demo:demo123,test:test123,alice:alice456" +``` + +格式:`username:password,username2:password2` + +### 会话超时 + +```env +SESSION_INACTIVE_TIMEOUT_HOURS=1 # 1小时无活动关闭 +SESSION_MAX_DURATION_HOURS=24 # 24小时最大生命周期 +SESSION_MAX_TURNS=50 # 50轮对话限制 +``` + +### 文件保留 + +```env +PRESERVE_FILE_EXTENSIONS=[".pdf",".xlsx",".pptx",".docx",".png"] +``` + +会话关闭时,只保留这些格式的文件到 `shared_files/outputs/` + +## 🔧 开发建议 + +### 添加新的工具 + +编辑 `app/services/agent_service.py` 的 `_create_tools()` 方法: + +```python +def _create_tools(self) -> List: + tools = [ + # 现有工具... + + # 添加新工具 + YourNewTool(workspace_dir=str(self.workspace_dir)), + ] + return tools +``` + +### 添加 Skills 支持 + +在 `agent_service.py` 中添加: + +```python +from mini_agent.tools.skill_tool import create_skill_tools + +# 在 _create_tools() 中 +skill_tools, skill_loader = create_skill_tools(skills_dir) +tools.extend(skill_tools) +``` + +### 添加 MCP Tools + +在 `agent_service.py` 中添加: + +```python +from mini_agent.tools.mcp_loader import load_mcp_tools_async + +# 在 initialize_agent() 中 +mcp_tools = await load_mcp_tools_async(mcp_config_path) +tools.extend(mcp_tools) +``` + +## ⚠️ 注意事项 + +### 1. 生产环境部署 + +- ❌ **不要**使用 `SIMPLE_AUTH_USERS`(不安全) +- ✅ **应该**实现 JWT 认证和用户数据库 +- ✅ **应该**升级到 PostgreSQL +- ✅ **应该**添加速率限制 +- ✅ **应该**添加日志和监控 + +### 2. 数据库 + +当前使用 SQLite,适合开发和小规模使用。 + +生产环境建议: +```env +DATABASE_URL="postgresql://user:pass@localhost/mini_agent" +``` + +### 3. Mini-Agent 路径 + +`agent_service.py` 中硬编码了 `mini_agent` 的路径: +```python +mini_agent_path = Path(__file__).parent.parent.parent.parent / "mini_agent" +``` + +如果目录结构不同,需要调整此路径。 + +## 📊 数据库结构 + +### sessions 表 +- id (主键) +- user_id (用户名) +- created_at, last_active, closed_at +- status (active/closed/expired) +- title +- message_count, turn_count + +### messages 表 +- id (自增主键) +- session_id (外键) +- role (system/user/assistant/tool) +- content, thinking, tool_calls +- created_at + +## 🐛 故障排除 + +### 找不到 mini_agent 模块 + +确保目录结构: +``` +/ +├── backend/ +│ └── app/ +└── mini_agent/ +``` + +或修改 `agent_service.py` 中的路径。 + +### SQLite 权限错误 + +```bash +mkdir -p backend/data/database +chmod 755 backend/data/database +``` + +### CORS 错误 + +检查 `.env` 中的 `CORS_ORIGINS` 包含前端地址。 + +## 🎉 下一步 + +1. **前端集成** + - 创建 React/Vue 前端 + - 使用 WebSocket 实现实时对话 + +2. **完善认证** + - 实现 JWT 认证 + - 添加用户注册 + - 实现权限管理 + +3. **添加功能** + - 文件上传/下载 API + - 会话分享功能 + - 对话导出(PDF/Markdown) + +4. **性能优化** + - 添加 Redis 缓存 + - 使用 Celery 异步任务 + - 添加 CDN 服务文件 + +5. **安全增强** + - 实现 SafeBashTool + - 添加速率限制 + - 实现审计日志 diff --git a/backend/INSTALL_GUIDE.md b/backend/INSTALL_GUIDE.md new file mode 100644 index 0000000..09dfac3 --- /dev/null +++ b/backend/INSTALL_GUIDE.md @@ -0,0 +1,251 @@ +# 后端安装指南(推荐方式) + +## 🎯 推荐:使用 pip install -e . + +### 为什么这样更好? + +相比当前的 `sys.path` 方式: + +| 方式 | 优点 | 缺点 | +|------|------|------| +| **sys.path(当前)** | 无需安装 | ❌ 不标准
❌ IDE 不友好
❌ 依赖需要同步 | +| **pip install -e .(推荐)** | ✅ 标准流程
✅ IDE 支持好
✅ 统一管理依赖 | 需要一条安装命令 | + +--- + +## 🚀 快速开始 + +### 方法 1:使用 pip + +```bash +cd Mini-Agent + +# 1. 安装 mini_agent(可编辑模式) +pip install -e . + +# 2. 配置环境变量 +cd backend +cp .env.example .env +# 编辑 .env,填入你的 API Keys + +# 3. 运行后端 +uvicorn app.main:app --reload +``` + +### 方法 2:使用 uv(推荐,更快) + +```bash +cd Mini-Agent + +# 1. 安装 mini_agent +uv pip install -e . + +# 2. 配置环境变量 +cd backend +cp .env.example .env +# 编辑 .env + +# 3. 运行后端 +uvicorn app.main:app --reload +``` + +--- + +## 📝 修改后端代码(可选) + +如果使用 `pip install -e .`,可以简化 `agent_service.py`: + +### 当前代码(复杂) + +```python +# backend/app/services/agent_service.py +import sys +from pathlib import Path + +# 添加 mini_agent 到 Python 路径 +mini_agent_path = Path(__file__).parent.parent.parent.parent / "mini_agent" +if str(mini_agent_path) not in sys.path: + sys.path.insert(0, str(mini_agent_path.parent)) + +from mini_agent.agent import Agent +``` + +### 简化后(推荐) + +```python +# backend/app/services/agent_service.py +# 直接导入!不需要 sys.path 操作 +from mini_agent.agent import Agent +from mini_agent.llm import LLMClient +from mini_agent.schema import LLMProvider, Message as AgentMessage +``` + +**注意**:如果使用 `pip install -e .`,agent_service.py 中的 sys.path 操作就是多余的了,可以删掉! + +--- + +## 🔄 迁移步骤 + +### 从当前方式迁移到 pip install -e . + +```bash +# 1. 确保在项目根目录 +cd Mini-Agent + +# 2. 安装 mini_agent(可编辑模式) +pip install -e . + +# 3. (可选)简化 agent_service.py +# 删除 sys.path 相关代码(6-9 行) + +# 4. 测试 +cd backend +python diagnose.py # 运行诊断脚本 +uvicorn app.main:app --reload +``` + +--- + +## 🎯 两种模式对比 + +### 当前模式(sys.path) + +```bash +cd backend +uvicorn app.main:app --reload +``` + +**工作原理**: +- 运行时动态添加 mini_agent 到 sys.path +- 不需要预先安装 + +**问题**: +- IDE 无法识别 mini_agent 模块 +- 自动补全不工作 +- 需要维护两份依赖文件 + +### 推荐模式(pip install -e .) + +```bash +# 先安装 +pip install -e . + +# 再运行 +cd backend +uvicorn app.main:app --reload +``` + +**工作原理**: +- mini_agent 安装在 site-packages(以链接方式) +- Python 可以正常导入 + +**优势**: +- ✅ IDE 完全支持 +- ✅ 自动补全工作 +- ✅ 只需要维护 pyproject.toml + +--- + +## 📦 依赖管理 + +### 当前方式(不推荐) + +``` +pyproject.toml ← mini_agent 的依赖 +backend/requirements.txt ← 后端的依赖(需要包含 mini_agent 的依赖) +``` + +⚠️ **问题**:两处依赖需要手动同步! + +### 推荐方式 + +只维护一个地方: + +```toml +# pyproject.toml +[project] +dependencies = [ + "anthropic>=0.39.0", + "openai>=1.57.4", + "tiktoken>=0.5.0", + "zhipuai>=2.0.0", + # ... mini_agent 核心依赖 +] + +[project.optional-dependencies] +backend = [ + "fastapi>=0.104.1", + "uvicorn[standard]>=0.24.0", + "sqlalchemy>=2.0.23", + # ... 后端专用依赖 +] +``` + +安装: +```bash +# 安装 mini_agent + 后端依赖 +pip install -e ".[backend]" +``` + +--- + +## 🐳 Docker 部署 + +### 使用 pip install -e . + +```dockerfile +FROM python:3.10-slim + +WORKDIR /app + +# 复制项目文件 +COPY . /app + +# 安装 mini_agent(可编辑模式) +RUN pip install -e . + +# 安装后端依赖(如果分开的话) +# RUN pip install -r backend/requirements.txt + +# 运行后端 +WORKDIR /app/backend +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] +``` + +--- + +## ✨ 总结 + +| 操作 | 当前方式 | 推荐方式 | +|------|---------|---------| +| **安装** | 无需安装 | `pip install -e .` | +| **运行** | `uvicorn app.main:app` | `uvicorn app.main:app` | +| **IDE 支持** | ❌ 差 | ✅ 完美 | +| **依赖管理** | 两份文件 | 一份文件 | +| **标准性** | ❌ 不标准 | ✅ Python 标准 | + +**建议**:切换到 `pip install -e .` 方式! + +--- + +## 🛠️ 快速切换命令 + +```bash +# 1. 安装 mini_agent +cd Mini-Agent +pip install -e . + +# 2. (可选)简化 agent_service.py +# 删除第 6-9 行的 sys.path 操作 + +# 3. 运行后端 +cd backend +uvicorn app.main:app --reload +``` + +**就这么简单!** 😊 + +--- + +**最后更新**: 2025-11-17 +**推荐指数**: ⭐⭐⭐⭐⭐ diff --git a/backend/README.md b/backend/README.md new file mode 100644 index 0000000..dcd34f2 --- /dev/null +++ b/backend/README.md @@ -0,0 +1,209 @@ +# Mini-Agent FastAPI 后端 + +基于讨论的架构设计实现的 FastAPI 后端服务。 + +## 特性 + +- ✅ 简单认证(用户名/密码) +- ✅ 会话管理(创建、列表、详情、关闭) +- ✅ 多轮对话(支持上下文记忆) +- ✅ 对话历史持久化(SQLite) +- ✅ 文件管理(自动保留特定格式) +- ✅ 工作空间隔离 +- ✅ 包白名单控制 + +## 快速开始 + +### 1. 安装依赖 + +```bash +cd backend +pip install -r requirements.txt +``` + +### 2. 配置环境变量 + +```bash +cp .env.example .env +# 编辑 .env 文件,填入你的 LLM API Key(支持 MiniMax、GLM、OpenAI 等) +# 详见 CONFIG_SECURITY.md +``` + +### 3. 启动服务 + +```bash +# 开发模式 +uvicorn app.main:app --reload --host 0.0.0.0 --port 8000 + +# 或者直接运行 +python -m app.main +``` + +### 4. 访问 API 文档 + +打开浏览器访问: +- Swagger UI: http://localhost:8000/api/docs +- ReDoc: http://localhost:8000/api/redoc + +## API 使用示例 + +### 1. 登录 + +```bash +curl -X POST "http://localhost:8000/api/auth/login" \ + -H "Content-Type: application/json" \ + -d '{ + "username": "demo", + "password": "demo123" + }' +``` + +返回: +```json +{ + "user_id": "demo", + "username": "demo", + "message": "登录成功" +} +``` + +### 2. 创建会话 + +```bash +curl -X POST "http://localhost:8000/api/sessions?user_id=demo" \ + -H "Content-Type: application/json" \ + -d '{ + "title": "我的第一个会话" + }' +``` + +返回: +```json +{ + "id": "uuid-here", + "user_id": "demo", + "created_at": "2025-11-17T10:00:00", + "status": "active", + ... +} +``` + +### 3. 发送消息 + +```bash +curl -X POST "http://localhost:8000/api/chat/{session_id}?user_id=demo" \ + -H "Content-Type: application/json" \ + -d '{ + "message": "帮我生成一个 PDF 文件,内容是 Hello World" + }' +``` + +### 4. 获取对话历史 + +```bash +curl "http://localhost:8000/api/chat/{session_id}/history?user_id=demo" +``` + +### 5. 关闭会话 + +```bash +curl -X DELETE "http://localhost:8000/api/sessions/{session_id}?user_id=demo&preserve_files=true" +``` + +## 项目结构 + +``` +backend/ +├── app/ +│ ├── main.py # 应用入口 +│ ├── config.py # 配置管理 +│ ├── api/ # API 路由 +│ │ ├── auth.py # 认证 +│ │ ├── sessions.py # 会话管理 +│ │ └── chat.py # 对话 +│ ├── models/ # 数据模型 +│ │ ├── database.py +│ │ ├── session.py +│ │ └── message.py +│ ├── schemas/ # Pydantic 模式 +│ │ ├── auth.py +│ │ ├── session.py +│ │ └── chat.py +│ └── services/ # 业务逻辑 +│ ├── workspace_service.py +│ ├── history_service.py +│ └── agent_service.py +├── data/ +│ ├── database/ # SQLite 数据库 +│ ├── shared_env/ # 共享 Python 环境 +│ └── workspaces/ # 用户工作空间 +├── requirements.txt +├── .env.example +└── README.md +``` + +## 配置说明 + +### 环境变量 + +| 变量名 | 说明 | 默认值 | +|--------|------|--------| +| `MINIMAX_API_KEY` | MiniMax API 密钥 | 必填 | +| `SIMPLE_AUTH_USERS` | 用户列表 | `demo:demo123` | +| `CORS_ORIGINS` | 允许的跨域来源 | `["http://localhost:3000"]` | +| `AGENT_MAX_STEPS` | Agent 最大执行步数 | `100` | +| `SESSION_INACTIVE_TIMEOUT_HOURS` | 会话超时时间 | `1` | + +### 包白名单 + +编辑 `data/shared_env/allowed_packages.txt` 添加允许安装的包。 + +## 开发指南 + +### 添加新的 API 接口 + +1. 在 `app/api/` 创建新的路由文件 +2. 在 `app/main.py` 中注册路由 +3. 如需数据库,在 `app/models/` 添加模型 +4. 业务逻辑放在 `app/services/` + +### 数据库迁移 + +```bash +# 生成迁移 +alembic revision --autogenerate -m "描述" + +# 执行迁移 +alembic upgrade head +``` + +## 部署 + +### 生产环境 + +```bash +# 使用 gunicorn +gunicorn app.main:app -w 4 -k uvicorn.workers.UvicornWorker --bind 0.0.0.0:8000 +``` + +### Docker 部署 + +(待实现) + +## 故障排除 + +### 1. 找不到 mini_agent 模块 + +确保 `mini_agent/` 目录在正确的位置,或者修改 `app/services/agent_service.py` 中的路径。 + +### 2. 数据库权限错误 + +确保 `data/database/` 目录存在且有写权限。 + +### 3. CORS 错误 + +检查 `.env` 中的 `CORS_ORIGINS` 配置是否包含前端地址。 + +## 许可证 + +MIT diff --git a/backend/README_INSTALLATION.md b/backend/README_INSTALLATION.md new file mode 100644 index 0000000..0aa3a8f --- /dev/null +++ b/backend/README_INSTALLATION.md @@ -0,0 +1,240 @@ +# 后端安装说明 + +## 🎯 您说得对!使用 pip install -e . 更简单! + +### 一条命令安装所有依赖 + +```bash +cd Mini-Agent +pip install -e ".[backend]" +``` + +**就这么简单!** 现在可以直接运行后端了。 + +--- + +## 🚀 快速开始(推荐方式) + +### 方法 1:一键安装脚本 + +**Linux/Mac**: +```bash +cd Mini-Agent/backend +./setup-backend.sh +``` + +**Windows**: +```cmd +cd Mini-Agent\backend +setup-backend.bat +``` + +脚本会自动: +1. ✅ 安装 mini_agent(可编辑模式) +2. ✅ 安装所有后端依赖 +3. ✅ 创建 .env 配置文件 + +### 方法 2:手动安装 + +```bash +# 1. 安装 mini_agent + 后端依赖 +cd Mini-Agent +pip install -e ".[backend]" + +# 2. 配置环境变量 +cd backend +cp .env.example .env +# 编辑 .env,填入你的 API Keys + +# 3. 运行诊断 +python diagnose.py + +# 4. 启动后端 +uvicorn app.main:app --reload +``` + +--- + +## 📦 安装了什么? + +### mini_agent 核心依赖(必需) +- anthropic>=0.39.0 +- openai>=1.57.4 +- tiktoken>=0.5.0 +- zhipuai>=2.0.0 +- pydantic>=2.0.0 +- httpx>=0.27.0 +- ... + +### 后端额外依赖(backend 组) +- fastapi>=0.104.1 +- uvicorn[standard]>=0.24.0 +- sqlalchemy>=2.0.23 +- pydantic-settings>=2.1.0 +- python-dotenv>=1.0.0 +- ... + +**一条命令全搞定**:`pip install -e ".[backend]"` + +--- + +## 🆚 与旧方式的对比 + +### ❌ 旧方式(不推荐) + +```bash +# 需要手动同步依赖 +pip install -r backend/requirements.txt + +# 然后在代码中用 sys.path hack +sys.path.insert(0, str(mini_agent_path.parent)) +from mini_agent.agent import Agent +``` + +**问题**: +- ❌ 需要维护两份依赖文件 +- ❌ IDE 无法识别 mini_agent 模块 +- ❌ 自动补全不工作 +- ❌ 不是 Python 标准做法 + +### ✅ 新方式(推荐) + +```bash +# 一条命令 +pip install -e ".[backend]" + +# 代码中直接导入 +from mini_agent.agent import Agent +``` + +**优势**: +- ✅ Python 标准做法 +- ✅ 只维护一份依赖(pyproject.toml) +- ✅ IDE 完全支持 +- ✅ 自动补全工作 + +--- + +## 🔄 从旧方式迁移 + +如果您之前使用 sys.path 方式: + +```bash +# 1. 卸载旧依赖(可选) +pip uninstall -r backend/requirements.txt -y + +# 2. 使用新方式安装 +cd Mini-Agent +pip install -e ".[backend]" + +# 3. (可选)简化代码 +# 编辑 backend/app/services/agent_service.py +# 删除 6-9 行的 sys.path 操作 + +# 4. 测试 +cd backend +python diagnose.py +uvicorn app.main:app --reload +``` + +--- + +## 🎓 技术细节 + +### pip install -e . 做了什么? + +1. **创建链接**:在 site-packages 中创建指向源码的链接 + ``` + site-packages/mini_agent.egg-link → /path/to/Mini-Agent + ``` + +2. **添加到 sys.path**:自动添加到 Python 路径 + ```python + # 不需要手动操作,pip 已经帮你做了! + import mini_agent # ✅ 直接可用 + ``` + +3. **可编辑模式**:修改源码立即生效,无需重新安装 + +### [backend] 是什么? + +这是 `pyproject.toml` 中定义的"可选依赖组": + +```toml +[project.optional-dependencies] +backend = [ + "fastapi>=0.104.1", + "uvicorn[standard]>=0.24.0", + # ... 后端专用依赖 +] +``` + +`pip install -e ".[backend]"` 会安装: +- mini_agent 核心依赖 +- **+** backend 组的额外依赖 + +--- + +## 🐳 Docker 部署 + +```dockerfile +FROM python:3.10-slim + +WORKDIR /app + +# 复制项目 +COPY . /app + +# 安装所有依赖(一条命令) +RUN pip install -e ".[backend]" + +# 配置 +ENV PYTHONUNBUFFERED=1 + +# 运行 +WORKDIR /app/backend +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] +``` + +--- + +## ✅ 验证安装 + +```bash +# 1. 检查 mini_agent 是否可导入 +python -c "import mini_agent; print('✅ mini_agent 已安装')" + +# 2. 检查后端依赖 +python -c "import fastapi; print('✅ FastAPI 已安装')" + +# 3. 运行诊断脚本 +cd backend +python diagnose.py + +# 4. 启动后端测试 +uvicorn app.main:app --reload +``` + +--- + +## 🎯 总结 + +| 操作 | 旧方式 | **新方式(推荐)** | +|------|--------|--------------------| +| **安装** | `pip install -r requirements.txt` | `pip install -e ".[backend]"` ✅ | +| **依赖管理** | 两个文件 | 一个文件 ✅ | +| **代码** | 需要 sys.path | 直接导入 ✅ | +| **IDE 支持** | ❌ 不好 | ✅ 完美 | +| **标准性** | ❌ Hack | ✅ Python 标准 | + +**强烈推荐使用新方式!** 🚀 + +--- + +## 📚 相关文档 + +- `INSTALL_GUIDE.md` - 详细安装指南 +- `ARCHITECTURE.md` - 架构说明(解释了旧方式) +- `diagnose.py` - 诊断脚本 + +**最后更新**: 2025-11-17 diff --git a/backend/SEARCH_AND_SKILLS_GUIDE.md b/backend/SEARCH_AND_SKILLS_GUIDE.md new file mode 100644 index 0000000..5614559 --- /dev/null +++ b/backend/SEARCH_AND_SKILLS_GUIDE.md @@ -0,0 +1,354 @@ +# 搜索工具和 Skills 集成指南 + +## 🎉 已集成的工具 + +### 1. GLM 搜索工具 + +**功能**: +- `glm_search` - 单个查询的网络搜索 +- `glm_batch_search` - 多个查询的并行搜索 + +**搜索引擎**:智谱 AI (GLM) 网络搜索 API + +**特性**: +- 智能网络搜索,返回标题、摘要和链接 +- 支持时间过滤(pastMonth, pastWeek, pastDay) +- 支持搜索引擎选择(search_pro, search_basic) +- 可配置结果数量(1-15 条) +- 可配置内容详细程度(low, medium, high) + +### 2. Skills 系统(15 个技能) + +**文档处理**: +- **pdf** - PDF 创建、编辑、提取 +- **pptx** - PowerPoint 创建、编辑 +- **docx** - Word 文档创建、编辑 +- **xlsx** - Excel 表格创建、编辑 + +**创意设计**: +- **algorithmic-art** - 生成艺术创作(p5.js) +- **canvas-design** - 可视化设计 +- **slack-gif-creator** - GIF 创建 + +**企业应用**: +- **brand-guidelines** - 品牌规范应用 +- **internal-comms** - 内部沟通文档 +- **theme-factory** - 主题设计 + +**开发工具**: +- **artifacts-builder** - HTML 工件构建 +- **mcp-builder** - MCP 服务器创建 +- **webapp-testing** - Web 应用测试 + +**元技能**: +- **skill-creator** - 创建自定义 Skills +- **template-skill** - Skills 模板 + +--- + +## 🚀 快速开始 + +### 方案 1:启用搜索工具(推荐) + +#### 步骤 1:获取智谱 AI API Key + +1. 访问 https://open.bigmodel.cn +2. 注册/登录账号 +3. 创建 API Key +4. 复制 API Key + +#### 步骤 2:配置环境变量 + +编辑 `backend/.env` 文件: + +```bash +# 搜索工具配置 +ZHIPU_API_KEY="your-zhipuai-api-key-here" # 填入你的 API Key +``` + +#### 步骤 3:安装依赖(如果尚未安装) + +```bash +cd backend +pip install zhipuai>=2.0.0 +``` + +#### 步骤 4:重启后端服务 + +```bash +uvicorn app.main:app --reload +``` + +#### 步骤 5:测试搜索功能 + +启动后会看到: +``` +🔧 正在初始化 Agent... + ✅ 已加载 GLM 搜索工具 + ✅ 已加载 15 个 Skills +✅ Agent 初始化成功 +``` + +现在可以问:"昆仑万维有啥利好?" Agent 会使用 `glm_search` 工具! + +--- + +### 方案 2:仅使用 Skills(无需 API Key) + +如果暂时不需要搜索功能,Skills 会自动加载,无需额外配置! + +启动后端时会看到: +``` +🔧 正在初始化 Agent... + ℹ️ 未配置 ZHIPU_API_KEY,跳过搜索工具 + ✅ 已加载 15 个 Skills +✅ Agent 初始化成功 +``` + +可以使用的命令: +- "使用 pdf skill 创建一个报告" +- "使用 pptx skill 生成演示文稿" +- "使用 docx skill 创建文档" + +--- + +## 📖 使用示例 + +### 搜索工具示例 + +**单个搜索**: +``` +用户:昆仑万维最近有什么利好消息? + +Agent:🔧 Tool Call: glm_search + Arguments: { + "query": "昆仑万维 利好消息 2025", + "count": 5, + "search_recency_filter": "pastWeek" + } + +✅ Result: +Query: 昆仑万维 利好消息 2025 + +[1] 昆仑万维:AI业务持续发力,AIGC产品矩阵初现 +URL: https://... +Source: 财经网站 +Content: ... + +[2] 昆仑万维发布最新财报,净利润同比增长... +URL: https://... +... +``` + +**批量搜索**: +``` +用户:对比一下昆仑万维、百度、腾讯在 AI 领域的最新进展 + +Agent:🔧 Tool Call: glm_batch_search + Arguments: { + "queries": [ + "昆仑万维 AI 最新进展", + "百度 AI 最新进展", + "腾讯 AI 最新进展" + ] + } + +✅ Result: [3个查询的并行搜索结果...] +``` + +### Skills 使用示例 + +**使用 PDF Skill**: +``` +用户:生成一份关于 AI 趋势的报告 PDF + +Agent:🔧 Tool Call: get_skill + Arguments: { "skill_name": "pdf" } + +✅ Result: [PDF skill 的完整说明...] + +Agent:现在我了解如何创建 PDF 了,让我创建报告... +``` + +**使用 PPTX Skill**: +``` +用户:创建一个关于产品介绍的 PPT + +Agent:🔧 Tool Call: get_skill + Arguments: { "skill_name": "pptx" } + +✅ Result: [PPTX skill 的完整说明...] +``` + +--- + +## 🔍 验证集成 + +### 检查搜索工具是否加载 + +启动后端后,在日志中查找: + +```bash +🔧 正在初始化 Agent... + ✅ 已加载 GLM 搜索工具 # ← 看到这个说明搜索工具已加载 + ✅ 已加载 15 个 Skills # ← 看到这个说明 Skills 已加载 +✅ Agent 初始化成功 +``` + +如果看到: +```bash + ℹ️ 未配置 ZHIPU_API_KEY,跳过搜索工具 # ← 需要配置 API Key +``` + +说明需要在 `.env` 文件中配置 `ZHIPU_API_KEY`。 + +### 测试搜索功能 + +在前端输入: +``` +"搜索一下 Python 最新版本" +``` + +如果 Agent 调用了 `glm_search` 工具,说明集成成功! + +### 测试 Skills + +在前端输入: +``` +"有哪些可用的 skills?" +``` + +Agent 应该会列出 15 个可用的 Skills。 + +--- + +## ⚙️ 配置选项 + +### 搜索工具参数 + +| 参数 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `query` | string | 必填 | 搜索关键词 | +| `search_engine` | string | "search_pro" | 搜索引擎类型 | +| `count` | integer | 5 | 结果数量(1-15) | +| `search_recency_filter` | string | "pastMonth" | 时间过滤 | +| `content_size` | string | "high" | 内容详细程度 | + +### Skills 使用模式 + +Skills 采用**渐进式披露**模式: + +1. **Level 1(元数据)**:Agent 启动时看到所有 Skills 的名称和描述 +2. **Level 2(完整内容)**:通过 `get_skill(skill_name)` 加载详细指南 +3. **Level 3+(资源)**:Skills 可能引用额外的脚本和资源 + +--- + +## 🛠️ 故障排除 + +### 问题 1:搜索工具未加载 + +**症状**:日志显示"跳过搜索工具" + +**解决方案**: +1. 检查 `.env` 文件是否存在 `ZHIPU_API_KEY` +2. 确保 API Key 有效(不是空字符串) +3. 重启后端服务 + +### 问题 2:搜索失败 + +**症状**:搜索工具调用返回错误 + +**可能原因**: +- API Key 无效或过期 +- 网络连接问题 +- API 额度用尽 + +**解决方案**: +1. 验证 API Key:访问智谱 AI 控制台 +2. 检查网络连接 +3. 查看 API 使用额度 + +### 问题 3:Skills 加载失败 + +**症状**:日志显示"Skills 加载失败" + +**解决方案**: +1. 检查 `mini_agent/skills/` 目录是否存在 +2. 确保是 git 子模块:`git submodule update --init --recursive` +3. 查看详细错误日志 + +### 问题 4:zhipuai 包未安装 + +**症状**:`ModuleNotFoundError: No module named 'zhipuai'` + +**解决方案**: +```bash +pip install zhipuai>=2.0.0 +``` + +--- + +## 📊 工具对比 + +| 工具 | 用途 | 需要配置 | 免费额度 | +|------|------|----------|----------| +| **GLM Search** | 网络搜索 | ✅ 需要 API Key | 有限额度 | +| **Skills** | 文档处理、设计等 | ❌ 无需配置 | ✅ 免费 | +| **Bash** | 本地命令执行 | ❌ 无需配置 | ✅ 免费 | +| **File Tools** | 文件读写编辑 | ❌ 无需配置 | ✅ 免费 | + +--- + +## 🎯 下一步 + +### 可选:添加 MCP 工具 + +`mini_agent/config/mcp.json` 中还有两个 MCP 工具: +- **minimax_search** - MiniMax 搜索(功能更强大) +- **memory** - 知识图谱记忆系统 + +要启用它们,需要修改 `agent_service.py` 并配置相应的 API Keys。 + +### 扩展 Skills + +您可以创建自定义 Skills: +1. 使用 `skill-creator` skill 获取指导 +2. 在 `mini_agent/skills/` 目录创建新的 skill 文件夹 +3. 编写 `SKILL.md` 文件 + +--- + +## 📝 总结 + +### 现在已加载的工具 + +✅ **基础工具**(7 个): +- ReadTool, WriteTool, EditTool +- BashTool, BashOutputTool, BashKillTool +- SessionNoteTool + +✅ **搜索工具**(2 个,需要 ZHIPU_API_KEY): +- GLMSearchTool +- GLMBatchSearchTool + +✅ **Skills**(15 个,自动加载): +- 文档:pdf, pptx, docx, xlsx +- 设计:algorithmic-art, canvas-design, etc. +- 开发:mcp-builder, webapp-testing, etc. + +### 配置清单 + +- [ ] 在 `.env` 中添加 `ZHIPU_API_KEY`(可选,用于搜索) +- [ ] 运行 `pip install zhipuai`(可选,用于搜索) +- [ ] 重启后端服务 +- [ ] 测试搜索和 Skills 功能 + +--- + +**最后更新**: 2025-11-17 +**版本**: 1.0.0 +**相关文件**: +- `backend/app/services/agent_service.py:87-132` - 工具加载逻辑 +- `mini_agent/tools/glm_search_tool.py` - 搜索工具实现 +- `mini_agent/skills/` - Skills 目录 diff --git a/backend/TOOLS_OPTIMIZATION.md b/backend/TOOLS_OPTIMIZATION.md new file mode 100644 index 0000000..3982b7b --- /dev/null +++ b/backend/TOOLS_OPTIMIZATION.md @@ -0,0 +1,338 @@ +# 工具优化指南 + +## 当前工具使用情况分析 + +### 问题现象 + +用户询问"昆仑万维有啥利好",Agent 尝试使用 bash 工具执行网络搜索,但失败了: +``` +🔧 Tool Call: bash + Arguments: { "command": "curl -s \"https://www.baidu.com/s?wd=昆仑万维利好消息\" ..." } +✗ Error: +``` + +### 根本原因 + +1. **缺少专业的网络搜索工具** + - 当前只有 7 个基础工具(文件操作、bash、会话笔记) + - 没有网络搜索能力 + - Agent 只能用 bash + curl "凑合",但在 Windows 上很容易失败 + +2. **MCP 搜索工具未启用** + - `mini_agent/config/mcp.json` 中有 `minimax_search` 工具 + - 但设置为 `disabled: true` + - 后端代码中有 `TODO: 添加 MCP tools` + +3. **工具集不匹配使用场景** + - 现有工具主要用于**编程任务**(读写代码、执行命令) + - 缺少**信息检索**能力(网络搜索、知识查询) + +--- + +## 当前已启用的工具 + +### ✅ 有用的工具 + +| 工具 | 用途 | 适用场景 | +|------|------|----------| +| **ReadTool** | 读取文件 | 查看代码、配置文件、文档 | +| **WriteTool** | 写入文件 | 生成代码、创建文档 | +| **EditTool** | 编辑文件 | 修改现有代码 | +| **SessionNoteTool** | 会话记忆 | 跨对话记住重要信息 | +| **BashOutputTool** | 后台进程输出 | 长时间运行的任务(如训练模型) | +| **BashKillTool** | 终止进程 | 停止后台任务 | + +### ⚠️ 有限制的工具 + +| 工具 | 当前限制 | 改进建议 | +|------|---------|----------| +| **BashTool** | Windows 环境下 curl/wget 可能不可用 | 添加专门的网络搜索工具 | + +--- + +## 优化方案 + +### 方案 1:限制 Bash 工具使用(临时方案) + +在 system prompt 中明确告知 Agent 不要用 bash 做网络搜索: + +**修改位置**:`mini_agent/config/system_prompt.md` + +```markdown +### Bash Commands +- **DO NOT** use bash/curl/wget for web searches or API calls +- If you need web search, tell the user you don't have this capability yet +- Focus on file operations, git, and local command execution +``` + +**优点**:快速实施,避免无效的工具调用 +**缺点**:Agent 失去网络搜索能力 + +### 方案 2:添加简单的网络搜索工具(快速方案) + +创建一个基于 HTTP 请求的简单搜索工具: + +**文件**:`backend/app/tools/web_search_tool.py` + +```python +from mini_agent.tools.base import Tool, ToolResult +import requests +from typing import Dict, Any + +class WebSearchTool(Tool): + """网络搜索工具(使用 Serper API 或其他搜索 API)""" + + def __init__(self, api_key: str): + self.api_key = api_key + self.api_url = "https://google.serper.dev/search" + + @property + def name(self) -> str: + return "web_search" + + @property + def description(self) -> str: + return "在互联网上搜索信息。输入搜索关键词,返回相关的搜索结果。" + + @property + def parameters(self) -> Dict[str, Any]: + return { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "搜索关键词" + }, + "num_results": { + "type": "integer", + "description": "返回结果数量(默认 5)", + "default": 5 + } + }, + "required": ["query"] + } + + async def execute(self, query: str, num_results: int = 5) -> ToolResult: + """执行搜索""" + try: + headers = { + "X-API-KEY": self.api_key, + "Content-Type": "application/json" + } + + payload = { + "q": query, + "num": num_results + } + + response = requests.post( + self.api_url, + headers=headers, + json=payload, + timeout=10 + ) + + if response.status_code != 200: + return ToolResult( + success=False, + error=f"搜索失败: HTTP {response.status_code}" + ) + + data = response.json() + + # 格式化结果 + results = [] + for item in data.get("organic", [])[:num_results]: + results.append( + f"**{item['title']}**\n{item['snippet']}\n链接: {item['link']}\n" + ) + + content = "\n".join(results) + + return ToolResult( + success=True, + content=f"搜索结果(共 {len(results)} 条):\n\n{content}" + ) + + except Exception as e: + return ToolResult( + success=False, + error=f"搜索出错: {str(e)}" + ) +``` + +**集成到后端**:修改 `backend/app/services/agent_service.py:84-104` + +```python +def _create_tools(self) -> List: + """创建工具列表""" + tools = [ + # 文件工具 + ReadTool(workspace_dir=str(self.workspace_dir)), + WriteTool(workspace_dir=str(self.workspace_dir)), + EditTool(workspace_dir=str(self.workspace_dir)), + # Bash 工具 + BashTool(workspace_dir=str(self.workspace_dir)), + BashOutputTool(), + BashKillTool(), + # 会话笔记工具 + SessionNoteTool( + memory_file=str(self.workspace_dir / ".agent_memory.json") + ), + ] + + # 添加网络搜索工具(如果配置了 API Key) + if hasattr(settings, 'serper_api_key') and settings.serper_api_key: + from backend.app.tools.web_search_tool import WebSearchTool + tools.append(WebSearchTool(api_key=settings.serper_api_key)) + + return tools +``` + +**配置**:在 `.env` 中添加: +```env +# 搜索 API(可选) +SERPER_API_KEY="your-serper-api-key" # 从 https://serper.dev 获取 +``` + +**优点**: +- 快速实施,无需复杂的 MCP 配置 +- 直接集成到后端服务 +- 提供真正的网络搜索能力 + +**缺点**: +- 需要第三方 API Key +- 功能相对简单 + +### 方案 3:启用 MCP 搜索工具(完整方案) + +启用已配置的 `minimax_search` MCP 工具: + +**步骤 1**:修改 `mini_agent/config/mcp.json` + +```json +{ + "mcpServers": { + "minimax_search": { + "description": "MiniMax Search - Powerful web search and intelligent browsing ⭐", + "type": "stdio", + "command": "uvx", + "args": [ + "--from", + "git+https://github.com/MiniMax-AI/minimax_search", + "minimax-search" + ], + "env": { + "JINA_API_KEY": "your-jina-api-key", + "SERPER_API_KEY": "your-serper-api-key", + "MINIMAX_API_KEY": "your-minimax-api-key" + }, + "disabled": false // 改为 false + } + } +} +``` + +**步骤 2**:在后端集成 MCP 工具 + +修改 `backend/app/services/agent_service.py:84-104`,实现 MCP 工具加载: + +```python +def _create_tools(self) -> List: + """创建工具列表""" + tools = [ + # ... 现有工具 ... + ] + + # 加载 MCP 工具 + from mini_agent.tools.mcp_loader import load_mcp_tools + mcp_config_path = Path(__file__).parent.parent.parent.parent / "mini_agent" / "config" / "mcp.json" + + if mcp_config_path.exists(): + try: + mcp_tools = load_mcp_tools(str(mcp_config_path)) + tools.extend(mcp_tools) + print(f" ✅ 加载了 {len(mcp_tools)} 个 MCP 工具") + except Exception as e: + print(f" ⚠️ MCP 工具加载失败: {e}") + + return tools +``` + +**优点**: +- 功能最完整(搜索 + 智能浏览) +- 与 CLI 版本保持一致 +- 支持多种搜索引擎 + +**缺点**: +- 配置相对复杂 +- 需要多个 API Key +- 需要 Node.js/Python 环境支持 + +--- + +## 推荐方案 + +### 对于您的情况 + +**推荐:方案 2(添加简单的网络搜索工具)** + +理由: +1. ✅ 快速实施(30 分钟内完成) +2. ✅ 满足基本需求(搜索最新信息) +3. ✅ 不需要复杂的 MCP 配置 +4. ✅ 成本低(Serper API 免费额度:2500 次/月) + +### 实施步骤 + +1. **获取 Serper API Key** + - 访问 https://serper.dev + - 注册并获取免费 API Key + +2. **创建搜索工具** + - 参考上面的 `WebSearchTool` 代码 + - 保存到 `backend/app/tools/web_search_tool.py` + +3. **集成到服务** + - 修改 `agent_service.py` + - 添加到 `.env` 配置 + +4. **重启服务** + - 重启后端 + - 测试搜索功能 + +--- + +## 其他工具优化建议 + +### 1. Skills 集成 + +目前 Skills 也未启用(`TODO: 添加 Skills`)。考虑启用: +- **pdf**: PDF 处理 +- **pptx**: PPT 生成 +- **docx**: Word 文档 +- **xlsx**: Excel 处理 + +### 2. 工具使用监控 + +添加工具使用统计,了解哪些工具最常用: +```python +# 在 agent_service.py 中添加 +self.tool_usage_stats = {} + +def _track_tool_usage(self, tool_name: str): + self.tool_usage_stats[tool_name] = self.tool_usage_stats.get(tool_name, 0) + 1 +``` + +--- + +## 总结 + +**当前工具都是有用的**,但针对您的使用场景(信息检索),缺少关键的**网络搜索能力**。 + +建议: +1. 立即实施**方案 2**,添加基础网络搜索 +2. 长期考虑**方案 3**,启用完整的 MCP 工具集 +3. 根据实际使用情况,启用 Skills + +这样 Agent 就能真正回答"昆仑万维有啥利好"这类需要实时信息的问题了! diff --git a/backend/TROUBLESHOOTING_WINDOWS.md b/backend/TROUBLESHOOTING_WINDOWS.md new file mode 100644 index 0000000..42297f5 --- /dev/null +++ b/backend/TROUBLESHOOTING_WINDOWS.md @@ -0,0 +1,377 @@ +# Mini-Agent Backend Windows 环境问题排查指南 + +## 问题概述 + +根据日志分析,发现了以下几个问题: + +### 1. BashTool 执行失败,错误信息为空 + +**症状**: +``` +🔧 Tool Call: bash + Arguments: + { + "command": "ls -la" + } +✗ Error: +``` + +**可能原因**: +1. **PowerShell 编码问题** - Windows PowerShell 的输出编码可能与 UTF-8 不兼容 +2. **工作空间路径过长** - Windows 路径长度限制可能导致问题 +3. **PowerShell 执行策略** - 可能需要调整 PowerShell 的执行策略 +4. **权限问题** - 工作空间目录可能没有执行权限 + +**解决方案**: + +#### 方案 1: 添加详细的错误日志 + +修改 `mini_agent/tools/bash_tool.py`,在 `execute` 方法中添加更详细的日志: + +```python +async def execute( + self, + command: str, + timeout: int = 120, + run_in_background: bool = False, +) -> ToolResult: + """Execute shell command with optional background execution.""" + + try: + # 添加调试日志 + print(f"[DEBUG] Executing command: {command}") + print(f"[DEBUG] Is Windows: {self.is_windows}") + print(f"[DEBUG] Workspace dir: {self.workspace_dir}") + + # ... 其余代码保持不变 ... + + # 在解码输出后添加日志 + stdout_text = stdout.decode("utf-8", errors="replace") + stderr_text = stderr.decode("utf-8", errors="replace") + + print(f"[DEBUG] Exit code: {process.returncode}") + print(f"[DEBUG] Stdout length: {len(stdout_text)}") + print(f"[DEBUG] Stderr length: {len(stderr_text)}") + + # ... 其余代码保持不变 ... +``` + +#### 方案 2: 修改 PowerShell 编码设置 + +在 `bash_tool.py` 的 Windows 命令构建部分添加编码参数: + +```python +if self.is_windows: + # Windows: Use PowerShell with UTF-8 encoding + shell_cmd = [ + "powershell.exe", + "-NoProfile", + "-OutputEncoding", "UTF8", + "-InputFormat", "Text", + "-Command", + command + ] +``` + +#### 方案 3: 简化工作空间路径 + +如果当前工作空间路径过长,考虑使用更短的路径。在 `.env` 文件中修改: + +```env +WORKSPACE_BASE="C:/mini-agent-data" +``` + +### 2. ReadTool 权限错误 + +**症状**: +``` +🔧 Tool Call: read_file + Arguments: + { + "path": "." + } +✗ Error: [Errno 13] Permission denied: 'C:\\Users\\...' +``` + +**原因**: +Agent 尝试读取目录 "." 而不是文件。ReadTool 只能读取文件,不能读取目录。 + +**解决方案**: + +这是 Agent 的行为问题,而不是代码问题。可以通过以下方式改进: + +1. **在 system prompt 中明确说明**:在 `mini_agent/config/system_prompt.md` 中添加: + +```markdown +## 文件操作注意事项 + +1. **read_file** 工具只能读取文件,不能读取目录 +2. 如果需要列出目录内容,使用 bash 工具: + - Windows: `dir` 或 `Get-ChildItem` + - Unix: `ls -la` +3. 不要尝试直接读取 "." 或 ".." +``` + +2. **改进 ReadTool 的错误提示**:修改 `mini_agent/tools/file_tools.py`: + +```python +async def execute(self, path: str, offset: int | None = None, limit: int | None = None) -> ToolResult: + """Execute read file.""" + try: + file_path = Path(path) + # Resolve relative paths relative to workspace_dir + if not file_path.is_absolute(): + file_path = self.workspace_dir / file_path + + # 添加目录检查 + if file_path.is_dir(): + return ToolResult( + success=False, + content="", + error=f"Cannot read directory: {path}. Use bash tool to list directory contents instead.", + ) + + if not file_path.exists(): + return ToolResult( + success=False, + content="", + error=f"File not found: {path}", + ) + + # ... 其余代码保持不变 ... +``` + +### 3. API 内容过滤错误 + +**症状**: +``` +Error code: 400 - {'contentFilter': [{'level': 1, 'role': 'assistant'}], +'error': {'code': '1301', 'message': '系统检测到输入或生成内容可能包含不安全或敏感内容'}} +``` + +**原因**: +用户查询 "昆仑万维有啥利好" 触发了 MiniMax API 的内容安全过滤。这是因为涉及股票投资建议等敏感内容。 + +**解决方案**: + +1. **修改查询方式** - 避免直接询问投资相关问题 +2. **调整 system prompt** - 在 prompt 中说明不提供投资建议 +3. **添加重试逻辑** - 当遇到内容过滤错误时,自动重新表述问题 + +在 `mini_agent/llm/anthropic_client.py` 或 `mini_agent/llm/openai_client.py` 中添加特殊处理: + +```python +# 检查是否是内容过滤错误 +if "contentFilter" in error_response or "1301" in str(error_response): + # 返回一个友好的错误信息 + raise Exception( + "检测到敏感内容。请尝试重新表述您的问题,避免涉及投资建议、敏感话题等内容。" + ) +``` + +### 4. ZHIPU_API_KEY 未配置 + +**症状**: +``` + ℹ️ 未配置 ZHIPU_API_KEY,跳过搜索工具 +``` + +**说明**: +这不是错误,只是一个信息提示。如果需要使用智谱 AI 的搜索工具,需要配置此 API Key。 + +**解决方案**: + +如果需要使用搜索功能: + +1. **获取智谱 AI API Key**: + - 访问 https://open.bigmodel.cn/ + - 注册账号并获取 API Key + +2. **配置环境变量**: + 在 `backend/.env` 文件中添加: + ```env + ZHIPU_API_KEY="your-zhipuai-api-key-here" + ``` + +3. **安装依赖**(如果还没有安装): + ```bash + pip install zhipuai + ``` + +4. **重启服务**: + ```bash + uvicorn app.main:app --reload + ``` + +如果不需要搜索功能,可以忽略这个提示。 + +## 完整诊断脚本 + +创建一个 Windows 专用的诊断脚本 `backend/diagnose_windows.py`: + +```python +#!/usr/bin/env python3 +"""Windows 环境专用诊断脚本""" +import sys +import os +import platform +from pathlib import Path +import asyncio + +print("🔍 Mini-Agent Windows 环境诊断\n") +print("=" * 60) + +# 1. 检查操作系统 +print("\n1️⃣ 检查操作系统") +print(f" 系统: {platform.system()}") +print(f" 版本: {platform.version()}") +print(f" 架构: {platform.machine()}") + +if platform.system() != "Windows": + print(" ⚠️ 此脚本专为 Windows 设计") +else: + print(" ✅ Windows 环境") + +# 2. 检查 PowerShell +print("\n2️⃣ 检查 PowerShell") +try: + import subprocess + result = subprocess.run( + ["powershell.exe", "-NoProfile", "-Command", "echo 'Test'"], + capture_output=True, + text=True, + timeout=5 + ) + if result.returncode == 0: + print(" ✅ PowerShell 可用") + print(f" 输出: {result.stdout.strip()}") + else: + print(f" ❌ PowerShell 执行失败: {result.stderr}") +except Exception as e: + print(f" ❌ PowerShell 测试失败: {e}") + +# 3. 检查工作空间路径 +print("\n3️⃣ 检查工作空间配置") +try: + from app.config import get_settings + settings = get_settings() + workspace_base = Path(settings.workspace_base) + print(f" 工作空间基础路径: {workspace_base}") + print(f" 路径长度: {len(str(workspace_base.absolute()))} 字符") + + if len(str(workspace_base.absolute())) > 200: + print(" ⚠️ 路径过长,可能导致问题") + else: + print(" ✅ 路径长度正常") + + if workspace_base.exists(): + print(" ✅ 工作空间目录存在") + else: + print(" ⚠️ 工作空间目录不存在,将自动创建") + workspace_base.mkdir(parents=True, exist_ok=True) +except Exception as e: + print(f" ❌ 工作空间检查失败: {e}") + +# 4. 测试 BashTool +print("\n4️⃣ 测试 BashTool") +try: + sys.path.insert(0, str(Path(__file__).parent.parent)) + from mini_agent.tools.bash_tool import BashTool + + bash_tool = BashTool(workspace_dir=str(workspace_base)) + print(f" 使用 Shell: {bash_tool.shell_name}") + print(f" 是否 Windows: {bash_tool.is_windows}") + + # 测试简单命令 + async def test_bash(): + result = await bash_tool.execute("echo 'Hello'", timeout=10) + return result + + result = asyncio.run(test_bash()) + if result.success: + print(" ✅ BashTool 测试成功") + print(f" 输出: {result.stdout[:100]}") + else: + print(f" ❌ BashTool 测试失败") + print(f" 错误: {result.error}") + print(f" Stdout: {result.stdout}") + print(f" Stderr: {result.stderr}") + print(f" Exit code: {result.exit_code}") +except Exception as e: + print(f" ❌ BashTool 测试失败: {e}") + import traceback + traceback.print_exc() + +# 5. 检查环境变量 +print("\n5️⃣ 检查环境变量") +env_vars = ["LLM_API_KEY", "LLM_API_BASE", "LLM_MODEL", "ZHIPU_API_KEY"] +for var in env_vars: + value = os.getenv(var) + if value: + if "KEY" in var: + masked = value[:8] + "..." + value[-4:] if len(value) > 12 else "***" + print(f" ✅ {var}: {masked}") + else: + print(f" ✅ {var}: {value}") + else: + if var == "ZHIPU_API_KEY": + print(f" ℹ️ {var}: 未配置(可选)") + else: + print(f" ❌ {var}: 未配置") + +print("\n" + "=" * 60) +print("✅ 诊断完成!") +``` + +## 快速修复步骤 + +1. **创建诊断脚本**: + ```bash + # 将上面的脚本保存为 backend/diagnose_windows.py + python backend/diagnose_windows.py + ``` + +2. **根据诊断结果修复问题**: + - 如果 PowerShell 不可用,检查系统环境变量 + - 如果路径过长,修改 `.env` 中的 `WORKSPACE_BASE` + - 如果 BashTool 失败,查看详细错误信息 + +3. **添加调试日志**: + 在测试期间,建议在 `bash_tool.py` 中添加详细的调试输出 + +4. **配置 ZHIPU_API_KEY**(可选): + 如果需要搜索功能,在 `.env` 中添加智谱 AI 的 API Key + +5. **重启服务并测试**: + ```bash + uvicorn app.main:app --reload + ``` + +## 常见问题 + +### Q1: 为什么所有 bash 命令都失败? +A: 可能是以下原因之一: +- PowerShell 执行策略限制 +- 工作空间路径权限问题 +- 编码问题导致输出无法正确解析 + +### Q2: 如何避免内容过滤错误? +A: +- 避免询问投资建议、政治敏感话题 +- 重新表述问题,使用更中性的语言 +- 在 system prompt 中明确说明不提供敏感内容 + +### Q3: 是否必须配置 ZHIPU_API_KEY? +A: 不是必须的。这是可选功能,只有需要使用智谱 AI 搜索工具时才需要配置。 + +## 后续改进建议 + +1. **添加 Windows 特定的测试用例** +2. **改进错误处理和日志记录** +3. **在 system prompt 中添加更详细的工具使用指南** +4. **考虑添加配置验证工具** +5. **为常见错误添加自动恢复机制** + +--- + +**最后更新**: 2025-01-17 diff --git a/backend/app/__init__.py b/backend/app/__init__.py new file mode 100644 index 0000000..025bafd --- /dev/null +++ b/backend/app/__init__.py @@ -0,0 +1,2 @@ +"""Mini-Agent Backend""" +__version__ = "0.1.0" diff --git a/backend/app/api/__init__.py b/backend/app/api/__init__.py new file mode 100644 index 0000000..fb31fe2 --- /dev/null +++ b/backend/app/api/__init__.py @@ -0,0 +1 @@ +"""API 路由""" diff --git a/backend/app/api/auth.py b/backend/app/api/auth.py new file mode 100644 index 0000000..16079db --- /dev/null +++ b/backend/app/api/auth.py @@ -0,0 +1,47 @@ +"""简单认证 API""" +from fastapi import APIRouter, HTTPException, Form +from app.schemas.auth import LoginResponse +from app.config import get_settings + +router = APIRouter() +settings = get_settings() + + +@router.post("/login", response_model=LoginResponse) +async def login(username: str = Form(...), password: str = Form(...)): + """ + 简单登录接口 + + 返回用户信息(username 作为 user_id) + """ + # 获取配置的用户列表 + auth_users = settings.get_auth_users() + + # 验证用户名和密码 + if username not in auth_users: + raise HTTPException(status_code=401, detail="用户名或密码错误") + + if auth_users[username] != password: + raise HTTPException(status_code=401, detail="用户名或密码错误") + + # 登录成功,返回用户信息 + # 使用 username 作为 session_id(简化方案) + return LoginResponse( + session_id=username, + message="登录成功", + ) + + +@router.get("/me") +async def get_current_user(user_id: str): + """ + 获取当前用户信息(简化版) + + 前端需要在查询参数中传递 user_id + """ + auth_users = settings.get_auth_users() + + if user_id not in auth_users: + raise HTTPException(status_code=404, detail="用户不存在") + + return {"user_id": user_id, "username": user_id} diff --git a/backend/app/api/chat.py b/backend/app/api/chat.py new file mode 100644 index 0000000..2f1e005 --- /dev/null +++ b/backend/app/api/chat.py @@ -0,0 +1,100 @@ +"""对话 API""" +from fastapi import APIRouter, Depends, HTTPException, Query +from sqlalchemy.orm import Session as DBSession +from app.models.database import get_db +from app.models.session import Session +from app.schemas.chat import SendMessageRequest, SendMessageResponse +from app.services.agent_service import AgentService +from app.services.history_service import HistoryService +from app.services.workspace_service import WorkspaceService +from datetime import datetime + +router = APIRouter() + +# 内存中的 Agent 实例缓存 +_agent_cache: dict[str, AgentService] = {} + + +@router.post("/{chat_session_id}/message", response_model=SendMessageResponse) +async def send_message( + chat_session_id: str, + request: SendMessageRequest, + session_id: str = Query(..., description="Session ID (user_id)"), + db: DBSession = Depends(get_db), +): + """发送消息并获取响应""" + # 验证会话 + session = ( + db.query(Session) + .filter(Session.id == chat_session_id, Session.user_id == session_id) + .first() + ) + + if not session: + raise HTTPException(status_code=404, detail="会话不存在") + + if session.status == "completed": + raise HTTPException(status_code=410, detail="会话已完成") + + # 获取或创建 Agent Service + if chat_session_id not in _agent_cache: + try: + workspace_service = WorkspaceService() + workspace_dir = workspace_service._get_session_dir(session_id, chat_session_id) + + history_service = HistoryService(db) + agent_service = AgentService(workspace_dir, history_service, chat_session_id) + + # 初始化 Agent + print(f"🔧 正在初始化 Agent...") + agent_service.initialize_agent() + print(f"✅ Agent 初始化成功") + + _agent_cache[chat_session_id] = agent_service + except Exception as e: + import traceback + error_detail = traceback.format_exc() + print(f"\n{'='*60}") + print(f"❌ Agent 初始化失败") + print(f"{'='*60}") + print(f"错误类型: {type(e).__name__}") + print(f"错误信息: {str(e)}") + print(f"\n详细堆栈:\n{error_detail}") + print(f"{'='*60}\n") + + # 返回更详细的错误信息给前端 + error_msg = f"Agent 初始化失败: {type(e).__name__}: {str(e)}" + if "api_key" in str(e).lower() or "apikey" in str(e).lower(): + error_msg += "\n\n💡 提示:请检查 .env 文件中的 LLM_API_KEY 配置是否正确" + raise HTTPException(status_code=500, detail=error_msg) + else: + agent_service = _agent_cache[chat_session_id] + + # 执行对话 + try: + print(f"🤖 开始执行对话...") + result = await agent_service.chat(request.message) + print(f"✅ 对话执行完成") + except Exception as e: + import traceback + error_detail = traceback.format_exc() + print(f"\n{'='*60}") + print(f"❌ 对话执行失败") + print(f"{'='*60}") + print(f"错误类型: {type(e).__name__}") + print(f"错误信息: {str(e)}") + print(f"\n详细堆栈:\n{error_detail}") + print(f"{'='*60}\n") + + # 返回更详细的错误信息给前端 + error_msg = f"对话执行失败: {type(e).__name__}: {str(e)}" + raise HTTPException(status_code=500, detail=error_msg) + + # 更新会话活跃时间 + session.updated_at = datetime.utcnow() + db.commit() + + return SendMessageResponse( + message=request.message, + response=result["response"], + ) diff --git a/backend/app/api/sessions.py b/backend/app/api/sessions.py new file mode 100644 index 0000000..b72bef6 --- /dev/null +++ b/backend/app/api/sessions.py @@ -0,0 +1,113 @@ +"""会话管理 API""" +from fastapi import APIRouter, Depends, HTTPException, Query +from sqlalchemy.orm import Session as DBSession +from app.models.database import get_db +from app.models.session import Session +from app.models.message import Message +from app.schemas.session import CreateSessionResponse, SessionResponse, SessionListResponse +from app.schemas.message import MessageHistoryResponse +from app.services.workspace_service import WorkspaceService +from datetime import datetime +import uuid + +router = APIRouter() + + +@router.post("/create", response_model=CreateSessionResponse) +async def create_session( + session_id: str = Query(..., description="Session ID (user_id)"), + db: DBSession = Depends(get_db), +): + """创建新会话""" + # 创建会话 + chat_session_id = str(uuid.uuid4()) + session = Session( + id=chat_session_id, user_id=session_id, title="新会话" + ) + db.add(session) + db.commit() + db.refresh(session) + + # 创建工作空间 + workspace_service = WorkspaceService() + workspace_service.create_session_workspace(session_id, chat_session_id) + + return CreateSessionResponse( + session_id=chat_session_id, + message="会话创建成功" + ) + + +@router.get("/list", response_model=SessionListResponse) +async def list_sessions( + session_id: str = Query(..., description="Session ID (user_id)"), + db: DBSession = Depends(get_db), +): + """获取用户的会话列表""" + sessions = ( + db.query(Session) + .filter(Session.user_id == session_id) + .order_by(Session.updated_at.desc()) + .all() + ) + + return SessionListResponse(sessions=sessions) + + +@router.get("/{chat_session_id}/history", response_model=MessageHistoryResponse) +async def get_session_history( + chat_session_id: str, + session_id: str = Query(..., description="Session ID (user_id)"), + db: DBSession = Depends(get_db), +): + """获取会话的消息历史""" + # 验证会话属于该用户 + session = ( + db.query(Session) + .filter(Session.id == chat_session_id, Session.user_id == session_id) + .first() + ) + + if not session: + raise HTTPException(status_code=404, detail="会话不存在") + + # 获取消息历史 + messages = ( + db.query(Message) + .filter(Message.session_id == chat_session_id) + .order_by(Message.created_at.asc()) + .all() + ) + + return MessageHistoryResponse(messages=messages) + + +@router.delete("/{chat_session_id}") +async def delete_session( + chat_session_id: str, + session_id: str = Query(..., description="Session ID (user_id)"), + db: DBSession = Depends(get_db), +): + """删除会话""" + # 验证会话属于该用户 + session = ( + db.query(Session) + .filter(Session.id == chat_session_id, Session.user_id == session_id) + .first() + ) + + if not session: + raise HTTPException(status_code=404, detail="会话不存在") + + # 删除消息 + db.query(Message).filter(Message.session_id == chat_session_id).delete() + + # 删除会话 + db.delete(session) + db.commit() + + # 清理工作空间 + workspace_service = WorkspaceService() + workspace_service.cleanup_session(session_id, chat_session_id, preserve_files=False) + + return {"message": "会话已删除"} diff --git a/backend/app/config.py b/backend/app/config.py new file mode 100644 index 0000000..9c004f3 --- /dev/null +++ b/backend/app/config.py @@ -0,0 +1,77 @@ +"""应用配置""" +from pydantic_settings import BaseSettings +from functools import lru_cache +from pathlib import Path +from typing import List + + +class Settings(BaseSettings): + """应用配置""" + + # 应用配置 + app_name: str = "Mini-Agent Backend" + app_version: str = "0.1.0" + debug: bool = False + + # API 配置 + api_prefix: str = "/api" + cors_origins: List[str] = ["http://localhost:3000"] + + # 简单认证(临时方案,格式:username:password,username2:password2) + simple_auth_users: str = "demo:demo123" + + # 数据库配置 + database_url: str = "sqlite:///./data/database/mini_agent.db" + + # LLM API 配置(支持 MiniMax、GLM、OpenAI 等) + llm_api_key: str # API 密钥 + llm_api_base: str = "https://api.minimax.chat" # API 基础地址 + llm_model: str = "MiniMax-Text-01" # 模型名称 + llm_provider: str = "anthropic" # 提供商:anthropic 或 openai + + # 搜索工具配置(可选) + zhipu_api_key: str = "" # 智谱 AI API 密钥,用于搜索工具 + + # 工作空间配置 + workspace_base: Path = Path("./data/workspaces") + shared_env_path: Path = Path("./data/shared_env/base.venv") + allowed_packages_file: Path = Path("./data/shared_env/allowed_packages.txt") + + # Agent 配置 + agent_max_steps: int = 100 + agent_token_limit: int = 80000 + + # 会话配置 + session_inactive_timeout_hours: int = 1 + session_max_duration_hours: int = 24 + session_max_turns: int = 50 + + # 文件保留配置 + preserve_file_extensions: List[str] = [ + ".pdf", + ".xlsx", + ".pptx", + ".docx", + ".png", + ".jpg", + ".jpeg", + ] + + class Config: + env_file = ".env" + env_file_encoding = "utf-8" + + def get_auth_users(self) -> dict[str, str]: + """解析简单认证用户列表""" + users = {} + for user_pair in self.simple_auth_users.split(","): + if ":" in user_pair: + username, password = user_pair.split(":", 1) + users[username.strip()] = password.strip() + return users + + +@lru_cache() +def get_settings() -> Settings: + """获取配置(单例)""" + return Settings() diff --git a/backend/app/main.py b/backend/app/main.py new file mode 100644 index 0000000..7d9b250 --- /dev/null +++ b/backend/app/main.py @@ -0,0 +1,90 @@ +"""FastAPI 主应用""" +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware +from app.config import get_settings +from app.api import auth, sessions, chat +from app.models.database import init_db +from app.utils.init_env import init_shared_env, check_shared_env +from pathlib import Path + +settings = get_settings() + +# 创建 FastAPI 应用 +app = FastAPI( + title=settings.app_name, + version=settings.app_version, + docs_url=f"{settings.api_prefix}/docs", + redoc_url=f"{settings.api_prefix}/redoc", +) + +# CORS 中间件 +app.add_middleware( + CORSMiddleware, + allow_origins=settings.cors_origins, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + + +# 启动事件 +@app.on_event("startup") +async def startup_event(): + """应用启动时执行""" + # 初始化数据库 + init_db() + print(f"✅ 数据库初始化完成") + + # 初始化共享环境 + shared_env_dir = Path(settings.workspace_base).parent / "shared_env" + venv_dir = shared_env_dir / "base.venv" + + if not check_shared_env(venv_dir): + print("🔨 首次启动,正在初始化共享环境...") + print(" 这可能需要几分钟时间(只会执行一次)") + + packages_file = shared_env_dir / "allowed_packages.txt" + success = init_shared_env( + base_dir=shared_env_dir, + packages_file=packages_file if packages_file.exists() else None, + force=False + ) + + if success: + print("✅ 共享环境初始化完成") + else: + print("⚠️ 共享环境初始化失败,部分功能可能不可用") + else: + print("✅ 共享环境已就绪") + + print(f"✅ {settings.app_name} v{settings.app_version} 启动成功") + + +# 路由 +app.include_router(auth.router, prefix=f"{settings.api_prefix}/auth", tags=["认证"]) +app.include_router( + sessions.router, prefix=f"{settings.api_prefix}/sessions", tags=["会话管理"] +) +app.include_router(chat.router, prefix=f"{settings.api_prefix}/chat", tags=["对话"]) + + +# 根路径 +@app.get("/") +async def root(): + return { + "message": "Mini-Agent API", + "version": settings.app_version, + "docs": f"{settings.api_prefix}/docs", + } + + +# 健康检查 +@app.get("/health") +async def health(): + return {"status": "healthy", "version": settings.app_version} + + +if __name__ == "__main__": + import uvicorn + + uvicorn.run(app, host="0.0.0.0", port=8000, reload=settings.debug) diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py new file mode 100644 index 0000000..3319d6f --- /dev/null +++ b/backend/app/models/__init__.py @@ -0,0 +1,6 @@ +"""数据模型""" +from .database import Base, get_db, init_db +from .session import Session +from .message import Message + +__all__ = ["Base", "get_db", "init_db", "Session", "Message"] diff --git a/backend/app/models/database.py b/backend/app/models/database.py new file mode 100644 index 0000000..e0d36e2 --- /dev/null +++ b/backend/app/models/database.py @@ -0,0 +1,39 @@ +"""数据库配置""" +from sqlalchemy import create_engine +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import sessionmaker +from pathlib import Path + +# 确保数据库目录存在 +db_dir = Path("./data/database") +db_dir.mkdir(parents=True, exist_ok=True) + +# 数据库URL +DATABASE_URL = "sqlite:///./data/database/mini_agent.db" + +# 创建引擎 +engine = create_engine( + DATABASE_URL, + connect_args={"check_same_thread": False}, # SQLite 需要 + echo=False, # 生产环境设为 False +) + +# 会话工厂 +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + +# Base 类 +Base = declarative_base() + + +def get_db(): + """依赖注入:获取数据库会话""" + db = SessionLocal() + try: + yield db + finally: + db.close() + + +def init_db(): + """初始化数据库(创建所有表)""" + Base.metadata.create_all(bind=engine) diff --git a/backend/app/models/message.py b/backend/app/models/message.py new file mode 100644 index 0000000..fc29ab1 --- /dev/null +++ b/backend/app/models/message.py @@ -0,0 +1,24 @@ +"""消息数据模型""" +from sqlalchemy import Column, String, Text, DateTime, ForeignKey +from datetime import datetime +from .database import Base + + +class Message(Base): + """消息表""" + + __tablename__ = "messages" + + id = Column(String(36), primary_key=True) # 改为 String UUID + session_id = Column( + String(36), + ForeignKey("sessions.id", ondelete="CASCADE"), + nullable=False, + index=True, + ) + role = Column(String(20), nullable=False) # system, user, assistant, tool + content = Column(Text, nullable=True) + thinking = Column(Text, nullable=True) + tool_calls = Column(Text, nullable=True) # JSON string + tool_call_id = Column(String(100), nullable=True) + created_at = Column(DateTime, default=datetime.utcnow, index=True) diff --git a/backend/app/models/session.py b/backend/app/models/session.py new file mode 100644 index 0000000..184f33a --- /dev/null +++ b/backend/app/models/session.py @@ -0,0 +1,17 @@ +"""会话数据模型""" +from sqlalchemy import Column, String, Integer, DateTime +from datetime import datetime +from .database import Base + + +class Session(Base): + """会话表""" + + __tablename__ = "sessions" + + id = Column(String(36), primary_key=True) + user_id = Column(String(100), nullable=False, index=True) # 简化为username + created_at = Column(DateTime, default=datetime.utcnow, index=True) + updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) # 前端期望的字段名 + status = Column(String(20), default="active", index=True) # active, paused, completed + title = Column(String(255), nullable=True) diff --git a/backend/app/schemas/__init__.py b/backend/app/schemas/__init__.py new file mode 100644 index 0000000..bf4b251 --- /dev/null +++ b/backend/app/schemas/__init__.py @@ -0,0 +1 @@ +"""Pydantic Schemas""" diff --git a/backend/app/schemas/auth.py b/backend/app/schemas/auth.py new file mode 100644 index 0000000..fea8045 --- /dev/null +++ b/backend/app/schemas/auth.py @@ -0,0 +1,16 @@ +"""认证相关 Schema""" +from pydantic import BaseModel + + +class LoginRequest(BaseModel): + """登录请求""" + + username: str + password: str + + +class LoginResponse(BaseModel): + """登录响应""" + + session_id: str # 前端期望的字段名 + message: str = "登录成功" diff --git a/backend/app/schemas/chat.py b/backend/app/schemas/chat.py new file mode 100644 index 0000000..72198a8 --- /dev/null +++ b/backend/app/schemas/chat.py @@ -0,0 +1,50 @@ +"""对话相关 Schema""" +from pydantic import BaseModel, Field +from typing import Optional, List + + +class ChatRequest(BaseModel): + """对话请求""" + + message: str = Field(..., min_length=1, max_length=10000, description="用户消息") + + +class SendMessageRequest(BaseModel): + """发送消息请求(前端期望的格式)""" + + message: str = Field(..., min_length=1, max_length=10000, description="用户消息") + + +class SendMessageResponse(BaseModel): + """发送消息响应(前端期望的格式)""" + + message: str # 用户发送的消息 + response: str # AI 的响应 + + +class ChatResponse(BaseModel): + """对话响应""" + + session_id: str + message: str + thinking: Optional[str] = None + files: List[str] = [] + turn: int + message_count: int + + +class MessageHistory(BaseModel): + """消息历史""" + + role: str + content: Optional[str] + thinking: Optional[str] = None + created_at: str + + +class HistoryResponse(BaseModel): + """历史记录响应""" + + session_id: str + messages: List[MessageHistory] + total: int diff --git a/backend/app/schemas/message.py b/backend/app/schemas/message.py new file mode 100644 index 0000000..2d06e98 --- /dev/null +++ b/backend/app/schemas/message.py @@ -0,0 +1,29 @@ +"""消息相关 Schema""" +from pydantic import BaseModel, field_validator +from datetime import datetime +from typing import List, Union + + +class MessageResponse(BaseModel): + """消息响应""" + + id: str + session_id: str + role: str # "user" | "assistant" | "system" + content: str + created_at: datetime + + @field_validator('id', 'session_id', mode='before') + @classmethod + def convert_to_string(cls, v: Union[str, int]) -> str: + """将 id 转换为字符串(兼容旧的整数 ID)""" + return str(v) + + class Config: + from_attributes = True + + +class MessageHistoryResponse(BaseModel): + """消息历史响应""" + + messages: List[MessageResponse] diff --git a/backend/app/schemas/session.py b/backend/app/schemas/session.py new file mode 100644 index 0000000..a118094 --- /dev/null +++ b/backend/app/schemas/session.py @@ -0,0 +1,37 @@ +"""会话相关 Schema""" +from pydantic import BaseModel, Field +from datetime import datetime +from typing import Optional + + +class SessionCreate(BaseModel): + """创建会话请求""" + + title: Optional[str] = None + + +class CreateSessionResponse(BaseModel): + """创建会话响应""" + + session_id: str + message: str = "会话创建成功" + + +class SessionResponse(BaseModel): + """会话响应""" + + id: str + user_id: str + status: str + created_at: datetime + updated_at: datetime # 前端期望 updated_at 而不是 last_active + title: Optional[str] = None + + class Config: + from_attributes = True + + +class SessionListResponse(BaseModel): + """会话列表响应""" + + sessions: list[SessionResponse] diff --git a/backend/app/services/__init__.py b/backend/app/services/__init__.py new file mode 100644 index 0000000..9e5a97e --- /dev/null +++ b/backend/app/services/__init__.py @@ -0,0 +1 @@ +"""业务服务""" diff --git a/backend/app/services/agent_service.py b/backend/app/services/agent_service.py new file mode 100644 index 0000000..b3488b0 --- /dev/null +++ b/backend/app/services/agent_service.py @@ -0,0 +1,225 @@ +"""Agent 服务 - 连接 Mini-Agent 核心""" +import sys +import os +from pathlib import Path + +# 添加 mini_agent 到 Python 路径 +mini_agent_path = Path(__file__).parent.parent.parent.parent / "mini_agent" +if str(mini_agent_path) not in sys.path: + sys.path.insert(0, str(mini_agent_path.parent)) + +from mini_agent.agent import Agent +from mini_agent.llm import LLMClient +from mini_agent.schema import LLMProvider, Message as AgentMessage +from mini_agent.tools.file_tools import ReadTool, WriteTool, EditTool +from mini_agent.tools.bash_tool import BashTool, BashOutputTool, BashKillTool +from mini_agent.tools.note_tool import SessionNoteTool +from mini_agent.tools.skill_loader import SkillLoader +from mini_agent.tools.skill_tool import GetSkillTool + +from app.services.history_service import HistoryService +from app.config import get_settings +from typing import List, Dict +from pathlib import Path as PathlibPath + +settings = get_settings() + + +class AgentService: + """Agent 服务""" + + def __init__( + self, workspace_dir: PathlibPath, history_service: HistoryService, session_id: str + ): + self.workspace_dir = workspace_dir + self.history_service = history_service + self.session_id = session_id + self.agent: Agent | None = None + self._last_saved_index = 0 + + def initialize_agent(self): + """初始化 Agent""" + # 根据配置确定 provider + if settings.llm_provider.lower() == "openai": + provider = LLMProvider.OPENAI + else: + provider = LLMProvider.ANTHROPIC + + # 创建 LLM 客户端 + llm_client = LLMClient( + api_key=settings.llm_api_key, + api_base=settings.llm_api_base, + provider=provider, + model=settings.llm_model, + ) + + # 加载 system prompt + system_prompt = self._load_system_prompt() + + # 创建工具列表 + tools = self._create_tools() + + # 创建 Agent + self.agent = Agent( + llm_client=llm_client, + system_prompt=system_prompt, + tools=tools, + max_steps=settings.agent_max_steps, + workspace_dir=str(self.workspace_dir), + token_limit=settings.agent_token_limit, + ) + + # 从数据库恢复历史 + self._restore_history() + + def _load_system_prompt(self) -> str: + """加载 system prompt""" + prompt_file = ( + Path(__file__).parent.parent.parent.parent + / "mini_agent" + / "config" + / "system_prompt.md" + ) + if prompt_file.exists(): + return prompt_file.read_text(encoding="utf-8") + return "You are Mini-Agent, an AI assistant." + + def _create_tools(self) -> List: + """创建工具列表""" + tools = [ + # 文件工具 + ReadTool(workspace_dir=str(self.workspace_dir)), + WriteTool(workspace_dir=str(self.workspace_dir)), + EditTool(workspace_dir=str(self.workspace_dir)), + # Bash 工具 + BashTool(workspace_dir=str(self.workspace_dir)), + BashOutputTool(), + BashKillTool(), + # 会话笔记工具 + SessionNoteTool( + memory_file=str(self.workspace_dir / ".agent_memory.json") + ), + ] + + # 添加搜索工具(如果配置了 API Key) + zhipu_api_key = settings.zhipu_api_key + if zhipu_api_key and zhipu_api_key.strip(): + try: + from mini_agent.tools.glm_search_tool import GLMSearchTool, GLMBatchSearchTool + tools.append(GLMSearchTool(api_key=zhipu_api_key)) + tools.append(GLMBatchSearchTool(api_key=zhipu_api_key)) + print(f" ✅ 已加载 GLM 搜索工具") + except Exception as e: + print(f" ⚠️ GLM 搜索工具加载失败: {e}") + else: + print(f" ℹ️ 未配置 ZHIPU_API_KEY,跳过搜索工具") + + # 添加 Skills + try: + skills_dir = Path(__file__).parent.parent.parent.parent / "mini_agent" / "skills" + if skills_dir.exists(): + skill_loader = SkillLoader(str(skills_dir)) + tools.append(GetSkillTool(skill_loader)) + skill_count = len(skill_loader.list_skills()) + print(f" ✅ 已加载 {skill_count} 个 Skills") + else: + print(f" ⚠️ Skills 目录不存在: {skills_dir}") + except Exception as e: + print(f" ⚠️ Skills 加载失败: {e}") + + # TODO: 添加 MCP tools + + return tools + + def _restore_history(self): + """从数据库恢复对话历史""" + if not self.agent: + return + + history = self.history_service.load_session_history(self.session_id) + + # 跳过 system message(index 0) + for msg_data in history: + if msg_data["role"] == "user": + self.agent.messages.append( + AgentMessage(role="user", content=msg_data["content"]) + ) + elif msg_data["role"] == "assistant": + self.agent.messages.append( + AgentMessage( + role="assistant", + content=msg_data["content"], + thinking=msg_data.get("thinking"), + tool_calls=msg_data.get("tool_calls"), + ) + ) + elif msg_data["role"] == "tool": + self.agent.messages.append( + AgentMessage( + role="tool", + content=msg_data["content"], + tool_call_id=msg_data.get("tool_call_id"), + ) + ) + + self._last_saved_index = len(self.agent.messages) + + async def chat(self, user_message: str) -> Dict: + """执行对话""" + if not self.agent: + raise RuntimeError("Agent not initialized") + + # 保存用户消息 + self.history_service.save_message( + session_id=self.session_id, role="user", content=user_message + ) + + # 添加到 agent + self.agent.add_user_message(user_message) + + # 执行 agent(添加详细错误处理) + try: + response = await self.agent.run() + except Exception as e: + # 记录详细错误 + import traceback + error_detail = traceback.format_exc() + print(f"\n{'='*60}") + print(f"❌ Agent 执行错误") + print(f"{'='*60}") + print(f"错误类型: {type(e).__name__}") + print(f"错误信息: {str(e)}") + print(f"\n详细堆栈:\n{error_detail}") + print(f"{'='*60}\n") + raise RuntimeError(f"Agent执行失败: {str(e)}") + + # 保存 agent 生成的消息 + self._save_new_messages() + + return {"response": response, "message_count": len(self.agent.messages)} + + def _save_new_messages(self): + """保存新增的消息到数据库""" + if not self.agent: + return + + for msg in self.agent.messages[self._last_saved_index :]: + if msg.role == "assistant": + self.history_service.save_message( + session_id=self.session_id, + role="assistant", + content=msg.content, + thinking=msg.thinking, + tool_calls=[tc.dict() for tc in msg.tool_calls] + if msg.tool_calls + else None, + ) + elif msg.role == "tool": + self.history_service.save_message( + session_id=self.session_id, + role="tool", + content=msg.content, + tool_call_id=msg.tool_call_id, + ) + + self._last_saved_index = len(self.agent.messages) diff --git a/backend/app/services/history_service.py b/backend/app/services/history_service.py new file mode 100644 index 0000000..26a852e --- /dev/null +++ b/backend/app/services/history_service.py @@ -0,0 +1,66 @@ +"""对话历史服务""" +from sqlalchemy.orm import Session as DBSession +from app.models.message import Message +from app.models.session import Session +from typing import List, Dict, Optional +import json +import uuid + + +class HistoryService: + """对话历史服务""" + + def __init__(self, db: DBSession): + self.db = db + + def save_message( + self, + session_id: str, + role: str, + content: Optional[str] = None, + thinking: Optional[str] = None, + tool_calls: Optional[List[Dict]] = None, + tool_call_id: Optional[str] = None, + ) -> Message: + """保存消息到数据库""" + message = Message( + id=str(uuid.uuid4()), # 生成 UUID + session_id=session_id, + role=role, + content=content, + thinking=thinking, + tool_calls=json.dumps(tool_calls, ensure_ascii=False) + if tool_calls + else None, + tool_call_id=tool_call_id, + ) + self.db.add(message) + self.db.commit() + self.db.refresh(message) + + return message + + def load_session_history(self, session_id: str) -> List[Dict]: + """加载会话历史""" + messages = ( + self.db.query(Message) + .filter(Message.session_id == session_id) + .order_by(Message.created_at) + .all() + ) + + return [ + { + "role": msg.role, + "content": msg.content, + "thinking": msg.thinking, + "tool_calls": json.loads(msg.tool_calls) if msg.tool_calls else None, + "tool_call_id": msg.tool_call_id, + "created_at": msg.created_at.isoformat(), + } + for msg in messages + ] + + def get_message_count(self, session_id: str) -> int: + """获取消息数量""" + return self.db.query(Message).filter(Message.session_id == session_id).count() diff --git a/backend/app/services/workspace_service.py b/backend/app/services/workspace_service.py new file mode 100644 index 0000000..3f95958 --- /dev/null +++ b/backend/app/services/workspace_service.py @@ -0,0 +1,85 @@ +"""工作空间管理服务""" +from pathlib import Path +import shutil +from typing import List +from datetime import datetime +from app.config import get_settings + +settings = get_settings() + + +class WorkspaceService: + """工作空间管理服务""" + + def __init__(self): + self.base_path = settings.workspace_base + self.base_path.mkdir(parents=True, exist_ok=True) + + def create_session_workspace(self, user_id: str, session_id: str) -> Path: + """创建会话工作空间""" + session_dir = self._get_session_dir(user_id, session_id) + + # 创建目录结构 + session_dir.mkdir(parents=True, exist_ok=True) + (session_dir / "files").mkdir(exist_ok=True) + (session_dir / "logs").mkdir(exist_ok=True) + + # 创建符号链接到 shared_files + shared_dir = self._get_user_shared_dir(user_id) + shared_dir.mkdir(parents=True, exist_ok=True) + + shared_link = session_dir / "shared" + if not shared_link.exists(): + try: + shared_link.symlink_to(shared_dir, target_is_directory=True) + except Exception: + # Windows 可能不支持符号链接,跳过 + pass + + return session_dir + + def cleanup_session( + self, user_id: str, session_id: str, preserve_files: bool = True + ) -> List[str]: + """清理会话工作空间""" + session_dir = self._get_session_dir(user_id, session_id) + preserved_files = [] + + if preserve_files: + # 保留特定格式的文件 + files_dir = session_dir / "files" + if files_dir.exists(): + for file in files_dir.iterdir(): + if ( + file.is_file() + and file.suffix.lower() in settings.preserve_file_extensions + ): + # 移动到 shared_files/outputs + dest_dir = self._get_user_shared_dir(user_id) / "outputs" + dest_dir.mkdir(parents=True, exist_ok=True) + + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + dest_file = dest_dir / f"{timestamp}_{file.name}" + shutil.copy2(file, dest_file) + preserved_files.append(str(dest_file.relative_to(self.base_path))) + + # 删除会话目录 + if session_dir.exists(): + shutil.rmtree(session_dir, ignore_errors=True) + + return preserved_files + + def get_session_files(self, user_id: str, session_id: str) -> List[Path]: + """获取会话的所有文件""" + files_dir = self._get_session_dir(user_id, session_id) / "files" + if not files_dir.exists(): + return [] + return [f for f in files_dir.iterdir() if f.is_file()] + + def _get_session_dir(self, user_id: str, session_id: str) -> Path: + """获取会话目录路径""" + return self.base_path / f"user_{user_id}" / "sessions" / session_id + + def _get_user_shared_dir(self, user_id: str) -> Path: + """获取用户共享目录路径""" + return self.base_path / f"user_{user_id}" / "shared_files" diff --git a/backend/app/utils/__init__.py b/backend/app/utils/__init__.py new file mode 100644 index 0000000..802943f --- /dev/null +++ b/backend/app/utils/__init__.py @@ -0,0 +1,4 @@ +"""工具模块""" +from .init_env import init_shared_env, check_shared_env + +__all__ = ["init_shared_env", "check_shared_env"] diff --git a/backend/app/utils/init_env.py b/backend/app/utils/init_env.py new file mode 100644 index 0000000..ae8546c --- /dev/null +++ b/backend/app/utils/init_env.py @@ -0,0 +1,134 @@ +"""共享环境初始化工具""" +import os +import sys +import subprocess +import venv +from pathlib import Path +import logging + +logger = logging.getLogger(__name__) + + +def init_shared_env( + base_dir: Path, + packages_file: Path | None = None, + force: bool = False +) -> bool: + """ + 初始化共享 Python 环境 + + Args: + base_dir: 基础目录(通常是 backend/data/shared_env) + packages_file: 包列表文件路径 + force: 是否强制重新创建 + + Returns: + bool: 初始化是否成功 + """ + try: + logger.info("🚀 开始初始化共享环境...") + + # 创建目录 + base_dir.mkdir(parents=True, exist_ok=True) + + # 虚拟环境路径 + venv_dir = base_dir / "base.venv" + + # 检查是否已存在 + if venv_dir.exists() and not force: + logger.info(f"✅ 共享环境已存在: {venv_dir}") + return True + + # 创建虚拟环境 + logger.info(f"🔨 创建虚拟环境: {venv_dir}") + if venv_dir.exists(): + import shutil + shutil.rmtree(venv_dir) + + venv.create(venv_dir, with_pip=True, clear=True) + logger.info("✅ 虚拟环境创建成功") + + # 确定 pip 路径 + if sys.platform == "win32": + pip_path = venv_dir / "Scripts" / "pip.exe" + python_path = venv_dir / "Scripts" / "python.exe" + else: + pip_path = venv_dir / "bin" / "pip" + python_path = venv_dir / "bin" / "python" + + if not pip_path.exists(): + logger.error(f"❌ 找不到 pip: {pip_path}") + return False + + # 升级 pip(静默) + logger.info("📦 升级 pip...") + try: + subprocess.run( + [str(python_path), "-m", "pip", "install", "--upgrade", "pip", "--quiet"], + check=True, + capture_output=True, + timeout=120 + ) + except Exception as e: + logger.warning(f"⚠️ pip 升级失败: {e}") + + # 安装包 + if packages_file and packages_file.exists(): + logger.info(f"📚 安装包列表: {packages_file}") + with open(packages_file, "r", encoding="utf-8") as f: + packages = [ + line.strip() + for line in f + if line.strip() and not line.strip().startswith("#") + ] + + if packages: + logger.info(f" 共 {len(packages)} 个包") + # 批量安装(更快) + try: + subprocess.run( + [str(pip_path), "install", "--quiet"] + packages, + check=True, + capture_output=True, + timeout=600 # 10分钟 + ) + logger.info(f"✅ 成功安装 {len(packages)} 个包") + except subprocess.TimeoutExpired: + logger.error("❌ 包安装超时") + return False + except subprocess.CalledProcessError as e: + logger.error(f"❌ 包安装失败: {e.stderr.decode() if e.stderr else str(e)}") + return False + else: + logger.warning("⚠️ 未找到包列表文件,跳过包安装") + + logger.info("✅ 共享环境初始化完成") + return True + + except Exception as e: + logger.error(f"❌ 共享环境初始化失败: {e}") + import traceback + logger.error(traceback.format_exc()) + return False + + +def check_shared_env(venv_dir: Path) -> bool: + """ + 检查共享环境是否存在且可用 + + Args: + venv_dir: 虚拟环境目录 + + Returns: + bool: 环境是否可用 + """ + if not venv_dir.exists(): + return False + + # 检查 Python 可执行文件 + if sys.platform == "win32": + python_path = venv_dir / "Scripts" / "python.exe" + else: + python_path = venv_dir / "bin" / "python" + + return python_path.exists() diff --git a/backend/data/shared_env/allowed_packages.txt b/backend/data/shared_env/allowed_packages.txt new file mode 100644 index 0000000..c3f25a0 --- /dev/null +++ b/backend/data/shared_env/allowed_packages.txt @@ -0,0 +1,39 @@ +# Mini-Agent 共享环境包白名单 +# 仅包含运行时必需的包,不包含开发工具 + +# === 核心数据处理(必需)=== +pandas +numpy + +# === 文档处理 === +# PDF +pypdf +reportlab + +# PowerPoint +python-pptx + +# Word +python-docx + +# Excel +openpyxl + +# === 图像处理 === +Pillow + +# === 可视化 === +matplotlib + +# === 网络请求 === +requests + +# === 工具库 === +pyyaml +python-dateutil + +# 注意: +# - 不包含开发工具(pytest, black, mypy 等) +# - 不包含系统危险包(subprocess, os-sys 等) +# - 只保留运行时必需的包 +# - 使用最新稳定版本,不锁定版本号(避免兼容性问题) diff --git a/backend/diagnose.py b/backend/diagnose.py new file mode 100644 index 0000000..620a91e --- /dev/null +++ b/backend/diagnose.py @@ -0,0 +1,181 @@ +#!/usr/bin/env python3 +"""后端配置诊断脚本 + +检查 Mini-Agent 后端的配置是否正确,帮助快速定位问题。 +""" +import sys +from pathlib import Path + +# 添加项目根目录到 Python 路径 +sys.path.insert(0, str(Path(__file__).parent)) + +print("🔍 Mini-Agent 后端配置诊断\n") +print("=" * 60) + +# 1. 检查 Python 版本 +print("\n1️⃣ 检查 Python 版本") +print(f" Python 版本: {sys.version}") +if sys.version_info < (3, 10): + print(" ❌ Python 版本过低,需要 3.10 或更高版本") + sys.exit(1) +else: + print(" ✅ Python 版本符合要求") + +# 2. 检查必要的包是否安装 +print("\n2️⃣ 检查依赖包") +required_packages = [ + "fastapi", + "uvicorn", + "sqlalchemy", + "pydantic", + "pydantic_settings", + "httpx", + "anthropic", + "openai", + "tiktoken", + "yaml", + "mcp", +] + +missing_packages = [] +for package in required_packages: + try: + if package == "yaml": + __import__("yaml") + else: + __import__(package) + print(f" ✅ {package}") + except ImportError: + print(f" ❌ {package} 未安装") + missing_packages.append(package) + +if missing_packages: + print(f"\n ⚠️ 缺少依赖包: {', '.join(missing_packages)}") + print(f" 💡 运行: pip install -r requirements.txt") + sys.exit(1) + +# 3. 检查 .env 文件 +print("\n3️⃣ 检查 .env 配置文件") +env_file = Path(__file__).parent / ".env" +if not env_file.exists(): + print(f" ❌ .env 文件不存在") + print(f" 💡 请复制 .env.example 为 .env 并修改配置") + print(f" 命令: cp .env.example .env") + sys.exit(1) +else: + print(f" ✅ .env 文件存在") + +# 4. 加载配置 +print("\n4️⃣ 加载配置") +try: + from app.config import get_settings + + settings = get_settings() + print(f" ✅ 配置加载成功") +except Exception as e: + print(f" ❌ 配置加载失败: {e}") + sys.exit(1) + +# 5. 检查关键配置项 +print("\n5️⃣ 检查关键配置项") + +# LLM API Key +if not settings.llm_api_key or settings.llm_api_key == "your-api-key-here": + print(f" ❌ LLM_API_KEY 未配置或使用默认值") + print(f" 💡 请在 .env 文件中设置正确的 API 密钥") + has_error = True +else: + masked_key = settings.llm_api_key[:8] + "..." + settings.llm_api_key[-4:] + print(f" ✅ LLM_API_KEY: {masked_key}") + has_error = False + +# LLM API Base +print(f" ✅ LLM_API_BASE: {settings.llm_api_base}") + +# LLM Model +print(f" ✅ LLM_MODEL: {settings.llm_model}") + +# LLM Provider +print(f" ✅ LLM_PROVIDER: {settings.llm_provider}") +if settings.llm_provider not in ["anthropic", "openai"]: + print(f" ⚠️ 警告:provider 应该是 'anthropic' 或 'openai'") + +# 数据库 +print(f" ✅ DATABASE_URL: {settings.database_url}") + +# 工作空间 +print(f" ✅ WORKSPACE_BASE: {settings.workspace_base}") + +# 6. 检查 mini_agent 源码路径 +print("\n6️⃣ 检查 mini_agent 源码") +mini_agent_path = Path(__file__).parent.parent / "mini_agent" +if not mini_agent_path.exists(): + print(f" ❌ mini_agent 目录不存在: {mini_agent_path}") + print(f" 💡 请确保在 Mini-Agent 项目根目录运行") + sys.exit(1) +else: + print(f" ✅ mini_agent 路径: {mini_agent_path}") + +# 检查是否可以导入 mini_agent +try: + sys.path.insert(0, str(mini_agent_path.parent)) + from mini_agent.agent import Agent + from mini_agent.llm import LLMClient + from mini_agent.schema import LLMProvider + + print(f" ✅ mini_agent 模块可以正常导入") +except ImportError as e: + print(f" ❌ 无法导入 mini_agent: {e}") + sys.exit(1) + +# 7. 测试 LLM 客户端初始化 +print("\n7️⃣ 测试 LLM 客户端初始化") +try: + provider = ( + LLMProvider.OPENAI + if settings.llm_provider.lower() == "openai" + else LLMProvider.ANTHROPIC + ) + llm_client = LLMClient( + api_key=settings.llm_api_key, + api_base=settings.llm_api_base, + provider=provider, + model=settings.llm_model, + ) + print(f" ✅ LLM 客户端初始化成功") + print(f" 📝 提供商: {provider.value}") + print(f" 📝 模型: {settings.llm_model}") +except Exception as e: + print(f" ❌ LLM 客户端初始化失败: {e}") + import traceback + + print(f"\n详细错误:\n{traceback.format_exc()}") + has_error = True + +# 8. 检查数据库 +print("\n8️⃣ 检查数据库") +try: + from app.models.database import init_db, engine + from sqlalchemy import text + + init_db() + with engine.connect() as conn: + conn.execute(text("SELECT 1")) + print(f" ✅ 数据库连接正常") +except Exception as e: + print(f" ❌ 数据库初始化失败: {e}") + has_error = True + +# 总结 +print("\n" + "=" * 60) +if has_error: + print("❌ 发现配置问题,请根据上述提示修复") + print("\n常见问题:") + print("1. 确保 .env 文件中的 LLM_API_KEY 已正确配置") + print("2. 确保所有依赖包已安装: pip install -r requirements.txt") + print("3. 确保在正确的目录运行(Mini-Agent/backend/)") + sys.exit(1) +else: + print("✅ 所有检查通过,后端配置正常!") + print("\n可以运行后端服务:") + print(" uvicorn app.main:app --reload") diff --git a/backend/migrate_database.py b/backend/migrate_database.py new file mode 100644 index 0000000..07b333b --- /dev/null +++ b/backend/migrate_database.py @@ -0,0 +1,148 @@ +#!/usr/bin/env python3 +"""数据库迁移脚本 + +将旧的整数 ID 迁移到 UUID 字符串格式。 +尝试保留现有数据。 +""" +import sys +from pathlib import Path +import sqlite3 +import uuid + +# 添加项目根目录到 Python 路径 +sys.path.insert(0, str(Path(__file__).parent)) + +def migrate_database(): + """迁移数据库""" + print("🔄 Mini-Agent 数据库迁移工具\n") + print("=" * 60) + + db_file = Path("./data/database/mini_agent.db") + if not db_file.exists(): + print("❌ 数据库文件不存在") + print(" 如果是首次运行,请直接启动后端服务,系统会自动初始化数据库。") + return False + + print("\n1️⃣ 检查数据库...") + + try: + conn = sqlite3.connect(str(db_file)) + cursor = conn.cursor() + + # 检查 messages 表结构 + cursor.execute("PRAGMA table_info(messages)") + columns = cursor.fetchall() + column_names = [col[1] for col in columns] + + print(f" ✅ 找到 messages 表,包含字段: {', '.join(column_names)}") + + # 检查是否有整数 ID + cursor.execute("SELECT id, typeof(id) FROM messages LIMIT 5") + sample_rows = cursor.fetchall() + + if not sample_rows: + print(" ℹ️ messages 表为空,无需迁移") + conn.close() + return True + + has_integer_ids = any(row[1] == 'integer' for row in sample_rows) + + if not has_integer_ids: + print(" ℹ️ 所有 ID 已经是字符串格式,无需迁移") + conn.close() + return True + + print(f" ⚠️ 检测到整数 ID,需要迁移") + + # 确认操作 + print("\n⚠️ 警告:此操作会修改数据库结构") + print(" 建议先备份数据库文件!") + response = input("\n确定要继续吗?(输入 'yes' 确认): ") + if response.lower() != "yes": + print("❌ 操作已取消") + conn.close() + return False + + # 2. 创建新表 + print("\n2️⃣ 创建新表结构...") + cursor.execute(""" + CREATE TABLE IF NOT EXISTS messages_new ( + id TEXT PRIMARY KEY, + session_id TEXT NOT NULL, + role TEXT NOT NULL, + content TEXT, + thinking TEXT, + tool_calls TEXT, + tool_call_id TEXT, + created_at TIMESTAMP NOT NULL + ) + """) + print(" ✅ 新表创建成功") + + # 3. 迁移数据 + print("\n3️⃣ 迁移数据...") + cursor.execute("SELECT * FROM messages") + old_rows = cursor.fetchall() + + id_mapping = {} # 旧 ID -> 新 UUID 的映射 + migrated_count = 0 + + for row in old_rows: + old_id = row[0] + + # 如果是整数,生成新的 UUID + if isinstance(old_id, int): + new_id = str(uuid.uuid4()) + id_mapping[old_id] = new_id + else: + new_id = str(old_id) + + # 插入到新表 + cursor.execute(""" + INSERT INTO messages_new (id, session_id, role, content, thinking, tool_calls, tool_call_id, created_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + """, (new_id, row[1], row[2], row[3], row[4], row[5], row[6], row[7])) + + migrated_count += 1 + + print(f" ✅ 成功迁移 {migrated_count} 条消息记录") + + # 4. 替换旧表 + print("\n4️⃣ 替换旧表...") + cursor.execute("DROP TABLE messages") + cursor.execute("ALTER TABLE messages_new RENAME TO messages") + print(" ✅ 表替换完成") + + # 5. 创建索引 + print("\n5️⃣ 创建索引...") + cursor.execute("CREATE INDEX IF NOT EXISTS idx_messages_session_id ON messages(session_id)") + cursor.execute("CREATE INDEX IF NOT EXISTS idx_messages_created_at ON messages(created_at)") + print(" ✅ 索引创建完成") + + # 提交更改 + conn.commit() + conn.close() + + # 6. 完成 + print("\n" + "=" * 60) + print("✅ 数据库迁移完成!") + print(f"\n迁移统计:") + print(f" - 总消息数: {migrated_count}") + print(f" - ID 映射数: {len(id_mapping)}") + print("\n可以重新启动后端服务:") + print(" uvicorn app.main:app --reload") + return True + + except Exception as e: + print(f"\n❌ 迁移失败: {e}") + import traceback + print(f"\n详细错误:\n{traceback.format_exc()}") + if 'conn' in locals(): + conn.rollback() + conn.close() + return False + + +if __name__ == "__main__": + success = migrate_database() + sys.exit(0 if success else 1) diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..d1283da --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,48 @@ +# FastAPI 后端依赖 + +# Web 框架 +fastapi==0.104.1 +uvicorn[standard]==0.24.0 +python-multipart==0.0.6 + +# 数据库 +sqlalchemy==2.0.23 +alembic==1.12.1 + +# 配置管理 +pydantic==2.5.0 +pydantic-settings==2.1.0 +python-dotenv==1.0.0 + +# CORS +fastapi-cors==0.0.6 + +# 工具 +python-jose[cryptography]==3.3.0 # JWT(未来用) +passlib[bcrypt]==1.7.4 # 密码哈希(未来用) + +# ========== Mini-Agent 核心依赖 ========== +# 后端通过 sys.path 导入 mini_agent 源代码 +# 需要安装 mini_agent 的依赖,但不需要安装 mini_agent 包本身 + +# HTTP 客户端(LLM API 调用) +httpx>=0.27.0 + +# LLM SDK +anthropic>=0.39.0 +openai>=1.57.4 + +# Token 计数 +tiktoken>=0.5.0 + +# 配置文件 +pyyaml>=6.0.0 + +# Model Context Protocol +mcp>=1.0.0 + +# 网络请求 +requests>=2.31.0 + +# 搜索工具(可选) +zhipuai>=2.0.0 # 智谱 AI 搜索工具 diff --git a/backend/reset_database.py b/backend/reset_database.py new file mode 100644 index 0000000..349d067 --- /dev/null +++ b/backend/reset_database.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python3 +"""数据库重置脚本 + +⚠️ 警告:此脚本会删除所有会话数据和消息历史! + +用于清理不兼容的旧数据库,重新初始化数据库表结构。 +""" +import sys +from pathlib import Path +import shutil + +# 添加项目根目录到 Python 路径 +sys.path.insert(0, str(Path(__file__).parent)) + +def reset_database(clean_workspaces: bool = False): + """重置数据库 + + Args: + clean_workspaces: 是否同时清理工作空间目录 + """ + print("🔄 Mini-Agent 数据库重置工具\n") + print("=" * 60) + + # 1. 确认操作 + print("\n⚠️ 警告:此操作将:") + print(" - 删除所有会话记录") + print(" - 删除所有消息历史") + print(" - 重新创建数据库表结构") + if clean_workspaces: + print(" - 清理所有工作空间文件") + print("\n此操作不可恢复!") + + response = input("\n确定要继续吗?(输入 'yes' 确认): ") + if response.lower() != "yes": + print("❌ 操作已取消") + return False + + # 2. 删除数据库文件 + print("\n1️⃣ 删除数据库文件...") + db_file = Path("./data/database/mini_agent.db") + if db_file.exists(): + try: + db_file.unlink() + print(f" ✅ 已删除: {db_file}") + except Exception as e: + print(f" ❌ 删除失败: {e}") + return False + else: + print(f" ℹ️ 数据库文件不存在: {db_file}") + + # 3. 清理工作空间(可选) + if clean_workspaces: + print("\n2️⃣ 清理工作空间...") + workspace_dir = Path("./data/workspaces") + if workspace_dir.exists(): + try: + shutil.rmtree(workspace_dir) + workspace_dir.mkdir(parents=True, exist_ok=True) + print(f" ✅ 已清理: {workspace_dir}") + except Exception as e: + print(f" ❌ 清理失败: {e}") + return False + else: + print(f" ℹ️ 工作空间目录不存在: {workspace_dir}") + + # 4. 重新初始化数据库 + print("\n3️⃣ 重新初始化数据库...") + try: + from app.models.database import init_db + from app.models.session import Session # 导入模型以注册表 + from app.models.message import Message + + init_db() + print(" ✅ 数据库表创建成功") + except Exception as e: + print(f" ❌ 数据库初始化失败: {e}") + import traceback + print(f"\n详细错误:\n{traceback.format_exc()}") + return False + + # 5. 完成 + print("\n" + "=" * 60) + print("✅ 数据库重置完成!") + print("\n可以重新启动后端服务:") + print(" uvicorn app.main:app --reload") + return True + + +if __name__ == "__main__": + import argparse + + parser = argparse.ArgumentParser(description="重置 Mini-Agent 数据库") + parser.add_argument( + "--clean-workspaces", + action="store_true", + help="同时清理所有工作空间文件" + ) + + args = parser.parse_args() + + success = reset_database(clean_workspaces=args.clean_workspaces) + sys.exit(0 if success else 1) diff --git a/backend/scripts/README.md b/backend/scripts/README.md new file mode 100644 index 0000000..320f523 --- /dev/null +++ b/backend/scripts/README.md @@ -0,0 +1,103 @@ +# 后端初始化脚本 + +## 共享环境初始化 + +在首次运行后端之前,需要初始化共享 Python 环境。 + +### 方法 1: Python 脚本(推荐,跨平台) + +```bash +# 在 backend 目录下运行 +cd backend +python scripts/init_shared_env.py +``` + +### 方法 2: Shell 脚本(Linux/Mac) + +```bash +cd backend +bash scripts/init_shared_env.sh +``` + +### 方法 3: 批处理脚本(Windows) + +```cmd +cd backend +scripts\init_shared_env.bat +``` + +## 初始化内容 + +脚本会自动完成以下操作: + +1. **创建目录结构** + - `data/shared_env/` - 共享虚拟环境 + - `data/workspaces/` - 用户工作空间 + - `data/database/` - SQLite 数据库 + +2. **创建 Python 虚拟环境** + - 路径: `data/shared_env/base.venv` + - 包含独立的 Python 解释器和 pip + +3. **安装允许的包** + - 从 `data/shared_env/allowed_packages.txt` 读取 + - 包括: pypdf, python-pptx, openpyxl, pandas, Pillow, matplotlib 等 + - 总共 20+ 个常用数据处理和可视化包 + +## 故障排除 + +### 问题:虚拟环境创建失败 + +**解决方案**: +- 确保已安装 Python 3.10+ +- Windows: 确保 Python 在 PATH 中 +- Linux/Mac: 可能需要安装 `python3-venv` + ```bash + sudo apt-get install python3-venv # Ubuntu/Debian + ``` + +### 问题:包安装失败 + +**解决方案**: +- 检查网络连接 +- 手动激活虚拟环境并安装: + ```bash + # Linux/Mac + source data/shared_env/base.venv/bin/activate + pip install + + # Windows + data\shared_env\base.venv\Scripts\activate + pip install + ``` + +### 问题:需要重新初始化 + +**解决方案**: +```bash +# 删除旧环境 +rm -rf data/shared_env/base.venv # Linux/Mac +rmdir /s data\shared_env\base.venv # Windows + +# 重新运行初始化脚本 +python scripts/init_shared_env.py +``` + +## 验证安装 + +运行以下命令验证环境: + +```bash +# Linux/Mac +data/shared_env/base.venv/bin/python -c "import pandas; import numpy; import PIL; print('✅ 环境正常')" + +# Windows +data\shared_env\base.venv\Scripts\python -c "import pandas; import numpy; import PIL; print('✅ 环境正常')" +``` + +## 注意事项 + +- **初始化时间**: 首次安装所有包可能需要 5-10 分钟 +- **磁盘空间**: 虚拟环境约占用 500MB-1GB 空间 +- **网络要求**: 需要稳定的网络连接以下载包 +- **安全性**: 只安装 `allowed_packages.txt` 中列出的包,确保安全 diff --git a/backend/scripts/init_shared_env.bat b/backend/scripts/init_shared_env.bat new file mode 100644 index 0000000..d1890e1 --- /dev/null +++ b/backend/scripts/init_shared_env.bat @@ -0,0 +1,65 @@ +@echo off +REM 初始化共享环境脚本 (Windows) + +echo 🚀 开始初始化 Mini-Agent 共享环境... + +REM 进入后端目录 +cd /d "%~dp0.." + +REM 创建目录 +echo 📁 创建目录结构... +if not exist "data\shared_env" mkdir "data\shared_env" +if not exist "data\workspaces" mkdir "data\workspaces" +if not exist "data\database" mkdir "data\database" + +REM 检查 Python +echo 🐍 检查 Python 版本... +python --version +if errorlevel 1 ( + echo ❌ Python 未安装或不在 PATH 中 + exit /b 1 +) + +REM 创建虚拟环境 +set VENV_DIR=data\shared_env\base.venv +if exist "%VENV_DIR%" ( + echo ⚠️ 虚拟环境已存在,跳过创建 +) else ( + echo 🔨 创建虚拟环境: %VENV_DIR% + python -m venv "%VENV_DIR%" + if errorlevel 1 ( + echo ❌ 创建虚拟环境失败 + exit /b 1 + ) +) + +REM 激活虚拟环境 +echo ✨ 激活虚拟环境... +call "%VENV_DIR%\Scripts\activate.bat" +if errorlevel 1 ( + echo ❌ 激活虚拟环境失败 + exit /b 1 +) + +REM 升级 pip +echo 📦 升级 pip... +python -m pip install --upgrade pip + +REM 安装允许的包 +set PACKAGES_FILE=data\shared_env\allowed_packages.txt +if exist "%PACKAGES_FILE%" ( + echo 📚 安装允许的包... + for /f "usebackq tokens=*" %%i in ("%PACKAGES_FILE%") do ( + echo 📦 安装: %%i + pip install "%%i" || echo ⚠️ 安装 %%i 失败,继续... + ) +) else ( + echo ⚠️ 找不到 allowed_packages.txt,跳过包安装 +) + +echo. +echo ✅ 共享环境初始化完成! +echo 📍 虚拟环境路径: %VENV_DIR% +echo. + +pause diff --git a/backend/scripts/init_shared_env.py b/backend/scripts/init_shared_env.py new file mode 100755 index 0000000..4e0a1e5 --- /dev/null +++ b/backend/scripts/init_shared_env.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python3 +"""初始化共享环境脚本(跨平台)""" +import os +import sys +import subprocess +import venv +from pathlib import Path + + +def main(): + print("🚀 开始初始化 Mini-Agent 共享环境...") + + # 确定后端目录 + backend_dir = Path(__file__).parent.parent + os.chdir(backend_dir) + + # 创建目录 + print("📁 创建目录结构...") + data_dir = Path("data") + (data_dir / "shared_env").mkdir(parents=True, exist_ok=True) + (data_dir / "workspaces").mkdir(parents=True, exist_ok=True) + (data_dir / "database").mkdir(parents=True, exist_ok=True) + + # 创建虚拟环境 + venv_dir = data_dir / "shared_env" / "base.venv" + if venv_dir.exists(): + print(f"⚠️ 虚拟环境已存在: {venv_dir}") + print(" 跳过创建...") + else: + print(f"🔨 创建虚拟环境: {venv_dir}") + try: + venv.create(venv_dir, with_pip=True) + print("✅ 虚拟环境创建成功") + except Exception as e: + print(f"❌ 创建虚拟环境失败: {e}") + return 1 + + # 确定 pip 路径 + if sys.platform == "win32": + pip_path = venv_dir / "Scripts" / "pip.exe" + python_path = venv_dir / "Scripts" / "python.exe" + else: + pip_path = venv_dir / "bin" / "pip" + python_path = venv_dir / "bin" / "python" + + if not pip_path.exists(): + print(f"❌ 找不到 pip: {pip_path}") + return 1 + + # 升级 pip + print("📦 升级 pip...") + try: + subprocess.run([str(python_path), "-m", "pip", "install", "--upgrade", "pip"], + check=True, capture_output=True) + print("✅ pip 升级成功") + except subprocess.CalledProcessError as e: + print(f"⚠️ pip 升级失败: {e}") + + # 读取并安装允许的包 + packages_file = data_dir / "shared_env" / "allowed_packages.txt" + if not packages_file.exists(): + print(f"⚠️ 找不到 {packages_file}") + print(" 跳过包安装") + else: + print(f"📚 从 {packages_file} 安装包...") + with open(packages_file, "r", encoding="utf-8") as f: + packages = [ + line.strip() + for line in f + if line.strip() and not line.strip().startswith("#") + ] + + if not packages: + print("⚠️ 包列表为空") + else: + print(f" 共 {len(packages)} 个包需要安装") + failed = [] + for i, package in enumerate(packages, 1): + print(f" [{i}/{len(packages)}] 安装: {package}") + try: + subprocess.run( + [str(pip_path), "install", package], + check=True, + capture_output=True, + timeout=300 # 5分钟超时 + ) + print(f" ✅ {package} 安装成功") + except subprocess.TimeoutExpired: + print(f" ⚠️ {package} 安装超时,跳过") + failed.append(package) + except subprocess.CalledProcessError as e: + print(f" ⚠️ {package} 安装失败") + failed.append(package) + + if failed: + print(f"\n⚠️ 以下包安装失败:") + for pkg in failed: + print(f" - {pkg}") + + print("\n" + "="*60) + print("✅ 共享环境初始化完成!") + print(f"📍 虚拟环境路径: {venv_dir.absolute()}") + print(f"🐍 Python: {python_path}") + print(f"📦 Pip: {pip_path}") + print("="*60) + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/backend/scripts/init_shared_env.sh b/backend/scripts/init_shared_env.sh new file mode 100755 index 0000000..f789689 --- /dev/null +++ b/backend/scripts/init_shared_env.sh @@ -0,0 +1,63 @@ +#!/bin/bash +# 初始化共享环境脚本 + +set -e # 遇到错误立即退出 + +echo "🚀 开始初始化 Mini-Agent 共享环境..." + +# 进入后端目录 +cd "$(dirname "$0")" +BACKEND_DIR="$(pwd)" + +# 创建目录 +echo "📁 创建目录结构..." +mkdir -p data/shared_env +mkdir -p data/workspaces +mkdir -p data/database + +# 检查 Python 版本 +echo "🐍 检查 Python 版本..." +python --version || python3 --version + +# 创建虚拟环境 +VENV_DIR="data/shared_env/base.venv" +if [ -d "$VENV_DIR" ]; then + echo "⚠️ 虚拟环境已存在,跳过创建" +else + echo "🔨 创建虚拟环境: $VENV_DIR" + python -m venv "$VENV_DIR" || python3 -m venv "$VENV_DIR" +fi + +# 激活虚拟环境 +echo "✨ 激活虚拟环境..." +if [ -f "$VENV_DIR/bin/activate" ]; then + source "$VENV_DIR/bin/activate" +elif [ -f "$VENV_DIR/Scripts/activate" ]; then + source "$VENV_DIR/Scripts/activate" +else + echo "❌ 找不到激活脚本" + exit 1 +fi + +# 升级 pip +echo "📦 升级 pip..." +pip install --upgrade pip + +# 读取允许的包列表并安装 +PACKAGES_FILE="data/shared_env/allowed_packages.txt" +if [ -f "$PACKAGES_FILE" ]; then + echo "📚 安装允许的包..." + while IFS= read -r package || [ -n "$package" ]; do + # 跳过空行和注释 + [[ -z "$package" || "$package" =~ ^# ]] && continue + echo " 📦 安装: $package" + pip install "$package" || echo " ⚠️ 安装 $package 失败,继续..." + done < "$PACKAGES_FILE" +else + echo "⚠️ 找不到 allowed_packages.txt,跳过包安装" +fi + +echo "" +echo "✅ 共享环境初始化完成!" +echo "📍 虚拟环境路径: $VENV_DIR" +echo "" diff --git a/backend/setup-backend.bat b/backend/setup-backend.bat new file mode 100644 index 0000000..1e69369 --- /dev/null +++ b/backend/setup-backend.bat @@ -0,0 +1,55 @@ +@echo off +REM 后端快速安装脚本(Windows) + +echo 🚀 Mini-Agent 后端快速安装 +echo ================================ +echo. + +REM 检查是否在正确的目录 +if not exist "..\pyproject.toml" ( + echo ❌ 错误:请从 backend\ 目录运行此脚本 + echo cd Mini-Agent\backend ^&^& setup-backend.bat + exit /b 1 +) + +cd .. + +echo 📦 步骤 1: 安装 mini_agent + 后端依赖 +echo 运行: pip install -e .[backend] +echo. + +REM 安装包(可编辑模式)+ 后端依赖 +pip install -e ".[backend]" + +echo. +echo ✅ mini_agent 和后端依赖已安装! +echo. + +REM 返回 backend 目录 +cd backend + +REM 检查 .env 文件 +if not exist ".env" ( + echo 📝 步骤 2: 创建 .env 配置文件 + copy .env.example .env + echo ✅ 已从 .env.example 复制 + echo ⚠️ 请编辑 .env 文件,填入你的 API Keys + echo. +) else ( + echo ✅ .env 文件已存在 + echo. +) + +echo ================================ +echo 🎉 安装完成! +echo. +echo 下一步: +echo 1. 编辑 backend\.env 文件,填入 API Keys +echo 2. 运行诊断: python diagnose.py +echo 3. 启动后端: uvicorn app.main:app --reload +echo. +echo 现在可以运行诊断脚本: +echo python diagnose.py +echo. + +pause diff --git a/backend/setup-backend.sh b/backend/setup-backend.sh new file mode 100755 index 0000000..30ca161 --- /dev/null +++ b/backend/setup-backend.sh @@ -0,0 +1,55 @@ +#!/bin/bash +# 后端快速安装脚本(推荐方式) + +set -e + +echo "🚀 Mini-Agent 后端快速安装" +echo "================================" +echo "" + +# 检查是否在正确的目录 +if [ ! -f "../pyproject.toml" ]; then + echo "❌ 错误:请从 backend/ 目录运行此脚本" + echo " cd Mini-Agent/backend && ./setup-backend.sh" + exit 1 +fi + +cd .. + +echo "📦 步骤 1: 安装 mini_agent + 后端依赖" +echo " 运行: pip install -e '.[backend]'" +echo "" + +# 安装包(可编辑模式)+ 后端依赖 +pip install -e ".[backend]" + +echo "" +echo "✅ mini_agent 和后端依赖已安装!" +echo "" + +# 返回 backend 目录 +cd backend + +# 检查 .env 文件 +if [ ! -f ".env" ]; then + echo "📝 步骤 2: 创建 .env 配置文件" + cp .env.example .env + echo " ✅ 已从 .env.example 复制" + echo " ⚠️ 请编辑 .env 文件,填入你的 API Keys" + echo "" +else + echo "✅ .env 文件已存在" + echo "" +fi + +echo "================================" +echo "🎉 安装完成!" +echo "" +echo "下一步:" +echo " 1. 编辑 backend/.env 文件,填入 API Keys" +echo " 2. 运行诊断: python diagnose.py" +echo " 3. 启动后端: uvicorn app.main:app --reload" +echo "" +echo "现在可以运行诊断脚本:" +echo " python diagnose.py" +echo "" diff --git a/backend/test_api.py b/backend/test_api.py new file mode 100644 index 0000000..df8748d --- /dev/null +++ b/backend/test_api.py @@ -0,0 +1,103 @@ +"""FastAPI 后端测试脚本""" +import requests +import json + +BASE_URL = "http://localhost:8000/api" + + +def test_login(): + """测试登录""" + print("\n=== 测试登录 ===") + response = requests.post( + f"{BASE_URL}/auth/login", json={"username": "demo", "password": "demo123"} + ) + print(f"状态码: {response.status_code}") + print(f"响应: {json.dumps(response.json(), indent=2, ensure_ascii=False)}") + return response.json()["user_id"] + + +def test_create_session(user_id): + """测试创建会话""" + print("\n=== 测试创建会话 ===") + response = requests.post( + f"{BASE_URL}/sessions?user_id={user_id}", + json={"title": "测试会话"}, + ) + print(f"状态码: {response.status_code}") + data = response.json() + print(f"响应: {json.dumps(data, indent=2, ensure_ascii=False)}") + return data["id"] + + +def test_chat(user_id, session_id): + """测试对话""" + print("\n=== 测试对话 ===") + response = requests.post( + f"{BASE_URL}/chat/{session_id}?user_id={user_id}", + json={"message": "你好,介绍一下你自己"}, + ) + print(f"状态码: {response.status_code}") + print(f"响应: {json.dumps(response.json(), indent=2, ensure_ascii=False)}") + + +def test_get_history(user_id, session_id): + """测试获取历史""" + print("\n=== 测试获取历史 ===") + response = requests.get(f"{BASE_URL}/chat/{session_id}/history?user_id={user_id}") + print(f"状态码: {response.status_code}") + data = response.json() + print(f"消息数量: {data['total']}") + for msg in data["messages"]: + print(f" - {msg['role']}: {msg['content'][:50]}...") + + +def test_list_sessions(user_id): + """测试会话列表""" + print("\n=== 测试会话列表 ===") + response = requests.get(f"{BASE_URL}/sessions?user_id={user_id}") + print(f"状态码: {response.status_code}") + data = response.json() + print(f"总会话数: {data['total']}") + for session in data["sessions"]: + print(f" - {session['title']} ({session['id'][:8]}...)") + + +def test_close_session(user_id, session_id): + """测试关闭会话""" + print("\n=== 测试关闭会话 ===") + response = requests.delete( + f"{BASE_URL}/sessions/{session_id}?user_id={user_id}&preserve_files=true" + ) + print(f"状态码: {response.status_code}") + print(f"响应: {json.dumps(response.json(), indent=2, ensure_ascii=False)}") + + +if __name__ == "__main__": + print("开始测试 Mini-Agent FastAPI 后端...") + + try: + # 1. 登录 + user_id = test_login() + + # 2. 创建会话 + session_id = test_create_session(user_id) + + # 3. 发送消息 + test_chat(user_id, session_id) + + # 4. 获取历史 + test_get_history(user_id, session_id) + + # 5. 列出会话 + test_list_sessions(user_id) + + # 6. 关闭会话 + test_close_session(user_id, session_id) + + print("\n✅ 所有测试完成!") + + except Exception as e: + print(f"\n❌ 测试失败: {e}") + import traceback + + traceback.print_exc() diff --git a/docs/BACKEND_WORKSPACE_DESIGN.md b/docs/BACKEND_WORKSPACE_DESIGN.md new file mode 100644 index 0000000..eed1c69 --- /dev/null +++ b/docs/BACKEND_WORKSPACE_DESIGN.md @@ -0,0 +1,616 @@ +# Mini-Agent 后端 Workspace 设计方案 + +## 核心问题 +Workspace 应该如何组织? +- 跟着用户走? +- 跟着 session 走? +- 统一环境? + +--- + +## 方案对比 + +### 📊 方案1: Workspace 跟用户走 + +``` +/data/workspaces/ + ├─ user_12345/ + │ ├─ .venv/ ← 用户的 Python 环境 + │ ├─ files/ ← 用户所有文件 + │ ├─ .agent_memory.json ← 持久化记忆 + │ └─ sessions/ + │ ├─ session_abc/ ← 会话日志 + │ └─ session_def/ + └─ user_67890/ + ├─ .venv/ + ├─ files/ + └─ ... +``` + +**优点**: +- ✅ 用户文件持久化(可以跨会话访问) +- ✅ 包只需装一次(reportlab 装一次,所有会话都能用) +- ✅ 有"个人工作空间"的感觉 + +**缺点**: +- ❌ 不同任务的包可能冲突(A任务装 pandas 1.0,B任务需要 2.0) +- ❌ 用户可能装一堆包,占用大量空间 +- ❌ 安全隔离不够强(一个会话的恶意代码影响整个用户空间) +- ❌ 需要配额和清理策略 + +**适用场景**: +- 个人使用 +- 需要长期保存文件的场景 +- 用户数量少 + +--- + +### 📊 方案2: Workspace 跟 Session 走 + +``` +/data/workspaces/ + ├─ session_abc123/ + │ ├─ .venv/ ← 这个会话的环境 + │ ├─ files/ ← 这个会话的文件 + │ ├─ user_id.txt ← 记录归属 + │ └─ .agent_memory.json + ├─ session_def456/ + │ ├─ .venv/ + │ └─ files/ + └─ session_ghi789/ + └─ ... + +会话结束 → 自动删除或归档 +``` + +**优点**: +- ✅ 完全隔离(每个会话独立环境) +- ✅ 会话结束直接删除,不占空间 +- ✅ 不会互相污染 +- ✅ 安全性最高 + +**缺点**: +- ❌ 无法跨会话访问文件 +- ❌ 每次都要重新装包(慢!) +- ❌ 资源浪费(每个会话都装一遍 pandas) +- ❌ 用户体验差(上次生成的文件这次看不到) + +**适用场景**: +- 一次性任务 +- 安全要求极高 +- 不需要文件持久化 + +--- + +### 📊 方案3: 统一环境 + 用户文件隔离 ⭐ 推荐 + +``` +/data/ + ├─ shared_env/ + │ ├─ base.venv/ ← 预装常用包的基础环境 + │ │ ├─ pandas + │ │ ├─ numpy + │ │ ├─ reportlab + │ │ ├─ python-pptx + │ │ └─ openpyxl + │ └─ allowed_packages.txt ← 白名单 + │ + └─ workspaces/ + ├─ user_12345/ + │ ├─ sessions/ + │ │ ├─ session_abc/ + │ │ │ ├─ files/ ← 会话文件 + │ │ │ └─ logs/ + │ │ └─ session_def/ + │ │ └─ files/ + │ └─ shared_files/ ← 跨会话共享文件 + └─ user_67890/ + └─ ... +``` + +**工作流程**: +```python +# 1. 创建会话时 +workspace = f"/data/workspaces/user_{user_id}/sessions/session_{session_id}" +os.makedirs(workspace) + +# 2. 使用共享环境(只读) +shared_venv = "/data/shared_env/base.venv" + +# 3. 如果需要额外的包 +if package in allowed_packages: + # 在用户空间临时安装 + uv pip install --prefix {workspace}/.local {package} +else: + raise PermissionError("Package not allowed") + +# 4. 会话结束 +# - 保留文件到 shared_files/ +# - 删除临时数据 +``` + +**优点**: +- ✅ 常用包预装,启动快 +- ✅ 用户之间完全隔离 +- ✅ 会话之间可以共享文件(shared_files) +- ✅ 可以限制允许安装的包 +- ✅ 资源占用适中 + +**缺点**: +- ⚠️ 需要维护共享环境 +- ⚠️ 白名单管理有成本 + +**适用场景**: ⭐ **生产环境推荐** +- 多用户 SaaS +- 需要性能和安全平衡 +- 有运维能力 + +--- + +### 📊 方案4: Docker 容器隔离(最安全) + +``` +每个会话一个容器: + +docker run --rm \ + --name "session_abc123" \ + -v /data/workspaces/user_12345/session_abc:/workspace \ + --cpus=0.5 \ + --memory=512m \ + --pids-limit=50 \ + --network=agent-net \ # 受限网络 + --read-only \ # 只读根文件系统 + --tmpfs /tmp:size=100m \ + mini-agent:latest +``` + +**镜像构建**: +```dockerfile +FROM python:3.11-slim +RUN uv venv /opt/venv && \ + /opt/venv/bin/pip install pandas numpy reportlab python-pptx openpyxl +COPY mini_agent /app/mini_agent +WORKDIR /workspace +CMD ["python", "-m", "mini_agent.agent_server"] +``` + +**优点**: +- ✅ 完全隔离(进程、网络、文件系统) +- ✅ 资源限制(CPU、内存、进程数) +- ✅ 安全性最高 +- ✅ 可以预装环境 +- ✅ 崩溃不影响宿主机 + +**缺点**: +- ❌ 需要 Docker 环境 +- ❌ 启动稍慢(1-2秒) +- ❌ 运维复杂度高 + +**适用场景**: ⭐ **大规模生产环境** +- 安全要求极高 +- 用户量大 +- 有 DevOps 团队 + +--- + +## 🎯 推荐方案组合 + +### 开发/小规模(< 1000 用户) +**方案 3: 统一环境 + 用户隔离** + +```python +# FastAPI 后端结构 +/backend/ + ├─ app/ + │ ├─ main.py + │ ├─ routers/ + │ │ ├─ chat.py # 聊天 API + │ │ └─ files.py # 文件管理 API + │ ├─ services/ + │ │ ├─ agent_service.py + │ │ └─ workspace_service.py + │ └─ models/ + │ ├─ user.py + │ └─ session.py + └─ config/ + ├─ allowed_packages.txt + └─ resource_limits.yaml +``` + +### 生产/大规模(> 1000 用户) +**方案 4: Docker 容器** + +```python +# 使用 Kubernetes/Docker Swarm +apiVersion: v1 +kind: Pod +metadata: + name: agent-session-{{ session_id }} +spec: + containers: + - name: mini-agent + image: mini-agent:latest + resources: + limits: + cpu: 500m + memory: 512Mi + volumeMounts: + - name: workspace + mountPath: /workspace +``` + +--- + +## 🔧 实现细节 + +### 方案3 详细设计 + +#### 1. 目录结构 +``` +/data/ + ├─ shared_env/ + │ ├─ base.venv/ + │ ├─ allowed_packages.txt + │ └─ package_cache/ # 预下载的包 + │ + └─ workspaces/ + ├─ user_12345/ + │ ├─ quota.json # 配额信息 + │ ├─ shared_files/ # 跨会话文件 + │ │ ├─ data/ + │ │ └─ outputs/ + │ └─ sessions/ + │ ├─ session_abc/ + │ │ ├─ files/ + │ │ ├─ logs/ + │ │ └─ .local/ # 会话特定的包 + │ └─ session_def/ + └─ user_67890/ +``` + +#### 2. 配额管理 +```yaml +# quota.json +{ + "user_id": "12345", + "limits": { + "max_workspace_size_mb": 1024, # 1GB + "max_sessions": 10, + "max_session_duration_hours": 24, + "max_files_per_session": 100 + }, + "current": { + "workspace_size_mb": 345, + "active_sessions": 2 + } +} +``` + +#### 3. 包白名单 +``` +# allowed_packages.txt +pandas>=2.0.0,<3.0.0 +numpy>=1.24.0,<2.0.0 +reportlab>=4.0.0 +python-pptx>=0.6.0 +openpyxl>=3.1.0 +matplotlib>=3.7.0 +requests>=2.31.0 +# 不允许危险包 +# NOT: os-sys, subprocess32, etc. +``` + +#### 4. Workspace Service +```python +# services/workspace_service.py +import os +import shutil +from pathlib import Path +from typing import Optional + +class WorkspaceService: + def __init__(self, base_path: str = "/data/workspaces"): + self.base_path = Path(base_path) + self.shared_env = Path("/data/shared_env/base.venv") + + def create_session_workspace( + self, + user_id: str, + session_id: str + ) -> Path: + """创建会话工作空间""" + user_dir = self.base_path / f"user_{user_id}" + session_dir = user_dir / "sessions" / session_id + + # 检查配额 + if not self._check_quota(user_id): + raise QuotaExceededError("User quota exceeded") + + # 创建目录 + session_dir.mkdir(parents=True, exist_ok=True) + (session_dir / "files").mkdir(exist_ok=True) + (session_dir / "logs").mkdir(exist_ok=True) + + # 创建符号链接到共享文件 + shared_link = session_dir / "shared" + shared_files = user_dir / "shared_files" + shared_files.mkdir(exist_ok=True) + if not shared_link.exists(): + shared_link.symlink_to(shared_files) + + return session_dir + + def cleanup_session( + self, + user_id: str, + session_id: str, + keep_files: bool = True + ): + """清理会话""" + session_dir = ( + self.base_path / + f"user_{user_id}" / + "sessions" / + session_id + ) + + if keep_files: + # 移动重要文件到 shared_files + files_dir = session_dir / "files" + if files_dir.exists(): + for file in files_dir.iterdir(): + if file.suffix in ['.pdf', '.xlsx', '.pptx', '.docx']: + dest = ( + self.base_path / + f"user_{user_id}" / + "shared_files" / + "outputs" / + file.name + ) + shutil.move(str(file), str(dest)) + + # 删除会话目录 + shutil.rmtree(session_dir, ignore_errors=True) + + def _check_quota(self, user_id: str) -> bool: + """检查用户配额""" + user_dir = self.base_path / f"user_{user_id}" + quota_file = user_dir / "quota.json" + + if not quota_file.exists(): + return True + + import json + with open(quota_file) as f: + quota = json.load(f) + + # 检查空间 + current_size = self._get_dir_size(user_dir) + if current_size > quota['limits']['max_workspace_size_mb'] * 1024 * 1024: + return False + + # 检查会话数 + sessions = list((user_dir / "sessions").iterdir()) + if len(sessions) >= quota['limits']['max_sessions']: + return False + + return True + + def _get_dir_size(self, path: Path) -> int: + """获取目录大小(字节)""" + total = 0 + for entry in path.rglob('*'): + if entry.is_file(): + total += entry.stat().st_size + return total +``` + +#### 5. 安全的 Bash Tool +```python +# tools/safe_bash_tool.py +import subprocess +from pathlib import Path +from typing import List + +FORBIDDEN_COMMANDS = [ + 'rm', 'rmdir', 'dd', 'mkfs', # 删除/格式化 + 'curl', 'wget', 'nc', 'telnet', # 网络(除非白名单) + 'sudo', 'su', 'chmod', 'chown', # 权限 + 'kill', 'killall', 'pkill', # 进程管理 +] + +ALLOWED_COMMANDS = [ + 'python', 'uv', 'pip', # Python + 'ls', 'cat', 'echo', 'cd', 'pwd', # 基础命令 + 'mkdir', 'touch', # 安全的文件操作 +] + +class SafeBashTool(BashTool): + def __init__(self, workspace_dir: str, allowed_packages: List[str]): + super().__init__(workspace_dir) + self.allowed_packages = allowed_packages + + async def execute(self, command: str, **kwargs) -> ToolResult: + # 解析命令 + cmd_parts = command.split() + if not cmd_parts: + return ToolResult(success=False, error="Empty command") + + base_cmd = cmd_parts[0] + + # 检查黑名单 + if base_cmd in FORBIDDEN_COMMANDS: + return ToolResult( + success=False, + error=f"Command '{base_cmd}' is not allowed" + ) + + # 检查白名单 + if base_cmd not in ALLOWED_COMMANDS: + return ToolResult( + success=False, + error=f"Command '{base_cmd}' is not in whitelist" + ) + + # 检查 pip install + if 'pip install' in command or 'uv pip install' in command: + packages = self._extract_packages(command) + for pkg in packages: + if pkg not in self.allowed_packages: + return ToolResult( + success=False, + error=f"Package '{pkg}' is not allowed" + ) + + # 执行命令(带超时) + try: + result = subprocess.run( + command, + shell=True, + cwd=self.workspace_dir, + capture_output=True, + text=True, + timeout=30, # 30秒超时 + env={ + **os.environ, + 'PYTHONPATH': str(self.workspace_dir), + } + ) + + return ToolResult( + success=result.returncode == 0, + content=result.stdout, + error=result.stderr if result.returncode != 0 else None + ) + + except subprocess.TimeoutExpired: + return ToolResult( + success=False, + error="Command execution timeout (30s)" + ) + + def _extract_packages(self, command: str) -> List[str]: + """从 pip install 命令提取包名""" + # 简化实现 + parts = command.split() + if 'install' in parts: + idx = parts.index('install') + return [p for p in parts[idx+1:] if not p.startswith('-')] + return [] +``` + +#### 6. FastAPI 集成 +```python +# app/main.py +from fastapi import FastAPI, HTTPException, Depends +from fastapi.responses import StreamingResponse +from pydantic import BaseModel +import asyncio +import uuid + +app = FastAPI(title="Mini-Agent API") + +# 服务初始化 +workspace_service = WorkspaceService() + +class ChatRequest(BaseModel): + user_id: str + session_id: str | None = None + message: str + +class ChatResponse(BaseModel): + session_id: str + message: str + files: list[str] = [] + +@app.post("/api/chat") +async def chat(request: ChatRequest): + """聊天接口""" + # 创建或获取会话 + session_id = request.session_id or str(uuid.uuid4()) + + try: + # 创建工作空间 + workspace = workspace_service.create_session_workspace( + request.user_id, + session_id + ) + + # 创建 Agent + agent = create_agent( + workspace_dir=str(workspace), + user_id=request.user_id + ) + + # 执行任务 + agent.add_user_message(request.message) + response = await agent.run() + + # 获取生成的文件 + files = list((workspace / "files").glob("*")) + + return ChatResponse( + session_id=session_id, + message=response, + files=[f.name for f in files] + ) + + except QuotaExceededError: + raise HTTPException(status_code=429, detail="Quota exceeded") + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +@app.get("/api/files/{user_id}/{filename}") +async def download_file(user_id: str, filename: str): + """下载文件""" + file_path = ( + Path("/data/workspaces") / + f"user_{user_id}" / + "shared_files" / + "outputs" / + filename + ) + + if not file_path.exists(): + raise HTTPException(status_code=404, detail="File not found") + + return FileResponse(file_path) + +@app.delete("/api/sessions/{user_id}/{session_id}") +async def cleanup_session(user_id: str, session_id: str): + """清理会话""" + workspace_service.cleanup_session(user_id, session_id) + return {"status": "success"} +``` + +--- + +## 📝 总结 + +### 推荐选择: + +1. **快速原型/个人使用**: 方案3(统一环境 + 用户隔离) +2. **生产环境**: 方案4(Docker 容器)+ 方案3 的文件组织 + +### 关键考虑点: + +| 维度 | 方案3 | 方案4 | +|------|-------|-------| +| 安全性 | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | +| 性能 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | +| 运维复杂度 | ⭐⭐⭐ | ⭐⭐ | +| 资源效率 | ⭐⭐⭐⭐ | ⭐⭐⭐ | +| 扩展性 | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | + +### 实施路径: + +``` +阶段1: 本地开发 +└─ 使用方案3,单机部署 + +阶段2: 小规模生产 +└─ 方案3 + Nginx + Redis(会话缓存) + +阶段3: 大规模生产 +└─ 迁移到方案4(Docker/K8s)+ 分布式存储 +``` diff --git a/docs/FASTAPI_BACKEND_ARCHITECTURE.md b/docs/FASTAPI_BACKEND_ARCHITECTURE.md new file mode 100644 index 0000000..ba3ac1a --- /dev/null +++ b/docs/FASTAPI_BACKEND_ARCHITECTURE.md @@ -0,0 +1,1452 @@ +# Mini-Agent FastAPI 后端架构设计 + +> 基于讨论确定的方案:统一环境 + 用户文件隔离 + SQLite + 多轮对话 + +## 📋 目录 +- [架构概览](#架构概览) +- [目录结构](#目录结构) +- [数据库设计](#数据库设计) +- [核心模块](#核心模块) +- [API 接口](#api-接口) +- [Workspace 管理](#workspace-管理) +- [安全机制](#安全机制) +- [部署配置](#部署配置) + +--- + +## 架构概览 + +### 核心架构图 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ 前端 (React/Vue) │ +│ • 会话列表 • 对话界面 • 文件管理 • 用户设置 │ +└────────────────────────────┬────────────────────────────────────┘ + │ HTTP/WebSocket + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ FastAPI 应用层 │ +├─────────────────────────────────────────────────────────────────┤ +│ Routers: │ +│ ├─ /api/auth - 认证授权 │ +│ ├─ /api/sessions - 会话管理 │ +│ ├─ /api/chat - 对话接口 │ +│ ├─ /api/files - 文件管理 │ +│ └─ /api/admin - 管理接口 │ +└────────────────────────────┬────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ 业务逻辑层 (Services) │ +├─────────────────────────────────────────────────────────────────┤ +│ • SessionService - 会话管理 │ +│ • AgentService - Agent 执行 │ +│ • WorkspaceService - 工作空间管理 │ +│ • HistoryService - 对话历史 │ +│ • FileService - 文件管理 │ +│ • QuotaService - 配额管理 │ +└────────────────────────────┬────────────────────────────────────┘ + │ + ┌──────────────┼──────────────┐ + │ │ │ + ▼ ▼ ▼ + ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ + │ SQLite │ │ Workspace │ │ Mini-Agent │ + │ 数据库 │ │ 文件系统 │ │ Core │ + └─────────────┘ └─────────────┘ └─────────────┘ +``` + +### 技术栈 + +```yaml +后端框架: FastAPI 0.104+ +数据库: SQLite 3.x (生产可升级 PostgreSQL) +ORM: SQLAlchemy 2.0+ +认证: JWT (python-jose) +任务队列: asyncio (简单) / Celery (复杂) +缓存: 内存 dict (简单) / Redis (生产) +日志: loguru +配置: pydantic-settings +``` + +--- + +## 目录结构 + +``` +backend/ +├── app/ +│ ├── __init__.py +│ ├── main.py # FastAPI 应用入口 +│ ├── config.py # 配置管理 +│ ├── dependencies.py # 依赖注入 +│ │ +│ ├── api/ # API 路由 +│ │ ├── __init__.py +│ │ ├── auth.py # 认证接口 +│ │ ├── sessions.py # 会话管理 +│ │ ├── chat.py # 对话接口 +│ │ ├── files.py # 文件管理 +│ │ └── admin.py # 管理接口 +│ │ +│ ├── services/ # 业务逻辑 +│ │ ├── __init__.py +│ │ ├── session_service.py # 会话服务 +│ │ ├── agent_service.py # Agent 服务 +│ │ ├── workspace_service.py # 工作空间服务 +│ │ ├── history_service.py # 历史服务 +│ │ ├── file_service.py # 文件服务 +│ │ └── quota_service.py # 配额服务 +│ │ +│ ├── models/ # 数据模型 +│ │ ├── __init__.py +│ │ ├── database.py # 数据库配置 +│ │ ├── user.py # 用户模型 +│ │ ├── session.py # 会话模型 +│ │ ├── message.py # 消息模型 +│ │ └── file.py # 文件模型 +│ │ +│ ├── schemas/ # Pydantic 模式 +│ │ ├── __init__.py +│ │ ├── auth.py # 认证请求/响应 +│ │ ├── session.py # 会话请求/响应 +│ │ ├── chat.py # 对话请求/响应 +│ │ └── file.py # 文件请求/响应 +│ │ +│ ├── core/ # 核心组件 +│ │ ├── __init__.py +│ │ ├── security.py # 安全相关 +│ │ ├── agent_wrapper.py # Agent 包装器 +│ │ └── allowed_packages.py # 包白名单 +│ │ +│ └── utils/ # 工具函数 +│ ├── __init__.py +│ ├── logger.py # 日志配置 +│ └── helpers.py # 辅助函数 +│ +├── data/ # 数据目录 +│ ├── database/ +│ │ └── mini_agent.db # SQLite 数据库 +│ ├── shared_env/ # 共享环境 +│ │ ├── base.venv/ # 预装包的虚拟环境 +│ │ └── allowed_packages.txt # 包白名单 +│ └── workspaces/ # 用户工作空间 +│ ├── user_{user_id}/ +│ │ ├── shared_files/ # 持久化文件 +│ │ │ ├── outputs/ # 生成的文档 +│ │ │ └── data/ # 数据文件 +│ │ └── sessions/ +│ │ └── {session_id}/ +│ │ ├── files/ # 会话临时文件 +│ │ └── logs/ # 会话日志 +│ └── user_{another_id}/ +│ +├── tests/ # 测试 +│ ├── __init__.py +│ ├── test_api/ +│ ├── test_services/ +│ └── test_models/ +│ +├── scripts/ # 脚本 +│ ├── init_db.py # 初始化数据库 +│ ├── setup_shared_env.py # 设置共享环境 +│ └── migrate.py # 数据迁移 +│ +├── requirements.txt # 依赖 +├── .env.example # 环境变量示例 +├── alembic.ini # 数据库迁移配置 +└── README.md +``` + +--- + +## 数据库设计 + +### ER 图 + +``` +┌──────────────┐ ┌──────────────┐ ┌──────────────┐ +│ users │───┐ │ sessions │───┐ │ messages │ +├──────────────┤ │ ├──────────────┤ │ ├──────────────┤ +│ id (PK) │ └──<│ user_id (FK) │ └──<│ session_id │ +│ username │ │ id (PK) │ │ id (PK) │ +│ email │ │ created_at │ │ role │ +│ hashed_pwd │ │ last_active │ │ content │ +│ created_at │ │ closed_at │ │ thinking │ +│ is_active │ │ status │ │ tool_calls │ +│ quota_* │ │ title │ │ created_at │ +└──────────────┘ └──────────────┘ └──────────────┘ + │ + │ + ▼ + ┌──────────────┐ + │ session_files│ + ├──────────────┤ + │ id (PK) │ + │ session_id │ + │ filename │ + │ file_path │ + │ file_size │ + │ mime_type │ + │ created_at │ + └──────────────┘ +``` + +### SQL Schema + +```sql +-- 用户表 +CREATE TABLE users ( + id VARCHAR(36) PRIMARY KEY, + username VARCHAR(100) UNIQUE NOT NULL, + email VARCHAR(255) UNIQUE NOT NULL, + hashed_password VARCHAR(255) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + is_active BOOLEAN DEFAULT TRUE, + + -- 配额字段 + quota_max_sessions INTEGER DEFAULT 10, + quota_max_storage_mb INTEGER DEFAULT 1024, + quota_max_session_duration_hours INTEGER DEFAULT 24, + + INDEX idx_username (username), + INDEX idx_email (email) +); + +-- 会话表 +CREATE TABLE sessions ( + id VARCHAR(36) PRIMARY KEY, + user_id VARCHAR(36) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + last_active TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + closed_at TIMESTAMP NULL, + status VARCHAR(20) DEFAULT 'active', -- active, closed, expired + title VARCHAR(255) NULL, + + -- 统计字段 + message_count INTEGER DEFAULT 0, + turn_count INTEGER DEFAULT 0, + + INDEX idx_user_id (user_id), + INDEX idx_status (status), + INDEX idx_created_at (created_at), + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE +); + +-- 消息表 +CREATE TABLE messages ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + session_id VARCHAR(36) NOT NULL, + role VARCHAR(20) NOT NULL, -- system, user, assistant, tool + content TEXT, + thinking TEXT, + tool_calls TEXT, -- JSON string + tool_call_id VARCHAR(100), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + + INDEX idx_session_id (session_id), + INDEX idx_created_at (created_at), + FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE +); + +-- 会话文件表 +CREATE TABLE session_files ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + session_id VARCHAR(36) NOT NULL, + filename VARCHAR(255) NOT NULL, + file_path VARCHAR(500) NOT NULL, + file_size INTEGER, + mime_type VARCHAR(100), + is_preserved BOOLEAN DEFAULT FALSE, -- 是否已保存到 shared_files + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + + INDEX idx_session_id (session_id), + INDEX idx_filename (filename), + FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE +); + +-- 用户配额使用记录表 +CREATE TABLE quota_usage ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id VARCHAR(36) NOT NULL, + date DATE NOT NULL, + sessions_created INTEGER DEFAULT 0, + storage_used_mb INTEGER DEFAULT 0, + + UNIQUE(user_id, date), + INDEX idx_user_date (user_id, date), + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE +); +``` + +--- + +## 核心模块 + +### 1. 配置管理 (`app/config.py`) + +```python +from pydantic_settings import BaseSettings +from functools import lru_cache +from pathlib import Path + +class Settings(BaseSettings): + # 应用配置 + app_name: str = "Mini-Agent Backend" + app_version: str = "0.1.0" + debug: bool = False + + # API 配置 + api_prefix: str = "/api" + cors_origins: list[str] = ["http://localhost:3000"] + + # 数据库配置 + database_url: str = "sqlite:///./data/database/mini_agent.db" + + # JWT 配置 + secret_key: str # 必须设置 + algorithm: str = "HS256" + access_token_expire_minutes: int = 60 * 24 # 24 小时 + + # MiniMax API 配置 + minimax_api_key: str + minimax_api_base: str = "https://api.minimax.chat" + minimax_model: str = "MiniMax-Text-01" + + # 工作空间配置 + workspace_base: Path = Path("./data/workspaces") + shared_env_path: Path = Path("./data/shared_env/base.venv") + allowed_packages_file: Path = Path("./data/shared_env/allowed_packages.txt") + + # 配额默认值 + default_max_sessions: int = 10 + default_max_storage_mb: int = 1024 + default_max_session_duration_hours: int = 24 + + # Agent 配置 + agent_max_steps: int = 100 + agent_token_limit: int = 80000 + + # 文件保留配置 + preserve_file_extensions: list[str] = [".pdf", ".xlsx", ".pptx", ".docx"] + + class Config: + env_file = ".env" + env_file_encoding = "utf-8" + +@lru_cache() +def get_settings() -> Settings: + return Settings() +``` + +### 2. 数据库模型 (`app/models/`) + +#### `database.py` +```python +from sqlalchemy import create_engine +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import sessionmaker +from app.config import get_settings + +settings = get_settings() + +# 创建引擎 +engine = create_engine( + settings.database_url, + connect_args={"check_same_thread": False}, # SQLite 需要 + echo=settings.debug +) + +# 会话工厂 +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + +# Base 类 +Base = declarative_base() + +# 依赖注入:获取数据库会话 +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() +``` + +#### `user.py` +```python +from sqlalchemy import Column, String, Boolean, Integer, DateTime +from datetime import datetime +from .database import Base + +class User(Base): + __tablename__ = "users" + + id = Column(String(36), primary_key=True) + username = Column(String(100), unique=True, nullable=False, index=True) + email = Column(String(255), unique=True, nullable=False, index=True) + hashed_password = Column(String(255), nullable=False) + created_at = Column(DateTime, default=datetime.utcnow) + is_active = Column(Boolean, default=True) + + # 配额 + quota_max_sessions = Column(Integer, default=10) + quota_max_storage_mb = Column(Integer, default=1024) + quota_max_session_duration_hours = Column(Integer, default=24) +``` + +#### `session.py` +```python +from sqlalchemy import Column, String, Integer, DateTime, ForeignKey +from datetime import datetime +from .database import Base + +class Session(Base): + __tablename__ = "sessions" + + id = Column(String(36), primary_key=True) + user_id = Column(String(36), ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True) + created_at = Column(DateTime, default=datetime.utcnow, index=True) + last_active = Column(DateTime, default=datetime.utcnow) + closed_at = Column(DateTime, nullable=True) + status = Column(String(20), default="active", index=True) + title = Column(String(255), nullable=True) + + message_count = Column(Integer, default=0) + turn_count = Column(Integer, default=0) +``` + +#### `message.py` +```python +from sqlalchemy import Column, String, Integer, Text, DateTime, ForeignKey +from datetime import datetime +from .database import Base + +class Message(Base): + __tablename__ = "messages" + + id = Column(Integer, primary_key=True, autoincrement=True) + session_id = Column(String(36), ForeignKey("sessions.id", ondelete="CASCADE"), nullable=False, index=True) + role = Column(String(20), nullable=False) + content = Column(Text, nullable=True) + thinking = Column(Text, nullable=True) + tool_calls = Column(Text, nullable=True) # JSON string + tool_call_id = Column(String(100), nullable=True) + created_at = Column(DateTime, default=datetime.utcnow, index=True) +``` + +### 3. Pydantic Schemas (`app/schemas/`) + +#### `session.py` +```python +from pydantic import BaseModel, Field +from datetime import datetime +from typing import Optional + +class SessionCreate(BaseModel): + """创建会话请求""" + title: Optional[str] = None + +class SessionResponse(BaseModel): + """会话响应""" + id: str + user_id: str + created_at: datetime + last_active: datetime + status: str + title: Optional[str] + message_count: int + turn_count: int + + class Config: + from_attributes = True + +class SessionList(BaseModel): + """会话列表响应""" + sessions: list[SessionResponse] + total: int +``` + +#### `chat.py` +```python +from pydantic import BaseModel, Field +from typing import Optional, List + +class ChatRequest(BaseModel): + """对话请求""" + message: str = Field(..., min_length=1, max_length=10000) + +class ChatResponse(BaseModel): + """对话响应""" + session_id: str + message: str + thinking: Optional[str] = None + files: List[str] = [] + turn: int + message_count: int +``` + +### 4. 业务服务 (`app/services/`) + +#### `workspace_service.py` +```python +from pathlib import Path +import shutil +from typing import List +from app.config import get_settings + +settings = get_settings() + +class WorkspaceService: + """工作空间管理服务""" + + def __init__(self): + self.base_path = settings.workspace_base + self.base_path.mkdir(parents=True, exist_ok=True) + + def create_session_workspace(self, user_id: str, session_id: str) -> Path: + """创建会话工作空间""" + session_dir = self._get_session_dir(user_id, session_id) + + # 创建目录结构 + session_dir.mkdir(parents=True, exist_ok=True) + (session_dir / "files").mkdir(exist_ok=True) + (session_dir / "logs").mkdir(exist_ok=True) + + # 创建符号链接到 shared_files + shared_dir = self._get_user_shared_dir(user_id) + shared_dir.mkdir(parents=True, exist_ok=True) + + shared_link = session_dir / "shared" + if not shared_link.exists(): + shared_link.symlink_to(shared_dir, target_is_directory=True) + + return session_dir + + def cleanup_session( + self, + user_id: str, + session_id: str, + preserve_files: bool = True + ) -> List[str]: + """清理会话工作空间""" + session_dir = self._get_session_dir(user_id, session_id) + preserved_files = [] + + if preserve_files: + # 保留特定格式的文件 + files_dir = session_dir / "files" + if files_dir.exists(): + for file in files_dir.iterdir(): + if file.suffix.lower() in settings.preserve_file_extensions: + # 移动到 shared_files/outputs + dest_dir = self._get_user_shared_dir(user_id) / "outputs" + dest_dir.mkdir(parents=True, exist_ok=True) + + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + dest_file = dest_dir / f"{timestamp}_{file.name}" + shutil.copy2(file, dest_file) + preserved_files.append(str(dest_file)) + + # 删除会话目录 + if session_dir.exists(): + shutil.rmtree(session_dir, ignore_errors=True) + + return preserved_files + + def _get_session_dir(self, user_id: str, session_id: str) -> Path: + """获取会话目录路径""" + return self.base_path / f"user_{user_id}" / "sessions" / session_id + + def _get_user_shared_dir(self, user_id: str) -> Path: + """获取用户共享目录路径""" + return self.base_path / f"user_{user_id}" / "shared_files" + + def get_session_files(self, user_id: str, session_id: str) -> List[Path]: + """获取会话的所有文件""" + files_dir = self._get_session_dir(user_id, session_id) / "files" + if not files_dir.exists(): + return [] + return list(files_dir.iterdir()) +``` + +#### `history_service.py` +```python +from sqlalchemy.orm import Session as DBSession +from app.models.message import Message +from app.models.session import Session +from typing import List, Dict +import json + +class HistoryService: + """对话历史服务""" + + def __init__(self, db: DBSession): + self.db = db + + def save_message( + self, + session_id: str, + role: str, + content: str = None, + thinking: str = None, + tool_calls: List[Dict] = None, + tool_call_id: str = None + ) -> Message: + """保存消息到数据库""" + message = Message( + session_id=session_id, + role=role, + content=content, + thinking=thinking, + tool_calls=json.dumps(tool_calls, ensure_ascii=False) if tool_calls else None, + tool_call_id=tool_call_id + ) + self.db.add(message) + self.db.commit() + self.db.refresh(message) + + # 更新会话的消息计数 + session = self.db.query(Session).filter(Session.id == session_id).first() + if session: + session.message_count += 1 + if role == "user": + session.turn_count += 1 + self.db.commit() + + return message + + def load_session_history(self, session_id: str) -> List[Dict]: + """加载会话历史""" + messages = self.db.query(Message)\ + .filter(Message.session_id == session_id)\ + .order_by(Message.created_at)\ + .all() + + return [ + { + "role": msg.role, + "content": msg.content, + "thinking": msg.thinking, + "tool_calls": json.loads(msg.tool_calls) if msg.tool_calls else None, + "tool_call_id": msg.tool_call_id, + "created_at": msg.created_at.isoformat() + } + for msg in messages + ] + + def get_message_count(self, session_id: str) -> int: + """获取消息数量""" + return self.db.query(Message)\ + .filter(Message.session_id == session_id)\ + .count() +``` + +#### `agent_service.py` +```python +from mini_agent.agent import Agent +from mini_agent.llm import LLMClient +from mini_agent.schema import LLMProvider, Message as AgentMessage +from app.services.history_service import HistoryService +from app.core.agent_wrapper import create_safe_agent +from typing import List, Dict +from pathlib import Path + +class AgentService: + """Agent 服务""" + + def __init__( + self, + workspace_dir: Path, + history_service: HistoryService, + session_id: str + ): + self.workspace_dir = workspace_dir + self.history_service = history_service + self.session_id = session_id + self.agent = None + self._last_saved_index = 0 + + def initialize_agent(self, system_prompt: str, tools: List): + """初始化 Agent""" + self.agent = create_safe_agent( + workspace_dir=str(self.workspace_dir), + system_prompt=system_prompt, + tools=tools + ) + + # 从数据库恢复历史 + self._restore_history() + + def _restore_history(self): + """从数据库恢复对话历史""" + history = self.history_service.load_session_history(self.session_id) + + # 跳过 system message(index 0) + for msg_data in history: + if msg_data["role"] == "user": + self.agent.messages.append( + AgentMessage(role="user", content=msg_data["content"]) + ) + elif msg_data["role"] == "assistant": + self.agent.messages.append( + AgentMessage( + role="assistant", + content=msg_data["content"], + thinking=msg_data.get("thinking"), + tool_calls=msg_data.get("tool_calls") + ) + ) + elif msg_data["role"] == "tool": + self.agent.messages.append( + AgentMessage( + role="tool", + content=msg_data["content"], + tool_call_id=msg_data.get("tool_call_id") + ) + ) + + self._last_saved_index = len(self.agent.messages) + + async def chat(self, user_message: str) -> Dict: + """执行对话""" + # 保存用户消息 + self.history_service.save_message( + session_id=self.session_id, + role="user", + content=user_message + ) + + # 添加到 agent + self.agent.add_user_message(user_message) + + # 执行 agent + response = await self.agent.run() + + # 保存 agent 生成的消息 + self._save_new_messages() + + return { + "response": response, + "message_count": len(self.agent.messages) + } + + def _save_new_messages(self): + """保存新增的消息到数据库""" + for msg in self.agent.messages[self._last_saved_index:]: + if msg.role == "assistant": + self.history_service.save_message( + session_id=self.session_id, + role="assistant", + content=msg.content, + thinking=msg.thinking, + tool_calls=[tc.dict() for tc in msg.tool_calls] if msg.tool_calls else None + ) + elif msg.role == "tool": + self.history_service.save_message( + session_id=self.session_id, + role="tool", + content=msg.content, + tool_call_id=msg.tool_call_id + ) + + self._last_saved_index = len(self.agent.messages) +``` + +--- + +## API 接口 + +### 路由结构 + +```python +# app/main.py +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware +from app.config import get_settings +from app.api import auth, sessions, chat, files, admin + +settings = get_settings() + +app = FastAPI( + title=settings.app_name, + version=settings.app_version, + docs_url="/api/docs", + redoc_url="/api/redoc" +) + +# CORS +app.add_middleware( + CORSMiddleware, + allow_origins=settings.cors_origins, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# 路由 +app.include_router(auth.router, prefix=f"{settings.api_prefix}/auth", tags=["认证"]) +app.include_router(sessions.router, prefix=f"{settings.api_prefix}/sessions", tags=["会话"]) +app.include_router(chat.router, prefix=f"{settings.api_prefix}/chat", tags=["对话"]) +app.include_router(files.router, prefix=f"{settings.api_prefix}/files", tags=["文件"]) +app.include_router(admin.router, prefix=f"{settings.api_prefix}/admin", tags=["管理"]) + +@app.get("/") +async def root(): + return {"message": "Mini-Agent API", "version": settings.app_version} + +@app.get("/health") +async def health(): + return {"status": "healthy"} +``` + +### 核心接口 + +#### 1. 会话管理 (`app/api/sessions.py`) + +```python +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.orm import Session as DBSession +from app.models.database import get_db +from app.models.session import Session +from app.models.user import User +from app.schemas.session import SessionCreate, SessionResponse, SessionList +from app.services.workspace_service import WorkspaceService +from app.core.security import get_current_user +from datetime import datetime +import uuid + +router = APIRouter() + +@router.post("", response_model=SessionResponse) +async def create_session( + request: SessionCreate, + current_user: User = Depends(get_current_user), + db: DBSession = Depends(get_db) +): + """创建新会话""" + # 检查配额 + active_sessions = db.query(Session).filter( + Session.user_id == current_user.id, + Session.status == "active" + ).count() + + if active_sessions >= current_user.quota_max_sessions: + raise HTTPException( + status_code=429, + detail=f"已达到最大会话数限制 ({current_user.quota_max_sessions})" + ) + + # 创建会话 + session_id = str(uuid.uuid4()) + session = Session( + id=session_id, + user_id=current_user.id, + title=request.title + ) + db.add(session) + db.commit() + db.refresh(session) + + # 创建工作空间 + workspace_service = WorkspaceService() + workspace_service.create_session_workspace(current_user.id, session_id) + + return session + +@router.get("", response_model=SessionList) +async def list_sessions( + limit: int = 20, + offset: int = 0, + current_user: User = Depends(get_current_user), + db: DBSession = Depends(get_db) +): + """获取用户的会话列表""" + sessions = db.query(Session)\ + .filter(Session.user_id == current_user.id)\ + .order_by(Session.created_at.desc())\ + .limit(limit)\ + .offset(offset)\ + .all() + + total = db.query(Session)\ + .filter(Session.user_id == current_user.id)\ + .count() + + return SessionList(sessions=sessions, total=total) + +@router.get("/{session_id}", response_model=SessionResponse) +async def get_session( + session_id: str, + current_user: User = Depends(get_current_user), + db: DBSession = Depends(get_db) +): + """获取会话详情""" + session = db.query(Session).filter( + Session.id == session_id, + Session.user_id == current_user.id + ).first() + + if not session: + raise HTTPException(status_code=404, detail="会话不存在") + + return session + +@router.delete("/{session_id}") +async def close_session( + session_id: str, + preserve_files: bool = True, + current_user: User = Depends(get_current_user), + db: DBSession = Depends(get_db) +): + """关闭会话""" + session = db.query(Session).filter( + Session.id == session_id, + Session.user_id == current_user.id + ).first() + + if not session: + raise HTTPException(status_code=404, detail="会话不存在") + + # 清理工作空间 + workspace_service = WorkspaceService() + preserved = workspace_service.cleanup_session( + current_user.id, + session_id, + preserve_files=preserve_files + ) + + # 更新数据库 + session.status = "closed" + session.closed_at = datetime.utcnow() + db.commit() + + return { + "status": "closed", + "preserved_files": preserved + } +``` + +#### 2. 对话接口 (`app/api/chat.py`) + +```python +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.orm import Session as DBSession +from app.models.database import get_db +from app.models.session import Session +from app.models.user import User +from app.schemas.chat import ChatRequest, ChatResponse +from app.services.agent_service import AgentService +from app.services.history_service import HistoryService +from app.services.workspace_service import WorkspaceService +from app.core.security import get_current_user +from app.core.agent_wrapper import load_tools, load_system_prompt +from datetime import datetime + +router = APIRouter() + +# 内存中的 Agent 实例缓存 +_agent_cache = {} + +@router.post("/{session_id}", response_model=ChatResponse) +async def chat( + session_id: str, + request: ChatRequest, + current_user: User = Depends(get_current_user), + db: DBSession = Depends(get_db) +): + """发送消息并获取响应""" + # 验证会话 + session = db.query(Session).filter( + Session.id == session_id, + Session.user_id == current_user.id + ).first() + + if not session: + raise HTTPException(status_code=404, detail="会话不存在") + + if session.status != "active": + raise HTTPException(status_code=410, detail="会话已关闭") + + # 检查会话是否过期 + if (datetime.utcnow() - session.created_at).total_seconds() > \ + current_user.quota_max_session_duration_hours * 3600: + session.status = "expired" + db.commit() + raise HTTPException(status_code=410, detail="会话已过期") + + # 获取或创建 Agent Service + if session_id not in _agent_cache: + workspace_service = WorkspaceService() + workspace_dir = workspace_service._get_session_dir(current_user.id, session_id) + + history_service = HistoryService(db) + agent_service = AgentService(workspace_dir, history_service, session_id) + + # 初始化 Agent + system_prompt = load_system_prompt() + tools = load_tools(workspace_dir) + agent_service.initialize_agent(system_prompt, tools) + + _agent_cache[session_id] = agent_service + else: + agent_service = _agent_cache[session_id] + + # 执行对话 + result = await agent_service.chat(request.message) + + # 更新会话活跃时间 + session.last_active = datetime.utcnow() + db.commit() + + # 获取生成的文件 + workspace_service = WorkspaceService() + files = workspace_service.get_session_files(current_user.id, session_id) + + return ChatResponse( + session_id=session_id, + message=result["response"], + files=[f.name for f in files], + turn=session.turn_count, + message_count=result["message_count"] + ) +``` + +#### 3. 文件管理 (`app/api/files.py`) + +```python +from fastapi import APIRouter, Depends, HTTPException +from fastapi.responses import FileResponse +from sqlalchemy.orm import Session as DBSession +from app.models.database import get_db +from app.models.session import Session +from app.models.user import User +from app.services.workspace_service import WorkspaceService +from app.core.security import get_current_user +from typing import List + +router = APIRouter() + +@router.get("/{session_id}") +async def list_files( + session_id: str, + current_user: User = Depends(get_current_user), + db: DBSession = Depends(get_db) +): + """列出会话的所有文件""" + # 验证会话归属 + session = db.query(Session).filter( + Session.id == session_id, + Session.user_id == current_user.id + ).first() + + if not session: + raise HTTPException(status_code=404, detail="会话不存在") + + workspace_service = WorkspaceService() + files = workspace_service.get_session_files(current_user.id, session_id) + + return { + "files": [ + { + "name": f.name, + "size": f.stat().st_size, + "modified": f.stat().st_mtime + } + for f in files + ] + } + +@router.get("/{session_id}/{filename}") +async def download_file( + session_id: str, + filename: str, + current_user: User = Depends(get_current_user), + db: DBSession = Depends(get_db) +): + """下载文件""" + session = db.query(Session).filter( + Session.id == session_id, + Session.user_id == current_user.id + ).first() + + if not session: + raise HTTPException(status_code=404, detail="会话不存在") + + workspace_service = WorkspaceService() + file_path = workspace_service._get_session_dir(current_user.id, session_id) / "files" / filename + + if not file_path.exists(): + raise HTTPException(status_code=404, detail="文件不存在") + + return FileResponse( + path=file_path, + filename=filename, + media_type="application/octet-stream" + ) +``` + +--- + +## 安全机制 + +### 1. 包白名单 (`data/shared_env/allowed_packages.txt`) + +``` +# 数据处理 +pandas>=2.0.0,<3.0.0 +numpy>=1.24.0,<2.0.0 + +# 文档生成 +reportlab>=4.0.0,<5.0.0 +python-pptx>=0.6.0,<1.0.0 +python-docx>=1.0.0,<2.0.0 +openpyxl>=3.1.0,<4.0.0 + +# 可视化 +matplotlib>=3.7.0,<4.0.0 +pillow>=10.0.0,<11.0.0 + +# 网络请求 +requests>=2.31.0,<3.0.0 +httpx>=0.25.0,<1.0.0 + +# 工具 +pyyaml>=6.0,<7.0 +jinja2>=3.1.0,<4.0.0 + +# 禁止的包(不在白名单) +# - os-sys +# - subprocess32 +# - eval, exec 相关 +``` + +### 2. 安全的 Agent 包装器 (`app/core/agent_wrapper.py`) + +```python +from mini_agent.agent import Agent +from mini_agent.llm import LLMClient +from mini_agent.schema import LLMProvider +from mini_agent.tools.base import Tool +from mini_agent.tools.file_tools import ReadTool, WriteTool, EditTool +from mini_agent.tools.bash_tool import BashTool +from app.core.security import SafeBashTool +from app.config import get_settings +from pathlib import Path +from typing import List + +settings = get_settings() + +def create_safe_agent( + workspace_dir: str, + system_prompt: str, + tools: List[Tool] = None +) -> Agent: + """创建安全的 Agent 实例""" + # 创建 LLM 客户端 + llm_client = LLMClient( + api_key=settings.minimax_api_key, + api_base=settings.minimax_api_base, + provider=LLMProvider.ANTHROPIC, + model=settings.minimax_model + ) + + # 加载工具(如果未提供) + if tools is None: + tools = load_tools(Path(workspace_dir)) + + # 创建 Agent + agent = Agent( + llm_client=llm_client, + system_prompt=system_prompt, + tools=tools, + max_steps=settings.agent_max_steps, + workspace_dir=workspace_dir, + token_limit=settings.agent_token_limit + ) + + return agent + +def load_tools(workspace_dir: Path) -> List[Tool]: + """加载受限的工具列表""" + # 读取包白名单 + allowed_packages = [] + if settings.allowed_packages_file.exists(): + allowed_packages = settings.allowed_packages_file.read_text().strip().split('\n') + allowed_packages = [p.split('>=')[0].split('==')[0] for p in allowed_packages if p and not p.startswith('#')] + + tools = [ + # 文件工具(限制在 workspace 内) + ReadTool(workspace_dir=str(workspace_dir)), + WriteTool(workspace_dir=str(workspace_dir)), + EditTool(workspace_dir=str(workspace_dir)), + + # 安全的 Bash 工具 + SafeBashTool( + workspace_dir=str(workspace_dir), + allowed_packages=allowed_packages + ), + ] + + # TODO: 加载 Skills + # TODO: 加载 MCP tools + + return tools + +def load_system_prompt() -> str: + """加载 system prompt""" + # 读取基础 prompt + prompt_file = Path("mini_agent/config/system_prompt.md") + if prompt_file.exists(): + return prompt_file.read_text(encoding="utf-8") + + return "You are Mini-Agent, an AI assistant." +``` + +### 3. 安全的 Bash Tool (`app/core/security.py`) + +```python +from mini_agent.tools.bash_tool import BashTool +from mini_agent.tools.base import ToolResult +import subprocess +from typing import List + +# 命令黑名单 +FORBIDDEN_COMMANDS = { + 'rm', 'rmdir', 'dd', 'mkfs', # 删除/格式化 + 'curl', 'wget', 'nc', 'telnet', # 网络 + 'sudo', 'su', 'chmod', 'chown', # 权限 + 'kill', 'killall', 'pkill', # 进程 + 'shutdown', 'reboot', # 系统 +} + +# 命令白名单 +ALLOWED_COMMANDS = { + 'python', 'python3', 'uv', 'pip', + 'ls', 'cat', 'echo', 'cd', 'pwd', + 'mkdir', 'touch', 'cp', 'mv', + 'grep', 'find', 'head', 'tail', +} + +class SafeBashTool(BashTool): + """安全的 Bash 工具""" + + def __init__(self, workspace_dir: str, allowed_packages: List[str]): + super().__init__(workspace_dir) + self.allowed_packages = allowed_packages + + async def execute(self, command: str, **kwargs) -> ToolResult: + """执行命令(带安全检查)""" + # 解析命令 + cmd_parts = command.split() + if not cmd_parts: + return ToolResult(success=False, error="空命令") + + base_cmd = cmd_parts[0] + + # 黑名单检查 + if base_cmd in FORBIDDEN_COMMANDS: + return ToolResult( + success=False, + error=f"命令 '{base_cmd}' 不允许执行(安全限制)" + ) + + # 白名单检查 + if base_cmd not in ALLOWED_COMMANDS: + return ToolResult( + success=False, + error=f"命令 '{base_cmd}' 不在允许列表中" + ) + + # pip install 检查 + if 'pip install' in command or 'uv pip install' in command: + packages = self._extract_packages(command) + for pkg in packages: + if pkg not in self.allowed_packages: + return ToolResult( + success=False, + error=f"包 '{pkg}' 不在白名单中。允许的包:{', '.join(self.allowed_packages[:10])}..." + ) + + # 执行命令(带超时) + try: + result = subprocess.run( + command, + shell=True, + cwd=self.workspace_dir, + capture_output=True, + text=True, + timeout=60, # 60秒超时 + env={ + 'HOME': self.workspace_dir, + 'PATH': '/usr/local/bin:/usr/bin:/bin', + } + ) + + return ToolResult( + success=result.returncode == 0, + content=result.stdout, + error=result.stderr if result.returncode != 0 else None + ) + + except subprocess.TimeoutExpired: + return ToolResult( + success=False, + error="命令执行超时(60秒)" + ) + except Exception as e: + return ToolResult( + success=False, + error=f"执行失败: {str(e)}" + ) + + def _extract_packages(self, command: str) -> List[str]: + """从 pip install 命令提取包名""" + parts = command.split() + if 'install' not in parts: + return [] + + idx = parts.index('install') + packages = [] + for p in parts[idx + 1:]: + if p.startswith('-'): + break + # 去除版本号 + pkg = p.split('==')[0].split('>=')[0].split('<=')[0] + packages.append(pkg) + + return packages +``` + +--- + +## 部署配置 + +### 环境变量 (`.env`) + +```bash +# 应用配置 +APP_NAME="Mini-Agent Backend" +DEBUG=false + +# JWT +SECRET_KEY="your-secret-key-change-in-production" +ALGORITHM="HS256" +ACCESS_TOKEN_EXPIRE_MINUTES=1440 + +# MiniMax API +MINIMAX_API_KEY="your-minimax-api-key" +MINIMAX_API_BASE="https://api.minimax.chat" +MINIMAX_MODEL="MiniMax-Text-01" + +# 数据库 +DATABASE_URL="sqlite:///./data/database/mini_agent.db" + +# CORS +CORS_ORIGINS=["http://localhost:3000","https://yourdomain.com"] + +# 工作空间 +WORKSPACE_BASE="./data/workspaces" +SHARED_ENV_PATH="./data/shared_env/base.venv" + +# 配额 +DEFAULT_MAX_SESSIONS=10 +DEFAULT_MAX_STORAGE_MB=1024 +DEFAULT_MAX_SESSION_DURATION_HOURS=24 +``` + +### 初始化脚本 (`scripts/init_db.py`) + +```python +"""初始化数据库""" +from app.models.database import Base, engine +from app.models.user import User +from app.models.session import Session +from app.models.message import Message + +def init_db(): + """创建所有表""" + print("创建数据库表...") + Base.metadata.create_all(bind=engine) + print("✅ 数据库初始化完成") + +if __name__ == "__main__": + init_db() +``` + +### 设置共享环境 (`scripts/setup_shared_env.py`) + +```python +"""设置共享 Python 环境""" +import subprocess +from pathlib import Path + +def setup_shared_env(): + """创建并配置共享环境""" + env_path = Path("./data/shared_env/base.venv") + + print("创建虚拟环境...") + subprocess.run(["uv", "venv", str(env_path)], check=True) + + print("安装预设包...") + packages = [ + "pandas>=2.0.0", + "numpy>=1.24.0", + "reportlab>=4.0.0", + "python-pptx>=0.6.0", + "openpyxl>=3.1.0", + "matplotlib>=3.7.0", + ] + + for pkg in packages: + print(f" 安装 {pkg}...") + subprocess.run( + [str(env_path / "bin" / "pip"), "install", pkg], + check=True + ) + + print("✅ 共享环境设置完成") + +if __name__ == "__main__": + setup_shared_env() +``` + +### 启动命令 + +```bash +# 开发模式 +uvicorn app.main:app --reload --host 0.0.0.0 --port 8000 + +# 生产模式 +gunicorn app.main:app -w 4 -k uvicorn.workers.UvicornWorker --bind 0.0.0.0:8000 +``` + +--- + +## 总结 + +### ✅ 核心特性 + +1. **多轮对话** - 支持连续对话和上下文记忆 +2. **用户隔离** - 每个用户独立的工作空间和配额 +3. **会话管理** - 手动创建/关闭,自动过期检测 +4. **对话持久化** - SQLite 存储完整历史 +5. **文件管理** - 自动保留重要文件到共享目录 +6. **安全控制** - 命令白名单 + 包白名单 +7. **配额管理** - 会话数、存储、时长限制 + +### 📊 技术指标 + +- **数据库**: SQLite (可升级 PostgreSQL) +- **并发**: 支持异步处理 +- **性能**: Agent 实例缓存 +- **安全**: 多层防护(命令/包/路径) +- **扩展性**: 模块化设计,易于扩展 + +### 🚀 下一步 + +1. 实现认证系统(JWT) +2. 添加 WebSocket 支持(实时对话) +3. 完善配额管理和监控 +4. 添加文件预览功能 +5. 实现对话导出(PDF/Markdown) diff --git a/frontend/.eslintrc.cjs b/frontend/.eslintrc.cjs new file mode 100644 index 0000000..d6c9537 --- /dev/null +++ b/frontend/.eslintrc.cjs @@ -0,0 +1,18 @@ +module.exports = { + root: true, + env: { browser: true, es2020: true }, + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:react-hooks/recommended', + ], + ignorePatterns: ['dist', '.eslintrc.cjs'], + parser: '@typescript-eslint/parser', + plugins: ['react-refresh'], + rules: { + 'react-refresh/only-export-components': [ + 'warn', + { allowConstantExport: true }, + ], + }, +} diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..1b578f6 --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,359 @@ +# Mini Agent 前端 + +基于 React + TypeScript + Vite + TailwindCSS 构建的现代化对话界面。 + +## 技术栈 + +- **框架**: React 18.2 +- **语言**: TypeScript 5.2 +- **构建工具**: Vite 5.0 +- **样式**: TailwindCSS 3.3 +- **路由**: React Router 6.20 +- **HTTP 客户端**: Axios 1.6 +- **Markdown 渲染**: react-markdown 9.0 +- **图标**: lucide-react +- **日期处理**: date-fns 3.0 + +## 功能特性 + +### 🔐 用户认证 +- 简单的用户名/密码登录 +- 自动 session 管理 +- 路由守卫保护 + +### 💬 会话管理 +- 创建新对话 +- 查看所有会话列表 +- 切换不同会话 +- 删除会话 +- 会话状态显示(活跃/暂停/完成) + +### 🤖 智能对话 +- 实时消息发送和接收 +- Markdown 格式支持 +- 代码高亮显示 +- 思考过程展示 +- 工具调用可视化 +- 消息历史记录 +- 自动滚动到最新消息 + +### 🎨 用户界面 +- 现代化设计 +- 响应式布局 +- 优雅的动画效果 +- 自定义滚动条 +- 自适应输入框 +- 加载状态提示 + +## 项目结构 + +``` +frontend/ +├── public/ # 静态资源 +├── src/ +│ ├── components/ # React 组件 +│ │ ├── Login.tsx # 登录页面 +│ │ ├── SessionList.tsx # 会话列表 +│ │ ├── Chat.tsx # 聊天界面 +│ │ └── Message.tsx # 消息展示 +│ ├── services/ # API 服务 +│ │ └── api.ts # API 客户端 +│ ├── types/ # TypeScript 类型 +│ │ └── index.ts # 类型定义 +│ ├── App.tsx # 主应用 +│ ├── main.tsx # 入口文件 +│ ├── index.css # 全局样式 +│ └── vite-env.d.ts # Vite 类型声明 +├── index.html # HTML 模板 +├── package.json # 依赖配置 +├── tsconfig.json # TypeScript 配置 +├── vite.config.ts # Vite 配置 +├── tailwind.config.js # Tailwind 配置 +├── postcss.config.js # PostCSS 配置 +└── README.md # 项目文档 +``` + +## 快速开始 + +### 1. 安装依赖 + +```bash +cd frontend +npm install +``` + +### 2. 启动开发服务器 + +```bash +npm run dev +``` + +前端将运行在 http://localhost:3000 + +### 3. 构建生产版本 + +```bash +npm run build +``` + +构建产物将输出到 `dist/` 目录。 + +### 4. 预览生产版本 + +```bash +npm run preview +``` + +## API 配置 + +前端通过 Vite 代理连接后端 API: + +```typescript +// vite.config.ts +export default defineConfig({ + server: { + port: 3000, + proxy: { + '/api': { + target: 'http://localhost:8000', + changeOrigin: true, + } + } + } +}) +``` + +如果后端运行在不同的地址,请修改 `target` 配置。 + +## 组件说明 + +### Login 组件 + +用户登录界面,包含: +- 用户名输入 +- 密码输入 +- 错误提示 +- 加载状态 + +**路径**: `src/components/Login.tsx` + +### SessionList 组件 + +会话列表侧边栏,包含: +- 创建新会话按钮 +- 会话列表展示 +- 会话状态标签 +- 删除会话功能 +- 退出登录按钮 + +**路径**: `src/components/SessionList.tsx` + +### Chat 组件 + +主聊天界面,包含: +- 消息历史展示 +- 流式响应支持 +- 消息输入框 +- 发送按钮 +- 错误提示 +- 加载状态 + +**路径**: `src/components/Chat.tsx` + +### Message 组件 + +单个消息展示,包含: +- 用户消息样式 +- AI 消息样式 +- Markdown 渲染 +- 代码高亮 +- 时间戳 +- 思考块展示 +- 工具调用展示 + +**路径**: `src/components/Message.tsx` + +## API 服务层 + +`src/services/api.ts` 提供了完整的 API 封装: + +### 认证 API +- `login(username, password)` - 用户登录 + +### 会话 API +- `createSession()` - 创建新会话 +- `getSessions()` - 获取会话列表 +- `getSessionHistory(chatSessionId)` - 获取会话历史 +- `deleteSession(chatSessionId)` - 删除会话 + +### 对话 API +- `sendMessage(chatSessionId, message)` - 发送消息 +- `sendMessageStream(...)` - 流式发送消息(预留接口) + +## 类型系统 + +所有类型定义在 `src/types/index.ts`: + +- `Session` - 会话类型 +- `Message` - 消息类型 +- `MessageRole` - 消息角色枚举 +- `SessionStatus` - 会话状态枚举 +- `ContentBlock` - 内容块类型(文本/工具/思考) + +## 样式系统 + +### TailwindCSS + +使用 Tailwind 的实用类进行样式开发: + +```tsx +
+ ... +
+``` + +### 主题颜色 + +```javascript +// tailwind.config.js +theme: { + extend: { + colors: { + primary: { + 50: '#f0f9ff', + // ... + 900: '#0c4a6e', + } + } + } +} +``` + +### 自定义样式 + +全局样式在 `src/index.css` 中定义: +- 滚动条样式 +- Markdown 样式 +- 代码块样式 +- 表格样式 + +## 开发规范 + +### TypeScript + +- 所有组件使用 TypeScript +- 为所有 props 定义接口 +- 使用严格模式 + +### 组件规范 + +- 使用函数组件和 Hooks +- 组件文件名使用 PascalCase +- 一个文件一个组件(除非是紧密相关的小组件) + +### 代码风格 + +- 使用 ESLint 进行代码检查 +- 遵循 React Hooks 规则 +- 避免不必要的重渲染 + +```bash +# 运行 ESLint +npm run lint +``` + +## 常见问题 + +### 1. 如何修改 API 地址? + +编辑 `vite.config.ts` 中的 proxy 配置: + +```typescript +proxy: { + '/api': { + target: 'http://your-backend-url', + changeOrigin: true, + } +} +``` + +### 2. 如何添加新的路由? + +在 `src/App.tsx` 中添加新路由: + +```tsx +} /> +``` + +### 3. 如何自定义主题颜色? + +编辑 `tailwind.config.js` 中的颜色配置。 + +### 4. 如何处理跨域问题? + +开发环境使用 Vite 代理解决跨域。生产环境需要后端配置 CORS。 + +## 性能优化 + +- 使用 React.memo 避免不必要的重渲染 +- 使用 useCallback 和 useMemo 优化性能 +- 消息列表虚拟化(如果消息数量很大) +- 图片懒加载 +- 代码分割和路由懒加载 + +## 部署 + +### 构建生产版本 + +```bash +npm run build +``` + +### 部署到静态服务器 + +将 `dist/` 目录部署到任何静态服务器: +- Nginx +- Apache +- Vercel +- Netlify +- GitHub Pages + +### Nginx 配置示例 + +```nginx +server { + listen 80; + server_name yourdomain.com; + root /path/to/dist; + index index.html; + + location / { + try_files $uri $uri/ /index.html; + } + + location /api { + proxy_pass http://localhost:8000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + } +} +``` + +## 浏览器支持 + +- Chrome (最新版本) +- Firefox (最新版本) +- Safari (最新版本) +- Edge (最新版本) + +## 许可证 + +MIT License + +## 相关链接 + +- [后端 API 文档](../backend/README.md) +- [项目主文档](../README.md) +- [React 官方文档](https://react.dev/) +- [Vite 官方文档](https://vitejs.dev/) +- [TailwindCSS 官方文档](https://tailwindcss.com/) diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..2d74d39 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + Mini Agent - 智能对话助手 + + +
+ + + diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..447a293 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,37 @@ +{ + "name": "mini-agent-frontend", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview", + "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0" + }, + "dependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router-dom": "^6.20.1", + "axios": "^1.6.2", + "react-markdown": "^9.0.1", + "remark-gfm": "^4.0.0", + "lucide-react": "^0.294.0", + "date-fns": "^3.0.0" + }, + "devDependencies": { + "@types/react": "^18.2.43", + "@types/react-dom": "^18.2.17", + "@typescript-eslint/eslint-plugin": "^6.14.0", + "@typescript-eslint/parser": "^6.14.0", + "@vitejs/plugin-react": "^4.2.1", + "autoprefixer": "^10.4.16", + "eslint": "^8.55.0", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-refresh": "^0.4.5", + "postcss": "^8.4.32", + "tailwindcss": "^3.3.6", + "typescript": "^5.2.2", + "vite": "^5.0.8" + } +} diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js new file mode 100644 index 0000000..2e7af2b --- /dev/null +++ b/frontend/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 0000000..2cf19a5 --- /dev/null +++ b/frontend/src/App.tsx @@ -0,0 +1,53 @@ +import { useState } from 'react'; +import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'; +import { Login } from './components/Login'; +import { SessionList } from './components/SessionList'; +import { Chat } from './components/Chat'; +import { apiService } from './services/api'; + +// 主页面组件 +function HomePage() { + const [currentSessionId, setCurrentSessionId] = useState(''); + + return ( +
+ + +
+ ); +} + +// 路由守卫组件 +function ProtectedRoute({ children }: { children: React.ReactNode }) { + const isAuthenticated = !!apiService.getSessionId(); + + if (!isAuthenticated) { + return ; + } + + return <>{children}; +} + +function App() { + return ( + + + } /> + + + + } + /> + } /> + + + ); +} + +export default App; diff --git a/frontend/src/components/Chat.tsx b/frontend/src/components/Chat.tsx new file mode 100644 index 0000000..6f76a6b --- /dev/null +++ b/frontend/src/components/Chat.tsx @@ -0,0 +1,247 @@ +import { useEffect, useState, useRef } from 'react'; +import { apiService } from '../services/api'; +import { Message as MessageType, MessageRole } from '../types'; +import { Message, ThinkingBlock, ToolUseBlock } from './Message'; +import { Send, Loader2, AlertCircle, MessageSquare } from 'lucide-react'; + +interface ChatProps { + sessionId: string; +} + +export function Chat({ sessionId }: ChatProps) { + const [messages, setMessages] = useState([]); + const [input, setInput] = useState(''); + const [loading, setLoading] = useState(false); + const [sending, setSending] = useState(false); + const [error, setError] = useState(''); + + // 流式响应状态 + const [streamingText, setStreamingText] = useState(''); + const [streamingThinking, setStreamingThinking] = useState(''); + const [streamingTools, setStreamingTools] = useState }>>([]); + + const messagesEndRef = useRef(null); + const textareaRef = useRef(null); + + useEffect(() => { + if (sessionId) { + loadMessages(); + } + }, [sessionId]); + + useEffect(() => { + scrollToBottom(); + }, [messages, streamingText, streamingThinking, streamingTools]); + + const scrollToBottom = () => { + messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); + }; + + const loadMessages = async () => { + setLoading(true); + setError(''); + try { + const response = await apiService.getSessionHistory(sessionId); + setMessages(response.messages); + } catch (err) { + console.error('Failed to load messages:', err); + setError('加载消息失败'); + } finally { + setLoading(false); + } + }; + + const handleSend = async () => { + if (!input.trim() || sending) return; + + const userMessage = input.trim(); + setInput(''); + setSending(true); + setError(''); + + // 清空流式状态 + setStreamingText(''); + setStreamingThinking(''); + setStreamingTools([]); + + // 立即添加用户消息到界面 + const tempUserMessage: MessageType = { + id: `temp-${Date.now()}`, + session_id: sessionId, + role: MessageRole.USER, + content: userMessage, + created_at: new Date().toISOString(), + }; + setMessages((prev) => [...prev, tempUserMessage]); + + try { + // 使用普通请求(因为流式响应需要特殊处理) + const response = await apiService.sendMessage(sessionId, userMessage); + + // 添加助手响应 + const assistantMessage: MessageType = { + id: `temp-assistant-${Date.now()}`, + session_id: sessionId, + role: MessageRole.ASSISTANT, + content: response.response, + created_at: new Date().toISOString(), + }; + + setMessages((prev) => [...prev, assistantMessage]); + + } catch (err: any) { + console.error('Failed to send message:', err); + + // 提取详细错误信息 + let errorMessage = '发送消息失败'; + if (err.response?.data?.detail) { + errorMessage = err.response.data.detail; + } else if (err.message) { + errorMessage = err.message; + } + + setError(errorMessage); + // 移除临时用户消息 + setMessages((prev) => prev.filter((m) => m.id !== tempUserMessage.id)); + } finally { + setSending(false); + } + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + handleSend(); + } + }; + + // 自动调整 textarea 高度 + useEffect(() => { + if (textareaRef.current) { + textareaRef.current.style.height = 'auto'; + textareaRef.current.style.height = `${textareaRef.current.scrollHeight}px`; + } + }, [input]); + + if (!sessionId) { + return ( +
+
+ +

请选择或创建一个对话

+
+
+ ); + } + + return ( +
+ {/* Messages Area */} +
+ {loading ? ( +
+ +
+ ) : messages.length === 0 && !streamingText ? ( +
+
+ +

开始新的对话

+

向 Mini Agent 提问任何问题

+
+
+ ) : ( +
+ {messages.map((message) => ( + + ))} + + {/* 流式响应显示 */} + {sending && ( +
+
+ +
+
+ {streamingThinking && ( + + )} + {streamingTools.map((tool, idx) => ( + + ))} + {streamingText && ( +
+
+ {streamingText} +
+
+ )} +
+
+ )} + +
+
+ )} +
+ + {/* Error Message */} + {error && ( +
+
+
+ +
+
发生错误
+
{error}
+
+ +
+
+
+ )} + + {/* Input Area */} +
+
+
+