wip: snapshot das alterações não commitadas (2026-10-20)

Origem: D:/Documents/Github/LocalAiApp
Arquivos omitidos (segredos, bancos de dados, logs, >50 MB): 0
This commit is contained in:
evandro committed 2026-10-20 00:48:09 -03:00
1 parent ac215b4229
commit e08189f640
17 files changed
+2914 -13236

No files matched your search

+190
View File
@@ -0,0 +1,190 @@
/**
* webSearchLogger.ts
* Centralized structured logger for the web search pipeline on the client side.
* Outputs consistently formatted, emoji-tagged logs to the React Native / Metro console.
*/
type LogLevel = 'INFO' | 'WARN' | 'ERROR' | 'DEBUG';
const ICONS: Record<LogLevel, string> = {
INFO: 'ℹ️ ',
WARN: '⚠️ ',
ERROR: '❌',
DEBUG: '🔍',
};
function formatMeta(meta?: Record<string, any>): string {
if (!meta) return '';
try {
return '\n ' + JSON.stringify(meta, null, 2).replace(/\n/g, '\n ');
} catch {
return '';
}
}
function log(level: LogLevel, phase: string, message: string, meta?: Record<string, any>) {
const ts = new Date().toISOString();
const metaStr = formatMeta(meta);
const prefix = `[${ts}] ${ICONS[level]} [WebSearch/${phase}]`;
const fullMsg = `${prefix} ${message}${metaStr}`;
switch (level) {
case 'ERROR':
console.error(fullMsg);
break;
case 'WARN':
console.warn(fullMsg);
break;
default:
console.log(fullMsg);
}
}
function logSection(title: string) {
const border = '─'.repeat(55);
console.log(`\n${border}`);
console.log(` ${title}`);
console.log(`${border}`);
}
/** Rough tokens estimate (~4 chars per token) */
function estimateTokens(text: string): number {
return Math.ceil(text.length / 4);
}
// ─── Public API ────────────────────────────────────────────────────────────────
export const WebSearchLogger = {
pipelineStart(query: string, searchEndpoint: string) {
logSection(`🚀 Web Search Pipeline Started`);
log('INFO', 'Init', `New search initiated`, {
query,
searchEndpoint,
timestamp: new Date().toISOString(),
});
},
endpointResolved(host: string, port: number, searchEndpoint: string) {
log('INFO', 'Endpoint', `Search service URL resolved`, {
lmStudioHost: host,
lmStudioPort: port,
searchServiceEndpoint: searchEndpoint,
});
},
requestSent(query: string, maxContextLength: number, timeoutMs = 30000) {
log('INFO', 'Request', `Sending search request to local service`, {
query,
maxContextLength,
estimatedTokenBudget: estimateTokens('x'.repeat(maxContextLength)),
timeoutMs,
});
},
requestFailed(status: number, statusText: string) {
log('ERROR', 'Request', `Search service returned error`, {
httpStatus: status,
statusText,
});
},
serviceUnreachable(error: unknown, isTimeout = false) {
const err = error instanceof Error ? error : new Error(String(error));
const isConnectionRefused =
err.message.includes('Network request failed') ||
err.message.includes('ECONNREFUSED') ||
err.message.includes('Connection refused');
const message = isTimeout
? `Search service timeout (30s) — service may be overloaded or unreachable`
: isConnectionRefused
? `Search service is NOT running — connection refused`
: `Cannot reach local search service`;
log('ERROR', 'Request', message, {
error: err.message,
isTimeout,
isConnectionRefused,
diagnosis: isConnectionRefused
? 'The local-search-service Node.js process is not running on this machine'
: isTimeout
? 'Service is running but not responding within 30s (SearXNG may be slow)'
: 'Unknown network error',
hint: 'Run: cd local-search-service && npx ts-node server.ts',
});
},
contextReceived(
context: string,
sources: Array<{ title: string; url: string }>,
meta?: {
requestId?: string;
totalLatencyMs?: number;
finalContextChars?: number;
finalContextEstimatedTokens?: number;
sourcesKept?: number;
sourcesDropped?: number;
pruned?: boolean;
},
) {
logSection(`✅ Context Received from Search Service`);
log('INFO', 'Context', `Web search context ready`, {
contextChars: context.length,
estimatedTokens: estimateTokens(context),
sourcesReceived: sources.length,
sourceTitles: sources.map((s) => s.title.substring(0, 60)),
sourceUrls: sources.map((s) => s.url),
...(meta ?? {}),
});
},
contextAnalysis(systemPromptChars: number, historyChars: number, searchContextChars: number) {
logSection(`🧠 Context Window Analysis`);
const totalChars = systemPromptChars + historyChars + searchContextChars;
const totalTokens = estimateTokens('x'.repeat(totalChars));
log('INFO', 'Analysis', `Total context before sending to LLM`, {
systemPromptChars,
systemPromptTokens: estimateTokens('x'.repeat(systemPromptChars)),
chatHistoryChars: historyChars,
chatHistoryTokens: estimateTokens('x'.repeat(historyChars)),
searchContextChars,
searchContextTokens: estimateTokens('x'.repeat(searchContextChars)),
totalChars,
estimatedTotalTokens: totalTokens,
note: 'Token estimates are approximate (~4 chars/token). Actual tokenization varies by model.',
});
},
llmRequestStart(model: string, messageCount: number) {
logSection(`🤖 LLM Request`);
log('INFO', 'LLM', `Sending messages to LM Studio`, {
model,
totalMessages: messageCount,
streamingEnabled: true,
});
},
llmStreamComplete(assistantMessageId: string, chunkCount?: number) {
log('INFO', 'LLM', `Stream complete — response generated`, {
assistantMessageId,
...(chunkCount !== undefined ? { chunkCount } : {}),
});
},
llmError(error: Error) {
log('ERROR', 'LLM', `LLM stream error`, {
error: error.message,
stack: error.stack?.split('\n').slice(0, 3).join(' | '),
});
},
pipelineComplete(query: string, totalLatencyMs: number, withSearch: boolean) {
logSection(`🏁 Pipeline Complete`);
log('INFO', 'Done', `Full request cycle finished`, {
query,
totalLatencyMs,
webSearchUsed: withSearch,
});
console.log('─'.repeat(55) + '\n');
},
};