#[path = "speech_to_text.rs"]
mod speech_to_text;
use clap::Subcommand;
use mockforge_core::multi_tenant::{MultiTenantConfig, MultiTenantWorkspaceRegistry};
use mockforge_core::voice::WorkspaceBuilder;
use mockforge_core::{ConversationManager, HookTranspiler, VoiceCommandParser, VoiceSpecGenerator};
use mockforge_foundation::intelligent_behavior::IntelligentBehaviorConfig;
use mockforge_openapi::OpenApiSpec;
use speech_to_text::{InteractiveVoiceInput, SpeechToTextManager};
use std::io::{self, Write};
use std::path::PathBuf;
#[derive(Subcommand, Debug)]
pub enum VoiceCommands {
Create {
#[arg(short, long)]
output: Option<PathBuf>,
#[arg(long)]
serve: bool,
#[arg(long, default_value = "3000")]
port: u16,
#[arg(short, long)]
command: Option<String>,
},
Interactive {
#[arg(short, long)]
output: Option<PathBuf>,
#[arg(long)]
serve: bool,
#[arg(long, default_value = "3000")]
port: u16,
},
TranspileHook {
#[arg(short, long)]
description: Option<String>,
#[arg(short, long)]
output: Option<PathBuf>,
#[arg(long, default_value = "yaml")]
format: String,
},
CreateWorkspace {
#[arg(short, long)]
command: Option<String>,
#[arg(long)]
yes: bool,
},
}
pub async fn handle_voice_command(
command: VoiceCommands,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
match command {
VoiceCommands::Create {
output,
serve,
port,
command,
} => {
handle_create(output, serve, port, command).await?;
}
VoiceCommands::Interactive {
output,
serve,
port,
} => {
handle_interactive(output, serve, port).await?;
}
VoiceCommands::TranspileHook {
description,
output,
format,
} => {
handle_transpile_hook(description, output, format).await?;
}
VoiceCommands::CreateWorkspace { command, yes } => {
handle_create_workspace(command, yes).await?;
}
}
Ok(())
}
async fn handle_create(
output: Option<PathBuf>,
serve: bool,
port: u16,
command: Option<String>,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
println!("🎤 Voice + LLM Interface - Single Shot Mode");
println!();
let command_text = if let Some(cmd) = command {
cmd
} else {
let stt_manager = SpeechToTextManager::new();
let available_backends = stt_manager.list_backends();
if available_backends.len() > 1 {
println!("🎤 Available input methods: {}", available_backends.join(", "));
}
stt_manager.transcribe().map_err(|e| format!("Failed to get input: {}", e))?
};
if command_text.is_empty() {
return Err("No command provided".into());
}
println!("📝 Command: {}", command_text);
println!("🤖 Parsing command with LLM...");
let config = IntelligentBehaviorConfig::default();
let parser = VoiceCommandParser::new(config);
let parsed = parser.parse_command(&command_text).await?;
println!("✅ Parsed command successfully");
println!(" - API Type: {}", parsed.api_type);
println!(" - Endpoints: {}", parsed.endpoints.len());
println!(" - Models: {}", parsed.models.len());
println!("📋 Generating OpenAPI specification...");
let spec_generator = VoiceSpecGenerator::new();
let spec = spec_generator.generate_spec(&parsed).await?;
println!("✅ Generated OpenAPI spec: {} v{}", spec.title(), spec.api_version());
if let Some(output_path) = output {
let spec_json = serde_json::to_value(&spec.spec)?;
let content = if output_path
.extension()
.and_then(|s| s.to_str())
.map(|s| s == "yaml" || s == "yml")
.unwrap_or(false)
{
serde_yaml::to_string(&spec_json)?
} else {
serde_json::to_string_pretty(&spec_json)?
};
tokio::fs::write(&output_path, content).await?;
println!("💾 Saved OpenAPI spec to: {}", output_path.display());
}
if serve {
println!("🚀 Starting mock server on port {}...", port);
println!("📡 Server will be available at: http://localhost:{}", port);
println!("🛑 Press Ctrl+C to stop the server");
println!();
let temp_spec =
std::env::temp_dir().join(format!("voice-spec-{}.json", uuid::Uuid::new_v4()));
let spec_json = serde_json::to_value(&spec.spec)?;
let content = serde_json::to_string_pretty(&spec_json)?;
tokio::fs::write(&temp_spec, content).await?;
use crate::serve;
serve::handle_serve(serve::ServeArgs {
http_port: Some(port),
admin: true,
spec: vec![temp_spec],
..serve::ServeArgs::default()
})
.await?;
}
Ok(())
}
async fn handle_interactive(
output: Option<PathBuf>,
serve: bool,
port: u16,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
println!("🎤 Voice + LLM Interface - Interactive Mode");
println!("💬 Start a conversation to build your API incrementally");
println!(" Type 'done' or 'exit' when finished");
println!(" Type 'help' for available commands");
println!();
let mut conversation_manager = ConversationManager::new();
let conversation_id = conversation_manager.start_conversation();
let config = IntelligentBehaviorConfig::default();
let parser = VoiceCommandParser::new(config);
let spec_generator = VoiceSpecGenerator::new();
let mut current_spec: Option<OpenApiSpec> = None;
let _voice_input = InteractiveVoiceInput::new();
let stt_manager = SpeechToTextManager::new();
let available_backends = stt_manager.list_backends();
if available_backends.len() > 1 {
println!("🎤 Available input methods: {}", available_backends.join(", "));
}
println!();
loop {
print!("🎤 > ");
Write::flush(&mut io::stdout())?;
let command = match stt_manager.transcribe() {
Ok(text) => text,
Err(e) => {
eprintln!("⚠️ Error getting input: {}", e);
continue;
}
};
if command.is_empty() {
continue;
}
match command.to_lowercase().as_str() {
"done" | "exit" | "quit" => {
println!("✅ Conversation complete!");
break;
}
"help" => {
println!("Available commands:");
println!(" - Describe your API: 'Create an e-commerce API'");
println!(" - Add endpoints: 'Add a products endpoint'");
println!(" - Modify: 'Add checkout flow'");
println!(" - View: 'show spec' or 'show endpoints'");
println!(" - Exit: 'done', 'exit', or 'quit'");
continue;
}
"show spec" | "show endpoints" => {
if let Some(ref spec) = current_spec {
println!("📋 Current API: {} v{}", spec.title(), spec.api_version());
let paths = spec.all_paths_and_operations();
for (path, ops) in paths {
println!(
" {} ({})",
path,
ops.keys().map(|s| s.as_str()).collect::<Vec<_>>().join(", ")
);
}
} else {
println!("ℹ️ No API created yet. Start by describing what you want to build.");
}
continue;
}
_ => {}
}
println!("🤖 Processing: {}", command);
let conversation_state = conversation_manager
.get_conversation(&conversation_id)
.ok_or("Conversation not found")?;
let parsed = if current_spec.is_some() {
parser
.parse_conversational_command(&command, &conversation_state.context)
.await?
} else {
parser.parse_command(&command).await?
};
let new_spec = if let Some(ref existing) = current_spec {
spec_generator.merge_spec(existing, &parsed).await?
} else {
spec_generator.generate_spec(&parsed).await?
};
conversation_manager.update_conversation(
&conversation_id,
&command,
Some(new_spec.clone()),
)?;
current_spec = Some(new_spec.clone());
println!("✅ Updated API: {} v{}", new_spec.title(), new_spec.api_version());
println!(" Endpoints: {}", new_spec.all_paths_and_operations().len());
}
if let Some(ref spec) = current_spec {
if let Some(output_path) = output {
let spec_json = serde_json::to_value(&spec.spec)?;
let content = if output_path
.extension()
.and_then(|s| s.to_str())
.map(|s| s == "yaml" || s == "yml")
.unwrap_or(false)
{
serde_yaml::to_string(&spec_json)?
} else {
serde_json::to_string_pretty(&spec_json)?
};
tokio::fs::write(&output_path, content).await?;
println!("💾 Saved OpenAPI spec to: {}", output_path.display());
}
if serve {
println!("🚀 Starting mock server on port {}...", port);
println!("📡 Server will be available at: http://localhost:{}", port);
println!("🛑 Press Ctrl+C to stop the server");
println!();
let temp_spec =
std::env::temp_dir().join(format!("voice-spec-{}.json", uuid::Uuid::new_v4()));
let spec_json = serde_json::to_value(&spec.spec)?;
let content = serde_json::to_string_pretty(&spec_json)?;
tokio::fs::write(&temp_spec, content).await?;
use crate::serve;
serve::handle_serve(serve::ServeArgs {
http_port: Some(port),
admin: true,
spec: vec![temp_spec],
..serve::ServeArgs::default()
})
.await?;
}
} else {
println!("ℹ️ No API was created. Exiting.");
}
Ok(())
}
async fn handle_transpile_hook(
description: Option<String>,
output: Option<PathBuf>,
format: String,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
println!("🔧 Hook Transpiler - Natural Language to Hook Configuration");
println!();
let description_text = if let Some(desc) = description {
desc
} else {
print!("Enter hook description: ");
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
input.trim().to_string()
};
if description_text.is_empty() {
return Err("No description provided".into());
}
println!("📝 Description: {}", description_text);
println!("🤖 Transpiling hook description with LLM...");
let config = IntelligentBehaviorConfig::default();
let transpiler = HookTranspiler::new(config);
let hook = match transpiler.transpile(&description_text).await {
Ok(hook) => hook,
Err(e) => {
return Err(format!("Failed to transpile hook: {}", e).into());
}
};
println!("✅ Hook transpiled successfully");
let hook_name = hook.get("name").and_then(|v| v.as_str()).unwrap_or("unknown");
let hook_type = hook.get("hook_type").and_then(|v| v.as_str()).unwrap_or("unknown");
let actions_count =
hook.get("actions").and_then(|v| v.as_array()).map(|a| a.len()).unwrap_or(0);
let has_condition = hook.get("condition").is_some();
println!(" - Name: {}", hook_name);
println!(" - Type: {:?}", hook_type);
println!(" - Actions: {}", actions_count);
if has_condition {
println!(" - Has condition: Yes");
}
let content = match format.to_lowercase().as_str() {
"yaml" | "yml" => serde_yaml::to_string(&hook)
.map_err(|e| format!("Failed to serialize hook to YAML: {}", e))?,
"json" => serde_json::to_string_pretty(&hook)
.map_err(|e| format!("Failed to serialize hook to JSON: {}", e))?,
_ => {
return Err(format!("Unsupported format: {}. Use 'yaml' or 'json'", format).into());
}
};
if let Some(output_path) = output {
tokio::fs::write(&output_path, content).await?;
println!("💾 Saved hook configuration to: {}", output_path.display());
} else {
println!();
println!("📄 Generated Hook Configuration:");
println!("{}", "─".repeat(60));
println!("{}", content);
println!("{}", "─".repeat(60));
}
Ok(())
}
async fn handle_create_workspace(
command: Option<String>,
auto_confirm: bool,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
println!("🏗️ Workspace Creator - Natural Language to Complete Workspace");
println!();
println!("This will create a complete workspace with:");
println!(" • Endpoints and API structure");
println!(" • Personas with relationships");
println!(" • Behavioral scenarios (happy path, failure, slow path)");
println!(" • Reality continuum configuration");
println!(" • Drift budget configuration");
println!();
let command_text = if let Some(cmd) = command {
cmd
} else {
let stt_manager = SpeechToTextManager::new();
let available_backends = stt_manager.list_backends();
if available_backends.len() > 1 {
println!("🎤 Available input methods: {}", available_backends.join(", "));
}
println!("🎤 Describe your workspace (or type your command):");
stt_manager.transcribe().map_err(|e| format!("Failed to get input: {}", e))?
};
if command_text.is_empty() {
return Err("No command provided".into());
}
println!("📝 Command: {}", command_text);
println!("🤖 Parsing workspace creation command with LLM...");
let config = IntelligentBehaviorConfig::default();
let parser = VoiceCommandParser::new(config);
let parsed = match parser.parse_workspace_creation_command(&command_text).await {
Ok(parsed) => parsed,
Err(e) => {
return Err(format!("Failed to parse command: {}", e).into());
}
};
println!("✅ Parsed command successfully");
println!();
println!("📋 Workspace Preview:");
println!("{}", "═".repeat(60));
println!("Name: {}", parsed.workspace_name);
println!("Description: {}", parsed.workspace_description);
println!();
println!("Entities: {}", parsed.entities.len());
for entity in &parsed.entities {
println!(" • {} ({} endpoints)", entity.name, entity.endpoints.len());
}
println!();
println!("Personas: {}", parsed.personas.len());
for persona in &parsed.personas {
println!(" • {} ({} relationships)", persona.name, persona.relationships.len());
}
println!();
println!("Scenarios: {}", parsed.scenarios.len());
for scenario in &parsed.scenarios {
println!(" • {} ({})", scenario.name, scenario.r#type);
}
if parsed.reality_continuum.is_some() {
println!();
println!("Reality Continuum: Configured");
}
if parsed.drift_budget.is_some() {
println!("Drift Budget: Configured");
}
println!("{}", "═".repeat(60));
println!();
if !auto_confirm {
print!("Create this workspace? [y/N]: ");
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
let response = input.trim().to_lowercase();
if response != "y" && response != "yes" {
println!("❌ Workspace creation cancelled.");
return Ok(());
}
}
println!("🏗️ Creating workspace...");
println!();
let mt_config = MultiTenantConfig {
enabled: true,
default_workspace: "default".to_string(),
..Default::default()
};
let mut registry = MultiTenantWorkspaceRegistry::new(mt_config);
let mut builder = WorkspaceBuilder::new();
let built = match builder.build_workspace(&mut registry, &parsed).await {
Ok(built) => built,
Err(e) => {
return Err(format!("Failed to create workspace: {}", e).into());
}
};
println!("✅ Workspace created successfully!");
println!();
println!("📊 Creation Summary:");
println!("{}", "─".repeat(60));
for log_entry in &built.creation_log {
println!(" {}", log_entry);
}
println!("{}", "─".repeat(60));
println!();
println!("📦 Workspace Details:");
println!(" ID: {}", built.workspace_id);
println!(" Name: {}", built.name);
if let Some(ref spec) = built.openapi_spec {
println!(" OpenAPI Spec: {} endpoints", spec.all_paths_and_operations().len());
}
println!(" Personas: {}", built.personas.len());
println!(" Scenarios: {}", built.scenarios.len());
if built.reality_continuum.is_some() {
println!(" Reality Continuum: Enabled");
}
if built.drift_budget.is_some() {
println!(" Drift Budget: Configured");
}
println!();
println!("🎉 Workspace '{}' is ready to use!", built.workspace_id);
println!();
println!("💡 Next steps:");
println!(" • Start the MockForge server to use this workspace");
println!(" • Access the workspace via: /workspace/{}", built.workspace_id);
println!(" • View personas and scenarios in the Admin UI");
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_voice_commands_variants() {
let create = VoiceCommands::Create {
output: Some(PathBuf::from("test.yaml")),
serve: false,
port: 3000,
command: Some("test command".to_string()),
};
match create {
VoiceCommands::Create {
output,
serve,
port,
command,
} => {
assert_eq!(output, Some(PathBuf::from("test.yaml")));
assert!(!serve);
assert_eq!(port, 3000);
assert_eq!(command, Some("test command".to_string()));
}
_ => panic!("Expected Create variant"),
}
}
#[test]
fn test_voice_commands_interactive() {
let interactive = VoiceCommands::Interactive {
output: None,
serve: true,
port: 8080,
};
match interactive {
VoiceCommands::Interactive {
output,
serve,
port,
} => {
assert_eq!(output, None);
assert!(serve);
assert_eq!(port, 8080);
}
_ => panic!("Expected Interactive variant"),
}
}
#[test]
fn test_voice_commands_transpile_hook() {
let transpile = VoiceCommands::TranspileHook {
description: Some("test hook".to_string()),
output: Some(PathBuf::from("hook.yaml")),
format: "yaml".to_string(),
};
match transpile {
VoiceCommands::TranspileHook {
description,
output,
format,
} => {
assert_eq!(description, Some("test hook".to_string()));
assert_eq!(output, Some(PathBuf::from("hook.yaml")));
assert_eq!(format, "yaml");
}
_ => panic!("Expected TranspileHook variant"),
}
}
#[test]
fn test_voice_commands_create_workspace() {
let workspace = VoiceCommands::CreateWorkspace {
command: Some("create workspace".to_string()),
yes: true,
};
match workspace {
VoiceCommands::CreateWorkspace { command, yes } => {
assert_eq!(command, Some("create workspace".to_string()));
assert!(yes);
}
_ => panic!("Expected CreateWorkspace variant"),
}
}
#[test]
fn test_voice_commands_debug_format() {
let create = VoiceCommands::Create {
output: Some(PathBuf::from("api.yaml")),
serve: true,
port: 3000,
command: None,
};
let debug_str = format!("{:?}", create);
assert!(debug_str.contains("Create"));
assert!(debug_str.contains("api.yaml"));
}
#[test]
fn test_create_with_default_port() {
let create = VoiceCommands::Create {
output: None,
serve: false,
port: 3000,
command: None,
};
match create {
VoiceCommands::Create { port, .. } => {
assert_eq!(port, 3000);
}
_ => panic!("Expected Create variant"),
}
}
#[test]
fn test_transpile_hook_format_options() {
let yaml_hook = VoiceCommands::TranspileHook {
description: None,
output: None,
format: "yaml".to_string(),
};
match yaml_hook {
VoiceCommands::TranspileHook { format, .. } => {
assert_eq!(format, "yaml");
}
_ => panic!("Expected TranspileHook variant"),
}
let json_hook = VoiceCommands::TranspileHook {
description: None,
output: None,
format: "json".to_string(),
};
match json_hook {
VoiceCommands::TranspileHook { format, .. } => {
assert_eq!(format, "json");
}
_ => panic!("Expected TranspileHook variant"),
}
}
#[test]
fn test_interactive_with_custom_port() {
let custom_port = 9999;
let interactive = VoiceCommands::Interactive {
output: Some(PathBuf::from("output.json")),
serve: false,
port: custom_port,
};
match interactive {
VoiceCommands::Interactive { port, .. } => {
assert_eq!(port, custom_port);
}
_ => panic!("Expected Interactive variant"),
}
}
#[test]
fn test_create_workspace_without_auto_confirm() {
let workspace = VoiceCommands::CreateWorkspace {
command: None,
yes: false,
};
match workspace {
VoiceCommands::CreateWorkspace { yes, .. } => {
assert!(!yes);
}
_ => panic!("Expected CreateWorkspace variant"),
}
}
#[test]
fn test_pathbuf_handling() {
let path = PathBuf::from("/tmp/test.yaml");
let create = VoiceCommands::Create {
output: Some(path.clone()),
serve: false,
port: 3000,
command: None,
};
match create {
VoiceCommands::Create { output, .. } => {
assert_eq!(output, Some(path));
}
_ => panic!("Expected Create variant"),
}
}
#[test]
fn test_optional_command_text() {
let with_command = VoiceCommands::Create {
output: None,
serve: false,
port: 3000,
command: Some("create API".to_string()),
};
match with_command {
VoiceCommands::Create { command, .. } => {
assert!(command.is_some());
assert_eq!(command.unwrap(), "create API");
}
_ => panic!("Expected Create variant"),
}
let without_command = VoiceCommands::Create {
output: None,
serve: false,
port: 3000,
command: None,
};
match without_command {
VoiceCommands::Create { command, .. } => {
assert!(command.is_none());
}
_ => panic!("Expected Create variant"),
}
}
#[test]
fn test_serve_flag_combinations() {
let create1 = VoiceCommands::Create {
output: None,
serve: true,
port: 3000,
command: None,
};
match create1 {
VoiceCommands::Create { output, serve, .. } => {
assert!(output.is_none());
assert!(serve);
}
_ => panic!("Expected Create variant"),
}
let create2 = VoiceCommands::Create {
output: Some(PathBuf::from("api.yaml")),
serve: true,
port: 3000,
command: None,
};
match create2 {
VoiceCommands::Create { output, serve, .. } => {
assert!(output.is_some());
assert!(serve);
}
_ => panic!("Expected Create variant"),
}
}
}