feat(chat): implement adaptive timeout and enhanced search functionality
- Introduced adaptive timeout logic for web searches and chat streaming, improving responsiveness and efficiency. - Updated `smartSearchRag` method to support timeout adjustments based on search rounds. - Enhanced `executeProgressiveSearch` to utilize adaptive timeouts and improve search result handling. - Added new utility functions for calculating adaptive timeouts based on rounds and attempts. - Integrated event listeners for chat session management, improving user experience during chat interactions. - Refactored chat message handling to accommodate new streaming and timeout features.
This commit is contained in:
1 parent
f2e8a1b933
commit
8c546f6474
13 files changed
+1547
-271
No files matched your search
@@ -0,0 +1,191 @@
|
||||
/**
|
||||
* Circuit Breaker para motores de busca
|
||||
* Desabilita motores que estão falhando consistentemente
|
||||
*/
|
||||
|
||||
export interface EngineHealth {
|
||||
engine: string;
|
||||
successCount: number;
|
||||
failureCount: number;
|
||||
totalAttempts: number;
|
||||
avgLatency: number; // ms
|
||||
lastFailure: number | null; // timestamp
|
||||
lastSuccess: number | null; // timestamp
|
||||
isOpen: boolean; // circuit breaker aberto?
|
||||
openedAt: number | null; // quando foi aberto
|
||||
}
|
||||
|
||||
export interface CircuitBreakerConfig {
|
||||
failureThreshold: number; // Taxa de falha para abrir (0-1, padrão: 0.5 = 50%)
|
||||
cooldownPeriod: number; // Tempo em ms antes de tentar novamente (padrão: 5 minutos)
|
||||
trackingWindow: number; // Número de tentativas para rastrear (padrão: 10)
|
||||
minAttempts: number; // Mínimo de tentativas antes de considerar (padrão: 3)
|
||||
}
|
||||
|
||||
const DEFAULT_CONFIG: CircuitBreakerConfig = {
|
||||
failureThreshold: 0.5, // 50% de falha
|
||||
cooldownPeriod: 5 * 60 * 1000, // 5 minutos
|
||||
trackingWindow: 10,
|
||||
minAttempts: 3,
|
||||
};
|
||||
|
||||
export class EngineCircuitBreaker {
|
||||
private health: Map<string, EngineHealth> = new Map();
|
||||
private config: CircuitBreakerConfig;
|
||||
|
||||
constructor(config: Partial<CircuitBreakerConfig> = {}) {
|
||||
this.config = { ...DEFAULT_CONFIG, ...config };
|
||||
}
|
||||
|
||||
/**
|
||||
* Registra uma tentativa de busca
|
||||
*/
|
||||
recordAttempt(engine: string, success: boolean, latency: number): void {
|
||||
const health = this.getOrCreateHealth(engine);
|
||||
|
||||
health.totalAttempts++;
|
||||
health.avgLatency = (health.avgLatency * (health.totalAttempts - 1) + latency) / health.totalAttempts;
|
||||
|
||||
if (success) {
|
||||
health.successCount++;
|
||||
health.lastSuccess = Date.now();
|
||||
|
||||
// Se teve sucesso e circuit estava aberto há tempo suficiente, tentar fechar
|
||||
if (health.isOpen && health.openedAt) {
|
||||
const timeSinceOpen = Date.now() - health.openedAt;
|
||||
if (timeSinceOpen >= this.config.cooldownPeriod) {
|
||||
health.isOpen = false;
|
||||
health.openedAt = null;
|
||||
console.log(`[CircuitBreaker] ${engine}: Circuit fechado após sucesso (${timeSinceOpen}ms desde abertura)`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
health.failureCount++;
|
||||
health.lastFailure = Date.now();
|
||||
}
|
||||
|
||||
// Limitar histórico às últimas N tentativas
|
||||
if (health.totalAttempts > this.config.trackingWindow) {
|
||||
// Remover tentativas mais antigas (simplificado: resetar contadores)
|
||||
const successRate = health.successCount / health.totalAttempts;
|
||||
health.successCount = Math.floor(successRate * this.config.trackingWindow);
|
||||
health.failureCount = this.config.trackingWindow - health.successCount;
|
||||
health.totalAttempts = this.config.trackingWindow;
|
||||
}
|
||||
|
||||
// Verificar se deve abrir circuit breaker
|
||||
if (!health.isOpen && health.totalAttempts >= this.config.minAttempts) {
|
||||
const failureRate = health.failureCount / health.totalAttempts;
|
||||
if (failureRate >= this.config.failureThreshold) {
|
||||
health.isOpen = true;
|
||||
health.openedAt = Date.now();
|
||||
console.warn(`[CircuitBreaker] ${engine}: Circuit aberto (taxa de falha: ${(failureRate * 100).toFixed(1)}%)`);
|
||||
}
|
||||
}
|
||||
|
||||
this.health.set(engine, health);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica se um motor está disponível (circuit não está aberto)
|
||||
*/
|
||||
isAvailable(engine: string): boolean {
|
||||
const health = this.getOrCreateHealth(engine);
|
||||
|
||||
if (!health.isOpen) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Se está aberto, verificar se passou tempo suficiente para tentar novamente
|
||||
if (health.openedAt) {
|
||||
const timeSinceOpen = Date.now() - health.openedAt;
|
||||
if (timeSinceOpen >= this.config.cooldownPeriod) {
|
||||
// Tentar fechar (half-open state)
|
||||
health.isOpen = false;
|
||||
this.health.set(engine, health);
|
||||
console.log(`[CircuitBreaker] ${engine}: Tentando fechar circuit (half-open state)`);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prioriza motores baseado em saúde
|
||||
*/
|
||||
prioritizeEngines(engines: string[]): string[] {
|
||||
return [...engines].sort((a, b) => {
|
||||
const healthA = this.getOrCreateHealth(a);
|
||||
const healthB = this.getOrCreateHealth(b);
|
||||
|
||||
// Motores não disponíveis vão para o final
|
||||
if (healthA.isOpen && !healthB.isOpen) return 1;
|
||||
if (!healthA.isOpen && healthB.isOpen) return -1;
|
||||
|
||||
// Comparar taxa de sucesso
|
||||
const successRateA = healthA.totalAttempts > 0
|
||||
? healthA.successCount / healthA.totalAttempts
|
||||
: 0.5;
|
||||
const successRateB = healthB.totalAttempts > 0
|
||||
? healthB.successCount / healthB.totalAttempts
|
||||
: 0.5;
|
||||
|
||||
if (Math.abs(successRateA - successRateB) > 0.1) {
|
||||
return successRateB - successRateA; // Maior taxa de sucesso primeiro
|
||||
}
|
||||
|
||||
// Se taxa similar, comparar latência
|
||||
return healthA.avgLatency - healthB.avgLatency; // Menor latência primeiro
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtém estatísticas de um motor
|
||||
*/
|
||||
getHealth(engine: string): EngineHealth | null {
|
||||
return this.health.get(engine) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtém ou cria health para um motor
|
||||
*/
|
||||
private getOrCreateHealth(engine: string): EngineHealth {
|
||||
if (!this.health.has(engine)) {
|
||||
this.health.set(engine, {
|
||||
engine,
|
||||
successCount: 0,
|
||||
failureCount: 0,
|
||||
totalAttempts: 0,
|
||||
avgLatency: 0,
|
||||
lastFailure: null,
|
||||
lastSuccess: null,
|
||||
isOpen: false,
|
||||
openedAt: null,
|
||||
});
|
||||
}
|
||||
return this.health.get(engine)!;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reseta estatísticas de um motor
|
||||
*/
|
||||
reset(engine: string): void {
|
||||
this.health.delete(engine);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reseta todas as estatísticas
|
||||
*/
|
||||
resetAll(): void {
|
||||
this.health.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtém todos os motores disponíveis
|
||||
*/
|
||||
getAvailableEngines(engines: string[]): string[] {
|
||||
return engines.filter(engine => this.isAvailable(engine));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
/**
|
||||
* Cache de falhas para evitar retentar queries que falharam recentemente
|
||||
*/
|
||||
|
||||
export interface FailureCacheEntry {
|
||||
query: string;
|
||||
timestamp: number;
|
||||
error: string;
|
||||
partialResults?: any[]; // Resultados parciais se houver
|
||||
attempts: number; // Número de tentativas antes de falhar
|
||||
}
|
||||
|
||||
export interface FailureCacheConfig {
|
||||
ttl: number; // Time to live em ms (padrão: 5 minutos)
|
||||
maxEntries: number; // Máximo de entradas no cache (padrão: 100)
|
||||
}
|
||||
|
||||
const DEFAULT_CONFIG: FailureCacheConfig = {
|
||||
ttl: 5 * 60 * 1000, // 5 minutos
|
||||
maxEntries: 100,
|
||||
};
|
||||
|
||||
export class FailureCache {
|
||||
private cache: Map<string, FailureCacheEntry> = new Map();
|
||||
private config: FailureCacheConfig;
|
||||
|
||||
constructor(config: Partial<FailureCacheConfig> = {}) {
|
||||
this.config = { ...DEFAULT_CONFIG, ...config };
|
||||
// Limpar entradas expiradas periodicamente
|
||||
setInterval(() => this.cleanExpired(), 60000); // A cada 1 minuto
|
||||
}
|
||||
|
||||
/**
|
||||
* Normaliza uma query para usar como chave
|
||||
*/
|
||||
private normalizeQuery(query: string): string {
|
||||
return query.trim().toLowerCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica se uma query falhou recentemente
|
||||
*/
|
||||
hasFailed(query: string): boolean {
|
||||
const normalized = this.normalizeQuery(query);
|
||||
const entry = this.cache.get(normalized);
|
||||
|
||||
if (!entry) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Verificar se expirou
|
||||
const age = Date.now() - entry.timestamp;
|
||||
if (age >= this.config.ttl) {
|
||||
this.cache.delete(normalized);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtém entrada de falha (se existir e não expirada)
|
||||
*/
|
||||
get(query: string): FailureCacheEntry | null {
|
||||
const normalized = this.normalizeQuery(query);
|
||||
const entry = this.cache.get(normalized);
|
||||
|
||||
if (!entry) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Verificar se expirou
|
||||
const age = Date.now() - entry.timestamp;
|
||||
if (age >= this.config.ttl) {
|
||||
this.cache.delete(normalized);
|
||||
return null;
|
||||
}
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registra uma falha
|
||||
*/
|
||||
recordFailure(
|
||||
query: string,
|
||||
error: string,
|
||||
partialResults?: any[],
|
||||
attempts: number = 1
|
||||
): void {
|
||||
const normalized = this.normalizeQuery(query);
|
||||
|
||||
// Limitar tamanho do cache
|
||||
if (this.cache.size >= this.config.maxEntries) {
|
||||
// Remover entrada mais antiga
|
||||
const oldest = Array.from(this.cache.entries())
|
||||
.sort((a, b) => a[1].timestamp - b[1].timestamp)[0];
|
||||
if (oldest) {
|
||||
this.cache.delete(oldest[0]);
|
||||
}
|
||||
}
|
||||
|
||||
this.cache.set(normalized, {
|
||||
query: normalized,
|
||||
timestamp: Date.now(),
|
||||
error,
|
||||
partialResults,
|
||||
attempts,
|
||||
});
|
||||
|
||||
console.log(`[FailureCache] Registrada falha para query: "${query.substring(0, 50)}..." (TTL: ${this.config.ttl}ms)`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove uma entrada do cache
|
||||
*/
|
||||
remove(query: string): void {
|
||||
const normalized = this.normalizeQuery(query);
|
||||
this.cache.delete(normalized);
|
||||
}
|
||||
|
||||
/**
|
||||
* Limpa todas as entradas expiradas
|
||||
*/
|
||||
cleanExpired(): void {
|
||||
const now = Date.now();
|
||||
let cleaned = 0;
|
||||
|
||||
for (const [key, entry] of this.cache.entries()) {
|
||||
const age = now - entry.timestamp;
|
||||
if (age >= this.config.ttl) {
|
||||
this.cache.delete(key);
|
||||
cleaned++;
|
||||
}
|
||||
}
|
||||
|
||||
if (cleaned > 0) {
|
||||
console.log(`[FailureCache] Limpas ${cleaned} entradas expiradas`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Limpa todo o cache
|
||||
*/
|
||||
clear(): void {
|
||||
this.cache.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtém estatísticas do cache
|
||||
*/
|
||||
getStats(): { size: number; maxEntries: number; ttl: number } {
|
||||
return {
|
||||
size: this.cache.size,
|
||||
maxEntries: this.config.maxEntries,
|
||||
ttl: this.config.ttl,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+179
-31
@@ -1,6 +1,15 @@
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { SearchCategory, useSettingsStore } from '@/store/settings-store';
|
||||
import { retryWithBackoff, withTimeout, isRetryableError, type RetryConfig } from '@/lib/retry-utils';
|
||||
import {
|
||||
retryWithBackoff,
|
||||
withTimeout,
|
||||
isRetryableError,
|
||||
type RetryConfig,
|
||||
calculateEngineTimeout,
|
||||
TIMEOUT_CONFIG
|
||||
} from '@/lib/retry-utils';
|
||||
import { EngineCircuitBreaker } from './engine-circuit-breaker';
|
||||
import { FailureCache } from './failure-cache';
|
||||
|
||||
export interface ScrapedContent {
|
||||
title: string;
|
||||
@@ -49,6 +58,8 @@ class WebSearchService {
|
||||
private lastSearchTime = 0;
|
||||
private pendingRequest: PendingRequest | null = null;
|
||||
private rateLimitQueue: PendingRequest[] = [];
|
||||
private circuitBreaker: EngineCircuitBreaker = new EngineCircuitBreaker();
|
||||
private failureCache: FailureCache = new FailureCache();
|
||||
|
||||
constructor() {
|
||||
// Carregar cache do localStorage na inicialização
|
||||
@@ -221,16 +232,50 @@ class WebSearchService {
|
||||
|
||||
/**
|
||||
* Executa busca de metadados (sem scraping)
|
||||
* Agora com timeout adaptativo baseado em tentativas, circuit breaker e cache de falhas
|
||||
*/
|
||||
private async executeMetadataSearch(
|
||||
query: string,
|
||||
limit: number = 5,
|
||||
searchConfig?: SearchConfig,
|
||||
timeoutMs: number = 15000
|
||||
timeoutMs: number = 10000, // Reduzido de 15s para 10s
|
||||
attempt: number = 1 // Tentativa atual para timeout adaptativo
|
||||
): Promise<SearchResultMetadata[]> {
|
||||
// Verificar cache de falhas
|
||||
const failureEntry = this.failureCache.get(query);
|
||||
if (failureEntry) {
|
||||
console.warn(`[WebSearch] Query falhou recentemente (há ${Math.round((Date.now() - failureEntry.timestamp) / 1000)}s), retornando resultados parciais ou vazio`);
|
||||
|
||||
// Retornar resultados parciais se disponíveis
|
||||
if (failureEntry.partialResults && failureEntry.partialResults.length > 0) {
|
||||
console.log(`[WebSearch] Retornando ${failureEntry.partialResults.length} resultados parciais do cache de falhas`);
|
||||
return failureEntry.partialResults as SearchResultMetadata[];
|
||||
}
|
||||
|
||||
// Se não há resultados parciais, retornar vazio imediatamente (sem retry)
|
||||
return [];
|
||||
}
|
||||
// Obter configurações de motores do store
|
||||
const settings = useSettingsStore.getState();
|
||||
const engineOrder = settings.webSearch.engineOrder || ['google', 'bing', 'yahoo', 'duckduckgo', 'startpage'];
|
||||
const defaultEngines = ['google', 'bing', 'yahoo', 'duckduckgo', 'startpage'];
|
||||
let engineOrder = settings.webSearch.engineOrder || defaultEngines;
|
||||
|
||||
// Priorizar motores usando circuit breaker
|
||||
engineOrder = this.circuitBreaker.prioritizeEngines(engineOrder);
|
||||
|
||||
// Filtrar motores não disponíveis (circuit breaker aberto)
|
||||
const availableEngines = this.circuitBreaker.getAvailableEngines(engineOrder);
|
||||
|
||||
if (availableEngines.length === 0) {
|
||||
console.warn(`[WebSearch] Todos os motores estão com circuit breaker aberto, tentando todos mesmo assim`);
|
||||
// Se todos estão abertos, tentar mesmo assim (pode ser temporário)
|
||||
engineOrder = engineOrder;
|
||||
} else {
|
||||
engineOrder = availableEngines;
|
||||
if (availableEngines.length < engineOrder.length) {
|
||||
console.log(`[WebSearch] ${engineOrder.length - availableEngines.length} motor(es) com circuit breaker aberto, usando ${availableEngines.length} disponível(is)`);
|
||||
}
|
||||
}
|
||||
|
||||
let rustConfig: any = undefined;
|
||||
if (searchConfig) {
|
||||
@@ -248,15 +293,23 @@ class WebSearchService {
|
||||
};
|
||||
}
|
||||
|
||||
// Calcular timeout adaptativo baseado na tentativa
|
||||
const adaptiveTimeout = calculateEngineTimeout(attempt, timeoutMs);
|
||||
|
||||
// Log da tentativa
|
||||
console.log(`[WebSearch] Executando busca multi-engine para: "${query}"`);
|
||||
console.log(`[WebSearch] Engine order: ${engineOrder.join(' → ')}`);
|
||||
console.log(`[WebSearch] Limit: ${limit}, Timeout: ${timeoutMs}ms`);
|
||||
console.log(`[WebSearch] Limit: ${limit}, Timeout: ${adaptiveTimeout}ms (tentativa ${attempt})`);
|
||||
|
||||
// Executar com retry e timeout
|
||||
// Executar com retry e timeout adaptativo
|
||||
let retryAttempt = 0;
|
||||
const retryResult = await retryWithBackoff<SearchResultMetadata[]>(
|
||||
async () => {
|
||||
retryAttempt++;
|
||||
const startTime = Date.now();
|
||||
// Timeout ainda mais reduzido em retries internos
|
||||
const retryTimeout = calculateEngineTimeout(retryAttempt, adaptiveTimeout);
|
||||
|
||||
try {
|
||||
const promise = invoke<SearchResultMetadata[]>('search_web_metadata', {
|
||||
query: query.trim(),
|
||||
@@ -267,18 +320,32 @@ class WebSearchService {
|
||||
|
||||
const result = await withTimeout(
|
||||
promise,
|
||||
timeoutMs,
|
||||
`Timeout ao buscar metadados para "${query}"`
|
||||
retryTimeout,
|
||||
`Timeout ao buscar metadados para "${query}" (tentativa ${retryAttempt})`
|
||||
);
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
console.log(`[WebSearch] Busca concluída em ${duration}ms: ${result.length} resultados`);
|
||||
|
||||
// Registrar sucesso no circuit breaker para cada motor usado
|
||||
// (assumindo que o primeiro motor disponível foi usado)
|
||||
if (engineOrder.length > 0) {
|
||||
const usedEngine = engineOrder[0]; // Simplificado: primeiro motor da ordem
|
||||
this.circuitBreaker.recordAttempt(usedEngine, true, duration);
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
const duration = Date.now() - startTime;
|
||||
const errorMsg = error instanceof Error ? error.message : String(error);
|
||||
console.error(`[WebSearch] Erro após ${duration}ms:`, errorMsg);
|
||||
console.error(`[WebSearch] Erro após ${duration}ms (tentativa ${retryAttempt}):`, errorMsg);
|
||||
|
||||
// Registrar falha no circuit breaker para cada motor tentado
|
||||
// (assumindo que tentou todos os motores na ordem)
|
||||
for (const engine of engineOrder) {
|
||||
this.circuitBreaker.recordAttempt(engine, false, duration);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
@@ -298,25 +365,39 @@ class WebSearchService {
|
||||
return results;
|
||||
}
|
||||
|
||||
// Se falhou mas é erro recuperável, logar detalhadamente
|
||||
if (retryResult.lastError && isRetryableError(retryResult.lastError)) {
|
||||
console.warn(
|
||||
`[WebSearch] Falha após ${retryResult.attempts} tentativas:`,
|
||||
retryResult.lastError.message
|
||||
);
|
||||
console.warn(`[WebSearch] Query: "${query}", Limit: ${limit}, Timeout: ${timeoutMs}ms`);
|
||||
console.warn(`[WebSearch] Motores tentados: ${engineOrder.join(', ')}`);
|
||||
return [];
|
||||
}
|
||||
|
||||
// Para erros não recuperáveis, logar detalhadamente
|
||||
// Se falhou, registrar no cache de falhas
|
||||
if (retryResult.lastError) {
|
||||
console.error(
|
||||
`[WebSearch] Erro não recuperável:`,
|
||||
retryResult.lastError.message
|
||||
const errorMsg = retryResult.lastError.message;
|
||||
const isRetryable = isRetryableError(retryResult.lastError);
|
||||
|
||||
// Registrar falha no cache (mesmo que seja recuperável, para evitar retentar imediatamente)
|
||||
this.failureCache.recordFailure(
|
||||
query,
|
||||
errorMsg,
|
||||
[], // Sem resultados parciais neste ponto
|
||||
retryResult.attempts
|
||||
);
|
||||
console.error(`[WebSearch] Query: "${query}", Limit: ${limit}, Timeout: ${timeoutMs}ms`);
|
||||
console.error(`[WebSearch] Motores tentados: ${engineOrder.join(', ')}`);
|
||||
|
||||
if (isRetryable) {
|
||||
console.warn(
|
||||
`[WebSearch] Falha após ${retryResult.attempts} tentativas (erro recuperável):`,
|
||||
errorMsg
|
||||
);
|
||||
console.warn(`[WebSearch] Query: "${query}", Limit: ${limit}, Timeout: ${adaptiveTimeout}ms`);
|
||||
console.warn(`[WebSearch] Motores tentados: ${engineOrder.join(', ')}`);
|
||||
console.warn(`[WebSearch] Retornando resultados parciais (se houver) ou array vazio`);
|
||||
// Retornar vazio mas não quebrar o fluxo - o caller pode continuar com outros motores
|
||||
return [];
|
||||
} else {
|
||||
// Para erros não recuperáveis, logar detalhadamente mas retornar vazio
|
||||
console.error(
|
||||
`[WebSearch] Erro não recuperável:`,
|
||||
errorMsg
|
||||
);
|
||||
console.error(`[WebSearch] Query: "${query}", Limit: ${limit}, Timeout: ${adaptiveTimeout}ms`);
|
||||
console.error(`[WebSearch] Motores tentados: ${engineOrder.join(', ')}`);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
@@ -403,23 +484,30 @@ class WebSearchService {
|
||||
|
||||
/**
|
||||
* Busca em duas etapas: metadados → scraping das melhores URLs
|
||||
* Agora com timeout adaptativo
|
||||
*/
|
||||
async smartSearchRag(
|
||||
query: string,
|
||||
limit: number = 3,
|
||||
searchConfig?: SearchConfig,
|
||||
timeoutMs: number = 15000
|
||||
timeoutMs: number = TIMEOUT_CONFIG.initialTimeout,
|
||||
round: number = 1 // Round atual para timeout escalonado
|
||||
): Promise<{ metadata: SearchResultMetadata[]; contents: ScrapedContent[] }> {
|
||||
if (!query || !query.trim()) {
|
||||
return { metadata: [], contents: [] };
|
||||
}
|
||||
|
||||
// Buscar metadados com retry e timeout (já implementado em executeMetadataSearch)
|
||||
// Calcular timeout escalonado por round
|
||||
const { calculateAdaptiveTimeout } = await import('@/lib/retry-utils');
|
||||
const roundTimeout = calculateAdaptiveTimeout(round, timeoutMs);
|
||||
|
||||
// Buscar metadados com retry e timeout adaptativo
|
||||
const metas = await this.executeMetadataSearch(
|
||||
query,
|
||||
Math.max(limit * 2, 5),
|
||||
searchConfig,
|
||||
timeoutMs
|
||||
roundTimeout,
|
||||
1 // Primeira tentativa
|
||||
);
|
||||
|
||||
// Continuar mesmo se metadados estiverem vazios (pode ter falhado parcialmente)
|
||||
@@ -431,22 +519,82 @@ class WebSearchService {
|
||||
}
|
||||
|
||||
// Tentar fazer scraping mesmo se metadados foram parciais
|
||||
// Usar timeout reduzido para scraping (50% do timeout de busca)
|
||||
const scrapingTimeout = Math.max(roundTimeout * 0.5, 5000);
|
||||
|
||||
try {
|
||||
const contents = await withTimeout(
|
||||
invoke<ScrapedContent[]>('scrape_urls', { urls: topUrls }),
|
||||
timeoutMs,
|
||||
scrapingTimeout,
|
||||
`Timeout ao fazer scraping para "${query}"`
|
||||
);
|
||||
return { metadata: metas, contents: contents || [] };
|
||||
} catch (error) {
|
||||
// Logar erro mas retornar metadados disponíveis (resultado parcial)
|
||||
// Graceful degradation: continuar com metadados mesmo se scraping falhar
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
console.warn('Erro ao fazer scraping em lote (continuando com metadados):', errorMessage);
|
||||
console.warn(`[WebSearch] Erro ao fazer scraping (continuando com metadados): ${errorMessage}`);
|
||||
console.warn(`[WebSearch] Retornando ${metas.length} metadados sem conteúdo scraped (resultado parcial)`);
|
||||
|
||||
// Retornar metadados mesmo se scraping falhou
|
||||
// Retornar metadados mesmo se scraping falhou - resultado parcial é melhor que nada
|
||||
return { metadata: metas, contents: [] };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Versão simplificada que aceita resultados parciais mais rapidamente
|
||||
* Retorna assim que qualquer motor retornar resultados, sem esperar todos
|
||||
*/
|
||||
async smartSearchRagPartial(
|
||||
query: string,
|
||||
limit: number = 3,
|
||||
searchConfig?: SearchConfig,
|
||||
timeoutMs: number = TIMEOUT_CONFIG.initialTimeout,
|
||||
round: number = 1
|
||||
): Promise<{ metadata: SearchResultMetadata[]; contents: ScrapedContent[] }> {
|
||||
if (!query || !query.trim()) {
|
||||
return { metadata: [], contents: [] };
|
||||
}
|
||||
|
||||
// Timeout ainda mais agressivo para resultados parciais
|
||||
const partialTimeout = Math.max(timeoutMs * 0.7, 5000); // 70% do timeout normal
|
||||
|
||||
try {
|
||||
// Buscar metadados com timeout reduzido
|
||||
const metas = await this.executeMetadataSearch(
|
||||
query,
|
||||
Math.max(limit * 2, 5),
|
||||
searchConfig,
|
||||
partialTimeout,
|
||||
1
|
||||
);
|
||||
|
||||
// Se temos metadados, tentar scraping rápido (timeout ainda menor)
|
||||
if (metas.length > 0) {
|
||||
const topUrls = metas.map(m => m.url).slice(0, limit);
|
||||
const scrapingTimeout = Math.max(partialTimeout * 0.4, 3000); // 40% do timeout parcial
|
||||
|
||||
try {
|
||||
const contents = await withTimeout(
|
||||
invoke<ScrapedContent[]>('scrape_urls', { urls: topUrls }),
|
||||
scrapingTimeout,
|
||||
`Timeout parcial ao fazer scraping para "${query}"`
|
||||
);
|
||||
return { metadata: metas, contents: contents || [] };
|
||||
} catch (error) {
|
||||
// Aceitar resultado parcial: metadados sem scraping
|
||||
console.warn(`[WebSearch] Scraping parcial falhou, retornando apenas metadados`);
|
||||
return { metadata: metas, contents: [] };
|
||||
}
|
||||
}
|
||||
|
||||
return { metadata: metas, contents: [] };
|
||||
} catch (error) {
|
||||
// Em caso de erro total, retornar vazio mas não quebrar
|
||||
console.warn(`[WebSearch] Busca parcial falhou completamente:`, error);
|
||||
return { metadata: [], contents: [] };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extrai conteúdo de uma URL específica
|
||||
|
||||
Reference in new issue
Block a user