M3a:息壤三级记忆域——归真触发提取管线/白名单明忆/重忆确认/正影版本链/删除墓碑/双通道召回注入,退役旧记忆-抽取-反思-大周天-演化管线(迁移 004,验收 7 用例)
This commit is contained in:
1 parent
2a640a901b
commit
9c98c2f85e
69 files changed
+1193
-6033
No files matched your search
@@ -1,602 +0,0 @@
|
||||
"""大周天:长期演化循环(architecture §3.2;v1-scope §3.3)。
|
||||
|
||||
双触发:记忆阈值 + 每轮对话后检查。循环阶段化执行(自我评估 → 演化策略 →
|
||||
候选生成),候选生成调神识模型产出统一 ``evolution_candidates``,按 ``level``
|
||||
半自动应用——人格特质/程序记忆等已成熟下游立即派生,L4 核心变更留人工审核
|
||||
(见 [大周天演化候选落地设计] ../../docs/domain/core-domains.md)。
|
||||
单进程 asyncio,多 worker 需任务队列。
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from dataclasses import asdict
|
||||
from datetime import UTC
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.ai_provider import ProviderManager, ProviderManagerFactory
|
||||
from app.core.config import settings
|
||||
from app.core.errors import AppError
|
||||
from app.core.ids import new_goal_id
|
||||
from app.core.logging import logger
|
||||
from app.core.time import utc_now
|
||||
from app.db.session import AsyncSessionLocal
|
||||
from app.models.big_cycle import BigCycle
|
||||
from app.models.candidates import MemoryCandidate
|
||||
from app.models.drive import CuriosityQuestion, ExploratorySubgoal
|
||||
from app.models.evolution import EvolutionCandidate
|
||||
from app.models.governance import Goal
|
||||
from app.models.memory import Memory
|
||||
from app.models.world import CharacterWorldEntity
|
||||
from app.modules.big_cycle.extract import parse_evolution_candidates
|
||||
from app.modules.big_cycle.prompts import (
|
||||
EvaluationSnapshot,
|
||||
build_evolution_extraction_messages,
|
||||
)
|
||||
from app.modules.drive.extract import parse_curiosities
|
||||
from app.modules.drive.prompts import build_drive_extraction_messages
|
||||
from app.modules.evolution.policies import should_auto_apply
|
||||
from app.modules.extraction.extract import parse_extraction
|
||||
from app.modules.extraction.prompts import build_extraction_messages
|
||||
from app.modules.runtime.events import Event
|
||||
from app.repositories import big_cycle as big_cycle_repo
|
||||
from app.repositories import characters as character_repo
|
||||
from app.repositories import drive as drive_repo
|
||||
from app.repositories import evolution as evolution_repo
|
||||
from app.repositories import memory as memory_repo
|
||||
from app.repositories import runtime as runtime_repo # 新增:活跃 run 检查
|
||||
from app.repositories import world as world_repo
|
||||
from app.repositories.json_utils import dumps
|
||||
from app.services import evolution as evolution_service
|
||||
from app.services.audit import record_audit_event
|
||||
from app.services.event_store import DbEventStore
|
||||
from app.services.personality import build_personality_summary
|
||||
|
||||
_running_big_cycle_tasks: set[asyncio.Task[None]] = set()
|
||||
_RECENT_MEMORY_LIMIT = 10
|
||||
# Token 预算估算(各阶段大致消耗,用于超预算时跳过非关键阶段)
|
||||
_ESTIMATED_EVALUATION_COST = 4000
|
||||
_ESTIMATED_CANDIDATE_COST = 16000
|
||||
_ESTIMATED_MEMORY_COST = 8000
|
||||
|
||||
_provider_manager: ProviderManager | None = None
|
||||
|
||||
|
||||
def _get_provider_manager() -> ProviderManager:
|
||||
global _provider_manager
|
||||
if _provider_manager is None:
|
||||
_provider_manager = ProviderManagerFactory.create()
|
||||
return _provider_manager
|
||||
|
||||
|
||||
def _get_event_store() -> DbEventStore:
|
||||
"""创建临时 EventStore 实例用于推送大周天事件(自管 session 工厂)。"""
|
||||
return DbEventStore(AsyncSessionLocal)
|
||||
|
||||
|
||||
async def _emit_big_cycle_event(
|
||||
event_type: str,
|
||||
character_id: str,
|
||||
cycle_id: str,
|
||||
*,
|
||||
payload: dict | None = None,
|
||||
) -> None:
|
||||
"""推送大周天事件到 EventStore(自管 Session,失败静默)。"""
|
||||
try:
|
||||
store = _get_event_store()
|
||||
await store.append(
|
||||
Event(
|
||||
type=event_type,
|
||||
source="big_cycle",
|
||||
visibility="public",
|
||||
character_id=character_id,
|
||||
payload={"cycle_id": cycle_id, **(payload or {})},
|
||||
)
|
||||
)
|
||||
except Exception: # noqa: BLE001
|
||||
logger.warning(f"推送大周天事件失败: {event_type}", cycle_id=cycle_id)
|
||||
|
||||
|
||||
async def drain_pending_big_cycles() -> None:
|
||||
"""等待所有进行中的大周天循环完成(测试断言 / 优雅停机用)。"""
|
||||
if _running_big_cycle_tasks:
|
||||
await asyncio.gather(*_running_big_cycle_tasks, return_exceptions=True)
|
||||
|
||||
|
||||
async def maybe_trigger_big_cycle(character_id: str, trigger: str = "session_turn") -> str | None:
|
||||
"""检查触发条件,满足则创建 cycle 记录并启动后台循环;返回 cycle_id 或 None。"""
|
||||
if not settings.enable_big_cycle:
|
||||
return None
|
||||
async with AsyncSessionLocal() as session:
|
||||
# 任务中断防护:如果有活跃运行中的 run,跳过大周天避免干扰对话
|
||||
if await runtime_repo.has_active_runs(session, character_id):
|
||||
logger.debug(f"大周天跳过:{character_id} 有活跃 run")
|
||||
return None
|
||||
|
||||
memory_count = await character_repo.count_memories(session, character_id)
|
||||
if memory_count < settings.big_cycle_memory_threshold:
|
||||
return None
|
||||
last = await big_cycle_repo.get_last_completed(session, character_id)
|
||||
if last is not None and last.completed_at is not None:
|
||||
last_completed = last.completed_at
|
||||
if last_completed.tzinfo is None:
|
||||
last_completed = last_completed.replace(tzinfo=UTC)
|
||||
if (
|
||||
utc_now() - last_completed
|
||||
).total_seconds() < settings.big_cycle_min_interval_seconds:
|
||||
return None
|
||||
cycle = BigCycle(
|
||||
character_id=character_id,
|
||||
trigger=trigger,
|
||||
status="pending",
|
||||
memory_count_at_trigger=memory_count,
|
||||
)
|
||||
await big_cycle_repo.add_cycle(session, cycle)
|
||||
await session.commit()
|
||||
cycle_id = cycle.id
|
||||
|
||||
start_big_cycle(cycle_id, character_id)
|
||||
return cycle_id
|
||||
|
||||
|
||||
def start_big_cycle(cycle_id: str, character_id: str) -> None:
|
||||
task = asyncio.create_task(_run_big_cycle(cycle_id, character_id))
|
||||
_running_big_cycle_tasks.add(task)
|
||||
task.add_done_callback(_running_big_cycle_tasks.discard)
|
||||
|
||||
|
||||
async def _phase_goal_progress(session: AsyncSession, cycle: BigCycle) -> None:
|
||||
"""大周天目标进度更新:根据演化结果增加关联目标进度。
|
||||
|
||||
当前为简单版本:每完成一轮大周天,character 的活跃 task/session 目标进度 +0.1。
|
||||
后续可改为 LLM 评估更精确的进度。
|
||||
"""
|
||||
from app.repositories import governance as governance_repo
|
||||
|
||||
active_goals = await governance_repo.list_goals_by_character(
|
||||
session, cycle.character_id, status_filter="active"
|
||||
)
|
||||
for goal in active_goals:
|
||||
if goal.level in ("task", "session") and goal.progress < 1.0:
|
||||
new_progress = min(1.0, goal.progress + 0.1)
|
||||
await governance_repo.update_goal_progress(session, goal.id, new_progress)
|
||||
if new_progress >= 1.0 and goal.level == "task":
|
||||
goal.status = "completed"
|
||||
from app.services.governance import emit_goal_event
|
||||
|
||||
await emit_goal_event(
|
||||
goal,
|
||||
"completed",
|
||||
extra_payload={"trigger": "big_cycle"},
|
||||
)
|
||||
|
||||
|
||||
def _score_autonomy(subgoal) -> float:
|
||||
"""按 information_gain / interruption_cost 比值排序。"""
|
||||
gain = getattr(subgoal, "expected_information_gain", None) or 0.0
|
||||
cost = getattr(subgoal, "interruption_cost", None) or 1.0
|
||||
return gain / max(cost, 0.1)
|
||||
|
||||
|
||||
async def _phase_autonomous_exploration(
|
||||
session: AsyncSession, cycle: BigCycle,
|
||||
) -> None:
|
||||
"""自主探索执行:将最高优先级的已批准次生目标标记为待叩心枢执行。
|
||||
|
||||
选择 expected_information_gain / interruption_cost 比值最高的已批准次生目标,
|
||||
标记为 auto_execute_pending,由叩心枢的下一个 tick 拾取。
|
||||
每次大周天最多标记 2 个。
|
||||
"""
|
||||
from app.repositories import drive as drive_repo
|
||||
|
||||
approved = await drive_repo.list_approved_subgoals(
|
||||
session, cycle.character_id, limit=5,
|
||||
)
|
||||
if not approved:
|
||||
return
|
||||
|
||||
sorted_subgoals = sorted(approved, key=_score_autonomy, reverse=True)
|
||||
to_execute = sorted_subgoals[:2]
|
||||
for sub in to_execute:
|
||||
sub.status = "auto_execute_pending"
|
||||
|
||||
await session.flush()
|
||||
logger.info(
|
||||
f"大周天自主探索标记 cycle={cycle.id} "
|
||||
f"subgoals={len(to_execute)}/{len(approved)}"
|
||||
)
|
||||
|
||||
|
||||
async def _run_big_cycle(cycle_id: str, character_id: str) -> None:
|
||||
"""大周天主循环:阶段化执行,超预算时跳过非关键阶段。"""
|
||||
budget_spent = 0
|
||||
|
||||
await _emit_big_cycle_event("big_cycle.started", character_id, cycle_id)
|
||||
async with AsyncSessionLocal() as session:
|
||||
try:
|
||||
cycle = await big_cycle_repo.get_cycle(session, cycle_id)
|
||||
if cycle is None:
|
||||
return
|
||||
cycle.status = "running"
|
||||
cycle.started_at = utc_now()
|
||||
await session.commit()
|
||||
|
||||
# 阶段 1:自我评估(关键阶段,始终执行)
|
||||
snapshot = await _phase_evaluation(session, cycle)
|
||||
budget_spent += _ESTIMATED_EVALUATION_COST
|
||||
await _emit_big_cycle_event("big_cycle.phase", character_id, cycle_id, payload={"phase": "evaluation", "status": "completed"})
|
||||
|
||||
# 阶段 2:演化策略(轻量,始终执行)
|
||||
await _phase_evolution(session, cycle)
|
||||
|
||||
# 预算检查:超过 token 预算则跳过候选/记忆/探索阶段
|
||||
if budget_spent >= settings.big_cycle_token_budget:
|
||||
logger.warning(f"大周天 token 预算耗尽 cycle={cycle_id} spent={budget_spent}")
|
||||
cycle.summary = "token 预算耗尽,跳过后续非关键阶段"
|
||||
await session.commit()
|
||||
await _emit_big_cycle_event(
|
||||
"big_cycle.phase", character_id, cycle_id,
|
||||
payload={"phase": "candidate", "status": "skipped", "reason": "budget_exhausted"},
|
||||
)
|
||||
await _emit_big_cycle_event(
|
||||
"big_cycle.phase", character_id, cycle_id,
|
||||
payload={"phase": "memory", "status": "skipped", "reason": "budget_exhausted"},
|
||||
)
|
||||
else:
|
||||
# 阶段 3:候选生成
|
||||
await _phase_candidates(session, cycle, snapshot)
|
||||
budget_spent += _ESTIMATED_CANDIDATE_COST
|
||||
await _emit_big_cycle_event("big_cycle.phase", character_id, cycle_id, payload={"phase": "candidate", "status": "completed"})
|
||||
|
||||
# 阶段 4:记忆/世界抽取
|
||||
await _phase_memory(session, cycle, snapshot)
|
||||
budget_spent += _ESTIMATED_MEMORY_COST
|
||||
await _emit_big_cycle_event("big_cycle.phase", character_id, cycle_id, payload={"phase": "memory", "status": "completed"})
|
||||
|
||||
# 阶段 5:目标进度更新
|
||||
await _phase_goal_progress(session, cycle)
|
||||
|
||||
# 阶段 6:自主探索执行(轻量,始终执行)
|
||||
await _phase_autonomous_exploration(session, cycle)
|
||||
|
||||
cycle.status = "completed"
|
||||
cycle.phase = "done"
|
||||
cycle.completed_at = utc_now()
|
||||
await record_audit_event(
|
||||
session,
|
||||
action="big_cycle.completed",
|
||||
character_id=character_id,
|
||||
actor="system",
|
||||
decision="auto",
|
||||
input_summary=(cycle.summary or "大周天循环完成")[:200],
|
||||
resource_type="big_cycle",
|
||||
resource_id=cycle.id,
|
||||
)
|
||||
await session.commit()
|
||||
await _emit_big_cycle_event(
|
||||
"big_cycle.completed", character_id, cycle_id,
|
||||
payload={"summary": cycle.summary},
|
||||
)
|
||||
except Exception as exc:
|
||||
await _fail_cycle(session, cycle_id, exc)
|
||||
await _emit_big_cycle_event(
|
||||
"big_cycle.failed", character_id, cycle_id,
|
||||
payload={"error": str(exc)[:200]},
|
||||
)
|
||||
|
||||
|
||||
async def _phase_evaluation(session: AsyncSession, cycle: BigCycle) -> EvaluationSnapshot:
|
||||
"""自我评估:汇总记忆/人格/世界态势,写入 cycle 快照供候选生成消费。"""
|
||||
cycle.phase = "evaluation"
|
||||
await session.commit()
|
||||
character = await character_repo.get_character(session, cycle.character_id)
|
||||
if character is None:
|
||||
raise AppError("not_found", "人物不存在。")
|
||||
memory_count = await character_repo.count_memories(session, cycle.character_id)
|
||||
recent, _ = await memory_repo.list_memories(
|
||||
session, cycle.character_id, page=1, page_size=_RECENT_MEMORY_LIMIT
|
||||
)
|
||||
personality = await build_personality_summary(session, character)
|
||||
_, world_count = await world_repo.list_entities(
|
||||
session, cycle.character_id, page=1, page_size=1
|
||||
)
|
||||
snapshot = EvaluationSnapshot(
|
||||
character_id=cycle.character_id,
|
||||
memory_count=memory_count,
|
||||
recent_memories=[(m.title, m.content) for m in recent],
|
||||
personality_summary=personality.text or "",
|
||||
world_entity_count=world_count,
|
||||
conclusion=_build_conclusion(memory_count, world_count, personality.text),
|
||||
evidence_ids=[m.id for m in recent],
|
||||
)
|
||||
|
||||
# 人格权重向量:查询最近大周天的累积漂移,注入态势快照供 LLM 感知
|
||||
from app.repositories import evolution as evolution_repo
|
||||
|
||||
recent_candidates, _ = await evolution_repo.list_candidates(
|
||||
session, cycle.character_id, status=None, page=1, page_size=100,
|
||||
)
|
||||
if recent_candidates:
|
||||
recent_drift = sum(c.drift_score or 0.0 for c in recent_candidates)
|
||||
drift_budget_remaining = max(0.0, settings.big_cycle_max_drift - recent_drift)
|
||||
snapshot.drift_budget_remaining = drift_budget_remaining
|
||||
snapshot.cumulative_drift = recent_drift
|
||||
cycle.evaluation_snapshot_json = dumps(asdict(snapshot))
|
||||
await session.commit()
|
||||
logger.info(f"大周天自我评估 cycle={cycle.id} memory={memory_count} world={world_count}")
|
||||
await _generate_drive(session, cycle, snapshot)
|
||||
return snapshot
|
||||
|
||||
|
||||
async def _generate_drive(
|
||||
session: AsyncSession, cycle: BigCycle, snapshot: EvaluationSnapshot
|
||||
) -> None:
|
||||
"""内驱力生成(C 子阶段):基于态势生成好奇问题与探索性次生目标。"""
|
||||
system, user = build_drive_extraction_messages(snapshot)
|
||||
raw = await _call_model(system, user, cycle.character_id)
|
||||
drafts = parse_curiosities(raw)[: settings.big_cycle_max_candidates]
|
||||
curiosities = [
|
||||
CuriosityQuestion(
|
||||
character_id=cycle.character_id,
|
||||
cycle_id=cycle.id,
|
||||
question=draft.question,
|
||||
source=draft.source,
|
||||
related_entities_json=dumps(draft.related_entities),
|
||||
expected_value=draft.expected_value,
|
||||
urgency=draft.urgency,
|
||||
risk_level=draft.risk_level,
|
||||
)
|
||||
for draft in drafts
|
||||
]
|
||||
if curiosities:
|
||||
await drive_repo.add_curiosities(session, curiosities)
|
||||
await session.flush()
|
||||
subgoals: list[ExploratorySubgoal] = []
|
||||
for curiosity, draft in zip(curiosities, drafts, strict=True):
|
||||
for sub in draft.subgoals:
|
||||
subgoals.append(
|
||||
ExploratorySubgoal(
|
||||
character_id=cycle.character_id,
|
||||
cycle_id=cycle.id,
|
||||
curiosity_question_id=curiosity.id,
|
||||
title=sub.title,
|
||||
description=sub.description,
|
||||
expected_information_gain=sub.expected_information_gain,
|
||||
interruption_cost=sub.interruption_cost,
|
||||
risk_level=sub.risk_level,
|
||||
status="candidate",
|
||||
)
|
||||
)
|
||||
if subgoals:
|
||||
await drive_repo.add_subgoals(session, subgoals)
|
||||
await session.flush()
|
||||
# 自动通过低风险次生目标(L0-L2)→ 创建 Goal
|
||||
auto_approve_levels = {"L0", "L1", "L2"}
|
||||
auto_count = 0
|
||||
for sub in subgoals:
|
||||
if sub.risk_level in auto_approve_levels:
|
||||
sub.status = "approved"
|
||||
goal = Goal(
|
||||
id=new_goal_id(),
|
||||
character_id=cycle.character_id,
|
||||
title=sub.title,
|
||||
description=sub.description or "由次生目标自动生成",
|
||||
level="task",
|
||||
type="exploration",
|
||||
status="active",
|
||||
)
|
||||
session.add(goal)
|
||||
auto_count += 1
|
||||
await session.flush()
|
||||
if auto_count:
|
||||
logger.info(f"大周天自动通过 {auto_count}/{len(subgoals)} 个低风险次生目标")
|
||||
logger.info(
|
||||
f"大周天内驱力生成 cycle={cycle.id} curiosities={len(curiosities)} subgoals={len(subgoals)}"
|
||||
)
|
||||
await _emit_big_cycle_event(
|
||||
"big_cycle.drive_generated", cycle.character_id, cycle.id,
|
||||
payload={
|
||||
"curiosity_count": len(curiosities),
|
||||
"subgoal_count": len(subgoals),
|
||||
"auto_approved_subgoals": auto_count,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
async def _phase_evolution(session: AsyncSession, cycle: BigCycle) -> None:
|
||||
"""演化策略阶段(轻量):演化策略融合进候选生成 prompt,此处仅标记阶段。"""
|
||||
cycle.phase = "evolution"
|
||||
await session.commit()
|
||||
logger.info(f"大周天演化策略 cycle={cycle.id}")
|
||||
|
||||
|
||||
async def _phase_candidates(
|
||||
session: AsyncSession, cycle: BigCycle, snapshot: EvaluationSnapshot
|
||||
) -> None:
|
||||
"""候选生成:调神识模型产出演化候选,按 level 半自动应用。"""
|
||||
cycle.phase = "candidate"
|
||||
await session.commit()
|
||||
system, user = build_evolution_extraction_messages(snapshot)
|
||||
raw = await _call_model(system, user, cycle.character_id)
|
||||
drafts = parse_evolution_candidates(raw)[: settings.big_cycle_max_candidates]
|
||||
candidates = [
|
||||
EvolutionCandidate(
|
||||
character_id=cycle.character_id,
|
||||
cycle_id=cycle.id,
|
||||
type=draft.type,
|
||||
source=draft.source,
|
||||
level=draft.level,
|
||||
scope=draft.scope,
|
||||
title=draft.title,
|
||||
proposal=draft.proposal,
|
||||
delta_json=dumps(draft.delta),
|
||||
evidence_ids_json=dumps(_resolve_evidence(snapshot, draft.evidence)),
|
||||
confidence=draft.confidence,
|
||||
benefit_score=draft.benefit_score,
|
||||
risk_score=draft.risk_score,
|
||||
drift_score=draft.drift_score,
|
||||
auto_apply_allowed=draft.auto_apply_allowed,
|
||||
status="pending",
|
||||
)
|
||||
for draft in drafts
|
||||
]
|
||||
await evolution_repo.add_candidates(session, candidates)
|
||||
await session.flush()
|
||||
|
||||
# 反漂移检测:高漂移候选不自动应用 + 累积漂移跟踪
|
||||
cumulative_drift = 0.0
|
||||
drift_blocked = 0
|
||||
for candidate in candidates:
|
||||
if candidate.drift_score >= settings.big_cycle_drift_threshold:
|
||||
# 高漂移候选强制标记为 pending(人工审核),不自动应用
|
||||
candidate.auto_apply_allowed = False
|
||||
candidate.status = "pending"
|
||||
candidate.error_message = f"漂移 {candidate.drift_score} >= 阈值 {settings.big_cycle_drift_threshold},需人工审核"
|
||||
drift_blocked += 1
|
||||
cumulative_drift += candidate.drift_score
|
||||
|
||||
# 累积漂移超过上限 → 全部候选降为 pending
|
||||
if cumulative_drift >= settings.big_cycle_max_drift:
|
||||
logger.warning(f"累积漂移 {cumulative_drift:.2f} >= 上限 {settings.big_cycle_max_drift},全部候选降为人工审核")
|
||||
for candidate in candidates:
|
||||
if candidate.status != "applied":
|
||||
candidate.auto_apply_allowed = False
|
||||
candidate.status = "pending"
|
||||
candidate.error_message = f"累积漂移 {cumulative_drift:.2f} 超限,暂停自动演化"
|
||||
|
||||
for candidate in candidates:
|
||||
if should_auto_apply(candidate):
|
||||
target_id = await evolution_service.apply_evolution_candidate(session, candidate)
|
||||
if target_id is not None:
|
||||
candidate.status = "applied"
|
||||
candidate.resulting_target_id = target_id
|
||||
else:
|
||||
candidate.error_message = "该类型派生待后续阶段,已降级为人工审核"
|
||||
applied = sum(1 for c in candidates if c.status == "applied")
|
||||
pending = sum(1 for c in candidates if c.status == "pending")
|
||||
cycle.summary = (
|
||||
f"大周天生成 {len(candidates)} 个演化候选,"
|
||||
f"{applied} 个已自动应用,{pending} 个待审核"
|
||||
f"{f'({drift_blocked} 个因漂移拦截)' if drift_blocked else ''}"
|
||||
f"{f',累积漂移 {cumulative_drift:.2f}' if cumulative_drift > 0 else ''}"
|
||||
)
|
||||
if drift_blocked:
|
||||
await _emit_big_cycle_event(
|
||||
"big_cycle.drift_blocked", cycle.character_id, cycle.id,
|
||||
payload={
|
||||
"drift_blocked": drift_blocked,
|
||||
"cumulative_drift": round(cumulative_drift, 2),
|
||||
"threshold": settings.big_cycle_drift_threshold,
|
||||
},
|
||||
)
|
||||
await session.commit()
|
||||
logger.info(f"大周天候选生成 cycle={cycle.id} count={len(candidates)} applied={applied}")
|
||||
|
||||
|
||||
async def _phase_memory(
|
||||
session: AsyncSession, cycle: BigCycle, snapshot: EvaluationSnapshot
|
||||
) -> None:
|
||||
"""记忆/世界抽取(D 子阶段):从态势抽取记忆候选与世界实体候选。"""
|
||||
cycle.phase = "memory"
|
||||
await session.commit()
|
||||
system, user = build_extraction_messages(snapshot)
|
||||
raw = await _call_model(system, user, cycle.character_id)
|
||||
memory_drafts, entity_drafts = parse_extraction(raw)
|
||||
memory_drafts = memory_drafts[: settings.big_cycle_max_candidates]
|
||||
entity_drafts = entity_drafts[: settings.big_cycle_max_candidates]
|
||||
memory_cands = [
|
||||
MemoryCandidate(
|
||||
character_id=cycle.character_id,
|
||||
cycle_id=cycle.id,
|
||||
type=draft.type,
|
||||
title=draft.title,
|
||||
content=draft.content,
|
||||
confidence=draft.confidence,
|
||||
importance=draft.importance,
|
||||
sensitivity=draft.sensitivity,
|
||||
reason=draft.reason,
|
||||
status="pending",
|
||||
)
|
||||
for draft in memory_drafts
|
||||
]
|
||||
entities = [
|
||||
CharacterWorldEntity(
|
||||
character_id=cycle.character_id,
|
||||
type=draft.type,
|
||||
name=draft.name,
|
||||
attributes_json=dumps(draft.attributes),
|
||||
state_type=draft.state_type,
|
||||
confidence=draft.confidence,
|
||||
status="pending",
|
||||
)
|
||||
for draft in entity_drafts
|
||||
]
|
||||
if memory_cands:
|
||||
session.add_all(memory_cands)
|
||||
if entities:
|
||||
session.add_all(entities)
|
||||
await session.flush()
|
||||
|
||||
# 自动落地高置信度(≥0.7)低敏感度的记忆候选到 memories 表
|
||||
auto_memories = 0
|
||||
for cand in memory_cands:
|
||||
if cand.confidence >= 0.7 and cand.sensitivity in ("low", "none"):
|
||||
mem = Memory(
|
||||
character_id=cycle.character_id,
|
||||
type=cand.type,
|
||||
title=cand.title,
|
||||
content=cand.content,
|
||||
source="big_cycle",
|
||||
importance=cand.importance,
|
||||
sensitivity=cand.sensitivity,
|
||||
confidence=cand.confidence,
|
||||
)
|
||||
session.add(mem)
|
||||
cand.status = "applied"
|
||||
auto_memories += 1
|
||||
# 自动落地高置信度世界实体候选
|
||||
auto_entities = 0
|
||||
for ent in entities:
|
||||
if ent.confidence >= 0.7:
|
||||
ent.status = "active"
|
||||
auto_entities += 1
|
||||
await session.flush()
|
||||
if auto_memories or auto_entities:
|
||||
logger.info(
|
||||
f"大周天记忆/世界自动落地 cycle={cycle.id} "
|
||||
f"memories={auto_memories} entities={auto_entities}"
|
||||
)
|
||||
logger.info(
|
||||
f"大周天记忆/世界抽取 cycle={cycle.id} "
|
||||
f"memories={len(memory_cands)} entities={len(entities)}"
|
||||
)
|
||||
|
||||
|
||||
def _resolve_evidence(snapshot: EvaluationSnapshot, indices: list[int]) -> list[str]:
|
||||
return [snapshot.evidence_ids[i] for i in indices if 0 <= i < len(snapshot.evidence_ids)]
|
||||
|
||||
|
||||
def _build_conclusion(memory_count: int, world_count: int, personality_text: str | None) -> str:
|
||||
parts = [f"记忆 {memory_count} 条", f"世界实体 {world_count} 个"]
|
||||
parts.append("已具备人格基线" if personality_text else "尚无人格基线")
|
||||
return ";".join(parts)
|
||||
|
||||
|
||||
async def _call_model(system: str, user: str, _character_id: str) -> str:
|
||||
manager = _get_provider_manager()
|
||||
messages = manager.to_message_list([system], [("user", user)])
|
||||
response = await manager.chat(messages, model_hint=settings.big_cycle_model)
|
||||
return response.content
|
||||
|
||||
|
||||
async def _fail_cycle(session: AsyncSession, cycle_id: str, exc: Exception) -> None:
|
||||
logger.error(f"大周天循环失败 cycle={cycle_id}: {exc}")
|
||||
try:
|
||||
cycle = await big_cycle_repo.get_cycle(session, cycle_id)
|
||||
if cycle is not None:
|
||||
cycle.status = "failed"
|
||||
cycle.error_code = "big_cycle_failed"
|
||||
cycle.error_message = str(exc)[:500]
|
||||
cycle.completed_at = utc_now()
|
||||
await session.commit()
|
||||
except Exception: # noqa: BLE001 — 失败处理本身不能再抛
|
||||
pass
|
||||
@@ -1,150 +0,0 @@
|
||||
"""小周天每轮记忆候选 + 返照反思(异步后台任务,跨 modules + ai_provider)。
|
||||
|
||||
言灵表达(流式回复完成)后触发:从本轮对话抽取记忆候选(待审核)+ 生成反思
|
||||
(直接建 memory type=reflection)。失败静默,不影响主流程。仅适用单进程;
|
||||
多 worker 部署需替换为跨进程任务队列。
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
|
||||
from app.ai_provider import ProviderManager, ProviderManagerFactory
|
||||
from app.core.config import settings
|
||||
from app.core.logging import logger
|
||||
from app.db.session import AsyncSessionLocal
|
||||
from app.models.candidates import MemoryCandidate
|
||||
from app.models.memory import Memory
|
||||
from app.modules.reflection.extract import parse_reflection
|
||||
from app.modules.reflection.prompts import build_reflection_messages
|
||||
|
||||
_running_reflection_tasks: set[asyncio.Task[None]] = set()
|
||||
|
||||
_provider_manager: ProviderManager | None = None
|
||||
|
||||
|
||||
def _get_provider_manager() -> ProviderManager:
|
||||
global _provider_manager
|
||||
if _provider_manager is None:
|
||||
_provider_manager = ProviderManagerFactory.create()
|
||||
return _provider_manager
|
||||
|
||||
|
||||
async def drain_pending_reflections() -> None:
|
||||
"""等待所有进行中的反思任务完成(测试断言 / 优雅停机用)。"""
|
||||
if _running_reflection_tasks:
|
||||
await asyncio.gather(*_running_reflection_tasks, return_exceptions=True)
|
||||
|
||||
|
||||
def start_reflection_after_turn(
|
||||
character_id: str,
|
||||
session_id: str,
|
||||
user_content: str,
|
||||
assistant_content: str,
|
||||
) -> None:
|
||||
"""每轮对话后启动反思抽取(后台任务);session_id 留作未来事件回写。"""
|
||||
task = asyncio.create_task(
|
||||
_run_reflection(character_id, session_id, user_content, assistant_content)
|
||||
)
|
||||
_running_reflection_tasks.add(task)
|
||||
task.add_done_callback(_running_reflection_tasks.discard)
|
||||
|
||||
|
||||
async def _run_reflection(
|
||||
character_id: str,
|
||||
session_id: str,
|
||||
user_content: str,
|
||||
assistant_content: str,
|
||||
) -> None:
|
||||
async with AsyncSessionLocal() as session:
|
||||
try:
|
||||
system, user = build_reflection_messages(user_content, assistant_content)
|
||||
raw = await _call_model(system, user, character_id)
|
||||
memory_drafts, reflection_text = parse_reflection(raw)
|
||||
memory_drafts = memory_drafts[: settings.big_cycle_max_candidates]
|
||||
|
||||
# 小周天记忆候选:高置信度(≥0.7)低敏感度直接落地为 Memory,
|
||||
# 其余留 pending 待大周天统一审核
|
||||
auto_memories = 0
|
||||
pending_count = 0
|
||||
for draft in memory_drafts:
|
||||
if draft.confidence >= 0.7 and draft.sensitivity in ("low", "none"):
|
||||
session.add(
|
||||
Memory(
|
||||
character_id=character_id,
|
||||
type=draft.type,
|
||||
title=draft.title,
|
||||
content=draft.content,
|
||||
source="reflection",
|
||||
importance=draft.importance,
|
||||
sensitivity=draft.sensitivity,
|
||||
confidence=draft.confidence,
|
||||
)
|
||||
)
|
||||
auto_memories += 1
|
||||
else:
|
||||
session.add(
|
||||
MemoryCandidate(
|
||||
character_id=character_id,
|
||||
type=draft.type,
|
||||
title=draft.title,
|
||||
content=draft.content,
|
||||
confidence=draft.confidence,
|
||||
importance=draft.importance,
|
||||
sensitivity=draft.sensitivity,
|
||||
reason=draft.reason,
|
||||
status="pending",
|
||||
)
|
||||
)
|
||||
pending_count += 1
|
||||
if reflection_text:
|
||||
title = reflection_text[:60] + ("…" if len(reflection_text) > 60 else "")
|
||||
session.add(
|
||||
Memory(
|
||||
character_id=character_id,
|
||||
type="reflection",
|
||||
title=title,
|
||||
content=reflection_text,
|
||||
source="reflection",
|
||||
importance=0.5,
|
||||
sensitivity="low",
|
||||
)
|
||||
)
|
||||
await session.commit()
|
||||
logger.info(
|
||||
f"小周天反思 character={character_id} session={session_id} "
|
||||
f"auto={auto_memories} pending={pending_count} "
|
||||
f"reflection={'yes' if reflection_text else 'no'}"
|
||||
)
|
||||
|
||||
# 推送反思完成事件(供叩心枢/前端观测)
|
||||
try:
|
||||
from app.modules.runtime.events import Event
|
||||
from app.services.event_store import DbEventStore
|
||||
|
||||
store = DbEventStore(AsyncSessionLocal)
|
||||
await store.append(
|
||||
Event(
|
||||
type="reflection.completed",
|
||||
source="reflection",
|
||||
visibility="public",
|
||||
character_id=character_id,
|
||||
payload={
|
||||
"session_id": session_id,
|
||||
"auto_memories": auto_memories,
|
||||
"pending_count": pending_count,
|
||||
"has_reflection": bool(reflection_text),
|
||||
"reflection_preview": (reflection_text or "")[:100],
|
||||
},
|
||||
)
|
||||
)
|
||||
except Exception: # noqa: BLE001
|
||||
logger.warning("推送 reflection.completed 事件失败")
|
||||
|
||||
except Exception as exc: # noqa: BLE001 — 后台任务失败静默
|
||||
logger.warning(f"小周天反思失败 character={character_id}: {exc}")
|
||||
|
||||
|
||||
async def _call_model(system: str, user: str, _character_id: str) -> str:
|
||||
manager = _get_provider_manager()
|
||||
messages = manager.to_message_list([system], [("user", user)])
|
||||
response = await manager.chat(messages, model_hint=settings.big_cycle_model)
|
||||
return response.content
|
||||
Reference in new issue
Block a user