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
269 lines
8.3 KiB
Python
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}
|