M3a:息壤三级记忆域——归真触发提取管线/白名单明忆/重忆确认/正影版本链/删除墓碑/双通道召回注入,退役旧记忆-抽取-反思-大周天-演化管线(迁移 004,验收 7 用例)

This commit is contained in:
chenxin committed 2026-08-27 09:16:32 +08:00
1 parent 2a640a901b
commit 9c98c2f85e
69 files changed
+1193 -6033

No files matched your search

-602
View File
@@ -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
-150
View File
@@ -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