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