caarleexx commited on
Commit
4e696f3
·
verified ·
1 Parent(s): 60f45b1

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +81 -82
app.py CHANGED
@@ -1,25 +1,23 @@
1
  # ╔════════════════════════════════════════════════════════════════════════════╗
2
- # ║ PIPELINE v39 (Compat): UI/UX com Correção de Compatibilidade
3
  # ╚════════════════════════════════════════════════════════════════════════════╝
4
  #
5
  # ==================== RESUMO TÉCNICO DA PIPELINE ====================
6
  #
7
- # OBJETIVO: Combinar a nova interface de usuário de duas etapas (v39) com
8
- # a compatibilidade de versões mais antigas do Gradio (v38 corrigida).
9
  #
10
- # MUDANÇAS IMPLEMENTADAS:
11
- # 1. FLUXO DE INICIAÇÃO (da v39): A UI começa com uma tela de "setup" para a
12
- # primeira análise. O chatbot aparece após a primeira execução.
13
- # 2. VISIBILIDADE DINÂMICA (da v39): Utiliza `gr.update(visible=...)` para alternar
14
- # entre a tela de início e a interface de chat.
15
- # 3. COMPATIBILIDADE GRADIO (da v38):
16
- # - O parâmetro `css` é passado para o método `.launch()`.
17
- # - O `gr.Chatbot` é definido sem parâmetros não suportados (`show_copy_button`).
18
- #
19
- # INOVAÇÕES CONSOLIDADAS (ADUC-SDR):
20
- # - Arquitetura de União Compositiva (ADUC)
21
- # - Escala Dinâmica Resiliente (SDR)
22
- # - FIX "last role user" e Lógica de STOP Refinada
23
  #
24
  # =====================================================================
25
 
@@ -39,15 +37,14 @@ groq_client = Groq(api_key=groq_key)
39
  ARQUIVO_CONFIG = "protocolo.json"
40
  ARQUIVO_HELP = "help.md"
41
  ARQUIVO_CONTEXTO = "contexto_persistente.json"
42
- ARQUIVO_DATA_MD = "data.md" # Arquivo de dados padrão da v39
43
  DELAY_ENTRE_AGENTES = 1
44
  STOP_KEYWORD = "STOP_PIPELINE"
45
 
46
- print("🚀 App inicializada - GROQ v39 (Compat) + FLUXO DE INICIAÇÃO")
47
  print(f" ✅ Groq: {'OK' if groq_key != 'SUA_GROQ_KEY_AQUI' else '⚠️ placeholder'}")
48
 
49
  # ==================== 2. UTILIDADES ====================
50
- # (Esta seção é idêntica à v38, sem alterações)
51
  def estimar_tokens(texto):
52
  return len(str(texto)) // 4
53
 
@@ -136,10 +133,9 @@ def verificar_stop(texto):
136
  return stop_detectado
137
 
138
  # ==================== 3. ENGINE DE EXECUÇÃO (GROQ) ====================
139
- # (Esta seção é idêntica à v38, sem alterações)
140
  def executar_no(timeline, config):
141
  print(f"\n🔥 === EXECUTANDO {config['nome']} ===")
142
- modelo = config.get('modelo', 'meta-llama/llama-4-maverick-17b-128e-instruct')
143
  print(f" Modelo Groq: {modelo}")
144
 
145
  try:
@@ -166,7 +162,7 @@ def executar_no(timeline, config):
166
 
167
  completion = groq_client.chat.completions.create(
168
  model=modelo, messages=messages, temperature=1,
169
- max_completion_tokens=6048, top_p=1, stream=True, stop=None
170
  )
171
 
172
  out_raw = "".join(chunk.choices[0].delta.content or "" for chunk in completion)
@@ -200,9 +196,8 @@ def executar_no(timeline, config):
200
  return {"role": "system", "error": erro_amistoso, "agent": config['nome']}, False
201
 
202
  # ==================== 4. ORQUESTRADOR COM CONTEXTO PERSISTENTE (ADUC-SDR) ====================
203
- # (Esta seção é idêntica à v38, sem alterações)
204
  def orquestrador(texto, anexos_list, history, json_config, contexto_objetivo):
205
- print("\n" + "="*80 + "\n🎬 INICIANDO ORQUESTRADOR")
206
  if not texto.strip():
207
  yield history, [], carregar_contexto_persistente()
208
  return
@@ -222,8 +217,6 @@ def orquestrador(texto, anexos_list, history, json_config, contexto_objetivo):
222
  contexto_inicial = ""
223
  if contexto_objetivo and contexto_objetivo.strip():
224
  contexto_inicial += f"[OBJETIVO DO MODELO]\n{contexto_objetivo.strip()}\n[FIM OBJETIVO]\n\n"
225
- for anexo in anexos_list:
226
- contexto_inicial += ler_anexo(anexo)
227
 
228
  timeline = [{"role": msg["role"], "content": msg["content"]} for msg in contexto_persistente[:-1]]
229
  timeline.append({"role": "user", "content": f"{contexto_inicial}{texto}".strip()})
@@ -231,8 +224,6 @@ def orquestrador(texto, anexos_list, history, json_config, contexto_objetivo):
231
  audit_data = []
232
  for idx, cfg in enumerate(protocolo):
233
  print(f"\n{'='*50}\n🚀 FASE {idx+1}/{len(protocolo)}: {cfg['nome']}")
234
- timeline_chars = sum(len(str(m.get("content", ""))) for m in timeline)
235
- timeline_tokens = estimar_tokens(timeline_chars)
236
  history[-1]["content"] = f"⏳ **Agente {idx+1}/{len(protocolo)}: {cfg['nome']}**..."
237
  yield history, audit_data, contexto_persistente
238
  time.sleep(DELAY_ENTRE_AGENTES)
@@ -240,9 +231,21 @@ def orquestrador(texto, anexos_list, history, json_config, contexto_objetivo):
240
  res, sucesso = executar_no(timeline, cfg)
241
  resposta_content = res.get('content', '')
242
 
243
- if verificar_stop(resposta_content):
244
- print("🛑 STOP_PIPELINE detectado - encerrando")
245
- texto_final = str(resposta_content).replace(STOP_KEYWORD, "").strip()
 
 
 
 
 
 
 
 
 
 
 
 
246
  history[-1]["content"] = texto_final
247
  yield history, audit_data, contexto_persistente
248
  return
@@ -258,91 +261,90 @@ def orquestrador(texto, anexos_list, history, json_config, contexto_objetivo):
258
  yield history, audit_data, contexto_persistente
259
  return
260
 
261
- audit_data.append({k: v for k, v in res.items() if k != 'raw'})
 
 
262
 
263
- if idx == len(protocolo) - 1 or cfg.get('tipo_saida') == 'texto':
264
- texto_final = json.dumps(resposta_content, ensure_ascii=False, indent=2) if not isinstance(resposta_content, str) else resposta_content
265
  history[-1]["content"] = texto_final
266
  yield history, audit_data, contexto_persistente
267
 
268
  print("🏁 Pipeline concluída com sucesso\n" + "="*80)
269
 
270
- # ==================== 5. UI (GRADIO) - LÓGICA V39 ====================
271
-
272
- # Função de inicialização que chama o orquestrador e gerencia a UI (da v39)
273
- def iniciar_analise(usar_data_md, anexo_inicial, objetivo, json_config):
274
- print("🚀 ANÁLISE INICIAL ACIONADA")
275
- prompt_inicial = "Inicie a análise com base nos documentos e no objetivo fornecidos. Apresente um resumo inicial e seus próximos passos."
276
- anexos_para_orquestrador = []
277
 
 
 
 
 
278
  if usar_data_md:
279
  if os.path.exists(ARQUIVO_DATA_MD):
280
  class MockFile:
281
  def __init__(self, name): self.name = name
282
- prompt_inicial += ler_anexo(MockFile(ARQUIVO_DATA_MD))
283
  print(f"✅ Anexado '{ARQUIVO_DATA_MD}'")
284
  else:
285
  print(f"⚠️ Checkbox 'data.md' marcado, mas o arquivo não foi encontrado.")
286
-
287
  if anexo_inicial is not None:
288
- anexos_para_orquestrador.append(anexo_inicial)
289
-
290
- gen = orquestrador(prompt_inicial, anexos_para_orquestrador, [], json_config, objetivo)
 
 
 
 
 
 
291
 
292
- final_history, final_audit, final_contexto = [], [], []
293
- for h, a, c in gen:
294
- final_history, final_audit, final_contexto = h, a, c
295
-
296
- print("✅ Análise inicial concluída. Exibindo chatbot.")
297
- return (
298
- final_history,
299
- final_audit,
300
- final_contexto,
301
- gr.update(visible=False), # Oculta a `start_view`
302
- gr.update(visible=True) # Mostra a `chat_view`
303
- )
 
 
 
 
 
 
 
 
304
 
305
  def ui_clean():
306
- """Constrói e retorna a interface Gradio."""
307
  config_init = carregar_protocolo()
308
  help_init = carregar_help()
309
 
310
- with gr.Blocks(title="AI Forensics - Groq (v39-Compat)") as app:
311
  with gr.Tabs():
312
- # Tab 1: Chat Principal com Lógica v39
313
  with gr.Tab("💬 Investigação"):
314
-
315
- # --- VISÃO INICIAL (SETUP) ---
316
  with gr.Column(visible=True) as start_view:
317
- gr.Markdown("## Iniciar Nova Análise")
318
- gr.Markdown("Configure os dados iniciais para a análise. Após iniciar, esta tela será substituída pelo chat interativo.")
319
-
320
  cb_data_md = gr.Checkbox(label=f"Anexar arquivo de dados principal ({ARQUIVO_DATA_MD})", value=True)
321
  upload_inicial = gr.File(label="Anexar arquivo de texto adicional (Opcional)", file_types=[".txt", ".md", ".json"])
322
  btn_iniciar = gr.Button("🚀 Iniciar Análise", variant="primary")
323
-
324
- # --- VISÃO DO CHAT (INTERAÇÃO) ---
325
  with gr.Column(visible=False) as chat_view:
326
  gr.Markdown("## Investigador AI (Orquestração ADUC-SDR)")
327
- # CORREÇÃO DE COMPATIBILIDADE: Chatbot simples
328
  chatbot = gr.Chatbot(label="Histórico Conversacional", height=500, value=[])
329
-
330
  with gr.Row():
331
  txt_in = gr.Textbox(show_label=False, placeholder="Digite sua mensagem...", lines=2, scale=9)
332
  btn_send = gr.Button("📤 Enviar", variant="primary", scale=1)
333
 
334
- # Tab 2: Configurações de Contexto (Original da v38)
335
- with gr.Tab("📎 Anexos & Contexto"):
336
- gr.Markdown("## Anexos e Contexto Factual\n**Objetivo:** Define o `System Prompt` e a orientação de todos os agentes.")
337
  objetivo_text = gr.Textbox(
338
  label="Objetivo do Modelo (System Prompt Global)",
339
- value="Voce é um agente chamado IndenizaAI...", # (Valor original omitido por brevidade)
340
  placeholder="Ex: Você é um analista forense imparcial...",
341
- lines=5
342
  )
343
- # O upload de anexos para conversas contínuas não é mais usado neste fluxo, mas deixamos a aba para o Objetivo.
344
 
345
- # As outras abas permanecem como na v38
346
  with gr.Tab("🧠 Contexto Persistente"):
347
  contexto_display = gr.JSON(label="Contexto Persistente", value=carregar_contexto_persistente())
348
  with gr.Row():
@@ -372,14 +374,12 @@ def ui_clean():
372
  btn_reload_help = gr.Button("🔄 Recarregar Help")
373
  btn_reload_help.click(lambda: carregar_help(), outputs=help_content)
374
 
375
- # Triggers de Ação com a lógica da v39
376
  btn_iniciar.click(
377
  fn=iniciar_analise,
378
- inputs=[cb_data_md, upload_inicial, objetivo_text, code_json],
379
  outputs=[chatbot, audit_display, contexto_display, start_view, chat_view]
380
  )
381
 
382
- # Anexos agora são passados como um State vazio, pois são tratados apenas no início
383
  btn_send.click(
384
  orquestrador,
385
  [txt_in, gr.State([]), chatbot, code_json, objetivo_text],
@@ -395,6 +395,5 @@ def ui_clean():
395
  return app
396
 
397
  if __name__ == "__main__":
398
- print("🎉 Lançando app Groq v39 (Compat)...")
399
- # CORREÇÃO DE COMPATIBILIDADE: Parâmetro `css` no método .launch()
400
  ui_clean().launch(css="footer{display:none!important;}")
 
1
  # ╔════════════════════════════════════════════════════════════════════════════╗
2
+ # ║ PIPELINE v40: SÍNTESE FINAL COM CHAMADA DIRETA
3
  # ╚════════════════════════════════════════════════════════════════════════════╝
4
  #
5
  # ==================== RESUMO TÉCNICO DA PIPELINE ====================
6
  #
7
+ # OBJETIVO: Versão final que integra todas as melhorias:
 
8
  #
9
+ # 1. FLUXO DE INICIAÇÃO (v39): UI começa com uma tela de "setup" e só
10
+ # exibe o chat após a primeira análise.
11
+ # 2. CHAMADA DE INICIALIZAÇÃO DIRETA: O botão "Iniciar" faz uma chamada
12
+ # única e direta ao modelo, sem usar o pipeline, para uma configuração
13
+ # rápida e eficiente.
14
+ # 3. ORQUESTRADOR DE PIPELINE: O chat interativo subsequente utiliza o
15
+ # `orquestrador` e o `protocolo.json` para análises multiagente.
16
+ # 4. EXTRAÇÃO DE RESPOSTA LIMPA: A lógica do `STOP_PIPELINE` foi aprimorada
17
+ # para extrair a mensagem da chave "proximo_passo" do JSON e exibi-la
18
+ # de forma limpa ao usuário.
19
+ # 5. COMPATIBILIDADE GRADIO: O código é compatível com versões mais antigas
20
+ # da biblioteca Gradio, evitando erros comuns.
 
21
  #
22
  # =====================================================================
23
 
 
37
  ARQUIVO_CONFIG = "protocolo.json"
38
  ARQUIVO_HELP = "help.md"
39
  ARQUIVO_CONTEXTO = "contexto_persistente.json"
40
+ ARQUIVO_DATA_MD = "data.md"
41
  DELAY_ENTRE_AGENTES = 1
42
  STOP_KEYWORD = "STOP_PIPELINE"
43
 
44
+ print("🚀 App inicializada - GROQ v40 (Síntese Final)")
45
  print(f" ✅ Groq: {'OK' if groq_key != 'SUA_GROQ_KEY_AQUI' else '⚠️ placeholder'}")
46
 
47
  # ==================== 2. UTILIDADES ====================
 
48
  def estimar_tokens(texto):
49
  return len(str(texto)) // 4
50
 
 
133
  return stop_detectado
134
 
135
  # ==================== 3. ENGINE DE EXECUÇÃO (GROQ) ====================
 
136
  def executar_no(timeline, config):
137
  print(f"\n🔥 === EXECUTANDO {config['nome']} ===")
138
+ modelo = config.get('modelo', 'llama3-8b-8192')
139
  print(f" Modelo Groq: {modelo}")
140
 
141
  try:
 
162
 
163
  completion = groq_client.chat.completions.create(
164
  model=modelo, messages=messages, temperature=1,
165
+ max_tokens=8192, top_p=1, stream=True, stop=None
166
  )
167
 
168
  out_raw = "".join(chunk.choices[0].delta.content or "" for chunk in completion)
 
196
  return {"role": "system", "error": erro_amistoso, "agent": config['nome']}, False
197
 
198
  # ==================== 4. ORQUESTRADOR COM CONTEXTO PERSISTENTE (ADUC-SDR) ====================
 
199
  def orquestrador(texto, anexos_list, history, json_config, contexto_objetivo):
200
+ print("\n" + "="*80 + "\n🎬 INICIANDO ORQUESTRADOR (PIPELINE)")
201
  if not texto.strip():
202
  yield history, [], carregar_contexto_persistente()
203
  return
 
217
  contexto_inicial = ""
218
  if contexto_objetivo and contexto_objetivo.strip():
219
  contexto_inicial += f"[OBJETIVO DO MODELO]\n{contexto_objetivo.strip()}\n[FIM OBJETIVO]\n\n"
 
 
220
 
221
  timeline = [{"role": msg["role"], "content": msg["content"]} for msg in contexto_persistente[:-1]]
222
  timeline.append({"role": "user", "content": f"{contexto_inicial}{texto}".strip()})
 
224
  audit_data = []
225
  for idx, cfg in enumerate(protocolo):
226
  print(f"\n{'='*50}\n🚀 FASE {idx+1}/{len(protocolo)}: {cfg['nome']}")
 
 
227
  history[-1]["content"] = f"⏳ **Agente {idx+1}/{len(protocolo)}: {cfg['nome']}**..."
228
  yield history, audit_data, contexto_persistente
229
  time.sleep(DELAY_ENTRE_AGENTES)
 
231
  res, sucesso = executar_no(timeline, cfg)
232
  resposta_content = res.get('content', '')
233
 
234
+ if verificar_stop(str(resposta_content)):
235
+ print("🛑 STOP_PIPELINE detectado - extraindo resposta final.")
236
+ texto_final = "Análise concluída."
237
+ json_data = None
238
+ if isinstance(resposta_content, dict):
239
+ json_data = resposta_content
240
+ elif isinstance(resposta_content, str):
241
+ try:
242
+ json_match = re.search(r'\{.*\}', resposta_content, re.DOTALL)
243
+ if json_match: json_data = json.loads(json_match.group(0))
244
+ except json.JSONDecodeError:
245
+ texto_final = str(resposta_content).replace(STOP_KEYWORD, "").strip()
246
+ if json_data and 'proximo_passo' in json_data and isinstance(json_data['proximo_passo'], str):
247
+ texto_final = json_data['proximo_passo']
248
+ texto_final = texto_final.replace(STOP_KEYWORD, "").strip()
249
  history[-1]["content"] = texto_final
250
  yield history, audit_data, contexto_persistente
251
  return
 
261
  yield history, audit_data, contexto_persistente
262
  return
263
 
264
+ audit_entry = {k:v for k,v in res.items() if k not in ['raw', 'content']}
265
+ audit_entry.update({"step": idx + 1, "agent": cfg['nome'], "model": cfg.get('modelo', 'default'), "response_preview": str(resposta_content)[:100] + "...", "sucesso": sucesso})
266
+ audit_data.append(audit_entry)
267
 
268
+ if idx == len(protocolo) - 1:
269
+ texto_final = json.dumps(resposta_content, ensure_ascii=False, indent=2) if not isinstance(resposta_content, str) else str(resposta_content)
270
  history[-1]["content"] = texto_final
271
  yield history, audit_data, contexto_persistente
272
 
273
  print("🏁 Pipeline concluída com sucesso\n" + "="*80)
274
 
275
+ # ==================== 5. UI (GRADIO) ====================
 
 
 
 
 
 
276
 
277
+ def iniciar_analise(usar_data_md, anexo_inicial, objetivo):
278
+ print("\n" + "="*80 + "\n🚀 ANÁLISE INICIAL ACIONADA (CHAMADA DIRETA)")
279
+
280
+ prompt_inicial_docs = ""
281
  if usar_data_md:
282
  if os.path.exists(ARQUIVO_DATA_MD):
283
  class MockFile:
284
  def __init__(self, name): self.name = name
285
+ prompt_inicial_docs += ler_anexo(MockFile(ARQUIVO_DATA_MD))
286
  print(f"✅ Anexado '{ARQUIVO_DATA_MD}'")
287
  else:
288
  print(f"⚠️ Checkbox 'data.md' marcado, mas o arquivo não foi encontrado.")
 
289
  if anexo_inicial is not None:
290
+ prompt_inicial_docs += ler_anexo(anexo_inicial)
291
+ if not prompt_inicial_docs:
292
+ prompt_inicial_docs = "Nenhum documento foi fornecido para análise."
293
+
294
+ config_inicializacao = {
295
+ "nome": "Agente_Inicializador",
296
+ "missao": f"Sua tarefa é analisar o objetivo do usuário e os documentos anexados, fornecer um resumo conciso e descrever os próximos passos sugeridos.\n\nOBJETIVO DO USUÁRIO:\n{objetivo}",
297
+ "modelo": "llama3-8b-8192"
298
+ }
299
 
300
+ timeline_inicial = [{"role": "user", "content": prompt_inicial_docs}]
301
+ res, sucesso = executar_no(timeline_inicial, config_inicializacao)
302
+
303
+ if sucesso:
304
+ resposta_content = res.get('content', 'Não foi possível gerar um resumo inicial.')
305
+ history = [
306
+ {"role": "user", "content": "Análise iniciada com base nos documentos fornecidos."},
307
+ {"role": "assistant", "content": resposta_content}
308
+ ]
309
+ contexto_persistente = carregar_contexto_persistente()
310
+ contexto_persistente.append({"role": "user", "content": f"[USUÁRIO] {prompt_inicial_docs}", "timestamp": datetime.now().strftime('%Y-%m-%d %H:%M:%S')})
311
+ contexto_persistente.append({"role": "assistant", "agent": config_inicializacao['nome'], "content": f"[{config_inicializacao['nome']}] {resposta_content}", "timestamp": datetime.now().strftime('%Y-%m-%d %H:%M:%S')})
312
+ salvar_contexto_persistente(contexto_persistente)
313
+ audit_data = [{"step": 1, "agent": config_inicializacao['nome'], "model": config_inicializacao.get('modelo'), "response_preview": str(resposta_content)[:100] + "...", "sucesso": True, "tempo": res.get('tempo'), "tokens_input": res.get('tokens_input'), "tokens_output": res.get('tokens_output')}]
314
+ print("✅ Análise inicial (chamada direta) concluída. Exibindo chatbot.")
315
+ return history, audit_data, contexto_persistente, gr.update(visible=False), gr.update(visible=True)
316
+ else:
317
+ erro_msg = res.get("error", "Ocorreu um erro desconhecido durante a inicialização.")
318
+ history = [{"role": "user", "content": "Tentativa de iniciar análise."}, {"role": "assistant", "content": f"❌ **Falha na Inicialização:**\n\n{erro_msg}"}]
319
+ return history, [], [], gr.update(visible=True), gr.update(visible=False)
320
 
321
  def ui_clean():
 
322
  config_init = carregar_protocolo()
323
  help_init = carregar_help()
324
 
325
+ with gr.Blocks(title="AI Forensics - Groq (v40)") as app:
326
  with gr.Tabs():
 
327
  with gr.Tab("💬 Investigação"):
 
 
328
  with gr.Column(visible=True) as start_view:
329
+ gr.Markdown("## Iniciar Nova Análise\nConfigure os dados iniciais. Após iniciar, esta tela será substituída pelo chat interativo.")
 
 
330
  cb_data_md = gr.Checkbox(label=f"Anexar arquivo de dados principal ({ARQUIVO_DATA_MD})", value=True)
331
  upload_inicial = gr.File(label="Anexar arquivo de texto adicional (Opcional)", file_types=[".txt", ".md", ".json"])
332
  btn_iniciar = gr.Button("🚀 Iniciar Análise", variant="primary")
 
 
333
  with gr.Column(visible=False) as chat_view:
334
  gr.Markdown("## Investigador AI (Orquestração ADUC-SDR)")
 
335
  chatbot = gr.Chatbot(label="Histórico Conversacional", height=500, value=[])
 
336
  with gr.Row():
337
  txt_in = gr.Textbox(show_label=False, placeholder="Digite sua mensagem...", lines=2, scale=9)
338
  btn_send = gr.Button("📤 Enviar", variant="primary", scale=1)
339
 
340
+ with gr.Tab("🎯 Objetivo & Configs"):
 
 
341
  objetivo_text = gr.Textbox(
342
  label="Objetivo do Modelo (System Prompt Global)",
343
+ value="Voce é um agente chamado IndenizaAI...",
344
  placeholder="Ex: Você é um analista forense imparcial...",
345
+ lines=8
346
  )
 
347
 
 
348
  with gr.Tab("🧠 Contexto Persistente"):
349
  contexto_display = gr.JSON(label="Contexto Persistente", value=carregar_contexto_persistente())
350
  with gr.Row():
 
374
  btn_reload_help = gr.Button("🔄 Recarregar Help")
375
  btn_reload_help.click(lambda: carregar_help(), outputs=help_content)
376
 
 
377
  btn_iniciar.click(
378
  fn=iniciar_analise,
379
+ inputs=[cb_data_md, upload_inicial, objetivo_text],
380
  outputs=[chatbot, audit_display, contexto_display, start_view, chat_view]
381
  )
382
 
 
383
  btn_send.click(
384
  orquestrador,
385
  [txt_in, gr.State([]), chatbot, code_json, objetivo_text],
 
395
  return app
396
 
397
  if __name__ == "__main__":
398
+ print("🎉 Lançando app Groq v40 (Síntese Final)...")
 
399
  ui_clean().launch(css="footer{display:none!important;}")