Sourcely
Asistente RAG para hacer preguntas a tus propios PDFs y audios, con respuestas citadas que enlazan a la página o al segundo exacto de la fuente.

El Desafío
Preguntar a un PDF con un LLM genérico es un ejercicio de fe: el modelo contesta con soltura pero las páginas que cita no existen. En un RAG casero se sumaban tres problemas reales:
- Alucinación de citas. Los LLM inventan referencias. Sin un sistema que ate la cita al chunk recuperado, el usuario no puede verificar nada.
- Fuga entre cuentas. La recuperación por similitud sin filtro de
user_idhace que un prompt manipulado del usuario A devuelva los chunks del usuario B. - Multiformato real. PDFs y MP3s requieren pipelines distintos que deben terminar en la misma representación vectorial. Y la cita tiene que ser exacta: página N para PDF, segundo M:SS para audio.
La Solución
Citas que apuntan al byte exacto. El prompt le dice al LLM: “responde solo del contexto, cita como [1], [2]…”. Cada [n] corresponde a una tarjeta de fuente. Para PDFs, la tarjeta enlaza a la URL firmada con #page=N. Para audios, salta al currentTime del elemento. El usuario siempre puede hacer click y comprobar.
Aislamiento por usuario en la capa SQL. Cada consulta hace JOIN documents ON chunks.document_id = documents.id WHERE documents.user_id = current_user.id. Lo escribo en la query, no en el código, precisamente porque el código puede tener bugs. Como segunda capa: Row-Level Security en las cinco tablas.
Multiformato con un solo pipeline. PDFs (pypdf) y audio (faster-whisper con timestamps) pasan por el mismo chunking, las mismas embeddings Gemini a 768 dim, el mismo índice HNSW. La cita cambia el formato, no el pipeline.
Streaming para que se sienta en vivo. La respuesta viaja por Server-Sent Events, no por una respuesta completa. Es la diferencia entre “esperar 8 segundos” y “ver la respuesta construirse”.
Decisiones técnicas que defendí
- 768 dimensiones, no 1536. Gemini
text-embedding-001permitedimensionsarbitrario (truncamiento Matryoshka). La pérdida de calidad es <2% y los embeddings caben el doble en memoria. - HNSW, no IVFFlat. IVFFlat requiere reentrenar el índice periódicamente. HNSW es más lento de construir pero no necesita reentrenamiento.
- JOIN explícito a
documents, no filtrar porchunk.user_id. El JOIN garantiza el aislamiento aunque alguien olvide el filtro. Test dedicado: usuario A sube, usuario B pregunta, B recibe lista vacía. - Gemini en vez de OpenAI para embeddings. OpenAI cobra por token; Gemini tiene tier gratuito con 1500 req/día.
- Groq en vez de OpenAI/Anthropic para el LLM. Tier gratuito con latencia <500ms. La calidad de
llama-3.3-70b-versatilees comparable a GPT-4-mini para RAG con citas.
Lo que aprendí
- El “impact” no está en el modelo, está en la tubería. Un LLM mejor no arregla citas falsas. La cita fiable viene de: prompt estricto + retrieval con JOIN explícito + UI que muestra el fragmento exacto.
- Row-Level Security es barato y pocos lo hacen. Activarlo fue 30 líneas de SQL.
- El cold start de Whisper pesa. La primera llamada descarga ~460 MB del modelo. En Render free son 30-60 segundos extra.
- SQLite para tests, Postgres para prod. Los unit tests usan sqlite; los integration usan Supabase real y detectan drift.
Licencia y costos
- Coste por uso: ~$0.0001 por consulta (embeddings Gemini + free-tier Groq).
- Infraestructura: Supabase free + Render free + Vercel free. Coste total: $0/mes.
- Repositorio: github.com/k1k3cb/Sourcely.
- Licencia: MIT.
analyticsImpacto
Highlights de la Interfaz
Hero de Sourcely
La propuesta: haz preguntas sobre tus propios PDFs y audios, obtén respuestas citadas.
Chat con citas
Respuestas en streaming con marcadores [1] [2] que enlazan a la fuente exacta.
Zona de documentos
Subida drag-and-drop de PDFs y audio con validación por magic bytes.
Arquitectura
Next.js habla con FastAPI; embeddings Gemini, pgvector HNSW y generación en Groq.