Nota
O acesso a esta página requer autorização. Podes tentar iniciar sessão ou mudar de diretório.
O acesso a esta página requer autorização. Podes tentar mudar de diretório.
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)