603 lines
25 KiB
Python
603 lines
25 KiB
Python
"""大周天:长期演化循环(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
|