"""Memory management APIs.""" from fastapi import APIRouter, Depends, HTTPException from pydantic import BaseModel, Field from sqlalchemy.ext.asyncio import AsyncSession from app.core.deps import require_user from app.db.database import get_db from app.db.models import ChildProfile, User from app.services import memory_service from app.services.memory_service import MemoryType router = APIRouter() class MemoryItemResponse(BaseModel): """Memory item response.""" id: str type: str value: dict base_weight: float ttl_days: int | None created_at: str last_used_at: str | None class Config: from_attributes = True class MemoryListResponse(BaseModel): """Memory list response.""" memories: list[MemoryItemResponse] total: int class CreateMemoryRequest(BaseModel): """Create memory request.""" type: str = Field(..., description="记忆类型") value: dict = Field(..., description="记忆内容") universe_id: str | None = Field(default=None, description="关联的故事宇宙 ID") weight: float | None = Field(default=None, description="权重") ttl_days: int | None = Field(default=None, description="过期天数") class CreateCharacterMemoryRequest(BaseModel): """Create character memory request.""" name: str = Field(..., description="角色名称") description: str | None = Field(default=None, description="角色描述") source_story_id: int | None = Field(default=None, description="来源故事 ID") affinity_score: float = Field(default=1.0, ge=0.0, le=1.0, description="喜爱程度") universe_id: str | None = Field(default=None, description="关联的故事宇宙 ID") class CreateScaryElementRequest(BaseModel): """Create scary element memory request.""" keyword: str = Field(..., description="回避的关键词") category: str = Field(default="other", description="分类") source_story_id: int | None = Field(default=None, description="来源故事 ID") async def _verify_profile_ownership( profile_id: str, user: User, db: AsyncSession ) -> ChildProfile: """验证档案所有权。""" from sqlalchemy import select result = await db.execute( select(ChildProfile).where( ChildProfile.id == profile_id, ChildProfile.user_id == user.id, ) ) profile = result.scalar_one_or_none() if not profile: raise HTTPException(status_code=404, detail="档案不存在") return profile @router.get("/profiles/{profile_id}/memories", response_model=MemoryListResponse) async def list_memories( profile_id: str, memory_type: str | None = None, universe_id: str | None = None, limit: int = 50, user: User = Depends(require_user), db: AsyncSession = Depends(get_db), ): """获取档案的记忆列表。""" await _verify_profile_ownership(profile_id, user, db) memories = await memory_service.get_profile_memories( db=db, profile_id=profile_id, memory_type=memory_type, universe_id=universe_id, limit=limit, ) return MemoryListResponse( memories=[ MemoryItemResponse( id=m.id, type=m.type, value=m.value, base_weight=m.base_weight, ttl_days=m.ttl_days, created_at=m.created_at.isoformat() if m.created_at else "", last_used_at=m.last_used_at.isoformat() if m.last_used_at else None, ) for m in memories ], total=len(memories), ) @router.post("/profiles/{profile_id}/memories", response_model=MemoryItemResponse) async def create_memory( profile_id: str, payload: CreateMemoryRequest, user: User = Depends(require_user), db: AsyncSession = Depends(get_db), ): """创建新的记忆项。""" await _verify_profile_ownership(profile_id, user, db) # 验证类型 valid_types = [ MemoryType.RECENT_STORY, MemoryType.FAVORITE_CHARACTER, MemoryType.SCARY_ELEMENT, MemoryType.VOCABULARY_GROWTH, MemoryType.EMOTIONAL_HIGHLIGHT, MemoryType.READING_PREFERENCE, MemoryType.MILESTONE, MemoryType.SKILL_MASTERED, ] if payload.type not in valid_types: raise HTTPException(status_code=400, detail=f"无效的记忆类型: {payload.type}") memory = await memory_service.create_memory( db=db, profile_id=profile_id, memory_type=payload.type, value=payload.value, universe_id=payload.universe_id, weight=payload.weight, ttl_days=payload.ttl_days, ) return MemoryItemResponse( id=memory.id, type=memory.type, value=memory.value, base_weight=memory.base_weight, ttl_days=memory.ttl_days, created_at=memory.created_at.isoformat() if memory.created_at else "", last_used_at=memory.last_used_at.isoformat() if memory.last_used_at else None, ) @router.post("/profiles/{profile_id}/memories/character", response_model=MemoryItemResponse) async def create_character_memory( profile_id: str, payload: CreateCharacterMemoryRequest, user: User = Depends(require_user), db: AsyncSession = Depends(get_db), ): """添加喜欢的角色。""" await _verify_profile_ownership(profile_id, user, db) memory = await memory_service.create_character_memory( db=db, profile_id=profile_id, name=payload.name, description=payload.description, source_story_id=payload.source_story_id, affinity_score=payload.affinity_score, universe_id=payload.universe_id, ) return MemoryItemResponse( id=memory.id, type=memory.type, value=memory.value, base_weight=memory.base_weight, ttl_days=memory.ttl_days, created_at=memory.created_at.isoformat() if memory.created_at else "", last_used_at=memory.last_used_at.isoformat() if memory.last_used_at else None, ) @router.post("/profiles/{profile_id}/memories/scary", response_model=MemoryItemResponse) async def create_scary_element_memory( profile_id: str, payload: CreateScaryElementRequest, user: User = Depends(require_user), db: AsyncSession = Depends(get_db), ): """添加回避元素。""" await _verify_profile_ownership(profile_id, user, db) memory = await memory_service.create_scary_element_memory( db=db, profile_id=profile_id, keyword=payload.keyword, category=payload.category, source_story_id=payload.source_story_id, ) return MemoryItemResponse( id=memory.id, type=memory.type, value=memory.value, base_weight=memory.base_weight, ttl_days=memory.ttl_days, created_at=memory.created_at.isoformat() if memory.created_at else "", last_used_at=memory.last_used_at.isoformat() if memory.last_used_at else None, ) @router.delete("/profiles/{profile_id}/memories/{memory_id}") async def delete_memory( profile_id: str, memory_id: str, user: User = Depends(require_user), db: AsyncSession = Depends(get_db), ): """删除记忆项。""" from sqlalchemy import select from app.db.models import MemoryItem await _verify_profile_ownership(profile_id, user, db) result = await db.execute( select(MemoryItem).where( MemoryItem.id == memory_id, MemoryItem.child_profile_id == profile_id, ) ) memory = result.scalar_one_or_none() if not memory: raise HTTPException(status_code=404, detail="记忆不存在") await db.delete(memory) await db.commit() return {"message": "Deleted"} @router.get("/memory-types") async def list_memory_types(): """获取所有可用的记忆类型及其配置。""" types = [] for type_name, config in MemoryType.CONFIG.items(): types.append({ "type": type_name, "default_weight": config[0], "default_ttl_days": config[1], "description": config[2], }) return {"types": types}