//! Montaje del juego de herramientas. //! //! `ToolRegistry::from_config` sólo conoce las que no dependen de nada //! —el reloj, la shell—, porque vive en el núcleo. Las que necesitan la red o //! el modelo multimodal se añaden aquí, que es exactamente el punto de //! extensión documentado en docs/EXTENDER.md. use std::sync::Arc; use std::time::Duration; use asist_core::config::{CameraConfig, Config, SearchConfig}; use asist_core::tools::ToolRegistry; use asist_llm::LlmClient; use asist_tools::camera::CaptureConfig; use asist_tools::{Camera, SearchBackend, WebSearch}; /// Lo que no se pudo activar y por qué, para decírselo al usuario al arrancar /// en vez de dejarlo en silencio. pub struct Skipped { pub tool: &'static str, pub reason: String, } pub fn build(config: &Config, llm: &Arc) -> (ToolRegistry, Vec) { let mut registry = ToolRegistry::from_config(&config.tools); let mut skipped = Vec::new(); if !config.tools.enabled { return (registry, skipped); } match search_tool(&config.search) { Ok(Some(tool)) => registry.register(Arc::new(tool)), Ok(None) => {} Err(reason) => skipped.push(Skipped { tool: "buscar_en_internet", reason, }), } match camera_tool(&config.camera, llm) { Ok(Some(tool)) => registry.register(Arc::new(tool)), Ok(None) => {} Err(reason) => skipped.push(Skipped { tool: "mirar_por_la_camara", reason, }), } (registry, skipped) } fn search_tool(config: &SearchConfig) -> Result, String> { if !config.enabled { return Ok(None); } let backend = match config.backend.trim().to_lowercase().as_str() { "tavily" => { // La clave sale del entorno, nunca del fichero de configuración. let key = std::env::var(&config.api_key_env) .ok() .map(|k| k.trim().to_string()) .filter(|k| !k.is_empty()) .ok_or_else(|| { format!( "la variable {} está vacía o sin definir; expórtala o cambia \ search.backend a «searxng»", config.api_key_env ) })?; SearchBackend::Tavily { api_key: key } } "searxng" => SearchBackend::SearxNG { base_url: config.base_url.clone(), }, "ddgs" | "comando" => { let (program, args) = config .command .split_first() .ok_or_else(|| "search.command está vacío".to_string())?; if !std::path::Path::new(program).exists() && !program.contains('/') { // Un nombre suelto se busca en el PATH; una ruta tiene que existir. } else if !std::path::Path::new(program).exists() { return Err(format!("no existe el buscador {program}")); } SearchBackend::Command { program: program.into(), args: args.to_vec(), } } other => return Err(format!("search.backend desconocido: «{other}»")), }; Ok(Some(WebSearch::new( backend, config.max_results, Duration::from_secs(config.timeout_secs), ))) } fn camera_tool(config: &CameraConfig, llm: &Arc) -> Result, String> { if !config.enabled { return Ok(None); } let capture = CaptureConfig { device: config.device.clone(), width: config.width, height: config.height, warmup_frames: config.warmup_frames, timeout: Duration::from_secs(config.timeout_secs), save_dir: (!config.save_dir.is_empty()).then(|| config.save_dir.clone().into()), }; // Registrar una herramienta que va a fallar siempre es peor que no tenerla: // el modelo la llamaría, se comería el error y gastaría un turno entero. if !Camera::available(&capture) { return Err(format!( "no existe {}; comprueba con «v4l2-ctl --list-devices»", capture.device.display() )); } Ok(Some(Camera::new(capture, Arc::clone(llm)))) }