Skip to main content

vtcode_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
9use vtcode_commons::tool_types::EnhancedToolResult;
10#[cfg(test)]
11use vtcode_config::constants::tools;
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    tool_name: String,
45    args: Value,
46    result: EnhancedToolResult,
47    duration_ms: u64,
48
49    /// Did this tool help fulfill the intent?
50    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    message_id: String,
78
79    /// Extracted intent from message
80    stated_intent: ToolIntent,
81
82    /// Original message text
83    message_text: String,
84
85    /// Tools executed to fulfill this message
86    tool_executions: Vec<ToolExecution>,
87
88    /// Overall success of fulfilling stated intent
89    intent_fulfillment: IntentFulfillment,
90
91    /// Confidence in fulfillment assessment (0.0-1.0)
92    confidence: f32,
93
94    /// Any issues encountered
95    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    /// Add a tool execution
112    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.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    /// Get summary of tool execution
152    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
167/// Extractor for tool intents from messages
168pub struct ToolIntentExtractor;
169
170impl ToolIntentExtractor {
171    /// Extract intent from message text
172    fn extract(text: &str) -> Option<ToolIntent> {
173        let text_lower = text.to_lowercase();
174
175        // Search patterns
176        if let Some(intent) = extract_search_intent(&text_lower) {
177            return Some(intent);
178        }
179
180        // Execute patterns
181        if let Some(intent) = extract_execute_intent(&text_lower) {
182            return Some(intent);
183        }
184
185        // Analyze patterns
186        if let Some(intent) = extract_analyze_intent(&text_lower) {
187            return Some(intent);
188        }
189
190        // Modify patterns
191        if let Some(intent) = extract_modify_intent(&text_lower) {
192            return Some(intent);
193        }
194
195        None
196    }
197}
198
199/// Extract search intent
200fn 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            // Try to extract what we're searching for
208            if let Some(pattern) = extract_quoted_string(text) {
209                return Some(ToolIntent::Search(pattern));
210            }
211
212            // Fallback: use keyword
213            return Some(ToolIntent::Search(keyword.to_string()));
214        }
215    }
216
217    None
218}
219
220/// Extract execute intent
221fn 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            // Try to extract command
227            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
238/// Extract analyze intent
239fn 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
255/// Extract modify intent
256fn 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
272/// Extract quoted string from text
273fn extract_quoted_string(text: &str) -> Option<String> {
274    // Look for "quoted" or 'quoted' strings
275    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
301/// Track correlations across a session
302pub struct MessageCorrelationTracker {
303    correlations: Vec<MessageToolCorrelation>,
304}
305
306impl MessageCorrelationTracker {
307    fn new() -> Self {
308        Self { correlations: vec![] }
309    }
310
311    /// Add a correlation
312    fn add(&mut self, correlation: MessageToolCorrelation) {
313        self.correlations.push(correlation);
314    }
315
316    /// Get all correlations
317    pub fn all(&self) -> &[MessageToolCorrelation] {
318        &self.correlations
319    }
320
321    /// Get unfulfilled intents
322    pub fn unfulfilled(&self) -> Vec<&MessageToolCorrelation> {
323        self.correlations
324            .iter()
325            .filter(|c| c.intent_fulfillment == IntentFulfillment::Failed)
326            .collect()
327    }
328
329    /// Get fulfillment statistics
330    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}