diff options
Diffstat (limited to 'docs/EXTENDER.md')
| -rw-r--r-- | docs/EXTENDER.md | 127 |
1 files changed, 123 insertions, 4 deletions
diff --git a/docs/EXTENDER.md b/docs/EXTENDER.md index e45eb16..ea6d937 100644 --- a/docs/EXTENDER.md +++ b/docs/EXTENDER.md @@ -40,22 +40,35 @@ impl Tool for Temperatura { } ``` -Y regístrala en `ToolRegistry::from_config` (en `crates/asist-core/src/tools.rs`) -o directamente antes de montar el pipeline: +Y regístrala. Si no depende de nada, en `ToolRegistry::from_config` +(`crates/asist-core/src/tools.rs`); si necesita la red, el modelo o un +dispositivo, en `crates/asist-app/src/registry.rs`, que es donde se montan +`buscar_en_internet` y `mirar_por_la_camara`: ```rust let mut tools = ToolRegistry::from_config(&config.tools); tools.register(std::sync::Arc::new(Temperatura)); ``` +Si tarda más de un segundo, dale una frase de acuse. Se pronuncia antes de +ejecutar y es lo que evita que la espera se lea como un cuelgue: + +```rust +fn acknowledgement(&self) -> Option<&str> { + Some("Déjame que lo mire.") +} +``` + 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). +- **Con este modelo, más herramientas es peor.** Con tres declaradas elige + bien entre ellas (15 de 16 medido), pero le cuesta abstenerse: busca en + internet cosas que ya sabe. Apartados 3 y 5 de + [RENDIMIENTO.md](RENDIMIENTO.md). ## Ejecutar órdenes del sistema @@ -85,6 +98,106 @@ 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. +## Buscar en internet + +Ya está montado, con tres buscadores intercambiables. + +```toml +[search] +enabled = true +backend = "tavily" # "tavily", "ddgs" o "searxng" +api_key_env = "TAVILY_API_KEY" +command = ["../scripts/buscar-ddgs.sh", "{consulta}", "{max}"] +base_url = "" # para searxng: "http://127.0.0.1:8888" +max_results = 5 +``` + +Cuál elegir, con las mismas dos preguntas medidas contra cada uno: + +| | «capital de Australia» | «qué tiempo hace hoy en Buenos Aires» | Clave | +|---|---|---|---| +| **tavily** | 0,54 s · *«El capital de Australia es Canberra…»* | 2,88 s · *«Hoy en Buenos Aires, cielo claro, máxima de 15°C…»* | sí | +| **ddgs** | 2,24 s · fragmentos de tres páginas | 3,59 s · fragmentos de tres páginas | no | + +La diferencia que importa no es el tiempo sino la forma: **Tavily devuelve una +respuesta ya redactada** y el asistente sólo tiene que leerla, mientras que +ddgs devuelve fragmentos que el modelo tiene que resumir. Con 2B eso sale bien +la mayoría de las veces —medido en el pipeline completo, «Hoy en Buenos Aires +hace soleado esta mañana y por la tarde tendremos nubes con temperaturas +alrededor de 9°C»— pero es una oportunidad más de equivocarse. + +Con clave, tavily. Sin clave, ddgs. + +### Sobre DuckDuckGo + +`scripts/buscar-ddgs.sh` se apoya en la librería +[`ddgs`](https://github.com/deedy5/ddgs), que es la misma que hay debajo de +`duckduckgo-mcp`. Consulta DuckDuckGo, Brave, Mojeek, Startpage y alguno más, +sin clave. Conviene instalarla de forma permanente para que cada búsqueda no +pague la resolución del paquete: + +```bash +uv tool install ddgs # el guion también funciona con uvx, pero más lento +``` + +Dos caminos que parecen equivalentes y **no funcionan**, comprobados aquí: + +- `curl` contra `html.duckduckgo.com/html/` devuelve **HTTP 202** con una + página anti-bot (`anomaly.js`) y ni un solo resultado. +- La API sin clave `api.duckduckgo.com` (Instant Answer) devuelve vacío para + casi todo lo que no sea una entidad de enciclopedia: probada con «capital de + Australia» y con «qué tiempo hace en Buenos Aires», ambas sin nada. + +La librería sí funciona porque rota buscadores y cabeceras. + +### Cualquier otro buscador + +El backend `ddgs` es en realidad un **backend de comando**: ejecuta un programa +y lee JSON de su salida estándar. `{consulta}` y `{max}` se sustituyen antes de +ejecutar, y el extractor acepta tanto la lista suelta que devuelve ddgs como la +forma `{answer, results}`, con los campos nombrados a la manera de cada uno +(`href` o `url`, `body` o `content`). + +Así que meter otro buscador —o un puente a un servidor MCP, que es un proceso +que habla JSON-RPC por stdio— no es tocar Rust, sino escribir un guion: + +```toml +command = ["/ruta/a/mi-buscador.sh", "{consulta}", "{max}"] +``` + +Los argumentos van al `execve` tal cual, sin shell que los interprete: la +consulta sale de lo que se ha oído por el micrófono y no puede acabar +ejecutándose. + +## Mirar por la cámara + +También montado. El servidor carga el proyector multimodal (`--mmproj`), así +que el mismo modelo que conversa describe la imagen. + +```toml +[camera] +enabled = true +device = "/dev/video0" +width = 640 # la resolución manda en la latencia: 2,9 s aquí, 7,8 s a 720p +height = 480 +warmup_frames = 5 # deja que se asiente la exposición automática +save_dir = "" # vacío = no se guarda ningún fotograma +``` + +Tres decisiones que conviene no deshacer sin pensarlo: + +- **La pregunta viaja hasta la cámara.** El modelo rellena `pregunta` con lo + que quiere averiguar, y esa pregunta acompaña a la imagen. Pedir una + descripción genérica y luego interrogarla pierde el detalle que se buscaba. +- **La imagen no entra en el historial.** Una foto ocupa cientos de tokens de + contexto y arrastrarla turno tras turno saldría carísimo para lo poco que + aporta una vez descrita. Lo que vuelve a la conversación es el texto. +- **No se guarda nada en disco.** `save_dir` existe sólo para depurar qué está + viendo el modelo; por defecto está vacío. + +Para ver la pantalla en vez de la cámara, `grim` ya está instalado: sería la +misma herramienta cambiando cómo se obtienen los bytes del JPEG. + ## Cambiar de motor Cada etapa habla con su motor a través de un único tipo, así que sustituir uno @@ -95,12 +208,18 @@ no toca el resto: | 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` | +| Visión | `asist_llm::LlmClient::look` | `crates/asist-llm/src/client.rs` | +| Buscar y mirar | `asist_tools` | `crates/asist-tools/src/` | 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. +Hay un segundo cliente, `ureq`, sólo en `asist-tools`: un buscador está detrás +de HTTPS, y ahí hace falta TLS y no hace falta cortar a media descarga. Son dos +clientes porque resuelven dos problemas distintos, no por descuido. + ## Añadir una etapa al pipeline El orquestador (`crates/asist-app/src/pipeline.rs`) es una cadena de hilos |