Constaia
Integraciones

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 → respuesta

Si 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 variable CONSTAIA_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/sdk

1. 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.

constaia-tool.js
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.

openai-agent.js
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.

anthropic-agent.js
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):

Resultado de validate_document
{
  "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:

verdictDecisión recomendada
VálidoTu código puede continuar el proceso
No válidoRechazar y explicar los motivos con severidad error
RevisarPasarlo a una persona (revisión humana)
null con status queued/processingEl 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:

URLVeredicto
…/uploads/dni_valid.jpgVálido
…/uploads/dni_expired.jpgNo válido "Caducado el 15/06/2020."
…/uploads/blurry.jpgRevisar 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 comprobar expect en 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_id si 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

En esta página