Spaces:
Running
Running
Update routers/inference.py
Browse files- 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
|
|
|
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 |
-
|
|
|
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
|
24 |
|
25 |
class NewsResponse(BaseModel):
|
26 |
title: str
|
27 |
subhead: str
|
28 |
content: str
|
29 |
-
title_instagram: str
|
30 |
-
content_instagram: str
|
|
|
31 |
|
32 |
-
# Referência ao diretório de arquivos temporários
|
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
|
323 |
-
Se file_id não for fornecido, gera automaticamente as fontes
|
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 |
-
|
|
|
|
|
332 |
if not news.file_id:
|
333 |
-
logger.info("File ID não fornecido
|
334 |
-
|
335 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
336 |
else:
|
337 |
-
|
338 |
-
|
|
|
|
|
|
|
339 |
|
340 |
-
# Carregar arquivo de fontes
|
341 |
-
sources_content =
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
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:
|