Skip to main content

mockforge_intelligence/intelligent_behavior/
validation_generator.rs

1//! AI-driven validation error generation
2//!
3//! This module generates realistic, context-aware validation error messages
4//! using LLMs, learning from example error responses to create human-like
5//! error messages.
6
7use super::config::BehaviorModelConfig;
8use super::llm_client::LlmClient;
9use super::mutation_analyzer::{ValidationIssue, ValidationIssueType, ValidationSeverity};
10use super::types::LlmGenerationRequest;
11use mockforge_foundation::Result;
12use serde::{Deserialize, Serialize};
13use serde_json::Value;
14use std::collections::HashMap;
15
16/// Example error response for learning validation error formats
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct ValidationErrorExample {
19    /// Field that caused the error (if applicable)
20    pub field: Option<String>,
21    /// Error type
22    pub error_type: String,
23    /// Error message
24    pub message: String,
25    /// Error response body
26    pub response: Value,
27    /// HTTP status code
28    pub status_code: u16,
29}
30
31/// Request context for error generation
32#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct RequestContext {
34    /// HTTP method
35    pub method: String,
36    /// Request path
37    pub path: String,
38    /// Request body
39    pub request_body: Option<Value>,
40    /// Query parameters
41    #[serde(default)]
42    pub query_params: HashMap<String, String>,
43    /// Headers
44    #[serde(default)]
45    pub headers: HashMap<String, String>,
46}
47
48/// Validation error response
49#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct ValidationErrorResponse {
51    /// HTTP status code
52    pub status_code: u16,
53    /// Error response body
54    pub body: Value,
55    /// Error format (field-level, object-level, custom)
56    pub format: ErrorFormat,
57}
58
59/// Error response format
60#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
61#[serde(rename_all = "lowercase")]
62pub enum ErrorFormat {
63    /// Field-level errors (each field has its own error)
64    FieldLevel,
65    /// Object-level error (single error message)
66    ObjectLevel,
67    /// Custom format
68    Custom,
69}
70
71/// Field-level error
72#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct FieldError {
74    /// Field name
75    pub field: String,
76    /// Error message
77    pub message: String,
78    /// Error code (optional)
79    pub code: Option<String>,
80    /// Rejected value (optional)
81    pub rejected_value: Option<Value>,
82}
83
84/// Validation error generator
85pub struct ValidationGenerator {
86    /// LLM client for generating error messages
87    llm_client: Option<LlmClient>,
88    /// Configuration
89    #[allow(dead_code)]
90    config: BehaviorModelConfig,
91    /// Learned error examples
92    error_examples: Vec<ValidationErrorExample>,
93}
94
95impl ValidationGenerator {
96    /// Create a new validation generator
97    pub fn new(config: BehaviorModelConfig) -> Self {
98        let llm_client = if config.llm_provider != "disabled" {
99            Some(LlmClient::new(config.clone()))
100        } else {
101            None
102        };
103
104        Self {
105            llm_client,
106            config,
107            error_examples: Vec::new(),
108        }
109    }
110
111    /// Learn from an error example
112    pub fn learn_from_example(&mut self, example: ValidationErrorExample) {
113        self.error_examples.push(example);
114    }
115
116    /// Generate validation error response
117    ///
118    /// Creates a realistic, context-aware validation error based on the
119    /// validation issue and request context.
120    pub async fn generate_validation_error(
121        &self,
122        issue: &ValidationIssue,
123        context: &RequestContext,
124    ) -> Result<ValidationErrorResponse> {
125        // Determine error format based on issue
126        let format = self.determine_error_format(issue);
127
128        // Generate error message
129        let error_message = self.format_error_message(issue, context).await?;
130
131        // Build error response body
132        let body = match format {
133            ErrorFormat::FieldLevel => {
134                self.build_field_level_error(issue, &error_message, context).await?
135            }
136            ErrorFormat::ObjectLevel => {
137                self.build_object_level_error(issue, &error_message, context).await?
138            }
139            ErrorFormat::Custom => self.build_custom_error(issue, &error_message, context).await?,
140        };
141
142        Ok(ValidationErrorResponse {
143            status_code: self.determine_status_code(issue),
144            body,
145            format,
146        })
147    }
148
149    /// Generate field-level error
150    pub async fn generate_field_error(
151        &self,
152        field: &str,
153        issue: &ValidationIssue,
154        context: &RequestContext,
155    ) -> Result<FieldError> {
156        let message = self.format_error_message(issue, context).await?;
157
158        // Extract rejected value from request if available
159        let rejected_value =
160            context.request_body.as_ref().and_then(|body| body.get(field)).cloned();
161
162        Ok(FieldError {
163            field: field.to_string(),
164            message,
165            code: Some(self.generate_error_code(issue)),
166            rejected_value,
167        })
168    }
169
170    /// Format error message using LLM or templates
171    async fn format_error_message(
172        &self,
173        issue: &ValidationIssue,
174        _context: &RequestContext,
175    ) -> Result<String> {
176        // First, try to find similar examples
177        if let Some(similar_example) = self.find_similar_example(issue, &self.error_examples) {
178            // Use similar example's message as template
179            return Ok(similar_example.message.clone());
180        }
181
182        // If LLM is available, generate message
183        if let Some(ref _llm_client) = self.llm_client {
184            return self.generate_message_with_llm(issue).await;
185        }
186
187        // Fallback to template-based message
188        Ok(self.generate_template_message(issue))
189    }
190
191    // ===== Private helper methods =====
192
193    /// Determine error format based on issue
194    fn determine_error_format(&self, issue: &ValidationIssue) -> ErrorFormat {
195        // If field is specified, use field-level format
196        if issue.field.is_some() {
197            return ErrorFormat::FieldLevel;
198        }
199
200        // Otherwise, use object-level
201        ErrorFormat::ObjectLevel
202    }
203
204    /// Build field-level error response
205    async fn build_field_level_error(
206        &self,
207        issue: &ValidationIssue,
208        message: &str,
209        _context: &RequestContext,
210    ) -> Result<Value> {
211        let field = issue.field.as_deref().unwrap_or("unknown");
212
213        // Standard field-level error format
214        Ok(serde_json::json!({
215            "error": {
216                "type": "validation_error",
217                "message": "Validation failed",
218                "fields": {
219                    field: {
220                        "message": message,
221                        "code": self.generate_error_code(issue),
222                        "type": format!("{:?}", issue.issue_type).to_lowercase()
223                    }
224                }
225            }
226        }))
227    }
228
229    /// Build object-level error response
230    async fn build_object_level_error(
231        &self,
232        issue: &ValidationIssue,
233        message: &str,
234        _context: &RequestContext,
235    ) -> Result<Value> {
236        Ok(serde_json::json!({
237            "error": {
238                "type": "validation_error",
239                "message": message,
240                "code": self.generate_error_code(issue)
241            }
242        }))
243    }
244
245    /// Build custom error response
246    async fn build_custom_error(
247        &self,
248        issue: &ValidationIssue,
249        message: &str,
250        context: &RequestContext,
251    ) -> Result<Value> {
252        // Use LLM to generate custom format if available
253        if let Some(ref _llm_client) = self.llm_client {
254            return self.generate_custom_format_with_llm(issue, message, context).await;
255        }
256
257        // Fallback to object-level
258        self.build_object_level_error(issue, message, context).await
259    }
260
261    /// Determine HTTP status code from issue
262    fn determine_status_code(&self, issue: &ValidationIssue) -> u16 {
263        match issue.severity {
264            ValidationSeverity::Critical | ValidationSeverity::Error => 400,
265            ValidationSeverity::Warning => 422,
266            ValidationSeverity::Info => 200, // Info doesn't block
267        }
268    }
269
270    /// Generate error code from issue type
271    fn generate_error_code(&self, issue: &ValidationIssue) -> String {
272        match issue.issue_type {
273            ValidationIssueType::Required => "REQUIRED_FIELD".to_string(),
274            ValidationIssueType::Format => "INVALID_FORMAT".to_string(),
275            ValidationIssueType::MinLength => "MIN_LENGTH".to_string(),
276            ValidationIssueType::MaxLength => "MAX_LENGTH".to_string(),
277            ValidationIssueType::Pattern => "INVALID_PATTERN".to_string(),
278            ValidationIssueType::Range => "OUT_OF_RANGE".to_string(),
279            ValidationIssueType::Type => "INVALID_TYPE".to_string(),
280            ValidationIssueType::Custom => "VALIDATION_ERROR".to_string(),
281        }
282    }
283
284    /// Find similar error example
285    fn find_similar_example<'a>(
286        &self,
287        issue: &ValidationIssue,
288        examples: &'a [ValidationErrorExample],
289    ) -> Option<&'a ValidationErrorExample> {
290        examples.iter().find(|ex| {
291            // Match by field if available
292            if let Some(ref field) = issue.field {
293                if let Some(ref ex_field) = ex.field {
294                    if field == ex_field {
295                        return true;
296                    }
297                }
298            }
299
300            // Match by error type
301            ex.error_type == format!("{:?}", issue.issue_type)
302        })
303    }
304
305    /// Generate error message using LLM
306    async fn generate_message_with_llm(&self, issue: &ValidationIssue) -> Result<String> {
307        let llm_client = self
308            .llm_client
309            .as_ref()
310            .ok_or_else(|| mockforge_foundation::Error::internal("LLM client not available"))?;
311
312        let issue_type_str = format!("{:?}", issue.issue_type);
313        let field_str =
314            issue.field.as_ref().map(|f| format!(" for field '{}'", f)).unwrap_or_default();
315
316        let system_prompt = "You are an API error message generator. Generate clear, helpful validation error messages.";
317        let user_prompt = format!(
318            "Generate a validation error message{} for a {} error. \
319             The error should be clear, helpful, and professional. \
320             Return only the error message text, no additional formatting.",
321            field_str, issue_type_str
322        );
323
324        let request = LlmGenerationRequest {
325            system_prompt: system_prompt.to_string(),
326            user_prompt,
327            temperature: 0.3, // Lower temperature for consistent error messages
328            max_tokens: 100,
329            schema: None,
330        };
331
332        let response = llm_client.generate(&request).await?;
333
334        // Extract message from response
335        if let Some(text) = response.as_str() {
336            Ok(text.trim().to_string())
337        } else if let Some(message) = response.get("message").and_then(|m| m.as_str()) {
338            Ok(message.to_string())
339        } else {
340            Ok(self.generate_template_message(issue))
341        }
342    }
343
344    /// Generate template-based error message
345    fn generate_template_message(&self, issue: &ValidationIssue) -> String {
346        let field_str = issue.field.as_ref().map(|f| format!("Field '{}' ", f)).unwrap_or_default();
347
348        match issue.issue_type {
349            ValidationIssueType::Required => {
350                format!("{}is required", field_str)
351            }
352            ValidationIssueType::Format => {
353                format!("{}has an invalid format", field_str)
354            }
355            ValidationIssueType::MinLength => {
356                format!("{}is too short", field_str)
357            }
358            ValidationIssueType::MaxLength => {
359                format!("{}is too long", field_str)
360            }
361            ValidationIssueType::Pattern => {
362                format!("{}does not match the required pattern", field_str)
363            }
364            ValidationIssueType::Range => {
365                format!("{}is out of range", field_str)
366            }
367            ValidationIssueType::Type => {
368                format!("{}has an invalid type", field_str)
369            }
370            ValidationIssueType::Custom => issue.error_message.clone(),
371        }
372    }
373
374    /// Generate custom error format using LLM
375    async fn generate_custom_format_with_llm(
376        &self,
377        issue: &ValidationIssue,
378        message: &str,
379        context: &RequestContext,
380    ) -> Result<Value> {
381        let llm_client = self
382            .llm_client
383            .as_ref()
384            .ok_or_else(|| mockforge_foundation::Error::internal("LLM client not available"))?;
385
386        let system_prompt = "You are an API error response generator. Generate realistic error responses in JSON format.";
387        let user_prompt = format!(
388            "Generate a validation error response for:\n\
389             Method: {}\n\
390             Path: {}\n\
391             Error: {:?}\n\
392             Message: {}\n\n\
393             Return a JSON object with error details. Use a realistic API error format.",
394            context.method, context.path, issue.issue_type, message
395        );
396
397        let request = LlmGenerationRequest {
398            system_prompt: system_prompt.to_string(),
399            user_prompt,
400            temperature: 0.5,
401            max_tokens: 300,
402            schema: None,
403        };
404
405        let response = llm_client.generate(&request).await?;
406
407        // Try to parse as JSON, fallback to wrapping in error object
408        if let Some(obj) = response.as_object() {
409            Ok(Value::Object(obj.clone()))
410        } else {
411            Ok(serde_json::json!({
412                "error": {
413                    "message": message,
414                    "type": format!("{:?}", issue.issue_type)
415                }
416            }))
417        }
418    }
419}
420
421#[cfg(test)]
422mod tests {
423    use super::*;
424    use serde_json::json;
425
426    #[tokio::test]
427    async fn test_generate_template_message() {
428        let config = BehaviorModelConfig::default();
429        let generator = ValidationGenerator::new(config);
430
431        let issue = ValidationIssue {
432            field: Some("email".to_string()),
433            issue_type: ValidationIssueType::Required,
434            severity: ValidationSeverity::Error,
435            context: json!({}),
436            error_message: "".to_string(),
437        };
438
439        let message = generator.generate_template_message(&issue);
440        assert!(message.contains("email"));
441        assert!(message.contains("required"));
442    }
443
444    #[tokio::test]
445    async fn test_determine_error_format() {
446        let config = BehaviorModelConfig::default();
447        let generator = ValidationGenerator::new(config);
448
449        let field_issue = ValidationIssue {
450            field: Some("email".to_string()),
451            issue_type: ValidationIssueType::Required,
452            severity: ValidationSeverity::Error,
453            context: json!({}),
454            error_message: "".to_string(),
455        };
456
457        assert_eq!(generator.determine_error_format(&field_issue), ErrorFormat::FieldLevel);
458
459        let object_issue = ValidationIssue {
460            field: None,
461            issue_type: ValidationIssueType::Required,
462            severity: ValidationSeverity::Error,
463            context: json!({}),
464            error_message: "".to_string(),
465        };
466
467        assert_eq!(generator.determine_error_format(&object_issue), ErrorFormat::ObjectLevel);
468    }
469
470    #[tokio::test]
471    async fn test_generate_error_code() {
472        let config = BehaviorModelConfig::default();
473        let generator = ValidationGenerator::new(config);
474
475        let issue = ValidationIssue {
476            field: Some("email".to_string()),
477            issue_type: ValidationIssueType::Format,
478            severity: ValidationSeverity::Error,
479            context: json!({}),
480            error_message: "".to_string(),
481        };
482
483        let code = generator.generate_error_code(&issue);
484        assert_eq!(code, "INVALID_FORMAT");
485    }
486}