"""小周天每轮记忆候选 + 返照反思(异步后台任务,跨 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