Files
hunjing/backend/app/memories/big_cycle.py
T
chenxin e333dc5ea5 ✨ feat: WP-04 记忆与知识——候选版本闭环、检索管线与大周天(ADR-011/018/023)
- 迁移 9:memory_candidate(来源/敏感度/作用域/状态检查约束)、memory(resource_version 作 ETag 源、有效期、当前版本指针)、memory_version((memory_id, version) 唯一、取代链)、big_cycle(cycle_ 聚合根、phase_memory、产物版本引用)
- 候选闭环:手动提交;白名单自动批准仅限 auto+fact+低敏感(撤销窗 72h 记录于审计);敏感/高影响默认人工审核;批准同事务写 Memory+Version+Audit+Outbox(契约 4.4)
- 纠正:If-Match 校验 resource_version(缺失/过期 412),创建新版本不原地覆盖(ADR-011)
- 删除:立即阻断召回 + 清除全部版本原文(墓碑不存被删原文,契约 4.4)
- 检索管线:数据库层域/角色预过滤 → 状态/有效期过滤 → 文本匹配 → 响应级召回证据(语义检索待 embedding 端点)
- 大周天最小实现:手动触发 → 后台四阶段(回顾/对照/提议/提交)→ 模型真实评估产出 pending persona_candidate(候选先于写入);无端点 fail-closed
- 测试 60 项全绿(+6):审批流、白名单三例、ETag 正/缺失/过期、检索隔离与召回证据、删除传播与墓碑原文清除、大周天端到端
2026-09-11 10:54:20 +08:00

164 lines
6.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""大周天执行器:回顾→对照→提议→提交的最小实现(ADR-018/023)。
评估调用经模型网关(真实流);提议产物为 persona_candidate(pending),
经既有审核机制确认后才影响道体——候选先于写入不变。
"""
from datetime import UTC, datetime
from typing import Any
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
from app.gateway.adapters import AdapterRequest, GatewayError, stream_chat
from app.models import (
AuditEvent,
BigCycle,
Character,
CharacterVersion,
Message,
OutboxEvent,
ProviderEndpoint,
)
EVALUATION_SYSTEM_PROMPT = (
"你是角色演化评估器。根据近期对话、当前道体基线与内驱力,"
"提出一至三条渐进式人格或关系演化建议。只输出建议正文,简洁分条。"
)
async def _select_endpoint(db: AsyncSession) -> ProviderEndpoint | None:
return (
await db.execute(
select(ProviderEndpoint)
.where(
ProviderEndpoint.enabled.is_(True),
ProviderEndpoint.logical_model_id == "default-chat",
)
.order_by(ProviderEndpoint.priority.asc())
.limit(1)
)
).scalar_one_or_none()
async def execute_big_cycle(
factory: async_sessionmaker[AsyncSession],
cycle_id: str,
transport: Any | None = None,
) -> None:
"""四阶段:回顾(拉取消息)→ 对照(道体/内驱力)→ 提议(模型)→ 提交(候选落库)。"""
async with factory() as db:
cycle = await db.get(BigCycle, cycle_id)
if cycle is None:
return
character = await db.get(Character, cycle.character_id)
assert character is not None
try:
cycle.status = "running"
await db.commit()
# 阶段一:回顾——同域近期消息(对话暂未绑定角色维度,按个人域取窗口)
from app.models import Conversation
messages = list(
(
await db.execute(
select(Message)
.where(
Message.conversation_id.in_(
select(Conversation.id).where(
Conversation.personal_domain_id
== character.personal_domain_id
)
),
Message.role.in_(["user", "assistant"]),
)
.order_by(Message.created_at.desc())
.limit(20)
)
).scalars().all()
)
def _text_of(m: Any) -> str:
joined = "".join(
str(b.get("text", "")) for b in m.content if isinstance(b, dict)
)
return joined[:120]
recent = "\n".join(f"{m.role}: {_text_of(m)}" for m in reversed(messages))
cycle.phase_memory = {**cycle.phase_memory, "recall": {"messages": len(messages)}}
await db.commit()
# 阶段二:对照——道体与内驱力快照
cycle.phase_memory = {
**cycle.phase_memory,
"evaluate": {
"identity_baseline": character.identity_baseline,
"drives": character.drives,
},
}
await db.commit()
# 阶段三:提议——模型评估(真实流)
endpoint = await _select_endpoint(db)
if endpoint is None:
cycle.status = "failed"
cycle.completed_at = datetime.now(UTC)
cycle.phase_memory = {**cycle.phase_memory, "error": "no_endpoint_available"}
await db.commit()
return
prompt = [
{"role": "system", "content": EVALUATION_SYSTEM_PROMPT},
{
"role": "user",
"content": (
f"角色:{character.name}\n道体基线:{character.identity_baseline}\n"
f"内驱力:{character.drives}\n\n近期对话:\n{recent}\n\n"
"请提出演化建议。"
),
},
]
parts: list[str] = []
async for chunk in stream_chat(
endpoint.base_url, endpoint.model_id, endpoint.api_key_env,
AdapterRequest(messages=prompt, model=endpoint.model_id), transport=transport,
):
if chunk.kind == "delta" and chunk.text:
parts.append(chunk.text)
proposal = "".join(parts).strip()
# 阶段四:提交——persona_candidate(pending,等待人工审核)
version = CharacterVersion(
character_id=character.id,
kind="persona_candidate",
status="pending",
payload={"proposal": proposal, "cycle_id": cycle.id, "source": "big_cycle"},
)
db.add(version)
await db.flush()
cycle.produced_version_ids = [version.id]
cycle.status = "completed"
cycle.completed_at = datetime.now(UTC)
db.add(
AuditEvent(
actor_type="system", actor_id="big_cycle", action="big_cycle.completed",
aggregate_type="character", aggregate_id=character.id,
payload={"cycle_id": cycle.id, "version_id": version.id},
)
)
db.add(
OutboxEvent(
aggregate_type="character", aggregate_id=character.id,
event_type="character.persona_candidate_submitted",
payload={"cycle_id": cycle.id, "version_id": version.id},
)
)
await db.commit()
except GatewayError as exc:
cycle.status = "failed"
cycle.completed_at = datetime.now(UTC)
cycle.phase_memory = {**cycle.phase_memory, "error": str(exc)[:300]}
await db.commit()
except Exception:
await db.rollback()
raise