Microsoft 365 エージェント SDK の AgentApplication

AgentApplication は Agents SDK で構築されたエージェントの中心的なレポート パーツです。 AgentApplication は、ユーザーからのメッセージ、会話ライフサイクルイベント、アダプティブ カードの対話、OAuth コールバックなど、すべての着信アクティビティのエントリーポイントです。

エージェントとは、本質的に AgentApplication です。 エージェントの動作を定義するハンドラを使って構成を行います。 この SDK は、ルーティング、状態管理、実行に必要なインフラをすべて処理します。

AgentApplication の仕組み

すべてのエージェントにはライフサイクルがあり、それはチャネル (Microsoft Teams、Bot Service、またはカスタム クライアント) がエージェントのエンドポイントに活動を配信した時点で始まります。 AgentApplication はそのライフサイクルの中心に位置しています:

Channel → Hosting layer → AgentApplication → Your handlers

Agents SDK を使用して構築されたエージェントにおける処理の各レイヤーは、次のように機能します。

  1. ホスティング レイヤーは HTTP 要求を受信し、その認証を行います。
  2. AgentApplication は、受信した活動をパイプラインを通じて処理します。
  3. ハンドラーは、ルートの一致に基づいて呼び出されます。

ハンドラーが実行される前に、エージェントがターン状態を読み込みます。 その後、エージェントはターン状態を保存します。

主要な概念

活動

Agents SDK 内のすべての処理は、活動として実行されます。 活動とは、発生した事象を表す構造化されたメッセージです。 活動には、メッセージ、イベント、呼び出し、conversationUpdate などといったタイプがあります。 そのタイプに関連するペイロードを搭載しています。 AgentApplication は活動を受け取り、適切なハンドラーに転送します。

工順

ルートは、セレクターハンドラーを関連付けます。 セレクターは、ルートが現在の活動と一致するかどうかを判断します。 ルートが一致すると、ハンドラーは指定されたロジックを実行します。

エージェントの構成時に、ルートを登録してください。 次のように一致します:

  • 特定のテキストを含む、または正規表現に一致するメッセージ
  • 特定の種類の活動
  • 会話のライフサイクル イベント (メンバーの追加、メンバーの削除)
  • アダプティブ カード アクション
  • カスタム条件

活動が到着すると、システムは一致するものが見つかるまで、順にルートを評価します。 既定では、1 つのルートのみが実行されます。

状態の切り替え

AgentApplication は、スコープごとに分割された構造化されたストレージである _turn state を管理します:

[スコープの種類] 内容
会話 会話内のすべてのユーザー間で共有され、ターン間で保持される
User すべての会話にわたって、個々のユーザーを対象とする
温度 現在のターンのみで、永続化されません

システムはハンドラーの実行前に自動的に状態を読み込み、実行後に自動的に保存します。

コンテキストの切り替え

ハンドラーが実行されると、ターン コンテキストを受け取ります。 ターン コンテキストとは、現在の活動、アダプター接続、応答を送信するためのユーティリティのスナップショットです。 ターンコンテキストは、現在の対話へのインターフェースです。

ミドルウェア

AgentApplicationミドルウェア パイプラインをサポートします。 ミドルウェアとは、ハンドラーの処理の前後で各ターンを処理するコンポーネントの連鎖のことです。 ミドルウェアは、活動フローを検査、変換、またはショートカットできます。 一般的な使用としては、ログ記録、認証チェック、リクエストの正規化などが挙げられます。

エージェントを作成する

AgentApplication をサブクラス化し、コンストラクタ内でハンドラを登録します。 ホスティング フレームワークは自動的に AgentApplicationOptions を挿入します。

public class MyAgent : AgentApplication
{
    public MyAgent(AgentApplicationOptions options) : base(options)
    {
        OnConversationUpdate(ConversationUpdateEvents.MembersAdded, WelcomeAsync);
        OnActivity(ActivityTypes.Message, OnMessageAsync, rank: RouteRank.Last);
    }

    private async Task WelcomeAsync(ITurnContext context, ITurnState state, CancellationToken ct)
    {
        foreach (var member in context.Activity.MembersAdded)
        {
            if (member.Id != context.Activity.Recipient.Id)
            {
                await context.SendActivityAsync("Hello! How can I help you?", cancellationToken: ct);
            }
        }
    }

    private async Task OnMessageAsync(ITurnContext context, ITurnState state, CancellationToken ct)
    {
        await context.SendActivityAsync($"You said: {context.Activity.Text}", cancellationToken: ct);
    }
}

Program.cs にエージェントを登録します:

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);

builder.Services.AddHttpClient();
builder.Services.AddSingleton<IStorage, MemoryStorage>();
builder.Services.AddAgent<MyAgent>();
builder.Services.AddAgentAspNetAuthentication(builder.Configuration);

WebApplication app = builder.Build();

app.UseAuthentication();
app.UseAuthorization();
app.MapAgentApplicationEndpoints(requireAuth: !app.Environment.IsDevelopment());

app.Run();

活動ハンドラーを登録する

メッセージの処理

メッセージを正確なテキストで照合する (大文字小文字を区別しない):

OnMessage("help", async (context, state, ct) =>
{
    await context.SendActivityAsync("Here's what I can do...", cancellationToken: ct);
});

正規表現でメッセージを一致させる:

OnMessage(new Regex(@"^order\s+\d+$", RegexOptions.IgnoreCase), async (context, state, ct) =>
{
    await context.SendActivityAsync("Looking up your order...", cancellationToken: ct);
});

会話の更新を処理する

メンバーの参加や退会など、会話のライフサイクルに関連するイベントのハンドラーを登録します。

OnConversationUpdate(ConversationUpdateEvents.MembersAdded, async (context, state, ct) =>
{
    foreach (var member in context.Activity.MembersAdded)
    {
        if (member.Id != context.Activity.Recipient.Id)
        {
            await context.SendActivityAsync("Welcome!", cancellationToken: ct);
        }
    }
});

OnConversationUpdate(ConversationUpdateEvents.MembersRemoved, async (context, state, ct) =>
{
    // Called when participants leave the conversation
});

あらゆる活動タイプを処理する

活動の種類を表す文字列に一致させることで、ルーティングを完全に制御します。

OnActivity(ActivityTypes.Message, async (context, state, ct) =>
{
    // Handles all message activities
});

OnActivity(ActivityTypes.Event, async (context, state, ct) =>
{
    // Handles event activities
});

ハードコードされた文字列の代わりに、ActivityTypes 定数を使用します。

ルート評価順序を制御する

システムは、ルートを実行時ではなく、登録時に固定された評価順序でソートします。 ソートは 2 つのレベルで行われます:

  1. ルート タイプ: このシステムはルートをタイプごとにグループ分けし、ランクにかかわらず、常に優先度の高いタイプを優先度低いタイプよりも先に評価します:

    優先度 ルーティングの種類
    1 (最高) エージェント型ルーティングの呼び出し
    2 ルートの呼び出し (アダプティブ カードのアクション、OAuth のコールバック、その他の時間依存の呼び出し)
    3 エージェント型ルーティング
    4 (最低) その他のすべてのルート
  2. ランク: 各ルート タイプ グループ内で、システムはルートをランク値で並べ替えます。 数値が小さいほど先に評価されます。

ハンドラーを登録する際は、RouteRank 定数を使用してランクを設定してください:

定数 意味
RouteRank.First 0 グループ内の他のルートより先に評価されます
RouteRank.Unspecified 32767 ランクが指定されていない場合の既定値
RouteRank.Last 65535 グループ内の他のすべてのルートの後に評価されます

既定では、最初の一致するルートが見つかった時点で評価が停止します。 RouteRank.Last を使用すると、より具体的なルートに一致しないすべてのリクエストを処理する、包括的なフォールバックとして機能します。

// Specific handlers use the default rank
OnMessage("status", HandleStatusAsync);
OnMessage("help", HandleHelpAsync);

// Catch-all — handles anything not matched above
OnActivity(ActivityTypes.Message, HandleUnknownMessageAsync, rank: RouteRank.Last);

ターン ライフサイクル フック

ルート マッチングの前後を問わず、毎ターン実行されるロジックを登録します。 これらのフックは、ロギング、横断的関心事、エラー処理に使用されます。

OnBeforeTurn(async (context, state, ct) =>
{
    logger.LogInformation("Turn started: {Type}", context.Activity.Type);
    return true; // Return false to abort the turn
});

OnAfterTurn(async (context, state, ct) =>
{
    logger.LogInformation("Turn completed");
    return true; // Return false to skip state saving
});

OnTurnError(async (context, state, exception, ct) =>
{
    logger.LogError(exception, "Turn error");
    await context.SendActivityAsync("Something went wrong. Please try again.", cancellationToken: ct);
});

OnBeforeTurnfalse を返した場合、そのターンは中止され、ルートは実行されません。 OnAfterTurnfalse を返すと、ターン状態は保存されません。

ターンの状態を利用する

エージェントは、ハンドラーが実行される前にターン状態を自動的に読み込み、実行後にそれを保存します。 ハンドラーに渡されるターン状態オブジェクトを使用すると、さまざまなスコープにアクセスできるため、ターンをまたいで保持されるデータや、現在のターンにのみ存在する一時的なデータを読み書きすることができます。

  • 会話スコープ: 会話の全ターンで共有されるデータ
  • ユーザースコープ: ユーザーごとに保持されるデータ
  • 一時スコープ: 現在のターンのみ存在するデータ
OnActivity(ActivityTypes.Message, async (context, state, ct) =>
{
    // Conversation scope — persisted per conversation
    var count = state.Conversation.GetValue<int>("messageCount", () => 0);
    state.Conversation.SetValue("messageCount", count + 1);

    // User scope — persisted per user
    var name = state.User.GetValue<string>("displayName");

    // Temp scope — current turn only
    state.Temp.SetValue("parsedInput", context.Activity.Text?.Trim());

    await context.SendActivityAsync($"Message #{count + 1}: {context.Activity.Text}", cancellationToken: ct);
});

メモ

ローカルでの開発やテストには MemoryStorage を使用します。 運用環境への展開、特に複数のインスタンスで実行される展開では、Azure Cosmos DB や Azure Blob Storage などの永続ストレージ プロバイダーを使用してください。 エージェントでストレージ プロバイダーを使用するを参照してください。

次の手順