Popolare una classe a partire dal testo, con Semantic Kernel e ASP.NET Core Web API

di Marco De Sanctis, in ASP.NET Core,

Uno dei compiti da sempre più complessi nello sviluppo software è quello di ricostruire dati strutturati a partire da semplice testo. Nello script precedente (https://www.aspitalia.com/script/1492/Inference-Dati-Strutturati-Testo-Semantic-Kernel-ASP.NET-Core-Web.aspx) abbiamo visto come, grazie a Semantic Kernel e Azure OpenAI, possiamo finalmente assolvere a questo compito in maniera piuttosto semplice, sfruttando le capacità dei Large Language Model.

Tuttavia, uno dei limiti dell'esempio visto è che si limita a forzare l'output in formato JSON, ma senza specificarne uno schema. In questo script, invece, vedremo come possiamo specificare con assoluta precisione, proprietà e regole che vogliamo estrarre dal testo.

Prima di procedere, dato che si tratta di una funzionalità piuttosto nuova, è importante verificare un paio di pre-requisiti. Intanto il model deve essere di tipo GPT-4o, versione 2024-08-06 o successiva:


Inoltre, anche Semantic Kernel deve essere aggiornato almeno alla versione 1.25:

<PackageReference Include="Microsoft.SemanticKernel" Version="1.25.0" />

Immaginiamo ora di avere a disposizione una recensione di un hotel, e di voler popolare una classe come la seguente:

public class ReviewDetails
{
    [Description("The name of the hotel of the review, if present in the text")]
    public string HotelName { get; set; }

    [Description("The number of nights spent in the hotel, if present in the text")]
    public int? Duration { get; set; }

    [Required]
    [Description(@"The sentiment of the review, from 0 to 1. 
        0 means that the experience was terrible, 0.5 means average, 
        1 means that the experience was perfect. 
        Can have decimals to represent a spectrum of sentiments")]
    public float SentimentValue { get; set; }

    [Description(@"Top 3 positive notes of the review, if present in the text. 
        This needs to be translated in English.")]
    public string[] PositiveNotes { get; set; }

    [Description(@"Top 3 negative notes of the review, if present in the text. 
        This needs to be translated in English.")]
    public string[] NegativeNotes { get; set; }

    [Required]
    [Description("The type of customer that wrote the review, if present in the text")]
    public CustomerType CustomerType { get; set; }
}

Come possiamo vedere, abbiamo usato le data annotation, e in particolare Description, per spiegare il significato di ogni proprietà e le regole con cui queste devono essere popolate. Scrivere buone descrizioni è assolutamente cruciale affinché il modello riesca a capire bene quale tipo di informazioni esse contengono ed eventuali constraint. Per esempio, nel caso delle Positive e Negative notes, abbiamo specificato che il testo deve essere tradotto in inglese e che debbano al più contenere 3 elementi ognuna.

Un altro paio di funzionalità interessanti sono l'uso dell'attributo Required per indicare quali proprietà sono obbligatorie, e aver sfruttato un enum CustomerType per indicare la tipologia di cliente, anche quest'ultimo decorato con descrizioni dove sia necessario:

public enum CustomerType
{
    Single,
    Couple,
    Family,
    Business,
    Group,

    [Description("Use this value if the customer type cannot be inferred")]
    Unspecified
}

A questo punto siamo pronti a creare il nostro InferenceController, che espone un endpoint in grado di restituire un ReviewDetails a partire dal testo in input:

public class InferenceController : ControllerBase
{
    private Kernel _kernel;

    public InferenceController(Kernel kernel)
    {
        _kernel = kernel;
    }

    [HttpPost]
    public async Task<ReviewDetails> GetReviewDetails([FromBody] string reviewText)
    {
        // .. altro codice qui ..
    }
}

Dato che non stiamo esponendo una funzionalità di Chat, possiamo evitare di iniettare un IChatCompletionService e semplicemente sfruttare il Kernel per la nostra singola richiesta al model. Guardiamo nel dettaglio il codice di GetReviewDetails:

[HttpPost]
public async Task<ReviewDetails> GetReviewDetails([FromBody] string reviewText)
{
    string prompt = string.Format(
        @"You are an AI which helps storing review data. You will be prompted with a review text, 
        between triple backticks.
        For example a prompt could be like:
        This is an example of a review:
        ```Hotel Corinthia was simply stunning```

        You need to infer the important details from the review text following the provided JSON schema.

        This is the actual review to analyze:

        ```{0}```", reviewText);

    var executionSettings = new AzureOpenAIPromptExecutionSettings()
    {
        ResponseFormat = typeof(ReviewDetails),
    };

    var result = await _kernel.InvokePromptAsync(reviewText, new KernelArguments(executionSettings));

    Console.WriteLine(result.ToString());

    var review = JsonSerializer.Deserialize<ReviewDetails>(result.ToString());

    return review;

}

Come prima cosa abbiamo definito il prompt da utilizzare, in cui spieghiamo al model i requisiti e il suo compito. In questo testo, tuttavia, non abbiamo ancora specificato il formato di output, che invece indichiamo negli ExecutionSettings, tramite la stessa opzione ResponseFormat che abbiamo introdotto nello script precedente. Questa volta, però, il valore del parametro sarà proprio il type ReviewDetails che vogliamo in output.

A questo punto, il gioco è fatto: non dobbiamo far altro che eseguire il prompt, e il modello ci restituirà un JSON che possiamo direttamente deserializzare in ReviewDetails, per poi restituirlo come risposta.

Per testarlo, proviamo a inviare il seguente testo:
Ridgemount Hotel,
Staff stupendo e b&b perfetto!,"Io e la mia famiglia abbiamo soggiornato un weekend in questo hotel rimanendo soddisfattissime di tutto! Eravamo 6 in una camera nonostante fossimo tante siamo state benissimo anche avendo il bagno sul piano non abbiamo avuto problemi. Inoltre era tutto pulitissimo! | Colazione british da 10! Buonissima! | Lo staff per noi tutte è stato il valore aggiunto di questo hotel, tutti super gentili simpatici disponibili e cordiali. Persone splendide! | Noi tutte e 6 ringraziamo lo staff e consigliamo vivamente il Ridgemount Hotel! | Ps inoltre posizione ottimale per visitare Londra..a 5 min dal British Museum!",
Firenze,10/12/2015


Se abbiamo svolto i passaggi correttamente, otterremo un risultato simile al seguente:

{
  "hotelName": "Ridgemount Hotel",
  "duration": 2,
  "sentimentValue": 1,
  "positiveNotes": [
    "Staff was amazing and friendly",
    "Breakfast was delicious",
    "Hotel was very clean"
  ],
  "negativeNotes": [],
  "customerType": "Family"
}

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