HubMix

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, async partout, IAsyncEnumerable pour le streaming.
  • AddGaspard() pour l'injection de dépendances (IHttpClientFactory).
  • JSON par System.Text.Json avec un contexte généré (compatible trimming / Native AOT).

Installation #

Terminal
dotnet add package ValaisConnect.Gaspard

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

JSON
{
  "Gaspard": {
    "ApiKey": "vck_live_…",
    "BaseUrl": "https://hubmix.dealmood-solutions.com",
    "Timeout": "00:01:00",
    "MaxRetries": 2
  }
}

Program.cs :

C#
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) #

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

C#
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) #

C#
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'oublier

OpenSessionAsync(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 :

C#
var forAlice = gaspard.ForEndUser("user-1842");                 // client dérivé, même transport
await forAlice.GetConsentAsync();
await gaspard.ExportDataAsync(new GaspardRequestOptions { EndUser = "user-1842" }); // par appel

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

C#
// 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?).
C#
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.