Constaia
Integraciones

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 en CONSTAIA_API_KEY.
  • Una clave del proveedor del modelo. Los ejemplos usan Anthropic (ANTHROPIC_API_KEY); con OpenAI solo cambia la clase del modelo (ChatOpenAI en 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 zod
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", "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.

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

llamaindex_agent.py
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:

verdictDecisión
VálidoContinuar el proceso
No válidoRechazar y explicar los motivos con severidad error
RevisarPasar 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:

URLResultado
…/uploads/dni_valid.jpgVálido MARÍA GARCÍA LÓPEZ, 12345678Z
…/uploads/dni_expired.jpgNo válido "Caducado el 15/06/2020."
…/uploads/blurry.jpgRevisar 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-After y limitan la concurrencia en el cliente (maxConcurrency en JavaScript, max_concurrency en Python; 4 por defecto). Ver Límites de uso.

Siguientes pasos

En esta página