Adicionar diagnósticos melhores
Exibe estado de modelo, progresso de indexação, contagens de arquivo e chunk e números reais de latência do profiler QVAC.
Onde eles pertencem: src/qvac.ts para estado do modelo e a
ponte do profiler, src/server.ts para exposição, src/ui/app.js
para exibição.
O LocalLens esconde a maior parte do ciclo de vida dele do usuário. Tudo bem quando tudo é rápido. É chato quando um modelo demora um minuto para carregar e a UI não mostra nada. Uma superfície pequena de diagnóstico resolve isso sem complicar o core.
Dois tipos de sinal valem ser mostrados:
- Estado operacional — o que é: loading vs loaded, contagens de arquivo e chunk, o modelo de chat ativo. O gateway já sabe isso; você só precisa expor.
- Métricas de performance — quão rápido: tempos de load de modelo,
latência de embedding, timings de busca e ingest RAG, throughput
de completion. O QVAC SDK entrega um utilitário
profilerque captura tudo isso sem nenhum código de timing do seu lado.
Os dois são complementares. Estado operacional te diz o que está acontecendo agora. O profiler te diz quanto tempo as coisas levaram.
O que expor
- Estado de carregamento do modelo. Loading / loaded / failed, mais quais modelos.
- Progresso de indexação. Bytes lidos, chunks criados, arquivo atual.
- Estatísticas do brain. Contagem de arquivos, contagem de chunks, nome do modelo de embedding.
- Modelo ativo. Qual modelo de chat está em uso agora (o 1.7B ou o fallback 600M).
- Agregados de latência. Último timing e média para
loadModel,embed,completion,ragIngest,ragSearch.
Os quatro primeiros vêm do gateway. O quinto vem do profiler.
A receita
1. Adiciona um getter de status ao gateway
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 …
}
}O getter de status é read-only e sem side-effects, então é seguro chamar do endpoint de diagnóstico sem tocar o ciclo de vida do modelo.
2. Liga o profiler QVAC
@qvac/sdk entrega um utilitário profiler que grava timing entre
loads de modelo, completions, embeddings e operações de RAG. Liga
uma vez no boot e deixa capturar toda chamada que o gateway faz:
import { profiler } from "@qvac/sdk";
// near the top of the file, alongside chatModelConfig
profiler.enable();profiler.enable() é um switch global. Toda chamada SDK seguinte
grava eventos. Se você preferir opt-in por chamada, toda função QVAC
também aceita { profiling: { enabled: true } } nas
opções, e você pode dar opt-out seletivo com
{ profiling: { enabled: false } }.
Três formatos de export estão disponíveis:
| Método | Retorna | Melhor para |
|---|---|---|
profiler.exportSummary() | String de resumo de alto nível | Logging no shutdown. |
profiler.exportTable() | Tabela agregada detalhada | Leitura num terminal. |
profiler.exportJSON() | { aggregates, recentEvents, config } | Servir por HTTP para a UI. |
JSON é o que o endpoint de diagnóstico deve retornar. A UI pode formatar do jeito que quiser.
Adiciona um helper minúsculo no gateway para os chamadores não terem que importar
profiler direto:
metrics() {
return profiler.exportJSON();
}Profiler não mede tudo
O profiler captura latência para chamadas SDK. Não mede progresso de indexação direto, contagens de arquivo ou chunk, ou qual modelo de chat está ativo no momento. Esses vêm do estado próprio do gateway, e é por isso que essa página mantém as duas superfícies. O profiler complementa o getter de status — não substitui.
3. Adiciona uma rota /api/diagnostics
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 é privado. Ou expõe diagnostics() e
metrics() em LocalLensApp que fazem proxy, ou torna o
gateway protected. Proxiar é mais limpo — mantém LocalLensApp como
a única coisa que o servidor conversa:
diagnostics() {
return this.qvac.status();
}
metrics() {
return this.qvac.metrics();
}4. Progresso de indexação
A indexação hoje roda como uma única chamada async. Para fazer stream do progresso, troca o workflow para um generator:
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;
}O servidor então expõe como 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" } });
}A UI consome o stream SSE e atualiza uma barra de progresso.
5. Renderiza
Três funções de render novas em 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ém o painel colapsável. A maior parte dos usuários não quer ele aberto por default.
Diagnósticos são read-only
Nenhum desses endpoints deve mudar estado. Se uma feature futura precisa reiniciar um modelo ou despejar um brain, são rotas separadas. Diagnóstico mostra o que está acontecendo. Não intervém.
O que você não precisa mudar
domain.ts— tipos continuam iguais.rag.ts— chunking e prompts inalterados.store.ts— o formato JSON está inalterado. O endpoint de diagnóstico lê o que já está lá.
Referências externas
- Referência do profiler QVAC —
API completa incluindo
enable,disable,exportSummary,exportTable,exportJSONe a opçãoprofilingpor chamada.