aboutsummaryrefslogtreecommitdiffstats
path: root/crates/asist-app/src/registry.rs
blob: 13bbb75e6879e752da8e5b13ee6fa700adfcd5db (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
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))))
}