Просмотр исходного кода

feat: SoftwareDevHelper 智能体 - 编程学习助手全功能实现

- 实现智能体核心逻辑,包含 UserMemoryTool 和 CodeTestTool 两个自定义工具
- 完成前后端交互,支持多会话管理、聊天记录持久化、用户记忆侧边栏展示
- 实现自动化测试与打分功能,支持上传 zip 项目压缩包并运行 pytest
- 新增工具调用可视化控件,在聊天界面实时展示工具调用的参数与结果
- 修复 ToolResponse 实例化兼容性问题,改用 ToolResponse.success() / .error() 静态方法
- 修复多会话历史记录恢复时的 JSON 解析错误和 SimpleAgent API 兼容性问题
- 修复提取 tool_calls 时对象属性访问和 arguments 序列化问题
- 优化系统提示词,指导模型编写支持子目录模块动态导入的稳健测试代码
- 完善 .gitignore 配置,忽略运行时数据和框架生成的 trace 文件
- 更新 README.md 和 Agent_Design.md 项目文档
angelen 2 месяцев назад
Родитель
Сommit
11ec1468a6

+ 5 - 1
.gitignore

@@ -162,4 +162,8 @@ cython_debug/
 .obsidian
 
 # 忽略各种文件夹下codex AGENRS.md
-AGENTS.md
+AGENTS.md
+test_*.py
+!test_tools.py
+.workbuddy/
+memory/

+ 3 - 0
Co-creation-projects/angelen-SoftwareDevHelper/.env.example

@@ -0,0 +1,3 @@
+LLM_API_KEY=your_api_key_here
+LLM_BASE_URL=https://api-inference.modelscope.cn/v1/
+LLM_MODEL_ID=Qwen/Qwen2.5-72B-Instruct

+ 12 - 0
Co-creation-projects/angelen-SoftwareDevHelper/.gitignore

@@ -0,0 +1,12 @@
+# 运行时生成的数据目录
+data/
+outputs/
+memory/
+
+# 临时测试脚本
+test_*.py
+!test_tools.py
+
+
+# 内部状态文件 (如果存在)
+.workbuddy/

+ 85 - 0
Co-creation-projects/angelen-SoftwareDevHelper/Agent_Design.md

@@ -0,0 +1,85 @@
+# SoftwareDevHelper 智能体设计文档
+
+## 1. 智能体概述
+
+**名称**:SoftwareDevHelper
+**角色**:专业的软件开发学习助手
+**目标**:为软件开发初学者提供个性化的学习体验,包括水平评估、智能出题、开发指导以及自动化的代码测试与打分。
+**核心框架**:基于 `HelloAgents` 框架的 `SimpleAgent` 范式构建。
+
+## 2. 系统提示词 (System Prompt)
+
+智能体的行为和职责由以下系统提示词严格定义:
+
+```text
+你是一个专业的软件开发学习助手。你的职责是:
+1. 使用 user_memory 工具了解用户的当前编程水平和历史做题记录。
+2. 根据用户水平,为他们出适合的编程题目,或者从网上搜索真实的开发案例。
+3. 在用户开发过程中,提供有针对性的建议和指导。
+4. 当用户完成开发并上传项目压缩包后,你需要:
+   - 仔细分析题目要求。
+   - 编写严谨的 pytest 测试用例代码。
+   - 使用 code_test 工具,传入压缩包路径和你的测试代码,对用户的项目进行自动化测试。
+   - 根据测试结果给出最终打分和详细的代码审查反馈。
+5. 任务完成后,使用 user_memory 工具更新用户的水平评估和做题记录。
+
+请始终保持鼓励和专业的态度。
+```
+
+## 3. 工具与技能 (Tools & Skills)
+
+该智能体挂载了两个核心的自定义工具,用于实现与外部环境(文件系统、测试环境、记忆库)的交互:
+
+### 3.1 UserMemoryTool (用户记忆工具)
+- **功能**:管理用户的编程水平和做题历史记录。实现跨会话的个性化记忆。
+- **参数**:
+  - `action` (string, 必填):操作类型,可选 `'get'`(读取记忆)或 `'update'`(更新记忆)。
+  - `level` (string, 选填):用户的新水平评估(例如 'beginner', 'intermediate', 'advanced')。
+  - `record` (string, 选填):新完成的题目记录。
+- **底层实现**:将数据持久化存储在后端的 `data/user_memory.json` 文件中。
+
+### 3.2 CodeTestTool (代码自动测试工具)
+- **功能**:接收用户上传的项目压缩包,自动解压,并运行由 LLM 动态生成的测试代码,最后给出评分。
+- **参数**:
+  - `zip_path` (string, 必填):用户上传的项目压缩包绝对路径。
+  - `test_code` (string, 必填):由智能体根据题目要求动态生成的 `pytest` 测试代码。
+- **底层实现**:
+  1. 清理并创建解压目录 `outputs/extracted`。
+  2. 使用 `zipfile` 解压用户代码。
+  3. 将 `test_code` 写入 `test_generated.py`。
+  4. 使用 `subprocess` 调用 `pytest` 运行测试,并捕获标准输出和错误信息。
+  5. 根据退出码(`returncode`)给出 100 分(全过)或 0 分(有失败),并返回详细的测试日志供智能体分析。
+
+## 4. 交互流程 (Workflow)
+
+1. **初始化/对话**:用户进入系统,智能体通过 `UserMemoryTool` 获取用户的当前水平(如 beginner)和历史记录。
+2. **出题阶段**:智能体根据记忆信息,生成难度适宜的编程题目。
+3. **开发与指导**:用户在本地开发,期间可通过聊天界面向智能体请教问题,智能体提供代码建议。
+4. **提交与测试**:
+   - 用户通过前端上传 `.zip` 格式的项目代码。
+   - 后端接收文件并保存,将文件路径以系统消息形式发送给智能体。
+   - 智能体编写对应的 `pytest` 测试用例(支持子目录模块动态导入)。
+   - 智能体调用 `CodeTestTool` 执行自动化测试。
+5. **反馈与更新**:
+   - 智能体根据测试输出日志,为用户提供打分和代码审查报告。
+   - 智能体调用 `UserMemoryTool` 将本次题目记录到历史中,并视情况升级用户的水平等级。
+
+## 5. 交互设计 (Interaction Design)
+
+- **输入输出格式**:
+  - 用户输入:自然语言文本、代码片段、项目压缩包 (.zip)。
+  - 智能体输出:格式化的 Markdown 文本、工具调用可视化控件(实时展示工具调用的参数与结果)。
+- **用户界面 (UI)**:
+  - 基于 Web 的聊天界面,提供清晰的对话流。
+  - 支持多会话管理,左侧边栏显示历史会话列表,支持切换和删除。
+  - 右侧边栏实时展示用户的当前水平和历史做题记录。
+  - 聊天气泡内嵌专门的 UI 控件,用于优雅地展示智能体后台调用工具的详细过程。
+- **反馈机制**:
+  - 在打分后,提供详细的代码审查反馈,指出优点和改进空间。
+  - 当测试失败时,明确指出是逻辑错误还是项目结构错误,并提供修改建议。
+  - 在执行工具操作前,智能体会先向用户解释即将进行的操作,保持透明度。
+
+## 6. 配置说明
+
+- **模型配置**:默认使用配置在 `.env` 中的大语言模型(如 `Qwen/Qwen2.5-72B-Instruct`、`Gemini` 或 `Azure OpenAI`)。
+- **特殊配置**:为兼容部分严格校验 JSON Schema 的模型(如 Azure/Gemini),在初始化 `SimpleAgent` 时,通过 `Config(todowrite_enabled=False)` 禁用了框架内置的 `TodoWrite` 工具。

+ 100 - 0
Co-creation-projects/angelen-SoftwareDevHelper/README.md

@@ -0,0 +1,100 @@
+# SoftwareDevHelper - 软件开发学习助手
+
+> 基于 HelloAgents 框架的智能软件开发学习助手,能够记忆用户水平、出题、测试代码并打分。
+
+## 📝 项目简介
+
+SoftwareDevHelper 是一个专为软件开发初学者设计的智能学习助手。它能够:
+- 记忆和评估用户的编程水平。
+- 根据用户水平自动出题或从网上搜索真实案例。
+- 提供开发过程中的智能建议。
+- 用户上传项目压缩包后,自动编写测试样例并进行测试。
+- 对用户的代码进行打分并记录学习轨迹。
+
+本项目包含完整的前后端实现,前端使用 HTML+JavaScript,后端使用 Python (FastAPI) 和 HelloAgents 框架。
+
+## ✨ 核心功能
+
+- [x] **用户水平记忆与评估**:记录用户的做题历史和水平信息(支持前端侧边栏实时查看与修改,跨会话共享)。
+- [x] **智能出题系统**:根据用户当前水平,动态生成编程题目或搜索实际案例。
+- [x] **开发建议助手**:在开发过程中提供代码审查和优化建议。
+- [x] **自动化测试与打分**:接收用户上传的项目压缩包,自动解压、编写稳健的测试用例(支持子目录模块动态导入,避免了暴力全量导入导致触发 Python `antigravity` 彩蛋的问题)、执行测试并给出评分。
+- [x] **完整的前后端交互**:提供友好的 Web 界面供用户交互。
+- [x] **多会话管理**:支持创建多个独立会话,聊天记录持久化存储在后端。支持在会话列表中悬停并优雅地删除历史会话。后端实现了稳健的上下文恢复机制,确保在服务重启或刷新页面后,智能体依然能准确记住之前的对话内容。
+- [x] **工具调用可视化**:在聊天界面中实时渲染智能体调用工具的过程,清晰展示输入参数和执行结果。
+
+## 🛠️ 技术栈
+
+- **智能体框架**:HelloAgents (SimpleAgent, ToolRegistry 等)
+- **后端框架**:FastAPI, Uvicorn
+- **前端技术**:HTML5, CSS3, Vanilla JavaScript
+- **大语言模型**:预留接口支持多种 LLM (如 Qwen 等)
+- **其他工具**:Python `zipfile` (处理压缩包), `pytest` 或内置 `unittest` (自动化测试)
+
+## 🚀 快速开始
+
+### 环境要求
+
+- Python 3.10+
+- 推荐使用 Conda 环境
+
+### 安装依赖
+
+```bash
+pip install -r requirements.txt
+```
+
+### 配置 API 密钥
+
+创建 `.env` 文件并填入相关配置:
+
+```bash
+cp .env.example .env
+```
+
+`.env` 文件内容示例:
+```env
+LLM_API_KEY=your_api_key_here
+LLM_BASE_URL=https://api-inference.modelscope.cn/v1/
+LLM_MODEL_ID=Qwen/Qwen2.5-72B-Instruct
+```
+
+### 运行项目
+
+1. **激活虚拟环境**(如果你使用的是 conda):
+   ```bash
+   conda activate hello-agent-homework
+   ```
+
+2. **进入项目目录并配置路径**:
+   ```bash
+   cd Co-creation-projects/angelen-SoftwareDevHelper
+   export PYTHONPATH=$PYTHONPATH:$(pwd)
+   ```
+
+3. **启动 FastAPI 后端服务**:
+   ```bash
+   uvicorn src.main:app --reload
+   ```
+
+4. **体验项目**:
+   打开浏览器,访问 [http://127.0.0.1:8000](http://127.0.0.1:8000) 即可开始与助手对话。
+
+**💡 常见启动问题与注意事项:**
+- **修改了 `.env` 文件?** `uvicorn` 的 `--reload` 参数默认只会监听 `.py` 代码文件的变化。如果你修改了 API Key 或模型配置,请在终端按 `Ctrl + C` 停止服务,然后重新运行启动命令。
+- **提示端口被占用?** 如果启动时遇到 `[Errno 48] Address already in use`,说明 8000 端口被占用。你可以指定新端口启动:`uvicorn src.main:app --reload --port 8001`,或者在终端执行 `lsof -ti :8000 | xargs kill -9` 杀掉占用该端口的进程。
+
+## 🎯 项目亮点
+
+- **个性化学习**:通过记忆机制实现因材施教。
+- **全链路自动化**:从出题到代码测试打分,实现闭环。
+- **前后端分离**:清晰的架构设计,易于扩展和维护。
+
+## 👤 作者
+
+- GitHub: [@angelen](https://github.com/angelen)
+- 项目链接: [SoftwareDevHelper](https://github.com/datawhalechina/hello-agents/tree/main/Co-creation-projects/angelen-SoftwareDevHelper)
+
+## 🙏 致谢
+
+感谢 Datawhale 社区和 Hello-Agents 项目!

+ 370 - 0
Co-creation-projects/angelen-SoftwareDevHelper/frontend/static/app.js

@@ -0,0 +1,370 @@
+document.addEventListener('DOMContentLoaded', () => {
+    const chatContainer = document.getElementById('chat-container');
+    const userInput = document.getElementById('user-input');
+    const sendBtn = document.getElementById('send-btn');
+    const projectUpload = document.getElementById('project-upload');
+    const fileNameDisplay = document.getElementById('file-name');
+    
+    // 新增元素
+    const sessionList = document.getElementById('session-list');
+    const newSessionBtn = document.getElementById('new-session-btn');
+    const currentSessionTitle = document.getElementById('current-session-title');
+    const userLevelSelect = document.getElementById('user-level');
+    const historyList = document.getElementById('history-list');
+
+    let currentSessionId = null;
+
+    // 自动滚动到底部
+    function scrollToBottom() {
+        chatContainer.scrollTop = chatContainer.scrollHeight;
+    }
+
+    // 添加消息到聊天界面
+    function addMessage(text, isUser = false, toolCalls = null) {
+        const msgDiv = document.createElement('div');
+        msgDiv.className = `message ${isUser ? 'user-message' : 'bot-message'}`;
+
+        // 如果有工具调用信息,先渲染工具调用块
+        if (toolCalls && toolCalls.length > 0) {
+            const toolsContainer = document.createElement('div');
+            toolsContainer.className = 'tool-calls-container';
+            
+            toolCalls.forEach(tc => {
+                const tcDiv = document.createElement('div');
+                tcDiv.className = 'tool-call-block';
+                
+                // 尝试格式化参数和结果
+                let formattedArgs = tc.arguments;
+                try {
+                    formattedArgs = JSON.stringify(JSON.parse(tc.arguments), null, 2);
+                } catch (e) {}
+                
+                let formattedResult = tc.result;
+                try {
+                    formattedResult = JSON.stringify(JSON.parse(tc.result), null, 2);
+                } catch (e) {}
+
+                tcDiv.innerHTML = `
+                    <div class="tool-call-header">
+                        <span class="tool-icon">🛠️</span>
+                        <span class="tool-name">调用工具: <strong>${tc.name}</strong></span>
+                    </div>
+                    <div class="tool-call-details">
+                        <div class="tool-args">
+                            <div class="tool-label">输入参数:</div>
+                            <pre><code>${formattedArgs}</code></pre>
+                        </div>
+                        <div class="tool-result">
+                            <div class="tool-label">执行结果:</div>
+                            <pre><code>${formattedResult || '无返回结果'}</code></pre>
+                        </div>
+                    </div>
+                `;
+                toolsContainer.appendChild(tcDiv);
+            });
+            msgDiv.appendChild(toolsContainer);
+        }
+
+        // 简单处理 markdown 换行
+        const textDiv = document.createElement('div');
+        textDiv.className = 'message-text';
+        const formattedText = text.replace(/\n/g, '<br>');
+        textDiv.innerHTML = formattedText;
+        msgDiv.appendChild(textDiv);
+
+        chatContainer.appendChild(msgDiv);
+        scrollToBottom();
+    }
+
+    // 显示加载状态
+    function showLoading() {
+        const loadingDiv = document.createElement('div');
+        loadingDiv.className = 'message loading';
+        loadingDiv.id = 'loading-msg';
+        loadingDiv.textContent = '助手正在思考...';
+        chatContainer.appendChild(loadingDiv);
+        scrollToBottom();
+    }
+
+    // 移除加载状态
+    function removeLoading() {
+        const loadingDiv = document.getElementById('loading-msg');
+        if (loadingDiv) {
+            loadingDiv.remove();
+        }
+    }
+
+    // 加载会话列表
+    async function loadSessions() {
+        try {
+            const response = await fetch('/api/sessions');
+            const data = await response.json();
+            sessionList.innerHTML = '';
+            
+            data.sessions.forEach(session => {
+                const div = document.createElement('div');
+                div.className = `session-item ${session.id === currentSessionId ? 'active' : ''}`;
+                
+                const titleSpan = document.createElement('span');
+                titleSpan.textContent = session.title;
+                titleSpan.className = 'session-title';
+                
+                const deleteBtn = document.createElement('button');
+                deleteBtn.innerHTML = '🗑️';
+                deleteBtn.className = 'delete-session-btn';
+                deleteBtn.title = '删除会话';
+                deleteBtn.onclick = (e) => {
+                    e.stopPropagation(); // 阻止触发切换会话
+                    deleteSession(session.id, div);
+                };
+                
+                div.appendChild(titleSpan);
+                div.appendChild(deleteBtn);
+                div.onclick = () => switchSession(session.id, session.title);
+                
+                sessionList.appendChild(div);
+            });
+        } catch (error) {
+            console.error('加载会话列表失败:', error);
+        }
+    }
+
+    // 删除会话
+    async function deleteSession(sessionId, element) {
+        if (!confirm('确定要删除这个会话吗?删除后无法恢复。')) return;
+        
+        try {
+            const response = await fetch(`/api/sessions/${sessionId}`, {
+                method: 'DELETE'
+            });
+            
+            if (response.ok) {
+                // 添加淡出动画
+                element.style.opacity = '0';
+                setTimeout(() => {
+                    element.remove();
+                    // 如果删除的是当前会话,新建一个会话
+                    if (sessionId === currentSessionId) {
+                        createNewSession();
+                    }
+                }, 300);
+            } else {
+                alert('删除失败');
+            }
+        } catch (error) {
+            console.error('删除会话失败:', error);
+            alert('网络错误,删除失败');
+        }
+    }
+
+    // 切换会话
+    async function switchSession(sessionId, title) {
+        currentSessionId = sessionId;
+        currentSessionTitle.textContent = title;
+        chatContainer.innerHTML = '';
+        
+        // 更新侧边栏高亮
+        document.querySelectorAll('.session-item').forEach(item => {
+            item.classList.remove('active');
+            if (item.textContent === title) {
+                item.classList.add('active');
+            }
+        });
+
+        try {
+            const response = await fetch(`/api/sessions/${sessionId}`);
+            const data = await response.json();
+            
+            if (data.messages && data.messages.length > 0) {
+                data.messages.forEach(msg => {
+                    addMessage(msg.text, msg.isUser, msg.tool_calls);
+                });
+            } else {
+                addMessage('你好!我是你的软件开发学习助手。我可以根据你的水平出题,或者帮你测试代码。请问有什么我可以帮你的?');
+            }
+        } catch (error) {
+            console.error('加载会话历史失败:', error);
+            addMessage('加载历史记录失败');
+        }
+    }
+
+    // 新建会话 (仅前端状态)
+    function createNewSession() {
+        currentSessionId = null;
+        currentSessionTitle.textContent = '👨‍💻 SoftwareDevHelper';
+        chatContainer.innerHTML = '';
+        document.querySelectorAll('.session-item').forEach(item => item.classList.remove('active'));
+        addMessage('你好!我是你的软件开发学习助手。我可以根据你的水平出题,提供开发建议,并测试你的代码。你想从哪里开始?');
+    }
+
+    // 发送文本消息
+    async function sendMessage() {
+        const text = userInput.value.trim();
+        if (!text) return;
+
+        userInput.value = '';
+        addMessage(text, true);
+        showLoading();
+        sendBtn.disabled = true;
+
+        try {
+            const response = await fetch('/api/chat', {
+                method: 'POST',
+                headers: {
+                    'Content-Type': 'application/json'
+                },
+                body: JSON.stringify({ 
+                    message: text,
+                    session_id: currentSessionId || ''
+                })
+            });
+
+            const data = await response.json();
+            removeLoading();
+            
+            if (response.ok) {
+                addMessage(data.response, false, data.tool_calls);
+                // 如果是新会话,更新当前 session_id 并刷新列表
+                if (!currentSessionId) {
+                    currentSessionId = data.session_id;
+                    currentSessionTitle.textContent = text.substring(0, 15) + (text.length > 15 ? '...' : '');
+                }
+                loadSessions();
+                loadUserMemory(); // 聊天后可能更新了记忆
+            } else {
+                addMessage(`错误: ${data.detail || '请求失败'}`);
+            }
+        } catch (error) {
+            removeLoading();
+            addMessage(`网络错误: ${error.message}`);
+        } finally {
+            sendBtn.disabled = false;
+            userInput.focus();
+        }
+    }
+
+    // 处理文件上传
+    async function handleFileUpload(event) {
+        const file = event.target.files[0];
+        if (!file) return;
+
+        fileNameDisplay.textContent = file.name;
+        
+        const formData = new FormData();
+        formData.append('file', file);
+        formData.append('session_id', currentSessionId || '');
+
+        addMessage(`[上传项目] ${file.name}`, true);
+        showLoading();
+        
+        projectUpload.value = '';
+
+        try {
+            const response = await fetch('/api/upload_project', {
+                method: 'POST',
+                body: formData
+            });
+
+            const data = await response.json();
+            removeLoading();
+            
+            if (response.ok) {
+                addMessage(data.response, false, data.tool_calls);
+                if (!currentSessionId) {
+                    currentSessionId = data.session_id;
+                    currentSessionTitle.textContent = "上传项目测试";
+                }
+                loadSessions();
+                loadUserMemory();
+            } else {
+                addMessage(`上传失败: ${data.detail || '未知错误'}`);
+            }
+        } catch (error) {
+            removeLoading();
+            addMessage(`上传出错: ${error.message}`);
+        } finally {
+            fileNameDisplay.textContent = '未选择文件';
+        }
+    }
+
+    // 加载用户记忆
+    async function loadUserMemory() {
+        try {
+            const response = await fetch('/api/user_memory');
+            const data = await response.json();
+            
+            userLevelSelect.value = data.level || 'beginner';
+            
+            historyList.innerHTML = '';
+            if (data.history && data.history.length > 0) {
+                data.history.forEach(record => {
+                    const li = document.createElement('li');
+                    li.textContent = record;
+                    historyList.appendChild(li);
+                });
+            } else {
+                const li = document.createElement('li');
+                li.textContent = '暂无做题记录';
+                li.style.color = '#999';
+                historyList.appendChild(li);
+            }
+        } catch (error) {
+            console.error('加载用户记忆失败:', error);
+        }
+    }
+
+    // 更新用户水平
+    async function updateUserLevel() {
+        const newLevel = userLevelSelect.value;
+        try {
+            await fetch('/api/user_memory/level', {
+                method: 'POST',
+                headers: {
+                    'Content-Type': 'application/json'
+                },
+                body: JSON.stringify({ level: newLevel })
+            });
+        } catch (error) {
+            console.error('更新用户水平失败:', error);
+        }
+    }
+
+    // 清空用户记忆记录
+    async function resetUserMemory() {
+        if (confirm('确定要清空所有的做题记录吗?这会将你的水平重置为入门(Beginner)。')) {
+            try {
+                await fetch('/api/user_memory', {
+                    method: 'DELETE'
+                });
+                loadUserMemory();
+                addMessage('您的做题记录已清空,水平已重置。', false);
+            } catch (error) {
+                console.error('清空记录失败:', error);
+            }
+        }
+    }
+
+    // 事件绑定
+    sendBtn.addEventListener('click', sendMessage);
+    
+    userInput.addEventListener('keypress', (e) => {
+        if (e.key === 'Enter' && !e.shiftKey) {
+            e.preventDefault();
+            sendMessage();
+        }
+    });
+
+    projectUpload.addEventListener('change', handleFileUpload);
+    newSessionBtn.addEventListener('click', createNewSession);
+    userLevelSelect.addEventListener('change', updateUserLevel);
+    
+    const resetMemoryBtn = document.getElementById('reset-memory-btn');
+    if (resetMemoryBtn) {
+        resetMemoryBtn.addEventListener('click', resetUserMemory);
+    }
+
+    // 初始化
+    loadSessions();
+    loadUserMemory();
+    createNewSession();
+});

+ 359 - 0
Co-creation-projects/angelen-SoftwareDevHelper/frontend/static/style.css

@@ -0,0 +1,359 @@
+:root {
+    --primary-color: #4CAF50;
+    --bg-color: #f5f7fa;
+    --chat-bg: #ffffff;
+    --text-color: #333333;
+    --border-color: #e1e4e8;
+}
+
+body {
+    font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
+    background-color: var(--bg-color);
+    color: var(--text-color);
+    margin: 0;
+    padding: 0;
+    display: flex;
+    justify-content: center;
+    height: 100vh;
+}
+
+.app-layout {
+    display: flex;
+    width: 100%;
+    max-width: 1400px;
+    height: 100%;
+    background-color: var(--chat-bg);
+    box-shadow: 0 0 20px rgba(0,0,0,0.1);
+}
+
+.sidebar {
+    width: 260px;
+    background-color: #f8f9fa;
+    border-right: 1px solid var(--border-color);
+    display: flex;
+    flex-direction: column;
+    padding: 20px;
+    overflow-y: auto;
+}
+
+.right-sidebar {
+    border-right: none;
+    border-left: 1px solid var(--border-color);
+}
+
+.new-btn {
+    width: 100%;
+    padding: 10px;
+    margin-bottom: 20px;
+    background-color: var(--primary-color);
+    color: white;
+    border: none;
+    border-radius: 6px;
+    cursor: pointer;
+    font-weight: bold;
+}
+
+.new-btn:hover {
+    background-color: #45a049;
+}
+
+.session-list {
+    display: flex;
+    flex-direction: column;
+    gap: 10px;
+}
+
+.session-item {
+    padding: 10px;
+    background-color: #fff;
+    border: 1px solid var(--border-color);
+    border-radius: 6px;
+    cursor: pointer;
+    font-size: 14px;
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    transition: all 0.2s ease;
+}
+
+.session-title {
+    flex-grow: 1;
+    white-space: nowrap;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    margin-right: 5px;
+}
+
+.delete-session-btn {
+    background: none;
+    border: none;
+    padding: 2px 5px;
+    cursor: pointer;
+    opacity: 0;
+    transition: opacity 0.2s ease, transform 0.2s ease;
+    color: #e74c3c;
+    font-size: 14px;
+}
+
+.session-item:hover .delete-session-btn {
+    opacity: 0.7;
+}
+
+.delete-session-btn:hover {
+    opacity: 1 !important;
+    transform: scale(1.2);
+}
+
+.session-item:hover, .session-item.active {
+    background-color: #e8f5e9;
+    border-color: var(--primary-color);
+}
+
+.session-item.active .delete-session-btn {
+    color: #c0392b;
+}
+
+.user-profile h3, .user-profile h4 {
+    margin-bottom: 15px;
+    margin-top: 0;
+    color: var(--text-color);
+}
+
+.level-section {
+    margin-bottom: 20px;
+}
+
+.level-section select {
+    width: 100%;
+    padding: 8px;
+    margin-top: 5px;
+    border-radius: 4px;
+    border: 1px solid #ccc;
+}
+
+.history-list {
+    list-style: none;
+    padding: 0;
+    margin: 0;
+}
+
+.history-list li {
+    background: #fff;
+    padding: 8px;
+    border: 1px solid var(--border-color);
+    border-radius: 4px;
+    margin-bottom: 8px;
+    font-size: 13px;
+}
+
+.container.main-chat {
+    flex: 1;
+    display: flex;
+    flex-direction: column;
+    height: 100%;
+    box-shadow: none;
+    max-width: none;
+}
+
+header {
+    padding: 20px;
+    text-align: center;
+    border-bottom: 1px solid var(--border-color);
+    background-color: #fff;
+}
+
+header h1 {
+    margin: 0;
+    color: var(--primary-color);
+    font-size: 24px;
+}
+
+header p {
+    margin: 5px 0 0;
+    color: #666;
+    font-size: 14px;
+}
+
+main {
+    flex: 1;
+    display: flex;
+    flex-direction: column;
+    overflow: hidden;
+}
+
+.chat-container {
+    flex: 1;
+    padding: 20px;
+    overflow-y: auto;
+    display: flex;
+    flex-direction: column;
+    gap: 15px;
+}
+
+.message {
+    max-width: 80%;
+    padding: 12px 16px;
+    border-radius: 8px;
+    line-height: 1.5;
+    word-wrap: break-word;
+}
+
+.bot-message {
+    align-self: flex-start;
+    background-color: #f0f2f5;
+    color: #1c1e21;
+    border-bottom-left-radius: 0;
+}
+
+.user-message {
+    align-self: flex-end;
+    background-color: var(--primary-color);
+    color: white;
+    border-bottom-right-radius: 0;
+}
+
+.input-area {
+    padding: 20px;
+    border-top: 1px solid var(--border-color);
+    background-color: #fff;
+}
+
+.file-upload {
+    margin-bottom: 15px;
+    display: flex;
+    align-items: center;
+    gap: 10px;
+}
+
+.upload-btn {
+    background-color: #f0f2f5;
+    border: 1px solid var(--border-color);
+    padding: 8px 15px;
+    border-radius: 4px;
+    cursor: pointer;
+    font-size: 14px;
+    transition: background-color 0.2s;
+}
+
+.upload-btn:hover {
+    background-color: #e4e6e9;
+}
+
+#file-name {
+    font-size: 14px;
+    color: #666;
+}
+
+.text-input {
+    display: flex;
+    gap: 10px;
+}
+
+textarea {
+    flex: 1;
+    padding: 10px;
+    border: 1px solid var(--border-color);
+    border-radius: 4px;
+    resize: none;
+    font-family: inherit;
+    font-size: 14px;
+}
+
+textarea:focus {
+    outline: none;
+    border-color: var(--primary-color);
+}
+
+button {
+    background-color: var(--primary-color);
+    color: white;
+    border: none;
+    padding: 0 20px;
+    border-radius: 4px;
+    cursor: pointer;
+    font-size: 16px;
+    transition: background-color 0.2s;
+}
+
+button:hover {
+    background-color: #45a049;
+}
+
+button:disabled {
+    background-color: #cccccc;
+    cursor: not-allowed;
+}
+
+.loading {
+    align-self: flex-start;
+    color: #666;
+    font-style: italic;
+    font-size: 14px;
+}
+
+/* 工具调用样式 */
+.tool-calls-container {
+    margin-bottom: 15px;
+    display: flex;
+    flex-direction: column;
+    gap: 10px;
+}
+
+.tool-call-block {
+    background-color: #f8f9fa;
+    border: 1px solid #e1e4e8;
+    border-radius: 6px;
+    overflow: hidden;
+    font-size: 13px;
+}
+
+.tool-call-header {
+    background-color: #e9ecef;
+    padding: 8px 12px;
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    border-bottom: 1px solid #e1e4e8;
+}
+
+.tool-icon {
+    font-size: 14px;
+}
+
+.tool-name {
+    color: #495057;
+}
+
+.tool-call-details {
+    padding: 10px 12px;
+    display: flex;
+    flex-direction: column;
+    gap: 10px;
+}
+
+.tool-label {
+    font-weight: bold;
+    color: #6c757d;
+    margin-bottom: 4px;
+    font-size: 12px;
+}
+
+.tool-args pre, .tool-result pre {
+    margin: 0;
+    padding: 8px;
+    background-color: #212529;
+    color: #f8f9fa;
+    border-radius: 4px;
+    overflow-x: auto;
+    max-height: 200px;
+}
+
+.tool-args code, .tool-result code {
+    font-family: 'Consolas', 'Monaco', monospace;
+    white-space: pre-wrap;
+    word-break: break-all;
+}
+
+.message-text {
+    margin-top: 5px;
+}

+ 73 - 0
Co-creation-projects/angelen-SoftwareDevHelper/frontend/templates/index.html

@@ -0,0 +1,73 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>SoftwareDevHelper - 软件开发学习助手</title>
+    <link rel="stylesheet" href="/static/style.css">
+</head>
+<body>
+    <div class="app-layout">
+        <!-- 左侧会话列表 -->
+        <aside class="sidebar left-sidebar">
+            <button id="new-session-btn" class="primary-btn new-btn">+ 新建会话</button>
+            <div class="session-list" id="session-list">
+                <!-- 会话列表动态加载 -->
+            </div>
+        </aside>
+
+        <!-- 中间主聊天区 -->
+        <div class="container main-chat">
+            <header class="chat-header">
+                <h1 id="current-session-title">👨‍💻 SoftwareDevHelper</h1>
+                <p>你的专属软件开发学习助手</p>
+            </header>
+            
+            <main>
+                <div class="chat-container" id="chat-container">
+                    <!-- 聊天消息会动态插入这里 -->
+                </div>
+                
+                <div class="input-area">
+                    <div class="file-upload">
+                        <label for="project-upload" class="upload-btn">上传项目 (.zip)</label>
+                        <input type="file" id="project-upload" accept=".zip" style="display: none;">
+                        <span id="file-name">未选择文件</span>
+                    </div>
+                    
+                    <div class="text-input">
+                        <textarea id="user-input" placeholder="输入你的问题或回复..." rows="3"></textarea>
+                        <button id="send-btn">发送</button>
+                    </div>
+                </div>
+            </main>
+        </div>
+
+        <!-- 右侧用户水平和记录窗口 -->
+        <aside class="sidebar right-sidebar">
+            <div class="user-profile">
+                <h3>用户档案</h3>
+                <div class="level-section">
+                    <label for="user-level">当前水平:</label>
+                    <select id="user-level">
+                        <option value="beginner">初学者 (Beginner)</option>
+                        <option value="intermediate">中级 (Intermediate)</option>
+                        <option value="advanced">高级 (Advanced)</option>
+                    </select>
+                </div>
+                <div class="history-section">
+                    <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
+                        <h4 style="margin: 0;">做题记录</h4>
+                        <button id="reset-memory-btn" style="background: none; border: none; font-size: 14px; cursor: pointer; color: #e74c3c;" title="清空记录并重置水平">🗑️ 清空</button>
+                    </div>
+                    <ul id="history-list" class="history-list">
+                        <!-- 记录动态加载 -->
+                    </ul>
+                </div>
+            </div>
+        </aside>
+    </div>
+
+    <script src="/static/app.js"></script>
+</body>
+</html>

+ 11 - 0
Co-creation-projects/angelen-SoftwareDevHelper/requirements.txt

@@ -0,0 +1,11 @@
+# 核心依赖
+hello-agents[all]>=0.2.7
+
+# Web框架
+fastapi>=0.109.0
+uvicorn>=0.27.0
+python-multipart>=0.0.9
+
+# 其他工具
+python-dotenv>=1.0.0
+pytest>=8.0.0

+ 184 - 0
Co-creation-projects/angelen-SoftwareDevHelper/src/agents/helper_agent.py

@@ -0,0 +1,184 @@
+import os
+import json
+import zipfile
+import subprocess
+import shutil
+from typing import Dict, Any, List
+
+from hello_agents import SimpleAgent, HelloAgentsLLM, ToolRegistry
+from hello_agents.tools import Tool, ToolParameter, ToolResponse
+from hello_agents.tools.response import ToolStatus
+
+class UserMemoryTool(Tool):
+    """管理用户水平记忆的工具"""
+
+    def __init__(self, memory_file: str = "user_memory.json"):
+        super().__init__(
+            name="user_memory",
+            description="获取或更新用户的编程水平和做题记录"
+        )
+        self.memory_file = os.path.join(os.path.dirname(__file__), "../../data", memory_file)
+        self._ensure_memory_file()
+
+    def _ensure_memory_file(self):
+        if not os.path.exists(self.memory_file):
+            os.makedirs(os.path.dirname(self.memory_file), exist_ok=True)
+            with open(self.memory_file, "w", encoding="utf-8") as f:
+                json.dump({"level": "beginner", "history": []}, f)
+
+    def run(self, parameters: Dict[str, Any]) -> ToolResponse:
+        action = parameters.get("action")
+        
+        with open(self.memory_file, "r", encoding="utf-8") as f:
+            memory = json.load(f)
+
+        if action == "get":
+            return ToolResponse.success(text=json.dumps(memory, ensure_ascii=False))
+        elif action == "update":
+            new_level = parameters.get("level")
+            new_record = parameters.get("record")
+            if new_level:
+                memory["level"] = new_level
+            if new_record:
+                memory["history"].append(new_record)
+            with open(self.memory_file, "w", encoding="utf-8") as f:
+                json.dump(memory, f, ensure_ascii=False, indent=2)
+            return ToolResponse.success(text="记忆更新成功")
+        else:
+            return ToolResponse.error(text="无效的 action")
+
+    def get_parameters(self) -> List[ToolParameter]:
+        return [
+            ToolParameter(
+                name="action",
+                type="string",
+                description="操作类型,可选 'get' 或 'update'",
+                required=True
+            ),
+            ToolParameter(
+                name="level",
+                type="string",
+                description="用户的新水平评估 (例如 'beginner', 'intermediate', 'advanced')",
+                required=False
+            ),
+            ToolParameter(
+                name="record",
+                type="string",
+                description="新完成的题目记录",
+                required=False
+            )
+        ]
+
+class CodeTestTool(Tool):
+    """代码自动测试与打分工具"""
+
+    def __init__(self):
+        super().__init__(
+            name="code_test",
+            description="解压用户上传的项目压缩包,运行测试并给出评分"
+        )
+        self.extract_dir = os.path.join(os.path.dirname(__file__), "../../outputs/extracted")
+
+    def run(self, parameters: Dict[str, Any]) -> ToolResponse:
+        zip_path = parameters.get("zip_path")
+        test_code = parameters.get("test_code") # 由LLM生成的测试代码
+        
+        if not zip_path or not os.path.exists(zip_path):
+            return ToolResponse.error(text="错误:压缩包路径不存在")
+            
+        if not test_code:
+            return ToolResponse.error(text="错误:缺少测试代码")
+
+        # 清理旧的解压目录
+        if os.path.exists(self.extract_dir):
+            shutil.rmtree(self.extract_dir)
+        os.makedirs(self.extract_dir, exist_ok=True)
+
+        # 解压
+        try:
+            with zipfile.ZipFile(zip_path, 'r') as zip_ref:
+                zip_ref.extractall(self.extract_dir)
+        except Exception as e:
+            return ToolResponse.error(text=f"解压失败: {str(e)}")
+
+        # 写入测试文件
+        test_file_path = os.path.join(self.extract_dir, "test_generated.py")
+        with open(test_file_path, "w", encoding="utf-8") as f:
+            f.write(test_code)
+
+        # 运行测试
+        try:
+            result = subprocess.run(
+                ["pytest", test_file_path, "-v"],
+                cwd=self.extract_dir,
+                capture_output=True,
+                text=True,
+                timeout=30
+            )
+            
+            output = result.stdout + "\n" + result.stderr
+            score = 100 if result.returncode == 0 else 0 # 简单评分逻辑,可根据pytest输出优化
+            
+            return ToolResponse.success(
+                text=json.dumps({
+                    "score": score,
+                    "test_output": output,
+                    "status": "success" if result.returncode == 0 else "failed"
+                }, ensure_ascii=False)
+            )
+            
+        except subprocess.TimeoutExpired:
+            return ToolResponse.error(text="测试执行超时")
+        except Exception as e:
+            return ToolResponse.error(text=f"测试执行出错: {str(e)}")
+
+    def get_parameters(self) -> List[ToolParameter]:
+        return [
+            ToolParameter(
+                name="zip_path",
+                type="string",
+                description="用户上传的项目压缩包绝对路径",
+                required=True
+            ),
+            ToolParameter(
+                name="test_code",
+                type="string",
+                description="用于测试用户代码的 pytest 测试代码",
+                required=True
+            )
+        ]
+
+def get_helper_agent() -> SimpleAgent:
+    """初始化并返回学习助手智能体"""
+    tool_registry = ToolRegistry()
+    tool_registry.register_tool(UserMemoryTool())
+    tool_registry.register_tool(CodeTestTool())
+
+    model_id = os.environ.get("LLM_MODEL_ID", "Qwen/Qwen2.5-72B-Instruct")
+    llm = HelloAgentsLLM(model=model_id)
+
+    system_prompt = """你是一个专业的软件开发学习助手。你的职责是:
+1. 使用 user_memory 工具了解用户的当前编程水平和历史做题记录。
+2. 根据用户水平,为他们出适合的编程题目,或者从网上搜索真实的开发案例。
+3. 在用户开发过程中,提供有针对性的建议和指导。
+4. 当用户完成开发并上传项目压缩包后,你需要:
+   - 仔细分析题目要求。
+   - 编写严谨的 pytest 测试用例代码。注意:用户的代码通常在解压目录的某个子文件夹中(如 `test-projects/main.py`),你的测试代码需要能够递归查找 `.py` 文件并动态导入模块,而不是简单地假设代码在当前目录下。可以参考使用 `sys.path.insert(0, str(project_root))` 来辅助导入。
+   - 使用 code_test 工具,传入压缩包路径和你的测试代码,对用户的项目进行自动化测试。
+   - 根据测试结果给出最终打分和详细的代码审查反馈。
+5. 任务完成后,使用 user_memory 工具更新用户的水平评估和做题记录。
+
+请始终保持鼓励和专业的态度。"""
+
+    from hello_agents.core.config import Config
+    
+    # 禁用 TodoWrite 工具,避免在 Azure/Gemini 下出现 schema 验证错误
+    config = Config(todowrite_enabled=False)
+
+    return SimpleAgent(
+        name="SoftwareDevHelper",
+        llm=llm,
+        system_prompt=system_prompt,
+        tool_registry=tool_registry,
+        config=config
+    )

+ 287 - 0
Co-creation-projects/angelen-SoftwareDevHelper/src/main.py

@@ -0,0 +1,287 @@
+from fastapi import FastAPI, UploadFile, File, Form, HTTPException, Query
+from fastapi.middleware.cors import CORSMiddleware
+from fastapi.staticfiles import StaticFiles
+from fastapi.responses import HTMLResponse
+from pydantic import BaseModel
+import os
+import json
+import shutil
+import uuid
+import datetime
+from dotenv import load_dotenv
+
+# 加载环境变量
+load_dotenv(os.path.join(os.path.dirname(__file__), "../.env"))
+
+from .agents.helper_agent import get_helper_agent
+
+app = FastAPI(title="SoftwareDevHelper API")
+
+# 配置 CORS
+app.add_middleware(
+    CORSMiddleware,
+    allow_origins=["*"],
+    allow_credentials=True,
+    allow_methods=["*"],
+    allow_headers=["*"],
+)
+
+# 挂载前端静态文件
+frontend_dir = os.path.join(os.path.dirname(__file__), "../frontend")
+app.mount("/static", StaticFiles(directory=os.path.join(frontend_dir, "static")), name="static")
+
+# 数据目录
+data_dir = os.path.join(os.path.dirname(__file__), "../data")
+sessions_dir = os.path.join(data_dir, "sessions")
+os.makedirs(sessions_dir, exist_ok=True)
+user_memory_file = os.path.join(data_dir, "user_memory.json")
+
+# 初始化智能体 (这里需要修改为支持多会话的智能体实例管理,但为了简单,我们每次请求动态恢复上下文)
+# 由于 SimpleAgent 默认在内存中保存历史,为了支持多会话,我们需要为每个会话维护一个 Agent 实例
+# 或者在每次请求时将历史记录注入到 Agent 中。
+# 为了保持与 HelloAgents 框架的兼容性,我们在内存中缓存 Agent 实例。
+agent_sessions = {}
+
+def get_or_create_agent(session_id: str):
+    if session_id not in agent_sessions:
+        agent = get_helper_agent()
+        # 尝试加载历史记录
+        session_file = os.path.join(sessions_dir, f"{session_id}.json")
+        if os.path.exists(session_file):
+            with open(session_file, "r", encoding="utf-8") as f:
+                data = json.load(f)
+                history = data.get("messages", [])
+                # 简单恢复历史记录到 agent
+                # 注意:SimpleAgent 内部使用 _history 列表存储消息
+                for msg in history:
+                    from hello_agents.core.message import Message
+                    if msg.get("isUser"):
+                        agent._history.append(Message(role="user", content=msg.get("text", "")))
+                    else:
+                        # 检查是否包含 tool_calls,这里为了简化,我们只恢复文本,
+                        # 避免不完整的 tool_calls 导致后续大模型调用报错
+                        agent._history.append(Message(role="assistant", content=msg.get("text", "")))
+        agent_sessions[session_id] = agent
+    return agent_sessions[session_id]
+
+def save_session_history(session_id: str, title: str, text: str, is_user: bool, tool_calls: list = None):
+    session_file = os.path.join(sessions_dir, f"{session_id}.json")
+    history = []
+    if os.path.exists(session_file):
+        with open(session_file, "r", encoding="utf-8") as f:
+            data = json.load(f)
+            history = data.get("messages", [])
+            title = data.get("title", title)
+
+    msg_data = {
+        "text": text,
+        "isUser": is_user,
+        "timestamp": datetime.datetime.now().isoformat()
+    }
+    if tool_calls:
+        msg_data["tool_calls"] = tool_calls
+
+    history.append(msg_data)
+
+    with open(session_file, "w", encoding="utf-8") as f:
+        json.dump({"title": title, "messages": history, "updated_at": datetime.datetime.now().isoformat()}, f, ensure_ascii=False, indent=2)
+
+class ChatRequest(BaseModel):
+    message: str
+    session_id: str
+
+class UserLevelRequest(BaseModel):
+    level: str
+
+@app.get("/", response_class=HTMLResponse)
+async def read_root():
+    with open(os.path.join(frontend_dir, "templates/index.html"), "r", encoding="utf-8") as f:
+        return f.read()
+
+@app.get("/api/sessions")
+async def get_sessions():
+    sessions = []
+    for filename in os.listdir(sessions_dir):
+        if filename.endswith(".json"):
+            session_id = filename[:-5]
+            with open(os.path.join(sessions_dir, filename), "r", encoding="utf-8") as f:
+                data = json.load(f)
+                sessions.append({
+                    "id": session_id,
+                    "title": data.get("title", "新会话"),
+                    "updated_at": data.get("updated_at", "")
+                })
+    # 按更新时间倒序排序
+    sessions.sort(key=lambda x: x["updated_at"], reverse=True)
+    return {"sessions": sessions}
+
+@app.get("/api/sessions/{session_id}")
+async def get_session_history(session_id: str):
+    session_file = os.path.join(sessions_dir, f"{session_id}.json")
+    if not os.path.exists(session_file):
+        return {"messages": []}
+    with open(session_file, "r", encoding="utf-8") as f:
+        data = json.load(f)
+        return {"messages": data.get("messages", [])}
+
+@app.delete("/api/sessions/{session_id}")
+async def delete_session(session_id: str):
+    session_file = os.path.join(sessions_dir, f"{session_id}.json")
+    if os.path.exists(session_file):
+        os.remove(session_file)
+    if session_id in agent_sessions:
+        del agent_sessions[session_id]
+    return {"status": "success"}
+
+@app.post("/api/chat")
+async def chat(request: ChatRequest):
+    try:
+        session_id = request.session_id
+        if not session_id:
+            session_id = str(uuid.uuid4())
+            
+        agent = get_or_create_agent(session_id)
+        
+        # 确定会话标题(取第一句话的前15个字符)
+        title = request.message[:15] + "..." if len(request.message) > 15 else request.message
+        
+        # 保存用户消息
+        save_session_history(session_id, title, request.message, True)
+        
+        # 获取回复
+        # 在运行前记录历史长度
+        history_len_before = len(agent.get_history())
+        response = agent.run(request.message)
+        
+        # 获取运行期间新增的历史记录,提取工具调用信息
+        tool_calls_info = []
+        current_history = agent.get_history()
+        new_messages = current_history[history_len_before:]
+        
+        for msg in new_messages:
+            # 查找带有 tool_calls 的 assistant 消息
+            if msg.role == "assistant" and getattr(msg, "tool_calls", None):
+                for tc in msg.tool_calls:
+                    # 获取 function 对象
+                    func = getattr(tc, "function", None)
+                    if func:
+                        # 确保 arguments 是字符串
+                        args = getattr(func, "arguments", "{}")
+                        if not isinstance(args, str):
+                            try:
+                                args = json.dumps(args, ensure_ascii=False)
+                            except:
+                                args = str(args)
+                        tool_calls_info.append({
+                            "id": getattr(tc, "id", ""),
+                            "name": getattr(func, "name", ""),
+                            "arguments": args,
+                            "result": "" # 稍后填充
+                        })
+            # 查找 tool 角色的消息(工具执行结果)
+            elif msg.role == "tool":
+                tool_call_id = getattr(msg, "tool_call_id", None)
+                if tool_call_id:
+                    for tc_info in tool_calls_info:
+                        if tc_info["id"] == tool_call_id:
+                            tc_info["result"] = msg.content
+                            break
+
+        # 保存助手消息(同时保存工具调用信息)
+        save_session_history(session_id, title, response, False, tool_calls=tool_calls_info)
+
+        return {"response": response, "session_id": session_id, "tool_calls": tool_calls_info}
+    except Exception as e:
+        raise HTTPException(status_code=500, detail=str(e))
+
+@app.post("/api/upload_project")
+async def upload_project(session_id: str = Form(...), file: UploadFile = File(...)):
+    if not file.filename.endswith('.zip'):
+        raise HTTPException(status_code=400, detail="只接受 .zip 格式的压缩包")
+
+    upload_dir = os.path.join(os.path.dirname(__file__), "../outputs/uploads")
+    os.makedirs(upload_dir, exist_ok=True)
+    
+    file_id = str(uuid.uuid4())
+    file_path = os.path.join(upload_dir, f"{file_id}_{file.filename}")
+    
+    try:
+        with open(file_path, "wb") as buffer:
+            shutil.copyfileobj(file.file, buffer)
+            
+        agent = get_or_create_agent(session_id)
+        
+        prompt = f"用户上传了项目压缩包,路径为:{file_path}。请根据当前题目要求,编写 pytest 测试用例,并使用 code_test 工具进行测试打分,最后给出反馈并更新用户水平记录。"
+
+        save_session_history(session_id, "上传项目测试", f"[上传项目] {file.filename}", True)
+
+        history_len_before = len(agent.get_history())
+        response = agent.run(prompt)
+        
+        tool_calls_info = []
+        current_history = agent.get_history()
+        new_messages = current_history[history_len_before:]
+        
+        for msg in new_messages:
+            if msg.role == "assistant" and getattr(msg, "tool_calls", None):
+                for tc in msg.tool_calls:
+                    func = getattr(tc, "function", None)
+                    if func:
+                        # 确保 arguments 是字符串
+                        args = getattr(func, "arguments", "{}")
+                        if not isinstance(args, str):
+                            try:
+                                args = json.dumps(args, ensure_ascii=False)
+                            except:
+                                args = str(args)
+                        tool_calls_info.append({
+                            "id": getattr(tc, "id", ""),
+                            "name": getattr(func, "name", ""),
+                            "arguments": args,
+                            "result": ""
+                        })
+            elif msg.role == "tool":
+                tool_call_id = getattr(msg, "tool_call_id", None)
+                if tool_call_id:
+                    for tc_info in tool_calls_info:
+                        if tc_info["id"] == tool_call_id:
+                            tc_info["result"] = msg.content
+                            break
+
+        save_session_history(session_id, "上传项目测试", response, False, tool_calls=tool_calls_info)
+
+        return {"response": response, "file_path": file_path, "session_id": session_id, "tool_calls": tool_calls_info}
+    except Exception as e:
+        raise HTTPException(status_code=500, detail=str(e))
+
+@app.get("/api/user_memory")
+async def get_user_memory():
+    if not os.path.exists(user_memory_file):
+        return {"level": "beginner", "history": []}
+    with open(user_memory_file, "r", encoding="utf-8") as f:
+        return json.load(f)
+
+@app.post("/api/user_memory/level")
+async def update_user_level(request: UserLevelRequest):
+    memory = {"level": "beginner", "history": []}
+    if os.path.exists(user_memory_file):
+        with open(user_memory_file, "r", encoding="utf-8") as f:
+            memory = json.load(f)
+            
+    memory["level"] = request.level
+    
+    with open(user_memory_file, "w", encoding="utf-8") as f:
+        json.dump(memory, f, ensure_ascii=False, indent=2)
+        
+    return {"status": "success", "level": memory["level"]}
+
+@app.delete("/api/user_memory")
+async def reset_user_memory():
+    """重置用户记忆(清空历史并重置为 beginner)"""
+    default_memory = {"level": "beginner", "history": []}
+    
+    os.makedirs(os.path.dirname(user_memory_file), exist_ok=True)
+    with open(user_memory_file, "w", encoding="utf-8") as f:
+        json.dump(default_memory, f, ensure_ascii=False, indent=2)
+        
+    return {"status": "success"}

+ 28 - 0
Co-creation-projects/angelen-SoftwareDevHelper/src/utils/test_tools.py

@@ -0,0 +1,28 @@
+import os
+import json
+
+def test_user_memory_tool():
+    from src.agents.helper_agent import UserMemoryTool
+    tool = UserMemoryTool(memory_file="test_memory.json")
+    
+    # Test get
+    res = tool.run({"action": "get"})
+    assert "beginner" in res or "level" in res
+    
+    # Test update
+    res = tool.run({"action": "update", "level": "intermediate", "record": "hello_world"})
+    assert res == "记忆更新成功"
+    
+    # Test get again
+    res = tool.run({"action": "get"})
+    assert "intermediate" in res
+    assert "hello_world" in res
+    
+    # cleanup
+    file_path = os.path.join(os.path.dirname(__file__), "../../data/test_memory.json")
+    if os.path.exists(file_path):
+        os.remove(file_path)
+    print("UserMemoryTool test passed!")
+
+if __name__ == "__main__":
+    test_user_memory_tool()