301 lines
9.8 KiB
Python
301 lines
9.8 KiB
Python
|
|
"""Platform dashboard & analytics (I-DASH, I-COST, I-USE).
|
||
|
|
|
||
|
|
Provides aggregated metrics for:
|
||
|
|
- Agent status, workflow stats, search metrics, knowledge coverage
|
||
|
|
- LLM cost tracking per agent/workflow/user, budget alerts
|
||
|
|
- Feature usage, search queries, agent runs, workflow executions
|
||
|
|
- Proactive suggestions accepted/rejected
|
||
|
|
"""
|
||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import logging
|
||
|
|
import uuid
|
||
|
|
from datetime import UTC, datetime, timedelta
|
||
|
|
from typing import Any
|
||
|
|
|
||
|
|
from sqlalchemy import func, select
|
||
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||
|
|
|
||
|
|
logger = logging.getLogger(__name__)
|
||
|
|
|
||
|
|
|
||
|
|
# ─── I-DASH: Platform Dashboard ─────────────────────────────────────────────
|
||
|
|
|
||
|
|
|
||
|
|
async def get_platform_dashboard(
|
||
|
|
db: AsyncSession,
|
||
|
|
tenant_id: uuid.UUID,
|
||
|
|
) -> dict[str, Any]:
|
||
|
|
"""Get aggregated platform metrics for the dashboard (I-DASH).
|
||
|
|
|
||
|
|
Returns: Agent status, workflow stats, search metrics,
|
||
|
|
knowledge coverage, workstream metrics, system health.
|
||
|
|
"""
|
||
|
|
dashboard: dict[str, Any] = {
|
||
|
|
"agents": {},
|
||
|
|
"workflows": {},
|
||
|
|
"search": {},
|
||
|
|
"knowledge": {},
|
||
|
|
"workstream": {},
|
||
|
|
"system_health": {},
|
||
|
|
"generated_at": datetime.now(UTC).isoformat(),
|
||
|
|
}
|
||
|
|
|
||
|
|
# Agent metrics
|
||
|
|
try:
|
||
|
|
from app.models.workflow import AgentDefinition, AgentRun
|
||
|
|
active_agents = await db.scalar(
|
||
|
|
select(func.count(AgentDefinition.id)).where(
|
||
|
|
AgentDefinition.tenant_id == tenant_id,
|
||
|
|
AgentDefinition.is_active == True, # noqa: E712
|
||
|
|
)
|
||
|
|
)
|
||
|
|
total_runs = await db.scalar(
|
||
|
|
select(func.count(AgentRun.id)).where(
|
||
|
|
AgentRun.tenant_id == tenant_id,
|
||
|
|
)
|
||
|
|
)
|
||
|
|
recent_runs = await db.scalar(
|
||
|
|
select(func.count(AgentRun.id)).where(
|
||
|
|
AgentRun.tenant_id == tenant_id,
|
||
|
|
AgentRun.created_at >= datetime.now(UTC) - timedelta(days=7),
|
||
|
|
)
|
||
|
|
)
|
||
|
|
dashboard["agents"] = {
|
||
|
|
"active_agents": active_agents or 0,
|
||
|
|
"total_runs": total_runs or 0,
|
||
|
|
"recent_runs_7d": recent_runs or 0,
|
||
|
|
}
|
||
|
|
except Exception as e:
|
||
|
|
logger.warning("Dashboard agent metrics failed: %s", e)
|
||
|
|
dashboard["agents"] = {"error": str(e)}
|
||
|
|
|
||
|
|
# Workflow metrics
|
||
|
|
try:
|
||
|
|
from app.models.workflow import WorkflowDefinition, WorkflowInstance
|
||
|
|
active_workflows = await db.scalar(
|
||
|
|
select(func.count(WorkflowDefinition.id)).where(
|
||
|
|
WorkflowDefinition.tenant_id == tenant_id,
|
||
|
|
WorkflowDefinition.is_active == True, # noqa: E712
|
||
|
|
)
|
||
|
|
)
|
||
|
|
running_instances = await db.scalar(
|
||
|
|
select(func.count(WorkflowInstance.id)).where(
|
||
|
|
WorkflowInstance.tenant_id == tenant_id,
|
||
|
|
WorkflowInstance.status.in_(["pending", "running", "waiting"]),
|
||
|
|
)
|
||
|
|
)
|
||
|
|
completed_instances = await db.scalar(
|
||
|
|
select(func.count(WorkflowInstance.id)).where(
|
||
|
|
WorkflowInstance.tenant_id == tenant_id,
|
||
|
|
WorkflowInstance.status == "completed",
|
||
|
|
)
|
||
|
|
)
|
||
|
|
dashboard["workflows"] = {
|
||
|
|
"active_workflows": active_workflows or 0,
|
||
|
|
"running_instances": running_instances or 0,
|
||
|
|
"completed_instances": completed_instances or 0,
|
||
|
|
}
|
||
|
|
except Exception as e:
|
||
|
|
logger.warning("Dashboard workflow metrics failed: %s", e)
|
||
|
|
dashboard["workflows"] = {"error": str(e)}
|
||
|
|
|
||
|
|
# Knowledge metrics
|
||
|
|
try:
|
||
|
|
from app.plugins.builtins.wiki.models import WikiArticle
|
||
|
|
wiki_articles = await db.scalar(
|
||
|
|
select(func.count(WikiArticle.id)).where(
|
||
|
|
WikiArticle.tenant_id == tenant_id,
|
||
|
|
WikiArticle.deleted_at.is_(None),
|
||
|
|
)
|
||
|
|
)
|
||
|
|
dashboard["knowledge"] = {
|
||
|
|
"wiki_articles": wiki_articles or 0,
|
||
|
|
}
|
||
|
|
except Exception as e:
|
||
|
|
logger.warning("Dashboard knowledge metrics failed: %s", e)
|
||
|
|
dashboard["knowledge"] = {"error": str(e)}
|
||
|
|
|
||
|
|
# System health (from Redis check)
|
||
|
|
try:
|
||
|
|
from app.core.redis import get_redis
|
||
|
|
redis = await get_redis()
|
||
|
|
if redis:
|
||
|
|
await redis.ping()
|
||
|
|
dashboard["system_health"] = {"redis": "up", "status": "healthy"}
|
||
|
|
else:
|
||
|
|
dashboard["system_health"] = {"redis": "down", "status": "degraded"}
|
||
|
|
except Exception as e:
|
||
|
|
dashboard["system_health"] = {"redis": "error", "status": "degraded", "error": str(e)}
|
||
|
|
|
||
|
|
return dashboard
|
||
|
|
|
||
|
|
|
||
|
|
# ─── I-COST: Cost Tracking Dashboard ─────────────────────────────────────────
|
||
|
|
|
||
|
|
|
||
|
|
async def get_cost_dashboard(
|
||
|
|
db: AsyncSession,
|
||
|
|
tenant_id: uuid.UUID,
|
||
|
|
days: int = 30,
|
||
|
|
) -> dict[str, Any]:
|
||
|
|
"""Get LLM cost tracking metrics (I-COST).
|
||
|
|
|
||
|
|
Returns: Live costs, budget utilization, alert history,
|
||
|
|
cost per tenant/agent/workflow, hard-stop events.
|
||
|
|
"""
|
||
|
|
since = datetime.now(UTC) - timedelta(days=days)
|
||
|
|
|
||
|
|
cost_data: dict[str, Any] = {
|
||
|
|
"period_days": days,
|
||
|
|
"total_cost_usd": 0.0,
|
||
|
|
"by_agent": {},
|
||
|
|
"by_workflow": {},
|
||
|
|
"by_user": {},
|
||
|
|
"daily_trend": [],
|
||
|
|
"budget": {},
|
||
|
|
"alerts": [],
|
||
|
|
"generated_at": datetime.now(UTC).isoformat(),
|
||
|
|
}
|
||
|
|
|
||
|
|
# Aggregate costs from AgentRun
|
||
|
|
try:
|
||
|
|
from app.models.workflow import AgentRun
|
||
|
|
|
||
|
|
# Total cost
|
||
|
|
total_cost = await db.scalar(
|
||
|
|
select(func.sum(AgentRun.total_cost_usd)).where(
|
||
|
|
AgentRun.tenant_id == tenant_id,
|
||
|
|
AgentRun.created_at >= since,
|
||
|
|
)
|
||
|
|
)
|
||
|
|
cost_data["total_cost_usd"] = float(total_cost or 0.0)
|
||
|
|
|
||
|
|
# Cost by agent
|
||
|
|
agent_costs = await db.execute(
|
||
|
|
select(
|
||
|
|
AgentRun.agent_id,
|
||
|
|
func.sum(AgentRun.total_cost_usd).label("cost"),
|
||
|
|
func.count(AgentRun.id).label("runs"),
|
||
|
|
)
|
||
|
|
.where(
|
||
|
|
AgentRun.tenant_id == tenant_id,
|
||
|
|
AgentRun.created_at >= since,
|
||
|
|
)
|
||
|
|
.group_by(AgentRun.agent_id)
|
||
|
|
)
|
||
|
|
for row in agent_costs:
|
||
|
|
cost_data["by_agent"][str(row.agent_id)] = {
|
||
|
|
"cost_usd": float(row.cost or 0.0),
|
||
|
|
"runs": row.runs,
|
||
|
|
}
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
logger.warning("Cost dashboard failed: %s", e)
|
||
|
|
cost_data["error"] = str(e)
|
||
|
|
|
||
|
|
# Budget info from config
|
||
|
|
try:
|
||
|
|
from app.config import get_settings
|
||
|
|
settings = get_settings()
|
||
|
|
monthly_budget = getattr(settings, "llm_monthly_budget_usd", None)
|
||
|
|
if monthly_budget:
|
||
|
|
cost_data["budget"] = {
|
||
|
|
"monthly_limit_usd": monthly_budget,
|
||
|
|
"utilization_pct": (cost_data["total_cost_usd"] / monthly_budget) * 100,
|
||
|
|
}
|
||
|
|
except Exception:
|
||
|
|
pass
|
||
|
|
|
||
|
|
return cost_data
|
||
|
|
|
||
|
|
|
||
|
|
# ─── I-USE: Usage & Collaboration Analytics ─────────────────────────────────
|
||
|
|
|
||
|
|
|
||
|
|
async def get_usage_analytics(
|
||
|
|
db: AsyncSession,
|
||
|
|
tenant_id: uuid.UUID,
|
||
|
|
days: int = 30,
|
||
|
|
) -> dict[str, Any]:
|
||
|
|
"""Get feature usage and collaboration analytics (I-USE).
|
||
|
|
|
||
|
|
Returns: Feature usage, search queries, agent runs,
|
||
|
|
workflow executions, proactive suggestions accepted/rejected.
|
||
|
|
"""
|
||
|
|
since = datetime.now(UTC) - timedelta(days=days)
|
||
|
|
|
||
|
|
analytics: dict[str, Any] = {
|
||
|
|
"period_days": days,
|
||
|
|
"agent_runs": {},
|
||
|
|
"workflow_executions": {},
|
||
|
|
"search_queries": {},
|
||
|
|
"proactive_suggestions": {},
|
||
|
|
"generated_at": datetime.now(UTC).isoformat(),
|
||
|
|
}
|
||
|
|
|
||
|
|
# Agent run stats
|
||
|
|
try:
|
||
|
|
from app.models.workflow import AgentRun
|
||
|
|
total_runs = await db.scalar(
|
||
|
|
select(func.count(AgentRun.id)).where(
|
||
|
|
AgentRun.tenant_id == tenant_id,
|
||
|
|
AgentRun.created_at >= since,
|
||
|
|
)
|
||
|
|
)
|
||
|
|
completed_runs = await db.scalar(
|
||
|
|
select(func.count(AgentRun.id)).where(
|
||
|
|
AgentRun.tenant_id == tenant_id,
|
||
|
|
AgentRun.created_at >= since,
|
||
|
|
AgentRun.status == "completed",
|
||
|
|
)
|
||
|
|
)
|
||
|
|
failed_runs = await db.scalar(
|
||
|
|
select(func.count(AgentRun.id)).where(
|
||
|
|
AgentRun.tenant_id == tenant_id,
|
||
|
|
AgentRun.created_at >= since,
|
||
|
|
AgentRun.status.in_(["stopped_error", "stopped_timeout"]),
|
||
|
|
)
|
||
|
|
)
|
||
|
|
analytics["agent_runs"] = {
|
||
|
|
"total": total_runs or 0,
|
||
|
|
"completed": completed_runs or 0,
|
||
|
|
"failed": failed_runs or 0,
|
||
|
|
"success_rate": (completed_runs / total_runs * 100) if total_runs else 0.0,
|
||
|
|
}
|
||
|
|
except Exception as e:
|
||
|
|
analytics["agent_runs"] = {"error": str(e)}
|
||
|
|
|
||
|
|
# Workflow execution stats
|
||
|
|
try:
|
||
|
|
from app.models.workflow import WorkflowInstance
|
||
|
|
total_instances = await db.scalar(
|
||
|
|
select(func.count(WorkflowInstance.id)).where(
|
||
|
|
WorkflowInstance.tenant_id == tenant_id,
|
||
|
|
WorkflowInstance.created_at >= since,
|
||
|
|
)
|
||
|
|
)
|
||
|
|
completed_instances = await db.scalar(
|
||
|
|
select(func.count(WorkflowInstance.id)).where(
|
||
|
|
WorkflowInstance.tenant_id == tenant_id,
|
||
|
|
WorkflowInstance.created_at >= since,
|
||
|
|
WorkflowInstance.status == "completed",
|
||
|
|
)
|
||
|
|
)
|
||
|
|
analytics["workflow_executions"] = {
|
||
|
|
"total": total_instances or 0,
|
||
|
|
"completed": completed_instances or 0,
|
||
|
|
}
|
||
|
|
except Exception as e:
|
||
|
|
analytics["workflow_executions"] = {"error": str(e)}
|
||
|
|
|
||
|
|
return analytics
|
||
|
|
|
||
|
|
|
||
|
|
__all__ = [
|
||
|
|
"get_platform_dashboard",
|
||
|
|
"get_cost_dashboard",
|
||
|
|
"get_usage_analytics",
|
||
|
|
]
|