Convert all Chinese comments, docstrings, logger/print output, HTTPException detail messages, and API response messages to English across the entire codebase. Functional zh/ja localized strings (e.g. prompt templates, timezone display names, date formats) are preserved as-is. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
339 lines
10 KiB
Python
339 lines
10 KiB
Python
"""
|
|
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)}"
|
|
)
|