Files
LocalMind/services/webSearchLogger.ts
T
evandro e08189f640 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
2026-10-20 00:48:09 -03:00

191 lines
6.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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');
},
};