GabrielGD commited on
Commit
81c208a
·
verified ·
1 Parent(s): f260604

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +399 -872
app.py CHANGED
@@ -21,6 +21,7 @@ from html import escape
21
  import random
22
  import logging
23
  import re
 
24
 
25
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
26
  logger = logging.getLogger(__name__)
@@ -29,991 +30,517 @@ load_dotenv()
29
 
30
  GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")
31
  if not GEMINI_API_KEY:
32
- raise ValueError("❌ A chave GEMINI_API_KEY não foi encontrada. Configure-a no arquivo .env ou como Secret.")
33
 
34
  GEMINI_API_URL = f"https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash-latest:generateContent?key={GEMINI_API_KEY}"
35
 
36
- CONFIG = {
37
- "max_retry_attempts": 3,
38
- "retry_delay": 5,
39
- "timeout": 180,
40
- "temperatura": 0.3,
41
- "max_tokens": 8192
42
- }
43
-
44
- CONFIG_OAB = {
45
- "temperatura": 0.2,
46
- "max_tokens": 8192
47
- }
48
-
49
  rate_limiter = RateLimiter(max_calls=30, period=60)
50
 
51
  TIPOS_DE_PETICAO = [
52
- "Petição Inicial",
53
- "Contestação",
54
- "Reconvenção",
55
- "Impugnação à Contestação",
56
- "Embargos de Declaração",
57
- "Recurso de Apelação",
58
- "Agravo de Instrumento",
59
- "Petição de Tutela Provisória",
60
- "Ação de Alimentos Gravídicos",
61
  ]
62
 
63
  DICAS_OAB_MELHORADO = {
64
- "Clareza e Concisão": "Seja direto e evite jargões excessivos. Facilite a leitura do juiz.",
65
- "Fundamentação Legal": "Sempre cite os artigos de lei, súmulas e jurisprudência pertinentes ao seu argumento.",
66
- "Coerência Argumentativa": "Mantenha uma linha lógica de raciocínio do início ao fim da peça.",
67
- "Revisão Ortográfica e Gramatical": "Erros de português podem prejudicar a credibilidade da sua peça.",
68
- "Formatação": "Utilize parágrafos bem definidos, espaçamento adequado e destaque pontos importantes (negrito, itálico) com moderação.",
69
- "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).",
70
- "Qualificação (Inicial)": "Inclua todos os dados do Art. 319, II, CPC. Use 'XXX' para dados não fornecidos.",
71
- "Fatos (Inicial)": "Narrativa cronológica, clara e objetiva, sem juízo de valor.",
72
- "Pedidos (Inicial)": "Certos, determinados e líquidos (sempre que possível). Cumule pedidos se necessário (Art. 327, CPC).",
73
- "Custas (Inicial)": "Sempre peça a juntada da guia ou a gratuidade de justiça (com declaração).",
74
- "Valor da Causa (Inicial)": "Deve corresponder ao benefício econômico pretendido (Art. 292, CPC).",
75
- "Preliminares (Contestação)": "Alegue todas as matérias do Art. 337 do CPC antes de discutir o mérito.",
76
- "Impugnação Específica (Contestação)": "Conteste cada fato alegado pelo autor, sob pena de presunção de veracidade (Art. 341, CPC).",
77
- "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.",
78
- "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.",
79
- "Pressupostos Recursais": "Verifique cabimento, tempestividade, preparo (custas recursais), regularidade formal e interesse/legitimidade.",
80
- "Dialeticidade (Recurso)": "Impugne especificamente os fundamentos da decisão recorrida (Art. 1010, II e III, CPC para Apelação).",
81
- "Delimitação da Matéria (Recurso)": "Indique claramente qual parte da decisão você está recorrendo.",
82
- "Fumus Boni Iuris": "Demonstre a plausibilidade do direito alegado com base em provas e argumentos sólidos.",
83
- "Periculum in Mora": "Evidencie o risco de dano irreparável ou de difícil reparação se a medida não for concedida imediatamente.",
84
- "Reversibilidade": "Se possível, argumente que a medida é reversível caso a decisão final seja diferente (Art. 300, §3º, CPC).",
85
  }
86
 
87
  HISTORICO = []
88
  CORRECAO_CACHE = {}
89
 
90
  def preprocess_image_for_ocr(img):
91
- try:
92
- gray = img.convert('L')
93
- np_img = np.array(gray)
94
- _, thresh = cv2.threshold(np_img, 150, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
95
- return Image.fromarray(thresh)
96
- except Exception as e:
97
- logger.error(f"Erro no pré-processamento da imagem: {str(e)}")
98
- return f"❌ Erro no pré-processamento: {str(e)}"
99
 
100
  def extract_text_from_pdf(file_path_or_obj):
101
- text = ""
102
- ocr_text = ""
103
- full_text = ""
104
  try:
105
  pdf_reader = pdfplumber_open(file_path_or_obj)
106
  for i, page in enumerate(pdf_reader.pages):
107
- page_full_text = ""
108
- page_text = page.extract_text()
109
- if page_text:
110
- page_full_text += page_text + "\n"
111
- text += page_text + "\n"
112
  else:
113
- logger.warning(f"Página {i+1} sem texto extraível, tentando OCR.")
114
- try:
115
- img = page.to_image(resolution=300)
116
- preprocessed_img = preprocess_image_for_ocr(img.original)
117
- page_ocr_text = pytesseract.image_to_string(preprocessed_img, lang='por')
118
- if page_ocr_text.strip():
119
- page_full_text += page_ocr_text + "\n"
120
- ocr_text += page_ocr_text + "\n"
121
- else:
122
- logger.warning(f"OCR falhou na página {i+1}.")
123
- page_full_text += f"[OCR falhou na página {i+1}]\n"
124
- ocr_text += f"[OCR falhou na página {i+1}]\n"
125
- except Exception as ocr_e:
126
- logger.error(f"Erro no OCR da página {i+1}: {str(ocr_e)}")
127
- page_full_text += f"[Erro no OCR da página {i+1}: {str(ocr_e)}]\n"
128
- ocr_text += f"[Erro no OCR da página {i+1}: {str(ocr_e)}]\n"
129
  full_text += page_full_text
130
  pdf_reader.close()
131
-
132
- if full_text.strip():
133
- logger.info("Texto combinado (extração/OCR) obtido do PDF.")
134
- return full_text
135
- else:
136
- logger.warning("Não foi possível extrair texto significativo do PDF (nem extração, nem OCR).")
137
- return "⚠️ Não foi possível extrair texto do PDF (tentativa de OCR falhou ou texto inexistente)."
138
-
139
  except Exception as e:
140
- logger.error(f"Erro geral ao processar o PDF: {str(e)}")
141
  try:
142
- logger.info("Tentando fallback com PyPDF2.")
143
- text_fallback = ""
144
- if isinstance(file_path_or_obj, str):
145
- with open(file_path_or_obj, 'rb') as f:
146
- reader = PyPDF2.PdfReader(f)
147
- for page in reader.pages: text_fallback += page.extract_text() or ""
148
- else:
149
- file_path_or_obj.seek(0)
150
- reader = PyPDF2.PdfReader(file_path_or_obj)
151
- for page in reader.pages: text_fallback += page.extract_text() or ""
152
-
153
- if text_fallback.strip():
154
- logger.info("Texto extraído com fallback PyPDF2.")
155
- return text_fallback
156
- else:
157
- return f"❌ Erro ao processar o PDF e fallback falhou: {str(e)}"
158
- except Exception as fallback_e:
159
- logger.error(f"Erro no fallback com PyPDF2: {str(fallback_e)}")
160
- return f"❌ Erro crítico ao processar o PDF: {str(e)}"
161
-
162
- def extract_text_from_docx(file_path_or_obj):
163
- try:
164
- doc = docx.Document(file_path_or_obj)
165
- text = "\n".join([para.text for para in doc.paragraphs])
166
- return text if text.strip() else "⚠️ Documento DOCX vazio ou sem texto."
167
- except Exception as e:
168
- logger.error(f"Erro ao processar o DOCX: {str(e)}")
169
- return f"❌ Erro ao processar o DOCX: {str(e)}"
170
-
171
- def extract_text_from_txt(file_path_or_obj):
172
  try:
173
- if isinstance(file_path_or_obj, str):
174
- with open(file_path_or_obj, 'rb') as f:
175
- content = f.read()
176
- elif hasattr(file_path_or_obj, 'read'):
177
- content = file_path_or_obj.read()
178
- if hasattr(file_path_or_obj, 'seek'):
179
- try: file_path_or_obj.seek(0)
180
- except: pass
181
- else:
182
- return " Tipo de entrada TXT inválido."
183
-
184
- encodings = ['utf-8', 'latin-1', 'cp1252', 'iso-8859-1']
185
- for encoding in encodings:
186
- try:
187
- decoded_text = content.decode(encoding)
188
- logger.info(f"Arquivo TXT decodificado com {encoding}.")
189
- return decoded_text if decoded_text.strip() else "⚠️ Arquivo TXT vazio."
190
- except UnicodeDecodeError:
191
- continue
192
- logger.error("Não foi possível decodificar o arquivo TXT com encodings comuns.")
193
- return "❌ Não foi possível decodificar o arquivo TXT. Verifique o encoding."
194
- except Exception as e:
195
- logger.error(f"Erro ao processar o TXT: {str(e)}")
196
- return f"❌ Erro ao processar o TXT: {str(e)}"
197
-
198
- def process_file(file_obj):
199
- if file_obj is None:
200
- logger.warning("Nenhum arquivo fornecido para processamento.")
201
- return "⚠️ Nenhum arquivo enviado."
202
  try:
203
- file_path = getattr(file_obj, 'name', None)
204
- if not file_path:
205
- logger.error("Não foi possível obter o nome do arquivo do objeto.")
206
- return "❌ Erro: Não foi possível identificar o tipo do arquivo."
207
-
208
- filename = os.path.basename(file_path).lower()
209
- logger.info(f"Processando arquivo: {filename}")
210
-
211
  try:
212
- file_size = os.path.getsize(file_path)
213
- logger.info(f"Tamanho do arquivo: {file_size} bytes")
214
- if file_size > 15 * 1024 * 1024:
215
- logger.warning(f"Arquivo {filename} excede o limite de tamanho.")
216
- return "⚠️ O arquivo é muito grande. Máx. 15 MB."
217
- except OSError as size_e:
218
- logger.warning(f"Não foi possível verificar o tamanho do arquivo: {size_e}")
219
-
220
- if filename.endswith('.pdf'):
221
- return extract_text_from_pdf(file_path)
222
- elif filename.endswith('.docx'):
223
- return extract_text_from_docx(file_path)
224
- elif filename.endswith('.txt'):
225
- return extract_text_from_txt(file_path)
226
- else:
227
- logger.warning(f"Formato de arquivo não suportado: {filename}")
228
- return "⚠️ Formato de arquivo não suportado. Use PDF, DOCX ou TXT."
229
-
230
- except Exception as e:
231
- logger.error(f"Erro geral ao processar o arquivo '{getattr(file_obj, 'name', 'N/A')}': {str(e)}", exc_info=True)
232
- return f"❌ Erro inesperado ao processar o arquivo: {str(e)}"
233
 
234
  def chamar_api(prompt, use_oab_config=False):
235
- config_to_use = CONFIG_OAB if use_oab_config else CONFIG
236
- payload = {
237
- "contents": [{"parts": [{"text": prompt}]}],
238
- "generationConfig": {
239
- "temperature": config_to_use["temperatura"],
240
- "maxOutputTokens": config_to_use["max_tokens"],
241
- "topP": 0.95,
242
- }
243
- }
244
- headers = {"Content-Type": "application/json"}
245
- logger.info(f"Chamando API Gemini com {len(prompt)} chars. Temp: {config_to_use['temperatura']}, MaxTokens: {config_to_use['max_tokens']}")
246
-
247
- for tentativa in range(CONFIG["max_retry_attempts"]):
248
  try:
249
- with rate_limiter:
250
- response = requests.post(GEMINI_API_URL, headers=headers, data=json.dumps(payload), timeout=CONFIG["timeout"])
251
-
252
- if response.status_code == 429:
253
- retry_after = int(response.headers.get("Retry-After", CONFIG["retry_delay"] * (tentativa + 1)))
254
- logger.warning(f"Erro 429. Tentando novamente em {retry_after}s...")
255
- if tentativa < CONFIG["max_retry_attempts"] - 1: time.sleep(retry_after); continue
256
- else: return "❌ Erro 429: Muitas requisições."
257
-
258
- if response.status_code != 200:
259
- error_info = response.json()
260
- error_msg = error_info.get("error", {}).get("message", "Erro desconhecido")
261
- logger.error(f"Erro API (Status {response.status_code}): {error_msg}")
262
- if tentativa < CONFIG["max_retry_attempts"] - 1: time.sleep(CONFIG["retry_delay"] * (tentativa + 1)); continue
263
- return f"❌ Erro API ({response.status_code}): {error_msg}"
264
-
265
- response_data = response.json()
266
-
267
- if not response_data.get("candidates"):
268
- prompt_feedback = response_data.get("promptFeedback", {})
269
- block_reason = prompt_feedback.get("blockReason")
270
- logger.error(f"Resposta API sem 'candidates'. Block Reason: {block_reason}")
271
- return f"❌ Solicitação bloqueada pela API. Motivo: {block_reason}." if block_reason else " Resposta vazia/inválida (sem 'candidates')."
272
-
273
- candidate = response_data["candidates"][0]
274
- finish_reason = candidate.get("finishReason")
275
- if finish_reason not in [None, "STOP", "MAX_TOKENS"]:
276
- logger.error(f"Geração interrompida: {finish_reason}")
277
- safety_ratings = candidate.get("safetyRatings", [])
278
- harm_categories = [r['category'] for r in safety_ratings if r['probability'] not in ['NEGLIGIBLE', 'LOW']]
279
- return f"❌ Resposta bloqueada por segurança: {finish_reason} (Categorias: {', '.join(harm_categories)})." if harm_categories else f"❌ Geração interrompida: {finish_reason}."
280
-
281
- if "content" not in candidate or "parts" not in candidate["content"] or not candidate["content"]["parts"]:
282
- logger.error("Estrutura de resposta inesperada.")
283
- if tentativa < CONFIG["max_retry_attempts"] - 1: time.sleep(CONFIG["retry_delay"] * (tentativa + 1)); continue
284
- return "❌ Resposta inválida (estrutura inesperada)."
285
-
286
- texto_gerado = candidate["content"]["parts"][0]["text"].strip()
287
- logger.info(f"API retornou com sucesso. Tamanho: {len(texto_gerado)} chars.")
288
- return texto_gerado
289
-
290
- except requests.Timeout:
291
- logger.warning(f"Timeout tentativa {tentativa + 1}.")
292
- if tentativa < CONFIG["max_retry_attempts"] - 1: time.sleep(CONFIG["retry_delay"] * (tentativa + 1)); continue
293
- return "⏱️ Tempo esgotado."
294
- except requests.RequestException as e:
295
- logger.error(f"Erro de rede/requisição: {e}")
296
- if tentativa < CONFIG["max_retry_attempts"] - 1: time.sleep(CONFIG["retry_delay"] * (tentativa + 1)); continue
297
- return f"❌ Erro na requisição: {e}"
298
- except json.JSONDecodeError as e:
299
- logger.error(f"Erro JSON: {e}. Resposta: {response.text[:200]}...")
300
- return f"❌ Erro processar resposta API (JSON inválido)."
301
- except Exception as e:
302
- logger.exception(f"Erro inesperado API: {str(e)}")
303
- if tentativa < CONFIG["max_retry_attempts"] - 1: time.sleep(CONFIG["retry_delay"] * (tentativa + 1)); continue
304
- return f"❌ Erro inesperado: {str(e)}"
305
-
306
- return "❌ Falha API após todas tentativas."
307
 
308
  def get_instrucoes_gerais_avaliacao(modo_treinamento=False):
309
- base = """
310
- **Estrutura da Avaliação (Geral - IA):**
311
- 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.
312
- 2. **Nota Final:** 0 a 10 (uma casa decimal), com justificativa BREVE dos principais fatores.
313
- 3. **Pontos Positivos:** Liste pelo menos 2-3 aspectos bem executados, com justificativa (legal/técnica).
314
- 4. **Pontos a Melhorar:** Liste **TODOS** os problemas em ordem de gravidade (erros crassos primeiro). Para cada ponto:
315
- * Descrição clara do erro/omissão.
316
- * Fundamentação legal/doutrinária do porquê é um erro.
317
- * **Impacto estimado na nota** (ex: -0.5, -1.0, -2.0).
318
- * Sugestão de correção ou como deveria ter sido feito.
319
- 5. **Análise Específica dos Requisitos:** Avalie os itens chave da peça (listados abaixo para cada tipo).
320
-
321
- **Critérios Fundamentais (Aplicáveis a Quase Todos):**
322
- * **Coerência:** A peça responde ao caso/situação processual? Os argumentos são lógicos?
323
- * **Fundamentação:** Usou a lei, doutrina e jurisprudência corretamente? Citou artigos relevantes?
324
- * **Linguagem:** Clareza, objetividade, correção gramatical e formalidade adequada?
325
- * **Pedidos:** São claros, certos, determinados e coerentes com a fundamentação?
326
- * **NÃO INVENTAR FATOS:** Penalidade severa (-3.0 a -5.0) se criar fatos não presentes no caso.
327
- * **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/...".
328
- """
329
- if modo_treinamento:
330
- base += """
331
- **Modo de Treinamento:**
332
- * Para cada Ponto a Melhorar, detalhe AINDA MAIS:
333
- a) Explicação aprofundada do erro e sua consequência prática.
334
- b) Base legal/doutrinária específica.
335
- c) **Exemplo de Redação Corrigida** para aquele trecho específico.
336
- d) Dica prática relevante (use o banco de dicas se aplicável).
337
- e) Possível estratégia alternativa.
338
- * Destaque inconsistências entre a peça e o caso com citações diretas.
339
- * Sugira estratégias jurídicas adicionais ou alternativas viáveis.
340
- """
341
  return base
342
 
343
  def estruturar_prompt_analise_geral(caso, peticao, tipo_peticao, modo_treinamento=False):
344
- instrucoes_gerais = get_instrucoes_gerais_avaliacao(modo_treinamento)
345
- prompt_especifico = f"""
346
- ## Análise de Peça Processual: {tipo_peticao} (Avaliação Geral IA - Critérios OAB Simulados)
347
-
348
- **Caso/Contexto Fornecido:**
349
- ```
350
- {caso}
351
- ```
352
-
353
- **Peça do Aluno ({tipo_peticao}):**
354
- ```
355
- {peticao}
356
- ```
357
-
358
- {instrucoes_gerais}
359
-
360
- **Requisitos Específicos para {tipo_peticao} (Considerados pela IA):**
361
- """
362
- if tipo_peticao == "Petição Inicial" or tipo_peticao == "Ação de Alimentos Gravídicos":
363
- prompt_especifico += """
364
- - Endereçamento e competência (Art. 319, I, CPC; leis especiais como CDC Art. 101, I, Lei 11.804/08).
365
- - Qualificação completa das partes (Art. 319, II, CPC).
366
- - Exposição clara e lógica dos fatos (causa de pedir remota).
367
- - Fundamentação jurídica pertinente (causa de pedir próxima - Art. 319, III, CPC).
368
- - Pedidos certos, determinados e líquidos (se possível) (Art. 319, IV, CPC). Incluir pedido de conversão se aplicável (Alimentos Gravídicos).
369
- - Valor da causa correto e justificado (Art. 291-292, 319, V, CPC).
370
- - Indicação das provas pretendidas (Art. 319, VI, CPC).
371
- - Opção pela audiência de conciliação/mediação (Art. 319, VII, CPC).
372
- - Pedido de citação do réu (forma específica se necessário).
373
- - Requerimento de juntada de documentos essenciais (procuração, custas/JG).
374
- - Fechamento formal ("Nestes termos pede deferimento", Local, data, Advogado, OAB).
375
- """
376
- elif tipo_peticao == "Contestação":
377
- prompt_especifico += """
378
- - Endereçamento ao juízo correto. Qualificação do réu. Indicação do processo. Tempestividade.
379
- - **Preliminares (Art. 337):** Alegar TODAS aplicáveis.
380
- - **Impugnação Específica dos Fatos (Art. 341):** Rebater PONTO A PONTO.
381
- - **Mérito:** Defesa direta/indireta. Fundamentação jurídica.
382
- - **Pedidos:** Acolhimento preliminares; IMPROCEDÊNCIA TOTAL; Condenação custas/honorários.
383
- - Indicação de provas. Fechamento formal.
384
- """
385
- # Add other types...
386
-
387
- prompt_especifico += "\n\n**Instruções Finais:** Analise a peça do aluno com base no caso e nos critérios gerais e específicos. Use seu conhecimento jurídico para simular um examinador da OAB. Forneça feedback construtivo e detalhado. Gere a resposta **exclusivamente** no formato solicitado (Análise Geral, Nota, Pontos Positivos, etc.)."
388
- return prompt_especifico
389
 
390
  def estruturar_prompt_peticao_corrigida_geral(caso, peticao_original, tipo_peticao):
391
- prompt = f"""
392
- **Caso/Contexto Fornecido:**
393
- ```
394
- {caso}
395
- ```
396
-
397
- **Peça Original do Aluno ({tipo_peticao}):**
398
- ```
399
- {peticao_original}
400
- ```
401
-
402
- **Tarefa:** Com base no caso e nos problemas identificados na peça original, **gere uma versão corrigida e aprimorada** da peça ({tipo_peticao}). Use seu conhecimento jurídico para aplicar as melhores práticas e os requisitos formais/materiais para este tipo de peça no exame da OAB.
403
-
404
- **Instruções Específicas para a Peça Corrigida ({tipo_peticao}):**
405
- """
406
- if tipo_peticao == "Petição Inicial" or tipo_peticao == "Ação de Alimentos Gravídicos":
407
- prompt += """
408
- 1. **Endereçamento:** Correto (Vara Cível/Família, Foro competente).
409
- 2. **Qualificação:** Completa (Art. 319, II), usando "XXX".
410
- 3. **Fatos:** Cronológicos, claros, fiéis ao caso. Indícios paternidade (Alim. Grav.).
411
- 4. **Fundamentação:** Artigos pertinentes (CC, CDC, CPC, Lei 11.804/08).
412
- 5. **Pedidos:** Claros, certos, determinados. Pedido alimentos gravídicos (valor provisório/definitivo), tutela urgência, conversão (Lei 11.804/08, Art. 6º, p. único).
413
- 6. **Valor da Causa:** Correto.
414
- 7. **Provas:** Indicar (genérico).
415
- 8. **Audiência:** Indicar opção (Art. 319, VII).
416
- 9. **Citação:** Pedido.
417
- 10. **Custas:** Pedido expresso juntada OU JG.
418
- 11. **Fechamento:** Padrão OAB.
419
- """
420
- elif tipo_peticao == "Contestação":
421
- prompt += """
422
- 1. **Endereçamento:** Ao juízo da ação.
423
- 2. **Identificação:** Processo, qualificação réu.
424
- 3. **Tempestividade:** (Opcional).
425
- 4. **Estrutura:** Síntese; Preliminares (Art. 337); Mérito (Impugnação Específica, Defesa); Reconvenção (Se aplicável).
426
- 5. **Pedidos:** Acolhimento preliminares; **IMPROCEDÊNCIA TOTAL**; Condenação custas/honorários.
427
- 6. **Provas:** Indicar.
428
- 7. **Fechamento:** Padrão OAB.
429
- """
430
- # Add other types...
431
-
432
- 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."
433
  return prompt
434
 
435
  def pos_corrigir_peticao(peticao_corrigida, tipo_peticao):
436
- if "Nestes termos, pede deferimento" in peticao_corrigida:
437
- peticao_corrigida = peticao_corrigida.replace("Nestes termos, pede deferimento", "Nestes termos pede deferimento")
438
-
439
  if tipo_peticao in ["Petição Inicial", "Ação de Alimentos Gravídicos"]:
440
- if "juntada da guia de custas" not in peticao_corrigida.lower() and "gratuidade de justiça" not in peticao_corrigida.lower():
441
- partes = peticao_corrigida.split("Nestes termos pede deferimento")
442
- prefixo = partes[0].strip() if len(partes) > 1 else peticao_corrigida.strip()
443
- sufixo = "\nNestes termos pede deferimento" + partes[1] if len(partes) > 1 else ""
444
- peticao_corrigida = prefixo + "\nRequer, ainda, a juntada da guia de custas devidamente recolhida." + sufixo
445
-
446
- if "audiência de conciliação" not in peticao_corrigida.lower() and "audiência de mediação" not in peticao_corrigida.lower():
447
- partes = peticao_corrigida.split("Nestes termos pede deferimento")
448
- prefixo = partes[0].strip() if len(partes) > 1 else peticao_corrigida.strip()
449
- sufixo = "\nNestes termos pede deferimento" + partes[1] if len(partes) > 1 else ""
450
- peticao_corrigida = prefixo + "\nOpta o Autor pela realização da audiência de conciliação ou mediação (Art. 319, VII, CPC)." + sufixo
451
-
452
- linhas = [l.strip() for l in peticao_corrigida.strip().split('\n') if l.strip()]
453
- fechamento_padrao = ["\nLocal, data.", "ADVOGADO(A)", "OAB/..."]
454
-
455
- # Remove potential multiple closing lines before adding the standard one
456
- while linhas and any(term.lower() in linhas[-1].lower() for term in ["advogado", "oab", "local", "data", "pede deferimento", "termos em que"]):
457
- # More robust check to avoid removing essential content like the final request
458
- last_line_lower = linhas[-1].lower()
459
- is_closing_line = any(std_close.strip().lower() in last_line_lower for std_close in fechamento_padrao) or "pede deferimento" in last_line_lower or "termos em que" in last_line_lower
460
- if not is_closing_line and len(linhas[-1]) > 30: # Heuristic: unlikely to be just closing
461
- break
462
  linhas.pop()
463
-
464
- peticao_corrigida = "\n".join(linhas).strip() + "".join(fechamento_padrao) # Join directly
465
-
466
- return peticao_corrigida.strip()
467
-
468
- def formatar_correcao(texto):
469
- texto = texto.replace("### Análise Geral", "## 📝 Análise Geral")
470
- texto = texto.replace("### Nota Final", "## ⭐ Nota Final")
471
- texto = texto.replace("### Pontos Positivos", "## Pontos Positivos")
472
- texto = texto.replace("### Pontos a Melhorar", "## ⚠️ Pontos a Melhorar")
473
- texto = texto.replace("### Análise Específica dos Requisitos", "## 🔎 Análise Específica dos Requisitos")
474
-
475
- linhas = texto.split('\n')
476
- linhas_formatadas = []
477
- in_list = False
478
- for linha in linhas:
479
- linha_strip = linha.strip()
480
- if linha_strip.startswith(("1.","2.","3.","4.","5.")):
481
- linhas_formatadas.append(f"\n**{linha_strip}**")
482
- in_list = False
483
- elif linha_strip.startswith(("- ","* ")):
484
- prefix = " * " if in_list else "* "
485
- linhas_formatadas.append(prefix + linha_strip[2:])
486
- in_list = True
487
- elif linha_strip.startswith(("a)","b)","c)","d)","e)")):
488
- linhas_formatadas.append(f" * {linha_strip}")
489
- in_list = True
490
- else:
491
- linhas_formatadas.append(linha)
492
- if not linha_strip.startswith(("*", "- ", "a)", "b)", "c)", "d)", "e)"," ")):
493
- in_list = False
494
-
495
- texto_final = "\n".join(linhas_formatadas)
496
- nota_pattern = r"(\*\*Nota Final:?\*\*\s*)(\d{1,2}[,.]\d{1}|\d{1,2})"
497
- match = re.search(nota_pattern, texto_final, re.IGNORECASE)
498
- if match:
499
- nota = match.group(2).replace(",",".")
500
- texto_final = re.sub(nota_pattern, f"\\1**{nota}/10**", texto_final, count=1, flags=re.IGNORECASE)
501
- return texto_final
502
 
503
  def corrigir_peticao_geral_ia(caso, tipo_peticao, peticao_texto=None, peticao_arquivo=None, modo_treinamento=False):
504
- logger.info(f"Iniciando correção GERAL (IA) para tipo: {tipo_peticao}")
505
- start_time = time.time()
506
-
507
- if not caso or not caso.strip(): return "⚠️ Forneça o caso.", "", "", "", "", "", ""
508
- if not tipo_peticao or tipo_peticao not in TIPOS_DE_PETICAO: return "⚠️ Selecione tipo.", "", "", "", "", "", ""
509
-
510
  peticao_conteudo = ""
511
- if peticao_arquivo is not None:
512
- peticao_conteudo = process_file(peticao_arquivo)
513
- if peticao_conteudo.startswith("❌") or peticao_conteudo.startswith("⚠️"):
514
- return peticao_conteudo, "", "", "", "", "", ""
515
- elif peticao_texto and peticao_texto.strip():
516
- peticao_conteudo = peticao_texto
517
- else: return "⚠️ Forneça a petição.", "", "", "", "", "", ""
518
-
519
- troll_triggers = [ "escreva a petição", "faça a petição"]
520
- if any(trigger in peticao_conteudo.lower() for trigger in troll_triggers):
521
- return random.choice(["Nope! 😉", "Faça você e eu corrijo!"]), "", "", "", "", "", ""
522
-
523
- aviso_validacao = ""
524
-
525
- cache_key = f"geral|{tipo_peticao}|{caso[:50]}|{peticao_conteudo[:50]}|{modo_treinamento}"
526
- if cache_key in CORRECAO_CACHE:
527
- logger.info("Correção GERAL (IA) encontrada no cache.")
528
- cached_data = CORRECAO_CACHE[cache_key]
529
- return ( cached_data["analise_formatada"], cached_data["peticao_original"],
530
- cached_data["peticao_corrigida"], cached_data["resumo_diff"],
531
- cached_data["criticas_diff"], cached_data.get("avaliacao_criterios_ia", ""),
532
- cached_data["diff_html"] )
533
-
534
- prompt_analise = estruturar_prompt_analise_geral(caso, peticao_conteudo, tipo_peticao, modo_treinamento)
535
- analise = chamar_api(prompt_analise, use_oab_config=False)
536
- if analise.startswith("❌") or analise.startswith("⏱️"): return aviso_validacao + analise, peticao_conteudo, "", "", "", "", ""
537
- analise_formatada = formatar_correcao(analise)
538
-
539
- prompt_peticao = estruturar_prompt_peticao_corrigida_geral(caso, peticao_conteudo, tipo_peticao)
540
- peticao_corrigida_bruta = chamar_api(prompt_peticao, use_oab_config=False)
541
- peticao_corrigida_final = ""
542
- if peticao_corrigida_bruta.startswith("❌") or peticao_corrigida_bruta.startswith("⏱️"):
543
- peticao_corrigida_final = f"⚠️ Falha gerar corrigida:\n{peticao_corrigida_bruta}"
544
- else:
545
- peticao_corrigida_final = pos_corrigir_peticao(peticao_corrigida_bruta, tipo_peticao)
546
-
547
- avaliacao_criterios_md = ""
548
-
549
- visualizacao_detalhada_html, resumo_diff, criticas_diff = calcular_diff_interno(peticao_conteudo, peticao_corrigida_final)
550
-
551
- correcao_completa_formatada = aviso_validacao + analise_formatada
552
- resultado_historico = { "data": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "modo": "Geral IA", "tipo_peticao": tipo_peticao, "caso": caso, "peticao_original": peticao_conteudo, "analise_formatada": correcao_completa_formatada, "peticao_corrigida": peticao_corrigida_final, "resumo_diff": resumo_diff,"criticas_diff": criticas_diff, "avaliacao_criterios_ia": avaliacao_criterios_md }
553
- HISTORICO.append(resultado_historico)
554
- if len(HISTORICO) > 10: HISTORICO.pop(0)
555
-
556
- 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": visualizacao_detalhada_html }
557
-
558
- end_time = time.time()
559
- logger.info(f"Correção GERAL (IA) concluída em {end_time - start_time:.2f}s.")
560
-
561
- return ( correcao_completa_formatada, peticao_conteudo, peticao_corrigida_final,
562
- resumo_diff, criticas_diff, avaliacao_criterios_md, visualizacao_detalhada_html )
563
-
564
- # --- Funções de Correção Padrão OAB ---
565
  def parse_oab_pdf(file_obj):
566
  if file_obj is None: return None, None, None, "⚠️ Nenhum PDF OAB."
567
  try:
568
  full_text = extract_text_from_pdf(file_obj.name)
569
- if full_text.startswith("❌") or full_text.startswith("⚠️"): return None, None, None, full_text
570
-
571
- enunciado, gabarito, distribuicao = "", "", ""
572
- current_section = None
573
- lines = full_text.split('\n')
574
-
575
  for line in lines:
576
- line_strip = line.strip()
577
- line_lower = line_strip.lower()
578
-
579
- if line_strip.startswith("Enunciado"): current_section = "enunciado"; continue
580
- if line_strip.startswith("Gabarito comentado"): current_section = "gabarito"; continue
581
- if line_strip.startswith("Distribuição dos Pontos"): current_section = "distribuicao"; continue
582
- if line_strip.startswith("PADRÃO DE RESPOSTA – QUESTÃO"): current_section = None; break
583
- if line_strip.startswith("Padrão de Resposta"): continue
584
-
585
- if current_section == "enunciado": enunciado += line + "\n"
586
- elif current_section == "gabarito": gabarito += line + "\n"
587
- elif current_section == "distribuicao": distribuicao += line + "\n"
588
-
589
- enunciado = enunciado.strip()
590
- gabarito = gabarito.strip()
591
- distribuicao = distribuicao.strip()
592
-
593
- if not enunciado or not gabarito or not distribuicao:
594
- logger.warning(f"Parse OAB PDF falhou. E:{bool(enunciado)}, G:{bool(gabarito)}, D:{bool(distribuicao)}")
595
- return None, None, None, f"⚠️ Falha separar seções. Verifique o texto completo:\n\n{full_text[:500]}..."
596
-
597
- logger.info("PDF OAB parseado.")
598
- return enunciado, gabarito, distribuicao, "✅ PDF OAB carregado e seções extraídas."
599
- except Exception as e:
600
- logger.error(f"Erro parsear PDF OAB: {str(e)}", exc_info=True)
601
- return None, None, None, f"❌ Erro processar PDF OAB: {str(e)}"
602
 
603
  def estruturar_prompt_geracao_modelo_oab(enunciado, gabarito, distribuicao):
604
- prompt = f"""
605
- **Tarefa:** Elabore a peça processual completa e adequada para o **Enunciado**, seguindo **estritamente** as diretrizes do **Gabarito Comentado** e os critérios da **Distribuição dos Pontos**. Crie uma peça modelo perfeita para este caso OAB.
606
-
607
- **Enunciado:**
608
- ```
609
- {enunciado}
610
- ```
611
-
612
- **Gabarito Comentado (Diretrizes Conteúdo/Fundamentação):**
613
- ```
614
- {gabarito}
615
- ```
616
-
617
- **Distribuição dos Pontos (Estrutura/Itens Essenciais):**
618
- ```
619
- {distribuicao}
620
- ```
621
-
622
- **Instruções:**
623
- 1. Use **exclusivamente** informações/requisitos dos textos fornecidos. **Não adicione nada externo.**
624
- 2. Incorpore **todos** argumentos/fundamentos do Gabarito.
625
- 3. Estruture a peça para contemplar **todos** itens da Distribuição.
626
- 4. Linguagem formal OAB.
627
- 5. Use "XXX" apenas se Enunciado omisso e necessário.
628
- 6. Finalize com "Local, data.", "ADVOGADO(A)", "OAB/...". **Não se identifique.**
629
- 7. Gere **apenas** o texto completo da peça. Sem comentários ou títulos fora da peça.
630
  """
631
  return prompt
632
 
633
  def gerar_peticao_modelo_oab(enunciado, gabarito, distribuicao):
634
- if not enunciado or not gabarito or not distribuicao:
635
- return "⚠️ Enunciado, Gabarito e Distribuição necessários."
636
-
637
- logger.info("Gerando peça modelo OAB...")
638
  prompt = estruturar_prompt_geracao_modelo_oab(enunciado, gabarito, distribuicao)
639
- peticao_modelo = chamar_api(prompt, use_oab_config=True)
640
-
641
- if peticao_modelo.startswith("") or peticao_modelo.startswith("⏱️"):
642
- logger.error(f"Erro API gerar modelo OAB: {peticao_modelo}")
643
- return f"❌ Falha gerar modelo OAB:\n{peticao_modelo}"
644
-
645
- logger.info("Peça modelo OAB gerada.")
646
- peticao_modelo = pos_corrigir_peticao(peticao_modelo, "Petição Desconhecida")
647
- return peticao_modelo
648
 
649
  def estruturar_prompt_correcao_pela_oab(peticao_usuario, enunciado, gabarito, distribuicao):
650
- prompt = f"""
651
- **Tarefa:** Avalie a **Peça do Usuário**, comparando-a **estritamente** com o padrão OAB (Gabarito Comentado, Distribuição dos Pontos) em relação ao **Enunciado**.
652
-
653
- **Enunciado:**
654
- ```
655
- {enunciado}
656
- ```
657
-
658
- **Gabarito Comentado (Esperado):**
659
- ```
660
- {gabarito}
661
- ```
662
-
663
- **Distribuição dos Pontos (Critérios):**
664
- ```
665
- {distribuicao}
666
- ```
667
 
668
- **Peça do Usuário (Avaliar):**
669
- ```
670
- {peticao_usuario}
671
- ```
672
 
673
  **Instruções:**
674
- 1. **Avaliação Pontual:** Para **cada item** na **Distribuição**, verifique se foi abordado pelo Usuário conforme o Gabarito. Atribua a pontuação correspondente.
675
- 2. **Justificativa:** Justifique brevemente a pontuação de cada item.
676
- 3. **Feedback:** Aponte erros/omissões do Usuário vs Gabarito/Distribuição.
677
- 4. **Peça Corrigida (Opcional):** Se possível, sugira trechos corrigidos para alinhar ao padrão.
678
  5. **Output Markdown:**
679
-
680
  ```markdown
681
- ### 📊 Avaliação Detalhada Conforme Padrão OAB
682
-
683
  **Pontuação por Item:**
684
-
685
  * **[Item 1 Distribuição]:** [Pontos Obtidos] / [Máximo]
686
  * *Justificativa:* [...]
687
- * **[Item 2 Distribuição]:** [...] / [...]
688
- * *Justificativa:* [...]
689
- * ... (Todos os itens)
690
-
691
  **Pontuação Total Estimada:** [Soma Obtidos] / [Soma Máximos]
692
-
693
- **Comentários e Pontos de Melhoria:**
694
- * [...]
695
-
696
  ---
697
- ### ✨ Sugestões de Correção (Se aplicável)
698
-
699
- [Sugestões ou trechos corrigidos]
700
  ```
701
- **Importante:** Baseie **toda** avaliação **exclusivamente** nos materiais OAB fornecidos.
702
  """
703
  return prompt
704
 
705
  def corrigir_pela_oab(peticao_usuario, enunciado, gabarito, distribuicao):
706
- if not peticao_usuario or not enunciado or not gabarito or not distribuicao:
707
- return "⚠️ Petição Usuário, Enunciado, Gabarito e Distribuição necessários."
708
-
709
  logger.info("Corrigindo pela OAB...")
710
  prompt = estruturar_prompt_correcao_pela_oab(peticao_usuario, enunciado, gabarito, distribuicao)
711
- correcao_oab = chamar_api(prompt, use_oab_config=True)
712
-
713
- if correcao_oab.startswith("❌") or correcao_oab.startswith("⏱️"):
714
- logger.error(f"Erro API corrigir OAB: {correcao_oab}")
715
- return f"❌ Falha corrigir/avaliar OAB:\n{correcao_oab}"
716
-
717
- logger.info("Correção/Avaliação OAB concluída.")
718
- return correcao_oab
719
-
720
- # --- Funções Auxiliares (Diff, Histórico, Salvar/Carregar, Gerar Caso Fictício, Dicas) ---
721
- def calcular_diff_interno(texto_original, texto_corrigido):
722
- if not texto_original or not texto_corrigido or texto_corrigido.startswith("⚠️") or texto_corrigido.startswith("❌"): return "","",""
723
- orig_lines = texto_original.splitlines(); corr_lines = texto_corrigido.splitlines()
724
- diff = difflib.ndiff(orig_lines, corr_lines)
725
- resultado_diff_html = []; criticas = []; l_rem = 0; l_add = 0
726
- kw_rem = ["invenção", "identificação", "erro crasso"]
727
- kw_add = ["custas", "art. 319", "art. 337", "lei 11.804"]
728
- for linha in diff:
729
- texto = linha[2:]; texto_escaped = escape(texto)
730
- if linha.startswith(' '): resultado_diff_html.append(f"<div class='linha-igual'>{texto_escaped}</div>")
731
- elif linha.startswith('- '):
732
- l_rem += 1; resultado_diff_html.append(f"<div class='linha-removida'><del>{texto_escaped}</del></div>")
733
- if any(kw in texto.lower() for kw in kw_rem): criticas.append(f"<span style='color:red;'>🔴 REMOVIDO:</span> {texto_escaped}")
734
- elif linha.startswith('+ '):
735
- l_add += 1; resultado_diff_html.append(f"<div class='linha-adicionada'><ins>{texto_escaped}</ins></div>")
736
- if any(kw in texto.lower() for kw in kw_add): criticas.append(f"<span style='color:green;'>🟢 ADICIONADO:</span> {texto_escaped}")
737
- resumo = f"**Resumo:**\n* Removidas: {l_rem}\n* Adicionadas: {l_add}\n* **Net:** {l_add - l_rem}"
738
- crit_txt = "### 🚨 Mudanças Chave:\n" + "\n".join(criticas) if criticas else "Nenhuma mudança chave detectada."
739
- css = """<style>.diff-container{font-family:monospace;white-space:pre-wrap;border:1px solid #ccc;padding:10px;background:#f9f9f9;}.linha-igual{color:#555;}.linha-removida{background:#ffebee;color:#c62828;text-decoration:line-through;}.linha-adicionada{background:#e8f5e9;color:#2e7d32;}.linha-removida del,.linha-adicionada ins{text-decoration:none;padding:1px 3px;border-radius:3px;}.linha-removida del{background:#ffcdd2;}.linha-adicionada ins{background:#c8e6c9;}</style>"""
740
- html = f"{css}<div class='diff-container'>{''.join(resultado_diff_html)}</div>"
741
- return html, resumo, crit_txt
742
 
743
  def calcular_progresso():
744
  if not HISTORICO: return "📊 Progresso vazio."
745
- notas_geral = []
746
- tipos_geral = set()
747
- count_geral = 0
748
- for entry in HISTORICO:
749
- if entry.get("modo", "Geral IA") == "Geral IA":
750
- count_geral += 1
751
- tipos_geral.add(entry["tipo_peticao"])
752
- match = re.search(r"\*\*(\d{1,2}[,.]?\d?)/10\*\*", entry.get("analise_formatada",""))
753
- if match:
754
- try: notas_geral.append(float(match.group(1).replace(",",".")))
755
- except: pass
756
- media_geral = sum(notas_geral) / len(notas_geral) if notas_geral else 0
757
- prog_txt = f"**📊 Progresso (Geral IA):**\n* Correções: {count_geral}\n* Tipos: {', '.join(sorted(list(tipos_geral)))}\n* Média Notas: {media_geral:.1f}/10"
758
- return prog_txt
759
 
760
  def mostrar_historico():
761
  if not HISTORICO: return "📜 Histórico vazio."
762
- hist_md = "### 📜 Histórico (Últimas 10)\n\n---\n"
763
- for i, entry in enumerate(reversed(HISTORICO)):
764
- modo = entry.get("modo", "Geral IA")
765
- nota_str = "N/A"
766
- match = re.search(r"\*\*(\d{1,2}[,.]?\d?)/10\*\*", entry.get("analise_formatada",""))
767
- if match: nota_str = f"{match.group(1).replace(',', '.')}/10"
768
- hist_md += f"**{len(HISTORICO)-i}.** {entry['data']} ({modo})\n"
769
- hist_md += f"**Tipo:** {entry['tipo_peticao']} **Nota (IA):** {nota_str}\n"
770
- hist_md += f"**Caso:** _{entry['caso'][:60]}..._\n---\n"
771
- return hist_md
772
 
773
  def salvar_progresso(caso, peticao, tipo_peticao):
774
- logger.info("Salvando progresso...")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
775
  try:
776
- if not caso or not peticao or not tipo_peticao: return "", "⚠️ Inputs necessários.", None
777
- dados = { "tipo_peticao": tipo_peticao, "caso": caso, "peticao": peticao, "ts": datetime.now().isoformat()}
778
- js = json.dumps(dados, ensure_ascii=False, indent=4)
779
  ts = datetime.now().strftime("%Y%m%d_%H%M%S")
780
- fname = f"progresso_{ts}.json"
781
- fpath = os.path.join(tempfile.gettempdir(), fname)
782
- with open(fpath, 'w', encoding='utf-8') as f: f.write(js)
783
- logger.info(f"Progresso salvo: {fpath}")
784
- msg = f"✅ Salvo! Baixe `{fname}`."
785
- return js, msg, fpath
 
786
  except Exception as e:
787
- logger.error(f"Erro salvar: {str(e)}", exc_info=True)
788
- return "", f"❌ Erro salvar: {str(e)}", None
789
 
790
- def carregar_progresso(arquivo_entrada):
791
- logger.info("Carregando progresso...")
792
- if arquivo_entrada is None: return "", "", "", "⚠️ Nenhum arquivo."
793
  try:
794
- fpath = arquivo_entrada.name
795
- with open(fpath, 'r', encoding='utf-8') as f: dados = json.load(f)
796
- caso = dados.get("caso")
797
- pet = dados.get("peticao")
798
- tipo = dados.get("tipo_peticao")
799
- if not all([caso, pet, tipo]): return "", "", "", "❌ Arquivo inválido."
800
- if tipo not in TIPOS_DE_PETICAO: return caso, pet, "", f"⚠️ Tipo '{tipo}' inválido."
801
- logger.info("Progresso carregado.")
802
- return tipo, caso, pet, "✅ Progresso carregado!"
803
- except Exception as e:
804
- logger.error(f"Erro carregar: {str(e)}", exc_info=True)
805
- return "", "", "", f"❌ Erro carregar: {str(e)}"
806
 
807
- def gerar_caso_ficticio(tipo_peticao="Petição Inicial"):
808
- if not tipo_peticao: return "Erro: Tipo não selecionado."
809
- logger.info(f"Gerando caso para: {tipo_peticao}")
810
- prompt = estruturar_prompt_geracao_caso(tipo_peticao)
811
- caso = chamar_api(prompt, use_oab_config=False)
812
- if caso.startswith("❌") or caso.startswith("⏱️"): return f"Falha gerar caso:\n{caso}"
813
- frase_final = f"**Situação-Problema:** Na qualidade de advogado(a) de"
814
- if frase_final.lower() not in caso.lower():
815
- nomes = re.findall(r"[A-Z][a-z]+(?:\s[A-Z][a-z]+)+", caso)
816
- cliente = nomes[0] if nomes else "[Cliente]"
817
- caso += f"\n\n**Situação-Problema:** Na qualidade de advogado(a) de {cliente}, elabore a peça cabível ({tipo_peticao})."
818
- logger.info("Caso gerado.")
819
- return caso
 
 
 
 
 
 
 
 
 
820
 
821
- def fornecer_dica_contextual(texto_peticao, tipo_peticao):
822
- if not texto_peticao or not texto_peticao.strip():
823
- return "Digite algo na petição para receber dicas..."
824
- dicas_relevantes = []
825
- texto_lower = texto_peticao.lower()
826
- for chave, dica in DICAS_OAB_MELHORADO.items():
827
- if chave in ["Clareza e Concisão", "Fundamentação Legal", "Coerência Argumentativa", "Revisão Ortográfica e Gramatical", "Formatação"]:
828
- dicas_relevantes.append(f"💡 **{chave}:** {dica}")
829
- keywords_por_tipo = {
830
- "Petição Inicial": ["endereçamento", "qualificação", "fatos", "fundamentos", "pedidos", "valor da causa", "provas", "audiência", "citação", "custas"],
831
- "Contestação": ["preliminar", "mérito", "impugnação", "fatos", "improcedência", "provas", "honorários"],
832
- "Reconvenção": ["reconvinte", "reconvindo", "conexão", "pedidos reconvencionais"],
833
- "Impugnação à Contestação": ["réplica", "preliminar", "fatos novos", "documentos", "ratifica"],
834
- "Embargos de Declaração": ["omissão", "contradição", "obscuridade", "erro material", "decisão embargada"],
835
- "Recurso de Apelação": ["sentença", "reforma", "anulação", "dialeticidade", "preparo", "tribunal", "razões"],
836
- "Agravo de Instrumento": ["decisão interlocutória", "tribunal", "cabimento", "urgência", "efeito suspensivo", "peças obrigatórias"],
837
- "Petição de Tutela Provisória": ["urgência", "evidência", "liminar", "periculum", "fumus boni", "probabilidade", "risco"],
838
- "Ação de Alimentos Gravídicos": ["gravídicos", "nascituro", "indícios de paternidade", "binômio", "conversão", "lei 11.804"],
839
- }
840
- if tipo_peticao in keywords_por_tipo:
841
- for keyword in keywords_por_tipo[tipo_peticao]:
842
- if keyword in texto_lower:
843
- for chave, dica in DICAS_OAB_MELHORADO.items():
844
- chave_lower = chave.lower()
845
- tipo_simples = tipo_peticao.split()[0].lower() if tipo_peticao else ""
846
- if f"({tipo_simples})" in chave_lower or keyword in chave_lower:
847
- dica_formatada = f"💡 **{chave}:** {dica}"
848
- if dica_formatada not in dicas_relevantes:
849
- dicas_relevantes.append(dica_formatada)
850
- if not dicas_relevantes:
851
- return "💡 Continue escrevendo ou selecione o tipo de peça para dicas mais específicas."
852
- return "\n\n".join(dicas_relevantes[:5])
853
 
854
  def estruturar_prompt_geracao_caso(tipo_peticao="Petição Inicial"):
855
- prompt = f"""
856
- 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).
857
- **Instruções Gerais para o Caso:**
858
- 1. **Complexidade Adequada:** (Ideal: 15-25 linhas).
859
- 2. **Dados Essenciais:** Nomes, locais, datas, valores, relação jurídica, conflito.
860
- 3. **Coerência:** Fatos lógicos para a peça indicada.
861
- 4. **Foco no Tipo de Peça:** Cenário exige **{tipo_peticao}**.
862
- **Instruções Específicas para Gerar Caso para "{tipo_peticao}":**
863
- """
864
- if tipo_peticao == "Petição Inicial" or tipo_peticao == "Ação de Alimentos Gravídicos":
865
- prompt += "* Descreva violação de direitos, busca inicial da tutela. Elementos para danos. Dados para qualificação. Temas: consumo, contrato, cobrança, despejo, família (alimentos gravídicos - incluir indícios paternidade, necessidade mãe/nascituro)."
866
- elif tipo_peticao == "Contestação":
867
- prompt += "* Resumo da Inicial recebida. Versão dos fatos do RÉU. Elementos para Preliminares (Art. 337), Impugnação Específica, Defesa de Mérito (direta/indireta). Dados qualificação réu."
868
- # ... (Add other types as before) ...
869
- else: prompt += "* Crie um cenário jurídico cível com conflito claro."
870
-
871
- prompt += """
872
- **Finalize o caso com:**
873
- "**Situação-Problema:** Na qualidade de advogado(a) de [Nome do Cliente], elabore a peça processual cabível ({tipo_peticao})."
874
- **Importante:** Evite temas excessivamente específicos. Foco em Civil/Processo Civil comum.
875
- """
876
  return prompt
877
 
878
-
879
- # --- Interface Gradio ---
880
  def interface_gradio():
881
  with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="orange")) as demo:
882
  gr.Markdown("# ⚖️ Corretor Jurídico OAB Master ⚖️")
883
- gr.Markdown("Use a **Correção Geral (IA)** para feedback amplo ou a **Correção Baseada no Padrão OAB** para avaliação específica.")
884
-
885
  with gr.Tabs() as main_tabs:
886
  with gr.TabItem("🤖 Correção Geral (IA)", id=0):
887
  with gr.Row():
888
  with gr.Column(scale=1):
889
- gr.Markdown("### 1. Configurações (Geral)")
890
- tipo_peticao_input_geral = gr.Dropdown(label="Tipo de Peça", choices=TIPOS_DE_PETICAO, value="Petição Inicial")
891
- modo_treinamento_geral = gr.Checkbox(label="🎓 Modo Treinamento", value=False)
892
- with gr.Accordion("⚙️ Configs IA", open=False):
893
- temperatura_slider = gr.Slider(0.0, 1.0, value=CONFIG["temperatura"], step=0.1, label="Criatividade")
894
- timeout_slider = gr.Slider(30, 300, value=CONFIG["timeout"], step=10, label="Timeout (s)")
895
- max_tokens_slider = gr.Slider(2048, 8192, value=CONFIG["max_tokens"], step=256, label="Max Tokens")
896
- def atualizar_config(temp, timeout, tokens): CONFIG["temperatura"], CONFIG["timeout"], CONFIG["max_tokens"] = temp, timeout, tokens; return "✅"
897
- config_update_btn = gr.Button("💾 Salvar")
898
- config_status_geral = gr.Textbox(label="Status", interactive=False)
899
- config_update_btn.click(atualizar_config, [temperatura_slider, timeout_slider, max_tokens_slider], config_status_geral)
900
- with gr.Accordion("📊 Progresso/Histórico", open=False):
901
- progresso_output = gr.Markdown("...")
902
- progresso_btn = gr.Button("🔄 Progresso")
903
- historico_output = gr.Markdown("...")
904
- historico_btn = gr.Button("🔄 Histórico")
905
- progresso_btn.click(calcular_progresso, outputs=progresso_output)
906
- historico_btn.click(mostrar_historico, outputs=historico_output)
907
- with gr.Accordion("💾/📂 Salvar/Carregar", open=False):
908
- salvar_btn_geral = gr.Button("💾 Salvar Inputs")
909
- carregar_btn_geral = gr.Button("📂 Carregar Inputs")
910
- arquivo_progresso_upload_geral = gr.File(label=".json", file_types=[".json"])
911
- progresso_download_file_geral = gr.File(label="Baixar", interactive=False)
912
-
913
  with gr.Column(scale=3):
914
- gr.Markdown("### 2. Insira Caso e Sua Peça")
915
- with gr.Row():
916
- caso_input_geral = gr.Textbox(label="📋 Caso", lines=8)
917
- caso_btn_geral = gr.Button("🎲 Gerar", variant="secondary", scale=0)
918
  with gr.Tabs():
919
- with gr.TabItem("✏️ Texto"):
920
- peticao_texto_input_geral = gr.Textbox(label="Sua Peça", lines=12)
921
- dica_contextual_output_geral = gr.Markdown("*Dicas...*")
922
- btn_corrigir_geral = gr.Button("🚀 Corrigir (IA)", variant="primary")
923
- with gr.TabItem("📎 Arquivo"):
924
- peticao_arquivo_input_geral = gr.File(label="Arquivo", file_types=[".pdf",".docx",".txt"])
925
- arquivo_preview_output_geral = gr.Textbox(label="🔍 Prévia", lines=3, interactive=False)
926
- btn_corrigir_arquivo_geral = gr.Button("🚀 Corrigir Arquivo (IA)", variant="primary")
927
-
928
- processing_message_geral = gr.Markdown("", visible=False)
929
  with gr.Tabs():
930
- with gr.TabItem("📊 Análise IA"): resultado_analise_output_geral = gr.Markdown()
931
- with gr.TabItem("✨ Correção IA"): resultado_peticao_corrigida_output_geral = gr.Textbox(lines=15, show_copy_button=True)
932
- with gr.TabItem("⚖️ Diff"):
933
- with gr.Row():
934
- with gr.Column(scale=1): resumo_alteracoes_output_geral = gr.Markdown()
935
- with gr.Column(scale=3): visualizacao_detalhada_output_geral = gr.HTML()
936
- criticas_output_geral = gr.Markdown()
937
-
938
  with gr.TabItem("📄 Correção Padrão OAB", id=1):
939
- gr.Markdown("Forneça o material oficial (PDF ou texto) e sua peça.")
940
  with gr.Row():
941
  with gr.Column(scale=2):
942
- gr.Markdown("### 1. Material Oficial OAB")
943
- with gr.Tabs():
944
- with gr.TabItem("📎 PDF Oficial"):
945
- oab_pdf_input = gr.File(label="Padrão Resposta OAB (.pdf)", file_types=[".pdf"])
946
- parse_status_output = gr.Textbox(label="Status PDF", interactive=False)
947
- with gr.TabItem("✏️ Manual"):
948
- oab_enunciado_input = gr.Textbox(label="Enunciado", lines=5)
949
- oab_gabarito_input = gr.Textbox(label="Gabarito", lines=8)
950
- oab_distribuicao_input = gr.Textbox(label="Distribuição", lines=8)
951
  with gr.Column(scale=2):
952
- gr.Markdown("### 2. Ações")
953
- btn_gerar_modelo_oab = gr.Button("📝 Gerar Peça Modelo OAB")
954
- gr.Markdown("---")
955
- peticao_usuario_oab_input = gr.Textbox(label="Sua Peça (para corrigir pela OAB)", lines=10)
956
- btn_corrigir_pela_oab = gr.Button("🎯 Corrigir pela OAB", variant="primary")
957
-
958
- processing_message_oab = gr.Markdown("", visible=False)
959
  with gr.Tabs():
960
- with gr.TabItem("📝 Modelo OAB"): resultado_modelo_oab_output = gr.Textbox(lines=20, show_copy_button=True)
961
- with gr.TabItem("🎯 Avaliação OAB"): resultado_correcao_oab_output = gr.Markdown()
962
-
963
- clear_btn = gr.Button("🗑️ Limpar Tudo")
964
-
965
- caso_btn_geral.click(gerar_caso_ficticio, [tipo_peticao_input_geral], [caso_input_geral])
966
- peticao_texto_input_geral.change(fornecer_dica_contextual, [peticao_texto_input_geral, tipo_peticao_input_geral], dica_contextual_output_geral)
967
- tipo_peticao_input_geral.change(fornecer_dica_contextual, [peticao_texto_input_geral, tipo_peticao_input_geral], dica_contextual_output_geral)
968
- peticao_arquivo_input_geral.change(process_file, [peticao_arquivo_input_geral], arquivo_preview_output_geral)
969
-
970
- btn_corrigir_geral.click( lambda: gr.update(value="⏳ Processando IA...", visible=True), None, processing_message_geral).then(
971
- corrigir_peticao_geral_ia,
972
- [caso_input_geral, tipo_peticao_input_geral, peticao_texto_input_geral, gr.State(None), modo_treinamento_geral],
973
- [resultado_analise_output_geral, gr.Textbox(visible=False), resultado_peticao_corrigida_output_geral,
974
- resumo_alteracoes_output_geral, criticas_output_geral, gr.Markdown(visible=False), visualizacao_detalhada_output_geral]
975
- ).then( lambda: gr.update(visible=False), None, processing_message_geral )
976
-
977
- btn_corrigir_arquivo_geral.click( lambda: gr.update(value="⏳ Processando IA...", visible=True), None, processing_message_geral).then(
978
- corrigir_peticao_geral_ia,
979
- [caso_input_geral, tipo_peticao_input_geral, gr.State(None), peticao_arquivo_input_geral, modo_treinamento_geral],
980
- [resultado_analise_output_geral, gr.Textbox(visible=False), resultado_peticao_corrigida_output_geral,
981
- resumo_alteracoes_output_geral, criticas_output_geral, gr.Markdown(visible=False), visualizacao_detalhada_output_geral]
982
- ).then( lambda: gr.update(visible=False), None, processing_message_geral )
983
-
984
- salvar_btn_geral.click( lambda: gr.update(value="⏳ Salvando...", visible=True), None, processing_message_geral).then(
985
- salvar_progresso, [caso_input_geral, peticao_texto_input_geral, tipo_peticao_input_geral],
986
- [gr.Textbox(visible=False), config_status_geral, progresso_download_file_geral]
987
- ).then( lambda x: gr.update(visible=bool(x)), [progresso_download_file_geral], [progresso_download_file_geral]
988
- ).then( lambda: gr.update(visible=False), None, processing_message_geral )
989
- carregar_btn_geral.click( lambda: gr.update(value="⏳ Carregando...", visible=True), None, processing_message_geral).then(
990
- carregar_progresso, [arquivo_progresso_upload_geral],
991
- [tipo_peticao_input_geral, caso_input_geral, peticao_texto_input_geral, config_status_geral]
992
- ).then( lambda: gr.update(visible=False), None, processing_message_geral )
993
-
994
- oab_pdf_input.change(parse_oab_pdf, [oab_pdf_input], [oab_enunciado_input, oab_gabarito_input, oab_distribuicao_input, parse_status_output])
995
- btn_gerar_modelo_oab.click( lambda: gr.update(value="⏳ Gerando Modelo...", visible=True), None, processing_message_oab).then(
996
- gerar_peticao_modelo_oab, [oab_enunciado_input, oab_gabarito_input, oab_distribuicao_input], [resultado_modelo_oab_output]
997
- ).then( lambda: gr.update(visible=False), None, processing_message_oab )
998
- btn_corrigir_pela_oab.click( lambda: gr.update(value="⏳ Corrigindo OAB...", visible=True), None, processing_message_oab).then(
999
- corrigir_pela_oab, [peticao_usuario_oab_input, oab_enunciado_input, oab_gabarito_input, oab_distribuicao_input], [resultado_correcao_oab_output]
1000
- ).then( lambda: gr.update(visible=False), None, processing_message_oab )
1001
-
1002
- clear_btn.click( fn=lambda: [
1003
- "Petição Inicial", False, None, None, None, None, "", "", "", "", "", "", "", None, None,
1004
- None, "", None, None, None, "", "", "", ""
1005
- ], inputs=None, outputs=[
1006
- tipo_peticao_input_geral, modo_treinamento_geral, caso_input_geral, peticao_texto_input_geral, peticao_arquivo_input_geral,
1007
- arquivo_preview_output_geral, processing_message_geral, resultado_analise_output_geral,
1008
- resultado_peticao_corrigida_output_geral, resumo_alteracoes_output_geral, criticas_output_geral,
1009
- visualizacao_detalhada_output_geral, dica_contextual_output_geral, arquivo_progresso_upload_geral,
1010
- progresso_download_file_geral,
1011
- oab_pdf_input, parse_status_output, oab_enunciado_input, oab_gabarito_input, oab_distribuicao_input,
1012
- peticao_usuario_oab_input, processing_message_oab, resultado_modelo_oab_output, resultado_correcao_oab_output
1013
- ])
1014
-
1015
  return demo
1016
 
1017
  if __name__ == "__main__":
 
 
 
1018
  app = interface_gradio()
1019
  app.launch()
 
21
  import random
22
  import logging
23
  import re
24
+ from fpdf import FPDF # Import FPDF
25
 
26
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
27
  logger = logging.getLogger(__name__)
 
30
 
31
  GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")
32
  if not GEMINI_API_KEY:
33
+ raise ValueError("❌ A chave GEMINI_API_KEY não foi encontrada.")
34
 
35
  GEMINI_API_URL = f"https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash-latest:generateContent?key={GEMINI_API_KEY}"
36
 
37
+ CONFIG = {"max_retry_attempts": 3, "retry_delay": 5, "timeout": 180, "temperatura": 0.3, "max_tokens": 8192}
38
+ CONFIG_OAB = {"temperatura": 0.2, "max_tokens": 8192}
 
 
 
 
 
 
 
 
 
 
 
39
  rate_limiter = RateLimiter(max_calls=30, period=60)
40
 
41
  TIPOS_DE_PETICAO = [
42
+ "Petição Inicial", "Contestação", "Reconvenção", "Impugnação à Contestação",
43
+ "Embargos de Declaração", "Recurso de Apelação", "Agravo de Instrumento",
44
+ "Petição de Tutela Provisória", "Ação de Alimentos Gravídicos",
 
 
 
 
 
 
45
  ]
46
 
47
  DICAS_OAB_MELHORADO = {
48
+ "Clareza e Concisão": "Seja direto e evite jargões excessivos.", "Fundamentação Legal": "Sempre cite lei/súmula/jurisprudência.",
49
+ "Coerência Argumentativa": "Mantenha linha lógica.", "Revisão Ortográfica e Gramatical": "Erros prejudicam credibilidade.",
50
+ "Formatação": "Parágrafos definidos, espaçamento, destaque moderado.",
51
+ "Endereçamento (Inicial)": "Verifique competência. Consumo: foro domicílio autor (Art. 101, I, CDC).",
52
+ "Qualificação (Inicial)": "Dados Art. 319, II, CPC. Use 'XXX'.", "Fatos (Inicial)": "Narrativa cronológica, clara, objetiva.",
53
+ "Pedidos (Inicial)": "Certos, determinados, líquidos. Cumule (Art. 327).", "Custas (Inicial)": "Peça juntada guia OU JG.",
54
+ "Valor da Causa (Inicial)": "Benefício econômico (Art. 292).", "Preliminares (Contestação)": "Art. 337 antes mérito.",
55
+ "Impugnação Específica (Contestação)": "Conteste cada fato (Art. 341).", "Mérito (Contestação)": "Sua versão + fundamento improcedência.",
56
+ "Pedidos (Contestação)": "Acolher prelim./improcedência total + custas/honorários.", "Pressupostos Recursais": "Cabimento, tempestiv., preparo, reg. formal, interesse.",
57
+ "Dialeticidade (Recurso)": "Impugne fundamentos decisão (Art. 1010 II/III Apel.).", "Delimitação da Matéria (Recurso)": "Indique parte recorrida.",
58
+ "Fumus Boni Iuris": "Plausibilidade direito (provas/argumentos).", "Periculum in Mora": "Risco dano irreparável/difícil reparação.",
59
+ "Reversibilidade": "Argumente reversibilidade (Art. 300, §3º).",
 
 
 
 
 
 
 
 
 
60
  }
61
 
62
  HISTORICO = []
63
  CORRECAO_CACHE = {}
64
 
65
  def preprocess_image_for_ocr(img):
66
+ 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)
67
+ except Exception as e: logger.error(f"Preprocess err: {e}"); return f"❌ Err preprocess: {e}"
 
 
 
 
 
 
68
 
69
  def extract_text_from_pdf(file_path_or_obj):
70
+ text, ocr_text, full_text = "", "", ""
 
 
71
  try:
72
  pdf_reader = pdfplumber_open(file_path_or_obj)
73
  for i, page in enumerate(pdf_reader.pages):
74
+ page_full_text = ""; page_text = page.extract_text()
75
+ if page_text: page_full_text += page_text + "\n"; text += page_text + "\n"
 
 
 
76
  else:
77
+ logger.warning(f"P{i+1} OCR try.")
78
+ try: img = page.to_image(resolution=300).original; page_ocr_text = pytesseract.image_to_string(preprocess_image_for_ocr(img), lang='por')
79
+ except Exception as ocr_setup_e: logger.error(f"Tesseract/Lang error? {ocr_setup_e}"); page_ocr_text = None # Handle potential Tesseract setup issues
80
+ if page_ocr_text and page_ocr_text.strip(): page_full_text += page_ocr_text + "\n"; ocr_text += page_ocr_text + "\n"
81
+ else: page_full_text += f"[OCR fail p{i+1}]\n"; ocr_text += f"[OCR fail p{i+1}]\n"
 
 
 
 
 
 
 
 
 
 
 
82
  full_text += page_full_text
83
  pdf_reader.close()
84
+ if full_text.strip(): logger.info("Combined PDF text."); return full_text
85
+ else: logger.warning("No PDF text."); return "⚠️ No text extracted."
 
 
 
 
 
 
86
  except Exception as e:
87
+ logger.error(f"PDF err: {e}")
88
  try:
89
+ logger.info("PyPDF2 fallback.")
90
+ fb_text = ""; reader_args = file_path_or_obj;
91
+ if hasattr(reader_args, 'seek'): reader_args.seek(0)
92
+ reader = PyPDF2.PdfReader(reader_args)
93
+ for page in reader.pages: fb_text += page.extract_text() or ""
94
+ if fb_text.strip(): logger.info("Fallback ok."); return fb_text
95
+ else: return f"❌ PDF err & fallback fail: {e}"
96
+ except Exception as fb_e: logger.error(f"Fallback err: {fb_e}"); return f"❌ Critical PDF err: {e}"
97
+
98
+ def extract_text_from_docx(f):
99
+ try: d=docx.Document(f); t="\n".join([p.text for p in d.paragraphs]); return t if t.strip() else "⚠️ DOCX empty."
100
+ except Exception as e: logger.error(f"DOCX err: {e}"); return f"❌ DOCX error: {e}"
101
+
102
+ def extract_text_from_txt(f):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
103
  try:
104
+ c = None
105
+ if isinstance(f, str):
106
+ with open(f, 'rb') as file: c = file.read()
107
+ elif hasattr(f, 'read'): c = f.read(); [getattr(f, 'seek', lambda x: None)(0)]
108
+ else: return "❌ Invalid TXT."
109
+ for enc in ['utf-8', 'latin-1', 'cp1252']:
110
+ try: dec = c.decode(enc); logger.info(f"TXT decoded {enc}."); return dec if dec.strip() else "⚠️ TXT empty."
111
+ except UnicodeDecodeError: continue
112
+ logger.error("Cannot decode TXT."); return "❌ Cannot decode TXT."
113
+ except Exception as e: logger.error(f"TXT err: {e}"); return f"❌ TXT error: {e}"
114
+
115
+ def process_file(f):
116
+ if f is None: return "⚠️ No file."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
117
  try:
118
+ fp=getattr(f,'name',None); fn=os.path.basename(fp).lower() if fp else None
119
+ if not fp or not fn: return "❌ No file info."
120
+ logger.info(f"Processing: {fn}")
 
 
 
 
 
121
  try:
122
+ fs=os.path.getsize(fp)
123
+ if fs > 15*1024*1024: return "⚠️ File >15MB."
124
+ except OSError as se: logger.warning(f"Size err: {se}")
125
+ if fn.endswith('.pdf'): return extract_text_from_pdf(fp)
126
+ if fn.endswith('.docx'): return extract_text_from_docx(fp)
127
+ if fn.endswith('.txt'): return extract_text_from_txt(fp)
128
+ return "⚠️ Format? (PDF,DOCX,TXT)."
129
+ except Exception as e: logger.error(f"File err: {e}", exc_info=True); return f"❌ File error: {e}"
 
 
 
 
 
 
 
 
 
 
 
 
 
130
 
131
  def chamar_api(prompt, use_oab_config=False):
132
+ cfg = CONFIG_OAB if use_oab_config else CONFIG
133
+ payload = {"contents": [{"parts": [{"text": prompt}]}], "generationConfig": {"temperature": cfg["temperatura"], "maxOutputTokens": cfg["max_tokens"], "topP": 0.95}}
134
+ hdrs = {"Content-Type": "application/json"}
135
+ logger.info(f"API: {len(prompt)}c. T:{cfg['temperatura']}, MaxT:{cfg['max_tokens']}")
136
+ for att in range(CONFIG["max_retry_attempts"]):
 
 
 
 
 
 
 
 
137
  try:
138
+ with rate_limiter: resp = requests.post(GEMINI_API_URL, headers=hdrs, json=payload, timeout=CONFIG["timeout"])
139
+ if resp.status_code==429:
140
+ wait=int(resp.headers.get("Retry-After", CONFIG["retry_delay"]*(att+1))); logger.warning(f"429. Wait {wait}s...");
141
+ if att < CONFIG["max_retry_attempts"]-1: time.sleep(wait); continue
142
+ else: return " 429 Error."
143
+ if resp.status_code!=200:
144
+ try: msg=resp.json().get("error",{}).get("message","Unknown")
145
+ except: msg=resp.text[:100]
146
+ logger.error(f"API Err {resp.status_code}: {msg}");
147
+ if att < CONFIG["max_retry_attempts"]-1: time.sleep(CONFIG["retry_delay"]*(att+1)); continue
148
+ return f"❌ API Err {resp.status_code}: {msg}"
149
+ data = resp.json()
150
+ 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)."
151
+ cand=data["candidates"][0]; reason=cand.get("finishReason")
152
+ if reason not in [None,"STOP","MAX_TOKENS"]:
153
+ logger.error(f"Gen stop: {reason}"); cats=[r['category'] for r in cand.get("safetyRatings",[]) if r['probability'] not in ['NEGLIGIBLE','LOW']];
154
+ return f"❌ Safety block: {reason} ({','.join(cats)})." if cats else f"❌ Gen stop: {reason}."
155
+ 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."
156
+ txt=cand["content"]["parts"][0]["text"].strip(); logger.info(f"API ok. Len: {len(txt)}."); return txt
157
+ except requests.Timeout: logger.warning(f"Timeout {att+1}."); if att<CONFIG["max_retry_attempts"]-1: time.sleep(CONFIG["retry_delay"]*(att+1)); continue; return "⏱️ Timeout."
158
+ except requests.RequestException as e: logger.error(f"Net err: {e}"); if att<CONFIG["max_retry_attempts"]-1: time.sleep(CONFIG["retry_delay"]*(att+1)); continue; return f"❌ Net error: {e}"
159
+ except json.JSONDecodeError as e: logger.error(f"JSON err: {e}. Resp: {resp.text[:100]}..."); return "❌ API JSON err."
160
+ except Exception as e: logger.exception(f"API Unexpected: {e}"); if att<CONFIG["max_retry_attempts"]-1: time.sleep(CONFIG["retry_delay"]*(att+1)); continue; return f"❌ Unexpected: {e}"
161
+ return "❌ API fail all retries."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
162
 
163
  def get_instrucoes_gerais_avaliacao(modo_treinamento=False):
164
+ base = "**Estrutura Avaliação (IA):**\n1.Análise Geral.\n2.Nota Final(0-10, justif.).\n3.Pontos Positivos.\n4.Pontos a Melhorar(TODOS, descr, fundam, impacto, sugestão).\n5.Análise Requisitos.\n\n**Critérios:** Coerência? Fundam.? Linguagem? Pedidos? **NÃO INVENTAR**(-3a-5). **NÃO IDENTIFICAR**(ZERO).\n"
165
+ if modo_treinamento: base += "**Modo Treino:** Detalhar+: Explicação, Base legal, Ex.Corrigido, Dica, Estrat.alt. Destaque inconsist. Sugira estrat.adic.\n"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
166
  return base
167
 
168
  def estruturar_prompt_analise_geral(caso, peticao, tipo_peticao, modo_treinamento=False):
169
+ instr = get_instrucoes_gerais_avaliacao(modo_treinamento)
170
+ prompt = f"## Análise Peça: {tipo_peticao} (IA)\n\n**Caso:**\n```\n{caso}\n```\n\n**Peça Aluno:**\n```\n{peticao}\n```\n\n{instr}\n\n**Reqs {tipo_peticao}(IA):**\n"
171
+ if tipo_peticao in ["Petição Inicial", "Ação de Alimentos Gravídicos"]: prompt += "- Ender/Comp(319 I; CDC 101 I; L.11804)\n- Qualif(319 II)\n- Fatos\n- Fundam(319 III)\n- Pedidos(319 IV; conversão?)\n- V.Causa(291-2; 319 V)\n- Provas(319 VI)\n- Aud(319 VII)\n- Citação\n- Docs(Proc; Custas/JG)\n- Fecho\n"
172
+ elif tipo_peticao == "Contestação": prompt += "- Ender.\n- Qualif.Réu; Proc#.\n- Tempest.\n- Prelim(337)\n- Impug.Esp(341)\n- Mérito(Defesa)\n- Fundam.\n- Pedidos(Acolh Prelim; IMPROCEDÊNCIA; Custas/Hon 85)\n- Provas.\n- Fecho.\n"
173
+ prompt += "\n\n**Instruções Finais:** Analise conf. caso/critérios. Simule OAB. Feedback construtivo. Formato solicitado."
174
+ return prompt
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
175
 
176
  def estruturar_prompt_peticao_corrigida_geral(caso, peticao_original, tipo_peticao):
177
+ prompt = f"**Caso:**\n```\n{caso}\n```\n\n**Peça Original({tipo_peticao}):**\n```\n{peticao_original}\n```\n\n**Tarefa:** Gere versão corrigida/aprimorada ({tipo_peticao}), baseada caso/problemas. Aplique práticas/reqs OAB.\n\n**Instruções Peça Corrigida({tipo_peticao}):**\n"
178
+ if tipo_peticao in ["Petição Inicial", "Ação de Alimentos Gravídicos"]: prompt += "1.Ender. ok\n2.Qualif. ok(319 II,'XXX')\n3.Fatos detalhados\n4.Fundam.(CC,CDC,CPC,L.11804)\n5.Pedidos ok(Alim.Grav prov/def;Tut Urg;Conversão L.11804)\n6.V.Causa ok\n7.Provas ok\n8.Aud ok(319 VII)\n9.Citação\n10.Custas ok(Req OU JG)\n11.Fecho OAB\n"
179
+ elif tipo_peticao == "Contestação": prompt += "1.Ender. ok\n2.Identif ok\n3.Tempest(opc)\n4.Estrut:Sínt;Prelim(337);Mérito(Impug/Def);Reconv?\n5.Pedidos ok(Acolh Prelim;IMPROCEDÊNCIA;Custas/Hon)\n6.Provas\n7.Fecho OAB\n"
180
+ prompt += "\n\n**Instrução Final:** Gere **APENAS** texto completo peça corrigida. Sem comentários extras."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
181
  return prompt
182
 
183
  def pos_corrigir_peticao(peticao_corrigida, tipo_peticao):
184
+ pc = peticao_corrigida.replace("Nestes termos, pede deferimento", "Nestes termos pede deferimento")
 
 
185
  if tipo_peticao in ["Petição Inicial", "Ação de Alimentos Gravídicos"]:
186
+ if "juntada da guia de custas" not in pc.lower() and "gratuidade de justiça" not in pc.lower():
187
+ 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 ""
188
+ pc=pfx+"\nRequer, ainda, a juntada da guia de custas devidamente recolhida."+sfx
189
+ if "audiência de conciliação" not in pc.lower() and "audiência de mediação" not in pc.lower():
190
+ 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 ""
191
+ pc=pfx+"\nOpta o Autor pela realização da audiência de conciliação ou mediação (Art. 319, VII, CPC)."+sfx
192
+ linhas=[l.strip() for l in pc.strip().split('\n') if l.strip()]
193
+ fechamento=["\nLocal, data.","ADVOGADO(A)","OAB/..."]
194
+ while linhas and any(t.lower() in linhas[-1].lower() for t in ["advogado","oab","local","data","pede deferimento","termos em que"]):
195
+ 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
196
+ if not is_close and len(linhas[-1]) > 30: break
 
 
 
 
 
 
 
 
 
 
 
197
  linhas.pop()
198
+ return "\n".join(linhas).strip()+"".join(fechamento)
199
+
200
+ def formatar_correcao(t):
201
+ 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")
202
+ linhas=t.split('\n'); fmt_linhas=[]; in_list=False
203
+ for l in linhas:
204
+ ls=l.strip()
205
+ if ls.startswith(("1.","2.","3.","4.","5.")): fmt_linhas.append(f"\n**{ls}**"); in_list=False
206
+ elif ls.startswith(("- ","* ")): fmt_linhas.append((" * " if in_list else "* ")+ls[2:]); in_list=True
207
+ elif ls.startswith(("a)","b)","c)","d)","e)")): fmt_linhas.append(f" * {ls}"); in_list=True
208
+ else: fmt_linhas.append(l); in_list=False
209
+ tf="\n".join(fmt_linhas); m=re.search(r"(\*\*Nota Final:?\*\*\s*)(\d{1,2}[,.]\d{1}|\d{1,2})",tf,re.I)
210
+ 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)
211
+ return tf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
212
 
213
  def corrigir_peticao_geral_ia(caso, tipo_peticao, peticao_texto=None, peticao_arquivo=None, modo_treinamento=False):
214
+ logger.info(f"Correção GERAL IA: {tipo_peticao}"); start=time.time()
215
+ if not caso or not caso.strip(): return "⚠️ Caso?", "", "", "", "", "", ""
216
+ if not tipo_peticao or tipo_peticao not in TIPOS_DE_PETICAO: return "⚠️ Tipo?", "", "", "", "", "", ""
 
 
 
217
  peticao_conteudo = ""
218
+ if peticao_arquivo: peticao_conteudo = process_file(peticao_arquivo)
219
+ elif peticao_texto: peticao_conteudo = peticao_texto
220
+ if not peticao_conteudo or peticao_conteudo.startswith(("❌","⚠️")): return peticao_conteudo or "⚠️ Petição?", "", "", "", "", "", ""
221
+ if any(tr in peticao_conteudo.lower() for tr in ["escreva a", "faça a"]): return random.choice(["Nope!", "Tente você!"]), "", "", "", "", "", ""
222
+ cache_key=f"geral|{tipo_peticao}|{caso[:50]}|{peticao_conteudo[:50]}|{modo_treinamento}"
223
+ 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"])
224
+ analise=chamar_api(estruturar_prompt_analise_geral(caso,peticao_conteudo,tipo_peticao,modo_treinamento),False)
225
+ if analise.startswith(("❌","⏱️")): return analise, peticao_conteudo, "", "", "", "", ""
226
+ analise_fmt=formatar_correcao(analise)
227
+ peticao_corr_bruta=chamar_api(estruturar_prompt_peticao_corrigida_geral(caso,peticao_conteudo,tipo_peticao),False)
228
+ peticao_corr_final = f"⚠️ Falha: {peticao_corr_bruta}" if peticao_corr_bruta.startswith(("","⏱️")) else pos_corrigir_peticao(peticao_corr_bruta, tipo_peticao)
229
+ aval_crit_md=""
230
+ html, resumo, crit = calcular_diff_interno(peticao_conteudo, peticao_corr_final if not peticao_corr_final.startswith("��️") else "")
231
+ 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}
232
+ HISTORICO.append(hist); HISTORICO[:] = HISTORICO[-10:]
233
+ CORRECAO_CACHE[cache_key] = {**hist, "diff_html": html}
234
+ logger.info(f"Correção GERAL IA took {time.time()-start:.2f}s")
235
+ return analise_fmt, peticao_conteudo, peticao_corr_final, resumo, crit, aval_crit_md, html
236
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
237
  def parse_oab_pdf(file_obj):
238
  if file_obj is None: return None, None, None, "⚠️ Nenhum PDF OAB."
239
  try:
240
  full_text = extract_text_from_pdf(file_obj.name)
241
+ if full_text.startswith(("❌","⚠️")): return None, None, None, full_text
242
+ e, g, d = "", "", ""; current = None; lines = full_text.split('\n')
 
 
 
 
243
  for line in lines:
244
+ ls=line.strip(); ll=ls.lower()
245
+ if ls.startswith("Enunciado"): current="e"; continue
246
+ if ls.startswith("Gabarito comentado"): current="g"; continue
247
+ if ls.startswith("Distribuição dos Pontos"): current="d"; continue
248
+ if ls.startswith("PADRÃO DE RESPOSTA – QUESTÃO"): current=None; break
249
+ if current=="e": e+=line+"\n"
250
+ elif current=="g": g+=line+"\n"
251
+ elif current=="d": d+=line+"\n"
252
+ e,g,d = e.strip(), g.strip(), d.strip()
253
+ 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]}..."
254
+ logger.info("PDF OAB parsed."); return e, g, d, "✅ PDF OAB carregado."
255
+ except Exception as ex: logger.error(f"Parse OAB err: {ex}", exc_info=True); return None,None,None,f"❌ Erro PDF OAB: {ex}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
256
 
257
  def estruturar_prompt_geracao_modelo_oab(enunciado, gabarito, distribuicao):
258
+ 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.
259
+
260
+ **Enunciado:**\n```\n{enunciado}\n```
261
+ **Gabarito Comentado (DESENVOLVER):**\n```\n{gabarito}\n```
262
+ **Distribuição dos Pontos (INCLUIR):**\n```\n{distribuicao}\n```
263
+
264
+ **Instruções Detalhadas:**
265
+ 1. **Qualificação:** Completa (319 II, 'XXX' se ausente).
266
+ 2. **Fatos:** Narre **detalhadamente** do Enunciado (datas, locais, indícios, necessidade).
267
+ 3. **Fundamentação Jurídica:** **Desenvolva** argumentos do Gabarito (Lei, binômio), **conectando aos fatos**. Cite artigos.
268
+ 4. **Tutela de Urgência:** Se aplicável, **argumente** requisitos (fumus/periculum) com fatos.
269
+ 5. **Pedidos:** **Todos** da Distribuição/Gabarito, claros/específicos (citação, provas, MP, JG, alimentos, conversão, custas/honorários).
270
+ 6. **Valor da Causa:** Atribua e justifique se possível.
271
+ 7. **Estrutura:** Lógica (Ender., Qualif., Fatos, Direito, Tutela, Pedidos, Valor, Fecho).
272
+ 8. **Linguagem:** Formal OAB.
273
+ 9. **Finalização:** "Local, data.", "ADVOGADO(A)", "OAB/...". Não se identifique.
274
+ 10. **Exclusividade:** Baseie-se **somente** nos textos fornecidos.
275
+ 11. **Output:** **Apenas** texto completo da peça. Sem comentários extras.
 
 
 
 
 
 
 
 
276
  """
277
  return prompt
278
 
279
  def gerar_peticao_modelo_oab(enunciado, gabarito, distribuicao):
280
+ if not all([enunciado, gabarito, distribuicao]): return "⚠️ E, G, D necessários."
281
+ logger.info("Gerando modelo OAB...")
 
 
282
  prompt = estruturar_prompt_geracao_modelo_oab(enunciado, gabarito, distribuicao)
283
+ modelo = chamar_api(prompt, use_oab_config=True)
284
+ if modelo.startswith(("❌","⏱️")): logger.error(f"Erro gerar modelo OAB: {modelo}"); return f"❌ Falha: {modelo}"
285
+ logger.info("Modelo OAB gerado."); return pos_corrigir_peticao(modelo, "Petição Desconhecida")
 
 
 
 
 
 
286
 
287
  def estruturar_prompt_correcao_pela_oab(peticao_usuario, enunciado, gabarito, distribuicao):
288
+ prompt = f"""**Tarefa:** Avalie **Peça do Usuário** vs padrão OAB (Gabarito, Distribuição) p/ **Enunciado**.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
289
 
290
+ **Enunciado:**\n```\n{enunciado}\n```
291
+ **Gabarito (Esperado):**\n```\n{gabarito}\n```
292
+ **Distribuição (Critérios):**\n```\n{distribuicao}\n```
293
+ **Peça Usuário (Avaliar):**\n```\n{peticao_usuario}\n```
294
 
295
  **Instruções:**
296
+ 1. **Avaliação Pontual:** Para **cada item** Distribuição, verifique se Usuário abordou conf. Gabarito. Atribua pontuação.
297
+ 2. **Justificativa:** Breve p/ cada item.
298
+ 3. **Feedback:** Erros/omissões vs padrão.
299
+ 4. **Peça Corrigida (Opcional):** Sugira trechos corrigidos p/ alinhar ao padrão.
300
  5. **Output Markdown:**
 
301
  ```markdown
302
+ ### 📊 Avaliação Conforme Padrão OAB
 
303
  **Pontuação por Item:**
 
304
  * **[Item 1 Distribuição]:** [Pontos Obtidos] / [Máximo]
305
  * *Justificativa:* [...]
306
+ * ... (Todos itens)
 
 
 
307
  **Pontuação Total Estimada:** [Soma Obtidos] / [Soma Máximos]
308
+ **Comentários e Pontos de Melhoria:** * [...]
 
 
 
309
  ---
310
+ ### ✨ Sugestões Correção
311
+ [Sugestões]
 
312
  ```
313
+ **Importante:** Baseie **toda** avaliação **exclusivamente** materiais OAB.
314
  """
315
  return prompt
316
 
317
  def corrigir_pela_oab(peticao_usuario, enunciado, gabarito, distribuicao):
318
+ if not all([peticao_usuario, enunciado, gabarito, distribuicao]): return "⚠️ Inputs OAB necessários."
 
 
319
  logger.info("Corrigindo pela OAB...")
320
  prompt = estruturar_prompt_correcao_pela_oab(peticao_usuario, enunciado, gabarito, distribuicao)
321
+ correcao = chamar_api(prompt, use_oab_config=True)
322
+ if correcao.startswith(("❌","⏱️")): logger.error(f"Erro corrigir OAB: {correcao}"); return f"❌ Falha OAB: {correcao}"
323
+ logger.info("Correção OAB concluída."); return correcao
324
+
325
+ def fornecer_dica_contextual(texto_peticao, tipo_peticao):
326
+ if not texto_peticao or not texto_peticao.strip(): return "Digite algo..."
327
+ dicas = []; txt_low = texto_peticao.lower()
328
+ for k, d in DICAS_OAB_MELHORADO.items():
329
+ 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}")
330
+ 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"]}
331
+ if tipo_peticao in kws:
332
+ for kw in kws[tipo_peticao]:
333
+ if kw in txt_low:
334
+ for k,d in DICAS_OAB_MELHORADO.items():
335
+ kl,tl=k.lower(),tipo_peticao.split()[0].lower() if tipo_peticao else ""
336
+ if f"({tl})" in kl or kw in kl: df=f"💡 **{k}:** {d}"; [dicas.append(df) if df not in dicas else None]
337
+ return "\n\n".join(dicas[:5]) if dicas else "💡 Sem dicas específicas."
338
+
339
+ def calcular_diff_interno(t1, t2):
340
+ if not t1 or not t2 or t2.startswith(("⚠️","❌")): return "","",""
341
+ l1,l2=t1.splitlines(),t2.splitlines();diff=difflib.ndiff(l1,l2);html,crit,lr,la=[],[],0,0
342
+ kw_rem=["invenção","identificação","erro crasso"]; kw_add=["custas","art. 319","art. 337","lei 11.804"]
343
+ for ln in diff:
344
+ txt=ln[2:];esc=escape(txt)
345
+ if ln.startswith(' '):html.append(f"<div class='l-i'>{esc}</div>")
346
+ elif ln.startswith('- '):lr+=1;html.append(f"<div class='l-r'><del>{esc}</del></div>");[crit.append(f"<span style='color:red'>🔴 REM:</span> {esc}") for k in kw_rem if k in txt.lower()]
347
+ elif ln.startswith('+ '):la+=1;html.append(f"<div class='l-a'><ins>{esc}</ins></div>");[crit.append(f"<span style='color:green'>🟢 ADD:</span> {esc}") for k in kw_add if k in txt.lower()]
348
+ 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."
349
+ css="<style>.l-i{color:#555}.l-r{background:#ffebee;color:#c62828;text-decoration:line-through}.l-a{background:#e8f5e9;color:#2e7d32}.l-r del,.l-a ins{text-decoration:none;padding:1px 3px;border-radius:3px}.l-r del{background:#ffcdd2}.l-a ins{background:#c8e6c9}</style>"
350
+ diff_html=f"{css}<div style='font-family:monospace;white-space:pre-wrap;border:1px solid #ccc;padding:10px;background:#f9f9f9;'>{''.join(html)}</div>"
351
+ return diff_html,res,crit_t
352
 
353
  def calcular_progresso():
354
  if not HISTORICO: return "📊 Progresso vazio."
355
+ notas,tipos,count=[],set(),0
356
+ for e in HISTORICO:
357
+ 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",""));
358
+ if m: try: notas.append(float(m.group(1).replace(",","."))) except: pass
359
+ media=sum(notas)/len(notas) if notas else 0
360
+ return f"**📊 Progresso (IA):** Corrs:{count}, Tipos:{', '.join(sorted(list(tipos)))}, Média:{media:.1f}/10"
 
 
 
 
 
 
 
 
361
 
362
  def mostrar_historico():
363
  if not HISTORICO: return "📜 Histórico vazio."
364
+ md="### 📜 Histórico (Últimas 10)\n\n---\n"
365
+ for i, e in enumerate(reversed(HISTORICO)):
366
+ modo=e.get("modo","Geral IA"); nota="N/A"; m=re.search(r"\*\*(\d{1,2}[,.]?\d?)/10\*\*",e.get("analise_formatada",""))
367
+ if m: nota=f"{m.group(1).replace(',','.')}/10"
368
+ md+=f"**{len(HISTORICO)-i}.** {e['data']} ({modo}) | **Tipo:** {e['tipo_peticao']} | **Nota IA:** {nota}\n**Caso:** _{e['caso'][:60]}..._\n---\n"
369
+ return md
 
 
 
 
370
 
371
  def salvar_progresso(caso, peticao, tipo_peticao):
372
+ logger.info("Salvando...")
373
+ try:
374
+ if not all([caso, peticao, tipo_peticao]): return "", "⚠️ Inputs?", None
375
+ 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"
376
+ fpath=os.path.join(tempfile.gettempdir(),fname);
377
+ with open(fpath,'w',encoding='utf-8') as f: f.write(js)
378
+ logger.info(f"Salvo: {fpath}"); return js, f"✅ Salvo `{fname}`", fpath
379
+ except Exception as e: logger.error(f"Salvar err: {e}"); return "", f"❌ Erro: {e}", None
380
+
381
+ def carregar_progresso(fobj):
382
+ logger.info("Carregando...")
383
+ if fobj is None: return "", "", "", "⚠️ Nenhum arq."
384
+ try:
385
+ with open(fobj.name,'r',encoding='utf-8') as f: d=json.load(f)
386
+ c,p,t=d.get("caso"), d.get("peticao"), d.get("tipo_peticao")
387
+ if not all([c,p,t]): return "", "", "", "❌ Arq inválido."
388
+ if t not in TIPOS_DE_PETICAO: return c,p,"",f"⚠️ Tipo '{t}' inválido."
389
+ logger.info("Carregado."); return t, c, p, "✅ Carregado!"
390
+ except Exception as e: logger.error(f"Carregar err: {e}"); return "", "", "", f"❌ Erro: {e}"
391
+
392
+ def gerar_caso_ficticio(tipo_peticao="Petição Inicial"):
393
+ if not tipo_peticao: return "Erro: Tipo?"
394
+ logger.info(f"Gerando caso: {tipo_peticao}")
395
+ prompt=estruturar_prompt_geracao_caso(tipo_peticao); caso=chamar_api(prompt,False)
396
+ if caso.startswith(("❌","⏱️")): return f"Falha:\n{caso}"
397
+ final="**Situação-Problema:** Na qualidade de advogado(a) de";
398
+ 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})."
399
+ logger.info("Caso gerado."); return caso
400
+
401
+ def export_to_txt(content, filename_prefix):
402
+ if not content: return None, "⚠️ Conteúdo vazio para exportar."
403
  try:
 
 
 
404
  ts = datetime.now().strftime("%Y%m%d_%H%M%S")
405
+ filename = f"{filename_prefix}_{ts}.txt"
406
+ temp_dir = tempfile.gettempdir()
407
+ file_path = os.path.join(temp_dir, filename)
408
+ with open(file_path, 'w', encoding='utf-8') as f:
409
+ f.write(content)
410
+ logger.info(f"Exportado para TXT: {file_path}")
411
+ return file_path, f"✅ Exportado para {filename}"
412
  except Exception as e:
413
+ logger.error(f"Erro exportar TXT: {e}")
414
+ return None, f"❌ Erro TXT: {e}"
415
 
416
+ def export_to_pdf(content, filename_prefix):
417
+ if not content: return None, "⚠️ Conteúdo vazio para exportar."
 
418
  try:
419
+ ts = datetime.now().strftime("%Y%m%d_%H%M%S")
420
+ filename = f"{filename_prefix}_{ts}.pdf"
421
+ temp_dir = tempfile.gettempdir()
422
+ file_path = os.path.join(temp_dir, filename)
 
 
 
 
 
 
 
 
423
 
424
+ pdf = FPDF()
425
+ pdf.add_page()
426
+ # Use a font that supports UTF-8 characters, handle potential missing font file
427
+ try:
428
+ # Try adding DejaVu, assuming the .ttf file is accessible
429
+ # Download from: https://dejavu-fonts.github.io/
430
+ # Place DejaVuSans.ttf in the same directory or provide the full path
431
+ pdf.add_font('DejaVu', '', 'DejaVuSans.ttf', uni=True)
432
+ pdf.set_font('DejaVu', size=11)
433
+ except RuntimeError:
434
+ logger.warning("Fonte DejaVuSans.ttf não encontrada. Usando Arial (pode ter problemas com caracteres especiais).")
435
+ pdf.set_font('Arial', size=11)
436
+
437
+ # Use multi_cell for automatic line breaks and UTF-8 handling
438
+ # Encode to latin-1 with error replacement if using standard fonts without proper UTF-8 support
439
+ # If using DejaVu, encoding might not be strictly necessary but good practice
440
+ try:
441
+ # Use write for better Markdown basic handling (bold/italic if font supports)
442
+ pdf.write(h=5, txt=content) # h is line height
443
+ except UnicodeEncodeError:
444
+ # Fallback if write fails with current font
445
+ pdf.multi_cell(0, 5, txt=content.encode('latin-1', 'replace').decode('latin-1'))
446
 
447
+
448
+ pdf.output(file_path)
449
+ logger.info(f"Exportado para PDF: {file_path}")
450
+ return file_path, f"✅ Exportado para {filename}"
451
+ except Exception as e:
452
+ logger.error(f"Erro exportar PDF: {e}", exc_info=True)
453
+ return None, f" Erro PDF: {e}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
454
 
455
  def estruturar_prompt_geracao_caso(tipo_peticao="Petição Inicial"):
456
+ 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"
457
+ if tipo_peticao in ["Petição Inicial","Ação de Alimentos Gravídicos"]: prompt+="* Descreva violação direitos, busca tutela. Elem. p/ danos. Qualif. partes. Temas:consumo,contrato,cobrança,despejo,família(alim.grav:indícios patern.,necess. mãe/nascituro)."
458
+ 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."
459
+ else: prompt+="* Cenário cível c/ conflito claro."
460
+ 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.'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
461
  return prompt
462
 
 
 
463
  def interface_gradio():
464
  with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="orange")) as demo:
465
  gr.Markdown("# ⚖️ Corretor Jurídico OAB Master ⚖️")
466
+ gr.Markdown("Use **Correção Geral (IA)** ou **Correção Padrão OAB**.")
 
467
  with gr.Tabs() as main_tabs:
468
  with gr.TabItem("🤖 Correção Geral (IA)", id=0):
469
  with gr.Row():
470
  with gr.Column(scale=1):
471
+ gr.Markdown("### 1. Configs (Geral)")
472
+ tipo_peticao_input_geral=gr.Dropdown("Tipo Peça", choices=TIPOS_DE_PETICAO, value="Petição Inicial")
473
+ modo_treinamento_geral=gr.Checkbox("🎓 Treinamento", value=False)
474
+ with gr.Accordion("⚙️ IA", open=False):
475
+ temp_s=gr.Slider(0.0, 1.0, value=CONFIG["temperatura"], step=0.1, label="Criatividade")
476
+ tout_s=gr.Slider(30, 300, value=CONFIG["timeout"], step=10, label="Timeout(s)")
477
+ maxt_s=gr.Slider(2048, 8192, value=CONFIG["max_tokens"], step=256, label="MaxTokens")
478
+ def upd_cfg(t,o,m): CONFIG["temperatura"],CONFIG["timeout"],CONFIG["max_tokens"]=t,o,m; return "✅"
479
+ cfg_btn=gr.Button("💾 Salvar"); cfg_stat=gr.Textbox("Status", interactive=False)
480
+ cfg_btn.click(upd_cfg, [temp_s, tout_s, maxt_s], cfg_stat)
481
+ 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)
482
+ 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)
 
 
 
 
 
 
 
 
 
 
 
 
483
  with gr.Column(scale=3):
484
+ gr.Markdown("### 2. Caso e Peça")
485
+ with gr.Row(): caso_in=gr.Textbox("📋 Caso", lines=8); caso_btn=gr.Button("🎲",variant="secondary",scale=0)
 
 
486
  with gr.Tabs():
487
+ 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")
488
+ 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")
489
+ msg_geral=gr.Markdown("", visible=False)
 
 
 
 
 
 
 
490
  with gr.Tabs():
491
+ 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)
492
+ 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)
493
+ 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()
 
 
 
 
 
494
  with gr.TabItem("📄 Correção Padrão OAB", id=1):
495
+ gr.Markdown("Forneça material oficial (PDF/texto) e sua peça.")
496
  with gr.Row():
497
  with gr.Column(scale=2):
498
+ gr.Markdown("### 1. Material OAB");
499
+ 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)
 
 
 
 
 
 
 
500
  with gr.Column(scale=2):
501
+ 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")
502
+ msg_oab=gr.Markdown("", visible=False)
 
 
 
 
 
503
  with gr.Tabs():
504
+ 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)
505
+ 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)
506
+ clr_btn = gr.Button("🗑️ Limpar Tudo")
507
+
508
+ caso_btn.click(gerar_caso_ficticio, [tipo_peticao_input_geral], [caso_in])
509
+ pet_txt_in.change(fornecer_dica_contextual, [pet_txt_in, tipo_peticao_input_geral], dicas_out)
510
+ tipo_peticao_input_geral.change(fornecer_dica_contextual, [pet_txt_in, tipo_peticao_input_geral], dicas_out)
511
+ pet_f_in.change(process_file, [pet_f_in], prev_out)
512
+
513
+ corr_inputs=[caso_in, tipo_peticao_input_geral, pet_txt_in, gr.State(None), modo_treinamento_geral]
514
+ corr_outputs=[an_out, gr.Textbox(visible=False), corr_out, res_out, crit_out, gr.Markdown(visible=False), diff_out]
515
+ 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)
516
+ corr_f_inputs=[caso_in, tipo_peticao_input_geral, gr.State(None), pet_f_in, modo_treinamento_geral]
517
+ 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)
518
+
519
+ 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)
520
+ 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)
521
+
522
+ oab_pdf.change(parse_oab_pdf, [oab_pdf], [oab_e, oab_g, oab_d, parse_st])
523
+ 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)
524
+ 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)
525
+
526
+ 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
527
+ 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])
528
+ 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])
529
+ 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])
530
+ 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])
531
+ 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])
532
+ 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])
533
+ 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])
534
+ 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])
535
+
536
+ clr_btn.click(lambda: ["Petição Inicial",False,None,None,None,None,"","","","","","",None,None,None,None,"",None,None,None,"","","","",None,None,None,None], None, [
537
+ 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,
538
+ 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])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
539
  return demo
540
 
541
  if __name__ == "__main__":
542
+ # Ensure Tesseract path is set if needed, or handle OCR errors gracefully
543
+ # try: pytesseract.pytesseract.tesseract_cmd = '/usr/bin/tesseract' # Example for Linux
544
+ # except: logger.warning("Tesseract not configured/found. OCR might fail.")
545
  app = interface_gradio()
546
  app.launch()