aboutsummaryrefslogtreecommitdiffstats
path: root/docs/EXTENDER.md
diff options
context:
space:
mode:
authorelvis <elvis@claros.ar>2026-09-06 22:05:14 -0300
committerelvis <elvis@claros.ar>2026-09-06 22:05:14 -0300
commit71764752f28d31028b9c2ca486c0fc1bcea1cf95 (patch)
treeffd463969826157819f5a3319e1895c3f51deb22 /docs/EXTENDER.md
parentf8f98e83481e7376a235fb305a095552433f79ab (diff)
downloadasist-p-71764752f28d31028b9c2ca486c0fc1bcea1cf95.tar.gz
asist-p-71764752f28d31028b9c2ca486c0fc1bcea1cf95.zip
Web search and camera vision
Dos capacidades nuevas en un crate aparte, asist-tools, registradas desde asist-app: son el ejemplo de que el punto de extensión documentado funciona sin tocar el orquestador. Buscar admite tres buscadores intercambiables. Tavily devuelve una respuesta ya redactada, que es lo que se puede leer en voz alta sin gastar otra vuelta del modelo en resumir (0,54 s para «capital de Australia»); ddgs no necesita clave y devuelve fragmentos que el modelo sintetiza (2,24 s). El tercero es un backend de comando genérico: ejecuta un programa y lee JSON de su salida, así que añadir otro buscador —o un puente a un servidor MCP— es escribir un guion. Sobre DuckDuckGo, comprobado y no supuesto: curl contra html.duckduckgo.com devuelve HTTP 202 con una página anti-bot y ni un resultado, y la API sin clave api.duckduckgo.com devuelve vacío para casi todo lo que no sea una entidad de enciclopedia. La librería ddgs —la misma que hay bajo duckduckgo-mcp— sí funciona porque rota buscadores y cabeceras, y es la que usa scripts/buscar-ddgs.sh. Mirar aprovecha que el servidor ya carga el proyector multimodal: el mismo modelo que conversa describe lo que capta la cámara. La pregunta del usuario viaja hasta ahí, porque «¿de qué color es mi camiseta?» y «¿cuánta gente hay?» necesitan la misma imagen y descripciones distintas. La imagen no entra en el historial —cientos de tokens por turno para algo ya descrito— ni toca el disco. Tres cosas más que salieron de medir, en docs/RENDIMIENTO.md: - La resolución de captura manda en la latencia: 7,8 s a 1280x720 frente a 2,9 s a 640x480, con la misma descripción útil. 640x480 pasa a ser el valor por defecto. - Una herramienta ejecutándose en silencio deja el turno seis segundos mudo y parece un cuelgue. Tool::acknowledgement pronuncia una frase antes de ejecutar y baja el primer audio de ~8,4 s a 4,1 s. - Con la instrucción de voz a secas en la pasada de redacción, el modelo anunciaba lo que acababa de hacer en vez de contar lo que averiguó, ignorando el resultado que tenía delante. general.tool_result_prompt lo corrige; ahí sí se puede añadir estilo sin riesgo, porque la llamada ya ocurrió. Con tres herramientas declaradas el modelo elige bien entre ellas (15 de 16 medido), pero le cuesta abstenerse y busca cosas que ya sabe. Intentar corregirlo con instrucciones empeora los aciertos sin reducir los falsos positivos, coherente con la fragilidad ya documentada. Corregido además un fallo que dejaba mudo al asistente tras la primera respuesta: el anillo de reproducción no bajaba nunca su bandera de actividad, así que el segmentador mantenía el micrófono cerrado creyendo que seguía hablando. El segmentador se guía ahora por la cola, que no puede desfasarse, y hay pruebas del anillo con un mando sin dispositivo detrás. Claude-Session: https://claude.ai/code/session_01FNxz5cSdQSscJH9H7b8uGU
Diffstat (limited to 'docs/EXTENDER.md')
-rw-r--r--docs/EXTENDER.md127
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