TutelaSpace / src /logic.py
jsant2608's picture
Actualizo logica a nivel de prompt engineering
bd64763
"""
Este m贸dulo contiene toda la l贸gica de negocio para la aplicaci贸n de contestaci贸n de tutelas.
Se encarga de interactuar con la API de Gemini, procesar los archivos PDF y DOCX,
y orquestar el flujo de trabajo completo.
"""
import os
import json
import re
import tempfile
import pathlib
from docx import Document
from docx.shared import Inches
import google.generativeai as genai
import gradio as gr
# --- CONFIGURACI脫N GLOBAL ---
API_KEY = os.environ.get("GEMINI_API_KEY")
if API_KEY:
genai.configure(api_key=API_KEY)
EXAMPLE_PDF_PATH = "ejemplo_tutela.pdf"
# --- PLANTILLAS DE PROMPTS ---
EXTRACT_PROMPT = '''
Del siguiente documento de acci贸n de tutela, extrae de forma precisa y concisa la siguiente informaci贸n:
1. **juzgado**: Nombre completo del juzgado al que se dirige la tutela.
2. **ciudad**: La ciudad donde se presenta la tutela.
3. **radicado**: El n煤mero de radicado o referencia del caso.
4. **accionante**: El nombre completo del demandante o accionante.
5. **direccion_accionante**: La direcci贸n f铆sica del accionante.
6. **email**: La lista de correos electr贸nicos de notificaci贸n.
7. **datos**: Una lista de las partes involucradas (demandante, demandado).
8. **hechos**: Un resumen claro y numerado de los hechos que motivan la tutela.
9. **argumentos**: Un resumen de los argumentos legales del accionante.
10. **peticiones**: Un resumen de lo que el accionante solicita al juez.
Devuelve la respuesta como un objeto JSON v谩lido con las claves especificadas.
'''
ENCABEZADO_PROMPT = '''
Con base en el documento de ejemplo, redacta el encabezado para una contestaci贸n de tutela.
Usa el mismo estilo, tono y formato del ejemplo.
Informaci贸n de la tutela a contestar:
- Juzgado: {juzgado}
- Ciudad: {ciudad}
- Radicado: {radicado}
- Accionante: {accionante}
- Accionado: (El accionado es la entidad que representas, no lo inventes, usa un placeholder como [Nombre de la Entidad Accionada])
Devuelve un JSON con la clave "encabezado" y el texto correspondiente.
'''
HECHOS_PROMPT = '''
Actuando como un abogado experto, redacta la secci贸n de \'HECHOS\' para una contestaci贸n de tutela, bas谩ndote estrictamente en el estilo y formato del documento de ejemplo.
**Instrucci贸n clave: Debes responder a CADA UNO de los hechos numerados presentados por el accionante.** La respuesta debe seguir el formato "Al hecho PRIMERO, es cierto.", "Al hecho SEGUNDO, no es cierto por...", o una variaci贸n similar que se alinee con el ejemplo.
**Hechos del Accionante a Responder:**
{hechos}
**Contexto General de las Partes:**
{datos}
Devuelve un JSON v谩lido con una 煤nica clave "hechos" que contenga el texto completo de la secci贸n.
'''
FUNDAMENTOS_PROMPT = '''
Actuando como un abogado experto, redacta la secci贸n de \'FUNDAMENTOS DE DERECHO\' para una contestaci贸n de tutela, bas谩ndote estrictamente en el estilo y formato del documento de ejemplo.
**Instrucci贸n clave: Debes rebatir, uno por uno, los argumentos legales del accionante.** La estructura de tu respuesta debe reflejar una contra-argumentaci贸n directa a cada punto presentado.
**Argumentos del Accionante a Rebatir:**
{argumentos}
**Contexto General de las Partes:**
{datos}
Devuelve un JSON v谩lido con una 煤nica clave "fundamentos" que contenga el texto completo de la secci贸n.
'''
PETICIONES_PROMPT = '''
Actuando como un abogado experto, redacta la secci贸n de \'PETICIONES\' para una contestaci贸n de tutela, bas谩ndote estrictamente en el estilo y formato del documento de ejemplo.
**Instrucci贸n clave: Debes pronunciarte sobre CADA UNA de las peticiones del accionante.** La respuesta debe ser una oposici贸n directa y formal a sus pretensiones, siguiendo el tono del ejemplo.
**Peticiones del Accionante a Responder:**
{peticiones}
**Contexto General de las Partes:**
{datos}
Devuelve un JSON v谩lido con una 煤nica clave "peticiones" que contenga el texto completo de la secci贸n.
'''
# --- FUNCIONES AUXILIARES ---
def _safe_json_loads(text: str) -> dict:
"""
Analiza de forma robusta un string que deber铆a ser JSON.
Intenta limpiar el texto y encontrar un objeto JSON v谩lido.
"""
if not isinstance(text, str):
text = str(text)
# Elimina las vallas de formato de c贸digo (```json ... ```) si existen.
text = re.sub(r"^```(?:json)?\s*|\s*```$", "", text, flags=re.S).strip()
try:
return json.loads(text)
except json.JSONDecodeError as e:
# Si falla, intenta encontrar el primer y m谩s grande bloque JSON (entre { y }).
match = re.search(r"\{.*\}", text, re.DOTALL)
if match:
json_like_string = match.group(0)
try:
return json.loads(json_like_string)
except json.JSONDecodeError:
raise ValueError(f"Error al decodificar el JSON extra铆do: {json_like_string}") from e
raise ValueError(f"No se pudo encontrar un objeto JSON v谩lido en la respuesta: {text}") from e
def _safe_json_field(raw, field):
"""Extrae de forma segura un campo de una respuesta JSON."""
if raw is None:
return ""
if isinstance(raw, list):
return str(raw)
if isinstance(raw, dict):
return str(raw.get(field, ""))
try:
obj = _safe_json_loads(str(raw))
if isinstance(obj, dict):
return str(obj.get(field, ""))
return str(obj) # Devuelve el objeto parseado si no es un diccionario
except ValueError:
return str(raw) # Si todo falla, devuelve el texto original
def _add_section(doc: Document, title: str, body: str):
"""A帽ade una secci贸n con t铆tulo y cuerpo a un documento de Word."""
doc.add_heading(title, level=1)
body = str(body) if body is not None else ""
blocks = [b.strip() for b in body.strip().split("\n\n") if b.strip()]
if not blocks:
doc.add_paragraph("(sin contenido)")
return
for b in blocks:
doc.add_paragraph(b)
# --- L脫GICA PRINCIPAL CON LA API DE GEMINI ---
def extract_tutela_parts(uploaded_file_path: str) -> dict:
"""Usa Gemini para extraer las partes clave de una tutela."""
model = genai.GenerativeModel("gemini-1.5-flash")
uploaded_file = genai.upload_file(path=uploaded_file_path, display_name="Tutela PDF")
try:
response = model.generate_content(
[EXTRACT_PROMPT, uploaded_file],
generation_config=genai.types.GenerationConfig(
response_mime_type="application/json",
response_schema={
"type": "object",
"properties": {
"datos": {"type": "array", "items": {"type": "string"}},
"hechos": {"type": "array", "items": {"type": "string"}},
"argumentos": {"type": "array", "items": {"type": "string"}},
"peticiones": {"type": "array", "items": {"type": "string"}},
"juzgado": {"type": "string"},
"direccion_juzgado": {"type": "string"},
"email": {"type": "array", "items": {"type": "string"}},
"radicado": {"type": "string"},
"accionante": {"type": "string"},
"direccion_accionante": {"type": "string"},
"ciudad": {"type": "string"}
},
"required": ["juzgado", "ciudad", "radicado", "accionante", "hechos", "argumentos", "peticiones"]
}
)
)
return _safe_json_loads(response.text)
finally:
genai.delete_file(uploaded_file.name)
def generate_response_section(section_name: str, prompt_template: str, context_data: dict) -> str:
"""Funci贸n gen茅rica para generar una secci贸n de la contestaci贸n."""
model = genai.GenerativeModel("gemini-1.5-flash")
example_filepath = pathlib.Path(EXAMPLE_PDF_PATH)
if not example_filepath.is_file():
raise FileNotFoundError(f"El archivo de ejemplo '{EXAMPLE_PDF_PATH}' no se encontr贸.")
system_instruction = prompt_template.format(**context_data)
example_file = genai.upload_file(path=str(example_filepath), display_name="Ejemplo Tutela")
try:
response = model.generate_content(
[system_instruction, example_file],
generation_config=genai.types.GenerationConfig(
response_mime_type="application/json",
response_schema={"type": "object", "properties": {section_name: {"type": "string"}}}
)
)
return _safe_json_field(response.text, section_name)
finally:
try:
genai.delete_file(example_file.name)
except Exception:
pass
def write_tutela_docx(encabezado: str, hechos: str, peticiones: str, fundamentos: str) -> str:
"""Crea un archivo .docx con el encabezado y las secciones generadas."""
doc = Document()
for s in doc.sections:
s.top_margin, s.bottom_margin, s.left_margin, s.right_margin = [Inches(1)] * 4
# A帽adir encabezado
if encabezado:
doc.add_paragraph(encabezado)
doc.add_paragraph("\n") # Espacio
_add_section(doc, "HECHOS", hechos)
_add_section(doc, "FUNDAMENTOS DE DERECHO", fundamentos)
_add_section(doc, "PETICIONES", peticiones)
with tempfile.NamedTemporaryFile(delete=False, suffix=".docx") as tmp:
doc.save(tmp.name)
return tmp.name
# --- FUNCI脫N PRINCIPAL DEL FLUJO DE TRABAJO ---
def full_workflow(uploaded_file_path: str):
"""Orquesta todo el proceso: desde la subida del PDF hasta la generaci贸n del DOCX."""
if not uploaded_file_path:
raise gr.Error("Por favor, sube un archivo de tutela en formato PDF.")
try:
# 1. Extraer las partes de la tutela del usuario.
extracted_data = extract_tutela_parts(uploaded_file_path)
# 2. Generar cada secci贸n de la contestaci贸n.
encabezado_generado = generate_response_section("encabezado", ENCABEZADO_PROMPT, extracted_data)
hechos_generados = generate_response_section("hechos", HECHOS_PROMPT, extracted_data)
fundamentos_generados = generate_response_section("fundamentos", FUNDAMENTOS_PROMPT, extracted_data)
peticiones_generadas = generate_response_section("peticiones", PETICIONES_PROMPT, extracted_data)
# 3. Crear el archivo .docx con los textos generados.
docx_path = write_tutela_docx(encabezado_generado, hechos_generados, peticiones_generadas, fundamentos_generados)
# 4. Devolver todos los resultados a la interfaz de Gradio.
return hechos_generados, fundamentos_generados, peticiones_generadas, docx_path
except FileNotFoundError as e:
print(f"Error: {e}")
raise gr.Error(f"Error de configuraci贸n del servidor: {e}")
except Exception as e:
print(f"An unexpected error occurred: {e}")
raise gr.Error(f"Ha ocurrido un error inesperado: {e}")