Compartilhar via


Armazenando o histórico de chat no armazenamento de terceiros

Este tutorial mostra como armazenar o histórico de conversas do agente no armazenamento externo, implementando um ChatMessageStore personalizado e usando-o com um ChatClientAgent.

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

Quando os serviços não exigem que o histórico de chat seja armazenado no serviço, é possível fornecer um repositório personalizado para persistir o histórico de chat em vez de depender do comportamento padrão na memória.

Pré-requisitos

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

Instalar os 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, você usará o repositório de vetores na memória para armazenar mensagens de chat.

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

Criar um Repositório de ChatMessage personalizado

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 recuperação e armazenamento de mensagens

Os métodos mais importantes a serem implementados são:

  • AddMessagesAsync – chamado para adicionar novas mensagens ao repositório.
  • GetMessagesAsync – chamado para recuperar as mensagens do repositório.

GetMessagesAsync deve retornar as mensagens em ordem cronológica crescente. Todas as mensagens retornadas serão usadas pelo ChatClientAgent 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 tratadas pelo modelo.

Qualquer lógica de redução do histórico de chat, como resumo ou corte, deve ser feita antes de retornar mensagens de 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 chat sejam armazenadas externamente, talvez a ChatMessageStore instância precise armazenar chaves ou outro estado para identificar o histórico de chat no repositório externo.

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

Implementação de ChatMessageStore de exemplo

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

AddMessagesAsync insere mensagens no repositório de vetores usando uma chave exclusiva para cada mensagem.

GetMessagesAsync recupera as mensagens do thread atual do repositório de vetores, as ordena 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 chat 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 usa um JsonElement. Portanto, essa chave será mantida como parte do AgentThread estado, permitindo que o thread seja retomado mais tarde e continue usando o mesmo histórico de chat.

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 ChatMessageStore especificado para cada thread.

Ao criar um ChatClientAgent , é possível fornecer um ChatClientAgentOptions objeto que permita fornecer além de ChatMessageStoreFactory todas as outras opções de 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 conversas do agente no armazenamento externo, implementando um ChatMessageStore personalizado e usando-o com um ChatAgent.

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

Quando os serviços não exigem ou não são capazes de armazenar o histórico de chat no serviço, é possível fornecer um repositório personalizado para persistir o histórico de chat em vez de depender do comportamento padrão na memória.

Pré-requisitos

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

Criar um Repositório de ChatMessage personalizado

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

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

Os métodos mais importantes a serem implementados são:

  • add_messages – chamado para adicionar novas mensagens ao repositório.
  • list_messages – chamado para recuperar as mensagens do repositório.

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

Qualquer lógica de redução do histórico de chat, como resumo ou corte, deve ser feita antes de retornar mensagens de 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 chat sejam armazenadas externamente, talvez a ChatMessageStore instância precise armazenar chaves ou outro estado para identificar o histórico de chat no repositório externo.

Para permitir threads persistentes, você precisa implementar os métodos serialize_state e deserialize_state do protocolo ChatMessageStore. Esses métodos permitem que o estado do repositório seja mantido e restaurado ao retomar um thread.

Implementação de ChatMessageStore de exemplo

A implementação de exemplo a seguir armazena mensagens de chat no Redis usando a estrutura de dados Listas Redis.

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

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

Quando a primeira mensagem é recebida, o repositório gera uma chave exclusiva para o thread, que é usada para identificar o histórico de chat 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. Portanto, esse estado será mantido como parte do AgentThread estado, permitindo que o thread seja retomado mais tarde e continue usando o mesmo histórico de chat.

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 ChatMessageStore especificado 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 de 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óximas etapas