1use serde::{Deserialize, Serialize};
6use serde_json::Value;
7use std::fmt;
8
9#[cfg(test)]
10use crate::config::constants::tools;
11use crate::tools::result_metadata::EnhancedToolResult;
12
13#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
15pub enum IntentFulfillment {
16 Fulfilled,
18
19 PartiallyFulfilled,
21
22 Attempted,
24
25 Failed,
27}
28
29impl fmt::Display for IntentFulfillment {
30 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
31 let s = match self {
32 Self::Fulfilled => "fulfilled",
33 Self::PartiallyFulfilled => "partially_fulfilled",
34 Self::Attempted => "attempted",
35 Self::Failed => "failed",
36 };
37 f.write_str(s)
38 }
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct ToolExecution {
44 pub tool_name: String,
45 pub args: Value,
46 pub result: EnhancedToolResult,
47 pub duration_ms: u64,
48
49 pub contributed_to_intent: bool,
51}
52
53#[derive(Debug, Clone, Serialize, Deserialize)]
55pub enum ToolIntent {
56 Search(String),
57 Execute(String),
58 Analyze(String),
59 Modify(String),
60}
61
62impl fmt::Display for ToolIntent {
63 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
64 match self {
65 Self::Search(s) => write!(f, "search: {}", s),
66 Self::Execute(s) => write!(f, "execute: {}", s),
67 Self::Analyze(s) => write!(f, "analyze: {}", s),
68 Self::Modify(s) => write!(f, "modify: {}", s),
69 }
70 }
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct MessageToolCorrelation {
76 pub message_id: String,
78
79 pub stated_intent: ToolIntent,
81
82 pub message_text: String,
84
85 pub tool_executions: Vec<ToolExecution>,
87
88 pub intent_fulfillment: IntentFulfillment,
90
91 pub confidence: f32,
93
94 pub issues: Vec<String>,
96}
97
98impl MessageToolCorrelation {
99 pub fn new(message_id: String, message_text: String, intent: ToolIntent) -> Self {
100 Self {
101 message_id,
102 stated_intent: intent,
103 message_text,
104 tool_executions: vec![],
105 intent_fulfillment: IntentFulfillment::Attempted,
106 confidence: 0.0,
107 issues: vec![],
108 }
109 }
110
111 pub fn add_execution(&mut self, execution: ToolExecution) {
113 self.tool_executions.push(execution);
114 self.reassess_fulfillment();
115 }
116
117 pub fn add_issue(&mut self, issue: String) {
119 self.issues.push(issue);
120 self.reassess_fulfillment();
121 }
122
123 fn reassess_fulfillment(&mut self) {
125 if self.tool_executions.is_empty() {
126 self.intent_fulfillment = IntentFulfillment::Failed;
127 self.confidence = 0.0;
128 return;
129 }
130
131 let contributing = self
133 .tool_executions
134 .iter()
135 .filter(|e| e.contributed_to_intent)
136 .count();
137
138 let avg_quality = self
139 .tool_executions
140 .iter()
141 .map(|e| e.result.metadata.quality_score())
142 .sum::<f32>()
143 / self.tool_executions.len() as f32;
144
145 self.intent_fulfillment = match (contributing, avg_quality) {
146 (n, q) if n == self.tool_executions.len() && q > 0.75 => IntentFulfillment::Fulfilled,
147 (n, q) if n > self.tool_executions.len() / 2 && q > 0.6 => {
148 IntentFulfillment::PartiallyFulfilled
149 }
150 (0, _) => IntentFulfillment::Failed,
151 _ => IntentFulfillment::Attempted,
152 };
153
154 self.confidence = (contributing as f32 / self.tool_executions.len() as f32) * avg_quality;
155 }
156
157 pub fn summary(&self) -> String {
159 format!(
160 "Intent: {} | Tools: {} | Fulfillment: {} (confidence: {:.0}%)",
161 self.stated_intent,
162 self.tool_executions
163 .iter()
164 .map(|e| e.tool_name.clone())
165 .collect::<Vec<_>>()
166 .join(", "),
167 self.intent_fulfillment,
168 self.confidence * 100.0
169 )
170 }
171}
172
173pub struct ToolIntentExtractor;
175
176impl ToolIntentExtractor {
177 pub fn extract(text: &str) -> Option<ToolIntent> {
179 let text_lower = text.to_lowercase();
180
181 if let Some(intent) = extract_search_intent(&text_lower) {
183 return Some(intent);
184 }
185
186 if let Some(intent) = extract_execute_intent(&text_lower) {
188 return Some(intent);
189 }
190
191 if let Some(intent) = extract_analyze_intent(&text_lower) {
193 return Some(intent);
194 }
195
196 if let Some(intent) = extract_modify_intent(&text_lower) {
198 return Some(intent);
199 }
200
201 None
202 }
203}
204
205fn extract_search_intent(text: &str) -> Option<ToolIntent> {
207 let search_keywords = [
208 "grep", "search", "find", "look for", "locate", "check if", "does", "exist",
209 ];
210
211 for keyword in &search_keywords {
212 if text.contains(keyword) {
213 if let Some(pattern) = extract_quoted_string(text) {
215 return Some(ToolIntent::Search(pattern));
216 }
217
218 return Some(ToolIntent::Search(keyword.to_string()));
220 }
221 }
222
223 None
224}
225
226fn extract_execute_intent(text: &str) -> Option<ToolIntent> {
228 let execute_keywords = [
229 "run", "execute", "command", "cargo", "npm", "python", "bash", "sh",
230 ];
231
232 for keyword in &execute_keywords {
233 if text.contains(keyword) {
234 if let Some(cmd) = extract_quoted_string(text) {
236 return Some(ToolIntent::Execute(cmd));
237 }
238
239 return Some(ToolIntent::Execute(keyword.to_string()));
240 }
241 }
242
243 None
244}
245
246fn extract_analyze_intent(text: &str) -> Option<ToolIntent> {
248 let analyze_keywords = ["analyze", "check", "review", "examine", "inspect", "parse"];
249
250 for keyword in &analyze_keywords {
251 if text.contains(keyword) {
252 if let Some(target) = extract_quoted_string(text) {
253 return Some(ToolIntent::Analyze(target));
254 }
255
256 return Some(ToolIntent::Analyze(keyword.to_string()));
257 }
258 }
259
260 None
261}
262
263fn extract_modify_intent(text: &str) -> Option<ToolIntent> {
265 let modify_keywords = ["edit", "modify", "change", "fix", "apply", "patch"];
266
267 for keyword in &modify_keywords {
268 if text.contains(keyword) {
269 if let Some(target) = extract_quoted_string(text) {
270 return Some(ToolIntent::Modify(target));
271 }
272
273 return Some(ToolIntent::Modify(keyword.to_string()));
274 }
275 }
276
277 None
278}
279
280fn extract_quoted_string(text: &str) -> Option<String> {
282 let mut in_quote = false;
284 let mut quote_char = ' ';
285 let mut current = String::new();
286
287 for c in text.chars() {
288 match c {
289 '"' | '\'' if !in_quote => {
290 in_quote = true;
291 quote_char = c;
292 }
293 c if in_quote && c == quote_char => {
294 in_quote = false;
295 if !current.is_empty() {
296 return Some(current);
297 }
298 }
299 c if in_quote => {
300 current.push(c);
301 }
302 _ => {}
303 }
304 }
305
306 None
307}
308
309pub struct MessageCorrelationTracker {
311 correlations: Vec<MessageToolCorrelation>,
312}
313
314impl MessageCorrelationTracker {
315 pub fn new() -> Self {
316 Self {
317 correlations: vec![],
318 }
319 }
320
321 pub fn add(&mut self, correlation: MessageToolCorrelation) {
323 self.correlations.push(correlation);
324 }
325
326 pub fn all(&self) -> &[MessageToolCorrelation] {
328 &self.correlations
329 }
330
331 pub fn unfulfilled(&self) -> Vec<&MessageToolCorrelation> {
333 self.correlations
334 .iter()
335 .filter(|c| c.intent_fulfillment == IntentFulfillment::Failed)
336 .collect()
337 }
338
339 pub fn stats(&self) -> CorrelationStats {
341 let total = self.correlations.len();
342 let fulfilled = self
343 .correlations
344 .iter()
345 .filter(|c| c.intent_fulfillment == IntentFulfillment::Fulfilled)
346 .count();
347 let partially_fulfilled = self
348 .correlations
349 .iter()
350 .filter(|c| c.intent_fulfillment == IntentFulfillment::PartiallyFulfilled)
351 .count();
352 let failed = self
353 .correlations
354 .iter()
355 .filter(|c| c.intent_fulfillment == IntentFulfillment::Failed)
356 .count();
357
358 let avg_confidence = if total > 0 {
359 self.correlations.iter().map(|c| c.confidence).sum::<f32>() / total as f32
360 } else {
361 0.0
362 };
363
364 CorrelationStats {
365 total,
366 fulfilled,
367 partially_fulfilled,
368 attempted: total - fulfilled - partially_fulfilled - failed,
369 failed,
370 avg_confidence,
371 }
372 }
373}
374
375impl Default for MessageCorrelationTracker {
376 fn default() -> Self {
377 Self::new()
378 }
379}
380
381#[derive(Debug, Clone, Serialize, Deserialize)]
382pub struct CorrelationStats {
383 pub total: usize,
384 pub fulfilled: usize,
385 pub partially_fulfilled: usize,
386 pub attempted: usize,
387 pub failed: usize,
388 pub avg_confidence: f32,
389}
390
391#[cfg(test)]
392mod tests {
393 use super::*;
394 use crate::tools::result_metadata::ResultMetadata;
395
396 #[test]
397 fn test_intent_extraction_search() {
398 let text = "Let me grep for 'error' in the logs";
399 let intent = ToolIntentExtractor::extract(text);
400
401 assert!(matches!(intent, Some(ToolIntent::Search(_))));
402 }
403
404 #[test]
405 fn test_intent_extraction_execute() {
406 let text = "Run 'cargo test' to check";
407 let intent = ToolIntentExtractor::extract(text);
408
409 assert!(matches!(intent, Some(ToolIntent::Execute(_))));
410 }
411
412 #[test]
413 fn test_intent_extraction_analyze() {
414 let text = "Analyze the config file please";
415 let intent = ToolIntentExtractor::extract(text);
416
417 assert!(matches!(intent, Some(ToolIntent::Analyze(_))));
418 }
419
420 #[test]
421 fn test_message_correlation() {
422 let mut corr = MessageToolCorrelation::new(
423 "msg-1".to_owned(),
424 "Let me grep for errors".to_owned(),
425 ToolIntent::Search("errors".to_owned()),
426 );
427
428 let exec = ToolExecution {
429 tool_name: tools::GREP_FILE.to_owned(),
430 args: Value::Null,
431 result: EnhancedToolResult::new(
432 Value::Null,
433 ResultMetadata::success(0.9, 0.9),
434 tools::GREP_FILE.to_owned(),
435 ),
436 duration_ms: 100,
437 contributed_to_intent: true,
438 };
439
440 corr.add_execution(exec);
441
442 assert!(matches!(
443 corr.intent_fulfillment,
444 IntentFulfillment::PartiallyFulfilled
445 ));
446 }
447
448 #[test]
449 fn test_correlation_tracker() {
450 let mut tracker = MessageCorrelationTracker::new();
451
452 let corr = MessageToolCorrelation::new(
453 "msg-1".to_owned(),
454 "test".to_owned(),
455 ToolIntent::Search("test".to_owned()),
456 );
457
458 tracker.add(corr);
459
460 let stats = tracker.stats();
461 assert_eq!(stats.total, 1);
462 }
463
464 #[test]
465 fn test_extract_quoted_string() {
466 assert_eq!(
467 extract_quoted_string("grep for \"error pattern\""),
468 Some("error pattern".to_owned())
469 );
470 assert_eq!(
471 extract_quoted_string("find 'test.rs'"),
472 Some("test.rs".to_owned())
473 );
474 }
475}