""" Memory management API routes. Provides memory viewing, adding, and deletion features. """ import logging from typing import Literal, Optional, List, Dict, Any from fastapi import APIRouter, HTTPException, Header, Query from fastapi.responses import JSONResponse from pydantic import BaseModel, Field logger = logging.getLogger('app') router = APIRouter(prefix="/api/v1", tags=["memory"]) class MemoryItem(BaseModel): """Memory item data model.""" id: str content: str created_at: Optional[str] = None updated_at: Optional[str] = None class MemoryListResponse(BaseModel): """Memory list response.""" memories: List[MemoryItem] total: int class DeleteAllResponse(BaseModel): """Delete-all memories response.""" deleted_count: int class ConversationMessage(BaseModel): """Conversation message.""" role: Literal["user", "assistant"] content: str = Field(..., min_length=1) class AddMemoryRequest(BaseModel): """Request body for adding memories.""" bot_id: str = Field(..., min_length=1) user_id: str = Field(..., min_length=1) messages: List[ConversationMessage] = Field(..., max_length=200) class AddMemoryResponse(BaseModel): """Response for adding memories.""" success: bool pairs_processed: int pairs_failed: int = 0 async def get_user_identifier_from_request( authorization: Optional[str], user_id: Optional[str] = None ) -> str: """ Get the user identifier Prefer the user_id from request parameters; otherwise try to parse it from the Authorization header Args: authorization: Authorization header user_id: user_id from request parameters Returns: User identifier Raises: HTTPException: If the user identifier cannot be determined """ if user_id: return user_id # Raise an exception if user_id is not provided # Note: According to the PRD, user_id is passed from the frontend raise HTTPException( status_code=400, detail="user_id is required" ) @router.post("/memory", response_model=AddMemoryResponse) async def add_memory_from_conversation(data: AddMemoryRequest): """ Extract and save memories from conversation messages Pairs user and assistant messages, then uses Mem0 to extract and store key facts. Used for scenarios such as realtime voice conversations that do not go through the Agent middleware. This endpoint is intended for internal services such as felo-mygpt and is not exposed to external users. """ try: from agent.mem0_manager import get_mem0_manager from utils.settings import MEM0_ENABLED if not MEM0_ENABLED: raise HTTPException( status_code=503, detail="Memory feature is not enabled" ) if not data.messages: return AddMemoryResponse(success=True, pairs_processed=0) manager = get_mem0_manager() # Pair messages into user-assistant pairs and then call add_memory pairs_processed = 0 pairs_failed = 0 i = 0 while i < len(data.messages): msg = data.messages[i] if msg.role == 'user': # Collect consecutive user messages user_contents = [msg.content] j = i + 1 while j < len(data.messages) and data.messages[j].role == 'user': user_contents.append(data.messages[j].content) j += 1 user_text = '\n'.join(user_contents) # Check whether there is a corresponding assistant reply assistant_text = "" if j < len(data.messages) and data.messages[j].role == 'assistant': assistant_text = data.messages[j].content or "" j += 1 if user_text and assistant_text: conversation_text = f"User: {user_text}\nAssistant: {assistant_text}" try: await manager.add_memory( text=conversation_text, user_id=data.user_id, agent_id=data.bot_id, metadata={"type": "realtime_conversation"}, ) pairs_processed += 1 except Exception as pair_error: pairs_failed += 1 logger.error( f"Failed to add memory for pair: {pair_error}" ) i = j else: i += 1 logger.info( f"Added {pairs_processed} memory pairs (failed={pairs_failed}) " f"for user={data.user_id}, bot={data.bot_id}" ) return AddMemoryResponse( success=pairs_failed == 0, pairs_processed=pairs_processed, pairs_failed=pairs_failed, ) except HTTPException: raise except Exception as e: logger.error(f"Failed to add memory from conversation: {e}") raise HTTPException( status_code=500, detail="Failed to add memory from conversation" ) @router.get("/memory", response_model=MemoryListResponse) async def get_memories( bot_id: str = Query(..., description="Bot ID (corresponds to agent_id)"), user_id: str = Query(..., description="User ID"), authorization: Optional[str] = Header(None, description="Authorization header"), ): """ Get all memories for the current user under the specified bot Args: bot_id: Bot ID (corresponds to agent_id) user_id: User identifier authorization: Authorization header (for authentication) Returns: MemoryListResponse: Memory list """ try: from agent.mem0_manager import get_mem0_manager from utils.settings import MEM0_ENABLED # Check whether the memory feature is enabled if not MEM0_ENABLED: raise HTTPException( status_code=503, detail="Memory feature is not enabled" ) # Get the Mem0Manager instance manager = get_mem0_manager() # Get all memories memories = await manager.get_all_memories( user_id=user_id, agent_id=bot_id ) # Convert to the response format 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 (used for permission validation)"), user_id: str = Query(..., description="User ID"), authorization: Optional[str] = Header(None, description="Authorization header"), ): """ Delete a single memory Args: memory_id: Memory ID bot_id: Bot ID (used for permission validation) user_id: User identifier authorization: Authorization header Returns: 204 No Content """ try: from agent.mem0_manager import get_mem0_manager from utils.settings import MEM0_ENABLED # Check whether the memory feature is enabled if not MEM0_ENABLED: raise HTTPException( status_code=503, detail="Memory feature is not enabled" ) # Get the Mem0Manager instance manager = get_mem0_manager() # Delete the memory 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="User ID"), authorization: Optional[str] = Header(None, description="Authorization header"), ): """ Clear all memories for the current user under the specified bot Args: bot_id: Bot ID user_id: User identifier authorization: Authorization header Returns: DeleteAllResponse: Number of deleted memories """ try: from agent.mem0_manager import get_mem0_manager from utils.settings import MEM0_ENABLED # Check whether the memory feature is enabled if not MEM0_ENABLED: raise HTTPException( status_code=503, detail="Memory feature is not enabled" ) # Get the Mem0Manager instance manager = get_mem0_manager() # Delete all memories 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)}" )