Aleph aprende a googlear (sin Google)
Cómo le dimos al agente acceso a la web sin usar ninguna API paga, qué problemas nos encontramos en el camino, y los trucos que tuvimos que implementar para que funcione bien.
Uno de los problemas más evidentes de un modelo local es que su conocimiento tiene fecha de corte. Si le preguntás por la documentación de una librería que salió hace tres meses, no tiene idea. Si necesita el changelog de una versión nueva de Rust, tampoco.
La solución obvia: darle acceso a internet. Pero acá está el dilema: los servicios de search con API cuestan plata (Google Search API, Bing, Serper...), y nosotros queremos que Aleph sea 100% gratuito para usar. Así que empezamos a buscar alternativas.
web_search: DuckDuckGo HTML al rescate
DuckDuckGo tiene un endpoint poco conocido: html.duckduckgo.com/html/. A diferencia de la página principal que es una SPA de JavaScript, este endpoint devuelve HTML estático con los resultados de búsqueda. Sin JavaScript, sin tracking agresivo, y sin API key.
DDG tiene una API "Instant Answers" pero está pensada para respuestas directas tipo Wikipedia, no para resultados de búsqueda generales. Para lo que necesitamos (listas de URLs relevantes), el scraping del HTML es más rico.
El proceso es simple: hacemos un GET con el query como parámetro, parseamos el HTML con la crate scraper, y extraemos títulos, snippets y URLs. Las URLs vienen envueltas en redirects de DDG con el parámetro uddg, así que las decodificamos para obtener la URL real.
// Los resultados excluyen anuncios con :not(.result--ad)
let sel_result = Selector::parse(".result:not(.result--ad)").unwrap();
// El href está en formato //duckduckgo.com/l/?uddg=<encoded-url>
// Extraemos y decodificamos:
fn extract_uddg(href: &str) -> Option<String> {
let parsed = url::Url::parse(&normalized).ok()?;
parsed.query_pairs()
.find(|(k, _)| k == "uddg")
.map(|(_, v)| urlencoding::decode(&v).ok()?.into_owned())
}
El resultado es una lista de hasta 20 resultados (configurable) con título, URL limpia y snippet. El modelo los recibe y decide qué URL quiere leer en detalle.
web_fetch: leer páginas reales
Buscar es sólo el primer paso. El modelo también necesita leer el contenido de una página. Para eso está web_fetch: hace un GET a la URL, detecta si es HTML, y si lo es, hace stripping: elimina <head>, <script>, <style>, <nav> y <footer>, y convierte el HTML restante a texto plano.
El texto plano es lo que recibe el modelo. Sin distracciones, sin tags, sólo el contenido.
El problema de GitHub
La primera prueba práctica fue pedirle a Aleph que buscara documentación de llama-server. Encontró el README en GitHub, intentó leerlo... y recibió "Skip to content" seguido de casi nada. El problema: GitHub renderiza su contenido con JavaScript. La URL de tipo github.com/ggerganov/llama.cpp/blob/master/README.md devuelve una SPA vacía si no ejecutás JS.
La solución fue simple: reescribir automáticamente esas URLs antes de fetchear:
// github.com/{owner}/{repo}/blob/{ref}/{path}
// → raw.githubusercontent.com/{owner}/{repo}/{ref}/{path}
fn rewrite_github_url(url: &str) -> (String, bool) {
if !url.starts_with("https://github.com/") { return (url.to_string(), false); }
let parts: Vec<&str> = rest.splitn(4, '/').collect();
if parts.len() == 4 && parts[2] == "blob" {
let raw = format!("https://raw.githubusercontent.com/{}/{}/{}",
parts[0], parts[1], parts[3]);
return (raw, true);
}
(url.to_string(), false)
}
Ahora cuando el modelo pide leer un archivo de GitHub, obtiene el Markdown crudo. Perfecto.
Detectar páginas JS-rendered
GitHub no es el único caso. Muchas páginas modernas son SPAs puras: el HTML que devuelven sin JS es básicamente un esqueleto vacío con <div id="root"></div>. Si el modelo recibe "3 caracteres de contenido útil", va a entrar en loop intentando fetchear de nuevo o inventar cosas.
La solución fue agregar un umbral mínimo: si el texto extraído tiene menos de 200 caracteres, en lugar de devolver ese texto vacío, devolvemos un mensaje explicativo con una sugerencia de qué hacer:
- Si es una URL de GitHub (pero no un archivo/blob): sugerir usar la herramienta
bashcon el CLI degh. - Para cualquier otra SPA: sugerir buscar una versión cacheada o un endpoint de API alternativo.
Un buen agente no sólo ejecuta herramientas, también sabe cuándo una herramienta no va a funcionar y dice por qué.
Probando en la práctica
El primer test real fue pedirle a Aleph: "Buscá la documentación del endpoint /v1/chat/completions de llama-server y resumí los parámetros más importantes." Lo que hizo:
- Llamó a
web_searchcon "llama-server /v1/chat/completions parameters". - Eligió el resultado de GitHub que era el README del repositorio.
- Llamó a
web_fetchcon la URL de GitHub. El rewrite automático la convirtió a raw.githubusercontent.com. - Recibió 8.400 caracteres de Markdown con toda la documentación.
- Resumió los parámetros más importantes en texto claro.
Funcionó sin intervención humana. Eso es lo que queremos.
Las limitaciones honestas
DuckDuckGo HTML a veces no devuelve resultados si detecta demasiadas requests seguidas. No es rate limiting agresivo, pero sí pasa. Estamos evaluando agregar un fallback, pero por ahora la solución es reformular la búsqueda. Para páginas JS-rendered complejas (apps de documentación modernas, portales con login), el fetch no va a funcionar y hay que buscar alternativas. Lo decimos explícitamente en el mensaje de error para que el modelo sepa qué hacer.
Es un 80/20 bastante bueno: la mayoría de la documentación técnica útil está en páginas estáticas, GitHub, o Stack Overflow. Con estas dos herramientas, Aleph puede leerlas todas.