mockforge_ui/handlers/
voice.rs

1//! Voice + LLM Interface API handlers for Admin UI
2//!
3//! Provides endpoints for processing voice commands and generating OpenAPI specs
4//! using natural language commands powered by LLM.
5
6use axum::{
7    extract::{Json, State},
8    http::StatusCode,
9    response::Json as ResponseJson,
10};
11use mockforge_core::intelligent_behavior::IntelligentBehaviorConfig;
12use mockforge_core::voice::{
13    command_parser::{ParsedWorkspaceCreation, VoiceCommandParser},
14    hook_transpiler::HookTranspiler,
15    spec_generator::VoiceSpecGenerator,
16    workspace_builder::WorkspaceBuilder,
17    workspace_scenario_generator::{GeneratedWorkspaceScenario, WorkspaceScenarioGenerator},
18};
19use serde::{Deserialize, Serialize};
20use serde_json::Value;
21
22use crate::handlers::workspaces::WorkspaceState;
23use crate::models::ApiResponse;
24
25/// Request to process a voice command
26#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct ProcessVoiceCommandRequest {
28    /// The voice command text (transcribed from speech or typed)
29    pub command: String,
30    /// Optional conversation ID for multi-turn interactions
31    #[serde(default)]
32    pub conversation_id: Option<String>,
33}
34
35/// Response from processing a voice command
36#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct ProcessVoiceCommandResponse {
38    /// The original command
39    pub command: String,
40    /// Parsed command structure
41    pub parsed: ParsedCommandData,
42    /// Generated OpenAPI spec (as JSON)
43    pub spec: Option<Value>,
44    /// Optional error message
45    pub error: Option<String>,
46}
47
48/// Parsed command data structure
49#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct ParsedCommandData {
51    /// API type/category
52    pub api_type: String,
53    /// API title
54    pub title: String,
55    /// API description
56    pub description: String,
57    /// List of endpoints
58    pub endpoints: Vec<Value>,
59    /// List of data models
60    pub models: Vec<Value>,
61}
62
63/// Process a voice command and generate an OpenAPI spec
64///
65/// POST /api/v2/voice/process
66pub async fn process_voice_command(
67    Json(request): Json<ProcessVoiceCommandRequest>,
68) -> Result<ResponseJson<ApiResponse<ProcessVoiceCommandResponse>>, StatusCode> {
69    if request.command.trim().is_empty() {
70        return Err(StatusCode::BAD_REQUEST);
71    }
72
73    // Create parser with default config
74    let config = IntelligentBehaviorConfig::default();
75    let parser = VoiceCommandParser::new(config);
76
77    // Parse the command
78    let parsed = match parser.parse_command(&request.command).await {
79        Ok(parsed) => parsed,
80        Err(e) => {
81            return Ok(ResponseJson(ApiResponse::error(format!("Failed to parse command: {}", e))));
82        }
83    };
84
85    // Generate OpenAPI spec
86    let spec_generator = VoiceSpecGenerator::new();
87    let spec_result = spec_generator.generate_spec(&parsed).await;
88    let spec = match spec_result {
89        Ok(spec) => {
90            // Convert spec to JSON and include title/version in response
91            let mut spec_json = serde_json::to_value(&spec.spec).unwrap_or(Value::Null);
92            // Add title and version to the spec JSON for easier frontend access
93            if let Value::Object(ref mut obj) = spec_json {
94                if let Some(Value::Object(ref mut info)) = obj.get_mut("info") {
95                    // Ensure title and version are present
96                    if !info.contains_key("title") {
97                        info.insert("title".to_string(), Value::String(parsed.title.clone()));
98                    }
99                    if !info.contains_key("version") {
100                        info.insert("version".to_string(), Value::String("1.0.0".to_string()));
101                    }
102                }
103            }
104            Some(spec_json)
105        }
106        Err(e) => {
107            return Ok(ResponseJson(ApiResponse::error(format!("Failed to generate spec: {}", e))));
108        }
109    };
110
111    // Convert parsed command to response format
112    let parsed_data = ParsedCommandData {
113        api_type: parsed.api_type.clone(),
114        title: parsed.title.clone(),
115        description: parsed.description.clone(),
116        endpoints: parsed
117            .endpoints
118            .iter()
119            .map(|e| serde_json::to_value(e).unwrap_or(Value::Null))
120            .collect(),
121        models: parsed
122            .models
123            .iter()
124            .map(|m| serde_json::to_value(m).unwrap_or(Value::Null))
125            .collect(),
126    };
127
128    let response = ProcessVoiceCommandResponse {
129        command: request.command,
130        parsed: parsed_data,
131        spec,
132        error: None,
133    };
134
135    Ok(ResponseJson(ApiResponse::success(response)))
136}
137
138/// Request to transpile a natural language hook description
139#[derive(Debug, Clone, Serialize, Deserialize)]
140pub struct TranspileHookRequest {
141    /// Natural language description of the hook logic
142    pub description: String,
143}
144
145/// Response from transpiling a hook description
146#[derive(Debug, Clone, Serialize, Deserialize)]
147pub struct TranspileHookResponse {
148    /// The original description
149    pub description: String,
150    /// Transpiled hook configuration (as YAML)
151    pub hook_yaml: Option<String>,
152    /// Transpiled hook configuration (as JSON)
153    pub hook_json: Option<Value>,
154    /// Optional error message
155    pub error: Option<String>,
156}
157
158/// Transpile a natural language hook description to hook configuration
159///
160/// POST /api/v2/voice/transpile-hook
161pub async fn transpile_hook(
162    Json(request): Json<TranspileHookRequest>,
163) -> Result<ResponseJson<ApiResponse<TranspileHookResponse>>, StatusCode> {
164    if request.description.trim().is_empty() {
165        return Err(StatusCode::BAD_REQUEST);
166    }
167
168    // Create transpiler with default config
169    let config = IntelligentBehaviorConfig::default();
170    let transpiler = HookTranspiler::new(config);
171
172    // Transpile the description
173    let hook = match transpiler.transpile(&request.description).await {
174        Ok(hook) => hook,
175        Err(e) => {
176            return Ok(ResponseJson(ApiResponse::error(format!(
177                "Failed to transpile hook: {}",
178                e
179            ))));
180        }
181    };
182
183    // Convert hook to YAML and JSON
184    let hook_yaml = match serde_yaml::to_string(&hook) {
185        Ok(yaml) => Some(yaml),
186        Err(e) => {
187            return Ok(ResponseJson(ApiResponse::error(format!(
188                "Failed to serialize hook to YAML: {}",
189                e
190            ))));
191        }
192    };
193
194    let hook_json = match serde_json::to_value(&hook) {
195        Ok(json) => Some(json),
196        Err(e) => {
197            return Ok(ResponseJson(ApiResponse::error(format!(
198                "Failed to serialize hook to JSON: {}",
199                e
200            ))));
201        }
202    };
203
204    let response = TranspileHookResponse {
205        description: request.description,
206        hook_yaml,
207        hook_json,
208        error: None,
209    };
210
211    Ok(ResponseJson(ApiResponse::success(response)))
212}
213
214/// Request to create a workspace scenario
215#[derive(Debug, Clone, Serialize, Deserialize)]
216pub struct CreateWorkspaceScenarioRequest {
217    /// Natural language description of the scenario
218    pub description: String,
219}
220
221/// Response from creating a workspace scenario
222#[derive(Debug, Clone, Serialize, Deserialize)]
223pub struct CreateWorkspaceScenarioResponse {
224    /// The original description
225    pub description: String,
226    /// Generated workspace scenario
227    pub scenario: Option<GeneratedWorkspaceScenario>,
228    /// Optional error message
229    pub error: Option<String>,
230}
231
232/// Create a workspace scenario from natural language description
233///
234/// POST /api/v2/voice/create-workspace-scenario
235pub async fn create_workspace_scenario(
236    Json(request): Json<CreateWorkspaceScenarioRequest>,
237) -> Result<ResponseJson<ApiResponse<CreateWorkspaceScenarioResponse>>, StatusCode> {
238    if request.description.trim().is_empty() {
239        return Err(StatusCode::BAD_REQUEST);
240    }
241
242    // Create parser with default config
243    let config = IntelligentBehaviorConfig::default();
244    let parser = VoiceCommandParser::new(config);
245
246    // Parse the scenario description
247    let parsed = match parser.parse_workspace_scenario_command(&request.description).await {
248        Ok(parsed) => parsed,
249        Err(e) => {
250            return Ok(ResponseJson(ApiResponse::error(format!(
251                "Failed to parse scenario description: {}",
252                e
253            ))));
254        }
255    };
256
257    // Generate the workspace scenario
258    let generator = WorkspaceScenarioGenerator::new();
259    let scenario = match generator.generate_scenario(&parsed).await {
260        Ok(scenario) => Some(scenario),
261        Err(e) => {
262            return Ok(ResponseJson(ApiResponse::error(format!(
263                "Failed to generate workspace scenario: {}",
264                e
265            ))));
266        }
267    };
268
269    let response = CreateWorkspaceScenarioResponse {
270        description: request.description,
271        scenario,
272        error: None,
273    };
274
275    Ok(ResponseJson(ApiResponse::success(response)))
276}
277
278/// Request to create a workspace from natural language
279#[derive(Debug, Clone, Serialize, Deserialize)]
280pub struct CreateWorkspaceRequest {
281    /// Natural language description of the workspace
282    pub description: String,
283}
284
285/// Response from parsing workspace creation command (preview)
286#[derive(Debug, Clone, Serialize, Deserialize)]
287pub struct CreateWorkspacePreviewResponse {
288    /// The original description
289    pub description: String,
290    /// Parsed workspace creation data (for preview)
291    pub parsed: ParsedWorkspaceCreation,
292    /// Optional error message
293    pub error: Option<String>,
294}
295
296/// Request to confirm and create workspace
297#[derive(Debug, Clone, Serialize, Deserialize)]
298pub struct ConfirmCreateWorkspaceRequest {
299    /// Parsed workspace creation data (from preview)
300    pub parsed: ParsedWorkspaceCreation,
301}
302
303/// Response from creating a workspace
304#[derive(Debug, Clone, Serialize, Deserialize)]
305pub struct CreateWorkspaceResponse {
306    /// Workspace ID
307    pub workspace_id: String,
308    /// Workspace name
309    pub name: String,
310    /// Creation log
311    pub creation_log: Vec<String>,
312    /// Number of endpoints created
313    pub endpoint_count: usize,
314    /// Number of personas created
315    pub persona_count: usize,
316    /// Number of scenarios created
317    pub scenario_count: usize,
318    /// Whether reality continuum is configured
319    pub has_reality_continuum: bool,
320    /// Whether drift budget is configured
321    pub has_drift_budget: bool,
322    /// Optional error message
323    pub error: Option<String>,
324}
325
326/// Parse workspace creation command and return preview
327///
328/// POST /api/v2/voice/create-workspace-preview
329pub async fn create_workspace_preview(
330    Json(request): Json<CreateWorkspaceRequest>,
331) -> Result<ResponseJson<ApiResponse<CreateWorkspacePreviewResponse>>, StatusCode> {
332    if request.description.trim().is_empty() {
333        return Err(StatusCode::BAD_REQUEST);
334    }
335
336    // Create parser with default config
337    let config = IntelligentBehaviorConfig::default();
338    let parser = VoiceCommandParser::new(config);
339
340    // Parse the workspace creation command
341    let parsed = match parser.parse_workspace_creation_command(&request.description).await {
342        Ok(parsed) => parsed,
343        Err(e) => {
344            return Ok(ResponseJson(ApiResponse::error(format!(
345                "Failed to parse workspace creation command: {}",
346                e
347            ))));
348        }
349    };
350
351    let response = CreateWorkspacePreviewResponse {
352        description: request.description,
353        parsed,
354        error: None,
355    };
356
357    Ok(ResponseJson(ApiResponse::success(response)))
358}
359
360/// Confirm and create workspace from parsed command
361///
362/// POST /api/v2/voice/create-workspace-confirm
363pub async fn create_workspace_confirm(
364    State(state): State<WorkspaceState>,
365    Json(request): Json<ConfirmCreateWorkspaceRequest>,
366) -> Result<ResponseJson<ApiResponse<CreateWorkspaceResponse>>, StatusCode> {
367    // Create workspace builder
368    let mut builder = WorkspaceBuilder::new();
369
370    // Get mutable access to workspace registry from state
371    let mut registry = state.registry.write().await;
372
373    // Build workspace
374    let built = match builder.build_workspace(&mut registry, &request.parsed).await {
375        Ok(built) => built,
376        Err(e) => {
377            return Ok(ResponseJson(ApiResponse::error(format!(
378                "Failed to create workspace: {}",
379                e
380            ))));
381        }
382    };
383
384    let endpoint_count = built
385        .openapi_spec
386        .as_ref()
387        .map(|s| s.all_paths_and_operations().len())
388        .unwrap_or(0);
389
390    let response = CreateWorkspaceResponse {
391        workspace_id: built.workspace_id,
392        name: built.name,
393        creation_log: built.creation_log,
394        endpoint_count,
395        persona_count: built.personas.len(),
396        scenario_count: built.scenarios.len(),
397        has_reality_continuum: built.reality_continuum.is_some(),
398        has_drift_budget: built.drift_budget.is_some(),
399        error: None,
400    };
401
402    Ok(ResponseJson(ApiResponse::success(response)))
403}