Självstudie: Distribuera en .NET Blazor-app som är ansluten till Azure SQL och Azure OpenAI i Azure App Service

När du skapar intelligenta appar kanske du vill grunda kontexten för din app med dina egna SQL-data. Med det senaste meddelandet om Stöd för Azure SQL-vektor (förhandsversion) kan du jorda kontexten med hjälp av de Azure SQL-data som du redan har med nya vektorfunktioner som hjälper dig att hantera vektordata.

I den här självstudien skapar du ett RAG-exempelprogram genom att konfigurera en Hybridvektorsökning mot din Azure SQL-databas med hjälp av en .NET 8 Blazor-app. Det här exemplet bygger från den tidigare dokumentationen för att distribuera en .NET Blazor-app med OpenAI. Om du vill distribuera appen med hjälp av en azd-mall kan du besöka Lagringsplatsen Azure Samples med distributionsinstruktioner.

Förutsättningar

  • En Azure OpenAI-resurs med distribuerade modeller
  • En .NET 8 eller 9 Blazor Web App som har distribuerats på App Service
  • En Azure SQL-databasresurs med vektorbäddningar.

1. Konfigurera Blazor-webbapp

I det här exemplet skapar vi en enkel chattruta att interagera med. Om du använder den nödvändiga .NET Blazor-appen från föregående artikel kan du hoppa över ändringarna i Filen OpenAI.razor eftersom innehållet är detsamma. Du måste dock se till att följande paket är installerade:

Installera följande paket för att interagera med Azure OpenAI och Azure SQL.

  • Microsoft.SemanticKernel
  • Microsoft.Data.SqlClient
  1. Högerklicka på mappen Pages som finns under mappen Komponenter och lägg till ett nytt objekt med namnet OpenAI.razor
  2. Lägg till följande kod i filen OpenAI.razor och klicka på Spara
@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;
	
	}

API-nycklar och slutpunkter

Användning av Azure OpenAI-resursen kräver användning av API-nycklar och slutpunktsvärden. Se Använda Key Vault-referenser som appinställningar i Azure App Service och Azure Functions för att hantera och hantera dina hemligheter med Azure OpenAI. Även om det inte krävs rekommenderar vi att du använder hanterad identitet för att skydda klienten utan att behöva hantera API-nycklar. Se föregående dokumentation för att konfigurera Din Azure OpenAI-klient i nästa steg för att använda hanterad identitet med Azure OpenAI.

2. Lägg till Azure OpenAI-klient

När du har lagt till chattgränssnittet kan vi konfigurera Azure OpenAI-klienten med hjälp av semantisk kernel. Lägg till följande kod för att skapa klienten som ansluter till din Azure OpenAI-resurs. Du måste använda dina Azure OpenAI API-nycklar och slutpunktsinformation som konfigurerades och hanterades i föregående steg.

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

Härifrån bör du ha ett fungerande chattprogram som är anslutet till OpenAI. Nu ska vi konfigurera vår Azure SQL-databas så att den fungerar med vårt chattprogram.

3. Distribuera Azure OpenAI-modeller

För att förbereda din Azure SQL-databas för vektorsökning måste du använda en inbäddningsmodell för att generera inbäddningar som används för sökning utöver den första distribuerade språkmodellen. I det här exemplet använder vi följande modeller:

  • text-embedding-ada-002 används för att generera inbäddningar
  • gpt-3.5-turbo används för språkmodellen

Dessa två modeller måste distribueras innan du fortsätter med nästa steg. Gå till dokumentationen för att distribuera modeller med Azure OpenAI med Hjälp av Microsoft Foundry.

4. Vektorisera din SQL-databas

Om du vill utföra en hybridvektorsökning i din Azure SQL-databas måste du först ha lämpliga inbäddningar i databasen. Det finns många sätt att vektorisera databasen. Ett alternativ är att använda följande Azure SQL-databasvektoriserare för att generera inbäddningar för din SQL-databas. Vektorisera din Azure SQL-databas innan du fortsätter.

5. Skapa procedur för att generera inbäddningar

Med stöd för Azure SQL-vektor (förhandsversion) kan du skapa en lagrad procedur som använder en vektordatatyp för att lagra genererade inbäddningar för sökfrågor. Den lagrade proceduren anropar en extern REST API-slutpunkt för att hämta inbäddningarna. Se dokumentationen för att använda Azure Data Studio för att ansluta till databasen innan du kör frågan.

Använd följande för att skapa en lagrad procedur med önskad SQL-frågeredigerare. Du måste fylla i parametern @url med ditt Azure OpenAI-resursnamn och fylla i restslutpunkten med API-nyckeln från din textinbäddningsmodell. Du ser modellnamnet som en del av @url, som fylls i med sökfrågan.

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

När du har skapat den lagrade proceduren bör du kunna visa den under mappen Lagrade procedurer som finns i mappen Programmability i SQL-databasen. När du har skapat den kan du köra en testlikhetssökning i SQL-frågeredigeraren med hjälp av ditt modellnamn för textinbäddning. Detta använder din lagrade procedur för att generera inbäddningar och använda en vektoravståndsfunktion för att beräkna vektoravståndet och returnera resultat baserat på textfrågan.

6. Anslut och sök i databasen

Nu när databasen har konfigurerats för att skapa inbäddningar kan vi ansluta till den i vårt program och konfigurera hybridvektorsökningsfrågan.

Lägg till följande kod i OpenAI.razor filen och kontrollera att anslutningssträng har uppdaterats för att använda din distribuerade Azure SQL-databas anslutningssträng. Koden använder en SQL-parameter som på ett säkert sätt passerar genom användarens indata från chattappen till frågan.

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

SJÄLVA SQL-frågan använder en hybridsökning som kör den lagrade procedur som konfigurerades tidigare för att skapa inbäddningar och använder SQL för att filtrera bort önskade resultat. I det här exemplet ger vi poäng till resultaten och sorterar outputen för att hämta de bästa resultaten innan de används som relevant kontext för att generera ett svar.

Skydda dina data med hanterad identitet

Azure SQL kan använda hanterad identitet med Microsoft Entra för att skydda din SQL-resurs genom att konfigurera lösenordslös autentisering. Följ stegen nedan för att konfigurera en lösenordsfri anslutningssträng som ska användas i ditt program.

  1. Gå till din Azure SQL-serverresurs och klicka på Microsoft Entra-ID under Inställningar.
  2. Klicka sedan på +Ange administratör, sök och välj dig själv för att konfigurera Entra ID, och klicka på Spara. Nu har Entra-ID konfigurerats på DIN SQL-server och accepterar Entra-ID-autentisering.
  3. Gå sedan till din databasresurs och kopiera ADO.NET (lösenordslös autentisering från Microsoft Entra)-anslutningssträngen och lägg till den i koden där du har din anslutningssträng.

Nu kan du testa ditt program lokalt med dina lösenordslösa anslutningssträng.

Bevilja åtkomst till App Service

Innan du kan göra ett anrop till databasen när du använder hanterad identitet med Azure SQL måste du först bevilja databasen åtkomst till App Service. Om du inte har gjort det nu måste du först skapa en webbapp innan du slutför nästa steg.

Följ dessa steg för att bevilja åtkomst till din webbapp:

  1. Gå till webbappen och klicka på identitetsbladet som finns under Inställningar.
  2. Aktivera den hanterade identiteten som tilldelats av systemet om du inte redan har gjort det.
  3. Gå till databasresursen och öppna frågeredigeraren som finns på menyn till vänster. Du kan behöva logga in för att använda redigeraren.
  4. Kör följande kommandon för att skapa en användare och ändra rollerna och lägga till webbappen som medlem
-- 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. Bevilja sedan åtkomst för att använda den lagrade proceduren och Azure OpenAI-slutpunkten
-- 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

Härifrån är din Azure SQL-databas nu säker och du kan distribuera ditt program till App Service.

Här är det fullständiga exemplet på den tillagda OpenAI.razor-sidan :

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

	}
}