Files
dreamweaver/backend/app/api/memories.py
torin b8d3cb4644
Some checks are pending
Build and Push Docker Images / changes (push) Waiting to run
Build and Push Docker Images / build-backend (push) Blocked by required conditions
Build and Push Docker Images / build-frontend (push) Blocked by required conditions
Build and Push Docker Images / build-admin-frontend (push) Blocked by required conditions
wip: snapshot full local workspace state
2026-04-17 18:58:11 +08:00

269 lines
8.3 KiB
Python

"""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}