habulaj commited on
Commit
187b405
·
verified ·
1 Parent(s): e6ffd02

Update routers/inference.py

Browse files
Files changed (1) hide show
  1. routers/inference.py +124 -132
routers/inference.py CHANGED
@@ -2,7 +2,8 @@ import os
2
  import logging
3
  import json
4
  import requests
5
- import httpx
 
6
  from fastapi import APIRouter, HTTPException
7
  from pydantic import BaseModel
8
  from google import genai
@@ -11,7 +12,8 @@ from datetime import datetime
11
  from zoneinfo import ZoneInfo
12
  import locale
13
  import re
14
- from pathlib import Path
 
15
 
16
  # Configurar logging
17
  logger = logging.getLogger(__name__)
@@ -20,18 +22,87 @@ router = APIRouter()
20
 
21
  class NewsRequest(BaseModel):
22
  content: str
23
- file_id: str = None # Agora é opcional
24
 
25
  class NewsResponse(BaseModel):
26
  title: str
27
  subhead: str
28
  content: str
29
- title_instagram: str # Campo Instagram title
30
- content_instagram: str # Campo Instagram description
 
31
 
32
- # Referência ao diretório de arquivos temporários (deve ser o mesmo do outro módulo)
33
  TEMP_DIR = Path("/tmp")
34
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
  def get_brazilian_date_string():
36
  """
37
  Retorna a data atual formatada em português brasileiro.
@@ -109,58 +180,6 @@ def get_brazilian_date_string():
109
  date_string = now.strftime("%d de %B de %Y")
110
  return date_string
111
 
112
- async def generate_sources_from_content(content: str) -> str:
113
- """
114
- Chama o endpoint de busca de termos para gerar fontes baseadas no conteúdo.
115
- Retorna o file_id do arquivo gerado.
116
- """
117
- try:
118
- # Configurar a URL base - ajuste conforme sua configuração
119
- base_url = os.getenv("BASE_URL", "http://localhost:8000")
120
- search_url = f"{base_url}/search-terms"
121
-
122
- # Fazer chamada HTTP para o endpoint de busca
123
- payload = {"context": content}
124
-
125
- async with httpx.AsyncClient(timeout=120.0) as client:
126
- response = await client.post(search_url, json=payload)
127
-
128
- if response.status_code != 200:
129
- logger.error(f"Erro na busca de termos: {response.status_code} - {response.text}")
130
- raise HTTPException(
131
- status_code=500,
132
- detail=f"Erro ao gerar fontes: {response.status_code}"
133
- )
134
-
135
- result = response.json()
136
- file_info = result.get("file_info", {})
137
- file_id = file_info.get("file_id")
138
-
139
- if not file_id:
140
- logger.error("File ID não encontrado na resposta da busca")
141
- raise HTTPException(
142
- status_code=500,
143
- detail="Erro ao obter ID do arquivo de fontes"
144
- )
145
-
146
- logger.info(f"Fontes geradas com sucesso. File ID: {file_id}")
147
- logger.info(f"Total de resultados encontrados: {result.get('total_results', 0)}")
148
-
149
- return file_id
150
-
151
- except httpx.RequestError as e:
152
- logger.error(f"Erro de conexão ao gerar fontes: {str(e)}")
153
- raise HTTPException(
154
- status_code=503,
155
- detail="Serviço de busca indisponível"
156
- )
157
- except Exception as e:
158
- logger.error(f"Erro inesperado ao gerar fontes: {str(e)}")
159
- raise HTTPException(
160
- status_code=500,
161
- detail=f"Erro interno ao gerar fontes: {str(e)}"
162
- )
163
-
164
  def load_sources_file(file_id: str) -> str:
165
  """
166
  Carrega o arquivo de fontes pelo ID do arquivo temporário.
@@ -272,55 +291,13 @@ def extract_text_from_response(response):
272
  except Exception as e:
273
  logger.error(f"Erro ao processar parts do candidate {i}: {e}")
274
 
275
- # Método 3: Tentar usar método _get_text() se existir
276
- try:
277
- if hasattr(response, '_get_text'):
278
- text_content = response._get_text()
279
- if text_content:
280
- logger.info(f"Texto extraído via _get_text(): {len(text_content)} caracteres")
281
- return text_content
282
- except Exception as e:
283
- logger.error(f"Erro ao usar _get_text(): {e}")
284
-
285
- # Método 4: Debug - tentar inspecionar a estrutura real
286
- try:
287
- logger.info("Tentando debug da estrutura:")
288
- if hasattr(response, 'candidates') and response.candidates:
289
- candidate = response.candidates[0]
290
- logger.info(f"Primeiro candidate: {type(candidate)}")
291
- logger.info(f"Atributos do candidate: {dir(candidate)}")
292
-
293
- if hasattr(candidate, 'content'):
294
- content = candidate.content
295
- logger.info(f"Content: {type(content)}")
296
- logger.info(f"Atributos do content: {dir(content)}")
297
-
298
- if hasattr(content, 'parts'):
299
- logger.info(f"Parts: {type(content.parts)}")
300
- try:
301
- parts_list = list(content.parts)
302
- if parts_list:
303
- first_part = parts_list[0]
304
- logger.info(f"Primeiro part: {type(first_part)}")
305
- logger.info(f"Atributos do part: {dir(first_part)}")
306
- except Exception as e:
307
- logger.error(f"Erro ao inspecionar parts: {e}")
308
- except Exception as e:
309
- logger.error(f"Erro no debug da estrutura: {e}")
310
-
311
  return ""
312
 
313
- def extract_sources_from_response(response):
314
- """
315
- Função removida - sources não são mais necessárias.
316
- """
317
- return []
318
-
319
  @router.post("/rewrite-news", response_model=NewsResponse)
320
  async def rewrite_news(news: NewsRequest):
321
  """
322
- Endpoint para reescrever notícias usando o modelo Gemini com arquivo de fontes.
323
- Se file_id não for fornecido, gera automaticamente as fontes baseadas no conteúdo.
324
  """
325
  try:
326
  # Verificar API key
@@ -328,17 +305,43 @@ async def rewrite_news(news: NewsRequest):
328
  if not api_key:
329
  raise HTTPException(status_code=500, detail="API key não configurada")
330
 
331
- # Se file_id não foi fornecido, gera as fontes automaticamente
 
 
332
  if not news.file_id:
333
- logger.info("File ID não fornecido. Gerando fontes automaticamente...")
334
- file_id = await generate_sources_from_content(news.content)
335
- logger.info(f"Fontes geradas automaticamente. File ID: {file_id}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
336
  else:
337
- file_id = news.file_id
338
- logger.info(f"Usando file_id fornecido: {file_id}")
 
 
 
339
 
340
- # Carregar arquivo de fontes pelo ID
341
- sources_content = load_sources_file(file_id)
 
 
 
 
 
 
 
342
 
343
  client = genai.Client(api_key=api_key)
344
  model = "gemini-2.5-pro"
@@ -346,6 +349,7 @@ async def rewrite_news(news: NewsRequest):
346
  # Obter data formatada
347
  date_string = get_brazilian_date_string()
348
 
 
349
  # Instruções do sistema
350
  SYSTEM_INSTRUCTIONS = f"""
351
  Você é um jornalista brasileiro, escrevendo para portais digitais. Sua missão é transformar notícias internacionais em matérias originais, detalhadas e atualizadas para o público brasileiro. Sempre use a notícia-base como ponto de partida, mas consulte o arquivo fontes.txt para extrair todas as informações relevantes, complementando fatos, contexto, dados e antecedentes. Não invente informações; na dúvida, não insira.
@@ -540,6 +544,13 @@ The 48th annual Kennedy Center Honors, set to air on the CBS network and stream
540
  config=config
541
  )
542
 
 
 
 
 
 
 
 
543
  logger.info("Resposta do modelo recebida com sucesso")
544
 
545
  # Extrair texto
@@ -547,20 +558,9 @@ The 48th annual Kennedy Center Honors, set to air on the CBS network and stream
547
 
548
  logger.info(f"Texto extraído: {len(response_text) if response_text else 0} caracteres")
549
 
550
- # Log da resposta bruta completa para debug
551
- logger.info("=== RESPOSTA BRUTA DA API ===")
552
- logger.info(f"Resposta completa: {response_text}")
553
- logger.info("=== FIM RESPOSTA BRUTA ===")
554
-
555
  # Verificar se o texto está vazio
556
  if not response_text or response_text.strip() == "":
557
  logger.error("Texto extraído está vazio")
558
- # Debug adicional: tentar logar a resposta crua
559
- try:
560
- logger.error(f"Resposta crua (primeiros 500 chars): {str(response)[:500]}")
561
- except:
562
- logger.error("Não foi possível converter resposta para string")
563
-
564
  raise HTTPException(
565
  status_code=500,
566
  detail="Modelo não retornou conteúdo válido"
@@ -583,22 +583,13 @@ The 48th annual Kennedy Center Honors, set to air on the CBS network and stream
583
  else:
584
  content = "Conteúdo não encontrado"
585
 
586
- # Campos do Instagram com debug adicional
587
  insta_title_match = re.search(r"<instagram_title>(.*?)</instagram_title>", response_text, re.DOTALL)
588
  title_instagram = insta_title_match.group(1).strip() if insta_title_match else "Título Instagram não encontrado"
589
 
590
  insta_desc_match = re.search(r"<instagram_description>(.*?)</instagram_description>", response_text, re.DOTALL)
591
  content_instagram = insta_desc_match.group(1).strip() if insta_desc_match else "Descrição Instagram não encontrada"
592
 
593
- # Debug específico para Instagram fields
594
- logger.info(f"Instagram Title Match: {bool(insta_title_match)}")
595
- logger.info(f"Instagram Description Match: {bool(insta_desc_match)}")
596
-
597
- if insta_title_match:
598
- logger.info(f"Instagram Title encontrado: {title_instagram[:100]}...")
599
- if insta_desc_match:
600
- logger.info(f"Instagram Description encontrado: {content_instagram[:100]}...")
601
-
602
  logger.info(f"Processamento concluído com sucesso - Título: {title[:50]}...")
603
 
604
  return NewsResponse(
@@ -606,7 +597,8 @@ The 48th annual Kennedy Center Honors, set to air on the CBS network and stream
606
  subhead=subhead,
607
  content=content,
608
  title_instagram=title_instagram,
609
- content_instagram=content_instagram
 
610
  )
611
 
612
  except HTTPException:
 
2
  import logging
3
  import json
4
  import requests
5
+ import importlib.util
6
+ from pathlib import Path
7
  from fastapi import APIRouter, HTTPException
8
  from pydantic import BaseModel
9
  from google import genai
 
12
  from zoneinfo import ZoneInfo
13
  import locale
14
  import re
15
+ import asyncio
16
+ from typing import Optional, Dict, Any
17
 
18
  # Configurar logging
19
  logger = logging.getLogger(__name__)
 
22
 
23
  class NewsRequest(BaseModel):
24
  content: str
25
+ file_id: str = None # Agora opcional
26
 
27
  class NewsResponse(BaseModel):
28
  title: str
29
  subhead: str
30
  content: str
31
+ title_instagram: str
32
+ content_instagram: str
33
+ sources_info: Optional[Dict[str, Any]] = None # Informações das fontes geradas
34
 
35
+ # Referência ao diretório de arquivos temporários
36
  TEMP_DIR = Path("/tmp")
37
 
38
+ def load_searchterm_module():
39
+ """Carrega o módulo searchterm.py dinamicamente"""
40
+ try:
41
+ # Procura o arquivo searchterm.py em diferentes locais
42
+ searchterm_path = Path(__file__).parent / "searchterm.py"
43
+
44
+ if not searchterm_path.exists():
45
+ # Tenta outros caminhos possíveis
46
+ possible_paths = [
47
+ Path(__file__).parent.parent / "searchterm.py",
48
+ Path("./searchterm.py"),
49
+ Path("../searchterm.py")
50
+ ]
51
+
52
+ for path in possible_paths:
53
+ if path.exists():
54
+ searchterm_path = path
55
+ break
56
+ else:
57
+ logger.error("searchterm.py não encontrado em nenhum dos caminhos")
58
+ return None
59
+
60
+ spec = importlib.util.spec_from_file_location("searchterm", searchterm_path)
61
+ searchterm_module = importlib.util.module_from_spec(spec)
62
+ spec.loader.exec_module(searchterm_module)
63
+
64
+ logger.info(f"Módulo searchterm.py carregado com sucesso: {searchterm_path}")
65
+ return searchterm_module
66
+ except Exception as e:
67
+ logger.error(f"Erro ao carregar searchterm.py: {str(e)}")
68
+ return None
69
+
70
+ # Carrega o módulo na inicialização
71
+ searchterm_module = load_searchterm_module()
72
+
73
+ async def generate_sources_from_content(content: str) -> Optional[str]:
74
+ """
75
+ Gera fontes usando o módulo searchterm baseado no conteúdo da notícia
76
+ """
77
+ try:
78
+ if not searchterm_module:
79
+ logger.error("Módulo searchterm não carregado")
80
+ return None
81
+
82
+ logger.info(f"Gerando fontes para conteúdo: {len(content)} caracteres")
83
+
84
+ # Prepara o payload para o searchterm
85
+ payload = {"context": content}
86
+
87
+ # Chama a função search_terms do módulo searchterm
88
+ # Simula uma requisição FastAPI criando um objeto com o método necessário
89
+ result = await searchterm_module.search_terms(payload)
90
+
91
+ if result and "file_info" in result:
92
+ file_id = result["file_info"]["file_id"]
93
+ logger.info(f"Fontes geradas com sucesso. File ID: {file_id}")
94
+ logger.info(f"Total de resultados: {result.get('total_results', 0)}")
95
+ logger.info(f"Termos gerados: {len(result.get('generated_terms', []))}")
96
+
97
+ return file_id
98
+ else:
99
+ logger.error("Resultado inválido do searchterm")
100
+ return None
101
+
102
+ except Exception as e:
103
+ logger.error(f"Erro ao gerar fontes: {str(e)}")
104
+ return None
105
+
106
  def get_brazilian_date_string():
107
  """
108
  Retorna a data atual formatada em português brasileiro.
 
180
  date_string = now.strftime("%d de %B de %Y")
181
  return date_string
182
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
183
  def load_sources_file(file_id: str) -> str:
184
  """
185
  Carrega o arquivo de fontes pelo ID do arquivo temporário.
 
291
  except Exception as e:
292
  logger.error(f"Erro ao processar parts do candidate {i}: {e}")
293
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
294
  return ""
295
 
 
 
 
 
 
 
296
  @router.post("/rewrite-news", response_model=NewsResponse)
297
  async def rewrite_news(news: NewsRequest):
298
  """
299
+ Endpoint para reescrever notícias usando o modelo Gemini.
300
+ Se file_id não for fornecido, gera automaticamente as fontes usando o conteúdo.
301
  """
302
  try:
303
  # Verificar API key
 
305
  if not api_key:
306
  raise HTTPException(status_code=500, detail="API key não configurada")
307
 
308
+ sources_info = None
309
+
310
+ # Se file_id não foi fornecido, gera fontes automaticamente
311
  if not news.file_id:
312
+ logger.info("File ID não fornecido, gerando fontes automaticamente...")
313
+ generated_file_id = await generate_sources_from_content(news.content)
314
+
315
+ if generated_file_id:
316
+ news.file_id = generated_file_id
317
+ sources_info = {
318
+ "generated": True,
319
+ "file_id": generated_file_id,
320
+ "message": "Fontes geradas automaticamente a partir do conteúdo"
321
+ }
322
+ logger.info(f"Fontes geradas automaticamente. File ID: {generated_file_id}")
323
+ else:
324
+ logger.warning("Não foi possível gerar fontes automaticamente, prosseguindo sem fontes")
325
+ sources_info = {
326
+ "generated": False,
327
+ "message": "Não foi possível gerar fontes automaticamente"
328
+ }
329
  else:
330
+ sources_info = {
331
+ "generated": False,
332
+ "file_id": news.file_id,
333
+ "message": "Usando file_id fornecido"
334
+ }
335
 
336
+ # Carregar arquivo de fontes se disponível
337
+ sources_content = ""
338
+ if news.file_id:
339
+ try:
340
+ sources_content = load_sources_file(news.file_id)
341
+ logger.info(f"Fontes carregadas: {len(sources_content)} caracteres")
342
+ except HTTPException as e:
343
+ logger.warning(f"Erro ao carregar fontes: {e.detail}")
344
+ sources_content = ""
345
 
346
  client = genai.Client(api_key=api_key)
347
  model = "gemini-2.5-pro"
 
349
  # Obter data formatada
350
  date_string = get_brazilian_date_string()
351
 
352
+ # Instruções do sistema (suas instruções originais aqui)
353
  # Instruções do sistema
354
  SYSTEM_INSTRUCTIONS = f"""
355
  Você é um jornalista brasileiro, escrevendo para portais digitais. Sua missão é transformar notícias internacionais em matérias originais, detalhadas e atualizadas para o público brasileiro. Sempre use a notícia-base como ponto de partida, mas consulte o arquivo fontes.txt para extrair todas as informações relevantes, complementando fatos, contexto, dados e antecedentes. Não invente informações; na dúvida, não insira.
 
544
  config=config
545
  )
546
 
547
+ # Gerar conteúdo
548
+ response = client.models.generate_content(
549
+ model=model,
550
+ contents=contents,
551
+ config=config
552
+ )
553
+
554
  logger.info("Resposta do modelo recebida com sucesso")
555
 
556
  # Extrair texto
 
558
 
559
  logger.info(f"Texto extraído: {len(response_text) if response_text else 0} caracteres")
560
 
 
 
 
 
 
561
  # Verificar se o texto está vazio
562
  if not response_text or response_text.strip() == "":
563
  logger.error("Texto extraído está vazio")
 
 
 
 
 
 
564
  raise HTTPException(
565
  status_code=500,
566
  detail="Modelo não retornou conteúdo válido"
 
583
  else:
584
  content = "Conteúdo não encontrado"
585
 
586
+ # Campos do Instagram
587
  insta_title_match = re.search(r"<instagram_title>(.*?)</instagram_title>", response_text, re.DOTALL)
588
  title_instagram = insta_title_match.group(1).strip() if insta_title_match else "Título Instagram não encontrado"
589
 
590
  insta_desc_match = re.search(r"<instagram_description>(.*?)</instagram_description>", response_text, re.DOTALL)
591
  content_instagram = insta_desc_match.group(1).strip() if insta_desc_match else "Descrição Instagram não encontrada"
592
 
 
 
 
 
 
 
 
 
 
593
  logger.info(f"Processamento concluído com sucesso - Título: {title[:50]}...")
594
 
595
  return NewsResponse(
 
597
  subhead=subhead,
598
  content=content,
599
  title_instagram=title_instagram,
600
+ content_instagram=content_instagram,
601
+ sources_info=sources_info
602
  )
603
 
604
  except HTTPException: