Skip to main content

mockforge_intelligence/ai_contract_diff/
recommendation_engine.rs

1//! AI-powered recommendation engine for contract diff analysis
2//!
3//! This module uses LLM to generate contextual recommendations for fixing contract mismatches,
4//! going beyond structural diffs to provide intelligent suggestions.
5
6use super::types::{ContractDiffConfig, Mismatch, Recommendation};
7use crate::intelligent_behavior::config::BehaviorModelConfig;
8use crate::intelligent_behavior::llm_client::LlmClient;
9use crate::intelligent_behavior::types::LlmGenerationRequest;
10use mockforge_foundation::Result;
11use std::collections::HashMap;
12
13/// AI-powered recommendation engine
14pub struct RecommendationEngine {
15    /// LLM client for generating recommendations
16    llm_client: Option<LlmClient>,
17
18    /// Configuration
19    config: ContractDiffConfig,
20}
21
22impl RecommendationEngine {
23    /// Create a new recommendation engine
24    pub fn new(config: ContractDiffConfig) -> Result<Self> {
25        let llm_client = if config.use_ai_recommendations {
26            // Create LLM client configuration
27            let llm_config = BehaviorModelConfig {
28                llm_provider: config.llm_provider.clone(),
29                model: config.llm_model.clone(),
30                api_key: config.api_key.clone(),
31                api_endpoint: None,
32                temperature: 0.7, // Lower temperature for more focused recommendations
33                max_tokens: 2000,
34                rules: crate::intelligent_behavior::BehaviorRules::default(), // No specific rules for contract diff recommendations
35                seed: None,
36            };
37
38            Some(LlmClient::new(llm_config))
39        } else {
40            None
41        };
42
43        Ok(Self { llm_client, config })
44    }
45
46    /// Generate recommendations for mismatches
47    pub async fn generate_recommendations(
48        &self,
49        mismatches: &[Mismatch],
50        request_context: &RequestContext,
51    ) -> Result<Vec<Recommendation>> {
52        if !self.config.use_ai_recommendations || self.llm_client.is_none() {
53            // Return basic recommendations without AI
54            return Ok(self.generate_basic_recommendations(mismatches));
55        }
56
57        let mut recommendations = Vec::new();
58
59        // Group mismatches by type for batch processing
60        let mut grouped: HashMap<String, Vec<&Mismatch>> = HashMap::new();
61        for mismatch in mismatches {
62            let key = format!("{:?}", mismatch.mismatch_type);
63            grouped.entry(key).or_default().push(mismatch);
64        }
65
66        // Generate recommendations for each group
67        for (_group_key, group_mismatches) in grouped {
68            if group_mismatches.len() > self.config.max_recommendations {
69                // Limit to max_recommendations
70                let limited = group_mismatches
71                    .iter()
72                    .take(self.config.max_recommendations)
73                    .copied()
74                    .collect::<Vec<_>>();
75                let group_recs =
76                    self.generate_ai_recommendations_for_group(&limited, request_context).await?;
77                recommendations.extend(group_recs);
78            } else {
79                let group_recs = self
80                    .generate_ai_recommendations_for_group(&group_mismatches, request_context)
81                    .await?;
82                recommendations.extend(group_recs);
83            }
84        }
85
86        Ok(recommendations)
87    }
88
89    /// Generate AI-powered recommendations for a group of mismatches
90    async fn generate_ai_recommendations_for_group(
91        &self,
92        mismatches: &[&Mismatch],
93        context: &RequestContext,
94    ) -> Result<Vec<Recommendation>> {
95        let llm_client = self
96            .llm_client
97            .as_ref()
98            .ok_or_else(|| mockforge_foundation::Error::internal("LLM client not initialized"))?;
99
100        // Build prompt for LLM
101        let prompt = self.build_recommendation_prompt(mismatches, context);
102
103        // Generate recommendation using LLM
104        let request = LlmGenerationRequest::new(self.get_system_prompt(), prompt)
105            .with_temperature(0.7)
106            .with_max_tokens(2000);
107
108        let response = llm_client.generate(&request).await?;
109
110        // Parse LLM response into recommendations
111        self.parse_llm_recommendations(response, mismatches)
112    }
113
114    /// Build prompt for LLM recommendation generation
115    fn build_recommendation_prompt(
116        &self,
117        mismatches: &[&Mismatch],
118        context: &RequestContext,
119    ) -> String {
120        let mut prompt = String::from(
121            "You are analyzing API contract mismatches between front-end requests and backend specifications.\n\n",
122        );
123
124        prompt.push_str("## Request Context\n");
125        prompt.push_str(&format!("Endpoint: {} {}\n", context.method, context.path));
126        if let Some(body) = &context.request_body {
127            prompt.push_str(&format!(
128                "Request Body: {}\n",
129                serde_json::to_string(body).unwrap_or_default()
130            ));
131        }
132        prompt.push_str(&format!("Contract Format: {}\n\n", context.contract_format));
133
134        prompt.push_str("## Detected Mismatches\n\n");
135        for (idx, mismatch) in mismatches.iter().enumerate() {
136            prompt.push_str(&format!("### Mismatch {}: {:?}\n", idx + 1, mismatch.mismatch_type));
137            prompt.push_str(&format!("Path: {}\n", mismatch.path));
138            prompt.push_str(&format!("Description: {}\n", mismatch.description));
139            if let Some(expected) = &mismatch.expected {
140                prompt.push_str(&format!("Expected: {}\n", expected));
141            }
142            if let Some(actual) = &mismatch.actual {
143                prompt.push_str(&format!("Actual: {}\n", actual));
144            }
145            prompt.push_str(&format!("Severity: {:?}\n\n", mismatch.severity));
146        }
147
148        prompt.push_str("## Task\n\n");
149        prompt.push_str("For each mismatch, provide:\n");
150        prompt.push_str("1. A clear, actionable recommendation for fixing the issue\n");
151        prompt.push_str("2. A suggested fix (code or configuration change)\n");
152        prompt.push_str("3. Reasoning explaining why this fix is appropriate\n");
153        if self.config.include_examples {
154            prompt.push_str("4. An example showing the fix applied\n");
155        }
156        prompt.push_str(
157            "\nReturn your response as a JSON array of recommendation objects with the following structure:\n",
158        );
159        prompt.push_str(
160            r#"[
161  {
162    "mismatch_index": 0,
163    "recommendation": "Clear recommendation text",
164    "suggested_fix": "Specific fix or action",
165    "reasoning": "Why this fix is appropriate",
166    "example": { "before": "...", "after": "..." }
167  }
168]"#,
169        );
170
171        prompt
172    }
173
174    /// Get system prompt for LLM
175    fn get_system_prompt(&self) -> String {
176        String::from(
177            "You are an expert API contract analyst. Your role is to analyze mismatches between \
178            front-end API requests and backend contract specifications, and provide clear, \
179            actionable recommendations for fixing these issues. Your recommendations should be \
180            practical, well-reasoned, and include specific examples when helpful. Always consider \
181            the context of the API and the severity of the mismatch when making recommendations.",
182        )
183    }
184
185    /// Parse LLM response into recommendation objects
186    fn parse_llm_recommendations(
187        &self,
188        response: serde_json::Value,
189        mismatches: &[&Mismatch],
190    ) -> Result<Vec<Recommendation>> {
191        let mut recommendations = Vec::new();
192
193        // Try to extract recommendations array from response
194        let recommendations_array = if response.is_array() {
195            Some(response.as_array().unwrap())
196        } else if let Some(arr) = response.get("recommendations") {
197            arr.as_array()
198        } else if let Some(arr) = response.get("data") {
199            arr.as_array()
200        } else {
201            None
202        };
203
204        if let Some(recs) = recommendations_array {
205            for (idx, rec_json) in recs.iter().enumerate() {
206                let mismatch_index =
207                    rec_json.get("mismatch_index").and_then(|v| v.as_u64()).unwrap_or(idx as u64)
208                        as usize;
209
210                if mismatch_index < mismatches.len() {
211                    let mismatch = mismatches[mismatch_index];
212                    let recommendation = Recommendation {
213                        id: format!("rec_{}_{}", mismatch.path, idx),
214                        mismatch_id: format!("mismatch_{}", mismatch_index),
215                        recommendation: rec_json
216                            .get("recommendation")
217                            .and_then(|v| v.as_str())
218                            .unwrap_or("No recommendation provided")
219                            .to_string(),
220                        suggested_fix: rec_json
221                            .get("suggested_fix")
222                            .and_then(|v| v.as_str())
223                            .map(|s| s.to_string()),
224                        confidence: mismatch.confidence, // Use mismatch confidence as base
225                        reasoning: rec_json
226                            .get("reasoning")
227                            .and_then(|v| v.as_str())
228                            .map(|s| s.to_string()),
229                        example: rec_json.get("example").cloned(),
230                    };
231
232                    recommendations.push(recommendation);
233                }
234            }
235        } else {
236            // Fallback: try to parse as text and extract JSON
237            if let Some(text) = response.as_str() {
238                // Try to find JSON in text
239                if let Some(start) = text.find('[') {
240                    if let Some(end) = text.rfind(']') {
241                        let json_str = &text[start..=end];
242                        if let Ok(parsed) = serde_json::from_str::<serde_json::Value>(json_str) {
243                            return self.parse_llm_recommendations(parsed, mismatches);
244                        }
245                    }
246                }
247            }
248
249            // If all else fails, generate basic recommendations
250            return Ok(self.generate_basic_recommendations(
251                &mismatches.iter().map(|m| (*m).clone()).collect::<Vec<_>>(),
252            ));
253        }
254
255        Ok(recommendations)
256    }
257
258    /// Generate basic recommendations without AI
259    fn generate_basic_recommendations(&self, mismatches: &[Mismatch]) -> Vec<Recommendation> {
260        mismatches
261            .iter()
262            .enumerate()
263            .map(|(idx, mismatch)| {
264                let (recommendation, suggested_fix) = match mismatch.mismatch_type {
265                    super::types::MismatchType::MissingRequiredField => (
266                        format!("Add the required field '{}' to the request", mismatch.path),
267                        format!("Add field: {}", mismatch.path),
268                    ),
269                    super::types::MismatchType::TypeMismatch => (
270                        format!(
271                            "Change the type of '{}' from {} to {}",
272                            mismatch.path,
273                            mismatch.actual.as_ref().unwrap_or(&"unknown".to_string()),
274                            mismatch.expected.as_ref().unwrap_or(&"unknown".to_string())
275                        ),
276                        format!(
277                            "Update field type: {} -> {}",
278                            mismatch.path,
279                            mismatch.expected.as_ref().unwrap_or(&"unknown".to_string())
280                        ),
281                    ),
282                    super::types::MismatchType::UnexpectedField => (
283                        format!("Remove the unexpected field '{}' from the request", mismatch.path),
284                        format!("Remove field: {}", mismatch.path),
285                    ),
286                    _ => (mismatch.description.clone(), "Review and fix the mismatch".to_string()),
287                };
288
289                Recommendation {
290                    id: format!("rec_{}_{}", mismatch.path, idx),
291                    mismatch_id: format!("mismatch_{}", idx),
292                    recommendation,
293                    suggested_fix: Some(suggested_fix),
294                    confidence: mismatch.confidence,
295                    reasoning: Some(format!(
296                        "Based on mismatch type: {:?}",
297                        mismatch.mismatch_type
298                    )),
299                    example: None,
300                }
301            })
302            .collect()
303    }
304}
305
306/// Context for recommendation generation
307#[derive(Debug, Clone)]
308pub struct RequestContext {
309    /// HTTP method
310    pub method: String,
311
312    /// Request path
313    pub path: String,
314
315    /// Request body
316    pub request_body: Option<serde_json::Value>,
317
318    /// Contract format
319    pub contract_format: String,
320
321    /// Additional context
322    pub additional_context: HashMap<String, serde_json::Value>,
323}
324
325impl RequestContext {
326    /// Create a new request context
327    pub fn new(method: impl Into<String>, path: impl Into<String>) -> Self {
328        Self {
329            method: method.into(),
330            path: path.into(),
331            request_body: None,
332            contract_format: "openapi-3.0".to_string(),
333            additional_context: HashMap::new(),
334        }
335    }
336
337    /// Add request body
338    pub fn with_body(mut self, body: serde_json::Value) -> Self {
339        self.request_body = Some(body);
340        self
341    }
342
343    /// Set contract format
344    pub fn with_contract_format(mut self, format: impl Into<String>) -> Self {
345        self.contract_format = format.into();
346        self
347    }
348}