Compartir a través de


Agregar memoria a un agente

En este tutorial se muestra cómo agregar memoria a un agente implementando un AIContextProvider y adjuntándolo al agente.

Importante

No todos los tipos de agente admiten AIContextProvider. En este paso se usa un ChatClientAgent, que admite AIContextProvider.

Prerrequisitos

Para conocer los requisitos previos e instalar paquetes NuGet, consulte el paso Creación y ejecución de un agente sencillo en este tutorial.

Creación de un AIContextProvider

AIContextProvider es una clase abstracta de la que se puede heredar y que puede asociarse con AgentThread para un ChatClientAgent. Le permite:

  1. Ejecute lógica personalizada antes y después de que el agente invoque el servicio de inferencia subyacente.
  2. Proporcione contexto adicional al agente antes de invocar el servicio de inferencia subyacente.
  3. Inspeccione todos los mensajes proporcionados y los generados por el agente.

Eventos de invocación previos y posteriores

La AIContextProvider clase tiene dos métodos que puede invalidar para ejecutar lógica personalizada antes y después de que el agente invoque el servicio de inferencia subyacente:

  • InvokingAsync : se llama antes de que el agente invoque el servicio de inferencia subyacente. Puede proporcionar contexto adicional al agente devolviendo un AIContext objeto . Este contexto se combinará con el contexto existente del agente antes de invocar el servicio subyacente. Es posible proporcionar instrucciones, herramientas y mensajes para agregar a la solicitud.
  • InvokedAsync : se llama después de que el agente haya recibido una respuesta del servicio de inferencia subyacente. Puede inspeccionar los mensajes de solicitud y respuesta y actualizar el estado del proveedor de contexto.

Serialización

AIContextProvider Las instancias se crean y se adjuntan a un AgentThread cuando se crea el subproceso y cuando se reanuda un subproceso desde un estado serializado.

La AIContextProvider instancia puede tener su propio estado que debe conservarse entre las invocaciones del agente. Por ejemplo, un componente de memoria que recuerda información sobre el usuario podría tener memorias como parte de su estado.

Para permitir hilos persistentes, debe implementar el método de la clase SerializeAsyncAIContextProvider. También debe proporcionar un constructor que tome un parámetro JsonElement, que se puede usar para deserializar el estado al reanudar un hilo de ejecución.

Implementación de AIContextProvider de ejemplo

En el ejemplo siguiente de un componente de memoria personalizada se recuerda el nombre y la antigüedad de un usuario y se proporciona al agente antes de cada invocación.

En primer lugar, cree una clase de modelo para almacenar los recuerdos.

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

A continuación, puede implementar AIContextProvider para administrar las memorias. La UserInfoMemory clase siguiente contiene el siguiente comportamiento:

  1. Utiliza un IChatClient para buscar el nombre y la edad del usuario en los mensajes de usuario cuando se añaden nuevos mensajes al hilo al final de cada ejecución.
  2. Proporciona memorias actuales al agente antes de cada invocación.
  3. Si no hay memorias disponibles, indica al agente que pida al usuario la información que falta y no responda a ninguna pregunta hasta que se proporcione la información.
  4. También implementa la serialización para permitir la conservación de las memorias como parte del estado del subproceso.
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);
    }
}

Uso de AIContextProvider con un agente

Para usar el elemento personalizado AIContextProvider, debe proporcionar un AIContextProviderFactory al crear el agente. Esta factoría permite al agente crear una nueva instancia del AIContextProvider deseado para cada subproceso.

Al crear un ChatClientAgent, es posible proporcionar un objeto ChatClientAgentOptions que permite incluir AIContextProviderFactory además de todas las demás opciones del 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)
});

Al crear un nuevo subproceso, el AIContextProvider será creado por GetNewThread y se asociará al subproceso. Una vez extraídas las memorias, es posible acceder al componente de memoria a través del método del subproceso GetService e inspeccionar las memorias.

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

En este tutorial se muestra cómo agregar memoria a un agente mediante la implementación de un ContextProvider y su posterior asociación al agente.

Importante

No todos los tipos de agente admiten ContextProvider. En este paso se usa un ChatAgent, que admite ContextProvider.

Prerrequisitos

Para conocer los requisitos previos e instalar paquetes, consulte el paso Crear y ejecutar un agente sencillo en este tutorial.

Creación de un contextProvider

ContextProvider es una clase abstracta de la que se puede heredar y que se puede asociar con un AgentThread para un ChatAgent. Le permite:

  1. Ejecute lógica personalizada antes y después de que el agente invoque el servicio de inferencia subyacente.
  2. Proporcione contexto adicional al agente antes de invocar el servicio de inferencia subyacente.
  3. Inspeccione todos los mensajes proporcionados y generados por el agente.

Eventos de invocación previos y posteriores

La ContextProvider clase tiene dos métodos que puede invalidar para ejecutar lógica personalizada antes y después de que el agente invoque el servicio de inferencia subyacente:

  • invoking : se llama antes de que el agente invoque el servicio de inferencia subyacente. Puede proporcionar contexto adicional al agente devolviendo un Context objeto . Este contexto se combinará con el contexto existente del agente antes de invocar el servicio subyacente. Es posible proporcionar instrucciones, herramientas y mensajes para agregar a la solicitud.
  • invoked : se llama después de que el agente haya recibido una respuesta del servicio de inferencia subyacente. Puede inspeccionar los mensajes de solicitud y respuesta y actualizar el estado del proveedor de contexto.

Serialización

ContextProvider Las instancias se crean y se adjuntan a un AgentThread cuando se crea el subproceso y cuando se reanuda un subproceso desde un estado serializado.

La ContextProvider instancia puede tener su propio estado que debe conservarse entre las invocaciones del agente. Por ejemplo, un componente de memoria que recuerda información sobre el usuario podría tener memorias como parte de su estado.

Para hacer persistentes los hilos, debe implementar la serialización para la clase ContextProvider. También debe proporcionar un constructor que pueda restaurar el estado a partir de datos serializados al reanudar un subproceso.

Implementación de ContextProvider de ejemplo

En el ejemplo siguiente de un componente de memoria personalizada se recuerda el nombre y la antigüedad de un usuario y se proporciona al agente antes de cada invocación.

En primer lugar, cree una clase de modelo para almacenar los recuerdos.

from pydantic import BaseModel

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

A continuación, puede implementar ContextProvider para administrar las memorias. La UserInfoMemory clase siguiente contiene el siguiente comportamiento:

  1. Usa un cliente de chat para buscar el nombre y la antigüedad del usuario en los mensajes de usuario cuando se agregan nuevos mensajes al subproceso al final de cada ejecución.
  2. Proporciona memorias actuales al agente antes de cada invocación.
  3. Si no hay memorias disponibles, indica al agente que pida al usuario la información que falta y no responda a ninguna pregunta hasta que se proporcione la información.
  4. También implementa la serialización para permitir la conservación de las memorias como parte del estado del subproceso.

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

Uso de ContextProvider con un agente

Para usar el elemento personalizado ContextProvider, debe proporcionar la instancia de ContextProvider cuando se crea el agente.

Al crear un ChatAgent , puede proporcionar el context_providers parámetro para asociar el componente de memoria al 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())

Pasos siguientes