LocalLens
Extender

Agregar mejores diagnósticos

Aflora el estado de los modelos, el progreso de indexación, las cuentas de archivos y chunks, y números reales de latencia del profiler de QVAC.

Dónde pertenece: src/qvac.ts para el estado del modelo y el puente con el profiler, src/server.ts para la exposición, src/ui/app.js para el display.

LocalLens esconde la mayor parte de su lifecycle del usuario. Eso está bien cuando todo es rápido. Es molesto cuando un modelo tarda un minuto en cargar y la UI no muestra nada. Una superficie de diagnósticos pequeña arregla esto sin sobrecomplicar el core.

Dos tipos de señales valen la pena mostrar:

  • Estado operacional — qué está: cargando vs cargado, cuentas de archivos y chunks, el modelo de chat activo. El gateway ya conoce esto; solo hay que exponerlo.
  • Métricas de performance — qué tan rápido: tiempos de carga de modelo, latencia de embedding, timings de búsqueda e ingestión de RAG, throughput de completion. El SDK de QVAC trae una utilidad profiler que captura todo esto sin que tengas que escribir código de timing.

Los dos son complementarios. El estado operacional te dice qué está pasando ahora. El profiler te dice cuánto tardaron las cosas.

Qué exponer

  1. Estado de carga del modelo. Loading / loaded / failed, más qué modelos.
  2. Progreso de indexación. Bytes leídos, chunks creados, archivo actual.
  3. Stats del cerebro. Cuenta de archivos, cuenta de chunks, nombre del modelo de embedding.
  4. Modelo activo. Qué modelo de chat está en uso ahora mismo (el de 1.7B o el fallback de 600M).
  5. Agregados de latencia. Último timing y promedio para loadModel, embed, completion, ragIngest, ragSearch.

Los primeros cuatro vienen del gateway. El quinto viene del profiler.

La receta

1. Agregar un getter de status en el gateway

src/qvac.ts
type ModelState = "unloaded" | "loading" | "loaded" | "error";

export class QvacGateway {
  // existing fields
  private state: { chat: ModelState; embedding: ModelState } = {
    chat: "unloaded",
    embedding: "unloaded",
  };

  status() {
    return {
      chat: { state: this.state.chat, modelId: this.chatModelId },
      embedding: { state: this.state.embedding, modelId: this.embeddingModelId },
    };
  }

  // wrap loadModel calls to update state
  private async loadModels(): Promise<void> {
    this.state.embedding = "loading";
    try {
      this.embeddingModelId ??= await loadModel({ modelSrc: GTE_LARGE_FP16 });
      this.state.embedding = "loaded";
    } catch (e) {
      this.state.embedding = "error";
      throw e;
    }
    // … same pattern for chat …
  }
}

El getter de status es read-only y sin efectos secundarios, así que es seguro llamarlo desde el endpoint de diagnósticos sin tocar el lifecycle del modelo.

2. Encender el profiler de QVAC

@qvac/sdk trae una utilidad profiler que registra timing en cargas de modelos, completions, embeddings y operaciones RAG. Actívalo una vez en el boot y déjalo capturar cada llamada que hace el gateway:

src/qvac.ts
import { profiler } from "@qvac/sdk";

// near the top of the file, alongside chatModelConfig
profiler.enable();

profiler.enable() es un switch global. Cada llamada siguiente al SDK registra eventos. Si prefieres opt-in por llamada, cada función de QVAC también acepta { profiling: { enabled: true } } en sus options, y puedes hacer opt-out selectivo con { profiling: { enabled: false } }.

Tres formas de export están disponibles:

MétodoDevuelveMejor para
profiler.exportSummary()String de resumen de alto nivelLogging en shutdown.
profiler.exportTable()Tabla agregada detalladaLectura en una terminal.
profiler.exportJSON(){ aggregates, recentEvents, config }Servir por HTTP para la UI.

JSON es lo que el endpoint de diagnósticos debería devolver. La UI puede darle el formato que quiera.

Agrega un helper chico en el gateway así los callers no tienen que importar profiler directamente:

src/qvac.ts
metrics() {
  return profiler.exportJSON();
}

El profiler no mide todo

El profiler captura latencia para llamadas al SDK. No mide directamente el progreso de indexación, las cuentas de archivos o chunks, ni qué modelo de chat está actualmente activo. Esos vienen del estado propio del gateway, que es por qué esta página mantiene ambas superficies. El profiler complementa al getter de status — no lo reemplaza.

3. Agregar una ruta /api/diagnostics

src/server.ts
if (url.pathname === "/api/diagnostics" && request.method === "GET") {
  return json({
    qvac: app.diagnostics(),
    metrics: app.metrics(),
    brains: (await app.listBrains()).map((b) => ({
      id: b.id,
      name: b.name,
      status: b.status,
      fileCount: b.fileCount,
      chunkCount: b.chunkCount,
      lastIndexedAt: b.lastIndexedAt,
      lastError: b.lastError,
    })),
  });
}

app.qvac es privado. O expones diagnostics() y metrics() en LocalLensApp que hagan proxy, o haces el gateway protected. Hacer proxy es más limpio — mantén a LocalLensApp como lo único con lo que el servidor habla:

src/locallens.ts
diagnostics() {
  return this.qvac.status();
}

metrics() {
  return this.qvac.metrics();
}

4. Progreso de indexación

La indexación actualmente corre como una sola llamada async. Para hacer stream del progreso, cambia el workflow a un generator:

src/locallens.ts
async *createBrainFromFolderProgress(input: CreateBrainFromFolderInput): AsyncGenerator<ProgressEvent, Brain> {
  yield { type: "discovery", message: "Walking folder…" };
  const documents = await discoverTextDocuments(folderPath);
  yield { type: "discovered", fileCount: documents.length };

  yield { type: "chunking" };
  const chunks = await chunkDocuments(documents, { brainId: brain.id });
  yield { type: "chunked", chunkCount: chunks.length };

  yield { type: "ingesting" };
  await this.qvac.ingestChunks(brain.workspace, chunks);
  yield { type: "ingested" };

  return indexed;
}

El servidor lo expone después como un stream de Server-Sent Events:

if (url.pathname === "/api/brains/from-files/stream") {
  const stream = new ReadableStream({ /* yield events */ });
  return new Response(stream, { headers: { "content-type": "text/event-stream" } });
}

La UI consume el stream SSE y actualiza una barra de progreso.

5. Renderizarlo

Tres funciones de render nuevas en app.js:

function renderDiagnostics(diagnostics) {
  // model state badge in the topbar
  // brain table in a side panel
}

function renderMetrics(metrics) {
  // "Last search: 230 ms · avg 280 ms"
  // "Embedding model loaded in 4.2s"
  // — pulled from metrics.aggregates and metrics.recentEvents
}

function renderIndexingProgress(event) {
  // progress bar updates from SSE events
}

Mantén el panel colapsable. La mayoría de los usuarios no lo quieren abierto por default.

Los diagnósticos son read-only

Ninguno de estos endpoints debería cambiar estado. Si una feature futura necesita reiniciar un modelo o evictar un cerebro, esas son rutas separadas. Los diagnósticos muestran qué está pasando. No intervienen.

Lo que no necesitas cambiar

  • domain.ts — los tipos se quedan igual.
  • rag.ts — el chunking y los prompts no se ven afectados.
  • store.ts — la forma del JSON no cambia. El endpoint de diagnósticos lee lo que ya está ahí.

Referencias externas

On this page