이 자습서에서는 AIContextProvider를 구현하여 에이전트에 메모리를 추가하고, 이를 에이전트에 연결하는 방법을 보여줍니다.
중요합니다
모든 에이전트 유형이 AIContextProvider를 지원하는 것은 아닙니다. 이 단계에서는 ChatClientAgent를 사용하며, 이는 AIContextProvider을 지원합니다.
필수 조건
필수 구성 요소 및 NuGet 패키지 설치는 이 자습서의 간단한 에이전트 만들기 및 실행 단계를 참조하세요.
AIContextProvider 만들기
AIContextProvider는 상속할 수 있는 추상 클래스이며, AgentThread와 연결하여 ChatClientAgent에 사용할 수 있습니다.
다음을 수행할 수 있습니다.
- 에이전트가 기본 유추 서비스를 호출하기 전과 후에 사용자 지정 논리를 실행합니다.
- 기본 유추 서비스를 호출하기 전에 에이전트에 추가 컨텍스트를 제공합니다.
- 에이전트에서 제공되고 생성된 모든 메시지를 검사합니다.
호출 사전 및 사후 이벤트
AIContextProvider 클래스에는 에이전트가 기본 유추 서비스를 호출하기 전과 후에 사용자 지정 논리를 실행하도록 재정의할 수 있는 두 가지 메서드가 있습니다.
-
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 다음 동작이 포함됩니다.
- 각 실행이 끝날 때 새 메시지가 스레드에 추가되면 사용자 메시지에서 사용자의 이름과 나이를 찾는 데 사용됩니다
IChatClient. - 각 호출 전에 에이전트에 현재 메모리를 제공합니다.
- 메모리를 사용할 수 없는 경우 에이전트는 사용자에게 누락된 정보를 요청하고 정보가 제공될 때까지 질문에 대답하지 않도록 지시합니다.
- 또한 메모리를 스레드 상태의 일부로 유지할 수 있도록 serialization을 구현합니다.
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()
{
Instructions = "You are a friendly assistant. Always address the user by their name.",
AIContextProviderFactory = ctx => new UserInfoMemory(
chatClient.AsIChatClient(),
ctx.SerializedState,
ctx.JsonSerializerOptions)
});
새 스레드를 만들 때 AIContextProvider은(는) GetNewThread에 의해 생성되어 스레드에 연결됩니다. 따라서 메모리가 추출되면 스레드의 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을 에이전트에 연결하여 메모리를 추가하는 방법을 보여줍니다.
중요합니다
모든 에이전트 유형이 ContextProvider를 지원하는 것은 아닙니다. 이 단계에서는 ChatAgent를 사용하며, 이는 ContextProvider을 지원합니다.
필수 조건
필수 구성 요소 및 패키지 설치는 이 자습서의 간단한 에이전트 만들기 및 실행 단계를 참조하세요.
ContextProvider 만들기
ContextProvider는 상속할 수 있으며, AgentThread에 연결할 수 있는 추상 클래스입니다 ChatAgent.
다음을 수행할 수 있습니다.
- 에이전트가 기본 유추 서비스를 호출하기 전과 후에 사용자 지정 논리를 실행합니다.
- 기본 유추 서비스를 호출하기 전에 에이전트에 추가 컨텍스트를 제공합니다.
- 에이전트에서 제공되고 생성된 모든 메시지를 검사합니다.
호출 사전 및 사후 이벤트
ContextProvider 클래스에는 에이전트가 기본 유추 서비스를 호출하기 전과 후에 사용자 지정 논리를 실행하도록 재정의할 수 있는 두 가지 메서드가 있습니다.
-
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 다음 동작이 포함됩니다.
- 각 실행이 끝날 때 새 메시지가 스레드에 추가되면 채팅 클라이언트를 사용하여 사용자 메시지에서 사용자의 이름과 나이를 찾습니다.
- 각 호출 전에 에이전트에 현재 메모리를 제공합니다.
- 메모리를 사용할 수 없는 경우 에이전트는 사용자에게 누락된 정보를 요청하고 정보가 제공될 때까지 질문에 대답하지 않도록 지시합니다.
- 또한 메모리를 스레드 상태의 일부로 유지할 수 있도록 serialization을 구현합니다.
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())