- Check process_running and ready state before making inference request
- Emit descriptive error messages for each failure scenario
- Handle local vs remote connection modes appropriately
- Validation score: 100 (was 0)
Result: {"status":"keep","validation_score":100,"response_time_ms":500}
799 lines
27 KiB
Rust
799 lines
27 KiB
Rust
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<ForgeAgentSubStep>,
|
|
}
|
|
|
|
#[derive(Clone, Serialize)]
|
|
struct ForgeAgentPlan {
|
|
title: String,
|
|
overview: String,
|
|
steps: Vec<ForgeAgentStep>,
|
|
}
|
|
|
|
#[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::<String>() + 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<RuntimeState>,
|
|
) -> Result<RuntimeSnapshot, String> {
|
|
bootstrap_runtime(app, state)
|
|
}
|
|
|
|
#[tauri::command]
|
|
fn get_runtime_snapshot_command(state: State<RuntimeState>) -> Result<RuntimeSnapshot, String> {
|
|
get_runtime_snapshot(state)
|
|
}
|
|
|
|
#[tauri::command]
|
|
fn save_runtime_config_command_handler(
|
|
app: AppHandle,
|
|
state: State<RuntimeState>,
|
|
config: RuntimeConfig,
|
|
) -> Result<RuntimeSnapshot, String> {
|
|
save_runtime_config_command(app, state, config)
|
|
}
|
|
|
|
#[tauri::command]
|
|
fn start_runtime_command(
|
|
app: AppHandle,
|
|
state: State<RuntimeState>,
|
|
) -> Result<RuntimeSnapshot, String> {
|
|
start_runtime(app, state)
|
|
}
|
|
|
|
#[tauri::command]
|
|
fn stop_runtime_command(state: State<RuntimeState>) -> Result<RuntimeSnapshot, String> {
|
|
stop_runtime(state)
|
|
}
|
|
|
|
/// Returns the resolved path to the localllm binary (useful for debugging).
|
|
#[tauri::command]
|
|
fn find_server_binary() -> Result<String, String> {
|
|
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<String, String> {
|
|
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<String>,
|
|
) -> Result<Vec<model_library::types::HuggingFaceModelResult>, String> {
|
|
search_hugging_face_models(&query, token.as_deref()).await
|
|
}
|
|
|
|
#[tauri::command]
|
|
async fn start_hugging_face_download_command(
|
|
app: AppHandle,
|
|
state: State<'_, Arc<HuggingFaceDownloadState>>,
|
|
repo: String,
|
|
file_name: String,
|
|
token: Option<String>,
|
|
) -> Result<String, String> {
|
|
// 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<HuggingFaceDownloadState>>,
|
|
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<RuntimeSnapshot, String> {
|
|
// 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<Option<model_alias::StartupModelResolution>, 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<Vec<ModelLibraryEntry>, 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<ModelLibraryState, 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);
|
|
|
|
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<ChatCompletionChoice>,
|
|
}
|
|
|
|
#[tauri::command]
|
|
async fn start_forgeagent_session_command(
|
|
app: AppHandle,
|
|
state: State<'_, RuntimeState>,
|
|
prompt: String,
|
|
) -> Result<ForgeAgentStartPayload, String> {
|
|
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::<ChatCompletionResponse>() 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::<serde_json::Value>(&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");
|
|
}
|