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
|
//! Assembling the tool set.
//!
//! `ToolRegistry::from_config` only knows the ones that depend on nothing
//! (the clock, the shell), because it lives in the core. The ones that need
//! the network or the multimodal model are added here, which is exactly the
//! extension point documented in 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,
};
/// What could not be enabled and why, to tell the user at startup instead
/// of staying silent.
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,
}),
}
// The two that see share a shape, so they are registered the same way.
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" => {
// The key comes from the environment, never from the configuration file.
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!(
"the variable {} is empty or unset; export it or change \
search.backend to «searxng»",
config.api_key_env
)
})?;
SearchBackend::Tavily { api_key: key }
}
"searxng" => SearchBackend::SearxNG {
base_url: config.base_url.clone(),
},
"ddgs" | "command" | "comando" => {
let (program, args) = config
.command
.split_first()
.ok_or_else(|| "search.command is empty".to_string())?;
if !std::path::Path::new(program).exists() && !program.contains('/') {
// A bare name is looked up in PATH; a path must exist.
} else if !std::path::Path::new(program).exists() {
return Err(format!("the search program {program} does not exist"));
}
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)))
}
/// Registering a tool that will always fail is worse than not having it:
/// the model would call it, swallow the error and waste a whole turn.
fn finish(tool: VisionTool) -> Result<Option<VisionTool>, String> {
match tool.available() {
Ok(()) => Ok(Some(tool)),
Err(err) => Err(err.to_string()),
}
}
|