systemprompt_analytics/repository/costs/
per_user.rs1use super::CostAnalyticsRepository;
11use crate::Result;
12use chrono::{DateTime, Utc};
13use systemprompt_identifiers::{ContextId, UserId};
14
15use crate::models::reporting::{
16 ContextGroupRow, ContextSummaryRow, CostBreakdownRow, CostSummaryRow, PreviousCostRow,
17 RecentContextRow,
18};
19
20impl CostAnalyticsRepository {
21 pub async fn get_summary_for_user(
22 &self,
23 user_id: &UserId,
24 start: DateTime<Utc>,
25 end: DateTime<Utc>,
26 ) -> Result<CostSummaryRow> {
27 sqlx::query_as!(
28 CostSummaryRow,
29 r#"
30 SELECT
31 COUNT(*)::bigint as "requests!",
32 SUM(cost_microdollars)::bigint as "cost",
33 SUM(tokens_used)::bigint as "tokens",
34 SUM(reasoning_tokens)::bigint as "reasoning_tokens",
35 SUM(cache_read_tokens)::bigint as "cache_read_tokens",
36 SUM(cache_creation_tokens)::bigint as "cache_creation_tokens"
37 FROM ai_requests
38 WHERE created_at >= $1 AND created_at < $2
39 AND NOT synthetic AND user_id = $3
40 "#,
41 start,
42 end,
43 user_id.as_str()
44 )
45 .fetch_one(&*self.pool)
46 .await
47 .map_err(Into::into)
48 }
49
50 pub async fn get_previous_cost_for_user(
51 &self,
52 user_id: &UserId,
53 start: DateTime<Utc>,
54 end: DateTime<Utc>,
55 ) -> Result<PreviousCostRow> {
56 sqlx::query_as!(
57 PreviousCostRow,
58 r#"
59 SELECT SUM(cost_microdollars)::bigint as "cost"
60 FROM ai_requests
61 WHERE created_at >= $1 AND created_at < $2
62 AND NOT synthetic AND user_id = $3
63 "#,
64 start,
65 end,
66 user_id.as_str()
67 )
68 .fetch_one(&*self.pool)
69 .await
70 .map_err(Into::into)
71 }
72
73 pub async fn get_breakdown_by_model_for_user(
74 &self,
75 user_id: &UserId,
76 start: DateTime<Utc>,
77 end: DateTime<Utc>,
78 limit: i64,
79 ) -> Result<Vec<CostBreakdownRow>> {
80 sqlx::query_as!(
81 CostBreakdownRow,
82 r#"
83 SELECT
84 model as "name!",
85 COALESCE(SUM(cost_microdollars), 0)::bigint as "cost!",
86 COUNT(*)::bigint as "requests!",
87 COALESCE(SUM(tokens_used), 0)::bigint as "tokens!"
88 FROM ai_requests
89 WHERE created_at >= $1 AND created_at < $2
90 AND NOT synthetic AND user_id = $4
91 AND model IS NOT NULL
92 GROUP BY model
93 ORDER BY SUM(cost_microdollars) DESC NULLS LAST
94 LIMIT $3
95 "#,
96 start,
97 end,
98 limit,
99 user_id.as_str()
100 )
101 .fetch_all(&*self.pool)
102 .await
103 .map_err(Into::into)
104 }
105
106 pub async fn get_context_summary_for_user(
107 &self,
108 user_id: &UserId,
109 start: DateTime<Utc>,
110 end: DateTime<Utc>,
111 ) -> Result<ContextSummaryRow> {
112 sqlx::query_as!(
113 ContextSummaryRow,
114 r#"
115 SELECT
116 COUNT(DISTINCT context_id)::bigint as "conversations!",
117 COUNT(*)::bigint as "ai_requests!"
118 FROM ai_requests
119 WHERE created_at >= $1 AND created_at < $2
120 AND NOT synthetic
121 AND user_id = $3
122 AND context_id IS NOT NULL
123 "#,
124 start,
125 end,
126 user_id.as_str()
127 )
128 .fetch_one(&*self.pool)
129 .await
130 .map_err(Into::into)
131 }
132
133 pub async fn get_contexts_by_model_for_user(
134 &self,
135 user_id: &UserId,
136 start: DateTime<Utc>,
137 end: DateTime<Utc>,
138 limit: i64,
139 ) -> Result<Vec<ContextGroupRow>> {
140 sqlx::query_as!(
141 ContextGroupRow,
142 r#"
143 SELECT
144 model as "name!",
145 COUNT(DISTINCT context_id)::bigint as "conversations!",
146 COUNT(*)::bigint as "ai_requests!"
147 FROM ai_requests
148 WHERE created_at >= $1 AND created_at < $2
149 AND NOT synthetic
150 AND user_id = $3
151 AND context_id IS NOT NULL
152 AND model IS NOT NULL
153 GROUP BY model
154 ORDER BY COUNT(DISTINCT context_id) DESC
155 LIMIT $4
156 "#,
157 start,
158 end,
159 user_id.as_str(),
160 limit
161 )
162 .fetch_all(&*self.pool)
163 .await
164 .map_err(Into::into)
165 }
166
167 pub async fn get_contexts_by_agent_for_user(
168 &self,
169 user_id: &UserId,
170 start: DateTime<Utc>,
171 end: DateTime<Utc>,
172 limit: i64,
173 ) -> Result<Vec<ContextGroupRow>> {
174 sqlx::query_as!(
175 ContextGroupRow,
176 r#"
177 SELECT
178 COALESCE(at.agent_name, 'unattributed') as "name!",
179 COUNT(DISTINCT r.context_id)::bigint as "conversations!",
180 COUNT(*)::bigint as "ai_requests!"
181 FROM ai_requests r
182 LEFT JOIN agent_tasks at ON at.task_id = r.task_id
183 WHERE r.created_at >= $1 AND r.created_at < $2
184 AND NOT r.synthetic
185 AND r.user_id = $3
186 AND r.context_id IS NOT NULL
187 GROUP BY COALESCE(at.agent_name, 'unattributed')
188 ORDER BY COUNT(DISTINCT r.context_id) DESC
189 LIMIT $4
190 "#,
191 start,
192 end,
193 user_id.as_str(),
194 limit
195 )
196 .fetch_all(&*self.pool)
197 .await
198 .map_err(Into::into)
199 }
200
201 pub async fn get_recent_contexts_for_user(
202 &self,
203 user_id: &UserId,
204 end: DateTime<Utc>,
205 limit: i64,
206 ) -> Result<Vec<RecentContextRow>> {
207 sqlx::query_as!(
208 RecentContextRow,
209 r#"
210 SELECT
211 ctx.context_id as "context_id!: ContextId",
212 ctx.last_activity as "last_activity!",
213 ctx.ai_requests as "ai_requests!",
214 last_req.model,
215 last_task.agent_name,
216 uc.name as "context_name?"
217 FROM (
218 SELECT
219 r.context_id,
220 MAX(r.created_at) AS last_activity,
221 COUNT(*) AS ai_requests
222 FROM ai_requests r
223 WHERE r.user_id = $1
224 AND r.created_at < $2
225 AND NOT r.synthetic
226 AND r.context_id IS NOT NULL
227 GROUP BY r.context_id
228 ORDER BY MAX(r.created_at) DESC
229 LIMIT $3
230 ) ctx
231 LEFT JOIN LATERAL (
232 SELECT model, task_id FROM ai_requests
233 WHERE context_id = ctx.context_id
234 ORDER BY created_at DESC
235 LIMIT 1
236 ) last_req ON TRUE
237 LEFT JOIN agent_tasks last_task ON last_task.task_id = last_req.task_id
238 LEFT JOIN user_contexts uc ON uc.context_id = ctx.context_id
239 ORDER BY ctx.last_activity DESC
240 "#,
241 user_id.as_str(),
242 end,
243 limit
244 )
245 .fetch_all(&*self.pool)
246 .await
247 .map_err(Into::into)
248 }
249}