RAG Hub — Arquitectura
Estructura del proyecto
| Ruta |
Contenido |
~/ai-stack/rag-api/main.py |
FastAPI app principal (228 líneas) |
~/ai-stack/rag-api/collection_config.py |
Config de colecciones (fuente de verdad) |
~/ai-stack/rag-api/embedding_router.py |
Routing de embeddings por colección |
~/ai-stack/rag-api/auto_indexer.py |
Watchdog para indexado automático |
~/ai-stack/rag-api/check_integrity.py |
Verificación de integridad nocturna |
~/ai-stack/rag-api/migrate_smallcountry.py |
Script de migración 768d → 1024d |
Flujo de una consulta (query/{collection})
POST /query/smallcountry {"query": "¿colmenas?", "top_k": 5}
│
├── embedding_router.get_embedding(query, "smallcountry")
│ → lee collection_config → mxbai-embed-large, 1024d
│ → POST http://localhost:11434/api/embeddings
│ → devuelve vector 1024d
│
├── qdrant.query_points(collection="smallcountry", query=vector, limit=5)
│ → devuelve ScoredPoint[] con payloads
│
├── extract_text(payload)
│ → busca "text", luego "_node_content.text" (formato LlamaIndex)
│
├── generate_llm(contexto, query, model="deepseek-r1:8b")
│ → POST http://localhost:11434/api/chat
│ → system prompt con contexto + user query
│
└── Response: {"answer": "...", "sources": [...]}
Flujo de Responses API (para AnythingLLM)
POST /responses {"model": "gpt-4o", "input": [...]}
│
├── flatten(input) → extrae textos recursivamente
│ input = [{"role":"user", "content":[{"type":"input_text", "text":"..."}]}]
│ → aplanado recursivo: dicts → listas → strings
│ → resultado: "¿pregunta?"
│
├── (mismo flujo que /query/: embedding → Qdrant → LLM)
│
└── Response: {"id": "resp_...", "object": "response", "output": [...]}
Decisiones de diseño
- Routing manual en lugar de LlamaIndex: más simple, menos dependencias
- Systemd timers en lugar de cron: logs en journald, Persistent=true
- 100% offline: Ollama + Qdrant locales, sin APIs externas
- API OpenAI-compatible + Responses API: cualquier cliente compatible funciona
extract_text() adaptado a payloads de LlamaIndex (_node_content anidado)
- Fallback de colección: si el modelo no coincide, usa
smallcountry
Parches y fixes aplicados
| Problema |
Fix |
qdrant-client 1.18 rompió search() |
Cambiado a query_points() + .points |
Payloads de LlamaIndex sin campo text |
extract_text() busca en _node_content |
Responses API con content como lista de {type, text} |
Flatten recursivo |
AnythingLLM llama a GET /models |
Endpoint añadido |
AnythingLLM usa POST /responses |
Endpoint añadido con Responses API |