ai_agent/utils/
analyze_context.rs1use std::collections::HashMap;
3
4use crate::types::{Message, MessageRole};
5use crate::services::token_estimation::rough_token_count_estimation_for_content;
6
7#[derive(Debug, Clone, Default)]
9pub struct TokenStats {
10 pub tool_requests: HashMap<String, u64>,
11 pub tool_results: HashMap<String, u64>,
12 pub human_messages: u64,
13 pub assistant_messages: u64,
14 pub local_command_outputs: u64,
15 pub other: u64,
16 pub attachments: HashMap<String, u64>,
17 pub duplicate_file_reads: HashMap<String, FileReadStats>,
18 pub total: u64,
19}
20
21#[derive(Debug, Clone, Default)]
22pub struct FileReadStats {
23 pub count: u64,
24 pub tokens: u64,
25}
26
27pub fn analyze_context(messages: &[Message]) -> TokenStats {
29 let mut stats = TokenStats::default();
30 let mut tool_ids_to_names: HashMap<String, String> = HashMap::new();
31 let mut read_tool_id_to_file_path: HashMap<String, String> = HashMap::new();
32 let mut seen_file_paths: HashMap<String, u64> = HashMap::new();
33
34 for msg in messages {
35 match msg.role {
36 MessageRole::User => {
37 let tokens = rough_token_count_estimation_for_content(&msg.content) as u64;
38 stats.human_messages += tokens;
39 stats.total += tokens;
40 }
41 MessageRole::Assistant => {
42 let tokens = rough_token_count_estimation_for_content(&msg.content) as u64;
43 stats.assistant_messages += tokens;
44 stats.total += tokens;
45
46 if let Some(ref tool_calls) = msg.tool_calls {
48 for call in tool_calls {
49 tool_ids_to_names.insert(call.id.clone(), call.name.clone());
50
51 if call.name == "Read" {
53 if let Some(path) = call.arguments.get("file_path").and_then(|p| p.as_str()) {
54 read_tool_id_to_file_path
55 .insert(call.id.clone(), path.to_string());
56
57 let tokens = rough_token_count_estimation_for_content(&msg.content) as u64;
58 let entry = seen_file_paths.entry(path.to_string()).or_insert(0);
59 *entry += 1;
60 if *entry > 1 {
61 stats.duplicate_file_reads.insert(
62 path.to_string(),
63 FileReadStats {
64 count: *entry,
65 tokens,
66 },
67 );
68 }
69 }
70 }
71 }
72 }
73 }
74 MessageRole::Tool => {
75 let tokens = rough_token_count_estimation_for_content(&msg.content) as u64;
76 stats.total += tokens;
77
78 if let Some(ref tool_call_id) = msg.tool_call_id {
79 if let Some(tool_name) = tool_ids_to_names.get(tool_call_id) {
80 *stats.tool_results.entry(tool_name.clone()).or_insert(0) += tokens;
81
82 *stats.tool_requests.entry(tool_name.clone()).or_insert(0) += 10; } else {
85 stats.local_command_outputs += tokens;
86 }
87 } else {
88 stats.other += tokens;
89 }
90 }
91 MessageRole::System => {
92 let tokens = rough_token_count_estimation_for_content(&msg.content) as u64;
93 stats.other += tokens;
94 stats.total += tokens;
95 }
96 }
97
98 if let Some(ref attachments) = msg.attachments {
100 for attachment in attachments {
101 let attachment_name = match attachment {
102 crate::types::Attachment::File { path } => "File".to_string(),
103 crate::types::Attachment::AlreadyReadFile { path, .. } => "AlreadyReadFile".to_string(),
104 crate::types::Attachment::PdfReference { .. } => "PdfReference".to_string(),
105 crate::types::Attachment::EditedTextFile { .. } => "EditedTextFile".to_string(),
106 crate::types::Attachment::EditedImageFile { .. } => "EditedImageFile".to_string(),
107 crate::types::Attachment::Directory { .. } => "Directory".to_string(),
108 crate::types::Attachment::SelectedLinesInIde { .. } => "SelectedLinesInIde".to_string(),
109 crate::types::Attachment::MemoryFile { .. } => "MemoryFile".to_string(),
110 crate::types::Attachment::SkillListing { .. } => "SkillListing".to_string(),
111 crate::types::Attachment::InvokedSkills { .. } => "InvokedSkills".to_string(),
112 crate::types::Attachment::TaskStatus { .. } => "TaskStatus".to_string(),
113 crate::types::Attachment::PlanFileReference { .. } => "PlanFileReference".to_string(),
114 crate::types::Attachment::McpResources { .. } => "McpResources".to_string(),
115 crate::types::Attachment::DeferredTools { .. } => "DeferredTools".to_string(),
116 crate::types::Attachment::AgentListing { .. } => "AgentListing".to_string(),
117 crate::types::Attachment::Custom { name, .. } => name.clone(),
118 };
119 let attachment_tokens = serde_json::to_string(attachment)
120 .map(|s| rough_token_count_estimation_for_content(&s) as u64)
121 .unwrap_or(0);
122 *stats.attachments.entry(attachment_name).or_insert(0) += attachment_tokens;
123 }
124 }
125 }
126
127 stats
128}
129
130pub fn token_stats_to_metrics(stats: &TokenStats) -> HashMap<String, f64> {
132 let mut metrics = HashMap::new();
133 let total = if stats.total > 0 { stats.total as f64 } else { 1.0 };
134
135 metrics.insert("tool_requests_count".to_string(), stats.tool_requests.len() as f64);
136 metrics.insert("tool_results_count".to_string(), stats.tool_results.len() as f64);
137 metrics.insert(
138 "human_messages_pct".to_string(),
139 stats.human_messages as f64 / total * 100.0,
140 );
141 metrics.insert(
142 "assistant_messages_pct".to_string(),
143 stats.assistant_messages as f64 / total * 100.0,
144 );
145 metrics.insert(
146 "local_command_outputs_pct".to_string(),
147 stats.local_command_outputs as f64 / total * 100.0,
148 );
149 metrics.insert(
150 "other_pct".to_string(),
151 stats.other as f64 / total * 100.0,
152 );
153 metrics.insert("attachments_count".to_string(), stats.attachments.len() as f64);
154 metrics.insert(
155 "duplicate_file_reads_count".to_string(),
156 stats.duplicate_file_reads.len() as f64,
157 );
158 metrics.insert("total_tokens".to_string(), stats.total as f64);
159
160 metrics
161}
162
163pub async fn analyze_context_usage() -> Result<serde_json::Value, String> {
165 Ok(serde_json::json!({
166 "message": "Use analyze_context() directly with messages",
167 }))
168}
169
170#[cfg(test)]
171mod tests {
172 use super::*;
173
174 #[test]
175 fn test_analyze_context_empty() {
176 let stats = analyze_context(&[]);
177 assert_eq!(stats.total, 0);
178 }
179
180 #[test]
181 fn test_analyze_context_user_message() {
182 let messages = vec![Message {
183 role: MessageRole::User,
184 content: "Hello world".to_string(),
185 ..Default::default()
186 }];
187 let stats = analyze_context(&messages);
188 assert!(stats.human_messages > 0);
189 assert_eq!(stats.total, stats.human_messages);
190 }
191
192 #[test]
193 fn test_analyze_context_assistant_message() {
194 let messages = vec![Message {
195 role: MessageRole::Assistant,
196 content: "Here's the answer".to_string(),
197 ..Default::default()
198 }];
199 let stats = analyze_context(&messages);
200 assert!(stats.assistant_messages > 0);
201 }
202
203 #[test]
204 fn test_token_stats_to_metrics() {
205 let mut stats = TokenStats::default();
206 stats.total = 1000;
207 stats.human_messages = 300;
208 let metrics = token_stats_to_metrics(&stats);
209 assert!((metrics.get("human_messages_pct").unwrap() - 30.0) < 0.1);
210 }
211}