ROMANCE DAWN for the new world

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

Japan Azure User Group 14周年イベントで Azure App Service の Sidecar の話をしてきました

Japan Azure User Group 14周年イベントに参加して、Azure App Service on Linux の Sidecar に Phi-3 を配置するアーキテクチャの話をしてきました。

jazug.connpass.com

App Service の Sidecar に SLM をセルフホストするアーキテクチャを中心に、Sidecar の使いどころの考察も含めて話しました。

speakerdeck.com

まだ Preview なので、実運用には足りない機能や制約もありますが、これからのインテリジェントアプリケーション開発において注目したいアーキテクチャです。

X(Twitter)での反応は比較的好評で楽しんでいただけたようですし、数名の方から直接フィードバックをいただくこともできたので、話して良かったなと思いました。
フィードバックをいただいた内容のうち、2点ほど補足しておきます。

どのくらいの App Service Plan が必要になるのか?

Sidecar で SLM を動かすためには、相応の SKU の App Service Plan が必要です。デモしたアプリは Premium v3 P3V3(CPU 8、Memory 32GB)を選択していました。
とくに初回起動時は、Phi-3 イメージのプルと展開で負荷がかかるので、デフォルトの SKU の Premium v3 P0V3(CPU 1、Memory 4GB)ではアプリを起動できませんでした。
セッションでは AOAI と比較したコスト削減のメリットを挙げていましたが、App Service Plan にそれなりのコストがかかることは知っておいてください。

Sidecar がリソースを消費しすぎてメインアプリに影響を与えそう

Sidecar の SLM が多くのリソース(CPU と Memory)を消費してしまうことで、メインのアプリケーションに影響を与えてしまうリスクがあります。
Kubernetes には Pod 内のコンテナーに対して Resource limits を設定できるので、このような機能が App Service の Sidecar にも必要だと思います。
kubernetes.io

公式フォーラムにフィードバックしておきました。
https://feedback.azure.com/d365community/idea/83acbe10-6584-ef11-9442-6045bd8115dc

今回のイベントでは、JAZUG に初参加の方が多くいました。お仕事でも趣味でも Azure が好きな人たちが集まって楽しめるコミュニティになるように、15周年に向けて盛り上げていきたいです。

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

Azure Functions の Azure OpenAI Extension を使ってチャットアシスタントを構築する

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

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

Chat completion binding とは

チャットアシスタント向けの3種類の binding が提供されています。

Assistant create output binding

Assistant create output binding を使うことで、指定したシステムプロンプトによる新しいアシスタントを作成できます。
learn.microsoft.com

Assistant post input binding

Assistant post input binding を使うことで、アシスタントにメッセージを送信するとともに、Table Storage にメッセージ内容を永続化します。
learn.microsoft.com

Assistant query input binding

Assistant query input binding を使うことで、アシスタントが永続化したチャット履歴を取得できます。
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"
  }

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

AssistantCreateOutput のバインディングを使うことで、指定した ID とシステムプロンプトを持つチャットアシスタントを作成できます。作成した結果は、AssistantCreateRequest で受け取ることができます。

public class Chat
{
    private readonly ILogger<Chat> _logger;

    public Chat(ILogger<Chat> logger)
    {
        _logger = logger;
    }

    [Function(nameof(CreateAssistant))]
    public CreateAssistantOutput CreateAssistant(
        [HttpTrigger(AuthorizationLevel.Function, "put", Route = "chat/{assistantId}")] HttpRequest req,
        string assistantId)
    {
        _logger.LogInformation("Assistant create output binding function processed a request.");

        var responseJson = new { assistantId };
        var instructions = """
            You are an AI assistant that helps people find information.
            """;

        return new CreateAssistantOutput
        {
            HttpResponse = new OkObjectResult(responseJson) { StatusCode = 201 },
            AssistantCreateRequest = new AssistantCreateRequest(assistantId, instructions)
        };
    }

    public class CreateAssistantOutput
    {
        [AssistantCreateOutput()]
        public AssistantCreateRequest? AssistantCreateRequest { get; set; }

        [HttpResult]
        public IActionResult? HttpResponse { get; set; }
    }
}

アシスタントにメッセージを送信する

AssistantPostInput のバインディングを使うことで、チャットアシスタントにメッセージ(プロンプト)を送信できます。送信した結果は、AssistantState で受け取ることができます。

[Function(nameof(PostUserMessage))]
public IActionResult PostUserMessage(
    [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "chat/{assistantId}")] HttpRequestData req,
    string assistantId,
    [AssistantPostInput("{assistantId}", "{userMessage}", Model = "%CHAT_MODEL_DEPLOYMENT_NAME%")] AssistantState state)
{
    _logger.LogInformation("Assistant post input binding function processed a request.");

    return new OkObjectResult(state.RecentMessages.LastOrDefault()?.Content ?? "No response returned.");
}

チャット履歴を取得する

AssistantQueryInput のバインディングを使うことで、チャットアシスタントが永続化したチャット履歴を取得するクエリを送信できます。クエリ結果は、AssistantState で受け取ることができます。

[Function(nameof(GetChatState))]
public IActionResult GetChatState(
    [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "chat/{assistantId}")] HttpRequestData req,
    string assistantId,
    [AssistantQueryInput("{assistantId}", TimestampUtc = "{timestampUtc}")] AssistantState state)
{
    _logger.LogInformation("Assistant query input binding function processed a request.");

    return new OkObjectResult(state);
}

動作確認

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 05:33:20 GMT
Server: Kestrel
Transfer-Encoding: chunked

{
  "assistantId": "1"
}

アシスタントにメッセージを送信する

### Assistant post input binding
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 05:35:26 GMT
Server: Kestrel
Transfer-Encoding: chunked

京都には多くの美しい観光地があります。いくつかのお勧めを紹介します。

1. **清水寺(きよみずでら)**:
   - 世界遺産にも登録されている清水寺は、特に「清水の舞台」からの眺めが見どころです。春の桜や秋の紅葉の季節がお勧めです。

2. **金閣寺(きんかくじ)**:
   - その美しい金色の外観で有名な金閣寺。正式名は鹿苑寺(ろくおんじ)で、日本の歴史や文化を感じることができます。

3. **銀閣寺(ぎんかくじ)**:
   - 正式には慈照寺(じしょうじ)。庭園と一緒に楽しむ禅の雰囲気が魅力です。

4. **伏見稲荷大社(ふしみいなりたいしゃ)**:
   - 多数の鳥居が連なり、その美しい光景で知られる伏見稲荷。山全体に広がる参道を散策すると、日本の伝統文化に触れることができます。

5. **嵐山(あらしやま)**:
   - 自然豊かな嵐山エリアは、竹林の道や渡月橋(とげつきょう)など、美しい風景が楽しめます。また、トロッコ列車や保津川下りも人気です。

6. **二条城(にじょうじょう)**:
   - 世界遺産に登録されている歴史的な城で、美しい庭園や御殿内に描かれた障壁画が見所です。

7. **祇園(ぎおん)**:
   - 京都の伝統的な街並みを感じることができるエリア。舞妓さんや茶屋など、日本の古き良き風情が楽しめます。

8. **天龍寺(てんりゅうじ)**:
   - 世界遺産である天龍寺は、美しい庭園が特徴です。特に曹源池庭園(そうげんちていえん)は見逃せません。

これら以外にも、京都には多数の素晴らしい観光地やお寺があります。訪れる時期や興味に合わせて計画を立ててくださいね。

チャットアシスタントから京都のお勧めの観光地が回答されたことが分かります。


チャット履歴を取得する

### Assistant query input binding
GET {{function_app_HostAddress}}/api/chat/1?TimestampUtc=2024-09-06T00:00:00
Accept: application/json
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Sat, 07 Sep 2024 05:40:48 GMT
Server: Kestrel
Transfer-Encoding: chunked

{
  "id": "1",
  "exists": true,
  "createdAt": "2024-09-07T05:33:21.9599132Z",
  "lastUpdatedAt": "2024-09-07T05:35:27.1306636Z",
  "totalMessages": 3,
  "totalTokens": 594,
  "recentMessages": [
    {
      "content": "You are an AI assistant that helps people find information.",
      "role": "system",
      "name": null
    },
    {
      "content": "京都のお勧めの観光地はどこですか?",
      "role": "user",
      "name": null
    },
    {
      "content": "京都には多くの美しい観光地があります。いくつかのお勧めを紹介します。\n\n1. **清水寺(きよみずでら)**:\n   - 世界遺産にも登録されている清水寺は、特に「清水の舞台」からの眺めが見どころです。春の桜や秋の紅葉の季節がお勧めです。\n\n2. **金閣寺(きんかくじ)**:\n   - その美しい金色の外観で有名な金閣寺。正式名は鹿苑寺(ろくおんじ)で、日本の歴史や文化を感じることができます。\n\n3. **銀閣寺(ぎんかくじ)**:\n   - 正式には慈照寺(じしょうじ)。庭園と一緒に楽しむ禅の雰囲気が魅力です。\n\n4. **伏見稲荷大社(ふしみいなりたいしゃ)**:\n   - 多数の鳥居が連なり、その美しい光景で知られる伏見稲荷。山全体に広がる参道を散策すると、日本の伝統文化に触れることができます。\n\n5. **嵐山(あらしやま)**:\n   - 自然豊かな嵐山エリアは、竹林の道や渡月橋(とげつきょう)など、美しい風景が楽しめます。また、トロッコ列車や保津川下りも人気です。\n\n6. **二条城(にじょうじょう)**:\n   - 世界遺産に登録されている歴史的な城で、美しい庭園や御殿内に描かれた障壁画が見所です。\n\n7. **祇園(ぎおん)**:\n   - 京都の伝統的な街並みを感じることができるエリア。舞妓さんや茶屋など、日本の古き良き風情が楽しめます。\n\n8. **天龍寺(てんりゅうじ)**:\n   - 世界遺産である天龍寺は、美しい庭園が特徴です。特に曹源池庭園(そうげんちていえん)は見逃せません。\n\nこれら以外にも、京都には多数の素晴らしい観光地やお寺があります。訪れる時期や興味に合わせて計画を立ててくださいね。",
      "role": "assistant",
      "name": null
    }
  ]
}

Table Storage の OpenAIChatState テーブルを確認すると、チャット履歴が永続化されています。


まとめ

Azure Functions 拡張機能の Chat completion binding を使って、チャットアシスタントを構築してみました。
AOAI の SDK を使った定型のコードが不要となり、シンプルな実装で Chat Completions API を呼び出すことができ、さらにチャット履歴も永続化できて便利だと感じました。

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