Documentation
PHP et Laravel
Le SDK Composer de Gaspard pour PHP 8.2+, avec fournisseur de services, façade et configuration Laravel auto-découverts.
Client PHP de l'API publique du hub HubMix (/v1/public) : Gaspard, l'assistant
IA, et depuis la 1.1.0 le tuteur, la confiance (signaux d'arnaque des annonces), les
embeddings et le vérificateur des webhooks (portail développeurs retiré le 28 octobre 2026 : plus aucun webhook n'est envoyé) ; avec
une intégration Laravel prête à l'emploi (fournisseur de services, façade et configuration
auto-découverts).
- PHP 8.2 ou plus récent, n'importe quel framework ; HTTP via Guzzle 7.8+ ou 8 (déjà livré avec Laravel : Laravel 13 est livré avec Guzzle 8).
- Objets typés et immuables (
final readonly), dates enDateTimeImmutable. - Réponses en flux (Server-Sent Events) lues au fil de l'eau, via un
Generator. - Nouvelles tentatives automatiques quand Gaspard est occupé, une seule exception (
GaspardException).
Installation #
composer require valais-connect/gaspardDémarrage rapide (PHP) #
use ValaisConnect\Gaspard\Gaspard;
$gaspard = new Gaspard(apiKey: getenv('GASPARD_API_KEY')); // clé serveur vck_live_…
$conversation = $gaspard->startConversation(locale: 'fr', page: 'accueil');
$answer = $gaspard->ask($conversation->id, 'Comment trouver un logement étudiant à Lyon ?');
echo $answer->text, PHP_EOL;
foreach ($answer->sources as $source) {
echo "- {$source->name} {$source->url}", PHP_EOL;
}La clé serveur (vck_live_…) est secrète : gardez-la côté serveur, jamais dans un navigateur ni
dans une application mobile.
Afficher la réponse au fil de l'eau #
askStream() renvoie un Generator d'événements StreamEvent : des morceaux de texte (delta),
puis la réponse complète (answer, toujours le dernier événement, aussi disponible via
getReturn()).
$stream = $gaspard->askStream($conversation->id, 'Que faire à Montréal ce week-end ?');
foreach ($stream as $event) {
if ($event->isDelta()) {
echo $event->text; // un morceau de la réponse
}
}
$answer = $stream->getReturn(); // Answer : texte complet, sources, offres (listings)…La requête part au premier tour de boucle. Un flux n'est jamais relancé une fois commencé : s'il se
termine par une erreur (assistant_busy, conversation_full…), une GaspardException est levée
pendant l'itération.
Recherche dans la base de connaissances #
use ValaisConnect\Gaspard\Topic;
$results = $gaspard->searchKnowledge('Berlin Universitäten', limit: 3, topics: [Topic::EDUCATION, Topic::HISTORY]);
foreach ($results as $passage) { // SearchResults est itérable et dénombrable
printf("%.2f %s — %s\n", $passage->score, $passage->title, $passage->source?->name);
}Configuration #
| Argument nommé | Défaut | Rôle |
|---|---|---|
apiKey |
— | Clé serveur vck_live_… → Authorization: Bearer …. |
publishableKey |
— | Clé publiable vcp_live_… → X-Gaspard-Key (exactement l'une des deux clés). |
origin |
— | Avec une clé publiable hors navigateur : une des origines déclarées pour le client. |
baseUrl |
https://hubmix.dealmood-solutions.com |
Hôte du service ; le SDK ajoute /v1/public. |
endUser |
— | Clé serveur : identifiant de votre utilisateur → X-End-User. |
timeout |
60.0 |
Secondes par requête (flux : connexion et chaque lecture, pas de délai total). |
maxRetries |
2 |
Nouvelles tentatives (voir plus bas) ; 0 les désactive. |
userAgent |
gaspard-php/1.1.0 |
En-tête User-Agent. |
http |
client Guzzle | Votre propre GuzzleHttp\ClientInterface (proxy, middlewares, tests). |
Une erreur de configuration (aucune clé ou les deux, endUser invalide, consent: false…) lève une
InvalidArgumentException avant toute requête.
Gaspard::fromConfig([...]) construit le client à partir d'un tableau aux clés snake_case
(api_key, base_url, timeout, max_retries, publishable_key, origin, end_user).
Laravel #
Le fournisseur de services et la façade sont découverts automatiquement. Dans .env :
GASPARD_API_KEY=vck_live_xxxxxxxxxxxxxxxx
# Facultatif :
GASPARD_BASE_URL=https://hubmix.dealmood-solutions.com
GASPARD_TIMEOUT=60
GASPARD_MAX_RETRIES=2Pour modifier la configuration, publiez config/gaspard.php :
php artisan vendor:publish --tag=gaspard-configLe client est un singleton du conteneur (ValaisConnect\Gaspard\Gaspard, alias gaspard), créé à
la première utilisation. Utilisez la façade ou l'injection de dépendances :
use ValaisConnect\Gaspard\Gaspard as GaspardClient;
use ValaisConnect\Gaspard\Laravel\Facades\Gaspard;
// Façade
$notice = Gaspard::privacyNotice('fr');
// Injection
public function __construct(private GaspardClient $gaspard) {}Vos utilisateurs (X-End-User) #
Avec une clé serveur, indiquez pour qui vous parlez : leurs conversations et consentements restent
les leurs (seule une empreinte à clé de l'identifiant est stockée). forEndUser() renvoie une
copie du client ; le singleton partagé n'est jamais modifié (sûr avec Octane).
$gaspard = Gaspard::forEndUser((string) auth()->id());Contrôleur : réponse en flux vers le navigateur #
Le contrôleur relaie le flux de Gaspard au navigateur, au même format SSE (delta, puis answer
ou error).
<?php
namespace App\Http\Controllers;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\StreamedResponse;
use ValaisConnect\Gaspard\GaspardException;
use ValaisConnect\Gaspard\Laravel\Facades\Gaspard;
class AssistantController extends Controller
{
public function start(Request $request): JsonResponse
{
$conversation = Gaspard::forEndUser((string) $request->user()->id)
->startConversation(locale: app()->getLocale(), page: 'aide');
return response()->json($conversation); // les objets du SDK sont JsonSerializable
}
public function ask(Request $request, string $conversation): StreamedResponse
{
$data = $request->validate(['text' => ['required', 'string', 'max:2000']]);
$user = $request->user();
$gaspard = Gaspard::forEndUser((string) $user->id);
return response()->stream(function () use ($gaspard, $conversation, $data, $user): void {
$send = static function (string $event, array $payload): void {
echo "event: {$event}\ndata: " . json_encode($payload, JSON_UNESCAPED_UNICODE) . "\n\n";
if (ob_get_level() > 0) {
ob_flush();
}
flush();
};
try {
foreach ($gaspard->askStream($conversation, $data['text'], $user->first_name) as $event) {
if ($event->isDelta()) {
$send('delta', ['text' => $event->text]);
} else {
$send('answer', $event->answer->toArray());
}
}
} catch (GaspardException $error) {
report($error);
$send('error', ['code' => $error->errorCode]);
}
}, 200, [
'Content-Type' => 'text/event-stream',
'Cache-Control' => 'no-cache',
'X-Accel-Buffering' => 'no', // nginx : ne pas mettre le flux en tampon
]);
}
}// routes/web.php
Route::middleware('auth')->group(function () {
Route::post('/assistant/conversations', [AssistantController::class, 'start']);
Route::post('/assistant/conversations/{conversation}/messages', [AssistantController::class, 'ask']);
});Côté navigateur, EventSource ne fait que des GET : lisez le flux avec fetch :
const response = await fetch(`/assistant/conversations/${id}/messages`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': csrfToken },
body: JSON.stringify({ text }),
});
const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
let buffer = '';
for (;;) {
const { value, done } = await reader.read();
if (done) break;
buffer += value;
let end;
while ((end = buffer.indexOf('\n\n')) >= 0) {
const block = buffer.slice(0, end);
buffer = buffer.slice(end + 2);
const event = /^event: (.*)$/m.exec(block)?.[1];
const data = JSON.parse(/^data: (.*)$/m.exec(block)?.[1] ?? 'null');
if (event === 'delta') output.textContent += data.text;
if (event === 'answer') showSources(data.sources);
if (event === 'error') showError(data.code);
}
}Tests de votre application #
La façade se simule comme toute façade Laravel :
use ValaisConnect\Gaspard\Laravel\Facades\Gaspard;
use ValaisConnect\Gaspard\Model\PrivacyNotice;
Gaspard::shouldReceive('privacyNotice')->once()->andReturn(new PrivacyNotice('2026-10', 'Texte…', 365));Hors Laravel, injectez un client Guzzle avec GuzzleHttp\Handler\MockHandler :
new Gaspard(apiKey: 'vck_live_test', http: new Client(['handler' => HandlerStack::create($mock)])).
Application avec une clé publiable #
Pour une application de bureau ou un outil qui parle directement à Gaspard au nom d'un visiteur, utilisez la clé publiable du client et son origine autorisée, puis ouvrez une session une fois que le visiteur a accepté l'avis de confidentialité :
$gaspard = new Gaspard(publishableKey: 'vcp_live_…', origin: 'https://mon-app.example.ch');
$notice = $gaspard->privacyNotice('fr');
// … afficher $notice->text et recueillir l'accord du visiteur …
$session = $gaspard->openSession(consent: true, allowTraining: false);
saveSomewhere($session->token); // valable 30 jours
// Plus tard : reprendre la même session
$gaspard->setVisitorToken(loadFromSomewhere()); // ou null pour l'oublier
$conversation = $gaspard->startConversation();Le jeton de visiteur (vst_…) est gardé dans le client et envoyé avec chaque requête suivante.
Consentement et données personnelles #
Avec une clé serveur, ces méthodes concernent l'utilisateur indiqué par forEndUser() ; avec une
clé publiable, le visiteur de la session.
$user = Gaspard::forEndUser((string) auth()->id());
$notice = $user->privacyNotice();
$user->giveConsent($notice->version, allowTraining: true); // après acceptation de $notice->text
$status = $user->consent(); // ConsentStatus : given, upToDate, noticeVersion…
$user->withdrawConsent();
$data = $user->exportData(); // VisitorData : droit d'accès
$erasure = $user->eraseData(); // Erasure : droit à l'effacementErreurs et nouvelles tentatives #
Tout échec d'un appel lève ValaisConnect\Gaspard\GaspardException (une RuntimeException) :
| Propriété | Contenu |
|---|---|
status |
Statut HTTP ; 0 sans réponse (réseau, délai, erreur dans un flux). getCode() le renvoie aussi. |
errorCode |
Code de l'API (consent_required, assistant_busy, conversation_full, token_quota_exceeded…), sinon http_<statut> ; network_error, timeout, invalid_response ou stream_incomplete côté SDK. |
getMessage() |
Message lisible. |
requestId |
Identifiant de la requête (request_id ou en-tête X-Request-Id), à citer au support. |
retryAfter |
Secondes à attendre (Retry-After), ou null. |
errorCodeet noncode:\Exception::$codeexiste déjà en PHP (un entier).
use ValaisConnect\Gaspard\GaspardException;
try {
$answer = $gaspard->ask($conversationId, $question);
} catch (GaspardException $error) {
match ($error->errorCode) {
'conversation_full' => $conversationId = $gaspard->startConversation()->id,
'token_quota_exceeded' => logger()->warning("Quota atteint, réessayer dans {$error->retryAfter} s"),
default => report($error),
};
}Le SDK réessaie tout seul, au plus maxRetries fois (2 par défaut) :
503 assistant_busyet429 rate_limited, pour toutes les méthodes ;- les erreurs réseau et délais dépassés, pour les
GETetDELETEseulement.
Il attend Retry-After secondes quand l'en-tête est présent (au-delà de 30 s, il ne réessaie pas et
lève l'erreur), sinon 0,5 s × 2^tentative. Jamais pour token_quota_exceeded, les autres 4xx, ni
pour un flux déjà commencé. Reposer une question est sans risque : le serveur n'enregistre un échange
qu'avec sa réponse. Vous pouvez aussi fournir votre identifiant de requête :
$gaspard->ask($id, $text, requestId: 'commande-12345') (8 à 64 caractères [A-Za-z0-9._-]).
Référence #
| Méthode | HTTP | Retour |
|---|---|---|
privacyNotice(locale = 'fr') |
GET /privacy-notice |
PrivacyNotice |
openSession(consent, locale = 'fr', allowTraining = false) |
POST /sessions |
Session |
setVisitorToken(?token) / visitorToken() |
— | — |
startConversation(locale = 'fr', page = null) |
POST /gaspard/conversations |
Conversation |
ask(conversationId, text, firstName = null) |
POST /gaspard/conversations/{id}/messages |
Answer |
askStream(conversationId, text, firstName = null) |
idem, Accept: text/event-stream |
Generator<StreamEvent> (retour : Answer) |
conversation(conversationId) |
GET /gaspard/conversations/{id} |
ConversationThread |
feedback(messageId, rating, comment = null) |
POST /gaspard/messages/{id}/feedback |
Feedback |
searchKnowledge(query, limit = 5, topics = []) |
POST /knowledge/search |
SearchResults |
consent() |
GET /visitors/me/consent |
ConsentStatus |
giveConsent(noticeVersion, allowTraining = false) |
PUT /visitors/me/consent |
ConsentStatus |
withdrawConsent() |
DELETE /visitors/me/consent |
ConsentStatus |
exportData() |
GET /visitors/me/data |
VisitorData |
eraseData() |
DELETE /visitors/me |
Erasure |
forEndUser(id) / withEndUser(id) |
— | copie du client |
products() |
GET /products |
list<Product> |
tutor()->explain(lesson, messages, mode, learner, locale) |
POST /tutor/explanations |
TutorReply |
tutor()->quiz(lesson, count, difficulty, kinds, learner, locale) |
POST /tutor/quizzes |
Quiz |
tutor()->flashcards(lesson, count, learner, locale) |
POST /tutor/flashcards |
Flashcards |
tutor()->grade(lesson, question, answer, countryCode, locale) |
POST /tutor/grades |
Grade |
trust()->assess(listings) |
POST /trust/assessments |
TrustResult |
embeddings()->create(texts) |
POST /embeddings |
Embeddings |
Webhook::verify(payload, signatureHeader, secret, tolerance = 300) |
— | WebhookEvent |
Les méthodes appelées pour un utilisateur acceptent aussi les arguments nommés endUser: (pour un
seul appel) et requestId:.
Les types (ValaisConnect\Gaspard\Model\…) exposent des propriétés en camelCase
($answer->remainingMessages, $listing->sourceName…) ; les champs inconnus envoyés par le serveur
sont ignorés. toArray() et json_encode() redonnent le format de l'API (snake_case).
Produits du hub (clé serveur) #
Le tuteur, la confiance et les embeddings sont activés par application par l'équipe HubMix
(sur demande) ; sans le produit, l'API répond 403 insufficient_scope. Ils demandent une clé
serveur : avec une clé publiable, tutor(), trust() et embeddings() lèvent une
InvalidArgumentException avant toute requête. Les valeurs imbriquées (leçon, apprenant, annonce)
sont des tableaux aux noms de l'API (snake_case) ; seuls les arguments donnés sont envoyés.
use ValaisConnect\Gaspard\Laravel\Facades\Gaspard;
// Confiance : à la publication d'une annonce
$result = Gaspard::trust()->assess([[
'kind' => 'housing',
'title' => $listing->title,
'description' => $listing->description,
'price_amount' => $listing->rent,
'currency' => 'EUR',
'country_code' => 'FR',
'city' => 'Lyon',
]]);
if ($result->assessments[0]->withheld) {
$listing->holdForReview($result->assessments[0]->findings);
}
// Tuteur : un quiz sur un cours
$quiz = Gaspard::tutor()->quiz(
lesson: ['title' => 'La photosynthèse', 'content' => $course->text],
count: 5,
difficulty: 'medium',
learner: ['level' => 'bachelor', 'country_code' => 'SN'],
locale: 'fr',
);
// Embeddings : un vecteur par texte, dans le même ordre
$vectors = Gaspard::embeddings()->create(['chambre à louer', 'room for rent'])->embeddings;