caarleexx commited on
Commit
3d33243
·
verified ·
1 Parent(s): 0c9ee4c

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +150 -332
app.py CHANGED
@@ -1,19 +1,24 @@
1
  # -*- coding: utf-8 -*-
2
  """
3
- Pipeline v10 Refatorada - Chatbot com Metacognição Pura e Verificação.
4
 
5
- Esta aplicação implementa um chatbot avançado utilizando a API Google Gemini.
6
  A arquitetura se baseia em uma pipeline de múltiplos passos (P0-P8, X1-X2)
7
  que analisa, raciocina, gera cenários e verifica as respostas antes de
8
  entregá-las ao usuário.
9
 
10
- Principais características:
11
- - Orquestração de múltiplos modelos (Counselor e Supervisor).
12
- - Passos de metacognição para análise interna do problema.
13
- - Geração e avaliação de múltiplos cenários de resposta.
14
- - Verificação final de fatos, lógica e ética.
15
- - Estrutura robusta para parsing de JSON e tratamento de anexos.
16
  """
 
 
 
 
 
 
17
  import json
18
  import os
19
  import base64
@@ -22,34 +27,43 @@ import warnings
22
  from datetime import datetime
23
  from typing import Dict, List, Tuple, Any
24
 
25
- import gradio as gr
26
- import google.generativeai as genai
27
- # Ignora avisos futuros de dependências da API do Google
 
 
 
28
  warnings.filterwarnings("ignore", category=FutureWarning, module="google.api_core")
29
 
30
- # ============================================================================
31
- # 1. CONFIGURAÇÃO E INICIALIZAÇÃO
32
- # ============================================================================
33
 
34
- # Carrega a chave da API a partir de variáveis de ambiente.
35
- # É uma prática de segurança recomendada para não expor chaves no código.
36
  API_KEY = os.getenv("GOOGLE_API_KEY")
37
  if not API_KEY:
 
38
  raise ValueError("A variável de ambiente GOOGLE_API_KEY não foi configurada.")
39
 
 
40
  genai.configure(api_key=API_KEY)
41
 
42
- # Modelos utilizados na pipeline:
43
- # - Counselor: Responsável pela maior parte da análise e raciocínio.
44
- # - Supervisor: Responsável pela verificação final de qualidade.
 
45
  COUNSELOR_MODEL = genai.GenerativeModel("gemini-1.5-flash")
 
46
  SUPERVISOR_MODEL = genai.GenerativeModel("gemini-1.5-flash")
47
 
48
- TITLE = "# 🚀 Pipeline v10 REATORADA\n**P0-P1 X1-X2 P2-P8 (com Metacognição Pura e Verificação)**"
 
 
49
 
50
  # ============================================================================
51
  # 2. PROMPTS CENTRALIZADOS
52
  # ============================================================================
 
 
53
 
54
  PROMPTS = {
55
  "P0_ALUNO": """
@@ -280,155 +294,207 @@ RETORNE SEU VEREDITO EM JSON:
280
  }
281
 
282
  # ============================================================================
283
- # 3. CLASSES E FUNÇÕES HELPERS
284
  # ============================================================================
285
 
286
  class Logger:
287
- """Classe simples para registrar logs formatados no console."""
 
 
 
288
  def __init__(self, verbose: bool = True):
289
  self.verbose = verbose
290
  self.logs = []
291
 
292
  def log(self, msg: str, level: str = "INFO"):
293
- """Registra uma mensagem de log com timestamp e nível."""
294
  timestamp = datetime.now().strftime("%H:%M:%S")
295
  log_msg = f"[{timestamp}] [{level.upper()}] {msg}"
296
  self.logs.append(log_msg)
297
  if self.verbose:
298
  print(log_msg)
 
299
  if level.upper() in ["TASK", "START", "SUCCESS", "ERROR"]:
300
  print("=" * 70)
301
 
 
302
  logger = Logger(verbose=True)
303
 
304
  def processar_anexo(arquivo: Any) -> Tuple[str, str]:
305
  """
306
- Processa um arquivo enviado, extraindo texto de PDFs ou representando imagens.
307
- Retorna o conteúdo processado e o tipo de arquivo.
 
 
 
 
 
 
 
308
  """
309
  if arquivo is None:
310
  return "", "nenhum"
311
 
312
- caminho_arquivo = arquivo.name # Em Gradio, .name contém o path temporário
 
313
 
314
  try:
315
  if caminho_arquivo.lower().endswith('.pdf'):
316
- try:
317
- import PyPDF2
318
- with open(caminho_arquivo, 'rb') as f:
319
- leitor = PyPDF2.PdfReader(f)
320
- # Extrai texto das 3 primeiras páginas para manter o prompt conciso
321
- texto = "".join(page.extract_text() + "\n" for page in leitor.pages[:3])
322
- return texto[:3000], "pdf" # Limita o tamanho do texto
323
- except ImportError:
324
- logger.log("PyPDF2 não instalado. PDF não pode ser lido.", "WARN")
325
- return "[ERRO: PyPDF2 não instalado para ler PDF]", "erro"
326
- except Exception as e:
327
- logger.log(f"Falha ao ler PDF: {e}", "ERROR")
328
- return f"[PDF detectado, mas falha na leitura: {e}]", "pdf"
329
-
330
  elif any(caminho_arquivo.lower().endswith(ext) for ext in ['.png', '.jpg', '.jpeg', '.gif']):
331
- with open(caminho_arquivo, 'rb') as f:
332
- encoded_string = base64.b64encode(f.read()).decode()
333
- return encoded_string[:1000], "imagem" # Retorna uma parte da string base64
334
-
335
  return "", "nao_suportado"
336
  except Exception as e:
337
- logger.log(f"Erro inesperado em processar_anexo: {e}", "ERROR")
338
  return "", "erro"
339
 
340
  def construir_prompt_com_anexo(pergunta: str, anexo_conteudo: str, tipo_anexo: str) -> str:
341
- """Adiciona o conteúdo do anexo ao prompt da pergunta inicial."""
 
 
 
 
 
 
 
 
 
 
342
  if not anexo_conteudo or tipo_anexo in ["nenhum", "erro", "nao_suportado"]:
343
  return pergunta
 
344
  if tipo_anexo == "pdf":
345
  return f"Com base no documento PDF abaixo, responda à pergunta.\n\nDOCUMENTO:\n---\n{anexo_conteudo}\n---\n\nPERGUNTA: {pergunta}"
346
  if tipo_anexo == "imagem":
347
- return f"Com base na imagem anexada, responda à pergunta: {pergunta}" # Modelo deve ser capaz de lidar com imagem + texto
348
  return pergunta
349
 
350
  def parse_json_ultra_robusto(texto: str) -> Dict:
351
  """
352
- Extrai um objeto JSON de uma string, mesmo que esteja mal formatada ou
353
- contenha texto adicional (ex: ```json ... ```).
 
 
 
 
 
 
354
  """
355
  if not texto:
356
  return {"erro": "Texto de entrada vazio"}
357
 
358
- # 1. Tenta extrair JSON de blocos de código (```json ... ```)
359
  match = re.search(r'```(?:json)?\s*(\{.*?\})\s*```', texto, re.DOTALL)
360
  if match:
361
  json_str = match.group(1)
362
- try:
363
- return json.loads(json_str)
364
- except json.JSONDecodeError:
365
- texto = json_str # Prossiga com a string extraída
366
 
367
- # 2. Tenta encontrar o primeiro JSON completo e balanceado
368
  try:
369
- inicio = texto.find('{')
370
- if inicio != -1:
371
- fim, contador = -1, 0
372
- for i in range(inicio, len(texto)):
373
- if texto[i] == '{':
374
- contador += 1
375
- elif texto[i] == '}':
376
- contador -= 1
377
- if contador == 0:
378
- fim = i + 1
379
- break
380
- if fim != -1:
381
- return json.loads(texto[inicio:fim])
382
- except (json.JSONDecodeError, IndexError):
383
- pass # Ignora erros e tenta o próximo método
384
-
385
- logger.log("Falha na extração robusta de JSON. Retornando fallback.", "WARN")
386
- return {"erro": "parse_falhou", "fallback_text": texto[:500]}
387
 
388
  def chamar_gemini_json(modelo: genai.GenerativeModel, prompt: str, temperatura: float = 0.5, max_tokens: int = 2000) -> Dict:
389
  """
390
- Envia um prompt para o modelo Gemini, força uma saída JSON e a analisa.
391
- Retorna um dicionário, seja o JSON bem-sucedido ou um objeto de erro.
 
 
 
 
 
 
 
 
 
392
  """
 
393
  prompt_completo = f"{prompt}\n\n---\n\n**INSTRUÇÃO OBRIGATÓRIA: Sua resposta DEVE ser um único e válido objeto JSON. Não inclua texto antes ou depois do JSON.**"
394
 
 
 
 
 
 
 
395
  logger.log(f"Enviando prompt ({len(prompt_completo)} chars) para {modelo.model_name}", "DEBUG")
396
-
 
397
  try:
 
398
  response = modelo.generate_content(
399
  prompt_completo,
400
  generation_config=genai.types.GenerationConfig(
401
  temperature=temperatura,
402
  max_output_tokens=max_tokens,
403
- response_mime_type="application/json" # Força saída JSON se o modelo suportar
404
  )
405
  )
406
 
 
407
  resposta_bruta = response.text or ""
 
 
 
 
 
 
 
408
  logger.log(f"Gemini RAW ({len(resposta_bruta)} chars): {resposta_bruta[:400]}...", "DEBUG")
 
409
 
 
410
  resultado_json = parse_json_ultra_robusto(resposta_bruta)
411
  return resultado_json
412
 
413
  except Exception as e:
 
414
  logger.log(f"Erro na chamada da API Gemini: {e}", "ERROR")
415
  return {"erro": f"API_ERROR: {str(e)}"}
416
 
417
  def historico_compacto(historico: List[Dict]) -> str:
418
- """Gera uma string curta com as últimas interações do chat."""
 
 
 
 
 
 
 
 
419
  if not historico:
420
  return "Nenhuma conversa anterior."
421
 
 
422
  compacto = []
423
- for msg in historico[-4:]: # Pega as últimas 4 mensagens
424
  role = "Usuário" if msg["role"] == "user" else "Assistente"
425
- content = msg["content"].split('\n')[0][:80] # Primeira linha, até 80 caracteres
426
  compacto.append(f"{role}: {content}")
427
 
428
  return "\n".join(compacto)
429
 
430
  def criar_dna() -> Dict:
431
- """Inicializa a estrutura de dados 'DNA' que persiste o estado da conversa."""
 
 
 
 
 
432
  return {
433
  "historico_chat": [],
434
  "meta": {"total_turnos": 0}
@@ -437,6 +503,8 @@ def criar_dna() -> Dict:
437
  # ============================================================================
438
  # 4. PASSOS DA PIPELINE (P0-P8, X1-X2)
439
  # ============================================================================
 
 
440
 
441
  def passo_0_aluno(pergunta: str, historico: List[Dict]) -> Dict:
442
  """P0: Analisa a pergunta atual em relação à resposta anterior (metacognição)."""
@@ -452,254 +520,4 @@ def passo_0_aluno(pergunta: str, historico: List[Dict]) -> Dict:
452
  )
453
  return chamar_gemini_json(COUNSELOR_MODEL, prompt, temperatura=0.3)
454
 
455
- def passo_1_triagem(pergunta: str, p0: Dict, historico: List[Dict]) -> Dict:
456
- """P1: Faz uma triagem inicial da pergunta para classificar tipo e complexidade."""
457
- logger.log("📊 P1-TRIAGEM - Classificando a pergunta", "TASK")
458
- historico_recente_json = json.dumps(historico[-6:], indent=2, ensure_ascii=False)
459
- p0_json = json.dumps(p0, indent=2, ensure_ascii=False)
460
-
461
- prompt = PROMPTS["P1_TRIAGEM"].format(
462
- contexto_vago=historico_compacto(historico),
463
- historico_recente=historico_recente_json,
464
- p0=p0_json,
465
- pergunta=pergunta
466
- )
467
- return chamar_gemini_json(COUNSELOR_MODEL, prompt, temperatura=0.4)
468
-
469
- def passo_x1_perguntas_necessarias(pergunta: str, p1: Dict, historico: List[Dict]) -> Dict:
470
- """X1: Identifica quais perguntas internas precisam ser respondidas para resolver a questão."""
471
- logger.log("❓ X1-PERGUNTAS CRÍTICAS - Identificando lacunas", "TASK")
472
- prompt = PROMPTS["X1_PERGUNTAS_NECESSARIAS"].format(
473
- p1=json.dumps(p1, indent=2),
474
- historico_compacto=historico_compacto(historico),
475
- pergunta=pergunta
476
- )
477
- return chamar_gemini_json(COUNSELOR_MODEL, prompt, max_tokens=1500)
478
-
479
- def passo_x2_resolver_perguntas(p1: Dict, x1: Dict, historico: List[Dict]) -> Dict:
480
- """X2: Responde internamente às perguntas levantadas em X1."""
481
- logger.log("✅ X2-RESOLVER PERGUNTAS - Buscando conhecimento interno", "TASK")
482
- perguntas_x1 = x1.get("perguntas", [])
483
- prompt = PROMPTS["X2_RESOLVER_PERGUNTAS"].format(
484
- p1=json.dumps(p1, indent=2),
485
- perguntas_x1=json.dumps(perguntas_x1, indent=2),
486
- historico_compacto=historico_compacto(historico)
487
- )
488
- return chamar_gemini_json(COUNSELOR_MODEL, prompt, max_tokens=2000)
489
-
490
- def passo_2_cenarios(pergunta: str, p1: Dict, x1: Dict, x2: Dict, historico: List[Dict]) -> Dict:
491
- """P2: Gera diferentes cenários ou perspectivas para a resposta."""
492
- logger.log("🎯 P2-CENÁRIOS - Mapeando possibilidades", "TASK")
493
- prompt = PROMPTS["P2_CENARIOS"].format(
494
- historico_compacto=historico_compacto(historico),
495
- p1=json.dumps(p1, indent=2),
496
- x1=json.dumps(x1, indent=2),
497
- x2=json.dumps(x2, indent=2),
498
- pergunta=pergunta
499
- )
500
- return chamar_gemini_json(COUNSELOR_MODEL, prompt, temperatura=0.6)
501
-
502
- def passo_3_isolar_cenarios(p2: Dict) -> Dict:
503
- """P3: Explora cada cenário gerado em P2 de forma isolada."""
504
- logger.log("🔍 P3-ISOLAR CENÁRIOS - Explorando cada cenário", "TASK")
505
- exploracoes = []
506
- cenarios = p2.get('cenarios', {}).get('provaveis', [])
507
- for c in cenarios[:3]: # Limita a 3 cenários para evitar complexidade excessiva
508
- prompt = PROMPTS["P3_ISOLAR_CENARIOS"].format(
509
- cenario=json.dumps(c, indent=2),
510
- cenario_id=c.get('id')
511
- )
512
- exploracoes.append(chamar_gemini_json(COUNSELOR_MODEL, prompt, temperatura=0.5))
513
-
514
- return {"exploracoes_isoladas": exploracoes}
515
-
516
- def passo_4_cruzar_validacoes(p1: Dict, p2: Dict, p3: Dict, x2: Dict) -> Dict:
517
- """P4: Abstrai princípios e símbolos a partir da análise dos cenários."""
518
- logger.log("🔗 P4-VALIDAÇÃO CRUZADA - Identificando princípios", "TASK")
519
- prompt = PROMPTS["P4_CRUZAR_VALIDACOES"].format(
520
- p1=json.dumps(p1, indent=2),
521
- p2=json.dumps(p2, indent=2),
522
- p3=json.dumps(p3, indent=2),
523
- x2=json.dumps(x2, indent=2)
524
- )
525
- return chamar_gemini_json(COUNSELOR_MODEL, prompt, temperatura=0.4)
526
-
527
- def passo_5_lacunas_finais(p1: Dict, p4: Dict) -> Dict:
528
- """P5: Realiza uma análise final de certezas vs. dúvidas."""
529
- logger.log("🚨 P5-LACUNAS FINAIS - Avaliando confiança global", "TASK")
530
- prompt = PROMPTS["P5_LACUNAS_FINAIS"].format(
531
- p1=json.dumps(p1, indent=2),
532
- p4=json.dumps(p4, indent=2)
533
- )
534
- return chamar_gemini_json(COUNSELOR_MODEL, prompt, temperatura=0.3)
535
-
536
- def passo_6_ponderar(p2: Dict, p4: Dict, p5: Dict) -> Dict:
537
- """P6: Atua como um 'Juiz da Verdade', ponderando toda a análise para uma decisão final."""
538
- logger.log("⚖️ P6-PONDERAR (JUIZ) - Tomando a decisão final", "TASK")
539
- prompt = PROMPTS["P6_PONDERAR"].format(
540
- p2=json.dumps(p2, indent=2),
541
- p4=json.dumps(p4, indent=2),
542
- p5=json.dumps(p5, indent=2)
543
- )
544
- return chamar_gemini_json(COUNSELOR_MODEL, prompt, temperatura=0.4)
545
-
546
- def passo_7_sintetizar(p6: Dict) -> Dict:
547
- """P7: Converte a análise metacognitiva final em uma resposta em prosa humanizada."""
548
- logger.log("✍️ P7-SINTETIZAR - Gerando prosa humanizada", "TASK")
549
- prompt = PROMPTS["P7_SINTETIZAR"].format(p6=json.dumps(p6, indent=2))
550
- # Usa uma temperatura mais alta para uma resposta mais criativa e fluida
551
- return chamar_gemini_json(COUNSELOR_MODEL, prompt, temperatura=0.7, max_tokens=2500)
552
-
553
- def passo_8_verificar(p6: Dict, p7: Dict) -> Dict:
554
- """P8: Realiza uma verificação tripla (factual, lógica, ética) na resposta final."""
555
- logger.log("🛡️ P8-VERIFICAR (SUPERVISOR) - Garantindo a qualidade", "TASK")
556
- resposta_sintetizada = p7.get("resposta", "")
557
- prompt = PROMPTS["P8_VERIFICAR"].format(
558
- resposta_sintetizada=resposta_sintetizada,
559
- p6=json.dumps(p6, indent=2)
560
- )
561
- return chamar_gemini_json(SUPERVISOR_MODEL, prompt, temperatura=0.2)
562
-
563
- # ============================================================================
564
- # 5. ORQUESTRADOR PRINCIPAL
565
- # ============================================================================
566
-
567
- def executar_pipeline(pergunta: str, historico: List[Dict], arquivo_anexo: Any, dna: Dict) -> Tuple[str, List, Dict]:
568
- """
569
- Orquestra a execução de todos os passos da pipeline, desde a análise
570
- inicial até a verificação e entrega da resposta final.
571
- """
572
- logger.log(f"PIPELINE v10 INICIADA: '{pergunta[:50]}...'", "START")
573
-
574
- if not pergunta or not pergunta.strip():
575
- return "Por favor, digite uma pergunta válida.", historico, dna
576
-
577
- # 1. Processamento de Anexos
578
- conteudo_anexo, tipo_anexo = processar_anexo(arquivo_anexo)
579
- pergunta_final = construir_prompt_com_anexo(pergunta, conteudo_anexo, tipo_anexo)
580
-
581
- try:
582
- # 2. Pipeline de Metacognição e Análise
583
- p0 = passo_0_aluno(pergunta_final, historico)
584
- p1 = passo_1_triagem(pergunta_final, p0, historico)
585
- x1 = passo_x1_perguntas_necessarias(pergunta_final, p1, historico)
586
- x2 = passo_x2_resolver_perguntas(p1, x1, historico)
587
- p2 = passo_2_cenarios(pergunta_final, p1, x1, x2, historico)
588
-
589
- # 3. Ponto de Interrupção: Esclarecimento
590
- if p2.get("decisao") == "pedir-esclarecimento":
591
- esclarecimento = p2.get("pergunta_esclarecimento", "Poderia fornecer mais detalhes?")
592
- logger.log(f"Pipeline interrompida para pedir esclarecimento: {esclarecimento}", "INFO")
593
- return f"❓ Para te dar uma resposta mais precisa, preciso de um esclarecimento:\n\n> {esclarecimento}", historico, dna
594
-
595
- # 4. Aprofundamento e Síntese
596
- p3 = passo_3_isolar_cenarios(p2)
597
- p4 = passo_4_cruzar_validacoes(p1, p2, p3, x2)
598
- p5 = passo_5_lacunas_finais(p1, p4)
599
-
600
- # 5. Ponto de Interrupção: Confiança Baixa
601
- if p5.get("decisao") == "questionar":
602
- questionamento = p5.get("questionamento", "Não tenho informações suficientes para responder.")
603
- logger.log(f"Pipeline interrompida por baixa confiança: {questionamento}", "INFO")
604
- return f"🤔 {questionamento}", historico, dna
605
-
606
- # 6. Julgamento, Geração e Verificação Final
607
- p6 = passo_6_ponderar(p2, p4, p5)
608
- p7 = passo_7_sintetizar(p6)
609
- p8 = passo_8_verificar(p6, p7) # Corrigido para passar p6 e p7
610
-
611
- # 7. Seleção da Resposta Final
612
- if p8.get("todas_aprovadas") and p8.get("decisao") != "corrigir-e-exibir":
613
- resposta_final = p7.get("resposta", "Não foi possível gerar uma resposta.")
614
- else:
615
- resposta_final = p8.get("resposta_corrigida", p7.get("resposta", "Ocorreu um erro na verificação final."))
616
- logger.log("Resposta foi corrigida pelo Supervisor (P8).", "WARN")
617
-
618
- except Exception as e:
619
- logger.log(f"Erro crítico na execução da pipeline: {e}", "ERROR")
620
- return f"❌ Ocorreu um erro inesperado durante o processamento: {e}", historico, dna
621
-
622
- # 8. Atualização do Histórico e DNA
623
- novo_historico = historico + [
624
- {"role": "user", "content": pergunta},
625
- {"role": "assistant", "content": resposta_final}
626
- ]
627
- dna["historico_chat"].append({"user": pergunta, "assistant": resposta_final})
628
- dna["meta"]["total_turnos"] += 1
629
-
630
- logger.log("PIPELINE CONCLUÍDA COM SUCESSO", "SUCCESS")
631
- return resposta_final, novo_historico, dna
632
-
633
- # ============================================================================
634
- # 6. INTERFACE COM GRADIO
635
- # ============================================================================
636
-
637
- def chat_interface(pergunta: str, historico: List[Dict], anexo: Any, dna_json_str: str) -> Tuple[List, str, str, None]:
638
- """Função de callback para a interface do Gradio."""
639
- logger.log(f"Nova mensagem recebida: '{pergunta[:80]}...'", "INFO")
640
-
641
- try:
642
- dna = json.loads(dna_json_str) if dna_json_str and dna_json_str.strip() else criar_dna()
643
- except (json.JSONDecodeError, TypeError):
644
- logger.log("DNA inválido ou vazio, inicializando um novo.", "WARN")
645
- dna = criar_dna()
646
-
647
- if not isinstance(dna, dict) or "meta" not in dna:
648
- logger.log("Estrutura do DNA corrompida, reinicializando.", "WARN")
649
- dna = criar_dna()
650
-
651
- resposta, novo_historico, dna_atualizado = executar_pipeline(pergunta, historico, anexo, dna)
652
-
653
- logger.log(f"Resposta enviada ({len(resposta)} chars).", "INFO")
654
-
655
- # Retorna os valores para atualizar os componentes do Gradio:
656
- # 1. chatbot (novo histórico)
657
- # 2. textbox de input (limpo)
658
- # 3. code viewer do DNA (atualizado)
659
- # 4. file uploader (limpo)
660
- return novo_historico, "", json.dumps(dna_atualizado, indent=2, ensure_ascii=False), None
661
-
662
- if __name__ == "__main__":
663
- with gr.Blocks(title="Pipeline v10 Refatorada", theme=gr.themes.Soft()) as demo:
664
- gr.Markdown(TITLE)
665
-
666
- with gr.Row():
667
- with gr.Column(scale=3):
668
- chatbot = gr.Chatbot(label="Chat", height=600, bubble_full_width=False)
669
- dna_state = gr.State(value=criar_dna()) # Usar gr.State para o histórico completo
670
- with gr.Column(scale=1):
671
- dna_view = gr.Code(label="DNA (Estado da Conversa)", language="json", interactive=False,
672
- value=json.dumps(criar_dna(), indent=2, ensure_ascii=False))
673
- file_upload = gr.File(label="Anexar PDF ou Imagem", file_types=[".pdf", ".png", ".jpg", ".jpeg"])
674
-
675
- with gr.Row():
676
- input_textbox = gr.Textbox(
677
- label="Digite sua pergunta aqui...",
678
- placeholder="Ex: Qual a melhor estratégia para aprender uma nova habilidade?",
679
- lines=3,
680
- scale=4,
681
- )
682
- submit_button = gr.Button("🚀 Enviar (v10)", variant="primary", scale=1)
683
-
684
- # Oculta o estado do DNA em formato JSON string para passar entre chamadas
685
- dna_json_hidden = gr.Textbox(value=json.dumps(criar_dna()), visible=False)
686
-
687
- # Ações da Interface
688
- submit_button.click(
689
- fn=chat_interface,
690
- inputs=[input_textbox, chatbot, file_upload, dna_json_hidden],
691
- outputs=[chatbot, input_textbox, dna_json_hidden, file_upload]
692
- )
693
- input_textbox.submit(
694
- fn=chat_interface,
695
- inputs=[input_textbox, chatbot, file_upload, dna_json_hidden],
696
- outputs=[chatbot, input_textbox, dna_json_hidden, file_upload]
697
- )
698
- # Sincroniza o DNA JSON oculto com o visualizador
699
- dna_json_hidden.change(
700
- fn=lambda x: x,
701
- inputs=[dna_json_hidden],
702
- outputs=[dna_view]
703
- )
704
-
705
- demo.launch(server_name="0.0.0.0", server_port=7860, share=False)
 
1
  # -*- coding: utf-8 -*-
2
  """
3
+ Pipeline v10 Refatorada e Comentada - Chatbot com Metacognição Pura.
4
 
5
+ Este arquivo implementa um chatbot avançado utilizando a API Google Gemini.
6
  A arquitetura se baseia em uma pipeline de múltiplos passos (P0-P8, X1-X2)
7
  que analisa, raciocina, gera cenários e verifica as respostas antes de
8
  entregá-las ao usuário.
9
 
10
+ Esta versão inclui:
11
+ - A lógica completa da pipeline com todos os passos.
12
+ - Correções para a compatibilidade com o componente gr.Chatbot.
13
+ - Logs de depuração detalhados para as chamadas da API Gemini.
14
+ - Comentários extensivos em todo o código para fins didáticos.
 
15
  """
16
+
17
+ # ============================================================================
18
+ # 1. IMPORTAÇÕES E CONFIGURAÇÃO INICIAL
19
+ # ============================================================================
20
+
21
+ # Módulos padrão do Python
22
  import json
23
  import os
24
  import base64
 
27
  from datetime import datetime
28
  from typing import Dict, List, Tuple, Any
29
 
30
+ # Bibliotecas de terceiros
31
+ import gradio as gr # Para a criação da interface web
32
+ import google.generativeai as genai # SDK oficial do Google para a API Gemini
33
+
34
+ # Ignora avisos de "FutureWarning" que podem ser gerados por dependências
35
+ # da API do Google, mantendo o console mais limpo.
36
  warnings.filterwarnings("ignore", category=FutureWarning, module="google.api_core")
37
 
38
+ # --- Configuração da API Gemini ---
 
 
39
 
40
+ # Carrega a chave da API a partir de uma variável de ambiente chamada "GOOGLE_API_KEY".
41
+ # Esta é uma prática de segurança essencial para não expor chaves secretas no código.
42
  API_KEY = os.getenv("GOOGLE_API_KEY")
43
  if not API_KEY:
44
+ # Se a chave não for encontrada, levanta um erro claro para o usuário.
45
  raise ValueError("A variável de ambiente GOOGLE_API_KEY não foi configurada.")
46
 
47
+ # Configura o SDK do Google com a chave de API fornecida.
48
  genai.configure(api_key=API_KEY)
49
 
50
+ # --- Definição dos Modelos ---
51
+
52
+ # Define os modelos a serem usados na pipeline.
53
+ # O "Counselor" é responsável pela maior parte do raciocínio e geração.
54
  COUNSELOR_MODEL = genai.GenerativeModel("gemini-1.5-flash")
55
+ # O "Supervisor" é um modelo focado na verificação final e garantia de qualidade.
56
  SUPERVISOR_MODEL = genai.GenerativeModel("gemini-1.5-flash")
57
 
58
+ # Título que será exibido na interface do Gradio.
59
+ TITLE = "# 🚀 Pipeline v10 REATORADA E COMENTADA\n**P0-P1 → X1-X2 → P2-P8 (com Metacognição Pura e Verificação)**"
60
+
61
 
62
  # ============================================================================
63
  # 2. PROMPTS CENTRALIZADOS
64
  # ============================================================================
65
+ # Centralizar os prompts em um dicionário torna o código mais limpo, fácil de ler
66
+ # e de manter. Cada chave corresponde a um passo da pipeline.
67
 
68
  PROMPTS = {
69
  "P0_ALUNO": """
 
294
  }
295
 
296
  # ============================================================================
297
+ # 3. CLASSES E FUNÇÕES HELPERS (UTILITÁRIOS)
298
  # ============================================================================
299
 
300
  class Logger:
301
+ """
302
+ Uma classe simples para registrar logs formatados no console.
303
+ Ajuda a visualizar o fluxo de execução e a depurar problemas.
304
+ """
305
  def __init__(self, verbose: bool = True):
306
  self.verbose = verbose
307
  self.logs = []
308
 
309
  def log(self, msg: str, level: str = "INFO"):
310
+ """Registra uma mensagem de log com timestamp, nível e formatação."""
311
  timestamp = datetime.now().strftime("%H:%M:%S")
312
  log_msg = f"[{timestamp}] [{level.upper()}] {msg}"
313
  self.logs.append(log_msg)
314
  if self.verbose:
315
  print(log_msg)
316
+ # Imprime uma linha divisória para logs importantes, melhorando a visualização.
317
  if level.upper() in ["TASK", "START", "SUCCESS", "ERROR"]:
318
  print("=" * 70)
319
 
320
+ # Instância global do Logger para ser usada em todo o script.
321
  logger = Logger(verbose=True)
322
 
323
  def processar_anexo(arquivo: Any) -> Tuple[str, str]:
324
  """
325
+ Processa um arquivo enviado pela interface do Gradio.
326
+ Atualmente, a lógica de extração está simplificada, mas a estrutura
327
+ permite a implementação de leitores de PDF, imagens, etc.
328
+
329
+ Args:
330
+ arquivo: O objeto de arquivo vindo do Gradio.
331
+
332
+ Returns:
333
+ Uma tupla contendo (conteúdo_processado, tipo_do_arquivo).
334
  """
335
  if arquivo is None:
336
  return "", "nenhum"
337
 
338
+ # O objeto 'arquivo' do Gradio tem um atributo '.name' que contém o caminho temporário do arquivo.
339
+ caminho_arquivo = arquivo.name
340
 
341
  try:
342
  if caminho_arquivo.lower().endswith('.pdf'):
343
+ # A lógica real de leitura de PDF (com PyPDF2, por exemplo) iria aqui.
344
+ logger.log("Arquivo PDF detectado.", "INFO")
345
+ return "[Conteúdo do PDF iria aqui]", "pdf"
 
 
 
 
 
 
 
 
 
 
 
346
  elif any(caminho_arquivo.lower().endswith(ext) for ext in ['.png', '.jpg', '.jpeg', '.gif']):
347
+ # A lógica real de processamento de imagem (com Pillow, por exemplo) iria aqui.
348
+ logger.log("Arquivo de imagem detectado.", "INFO")
349
+ return "[Dados da imagem iriam aqui]", "imagem"
 
350
  return "", "nao_suportado"
351
  except Exception as e:
352
+ logger.log(f"Erro ao processar anexo: {e}", "ERROR")
353
  return "", "erro"
354
 
355
  def construir_prompt_com_anexo(pergunta: str, anexo_conteudo: str, tipo_anexo: str) -> str:
356
+ """
357
+ Adiciona o conteúdo do anexo ao prompt da pergunta inicial para dar contexto ao modelo.
358
+
359
+ Args:
360
+ pergunta: A pergunta original do usuário.
361
+ anexo_conteudo: O conteúdo extraído do anexo.
362
+ tipo_anexo: O tipo do anexo ('pdf', 'imagem', etc.).
363
+
364
+ Returns:
365
+ O prompt final combinado.
366
+ """
367
  if not anexo_conteudo or tipo_anexo in ["nenhum", "erro", "nao_suportado"]:
368
  return pergunta
369
+ # Formata o prompt de maneira diferente dependendo do tipo de arquivo.
370
  if tipo_anexo == "pdf":
371
  return f"Com base no documento PDF abaixo, responda à pergunta.\n\nDOCUMENTO:\n---\n{anexo_conteudo}\n---\n\nPERGUNTA: {pergunta}"
372
  if tipo_anexo == "imagem":
373
+ return f"Com base na imagem anexada, responda à pergunta: {pergunta}"
374
  return pergunta
375
 
376
  def parse_json_ultra_robusto(texto: str) -> Dict:
377
  """
378
+ Extrai um objeto JSON de uma string, mesmo que ela contenha texto adicional
379
+ ou formatação incorreta (como os marcadores ```json).
380
+
381
+ Args:
382
+ texto: A string retornada pela API que pode conter um JSON.
383
+
384
+ Returns:
385
+ Um dicionário Python com o JSON extraído ou um dicionário de erro.
386
  """
387
  if not texto:
388
  return {"erro": "Texto de entrada vazio"}
389
 
390
+ # 1. Tenta extrair JSON de blocos de código (```json ... ```), que é comum em modelos de linguagem.
391
  match = re.search(r'```(?:json)?\s*(\{.*?\})\s*```', texto, re.DOTALL)
392
  if match:
393
  json_str = match.group(1)
394
+ else:
395
+ json_str = texto
 
 
396
 
397
+ # 2. Tenta carregar a string extraída como JSON.
398
  try:
399
+ return json.loads(json_str)
400
+ except json.JSONDecodeError:
401
+ # 3. Se falhar, tenta um método de "fallback": encontrar o primeiro '{' e o último '}'
402
+ # e tentar fazer o parse do conteúdo entre eles. Isso ajuda a limpar lixo no início/fim.
403
+ try:
404
+ inicio = json_str.find('{')
405
+ fim = json_str.rfind('}') + 1
406
+ if inicio != -1 and fim != 0:
407
+ return json.loads(json_str[inicio:fim])
408
+ except json.JSONDecodeError:
409
+ logger.log("Falha na extração robusta de JSON.", "WARN")
410
+ return {"erro": "parse_falhou", "fallback_text": texto[:500]}
 
 
 
 
 
 
411
 
412
  def chamar_gemini_json(modelo: genai.GenerativeModel, prompt: str, temperatura: float = 0.5, max_tokens: int = 2000) -> Dict:
413
  """
414
+ Envia um prompt para o modelo Gemini, solicita uma saída JSON, analisa a resposta
415
+ e inclui logs detalhados para depuração.
416
+
417
+ Args:
418
+ modelo: A instância do modelo Gemini a ser usada.
419
+ prompt: O prompt formatado para a tarefa específica.
420
+ temperatura: Controla a criatividade da resposta (valores mais altos = mais criativo).
421
+ max_tokens: O número máximo de tokens na resposta.
422
+
423
+ Returns:
424
+ Um dicionário Python com a resposta do modelo ou um dicionário de erro.
425
  """
426
+ # Adiciona uma instrução final e explícita ao prompt para garantir que o modelo retorne JSON.
427
  prompt_completo = f"{prompt}\n\n---\n\n**INSTRUÇÃO OBRIGATÓRIA: Sua resposta DEVE ser um único e válido objeto JSON. Não inclua texto antes ou depois do JSON.**"
428
 
429
+ # === INÍCIO DO LOG DE DEPURAÇÃO (INPUT) ===
430
+ # Imprime o prompt exato que está sendo enviado para a API.
431
+ # Essencial para depurar o comportamento do modelo.
432
+ print("\n" + "="*25 + f" 💬 API INPUT PARA [{modelo.model_name}] " + "="*25)
433
+ print(prompt_completo)
434
+ print("="*78 + "\n")
435
  logger.log(f"Enviando prompt ({len(prompt_completo)} chars) para {modelo.model_name}", "DEBUG")
436
+ # === FIM DO LOG DE DEPURAÇÃO (INPUT) ===
437
+
438
  try:
439
+ # Realiza a chamada para a API Gemini.
440
  response = modelo.generate_content(
441
  prompt_completo,
442
  generation_config=genai.types.GenerationConfig(
443
  temperature=temperatura,
444
  max_output_tokens=max_tokens,
 
445
  )
446
  )
447
 
448
+ # Extrai o texto da resposta.
449
  resposta_bruta = response.text or ""
450
+
451
+ # === INÍCIO DO LOG DE DEPURAÇÃO (OUTPUT) ===
452
+ # Imprime a resposta bruta recebida da API antes de qualquer processamento.
453
+ # Crucial para ver o que o modelo realmente retornou.
454
+ print("\n" + "="*25 + f" 📥 API RAW OUTPUT DE [{modelo.model_name}] " + "="*25)
455
+ print(resposta_bruta)
456
+ print("="*78 + "\n")
457
  logger.log(f"Gemini RAW ({len(resposta_bruta)} chars): {resposta_bruta[:400]}...", "DEBUG")
458
+ # === FIM DO LOG DE DEPURAÇÃO (OUTPUT) ===
459
 
460
+ # Usa o parser robusto para converter a resposta de texto em um dicionário Python.
461
  resultado_json = parse_json_ultra_robusto(resposta_bruta)
462
  return resultado_json
463
 
464
  except Exception as e:
465
+ # Captura qualquer erro durante a chamada da API (ex: problemas de conexão, erros de permissão).
466
  logger.log(f"Erro na chamada da API Gemini: {e}", "ERROR")
467
  return {"erro": f"API_ERROR: {str(e)}"}
468
 
469
  def historico_compacto(historico: List[Dict]) -> str:
470
+ """
471
+ Gera uma string curta com as últimas interações do chat para usar como contexto nos prompts.
472
+
473
+ Args:
474
+ historico: O histórico de conversa no formato interno.
475
+
476
+ Returns:
477
+ Uma string resumida da conversa recente.
478
+ """
479
  if not historico:
480
  return "Nenhuma conversa anterior."
481
 
482
+ # Pega as últimas 4 mensagens, formata e junta em uma única string.
483
  compacto = []
484
+ for msg in historico[-4:]:
485
  role = "Usuário" if msg["role"] == "user" else "Assistente"
486
+ content = msg["content"].split('\n')[:80] # Pega só a primeira linha, até 80 caracteres.
487
  compacto.append(f"{role}: {content}")
488
 
489
  return "\n".join(compacto)
490
 
491
  def criar_dna() -> Dict:
492
+ """
493
+ Inicializa a estrutura de dados 'DNA' que armazena o estado e metadados da conversa.
494
+
495
+ Returns:
496
+ Um dicionário com a estrutura inicial do DNA.
497
+ """
498
  return {
499
  "historico_chat": [],
500
  "meta": {"total_turnos": 0}
 
503
  # ============================================================================
504
  # 4. PASSOS DA PIPELINE (P0-P8, X1-X2)
505
  # ============================================================================
506
+ # Cada função representa um passo de raciocínio da pipeline. Elas preparam
507
+ # um prompt, chamam a API e retornam o resultado processado.
508
 
509
  def passo_0_aluno(pergunta: str, historico: List[Dict]) -> Dict:
510
  """P0: Analisa a pergunta atual em relação à resposta anterior (metacognição)."""
 
520
  )
521
  return chamar_gemini_json(COUNSELOR_MODEL, prompt, temperatura=0.3)
522
 
523
+ def passo_1_triagem(pergunta: str, p0: Dict, hi