habulaj commited on
Commit
532f1bb
·
verified ·
1 Parent(s): 8bc855b

Update routers/searchterm.py

Browse files
Files changed (1) hide show
  1. routers/searchterm.py +290 -195
routers/searchterm.py CHANGED
@@ -17,10 +17,8 @@ from newspaper import Article
17
  from threading import Timer
18
  from google import genai
19
  from google.genai import types
20
- from asyncio import Queue, create_task, gather
21
- from concurrent.futures import ThreadPoolExecutor
22
- import aiofiles
23
- import ujson # JSON mais rápido
24
 
25
  router = APIRouter()
26
 
@@ -45,16 +43,12 @@ USER_AGENTS = [
45
  "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
46
  ]
47
 
48
- BLOCKED_DOMAINS = frozenset({ # frozenset é mais rápido para lookup
49
- "reddit.com", "www.reddit.com", "old.reddit.com",
50
- "quora.com", "www.quora.com"
51
- })
52
 
53
  MAX_TEXT_LENGTH = 4000
54
- MAX_CONCURRENT_SEARCHES = 30 # Aumentado
55
- MAX_CONCURRENT_EXTRACTIONS = 80 # Aumentado significativamente
56
- EXTRACTION_TIMEOUT = 8 # Reduzido
57
- HTTP_TIMEOUT = 10 # Reduzido
58
 
59
  # Diretório para arquivos temporários
60
  TEMP_DIR = Path("/tmp")
@@ -63,30 +57,25 @@ TEMP_DIR.mkdir(exist_ok=True)
63
  # Dicionário para controlar arquivos temporários
64
  temp_files = {}
65
 
66
- # Pool de threads para operações CPU-intensive
67
- thread_pool = ThreadPoolExecutor(max_workers=20)
68
 
69
- # Cache de domínios bloqueados para evitar verificações repetidas
70
- domain_cache = {}
71
 
72
  def is_blocked_domain(url: str) -> bool:
73
  try:
74
  host = urlparse(url).netloc.lower()
75
-
76
- # Cache lookup
77
- if host in domain_cache:
78
- return domain_cache[host]
79
-
80
- is_blocked = any(host == b or host.endswith("." + b) for b in BLOCKED_DOMAINS)
81
- domain_cache[host] = is_blocked
82
- return is_blocked
83
  except Exception:
84
  return False
85
 
 
86
  def clamp_text(text: str) -> str:
87
- if not text or len(text) <= MAX_TEXT_LENGTH:
88
- return text
89
- return text[:MAX_TEXT_LENGTH]
 
 
 
90
 
91
  def get_realistic_headers() -> Dict[str, str]:
92
  return {
@@ -95,8 +84,11 @@ def get_realistic_headers() -> Dict[str, str]:
95
  "Accept-Language": "en-US,en;q=0.7,pt-BR;q=0.6",
96
  "Connection": "keep-alive",
97
  "Accept-Encoding": "gzip, deflate, br",
 
 
98
  }
99
 
 
100
  def delete_temp_file(file_id: str, file_path: Path):
101
  """Remove arquivo temporário após expiração"""
102
  try:
@@ -107,16 +99,17 @@ def delete_temp_file(file_id: str, file_path: Path):
107
  except Exception as e:
108
  print(f"Erro ao remover arquivo temporário: {e}")
109
 
110
- async def create_temp_file(data: Dict[str, Any]) -> Dict[str, str]:
111
- """Cria arquivo temporário assíncrono e agenda sua remoção"""
 
112
  file_id = str(uuid.uuid4())
113
  file_path = TEMP_DIR / f"fontes_{file_id}.txt"
114
 
115
- # Salva o JSON no arquivo de forma assíncrona
116
- async with aiofiles.open(file_path, 'w', encoding='utf-8') as f:
117
- await f.write(ujson.dumps(data, ensure_ascii=False, indent=2))
118
 
119
- # Agenda remoção em 24 horas
120
  timer = Timer(86400, delete_temp_file, args=[file_id, file_path])
121
  timer.start()
122
 
@@ -133,6 +126,22 @@ async def create_temp_file(data: Dict[str, Any]) -> Dict[str, str]:
133
  "expires_in_hours": 24
134
  }
135
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
136
  async def generate_search_terms(context: str) -> List[str]:
137
  """Gera termos de pesquisa usando o modelo Gemini"""
138
  try:
@@ -179,7 +188,9 @@ Retorne apenas o JSON, sem mais nenhum texto."""
179
  ]
180
 
181
  generate_content_config = types.GenerateContentConfig(
182
- thinking_config=types.ThinkingConfig(thinking_budget=0),
 
 
183
  )
184
 
185
  # Coletamos toda a resposta em stream
@@ -194,6 +205,7 @@ Retorne apenas o JSON, sem mais nenhum texto."""
194
 
195
  # Tenta extrair o JSON da resposta
196
  try:
 
197
  clean_response = full_response.strip()
198
  if clean_response.startswith("```json"):
199
  clean_response = clean_response[7:]
@@ -201,170 +213,234 @@ Retorne apenas o JSON, sem mais nenhum texto."""
201
  clean_response = clean_response[:-3]
202
  clean_response = clean_response.strip()
203
 
204
- response_data = ujson.loads(clean_response)
 
205
  terms = response_data.get("terms", [])
206
 
 
207
  if not isinstance(terms, list):
208
  raise ValueError("Terms deve ser uma lista")
209
 
210
- return terms[:20]
211
 
212
- except (ujson.JSONDecodeError, ValueError) as e:
213
  print(f"Erro ao parsear resposta do Gemini: {e}")
 
 
214
  return []
215
 
216
  except Exception as e:
217
  print(f"Erro ao gerar termos de pesquisa: {str(e)}")
218
  return []
219
 
 
220
  async def search_brave_batch(client: httpx.AsyncClient, terms: List[str]) -> List[Tuple[str, List[Dict[str, str]]]]:
221
- """Busca múltiplos termos em paralelo com otimizações"""
222
- semaphore = asyncio.Semaphore(MAX_CONCURRENT_SEARCHES)
223
 
224
  async def search_single_term(term: str) -> Tuple[str, List[Dict[str, str]]]:
225
- async with semaphore:
226
- params = {"q": term, "count": 10, "safesearch": "off", "summary": "false"}
 
 
 
 
 
 
 
227
 
228
- try:
229
- resp = await client.get(BRAVE_SEARCH_URL, headers=BRAVE_HEADERS, params=params)
230
- if resp.status_code != 200:
231
- return (term, [])
 
 
 
232
 
233
- data = resp.json()
234
- results = []
235
-
236
- if "web" in data and "results" in data["web"]:
237
- for item in data["web"]["results"]:
238
- url = item.get("url")
239
- age = item.get("age", "Unknown")
240
-
241
- if url and not is_blocked_domain(url):
242
- results.append({"url": url, "age": age})
243
-
244
- return (term, results)
245
- except Exception as e:
246
- print(f"Erro na busca do termo '{term}': {e}")
247
- return (term, [])
248
-
249
- # Executa todas as buscas em paralelo
250
- tasks = [search_single_term(term) for term in terms]
251
- return await gather(*tasks, return_exceptions=False)
252
-
253
- def extract_with_trafilatura(html: str) -> str:
254
- """Extração CPU-intensive executada em thread pool"""
255
- try:
256
- extracted = trafilatura.extract(html)
257
- return extracted.strip() if extracted else ""
258
- except Exception:
259
- return ""
260
 
261
- def extract_with_newspaper(url: str) -> str:
262
- """Extração com newspaper executada em thread pool"""
263
- try:
264
- art = Article(url)
265
- art.config.browser_user_agent = random.choice(USER_AGENTS)
266
- art.config.request_timeout = 6
267
- art.config.number_threads = 1
268
- art.download()
269
- art.parse()
270
- return (art.text or "").strip()
271
- except Exception:
272
- return ""
273
 
274
- async def extract_article_text_optimized(url: str, session: aiohttp.ClientSession) -> str:
275
- """Extração de artigo otimizada com fallback robusto"""
 
 
 
276
 
277
- # Método 1: Tentar com trafilatura primeiro (mais rápido)
278
  try:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
279
  headers = get_realistic_headers()
280
- async with session.get(url, headers=headers, timeout=EXTRACTION_TIMEOUT) as resp:
281
- if resp.status == 200:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
282
  html = await resp.text()
283
 
284
- # Verifica paywall rapidamente
285
- if not re.search(r"(paywall|subscribe|metered|registration|captcha|access denied)",
286
- html[:2000], re.I):
287
-
288
- # Extração com trafilatura em thread pool
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
289
  try:
290
- trafilatura_result = await asyncio.get_event_loop().run_in_executor(
291
- thread_pool, extract_with_trafilatura, html
292
- )
293
-
294
- if trafilatura_result and len(trafilatura_result.strip()) > 100:
295
- return clamp_text(trafilatura_result.strip())
296
- except Exception as e:
297
- print(f"Erro trafilatura para {url}: {e}")
298
-
299
- except Exception as e:
300
- print(f"Erro HTTP para {url}: {e}")
301
-
302
- # Método 2: Fallback para newspaper
303
- try:
304
- newspaper_result = await asyncio.get_event_loop().run_in_executor(
305
- thread_pool, extract_with_newspaper, url
306
- )
307
 
308
- if newspaper_result and len(newspaper_result.strip()) > 100:
309
- return clamp_text(newspaper_result.strip())
310
-
311
- except Exception as e:
312
- print(f"Erro newspaper para {url}: {e}")
313
-
314
- return ""
315
-
316
- async def process_urls_batch(session: aiohttp.ClientSession, urls_data: List[Tuple[str, str, str]]) -> List[Dict[str, Any]]:
317
- """Processa URLs em lotes otimizados com logging detalhado"""
318
- semaphore = asyncio.Semaphore(MAX_CONCURRENT_EXTRACTIONS)
319
- results = []
320
- used_urls: Set[str] = set()
321
- success_count = 0
322
-
323
- async def process_single_url(term: str, url: str, age: str) -> Optional[Dict[str, Any]]:
324
- nonlocal success_count
325
- async with semaphore:
326
- if url in used_urls:
327
- return None
328
-
329
  try:
330
- text = await extract_article_text_optimized(url, session)
331
- if text:
332
- used_urls.add(url)
333
- success_count += 1
334
- print(f"✓ Extraído: {url[:60]}... ({len(text)} chars)")
335
  return {
336
  "term": term,
337
  "age": age,
338
  "url": url,
339
- "text": text
 
340
  }
341
- else:
342
- print(f"✗ Falhou: {url[:60]}... (sem conteúdo)")
343
- except Exception as e:
344
- print(f"✗ Erro: {url[:60]}... - {str(e)[:50]}")
345
-
346
- return None
347
 
348
- # Cria todas as tasks de uma vez
349
- tasks = []
350
- for term, url, age in urls_data:
351
- tasks.append(process_single_url(term, url, age))
 
352
 
353
- print(f"Processando {len(tasks)} URLs com semáforo de {MAX_CONCURRENT_EXTRACTIONS}...")
 
 
354
 
355
- # Processa tudo em paralelo
356
- processed_results = await gather(*tasks, return_exceptions=True)
 
 
 
 
357
 
358
- # Filtra resultados válidos
359
- valid_results = [r for r in processed_results if r is not None and not isinstance(r, Exception)]
360
 
361
- print(f"Sucesso: {success_count}/{len(urls_data)} URLs extraídas")
362
- return valid_results
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
363
 
364
  @router.post("/search-terms")
365
  async def search_terms(payload: Dict[str, str] = Body(...)) -> Dict[str, Any]:
366
- start_time = time.time()
367
-
368
  context = payload.get("context")
369
  if not context or not isinstance(context, str):
370
  raise HTTPException(status_code=400, detail="Campo 'context' é obrigatório e deve ser uma string.")
@@ -372,73 +448,92 @@ async def search_terms(payload: Dict[str, str] = Body(...)) -> Dict[str, Any]:
372
  if len(context.strip()) == 0:
373
  raise HTTPException(status_code=400, detail="Campo 'context' não pode estar vazio.")
374
 
375
- print(f"Iniciando geração de termos...")
 
 
376
  # Gera os termos de pesquisa usando o Gemini
377
  terms = await generate_search_terms(context)
378
 
379
  if not terms:
380
  raise HTTPException(status_code=500, detail="Não foi possível gerar termos de pesquisa válidos.")
381
-
382
- print(f"Termos gerados em {time.time() - start_time:.2f}s. Iniciando buscas...")
383
 
384
- # Configurações otimizadas para conexões
 
 
 
 
385
  connector = aiohttp.TCPConnector(
386
- limit=200, # Aumentado
387
- limit_per_host=30, # Aumentado
388
- ttl_dns_cache=300,
389
- use_dns_cache=True,
390
- enable_cleanup_closed=True
 
391
  )
392
- timeout = aiohttp.ClientTimeout(total=HTTP_TIMEOUT, connect=5)
393
-
394
- # Cliente HTTP otimizado
395
- http_client = httpx.AsyncClient(
396
- timeout=HTTP_TIMEOUT,
397
- limits=httpx.Limits(
398
- max_connections=200, # Aumentado
399
- max_keepalive_connections=50 # Aumentado
400
- ),
401
- http2=True # Ativa HTTP/2 para melhor performance
402
  )
403
 
404
- try:
 
 
 
 
 
 
405
  async with aiohttp.ClientSession(connector=connector, timeout=timeout) as session:
406
- # Fase 1: Busca todos os termos em paralelo
 
 
 
 
407
  search_results = await search_brave_batch(http_client, terms)
408
- print(f"Buscas concluídas em {time.time() - start_time:.2f}s. Iniciando extrações...")
409
 
410
- # Fase 2: Prepara dados para extração em lote
411
- urls_data = []
 
 
412
  for term, results in search_results:
413
  for result in results:
414
- urls_data.append((term, result["url"], result["age"]))
 
 
 
 
415
 
416
- print(f"Processando {len(urls_data)} URLs...")
417
 
418
- # Fase 3: Processa todas as URLs em paralelo
419
- final_results = await process_urls_batch(session, urls_data)
 
420
 
421
- print(f"Extração concluída em {time.time() - start_time:.2f}s. Salvando arquivo...")
422
-
423
- finally:
424
- await http_client.aclose()
425
 
426
- # Fase 4: Cria arquivo temporário assíncrono
 
 
 
427
  result_data = {"results": final_results}
428
- temp_file_info = await create_temp_file(result_data)
429
 
430
- total_time = time.time() - start_time
431
- print(f"Processo completo em {total_time:.2f}s")
432
 
433
  return {
434
  "message": "Dados salvos em arquivo temporário",
435
  "total_results": len(final_results),
436
  "context": context,
437
  "generated_terms": terms,
438
- "file_info": temp_file_info,
439
- "processing_time": f"{total_time:.2f}s"
 
440
  }
441
 
 
442
  @router.get("/download-temp/{file_id}")
443
  async def download_temp_file(file_id: str):
444
  """Endpoint para download do arquivo temporário"""
 
17
  from threading import Timer
18
  from google import genai
19
  from google.genai import types
20
+ import concurrent.futures
21
+ from collections import deque
 
 
22
 
23
  router = APIRouter()
24
 
 
43
  "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
44
  ]
45
 
46
+ BLOCKED_DOMAINS = {"reddit.com", "www.reddit.com", "old.reddit.com",
47
+ "quora.com", "www.quora.com"}
 
 
48
 
49
  MAX_TEXT_LENGTH = 4000
50
+ MAX_CONCURRENT_EXTRACTIONS = 100 # Aumentado drasticamente
51
+ MAX_CONCURRENT_SEARCHES = 50 # Aumentado para pesquisas
 
 
52
 
53
  # Diretório para arquivos temporários
54
  TEMP_DIR = Path("/tmp")
 
57
  # Dicionário para controlar arquivos temporários
58
  temp_files = {}
59
 
60
+ # Pool de threads para operações CPU-intensivas
61
+ THREAD_POOL = concurrent.futures.ThreadPoolExecutor(max_workers=20)
62
 
 
 
63
 
64
  def is_blocked_domain(url: str) -> bool:
65
  try:
66
  host = urlparse(url).netloc.lower()
67
+ return any(host == b or host.endswith("." + b) for b in BLOCKED_DOMAINS)
 
 
 
 
 
 
 
68
  except Exception:
69
  return False
70
 
71
+
72
  def clamp_text(text: str) -> str:
73
+ if not text:
74
+ return ""
75
+ if len(text) > MAX_TEXT_LENGTH:
76
+ return text[:MAX_TEXT_LENGTH]
77
+ return text
78
+
79
 
80
  def get_realistic_headers() -> Dict[str, str]:
81
  return {
 
84
  "Accept-Language": "en-US,en;q=0.7,pt-BR;q=0.6",
85
  "Connection": "keep-alive",
86
  "Accept-Encoding": "gzip, deflate, br",
87
+ "Cache-Control": "no-cache",
88
+ "Pragma": "no-cache",
89
  }
90
 
91
+
92
  def delete_temp_file(file_id: str, file_path: Path):
93
  """Remove arquivo temporário após expiração"""
94
  try:
 
99
  except Exception as e:
100
  print(f"Erro ao remover arquivo temporário: {e}")
101
 
102
+
103
+ def create_temp_file(data: Dict[str, Any]) -> Dict[str, str]:
104
+ """Cria arquivo temporário e agenda sua remoção"""
105
  file_id = str(uuid.uuid4())
106
  file_path = TEMP_DIR / f"fontes_{file_id}.txt"
107
 
108
+ # Salva o JSON no arquivo
109
+ with open(file_path, 'w', encoding='utf-8') as f:
110
+ json.dump(data, f, ensure_ascii=False, indent=2)
111
 
112
+ # Agenda remoção em 24 horas (86400 segundos)
113
  timer = Timer(86400, delete_temp_file, args=[file_id, file_path])
114
  timer.start()
115
 
 
126
  "expires_in_hours": 24
127
  }
128
 
129
+
130
+ def extract_text_cpu_intensive(html_content: str) -> str:
131
+ """Função CPU-intensiva para extrair texto (roda em thread separada)"""
132
+ try:
133
+ if re.search(r"(paywall|subscribe|metered|registration|captcha|access denied)", html_content, re.I):
134
+ return ""
135
+
136
+ extracted = trafilatura.extract(html_content) or ""
137
+ extracted = extracted.strip()
138
+ if extracted and len(extracted) > 100:
139
+ return clamp_text(extracted)
140
+ except Exception:
141
+ pass
142
+ return ""
143
+
144
+
145
  async def generate_search_terms(context: str) -> List[str]:
146
  """Gera termos de pesquisa usando o modelo Gemini"""
147
  try:
 
188
  ]
189
 
190
  generate_content_config = types.GenerateContentConfig(
191
+ thinking_config=types.ThinkingConfig(
192
+ thinking_budget=0,
193
+ ),
194
  )
195
 
196
  # Coletamos toda a resposta em stream
 
205
 
206
  # Tenta extrair o JSON da resposta
207
  try:
208
+ # Remove possíveis ```json e ``` da resposta
209
  clean_response = full_response.strip()
210
  if clean_response.startswith("```json"):
211
  clean_response = clean_response[7:]
 
213
  clean_response = clean_response[:-3]
214
  clean_response = clean_response.strip()
215
 
216
+ # Parse do JSON
217
+ response_data = json.loads(clean_response)
218
  terms = response_data.get("terms", [])
219
 
220
+ # Validação básica
221
  if not isinstance(terms, list):
222
  raise ValueError("Terms deve ser uma lista")
223
 
224
+ return terms[:20] # Garante máximo de 20 termos
225
 
226
+ except (json.JSONDecodeError, ValueError) as e:
227
  print(f"Erro ao parsear resposta do Gemini: {e}")
228
+ print(f"Resposta recebida: {full_response}")
229
+ # Retorna uma lista vazia em caso de erro
230
  return []
231
 
232
  except Exception as e:
233
  print(f"Erro ao gerar termos de pesquisa: {str(e)}")
234
  return []
235
 
236
+
237
  async def search_brave_batch(client: httpx.AsyncClient, terms: List[str]) -> List[Tuple[str, List[Dict[str, str]]]]:
238
+ """Realiza múltiplas pesquisas em paralelo com batch otimizado"""
 
239
 
240
  async def search_single_term(term: str) -> Tuple[str, List[Dict[str, str]]]:
241
+ params = {"q": term, "count": 10, "safesearch": "off", "summary": "false"}
242
+
243
+ try:
244
+ resp = await client.get(BRAVE_SEARCH_URL, headers=BRAVE_HEADERS, params=params)
245
+ if resp.status_code != 200:
246
+ return term, []
247
+
248
+ data = resp.json()
249
+ results = []
250
 
251
+ if "web" in data and "results" in data["web"]:
252
+ for item in data["web"]["results"]:
253
+ url = item.get("url")
254
+ age = item.get("age", "Unknown")
255
+
256
+ if url and not is_blocked_domain(url):
257
+ results.append({"url": url, "age": age, "term": term})
258
 
259
+ return term, results
260
+ except Exception:
261
+ return term, []
262
+
263
+ # Executa todas as pesquisas em paralelo
264
+ search_tasks = [search_single_term(term) for term in terms]
265
+ results = await asyncio.gather(*search_tasks, return_exceptions=True)
266
+
267
+ # Filtra apenas resultados válidos
268
+ valid_results = [r for r in results if isinstance(r, tuple)]
269
+ return valid_results
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
270
 
 
 
 
 
 
 
 
 
 
 
 
 
271
 
272
+ async def extract_content_ultra_fast(session: aiohttp.ClientSession, url_data: Dict[str, str]) -> Optional[Dict[str, Any]]:
273
+ """Extração de conteúdo ultra-rápida com fallbacks otimizados"""
274
+ url = url_data["url"]
275
+ term = url_data["term"]
276
+ age = url_data["age"]
277
 
278
+ # Primeira tentativa: Newspaper3k (mais rápido para muitos sites)
279
  try:
280
+ loop = asyncio.get_event_loop()
281
+
282
+ # Executa newspaper em thread separada
283
+ def newspaper_extract():
284
+ try:
285
+ art = Article(url)
286
+ art.config.browser_user_agent = random.choice(USER_AGENTS)
287
+ art.config.request_timeout = 5 # Reduzido para 5s
288
+ art.config.number_threads = 1
289
+ art.download()
290
+ art.parse()
291
+ text = (art.text or "").strip()
292
+ return text if text and len(text) > 100 else None
293
+ except Exception:
294
+ return None
295
+
296
+ # Tenta newspaper em paralelo com download HTTP
297
+ newspaper_task = loop.run_in_executor(THREAD_POOL, newspaper_extract)
298
+
299
+ # Download HTTP em paralelo
300
  headers = get_realistic_headers()
301
+
302
+ try:
303
+ async with session.get(url, headers=headers, timeout=8) as resp: # Timeout reduzido
304
+ if resp.status != 200:
305
+ # Se HTTP falhar, espera newspaper
306
+ newspaper_result = await newspaper_task
307
+ if newspaper_result:
308
+ return {
309
+ "term": term,
310
+ "age": age,
311
+ "url": url,
312
+ "text": clamp_text(newspaper_result),
313
+ "method": "newspaper"
314
+ }
315
+ return None
316
+
317
  html = await resp.text()
318
 
319
+ # Executa extração de texto em thread separada
320
+ text_extraction_task = loop.run_in_executor(
321
+ THREAD_POOL,
322
+ extract_text_cpu_intensive,
323
+ html
324
+ )
325
+
326
+ # Aguarda tanto newspaper quanto trafilatura, pega o primeiro que terminar
327
+ done, pending = await asyncio.wait(
328
+ [newspaper_task, text_extraction_task],
329
+ return_when=asyncio.FIRST_COMPLETED,
330
+ timeout=10
331
+ )
332
+
333
+ # Cancela tarefas pendentes
334
+ for task in pending:
335
+ task.cancel()
336
+
337
+ # Processa resultados
338
+ results = []
339
+ for task in done:
340
  try:
341
+ result = await task
342
+ if result:
343
+ results.append(result)
344
+ except Exception:
345
+ continue
346
+
347
+ # Retorna o melhor resultado
348
+ if results:
349
+ # Prioriza o texto mais longo
350
+ best_text = max(results, key=len)
351
+ return {
352
+ "term": term,
353
+ "age": age,
354
+ "url": url,
355
+ "text": clamp_text(best_text),
356
+ "method": "hybrid"
357
+ }
358
 
359
+ except asyncio.TimeoutError:
360
+ # Se HTTP der timeout, ainda tenta newspaper
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
361
  try:
362
+ newspaper_result = await asyncio.wait_for(newspaper_task, timeout=5)
363
+ if newspaper_result:
 
 
 
364
  return {
365
  "term": term,
366
  "age": age,
367
  "url": url,
368
+ "text": clamp_text(newspaper_result),
369
+ "method": "newspaper_fallback"
370
  }
371
+ except asyncio.TimeoutError:
372
+ pass
373
+
374
+ except Exception:
375
+ pass
 
376
 
377
+ return None
378
+
379
+
380
+ async def process_urls_ultra_parallel(session: aiohttp.ClientSession, all_urls: List[Dict[str, str]], used_urls: Set[str]) -> List[Dict[str, Any]]:
381
+ """Processa URLs com máximo paralelismo"""
382
 
383
+ # Remove URLs duplicadas imediatamente
384
+ unique_urls = []
385
+ local_used = set()
386
 
387
+ for url_data in all_urls:
388
+ url = url_data["url"]
389
+ if url not in used_urls and url not in local_used:
390
+ unique_urls.append(url_data)
391
+ local_used.add(url)
392
+ used_urls.add(url) # Adiciona ao set global imediatamente
393
 
394
+ if not unique_urls:
395
+ return []
396
 
397
+ print(f"Processando {len(unique_urls)} URLs únicas em paralelo...")
398
+
399
+ # Cria semáforo com limite alto
400
+ semaphore = asyncio.Semaphore(MAX_CONCURRENT_EXTRACTIONS)
401
+
402
+ async def extract_with_semaphore(url_data):
403
+ async with semaphore:
404
+ return await extract_content_ultra_fast(session, url_data)
405
+
406
+ # Executa TODAS as extrações em paralelo
407
+ tasks = [extract_with_semaphore(url_data) for url_data in unique_urls]
408
+
409
+ # Aguarda todas as tarefas com timeout global
410
+ try:
411
+ results = await asyncio.wait_for(
412
+ asyncio.gather(*tasks, return_exceptions=True),
413
+ timeout=30 # 30 segundos para todas as extrações
414
+ )
415
+
416
+ # Filtra resultados válidos
417
+ valid_results = [
418
+ r for r in results
419
+ if r is not None and not isinstance(r, Exception) and isinstance(r, dict)
420
+ ]
421
+
422
+ print(f"Extraídos {len(valid_results)} artigos de {len(unique_urls)} URLs")
423
+ return valid_results
424
+
425
+ except asyncio.TimeoutError:
426
+ print("Timeout global atingido, retornando resultados parciais...")
427
+ # Em caso de timeout, pega os resultados que já terminaram
428
+ completed_tasks = [task for task in tasks if task.done()]
429
+ valid_results = []
430
+
431
+ for task in completed_tasks:
432
+ try:
433
+ result = task.result()
434
+ if result is not None and isinstance(result, dict):
435
+ valid_results.append(result)
436
+ except Exception:
437
+ continue
438
+
439
+ return valid_results
440
+
441
 
442
  @router.post("/search-terms")
443
  async def search_terms(payload: Dict[str, str] = Body(...)) -> Dict[str, Any]:
 
 
444
  context = payload.get("context")
445
  if not context or not isinstance(context, str):
446
  raise HTTPException(status_code=400, detail="Campo 'context' é obrigatório e deve ser uma string.")
 
448
  if len(context.strip()) == 0:
449
  raise HTTPException(status_code=400, detail="Campo 'context' não pode estar vazio.")
450
 
451
+ start_time = time.time()
452
+ print(f"Iniciando busca para contexto: {context[:100]}...")
453
+
454
  # Gera os termos de pesquisa usando o Gemini
455
  terms = await generate_search_terms(context)
456
 
457
  if not terms:
458
  raise HTTPException(status_code=500, detail="Não foi possível gerar termos de pesquisa válidos.")
 
 
459
 
460
+ print(f"Gerados {len(terms)} termos em {time.time() - start_time:.2f}s")
461
+
462
+ used_urls: Set[str] = set()
463
+
464
+ # Configurações otimizadas para máxima velocidade
465
  connector = aiohttp.TCPConnector(
466
+ limit=200, # Dobrou o limite total
467
+ limit_per_host=50, # Aumentou limite por host
468
+ keepalive_timeout=30,
469
+ enable_cleanup_closed=True,
470
+ force_close=False,
471
+ ttl_dns_cache=300, # Cache DNS por 5 minutos
472
  )
473
+
474
+ timeout = aiohttp.ClientTimeout(
475
+ total=25, # Timeout total reduzido
476
+ connect=8, # Timeout de conexão reduzido
477
+ sock_read=8 # Timeout de leitura reduzido
 
 
 
 
 
478
  )
479
 
480
+ # Configurações HTTPX otimizadas
481
+ http_limits = httpx.Limits(
482
+ max_connections=MAX_CONCURRENT_SEARCHES,
483
+ max_keepalive_connections=40
484
+ )
485
+
486
+ async with httpx.AsyncClient(timeout=12.0, limits=http_limits) as http_client:
487
  async with aiohttp.ClientSession(connector=connector, timeout=timeout) as session:
488
+
489
+ # Fase 1: Busca em lote (todas as pesquisas em paralelo)
490
+ print("Fase 1: Executando pesquisas em paralelo...")
491
+ search_start = time.time()
492
+
493
  search_results = await search_brave_batch(http_client, terms)
 
494
 
495
+ print(f"Pesquisas concluídas em {time.time() - search_start:.2f}s")
496
+
497
+ # Fase 2: Coleta e organiza todas as URLs
498
+ all_urls = []
499
  for term, results in search_results:
500
  for result in results:
501
+ all_urls.append({
502
+ "url": result["url"],
503
+ "age": result["age"],
504
+ "term": term
505
+ })
506
 
507
+ print(f"Total de URLs coletadas: {len(all_urls)}")
508
 
509
+ # Fase 3: Extração ultra-paralela
510
+ print("Fase 2: Extraindo conteúdo em máximo paralelismo...")
511
+ extraction_start = time.time()
512
 
513
+ final_results = await process_urls_ultra_parallel(session, all_urls, used_urls)
514
+
515
+ print(f"Extração concluída em {time.time() - extraction_start:.2f}s")
 
516
 
517
+ total_time = time.time() - start_time
518
+ print(f"Processo completo em {total_time:.2f}s - {len(final_results)} artigos extraídos")
519
+
520
+ # Cria o JSON final
521
  result_data = {"results": final_results}
 
522
 
523
+ # Cria arquivo temporário
524
+ temp_file_info = create_temp_file(result_data)
525
 
526
  return {
527
  "message": "Dados salvos em arquivo temporário",
528
  "total_results": len(final_results),
529
  "context": context,
530
  "generated_terms": terms,
531
+ "processing_time_seconds": round(total_time, 2),
532
+ "urls_processed": len(all_urls),
533
+ "file_info": temp_file_info
534
  }
535
 
536
+
537
  @router.get("/download-temp/{file_id}")
538
  async def download_temp_file(file_id: str):
539
  """Endpoint para download do arquivo temporário"""