LangChain y LlamaIndex
Envuelve Constaia como herramienta de LangChain (JS y Python) y de LlamaIndex para que tus agentes validen documentos y reciban un veredicto claro.
Esta guía convierte la validación de documentos de Constaia en una herramienta validate_document para
LangChain (JavaScript y Python) y LlamaIndex (Python). La lógica es la misma que en
function calling: el agente elige los argumentos, tu código los valida,
llama a Constaia con tu clave y devuelve un resultado recortado.
Si solo quieres usar Constaia desde un cliente de escritorio (Claude Desktop, Cursor, VS Code), usa el servidor MCP.
Requisitos
- Node.js ≥ 18 o Python ≥ 3.9.
- Una clave de test
ck_test_…del panel enCONSTAIA_API_KEY. - Una clave del proveedor del modelo. Los ejemplos usan Anthropic (
ANTHROPIC_API_KEY); con OpenAI solo cambia la clase del modelo (ChatOpenAIen LangChain). - Los documentos en una URL
https://de tu propio almacenamiento.
La función que llama a Constaia
Las tres integraciones usan esta función. Comprueba la URL y los tipos en el servidor, llama a POST /v1/analyze y devuelve solo el veredicto, los motivos y los campos principales.
npm i @constaia/sdk @langchain/core @langchain/anthropic zodimport { Constaia, ConstaiaError } from "@constaia/sdk";
const constaia = new Constaia(); // lee CONSTAIA_API_KEY
export const ALLOWED_TYPES = ["es_dni", "es_nie", "passport", "medical_certificate_sport", "payment_receipt"];
const ALLOWED_URL_PREFIXES = ["https://files.example.com/uploads/"];
const KEY_FIELDS = [
"document_number", "nie_number", "first_name", "last_name", "last_name_1", "last_name_2",
"birth_date", "expiry_date", "amount", "currency",
];
export const TOOL_DESCRIPTION =
"Valida un documento que el usuario ha subido (DNI, NIE, pasaporte, certificado médico deportivo o " +
"justificante de pago). Devuelve el veredicto valid, invalid o review, los motivos y los datos principales.";
export async function runValidateDocument({ file_url, expect, checks = {} }) {
if (!ALLOWED_URL_PREFIXES.some((p) => file_url.startsWith(p))) {
return { error: "file_url no permitida: solo se aceptan documentos subidos a la aplicación." };
}
if (!expect.length || !expect.every((t) => ALLOWED_TYPES.includes(t))) {
return { error: `expect debe contener solo: ${ALLOWED_TYPES.join(", ")}` };
}
try {
const analysis = await constaia.analyze(
{ fileUrl: file_url },
{
expect,
language: "es",
checks: {
notExpired: checks.not_expired,
minAgeYears: checks.min_age_years,
holder: checks.holder_full_name ? { fullName: checks.holder_full_name } : undefined,
},
},
);
return {
analysis_id: analysis.id,
status: analysis.status,
verdict: analysis.verdict?.status ?? null,
reasons: (analysis.verdict?.reasons ?? []).map(({ code, severity, message }) => ({ code, severity, message })),
fields: Object.fromEntries(
KEY_FIELDS.filter((n) => analysis.fields?.[n]).map((n) => [n, analysis.fields[n].value]),
),
warnings: analysis.warnings ?? [],
};
} catch (error) {
if (error instanceof ConstaiaError) {
return { error: `${error.code ?? error.type}: ${error.message}`, request_id: error.requestId };
}
throw error;
}
}LangChain
La función tool() de @langchain/core/tools recibe la implementación y un esquema de zod, que LangChain
convierte al formato de herramientas del modelo.
import { tool } from "@langchain/core/tools";
import { HumanMessage, SystemMessage } from "@langchain/core/messages";
import { ChatAnthropic } from "@langchain/anthropic";
import { z } from "zod";
import { TOOL_DESCRIPTION, runValidateDocument } from "./constaia-tool.js";
const validateDocument = tool(async (input) => JSON.stringify(await runValidateDocument(input)), {
name: "validate_document",
description: TOOL_DESCRIPTION,
schema: z.object({
file_url: z.string().describe("URL https del documento, tal como la proporciona la aplicación."),
expect: z
.array(z.enum(["es_dni", "es_nie", "passport", "medical_certificate_sport", "payment_receipt"]))
.min(1)
.describe("Tipos de documento aceptables."),
checks: z
.object({
not_expired: z.boolean().optional(),
min_age_years: z.number().int().optional(),
holder_full_name: z.string().optional().describe("Nombre completo esperado del titular."),
})
.optional(),
}),
});
const model = new ChatAnthropic({ model: "claude-sonnet-4-5", maxTokens: 1024 }).bindTools([validateDocument]);
const messages = [
new SystemMessage(
"Ayudas a revisar inscripciones. Usa validate_document para comprobar documentos. " +
"Si verdict es review o status no es completed, di que queda pendiente de revisión.",
),
new HumanMessage(
"¿Es https://files.example.com/uploads/dni_valid.jpg un DNI vigente de María García López, mayor de edad?",
),
];
for (let turn = 0; turn < 5; turn++) {
const ai = await model.invoke(messages);
messages.push(ai);
if (!ai.tool_calls?.length) {
console.log(ai.content);
break;
}
for (const call of ai.tool_calls) {
messages.push(await validateDocument.invoke(call)); // devuelve un ToolMessage
}
}La herramienta funciona igual con cualquier modelo de chat de LangChain que admita herramientas (bindTools /
bind_tools) y dentro de los agentes prediseñados de LangChain o LangGraph: pásala en la lista de herramientas.
LlamaIndex
FunctionTool.from_defaults deduce el esquema de la firma y de las anotaciones de tipos de la función; la
descripción sale del parámetro description (o del docstring).
import asyncio
import json
from typing import List, Optional
from llama_index.core.agent.workflow import FunctionAgent
from llama_index.core.tools import FunctionTool
from llama_index.llms.anthropic import Anthropic
from constaia_tool import TOOL_DESCRIPTION, run_validate_document
def validate_document(
file_url: str,
expect: List[str],
not_expired: Optional[bool] = None,
min_age_years: Optional[int] = None,
holder_full_name: Optional[str] = None,
) -> str:
"""Valida un documento. expect admite: es_dni, es_nie, passport, medical_certificate_sport, payment_receipt."""
return json.dumps(
run_validate_document(file_url, expect, not_expired, min_age_years, holder_full_name),
ensure_ascii=False,
)
tool = FunctionTool.from_defaults(fn=validate_document, name="validate_document", description=TOOL_DESCRIPTION)
agent = FunctionAgent(
tools=[tool],
llm=Anthropic(model="claude-sonnet-4-5", max_tokens=1024),
system_prompt=(
"Ayudas a revisar inscripciones. Usa validate_document para comprobar documentos. "
"Si verdict es review o status no es completed, di que queda pendiente de revisión."
),
)
async def main() -> None:
response = await agent.run(
"¿Es https://files.example.com/uploads/dni_valid.jpg un DNI vigente de María García López, mayor de edad?"
)
print(str(response))
asyncio.run(main())Como FunctionTool no aplica un enum a expect, la comprobación de tipos permitidos de
run_validate_document es la que protege tu cuenta.
Qué devolver y qué decidir
La herramienta devuelve JSON con verdict, reasons, fields y warnings. Deja que el agente lo explique, pero
decide en tu código lo que tenga efectos:
verdict | Decisión |
|---|---|
| Válido | Continuar el proceso |
| No válido | Rechazar y explicar los motivos con severidad error |
| Revisar | Pasar a una persona (revisión humana) |
null (status queued/processing) | El análisis sigue en curso: consulta GET /v1/analyses/{id} o usa webhooks |
Probar en modo test
Con ck_test_… no se consumen créditos y el resultado depende del nombre del fichero, que con file_url es el
último segmento de la URL. Sube imágenes reales con estos nombres a tu almacenamiento:
| URL | Resultado |
|---|---|
…/uploads/dni_valid.jpg | Válido MARÍA GARCÍA LÓPEZ, 12345678Z |
…/uploads/dni_expired.jpg | No válido "Caducado el 15/06/2020." |
…/uploads/blurry.jpg | Revisar low_quality |
Con holder_full_name distinto de "María García López" y dni_valid.jpg, el veredicto es invalid con el
motivo holder. Más en Modo test.
Seguridad
- La clave de Constaia vive en el entorno del servidor; el agente nunca la ve.
- Comprueba en el servidor los tipos permitidos y el origen de las URLs, aunque el esquema ya los limite.
- No dejes que el agente analice URLs que vienen de contenido no confiable (correos, webs, el propio documento): una instrucción oculta podría hacerle gastar tus créditos con ficheros de terceros. Limita también el número de llamadas por conversación.
- Devuelve al modelo solo los campos necesarios y trata el texto extraído como no confiable.
- Desarrolla con claves de test. Más en Seguridad.
Límites
- 20 MB por fichero; PDFs de hasta 30 páginas en síncrono (si tarda más de 30 s, la API responde 202).
- 2 peticiones por segundo por clave en el plan gratuito (10 en el de pago). Los SDKs reintentan los 429 respetando
Retry-Aftery limitan la concurrencia en el cliente (maxConcurrencyen JavaScript,max_concurrencyen Python; 4 por defecto). Ver Límites de uso.
Siguientes pasos
Function calling (OpenAI y Anthropic)
Expón Constaia como herramienta validate_document para modelos de OpenAI y Anthropic, ejecútala en tu servidor y devuelve al modelo un resultado recortado.
Autenticación
Autentica tus llamadas a la API de Constaia con claves Bearer ck_live_ y ck_test_, dónde crearlas y revocarlas, y por qué nunca van al navegador.