ROMANCE DAWN for the new world

Microsoft Azure を中心とした技術情報を書いています。

Azure Functions の Azure OpenAI Extension を使ってチャットアシスタントにカスタムスキルを追加する

Azure Functions では、Azure OpenAI 向けの拡張機能(Preview)が提供されています。
learn.microsoft.com

今回は、拡張機能の Assistant trigger を使って、Chat completion binding で構築したチャットアシスタントにカスタムスキルを追加してみました。
その他の拡張機能については、別記事を参照してください。
gooner.hateblo.jp
gooner.hateblo.jp
gooner.hateblo.jp

Assistant trigger とは

Function に Assistant trigger を定義することで、チャットアシスタントにカスタムスキルを提供できます。OpenAI の Function calling が内部的に使われており、呼び出す Function とそのタイミングを判定してくれます。
learn.microsoft.com

前準備

Azure Functions を Isolated worker model で作成し、NGet ライブラリをインストールします。インストールする前に、Visual Studio などのテンプレートで作成されたプロジェクトのライブラリ群を最新版に更新しておくことをお勧めします。

$ dotnet add package Microsoft.Azure.Functions.Worker.Extensions.OpenAI --version 0.16.0-alpha

local.settings.json において、Azure OpenAI Service の エンドポイントとキーを定義しておきます。

{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "UseDevelopmentStorage=true",
    "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
    "AZURE_OPENAI_ENDPOINT": "https://xxx.openai.azure.com/",
    "AZURE_OPENAI_KEY": "xxx",
    "CHAT_MODEL_DEPLOYMENT_NAME": "gpt-4o"
  }

カスタムスキルを追加する

AssistantSkillTrigger のバインディングを使って、チャットアシスタントによってトリガーできる2つの Function を作成します。

  • Todo リストにタスクを追加する
  • Todo リストからタスクを取得する

関数と引数の名前、AssistantSkillTrigger で定義する説明は、Function calling が呼び出す Function とそのタイミングを判断するための重要な情報となります。

public class AssistantSkills
{
    private readonly ITodoManager _todoManager;
    private readonly ILogger<AssistantSkills> _logger;

    public AssistantSkills(ITodoManager todoManager, ILogger<AssistantSkills> logger)
    {
        _todoManager = todoManager;
        _logger = logger;
    }

    [Function(nameof(AddTodo))]
    public Task AddTodo([AssistantSkillTrigger("Create a new todo task")] string taskDescription)
    {
        if (string.IsNullOrEmpty(taskDescription))
        {
            throw new ArgumentException("Task description cannot be empty");
        }

        _logger.LogInformation("Adding todo: {task}", taskDescription);

        string todoId = Guid.NewGuid().ToString()[..6];
        return _todoManager.AddTodoAsync(new TodoItem(todoId, taskDescription));
    }

    [Function(nameof(GetTodos))]
    public Task<IReadOnlyList<TodoItem>> GetTodos(
        [AssistantSkillTrigger("Fetch the list of previously created todo tasks")] object inputIgnored)
    {
        _logger.LogInformation("Fetching list of todos");

        return _todoManager.GetTodosAsync();
    }
}

Todo リストに追加するタスクの永続化は、データベースを使わずに、オンメモリで保持するようにしました。

public record TodoItem(string Id, string Task);

public interface ITodoManager
{
    Task AddTodoAsync(TodoItem todo);

    Task<IReadOnlyList<TodoItem>> GetTodosAsync();
}

class InMemoryTodoManager : ITodoManager
{
    readonly List<TodoItem> todos = new();

    public Task AddTodoAsync(TodoItem todo)
    {
        this.todos.Add(todo);
        return Task.CompletedTask;
    }

    public Task<IReadOnlyList<TodoItem>> GetTodosAsync()
    {
        return Task.FromResult<IReadOnlyList<TodoItem>>(this.todos.ToImmutableList());
    }
}

最後に、Program.cs で TodoManager のインスタンスを追加しておきます。

var host = new HostBuilder()
    .ConfigureFunctionsWebApplication()
    .ConfigureServices(services =>
    {
        services.AddApplicationInsightsTelemetryWorkerService();
        services.ConfigureFunctionsApplicationInsights();

        services.AddSingleton<ITodoManager, InMemoryTodoManager>();
    })
    .Build();

host.Run();

動作確認

VS Code 拡張機能の REST Client を使って、チャットアシスタントと会話してみます。

新しいアシスタントを作成する

@function_app_HostAddress = http://localhost:7074

### Assistant create output binding
PUT {{function_app_HostAddress}}/api/chat/1 HTTP/1.1
content-type: application/json
HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8
Date: Sat, 07 Sep 2024 14:06:32 GMT
Server: Kestrel
Transfer-Encoding: chunked

{
  "assistantId": "1"
}

アシスタントにタスクの追加を依頼する

### Assistant trigger
POST {{function_app_HostAddress}}/api/chat/1 HTTP/1.1
content-type: application/json

{
  "userMessage": "出張の新幹線を予約するのを忘れないように"
}
HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
Date: Sat, 07 Sep 2024 14:12:52 GMT
Server: Kestrel
Transfer-Encoding: chunked

「出張の新幹線を予約するのを忘れないように」というタスクを追加しました。

さらに、もう1つタスクを追加してみます。

### Assistant trigger
POST {{function_app_HostAddress}}/api/chat/1 HTTP/1.1
content-type: application/json

{
  "userMessage": "それと、ホテルの予約も"
}
HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
Date: Sat, 07 Sep 2024 14:13:46 GMT
Server: Kestrel
Transfer-Encoding: chunked

「ホテルの予約をする」というタスクも追加しました。

アシスタントに今日のタスクを確認する

### Assistant trigger
POST {{function_app_HostAddress}}/api/chat/1 HTTP/1.1
content-type: application/json

{
  "userMessage": "今日は何をすればいい?"
}
HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
Date: Sat, 07 Sep 2024 14:14:27 GMT
Server: Kestrel
Transfer-Encoding: chunked

今日のタスクは以下の通りです:

1. 出張の新幹線を予約するのを忘れないように
2. ホテルの予約をする

頑張ってください!

チャットアシスタントから今日のタスクが回答されたことが分かります。


まとめ

Azure Functions 拡張機能の Assistant trigger を使って、Chat completion binding で構築したチャットアシスタントにカスタムスキルを追加してみました。
AOAI の SDK を使った定型のコードが不要となり、Azure Functions のプログラミングモデルで Function calling を実装できて便利だと感じました。

今回のソースコードは、こちらのリポジトリで公開しています。
github.com