Skip to main content

systemprompt_cli/commands/infrastructure/logs/
audit.rs

1use std::sync::Arc;
2
3use anyhow::Result;
4use clap::Args;
5use schemars::JsonSchema;
6use serde::{Deserialize, Serialize};
7use systemprompt_identifiers::{AiRequestId, TaskId, TraceId};
8use systemprompt_logging::TraceQueryService;
9
10use super::types::MessageRow;
11use crate::CliConfig;
12use crate::shared::{CommandOutput, render_result};
13
14#[derive(Debug, Args)]
15pub struct AuditArgs {
16    #[arg(help = "AI request ID, task ID, or trace ID")]
17    pub id: String,
18}
19
20#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
21pub struct AuditOutput {
22    pub request_id: AiRequestId,
23    pub provider: String,
24    pub model: String,
25    pub requested_model: Option<String>,
26    pub input_tokens: i32,
27    pub output_tokens: i32,
28    pub cost_dollars: f64,
29    pub latency_ms: i64,
30    pub task_id: Option<TaskId>,
31    pub trace_id: Option<TraceId>,
32    pub messages: Vec<MessageRow>,
33    pub tool_calls: Vec<AuditToolCall>,
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
37pub struct AuditToolCall {
38    pub tool_name: String,
39    pub tool_input: String,
40    pub sequence: i32,
41}
42
43crate::define_pool_command!(AuditArgs => (), with_config);
44
45async fn execute_with_pool_inner(
46    args: AuditArgs,
47    pool: &Arc<sqlx::PgPool>,
48    config: &CliConfig,
49) -> Result<()> {
50    let service = TraceQueryService::new(Arc::clone(pool));
51
52    let row = service.find_ai_request_for_audit(&args.id).await?;
53
54    let Some(row) = row else {
55        render_result(&not_found_output(&args.id), config);
56        return Ok(());
57    };
58
59    let request_id = AiRequestId::new(row.id);
60    let (messages, tool_calls) = tokio::try_join!(
61        service.list_audit_messages(request_id.as_str()),
62        service.list_audit_tool_calls(request_id.as_str()),
63    )?;
64
65    let output = AuditOutput {
66        request_id,
67        provider: row.provider,
68        model: row.model,
69        requested_model: row.requested_model,
70        input_tokens: row.input_tokens.unwrap_or(0),
71        output_tokens: row.output_tokens.unwrap_or(0),
72        cost_dollars: row.cost_microdollars as f64 / 1_000_000.0,
73        latency_ms: i64::from(row.latency_ms.unwrap_or(0)),
74        task_id: row.task_id,
75        trace_id: row.trace_id.map(TraceId::new),
76        messages: messages
77            .into_iter()
78            .map(|m| MessageRow {
79                sequence: m.sequence_number,
80                role: m.role,
81                content: m.content,
82            })
83            .collect(),
84        tool_calls: tool_calls
85            .into_iter()
86            .map(|t| AuditToolCall {
87                tool_name: t.tool_name,
88                tool_input: t.tool_input,
89                sequence: t.sequence_number,
90            })
91            .collect(),
92    };
93
94    render_result(&build_audit(&output), config);
95
96    Ok(())
97}
98
99#[must_use]
100pub fn build_audit(output: &AuditOutput) -> CommandOutput {
101    CommandOutput::card_value("AI Request Audit", output)
102}
103
104#[must_use]
105pub fn not_found_output(id: &str) -> CommandOutput {
106    use systemprompt_models::artifacts::NoticeLine;
107    CommandOutput::message(vec![
108        NoticeLine::new("warning", format!("No AI request found for: {id}")),
109        NoticeLine::new(
110            "info",
111            "Tip: Use 'systemprompt infra logs request list' to see recent requests",
112        ),
113        NoticeLine::new(
114            "info",
115            "Use 'systemprompt infra logs trace list' to see recent traces",
116        ),
117    ])
118}