---
title: "Isi-IA — Assistance IA sur les Tickets"
module: tickets
type: functional
status: active
updated: 2026-04-17
---

# Isi-IA — Assistance IA sur les Tickets

> Document à destination des admins DSI, chefs de projet, et développeurs.

---

## PARTIE 1 — Guide Fonctionnel

### 1.1 Présentation

**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 :
- Analyser le contenu du ticket et proposer des pistes de résolution
- Répondre aux questions du technicien dans le contexte du ticket
- Suggérer de **clôturer** le ticket (si le problème est résolu)
- Suggérer d'**escalader** le ticket au niveau supérieur (si le problème dépasse le périmètre)

L'assistance IA est optionnelle et configurable par sujet de ticket.

---

### 1.2 Configuration par sujet de ticket

L'activation d'Isi-IA se fait au niveau du **sujet de ticket** (Listdata, référentiel 810098).

#### Champ `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 |

#### Champ `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 :
- Un prompt système personnalisé (contexte métier, instructions spécifiques)
- Un champ `ai_context` sur le sujet pour injecter des informations contextuelles supplémentaires (procédures internes, liens vers la documentation, etc.)
- Un modèle IA spécifique (ex : Mistral Large, GPT-4o)

---

### 1.3 Comportement en situation réelle

#### Mode manuel (`blhelpai = on`)

1. Le technicien ouvre un ticket
2. Le panneau "Isi-IA" est visible avec un bouton **Activer Isi-IA**
3. En cliquant, la conversation démarre : le contenu du ticket est envoyé automatiquement à l'agent IA
4. L'IA répond dans les secondes qui suivent (indicateur de chargement animé pendant la génération)
5. Le technicien peut envoyer des messages supplémentaires (max 2 000 caractères, 50 messages/minute)

#### Mode automatique (`blhelpai = auto`)

1. Dès qu'un ticket est créé sur ce sujet, `TicketV4Helper::checkAndCreateAiAnswer()` crée automatiquement la conversation et dispatch le job IA
2. Le technicien ouvre le ticket et voit déjà la réponse de l'IA (ou l'indicateur de chargement si la réponse n'est pas encore prête)

#### Actions suggérées par l'IA

Aprè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**.

---

### 1.4 Bonnes pratiques

- **Prompts système** : rédiger en français, indiquer le rôle de l'agent ("Tu es un agent de support IT pour l'entreprise X"), le périmètre du sujet, et les instructions de clôture/escalade
- **Contexte spécifique** (`ai_context`) : insérer les procédures internes, les codes d'erreur connus, les contacts à escalader
- **Modèle** : Mistral Large pour les sujets complexes nécessitant un raisonnement approfondi, Mistral Small pour les sujets simples (plus rapide, moins coûteux)
- **Escalade** : bien définir dans le prompt quand l'IA doit proposer une escalade (ex : "Si le problème concerne le réseau physique, demande une escalade")
- **Ne pas activer `auto`** sur des sujets à volume très élevé sans avoir d'abord testé en mode `on`

---

## PARTIE 2 — Documentation Technique

### 2.1 Architecture

```
[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
```

---

### 2.2 Composant Livewire — `TicketAiChat`

**Fichier :** `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és

| 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éthodes publiques

| 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 |

#### État de chargement (`syncLoadingState`)

```php
$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.

#### Rate limiter

```php
$rateLimitKey = 'ai-message:{userId}:{assistantId}';
// 50 tentatives par fenêtre de 60 secondes
```

---

### 2.3 Job — `ProcessTicketAiResponseJob`

**Fichier :** `app/Jobs/Ticket/ProcessTicketAiResponseJob.php`
**Queue :** synchrone ou `default` selon la config
**Timeout :** 180 secondes | **Tries :** 2

```php
public function __construct(
    public AiConversation $conversation,
    public Ticket $ticket,
    public Listdata $listdata,
) {}
```

#### Flux d'exécution

1. Charge l'assistant via `$conversation->assistant()->with(['provider', 'model'])->first()`
2. Si pas d'assistant ou pas de provider → retour silencieux
3. Construit l'historique (20 derniers messages, ordre chronologique)
4. Construit le system prompt (`txsystemprompt` + `ai_context` du listdata)
5. Appelle `AiManager` :
   ```php
   $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)
   ```
6. Détecte les tags dans la réponse (voir §2.6)
7. Sauvegarde `AiMessage(ROLE_ASSISTANT)` avec jsmetadata

#### Note sur `billable(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`).

---

### 2.4 Mode automatique (`blhelpai = auto`)

Déclenché dans `TicketV4Helper::checkAndCreateAiAnswer()` lors de la création d'un ticket.

**Fichier :** `app/Helpers/TicketV4Helper.php`

```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).

---

### 2.5 Résolution de l'assistant (`resolveAssistant`)

```php
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).

---

### 2.6 Convention de clôture/escalade dans le prompt

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) :**

```php
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>>.
```

---

### 2.7 Construction du prompt système

```php
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);
}
```

---

### 2.8 Modèles et tables

#### Table `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 :**

```php
// 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 `idask` est explicite car le modèle `Ticket` n'utilise pas `id` comme PK.

#### Table `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 :**

```php
AiMessage::ROLE_USER      // 'user'
AiMessage::ROLE_ASSISTANT // 'assistant'
```

**Structure `jsmetadata` (messages assistant) :**

```json
{
    "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
}
```

#### Table `_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 |

---

### 2.9 Référence des fichiers

| 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` |
