diff options
| author | elvis <elvis@claros.ar> | 2026-09-26 20:20:19 -0300 |
|---|---|---|
| committer | elvis <elvis@claros.ar> | 2026-09-26 20:20:19 -0300 |
| commit | 8518a63f55153e7f45fd49ad6caff5555f4e374f (patch) | |
| tree | 636684eea3fa6f35d78282ab95eb687ef49154af /crates/asist-core/src/tools.rs | |
| parent | 69de76dc9cbedc6092d1e5ce84094a8030de1470 (diff) | |
| download | asist-p-8518a63f55153e7f45fd49ad6caff5555f4e374f.tar.gz asist-p-8518a63f55153e7f45fd49ad6caff5555f4e374f.zip | |
Translate code, comments, logs and terminal UI to English; add English README; rename scriptsHEADmain
Diffstat (limited to 'crates/asist-core/src/tools.rs')
| -rw-r--r-- | crates/asist-core/src/tools.rs | 158 |
1 files changed, 79 insertions, 79 deletions
diff --git a/crates/asist-core/src/tools.rs b/crates/asist-core/src/tools.rs index 3911985..1bf1510 100644 --- a/crates/asist-core/src/tools.rs +++ b/crates/asist-core/src/tools.rs @@ -1,10 +1,10 @@ -//! Herramientas que el modelo puede invocar. +//! Tools the model can call. //! -//! El punto de extensión del asistente. Una herramienta es un objeto que se -//! describe a sí mismo en JSON Schema y sabe ejecutarse; el registro las -//! traduce al formato `tools` de la API de chat y despacha las llamadas que -//! vuelven. Añadir una capacidad nueva es implementar el trait y registrarla, -//! sin tocar el orquestador. +//! The assistant's extension point. A tool is an object that describes itself +//! in JSON Schema and knows how to run; the registry translates them to the +//! chat API `tools` format and dispatches the calls that come back. Adding a +//! new capability means implementing the trait and registering it, without +//! touching the orchestrator. use std::collections::BTreeMap; use std::sync::Arc; @@ -15,49 +15,49 @@ use serde_json::{json, Value}; use crate::config::ToolsConfig; use crate::error::{Error, Result}; -/// Lo que el modelo puede pedir que se haga. +/// Something the model can ask to be done. pub trait Tool: Send + Sync { - /// Identificador que usa el modelo. En minúsculas y sin espacios. + /// Identifier the model uses. Lowercase, no spaces. fn name(&self) -> &str; - /// Para qué sirve. Lo lee el modelo, así que se escribe para él: concreto - /// y en la lengua de la conversación. + /// What it is for. The model reads it, so it is written for the model: + /// concrete and in the conversation language. fn description(&self) -> &str; - /// JSON Schema de los argumentos. + /// JSON Schema of the arguments. fn parameters(&self) -> Value; - /// Ejecuta y devuelve el texto que se le entrega al modelo como resultado. + /// Runs and returns the text handed to the model as the result. fn call(&self, args: &Value) -> Result<String>; - /// `true` si la herramienta cambia algo fuera del proceso. El orquestador - /// lo anuncia en voz alta antes de ejecutarla. + /// `true` if the tool changes something outside the process. The + /// orchestrator announces it aloud before running it. fn is_side_effecting(&self) -> bool { false } - /// Frase que se pronuncia nada más empezar a ejecutarla. + /// Sentence spoken right when it starts running. /// - /// Existe por una razón de trato, no de adorno: una búsqueda son unos - /// 2,5 s, la cámara casi 3, y a eso hay que sumarle las dos pasadas del - /// modelo. Seis segundos de silencio absoluto se leen como que el - /// asistente se ha colgado. Devuelve `None` la herramienta que responda - /// al instante, donde el acuse molestaría más que ayudar. + /// It exists for the sake of the conversation, not decoration: a search takes + /// about 2.5 s, the camera almost 3, and the two model passes come on top. + /// Six seconds of total silence read as the assistant having hung. A tool + /// that answers instantly returns `None`, where the acknowledgement would + /// annoy more than help. fn acknowledgement(&self) -> Option<&str> { None } } -/// Una llamada tal y como la pide el modelo. +/// A call as the model requests it. #[derive(Debug, Clone)] pub struct ToolCall { pub id: String, pub name: String, - /// Argumentos en JSON, aún sin validar. + /// JSON arguments, not validated yet. pub arguments: String, } -/// Resultado de ejecutarla. +/// Result of running it. #[derive(Debug, Clone)] pub struct ToolOutcome { pub id: String, @@ -77,7 +77,7 @@ impl ToolRegistry { Self::default() } - /// Monta el juego de herramientas que pide la configuración. + /// Builds the tool set the configuration asks for. pub fn from_config(config: &ToolsConfig) -> Self { let mut registry = Self::new(); if !config.enabled { @@ -106,8 +106,8 @@ impl ToolRegistry { self.tools.get(name) } - /// Descripción en el formato `tools` de la API de chat de OpenAI, que es - /// el que habla llama-server. + /// Description in the OpenAI chat API `tools` format, which is what + /// llama-server speaks. pub fn schema(&self) -> Value { Value::Array( self.tools @@ -126,9 +126,9 @@ impl ToolRegistry { ) } - /// Ejecuta una llamada. Nunca propaga el error hacia arriba: un fallo de - /// herramienta se le devuelve al modelo como texto para que lo explique o - /// lo reintente, en vez de tumbar el turno. + /// Runs a call. It never propagates the error upwards: a tool failure is + /// handed back to the model as text so it can explain it or retry, instead + /// of bringing the turn down. pub fn dispatch(&self, call: &ToolCall) -> ToolOutcome { let started = std::time::Instant::now(); let result = match self.tools.get(&call.name) { @@ -141,8 +141,8 @@ impl ToolRegistry { }), Some(tool) => serde_json::from_str::<Value>(&call.arguments) .or_else(|_| { - // Los modelos pequeños mandan a veces la cadena vacía en - // lugar de «{}» cuando la función no lleva argumentos. + // Small models sometimes send an empty string instead of «{}» + // when the function takes no arguments. if call.arguments.trim().is_empty() { Ok(json!({})) } else { @@ -180,8 +180,8 @@ pub mod builtin { use std::path::Path; use std::process::Command; - /// Fecha y hora locales. Existe porque el modelo no las sabe y las inventa - /// con aplomo, y de paso sirve de ejemplo mínimo de herramienta. + /// Local date and time. It exists because the model does not know them and + /// confidently makes them up, and it doubles as a minimal tool example. pub struct Clock; impl Tool for Clock { @@ -199,11 +199,11 @@ pub mod builtin { } fn call(&self, _args: &Value) -> Result<String> { - // `date` evita arrastrar una dependencia de calendario entera para - // una sola llamada, y respeta la zona horaria del sistema. Se le - // piden campos numéricos y los nombres se ponen aquí: `%A` y `%B` - // salen en el idioma de la locale, que en esta máquina es inglés, - // y el asistente acabaría diciendo «Sunday 6 de September». + // `date` avoids pulling in a whole calendar dependency for a single + // call, and it honours the system time zone. It is asked for numeric + // fields and the names are filled in here: `%A` and `%B` come out in + // the locale language, which on this machine is English, and the + // assistant would end up saying «Sunday 6 de September». let out = Command::new("date") .arg("+%w %-d %-m %Y %H:%M") .output() @@ -219,7 +219,7 @@ pub mod builtin { } } - pub(super) const DIAS: [&str; 7] = [ + pub(super) const WEEKDAYS: [&str; 7] = [ "domingo", "lunes", "martes", @@ -228,7 +228,7 @@ pub mod builtin { "viernes", "sábado", ]; - const MESES: [&str; 12] = [ + const MONTHS: [&str; 12] = [ "enero", "febrero", "marzo", @@ -243,7 +243,7 @@ pub mod builtin { "diciembre", ]; - /// Compone «domingo 6 de septiembre de 2026, 19:13» a partir de + /// Builds «domingo 6 de septiembre de 2026, 19:13» from /// «0 6 9 2026 19:13». pub(super) fn format_spanish_date(raw: &str) -> Option<String> { let mut fields = raw.split_whitespace(); @@ -254,18 +254,18 @@ pub mod builtin { let time = fields.next()?; Some(format!( "{} {day} de {} de {year}, {time}", - DIAS.get(weekday)?, - MESES.get(month.checked_sub(1)?)? + WEEKDAYS.get(weekday)?, + MONTHS.get(month.checked_sub(1)?)? )) } - /// Ejecución de órdenes del sistema, restringida por lista blanca. + /// System command execution, restricted by an allowlist. /// - /// Es la puerta por la que el asistente pasa de hablar a actuar, y también - /// la más delicada del proyecto: lo que llega aquí viene, en última - /// instancia, de lo que se oye por el micrófono. De ahí las cuatro - /// barreras: apagada por defecto, lista blanca sobre el ejecutable, - /// argumentos sin shell que los interprete, y un plazo máximo. + /// It is the door through which the assistant goes from talking to acting, + /// and also the most delicate one in the project: whatever arrives here comes, + /// ultimately, from what the microphone hears. Hence four barriers: off by + /// default, an allowlist on the executable, arguments with no shell to + /// interpret them, and a deadline. pub struct Shell { allowlist: Vec<String>, timeout: Duration, @@ -284,8 +284,8 @@ pub mod builtin { } fn permitted(&self, program: &str) -> bool { - // Se compara sólo el nombre del ejecutable: dejar pasar rutas - // completas invitaría a colar «/tmp/ls». + // Only the executable name is compared: letting full paths through + // would invite sneaking in «/tmp/ls». !program.contains('/') && self.allowlist.iter().any(|a| a == program) } } @@ -343,7 +343,7 @@ pub mod builtin { other => Ok(other.to_string()), }) .collect::<Result<_>>()?, - // Algunos modelos mandan los argumentos en una sola cadena. + // Some models send the arguments as a single string. Some(Value::String(s)) => s.split_whitespace().map(String::from).collect(), Some(other) => return Err(fail(format!("«argumentos» inválido: {other}"))), }; @@ -360,8 +360,8 @@ pub mod builtin { return Ok(format!("[simulación] no se ejecutó: {}", rendered.trim())); } - // Sin shell de por medio: los argumentos van al `execve` tal cual, - // así que un «; rm -rf /» es un argumento literal, no otra orden. + // No shell in between: the arguments go to `execve` as they are, + // so a «; rm -rf /» is a literal argument, not another command. let working_dir = (!self.working_dir.is_empty()).then(|| Path::new(&self.working_dir)); let output = crate::proc::run(program, &arguments, self.timeout, working_dir) .map_err(|e| fail(e.to_string()))?; @@ -371,8 +371,8 @@ pub mod builtin { if body.is_empty() { body = output.stderr.trim().to_string(); } - // La salida se va a leer en voz alta: pasado cierto punto sólo - // sirve para aburrir a quien escucha. + // The output will be read aloud: past a certain point it only + // bores the listener. const MAX: usize = 2000; if body.chars().count() > MAX { body = body.chars().take(MAX).collect::<String>() + "… (salida recortada)"; @@ -398,7 +398,7 @@ mod tests { use super::*; #[test] - fn la_fecha_se_compone_en_espanol_sin_depender_de_la_locale() { + fn date_is_built_in_spanish_regardless_of_locale() { assert_eq!( format_spanish_date("0 6 9 2026 19:13").unwrap(), "domingo 6 de septiembre de 2026, 19:13" @@ -410,28 +410,28 @@ mod tests { } #[test] - fn una_salida_de_date_inesperada_no_provoca_un_panico() { + fn unexpected_date_output_does_not_panic() { assert!(format_spanish_date("").is_none()); assert!( format_spanish_date("9 6 9 2026 19:13").is_none(), - "día de semana fuera de rango" + "weekday out of range" ); assert!( format_spanish_date("0 6 13 2026 19:13").is_none(), - "mes fuera de rango" + "month out of range" ); assert!( format_spanish_date("0 6 0 2026 19:13").is_none(), - "mes cero" + "month zero" ); } #[test] - fn la_herramienta_de_hora_devuelve_algo_en_espanol() { + fn time_tool_returns_spanish_text() { let out = builtin::Clock.call(&json!({})).unwrap(); assert!( - super::builtin::DIAS.iter().any(|d| out.starts_with(d)), - "esperaba un día en español, salió «{out}»" + super::builtin::WEEKDAYS.iter().any(|d| out.starts_with(d)), + "expected a Spanish weekday, got «{out}»" ); } @@ -449,7 +449,7 @@ mod tests { } #[test] - fn la_lista_blanca_deja_pasar_lo_autorizado() { + fn allowlist_lets_allowed_commands_through() { let shell = Shell::new(&shell_config()); let out = shell .call(&json!({ "programa": "echo", "argumentos": ["hola"] })) @@ -458,39 +458,39 @@ mod tests { } #[test] - fn lo_no_autorizado_se_rechaza() { + fn unauthorized_commands_are_rejected() { let shell = Shell::new(&shell_config()); let err = shell.call(&json!({ "programa": "rm", "argumentos": ["-rf", "/"] })); assert!( err.is_err(), - "«rm» no estaba en la lista y debió rechazarse" + "«rm» was not in the list and should have been rejected" ); } #[test] - fn una_ruta_absoluta_no_esquiva_la_lista_blanca() { + fn absolute_path_does_not_bypass_the_allowlist() { let shell = Shell::new(&shell_config()); assert!( shell.call(&json!({ "programa": "/bin/echo" })).is_err(), - "«/bin/echo» debe rechazarse: si no, basta con dar una ruta para colar cualquier binario" + "«/bin/echo» must be rejected: otherwise any binary gets in by giving a path" ); } #[test] - fn no_hay_shell_que_interprete_los_metacaracteres() { - // Si esto llegara a una shell, se ejecutarían dos órdenes. + fn no_shell_interprets_the_metacharacters() { + // If this reached a shell, two commands would run. let shell = Shell::new(&shell_config()); let out = shell .call(&json!({ "programa": "echo", "argumentos": ["a; echo b"] })) .unwrap(); assert_eq!( out, "a; echo b", - "el punto y coma debe ser texto, no un separador" + "the semicolon must be text, not a separator" ); } #[test] - fn una_orden_colgada_se_mata_al_vencer_el_plazo() { + fn hung_command_is_killed_at_the_deadline() { let shell = Shell::new(&shell_config()); let err = shell .call(&json!({ "programa": "sleep", "argumentos": ["30"] })) @@ -499,12 +499,12 @@ mod tests { assert!(err.contains("tardó más de"), "{err}"); assert!( err.contains("sleep"), - "el error debe decir qué orden se colgó: {err}" + "the error must say which command hung: {err}" ); } #[test] - fn la_simulacion_no_ejecuta_nada() { + fn dry_run_executes_nothing() { let mut config = shell_config(); config.shell_dry_run = true; let shell = Shell::new(&config); @@ -515,14 +515,14 @@ mod tests { } #[test] - fn la_shell_no_se_registra_si_la_configuracion_no_la_pide() { + fn shell_is_not_registered_unless_configured() { let registry = ToolRegistry::from_config(&ToolsConfig::default()); assert!(registry.get("ejecutar_comando").is_none()); assert!(registry.get("hora_actual").is_some()); } #[test] - fn una_herramienta_inexistente_devuelve_error_al_modelo_sin_romper_el_turno() { + fn unknown_tool_returns_an_error_to_the_model_without_breaking_the_turn() { let registry = ToolRegistry::from_config(&ToolsConfig::default()); let outcome = registry.dispatch(&ToolCall { id: "1".into(), @@ -534,7 +534,7 @@ mod tests { } #[test] - fn los_argumentos_vacios_valen_como_objeto_vacio() { + fn empty_arguments_count_as_an_empty_object() { let registry = ToolRegistry::from_config(&ToolsConfig::default()); let outcome = registry.dispatch(&ToolCall { id: "1".into(), @@ -545,7 +545,7 @@ mod tests { } #[test] - fn el_esquema_sale_en_el_formato_de_la_api_de_chat() { + fn schema_uses_the_chat_api_format() { let registry = ToolRegistry::from_config(&ToolsConfig::default()); let schema = registry.schema(); let first = &schema.as_array().unwrap()[0]; |