from __future__ import annotations import hashlib from dataclasses import dataclass from typing import Any, Optional from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from ..models import 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 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" 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 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]: 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=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 == (self._compact(source_system, max_len=32) or "unknown"), MountedTopicWorkspace.topic_key == topic_key, ) ) ).scalar_one_or_none() 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=self._compact(source_system, max_len=32) or "unknown", channel_ref=self._compact(channel_ref, max_len=128), conversation_scope_key=self._compact(conversation_scope_key, max_len=160), thread_key=self._compact((thread_context or {}).get("thread_id"), max_len=255), root_message_id=self._compact((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((thread_context or {}).get("thread_id"), max_len=255) topic_row.root_message_id = self._compact((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((thread_context or {}).get("reply_to_message_id"), max_len=128), "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((thread_context or {}).get("root_message_id") or (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 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: mode_after = self._mode_for_route( workflow_route=workflow_route, auto_execute=auto_execute, mode_override=mode_override, ) topic_row = tracking.topic_workspace run_row = tracking.conversation_run topic_row.mode = mode_after topic_row.metadata_json = { **(topic_row.metadata_json if isinstance(topic_row.metadata_json, dict) else {}), "last_workflow_route": str(workflow_route or "").strip() 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, } 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": str(workflow_route or "").strip() 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, }, ) next_plan_status = None if isinstance(semantic_gate_result, dict): next_plan_status = self._compact(semantic_gate_result.get("next_plan_status"), max_len=32) if next_plan_status: topic_row.plan_status = next_plan_status 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: 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, { "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, }, ) 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()