diff --git "a/app.py" "b/app.py" --- "a/app.py" +++ "b/app.py" @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- import gradio as gr import requests import os @@ -11,7 +10,7 @@ import tempfile from dotenv import load_dotenv from pdfplumber import open as pdfplumber_open from PIL import Image -import pytesseract +import pytesseract # Requires Tesseract installation import cv2 import numpy as np from datetime import datetime @@ -21,7 +20,7 @@ from html import escape import random import logging import re -from fpdf import FPDF # Import FPDF +from fpdf import FPDF # Added for PDF export logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') logger = logging.getLogger(__name__) @@ -30,517 +29,2258 @@ 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.") + 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} +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", + "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.", "Fundamentação Legal": "Sempre cite lei/súmula/jurisprudência.", - "Coerência Argumentativa": "Mantenha linha lógica.", "Revisão Ortográfica e Gramatical": "Erros prejudicam credibilidade.", - "Formatação": "Parágrafos definidos, espaçamento, destaque moderado.", - "Endereçamento (Inicial)": "Verifique competência. Consumo: foro domicílio autor (Art. 101, I, CDC).", - "Qualificação (Inicial)": "Dados Art. 319, II, CPC. Use 'XXX'.", "Fatos (Inicial)": "Narrativa cronológica, clara, objetiva.", - "Pedidos (Inicial)": "Certos, determinados, líquidos. Cumule (Art. 327).", "Custas (Inicial)": "Peça juntada guia OU JG.", - "Valor da Causa (Inicial)": "Benefício econômico (Art. 292).", "Preliminares (Contestação)": "Art. 337 antes mérito.", - "Impugnação Específica (Contestação)": "Conteste cada fato (Art. 341).", "Mérito (Contestação)": "Sua versão + fundamento improcedência.", - "Pedidos (Contestação)": "Acolher prelim./improcedência total + custas/honorários.", "Pressupostos Recursais": "Cabimento, tempestiv., preparo, reg. formal, interesse.", - "Dialeticidade (Recurso)": "Impugne fundamentos decisão (Art. 1010 II/III Apel.).", "Delimitação da Matéria (Recurso)": "Indique parte recorrida.", - "Fumus Boni Iuris": "Plausibilidade direito (provas/argumentos).", "Periculum in Mora": "Risco dano irreparável/difícil reparação.", - "Reversibilidade": "Argumente reversibilidade (Art. 300, §3º).", + "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"Preprocess err: {e}"); return f"❌ Err preprocess: {e}" + 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 = "", "", "" + 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" + 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{i+1} OCR try.") - try: img = page.to_image(resolution=300).original; page_ocr_text = pytesseract.image_to_string(preprocess_image_for_ocr(img), lang='por') - except Exception as ocr_setup_e: logger.error(f"Tesseract/Lang error? {ocr_setup_e}"); page_ocr_text = None # Handle potential Tesseract setup issues - if page_ocr_text and page_ocr_text.strip(): page_full_text += page_ocr_text + "\n"; ocr_text += page_ocr_text + "\n" - else: page_full_text += f"[OCR fail p{i+1}]\n"; ocr_text += f"[OCR fail p{i+1}]\n" + 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("Combined PDF text."); return full_text - else: logger.warning("No PDF text."); return "⚠️ No text extracted." + + 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"PDF err: {e}") + logger.error(f"Erro geral ao processar o PDF: {str(e)}") try: - logger.info("PyPDF2 fallback.") - fb_text = ""; reader_args = file_path_or_obj; - if hasattr(reader_args, 'seek'): reader_args.seek(0) - reader = PyPDF2.PdfReader(reader_args) - for page in reader.pages: fb_text += page.extract_text() or "" - if fb_text.strip(): logger.info("Fallback ok."); return fb_text - else: return f"❌ PDF err & fallback fail: {e}" - except Exception as fb_e: logger.error(f"Fallback err: {fb_e}"); return f"❌ Critical PDF err: {e}" - -def extract_text_from_docx(f): - try: d=docx.Document(f); t="\n".join([p.text for p in d.paragraphs]); return t if t.strip() else "⚠️ DOCX empty." - except Exception as e: logger.error(f"DOCX err: {e}"); return f"❌ DOCX error: {e}" - -def extract_text_from_txt(f): + 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: - c = None - if isinstance(f, str): - with open(f, 'rb') as file: c = file.read() - elif hasattr(f, 'read'): c = f.read(); [getattr(f, 'seek', lambda x: None)(0)] - else: return "❌ Invalid TXT." - for enc in ['utf-8', 'latin-1', 'cp1252']: - try: dec = c.decode(enc); logger.info(f"TXT decoded {enc}."); return dec if dec.strip() else "⚠️ TXT empty." - except UnicodeDecodeError: continue - logger.error("Cannot decode TXT."); return "❌ Cannot decode TXT." - except Exception as e: logger.error(f"TXT err: {e}"); return f"❌ TXT error: {e}" - -def process_file(f): - if f is None: return "⚠️ No file." + 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: - fp=getattr(f,'name',None); fn=os.path.basename(fp).lower() if fp else None - if not fp or not fn: return "❌ No file info." - logger.info(f"Processing: {fn}") + 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: - fs=os.path.getsize(fp) - if fs > 15*1024*1024: return "⚠️ File >15MB." - except OSError as se: logger.warning(f"Size err: {se}") - if fn.endswith('.pdf'): return extract_text_from_pdf(fp) - if fn.endswith('.docx'): return extract_text_from_docx(fp) - if fn.endswith('.txt'): return extract_text_from_txt(fp) - return "⚠️ Format? (PDF,DOCX,TXT)." - except Exception as e: logger.error(f"File err: {e}", exc_info=True); return f"❌ File error: {e}" + 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): - cfg = CONFIG_OAB if use_oab_config else CONFIG - payload = {"contents": [{"parts": [{"text": prompt}]}], "generationConfig": {"temperature": cfg["temperatura"], "maxOutputTokens": cfg["max_tokens"], "topP": 0.95}} - hdrs = {"Content-Type": "application/json"} - logger.info(f"API: {len(prompt)}c. T:{cfg['temperatura']}, MaxT:{cfg['max_tokens']}") - for att in range(CONFIG["max_retry_attempts"]): + 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: - with rate_limiter: resp = requests.post(GEMINI_API_URL, headers=hdrs, json=payload, timeout=CONFIG["timeout"]) - if resp.status_code==429: - wait=int(resp.headers.get("Retry-After", CONFIG["retry_delay"]*(att+1))); logger.warning(f"429. Wait {wait}s..."); - if att < CONFIG["max_retry_attempts"]-1: time.sleep(wait); continue - else: return "❌ 429 Error." - if resp.status_code!=200: - try: msg=resp.json().get("error",{}).get("message","Unknown") - except: msg=resp.text[:100] - logger.error(f"API Err {resp.status_code}: {msg}"); - if att < CONFIG["max_retry_attempts"]-1: time.sleep(CONFIG["retry_delay"]*(att+1)); continue - return f"❌ API Err {resp.status_code}: {msg}" - data = resp.json() - if not data.get("candidates"): reason=data.get("promptFeedback",{}).get("blockReason"); logger.error(f"No candidates. Block: {reason}"); return f"❌ Blocked: {reason}." if reason else "❌ Invalid (no candidates)." - cand=data["candidates"][0]; reason=cand.get("finishReason") - if reason not in [None,"STOP","MAX_TOKENS"]: - logger.error(f"Gen stop: {reason}"); cats=[r['category'] for r in cand.get("safetyRatings",[]) if r['probability'] not in ['NEGLIGIBLE','LOW']]; - return f"❌ Safety block: {reason} ({','.join(cats)})." if cats else f"❌ Gen stop: {reason}." - if not cand.get("content",{}).get("parts"): logger.error("Bad structure."); if att < CONFIG["max_retry_attempts"]-1: time.sleep(CONFIG["retry_delay"]*(att+1)); continue; return "❌ Invalid structure." - txt=cand["content"]["parts"][0]["text"].strip(); logger.info(f"API ok. Len: {len(txt)}."); return txt - except requests.Timeout: logger.warning(f"Timeout {att+1}."); if att1 else pc.strip(); sfx="\nNestes termos pede deferimento"+partes[1] if len(partes)>1 else "" - pc=pfx+"\nRequer, ainda, a juntada da guia de custas devidamente recolhida."+sfx - if "audiência de conciliação" not in pc.lower() and "audiência de mediação" not in pc.lower(): - partes=pc.split("Nestes termos pede deferimento"); pfx=partes[0].strip() if len(partes)>1 else pc.strip(); sfx="\nNestes termos pede deferimento"+partes[1] if len(partes)>1 else "" - pc=pfx+"\nOpta o Autor pela realização da audiência de conciliação ou mediação (Art. 319, VII, CPC)."+sfx - linhas=[l.strip() for l in pc.strip().split('\n') if l.strip()] - fechamento=["\nLocal, data.","ADVOGADO(A)","OAB/..."] - while linhas and any(t.lower() in linhas[-1].lower() for t in ["advogado","oab","local","data","pede deferimento","termos em que"]): - last_lower=linhas[-1].lower(); is_close=any(std.strip().lower() in last_lower for std in fechamento) or "pede deferimento" in last_lower or "termos em que" in last_lower - if not is_close and len(linhas[-1]) > 30: break - linhas.pop() - return "\n".join(linhas).strip()+"".join(fechamento) - -def formatar_correcao(t): - t=t.replace("### Análise Geral","## 📝 Análise Geral").replace("### Nota Final","## ⭐ Nota Final").replace("### Pontos Positivos","## ✅ Pontos Positivos").replace("### Pontos a Melhorar","## ⚠️ Pontos a Melhorar").replace("### Análise Específica dos Requisitos","## 🔎 Análise Específica") - linhas=t.split('\n'); fmt_linhas=[]; in_list=False - for l in linhas: - ls=l.strip() - if ls.startswith(("1.","2.","3.","4.","5.")): fmt_linhas.append(f"\n**{ls}**"); in_list=False - elif ls.startswith(("- ","* ")): fmt_linhas.append((" * " if in_list else "* ")+ls[2:]); in_list=True - elif ls.startswith(("a)","b)","c)","d)","e)")): fmt_linhas.append(f" * {ls}"); in_list=True - else: fmt_linhas.append(l); in_list=False - tf="\n".join(fmt_linhas); m=re.search(r"(\*\*Nota Final:?\*\*\s*)(\d{1,2}[,.]\d{1}|\d{1,2})",tf,re.I) - if m: nota=m.group(2).replace(",","."); tf=re.sub(r"(\*\*Nota Final:?\*\*\s*)(\d{1,2}[,.]\d{1}|\d{1,2})",f"\\1**{nota}/10**",tf,1,re.I) - return tf + 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"Correção GERAL IA: {tipo_peticao}"); start=time.time() - if not caso or not caso.strip(): return "⚠️ Caso?", "", "", "", "", "", "" - if not tipo_peticao or tipo_peticao not in TIPOS_DE_PETICAO: return "⚠️ Tipo?", "", "", "", "", "", "" + 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 = "" - if peticao_arquivo: peticao_conteudo = process_file(peticao_arquivo) - elif peticao_texto: peticao_conteudo = peticao_texto - if not peticao_conteudo or peticao_conteudo.startswith(("❌","⚠️")): return peticao_conteudo or "⚠️ Petição?", "", "", "", "", "", "" - if any(tr in peticao_conteudo.lower() for tr in ["escreva a", "faça a"]): return random.choice(["Nope!", "Tente você!"]), "", "", "", "", "", "" - cache_key=f"geral|{tipo_peticao}|{caso[:50]}|{peticao_conteudo[:50]}|{modo_treinamento}" - if cache_key in CORRECAO_CACHE: logger.info("Cache GERAL hit."); return tuple(CORRECAO_CACHE[cache_key].get(k,"") for k in ["analise_formatada","peticao_original","peticao_corrigida","resumo_diff","criticas_diff","avaliacao_criterios_ia","diff_html"]) - analise=chamar_api(estruturar_prompt_analise_geral(caso,peticao_conteudo,tipo_peticao,modo_treinamento),False) - if analise.startswith(("❌","⏱️")): return analise, peticao_conteudo, "", "", "", "", "" - analise_fmt=formatar_correcao(analise) - peticao_corr_bruta=chamar_api(estruturar_prompt_peticao_corrigida_geral(caso,peticao_conteudo,tipo_peticao),False) - peticao_corr_final = f"⚠️ Falha: {peticao_corr_bruta}" if peticao_corr_bruta.startswith(("❌","⏱️")) else pos_corrigir_peticao(peticao_corr_bruta, tipo_peticao) - aval_crit_md="" - html, resumo, crit = calcular_diff_interno(peticao_conteudo, peticao_corr_final if not peticao_corr_final.startswith("⚠️") else "") - hist = {"data":datetime.now().strftime("%Y%m%d %H%M"), "modo":"Geral IA", "tipo_peticao":tipo_peticao, "caso":caso, "peticao_original":peticao_conteudo, "analise_formatada":analise_fmt, "peticao_corrigida":peticao_corr_final, "resumo_diff":resumo,"criticas_diff":crit, "avaliacao_criterios_ia":aval_crit_md} - HISTORICO.append(hist); HISTORICO[:] = HISTORICO[-10:] - CORRECAO_CACHE[cache_key] = {**hist, "diff_html": html} - logger.info(f"Correção GERAL IA took {time.time()-start:.2f}s") - return analise_fmt, peticao_conteudo, peticao_corr_final, resumo, crit, aval_crit_md, html + 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." + 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_obj.name) - if full_text.startswith(("❌","⚠️")): return None, None, None, full_text - e, g, d = "", "", ""; current = None; lines = full_text.split('\n') + # 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: - ls=line.strip(); ll=ls.lower() - if ls.startswith("Enunciado"): current="e"; continue - if ls.startswith("Gabarito comentado"): current="g"; continue - if ls.startswith("Distribuição dos Pontos"): current="d"; continue - if ls.startswith("PADRÃO DE RESPOSTA – QUESTÃO"): current=None; break - if current=="e": e+=line+"\n" - elif current=="g": g+=line+"\n" - elif current=="d": d+=line+"\n" - e,g,d = e.strip(), g.strip(), d.strip() - if not all([e,g,d]): logger.warning(f"Parse fail. E:{bool(e)}, G:{bool(g)}, D:{bool(d)}"); return None, None, None, f"⚠️ Falha separar. Txt:\n{full_text[:300]}..." - logger.info("PDF OAB parsed."); return e, g, d, "✅ PDF OAB carregado." - except Exception as ex: logger.error(f"Parse OAB err: {ex}", exc_info=True); return None,None,None,f"❌ Erro PDF OAB: {ex}" + 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 estruturar_prompt_geracao_modelo_oab(enunciado, gabarito, distribuicao): - prompt = f"""**Tarefa:** Elabore a peça processual completa, **detalhada e bem fundamentada** para o **Enunciado**. Utilize o **Gabarito Comentado** como guia para argumentos e a **Distribuição dos Pontos** para estrutura/pedidos. Crie uma peça modelo que **desenvolva** os pontos OAB, usando os fatos. - - **Enunciado:**\n```\n{enunciado}\n``` - **Gabarito Comentado (DESENVOLVER):**\n```\n{gabarito}\n``` - **Distribuição dos Pontos (INCLUIR):**\n```\n{distribuicao}\n``` - - **Instruções Detalhadas:** - 1. **Qualificação:** Completa (319 II, 'XXX' se ausente). - 2. **Fatos:** Narre **detalhadamente** do Enunciado (datas, locais, indícios, necessidade). - 3. **Fundamentação Jurídica:** **Desenvolva** argumentos do Gabarito (Lei, binômio), **conectando aos fatos**. Cite artigos. - 4. **Tutela de Urgência:** Se aplicável, **argumente** requisitos (fumus/periculum) com fatos. - 5. **Pedidos:** **Todos** da Distribuição/Gabarito, claros/específicos (citação, provas, MP, JG, alimentos, conversão, custas/honorários). - 6. **Valor da Causa:** Atribua e justifique se possível. - 7. **Estrutura:** Lógica (Ender., Qualif., Fatos, Direito, Tutela, Pedidos, Valor, Fecho). - 8. **Linguagem:** Formal OAB. - 9. **Finalização:** "Local, data.", "ADVOGADO(A)", "OAB/...". Não se identifique. - 10. **Exclusividade:** Baseie-se **somente** nos textos fornecidos. - 11. **Output:** **Apenas** texto completo da peça. Sem comentários extras. - """ - return prompt def gerar_peticao_modelo_oab(enunciado, gabarito, distribuicao): - if not all([enunciado, gabarito, distribuicao]): return "⚠️ E, G, D necessários." - logger.info("Gerando modelo OAB...") + 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) - modelo = chamar_api(prompt, use_oab_config=True) - if modelo.startswith(("❌","⏱️")): logger.error(f"Erro gerar modelo OAB: {modelo}"); return f"❌ Falha: {modelo}" - logger.info("Modelo OAB gerado."); return pos_corrigir_peticao(modelo, "Petição Desconhecida") + # 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): - prompt = f"""**Tarefa:** Avalie **Peça do Usuário** vs padrão OAB (Gabarito, Distribuição) p/ **Enunciado**. - - **Enunciado:**\n```\n{enunciado}\n``` - **Gabarito (Esperado):**\n```\n{gabarito}\n``` - **Distribuição (Critérios):**\n```\n{distribuicao}\n``` - **Peça Usuário (Avaliar):**\n```\n{peticao_usuario}\n``` - - **Instruções:** - 1. **Avaliação Pontual:** Para **cada item** Distribuição, verifique se Usuário abordou conf. Gabarito. Atribua pontuação. - 2. **Justificativa:** Breve p/ cada item. - 3. **Feedback:** Erros/omissões vs padrão. - 4. **Peça Corrigida (Opcional):** Sugira trechos corrigidos p/ alinhar ao padrão. - 5. **Output Markdown:** + # 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 Conforme Padrão OAB + ### 📊 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 Distribuição]:** [Pontos Obtidos] / [Máximo] - * *Justificativa:* [...] - * ... (Todos itens) - **Pontuação Total Estimada:** [Soma Obtidos] / [Soma Máximos] - **Comentários e Pontos de Melhoria:** * [...] + + * **[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} + --- - ### ✨ Sugestões Correção - [Sugestões] + + **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** avaliação **exclusivamente** materiais OAB. - """ + +**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 all([peticao_usuario, enunciado, gabarito, distribuicao]): return "⚠️ Inputs OAB necessários." - logger.info("Corrigindo pela OAB...") + 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 = chamar_api(prompt, use_oab_config=True) - if correcao.startswith(("❌","⏱️")): logger.error(f"Erro corrigir OAB: {correcao}"); return f"❌ Falha OAB: {correcao}" - logger.info("Correção OAB concluída."); return correcao + # Use OAB specific config (lower temperature for consistency) + correcao_oab_bruta = chamar_api(prompt, use_oab_config=True) -def fornecer_dica_contextual(texto_peticao, tipo_peticao): - if not texto_peticao or not texto_peticao.strip(): return "Digite algo..." - dicas = []; txt_low = texto_peticao.lower() - for k, d in DICAS_OAB_MELHORADO.items(): - if k in ["Clareza e Concisão","Fundamentação Legal","Coerência Argumentativa","Revisão Ortográfica e Gramatical","Formatação"]: dicas.append(f"💡 **{k}:** {d}") - kws = {"Petição Inicial":["endereçamento","qualificação","fatos","pedidos","valor da causa","custas"], "Contestação":["preliminar","mérito","impugnação","improcedência"], "Ação de Alimentos Gravídicos":["gravídicos","nascituro","indícios","binômio","conversão"]} - if tipo_peticao in kws: - for kw in kws[tipo_peticao]: - if kw in txt_low: - for k,d in DICAS_OAB_MELHORADO.items(): - kl,tl=k.lower(),tipo_peticao.split()[0].lower() if tipo_peticao else "" - if f"({tl})" in kl or kw in kl: df=f"💡 **{k}:** {d}"; [dicas.append(df) if df not in dicas else None] - return "\n\n".join(dicas[:5]) if dicas else "💡 Sem dicas específicas." - -def calcular_diff_interno(t1, t2): - if not t1 or not t2 or t2.startswith(("⚠️","❌")): return "","","" - l1,l2=t1.splitlines(),t2.splitlines();diff=difflib.ndiff(l1,l2);html,crit,lr,la=[],[],0,0 - kw_rem=["invenção","identificação","erro crasso"]; kw_add=["custas","art. 319","art. 337","lei 11.804"] - for ln in diff: - txt=ln[2:];esc=escape(txt) - if ln.startswith(' '):html.append(f"
{esc}
") - elif ln.startswith('- '):lr+=1;html.append(f"
{esc}
");[crit.append(f"🔴 REM: {esc}") for k in kw_rem if k in txt.lower()] - elif ln.startswith('+ '):la+=1;html.append(f"
{esc}
");[crit.append(f"🟢 ADD: {esc}") for k in kw_add if k in txt.lower()] - res=f"**Diff:** Rem:{lr}, Add:{la}, Net:{la-lr}"; crit_t="### 🚨 Mudanças Chave:\n"+"\n".join(crit) if crit else "Nenhuma mudança chave." - css="" - diff_html=f"{css}
{''.join(html)}
" - return diff_html,res,crit_t + 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}" -def calcular_progresso(): - if not HISTORICO: return "📊 Progresso vazio." - notas,tipos,count=[],set(),0 - for e in HISTORICO: - if e.get("modo","Geral IA")=="Geral IA": count+=1;tipos.add(e["tipo_peticao"]); m=re.search(r"\*\*(\d{1,2}[,.]?\d?)/10\*\*",e.get("analise_formatada","")); - if m: try: notas.append(float(m.group(1).replace(",","."))) except: pass - media=sum(notas)/len(notas) if notas else 0 - return f"**📊 Progresso (IA):** Corrs:{count}, Tipos:{', '.join(sorted(list(tipos)))}, Média:{media:.1f}/10" + 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") -def mostrar_historico(): - if not HISTORICO: return "📜 Histórico vazio." - md="### 📜 Histórico (Últimas 10)\n\n---\n" - for i, e in enumerate(reversed(HISTORICO)): - modo=e.get("modo","Geral IA"); nota="N/A"; m=re.search(r"\*\*(\d{1,2}[,.]?\d?)/10\*\*",e.get("analise_formatada","")) - if m: nota=f"{m.group(1).replace(',','.')}/10" - md+=f"**{len(HISTORICO)-i}.** {e['data']} ({modo}) | **Tipo:** {e['tipo_peticao']} | **Nota IA:** {nota}\n**Caso:** _{e['caso'][:60]}..._\n---\n" - return md + return final_output.strip() -def salvar_progresso(caso, peticao, tipo_peticao): - logger.info("Salvando...") - try: - if not all([caso, peticao, tipo_peticao]): return "", "⚠️ Inputs?", None - d={"tipo_peticao":tipo_peticao, "caso":caso, "peticao":peticao, "ts":datetime.now().isoformat()}; js=json.dumps(d, ensure_ascii=False, indent=2); ts=datetime.now().strftime("%Y%m%d%H%M"); fname=f"prog_{ts}.json" - fpath=os.path.join(tempfile.gettempdir(),fname); - with open(fpath,'w',encoding='utf-8') as f: f.write(js) - logger.info(f"Salvo: {fpath}"); return js, f"✅ Salvo `{fname}`", fpath - except Exception as e: logger.error(f"Salvar err: {e}"); return "", f"❌ Erro: {e}", None - -def carregar_progresso(fobj): - logger.info("Carregando...") - if fobj is None: return "", "", "", "⚠️ Nenhum arq." - try: - with open(fobj.name,'r',encoding='utf-8') as f: d=json.load(f) - c,p,t=d.get("caso"), d.get("peticao"), d.get("tipo_peticao") - if not all([c,p,t]): return "", "", "", "❌ Arq inválido." - if t not in TIPOS_DE_PETICAO: return c,p,"",f"⚠️ Tipo '{t}' inválido." - logger.info("Carregado."); return t, c, p, "✅ Carregado!" - except Exception as e: logger.error(f"Carregar err: {e}"); return "", "", "", f"❌ Erro: {e}" -def gerar_caso_ficticio(tipo_peticao="Petição Inicial"): - if not tipo_peticao: return "Erro: Tipo?" - logger.info(f"Gerando caso: {tipo_peticao}") - prompt=estruturar_prompt_geracao_caso(tipo_peticao); caso=chamar_api(prompt,False) - if caso.startswith(("❌","⏱️")): return f"Falha:\n{caso}" - final="**Situação-Problema:** Na qualidade de advogado(a) de"; - if final.lower() not in caso.lower(): nomes=re.findall(r"[A-Z][a-z]+(?:\s[A-Z][a-z]+)+",caso); cli=nomes[0] if nomes else "[Cliente]"; caso+=f"\n\n{final} {cli}, elabore a peça cabível ({tipo_peticao})." - logger.info("Caso gerado."); return caso +# --- Funções Auxiliares (Diff, Histórico, Salvar/Carregar, Gerar Caso Fictício, Dicas, Export) --- +# --- Export Functions --- def export_to_txt(content, filename_prefix): - if not content: return None, "⚠️ Conteúdo vazio para exportar." + """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: - ts = datetime.now().strftime("%Y%m%d_%H%M%S") - filename = f"{filename_prefix}_{ts}.txt" - temp_dir = tempfile.gettempdir() - file_path = os.path.join(temp_dir, filename) - with open(file_path, 'w', encoding='utf-8') as f: + 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"Exportado para TXT: {file_path}") - return file_path, f"✅ Exportado para {filename}" + + logger.info(f"Conteúdo exportado para TXT: {filepath}") + return filepath # Return path for Gradio download except Exception as e: - logger.error(f"Erro exportar TXT: {e}") - return None, f"❌ Erro TXT: {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): - if not content: return None, "⚠️ Conteúdo vazio para exportar." + """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: - ts = datetime.now().strftime("%Y%m%d_%H%M%S") - filename = f"{filename_prefix}_{ts}.pdf" - temp_dir = tempfile.gettempdir() - file_path = os.path.join(temp_dir, filename) + timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') + filename = f"{filename_prefix}_{timestamp}.pdf" + filepath = os.path.join(tempfile.gettempdir(), filename) - pdf = FPDF() + # Ensure content is string + if not isinstance(content, str): + content = str(content) + + pdf = PDF() + pdf.alias_nb_pages() pdf.add_page() - # Use a font that supports UTF-8 characters, handle potential missing font file - try: - # Try adding DejaVu, assuming the .ttf file is accessible - # Download from: https://dejavu-fonts.github.io/ - # Place DejaVuSans.ttf in the same directory or provide the full path - pdf.add_font('DejaVu', '', 'DejaVuSans.ttf', uni=True) - pdf.set_font('DejaVu', size=11) - except RuntimeError: - logger.warning("Fonte DejaVuSans.ttf não encontrada. Usando Arial (pode ter problemas com caracteres especiais).") - pdf.set_font('Arial', size=11) - - # Use multi_cell for automatic line breaks and UTF-8 handling - # Encode to latin-1 with error replacement if using standard fonts without proper UTF-8 support - # If using DejaVu, encoding might not be strictly necessary but good practice + 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: - # Use write for better Markdown basic handling (bold/italic if font supports) - pdf.write(h=5, txt=content) # h is line height - except UnicodeEncodeError: - # Fallback if write fails with current font - pdf.multi_cell(0, 5, txt=content.encode('latin-1', 'replace').decode('latin-1')) + # 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:

{escape(texto_corrigido)}
", f"Adicionadas: {len(corr_lines)}", "Texto corrigido adicionado." + if not corr_lines: + return f"

Texto corrigido vazio. Texto original removido:

{escape(texto_original)}
", f"Removidas: {len(orig_lines)}", "Texto corrigido está vazio." + + + # Use SequenceMatcher for potentially better performance on large files? ndiff is fine for moderate size. + diff = difflib.ndiff(orig_lines, corr_lines) + + resultado_diff_html = [] + criticas = [] + l_rem = 0 + l_add = 0 + l_igual = 0 + + # Keywords to highlight in diff summary (adjust as needed) + kw_rem_critico = ["invenção", "identificação", "erro crasso", "não fundamentado", "pedido inepto"] + kw_add_importante = ["custas", "art. 319", "art. 337", "lei 11.804", "tutela provisória", "preliminar", "impugnação específica", "honorários"] + for linha in diff: + prefix = linha[0] + texto = linha[2:] + texto_escaped = escape(texto) # Escape HTML special chars - pdf.output(file_path) - logger.info(f"Exportado para PDF: {file_path}") - return file_path, f"✅ Exportado para {filename}" + if prefix == ' ': + resultado_diff_html.append(f"
{texto_escaped}
") + l_igual += 1 + elif prefix == '-': + l_rem += 1 + resultado_diff_html.append(f"
{texto_escaped}
") + # Check for critical keywords in removed lines + if any(kw in texto.lower() for kw in kw_rem_critico): + criticas.append(f"🔴 REMOVIDO (Crítico): {texto_escaped[:100]}{'...' if len(texto_escaped)>100 else ''}") + elif prefix == '+': + l_add += 1 + resultado_diff_html.append(f"
{texto_escaped}
") + # Check for important keywords in added lines + if any(kw in texto.lower() for kw in kw_add_importante): + criticas.append(f"🟢 ADICIONADO (Importante): {texto_escaped[:100]}{'...' if len(texto_escaped)>100 else ''}") + elif prefix == '?': + # Ignore hint lines from ndiff, or display them differently if desired + # resultado_diff_html.append(f"
{texto_escaped}
") + pass + + # --- Calculate similarity and generate summary --- + # Similarity Ratio + # Need SequenceMatcher for ratio, ndiff doesn't provide it directly + matcher = difflib.SequenceMatcher(None, texto_original, texto_corrigido) + similarity_ratio = matcher.ratio() + + # Summary Text + resumo = (f"**📊 Resumo das Mudanças:**\n" + f"* Similaridade: {similarity_ratio:.1%}\n" + f"* Linhas Iguais: {l_igual}\n" + f"* Linhas Removidas: {l_rem}\n" + f"* Linhas Adicionadas: {l_add}\n" + f"* **Saldo:** {l_add - l_rem:+}") # Show sign explicitly + + # Critical Changes Text + crit_txt = "### 🔑 Mudanças Chave Detectadas:\n" + "\n".join([f"* {c}" for c in criticas]) if criticas else "Nenhuma mudança chave (baseada em keywords) detectada." + + # --- CSS for HTML Output --- + # (Improved styling slightly) + css = """ + +""" + html_content = ''.join(resultado_diff_html) + if not html_content: + html_content = "

Nenhuma diferença visual a ser exibida.

" + + html_output = f"{css}
{html_content}
" + + return html_output, resumo, crit_txt + +# --- History, Progress, Save/Load (Minor improvements/logging) --- +def calcular_progresso(): + if not HISTORICO: return "📊 Progresso vazio. Nenhuma correção no histórico ainda." + + # Separate stats for IA and OAB modes if needed, focus on IA for now + notas_geral = [] + tipos_geral = set() + count_geral = 0 + + for entry in HISTORICO: + # Ensure entry has the expected keys + if entry.get("modo") == "Geral IA" and "analise_formatada" in entry and "tipo_peticao" in entry: + count_geral += 1 + tipos_geral.add(entry["tipo_peticao"]) + # More robust Nota extraction + nota_pattern = r"\*\*?(\d{1,2}[,.]\d?)\s*/\s*10\*\*?" + match = re.search(nota_pattern, entry["analise_formatada"]) + if match: + try: + nota = float(match.group(1).replace(",", ".")) + notas_geral.append(nota) + except (ValueError, TypeError): + logger.warning(f"Não foi possível converter nota '{match.group(1)}' para float no histórico.") + # else: logger.debug("Nota não encontrada no formato esperado na entrada do histórico.") # Optional debug log + + + if count_geral == 0: + return "📊 Nenhuma correção no modo 'Geral IA' encontrada no histórico." + + media_geral = sum(notas_geral) / len(notas_geral) if notas_geral else 0 + tipos_str = ", ".join(sorted(list(tipos_geral))) if tipos_geral else "Nenhum" + + prog_txt = (f"**📊 Progresso (Modo Geral IA):**\n" + f"* **Correções Realizadas:** {count_geral}\n" + f"* **Tipos de Peça Corrigidos:** {tipos_str}\n" + f"* **Média das Notas:** {media_geral:.1f}/10 (Baseado em {len(notas_geral)} notas encontradas)") + return prog_txt + +def mostrar_historico(): + if not HISTORICO: return "📜 Histórico vazio." + + hist_md = "### 📜 Histórico Recente (Últimas 10 Correções)\n\n---\n" + # Iterate reversed HISTORICO to show newest first + for i, entry in enumerate(reversed(HISTORICO)): + entry_index = len(HISTORICO) - i + # Safely get data with defaults + modo = entry.get("modo", "Desconhecido") + data_hora = entry.get("data", "Data Indisponível") + tipo = entry.get("tipo_peticao", "Tipo Indisponível") + caso_preview = entry.get("caso", "")[:80].strip() + "..." if entry.get("caso") else "[Caso não disponível]" # Truncate case preview + + nota_str = "N/A" + analise_formatada = entry.get("analise_formatada", "") + if analise_formatada: + # Use the same robust regex as in calcular_progresso + nota_pattern = r"\*\*?(\d{1,2}[,.]\d?)\s*/\s*10\*\*?" + match = re.search(nota_pattern, analise_formatada) + if match: + nota_num_str = match.group(1).replace(',', '.') + nota_str = f"{nota_num_str}/10" + # else: logger.debug(f"Nota não encontrada na entrada {entry_index} do histórico.") # Optional debug + + hist_md += f"**{entry_index}. {data_hora} ({modo})**\n" + hist_md += f"* **Tipo:** {tipo}\n" + # Only show Nota if it's from Geral IA mode + if modo == "Geral IA": + hist_md += f"* **Nota (IA):** {nota_str}\n" + hist_md += f"* **Caso:** _{caso_preview}_\n" + hist_md += "---\n" # Separator + + return hist_md + +def salvar_progresso(caso, peticao, tipo_peticao): + logger.info("Tentando salvar inputs atuais...") + # Basic validation + if not caso or not caso.strip(): return "", "⚠️ Caso está vazio. Não é possível salvar.", None + if not peticao or not peticao.strip(): return "", "⚠️ Petição está vazia. Não é possível salvar.", None + if not tipo_peticao or tipo_peticao not in TIPOS_DE_PETICAO: return "", f"⚠️ Tipo de peça '{tipo_peticao}' é inválido. Não é possível salvar.", None + + try: + dados = { + "tipo_peticao": tipo_peticao, + "caso": caso, + "peticao": peticao, # Save the original user petition text + "timestamp_salvo": datetime.now().isoformat() # Add timestamp + } + # Ensure JSON is human-readable + json_output = json.dumps(dados, ensure_ascii=False, indent=4) + + # Create a unique filename + ts_filename = datetime.now().strftime("%Y%m%d_%H%M%S") + # Sanitize tipo_peticao for filename + tipo_peticao_sanitized = re.sub(r'\W+', '_', tipo_peticao).lower() + filename = f"oab_inputs_{tipo_peticao_sanitized}_{ts_filename}.json" + # Use tempfile directory for safety + filepath = os.path.join(tempfile.gettempdir(), filename) + + with open(filepath, 'w', encoding='utf-8') as f: + f.write(json_output) + + logger.info(f"Inputs salvos com sucesso em: {filepath}") + # Provide user feedback including the filename + msg = f"✅ Inputs salvos! Você pode baixar o arquivo `{filename}`." + # Return values expected by Gradio: (hidden_data, status_message, file_output_component_value) + return json_output, msg, filepath # filepath makes the gr.File component offer the download + + except TypeError as te: + logger.error(f"Erro de tipo ao serializar para JSON: {str(te)}", exc_info=True) + return "", f"❌ Erro de tipo ao salvar (serialização JSON): {str(te)}", None + except IOError as ioe: + logger.error(f"Erro de I/O ao salvar arquivo: {str(ioe)}", exc_info=True) + return "", f"❌ Erro de I/O ao salvar arquivo: {str(ioe)}", None except Exception as e: - logger.error(f"Erro exportar PDF: {e}", exc_info=True) - return None, f"❌ Erro PDF: {e}" + logger.error(f"Erro inesperado ao salvar inputs: {str(e)}", exc_info=True) + return "", f"❌ Erro inesperado ao salvar: {str(e)}", None + +def carregar_progresso(arquivo_entrada): + logger.info("Tentando carregar inputs de arquivo...") + if arquivo_entrada is None: + return "", "", "", "⚠️ Nenhum arquivo selecionado para carregar." + + file_path = getattr(arquivo_entrada, 'name', None) + original_filename = getattr(arquivo_entrada, 'orig_name', file_path) + logger.info(f"Carregando do arquivo: {original_filename} (path: {file_path})") + + + if not file_path: + return "", "", "", "❌ Erro interno: Caminho do arquivo não encontrado." + + try: + with open(file_path, 'r', encoding='utf-8') as f: + dados = json.load(f) + + # Validate loaded data structure + caso = dados.get("caso") + peticao = dados.get("peticao") # Load the 'peticao' field + tipo = dados.get("tipo_peticao") + timestamp_salvo = dados.get("timestamp_salvo", "N/A") + if not isinstance(caso, str) or not caso.strip(): + return "", "", "", f"❌ Arquivo '{original_filename}' inválido: campo 'caso' ausente, vazio ou não é texto." + if not isinstance(peticao, str) or not peticao.strip(): + return caso, "", tipo, f"⚠️ Arquivo '{original_filename}' carregado, mas campo 'peticao' ausente ou vazio." # Return case/type anyway + if not isinstance(tipo, str) or tipo not in TIPOS_DE_PETICAO: + return caso, peticao, "", f"⚠️ Tipo de peça '{tipo}' no arquivo '{original_filename}' não é mais válido ou está ausente. Verifique e selecione manualmente." + + logger.info(f"Inputs carregados com sucesso de '{original_filename}' (Salvo em: {timestamp_salvo}).") + # Return values for Gradio: (tipo_peticao, caso, peticao_texto, status_message) + return tipo, caso, peticao, f"✅ Inputs carregados de '{original_filename}'!" + + except FileNotFoundError: + logger.error(f"Erro ao carregar: Arquivo não encontrado em {file_path}") + return "", "", "", f"❌ Erro: Arquivo '{original_filename}' não encontrado no caminho esperado." + except json.JSONDecodeError: + logger.error(f"Erro ao carregar: Arquivo '{original_filename}' não contém JSON válido.") + return "", "", "", f"❌ Erro: Arquivo '{original_filename}' não é um JSON válido." + except KeyError as ke: + logger.error(f"Erro ao carregar: Campo obrigatório ausente no JSON '{original_filename}': {ke}") + return "", "", "", f"❌ Erro: Arquivo '{original_filename}' inválido (campo '{ke}' ausente)." + except Exception as e: + logger.error(f"Erro inesperado ao carregar de '{original_filename}': {str(e)}", exc_info=True) + return "", "", "", f"❌ Erro inesperado ao carregar '{original_filename}': {str(e)}" + + +# --- Case Generation and Contextual Tips (Minor improvements/logging) --- +def gerar_caso_ficticio(tipo_peticao="Petição Inicial"): + if not tipo_peticao or tipo_peticao not in TIPOS_DE_PETICAO: + logger.error(f"Tentativa de gerar caso com tipo inválido: {tipo_peticao}") + return f"⚠️ Erro: Tipo de peça '{tipo_peticao}' inválido. Selecione um tipo válido." + + logger.info(f"Iniciando geração de caso fictício para: {tipo_peticao}") + prompt = estruturar_prompt_geracao_caso(tipo_peticao) + processing_log = ["⏳ Enviando pedido de geração de caso para a IA..."] # Log for user + + caso_bruto = chamar_api(prompt, use_oab_config=False) # Use general config (more creative) + + if caso_bruto.startswith("❌") or caso_bruto.startswith("⏱️") or caso_bruto.startswith("⚠️"): + logger.error(f"Falha API ao gerar caso: {caso_bruto}") + return f"❌ **Falha ao gerar caso fictício pela IA:**\n{caso_bruto}" + + logger.info("Caso fictício (bruto) recebido da API.") + processing_log.append("✅ Caso fictício recebido.") + + # --- Post-processing the generated case --- + caso_final = caso_bruto.strip() + + # Ensure the "Situação-Problema" line is present and correctly formatted + situacao_problema_pattern = r"\*\*Situação-Problema:\*\*?\s*Na qualidade de advogado\(a\)\s*d[eo]\s*(.*?),\s*elabore a peça processual cabível\s*\(?" + re.escape(tipo_peticao) + r"\)?\s*\.?" + match = re.search(situacao_problema_pattern, caso_final, re.IGNORECASE | re.DOTALL) + + if not match: + logger.warning("Linha 'Situação-Problema' padrão não encontrada ou mal formatada no caso gerado. Tentando adicionar...") + processing_log.append("⚠️ Linha 'Situação-Problema' não encontrada, adicionando...") + # Try to extract a client name (heuristic: first capitalized name phrase) + nomes_match = re.findall(r"\b([A-Z][a-z]+(?:\s+[A-Z][a-z]+)+)\b", caso_final) + cliente = nomes_match[0] if nomes_match else "[Nome do Cliente]" + # Append the standard line + caso_final += f"\n\n**Situação-Problema:** Na qualidade de advogado(a) de {cliente}, elabore a peça processual cabível ({tipo_peticao})." + else: + # Ensure consistent formatting if found + cliente_extraido = match.group(1).strip() + linha_padrao = f"**Situação-Problema:** Na qualidade de advogado(a) de {cliente_extraido}, elabore a peça processual cabível ({tipo_peticao})." + # Replace the found line with the standardized version + caso_final = re.sub(situacao_problema_pattern, linha_padrao, caso_final, flags=re.IGNORECASE | re.DOTALL) + processing_log.append("✅ Linha 'Situação-Problema' verificada/formatada.") + + logger.info("Caso fictício gerado e pós-processado.") + # Return combined processing log + case + # log_output_caso = "### 📋 Log da Geração do Caso:\n" + "\n".join([f"* {msg}" for msg in processing_log]) + "\n\n---\n" + # return log_output_caso + caso_final + # Or just return the case itself if log isn't desired here + return caso_final + + +def fornecer_dica_contextual(texto_peticao, tipo_peticao): + if not texto_peticao or not texto_peticao.strip(): + return "💡 Digite ou cole o texto da sua petição acima para receber dicas contextuais aqui..." + if not tipo_peticao or tipo_peticao not in TIPOS_DE_PETICAO: + return "⚠️ Selecione o **Tipo de Peça** acima para receber dicas mais relevantes." + + dicas_relevantes = [] + texto_lower = texto_peticao.lower() + + # 1. Add General Tips Always + general_tips_keys = ["Clareza e Concisão", "Fundamentação Legal", "Coerência Argumentativa", "Revisão Ortográfica e Gramatical", "Formatação"] + for key in general_tips_keys: + if key in DICAS_OAB_MELHORADO: + dicas_relevantes.append(f"💡 **{key}:** {DICAS_OAB_MELHORADO[key]}") + + # 2. Add Tips Based on Keywords Found in Text (related to any petition type) + # These keywords might trigger tips even if the selected type doesn't match perfectly yet + keywords_gerais = { + "endereçamento": ["Endereçamento (Inicial)"], + "qualifica": ["Qualificação (Inicial)"], # Matches qualificação, qualificar... + "fatos": ["Fatos (Inicial)"], + "fundamento": ["Fundamentação Legal", "Coerência Argumentativa"], # Matches fundamento, fundamentação... + "pedido": ["Pedidos (Inicial)", "Pedidos (Contestação)"], # Matches pedido, pedir... + "valor da causa": ["Valor da Causa (Inicial)"], + "custas": ["Custas (Inicial)", "Pressupostos Recursais"], + "gratuidade": ["Custas (Inicial)"], + "justiça gratuita": ["Custas (Inicial)"], + "prova": ["Indicação das Provas"], # Matches prova, provar... + "audiência": ["Opção pela Audiência"], + "citação": ["Pedido de Citação"], + "preliminar": ["Preliminares (Contestação)"], + "mérito": ["Mérito (Contestação)"], + "impugn": ["Impugnação Específica (Contestação)"], # Matches impugna, impugnação... + "improcedência": ["Pedidos (Contestação)"], + "reconvenção": ["Reconvenção"], + "réplica": ["Impugnação à Contestação"], + "embargos": ["Embargos de Declaração"], + "apelação": ["Recurso de Apelação", "Dialeticidade (Recurso)"], + "agravo": ["Agravo de Instrumento"], + "tutela": ["Petição de Tutela Provisória", "Fumus Boni Iuris", "Periculum in Mora", "Reversibilidade"], + "liminar": ["Petição de Tutela Provisória", "Periculum in Mora"], + "urgência": ["Petição de Tutela Provisória", "Periculum in Mora"], + "gravídicos": ["Ação de Alimentos Gravídicos"], + "paternidade": ["Ação de Alimentos Gravídicos"], + "recurso": ["Pressupostos Recursais", "Dialeticidade (Recurso)", "Delimitação da Matéria (Recurso)"], + "sentença": ["Recurso de Apelação"], + "decisão": ["Embargos de Declaração", "Agravo de Instrumento"], + } + + found_tips_keys = set(general_tips_keys) # Start with general tips already added + for keyword, dica_keys in keywords_gerais.items(): + if keyword in texto_lower: + for key in dica_keys: + if key in DICAS_OAB_MELHORADO and key not in found_tips_keys: + dicas_relevantes.append(f"🔑 **{key} (Detectado por '{keyword}'):** {DICAS_OAB_MELHORADO[key]}") + found_tips_keys.add(key) + + # 3. Add Tips Specific to the Selected Petition Type (if not already added) + tipo_simples = tipo_peticao.split()[0].lower() if tipo_peticao else "" + for chave, dica in DICAS_OAB_MELHORADO.items(): + # Check if the tip key contains the simple type name (e.g., "(Inicial)", "(Contestação)") + if f"({tipo_simples})" in chave.lower(): + if chave not in found_tips_keys: + dicas_relevantes.append(f"📌 **{chave} (Específico para {tipo_peticao}):** {dica}") + found_tips_keys.add(chave) + + if not dicas_relevantes: # Should have at least general tips if text is present + return "💡 Continue escrevendo ou selecione o Tipo de Peça para dicas mais específicas." + + # Limit the number of tips shown to avoid overwhelming the user + max_tips = 7 + output_dicas = "### ✨ Dicas Contextuais:\n\n" + "\n\n".join(dicas_relevantes[:max_tips]) + if len(dicas_relevantes) > max_tips: + output_dicas += "\n\n*(Mais dicas podem ser aplicáveis...)*" + + return output_dicas + +# --- Prompt Structure for Case Generation (Review/Refine if needed) --- def estruturar_prompt_geracao_caso(tipo_peticao="Petição Inicial"): - prompt = f"Crie caso fictício detalhado/realista p/ peça **{tipo_peticao}** (simulando OAB).\n**Instruções:**\n1.Complexidade adequada (15-25l).\n2.Dados essenciais(nomes,locais,datas,valores,conflito).\n3.Coerência.\n4.Foco em **{tipo_peticao}**.\n**Específico p/ {tipo_peticao}:**\n" - if tipo_peticao in ["Petição Inicial","Ação de Alimentos Gravídicos"]: prompt+="* Descreva violação direitos,1ª busca tutela. Elem. p/ danos. Qualif. partes. Temas:consumo,contrato,cobrança,despejo,família(alim.grav:indícios patern.,necess. mãe/nascituro)." - elif tipo_peticao=="Contestação": prompt+="* Resumo Inicial recebida. Versão RÉU. Elem. p/ Prelim(337),Impug.Esp,Def.Mérito. Qualif.réu." - else: prompt+="* Cenário cível c/ conflito claro." - prompt+=f'\n**Finalize:** "**Situação-Problema:** Na qualidade de advogado(a) de [Cliente], elabore a peça cabível ({tipo_peticao})."\n**Importante:** Temas comuns Civil/Proc.Civil.' + prompt = f""" +Crie um caso fictício **detalhado e realista** para um exercício prático de elaboração de uma peça processual do tipo **"{tipo_peticao}"**, simulando um cenário típico de 2ª Fase do Exame de Ordem (OAB) na área Cível ou Empresarial (evite Tributário ou Trabalho). + +**Instruções Gerais para o Caso:** +1. **Tamanho Ideal:** Entre 15 a 30 linhas de texto. +2. **Detalhes Essenciais:** Inclua nomes completos fictícios para as partes, datas relevantes (dia, mês, ano), locais (cidades/estados fictícios ou genéricos como "Cidade Alfa"), valores monetários (se aplicável), a natureza da relação jurídica entre as partes (contrato, relação de consumo, vizinhança, família, etc.), e o cerne do conflito/lide. +3. **Coerência Fática e Jurídica:** Os fatos devem ser lógicos, consistentes entre si, e levar naturalmente à necessidade de elaboração da peça do tipo "{tipo_peticao}". O cenário deve ter uma solução jurídica plausível no direito brasileiro. +4. **Foco na Peça Solicitada:** O problema deve claramente indicar que a peça a ser elaborada pelo advogado(a) do cliente é uma **{tipo_peticao}**. Evite ambiguidades que poderiam levar a outras peças. +5. **Informações Suficientes (e algumas omissões):** Forneça dados suficientes para a elaboração da peça principal (qualificação, fatos essenciais, fundamentos básicos), mas **omita alguns dados secundários** (ex: número exato de RG/CPF, e-mail específico, profissão exata se não relevante) para que o aluno precise usar "XXX" ou inferir/usar dados genéricos conforme padrão OAB. +6. **Evitar Complexidade Excessiva:** Foque em temas comuns do direito civil/processual civil/empresarial. Evite questões excessivamente nichadas, controversas ou que dependam de conhecimento jurisprudencial muito específico, a menos que seja essencial para o tipo de peça (ex: recurso). + +**Instruções Específicas para Gerar Caso para "{tipo_peticao}":** +""" + # Add specific scenario ideas based on the petition type + if tipo_peticao == "Petição Inicial": + prompt += "* **Cenário:** Descreva uma situação onde o cliente teve um direito violado (ex: inadimplemento contratual, acidente de trânsito com danos, cobrança indevida, problema com produto/serviço, despejo por falta de pagamento) e busca a tutela jurisdicional pela primeira vez. Inclua elementos que permitam ao aluno narrar os fatos, identificar o direito, os danos (materiais/morais, se houver) e formular os pedidos correspondentes." + elif tipo_peticao == "Ação de Alimentos Gravídicos": + prompt += "* **Cenário:** Apresente uma gestante (cliente) que teve um relacionamento com o suposto pai, que agora se recusa a ajudar nas despesas da gravidez. Inclua **detalhes que sirvam como indícios de paternidade** (fotos juntos, mensagens, testemunhas do relacionamento, reconhecimento inicial da gravidez pelo réu, etc.). Mencione as necessidades financeiras da gestante (despesas médicas, enxoval) e, se possível, a capacidade financeira do réu." + elif tipo_peticao == "Contestação": + prompt += "* **Cenário:** Informe que o cliente (réu) foi citado em uma ação (resuma brevemente os pedidos e fatos alegados pelo autor na petição inicial). Apresente a **versão dos fatos segundo o cliente (réu)**, que contradiga ou justifique a improcedência do pedido autoral. Inclua elementos que possibilitem a alegação de **matérias preliminares** (ex: incompetência do foro, ilegitimidade de parte, inépcia da inicial) e que permitam a **impugnação específica** dos fatos e a apresentação de defesa de mérito (direta ou indireta)." + elif tipo_peticao == "Recurso de Apelação": + prompt += "* **Cenário:** Descreva uma situação onde foi proferida uma **sentença desfavorável** ao cliente (autor ou réu). **Resuma os principais pontos da sentença** (o que o juiz decidiu e por quê, de forma concisa). Apresente os motivos pelos quais o cliente discorda da decisão, focando nos **erros de julgamento (error in judicando - má aplicação do direito, má interpretação da prova)** ou **erros de procedimento (error in procedendo - nulidades)** que justifiquem a reforma ou anulação da sentença pelo Tribunal." + elif tipo_peticao == "Agravo de Instrumento": + prompt += "* **Cenário:** Informe que foi proferida uma **decisão interlocutória desfavorável** ao cliente (ex: indeferimento de tutela provisória, rejeição de preliminar de ilegitimidade, decisão sobre produção de prova). **Descreva o conteúdo da decisão interlocutória**. Apresente os argumentos do cliente para **reformar ou invalidar essa decisão específica**, demonstrando o cabimento do Agravo (Art. 1.015 CPC ou tese de taxatividade mitigada) e, se aplicável, a urgência (risco de dano grave) para pedido de efeito suspensivo ou tutela antecipada recursal." + elif tipo_peticao == "Embargos de Declaração": + prompt += "* **Cenário:** Apresente uma **decisão judicial (sentença ou acórdão)** que contenha **omissão, contradição, obscuridade ou erro material**. **Descreva o trecho específico da decisão** que contém o vício e explique claramente qual é o vício (o que foi omitido, qual a contradição, o que está obscuro, qual o erro material). O objetivo do cliente é apenas sanar esse vício, sem rediscutir o mérito." + # TODO: Add specific scenario ideas for other petition types (Reconvenção, Impugnação à Contestação, Tutela Provisória standalone) + else: + prompt += f"* Crie um cenário jurídico cível ou empresarial claro e plausível que demande especificamente a elaboração de uma peça do tipo **{tipo_peticao}**." + + prompt += """ + +**Finalização Obrigatória:** +Conclua o texto do caso **exatamente** com a seguinte frase, substituindo "[Nome do Cliente]" pelo nome do cliente fictício criado no caso: +"**Situação-Problema:** Na qualidade de advogado(a) de [Nome do Cliente], elabore a peça processual cabível ({tipo_peticao})." + +**Exemplo (NÃO USAR ESTE, apenas como guia de formato):** +[...] Maria, residente na Cidade Beta, contratou os serviços de Buffet Estrela Ltda. para sua festa de casamento em 10/01/2023. O contrato previa serviço completo para 100 convidados por R$ 15.000,00, pagos integralmente por Maria. No dia da festa, o buffet entregou comida de péssima qualidade, em quantidade insuficiente, causando constrangimento. Maria notificou a empresa, que se recusou a devolver qualquer valor. Maria o(a) procura desolada. +**Situação-Problema:** Na qualidade de advogado(a) de Maria, elabore a peça processual cabível (Petição Inicial). +""" return prompt + +# --- Interface Gradio --- def interface_gradio(): - with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="orange")) as demo: + with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="orange"), title="Corretor Jurídico OAB Master") as demo: gr.Markdown("# ⚖️ Corretor Jurídico OAB Master ⚖️") - gr.Markdown("Use **Correção Geral (IA)** ou **Correção Padrão OAB**.") + gr.Markdown("Ferramenta para auxiliar na prática da 2ª Fase da OAB. Use a **Correção Geral (IA)** para feedback amplo ou a **Correção Baseada no Padrão OAB** para avaliação específica de provas anteriores.") + + # --- Hidden state for original petition (used in IA correction comparison) --- + peticao_original_state_geral = gr.State(value="") + with gr.Tabs() as main_tabs: + # ============================ + # TAB: Correção Geral (IA) + # ============================ with gr.TabItem("🤖 Correção Geral (IA)", id=0): with gr.Row(): - with gr.Column(scale=1): - gr.Markdown("### 1. Configs (Geral)") - tipo_peticao_input_geral=gr.Dropdown("Tipo Peça", choices=TIPOS_DE_PETICAO, value="Petição Inicial") - modo_treinamento_geral=gr.Checkbox("🎓 Treinamento", value=False) - with gr.Accordion("⚙️ IA", open=False): - temp_s=gr.Slider(0.0, 1.0, value=CONFIG["temperatura"], step=0.1, label="Criatividade") - tout_s=gr.Slider(30, 300, value=CONFIG["timeout"], step=10, label="Timeout(s)") - maxt_s=gr.Slider(2048, 8192, value=CONFIG["max_tokens"], step=256, label="MaxTokens") - def upd_cfg(t,o,m): CONFIG["temperatura"],CONFIG["timeout"],CONFIG["max_tokens"]=t,o,m; return "✅" - cfg_btn=gr.Button("💾 Salvar"); cfg_stat=gr.Textbox("Status", interactive=False) - cfg_btn.click(upd_cfg, [temp_s, tout_s, maxt_s], cfg_stat) - with gr.Accordion("📊 Prog/Hist", open=False): prog_out=gr.Markdown("..."); prog_btn=gr.Button("🔄 Prog"); hist_out=gr.Markdown("..."); hist_btn=gr.Button("🔄 Hist"); prog_btn.click(calcular_progresso,outputs=prog_out); hist_btn.click(mostrar_historico,outputs=hist_out) - with gr.Accordion("💾/📂 I/O", open=False): sv_btn=gr.Button("💾 Salvar"); ld_btn=gr.Button("📂 Carregar"); upld=gr.File(".json",ft=[".json"]); dwnld=gr.File("Baixar", interactive=False, visible=False) - with gr.Column(scale=3): - gr.Markdown("### 2. Caso e Peça") - with gr.Row(): caso_in=gr.Textbox("📋 Caso", lines=8); caso_btn=gr.Button("🎲",variant="secondary",scale=0) + # --- Coluna 1: Configurações e Inputs --- + with gr.Column(scale=2): + gr.Markdown("### 1. Insira o Caso e Sua Peça") + tipo_peticao_input_geral = gr.Dropdown(label="Tipo de Peça Prática", choices=TIPOS_DE_PETICAO, value="Petição Inicial", info="Selecione o tipo de peça que você elaborou.") + with gr.Row(): + caso_input_geral = gr.Textbox(label="📋 Caso / Enunciado", lines=8, placeholder="Cole aqui o caso ou enunciado do problema...") + caso_btn_geral = gr.Button("🎲 Gerar Caso", scale=0, icon="🎲", variant="secondary", tooltip="Gerar um caso fictício para o tipo de peça selecionado") + + gr.Markdown("**Sua Peça Prática:**") with gr.Tabs(): - with gr.TabItem("✏️ Texto"): pet_txt_in=gr.Textbox("Sua Peça", lines=12); dicas_out=gr.Markdown("*Dicas...*"); btn_corr=gr.Button("🚀 Corrigir (IA)", variant="primary") - with gr.TabItem("📎 Arquivo"): pet_f_in=gr.File("Arquivo", ft=[".pdf",".docx",".txt"]); prev_out=gr.Textbox("🔍 Prévia", lines=3, interactive=False); btn_f_corr=gr.Button("🚀 Corrigir Arq (IA)", variant="primary") - msg_geral=gr.Markdown("", visible=False) + with gr.TabItem("✏️ Digitar Texto"): + peticao_texto_input_geral = gr.Textbox(label=None, lines=12, placeholder="Digite ou cole aqui o texto completo da sua petição...") + dica_contextual_output_geral = gr.Markdown("*(Dicas aparecerão aqui enquanto você digita...)*") + # Store the text input in the hidden state when it changes + peticao_texto_input_geral.change(lambda x: x, inputs=peticao_texto_input_geral, outputs=peticao_original_state_geral) + + with gr.TabItem("📎 Anexar Arquivo"): + peticao_arquivo_input_geral = gr.File(label="Selecione .pdf, .docx ou .txt", file_types=[".pdf",".docx",".txt"], type="filepath") # Use filepath for easier handling + # Clear text input if file is uploaded, and vice-versa? Maybe not, allow both? Let process_file handle priority. + # Add preview? process_file already returns preview/error + arquivo_preview_output_geral = gr.Textbox(label="🔍 Status/Prévia Arquivo", lines=2, interactive=False) + # Store file content in hidden state? More complex - process_file needs to return it if successful. + # We'll handle getting content inside the correction function based on which input is provided. + + btn_corrigir_geral = gr.Button("🚀 Analisar e Corrigir Peça (IA)", variant="primary", scale=2) + + + # --- Coluna 2: Controles e Informações Adicionais --- + with gr.Column(scale=1): + gr.Markdown("### 2. Opções e Controles") + modo_treinamento_geral = gr.Checkbox(label="🎓 Modo Treinamento Detalhado", value=False, info="Fornece feedback mais aprofundado e exemplos de correção.") + with gr.Accordion("⚙️ Configurações Avançadas da IA", open=False): + temperatura_slider = gr.Slider(0.0, 1.0, value=CONFIG["temperatura"], step=0.1, label="Criatividade (Temperatura)", info="Valores menores = mais focado/determinístico; Valores maiores = mais criativo/variado.") + # timeout_slider = gr.Slider(30, 300, value=CONFIG["timeout"], step=10, label="Timeout API (s)", info="Tempo máximo de espera pela resposta da IA.") + max_tokens_slider = gr.Slider(1024, 8192, value=CONFIG["max_tokens"], step=256, label="Tamanho Máx. Resposta (Tokens)", info="Limite máximo para o tamanho da resposta da IA.") + def atualizar_config_geral(temp, tokens): # Removed timeout from direct config for now + CONFIG["temperatura"] = temp + # CONFIG["timeout"] = timeout # Timeout might be better set elsewhere or less user-facing + CONFIG["max_tokens"] = tokens + logger.info(f"Config IA Geral atualizada: Temp={temp}, MaxTokens={tokens}") + return f"✅ Configs IA salvas (Temp={temp}, MaxTokens={tokens})" + config_update_btn_geral = gr.Button("💾 Salvar Configs IA") + config_status_geral = gr.Textbox(label="Status Config", interactive=False) + config_update_btn_geral.click( + atualizar_config_geral, + [temperatura_slider, max_tokens_slider], # Removed timeout_slider + config_status_geral + ) + + with gr.Accordion("📊 Progresso e Histórico", open=False): + progresso_output = gr.Markdown("*(Clique para carregar)*") + progresso_btn = gr.Button("🔄 Ver Progresso (Notas IA)") + historico_output = gr.Markdown("*(Clique para carregar)*") + historico_btn = gr.Button("🔄 Ver Histórico (Últimas 10)") + progresso_btn.click(calcular_progresso, outputs=progresso_output) + historico_btn.click(mostrar_historico, outputs=historico_output) + + with gr.Accordion("💾/📂 Salvar / Carregar Inputs", open=False): + gr.Markdown("Salve os textos do Caso e da Sua Peça para continuar depois.") + salvar_btn_geral = gr.Button("💾 Salvar Caso e Peça Atual") + progresso_download_file_geral = gr.File(label="Baixar Arquivo Salvo (.json)", interactive=False) + # Use a separate status box for save/load messages + saveload_status_geral = gr.Textbox(label="Status Salvar/Carregar", interactive=False, scale=1) + gr.Markdown("Carregue um arquivo .json salvo anteriormente.") + arquivo_progresso_upload_geral = gr.File(label="Carregar Arquivo .json", file_types=[".json"], type="filepath") + carregar_btn_geral = gr.Button("📂 Carregar Caso e Peça") + + + # --- Area de Resultados da Correção IA --- + gr.Markdown("---") + gr.Markdown("### 3. Resultados da Correção (IA)") + processing_message_geral = gr.Markdown("", visible=False) # Para mensagens de "Processando..." + with gr.Tabs(): - with gr.TabItem("📊 Análise IA"): an_out=gr.Markdown(); with gr.Row(): exp_an_txt_btn=gr.Button("Export TXT"); exp_an_pdf_btn=gr.Button("Export PDF"); an_file_out=gr.File(label="Download Análise", interactive=False, visible=False) - with gr.TabItem("✨ Correção IA"): corr_out=gr.Textbox(lines=15, show_copy_button=True); with gr.Row(): exp_corr_txt_btn=gr.Button("Export TXT"); exp_corr_pdf_btn=gr.Button("Export PDF"); corr_file_out=gr.File(label="Download Correção", interactive=False, visible=False) - with gr.TabItem("⚖️ Diff"): with gr.Row(): with gr.Column(scale=1): res_out=gr.Markdown(); with gr.Column(scale=3): diff_out=gr.HTML(); crit_out=gr.Markdown() + with gr.TabItem("📊 Análise Detalhada (IA)"): + resultado_analise_output_geral = gr.Markdown("*A análise da IA aparecerá aqui...*") + with gr.Row(): + export_analise_txt_btn = gr.Button("Exportar Análise (TXT)", icon="📄") + export_analise_pdf_btn = gr.Button("Exportar Análise (PDF)", icon="📜") + download_analise_file = gr.File(label="Baixar Análise", interactive=False) + + with gr.TabItem("✨ Peça Corrigida (Sugestão IA)"): + resultado_peticao_corrigida_output_geral = gr.Textbox(label="Texto da Peça Corrigida/Aprimorada pela IA:", lines=20, show_copy_button=True, interactive=True) # Allow editing/copying + with gr.Row(): + export_correcao_txt_btn = gr.Button("Exportar Correção (TXT)", icon="📄") + export_correcao_pdf_btn = gr.Button("Exportar Correção (PDF)", icon="📜") + download_correcao_file = gr.File(label="Baixar Correção", interactive=False) + + with gr.TabItem("⚖️ Comparativo (Diff)"): + gr.Markdown("**Comparação entre sua peça original e a versão corrigida pela IA:**") + with gr.Row(): + with gr.Column(scale=1): + resumo_alteracoes_output_geral = gr.Markdown("*Resumo das alterações...*") + criticas_output_geral = gr.Markdown("*Mudanças chave...*") + with gr.Column(scale=3): + visualizacao_detalhada_output_geral = gr.HTML(value="

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("Forneça material oficial (PDF/texto) e sua peça.") + 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=2): - gr.Markdown("### 1. Material OAB"); - with gr.Tabs(): with gr.TabItem("📎 PDF"): oab_pdf=gr.File("Padrão OAB (.pdf)", ft=[".pdf"]); parse_st=gr.Textbox("Status PDF", interactive=False); with gr.TabItem("✏️ Manual"): oab_e=gr.Textbox("Enunciado", lines=5); oab_g=gr.Textbox("Gabarito", lines=8); oab_d=gr.Textbox("Distribuição", lines=8) - with gr.Column(scale=2): - gr.Markdown("### 2. Ações"); btn_mod=gr.Button("📝 Gerar Modelo OAB"); gr.Markdown("---"); pet_usr=gr.Textbox("Sua Peça p/ OAB", lines=10); btn_corr_oab=gr.Button("🎯 Corrigir pela OAB", variant="primary") - msg_oab=gr.Markdown("", visible=False) + # --- 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("📝 Modelo OAB"): mod_out=gr.Textbox(lines=20, show_copy_button=True); with gr.Row(): exp_mod_txt_btn=gr.Button("Export TXT"); exp_mod_pdf_btn=gr.Button("Export PDF"); mod_file_out=gr.File("Download Modelo", interactive=False, visible=False) - with gr.TabItem("🎯 Avaliação OAB"): corr_oab_out=gr.Markdown(); with gr.Row(): exp_avaoab_txt_btn=gr.Button("Export TXT"); exp_avaoab_pdf_btn=gr.Button("Export PDF"); avaoab_file_out=gr.File("Download Avaliação", interactive=False, visible=False) - clr_btn = gr.Button("🗑️ Limpar Tudo") - - caso_btn.click(gerar_caso_ficticio, [tipo_peticao_input_geral], [caso_in]) - pet_txt_in.change(fornecer_dica_contextual, [pet_txt_in, tipo_peticao_input_geral], dicas_out) - tipo_peticao_input_geral.change(fornecer_dica_contextual, [pet_txt_in, tipo_peticao_input_geral], dicas_out) - pet_f_in.change(process_file, [pet_f_in], prev_out) - - corr_inputs=[caso_in, tipo_peticao_input_geral, pet_txt_in, gr.State(None), modo_treinamento_geral] - corr_outputs=[an_out, gr.Textbox(visible=False), corr_out, res_out, crit_out, gr.Markdown(visible=False), diff_out] - btn_corr.click(lambda: gr.update(value="⏳ IA...", visible=True), None, msg_geral).then(corrigir_peticao_geral_ia, corr_inputs, corr_outputs).then(lambda: gr.update(visible=False), None, msg_geral) - corr_f_inputs=[caso_in, tipo_peticao_input_geral, gr.State(None), pet_f_in, modo_treinamento_geral] - btn_f_corr.click(lambda: gr.update(value="⏳ IA...", visible=True), None, msg_geral).then(corrigir_peticao_geral_ia, corr_f_inputs, corr_outputs).then(lambda: gr.update(visible=False), None, msg_geral) - - sv_btn.click(lambda: gr.update(value="⏳...", visible=True), None, msg_geral).then(salvar_progresso, [caso_in, pet_txt_in, tipo_peticao_input_geral], [gr.Textbox(visible=False), cfg_stat, dwnld]).then(lambda x: gr.update(value=x, visible=bool(x)), [dwnld], [dwnld]).then(lambda: gr.update(visible=False), None, msg_geral) - ld_btn.click(lambda: gr.update(value="⏳...", visible=True), None, msg_geral).then(carregar_progresso, [upld], [tipo_peticao_input_geral, caso_in, pet_txt_in, cfg_stat]).then(lambda: gr.update(visible=False), None, msg_geral) - - oab_pdf.change(parse_oab_pdf, [oab_pdf], [oab_e, oab_g, oab_d, parse_st]) - btn_mod.click(lambda: gr.update(value="⏳...", visible=True), None, msg_oab).then(gerar_peticao_modelo_oab, [oab_e, oab_g, oab_d], [mod_out]).then(lambda: gr.update(visible=False), None, msg_oab) - btn_corr_oab.click(lambda: gr.update(value="⏳...", visible=True), None, msg_oab).then(corrigir_pela_oab, [pet_usr, oab_e, oab_g, oab_d], [corr_oab_out]).then(lambda: gr.update(visible=False), None, msg_oab) - - def export_wrapper(func, content, prefix): status_msg = ""; file_path = None; if content: file_path, status_msg = func(content, prefix); else: status_msg = "⚠️ Nada para exportar."; return file_path, status_msg - exp_an_txt_btn.click(export_wrapper, [gr.State(export_to_txt), an_out, gr.State("analise_ia")], [an_file_out, cfg_stat]).then(lambda x: gr.update(visible=bool(x)), [an_file_out], [an_file_out]) - exp_an_pdf_btn.click(export_wrapper, [gr.State(export_to_pdf), an_out, gr.State("analise_ia")], [an_file_out, cfg_stat]).then(lambda x: gr.update(visible=bool(x)), [an_file_out], [an_file_out]) - exp_corr_txt_btn.click(export_wrapper, [gr.State(export_to_txt), corr_out, gr.State("correcao_ia")], [corr_file_out, cfg_stat]).then(lambda x: gr.update(visible=bool(x)), [corr_file_out], [corr_file_out]) - exp_corr_pdf_btn.click(export_wrapper, [gr.State(export_to_pdf), corr_out, gr.State("correcao_ia")], [corr_file_out, cfg_stat]).then(lambda x: gr.update(visible=bool(x)), [corr_file_out], [corr_file_out]) - exp_mod_txt_btn.click(export_wrapper, [gr.State(export_to_txt), mod_out, gr.State("modelo_oab")], [mod_file_out, cfg_stat]).then(lambda x: gr.update(visible=bool(x)), [mod_file_out], [mod_file_out]) - exp_mod_pdf_btn.click(export_wrapper, [gr.State(export_to_pdf), mod_out, gr.State("modelo_oab")], [mod_file_out, cfg_stat]).then(lambda x: gr.update(visible=bool(x)), [mod_file_out], [mod_file_out]) - exp_avaoab_txt_btn.click(export_wrapper, [gr.State(export_to_txt), corr_oab_out, gr.State("avaliacao_oab")], [avaoab_file_out, cfg_stat]).then(lambda x: gr.update(visible=bool(x)), [avaoab_file_out], [avaoab_file_out]) - exp_avaoab_pdf_btn.click(export_wrapper, [gr.State(export_to_pdf), corr_oab_out, gr.State("avaliacao_oab")], [avaoab_file_out, cfg_stat]).then(lambda x: gr.update(visible=bool(x)), [avaoab_file_out], [avaoab_file_out]) - - clr_btn.click(lambda: ["Petição Inicial",False,None,None,None,None,"","","","","","",None,None,None,None,"",None,None,None,"","","","",None,None,None,None], None, [ - tipo_peticao_input_geral,modo_treinamento_geral,caso_in,pet_txt_in,pet_f_in,prev_out,msg_geral,an_out,corr_out,res_out,crit_out,diff_out,dwnld,an_file_out,corr_file_out, - oab_pdf,parse_st,oab_e,oab_g,oab_d,pet_usr,msg_oab,mod_out,corr_oab_out,mod_file_out,avaoab_file_out]) + 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)", icon="📄") + export_modelo_pdf_btn = gr.Button("Exportar Modelo (PDF)", icon="📜") + 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)", icon="📄") + export_avaliacao_pdf_btn = gr.Button("Exportar Avaliação (PDF)", icon="📜") + 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 Tesseract path is set if needed, or handle OCR errors gracefully - # try: pytesseract.pytesseract.tesseract_cmd = '/usr/bin/tesseract' # Example for Linux - # except: logger.warning("Tesseract not configured/found. OCR might fail.") - app = interface_gradio() - app.launch() \ No newline at end of file + # 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}") \ No newline at end of file