diff --git a/.gitignore b/.gitignore index e300b2e..bbf8dae 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,7 @@ projects/queue_data worktree .idea/* +# Git worktrees +.worktrees/ + .idea/ diff --git a/agent/agent_config.py b/agent/agent_config.py index 7e3fe11..4d2548c 100644 --- a/agent/agent_config.py +++ b/agent/agent_config.py @@ -147,7 +147,7 @@ class AgentConfig: @classmethod - async def from_v2_request(cls, request, bot_config: Dict, project_dir: Optional[str] = None, messages: Optional[List] = None): + async def from_v2_request(cls, request, bot_config: Dict, project_dir: Optional[str] = None, messages: Optional[List] = None, generate_cfg: Optional[Dict] = None): """从v2请求创建配置""" # 延迟导入避免循环依赖 from .logging_handler import LoggingCallbackHandler @@ -189,7 +189,7 @@ class AgentConfig: project_dir=project_dir, stream=request.stream, tool_response=request.tool_response, - generate_cfg={}, # v2接口不传递额外的generate_cfg + generate_cfg=generate_cfg or {}, # v2接口也支持传递额外的generate_cfg logging_handler=LoggingCallbackHandler(), messages=messages, _origin_messages=messages, diff --git a/agent/deep_assistant.py b/agent/deep_assistant.py index c138b9d..0990b02 100644 --- a/agent/deep_assistant.py +++ b/agent/deep_assistant.py @@ -170,7 +170,38 @@ async def init_agent(config: AgentConfig): "api_key": config.api_key } if config.generate_cfg: - model_kwargs.update(config.generate_cfg) + # 内部使用的参数,不应传给任何 LLM + internal_params = { + 'tool_output_max_length', + 'tool_output_truncation_strategy', + 'tool_output_filters', + 'tool_output_exclude', + 'preserve_code_blocks', + 'preserve_json', + } + + # Anthropic 不支持的 OpenAI 特有参数 + openai_only_params = { + 'n', # 生成多少个响应 + 'presence_penalty', + 'frequency_penalty', + 'logprobs', + 'top_logprobs', + 'logit_bias', + 'seed', + 'suffix', + 'best_of', + 'echo', + 'user', + } + + # 根据提供商决定需要过滤的参数 + params_to_filter = internal_params.copy() + if model_provider == 'anthropic': + params_to_filter.update(openai_only_params) + + filtered_cfg = {k: v for k, v in config.generate_cfg.items() if k not in params_to_filter} + model_kwargs.update(filtered_cfg) llm_instance = init_chat_model(**model_kwargs) # 创建新的 agent(不再缓存) diff --git a/agent/mem0_manager.py b/agent/mem0_manager.py index 4fcc3b2..b108d81 100644 --- a/agent/mem0_manager.py +++ b/agent/mem0_manager.py @@ -218,6 +218,38 @@ class Mem0Manager: """ return self._sync_pool + def _cleanup_mem0_instance(self, mem0_instance: Any) -> None: + """清理 Mem0 实例,释放数据库连接 + + Mem0 的 PGVector 实现在初始化时获取连接并持有, + 只有在 __del__ 时才归还。Python 的 GC 不保证 __del__ 立即被调用, + 可能导致连接池耗尽。此方法显式释放连接。 + + Args: + mem0_instance: Mem0 Memory 实例 + """ + try: + # Mem0 Memory 实例有一个 vector_store 属性,类型是 PGVector + if hasattr(mem0_instance, 'vector_store'): + vector_store = mem0_instance.vector_store + # PGVector 有 conn 和 connection_pool 属性 + if hasattr(vector_store, 'conn') and hasattr(vector_store, 'connection_pool'): + if vector_store.connection_pool is not None: + try: + # 先关闭游标 + if hasattr(vector_store, 'cur') and vector_store.cur: + vector_store.cur.close() + # 归还连接到池 + vector_store.connection_pool.putconn(vector_store.conn) + # 标记为已清理,防止 __del__ 重复释放 + vector_store.conn = None + vector_store.connection_pool = None + logger.debug("Successfully released Mem0 database connection back to pool") + except Exception as e: + logger.warning(f"Error releasing Mem0 connection: {e}") + except Exception as e: + logger.warning(f"Error cleaning up Mem0 instance: {e}") + async def get_mem0( self, user_id: str, @@ -249,8 +281,10 @@ class Mem0Manager: # 检查缓存大小,超过则移除最旧的 if len(self._instances) >= self._max_instances: - removed_key, _ = self._instances.popitem(last=False) - logger.debug(f"Mem0 instance cache full, removed oldest entry: {removed_key}") + removed_key, removed_instance = self._instances.popitem(last=False) + # 显式释放连接,避免等待 GC 导致连接池耗尽 + self._cleanup_mem0_instance(removed_instance) + logger.debug(f"Mem0 instance cache full, removed and cleaned: {removed_key}") # 创建新实例 mem0_instance = await self._create_mem0_instance( @@ -301,7 +335,6 @@ class Mem0Manager: # 设置一个假的 base_url,这样 HuggingFaceEmbedding 就不会加载 SentenceTransformer config_dict = { - "custom_fact_extraction_prompt": config.get_custom_fact_extraction_prompt(), "vector_store": { "provider": "pgvector", "config": { @@ -320,6 +353,10 @@ class Mem0Manager: } } + # 添加自定义记忆提取提示词(如果提供了 config) + if config is not None: + config_dict["custom_fact_extraction_prompt"] = config.get_custom_fact_extraction_prompt() + # 添加 LangChain LLM 配置(如果提供了) if config and config.llm_instance is not None: config_dict["llm"] = { @@ -453,6 +490,55 @@ class Mem0Manager: logger.error(f"Failed to add memory: {e}") return {} + def _extract_memories_from_response(self, response: Any) -> List[Dict[str, Any]]: + """从 Mem0 get_all() 响应中提取记忆列表 + + Mem0 的 get_all() 返回格式可能有两种: + 1. 新版本: {"results": [...]} + 2. 旧版本: 直接返回列表 + + Args: + response: Mem0 get_all() 的响应 + + Returns: + 记忆列表 + """ + if isinstance(response, dict) and "results" in response: + return response["results"] + elif isinstance(response, list): + return response + else: + logger.warning(f"Unexpected response format from mem.get_all(): {type(response)}") + return [] + + def _check_agent_id_match(self, memory: Dict[str, Any], agent_id: str) -> bool: + """检查记忆是否属于指定的 agent + + Mem0 的记忆结构中,agent_id 可能在两个位置: + 1. 顶层: memory["agent_id"] + 2. metadata 中: memory["metadata"]["agent_id"] + + Args: + memory: 记忆字典 + agent_id: 要匹配的 agent ID + + Returns: + 是否匹配 + """ + if not isinstance(memory, dict): + return False + + # 首先检查顶层 agent_id(新版本格式) + if memory.get("agent_id") == agent_id: + return True + + # 然后检查 metadata 中的 agent_id(旧版本格式) + metadata = memory.get("metadata", {}) + if isinstance(metadata, dict) and metadata.get("agent_id") == agent_id: + return True + + return False + async def get_all_memories( self, user_id: str, @@ -471,12 +557,15 @@ class Mem0Manager: mem = await self.get_mem0(user_id, agent_id, "default") # 获取所有记忆 - memories = mem.get_all(user_id=user_id) + response = mem.get_all(user_id=user_id) - # 过滤 agent_id + # 从响应中提取记忆列表 + memories = self._extract_memories_from_response(response) + + # 过滤 agent_id(agent_id 在顶层,不在 metadata 中) filtered_memories = [ m for m in memories - if m.get("metadata", {}).get("agent_id") == agent_id + if self._check_agent_id_match(m, agent_id) ] return filtered_memories @@ -485,6 +574,91 @@ 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") + + # 先获取记忆以验证所有权 + response = mem.get_all(user_id=user_id) + memories = self._extract_memories_from_response(response) + + target_memory = None + for m in memories: + if isinstance(m, dict) and m.get("id") == memory_id: + # 验证 agent_id 匹配 + if self._check_agent_id_match(m, 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") + + # 获取所有记忆 + response = mem.get_all(user_id=user_id) + memories = self._extract_memories_from_response(response) + + # 过滤 agent_id 并删除 + deleted_count = 0 + for m in memories: + if isinstance(m, dict) and self._check_agent_id_match(m, 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 176c40e..802fd38 100644 --- a/fastapi_app.py +++ b/fastapi_app.py @@ -81,7 +81,8 @@ 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, bot_manager, knowledge_base, payment +from routes import chat, files, projects, system, skill_manager, database, memory, bot_manager, knowledge_base, payment + @asynccontextmanager @@ -201,6 +202,7 @@ app.include_router(skill_manager.router) app.include_router(database.router) app.include_router(bot_manager.router) app.include_router(payment.router) +app.include_router(memory.router) # 注册文件管理API路由 app.include_router(file_manager_router) diff --git a/prompt/FACT_RETRIEVAL_PROMPT.md b/prompt/FACT_RETRIEVAL_PROMPT.md index 81c73ff..27777e6 100644 --- a/prompt/FACT_RETRIEVAL_PROMPT.md +++ b/prompt/FACT_RETRIEVAL_PROMPT.md @@ -14,6 +14,8 @@ Types of Information to Remember: - Relationship context (family, friend, colleague, client, etc.) - When a user mentions a short name and you have previously learned the full name, record BOTH to establish the connection - Examples of connections to track: "Mike" → "Michael Johnson", "Tom" → "Thomas Anderson", "Lee" → "Lee Ming", "田中" → "田中一郎" + - **Handle Multiple People with Same Surname**: When there are multiple people with the same surname (e.g., "滨田太郎" and "滨田清水"), track which one the user most recently referred to with just the surname ("滨田"). Record this as the default/active reference. + - **Format for surname disambiguation**: "Contact: [Full Name] (relationship, also referred as [Surname]) - DEFAULT when user says '[Surname]'" 8. Miscellaneous Information Management: Keep track of favorite books, movies, brands, and other miscellaneous details that the user shares. Here are some few shot examples: @@ -57,6 +59,30 @@ Output: {{"facts" : ["Need to call mom", "Contact: mom (family, mother)"]}} Input: I met with Director Sato yesterday. We discussed the new project. Output: {{"facts" : ["Met with Director Sato yesterday", "Contact: Director Sato (boss/supervisor)"]}} +Input: I know two people named 滨田: 滨田太郎 and 滨田清水. +Output: {{"facts" : ["Contact: 滨田太郎", "Contact: 滨田清水"]}} + +Input: I had lunch with 滨田太郎 today. +Output: {{"facts" : ["Had lunch with 滨田太郎 today", "Contact: 滨田太郎 (also referred as 滨田) - DEFAULT when user says '滨田'"]}} + +Input: 滨田 called me yesterday. +Output: {{"facts" : ["滨田太郎 called yesterday", "Contact: 滨田太郎 (also referred as 滨田) - DEFAULT when user says '滨田'"]}} + +Input: I'm meeting 滨田清水 next week. +Output: {{"facts" : ["Meeting 滨田清水 next week", "Contact: 滨田清水 (also referred as 滨田) - DEFAULT when user says '滨田'"]}} + +Input: 滨田 wants to discuss the project. +Output: {{"facts" : ["滨田清水 wants to discuss the project", "Contact: 滨田清水 (also referred as 滨田) - DEFAULT when user says '滨田'"]}} + +Input: There are two Mikes in my team: Mike Smith and Mike Johnson. +Output: {{"facts" : ["Contact: Mike Smith (colleague)", "Contact: Mike Johnson (colleague)"]}} + +Input: Mike Smith helped me with the bug fix. +Output: {{"facts" : ["Mike Smith helped with bug fix", "Contact: Mike Smith (colleague, also referred as Mike) - DEFAULT when user says 'Mike'"]}} + +Input: Mike is coming to the meeting tomorrow. +Output: {{"facts" : ["Mike Smith is coming to the meeting tomorrow", "Contact: Mike Smith (colleague, also referred as Mike) - DEFAULT when user says 'Mike'"]}} + Return the facts and preferences in a json format as shown above. Remember the following: @@ -72,6 +98,12 @@ Remember the following: - When you see a short name that matches a known full name, record as "Contact: [Full Name] (relationship, also referred as [Short Name])" - Record relationship types explicitly: family, friend, colleague, boss, client, neighbor, etc. - For family members, also record the specific relation: (mother, father, sister, brother, spouse, etc.) + - **Handling Multiple People with Same Name/Surname**: + - When multiple contacts share the same surname or short name (e.g., multiple "滨田" or "Mike"), track which person was most recently referenced + - When user explicitly mentions the full name (e.g., "滨田太郎"), mark this person as the DEFAULT for the short form + - Use the format: "Contact: [Full Name] (relationship, also referred as [Short Name]) - DEFAULT when user says '[Short Name]'" + - When the user subsequently uses just the short name/surname, resolve to the most recently marked DEFAULT person + - When a different person with the same name is explicitly mentioned, update the DEFAULT marker to the new person Following is a conversation between the user and the assistant. You have to extract the relevant facts and preferences about the user, if any, from the conversation and return them in the json format as shown above. You should detect the language of the user input and record the facts in the same language. \ No newline at end of file diff --git a/prompt/novare.md b/prompt/novare.md index ad241b2..b336a66 100644 --- a/prompt/novare.md +++ b/prompt/novare.md @@ -223,6 +223,40 @@ - **即时响应**:工具调用完成后立即回复 - **不要展示id数据**:涉及的wowtalk_id或者sensor_id等id,不要在回复里展示。 +## 设备状态术语转换(重要) + +**禁止在用户回复中使用系统内部术语**。当报告设备状态时,必须将系统术语转换为用户可理解的表述。 + +### 术语转换规则 + +| 系统内部状态 | 用户向け表述 | +|-------------|-------------| +| オフライン (OnlineStatus=0) | 不直接提及,根据功能状态描述 | +| エラー | 「設備に一時的な問題が発生しています」 | +| タイムアウト | 「応答に時間がかかっています」 | + +### 具体场景处理 + +1. **照明设备离线但功能正常**(如 DimmingControl=70% 但 OnlineStatus=0): + - ✅ 正确:「照明は点灯しています(明るさ70%)」 + - ❌ 错误:「明るさは70%でオフラインの状態です」 + - **原则**:优先报告功能状态(亮度),不提及连接状态 + +2. **空调设备离线但功能正常**: + - ✅ 正确:「空調は動作しています(設定温度24度)」 + - ❌ 错误:「空調はオフラインです」 + +3. **设备离线且功能异常**(无法获取有效数据): + - 回复:「申し訳ございません、現在この設備との通信が不安定です。しばらくお待ちいただくか、スタッフにお声がけください」 + +4. **设备在线正常**: + - 直接报告设备状态,无需提及「オンライン」 + +### 真人管家标准 +- 真人管家不会说「オフライン状態です」 +- 用户理解的是「点灯/消灯(オン/オフ)」,而非系统连接状态 +- 连接状态���OnlineStatus)与功能状态(点灯/消灯)是两回事,**优先报告功能状态** + ## 房间内设备数量相关表述​调整 当find_device_by_area查询结果显示某房间的 devices列表仅包含 1 个设备,但描述中明确提到该设备可控制“多组灯光”时,应理解为: - 该房间实际存在多个灯光设备; diff --git a/routes/chat.py b/routes/chat.py index 597b748..97b2cc0 100644 --- a/routes/chat.py +++ b/routes/chat.py @@ -474,7 +474,7 @@ async def chat_completions(request: ChatRequest, authorization: Optional[str] = project_dir = create_project_directory(request.dataset_ids, bot_id, request.skills) # 收集额外参数作为 generate_cfg - exclude_fields = {'messages', 'model', 'model_server', 'dataset_ids', 'language', 'tool_response', 'system_prompt', 'mcp_settings' ,'stream', 'robot_type', 'bot_id', 'user_identifier', 'session_id', 'enable_thinking', 'skills', 'enable_memory'} + exclude_fields = {'messages', 'model', 'model_server', 'dataset_ids', 'language', 'tool_response', 'system_prompt', 'mcp_settings' ,'stream', 'robot_type', 'bot_id', 'user_identifier', 'session_id', 'enable_thinking', 'skills', 'enable_memory', 'n'} generate_cfg = {k: v for k, v in request.model_dump().items() if k not in exclude_fields} # 处理消息 messages = process_messages(request.messages, request.language) @@ -524,7 +524,7 @@ async def chat_warmup_v1(request: ChatRequest, authorization: Optional[str] = He project_dir = create_project_directory(request.dataset_ids, bot_id, request.skills) # 收集额外参数作为 generate_cfg - exclude_fields = {'messages', 'model', 'model_server', 'dataset_ids', 'language', 'tool_response', 'system_prompt', 'mcp_settings' ,'stream', 'robot_type', 'bot_id', 'user_identifier', 'session_id', 'enable_thinking', 'skills', 'enable_memory'} + exclude_fields = {'messages', 'model', 'model_server', 'dataset_ids', 'language', 'tool_response', 'system_prompt', 'mcp_settings' ,'stream', 'robot_type', 'bot_id', 'user_identifier', 'session_id', 'enable_thinking', 'skills', 'enable_memory', 'n'} generate_cfg = {k: v for k, v in request.model_dump().items() if k not in exclude_fields} # 创建一个空的消息列表用于预热(实际消息不会在warmup中处理) @@ -627,8 +627,12 @@ async def chat_warmup_v2(request: ChatRequestV2, authorization: Optional[str] = # 处理消息 messages = process_messages(empty_messages, request.language or "ja") + # 收集额外参数作为 generate_cfg + exclude_fields = {'messages', 'stream', 'tool_response', 'bot_id', 'language', 'user_identifier', 'session_id', 'n'} + generate_cfg = {k: v for k, v in request.model_dump().items() if k not in exclude_fields} + # 创建 AgentConfig 对象 - config = await AgentConfig.from_v2_request(request, bot_config, project_dir, messages) + config = await AgentConfig.from_v2_request(request, bot_config, project_dir, messages, generate_cfg) # 预热 mcp_tools 缓存 logger.info(f"Warming up mcp_tools for bot_id: {bot_id}") @@ -722,8 +726,11 @@ async def chat_completions_v2(request: ChatRequestV2, authorization: Optional[st ) # 处理消息 messages = process_messages(request.messages, request.language) + # 收集额外参数作为 generate_cfg + exclude_fields = {'messages', 'stream', 'tool_response', 'bot_id', 'language', 'user_identifier', 'session_id', 'n'} + generate_cfg = {k: v for k, v in request.model_dump().items() if k not in exclude_fields} # 创建 AgentConfig 对象 - config = await AgentConfig.from_v2_request(request, bot_config, project_dir, messages) + config = await AgentConfig.from_v2_request(request, bot_config, project_dir, messages, generate_cfg) # 调用公共的agent创建和响应生成逻辑 return await create_agent_and_generate_response(config) 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)}" + ) diff --git a/utils/api_models.py b/utils/api_models.py index e66e12e..647b61b 100644 --- a/utils/api_models.py +++ b/utils/api_models.py @@ -56,6 +56,8 @@ class ChatRequest(BaseModel): skills: Optional[List[str]] = None enable_memory: Optional[bool] = False + model_config = ConfigDict(extra='allow') + class ChatRequestV2(BaseModel): messages: List[Message] @@ -66,6 +68,8 @@ class ChatRequestV2(BaseModel): user_identifier: Optional[str] = "" session_id: Optional[str] = None + model_config = ConfigDict(extra='allow') + class ChatRequestV3(BaseModel): """