Extraire des champs de ticket validés

JavaScriptBeginner
Pratiquer maintenant

Introduction

Une réponse d’IA destinée à une personne peut varier dans sa formulation sans poser de problème. Le code d’une application exige davantage de rigueur. Un service de routage de tickets, par exemple, a besoin de champs nommés tels que category et priority, avec des valeurs provenant d’un ensemble connu. La sortie structurée demande au modèle de renvoyer des données lisibles par une machine plutôt qu’un texte libre.

Dans ce lab, vous utiliserez le JSON Mode avec un JSON Schema. JSON est le format des données. Le schéma est un contrat qui décrit les champs obligatoires, les types de valeurs autorisés et l’interdiction éventuelle des champs inattendus. Demander à un modèle de respecter un schéma améliore la structure de sa réponse, mais ne constitue pas une limite de confiance : la sortie du modèle reste une donnée externe qui peut être incomplète, mal formée ou incompatible avec l’application.

Vous allez créer POST /extract. Le Worker envoie un petit ticket d’assistance synthétique à un modèle Llama hébergé par Cloudflare et demande quatre champs : une catégorie, une priorité, un bref résumé et une décision de suivi. Le même schéma est ensuite vérifié indépendamment avec Ajv avant que le Worker ne renvoie un enregistrement accepté. Des fixtures déterministes injecteront des sorties de modèle mal formées afin de démontrer que les données invalides suivent un chemin d’erreur au lieu d’entrer dans la réponse acceptée.

Il s’agit du troisième lab du cours. Vous devez savoir qu’un Cloudflare Worker traite les requêtes HTTP et que le binding AI expose Workers AI via env.AI. Si vous avez commencé directement par ce cours, suivez d’abord Connect LabEx to Your Cloudflare Account afin d’apprendre à utiliser le terminal de la VM, à autoriser Wrangler, à confirmer votre compte d’apprentissage et à enregistrer son ID de compte.

Le lab utilise @cf/meta/llama-3.3-70b-instruct-fp8-fast, qui prend en charge le JSON Mode, et limite la taille de chaque invite et résultat. Les comptes Workers Free disposent actuellement d’une allocation quotidienne partagée de 10 000 Neurons ; Workers Paid n’est donc pas nécessaire tant que le compte dispose encore d’une allocation gratuite. L’inférence locale contacte néanmoins Cloudflare et consomme cette allocation. Si le modèle ou l’allocation n’est pas disponible, arrêtez-vous au lieu d’envoyer des requêtes répétées.

La configuration installe Node.js 22.22.0, Wrangler 4.132.0 au niveau du projet et Ajv 8.17.1 dans /home/labex/project/ticket-fields. Elle fournit des tests déterministes et des vérifications indépendantes. La configuration ne vous connecte pas, n’appelle aucun modèle, ne déploie aucun Worker et ne crée aucune ressource cloud. Gardez cette VM ouverte jusqu’à la suppression du Worker temporaire et à la vérification de la déconnexion.

Autoriser la VM et configurer le Worker d’extraction

Dans cette étape, vous allez autoriser cette VM fraîchement créée et configurer un Worker temporaire. Une connexion au Dashboard concerne le navigateur ; Wrangler, dans une nouvelle VM, a besoin de sa propre autorisation limitée avant de pouvoir gérer le compte d’apprentissage.

Accédez au projet préparé et vérifiez la version épinglée de Wrangler :

cd /home/labex/project/ticket-fields
npx wrangler --version

La version attendue est 4.132.0. Demandez les mêmes autorisations limitées que dans les labs précédents sur Workers AI. Wrangler 4.132.0 vérifie les dépendances KV lors de la suppression d’un Worker. workers_kv:write évite donc une erreur de nettoyage sans rapport avec le lab, même si celui-ci ne crée aucune donnée KV.

npx wrangler login --device --browser=false --scopes account:read user:read workers_scripts:write workers_kv:write ai:write

Ouvrez le lien affiché, saisissez le code de l’appareil actuel, vérifiez le compte et les autorisations, puis autorisez le compte d’apprentissage. Revenez au terminal et examinez les informations d’identité structurées :

npx wrangler whoami --json

Vérifiez que loggedIn: true est présent, puis lisez le name et l’id du compte souhaité. Générez un nom unique pour le Worker temporaire :

RUN="labex-c07-a03-$(openssl rand -hex 6)"
printf '%s\n' "$RUN"

Remplacez YOUR_ACCOUNT_ID par l’ID réel de ce compte :

cat > wrangler.jsonc <<JSON
{
  "\$schema": "./node_modules/wrangler/config-schema.json",
  "name": "$RUN",
  "account_id": "YOUR_ACCOUNT_ID",
  "main": "src/index.js",
  "compatibility_date": "2026-09-16",
  "workers_dev": true,
  "preview_urls": false,
  "observability": {
    "enabled": true,
    "head_sampling_rate": 1
  },
  "ai": {
    "binding": "AI",
    "remote": true
  }
}
JSON

Le binding AI devient env.AI. remote: true signifie que le processus Worker local appelle toujours le modèle réel associé au compte. L’observabilité enregistre les petits événements du cycle de vie que vous inspecterez après le déploiement. Aucune inférence ni aucun déploiement n’a encore eu lieu.

Lire le contrat de sortie structurée

Dans cette étape, vous allez examiner les deux couches qui protègent l’application. Le JSON Mode envoie un schéma avec la requête destinée au modèle. Ajv vérifie la valeur renvoyée par rapport à ce schéma dans le Worker. La première couche guide la génération ; la seconde décide si la valeur peut être acceptée sans risque.

Générez les types d’environnement du Worker :

npx wrangler types
grep -A4 'interface __BaseEnv_Env' worker-configuration.d.ts

Repérez AI: Ai. Il s’agit d’un binding fourni par la plateforme, et non d’une clé d’API de modèle stockée dans le code source.

L’enregistrement comportera quatre champs :

category: billing | account | upload | other
priority: low | medium | high
summary: nonempty text, at most 160 characters
needs_follow_up: true or false

Dans JSON Schema, type contrôle le type de valeur, enum limite une valeur à une liste connue, required indique les champs qui doivent exister et additionalProperties: false rejette les champs inattendus. Cette dernière règle est importante : sans elle, un champ inventé pourrait être accepté sans être détecté. Le schéma décrit la structure, mais ne garantit pas que l’interprétation du modèle soit objectivement correcte ; un humain ou une règle métier ultérieure peut encore examiner les champs acceptés.

Examinez les fixtures mal formées fournies pour le test déterministe :

grep -nE 'security|priority: 1|internal_note|not-an-object' test/worker.test.mjs

Ces fixtures ne consomment aucun Neuron. Elles permettent au test de couvrir de manière fiable des cas qui ne devraient jamais être produits volontairement par des invites réelles répétées.

Construire le point de terminaison d’extraction validé

Dans cette étape, vous allez implémenter le schéma, la requête destinée au modèle et la validation côté application. Seule la branche qui réussit la validation Ajv renvoie un record.

Créez le point d’entrée du Worker :

cat > src/index.js <<'JS'
import Ajv from "ajv";

const MODEL = "@cf/meta/llama-3.3-70b-instruct-fp8-fast";
const MAX_TICKET = 1200;

export const TICKET_SCHEMA = {
  type: "object",
  properties: {
    category: { type: "string", enum: ["billing", "account", "upload", "other"] },
    priority: { type: "string", enum: ["low", "medium", "high"] },
    summary: { type: "string", minLength: 1, maxLength: 160 },
    needs_follow_up: { type: "boolean" }
  },
  required: ["category", "priority", "summary", "needs_follow_up"],
  additionalProperties: false
};

const ajv = new Ajv({ allErrors: true });
const isTicketRecord = ajv.compile(TICKET_SCHEMA);

function json(data, status = 200) {
  return Response.json(data, { status });
}

async function readTicket(request) {
  const contentType = request.headers.get("content-type") || "";
  if (!contentType.toLowerCase().includes("application/json")) {
    return { error: json({ error: "json_required" }, 415) };
  }

  const raw = await request.text();
  if (raw.length > 2048) {
    return { error: json({ error: "ticket_too_large" }, 413) };
  }

  let body;
  try {
    body = JSON.parse(raw);
  } catch {
    return { error: json({ error: "invalid_json" }, 400) };
  }

  const ticket = typeof body?.ticket === "string" ? body.ticket.trim() : "";
  if (!ticket) return { error: json({ error: "invalid_ticket" }, 400) };
  if (ticket.length > MAX_TICKET) {
    return { error: json({ error: "ticket_too_large" }, 413) };
  }
  return { ticket };
}

async function extractTicket(request, env) {
  const parsed = await readTicket(request);
  if (parsed.error) return parsed.error;

  const requestId = crypto.randomUUID();
  const details = { requestId, model: MODEL };

  let result;
  try {
    result = await env.AI.run(MODEL, {
      messages: [
        {
          role: "system",
          content: "Extract support-ticket fields. Use only evidence in the ticket. Keep the summary short and do not add fields."
        },
        { role: "user", content: parsed.ticket }
      ],
      response_format: {
        type: "json_schema",
        json_schema: TICKET_SCHEMA
      },
      max_tokens: 160,
      temperature: 0
    });
  } catch {
    console.error(JSON.stringify({ event: "ticket_extraction_failed", ...details }));
    return json({ error: "model_unavailable", requestId }, 502);
  }

  const candidate = result?.response;
  if (!isTicketRecord(candidate)) {
    console.error(JSON.stringify({ event: "ticket_output_rejected", ...details }));
    return json({ error: "invalid_model_output", requestId }, 502);
  }

  console.log(JSON.stringify({ event: "ticket_output_accepted", ...details }));
  return json({ record: candidate, requestId });
}

export default {
  async fetch(request, env) {
    const url = new URL(request.url);
    if (request.method === "GET" && url.pathname === "/health") {
      return json({ status: "ok" });
    }
    if (request.method === "POST" && url.pathname === "/extract") {
      return extractTicket(request, env);
    }
    return json({ error: "not_found" }, 404);
  }
};
JS

Le Worker n’enregistre jamais le ticket ni les champs renvoyés. Son identifiant de requête relie la réponse du client à un événement accepté, rejeté ou en échec, sans copier le contenu du ticket d’assistance dans les données d’observabilité. Les détails des erreurs Ajv restent également absents de la réponse client, car ils pourraient révéler la conception interne de la validation ; les clients reçoivent le contrat stable invalid_model_output.

Exécutez les tests déterministes :

node --test test/worker.test.mjs

Vous devez obtenir cinq tests réussis. Un test injecte sept candidats mal formés via un faux binding AI et exige que chaque réponse ne contienne pas record. Regroupez ensuite le vrai Worker sans le déployer :

npx wrangler deploy --dry-run

Les fixtures prouvent le comportement de rejet sans dépendre d’une sortie de modèle variable. L’exécution à blanc vérifie que le code source, la dépendance Ajv et la configuration du Worker sont correctement regroupés. L’étape suivante effectue une véritable inférence structurée.

Exécuter une véritable requête structurée

Dans cette étape, vous allez exécuter le Worker depuis la VM et envoyer une véritable requête en JSON Mode. « Local » décrit le gestionnaire de requêtes ; le binding AI utilise toujours le compte Cloudflare sélectionné et consomme une partie de son allocation quotidienne.

Démarrez Wrangler en arrière-plan et enregistrez son ID de processus :

npx wrangler dev --port 8787 > .labex/dev.log 2>&1 &
echo $! > .labex/dev.pid

Attendez que la route de vérification, qui n’utilise pas l’IA, réponde :

for attempt in $(seq 1 30); do
  if curl --silent --fail http://127.0.0.1:8787/health; then
    break
  fi
  sleep 1
done

Envoyez un ticket synthétique clair :

curl --silent --show-error http://127.0.0.1:8787/extract \
  --header 'Content-Type: application/json' \
  --data '{"ticket":"Customer cannot upload a PDF and needs help before today’s deadline."}'

Vous devez obtenir une réponse JSON contenant record et requestId. La catégorie exacte, la priorité, la formulation du résumé et la décision de suivi peuvent varier. L’élément important est que l’enregistrement contienne exactement quatre champs et que chaque valeur respecte le schéma.

Vérifiez maintenant qu’une requête d’application invalide est rejetée avant l’appel au modèle :

curl --silent --show-error --write-out '\nHTTP %{http_code}\n' \
  http://127.0.0.1:8787/extract \
  --header 'Content-Type: application/json' \
  --data '{"ticket":""}'

Vous devez obtenir {"error":"invalid_ticket"} et le code HTTP 400. La validation des entrées protège l’appel au modèle ; la validation de la sortie protège l’enregistrement de l’application. Il s’agit de deux limites distinctes.

Déployer et examiner la sortie acceptée

Dans cette étape, vous allez déployer le même point de terminaison validé et relier son état visible dans le Dashboard au résultat d’exécution. Arrêtez d’abord uniquement le processus de développement enregistré :

kill "$(cat .labex/dev.pid)"
wait "$(cat .labex/dev.pid)" 2>/dev/null || true

Déployez le Worker :

npx wrangler deploy

Enregistrez l’URL workers.dev exacte affichée par Wrangler :

WORKER_URL="https://YOUR_WORKER_URL"

Envoyez une requête publique limitée :

curl --silent --show-error "$WORKER_URL/extract" \
  --header 'Content-Type: application/json' \
  --data '{"ticket":"Customer cannot upload a PDF and needs help before today’s deadline."}'

Vérifiez que la réponse publique contient de nouveau exactement les champs du schéma sous record. Un statut HTTP réussi ne suffit pas ; la vérification indépendante valide également chaque champ renvoyé ainsi que le binding AI déployé.

Ouvrez le Cloudflare Dashboard, puis accédez à Workers & Pages → Overview → votre Worker labex-c07-a03-.... Examinez son binding, puis ouvrez Observability → Logs. Recherchez ticket_output_accepted, développez l’événement et vérifiez son model, son requestId et le nom de l’événement. Le journal exclut délibérément le ticket et l’enregistrement extrait.

La vue du binding ci-dessous provient d’une exécution de débogage temporaire. Le diagramme et le tableau associent tous deux le nom AI à Workers AI ; il s’agit de l’équivalent visible dans le Dashboard de env.AI dans le Worker. Le nom unique de votre Worker sera différent.

Binding Workers AI nommé AI sur le Worker d’extraction

La même exécution a enregistré 3 Success et 0 Errors après la requête publique et les vérifications indépendantes. Ces totaux sont des exemples et ne constituent pas des valeurs obligatoires. L’élément important est que le Worker sélectionné ait traité avec succès les requêtes /extract visibles.

Événements réussis du Worker d’extraction sans erreur d’invocation

Après avoir filtré sur ticket_output_accepted, l’événement applicatif développé affiche le modèle Llama exact, un identifiant de requête et le nom de l’événement accepté. Il ne contient ni le ticket synthétique ni l’enregistrement extrait. Cela confirme la limite de confidentialité, sans considérer une ligne de journal comme la preuve que la validation du schéma a réussi ; la réponse d’exécution et la vérification indépendante fournissent cette preuve.

Événement de cycle de vie de sortie structurée acceptée, limité aux données nécessaires

Ouvrez ensuite Workers AI et examinez l’utilisation du modèle pour aujourd’hui. Trouvez le modèle Llama 3.3 et vérifiez que les exercices limités restent dans l’allocation Workers Free de 10 000 Neurons. L’affichage dans le Dashboard peut être retardé ; patientez donc brièvement au lieu de répéter l’inférence uniquement pour forcer la mise à jour d’un graphique ou d’un journal.

Le compte présenté dans l’exemple affichait 261.63/10k Neurons pour le modèle Llama. Ce total inclut des exercices précédents de production du cours effectués sur le même compte d’apprentissage ; il ne correspond donc pas au coût de ce lab uniquement, et votre valeur sera différente. Le point de contrôle consiste à rester dans l’allocation Free, et non à reproduire le nombre de l’exemple.

Allocation quotidienne Free de Workers AI et utilisation du modèle Llama

Les valeurs du Dashboard correspondent à cette exécution temporaire. Les objectifs pédagogiques sont l’identité exacte du Worker, son binding AI, un événement accepté respectueux de la confidentialité et l’utilisation de l’allocation Free. Les vérifications CLI, API et d’exécution restent les sources de référence si une vue du Dashboard est retardée.

Supprimer le Worker et se déconnecter

Dans cette étape, vous allez supprimer le Worker temporaire, puis retirer l’autorisation de cette VM. L’utilisation de Workers AI est enregistrée au niveau du compte : supprimer le Worker retire son point de terminaison, mais n’efface pas l’historique d’utilisation et ne modifie pas l’offre du compte.

Supprimez exactement le Worker nommé dans wrangler.jsonc :

npx wrangler delete

Ne confirmez que lorsque Wrangler affiche le nom unique de ce lab, labex-c07-a03-.... La commande doit se terminer par Successfully deleted. Actualisez Workers & Pages → Overview et vérifiez que ce nom précis n’est plus présent.

Tant que la VM est encore autorisée, exécutez la vérification indépendante de gestion :

python3 .labex/verify.py deleted

Attendez l’affichage de PASS: deleted, puis retirez l’autorisation enregistrée sur la VM :

npx wrangler logout
npx wrangler whoami --json

Vérifiez que loggedIn: false est indiqué. L’absence d’un fichier local, la fermeture d’un onglet du navigateur ou une erreur réseau ne prouverait ni la suppression dans le cloud ni la déconnexion.

Résumé

Vous avez créé un point de terminaison Workers AI qui demande des champs de ticket structurés avec le JSON Mode et un JSON Schema. Vous avez compris pourquoi une structure demandée ne constitue pas nécessairement une donnée fiable, utilisé Ajv comme limite indépendante côté application et démontré, à l’aide de fixtures mal formées, qu’une sortie de modèle invalide ne devient jamais un enregistrement accepté. Vous avez exécuté un résultat réel, en local puis après déploiement, avec Workers Free, relié l’événement accepté à l’observabilité du Dashboard, supprimé le Worker temporaire et déconnecté la VM fraîchement autorisée.