Compartilhar via


Tutorial: Implantar um aplicativo Blazor do .NET conectado ao SQL do Azure e ao Azure OpenAI no Serviço de Aplicativo do Azure

Ao criar aplicativos inteligentes, talvez você queira fundamentar o contexto do seu aplicativo usando seus próprios dados SQL. Com o anúncio recente do suporte a vetores do SQL do Azure (versão prévia), você pode fundamentar o contexto usando os dados do SQL do Azure que você já tem com novas funções vetoriais que ajudam a gerenciar dados vetoriais.

Neste tutorial, você criará um aplicativo de exemplo RAG configurando uma busca em vetores híbridos em relação ao banco de dados SQL do Azure usando um aplicativo Blazor do .NET 8. Este exemplo se baseia na documentação anterior para implantar um aplicativo Blazor do .NET com o OpenAI. Se você quiser implantar o aplicativo usando um modelo azd, visite o repositório de Exemplos do Azure com instruções de implantação.

Pré-requisitos

  • Um recurso do OpenAI do Azure com modelos implantados
  • Um aplicativo Web Blazor do .NET 8 ou 9 implantado no Serviço de Aplicativo
  • Um recurso do banco de dados SQL do Azure com incorporações de vetores.

1. Configurar o aplicativo Web Blazor

Para este exemplo, estamos criando uma caixa de chat simples para interagir. Se você estiver usando o aplicativo .NET Blazor do artigo anterior como requisito prévio, pode ignorar as alterações no arquivo OpenAI.razor, pois o conteúdo é o mesmo. No entanto, você precisa verificar se os seguintes pacotes estão instalados:

Instale os seguintes pacotes para interagir com o OpenAI do Azure e o SQL do Azure.

  • Microsoft.SemanticKernel
  • Microsoft.Data.SqlClient
  1. Clique com o botão direito do mouse na pasta Páginas encontrada na pasta Componentes e adicione um novo item chamado OpenAI.razor
  2. Adicione o seguinte código ao arquivo OpenAI.razor e clique em Salvar
@page "/openai"
@rendermode InteractiveServer
@inject Microsoft.Extensions.Configuration.IConfiguration _config

<PageTitle>OpenAI</PageTitle>

<h3>OpenAI input query: </h3>
<input class="col-sm-4" @bind="userMessage" />
<button class="btn btn-primary" @onclick="SemanticKernelClient">Send Request</button>

<br />
<br />

<h4>Server response:</h4> <p>@serverResponse</p>

@code {

	@using Microsoft.SemanticKernel;
	@using Microsoft.SemanticKernel.ChatCompletion;
	
	}

Chaves de API e pontos de extremidade

O recurso Azure OpenAI requer o uso de chaves de API e valores de endpoint. Consulte Usar referências do Key Vault como configurações de aplicativo no Serviço de Aplicativo do Azure e no Azure Functions para gerenciar e lidar com seus segredos com o OpenAI do Azure. Embora não seja obrigatório, recomendamos usar a identidade gerenciada para proteger seu cliente sem a necessidade de gerenciar as chaves de API. Confira a documentação anterior para configurar seu cliente do OpenAI do Azure na próxima etapa para usar a identidade gerenciada com o OpenAI do Azure.

2. Adicionar o cliente do OpenAI do Azure

Depois de adicionar a interface do chat, podemos configurar o cliente do OpenAI do Azure usando o Semantic Kernel. Adicione o código a seguir para criar o cliente que se conecta ao recurso do OpenAI do Azure. Você precisa usar suas chaves de API do OpenAI do Azure e as informações do ponto de extremidade que foram configuradas e manipuladas na etapa anterior.

@inject Microsoft.Extensions.Configuration.IConfiguration _config

@code {

	@using Microsoft.SemanticKernel;
	@using Microsoft.SemanticKernel.ChatCompletion;

	private string? userMessage;
	private string? serverResponse;

	private async Task SemanticKernelClient()
	{
	
		// App settings
		string deploymentName = _config["DEPLOYMENT_NAME"];
		string endpoint = _config["ENDPOINT"];
		string apiKey = _config["API_KEY"];
		string modelId = _config["MODEL_ID"];

		var builder = Kernel.CreateBuilder();

		// Chat completion service
		builder.Services.AddAzureOpenAIChatCompletion(
			deploymentName: deploymentName,
			endpoint: endpoint,
			apiKey: apiKey,
			modelId: modelId
		);

		var kernel = builder.Build();

		// Create prompt template
		var chat = kernel.CreateFunctionFromPrompt(
            @"{{$history}}
            User: {{$request}}
            Assistant: ");

		ChatHistory chatHistory = new("""You are a helpful assistant that answers questions""");

		var chatResult = kernel.InvokeStreamingAsync<StreamingChatMessageContent>(
				chat,
				new()
					{
						{ "request", userMessage },
						{ "history", string.Join("\n", chatHistory.Select(x => x.Role + ": " + x.Content)) }
					}
			);

		string message = "";
		await foreach (var chunk in chatResult)
		{
			message += chunk;
		}

		// Add messages to chat history
		chatHistory.AddUserMessage(userMessage!);
		chatHistory.AddAssistantMessage(message);

		serverResponse = message;

A partir daqui, você deve ter um aplicativo de chat de trabalho conectado ao OpenAI. Em seguida, configuraremos nosso banco de dados SQL do Azure para trabalhar com nosso aplicativo de chat.

3. Implantar modelos OpenAI do Azure

Para preparar seu banco de dados SQL do Azure para busca em vetores, você precisa usar um modelo de incorporação para gerar incorporações usadas para pesquisa, além do seu modelo de linguagem implantado inicial. Para este exemplo, estamos usando os seguintes modelos:

  • text-embedding-ada-002 é usado para gerar as incorporações
  • gpt-3.5-turbo é usado para o modelo de linguagem

Esses dois modelos precisam ser implantados antes de continuar a próxima etapa. Visite a documentação para implantar modelos com o Azure OpenAI usando o Microsoft Foundry.

4. Vetorizar seu banco de dados SQL

Para executar uma busca em vetores híbridos no seu banco de dados SQL do Azure, primeiro você precisa ter as incorporações apropriadas no seu banco de dados. Há várias maneiras de vetorizar seu banco de dados. Uma opção é usar o seguinte vetorizador do banco de dados SQL do Azure para gerar incorporações para seu banco de dados SQL. Vectorize seu banco de dados SQL do Azure antes de continuar.

5. Criar procedimento para gerar incorporações

Com o suporte a vetores do SQL do Azure (versão prévia), você pode criar um procedimento armazenado que usará um tipo de dados Vetor para armazenar as incorporações geradas para consultas de pesquisa. O procedimento armazenado invoca um ponto de extremidade da API REST externa para obter as incorporações. Confira a documentação para usar o Azure Data Studio para se conectar ao banco de dados antes de executar a consulta.

Use o seguinte para criar um procedimento armazenado com seu editor de consultas SQL preferido. Você precisa preencher o parâmetro @url com o nome do recurso Azure OpenAI e preencher o endpoint com a chave de API do seu modelo de incorporação de texto. Você observará o nome do modelo como parte do @url, que será preenchido com sua consulta de pesquisa.

CREATE PROCEDURE [dbo].[GET_EMBEDDINGS]
(
    @model VARCHAR(MAX),
    @text NVARCHAR(MAX),
    @embedding VECTOR(1536) OUTPUT
)
AS
BEGIN
    DECLARE @retval INT, @response NVARCHAR(MAX);
    DECLARE @url VARCHAR(MAX);
    DECLARE @payload NVARCHAR(MAX) = JSON_OBJECT('input': @text);

    -- Set the @url variable with proper concatenation before the EXEC statement
    SET @url = 'https://<resourcename>.openai.azure.com/openai/deployments/' + @model + '/embeddings?api-version=2023-03-15-preview';

    EXEC dbo.sp_invoke_external_rest_endpoint 
        @url = @url,
        @method = 'POST',   
        @payload = @payload,   
        @headers = '{"Content-Type":"application/json", "api-key":"<openAIkey>"}', 
        @response = @response OUTPUT;

    -- Use JSON_QUERY to extract the embedding array directly
    DECLARE @jsonArray NVARCHAR(MAX) = JSON_QUERY(@response, '$.result.data[0].embedding');

    
    SET @embedding = CAST(@jsonArray as VECTOR(1536));
END
GO

Depois de criar o procedimento armazenado, você poderá exibi-lo na pasta Procedimentos Armazenados encontrada na pasta Programabilidade do banco de dados SQL. Depois de criado, você poderá executar uma pesquisa de similaridade de teste no editor de consultas SQL usando o nome do modelo de incorporação de texto. Isso usa o procedimento armazenado para gerar incorporações e usar uma função de distância de vetor para calcular a distância do vetor e retornar resultados com base na consulta de texto.

6. Conectar e pesquisar seu banco de dados

Agora que seu banco de dados está configurado para criar incorporações, podemos nos conectar a ele em nosso aplicativo e configurar a consulta de busca em vetores híbridos.

Adicione o código a seguir ao seu arquivo OpenAI.razor e certifique-se de que a cadeia de conexão esteja atualizada para usar a cadeia de conexão do banco de dados SQL do Azure implantado. O código está usando um parâmetro SQL que passará com segurança a entrada do usuário do aplicativo de chat para a consulta.

// Database connection string
var connectionString = _config["AZURE_SQL_CONNSTRING"];

try
{
    await using var connection = new SqlConnection(connectionString);
    Console.WriteLine("\nQuery results:");

    await connection.OpenAsync();

    // Hybrid search query
    var sql =
        @"DECLARE @e VECTOR(1536);
		EXEC dbo.GET_EMBEDDINGS @model = 'text-embedding-ada-002', @text = '@userMessage', @embedding = @e OUTPUT;

			 -- Comprehensive query with multiple filters.
		SELECT TOP(5)
			f.Score,
			f.Summary,
			f.Text,
			VECTOR_DISTANCE('cosine', @e, VectorBinary) AS Distance,
			CASE
				WHEN LEN(f.Text) > 100 THEN 'Detailed Review'
				ELSE 'Short Review'
			END AS ReviewLength,
			CASE
				WHEN f.Score >= 4 THEN 'High Score'
				WHEN f.Score BETWEEN 2 AND 3 THEN 'Medium Score'
				ELSE 'Low Score'
			END AS ScoreCategory
		FROM finefoodembeddings10k$ f
		WHERE
			f.UserId NOT LIKE 'Anonymous%' -- User-based filter to exclude anonymous users
			AND f.Score >= 4 -- Score threshold filter
			AND LEN(f.Text) > 50 -- Text length filter for detailed reviews
            AND (f.Text LIKE '%juice%') -- Inclusion of specific words
		ORDER BY
			Distance,  -- Order by distance
			f.Score DESC, -- Secondary order by review score
			ReviewLength DESC; -- Tertiary order by review length
	";

    // Set SQL Parameter to pass in user message
    SqlParameter param = new SqlParameter();
    param.ParameterName = "@userMessage";
    param.Value = userMessage;
    
    await using var command = new SqlCommand(sql, connection);

    // add parameter to SqlCommand
    command.Parameters.Add(param);

    await using var reader = await command.ExecuteReaderAsync();

    while (await reader.ReadAsync())
    {
        // write results to console logs
        Console.WriteLine("{0} {1} {2} {3}", "Score: " + reader.GetDouble(0), "Text: " + reader.GetString(1), "Summary: " + reader.GetString(2), "Distance: " + reader.GetDouble(3));
        Console.WriteLine();

        // add results to chat history
        chatHistory.AddSystemMessage(reader.GetString(1) + ", " + reader.GetString(2));
    }
}
catch (SqlException e)
{
    Console.WriteLine($"SQL Error: {e.Message}");
}
catch (Exception e)
{
    Console.WriteLine(e.ToString());
}

Console.WriteLine("Done");

A consulta SQL em si está usando uma pesquisa híbrida que executa o procedimento armazenado configurado anteriormente para criar incorporações e usa o SQL para filtrar os resultados desejados. Neste exemplo, estamos dando pontuações aos resultados e ordenando a saída para obter os melhores resultados antes de usá-los como contexto fundamentado para gerar uma resposta.

Proteger seus dados com a Identidade Gerenciada

O SQL do Azure pode usar a Identidade Gerenciada com o Microsoft Entra para proteger o recurso SQL configurando a autenticação sem senha. Siga as etapas abaixo para configurar uma cadeia de conexão sem senha que será usada em seu aplicativo.

  1. Navegue até o recurso do servidor SQL do Azure e clique no Microsoft Entra ID em Configurações.
  2. Em seguida, clique em +Definir administrador e procure e selecione seu nome para configurar o Entra ID e clique em Salvar. Agora, o Entra ID está configurado em seu servidor SQL e aceita a autenticação do Entra ID.
  3. Em seguida, vá para o recurso de banco de dados e copie a cadeia de conexão ADO.NET (autenticação sem senha do Microsoft Entra) e adicione-a ao código onde você mantém sua cadeia de conexão.

Neste ponto, você pode testar seu aplicativo localmente com sua cadeia de conexão sem senha.

Conceder acesso ao Serviço de Aplicativo

Antes de fazer uma chamada para seu banco de dados ao usar a identidade gerenciada com o SQL do Azure, primeiro você precisará conceder ao Serviço de Aplicativo acesso ao banco de dados. Se você não tiver feito isso neste momento, precisará criar um aplicativo Web primeiro antes de concluir as próximas etapas.

Siga estas etapas para conceder acesso ao seu aplicativo Web:

  1. Navegue até seu aplicativo Web e clique na folha Identidade encontrada em Configurações.
  2. Ative a identidade gerenciada atribuída pelo sistema, caso ainda não o tenha feito.
  3. Navegue até o recurso de banco de dados e abra o editor de consultas encontrado no menu do lado esquerdo. Talvez seja necessário entrar para usar o editor.
  4. Execute os comandos a seguir para criar um usuário e alterar as funções, adicionando o aplicativo web como membro
-- Create member, alter roles to your database
CREATE USER "<your-app-name>" FROM EXTERNAL PROVIDER;
ALTER ROLE db_datareader ADD MEMBER "<your-app-name>";
ALTER ROLE db_datawriter ADD MEMBER "<your-app-name>";
ALTER ROLE db_ddladmin ADD MEMBER "<your-app-name>";
GO
  1. Em seguida, conceda o acesso para usar o procedimento armazenado e o ponto de extremidade do OpenAI do Azure
-- Grant access to use stored procedure
GRANT EXECUTE ON OBJECT::[dbo].[GET_EMBEDDINGS]  
  TO "<your-app-name>"  
GO

-- Grant access to use Azure OpenAI endpoint in stored procedure
GRANT EXECUTE ANY EXTERNAL ENDPOINT TO "<your-app-name>";
GO

A partir daqui, o banco de dados SQL do Azure agora está seguro e você pode implantar seu aplicativo no Serviço de Aplicativo.

Este é o exemplo completo da página OpenAI.razor adicionada:

@page "/openai"
@rendermode InteractiveServer
@inject Microsoft.Extensions.Configuration.IConfiguration _config

<PageTitle>OpenAI</PageTitle>

<h3>OpenAI input query: </h3>
<input class="col-sm-4" @bind="userMessage" />
<button class="btn btn-primary" @onclick="SemanticKernelClient">Send Request</button>

<br />
<br />

<h4>Server response:</h4> <p>@serverResponse</p>

@code {

	@using Microsoft.SemanticKernel;
	@using Microsoft.SemanticKernel.ChatCompletion;
	@using Microsoft.Data.SqlClient;

	private string? userMessage;
	private string? serverResponse;

	private async Task SemanticKernelClient()
	{
		// App settings
		string deploymentName = _config["DEPLOYMENT_NAME"];
		string endpoint = _config["ENDPOINT"];
		string apiKey = _config["API_KEY"];
		string modelId = _config["MODEL_ID"];

		// Semantic Kernel builder
		var builder = Kernel.CreateBuilder();

		// Chat completion service
		builder.Services.AddAzureOpenAIChatCompletion(
			deploymentName: deploymentName,
			endpoint: endpoint,
			apiKey: apiKey,
			modelId: modelId
		);

		var kernel = builder.Build();

		// Create prompt template
		var chat = kernel.CreateFunctionFromPrompt(
            @"{{$history}}
            User: {{$request}}
            Assistant: ");

		ChatHistory chatHistory = new("""You are a helpful assistant that answers questions about my data""");

		#region Azure SQL
		// Database connection string
		var connectionString = _config["AZURE_SQL_CONNECTIONSTRING"];

		try
		{
			await using var connection = new SqlConnection(connectionString);
			Console.WriteLine("\nQuery results:");
	
			await connection.OpenAsync();

			// Hybrid search query
			var sql =
					@"DECLARE @e VECTOR(1536);
					EXEC dbo.GET_EMBEDDINGS @model = 'text-embedding-ada-002', @text = '@userMessage', @embedding = @e OUTPUT;

						 -- Comprehensive query with multiple filters.
					SELECT TOP(5)
						f.Score,
						f.Summary,
						f.Text,
						VECTOR_DISTANCE('cosine', @e, VectorBinary) AS Distance,
						CASE
							WHEN LEN(f.Text) > 100 THEN 'Detailed Review'
							ELSE 'Short Review'
						END AS ReviewLength,
						CASE
							WHEN f.Score >= 4 THEN 'High Score'
							WHEN f.Score BETWEEN 2 AND 3 THEN 'Medium Score'
							ELSE 'Low Score'
						END AS ScoreCategory
					FROM finefoodembeddings10k$ f
					WHERE
						f.UserId NOT LIKE 'Anonymous%' -- User-based filter to exclude anonymous users
						AND f.Score >= 4 -- Score threshold filter
						AND LEN(f.Text) > 50 -- Text length filter for detailed reviews
                        AND (f.Text LIKE '%juice%') -- Inclusion of specific words
					ORDER BY
						Distance,  -- Order by distance
						f.Score DESC, -- Secondary order by review score
						ReviewLength DESC; -- Tertiary order by review length
				";

			// Set SQL Parameter to pass in user message
			SqlParameter param = new SqlParameter();
			param.ParameterName = "@userMessage";
			param.Value = userMessage;

			await using var command = new SqlCommand(sql, connection);

			// add parameter to SqlCommand
			command.Parameters.Add(param);

			await using var reader = await command.ExecuteReaderAsync();

			while (await reader.ReadAsync())
			{
				// write results to console logs
				Console.WriteLine("{0} {1} {2} {3}", "Score: " + reader.GetDouble(0), "Text: " + reader.GetString(1), "Summary: " + reader.GetString(2), "Distance: " + reader.GetDouble(3));
				Console.WriteLine();

				// add results to chat history
				chatHistory.AddSystemMessage(reader.GetString(1) + ", " + reader.GetString(2));

			}
		}
		catch (SqlException e)
		{
			Console.WriteLine($"SQL Error: {e.Message}");
		}
		catch (Exception e)
		{
			Console.WriteLine(e.ToString());
		}

		Console.WriteLine("Done");
		#endregion

		var chatResult = kernel.InvokeStreamingAsync<StreamingChatMessageContent>(
				chat,
				new()
					{
						{ "request", userMessage },
						{ "history", string.Join("\n", chatHistory.Select(x => x.Role + ": " + x.Content)) }
					}
			);

		string message = "";
		await foreach (var chunk in chatResult)
		{
			message += chunk;
		}

		// Append messages to chat history
		chatHistory.AddUserMessage(userMessage!);
		chatHistory.AddAssistantMessage(message);

		serverResponse = message;

	}
}