Askly | Automata
secciones (10)
$ cd .. // volver a proyectos
automata@latam: ~/proyectos/askly
PRODUCTO · slug: askly ·2026 · 4 min read · 905 words

> Askly

// Chatbot RAG aislado por sesion: subis PDF/DOCX/XLSX, preguntas, recibis respuestas en streaming basadas en tus documentos — pgvector + embeddings HuggingFace + LLM MiniMax.

Next.js 16React 19SupabasepgvectorHuggingFaceMiniMax M2Vercel AI SDK
Askly
▸ rol
Full-stack · RAG
▸ equipo
Solo
▸ status
online
// section 01 · descubrimiento

$ cat ./descubrimiento.md

▸ descripcion

Subis un PDF, un Word o un Excel y chateas con el. El frontend es una app Next.js de tres paneles: historial de chats, mensajes + input, libreria de documentos. Los uploads se parsean (unpdf / mammoth / xlsx), se chunkean (1000 chars, 200 de overlap) y se embedean via HuggingFace Inference (384 dims). Los embeddings viven en Supabase con la extension pgvector y un indice HNSW. En una pregunta, la query se embedea, se matchea contra el HNSW, los chunks top se concatenan en un prompt para MiniMax M2, y la respuesta llega en streaming via Vercel AI SDK. Las sesiones son aisladas por navegador (UUID en localStorage, header x-session-id) — sin cuentas.

▸ problema

La busqueda por keyword sobre documentos subidos falla apenas la pregunta no calza palabra por palabra con el texto. La solucion correcta es busqueda semantica, pero normalmente eso significa un vector DB administrado (Pinecone, Weaviate). pgvector dentro de Supabase alcanza para el caso de uso y saca un vendor de encima.

▸ audiencia

Devs, investigadores y estudiantes que quieren chatear con sus propios documentos en privado, sin que el material se vaya a una cuenta que no controlan.

// section 03 · arquitectura

$ cat ./arquitectura.md

// section 03b · secuencias

$ cat ./secuencias.md

// flujos en runtime — quien habla con quien y en que orden

// Desde la pregunta tipeada hasta la respuesta en streaming que cita los chunks de documento que uso.

Query RAG — pregunta a respuesta apoyada en los documentos · flow-01
loading…
// section 04 · infraestructura

$ cat ./infraestructura.md

▸ servicios
provider: Supabase (Postgres + pgvector) + HuggingFace Inference + API MiniMax
  • Next.js 16 App Router en Vercel
  • Supabase Postgres con extension pgvector
  • Indice HNSW sobre document_chunks.embedding
  • HuggingFace Inference API (embeddings 384 dims)
  • MiniMax M2 (LLM, respuestas streaming)
  • Vercel AI SDK para la UI de streaming
// section 05 · implementacion

$ cat ./implementacion.md

▸ frontend
  • · Next.js 16
  • · React 19
  • · Tailwind CSS 4
  • · Vercel AI SDK (useChat)
  • · react-markdown + GFM
▸ backend
  • · API routes de Next.js (6 handlers)
  • · Supabase JS client (service role para escrituras admin)
  • · LangChain RecursiveCharacterTextSplitter
  • · Parsers unpdf / mammoth / xlsx
  • · Vercel AI SDK para streaming
▸ datos
  • · Postgres 16 (gestionado por Supabase)
  • · pgvector con indice HNSW
  • · 7 migrations SQL (schema inicial, dims de embedding, doc names en match results, tuning de indice, sessions/chats)
▸ devops
  • · Hosting en Vercel
  • · NODE_OPTIONS=--max-old-space-size=4096 para uploads grandes
// section 06 · desafios tecnicos

$ cat ./challenges/*.md

// 3 problemas tecnicos resueltos

01 / 03
challenge-01.md · rag · chunking · embeddings
▸ problema

Chunkear PDFs sin partir oraciones y sin agotar el rate limit de embedding.

restriccion: HuggingFace Inference rate-limita agresivamente; un PDF de 200 paginas puede dar 800+ chunks. Cada llamada reintentada cuenta. Chunks muy chicos pierden coherencia semantica; muy grandes desbordan el context window del LLM al concatenarse.

▸ enfoque

RecursiveCharacterTextSplitter con chunkSize=1000 y chunkOverlap=200. Los embeddings se batchean (10-20 por vez) con backoff exponencial sobre 429. El chunking y el embedding pasan en la API route, nunca en el browser.

lib/chunking.ts typescript
const splitter = new RecursiveCharacterTextSplitter({
  chunkSize: 1000,
  chunkOverlap: 200,
});

const docs = await splitter.createDocuments([cleanText]);

return docs.map((doc, index) => ({
  content: doc.pageContent,
  index,
}));
challenge-02.md · rag · intent · busqueda
▸ problema

Un umbral de similitud fijo da mala calidad RAG: preguntas generales ("de que trata?") necesitan muchos chunks de baja similitud; preguntas especificas necesitan pocos chunks de alta similitud.

restriccion: El HNSW de pgvector no permite cambiar umbral por query sin re-emitir el RPC. La division entre "general" y "especifica" no se puede inferir solo de similitud — para entonces ya es tarde.

▸ enfoque

Un clasificador de intencion regex simple mira la query cruda (patrones ES + EN: "de que trata", "summary", "overview"). Intencion general → match_count = 30, threshold = 0.2. Intencion especifica → match_count = 5, threshold = 0.3. Los nombres de los documentos matcheados se inyectan en el prompt asi el modelo puede citar fuentes.

lib/rag.ts typescript
const GENERAL_PATTERNS = [
  /\bde\s+qu[]\s+(va|trata|habla)\b/i, // ES: "de que trata"
  /\bsummar(y|ize|ise)\b/i,                  // EN
  /\boverview\b/i,
];

function classifyQueryIntent(query: string) {
  return GENERAL_PATTERNS.some((p) => p.test(query))
    ? { type: "general" as const }
    : { type: "specific" as const };
}

const { data } = await supabaseAdmin.rpc("match_document_chunks", {
  query_embedding:    embedding,
  match_threshold:    isGeneral ? 0.2 : 0.3,
  match_count:        isGeneral ? 30  : 5,
  filter_session_id:  sessionId,
});
challenge-03.md · sesion · privacidad · tradeoffs
▸ problema

Aislar los documentos de un user de los de otro sin sistema de auth.

restriccion: Sin cuentas, sin JWT. Lo que vive solo en localStorage desaparece si el user lo limpia. Los UUID en headers son adivinables en teoria (colisiones v4 son astronomicamente improbables pero la API no tiene prueba de ownership).

▸ enfoque

Tradeoff honesto: se genera un UUID v4 en el primer load, se persiste en localStorage, y se envia como x-session-id en cada request. Cada fila de Postgres lleva session_id, cada query filtra por ella. Aceptable para MVP; un launch real necesita Row-Level Security de Supabase con auth.

lib/client-session.ts typescript
// Lado cliente
export function getOrCreateSessionId(): string {
  let id = localStorage.getItem("askly-session-id");
  if (!id) {
    id = crypto.randomUUID();
    localStorage.setItem("askly-session-id", id);
  }
  return id;
}

// Lado servidor (cada API route)
const sessionId = req.headers.get("x-session-id");
if (!sessionId) return new Response("Unauthorized", { status: 401 });

const { data } = await supabase
  .from("chats")
  .select("*")
  .eq("session_id", sessionId);
// section 07 · testing & ci

$ cat ./testing.md

▸ estrategia

Tests de componentes sobre la UI de chat y el flujo de upload. Tests de integracion de API mockean Supabase + HuggingFace. La logica RAG se testea con PDFs de muestra (chunking, embedding, retrieval). Sin e2e Playwright todavia.

▸ herramientas
Jest@testing-library/react
// section 09 · resultados

$ cat ./resultados.md

01 /
6
handlers de API route Next.js
02 /
15
componentes React
03 /
7
migrations SQL
04 /
7
parsers de documento (pdf/docx/xlsx/txt/csv/html/md)
05 /
384
dimensiones de embedding (HuggingFace)
▸ outcomes

MVP corriendo. Los users suben docs, hacen preguntas, reciben respuestas en streaming basadas en la fuente. El retrieval hibrido por intencion le gana a un umbral fijo. Los rate limits de la API de embedding son el techo actual para uploads grandes — el batching + backoff esta puesto pero un tier pago destrabaria workloads reales.

// section 10 · lecciones

$ cat ./lessons.md

// si lo hiciera de nuevo

  • 01 /

    pgvector + HNSW alcanza para un RAG MVP

    El primer instinto fue agarrar Pinecone. pgvector dentro de Supabase entrego latencias de query que la UI ni notaba, con cero costo extra de vendor. El dia que llegue un workload real, el swap de indice es un ALTER.

  • 02 /

    Clasificar intencion > tunear un umbral unico

    Buscar el unico valor de umbral que funcione tanto para "de que trata?" como para "cual es el deadline en la pagina 47?" es un callejon sin salida. Bifurcar por intencion (general vs especifica) en el sitio de la llamada RPC le gano a cada umbral que probe.

// siguiente paso

$ automata deploy --tu-operacion

// Conversemos sobre como adaptamos esto a tu caso.

./contactar.sh