Document à destination des admins DSI, chefs de projet, et développeurs.
Isi-IA est le module d'assistance intelligente intégré au module Tickets. Il permet à un technicien de dialoguer en temps réel avec un agent IA pendant le traitement d'un ticket.
L'agent IA peut :
L'assistance IA est optionnelle et configurable par sujet de ticket.
L'activation d'Isi-IA se fait au niveau du sujet de ticket (Listdata, référentiel 810098).
blhelpai| Valeur | Comportement |
|---|---|
off (ou absent) |
Isi-IA masqué — aucun panneau affiché |
on |
Isi-IA disponible — le technicien décide manuellement d'activer l'IA |
auto |
Isi-IA automatique — la conversation démarre dès la création du ticket |
idassistant (optionnel)Permet d'associer un agent IA spécifique à ce sujet de ticket. Si non renseigné, l'agent de fallback ticket-support-it est utilisé.
L'agent IA peut être configuré avec :
ai_context sur le sujet pour injecter des informations contextuelles supplémentaires (procédures internes, liens vers la documentation, etc.)blhelpai = on)blhelpai = auto)TicketV4Helper::checkAndCreateAiAnswer() crée automatiquement la conversation et dispatch le job IAAprès chaque réponse, l'IA peut inclure une suggestion d'action dans son message. Le panneau affiche alors un bouton de confirmation :
| Suggestion IA | Affichage | Action si confirmée |
|---|---|---|
| Clôturer | Bandeau vert + bouton "Confirmer la clôture" | Ticket fermé (tpstate=C, tpresolve=R, tpclose=ai), redirection |
| Escalader | Bandeau jaune + bouton "Confirmer l'escalade" | nblevel incrémenté, redirection |
La suggestion peut être ignorée en cliquant Continuer la discussion.
ai_context) : insérer les procédures internes, les codes d'erreur connus, les contacts à escaladerauto sur des sujets à volume très élevé sans avoir d'abord testé en mode on[Ticket créé / Technicien clique "Activer Isi-IA"]
│
▼
TicketAiChat (Livewire)
└── enableAi() / sendMessage()
│ créé AiConversation + AiMessage(user)
│ dispatch(ProcessTicketAiResponseJob)
▼
ProcessTicketAiResponseJob (Queue)
├── Résout l'assistant (avec provider + model)
├── Construit l'historique (20 derniers messages)
├── Construit le system prompt
├── Appelle AiManager->provider()->billable(false)->chat()
├── Détecte les tags <<TICKET:CLOSE>> / <<TICKET:ESCALATE>>
└── Sauvegarde AiMessage(assistant) avec jsmetadata
│
▼
TicketAiChat::refreshConversation() (wire:poll.3s quand $isLoading)
└── syncLoadingState() → désactive le loader quand la réponse arrive
TicketAiChatFichier : app/Livewire/Ticketv4/TicketAiChat.php
Vue : resources/views/livewire/ticketv4/ticket-ai-chat.blade.php
Balise : <livewire:ticketv4.ticket-ai-chat :idask="$ticket->idask"/>
| Propriété | Type | Description |
|---|---|---|
$idask |
int |
ID du ticket |
$ticket |
Ticket |
Instance du ticket |
$listdata |
Listdata\|null |
Sujet de ticket (config IA) |
$conversation |
AiConversation\|null |
Conversation en cours |
$message |
string |
Saisie de l'utilisateur |
$isLoading |
bool |
Indicateur de génération en cours |
| Méthode | Description |
|---|---|
mount(int $idask) |
Charge ticket + listdata + conversation existante |
enableAi() |
Crée la conversation et dispatch le premier job IA |
sendMessage() |
Valide, rate-limite, sauvegarde le message, dispatch le job |
refreshConversation() |
Polling : détecte si la réponse est arrivée |
closeTicket() |
Clôture le ticket avec tpclose=ai, redirige |
escalateTicket() |
Incrémente nblevel, redirige |
syncLoadingState)$isUserMessage = $lastMessage && $lastMessage->tprole === AiMessage::ROLE_USER;
$isTimedOut = $lastMessage && $lastMessage->created_at->diffInMinutes(now()) > 5;
$this->isLoading = $isUserMessage && !$isTimedOut;
Si le job IA ne répond pas en 5 minutes, le loader est masqué automatiquement pour ne pas bloquer l'interface.
$rateLimitKey = 'ai-message:{userId}:{assistantId}';
// 50 tentatives par fenêtre de 60 secondes
ProcessTicketAiResponseJobFichier : app/Jobs/Ticket/ProcessTicketAiResponseJob.php
Queue : synchrone ou default selon la config
Timeout : 180 secondes | Tries : 2
public function __construct(
public AiConversation $conversation,
public Ticket $ticket,
public Listdata $listdata,
) {}
$conversation->assistant()->with(['provider', 'model'])->first()txsystemprompt + ai_context du listdata)AiManager :$ai->provider($assistant->provider->lbslug)
->billable(false)
->model($assistant->model->lbmodel) // si model défini
->system($systemPrompt) // si non vide
->temperature($config['temperature']) // si configuré
->maxTokens($config['max_tokens']) // si configuré
->chat($history)
AiMessage(ROLE_ASSISTANT) avec jsmetadatabillable(false)L'appel est marqué billable(false) : la consommation de tokens est financée par ISI et non imputée au tenant. Cela utilise la credential "par défaut" du provider (configurée manuellement par l'admin ISI via /admin/ai/credentials). La méthode ->systemCredential() a été abandonnée (provoquait des erreurs 401 en l'absence de clés .env).
blhelpai = auto)Déclenché dans TicketV4Helper::checkAndCreateAiAnswer() lors de la création d'un ticket.
Fichier : app/Helpers/TicketV4Helper.php
// Flux simplifié
$assistant = $this->resolveAssistantForTicket($ticket, $listdata);
if ($assistant) {
$conversation = AiConversation::create([...]);
AiMessage::create(['tprole' => ROLE_USER, 'txcontent' => titre + contenu]);
dispatch(new ProcessTicketAiResponseJob($conversation, $ticket, $listdata));
} else {
// Fallback legacy : GenerateAiTicketResponseJob
dispatch(new GenerateAiTicketResponseJob($ticket));
}
Le fallback GenerateAiTicketResponseJob est conservé pour les tickets dont le sujet n'a pas d'assistant IA configuré (compatibilité ascendante).
resolveAssistant)protected function resolveAssistant(): ?AiAssistant
{
// 1. Assistant spécifique du sujet
if ($this->listdata?->idassistant) {
$assistant = AiAssistant::find($this->listdata->idassistant);
if ($assistant && $assistant->blactive) {
return $assistant;
}
}
// 2. Fallback global
return AiAssistant::ticketSupportIT();
}
AiAssistant::ticketSupportIT() retourne l'agent avec lbslug = 'ticket-support-it' et _id IS NULL (agent global, non rattaché à un tenant).
L'agent IA peut déclencher des actions en incluant un tag spécial en fin de réponse :
| Tag | Action |
|---|---|
<<TICKET:CLOSE>> |
Suggère la clôture du ticket |
<<TICKET:ESCALATE>> |
Suggère l'escalade |
Détection (insensible à la casse) :
if (preg_match('/<<TICKET:(CLOSE|ESCALATE)>>/i', $content, $matches)) {
$ticketAction = strtolower($matches[1]); // 'close' ou 'escalate'
$content = trim(preg_replace('/<<TICKET:(CLOSE|ESCALATE)>>/i', '', $content));
}
Le tag est supprimé du contenu avant sauvegarde. L'action est stockée dans jsmetadata['ticket_action'] du AiMessage.
Exemple de prompt système recommandé :
Tu es un agent de support IT de niveau 1. Aide le technicien à résoudre les problèmes courants.
Si la solution proposée a été appliquée et confirmée, termine ton message par <<TICKET:CLOSE>>.
Si le problème dépasse le niveau 1 (réseau physique, serveur, AD), termine ton message par <<TICKET:ESCALATE>>.
protected function buildSystemPrompt(AiAssistant $assistant): string
{
$parts = [];
if ($assistant->txsystemprompt) {
$parts[] = $assistant->txsystemprompt; // Prompt de l'assistant
}
if ($this->listdata->ai_context) {
$parts[] = 'Contexte spécifique : ' . $this->listdata->ai_context; // Contexte du sujet
}
return implode("\n\n", $parts);
}
ai_conversations| Colonne | Type | Description |
|---|---|---|
id |
bigint | PK |
_id |
string | Tenant ID |
idassistant |
bigint | FK → ai_assistants |
user_id |
bigint | Utilisateur ayant démarré la conversation |
lbtitle |
string | Titre (ex: "Ticket #42 — Problème VPN") |
model_type |
string | Classe du modèle lié (ex: App\Models\Ticket) |
model_id |
bigint | ID du modèle lié (ex: idask du ticket) |
created_at |
timestamp |
Relation polymorphique :
// AiConversation
public function conversable(): MorphTo
{
return $this->morphTo('model');
}
// Ticket
public function aiConversations(): MorphMany
{
return $this->morphMany(AiConversation::class, 'model', 'model_type', 'model_id', 'idask');
}
Note : La clé locale
idaskest explicite car le modèleTicketn'utilise pasidcomme PK.
ai_messages| Colonne | Type | Description |
|---|---|---|
id |
bigint | PK |
idconversation |
bigint | FK → ai_conversations |
tprole |
string | user ou assistant |
txcontent |
text | Contenu du message |
jsmetadata |
json | Métadonnées (tokens, model, ticket_action…) |
created_at |
timestamp |
Constantes :
AiMessage::ROLE_USER // 'user'
AiMessage::ROLE_ASSISTANT // 'assistant'
Structure jsmetadata (messages assistant) :
{
"provider": "mistral",
"model": "mistral-large-latest",
"prompt_tokens": 450,
"completion_tokens": 120,
"total_tokens": 570,
"ticket_action": "close" // null si aucune action suggérée
}
_listdata (sujets de tickets)| Colonne | Type | Description |
|---|---|---|
blhelpai |
string | off / on / auto |
idassistant |
bigint|null | FK → ai_assistants (assistant spécifique) |
ai_context |
text|null | Contexte injecté dans le system prompt |
| Rôle | Fichier |
|---|---|
| Composant Livewire | app/Livewire/Ticketv4/TicketAiChat.php |
| Vue Blade | resources/views/livewire/ticketv4/ticket-ai-chat.blade.php |
| Job IA async | app/Jobs/Ticket/ProcessTicketAiResponseJob.php |
| Helper tickets (mode auto) | app/Helpers/TicketV4Helper.php |
| Modèle conversation | app/Models/Ai/AiConversation.php |
| Modèle message | app/Models/Ai/AiMessage.php |
| Modèle assistant | app/Models/Ai/AiAssistant.php |
| Service IA | app/Services/Ai/AiManager.php |
| Seeder agents tickets | database/seeders/AiTicketAssistantSeeder.php |
| Seeder providers | database/seeders/AiSystemCredentialSeeder.php |
| Migration polymorphique | database/migrations/2026_04_10_190451_add_model_type_model_id_to_ai_conversations.php |
| Migration listdata | database/migrations/..._add_idassistant_ai_context_to_listdata.php |
| Page ticket (intégration) | resources/views/ticketv4/show.blade.php |
| Tests | tests/Feature/TicketAiChatTest.php |