systemprompt_analytics/services/
profile_usage.rs1use std::sync::Arc;
12
13use chrono::{DateTime, Duration, Utc};
14use sqlx::PgPool;
15use systemprompt_identifiers::UserId;
16use systemprompt_models::api::cloud::{
17 BridgeProfileUsage, ConversationGroup, ConversationSummary, ModelShare,
18 RecentConversationSummary, UsageWindow,
19};
20
21use crate::error::Result;
22use crate::repository::CostAnalyticsRepository;
23
24const TOP_MODELS_LIMIT: i64 = 5;
25const TOP_GROUPS_LIMIT: i64 = 10;
26const RECENT_LIMIT: i64 = 10;
27
28#[derive(Debug, Clone)]
33pub struct ProfileUsageService {
34 cost_repo: CostAnalyticsRepository,
35}
36
37impl ProfileUsageService {
38 #[must_use]
39 pub const fn new(cost_repo: CostAnalyticsRepository) -> Self {
40 Self { cost_repo }
41 }
42
43 #[must_use]
44 pub const fn from_pool(pool: Arc<PgPool>) -> Self {
45 Self::new(CostAnalyticsRepository::from_pool(pool))
46 }
47
48 pub async fn get_profile_usage(
53 &self,
54 user_id: &UserId,
55 now: DateTime<Utc>,
56 ) -> Result<BridgeProfileUsage> {
57 let repo = &self.cost_repo;
58 let d30_start = now - Duration::days(30);
59
60 let (d1, d7, d30) = tokio::try_join!(
61 self.get_usage_window(user_id, now, Duration::days(1)),
62 self.get_usage_window(user_id, now, Duration::days(7)),
63 self.get_usage_window(user_id, now, Duration::days(30)),
64 )?;
65
66 let top_models = self.list_top_models(user_id, d30_start, now).await?;
67
68 let total = repo
69 .get_context_summary_for_user(user_id, d30_start, now)
70 .await?;
71 let by_model = repo
72 .get_contexts_by_model_for_user(user_id, d30_start, now, TOP_GROUPS_LIMIT)
73 .await?;
74 let by_agent = repo
75 .get_contexts_by_agent_for_user(user_id, d30_start, now, TOP_GROUPS_LIMIT)
76 .await?;
77 let recent = repo
78 .get_recent_contexts_for_user(user_id, now, RECENT_LIMIT)
79 .await?;
80
81 let to_group = |r: crate::models::ContextGroupRow| ConversationGroup {
82 name: r.name,
83 conversations: r.conversations,
84 ai_requests: r.ai_requests,
85 };
86
87 Ok(BridgeProfileUsage {
88 d1,
89 d7,
90 d30,
91 top_models,
92 conversations: ConversationSummary {
93 total_conversations: total.conversations,
94 total_ai_requests: total.ai_requests,
95 by_model: by_model.into_iter().map(to_group).collect(),
96 by_agent: by_agent.into_iter().map(to_group).collect(),
97 recent: recent
98 .into_iter()
99 .map(|r| RecentConversationSummary {
100 context_id: r.context_id,
101 last_activity: r.last_activity,
102 ai_requests: r.ai_requests,
103 model: r.model,
104 agent_name: r.agent_name,
105 context_name: r.context_name,
106 })
107 .collect(),
108 },
109 })
110 }
111
112 pub async fn get_usage_window(
115 &self,
116 user_id: &UserId,
117 now: DateTime<Utc>,
118 span: Duration,
119 ) -> Result<UsageWindow> {
120 let start = now - span;
121 let summary = self
122 .cost_repo
123 .get_summary_for_user(user_id, start, now)
124 .await?;
125 let prev = self
126 .cost_repo
127 .get_previous_cost_for_user(user_id, start - span, start)
128 .await?;
129 Ok(UsageWindow {
130 requests: summary.requests,
131 tokens: summary.tokens.unwrap_or(0),
132 cost_microdollars: summary.cost.unwrap_or(0),
133 previous_cost_microdollars: prev.cost,
134 })
135 }
136
137 pub async fn list_top_models(
138 &self,
139 user_id: &UserId,
140 start: DateTime<Utc>,
141 end: DateTime<Utc>,
142 ) -> Result<Vec<ModelShare>> {
143 let rows = self
144 .cost_repo
145 .get_breakdown_by_model_for_user(user_id, start, end, TOP_MODELS_LIMIT)
146 .await?;
147
148 let total_tokens: i64 = rows.iter().map(|r| r.tokens).sum();
149 Ok(rows
150 .into_iter()
151 .map(|r| ModelShare {
152 token_share: if total_tokens > 0 {
153 r.tokens as f64 / total_tokens as f64
154 } else {
155 0.0
156 },
157 model: r.name,
158 requests: r.requests,
159 tokens: r.tokens,
160 cost_microdollars: r.cost,
161 })
162 .collect())
163 }
164}