Compartilhar 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 agente dão suporte AIContextProvider. Esta etapa usa um ChatClientAgent, que oferece suporte a AIContextProvider.

Pré-requisitos

Para pré-requisitos e instalação de pacotes NuGet, consulte a etapa Criar e executar um agente simples neste tutorial.

Criar um AIContextProvider

AIContextProvider é uma classe abstrata da qual você pode herdar e que pode ser associada ao AgentThread para um ChatClientAgent. Ele permite que você:

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

Eventos de 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 que o agente invoque 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 que o agente recebeu 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, você precisa implementar o SerializeAsync método da AIContextProvider classe. 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.

Implementação de AIContextProvider de exemplo

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

Primeiro, crie uma classe de 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 a AIContextProvider para gerenciar as memórias. A UserInfoMemory classe a seguir contém o seguinte comportamento:

  1. Ele usa um IChatClient recurso para procurar o nome e a idade do usuário em mensagens de usuário quando novas mensagens são adicionadas ao thread no final de cada execução.
  2. Ele fornece as memórias atuais para o agente antes de cada invocação.
  3. Se nenhuma memória estiver disponível, ele instruirá o agente a pedir ao usuário as informações ausentes e 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. Essa fábrica permite que o agente crie uma nova instância do AIContextProvider especificado para cada thread.

Ao criar um ChatClientAgent , é possível fornecer um ChatClientAgentOptions objeto que permita fornecer além de AIContextProviderFactory todas as outras opções de 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. Depois que as memórias são extraídas, portanto, é possível acessar o componente de memória por meio 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 agente dão suporte ContextProvider. Esta etapa usa um ChatAgent, que oferece suporte a ContextProvider.

Pré-requisitos

Para pré-requisitos e instalação de pacotes, consulte a etapa Criar e executar um agente simples neste tutorial.

Criar um ContextProvider

ContextProvider é uma classe abstrata da qual você pode herdar, e que pode ser associada a um AgentThread para um ChatAgent. Ele permite que você:

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

Eventos de 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 que o agente invoque 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 que o agente recebeu 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.

Implementação de ContextProvider de exemplo

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

Primeiro, crie uma classe de 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 a ContextProvider para gerenciar as memórias. A UserInfoMemory classe a seguir contém o seguinte comportamento:

  1. Ele usa um cliente de chat para procurar o nome e a idade do usuário em mensagens de usuário quando novas mensagens são adicionadas ao thread no final de cada execução.
  2. Ele fornece as memórias atuais para o agente antes de cada invocação.
  3. Se nenhuma memória estiver disponível, ele instruirá o agente a pedir ao usuário as informações ausentes e 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 ContextProvider personalizado, você precisa fornecer o ContextProvider instanciado ao criar o agente.

Ao criar um ChatAgent, você pode fornecer o parâmetro context_providers 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óximas etapas