Ctrl K
ComunidadGuías…

packages/ai/test/extraction.test.ts

Rama dev · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import { describe, expect } from "bun:test"
import { Effect } from "effect"
import { Extraction } from "../src/experimental.js"
import { CloudflareWorkersAI } from "../src/providers.js"
import { it } from "./lib/effect.js"
import { dynamicResponse, json } from "./lib/http.js"

const model = CloudflareWorkersAI.configure({
  accountId: "test-account",
  apiKey: "cloudflare-key",
}).experimental.extraction("@cf/cloudflare/clairic")

describe("Extraction", () => {
  it.effect("sends Clairic fields and decodes typed answers", () =>
    Extraction.generate({
      model,
      input: "Invoice INV-7 issued March 2, net 30, total $1,080.00. Contacts: Dana, Priya.",
      fields: {
        invoice: { type: "text", nullThreshold: 0.8 },
        total: { type: "number", decimals: 2, unit: "USD" },
        due: { type: "date", instructions: "Issue date plus net days" },
        contacts: { type: "list", items: "text" },
        payDays: { type: "estimate", min: 0, max: 120, step: 1 },
        sales: { type: "forecast", min: 0, max: 500, horizon: 2, interval: "month" },
      },
      referenceDate: "2026-01-01",
      providerOptions: { options: { include_quantiles: true } },
    }).pipe(
      Effect.tap((response) =>
        Effect.sync(() => {
          expect(response.answers).toEqual({
            invoice: {
              value: "INV-7",
              guess: "INV-7",
              nullProbability: 0.01,
              confidence: 0.99,
              span: [8, 13],
            },
            total: { value: null, guess: 1080, nullProbability: 0.6, confidence: 0.9 },
            due: { value: "2026-04-01", nullProbability: 0, confidence: 1 },
            contacts: {
              value: ["Dana", "Priya"],
              spans: [
                [67, 71],
                [73, 78],
              ],
              confidence: 0.98,
            },
            payDays: {
              value: 30,
              mean: 29.5,
              median: 30,
              p10: 21,
              p90: 38,
              belowProbability: 0.001,
              aboveProbability: 0.002,
              quantiles: [20, 30, 40],
            },
            sales: {
              value: [140, 148],
              steps: [
                { value: 140, mean: 140, median: 141, p10: 130, p90: 151 },
                { value: 148, mean: 148, median: 148, p10: 131, p90: 165 },
              ],
            },
          })
          expect(response.usage?.inputTokens).toBe(655)
          expect(response.usage?.providerMetadata?.["cloudflare-workers-ai"]).toMatchObject({ passes: 3 })
          expect(response.providerMetadata).toEqual({ "cloudflare-workers-ai": { model: "clairic", truncated: false } })
        }),
      ),
      Effect.provide(
        dynamicResponse((input) => {
          expect(input.request.url).toBe(
            "https://api.cloudflare.com/client/v4/accounts/test-account/ai/run/@cf/cloudflare/clairic",
          )
          expect(input.request.headers.authorization).toBe("Bearer cloudflare-key")
          expect(JSON.parse(input.text)).toEqual({
            options: { include_quantiles: true },
            model: "@cf/cloudflare/clairic",
            state: "Invoice INV-7 issued March 2, net 30, total $1,080.00. Contacts: Dana, Priya.",
            fields: {
              invoice: { type: "text", null_threshold: 0.8 },
              total: { type: "number", decimals: 2, unit: "USD" },
              due: { type: "date", instructions: "Issue date plus net days" },
              contacts: { type: "list", items: "text" },
              payDays: { type: "estimate", min: 0, max: 120, step: 1 },
              sales: { type: "forecast", min: 0, max: 500, horizon: 2, interval: "month" },
            },
            reference_date: "2026-01-01",
          })
          return Effect.succeed(
            json(input, {
              result: {
                model: "clairic",
                answers: {
                  invoice: {
                    type: "text",
                    p_null: 0.01,
                    span: [8, 13],
                    guess: "INV-7",
                    confidence: 0.99,
                    value: "INV-7",
                  },
                  total: { type: "number", p_null: 0.6, guess: 1080, confidence: 0.9, value: null },
                  due: { type: "date", p_null: 0, confidence: 1, value: "2026-04-01" },
                  contacts: {
                    type: "list",
                    value: ["Dana", "Priya"],
                    spans: [
                      [67, 71],
                      [73, 78],
                    ],
                    confidence: 0.98,
                  },
                  payDays: {
                    type: "estimate",
                    value: 30,
                    mean: 29.5,
                    median: 30,
                    p10: 21,
                    p90: 38,
                    p_below: 0.001,
                    p_above: 0.002,
                    quantiles: [20, 30, 40],
                  },
                  sales: {
                    type: "forecast",
                    value: [140, 148],
                    median: [141, 148],
                    p10: [130, 131],
                    p90: [151, 165],
                    steps: [
                      { value: 140, mean: 140, median: 141, p10: 130, p90: 151 },
                      { value: 148, mean: 148, median: 148, p10: 131, p90: 165 },
                    ],
                  },
                },
                truncated: false,
                usage: {
                  state_tokens: 61,
                  image_tokens: 0,
                  schema_tokens: 472,
                  input_tokens: 655,
                  output_tokens: 0,
                  passes: 3,
                },
              },
              success: true,
              errors: [],
              messages: [],
            }),
          )
        }),
      ),
    ),
  )

  it.effect("passes unknown field types through and drops extra answers", () =>
    Extraction.generate({
      model,
      input: "Ship by May 20 in EUR.",
      fields: { ship: { type: "date" }, money: { type: "currency", codes: ["EUR"] } },
    }).pipe(
      Effect.tap((response) =>
        Effect.sync(() =>
          expect(response.answers).toEqual({
            ship: { value: "2026-05-20", confidence: 0.97 },
            money: { type: "currency", value: "EUR", p_null: 0 },
          }),
        ),
      ),
      Effect.provide(
        dynamicResponse((input) => {
          expect(JSON.parse(input.text).fields.money).toEqual({ type: "currency", codes: ["EUR"] })
          return Effect.succeed(
            json(input, {
              result: {
                model: "clairic",
                answers: {
                  ship: { type: "date", value: "2026-05-20", confidence: 0.97, calendar: "gregorian" },
                  money: { type: "currency", value: "EUR", p_null: 0 },
                  extra: { type: "hologram", value: { x: 1 } },
                },
                future_flag: true,
              },
            }),
          )
        }),
      ),
    ),
  )

  it.effect("fails when a requested field has no answer", () =>
    Extraction.generate({ model, input: "Nothing here.", fields: { total: { type: "number" } } }).pipe(
      Effect.flip,
      Effect.tap((error) => Effect.sync(() => expect(error.reason._tag).toBe("InvalidProviderOutput"))),
      Effect.provide(
        dynamicResponse((input) =>
          Effect.succeed(json(input, { result: { model: "clairic", answers: {}, usage: { input_tokens: 1 } } })),
        ),
      ),
    ),
  )

  it.effect("fails when an answer value does not match its field", () =>
    Extraction.generate({ model, input: "Total: lots.", fields: { total: { type: "number" } } }).pipe(
      Effect.flip,
      Effect.tap((error) => Effect.sync(() => expect(error.reason._tag).toBe("InvalidProviderOutput"))),
      Effect.provide(
        dynamicResponse((input) =>
          Effect.succeed(
            json(input, { result: { model: "clairic", answers: { total: { type: "number", value: "lots" } } } }),
          ),
        ),
      ),
    ),
  )

  it.effect("rejects an empty field map before the network", () =>
    Extraction.generate({ model, input: "x", fields: {} }).pipe(
      Effect.flip,
      Effect.tap((error) => Effect.sync(() => expect(error.reason._tag).toBe("InvalidRequest"))),
      Effect.provide(dynamicResponse(() => Effect.die("an invalid extraction request reached the network"))),
    ),
  )
})