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
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
|
//! 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::{self as cfg, Config, SearchConfig};
use asist_core::tools::ToolRegistry;
use asist_llm::LlmClient;
use asist_tools::{
Camera, CameraConfig, Screen, ScreenConfig, SearchBackend, VisionTool, 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,
}),
}
// Las dos que ven comparten forma, así que se registran igual.
for (result, tool) in [
(camera_tool(&config.camera, llm), "mirar_por_la_camara"),
(screen_tool(&config.screen, llm), "mirar_la_pantalla"),
] {
match result {
Ok(Some(vision)) => registry.register(Arc::new(vision)),
Ok(None) => {}
Err(reason) => skipped.push(Skipped { tool, 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: &cfg::CameraConfig,
llm: &Arc<LlmClient>,
) -> Result<Option<VisionTool>, String> {
if !config.enabled {
return Ok(None);
}
let source = Camera::new(CameraConfig {
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()),
});
finish(VisionTool::camera(Box::new(source), Arc::clone(llm)))
}
fn screen_tool(
config: &cfg::ScreenConfig,
llm: &Arc<LlmClient>,
) -> Result<Option<VisionTool>, String> {
if !config.enabled {
return Ok(None);
}
let source = Screen::new(ScreenConfig {
command: config.command.clone(),
width: config.width,
output: config.output.clone(),
timeout: Duration::from_secs(config.timeout_secs),
save_dir: (!config.save_dir.is_empty()).then(|| config.save_dir.clone().into()),
});
finish(VisionTool::screen(Box::new(source), Arc::clone(llm)))
}
/// 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.
fn finish(tool: VisionTool) -> Result<Option<VisionTool>, String> {
match tool.available() {
Ok(()) => Ok(Some(tool)),
Err(err) => Err(err.to_string()),
}
}
|