Skip to main content

vtcode_core/llm/
tool_bridge.rs

1//! Bridge between messages and tool executions
2//!
3//! Links LLM messages to their tool executions and tracks intent fulfillment.
4
5use 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/// Tracks intent fulfillment
14#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
15pub enum IntentFulfillment {
16    /// Message goal completely achieved
17    Fulfilled,
18
19    /// Message goal partially achieved
20    PartiallyFulfilled,
21
22    /// Tools executed but results inconclusive
23    Attempted,
24
25    /// Tools failed or results contradicted intent
26    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/// Tool execution record tied to message
42#[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    /// Did this tool help fulfill the intent?
50    pub contributed_to_intent: bool,
51}
52
53/// Stated intent extracted from message
54#[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/// Correlation between message intent and tool execution
74#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct MessageToolCorrelation {
76    /// Unique message identifier
77    pub message_id: String,
78
79    /// Extracted intent from message
80    pub stated_intent: ToolIntent,
81
82    /// Original message text
83    pub message_text: String,
84
85    /// Tools executed to fulfill this message
86    pub tool_executions: Vec<ToolExecution>,
87
88    /// Overall success of fulfilling stated intent
89    pub intent_fulfillment: IntentFulfillment,
90
91    /// Confidence in fulfillment assessment (0.0-1.0)
92    pub confidence: f32,
93
94    /// Any issues encountered
95    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    /// Add a tool execution
112    pub fn add_execution(&mut self, execution: ToolExecution) {
113        self.tool_executions.push(execution);
114        self.reassess_fulfillment();
115    }
116
117    /// Add an issue
118    pub fn add_issue(&mut self, issue: String) {
119        self.issues.push(issue);
120        self.reassess_fulfillment();
121    }
122
123    /// Reassess whether intent was fulfilled
124    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        // Count contributing executions
132        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    /// Get summary of tool execution
158    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
173/// Extractor for tool intents from messages
174pub struct ToolIntentExtractor;
175
176impl ToolIntentExtractor {
177    /// Extract intent from message text
178    pub fn extract(text: &str) -> Option<ToolIntent> {
179        let text_lower = text.to_lowercase();
180
181        // Search patterns
182        if let Some(intent) = extract_search_intent(&text_lower) {
183            return Some(intent);
184        }
185
186        // Execute patterns
187        if let Some(intent) = extract_execute_intent(&text_lower) {
188            return Some(intent);
189        }
190
191        // Analyze patterns
192        if let Some(intent) = extract_analyze_intent(&text_lower) {
193            return Some(intent);
194        }
195
196        // Modify patterns
197        if let Some(intent) = extract_modify_intent(&text_lower) {
198            return Some(intent);
199        }
200
201        None
202    }
203}
204
205/// Extract search intent
206fn 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            // Try to extract what we're searching for
214            if let Some(pattern) = extract_quoted_string(text) {
215                return Some(ToolIntent::Search(pattern));
216            }
217
218            // Fallback: use keyword
219            return Some(ToolIntent::Search(keyword.to_string()));
220        }
221    }
222
223    None
224}
225
226/// Extract execute intent
227fn 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            // Try to extract command
235            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
246/// Extract analyze intent
247fn 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
263/// Extract modify intent
264fn 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
280/// Extract quoted string from text
281fn extract_quoted_string(text: &str) -> Option<String> {
282    // Look for "quoted" or 'quoted' strings
283    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
309/// Track correlations across a session
310pub struct MessageCorrelationTracker {
311    correlations: Vec<MessageToolCorrelation>,
312}
313
314impl MessageCorrelationTracker {
315    pub fn new() -> Self {
316        Self {
317            correlations: vec![],
318        }
319    }
320
321    /// Add a correlation
322    pub fn add(&mut self, correlation: MessageToolCorrelation) {
323        self.correlations.push(correlation);
324    }
325
326    /// Get all correlations
327    pub fn all(&self) -> &[MessageToolCorrelation] {
328        &self.correlations
329    }
330
331    /// Get unfulfilled intents
332    pub fn unfulfilled(&self) -> Vec<&MessageToolCorrelation> {
333        self.correlations
334            .iter()
335            .filter(|c| c.intent_fulfillment == IntentFulfillment::Failed)
336            .collect()
337    }
338
339    /// Get fulfillment statistics
340    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}