---
title: "Plugin d'agent"
description: "Un outil d'agent utilisant le style ReAct. Étendez l'Outil pour lui donner toute capacité PHP que vous pouvez écrire."
---

Le plugin Agent vous permet de donner à un modèle LLM **la capacité d'agir** — d'exécuter des requêtes SQL, d'appeler des points de terminaison HTTP, de lire des fichiers ou de faire n'importe quoi que vous pouvez exprimer comme une méthode PHP. Il exécute une boucle de style ReAct : pensez → appel d'outil → observez → répétez → répondez.

## Cinq lignes, un agent
```php
use Nibiru\Module\Ai\Ai;
use Nibiru\Module\Ai\Plugin\Tools\PdoQuery;

$ai = new Ai();
echo $ai->agent()
        ->withTools([new PdoQuery()])
        ->run('How many active users do we have?');
// → "We have 1,247 active users." (after the agent ran SELECT count(*)…)
```
## Comment ça fonctionne
```
user task
    ↓
LLM gets system prompt with tool definitions
    ↓
LLM emits ```tool {"tool":"pdo_query","args":{"sql":"SELECT…"}}```
    ↓
Agent runs the tool, captures result
    ↓
LLM gets observation, decides: more tools or final answer?
    ↓
"FINAL: 1,247 active users."
```
Le protocole utilise un **sentinelle JSON encadrée** — `\`\`\`tool {...}\`\`\`` — que tout modèle peut produire. Aucune API native d'appel d'outil n'est requise, donc cela fonctionne sur tous les modèles Ollama par défaut. (Les modèles qui prennent en charge l'appel natif d'un outil peuvent être intégrés via une sous-classe qui remplace `parseToolCall()`.)

## Outils intégrés

Nibiru embarque trois :

| Outil | Ce qu'il fait |
|---|---|
| `Tools\PdoQuery` | Lecture seule unique `SELECT` sur la base de données de l'application. Bloque INSERT/UPDATE/DELETE/DROP/TRUNCATE/ALTER. Retourne jusqu'à 50 lignes sous forme de JSON. |
| `Tools\HttpGet` | Effectue une requête GET sur une URL HTTP/HTTPS avec des en-têtes optionnels. Retourne le corps, tronqué à 8 Ko. |
| `Tools\FileRead` | Lit un fichier de projet par chemin relatif. Bloque la traversée `..`. Retourne jusqu'à 8 Ko. |
```php
use Nibiru\Module\Ai\Plugin\Tools;

$agent = $ai->agent()->withTools([
    new Tools\PdoQuery(),
    new Tools\HttpGet(),
    new Tools\FileRead(),
]);

// Multi-step task
echo $agent->run(
    'Read application/controller/loginController.php and tell me '
  . 'whether it implements rate limiting.'
);
```
L'agent appellera `file_read` avec le chemin, observera la source et répondra en fonction de ce qu'il a vraiment vu — pas sur ce qu'il imagine.

## Écrire un outil personnalisé

Étendre `Tool`:
```php
namespace App\AiTools;

use Nibiru\Module\Ai\Plugin\Tool;

class StripeRefund extends Tool
{
    public function name(): string { return 'stripe_refund'; }

    public function description(): string {
        return 'Issue a Stripe refund for a charge ID.';
    }

    public function schema(): array {
        return [
            'charge_id' => [
                'type' => 'string',
                'description' => 'A Stripe charge ID, e.g. ch_3K…',
                'required' => true,
            ],
            'amount_cents' => [
                'type' => 'integer',
                'description' => 'Amount to refund in cents. Omit for full refund.',
                'required' => false,
            ],
        ];
    }

    public function execute(array $args): mixed {
        $stripe = new \Stripe\StripeClient(getenv('STRIPE_SECRET_KEY'));
        $refund = $stripe->refunds->create(array_filter([
            'charge' => $args['charge_id'],
            'amount' => $args['amount_cents'] ?? null,
        ]));
        return json_encode([
            'refund_id' => $refund->id,
            'status'    => $refund->status,
            'amount'    => $refund->amount,
        ]);
    }
}
```
Puis branchez-le :
```php
$ai->agent()
   ->withTools([new \App\AiTools\StripeRefund(), new Tools\PdoQuery()])
   ->run('Refund order #4421 — they were charged twice.');
```
L'agent utilisera `pdo_query` pour trouver la charge, puis appellera `stripe_refund` avec cet identifiant de charge.

## Examen de la trace
```php
$agent = $ai->agent()->withTools([new Tools\PdoQuery()]);
$answer = $agent->run('How many products in the gold-plating category?');

foreach ($agent->trace() as $step) {
    echo "Step {$step['step']}: action={$step['action']}\n  obs={$step['observation']}\n";
}
```
Utile pour le débogage, les traces d'audit ou la construction d'une interface utilisateur « montrer votre travail ».

## Sécurité

- **Les blocs `PdoQuery` bloquent les écritures.** Si vous avez besoin d'accès en écriture, créez une sous-classe plus privilégiée avec un fil de trame d'audit. Ne levez pas la restriction en lecture seule dans l'outil intégré.
- **Les `HttpGet` permettent n'importe quelle URL par défaut.** Verrouillez via une liste autorisée dans `[AI] http_allowed_hosts[]` (prévu), ou écrivez une sous-classe `RestrictedHttpGet` qui filtre les URLs.
- **Les `FileRead` bloquent `..`.** Il est limité à la racine de l'application.
- **Nombre maximal d'itérations.** `agent.max_iterations = 6` dans le fichier INI empêche les boucles infinies. Augmentez avec prudence.
- **Délai d'expiration de l'outil.** `agent.tool_timeout = 30` (secondes). Un outil qui bloque ne bloquera pas la requête indéfiniment.

## Trépieds courants

- **Oublier `withTools()`.** Sans outils, l'agent est simplement un `Chat` régulier.
- **Laisser l'agent voir les secrets.** Ne mettez jamais de clés API, de mots de passe bruts ou d'informations personnelles identifiables (PII) dans la réponse d'un outil — le modèle reçoit la chaîne complète.
- **Longues sorties d'outils.** Chaque observation est ajoutée à la conversation. Un outil qui déverse 50 Ko épuisera rapidement le contexte. Les outils intégrés sont limités à 8 Ko ; faites de même avec vos outils personnalisés.
- **Pas d'appel d'outil dans la réponse = réponse finale.** Si le modèle produit une réponse finale qui *ressemble* à un appel d'outil mais ne valide pas, l'agent la traite comme finale. Soyez explicite dans l'invite : "Sortez un appel d'outil OU une réponse finale, jamais les deux."
