1use crate::llm::message::{ContentBlock, Message};
7
8pub fn message_counts(messages: &[Message]) -> (usize, usize, usize) {
10 let mut user = 0;
11 let mut assistant = 0;
12 let mut system = 0;
13
14 for msg in messages {
15 match msg {
16 Message::User(_) => user += 1,
17 Message::Assistant(_) => assistant += 1,
18 Message::System(_) => system += 1,
19 }
20 }
21
22 (user, assistant, system)
23}
24
25pub fn extract_text(messages: &[Message]) -> String {
27 let mut text = String::new();
28 for msg in messages {
29 let blocks = match msg {
30 Message::User(u) => &u.content,
31 Message::Assistant(a) => &a.content,
32 Message::System(s) => {
33 text.push_str(&s.content);
34 text.push('\n');
35 continue;
36 }
37 };
38 for block in blocks {
39 if let ContentBlock::Text { text: t } = block {
40 text.push_str(t);
41 text.push('\n');
42 }
43 }
44 }
45 text
46}
47
48pub fn last_user_message_index(messages: &[Message]) -> Option<usize> {
50 messages
51 .iter()
52 .rposition(|m| matches!(m, Message::User(u) if !u.is_meta))
53}
54
55pub fn last_assistant_index(messages: &[Message]) -> Option<usize> {
57 messages
58 .iter()
59 .rposition(|m| matches!(m, Message::Assistant(_)))
60}
61
62pub fn tool_use_count(messages: &[Message]) -> usize {
64 messages
65 .iter()
66 .filter_map(|m| match m {
67 Message::Assistant(a) => Some(&a.content),
68 _ => None,
69 })
70 .flat_map(|blocks| blocks.iter())
71 .filter(|b| matches!(b, ContentBlock::ToolUse { .. }))
72 .count()
73}
74
75pub fn tools_used(messages: &[Message]) -> Vec<String> {
77 let mut tools: Vec<String> = messages
78 .iter()
79 .filter_map(|m| match m {
80 Message::Assistant(a) => Some(&a.content),
81 _ => None,
82 })
83 .flat_map(|blocks| blocks.iter())
84 .filter_map(|b| match b {
85 ContentBlock::ToolUse { name, .. } => Some(name.clone()),
86 _ => None,
87 })
88 .collect();
89
90 tools.sort();
91 tools.dedup();
92 tools
93}
94
95pub fn truncate_to_budget(messages: &mut Vec<Message>, max_tokens: u64) {
100 while crate::services::tokens::estimate_context_tokens(messages) > max_tokens
101 && messages.len() > 2
102 {
103 messages.remove(1);
105 }
106}
107
108#[cfg(test)]
109mod tests {
110 use super::*;
111 use crate::llm::message::{AssistantMessage, ContentBlock, user_message};
112 use uuid::Uuid;
113
114 fn assistant_msg(text: &str) -> Message {
115 Message::Assistant(AssistantMessage {
116 uuid: Uuid::new_v4(),
117 timestamp: String::new(),
118 content: vec![ContentBlock::Text {
119 text: text.to_string(),
120 }],
121 model: None,
122 usage: None,
123 stop_reason: None,
124 request_id: None,
125 })
126 }
127
128 #[test]
129 fn test_message_counts() {
130 let msgs = vec![
131 user_message("hello"),
132 assistant_msg("hi"),
133 user_message("bye"),
134 ];
135 assert_eq!(message_counts(&msgs), (2, 1, 0));
136 }
137
138 #[test]
139 fn test_tool_use_count() {
140 let msgs = vec![Message::Assistant(AssistantMessage {
141 uuid: Uuid::new_v4(),
142 timestamp: String::new(),
143 content: vec![
144 ContentBlock::ToolUse {
145 id: "1".into(),
146 name: "Bash".into(),
147 input: serde_json::json!({}),
148 },
149 ContentBlock::Text {
150 text: "done".into(),
151 },
152 ],
153 model: None,
154 usage: None,
155 stop_reason: None,
156 request_id: None,
157 })];
158 assert_eq!(tool_use_count(&msgs), 1);
159 }
160
161 #[test]
162 fn test_extract_text() {
163 let msgs = vec![user_message("hello world"), assistant_msg("response here")];
164 let text = extract_text(&msgs);
165 assert!(text.contains("hello world"));
166 assert!(text.contains("response here"));
167 }
168
169 #[test]
170 fn test_last_user_message_index() {
171 let msgs = vec![
172 user_message("first"),
173 assistant_msg("reply"),
174 user_message("second"),
175 ];
176 assert_eq!(last_user_message_index(&msgs), Some(2));
177 }
178
179 #[test]
180 fn test_last_assistant_index() {
181 let msgs = vec![
182 user_message("first"),
183 assistant_msg("reply"),
184 user_message("second"),
185 ];
186 assert_eq!(last_assistant_index(&msgs), Some(1));
187 }
188
189 #[test]
190 fn test_tools_used() {
191 let msgs = vec![Message::Assistant(AssistantMessage {
192 uuid: Uuid::new_v4(),
193 timestamp: String::new(),
194 content: vec![
195 ContentBlock::ToolUse {
196 id: "1".into(),
197 name: "Bash".into(),
198 input: serde_json::json!({}),
199 },
200 ContentBlock::ToolUse {
201 id: "2".into(),
202 name: "FileRead".into(),
203 input: serde_json::json!({}),
204 },
205 ContentBlock::ToolUse {
206 id: "3".into(),
207 name: "Bash".into(),
208 input: serde_json::json!({}),
209 },
210 ],
211 model: None,
212 usage: None,
213 stop_reason: None,
214 request_id: None,
215 })];
216 let used = tools_used(&msgs);
217 assert!(used.contains(&"Bash".to_string()));
218 assert!(used.contains(&"FileRead".to_string()));
219 assert_eq!(used.len(), 2); }
221
222 #[test]
223 fn test_empty_messages() {
224 assert_eq!(message_counts(&[]), (0, 0, 0));
225 assert_eq!(tool_use_count(&[]), 0);
226 assert!(extract_text(&[]).is_empty());
227 assert_eq!(last_user_message_index(&[]), None);
228 assert_eq!(last_assistant_index(&[]), None);
229 }
230}