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.
Si tu producto tiene un asistente o un agente basado en un LLM, puedes darle una herramienta
validate_document para que compruebe documentos con Constaia. El modelo decide cuándo llamarla y con qué
argumentos; tu código la ejecuta llamando a la API con tu clave y devuelve al modelo solo lo necesario.
usuario → modelo → tool call validate_document(file_url, expect, checks)
→ tu servidor valida los argumentos → Constaia /v1/analyze
→ resultado recortado (veredicto, motivos, campos clave) → modelo → respuestaSi lo que quieres es usar Constaia desde Claude Desktop, Claude Code, Cursor u otro cliente de escritorio, no necesitas escribir código: usa el servidor MCP.
Requisitos
- Node.js ≥ 18 o Python ≥ 3.9.
- Una clave de test
ck_test_…del panel en la variableCONSTAIA_API_KEY. - Una clave de OpenAI (
OPENAI_API_KEY) o de Anthropic (ANTHROPIC_API_KEY). - Los documentos accesibles en una URL
https://de tu propio almacenamiento (por ejemplo, un enlace firmado temporal que genera tu aplicación cuando el usuario sube el fichero).
npm i @constaia/sdk openai @anthropic-ai/sdk1. Define la herramienta
Este módulo contiene todo lo que no depende del proveedor del modelo: el esquema JSON de la herramienta, la validación de los argumentos y la llamada a Constaia. El modelo nunca ve la clave: solo ve el esquema y el resultado.
import { 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", "issue_date", "amount", "currency",
];
export const TOOL_NAME = "validate_document";
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 const TOOL_SCHEMA = {
type: "object",
properties: {
file_url: { type: "string", description: "URL https del documento, tal como la proporciona la aplicación." },
expect: {
type: "array",
items: { type: "string", enum: ALLOWED_TYPES },
minItems: 1,
description: "Tipos de documento aceptables.",
},
checks: {
type: "object",
description: "Reglas opcionales que debe cumplir el documento.",
properties: {
not_expired: { type: "boolean" },
min_age_years: { type: "integer" },
max_age_days: { type: "integer", description: "Antigüedad máxima de la fecha de emisión, en días." },
holder: {
type: "object",
properties: {
full_name: { type: "string" },
document_number: { type: "string" },
birth_date: { type: "string", description: "YYYY-MM-DD" },
},
additionalProperties: false,
},
expected_amount: { type: "number" },
expected_reference: { type: "string" },
},
additionalProperties: false,
},
},
required: ["file_url", "expect"],
additionalProperties: false,
};
function toSdkChecks(checks = {}) {
const { holder } = checks;
return {
notExpired: checks.not_expired,
minAgeYears: checks.min_age_years,
maxAgeDays: checks.max_age_days,
expectedAmount: checks.expected_amount,
expectedReference: checks.expected_reference,
holder: holder
? { fullName: holder.full_name, documentNumber: holder.document_number, birthDate: holder.birth_date }
: undefined,
};
}
export async function runValidateDocument(input) {
const fileUrl = input?.file_url;
const expect = [].concat(input?.expect ?? []);
if (typeof fileUrl !== "string" || !ALLOWED_URL_PREFIXES.some((p) => fileUrl.startsWith(p))) {
return { error: "file_url no permitida: solo se aceptan documentos subidos a la aplicación." };
}
if (expect.length === 0 || !expect.every((t) => ALLOWED_TYPES.includes(t))) {
return { error: `expect debe contener solo: ${ALLOWED_TYPES.join(", ")}` };
}
try {
const analysis = await constaia.analyze(
{ fileUrl },
{ expect, checks: toSdkChecks(input.checks ?? {}), language: "es" },
);
return {
analysis_id: analysis.id,
status: analysis.status, // "queued" o "processing" si no terminó en 30 s
document_type: analysis.document?.type ?? null,
verdict: analysis.verdict?.status ?? null,
reasons: (analysis.verdict?.reasons ?? []).map(({ code, severity, message }) => ({ code, severity, message })),
fields: Object.fromEntries(
KEY_FIELDS.filter((name) => analysis.fields?.[name]).map((name) => [name, analysis.fields[name].value]),
),
warnings: analysis.warnings ?? [],
};
} catch (error) {
if (error instanceof ConstaiaError) {
return { error: `${error.code ?? error.type}: ${error.message}`, request_id: error.requestId };
}
throw error;
}
}Las opciones y los checks disponibles están en POST /v1/analyze y
Checks. Los códigos de reasons están explicados en
Veredictos.
2. OpenAI (Chat Completions)
La herramienta se declara con type: "function". Cuando el modelo la pide, la respuesta trae tool_calls;
ejecutas cada una y respondes con un mensaje role: "tool" con el mismo tool_call_id.
import OpenAI from "openai";
import { TOOL_DESCRIPTION, TOOL_NAME, TOOL_SCHEMA, runValidateDocument } from "./constaia-tool.js";
const openai = new OpenAI(); // lee OPENAI_API_KEY
const model = process.env.OPENAI_MODEL ?? "gpt-4.1";
const tools = [
{ type: "function", function: { name: TOOL_NAME, description: TOOL_DESCRIPTION, parameters: TOOL_SCHEMA } },
];
const messages = [
{
role: "system",
content:
"Ayudas a revisar inscripciones. Usa validate_document para comprobar documentos. " +
"Si verdict es review o status no es completed, di que el documento queda pendiente de revisión.",
},
{
role: "user",
content:
"Comprueba que https://files.example.com/uploads/dni_valid.jpg es un DNI vigente de una persona mayor de edad.",
},
];
for (let turn = 0; turn < 5; turn++) {
const completion = await openai.chat.completions.create({ model, messages, tools });
const message = completion.choices[0].message;
messages.push(message);
if (!message.tool_calls?.length) {
console.log(message.content);
break;
}
for (const call of message.tool_calls) {
let result;
if (call.function.name !== TOOL_NAME) {
result = { error: `Herramienta desconocida: ${call.function.name}` };
} else {
try {
result = await runValidateDocument(JSON.parse(call.function.arguments));
} catch {
result = { error: "No se pudo validar el documento." };
}
}
messages.push({ role: "tool", tool_call_id: call.id, content: JSON.stringify(result) });
}
}3. Anthropic (Messages API)
En la API de Anthropic la herramienta se declara con input_schema. Cuando el modelo la usa, stop_reason es
tool_use y el contenido incluye bloques tool_use; respondes con un mensaje de usuario que contiene bloques
tool_result con el mismo tool_use_id.
import Anthropic from "@anthropic-ai/sdk";
import { TOOL_DESCRIPTION, TOOL_NAME, TOOL_SCHEMA, runValidateDocument } from "./constaia-tool.js";
const anthropic = new Anthropic(); // lee ANTHROPIC_API_KEY
const tools = [{ name: TOOL_NAME, description: TOOL_DESCRIPTION, input_schema: TOOL_SCHEMA }];
const system =
"Ayudas a revisar inscripciones. Usa validate_document para comprobar documentos. " +
"Si verdict es review o status no es completed, di que el documento queda pendiente de revisión.";
const messages = [
{
role: "user",
content:
"Comprueba que https://files.example.com/uploads/dni_valid.jpg es un DNI vigente de una persona mayor de edad.",
},
];
for (let turn = 0; turn < 5; turn++) {
const response = await anthropic.messages.create({
model: "claude-sonnet-4-5",
max_tokens: 1024,
system,
tools,
messages,
});
messages.push({ role: "assistant", content: response.content });
if (response.stop_reason !== "tool_use") {
console.log(response.content.filter((b) => b.type === "text").map((b) => b.text).join("\n"));
break;
}
const results = [];
for (const block of response.content) {
if (block.type !== "tool_use") continue;
let result;
try {
result =
block.name === TOOL_NAME
? await runValidateDocument(block.input)
: { error: `Herramienta desconocida: ${block.name}` };
} catch {
result = { error: "No se pudo validar el documento." };
}
results.push({
type: "tool_result",
tool_use_id: block.id,
content: JSON.stringify(result),
is_error: Boolean(result.error),
});
}
messages.push({ role: "user", content: results });
}Qué hace el modelo con el resultado
La herramienta devuelve un objeto pequeño como este (modo test, dni_valid.jpg, sin checks):
{
"analysis_id": "an_01J…",
"status": "completed",
"document_type": "es_dni",
"verdict": "valid",
"reasons": [
{ "code": "type_match", "severity": "info", "message": "El documento es DNI (España)." },
{ "code": "not_expired", "severity": "info", "message": "Vigente hasta el 12/03/2031." }
],
"fields": {
"document_number": "12345678Z",
"first_name": "MARÍA",
"last_name_1": "GARCÍA",
"last_name_2": "LÓPEZ",
"birth_date": "1990-05-14",
"expiry_date": "2031-03-12",
"issue_date": "2021-03-12"
},
"warnings": []
}Deja que el modelo explique el resultado, pero toma las decisiones con efectos (aprobar una inscripción, pagar,
dar de alta) en tu código a partir de verdict, no a partir del texto que genera el modelo:
verdict | Decisión recomendada |
|---|---|
| Válido | Tu código puede continuar el proceso |
| No válido | Rechazar y explicar los motivos con severidad error |
| Revisar | Pasarlo a una persona (revisión humana) |
null con status queued/processing | El análisis sigue en curso: consulta GET /v1/analyses/{id} o espera al webhook |
Probar en modo test
Con una clave ck_test_… no se consumen créditos y la respuesta depende del nombre del fichero. Con
file_url, Constaia toma el nombre del último segmento de la URL, así que sube a tu almacenamiento imágenes
reales llamadas dni_valid.jpg, dni_expired.jpg o blurry.jpg y pide al modelo que las compruebe:
| URL | Veredicto |
|---|---|
…/uploads/dni_valid.jpg | Válido |
…/uploads/dni_expired.jpg | No válido "Caducado el 15/06/2020." |
…/uploads/blurry.jpg | Revisar low_quality |
Con checks.holder.full_name distinto de "María García López" obtendrás invalid con el motivo holder. Todos
los escenarios en Modo test.
Seguridad
- La clave nunca llega al modelo. Vive en la variable de entorno del servidor que ejecuta la herramienta; el modelo solo ve el esquema y el resultado.
- Lista blanca de tipos en el servidor. Aunque el esquema tenga un
enum, vuelve a comprobarexpecten tu código: el modelo puede generar argumentos que no cumplen el esquema. - No dejes que el modelo elija URLs arbitrarias. Una instrucción oculta en un correo, una web o el propio documento podría hacer que el modelo pida analizar ficheros de terceros y gaste tus créditos. Acepta solo URLs de tu almacenamiento (o, mejor aún, un identificador interno que tu código traduce a URL) y limita el número de llamadas por conversación.
- Devuelve solo lo necesario. El resultado recortado reduce los datos personales que envías al proveedor del
modelo. Guarda el
analysis_idsi luego necesitas el detalle completo. - Trata los datos extraídos como texto no confiable. Un documento puede contener texto escrito para manipular
al modelo; no ejecutes acciones solo porque aparezcan en
fields. - Usa claves de test durante el desarrollo y una clave live distinta por entorno. Más en Seguridad.
Límites
- 20 MB por fichero; PDFs de hasta 30 páginas en síncrono. Si el análisis tarda más de 30 s, la API devuelve
202 y
status: "queued"o"processing". - 2 peticiones por segundo por clave en el plan gratuito (10 en el de pago); el SDK reintenta los 429 respetando
Retry-After. Ver Límites de uso.
Siguientes pasos
Retool
Crea en Retool un panel interno para validar documentos con Constaia: recurso REST con la clave en variables de configuración, subida de ficheros y resultados.
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.