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