Créer un Web Scraper Intelligent avec Playwright et l'API Claude en TypeScript

Créer un Web Scraper Intelligent avec Playwright et l'API Claude en TypeScript
Les scrapers web traditionnels cassent à chaque mise à jour d'un site. Vous écrivez des sélecteurs CSS, le site modifie un nom de classe, et votre pipeline est mort. Ça vous dit quelque chose ?
Il existe une meilleure approche. Au lieu de dire à votre scraper exactement où se trouvent les données, laissez un modèle d'IA comprendre la page et extraire ce dont vous avez besoin — structuré, propre, et résistant aux changements de mise en page.
Dans ce tutoriel, vous allez construire un scraper web prêt pour la production qui combine Playwright pour l'automatisation du navigateur headless avec l'API Claude d'Anthropic pour l'extraction intelligente de données. À la fin, vous aurez un outil CLI en TypeScript capable de scraper n'importe quel site et de retourner du JSON structuré — quelle que soit la structure du HTML.
Ce que vous allez construire
Un outil CLI appelé ai-scraper qui :
- Navigue vers n'importe quelle URL avec un vrai navigateur headless (gère le contenu rendu par JavaScript)
- Capture le contenu de la page et prend optionnellement des captures d'écran
- Envoie le contenu à Claude avec un prompt d'extraction structuré
- Retourne des données JSON propres et typées
- Supporte la pagination et le scraping multi-pages
- Inclut une logique de retry et de limitation de débit
Prérequis
Avant de commencer, assurez-vous d'avoir :
- Node.js 20+ installé (vérifiez avec
node --version) - Des bases en TypeScript (types, interfaces, async/await)
- Une clé API Anthropic — obtenez-en une sur console.anthropic.com
- Une familiarité de base avec le terminal
- Environ 30 minutes de temps concentré
Étape 1 : Initialiser le projet
Créez un nouveau répertoire et initialisez le projet :
mkdir ai-scraper && cd ai-scraper
npm init -yInstallez les dépendances :
npm install playwright @anthropic-ai/sdk zod commander dotenv
npm install -D typescript @types/node tsxVoici le rôle de chaque package :
| Package | Rôle |
|---|---|
playwright | Automatisation de navigateur headless |
@anthropic-ai/sdk | Client pour l'API Claude |
zod | Validation de schéma à l'exécution |
commander | Parsing des arguments CLI |
dotenv | Chargement des variables d'environnement |
tsx | Exécution TypeScript sans compilation |
Installez les navigateurs Playwright :
npx playwright install chromiumInitialisez TypeScript :
npx tsc --initMettez à jour votre tsconfig.json :
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"esModuleInterop": true,
"outDir": "./dist",
"rootDir": "./src",
"declaration": true,
"resolveJsonModule": true,
"skipLibCheck": true
},
"include": ["src/**/*"]
}Créez la structure du projet :
mkdir -p src/{extractors,utils}
touch .env src/index.ts src/scraper.ts src/ai-extractor.tsÉtape 2 : Configurer les variables d'environnement
Ajoutez votre clé API Anthropic dans .env :
ANTHROPIC_API_KEY=sk-ant-your-key-here
MAX_RETRIES=3
RATE_LIMIT_MS=1000⚠️ Ne commitez jamais votre fichier
.env. Ajoutez-le immédiatement à.gitignore.
Étape 3 : Définir les types
Créez src/types.ts — la colonne vertébrale de votre scraper type-safe :
import { z } from "zod";
// Schéma d'un élément extrait — personnalisez selon vos besoins
export const ScrapedItemSchema = z.object({
title: z.string(),
description: z.string().optional(),
price: z.string().optional(),
url: z.string().url().optional(),
imageUrl: z.string().url().optional(),
metadata: z.record(z.string()).optional(),
});
export type ScrapedItem = z.infer<typeof ScrapedItemSchema>;
// Schéma du résultat d'extraction complet
export const ExtractionResultSchema = z.object({
items: z.array(ScrapedItemSchema),
totalFound: z.number(),
pageInfo: z.object({
title: z.string(),
url: z.string(),
scrapedAt: z.string(),
}),
});
export type ExtractionResult = z.infer<typeof ExtractionResultSchema>;
// Configuration du scraper
export interface ScraperConfig {
url: string;
prompt: string;
schema?: z.ZodSchema;
waitForSelector?: string;
maxPages?: number;
screenshot?: boolean;
timeout?: number;
}
// Contenu d'une page du navigateur
export interface PageContent {
html: string;
text: string;
url: string;
title: string;
screenshot?: Buffer;
}Zod vous donne une validation à l'exécution. C'est crucial — la sortie de Claude est une chaîne qui devrait être du JSON, mais vous devez vérifier la structure avant de lui faire confiance.
Étape 4 : Construire la couche d'automatisation du navigateur
Créez src/scraper.ts :
import { chromium, Browser, Page } from "playwright";
import type { PageContent, ScraperConfig } from "./types";
export class BrowserScraper {
private browser: Browser | null = null;
async launch(): Promise<void> {
this.browser = await chromium.launch({
headless: true,
args: [
"--no-sandbox",
"--disable-setuid-sandbox",
"--disable-dev-shm-usage",
],
});
}
async scrape(config: ScraperConfig): Promise<PageContent> {
if (!this.browser) {
throw new Error("Navigateur non lancé. Appelez launch() d'abord.");
}
const context = await this.browser.newContext({
userAgent:
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) " +
"AppleWebKit/537.36 (KHTML, like Gecko) " +
"Chrome/122.0.0.0 Safari/537.36",
viewport: { width: 1280, height: 720 },
});
const page = await context.newPage();
try {
await page.goto(config.url, {
waitUntil: "networkidle",
timeout: config.timeout || 30000,
});
if (config.waitForSelector) {
await page.waitForSelector(config.waitForSelector, {
timeout: 10000,
});
}
// Défilement automatique pour charger le contenu lazy
await this.autoScroll(page);
const content = await this.extractContent(page);
let screenshot: Buffer | undefined;
if (config.screenshot) {
screenshot = await page.screenshot({
fullPage: true,
type: "png",
});
}
return { ...content, screenshot };
} finally {
await context.close();
}
}
private async autoScroll(page: Page): Promise<void> {
await page.evaluate(async () => {
await new Promise<void>((resolve) => {
let totalHeight = 0;
const distance = 300;
const timer = setInterval(() => {
const scrollHeight = document.body.scrollHeight;
window.scrollBy(0, distance);
totalHeight += distance;
if (totalHeight >= scrollHeight) {
clearInterval(timer);
resolve();
}
}, 100);
// Timeout de sécurité : arrêter le défilement après 10s
setTimeout(() => {
clearInterval(timer);
resolve();
}, 10000);
});
});
}
private async extractContent(
page: Page
): Promise<Omit<PageContent, "screenshot">> {
const title = await page.title();
const url = page.url();
const text = await page.evaluate(() => {
const scripts = document.querySelectorAll(
"script, style, noscript, iframe"
);
scripts.forEach((el) => el.remove());
return document.body.innerText || "";
});
const html = await page.evaluate(() => {
return document.body.innerHTML;
});
return { html, text, url, title };
}
async close(): Promise<void> {
if (this.browser) {
await this.browser.close();
this.browser = null;
}
}
}Décisions de conception clés :
- Stratégie
networkidle— garantit le chargement du contenu rendu par JavaScript - Défilement automatique — déclenche le chargement du contenu lazy (courant sur les sites modernes)
- Extraction de texte propre — supprime les scripts et styles avant l'envoi à Claude
- Isolation du contexte — chaque scrape obtient un contexte de navigateur frais
Étape 5 : Construire la couche d'extraction IA
C'est ici que la magie opère. Créez src/ai-extractor.ts :
import Anthropic from "@anthropic-ai/sdk";
import { z } from "zod";
import type { PageContent, ExtractionResult } from "./types";
import { ExtractionResultSchema } from "./types";
export class AIExtractor {
private client: Anthropic;
private model = "claude-sonnet-4-20250514";
constructor(apiKey: string) {
this.client = new Anthropic({ apiKey });
}
async extract(
content: PageContent,
userPrompt: string,
schema?: z.ZodSchema
): Promise<ExtractionResult> {
const systemPrompt = `You are a precise data extraction assistant.
Your job is to analyze web page content and extract structured data.
Rules:
1. Return ONLY valid JSON — no markdown, no explanations.
2. Extract ALL matching items from the page content.
3. If a field is not found, use null.
4. URLs should be absolute.
5. Clean up text: remove extra whitespace, fix encoding.
6. Be thorough — scan the entire content.`;
const userMessage = `
Page URL: ${content.url}
Page Title: ${content.title}
--- PAGE CONTENT START ---
${this.truncateContent(content.text, 80000)}
--- PAGE CONTENT END ---
--- HTML STRUCTURE (first 20000 chars) ---
${this.truncateContent(content.html, 20000)}
--- HTML STRUCTURE END ---
EXTRACTION REQUEST: ${userPrompt}
Return JSON matching this structure:
{
"items": [
{
"title": "string",
"description": "string or null",
"price": "string or null",
"url": "absolute URL or null",
"imageUrl": "absolute URL or null",
"metadata": { "key": "value" }
}
],
"totalFound": number,
"pageInfo": {
"title": "page title",
"url": "page url",
"scrapedAt": "ISO date string"
}
}`;
const response = await this.client.messages.create({
model: this.model,
max_tokens: 4096,
system: systemPrompt,
messages: [{ role: "user", content: userMessage }],
});
const responseText = response.content
.filter((block) => block.type === "text")
.map((block) => block.text)
.join("");
return this.parseResponse(responseText, schema);
}
private parseResponse(
text: string,
schema?: z.ZodSchema
): ExtractionResult {
let cleaned = text.trim();
if (cleaned.startsWith("\`\`\`")) {
cleaned = cleaned
.replace(/^\`\`\`(?:json)?\n?/, "")
.replace(/\n?\`\`\`$/, "");
}
let parsed: unknown;
try {
parsed = JSON.parse(cleaned);
} catch (error) {
throw new Error(
`Échec du parsing de la réponse IA en JSON : ${(error as Error).message}`
);
}
const validationSchema = schema || ExtractionResultSchema;
const result = validationSchema.safeParse(parsed);
if (!result.success) {
throw new Error(
`Échec de la validation de la réponse :\n${result.error.issues
.map((i) => ` - ${i.path.join(".")}: ${i.message}`)
.join("\n")}`
);
}
return result.data as ExtractionResult;
}
private truncateContent(text: string, maxChars: number): string {
if (text.length <= maxChars) return text;
return (
text.substring(0, maxChars) +
`\n\n[... ${text.length - maxChars} caractères tronqués ...]`
);
}
}🚀 Besoin d'aide pour implémenter une automatisation intelligente ? Noqta construit des solutions IA pour les équipes qui veulent des résultats, pas des expériences.
Points importants :
- Troncature du contenu — Claude a une grande fenêtre de contexte, mais on reste intelligent sur l'utilisation des tokens. Le texte obtient 80K caractères, le HTML 20K pour les indices structurels.
- Validation Zod — la réponse de l'IA est validée contre un schéma. Si Claude retourne une structure inattendue, vous obtenez une erreur claire.
- Nettoyage de la réponse — parfois Claude enveloppe le JSON dans des blocs de code markdown malgré les instructions. On gère ça élégamment.
Étape 6 : Ajouter la logique de retry et la limitation de débit
Créez src/utils/retry.ts :
export interface RetryConfig {
maxRetries: number;
baseDelayMs: number;
maxDelayMs: number;
}
const DEFAULT_CONFIG: RetryConfig = {
maxRetries: 3,
baseDelayMs: 1000,
maxDelayMs: 10000,
};
export async function withRetry<T>(
fn: () => Promise<T>,
config: Partial<RetryConfig> = {}
): Promise<T> {
const { maxRetries, baseDelayMs, maxDelayMs } = {
...DEFAULT_CONFIG,
...config,
};
let lastError: Error | undefined;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
lastError = error as Error;
if (attempt === maxRetries) break;
// Backoff exponentiel avec jitter
const delay = Math.min(
baseDelayMs * Math.pow(2, attempt) + Math.random() * 1000,
maxDelayMs
);
console.warn(
`Tentative ${attempt + 1} échouée : ${lastError.message}. ` +
`Nouvelle tentative dans ${Math.round(delay)}ms...`
);
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
throw lastError;
}Créez src/utils/rate-limiter.ts :
export class RateLimiter {
private lastCall = 0;
constructor(private minIntervalMs: number) {}
async wait(): Promise<void> {
const now = Date.now();
const elapsed = now - this.lastCall;
if (elapsed < this.minIntervalMs) {
const waitTime = this.minIntervalMs - elapsed;
await new Promise((resolve) => setTimeout(resolve, waitTime));
}
this.lastCall = Date.now();
}
}Étape 7 : Construire le scraper multi-pages
Créez src/multi-page-scraper.ts :
import { BrowserScraper } from "./scraper";
import { AIExtractor } from "./ai-extractor";
import { RateLimiter } from "./utils/rate-limiter";
import { withRetry } from "./utils/retry";
import type { ScraperConfig, ExtractionResult, ScrapedItem } from "./types";
export class MultiPageScraper {
private browserScraper: BrowserScraper;
private aiExtractor: AIExtractor;
private rateLimiter: RateLimiter;
constructor(apiKey: string, rateLimitMs = 1000) {
this.browserScraper = new BrowserScraper();
this.aiExtractor = new AIExtractor(apiKey);
this.rateLimiter = new RateLimiter(rateLimitMs);
}
async scrapeMultiplePages(
configs: ScraperConfig[]
): Promise<ExtractionResult> {
await this.browserScraper.launch();
const allItems: ScrapedItem[] = [];
let totalFound = 0;
try {
for (const [index, config] of configs.entries()) {
console.log(
`\n📄 Extraction de la page ${index + 1}/${configs.length} : ${config.url}`
);
await this.rateLimiter.wait();
const result = await withRetry(async () => {
const content = await this.browserScraper.scrape(config);
console.log(
` ✓ Page chargée (${content.text.length} caractères)`
);
const extraction = await this.aiExtractor.extract(
content,
config.prompt,
config.schema
);
console.log(
` ✓ ${extraction.items.length} éléments extraits`
);
return extraction;
});
allItems.push(...result.items);
totalFound += result.totalFound;
}
} finally {
await this.browserScraper.close();
}
return {
items: allItems,
totalFound,
pageInfo: {
title: `Scraping multi-pages (${configs.length} pages)`,
url: configs[0]?.url || "",
scrapedAt: new Date().toISOString(),
},
};
}
}Étape 8 : Créer l'interface CLI
Créez src/index.ts :
import "dotenv/config";
import { Command } from "commander";
import { BrowserScraper } from "./scraper";
import { AIExtractor } from "./ai-extractor";
import { MultiPageScraper } from "./multi-page-scraper";
import { withRetry } from "./utils/retry";
import { writeFileSync } from "fs";
const program = new Command();
program
.name("ai-scraper")
.description("Web scraper intelligent avec Playwright et Claude")
.version("1.0.0");
program
.command("scrape")
.description("Extraire les données d'une URL")
.requiredOption("-u, --url <url>", "URL à scraper")
.requiredOption("-p, --prompt <prompt>", "Quoi extraire")
.option("-o, --output <file>", "Fichier JSON de sortie")
.option("-s, --screenshot", "Prendre une capture d'écran pleine page")
.option("--wait <selector>", "Sélecteur CSS à attendre")
.option("--timeout <ms>", "Timeout de navigation en ms", "30000")
.action(async (options) => {
const apiKey = process.env.ANTHROPIC_API_KEY;
if (!apiKey) {
console.error("❌ ANTHROPIC_API_KEY non défini dans .env");
process.exit(1);
}
const scraper = new BrowserScraper();
const extractor = new AIExtractor(apiKey);
try {
console.log(`🌐 Navigation vers ${options.url}...`);
await scraper.launch();
const content = await withRetry(() =>
scraper.scrape({
url: options.url,
prompt: options.prompt,
waitForSelector: options.wait,
screenshot: options.screenshot,
timeout: parseInt(options.timeout),
})
);
console.log(`📝 Page chargée : "${content.title}"`);
if (content.screenshot) {
writeFileSync("screenshot.png", content.screenshot);
console.log(`📸 Capture d'écran sauvegardée`);
}
console.log(`\n🤖 Envoi à Claude pour extraction...`);
const result = await withRetry(() =>
extractor.extract(content, options.prompt)
);
console.log(`✅ ${result.items.length} éléments extraits\n`);
const output = JSON.stringify(result, null, 2);
if (options.output) {
writeFileSync(options.output, output);
console.log(`💾 Sauvegardé dans ${options.output}`);
} else {
console.log(output);
}
} catch (error) {
console.error(`❌ Erreur : ${(error as Error).message}`);
process.exit(1);
} finally {
await scraper.close();
}
});
program
.command("multi")
.description("Scraper plusieurs URLs")
.requiredOption("-u, --urls <urls...>", "URLs à scraper (séparées par des espaces)")
.requiredOption("-p, --prompt <prompt>", "Quoi extraire")
.option("-o, --output <file>", "Fichier JSON de sortie")
.action(async (options) => {
const apiKey = process.env.ANTHROPIC_API_KEY;
if (!apiKey) {
console.error("❌ ANTHROPIC_API_KEY non défini dans .env");
process.exit(1);
}
const multiScraper = new MultiPageScraper(
apiKey,
parseInt(process.env.RATE_LIMIT_MS || "1000")
);
try {
const configs = options.urls.map((url: string) => ({
url,
prompt: options.prompt,
}));
const result = await multiScraper.scrapeMultiplePages(configs);
const output = JSON.stringify(result, null, 2);
if (options.output) {
writeFileSync(options.output, output);
console.log(
`\n💾 ${result.items.length} éléments sauvegardés dans ${options.output}`
);
} else {
console.log(output);
}
} catch (error) {
console.error(`❌ Erreur : ${(error as Error).message}`);
process.exit(1);
}
});
program.parse();Étape 9 : Ajouter les scripts npm
Mettez à jour votre package.json :
{
"type": "module",
"scripts": {
"scrape": "tsx src/index.ts scrape",
"multi": "tsx src/index.ts multi",
"build": "tsc",
"start": "node dist/index.js"
}
}Étape 10 : Tester — Exemples concrets
Exemple 1 : Extraire des listes de produits
npx tsx src/index.ts scrape \
-u "https://books.toscrape.com" \
-p "Extract all book titles, prices, ratings, and availability" \
-o books.jsonRésultat attendu :
{
"items": [
{
"title": "A Light in the Attic",
"price": "£51.77",
"metadata": {
"rating": "Three",
"availability": "In stock"
}
}
],
"totalFound": 20,
"pageInfo": {
"title": "All products | Books to Scrape",
"url": "https://books.toscrape.com",
"scrapedAt": "2026-03-13T12:00:00.000Z"
}
}Exemple 2 : Extraire des offres d'emploi
npx tsx src/index.ts scrape \
-u "https://news.ycombinator.com/jobs" \
-p "Extract all job postings: company name, job title, posting date, and link" \
-o hn-jobs.jsonExemple 3 : Scraping multi-pages
npx tsx src/index.ts multi \
-u "https://books.toscrape.com/catalogue/page-1.html" \
"https://books.toscrape.com/catalogue/page-2.html" \
"https://books.toscrape.com/catalogue/page-3.html" \
-p "Extract all book titles and prices" \
-o all-books.jsonÉtape 11 : Schémas d'extraction personnalisés
La vraie puissance réside dans les schémas personnalisés. Créez src/extractors/product-extractor.ts :
import { z } from "zod";
export const ProductSchema = z.object({
items: z.array(
z.object({
name: z.string(),
price: z.object({
amount: z.number(),
currency: z.string(),
}),
rating: z.number().min(0).max(5).optional(),
reviewCount: z.number().optional(),
availability: z.enum(["in_stock", "out_of_stock", "preorder"]),
sku: z.string().optional(),
brand: z.string().optional(),
category: z.string().optional(),
url: z.string().url(),
imageUrl: z.string().url().optional(),
})
),
totalFound: z.number(),
pageInfo: z.object({
title: z.string(),
url: z.string(),
scrapedAt: z.string(),
}),
});
export type ProductResult = z.infer<typeof ProductSchema>;Utilisez-le dans votre appel d'extraction :
import { ProductSchema } from "./extractors/product-extractor";
const result = await extractor.extract(content, userPrompt, ProductSchema);
// result est maintenant entièrement typé💡 Prêt à passer de la lecture à la construction ? Discutez avec notre équipe de l'implémentation d'automatisations et d'intégrations API personnalisées pour votre entreprise.
Conseils et bonnes pratiques
✅ À faire
- Utilisez
networkidlepour les sites lourds en JavaScript - Validez avec Zod — ne faites jamais confiance à la sortie brute de l'IA en production
- Limitez le débit — soyez un bon citoyen du web, ne surchargez pas les serveurs
- Mettez en cache — sauvegardez le HTML brut avec les données extraites pour le débogage
- Utilisez les captures d'écran pour diagnostiquer les problèmes d'extraction
⚠️ Attention
- robots.txt — vérifiez toujours et respectez les règles de crawling
- Conditions d'utilisation — assurez-vous que le scraping est autorisé
- Limites de débit — pour les sites cibles ET votre API Anthropic
- Coûts des tokens — les grandes pages signifient plus de tokens. Surveillez votre utilisation
- Contenu dynamique — certains sites nécessitent des interactions spécifiques avant l'affichage du contenu
❌ À ne pas faire
- Extraire des données personnelles sans consentement
- Contourner les mécanismes d'authentification
- Ignorer les limites de débit ou
robots.txt - Utiliser cet outil pour le spam ou la génération de contenu sans attribution
Vue d'ensemble de l'architecture
Voici comment les composants s'articulent :
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Couche CLI │────▶│ Playwright │────▶│ Site cible │
│ (Commander) │ │ (Navigateur) │ │ │
└──────┬───────┘ └──────┬───────┘ └──────────────┘
│ │
│ Contenu de page
│ (html + texte)
│ │
│ ┌──────▼───────┐ ┌──────────────┐
│ │ Extracteur │────▶│ Claude API │
│ │ IA │ │ │
│ └──────┬───────┘ └──────────────┘
│ │
│ JSON structuré
│ (validé par Zod)
│ │
▼ ▼
┌──────────────────────────────┐
│ Sortie (stdout ou .json) │
└──────────────────────────────┘
Aller plus loin
Voici quelques idées pour étendre ce projet :
- Couche de cache — stockez le HTML brut dans SQLite pour ré-extraire sans re-scraper
- Interface web — encapsulez le CLI dans une app Next.js avec un formulaire
- Planification — utilisez des cron jobs ou une file de tâches pour scraper périodiquement
- Support proxy — alternez les proxies pour le scraping à grande échelle
- Streaming — utilisez l'API de streaming de Claude pour un feedback en temps réel
- Extraction visuelle — envoyez des captures d'écran à la capacité de vision de Claude
Résumé
Vous avez construit un web scraper intelligent prêt pour la production qui :
- Utilise Playwright pour une automatisation fiable du navigateur (gère le contenu JavaScript)
- Exploite Claude pour une extraction intelligente sans sélecteurs CSS
- Valide la sortie avec Zod pour la sécurité des types
- Supporte le scraping multi-pages avec limitation de débit
- Inclut une logique de retry avec backoff exponentiel
L'idée clé : au lieu d'écrire des sélecteurs CSS fragiles qui cassent à chaque mise à jour, vous décrivez ce que vous voulez en langage naturel et laissez l'IA trouver où ça se trouve. Cette approche est plus résiliente, plus flexible, et étonnamment plus précise que le scraping traditionnel.
Le code source complet est disponible pour l'adapter à vos cas d'usage spécifiques. Bon scraping — et n'oubliez pas l'éthique !
Construire des outils d'automatisation, c'est une chose. Les construire correctement — résilients, type-safe, prêts pour la production — c'en est une autre. Chez Noqta, nous aidons les équipes à implémenter des solutions IA qui fonctionnent vraiment en production. Parlons de votre prochain projet.
Discutez de votre projet avec nous
Nous sommes ici pour vous aider avec vos besoins en développement Web. Planifiez un appel pour discuter de votre projet et comment nous pouvons vous aider.
Trouvons les meilleures solutions pour vos besoins.
Articles connexes

Déployer votre première fonction IA serverless avec Cloudflare Workers et Claude
Apprenez à construire et déployer une fonction IA serverless prête pour la production avec Cloudflare Workers et l'API Claude. Guide complet de la configuration au déploiement.

Construire des Agents IA Stateful avec LangGraph.js et TypeScript
Apprenez à construire des agents IA prêts pour la production avec mémoire conversationnelle, appels d'outils et routage conditionnel en utilisant LangGraph.js et TypeScript. Des bases du graphe aux workflows d'agents multi-étapes.

AI SDK 4.0 : Nouvelles Fonctionnalites et Cas d'Utilisation
Decouvrez les nouvelles fonctionnalites et cas d'utilisation d'AI SDK 4.0, incluant le support PDF, l'utilisation de l'ordinateur et plus encore.