Exercício - Implementar o serviço Azure Cosmos DB para NoSQL
O serviço Azure Cosmos DB (CosmosDbService
) gerencia consultas, criações, exclusões e atualizações de sessões e mensagens em seu aplicativo assistente de IA. Para gerenciar todas essas operações, o serviço é necessário para implementar vários métodos para cada operação potencial usando vários recursos do SDK do .NET.
Há vários requisitos fundamentais a abordar neste exercício:
- Implementar operações para criar uma sessão ou mensagem
- Implementar consultas para recuperar várias sessões ou mensagens
- Implementar uma operação para atualizar uma única sessão ou atualização em lote de várias mensagens
- Implementar uma operação para consultar e excluir várias sessões e mensagens relacionadas
Criar uma sessão ou mensagem
O Azure Cosmos DB para NoSQL armazena dados no formato JSON, o que nos permite armazenar vários tipos de dados em um único contêiner. Esta aplicação armazena tanto uma "sessão" de chat com o assistente de IA como as "mensagens" individuais dentro de cada sessão. Com a API para NoSQL, o aplicativo pode armazenar os dois tipos de dados no mesmo contêiner e, em seguida, diferenciar entre esses tipos usando um campo simples type
.
Abra o arquivo Serviços/CosmosDbService.cs .
Dentro do
InsertSessionAsync
método, remova qualquer código de espaço reservado existente.public async Task<Session> InsertSessionAsync(Session session) { }
Crie uma nova variável chamada
partitionKey
de tipoPartitionKey
usando a propriedade daSessionId
sessão atual como parâmetro.PartitionKey partitionKey = new(session.SessionId);
Invoque o
CreateItemAsync
método do contêiner passando no parâmetro epartitionKey
nasession
variável. Retorne a resposta como resultado doInsertSessionAsync
método.return await _container.CreateItemAsync<Session>( item: session, partitionKey: partitionKey );
Dentro do
InsertMessageAsync
método, remova qualquer código de espaço reservado existente.public async Task<Message> InsertMessageAsync(Message message) { }
Crie uma
PartitionKey
variável usandosession.SessionId
como o valor da chave de partição.PartitionKey partitionKey = new(message.SessionId);
Crie uma nova variável de mensagem nomeada
newMessage
com aTimestamp
propriedade atualizada para o carimbo de data/hora UTC atual.Message newMessage = message with { TimeStamp = DateTime.UtcNow };
Invoque
CreateItemAsync
a passagem das novas variáveis de chave de mensagem e de partição. Retorne a resposta como resultado deInsertMessageAsync
.return await _container.CreateItemAsync<Message>( item: newMessage, partitionKey: partitionKey );
Salve o arquivo Serviços/CosmosDbService.cs .
Recuperar várias sessões ou mensagens
Há dois casos de uso principais em que o aplicativo precisa recuperar vários itens do nosso contêiner. Primeiro, o aplicativo recupera todas as sessões para o usuário atual filtrando os itens para aqueles em que type = Session
. Em segundo lugar, o aplicativo recupera todas as mensagens de uma sessão executando um filtro semelhante onde type = Session & sessionId = <current-session-id>
. Implemente ambas as consultas aqui usando o SDK do .NET e um iterador de feed.
Dentro do
GetSessionsAsync
método, remova qualquer código de espaço reservado existente.public async Task<List<Session>> GetSessionsAsync() { }
Crie uma nova variável chamada
query
do tipoQueryDefinition
com a consultaSELECT DISTINCT * FROM c WHERE c.type = @type
SQL . Use o método fluentWithParameter
para atribuir oSession
nome da classe como o valor para o parâmetro.QueryDefinition query = new QueryDefinition("SELECT DISTINCT * FROM c WHERE c.type = @type") .WithParameter("@type", nameof(Session));
Invoque o método genérico
GetItemQueryIterator<>
na_container
variável passando o tipoSession
genérico e aquery
variável como um parâmetro. Armazene o resultado em uma variável do tipoFeedIterator<Session>
chamadaresponse
.FeedIterator<Session> response = _container.GetItemQueryIterator<Session>(query);
Crie uma nova variável de lista genérica chamada
output
.List<Session> output = new();
Crie um loop while que é executado até
response.HasMoreResults
que não seja mais verdadeiro.while (response.HasMoreResults) { }
Nota
Usar um loop while aqui irá efetivamente percorrer todas as páginas da sua resposta até que não restem páginas.
Dentro do loop while, obtenha de forma assíncrona a próxima página de resultados invocando
ReadNextAsync
aresponse
variável e, em seguida, adicione esses resultados à variável de lista chamadaoutput
.FeedResponse<Session> results = await response.ReadNextAsync(); output.AddRange(results);
Fora do loop while, retorne a
output
variável com uma lista de sessões como resultado doGetSessionsAsync
método.return output;
Dentro do
GetSessionMessagesAsync
método, remova qualquer código de espaço reservado existente.public async Task<List<Message>> GetSessionMessagesAsync(string sessionId) { }
Crie uma
query
variável do tipoQueryDefinition
. Use a consultaSELECT * FROM c WHERE c.sessionId = @sessionId AND c.type = @type
SQL . Use o método fluentWithParameter
para atribuir o@sessionId
parâmetro ao identificador de sessão passado como um parâmetro e o@type
parâmetro ao nome daMessage
classe.QueryDefinition query = new QueryDefinition("SELECT * FROM c WHERE c.sessionId = @sessionId AND c.type = @type") .WithParameter("@sessionId", sessionId) .WithParameter("@type", nameof(Message));
Crie um
FeedIterator<Message>
usando aquery
variável e oGetItemQueryIterator<>
método.FeedIterator<Message> response = _container.GetItemQueryIterator<Message>(query);
Use um loop while para iterar todas as páginas de resultados e armazenar os resultados em uma única
List<Message>
variável chamadaoutput
.List<Message> output = new(); while (response.HasMoreResults) { FeedResponse<Message> results = await response.ReadNextAsync(); output.AddRange(results); }
Retornar a
output
variável como resultado doGetSessionMessagesAsync
método.return output;
Salve o arquivo Serviços/CosmosDbService.cs .
Atualizar uma ou mais sessões ou mensagens
Há cenários em que uma única sessão requer uma atualização ou mais de uma mensagem requer uma atualização. Para o primeiro cenário, use o ReplaceItemAsync
método do SDK para substituir um item existente por uma versão modificada. Para o segundo cenário, use o recurso de lote transacional do SDK para modificar várias mensagens em um único lote.
Dentro do
UpdateSessionAsync
método, remova qualquer código de espaço reservado existente.public async Task<Session> UpdateSessionAsync(Session session) { }
Crie uma
PartitionKey
variável usandosession.SessionId
como o valor da chave de partição.PartitionKey partitionKey = new(session.SessionId);
Invoque
ReplaceItemAsync
a passagem do identificador exclusivo e da chave de partição da nova mensagem. Retorne a resposta como resultado deUpdateSessionAsync
.return await _container.ReplaceItemAsync( item: session, id: session.Id, partitionKey: partitionKey );
Dentro do
UpsertSessionBatchAsync
método, remova qualquer código de espaço reservado existente.public async Task UpsertSessionBatchAsync(params dynamic[] messages) { }
validar se todas as mensagens contêm um único identificador de sessão (
SessionId
) usando a consulta integrada ao idioma (LINQ). Se alguma das mensagens contiver um valor diferente, lance umArgumentException
arquivo .if (messages.Select(m => m.SessionId).Distinct().Count() > 1) { throw new ArgumentException("All items must have the same partition key."); }
Crie uma nova
PartitionKey
variável usando aSessionId
propriedade da primeira mensagem.PartitionKey partitionKey = new(messages.First().SessionId);
Nota
Lembre-se, você pode assumir com segurança que todas as mensagens têm o mesmo identificador de sessão se o aplicativo tiver sido movido para este ponto no código do método.
Crie uma nova variável chamada
batch
de tipoTransactionalBatch
invocando oCreateTransactionalBatch
método da_container
variável. Use a variável de chave de partição atual para as operações em lote.TransactionalBatch batch = _container.CreateTransactionalBatch(partitionKey);
Importante
Lembre-se, todas as transações dentro deste lote devem estar na mesma partição lógica.
Itere
messages
sobre cada mensagem na matriz usando um loop foreach.foreach (var message in messages) { }
Dentro do loop foreach, adicione cada mensagem como uma operação de upsert ao lote.
batch.UpsertItem( item: message );
Nota
Upsert diz ao Azure Cosmos DB para determinar, do lado do servidor, se um item deve ser substituído ou atualizado. O Azure Cosmos DB fará essa determinação com a
id
chave de partição e de cada item.Fora do loop foreach, invoque de forma assíncrona o
ExecuteAsync
método do lote para executar todas as operações dentro do lote.await batch.ExecuteAsync();
Salve o arquivo Serviços/CosmosDbService.cs .
Remover uma sessão e todas as mensagens relacionadas
Por fim, combine a funcionalidade de consulta e lote transacional para remover vários itens. Neste exemplo, obtenha o item de sessão e todas as mensagens relacionadas consultando todos os itens com um identificador de sessão específico, independentemente do tipo. Em seguida, crie um lote transacional para excluir todos os itens correspondentes como uma única transação.
Dentro do
DeleteSessionAndMessagesAsync
método, remova qualquer código de espaço reservado existente.public async Task DeleteSessionAndMessagesAsync(string sessionId) { }
Crie uma variável chamada
partitionKey
de tipoPartitionKey
usando osesionId
valor da cadeia de caracteres passado como um parâmetro para esse método.PartitionKey partitionKey = new(sessionId);
Usando o mesmo
sessionId
parâmetro de método, crie umQueryDefinition
objeto que localize todos os itens que correspondem ao identificador de sessão. Use um parâmetro de consulta para osessionId
e certifique-se de não filtrar a consulta no tipo de item.QueryDefinition query = new QueryDefinition("SELECT VALUE c.id FROM c WHERE c.sessionId = @sessionId") .WithParameter("@sessionId", sessionId);
Nota
Se aplicar um
type
filtro nesta consulta, poderá perder inadvertidamente mensagens ou sessões relacionadas que devem ser removidas em massa como parte desta operação.Crie um novo
FeedIterator<string>
usoGetItemQueryIterator
e a consulta que você criou.FeedIterator<string> response = _container.GetItemQueryIterator<string>(query);
Crie um
TransactionalBatch
nomebatch
usandoCreateTransactionalBatch
e a variável de chave de partição.TransactionalBatch batch = _container.CreateTransactionalBatch(partitionKey);
Crie um loop while para iterar em todas as páginas de resultados. Dentro do loop while, obtenha a próxima página de resultados e use um loop foreach para iterar todos os identificadores de item por página. Dentro do loop foreach, adicione uma operação em lote para excluir o item usando
batch.DeleteItem
.while (response.HasMoreResults) { FeedResponse<string> results = await response.ReadNextAsync(); foreach (var itemId in results) { batch.DeleteItem( id: itemId ); } }
Após o loop while, execute o lote usando
batch.ExecuteAsync
.await batch.ExecuteAsync();
Salve o arquivo Serviços/CosmosDbService.cs .
Verifique o seu trabalho
Agora seu aplicativo tem uma implementação completa do Azure OpenAI e do Azure Cosmos DB. Você pode testar o aplicativo de ponta a ponta depurando a solução.
Abra um novo terminal.
Inicie o aplicativo com recarregamentos a quente habilitados usando
dotnet watch
o .dotnet watch run --non-interactive
O Visual Studio Code inicia o navegador simples na ferramenta novamente com o aplicativo Web em execução. Na aplicação web, crie uma nova sessão de chat e faça uma pergunta ao assistente de IA. Em seguida, feche o aplicativo Web em execução.
Feche o terminal. Agora, abra um novo terminal.
Inicie o aplicativo mais uma vez com recarregamentos quentes habilitados usando
dotnet watch
o .dotnet watch run --non-interactive
O Visual Studio Code inicia o navegador simples na ferramenta mais uma vez com o aplicativo Web em execução. Para essa iteração, observe que suas sessões de bate-papo persistem entre as sessões de depuração.
Feche o terminal.