Usare i servizi di Azure OpenAI e ChatGPT in ASP.NET Core con Semantic Kernel

di Marco De Sanctis, in ASP.NET Core,

Probabilmente è persino superfluo spiegare i vantaggi e le peculiarità dei Large Language Model (LLM) nell'ambito delle applicazioni moderne. Grazie a OpenAI - o ad Azure OpenAI - possiamo aggiungere funzionalità alle nostre applicazioni che erano assolutamente impensabili fino a un paio di anni fa.

Nel corso di questo script, e dei successivi, vedremo come possiamo integrare questi servizi in un'applicazione ASP.NET Core.

Il primo passo è quello di determinare quale libreria vogliamo usare per interfacciarci con i modelli. Esistono diverse opzioni, dalle librerie native di OpenAI, all'Azure OpenAI SDK. Tuttavia, uno degli approcci più utilizzati è quello di sfruttare Semantic Kernel, una libreria open source di Microsoft che costituisce un livello di astrazione intermedio tra il nostro codice e le API native.

Quali sono i vantaggi?

  • Innanzi tutto il fatto che abbiamo un'unica codebase, che possiamo usare per interagire con diversi modelli, spesso cambiando solo alcune righe di codice nella configurazione.
  • Inoltre, Semantic Kernel ha una serie di primitive, di oggetti e componenti di alto livello, che implementano già molte delle logiche che altrimenti dovremmo realizzare autonomamente.
  • Infine, la libreria tende a essere un po' più stabile in termini di breaking change tra una versione e l'altra, rispetto alle controparti native.

Iniziamo a vedere un esempio base, che poi renderemo via via più complesso nei prossimi script.

Come al solito, il primo passo è quello di aggiungere il pacchetto NuGet al progetto ASP.NET Core:

dotnet add package Microsoft.SemanticKernel

Il modo più semplice per sfruttare il Chat Completion endpoint di OpenAI è tramite un servizio chiamato IChatCompletionService, che possiamo registrare nella dependency injection di ASP.NET Core:

public static void Main(string[] args)
{
    var builder = WebApplication.CreateBuilder(args);

    // Altro codice qui...

    builder.Services.Configure<AzureConfig>(builder.Configuration.GetSection("AzureConfig"));

    builder.Services.AddSingleton<IChatCompletionService>(sp =>
    {
        AzureConfig options = sp.GetRequiredService<IOptions<AzureConfig>>().Value;

        return new AzureOpenAIChatCompletionService(
            options.OpenAi.DeploymentName,
            options.OpenAi.OpenAiEndpoint,
            options.OpenAi.OpenAiKey);
    });

    // .. altro codice qui ..
}

Nel codice in alto abbiamo registrato IChatCompletionService come singleton, in particolare restituendo un'istanza di AzureOpenAIChatCompletionService, ossia la versione di questo servizio fatta per interfacciarsi con Azure OpenAI. Se invece stessimo utilizzando OpenAI, avremmo dovuto restituire un'istanza di OpenAIChatCompletionService.

Come possiamo notare, in fase di inizializzazione, abbiamo recuperato gli estremi del servizio dalla configurazione, e poi passato DeploymentName, Endpoint e Key al costruttore.

A questo punto siamo pronti per sfruttare questo oggetto in un ChatController:

[Route("api/[controller]")]
[ApiController]
public class ChatController : ControllerBase
{
    private IChatCompletionService _chatCompletionService;

    public static ChatHistory ChatHistory { get; } = new ChatHistory(
        "You are a useful AI who answers questions using rhymes.");

    public ChatController(IChatCompletionService chatCompletionService)
    {
        _chatCompletionService = chatCompletionService;
    }
}

Il controller in alto, oltre che iniettare un'istanza di IChatCompletionService, mantiene anche una ChatHistory (qui per semplicità, in un field static), che conterrà l'elenco dei messaggi scambiati con la AI: i modelli GPT sono infatti stateless, e pertanto l'unico modo con cui possono avere cognizione dei messaggi passati è quello di mandare l'intera history a ogni richiesta.

Quando creiamo questo oggetto, dobbiamo anche specificare il cosidetto System Prompt, ossia le istruzioni di base per il modello GPT su come comportarsi durante la chat.

Ora non ci resta che creare un'action tramite cui inviare e ricevere messaggi:

[HttpPost]
public async Task<IActionResult> PostMessage([FromBody] string message)
{
    ChatHistory.AddUserMessage(message);

    var result = await _chatCompletionService.GetChatMessageContentAsync(ChatHistory);
            
    string responseMessage = result.ToString();

    ChatHistory.AddAssistantMessage(responseMessage);

    return Ok(responseMessage);
}

Questa action, invocabile in POST, accetta un messaggio da parte dell'utente che, come prima cosa, viene aggiunto alla History. Successivamente invoca GetChatMessageContentAsync passando l'intera history, così che - come detto - il modello abbia conoscenza non solo dell'ultimo messaggio dell'utente, ma anche di tutto il resto della conversazione eseguita fino a quel momento.

Una volta ottenuta la risposta, non dobbiamo far altro che restituirla al client.

Commenti

Visualizza/aggiungi commenti

| Condividi su: Twitter, Facebook, LinkedIn

Per inserire un commento, devi avere un account.

Fai il login e torna a questa pagina, oppure registrati alla nostra community.

Approfondimenti

I più letti di oggi