diff options
Diffstat (limited to 'docs/EXTENDER.md')
| -rw-r--r-- | docs/EXTENDER.md | 138 |
1 files changed, 138 insertions, 0 deletions
diff --git a/docs/EXTENDER.md b/docs/EXTENDER.md new file mode 100644 index 0000000..e45eb16 --- /dev/null +++ b/docs/EXTENDER.md @@ -0,0 +1,138 @@ +# Extender el asistente + +## Añadir una herramienta + +Las herramientas son el punto de extensión pensado para lo que venga después: +ejecutar órdenes, consultar cosas, encender luces. Una herramienta se describe +a sí misma en JSON Schema y sabe ejecutarse; el registro se encarga del resto. + +Implementa `asist_core::tools::Tool`: + +```rust +use asist_core::error::{Error, Result}; +use asist_core::tools::Tool; +use serde_json::{json, Value}; + +pub struct Temperatura; + +impl Tool for Temperatura { + fn name(&self) -> &str { "temperatura_cpu" } + + // La lee el modelo, así que se escribe para él: concreta y en español. + fn description(&self) -> &str { + "Devuelve la temperatura actual de la CPU en grados. Úsala cuando \ + pregunten si el equipo está caliente." + } + + fn parameters(&self) -> Value { + json!({ "type": "object", "properties": {}, "required": [] }) + } + + fn call(&self, _args: &Value) -> Result<String> { + let raw = std::fs::read_to_string("/sys/class/thermal/thermal_zone0/temp") + .map_err(|e| Error::Tool { tool: self.name().into(), message: e.to_string() })?; + let grados = raw.trim().parse::<f32>().unwrap_or(0.0) / 1000.0; + Ok(format!("{grados:.1} grados")) + } + + // Ponlo a true si cambia algo fuera del proceso. + fn is_side_effecting(&self) -> bool { false } +} +``` + +Y regístrala en `ToolRegistry::from_config` (en `crates/asist-core/src/tools.rs`) +o directamente antes de montar el pipeline: + +```rust +let mut tools = ToolRegistry::from_config(&config.tools); +tools.register(std::sync::Arc::new(Temperatura)); +``` + +Tres cosas que conviene saber antes de escribir la primera: + +- **El error nunca tumba el turno.** `dispatch` convierte un fallo en texto y + se lo devuelve al modelo, que lo explica o reintenta. +- **La salida se lee en voz alta.** Devuelve una frase, no una tabla. El + registro recorta a 2000 caracteres, pero mucho antes de eso ya aburre. +- **Con este modelo, más herramientas es peor.** Qwen3.5-2B ya es frágil + eligiendo entre dos; ver el apartado 3 de [RENDIMIENTO.md](RENDIMIENTO.md). + +## Ejecutar órdenes del sistema + +Ya está implementado y apagado (`tools.shell = false`). Encenderlo es un cambio +de postura de seguridad, no una comodidad: lo que llega a esa herramienta +viene, en última instancia, de lo que se oye por el micrófono. + +Las cuatro barreras que tiene, y que conviene no quitar: + +1. **Apagada por defecto**, y la configuración se niega a arrancar con la lista + blanca vacía. +2. **Lista blanca sobre el ejecutable.** Se compara `argv[0]` exacto, y las + rutas se rechazan: `/bin/echo` no cuela aunque `echo` esté permitido. +3. **Sin shell de por medio.** Los argumentos van al `execve` tal cual, así que + `; rm -rf /` es un argumento literal y no otra orden. +4. **Plazo máximo**, tras el cual el proceso se mata. + +```toml +[tools] +shell = true +shell_allowlist = ["date", "uptime", "free", "df", "ls"] +shell_timeout_secs = 10 +shell_dry_run = true # estrena la lista sin ejecutar nada +shell_working_dir = "/home/elvis" +``` + +Empieza con `shell_dry_run = true`: registra lo que el modelo habría ejecutado +sin llegar a hacerlo, que es la forma barata de descubrir qué pide de verdad. + +## Cambiar de motor + +Cada etapa habla con su motor a través de un único tipo, así que sustituir uno +no toca el resto: + +| Etapa | Tipo | Fichero | +|---|---|---| +| Voz a texto | `asist_asr::Recognizer` | `crates/asist-asr/src/lib.rs` | +| Modelo | `asist_llm::LlmClient` | `crates/asist-llm/src/client.rs` | +| Texto a voz | `asist_tts::TtsClient` | `crates/asist-tts/src/lib.rs` | + +El cliente HTTP (`asist_core::http`) es propio y mínimo a propósito: todo es +HTTP plano contra localhost, y lo que ninguna librería genérica da cómodo es +**abortar una respuesta a media descarga**, que es justo lo que sostiene la +interrupción. + +## Añadir una etapa al pipeline + +El orquestador (`crates/asist-app/src/pipeline.rs`) es una cadena de hilos +unidos por canales. Para meter una etapa nueva —clasificar la intención, +filtrar palabrotas, registrar la conversación— añade un hilo con +`spawn_named` y un canal, y respeta las dos reglas que sostienen el conjunto: + +1. **Nada bloquea al que va delante.** Si vas más lento que tu emisor, + descarta trabajo; no acumules retraso. +2. **Comprueba el turno antes de gastar esfuerzo.** `session.is_current(turn)` + dice si lo que estás a punto de hacer sigue interesando a alguien, o si el + usuario ya ha dicho otra cosa. + +## Cambiar la voz + +```bash +# Graba unos segundos de la voz que quieras y transcríbelos a mano. +scripts/clonar-voz.sh grabacion.wav transcripcion.txt mivoz +``` + +Deja `assets/voices/mivoz.{spk,rvq,txt}`. Apunta ahí `[tts.reference]` en la +configuración. Los latentes se extraen una vez; en cada arranque se manda lo ya +extraído y no la grabación. + +También sirven las voces del propio modelo: quita `[tts.reference]` y pon +`tts.voice` con un nombre de `GET /v1/audio/voices`. + +## Ideas que el diseño ya admite + +- **Palabra de activación.** El segmentador ya entrega intervenciones sueltas: + bastaría con filtrar por la transcripción antes de abrir turno. +- **Memoria entre sesiones.** `Conversation` es serializable en la práctica; + guardarla y recargarla al arrancar es un cambio pequeño. +- **Multimodal.** El servidor ya carga `mmproj`, así que el modelo puede ver + imágenes; faltaría meterlas en el mensaje de usuario. |