External Providers
Cómo habla el research framework con servicios externos: patrón de adaptador, token pool, request journal, retry y failover.
Estado
Implemented. Todos los adaptadores están operativos. El token pool está en uso en producción para la integración con Perplexity. El request journal está habilitado para todos los adaptadores de pago.
Propósito
El research framework delega toda la comunicación externa en adaptadores. El objetivo es:
- Mantener el núcleo del framework (orquestador, fases, esquemas) agnóstico al proveedor.
- Hacer explícitos el coste, los límites de tasa y los modos de fallo.
- Proporcionar un audit trail completo de cada llamada externa.
Esta página es la referencia canónica para la capa de adaptadores y las políticas cross-cutting (token pool, request journal, retry, failover). El framework en su conjunto está en Framework. El modelo de auditoría y seguridad están en Modelo de auditoría y Principios de seguridad.
Contrato de adaptador
Cada adaptador implementa (o envuelve) una interfaz pequeña y estable. El contrato mínimo:
Para adaptadores especializados (YouTube, Perplexity), el contrato se extiende pero siempre devuelve objetos de scout/schemas/sources.py (o un equivalente tipado).
El esquema Source:
| Campo | Tipo | Descripción |
|---|---|---|
id | string | ID estable de la fuente. |
title | string | Título de la fuente. |
url | string | URL canónica de la fuente. |
publisher | string | El publisher o host. |
phase | string | La fase que produjo esta fuente. |
retrieved_at | datetime (UTC) | Cuándo se recuperó la fuente. |
Devolver objetos Source tipados es el contrato que hace que el resto del framework sea agnóstico al proveedor.
Inventario de adaptadores
| Adaptador | Proveedor | Lo usa | Auth requerida | Modelo de coste |
|---|---|---|---|---|
tavily_adapter.py | Tavily | F1, F2 | API key | Por request |
duckduckgo_adapter.py | DuckDuckGo | F1 (fallback) | ninguna | Free |
google_search_adapter.py | Google Custom Search | F1 (opcional) | API key + cx | Por query (100/día free) |
youtube_transcript_api.py | YouTube Transcript | Y2 | ninguna | Free |
yt_dlp_adapter.py | yt-dlp | Y2 (fallback) | ninguna | Free |
perplexity_adapter.py | Perplexity | P3 | Token pool | Por token |
El adaptador Perplexity se documenta en detalle más abajo. Los adaptadores de búsqueda comparten las mismas políticas de retry y rate limit; lo único que difiere es la auth y el modelo de coste.
Token Pool
La integración con Perplexity es la única que usa un token pool. El pool existe porque el lab tiene múltiples cuentas de Perplexity (por capacidad y redundancia) y porque la integración se construyó originalmente alrededor de un plan de tokens sk-cp-... que tiene límites de tasa por cuenta.
Configuración del pool
El pool se configura con token_pool_config.json (o token_pool_config.example.json para la plantilla):
| Campo | Descripción |
|---|---|
name | Una etiqueta amigable para el token. |
value | La API key real. |
weight | El peso relativo en el round-robin (mayor = más a menudo). |
La estrategia puede ser round-robin, weighted-round-robin, least-used o failover. El default es weighted-round-robin.
Runtime del pool
El token pool se mantiene en memoria. En cada request:
- El pool selecciona un token según la estrategia.
- El adaptador hace la request con el token seleccionado.
- El pool registra la request y la respuesta.
- Si la request falla con 401 o 403, el pool marca el token como
quota_exhaustedy selecciona el siguiente. La request se reintenta con el nuevo token. - Si todos los tokens están
quota_exhausted, el pool devuelve un error y la fase se marca comofailed_retryable.
El pool también registra:
- Las requests totales por token.
- Los errores totales por token.
- El último error por token.
- La última request exitosa por token.
El estado del pool se expone como un fichero JSON bajo {framework-root}/runtime/token_pool_state.json. Se le puede pedir al Coordinator que muestre el estado actual.
Seguridad del pool
El fichero del pool es el fichero más sensible del framework. Es:
- Almacenado con permisos
0600(solo lectura/escritura del owner). - Excluido de cualquier backup que se suba fuera de la máquina.
- Recargado en cada restart del proceso (el fichero es la fuente de verdad, el estado en memoria es un caché).
- Auditado vía el request journal (cada request registra qué token se usó, por nombre, nunca por valor).
El valor de un token nunca se escribe en ningún log, journal o artefacto. El name del token es lo que registra el journal.
Request Journal
Cada llamada de adaptador se registra en un único journal append-only. El journal es {framework-root}/runtime/request_journal.jsonl. Cada línea es un objeto JSON:
El journal es append-only. Las entradas nunca se actualizan ni se borran. La rotación es por fichero: cuando el journal excede un tamaño configurado (SCOUTE_JOURNAL_MAX_BYTES, default 50 MB), se renombra a request_journal.<timestamp>.jsonl y se empieza un fichero nuevo.
El journal es la base del modelo de auditoría. Se le puede pedir al Coordinator que:
- Muestre las últimas N requests para una misión dada.
- Muestre las requests para un token dado.
- Muestre las requests que fallaron.
- Replay una request (ayuda de debug).
El journal no almacena cuerpos de request que puedan contener datos personales. Si un adaptador acepta un input libre, el journal almacena una versión redactada (p. ej., keywords de la query pero no el texto completo).
Política de retry
Todos los adaptadores comparten una política de retry por defecto. La política la aplica un helper de retry compartido que envuelve la llamada del adaptador.
| Parámetro | Default | Descripción |
|---|---|---|
max_attempts | 3 | Intentos totales (1 original + 2 reintentos). |
initial_backoff | 1.0 s | Espera inicial entre intentos. |
backoff_factor | 2.0 | Multiplicador para la espera en cada intento posterior. |
max_backoff | 30.0 s | Tope para la espera del backoff. |
retry_on_status | 429, 5xx | Códigos HTTP que disparan un retry. |
retry_on_timeout | true | Si reintentar en timeouts de conexión/lectura. |
retry_on_socket | true | Si reintentar en errores a nivel de socket. |
El helper de retry usa backoff exponencial con jitter completo. No reintenta en errores 4xx distintos de 429.
Failover
Failover es el acto de cambiar de un proveedor a otro cuando el primario no está disponible. El framework implementa failover a dos niveles.
Failover a nivel de adaptador
Cada fase declara una lista ordenada de adaptadores. La fase llama al primer adaptador; si devuelve failed_retryable tras agotar sus reintentos, la fase llama al siguiente adaptador.
Ejemplo para F1:
Si Tavily devuelve un 5xx, F1 reintenta Tavily dos veces, luego recurre a DuckDuckGo, luego a Google. Si los tres fallan, la fase es failed_retryable.
Failover a nivel de proveedor (solo Perplexity)
El token pool implementa failover a nivel de proveedor. Cuando una request a Perplexity falla por un problema de token (401, 403, 429 desde la perspectiva de cuota), el pool cambia al siguiente token. La request se reintenta de forma transparente.
Esto es adicional al retry HTTP normal. El flujo completo para una request a Perplexity es:
- Escoger un token del pool.
- Hacer la request.
- Si la respuesta es 200, devolver.
- Si la respuesta es 429 o 5xx, reintentar con el mismo token (backoff exponencial).
- Si se agotan los reintentos, marcar el token como
quota_exhausted(para 429) oerror(para 5xx) y escoger el siguiente token. - Si todos los tokens están
quota_exhausted, fallar la fase.
Límites de tasa
Cada adaptador tiene un cap de requests por minuto. El cap lo aplica un token bucket por adaptador:
| Adaptador | Cap por defecto (req/min) |
|---|---|
tavily | 60 |
duckduckgo | 30 |
google_search | 100 |
youtube_transcript | 60 |
yt_dlp | 20 |
perplexity | 20 |
El cap es configurable por despliegue. El cap se aplica antes de la request, no después, así que el framework no desperdicia requests en proveedores rate-limited.
Tracking de coste
Para adaptadores de pago (Tavily, Perplexity), el framework registra el coste de cada request en el journal. El coste es el producto del usage reportado en la respuesta y la tarifa publicada del proveedor.
El campo cost del journal se rellena cuando el adaptador devuelve información de usage. El framework expone un endpoint /api/cost-summary que agrega costes por misión, por adaptador y por token.