Partilhar via


Armazenando o histórico de bate-papo no armazenamento de terceiros.

Este tutorial mostra como armazenar o histórico de bate-papo do agente em armazenamento externo ao implementar uma solução personalizada com ChatMessageStore associado ao ChatClientAgent.

Por padrão, ao usar ChatClientAgent, o histórico de bate-papo é armazenado na memória no objeto AgentThread ou no serviço de inferência subjacente, se o serviço oferecer suporte a ele.

Quando os serviços não exigem que o histórico de bate-papo seja armazenado no serviço, é possível fornecer um armazenamento personalizado para persistir o histórico de bate-papo em vez de confiar no comportamento padrão na memória.

Pré-requisitos

Para obter os pré-requisitos, consulte a etapa Criar e executar um agente simples neste tutorial.

Instalar pacotes NuGet

Para usar o Microsoft Agent Framework com o Azure OpenAI, você precisa instalar os seguintes pacotes NuGet:

dotnet add package Azure.AI.OpenAI --prerelease
dotnet add package Azure.Identity
dotnet add package Microsoft.Agents.AI.OpenAI --prerelease

Além disso, utilizarás o armazenamento de vetores na memória para guardar mensagens de chat.

dotnet add package Microsoft.SemanticKernel.Connectors.InMemory --prerelease

Criar uma Loja ChatMessage personalizada

Para criar um personalizado ChatMessageStore, você precisa implementar a classe abstrata ChatMessageStore e fornecer implementações para os métodos necessários.

Métodos de armazenamento e recuperação de mensagens

Os métodos mais importantes a implementar são:

  • AddMessagesAsync - Chamado para adicionar novas mensagens à loja.
  • GetMessagesAsync - chamado para recuperar as mensagens da loja.

GetMessagesAsync deve retornar as mensagens em ordem cronológica crescente. Todas as mensagens ChatClientAgent retornadas por ele serão usadas ao fazer chamadas para o subjacente IChatClient. Portanto, é importante que esse método considere os limites do modelo subjacente e retorne apenas quantas mensagens puderem ser manipuladas pelo modelo.

Qualquer lógica de redução do histórico de bate-papo, como resumo ou corte, deve ser feita antes de retornar mensagens do GetMessagesAsync.

Serialização

ChatMessageStore As instâncias são criadas e anexadas a um AgentThread quando o thread é criado e quando um thread é retomado de um estado serializado.

Embora as mensagens reais que compõem o histórico de bate-papo sejam armazenadas externamente, a ChatMessageStore instância pode precisar armazenar chaves ou outro estado para identificar o histórico de bate-papo no armazenamento externo.

Para permitir threads persistentes, deve implementar o método SerializeStateAsync da classe ChatMessageStore. 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.

Exemplo de implementação do ChatMessageStore

A implementação de exemplo a seguir armazena mensagens de bate-papo em um repositório de vetores.

AddMessagesAsync atualiza mensagens para o repositório vetorial, usando uma chave exclusiva para cada mensagem.

GetMessagesAsync Recupera as mensagens para o thread atual do repositório de vetores, ordena-as por carimbo de data/hora e as retorna em ordem crescente.

Quando a primeira mensagem é recebida, o repositório gera uma chave exclusiva para o thread, que é usada para identificar o histórico de bate-papo no repositório de vetores para chamadas subsequentes.

A chave exclusiva é armazenada na ThreadDbKey propriedade, que é serializada e desserializada usando o SerializeStateAsync método e o construtor que recebe um JsonElement. Portanto, esta chave será mantida como parte do estado AgentThread, permitindo que o tópico seja retomado mais tarde e continue usando o mesmo histórico de bate-papo.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.VectorData;
using Microsoft.SemanticKernel.Connectors.InMemory;

internal sealed class VectorChatMessageStore : ChatMessageStore
{
    private readonly VectorStore _vectorStore;

    public VectorChatMessageStore(
        VectorStore vectorStore,
        JsonElement serializedStoreState,
        JsonSerializerOptions? jsonSerializerOptions = null)
    {
        this._vectorStore = vectorStore ?? throw new ArgumentNullException(nameof(vectorStore));
        if (serializedStoreState.ValueKind is JsonValueKind.String)
        {
            this.ThreadDbKey = serializedStoreState.Deserialize<string>();
        }
    }

    public string? ThreadDbKey { get; private set; }

    public override async Task AddMessagesAsync(
        IEnumerable<ChatMessage> messages,
        CancellationToken cancellationToken)
    {
        this.ThreadDbKey ??= Guid.NewGuid().ToString("N");
        var collection = this._vectorStore.GetCollection<string, ChatHistoryItem>("ChatHistory");
        await collection.EnsureCollectionExistsAsync(cancellationToken);
        await collection.UpsertAsync(messages.Select(x => new ChatHistoryItem()
        {
            Key = this.ThreadDbKey + x.MessageId,
            Timestamp = DateTimeOffset.UtcNow,
            ThreadId = this.ThreadDbKey,
            SerializedMessage = JsonSerializer.Serialize(x),
            MessageText = x.Text
        }), cancellationToken);
    }

    public override async Task<IEnumerable<ChatMessage>> GetMessagesAsync(
        CancellationToken cancellationToken)
    {
        var collection = this._vectorStore.GetCollection<string, ChatHistoryItem>("ChatHistory");
        await collection.EnsureCollectionExistsAsync(cancellationToken);
        var records = collection
            .GetAsync(
                x => x.ThreadId == this.ThreadDbKey, 10,
                new() { OrderBy = x => x.Descending(y => y.Timestamp) },
                cancellationToken);

        List<ChatMessage> messages = [];
        await foreach (var record in records)
        {
            messages.Add(JsonSerializer.Deserialize<ChatMessage>(record.SerializedMessage!)!);
        }

        messages.Reverse();
        return messages;
    }

    public override JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptions = null) =>
        // We have to serialize the thread id, so that on deserialization you can retrieve the messages using the same thread id.
        JsonSerializer.SerializeToElement(this.ThreadDbKey);

    private sealed class ChatHistoryItem
    {
        [VectorStoreKey]
        public string? Key { get; set; }
        [VectorStoreData]
        public string? ThreadId { get; set; }
        [VectorStoreData]
        public DateTimeOffset? Timestamp { get; set; }
        [VectorStoreData]
        public string? SerializedMessage { get; set; }
        [VectorStoreData]
        public string? MessageText { get; set; }
    }
}

Usando o ChatMessageStore personalizado com um ChatClientAgent

Para usar o personalizado ChatMessageStore, você precisa fornecer um ChatMessageStoreFactory ao criar o agente. Essa fábrica permite que o agente crie uma nova instância do desejado ChatMessageStore para cada thread.

Ao criar um ChatClientAgent é possível fornecer um ChatClientAgentOptions objeto que permite fornecer o ChatMessageStoreFactory além de todas as outras opções do agente.

using Azure.AI.OpenAI;
using Azure.Identity;
using OpenAI;

AIAgent agent = new AzureOpenAIClient(
    new Uri("https://<myresource>.openai.azure.com"),
    new AzureCliCredential())
     .GetChatClient("gpt-4o-mini")
     .CreateAIAgent(new ChatClientAgentOptions
     {
         Name = "Joker",
         Instructions = "You are good at telling jokes.",
         ChatMessageStoreFactory = ctx =>
         {
             // Create a new chat message store for this agent that stores the messages in a vector store.
             return new VectorChatMessageStore(
                new InMemoryVectorStore(),
                ctx.SerializedState,
                ctx.JsonSerializerOptions);
         }
     });

Este tutorial mostra como armazenar o histórico de bate-papo do agente em armazenamento externo ao implementar uma solução personalizada com ChatMessageStore associado ao ChatAgent.

Por padrão, ao usar ChatAgent, o histórico de bate-papo é armazenado na memória no objeto AgentThread ou no serviço de inferência subjacente, se o serviço oferecer suporte a ele.

Quando os serviços não exigem ou não são capazes de armazenar o histórico de bate-papo no serviço, é possível fornecer um armazenamento personalizado para o histórico de bate-papo persistente em vez de confiar no comportamento padrão na memória.

Pré-requisitos

Para obter os pré-requisitos, consulte a etapa Criar e executar um agente simples neste tutorial.

Criar uma Loja ChatMessage personalizada

Para criar um personalizado ChatMessageStore, você precisa implementar o ChatMessageStore protocolo e fornecer implementações para os métodos necessários.

Métodos de armazenamento e recuperação de mensagens

Os métodos mais importantes a implementar são:

  • add_messages - Chamado para adicionar novas mensagens à loja.
  • list_messages - chamado para recuperar as mensagens da loja.

list_messages deve retornar as mensagens em ordem cronológica crescente. Todas as mensagens retornadas por ele serão usadas pelo ChatAgent ao fazer chamadas para o cliente de bate-papo subjacente. Portanto, é importante que esse método considere os limites do modelo subjacente e retorne apenas quantas mensagens puderem ser manipuladas pelo modelo.

Qualquer lógica de redução do histórico de bate-papo, como resumo ou corte, deve ser feita antes de retornar mensagens do list_messages.

Serialização

ChatMessageStore As instâncias são criadas e anexadas a um AgentThread quando o thread é criado e quando um thread é retomado de um estado serializado.

Embora as mensagens reais que compõem o histórico de bate-papo sejam armazenadas externamente, a ChatMessageStore instância pode precisar armazenar chaves ou outro estado para identificar o histórico de bate-papo no armazenamento externo.

Para permitir a persistência de threads, é necessário implementar os métodos serialize_state e deserialize_state do protocolo ChatMessageStore. Esses métodos permitem que o estado do repositório seja persistente e restaurado ao retomar um thread.

Exemplo de implementação do ChatMessageStore

A implementação de exemplo a seguir armazena mensagens de bate-papo no Redis usando a estrutura de dados das Listas Redis.

No add_messages, ele armazena mensagens no Redis usando RPUSH para anexá-las ao final da lista em ordem cronológica.

list_messages recupera as mensagens para o thread atual do Redis usando LRANGE e as retorna em ordem cronológica crescente.

Quando a primeira mensagem é recebida, a loja gera uma chave exclusiva para o thread, que é usada para identificar o histórico de bate-papo no Redis para chamadas subsequentes.

A chave exclusiva e outras configurações são armazenadas e podem ser serializadas e desserializadas usando os métodos serialize_state e deserialize_state. Esse estado, portanto, será persistido como parte do estado AgentThread, permitindo que o tópico seja retomado mais tarde e continue usando o mesmo histórico de conversas.

from collections.abc import Sequence
from typing import Any
from uuid import uuid4
from pydantic import BaseModel
import json
import redis.asyncio as redis
from agent_framework import ChatMessage


class RedisStoreState(BaseModel):
    """State model for serializing and deserializing Redis chat message store data."""

    thread_id: str
    redis_url: str | None = None
    key_prefix: str = "chat_messages"
    max_messages: int | None = None


class RedisChatMessageStore:
    """Redis-backed implementation of ChatMessageStore using Redis Lists."""

    def __init__(
        self,
        redis_url: str | None = None,
        thread_id: str | None = None,
        key_prefix: str = "chat_messages",
        max_messages: int | None = None,
    ) -> None:
        """Initialize the Redis chat message store.

        Args:
            redis_url: Redis connection URL (for example, "redis://localhost:6379").
            thread_id: Unique identifier for this conversation thread.
                      If not provided, a UUID will be auto-generated.
            key_prefix: Prefix for Redis keys to namespace different applications.
            max_messages: Maximum number of messages to retain in Redis.
                         When exceeded, oldest messages are automatically trimmed.
        """
        if redis_url is None:
            raise ValueError("redis_url is required for Redis connection")

        self.redis_url = redis_url
        self.thread_id = thread_id or f"thread_{uuid4()}"
        self.key_prefix = key_prefix
        self.max_messages = max_messages

        # Initialize Redis client
        self._redis_client = redis.from_url(redis_url, decode_responses=True)

    @property
    def redis_key(self) -> str:
        """Get the Redis key for this thread's messages."""
        return f"{self.key_prefix}:{self.thread_id}"

    async def add_messages(self, messages: Sequence[ChatMessage]) -> None:
        """Add messages to the Redis store.

        Args:
            messages: Sequence of ChatMessage objects to add to the store.
        """
        if not messages:
            return

        # Serialize messages and add to Redis list
        serialized_messages = [self._serialize_message(msg) for msg in messages]
        await self._redis_client.rpush(self.redis_key, *serialized_messages)

        # Apply message limit if configured
        if self.max_messages is not None:
            current_count = await self._redis_client.llen(self.redis_key)
            if current_count > self.max_messages:
                # Keep only the most recent max_messages using LTRIM
                await self._redis_client.ltrim(self.redis_key, -self.max_messages, -1)

    async def list_messages(self) -> list[ChatMessage]:
        """Get all messages from the store in chronological order.

        Returns:
            List of ChatMessage objects in chronological order (oldest first).
        """
        # Retrieve all messages from Redis list (oldest to newest)
        redis_messages = await self._redis_client.lrange(self.redis_key, 0, -1)

        messages = []
        for serialized_message in redis_messages:
            message = self._deserialize_message(serialized_message)
            messages.append(message)

        return messages

    async def serialize_state(self, **kwargs: Any) -> Any:
        """Serialize the current store state for persistence.

        Returns:
            Dictionary containing serialized store configuration.
        """
        state = RedisStoreState(
            thread_id=self.thread_id,
            redis_url=self.redis_url,
            key_prefix=self.key_prefix,
            max_messages=self.max_messages,
        )
        return state.model_dump(**kwargs)

    async def deserialize_state(self, serialized_store_state: Any, **kwargs: Any) -> None:
        """Deserialize state data into this store instance.

        Args:
            serialized_store_state: Previously serialized state data.
            **kwargs: Additional arguments for deserialization.
        """
        if serialized_store_state:
            state = RedisStoreState.model_validate(serialized_store_state, **kwargs)
            self.thread_id = state.thread_id
            self.key_prefix = state.key_prefix
            self.max_messages = state.max_messages

            # Recreate Redis client if the URL changed
            if state.redis_url and state.redis_url != self.redis_url:
                self.redis_url = state.redis_url
                self._redis_client = redis.from_url(self.redis_url, decode_responses=True)

    def _serialize_message(self, message: ChatMessage) -> str:
        """Serialize a ChatMessage to JSON string."""
        message_dict = message.model_dump()
        return json.dumps(message_dict, separators=(",", ":"))

    def _deserialize_message(self, serialized_message: str) -> ChatMessage:
        """Deserialize a JSON string to ChatMessage."""
        message_dict = json.loads(serialized_message)
        return ChatMessage.model_validate(message_dict)

    async def clear(self) -> None:
        """Remove all messages from the store."""
        await self._redis_client.delete(self.redis_key)

    async def aclose(self) -> None:
        """Close the Redis connection."""
        await self._redis_client.aclose()

Usando o ChatMessageStore personalizado com um ChatAgent

Para usar o personalizado ChatMessageStore, você precisa fornecer um chat_message_store_factory ao criar o agente. Essa fábrica permite que o agente crie uma nova instância do desejado ChatMessageStore para cada thread.

Ao criar um ChatAgent, você pode fornecer o chat_message_store_factory parâmetro além de todas as outras opções do agente.

from azure.identity import AzureCliCredential
from agent_framework import ChatAgent
from agent_framework.openai import AzureOpenAIChatClient

# Create the chat agent with custom message store factory
agent = ChatAgent(
    chat_client=AzureOpenAIChatClient(
        endpoint="https://<myresource>.openai.azure.com",
        credential=AzureCliCredential(),
        ai_model_id="gpt-4o-mini"
    ),
    name="Joker",
    instructions="You are good at telling jokes.",
    chat_message_store_factory=lambda: RedisChatMessageStore(
        redis_url="redis://localhost:6379"
    )
)

# Use the agent with persistent chat history
thread = agent.get_new_thread()
response = await agent.run("Tell me a joke about pirates", thread=thread)
print(response.text)

Próximos passos