Spaces:
Restarting
Restarting
Update routers/inference.py
Browse files- routers/inference.py +61 -297
routers/inference.py
CHANGED
@@ -1,6 +1,7 @@
|
|
1 |
import os
|
2 |
import logging
|
3 |
import json
|
|
|
4 |
from fastapi import APIRouter, HTTPException
|
5 |
from pydantic import BaseModel
|
6 |
from google import genai
|
@@ -17,6 +18,7 @@ router = APIRouter()
|
|
17 |
|
18 |
class NewsRequest(BaseModel):
|
19 |
content: str
|
|
|
20 |
|
21 |
class NewsResponse(BaseModel):
|
22 |
title: str
|
@@ -32,12 +34,12 @@ def get_brazilian_date_string():
|
|
32 |
try:
|
33 |
# Tenta configurar o locale brasileiro
|
34 |
locale_variants = [
|
35 |
-
'pt_BR.UTF-8',
|
36 |
-
'pt_BR.utf8',
|
37 |
-
'pt_BR',
|
38 |
-
'Portuguese_Brazil.1252',
|
39 |
-
'Portuguese_Brazil',
|
40 |
-
'pt_BR.ISO8859-1',
|
41 |
]
|
42 |
|
43 |
locale_set = False
|
@@ -45,20 +47,16 @@ def get_brazilian_date_string():
|
|
45 |
try:
|
46 |
locale.setlocale(locale.LC_TIME, loc)
|
47 |
locale_set = True
|
48 |
-
logger.info(f"Locale configurado com sucesso: {loc}")
|
49 |
break
|
50 |
except locale.Error:
|
51 |
-
logger.debug(f"Locale {loc} não disponível")
|
52 |
continue
|
53 |
|
54 |
if not locale_set:
|
55 |
-
|
56 |
-
locale.setlocale(locale.LC_TIME, '') # Usa locale padrão do sistema
|
57 |
|
58 |
-
# Obtém a data atual no fuso horário de São Paulo
|
59 |
now = datetime.now(ZoneInfo("America/Sao_Paulo"))
|
60 |
|
61 |
-
# Dicionários para tradução manual (fallback
|
62 |
meses = {
|
63 |
1: 'janeiro', 2: 'fevereiro', 3: 'março', 4: 'abril',
|
64 |
5: 'maio', 6: 'junho', 7: 'julho', 8: 'agosto',
|
@@ -71,139 +69,100 @@ def get_brazilian_date_string():
|
|
71 |
}
|
72 |
|
73 |
try:
|
74 |
-
# Tenta usar strftime com locale configurado
|
75 |
if locale_set:
|
76 |
-
# Para sistemas Unix/Linux usa %-d (sem zero à esquerda)
|
77 |
try:
|
78 |
date_string = now.strftime("%-d de %B de %Y (%A)")
|
79 |
except ValueError:
|
80 |
-
# Para Windows usa %#d
|
81 |
try:
|
82 |
date_string = now.strftime("%#d de %B de %Y (%A)")
|
83 |
except ValueError:
|
84 |
-
# Fallback: usa %d e remove zero manualmente se necessário
|
85 |
date_string = now.strftime("%d de %B de %Y (%A)")
|
86 |
if date_string.startswith('0'):
|
87 |
date_string = date_string[1:]
|
88 |
|
89 |
-
# Força dia da semana em minúscula
|
90 |
date_string = date_string.replace(date_string.split('(')[1].split(')')[0],
|
91 |
date_string.split('(')[1].split(')')[0].lower())
|
92 |
else:
|
93 |
-
# Fallback manual completo
|
94 |
dia = now.day
|
95 |
mes = meses[now.month]
|
96 |
ano = now.year
|
97 |
dia_semana = dias_semana[now.weekday()]
|
98 |
date_string = f"{dia} de {mes} de {ano} ({dia_semana})"
|
99 |
|
100 |
-
except Exception
|
101 |
-
logger.warning(f"Erro ao formatar data com locale: {e}")
|
102 |
-
# Fallback manual completo em caso de erro
|
103 |
dia = now.day
|
104 |
mes = meses[now.month]
|
105 |
ano = now.year
|
106 |
dia_semana = dias_semana[now.weekday()]
|
107 |
date_string = f"{dia} de {mes} de {ano} ({dia_semana})"
|
108 |
|
109 |
-
logger.info(f"Data detectada e formatada: {date_string}")
|
110 |
return date_string
|
111 |
|
112 |
-
except Exception
|
113 |
-
logger.error(f"Erro crítico na formatação de data: {e}")
|
114 |
-
# Último fallback: data em inglês
|
115 |
now = datetime.now(ZoneInfo("America/Sao_Paulo"))
|
116 |
date_string = now.strftime("%d de %B de %Y")
|
117 |
-
logger.warning(f"Usando fallback de data: {date_string}")
|
118 |
return date_string
|
119 |
|
120 |
-
def
|
121 |
-
"""
|
|
|
|
|
122 |
try:
|
123 |
-
|
|
|
|
|
124 |
except Exception as e:
|
125 |
-
logger.
|
126 |
-
|
127 |
|
128 |
def extract_text_from_response(response):
|
129 |
"""
|
130 |
-
Extrai o texto da resposta de forma robusta
|
131 |
"""
|
132 |
response_text = ""
|
133 |
|
134 |
-
# Método 1: Tentar response.text diretamente
|
135 |
if hasattr(response, 'text') and response.text:
|
136 |
-
|
137 |
-
logger.info(f"Texto extraído via response.text: {len(response_text)} caracteres")
|
138 |
-
return response_text
|
139 |
|
140 |
-
# Método 2: Tentar extrair dos candidates
|
141 |
if hasattr(response, 'candidates') and response.candidates:
|
142 |
-
for
|
143 |
-
logger.debug(f"Processando candidate {i}")
|
144 |
-
|
145 |
if not hasattr(candidate, 'content') or not candidate.content:
|
146 |
-
logger.debug(f"Candidate {i} não tem content válido")
|
147 |
continue
|
148 |
|
149 |
content = candidate.content
|
150 |
|
151 |
-
# Verificar se content tem parts e se não é None
|
152 |
if not hasattr(content, 'parts') or content.parts is None:
|
153 |
-
logger.debug(f"Candidate {i} content.parts é None ou não existe")
|
154 |
continue
|
155 |
|
156 |
-
# Iterar pelas parts de forma segura
|
157 |
try:
|
158 |
parts_list = list(content.parts) if content.parts else []
|
159 |
-
logger.debug(f"Candidate {i} tem {len(parts_list)} parts")
|
160 |
|
161 |
-
for
|
162 |
if hasattr(part, 'text') and part.text:
|
163 |
response_text += part.text
|
164 |
-
logger.debug(f"Texto adicionado do candidate {i}, part {j}: {len(part.text)} chars")
|
165 |
|
166 |
-
except Exception
|
167 |
-
logger.warning(f"Erro ao processar parts do candidate {i}: {e}")
|
168 |
continue
|
169 |
|
170 |
-
# Método 3: Tentar atributos alternativos
|
171 |
-
if not response_text and hasattr(response, 'candidates'):
|
172 |
-
for i, candidate in enumerate(response.candidates):
|
173 |
-
for attr_name in ['output', 'generated_text', 'text_content']:
|
174 |
-
if hasattr(candidate, attr_name):
|
175 |
-
attr_value = getattr(candidate, attr_name)
|
176 |
-
if attr_value and str(attr_value).strip():
|
177 |
-
response_text += str(attr_value)
|
178 |
-
logger.info(f"Texto encontrado em candidate.{attr_name}")
|
179 |
-
break
|
180 |
-
|
181 |
-
logger.info(f"Texto total extraído: {len(response_text)} caracteres")
|
182 |
return response_text
|
183 |
|
184 |
def extract_sources_from_response(response):
|
185 |
"""
|
186 |
-
Extrai as fontes (URLs) do grounding metadata
|
187 |
"""
|
188 |
sources = []
|
189 |
|
190 |
if not (hasattr(response, 'candidates') and response.candidates):
|
191 |
-
logger.debug("Response não tem candidates")
|
192 |
return sources
|
193 |
|
194 |
-
for
|
195 |
if not (hasattr(candidate, 'grounding_metadata') and candidate.grounding_metadata):
|
196 |
-
logger.debug(f"Candidate {i} não tem grounding_metadata")
|
197 |
continue
|
198 |
|
199 |
grounding_metadata = candidate.grounding_metadata
|
200 |
-
logger.debug(f"Processando grounding_metadata do candidate {i}")
|
201 |
|
202 |
-
# Verificar grounding_chunks
|
203 |
if hasattr(grounding_metadata, 'grounding_chunks') and grounding_metadata.grounding_chunks:
|
204 |
-
|
205 |
-
|
206 |
-
for j, chunk in enumerate(grounding_metadata.grounding_chunks):
|
207 |
try:
|
208 |
if (hasattr(chunk, 'web') and chunk.web and
|
209 |
hasattr(chunk.web, 'uri') and chunk.web.uri):
|
@@ -211,90 +170,69 @@ def extract_sources_from_response(response):
|
|
211 |
uri = chunk.web.uri
|
212 |
if uri and uri not in sources:
|
213 |
sources.append(uri)
|
214 |
-
logger.debug(f"Fonte adicionada do chunk {j}: {uri}")
|
215 |
|
216 |
-
except Exception
|
217 |
-
logger.warning(f"Erro ao processar chunk {j}: {e}")
|
218 |
continue
|
219 |
-
|
220 |
-
# Verificar search_entry_point (método alternativo)
|
221 |
-
if hasattr(grounding_metadata, 'search_entry_point') and grounding_metadata.search_entry_point:
|
222 |
-
logger.debug("Search entry point encontrado")
|
223 |
-
# Aqui você pode adicionar lógica adicional se necessário
|
224 |
|
225 |
-
logger.info(f"Total de fontes únicas encontradas: {len(sources)}")
|
226 |
return sources
|
227 |
|
228 |
@router.post("/rewrite-news", response_model=NewsResponse)
|
229 |
async def rewrite_news(news: NewsRequest):
|
230 |
"""
|
231 |
-
Endpoint para reescrever notícias usando o modelo Gemini.
|
232 |
"""
|
233 |
try:
|
234 |
-
|
235 |
-
|
236 |
-
# Verificar se a API key está configurada
|
237 |
api_key = os.environ.get("GEMINI_API_KEY")
|
238 |
if not api_key:
|
239 |
-
logger.error("GEMINI_API_KEY não encontrada nas variáveis de ambiente")
|
240 |
raise HTTPException(status_code=500, detail="API key não configurada")
|
241 |
|
|
|
|
|
|
|
242 |
client = genai.Client(api_key=api_key)
|
243 |
model = "gemini-2.5-pro"
|
244 |
|
245 |
# Obter data formatada
|
246 |
date_string = get_brazilian_date_string()
|
247 |
-
logger.info(f"Usando data: {date_string}")
|
248 |
|
249 |
# Instruções do sistema
|
250 |
SYSTEM_INSTRUCTIONS = f"""
|
251 |
-
Você é um jornalista
|
252 |
|
253 |
ESTILO DE ESCRITA:
|
254 |
|
255 |
-
- Tom natural, direto e conversacional, sem frases feitas ou jargões jornalísticos
|
256 |
- Varie estruturas e conectivos para evitar robotização. Ou seja, sem frases redundantes, exemplo: "destacando como a experiência pode ser um divisor de águas profissional", "reafirma a força criativa do país no cenário global", "A revelação contextualizou não apenas sua performance na dança, mas também". É para noticiar a notícia de fato, sempre buscando mais informações que se conectam, e não opiniões pessoais.
|
257 |
-
- Sem linguagem militante ou juízos morais: mantenha tom informativo e predominantemente conservador
|
258 |
- Evite opiniões genéricas; foque em fatos e contexto
|
259 |
|
260 |
-
CONTEÚDO E ABORDAGEM:
|
261 |
|
262 |
-
- Use a notícia-base como ponto de partida, não como roteiro
|
263 |
-
-
|
264 |
-
-
|
265 |
-
- Sempre que possível, inclua títulos de obras em Português do Brasil
|
266 |
-
|
267 |
-
ESTRUTURA E FORMATAÇÃO:
|
268 |
-
|
269 |
-
- Comece com um lead que já entrega o essencial
|
270 |
-
- Desenvolva com profundidade, conectando com outros fatos e entregando contexto completo, principalmente no cenário brasileiro
|
271 |
-
- Termine com uma informação concreta (data, lançamento, próximos passos), nunca com opinião genérica. Ou seja, a conclusão da noticia deve ser com mais noticia, sem redundância genérica e robótica
|
272 |
- Otimize para leitura digital e SEO (parágrafos bem segmentados, palavras chaves, etc)
|
273 |
|
274 |
FORMATO:
|
275 |
|
276 |
<headline>título aqui</headline>
|
277 |
-
<subhead>subtítulo aqui</subhead>
|
278 |
<body>conteúdo aqui</body>
|
279 |
|
280 |
-
Use <strong> para destaques e <em> para títulos de obras ou citações.
|
281 |
|
282 |
TÍTULOS:
|
283 |
|
284 |
-
- Padrão brasileiro: só a primeira palavra em maiúscula (exceto nomes próprios e títulos de filmes/séries)
|
285 |
-
- Seja claro, direto e específico... Se houver, não inclua spoilers no título, apenas no body
|
286 |
-
|
287 |
-
OBRIGAÇÕES:
|
288 |
|
289 |
-
|
290 |
-
2. Traga sempre informações novas e contextualizadas.
|
291 |
-
|
292 |
-
O resultado deve soar como uma matéria escrita por um profissional experiente, não por IA. Seja preciso, atual e interessante. Sempre pesquise informações adicionais se no prompt contiver /google, isso é obrigatório. Sempre complete a notícia com acontecimentos que se ligam, sempre contextualize tudo para o leitor. A data de hoje é {date_string}
|
293 |
"""
|
294 |
|
295 |
-
#
|
296 |
EXAMPLE_INPUT_1 = """
|
297 |
-
|
298 |
"""
|
299 |
|
300 |
EXAMPLE_OUTPUT_1 = """<headline>"Se Beber, Não Case!": Ed Helms, o Dr. Stuart, revela medo do que os pais iriam pensar, mas tudo deu certo</headline>
|
@@ -307,68 +245,7 @@ O resultado deve soar como uma matéria escrita por um profissional experiente,
|
|
307 |
<p>Sobre a possibilidade de um quarto filme, <strong>Bradley Cooper</strong> afirmou em 2023 que toparia participar sem hesitar, principalmente pela chance de reencontrar colegas e diretor. Ainda assim, reconheceu que o projeto é improvável, já que <strong>Phillips</strong> está atualmente focado em empreendimentos de maior escala, como a série de filmes <em>Coringa</em>.</p>
|
308 |
</body>"""
|
309 |
|
310 |
-
#
|
311 |
-
EXAMPLE_INPUT_2 = """
|
312 |
-
/google News base: The Office spinoff series 'The Paper' has set a September premiere date at Peacock.
|
313 |
-
|
314 |
-
The new mockumentary series from Greg Daniels and Michael Koman will debut Sept. 4 on Peacock, the streamer announced Thursday. The first four episodes of 'The Paper' will premiere on Sept. 4, with two new episodes dropping every Thursday through Sept. 25.
|
315 |
-
|
316 |
-
'The Paper' follows the documentary crew that immortalized Dunder Mifflin's Scranton branch in 'The Office' as they find a new subject when they discover a historic Midwestern newspaper and the publisher trying to revive it, according to the official logline.
|
317 |
-
|
318 |
-
'The Office' fan-favorite Oscar Nuñez returns to the franchise in 'The Paper,' joining series regulars Domhnall Gleeson, Sabrina Impacciatore, Chelsea Frei, Melvin Gregg, Gbemisola Ikumelo, Alex Edelman, Ramona Young and Tim Key.
|
319 |
-
|
320 |
-
Guest stars for the show include Eric Rahill, Tracy Letts, Molly Ephraim, Mo Welch, Allan Havey, Duane Shepard Sr., Nate Jackson and Nancy Lenehan.
|
321 |
-
|
322 |
-
'The Paper' was created by Daniels, who created 'The Office,' under his banner Deedle-Dee Productions, and Koman, who has written on 'Nathan for You' and 'SNL.' Produced by Universal Television, a division of Universal Studio Group, 'The Paper' is executive produced by Ricky Gervais, Stephen Merchant, Howard Klein, Ben Silverman and Banijay Americas (formerly Reveille).
|
323 |
-
|
324 |
-
Daniels serves as a director on the show alongside Ken Kwapis, Yana Gorskaya, Paul Lieberstein, Tazbah Chavez, Jason Woliner, Jennifer Celotta, Matt Sohn, Dave Rogers and Jeff Blitz.
|
325 |
-
|
326 |
-
'The Office' launched in 2005 on NBC and ran for nine seasons leading up to the series finale in 2013. The cast of the beloved sitcom included Steve Carell, Rainn Wilson, John Krasinski, Jenna Fischer, Mindy Kaling and B.J. Novak, among others. The article is out of date, more information is needed.
|
327 |
-
"""
|
328 |
-
|
329 |
-
EXAMPLE_OUTPUT_2 = """<headline>Nova série do universo 'The Office' ganha título, data de estreia e um rosto familiar</headline>
|
330 |
-
<subhead>Intitulada 'The Paper', produção de Greg Daniels e Michael Koman chega em setembro com Domhnall Gleeson, Sabrina Impacciatore e o retorno de Oscar Nuñez</subhead>
|
331 |
-
<body>
|
332 |
-
<p>A equipe original de documentaristas de <em>"Insane Daily Life at Dunder Mifflin"</em> voltou ao trabalho, desta vez mudando para uma nova história, três anos após o fim de <em>"The Office"</em>. Após uma década de espera, o derivado da amada série de comédia finalmente saiu do papel e será lançado em <strong>4 de setembro de 2025</strong>. O nome do derivado é <em>"The Paper"</em> e estará disponível na plataforma de streaming <strong>Peacock</strong>.</p>
|
333 |
-
<p>A trama agora se desloca da fictícia <strong>Scranton, Pensilvânia</strong>, para o escritório de um jornal histórico, porém problemático, localizado no meio-oeste dos Estados Unidos, focando em um jornal em dificuldades na região. A equipe busca uma nova história após cobrir a vida de <strong>Michael Scott</strong> e <strong>Dwight Schrute</strong>. Agora, a equipe acompanha o <strong>Toledo Truth Teller</strong>, um jornal em <strong>Toledo, Ohio</strong>, e o editor que tenta reviver o jornal com a ajuda de repórteres voluntários.</p>
|
334 |
-
<p>O novo elenco conta com <strong>Domhnall Gleeson</strong>, ator irlandês famoso por <em>"Ex Machina"</em> e <em>"Questão de Tempo"</em>, ao lado da atriz italiana <strong>Sabrina Impacciatore</strong>, que ganhou amplo reconhecimento por seu papel na segunda temporada de <em>"The White Lotus"</em>. Gleeson interpreta o novo editor otimista do jornal, enquanto Impacciatore atua como gerente de redação.</p>
|
335 |
-
<p>Nas entrevistas mais recentes, Gleeson tenta se distanciar das comparações com o gerente da <strong>Dunder Mifflin</strong>. <em>"Acho que se você tentar competir com o que Steve [Carell] ou Ricky Gervais fizeram, seria um enorme erro,"</em> enfatizou o ator, visando construir uma persona totalmente nova. Ele também revelou ter recebido um tipo de conselho de <strong>John Krasinski</strong> e até de <strong>Steve Carell</strong> para aceitar o papel, especialmente porque se tratava de um projeto de <strong>Greg Daniels</strong>.</p>
|
336 |
-
<p>Como <em>"The Paper"</em> está reintroduzindo os personagens originais, os fãs de longa data da série parecem estar encantados, já que também traz <strong>Oscar Nuñez</strong> reprisando seu papel como o contador <strong>Oscar Martinez</strong>. Oscar, que estava iniciando uma carreira política em <em>"The Office"</em>, agora parece ter se mudado para <strong>Toledo</strong>. <em>"Eu disse ao Sr. Greg Daniels que, se Oscar voltasse, ele provavelmente estaria morando em uma cidade mais agitada e cosmopolita. Greg me ouviu e mudou Oscar para Toledo, Ohio, que tem três vezes a população de Scranton. Então, foi bom ser ouvido"</em>, brincou Nuñez durante um evento da <strong>NBCUniversal</strong>.</p>
|
337 |
-
<p><strong>Greg Daniels</strong>, que anteriormente adaptou <em>"The Office"</em> para o público americano, está em parceria com <strong>Michael Koman</strong>, cocriador de <em>"Nathan for You"</em>, para este novo projeto. Koman e Daniels, junto com <strong>Ricky Gervais</strong> e <strong>Stephen Merchant</strong>, criadores da série britânica original, formam a equipe de produção executiva.</p>
|
338 |
-
<p>A primeira temporada de <em>"The Paper"</em> será dividida em <strong>dez episódios</strong>. Nos Estados Unidos, os <strong>quatro primeiros episódios</strong> estarão disponíveis para streaming em <strong>4 de setembro</strong>. Depois disso, os episódios restantes serão lançados no formato de <strong>dois episódios por semana</strong>, com um total de seis episódios liberados até o final em <strong>25 de setembro</strong>.</p>
|
339 |
-
<p>A série ainda não tem data de estreia confirmada no Brasil, mas a expectativa é de que seja lançada no <strong>Universal+</strong>, serviço de streaming que costuma exibir produções do catálogo da <strong>Peacock</strong>.</p>
|
340 |
-
</body>"""
|
341 |
-
|
342 |
-
# Terceiro exemplo
|
343 |
-
EXAMPLE_INPUT_3 = """
|
344 |
-
/google News base: The first trailer for "Eyes of Wakanda," a four-episode limited animated series executive produced by "Black Panther" director Ryan Coogler, has been released. It will release on Disney+ on Aug. 1.
|
345 |
-
|
346 |
-
The upcoming show will be set in Wakanda, the home of the Black Panther that was extensively explored in Coogler's two Marvel movies. The show will follow "the adventures of brave Wakandan warriors throughout history. In this globe-trotting adventure, the heroes must carry-out dangerous missions to retrieve Vibranium artifacts from the enemies of Wakanda. They are the Hatut Zaraze and this is their story."
|
347 |
-
|
348 |
-
The voice cast includes Winnie Harlow, Cress Williams, Patricia Belcher, Larry Herron, Adam Gold, Lynn Whitfield, Jacques Colimon, Jona Xiao, Isaac Robinson-Smith, Gary Anthony Williams, Zeke Alton, Steve Toussaint and Anika Noni Rose.
|
349 |
-
|
350 |
-
Popular on Variety
|
351 |
-
|
352 |
-
The last time fans saw Wakanda was in 2022's "Black Panther: Wakanda Forever," the sequel to Marvel's 2018 best picture nominee. That movie introduced the underwater world of Atlantis and featured the returns of Letitia Wright, Lupita Nyong'o, Angela Bassett, Winston Duke, Danai Gurira and Martin Freeman. Wakandans have popped up in other places in the MCU, including an appearance by Ayo, played by Florence Kasumba, in the Disney+ series "The Falcon and The Winter Soldier." The second-in-command of the Dora Milaje, Ayo first appeared in "Captain America: Civil War."
|
353 |
-
|
354 |
-
The "Wakanda" series is the first in a multi-year overall TV deal with Coogler's Proximity production company and The Walt Disney Company, with other TV projects in currently in development for the MCU.
|
355 |
-
|
356 |
-
"Eyes of Wakanda" is helmed by director/executive producer Todd Harris, who was first a storyboard artist on "Black Panther" and "Black Panther: Wakanda Forever" then as an illustrator on "Sinners" before directing "Eyes of Wakanda." The show is executive produced by Coogler, Brad Winderbaum, Kevin Feige, Louis D'Esposito and Dana Vasquez-Eberhardt.
|
357 |
-
|
358 |
-
The article is out of date, more information is needed.
|
359 |
-
"""
|
360 |
-
|
361 |
-
EXAMPLE_OUTPUT_3 = """<headline>Olhos de Wakanda: nova animação da Marvel ganha trailer e data de estreia</headline>
|
362 |
-
<subhead>A série de Ryan Coogler vai mergulhar na história do reino de T'Challa e nas missões secretas de seus guerreiros para proteger o vibranium.</subhead>
|
363 |
-
<body>
|
364 |
-
<p>A <strong>Marvel Studios</strong> acaba de publicar o primeiro trailer da sua próxima série animada, <em>Olhos de Wakanda</em>, que explora a história do avançado reino africano. Foi anunciado que a série terá produção executiva de <strong>Ryan Coogler</strong>, diretor dos filmes do <strong>Pantera Negra</strong>, e será lançada como uma minissérie em quatro partes no dia <strong>1º de agosto</strong> no <strong>Disney Plus</strong>. A data é um pouco surpreendente, pois o lançamento estava inicialmente marcado para <strong>27 de agosto</strong>.</p>
|
365 |
-
<p>A narrativa acompanhará as façanhas dos <strong>Hatut Zaraze</strong>, os guerreiros de elite de <strong>Wakanda</strong>, enquanto embarcam em missões globais para recuperar artefatos de <strong>vibranium</strong> roubados. Conhecida nos quadrinhos como os <em>"Cães de Guerra"</em>, essa força secreta atua como uma agência clandestina, protegendo os recursos e inovações ocultas da nação. Os ancestrais de <strong>T'Challa</strong> serão ainda mais explorados na série, mostrando como eles, junto de seus descendentes, forjaram o legado do <strong>Pantera Negra</strong>, que ele um dia herdará, ao longo de séculos de defesa, em arcos cronológicos marcantes.</p>
|
366 |
-
<p><strong>Todd Harris</strong>, que trabalhou como artista de storyboard nos filmes da franquia, criou e dirigiu a obra, que terá um visual distinto, separado das adaptações. Segundo Harris, a trama integra espionagem com eventos históricos, começando no final da <strong>Idade do Bronze</strong> e se estendendo por séculos. O diretor acrescentou: <em>"Você tem um James Bond com a qualidade de Wakanda e às vezes uma Jane Bond com toda a grandiosidade visual e cultural que a nação representa."</em></p>
|
367 |
-
<p><strong>Winnie Harlow</strong>, <strong>Cress Williams</strong>, <strong>Patricia Belcher</strong>, <strong>Lynn Whitfield</strong> e <strong>Anika Noni Rose</strong> fazem parte do elenco de vozes. Além de expandir a mitologia de <strong>Wakanda</strong> dentro do <strong>MCU</strong>, a série também apresentará uma versão ancestral do herói <strong>Punho de Ferro</strong>. <strong>Brad Winderbaum</strong>, chefe de TV e streaming da <strong>Marvel Studios</strong>, confirmou que <em>Olhos de Wakanda</em> estará profundamente integrada com a linha do tempo principal do <strong>MCU</strong>.</p>
|
368 |
-
<p>A produtora de <strong>Coogler</strong>, <strong>Proximity Media</strong>, e a <strong>Disney</strong> têm um acordo televisivo de vários anos que inclui a produção de outros projetos do <strong>MCU</strong>. No dia da estreia, todos os quatro episódios de <em>Olhos de Wakanda</em> estarão disponíveis ao mesmo tempo.</p>
|
369 |
-
</body>"""
|
370 |
-
|
371 |
-
# Configuração correta da ferramenta de pesquisa
|
372 |
grounding_tool = types.Tool(
|
373 |
google_search=types.GoogleSearch()
|
374 |
)
|
@@ -378,7 +255,7 @@ The article is out of date, more information is needed.
|
|
378 |
thinking_config=types.ThinkingConfig(
|
379 |
thinking_budget=-1,
|
380 |
),
|
381 |
-
tools=[grounding_tool],
|
382 |
response_mime_type="text/plain",
|
383 |
max_output_tokens=4096,
|
384 |
temperature=0.8,
|
@@ -386,145 +263,54 @@ The article is out of date, more information is needed.
|
|
386 |
|
387 |
# Conteúdo da conversa
|
388 |
contents = [
|
389 |
-
#
|
390 |
types.Content(
|
391 |
role="user",
|
392 |
parts=[
|
393 |
types.Part.from_text(text=EXAMPLE_INPUT_1)
|
394 |
]
|
395 |
),
|
396 |
-
# Primeiro exemplo: modelo responde com o formato correto
|
397 |
types.Content(
|
398 |
role="model",
|
399 |
parts=[
|
400 |
types.Part.from_text(text=EXAMPLE_OUTPUT_1)
|
401 |
]
|
402 |
),
|
403 |
-
#
|
404 |
-
types.Content(
|
405 |
-
role="user",
|
406 |
-
parts=[
|
407 |
-
types.Part.from_text(text=EXAMPLE_INPUT_2)
|
408 |
-
]
|
409 |
-
),
|
410 |
-
# Segundo exemplo: modelo responde com o formato correto
|
411 |
-
types.Content(
|
412 |
-
role="model",
|
413 |
-
parts=[
|
414 |
-
types.Part.from_text(text=EXAMPLE_OUTPUT_2)
|
415 |
-
]
|
416 |
-
),
|
417 |
-
# Terceiro exemplo: usuário envia outra notícia
|
418 |
types.Content(
|
419 |
role="user",
|
420 |
parts=[
|
421 |
-
types.Part.from_text(text=
|
422 |
-
|
423 |
-
),
|
424 |
-
# Terceiro exemplo: modelo responde com o formato correto
|
425 |
-
types.Content(
|
426 |
-
role="model",
|
427 |
-
parts=[
|
428 |
-
types.Part.from_text(text=EXAMPLE_OUTPUT_3)
|
429 |
-
]
|
430 |
-
),
|
431 |
-
# Agora o usuário envia a notícia real para ser reescrita
|
432 |
-
types.Content(
|
433 |
-
role="user",
|
434 |
-
parts=[
|
435 |
-
types.Part.from_text(text=f"/google News base: {news.content}. The article is out of date, more information is needed.")
|
436 |
]
|
437 |
)
|
438 |
]
|
439 |
|
440 |
-
|
441 |
-
|
442 |
-
# Usar generate_content em vez de generate_content_stream para melhor controle do grounding
|
443 |
response = client.models.generate_content(
|
444 |
model=model,
|
445 |
contents=contents,
|
446 |
config=config
|
447 |
)
|
448 |
|
449 |
-
#
|
450 |
-
# LOGS DETALHADOS DA RESPOSTA BRUTA
|
451 |
-
# =================================
|
452 |
-
logger.info("=== RESPOSTA BRUTA COMPLETA DO GEMINI ===")
|
453 |
-
logger.info(f"Tipo da resposta: {type(response)}")
|
454 |
-
logger.info(f"Resposta completa: {safe_json_serialize(response)}")
|
455 |
-
|
456 |
-
# Log dos atributos principais
|
457 |
-
logger.info(f"response.text: '{response.text if hasattr(response, 'text') else 'ATRIBUTO text NÃO EXISTE'}'")
|
458 |
-
logger.info(f"response.candidates: {safe_json_serialize(response.candidates) if hasattr(response, 'candidates') else 'ATRIBUTO candidates NÃO EXISTE'}")
|
459 |
-
|
460 |
-
# Log detalhado dos candidates - COM PROTEÇÃO CONTRA PARTS = None
|
461 |
-
if hasattr(response, 'candidates') and response.candidates:
|
462 |
-
for i, candidate in enumerate(response.candidates):
|
463 |
-
logger.info(f"=== CANDIDATE {i} ===")
|
464 |
-
logger.info(f"Candidate completo: {safe_json_serialize(candidate)}")
|
465 |
-
|
466 |
-
if hasattr(candidate, 'content'):
|
467 |
-
logger.info(f"Candidate.content: {safe_json_serialize(candidate.content)}")
|
468 |
-
|
469 |
-
if hasattr(candidate.content, 'parts'):
|
470 |
-
if candidate.content.parts is None:
|
471 |
-
logger.warning(f"ATENÇÃO: Candidate {i} content.parts é None!")
|
472 |
-
else:
|
473 |
-
try:
|
474 |
-
parts_list = list(candidate.content.parts)
|
475 |
-
logger.info(f"Candidate {i} tem {len(parts_list)} parts")
|
476 |
-
|
477 |
-
for j, part in enumerate(parts_list):
|
478 |
-
logger.info(f"Part {j}: {safe_json_serialize(part)}")
|
479 |
-
if hasattr(part, 'text'):
|
480 |
-
logger.info(f"Part {j} text: '{part.text}'")
|
481 |
-
|
482 |
-
except Exception as e:
|
483 |
-
logger.error(f"Erro ao processar parts do candidate {i}: {e}")
|
484 |
-
else:
|
485 |
-
logger.warning(f"Candidate {i} content não tem atributo 'parts'")
|
486 |
-
|
487 |
-
if hasattr(candidate, 'finish_reason'):
|
488 |
-
logger.info(f"Finish reason: {candidate.finish_reason}")
|
489 |
-
|
490 |
-
if hasattr(candidate, 'safety_ratings'):
|
491 |
-
logger.info(f"Safety ratings: {safe_json_serialize(candidate.safety_ratings)}")
|
492 |
-
|
493 |
-
if hasattr(candidate, 'grounding_metadata'):
|
494 |
-
logger.info(f"Grounding metadata: {safe_json_serialize(candidate.grounding_metadata)}")
|
495 |
-
|
496 |
-
# Log de outros atributos possíveis
|
497 |
-
for attr in ['usage_metadata', 'prompt_feedback', 'model_version']:
|
498 |
-
if hasattr(response, attr):
|
499 |
-
logger.info(f"response.{attr}: {safe_json_serialize(getattr(response, attr))}")
|
500 |
-
|
501 |
-
logger.info("=== FIM DA RESPOSTA BRUTA ===")
|
502 |
-
|
503 |
-
# Usar as funções seguras para extração
|
504 |
response_text = extract_text_from_response(response)
|
505 |
sources = extract_sources_from_response(response)
|
506 |
-
|
507 |
-
logger.info(f"Resposta do modelo recebida com {len(response_text)} caracteres")
|
508 |
-
logger.info(f"Encontradas {len(sources)} fontes únicas: {sources}")
|
509 |
|
510 |
# Verificar se o texto está vazio
|
511 |
if not response_text or response_text.strip() == "":
|
512 |
-
logger.error("ERRO CRÍTICO: Texto da resposta está vazio!")
|
513 |
raise HTTPException(
|
514 |
status_code=500,
|
515 |
-
detail="Modelo não retornou conteúdo válido
|
516 |
)
|
517 |
|
518 |
-
# Extração do título, subtítulo e conteúdo
|
519 |
title_match = re.search(r"<headline>(.*?)</headline>", response_text, re.DOTALL)
|
520 |
title = title_match.group(1).strip() if title_match else "Título não encontrado"
|
521 |
-
logger.info(f"Título extraído: '{title}'")
|
522 |
|
523 |
subhead_match = re.search(r"<subhead>(.*?)</subhead>", response_text, re.DOTALL)
|
524 |
subhead = subhead_match.group(1).strip() if subhead_match else "Subtítulo não encontrado"
|
525 |
-
logger.info(f"Subtítulo extraído: '{subhead}'")
|
526 |
|
527 |
-
# Extração do conteúdo com lógica mais flexível para <body>
|
528 |
body_match = re.search(r"<body>(.*?)</body>", response_text, re.DOTALL)
|
529 |
if body_match:
|
530 |
content = body_match.group(1).strip()
|
@@ -534,33 +320,11 @@ The article is out of date, more information is needed.
|
|
534 |
content = body_start_match.group(1).strip()
|
535 |
else:
|
536 |
content = "Conteúdo não encontrado"
|
537 |
-
|
538 |
-
logger.info(f"Conteúdo extraído: {len(content)} caracteres - '{content[:200]}...' (primeiros 200 chars)")
|
539 |
-
|
540 |
-
# Verificações adicionais
|
541 |
-
if title == "Título não encontrado" or subhead == "Subtítulo não encontrado" or content == "Conteúdo não encontrado":
|
542 |
-
logger.warning("Algumas partes do conteúdo não foram encontradas!")
|
543 |
-
logger.warning(f"Texto completo da resposta: '{response_text}'")
|
544 |
-
|
545 |
-
logger.info("Artigo reescrito com sucesso")
|
546 |
-
logger.debug(f"Título extraído: {title[:50]}...")
|
547 |
-
logger.debug(f"Subtítulo extraído: {subhead[:50]}...")
|
548 |
-
logger.debug(f"Conteúdo extraído: {len(content)} caracteres")
|
549 |
-
logger.debug(f"Sources encontradas: {sources}")
|
550 |
-
|
551 |
-
# Log adicional para debug
|
552 |
-
logger.info("=== RESULTADO FINAL ===")
|
553 |
-
logger.info(f"Title: '{title}'")
|
554 |
-
logger.info(f"Subhead: '{subhead}'")
|
555 |
-
logger.info(f"Content length: {len(content)}")
|
556 |
-
logger.info(f"Sources count: {len(sources)}")
|
557 |
|
558 |
return NewsResponse(title=title, subhead=subhead, content=content, sources=sources)
|
559 |
|
560 |
except HTTPException:
|
561 |
-
# Re-lança HTTPExceptions para manter o status code correto
|
562 |
raise
|
563 |
except Exception as e:
|
564 |
-
|
565 |
-
|
566 |
-
raise HTTPException(status_code=500, detail=error_msg)
|
|
|
1 |
import os
|
2 |
import logging
|
3 |
import json
|
4 |
+
import requests
|
5 |
from fastapi import APIRouter, HTTPException
|
6 |
from pydantic import BaseModel
|
7 |
from google import genai
|
|
|
18 |
|
19 |
class NewsRequest(BaseModel):
|
20 |
content: str
|
21 |
+
sources_url: str # URL do arquivo fontes.txt
|
22 |
|
23 |
class NewsResponse(BaseModel):
|
24 |
title: str
|
|
|
34 |
try:
|
35 |
# Tenta configurar o locale brasileiro
|
36 |
locale_variants = [
|
37 |
+
'pt_BR.UTF-8',
|
38 |
+
'pt_BR.utf8',
|
39 |
+
'pt_BR',
|
40 |
+
'Portuguese_Brazil.1252',
|
41 |
+
'Portuguese_Brazil',
|
42 |
+
'pt_BR.ISO8859-1',
|
43 |
]
|
44 |
|
45 |
locale_set = False
|
|
|
47 |
try:
|
48 |
locale.setlocale(locale.LC_TIME, loc)
|
49 |
locale_set = True
|
|
|
50 |
break
|
51 |
except locale.Error:
|
|
|
52 |
continue
|
53 |
|
54 |
if not locale_set:
|
55 |
+
locale.setlocale(locale.LC_TIME, '')
|
|
|
56 |
|
|
|
57 |
now = datetime.now(ZoneInfo("America/Sao_Paulo"))
|
58 |
|
59 |
+
# Dicionários para tradução manual (fallback)
|
60 |
meses = {
|
61 |
1: 'janeiro', 2: 'fevereiro', 3: 'março', 4: 'abril',
|
62 |
5: 'maio', 6: 'junho', 7: 'julho', 8: 'agosto',
|
|
|
69 |
}
|
70 |
|
71 |
try:
|
|
|
72 |
if locale_set:
|
|
|
73 |
try:
|
74 |
date_string = now.strftime("%-d de %B de %Y (%A)")
|
75 |
except ValueError:
|
|
|
76 |
try:
|
77 |
date_string = now.strftime("%#d de %B de %Y (%A)")
|
78 |
except ValueError:
|
|
|
79 |
date_string = now.strftime("%d de %B de %Y (%A)")
|
80 |
if date_string.startswith('0'):
|
81 |
date_string = date_string[1:]
|
82 |
|
|
|
83 |
date_string = date_string.replace(date_string.split('(')[1].split(')')[0],
|
84 |
date_string.split('(')[1].split(')')[0].lower())
|
85 |
else:
|
|
|
86 |
dia = now.day
|
87 |
mes = meses[now.month]
|
88 |
ano = now.year
|
89 |
dia_semana = dias_semana[now.weekday()]
|
90 |
date_string = f"{dia} de {mes} de {ano} ({dia_semana})"
|
91 |
|
92 |
+
except Exception:
|
|
|
|
|
93 |
dia = now.day
|
94 |
mes = meses[now.month]
|
95 |
ano = now.year
|
96 |
dia_semana = dias_semana[now.weekday()]
|
97 |
date_string = f"{dia} de {mes} de {ano} ({dia_semana})"
|
98 |
|
|
|
99 |
return date_string
|
100 |
|
101 |
+
except Exception:
|
|
|
|
|
102 |
now = datetime.now(ZoneInfo("America/Sao_Paulo"))
|
103 |
date_string = now.strftime("%d de %B de %Y")
|
|
|
104 |
return date_string
|
105 |
|
106 |
+
def download_sources_file(url: str) -> str:
|
107 |
+
"""
|
108 |
+
Baixa o arquivo fontes.txt da URL fornecida.
|
109 |
+
"""
|
110 |
try:
|
111 |
+
response = requests.get(url, timeout=30)
|
112 |
+
response.raise_for_status()
|
113 |
+
return response.text
|
114 |
except Exception as e:
|
115 |
+
logger.error(f"Erro ao baixar arquivo de fontes: {e}")
|
116 |
+
raise HTTPException(status_code=400, detail=f"Erro ao baixar arquivo de fontes: {str(e)}")
|
117 |
|
118 |
def extract_text_from_response(response):
|
119 |
"""
|
120 |
+
Extrai o texto da resposta de forma robusta.
|
121 |
"""
|
122 |
response_text = ""
|
123 |
|
|
|
124 |
if hasattr(response, 'text') and response.text:
|
125 |
+
return response.text
|
|
|
|
|
126 |
|
|
|
127 |
if hasattr(response, 'candidates') and response.candidates:
|
128 |
+
for candidate in response.candidates:
|
|
|
|
|
129 |
if not hasattr(candidate, 'content') or not candidate.content:
|
|
|
130 |
continue
|
131 |
|
132 |
content = candidate.content
|
133 |
|
|
|
134 |
if not hasattr(content, 'parts') or content.parts is None:
|
|
|
135 |
continue
|
136 |
|
|
|
137 |
try:
|
138 |
parts_list = list(content.parts) if content.parts else []
|
|
|
139 |
|
140 |
+
for part in parts_list:
|
141 |
if hasattr(part, 'text') and part.text:
|
142 |
response_text += part.text
|
|
|
143 |
|
144 |
+
except Exception:
|
|
|
145 |
continue
|
146 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
147 |
return response_text
|
148 |
|
149 |
def extract_sources_from_response(response):
|
150 |
"""
|
151 |
+
Extrai as fontes (URLs) do grounding metadata.
|
152 |
"""
|
153 |
sources = []
|
154 |
|
155 |
if not (hasattr(response, 'candidates') and response.candidates):
|
|
|
156 |
return sources
|
157 |
|
158 |
+
for candidate in response.candidates:
|
159 |
if not (hasattr(candidate, 'grounding_metadata') and candidate.grounding_metadata):
|
|
|
160 |
continue
|
161 |
|
162 |
grounding_metadata = candidate.grounding_metadata
|
|
|
163 |
|
|
|
164 |
if hasattr(grounding_metadata, 'grounding_chunks') and grounding_metadata.grounding_chunks:
|
165 |
+
for chunk in grounding_metadata.grounding_chunks:
|
|
|
|
|
166 |
try:
|
167 |
if (hasattr(chunk, 'web') and chunk.web and
|
168 |
hasattr(chunk.web, 'uri') and chunk.web.uri):
|
|
|
170 |
uri = chunk.web.uri
|
171 |
if uri and uri not in sources:
|
172 |
sources.append(uri)
|
|
|
173 |
|
174 |
+
except Exception:
|
|
|
175 |
continue
|
|
|
|
|
|
|
|
|
|
|
176 |
|
|
|
177 |
return sources
|
178 |
|
179 |
@router.post("/rewrite-news", response_model=NewsResponse)
|
180 |
async def rewrite_news(news: NewsRequest):
|
181 |
"""
|
182 |
+
Endpoint para reescrever notícias usando o modelo Gemini com arquivo de fontes.
|
183 |
"""
|
184 |
try:
|
185 |
+
# Verificar API key
|
|
|
|
|
186 |
api_key = os.environ.get("GEMINI_API_KEY")
|
187 |
if not api_key:
|
|
|
188 |
raise HTTPException(status_code=500, detail="API key não configurada")
|
189 |
|
190 |
+
# Baixar arquivo de fontes
|
191 |
+
sources_content = download_sources_file(news.sources_url)
|
192 |
+
|
193 |
client = genai.Client(api_key=api_key)
|
194 |
model = "gemini-2.5-pro"
|
195 |
|
196 |
# Obter data formatada
|
197 |
date_string = get_brazilian_date_string()
|
|
|
198 |
|
199 |
# Instruções do sistema
|
200 |
SYSTEM_INSTRUCTIONS = f"""
|
201 |
+
Você é um jornalista brasileiro, escrevendo para portais digitais. Sua missão é transformar notícias internacionais em matérias originais, atualizadas e envolventes para o público brasileiro.
|
202 |
|
203 |
ESTILO DE ESCRITA:
|
204 |
|
205 |
+
- Tom natural, direto e conversacional, sem frases feitas ou jargões jornalísticos.
|
206 |
- Varie estruturas e conectivos para evitar robotização. Ou seja, sem frases redundantes, exemplo: "destacando como a experiência pode ser um divisor de águas profissional", "reafirma a força criativa do país no cenário global", "A revelação contextualizou não apenas sua performance na dança, mas também". É para noticiar a notícia de fato, sempre buscando mais informações que se conectam, e não opiniões pessoais.
|
207 |
+
- Sem linguagem militante ou juízos morais: mantenha tom informativo e predominantemente conservador / aspectro de Direita que o jornal tem.
|
208 |
- Evite opiniões genéricas; foque em fatos e contexto
|
209 |
|
210 |
+
CONTEÚDO, ESTRUTURA E ABORDAGEM:
|
211 |
|
212 |
+
- Use a notícia-base como ponto de partida, não como roteiro. Avalie o fontes.txt por completo e extraia absolutamente todas as coisas relevantes para formentar a notícia. Não traga informações falsas ou inventadas. Na dúvida, não insira.
|
213 |
+
- Sempre que possível, inclua títulos de obras em Português do Brasil.
|
214 |
+
- Termine com uma informação concreta (data, lançamento, próximos passos), nunca com opinião genérica. Ou seja, a conclusão da noticia deve ser com mais noticia, sem redundância genérica e robótica.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
215 |
- Otimize para leitura digital e SEO (parágrafos bem segmentados, palavras chaves, etc)
|
216 |
|
217 |
FORMATO:
|
218 |
|
219 |
<headline>título aqui</headline>
|
220 |
+
<subhead>subtítulo aqui</subhead>
|
221 |
<body>conteúdo aqui</body>
|
222 |
|
223 |
+
Use <strong> para destaques e <em> para títulos de obras ou citações.
|
224 |
|
225 |
TÍTULOS:
|
226 |
|
227 |
+
- Padrão brasileiro: só a primeira palavra em maiúscula (exceto nomes próprios e títulos de filmes/séries/obras, etc.)
|
228 |
+
- Seja claro, direto e específico... Se houver, não inclua spoilers no título, apenas no body.
|
|
|
|
|
229 |
|
230 |
+
O resultado deve soar como uma matéria escrita por um profissional experiente, não por IA. Seja preciso, atual e interessante. Sempre complete a notícia com acontecimentos que se ligam, sempre contextualize tudo para o leitor. A data de hoje é {date_string}
|
|
|
|
|
|
|
231 |
"""
|
232 |
|
233 |
+
# Exemplos (mantidos os mesmos do código original)
|
234 |
EXAMPLE_INPUT_1 = """
|
235 |
+
News base: Ed Helms revealed in an interview that he was nervous about his parents' reaction to the film The Hangover, but in the end everything worked out and her mother loved the movie. The article is out of date, more information is needed.
|
236 |
"""
|
237 |
|
238 |
EXAMPLE_OUTPUT_1 = """<headline>"Se Beber, Não Case!": Ed Helms, o Dr. Stuart, revela medo do que os pais iriam pensar, mas tudo deu certo</headline>
|
|
|
245 |
<p>Sobre a possibilidade de um quarto filme, <strong>Bradley Cooper</strong> afirmou em 2023 que toparia participar sem hesitar, principalmente pela chance de reencontrar colegas e diretor. Ainda assim, reconheceu que o projeto é improvável, já que <strong>Phillips</strong> está atualmente focado em empreendimentos de maior escala, como a série de filmes <em>Coringa</em>.</p>
|
246 |
</body>"""
|
247 |
|
248 |
+
# Configuração da ferramenta de pesquisa
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
249 |
grounding_tool = types.Tool(
|
250 |
google_search=types.GoogleSearch()
|
251 |
)
|
|
|
255 |
thinking_config=types.ThinkingConfig(
|
256 |
thinking_budget=-1,
|
257 |
),
|
258 |
+
tools=[grounding_tool],
|
259 |
response_mime_type="text/plain",
|
260 |
max_output_tokens=4096,
|
261 |
temperature=0.8,
|
|
|
263 |
|
264 |
# Conteúdo da conversa
|
265 |
contents = [
|
266 |
+
# Exemplo
|
267 |
types.Content(
|
268 |
role="user",
|
269 |
parts=[
|
270 |
types.Part.from_text(text=EXAMPLE_INPUT_1)
|
271 |
]
|
272 |
),
|
|
|
273 |
types.Content(
|
274 |
role="model",
|
275 |
parts=[
|
276 |
types.Part.from_text(text=EXAMPLE_OUTPUT_1)
|
277 |
]
|
278 |
),
|
279 |
+
# Notícia atual com arquivo de fontes
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
280 |
types.Content(
|
281 |
role="user",
|
282 |
parts=[
|
283 |
+
types.Part.from_text(text=f"News base: {news.content}. The article is out of date, more information is needed."),
|
284 |
+
types.Part.from_text(text=f"Fontes adicionais disponíveis:\n\n{sources_content}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
285 |
]
|
286 |
)
|
287 |
]
|
288 |
|
289 |
+
# Gerar conteúdo
|
|
|
|
|
290 |
response = client.models.generate_content(
|
291 |
model=model,
|
292 |
contents=contents,
|
293 |
config=config
|
294 |
)
|
295 |
|
296 |
+
# Extrair texto e fontes
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
297 |
response_text = extract_text_from_response(response)
|
298 |
sources = extract_sources_from_response(response)
|
|
|
|
|
|
|
299 |
|
300 |
# Verificar se o texto está vazio
|
301 |
if not response_text or response_text.strip() == "":
|
|
|
302 |
raise HTTPException(
|
303 |
status_code=500,
|
304 |
+
detail="Modelo não retornou conteúdo válido"
|
305 |
)
|
306 |
|
307 |
+
# Extração do título, subtítulo e conteúdo
|
308 |
title_match = re.search(r"<headline>(.*?)</headline>", response_text, re.DOTALL)
|
309 |
title = title_match.group(1).strip() if title_match else "Título não encontrado"
|
|
|
310 |
|
311 |
subhead_match = re.search(r"<subhead>(.*?)</subhead>", response_text, re.DOTALL)
|
312 |
subhead = subhead_match.group(1).strip() if subhead_match else "Subtítulo não encontrado"
|
|
|
313 |
|
|
|
314 |
body_match = re.search(r"<body>(.*?)</body>", response_text, re.DOTALL)
|
315 |
if body_match:
|
316 |
content = body_match.group(1).strip()
|
|
|
320 |
content = body_start_match.group(1).strip()
|
321 |
else:
|
322 |
content = "Conteúdo não encontrado"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
323 |
|
324 |
return NewsResponse(title=title, subhead=subhead, content=content, sources=sources)
|
325 |
|
326 |
except HTTPException:
|
|
|
327 |
raise
|
328 |
except Exception as e:
|
329 |
+
logger.error(f"Erro na reescrita: {str(e)}")
|
330 |
+
raise HTTPException(status_code=500, detail=str(e))
|
|