aboutsummaryrefslogtreecommitdiffstats
path: root/crates/asist-app/src/main.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/asist-app/src/main.rs')
-rw-r--r--crates/asist-app/src/main.rs95
1 files changed, 92 insertions, 3 deletions
diff --git a/crates/asist-app/src/main.rs b/crates/asist-app/src/main.rs
index 2788d97..7b00b1f 100644
--- a/crates/asist-app/src/main.rs
+++ b/crates/asist-app/src/main.rs
@@ -5,11 +5,13 @@
//! espera a la siguiente.
mod pipeline;
+mod registry;
mod render;
mod session;
mod supervisor;
use std::path::PathBuf;
+use std::sync::Arc;
use std::time::Duration;
use anyhow::{bail, Context, Result};
@@ -19,7 +21,6 @@ use asist_asr::Recognizer;
use asist_audio::{Capture, CaptureBlock, Playback};
use asist_core::config::Config;
use asist_core::event::Event;
-use asist_core::tools::ToolRegistry;
use asist_llm::LlmClient;
use asist_tts::TtsClient;
@@ -52,7 +53,9 @@ fn run(config: Config, args: &Args) -> Result<()> {
// Los clientes se crean antes de arrancar nada: así se detecta lo que ya
// esté escuchando y no se levanta un servidor por duplicado.
- let llm = LlmClient::new(config.llm_authority(), &config.llm);
+ // Compartido: el orquestador conversa con él y la herramienta de cámara
+ // lo usa para describir lo que capta.
+ let llm = Arc::new(LlmClient::new(config.llm_authority(), &config.llm));
let tts = TtsClient::new(config.tts_authority(), &config.tts);
let (llm_up, tts_up) = (llm.healthy(), tts.healthy());
@@ -92,12 +95,33 @@ fn run(config: Config, args: &Args) -> Result<()> {
let (capture_tx, capture_rx) = unbounded::<CaptureBlock>();
let capture = Capture::open(&config.audio, capture_tx.clone())?;
- let tools = ToolRegistry::from_config(&config.tools);
+ let (tools, skipped) = registry::build(&config, &llm);
if tools.is_empty() {
eprintln!("Herramientas: ninguna");
} else {
eprintln!("Herramientas: {}", tools.names().join(", "));
}
+ // Lo que no se pudo activar se dice en voz alta, en vez de dejar al
+ // usuario preguntándose por qué el asistente no busca ni ve.
+ for skip in &skipped {
+ eprintln!(" · {} no disponible: {}", skip.tool, skip.reason);
+ }
+ // Las dos capacidades que tocan algo fuera del proceso se anuncian: una
+ // enciende la cámara y la otra ejecuta órdenes. Quien lo arranca debería
+ // saberlo sin tener que leerse la configuración.
+ if tools.get("mirar_por_la_camara").is_some() {
+ eprintln!(
+ "Cámara ACTIVA — {} a {}x{}{}",
+ config.camera.device.display(),
+ config.camera.width,
+ config.camera.height,
+ if config.camera.save_dir.is_empty() {
+ String::new()
+ } else {
+ format!(", guardando fotogramas en {}", config.camera.save_dir)
+ }
+ );
+ }
if config.tools.shell {
eprintln!(
"Ejecución de órdenes ACTIVA — permitidas: {}{}",
@@ -263,6 +287,61 @@ fn check(config: &Config) -> Result<()> {
}
}
+ println!("Capacidades:");
+ // Ninguna de las dos es fatal: el asistente conversa igual sin ellas, así
+ // que se informa y no se cuentan como problema.
+ if !config.search.enabled {
+ println!(" - búsqueda desactivada en la configuración");
+ } else {
+ match config.search.backend.trim().to_lowercase().as_str() {
+ "tavily" => match std::env::var(&config.search.api_key_env) {
+ Ok(key) if !key.trim().is_empty() => ok(
+ "búsqueda (tavily)",
+ format!("clave en ${}", config.search.api_key_env),
+ ),
+ _ => println!(
+ " - búsqueda (tavily) falta ${}",
+ config.search.api_key_env
+ ),
+ },
+ "ddgs" | "comando" => match config.search.command.first() {
+ // Un nombre suelto se resuelve por el PATH; una ruta tiene que
+ // existir, y más vale decirlo aquí que a mitad de una pregunta.
+ Some(program) if !program.contains('/') || PathBuf::from(program).exists() => {
+ ok("búsqueda (ddgs)", config.search.command.join(" "))
+ }
+ Some(program) => {
+ println!(" - búsqueda (ddgs) no existe {program}")
+ }
+ None => println!(" - búsqueda (ddgs) search.command está vacío"),
+ },
+ "searxng" => ok("búsqueda (searxng)", config.search.base_url.clone()),
+ other => println!(" - búsqueda backend desconocido: «{other}»"),
+ }
+ }
+
+ if !config.camera.enabled {
+ println!(" - cámara desactivada en la configuración");
+ } else if config.camera.device.exists() {
+ ok(
+ "cámara",
+ format!(
+ "{} a {}x{}",
+ config.camera.device.display(),
+ config.camera.width,
+ config.camera.height
+ ),
+ );
+ if which("ffmpeg").is_none() {
+ println!(" - ffmpeg no está; hace falta para capturar");
+ }
+ } else {
+ println!(
+ " - cámara no existe {}",
+ config.camera.device.display()
+ );
+ }
+
println!("Servidores:");
let llm = LlmClient::new(config.llm_authority(), &config.llm);
let tts = TtsClient::new(config.tts_authority(), &config.tts);
@@ -412,6 +491,16 @@ impl Args {
}
}
+/// Busca un ejecutable en el PATH, para avisar de lo que falta antes de que
+/// falle a mitad de una conversación.
+fn which(program: &str) -> Option<PathBuf> {
+ std::env::var_os("PATH").and_then(|path| {
+ std::env::split_paths(&path)
+ .map(|dir| dir.join(program))
+ .find(|candidate| candidate.is_file())
+ })
+}
+
/// ¿Hay una persona al otro lado de la entrada estándar?
fn stdin_is_tty() -> bool {
#[cfg(unix)]