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.
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"]
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.
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
- 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).
python -m venv .venv
source .venv/bin/activate
python -m pip install -r requirements.txt
cp .env.example .envCompletá .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=trueLANGFUSE_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.
Par simple:
python src/main.py \
data/test_contracts/pair_1_original.png \
data/test_contracts/pair_1_amendment.pngPar complejo:
python src/main.py \
data/test_contracts/pair_2_original.png \
data/test_contracts/pair_2_amendment.pngPara una corrida sin Langfuse:
python src/main.py contrato.png adenda.png --no-langfuseLa 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."
}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.
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 subirOPENAI_TIMEOUT_SECONDS.BadRequestErrorconcode=context_length_exceeded→ avisa que el contrato transcripto excede la ventana de contexto del modelo.AuthenticationError,APIConnectionErrory el resto deAPIStatusErrortienen 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.
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.
Instalá dependencias de desarrollo y ejecutá:
python -m pip install -r requirements-dev.txt
pytestLos 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- 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.pyademá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.