Dela via


Lägga till minne i en agent

Den här handledningen visar hur du lägger till minne i en agent genom att implementera ett AIContextProvider och koppla det till agenten.

Viktigt!

Inte alla agenttyper stöder AIContextProvider. Det här steget använder en ChatClientAgent, som stöder AIContextProvider.

Förutsättningar

Förutsättningar och installation av NuGet-paket finns i steget Skapa och kör en enkel agent i den här självstudien.

Skapa en AIContextProvider

AIContextProvider är en abstrakt klass som du kan ärva från och som kan associeras med AgentThread för en ChatClientAgent. Det gör att du kan:

  1. Kör anpassad logik före och efter att agenten anropar den underliggande slutsatsdragningstjänsten.
  2. Ange ytterligare kontext till agenten innan den anropar den underliggande inferenstjänsten.
  3. Granska alla meddelanden som tillhandahålls och produceras av agenten.

Händelser före och efter anrop

Klassen AIContextProvider har två metoder som du kan åsidosätta för att köra anpassad logik före och efter att agenten anropar den underliggande inferenstjänsten:

  • InvokingAsync – anropas innan agenten anropar den underliggande inferenstjänsten. Du kan ange ytterligare kontext till agenten genom att returnera ett AIContext objekt. Den här kontexten sammanfogas med agentens befintliga kontext innan den underliggande tjänsten anropas. Det är möjligt att tillhandahålla instruktioner, verktyg och meddelanden att lägga till i begäran.
  • InvokedAsync – anropas efter att agenten har fått ett svar från den underliggande inferenstjänsten. Du kan granska begärande- och svarsmeddelandena och uppdatera tillstånd för kontextprovidern.

Serialisering

AIContextProvider instanser skapas och kopplas till en AgentThread när tråden skapas och när en tråd återupptas från ett serialiserat tillstånd.

Instansen AIContextProvider kan ha ett eget tillstånd som måste bevaras mellan agentens anrop. Till exempel kan en minneskomponent som kommer ihåg information om användaren ha minnen som en del av dess tillstånd.

Om du vill tillåta kvarstående trådar måste du implementera SerializeAsync -metoden för AIContextProvider klassen. Du måste också ange en konstruktor som tar en JsonElement parameter, som kan användas för att deserialisera tillståndet när du återupptar en tråd.

Exempelimplementering av AIContextProvider

Följande exempel på en anpassad minneskomponent kommer ihåg en användares namn och ålder och ger den till agenten före varje anrop.

Skapa först en modellklass för att lagra minnena.

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

Sedan kan du implementera AIContextProvider för att hantera minnena. Klassen UserInfoMemory nedan innehåller följande beteende:

  1. Den använder en IChatClient för att söka efter användarens namn och ålder i användarmeddelanden när nya meddelanden läggs till i tråden i slutet av varje körning.
  2. Det ger alla aktuella minnen till agenten före varje anrop.
  3. Om inga minnen är tillgängliga instrueras agenten att be användaren om den saknade informationen och inte svara på några frågor förrän informationen har angetts.
  4. Den implementerar också serialisering för att tillåta att minnena bevaras som en del av trådtillståndet.
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);
    }
}

Använda AIContextProvider med en agent

Om du vill använda den anpassade AIContextProvidermåste du ange en AIContextProviderFactory när du skapar agenten. Med den här fabriken kan agenten skapa en ny instans av önskad AIContextProvider för varje tråd.

När du skapar en ChatClientAgent är det möjligt att tillhandahålla ett ChatClientAgentOptions objekt som gör det möjligt att tillhandahålla AIContextProviderFactory utöver alla andra agentalternativ.

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()
{
    ChatOptions = new() { Instructions = "You are a friendly assistant. Always address the user by their name." },
    AIContextProviderFactory = ctx => new UserInfoMemory(
        chatClient.AsIChatClient(),
        ctx.SerializedState,
        ctx.JsonSerializerOptions)
});

När du skapar en ny tråd skapas den AIContextProvider av GetNewThread och kopplas till tråden. När minnena har extraherats är det därför möjligt att komma åt minneskomponenten via trådens GetService metod och inspektera minnena.

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

Den här handledningen visar hur du tillför minne till en agent genom att implementera en ContextProvider och koppla den till agenten.

Viktigt!

Inte alla agenttyper stöder ContextProvider. Det här steget använder en ChatAgent, som stöder ContextProvider.

Förutsättningar

Förutsättningar och installation av paket finns i steget Skapa och kör en enkel agent i den här självstudien.

Skapa en ContextProvider

ContextProvider är en abstrakt klass som du kan ärva från och som kan associeras med en AgentThread för en ChatAgent. Det gör att du kan:

  1. Kör anpassad logik före och efter att agenten anropar den underliggande slutsatsdragningstjänsten.
  2. Ange ytterligare kontext till agenten innan den anropar den underliggande inferenstjänsten.
  3. Granska alla meddelanden som tillhandahålls och produceras av agenten.

Händelser före och efter anrop

Klassen ContextProvider har två metoder som du kan åsidosätta för att köra anpassad logik före och efter att agenten anropar den underliggande inferenstjänsten:

  • invoking – anropas innan agenten anropar den underliggande inferenstjänsten. Du kan ange ytterligare kontext till agenten genom att returnera ett Context objekt. Den här kontexten sammanfogas med agentens befintliga kontext innan den underliggande tjänsten anropas. Det är möjligt att tillhandahålla instruktioner, verktyg och meddelanden att lägga till i begäran.
  • invoked – anropas efter att agenten har fått ett svar från den underliggande inferenstjänsten. Du kan granska begärande- och svarsmeddelandena och uppdatera tillstånd för kontextprovidern.

Serialisering

ContextProvider instanser skapas och kopplas till en AgentThread när tråden skapas och när en tråd återupptas från ett serialiserat tillstånd.

Instansen ContextProvider kan ha ett eget tillstånd som måste bevaras mellan agentens anrop. Till exempel kan en minneskomponent som kommer ihåg information om användaren ha minnen som en del av dess tillstånd.

Om du vill tillåta kvarstående trådar måste du implementera serialisering för ContextProvider klassen. Du måste också ange en konstruktor som kan återställa tillstånd från serialiserade data när du återupptar en tråd.

Exempel på ContextProvider-implementering

Följande exempel på en anpassad minneskomponent kommer ihåg en användares namn och ålder och ger den till agenten före varje anrop.

Skapa först en modellklass för att lagra minnena.

from pydantic import BaseModel

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

Sedan kan du implementera ContextProvider för att hantera minnena. Klassen UserInfoMemory nedan innehåller följande beteende:

  1. Den använder en chattklient för att leta efter användarens namn och ålder i användarmeddelanden när nya meddelanden läggs till i tråden i slutet av varje körning.
  2. Det ger alla aktuella minnen till agenten före varje anrop.
  3. Om inga minnen är tillgängliga instrueras agenten att be användaren om den saknade informationen och inte svara på några frågor förrän informationen har angetts.
  4. Den implementerar också serialisering för att tillåta att minnena bevaras som en del av trådtillståndet.

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

Använda ContextProvider med en agent

Om du vill använda den anpassade ContextProvidermåste du ange den instansierade ContextProvider när du skapar agenten.

När du skapar en ChatAgent kan du ange parametern context_providers för att koppla minneskomponenten till agenten.

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ästa steg