Microsoft 365 エージェント SDKでの AgentApplication

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

エージェントの中核となるのは、 AgentApplicationです。 エージェントの動作を記述するハンドラーを使用して構成します。 SDK は、ルーティング、状態管理、および実行に必要なインフラストラクチャを処理します。

AgentApplication のしくみ

すべてのエージェントには、チャネル (Microsoft Teams、ボット サービス、またはカスタム クライアント) がエージェントのエンドポイントにアクティビティを配信したときに開始されるライフサイクルがあります。 AgentApplication は、そのライフサイクルの中心に配置されます。

Channel → Hosting layer → AgentApplication → Your handlers

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

  1. ホスティング 層は HTTP 要求を受け取り、それを認証します。
  2. AgentApplicationは、パイプラインを介して受信アクティビティを処理します。
  3. ハンドラーは、一致するルートに基づいて呼び出されます。

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

主要な概念

活動

Agents SDK 内のすべてのものが アクティビティとして流れます。 アクティビティは、発生したことを表す構造化されたメッセージです。 アクティビティには、メッセージ、イベント、呼び出し、conversationUpdate などの型があります。 その型に該当するペイロードが搭載されます。 AgentApplication はアクティビティを受信し、適切なハンドラーにルーティングします。

Routes

ルートは セレクター とハンドラーをペアリング します。 セレクターは、ルートが現在のアクティビティと一致するかどうかを決定します。 ハンドラーは、ルートが一致したときにロジックを実行します。

エージェントを構成するときにルートを登録します。 次の内容と一致する可能性があります。

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

アクティビティが到着すると、システムは一致が見つかるまで順番にルートを評価します。 既定では、実行されるルートは 1 つだけです。

ターン状態

AgentApplication は、_turn状態を管理します。これは、スコープにパーティション分割された構造化ストレージです。

スコープの種類 Description
会話 会話内のすべてのユーザー間で共有され、ターン間で保持されます
ユーザー すべての会話にわたって個々のユーザーにスコープを設定する
温度 現在のターンのみ - 保持されない

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

ターン コンテキスト

ハンドラーを実行すると、 ターン コンテキストを受け取ります。 ターン コンテキストは、現在のアクティビティ、アダプター接続、および応答を送信するためのユーティリティのスナップショットです。 ターン コンテキストは、現在の相互作用に対するインターフェイスです。

Middleware

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);
});

Note

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

次のステップ