from __future__ import annotations import hashlib from dataclasses import dataclass from typing import Any, Optional from sqlalchemy import or_, select from sqlalchemy.ext.asyncio import AsyncSession from .ai_pmo.ai_pmo_intent import ROUTE_QUERY_ANSWER, ROUTE_REQUIREMENT_PLAN_COLLECT, ROUTE_REQUIREMENT_PLAN_DRAFT from ..models import FeishuReplyOutbox, MountedAgentTrace, MountedConversationRun, MountedSourceActivation, MountedTopicWorkspace from ..utils.thread_context import build_feishu_conversation_scope_key from ..utils.timezone import now_cn_naive @dataclass class MountedIngressTracking: topic_workspace: MountedTopicWorkspace conversation_run: MountedConversationRun context_layers_hit: dict[str, bool] source_activation: Optional[MountedSourceActivation] = None class MountedAIRuntimeWriteService: def __init__(self, db: AsyncSession): self.db = db @staticmethod def _compact(value: Any, *, max_len: int = 128) -> Optional[str]: text = str(value or "").strip() if not text: return None return text[:max_len] @staticmethod def _merge_json_dict(existing: Any, updates: dict[str, Any]) -> dict[str, Any]: base = dict(existing) if isinstance(existing, dict) else {} for key, value in updates.items(): if value is None: continue base[key] = value return base @classmethod def _reconcile_semantic_packet_summary_requirement_card( cls, execution_outcome: Any, requirement_plan_context: Any, ) -> dict[str, Any]: base = dict(execution_outcome) if isinstance(execution_outcome, dict) else {} context = dict(requirement_plan_context) if isinstance(requirement_plan_context, dict) else {} if not context: return base packet_summary = ( dict(base.get("semantic_packet_summary") or {}) if isinstance(base.get("semantic_packet_summary"), dict) else {} ) draft_summary = ( dict(packet_summary.get("draft") or {}) if isinstance(packet_summary.get("draft"), dict) else {} ) requirement_card = ( dict(draft_summary.get("requirement_card") or {}) if isinstance(draft_summary.get("requirement_card"), dict) else {} ) requirement_card.update(context) draft_summary["requirement_card"] = requirement_card packet_summary["draft"] = draft_summary base["semantic_packet_summary"] = packet_summary return base @classmethod def build_topic_key( cls, *, source_system: str, channel_ref: Optional[str], source_message_id: Optional[str], sender: str, message_text: str, thread_context: Optional[dict[str, Any]], ) -> str: normalized_source = cls._compact(source_system, max_len=32) or "unknown" normalized_channel = cls._compact(channel_ref, max_len=128) or "direct" root_message_id = cls._compact((thread_context or {}).get("root_message_id"), max_len=128) if root_message_id: return f"{normalized_source}:{normalized_channel}:root:{root_message_id}" thread_id = cls._compact((thread_context or {}).get("thread_id"), max_len=128) if thread_id: return f"{normalized_source}:{normalized_channel}:thread:{thread_id}" message_id = cls._compact(source_message_id, max_len=128) if message_id: return f"{normalized_source}:{normalized_channel}:message:{message_id}" digest = hashlib.sha1( f"{normalized_source}|{normalized_channel}|{sender}|{message_text}".encode("utf-8"), usedforsecurity=False, ).hexdigest() return f"{normalized_source}:{normalized_channel}:synthetic:{digest[:20]}" @classmethod def build_run_trace_id( cls, *, source_system: str, source_message_id: Optional[str], topic_key: str, ) -> str: normalized_source = cls._compact(source_system, max_len=32) or "unknown" normalized_message_id = cls._compact(source_message_id, max_len=128) if normalized_message_id: return f"mounted:{normalized_source}:{normalized_message_id}"[:64] digest = hashlib.sha1(topic_key.encode("utf-8"), usedforsecurity=False).hexdigest() return f"mounted:{normalized_source}:{digest[:48]}"[:64] @classmethod def build_conversation_scope_key( cls, *, source_system: str, channel_ref: Optional[str], sender: str, source_sender_open_id: Optional[str] = None, ) -> Optional[str]: normalized_source = cls._compact(source_system, max_len=32) or "unknown" if normalized_source == "feishu": return build_feishu_conversation_scope_key( source_chat_id=channel_ref, source_sender_open_id=source_sender_open_id, sender=sender, ) or None normalized_channel = cls._compact(channel_ref, max_len=128) normalized_sender = cls._compact(source_sender_open_id or sender, max_len=128) if normalized_channel and normalized_sender: return f"{normalized_source}:{normalized_channel}:{normalized_sender}"[:160] if normalized_sender: return f"{normalized_source}:{normalized_sender}"[:160] if normalized_channel: return f"{normalized_source}:{normalized_channel}"[:160] return None @staticmethod def _mode_for_route( *, workflow_route: str, auto_execute: bool, mode_override: Optional[str] = None, ) -> str: if str(mode_override or "").strip(): return str(mode_override).strip() normalized_route = str(workflow_route or "").strip().lower() if auto_execute: return "execute" if normalized_route.startswith("query."): return "chat" if normalized_route.startswith("followup."): return "plan" return "plan" @classmethod def _normalize_runtime_workflow_route( cls, *, workflow_route: Any, topic_row: Optional[MountedTopicWorkspace] = None, route_metadata: Optional[dict[str, Any]] = None, result_payload: Optional[dict[str, Any]] = None, ) -> Optional[str]: normalized_route = str(workflow_route or "").strip().lower() if normalized_route in {ROUTE_REQUIREMENT_PLAN_COLLECT, ROUTE_REQUIREMENT_PLAN_DRAFT}: return normalized_route message_intent = ( str((route_metadata or {}).get("message_intent") or "").strip().lower() if isinstance(route_metadata, dict) else "" ) has_requirement_context = bool( topic_row is not None and cls._topic_has_requirement_plan_context(topic_row) ) has_requirement_snapshot = bool( isinstance((result_payload or {}).get("requirement_plan_context"), dict) ) has_requirement_draft = bool( isinstance((result_payload or {}).get("draft_artifact"), dict) and str(((result_payload or {}).get("draft_artifact") or {}).get("type") or "").strip() == "requirement_plan_draft" ) if has_requirement_draft: return ROUTE_REQUIREMENT_PLAN_DRAFT if normalized_route == ROUTE_QUERY_ANSWER and (message_intent == "requirement_plan" or has_requirement_context or has_requirement_snapshot): return ROUTE_REQUIREMENT_PLAN_COLLECT if not normalized_route and (message_intent == "requirement_plan" or has_requirement_context or has_requirement_snapshot): return ROUTE_REQUIREMENT_PLAN_COLLECT return normalized_route or None @classmethod def _topic_has_requirement_plan_context(cls, topic_row: MountedTopicWorkspace) -> bool: metadata = topic_row.metadata_json if isinstance(getattr(topic_row, "metadata_json", None), dict) else {} if bool(metadata.get("plan_mode")): return True if str(getattr(topic_row, "mode", "") or "").strip().lower() == "plan": return True if str(getattr(topic_row, "plan_status", "") or "").strip().lower() in { "collecting", "options_ready", "plan_ready", "awaiting_execute_confirm", "draft_pending_confirm", }: return True pending_actions = getattr(topic_row, "pending_actions", None) if isinstance(pending_actions, list) and any( str(action or "").strip() in {"continue_requirement_plan", "confirm_requirement_draft", "resume_requirement_plan"} for action in pending_actions ): return True return False @staticmethod def _extract_result_topic_workspace_id(result: Optional[dict[str, Any]]) -> Optional[int]: if not isinstance(result, dict): return None artifact = result.get("draft_artifact") if isinstance(result.get("draft_artifact"), dict) else {} for candidate in ( artifact.get("topic_workspace_id"), result.get("topic_workspace_id"), ): try: if candidate is None or candidate == "": continue return int(candidate) except (TypeError, ValueError): continue return None async def _resolve_source_activation( self, *, tenant_id: int, source_system: str, channel_ref: Optional[str], ) -> tuple[Optional[MountedSourceActivation], bool]: normalized_source = self._compact(source_system, max_len=32) or "unknown" normalized_channel = self._compact(channel_ref, max_len=128) candidates = ( await self.db.execute( select(MountedSourceActivation).where( MountedSourceActivation.tenant_id == int(tenant_id), MountedSourceActivation.source_system == normalized_source, MountedSourceActivation.status == "active", ) ) ).scalars().all() activation_map = { (str(row.scope_type or "").strip().lower(), str(row.scope_ref or "").strip()): row for row in candidates } for key in ( ("chat", normalized_channel or ""), ("source", normalized_source), ("tenant", str(int(tenant_id))), ): match = activation_map.get(key) if match is not None: return match, True return None, bool(candidates) async def resolve_source_activation_scope( self, *, tenant_id: int, source_system: str, channel_ref: Optional[str], thread_context: Optional[dict[str, Any]] = None, ) -> tuple[Optional[MountedSourceActivation], bool]: _ = thread_context return await self._resolve_source_activation( tenant_id=tenant_id, source_system=source_system, channel_ref=channel_ref, ) async def canonicalize_thread_context( self, *, tenant_id: int, source_system: str, channel_ref: Optional[str], thread_context: Optional[dict[str, Any]], ) -> tuple[Optional[dict[str, Any]], dict[str, Optional[str]]]: normalized_source = self._compact(source_system, max_len=32) or "unknown" normalized_thread_context = dict(thread_context) if isinstance(thread_context, dict) else thread_context if normalized_source != "feishu": return normalized_thread_context, {} return await self._canonicalize_feishu_thread_context( tenant_id=tenant_id, channel_ref=channel_ref, thread_context=normalized_thread_context, ) async def _canonicalize_feishu_thread_context( self, *, tenant_id: int, channel_ref: Optional[str], thread_context: Optional[dict[str, Any]], ) -> tuple[Optional[dict[str, Any]], dict[str, Optional[str]]]: if not isinstance(thread_context, dict): return thread_context, {} candidate_sent_ids: list[str] = [] for key in ("root_message_id", "reply_to_message_id"): message_id = self._compact(thread_context.get(key), max_len=128) if message_id and message_id not in candidate_sent_ids: candidate_sent_ids.append(message_id) if not candidate_sent_ids: return dict(thread_context), {} normalized_chat_id = self._compact(channel_ref, max_len=128) stmt = select(FeishuReplyOutbox.sent_message_id, FeishuReplyOutbox.source_message_id).where( FeishuReplyOutbox.tenant_id == int(tenant_id), FeishuReplyOutbox.status == "sent", FeishuReplyOutbox.sent_message_id.in_(candidate_sent_ids), ) if normalized_chat_id: stmt = stmt.where(FeishuReplyOutbox.chat_id == normalized_chat_id) rows = ( await self.db.execute(stmt.order_by(FeishuReplyOutbox.id.desc())) ).all() source_by_sent_id: dict[str, str] = {} for sent_message_id, source_message_id in rows: sent_key = self._compact(sent_message_id, max_len=128) source_key = self._compact(source_message_id, max_len=128) if sent_key and source_key and sent_key not in source_by_sent_id: source_by_sent_id[sent_key] = source_key if not source_by_sent_id: return dict(thread_context), {} canonical_thread_context = dict(thread_context) original_root_message_id = self._compact(thread_context.get("root_message_id"), max_len=128) original_reply_to_message_id = self._compact(thread_context.get("reply_to_message_id"), max_len=128) canonical_root_message_id = source_by_sent_id.get(original_root_message_id or "") canonical_reply_to_message_id = source_by_sent_id.get(original_reply_to_message_id or "") if canonical_root_message_id: canonical_thread_context["root_message_id"] = canonical_root_message_id elif not original_root_message_id and canonical_reply_to_message_id: canonical_thread_context["root_message_id"] = canonical_reply_to_message_id if canonical_reply_to_message_id: canonical_thread_context["reply_to_message_id"] = canonical_reply_to_message_id return canonical_thread_context, { "original_root_message_id": original_root_message_id, "original_reply_to_message_id": original_reply_to_message_id, "canonical_root_message_id": self._compact(canonical_thread_context.get("root_message_id"), max_len=128), "canonical_reply_to_message_id": self._compact(canonical_thread_context.get("reply_to_message_id"), max_len=128), } async def begin_ingress_run( self, *, tenant_id: int, source_system: str, source_message_id: Optional[str], message_text: str, sender: str, channel_ref: Optional[str], thread_context: Optional[dict[str, Any]], source_sender_open_id: Optional[str] = None, ) -> Optional[MountedIngressTracking]: normalized_source_system = self._compact(source_system, max_len=32) or "unknown" effective_thread_context, canonicalization_metadata = await self.canonicalize_thread_context( tenant_id=tenant_id, source_system=normalized_source_system, channel_ref=channel_ref, thread_context=thread_context, ) topic_key = self.build_topic_key( source_system=source_system, channel_ref=channel_ref, source_message_id=source_message_id, sender=sender, message_text=message_text, thread_context=effective_thread_context, ) conversation_scope_key = self.build_conversation_scope_key( source_system=source_system, channel_ref=channel_ref, sender=sender, source_sender_open_id=source_sender_open_id, ) activation, has_activation_rules = await self._resolve_source_activation( tenant_id=tenant_id, source_system=source_system, channel_ref=channel_ref, ) if has_activation_rules and activation is None: return None topic_row = ( await self.db.execute( select(MountedTopicWorkspace).where( MountedTopicWorkspace.tenant_id == int(tenant_id), MountedTopicWorkspace.source_system == normalized_source_system, MountedTopicWorkspace.topic_key == topic_key, ) ) ).scalar_one_or_none() if topic_row is None: topic_row = await self._resolve_existing_topic_for_reply_anchor( tenant_id=tenant_id, source_system=normalized_source_system, channel_ref=channel_ref, conversation_scope_key=conversation_scope_key, thread_context=effective_thread_context, ) topic_exists = topic_row is not None now_at = now_cn_naive() if topic_row is None: topic_row = MountedTopicWorkspace( tenant_id=int(tenant_id), topic_key=topic_key, source_system=normalized_source_system, channel_ref=self._compact(channel_ref, max_len=128), conversation_scope_key=self._compact(conversation_scope_key, max_len=160), thread_key=self._compact((effective_thread_context or {}).get("thread_id"), max_len=255), root_message_id=self._compact((effective_thread_context or {}).get("root_message_id"), max_len=128), mode="chat", status="active", plan_status="idle", metadata_json={}, ) self.db.add(topic_row) await self.db.flush() if activation is not None: activation.last_seen_at = now_at topic_row.channel_ref = self._compact(channel_ref, max_len=128) topic_row.conversation_scope_key = self._compact(conversation_scope_key, max_len=160) topic_row.thread_key = self._compact((effective_thread_context or {}).get("thread_id"), max_len=255) topic_row.root_message_id = self._compact((effective_thread_context or {}).get("root_message_id"), max_len=128) topic_row.last_message_id = self._compact(source_message_id, max_len=128) topic_row.last_triggered_at = now_at if not str(topic_row.plan_status or "").strip(): topic_row.plan_status = "idle" topic_row.metadata_json = { **(topic_row.metadata_json if isinstance(topic_row.metadata_json, dict) else {}), "last_sender": self._compact(sender, max_len=128), "last_message_preview": self._compact(message_text, max_len=200), "last_reply_to_message_id": self._compact((effective_thread_context or {}).get("reply_to_message_id"), max_len=128), "thread_context_canonicalization": canonicalization_metadata or None, "source_activation": ( { "scope_type": activation.scope_type, "scope_ref": activation.scope_ref, "cutover_mode": activation.cutover_mode, "auto_reply_enabled": bool(activation.auto_reply_enabled), } if activation is not None else None ), } context_layers_hit = { "current_message": True, "thread": bool((effective_thread_context or {}).get("root_message_id") or (effective_thread_context or {}).get("thread_id")), "topic": bool(topic_exists), "source_activation": bool(activation is not None), } trace_id = self.build_run_trace_id( source_system=source_system, source_message_id=source_message_id, topic_key=topic_key, ) run_row = ( await self.db.execute( select(MountedConversationRun).where( MountedConversationRun.tenant_id == int(tenant_id), MountedConversationRun.trace_id == trace_id, ) ) ).scalar_one_or_none() if run_row is None: run_row = MountedConversationRun( tenant_id=int(tenant_id), trace_id=trace_id, topic_workspace_id=int(topic_row.id), source_system=self._compact(source_system, max_len=32) or "unknown", source_message_id=self._compact(source_message_id, max_len=128), mode_before=self._compact(topic_row.mode, max_len=24) if topic_exists else None, active_agent="ConversationOrchestrator", status="running", trigger_reason=( f"ingress:{self._compact(source_system, max_len=32) or 'unknown'}:{activation.cutover_mode}" if activation is not None else f"ingress:{self._compact(source_system, max_len=32) or 'unknown'}" ), context_layers_hit=context_layers_hit, started_at=now_at, ) self.db.add(run_row) await self.db.flush() else: run_row.topic_workspace_id = int(topic_row.id) run_row.status = "running" run_row.active_agent = "ConversationOrchestrator" run_row.context_layers_hit = context_layers_hit if run_row.started_at is None: run_row.started_at = now_at return MountedIngressTracking( topic_workspace=topic_row, conversation_run=run_row, context_layers_hit=context_layers_hit, source_activation=activation, ) async def _resolve_existing_topic_for_reply_anchor( self, *, tenant_id: int, source_system: str, channel_ref: Optional[str], conversation_scope_key: Optional[str], thread_context: Optional[dict[str, Any]], ) -> Optional[MountedTopicWorkspace]: if str(source_system or "").strip().lower() != "feishu": return None anchor_ids: list[str] = [] for key in ("root_message_id", "reply_to_message_id"): candidate = self._compact((thread_context or {}).get(key), max_len=128) if candidate and candidate not in anchor_ids: anchor_ids.append(candidate) if not anchor_ids: return None normalized_channel = self._compact(channel_ref, max_len=128) normalized_scope = self._compact(conversation_scope_key, max_len=160) topic_key_candidates: list[str] = [] for anchor_id in anchor_ids: if normalized_channel: topic_key_candidates.append(f"feishu:{normalized_channel}:message:{anchor_id}") topic_key_candidates.append(f"feishu:{normalized_channel}:root:{anchor_id}") stmt = select(MountedTopicWorkspace).where( MountedTopicWorkspace.tenant_id == int(tenant_id), MountedTopicWorkspace.source_system == "feishu", or_( MountedTopicWorkspace.root_message_id.in_(anchor_ids), MountedTopicWorkspace.last_message_id.in_(anchor_ids), MountedTopicWorkspace.topic_key.in_(topic_key_candidates), ), ) if normalized_channel: stmt = stmt.where(MountedTopicWorkspace.channel_ref == normalized_channel) rows = ( await self.db.execute( stmt.order_by(MountedTopicWorkspace.updated_at.desc(), MountedTopicWorkspace.id.desc()) ) ).scalars().all() if not rows: return None scored_rows: list[tuple[int, MountedTopicWorkspace]] = [] for row in rows: score = 0 row_topic_key = str(getattr(row, "topic_key", "") or "").strip() row_root_message_id = str(getattr(row, "root_message_id", "") or "").strip() row_last_message_id = str(getattr(row, "last_message_id", "") or "").strip() row_scope = str(getattr(row, "conversation_scope_key", "") or "").strip() if row_topic_key in topic_key_candidates: score += 120 if row_last_message_id in anchor_ids: score += 90 if row_root_message_id in anchor_ids: score += 80 if normalized_scope and row_scope == normalized_scope: score += 15 scored_rows.append((score, row)) scored_rows.sort(key=lambda item: (item[0], int(getattr(item[1], "id", 0) or 0)), reverse=True) best_score, best_row = scored_rows[0] return best_row if best_score > 0 else None async def update_topic_plan_state( self, *, tracking: MountedIngressTracking, plan_status: str, topic_status: Optional[str] = None, pending_actions: Optional[list[str]] = None, metadata: Optional[dict[str, Any]] = None, ) -> None: topic_row = tracking.topic_workspace topic_row.plan_status = self._compact(plan_status, max_len=32) or str(topic_row.plan_status or "idle") if str(topic_status or "").strip(): topic_row.status = self._compact(topic_status, max_len=24) or topic_row.status if pending_actions is not None: topic_row.pending_actions = [str(item).strip() for item in pending_actions if str(item or "").strip()] if isinstance(metadata, dict) and metadata: topic_row.metadata_json = { **(topic_row.metadata_json if isinstance(topic_row.metadata_json, dict) else {}), **metadata, } await self.db.flush() async def record_orchestrator_decision( self, *, tracking: MountedIngressTracking, workflow_route: str, intent_name: str, intent_confidence: float, trigger_reason: str, auto_execute: bool, work_item_id: Optional[int], route_metadata: Optional[dict[str, Any]], mode_override: Optional[str] = None, ) -> str: normalized_workflow_route = self._normalize_runtime_workflow_route( workflow_route=workflow_route, topic_row=tracking.topic_workspace, route_metadata=route_metadata, ) or str(workflow_route or "").strip() mode_after = self._mode_for_route( workflow_route=normalized_workflow_route, auto_execute=auto_execute, mode_override=mode_override, ) topic_row = tracking.topic_workspace run_row = tracking.conversation_run if mode_after == "chat" and self._topic_has_requirement_plan_context(topic_row): mode_after = "plan" topic_row.mode = mode_after topic_row.metadata_json = { **(topic_row.metadata_json if isinstance(topic_row.metadata_json, dict) else {}), "last_workflow_route": normalized_workflow_route or None, "last_intent_name": str(intent_name or "").strip() or None, "last_intent_confidence": float(intent_confidence or 0.0), "last_work_item_id": int(work_item_id) if work_item_id is not None else None, "plan_mode": True if mode_after == "plan" and self._topic_has_requirement_plan_context(topic_row) else ( topic_row.metadata_json.get("plan_mode") if isinstance(topic_row.metadata_json, dict) else None ), } run_row.mode_after = mode_after run_row.active_agent = "ConversationOrchestrator" run_row.trigger_reason = self._compact(trigger_reason, max_len=128) self.db.add( MountedAgentTrace( tenant_id=int(topic_row.tenant_id), conversation_run_id=int(run_row.id), agent_name="ConversationOrchestrator", status="completed", input_summary={ "mode_before": run_row.mode_before, "context_layers_hit": tracking.context_layers_hit, }, output_summary={ "mode_after": mode_after, "workflow_route": normalized_workflow_route or None, "intent_name": str(intent_name or "").strip() or None, "auto_execute": bool(auto_execute), "route_metadata": dict(route_metadata or {}), }, ) ) await self.db.flush() return mode_after async def record_agent_trace( self, *, tracking: MountedIngressTracking, agent_name: str, status: str = "completed", latency_ms: Optional[int] = None, input_summary: Optional[Any] = None, output_summary: Optional[Any] = None, tool_trace: Optional[Any] = None, failure_reason: Optional[str] = None, ) -> None: topic_row = tracking.topic_workspace run_row = tracking.conversation_run normalized_agent = self._compact(agent_name, max_len=64) or "SpecialistAgent" normalized_status = self._compact(status, max_len=24) or "completed" if normalized_status != "failed": run_row.active_agent = normalized_agent self.db.add( MountedAgentTrace( tenant_id=int(topic_row.tenant_id), conversation_run_id=int(run_row.id), agent_name=normalized_agent, status=normalized_status, latency_ms=int(latency_ms) if latency_ms is not None else None, input_summary=input_summary if isinstance(input_summary, (dict, list)) else None, output_summary=output_summary if isinstance(output_summary, (dict, list)) else None, tool_trace=tool_trace if isinstance(tool_trace, (dict, list)) else None, failure_reason=self._compact(failure_reason, max_len=500), ) ) await self.db.flush() async def record_semantic_shadow( self, *, tracking: MountedIngressTracking, source_message_preview: Optional[str], semantic_packet_summary: Optional[dict[str, Any]], semantic_decision: Optional[dict[str, Any]], semantic_gate_result: Optional[dict[str, Any]], semantic_diff: Optional[dict[str, Any]], status: str = "completed", failure_reason: Optional[str] = None, ) -> None: topic_row = tracking.topic_workspace run_row = tracking.conversation_run normalized_status = self._compact(status, max_len=24) or "completed" run_row.execution_outcome = self._merge_json_dict( run_row.execution_outcome, { "source_message_preview": self._compact(source_message_preview, max_len=280), "semantic_packet_summary": semantic_packet_summary if isinstance(semantic_packet_summary, dict) else None, "semantic_decision": semantic_decision if isinstance(semantic_decision, dict) else None, "semantic_gate_result": semantic_gate_result if isinstance(semantic_gate_result, dict) else None, "semantic_diff": semantic_diff if isinstance(semantic_diff, dict) else None, }, ) self.db.add( MountedAgentTrace( tenant_id=int(topic_row.tenant_id), conversation_run_id=int(run_row.id), agent_name="SemanticShadowPlanner", status=normalized_status, input_summary={ "topic_workspace_id": int(topic_row.id), "mode_before": run_row.mode_before, }, output_summary={ "semantic_packet_summary": semantic_packet_summary if isinstance(semantic_packet_summary, dict) else None, "semantic_decision": semantic_decision if isinstance(semantic_decision, dict) else None, "semantic_gate_result": semantic_gate_result if isinstance(semantic_gate_result, dict) else None, "semantic_diff": semantic_diff if isinstance(semantic_diff, dict) else None, }, tool_trace=[ { "tool_name": "semantic.shadow", "status": "completed", } ], failure_reason=self._compact(failure_reason, max_len=500), ) ) await self.db.flush() async def finalize_run( self, *, tracking: MountedIngressTracking, result: Optional[dict[str, Any]] = None, error_text: Optional[str] = None, ) -> None: topic_row = tracking.topic_workspace run_row = tracking.conversation_run now_at = now_cn_naive() normalized_error = str(error_text or "").strip() final_reply = str((result or {}).get("direct_reply_text") or "").strip() if isinstance(result, dict) else "" if normalized_error: run_row.status = "failed" run_row.active_agent = "ConversationOrchestrator" run_row.error_text = normalized_error else: persisted_result_fields: dict[str, Any] = { "handled": bool((result or {}).get("handled")) if isinstance(result, dict) else False, "status": str((result or {}).get("status") or "").strip() if isinstance(result, dict) else "", "classification": str((result or {}).get("classification") or "").strip() if isinstance(result, dict) else "", "workflow_route": str((result or {}).get("workflow_route") or "").strip() if isinstance(result, dict) else "", "work_item_id": (result or {}).get("work_item_id") if isinstance(result, dict) else None, "direct_reply_text": final_reply or None, } if isinstance(result, dict): for field in ( "answer", "rag_result", "reply_template_key", "draft_version", "draft_artifact", "requirement_plan_context", "semantic_bundle", "semantic_bundle_id", "semantic_version", "semantic_source", ): value = result.get(field) if value is None: continue if isinstance(value, str) and not value.strip(): continue persisted_result_fields[field] = value normalized_workflow_route = self._normalize_runtime_workflow_route( workflow_route=persisted_result_fields.get("workflow_route"), topic_row=topic_row, result_payload=result, ) if normalized_workflow_route: persisted_result_fields["workflow_route"] = normalized_workflow_route run_row.status = "completed" run_row.active_agent = "ReplyComposer" run_row.final_reply = final_reply or None run_row.execution_outcome = self._merge_json_dict( run_row.execution_outcome, persisted_result_fields, ) run_row.execution_outcome = self._reconcile_semantic_packet_summary_requirement_card( run_row.execution_outcome, persisted_result_fields.get("requirement_plan_context"), ) canonical_topic_workspace_id = self._extract_result_topic_workspace_id(result) if canonical_topic_workspace_id is not None and canonical_topic_workspace_id != int(topic_row.id): canonical_topic = ( await self.db.execute( select(MountedTopicWorkspace).where( MountedTopicWorkspace.tenant_id == int(topic_row.tenant_id), MountedTopicWorkspace.id == int(canonical_topic_workspace_id), ) ) ).scalar_one_or_none() if canonical_topic is not None: topic_row = canonical_topic tracking.topic_workspace = canonical_topic run_row.topic_workspace_id = int(canonical_topic.id) if normalized_workflow_route: topic_row.metadata_json = { **(topic_row.metadata_json if isinstance(topic_row.metadata_json, dict) else {}), "last_workflow_route": normalized_workflow_route, "plan_mode": ( True if normalized_workflow_route.startswith("requirement_plan.") else ( topic_row.metadata_json.get("plan_mode") if isinstance(topic_row.metadata_json, dict) else None ) ), } if normalized_workflow_route.startswith("requirement_plan.") and str(topic_row.mode or "").strip().lower() == "chat": topic_row.mode = "plan" run_row.completed_at = now_at self.db.add( MountedAgentTrace( tenant_id=int(topic_row.tenant_id), conversation_run_id=int(run_row.id), agent_name="ReplyComposer" if not normalized_error else "ConversationOrchestrator", status="failed" if normalized_error else "completed", output_summary=( {"final_reply": final_reply or None, "status": str((result or {}).get("status") or "").strip() or None} if not normalized_error else None ), failure_reason=normalized_error or None, ) ) await self.db.flush()