199 lines
7.3 KiB
Python
199 lines
7.3 KiB
Python
"""世界模型服务(河图枢运行时,api-spec §12)。
|
|
|
|
按人物视角隔离:所有读写经 ``resolve_active_character`` 绑定当前激活人物。
|
|
自动抽取(世界引擎)属 v1 半自动留后项,本层先承载手动 CRUD。
|
|
"""
|
|
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.core.config import settings
|
|
from app.core.errors import AppError
|
|
from app.db.session import AsyncSessionLocal
|
|
from app.models.world import CharacterWorldEntity, WorldRelation
|
|
from app.repositories import world as world_repo
|
|
from app.repositories.json_utils import dumps
|
|
from app.schemas.common import Page
|
|
from app.schemas.world import (
|
|
WorldEntityCreateRequest,
|
|
WorldEntityResponse,
|
|
WorldGraphResponse,
|
|
WorldRelationCreateRequest,
|
|
WorldRelationResponse,
|
|
)
|
|
from app.services.audit import record_audit_event
|
|
from app.services.characters import resolve_active_character
|
|
from app.services.serializers import (
|
|
character_world_entity_to_response,
|
|
world_relation_to_response,
|
|
)
|
|
|
|
|
|
async def list_entities_page(
|
|
character_id: str | None = None,
|
|
*,
|
|
entity_type: str | None = None,
|
|
page: int = 1,
|
|
page_size: int = 20,
|
|
owner_scope: str = settings.default_owner_scope,
|
|
) -> Page[WorldEntityResponse]:
|
|
async with AsyncSessionLocal() as session:
|
|
character = await resolve_active_character(session, character_id, owner_scope=owner_scope)
|
|
safe_page = max(page, 1)
|
|
safe_size = min(max(page_size, 1), 100)
|
|
items, total = await world_repo.list_entities(
|
|
session,
|
|
character.id,
|
|
entity_type=entity_type,
|
|
page=safe_page,
|
|
page_size=safe_size,
|
|
)
|
|
return Page(
|
|
items=[character_world_entity_to_response(item) for item in items],
|
|
page=safe_page,
|
|
page_size=safe_size,
|
|
total=total,
|
|
)
|
|
|
|
|
|
async def get_entity_by_id(
|
|
entity_id: str, owner_scope: str = settings.default_owner_scope
|
|
) -> WorldEntityResponse:
|
|
async with AsyncSessionLocal() as session:
|
|
character = await resolve_active_character(session, None, owner_scope=owner_scope)
|
|
entity = await world_repo.get_entity(session, entity_id)
|
|
if entity is None or entity.character_id != character.id:
|
|
raise AppError("not_found", "世界实体不存在。")
|
|
return character_world_entity_to_response(entity)
|
|
|
|
|
|
async def create_entity(
|
|
payload: WorldEntityCreateRequest, owner_scope: str = settings.default_owner_scope
|
|
) -> WorldEntityResponse:
|
|
async with AsyncSessionLocal() as session:
|
|
character = await resolve_active_character(
|
|
session, payload.character_id, owner_scope=owner_scope
|
|
)
|
|
entity = CharacterWorldEntity(
|
|
character_id=character.id,
|
|
type=payload.type,
|
|
name=payload.name,
|
|
attributes_json=dumps(payload.attributes),
|
|
state_type=payload.state_type,
|
|
confidence=payload.confidence,
|
|
evidence_ids_json=dumps(payload.evidence_ids),
|
|
sources_json=dumps(payload.sources),
|
|
)
|
|
await world_repo.add_entity(session, entity)
|
|
await _audit(session, character.id, "world.entity.created", entity.name, entity.id)
|
|
await session.commit()
|
|
await session.refresh(entity)
|
|
return character_world_entity_to_response(entity)
|
|
|
|
|
|
async def list_relations_page(
|
|
character_id: str | None = None,
|
|
*,
|
|
page: int = 1,
|
|
page_size: int = 20,
|
|
owner_scope: str = settings.default_owner_scope,
|
|
) -> Page[WorldRelationResponse]:
|
|
async with AsyncSessionLocal() as session:
|
|
character = await resolve_active_character(session, character_id, owner_scope=owner_scope)
|
|
safe_page = max(page, 1)
|
|
safe_size = min(max(page_size, 1), 100)
|
|
items, total = await world_repo.list_relations(
|
|
session, character.id, page=safe_page, page_size=safe_size
|
|
)
|
|
return Page(
|
|
items=[world_relation_to_response(item) for item in items],
|
|
page=safe_page,
|
|
page_size=safe_size,
|
|
total=total,
|
|
)
|
|
|
|
|
|
async def create_relation(
|
|
payload: WorldRelationCreateRequest, owner_scope: str = settings.default_owner_scope
|
|
) -> WorldRelationResponse:
|
|
async with AsyncSessionLocal() as session:
|
|
character = await resolve_active_character(
|
|
session, payload.character_id, owner_scope=owner_scope
|
|
)
|
|
subject = await _ensure_owned_entity(session, character.id, payload.subject_id, "主体")
|
|
obj = await _ensure_owned_entity(session, character.id, payload.object_id, "客体")
|
|
relation = WorldRelation(
|
|
character_id=character.id,
|
|
subject_id=subject.id,
|
|
predicate=payload.predicate,
|
|
object_id=obj.id,
|
|
confidence=payload.confidence,
|
|
evidence_ids_json=dumps(payload.evidence_ids),
|
|
)
|
|
await world_repo.add_relation(session, relation)
|
|
await _audit(
|
|
session, character.id, "world.relation.created", relation.predicate, relation.id
|
|
)
|
|
await session.commit()
|
|
await session.refresh(relation)
|
|
return world_relation_to_response(relation)
|
|
|
|
|
|
async def delete_relation(
|
|
relation_id: str, owner_scope: str = settings.default_owner_scope
|
|
) -> None:
|
|
"""删除世界关系。"""
|
|
async with AsyncSessionLocal() as session:
|
|
character = await resolve_active_character(session, None, owner_scope=owner_scope)
|
|
relation = await world_repo.get_relation(session, relation_id)
|
|
if relation is None or relation.character_id != character.id:
|
|
raise AppError("not_found", "关系不存在。")
|
|
await world_repo.delete_relation(session, relation_id)
|
|
await _audit(
|
|
session, character.id, "world.relation.deleted", relation.predicate, relation_id
|
|
)
|
|
await session.commit()
|
|
|
|
|
|
async def get_graph(
|
|
character_id: str | None = None,
|
|
*,
|
|
owner_scope: str = settings.default_owner_scope,
|
|
) -> WorldGraphResponse:
|
|
"""返回人物的完整世界图谱(无分页,所有节点与边),供前端渲染力导向图。"""
|
|
async with AsyncSessionLocal() as session:
|
|
character = await resolve_active_character(session, character_id, owner_scope=owner_scope)
|
|
entities = await world_repo.list_all_entities(session, character.id)
|
|
relations = await world_repo.list_all_relations(session, character.id)
|
|
return WorldGraphResponse(
|
|
nodes=[character_world_entity_to_response(e) for e in entities],
|
|
edges=[world_relation_to_response(r) for r in relations],
|
|
)
|
|
|
|
|
|
async def _ensure_owned_entity(
|
|
session: AsyncSession, character_id: str, entity_id: str, role: str
|
|
) -> CharacterWorldEntity:
|
|
entity = await world_repo.get_entity(session, entity_id)
|
|
if entity is None or entity.character_id != character_id:
|
|
raise AppError("not_found", f"关系{role}实体不存在。")
|
|
return entity
|
|
|
|
|
|
async def _audit(
|
|
session: AsyncSession,
|
|
character_id: str,
|
|
action: str,
|
|
summary: str,
|
|
resource_id: str,
|
|
) -> None:
|
|
await record_audit_event(
|
|
session,
|
|
action=action,
|
|
character_id=character_id,
|
|
actor="user",
|
|
decision="auto",
|
|
input_summary=summary,
|
|
resource_type="world_entity" if "entity" in action else "world_relation",
|
|
resource_id=resource_id,
|
|
)
|