Azure OpenAI verwenden, um einen Copiloten für eigenen Daten zu erstellen – Teil 2

Während der Microsoft Build Conference 2023 in Seattle stellte Microsoft die Möglichkeit vor, eigene Daten zum Azure OpenAI Service hinzuzufügen. In meinem letzten Beitrag habe ich euch bereits gezeigt, wie ihr einen Azure OpenAI-Dienst einrichten und eigenen Daten hinzufügen könnt. In diesem Beitrag wollen wir nun eine kleine C#-Konsolenapplikation schreiben, welche die Daten konsumiert. Dieser Beitrag ist auch in englischer Sprache bereits auf Medium veröffentlicht worden.

Einführung

In meinem letzten Beitrag habe ich euch gezeigt, wie ihr die erforderlichen Azure-Ressourcen einrichten und eure benutzerdefinierten Daten hinzufügen könnt. In diesem Beitrag werden wir eine Konsolenanwendung in C# schreiben, um auf eure Daten zuzugreifen.

Allgemeiner Prozess

Bevor wir mit der Implementierung unserer Konsolenanwendung beginnen, möchte ich euch einen Flussdiagramm zeigen, das erklärt, wie die Kommunikation funktioniert.

Zuerst gibt der Benutzer eine Nachricht oder eine Frage ein. Diese Nachricht wird zusammen mit der gesamten Chat-Historie an unseren Azure OpenAI-Dienst gesendet, um unserem Dienst etwas Kontext zu geben. In dieser Anfrage haben wir bereits Informationen über unsere Datenquelle, in unserem Fall ein Azure Cognitive Service. Nun generiert der Azure OpenAI-Dienst eine Abfrage aus der Benutzer-Nachricht, sucht nach den Dokumenten im Azure Cognitive Search-Dienst und verwendet schließlich die Fähigkeiten des Large Language Models, um eine Antwort für den Benutzer zu generieren, die wir ihm anzeigen werden.

Schreiben einer C#-Konsolenapplikation

Nun, da wir alle Azure-Ressourcen eingerichtet haben, können wir Visual Studio starten und eine einfache Konsolenanwendung in C# implementieren, um auf unseren Copilot zuzugreifen.

Erstellt eine neue Konsolenanwendung. Ich werde sie SimpleAzureOpenAIChat nennen, aber ihr könnt einen beliebigen Namen wählen.

Fügt das NuGet-Paket Microsoft.Extensions.Configuration.UserSecrets zu eurer Lösung hinzu, da wir die Anmeldeinformationen über Benutzer-Secrets hinzufügen möchten.

Nachdem das NuGet-Paket hinzugefügt wurde, öffnet ein neues Terminalfenster in Visual Studio, indem ihr auf „View > Terminal“ klickt. Verwendet den Befehl cd, um zum Projektordner zu navigieren.

Ruft nun den folgenden Befehl auf: dotnet user-secrets init, um die Benutzer-Secrets für das Projekt zu initialisieren. Im nächsten Schritt müssen wir sechs verschiedene Secrets hinzufügen. Verwendet einfach den Befehl dotnet user-secrets set und fügt die Werte nacheinander hinzu.

dotnet user-secrets set Azure:Search:Endpoint <COMPLETE ENDPOINT>

dotnet user-secrets set Azure:Search:ApiKey <API KEY>

dotnet user-secrets set Azure:Search:IndexName <INDEX NAME>


dotnet user-secrets set Azure:OpenAI:Resource = <RESOURCE NAME>

dotnet user-secrets set Azure:OpenAI:Deployment = <DEPLOYMENT NAME>

dotnet user-secrets set Azure:OpenAI:ApiKey = <API KEY>

Ihr könnt die Werte entweder aus dem Azure-Portal (für den Azure Cognitive Search) oder durch Klicken auf die Schaltfläche „View Code“ im Chat-Sitzungs-Panel in Azure AI Studio erhalten.

Nun, da wir alle benötigten Anmeldeinformationen zu den Benutzer-Secrets hinzugefügt haben, können wir mit dem Hinzufügen der Model-Klassen beginnen. Momentan gibt es kein von Microsoft bereitgestelltes NuGet-Paket, um mit den benutzerdefinierten Daten zu arbeiten, was bedeutet, dass wir die Aufrufe selbst implementieren müssen.

Fügt einen Ordner namens „Models“ hinzu und darin eine Klasse mit dem Namen ChatMessage mit dem folgenden Inhalt hinzu:

using System.Text.Json.Serialization;

namespace SimpleAzureOpenAIChat.Models;


public record ChatMessage(
    [property: JsonPropertyName("role")] string? Role, 
    [property: JsonPropertyName("content")] string? Content);

Fügt dem Ordner „Models“ eine weitere Klasse namens Request hinzu. Diese Datei enthält die Struktur, um ein Request-Objekt zu erstellen, das als Body in der Anfrage gesendet wird.

using System.Text.Json.Serialization;

namespace SimpleAzureOpenAIChat.Models;


public record RequestBody(
    [property: JsonPropertyName("messages")] List<ChatMessage>? Messages,
    [property: JsonPropertyName("dataSources")] List<DataSource>? DataSources,
    [property: JsonPropertyName("temperature")] float Temperature = 0f,
    [property: JsonPropertyName("max_tokens")] int MaxTokens = 800,
    [property: JsonPropertyName("top_p")] float TopP = 1.0f,
    [property: JsonPropertyName("stop")] string? Stop = null,
    [property: JsonPropertyName("stream")] bool Stream = false);

public record DataSource(
    [property: JsonPropertyName("parameters")] DataSourceParameters? Parameters,
    [property: JsonPropertyName("type")] string? Type = "AzureCognitiveSearch");

public record DataSourceParameters(
    [property: JsonPropertyName("endpoint")] string? SearchEndpoint,
    [property: JsonPropertyName("key")] string? SearchApiKey,
    [property: JsonPropertyName("indexName")] string? SearchIndexName,
    [property: JsonPropertyName("roleInformation")] string? RoleInformation,
    [property: JsonPropertyName("fieldsMapping")] DataSourceParametersFieldsMapping? FieldsMapping,
    [property: JsonPropertyName("inScope")] bool InScope = true,
    [property: JsonPropertyName("topNDocuments")] string? TopNDocuments = "5",
    [property: JsonPropertyName("queryType")] string? QueryType = "simple",
    [property: JsonPropertyName("semanticConfiguration")] string? SemanticConfiguration = "");

public record DataSourceParametersFieldsMapping(
    [property: JsonPropertyName("contentField")] string? ContentField = "",
    [property: JsonPropertyName("titleField")] string? TitleField = null,
    [property: JsonPropertyName("urlField")] string? UrlField = null,
    [property: JsonPropertyName("filepathField")] string? FilePathField = null);


Wir fügen auch die Datei Response zum Ordner „Models“ hinzu. Wie ihr vermutet habt, enthält diese Datei die Struktur der Antwort vom Azure OpenAI-Endpunkt.

using Microsoft.Extensions.Configuration;

namespace SimpleAzureOpenAIChat.Helpers;

public static class HttpClientHelper
{
    public static HttpClient CreateHttpClient(IConfigurationRoot configuration)
    {
        var httpClient = new HttpClient();

        httpClient.DefaultRequestHeaders.Add(
            "api-key", 
            configuration["Azure:OpenAI:ApiKey"]);

        httpClient.DefaultRequestHeaders.Add(
            "chatgpt_url", 
            $"https://{configuration["Azure:OpenAI:Resource"]}.openai.azure.com/openai/" +
            $"deployments/{configuration["Azure:OpenAI:Deployment"]}/" +
            $"completions?api-version=2023-03-15-preview");

        httpClient.DefaultRequestHeaders.Add(
            "chatgpt_key", 
            configuration["Azure:OpenAI:ApiKey"]);

        httpClient.DefaultRequestHeaders.Add(
            "x-ms-useragent", 
            "MyCopilotOnData/0.0.1");

        return httpClient;
    }
}

Als Nächstes erstellen wir eine weitere Datei im Ordner „Helpers“ namens RequestHelper. Diese Klasse wird das RequestBody-Objekt erstellen, das an den Endpunkt gesendet wird.


using Microsoft.Extensions.Configuration;
using SimpleAzureOpenAIChat.Models;

namespace SimpleAzureOpenAIChat.Helpers;


public static class RequestHelper
{
    public static RequestBody CreateRequestBody(
        List<ChatMessage> messages,
        IConfigurationRoot configuration)
    {
        return new RequestBody(
            messages,
            new List<DataSource>
            {
                new DataSource(
                    new DataSourceParameters(
                        configuration["Azure:Search:Endpoint"],
                        configuration["Azure:Search:ApiKey"],
                        configuration["Azure:Search:IndexName"],
                        "You are the Copilot of Sebastian and " +
                        "you help with his Medium blog posts.",
                        new DataSourceParametersFieldsMapping()))
            });
    }
}

Bevor wir uns die Datei „Program.cs“ ansehen können, müssen wir einen weiteren Ordner namens „ExtensionMethods“ erstellen. Hier werden wir die Klasse HttpResponseMessageExtensions erstellen. Diese Datei hilft uns dabei, die Antwort des HTTP-Requests zu deserialisieren, um die benötigten Informationen zu erhalten.

using SimpleAzureOpenAIChat.Models;
using System.Text.Json;

namespace SimpleAzureOpenAIChat.ExtensionMethods;


public static class HttpResponseMessageExtensions
{
    public static async Task<ChatMessage?> GetAssistantChatMessageAsync(
        this HttpResponseMessage httpResponseMessage)
    {
        if (!httpResponseMessage.IsSuccessStatusCode)
            throw new Exception();

        string content = await httpResponseMessage.Content
            .ReadAsStringAsync();

        RequestResponse? response = JsonSerializer
            .Deserialize<RequestResponse>(content);

        return response?
            .Choices?.FirstOrDefault()?
            .Messages?
            .FirstOrDefault(m => m.Role == "assistant");
    }
}

Nun können wir uns dem „richtigen“ Programm widmen. Öffnet die Datei Program.cs und fügt den folgenden Codeausschnitt ein:

using Microsoft.Extensions.Configuration;
using SimpleAzureOpenAIChat.ExtensionMethods;
using SimpleAzureOpenAIChat.Helpers;
using SimpleAzureOpenAIChat.Models;
using System.Net.Http.Json;
using System.Text.Json;

// 1. Make user secrets accessible
IConfigurationRoot configuration = new ConfigurationBuilder()
    .AddUserSecrets<Program>()
    .Build();

// 2. Setup messages list
List<ChatMessage> messages = new();

// 3. Create httpClient with headers
HttpClient httpClient = HttpClientHelper.CreateHttpClient(configuration);

while (true)
{
    // 4. Get message from the user
    Console.WriteLine("USER MESSAGE:");
    string? messageContent = Console.ReadLine();
    messages.Add(new ChatMessage("user", messageContent));

    // 5. Create request
    RequestBody requestBody = RequestHelper.CreateRequestBody(messages, configuration);

    // 6. Make request
    string endpoint = $"https://{configuration["Azure:OpenAI:Resource"]}.openai.azure.com/" +
        $"openai/deployments/{configuration["Azure:OpenAI:Deployment"]}/" +
        $"extensions/chat/completions?api-version=2023-06-01-preview";

    HttpResponseMessage response = await httpClient
        .PostAsJsonAsync(endpoint, requestBody);

    // 7. Handle request
    ChatMessage? assistantMessage = await response
        .GetAssistantChatMessageAsync();

    if (assistantMessage is not null)
    {
        messages.Add(assistantMessage);
        Console.WriteLine();
        Console.WriteLine($"COPILOT ANSWER: {assistantMessage.Content}");
        Console.WriteLine();
        Console.WriteLine();
    }
}

Erstens müssen wir die Benutzer-Secrets einbeziehen, damit wir darauf innerhalb unseres Codes zugreifen können.

Im nächsten Schritt erstellen wir eine leere Liste von Chat-Nachrichten. Wir müssen das Gespräch in dieser Variable speichern und es an den Azure OpenAI-Endpunkt senden, um dem Copilot etwas Kontext zu geben.

Wir verwenden die Methode CreateHttpClient aus der Klasse HttpClientHelper, um die HttpClient-Instanz zu erstellen.

Nun verwenden wir eine Endlosschleife (while true). Innerhalb der Schleife lesen wir eine Zeile von der Konsole, die die Benutzernachricht repräsentiert. Wir fügen diese Nachricht unserer Liste von Nachrichten hinzu.

Als nächstes verwenden wir die Methode CreateRequestBody aus der Klasse RequestHelper, um das RequestBody-Objekt zu erstellen, das alle benötigten Daten enthält.

Nun ist es an der Zeit, die Anfrage an den Azure OpenAI-Dienst zu stellen. Zuerst erhalten wir den entsprechenden Endpunkt. Anschließend verwenden wir die Methode PostAsJsonAsync auf dem HttpClient, um eine POST-Anfrage mit dem RequestBody-Objekt zu senden.

Im letzten Schritt behandeln wir die Antwort. Wir verwenden die Methode GetAssistantChatMessageAsync, um die entsprechende Chat-Nachricht zu erhalten, die wir dann auf der Konsole ausgeben.

Lasst uns den Code in Aktion sehen. Wir starten die Anwendung und können die Nachricht eingeben, zum Beispiel: „Wie kann ich eine benutzerdefinierte Schriftart zu meiner Xamarin.Forms-App hinzufügen?

Der Copilot wird entsprechend antworten und den Verweis auf die entsprechende Datei hinzufügen.

Fazit

In diesem Blog-Beitrag haben wir eine einfache Konsolenanwendung in C# erstellt, um auf den Azure OpenAI-Dienst zuzugreifen und mit eigenen Daten zu arbeiten. Bitte werft einen Blick auf meinen ersten Blog-Beitrag, um die erforderlichen Azure-Ressourcen entsprechend einzurichten.

Den Quellcode findet ihr in diesem GitHub-Repository.

Rückblick: Global Azure Bootcamp 2019 in München ChatGPT: Der ChatBot mit der künstlichen Intelligenz Rückblick: Azure Saturday in München