Documentation
.NET et ASP.NET Core
Le SDK NuGet de Gaspard pour .NET 8+ : AddGaspard() pour l’injection de dépendances, IAsyncEnumerable pour le flux.
SDK officiel .NET du hub HubMix : Gaspard, l'assistant IA (conversations en réponse
complète ou en streaming, recherche dans sa base de connaissances, sessions visiteurs et droits des
personnes, nLPD) et, depuis la 1.1.0, le tuteur, la confiance (signaux d'arnaque des
annonces), les embeddings et la vérification des webhooks (portail développeurs retiré le 28 octobre 2026 : plus aucun webhook n'est envoyé). Il
enveloppe l'API publique /v1/public.
- .NET 8 ou plus récent,
asyncpartout,IAsyncEnumerablepour le streaming. AddGaspard()pour l'injection de dépendances (IHttpClientFactory).- JSON par
System.Text.Jsonavec un contexte généré (compatible trimming / Native AOT).
Installation #
dotnet add package ValaisConnect.GaspardLes deux sortes de clés #
| Clé | Où | Option | En-tête envoyé |
|---|---|---|---|
Secrète vck_live_… |
Serveur uniquement (ASP.NET Core, worker, console) | ApiKey |
Authorization: Bearer … |
Publiable vcp_live_… |
Application de bureau ou mobile (.NET MAUI, WPF…) | PublishableKey + Origin |
X-Gaspard-Key, Origin, puis le jeton visiteur |
Une seule des deux est acceptée ; toute erreur de configuration lève une ArgumentException (ou
une OptionsValidationException au démarrage avec AddGaspard) avant la moindre requête.
Ne mettez jamais la clé secrète dans une application distribuée.
Options #
| Option | Défaut | Rôle |
|---|---|---|
ApiKey / PublishableKey |
— | Une des deux, obligatoire. |
Origin |
— | Avec une clé publiable hors navigateur : une des origines déclarées pour le client. |
EndUser |
— | Clé serveur : identifiant de votre utilisateur (X-End-User, 1–128 caractères ASCII imprimables, seul un hachage est conservé). |
BaseUrl |
https://hubmix.dealmood-solutions.com |
Hôte du service ; /v1/public est ajouté. |
Timeout |
60 s | Par requête ; un stream n'est limité que jusqu'à l'arrivée des en-têtes. |
MaxRetries |
2 | Nouvelles tentatives (voir Erreurs et nouvelles tentatives), 0 les désactive. |
UserAgent |
gaspard-dotnet/1.1.0 |
En-tête User-Agent. |
ASP.NET Core (API minimale) #
appsettings.json (la clé elle-même vient plutôt des user secrets ou de la variable
d'environnement Gaspard__ApiKey) :
{
"Gaspard": {
"ApiKey": "vck_live_…",
"BaseUrl": "https://hubmix.dealmood-solutions.com",
"Timeout": "00:01:00",
"MaxRetries": 2
}
}Program.cs :
using ValaisConnect.Gaspard;
var builder = WebApplication.CreateBuilder(args);
// Client typé enregistré via IHttpClientFactory, options validées au démarrage.
builder.Services.AddGaspard(builder.Configuration.GetSection("Gaspard"));
// ou : builder.Services.AddGaspard(o => o.ApiKey = builder.Configuration["Gaspard:ApiKey"]);
var app = builder.Build();
app.MapPost("/chat", async (ChatRequest request, GaspardClient gaspard, HttpContext http, CancellationToken ct) =>
{
// Chaque utilisateur de votre service a ses propres conversations.
var client = gaspard.ForEndUser(http.User.Identity?.Name ?? "anonyme");
var conversationId = request.ConversationId
?? (await client.StartConversationAsync("fr", page: "aide", cancellationToken: ct)).Id;
var answer = await client.AskAsync(conversationId, request.Text, cancellationToken: ct);
return Results.Ok(new { conversationId, answer.Text, answer.Sources, answer.MessageId });
});
app.Run();
record ChatRequest(string? ConversationId, string Text);AddGaspard renvoie l'IHttpClientBuilder : vous pouvez y ajouter vos propres handlers
(journalisation, résilience…). Le HttpClient enregistré n'a pas de délai propre
(Timeout.InfiniteTimeSpan) : c'est GaspardOptions.Timeout qui s'applique à chaque requête, sans
couper les streams.
Streaming vers le navigateur (réémission SSE) #
using System.Text.Json;
app.MapPost("/chat/stream", async (ChatRequest request, GaspardClient gaspard, HttpResponse response, CancellationToken ct) =>
{
response.Headers.ContentType = "text/event-stream";
response.Headers.CacheControl = "no-cache";
response.Headers["X-Accel-Buffering"] = "no";
try
{
await foreach (var item in gaspard.AskStreamAsync(request.ConversationId!, request.Text, cancellationToken: ct))
{
if (item.IsDelta)
{
await response.WriteAsync($"event: delta\ndata: {JsonSerializer.Serialize(new { text = item.Text })}\n\n", ct);
}
else
{
var json = JsonSerializer.Serialize(item.Answer!, GaspardJsonContext.Default.Answer);
await response.WriteAsync($"event: answer\ndata: {json}\n\n", ct);
}
await response.Body.FlushAsync(ct);
}
}
catch (GaspardException error)
{
await response.WriteAsync($"event: error\ndata: {JsonSerializer.Serialize(new { code = error.Code })}\n\n", ct);
}
});GaspardJsonContext.Default sérialise les types du SDK avec les noms du fil (snake_case), ce qui
permet de renvoyer la réponse telle quelle à un widget.
Console #
using ValaisConnect.Gaspard;
var gaspard = new GaspardClient(new GaspardOptions
{
ApiKey = Environment.GetEnvironmentVariable("GASPARD_API_KEY"),
});
var conversation = await gaspard.StartConversationAsync();
await foreach (var item in gaspard.AskStreamAsync(conversation.Id, "Comment trouver un logement étudiant à Lyon ?"))
{
if (item.IsDelta) Console.Write(item.Text);
else Console.WriteLine($"\n\nSources : {string.Join(", ", item.Answer!.Sources.Select(s => s.Name))}");
}
var passages = await gaspard.SearchKnowledgeAsync("Montréal universités", limit: 3, topics: [KnowledgeTopic.Education]);
foreach (var passage in passages.Passages)
{
Console.WriteLine($"{passage.Title} ({passage.Score:0.00})");
}Le constructeur GaspardClient(GaspardOptions) utilise un HttpClient partagé et mutualisé ; sur un
serveur, préférez AddGaspard ou new GaspardClient(httpClient, options).
Application de bureau ou mobile (clé publiable) #
var gaspard = new GaspardClient(new GaspardOptions
{
PublishableKey = "vcp_live_…",
Origin = "https://mon-app.example.ch", // une des origines déclarées pour la clé
});
var notice = await gaspard.PrivacyNoticeAsync("fr");
// … afficher notice.Text ; si la personne accepte :
var session = await gaspard.OpenSessionAsync(consent: true, locale: "fr");
SaveSomewhereSafe(session.Token); // valable 30 jours
// Au prochain lancement :
gaspard.SetVisitorToken(LoadToken()); // ou null pour l'oublierOpenSessionAsync(false) lève une ArgumentException : le consentement doit avoir été donné.
Avec une clé serveur, OpenSessionAsync et SetVisitorToken lèvent une
InvalidOperationException.
Utilisateurs finaux (clé serveur) #
L'en-tête X-End-User sépare les conversations et les données de chacun de vos utilisateurs :
var forAlice = gaspard.ForEndUser("user-1842"); // client dérivé, même transport
await forAlice.GetConsentAsync();
await gaspard.ExportDataAsync(new GaspardRequestOptions { EndUser = "user-1842" }); // par appelDroits des personnes : GetConsentAsync, GiveConsentAsync(noticeVersion, allowTraining),
WithdrawConsentAsync, ExportDataAsync, EraseDataAsync (avec une clé publiable, le jeton
visiteur est oublié après l'effacement).
Toutes les méthodes #
| Méthode | Requête |
|---|---|
PrivacyNoticeAsync(locale = "fr") |
GET /privacy-notice |
OpenSessionAsync(consent, locale, allowTraining) |
POST /sessions |
StartConversationAsync(locale = "fr", page) |
POST /gaspard/conversations |
AskAsync(conversationId, text, firstName) |
POST /gaspard/conversations/{id}/messages |
AskStreamAsync(conversationId, text, firstName) |
idem, Accept: text/event-stream |
GetConversationAsync(conversationId) |
GET /gaspard/conversations/{id} |
FeedbackAsync(messageId, Rating.Up/Down, comment) |
POST /gaspard/messages/{id}/feedback |
SearchKnowledgeAsync(query, limit = 5, topics) |
POST /knowledge/search |
GetConsentAsync() / GiveConsentAsync(…) / WithdrawConsentAsync() |
GET / PUT / DELETE /visitors/me/consent |
ExportDataAsync() |
GET /visitors/me/data |
EraseDataAsync() |
DELETE /visitors/me |
ListProductsAsync() |
GET /products |
Tutor.ExplainAsync(TutorExplainRequest) |
POST /tutor/explanations |
Tutor.QuizAsync(TutorQuizRequest) |
POST /tutor/quizzes |
Tutor.FlashcardsAsync(TutorFlashcardsRequest) |
POST /tutor/flashcards |
Tutor.GradeAsync(TutorGradeRequest) |
POST /tutor/grades |
Trust.AssessAsync(listings) |
POST /trust/assessments |
Embeddings.CreateAsync(texts) |
POST /embeddings |
GaspardWebhook.Verify(payload, signature, secret) (statique) |
— |
Chaque méthode accepte aussi GaspardRequestOptions (RequestId → X-Request-Id, EndUser) et un
CancellationToken. Les champs inconnus renvoyés par le serveur sont ignorés ; une valeur
d'énumération inconnue devient Unknown (par ex. ListingKind.Unknown).
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, l'appel lève InvalidOperationException avant toute requête.
Les valeurs que l'API peut étendre (niveaux, verdicts, signaux) sont des chaînes.
// Confiance : à la publication d'une annonce
var result = await gaspard.Trust.AssessAsync([
new TrustListing
{
Kind = "housing", Title = listing.Title, Description = listing.Description,
PriceAmount = listing.Rent, Currency = "EUR", CountryCode = "FR", City = "Lyon",
},
]);
if (result.Assessments[0].Withheld) { /* à faire vérifier ; Findings dit pourquoi */ }
// Tuteur : des cartes de révision
var cards = await gaspard.Tutor.FlashcardsAsync(new TutorFlashcardsRequest
{
Lesson = new Lesson { Title = "La photosynthèse", Content = courseText },
Count = 8,
Learner = new Learner { Level = "bachelor", CountryCode = "SN" },
Locale = "fr",
});
// Embeddings : un vecteur par texte, dans le même ordre
var vectors = (await gaspard.Embeddings.CreateAsync(["chambre à louer", "room for rent"])).Embeddings;Erreurs et nouvelles tentatives #
Tout échec est une GaspardException :
| Propriété | Contenu |
|---|---|
Status |
Statut HTTP, 0 sans réponse (réseau, délai, erreur en cours de stream). |
Code |
error.code de l'API (consent_required, assistant_busy, token_quota_exceeded…), sinon http_<statut> ; network_error, timeout, invalid_response, stream_incomplete. |
Message |
Explication destinée aux développeurs. |
RequestId |
request_id du corps ou en-tête X-Request-Id (à communiquer au support). |
RetryAfter |
Délai de l'en-tête Retry-After, s'il existe (TimeSpan?). |
try
{
var answer = await gaspard.AskAsync(conversationId, question);
}
catch (GaspardException error) when (error.Code == "conversation_full")
{
// démarrer une nouvelle conversation
}
catch (GaspardException error) when (error.Code == "token_quota_exceeded")
{
logger.LogWarning("Quota atteint, réessayer dans {Delay}", error.RetryAfter);
}Le SDK réessaie seul, jusqu'à MaxRetries fois : 503 assistant_busy, 429 rate_limited et, pour
les GET et DELETE seulement, les erreurs réseau et les délais dépassés. Il attend Retry-After
(au-delà de 30 s, il ne réessaie pas et lève l'erreur), sinon 0,5 s × 2^tentative. Jamais
token_quota_exceeded, ni les autres 4xx, ni un stream déjà commencé. Réessayer une question est
sans risque : le serveur n'enregistre un tour qu'avec sa réponse.
Un stream qui se termine par un événement error lève une GaspardException portant ce code
(503 pour assistant_busy/assistant_unavailable, 409 pour conversation_full). L'annulation
par votre CancellationToken lève une OperationCanceledException, pas une GaspardException.
Concurrence #
GaspardClient est sûr entre threads : une instance (ou le client typé de AddGaspard) sert des
requêtes concurrentes. Seul le jeton visiteur d'un client à clé publiable est propre à
l'instance : une instance par visiteur. ForEndUser crée un client dérivé sans état partagé
modifiable.