次の方法で共有


エージェントへのメモリの追加

このチュートリアルでは、 AIContextProvider を実装してエージェントにアタッチすることで、エージェントにメモリを追加する方法について説明します。

Important

すべてのエージェントの種類で AIContextProviderがサポートされているわけではありません。 この手順では、ChatClientAgentをサポートするAIContextProviderを使用します。

[前提条件]

前提条件と NuGet パッケージのインストールについては、このチュートリアルの 「エージェントの作成と実行 」の手順を参照してください。

AIContextProvider を作成する

AIContextProviderは継承可能な抽象クラスであり、AgentThreadChatClientAgentに関連付けることができます。 これにより、次のことができます。

  1. エージェントが基になる推論サービスを呼び出す前と後にカスタム ロジックを実行します。
  2. 基になる推論サービスを呼び出す前に、エージェントに追加のコンテキストを指定します。
  3. エージェントに提供され、エージェントによって生成されたすべてのメッセージを検査します。

呼び出し前イベントと呼び出し後イベント

AIContextProvider クラスには、エージェントが基になる推論サービスを呼び出す前と後にカスタム ロジックを実行するためにオーバーライドできる 2 つのメソッドがあります。

  • InvokingAsync - エージェントが基になる推論サービスを呼び出す前に呼び出されます。 AIContext オブジェクトを返すことで、エージェントに追加のコンテキストを提供できます。 このコンテキストは、基になるサービスを呼び出す前に、エージェントの既存のコンテキストとマージされます。 要求に追加する指示、ツール、メッセージを提供できます。
  • InvokedAsync - 基になる推論サービスからエージェントが応答を受信した後に呼び出されます。 要求メッセージと応答メッセージを検査し、コンテキスト プロバイダーの状態を更新できます。

シリアル化

AIContextProvider インスタンスは、スレッドの作成時、およびシリアル化された状態からスレッドが再開されるときに、作成され、 AgentThread にアタッチされます。

AIContextProvider インスタンスには、エージェントの呼び出し間で永続化する必要がある独自の状態がある場合があります。 たとえば、ユーザーに関する情報を記憶するメモリ コンポーネントは、その状態の一部としてメモリを持っている可能性があります。

スレッドの永続化を許可するには、SerializeAsync クラスのAIContextProvider メソッドを実装する必要があります。 また、 JsonElement パラメーターを受け取るコンストラクターも指定する必要があります。これは、スレッドを再開するときに状態を逆シリアル化するために使用できます。

AIContextProvider 実装のサンプル

カスタム メモリ コンポーネントの次の例では、ユーザーの名前と経過時間を記憶し、各呼び出しの前にエージェントに提供します。

まず、メモリを保持するモデル クラスを作成します。

internal sealed class UserInfo
{
    public string? UserName { get; set; }
    public int? UserAge { get; set; }
}

その後、 AIContextProvider を実装してメモリを管理できます。 以下の UserInfoMemory クラスには、次の動作が含まれています。

  1. IChatClientを使用して、各実行の最後に新しいメッセージがスレッドに追加されたときに、ユーザー メッセージ内のユーザーの名前と経過時間を検索します。
  2. 各呼び出しの前に、エージェントに現在のメモリを提供します。
  3. 使用可能なメモリがない場合は、不足している情報をユーザーに要求し、情報が提供されるまで質問に答えないようにエージェントに指示します。
  4. また、スレッド状態の一部としてメモリを永続化できるようにシリアル化も実装します。
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;

internal sealed class UserInfoMemory : AIContextProvider
{
    private readonly IChatClient _chatClient;
    public UserInfoMemory(IChatClient chatClient, UserInfo? userInfo = null)
    {
        this._chatClient = chatClient;
        this.UserInfo = userInfo ?? new UserInfo();
    }

    public UserInfoMemory(IChatClient chatClient, JsonElement serializedState, JsonSerializerOptions? jsonSerializerOptions = null)
    {
        this._chatClient = chatClient;
        this.UserInfo = serializedState.ValueKind == JsonValueKind.Object ?
            serializedState.Deserialize<UserInfo>(jsonSerializerOptions)! :
            new UserInfo();
    }

    public UserInfo UserInfo { get; set; }

    public override async ValueTask InvokedAsync(
        InvokedContext context,
        CancellationToken cancellationToken = default)
    {
        if ((this.UserInfo.UserName is null || this.UserInfo.UserAge is null) && context.RequestMessages.Any(x => x.Role == ChatRole.User))
        {
            var result = await this._chatClient.GetResponseAsync<UserInfo>(
                context.RequestMessages,
                new ChatOptions()
                {
                    Instructions = "Extract the user's name and age from the message if present. If not present return nulls."
                },
                cancellationToken: cancellationToken);
            this.UserInfo.UserName ??= result.Result.UserName;
            this.UserInfo.UserAge ??= result.Result.UserAge;
        }
    }

    public override ValueTask<AIContext> InvokingAsync(
        InvokingContext context,
        CancellationToken cancellationToken = default)
    {
        StringBuilder instructions = new();
        instructions
            .AppendLine(
                this.UserInfo.UserName is null ?
                    "Ask the user for their name and politely decline to answer any questions until they provide it." :
                    $"The user's name is {this.UserInfo.UserName}.")
            .AppendLine(
                this.UserInfo.UserAge is null ?
                    "Ask the user for their age and politely decline to answer any questions until they provide it." :
                    $"The user's age is {this.UserInfo.UserAge}.");
        return new ValueTask<AIContext>(new AIContext
        {
            Instructions = instructions.ToString()
        });
    }

    public override JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptions = null)
    {
        return JsonSerializer.SerializeToElement(this.UserInfo, jsonSerializerOptions);
    }
}

エージェントでの AIContextProvider の使用

カスタム AIContextProviderを使用するには、エージェントの作成時に AIContextProviderFactory を指定する必要があります。 このファクトリを使用すると、エージェントはスレッドごとに目的の AIContextProvider の新しいインスタンスを作成できます。

ChatClientAgentを作成するときに、他のすべてのエージェント オプションに加えてChatClientAgentOptionsを提供できるAIContextProviderFactory オブジェクトを提供できます。

using System;
using Azure.AI.OpenAI;
using Azure.Identity;
using OpenAI.Chat;
using OpenAI;

ChatClient chatClient = new AzureOpenAIClient(
    new Uri("https://<myresource>.openai.azure.com"),
    new AzureCliCredential())
    .GetChatClient("gpt-4o-mini");

AIAgent agent = chatClient.CreateAIAgent(new ChatClientAgentOptions()
{
    ChatOptions = new() { Instructions = "You are a friendly assistant. Always address the user by their name." },
    AIContextProviderFactory = ctx => new UserInfoMemory(
        chatClient.AsIChatClient(),
        ctx.SerializedState,
        ctx.JsonSerializerOptions)
});

新しいスレッドを作成すると、 AIContextProviderGetNewThread によって作成され、スレッドにアタッチされます。 メモリが抽出されると、スレッドの GetService メソッドを介してメモリ コンポーネントにアクセスし、メモリを検査することができます。

// Create a new thread for the conversation.
AgentThread thread = agent.GetNewThread();

Console.WriteLine(await agent.RunAsync("Hello, what is the square root of 9?", thread));
Console.WriteLine(await agent.RunAsync("My name is Ruaidhrí", thread));
Console.WriteLine(await agent.RunAsync("I am 20 years old", thread));

// Access the memory component via the thread's GetService method.
var userInfo = thread.GetService<UserInfoMemory>()?.UserInfo;
Console.WriteLine($"MEMORY - User Name: {userInfo?.UserName}");
Console.WriteLine($"MEMORY - User Age: {userInfo?.UserAge}");

このチュートリアルでは、 ContextProvider を実装してエージェントにアタッチすることで、エージェントにメモリを追加する方法について説明します。

Important

すべてのエージェントの種類で ContextProviderがサポートされているわけではありません。 この手順では、ChatAgentをサポートするContextProviderを使用します。

[前提条件]

前提条件とパッケージのインストールについては、このチュートリアルの 「エージェントの作成と実行 」の手順を参照してください。

ContextProvider を作成する

ContextProviderは継承できる抽象クラスであり、AgentThreadのためにChatAgentと関連付けることができます。 これにより、次のことができます。

  1. エージェントが基になる推論サービスを呼び出す前と後にカスタム ロジックを実行します。
  2. 基になる推論サービスを呼び出す前に、エージェントに追加のコンテキストを指定します。
  3. エージェントに提供され、エージェントによって生成されたすべてのメッセージを検査します。

呼び出し前イベントと呼び出し後イベント

ContextProvider クラスには、エージェントが基になる推論サービスを呼び出す前と後にカスタム ロジックを実行するためにオーバーライドできる 2 つのメソッドがあります。

  • invoking - エージェントが基になる推論サービスを呼び出す前に呼び出されます。 Context オブジェクトを返すことで、エージェントに追加のコンテキストを提供できます。 このコンテキストは、基になるサービスを呼び出す前に、エージェントの既存のコンテキストとマージされます。 要求に追加する指示、ツール、メッセージを提供できます。
  • invoked - 基になる推論サービスからエージェントが応答を受信した後に呼び出されます。 要求メッセージと応答メッセージを検査し、コンテキスト プロバイダーの状態を更新できます。

シリアル化

ContextProvider インスタンスは、スレッドの作成時、およびシリアル化された状態からスレッドが再開されるときに、作成され、 AgentThread にアタッチされます。

ContextProvider インスタンスには、エージェントの呼び出し間で永続化する必要がある独自の状態がある場合があります。 たとえば、ユーザーに関する情報を記憶するメモリ コンポーネントは、その状態の一部としてメモリを持っている可能性があります。

スレッドの永続化を許可するには、 ContextProvider クラスのシリアル化を実装する必要があります。 また、スレッドの再開時にシリアル化されたデータから状態を復元できるコンストラクターも提供する必要があります。

ContextProvider の実装例

カスタム メモリ コンポーネントの次の例では、ユーザーの名前と経過時間を記憶し、各呼び出しの前にエージェントに提供します。

まず、メモリを保持するモデル クラスを作成します。

from pydantic import BaseModel

class UserInfo(BaseModel):
    name: str | None = None
    age: int | None = None

その後、 ContextProvider を実装してメモリを管理できます。 以下の UserInfoMemory クラスには、次の動作が含まれています。

  1. チャット クライアントを使用して、各実行の最後に新しいメッセージがスレッドに追加されたときに、ユーザー メッセージ内のユーザーの名前と経過時間を検索します。
  2. 各呼び出しの前に、エージェントに現在のメモリを提供します。
  3. 使用可能なメモリがない場合は、不足している情報をユーザーに要求し、情報が提供されるまで質問に答えないようにエージェントに指示します。
  4. また、スレッド状態の一部としてメモリを永続化できるようにシリアル化も実装します。

from collections.abc import MutableSequence, Sequence
from typing import Any

from agent_framework import ContextProvider, Context, ChatAgent, ChatClientProtocol, ChatMessage, ChatOptions


class UserInfoMemory(ContextProvider):
    def __init__(self, chat_client: ChatClientProtocol, user_info: UserInfo | None = None, **kwargs: Any):
        """Create the memory.

        If you pass in kwargs, they will be attempted to be used to create a UserInfo object.
        """
        self._chat_client = chat_client
        if user_info:
            self.user_info = user_info
        elif kwargs:
            self.user_info = UserInfo.model_validate(kwargs)
        else:
            self.user_info = UserInfo()

    async def invoked(
        self,
        request_messages: ChatMessage | Sequence[ChatMessage],
        response_messages: ChatMessage | Sequence[ChatMessage] | None = None,
        invoke_exception: Exception | None = None,
        **kwargs: Any,
    ) -> None:
        """Extract user information from messages after each agent call."""
        # Ensure request_messages is a list
        messages_list = [request_messages] if isinstance(request_messages, ChatMessage) else list(request_messages)

        # Check if we need to extract user info from user messages
        user_messages = [msg for msg in messages_list if msg.role.value == "user"]

        if (self.user_info.name is None or self.user_info.age is None) and user_messages:
            try:
                # Use the chat client to extract structured information
                result = await self._chat_client.get_response(
                    messages=messages_list,
                    chat_options=ChatOptions(
                        instructions=(
                            "Extract the user's name and age from the message if present. "
                            "If not present return nulls."
                        ),
                        response_format=UserInfo,
                    ),
                )

                # Update user info with extracted data
                if result.value and isinstance(result.value, UserInfo):
                    if self.user_info.name is None and result.value.name:
                        self.user_info.name = result.value.name
                    if self.user_info.age is None and result.value.age:
                        self.user_info.age = result.value.age

            except Exception:
                pass  # Failed to extract, continue without updating

    async def invoking(self, messages: ChatMessage | MutableSequence[ChatMessage], **kwargs: Any) -> Context:
        """Provide user information context before each agent call."""
        instructions: list[str] = []

        if self.user_info.name is None:
            instructions.append(
                "Ask the user for their name and politely decline to answer any questions until they provide it."
            )
        else:
            instructions.append(f"The user's name is {self.user_info.name}.")

        if self.user_info.age is None:
            instructions.append(
                "Ask the user for their age and politely decline to answer any questions until they provide it."
            )
        else:
            instructions.append(f"The user's age is {self.user_info.age}.")

        # Return context with additional instructions
        return Context(instructions=" ".join(instructions))

    def serialize(self) -> str:
        """Serialize the user info for thread persistence."""
        return self.user_info.model_dump_json()

エージェントでの ContextProvider の使用

カスタム ContextProviderを使用するには、エージェントの作成時にインスタンス化された ContextProvider を指定する必要があります。

ChatAgentを作成するときに、context_providers パラメーターを指定して、メモリ コンポーネントをエージェントにアタッチできます。

import asyncio
from agent_framework import ChatAgent
from agent_framework.azure import AzureAIAgentClient
from azure.identity.aio import AzureCliCredential

async def main():
    async with AzureCliCredential() as credential:
        chat_client = AzureAIAgentClient(credential=credential)

        # Create the memory provider
        memory_provider = UserInfoMemory(chat_client)

        # Create the agent with memory
        async with ChatAgent(
            chat_client=chat_client,
            instructions="You are a friendly assistant. Always address the user by their name.",
            context_providers=memory_provider,
        ) as agent:
            # Create a new thread for the conversation
            thread = agent.get_new_thread()

            print(await agent.run("Hello, what is the square root of 9?", thread=thread))
            print(await agent.run("My name is Ruaidhrí", thread=thread))
            print(await agent.run("I am 20 years old", thread=thread))

            # Access the memory component via the thread's context_providers attribute and inspect the memories
            if thread.context_provider:
                user_info_memory = thread.context_provider.providers[0]
                if isinstance(user_info_memory, UserInfoMemory):
                    print()
                    print(f"MEMORY - User Name: {user_info_memory.user_info.name}")
                    print(f"MEMORY - User Age: {user_info_memory.user_info.age}")


if __name__ == "__main__":
    asyncio.run(main())

次のステップ