-
Notifications
You must be signed in to change notification settings - Fork 1.7k
.NET: Hosted Agents - RAG Sample with Azure AI Search (#5693) #5701
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
rogerbarreto
wants to merge
2
commits into
microsoft:main
Choose a base branch
from
rogerbarreto:issues/5693-net-hosted-agents-rag-sample-with-azure-ai-search
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
8 changes: 8 additions & 0 deletions
8
dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AzureSearchRag/.env.example
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| AZURE_AI_PROJECT_ENDPOINT=<your-azure-ai-project-endpoint> | ||
| AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o | ||
| AZURE_SEARCH_ENDPOINT=<your-azure-search-endpoint> | ||
| AZURE_SEARCH_INDEX_NAME=contoso-outdoors | ||
| AZURE_BEARER_TOKEN_FOUNDRY=DefaultAzureCredential | ||
| AZURE_BEARER_TOKEN_SEARCH=DefaultAzureCredential | ||
| ASPNETCORE_URLS=http://+:8088 | ||
| ASPNETCORE_ENVIRONMENT=Development |
17 changes: 17 additions & 0 deletions
17
dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AzureSearchRag/Dockerfile
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| # Use the official .NET 10.0 ASP.NET runtime as a parent image | ||
| FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base | ||
| WORKDIR /app | ||
|
|
||
| FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build | ||
| WORKDIR /src | ||
| COPY . . | ||
| RUN dotnet restore | ||
| RUN dotnet publish -c Release -o /app/publish | ||
|
|
||
| # Final stage | ||
| FROM base AS final | ||
| WORKDIR /app | ||
| COPY --from=build /app/publish . | ||
| EXPOSE 8088 | ||
| ENV ASPNETCORE_URLS=http://+:8088 | ||
| ENTRYPOINT ["dotnet", "HostedAzureSearchRag.dll"] |
23 changes: 23 additions & 0 deletions
23
...les/04-hosting/FoundryHostedAgents/responses/Hosted-AzureSearchRag/Dockerfile.contributor
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| # Dockerfile for contributors building from the agent-framework repository source. | ||
| # | ||
| # This project uses ProjectReference to the local Microsoft.Agents.AI.Foundry source, | ||
| # which means a standard multi-stage Docker build cannot resolve dependencies outside | ||
| # this folder. Instead, pre-publish the app targeting the container runtime and copy | ||
| # the output into the container: | ||
| # | ||
| # dotnet publish -c Debug -f net10.0 -r linux-musl-x64 --self-contained false -o out | ||
| # docker build -f Dockerfile.contributor -t hosted-azure-search-rag . | ||
| # docker run --rm -p 8088:8088 \ | ||
| # -e AGENT_NAME=hosted-azure-search-rag \ | ||
| # -e AZURE_BEARER_TOKEN_FOUNDRY=$AZURE_BEARER_TOKEN_FOUNDRY \ | ||
| # -e AZURE_BEARER_TOKEN_SEARCH=$AZURE_BEARER_TOKEN_SEARCH \ | ||
| # --env-file .env hosted-azure-search-rag | ||
| # | ||
| # For end-users consuming the NuGet package (not ProjectReference), use the standard | ||
| # Dockerfile which performs a full dotnet restore + publish inside the container. | ||
| FROM mcr.microsoft.com/dotnet/aspnet:10.0-alpine AS final | ||
| WORKDIR /app | ||
| COPY out/ . | ||
| EXPOSE 8088 | ||
| ENV ASPNETCORE_URLS=http://+:8088 | ||
| ENTRYPOINT ["dotnet", "HostedAzureSearchRag.dll"] |
35 changes: 35 additions & 0 deletions
35
...4-hosting/FoundryHostedAgents/responses/Hosted-AzureSearchRag/HostedAzureSearchRag.csproj
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| <Project Sdk="Microsoft.NET.Sdk.Web"> | ||
|
|
||
| <PropertyGroup> | ||
| <TargetFrameworks>net10.0</TargetFrameworks> | ||
| <Nullable>enable</Nullable> | ||
| <ImplicitUsings>enable</ImplicitUsings> | ||
| <CentralPackageTransitivePinningEnabled>false</CentralPackageTransitivePinningEnabled> | ||
| <RootNamespace>HostedAzureSearchRag</RootNamespace> | ||
| <AssemblyName>HostedAzureSearchRag</AssemblyName> | ||
| <NoWarn>$(NoWarn);</NoWarn> | ||
| </PropertyGroup> | ||
|
|
||
| <ItemGroup> | ||
| <PackageReference Include="Azure.AI.Projects" VersionOverride="2.1.0-beta.1" /> | ||
| <PackageReference Include="Azure.Identity" /> | ||
| <PackageReference Include="Azure.Search.Documents" /> | ||
| <PackageReference Include="DotNetEnv" /> | ||
| </ItemGroup> | ||
|
|
||
| <!-- For contributors: uses ProjectReference to build against local source --> | ||
| <ItemGroup> | ||
| <ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Foundry\Microsoft.Agents.AI.Foundry.csproj" /> | ||
| <ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Foundry.Hosting\Microsoft.Agents.AI.Foundry.Hosting.csproj" /> | ||
| <ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" /> | ||
| </ItemGroup> | ||
|
|
||
| <!-- For end-users: uncomment the PackageReference below and remove the ProjectReferences above | ||
| <ItemGroup> | ||
| <PackageReference Include="Microsoft.Agents.AI.Foundry" Version="1.0.0" /> | ||
| <PackageReference Include="Microsoft.Agents.AI.Foundry.Hosting" Version="1.0.0" /> | ||
| <PackageReference Include="Microsoft.Agents.AI.OpenAI" Version="1.0.0" /> | ||
| </ItemGroup> | ||
| --> | ||
|
|
||
| </Project> |
171 changes: 171 additions & 0 deletions
171
dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AzureSearchRag/Program.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,171 @@ | ||
| // Copyright (c) Microsoft. All rights reserved. | ||
|
|
||
| // This sample shows how to add Retrieval Augmented Generation (RAG) capabilities to a hosted | ||
| // agent using Azure AI Search. The sample assumes the search index has already been provisioned | ||
| // and populated out of band (see README.md for the required schema and example seed content). | ||
| // A SearchClient-backed adapter is plugged into TextSearchProvider, which runs a keyword search | ||
| // against the index before each model invocation and injects the matching documents into the | ||
| // model context. | ||
|
|
||
| using Azure; | ||
| using Azure.AI.Projects; | ||
| using Azure.Core; | ||
| using Azure.Identity; | ||
| using Azure.Search.Documents; | ||
| using Azure.Search.Documents.Models; | ||
| using DotNetEnv; | ||
| using Microsoft.Agents.AI; | ||
| using Microsoft.Agents.AI.Foundry.Hosting; | ||
| using Microsoft.Extensions.AI; | ||
| using OpenAI.Chat; | ||
|
|
||
| // Load .env file if present (for local development) | ||
| Env.TraversePath().Load(); | ||
|
|
||
| string projectEndpoint = Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT") | ||
| ?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set."); | ||
| string deploymentName = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-4o"; | ||
|
|
||
| string searchEndpoint = Environment.GetEnvironmentVariable("AZURE_SEARCH_ENDPOINT") | ||
| ?? throw new InvalidOperationException("AZURE_SEARCH_ENDPOINT is not set."); | ||
| string searchIndexName = Environment.GetEnvironmentVariable("AZURE_SEARCH_INDEX_NAME") | ||
| ?? throw new InvalidOperationException("AZURE_SEARCH_INDEX_NAME is not set."); | ||
|
|
||
| // Use a chained credential. Try a temporary dev token first (for local Docker debugging), | ||
| // then fall back to DefaultAzureCredential (for local dev via dotnet run / managed identity in | ||
| // production). The dev credential is scope aware so a single instance serves both Foundry and | ||
| // Azure AI Search clients (each Azure SDK client requests a token for its own audience). | ||
| TokenCredential credential = new ChainedTokenCredential( | ||
| new DevTemporaryTokenCredential(), | ||
| new DefaultAzureCredential()); | ||
|
|
||
| // Connect to the pre-provisioned search index. The caller is expected to have created the | ||
| // index and populated it with documents matching the schema (id / content / sourceName / | ||
| // sourceLink) before running this sample. See README.md for an example provisioning script. | ||
| var searchClient = new SearchClient(new Uri(searchEndpoint), searchIndexName, credential); | ||
|
|
||
| TextSearchProviderOptions textSearchOptions = new() | ||
| { | ||
| SearchTime = TextSearchProviderOptions.TextSearchBehavior.BeforeAIInvoke, | ||
| RecentMessageMemoryLimit = 6, | ||
| }; | ||
|
|
||
| AIAgent agent = new AIProjectClient(new Uri(projectEndpoint), credential) | ||
| .AsAIAgent(new ChatClientAgentOptions | ||
| { | ||
| Name = Environment.GetEnvironmentVariable("AGENT_NAME") ?? "hosted-azure-search-rag", | ||
| ChatOptions = new ChatOptions | ||
| { | ||
| ModelId = deploymentName, | ||
| Instructions = "You are a helpful support specialist for Contoso Outdoors. " + | ||
| "Answer questions using the provided context and cite the source document when available.", | ||
| }, | ||
| AIContextProviders = [new TextSearchProvider(CreateSearchAdapter(searchClient), textSearchOptions)] | ||
| }); | ||
|
|
||
| // Host the agent as a Foundry Hosted Agent using the Responses API. | ||
| var builder = WebApplication.CreateBuilder(args); | ||
| builder.Services.AddFoundryResponses(agent); | ||
|
|
||
| var app = builder.Build(); | ||
| app.MapFoundryResponses(); | ||
|
|
||
| if (app.Environment.IsDevelopment()) | ||
| { | ||
| app.MapFoundryResponses("openai/v1"); | ||
| } | ||
|
|
||
| app.Run(); | ||
|
|
||
| // ── Search adapter ─────────────────────────────────────────────────────────── | ||
| // Wraps a SearchClient as the delegate TextSearchProvider expects. Keyword/full-text only; | ||
| // no embeddings. Returns the top results and projects them into TextSearchResult entries | ||
| // the provider will inject into the model context. | ||
|
|
||
| static Func<string, CancellationToken, Task<IEnumerable<TextSearchProvider.TextSearchResult>>> | ||
| CreateSearchAdapter(SearchClient client, int top = 3) => | ||
| async (query, cancellationToken) => | ||
| { | ||
| var options = new SearchOptions { Size = top }; | ||
| Response<SearchResults<SearchDocument>> response = | ||
| await client.SearchAsync<SearchDocument>(query, options, cancellationToken).ConfigureAwait(false); | ||
|
|
||
| var results = new List<TextSearchProvider.TextSearchResult>(); | ||
| await foreach (SearchResult<SearchDocument> hit in response.Value.GetResultsAsync().WithCancellation(cancellationToken).ConfigureAwait(false)) | ||
| { | ||
| results.Add(new TextSearchProvider.TextSearchResult | ||
| { | ||
| SourceName = hit.Document.TryGetValue("sourceName", out var name) ? name?.ToString() ?? string.Empty : string.Empty, | ||
| SourceLink = hit.Document.TryGetValue("sourceLink", out var link) ? link?.ToString() ?? string.Empty : string.Empty, | ||
| Text = hit.Document.TryGetValue("content", out var content) ? content?.ToString() ?? string.Empty : string.Empty, | ||
| RawRepresentation = hit | ||
| }); | ||
| } | ||
|
|
||
| return results; | ||
| }; | ||
|
|
||
| /// <summary> | ||
| /// A scope aware <see cref="TokenCredential"/> for local Docker debugging only. | ||
| /// Reads pre-fetched bearer tokens from environment variables, dispensing the right token | ||
| /// based on the requested scope: | ||
| /// <list type="bullet"> | ||
| /// <item><description><c>ai.azure.com</c> scopes -> <c>AZURE_BEARER_TOKEN_FOUNDRY</c></description></item> | ||
| /// <item><description><c>search.azure.com</c> scopes -> <c>AZURE_BEARER_TOKEN_SEARCH</c></description></item> | ||
| /// </list> | ||
| /// For any other scope, throws <see cref="CredentialUnavailableException"/> so a chained | ||
| /// credential will fall through. This should NOT be used in production: tokens expire (~1 hour) | ||
| /// and cannot be refreshed. | ||
| /// | ||
| /// Generate the tokens on your host and pass them to the container: | ||
| /// <code> | ||
| /// export AZURE_BEARER_TOKEN_FOUNDRY=$(az account get-access-token --resource https://ai.azure.com --query accessToken -o tsv) | ||
| /// export AZURE_BEARER_TOKEN_SEARCH=$(az account get-access-token --resource https://search.azure.com --query accessToken -o tsv) | ||
| /// docker run -e AZURE_BEARER_TOKEN_FOUNDRY -e AZURE_BEARER_TOKEN_SEARCH ... | ||
| /// </code> | ||
| /// </summary> | ||
| internal sealed class DevTemporaryTokenCredential : TokenCredential | ||
| { | ||
| private const string FoundryEnvironmentVariable = "AZURE_BEARER_TOKEN_FOUNDRY"; | ||
| private const string SearchEnvironmentVariable = "AZURE_BEARER_TOKEN_SEARCH"; | ||
|
|
||
| public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken) | ||
| => Resolve(requestContext.Scopes); | ||
|
|
||
| public override ValueTask<AccessToken> GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken) | ||
| => new(Resolve(requestContext.Scopes)); | ||
|
|
||
| private static AccessToken Resolve(IReadOnlyList<string> scopes) | ||
| { | ||
| string? envVar = null; | ||
| foreach (var scope in scopes) | ||
| { | ||
| if (scope.Contains("search.azure.com", StringComparison.OrdinalIgnoreCase)) | ||
| { | ||
| envVar = SearchEnvironmentVariable; | ||
| break; | ||
| } | ||
|
|
||
| if (scope.Contains("ai.azure.com", StringComparison.OrdinalIgnoreCase)) | ||
| { | ||
| envVar = FoundryEnvironmentVariable; | ||
| break; | ||
| } | ||
| } | ||
|
|
||
| if (envVar is null) | ||
| { | ||
| throw new CredentialUnavailableException( | ||
| $"DevTemporaryTokenCredential cannot serve scopes [{string.Join(", ", scopes)}]; falling through."); | ||
| } | ||
|
|
||
| var token = Environment.GetEnvironmentVariable(envVar); | ||
| if (string.IsNullOrEmpty(token) || string.Equals(token, "DefaultAzureCredential", StringComparison.Ordinal)) | ||
| { | ||
| throw new CredentialUnavailableException( | ||
| $"{envVar} environment variable is not set; falling through to next credential."); | ||
| } | ||
|
|
||
| return new AccessToken(token, DateTimeOffset.UtcNow.AddHours(1)); | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.