Skip to main content

vtcode_core/skills/
auto_verification.rs

1//! # Auto Skill Verification
2//!
3//! Automatically enhances ALL skill outputs with file verification.
4//! This is the generic layer that works for pdf-generator-vtcode, spreadsheet-generator,
5//! and any other skill without requiring skill-specific code.
6
7use crate::skills::skill_file_tracker::SkillFileTracker;
8use anyhow::Result;
9use serde_json::Value;
10use std::path::PathBuf;
11use tracing::{debug, info};
12
13/// Auto-verification wrapper for skill outputs
14pub struct AutoSkillVerifier {
15    tracker: SkillFileTracker,
16    enabled: bool,
17}
18
19impl AutoSkillVerifier {
20    pub fn new(workspace_root: PathBuf) -> Self {
21        Self {
22            tracker: SkillFileTracker::new(workspace_root),
23            enabled: true,
24        }
25    }
26
27    /// Enable or disable auto-verification
28    pub fn set_enabled(&mut self, enabled: bool) {
29        self.enabled = enabled;
30    }
31
32    /// Process skill output and add file verification (generic for ALL skills)
33    pub async fn process_skill_output(&self, skill_name: &str, original_output: String) -> Result<String> {
34        if !self.enabled {
35            return Ok(original_output);
36        }
37
38        info!("Auto-verifying skill output for: {}", skill_name);
39
40        // Check if output already contains verification (prevent double-verification)
41        if Self::already_verified(&original_output) {
42            debug!("Skill output already contains verification, skipping");
43            return Ok(original_output);
44        }
45
46        // Enhance the output with file verification
47        let enhanced = self.tracker.enhance_skill_output(original_output.clone()).await?;
48
49        // Add skill-specific header
50        let final_output = if enhanced.len() > original_output.len() {
51            // Files were detected and verification added
52            let verification = enhanced.strip_prefix(&format!("{original_output}\n\n")).unwrap_or(&enhanced);
53
54            format!(
55                "āœ“ Skill '{}' executed\n\n{}{}",
56                skill_name,
57                original_output,
58                if !verification.is_empty() {
59                    format!("\n\n{verification}")
60                } else {
61                    String::new()
62                }
63            )
64        } else {
65            // No files detected, return original
66            enhanced
67        };
68
69        Ok(final_output)
70    }
71
72    /// Process JSON skill result and enhance it
73    pub async fn process_skill_result(&self, skill_name: &str, mut result: Value) -> Result<Value> {
74        if !self.enabled {
75            return Ok(result);
76        }
77
78        // Extract text output from various skill result formats
79        let output_text = Self::extract_output_text(&result);
80
81        if let Some(text) = output_text {
82            let enhanced = self.process_skill_output(skill_name, text).await?;
83
84            // Update the result with enhanced output
85            if let Some(output_field) = result.get_mut("output") {
86                *output_field = Value::String(enhanced);
87            } else if let Some(message_field) = result.get_mut("message") {
88                *message_field = Value::String(enhanced);
89            } else {
90                result["enhanced_output"] = Value::String(enhanced);
91            }
92        }
93
94        Ok(result)
95    }
96
97    /// Check if output already contains verification
98    fn already_verified(output: &str) -> bool {
99        output.contains("Generated Files:")
100            || output.contains("Missing Files:")
101            || output.contains("āœ“ Generated:")
102            || output.contains("File generated at:")
103    }
104
105    /// Extract output text from various skill result formats
106    fn extract_output_text(result: &Value) -> Option<String> {
107        // Check common output fields
108        if let Some(output) = result.get("output").and_then(|v| v.as_str()) {
109            return Some(output.to_string());
110        }
111
112        if let Some(message) = result.get("message").and_then(|v| v.as_str()) {
113            return Some(message.to_string());
114        }
115
116        if let Some(result_str) = result.get("result").and_then(|v| v.as_str()) {
117            return Some(result_str.to_string());
118        }
119
120        // Fallback: stringify the entire result
121        Some(serde_json::to_string_pretty(result).unwrap_or_default())
122    }
123
124    /// Create a standard success response with verification
125    pub async fn create_success_response(
126        &self,
127        skill_name: &str,
128        details: &str,
129        output_hint: Option<&str>,
130    ) -> Result<String> {
131        let mut response = format!("āœ“ Skill '{skill_name}' executed successfully\n\n{details}");
132
133        if let Some(hint) = output_hint {
134            // Scan the hint for files
135            let verification = self.tracker.scan_and_verify_skill_output(hint).await?;
136
137            if !verification.verified_files.is_empty() || !verification.missing_files.is_empty() {
138                response.push_str("\n\n");
139                response.push_str(&verification.summary);
140            }
141        }
142
143        Ok(response)
144    }
145
146    /// Create error response with helpful suggestions
147    pub fn create_error_response(skill_name: &str, error: &str) -> String {
148        format!(
149            "āŒ Skill '{skill_name}' failed\n\nError: {error}\n\nšŸ’” Try:\n   • Verify the skill is properly installed\n   • Check that all dependencies are available\n   • Ensure you have the required permissions"
150        )
151    }
152}
153
154// Note: Global instance intentionally omitted to avoid static mut warnings.
155// Use AutoSkillVerifier::new() to create local instances as needed.
156
157#[cfg(test)]
158mod tests {
159    use super::*;
160    use tempfile::TempDir;
161
162    #[tokio::test]
163    async fn test_auto_verifier_creation() {
164        let temp_dir = TempDir::new().unwrap();
165        let verifier = AutoSkillVerifier::new(temp_dir.path().to_path_buf());
166        assert!(verifier.enabled);
167    }
168
169    #[tokio::test]
170    async fn test_process_skill_output() {
171        let temp_dir = TempDir::new().unwrap();
172        let verifier = AutoSkillVerifier::new(temp_dir.path().to_path_buf());
173
174        let output = "Generated report.pdf".to_string();
175        let enhanced = verifier.process_skill_output("test-skill", output).await.unwrap();
176
177        assert!(enhanced.contains("test-skill"));
178        assert!(enhanced.contains("Generated report.pdf"));
179    }
180
181    #[tokio::test]
182    async fn test_already_verified_detection() {
183        let output = "File generated at: test.pdf";
184        assert!(AutoSkillVerifier::already_verified(output));
185
186        let output = "Some random text";
187        assert!(!AutoSkillVerifier::already_verified(output));
188    }
189
190    #[tokio::test]
191    async fn test_extract_output_text() {
192        let json = serde_json::json!({
193            "output": "Generated file.pdf"
194        });
195        assert_eq!(AutoSkillVerifier::extract_output_text(&json), Some("Generated file.pdf".to_string()));
196
197        let json_str = serde_json::json!("Plain string output");
198        assert!(AutoSkillVerifier::extract_output_text(&json_str).is_some());
199    }
200}