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            seed: None,
186        };
187
188        let llm_response = self.llm_client.generate(&request).await?;
189
190        // Parse and structure the response
191        self.parse_llm_response(llm_response, input).await
192    }
193
194    /// Build prompts based on input type
195    fn build_prompts(&self, input: &SuggestionInput) -> Result<(String, String)> {
196        let system_prompt = self.build_system_prompt();
197        let user_prompt = match input {
198            SuggestionInput::Endpoint {
199                method,
200                path,
201                request,
202                response,
203                description,
204            } => self.build_endpoint_prompt(method, path, request, response, description),
205            SuggestionInput::Description { text } => self.build_description_prompt(text),
206            SuggestionInput::PartialSpec { spec } => self.build_partial_spec_prompt(spec),
207            SuggestionInput::Paths { paths } => self.build_paths_prompt(paths),
208        };
209
210        Ok((system_prompt, user_prompt))
211    }
212
213    /// Build system prompt for spec generation
214    fn build_system_prompt(&self) -> String {
215        let format_desc = match self.config.output_format {
216            OutputFormat::OpenAPI => "OpenAPI 3.0 specification",
217            OutputFormat::MockForge => "MockForge YAML configuration",
218            OutputFormat::Both => "both OpenAPI 3.0 specification and MockForge YAML configuration",
219        };
220
221        format!(
222            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.
223
224Your task is to generate {}. When generating specifications, follow these principles:
225
2261. **RESTful Best Practices**: Use appropriate HTTP methods, status codes, and follow REST conventions
2272. **Consistency**: Maintain consistent naming conventions, response structures, and error handling
2283. **Completeness**: Include request/response schemas, parameters, error responses, and examples
2294. **Realistic**: Generate realistic and practical API designs that solve real problems
2305. **Security**: Include authentication/authorization considerations where appropriate
2316. **Documentation**: Provide clear descriptions for all endpoints, parameters, and responses
232
233When suggesting additional endpoints, consider:
234- CRUD operations for identified resources
235- Common utility endpoints (health, status, metrics)
236- Related resources and their relationships
237- Filtering, pagination, and search capabilities
238- Batch operations where appropriate
239
240Respond with valid JSON in the following structure:
241{{
242  "detected_domain": "string (e.g., 'e-commerce', 'social-media', 'fintech')",
243  "endpoints": [
244    {{
245      "method": "GET|POST|PUT|DELETE|PATCH",
246      "path": "/api/resource",
247      "description": "What this endpoint does",
248      "parameters": [
249        {{
250          "name": "param_name",
251          "location": "path|query|header|body",
252          "data_type": "string|integer|boolean|object",
253          "required": true|false,
254          "description": "Parameter description"
255        }}
256      ],
257      "response_schema": {{ /* JSON schema */ }},
258      "reasoning": "Why this endpoint is suggested"
259    }}
260  ],
261  "openapi_spec": {{ /* Complete OpenAPI 3.0 spec if requested */ }},
262  "mockforge_config": {{ /* Complete MockForge config if requested */ }}
263}}
264
265Generate {} additional endpoint suggestions beyond what was provided in the input."#,
266            format_desc, self.config.num_suggestions
267        )
268    }
269
270    /// Build prompt for single endpoint input
271    fn build_endpoint_prompt(
272        &self,
273        method: &str,
274        path: &str,
275        request: &Option<Value>,
276        response: &Option<Value>,
277        description: &Option<String>,
278    ) -> String {
279        let domain_hint = self.config.domain_hint.as_deref().unwrap_or("general");
280
281        let desc_text = description
282            .as_ref()
283            .map(|d| format!("Description: {}\n", d))
284            .unwrap_or_default();
285
286        let request_text = request
287            .as_ref()
288            .map(|r| {
289                format!(
290                    "Request:\n```json\n{}\n```\n",
291                    serde_json::to_string_pretty(r).unwrap_or_default()
292                )
293            })
294            .unwrap_or_default();
295
296        let response_text = response
297            .as_ref()
298            .map(|r| {
299                format!(
300                    "Response:\n```json\n{}\n```\n",
301                    serde_json::to_string_pretty(r).unwrap_or_default()
302                )
303            })
304            .unwrap_or_default();
305
306        format!(
307            r#"I have the following API endpoint example:
308
309Method: {}
310Path: {}
311{}{}{}
312API Domain/Category: {}
313
314Based on this single endpoint, please:
3151. Analyze the API's purpose and domain
3162. Suggest additional endpoints that would typically exist in such an API
3173. Generate a complete specification with realistic request/response schemas
3184. Include appropriate error handling and status codes
3195. Add pagination, filtering, or search capabilities where relevant
320
321Focus on creating a cohesive and practical API design that follows industry best practices."#,
322            method, path, desc_text, request_text, response_text, domain_hint
323        )
324    }
325
326    /// Build prompt for description input
327    fn build_description_prompt(&self, description: &str) -> String {
328        let domain_hint = self.config.domain_hint.as_deref().unwrap_or("general");
329
330        format!(
331            r#"I need to create an API with the following description:
332
333{}
334
335API Domain/Category: {}
336
337Based on this description, please:
3381. Design a comprehensive REST API with all necessary endpoints
3392. Define resource models and their relationships
3403. Include CRUD operations for main resources
3414. Add supporting endpoints (search, filters, pagination)
3425. Generate complete request/response schemas with realistic examples
3436. Consider authentication, authorization, and error handling
3447. Generate a complete specification ready for implementation
345
346Create a production-ready API design that follows REST best practices and industry standards."#,
347            description, domain_hint
348        )
349    }
350
351    /// Build prompt for partial spec input
352    fn build_partial_spec_prompt(&self, spec: &Value) -> String {
353        format!(
354            r#"I have a partial API specification:
355
356```json
357{}
358```
359
360Please:
3611. Analyze the existing specification structure
3622. Complete missing sections (schemas, responses, parameters)
3633. Suggest additional endpoints that would complement the existing ones
3644. Ensure consistency across all endpoints
3655. Add realistic examples and descriptions
3666. Fill in any gaps in the specification
3677. Generate a complete, production-ready specification
368
369Maintain the style and conventions of the original specification while expanding it."#,
370            serde_json::to_string_pretty(spec).unwrap_or_default()
371        )
372    }
373
374    /// Build prompt for paths-only input
375    fn build_paths_prompt(&self, paths: &[String]) -> String {
376        let paths_list = paths.join("\n- ");
377        let domain_hint = self.config.domain_hint.as_deref().unwrap_or("general");
378
379        format!(
380            r#"I have a list of API endpoint paths:
381
382- {}
383
384API Domain/Category: {}
385
386Based on these paths, please:
3871. Infer the API's purpose and resource model
3882. Design appropriate HTTP methods for each path
3893. Generate complete request/response schemas
3904. Add query parameters for filtering, pagination, and sorting where appropriate
3915. Include proper error responses
3926. Suggest additional related endpoints that are missing
3937. Generate a complete specification
394
395Create a cohesive API design that makes sense for these endpoints and follows REST conventions."#,
396            paths_list, domain_hint
397        )
398    }
399
400    /// Parse LLM response into structured result
401    async fn parse_llm_response(
402        &self,
403        response: Value,
404        _input: &SuggestionInput,
405    ) -> Result<SuggestionResult> {
406        // Extract endpoints
407        let endpoints = response
408            .get("endpoints")
409            .and_then(|e| e.as_array())
410            .ok_or_else(|| mockforge_foundation::Error::internal("No endpoints in LLM response"))?;
411
412        let suggestions: Vec<EndpointSuggestion> =
413            endpoints.iter().filter_map(|e| self.parse_endpoint_suggestion(e)).collect();
414
415        // Extract specs based on format
416        let openapi_spec =
417            if matches!(self.config.output_format, OutputFormat::OpenAPI | OutputFormat::Both) {
418                response.get("openapi_spec").cloned()
419            } else {
420                None
421            };
422
423        let mockforge_config =
424            if matches!(self.config.output_format, OutputFormat::MockForge | OutputFormat::Both) {
425                response.get("mockforge_config").cloned()
426            } else {
427                None
428            };
429
430        // Extract metadata
431        let detected_domain =
432            response.get("detected_domain").and_then(|d| d.as_str()).map(String::from);
433
434        let metadata = SuggestionMetadata {
435            endpoint_count: suggestions.len(),
436            detected_domain,
437            timestamp: chrono::Utc::now().to_rfc3339(),
438            model: self.config.llm_config.model.clone(),
439        };
440
441        Ok(SuggestionResult {
442            openapi_spec,
443            mockforge_config,
444            suggestions,
445            metadata,
446        })
447    }
448
449    /// Parse individual endpoint suggestion
450    fn parse_endpoint_suggestion(&self, endpoint: &Value) -> Option<EndpointSuggestion> {
451        let method = endpoint.get("method")?.as_str()?.to_string();
452        let path = endpoint.get("path")?.as_str()?.to_string();
453        let description = endpoint.get("description")?.as_str()?.to_string();
454        let reasoning = endpoint
455            .get("reasoning")
456            .and_then(|r| r.as_str())
457            .unwrap_or("Suggested by AI")
458            .to_string();
459
460        let parameters = endpoint
461            .get("parameters")
462            .and_then(|p| p.as_array())
463            .map(|params| params.iter().filter_map(|p| self.parse_parameter(p)).collect())
464            .unwrap_or_default();
465
466        let response_schema = endpoint.get("response_schema").cloned();
467
468        Some(EndpointSuggestion {
469            method,
470            path,
471            description,
472            parameters,
473            response_schema,
474            reasoning,
475        })
476    }
477
478    /// Parse parameter information
479    fn parse_parameter(&self, param: &Value) -> Option<ParameterInfo> {
480        Some(ParameterInfo {
481            name: param.get("name")?.as_str()?.to_string(),
482            location: param.get("location")?.as_str()?.to_string(),
483            data_type: param.get("data_type")?.as_str()?.to_string(),
484            required: param.get("required")?.as_bool()?,
485            description: param.get("description").and_then(|d| d.as_str()).map(String::from),
486        })
487    }
488}
489
490#[cfg(test)]
491mod tests {
492    use super::*;
493
494    #[test]
495    fn test_output_format_from_str() {
496        assert_eq!("openapi".parse::<OutputFormat>().unwrap(), OutputFormat::OpenAPI);
497        assert_eq!("mockforge".parse::<OutputFormat>().unwrap(), OutputFormat::MockForge);
498        assert_eq!("both".parse::<OutputFormat>().unwrap(), OutputFormat::Both);
499        assert!("invalid".parse::<OutputFormat>().is_err());
500    }
501
502    #[test]
503    fn test_suggestion_config_default() {
504        let config = SuggestionConfig::default();
505        assert_eq!(config.output_format, OutputFormat::OpenAPI);
506        assert_eq!(config.num_suggestions, 5);
507        assert!(config.include_examples);
508    }
509}