Volver a la documentación
Guía 05

Guía de funcionalidades

Guía de las features con internals, flujo de datos y referencias file:line. Para cada feature: cómo funciona, cómo se usa, cómo se extiende.

8 min de lectura

Guía de las features con internals, flujo de datos y referencias file:line. Para cada feature: cómo funciona, cómo se usa, cómo se extiende.

RAG híbrido (Vector + PostgreSQL tsvector, RRF)

La búsqueda híbrida está repartida en dos procesos: el collector solo posee la componente vectorial (LanceDB/Qdrant); el servidor posee la FTS (PostgreSQL tsvector) y el RRF que fusiona ambas.

Flujo (packages/server/src/services/hybridSearchService.ts:104):

  1. El agente invoca la skill rag_search (agent/builtinSkills.ts:27) con query, workspaceId, archiveId.
  2. hybridSearch - Guard D-01: compara EMBEDDING_MODEL con los embeddingModel de los Document del workspace; si hay mismatch → salta el leg vector (FTS-only, sin error).
  3. Promise.all([vectorSearchViaCollector(), ftsSearch()]) - vector vía POST /api/ingest/query del collector; FTS vía prisma.$queryRaw sobre document_chunks.searchVector (índice GIN, ts_rank).
  4. RRF fusion (k=60): para cada chunk score += 1/(60+rank+1); el Set sources distingue vector/fts/both. Tiebreaker determinista: score DESC → documentId ASC → chunkIndex ASC.
  5. rag_search formatea los chunks con [Source: name (match: semantic+keyword|vector|fts, score: ...)] y devuelve SourceCitation[].
  6. Fallback archive D-07: si 0 resultados y archiveId presente, relanza sobre archive:${archiveId} y reetiqueta source: "archive".
  7. Citas propagadas al SSE citations (routes/chat.ts:508).

Metadata filtering (v0.22): rag_search acepta filtros documentTypes (pdf/md/txt/csv/docx/xlsx) y dateFrom/dateTo (ISO; el límite solo-fecha incluye el día completo). Los filtros atraviesan ambos legs de la búsqueda híbrida: el leg vector llega al collector como pre-filtros sobre los metadatos de ingest con timestamp (predicados JSONB pgvector + must-clause payload Qdrant; LanceDB y Chroma ignoran las nuevas claves con warn logueado), el leg FTS gana predicados SQL parametrizados sobre la tabla documents. Un backstop server-side post-retrieval contra la tabla documents autoritativa garantiza la corrección del filtro en cada provider. Los documentos ingestados desde ahora son siempre filtrables; los vectores legacy (pre-stamping) quedan excluidos mientras un filtro está activo y recuperan filtrabilidad vía re-embed admin - ningún schema change, ningún re-embed de los documentos existentes, comportamiento sin filtros sin cambios.

Citas grounded (v0.22.0): tras la deduplicación, un filtro de grounding topea las citas por documento a las top-2 por relevance score y elimina las citas de chunks cuyo texto no comparte solapamiento léxico significativo con la respuesta final (gate conservador - páginas wiki/archive, resultados web/memory, citas sin chunk text y respuestas muy breves se mantienen siempre). El disclaimer "No supporting evidence" se juzga sobre el conjunto filtrado (rutas streaming y no-streaming).

Chunking (collector/services/chunker.ts:16): RecursiveCharacterTextSplitter, chunkSize=1000, overlap=200, separadores ["\n\n","\n",". "," ",""]. Metadata: documentId, paragraph, charStart.

Embedding (collector/services/embeddings.ts): LocalEmbeddingProvider (Xenova all-MiniLM-L6-v2, 384-dim, air-gap) por defecto; OpenAIEmbeddingProvider (text-embedding-3-small, 1536-dim). Singleton cacheado por providerType:modelName.

Vector store (collector/services/vectorStore.ts): LanceDBProvider (default, ruta ${STORAGE_PATH}/vectors/lancedb, tabla por-workspace ws_<name>_<shortId>); QdrantProvider (placeholder enterprise).

FTS pass-through: el collector devuelve chunks[].chunkText al servidor, que escribe DocumentChunk con searchVector = to_tsvector('simple', text). El collector nunca abre Prisma.

Extender: nuevo provider de embedding → implementa EmbeddingProvider + factory getEmbeddingProvider + dimensiones en KNOWN_MODEL_DIMS. Tuning RRF → RRF_K en hybridSearchService.ts:15. Cambio de modelo de embedding → reindex vía Settings → Maintenance (POST /api/system/reindex-documents).

Agent Orchestrator (ReAct)

packages/server/src/agent/orchestrator.ts - entrada runAgent (non-stream, :78) y runAgentStreaming (:416). Patrón ReAct single-call: un solo `streamLLM` por iteración es la decision call - streamResult.toolCall determina la rama tool-vs-respuesta.

Loop:

  1. Setup: carga WorkspaceAgentConfig, parsea enabledSkills, resuelve el template (resolveSystemPrompt), antepone user.customInstructions.
  2. Resuelve provider: resolveProviderConfig(providerId, model) → fallback buildFallbackConfig.
  3. Template de constraints: localLLMOnly (bloquea providers que no sean ollama), hybridSearchForced, citationRequired.
  4. Si ragContext (widget) → lo inyecta en el system prompt, elimina rag_search de activeSkills.
  5. Plan mode (agentConfig.planMode): fase planning síncrona → SSE plan event → inyección en el prompt de ejecución.
  6. ReAct while(true):
    • Watchdog wallclock + token budget.
    • streamLLM con buffered replay D-02: iteración tool-call → descarta el buffer; final-answer → replay de tokens al onToken real (progressive DLP flush).
    • !toolCall → prueba resolveImplicitToolCall (recovery XML-ish tag) → final answer, break.
    • Skill no encontrada → circuit breaker BOT-03: trip a 3 consecutivos / 5 en total → abortReason: "unknown_tool_breaker".
    • Loop detection, per-skill timeout, truncateToolOutput, context cap.
  7. finally: libera el slot de concurrencia + persiste WorkspaceTokenUsage en todas las rutas de salida.

Skills builtin (agent/builtinSkills.ts): rag_search, workspace_memory (KV sobre SystemConfig con clave ws_memory:<ws>:<key>), document_temp_process (upload ad-hoc al collector), wiki_query (FTS archive_pages + fallback vectorial + BFS wikilinks, max 10 páginas depth 3), wiki_write (preview + SSE wiki_edit, RBAC archive:write).

Budget tracker (services/agentBudgetService.ts:122): watchdog wallclock/token/context/loop-detection/per-skill-timeout/per-user-concurrency (CHAT_MAX_CONCURRENT_PER_USER). AbortReason: none|wallclock|token_budget|context_overflow|loop_detected|aborted|done|unknown_tool_breaker. truncateContextToByteBudget conserva sysMsg + pinned user msg + last N.

Selección de modelo + fallback: prioridad per-chat → workspace default → global default → ENV (providerService.resolveProviderConfig). deriveCapabilities etiqueta local-only/fastest/smartest/reasoning. Polling 30s refreshModels → Provider.isAvailable. Frontend fallback 3-tier en useChat.ts con toast Undo. Servidor fallback buildFallbackConfig (orchestrator.ts:879). SSE done incluye modelUsed, providerUsed.

Extender: nueva skill → registerSkill({...}) en builtinSkills.ts. Modelo per-chat → PATCH /workspaces/:ws/chats/:chatId/model. Plan mode → activa WorkspaceAgentConfig.planMode. Tuning de budget → env AGENT_* (ver 03 - Configuration).

Chat streaming (SSE)

POST /api/workspaces/:id/chat/stream → SSE. Eventos: token, status, citations ({ sources }), done ({ chatId, messageId, modelUsed, providerUsed, mcpSources }), error, plan, wiki_edit. Frontend: useChat.ts (@microsoft/fetch-event-source), abort vía AbortController. Slash /model, paleta Cmd+K, comparación Cmd+Shift+M. Persistencia del modelo efectivo vía loadChat PATCH (stale default skipped, ver frontend/CLAUDE.md).

Documentos, ingestión, OCR

Ingestión (collector/routes/ingest.ts:219): multer (100 MB, allowlist MIME) → parseFile → chunkText → embed → vectorStore.addDocuments → notifyServerStatus (PUT /api/documents/:id/status con X-Collector-Secret). La respuesta incluye chunks[].chunkText para la FTS del lado servidor.

Parser (collector/services/parser.ts): PDF (pdf-parse, <100 chars → OCR skip D-04, vision es server-side), DOCX (officeparser → fallback mammoth), PPTX (officeparser), XLSX (node-xlsx), TXT/MD/CSV (UTF-8), YouTube (youtube-transcript-plus, 422 si no hay transcript). ocrMode (auto/vision/skip) en IngestUploadBodySchema.

OCR vision (server-side) (services/ragOcrService.ts:59): pdfjs-dist carga el PDF (cap 50 páginas) → render PNG → modelo vision Ollama → strip grounding tags → concatenación. Job service ocrJobService.ts con workflow approve/reject, qualityScore, credibilityScore. Routes routes/ocr.ts. Frontend SettingsOcr (catálogo de modelos, job list, preview, custom instructions).

DLP filter (services/dlpFilter.ts): scanContent(text) → {hasMatch, matches, redactedText}. Desde la v0.22 los patrones son 10 built-in database-backed (tabla dlp_patterns, hasta 50 patrones custom con live test preview): además de los 6 originales (email, credit_card, ssn, api_key sk-..., aws_key AKIA..., private_key PEM) se añadieron 4 patrones EU/IT - it_vat_iva (Partita IVA label-anchored), it_codice_fiscale (16 caracteres, uppercase-only), iban (prefijo de país + longitud ≥ 15, ReDoS-safe), eu_phone (teléfono IT/EU, seedeado DISABLED para revisión admin). Redaction [REDACTED]. Gate DLP_ENABLED (default `"true"` en CONFIG_DEFAULTS, permissions.ts:173); setting DLP_BYPASS_ROLES (array JSON de roles, default []) exime a los roles seleccionados de cada scan/redaction con audit event dlp.bypassed. Toggle admin "Show/Hide DLP texts" para revelar/re-ocultar los textos redactados (solo admin, session-scoped). Cada audit event dlp.* etiqueta la fuente (source: "chat" | "widget") derivada del header X-Widget-Id, con filtros en el panel DLP Match History. Fallback al set built-in si la DB es inalcanzable (cache TTL 5 min); el streaming flush token-by-token mantiene los patrones built-in. Integrado en routes/chat.ts (entrada + salida); SSE done → dlp_warning: true; el widget muestra PIIWarningPrompt.

Archivos, Wiki, Synthesis

Archive: knowledge base multipágina workspace-scoped, páginas markdown con path slug y wikilink [[Title]]. 14 servicios archive (archivePageService, archiveGraphService, archiveIndexService, archiveBacklinkService, ...). Routes archive*.ts (20+ archivos): CRUD, pages, config, search (hybrid RRF), graph, export, index, import (copy-from-doc), schema templates.

Wiki embedding (services/wikiEmbeddingService.ts:8): indexWikiPage → chunk 800/100 → collector POST /api/ingest/wiki-pages (tabla wiki_pages, workspaceId = archive:<id>). Skill wiki_query (FTS + BFS wikilinks) y wiki_write (preview/dry-run + wiki_edit SSE).

Synthesis (services/synthesisService.ts): pipeline 5-pass: entity_extraction → summary_generation → candidate_search (BM25 tsvector) → llm_decision (CREATE/UPDATE/SKIP/FLAG_CONTRADICTION) → write_overview. PHI gate: si ArchiveConfig.localLLMOnly=true y provider no-ollama → abort pre-egress. Workflow admin: trigger → preview → approve/reject (routes/synthesis.ts).

Backup

Scheduler Bree (services/backupSchedulerService.ts): @mintplex-labs/bree, closeWorkerAfterMs:0/timeout:0 (long-running). bootstrapJobs al arranque (cleanStaleLocks → carga los BackupJob enabled). Mutex DB-level vía BackupLog.status="running"; cleanStaleLocks → running >2h = failed. Cron helper buildCronExpression(frequency, time, ...).

Pipeline (services/backupService.ts:282): dumpDatabase (pg_dump spawn no-shell) → collectFiles (storage dirs, path-traversal guard) → createArchive (archiver zlib level 9) → computeChecksum (SHA-256 streaming) → uploadWithRetry (3 retry backoff) → sendFailureNotification (SMTP). Safety backup pre-restore (runSafetyBackup).

Encryption (services/encryptionService.ts): AES-256-GCM. Key: ENCRYPTION_KEY (base64 32 byte) o scryptSync(JWT_SECRET, salt). Decrypt chain rotation-safe: [current, ...LEGACY_PREVIOUS_ENCRYPTION_KEYS, scryptLegacy?].

Destinos (services/backup/providers/): LocalProvider, S3Provider, S3CompatibleProvider, DropboxProvider, GoogleDriveProvider, FtpProvider, SftpProvider, EmailProvider (interfaz IBackupDestinationProvider). License backup_enabled + max_backup_destinations (Community 1, Enterprise ∞).

Restore (backupService.ts:617): restoreDatabaseWithPsql (psql --single-transaction --set ON_ERROR_STOP=on, atomicidad all-or-nothing). Routes restore.ts + POST /api/backups/trigger (manual).

Retention (backupRetentionService.ts): per-job retentionDays (default 30); cleanup BackupLog success más allá del cutoff + hard delete. Los errores individuales no bloquean.

Push (VAPID Web Push)

routes/push.ts: initVapid lee las env VAPID_* (auto-generadas en dev). GET /vapid-key (público), POST/DELETE /subscribe (license push_notifications), POST /test (admin). sendPushNotification(title, body) export para event-trigger; elimina subscription 410 Gone. Frontend: service worker API.

Webhook

services/webhookService.ts: dispatchWebhookEvent(event, payload) fire-and-forget paralelo. deliverWebhook: body {event, timestamp, data}, header X-Webhook-Signature: sha256=HMAC-SHA256(secret, body). Retry 3 (1s/2s/4s); 10 fallos consecutivos → auto-disable. Eventos: document.processed, chat.completed, user.created, workspace.updated, mcp.installed/uninstalled, ... Disparados por eventLogService.logEvent (async post-DB). Routes webhooks.ts (license webhooks).