Skip to main content

mockforge_intelligence/intelligent_behavior/
spec_suggestion.rs

1//! AI-powered specification suggestion and generation
2//!
3//! This module provides intelligent API specification extrapolation using LLMs.
4//! Given minimal input (e.g., a single endpoint example or API description),
5//! it can generate complete OpenAPI specifications or MockForge configurations.
6
7use super::config::BehaviorModelConfig;
8use super::llm_client::LlmClient;
9use super::types::LlmGenerationRequest;
10use mockforge_foundation::Result;
11use serde::{Deserialize, Serialize};
12use serde_json::Value;
13
14/// Input type for spec suggestion
15#[derive(Debug, Clone, Serialize, Deserialize)]
16#[serde(tag = "type", rename_all = "lowercase")]
17pub enum SuggestionInput {
18    /// Single endpoint example with request/response
19    Endpoint {
20        /// HTTP method
21        method: String,
22        /// Path
23        path: String,
24        /// Request example
25        request: Option<Value>,
26        /// Response example
27        response: Option<Value>,
28        /// Optional description
29        description: Option<String>,
30    },
31    /// Text description of the API
32    Description {
33        /// API description text
34        text: String,
35    },
36    /// Partial OpenAPI specification
37    PartialSpec {
38        /// Partial OpenAPI spec
39        spec: Value,
40    },
41    /// List of endpoint paths only
42    Paths {
43        /// List of paths
44        paths: Vec<String>,
45    },
46}
47
48/// Output format for generated specs
49#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
50#[serde(rename_all = "lowercase")]
51pub enum OutputFormat {
52    /// OpenAPI 3.0 specification
53    OpenAPI,
54    /// MockForge YAML configuration
55    MockForge,
56    /// Both formats
57    Both,
58}
59
60impl std::str::FromStr for OutputFormat {
61    type Err = String;
62
63    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
64        match s.to_lowercase().as_str() {
65            "openapi" => Ok(Self::OpenAPI),
66            "mockforge" => Ok(Self::MockForge),
67            "both" => Ok(Self::Both),
68            _ => Err(format!("Invalid output format: {}", s)),
69        }
70    }
71}
72
73/// Configuration for spec suggestion
74#[derive(Debug, Clone)]
75pub struct SuggestionConfig {
76    /// LLM configuration
77    pub llm_config: BehaviorModelConfig,
78    /// Output format
79    pub output_format: OutputFormat,
80    /// Number of additional endpoints to suggest
81    pub num_suggestions: usize,
82    /// Whether to include examples in generated specs
83    pub include_examples: bool,
84    /// API domain/category hint
85    pub domain_hint: Option<String>,
86}
87
88impl Default for SuggestionConfig {
89    fn default() -> Self {
90        Self {
91            llm_config: BehaviorModelConfig::default(),
92            output_format: OutputFormat::OpenAPI,
93            num_suggestions: 5,
94            include_examples: true,
95            domain_hint: None,
96        }
97    }
98}
99
100/// Result from spec suggestion
101#[derive(Debug, Clone, Serialize, Deserialize)]
102pub struct SuggestionResult {
103    /// Generated OpenAPI spec (if requested)
104    pub openapi_spec: Option<Value>,
105    /// Generated MockForge config (if requested)
106    pub mockforge_config: Option<Value>,
107    /// Suggestions and reasoning
108    pub suggestions: Vec<EndpointSuggestion>,
109    /// Metadata about the generation
110    pub metadata: SuggestionMetadata,
111}
112
113/// Individual endpoint suggestion
114#[derive(Debug, Clone, Serialize, Deserialize)]
115pub struct EndpointSuggestion {
116    /// HTTP method
117    pub method: String,
118    /// Path
119    pub path: String,
120    /// Description
121    pub description: String,
122    /// Suggested parameters
123    pub parameters: Vec<ParameterInfo>,
124    /// Suggested response schema
125    pub response_schema: Option<Value>,
126    /// Reasoning for this suggestion
127    pub reasoning: String,
128}
129
130/// Parameter information
131#[derive(Debug, Clone, Serialize, Deserialize)]
132pub struct ParameterInfo {
133    /// Parameter name
134    pub name: String,
135    /// Parameter location (path, query, header, body)
136    pub location: String,
137    /// Data type
138    pub data_type: String,
139    /// Whether required
140    pub required: bool,
141    /// Description
142    pub description: Option<String>,
143}
144
145/// Metadata about the suggestion generation
146#[derive(Debug, Clone, Serialize, Deserialize)]
147pub struct SuggestionMetadata {
148    /// Number of endpoints generated
149    pub endpoint_count: usize,
150    /// Detected API domain/category
151    pub detected_domain: Option<String>,
152    /// Generation timestamp
153    pub timestamp: String,
154    /// Model used
155    pub model: String,
156}
157
158/// Engine for AI-powered spec suggestion
159pub struct SpecSuggestionEngine {
160    /// LLM client
161    llm_client: LlmClient,
162    /// Configuration
163    config: SuggestionConfig,
164}
165
166impl SpecSuggestionEngine {
167    /// Create a new spec suggestion engine
168    pub fn new(config: SuggestionConfig) -> Self {
169        let llm_client = LlmClient::new(config.llm_config.clone());
170        Self { llm_client, config }
171    }
172
173    /// Generate spec suggestions from input
174    pub async fn suggest(&self, input: &SuggestionInput) -> Result<SuggestionResult> {
175        // Build prompt based on input type
176        let (system_prompt, user_prompt) = self.build_prompts(input)?;
177
178        // Generate using LLM
179        let request = LlmGenerationRequest {
180            system_prompt,
181            user_prompt,
182            temperature: 0.7,
183            max_tokens: 4000,
184            schema: None,
185        };
186
187        let llm_response = self.llm_client.generate(&request).await?;
188
189        // Parse and structure the response
190        self.parse_llm_response(llm_response, input).await
191    }
192
193    /// Build prompts based on input type
194    fn build_prompts(&self, input: &SuggestionInput) -> Result<(String, String)> {
195        let system_prompt = self.build_system_prompt();
196        let user_prompt = match input {
197            SuggestionInput::Endpoint {
198                method,
199                path,
200                request,
201                response,
202                description,
203            } => self.build_endpoint_prompt(method, path, request, response, description),
204            SuggestionInput::Description { text } => self.build_description_prompt(text),
205            SuggestionInput::PartialSpec { spec } => self.build_partial_spec_prompt(spec),
206            SuggestionInput::Paths { paths } => self.build_paths_prompt(paths),
207        };
208
209        Ok((system_prompt, user_prompt))
210    }
211
212    /// Build system prompt for spec generation
213    fn build_system_prompt(&self) -> String {
214        let format_desc = match self.config.output_format {
215            OutputFormat::OpenAPI => "OpenAPI 3.0 specification",
216            OutputFormat::MockForge => "MockForge YAML configuration",
217            OutputFormat::Both => "both OpenAPI 3.0 specification and MockForge YAML configuration",
218        };
219
220        format!(
221            r#"You are an expert API architect and specification designer. Your role is to analyze API examples or descriptions and generate comprehensive, production-ready API specifications.
222
223Your task is to generate {}. When generating specifications, follow these principles:
224
2251. **RESTful Best Practices**: Use appropriate HTTP methods, status codes, and follow REST conventions
2262. **Consistency**: Maintain consistent naming conventions, response structures, and error handling
2273. **Completeness**: Include request/response schemas, parameters, error responses, and examples
2284. **Realistic**: Generate realistic and practical API designs that solve real problems
2295. **Security**: Include authentication/authorization considerations where appropriate
2306. **Documentation**: Provide clear descriptions for all endpoints, parameters, and responses
231
232When suggesting additional endpoints, consider:
233- CRUD operations for identified resources
234- Common utility endpoints (health, status, metrics)
235- Related resources and their relationships
236- Filtering, pagination, and search capabilities
237- Batch operations where appropriate
238
239Respond with valid JSON in the following structure:
240{{
241  "detected_domain": "string (e.g., 'e-commerce', 'social-media', 'fintech')",
242  "endpoints": [
243    {{
244      "method": "GET|POST|PUT|DELETE|PATCH",
245      "path": "/api/resource",
246      "description": "What this endpoint does",
247      "parameters": [
248        {{
249          "name": "param_name",
250          "location": "path|query|header|body",
251          "data_type": "string|integer|boolean|object",
252          "required": true|false,
253          "description": "Parameter description"
254        }}
255      ],
256      "response_schema": {{ /* JSON schema */ }},
257      "reasoning": "Why this endpoint is suggested"
258    }}
259  ],
260  "openapi_spec": {{ /* Complete OpenAPI 3.0 spec if requested */ }},
261  "mockforge_config": {{ /* Complete MockForge config if requested */ }}
262}}
263
264Generate {} additional endpoint suggestions beyond what was provided in the input."#,
265            format_desc, self.config.num_suggestions
266        )
267    }
268
269    /// Build prompt for single endpoint input
270    fn build_endpoint_prompt(
271        &self,
272        method: &str,
273        path: &str,
274        request: &Option<Value>,
275        response: &Option<Value>,
276        description: &Option<String>,
277    ) -> String {
278        let domain_hint = self.config.domain_hint.as_deref().unwrap_or("general");
279
280        let desc_text = description
281            .as_ref()
282            .map(|d| format!("Description: {}\n", d))
283            .unwrap_or_default();
284
285        let request_text = request
286            .as_ref()
287            .map(|r| {
288                format!(
289                    "Request:\n```json\n{}\n```\n",
290                    serde_json::to_string_pretty(r).unwrap_or_default()
291                )
292            })
293            .unwrap_or_default();
294
295        let response_text = response
296            .as_ref()
297            .map(|r| {
298                format!(
299                    "Response:\n```json\n{}\n```\n",
300                    serde_json::to_string_pretty(r).unwrap_or_default()
301                )
302            })
303            .unwrap_or_default();
304
305        format!(
306            r#"I have the following API endpoint example:
307
308Method: {}
309Path: {}
310{}{}{}
311API Domain/Category: {}
312
313Based on this single endpoint, please:
3141. Analyze the API's purpose and domain
3152. Suggest additional endpoints that would typically exist in such an API
3163. Generate a complete specification with realistic request/response schemas
3174. Include appropriate error handling and status codes
3185. Add pagination, filtering, or search capabilities where relevant
319
320Focus on creating a cohesive and practical API design that follows industry best practices."#,
321            method, path, desc_text, request_text, response_text, domain_hint
322        )
323    }
324
325    /// Build prompt for description input
326    fn build_description_prompt(&self, description: &str) -> String {
327        let domain_hint = self.config.domain_hint.as_deref().unwrap_or("general");
328
329        format!(
330            r#"I need to create an API with the following description:
331
332{}
333
334API Domain/Category: {}
335
336Based on this description, please:
3371. Design a comprehensive REST API with all necessary endpoints
3382. Define resource models and their relationships
3393. Include CRUD operations for main resources
3404. Add supporting endpoints (search, filters, pagination)
3415. Generate complete request/response schemas with realistic examples
3426. Consider authentication, authorization, and error handling
3437. Generate a complete specification ready for implementation
344
345Create a production-ready API design that follows REST best practices and industry standards."#,
346            description, domain_hint
347        )
348    }
349
350    /// Build prompt for partial spec input
351    fn build_partial_spec_prompt(&self, spec: &Value) -> String {
352        format!(
353            r#"I have a partial API specification:
354
355```json
356{}
357```
358
359Please:
3601. Analyze the existing specification structure
3612. Complete missing sections (schemas, responses, parameters)
3623. Suggest additional endpoints that would complement the existing ones
3634. Ensure consistency across all endpoints
3645. Add realistic examples and descriptions
3656. Fill in any gaps in the specification
3667. Generate a complete, production-ready specification
367
368Maintain the style and conventions of the original specification while expanding it."#,
369            serde_json::to_string_pretty(spec).unwrap_or_default()
370        )
371    }
372
373    /// Build prompt for paths-only input
374    fn build_paths_prompt(&self, paths: &[String]) -> String {
375        let paths_list = paths.join("\n- ");
376        let domain_hint = self.config.domain_hint.as_deref().unwrap_or("general");
377
378        format!(
379            r#"I have a list of API endpoint paths:
380
381- {}
382
383API Domain/Category: {}
384
385Based on these paths, please:
3861. Infer the API's purpose and resource model
3872. Design appropriate HTTP methods for each path
3883. Generate complete request/response schemas
3894. Add query parameters for filtering, pagination, and sorting where appropriate
3905. Include proper error responses
3916. Suggest additional related endpoints that are missing
3927. Generate a complete specification
393
394Create a cohesive API design that makes sense for these endpoints and follows REST conventions."#,
395            paths_list, domain_hint
396        )
397    }
398
399    /// Parse LLM response into structured result
400    async fn parse_llm_response(
401        &self,
402        response: Value,
403        _input: &SuggestionInput,
404    ) -> Result<SuggestionResult> {
405        // Extract endpoints
406        let endpoints = response
407            .get("endpoints")
408            .and_then(|e| e.as_array())
409            .ok_or_else(|| mockforge_foundation::Error::internal("No endpoints in LLM response"))?;
410
411        let suggestions: Vec<EndpointSuggestion> =
412            endpoints.iter().filter_map(|e| self.parse_endpoint_suggestion(e)).collect();
413
414        // Extract specs based on format
415        let openapi_spec =
416            if matches!(self.config.output_format, OutputFormat::OpenAPI | OutputFormat::Both) {
417                response.get("openapi_spec").cloned()
418            } else {
419                None
420            };
421
422        let mockforge_config =
423            if matches!(self.config.output_format, OutputFormat::MockForge | OutputFormat::Both) {
424                response.get("mockforge_config").cloned()
425            } else {
426                None
427            };
428
429        // Extract metadata
430        let detected_domain =
431            response.get("detected_domain").and_then(|d| d.as_str()).map(String::from);
432
433        let metadata = SuggestionMetadata {
434            endpoint_count: suggestions.len(),
435            detected_domain,
436            timestamp: chrono::Utc::now().to_rfc3339(),
437            model: self.config.llm_config.model.clone(),
438        };
439
440        Ok(SuggestionResult {
441            openapi_spec,
442            mockforge_config,
443            suggestions,
444            metadata,
445        })
446    }
447
448    /// Parse individual endpoint suggestion
449    fn parse_endpoint_suggestion(&self, endpoint: &Value) -> Option<EndpointSuggestion> {
450        let method = endpoint.get("method")?.as_str()?.to_string();
451        let path = endpoint.get("path")?.as_str()?.to_string();
452        let description = endpoint.get("description")?.as_str()?.to_string();
453        let reasoning = endpoint
454            .get("reasoning")
455            .and_then(|r| r.as_str())
456            .unwrap_or("Suggested by AI")
457            .to_string();
458
459        let parameters = endpoint
460            .get("parameters")
461            .and_then(|p| p.as_array())
462            .map(|params| params.iter().filter_map(|p| self.parse_parameter(p)).collect())
463            .unwrap_or_default();
464
465        let response_schema = endpoint.get("response_schema").cloned();
466
467        Some(EndpointSuggestion {
468            method,
469            path,
470            description,
471            parameters,
472            response_schema,
473            reasoning,
474        })
475    }
476
477    /// Parse parameter information
478    fn parse_parameter(&self, param: &Value) -> Option<ParameterInfo> {
479        Some(ParameterInfo {
480            name: param.get("name")?.as_str()?.to_string(),
481            location: param.get("location")?.as_str()?.to_string(),
482            data_type: param.get("data_type")?.as_str()?.to_string(),
483            required: param.get("required")?.as_bool()?,
484            description: param.get("description").and_then(|d| d.as_str()).map(String::from),
485        })
486    }
487}
488
489#[cfg(test)]
490mod tests {
491    use super::*;
492
493    #[test]
494    fn test_output_format_from_str() {
495        assert_eq!("openapi".parse::<OutputFormat>().unwrap(), OutputFormat::OpenAPI);
496        assert_eq!("mockforge".parse::<OutputFormat>().unwrap(), OutputFormat::MockForge);
497        assert_eq!("both".parse::<OutputFormat>().unwrap(), OutputFormat::Both);
498        assert!("invalid".parse::<OutputFormat>().is_err());
499    }
500
501    #[test]
502    fn test_suggestion_config_default() {
503        let config = SuggestionConfig::default();
504        assert_eq!(config.output_format, OutputFormat::OpenAPI);
505        assert_eq!(config.num_suggestions, 5);
506        assert!(config.include_examples);
507    }
508}