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