Freigeben über


Hinzufügen von Arbeitsspeicher zu einem Agent

In diesem Tutorial wird gezeigt, wie Sie einem Agenten Arbeitsspeicher hinzufügen, indem Sie ein AIContextProvider implementieren und es an den Agenten anfügen.

Von Bedeutung

Nicht alle Agenttypen unterstützen AIContextProvider. In diesem Schritt wird ein ChatClientAgent verwendet, das AIContextProvider unterstützt.

Voraussetzungen

Die erforderlichen Komponenten und die Installation von NuGet-Paketen finden Sie im Schritt Erstellen und Ausführen eines einfachen Agents in diesem Lernprogramm.

Erstellen eines AIContextProviders

AIContextProvider ist eine abstrakte Klasse, von der Sie erben können, und die mit dem AgentThread für ein ChatClientAgent assoziiert werden kann. Sie können folgende Aktionen ausführen:

  1. Führen Sie benutzerdefinierte Logik vor und nach dem Aufruf des zugrunde liegenden Rückschlussdiensts durch den Agent aus.
  2. Stellen Sie dem Agent zusätzlichen Kontext bereit, bevor er den zugrunde liegenden Rückschlussdienst aufruft.
  3. Überprüfen Sie alle vom Agenten bereitgestellten und erstellten Nachrichten.

Ereignisse vor und nach dem Aufrufen

Die AIContextProvider Klasse verfügt über zwei Methoden, mit denen Sie benutzerdefinierte Logik vor und nach dem Aufrufen des zugrunde liegenden Rückschlussdiensts außer Kraft setzen können:

  • InvokingAsync – wird aufgerufen, bevor der Agent den zugrunde liegenden Rückschlussdienst aufruft. Sie können dem Agent zusätzlichen Kontext bereitstellen, indem Sie ein AIContext Objekt zurückgeben. Dieser Kontext wird mit dem vorhandenen Kontext des Agents zusammengeführt, bevor er den zugrunde liegenden Dienst aufruft. Es ist möglich, Anweisungen, Tools und Nachrichten zur Anforderung hinzuzufügen.
  • InvokedAsync – aufgerufen, nachdem der Agent eine Antwort vom zugrunde liegenden Rückschlussdienst erhalten hat. Sie können die Anforderungs- und Antwortnachrichten überprüfen und den Status des Kontextanbieters aktualisieren.

Serialisierung

AIContextProvider Instanzen werden erstellt und an ein AgentThread angefügt, wenn der Thread erstellt wird und wenn er aus einem serialisierten Zustand fortgesetzt wird.

Die AIContextProvider Instanz verfügt möglicherweise über einen eigenen Zustand, der zwischen Aufrufen des Agents beibehalten werden muss. Beispielsweise kann eine Speicherkomponente, die Informationen über den Benutzer speichert, Erinnerungen als Teil des Zustands haben.

Um das Beibehalten von Threads zuzulassen, müssen Sie die SerializeAsync Methode der AIContextProvider Klasse implementieren. Sie müssen auch einen Konstruktor bereitstellen, der einen JsonElement Parameter verwendet, der zum Deserialisieren des Zustands beim Fortsetzen eines Threads verwendet werden kann.

AiContextProvider-Beispielimplementierung

Im folgenden Beispiel einer benutzerdefinierten Speicherkomponente wird der Name und das Alter eines Benutzers gespeichert und dem Agent vor jedem Aufruf bereitgestellt.

Erstellen Sie zunächst eine Modellklasse, um die Erinnerungen zu speichern.

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

Dann können Sie die AIContextProvider Erinnerungen implementieren, um die Erinnerungen zu verwalten. Die UserInfoMemory folgende Klasse enthält das folgende Verhalten:

  1. Es verwendet eine IChatClient , um nach dem Namen und Alter des Benutzers in Benutzernachrichten zu suchen, wenn neue Nachrichten am Ende jeder Ausführung dem Thread hinzugefügt werden.
  2. Es stellt dem Agenten vor jedem Aufruf alle aktuellen Erinnerungen zur Verfügung.
  3. Wenn keine Erinnerungen verfügbar sind, wird der Agent angewiesen, den Benutzer um die fehlenden Informationen zu bitten und keine Fragen zu beantworten, bis die Informationen bereitgestellt werden.
  4. Außerdem wird serialisierung implementiert, um das Beibehalten der Erinnerungen als Teil des Threadzustands zu ermöglichen.
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);
    }
}

Verwenden des AIContextProviders mit einem Agent

Um das benutzerdefinierte AIContextProvider zu verwenden, müssen Sie beim Erstellen des Agents ein AIContextProviderFactory bereitstellen. Diese Factory ermöglicht es dem Agenten, für jeden Thread eine neue Instanz des gewünschten AIContextProvider zu erstellen.

Beim Erstellen eines ChatClientAgent Objekts ist es möglich, ein ChatClientAgentOptions Objekt bereitzustellen, das zusätzlich zu allen anderen Agent-Optionen die AIContextProviderFactory Bereitstellung ermöglicht.

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

Beim Erstellen eines neuen Threads wird AIContextProvider von GetNewThread erstellt und an den Thread angefügt. Sobald Erinnerungen extrahiert werden, ist es daher möglich, über die Methode des GetService Threads auf die Speicherkomponente zuzugreifen und die Erinnerungen zu prüfen.

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

In diesem Lernprogramm wird gezeigt, wie Sie einem Agenten Arbeitsspeicher hinzufügen, indem Sie einen ContextProvider implementieren und an den Agenten anfügen.

Von Bedeutung

Nicht alle Agenttypen unterstützen ContextProvider. In diesem Schritt wird ein ChatAgent verwendet, das ContextProvider unterstützt.

Voraussetzungen

Die erforderlichen Komponenten und die Installation von Paketen finden Sie im Schritt "Erstellen und Ausführen eines einfachen Agents " in diesem Lernprogramm.

Erstellen eines ContextProviders

ContextProvider ist eine abstrakte Klasse, von der abgeleitet werden kann und die mit einem AgentThread für ein ChatAgent assoziiert werden kann. Sie können folgende Aktionen ausführen:

  1. Führen Sie benutzerdefinierte Logik vor und nach dem Aufruf des zugrunde liegenden Rückschlussdiensts durch den Agent aus.
  2. Stellen Sie dem Agent zusätzlichen Kontext bereit, bevor er den zugrunde liegenden Rückschlussdienst aufruft.
  3. Überprüfen Sie alle vom Agenten bereitgestellten und erstellten Nachrichten.

Ereignisse vor und nach dem Aufrufen

Die ContextProvider Klasse verfügt über zwei Methoden, mit denen Sie benutzerdefinierte Logik vor und nach dem Aufrufen des zugrunde liegenden Rückschlussdiensts außer Kraft setzen können:

  • invoking – wird aufgerufen, bevor der Agent den zugrunde liegenden Rückschlussdienst aufruft. Sie können dem Agent zusätzlichen Kontext bereitstellen, indem Sie ein Context Objekt zurückgeben. Dieser Kontext wird mit dem vorhandenen Kontext des Agents zusammengeführt, bevor er den zugrunde liegenden Dienst aufruft. Es ist möglich, Anweisungen, Tools und Nachrichten zur Anforderung hinzuzufügen.
  • invoked – aufgerufen, nachdem der Agent eine Antwort vom zugrunde liegenden Rückschlussdienst erhalten hat. Sie können die Anforderungs- und Antwortnachrichten überprüfen und den Status des Kontextanbieters aktualisieren.

Serialisierung

ContextProvider Instanzen werden erstellt und an ein AgentThread angefügt, wenn der Thread erstellt wird und wenn er aus einem serialisierten Zustand fortgesetzt wird.

Die ContextProvider Instanz verfügt möglicherweise über einen eigenen Zustand, der zwischen Aufrufen des Agents beibehalten werden muss. Beispielsweise kann eine Speicherkomponente, die Informationen über den Benutzer speichert, Erinnerungen als Teil des Zustands haben.

Um das Beibehalten von Threads zuzulassen, müssen Sie die Serialisierung für die ContextProvider Klasse implementieren. Sie müssen auch einen Konstruktor bereitstellen, der den Zustand aus serialisierten Daten wiederherstellen kann, wenn ein Thread fortgesetzt wird.

Beispielimplementierung "ContextProvider"

Im folgenden Beispiel einer benutzerdefinierten Speicherkomponente wird der Name und das Alter eines Benutzers gespeichert und dem Agent vor jedem Aufruf bereitgestellt.

Erstellen Sie zunächst eine Modellklasse, um die Erinnerungen zu speichern.

from pydantic import BaseModel

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

Dann können Sie die ContextProvider Erinnerungen implementieren, um die Erinnerungen zu verwalten. Die UserInfoMemory folgende Klasse enthält das folgende Verhalten:

  1. Er verwendet einen Chatclient, um nach dem Namen und Alter des Benutzers in Benutzernachrichten zu suchen, wenn neue Nachrichten am Ende jeder Ausführung dem Thread hinzugefügt werden.
  2. Es stellt dem Agenten vor jedem Aufruf alle aktuellen Erinnerungen zur Verfügung.
  3. Wenn keine Erinnerungen verfügbar sind, wird der Agent angewiesen, den Benutzer um die fehlenden Informationen zu bitten und keine Fragen zu beantworten, bis die Informationen bereitgestellt werden.
  4. Außerdem wird serialisierung implementiert, um das Beibehalten der Erinnerungen als Teil des Threadzustands zu ermöglichen.

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

Verwenden des ContextProviders mit einem Agent

Um die benutzerdefinierten ContextProvider zu verwenden, müssen Sie beim Erstellen des Agents die instanziierte ContextProvider bereitstellen.

Beim Erstellen eines Parameters ChatAgent können Sie den context_providers Parameter angeben, um die Speicherkomponente an den Agent anzufügen.

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

Nächste Schritte