feat(awooop): surface ai provider route status
All checks were successful
Code Review / ai-code-review (push) Successful in 14s
CD Pipeline / tests (push) Successful in 6m2s
CD Pipeline / build-and-deploy (push) Successful in 4m21s
CD Pipeline / post-deploy-checks (push) Successful in 1m21s

This commit is contained in:
Your Name
2026-05-19 13:45:04 +08:00
parent 3477c7569a
commit 56a8085dcf
6 changed files with 594 additions and 1 deletions

View File

@@ -25,6 +25,9 @@ from src.core.awooop_operator_auth import (
from src.services.platform_operator_service import (
decide_approval as decide_approval_svc,
)
from src.services.platform_operator_service import (
get_ai_route_status as get_ai_route_status_svc,
)
from src.services.platform_operator_service import (
get_awooop_status_chain as get_awooop_status_chain_svc,
)
@@ -99,6 +102,21 @@ class ListCallbackRepliesResponse(BaseModel):
per_page: int
class AiRouteStatusResponse(BaseModel):
schema_version: str
workload_type: str
policy_order: list[dict[str, Any]]
selected_provider: str | None = None
selected_url: str | None = None
selected_model: str | None = None
fallback_chain: list[dict[str, Any]]
route_reason: str
route_source: str
route_error: str | None = None
health: dict[str, dict[str, Any]]
checked_at: datetime
class ApprovalItem(BaseModel):
run_id: UUID
project_id: str
@@ -198,6 +216,24 @@ async def list_callback_replies(
)
@router.get(
"/ai-route-status",
response_model=AiRouteStatusResponse,
summary="查詢 AI Provider 路由狀態",
description=(
"回傳目前 Ollama/Gemini 路由策略、即時 primary、fallback chain 與健康狀態;"
"只讀,不觸發推理或自動修復。"
),
)
async def get_ai_route_status(
workload_type: str | None = Query(
"deep_rca",
description="工作負載類型,例如 deep_rca/hermes/interactive/embedding/rag/code_review/image_analysis",
),
) -> dict[str, Any]:
return await get_ai_route_status_svc(workload_type=workload_type)
@router.get(
"/runs/{run_id}/detail",
summary="查詢 Run 詳細時間線",

View File

@@ -13,7 +13,7 @@ import uuid
from collections import defaultdict
from collections.abc import Mapping
from datetime import UTC, datetime
from typing import Any
from typing import Any, get_args
from uuid import UUID
import structlog
@@ -38,6 +38,17 @@ from src.services.awooop_truth_chain_service import (
_summarize_mcp,
fetch_truth_chain,
)
from src.services.ollama_endpoint_resolver import (
OllamaEndpointSelection,
OllamaWorkloadType,
resolve_ollama_order,
)
from src.services.ollama_failover_manager import (
OllamaEndpoint,
OllamaRoutingResult,
get_ollama_failover_manager,
)
from src.services.ollama_health_monitor import HealthReport
from src.services.run_state_machine import transition
logger = structlog.get_logger(__name__)
@@ -74,6 +85,8 @@ _CALLBACK_REPLY_RAW_STATUS_BY_FILTER = {
"failed": "callback_reply_failed",
}
_CALLBACK_REPLY_ACTION_RE = re.compile(r"^[a-z0-9_:-]{1,64}$", re.IGNORECASE)
_AI_ROUTE_STATUS_SCHEMA_VERSION = "awooop_ai_route_status_v1"
_AI_ROUTE_WORKLOADS = set(get_args(OllamaWorkloadType))
# =============================================================================
# Tenants
@@ -283,6 +296,7 @@ async def list_callback_replies(
"limit": per_page,
"offset": (page - 1) * per_page,
}
if project_id:
where_clauses.append("m.project_id = :project_id")
params["project_id"] = project_id
@@ -395,6 +409,150 @@ async def list_callback_replies(
}
async def get_ai_route_status(
workload_type: str | None = None,
) -> dict[str, Any]:
"""回傳目前 AI/Ollama provider routing 的只讀狀態,供 Operator Console 顯示。"""
workload = _validate_ai_route_workload(workload_type)
policy_order = _ai_route_policy_order(workload)
checked_at = _utc_now_naive()
try:
route = await get_ollama_failover_manager().select_provider(task_type=workload)
except Exception as exc:
return {
"schema_version": _AI_ROUTE_STATUS_SCHEMA_VERSION,
"workload_type": workload,
"policy_order": policy_order,
"selected_provider": None,
"selected_url": None,
"selected_model": None,
"fallback_chain": [],
"route_reason": "route_check_failed",
"route_source": "ollama_failover_manager",
"route_error": str(exc),
"health": {},
"checked_at": checked_at,
}
return {
"schema_version": _AI_ROUTE_STATUS_SCHEMA_VERSION,
"workload_type": workload,
"policy_order": policy_order,
"selected_provider": route.primary.provider_name,
"selected_url": route.primary.url or None,
"selected_model": route.primary.model,
"fallback_chain": [
_ai_route_runtime_endpoint_item(endpoint, priority=index + 2)
for index, endpoint in enumerate(route.fallback_chain)
],
"route_reason": route.routing_reason,
"route_source": "ollama_failover_manager",
"route_error": None,
"health": _ai_route_health_map(route),
"checked_at": checked_at,
}
def _validate_ai_route_workload(workload_type: str | None) -> OllamaWorkloadType:
"""Normalize and validate workload filter for the public route status endpoint."""
workload = str(workload_type or "deep_rca").strip() or "deep_rca"
if workload not in _AI_ROUTE_WORKLOADS:
allowed = ", ".join(sorted(_AI_ROUTE_WORKLOADS))
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
detail=f"Unsupported workload_type: {workload}. Allowed: {allowed}",
)
return workload # type: ignore[return-value]
def _ai_route_policy_order(workload: OllamaWorkloadType) -> list[dict[str, Any]]:
"""Expose configured policy order: GCP-A -> GCP-B -> 111 -> Gemini."""
items = [
_ai_route_policy_endpoint_item(endpoint, priority=index + 1)
for index, endpoint in enumerate(resolve_ollama_order(workload))
]
items.append({
"priority": len(items) + 1,
"provider_name": "gemini",
"url": None,
"workload_type": workload,
"reason": "final_cloud_fallback_after_all_ollama_endpoints",
"role": "final_fallback",
"runtime": "cloud",
})
return items
def _ai_route_policy_endpoint_item(
endpoint: OllamaEndpointSelection,
*,
priority: int,
) -> dict[str, Any]:
role = {
"ollama_gcp_a": "primary",
"ollama_gcp_b": "secondary",
"ollama_local": "local_fallback",
}.get(endpoint.provider_name, "ollama")
return {
"priority": priority,
"provider_name": endpoint.provider_name,
"url": endpoint.url,
"workload_type": endpoint.workload_type,
"reason": endpoint.reason,
"role": role,
"runtime": "ollama",
}
def _ai_route_runtime_endpoint_item(
endpoint: OllamaEndpoint,
*,
priority: int,
) -> dict[str, Any]:
return {
"priority": priority,
"provider_name": endpoint.provider_name,
"url": endpoint.url or None,
"model": endpoint.model,
"runtime": "ollama" if endpoint.provider_name.startswith("ollama") else "cloud",
}
def _ai_route_health_map(route: OllamaRoutingResult) -> dict[str, dict[str, Any]]:
"""Convert failover health reports into provider keyed status for the UI."""
health: dict[str, dict[str, Any]] = {
"ollama_gcp_a": _ai_route_health_item(route.health_gcp_a),
}
if route.health_gcp_b:
health["ollama_gcp_b"] = _ai_route_health_item(route.health_gcp_b)
else:
health["ollama_gcp_b"] = _ai_route_not_checked_health_item()
if route.health_local:
health["ollama_local"] = _ai_route_health_item(route.health_local)
else:
health["ollama_local"] = _ai_route_not_checked_health_item()
return health
def _ai_route_health_item(report: HealthReport) -> dict[str, Any]:
payload = report.to_dict()
payload["checked"] = True
return payload
def _ai_route_not_checked_health_item() -> dict[str, Any]:
return {
"status": "not_checked",
"host": "",
"latency_ms": None,
"reason": "standby_not_checked_primary_healthy",
"checked_at": None,
"from_cache": False,
"checked": False,
}
def _timeline_item(
*,
ts: Any,