Skip to content
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
224 changes: 123 additions & 101 deletions dotnet/semantic-kernel/sample-agent/Agents/Agent365Agent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,105 +11,127 @@
using Microsoft.SemanticKernel.ChatCompletion;
using Microsoft.SemanticKernel.Connectors.OpenAI;
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Threading.Tasks;

namespace Agent365SemanticKernelSampleAgent.Agents;

public class Agent365Agent
{
private Kernel? _kernel;
private ChatCompletionAgent? _agent;

private const string AgentName = "Agent365Agent";
private const string TermsAndConditionsNotAcceptedInstructions = "The user has not accepted the terms and conditions. You must ask the user to accept the terms and conditions before you can help them with any tasks. You may use the 'accept_terms_and_conditions' function to accept the terms and conditions on behalf of the user. If the user tries to perform any action before accepting the terms and conditions, you must use the 'terms_and_conditions_not_accepted' function to inform them that they must accept the terms and conditions to proceed.";
private const string TermsAndConditionsAcceptedInstructions = "You may ask follow up questions until you have enough information to answer the user's question.";
private string AgentInstructions() => $@"
You are a friendly assistant that helps office workers with their daily tasks.
{(MyAgent.TermsAndConditionsAccepted ? TermsAndConditionsAcceptedInstructions : TermsAndConditionsNotAcceptedInstructions)}

Respond in JSON format with the following JSON schema:

{{
""contentType"": ""'Text'"",
""content"": ""{{The content of the response in plain text}}""
}}
";

private string AgentInstructions_Streaming() => $@"
You are a friendly assistant that helps office workers with their daily tasks.
{(MyAgent.TermsAndConditionsAccepted ? TermsAndConditionsAcceptedInstructions : TermsAndConditionsNotAcceptedInstructions)}

Respond in Markdown format
";

public static async Task<Agent365Agent> CreateA365AgentWrapper(Kernel kernel, IServiceProvider service, IMcpToolRegistrationService toolService, string authHandlerName, UserAuthorization userAuthorization, ITurnContext turnContext, IConfiguration configuration)
{
var _agent = new Agent365Agent();
await _agent.InitializeAgent365Agent(kernel, service, toolService, userAuthorization, authHandlerName, turnContext, configuration).ConfigureAwait(false);
return _agent;
}

/// <summary>
using System.Threading.Tasks;

namespace Agent365SemanticKernelSampleAgent.Agents;

public class Agent365Agent
{
private Kernel? _kernel;
private ChatCompletionAgent? _agent;

private const string AgentName = "Agent365Agent";
private const string TermsAndConditionsNotAcceptedInstructions = "The user has not accepted the terms and conditions. You must ask the user to accept the terms and conditions before you can help them with any tasks. You may use the 'accept_terms_and_conditions' function to accept the terms and conditions on behalf of the user. If the user tries to perform any action before accepting the terms and conditions, you must use the 'terms_and_conditions_not_accepted' function to inform them that they must accept the terms and conditions to proceed.";
private const string TermsAndConditionsAcceptedInstructions = "You may ask follow up questions until you have enough information to answer the user's question.";
private string AgentInstructions() => $@"
You are a friendly assistant that helps office workers with their daily tasks.
{(MyAgent.TermsAndConditionsAccepted ? TermsAndConditionsAcceptedInstructions : TermsAndConditionsNotAcceptedInstructions)}

Respond in JSON format with the following JSON schema:

{{
""contentType"": ""'Text'"",
""content"": ""{{The content of the response in plain text}}""
}}
";

private string AgentInstructions_Streaming() => $@"
You are a friendly assistant that helps office workers with their daily tasks.
{(MyAgent.TermsAndConditionsAccepted ? TermsAndConditionsAcceptedInstructions : TermsAndConditionsNotAcceptedInstructions)}

Respond in Markdown format
";

public static async Task<Agent365Agent> CreateA365AgentWrapper(Kernel kernel, IServiceProvider service, IMcpToolRegistrationService toolService, string authHandlerName, UserAuthorization userAuthorization, ITurnContext turnContext, IConfiguration configuration)
{
var _agent = new Agent365Agent();
await _agent.InitializeAgent365Agent(kernel, service, toolService, userAuthorization, authHandlerName, turnContext, configuration).ConfigureAwait(false);
return _agent;
}

public static (bool, string?) UseBearerTokenForDevelopment()
Comment thread
Josina20 marked this conversation as resolved.
Outdated
{
var token = Environment.GetEnvironmentVariable("BEARER_TOKEN");
if (string.IsNullOrEmpty(token))
{
return (false, null);
}
return (true, token);
}

/// <summary>
///
/// </summary>
public Agent365Agent(){}

/// <summary>
/// Initializes a new instance of the <see cref="Agent365Agent"/> class.
/// </summary>
/// <param name="serviceProvider">The service provider to use for dependency injection.</param>
public async Task InitializeAgent365Agent(Kernel kernel, IServiceProvider service, IMcpToolRegistrationService toolService, UserAuthorization userAuthorization , string authHandlerName, ITurnContext turnContext, IConfiguration configuration)
{
this._kernel = kernel;

// Only add the A365 tools if the user has accepted the terms and conditions
if (MyAgent.TermsAndConditionsAccepted)
{
// Provide the tool service with necessary parameters to connect to A365
this._kernel.ImportPluginFromType<TermsAndConditionsAcceptedPlugin>();

await turnContext.StreamingResponse.QueueInformativeUpdateAsync("Loading tools...");

await toolService.AddToolServersToAgentAsync(kernel, userAuthorization, authHandlerName, turnContext);
}
else
{
// If the user has not accepted the terms and conditions, import the plugin that allows them to accept or reject
this._kernel.ImportPluginFromObject(new TermsAndConditionsNotAcceptedPlugin(), "license");
}

// Define the agent
this._agent =
new()
{
Id = turnContext.Activity.Recipient.AgenticAppId ?? Guid.NewGuid().ToString(),
Instructions = turnContext.StreamingResponse.IsStreamingChannel ? AgentInstructions_Streaming() : AgentInstructions(),
Name = AgentName,
Kernel = this._kernel,
Arguments = new KernelArguments(new OpenAIPromptExecutionSettings()
{
#pragma warning disable SKEXP0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(options: new() { RetainArgumentTypes = true }),
#pragma warning restore SKEXP0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
ResponseFormat = turnContext.StreamingResponse.IsStreamingChannel ? "text" : "json_object",
}),
};
}

/// <summary>
/// Invokes the agent with the given input and returns the response.
/// </summary>
/// <param name="input">A message to process.</param>
/// <returns>An instance of <see cref="Agent365AgentResponse"/></returns>
public async Task<Agent365AgentResponse> InvokeAgentAsync(string input, ChatHistory chatHistory, ITurnContext? context = null)
{
ArgumentNullException.ThrowIfNull(chatHistory);
AgentThread thread = new ChatHistoryAgentThread();
ChatMessageContent message = new(AuthorRole.User, input);
chatHistory.Add(message);

/// </summary>
public Agent365Agent(){}

/// <summary>
/// Initializes a new instance of the <see cref="Agent365Agent"/> class.
/// </summary>
/// <param name="serviceProvider">The service provider to use for dependency injection.</param>
public async Task InitializeAgent365Agent(Kernel kernel, IServiceProvider service, IMcpToolRegistrationService toolService, UserAuthorization userAuthorization , string authHandlerName, ITurnContext turnContext, IConfiguration configuration)
{
this._kernel = kernel;

// Only add the A365 tools if the user has accepted the terms and conditions
if (MyAgent.TermsAndConditionsAccepted)
{
// Provide the tool service with necessary parameters to connect to A365
this._kernel.ImportPluginFromType<TermsAndConditionsAcceptedPlugin>();

await turnContext.StreamingResponse.QueueInformativeUpdateAsync("Loading tools...");

var (useBearerToken, token) = UseBearerTokenForDevelopment();

if (useBearerToken)
{
await toolService.AddToolServersToAgentAsync(kernel, userAuthorization, authHandlerName, turnContext, token);
}
else
{
await toolService.AddToolServersToAgentAsync(kernel, userAuthorization, authHandlerName, turnContext);
}
Comment thread
Josina20 marked this conversation as resolved.
}
else
{
// If the user has not accepted the terms and conditions, import the plugin that allows them to accept or reject
this._kernel.ImportPluginFromObject(new TermsAndConditionsNotAcceptedPlugin(), "license");
}

// Define the agent
this._agent =
new()
{
Id = turnContext.Activity.Recipient.AgenticAppId ?? Guid.NewGuid().ToString(),
Instructions = turnContext.StreamingResponse.IsStreamingChannel ? AgentInstructions_Streaming() : AgentInstructions(),
Name = AgentName,
Kernel = this._kernel,
Arguments = new KernelArguments(new OpenAIPromptExecutionSettings()
{
#pragma warning disable SKEXP0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(options: new() { RetainArgumentTypes = true }),
#pragma warning restore SKEXP0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
ResponseFormat = turnContext.StreamingResponse.IsStreamingChannel ? "text" : "json_object",
}),
};
}

/// <summary>
/// Invokes the agent with the given input and returns the response.
/// </summary>
/// <param name="input">A message to process.</param>
/// <returns>An instance of <see cref="Agent365AgentResponse"/></returns>
public async Task<Agent365AgentResponse> InvokeAgentAsync(string input, ChatHistory chatHistory, ITurnContext? context = null)
{
ArgumentNullException.ThrowIfNull(chatHistory);
AgentThread thread = new ChatHistoryAgentThread();
ChatMessageContent message = new(AuthorRole.User, input);
chatHistory.Add(message);

if (context!.StreamingResponse.IsStreamingChannel)
{
await foreach (var response in _agent!.InvokeStreamingAsync(chatHistory, thread: thread))
Expand All @@ -124,7 +146,7 @@ public async Task<Agent365AgentResponse> InvokeAgentAsync(string input, ChatHist
Content = "Boo",
ContentType = Enum.Parse<Agent365AgentResponseContentType>("text", true)
}; ;
}
}
else
{
StringBuilder sb = new();
Expand All @@ -139,7 +161,7 @@ public async Task<Agent365AgentResponse> InvokeAgentAsync(string input, ChatHist
chatHistory.Add(response);
sb.Append(response.Content);
}

// Make sure the response is in the correct format and retry if necessary
try
{
Expand All @@ -156,6 +178,6 @@ public async Task<Agent365AgentResponse> InvokeAgentAsync(string input, ChatHist
{
return await InvokeAgentAsync($"That response did not match the expected format. Please try again. Error: {je.Message}", chatHistory);
}
}
}
}
}
}
}
35 changes: 18 additions & 17 deletions dotnet/semantic-kernel/sample-agent/Agents/MyAgent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ public class MyAgent : AgentApplication
private readonly ILogger<MyAgent> _logger;
private readonly IConfiguration _configuration;
// Setup reusable auto sign-in handlers
private readonly string AgenticIdAuthHanlder = "agentic";
private readonly string MyAuthHanlder = "me";
private readonly string AgenticIdAuthHandler = "agentic";
private readonly string MyAuthHandler = "me";


internal static bool IsApplicationInstalled { get; set; } = false;
Expand All @@ -46,16 +46,17 @@ public MyAgent(AgentApplicationOptions options, IConfiguration configuration, Ke
_agentTokenCache = agentTokenCache ?? throw new ArgumentNullException(nameof(agentTokenCache));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));


// Disable for development purpose. In production, you would typically want to have the user accept the terms and conditions on first use and then store that in a retrievable location.
TermsAndConditionsAccepted = true;

var (useBearerToken, token) = Agent365Agent.UseBearerTokenForDevelopment();
string[] autoSignInHandlersForNotAgenticAuth = useBearerToken ? [] : new[] { MyAuthHandler };

// Register Agentic specific Activity routes. These will only be used if the incoming Activity is Agentic.
this.OnAgentNotification("*", AgentNotificationActivityAsync, RouteRank.Last, autoSignInHandlers: new[] { AgenticIdAuthHanlder });
OnActivity(ActivityTypes.InstallationUpdate, OnHireMessageAsync, isAgenticOnly: true, autoSignInHandlers: new[] { AgenticIdAuthHanlder });
OnActivity(ActivityTypes.Message, MessageActivityAsync, rank: RouteRank.Last, isAgenticOnly: true, autoSignInHandlers: new[] { AgenticIdAuthHanlder });
OnActivity(ActivityTypes.Message, MessageActivityAsync, rank: RouteRank.Last, isAgenticOnly: false, autoSignInHandlers: new[] { MyAuthHanlder });
this.OnAgentNotification("*", AgentNotificationActivityAsync, RouteRank.Last, autoSignInHandlers: new[] { AgenticIdAuthHandler });
Comment thread
Josina20 marked this conversation as resolved.
OnActivity(ActivityTypes.InstallationUpdate, OnHireMessageAsync, isAgenticOnly: true, autoSignInHandlers: new[] { AgenticIdAuthHandler });
OnActivity(ActivityTypes.Message, MessageActivityAsync, rank: RouteRank.Last, isAgenticOnly: true, autoSignInHandlers: new[] { AgenticIdAuthHandler });
OnActivity(ActivityTypes.Message, MessageActivityAsync, rank: RouteRank.Last, isAgenticOnly: false, autoSignInHandlers: autoSignInHandlersForNotAgenticAuth);
}

/// <summary>
Expand All @@ -71,13 +72,13 @@ protected async Task MessageActivityAsync(ITurnContext turnContext, ITurnState t
string ToolAuthHandlerName = "";
if (turnContext.IsAgenticRequest())
{
ObservabilityAuthHandlerName = AgenticIdAuthHanlder;
ToolAuthHandlerName = AgenticIdAuthHanlder;
ObservabilityAuthHandlerName = AgenticIdAuthHandler;
ToolAuthHandlerName = AgenticIdAuthHandler;
}
else
{
ObservabilityAuthHandlerName = MyAuthHanlder;
ToolAuthHandlerName = MyAuthHanlder;
ObservabilityAuthHandlerName = MyAuthHandler;
ToolAuthHandlerName = MyAuthHandler;
}
// Init the activity for observability

Expand Down Expand Up @@ -143,13 +144,13 @@ private async Task AgentNotificationActivityAsync(ITurnContext turnContext, ITur
string ToolAuthHandlerName = "";
if (turnContext.IsAgenticRequest())
{
ObservabilityAuthHandlerName = AgenticIdAuthHanlder;
ToolAuthHandlerName = AgenticIdAuthHanlder;
ObservabilityAuthHandlerName = AgenticIdAuthHandler;
ToolAuthHandlerName = AgenticIdAuthHandler;
}
else
{
ObservabilityAuthHandlerName = MyAuthHanlder;
ToolAuthHandlerName = MyAuthHanlder;
ObservabilityAuthHandlerName = MyAuthHandler;
ToolAuthHandlerName = MyAuthHandler;
}
// Init the activity for observability
await A365OtelWrapper.InvokeObservedAgentOperation(
Expand Down Expand Up @@ -258,11 +259,11 @@ protected async Task OnHireMessageAsync(ITurnContext turnContext, ITurnState tur
string ObservabilityAuthHandlerName = "";
if (turnContext.IsAgenticRequest())
{
ObservabilityAuthHandlerName = AgenticIdAuthHanlder;
ObservabilityAuthHandlerName = AgenticIdAuthHandler;
}
else
{
ObservabilityAuthHandlerName = MyAuthHanlder;
ObservabilityAuthHandlerName = MyAuthHandler;
}
// Init the activity for observability
await A365OtelWrapper.InvokeObservedAgentOperation(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
{
"profiles": {
"Sample Agent with MCP Platform": {
"Sample Agent": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "https://localhost:64896;http://localhost:64897"
},
"Sample Agent with Bearer Token Support": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"USE_AGENTIC_AUTH": "false"
"BEARER_TOKEN": ""
Comment thread
Josina20 marked this conversation as resolved.
},
"applicationUrl": "https://localhost:64896;http://localhost:64897"
}
Expand Down
24 changes: 24 additions & 0 deletions dotnet/semantic-kernel/sample-agent/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,30 @@ For comprehensive documentation and guidance on building agents with the Microso
- Semantic Kernel 1.66.0 or higher
- Azure/OpenAI API credentials

## Launch Profiles

This sample includes two launch profiles in `Properties/launchSettings.json`:

### Sample Agent

Uses standard Azure Bot authentication with Client Credentials or Managed Identity. Use this for production or when testing with full Azure Bot Service configuration.
Comment thread
Josina20 marked this conversation as resolved.
Outdated

### Sample Agent with Bearer Token Support

Simplified profile for local development using bearer token authentication.

**Quick setup:**
1. Get a bearer token using the [a365 CLI](https://github.com/microsoft/Agent365-dotnet):
```bash
a365 develop gettoken
```
The CLI will either automatically add the token to your `launchSettings.json` or provide it for you to copy/paste.

2. Select the "Sample Agent with Bearer Token Support" launch profile in Visual Studio
3. Run the agent

> **Note**: Bearer tokens are for development only and expire regularly. Refresh with `a365 develop gettoken`.

## Running the Agent

To set up and test this agent, refer to the [Configure Agent Testing](https://learn.microsoft.com/en-us/microsoft-agent-365/developer/testing?tabs=dotnet) guide for complete instructions.
Expand Down