From 108c675c3dbea715ca0085169ef6fb4eee622265 Mon Sep 17 00:00:00 2001 From: autobee-sparticle Date: Fri, 27 Feb 2026 18:01:12 +0900 Subject: [PATCH] feat(memory): add memory management API endpoints (#10) * chore: add .worktrees/ to .gitignore Co-Authored-By: Claude Opus 4.6 * feat(memory): add memory management API endpoints Add new /api/v1/memory endpoints for viewing and managing user memories: - GET /api/v1/memory - list all memories for a bot - DELETE /api/v1/memory/{memory_id} - delete single memory - DELETE /api/v1/memory - delete all memories for a bot Also add delete_memory and delete_all_memories methods to Mem0Manager. Issue: #1844 Co-Authored-By: Claude Opus 4.6 --------- Co-authored-by: zhuchao Co-authored-by: Claude Opus 4.6 --- agent/mem0_manager.py | 82 +++++++++++++++ fastapi_app.py | 3 +- routes/memory.py | 232 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 316 insertions(+), 1 deletion(-) create mode 100644 routes/memory.py diff --git a/agent/mem0_manager.py b/agent/mem0_manager.py index 4fcc3b2..0c9cef1 100644 --- a/agent/mem0_manager.py +++ b/agent/mem0_manager.py @@ -485,6 +485,88 @@ class Mem0Manager: logger.error(f"Failed to get all memories: {e}") return [] + async def delete_memory( + self, + memory_id: str, + user_id: str, + agent_id: str, + ) -> bool: + """删除单条记忆 + + Args: + memory_id: 记忆 ID + user_id: 用户 ID + agent_id: Agent/Bot ID + + Returns: + 是否删除成功 + """ + try: + mem = await self.get_mem0(user_id, agent_id, "default") + + # 先获取记忆以验证所有权 + memories = mem.get_all(user_id=user_id) + target_memory = None + for m in memories: + if m.get("id") == memory_id: + # 验证 agent_id 匹配 + if m.get("metadata", {}).get("agent_id") == agent_id: + target_memory = m + break + + if not target_memory: + logger.warning(f"Memory {memory_id} not found or access denied for user={user_id}, agent={agent_id}") + return False + + # 删除记忆 + mem.delete(memory_id=memory_id) + + logger.info(f"Deleted memory {memory_id} for user={user_id}, agent={agent_id}") + return True + + except Exception as e: + logger.error(f"Failed to delete memory {memory_id}: {e}") + return False + + async def delete_all_memories( + self, + user_id: str, + agent_id: str, + ) -> int: + """删除用户在指定 Agent 下的所有记忆 + + Args: + user_id: 用户 ID + agent_id: Agent/Bot ID + + Returns: + 删除的记忆数量 + """ + try: + mem = await self.get_mem0(user_id, agent_id, "default") + + # 获取所有记忆 + memories = mem.get_all(user_id=user_id) + + # 过滤 agent_id 并删除 + deleted_count = 0 + for m in memories: + if m.get("metadata", {}).get("agent_id") == agent_id: + memory_id = m.get("id") + if memory_id: + try: + mem.delete(memory_id=memory_id) + deleted_count += 1 + except Exception as e: + logger.warning(f"Failed to delete memory {memory_id}: {e}") + + logger.info(f"Deleted {deleted_count} memories for user={user_id}, agent={agent_id}") + return deleted_count + + except Exception as e: + logger.error(f"Failed to delete all memories: {e}") + return 0 + def clear_cache(self, user_id: Optional[str] = None, agent_id: Optional[str] = None) -> None: """清除缓存的 Mem0 实例 diff --git a/fastapi_app.py b/fastapi_app.py index 5e88bb7..68007c2 100644 --- a/fastapi_app.py +++ b/fastapi_app.py @@ -71,7 +71,7 @@ from utils.log_util.logger import init_with_fastapi logger = logging.getLogger('app') # Import route modules -from routes import chat, files, projects, system, skill_manager, database +from routes import chat, files, projects, system, skill_manager, database, memory @asynccontextmanager @@ -175,6 +175,7 @@ app.include_router(projects.router) app.include_router(system.router) app.include_router(skill_manager.router) app.include_router(database.router) +app.include_router(memory.router) # 注册文件管理API路由 app.include_router(file_manager_router) diff --git a/routes/memory.py b/routes/memory.py new file mode 100644 index 0000000..a0b6a95 --- /dev/null +++ b/routes/memory.py @@ -0,0 +1,232 @@ +""" +Memory 管理 API 路由 +提供记忆查看和删除功能 +""" + +import logging +from typing import Optional, List, Dict, Any +from fastapi import APIRouter, HTTPException, Header, Query +from fastapi.responses import JSONResponse +from pydantic import BaseModel + +logger = logging.getLogger('app') + +router = APIRouter(prefix="/api/v1", tags=["memory"]) + + +class MemoryItem(BaseModel): + """单条记忆的数据模型""" + id: str + content: str + created_at: Optional[str] = None + updated_at: Optional[str] = None + + +class MemoryListResponse(BaseModel): + """记忆列表响应""" + memories: List[MemoryItem] + total: int + + +class DeleteAllResponse(BaseModel): + """删除所有记忆响应""" + deleted_count: int + + +async def get_user_identifier_from_request( + authorization: Optional[str], + user_id: Optional[str] = None +) -> str: + """ + 获取用户标识符 + + 优先使用请求参数中的 user_id,否则尝试从 Authorization header 解析 + + Args: + authorization: Authorization header + user_id: 请求参数中的 user_id + + Returns: + 用户标识符 + + Raises: + HTTPException: 如果无法获取用户标识符 + """ + if user_id: + return user_id + + # 如果没有提供 user_id,抛出异常 + # 注意:根据 PRD,user_id 从前端传入 + raise HTTPException( + status_code=400, + detail="user_id is required" + ) + + +@router.get("/memory", response_model=MemoryListResponse) +async def get_memories( + bot_id: str = Query(..., description="Bot ID (对应 agent_id)"), + user_id: str = Query(..., description="用户 ID"), + authorization: Optional[str] = Header(None, description="Authorization header"), +): + """ + 获取当前用户在指定 Bot 下的所有记忆 + + Args: + bot_id: Bot ID(对应 agent_id) + user_id: 用户标识符 + authorization: Authorization header(用于鉴权) + + Returns: + MemoryListResponse: 记忆列表 + """ + try: + from agent.mem0_manager import get_mem0_manager + from utils.settings import MEM0_ENABLED + + # 检查 Memory 功能是否启用 + if not MEM0_ENABLED: + raise HTTPException( + status_code=503, + detail="Memory feature is not enabled" + ) + + # 获取 Mem0Manager 实例 + manager = get_mem0_manager() + + # 获取所有记忆 + memories = await manager.get_all_memories( + user_id=user_id, + agent_id=bot_id + ) + + # 转换为响应格式 + memory_items = [] + for m in memories: + memory_items.append(MemoryItem( + id=m.get("id", ""), + content=m.get("memory", ""), + created_at=m.get("created_at"), + updated_at=m.get("updated_at") + )) + + return MemoryListResponse( + memories=memory_items, + total=len(memory_items) + ) + + except HTTPException: + raise + except Exception as e: + logger.error(f"Failed to get memories: {e}") + raise HTTPException( + status_code=500, + detail=f"Failed to get memories: {str(e)}" + ) + + +@router.delete("/memory/{memory_id}", status_code=204) +async def delete_memory( + memory_id: str, + bot_id: str = Query(..., description="Bot ID (用于权限校验)"), + user_id: str = Query(..., description="用户 ID"), + authorization: Optional[str] = Header(None, description="Authorization header"), +): + """ + 删除单条记忆 + + Args: + memory_id: 记忆 ID + bot_id: Bot ID(用于权限校验) + user_id: 用户标识符 + authorization: Authorization header + + Returns: + 204 No Content + """ + try: + from agent.mem0_manager import get_mem0_manager + from utils.settings import MEM0_ENABLED + + # 检查 Memory 功能是否启用 + if not MEM0_ENABLED: + raise HTTPException( + status_code=503, + detail="Memory feature is not enabled" + ) + + # 获取 Mem0Manager 实例 + manager = get_mem0_manager() + + # 删除记忆 + success = await manager.delete_memory( + memory_id=memory_id, + user_id=user_id, + agent_id=bot_id + ) + + if not success: + raise HTTPException( + status_code=404, + detail="Memory not found or delete failed" + ) + + return JSONResponse(status_code=204, content=None) + + except HTTPException: + raise + except Exception as e: + logger.error(f"Failed to delete memory {memory_id}: {e}") + raise HTTPException( + status_code=500, + detail=f"Failed to delete memory: {str(e)}" + ) + + +@router.delete("/memory", response_model=DeleteAllResponse) +async def delete_all_memories( + bot_id: str = Query(..., description="Bot ID"), + user_id: str = Query(..., description="用户 ID"), + authorization: Optional[str] = Header(None, description="Authorization header"), +): + """ + 清除指定 Bot 下当前用户的所有记忆 + + Args: + bot_id: Bot ID + user_id: 用户标识符 + authorization: Authorization header + + Returns: + DeleteAllResponse: 删除的记忆数量 + """ + try: + from agent.mem0_manager import get_mem0_manager + from utils.settings import MEM0_ENABLED + + # 检查 Memory 功能是否启用 + if not MEM0_ENABLED: + raise HTTPException( + status_code=503, + detail="Memory feature is not enabled" + ) + + # 获取 Mem0Manager 实例 + manager = get_mem0_manager() + + # 删除所有记忆 + deleted_count = await manager.delete_all_memories( + user_id=user_id, + agent_id=bot_id + ) + + return DeleteAllResponse(deleted_count=deleted_count) + + except HTTPException: + raise + except Exception as e: + logger.error(f"Failed to delete all memories: {e}") + raise HTTPException( + status_code=500, + detail=f"Failed to delete all memories: {str(e)}" + )