Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
85 changes: 85 additions & 0 deletions dotnet/semantic-kernel/sample-agent/Agent-Code-Walkthrough.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# Agent Code Walkthrough

This document provides a detailed walkthrough of the code for this agent. The
agent is designed to perform specific tasks autonomously, interacting with the
user as needed.

## Key Files in this Solution
- `Program.cs`:
- This is the entry point for the application. It sets up the necessary services
and middleware for the agent.
- Tracing is configured here to help with debugging and monitoring the agent's activities.
```csharp
builder.Services
.AddTracing(config => config
.WithSemanticKernel());

builder.Services
.AddOpenTelemetry()
.WithTracing(tracing => tracing
.AddConsoleExporter());
```
- `MyAgent.cs`:
- This file contains the implementation of the agent's core logic, including how
it registers handling of activities.
- The constructor has three lines that register the agent's handling of activities:
- `this.OnAgentNotification("*", AgentNotificationActivityAsync);`:
- This registers a handler for notifications, such as when the agent
receives an email or a user mentions the agent in a comment in a Word document.
- `OnActivity(ActivityTypes.InstallationUpdate, OnHireMessageAsync);`:
- This registers the `InstallationUpdate` activity type, which is triggered
when the agent is installed ("hired") or uninstalled ("offboarded").
- `OnActivity(ActivityTypes.Message, MessageActivityAsync, rank: RouteRank.Last);`:
- This registers a handler for messages sent to the agent.
- Based on the activity handlers registered above, when the agent receives a message
about an activity, the relevant handler is invoked to process the activity.
- `Agents/Agent365Agent.cs`:
- This file contains the implementation of the Agent 365 specific logic, including how it
integrates with the Agent 365 platform and handles user interactions.
- We call `IMcpToolRegistrationService.AddToolServersToAgent(...)` to register
the Agent 365 tools with the agent.
- `Plugins/TermsAndConditionsAcceptedPlugin.cs`:
- This file contains a Semantic Kernel plugin that handles the scenario where
the user has accepted the terms and conditions.
- This contains a simple tool that allows the user to reject the terms and conditions
if they change their mind.
- `Plugins/TermsAndConditionsNotAcceptedPlugin.cs`:
- This file contains a Semantic Kernel plugin that handles the scenario where
the user has not accepted the terms and conditions.
- This contains a simple tool that allows the user to accept the terms and conditions.

## Activities Handled by the Agent

### InstallationUpdate Activity

- This activity is triggered when the agent is installed or uninstalled.
- The `OnHireMessageAsync` method in `MyAgent.cs` handles this activity:
- If the agent is installed, it sends a welcome message to the user, asking
the user to accept the terms and conditions.
- If the agent is uninstalled, it sends a goodbye message to the user, and it
resets the user's acceptance of the terms and conditions.
- The `TermsAndConditionsAccepted` flag has been implemented as a static property
in the `MyAgent` class for simplicity. In a production scenario, this should be
stored in a persistent storage solution. It is only intended as a simple example
to demonstrate the `InstallationUpdate` activity.

### Notification Activity

- This activity is triggered when the agent receives a notification, such as
when the user mentions the agent in a comment in a Word document or when the
agent receives an email.
- The `AgentNotificationActivityAsync` method in `MyAgent.cs` handles this activity:
- It processes the notification and takes appropriate action based on the content
of the notification.

### Message Activity

- This activity is triggered when the agent receives a message from the user.
- The `MessageActivityAsync` method in `MyAgent.cs` handles this activity:
- It processes the message and takes appropriate action based on the content
of the message.

### Activity Protocol Samples

For more information on the activity protocol and sample payloads, please refer to the
[Activity Protocol Samples](Activity-Protocol-Samples.md).
117 changes: 117 additions & 0 deletions dotnet/semantic-kernel/sample-agent/Agents/Agent365Agent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using Agent365SemanticKernelSampleAgent.Plugins;
using Microsoft.Agents.A365.Tooling.Extensions.SemanticKernel.Services;
using Microsoft.Agents.Builder;
using Microsoft.Agents.Builder.App.UserAuth;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Agents;
using Microsoft.SemanticKernel.ChatCompletion;
using Microsoft.SemanticKernel.Connectors.OpenAI;
using System;
using System.Text;
using System.Text.Json.Nodes;
using System.Threading.Tasks;

namespace Agent365SemanticKernelSampleAgent.Agents;

public class Agent365Agent
{
private readonly Kernel _kernel;
private readonly 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 responsein plain text}}""
}}
";

/// <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 Agent365Agent(Kernel kernel, IServiceProvider service, IMcpToolRegistrationService toolService, UserAuthorization userAuthorization, ITurnContext turnContext)
{
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
// The environmentId will be extracted programmatically
string environmentId = Environment.GetEnvironmentVariable("ENVIRONMENT_ID") ?? string.Empty;
this._kernel.ImportPluginFromType<TermsAndConditionsAcceptedPlugin>();

toolService.AddToolServersToAgent(kernel, environmentId, userAuthorization, 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 = 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 = "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)
{
ArgumentNullException.ThrowIfNull(chatHistory);
AgentThread thread = new ChatHistoryAgentThread();
ChatMessageContent message = new(AuthorRole.User, input);
chatHistory.Add(message);

StringBuilder sb = new();
await foreach (ChatMessageContent response in this._agent.InvokeAsync(chatHistory, thread: thread))
{
chatHistory.Add(response);
sb.Append(response.Content);
}

// Make sure the response is in the correct format and retry if necessary
try
{
string resultContent = sb.ToString();
var jsonNode = JsonNode.Parse(resultContent);
Agent365AgentResponse result = new()
{
Content = jsonNode!["content"]!.ToString(),
ContentType = Enum.Parse<Agent365AgentResponseContentType>(jsonNode["contentType"]!.ToString(), true)
};
return result;
}
catch (Exception je)
{
return await InvokeAgentAsync($"That response did not match the expected format. Please try again. Error: {je.Message}", chatHistory);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System.ComponentModel;
using System.Text.Json.Serialization;

namespace Agent365SemanticKernelSampleAgent.Agents;

public enum Agent365AgentResponseContentType
{
[JsonPropertyName("text")]
Text
}

public class Agent365AgentResponse
{
[JsonPropertyName("contentType")]
[JsonConverter(typeof(JsonStringEnumConverter))]
public Agent365AgentResponseContentType ContentType { get; set; }

[JsonPropertyName("content")]
[Description("The content of the response. May only be plain text.")]
public string? Content { get; set; }
}
Loading