# DocSummarizer Part 5 - lucidRAG: MultiM SK3Document RAG Web Application

<!--category-- AI, LLM, RAG, C#, HTMX, GraphRAG, Semantic Search, DuckDB -->
<datetime class="hidden">2026-01-01T18:00</datetime>

Questo è **Parte 5** della serie DocSummarizer, ed è anche la culminazione del [Serie GraphRAG](/blog/graphrag-minimum-viable-implementation) e [Serie di ricerche semantiche](/blog/semantic-search-with-onnx-and-qdrant). Noi' stiamo combinando tutto in un'applicazione web deployabile.

> 🚨🚨 ARTICLE DI PREVIEW 🚨🚨 Stiamo ancora lavorando su alcuni kinks e aggiungendo delle caratteristicheM SK2 Ma il cuore è finito e funziona bene. Ci aspettiamo delle nuove versioni nei prossimi settimaneMSC4 Sarà al lucidRAGMST5comMst6 IoMSt7 Aggiungerò screenshot qui una volta che ho tirato fuori il designMSST8

> **L'intero obiettivo di costruire infrastrutture RAG è usarle per qualcosa di reale.**

Nelle ultime settimane abbiamo costruito

- **DocSummarizer** - Percezione del documento, blocco semanticoM SK2 Inserzioni ONNX
- **GraphRAG** - Extrazione delle entitàM SK1 grafici di conoscenza, rilevamento della comunità
- **La ricerca semantica** - BMM SK1 + Ricerca ibrida BERT con fusione RRF

Ora li mettiamo insieme. **lucidRAG** - un'applicazione web standalone per la risposta a domande multi-documenti con il grafico della conoscenza.

**Il sito web:** [lucidrag.com](https://lucidrag.com) | **Source:** [GitHub](https://github.com/scottgal/mostlylucidweb/tree/main/Mostlylucid.RagDocuments)

[TOC]

## Cosa fa lucidRAG?

Lavorare i documenti. Chiedere domande. Ricevere risposte con citazioni e un grafico della conoscenza che mostra come i concetti si connettono.

**Principali aspetti:**

- **La carica di un documento multi-** con il drag-and-drop
- **Agentic RAG** - Decomposizione della domanda e auto-determinazioneM SK1Corrigione dipendente, determinista nella strutturaMSC4 singolo ciclo di vita della richiestaMST5
- **Visualizzazione del grafico della conoscenza** - Vedete le relazioni delle entità
- **Il punto di vista delle prove** - Sentenza-citatione di sorgenti a livello
- **Dispiegamento autonomo** - Single executable or Docker

**Contrattivi di design:**

- Nessuna dipendenza dalla nuvola per l'indexazione.
- Preprocesso deterministico (chunking
- Lo stato del vettore ricostruibile dai documenti sorgenti
- LLM usati *Solo* per la sintesi delle risposte sulle prove rilevate.

> In nessun momento i LLM sono usati per un blocco, embedding, extrazione di entitàM SK2 o immagazzinamento - solo per sintetizzare le risposte su quelle che vengono ricavateMSC4 citazioniMST5 prove supportateMst6

## Perché combinare la ricerca dei vettori + Grafi di conoscenza?

La ricerca a vector solo si rompe per alcuni tipi di query:

| Tipo di domanda | Problema di ricerca dei vectori | | | Risolto grafico ||
|------------|----------------------|----------------|
| CrossM SK1documento | SSK3Come X si riferisce a Y?"
| EntitàM SK1centrica | "Che ne dite del Docker?" \| Tracce grafiche dall'entità SSK6
| Sommari globali | "Tema principaleM SK3 | | | Detezione nella comunità ♫|

lucidRAG usa entrambi i vektori: per la precisione, grafici per il contestoM SK2 Le domande di grafico sono profonditàMSC3limitata MST4max MST5 hopsM ST6 e scopata per i documenti recuperati per prevenire il traversamento senza limiti su grandi corporaMSST7

## Architecture Overview

L'app stratifica tre progetti che abbiamo già costruito, ' , orchestrati tramite **[StyloFlow](/blog/styloflow-signal-driven-workflows)** - un segnale - motore di flusso di lavoro guidato:

```
lucidRAG
├── Controllers/Api/    # REST endpoints
├── Services/           # Business logic
│   ├── DocumentProcessingService   # Wraps DocSummarizer
│   ├── EntityGraphService          # Wraps GraphRAG
│   └── Background/                 # Async queue processing (StyloFlow waves)
└── Views/              # HTMX + Alpine.js UI
```

**Perché StyloFlow?** Invece di pipelines codificati, ogni fase di processo è una M SK1onda" che emette segnaliMSC3 Le onde funzionano quando le condizioni di attenuazione corrispondono a quelle di un'altra , permettendo l'esecuzione in parallelo ♫( inserendo ♫ + estrazione delle entità ♫ [StyloFlow: Signal-Orchestrazione del flusso di lavoro guidato](/blog/styloflow-signal-driven-workflows) per i dettagli di applicazione.

## Il tubo di trattamento

Quando si carica un documento, si passa attraverso tre fasi:

### Livello 1: La carica e la fila

Il punto finale dell'upload valida il file, calcola un hash del contenuto per la deduplicazione, e lo rinchiusi per il processo di fondoM SK2

```csharp
public async Task<Guid> QueueDocumentAsync(Stream fileStream, string fileName)
{
    // Compute hash to detect duplicates
    var contentHash = ComputeHash(fileStream);

    var existing = await _db.Documents
        .FirstOrDefaultAsync(d => d.ContentHash == contentHash);
    if (existing != null)
        return existing.Id; // Already processed
```

L'intuizione chiave: lo scartamo prima, lo risparmiamo più tardiM SK2 Questo impedisce di sprecare il tempo di processamento per i ricavi duplicatiMSC3

```csharp
    // Save to disk, create DB record
    var docId = Guid.NewGuid();
    await SaveFileToDiskAsync(fileStream, docId, fileName);

    // Queue for background processing
    await _queue.EnqueueAsync(new DocumentProcessingJob(docId, filePath));

    return docId;
}
```

### Livello 2: Cambiamento e inserzione

Il processore di fondo raccoglie i documenti in fila e li fa passare attraverso DocSummarizer:

```csharp
var result = await _summarizer.SummarizeFileAsync(job.FilePath, progressChannel);
```

Questa singola linea fa molto lavoro (see [DocSummarizer Part 1](/blog/building-a-document-summarizer-with-rag)):

- Parlare la struttura del documento (PDF, DOCXM SK2 Markdown )
- Dividere in blocchi semantici rispetto alle sedi.
- Generano inserzioni ONNX per ogni frammento.
- Gestire i vettori in DuckDB con l'indexazione HNSW

### Stage 3: Extrazione delle entità

Dopo la frammentazione, estragiamo le entità usando l'approccio heuristico di GraphRAG'

```csharp
var segments = await _vectorStore.GetDocumentSegmentsAsync(documentId);
var entityResult = await _entityGraph.ExtractAndStoreEntitiesAsync(documentId, segments);
```

Si usa il punteggio IDF e i segnali strutturali piuttosto che per le chiamate -chunk LLM - vedete. [GraphRAG Part 2](/blog/graphrag-minimum-viable-implementation) per i dettagli.

## I canali confinati per la pressione posteriore

Un'implementazione naiva avrebbe usato quei filamenti senza limiti, rischiando di uscire-diM SK2cassamenti della memoria durante le inondazioni di caricamento . Usiamo canali confinati con limiti di capacità esplicitiMSC4

```csharp
private readonly Channel<DocumentProcessingJob> _queue =
    Channel.CreateBounded<DocumentProcessingJob>(new BoundedChannelOptions(100)
    {
        FullMode = BoundedChannelFullMode.Wait
    });
```

Quando la fila si riempie, `Wait` il modo blocca i nuovi scritti finché non si apre lo spazio. aggiungiamo un'interruzione di tempo così che gli utenti ricevono un errore chiaro invece di appendere:

```csharp
using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
timeoutCts.CancelAfter(TimeSpan.FromMinutes(5));

try {
    await _queue.Writer.WriteAsync(job, timeoutCts.Token);
} catch (OperationCanceledException) when (!ct.IsCancellationRequested) {
    throw new InvalidOperationException("Queue full. Try again later.");
}
```

## Per-Temperature di Documentazione

I grandi documenti possono impiegare pochi minuti per essere processati. Ma un documento bloccato non dovrebbe' bloccare l'intera filaM SK2 Ogni documento ha la propria interruzione di tempo.

```csharp
while (!stoppingToken.IsCancellationRequested)
{
    var job = await _queue.DequeueAsync(stoppingToken);

    // 30-minute timeout per document
    using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(stoppingToken);
    timeoutCts.CancelAfter(TimeSpan.FromMinutes(30));

    try {
        await ProcessDocumentAsync(job, timeoutCts.Token);
    } catch (OperationCanceledException) when (!stoppingToken.IsCancellationRequested) {
        await MarkDocumentFailedAsync(job.DocumentId, "Processing timed out");
    }
}
```

Il token collegato ci assicura di continuare a rispettare la fine dell'applicazione aggiungendo il limite per-document.

## Il ripulimento del canale Progress

Ogni documento di processazione riceve un canale di progresso per le SSE aggiornazioni. Ma se l'utente chiude il suo browser a metàM SK1upload, quel canale diventa orfano . Seguiamo i tempi della creazione e puliamo periodicamenteMSC4

```csharp
private readonly ConcurrentDictionary<Guid, ProgressChannelEntry> _progressChannels = new();

public int CleanupAbandonedChannels()
{
    var cutoff = DateTimeOffset.UtcNow - TimeSpan.FromHours(1);
    var cleaned = 0;

    foreach (var kvp in _progressChannels.Where(x => x.Value.CreatedAt < cutoff))
    {
        if (_progressChannels.TryRemove(kvp.Key, out var entry))
        {
            entry.Channel.Writer.TryComplete();
            cleaned++;
        }
    }
    return cleaned;
}
```

A. `PeriodicTimer` lo chiama ogni 15 minuto nel processore di fondo

## Storage: DuckDB + PostgreSQLM SK2SQLite

Usiamo due database per scopi diversi:

**PostgreSQL/SQLite (EF CoreM SK2** archivia i metadati del documento - ciò che esiste, lo stato di processoM SK2 le relazioni . Questi dati sono durabili e rintracciabiliMSC4

**DuckDB** conserva i vettori e il grafico dell'entità. E' l'efemera M SK2 lo si può ricostruire dai documenti di sorgente . Questa separazione significa che la corruzione del magazzino dei vettori non uccide l'inventario dei documentiMSC5

```csharp
// Metadata in PostgreSQL
public class DocumentEntity
{
    public Guid Id { get; set; }
    public string Name { get; set; }
    public string ContentHash { get; set; }
    public DocumentStatus Status { get; set; }
}

// Vectors in DuckDB (managed by DocSummarizer)
// Entities in DuckDB (managed by GraphRAG)
```

## L'API del Chat

Le domande passano attraverso la catena di ricerca agentica:

```csharp
[HttpPost]
public async Task<IActionResult> ChatAsync([FromBody] ChatRequest request)
{
    // 1. Get or create conversation for memory
    var conversation = await GetOrCreateConversationAsync(request.ConversationId);

    // 2. Search with hybrid retrieval
    var searchResult = await _search.SearchAsync(request.Query, new SearchOptions
    {
        TopK = 10,
        IncludeGraphData = request.IncludeGraphData
    });
```

Il servizio di ricerca gestisce la decomposizione della domanda se necessario, poi sintetizza una risposta:

```csharp
    // 3. Generate answer with LLM
    var answer = await _summarizer.SummarizeAsync(
        request.Query,
        searchResult.Segments,
        new SummarizeOptions { IncludeCitations = true });

    // 4. Save to conversation history
    await SaveToConversationAsync(conversation.Id, request.Query, answer);

    return Ok(new ChatResponse
    {
        Answer = answer.Text,
        Sources = answer.Citations,
        GraphData = searchResult.GraphData
    });
}
```

## L'UI: HTMX + AlpineM SK2js

L'interfaccia è una singola pagina con i documenti sulla sinistra, chat sulla destra:

```
┌──────────────────┬─────────────────────────────────────┐
│  📁 Documents    │  💬 Chat                            │
│  ─────────────   │  [Answer] [Evidence] [Graph]       │
│  [+ Upload]      │                                     │
│  📄 api-docs.pdf │  Q: How does auth work?            │
│  📝 readme.md    │  A: JWT tokens stored... [1][2]    │
│  ─────────────   │                                     │
│  🕸️ Graph: 168   │  ┌─────────────────────────────┐   │
│                  │  │ Ask about your documents... │   │
└──────────────────┴──┴─────────────────────────────┴───┘
```

Alpine.js gestisce lo stato; HTMX gestisce l'update della lista dei documentiM SK2

```javascript
function ragApp() {
    return {
        messages: [],
        isTyping: false,

        async sendMessage() {
            const query = this.currentMessage.trim();
            this.messages.push({ role: 'user', content: query });
            this.isTyping = true;

            const result = await fetch('/api/chat', {
                method: 'POST',
                body: JSON.stringify({ query })
            }).then(r => r.json());

            this.messages.push({
                role: 'assistant',
                content: result.answer,
                sources: result.sources
            });
            this.isTyping = false;
        }
    };
}
```

## Il modo di dimostrazione

Per i deploimenti pubblici come lucidrag.com, il modo di dimostrazione disabilita l'upload e usa preM SK2 contenuti caricatiMSC3 Il modo di demo esiste per rendere le deploiezioni pubbliche sicure , deterministiceMST5 ed economiche senza codici speciali

```csharp
public class DemoModeConfig
{
    public bool Enabled { get; set; } = false;
    public string ContentPath { get; set; } = "./demo-content";
    public string BannerMessage { get; set; } = "Demo Mode: Pre-loaded RAG articles";
}
```

A. `DemoContentSeeder` Il servizio di sfondo guarda il directorio del contenuto e processa tutti i file abbandonati:

```csharp
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
    if (!_config.DemoMode.Enabled) return;

    await SeedExistingContentAsync();
    StartFileWatcher(_config.DemoMode.ContentPath);
}
```

Questo vi permette di aggiornare il contenuto della dimostrazione semplicemente copiando i file - non è necessario ricominciare

## Il lucidRAG in funzione

### Standalone (No Dependencies)

```bash
dotnet run --project Mostlylucid.RagDocuments -- --standalone
```

Usa SQLite + DuckDB locally. Open `http://localhost:5080`.

### Docker

```yaml
services:
  lucidrag:
    build: .
    ports: ["5080:8080"]
    depends_on: [postgres, ollama]
```

## Cosa funziona davvero?

| Componente
|-----------|--------|---------|
| Percezione del documento | DocSummarizer | PDFM SK3 DOCX, Markdown |
| Inserzioni ONNX | DocSummarizer
| Extrazione delle entità | | | GraphRAG || | IDF | МSK3 | Signali strutturali |
| Ricerca ibrida | Entrambi SSK2 BMM SK3 + BERT con RRF S|
| Trattamento asynco | Nuovi | Canali confinati, Interruzioni temporali ||
| Interfaccia web | Nuovo | HTMX M+ Alpine.js P|

### Costo

**Zero Costi di API** per l'indexazione - gli inserimenti sono ONNXM SK1 le entità sono heuristice. pagate solo la sintesi LLM al momento della domandaMSC3 e questo funziona con il locale Ollama

## Article connessi

- [DocSummarizer Part 1 - Architecture](/blog/building-a-document-summarizer-with-rag)
- [DocSummarizer Part 4 - Pipeline RAG](/blog/docsummarizer-rag-pipeline)
- [GraphRAG Part 2 - Implementazione](/blog/graphrag-minimum-viable-implementation)
- [La ricerca semantica con ONNX](/blog/semantic-search-with-onnx-and-qdrant)