systemprompt_runtime/trace/repository/
ai_trace.rs1use systemprompt_identifiers::{AgentName, AiRequestId, ContextId, ExecutionStepId, TaskId};
12
13use super::{Result, TraceRepository};
14use crate::trace::models::{AiRequestInfo, ConversationMessage, ExecutionStep, TaskInfo};
15
16impl TraceRepository {
17 pub async fn resolve_task_id(&self, partial_id: &str) -> Result<Option<String>> {
18 let pattern = format!("{}%", partial_id);
19 let row = sqlx::query!(
20 "SELECT task_id FROM agent_tasks WHERE task_id LIKE $1 ORDER BY created_at DESC LIMIT 1",
21 pattern
22 )
23 .fetch_optional(&*self.pool)
24 .await?;
25
26 Ok(row.map(|r| r.task_id))
27 }
28
29 pub async fn fetch_task_info(&self, task_id: &TaskId) -> Result<TaskInfo> {
30 let row = sqlx::query!(
31 r#"SELECT task_id, context_id, agent_name, status, created_at, started_at, completed_at,
32 execution_time_ms, error_message
33 FROM agent_tasks WHERE task_id = $1"#,
34 task_id.as_str()
35 )
36 .fetch_one(&*self.pool)
37 .await?;
38
39 Ok(TaskInfo {
40 task_id: TaskId::new(row.task_id),
41 context_id: ContextId::try_new(&row.context_id)?,
42 agent_name: row.agent_name.map(AgentName::new),
43 status: row.status,
44 created_at: row.created_at,
45 started_at: row.started_at,
46 completed_at: row.completed_at,
47 execution_time_ms: row.execution_time_ms,
48 error_message: row.error_message,
49 })
50 }
51
52 pub async fn fetch_user_input(&self, task_id: &TaskId) -> Result<Option<String>> {
53 let row = sqlx::query!(
54 r#"SELECT mp.text_content
55 FROM task_messages tm
56 JOIN message_parts mp ON mp.message_id = tm.message_id AND mp.task_id = tm.task_id
57 WHERE tm.task_id = $1 AND tm.role = 'user' AND mp.part_kind = 'text'
58 ORDER BY tm.sequence_number DESC LIMIT 1"#,
59 task_id.as_str()
60 )
61 .fetch_optional(&*self.pool)
62 .await?;
63
64 Ok(row.and_then(|r| r.text_content))
65 }
66
67 pub async fn fetch_agent_response(&self, task_id: &TaskId) -> Result<Option<String>> {
68 let row = sqlx::query!(
69 r#"SELECT mp.text_content
70 FROM task_messages tm
71 JOIN message_parts mp ON mp.message_id = tm.message_id AND mp.task_id = tm.task_id
72 WHERE tm.task_id = $1 AND tm.role = 'agent' AND mp.part_kind = 'text'
73 ORDER BY tm.sequence_number DESC LIMIT 1"#,
74 task_id.as_str()
75 )
76 .fetch_optional(&*self.pool)
77 .await?;
78
79 Ok(row.and_then(|r| r.text_content))
80 }
81
82 pub async fn fetch_execution_steps(&self, task_id: &TaskId) -> Result<Vec<ExecutionStep>> {
83 let rows = sqlx::query!(
84 r#"SELECT
85 step_id as id,
86 content->>'type' as step_type,
87 COALESCE(content->>'title', content->>'type') as title,
88 status,
89 duration_ms,
90 error_message
91 FROM task_execution_steps
92 WHERE task_id = $1
93 ORDER BY started_at"#,
94 task_id.as_str()
95 )
96 .fetch_all(&*self.pool)
97 .await?;
98
99 Ok(rows
100 .into_iter()
101 .map(|r| ExecutionStep {
102 step_id: ExecutionStepId::new(r.id),
103 step_type: r.step_type,
104 title: r.title,
105 status: r.status,
106 duration_ms: r.duration_ms,
107 error_message: r.error_message,
108 })
109 .collect())
110 }
111
112 pub async fn fetch_ai_requests(&self, task_id: &TaskId) -> Result<Vec<AiRequestInfo>> {
113 let rows = sqlx::query!(
114 r#"SELECT id, model, provider, max_tokens, input_tokens, output_tokens, cost_microdollars, latency_ms
115 FROM ai_requests
116 WHERE task_id = $1
117 ORDER BY created_at"#,
118 task_id.as_str()
119 )
120 .fetch_all(&*self.pool)
121 .await?;
122
123 Ok(rows
124 .into_iter()
125 .map(|r| AiRequestInfo {
126 id: AiRequestId::new(r.id),
127 provider: r.provider,
128 model: r.model,
129 max_tokens: r.max_tokens,
130 input_tokens: r.input_tokens,
131 output_tokens: r.output_tokens,
132 cost_microdollars: r.cost_microdollars,
133 latency_ms: r.latency_ms,
134 })
135 .collect())
136 }
137
138 pub async fn fetch_system_prompt(&self, request_id: &AiRequestId) -> Result<Option<String>> {
139 let row = sqlx::query!(
140 r#"SELECT content
141 FROM ai_request_messages
142 WHERE request_id = $1 AND role = 'system' AND sequence_number = 0
143 LIMIT 1"#,
144 request_id.as_str()
145 )
146 .fetch_optional(&*self.pool)
147 .await?;
148
149 Ok(row.map(|r| r.content))
150 }
151
152 pub async fn fetch_conversation_messages(
153 &self,
154 request_id: &AiRequestId,
155 ) -> Result<Vec<ConversationMessage>> {
156 let rows = sqlx::query!(
157 r#"SELECT role, content, sequence_number
158 FROM ai_request_messages
159 WHERE request_id = $1
160 ORDER BY sequence_number"#,
161 request_id.as_str()
162 )
163 .fetch_all(&*self.pool)
164 .await?;
165
166 Ok(rows
167 .into_iter()
168 .map(|r| ConversationMessage {
169 role: r.role,
170 content: r.content,
171 sequence_number: r.sequence_number,
172 })
173 .collect())
174 }
175
176 pub async fn fetch_ai_request_message_previews(
177 &self,
178 request_id: &AiRequestId,
179 ) -> Result<Vec<ConversationMessage>> {
180 let rows = sqlx::query!(
181 r#"SELECT role, LEFT(content, 500) as content_preview, sequence_number
182 FROM ai_request_messages
183 WHERE request_id = $1
184 ORDER BY sequence_number"#,
185 request_id.as_str()
186 )
187 .fetch_all(&*self.pool)
188 .await?;
189
190 Ok(rows
191 .into_iter()
192 .map(|r| ConversationMessage {
193 role: r.role,
194 content: r.content_preview.unwrap_or_else(String::new),
195 sequence_number: r.sequence_number,
196 })
197 .collect())
198 }
199}