aboutsummaryrefslogtreecommitdiffstats
path: root/docs
diff options
context:
space:
mode:
Diffstat (limited to 'docs')
-rw-r--r--docs/EXTENDER.md127
-rw-r--r--docs/RENDIMIENTO.md131
2 files changed, 254 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
diff --git a/docs/RENDIMIENTO.md b/docs/RENDIMIENTO.md
index 543ee62..b51f392 100644
--- a/docs/RENDIMIENTO.md
+++ b/docs/RENDIMIENTO.md
@@ -17,6 +17,8 @@ Linux 7.2 (CachyOS) · PipeWire.
| El TTS decodificaba el audio en bloques de 24 s | 4948 ms al primer audio | 585 ms | **8,5×** |
| La plantilla del LLM abría `<think>` y no lo cerraba | 8630 ms al primer token | 413 ms | **21×** |
| El prompt de estilo anulaba las llamadas a herramientas | 0 de 8 aciertos | 8 de 8 | — |
+| La cámara capturaba a 1280x720 | 7,8 s por imagen | 2,9 s a 640x480 | **2,7×** |
+| Una herramienta se ejecutaba en silencio | ~8,4 s hasta oír algo | 4,1 s con acuse hablado | **2×** |
Los tres son de configuración, no de código. Ninguno da error: el sistema
funciona, sólo que despacio o mal, que es lo que los hace difíciles de ver.
@@ -120,6 +122,122 @@ estilo. Se compensa en parte porque `clean_for_speech` quita el markdown antes
de hablar, y `max_tokens` acota la longitud. Con
`tools.dedicated_prompt = false` se invierte la prioridad.
+## 4. La cámara: la resolución manda en la latencia
+
+El servidor ya carga el proyector multimodal, así que el mismo modelo que
+conversa describe lo que capta la cámara. Lo que cuesta es la imagen: cada
+píxel se convierte en tokens que el modelo tiene que procesar.
+
+Misma escena, misma pregunta, sólo cambia la resolución de captura:
+
+| Resolución | Tamaño | Respuesta del modelo | Calidad |
+|---|---|---|---|
+| 320x240 | 3 KB | **1,33 s** | «Una pared blanca con un interruptor» |
+| 640x480 | 9 KB | **2,88 s** | «Una pared blanca y un interruptor blanco en la pared izquierda» |
+| 960x540 | 15 KB | 5,23 s | describe además el marco de la puerta |
+| 1280x720 | 24 KB | 7,81 s | equivalente a 960x540 |
+
+640x480 es el punto de equilibrio: sigue distinguiendo objetos y colores, y
+cuesta la tercera parte que la resolución máxima. Por encima de 960x540 el
+tiempo se duplica sin que la descripción mejore.
+
+La captura en sí es barata: **0,45 s** con ffmpeg, y descartar unos fotogramas
+para que la exposición automática se asiente sale prácticamente gratis (0,53 s
+sin descartar ninguno frente a 0,45 s descartando tres; el brillo medio baja de
+176 a 174 mientras el sensor se ajusta).
+
+## 4 bis. Los tres caminos a DuckDuckGo, y cuál funciona
+
+Buscar sin clave es posible, pero no por donde parece. Comprobado aquí:
+
+| Camino | Resultado |
+|---|---|
+| `curl` a `html.duckduckgo.com/html/` | **HTTP 202** con página anti-bot (`anomaly.js`), cero resultados |
+| API sin clave `api.duckduckgo.com` (Instant Answer) | **vacío** para «capital de Australia» y para «qué tiempo hace en Buenos Aires» |
+| Librería `ddgs` (la que usa `duckduckgo-mcp`) | **funciona**, 1,5–5,8 s |
+
+La librería rota buscadores y cabeceras, y por eso pasa donde curl no.
+
+Comparado con Tavily sobre las mismas preguntas:
+
+| | «capital de Australia» | «tiempo hoy en Buenos Aires» | Forma del resultado |
+|---|---|---|---|
+| tavily | **0,54 s** | 2,88 s | respuesta ya redactada |
+| ddgs | 2,24 s | 3,59 s | fragmentos de tres páginas |
+
+Lo que decide no es el tiempo sino la forma. Con Tavily el asistente lee la
+respuesta; con ddgs el modelo tiene que resumir los fragmentos, y con 2B eso
+sale bien casi siempre —en el pipeline completo devolvió «Hoy en Buenos Aires
+hace soleado esta mañana y por la tarde tendremos nubes con temperaturas
+alrededor de 9°C», correcto— pero es una oportunidad más de equivocarse.
+
+## 5. Con tres herramientas el modelo elige bien, pero busca de más
+
+Con `hora_actual`, `buscar_en_internet` y `mirar_por_la_camara` declaradas a la
+vez, sobre 11 preguntas y dos intentos cada una:
+
+| Tipo de pregunta | Acierto |
+|---|---|
+| «¿Qué hora es?», «¿Qué día es hoy?» | 4 / 4 |
+| «¿Qué tiempo hace?», «¿A cuánto está el dólar?», «Busca noticias» | 6 / 6 |
+| «¿Qué ves?», «¿De qué color es mi camiseta?», «Mira por la cámara» | 5 / 6 |
+| Sin herramienta: «¿Por qué el cielo es azul?», «Cuéntame un chiste», «¿Cuánto es 15 por 4?» | 1 / 6 |
+
+**Elegir entre las tres se le da bien (15/16).** Lo que falla es abstenerse:
+busca en internet cosas que ya sabe. Cuesta unos 2,5 s de más, pero la
+respuesta sigue siendo correcta, así que es un problema de latencia y no de
+exactitud.
+
+Intentar corregirlo con instrucciones no funcionó, coherente con lo del
+apartado 3:
+
+| Guía de herramientas | Aciertos | Falsos positivos |
+|---|---|---|
+| Sola | **16 / 22** | 5 |
+| + «No uses ninguna herramienta para conocimiento general» | 16 / 22 | 6 |
+| + «Las herramientas son sólo para datos que cambian» | 13 / 22 | 6 |
+
+Añadir la prohibición no reduce los falsos positivos y sí empeora los
+aciertos. Se queda la guía sola.
+
+## 6. Sin acuse hablado, una herramienta son seis segundos de silencio
+
+Una búsqueda tarda unos 2,4 s y la cámara unos 4,5, y a eso hay que sumarle las
+dos pasadas del modelo. El turno entero sale por encima de los 7 s, y sin nada
+que oír se lee como que el asistente se ha colgado.
+
+Por eso `Tool::acknowledgement` devuelve una frase que se pronuncia **antes**
+de ejecutar —«Déjame que lo busque», «Voy a mirar»—. Medido en turnos reales:
+
+| | Primer audio |
+|---|---|
+| Turno con búsqueda, sin acuse | ~8,4 s (estimado: suma de las dos pasadas) |
+| Turno con búsqueda, con acuse | **4,1 s** |
+| Turno con cámara, con acuse | **3,2 s** |
+
+El turno sigue durando lo mismo; lo que cambia es cuándo empieza a oírse algo,
+que es lo único que percibe quien pregunta.
+
+## 7. El resultado de la herramienta hay que mandarlo usar
+
+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ó:
+
+> He tomado una foto de la cámara para mostrarte lo que hay delante. Ahora
+> puedo responderte sobre el objeto o color, pero si quieres saber más
+> información actualizada como precios, noticias o datos específicos, ¡puedo
+> buscarlo por internet!
+
+El resultado —«Un hombre con gorro sostiene un teléfono»— estaba en la
+conversación y lo ignoró. Con `general.tool_result_prompt` añadido a esa
+pasada:
+
+> Se ve un hombre con una chaqueta de peluche y el capuchón puesto,
+> sosteniendo un teléfono en su mano y mirando directamente a la cámara.
+
+Aquí sí se puede añadir estilo sin romper nada, al contrario que en el
+apartado 3: la llamada ya ocurrió, así que no queda nada que estropear.
+
---
## Un turno real, medido de punta a punta
@@ -146,6 +264,16 @@ turno #1 — desglose de latencia
- **Latencia percibida: 3213 ms sin herramienta, 6320 ms con ella.** La
diferencia es la segunda pasada del modelo, que sólo ocurre si llama.
+Turnos con las herramientas nuevas, también medidos de punta a punta:
+
+| Turno | ASR | LLM 1ª | Herramienta | LLM 2ª | Primer audio |
+|---|---|---|---|---|---|
+| «¿Qué tiempo hace en Buenos Aires?» | 194 ms | 2243 ms | 2132 ms (búsqueda) | 4079 ms | **4122 ms** |
+| «¿Qué ves por la cámara?» | 185 ms | 1649 ms | 5229 ms (captura + visión) | 1602 ms | **3153 ms** |
+
+El primer audio llega mucho antes que el final del turno porque suena el acuse
+mientras la herramienta trabaja.
+
## La GPU es de 4 GB y no caben los dos
`tts-server` deja residentes unos 3,2 GB (1765 MB del hablante, 177 del
@@ -210,6 +338,9 @@ RUST_LOG=info,latencia=info cargo run --release -- run
# Cada ventana del ASR, cada síntesis, cada frase
RUST_LOG=info,asr=debug,tts=debug cargo run --release -- run
+# Qué busca y qué mira
+RUST_LOG=info,herramientas=info,camara=info cargo run --release -- run
+
# Comprobar de nuevo las cifras de arriba
scripts/servidores.sh arrancar
cargo test --release -p asist-app --test integracion -- --nocapture