systemprompt_cli/commands/infrastructure/logs/
audit.rs1use 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(¬_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}