1use serde::{Deserialize, Serialize};
2
3pub const MAX_ANALYTICS_GROUP_LIMIT: usize = 200;
5
6#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
8#[serde(rename_all = "snake_case")]
9pub enum AnalyticsTimeBucket {
10 Hour,
11 Day,
12}
13
14impl AnalyticsTimeBucket {
15 #[must_use]
16 pub fn width_secs(self) -> u64 {
17 match self {
18 Self::Hour => 3_600,
19 Self::Day => 86_400,
20 }
21 }
22}
23
24#[derive(Debug, Clone, Serialize, Deserialize)]
26#[serde(rename_all = "camelCase")]
27pub struct ReceiptAnalyticsQuery {
28 #[serde(default, skip_serializing_if = "Option::is_none")]
29 pub capability_id: Option<String>,
30 #[serde(default, skip_serializing_if = "Option::is_none")]
31 pub agent_subject: Option<String>,
32 #[serde(default, skip_serializing_if = "Option::is_none")]
33 pub tool_server: Option<String>,
34 #[serde(default, skip_serializing_if = "Option::is_none")]
35 pub tool_name: Option<String>,
36 #[serde(default, skip_serializing_if = "Option::is_none")]
37 pub since: Option<u64>,
38 #[serde(default, skip_serializing_if = "Option::is_none")]
39 pub until: Option<u64>,
40 #[serde(default, skip_serializing_if = "Option::is_none")]
41 pub group_limit: Option<usize>,
42 #[serde(default, skip_serializing_if = "Option::is_none")]
43 pub time_bucket: Option<AnalyticsTimeBucket>,
44}
45
46impl Default for ReceiptAnalyticsQuery {
47 fn default() -> Self {
48 Self {
49 capability_id: None,
50 agent_subject: None,
51 tool_server: None,
52 tool_name: None,
53 since: None,
54 until: None,
55 group_limit: Some(50),
56 time_bucket: Some(AnalyticsTimeBucket::Day),
57 }
58 }
59}
60
61#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
63#[serde(rename_all = "camelCase")]
64pub struct ReceiptAnalyticsMetrics {
65 pub total_receipts: u64,
66 pub allow_count: u64,
67 pub deny_count: u64,
68 pub cancelled_count: u64,
69 pub incomplete_count: u64,
70 pub total_cost_charged: u64,
71 pub total_attempted_cost: u64,
72 #[serde(default, skip_serializing_if = "Option::is_none")]
73 pub reliability_score: Option<f64>,
74 #[serde(default, skip_serializing_if = "Option::is_none")]
75 pub compliance_rate: Option<f64>,
76 #[serde(default, skip_serializing_if = "Option::is_none")]
77 pub budget_utilization_rate: Option<f64>,
78}
79
80impl ReceiptAnalyticsMetrics {
81 #[must_use]
82 pub fn from_raw(
83 total_receipts: u64,
84 allow_count: u64,
85 deny_count: u64,
86 cancelled_count: u64,
87 incomplete_count: u64,
88 total_cost_charged: u64,
89 total_attempted_cost: u64,
90 ) -> Self {
91 let terminal_total = allow_count
92 .saturating_add(cancelled_count)
93 .saturating_add(incomplete_count);
94 let attempted_total = total_cost_charged.saturating_add(total_attempted_cost);
95
96 Self {
97 total_receipts,
98 allow_count,
99 deny_count,
100 cancelled_count,
101 incomplete_count,
102 total_cost_charged,
103 total_attempted_cost,
104 reliability_score: ratio_option(allow_count, terminal_total),
105 compliance_rate: ratio_option(
106 total_receipts.saturating_sub(deny_count),
107 total_receipts,
108 ),
109 budget_utilization_rate: ratio_option(total_cost_charged, attempted_total),
110 }
111 }
112}
113
114#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
115#[serde(rename_all = "camelCase")]
116pub struct AgentAnalyticsRow {
117 pub subject_key: String,
118 pub metrics: ReceiptAnalyticsMetrics,
119}
120
121#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
122#[serde(rename_all = "camelCase")]
123pub struct ToolAnalyticsRow {
124 pub tool_server: String,
125 pub tool_name: String,
126 pub metrics: ReceiptAnalyticsMetrics,
127}
128
129#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
130#[serde(rename_all = "camelCase")]
131pub struct TimeAnalyticsRow {
132 pub bucket_start: u64,
133 pub bucket_end: u64,
134 pub metrics: ReceiptAnalyticsMetrics,
135}
136
137#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
138#[serde(rename_all = "camelCase")]
139pub struct ReceiptAnalyticsResponse {
140 pub summary: ReceiptAnalyticsMetrics,
141 pub by_agent: Vec<AgentAnalyticsRow>,
142 pub by_tool: Vec<ToolAnalyticsRow>,
143 pub by_time: Vec<TimeAnalyticsRow>,
144}
145
146fn ratio_option(numerator: u64, denominator: u64) -> Option<f64> {
147 if denominator == 0 {
148 None
149 } else {
150 Some(numerator as f64 / denominator as f64)
151 }
152}