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(
49 &self,
50 user_id: &UserId,
51 now: DateTime<Utc>,
52 ) -> Result<BridgeProfileUsage> {
53 let repo = &self.cost_repo;
54 let d30_start = now - Duration::days(30);
55
56 let (d1, d7, d30) = tokio::try_join!(
57 self.get_usage_window(user_id, now, Duration::days(1)),
58 self.get_usage_window(user_id, now, Duration::days(7)),
59 self.get_usage_window(user_id, now, Duration::days(30)),
60 )?;
61
62 let top_models = self.list_top_models(user_id, d30_start, now).await?;
63
64 let total = repo
65 .get_context_summary_for_user(user_id, d30_start, now)
66 .await?;
67 let by_model = repo
68 .get_contexts_by_model_for_user(user_id, d30_start, now, TOP_GROUPS_LIMIT)
69 .await?;
70 let by_agent = repo
71 .get_contexts_by_agent_for_user(user_id, d30_start, now, TOP_GROUPS_LIMIT)
72 .await?;
73 let recent = repo
74 .get_recent_contexts_for_user(user_id, now, RECENT_LIMIT)
75 .await?;
76
77 let to_group = |r: crate::models::ContextGroupRow| ConversationGroup {
78 name: r.name,
79 conversations: r.conversations,
80 ai_requests: r.ai_requests,
81 };
82
83 Ok(BridgeProfileUsage {
84 d1,
85 d7,
86 d30,
87 top_models,
88 conversations: ConversationSummary {
89 total_conversations: total.conversations,
90 total_ai_requests: total.ai_requests,
91 by_model: by_model.into_iter().map(to_group).collect(),
92 by_agent: by_agent.into_iter().map(to_group).collect(),
93 recent: recent
94 .into_iter()
95 .map(|r| RecentConversationSummary {
96 context_id: r.context_id,
97 last_activity: r.last_activity,
98 ai_requests: r.ai_requests,
99 model: r.model,
100 agent_name: r.agent_name,
101 context_name: r.context_name,
102 })
103 .collect(),
104 },
105 })
106 }
107
108 pub async fn get_usage_window(
109 &self,
110 user_id: &UserId,
111 now: DateTime<Utc>,
112 span: Duration,
113 ) -> Result<UsageWindow> {
114 let start = now - span;
115 let summary = self
116 .cost_repo
117 .get_summary_for_user(user_id, start, now)
118 .await?;
119 let prev = self
120 .cost_repo
121 .get_previous_cost_for_user(user_id, start - span, start)
122 .await?;
123 Ok(UsageWindow {
124 requests: summary.requests,
125 tokens: summary.tokens.unwrap_or(0),
126 cost_microdollars: summary.cost.unwrap_or(0),
127 previous_cost_microdollars: prev.cost,
128 })
129 }
130
131 pub async fn list_top_models(
132 &self,
133 user_id: &UserId,
134 start: DateTime<Utc>,
135 end: DateTime<Utc>,
136 ) -> Result<Vec<ModelShare>> {
137 let rows = self
138 .cost_repo
139 .get_breakdown_by_model_for_user(user_id, start, end, TOP_MODELS_LIMIT)
140 .await?;
141
142 let total_tokens: i64 = rows.iter().map(|r| r.tokens).sum();
143 Ok(rows
144 .into_iter()
145 .map(|r| ModelShare {
146 token_share: if total_tokens > 0 {
147 r.tokens as f64 / total_tokens as f64
148 } else {
149 0.0
150 },
151 model: r.name,
152 requests: r.requests,
153 tokens: r.tokens,
154 cost_microdollars: r.cost,
155 })
156 .collect())
157 }
158}