1use serde::{Deserialize, Serialize};
6use serde_json::Value;
7use std::fmt;
8
9use vtcode_commons::tool_types::EnhancedToolResult;
10#[cfg(test)]
11use vtcode_config::constants::tools;
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 tool_name: String,
45 args: Value,
46 result: EnhancedToolResult,
47 duration_ms: u64,
48
49 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 message_id: String,
78
79 stated_intent: ToolIntent,
81
82 message_text: String,
84
85 tool_executions: Vec<ToolExecution>,
87
88 intent_fulfillment: IntentFulfillment,
90
91 confidence: f32,
93
94 issues: Vec<String>,
96}
97
98impl MessageToolCorrelation {
99 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 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.tool_executions.iter().filter(|e| e.contributed_to_intent).count();
133
134 let avg_quality = self
135 .tool_executions
136 .iter()
137 .map(|e| e.result.metadata.quality_score())
138 .sum::<f32>()
139 / self.tool_executions.len() as f32;
140
141 self.intent_fulfillment = match (contributing, avg_quality) {
142 (n, q) if n == self.tool_executions.len() && q > 0.75 => IntentFulfillment::Fulfilled,
143 (n, q) if n > self.tool_executions.len() / 2 && q > 0.6 => IntentFulfillment::PartiallyFulfilled,
144 (0, _) => IntentFulfillment::Failed,
145 _ => IntentFulfillment::Attempted,
146 };
147
148 self.confidence = (contributing as f32 / self.tool_executions.len() as f32) * avg_quality;
149 }
150
151 pub fn summary(&self) -> String {
153 format!(
154 "Intent: {} | Tools: {} | Fulfillment: {} (confidence: {:.0}%)",
155 self.stated_intent,
156 self.tool_executions
157 .iter()
158 .map(|e| e.tool_name.clone())
159 .collect::<Vec<_>>()
160 .join(", "),
161 self.intent_fulfillment,
162 self.confidence * 100.0
163 )
164 }
165}
166
167pub struct ToolIntentExtractor;
169
170impl ToolIntentExtractor {
171 fn extract(text: &str) -> Option<ToolIntent> {
173 let text_lower = text.to_lowercase();
174
175 if let Some(intent) = extract_search_intent(&text_lower) {
177 return Some(intent);
178 }
179
180 if let Some(intent) = extract_execute_intent(&text_lower) {
182 return Some(intent);
183 }
184
185 if let Some(intent) = extract_analyze_intent(&text_lower) {
187 return Some(intent);
188 }
189
190 if let Some(intent) = extract_modify_intent(&text_lower) {
192 return Some(intent);
193 }
194
195 None
196 }
197}
198
199fn extract_search_intent(text: &str) -> Option<ToolIntent> {
201 let search_keywords = [
202 "grep", "search", "find", "look for", "locate", "check if", "does", "exist",
203 ];
204
205 for keyword in &search_keywords {
206 if text.contains(keyword) {
207 if let Some(pattern) = extract_quoted_string(text) {
209 return Some(ToolIntent::Search(pattern));
210 }
211
212 return Some(ToolIntent::Search(keyword.to_string()));
214 }
215 }
216
217 None
218}
219
220fn extract_execute_intent(text: &str) -> Option<ToolIntent> {
222 let execute_keywords = ["run", "execute", "command", "cargo", "npm", "python", "bash", "sh"];
223
224 for keyword in &execute_keywords {
225 if text.contains(keyword) {
226 if let Some(cmd) = extract_quoted_string(text) {
228 return Some(ToolIntent::Execute(cmd));
229 }
230
231 return Some(ToolIntent::Execute(keyword.to_string()));
232 }
233 }
234
235 None
236}
237
238fn extract_analyze_intent(text: &str) -> Option<ToolIntent> {
240 let analyze_keywords = ["analyze", "check", "review", "examine", "inspect", "parse"];
241
242 for keyword in &analyze_keywords {
243 if text.contains(keyword) {
244 if let Some(target) = extract_quoted_string(text) {
245 return Some(ToolIntent::Analyze(target));
246 }
247
248 return Some(ToolIntent::Analyze(keyword.to_string()));
249 }
250 }
251
252 None
253}
254
255fn extract_modify_intent(text: &str) -> Option<ToolIntent> {
257 let modify_keywords = ["edit", "modify", "change", "fix", "apply", "patch"];
258
259 for keyword in &modify_keywords {
260 if text.contains(keyword) {
261 if let Some(target) = extract_quoted_string(text) {
262 return Some(ToolIntent::Modify(target));
263 }
264
265 return Some(ToolIntent::Modify(keyword.to_string()));
266 }
267 }
268
269 None
270}
271
272fn extract_quoted_string(text: &str) -> Option<String> {
274 let mut in_quote = false;
276 let mut quote_char = ' ';
277 let mut current = String::new();
278
279 for c in text.chars() {
280 match c {
281 '"' | '\'' if !in_quote => {
282 in_quote = true;
283 quote_char = c;
284 }
285 c if in_quote && c == quote_char => {
286 in_quote = false;
287 if !current.is_empty() {
288 return Some(current);
289 }
290 }
291 c if in_quote => {
292 current.push(c);
293 }
294 _ => {}
295 }
296 }
297
298 None
299}
300
301pub struct MessageCorrelationTracker {
303 correlations: Vec<MessageToolCorrelation>,
304}
305
306impl MessageCorrelationTracker {
307 fn new() -> Self {
308 Self { correlations: vec![] }
309 }
310
311 fn add(&mut self, correlation: MessageToolCorrelation) {
313 self.correlations.push(correlation);
314 }
315
316 pub fn all(&self) -> &[MessageToolCorrelation] {
318 &self.correlations
319 }
320
321 pub fn unfulfilled(&self) -> Vec<&MessageToolCorrelation> {
323 self.correlations
324 .iter()
325 .filter(|c| c.intent_fulfillment == IntentFulfillment::Failed)
326 .collect()
327 }
328
329 fn stats(&self) -> CorrelationStats {
331 let total = self.correlations.len();
332 let fulfilled = self
333 .correlations
334 .iter()
335 .filter(|c| c.intent_fulfillment == IntentFulfillment::Fulfilled)
336 .count();
337 let partially_fulfilled = self
338 .correlations
339 .iter()
340 .filter(|c| c.intent_fulfillment == IntentFulfillment::PartiallyFulfilled)
341 .count();
342 let failed = self
343 .correlations
344 .iter()
345 .filter(|c| c.intent_fulfillment == IntentFulfillment::Failed)
346 .count();
347
348 let avg_confidence = if total > 0 {
349 self.correlations.iter().map(|c| c.confidence).sum::<f32>() / total as f32
350 } else {
351 0.0
352 };
353
354 CorrelationStats {
355 total,
356 fulfilled,
357 partially_fulfilled,
358 attempted: total - fulfilled - partially_fulfilled - failed,
359 failed,
360 avg_confidence,
361 }
362 }
363}
364
365impl Default for MessageCorrelationTracker {
366 fn default() -> Self {
367 Self::new()
368 }
369}
370
371#[derive(Debug, Clone, Serialize, Deserialize)]
372pub struct CorrelationStats {
373 total: usize,
374 fulfilled: usize,
375 partially_fulfilled: usize,
376 attempted: usize,
377 failed: usize,
378 avg_confidence: f32,
379}
380
381#[cfg(test)]
382mod tests {
383 use super::*;
384 use vtcode_commons::tool_types::ResultMetadata;
385
386 #[test]
387 fn test_intent_extraction_search() {
388 let text = "Let me grep for 'error' in the logs";
389 let intent = ToolIntentExtractor::extract(text);
390
391 assert!(matches!(intent, Some(ToolIntent::Search(_))));
392 }
393
394 #[test]
395 fn test_intent_extraction_execute() {
396 let text = "Run 'cargo test' to check";
397 let intent = ToolIntentExtractor::extract(text);
398
399 assert!(matches!(intent, Some(ToolIntent::Execute(_))));
400 }
401
402 #[test]
403 fn test_intent_extraction_analyze() {
404 let text = "Analyze the config file please";
405 let intent = ToolIntentExtractor::extract(text);
406
407 assert!(matches!(intent, Some(ToolIntent::Analyze(_))));
408 }
409
410 #[test]
411 fn test_message_correlation() {
412 let mut corr = MessageToolCorrelation::new(
413 "msg-1".to_owned(),
414 "Let me grep for errors".to_owned(),
415 ToolIntent::Search("errors".to_owned()),
416 );
417
418 let exec = ToolExecution {
419 tool_name: tools::GREP_FILE.to_owned(),
420 args: Value::Null,
421 result: EnhancedToolResult::new(
422 Value::Null,
423 ResultMetadata::success(0.9, 0.9),
424 tools::GREP_FILE.to_owned(),
425 ),
426 duration_ms: 100,
427 contributed_to_intent: true,
428 };
429
430 corr.add_execution(exec);
431
432 assert!(matches!(corr.intent_fulfillment, IntentFulfillment::PartiallyFulfilled));
433 }
434
435 #[test]
436 fn test_correlation_tracker() {
437 let mut tracker = MessageCorrelationTracker::new();
438
439 let corr =
440 MessageToolCorrelation::new("msg-1".to_owned(), "test".to_owned(), ToolIntent::Search("test".to_owned()));
441
442 tracker.add(corr);
443
444 let stats = tracker.stats();
445 assert_eq!(stats.total, 1);
446 }
447
448 #[test]
449 fn test_extract_quoted_string() {
450 assert_eq!(extract_quoted_string("grep for \"error pattern\""), Some("error pattern".to_owned()));
451 assert_eq!(extract_quoted_string("find 'test.rs'"), Some("test.rs".to_owned()));
452 }
453}