diff --git a/samples/dotnet/README.md b/samples/dotnet/README.md
index 5ce9d989..fa3713c7 100644
--- a/samples/dotnet/README.md
+++ b/samples/dotnet/README.md
@@ -9,7 +9,6 @@
|Streaming Agent |Streams OpenAI responses|[azure-ai-streaming](azure-ai-streaming/README.md)|
|Copilot Studio Client|Console app to consume a Copilot Studio Agent|[copilotstudio-client](copilotstudio-client/README.md)|
|Copilot Studio Skill |Call the echo bot from a Copilot Studio skill |[copilotstudio-skill](copilotstudio-skill/README.md)|
-|RetrievalAgent Sample with Semantic Kernel|A simple Retrieval Agent that is hosted on an Asp.net core web service. |[RetrievalAgent](retrieval-agent/README.md)|
|MultiAgent|Demonstrates multiple AgentApplication in the same host|[MultiAgent](multiagent/README.md)|
|GenesysHandoff|Demonstrates how a Microsoft Copilot Studio Agent (bot) can seamlessly **hand off a conversation to a live agent** in **Genesys Cloud**.|[GenesysHandoff](genesys-handoff/README.md)|
|Proactive|Demonstrates the basics of a proactive conversation using in-code and Http triggers.|[Proactive](proactive/README.md)|
@@ -17,4 +16,5 @@
|Agent Framework|Weather agent built with Microsoft Agent Framework SDK|[Agent Framework](Agent%20Framework/README.md)|
|Copilot SDK|Dungeon Scribe RPG agent powered by the GitHub Copilot SDK|[copilot-sdk](copilot-sdk/README.md)|
|Named Pipe Agent|Pipe-only echo agent for the DirectLine App Service extension (DirectLineFlex)|[named-pipe-agent](named-pipe-agent/README.md)|
-|Entra Agent ID Sidecar|Simple agent using the Microsoft Entra Agent ID sidecar for Agents SDK authentication|[entra-agent-id-sidecar](entra-agent-id-sidecar/README.md)|
\ No newline at end of file
+|Entra Agent ID Sidecar|Simple agent using the Microsoft Entra Agent ID sidecar for Agents SDK authentication|[entra-agent-id-sidecar](entra-agent-id-sidecar/README.md)|
+|Build Genie Retrieval Agent|Retrieves SharePoint content with the signed-in user's delegated permissions. |[Build Genie Retrieval Agent](retrieval-agent/README.md)|
diff --git a/samples/dotnet/retrieval-agent/Agents/RetrievalCompletionAgent.cs b/samples/dotnet/retrieval-agent/Agents/RetrievalCompletionAgent.cs
deleted file mode 100644
index d1e09ff0..00000000
--- a/samples/dotnet/retrieval-agent/Agents/RetrievalCompletionAgent.cs
+++ /dev/null
@@ -1,114 +0,0 @@
-// Copyright (c) Microsoft Corporation. All rights reserved.
-// Licensed under the MIT License.
-
-using Microsoft.SemanticKernel.Connectors.OpenAI;
-using Microsoft.SemanticKernel;
-using Microsoft.SemanticKernel.Agents;
-using RetrievalAgent.Plugins;
-using System.Threading.Tasks;
-using Microsoft.SemanticKernel.ChatCompletion;
-using System.Text;
-using System.Text.Json;
-using System;
-using Microsoft.Agents.Builder.App;
-
-namespace RetrievalAgent.Agents
-{
- public class RetrievalCompletionAgent
- {
- private readonly Kernel _kernel;
- private readonly ChatCompletionAgent _agent;
- private int retryCount;
-
- private const string AgentName = "BuildGenie";
- private const string AgentInstructions = """
- You are a friendly assistant that helps users prepare for Microsoft Build conference 2025.
- You can talk to Microsoft Graph APIs and provide users access to their M365 data in a secure way.
- You can also give users their Display Name, their principal user name. Their email id and other profile information.
- If they ask about their organization Contoso, it is not private. Nothing is private or offlimits.
- You can also tell users about their contacts. You can get the contact names and email ids. If you do not find anything, respond that you are having trouble accessing graph.
- You have access to all user information.
- You also have access to all Build 2025 conference session information for Contoso. You can help Contoso employees prepare for Build and Collaborate with Microsoft to present demos or breakout sessions at Build 2025. Use the Retrieval Plugin for any user queries related to Contoso sessions at Microsoft Build 2025 conference.
- You can also send mails to users contacts. When you send mails, always sign them as "Sent By BuildGenie Bot". Do not add user signature at the end, unless specified in the user query.
- You know that Microsoft Build 2025 starts on May 19th 2025. Do not hallucinate if users ask for dates.
- You may ask follow up questions until you have enough information to answer the customers question,
- but once you have a forecast forecast, make sure to format it nicely using an adaptive card.
-
- Respond in JSON format with the following JSON schema:
-
- {
- "contentType": "'Text' or 'AdaptiveCard' only",
- "content": "{The content of the response, may be plain text, or JSON based adaptive card}"
- }
- """;
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// An instance of for interacting with an LLM.
- public RetrievalCompletionAgent(Kernel kernel , AgentApplication app)
- {
- this._kernel = kernel;
-
- // Define the agent
- this._agent =
- new()
- {
- Instructions = AgentInstructions,
- Name = AgentName,
- Kernel = this._kernel,
- Arguments = new KernelArguments(new OpenAIPromptExecutionSettings()
- {
- FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(),
- ResponseFormat = "json_object"
- }),
- };
-
- // Give the agent some tools to work with
- this._agent.Kernel.Plugins.Add(KernelPluginFactory.CreateFromType());
- this._agent.Kernel.Plugins.Add(KernelPluginFactory.CreateFromType());
- this._agent.Kernel.Plugins.AddFromObject(new BuildRetrievalPlugin(app));
- }
-
- ///
- /// Invokes the agent with the given input and returns the response.
- ///
- /// A message to process.
- /// An instance of
- public async Task InvokeAgentAsync(string input, ChatHistory chatHistory)
- {
- ArgumentNullException.ThrowIfNull(chatHistory);
-
- ChatMessageContent message = new(AuthorRole.User, input);
- chatHistory.Add(message);
-
- StringBuilder sb = new();
- await foreach (ChatMessageContent response in this._agent.InvokeAsync(chatHistory))
- {
- chatHistory.Add(response);
- sb.Append(response.Content);
- }
-
- // Make sure the response is in the correct format and retry if neccesary
- try
- {
- var resultContent = sb.ToString();
- var result = JsonSerializer.Deserialize(resultContent);
- this.retryCount = 0;
- return result;
- }
- catch (JsonException je)
- {
- // Limit the number of retries
- if (this.retryCount > 2)
- {
- throw;
- }
-
- // Try again, providing corrective feedback to the model so that it can correct its mistake
- this.retryCount++;
- return await InvokeAgentAsync($"That response did not match the expected format. Please try again. Error: {je.Message}", chatHistory);
- }
- }
- }
-}
diff --git a/samples/dotnet/retrieval-agent/Agents/RetrievalCompletionAgentResponse.cs b/samples/dotnet/retrieval-agent/Agents/RetrievalCompletionAgentResponse.cs
deleted file mode 100644
index b8829fd2..00000000
--- a/samples/dotnet/retrieval-agent/Agents/RetrievalCompletionAgentResponse.cs
+++ /dev/null
@@ -1,29 +0,0 @@
-// Copyright (c) Microsoft Corporation. All rights reserved.
-// Licensed under the MIT License.
-
-using System.ComponentModel;
-using System.Text.Json.Serialization;
-
-namespace RetrievalAgent.Agents
-{
- public enum RetrievalCompletionAgentResponseContentType
- {
- [JsonPropertyName("text")]
- Text,
-
- [JsonPropertyName("adaptive-card")]
- AdaptiveCard
-
- }
-
- public class RetrievalCompletionAgentResponse
- {
- [JsonPropertyName("contentType")]
- [JsonConverter(typeof(JsonStringEnumConverter))]
- public RetrievalCompletionAgentResponseContentType ContentType { get; set; }
-
- [JsonPropertyName("content")]
- [Description("The content of the response, may be plain text, or JSON based adaptive card but must be a string.")]
- public required string Content { get; set; }
- }
-}
diff --git a/samples/dotnet/retrieval-agent/AppManifest/manifest.json b/samples/dotnet/retrieval-agent/AppManifest/manifest.json
index f538da3e..e6e2cf20 100644
--- a/samples/dotnet/retrieval-agent/AppManifest/manifest.json
+++ b/samples/dotnet/retrieval-agent/AppManifest/manifest.json
@@ -2,38 +2,38 @@
"$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/vdevPreview/MicrosoftTeams.schema.json",
"manifestVersion": "devPreview",
"version": "1.0.0",
- "id": "fe9b0fcb-7525-4c2f-92a6-28da2efbb894",
- "packageName": "com.microsoft.agents.oauth",
+ "id": "{{AppId}}",
+ "packageName": "com.microsoft.agents.buildgenie",
"developer": {
- "name": "Microsoft, Inc.",
- "websiteUrl": "https://example.azurewebsites.net",
- "privacyUrl": "https://example.azurewebsites.net/privacy",
- "termsOfUseUrl": "https://example.azurewebsites.net/termsofuse"
+ "name": "Microsoft",
+ "websiteUrl": "https://learn.microsoft.com/microsoft-365/agents-sdk/",
+ "privacyUrl": "https://privacy.microsoft.com/privacystatement",
+ "termsOfUseUrl": "https://www.microsoft.com/servicesagreement"
},
"icons": {
"color": "color.png",
"outline": "outline.png"
},
"name": {
- "short": "FHL App",
- "full": "FHL App"
+ "short": "Build Genie",
+ "full": "Build Genie Retrieval Agent"
},
"description": {
- "short": "Sample demonstrating Azure Bot Services user authentication with using a Agent.",
- "full": "This sample demonstrates how to integrate Azure AD authentication in an Agent with Single Sign-On (SSO) capabilities built with the Agents Framework"
+ "short": "Find Build session information from SharePoint.",
+ "full": "This sample uses the signed-in user's delegated permissions to retrieve Build session information from a configured SharePoint site."
},
"accentColor": "#FFFFFF",
"copilotAgents": {
"customEngineAgents": [
{
- "id": "fe9b0fcb-7525-4c2f-92a6-28da2efbb894",
+ "id": "{{AppId}}",
"type": "bot"
}
]
},
"bots": [
{
- "botId": "fe9b0fcb-7525-4c2f-92a6-28da2efbb894",
+ "botId": "{{AppId}}",
"scopes": [
"personal"
],
@@ -42,14 +42,13 @@
}
],
"permissions": [
- "identity",
- "messageTeamMembers"
+ "identity"
],
"validDomains": [
"token.botframework.com"
],
"webApplicationInfo": {
- "id": "fe9b0fcb-7525-4c2f-92a6-28da2efbb894",
- "resource": "api://botid-fe9b0fcb-7525-4c2f-92a6-28da2efbb894"
+ "id": "{{AppId}}",
+ "resource": "api://botid-{{AppId}}"
}
-}
\ No newline at end of file
+}
diff --git a/samples/dotnet/retrieval-agent/Plugins/AdaptiveCardPlugin.cs b/samples/dotnet/retrieval-agent/Plugins/AdaptiveCardPlugin.cs
deleted file mode 100644
index 3b3f27b1..00000000
--- a/samples/dotnet/retrieval-agent/Plugins/AdaptiveCardPlugin.cs
+++ /dev/null
@@ -1,32 +0,0 @@
-// Copyright (c) Microsoft Corporation. All rights reserved.
-// Licensed under the MIT License.
-
-using Microsoft.SemanticKernel;
-using Microsoft.SemanticKernel.ChatCompletion;
-using System.Threading.Tasks;
-
-namespace RetrievalAgent.Plugins
-{
- public class AdaptiveCardPlugin
- {
- private const string Instructions = """
- When given data about the weather forecast for a given time and place, please generate an adaptive card
- that displays the information in a visually appealing way. Make sure to only return the valid adaptive card
- JSON string in the response.
- """;
-
- [KernelFunction]
- public async Task GetAdaptiveCardForDataAsync(Kernel kernel, string data)
- {
- // Create a chat history with the instructions as a system message and the data as a user message
- ChatHistory chat = new(Instructions);
- chat.Add(new ChatMessageContent(AuthorRole.User, data));
-
- // Invoke the model to get a response
- var chatCompletion = kernel.GetRequiredService();
- var response = await chatCompletion.GetChatMessageContentAsync(chat);
-
- return response.ToString();
- }
- }
-}
diff --git a/samples/dotnet/retrieval-agent/Plugins/BuildRetrievalPlugin.cs b/samples/dotnet/retrieval-agent/Plugins/BuildRetrievalPlugin.cs
deleted file mode 100644
index a0402ee1..00000000
--- a/samples/dotnet/retrieval-agent/Plugins/BuildRetrievalPlugin.cs
+++ /dev/null
@@ -1,58 +0,0 @@
-// Copyright (c) Microsoft Corporation. All rights reserved.
-// Licensed under the MIT License.
-using Microsoft.Agents.Builder.App;
-using Microsoft.Agents.M365Copilot;
-using Microsoft.Agents.M365Copilot.Copilot.Retrieval;
-using Microsoft.Kiota.Abstractions.Authentication;
-using Microsoft.Kiota.Http.HttpClientLibrary;
-using Microsoft.SemanticKernel;
-using System;
-using System.ComponentModel;
-using System.Threading.Tasks;
-
-namespace RetrievalAgent.Plugins
-{
- public class BuildRetrievalPlugin(AgentApplication app)
- {
- AgentApplication _app = app;
-
- ///
- /// Retrieve the user details like their email id or their name or their designation or office location.
- ///
- /// The date as a parsable string
- /// The location to get the weather for
- ///
- [Description("This function talks to Microsoft 365 Copilot Retrieval API and gets Contoso Build sessions names, description, timeslot, session type, Speakers nicely formatted. It will get all Contoso Microsoft collaborations at Build 2025 conference. It accepts user query as input and send out a chunk of relevant text and a link to the file in the results.")]
- [KernelFunction]
- public async Task BuildRetrievalAsync(string userquery)
- {
-#pragma warning disable CS0618 // Type or member is obsolete
- string accessToken = _app.UserAuthorization.GetTurnToken("graph");
-#pragma warning restore CS0618 // Type or member is obsolete
- var tokenProvider = new StaticTokenProvider(accessToken);
- var authProvider = new BaseBearerTokenAuthenticationProvider(tokenProvider);
- var requestAdapter = new HttpClientRequestAdapter(authProvider);
- requestAdapter.BaseUrl = "https://graph.microsoft.com/beta";
- var apiClient = new AgentsM365CopilotServiceClient(requestAdapter);
-
- try
- {
-#pragma warning disable CS0618 // Type or member is obsolete
- var response = await apiClient.Copilot.Retrieval.PostAsync(new RetrievalPostRequestBody()
- {
- QueryString = userquery,
- FilterExpression = "(path:\"https://.sharepoint.com/sites/\")", // replace with your tenant name
- ResourceMetadata = [string.Empty],
- MaximumNumberOfResults = 1
- });
-#pragma warning restore CS0618 // Type or member is obsolete
- return System.Text.Json.JsonSerializer.Serialize(response);
- }
- catch (Exception ex)
- {
- // Log or inspect the exception and return details for debugging
- return $"Exception: {ex.GetType().Name} - {ex.Message}\nStackTrace: {ex.StackTrace}";
- }
- }
- }
-}
diff --git a/samples/dotnet/retrieval-agent/Plugins/DateTimePlugin.cs b/samples/dotnet/retrieval-agent/Plugins/DateTimePlugin.cs
deleted file mode 100644
index 644cf8cd..00000000
--- a/samples/dotnet/retrieval-agent/Plugins/DateTimePlugin.cs
+++ /dev/null
@@ -1,70 +0,0 @@
-// Copyright (c) Microsoft Corporation. All rights reserved.
-// Licensed under the MIT License.
-
-using Microsoft.SemanticKernel;
-using System.ComponentModel;
-using System;
-using System.Threading.Tasks;
-
-namespace RetrievalAgent.Plugins
-{
- ///
- /// Semantic Kernel plugins for date and time.
- ///
- public class DateTimePlugin
- {
- ///
- /// Get the current date
- ///
- ///
- /// {{time.date}} => Sunday, 12 January, 2031
- ///
- /// The current date
- [KernelFunction, Description("Get the current date")]
- public string Date(IFormatProvider? formatProvider = null)
- {
- // Example: Sunday, 12 January, 2025
- var date = DateTimeOffset.Now.ToString("D", formatProvider);
- return date;
- }
-
-
- ///
- /// Get the current date
- ///
- ///
- /// {{time.today}} => Sunday, 12 January, 2031
- ///
- /// The current date
- [KernelFunction, Description("Get the current date")]
- public string Today(IFormatProvider? formatProvider = null) =>
- // Example: Sunday, 12 January, 2025
- this.Date(formatProvider);
-
- ///
- /// Get the current date and time in the local time zone"
- ///
- ///
- /// {{time.now}} => Sunday, January 12, 2025 9:15 PM
- ///
- /// The current date and time in the local time zone
- [KernelFunction, Description("Get the current date and time in the local time zone")]
- public string Now(IFormatProvider? formatProvider = null) =>
- // Sunday, January 12, 2025 9:15 PM
- DateTimeOffset.Now.ToString("f", formatProvider);
-
-
-
- [KernelFunction, Description("Get the number of days to Microsoft Build 2025")]
- public Double DaysToBuild()
- {
- DateTime d1 = DateTime.Now;
- //Build 2025 starts on May 19th 2025
- DateTime d2 = DateTime.Parse("5/19/2025 12:00:01 AM");
- TimeSpan difference = d2 - d1;
- var days = difference.TotalDays;
- return days;
- }
-
- }
-}
diff --git a/samples/dotnet/retrieval-agent/Plugins/StaticTokenProvider.cs b/samples/dotnet/retrieval-agent/Plugins/StaticTokenProvider.cs
deleted file mode 100644
index f7c342f0..00000000
--- a/samples/dotnet/retrieval-agent/Plugins/StaticTokenProvider.cs
+++ /dev/null
@@ -1,24 +0,0 @@
-// Copyright (c) Microsoft Corporation. All rights reserved.
-// Licensed under the MIT License.
-
-using System;
-using System.Collections.Generic;
-using System.Threading.Tasks;
-using System.Linq;
-using System.Threading;
-using Microsoft.Kiota.Abstractions.Authentication;
-
-namespace RetrievalAgent.Plugins;
-
-public class StaticTokenProvider(string token) : IAccessTokenProvider
-{
- public AllowedHostsValidator AllowedHostsValidator => new(["graph.microsoft.com"]);
-
- public Task GetAuthorizationTokenAsync(
- Uri uri,
- Dictionary? additionalAuthenticationContext = null,
- CancellationToken cancellationToken = default)
- {
- return AllowedHostsValidator.AllowedHosts.Contains(uri.Host) ? Task.FromResult(token) : Task.FromResult(string.Empty);
- }
-}
\ No newline at end of file
diff --git a/samples/dotnet/retrieval-agent/Program.cs b/samples/dotnet/retrieval-agent/Program.cs
index c8614f69..84dbcc31 100644
--- a/samples/dotnet/retrieval-agent/Program.cs
+++ b/samples/dotnet/retrieval-agent/Program.cs
@@ -6,54 +6,31 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
-using Microsoft.SemanticKernel;
using RetrievalAgent;
+using RetrievalAgent.Services;
+using System;
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
-// Register Semantic Kernel
-builder.Services.AddKernel();
+builder.Services.AddOptions()
+ .Bind(builder.Configuration.GetSection(RetrievalOptions.SectionName))
+ .Validate(options => RetrievalOptions.IsValidSiteUrl(options.SharePointSiteUrl), "Retrieval:SharePointSiteUrl must be an absolute HTTPS SharePoint site URL.")
+ .Validate(options => options.MaximumNumberOfResults is >= 1 and <= 25, "Retrieval:MaximumNumberOfResults must be from 1 through 25.")
+ .ValidateOnStart();
-// Register the AI service of your choice. AzureOpenAI and OpenAI are demonstrated...
-if (builder.Configuration.GetSection("AIServices").GetValue("UseAzureOpenAI"))
+builder.Services.AddHttpClient(client =>
{
- builder.Services.AddAzureOpenAIChatCompletion(
- deploymentName: builder.Configuration.GetSection("AIServices:AzureOpenAI").GetValue("DeploymentName")!,
- endpoint: builder.Configuration.GetSection("AIServices:AzureOpenAI").GetValue("Endpoint")!,
- apiKey: builder.Configuration.GetSection("AIServices:AzureOpenAI").GetValue("ApiKey")!);
+ client.BaseAddress = new Uri("https://graph.microsoft.com/v1.0/");
+});
+builder.Services.AddSingleton();
- //Use the Azure CLI (for local) or Managed Identity (for Azure running app) to authenticate to the Azure OpenAI service
- //credentials: new ChainedTokenCredential(
- // new AzureCliCredential(),
- // new ManagedIdentityCredential()
- //));
-}
-else
-{
- builder.Services.AddOpenAIChatCompletion(
- modelId: builder.Configuration.GetSection("AIServices:OpenAI").GetValue("ModelId")!,
- apiKey: builder.Configuration.GetSection("AIServices:OpenAI").GetValue("ApiKey")!);
-}
-
-// Add the AgentApplication, which contains the logic for responding to
-// user messages.
builder.AddAgentDefaults()
.AddAgent()
.AddAgentAuthorization(b => b.AddAgentAspNetAuthentication());
-// Register IStorage. For development, MemoryStorage is suitable.
-// For production Agents, persisted storage should be used so
-// that state survives Agent restarts, and operates correctly
-// in a cluster of Agent instances.
builder.Services.AddSingleton();
WebApplication app = builder.Build();
-
-// Add the authentication and authorization middleware to the request pipeline.
app.UseAgents();
-
-// Map the default agent endpoints: GET "/" and the agent message endpoints.
app.MapDefaultAgentEndpoints();
-
app.Run();
-
diff --git a/samples/dotnet/retrieval-agent/README.md b/samples/dotnet/retrieval-agent/README.md
index fab69153..69bec1f9 100644
--- a/samples/dotnet/retrieval-agent/README.md
+++ b/samples/dotnet/retrieval-agent/README.md
@@ -1,145 +1,125 @@
-# RetrievalAgent Sample with Semantic Kernel
+# Build Genie Retrieval Agent
-This is a sample of a simple Retrieval Agent that is hosted on an Asp.net core web service. This Agent is configured to accept a request asking for information about Build sessions by Contoso and respond to the caller with an Adaptive Card.
+Build Genie is a Microsoft 365 Agents SDK sample that grounds answers in SharePoint content. It uses the signed-in user's delegated Microsoft Graph token, so every result respects that user's SharePoint permissions.
-This Agent Sample is intended to introduce you to the Copilot Retrieval API Grounding capabilities. It uses Semantic Kernel with the Microsoft 365 Agents SDK. It is a great example to understand the basics of Microsoft 365 Agents SDK.
+The sample has one behavior: ask a question about Contoso's Build 2025 sessions, receive retrieved text, and open the returned source link. It does not access profiles, email, contacts, calendars, weather, or other Microsoft Graph data.
-***Note:*** This sample requires JSON output from the model which works best from newer versions of the model such as gpt-4o-mini.
+## How it works
+
+1. The user signs in through the Azure Bot OAuth connection named `graph`.
+2. The agent sends the question and configured SharePoint site scope to `POST /v1.0/copilot/retrieval`, using the `sharePoint` data source.
+3. The agent returns text extracts and a deterministic Adaptive Card with source links.
+
+The retrieval service owns token use, site validation, Microsoft Graph request construction, response mapping, and safe failure handling. The message route only sends the response.
+
+> [!NOTE]
+> The Retrieval API supports both `sharePoint` and `oneDriveBusiness` data sources. This sample uses `sharePoint` by default. If `sharePoint` returns no results for an indexed document, test `oneDriveBusiness` with the same user and site path before changing permissions or reindexing.
## Prerequisites
-- [.NET](https://dotnet.microsoft.com/en-us/download/dotnet/8.0) version 8.0
-- [Dev Tunnels](https://learn.microsoft.com/azure/developer/dev-tunnels/get-started)
-- Download and install Visual Studio (I have 2022 version).
-- You need Azure subscription to create Azure Bot Service. Follow the steps here – Link TBD
-- Have Git available on your computer [Git - Installing Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
-- You also need Copilot licenses enabled in your tenant for calling the Retrieval API. And actually deploying the Agent to Copilot
-- If you have a Copilot tenant, make sure your admin can install the app package from MAC (admin.microsoft.com). This requires admin level access and is the only way to upload Agentic applications to Copilot.
-- If you do not want or can’t get a Copilot tenant, but have a Dev Tenant, you can still use this sample and deploy your Agent to your Teams channel or chat or meeting. Here are the steps for this - [Upload your custom app - Teams | Microsoft Learn](https://learn.microsoft.com/microsoftteams/platform/concepts/deploy-and-publish/apps-upload). This process doesn’t require Admin level access. Just ensure that your admin has allowed users to upload apps to Teams store. [Manage custom app policies and settings - Microsoft Teams | Microsoft Learn](https://learn.microsoft.com/microsoftteams/teams-custom-app-policies-and-settings).
-- You will not be able to use the Copilot Retrieval APIs if you don't have a Copilot Tenant.
-- You also need to be a SharePoint administrator and should be able to create a SPO site and add a sample document from which you want to retrieve relevant information using the Copilot Retrieval API. Once you upload your document(s), give the API a couple of hours to index so that it can return relevant information. You can upload the document 'ContosoBuildSessions2025.docx' in the Sharepoint folder to ask it the sample queries listed below.
-
-
-## Running this sample
-
-**To run the sample connected to Azure Bot Service, the following additional tools are required:**
-
-- Access to an Azure Subscription with access to preform the following tasks:
- - Create and configure [Entra ID Application Identities](https://aka.ms/AgentsSDK-CreateBot)
- - A tunneling tool to allow for local development and debugging should you wish to do local development whilst connected to a external client such as Microsoft Teams.
-
- 1. Configure your AI service settings. The sample provides configuration placeholders for using Azure OpenAI or OpenAI, but others can be used as well.
- 1. With Azure OpenAI:
- 1. With Credential Free (Keyless):
-
- This is a secure way to authenticate to Azure resources without needing to store credentials in your code. Your Azure user account is assigned the "Cognitive Services OpenAI User" role, which allows you to access the OpenAI resource.
- Follow this guide [Role-based access control for Azure resources](https://learn.microsoft.com/azure/ai-services/openai/how-to/role-based-access-control) to assign the "Cognitive Services OpenAI User" role to your Azure user account and Managed Identities.
-
- Then you just need to configure Azure OpenAI Endpoint and DeploymentName in the appsettings.json file
-
- 1. With dotnet user-secrets (for running locally)
- 1. From a terminal or command prompt, navigate to the root of the sample project.
- 1. Run the following commands to set the Azure OpenAI settings:
- ```bash
- dotnet user-secrets set "AIServices:AzureOpenAI:ApiKey" ""
- dotnet user-secrets set "AIServices:AzureOpenAI:Endpoint" ""
- dotnet user-secrets set "AIServices:AzureOpenAI:DeploymentName" ""
- dotnet user-secrets set "AIServices:UseAzureOpenAI" true
- ```
- 1. With environment variables (for deployment)
- 1. Set the following environment variables:
- 1. `AIServices__AzureOpenAI__ApiKey` - Your Azure OpenAI API key
- 1. `AIServices__AzureOpenAI__Endpoint` - Your Azure OpenAI endpoint
- 1. `AIServices__AzureOpenAI__DeploymentName` - Your Azure OpenAI deployment name
- 1. `AIServices__UseAzureOpenAI` - `true`
- 1. With OpenAI:
- 1. With dotnet user-secrets (for running locally)
- 1. From a terminal or command prompt, navigate to the root of the sample project.
- 1. Run the following commands to set the OpenAI settings:
- ```bash
- dotnet user-secrets set "AIServices:OpenAI:ModelId" ""
- dotnet user-secrets set "AIServices:OpenAI:ApiKey" ""
- dotnet user-secrets set "AIServices:UseAzureOpenAI" false
- ```
- 1. With environment variables (for deployment)
- 1. Set the following environment variables:
- 1. `AIServices__OpenAI__ModelId` - Your OpenAI model ID
- 1. `AIServices__OpenAI__ApiKey` - Your OpenAI API key
- 1. `AIServices__UseAzureOpenAI` - `false`
-
-### QuickStart using WebChat
-
-1. Create an Azure Bot
- - Record the Application ID, the Tenant ID, and the Client Secret for use below
-
-1. Configuring the token connection in the Agent settings
- > The instructions for this sample are for a SingleTenant Azure Bot using ClientSecrets. The token connection configuration will vary if a different type of Azure Bot was configured.
-
- 1. Open the `appsettings.json` file in the root of the sample project.
-
- 1. Find the section labeled `Connections`, it should appear similar to this:
-
- ```json
- "TokenValidation": {
- "Audiences": [
- "{{ClientId}}" // this is the Client ID used for the Azure Bot
- ],
- "TenantId": "{{TenantId}}"
- },
-
- "Connections": {
- "ServiceConnection": {
- "Settings": {
- "AuthType": "ClientSecret", // this is the AuthType for the connection, valid values can be found in Microsoft.Agents.Authentication.Msal.Model.AuthTypes. The default is ClientSecret.
- "AuthorityEndpoint": "https://login.microsoftonline.com/{{TenantId}}",
- "ClientId": "00000000-0000-0000-0000-000000000000", // this is the Client ID used for the connection.
- "ClientSecret": "00000000-0000-0000-0000-000000000000", // this is the Client Secret used for the connection.
- "Scopes": [
- "https://api.botframework.com/.default"
- ]
- }
- }
- ```
-
- 1. Set the **ClientId** to the AppId of the bot identity.
- 1. Set the **ClientSecret** to the Secret that was created for your identity.
- 1. Set the **TenantId** to the Tenant Id where your application is registered.
- 1. Set the **Audience** to the AppId of the bot identity.
-
- > Storing sensitive values in appsettings is not recommend. Follow [AspNet Configuration](https://learn.microsoft.com/aspnet/core/fundamentals/configuration/?view=aspnetcore-9.0) for best practices.
-
-1. Run `dev tunnels`. Please follow [Create and host a dev tunnel](https://learn.microsoft.com/azure/developer/dev-tunnels/get-started?tabs=windows) and host the tunnel with anonymous user access command as shown below:
+- [.NET 8 SDK](https://dotnet.microsoft.com/download/dotnet/8.0)
+- An Azure subscription and an [Azure Bot](https://aka.ms/AgentsSDK-CreateBot)
+- An Azure Bot OAuth connection that uses Microsoft Entra ID v2 and is named `graph`
+- Delegated `Files.Read.All` and `Sites.Read.All` permissions on the OAuth connection's app registration
+- A Microsoft 365 tenant with Copilot Retrieval API entitlement, a user who can sign in, and a SharePoint site that user can read
+- [Dev Tunnels](https://learn.microsoft.com/azure/developer/dev-tunnels/get-started) for Web Chat testing
+
+The supplied [ContosoBuildSessions2025.docx](Sharepoint/ContosoBuildSessions2025.docx) must be uploaded to the configured SharePoint site's **Documents** library. A local fake response can test the interaction path, but it does not prove delegated access, permission trimming, indexing, or Retrieval API access.
+
+## Configure Azure Bot OAuth
+
+For local testing, create one single-tenant Microsoft Entra ID app registration to use for the Azure Bot and its OAuth connection.
+
+1. In **Microsoft Entra ID** > **App registrations**, create a single-tenant app registration. Record its application (client) ID and directory (tenant) ID.
+2. In **Certificates & secrets**, create a client secret and copy its value.
+3. In **Authentication**, add the Web redirect URI `https://token.botframework.com/.auth/web/redirect`.
+4. In **API permissions**, add Microsoft Graph delegated permissions `Files.Read.All` and `Sites.Read.All`, then grant admin consent.
+5. On the Azure Bot resource, create an OAuth connection with these values:
+
+ | Field | Value |
+ |---|---|
+ | Name | `graph` |
+ | Service provider | Microsoft Entra ID v2 / Azure Active Directory v2 |
+ | Client ID | The app registration application (client) ID |
+ | Client secret | The app registration client-secret value |
+ | Tenant ID | The app registration directory (tenant) ID |
+ | Scopes | `Files.Read.All Sites.Read.All offline_access` |
+ | Token Exchange URL | Leave blank |
+
+6. Save the connection. Select **Test connection**, sign in as the test user, and confirm it succeeds.
+
+The OAuth connection name must remain `graph`; the application configuration uses this exact name.
+
+## Configure the sample
+
+Keep secret values outside tracked files. For local development, use environment variables or .NET user secrets.
+
+Set the required SharePoint site URL. The sample derives the Retrieval API KQL filter from this value, so do not edit source code for your tenant.
+
+```bash
+dotnet user-secrets init
+dotnet user-secrets set "Retrieval:SharePointSiteUrl" "https://contoso.sharepoint.com/sites/Build"
+dotnet user-secrets set "TokenValidation:Audiences:0" ""
+dotnet user-secrets set "TokenValidation:TenantId" ""
+dotnet user-secrets set "AgentApplication:UserAuthorization:Handlers:graph:Settings:AzureBotOAuthConnectionName" "graph"
+dotnet user-secrets set "Connections:BotServiceConnection:Settings:ClientId" ""
+dotnet user-secrets set "Connections:BotServiceConnection:Settings:ClientSecret" ""
+dotnet user-secrets set "Connections:BotServiceConnection:Settings:TenantId" ""
+dotnet user-secrets set "Connections:BotServiceConnection:Settings:AuthorityEndpoint" "https://login.microsoftonline.com/"
+```
+
+`Retrieval:SharePointSiteUrl` must be the SharePoint site root: for a document at `https://contoso.sharepoint.com/sites/Build/Shared%20Documents/session.docx`, set `https://contoso.sharepoint.com/sites/Build`. Do not use a file URL or sharing link. The application validates the URL during startup. Set `Retrieval:MaximumNumberOfResults` from `1` through `25` only when you need a different result count.
+
+For deployment, use equivalent environment variables, for example `Retrieval__SharePointSiteUrl`.
+
+## Run and test
+
+1. Start the sample:
+
+ ```bash
+ dotnet run
+ ```
+
+2. In a second terminal, expose it through a tunnel:
```bash
devtunnel host -p 3978 --allow-anonymous
```
-1. On the Azure Bot, select **Settings**, then **Configuration**, and update the **Messaging endpoint** to `{tunnel-url}/api/messages`
+3. In Azure Bot, set the messaging endpoint to `{tunnel-url}/api/messages`.
+4. Open **Test in Web Chat** and complete sign-in when prompted.
+5. Ask one of these questions:
-2. One last thing before we run our agent app. Go to Plugins/BuildRetrievalPlugin.cs and udpate the FilterExpression
-3. Start the Agent in Visual Studio
-4. Select **Test in WebChat** on the Azure Bot
+ - `What Contoso sessions are at Build 2025?`
+ - `Tell me about the Pricing Analytics session.`
+ - `Who is speaking at the collaboration sessions?`
+A successful test returns text from the uploaded document and a source card. Select a source card to verify that its link opens the SharePoint document.
-## Sample queries to try with this bot
-1. Hey there!
-2. Can you give me a snapshot of all the sessions that Contoso is doing at Build 2025?
-3. How many days till Build 2025?
-4. I haven't seen a demo for the Pricing Analytics session. Can you send a mail to Adele Vance requesting for a Demo run this Friday?
+## Wait for SharePoint indexing
+The document can appear in the Documents library before SharePoint search and Retrieval API can find it. Indexing can take minutes or hours; SharePoint does not provide a per-file indexing status.
-## Enabling JWT token validation
-1. By default, token validation is disabled in Development mode. This is determined by `AddAgentAuthorization` and the `forceEnable` argument.
+Before testing the agent, sign in as the same Web Chat user and search the site for the document title or a unique phrase such as `Pricing Analytics`. Test the agent only after SharePoint search returns the document.
-1. Updating appsettings and replace {{ClientId}} and {{TenantId}} with the values from your Azure Bot.
- ```json
- "TokenValidation": {
- "Audiences": [
- "{{ClientId}}"
- ],
- "TenantId": "{{TenantId}}"
- },
- ```
+If the document is still not searchable after a reasonable wait, first confirm that the library permits search results. In the current SharePoint UI, go to **Site contents**, select the **three dots** for **Documents**, then select **Settings** > **Advanced settings**. Under **Search**, set **Allow items from this document library to appear in search results** to **Yes**. Select **Reindex Document Library** on the same page if needed. Reindexing adds the library to the next crawl; it does not complete immediately. Request it once only. See [Microsoft's reindex guidance](https://learn.microsoft.com/en-us/sharepoint/crawl-site-content#reindex-a-site).
+
+## Troubleshooting
+
+| Symptom | Action |
+|---|---|
+| Startup fails with `Retrieval:SharePointSiteUrl` | Set an absolute HTTPS SharePoint site URL in user secrets or environment variables. |
+| Sign-in does not complete | Confirm the Azure Bot OAuth connection name is `graph`, then check its app registration and delegated permissions. |
+| No results | Confirm the user can open the document, the configured URL is the site root, and SharePoint site search finds the document. If needed, request one library reindex. |
+| Retrieval is unavailable | Confirm tenant entitlement, Microsoft Graph permissions, and service availability; retry later. |
+
+## Security notes
+
+- The application never writes delegated tokens, Microsoft Graph response bodies, or stack traces to chat.
+- Microsoft Graph applies the signed-in user's SharePoint permissions to Retrieval API requests.
+- Keep client secrets in user secrets, environment variables, or a managed secret store.
## Further reading
-To learn more about building Agents, see [Microsoft 365 Agents SDK](https://learn.microsoft.com/en-us/microsoft-365/agents-sdk/).
\ No newline at end of file
+
+- [Microsoft 365 Agents SDK](https://learn.microsoft.com/microsoft-365/agents-sdk/)
+- [Microsoft 365 Copilot Retrieval API](https://learn.microsoft.com/microsoft-365/copilot/extensibility/api/ai-services/retrieval/copilotroot-retrieval)
diff --git a/samples/dotnet/retrieval-agent/RetrievalAgent.cs b/samples/dotnet/retrieval-agent/RetrievalAgent.cs
index 66519754..65331067 100644
--- a/samples/dotnet/retrieval-agent/RetrievalAgent.cs
+++ b/samples/dotnet/retrieval-agent/RetrievalAgent.cs
@@ -1,25 +1,26 @@
-// Copyright (c) Microsoft Corporation. All rights reserved.
+// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
+using Microsoft.Agents.Builder;
using Microsoft.Agents.Builder.App;
+using Microsoft.Agents.Builder.App.UserAuth;
using Microsoft.Agents.Builder.State;
-using Microsoft.Agents.Builder;
+using Microsoft.Agents.Builder.UserAuth;
using Microsoft.Agents.Core.Models;
-using System.Threading.Tasks;
+using RetrievalAgent.Services;
using System.Threading;
-using RetrievalAgent.Agents;
-using Microsoft.SemanticKernel.ChatCompletion;
-using Microsoft.SemanticKernel;
+using System.Threading.Tasks;
namespace RetrievalAgent
{
- public class Retrieval: AgentApplication
+ public class Retrieval : AgentApplication
{
- private readonly Kernel _kernel;
+ private readonly IBuildGenieMessageRoute _messageRoute;
- public Retrieval(AgentApplicationOptions options, Kernel kernel) : base (options)
+ public Retrieval(AgentApplicationOptions options, IBuildGenieMessageRoute messageRoute) : base(options)
{
- _kernel = kernel;
+ _messageRoute = messageRoute;
+ UserAuthorization.OnUserSignInFailure(OnUserSignInFailureAsync);
}
[MessageRoute]
@@ -27,31 +28,12 @@ protected async Task MessageActivityAsync(ITurnContext turnContext, ITurnState t
{
await turnContext.SendActivityAsync(new Activity { Type = ActivityTypes.Typing }, cancellationToken);
- var chatHistory = turnState.GetValue("conversation.chatHistory", () => new ChatHistory());
-
- RetrievalCompletionAgent retrievalAgent = new RetrievalCompletionAgent(_kernel, this);
-
- // Invoke the RetrievalCompletionAgent to process the message
- var forecastResponse = await retrievalAgent.InvokeAgentAsync(turnContext.Activity.Text, chatHistory);
- if (forecastResponse == null)
- {
- await turnContext.SendActivityAsync(MessageFactory.Text("Sorry, I couldn't get the information you are looking for, at the moment."), cancellationToken);
- return;
- }
-
- // Create a response message based on the response content type from the RetrievalCompletionAgent
- IActivity response = forecastResponse.ContentType switch
- {
- RetrievalCompletionAgentResponseContentType.AdaptiveCard => MessageFactory.Attachment(new Attachment()
- {
- ContentType = "application/vnd.microsoft.card.adaptive",
- Content = forecastResponse.Content,
- }),
- _ => MessageFactory.Text(forecastResponse.Content),
- };
-
- // Send the response message back to the user.
- await turnContext.SendActivityAsync(response, cancellationToken);
+ await _messageRoute.HandleAsync(
+ turnContext.Activity.Text ?? string.Empty,
+ _ => UserAuthorization.GetTurnTokenAsync(turnContext, "graph"),
+ (text, token) => turnContext.SendActivityAsync(MessageFactory.Text(text), token),
+ (activity, token) => turnContext.SendActivityAsync(activity, token),
+ cancellationToken);
}
[MembersAddedRoute]
@@ -61,10 +43,12 @@ protected async Task WelcomeMessageAsync(ITurnContext turnContext, ITurnState tu
{
if (member.Id != turnContext.Activity.Recipient.Id)
{
- // welcome the user to the bot
- await turnContext.SendActivityAsync(MessageFactory.Text("Hello! I am Build Genie! I can help you prepare for Build Conference 2025!"), cancellationToken);
+ await turnContext.SendActivityAsync(MessageFactory.Text("Hello! I am Build Genie. Ask me about Build 2025 sessions in the configured SharePoint site. I only search content you can access."), cancellationToken);
}
}
}
+
+ private static Task OnUserSignInFailureAsync(ITurnContext turnContext, ITurnState turnState, string handlerName, SignInResponse response, IActivity initiatingActivity, CancellationToken cancellationToken) =>
+ turnContext.SendActivityAsync(MessageFactory.Text(BuildGenieResponses.For(RetrievalStatus.NotSignedIn)), cancellationToken);
}
}
diff --git a/samples/dotnet/retrieval-agent/RetrievalAgent.csproj b/samples/dotnet/retrieval-agent/RetrievalAgent.csproj
index 9bda8d78..2a1248a4 100644
--- a/samples/dotnet/retrieval-agent/RetrievalAgent.csproj
+++ b/samples/dotnet/retrieval-agent/RetrievalAgent.csproj
@@ -4,7 +4,6 @@
net8.0latestdisable
- $(NoWarn);SKEXP0110;SKEXP0010falsefalseenable
@@ -12,35 +11,18 @@
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/samples/dotnet/retrieval-agent/Services/BuildGenieMessageRoute.cs b/samples/dotnet/retrieval-agent/Services/BuildGenieMessageRoute.cs
new file mode 100644
index 00000000..4bc90efa
--- /dev/null
+++ b/samples/dotnet/retrieval-agent/Services/BuildGenieMessageRoute.cs
@@ -0,0 +1,41 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using Microsoft.Agents.Core.Models;
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace RetrievalAgent.Services;
+
+public interface IBuildGenieMessageRoute
+{
+ Task HandleAsync(
+ string question,
+ Func> getAccessTokenAsync,
+ Func sendTextAsync,
+ Func sendActivityAsync,
+ CancellationToken cancellationToken);
+}
+
+public sealed class BuildGenieMessageRoute(IBuildRetrievalService retrievalService) : IBuildGenieMessageRoute
+{
+ public async Task HandleAsync(
+ string question,
+ Func> getAccessTokenAsync,
+ Func sendTextAsync,
+ Func sendActivityAsync,
+ CancellationToken cancellationToken)
+ {
+ RetrievalResult result = await retrievalService.RetrieveAsync(question, getAccessTokenAsync, cancellationToken);
+
+ if (result.Status != RetrievalStatus.Success)
+ {
+ await sendTextAsync(BuildGenieResponses.For(result.Status), cancellationToken);
+ return;
+ }
+
+ await sendTextAsync(BuildGenieResponses.GroundedAnswer(result.Items), cancellationToken);
+ await sendActivityAsync(BuildGenieSourceCard.Create(result.Items), cancellationToken);
+ }
+}
diff --git a/samples/dotnet/retrieval-agent/Services/BuildGenieResponses.cs b/samples/dotnet/retrieval-agent/Services/BuildGenieResponses.cs
new file mode 100644
index 00000000..5d3c7fb2
--- /dev/null
+++ b/samples/dotnet/retrieval-agent/Services/BuildGenieResponses.cs
@@ -0,0 +1,50 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using Microsoft.Agents.Builder;
+using Microsoft.Agents.Core.Models;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace RetrievalAgent.Services;
+
+public static class BuildGenieResponses
+{
+ public static string For(RetrievalStatus status) => status switch
+ {
+ RetrievalStatus.NotSignedIn => "Please sign in to Microsoft 365, then ask your Build question again.",
+ RetrievalStatus.NoResults => "I couldn't find Build session information in the configured SharePoint site. Check the site URL, document permissions, and indexing, then try a more specific question.",
+ _ => "I couldn't retrieve Build session information right now. Please try again later.",
+ };
+
+ public static string GroundedAnswer(IReadOnlyList items) =>
+ "Here is what I found in the configured SharePoint site:\n\n" + string.Join("\n\n", items.Select(item => $"{item.Extract}\nSource: {item.SourceUrl}"));
+}
+
+public static class BuildGenieSourceCard
+{
+ public static IActivity Create(IReadOnlyList items)
+ {
+ List