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

Vous pouvez utiliser vos propres données SQL pour baser le contexte de votre application intelligente. Cet article décrit la création d’une application de génération augmentée de récupération (RAG) .NET Blazor en configurant la recherche de vecteurs hybrides sur une base de données Azure SQL qui a des incorporations vectorisées. La prise en charge des vecteurs Azure SQL fournit de nouvelles fonctions vectorielles qui aident à gérer les données vectorielles.

Prérequis

  • Une base de données Azure SQL avec des données que vous pouvez vectoriser
  • Ressource Azure OpenAI
  • Une application web Blazor .NET 8 ou 9 déployée sur Azure App Service

Ce tutoriel utilise un exemple complémentaire de Clone et déploie une application de conversation Blazor .NET 9 connectée à Azure OpenAI.

1. Configurer l’application web Blazor

Créez une boîte de conversation de base pour interagir avec.

  1. Dans l’environnement de développement, assurez-vous que les packages Microsoft.SemanticKernel et Microsoft.Data.SqlClient sont bien installés.

  2. Dans l'arborescence du projet d'application web, développez Composants>Pages, et créez un nouveau fichier dans Pages nommé OpenAI.razor.

  3. Ajoutez le code de zone de conversation suivant au fichier OpenAI.razor et enregistrez le fichier.

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

2. Configurer le client Azure OpenAI

Après avoir ajouté l’interface de conversation, configurez le client Azure OpenAI à l’aide du noyau sémantique. Le code suivant crée le client qui se connecte à la ressource Azure OpenAI.

Le code référence DEPLOYMENT_NAME, ENDPOINT, API_KEY et MODEL_ID. Veillez à stocker ces valeurs dans appsettings.json ou en tant que variables d’environnement. Pour obtenir des instructions sur l’obtention et la gestion des informations de clé et de point de terminaison Azure OpenAI, consultez Utiliser les références Key Vault en tant que paramètres d’application dans Azure App Service et Azure Functions.

Note

Si possible, vous devez utiliser l’identité managée pour sécuriser votre client sans avoir à gérer les clés API. Consultez le tutoriel : Créer un chatbot avec Azure App Service et Azure OpenAI (.NET) pour obtenir des instructions sur la configuration d’un client Azure OpenAI pour utiliser l’identité managée.

Ajoutez le code suivant au fichier OpenAI.razor .

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

Vous disposez maintenant d’une application de conversation opérationnelle connectée à OpenAI. Ensuite, configurez votre base de données Azure SQL pour qu’elle fonctionne avec votre application de conversation.

3. Déployer des modèles Azure OpenAI

Pour préparer une base de données Azure SQL pour la recherche de vecteurs hybrides, vous utilisez un modèle d’incorporation pour générer des incorporations à utiliser pour la recherche. Une fois les incorporations appropriées dans la base de données, vous pouvez les utiliser avec votre modèle de langage initial pour effectuer une recherche vectorielle hybride sur la base de données.

Cet exemple utilise le text-embedding-ada-002 modèle pour générer des incorporations et gpt-4o-mini pour le modèle de langage. Avant de continuer, utilisez le portail Microsoft Foundry pour déployer les deux modèles dans votre ressource OpenAI. Pour plus d’informations, consultez Déployer des modèles Microsoft Foundry dans le portail Foundry.

4. Vectoriser votre base de données Azure SQL

Pour effectuer une recherche vectorielle hybride sur une base de données Azure SQL, les incorporations appropriées doivent se trouver dans la base de données. Vectorisez votre base de données avant de continuer. Il existe de nombreuses façons de vectoriser une base de données. L’une des options consiste à utiliser le vectoriseur Azure SQLDB.

5. Créer une procédure stockée qui génère des incorporations

Vous pouvez utiliser la prise en charge de vecteurs Azure SQL pour créer une procédure stockée qui utilise un VECTOR type de données pour stocker des 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.

La requête SQL suivante crée la procédure stockée. Remplacez l’espace <resourcename> réservé dans le @url paramètre par le nom de votre ressource Azure OpenAI, puis remplacez l’espace réservé par la <openAIkey> clé API de votre modèle d’incorporation de texte. Le nom du modèle fait partie de @url, qui est rempli avec la requête de recherche.

Vous pouvez utiliser l’Éditeur de requête dans le portail Azure ou Visual Studio Code pour vous connecter à votre base de données et exécuter la requête. Pour plus d’informations sur l’utilisation de Visual Studio Code, consultez la documentation sur l’extension MSSQL.

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

Après la création, cette procédure stockée apparaît sous Procédures stockées dans le dossier Programmability de la base de données Azure SQL. Vous pouvez exécuter une recherche de similarité de test dans l’éditeur de requête SQL à l’aide du nom du modèle d’incorporation de texte. Ce test utilise la procédure stockée pour générer des incorporations, calculer la distance vectorielle à l’aide d’une fonction de distance vectorielle et 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, vous pouvez vous connecter à la base de données dans votre application et configurer la requête de recherche vectorielle hybride. Ajoutez le code suivant au fichier OpenAI.razor , en vous assurant que la chaîne de connexion utilise la chaîne de connexion de base de données Azure SQL déployée.

Le code utilise un paramètre SQL qui passe en toute sécurité l’entrée utilisateur de l’application de conversation à la requête. L’exemple utilise le jeu de données Amazon Fine Food Reviews .

// 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 pour exécuter la procédure stockée qui crée des incorporations et filtre les résultats à l’aide de SQL. Cet exemple affecte des scores aux résultats, trie la sortie pour récupérer les meilleurs résultats et les utilise 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 l’identité managée avec Microsoft Entra pour sécuriser la ressource SQL en configurant l’authentification sans mot de passe. Procédez comme suit pour configurer une chaîne de connexion sans mot de passe à utiliser dans votre application.

  1. Dans la ressource azure SQL Server dans le portail Azure, sélectionnez Paramètres>Microsoft Entra ID dans le menu de navigation de gauche.
  2. Sélectionnez Définir l’administrateur, recherchez votre nom, puis sélectionnez Enregistrer. L'ID Entra est configuré maintenant sur votre serveur SQL et accepte l'authentification Entra ID.
  3. Accédez à votre ressource de base de données, sélectionnezChaîne de connexion > dans le menu de navigation de gauche, puis copiez la chaîne de connexion ADO.NET (authentification sans mot de passe Microsoft Entra).
  4. Mettez à jour la chaîne de connexion dans le appsettings.jsonde votre application.

Vous pouvez maintenant tester votre application localement avec votre chaîne de connexion sans mot de passe.

Accorder l’accès à la base de données à App Service

Avant de pouvoir utiliser votre application web pour appeler la base de données Azure SQL à l’aide de l’identité managée, vous devez accorder à l’application l’accès nécessaire à la base de données.

  1. Dans votre application web dans le portail Azure, sélectionnez Paramètres>Identité dans le menu de navigation de gauche.

  2. Sous l’onglet Affecté par le système, définissez le statut sur Activé s’il n’est pas déjà activé, puis sélectionnez Enregistrer.

  3. Accédez à votre ressource de base de données Azure SQL et sélectionnez l’éditeur de requête dans le menu de navigation de gauche. Connectez-vous à votre base de données si nécessaire.

  4. Dans l’éditeur de requête, exécutez les commandes SQL suivantes qui créent l’application web en tant qu’utilisateur et lui attribuent les appartenances de rôle nécessaires, en remplaçant <your-app-name> par le nom de votre application web.

    -- 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
    
  5. Ensuite, accordez à l’application web 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
    

Votre base de données Azure SQL est désormais sécurisée.

7. Déployer l’application

Vous pouvez maintenant déployer votre application sur Azure App Service. Si vous souhaitez déployer l’application à l’aide d’un azd modèle, consultez Déployer avec Azure Developer CLI.

Exemple complet

Le code suivant montre la page OpenAI.razor ajoutée complète.

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

    }
}