systemprompt-cli 0.1.22

systemprompt.io OS - CLI for agent orchestration, AI operations, and system management
Documentation
use anyhow::Result;
use chrono::{DateTime, Utc};
use clap::Args;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use systemprompt_analytics::OverviewAnalyticsRepository;
use systemprompt_logging::CliService;
use systemprompt_runtime::{AppContext, DatabaseContext};

use super::shared::{CsvBuilder, parse_time_range, resolve_export_path};
use crate::CliConfig;
use crate::shared::{CommandResult, RenderingHints};

#[derive(Debug, Args)]
pub struct OverviewArgs {
    #[arg(
        long,
        alias = "from",
        default_value = "24h",
        help = "Time range (e.g., '1h', '24h', '7d')"
    )]
    pub since: Option<String>,

    #[arg(long, alias = "to", help = "End time for range")]
    pub until: Option<String>,

    #[arg(long, help = "Export results to CSV file")]
    pub export: Option<PathBuf>,
}

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct OverviewOutput {
    pub period: String,
    pub conversations: ConversationMetrics,
    pub agents: AgentMetrics,
    pub requests: RequestMetrics,
    pub tools: ToolMetrics,
    pub sessions: SessionMetrics,
    pub costs: CostMetrics,
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema)]
pub struct ConversationMetrics {
    pub total: i64,
    pub change_percent: Option<f64>,
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema)]
pub struct AgentMetrics {
    pub active_count: i64,
    pub total_tasks: i64,
    pub success_rate: f64,
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema)]
pub struct RequestMetrics {
    pub total: i64,
    pub total_tokens: i64,
    pub avg_latency_ms: i64,
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema)]
pub struct ToolMetrics {
    pub total_executions: i64,
    pub success_rate: f64,
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema)]
pub struct SessionMetrics {
    #[serde(rename = "currently_active")]
    pub active: i64,
    #[serde(rename = "created_in_period")]
    pub total_today: i64,
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema)]
pub struct CostMetrics {
    pub total_cost_microdollars: i64,
    pub change_percent: Option<f64>,
}

pub async fn execute(
    args: OverviewArgs,
    _config: &CliConfig,
) -> Result<CommandResult<OverviewOutput>> {
    let ctx = AppContext::new().await?;
    let repo = OverviewAnalyticsRepository::new(ctx.db_pool())?;
    execute_internal(args, &repo).await
}

pub async fn execute_with_pool(
    args: OverviewArgs,
    db_ctx: &DatabaseContext,
    _config: &CliConfig,
) -> Result<CommandResult<OverviewOutput>> {
    let repo = OverviewAnalyticsRepository::new(db_ctx.db_pool())?;
    execute_internal(args, &repo).await
}

async fn execute_internal(
    args: OverviewArgs,
    repo: &OverviewAnalyticsRepository,
) -> Result<CommandResult<OverviewOutput>> {
    let (start, end) = parse_time_range(args.since.as_ref(), args.until.as_ref())?;
    let output = fetch_overview_data(repo, start, end).await?;

    if let Some(ref path) = args.export {
        let resolved_path = resolve_export_path(path)?;
        export_overview_csv(&output, &resolved_path)?;
        CliService::success(&format!("Exported to {}", resolved_path.display()));
        return Ok(CommandResult::dashboard(output).with_skip_render());
    }

    Ok(CommandResult::dashboard(output)
        .with_title("Analytics Overview")
        .with_hints(RenderingHints::default()))
}

async fn fetch_overview_data(
    repo: &OverviewAnalyticsRepository,
    start: DateTime<Utc>,
    end: DateTime<Utc>,
) -> Result<OverviewOutput> {
    let period_duration = end - start;
    let prev_start = start - period_duration;

    let current_conversations = repo.get_conversation_count(start, end).await?;
    let prev_conversations = repo.get_conversation_count(prev_start, start).await?;

    let conversations = ConversationMetrics {
        total: current_conversations,
        change_percent: calculate_change(current_conversations, prev_conversations),
    };

    let agent_metrics = repo.get_agent_metrics(start, end).await?;
    let success_rate = if agent_metrics.total_tasks > 0 {
        (agent_metrics.completed_tasks as f64 / agent_metrics.total_tasks as f64) * 100.0
    } else {
        0.0
    };

    let agents = AgentMetrics {
        active_count: agent_metrics.active_agents,
        total_tasks: agent_metrics.total_tasks,
        success_rate,
    };

    let request_metrics = repo.get_request_metrics(start, end).await?;
    let requests = RequestMetrics {
        total: request_metrics.total,
        total_tokens: request_metrics.total_tokens.unwrap_or(0),
        avg_latency_ms: request_metrics.avg_latency.map_or(0, |v| v as i64),
    };

    let tool_metrics = repo.get_tool_metrics(start, end).await?;
    let tool_success_rate = if tool_metrics.total > 0 {
        (tool_metrics.successful as f64 / tool_metrics.total as f64) * 100.0
    } else {
        0.0
    };

    let tools = ToolMetrics {
        total_executions: tool_metrics.total,
        success_rate: tool_success_rate,
    };

    let active_sessions = repo.get_active_session_count(start).await?;
    let total_sessions = repo.get_total_session_count(start, end).await?;

    let sessions = SessionMetrics {
        active: active_sessions,
        total_today: total_sessions,
    };

    let current_cost = repo.get_cost(start, end).await?;
    let prev_cost = repo.get_cost(prev_start, start).await?;

    let costs = CostMetrics {
        total_cost_microdollars: current_cost.cost.unwrap_or(0),
        change_percent: calculate_change(
            current_cost.cost.unwrap_or(0),
            prev_cost.cost.unwrap_or(0),
        ),
    };

    Ok(OverviewOutput {
        period: format_period(start, end),
        conversations,
        agents,
        requests,
        tools,
        sessions,
        costs,
    })
}

fn calculate_change(current: i64, previous: i64) -> Option<f64> {
    (previous != 0).then(|| ((current - previous) as f64 / previous as f64) * 100.0)
}

fn format_period(start: DateTime<Utc>, end: DateTime<Utc>) -> String {
    format!(
        "{} to {}",
        start.format("%Y-%m-%d %H:%M"),
        end.format("%Y-%m-%d %H:%M")
    )
}

fn export_overview_csv(output: &OverviewOutput, path: &std::path::Path) -> Result<()> {
    let mut csv = CsvBuilder::new().headers(vec![
        "period",
        "conversations_total",
        "conversations_change_pct",
        "agents_active",
        "agents_tasks",
        "agents_success_rate",
        "requests_total",
        "requests_tokens",
        "requests_avg_latency_ms",
        "tools_executions",
        "tools_success_rate",
        "sessions_currently_active",
        "sessions_created_in_period",
        "costs_microdollars",
        "costs_change_pct",
    ]);

    csv.add_row(vec![
        output.period.clone(),
        output.conversations.total.to_string(),
        output
            .conversations
            .change_percent
            .map_or(String::new(), |v| format!("{:.2}", v)),
        output.agents.active_count.to_string(),
        output.agents.total_tasks.to_string(),
        format!("{:.2}", output.agents.success_rate),
        output.requests.total.to_string(),
        output.requests.total_tokens.to_string(),
        output.requests.avg_latency_ms.to_string(),
        output.tools.total_executions.to_string(),
        format!("{:.2}", output.tools.success_rate),
        output.sessions.active.to_string(),
        output.sessions.total_today.to_string(),
        output.costs.total_cost_microdollars.to_string(),
        output
            .costs
            .change_percent
            .map_or(String::new(), |v| format!("{:.2}", v)),
    ]);

    csv.write_to_file(path)
}