Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
284 changes: 185 additions & 99 deletions dotnet/semantic-kernel/sample-agent/Agents/Agent365Agent.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using Agent365SemanticKernelSampleAgent.Constants;
using Agent365SemanticKernelSampleAgent.Plugins;
using Microsoft.Agents.A365.Tooling.Extensions.SemanticKernel.Services;
using Microsoft.Agents.Builder;
Expand All @@ -11,105 +12,190 @@
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)
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 useToken = Environment.GetEnvironmentVariable("USE_BEARER_TOKEN");
if (useToken != "true")
{
return (false, null);
}
var token = ReadMcpBearerToken();
Comment thread
Josina20 marked this conversation as resolved.
Outdated

return (true, token);
}

/// <summary>
/// Reads the cached MCP bearer token from the a365 CLI cache.
/// Throws an exception if the token is missing, invalid, or expired.
/// </summary>
/// <returns>Valid access token string</returns>
/// <exception cref="InvalidOperationException">Thrown when token is missing, invalid, or expired</exception>
public static string ReadMcpBearerToken()
Comment thread
Josina20 marked this conversation as resolved.
Outdated
{
var _agent = new Agent365Agent();
await _agent.InitializeAgent365Agent(kernel, service, toolService, userAuthorization, authHandlerName, turnContext, configuration).ConfigureAwait(false);
return _agent;
}
var appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
var tokenCachePath = Path.Combine(appDataPath,
Agent365Constants.A365CliCacheDirectory,
Agent365Constants.McpBearerTokenFileName);

/// <summary>
if (!File.Exists(tokenCachePath))
{
throw new InvalidOperationException(
$"Token cache file not found at: {tokenCachePath}\n" +
"Run 'a365 develop gettoken' to authenticate.");
}

try
{
var json = File.ReadAllText(tokenCachePath);
using var doc = JsonDocument.Parse(json);

var root = doc.RootElement;
var accessToken = root.GetProperty("AccessToken").GetString();
var expiresOn = root.GetProperty("ExpiresOn").GetDateTime();

if (string.IsNullOrEmpty(accessToken))
{
throw new InvalidOperationException(
"Invalid token in cache file. Run 'a365 develop gettoken' to re-authenticate.");
}

// Check if token is expired (with 5 minute buffer)
if (expiresOn <= DateTime.UtcNow.AddMinutes(5))
{
throw new InvalidOperationException(
$"Token expired at {expiresOn:u}. Run 'a365 develop gettoken' to refresh.");
}

return accessToken;
}
catch (JsonException ex)
{
throw new InvalidOperationException(
$"Failed to parse token cache file: {ex.Message}. Run 'a365 develop gettoken' to re-authenticate.", ex);
}
catch (KeyNotFoundException)
{
throw new InvalidOperationException(
"Invalid token format in cache file. Run 'a365 develop gettoken' to re-authenticate.");
}
catch (IOException ex)
{
throw new InvalidOperationException(
$"Failed to read token cache file: {ex.Message}", ex);
}
}

/// <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 +210,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 +225,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 +242,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);
}
}
}
}
}
}
}
Loading