O Azure OpenAI é um serviço da Microsoft que integra modelos avançados de inteligência artificial, como o GPT-4o, em diversas aplicações. Ele oferece funcionalidades como geração de texto, tradução de idiomas, análise de sentimentos e resumo de textos, aprimorando a automação e eficiência de soluções empresariais. Neste tutorial, vamos configurar uma web API em .NET que utiliza o serviço Azure OpenAI com a funcionalidade de function calling.

O function calling da OpenAI é uma funcionalidade que permite a modelos de linguagem executarem funções específicas durante uma conversa. Em vez de apenas gerar texto, o modelo pode reconhecer quando uma ação ou operação é necessária e chamar uma função predefinida para realizar essa tarefa. Isso pode incluir consultar dados, executar cálculos, acessar APIs externas ou manipular informações de forma mais estruturada. A capacidade de chamar funções aumenta a utilidade prática dos modelos de linguagem, permitindo a integração com sistemas complexos e proporcionando respostas mais precisas e contextualizadas.

Vamos ao projeto! Começaremos criando uma pasta raiz para nossa solução.

mkdir AzureOpenAiFunctionCalling

Em seguida, vamos criar o arquivo .gitignore para nossa solução:

dotnet new gitignore

Para nosso projeto, vamos criar uma Web API que será o coração da nossa aplicação:

dotnet new webapi

Agora, vamos instalar o pacote NuGet Azure.AI.OpenAI que nos permitirá integrar o serviço Azure OpenAI:

dotnet add package Azure.AI.OpenAI --prerelease 

Vamos criar um arquivo para configurar as IOptions do Azure OpenAI. Crie um novo arquivo e adicione o seguinte código:

namespace AzureOpenAiFunctionCalling.AzureOpenAi;

public class AzureOpenAiConfig
{
    public string Endpoint { get; set; } = default!;
    public string Key { get; set; } = default!;
    public string Model { get; set; } = default!;
}

Adicione as chaves necessárias no arquivo appsettings.json:

  "AzureOpenAi": {
    "Endpoint": "<your-endpoint>",
    "Key" : "<your-key>",
    "Model" : "<your-model>"
  }

Adicione a configuração no arquivo Program.cs para que nossa aplicação utilize as opções do Azure OpenAI:

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

Agora vamos criar um record para representar os assistentes na pasta Models:

namespace AzureOpenAiFunctionCalling.AzureOpenAi.Models;

public record Assistants(string Id, string Name, string Instructions);

Crie outro record para as requisições de criação de assistentes:

public record CreateAssistantRequest(string Name, string Description);

Defina a interface do serviço que gerenciará os assistentes do Azure Open AI:

public interface IAssistantService
{
    Task<IList<Assistants>> ListAssistantAsync();
    Task<Assistants?> GetAssistantsByIdAsync(string id);
    Task<Assistants> CreateAssistantAsync(CreateAssistantRequest request);
}

Implemente o serviço que gerenciará os assistentes:

public partial class AssistantService(IOptions<AzureOpenAiConfig> config) : IAssistantService
{
    private readonly AzureOpenAiConfig _azureOpenAiConfig = config.Value;
    
    private AzureOpenAIClient GetClient()
    {
        return new AzureOpenAIClient(
            new Uri(_azureOpenAiConfig.Endpoint), 
            new AzureKeyCredential(_azureOpenAiConfig.Key));
    }
}
#pragma warning disable OPENAI001
public partial class AssistantService
{
    public async Task<IList<Assistants>> ListAssistantAsync()
    {
        var client = GetClient();
        var assistantClient = client.GetAssistantClient();

         AsyncPageableCollection<Assistant> assistants = assistantClient.GetAssistantsAsync();

         var result = new List<Assistants>();
         await foreach (Assistant assistant in assistants)
         {
             result.Add(new Assistants(
                 assistant.Id, 
                 assistant.Name,
                 assistant.Instructions));
         }

         return result;
    }
}
#pragma warning restore OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
#pragma warning disable OPENAI001
public partial class AssistantService
{
    public async Task<Assistants?> GetAssistantsByIdAsync(string id)
    {
        try
        {
            var client = GetClient();
            var assistantClient = client.GetAssistantClient();

            var assistant = await assistantClient.GetAssistantAsync(id);

            return new Assistants(
                assistant.Value.Id, 
                assistant.Value.Name,
                assistant.Value.Instructions);
        }
        catch (ClientResultException ex) when (ex.Status == 404)
        {
            return null;
        }
    }
}
#pragma warning restore OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
#pragma warning disable OPENAI001
public partial class AssistantService
{
    public async Task<Assistants> CreateAssistantAsync(CreateAssistantRequest request)
    {
        var client = GetClient();
        var assistantClient = client.GetAssistantClient();
        
        var assistantOptions = new AssistantCreationOptions
        {
            Name = request.Name,
            Instructions = request.Description,
            ToolResources = new()
        };

        Assistant assistant = await assistantClient.CreateAssistantAsync(_azureOpenAiConfig.Model, assistantOptions);

        return new Assistants(
            assistant.Id, 
            assistant.Name,
            assistant.Instructions);
    }
}
#pragma warning restore OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.

Observe que utilizamos partial classes para a implementação acima. Um arquivo para cada método!

Configure a injeção de dependência para o serviço criado anteriormente:

builder.Services.AddScoped<IAssistantService, AssistantService>();

Configure os endpoints para os métodos implementados no Program.cs:

app.MapGet("/assistants", async (IAssistantService service) => 
        Results.Ok(await service.ListAssistantAsync()))
.WithName("ListAssistants")
.WithOpenApi();

app.MapGet("/assistants/{id}", async (string id, IAssistantService service) => 
        Results.Ok(await service.GetAssistantsByIdAsync(id)))
    .WithName("GetAssistantsById")
    .WithOpenApi();

app.MapPost("/assistants", async (CreateAssistantRequest request, IAssistantService service) => 
        Results.Ok(await service.CreateAssistantAsync(request)))
    .WithName("CreateAssistant")
    .WithOpenApi();

Aqui está um exemplo de chamada para o endpoint de criação de assistentes:

Aqui está um exemplo de chamada para o endpoint de listagem de assistentes por ID:

Vamos começar a implementação das threads!

Iniciaremos criando o record ThreadResponse.

namespace AzureOpenAiFunctionCalling.AzureOpenAi.Models;

public record ThreadResponse(string Id);

Adicione o método CreateThreadAsync na interface do serviço:

Task<ThreadResponse> CreateThreadAsync();
#pragma warning disable OPENAI001
public partial class AssistantService
{
    public async Task<ThreadResponse> CreateThreadAsync()
    {
        var client = GetClient();
        var assistantClient = client.GetAssistantClient();
        
        var thread = await assistantClient.CreateThreadAsync();

        return new ThreadResponse(thread.Value.Id);
    }
}
#pragma warning restore OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.

Com o novo método pronto, podemos criar o novo endpoint para criação de threads.

app.MapPost("/threads", async (IAssistantService service) => 
        Results.Ok(await service.CreateThreadAsync()))
    .WithName("CreateThread")
    .WithOpenApi();

Agora vamos definir o métodoRunAsync que irá processar as mensagens:

Task<string> RunAsync(RunRequest request);
#pragma warning disable OPENAI001
public partial class AssistantService
{
    public async Task<string> RunAsync(RunRequest request)
    {
        var client = GetClient();
        var assistantClient = client.GetAssistantClient();

        var thread = (await assistantClient.GetThreadAsync(request.ThreadId)).Value;
        var assistant = (await assistantClient.GetAssistantAsync(request.AssistantId)).Value;
        
        await assistantClient.CreateMessageAsync(thread.Id, [MessageContent.FromText(request.Message)]);
        
        ThreadRun threadRun = await assistantClient.CreateRunAsync(thread.Id, assistant.Id);
        
        do
        {
            await Task.Delay(TimeSpan.FromMilliseconds(100));
            threadRun = await assistantClient.GetRunAsync(thread.Id, threadRun.Id);
        }
        while (threadRun.Status == RunStatus.Queued || threadRun.Status == RunStatus.InProgress);
        
        AsyncPageableCollection<ThreadMessage> messagePage = assistantClient.GetMessagesAsync(thread.Id, ListOrder.NewestFirst);
        await using var enumerator = messagePage.GetAsyncEnumerator();
        var messageItem = await enumerator.MoveNextAsync() ? enumerator.Current : null;

        return messageItem?.Content.FirstOrDefault()?.Text ?? string.Empty;
    }
}
#pragma warning restore OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.

Vamos configurar o endpoint para o método RunAsync:

app.MapPost("/run", async (RunRequest request, IAssistantService azureOpenApiService) => 
        Results.Ok(await azureOpenApiService.RunAsync(request)))
    .WithName("Run")
    .WithOpenApi();

Aqui está um exemplo de chamada para o endpoint Run:

Observe que enviamos o threadId, o assistantId e a mensagem que deverá ser colocada na conversa.

Temos a resposta do endpoint!

Vamos enviar outra mensagem:

Observe que para responder a segunda mensagem, ele utilizou o histórico de mensagens da thread.

Nosso assistant está funcionando perfeitamento. Agora é hora de adicionarmos algumas tools (function calling).

Vamos criar e implementar o serviço ChatCompletions:

public interface IChatCompletionsService
{
    string Complete(string systemMessage, string userMessage);
    string Complete(string userMessage);
}
using Azure;
using Azure.AI.OpenAI;
using Microsoft.Extensions.Options;
using OpenAI.Chat;

namespace AzureOpenAiFunctionCalling.AzureOpenAi.Services.ChatCompletions;

public class ChatCompletionsService(IOptions<AzureOpenAiConfig> config) : IChatCompletionsService
{
    private AzureOpenAIClient GetClient()
    {
        return new AzureOpenAIClient(
            new Uri(config.Value.Endpoint), 
            new AzureKeyCredential(config.Value.Key));
    }
    
    public string Complete(string userMessage, string systemMessage)
    {
        var client = GetClient();
        var chatClient = client.GetChatClient(config.Value.Model);

        var msgs = new List<ChatMessage>();

        if (!string.IsNullOrWhiteSpace(systemMessage))
        {
            msgs.Add(new SystemChatMessage(systemMessage));
        }

        msgs.Add(new UserChatMessage(userMessage));
        
        var completion = chatClient.CompleteChat(msgs.ToArray());

        var result = completion.Value.Content[0].Text;

        return result;
    }

    public string Complete(string userMessage)
    {
        return Complete(userMessage, string.Empty);
    }
}

Iremos, agora, definir a interface ITool. Ela será utilizado por todas as ferramentas que serão utilizadas via function calling.

using OpenAI.Assistants;

namespace AzureOpenAiFunctionCalling.AzureOpenAi.Tools;

public interface ITool
{
    public static abstract string Name { get; }

    public static abstract FunctionToolDefinition Definition { get; }
}

Vamos implementar a primeira tool, que será especializada em escrever artigos sobre um tema qualquer (parâmetro) em um determinado número de linhas (outo parâmetro):

using System.Text.Json;
using AzureOpenAiFunctionCalling.AzureOpenAi.Services.ChatCompletions;
using OpenAI.Assistants;

namespace AzureOpenAiFunctionCalling.AzureOpenAi.Tools;

public class BlogArticleWriter : ITool
{
    private const string MessageTemplate = """
          Create a article about '{generalIdea}' in {numberOfWords} words.
         Use the '{title}' as title.
         Outputs only the article 

         -----

         Use the following schema:

         <title>
         
         <content>
         """;
    
    public static string Name => nameof(BlogArticleWriter);

    public static FunctionToolDefinition Definition => new(
        name: Name,
        description: "Write article for a personal tech blog",
        parameters: BinaryData.FromObjectAsJson(
            new
            {
                Type = "object",
                Properties = new
                {
                    Title = new
                    {
                        Type = "string",
                        Description = "The title of the article",
                    },
                    GeneralIdea = new
                    {
                        Type = "string",
                        Description = "The idea of the article",
                    },
                    NumberOfLines = new
                    {
                        Type = "integer",
                        Description = "The number of lines",
                    },
                },
                Required = new[] { "Title", "GeneralIdea", "NumberOfLines" },
            },
            new JsonSerializerOptions() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }));

    public string Execute(
        ILogger logger, 
        IChatCompletionsService chatCompletionsService, 
        string title, 
        string generalIdea, 
        int numberOfLines)
    {
        var formattedMsg = MessageTemplate
            .Replace("{generalIdea}", generalIdea)
            .Replace("{title}", title)
            .Replace("{numberOfLines}", numberOfLines.ToString());
        
        var result = chatCompletionsService.Complete(formattedMsg);

        logger.LogInformation($"Article with title '{title}' and general idea '{generalIdea}' was created");

        return result;
    }
}

E a tool que enviar emails:

using System.Text.Json;
using OpenAI.Assistants;

namespace AzureOpenAiFunctionCalling.AzureOpenAi.Tools;

public class EmailTool : ITool
{
    public static string Name => nameof(EmailTool);

    public static FunctionToolDefinition Definition => new(
        name: Name,
        description: "Send emails",
        parameters: BinaryData.FromObjectAsJson(
            new
            {
                Type = "object",
                Properties = new
                {
                    ReceiverEmail = new
                    {
                        Type = "string",
                        Description = "E-mail of the person who will receive the email",
                    },
                    Subject = new
                    {
                        Type = "string",
                        Description = "E-mail subject",
                    },
                    Message = new
                    {
                        Type = "string",
                        Description = "Email message",
                    },
                },
                Required = new[] { "ReceiverEmail", "Subject", "Message" },
            },
            new JsonSerializerOptions() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }));

    public string Execute(ILogger logger, string receiverEmail, string subject, string message)
    {
        logger.LogInformation($"Email sent to '{receiverEmail}' with subject '{subject}' and message '{message}'");

        return "Email sent successfully";
    }
}

Precisamos atualizar a criação do assistente para incluir as tools. Dessa forma, os assistentes criados irão saber que ele pode utilizá-las quando preciso.

  var assistantOptions = new AssistantCreationOptions
        {
            Name = request.Name,
            Instructions = request.Description,
            ToolResources = new(),
            Tools =
            {
                EmailTool.Definition,
                BlogArticleWriter.Definition
            },
        };

Vamos implementar o serviço que chamará as tools definidas. Ele será responsável por decidir qual tool será chamada via function calling.

public interface IToolHandler
{
    ToolOutput Handle(RequiredAction requiredAction);
}
using System.Text.Json;
using AzureOpenAiFunctionCalling.AzureOpenAi.Services.ChatCompletions;
using AzureOpenAiFunctionCalling.AzureOpenAi.Tools;
using OpenAI.Assistants;

namespace AzureOpenAiFunctionCalling.AzureOpenAi.Services.ToolHandler;

public class ToolHandler(ILogger<ToolHandler> logger, IChatCompletionsService chatCompletionsService) : IToolHandler
{
    public ToolOutput Handle(RequiredAction requiredAction)
    {
        using JsonDocument argumentsJson = JsonDocument.Parse(requiredAction.FunctionArguments);

        if (requiredAction.FunctionName == EmailTool.Name)
        {
            return HandlerEmailTool(requiredAction, argumentsJson);
        }
        
        if (requiredAction.FunctionName == BlogArticleWriter.Name)
        {
            return HandlerBlogArticleWriterTool(requiredAction, argumentsJson);
        }
        
        return null!;
    }

    private ToolOutput HandlerBlogArticleWriterTool(RequiredAction requiredAction, JsonDocument argumentsJson)
    {
        string title = argumentsJson.RootElement.GetProperty("title").GetString()!;
        string generalIdea = argumentsJson.RootElement.GetProperty("generalIdea").GetString()!;
        int numberOfLines = argumentsJson.RootElement.GetProperty("numberOfLines").GetInt32()!;

        var result = new BlogArticleWriter().Execute(
            logger, 
            chatCompletionsService, 
            title, 
            generalIdea, 
            numberOfLines);
        
        return new ToolOutput(requiredAction.ToolCallId, result);
    }

    private ToolOutput HandlerEmailTool(RequiredAction requiredAction, JsonDocument argumentsJson)
    {
        string receiverEmail = argumentsJson.RootElement.GetProperty("receiverEmail").GetString()!;
        string subject = argumentsJson.RootElement.GetProperty("subject").GetString()!;
        string message = argumentsJson.RootElement.GetProperty("message").GetString()!;

        var result = new EmailTool().Execute(
            logger, 
            receiverEmail, 
            subject,
            message);
        
        return new ToolOutput(requiredAction.ToolCallId, result);
    }
}

Precisamos atualizar o loop para fazer as chamadas das tools (quando necessário):

 do
        {
            await Task.Delay(TimeSpan.FromMilliseconds(100));
            threadRun = await assistantClient.GetRunAsync(thread.Id, threadRun.Id);
            
            if (threadRun.Status == RunStatus.RequiresAction)
            {
                List<ToolOutput> toolOutputs = new();
                foreach (var requiredAction in threadRun.RequiredActions)
                {
                    var result = toolHandler.Handle(requiredAction);
                    
                    toolOutputs.Add(result);
                }
                
                threadRun = await assistantClient.SubmitToolOutputsToRunAsync(threadRun, toolOutputs);
            }
        }
        while (threadRun.Status == RunStatus.Queued || threadRun.Status == RunStatus.InProgress);

Com isso pronto, podemos criar um novo prompt. Nesse exemplo, irei pedir para o assistente criar um artigo sobre C# e depois enviar para o meu e-mail. Dessa forma, o assistente deverá utilizar as duas tools criadas anteriormente.

"Create an article about C# (basic introduction to the language) in 100 words. After that, email talles.dsv@gmail.com with the article as message"

Temos o resultado:

Vendo os logs, podemos ver que as duas tools foram utilizadas sequencialmente!

E é isso pessoal! Teremos muito mais para falar sobre essa ferramenta incrível nos posts futuros.

Você já pode baixar o projeto e não esquece de me seguir no LinkedIn!

Até a próxima, abraços!

GitHub - TallesValiatti/AzureOpenAiFunctionCalling
Contribute to TallesValiatti/AzureOpenAiFunctionCalling development by creating an account on GitHub.
💡
Podemos te ajudar com uma revisão 100% gratuita do seu ambiente cloud.
Share this post