2026-10-01 · Guide · 7 min de lecture · Kouakou Ghislain Boris
Guide : rendre une boutique React pilotable par un agent, pas à pas
Dans ce guide, on part d'une petite boutique React (une liste de produits, une fiche produit, un panier et une page de paiement) et on la rend pilotable par un agent, au clavier comme à la voix. À la fin, l'utilisateur peut dire « montre-moi les casques, ajoute le moins cher au panier et passe commande », et l'agent le fait, en demandant l'accord de l'utilisateur au moment de payer.
On n'écrit aucun sélecteur CSS et aucun clic simulé. Chaque action passe par du code que vous contrôlez.
Ce qu'on va construire
| Étape | Brique OwlLayer | Rôle |
|---|---|---|
| 1 | OwlLayerServer | Le serveur qui parle au modèle |
| 2 | OwlLayerProvider | La connexion de l'application au serveur, avec le widget |
| 3 | useAgentContext | Ce que l'agent sait de l'écran (Shadow Context) |
| 4 | useAgentTool | Une action locale sur la fiche produit |
| 5 | useAgentToolResolver | Les actions du panier, groupées |
| 6 | useNavigationTool | La navigation entre les pages |
| 7 | risk: 'critical' | Le paiement, soumis à approbation |
Prérequis : Node.js 18 ou plus, pnpm 9, une clé d'API Gemini (ou OpenAI, ou Anthropic), et une application React avec react-router-dom.
Étape 1 : le serveur
pnpm add @owllayer/server @owllayer/core @owllayer/adapter-google dotenv// server.tsimport 'dotenv/config';import { OwlLayerServer } from '@owllayer/server';import { GoogleAdapter, GoogleLiveAdapter } from '@owllayer/adapter-google'; const LANGUAGE = 'fr';const SYSTEM_PROMPT = 'You are the shopping assistant of this store. Use the tools to act for the user. ' + 'Never claim an action is done before its tool result confirms it.'; const server = new OwlLayerServer({ // Modèle texte : répond et choisit les outils llm: new GoogleAdapter({ model: 'gemini-2.0-flash', apiKey: process.env.GOOGLE_API_KEY!, systemPrompt: SYSTEM_PROMPT, language: LANGUAGE, }), // Voix temps réel : écoute, répond à voix haute et appelle les mêmes outils live: new GoogleLiveAdapter({ apiKey: process.env.GOOGLE_API_KEY!, model: 'gemini-2.5-flash-native-audio-preview-12-2025', voice: 'Fenrir', systemPrompt: SYSTEM_PROMPT, }), port: 3000, path: '/owllayer', language: LANGUAGE, // Délai maximal d'exécution d'un outil côté client (30 s par défaut) toolTimeout: 15_000, // Messages gardés dans la conversation de chaque session maxConversationMessages: 50, // Outils actifs au maximum par session (30 par défaut) maxActiveTools: 30, // Ajoute au prompt les consignes d'usage selon le niveau de risque toolGuidance: true, // Tableau de bord embarqué : http://localhost:3000/owllayer-ui ui: { enabled: true, language: LANGUAGE }, // Seules les clés ajoutées avec addApiKey peuvent se connecter client: { requireApiKey: true },}); server.addApiKey('pk_dev_123');server.listen(() => console.log('OwlLayer on ws://localhost:3000/owllayer'));Le serveur ne connaît aucun outil à l'avance : c'est l'interface qui les lui annonce, écran par écran. maxActiveTools borne leur nombre ; la valeur est envoyée au navigateur à la connexion.
llm sert au texte, live à la voix temps réel : les deux appellent les mêmes outils, avec les mêmes approbations. Pour la voix, vous pouvez aussi utiliser OpenAI Realtime (OpenAILiveAdapter) ou Deepgram (@owllayer/adapter-deepgram : transcription et synthèse par lots, streaming, ou Voice Agent en temps réel). Avec OpenAIAdapter en modèle texte, l'option timeout borne aussi le temps de réponse du modèle.
Étape 2 : le provider et le widget
pnpm add @owllayer/react @owllayer/core zod// main.tsximport { OwlLayerProvider } from '@owllayer/react';import { BrowserRouter } from 'react-router-dom'; createRoot(document.getElementById('root')!).render( <BrowserRouter> <OwlLayerProvider apiKey="pk_dev_123" endpoint="ws://localhost:3000/owllayer" config={{ voice: true, hitl: { ui: 'modal' }, widget: { enabled: true }, }} > <App /> </OwlLayerProvider> </BrowserRouter>,);Le widget ajoute une bulle de conversation en texte et en voix. hitl: { ui: 'modal' } affiche les demandes d'approbation dans une fenêtre modale, isolée dans un Shadow DOM fermé.
Étape 3 : ce que l'agent sait de l'écran
L'agent n'a pas accès au DOM. Il sait ce que vous lui publiez, avec useAgentContext :
// ProductPage.tsximport { useAgentContext } from '@owllayer/react'; function ProductPage({ product }: { product: Product }) { const cart = useCart(); useAgentContext({ page: 'product', product: { id: product.id, name: product.name, price: product.price, stock: product.stock }, cartCount: cart.count, }); // …}Choisissez peu de champs, et utiles. Le prix et le stock aident l'agent à répondre ; l'identifiant lui permet d'agir. Ne publiez jamais de données que l'utilisateur ne doit pas voir : ce contexte part au modèle.
Étape 4 : une action locale sur la fiche produit
// ProductPage.tsx (suite)import { useAgentTool } from '@owllayer/react';import { z } from 'zod'; useAgentTool( { name: 'add_to_cart', description: `Add "${product.name}" to the cart.`, schema: z.object({ quantity: z.number().int().min(1).max(10).default(1) }), risk: 'low', }, async ({ quantity }) => { if (product.stock < quantity) { return { added: false, reason: `Only ${product.stock} left in stock` }; } await cart.add(product.id, quantity); return { added: true, productId: product.id, quantity, cartCount: cart.count + quantity }; },);Trois choses à noter :
- L'outil n'existe que sur la fiche produit. Quand l'utilisateur quitte la page, le composant se démonte et l'outil disparaît. Sur la liste des produits, l'agent ne peut pas ajouter « le produit courant » au panier, puisqu'il n'y en a pas.
- Le schéma est vérifié avant l'exécution. Si le modèle demande
quantity: 0, il reçoit une erreur de validation et corrige son appel ; votre handler n'est pas appelé. Avec.default(1), un appel sans quantité arrive avecquantity: 1. - Le handler renvoie un résultat utile, y compris quand l'action n'a pas lieu. Le modèle s'appuie sur ce résultat pour répondre à l'utilisateur.
risk: 'low' exécute l'action et affiche une notification : l'utilisateur voit ce que l'agent vient de faire.
Étape 5 : les actions du panier, groupées
Le panier est accessible depuis toutes les pages. Ses actions vont dans le layout, groupées par un resolver :
// Layout.tsximport { useAgentToolResolver } from '@owllayer/react';import { z } from 'zod'; function Layout({ children }: { children: ReactNode }) { const cart = useCart(); useAgentToolResolver( { cart: { prefix: 'cart_', tools: { view: { description: 'List the items in the cart with their quantities and the total.', schema: z.object({}), risk: 'none', handler: () => ({ items: cart.items, total: cart.total }), }, remove: { description: 'Remove one product from the cart.', schema: z.object({ productId: z.string() }), risk: 'high', handler: async ({ productId }) => { await cart.remove(productId); return { removed: productId, cartCount: cart.count - 1 }; }, }, }, }, }, { onErrorAnyCall: (name, _args, error) => console.error(`[agent] ${name} failed`, error), }, ); return <>{children}</>;}Le resolver enregistre cart_view et cart_remove sous un même composant, et ajoute un hook d'erreur commun. cart_remove est en high : retirer un article demande l'accord de l'utilisateur.
Étape 6 : la navigation
Sans navigation, l'agent reste sur la page où il a été appelé. useNavigationTool lui donne un outil navigate global, relié à votre routeur :
// Layout.tsx (suite)import { useNavigationTool } from '@owllayer/react';import { useNavigate } from 'react-router-dom'; const routerNavigate = useNavigate(); useNavigationTool(({ url }) => routerNavigate(url), { description: 'Navigate in the store. Routes: / (home), /products?q=<search> (product list), ' + '/products/<id> (product page), /cart, /checkout.',});Listez vos routes dans la description : c'est ainsi que le modèle sait où aller. Quand navigate change de page, OwlLayer attend que la nouvelle page ait enregistré ses outils avant de rendre la main au modèle. Il peut donc enchaîner « va sur la fiche du casque X » puis « ajoute-le au panier » dans la même demande.
Étape 7 : le paiement, soumis à approbation
// CheckoutPage.tsxfunction CheckoutPage() { const cart = useCart(); useAgentContext({ page: 'checkout', total: cart.total, itemCount: cart.count }); useAgentTool( { name: 'confirm_order', description: 'Place the order and charge the saved payment method. Irreversible.', schema: z.object({}), risk: 'critical', }, async () => { const order = await api.placeOrder(cart.items); return { orderId: order.id, total: order.total, status: order.status }; }, ); // …}Avec risk: 'critical', le handler ne s'exécute qu'après un clic de l'utilisateur dans la fenêtre d'approbation, qui l'avertit que l'action est irréversible. S'il refuse, le modèle reçoit le refus et l'annonce. L'agent peut préparer la commande ; seul l'humain la passe.

Essayer
Lancez le serveur et l'application, ouvrez le widget, puis essayez :
- « Montre-moi les casques Bluetooth. » L'agent navigue vers
/products?q=casque. - « Ouvre le moins cher et ajoutes-en deux. » Navigation vers la fiche, puis
add_to_cartavecquantity: 2, et une notification. - « Qu'est-ce qu'il y a dans mon panier ? »
cart_view, sans confirmation. - « Enlève le casque. »
cart_remove: la fenêtre d'approbation s'ouvre. - « Passe la commande. » Navigation vers
/checkout, puisconfirm_orderavec l'avertissement « action irréversible ».
Avant la production
- Utilisez une clé par application, ajoutez-la côté serveur avec
addApiKeyet gardezclient.requireApiKeyàtrue. Cette clé est visible dans le navigateur : elle identifie l'application, elle ne protège pas un secret. - Relisez les niveaux de risque. Tout ce qui dépense de l'argent, supprime des données ou envoie un message au nom de l'utilisateur mérite
highoucritical. - Bloquez côté serveur ce qui ne doit jamais être appelé, même si un client le déclare, avec
server.blockTool(name). - Gardez peu d'outils par écran. Un outil paramétré (
add_to_cart({ productId })) vaut mieux que cinquante outilsadd_product_123. La limite d'outils actifs est là pour vous le rappeler. - Vérifiez vos handlers : ils doivent renvoyer une Promise qui se résout quand l'action est vraiment terminée.
Pour aller plus loin
- Les concepts d'OwlLayer AI : Neural-DOM Binding, Tools, Shadow Context, exécution HITL et AITP.
- Le guide des outils et la page sécurité et HITL.
- La démo e-commerce React, qui reprend ce guide avec une vraie boutique.
- Le même principe en Vue, Svelte, Angular et HTML.