Skip to content

Repository files navigation

LegalMove — Agente Autónomo de Comparación de Contratos

Pipeline multiagente en Python que recibe dos imágenes (contrato original y adenda), transcribe ambos documentos con visión de OpenAI, construye un mapa contextual y devuelve los cambios en un JSON estricto validado con Pydantic. Cada etapa queda registrada como una observación anidada en Langfuse.

Este proyecto asiste la revisión contractual; no reemplaza el criterio de un profesional legal. Los documentos de data/test_contracts/ son ficticios.

Arquitectura

flowchart LR
    A["PNG/JPEG<br/>Contrato original"] --> V1["GPT-4o Vision<br/>Transcripción fiel"]
    B["PNG/JPEG<br/>Adenda"] --> V2["GPT-4o Vision<br/>Transcripción fiel"]
    V1 --> C["Agente 1<br/>Contextualización"]
    V2 --> C
    C --> D["Mapa estructural<br/>y correspondencias"]
    V1 --> E["Agente 2<br/>Extracción"]
    V2 --> E
    D --> E
    E --> P["Pydantic<br/>ContractChangeOutput"]
    P --> J["JSON validado"]
Loading

La separación de responsabilidades reduce falsos positivos: el primer agente solo alinea la estructura y nunca decide cambios; el segundo es el único que clasifica adiciones, eliminaciones y modificaciones. El límite final vuelve a validar el JSON aunque OpenAI Structured Outputs ya haya aplicado el schema.

La posta entre los dos agentes (Agente 1 → Agente 2) la resuelve LangChain: src/agents/pipeline.py los compone con el operador | en una RunnableSequence real (LCEL). main.py construye esa cadena una sola vez y la invoca de punta a punta — no llama a cada agente a mano ni encadena su output manualmente.

Estructura

src/
├── main.py
├── image_parser.py
├── models.py
├── config.py
├── observability.py
└── agents/
    ├── contextualization_agent.py
    ├── extraction_agent.py
    └── pipeline.py
data/test_contracts/
├── pair_1_original.png
├── pair_1_amendment.png
├── pair_2_original.png
└── pair_2_amendment.png
tests/
scripts/generate_test_images.py
requirements.txt
.env.example

Requisitos

  • Python 3.10 o superior.
  • API key de OpenAI con acceso a gpt-4o.
  • Proyecto de Langfuse (opcional para ejecución local, requerido para enviar trazas al dashboard).

Instalación

python -m venv .venv
source .venv/bin/activate
python -m pip install -r requirements.txt
cp .env.example .env

Completá .env:

OPENAI_API_KEY=your-key-here
OPENAI_MODEL=gpt-4o
OPENAI_TIMEOUT_SECONDS=60
OPENAI_MAX_RETRIES=2
LANGFUSE_PUBLIC_KEY=pk-lf-xxx
LANGFUSE_SECRET_KEY=sk-lf-xxx
LANGFUSE_HOST=https://cloud.langfuse.com
LANGFUSE_CAPTURE_CONTENT=true

LANGFUSE_CAPTURE_CONTENT=false conserva métricas, tokens y jerarquía pero redacta el contenido contractual en todos los spans: los textos transcriptos, el resultado final del análisis y los mensajes de las excepciones que pudieran incluir fragmentos del contrato. Es útil cuando la política de privacidad no permite enviar el contenido a la plataforma de observabilidad.

Uso

Par simple:

python src/main.py \
  data/test_contracts/pair_1_original.png \
  data/test_contracts/pair_1_amendment.png

Par complejo:

python src/main.py \
  data/test_contracts/pair_2_original.png \
  data/test_contracts/pair_2_amendment.png

Para una corrida sin Langfuse:

python src/main.py contrato.png adenda.png --no-langfuse

La salida estándar contiene únicamente el JSON final:

{
  "sections_changed": [
    "3. HONORARIOS",
    "6. VIGENCIA"
  ],
  "topics_touched": [
    "Compensación",
    "Plazo contractual"
  ],
  "summary_of_the_change": "MODIFICACIÓN: el honorario mensual cambia de USD 5.000 a USD 6.500. MODIFICACIÓN: el vencimiento se extiende del 31 de diciembre de 2025 al 30 de junio de 2026."
}

Trazabilidad

Una ejecución genera esta jerarquía:

contract-analysis
├── parse_original_contract
├── parse_amendment_contract
├── contextualization_agent
└── extraction_agent

El span raíz registra paths, modelo, versión del pipeline y JSON final. Cada generación hija registra input, output, latencia automática, modelo, ID de respuesta y uso de tokens cuando el proveedor lo devuelve. Las excepciones se marcan con nivel ERROR; flush() se ejecuta incluso si el pipeline falla.

Manejo de errores de API

Tanto la llamada de visión (client.responses.create en image_parser.py) como las dos llamadas de LangChain (ChatOpenAI.invoke en contextualization_agent.py y extraction_agent.py) están envueltas en except openai.APIError. src/api_errors.py traduce cada subtipo a un mensaje accionable en vez de dejar pasar la excepción cruda del SDK o un except Exception genérico:

  • RateLimitError (429) → sugiere esperar y revisar la cuota.
  • APITimeoutError → indica que se puede subir OPENAI_TIMEOUT_SECONDS.
  • BadRequestError con code=context_length_exceeded → avisa que el contrato transcripto excede la ventana de contexto del modelo.
  • AuthenticationError, APIConnectionError y el resto de APIStatusError tienen su propio mensaje.

OPENAI_TIMEOUT_SECONDS y OPENAI_MAX_RETRIES (.env.example) configuran el timeout y los reintentos automáticos tanto del cliente de visión como de los dos agentes; nunca están hardcodeados en el código.

Modelo de salida

ContractChangeOutput rechaza campos extra, strings vacíos y tipos coercionados:

  • sections_changed: list[str]
  • topics_touched: list[str]
  • summary_of_the_change: str

Los elementos duplicados en las listas se eliminan conservando el orden. Una adenda sin cambios expresos puede devolver listas vacías, pero el resumen debe explicar ese resultado.

Tests

Instalá dependencias de desarrollo y ejecutá:

python -m pip install -r requirements-dev.txt
pytest

Los tests no consumen la API: usan clientes y respuestas simuladas para verificar base64, prompts, structured output, validación y flush() de Langfuse.

Para regenerar los cuatro contratos PNG:

python scripts/generate_test_images.py

Decisiones técnicas

  • Responses API para visión: acepta imágenes base64 como data URL y devuelve output_text; el detalle alto mejora la lectura de texto pequeño.
  • LangChain en los agentes: cada agente encapsula sus system prompts, mensajes y structured output; agents/pipeline.py además usa LangChain para orquestar la colaboración entre ambos (RunnableLambda | RunnableLambda), en vez de encadenarlos con una llamada Python secuencial.
  • Temperatura 0: favorece resultados repetibles en extracción jurídica.
  • Schema mínimo: se mantienen exactamente los tres campos requeridos; la clasificación de cada diferencia vive en el resumen.
  • Trazas manuales: hacen explícita la jerarquía solicitada y permiten capturar también las llamadas directas de visión.

Documentación de referencia

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages