CAPI / app.py
GabrielGD's picture
Update app.py
bf9a754 verified
Raw History Blame
67.7 kB
import gradio as gr
import requests
import os
import json
import docx
import io
import time
import tempfile
from dotenv import load_dotenv
from pdfplumber import open as pdfplumber_open
from PIL import Image
import pytesseract
try:
import cv2
import numpy as np
CV2_AVAILABLE = True
except ImportError:
CV2_AVAILABLE = False
print("Aviso: OpenCV (cv2) não encontrado. Pré-processamento de imagem para OCR será limitado.")
from datetime import datetime
from ratelimiter import RateLimiter
import difflib
from html import escape
import random
import logging
import traceback
import re
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - [%(funcName)s] - %(message)s')
logger = logging.getLogger(__name__)
load_dotenv()
GEMINI_API_KEY = os.getenv("GEMINI_API_KEY") or os.environ.get("GEMINI_API_KEY")
if not GEMINI_API_KEY:
raise ValueError("❌ A chave GEMINI_API_KEY não foi encontrada. Configure-a no arquivo .env ou como Secret no Hugging Face.")
GEMINI_API_URL = f"https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash-latest:generateContent?key={GEMINI_API_KEY}"
logger.info(f"Usando API URL: ...{GEMINI_API_URL[-50:]}")
CONFIG = {"max_retry_attempts": 3, "retry_delay": 5, "timeout": 120, "temperatura": 0.3, "max_tokens": 8192}
rate_limiter = RateLimiter(max_calls=20, 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"]
DICAS_OAB_MELHORADO = {
"Clareza e Concisão": "Seja direto e evite jargões excessivos.",
"Fundamentação Legal": "Sempre cite os artigos de lei, súmulas e jurisprudência pertinentes.",
"Revisão Ortográfica": "Erros de português prejudicam a credibilidade.",
"Endereçamento (Inicial)": "Verifique a competência (Art. 46 CPC / Art. 101 CDC).",
"Pedidos (Inicial)": "Certos, determinados e líquidos (se possível).",
"Preliminares (Contestação)": "Alegue todas as matérias do Art. 337 do CPC antes do mérito.",
"Impugnação Específica (Contestação)": "Conteste cada fato alegado (Art. 341, CPC)."
}
HISTORICO = []
CORRECAO_CACHE = {}
def preprocess_image_for_ocr(img_pil):
if not CV2_AVAILABLE:
logger.warning("OpenCV não disponível, usando conversão simples para OCR.")
return img_pil.convert('L')
try:
img_cv = cv2.cvtColor(np.array(img_pil), cv2.COLOR_RGB2GRAY)
_, thresh = cv2.threshold(img_cv, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
return Image.fromarray(thresh)
except Exception as e:
logger.error(f"Erro no pré-processamento da imagem com OpenCV: {str(e)}")
return img_pil.convert('L')
def extract_text_from_pdf(file_path_or_obj):
text = ""
ocr_text_parts = []
try:
logger.info(f"Abrindo PDF: {getattr(file_path_or_obj, 'name', file_path_or_obj)}")
pdf_reader = pdfplumber_open(file_path_or_obj)
logger.info(f"PDF com {len(pdf_reader.pages)} páginas.")
for i, page in enumerate(pdf_reader.pages):
page_text = page.extract_text(x_tolerance=2, y_tolerance=2)
if page_text and len(page_text.strip()) > 20:
text += page_text + "\n"
else:
logger.warning(f"Página {i+1} sem texto significativo extraído, tentando OCR.")
try:
img = page.to_image(resolution=200)
preprocessed_img = preprocess_image_for_ocr(img.original)
page_ocr_text = pytesseract.image_to_string(preprocessed_img, lang='por', config='--psm 6')
if page_ocr_text and page_ocr_text.strip():
ocr_text_parts.append(page_ocr_text)
else:
logger.warning(f"OCR retornou vazio para a página {i+1}.")
ocr_text_parts.append(f"[OCR Página {i+1}: Vazio ou falhou]")
except Exception as ocr_e:
logger.error(f"Erro no OCR da página {i+1}: {str(ocr_e)}")
ocr_text_parts.append(f"[Erro OCR Página {i+1}: {str(ocr_e)}]")
pdf_reader.close()
ocr_text = "\n".join(ocr_text_parts)
if text.strip() and len(text) > len(ocr_text) * 0.7:
logger.info(f"Usando texto extraído do PDF ({len(text)} chars).")
return text
elif ocr_text.strip():
logger.info(f"Usando texto do OCR do PDF ({len(ocr_text)} chars).")
return ocr_text
elif text.strip():
logger.info("Usando texto extraído curto como fallback.")
return text
else:
logger.warning("Não foi possível extrair texto significativo do PDF.")
return "⚠️ Não foi possível extrair texto do PDF (extração e OCR falharam)."
except Exception as e:
logger.error(f"Erro geral ao processar o PDF: {str(e)}", exc_info=True)
return f"❌ Erro crítico ao processar o PDF: {str(e)}"
def extract_text_from_docx(file_path_or_obj):
try:
doc = docx.Document(file_path_or_obj)
text = "\n".join([para.text for para in doc.paragraphs if para.text])
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)}", exc_info=True)
return f"❌ Erro ao processar o DOCX: {str(e)}"
def extract_text_from_txt(file_path_or_obj):
try:
content = b''
if isinstance(file_path_or_obj, str):
with open(file_path_or_obj, 'rb') as f:
content = f.read()
elif hasattr(file_path_or_obj, 'read'):
if hasattr(file_path_or_obj, 'seek') and callable(file_path_or_obj.seek):
try: file_path_or_obj.seek(0)
except Exception as seek_e: logger.warning(f"Não foi possível resetar ponteiro do arquivo: {seek_e}")
content = file_path_or_obj.read()
else:
return "❌ Tipo de entrada TXT inválido."
encodings = ['utf-8', 'latin-1', 'cp1252']
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.")
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)}", exc_info=True)
return f"❌ Erro ao processar o TXT: {str(e)}"
def process_file(file_obj):
if file_obj is None:
logger.warning("process_file chamado com file_obj=None")
return "⚠️ Nenhum arquivo enviado."
try:
file_path = getattr(file_obj, 'name', None)
if not file_path:
logger.error("Não foi possível obter o nome do arquivo (file_obj.name está ausente).")
return "❌ Erro: Não foi possível identificar o arquivo enviado."
filename = os.path.basename(file_path).lower()
logger.info(f"Processando arquivo: {filename}")
try:
file_size = os.path.getsize(file_path)
logger.info(f"Tamanho do arquivo: {file_size} bytes")
if file_size > 15 * 1024 * 1024:
logger.warning(f"Arquivo {filename} ({file_size} bytes) excede o limite de 15MB.")
return "⚠️ Arquivo muito grande (Máx 15MB)."
except OSError as size_e:
logger.warning(f"Não foi possível verificar o tamanho do arquivo: {size_e}")
if filename.endswith('.pdf'):
return extract_text_from_pdf(file_path)
elif filename.endswith('.docx'):
return extract_text_from_docx(file_path)
elif filename.endswith('.txt'):
return extract_text_from_txt(file_path)
else:
logger.warning(f"Formato de arquivo não suportado: {filename}")
return "⚠️ Formato de arquivo não suportado. Use PDF, DOCX ou TXT."
except Exception as e:
logger.error(f"Erro GERAL ao processar o arquivo '{getattr(file_obj, 'name', 'N/A')}': {str(e)}", exc_info=True)
return f"❌ Erro inesperado ao processar o arquivo: {str(e)}"
def get_instrucoes_gerais_avaliacao(modo_treinamento=False):
base = """
**Estrutura da Avaliação:**
1. **Análise Geral:** Visão geral concisa (2-3 linhas) sobre a adequação da peça ao caso e tipo, consistência e clareza.
2. **Nota Final:** 0 a 10 (uma casa decimal), com justificativa BREVE dos principais fatores.
3. **Pontos Positivos:** Liste pelo menos 2-3 aspectos bem executados, com justificativa (legal/técnica).
4. **Pontos a Melhorar:** Liste **TODOS** os problemas em ordem de gravidade (erros crassos primeiro). Para cada ponto:
* Descrição clara do erro/omissão.
* Fundamentação legal/doutrinária do porquê é um erro.
* **Impacto estimado na nota** (ex: -0.5, -1.0, -2.0).
* Sugestão de correção ou como deveria ter sido feito.
5. **Análise Específica dos Requisitos:** Avalie os itens chave da peça (listados abaixo para cada tipo).
**Critérios Fundamentais (Aplicáveis a Quase Todos):**
* **Coerência:** A peça responde ao caso/situação processual? Os argumentos são lógicos?
* **Fundamentação:** Usou a lei, doutrina e jurisprudência corretamente? Citou artigos relevantes?
* **Linguagem:** Clareza, objetividade, correção gramatical e formalidade adequada?
* **Pedidos:** São claros, certos, determinados e coerentes com a fundamentação?
* **NÃO INVENTAR FATOS:** Penalidade severa (-3.0 a -5.0) se criar fatos não presentes no caso.
* **NÃO SE IDENTIFICAR:** Nota ZERO se houver nome, OAB, CPF, etc., do aluno na peça (exceto nomes das partes do caso). Use "ADVOGADO(A)", "OAB/...".
"""
if modo_treinamento:
base += """
**Modo de Treinamento:**
* Para cada Ponto a Melhorar, detalhe AINDA MAIS:
a) Explicação aprofundada do erro e sua consequência prática.
b) Base legal/doutrinária específica.
c) **Exemplo de Redação Corrigida** para aquele trecho específico.
d) Dica prática relevante (use o banco de dicas se aplicável).
e) Possível estratégia alternativa.
* Destaque inconsistências entre a peça e o caso com citações diretas.
* Sugira estratégias jurídicas adicionais ou alternativas viáveis.
"""
return base
def estruturar_prompt_analise(caso, peticao, tipo_peticao, modo_treinamento=False):
instrucoes_gerais = get_instrucoes_gerais_avaliacao(modo_treinamento)
prompt_especifico = f"""
## Análise de Peça Processual: {tipo_peticao} (Critérios OAB Simulados)
**Caso/Contexto Fornecido:**
```
{caso}
```
**Peça do Aluno ({tipo_peticao}):**
```
{peticao}
```
{instrucoes_gerais}
**Requisitos Específicos para {tipo_peticao}:**
"""
if tipo_peticao == "Petição Inicial":
prompt_especifico += """
- Endereçamento e competência (Art. 319, I, CPC; leis especiais como CDC Art. 101, I).
- Qualificação completa das partes (Art. 319, II, CPC).
- Exposição clara e lógica dos fatos (causa de pedir remota).
- Fundamentação jurídica pertinente (causa de pedir próxima - Art. 319, III, CPC).
- Pedidos certos, determinados e líquidos (se possível) (Art. 319, IV, CPC).
- Valor da causa correto e justificado (Art. 291-292, 319, V, CPC).
- Indicação das provas pretendidas (Art. 319, VI, CPC).
- Opção pela audiência de conciliação/mediação (Art. 319, VII, CPC).
- Pedido de citação do réu (forma específica se necessário).
- Requerimento de juntada de documentos essenciais (procuração, custas/JG).
- Fechamento formal ("Nestes termos pede deferimento", Local, data, Advogado, OAB).
**Critérios Adicionais (Inicial):**
* Uso de "XXX" para dados ausentes. Penalizar invenção (-0.5 por ocorrência).
* Falta de pedido de juntada de custas/JG: -1.0 ponto.
* Erro em "Nestes termos, pede deferimento" (com vírgula): -0.2 ponto.
* Analisar foro (Art. 46 CPC vs. Art. 101 CDC): Escolha do foro do consumidor é válida se aplicável; não penalizar, apenas comentar se gerar dificuldades práticas.
"""
elif tipo_peticao == "Contestação":
prompt_especifico += """
- Endereçamento ao juízo correto onde tramita a ação.
- Qualificação completa do réu (se já não constante nos autos, referenciar).
- Indicação do processo (número).
- Tempestividade (menção implícita ou explícita de que está no prazo).
- **Preliminares (Art. 337, CPC):** Alegar TODAS as aplicáveis (inexistência/nulidade citação, incompetência, inépcia, perempção, litispendência, coisa julgada, conexão, incapacidade, convenção de arbitragem, ausência de legitimidade/interesse, falta de caução, indevida concessão de JG).
- **Impugnação Específica dos Fatos:** Rebater PONTO A PONTO os fatos alegados na inicial (Art. 341, CPC). A falta de impugnação específica torna o fato incontroverso!
- **Mérito:** Apresentar a versão do réu sobre os fatos; apresentar a defesa de mérito direta (negando direito do autor) e/ou indireta (fatos impeditivos, modificativos, extintivos do direito do autor - ex: pagamento, prescrição, compensação).
- Fundamentação jurídica da defesa.
- **Pedidos:** Requerer acolhimento das preliminares (com extinção ou remessa); NO MÉRITO, requerer a IMPROCEDÊNCIA TOTAL dos pedidos do autor; Requerer a condenação do autor nas custas e honorários advocatícios (Art. 85, CPC).
- Indicação das provas que pretende produzir.
- Fechamento formal.
**Critérios Adicionais (Contestação):**
* A não alegação de preliminar arguível (exceto incompetência absoluta, conexão, convenção de arbitragem) preclui? Avaliar impacto.
* Falha grave na impugnação específica: -2.0 a -4.0 pontos.
* Ausência de pedido de improcedência: -1.5 pontos.
"""
elif tipo_peticao == "Reconvenção":
prompt_especifico += """
- Apresentada NA MESMA PEÇA da Contestação (Art. 343, CPC).
- Endereçamento ao mesmo juízo.
- Indicação do processo.
- Qualificação completa do réu-reconvinte e do autor-reconvindo (podem ser referenciadas se já completas).
- **Conexão com a Ação Principal ou Fundamento de Defesa:** O pedido da reconvenção deve ser conexo (Art. 343, caput).
- **Estrutura Similar à Petição Inicial:** Fatos, Fundamentação, Pedidos específicos, Valor da causa, Provas.
- Pedido de intimação do autor-reconvindo para contestar a reconvenção (na pessoa de seu advogado - Art. 343, §1º).
- Fechamento formal (pode ser conjunto com a contestação).
**Critérios Adicionais (Reconvenção):**
* Ausência de conexão clara: -2.0 pontos. Falta de pedido/valor da causa/provas específicos: -1.0 por item. Apresentada em peça separada (erro formal grave): -3.0 pontos.
"""
elif tipo_peticao == "Impugnação à Contestação":
prompt_especifico += """
- Endereçamento ao juízo correto. Indicação do processo. Qualificação do autor (impugnante). Tempestividade (15 dias - Art. 350/351 CPC).
- **Resposta às Preliminares:** Rebater cada preliminar arguida.
- **Resposta a Fatos Impeditivos/Modificativos/Extintivos:** Manifestar-se sobre fatos novos (Art. 350).
- **Resposta a Documentos Novos:** Manifestar-se sobre documentos juntados (Art. 437 CPC).
- **Ratificação da Inicial:** Reiterar os termos e pedidos da inicial.
- **Especificação de Provas:** Indicar provas para contrapor defesa indireta/preliminares (Art. 350/351).
- Pedidos: Rejeição das preliminares, refutação da defesa, procedência dos pedidos da inicial. Fechamento formal.
**Critérios Adicionais (Impugnação):**
* Não responder a preliminar relevante: -1.5 pontos. Não se manifestar sobre fato novo: -2.0 pontos. Não ratificar a inicial: -0.5 pontos.
"""
elif tipo_peticao == "Embargos de Declaração":
prompt_especifico += """
- **Contra Decisão Específica:** Indicar claramente qual decisão está sendo embargada. Endereçamento ao PRÓPRIO juízo prolator. Indicação do processo. Qualificação das partes. Tempestividade (5 dias - Art. 1023 CPC).
- **Cabimento (Art. 1022, CPC):** Apontar **PRECISAMENTE** e **FUNDAMENTADAMENTE** a ocorrência de: Omissão, Contradição, Obscuridade, Erro Material.
- **NÃO PODE REJULGAR A CAUSA:** Objetivo é sanar vício, não modificar o mérito (salvo efeitos infringentes excepcionais).
- **Pedido:** Requerer o conhecimento e ACOLHIMENTO para sanar o vício (com ou sem efeitos infringentes, se for o caso). Fechamento formal.
**Critérios Adicionais (Embargos):**
* Usar para rediscutir mérito (erro grave): -4.0 pontos. Não apontar vício claramente: -3.0 pontos. Endereçar ao tribunal (erro): -1.0 ponto.
"""
elif tipo_peticao == "Recurso de Apelação":
prompt_especifico += """
- **Peça de Interposição:** Endereçamento ao juízo a quo. Indicação do processo. Qualificação das partes. Pressupostos (cabimento - Art. 1009; tempestividade - 15 dias Art. 1003, §5º; preparo/JG; regularidade). Pedido de recebimento e remessa ao Tribunal.
- **Razões Recursais:** Endereçamento ao TRIBUNAL competente. Exposição dos fatos e da sentença. **Fundamentos para Reforma/Anulação (Art. 1010, II e III):** Error in procedendo (anulação) ou Error in judicando (reforma). **Princípio da Dialeticidade:** Impugnar ESPECIFICAMENTE os fundamentos da sentença.
- **Pedidos:** Conhecimento; PROVIMENTO para ANULAR ou REFORMAR (especificar); inversão da sucumbência. Fechamento formal.
**Critérios Adicionais (Apelação):**
* Falta de interposição ou razões: Nota zero. Endereçamento incorreto das razões: -1.0. Ausência de dialeticidade: -3.0 a -5.0. Não pedir anulação/reforma: -1.5. Falta de preparo/JG: -2.0.
"""
elif tipo_peticao == "Agravo de Instrumento":
prompt_especifico += """
- **Peça Única:** Endereçamento direto ao TRIBUNAL. Indicação processo origem. Qualificação partes e advogados (com endereços - Art. 1016, IV). Cópia da decisão agravada (OBRIGATÓRIA - Art. 1017, I).
- **Cabimento (Art. 1015 ou taxatividade mitigada):** Demonstrar hipótese legal ou urgência (Tema 988 STJ). Tempestividade (15 dias). Preparo/JG.
- **Peças Obrigatórias (Art. 1017, I):** Decisão agravada, certidão de intimação, procurações. Peças Facultativas (Art. 1017, III).
- **Razões (Art. 1016, II e III):** Exposição do fato e direito; razões para reforma/invalidação da decisão interlocutória.
- **Pedido de Efeito Suspensivo ou Tutela Antecipada Recursal (Art. 1019, I):** Se houver risco, pedir fundamentadamente.
- **Pedidos Finais:** Conhecimento; (se aplicável) efeito suspensivo/tutela antecipada; PROVIMENTO para reformar/invalidar; comunicação ao juiz (Art. 1018 - opcional se eletrônico). Fechamento formal.
**Critérios Adicionais (Agravo):**
* Ausência de peça obrigatória: Inadmissibilidade (-4.0). Não demonstrar cabimento: -3.0. Endereçar ao juiz a quo: -1.0. Não indicar advogados: -0.5.
"""
elif tipo_peticao == "Petição de Tutela Provisória":
prompt_especifico += """
- **Tipo (Urgência ou Evidência):** Deixar claro. Antecedente ou incidental. Endereçamento (competente ou da causa). Qualificação partes. Indicação processo (se incidental).
- **Tutela de Urgência (Art. 300):** **Fumus Boni Iuris** (probabilidade do direito) E **Periculum in Mora** (perigo de dano/risco ao resultado útil). **Reversibilidade** (Art. 300, §3º - se possível).
- **Tutela de Evidência (Art. 311):** Demonstrar uma das hipóteses (I a IV). **NÃO precisa periculum in mora.**
- **Pedido Específico:** Indicar CLARAMENTE a medida liminar solicitada.
- **Se Antecedente (Art. 303 ou 305):** Indicar pedido final ou aditar/apresentar inicial no prazo. Valor da Causa.
- **Pedidos:** Concessão da tutela (liminar/justificação prévia); citação/intimação; (se antecedente) indicação da ação principal/aditamento. Fechamento formal.
**Critérios Adicionais (Tutela):**
* Não demonstrar Fumus/Periculum (Urgência): -3.0 a -5.0. Não enquadrar na Evidência: -3.0. Pedido genérico: -2.0. Confundir requisitos: -1.5.
"""
else:
prompt_especifico += """
- **Adequação ao Tipo:** A peça corresponde à estrutura e finalidade do tipo?
- **Clareza:** É possível entender a pretensão?
- **Fundamentação Mínima:** Tentativa de basear em lei ou fatos?
- **Pedido:** Existe um pedido?
"""
prompt_especifico += "\n\n**Instruções Finais:** Analise a peça do aluno estritamente com base no caso/contexto fornecido e nos critérios acima. Seja rigoroso como um examinador da OAB, mas forneça feedback construtivo e detalhado. Gere a resposta **exclusivamente** no formato solicitado."
return prompt_especifico
def estruturar_prompt_peticao_corrigida(caso, peticao_original, tipo_peticao):
prompt = f"""
**Caso/Contexto Fornecido:**
```
{caso}
```
**Peça Original do Aluno ({tipo_peticao}):**
```
{peticao_original}
```
**Tarefa:** Com base no caso e nos problemas da peça original, **gere uma versão corrigida e aprimorada** da peça ({tipo_peticao}). Siga **rigorosamente** as melhores práticas e requisitos formais/materiais para este tipo de peça no exame da OAB.
**Instruções Específicas para a Peça Corrigida ({tipo_peticao}):**
"""
if tipo_peticao == "Petição Inicial":
prompt += """
1. **Endereçamento:** Correto e fundamentado (Art. 46 CPC ou Art. 101 CDC se aplicável).
2. **Qualificação:** Completa (Art. 319, II), usando "XXX" para dados ausentes.
3. **Fatos:** Narre de forma cronológica, clara e fiel ao caso.
4. **Fundamentação:** Cite artigos pertinentes (CC, CDC, CPC) e súmulas/jurisprudência, se aplicável.
5. **Pedidos:** Claros, certos, determinados (quantificar danos se possível). Cumulados se cabível.
6. **Valor da Causa:** Correto (soma dos benefícios - Art. 292).
7. **Provas:** Indicar ("protesta provar o alegado por todos os meios...").
8. **Audiência:** Indicar opção (Art. 319, VII).
9. **Citação:** Pedido de citação (ex: "via postal, Art. 246, I, CPC").
10. **Custas:** Requerimento expresso de juntada da guia OU pedido de gratuidade (com pedido de declaração). **NUNCA OMITIR.**
11. **Fechamento:** "Nestes termos pede deferimento." (SEM vírgula). Seguido de "Local, data.", "ADVOGADO(A)", "OAB/...". **NUNCA identificar.**
"""
elif tipo_peticao == "Contestação":
prompt += """
1. **Endereçamento:** Ao juízo da ação.
2. **Identificação:** Número do processo, qualificação do réu.
3. **Tempestividade:** Mencionar ("A presente contestação é tempestiva...").
4. **Estrutura:** Síntese da Inicial; Preliminares (Art. 337, fundamentadas); Mérito (Impugnação Específica + Defesa de Mérito Direta/Indireta com fundamentos); Reconvenção (Se houver, ANTES dos pedidos da contestação).
5. **Pedidos da Contestação:** Acolhimento das preliminares (com consequência); **IMPROCEDÊNCIA TOTAL** dos pedidos do autor; Condenação em custas e honorários.
6. **Provas:** Indicar as provas do réu.
7. **Fechamento:** Padrão OAB.
"""
elif tipo_peticao == "Reconvenção":
prompt += """
**IMPORTANTE:** Gere a Reconvenção como parte da Contestação.
1. **Após a defesa de mérito da Contestação e ANTES dos pedidos da Contestação**, insira: "DA RECONVENÇÃO".
2. **Qualificação:** Réu-Reconvinte e Autor-Reconvindo.
3. **Conexão:** Justifique brevemente.
4. **Fatos e Direito Reconvencionais:** Narre e fundamente o pedido do reconvinte.
5. **Pedidos da Reconvenção:** Claros e específicos contra o autor-reconvindo.
6. **Valor da Causa e Provas da Reconvenção:** Específicos.
7. **Intimação:** Peça a intimação do autor-reconvindo para contestar.
8. **Continue com os Pedidos da Contestação**.
9. **Fechamento:** Padrão OAB.
"""
elif tipo_peticao == "Impugnação à Contestação":
prompt += """
1. Endereçamento, processo, qualificação autor, tempestividade.
2. Tópico: **DA(S) PRELIMINAR(ES) ARGUIDA(S)** - Rebater cada uma.
3. Tópico: **DO MÉRITO E DOS FATOS NOVOS** - Manifestar-se sobre fatos impeditivos/modificativos/extintivos e documentos.
4. Tópico: **DA RATIFICAÇÃO DA INICIAL** - Reiterar termos e pedidos.
5. Tópico: **DAS PROVAS** - Especificar provas contrapostas.
6. **Pedidos:** Rejeição das preliminares, refutação da defesa, procedência dos pedidos da inicial.
7. Fechamento padrão OAB.
"""
elif tipo_peticao == "Embargos de Declaração":
prompt += """
1. Endereçamento ao juízo prolator. Processo, Embargante, Embargado. Decisão embargada identificada. Tempestividade (5 dias).
2. Tópico para cada vício (Art. 1022): **DA OMISSÃO / DA CONTRADIÇÃO / DA OBSCURIDADE / DO ERRO MATERIAL**.
3. Dentro de cada tópico: **Transcrever trecho relevante da decisão** e explicar **objetivamente** o vício. NÃO REJULGAR.
4. **Pedido:** Conhecimento e ACOLHIMENTO para sanar o(s) vício(s) apontado(s), especificando o que se espera. Pedir efeitos infringentes se cabível e fundamentado.
5. Fechamento padrão OAB.
"""
elif tipo_peticao == "Recurso de Apelação":
prompt += """
1. **Peça de Interposição:** Endereçamento ao juízo a quo. Processo, Apelante, Apelado. Pressupostos (cabimento, tempestividade, preparo/JG). Pedido de recebimento e remessa.
2. **Razões Recursais:** Endereçamento ao Tribunal. Breve relato. **DOS FUNDAMENTOS PARA REFORMA/ANULAÇÃO:** Error in procedendo ou Error in judicando. Impugnar especificamente a sentença (dialeticidade).
3. **Pedidos:** Conhecimento; PROVIMENTO para ANULAR ou REFORMAR (especificar); inversão sucumbência.
4. Fechamento padrão OAB.
"""
elif tipo_peticao == "Agravo de Instrumento":
prompt += """
1. Peça única ao Tribunal. Processo origem, Agravante, Agravado, Advogados (com endereços). Decisão agravada identificada (juntar cópia).
2. **DO CABIMENTO:** Demonstrar hipótese Art. 1015 ou mitigação. **DA TEMPESTIVIDADE.** **DO PREPARO/JG.** **DAS PEÇAS OBRIGATÓRIAS E FACULTATIVAS.**
3. **DAS RAZÕES RECURSAIS:** Exposição fato/direito, motivos para reforma/invalidação da interlocutória.
4. **DO PEDIDO DE EFEITO SUSPENSIVO / TUTELA ANTECIPADA RECURSAL** (se cabível e fundamentado).
5. **Pedidos:** Conhecimento; (se aplicável) efeito suspensivo/tutela; PROVIMENTO para reformar/invalidar; comunicação ao juiz.
6. Fechamento padrão OAB.
"""
elif tipo_peticao == "Petição de Tutela Provisória":
prompt += """
1. Indicar TIPO (Urgência/Evidência) e CARÁTER (Antecedente/Incidental). Endereçamento. Qualificação. Processo (se incidental).
2. **DOS REQUISITOS:**
* **Urgência:** Tópico **DA PROBABILIDADE DO DIREITO (FUMUS BONI IURIS)** e tópico **DO PERIGO DE DANO OU RISCO AO RESULTADO ÚTIL (PERICULUM IN MORA)**. Se possível, **DA REVERSIBILIDADE**.
* **Evidência:** Tópico **DA HIPÓTESE DE EVIDÊNCIA (ART. 311, INCISO __)** - Demonstrar o enquadramento.
3. **DO PEDIDO LIMINAR:** Descrever CLARAMENTE a medida solicitada.
4. **Se Antecedente:** Indicar pedido final ou aditamento/inicial completa. Valor da Causa.
5. **Pedidos:** Concessão da tutela; citação/intimação; (se antecedente) indicação ação principal/aditamento.
6. Fechamento padrão OAB.
"""
else:
prompt += """
1. Estrutura Lógica: Começo, meio, fim.
2. Argumentos Claros.
3. Pedido Claro.
4. Formalidades: Linguagem adequada, fechamento OAB.
"""
prompt += "\n\n**Instrução Final:** Gere **APENAS** o texto completo da peça corrigida abaixo, começando pelo endereçamento e terminando com 'OAB/...'. Não inclua NENHUM comentário, introdução ou explicação ANTES ou DEPOIS do texto da peça."
return prompt
def pos_corrigir_peticao(peticao_corrigida, tipo_peticao):
if "Nestes termos, pede deferimento" in peticao_corrigida:
peticao_corrigida = peticao_corrigida.replace("Nestes termos, pede deferimento", "Nestes termos pede deferimento")
if tipo_peticao == "Petição Inicial":
peticao_lower = peticao_corrigida.lower()
if "juntada da guia de custas" not in peticao_lower and "gratuidade de justiça" not in peticao_lower:
partes = peticao_corrigida.split("Nestes termos pede deferimento")
if len(partes) == 2:
peticao_corrigida = partes[0].strip() + "\nRequer, ainda, a juntada da guia de custas devidamente recolhida.\nNestes termos pede deferimento" + partes[1]
else:
peticao_corrigida = peticao_corrigida.replace("Local, data.", "Requer, ainda, a juntada da guia de custas devidamente recolhida.\n\nLocal, data.")
if "audiência de conciliação" not in peticao_lower and "audiência de mediação" not in peticao_lower:
partes = peticao_corrigida.split("Nestes termos pede deferimento")
if len(partes) == 2:
peticao_corrigida = partes[0].strip() + "\nOpta o Autor pela realização da audiência de conciliação ou mediação (Art. 319, VII, CPC).\nNestes termos pede deferimento" + partes[1]
else:
peticao_corrigida = peticao_corrigida.replace("Local, data.", "Opta o Autor pela realização da audiência de conciliação ou mediação (Art. 319, VII, CPC).\n\nLocal, data.")
linhas = [l.strip() for l in peticao_corrigida.strip().split('\n') if l.strip()]
fechamento_padrao = ["Local, data.", "ADVOGADO(A)", "OAB/..."]
while linhas and any(term in linhas[-1] for term in ["ADVOGADO", "OAB", "Local", "data"]):
linhas.pop()
linhas.extend(fechamento_padrao)
peticao_corrigida = "\n".join(linhas)
return peticao_corrigida
def chamar_api(prompt):
payload = {"contents": [{"parts": [{"text": prompt}]}],"generationConfig": {"temperature": CONFIG["temperatura"],"maxOutputTokens": CONFIG["max_tokens"],"topP": 0.95,}}
headers = {"Content-Type": "application/json"}
logger.info(f"Chamando API Gemini. Temp={CONFIG['temperatura']}, MaxTokens={CONFIG['max_tokens']}")
for tentativa in range(CONFIG["max_retry_attempts"]):
try:
with rate_limiter:
logger.debug(f"API Call - Tentativa {tentativa + 1}/{CONFIG['max_retry_attempts']}")
response = requests.post(GEMINI_API_URL, headers=headers, data=json.dumps(payload), timeout=CONFIG["timeout"])
logger.debug(f"API Response - Status: {response.status_code}")
if response.status_code == 429:
retry_after = int(response.headers.get("Retry-After", CONFIG["retry_delay"] * (tentativa + 1)))
logger.warning(f"Erro 429 (Too Many Requests). Aguardando {retry_after}s...")
if tentativa < CONFIG["max_retry_attempts"] - 1: time.sleep(retry_after); continue
else: return "❌ Erro 429: Muitas requisições. Tente novamente mais tarde."
if response.status_code != 200:
error_msg = "Erro desconhecido na API"
try:
error_info = response.json()
error_msg = error_info.get("error", {}).get("message", error_msg)
except json.JSONDecodeError:
error_msg = f"Erro {response.status_code}: {response.text[:200]}" # Mostra início do erro não-JSON
logger.error(f"API Error (Status {response.status_code}): {error_msg}")
if tentativa < CONFIG["max_retry_attempts"] - 1: time.sleep(CONFIG['retry_delay'] * (tentativa + 1)); continue
return f"❌ Erro da API ({response.status_code}): {error_msg}"
response_data = response.json()
if not response_data.get("candidates"):
prompt_feedback = response_data.get("promptFeedback", {})
block_reason = prompt_feedback.get("blockReason")
logger.error(f"API Response sem 'candidates'. Block Reason: {block_reason}")
if block_reason: return f"❌ A solicitação foi bloqueada pela API. Motivo: {block_reason}. Tente reformular."
else: return "❌ Resposta vazia ou inválida da API (sem 'candidates')."
candidate = response_data["candidates"][0]
finish_reason = candidate.get("finishReason")
if finish_reason and finish_reason not in ["STOP", "MAX_TOKENS", "FINISH_REASON_UNSPECIFIED"]:
logger.error(f"Geração interrompida pela API. Motivo: {finish_reason}")
safety_ratings = candidate.get("safetyRatings", [])
harm_categories = [r['category'] for r in safety_ratings if r['probability'] not in ['NEGLIGIBLE', 'LOW']]
if harm_categories: return f"❌ Resposta bloqueada por segurança. Motivo: {finish_reason} ({', '.join(harm_categories)})."
else: return f"❌ Geração interrompida. Motivo: {finish_reason}."
if not candidate.get("content") or not candidate["content"].get("parts"):
logger.error("Estrutura de resposta inesperada: Faltando content/parts.")
if tentativa < CONFIG["max_retry_attempts"] - 1: time.sleep(CONFIG['retry_delay'] * (tentativa + 1)); continue
return "❌ Resposta inválida da API (estrutura inesperada)."
texto_gerado = candidate["content"]["parts"][0]["text"].strip()
logger.info(f"API retornou com sucesso ({len(texto_gerado)} chars). Finish Reason: {finish_reason}")
return texto_gerado
except requests.Timeout:
logger.warning(f"Timeout na API - Tentativa {tentativa + 1}.")
if tentativa < CONFIG["max_retry_attempts"] - 1: time.sleep(CONFIG['retry_delay'] * (tentativa + 1)); continue
return "⏱️ Tempo esgotado na comunicação com a API."
except requests.RequestException as e:
logger.error(f"Erro de Rede/Requisição API - Tentativa {tentativa + 1}: {e}")
if tentativa < CONFIG["max_retry_attempts"] - 1: time.sleep(CONFIG['retry_delay'] * (tentativa + 1)); continue
return f"❌ Erro de conexão com a API: {e}"
except json.JSONDecodeError as e:
logger.error(f"Erro ao decodificar JSON da API: {e}")
logger.debug(f"Resposta não-JSON: {response.text[:500]}")
return f"❌ Erro ao processar a resposta da API (JSON inválido)."
except Exception as e:
logger.exception(f"Erro inesperado na chamada da API - Tentativa {tentativa + 1}: {str(e)}")
if tentativa < CONFIG["max_retry_attempts"] - 1: time.sleep(CONFIG['retry_delay'] * (tentativa + 1)); continue
return f"❌ Erro inesperado no processamento da API: {str(e)}"
logger.error("Falha ao chamar a API após todas as tentativas.")
return "❌ Falha ao comunicar com a API após múltiplas tentativas."
def corrigir_peticao(caso, tipo_peticao, peticao_texto=None, peticao_arquivo=None, modo_treinamento=False):
logger.info(f"Iniciando correção. Tipo={tipo_peticao}, Treinamento={modo_treinamento}")
start_time = time.time()
analise_formatada, peticao_corrigida_final, resumo_diff, criticas_diff, diff_html = "", "", "", "", ""
peticao_conteudo = ""
try:
if not caso or not caso.strip():
logger.warning("Caso não fornecido.")
return "⚠️ Forneça o caso/contexto da peça.", "", "", "", ""
if not tipo_peticao or tipo_peticao not in TIPOS_DE_PETICAO:
logger.warning(f"Tipo de petição inválido ou não fornecido: {tipo_peticao}")
return "⚠️ Selecione um tipo de petição válido.", "", "", "", ""
if peticao_arquivo is not None:
logger.info("Processando arquivo de petição...")
peticao_conteudo = process_file(peticao_arquivo)
if peticao_conteudo.startswith("❌") or peticao_conteudo.startswith("⚠️"):
logger.error(f"Erro ao processar arquivo: {peticao_conteudo}")
return peticao_conteudo, "", "", "", ""
elif peticao_texto and peticao_texto.strip():
peticao_conteudo = peticao_texto
else:
logger.warning("Nenhuma petição fornecida (texto ou arquivo).")
return "⚠️ Forneça a petição (em texto ou arquivo).", "", "", "", ""
logger.info(f"Petição recebida ({len(peticao_conteudo)} chars).")
troll_triggers = ["escreva a petição", "faça a petição", "redija a petição", "elabore a petição", "crie a petição", "gere a petição", "faça para mim", "faça por mim"]
if any(trigger in peticao_conteudo.lower() for trigger in troll_triggers):
logger.info("Detectado pedido para escrever petição.")
return random.choice([
"Hummm... Parece que você quer que *eu* faça o trabalho, né? 😉 Que tal você tentar escrever a peça e *eu* te ajudo a corrigir?",
"🚫 Opa! Meu trabalho é *corrigir* e *ensinar*, não fazer a peça do zero. Mostre o que você fez e eu te dou o feedback!",
]), "", "", "", ""
cache_key = f"{tipo_peticao}|{caso[:100]}|{peticao_conteudo[:100]}|{modo_treinamento}"
if cache_key in CORRECAO_CACHE:
logger.info("Correção encontrada no cache.")
cached_data = CORRECAO_CACHE[cache_key]
return cached_data["analise_formatada"], cached_data["peticao_corrigida"], cached_data["resumo_diff"], cached_data["criticas_diff"], cached_data["diff_html"]
logger.info("Estruturando prompt de análise...")
prompt_analise = estruturar_prompt_analise(caso, peticao_conteudo, tipo_peticao, modo_treinamento)
logger.info("Chamando API para análise...")
analise_bruta = chamar_api(prompt_analise)
if analise_bruta.startswith(("❌", "⏱️", "⚠️")):
logger.error(f"Erro retornado pela API (Análise): {analise_bruta}")
return analise_bruta, "", "", "", ""
logger.info("Análise recebida. Formatando...")
analise_formatada = formatar_correcao(analise_bruta)
logger.info("Estruturando prompt para peça corrigida...")
prompt_peticao = estruturar_prompt_peticao_corrigida(caso, peticao_conteudo, tipo_peticao)
logger.info("Chamando API para gerar peça corrigida...")
peticao_corrigida_bruta = chamar_api(prompt_peticao)
if peticao_corrigida_bruta.startswith(("❌", "⏱️", "⚠️")):
logger.error(f"Erro retornado pela API (Correção): {peticao_corrigida_bruta}")
peticao_corrigida_final = f"⚠️ Falha ao gerar a versão corrigida:\n{peticao_corrigida_bruta}"
else:
logger.info("Peça corrigida recebida. Aplicando pós-correção...")
start_markers = ["excelentíssimo", "eméritos julgadores", "meritíssimo juízo"]
start_index = -1
peticao_lower_corr = peticao_corrigida_bruta.lower()
for marker in start_markers:
idx = peticao_lower_corr.find(marker)
if idx != -1: start_index = idx; break
peticao_extraida = peticao_corrigida_bruta[start_index:] if start_index != -1 else peticao_corrigida_bruta
peticao_corrigida_final = pos_corrigir_peticao(peticao_extraida, tipo_peticao)
logger.info("Pós-correção aplicada.")
logger.info("Calculando diferenças...")
diff_html, resumo_diff, criticas_diff = calcular_diff_interno(peticao_conteudo, peticao_corrigida_final)
logger.info("Diferenças calculadas.")
resultado_cache = {"analise_formatada": analise_formatada,"peticao_corrigida": peticao_corrigida_final,"resumo_diff": resumo_diff,"criticas_diff": criticas_diff,"diff_html": diff_html}
CORRECAO_CACHE[cache_key] = resultado_cache
HISTORICO.append({"data": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),"tipo_peticao": tipo_peticao,"caso": caso,"peticao_original": peticao_conteudo,**resultado_cache})
if len(HISTORICO) > 10: HISTORICO.pop(0)
end_time = time.time()
logger.info(f"Correção concluída em {end_time - start_time:.2f} segundos.")
return analise_formatada, peticao_corrigida_final, resumo_diff, criticas_diff, diff_html
except Exception as e:
logger.exception("Erro GERAL inesperado dentro de corrigir_peticao:")
error_message = f"❌ Erro inesperado na função de correção: {str(e)}\nConsulte os logs para detalhes."
return error_message, "", "", "", ""
def formatar_correcao(texto):
texto = texto.replace("### Análise Geral", "## 📝 Análise Geral")
texto = texto.replace("### Nota Final", "## ⭐ Nota Final")
texto = texto.replace("### Pontos Positivos", "## ✅ Pontos Positivos")
texto = texto.replace("### Pontos a Melhorar", "## ⚠️ Pontos a Melhorar")
texto = texto.replace("### Análise Específica dos Requisitos", "## 🔎 Análise Específica dos Requisitos")
linhas = texto.split('\n')
linhas_formatadas = []
in_list = False
for linha in linhas:
linha_strip = linha.strip()
if linha_strip.startswith(("1.","2.","3.","4.","5.")):
linhas_formatadas.append(f"\n**{linha_strip}**")
in_list = False
elif linha_strip.startswith(("- ","* ")):
prefix = " * " if in_list else "* "
linhas_formatadas.append(prefix + linha_strip[2:])
in_list = True
elif linha_strip.startswith(("a)","b)","c)","d)","e)")):
linhas_formatadas.append(f" * {linha_strip}")
in_list = True
else:
linhas_formatadas.append(linha)
if not linha_strip.startswith(("*", "- ", "a)", "b)", "c)", "d)", "e)"," ")):
in_list = False
texto_final = "\n".join(linhas_formatadas)
nota_pattern = r"(\*\*Nota Final:?\*\*\s*)(\d{1,2}[,.]\d{1}|\d{1,2})"
match = re.search(nota_pattern, texto_final, re.IGNORECASE)
if match:
nota = match.group(2).replace(",",".")
texto_final = re.sub(nota_pattern, f"\\1**{nota}/10**", texto_final, count=1, flags=re.IGNORECASE)
return texto_final
def fornecer_dica_contextual(texto_peticao, tipo_peticao):
if not texto_peticao or not texto_peticao.strip():
return "*Digite algo na peça para receber dicas...*"
dicas_relevantes = []
texto_lower = texto_peticao.lower()
for chave in ["Clareza e Concisão", "Fundamentação Legal", "Revisão Ortográfica"]:
if chave in DICAS_OAB_MELHORADO:
dicas_relevantes.append(f"💡 **{chave}:** {DICAS_OAB_MELHORADO[chave]}")
keywords_por_tipo = {
"Petição Inicial": ["endereçamento", "qualificação", "fatos", "fundamentos", "pedidos", "valor", "provas", "audiência", "citação", "custas"],
"Contestação": ["preliminar", "mérito", "impugnação", "improcedência", "honorários", "art. 337", "art. 341"],
"Reconvenção": ["reconvinte", "reconvindo", "conexão", "art. 343"],
"Impugnação à Contestação": ["réplica", "preliminar", "fatos novos", "documentos", "ratifica", "art. 350"],
"Embargos de Declaração": ["omissão", "contradição", "obscuridade", "erro material", "art. 1022"],
"Recurso de Apelação": ["sentença", "reforma", "anulação", "dialeticidade", "preparo", "tribunal", "art. 1009"],
"Agravo de Instrumento": ["interlocutória", "cabimento", "urgência", "efeito suspensivo", "art. 1015", "art. 1017"],
"Petição de Tutela Provisória": ["urgência", "evidência", "liminar", "periculum", "fumus boni", "art. 300", "art. 311"],
}
if tipo_peticao in keywords_por_tipo:
for chave, dica in DICAS_OAB_MELHORADO.items():
chave_lower = chave.lower()
if f"({tipo_peticao.split()[0].lower()})" in chave_lower or \
any(kw in chave_lower for kw in keywords_por_tipo[tipo_peticao] if kw in texto_lower):
dica_formatada = f"💡 **{chave}:** {dica}"
if dica_formatada not in dicas_relevantes:
dicas_relevantes.append(dica_formatada)
if not dicas_relevantes:
return "*Continue escrevendo ou selecione o tipo de peça para dicas mais específicas.*"
return "\n\n".join(dicas_relevantes[:4])
def calcular_diff_interno(texto_original, texto_corrigido):
if not texto_original or not texto_corrigido or texto_corrigido.startswith(("⚠️","❌")):
return "","",""
orig_lines = texto_original.splitlines()
corr_lines = texto_corrigido.splitlines()
diff = difflib.ndiff(orig_lines, corr_lines)
resultado_diff_html = []
criticas = []
linhas_removidas = 0
linhas_adicionadas = 0
keywords_criticas_removidas = ["invenção", "identificação", "erro crasso", "incorreto", "prazo"]
keywords_criticas_adicionadas = ["custas", "art. 319", "art. 337", "art. 1022", "art. 1015", "art. 300", "fumus", "periculum", "essencial", "preliminar", "tempestividade", "cabimento"]
for linha in diff:
texto = linha[2:]
texto_escaped = escape(texto)
if linha.startswith(' '): resultado_diff_html.append(f"<div class='linha-igual'>{texto_escaped}</div>")
elif linha.startswith('- '):
linhas_removidas += 1
resultado_diff_html.append(f"<div class='linha-removida' title='Removido'><del>{texto_escaped}</del></div>")
if texto.strip() and any(kw in texto.lower() for kw in keywords_criticas_removidas): criticas.append(f"<span style='color: red;'>🔴 REMOVIDO:</span> {texto_escaped}")
elif linha.startswith('+ '):
linhas_adicionadas += 1
resultado_diff_html.append(f"<div class='linha-adicionada' title='Adicionado'><ins>{texto_escaped}</ins></div>")
if texto.strip() and any(kw in texto.lower() for kw in keywords_criticas_adicionadas): criticas.append(f"<span style='color: green;'>🟢 ADICIONADO:</span> {texto_escaped}")
resumo_estatistico = f"**Resumo:**\n* Removidas: {linhas_removidas}\n* Adicionadas: {linhas_adicionadas}\n* **Saldo:** {linhas_adicionadas - linhas_removidas} linhas"
criticas_texto = "### 🚨 Mudanças Críticas / Essenciais:\n" + "\n".join(criticas) if criticas else "*Nenhuma mudança crítica/essencial detectada.*"
css = """<style>.diff-container { font-family: monospace; white-space: pre-wrap; border: 1px solid #ccc; padding: 10px; background-color: #f9f9f9; max-height: 600px; overflow-y: auto;} .linha-igual { color: #555; margin: 1px 0; } .linha-removida { background-color: #ffebee; color: #c62828; margin: 1px 0; } .linha-adicionada { background-color: #e8f5e9; color: #2e7d32; margin: 1px 0; } .linha-removida del, .linha-adicionada ins { text-decoration: none; padding: 0 2px; border-radius: 2px; } .linha-removida del { background-color: #ffcdd2; } .linha-adicionada ins { background-color: #c8e6c9; }</style>"""
visualizacao_detalhada_html = f"{css}<div class='diff-container'>{''.join(resultado_diff_html)}</div>"
return visualizacao_detalhada_html, resumo_estatistico, criticas_texto
def calcular_progresso():
if not HISTORICO: return "📊 Nenhum progresso registrado ainda."
notas = []
tipos_corrigidos = set()
for entry in HISTORICO:
tipos_corrigidos.add(entry["tipo_peticao"])
match = re.search(r"Nota Final:?\s*\**(\d{1,2}[,.]\d{1}|\d{1,2})\s*/\s*10", entry["analise_formatada"], re.IGNORECASE)
if match:
try: notas.append(float(match.group(1).replace(",", ".")))
except ValueError: pass
total_corridas = len(HISTORICO)
media = sum(notas) / len(notas) if notas else 0
tendencia = ""
if len(notas) > 1:
if notas[-1] > notas[-2]: tendencia = "⬆️ Melhorando"
elif notas[-1] < notas[-2]: tendencia = "⬇️ Piorando"
else: tendencia = "↔️ Estável"
progresso_texto = f"**📊 Resumo:**\n* Correções: {total_corridas}\n* Tipos: {', '.join(sorted(list(tipos_corrigidos)))}\n* Média: {media:.1f}/10 ({len(notas)} válidas)\n* Tendência: {tendencia if tendencia else 'N/A'}"
return progresso_texto
def mostrar_historico():
if not HISTORICO: return "📜 Histórico vazio."
historico_md = "### 📜 Histórico (Últimas 10)\n---\n"
for i, entry in enumerate(reversed(HISTORICO)):
caso_resumo = (entry['caso'][:80] + '...') if len(entry['caso']) > 80 else entry['caso']
nota_str = "N/A"
match = re.search(r"Nota Final:?\s*\**(\d{1,2}[,.]\d{1}|\d{1,2})\s*/\s*10", entry["analise_formatada"], re.IGNORECASE)
if match: nota_str = f"{match.group(1).replace(',', '.')}/10"
historico_md += f"**{len(HISTORICO)-i}.** {entry['data']} | **Tipo:** {entry['tipo_peticao']} | **Nota:** {nota_str}\n **Caso:** _{caso_resumo}_\n---\n"
return historico_md
def salvar_progresso(caso, peticao, tipo_peticao):
logger.info("Iniciando salvamento de progresso...")
try:
if not caso or not peticao or not tipo_peticao:
logger.warning("Inputs insuficientes para salvar progresso.")
return "", "⚠️ Caso, Petição e Tipo são necessários para salvar.", None
dados_progresso = {"tipo_peticao": tipo_peticao, "caso": caso, "peticao": peticao, "timestamp": datetime.now().isoformat()}
json_content = json.dumps(dados_progresso, ensure_ascii=False, indent=4)
timestamp_str = datetime.now().strftime("%Y%m%d_%H%M%S")
filename = f"progresso_peticao_{timestamp_str}.json"
temp_dir = tempfile.gettempdir()
temp_file_path = os.path.join(temp_dir, filename)
with open(temp_file_path, 'w', encoding='utf-8') as f: f.write(json_content)
logger.info(f"Progresso salvo com sucesso em: {temp_file_path}")
success_message = f"✅ Progresso salvo! Baixe `{filename}`."
return json_content, success_message, temp_file_path
except Exception as e:
logger.error(f"Erro ao salvar progresso: {str(e)}", exc_info=True)
return "", f"❌ Erro ao salvar: {str(e)}", None
def carregar_progresso(arquivo_entrada):
logger.info("Iniciando carregamento de progresso...")
if arquivo_entrada is None: return "", "", "", "⚠️ Nenhum arquivo selecionado."
try:
file_path = arquivo_entrada.name
logger.info(f"Carregando progresso do arquivo: {file_path}")
with open(file_path, 'r', encoding='utf-8') as f: dados = json.load(f)
caso = dados.get("caso")
peticao = dados.get("peticao")
tipo_peticao = dados.get("tipo_peticao")
if caso is None or peticao is None or tipo_peticao is None:
logger.warning(f"Arquivo JSON carregado não contém chaves esperadas: {dados.keys()}")
return "", "", "", "❌ Erro: Arquivo de progresso inválido."
if tipo_peticao not in TIPOS_DE_PETICAO:
logger.warning(f"Tipo de petição '{tipo_peticao}' do arquivo não é válido.")
return caso, peticao, "", f"⚠️ Tipo '{tipo_peticao}' inválido. Selecione manualmente."
logger.info("Progresso carregado com sucesso.")
return tipo_peticao, caso, peticao, "✅ Progresso carregado!"
except json.JSONDecodeError:
logger.error(f"Erro ao decodificar JSON do arquivo: {file_path}")
return "", "", "", "❌ Erro: Arquivo não é JSON válido."
except Exception as e:
logger.error(f"Erro ao carregar o progresso: {str(e)}", exc_info=True)
return "", "", "", f"❌ Erro inesperado ao carregar: {str(e)}"
def estruturar_prompt_geracao_caso(tipo_peticao="Petição Inicial"):
prompt = f"""
Crie um caso fictício **detalhado e realista** para um exercício prático de elaboração de uma peça do tipo **"{tipo_peticao}"**, simulando um cenário de Exame de Ordem (OAB).
**Instruções Gerais:**
1. Complexidade Adequada (15-25 linhas).
2. Dados Essenciais: Nomes, locais, datas, valores, relação jurídica, conflito.
3. Coerência: Fatos lógicos justificando a peça.
4. Foco no Tipo de Peça: Cenário deve exigir a **{tipo_peticao}**.
**Instruções Específicas para "{tipo_peticao}":**
"""
if tipo_peticao == "Petição Inicial":
prompt += """* Descreva violação de direitos (contrato descumprido, dano, etc.) onde cliente busca tutela inicial. * Inclua elementos para danos materiais/morais. * Forneça dados para qualificação das partes. * **Exemplo Tema:** Consumo, inadimplemento, cobrança indevida, despejo."""
elif tipo_peticao == "Contestação":
prompt += """* **PRIMEIRO:** Resumo da Petição Inicial recebida pelo réu (autor, alegações, pedidos). * **DEPOIS:** Versão dos fatos do RÉU, incluindo informações para: Alegar preliminares; Impugnar fatos; Apresentar defesa de mérito (pagamento, prescrição, etc.). * Forneça dados qualificação réu."""
elif tipo_peticao == "Reconvenção":
prompt += """* Siga estrutura **Contestação**: Resumo Petição Inicial + Versão Réu. * **ADICIONE fatos/fundamentos** para RÉU fazer pedido PRÓPRIO contra AUTOR ORIGINAL (CONEXO com ação/defesa). * **Exemplo:** Cobrança -> Réu contesta e reconvém pedindo indenização por danos no mesmo negócio."""
elif tipo_peticao == "Impugnação à Contestação":
prompt += """* Resumo Petição Inicial do Autor. * **Resumo Contestação** do réu, destacando: Preliminares arguidas; Fatos novos (impeditivos/modificativos/extintivos); Documentos juntados. * Objetivo: fornecer elementos para Autor rebater contestação."""
elif tipo_peticao == "Embargos de Declaração":
prompt += """* Breve descrição caso original. * **Apresente DISPOSITIVO ou TRECHO FUNDAMENTAL de DECISÃO JUDICIAL** (sentença/acórdão). * **INDIQUE CLARAMENTE vício** (omissão, contradição, obscuridade, erro material) NESSA DECISÃO, dando contexto. * **Exemplo:** "Sentença julgou X, mas omissa quanto a Y." OU "Acórdão afirmou X e concluiu Y (contradição)." """
elif tipo_peticao == "Recurso de Apelação":
prompt += """* Descrição caso original. * **Resumo da SENTENÇA** (procedente/improcedente/parcial, fundamentos juiz). * **FORNEÇA ELEMENTOS indicando ERRO na sentença** (in judicando/in procedendo) que justifiquem reforma/anulação. * **Exemplo:** "Sentença ignorou prova X." OU "Juiz indeferiu testemunha chave Y (cerceamento)." * Indique cliente (apelante)."""
elif tipo_peticao == "Agravo de Instrumento":
prompt += """* Descrição caso em andamento. * **Apresente DECISÃO INTERLOCUTÓRIA específica**. * Decisão DEVE se enquadrar no Art. 1015 ou justificar mitigação. * **FORNEÇA ELEMENTOS indicando ERRO** na decisão E, se possível, **URGÊNCIA**. * **Exemplo:** "Juiz indeferiu JG apesar de prova." OU "Deferiu liminar sem comprovação mora." * Indique cliente (agravante)."""
elif tipo_peticao == "Petição de Tutela Provisória":
prompt += """* Situação de conflito com **urgência** (risco iminente) ou direito **evidente**. * **FORNEÇA ELEMENTOS claros demonstrando:** **Urgência:** Probabilidade do direito (fumus) E Perigo de dano (periculum). **Evidência:** Hipótese do Art. 311 (abuso defesa, repetitivo, etc.). * Indique medida específica desejada. * Especifique se antecedente ou incidental."""
else:
prompt += "* Crie cenário jurídico cível com conflito claro."
prompt += """
**Finalize o caso com:**
"**Situação-Problema:** Na qualidade de advogado(a) de [Nome do Cliente], elabore a peça processual cabível ({tipo_peticao})."
**Importante:** Evite casos muito específicos. Foque em temas comuns. Seja criativo!
"""
return prompt
def gerar_caso_ficticio(tipo_peticao="Petição Inicial"):
logger.info(f"Solicitada geração de caso para: {tipo_peticao}")
try:
if not tipo_peticao or tipo_peticao not in TIPOS_DE_PETICAO:
logger.warning(f"Tentativa de gerar caso com tipo inválido: {tipo_peticao}")
return "⚠️ Erro: Selecione um tipo de petição válido antes de gerar o caso."
logger.info("Estruturando prompt para geração de caso...")
prompt = estruturar_prompt_geracao_caso(tipo_peticao)
logger.info("Chamando API para gerar caso...")
caso_gerado = chamar_api(prompt)
if caso_gerado.startswith(("❌","⏱️","⚠️")):
logger.error(f"Erro da API ao gerar caso: {caso_gerado}")
return f"⚠️ Falha ao gerar caso fictício:\n{caso_gerado}"
frase_final_padrao = f"**Situação-Problema:** Na qualidade de advogado(a) de"
if frase_final_padrao.lower() not in caso_gerado.lower():
logger.warning("Frase 'Situação-Problema' não encontrada. Adicionando manualmente.")
nomes_possiveis = re.findall(r"[A-Z][a-z]+(?:\s[A-Z][a-z]+)+", caso_gerado)
cliente_nome = nomes_possiveis[0] if nomes_possiveis else "[Cliente]"
caso_gerado += f"\n\n**Situação-Problema:** Na qualidade de advogado(a) de {cliente_nome}, elabore a peça processual cabível ({tipo_peticao})."
logger.info("Caso fictício gerado com sucesso.")
return caso_gerado
except Exception as e:
logger.exception("Erro inesperado dentro de gerar_caso_ficticio:")
return f"❌ Erro inesperado ao gerar o caso: {str(e)}"
def interface_gradio():
with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="orange")) as demo:
gr.Markdown("# ⚖️ Corretor Jurídico OAB Master ⚖️")
gr.Markdown("Analise e aprimore suas peças processuais!")
with gr.Row():
with gr.Column(scale=1):
gr.Markdown("### ⚙️ Configurações")
tipo_peticao_input = gr.Dropdown(label="1. Selecione o Tipo de Peça", choices=TIPOS_DE_PETICAO, value="Petição Inicial")
modo_treinamento = gr.Checkbox(label="🎓 Modo Treinamento", value=False)
with gr.Accordion("🛠️ Opções Avançadas da IA", open=False):
temperatura_slider = gr.Slider(minimum=0.0, maximum=1.0, value=CONFIG["temperatura"], step=0.1, label="Criatividade")
timeout_slider = gr.Slider(minimum=30, maximum=180, value=CONFIG["timeout"], step=10, label="Timeout API (seg)")
max_tokens_slider = gr.Slider(minimum=2048, maximum=8192, value=CONFIG["max_tokens"], step=256, label="Max Resposta (Tokens)")
config_update_btn = gr.Button("💾 Salvar Configs")
config_status = gr.Textbox(label="Status", interactive=False, lines=1)
with gr.Accordion("📊 Dashboard", open=False):
progresso_output = gr.Markdown("*Atualize para ver estatísticas.*")
progresso_btn = gr.Button("🔄 Atualizar Progresso")
with gr.Accordion("📜 Histórico", open=False):
historico_output = gr.Markdown("*Atualize para ver histórico.*")
historico_btn = gr.Button("🔄 Ver Histórico")
with gr.Accordion("💾📂 Salvar/Carregar", open=False):
salvar_btn = gr.Button("💾 Salvar Caso/Peça")
carregar_btn = gr.Button("📂 Carregar Caso/Peça")
arquivo_progresso_upload = gr.File(label="Selecione .json", file_types=[".json"], scale=2)
progresso_download_file = gr.File(label="Baixar Progresso", interactive=False)
with gr.Column(scale=3):
gr.Markdown("### 1. Forneça o Caso / Contexto")
with gr.Row():
caso_input = gr.Textbox(label="📋 Caso ou Situação", lines=8, placeholder="Insira o texto COMPLETO do caso...")
caso_btn = gr.Button("🎲 Gerar Caso", variant="secondary", scale=0)
gr.Markdown("### 2. Insira Sua Peça")
with gr.Tabs():
with gr.TabItem("✏️ Digitar Texto"):
peticao_texto_input = gr.Textbox(label="Texto da Peça", lines=12, placeholder="Cole ou digite sua peça aqui...")
dica_contextual_output = gr.Markdown(value="*Dicas aparecerão aqui...*")
btn_corrigir = gr.Button("🚀 CORRIGIR E ANALISAR!", variant="primary")
with gr.TabItem("📎 Anexar Arquivo"):
peticao_arquivo_input = gr.File(label="Arquivo (.pdf, .docx, .txt - Máx 15MB)", file_types=[".pdf", ".docx", ".txt"])
arquivo_preview_output = gr.Textbox(label="🔍 Prévia do Arquivo", lines=4, interactive=False)
btn_corrigir_arquivo = gr.Button("🚀 CORRIGIR E ANALISAR ARQUIVO!", variant="primary")
processing_message = gr.Markdown("", visible=False)
gr.Markdown("--- \n ### 3. Resultados da Correção")
with gr.Tabs():
with gr.TabItem("📊 Análise e Correção Sugerida"):
resultado_analise_output = gr.Markdown(label="Análise Detalhada e Nota")
resultado_peticao_corrigida_output = gr.Textbox(label="✨ Versão Sugerida", lines=18, show_copy_button=True)
with gr.TabItem("⚖️ Comparativo (Diff)"):
gr.Markdown("### ↔️ Comparativo: Sua Peça vs. Versão Sugerida")
with gr.Row():
with gr.Column(scale=1):
resumo_alteracoes_output = gr.Markdown(label="Resumo")
criticas_output = gr.Markdown(label="Mudanças Críticas")
with gr.Column(scale=3):
visualizacao_detalhada_output = gr.HTML(label="Alterações Detalhadas")
clear_btn = gr.Button("🗑️ Limpar Campos e Resultados")
caso_btn.click(
fn=lambda: gr.update(value="⏳ Gerando caso...", visible=True), outputs=processing_message
).then(
fn=gerar_caso_ficticio, inputs=[tipo_peticao_input], outputs=[caso_input]
).then(
fn=lambda: gr.update(value="", visible=False), outputs=processing_message
)
peticao_texto_input.change(fn=fornecer_dica_contextual, inputs=[peticao_texto_input, tipo_peticao_input], outputs=dica_contextual_output)
tipo_peticao_input.change(fn=fornecer_dica_contextual, inputs=[peticao_texto_input, tipo_peticao_input], outputs=dica_contextual_output)
peticao_arquivo_input.change(fn=process_file, inputs=[peticao_arquivo_input], outputs=[arquivo_preview_output])
btn_corrigir.click(
fn=lambda: gr.update(value="⏳ Processando texto... Gerando análise e correção...", visible=True),
outputs=processing_message
).then(
fn=corrigir_peticao,
inputs=[caso_input, tipo_peticao_input, peticao_texto_input, gr.State(None), modo_treinamento],
outputs=[resultado_analise_output, resultado_peticao_corrigida_output,
resumo_alteracoes_output, criticas_output, visualizacao_detalhada_output],
concurrency_limit=1
).then(
fn=lambda: gr.update(value="", visible=False),
outputs=processing_message
)
btn_corrigir_arquivo.click(
fn=lambda: gr.update(value="⏳ Processando arquivo... Gerando análise e correção...", visible=True),
outputs=processing_message
).then(
fn=corrigir_peticao,
inputs=[caso_input, tipo_peticao_input, gr.State(None), peticao_arquivo_input, modo_treinamento],
outputs=[resultado_analise_output, resultado_peticao_corrigida_output,
resumo_alteracoes_output, criticas_output, visualizacao_detalhada_output],
concurrency_limit=1
).then(
fn=lambda: gr.update(value="", visible=False),
outputs=processing_message
)
config_update_btn.click(atualizar_config, inputs=[temperatura_slider, timeout_slider, max_tokens_slider], outputs=config_status)
progresso_btn.click(fn=calcular_progresso, outputs=progresso_output)
historico_btn.click(fn=mostrar_historico, outputs=historico_output)
salvar_btn.click(
fn=lambda: gr.update(value="⏳ Salvando...", visible=True), outputs=processing_message
).then(
fn=salvar_progresso, inputs=[caso_input, peticao_texto_input, tipo_peticao_input],
outputs=[gr.Textbox(visible=False), config_status, progresso_download_file]
).then(
fn=lambda x: gr.update(visible=bool(x)), inputs=[progresso_download_file], outputs=[progresso_download_file]
).then(
lambda: gr.update(value="", visible=False), None, processing_message
)
carregar_btn.click(
fn=lambda: gr.update(value="⏳ Carregando...", visible=True), outputs=processing_message
).then(
fn=carregar_progresso, inputs=[arquivo_progresso_upload],
outputs=[tipo_peticao_input, caso_input, peticao_texto_input, config_status]
).then(
lambda: gr.update(value="", visible=False), None, processing_message
)
clear_btn.click(
fn=lambda: ["Petição Inicial", False, None, None, None, None, "*Dicas aparecerão aqui...*", "", "", "", "", "", None, None, ""],
outputs=[
tipo_peticao_input, modo_treinamento, caso_input, peticao_texto_input,
peticao_arquivo_input, arquivo_preview_output, dica_contextual_output,
resultado_analise_output, resultado_peticao_corrigida_output,
resumo_alteracoes_output, criticas_output, visualizacao_detalhada_output,
arquivo_progresso_upload, progresso_download_file, config_status
]
)
return demo
if __name__ == "__main__":
logger.info("Iniciando a aplicação Gradio...")
app = interface_gradio()
app.launch()
logger.info("Aplicação Gradio encerrada.")