Origem: D:/Documents/Github/LocalAiApp Arquivos omitidos (segredos, bancos de dados, logs, >50 MB): 0
191 lines
6.6 KiB
TypeScript
191 lines
6.6 KiB
TypeScript
/**
|
||
* 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');
|
||
},
|
||
};
|