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, MountedTopicWorkspace from ..utils.timezone import now_cn_naive @dataclass class MountedIngressTracking: topic_workspace: MountedTopicWorkspace conversation_run: MountedConversationRun context_layers_hit: dict[str, bool] 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] @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] @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 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]], ) -> 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, ) 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), 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", metadata_json={}, ) self.db.add(topic_row) await self.db.flush() topic_row.channel_ref = self._compact(channel_ref, max_len=128) 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 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), } 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), } 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'}", 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, ) 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 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 = { "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()