Les agents conversationnels se limitent souvent à des échanges textuels fastidieux. Et si votre agent pouvait générer des formulaires, boutons et cartes interactives à la volée ? Découvrez comment nous avons transformé l'expérience utilisateur de notre agent grâce à LangChain DeepAgents.
Introduction
Imaginons un cas concret. Un utilisateur veut créer un ticket dans Jira via un chatbot. Sans Skills ni structure, voici ce qui se passe réellement :
Utilisateur : @jira je veux créer un ticket
Agent : Bonjour, oui bien sûr ! Veuillez renseigner les informations suivantes :
* Résumé : de quoi parle le ticket
* Description : description détaillée du ticket
* Priorité : quelle priorité
Le problème ? Cette réponse est incomplète et incorrecte :
- L'agent n'a pas le
project_keyà utiliser – impossible de créer le ticket sans cette information, à moins que cette information ait été fournie dans la demande initiale (@jira je veux créer un ticket dans le projet X), - Des champs obligatoires manquent : environnement, assigné, labels...
- L'agent présume qu'il faut créer une User Story sans demander le type de ticket
- Les champs requis varient selon le type : un Bug nécessite un environnement, pas une Story
Résultat : des tickets impossibles à créer, des erreurs API, et une frustration côté utilisateur.

Suite à la migration vers le framework DeepAgents, pilotée par Fedi CHERIF, il est désormais possible d'introduire des Skills pour structurer et affiner le comportement de nos agents. C'est cette brique qui a ouvert la porte à une refonte de l'expérience utilisateur.
L'architecture existante
Avant cette évolution, l'agent Jira fonctionnait dans un pattern agent-as-tool au sein de LangGraph, mais sans guidage structuré ni transformation UI :

L'agent répondait en texte brut, sans validation côté client ni structuration des échanges. La qualité dépendait entièrement du LLM.
La solution : Skills + middleware GenerateUI
En m'appuyant sur DeepAgents, j'ai introduit trois briques complémentaires :
- Des Skills en Markdown pour structurer le comportement de l'agent
- Un middleware GenerateUI qui transforme automatiquement les réponses texte en composants d'interface en format JSON
- Un moteur de rendu dynamique côté Angular capable d'afficher des composants UI
Avec ces ajouts, le même flux devient structuré et complet :
Utilisateur : @jira je veux créer un ticket
Agent : Dans quel projet ? (texte libre)
Utilisateur : Nom du projet: X
Agent : Quel type ? → [Bug] [Story] [Task] [Business Epic] (boutons)
Utilisateur : clique sur [Bug]
Agent : Formulaire → résumé*, environnement*, description, priorité, assigné, labels
Utilisateur : remplit et soumet
Agent : ✅ Ticket X-123 créé 
Les bénéfices :
- Découverte progressive :
project_keypuis type de ticket - Validation du type : l'agent récupère les types disponibles via l'API Jira
- Champs adaptés au type : un Bug demande l'environnement, pas une Story
- Validation côté client : aucun champ obligatoire oublié
- Interface claire : formulaire complet en une seule fois
Voici l'architecture résultante :

Les trois sections suivantes détaillent chacune de ces briques.
Skills — guider l'agent avec des instructions structurées
DeepAgents permet de charger des Skills via un simple paramètre :
from deepagents import create_deep_agent
agent = create_deep_agent(
...
skills=["skills/"], # Charge tous les .md du dossier
backend=FilesystemBackend(
root_dir=str(Path(__file__).parent), virtual_mode=True
),
)Chaque fichier Markdown du dossier skills/ devient une instruction structurée, activée automatiquement quand sa description correspond à l'intention de l'utilisateur.
Anatomie du Skill create-issue
Voici le Skill le plus complexe — la création de ticket Jira — dans son intégralité structurelle :
---
name: create-issue
description: Use these instructions whenever the user wants to create,
add, or open a new Jira issue or ticket — even if no details have been
provided yet. Covers discovery, type validation, form collection,
and creation. Do NOT collect information yourself — follow these steps.
---
## STEP 1 — Discovery
### 1a — Project key
- Si `project_key` inconnu → demander, puis STOP.
### 1b — Issue type
- Appeler `jira_get_issue_types(project_key)` pour récupérer les types.
- Si `issue_type` inconnu → lister les types et demander, puis STOP.
## STEP 2 — Type Validation
- Normaliser et vérifier que le type choisi existe dans la liste.
## STEP 3 — Collect Form Data
- **Bug** → champs requis : résumé + environnement.
Champs optionnels : description, priorité, assigné, labels.
- **Autre type** → champ requis : résumé uniquement.
- Présenter TOUS les champs en un seul message. STOP.
## STEP 4 — Create the Issue
- Appeler `jira_create_issue` avec tous les champs collectés.
- Afficher le résultat : clé, résumé, statut, lien.
## STEP 5 — Post-Creation
- Proposer la suite : "Créer un autre ticket ?"
## Rules
- Ne JAMAIS deviner project_key ou issue_type.
- Ne JAMAIS sauter la discovery.
- `environment` requis pour Bug uniquement.
- Présenter tous les champs en une seule fois (pas un par un).
Les points STOP sont la clé. Sans eux, le LLM a tendance à "anticiper" la réponse de l'utilisateur et à enchaîner les étapes sans attendre. Avec les STOP, chaque étape de découverte devient un vrai point de synchronisation homme-machine.
Le Skill garantit aussi que le formulaire présente tous les champs en une seule fois — requis et optionnels. C'est cette réponse-formulaire que le middleware GenerateUI va ensuite transformer en composant form interactif.
Le middleware GenerateUI : du texte au JSON
Rôle et déclenchement
Le middleware GenerateUiMiddleware est le pont entre le monde textuel des agents et le monde graphique du frontend. Il s'insère dans le pipeline via le paramètre middleware de create_deep_agent :
from deepagents import create_deep_agent
orchestrator = create_deep_agent(
...
middleware=[GenerateUiMiddleware()],
)Il implémente la méthode aafter_agent : un hook qui s'exécute après chaque tour d'agent.
from langchain.agent.middleware import AgentMiddleware
CLASSIFY_SYSTEM = (
"You receive the final message that an agent is about to show to a user\n",
"Decide which UI component best wraps it and extract the required parameters \n\n",
"Decision Table:\n"
...
)
Class UIDecision(BaseModel):
kind: Literal["form", "buttons", "card", ...] = Field(...)
fields: list[FormField] | None = Field(...)
...
class GenerateUiMiddleware(AgentMiddleware):
async def aafter_agent():
llm = define_llm(...).with_structured_output(UIDecision)
decision = await llm.ainvoke([CLASSIFY_SYSTEM, jira_agent_respose])Le middleware extrait le texte de la réponse IA, puis le soumet au LLM avec un prompt de classification. Le with_structured_output force le LLM à retourner un objet conforme au schéma Pydantic UIDecision.
La table de décision
Le prompt contient des règles explicites pour choisir le bon composant :
Le schéma UIDecision
Le modèle Pydantic UIDecision porte la décision. Son champ kind détermine quel type de composant sera rendu (form, buttons ou vide). Chaque kind active un sous-ensemble de champs (fields, options, title/content, cards) — le LLM n'a qu'à remplir les bons, le schéma Pydantic s'occupe de la validation.
Exemples de JSON générés
Formulaire — quand l'agent demande les champs pour créer un bug :
{
"kind": "form",
"payload": {
"fields": [
{"id": "summary", "label": "Résumé", "type": "text", "required": true,
"placeholder": "Titre court du problème"},
{"id": "description", "label": "Description", "type": "textarea",
"required": false},
{"id": "priority", "label": "Priorité", "type": "select",
"required": false, "options": ["High", "Medium", "Low"]},
{"id": "assignee", "label": "Assigné", "type": "text", "required": false},
{"id": "labels", "label": "Labels", "type": "text", "required": false,
"placeholder": "label1, label2"}
],
"submitLabel": "Créer le ticket"
}
}Frontend Angular : rendu dynamique
Le composant orchestrateur : DynamicUiComponent
Côté Angular, le contrat JSON est reflété par une union discriminée TypeScript sur le champ kind. Un unique composant DynamicUiComponent fait le dispatch vers le bon sous-composant via un @switch :
@switch (uiComponent.kind) {
@case ('buttons') {
<app-ui-buttons [config]="uiComponent.payload" [disabled]="disabled" (action)="onAction($event)" />
}
@case ('form') {
<app-ui-form [config]="uiComponent.payload" [disabled]="disabled" (action)="onAction($event)" />
}
@case ('card') {
<app-ui-card [config]="uiComponent.payload" />
}
}Les sous-composants
Chaque sous-composant est un standalone component Angular qui reçoit son payload typé :
- UiButtonsComponent : rend des boutons PrimeNG en flexbox. Au clic, émet la value du bouton.
- UiFormComponent : construit dynamiquement un FormGroup (Reactive Forms) à partir du tableau fields. Gère la validation (required, email), les types de champs (text, select, textarea, checkbox) et formate la soumission en texte Markdown.
- UiCardComponent : affiche un PrimeNG Card avec un titre, du contenu Markdown (via ngx-markdown) et une image optionnelle.
Gestion des actions utilisateur
Quand l'utilisateur interagit avec un composant UI (clic bouton, soumission formulaire), l'action est renvoyée comme un message humain à l'agent. Par exemple, le formulaire formate ses valeurs :
Résumé: Bug login
Environnement: Production
Priorité: HighL'agent reçoit ce texte et poursuit son workflow — le Skill passe au STEP suivant.
Retour d'expérience et perspectives
Résultats observés
Découverte structurée et fiable : Le workflow en étapes (project → type → champs adaptés) élimine les erreurs de création. L'agent ne devine plus, il découvre méthodiquement les informations nécessaires.
Champs complets et contextuels : Grâce aux Skills, l'agent présente tous les champs requis et optionnels en fonction du type de ticket. Fini les tickets incomplets ou les champs manquants.
Validation côté client : Le formulaire Angular valide les champs obligatoires avant soumission. Aucune erreur API silencieuse, aucun ticket créé avec des données incomplètes.
Gain de temps mesurable : La création d'un ticket passe d'une interaction incertaine et souvent incomplète à un processus guidé en 4 étapes claires, avec validation à chaque niveau.
Améliorations futures
- Autres agents spécialisés : Confluence, SharePoint et Teams sont les prochains candidats.
- Nouveaux composants UI : modales de confirmation, steppers multi-étapes, composants de visualisation de données.
- Enrichissement des Skills : ajout de validations métier (ex: vérifier que l'assigné existe dans Jira avant soumission), suggestions intelligentes basées sur l'historique.
Conclusion : une approche généralisable
Le pattern Skills + middleware UI n'est pas spécifique à Jira. Tout agent qui manipule des données structurées peut en bénéficier : un agent RH qui collecte des demandes de congés, un agent IT qui gère des incidents, un agent commercial qui qualifie des leads, ou encore un agent support qui crée des tickets d'assistance.
La recette reste la même :
- Écrire des Skills pour structurer le workflow de l'agent et guider sa découverte
- Laisser le middleware transformer automatiquement les réponses en composants UI
- Ajouter les composants Angular nécessaires (si les 5 existants ne suffisent pas)
L'élégance de cette architecture ? Aucun agent n'est modifié. Aucun prompt n'est pollué par du JSON UI. La séparation des responsabilités est totale : les agents pensent en langage naturel, le middleware traduit en interface, Angular affiche.
Cette transformation prouve qu'on peut allier la puissance des LLM et l'ergonomie des interfaces modernes sans compromis. Les agents conversationnels ne sont plus condamnés au texte brut – ils peuvent désormais offrir une expérience visuelle riche, guidée et fiable.


