Function-Calling mit Azure OpenAI (GPT-4)

!sredna hcua nnak hcI

Guten Tag! Ich bin ihr Large-Language-Model und kann jetzt rückwärts buchstabieren.

Dachten Sie, die Überschrift ist kaputt? Beim zweiten Lesen haben Sie aber verstanden, was da los ist, oder?

Das liegt daran, dass Sie eine (Natürliche) Allgemeine Intelligenz sind. Ich, Ihr LLM, bin allerdings noch keine (Künstliche) Allgemeine Intelligenz. Deswegen kann ich nicht so ohne Weiteres rückwärts buchstabieren (Abb. 1). Dabei muss man nämlich algorithmisch vorgehen – also nach einer strengen Rechenvorschrift – und das liegt mir nicht. Ich jongliere lieber mit Wörtern.

Jetzt kam aber mein Entwickler auf die Idee, mir so eine Funktion zum Rückwärtsbuchstabieren zu spendieren. Das ist im Alltag nicht super hilfreich, aber dies soll ja vor allem eine Demo sein. Und das war ruckzuck fertig. Ich bin selbst verblüfft, wie leicht man mich um neue Funktionen erweitern kann. !looc hcilmeiz sad ednif hci dnU

Ich zeige Ihnen jetzt mal, wie Sie das mit dem Azure OpenAI Dienst, C# und .NET MAUI auch hinbekommen. Für die Entwicklung nehmen Sie Visual Studio oder Visual Studio Code.

Als erstes fügen Sie Ihrem .NET-MAUI-Projekt die NuGet-Pakete Azure.AI.OpenAI und Newtonsoft.Json hinzu. Die Datei MainPage.xaml passen Sie so an, dass Sie mit mir kommunizieren können. Die fertige Datei sehen Sie in Listing 1.

Listing 1: MainPage.xaml

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="AzureOpenAIExample3.MainPage"
             Title="Function Calling">
    <Grid Margin="20" RowSpacing="20">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>
        <StackLayout Grid.Row="0" Spacing="20">
            <Editor x:Name="edt1" HeightRequest="140" FontSize="Medium" IsSpellCheckEnabled="False" />
            <FlexLayout Direction="Row" Wrap="Wrap" JustifyContent="Center">
                <Button x:Name="btnSend" Text="Absenden" Clicked="BtnSend_Clicked" Margin="4" />
            </FlexLayout>
        </StackLayout>
        <ScrollView Grid.Row="1" x:Name="scvMain" VerticalOptions="Fill">
            <StackLayout x:Name="stlMain" Spacing="20" />
        </ScrollView>
    </Grid>
</ContentPage>

Spannend wird es in MainPage.xaml.cs (Listing 2). Sie definieren dort die Funktion zum Umdrehen eines Texts (Reverse) und übergeben Sie dem ChatCompletionsOptions-Objekt. Den benötigten Parameter text legen Sie über eine anonyme Klasse fest (functionParams) und wandeln ihn ins JSON-Format um. Dann übergeben Sie noch die Funktionsdefinition als ChatCompletionsFunctionToolDefinition-Objekt an die ChatCompletionsOptions.

Ab diesem Zeitpunkt weiß ich, dass Sie mir eine Reverse-Funktion anbieten. Wenn ich Ihnen eine Antwort schicke, liefere ich Ihnen entweder meine finale Antwort zurück, oder ich fordere die Funktion an (FinishReason ist dann ToolCalls). Sie müssen die Funktion dann aufrufen und mir das Ergebnis zurückschicken, damit ich damit weiterarbeiten kann.

Damit das Ganze klappt, geben Sie der Funktion Reverse – und auch dem Parameter text – eine aussagekräftige Beschreibung (description), denn nur so kann ich erkennen, was die Funktion macht.

Listing 2: MainPage.xaml.cs

using Azure;
using Azure.AI.OpenAI;
using Newtonsoft.Json;

namespace AzureOpenAIExample3;

public partial class MainPage : ContentPage
{
    private static readonly OpenAIClient openAIClient = new(new Uri("(your OpenAI service endpoint)"), new AzureKeyCredential("(your OpenAI service key)"));
    private static readonly ChatCompletionsOptions chatCompletionsOptions = new();

    public MainPage()
    {
        InitializeComponent();

        chatCompletionsOptions.DeploymentName = "(your model deployment name)";
        chatCompletionsOptions.MaxTokens = 500;
        // Assign dummy entry to be replaced with a system message later
        chatCompletionsOptions.Messages.Add(null);

        dynamic functionParams;

        // Anonymous class for defining parameters of function "Reverse"
        functionParams = new
        {
            type = "object",
            properties = new
            {
                text = new
                {
                    type = "string",
                    description = "The text to be reversed"
                },
            },
            required = new[] { "text" }
        };

        AddCustomFunction("Reverse", "Reverses the order of characters in a text", functionParams, chatCompletionsOptions);
    }

    private static ChatCompletionsOptions AddCustomFunction(string name, string description, object functionParams, ChatCompletionsOptions chatCompletionsOptions)
    {
        var customFunction = new FunctionDefinition
        {
            Name = name,
            Description = description,
            Parameters = BinaryData.FromObjectAsJson(functionParams)
        };

        var customFunctionTool = new ChatCompletionsFunctionToolDefinition(customFunction);
        chatCompletionsOptions.Tools.Add(customFunctionTool);

        return chatCompletionsOptions;
    }

    private async void BtnSend_Clicked(object sender, EventArgs e)
    {
        var query = edt1.Text;
        stlMain.Children.Add(new Label { Text = query, HorizontalTextAlignment = TextAlignment.End, TextColor = Color.FromRgb(0, 255, 0), FontSize = 18 });

        var responseText = await Chat(query);
        stlMain.Children.Add(new Label { Text = responseText, HorizontalTextAlignment = TextAlignment.Start, TextColor = Color.FromRgb(51, 153, 255), FontSize = 18 });
    }

    private static async Task<string> Chat(string prompt)
    {
        var systemChatMessage = new ChatRequestSystemMessage("You are a helpful assistant.");
        var userChatMessage = new ChatRequestUserMessage(prompt + $". Write your reply in German language.");

        chatCompletionsOptions.Messages[0] = systemChatMessage;
        chatCompletionsOptions.Messages.Add(userChatMessage);

        string responseText;
        try
        {
            // Get chatbot response
            var completionsResponse = openAIClient.GetChatCompletions(chatCompletionsOptions);

            var completionsMessage = completionsResponse.Value.Choices[0];
            var responseMessage = completionsMessage.Message;

            var assistantChatMessage = new ChatRequestAssistantMessage(responseMessage);
            chatCompletionsOptions.Messages.Add(assistantChatMessage);

            if (completionsMessage.FinishReason == CompletionsFinishReason.ToolCalls)
            {
                var toolCalls = responseMessage.ToolCalls;

                while (toolCalls.Count > 0)
                {
                    string toolResponseText = string.Empty;

                    foreach (ChatCompletionsToolCall toolCall in toolCalls)
                    {
                        if (toolCall is ChatCompletionsFunctionToolCall functionToolCall)
                        {
                            var toolName = functionToolCall.Name;

                            switch (toolName)
                            {
                                case "Reverse":
                                    var text = JsonConvert.DeserializeAnonymousType(functionToolCall.Arguments, new { text = "" })!.text;
                                    toolResponseText = Reverse(text);
                                    break;

                                default:
                                    throw new Exception($"Unknown custom function '{toolName}'.");
                            }

                            var newToolMessage = new ChatRequestToolMessage(toolResponseText, toolCall.Id);
                            chatCompletionsOptions.Messages.Add(newToolMessage);

                            // Get another chatbot response, this time including the results from the tool call(s).
                            completionsResponse = await openAIClient.GetChatCompletionsAsync(chatCompletionsOptions);
                            responseMessage = completionsResponse.Value.Choices[0].Message;
                            toolCalls = responseMessage.ToolCalls;

                            chatCompletionsOptions.Messages.Add(new ChatRequestAssistantMessage(responseMessage));
                        }
                    }
                }
            }

            responseText = responseMessage.Content;
        }
        catch (RequestFailedException e)
        {
            if (e.ErrorCode == "content_filter")
            {
                responseText = "Oops, content was filtered.";
            }
            else
            {
                responseText = "Request failed: " + e.Message;
            }
        }
        catch (HttpRequestException e)
        {
            if (e.StatusCode == System.Net.HttpStatusCode.BadRequest)
            {
                responseText = "Sorry, I don't know how to interpret your request. Please try rephrasing it.";
            }
            else
            {
                responseText = e.Message;
            }
        }
        catch (Exception e)
        {
            responseText = e.Message;
        }

        return responseText;
    }

    private static string Reverse(string text)
    {
        var reversedText = text.Reverse().ToArray();
        return new string(reversedText);
    }
}

Et voilà: Ich buchstabiere rückwärts (Abb. 2), wann immer Sie es brauchen (wohl nie, aber das ignorieren wir hier).

Übrigens: Ich rede die ganze Zeit von „Funktionen“. Damit meine ich nicht unbedingt mathematische Funktionen oder auch nur deterministische Algorithmen. Sie können jede beliebige Prozedur implementieren. Wie wärs zum Beispiel, wenn Sie auf einen Web Service oder eine Suchmaschine zugreifen? Ich kann dann sogar aktuelle Inhalte in meine Antworten einbauen. !thsalfeg githcir nib hcI

Aber es kann passieren, dass ich eine Funktion manchmal nicht aufrufe, obwohl es sinnvoll wäre. Oder ich erkenne die nötigen Parameter in Ihrer Anfrage nicht. Ich bin halt auch nur ein LLM.

Daher noch ein Tipp: Gehen Sie mit solchen Fällen irgendwie sinnvoll um. Geben Sie zum Beispiel eine Nachricht wie in Listing 2 aus („Sorry, I don't know how to interpret your request…“). Damit nicht gleich alles zusammenbricht, wenn ich mich mal irre *grin*😬.

!gnidoc yppah dnu eßürG enöhcS

Jetzt teilen: