# Recipes: Sırdaş in your own agent

> **Since 0.5.0.** The code below runs in this
> repository's CI against the real Vercel AI SDK (`ai@7`) and a mock model, so
> it is the code that is tested, not a sketch.

Three patterns we see most: a support bot that must never forward an SSN, a
KYB agent whose model drafts the note while the arithmetic stays on your
machine, and questions over a folder of health records where only the blocks
that answer leave, redacted. Both use your own model and key; Sırdaş opens no connection of its own.

```bash
npm i ai @ai-sdk/openai @sirdaspdf/gateway @sirdaspdf/mcp
```

## A support bot that never sends an SSN

Names, emails and account numbers become placeholders before the request
leaves and come back in the answer. `block: ["SSN"]` refuses the call before
the model sees anything, because support has no reason to handle one, even
redacted. The log receives counts by type, never values.

```js
// Receta: un bot de soporte que contesta con un modelo en la nube y nunca le
// manda un SSN, ni siquiera tapado. Nombres, correos y cuentas se tapan antes
// de salir y vuelven en la respuesta; un SSN para la llamada en seco, porque
// soporte no tiene por qué tratarlo.
import { generateText, wrapLanguageModel } from "ai";
import { sirdasMiddleware, SirdasRedactionError } from "@sirdaspdf/gateway/ai-sdk";

export function supportModel(model, { log = () => {} } = {}) {
  return wrapLanguageModel({
    model,
    middleware: sirdasMiddleware({ block: ["SSN"], onRedact: (counts) => log({ redacted: counts }) }),
  });
}

export async function answerTicket(model, ticket) {
  try {
    const { text } = await generateText({
      model: supportModel(model),
      system: "You are a support agent. Answer in two sentences.",
      prompt: ticket,
    });
    return { ok: true, text };
  } catch (e) {
    // Un SSN en el ticket no es un error del sistema: es la regla funcionando.
    if (e instanceof SirdasRedactionError && e.reason === "blocked") {
      return { ok: false, text: "This ticket contains a Social Security number. Please remove it and send it again." };
    }
    throw e;
  }
}
```

Use it with any AI SDK model:

```js
import { openai } from "@ai-sdk/openai";
const reply = await answerTicket(openai("gpt-5"), ticketText);
```

## A KYB agent that keeps the EIN on your machine

`checkPackageBytes` compares the real values across documents locally (legal
name, EIN, statement balances row by row) and returns the result already
redacted. The model only sees `[ORG_1]` and `[TAX_ID_1]` and writes the note
for the person who decides; it does not approve anyone. The middleware checks
the prompt a second time before it leaves.

```js
// Receta: un agente de KYB que revisa el paquete en tu máquina y solo manda al
// modelo el resultado anonimizado. Las cuentas cuadran con los valores reales,
// en local; el modelo ve [ORG_1] y [TAX_ID_1], nunca el EIN, y redacta la nota
// para la persona que decide. El modelo no aprueba a nadie.
import { readFile, readdir } from "node:fs/promises";
import path from "node:path";
import { generateText, wrapLanguageModel } from "ai";
import { checkPackageBytes } from "@sirdaspdf/mcp";
import { sirdasMiddleware } from "@sirdaspdf/gateway/ai-sdk";

export async function reviewFolder(folder, profile = "lending") {
  const names = (await readdir(folder)).filter((n) => n.endsWith(".pdf")).sort();
  const files = await Promise.all(names.map(async (name) => ({ name, bytes: new Uint8Array(await readFile(path.join(folder, name))) })));
  return checkPackageBytes(files, { profile });
}

export async function underwriterNote(model, folder) {
  const review = await reviewFolder(folder);
  const findings = review.checks
    .filter((c) => c.status !== "pass")
    .map((c) => `- ${c.status.toUpperCase()} ${c.id}: ${c.message}${c.evidence[0] ? ` (${c.evidence[0].source}, page ${c.evidence[0].page ?? "?"})` : ""}`)
    .join("\n");
  // Segunda capa: aunque el resultado ya sale anonimizado, el middleware vuelve
  // a mirar el prompt antes de que salga.
  const { text } = await generateText({
    model: wrapLanguageModel({ model, middleware: sirdasMiddleware() }),
    system: "You write a short note for a human underwriter. Do not approve or decline.",
    prompt: `Package review findings:\n${findings || "- nothing flagged"}\n\nWrite the note.`,
  });
  return { review, note: text };
}
```

## Ask a folder of health records

The context engine picks, on your machine, the few blocks that answer the
question (BM25 over sections, no embeddings and no model), redacts them in
place and sends only that, each block with its source and page. The answer
comes back with the real names on your machine: the clinician sees them, the
provider does not.

```js
// Receta: preguntar a una carpeta de historias clínicas. El motor de contexto
// elige en tu máquina los pocos bloques que responden, los anonimiza en su
// sitio y solo eso va al modelo, con la fuente y la página de cada bloque. La
// respuesta vuelve con los nombres reales también en tu máquina: quien atiende
// los ve, el proveedor no.
import { generateText, wrapLanguageModel } from "ai";
import { anonymizeContext, buildContext, buildDocument, createIndex, CONTEXT_PREAMBLE } from "@sirdaspdf/core/context";
import { deanonymize } from "@sirdaspdf/core/anonymize";
import { sirdasMiddleware } from "@sirdaspdf/gateway/ai-sdk";

/** notes: [{ name, markdown }], el texto de cada historia (sirdas markdown la saca de un PDF). */
export function indexRecords(notes) {
  return createIndex(notes.map((n) => buildDocument(n.markdown, { source: { name: n.name } })));
}

export async function askRecords(model, index, question) {
  const context = anonymizeContext(buildContext(index, question, { maxTokens: 1500 }));
  const { text } = await generateText({
    // Segunda red: si algo se escapó del contexto, el middleware lo tapa antes de salir.
    model: wrapLanguageModel({ model, middleware: sirdasMiddleware() }),
    system: `${CONTEXT_PREAMBLE} Answer only from the documents and name the source of each fact.`,
    prompt: `${context.text}\n\nQuestion: ${question}`,
  });
  return {
    answer: deanonymize(text, context.findings).text,
    sources: context.items.map((i) => ({ source: i.source, pages: i.pages })),
    tokens: { corpus: context.stats.sourceTokens, sent: context.stats.contextTokens },
  };
}
```

## LangChain JS agents

LangChain 1.x ships its own `piiMiddleware` with strategies (`redact`,
`mask`, `hash`, `block`) but detects only emails, cards, IPs, MAC addresses
and URLs. It accepts a custom detector, and `sirdasDetector` is one: it adds
SSNs, EINs, labelled names, MRNs, account and ID numbers. LangChain keeps the
strategy; Sırdaş decides what is personal data.

```js
import { createAgent, piiMiddleware } from "langchain";
import { sirdasDetector } from "@sirdaspdf/core/detect";

const agent = createAgent({
  model: "openai:gpt-5",
  middleware: [
    piiMiddleware("ssn", { detector: sirdasDetector({ types: ["SSN"] }), strategy: "block" }),
    piiMiddleware("sirdas", { detector: sirdasDetector(), strategy: "redact" }),
  ],
});
```

Checked by hand against `langchain@1.6.2`: the model receives
`Patient Name: [REDACTED_SIRDAS]`, and an SSN raises `PIIDetectionError`
before the call. LangChain's strategies do not put the real values back in the
answer; if you need that round trip, use the Gateway or the AI SDK middleware.

In Python (`pip install sirdas`), the same detector plugs into LangChain's
`PIIMiddleware`; positions index the Python `str` directly:

```python
from langchain.agents.middleware import PIIMiddleware
from sirdas.integrations import langchain_pii_detector

PIIMiddleware("sirdas", detector=langchain_pii_detector(), strategy="redact")
```

Checked by hand against `langchain==1.4.9` (Python): the model receives
`Patient Name: [REDACTED_PERSON]`.

## What these recipes do not do

- They do not redact attached files: the middleware rejects them. Convert PDFs
  to text first (`sirdas markdown`) or review them with `checkPackageBytes`.
- Rule-based redaction misses names in free prose with no label; that is why
  `block` exists. The public benchmark (`/benchmarks/pii-v1.jsonl`, scored in
  `docs/benchmarks/pii.md`) lists what it catches and what it misses.
