✨ 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 正/缺失/过期、检索隔离与召回证据、删除传播与墓碑原文清除、大周天端到端
This commit is contained in:
1 parent
b4242f3844
commit
e333dc5ea5
8 files changed
+1069
No files matched your search
@@ -0,0 +1,163 @@
|
||||
"""大周天执行器:回顾→对照→提议→提交的最小实现(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
|
||||
Reference in new issue
Block a user