Partager via


Tutoriel : Déployer une application .NET Blazor connectée à Azure SQL et Azure OpenAI sur Azure App Service

Lors de la création d’applications intelligentes, vous souhaitez peut-être ancrer le contexte de votre application à l’aide de vos propres données SQL. Avec la récente annonce de la prise en charge des vecteurs Azure SQL (préversion), vous pouvez ancrer le contexte à l’aide des données Azure SQL dont vous disposez déjà avec de nouvelles fonctions vectorielles qui facilitent la gestion des données vectorielles.

Dans ce tutoriel, vous créez un exemple d’application RAG en paramétrant une recherche vectorielle hybride sur votre base de données Azure SQL à l’aide d’une application Blazor .NET 8. Cet exemple s’appuie sur la documentation précédente pour déployer une application Blazor .NET avec OpenAI. Si vous souhaitez déployer l’application à l’aide d’un modèle azd, vous pouvez consulter le référentiel Azure Samples avec des instructions de déploiement.

Prérequis

  • Une ressource Azure OpenAI avec des modèles déployés
  • Une application web Blazor .NET 8 ou 9 déployée sur App Service
  • Une ressource de base de données Azure SQL avec incorporations de vecteurs.

1. Configurer l’application web Blazor

Pour cet exemple, nous créons une boîte de conversation simple avec laquelle interagir. Si vous utilisez l’application Blazor .NET requise de l’article précédent, vous pouvez ignorer les modifications apportées au fichier OpenAI.razor, car le contenu est le même. Toutefois, vous devez vous assurer que les packages suivants sont installés :

Installez les packages suivants pour interagir avec Azure OpenAI et Azure SQL.

  • Microsoft.SemanticKernel
  • Microsoft.Data.SqlClient
  1. Cliquez avec le bouton droit sur le dossier Pages figurant sous le dossier Components et ajoutez un nouvel élément nommé OpenAI.razor
  2. Ajoutez le code suivant au fichier OpenAI.razor, puis cliquez sur Enregistrer
@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;
	
	}

Clés API et points de terminaison

L’utilisation de la ressource Azure OpenAI nécessite l’utilisation de clés API et de valeurs de point de terminaison. Consultez l'article Utiliser les références Key Vault en tant que paramètres d’application dans Azure App Service et Azure Functions pour gérer et manipuler vos secrets avec l'Azure OpenAI. Bien que cela ne soit pas nécessaire, nous vous recommandons d’utiliser une identité managée pour sécuriser votre client sans avoir besoin de gérer des clés API. Consultez la documentation précédente pour configurer votre client Azure OpenAI dans la prochaine étape pour qu’il utilise une identité managée avec Azure OpenAI.

2. Ajouter un client Azure OpenAI

Une fois l’interface de conversation ajoutée, nous pouvons configurer le client Azure OpenAI à l’aide d’un noyau sémantique. Ajoutez le code suivant pour créer le client qui se connecte à votre ressource Azure OpenAI. Vous devez utiliser les clés API et les informations de point de terminaison Azure OpenAI que vous avez configurées et traitées à l’étape précédente.

@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;

À partir de là, vous devriez avoir une application de conversation opérationnelle connectée à OpenAI. Ensuite, nous configurons notre base de données Azure SQL pour qu’elle fonctionne avec notre application de conversation.

3. Déployer des modèles Azure OpenAI

Pour préparer votre base de données Azure SQL à une recherche vectorielle, vous devez utiliser un modèle d’incorporation pour générer les incorporations utilisées lors de la recherche en plus de votre modèle de langage déployé initial. Pour cet exemple, nous utilisons les modèles suivants :

  • text-embedding-ada-002 est utilisé pour générer les incorporations
  • gpt-3.5-turbo est utilisé pour le modèle de langage

Ces deux modèles doivent être déployés avant de passer à l’étape suivante. Consultez la documentation relative au déploiement de modèles avec Azure OpenAI à l’aide de Microsoft Foundry.

4. Vectoriser votre base de données SQL

Pour effectuer une recherche vectorielle hybride sur votre base de données Azure SQL, votre base de données doit d’abord contenir les incorporations appropriées. Il existe de nombreuses façons de vectoriser votre base de données. L’une des options consiste à utiliser le vectoriseur de base de données Azure SQL suivant pour générer les incorporations de votre base de données SQL. Vectorisez votre base de données Azure SQL avant de continuer.

5. Créer une procédure pour générer des incorporations

Avec la prise en charge de vecteurs Azure SQL (préversion), vous pouvez créer une procédure stockée qui utilisera un type de données Vecteur pour stocker les incorporations générées pour les requêtes de recherche. La procédure stockée appelle un point de terminaison d’API REST externe pour obtenir les incorporations. Consultez la documentation pour utiliser Azure Data Studio pour vous connecter à votre base de données avant d’exécuter la requête.

Utilisez ce qui suit pour créer une procédure stockée avec votre éditeur de requête SQL préféré. Vous devez remplir le paramètre @url avec le nom de votre ressource Azure OpenAI et remplir le point de terminaison REST avec la clé API de votre modèle d’incorporation de texte. Vous remarquerez le nom du modèle dans @url, qui sera rempli avec votre requête de recherche.

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

Une fois votre procédure stockée créée, vous devez pouvoir l’afficher sous le dossier Procédures stockées qui se trouve dans le dossier Programmabilité de votre base de données SQL. Une fois créé, vous pouvez exécuter un test de recherche de similarité dans votre éditeur de requête SQL à l’aide du nom de votre modèle d’incorporation de texte. Cela utilise votre procédure stockée pour générer des incorporations et utiliser une fonction de distance vectorielle pour calculer la distance vectorielle, puis pour retourner les résultats en fonction de la requête de texte.

6. Se connecter à votre base de données et effectuer une recherche

Maintenant que votre base de données est configurée pour créer des incorporations, nous pouvons nous y connecter dans notre application et configurer la requête de recherche vectorielle hybride.

Ajoutez le code suivant à votre fichier OpenAI.razor et assurez-vous que la chaîne de connexion est mise à jour pour utiliser la chaîne de connexion de votre base de données Azure SQL déployée. Le code utilise un paramètre SQL qui transmet en toute sécurité l’entrée utilisateur de l’application de conversation à la requête.

// 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");

La requête SQL elle-même utilise une recherche hybride qui exécute la procédure stockée configurée précédemment pour créer des incorporations et utilise SQL pour filtrer les résultats souhaités. Dans cet exemple, nous donnons les scores des résultats et classons la sortie pour retenir les meilleurs résultats avant de les utiliser comme contexte de base pour générer une réponse.

Sécuriser vos données avec une identité managée

Azure SQL peut utiliser une identité managée avec Microsoft Entra pour sécuriser votre ressource SQL en configurant une authentification sans mot de passe. Suivez les étapes ci-dessous pour configurer une chaîne de connexion sans mot de passe qui sera utilisée dans votre application.

  1. Accédez à votre ressource Azure SQL Server et cliquez sur Microsoft Entra ID sous Paramètres.
  2. Cliquez ensuite sur +Définir l’administrateur, recherchez et choisissez-vous pour configurer Entra ID, puis cliquez sur Enregistrer. Entra ID est maintenant configuré sur votre serveur SQL et accepte l’authentification Entra ID.
  3. Accédez ensuite à votre ressource de base de données et copiez la chaîne de connexion ADO.NET (authentification sans mot de passe Microsoft Entra), puis ajoutez-la au code dans lequel vous conservez votre chaîne de connexion.

À ce point, vous pouvez tester votre application localement avec votre chaîne de connexion sans mot de passe.

Permettre d’accéder à App Service

Avant de pouvoir effectuer un appel à votre base de données en utilisant une identité managée avec Azure SQL, vous devez d’abord octroyer à la base de données un accès à App Service. Si vous ne l’avez pas déjà fait, vous devez d’abord créer une application web avant d’effectuer les étapes suivantes.

Suivez ces étapes pour permettre d’accéder à votre application web :

  1. Accédez à votre application web et cliquez sur le panneau Identité qui se trouve sous Paramètres.
  2. Activer l’identité managée affectée par le système si vous ne l’avez pas déjà fait.
  3. Accédez à votre ressource de base de données et ouvrez l’éditeur de requête qui se trouve dans le menu de gauche. Vous devrez peut-être vous connecter pour utiliser l’éditeur.
  4. Exécutez les commandes suivantes pour créer un utilisateur et modifier les rôles en leur ajoutant l’application web en tant que membre
-- 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. Ensuite, octroyez l’accès pour utiliser la procédure stockée et le point de terminaison Azure OpenAI
-- 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

À partir de là, votre base de données Azure SQL est sécurisée et vous pouvez déployer votre application sur App Service.

Voici l’exemple complet de la page OpenAI.razor ajoutée :

@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;

	}
}