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))))
}
|