use std::sync::Arc; use std::time::Duration; use serde::Serialize; use tauri::{AppHandle, Emitter, Manager, State}; mod gpu; mod model_library; mod model_files; mod model_alias; mod runtime; use runtime::process::find_localllm_binary; use runtime::types::{RuntimeConfig, RuntimeSnapshot}; use runtime::{ bootstrap_runtime, get_runtime_snapshot, save_runtime_config_command, start_runtime, start_runtime_with_status, stop_runtime, RuntimeState, }; use gpu::query_gpu; use model_library::huggingface::{ cancel_hugging_face_download, search_hugging_face_models, start_hugging_face_download, HuggingFaceDownloadState, }; use model_library::local_index::list_entries; use model_library::paths::{managed_models_dir, model_library_index_path, resolve_models_dir}; use runtime::config::{load_runtime_config, save_runtime_config}; use model_library::types::{ModelLibraryEntry, ModelLibraryState}; use model_files::{import_model, pick_model_files, pick_models_dir, scan_models}; // ─── ForgeAgent types ───────────────────────────────────────────────────────── #[derive(Clone, Serialize)] #[serde(rename_all = "lowercase")] enum ForgeAgentStepStatus { Pending, Running, Completed, Error, } #[derive(Clone, Serialize)] struct ForgeAgentSubStep { id: String, description: String, status: ForgeAgentStepStatus, } #[derive(Clone, Serialize)] struct ForgeAgentStep { id: u32, title: String, description: String, status: ForgeAgentStepStatus, sub_steps: Vec, } #[derive(Clone, Serialize)] struct ForgeAgentPlan { title: String, overview: String, steps: Vec, } #[derive(Clone, Serialize)] struct ForgeAgentArtifact { filename: String, content: String, language: String, cta_label: String, } #[derive(Clone, Serialize)] struct ForgeAgentStartPayload { plan_id: String, plan: ForgeAgentPlan, } #[derive(Clone, Serialize)] struct ForgeAgentStepStartedPayload { plan_id: String, step_id: u32, title: String, } #[derive(Clone, Serialize)] struct ForgeAgentStepCompletedPayload { plan_id: String, step_id: u32, message: String, } #[derive(Clone, Serialize)] struct ForgeAgentArtifactReadyPayload { plan_id: String, artifact: ForgeAgentArtifact, } #[derive(Clone, Serialize)] struct ForgeAgentPlanCompletedPayload { plan_id: String, summary: String, } // ─── Specialist event types ─────────────────────────────────────────────────── #[derive(Clone, Serialize)] struct SpecialistThinkingStartedPayload { plan_id: String, specialist_id: String, specialist_name: String, specialist_title: String, } #[derive(Clone, Serialize)] struct SpecialistThinkingUpdatePayload { plan_id: String, specialist_id: String, thinking: String, } #[derive(Clone, Serialize)] struct SpecialistResponseCompletedPayload { plan_id: String, specialist_id: String, specialist_name: String, thinking: String, response: String, } #[derive(Clone, Serialize)] struct AgentFinalResponsePayload { plan_id: String, summary: String, } // ─── Specialist definitions ─────────────────────────────────────────────────── struct Specialist { id: &'static str, name: &'static str, title: &'static str, emoji: &'static str, prompt_snippet: &'static str, } const SPECIALISTS: &[Specialist] = &[ Specialist { id: "analyst", name: "Analista", title: "Análise Técnica", emoji: "🔍", prompt_snippet: "Você é um analista técnico rigoroso. Analise o problema decompondo-o em componentes claros, identificando requisitos funcionais e não-funcionais, e avaliando trade-offs.", }, Specialist { id: "architect", name: "Arquiteto", title: "Arquitetura de Software", emoji: "🏗️", prompt_snippet: "Você é um arquiteto de software experiente. Analise a estrutura, identifique padrões relevantes, proponha abordagens escaláveis e considere a evolução do sistema.", }, Specialist { id: "developer", name: "Desenvolvedor", title: "Implementação", emoji: "💻", prompt_snippet: "Você é um desenvolvedor pragmático. Foque em código funcional, práticas modernas, legibilidade e manutenibilidade.", }, Specialist { id: "critic", name: "Crítico", title: "Revisão e Riscos", emoji: "⚖️", prompt_snippet: "Você é um crítico técnico. Identifique riscos, pontos fracos, casos edge e possíveis problemas antes de recomendar uma solução.", }, ]; fn build_forgeagent_plan(prompt: &str) -> ForgeAgentPlan { let trimmed = prompt.trim().trim_end_matches('?'); let title = if trimmed.is_empty() { "Planejando a tarefa".to_string() } else { trimmed.chars().take(48).collect() }; let mut title_chars = title.chars(); let capitalized_title = match title_chars.next() { Some(first) => first.to_uppercase().collect::() + title_chars.as_str(), None => "Planejando a tarefa".to_string(), }; ForgeAgentPlan { title: capitalized_title, overview: "Vou estruturar um plano curto, executar cada etapa visualmente e entregar a resposta final no estilo ForgeAgent.".to_string(), steps: vec![ ForgeAgentStep { id: 1, title: "Entender a solicitação".to_string(), description: "Quebrar o pedido em objetivo, stack e formato de entrega.".to_string(), status: ForgeAgentStepStatus::Pending, sub_steps: vec![ForgeAgentSubStep { id: "1.1".to_string(), description: "Extrair o objetivo principal do prompt".to_string(), status: ForgeAgentStepStatus::Pending, }], }, ForgeAgentStep { id: 2, title: "Preparar a solução".to_string(), description: "Montar a estrutura, dependências e fluxo recomendado.".to_string(), status: ForgeAgentStepStatus::Pending, sub_steps: vec![ForgeAgentSubStep { id: "2.1".to_string(), description: "Organizar a implementação em blocos claros".to_string(), status: ForgeAgentStepStatus::Pending, }], }, ForgeAgentStep { id: 3, title: "Gerar a resposta final".to_string(), description: "Entregar o conteúdo final com orientação prática e artefato.".to_string(), status: ForgeAgentStepStatus::Pending, sub_steps: vec![ForgeAgentSubStep { id: "3.1".to_string(), description: "Consolidar a saída final para o usuário".to_string(), status: ForgeAgentStepStatus::Pending, }], }, ], } } fn resolve_runtime_url(config: &RuntimeConfig) -> String { match config.connection_mode { runtime::types::ConnectionMode::Local => format!("http://{}", config.addr.trim()), runtime::types::ConnectionMode::Remote => config.server_url.trim().trim_end_matches('/').to_string(), } } fn build_multiagent_system_prompt(user_prompt: &str) -> String { let mut specialist_prompts = String::new(); for spec in SPECIALISTS { specialist_prompts.push_str(&format!( "- **{}** ({}) {}: {}\n", spec.name, spec.title, spec.emoji, spec.prompt_snippet )); } format!( r#"Você é o agente principal de um sistema multi-agente. Sua tarefa é coordenar múltiplos especialistas para responder à pergunta do usuário da melhor forma possível. **Especialistas disponíveis:** {} **Tarefa:** A pergunta do usuário é: "{}" **Instruções:** 1. Analise a pergunta e identifique quais especialistas são relevantes (1 a 4) 2. Para cada especialista relevante, gere uma resposta no seguinte formato JSON: {{ "specialist_id": "id_do_especialista", "specialist_name": "Nome", "thinking": "Sua cadeia de raciocínio - pense passo a passo sobre o problema", "response": "Sua resposta final ao usuário sobre o assunto" }} 3. After all specialists have responded, provide your synthesis as the main agent: {{ "type": "final_response", "summary": "Resumo consolidado de todas as perspectivas, com a recomendação final para o usuário" }} **Importante:** - Responda TODOS os especialistas relevantes em uma única mensagem JSON - O campo "thinking" deve mostrar a cadeia de raciocínio completa - O campo "response" deve ser uma resposta substancial, não um resumo - Após os especialistas, inclua sempre a resposta final do agente principal - NÃO gere artefatos de arquivo a menos que o usuário solicite explicitamente - Responda em português brasileiro"#, specialist_prompts, user_prompt ) } // ─── Server lifecycle ───────────────────────────────────────────────────────── #[tauri::command] fn bootstrap_runtime_command( app: AppHandle, state: State, ) -> Result { bootstrap_runtime(app, state) } #[tauri::command] fn get_runtime_snapshot_command(state: State) -> Result { get_runtime_snapshot(state) } #[tauri::command] fn save_runtime_config_command_handler( app: AppHandle, state: State, config: RuntimeConfig, ) -> Result { save_runtime_config_command(app, state, config) } #[tauri::command] fn start_runtime_command( app: AppHandle, state: State, ) -> Result { start_runtime(app, state) } #[tauri::command] fn stop_runtime_command(state: State) -> Result { stop_runtime(state) } /// Returns the resolved path to the localllm binary (useful for debugging). #[tauri::command] fn find_server_binary() -> Result { find_localllm_binary() .map(|p| p.to_string_lossy().to_string()) .ok_or_else(|| { "localllm binary not found. Build with: cargo build --release -p inference-server" .to_string() }) } /// Returns the app data directory path (useful for debugging). #[tauri::command] fn get_app_data_dir(app: AppHandle) -> Result { app.path() .app_data_dir() .map(|p| p.to_string_lossy().to_string()) .map_err(|e| format!("Cannot resolve app data dir: {e}")) } #[tauri::command] async fn search_hugging_face_models_command( query: String, token: Option, ) -> Result, String> { search_hugging_face_models(&query, token.as_deref()).await } #[tauri::command] async fn start_hugging_face_download_command( app: AppHandle, state: State<'_, Arc>, repo: String, file_name: String, token: Option, ) -> Result { // Load the user-configured models_dir (defaults to ./models if None/empty) let config = load_runtime_config(&app)?; let models_root = resolve_models_dir(config.models_dir.as_deref()); start_hugging_face_download( app, state.inner().clone(), repo, file_name, token, models_root, ) .await } #[tauri::command] fn cancel_hugging_face_download_command( state: State<'_, Arc>, download_id: String, ) -> Result<(), String> { cancel_hugging_face_download(state.inner(), &download_id) } /// Swap the active model: stops the runtime, updates model_path, restarts. /// This is the hot-swap primitive — used when user picks a different local model. #[tauri::command] async fn swap_model_command( app: AppHandle, state: State<'_, RuntimeState>, new_model_path: String, ) -> Result { // Stop first — this is a no-op if already stopped stop_runtime(state.clone())?; // Load, update model_path, save let mut config = load_runtime_config(&app)?; config.model_path = Some(new_model_path.clone()); save_runtime_config(&app, &config) .map_err(|e| format!("swap save failed: {e}"))?; // CRITICAL: Update the internal state config so start_runtime uses the new model { let mut inner = state.0.lock().map_err(|e| e.to_string())?; inner.config = config; } // Restart with the updated config start_runtime_with_status(app, state, runtime::types::RuntimeStatus::Loading) } /// Delete a local .gguf model file from disk. #[tauri::command] fn delete_model_file(file_path: String) -> Result<(), String> { let path = std::path::Path::new(&file_path); if !path.exists() { return Err(format!("File not found: {file_path}")); } std::fs::remove_file(path).map_err(|e| format!("Delete failed: {e}")) } /// Resolve which model should be used at startup based on alias policy. /// Returns None if no suitable model is found. #[tauri::command] fn resolve_startup_model_command( app: AppHandle, ) -> Result, String> { use crate::model_library::paths::resolve_models_dir; let config = runtime::config::load_runtime_config(&app).unwrap_or_default(); let models_dir = resolve_models_dir(config.models_dir.as_deref()); let models = if models_dir.exists() { let dir_str = models_dir.to_string_lossy().to_string(); model_files::scan_models(dir_str).unwrap_or_default() } else { Vec::new() }; let preferred = config.preferred_startup_model_alias.as_deref(); Ok(model_alias::resolve_startup_model(preferred, &models)) } /// Open the folder containing a model file in the native file explorer /// and optionally select/highlight the file. #[tauri::command] fn open_model_folder_command(file_path: String) -> Result<(), String> { let path = std::path::Path::new(&file_path); let _folder = path.parent().unwrap_or(path); #[cfg(target_os = "windows")] { std::process::Command::new("explorer") .args(["/select,", &file_path]) .spawn() .map_err(|e| format!("Failed to open Explorer: {e}"))?; } #[cfg(target_os = "macos")] { std::process::Command::new("open") .args(["-R", &file_path]) .spawn() .map_err(|e| format!("Failed to open Finder: {e}"))?; } #[cfg(target_os = "linux")] { std::process::Command::new("xdg-open") .arg(folder) .spawn() .map_err(|e| format!("Failed to open file manager: {e}"))?; } #[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))] { let _ = folder; // folder is only used on some platforms return Err("Unsupported platform".into()); } Ok(()) } #[tauri::command] fn list_model_library_entries_command(app: AppHandle) -> Result, String> { let app_data_dir = app .path() .app_data_dir() .map_err(|error| format!("Cannot resolve app data dir: {error}"))?; let index_path = model_library_index_path(&app_data_dir); list_entries(&index_path) } #[tauri::command] fn get_model_library_state_command(app: AppHandle) -> Result { let app_data_dir = app .path() .app_data_dir() .map_err(|error| format!("Cannot resolve app data dir: {error}"))?; let index_path = model_library_index_path(&app_data_dir); Ok(ModelLibraryState { entries: list_entries(&index_path)?, managed_dir: managed_models_dir(&app_data_dir).to_string_lossy().to_string(), }) } #[derive(serde::Deserialize)] struct ChatCompletionMessage { role: String, content: String, } #[derive(serde::Deserialize)] struct ChatCompletionChoice { message: ChatCompletionMessage, } #[derive(serde::Deserialize)] struct ChatCompletionResponse { choices: Vec, } #[tauri::command] async fn start_forgeagent_session_command( app: AppHandle, state: State<'_, RuntimeState>, prompt: String, ) -> Result { let now = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .map_err(|error| format!("clock error: {error}"))? .as_millis(); let plan_id = format!("forgeagent-{now}"); let plan = build_forgeagent_plan(&prompt); let response = ForgeAgentStartPayload { plan_id: plan_id.clone(), plan: plan.clone(), }; let app_handle = app.clone(); // Get runtime state and validate before proceeding let (runtime_url, connection_mode) = { let inner = state.0.lock().map_err(|e| e.to_string())?; let url = resolve_runtime_url(&inner.config); let mode = inner.config.connection_mode.clone(); (url, mode) }; // Validate runtime state for local mode before spawning background task if connection_mode == runtime::types::ConnectionMode::Local { let snapshot = get_runtime_snapshot_command(state.clone())?; if !snapshot.process_running { let _ = app_handle.emit( "forgeagent:plan-completed", ForgeAgentPlanCompletedPayload { plan_id: plan_id.clone(), summary: "O runtime local não está iniciado. Inicie o runtime antes de usar o ForgeAgent.".to_string(), }, ); return Err("Runtime not running".into()); } if !snapshot.ready { let error_message = match snapshot.status.as_str() { "loading" => "O modelo está carregando. Aguarde antes de enviar mensagens.".to_string(), "error" => { snapshot.last_error .map(|e| format!("Erro no runtime: {}", e.message)) .unwrap_or_else(|| "O runtime encontrou um erro. Verifique os logs.".to_string()) }, "starting" => "O runtime está iniciando. Aguarde...".to_string(), "stopping" => "O runtime está parando. Aguarde finalizar.".to_string(), _ => "O runtime não está pronto. Tente reiniciar.".to_string(), }; let _ = app_handle.emit( "forgeagent:plan-completed", ForgeAgentPlanCompletedPayload { plan_id: plan_id.clone(), summary: error_message, }, ); return Err("Runtime not ready".into()); } } let system_prompt = build_multiagent_system_prompt(&prompt); // Spawn background task std::thread::spawn(move || { // Simulate step progression for step in &plan.steps { let _ = app_handle.emit( "forgeagent:step-started", ForgeAgentStepStartedPayload { plan_id: plan_id.clone(), step_id: step.id, title: step.title.clone(), }, ); std::thread::sleep(Duration::from_millis(300)); let _ = app_handle.emit( "forgeagent:step-completed", ForgeAgentStepCompletedPayload { plan_id: plan_id.clone(), step_id: step.id, message: match step.id { 1 => "Analisei a solicitação e defini o objetivo principal.".to_string(), 2 => "Estruturei a solução e organizei a execução em blocos claros.".to_string(), _ => "Consolidei a entrega final.".to_string(), }, }, ); } // Build HTTP client let Ok(client) = reqwest::blocking::Client::builder() .timeout(Duration::from_secs(120)) .build() else { eprintln!("Failed to create HTTP client"); let _ = app_handle.emit( "forgeagent:plan-completed", ForgeAgentPlanCompletedPayload { plan_id: plan_id.clone(), summary: "Desculpe, houve um erro ao processar sua solicitação. Tente novamente.".to_string(), }, ); return; }; let request_body = serde_json::json!({ "model": "local", "messages": [ { "role": "system", "content": system_prompt }, { "role": "user", "content": &prompt } ], "stream": false, "max_tokens": 2048 }); let Ok(resp) = client .post(format!("{}/v1/chat/completions", runtime_url)) .header("Content-Type", "application/json") .json(&request_body) .send() else { eprintln!("Failed to call LLM"); let _ = app_handle.emit( "forgeagent:plan-completed", ForgeAgentPlanCompletedPayload { plan_id: plan_id.clone(), summary: "Desculpe, houve um erro de comunicação com o servidor de inferência.".to_string(), }, ); return; }; if !resp.status().is_success() { eprintln!("LLM request failed: {}", resp.status()); let _ = app_handle.emit( "forgeagent:plan-completed", ForgeAgentPlanCompletedPayload { plan_id: plan_id.clone(), summary: "Desculpe, o servidor de inferência não está disponível. Verifique se o modelo está carregado.".to_string(), }, ); return; } let Ok(data) = resp.json::() else { eprintln!("Failed to parse LLM response"); let _ = app_handle.emit( "forgeagent:plan-completed", ForgeAgentPlanCompletedPayload { plan_id: plan_id.clone(), summary: "Desculpe, houve um erro ao processar a resposta. Tente novamente.".to_string(), }, ); return; }; let content = data.choices.first() .map(|c| c.message.content.clone()) .unwrap_or_default(); let Ok(json) = serde_json::from_str::(&content) else { let _ = app_handle.emit( "forgeagent:plan-completed", ForgeAgentPlanCompletedPayload { plan_id: plan_id.clone(), summary: content, }, ); return; }; if let Some(specialists) = json.get("specialists").and_then(|v| v.as_array()) { for spec in specialists { let specialist_id = spec.get("specialist_id") .and_then(|v| v.as_str()).unwrap_or("unknown").to_string(); let specialist_name = spec.get("specialist_name") .and_then(|v| v.as_str()).unwrap_or("Especialista").to_string(); let thinking = spec.get("thinking") .and_then(|v| v.as_str()).unwrap_or("").to_string(); let spec_response = spec.get("response") .and_then(|v| v.as_str()).unwrap_or("").to_string(); let _ = app_handle.emit( "forgeagent:specialist-thought-started", SpecialistThinkingStartedPayload { plan_id: plan_id.clone(), specialist_id: specialist_id.clone(), specialist_name: specialist_name.clone(), specialist_title: specialist_name.clone(), }, ); if !thinking.is_empty() { let _ = app_handle.emit( "forgeagent:specialist-thought-update", SpecialistThinkingUpdatePayload { plan_id: plan_id.clone(), specialist_id: specialist_id.clone(), thinking: thinking.clone(), }, ); } let _ = app_handle.emit( "forgeagent:specialist-response-completed", SpecialistResponseCompletedPayload { plan_id: plan_id.clone(), specialist_id, specialist_name, thinking, response: spec_response, }, ); } } let summary = json.get("summary") .and_then(|v| v.as_str()) .unwrap_or(&content) .to_string(); let _ = app_handle.emit( "forgeagent:plan-completed", ForgeAgentPlanCompletedPayload { plan_id: plan_id.clone(), summary, }, ); }); Ok(response) } // ─── App entry ──────────────────────────────────────────────────────────────── #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() .plugin(tauri_plugin_shell::init()) .plugin(tauri_plugin_dialog::init()) .manage(RuntimeState::new()) .manage(Arc::new(HuggingFaceDownloadState::default())) .invoke_handler(tauri::generate_handler![ query_gpu, scan_models, pick_models_dir, pick_model_files, import_model, search_hugging_face_models_command, start_hugging_face_download_command, cancel_hugging_face_download_command, swap_model_command, delete_model_file, open_model_folder_command, list_model_library_entries_command, get_model_library_state_command, bootstrap_runtime_command, get_runtime_snapshot_command, save_runtime_config_command_handler, start_runtime_command, stop_runtime_command, resolve_startup_model_command, get_app_data_dir, find_server_binary, start_forgeagent_session_command, ]) .setup(|app| { #[cfg(debug_assertions)] { let window = app.get_webview_window("main").unwrap(); window.open_devtools(); } Ok(()) }) .run(tauri::generate_context!()) .expect("error while running tauri application"); }