Skip to content

RAG Pipeline

CaseSolo's retrieval-augmented generation pipeline stores firm-scoped source chunks in PostgreSQL pgvector and converts retrieval hits into citation-ready SourceSpan records for AI outputs.

Core Flow

  1. Source text comes from a Document or Note.
  2. chunkText() creates token-bounded, overlapping chunks with original character offsets.
  3. embedTexts() generates embeddings through the configured provider.
  4. writeChunks() replaces stale chunks for the source and writes fresh rows to document_chunks.
  5. semanticSearch() embeds a query and performs firm-scoped pgvector cosine search.
  6. retrieveCitations() maps search hits into source spans that can be attached to AI envelopes and persisted on AgentRun.sourceSpans.

Important Files

Namespace And Tenant Safety

Every chunk row stores firmId, optional caseId, sourceType, and source ID. Retrieval requires firmId, and optional case/source filters are applied in fixed SQL templates rather than dynamically interpolated SQL. buildRagNamespace() produces deterministic keys for firm-level and case-level telemetry and future cache keys.

Vector Dimension Policy

The database column is currently document_chunks.embedding vector(1536). assertRagVectorCompatibility() fails fast when the active embedding provider emits a different dimension. This prevents a 1024-dimension provider configuration from reaching the database and producing harder-to-debug pgvector errors.

If the team intentionally switches to a 1024-dimension provider, ship a migration that changes the vector column and HNSW index, then re-ingest all chunks.

Job Path

Use enqueueRagIngestion() for explicit RAG ingestion jobs:

await enqueueRagIngestion({
  sourceType: 'DOCUMENT',
  sourceId: documentId,
  firmId,
  caseId,
  requestedBy: userId,
})

When REDIS_URL is not configured, the enqueue helper processes synchronously. When jobs are configured, it writes to the BullMQ-compatible casesolo-rag-ingestion queue with deterministic job IDs.

The document-processing worker still runs OCR first and then calls ingestDocument() directly so uploaded files become searchable without requiring a second queue hop.

Verification

Run the focused RAG suite:

npm run test:run -- rag-pipeline rag-ingestion ingestion retrieval citation

Run targeted lint and typecheck:

npx eslint src/lib/ai/rag-pipeline.ts src/lib/ai/ingestion.ts src/lib/ai/retrieval.ts src/lib/jobs/processors/rag-ingestion.ts src/lib/jobs/queue.ts src/lib/jobs/workers.ts src/__tests__/lib/rag-pipeline.test.ts src/__tests__/lib/rag-ingestion.test.ts src/__tests__/lib/ingestion.test.ts src/__tests__/lib/retrieval.test.ts
npm run typecheck

Source: docs/dev/rag-pipeline.md