864 lines
38 KiB
Python
864 lines
38 KiB
Python
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()
|