aboutsummaryrefslogtreecommitdiffstats
path: root/crates/asist-app/src/registry.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/asist-app/src/registry.rs')
-rw-r--r--crates/asist-app/src/registry.rs122
1 files changed, 122 insertions, 0 deletions
diff --git a/crates/asist-app/src/registry.rs b/crates/asist-app/src/registry.rs
new file mode 100644
index 0000000..13bbb75
--- /dev/null
+++ b/crates/asist-app/src/registry.rs
@@ -0,0 +1,122 @@
+//! 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<LlmClient>) -> (ToolRegistry, Vec<Skipped>) {
+ 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<Option<WebSearch>, 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<LlmClient>) -> Result<Option<Camera>, 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))))
+}