Saltar a contenido

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