from __future__ import annotations import json from pathlib import Path from typing import Any, Optional from sqlalchemy import case, func, select from sqlalchemy.ext.asyncio import AsyncSession from ..models import ( JudgeRun, MountedAgentTrace, MountedConversationRun, MountedDraftWorkspace, MountedSourceActivation, MountedToolCapabilityPolicy, MountedTopicWorkspace, QualityAsset, QualityAssetPair, TrainingDataset, TrainingEvaluation, TrainingJob, TrainedModelDeployment, ) from ..services.training_pipeline import TrainingPipelineService from ..utils.timezone import now_cn_naive class MountedAIRuntimeService: def __init__(self, db: AsyncSession): self.db = db @staticmethod def _dt(value: Any) -> Optional[str]: return value.isoformat() if value is not None else None @staticmethod def _backend_root() -> Path: return Path(__file__).resolve().parents[2] @classmethod def _read_backend_source(cls, relative_path: str) -> str: try: return (cls._backend_root() / relative_path).read_text(encoding="utf-8") except OSError: return "" @staticmethod def _legacy_cutover_item( *, key: str, title: str, retired: bool, summary: str, evidence: str, ) -> dict[str, Any]: return { "key": key, "title": title, "status": "retired" if retired else "active", "summary": summary, "evidence": evidence, } @classmethod def _build_legacy_cutover_items(cls) -> list[dict[str, Any]]: ingress_render = cls._read_backend_source("app/services/ai_pmo/ai_pmo_kernel/mixins/ingress/ai_pmo_kernel_mixins_ingress_render.py") feishu_reply = cls._read_backend_source("app/services/integrations/feishu_integration_service/mixin_reply_llm_core.py") signal_capture_service = cls._read_backend_source("app/services/signal/signal_capture_service/service.py") feishu_pipeline_service = cls._read_backend_source("app/services/integrations/feishu_signal_pipeline/service.py") feishu_process_core = cls._read_backend_source("app/services/integrations/feishu_signal_pipeline/mixin_process_message_core.py") feishu_followup_a = cls._read_backend_source("app/services/integrations/feishu_signal_pipeline/mixin_followup_a.py") return [ cls._legacy_cutover_item( key="ai_pmo_ingress_consultant_capture", title="AI PMO ingress consultant capture", retired="build_consultant_dispatch_contract" not in ingress_render and '"consultant_dispatch"' not in ingress_render, summary="capture result 不再生成或透传 consultant_dispatch 到 AI PMO ingress 主链。", evidence="ai_pmo_kernel_mixins_ingress_render.py 不再包含 consultant dispatch contract/import 与 rag_result 注入。", ), cls._legacy_cutover_item( key="feishu_mention_consultant_reply", title="Feishu mention consultant reply", retired="build_channel_consultant_reply" not in feishu_reply and "_build_consultant_dispatch_reply(capture_result)" not in feishu_reply, summary="飞书 mention 主回复不再走 consultant reply 分支。", evidence="mixin_reply_llm_core.py 已移除 consultant reply import 和 active reply branch;helper 仅保留 no-op。", ), cls._legacy_cutover_item( key="signal_capture_legacy_entry", title="Signal capture legacy entry", retired="SignalCaptureLegacyMixin" not in signal_capture_service, summary="主动信号捕获服务不再继承 legacy capture mixin。", evidence="signal_capture_service/service.py 的 ProactiveSignalCaptureService 继承链已去掉 SignalCaptureLegacyMixin。", ), cls._legacy_cutover_item( key="legacy_feishu_queue_followup", title="Legacy Feishu queue follow-up", retired=( "FeishuPipelineFollowupAMixin" not in feishu_pipeline_service and "FeishuPipelineFollowupBMixin" not in feishu_pipeline_service and "_maybe_handle_queue_followup(" not in feishu_process_core and "return None" in feishu_followup_a and "Legacy feishu_items queue follow-up is retired." in feishu_followup_a ), summary="旧 feishu_items 队列 continue/status follow-up 已从真实入口退役。", evidence="service.py 不再混入 FollowupA/B,process_message_core 不再调用 queue followup,mixin_followup_a 已降为 retired stub。", ), ] def _topic_snapshot( self, row: MountedTopicWorkspace, *, draft: Optional[MountedDraftWorkspace] = None, ) -> dict[str, Any]: return { "id": int(row.id), "topic_key": row.topic_key, "source_system": row.source_system, "channel_ref": row.channel_ref, "thread_key": row.thread_key, "mode": row.mode, "status": row.status, "plan_status": getattr(row, "plan_status", None), "readiness_score": float(row.readiness_score or 0.0) if row.readiness_score is not None else None, "draft_state": draft.state if draft is not None else None, "draft_version": int(draft.draft_version) if draft is not None else None, "updated_at": self._dt(row.updated_at), } def _draft_snapshot(self, row: MountedDraftWorkspace) -> dict[str, Any]: return { "id": int(row.id), "topic_workspace_id": int(row.topic_workspace_id), "state": row.state, "draft_version": int(row.draft_version or 0), "requirement_card": row.requirement_card, "draft_payload": row.draft_payload, "required_confirmations": row.required_confirmations, "last_ready_at": self._dt(row.last_ready_at), "confirm_message_id": row.confirm_message_id, } @staticmethod def _source_activation_snapshot(row: MountedSourceActivation) -> dict[str, Any]: return { "id": int(row.id), "source_system": row.source_system, "scope_type": row.scope_type, "scope_ref": row.scope_ref, "status": row.status, "cutover_mode": row.cutover_mode, "auto_reply_enabled": bool(row.auto_reply_enabled), "last_seen_at": MountedAIRuntimeService._dt(row.last_seen_at), "notes": row.notes, "metadata_json": row.metadata_json, } @staticmethod def _tool_policy_snapshot(row: MountedToolCapabilityPolicy) -> dict[str, Any]: return { "id": int(row.id), "tool_name": row.tool_name, "risk_level": row.risk_level, "confirm_required": bool(row.confirm_required), "auto_execute_allowed": bool(row.auto_execute_allowed), "is_active": bool(row.is_active), "source_scope": row.source_scope, "tenant_scope": row.tenant_scope, "policy_metadata": row.policy_metadata, "notes": row.notes, } def _run_snapshot(self, row: MountedConversationRun, *, trace_count: int = 0) -> dict[str, Any]: return { "id": int(row.id), "trace_id": row.trace_id, "source_system": row.source_system, "source_message_id": row.source_message_id, "mode_before": row.mode_before, "mode_after": row.mode_after, "active_agent": row.active_agent, "status": row.status, "trigger_reason": row.trigger_reason, "final_reply": row.final_reply, "trace_count": int(trace_count), "updated_at": self._dt(row.updated_at), } @staticmethod def _trace_snapshot(row: MountedAgentTrace) -> dict[str, Any]: return { "id": int(row.id), "agent_name": row.agent_name, "status": row.status, "latency_ms": int(row.latency_ms) if row.latency_ms is not None else None, "failure_reason": row.failure_reason, "input_summary": row.input_summary, "output_summary": row.output_summary, "tool_trace": row.tool_trace, "created_at": row.created_at.isoformat() if row.created_at else None, } @staticmethod def _evaluation_snapshot(row: TrainingEvaluation) -> dict[str, Any]: return { "evaluation_type": row.evaluation_type, "status": row.status, "score": float(row.score or 0.0) if row.score is not None else None, } @staticmethod def _eval_suite_snapshot( row: TrainingDataset, *, latest_score: Optional[float], last_evaluated_at: Optional[Any], ) -> dict[str, Any]: selection_rules = row.selection_rules if isinstance(row.selection_rules, dict) else {} suite_kind = str(selection_rules.get("suite_kind") or "custom").strip() or "custom" return { "id": int(row.id), "dataset_type": row.dataset_type, "version": row.version, "status": row.status, "suite_kind": suite_kind, "sample_count": int(row.sample_count or 0), "latest_score": latest_score, "last_evaluated_at": MountedAIRuntimeService._dt(last_evaluated_at), } @staticmethod def _quality_asset_snapshot(row: QualityAsset) -> dict[str, Any]: return { "id": int(row.id), "asset_type": row.asset_type, "status": row.status, "prompt_key": row.prompt_key, "workflow_type": row.workflow_type, "asked_via": row.asked_via, "question_text": row.question_text, "answer_text": row.answer_text, "corrected_answer": row.corrected_answer, "notes": row.notes, "created_at": MountedAIRuntimeService._dt(row.created_at), } @staticmethod def _judge_run_snapshot(row: JudgeRun, *, asset: Optional[QualityAsset] = None) -> dict[str, Any]: return { "judge_run_id": int(row.id), "asset_id": int(row.asset_id) if row.asset_id is not None else None, "bucket_key": row.bucket_key, "judge_status": row.judge_status, "overall_score": float(row.overall_score or 0.0) if row.overall_score is not None else None, "prompt_key": row.prompt_key, "workflow_type": row.workflow_type, "asked_via": row.asked_via, "question_text": asset.question_text if asset is not None else None, "answer_text": asset.answer_text if asset is not None else None, "corrected_answer": asset.corrected_answer if asset is not None else None, "findings": row.findings, "created_at": MountedAIRuntimeService._dt(row.created_at), } async def list_sources(self, *, tenant_id: int) -> dict[str, Any]: source_map: dict[str, dict[str, Any]] = {} run_rows = ( await self.db.execute( select( MountedConversationRun.source_system, func.count(MountedConversationRun.id).label("run_count"), func.max(MountedConversationRun.updated_at).label("last_seen_at"), ) .where(MountedConversationRun.tenant_id == int(tenant_id)) .group_by(MountedConversationRun.source_system) ) ).all() for row in run_rows: source_system = str(row.source_system or "").strip() source_map[source_system] = { "source_system": source_system, "run_count": int(row.run_count or 0), "topic_count": 0, "active_topic_count": 0, "last_seen_at": self._dt(row.last_seen_at), } topic_rows = ( await self.db.execute( select( MountedTopicWorkspace.source_system, func.count(MountedTopicWorkspace.id).label("topic_count"), func.sum( case((MountedTopicWorkspace.status == "active", 1), else_=0) ).label("active_topic_count"), func.max(MountedTopicWorkspace.updated_at).label("topic_last_seen_at"), ) .where(MountedTopicWorkspace.tenant_id == int(tenant_id)) .group_by(MountedTopicWorkspace.source_system) ) ).all() for row in topic_rows: source_system = str(row.source_system or "").strip() payload = source_map.setdefault( source_system, { "source_system": source_system, "run_count": 0, "topic_count": 0, "active_topic_count": 0, "last_seen_at": None, }, ) payload["topic_count"] = int(row.topic_count or 0) payload["active_topic_count"] = int(row.active_topic_count or 0) topic_last_seen_at = self._dt(row.topic_last_seen_at) if topic_last_seen_at and not payload.get("last_seen_at"): payload["last_seen_at"] = topic_last_seen_at items = sorted(source_map.values(), key=lambda item: (item["source_system"] or "")) return {"items": items} async def list_source_activations(self, *, tenant_id: int) -> dict[str, Any]: rows = ( await self.db.execute( select(MountedSourceActivation) .where(MountedSourceActivation.tenant_id == int(tenant_id)) .order_by( MountedSourceActivation.source_system.asc(), MountedSourceActivation.scope_type.asc(), MountedSourceActivation.scope_ref.asc(), ) ) ).scalars().all() return {"items": [self._source_activation_snapshot(row) for row in rows]} async def upsert_source_activation( self, *, tenant_id: int, source_system: str, scope_type: str, scope_ref: str, status: str, cutover_mode: str, auto_reply_enabled: bool, notes: Optional[str], metadata_json: Optional[Any], ) -> dict[str, Any]: normalized_source = str(source_system or "").strip().lower() normalized_scope_type = str(scope_type or "").strip().lower() normalized_scope_ref = str(scope_ref or "").strip() normalized_status = str(status or "").strip().lower() normalized_cutover_mode = str(cutover_mode or "").strip().lower() if normalized_status not in {"active", "paused"}: raise ValueError("unsupported source activation status") if normalized_cutover_mode not in {"shadow", "mounted_only", "legacy_only"}: raise ValueError("unsupported cutover mode") row = ( await self.db.execute( select(MountedSourceActivation).where( MountedSourceActivation.tenant_id == int(tenant_id), MountedSourceActivation.source_system == normalized_source, MountedSourceActivation.scope_type == normalized_scope_type, MountedSourceActivation.scope_ref == normalized_scope_ref, ) ) ).scalar_one_or_none() if row is None: row = MountedSourceActivation( tenant_id=int(tenant_id), source_system=normalized_source, scope_type=normalized_scope_type, scope_ref=normalized_scope_ref, ) self.db.add(row) row.status = normalized_status row.cutover_mode = normalized_cutover_mode row.auto_reply_enabled = bool(auto_reply_enabled) row.notes = str(notes or "").strip() or None row.metadata_json = metadata_json if isinstance(metadata_json, (dict, list)) else None await self.db.commit() await self.db.refresh(row) return {"item": self._source_activation_snapshot(row)} async def list_tool_policies(self, *, tenant_id: int) -> dict[str, Any]: rows = ( await self.db.execute( select(MountedToolCapabilityPolicy) .where(MountedToolCapabilityPolicy.tenant_id == int(tenant_id)) .order_by(MountedToolCapabilityPolicy.tool_name.asc()) ) ).scalars().all() return {"items": [self._tool_policy_snapshot(row) for row in rows]} async def upsert_tool_policy( self, *, tenant_id: int, tool_name: str, risk_level: str, confirm_required: bool, auto_execute_allowed: bool, is_active: bool, source_scope: Optional[Any], tenant_scope: Optional[Any], policy_metadata: Optional[Any], notes: Optional[str], ) -> dict[str, Any]: normalized_tool_name = str(tool_name or "").strip() normalized_risk_level = str(risk_level or "").strip().lower() if not normalized_tool_name: raise ValueError("tool_name is required") if normalized_risk_level not in {"low", "medium", "high", "critical"}: raise ValueError("unsupported tool policy risk_level") row = ( await self.db.execute( select(MountedToolCapabilityPolicy).where( MountedToolCapabilityPolicy.tenant_id == int(tenant_id), MountedToolCapabilityPolicy.tool_name == normalized_tool_name, ) ) ).scalar_one_or_none() if row is None: row = MountedToolCapabilityPolicy( tenant_id=int(tenant_id), tool_name=normalized_tool_name, ) self.db.add(row) row.risk_level = normalized_risk_level row.confirm_required = bool(confirm_required) row.auto_execute_allowed = bool(auto_execute_allowed) row.is_active = bool(is_active) row.source_scope = source_scope if isinstance(source_scope, (dict, list)) else None row.tenant_scope = tenant_scope if isinstance(tenant_scope, (dict, list)) else None row.policy_metadata = policy_metadata if isinstance(policy_metadata, (dict, list)) else None row.notes = str(notes or "").strip() or None await self.db.commit() await self.db.refresh(row) return {"item": self._tool_policy_snapshot(row)} async def list_topics( self, *, tenant_id: int, limit: int, offset: int, mode: Optional[str] = None, ) -> dict[str, Any]: base_stmt = select(MountedTopicWorkspace).where(MountedTopicWorkspace.tenant_id == int(tenant_id)) if mode: base_stmt = base_stmt.where(MountedTopicWorkspace.mode == str(mode).strip()) total = ( await self.db.execute( select(func.count()).select_from(base_stmt.order_by(None).subquery()) ) ).scalar_one() rows = ( await self.db.execute( base_stmt.order_by(MountedTopicWorkspace.updated_at.desc(), MountedTopicWorkspace.id.desc()) .offset(int(offset)) .limit(int(limit)) ) ).scalars().all() topic_ids = [int(row.id) for row in rows] draft_map: dict[int, MountedDraftWorkspace] = {} if topic_ids: draft_rows = ( await self.db.execute( select(MountedDraftWorkspace).where( MountedDraftWorkspace.tenant_id == int(tenant_id), MountedDraftWorkspace.topic_workspace_id.in_(topic_ids), ) ) ).scalars().all() draft_map = {int(row.topic_workspace_id): row for row in draft_rows} return { "items": [self._topic_snapshot(row, draft=draft_map.get(int(row.id))) for row in rows], "total": int(total or 0), } async def list_runs( self, *, tenant_id: int, limit: int, offset: int, status: Optional[str] = None, ) -> dict[str, Any]: base_stmt = select(MountedConversationRun).where(MountedConversationRun.tenant_id == int(tenant_id)) if status: base_stmt = base_stmt.where(MountedConversationRun.status == str(status).strip()) total = ( await self.db.execute( select(func.count()).select_from(base_stmt.order_by(None).subquery()) ) ).scalar_one() rows = ( await self.db.execute( base_stmt.order_by(MountedConversationRun.updated_at.desc(), MountedConversationRun.id.desc()) .offset(int(offset)) .limit(int(limit)) ) ).scalars().all() run_ids = [int(row.id) for row in rows] trace_counts: dict[int, int] = {} if run_ids: trace_rows = ( await self.db.execute( select( MountedAgentTrace.conversation_run_id, func.count(MountedAgentTrace.id).label("trace_count"), ) .where( MountedAgentTrace.tenant_id == int(tenant_id), MountedAgentTrace.conversation_run_id.in_(run_ids), ) .group_by(MountedAgentTrace.conversation_run_id) ) ).all() trace_counts = {int(row.conversation_run_id): int(row.trace_count or 0) for row in trace_rows} return { "items": [self._run_snapshot(row, trace_count=trace_counts.get(int(row.id), 0)) for row in rows], "total": int(total or 0), } async def get_run_detail(self, *, tenant_id: int, run_id: int) -> dict[str, Any]: row = ( await self.db.execute( select(MountedConversationRun).where( MountedConversationRun.tenant_id == int(tenant_id), MountedConversationRun.id == int(run_id), ) ) ).scalar_one_or_none() if row is None: raise ValueError("mounted ai run not found") trace_rows = ( await self.db.execute( select(MountedAgentTrace) .where( MountedAgentTrace.tenant_id == int(tenant_id), MountedAgentTrace.conversation_run_id == int(row.id), ) .order_by(MountedAgentTrace.id.asc()) ) ).scalars().all() topic_row: MountedTopicWorkspace | None = None draft_row: MountedDraftWorkspace | None = None if row.topic_workspace_id is not None: topic_row = ( await self.db.execute( select(MountedTopicWorkspace).where( MountedTopicWorkspace.tenant_id == int(tenant_id), MountedTopicWorkspace.id == int(row.topic_workspace_id), ) ) ).scalar_one_or_none() if topic_row is not None: draft_row = ( await self.db.execute( select(MountedDraftWorkspace).where( MountedDraftWorkspace.tenant_id == int(tenant_id), MountedDraftWorkspace.topic_workspace_id == int(topic_row.id), ) ) ).scalar_one_or_none() execution_outcome = row.execution_outcome if isinstance(row.execution_outcome, dict) else row.execution_outcome return { "item": self._run_snapshot(row, trace_count=len(trace_rows)), "topic": self._topic_snapshot(topic_row, draft=draft_row) if topic_row is not None else None, "draft": self._draft_snapshot(draft_row) if draft_row is not None else None, "agent_traces": [self._trace_snapshot(trace) for trace in trace_rows], "context_layers_hit": row.context_layers_hit, "tool_calls": row.tool_calls, "execution_outcome": execution_outcome, "semantic_packet_summary": ( execution_outcome.get("semantic_packet_summary") if isinstance(execution_outcome, dict) else None ), "semantic_decision": ( execution_outcome.get("semantic_decision") if isinstance(execution_outcome, dict) else None ), "semantic_gate_result": ( execution_outcome.get("semantic_gate_result") if isinstance(execution_outcome, dict) else None ), "semantic_diff": ( execution_outcome.get("semantic_diff") if isinstance(execution_outcome, dict) else None ), } async def replay_run( self, *, tenant_id: int, actor_id: Optional[int], run_id: int, asset_type: str, bucket_key: Optional[str], question_text: Optional[str], answer_text: Optional[str], corrected_answer: Optional[str], notes: Optional[str], persist_quality_asset: bool, overall_score: Optional[float], rule_score: Optional[float], llm_score: Optional[float], findings: Optional[Any], ) -> dict[str, Any]: run = ( await self.db.execute( select(MountedConversationRun).where( MountedConversationRun.tenant_id == int(tenant_id), MountedConversationRun.id == int(run_id), ) ) ).scalar_one_or_none() if run is None: raise ValueError("mounted ai run not found") topic = None if run.topic_workspace_id is not None: topic = ( await self.db.execute( select(MountedTopicWorkspace).where( MountedTopicWorkspace.tenant_id == int(tenant_id), MountedTopicWorkspace.id == int(run.topic_workspace_id), ) ) ).scalar_one_or_none() topic_metadata = topic.metadata_json if isinstance(getattr(topic, "metadata_json", None), dict) else {} normalized_question = str( question_text or topic_metadata.get("last_message_preview") or run.source_message_id or f"mounted run {run.id}" ).strip() normalized_answer = str(answer_text or run.final_reply or run.error_text or "").strip() if not normalized_answer and run.execution_outcome is not None: normalized_answer = json.dumps(run.execution_outcome, ensure_ascii=False) if not normalized_answer: raise ValueError("mounted ai run missing replayable answer") normalized_asset_type = str(asset_type or "").strip().lower() or "failure" if normalized_asset_type not in {"golden", "failure"}: raise ValueError("unsupported replay asset_type") asset_row: Optional[QualityAsset] = None if persist_quality_asset: asset_row = QualityAsset( tenant_id=int(tenant_id), project_id=None, asset_type=normalized_asset_type, status="approved", prompt_key="mounted.runtime.reply", workflow_type=f"mounted.{run.mode_after or run.mode_before or 'chat'}", asked_via=run.source_system, route_rule_name=run.trigger_reason, question_text=normalized_question, answer_text=normalized_answer, corrected_answer=str(corrected_answer or "").strip() or None, source_snapshot={ "run_id": int(run.id), "trace_id": run.trace_id, "topic_workspace_id": int(run.topic_workspace_id) if run.topic_workspace_id is not None else None, "context_layers_hit": run.context_layers_hit, "execution_outcome": run.execution_outcome, }, tags=["mounted_runtime", run.active_agent or "ConversationOrchestrator"], notes=str(notes or "").strip() or None, approved_by=actor_id, approved_at=now_cn_naive().isoformat(), ) self.db.add(asset_row) await self.db.flush() judge_row = JudgeRun( tenant_id=int(tenant_id), project_id=None, asset_id=int(asset_row.id) if asset_row is not None else None, prompt_key="mounted.runtime.reply", workflow_type=f"mounted.{run.mode_after or run.mode_before or 'chat'}", asked_via=run.source_system, judge_status="completed", bucket_key=str(bucket_key or f"mounted_runtime.{(run.active_agent or 'conversation_orchestrator').lower()}").strip(), rule_score=rule_score, llm_score=llm_score, overall_score=overall_score, findings=findings if findings is not None else [], metadata_json={ "run_id": int(run.id), "trace_id": run.trace_id, "topic_key": getattr(topic, "topic_key", None), "trigger_reason": run.trigger_reason, }, ) self.db.add(judge_row) await self.db.commit() await self.db.refresh(judge_row) if asset_row is not None: await self.db.refresh(asset_row) return { "run_id": int(run.id), "asset": self._quality_asset_snapshot(asset_row) if asset_row is not None else None, "judge_run": self._judge_run_snapshot(judge_row, asset=asset_row), } async def get_cutover_status(self, *, tenant_id: int) -> dict[str, Any]: topic_workspace_count = ( await self.db.execute( select(func.count(MountedTopicWorkspace.id)).where( MountedTopicWorkspace.tenant_id == int(tenant_id) ) ) ).scalar_one() conversation_run_count = ( await self.db.execute( select(func.count(MountedConversationRun.id)).where( MountedConversationRun.tenant_id == int(tenant_id) ) ) ).scalar_one() tool_policy_count = ( await self.db.execute( select(func.count(MountedToolCapabilityPolicy.id)).where( MountedToolCapabilityPolicy.tenant_id == int(tenant_id) ) ) ).scalar_one() observed_sources = ( await self.db.execute( select(MountedConversationRun.source_system) .where(MountedConversationRun.tenant_id == int(tenant_id)) .distinct() .order_by(MountedConversationRun.source_system.asc()) ) ).scalars().all() legacy_cutover_items = self._build_legacy_cutover_items() active_item_count = sum(1 for item in legacy_cutover_items if item["status"] == "active") retired_item_count = sum(1 for item in legacy_cutover_items if item["status"] == "retired") legacy_cutover_state = "retired" if active_item_count == 0 else "active_legacy_paths_remaining" return { "mounted_runtime_ready": True, "topic_workspace_count": int(topic_workspace_count or 0), "conversation_run_count": int(conversation_run_count or 0), "tool_policy_count": int(tool_policy_count or 0), "observed_source_systems": [str(item) for item in observed_sources if str(item or "").strip()], "legacy_cutover_state": legacy_cutover_state, "retired_item_count": int(retired_item_count), "active_item_count": int(active_item_count), "legacy_cutover_items": legacy_cutover_items, } async def list_training_ops(self, *, tenant_id: int) -> dict[str, Any]: dataset_count = ( await self.db.execute( select(func.count(TrainingDataset.id)).where(TrainingDataset.tenant_id == int(tenant_id)) ) ).scalar_one() ready_eval_suite_count = ( await self.db.execute( select(func.count(TrainingDataset.id)).where( TrainingDataset.tenant_id == int(tenant_id), TrainingDataset.dataset_type == "eval", TrainingDataset.status == "ready", ) ) ).scalar_one() pending_job_count = ( await self.db.execute( select(func.count(TrainingJob.id)).where( TrainingJob.tenant_id == int(tenant_id), TrainingJob.status.in_(("queued", "running")), ) ) ).scalar_one() candidate_deployment_count = ( await self.db.execute( select(func.count(TrainedModelDeployment.id)).where( TrainedModelDeployment.tenant_id == int(tenant_id), TrainedModelDeployment.deployment_status == "candidate", ) ) ).scalar_one() approved_golden_asset_count = ( await self.db.execute( select(func.count(QualityAsset.id)).where( QualityAsset.tenant_id == int(tenant_id), QualityAsset.asset_type == "golden", QualityAsset.status == "approved", ) ) ).scalar_one() approved_failure_asset_count = ( await self.db.execute( select(func.count(QualityAsset.id)).where( QualityAsset.tenant_id == int(tenant_id), QualityAsset.asset_type == "failure", QualityAsset.status == "approved", ) ) ).scalar_one() preference_pair_count = ( await self.db.execute( select(func.count(QualityAssetPair.id)).where( QualityAssetPair.tenant_id == int(tenant_id), QualityAssetPair.status == "approved", ) ) ).scalar_one() hard_case_filter = case((JudgeRun.overall_score.is_(None), True), else_=JudgeRun.overall_score < 90) hard_case_count = ( await self.db.execute( select(func.count(JudgeRun.id)).where( JudgeRun.tenant_id == int(tenant_id), hard_case_filter, ) ) ).scalar_one() last_training_job_at = ( await self.db.execute( select(func.max(TrainingJob.created_at)).where(TrainingJob.tenant_id == int(tenant_id)) ) ).scalar_one() bucket_rows = ( await self.db.execute( select( JudgeRun.bucket_key, func.count(JudgeRun.id).label("run_count"), func.avg(JudgeRun.overall_score).label("avg_score"), func.max(JudgeRun.created_at).label("last_seen_at"), ) .where( JudgeRun.tenant_id == int(tenant_id), hard_case_filter, ) .group_by(JudgeRun.bucket_key) .order_by(func.count(JudgeRun.id).desc(), func.max(JudgeRun.created_at).desc()) .limit(5) ) ).all() recent_datasets = ( await self.db.execute( select(TrainingDataset) .where(TrainingDataset.tenant_id == int(tenant_id)) .order_by(TrainingDataset.id.desc()) .limit(4) ) ).scalars().all() recent_jobs = ( await self.db.execute( select(TrainingJob) .where(TrainingJob.tenant_id == int(tenant_id)) .order_by(TrainingJob.id.desc()) .limit(6) ) ).scalars().all() acceptance_gate = await TrainingPipelineService(self.db).get_acceptance_gate(tenant_id=tenant_id) return { "dataset_count": int(dataset_count or 0), "ready_eval_suite_count": int(ready_eval_suite_count or 0), "pending_job_count": int(pending_job_count or 0), "candidate_deployment_count": int(candidate_deployment_count or 0), "approved_golden_asset_count": int(approved_golden_asset_count or 0), "approved_failure_asset_count": int(approved_failure_asset_count or 0), "preference_pair_count": int(preference_pair_count or 0), "hard_case_count": int(hard_case_count or 0), "last_training_job_at": self._dt(last_training_job_at), "regression_buckets": [ { "bucket_key": str(row.bucket_key or "unbucketed"), "run_count": int(row.run_count or 0), "avg_score": float(row.avg_score or 0.0) if row.avg_score is not None else None, "last_seen_at": self._dt(row.last_seen_at), } for row in bucket_rows ], "recent_datasets": [ { "id": int(row.id), "dataset_type": row.dataset_type, "version": row.version, "status": row.status, "sample_count": int(row.sample_count or 0), "suite_kind": ( str((row.selection_rules or {}).get("suite_kind") or "").strip() or None if isinstance(row.selection_rules, dict) else None ), } for row in recent_datasets ], "recent_jobs": [ { "id": int(row.id), "job_type": row.job_type, "target_slot": row.target_slot, "status": row.status, "provider_backend": row.provider_backend, "dataset_id": int(row.dataset_id), "artifact_ref": row.artifact_ref, } for row in recent_jobs ], "acceptance_gate": acceptance_gate, } async def list_hard_cases(self, *, tenant_id: int, limit: int = 20, offset: int = 0) -> dict[str, Any]: hard_case_filter = case((JudgeRun.overall_score.is_(None), True), else_=JudgeRun.overall_score < 90) base_stmt = ( select(JudgeRun) .where( JudgeRun.tenant_id == int(tenant_id), hard_case_filter, ) ) total = ( await self.db.execute( select(func.count()).select_from(base_stmt.order_by(None).subquery()) ) ).scalar_one() rows = ( await self.db.execute( base_stmt.order_by(JudgeRun.created_at.desc(), JudgeRun.id.desc()) .offset(int(offset)) .limit(int(limit)) ) ).scalars().all() asset_ids = [int(row.asset_id) for row in rows if row.asset_id is not None] asset_map: dict[int, QualityAsset] = {} if asset_ids: asset_rows = ( await self.db.execute( select(QualityAsset).where( QualityAsset.tenant_id == int(tenant_id), QualityAsset.id.in_(asset_ids), ) ) ).scalars().all() asset_map = {int(row.id): row for row in asset_rows} return { "items": [self._judge_run_snapshot(row, asset=asset_map.get(int(row.asset_id))) for row in rows], "total": int(total or 0), } async def list_eval_suites(self, *, tenant_id: int) -> dict[str, Any]: datasets = ( await self.db.execute( select(TrainingDataset) .where( TrainingDataset.tenant_id == int(tenant_id), TrainingDataset.dataset_type == "eval", ) .order_by(TrainingDataset.id.desc()) ) ).scalars().all() if not datasets: return {"items": [], "total": 0} dataset_ids = [int(row.id) for row in datasets] jobs = ( await self.db.execute( select(TrainingJob) .where( TrainingJob.tenant_id == int(tenant_id), TrainingJob.dataset_id.in_(dataset_ids), ) .order_by(TrainingJob.id.desc()) ) ).scalars().all() latest_job_by_dataset: dict[int, TrainingJob] = {} for job in jobs: latest_job_by_dataset.setdefault(int(job.dataset_id), job) job_ids = [int(job.id) for job in latest_job_by_dataset.values()] evaluations = [] if job_ids: evaluations = ( await self.db.execute( select(TrainingEvaluation) .where( TrainingEvaluation.tenant_id == int(tenant_id), TrainingEvaluation.training_job_id.in_(job_ids), ) .order_by(TrainingEvaluation.id.desc()) ) ).scalars().all() evaluations_by_job: dict[int, list[TrainingEvaluation]] = {} for item in evaluations: evaluations_by_job.setdefault(int(item.training_job_id), []).append(item) items: list[dict[str, Any]] = [] for dataset in datasets: latest_job = latest_job_by_dataset.get(int(dataset.id)) latest_evaluations = evaluations_by_job.get(int(latest_job.id), []) if latest_job is not None else [] completed_scores = [ float(row.score or 0.0) for row in latest_evaluations if row.score is not None and str(row.status or "").strip().lower() == "completed" ] latest_score = round(sum(completed_scores) / len(completed_scores), 2) if completed_scores else None last_evaluated_at = None if latest_evaluations: last_evaluated_at = latest_evaluations[0].updated_at items.append( self._eval_suite_snapshot( dataset, latest_score=latest_score, last_evaluated_at=last_evaluated_at, ) ) return {"items": items, "total": len(items)} async def get_eval_suite_detail(self, *, tenant_id: int, dataset_id: int) -> dict[str, Any]: dataset = ( await self.db.execute( select(TrainingDataset).where( TrainingDataset.tenant_id == int(tenant_id), TrainingDataset.id == int(dataset_id), TrainingDataset.dataset_type == "eval", ) ) ).scalar_one_or_none() if dataset is None: raise ValueError("mounted ai eval suite not found") latest_job = ( await self.db.execute( select(TrainingJob) .where( TrainingJob.tenant_id == int(tenant_id), TrainingJob.dataset_id == int(dataset.id), ) .order_by(TrainingJob.id.desc()) .limit(1) ) ).scalar_one_or_none() evaluations: list[TrainingEvaluation] = [] if latest_job is not None: evaluations = ( await self.db.execute( select(TrainingEvaluation) .where( TrainingEvaluation.tenant_id == int(tenant_id), TrainingEvaluation.training_job_id == int(latest_job.id), ) .order_by(TrainingEvaluation.id.asc()) ) ).scalars().all() completed_scores = [ float(row.score or 0.0) for row in evaluations if row.score is not None and str(row.status or "").strip().lower() == "completed" ] latest_score = round(sum(completed_scores) / len(completed_scores), 2) if completed_scores else None last_evaluated_at = evaluations[-1].updated_at if evaluations else None return { "item": self._eval_suite_snapshot( dataset, latest_score=latest_score, last_evaluated_at=last_evaluated_at, ), "selection_rules": dataset.selection_rules if isinstance(dataset.selection_rules, dict) else {}, "notes": dataset.notes, "evaluations": [self._evaluation_snapshot(row) for row in evaluations], }