Udostępnij przez


Dodawanie pamięci do agenta

W tym samouczku pokazano, jak dodać pamięć do agenta, implementując komponent AIContextProvider i integrując go z agentem.

Ważne

Nie wszystkie typy agentów obsługują AIContextProvider. W tym kroku jest używany element ChatClientAgent, który obsługuje AIContextProvider element.

Wymagania wstępne

Aby uzyskać wymagania wstępne i zainstalować pakiety NuGet, zobacz krok Tworzenie i uruchamianie prostego agenta w tym samouczku.

Utwórz komponent AIContextProvider

AIContextProvider jest abstrakcyjną klasą, po której można dziedziczyć, i którą można skojarzyć z AgentThread dla ChatClientAgent. Umożliwia to:

  1. Uruchom logikę niestandardową przed wywołaniem przez agenta podstawowej usługi wnioskowania i po nim.
  2. Podaj dodatkowy kontekst agentowi przed wywołaniem podstawowej usługi wnioskowania.
  3. Sprawdź wszystkie komunikaty dostarczone i wygenerowane przez agenta.

Zdarzenia przed i po wywołaniu

Klasa AIContextProvider ma dwie metody, które można przesłonić, aby uruchomić logikę niestandardową przed i po wywołaniu podstawowej usługi wnioskowania przez agenta:

  • InvokingAsync - wywoływane przed wywołaniem przez agenta podstawowej usługi wnioskowania. Możesz podać dodatkowy kontekst agentowi, zwracając AIContext obiekt. Ten kontekst zostanie scalony z istniejącym kontekstem agenta przed wywołaniem usługi bazowej. Można podać instrukcje, narzędzia i komunikaty, które mają zostać dodane do żądania.
  • InvokedAsync — wywoływana po otrzymaniu przez agenta odpowiedzi z wewnętrznej usługi inferencyjnej. Możesz sprawdzić komunikaty żądania i odpowiedzi oraz zaktualizować stan dostawcy kontekstu.

Serializacja

AIContextProvider wystąpienia są tworzone i dołączane do elementu AgentThread w momencie utworzenia wątku oraz gdy wątek jest wznawiany ze stanu serializowanego.

Instancja AIContextProvider może mieć własny stan, który powinien być zachowywany między wywołaniami agenta. Na przykład składnik pamięci, który zapamiętuje informacje o użytkowniku, może mieć wspomnienia w ramach jego stanu.

Aby umożliwić przechowywanie wątków, należy zaimplementować metodę SerializeAsyncAIContextProvider klasy. Należy również podać konstruktor, który przyjmuje parametr JsonElement, którego można użyć do deserializacji stanu podczas wznawiania wątku.

Przykładowa implementacja AIContextProvider

Poniższy przykład niestandardowego składnika pamięci zapamiętuje nazwę i wiek użytkownika i udostępnia go agentowi przed każdym wywołaniem.

Najpierw utwórz klasę modelu do przechowywania wspomnień.

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

Następnie można wdrożyć element AIContextProvider aby zarządzać pamięcią. Poniższa UserInfoMemory klasa zawiera następujące zachowanie:

  1. Używa elementu IChatClient , aby wyszukać nazwę użytkownika i wiek w komunikatach użytkownika po dodaniu nowych komunikatów do wątku na końcu każdego uruchomienia.
  2. Udostępnia on wszystkie bieżące wspomnienia agentowi przed każdym wywołaniem.
  3. Jeśli nie są dostępne żadne wspomnienia, instruuje agenta, aby poprosił użytkownika o brakujące informacje i nie odpowiadał na żadne pytania do momentu podania tych informacji.
  4. Implementuje również serializację, aby umożliwić zapisywanie pamięci w ramach stanu wątku.
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);
    }
}

Używanie komponentu AIContextProvider z agentem

Aby użyć niestandardowego AIContextProviderelementu , należy podać element AIContextProviderFactory podczas tworzenia agenta. Ta fabryka umożliwia agentowi utworzenie nowej instancji żądanego AIContextProvider dla każdego wątku.

Podczas tworzenia ChatClientAgent obiektu można podać ChatClientAgentOptions obiekt, który umożliwia udostępnianie AIContextProviderFactory elementu oprócz wszystkich innych opcji agenta.

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

Podczas tworzenia nowego wątku AIContextProvider element zostanie utworzony przez GetNewThread i dołączony do wątku. Po wyodrębnieniu pamięci możliwe jest uzyskanie dostępu do składnika pamięci za pośrednictwem metody wątku GetService i sprawdzenie pamięci.

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

W tym samouczku pokazano, jak dodać pamięć do agenta, implementując ContextProvider moduł i dołączając go do agenta.

Ważne

Nie wszystkie typy agentów obsługują ContextProvider. W tym kroku jest używany element ChatAgent, który obsługuje ContextProvider element.

Wymagania wstępne

Aby uzyskać informacje o wymaganiach wstępnych i instalowanych pakietach, zobacz krok Tworzenie i uruchamianie prostego agenta w tym samouczku.

Utwórz element ContextProvider

ContextProvider jest abstrakcyjną klasą, z którą można dziedziczyć, i którą można skojarzyć z elementem AgentThread dla klasy ChatAgent. Umożliwia to:

  1. Uruchom logikę niestandardową przed i po tym, jak agent wywoła podstawową usługę wnioskowania.
  2. Podaj dodatkowy kontekst agentowi przed wywołaniem podstawowej usługi wnioskowania.
  3. Sprawdź wszystkie komunikaty dostarczone i wygenerowane przez agenta.

Zdarzenia przed i po wywołaniu

Klasa ContextProvider ma dwie metody, które można przesłonić, aby uruchomić logikę niestandardową przed i po wywołaniu podstawowej usługi wnioskowania przez agenta:

  • invoking - wywoływane przed wywołaniem przez agenta podstawowej usługi wnioskowania. Możesz podać dodatkowy kontekst agentowi, zwracając Context obiekt. Ten kontekst zostanie scalony z istniejącym kontekstem agenta przed wywołaniem usługi bazowej. Można podać instrukcje, narzędzia i komunikaty, które mają zostać dodane do żądania.
  • invoked — wywoływana po otrzymaniu przez agenta odpowiedzi z wewnętrznej usługi inferencyjnej. Możesz sprawdzić komunikaty żądania i odpowiedzi oraz zaktualizować stan dostawcy kontekstu.

Serializacja

ContextProvider wystąpienia są tworzone i dołączane do elementu AgentThread w momencie utworzenia wątku oraz gdy wątek jest wznawiany ze stanu serializowanego.

Instancja ContextProvider może mieć własny stan, który powinien być zachowywany między wywołaniami agenta. Na przykład składnik pamięci, który zapamiętuje informacje o użytkowniku, może mieć wspomnienia w ramach jego stanu.

Aby umożliwić utrwalanie wątków, należy zaimplementować serializację dla klasy ContextProvider. Należy również podać konstruktor, który może przywrócić stan z serializowanych danych podczas wznawiania wątku.

Przykładowa implementacja ContextProvider

Poniższy przykład niestandardowego składnika pamięci zapamiętuje nazwę i wiek użytkownika i udostępnia go agentowi przed każdym wywołaniem.

Najpierw utwórz klasę modelu do przechowywania wspomnień.

from pydantic import BaseModel

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

Następnie można wdrożyć element ContextProvider aby zarządzać pamięcią. Poniższa UserInfoMemory klasa zawiera następujące zachowanie:

  1. Używa klienta czatu do wyszukiwania nazwy użytkownika i wieku w wiadomościach użytkownika, gdy nowe wiadomości są dodawane do wątku na końcu każdego uruchomienia.
  2. Udostępnia on wszystkie bieżące wspomnienia agentowi przed każdym wywołaniem.
  3. Jeśli nie są dostępne żadne wspomnienia, instruuje agenta, aby poprosił użytkownika o brakujące informacje i nie odpowiadał na żadne pytania do momentu podania tych informacji.
  4. Implementuje również serializację, aby umożliwić zapisywanie pamięci w ramach stanu wątku.

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

Używanie komponentu ContextProvider z agentem

Aby użyć niestandardowego elementu ContextProvider, należy dostarczyć zainstancjonowane ContextProvider podczas tworzenia agenta.

Podczas tworzenia ChatAgent można podać context_providers parametr w celu dołączenia składnika pamięci do agenta.

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

Dalsze kroki