mockforge_intelligence/ai_contract_diff/
recommendation_engine.rs1use 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
13pub struct RecommendationEngine {
15 llm_client: Option<LlmClient>,
17
18 config: ContractDiffConfig,
20}
21
22impl RecommendationEngine {
23 pub fn new(config: ContractDiffConfig) -> Result<Self> {
25 let llm_client = if config.use_ai_recommendations {
26 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, max_tokens: 2000,
34 rules: crate::intelligent_behavior::BehaviorRules::default(), 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 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 Ok(self.generate_basic_recommendations(mismatches));
55 }
56
57 let mut recommendations = Vec::new();
58
59 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 for (_group_key, group_mismatches) in grouped {
68 if group_mismatches.len() > self.config.max_recommendations {
69 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 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 let prompt = self.build_recommendation_prompt(mismatches, context);
102
103 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 self.parse_llm_recommendations(response, mismatches)
112 }
113
114 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 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 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 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, 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 if let Some(text) = response.as_str() {
238 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 return Ok(self.generate_basic_recommendations(
251 &mismatches.iter().map(|m| (*m).clone()).collect::<Vec<_>>(),
252 ));
253 }
254
255 Ok(recommendations)
256 }
257
258 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#[derive(Debug, Clone)]
308pub struct RequestContext {
309 pub method: String,
311
312 pub path: String,
314
315 pub request_body: Option<serde_json::Value>,
317
318 pub contract_format: String,
320
321 pub additional_context: HashMap<String, serde_json::Value>,
323}
324
325impl RequestContext {
326 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 pub fn with_body(mut self, body: serde_json::Value) -> Self {
339 self.request_body = Some(body);
340 self
341 }
342
343 pub fn with_contract_format(mut self, format: impl Into<String>) -> Self {
345 self.contract_format = format.into();
346 self
347 }
348}