M3a:息壤三级记忆域——归真触发提取管线/白名单明忆/重忆确认/正影版本链/删除墓碑/双通道召回注入,退役旧记忆-抽取-反思-大周天-演化管线(迁移 004,验收 7 用例)
This commit is contained in:
1 parent
2a640a901b
commit
9c98c2f85e
69 files changed
+1193
-6033
No files matched your search
@@ -387,7 +387,7 @@ async def admin_tenants_overview() -> list[_TenantUserOverview]:
|
||||
|
||||
from app.db.session import AsyncSessionLocal
|
||||
from app.models.character import Character
|
||||
from app.models.memory import Memory
|
||||
from app.models.xirang import Memory
|
||||
from app.models.user import User
|
||||
|
||||
async with AsyncSessionLocal() as session:
|
||||
@@ -406,7 +406,7 @@ async def admin_tenants_overview() -> list[_TenantUserOverview]:
|
||||
mem_count = await session.scalar(
|
||||
select(func.count())
|
||||
.select_from(Memory)
|
||||
.join(Character, Memory.character_id == Character.id)
|
||||
.join(Character, Memory.character_id == Character.id).where(Memory.status != "tombstoned")
|
||||
.where(
|
||||
Character.owner_scope == user.id,
|
||||
Character.is_system_template.is_(False),
|
||||
@@ -454,13 +454,13 @@ async def admin_tenant_memories(user_id: str) -> list[dict]:
|
||||
|
||||
from app.db.session import AsyncSessionLocal
|
||||
from app.models.character import Character
|
||||
from app.models.memory import Memory
|
||||
from app.models.xirang import Memory
|
||||
from app.services.serializers import memory_to_response
|
||||
|
||||
async with AsyncSessionLocal() as session:
|
||||
result = await session.execute(
|
||||
select(Memory)
|
||||
.join(Character, Memory.character_id == Character.id)
|
||||
.join(Character, Memory.character_id == Character.id).where(Memory.status != "tombstoned")
|
||||
.where(
|
||||
Character.owner_scope == user_id,
|
||||
Character.is_system_template.is_(False),
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
"""演化候选审核 API。"""
|
||||
|
||||
from fastapi import APIRouter, Query
|
||||
|
||||
from app.schemas.common import Page
|
||||
from app.schemas.evolution import EvolutionCandidateResponse
|
||||
from app.services.evolution import (
|
||||
approve_evolution_candidate,
|
||||
list_evolution_candidates,
|
||||
reject_evolution_candidate,
|
||||
)
|
||||
|
||||
router = APIRouter(tags=["evolution"])
|
||||
|
||||
|
||||
@router.get(
|
||||
"/characters/{character_id}/evolution-candidates",
|
||||
response_model=Page[EvolutionCandidateResponse],
|
||||
)
|
||||
async def list_evolution_candidates_endpoint(
|
||||
character_id: str,
|
||||
status: str | None = Query(
|
||||
default=None, description="按状态过滤:pending/approved/rejected/applied"
|
||||
),
|
||||
candidate_type: str | None = Query(
|
||||
default=None, alias="type", description="按候选类型过滤(契约字典 §12.1)"
|
||||
),
|
||||
page: int = Query(default=1, ge=1),
|
||||
page_size: int = Query(default=20, ge=1, le=100),
|
||||
) -> Page[EvolutionCandidateResponse]:
|
||||
"""列出某人物的演化候选(大周天生成 + 审核状态)。"""
|
||||
return await list_evolution_candidates(
|
||||
character_id=character_id,
|
||||
status=status,
|
||||
candidate_type=candidate_type,
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/evolution-candidates/{candidate_id}/approve",
|
||||
response_model=EvolutionCandidateResponse,
|
||||
)
|
||||
async def approve_candidate(
|
||||
candidate_id: str,
|
||||
) -> EvolutionCandidateResponse:
|
||||
"""审核通过演化候选:派生到下游(人格版本/程序记忆等),status → applied。"""
|
||||
return await approve_evolution_candidate(candidate_id)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/evolution-candidates/{candidate_id}/reject",
|
||||
response_model=EvolutionCandidateResponse,
|
||||
)
|
||||
async def reject_candidate(
|
||||
candidate_id: str,
|
||||
) -> EvolutionCandidateResponse:
|
||||
"""驳回演化候选:status → rejected,不派生下游。"""
|
||||
return await reject_evolution_candidate(candidate_id)
|
||||
@@ -1,90 +0,0 @@
|
||||
"""大周天抽取候选审核 API(记忆候选 + 世界实体候选)。"""
|
||||
|
||||
from fastapi import APIRouter, Query
|
||||
|
||||
from app.schemas.candidates import MemoryCandidateResponse
|
||||
from app.schemas.common import Page
|
||||
from app.schemas.world import WorldEntityResponse
|
||||
from app.services.extraction import (
|
||||
approve_memory_proposal,
|
||||
approve_world_entity,
|
||||
list_memory_proposals,
|
||||
list_world_entities_pending,
|
||||
reject_memory_proposal,
|
||||
reject_world_entity,
|
||||
)
|
||||
|
||||
router = APIRouter(tags=["extraction"])
|
||||
|
||||
|
||||
@router.get(
|
||||
"/characters/{character_id}/memory-candidates",
|
||||
response_model=Page[MemoryCandidateResponse],
|
||||
)
|
||||
async def list_memory_candidates_endpoint(
|
||||
character_id: str,
|
||||
source: str | None = Query(
|
||||
default=None, description="来源过滤:cycle(大周天)/ import(资料导入)"
|
||||
),
|
||||
status: str | None = Query(default=None),
|
||||
page: int = Query(default=1, ge=1),
|
||||
page_size: int = Query(default=20, ge=1, le=100),
|
||||
) -> Page[MemoryCandidateResponse]:
|
||||
"""列出记忆候选(大周天 + 资料导入统一)。"""
|
||||
return await list_memory_proposals(
|
||||
character_id=character_id,
|
||||
source=source,
|
||||
status=status,
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/memory-candidates/{candidate_id}/approve",
|
||||
response_model=MemoryCandidateResponse,
|
||||
)
|
||||
async def approve_memory_candidate_endpoint(
|
||||
candidate_id: str,
|
||||
) -> MemoryCandidateResponse:
|
||||
"""通过记忆候选:派生为 memories 记录(source=big_cycle)。"""
|
||||
return await approve_memory_proposal(candidate_id)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/memory-candidates/{candidate_id}/reject",
|
||||
response_model=MemoryCandidateResponse,
|
||||
)
|
||||
async def reject_memory_candidate_endpoint(
|
||||
candidate_id: str,
|
||||
) -> MemoryCandidateResponse:
|
||||
"""驳回记忆候选。"""
|
||||
return await reject_memory_proposal(candidate_id)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/characters/{character_id}/world/entity-proposals",
|
||||
response_model=Page[WorldEntityResponse],
|
||||
)
|
||||
async def list_world_entity_proposals_endpoint(
|
||||
character_id: str,
|
||||
status: str | None = Query(default="pending", description="默认 pending(候选)"),
|
||||
page: int = Query(default=1, ge=1),
|
||||
page_size: int = Query(default=20, ge=1, le=100),
|
||||
) -> Page[WorldEntityResponse]:
|
||||
"""列出世界实体候选(大周天抽取,status=pending 待审核)。"""
|
||||
return await list_world_entities_pending(
|
||||
character_id=character_id, status=status, page=page, page_size=page_size
|
||||
)
|
||||
|
||||
|
||||
@router.post("/world/entities/{entity_id}/approve", response_model=WorldEntityResponse)
|
||||
async def approve_world_entity_endpoint(entity_id: str) -> WorldEntityResponse:
|
||||
"""通过世界实体候选:status → confirmed 进入世界模型。"""
|
||||
return await approve_world_entity(entity_id)
|
||||
|
||||
|
||||
@router.post("/world/entities/{entity_id}/reject", response_model=WorldEntityResponse)
|
||||
async def reject_world_entity_endpoint(entity_id: str) -> WorldEntityResponse:
|
||||
"""驳回世界实体候选。"""
|
||||
return await reject_world_entity(entity_id)
|
||||
@@ -1,73 +0,0 @@
|
||||
"""记忆 API。"""
|
||||
|
||||
from fastapi import APIRouter, Depends, Header, Query, Request
|
||||
|
||||
from app.api.v1.deps import get_current_user
|
||||
from app.schemas.auth import CurrentUser
|
||||
from app.schemas.common import Page
|
||||
from app.schemas.memory import MemoryCreateRequest, MemoryResponse, MemoryUpdateRequest
|
||||
from app.services.idempotency import run_idempotent
|
||||
from app.services.memory import create_memory, delete_memory, list_memory_page, update_memory
|
||||
|
||||
router = APIRouter(prefix="/memory", tags=["memory"])
|
||||
|
||||
|
||||
@router.get("", response_model=Page[MemoryResponse])
|
||||
async def list_memory(
|
||||
page: int = Query(default=1, ge=1),
|
||||
page_size: int = Query(default=20, ge=1, le=100),
|
||||
memory_type: str | None = Query(
|
||||
default=None,
|
||||
alias="type",
|
||||
description="按类型过滤(如 user/assistant/system,契约字典 memory.type)。",
|
||||
),
|
||||
source: str | None = Query(
|
||||
default=None,
|
||||
description="按来源过滤(如 user_explicit/big_cycle/import,契约字典 memory.source)。",
|
||||
),
|
||||
current_user: CurrentUser = Depends(get_current_user),
|
||||
) -> Page[MemoryResponse]:
|
||||
return await list_memory_page(
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
memory_type=memory_type,
|
||||
source=source,
|
||||
owner_scope=current_user.owner_scope,
|
||||
)
|
||||
|
||||
|
||||
@router.post("", response_model=MemoryResponse)
|
||||
async def create_memory_api(
|
||||
payload: MemoryCreateRequest,
|
||||
request: Request,
|
||||
idempotency_key: str | None = Header(default=None, alias="Idempotency-Key"),
|
||||
current_user: CurrentUser = Depends(get_current_user),
|
||||
) -> MemoryResponse:
|
||||
return await run_idempotent(
|
||||
key=idempotency_key,
|
||||
method="POST",
|
||||
path=str(request.url.path),
|
||||
payload=payload,
|
||||
response_type=MemoryResponse,
|
||||
resource_type="memory",
|
||||
handler=lambda session: create_memory(
|
||||
session, payload, owner_scope=current_user.owner_scope
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@router.patch("/{memory_id}", response_model=MemoryResponse)
|
||||
async def update_memory_api(
|
||||
memory_id: str,
|
||||
payload: MemoryUpdateRequest,
|
||||
current_user: CurrentUser = Depends(get_current_user),
|
||||
) -> MemoryResponse:
|
||||
return await update_memory(memory_id, payload, owner_scope=current_user.owner_scope)
|
||||
|
||||
|
||||
@router.delete("/{memory_id}", response_model=MemoryResponse)
|
||||
async def delete_memory_api(
|
||||
memory_id: str,
|
||||
current_user: CurrentUser = Depends(get_current_user),
|
||||
) -> MemoryResponse:
|
||||
return await delete_memory(memory_id, owner_scope=current_user.owner_scope)
|
||||
@@ -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