M3a:息壤三级记忆域——归真触发提取管线/白名单明忆/重忆确认/正影版本链/删除墓碑/双通道召回注入,退役旧记忆-抽取-反思-大周天-演化管线(迁移 004,验收 7 用例)
This commit is contained in:
1 parent
2a640a901b
commit
9c98c2f85e
69 files changed
+1193
-6033
No files matched your search
@@ -0,0 +1,157 @@
|
||||
"""息壤(记忆域)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
|
||||
]
|
||||
Reference in new issue
Block a user