diff --git "a/app.py" "b/app.py" new file mode 100644--- /dev/null +++ "b/app.py" @@ -0,0 +1,1984 @@ +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 +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 + +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", + "Reclamação Trabalhista", + "Contestação Trabalhista", + "Recurso Ordinário", + "Mandado de Segurança", + "Habeas Data", + "Ação Popular", +] + +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).", + "Reclamação Trabalhista (Endereçamento)": "Enderece à Vara do Trabalho competente. A competência territorial é, em regra, o local da prestação de serviços (Art. 651, CLT).", + "Reclamação Trabalhista (Pedidos)": "Os pedidos devem ser líquidos (com valor atribuído) nos ritos sumaríssimo e ordinário, conforme Art. 840, §1º, da CLT.", + "Recurso Ordinário (Pressupostos)": "Além dos pressupostos comuns, atente-se ao depósito recursal e ao recolhimento das custas processuais (Art. 899, CLT).", + "Mandado de Segurança (Fundamentação)": "É crucial demonstrar o 'direito líquido e certo', o 'ato coator' ilegal ou abusivo e a 'autoridade coatora'. Fundamente na Lei 12.016/09.", + "Mandado de Segurança (Cabimento)": "Lembre-se: não cabe MS quando houver recurso específico com efeito suspensivo ou contra lei em tese (Súmulas STF/STJ).", +} + +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: + 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 + 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 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 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'): + if hasattr(file_path_or_obj, 'seek'): + try: file_path_or_obj.seek(0) + except io.UnsupportedOperation: pass + content = file_path_or_obj.read() + 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: + 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: + file_path = getattr(file_obj, 'name', None) + 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: + 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}") + + if filename_lower.endswith('.pdf'): + return extract_text_from_pdf(file_path) + elif filename_lower.endswith('.docx'): + return extract_text_from_docx(file_path) + elif filename_lower.endswith('.txt'): + 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." + + 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." + 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." + 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) + 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, + } + } + 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: + with rate_limiter: + start_req = time.time() + response = requests.post( + GEMINI_API_URL, + headers=headers, + data=json.dumps(payload), + timeout=CONFIG["timeout"] + ) + end_req = time.time() + logger.info(f"API request time: {end_req - start_req:.2f}s") + + + if response.status_code == 429: + retry_after = response.headers.get("Retry-After") + wait_time = CONFIG["retry_delay"] * (tentativa + 1) + if retry_after: + try: + wait_time = int(retry_after) + except ValueError: + logger.warning(f"Invalid Retry-After header: {retry_after}. Using default backoff.") + 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 + 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." + + if response.status_code != 200: + error_msg = f"Erro desconhecido (Status {response.status_code})" + try: + error_info = response.json() + error_details = error_info.get("error", {}).get("message", "") + if error_details: + error_msg = f"Erro API ({response.status_code}): {error_details}" + else: + error_msg = f"Erro API ({response.status_code}). Resposta: {response.text[:200]}..." + + except json.JSONDecodeError: + error_msg = f"Erro API ({response.status_code}): Resposta não-JSON: {response.text[:200]}..." + except Exception as json_e: + 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: + time.sleep(CONFIG["retry_delay"] * (tentativa + 1)) + continue + else: + return f"❌ {error_msg} (Após {CONFIG['max_retry_attempts']} tentativas)" + + + 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]}...") + return f"❌ Erro processar resposta API (JSON inválido na resposta 200 OK)." + + + if not response_data.get("candidates"): + prompt_feedback = response_data.get("promptFeedback", {}) + block_reason = prompt_feedback.get("blockReason") + safety_ratings = prompt_feedback.get("safetyRatings", []) + + 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.") + 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." + + + candidate = response_data["candidates"][0] + + finish_reason = candidate.get("finishReason") + if finish_reason and finish_reason not in ["STOP", "MAX_TOKENS"]: + logger.error(f"Geração interrompida pela API: {finish_reason}") + 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)}." + + 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: + return f"❌ Geração interrompida inesperadamente. {reason_details}" + + + 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." + + 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.") + return texto_gerado + 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." + + + 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)) + 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: + 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}" + + except Exception as e: + logger.exception(f"Erro inesperado durante chamada API (tentativa {tentativa + 1}): {str(e)}") + if tentativa < CONFIG["max_retry_attempts"] - 1: + time.sleep(CONFIG["retry_delay"] * (tentativa + 1)) + continue + else: + return f"❌ Erro inesperado durante a comunicação com a API: {str(e)}" + + + 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 = [] + 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 == "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 == "Reclamação Trabalhista": + requisitos = [ + "- **Endereçamento:** Vara do Trabalho competente (Art. 651, CLT).", + "- **Qualificação Completa das Partes:** Reclamante e Reclamada, incluindo CPF/CNPJ (Art. 840, §1º, CLT).", + "- **Exposição dos Fatos:** Breve exposição dos fatos que resultam o dissídio (contratação, função, salário, jornada, demissão).", + "- **Fundamentação Jurídica:** Artigos da CLT, Súmulas do TST e legislação correlata aplicável ao caso.", + "- **Pedidos:** Certos, determinados e **com indicação de seu valor** (líquidos) (Art. 840, §1º, CLT). Ex: verbas rescisórias, horas extras, adicionais.", + "- **Valor da Causa:** Soma dos valores de todos os pedidos.", + "- **Indicação das Provas:** Especificação dos meios de prova pretendidos.", + "- **Notificação da Reclamada:** Pedido de notificação postal da reclamada.", + "- **Requerimentos Finais:** Pedido de procedência, juros, correção monetária, honorários sucumbenciais (Art. 791-A, CLT), justiça gratuita.", + "- **Fechamento Formal:** Padrão OAB." + ] + elif tipo_peticao == "Contestação Trabalhista": + requisitos = [ + "- **Endereçamento:** Ao juízo da causa (Vara do Trabalho).", + "- **Identificação:** Número do processo, qualificação da Reclamada.", + "- **Tempestividade:** Alegação da apresentação dentro do prazo legal.", + "- **Preliminares/Prejudiciais:** Inépcia, prescrição bienal/quinquenal, etc.", + "- **Mérito - Impugnação Específica:** Rebater todos os fatos e pedidos da inicial (Súmula 338 TST sobre jornada, por exemplo).", + "- **Fundamentação Jurídica (Mérito):** Leis, Súmulas e OJs que amparam a defesa.", + "- **Compensação/Dedução:** Alegar eventuais valores já pagos sob os mesmos títulos.", + "- **Pedidos:** Acolhimento das preliminares/prejudiciais; No mérito, IMPROCEDÊNCIA TOTAL dos pedidos; Condenação em honorários sucumbenciais.", + "- **Indicação de Provas:** Especificar as provas que a reclamada pretende produzir.", + "- **Fechamento Formal:** Padrão OAB." + ] + elif tipo_peticao == "Recurso Ordinário": + requisitos = [ + "- **Peça de Interposição:** Endereçada ao juízo 'a quo' (Vara do Trabalho).", + "- **Peça de Razões:** Endereçada ao Tribunal Regional do Trabalho (TRT).", + "- **Pressupostos de Admissibilidade:** Cabimento (sentença), Tempestividade (8 dias), Preparo (custas e **depósito recursal**, se empresa).", + "- **Fundamentação da Reforma (Razões):** Princípio da dialeticidade - Impugnar especificamente os fundamentos da sentença recorrida.", + "- **Pedidos (Razões):** Conhecimento e Provimento do recurso para reformar a sentença; Inversão do ônus da sucumbência.", + "- **Fechamento Formal:** Padrão OAB (duas peças)." + ] + elif tipo_peticao == "Mandado de Segurança": + requisitos = [ + "- **Endereçamento e Competência:** Definição correta do juízo competente para julgar o ato da autoridade coatora.", + "- **Qualificação:** Impetrante, e indicação da Autoridade Coatora e da pessoa jurídica a que pertence.", + "- **Cabimento:** Demonstração da ausência de amparo por Habeas Corpus/Data e de recurso com efeito suspensivo.", + "- **Exposição dos Fatos e do Ato Coator:** Narrativa clara do ocorrido e do ato específico tido como ilegal ou abusivo.", + "- **Fundamentação - Direito Líquido e Certo:** Prova pré-constituída e demonstração inequívoca do direito violado (Lei 12.016/09).", + "- **Pedido Liminar:** Se houver urgência (fumus boni iuris e periculum in mora), pedir a suspensão do ato coator.", + "- **Pedidos Finais:** Notificação da autoridade, ciência ao órgão de representação judicial, concessão da segurança para anular/modificar o ato, condenação em custas.", + "- **Valor da Causa:** Geralmente para fins fiscais.", + "- **Fechamento Formal:** Padrão OAB." + ] + 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 + +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}):** +""" + 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 == "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 == "Reclamação Trabalhista": + instrucoes_tipo = [ + "1. **Endereçamento:** 'AO JUÍZO DA ___ VARA DO TRABALHO DE ___'.", + "2. **Qualificação:** Completa do Reclamante e da Reclamada (Art. 840, CLT).", + "3. **Fatos:** Narrativa clara e cronológica da relação de emprego.", + "4. **Direito:** Tópicos separados para cada verba/direito pleiteado (horas extras, verbas rescisórias, adicionais), com fundamentação legal (CLT, Súmulas).", + "5. **Pedidos:** Lista de pedidos certos, determinados e **líquidos (com valor)**, em conformidade com o Art. 840, §1º, da CLT.", + "6. **Requerimentos:** Notificação da reclamada, produção de provas, justiça gratuita, honorários de sucumbência.", + "7. **Valor da Causa:** A soma dos valores dos pedidos.", + "8. **Fechamento:** Padrão OAB.", + ] + elif tipo_peticao == "Mandado de Segurança": + instrucoes_tipo = [ + "1. **Endereçamento:** Juízo competente para julgar o ato da autoridade coatora.", + "2. **Qualificação:** Completa do Impetrante. Indicação precisa da Autoridade Coatora e do órgão/pessoa jurídica vinculada.", + "3. **Estrutura:** Tópicos claros para 'Do Cabimento', 'Dos Fatos e do Ato Coator', 'Do Direito Líquido e Certo', 'Da Medida Liminar' (se houver).", + "4. **Prova Pré-Constituída:** Enfatizar que as provas do direito estão anexas e são inequívocas.", + "5. **Pedidos:** Deferimento da liminar (se pedida); Notificação da autoridade; Ciência ao órgão de representação; No mérito, a **concessão da segurança** para o fim específico (anular/corrigir o ato); condenação em custas.", + "6. **Fechamento:** Padrão OAB.", + ] + 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)" + + 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: + linhas_sem_vazias_extras.append("") + last_line_was_blank = True + 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) + + peticao_corrigida = re.sub(r"[Nn]estes termos,?\s*pede deferimento\.?", "Nestes termos, pede deferimento.", peticao_corrigida) + + 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: + 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() + + 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}") + prefix += "\nRequer, por fim, a juntada do comprovante de recolhimento das custas processuais." + + 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." + + peticao_corrigida = prefix.strip() + suffix.strip() + + 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 = [] + 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 + if not is_closing_line and linha_lower != "": + end_content_index = i + 1 + break + elif linha_lower == "": + continue + else: + pass + + linhas_conteudo = linhas_finais[:end_content_index] + + peticao_final = "\n".join(linhas_conteudo).strip() + "\n\n" + "\n".join(fechamento_padrao) + + return peticao_final.strip() + + +def formatar_correcao(texto): + 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) + + linhas = texto.split('\n') + linhas_formatadas = [] + in_list = False + for linha in linhas: + linha_strip = linha.strip() + + match_num_list = re.match(r"^\s*(\d+)\.\s*(.*)", linha) + match_bullet_list = re.match(r"^\s*([\*\-])\s+(.*)", linha) + match_sub_list = re.match(r"^\s+([a-z])\)\s+(.*)", linha) + match_sub_sub_list = re.match(r"^\s{4,}([ivx]+)\.\s+(.*)", linha) + + if match_num_list: + num, content = match_num_list.groups() + linhas_formatadas.append(f"\n**{num}. {content.strip()}**") + in_list = False + elif match_bullet_list: + bullet, content = match_bullet_list.groups() + prefix = "\n* " if not linhas_formatadas or not linhas_formatadas[-1].strip().endswith(("*",":")) else "* " + linhas_formatadas.append(prefix + content.strip()) + in_list = True + elif match_sub_list: + letter, content = match_sub_list.groups() + linhas_formatadas.append(f" * {letter}) {content.strip()}") + in_list = True + elif match_sub_sub_list: + roman, content = match_sub_sub_list.groups() + linhas_formatadas.append(f" - {roman}. {content.strip()}") + in_list = True + else: + if linha.startswith(" ") or linha.startswith("```"): + linhas_formatadas.append(linha) + else: + if linha_strip and (not linhas_formatadas or linhas_formatadas[-1].strip() == "" or linhas_formatadas[-1].startswith("##")): + linhas_formatadas.append(linha_strip) + elif linha_strip: + linhas_formatadas.append(" " + linha_strip) + elif linhas_formatadas and linhas_formatadas[-1].strip() != "": + linhas_formatadas.append("") + + 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) + + 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" + 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}") + + 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 = [] + + 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: + logger.warning(f"Petição parece muito curta ({len(peticao_conteudo)} chars). Verificando 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.") + + + 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] + 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", ""), + cached_data["diff_html"] ) + else: + logger.warning("Cache hit, but data incomplete. Regenerating.") + + + 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) + + 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.") + + + 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) + + 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}") + 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.") + + + logger.info("Calculando Diffs entre original e corrigida...") + processing_log.append("⏳ Calculando diferenças...") + 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).
" + + + avaliacao_criterios_md = "*(Avaliação específica de critérios ainda não implementada nesta extração)*" + 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() + + 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 + + resultado_historico = { + "data": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), + "modo": "Geral IA", + "tipo_peticao": tipo_peticao, + "caso": caso, + "peticao_original": peticao_conteudo, + "analise_formatada": correcao_completa_formatada, + "peticao_corrigida": peticao_corrigida_final, + "resumo_diff": resumo_diff, + "criticas_diff": criticas_diff, + "avaliacao_criterios_ia": avaliacao_criterios_md, + } + HISTORICO.append(resultado_historico) + if len(HISTORICO) > 10: + HISTORICO.pop(0) + + 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 + } + + end_time = time.time() + logger.info(f"Correção GERAL (IA) concluída em {end_time - start_time:.2f}s.") + + return ( + correcao_completa_formatada, + peticao_conteudo, + peticao_corrigida_final, + resumo_diff, + criticas_diff, + avaliacao_criterios_md, + diff_html + ) + + +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: + full_text = extract_text_from_pdf(file_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 + lines = full_text.split('\n') + + enunciado_header_pattern = re.compile(r"^\s*\W*\s*ENUNCIADO\s*\W*\s*$", re.IGNORECASE) + 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) + 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() + + if end_section_pattern.match(line_strip): + logger.info(f"Fim da seção de peça detectado em: '{line_strip}'") + current_section = None + break + + if enunciado_header_pattern.match(line_strip): + logger.info("Seção 'Enunciado' encontrada.") + current_section = "enunciado" + continue + elif gabarito_header_pattern.match(line_strip): + logger.info("Seção 'Gabarito/Padrão' encontrada.") + current_section = "gabarito" + if enunciado: gabarito += "\n---\n" + continue + elif distribuicao_header_pattern.match(line_strip): + logger.info("Seção 'Distribuição' encontrada.") + current_section = "distribuicao" + if gabarito: distribuicao += "\n---\n" + continue + + if current_section == "enunciado": + 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")): + if not re.match(r"^\s*(?:Página\s*\d+|\d+\s*$|EXAME DE ORDEM|FGV)", line_strip, re.IGNORECASE): + distribuicao += line + "\n" + + + enunciado = enunciado.strip() + gabarito = gabarito.strip() + distribuicao = distribuicao.strip() + + 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)}") + preview_text = full_text[:1000].strip() + 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 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) + 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...") + 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): + total_pontos_max = 5.0 + try: + 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) + 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.") + 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() + + +def export_to_txt(content, filename_prefix): + if not content: + logger.warning("Tentativa de exportar conteúdo vazio para TXT.") + return None + + try: + timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') + filename = f"{filename_prefix}_{timestamp}.txt" + filepath = os.path.join(tempfile.gettempdir(), filename) + + 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 + except Exception as e: + logger.error(f"Erro ao exportar para TXT ({filename_prefix}): {str(e)}", exc_info=True) + return None + + +class PDF(FPDF): + def header(self): + pass + + def footer(self): + self.set_y(-15) + self.set_font('Arial', 'I', 8) + self.cell(0, 10, 'Página ' + str(self.page_no()) + '/{nb}', 0, 0, 'C') + +def export_to_pdf(content, filename_prefix): + 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) + + if not isinstance(content, str): + content = str(content) + + pdf = PDF() + pdf.alias_nb_pages() + pdf.add_page() + pdf.set_font('Arial', '', 10) + + try: + 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}") + content_for_pdf = content.encode('utf-8', 'replace').decode('utf-8') + + + pdf.multi_cell(0, 5, content_for_pdf) + + pdf.output(filepath, 'F') + + 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) + 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.") + return None + +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" + + 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...
") + + 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(): + 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...") + + 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") + + + gr.Markdown("---") + gr.Markdown("### 3. Resultados (Padrão OAB)") + processing_message_oab = gr.Markdown("", visible=False) + + 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) + + clear_btn = gr.Button("🗑️ Limpar Todos os Campos", variant="stop") + + + caso_btn_geral.click( + gerar_caso_ficticio, + inputs=[tipo_peticao_input_geral], + outputs=[caso_input_geral] + ) + + peticao_texto_input_geral.change( + fornecer_dica_contextual, + inputs=[peticao_texto_input_geral, tipo_peticao_input_geral], + outputs=dica_contextual_output_geral, + ) + tipo_peticao_input_geral.change( + fornecer_dica_contextual, + inputs=[peticao_texto_input_geral, tipo_peticao_input_geral], + outputs=dica_contextual_output_geral, + ) + + peticao_arquivo_input_geral.change( + process_file, + inputs=[peticao_arquivo_input_geral], + outputs=[arquivo_preview_output_geral] + ) + + 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, peticao_arquivo_input_geral, modo_treinamento_geral], + outputs=[ + resultado_analise_output_geral, + peticao_original_state_geral, + resultado_peticao_corrigida_output_geral, + resumo_alteracoes_output_geral, + criticas_output_geral, + gr.Markdown(visible=False), + visualizacao_detalhada_output_geral + ] + ).then( + lambda: gr.update(visible=False), outputs=processing_message_geral + ) + + 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] + ).then( + 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] + ).then( + lambda: gr.update(visible=False), outputs=processing_message_geral + ) + + 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 + ) + + 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_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_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_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) + + all_inputs_outputs = [ + 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, + 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, + oab_pdf_input, parse_status_output, oab_enunciado_input, oab_gabarito_input, + oab_distribuicao_input, peticao_usuario_oab_input, + processing_message_oab, resultado_modelo_oab_output, resultado_correcao_oab_output, + download_modelo_file, download_avaliacao_file, + config_status_geral, progresso_output, historico_output, + peticao_original_state_geral + ] + clear_values = [ + "Petição Inicial", False, "", + "", None, "", + "*(Dicas aparecerão aqui...)*", "", None, None, + "", "", "", + "", "", "(Diff será exibido aqui)
", + None, None, + None, "", "", "", "", "", + "", "", "", + None, None, + "", "", "", + "" + ] + + clear_btn.click( + fn=lambda: clear_values, + inputs=None, + outputs=all_inputs_outputs + ) + + return demo + +if __name__ == "__main__": + logger.info("Iniciando a aplicação Gradio Corretor Jurídico OAB Master...") + try: + app = interface_gradio() + 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.") + print(f"ERRO FATAL: {e}") \ No newline at end of file