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