158 lines
5.5 KiB
Python
158 lines
5.5 KiB
Python
"""息壤(记忆域)API(api-contracts §3.5)。"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
|
||
from fastapi import APIRouter, Depends, Query
|
||
from pydantic import BaseModel, Field
|
||
from sqlalchemy import select
|
||
|
||
from app.api.v1.deps import get_current_user
|
||
from app.core.errors import AppError
|
||
from app.db.session import AsyncSessionLocal
|
||
from app.domains import xirang
|
||
from app.models.character import Character
|
||
from app.models.xirang import FactualRecord, Memory
|
||
from app.schemas.auth import CurrentUser
|
||
|
||
router = APIRouter(tags=["xirang"])
|
||
|
||
|
||
class MemorizeRequest(BaseModel):
|
||
label: str = Field(description="fact / insight / opinion / hypothesis")
|
||
|
||
|
||
class CorrectRequest(BaseModel):
|
||
new_content: str = Field(min_length=1, max_length=8000)
|
||
|
||
|
||
def _memory_view(m: Memory, with_content: bool = True) -> dict:
|
||
return {
|
||
"id": m.id,
|
||
"character_id": m.character_id,
|
||
"tier": m.tier,
|
||
"type": m.type,
|
||
"status": m.status,
|
||
"source": m.source,
|
||
"content": m.content if with_content else "",
|
||
"source_refs": json.loads(m.source_refs_json or "[]"),
|
||
"sensitivity": m.sensitivity,
|
||
"priority": m.priority,
|
||
"confidence": m.confidence,
|
||
"vitality": round(m.vitality, 3),
|
||
"realm": m.realm,
|
||
"prev_version_id": m.prev_version_id,
|
||
"recall_count": m.recall_count,
|
||
"last_recalled_at": m.last_recalled_at.isoformat() if m.last_recalled_at else None,
|
||
"created_at": m.created_at.isoformat(),
|
||
}
|
||
|
||
|
||
async def _owned_character(character_id: str, owner_scope: str) -> Character:
|
||
async with AsyncSessionLocal() as session:
|
||
character = await session.get(Character, character_id)
|
||
if character is None or character.owner_scope != owner_scope:
|
||
raise AppError("not_found", "人物不存在。")
|
||
return character
|
||
|
||
|
||
@router.get("/characters/{character_id}/memories")
|
||
async def list_memories(
|
||
character_id: str,
|
||
tier: str | None = Query(default=None),
|
||
status: str | None = Query(default=None),
|
||
type_: str | None = Query(default=None, alias="type"),
|
||
user: CurrentUser = Depends(get_current_user),
|
||
) -> list[dict]:
|
||
await _owned_character(character_id, user.owner_scope)
|
||
async with AsyncSessionLocal() as session:
|
||
stmt = select(Memory).where(Memory.character_id == character_id)
|
||
if tier:
|
||
stmt = stmt.where(Memory.tier == tier)
|
||
if status:
|
||
stmt = stmt.where(Memory.status == status)
|
||
if type_:
|
||
stmt = stmt.where(Memory.type == type_)
|
||
stmt = stmt.order_by(Memory.created_at.desc()).limit(200)
|
||
rows = (await session.execute(stmt)).scalars().all()
|
||
# 高敏不展示原文(sensitivity=high 摘要化);墓碑不承载原文
|
||
return [
|
||
_memory_view(
|
||
m,
|
||
with_content=not (m.sensitivity == "high" or m.status == "tombstoned"),
|
||
)
|
||
for m in rows
|
||
]
|
||
|
||
|
||
@router.get("/memories/{memory_id}")
|
||
async def get_memory(memory_id: str, user: CurrentUser = Depends(get_current_user)) -> dict:
|
||
async with AsyncSessionLocal() as session:
|
||
memory = await session.get(Memory, memory_id)
|
||
if memory is not None:
|
||
character = await session.get(Character, memory.character_id)
|
||
if memory is None or character is None or character.owner_scope != user.owner_scope:
|
||
raise AppError("not_found", "记忆不存在。")
|
||
return _memory_view(memory)
|
||
|
||
|
||
@router.post("/memories/{memory_id}/confirm")
|
||
async def confirm_memory_api(memory_id: str, user: CurrentUser = Depends(get_current_user)) -> dict:
|
||
memory = await xirang.confirm_memory(memory_id, user.owner_scope)
|
||
return _memory_view(memory)
|
||
|
||
|
||
@router.post("/memories/{memory_id}/deny")
|
||
async def deny_memory_api(memory_id: str, user: CurrentUser = Depends(get_current_user)) -> dict:
|
||
return await xirang.deny_memory(memory_id, user.owner_scope)
|
||
|
||
|
||
@router.post("/memories/{memory_id}/correct")
|
||
async def correct_memory_api(
|
||
memory_id: str, payload: CorrectRequest, user: CurrentUser = Depends(get_current_user)
|
||
) -> dict:
|
||
memory = await xirang.correct_memory(memory_id, user.owner_scope, payload.new_content)
|
||
return _memory_view(memory)
|
||
|
||
|
||
@router.delete("/memories/{memory_id}", status_code=204)
|
||
async def delete_memory_api(memory_id: str, user: CurrentUser = Depends(get_current_user)) -> None:
|
||
await xirang.delete_memory(memory_id, user.owner_scope)
|
||
|
||
|
||
@router.post("/nodes/{node_id}/memorize", status_code=201)
|
||
async def memorize_node_api(
|
||
node_id: str, payload: MemorizeRequest, user: CurrentUser = Depends(get_current_user)
|
||
) -> dict:
|
||
memory = await xirang.memorize_node(
|
||
node_id=node_id, owner_scope=user.owner_scope, label=payload.label
|
||
)
|
||
return _memory_view(memory)
|
||
|
||
|
||
@router.get("/characters/{character_id}/factual-records")
|
||
async def list_factual_records(
|
||
character_id: str, user: CurrentUser = Depends(get_current_user)
|
||
) -> list[dict]:
|
||
await _owned_character(character_id, user.owner_scope)
|
||
async with AsyncSessionLocal() as session:
|
||
rows = (
|
||
await session.execute(
|
||
select(FactualRecord)
|
||
.where(FactualRecord.character_id == character_id)
|
||
.order_by(FactualRecord.occurred_at.desc())
|
||
.limit(100)
|
||
)
|
||
).scalars().all()
|
||
return [
|
||
{
|
||
"id": r.id,
|
||
"canonization_id": r.canonization_id,
|
||
"summary": r.summary,
|
||
"evidence": json.loads(r.evidence_json or "{}"),
|
||
"occurred_at": r.occurred_at.isoformat(),
|
||
}
|
||
for r in rows
|
||
]
|