import gradio as gr import requests import os import json import PyPDF2 import docx import io import time import tempfile from dotenv import load_dotenv from pdfplumber import open as pdfplumber_open from PIL import Image import pytesseract # Requires Tesseract installation import cv2 import numpy as np from datetime import datetime from ratelimiter import RateLimiter import difflib from html import escape import random import logging import re from fpdf import FPDF # Added for PDF export logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') logger = logging.getLogger(__name__) load_dotenv() GEMINI_API_KEY = os.getenv("GEMINI_API_KEY") if not GEMINI_API_KEY: raise ValueError("❌ A chave GEMINI_API_KEY não foi encontrada. Configure-a no arquivo .env ou como Secret.") GEMINI_API_URL = f"https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash-latest:generateContent?key={GEMINI_API_KEY}" CONFIG = { "max_retry_attempts": 3, "retry_delay": 5, "timeout": 180, "temperatura": 0.3, "max_tokens": 8192 } CONFIG_OAB = { "temperatura": 0.2, "max_tokens": 8192 } rate_limiter = RateLimiter(max_calls=30, period=60) TIPOS_DE_PETICAO = [ "Petição Inicial", "Contestação", "Reconvenção", "Impugnação à Contestação", "Embargos de Declaração", "Recurso de Apelação", "Agravo de Instrumento", "Petição de Tutela Provisória", "Ação de Alimentos Gravídicos", ] DICAS_OAB_MELHORADO = { "Clareza e Concisão": "Seja direto e evite jargões excessivos. Facilite a leitura do juiz.", "Fundamentação Legal": "Sempre cite os artigos de lei, súmulas e jurisprudência pertinentes ao seu argumento.", "Coerência Argumentativa": "Mantenha uma linha lógica de raciocínio do início ao fim da peça.", "Revisão Ortográfica e Gramatical": "Erros de português podem prejudicar a credibilidade da sua peça.", "Formatação": "Utilize parágrafos bem definidos, espaçamento adequado e destaque pontos importantes (negrito, itálico) com moderação.", "Endereçamento (Inicial)": "Em relações de consumo, prefira o foro do domicílio do autor (Art. 101, I, CDC). Verifique a competência (material, territorial).", "Qualificação (Inicial)": "Inclua todos os dados do Art. 319, II, CPC. Use 'XXX' para dados não fornecidos.", "Fatos (Inicial)": "Narrativa cronológica, clara e objetiva, sem juízo de valor.", "Pedidos (Inicial)": "Certos, determinados e líquidos (sempre que possível). Cumule pedidos se necessário (Art. 327, CPC).", "Custas (Inicial)": "Sempre peça a juntada da guia ou a gratuidade de justiça (com declaração).", "Valor da Causa (Inicial)": "Deve corresponder ao benefício econômico pretendido (Art. 292, CPC).", "Preliminares (Contestação)": "Alegue todas as matérias do Art. 337 do CPC antes de discutir o mérito.", "Impugnação Específica (Contestação)": "Conteste cada fato alegado pelo autor, sob pena de presunção de veracidade (Art. 341, CPC).", "Mérito (Contestação)": "Apresente sua versão dos fatos e a fundamentação jurídica que ampara a improcedência dos pedidos do autor.", "Pedidos (Contestação)": "Requeira o acolhimento das preliminares e/ou a improcedência total dos pedidos do autor, além da condenação em custas e honorários.", "Pressupostos Recursais": "Verifique cabimento, tempestividade, preparo (custas recursais), regularidade formal e interesse/legitimidade.", "Dialeticidade (Recurso)": "Impugne especificamente os fundamentos da decisão recorrida (Art. 1010, II e III, CPC para Apelação).", "Delimitação da Matéria (Recurso)": "Indique claramente qual parte da decisão você está recorrendo.", "Fumus Boni Iuris": "Demonstre a plausibilidade do direito alegado com base em provas e argumentos sólidos.", "Periculum in Mora": "Evidencie o risco de dano irreparável ou de difícil reparação se a medida não for concedida imediatamente.", "Reversibilidade": "Se possível, argumente que a medida é reversível caso a decisão final seja diferente (Art. 300, §3º, CPC).", } HISTORICO = [] CORRECAO_CACHE = {} def preprocess_image_for_ocr(img): try: gray = img.convert('L') np_img = np.array(gray) _, thresh = cv2.threshold(np_img, 150, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) return Image.fromarray(thresh) except Exception as e: logger.error(f"Erro no pré-processamento da imagem: {str(e)}") return f"❌ Erro no pré-processamento: {str(e)}" def extract_text_from_pdf(file_path_or_obj): text = "" ocr_text = "" full_text = "" try: pdf_reader = pdfplumber_open(file_path_or_obj) for i, page in enumerate(pdf_reader.pages): page_full_text = "" page_text = page.extract_text() if page_text: page_full_text += page_text + "\n" text += page_text + "\n" else: logger.warning(f"Página {i+1} sem texto extraível, tentando OCR.") try: # Check if pytesseract is available before attempting OCR if 'pytesseract' in globals(): img = page.to_image(resolution=300) preprocessed_img = preprocess_image_for_ocr(img.original) page_ocr_text = pytesseract.image_to_string(preprocessed_img, lang='por') if page_ocr_text.strip(): page_full_text += page_ocr_text + "\n" ocr_text += page_ocr_text + "\n" else: logger.warning(f"OCR falhou na página {i+1}.") page_full_text += f"[OCR falhou na página {i+1} - Tesseract instalado?]\n" ocr_text += f"[OCR falhou na página {i+1}]\n" else: logger.warning(f"OCR não tentado na página {i+1} - biblioteca pytesseract não encontrada.") page_full_text += f"[OCR não disponível na página {i+1}]\n" ocr_text += f"[OCR não disponível na página {i+1}]\n" except Exception as ocr_e: logger.error(f"Erro no OCR da página {i+1}: {str(ocr_e)}") page_full_text += f"[Erro no OCR da página {i+1}: {str(ocr_e)}]\n" ocr_text += f"[Erro no OCR da página {i+1}: {str(ocr_e)}]\n" full_text += page_full_text pdf_reader.close() if full_text.strip(): logger.info("Texto combinado (extração/OCR) obtido do PDF.") return full_text else: logger.warning("Não foi possível extrair texto significativo do PDF (nem extração, nem OCR).") return "⚠️ Não foi possível extrair texto do PDF (tentativa de OCR falhou ou texto inexistente)." except Exception as e: logger.error(f"Erro geral ao processar o PDF: {str(e)}") try: logger.info("Tentando fallback com PyPDF2.") text_fallback = "" reader = None # Initialize reader if isinstance(file_path_or_obj, str): with open(file_path_or_obj, 'rb') as f: try: reader = PyPDF2.PdfReader(f) except PyPDF2.errors.PdfReadError as pdf_err: logger.error(f"Erro PyPDF2 ao ler {file_path_or_obj}: {pdf_err}") return f"❌ Erro ao ler PDF (PyPDF2): {pdf_err}" if reader: for page in reader.pages: text_fallback += page.extract_text() or "" elif hasattr(file_path_or_obj, 'seek') and hasattr(file_path_or_obj, 'read'): file_path_or_obj.seek(0) try: reader = PyPDF2.PdfReader(file_path_or_obj) except PyPDF2.errors.PdfReadError as pdf_err: logger.error(f"Erro PyPDF2 ao ler objeto stream: {pdf_err}") return f"❌ Erro ao ler PDF (PyPDF2 Stream): {pdf_err}" if reader: for page in reader.pages: text_fallback += page.extract_text() or "" else: logger.error("Tipo de entrada PDF inválido para PyPDF2.") return f"❌ Erro interno: Tipo de entrada PDF inválido para fallback." if text_fallback.strip(): logger.info("Texto extraído com fallback PyPDF2.") return text_fallback else: # Return the original error if fallback also fails to get text return f"❌ Erro ao processar o PDF: {str(e)} (Fallback PyPDF2 também não extraiu texto)." except Exception as fallback_e: logger.error(f"Erro no fallback com PyPDF2: {str(fallback_e)}") # Return the original error + fallback error return f"❌ Erro crítico ao processar o PDF: {str(e)}. Erro fallback: {str(fallback_e)}" def extract_text_from_docx(file_path_or_obj): try: doc = docx.Document(file_path_or_obj) text = "\n".join([para.text for para in doc.paragraphs]) return text if text.strip() else "⚠️ Documento DOCX vazio ou sem texto." except Exception as e: logger.error(f"Erro ao processar o DOCX: {str(e)}") return f"❌ Erro ao processar o DOCX: {str(e)}" def extract_text_from_txt(file_path_or_obj): try: content = None if isinstance(file_path_or_obj, str): with open(file_path_or_obj, 'rb') as f: content = f.read() elif hasattr(file_path_or_obj, 'read'): # Ensure we read from the beginning if it's a file-like object if hasattr(file_path_or_obj, 'seek'): try: file_path_or_obj.seek(0) except io.UnsupportedOperation: pass # Some streams might not support seek content = file_path_or_obj.read() # Try to rewind again for potential future reads if supported if hasattr(file_path_or_obj, 'seek'): try: file_path_or_obj.seek(0) except io.UnsupportedOperation: pass else: return "❌ Tipo de entrada TXT inválido." if content is None: return "❌ Erro ao ler conteúdo TXT." encodings = ['utf-8', 'latin-1', 'cp1252', 'iso-8859-1'] for encoding in encodings: try: decoded_text = content.decode(encoding) logger.info(f"Arquivo TXT decodificado com {encoding}.") return decoded_text if decoded_text.strip() else "⚠️ Arquivo TXT vazio." except UnicodeDecodeError: continue logger.error("Não foi possível decodificar o arquivo TXT com encodings comuns.") # Try decoding with 'replace' as a last resort try: decoded_text_replace = content.decode('utf-8', errors='replace') logger.warning("Decodificado TXT com UTF-8 e 'replace'. Pode haver caracteres substituídos.") return decoded_text_replace if decoded_text_replace.strip() else "⚠️ Arquivo TXT vazio." except Exception: return "❌ Não foi possível decodificar o arquivo TXT. Verifique o encoding." except Exception as e: logger.error(f"Erro ao processar o TXT: {str(e)}") return f"❌ Erro ao processar o TXT: {str(e)}" def process_file(file_obj): if file_obj is None: logger.warning("Nenhum arquivo fornecido para processamento.") return "⚠️ Nenhum arquivo enviado." try: # Gradio File object has 'name' attribute which is the temporary path file_path = getattr(file_obj, 'name', None) # Get original filename if available (useful for extension check) original_filename = getattr(file_obj, 'orig_name', file_path) if not file_path or not original_filename: logger.error("Não foi possível obter o nome/caminho do arquivo do objeto.") return "❌ Erro: Não foi possível identificar o tipo do arquivo." filename_lower = os.path.basename(original_filename).lower() logger.info(f"Processando arquivo: {original_filename} (path: {file_path})") try: file_size = os.path.getsize(file_path) logger.info(f"Tamanho do arquivo: {file_size} bytes") if file_size > 15 * 1024 * 1024: # 15 MB limit logger.warning(f"Arquivo {original_filename} excede o limite de tamanho.") return "⚠️ O arquivo é muito grande. Máx. 15 MB." if file_size == 0: logger.warning(f"Arquivo {original_filename} está vazio.") return "⚠️ Arquivo vazio." except OSError as size_e: logger.warning(f"Não foi possível verificar o tamanho do arquivo: {size_e}") # Don't stop processing, just log the warning if filename_lower.endswith('.pdf'): # Pass the file path (temp path from Gradio) return extract_text_from_pdf(file_path) elif filename_lower.endswith('.docx'): # Pass the file path return extract_text_from_docx(file_path) elif filename_lower.endswith('.txt'): # Pass the file path return extract_text_from_txt(file_path) else: logger.warning(f"Formato de arquivo não suportado: {original_filename}") return f"⚠️ Formato de arquivo não suportado ({os.path.splitext(original_filename)[1]}). Use PDF, DOCX ou TXT." # Catch specific expected errors first except FileNotFoundError: logger.error(f"Erro: Arquivo temporário não encontrado '{getattr(file_obj, 'name', 'N/A')}'", exc_info=True) return f"❌ Erro: Arquivo não encontrado. Tente reenviar." except PermissionError: logger.error(f"Erro de permissão ao acessar o arquivo '{getattr(file_obj, 'name', 'N/A')}'", exc_info=True) return f"❌ Erro: Permissão negada para ler o arquivo." # Catch errors from specific libraries if they raise unique exceptions except PyPDF2.errors.PdfReadError as pdf_err: logger.error(f"Erro PyPDF2 ao ler '{getattr(file_obj, 'name', 'N/A')}': {pdf_err}", exc_info=True) return f"❌ Erro ao ler PDF (PyPDF2): {pdf_err}" except docx.opc.exceptions.PackageNotFoundError: logger.error(f"Erro DOCX: Arquivo não parece ser um DOCX válido '{getattr(file_obj, 'name', 'N/A')}'", exc_info=True) return f"❌ Erro: O arquivo .docx parece estar corrompido ou não é um DOCX válido." # General catch-all except Exception as e: logger.error(f"Erro geral ao processar o arquivo '{getattr(file_obj, 'orig_name', 'N/A')}': {str(e)}", exc_info=True) # Provide a slightly more user-friendly general error return f"❌ Erro inesperado ao processar o arquivo. Verifique se o arquivo não está corrompido ou protegido. Detalhe: {str(e)}" def chamar_api(prompt, use_oab_config=False): config_to_use = CONFIG_OAB if use_oab_config else CONFIG payload = { "contents": [{"parts": [{"text": prompt}]}], "generationConfig": { "temperature": config_to_use["temperatura"], "maxOutputTokens": config_to_use["max_tokens"], "topP": 0.95, # "topK": 64, # Can add if needed # "stopSequences": ["some_sequence"] # Can add if needed } } headers = {"Content-Type": "application/json"} logger.info(f"Chamando API Gemini com {len(prompt)} chars. Temp: {config_to_use['temperatura']}, MaxTokens: {config_to_use['max_tokens']}") for tentativa in range(CONFIG["max_retry_attempts"]): try: # Ensure rate limiter waits if necessary with rate_limiter: start_req = time.time() response = requests.post( GEMINI_API_URL, headers=headers, data=json.dumps(payload), timeout=CONFIG["timeout"] # Use configured timeout ) end_req = time.time() logger.info(f"API request time: {end_req - start_req:.2f}s") # Handle Rate Limiting specifically if response.status_code == 429: # Use Retry-After header if available, otherwise calculate backoff retry_after = response.headers.get("Retry-After") wait_time = CONFIG["retry_delay"] * (tentativa + 1) # Default backoff if retry_after: try: wait_time = int(retry_after) except ValueError: logger.warning(f"Invalid Retry-After header: {retry_after}. Using default backoff.") # Ensure wait time is reasonable (e.g., max 60s) wait_time = min(wait_time, 60) logger.warning(f"Erro 429 (Rate Limit Exceeded). Tentativa {tentativa + 1}/{CONFIG['max_retry_attempts']}. Aguardando {wait_time}s...") if tentativa < CONFIG["max_retry_attempts"] - 1: time.sleep(wait_time) continue # Retry the loop else: logger.error("Erro 429: Limite de taxa excedido após todas as tentativas.") return "❌ Erro 429: Muitas requisições. Tente novamente em um minuto." # Check for other non-200 status codes if response.status_code != 200: error_msg = f"Erro desconhecido (Status {response.status_code})" try: # Attempt to get more details from the JSON response error_info = response.json() # Navigate the potential error structure safely error_details = error_info.get("error", {}).get("message", "") if error_details: error_msg = f"Erro API ({response.status_code}): {error_details}" else: # Log the raw response if details aren't clear error_msg = f"Erro API ({response.status_code}). Resposta: {response.text[:200]}..." except json.JSONDecodeError: # Handle cases where the error response isn't valid JSON error_msg = f"Erro API ({response.status_code}): Resposta não-JSON: {response.text[:200]}..." except Exception as json_e: # Catch other potential errors during error parsing error_msg = f"Erro API ({response.status_code}): Erro ao parsear resposta de erro ({json_e}): {response.text[:200]}..." logger.error(f"{error_msg} - Tentativa {tentativa + 1}/{CONFIG['max_retry_attempts']}.") if tentativa < CONFIG["max_retry_attempts"] - 1: # Apply backoff delay before retrying time.sleep(CONFIG["retry_delay"] * (tentativa + 1)) continue # Retry the loop else: # Return the last error message after all retries fail return f"❌ {error_msg} (Após {CONFIG['max_retry_attempts']} tentativas)" # Process successful (200) response try: response_data = response.json() except json.JSONDecodeError as e: logger.error(f"Erro JSON ao decodificar resposta 200 OK: {e}. Resposta: {response.text[:200]}...") # Consider retrying even on 200 if JSON is bad? Or fail immediately? Let's fail. return f"❌ Erro processar resposta API (JSON inválido na resposta 200 OK)." # --- Check for blocked content or empty response --- if not response_data.get("candidates"): # Check for prompt feedback which indicates potential blocking prompt_feedback = response_data.get("promptFeedback", {}) block_reason = prompt_feedback.get("blockReason") safety_ratings = prompt_feedback.get("safetyRatings", []) # Check prompt safety ratings too block_details = "" if block_reason: block_details += f"Motivo: {block_reason}." if safety_ratings: harm_categories = [f"{r.get('category', 'N/A')}:{r.get('probability', 'N/A')}" for r in safety_ratings if r.get('probability') not in ['NEGLIGIBLE', 'LOW']] if harm_categories: block_details += f" Classificações Prompt: {', '.join(harm_categories)}." if block_details: logger.error(f"Resposta API sem 'candidates'. {block_details}") return f"❌ Solicitação bloqueada pela API. {block_details}" else: logger.error("Resposta API válida (200 OK) mas sem 'candidates' e sem motivo de bloqueio claro.") # This might be a temporary issue or unexpected API behavior. Retry might help. if tentativa < CONFIG["max_retry_attempts"] - 1: logger.warning("Tentando novamente devido a resposta sem 'candidates'...") time.sleep(CONFIG["retry_delay"] * (tentativa + 1)) continue else: return "❌ Resposta vazia/inválida da API (sem 'candidates') após retentativas." # --- Process the first candidate --- candidate = response_data["candidates"][0] # Check finish reason finish_reason = candidate.get("finishReason") if finish_reason and finish_reason not in ["STOP", "MAX_TOKENS"]: # Log the specific reason logger.error(f"Geração interrompida pela API: {finish_reason}") # Check safety ratings on the candidate level safety_ratings = candidate.get("safetyRatings", []) harm_categories = [f"{r.get('category', 'N/A')}:{r.get('probability', 'N/A')}" for r in safety_ratings if r.get('probability') not in ['NEGLIGIBLE', 'LOW']] reason_details = f"Motivo: {finish_reason}." if harm_categories: reason_details += f" Classificações Conteúdo: {', '.join(harm_categories)}." # Return specific error based on finish reason if finish_reason == "SAFETY": return f"❌ Resposta bloqueada por segurança. {reason_details}" elif finish_reason == "RECITATION": return f"❌ Resposta bloqueada por recitação (similar a fontes protegidas). {reason_details}" else: # OTHER, UNSPECIFIED, etc. return f"❌ Geração interrompida inesperadamente. {reason_details}" # Check content structure if "content" not in candidate or "parts" not in candidate["content"] or not candidate["content"]["parts"]: logger.error("Estrutura de resposta inesperada no candidato.") if tentativa < CONFIG["max_retry_attempts"] - 1: logger.warning("Tentando novamente devido a estrutura de resposta inesperada...") time.sleep(CONFIG["retry_delay"] * (tentativa + 1)) continue else: return "❌ Resposta inválida da API (estrutura inesperada) após retentativas." # Extract text safely try: texto_gerado = candidate["content"]["parts"][0]["text"].strip() logger.info(f"API retornou com sucesso. Tamanho: {len(texto_gerado)} chars. Finish Reason: {finish_reason or 'STOP'}") if finish_reason == "MAX_TOKENS": logger.warning("Resposta pode estar incompleta devido a MAX_TOKENS.") # Optionally append a warning to the output? # texto_gerado += "\n\n[⚠️ Atenção: A resposta pode ter sido truncada devido ao limite de tokens.]" return texto_gerado # Success! Exit the loop and function. except (KeyError, IndexError, AttributeError) as e: logger.error(f"Erro ao extrair texto da estrutura da resposta: {e}") if tentativa < CONFIG["max_retry_attempts"] - 1: logger.warning("Tentando novamente devido a erro na extração do texto...") time.sleep(CONFIG["retry_delay"] * (tentativa + 1)) continue else: return "❌ Erro ao processar o conteúdo da resposta da API após retentativas." # Handle specific request exceptions except requests.Timeout: logger.warning(f"Timeout na tentativa {tentativa + 1}/{CONFIG['max_retry_attempts']}.") if tentativa < CONFIG["max_retry_attempts"] - 1: time.sleep(CONFIG["retry_delay"] * (tentativa + 1)) # Wait before retrying on timeout continue else: return f"⏱️ Tempo esgotado ao conectar com a API após {CONFIG['max_retry_attempts']} tentativas." except requests.ConnectionError as e: logger.error(f"Erro de conexão na tentativa {tentativa + 1}/{CONFIG['max_retry_attempts']}: {e}") if tentativa < CONFIG["max_retry_attempts"] - 1: time.sleep(CONFIG["retry_delay"] * (tentativa + 1)) continue else: return f"❌ Erro de conexão com a API: {e}" except requests.RequestException as e: # Catch other potential request errors logger.error(f"Erro de requisição na tentativa {tentativa + 1}/{CONFIG['max_retry_attempts']}: {e}") if tentativa < CONFIG["max_retry_attempts"] - 1: time.sleep(CONFIG["retry_delay"] * (tentativa + 1)) continue else: return f"❌ Erro na requisição para a API: {e}" # Catch unexpected errors during the loop except Exception as e: logger.exception(f"Erro inesperado durante chamada API (tentativa {tentativa + 1}): {str(e)}") # Use logger.exception to include stack trace if tentativa < CONFIG["max_retry_attempts"] - 1: time.sleep(CONFIG["retry_delay"] * (tentativa + 1)) continue else: # Provide a generic unexpected error message return f"❌ Erro inesperado durante a comunicação com a API: {str(e)}" # This line should only be reached if all retries failed for some reason (e.g., loop logic error) # or if the loop finished without returning (which shouldn't happen with the current logic). logger.error("Falha na chamada API após todas as tentativas. Nenhuma resposta ou erro específico retornado.") return "❌ Falha na comunicação com a API após todas as tentativas." def get_instrucoes_gerais_avaliacao(modo_treinamento=False): base = """ **Estrutura da Avaliação (Geral - IA):** 1. **Análise Geral:** Visão geral concisa (2-3 linhas) sobre a adequação da peça ao caso e tipo, consistência e clareza. 2. **Nota Final:** 0 a 10 (uma casa decimal), com justificativa BREVE dos principais fatores. 3. **Pontos Positivos:** Liste pelo menos 2-3 aspectos bem executados, com justificativa (legal/técnica). 4. **Pontos a Melhorar:** Liste **TODOS** os problemas em ordem de gravidade (erros crassos primeiro). Para cada ponto: * Descrição clara do erro/omissão. * Fundamentação legal/doutrinária do porquê é um erro. * **Impacto estimado na nota** (ex: -0.5, -1.0, -2.0). * Sugestão de correção ou como deveria ter sido feito. 5. **Análise Específica dos Requisitos:** Avalie os itens chave da peça (listados abaixo para cada tipo). **Critérios Fundamentais (Aplicáveis a Quase Todos):** * **Coerência:** A peça responde ao caso/situação processual? Os argumentos são lógicos? * **Fundamentação:** Usou a lei, doutrina e jurisprudência corretamente? Citou artigos relevantes? * **Linguagem:** Clareza, objetividade, correção gramatical e formalidade adequada? * **Pedidos:** São claros, certos, determinados e coerentes com a fundamentação? * **NÃO INVENTAR FATOS:** Penalidade severa (-3.0 a -5.0) se criar fatos não presentes no caso. * **NÃO SE IDENTIFICAR:** Nota ZERO se houver nome, OAB, CPF, etc., do aluno na peça (exceto nomes das partes do caso). Use "ADVOGADO(A)", "OAB/...". """ if modo_treinamento: base += """ **Modo de Treinamento (Detalhes Adicionais):** * Para cada Ponto a Melhorar, detalhe AINDA MAIS: a) Explicação aprofundada do erro e sua consequência prática (ex: risco de inépcia, preclusão, sucumbência). b) Base legal/doutrinária específica (cite artigos, súmulas, enunciados). c) **Exemplo Concreto de Redação Corrigida** para o trecho específico apontado. d) Dica prática relevante (pode usar o banco de dicas se aplicável ou criar uma nova). e) Possível estratégia alternativa ou ponto de atenção relacionado. * Destaque inconsistências ou falta de conexão entre a peça e os fatos do caso, citando trechos se necessário. * Sugira estratégias jurídicas adicionais ou alternativas viáveis com base no caso. """ return base def estruturar_prompt_analise_geral(caso, peticao, tipo_peticao, modo_treinamento=False): instrucoes_gerais = get_instrucoes_gerais_avaliacao(modo_treinamento) prompt_especifico = f""" ## Análise de Peça Processual: {tipo_peticao} (Avaliação Geral IA - Critérios OAB Simulados) **Caso/Contexto Fornecido:** ``` {caso} ``` **Peça do Aluno ({tipo_peticao}):** ``` {peticao} ``` {instrucoes_gerais} **Requisitos Específicos para {tipo_peticao} (Checklist Mínimo Considerado pela IA):** """ # --- Requisitos Específicos (Checklist) --- # Using a more structured way to define these might be better later requisitos = [] if tipo_peticao == "Petição Inicial": requisitos = [ "- **Endereçamento e Competência:** Verificação do juízo e foro corretos (Art. 319, I, CPC; leis especiais como CDC Art. 101, I).", "- **Qualificação Completa das Partes:** Nome, prenome, estado civil, profissão, CPF/CNPJ, e-mail, domicílio (Art. 319, II, CPC).", "- **Exposição Clara e Lógica dos Fatos:** Causa de pedir remota, narrativa coerente.", "- **Fundamentação Jurídica Pertinente:** Causa de pedir próxima, artigos de lei, doutrina/jurisprudência aplicáveis (Art. 319, III, CPC).", "- **Pedidos Certos e Determinados:** Claros, específicos, e se possível, líquidos (Art. 319, IV, CPC). Cumulação correta (Art. 327).", "- **Valor da Causa:** Correto e justificado (Art. 291-292, 319, V, CPC).", "- **Indicação das Provas:** Especificação dos meios de prova pretendidos (Art. 319, VI, CPC).", "- **Opção pela Audiência:** Indicação sobre interesse na audiência de conciliação/mediação (Art. 319, VII, CPC).", "- **Pedido de Citação do Réu:** Requerimento expresso e forma (se necessário).", "- **Requerimento de Juntada de Documentos:** Procuração, custas/JG, documentos essenciais.", "- **Fechamento Formal:** Padrão OAB ('Nestes termos...', Local, data, Advogado, OAB)." ] elif tipo_peticao == "Ação de Alimentos Gravídicos": requisitos = [ "- **Endereçamento e Competência:** Vara de Família, foro adequado (domicílio alimentando - Súmula 1 STJ / Lei 5.478/68 Art 2º).", "- **Qualificação Completa das Partes:** Mãe (representando nascituro), suposto pai (Art. 319, II, CPC).", "- **Exposição dos Fatos:** Relacionamento, gravidez, **indícios suficientes de paternidade** (essencial - Lei 11.804/08, Art. 6º), necessidades da gestante/nascituro.", "- **Fundamentação Jurídica:** Lei nº 11.804/08 (especialmente Arts. 1º, 2º, 6º), binômio necessidade-possibilidade (adaptado), proteção ao nascituro (CC).", "- **Tutela Provisória de Urgência (Alimentos Provisórios):** Pedido expresso e fundamentação (fumus boni iuris = indícios paternidade; periculum in mora = necessidades urgentes).", "- **Pedidos:** Citação réu; **Fixação alimentos gravídicos provisórios** (valor ou %); Procedência para fixar definitivos; **Conversão em pensão alimentícia** após nascimento (Art. 6º, p. único); Condenação custas/honorários.", "- **Valor da Causa:** (ex: 12x valor pretendido - Art. 292, III, CPC).", "- **Indicação das Provas:** Documental (exames, fotos), testemunhal, pericial (DNA futuro).", "- **Opção pela Audiência:** (Art. 319, VII, CPC).", "- **Pedido de Gratuidade de Justiça:** Se aplicável, com declaração.", "- **Intervenção MP:** Obrigatória (Art. 178, II, CPC).", "- **Fechamento Formal:** Padrão OAB." ] elif tipo_peticao == "Contestação": requisitos = [ "- **Endereçamento:** Ao juízo correto da causa.", "- **Identificação:** Número do processo, qualificação completa do réu.", "- **Tempestividade:** Alegação (se prazo relevante ou prova fornecida).", "- **Síntese da Inicial:** Breve resumo (opcional, mas comum).", "- **Preliminares (Art. 337 CPC):** Alegar TODAS as aplicáveis antes do mérito (inexistência/nulidade citação, incompetência, inépcia, perempção, litispendência, coisa julgada, conexão, incapacidade, etc.).", "- **Mérito - Impugnação Específica:** Rebater PONTO A PONTO os fatos alegados pelo autor (Art. 341 CPC).", "- **Mérito - Defesa Material:** Apresentar a versão dos fatos do réu, defesa direta (nega fato/consequência) e/ou indireta (fato impeditivo, modificativo, extintivo).", "- **Fundamentação Jurídica (Mérito):** Leis, doutrina, jurisprudência que sustentam a defesa.", "- **Reconvenção (se aplicável):** Pedido do réu contra o autor no mesmo processo (Art. 343 CPC).", "- **Pedidos:** **Acolhimento das preliminares** (com extinção ou remessa); NO MÉRITO, **IMPROCEDÊNCIA TOTAL** dos pedidos do autor; Condenação do autor em custas e honorários advocatícios (Art. 85 CPC).", "- **Indicação de Provas:** Especificar as provas que o réu pretende produzir.", "- **Fechamento Formal:** Padrão OAB." ] elif tipo_peticao == "Recurso de Apelação": requisitos = [ "- **Endereçamento:** Interposição ao juízo 'a quo' (quem proferiu a sentença); Razões ao Tribunal 'ad quem'.", "- **Identificação:** Número do processo, nome/qualificação das partes (Apelante/Apelado).", "- **Pressupostos de Admissibilidade:** Cabimento (sentença - Art. 1009); Tempestividade (15 dias úteis - Art. 1003, §5º); Preparo (comprovação custas ou pedido JG); Regularidade formal.", "- **Exposição Fato/Direito (Razões):** Breve resumo do caso e da sentença recorrida.", "- **Fundamentação da Reforma/Anulação (Razões):** **Dialeticidade** - Impugnar ESPECIFICAMENTE os fundamentos da sentença (Art. 1010, II e III). Apontar erros 'in judicando' (má apreciação fatos/direito) ou 'in procedendo' (nulidades processuais).", "- **Pedidos (Razões):** Conhecimento do recurso; Provimento para **reformar** ou **anular** a sentença; Inversão ônus sucumbência.", "- **Requerimentos (Interposição):** Recebimento, processamento, intimação Apelado para contrarrazões, remessa ao Tribunal.", "- **Fechamento Formal:** Padrão OAB (peças separadas)." ] # TODO: Add specific requirements for other petition types (Reconvenção, Impugnação, Embargos, Agravo, Tutela Provisória) else: requisitos.append("- Requisitos específicos para este tipo de peça não detalhados no prompt. A IA avaliará com base em conhecimento jurídico geral e nos critérios fundamentais.") prompt_especifico += "\n".join(requisitos) prompt_especifico += "\n\n**Instruções Finais:** Analise a peça do aluno CRITICAMENTE com base no caso, nos critérios gerais e nos requisitos específicos listados. Use seu conhecimento jurídico para simular um examinador da OAB rigoroso, mas justo. Forneça feedback construtivo, claro e detalhado. Gere a resposta **exclusivamente** no formato solicitado (Análise Geral, Nota, Pontos Positivos, Pontos a Melhorar detalhados, Análise Específica). Foque na aplicação correta do direito aos fatos do caso." return prompt_especifico # --- Prompt OAB Generation (User Provided - Integrated) --- def estruturar_prompt_geracao_modelo_oab(enunciado, gabarito, distribuicao): prompt = f""" **Tarefa:** Elabore a peça processual completa e **fundamentada** para o **Enunciado** fornecido. Utilize o **Gabarito Comentado** como guia OBRIGATÓRIO para os argumentos jurídicos essenciais e a **Distribuição dos Pontos** para garantir que TODOS os itens estruturais e pedidos necessários sejam incluídos e abordados corretamente. O objetivo é criar uma peça modelo que desenvolva os pontos esperados pela OAB, usando **ESTRITAMENTE** os fatos e dados fornecidos no Enunciado, Gabarito e Distribuição. **Enunciado:** ``` {enunciado} ``` **Gabarito Comentado (Diretrizes de Conteúdo e Fundamentação a serem DESENVOLVIDAS):** ``` {gabarito} ``` **Distribuição dos Pontos (Estrutura e Itens Essenciais a serem INCLUÍDOS):** ``` {distribuicao} ``` **Instruções DETALHADAS E RESTRITIVAS para Geração:** 1. **PROIBIDO INVENTAR DADOS:** NÃO invente NENHUMA informação que não esteja EXPLICITAMENTE presente no Enunciado, Gabarito ou Distribuição. Isso inclui, mas não se limita a: * Endereços exatos (rua, número, bairro, CEP) - Use "XXX" se não fornecido. * Números de documentos (RG, CPF) - Use "XXX" se não fornecido. * Endereços de e-mail - Use "XXX" ou omita se não fornecido. * Números de telefone - Use "XXX" ou omita se não fornecido. * **Valores monetários específicos** para pedidos (alimentos, danos, etc.) ou valor da causa, A MENOS QUE o valor esteja CLARAMENTE especificado nos textos fornecidos. Se não houver valor, peça "a ser arbitrado por Vossa Excelência" ou use "valor simbólico" ou calcule conforme regras legais (ex: 12x alimentos a serem arbitrados para valor da causa). NÃO invente um número como R$ 3.000,00. * Nomes de médicos, números de CRM, ou outros detalhes factuais não mencionados. 2. **USE "XXX" OBRIGATORIAMENTE:** Para TODA E QUALQUER informação específica necessária pela estrutura da peça (ex: qualificação, endereço, documentos) que NÃO foi fornecida no Enunciado, use **"XXX"**. 3. **Qualificação:** Inclua os dados MENCIONADOS no Enunciado (nome, nacionalidade, estado civil, profissão). Para os DEMAIS dados exigidos pelo Art. 319, II, CPC que não foram fornecidos, use "XXX". 4. **Fatos:** Narre APENAS os fatos apresentados no **Enunciado**, de forma cronológica e objetiva. NÃO adicione detalhes ou circunstâncias não descritas. 5. **Fundamentação Jurídica:** Desenvolva os argumentos legais mencionados no **Gabarito Comentado**, citando os artigos de lei mencionados. Conecte a lei aos fatos **DO ENUNCIADO**. 6. **Tutela de Urgência/Antecipada:** Se o gabarito/distribuição mencionar, argumente sobre os requisitos (fumus/periculum) usando **FATOS DO ENUNCIADO** para demonstrá-los. NÃO invente fatos para justificar a urgência. 7. **Pedidos:** Formule **TODOS** os pedidos listados na **Distribuição dos Pontos** e/ou **Gabarito**. Se um valor monetário for pedido, e ele NÃO foi dado no enunciado, peça "valor a ser arbitrado" ou use uma fórmula genérica baseada na lei (ex: percentual sobre rendimentos, se aplicável e mencionado). 8. **Valor da Causa:** Atribua um valor à causa seguindo as regras do CPC (Art. 292). Se depender de valores não fornecidos no enunciado (como alimentos a serem arbitrados), indique a fórmula (ex: "12 vezes o valor mensal dos alimentos a serem arbitrados") ou um valor simbólico se a lei permitir/exigir. NÃO invente um valor numérico alto arbitrariamente. 9. **Estrutura:** Siga a ordem lógica (Endereçamento, Qualificação, Fatos, Direito, Tutela, Pedidos, Valor da Causa, Fechamento). 10. **Linguagem:** Formal e técnica (padrão OAB). 11. **Finalização:** Use "Local, data.", "ADVOGADO(A)", "OAB/...". NÃO se identifique. 12. **Exclusividade:** Baseie-se **SOMENTE** nas informações dos textos fornecidos. A violação desta regra (inventar dados) compromete a tarefa. 13. **Output:** Gere **APENAS** o texto completo da peça processual. Sem comentários ou títulos fora da peça. """ return prompt def estruturar_prompt_peticao_corrigida_geral(caso, peticao_original, tipo_peticao): prompt = f""" **Caso/Contexto Fornecido:** ``` {caso} ``` **Peça Original do Aluno ({tipo_peticao}):** ``` {peticao_original} ``` **Tarefa:** Com base no caso e nos problemas identificados na peça original (se houver), **gere uma versão corrigida e aprimorada** da peça ({tipo_peticao}). Aja como um advogado experiente preparando esta peça para protocolo, aplicando as melhores práticas e todos os requisitos formais e materiais exigidos para este tipo de peça no contexto do caso apresentado, simulando o padrão esperado em um exame da OAB. **Corrija erros, omissões e melhore a clareza, fundamentação e estrutura.** **Instruções Específicas para a Peça Corrigida ({tipo_peticao}):** """ # --- Instruções Específicas por Tipo (Revisadas) --- instrucoes_tipo = [] if tipo_peticao == "Petição Inicial": instrucoes_tipo = [ "1. **Endereçamento:** Defina o juízo e foro competentes com base nos fatos do caso (Vara Cível/Família/Fazenda, Comarca).", "2. **Qualificação:** Completa (Art. 319, II), usando 'XXX' para dados ausentes no caso.", "3. **Nome da Ação:** Adequado à pretensão.", "4. **Fatos:** Reestruture a narrativa para ser cronológica, clara, objetiva e completa, usando apenas os fatos do caso.", "5. **Fundamentação Jurídica:** Apresente os fundamentos legais (artigos de lei), doutrinários e jurisprudenciais pertinentes e **aplicados ao caso concreto**.", "6. **Tutela Provisória (se cabível):** Se os fatos indicarem urgência ou evidência, inclua um tópico específico, fundamentando os requisitos (fumus/periculum ou Art. 311).", "7. **Pedidos:** Liste de forma clara, certa e determinada. Inclua pedidos principais, acessórios (juros, correção), e processuais (citação, procedência, sucumbência).", "8. **Requerimentos Finais:** Citação do réu, produção de provas (especificar minimamente), opção pela audiência (Art. 319, VII), pedido de JG ou juntada de custas.", "9. **Valor da Causa:** Atribua e justifique conforme Art. 292.", "10. **Fechamento:** Padrão OAB (Local, data, Advogado(a), OAB/...).", ] elif tipo_peticao == "Ação de Alimentos Gravídicos": instrucoes_tipo = [ "1. **Endereçamento:** Vara de Família competente.", "2. **Qualificação:** Completa (mãe representando nascituro, réu).", "3. **Nome da Ação:** 'Ação de Alimentos Gravídicos com Pedido de Tutela Provisória de Urgência'.", "4. **Fatos:** Detalhar relacionamento, gravidez, **indícios de paternidade (essencial!)**, necessidades da gestante/nascituro, recusa do réu.", "5. **Fundamentação:** Lei 11.804/08 (Arts. 1º, 2º, 6º), CC (proteção nascituro), CPC (tutela provisória).", "6. **Tutela Provisória (Alimentos Provisórios):** Fundamentar requisitos (indícios paternidade = fumus; necessidades urgentes = periculum).", "7. **Pedidos:** Citação; Deferimento tutela para fixar alimentos provisórios; Procedência para fixar definitivos; Conversão em pensão (Art. 6º p.ú.); Condenação sucumbência.", "8. **Requerimentos:** Provas (documental, testemunhal, DNA futuro), Intimação MP, JG ou custas.", "9. **Valor da Causa:** (12x valor pretendido/arbitrado).", "10. **Fechamento:** Padrão OAB.", ] elif tipo_peticao == "Contestação": instrucoes_tipo = [ "1. **Endereçamento:** Ao juízo da causa.", "2. **Identificação:** Processo, qualificação completa do réu.", "3. **Tempestividade:** Mencionar.", "4. **Estrutura:** Síntese da inicial (opcional); Preliminares (Art. 337, TODAS aplicáveis); Mérito (Impugnação Específica PONTO A PONTO + Defesa Material direta/indireta bem fundamentada); Reconvenção (se cabível, em peça separada ou na própria contestação).", "5. **Pedidos:** Acolhimento PRELIMINARES (com consequências); NO MÉRITO, **IMPROCEDÊNCIA TOTAL** dos pedidos autorais; Procedência da RECONVENÇÃO (se houver); Condenação do autor em custas e honorários.", "6. **Requerimentos:** Produção de provas pelo réu.", "7. **Fechamento:** Padrão OAB.", ] elif tipo_peticao == "Recurso de Apelação": instrucoes_tipo = [ "1. **Peça de Interposição:** Endereçada ao juízo 'a quo'. Identificação, pedido de recebimento, preparo/JG, intimação contrarrazões, remessa ao Tribunal.", "2. **Peça de Razões:** Endereçada ao Tribunal 'ad quem'. Identificação (Apelante/Apelado). Breve resumo (fatos, sentença). **Fundamentos da Reforma/Anulação (DIALETICIDADE ESSENCIAL - atacar os fundamentos da sentença)**. Pedido de conhecimento e PROVIMENTO para reformar/anular. Inversão sucumbência.", "3. **Fechamento:** Padrão OAB (duas peças).", ] # TODO: Add specific instructions for other petition types else: instrucoes_tipo.append("1. Siga a estrutura padrão para este tipo de peça.") instrucoes_tipo.append("2. Fundamente juridicamente a pretensão ou defesa.") instrucoes_tipo.append("3. Formule pedidos claros e coerentes.") instrucoes_tipo.append("4. Use linguagem técnica adequada.") instrucoes_tipo.append("5. Feche no padrão OAB.") prompt += "\n".join(instrucoes_tipo) prompt += "\n\n**Instrução Final:** Gere **APENAS** o texto completo da peça corrigida abaixo, começando pelo endereçamento e terminando com 'OAB/...'. Não inclua NENHUM comentário, introdução, explicação, ou título como 'Peça Corrigida' ANTES ou DEPOIS do texto da peça." return prompt def pos_corrigir_peticao(peticao_corrigida, tipo_peticao): if not peticao_corrigida or peticao_corrigida.strip() == "": logger.warning(f"Pós-correção recebida petição vazia para tipo {tipo_peticao}.") return "⚠️ (Pós-processamento falhou: Peça corrigida estava vazia)" # Normalizar espaçamento e linhas em branco linhas = [l.strip() for l in peticao_corrigida.strip().split('\n')] linhas_sem_vazias_extras = [] last_line_was_blank = False for linha in linhas: if linha: linhas_sem_vazias_extras.append(linha) last_line_was_blank = False elif not last_line_was_blank: # Allow one blank line for paragraph separation linhas_sem_vazias_extras.append("") last_line_was_blank = True # Remove trailing blank lines potentially added while linhas_sem_vazias_extras and not linhas_sem_vazias_extras[-1]: linhas_sem_vazias_extras.pop() peticao_corrigida = "\n".join(linhas_sem_vazias_extras) # Simplificar "Nestes termos, pede deferimento" peticao_corrigida = re.sub(r"[Nn]estes termos,?\s*pede deferimento\.?", "Nestes termos, pede deferimento.", peticao_corrigida) # --- Garantir Requerimentos Finais Essenciais (se ausentes) --- # Define where to insert these - typically before "Nestes termos" or Valor da Causa/Fechamento # Find a suitable insertion point - try before "Nestes termos", then before "Valor da Causa", else append before closing. insertion_point = -1 ntpd_match = re.search(r"Nestes termos, pede deferimento\.", peticao_corrigida, re.IGNORECASE) if ntpd_match: insertion_point = ntpd_match.start() else: vc_match = re.search(r"D[aá]\s*à\s*causa\s*o\s*valor\s*de", peticao_corrigida, re.IGNORECASE) if vc_match: insertion_point = vc_match.start() prefix = peticao_corrigida suffix = "" if insertion_point != -1: prefix = peticao_corrigida[:insertion_point].strip() suffix = "\n" + peticao_corrigida[insertion_point:].strip() else: # If no good insertion point found, find the closing lines and insert before them fechamento_pattern = r"(Local,\s*data\.|ADVOGADO\(A\)|OAB/\.\.\.)" closing_match = list(re.finditer(fechamento_pattern, peticao_corrigida, re.IGNORECASE)) if closing_match: insertion_point = closing_match[0].start() prefix = peticao_corrigida[:insertion_point].strip() suffix = "\n" + peticao_corrigida[insertion_point:].strip() # else: append before the very end (less ideal) # Check and Add JG/Custas for Initials/Alimentos Grav. if tipo_peticao in ["Petição Inicial", "Ação de Alimentos Gravídicos"]: custas_jg_present = re.search(r"(juntada\s*d[ao]s?\s*comprovantes?\s*d[ae]\s*custas|recolhimento\s*d[ae]s?\s*custas|gratuidade\s*d[ae]?\s*justiça|benefícios?\s*d[ae]?\s*justiça\s*gratuita|JG)", prefix + suffix, re.IGNORECASE) if not custas_jg_present: logger.warning(f"Adicionando requerimento de custas/JG ausente para {tipo_peticao}") # Add a standard request - prefer JG if case suggests, else custas. Default to custas. # A better approach might need context, but this is a fallback. prefix += "\nRequer, por fim, a juntada do comprovante de recolhimento das custas processuais." # OR: prefix += "\nRequer, por fim, a concessão dos benefícios da gratuidade de justiça, por ser pobre na forma da lei, conforme declaração anexa." # Check and Add Audiência Opt-in for Initials/Alimentos Grav. if tipo_peticao in ["Petição Inicial", "Ação de Alimentos Gravídicos"]: audiencia_present = re.search(r"(audiência\s*d[eao]?\s*(conciliação|mediação)|Art\.\s*319,\s*VII|Art\.\s*334)", prefix + suffix, re.IGNORECASE) if not audiencia_present: logger.warning(f"Adicionando opção de audiência ausente para {tipo_peticao}") prefix += "\nNos termos do Art. 319, VII, do CPC, informa o Autor ter interesse na designação de audiência de conciliação ou mediação." # OR: prefix += "\nNos termos do Art. 319, VII, do CPC, informa o Autor NÃO ter interesse na designação de audiência de conciliação ou mediação." (Choose one based on common practice/OAB) # --- Garantir Fechamento Padrão OAB --- # Recombine prefix and suffix peticao_corrigida = prefix.strip() + suffix.strip() # Remove any existing partial/incorrect closing lines before adding standard one linhas_finais = peticao_corrigida.split('\n') fechamento_padrao = ["Nestes termos, pede deferimento.", "Local, data.", "ADVOGADO(A)", "OAB/..."] termos_busca_fechamento = ["advogado", "oab", "local", "data", "pede deferimento", "termos,", "termos em que"] cleaned_lines = [] # Iterate backwards to find where content likely ends and closing begins end_content_index = len(linhas_finais) for i in range(len(linhas_finais) - 1, -1, -1): linha_lower = linhas_finais[i].lower().strip() is_closing_line = any(term in linha_lower for term in termos_busca_fechamento) and len(linha_lower) < 50 # Heuristic if not is_closing_line and linha_lower != "": # Found likely last line of real content end_content_index = i + 1 break elif linha_lower == "": # Ignore blank lines at the end when searching continue else: # It's likely part of the closing, continue checking previous lines pass # Check previous lines # Keep only lines before the detected closing section linhas_conteudo = linhas_finais[:end_content_index] # Join content and add standard closing peticao_final = "\n".join(linhas_conteudo).strip() + "\n\n" + "\n".join(fechamento_padrao) return peticao_final.strip() def formatar_correcao(texto): # Replace markdown headers with emoji headers texto = re.sub(r"^\#\#\#\s*Análise Geral", "## 📝 Análise Geral", texto, flags=re.MULTILINE) texto = re.sub(r"^\#\#\#\s*Nota Final", "## ⭐ Nota Final", texto, flags=re.MULTILINE) texto = re.sub(r"^\#\#\#\s*Pontos Positivos", "## ✅ Pontos Positivos", texto, flags=re.MULTILINE) texto = re.sub(r"^\#\#\#\s*Pontos a Melhorar", "## ⚠️ Pontos a Melhorar", texto, flags=re.MULTILINE) texto = re.sub(r"^\#\#\#\s*Análise Específica dos Requisitos", "## 🔎 Análise Específica dos Requisitos", texto, flags=re.MULTILINE) # Format lists and sublists linhas = texto.split('\n') linhas_formatadas = [] in_list = False for linha in linhas: linha_strip = linha.strip() # Match main numbered points (e.g., 1., 2. ...) match_num_list = re.match(r"^\s*(\d+)\.\s*(.*)", linha) # Match main bullet points (* or -) match_bullet_list = re.match(r"^\s*([\*\-])\s+(.*)", linha) # Match sub-bullet points (a), b), c)...) indented match_sub_list = re.match(r"^\s+([a-z])\)\s+(.*)", linha) # Match sub-sub-bullet points (i., ii., iii)...) indented further match_sub_sub_list = re.match(r"^\s{4,}([ivx]+)\.\s+(.*)", linha) # Example for roman numerals if match_num_list: num, content = match_num_list.groups() linhas_formatadas.append(f"\n**{num}. {content.strip()}**") in_list = False # Reset list context elif match_bullet_list: bullet, content = match_bullet_list.groups() # Add a newline before top-level bullet points if not already following a blank line or header prefix = "\n* " if not linhas_formatadas or not linhas_formatadas[-1].strip().endswith(("*",":")) else "* " linhas_formatadas.append(prefix + content.strip()) in_list = True # Set list context elif match_sub_list: letter, content = match_sub_list.groups() linhas_formatadas.append(f" * {letter}) {content.strip()}") in_list = True # Continue list context elif match_sub_sub_list: roman, content = match_sub_sub_list.groups() linhas_formatadas.append(f" - {roman}. {content.strip()}") in_list = True else: # Preserve indentation for code blocks or other formatting if linha.startswith(" ") or linha.startswith("```"): linhas_formatadas.append(linha) else: # Add non-list lines, ensuring paragraph breaks if linha_strip and (not linhas_formatadas or linhas_formatadas[-1].strip() == "" or linhas_formatadas[-1].startswith("##")): linhas_formatadas.append(linha_strip) # Start new paragraph elif linha_strip: linhas_formatadas.append(" " + linha_strip) # Continue paragraph (usually not needed with splitlines) elif linhas_formatadas and linhas_formatadas[-1].strip() != "": # Add a blank line if the previous wasn't blank linhas_formatadas.append("") # Reset list context if it's not a list item continuation if not match_bullet_list and not match_sub_list and not match_sub_sub_list: in_list = False texto_final = "\n".join(linhas_formatadas) # Standardize Nota format (X.Y/10) # Match variations like "Nota Final:", "Nota:", X.Y, X,Y, X /10 etc. nota_pattern = r"(\*\*?\s*(?:Nota|Nota Final)\s*:\s*\*\*?\s*)(\d{1,2}(?:[,.]\d)?)(?:\s*/\s*10)?(\s*\*\*?)" match = re.search(nota_pattern, texto_final, re.IGNORECASE | re.DOTALL) if match: prefix, nota_str, suffix = match.groups() try: nota_float = float(nota_str.replace(",", ".")) nota_formatada = f"{nota_float:.1f}/10" # Ensure one decimal place texto_final = re.sub(nota_pattern, f"{prefix.strip()} **{nota_formatada}**{suffix.strip()}", texto_final, count=1, flags=re.IGNORECASE | re.DOTALL) except ValueError: logger.warning(f"Não foi possível formatar a nota encontrada: {nota_str}") # Leave original match if conversion fails # Ensure headers have a blank line before them if not at the start texto_final = re.sub(r"([^\n])\n(##\s)", r"\1\n\n\2", texto_final) return texto_final.strip() def corrigir_peticao_geral_ia(caso, tipo_peticao, peticao_texto=None, peticao_arquivo=None, modo_treinamento=False): logger.info(f"Iniciando correção GERAL (IA) para tipo: {tipo_peticao}, Modo Treinamento: {modo_treinamento}") start_time = time.time() processing_log = [] # Store messages for user # 1. Input Validation if not caso or not caso.strip(): return "⚠️ **Erro:** Forneça o texto do caso/enunciado.", "", "", "", "", "", "" if not tipo_peticao or tipo_peticao not in TIPOS_DE_PETICAO: return f"⚠️ **Erro:** Selecione um Tipo de Peça válido. Tipos disponíveis: {', '.join(TIPOS_DE_PETICAO)}", "", "", "", "", "", "" peticao_conteudo = "" input_source = "" if peticao_arquivo is not None: input_source = f"arquivo '{getattr(peticao_arquivo, 'orig_name', 'N/A')}'" logger.info(f"Processando petição do {input_source}") peticao_conteudo = process_file(peticao_arquivo) if peticao_conteudo.startswith("❌") or peticao_conteudo.startswith("⚠️"): logger.error(f"Falha ao processar arquivo: {peticao_conteudo}") return f"**Erro ao Ler Arquivo:**\n{peticao_conteudo}", "", "", "", "", "", "" processing_log.append(f"✅ Texto extraído do {input_source}.") elif peticao_texto and peticao_texto.strip(): input_source = "campo de texto" logger.info(f"Processando petição do {input_source}") peticao_conteudo = peticao_texto.strip() processing_log.append(f"✅ Texto da petição obtido do {input_source}.") else: return "⚠️ **Erro:** Forneça a petição (seja via texto ou arquivo PDF/DOCX/TXT).", "", "", "", "", "", "" if len(peticao_conteudo) < 50: # Arbitrary short length check logger.warning(f"Petição parece muito curta ({len(peticao_conteudo)} chars). Verificando triggers...") # Check for troll/instruction triggers troll_triggers = ["faça a petição", "escreva a peça", "elabore a petição", "crie a petição", "gerar a petição", "preciso de uma petição"] peticao_lower = peticao_conteudo.lower() if any(trigger in peticao_lower for trigger in troll_triggers): logger.warning("Trigger de 'faça por mim' detectado.") return random.choice([ "✋ Opa! A ideia é **você** fazer a peça e **eu** corrigir. 😉 Tente elaborar a petição e me envie para análise!", "📝 Hmm, parece que você está pedindo para eu fazer a peça. Que tal você tentar primeiro? Envie sua versão que eu ajudo a melhorar!", "🤖 Meu trabalho é ser seu corretor, não seu redator! Escreva sua petição com base no caso e eu darei o feedback. 👍", "🧐 Entendido! Mas para corrigir, preciso da *sua* versão da peça. Elabore-a e submeta para avaliação." ]), peticao_conteudo, "", "", "", "", "" else: processing_log.append("⚠️ Atenção: A petição enviada é muito curta, a análise pode ser limitada.") # 2. Cache Check (Optional, but good for debugging/repeated calls) # Simple cache key, might need more robust hashing for large inputs cache_key = f"geral|{tipo_peticao}|{hash(caso)}|{hash(peticao_conteudo)}|{modo_treinamento}" if cache_key in CORRECAO_CACHE: logger.info("Correção GERAL (IA) encontrada no cache.") cached_data = CORRECAO_CACHE[cache_key] # Ensure all expected keys are present in cache required_keys = ["analise_formatada", "peticao_original", "peticao_corrigida", "resumo_diff", "criticas_diff", "avaliacao_criterios_ia", "diff_html"] if all(key in cached_data for key in required_keys): return ( cached_data["analise_formatada"], cached_data["peticao_original"], cached_data["peticao_corrigida"], cached_data["resumo_diff"], cached_data["criticas_diff"], cached_data.get("avaliacao_criterios_ia", ""), # Use .get for safety cached_data["diff_html"] ) else: logger.warning("Cache hit, but data incomplete. Regenerating.") # Optionally remove the incomplete cache entry: del CORRECAO_CACHE[cache_key] # 3. Call Gemini API for Analysis logger.info("Estruturando e enviando prompt de ANÁLISE para a API...") prompt_analise = estruturar_prompt_analise_geral(caso, peticao_conteudo, tipo_peticao, modo_treinamento) processing_log.append("⏳ Enviando pedido de análise para a IA...") analise_bruta = chamar_api(prompt_analise, use_oab_config=False) # Use general config if analise_bruta.startswith("❌") or analise_bruta.startswith("⏱️") or analise_bruta.startswith("⚠️"): logger.error(f"Falha na API (Análise): {analise_bruta}") return f"**Erro na Análise da IA:**\n{analise_bruta}", peticao_conteudo, "", "", "", "", "" processing_log.append("✅ Análise da IA recebida.") logger.info("Análise da IA recebida, formatando...") analise_formatada = formatar_correcao(analise_bruta) processing_log.append("✅ Análise formatada.") # 4. Call Gemini API for Corrected Petition logger.info("Estruturando e enviando prompt de CORREÇÃO para a API...") prompt_peticao = estruturar_prompt_peticao_corrigida_geral(caso, peticao_conteudo, tipo_peticao) processing_log.append("⏳ Enviando pedido de peça corrigida para a IA...") peticao_corrigida_bruta = chamar_api(prompt_peticao, use_oab_config=False) # Use general config, maybe OAB config is better? Test. peticao_corrigida_final = "" if peticao_corrigida_bruta.startswith("❌") or peticao_corrigida_bruta.startswith("⏱️") or peticao_corrigida_bruta.startswith("⚠️"): logger.error(f"Falha na API (Correção): {peticao_corrigida_bruta}") # Don't stop the whole process, just note the failure for this part peticao_corrigida_final = f"⚠️ **Falha ao gerar a peça corrigida pela IA:**\n{peticao_corrigida_bruta}" processing_log.append(f"❌ Falha ao gerar peça corrigida: {peticao_corrigida_bruta}") else: processing_log.append("✅ Peça corrigida (bruta) recebida da IA.") logger.info("Peça corrigida recebida, pós-processando...") peticao_corrigida_final = pos_corrigir_peticao(peticao_corrigida_bruta, tipo_peticao) processing_log.append("✅ Peça corrigida pós-processada.") # 5. Calculate Diff logger.info("Calculando Diffs entre original e corrigida...") processing_log.append("⏳ Calculando diferenças...") # Only calculate diff if correction was successful diff_html, resumo_diff, criticas_diff = "", "", "" if not peticao_corrigida_final.startswith("⚠️"): try: diff_html, resumo_diff, criticas_diff = calcular_diff_interno(peticao_conteudo, peticao_corrigida_final) processing_log.append("✅ Diferenças calculadas.") logger.info("Diffs calculados.") except Exception as diff_e: logger.error(f"Erro ao calcular diff: {diff_e}", exc_info=True) processing_log.append(f"❌ Erro ao calcular diferenças: {diff_e}") resumo_diff = "Erro ao gerar resumo diff." criticas_diff = "Erro ao gerar críticas diff." diff_html = f"
Erro ao gerar visualização diff: {escape(str(diff_e))}
" else: processing_log.append("⚠️ Diferenças não calculadas (falha na geração da peça corrigida).") resumo_diff = "N/A (Peça corrigida não gerada)" criticas_diff = "N/A (Peça corrigida não gerada)" diff_html = "Visualização não disponível (peça corrigida não foi gerada).
" # 6. Prepare Final Output & Update History/Cache # Placeholder for criteria evaluation - could be extracted from analysis_formatada or a separate call avaliacao_criterios_md = "*(Avaliação específica de critérios ainda não implementada nesta extração)*" # Extract from formatted analysis if possible (look for the specific header) crit_match = re.search(r"##\s*🔎\s*Análise Específica dos Requisitos\s*(.*)", analise_formatada, re.DOTALL | re.IGNORECASE) if crit_match: avaliacao_criterios_md = crit_match.group(1).strip() # Remove this section from the main analysis to avoid duplication? Optional. # analise_formatada = re.sub(r"##\s*🔎\s*Análise Específica dos Requisitos.*", "", analise_formatada, flags=re.DOTALL | re.IGNORECASE).strip() # Add processing log to the beginning of the analysis for user feedback log_output = "### 📋 Log do Processamento:\n" + "\n".join([f"* {msg}" for msg in processing_log]) + "\n\n---\n" correcao_completa_formatada = log_output + analise_formatada # Store in history (limit size) resultado_historico = { "data": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "modo": "Geral IA", "tipo_peticao": tipo_peticao, "caso": caso, # Store potentially large case? Consider truncating. "peticao_original": peticao_conteudo, # Store potentially large petition? Consider truncating. "analise_formatada": correcao_completa_formatada, # Store formatted result "peticao_corrigida": peticao_corrigida_final, "resumo_diff": resumo_diff, "criticas_diff": criticas_diff, "avaliacao_criterios_ia": avaliacao_criterios_md, # Don't store potentially huge HTML diff in memory history? # "diff_html": diff_html } HISTORICO.append(resultado_historico) if len(HISTORICO) > 10: # Keep only last 10 entries HISTORICO.pop(0) # Update Cache # Avoid caching huge HTML diff CORRECAO_CACHE[cache_key] = { "analise_formatada": correcao_completa_formatada, "peticao_original": peticao_conteudo, "peticao_corrigida": peticao_corrigida_final, "resumo_diff": resumo_diff, "criticas_diff": criticas_diff, "avaliacao_criterios_ia": avaliacao_criterios_md, "diff_html": diff_html # Cache the HTML diff as well } end_time = time.time() logger.info(f"Correção GERAL (IA) concluída em {end_time - start_time:.2f}s.") # Return all parts for the UI return ( correcao_completa_formatada, # Main analysis + log peticao_conteudo, # Original petition (for reference, maybe hidden) peticao_corrigida_final, # Corrected petition text resumo_diff, # Diff summary markdown criticas_diff, # Diff key changes markdown avaliacao_criterios_md, # Specific criteria evaluation (extracted/placeholder) diff_html # Detailed HTML diff view ) # --- Funções de Correção Padrão OAB --- def parse_oab_pdf(file_obj): if file_obj is None: return None, None, None, "⚠️ Nenhum PDF OAB fornecido." file_path = getattr(file_obj, 'name', None) original_filename = getattr(file_obj, 'orig_name', file_path) logger.info(f"Iniciando parse do PDF OAB: {original_filename}") if not file_path: return None, None, None, "❌ Erro interno: Caminho do arquivo PDF não encontrado." try: # Use the robust text extraction first full_text = extract_text_from_pdf(file_path) # Pass the path if not full_text or full_text.startswith("❌") or full_text.startswith("⚠️"): logger.error(f"Falha na extração de texto do PDF OAB: {full_text}") return None, None, None, f"❌ Falha ao extrair texto do PDF OAB: {full_text}" logger.info("Texto extraído do PDF OAB, procurando seções...") enunciado, gabarito, distribuicao = "", "", "" current_section = None # 'enunciado', 'gabarito', 'distribuicao' lines = full_text.split('\n') # Define more robust section headers, case-insensitive, ignoring surrounding whitespace/chars # Using regex for more flexibility enunciado_header_pattern = re.compile(r"^\s*\W*\s*ENUNCIADO\s*\W*\s*$", re.IGNORECASE) # Gabarito can have variations like "Gabarito Comentado", "Padrão de Resposta - Peça Profissional" gabarito_header_pattern = re.compile(r"^\s*\W*\s*(GABARITO\s*(?:COMENTADO)?|PADRÃO\s*DE\s*RESPOSTA)\s*(?:[-–—]\s*(?:PEÇA|QUESTÃO))?\s*\W*\s*$", re.IGNORECASE) distribuicao_header_pattern = re.compile(r"^\s*\W*\s*DISTRIBUIÇÃO\s*D[OA]S?\s*PONTOS\s*\W*\s*$", re.IGNORECASE) # Define patterns that indicate the END of the relevant sections end_section_pattern = re.compile(r"^\s*\W*\s*(?:PADRÃO\s*DE\s*RESPOSTA\s*[-–—]\s*QUEST(?:ÃO|ÕES)|QUEST(?:ÃO|ÕES)\s*DISCURSIVAS?)\s*\W*\s*$", re.IGNORECASE) for line in lines: line_strip = line.strip() # Check for END pattern first if end_section_pattern.match(line_strip): logger.info(f"Fim da seção de peça detectado em: '{line_strip}'") current_section = None # Stop capturing break # Exit loop once we hit the questions part # Check for Section Headers if enunciado_header_pattern.match(line_strip): logger.info("Seção 'Enunciado' encontrada.") current_section = "enunciado" continue # Skip the header line itself elif gabarito_header_pattern.match(line_strip): logger.info("Seção 'Gabarito/Padrão' encontrada.") current_section = "gabarito" # Add a separator if Gabarito wasn't the first section found if enunciado: gabarito += "\n---\n" continue # Skip the header line itself elif distribuicao_header_pattern.match(line_strip): logger.info("Seção 'Distribuição' encontrada.") current_section = "distribuicao" # Add a separator if Distribuição wasn't the first section found after Gabarito if gabarito: distribuicao += "\n---\n" continue # Skip the header line itself # Append content to the current section if current_section == "enunciado": # Simple cleaning: remove excessive newlines within the section if line_strip or (enunciado and not enunciado.endswith("\n\n")): enunciado += line + "\n" elif current_section == "gabarito": if line_strip or (gabarito and not gabarito.endswith("\n\n")): gabarito += line + "\n" elif current_section == "distribuicao": if line_strip or (distribuicao and not distribuicao.endswith("\n\n")): # Filter out lines that are just page numbers or headers/footers if not re.match(r"^\s*(?:Página\s*\d+|\d+\s*$|EXAME DE ORDEM|FGV)", line_strip, re.IGNORECASE): distribuicao += line + "\n" # Trim final results enunciado = enunciado.strip() gabarito = gabarito.strip() distribuicao = distribuicao.strip() # Validation missing_sections = [] if not enunciado: missing_sections.append("Enunciado") if not gabarito: missing_sections.append("Gabarito/Padrão de Resposta") if not distribuicao: missing_sections.append("Distribuição de Pontos") if missing_sections: logger.warning(f"Parse OAB PDF falhou em encontrar seções: {', '.join(missing_sections)}") # Provide more context in the error message preview_text = full_text[:1000].strip() # Show first 1000 chars error_msg = (f"⚠️ Falha ao separar seções obrigatórias ({', '.join(missing_sections)}). " f"Verifique se o PDF contém os cabeçalhos esperados (ex: 'Enunciado', 'Gabarito Comentado' ou 'Padrão de Resposta', 'Distribuição dos Pontos').\n\n" f"**Início do Texto Extraído:**\n```\n{preview_text}...\n```") # Return the extracted text in the 'gabarito' field for manual inspection if parsing fails badly return None, full_text, None, error_msg logger.info("PDF OAB parseado com sucesso. Seções extraídas.") return enunciado, gabarito, distribuicao, f"✅ PDF OAB carregado e seções extraídas (Enunciado: {len(enunciado)}c, Gabarito: {len(gabarito)}c, Distribuição: {len(distribuicao)}c)." except Exception as e: logger.error(f"Erro inesperado ao parsear PDF OAB ({original_filename}): {str(e)}", exc_info=True) return None, None, None, f"❌ Erro inesperado ao processar PDF OAB: {str(e)}" def gerar_peticao_modelo_oab(enunciado, gabarito, distribuicao): if not enunciado or not gabarito or not distribuicao: return "⚠️ **Erro:** Enunciado, Gabarito e Distribuição são necessários para gerar a peça modelo OAB." if len(enunciado) < 50 or len(gabarito) < 50 or len(distribuicao) < 20: return "⚠️ **Atenção:** O conteúdo do Enunciado, Gabarito ou Distribuição parece muito curto. Verifique se as seções foram extraídas corretamente do PDF ou inseridas manualmente." logger.info("Iniciando geração da peça modelo OAB via API...") prompt = estruturar_prompt_geracao_modelo_oab(enunciado, gabarito, distribuicao) # Use OAB specific config (lower temperature) peticao_modelo_bruta = chamar_api(prompt, use_oab_config=True) if peticao_modelo_bruta.startswith("❌") or peticao_modelo_bruta.startswith("⏱️") or peticao_modelo_bruta.startswith("⚠️"): logger.error(f"Erro API ao gerar modelo OAB: {peticao_modelo_bruta}") return f"❌ **Falha ao gerar peça modelo OAB pela IA:**\n{peticao_modelo_bruta}" logger.info("Peça modelo OAB (bruta) recebida da API. Pós-processando...") # Use a generic type hint for post-processing as we don't know the exact type from the OAB material easily peticao_modelo_final = pos_corrigir_peticao(peticao_modelo_bruta, "Petição Desconhecida (Modelo OAB)") logger.info("Pós-processamento da peça modelo OAB concluído.") return peticao_modelo_final def estruturar_prompt_correcao_pela_oab(peticao_usuario, enunciado, gabarito, distribuicao): # Determine the maximum points from the distribution if possible total_pontos_max = 5.0 # Default OAB score try: # Simple regex to find point values like (0,10 / 0,20 / ... / 1,00) pontos_encontrados = re.findall(r"\(?\s*(\d+[,.]\d+)\s*\)?", distribuicao) if pontos_encontrados: total_pontos_max = sum(float(p.replace(",", ".")) for p in pontos_encontrados) logger.info(f"Pontuação máxima calculada da distribuição: {total_pontos_max}") else: logger.warning("Não foi possível calcular pontuação máxima da distribuição via regex. Usando padrão 5.0.") except Exception as e: logger.error(f"Erro ao calcular pontuação máxima da distribuição: {e}. Usando padrão 5.0.") total_pontos_max = 5.0 prompt = f""" **Tarefa:** Avalie a **Peça do Usuário** comparando-a **rigorosamente** com o padrão oficial da OAB fornecido (Enunciado, Gabarito Comentado, Distribuição dos Pontos). Atribua pontuação para cada item da distribuição e forneça feedback detalhado. **Enunciado (Contexto do Caso):** ``` {enunciado} ``` **Gabarito Comentado (Conteúdo e Fundamentação Esperados):** ``` {gabarito} ``` **Distribuição dos Pontos (Critérios de Avaliação e Estrutura Obrigatória):** ``` {distribuicao} ``` **Peça do Usuário (A ser Avaliada):** ``` {peticao_usuario} ``` **Instruções Detalhadas para Avaliação:** 1. **Análise Item a Item:** Percorra **CADA ITEM** listado na **Distribuição dos Pontos**. 2. **Verificação de Conteúdo:** Para cada item, verifique se a **Peça do Usuário** abordou o ponto de forma correta e completa, conforme o **Gabarito Comentado**. 3. **Atribuição de Pontuação:** Atribua a pontuação PARCIAL ou TOTAL prevista para o item na Distribuição, ou ZERO se ausente/incorreto. Seja rigoroso como um examinador OAB. 4. **Justificativa Clara:** Para **CADA ITEM**, justifique brevemente a pontuação atribuída, explicando o acerto, erro ou omissão do usuário em relação ao padrão esperado (Gabarito/Distribuição). Cite trechos da peça do usuário se relevante. 5. **Cálculo da Nota Final:** Some todas as pontuações obtidas nos itens. 6. **Feedback Geral:** Inclua um breve resumo dos principais acertos e erros gerais da peça. 7. **Formato de Saída (Obrigatório - Use Markdown):** ```markdown ### 📊 Avaliação Detalhada Conforme Padrão OAB **Observação:** A avaliação abaixo segue estritamente os critérios da Distribuição de Pontos e o conteúdo do Gabarito Comentado fornecidos. **Pontuação por Item:** * **[Item 1 da Distribuição - Copiar Texto Exato] (.../... pts):** [Pontos Obtidos] / [Máximo Item 1] * *Justificativa:* [Explicação concisa do acerto/erro/omissão do usuário em relação a este item específico, comparando com o Gabarito. Ex: "Usuário abordou corretamente...", "Usuário omitiu...", "Usuário fundamentou de forma diferente do gabarito..."]. * **[Item 2 da Distribuição - Copiar Texto Exato] (.../... pts):** [Pontos Obtidos] / [Máximo Item 2] * *Justificativa:* [...]. * ... (Repetir para **TODOS** os itens da Distribuição) --- **Pontuação Total Estimada:** [Soma dos Pontos Obtidos] / {total_pontos_max:.2f} --- **Comentários Gerais e Pontos de Melhoria:** * **Principais Acertos:** [Listar 2-3 pontos positivos gerais, se houver]. * **Principais Omissões/Erros:** [Listar os erros ou omissões mais impactantes que não se encaixam perfeitamente nos itens pontuados, ou reforçar os mais graves]. * **Sugestões:** [Breves sugestões para alinhamento ao padrão OAB]. --- *(Fim da Avaliação)* ``` **Importante:** * Baseie **TODA** a avaliação **EXCLUSIVAMENTE** nos materiais OAB fornecidos (Enunciado, Gabarito, Distribuição). Não use conhecimento externo. * Seja **objetivo** e **preciso** nas justificativas e pontuações. * Não invente fatos ou critérios não presentes nos materiais. * Use exatamente o formato Markdown especificado acima. """ return prompt def corrigir_pela_oab(peticao_usuario, enunciado, gabarito, distribuicao): if not peticao_usuario or not enunciado or not gabarito or not distribuicao: return "⚠️ **Erro:** Petição do Usuário, Enunciado, Gabarito e Distribuição são necessários para a correção padrão OAB." if len(peticao_usuario) < 50: return "⚠️ **Atenção:** A peça do usuário parece muito curta para ser avaliada de forma significativa." if len(enunciado) < 50 or len(gabarito) < 50 or len(distribuicao) < 20: return "⚠️ **Atenção:** O conteúdo do Enunciado, Gabarito ou Distribuição parece muito curto. Verifique se as seções foram extraídas corretamente do PDF ou inseridas manualmente antes de prosseguir com a correção." logger.info("Iniciando correção/avaliação conforme padrão OAB via API...") prompt = estruturar_prompt_correcao_pela_oab(peticao_usuario, enunciado, gabarito, distribuicao) # Use OAB specific config (lower temperature for consistency) correcao_oab_bruta = chamar_api(prompt, use_oab_config=True) if correcao_oab_bruta.startswith("❌") or correcao_oab_bruta.startswith("⏱️") or correcao_oab_bruta.startswith("⚠️"): logger.error(f"Erro API ao corrigir/avaliar OAB: {correcao_oab_bruta}") return f"❌ **Falha ao corrigir/avaliar peça conforme padrão OAB pela IA:**\n{correcao_oab_bruta}" logger.info("Correção/Avaliação OAB recebida da API.") # Basic formatting cleanup for markdown might be useful, but the prompt requests specific markdown # final_output = formatar_correcao(correcao_oab_bruta) # Maybe too aggressive? Test. # Let's do minimal cleanup: ensure headers are prominent final_output = correcao_oab_bruta.replace("### 📊 Avaliação Detalhada Conforme Padrão OAB", "\n### 📊 Avaliação Detalhada Conforme Padrão OAB\n") final_output = final_output.replace("**Pontuação por Item:**", "\n**Pontuação por Item:**\n") final_output = final_output.replace("---", "\n---\n") final_output = final_output.replace("**Pontuação Total Estimada:**", "\n**Pontuação Total Estimada:**\n") final_output = final_output.replace("**Comentários Gerais e Pontos de Melhoria:**", "\n**Comentários Gerais e Pontos de Melhoria:**\n") return final_output.strip() # --- Funções Auxiliares (Diff, Histórico, Salvar/Carregar, Gerar Caso Fictício, Dicas, Export) --- # --- Export Functions --- def export_to_txt(content, filename_prefix): """Salva o conteúdo em um arquivo TXT temporário.""" if not content: logger.warning("Tentativa de exportar conteúdo vazio para TXT.") return None # Indicate failure to Gradio try: timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') filename = f"{filename_prefix}_{timestamp}.txt" filepath = os.path.join(tempfile.gettempdir(), filename) # Ensure content is string if not isinstance(content, str): content = str(content) with open(filepath, 'w', encoding='utf-8') as f: f.write(content) logger.info(f"Conteúdo exportado para TXT: {filepath}") return filepath # Return path for Gradio download except Exception as e: logger.error(f"Erro ao exportar para TXT ({filename_prefix}): {str(e)}", exc_info=True) # Optionally raise or return an error indicator if needed by Gradio handling return None # Indicate failure class PDF(FPDF): """Classe FPDF customizada para lidar com cabeçalho/rodapé (opcional).""" def header(self): # Arial bold 15 # self.set_font('Arial', 'B', 15) # Move to the right # self.cell(80) # Title # self.cell(30, 10, 'Título', 1, 0, 'C') # Line break # self.ln(20) pass # No header needed for simple export def footer(self): # Position at 1.5 cm from bottom self.set_y(-15) # Arial italic 8 self.set_font('Arial', 'I', 8) # Page number self.cell(0, 10, 'Página ' + str(self.page_no()) + '/{nb}', 0, 0, 'C') def export_to_pdf(content, filename_prefix): """Salva o conteúdo em um arquivo PDF temporário.""" if not content: logger.warning("Tentativa de exportar conteúdo vazio para PDF.") return None try: timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') filename = f"{filename_prefix}_{timestamp}.pdf" filepath = os.path.join(tempfile.gettempdir(), filename) # Ensure content is string if not isinstance(content, str): content = str(content) pdf = PDF() pdf.alias_nb_pages() pdf.add_page() pdf.set_font('Arial', '', 10) # Use standard font, adjust size as needed # --- Handling potential encoding issues --- # FPDF's default encoding is latin-1. For broader Unicode support, # we often need to encode to latin-1 replacing unsupported chars, # or use fonts that support UTF-8 (like DejaVu, requires font file). # Let's try the latin-1 replacement method first for simplicity. try: # Encode to latin-1, replacing unknown chars, then decode back (needed by FPDF) # This ensures FPDF doesn't crash on Unicode, but might show '?' for some chars. content_for_pdf = content.encode('latin-1', 'replace').decode('latin-1') except Exception as enc_e: logger.warning(f"Erro ao codificar para Latin-1 no PDF, tentando UTF-8 com replace: {enc_e}") # Fallback: try encoding UTF-8 with replace (less likely to work directly with standard fonts) content_for_pdf = content.encode('utf-8', 'replace').decode('utf-8') # Add content using multi_cell for automatic line breaks pdf.multi_cell(0, 5, content_for_pdf) # Width 0 = full width, Height 5 = line height pdf.output(filepath, 'F') # 'F' saves to local file logger.info(f"Conteúdo exportado para PDF: {filepath}") return filepath except Exception as e: logger.error(f"Erro ao exportar para PDF ({filename_prefix}): {str(e)}", exc_info=True) # Example: FPDFError: Some data cannot be encoded in latin-1 if "cannot be encoded in latin-1" in str(e): logger.error("PDF Export Failed: Content has characters not supported by the standard PDF font encoding (Latin-1). Consider installing/using a UTF-8 font like DejaVu.") # You could return a specific message here if Gradio handles it # return "ERROR: PDF_ENCODING_ISSUE" return None # Indicate failure # --- Diff Function (Internal - No changes from original) --- def calcular_diff_interno(texto_original, texto_corrigido): if not texto_original or not texto_corrigido or texto_corrigido.startswith("⚠️") or texto_corrigido.startswith("❌"): logger.warning("Diff não calculado: textos inválidos ou ausentes.") return "","N/A (Textos inválidos/ausentes)","N/A" # Normalize line endings and split orig_lines = texto_original.replace('\r\n', '\n').splitlines() corr_lines = texto_corrigido.replace('\r\n', '\n').splitlines() if not orig_lines and not corr_lines: return "Ambos os textos estão vazios.
", "Vazio", "Nenhuma mudança." if not orig_lines: return f"Texto original vazio. Texto corrigido adicionado:
Texto corrigido vazio. Texto original removido:
Nenhuma diferença visual a ser exibida.
" html_output = f"{css}A visualização das diferenças (diff) aparecerá aqui após a correção...
") # ================================= # TAB: Correção Padrão OAB # ================================= with gr.TabItem("📄 Correção Padrão OAB", id=1): gr.Markdown("Use esta aba para **corrigir sua peça** usando o **padrão de resposta oficial** de um Exame da OAB anterior, ou para **gerar uma peça modelo** com base nesse padrão.") with gr.Row(): # --- Coluna 1: Inputs do Padrão OAB --- with gr.Column(scale=1): gr.Markdown("### 1. Material Oficial da OAB") gr.Markdown("Forneça o Enunciado, Gabarito Comentado e Distribuição de Pontos.") with gr.Tabs(): with gr.TabItem("📎 Carregar PDF Oficial"): oab_pdf_input = gr.File(label="Padrão de Resposta OAB (.pdf)", file_types=[".pdf"], type="filepath") parse_status_output = gr.Textbox(label="Status Extração PDF", interactive=False, lines=2) gr.Markdown("_O sistema tentará extrair as seções automaticamente do PDF._") with gr.TabItem("✏️ Inserir Manualmente"): oab_enunciado_input = gr.Textbox(label="Enunciado", lines=5, placeholder="Cole o Enunciado aqui...") oab_gabarito_input = gr.Textbox(label="Gabarito Comentado / Padrão Resposta", lines=8, placeholder="Cole o Gabarito Comentado ou Padrão de Resposta aqui...") oab_distribuicao_input = gr.Textbox(label="Distribuição dos Pontos", lines=8, placeholder="Cole a Distribuição dos Pontos aqui...") # --- Coluna 2: Ações e Input da Peça do Usuário --- with gr.Column(scale=1): gr.Markdown("### 2. Ações e Sua Peça") gr.Markdown("**Gerar Peça Modelo:**") btn_gerar_modelo_oab = gr.Button("📝 Gerar Peça Modelo OAB (Baseado no Material Acima)") gr.Markdown("---") gr.Markdown("**Corrigir Sua Peça:**") peticao_usuario_oab_input = gr.Textbox(label="Sua Peça Prática (para corrigir pela OAB)", lines=10, placeholder="Cole aqui a peça que você elaborou para este caso OAB...") btn_corrigir_pela_oab = gr.Button("🎯 Corrigir Minha Peça (Conforme Padrão OAB)", variant="primary") # --- Area de Resultados da Correção OAB --- gr.Markdown("---") gr.Markdown("### 3. Resultados (Padrão OAB)") processing_message_oab = gr.Markdown("", visible=False) # Para mensagens de "Processando..." with gr.Tabs(): with gr.TabItem("📝 Peça Modelo OAB (Gerada)"): resultado_modelo_oab_output = gr.Textbox(label="Texto da Peça Modelo Gerada pela IA:", lines=20, show_copy_button=True, interactive=True) with gr.Row(): export_modelo_txt_btn = gr.Button("📄 Exportar Modelo (TXT)") export_modelo_pdf_btn = gr.Button("📜 Exportar Modelo (PDF)") download_modelo_file = gr.File(label="Baixar Modelo", interactive=False) with gr.TabItem("🎯 Avaliação da Sua Peça (Conforme OAB)"): resultado_correcao_oab_output = gr.Markdown("*A avaliação da sua peça conforme o padrão OAB aparecerá aqui...*") with gr.Row(): export_avaliacao_txt_btn = gr.Button("📄Exportar Avaliação (TXT)") export_avaliacao_pdf_btn = gr.Button("📜Exportar Avaliação (PDF)") download_avaliacao_file = gr.File(label="Baixar Avaliação", interactive=False) # --- Botão Limpar --- clear_btn = gr.Button("🗑️ Limpar Todos os Campos", variant="stop") # ============================ # DEFINIÇÃO DOS EVENTOS (Clicks e Changes) # ============================ # --- Eventos da Aba Geral IA --- caso_btn_geral.click( gerar_caso_ficticio, inputs=[tipo_peticao_input_geral], outputs=[caso_input_geral] ) # Atualizar dicas contextuais ao digitar na petição ou mudar tipo peticao_texto_input_geral.change( fornecer_dica_contextual, inputs=[peticao_texto_input_geral, tipo_peticao_input_geral], outputs=dica_contextual_output_geral, # queue=False # Make tips appear faster? Test impact. ) tipo_peticao_input_geral.change( fornecer_dica_contextual, inputs=[peticao_texto_input_geral, tipo_peticao_input_geral], outputs=dica_contextual_output_geral, # queue=False ) # Processar arquivo ao ser carregado para preview/status peticao_arquivo_input_geral.change( process_file, inputs=[peticao_arquivo_input_geral], outputs=[arquivo_preview_output_geral] ) # Botão Principal de Correção (Texto) btn_corrigir_geral.click( lambda: gr.update(value="⏳ Processando Análise e Correção (IA)... Pode levar alguns instantes.", visible=True), outputs=processing_message_geral ).then( corrigir_peticao_geral_ia, inputs=[caso_input_geral, tipo_peticao_input_geral, peticao_texto_input_geral, gr.State(None), modo_treinamento_geral], # Pass None for peticao_arquivo outputs=[ resultado_analise_output_geral, # 📊 Análise IA peticao_original_state_geral, # Hidden state for original petition text resultado_peticao_corrigida_output_geral, # ✨ Correção IA Textbox resumo_alteracoes_output_geral, # ⚖️ Diff Resumo Markdown criticas_output_geral, # ⚖️ Diff Críticas Markdown gr.Markdown(visible=False), # Placeholder (was avaliacao_criterios_md) - ignore direct output visualizacao_detalhada_output_geral # ⚖️ Diff HTML ] ).then( lambda: gr.update(visible=False), outputs=processing_message_geral # Hide message on completion ) # Botão Principal de Correção (Arquivo) - Similar ao de Texto # Note: Renamed button in UI logic for clarity, even if UI shows one button concept per tab btn_corrigir_arquivo_geral = btn_corrigir_geral # Link logic to the same button for simplicity if UI merges them visually # We need distinct click handlers if UI has separate buttons, or handle internally in corrigir_peticao_geral_ia # Assuming for now the UI has one button, and corrigir_peticao_geral_ia handles which input (text/file) is present. # If UI had separate buttons, copy the .click() block above, switching peticao_texto_input_geral to gr.State(None) # and peticao_arquivo_input_geral to the actual file input component. # Let's refine the click handler to explicitly pass the file if the file tab is active (conceptually) # Gradio doesn't easily expose active tab, so we pass both text and file and let the Python function decide. # The logic inside corrigir_peticao_geral_ia already prioritizes file if present. # Salvar e Carregar Inputs salvar_btn_geral.click( lambda: gr.update(value="⏳ Salvando...", visible=True), outputs=processing_message_geral ).then( salvar_progresso, inputs=[caso_input_geral, peticao_texto_input_geral, tipo_peticao_input_geral], outputs=[gr.State(None), saveload_status_geral, progresso_download_file_geral] # Hidden data, status, file output ).then( # Make file component visible only if a path was returned lambda x: gr.update(visible=bool(x)), inputs=[progresso_download_file_geral], outputs=[progresso_download_file_geral] ).then( lambda: gr.update(visible=False), outputs=processing_message_geral ) carregar_btn_geral.click( lambda: gr.update(value="⏳ Carregando...", visible=True), outputs=processing_message_geral ).then( carregar_progresso, inputs=[arquivo_progresso_upload_geral], outputs=[tipo_peticao_input_geral, caso_input_geral, peticao_texto_input_geral, saveload_status_geral] # Update the main inputs ).then( lambda: gr.update(visible=False), outputs=processing_message_geral ) # --- Eventos da Aba Padrão OAB --- oab_pdf_input.change( parse_oab_pdf, inputs=[oab_pdf_input], outputs=[oab_enunciado_input, oab_gabarito_input, oab_distribuicao_input, parse_status_output] ) btn_gerar_modelo_oab.click( lambda: gr.update(value="⏳ Gerando Peça Modelo OAB (IA)...", visible=True), outputs=processing_message_oab ).then( gerar_peticao_modelo_oab, inputs=[oab_enunciado_input, oab_gabarito_input, oab_distribuicao_input], outputs=[resultado_modelo_oab_output] ).then( lambda: gr.update(visible=False), outputs=processing_message_oab ) btn_corrigir_pela_oab.click( lambda: gr.update(value="⏳ Corrigindo sua peça conforme Padrão OAB (IA)...", visible=True), outputs=processing_message_oab ).then( corrigir_pela_oab, inputs=[peticao_usuario_oab_input, oab_enunciado_input, oab_gabarito_input, oab_distribuicao_input], outputs=[resultado_correcao_oab_output] ).then( lambda: gr.update(visible=False), outputs=processing_message_oab ) # --- Eventos de Export --- # Export Análise IA export_analise_txt_btn.click(export_to_txt, inputs=[resultado_analise_output_geral, gr.State("analise_ia")], outputs=download_analise_file) export_analise_pdf_btn.click(export_to_pdf, inputs=[resultado_analise_output_geral, gr.State("analise_ia")], outputs=download_analise_file) # Export Correção IA export_correcao_txt_btn.click(export_to_txt, inputs=[resultado_peticao_corrigida_output_geral, gr.State("correcao_ia")], outputs=download_correcao_file) export_correcao_pdf_btn.click(export_to_pdf, inputs=[resultado_peticao_corrigida_output_geral, gr.State("correcao_ia")], outputs=download_correcao_file) # Export Modelo OAB export_modelo_txt_btn.click(export_to_txt, inputs=[resultado_modelo_oab_output, gr.State("modelo_oab")], outputs=download_modelo_file) export_modelo_pdf_btn.click(export_to_pdf, inputs=[resultado_modelo_oab_output, gr.State("modelo_oab")], outputs=download_modelo_file) # Export Avaliação OAB export_avaliacao_txt_btn.click(export_to_txt, inputs=[resultado_correcao_oab_output, gr.State("avaliacao_oab")], outputs=download_avaliacao_file) export_avaliacao_pdf_btn.click(export_to_pdf, inputs=[resultado_correcao_oab_output, gr.State("avaliacao_oab")], outputs=download_avaliacao_file) # --- Evento Limpar Tudo --- # Define ALL components to be cleared all_inputs_outputs = [ # Aba Geral IA Inputs tipo_peticao_input_geral, modo_treinamento_geral, caso_input_geral, peticao_texto_input_geral, peticao_arquivo_input_geral, arquivo_preview_output_geral, dica_contextual_output_geral, saveload_status_geral, arquivo_progresso_upload_geral, progresso_download_file_geral, # Aba Geral IA Outputs processing_message_geral, resultado_analise_output_geral, resultado_peticao_corrigida_output_geral, resumo_alteracoes_output_geral, criticas_output_geral, visualizacao_detalhada_output_geral, download_analise_file, download_correcao_file, # Aba OAB Inputs oab_pdf_input, parse_status_output, oab_enunciado_input, oab_gabarito_input, oab_distribuicao_input, peticao_usuario_oab_input, # Aba OAB Outputs processing_message_oab, resultado_modelo_oab_output, resultado_correcao_oab_output, download_modelo_file, download_avaliacao_file, # Configs / Historico Outputs config_status_geral, progresso_output, historico_output, # States peticao_original_state_geral ] # Define default values for each component type upon clearing clear_values = [ "Petição Inicial", False, "", # tipo, modo_trein, caso "", None, "", # peticao_texto, peticao_arquivo, preview "*(Dicas aparecerão aqui...)*", "", None, None, # dicas, saveload_status, upload, download "", "", "", # processing_msg, analise, correcao "", "", "(Diff será exibido aqui)
", # resumo, criticas, diff_html None, None, # download_analise, download_correcao None, "", "", "", "", "", # oab_pdf, parse_status, enunciado, gabarito, distribuicao, peticao_usuario "", "", "", # processing_oab, modelo, correcao_oab None, None, # download_modelo, download_avaliacao "", "", "", # config_status, progresso, historico "" # peticao_original_state ] clear_btn.click( fn=lambda: clear_values, inputs=None, outputs=all_inputs_outputs ) return demo if __name__ == "__main__": # Ensure necessary directories exist (like temp) - usually handled by OS # Consider adding font file download/check for PDF export if using custom fonts logger.info("Iniciando a aplicação Gradio Corretor Jurídico OAB Master...") try: app = interface_gradio() # Launch with share=True for public link (optional) and debug=True for more logs (optional) app.launch(debug=False, share=False) logger.info("Aplicação Gradio encerrada.") except Exception as e: logger.exception("Erro fatal ao iniciar ou executar a aplicação Gradio.") # Fallback print if logging fails print(f"ERRO FATAL: {e}")