Skip to main content

mockforge_intelligence/ai_studio/
nl_mock_generator.rs

1//! Natural language mock generator
2//!
3//! This module provides functionality to generate mocks from natural language descriptions.
4//! It integrates with the existing VoiceCommandParser and VoiceSpecGenerator to leverage
5//! the proven mock generation infrastructure.
6
7use crate::ai_studio::artifact_freezer::{ArtifactFreezer, FreezeMetadata};
8use crate::ai_studio::config::DeterministicModeConfig;
9use crate::intelligent_behavior::IntelligentBehaviorConfig;
10use crate::voice::{command_parser::VoiceCommandParser, spec_generator::VoiceSpecGenerator};
11use mockforge_foundation::Result;
12use mockforge_openapi::OpenApiSpec;
13use serde::{Deserialize, Serialize};
14use sha2::{Digest, Sha256};
15use std::collections::hash_map::DefaultHasher;
16use std::hash::{Hash, Hasher};
17
18/// Mock generator for creating mocks from natural language
19pub struct MockGenerator {
20    /// Voice command parser for parsing NL descriptions
21    parser: VoiceCommandParser,
22    /// Spec generator for creating OpenAPI specs
23    spec_generator: VoiceSpecGenerator,
24    /// Configuration (needed for accessing LLM provider/model info)
25    config: IntelligentBehaviorConfig,
26}
27
28impl MockGenerator {
29    /// Create a new mock generator with default configuration
30    pub fn new() -> Self {
31        let config = IntelligentBehaviorConfig::default();
32        Self {
33            parser: VoiceCommandParser::new(config.clone()),
34            spec_generator: VoiceSpecGenerator::new(),
35            config,
36        }
37    }
38
39    /// Create a new mock generator with custom configuration
40    pub fn with_config(config: IntelligentBehaviorConfig) -> Self {
41        Self {
42            parser: VoiceCommandParser::new(config.clone()),
43            spec_generator: VoiceSpecGenerator::new(),
44            config,
45        }
46    }
47
48    /// Generate a mock from natural language description
49    ///
50    /// This method parses the natural language description and generates a complete
51    /// OpenAPI specification ready for use with MockForge.
52    ///
53    /// # Example
54    ///
55    /// ```rust,ignore
56    /// use mockforge_core::ai_studio::nl_mock_generator::MockGenerator;
57    ///
58    /// async fn example() -> mockforge_core::Result<()> {
59    ///     let generator = MockGenerator::new();
60    ///     let result = generator.generate(
61    ///         "Create a user API with CRUD operations for managing users",
62    ///         None,
63    ///         None,
64    ///         None,
65    ///     ).await?;
66    ///     Ok(())
67    /// }
68    /// ```
69    pub async fn generate(
70        &self,
71        description: &str,
72        _workspace_id: Option<&str>,
73        ai_mode: Option<crate::ai_studio::config::AiMode>,
74        deterministic_config: Option<&DeterministicModeConfig>,
75    ) -> Result<MockGenerationResult> {
76        // In deterministic mode, check for frozen artifacts first
77        if ai_mode == Some(crate::ai_studio::config::AiMode::GenerateOnceFreeze) {
78            let freezer = ArtifactFreezer::new();
79
80            // Create identifier from description hash
81            let mut hasher = DefaultHasher::new();
82            description.hash(&mut hasher);
83            let description_hash = format!("{:x}", hasher.finish());
84
85            // Try to load frozen artifact
86            if let Some(frozen) = freezer.load_frozen("mock", Some(&description_hash)).await? {
87                // Extract spec from frozen content (remove metadata)
88                let mut spec = frozen.content.clone();
89                if let Some(obj) = spec.as_object_mut() {
90                    obj.remove("_frozen_metadata");
91                }
92
93                return Ok(MockGenerationResult {
94                    spec: Some(spec),
95                    message: format!(
96                        "Loaded frozen mock artifact from {} (deterministic mode)",
97                        frozen.path
98                    ),
99                    parsed_command: None,
100                    frozen_artifact: Some(frozen),
101                });
102            }
103        }
104
105        // Parse the natural language command
106        let parsed = self.parser.parse_command(description).await?;
107
108        // Generate OpenAPI spec from parsed command
109        let spec = self.spec_generator.generate_spec(&parsed).await?;
110
111        // Convert spec to JSON for response
112        let spec_json = serde_json::to_value(&spec.spec)?;
113
114        // Auto-freeze if enabled
115        let frozen_artifact = if let Some(config) = deterministic_config {
116            if config.enabled && config.is_auto_freeze_enabled() {
117                let freezer = ArtifactFreezer::new();
118
119                // Calculate prompt hash
120                let mut hasher = Sha256::new();
121                hasher.update(description.as_bytes());
122                let prompt_hash = format!("{:x}", hasher.finalize());
123
124                // Create metadata
125                let metadata = if config.track_metadata {
126                    Some(FreezeMetadata {
127                        llm_provider: Some(self.config.behavior_model.llm_provider.clone()),
128                        llm_model: Some(self.config.behavior_model.model.clone()),
129                        llm_version: None, // Would need to be passed in or retrieved
130                        prompt_hash: Some(prompt_hash),
131                        output_hash: None, // Will be calculated by freezer
132                        original_prompt: Some(description.to_string()),
133                    })
134                } else {
135                    None
136                };
137
138                let freeze_request = crate::ai_studio::artifact_freezer::FreezeRequest {
139                    artifact_type: "mock".to_string(),
140                    content: spec_json.clone(),
141                    format: config.freeze_format.clone(),
142                    path: None,
143                    metadata,
144                };
145
146                freezer.auto_freeze_if_enabled(&freeze_request, config).await?
147            } else {
148                None
149            }
150        } else {
151            None
152        };
153
154        // Record AI pillar usage (ai_generation type=mock)
155        mockforge_foundation::pillar_tracking::record_ai_usage(
156            _workspace_id.map(String::from),
157            None,
158            "ai_generation",
159            serde_json::json!({
160                "type": "mock",
161                "endpoints": parsed.endpoints.len(),
162                "models": parsed.models.len(),
163            }),
164        )
165        .await;
166
167        Ok(MockGenerationResult {
168            spec: Some(spec_json),
169            message: format!(
170                "Successfully generated API '{}' with {} endpoints and {} models{}",
171                parsed.title,
172                parsed.endpoints.len(),
173                parsed.models.len(),
174                if frozen_artifact.is_some() {
175                    " (auto-frozen)"
176                } else {
177                    ""
178                }
179            ),
180            parsed_command: Some(parsed),
181            frozen_artifact,
182        })
183    }
184
185    /// Generate a mock with additional context (for conversational mode)
186    ///
187    /// This method allows generating mocks that extend or modify existing specifications.
188    pub async fn generate_with_context(
189        &self,
190        description: &str,
191        existing_spec: Option<&OpenApiSpec>,
192    ) -> Result<MockGenerationResult> {
193        // Parse the natural language command
194        let parsed = self.parser.parse_command(description).await?;
195
196        // Generate or merge spec
197        let spec = if let Some(existing) = existing_spec {
198            // Merge with existing spec
199            self.spec_generator.merge_spec(existing, &parsed).await?
200        } else {
201            // Generate new spec
202            self.spec_generator.generate_spec(&parsed).await?
203        };
204
205        // Convert spec to JSON for response
206        let spec_json = serde_json::to_value(&spec.spec)?;
207
208        // Record AI pillar usage. Merge counts as ai_refinement; new generation as ai_generation.
209        let metric_name = if existing_spec.is_some() {
210            "ai_refinement"
211        } else {
212            "ai_generation"
213        };
214        mockforge_foundation::pillar_tracking::record_ai_usage(
215            None,
216            None,
217            metric_name,
218            serde_json::json!({
219                "type": "mock",
220                "endpoints": parsed.endpoints.len(),
221                "models": parsed.models.len(),
222            }),
223        )
224        .await;
225
226        Ok(MockGenerationResult {
227            spec: Some(spec_json),
228            message: format!(
229                "Successfully {} API '{}' with {} endpoints and {} models",
230                if existing_spec.is_some() {
231                    "updated"
232                } else {
233                    "generated"
234                },
235                parsed.title,
236                parsed.endpoints.len(),
237                parsed.models.len()
238            ),
239            parsed_command: Some(parsed),
240            frozen_artifact: None,
241        })
242    }
243}
244
245impl Default for MockGenerator {
246    fn default() -> Self {
247        Self::new()
248    }
249}
250
251/// Result of mock generation
252#[derive(Debug, Clone, Serialize, Deserialize)]
253pub struct MockGenerationResult {
254    /// Generated OpenAPI spec (if any)
255    pub spec: Option<serde_json::Value>,
256
257    /// Status message
258    pub message: String,
259
260    /// Parsed command details (for debugging/preview)
261    #[serde(skip_serializing_if = "Option::is_none")]
262    pub parsed_command: Option<crate::voice::command_parser::ParsedCommand>,
263
264    /// Frozen artifact (if auto-freeze was enabled)
265    #[serde(skip_serializing_if = "Option::is_none")]
266    pub frozen_artifact: Option<crate::ai_studio::artifact_freezer::FrozenArtifact>,
267}