Partilhar via


Adicionando memória a um agente

Este tutorial mostra como adicionar memória a um agente implementando um AIContextProvider e anexando-o ao agente.

Importante

Nem todos os tipos de agentes suportam AIContextProvider. Esta etapa utiliza um ChatClientAgent, que oferece suporte ao AIContextProvider.

Pré-requisitos

Para obter pré-requisitos e instalar pacotes NuGet, consulte a etapa Criar e executar um agente simples neste tutorial.

Criar um AIContextProvider

AIContextProvider é uma classe abstrata da qual se pode herdar e que pode ser associada ao AgentThread de um ChatClientAgent. Permite-lhe:

  1. Execute a lógica personalizada antes e depois que o agente invoca o serviço de inferência subjacente.
  2. Forneça contexto adicional ao agente antes que ele invoque o serviço de inferência subjacente.
  3. Inspecione todas as mensagens fornecidas e produzidas pelo agente.

Eventos pré e pós invocação

A AIContextProvider classe tem dois métodos que você pode substituir para executar a lógica personalizada antes e depois que o agente invoca o serviço de inferência subjacente:

  • InvokingAsync - chamado antes de o agente invocar o serviço de inferência subjacente. Você pode fornecer contexto adicional ao agente retornando um AIContext objeto. Esse contexto será mesclado com o contexto existente do agente antes de invocar o serviço subjacente. É possível fornecer instruções, ferramentas e mensagens para adicionar à solicitação.
  • InvokedAsync - chamado depois de o agente ter recebido uma resposta do serviço de inferência subjacente. Você pode inspecionar as mensagens de solicitação e resposta e atualizar o estado do provedor de contexto.

Serialização

AIContextProvider As instâncias são criadas e anexadas a um AgentThread quando o thread é criado e quando um thread é retomado de um estado serializado.

A AIContextProvider instância pode ter seu próprio estado que precisa ser persistido entre invocações do agente. Por exemplo, um componente de memória que lembra informações sobre o usuário pode ter memórias como parte de seu estado.

Para permitir threads persistentes, precisa de implementar o SerializeAsync método da classe AIContextProvider. Você também precisa fornecer um construtor que usa um JsonElement parâmetro, que pode ser usado para desserializar o estado ao retomar um thread.

Exemplo de implementação AIContextProvider

O exemplo a seguir de um componente de memória personalizado lembra o nome e a idade de um usuário e os fornece ao agente antes de cada invocação.

Primeiro, crie uma classe modelo para armazenar as memórias.

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

Em seguida, você pode implementar o AIContextProvider para gerenciar as memórias. A UserInfoMemory classe abaixo contém o seguinte comportamento:

  1. Ele usa um IChatClient para procurar o nome e a idade do usuário nas mensagens do usuário quando novas mensagens são adicionadas ao thread no final de cada execução.
  2. Ele fornece todas as memórias atuais ao agente antes de cada invocação.
  3. Se não houver memórias disponíveis, ele instrui o agente a pedir ao usuário as informações ausentes e a não responder a nenhuma pergunta até que as informações sejam fornecidas.
  4. Ele também implementa a serialização para permitir a persistência das memórias como parte do estado do thread.
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);
    }
}

Usando o AIContextProvider com um agente

Para usar o personalizado AIContextProvider, você precisa fornecer um AIContextProviderFactory ao criar o agente. Esta fábrica permite ao agente criar uma nova instância do AIContextProvider desejado para cada thread.

Ao criar um ChatClientAgent é possível fornecer um ChatClientAgentOptions objeto que permite fornecer o AIContextProviderFactory além de todas as outras opções do agente.

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

Ao criar um novo tópico, o AIContextProvider será criado por GetNewThread e anexado ao tópico. Uma vez que as memórias são extraídas, é possível acessar o componente de memória através do método do GetService thread e inspecionar as memórias.

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

Este tutorial mostra como adicionar memória a um agente implementando um ContextProvider e anexando-o ao agente.

Importante

Nem todos os tipos de agentes suportam ContextProvider. Esta etapa utiliza um ChatAgent, que oferece suporte ao ContextProvider.

Pré-requisitos

Para obter pré-requisitos e instalar pacotes, consulte a etapa Criar e executar um agente simples neste tutorial.

Criar um ContextProvider

ContextProvider é uma classe abstrata da qual se pode herdar e que pode ser associada a um AgentThread para um ChatAgent. Permite-lhe:

  1. Execute a lógica personalizada antes e depois que o agente invoca o serviço de inferência subjacente.
  2. Forneça contexto adicional ao agente antes que ele invoque o serviço de inferência subjacente.
  3. Inspecione todas as mensagens fornecidas e produzidas pelo agente.

Eventos pré e pós invocação

A ContextProvider classe tem dois métodos que você pode substituir para executar a lógica personalizada antes e depois que o agente invoca o serviço de inferência subjacente:

  • invoking - chamado antes de o agente invocar o serviço de inferência subjacente. Você pode fornecer contexto adicional ao agente retornando um Context objeto. Esse contexto será mesclado com o contexto existente do agente antes de invocar o serviço subjacente. É possível fornecer instruções, ferramentas e mensagens para adicionar à solicitação.
  • invoked - chamado depois de o agente ter recebido uma resposta do serviço de inferência subjacente. Você pode inspecionar as mensagens de solicitação e resposta e atualizar o estado do provedor de contexto.

Serialização

ContextProvider As instâncias são criadas e anexadas a um AgentThread quando o thread é criado e quando um thread é retomado de um estado serializado.

A ContextProvider instância pode ter seu próprio estado que precisa ser persistido entre invocações do agente. Por exemplo, um componente de memória que lembra informações sobre o usuário pode ter memórias como parte de seu estado.

Para permitir threads persistentes, você precisa implementar a serialização para a ContextProvider classe. Você também precisa fornecer um construtor que possa restaurar o estado de dados serializados ao retomar um thread.

Exemplo de implementação do ContextProvider

O exemplo a seguir de um componente de memória personalizado lembra o nome e a idade de um usuário e os fornece ao agente antes de cada invocação.

Primeiro, crie uma classe modelo para armazenar as memórias.

from pydantic import BaseModel

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

Em seguida, você pode implementar o ContextProvider para gerenciar as memórias. A UserInfoMemory classe abaixo contém o seguinte comportamento:

  1. Ele usa um cliente de bate-papo para procurar o nome e a idade do usuário nas mensagens do usuário quando novas mensagens são adicionadas ao thread no final de cada execução.
  2. Ele fornece todas as memórias atuais ao agente antes de cada invocação.
  3. Se não houver memórias disponíveis, ele instrui o agente a pedir ao usuário as informações ausentes e a não responder a nenhuma pergunta até que as informações sejam fornecidas.
  4. Ele também implementa a serialização para permitir a persistência das memórias como parte do estado do thread.

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()

Usando o ContextProvider com um agente

Para usar o personalizado ContextProvider, você precisa fornecer o instanciado ContextProvider ao criar o agente.

Ao criar um ChatAgent , você pode fornecer o context_providers parâmetro para anexar o componente de memória ao agente.

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())

Próximos passos