Skip to content
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

.Net: feat/declarative agents #9849

Merged
merged 26 commits into from
Dec 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
1f5d051
chore: minor linting on CAP extensions
baywet Nov 29, 2024
f97fb95
chore: typo fix
baywet Nov 29, 2024
f0e4c35
feat: draft design for loading DAs from manifests
baywet Nov 29, 2024
01af860
chore: adds agents package reference
baywet Nov 29, 2024
d89d041
docs: adds missing doc comment for new method extension
baywet Nov 29, 2024
4517242
chore: fixes formatting
baywet Nov 29, 2024
97fe3cf
chore: use range for net core apps
baywet Dec 2, 2024
5268fc5
feat: adds plugins loading from DA
baywet Dec 2, 2024
472ec6e
chore: linting
baywet Dec 2, 2024
2544e7e
feat: adds test data for declarative agents
baywet Dec 2, 2024
7cadf54
Merge branch 'main' into feat/declarative-agents
crickman Dec 2, 2024
fce6c44
Merge branch 'feat/declarative-agents' of https://github.com/baywet/s…
baywet Dec 2, 2024
c4f95a2
fix: instructions file name
baywet Dec 2, 2024
60e91b4
fix: path management for CAP loading of DAs
baywet Dec 2, 2024
2d478a5
chore: adds file copy for DAs in concept project
baywet Dec 2, 2024
57b4b2c
chore: refactoring makes CAPs unit test auth available to other tests
baywet Dec 2, 2024
ae8bb7f
fix: agent name to follow required conventions
baywet Dec 2, 2024
96d8fd7
chore: initial unit test design
baywet Dec 2, 2024
2c93e97
Merge branch 'main' into feat/declarative-agents
baywet Dec 3, 2024
5c89ca1
feat: adds logging for DA imports
baywet Dec 3, 2024
b6a9d0e
fix: adds positive reinforcement to the instructions of DA
baywet Dec 3, 2024
10063ef
chore: test cleanup
baywet Dec 3, 2024
d776bb9
Merge branch 'main' into feat/declarative-agents
baywet Dec 4, 2024
2b3884a
Merge branch 'main' into feat/declarative-agents
baywet Dec 4, 2024
1bfa192
fix: adds function filter to reduce tokens use
baywet Dec 4, 2024
367b804
chore: adds bang operators since netstandard2 string null or empty me…
baywet Dec 4, 2024
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
67 changes: 67 additions & 0 deletions dotnet/samples/Concepts/Agents/DeclarativeAgents.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Text;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Agents;
using Microsoft.SemanticKernel.ChatCompletion;
using Plugins;

namespace Agents;

public class DeclarativeAgents(ITestOutputHelper output) : BaseAgentsTest(output)
{
[InlineData("SchedulingAssistant.json", "Read the body of my last five emails, if any contain a meeting request for today, check that it's already on my calendar, if not, call out which email it is.")]
[Theory]
public async Task LoadsAgentFromDeclarativeAgentManifestAsync(string agentFileName, string input)
{
var kernel = CreateKernel();
kernel.AutoFunctionInvocationFilters.Add(new ExpectedSchemaFunctionFilter());
var manifestLookupDirectory = Path.Combine(Directory.GetCurrentDirectory(), "..", "..", "..", "Resources", "DeclarativeAgents");
var manifestFilePath = Path.Combine(manifestLookupDirectory, agentFileName);

var parameters = await CopilotAgentBasedPlugins.GetAuthenticationParametersAsync();

var agent = await kernel.CreateChatCompletionAgentFromDeclarativeAgentManifestAsync<ChatCompletionAgent>(manifestFilePath, parameters);

Assert.NotNull(agent);
Assert.NotNull(agent.Name);
Assert.NotEmpty(agent.Name);
Assert.NotNull(agent.Description);
Assert.NotEmpty(agent.Description);
Assert.NotNull(agent.Instructions);
Assert.NotEmpty(agent.Instructions);

ChatMessageContent message = new(AuthorRole.User, input);
ChatHistory chatHistory = [message];
StringBuilder sb = new();
await foreach (ChatMessageContent response in agent.InvokeAsync(chatHistory))
baywet marked this conversation as resolved.
Show resolved Hide resolved
{
chatHistory.Add(response);
sb.Append(response.Content);
}
Assert.NotEmpty(chatHistory.Skip(1));
}
private Kernel CreateKernel()
{
IKernelBuilder builder = Kernel.CreateBuilder();

base.AddChatCompletionToKernel(builder);

return builder.Build();
}
private sealed class ExpectedSchemaFunctionFilter : IAutoFunctionInvocationFilter
{//TODO: this eventually needs to be added to all CAP or DA but we're still discussing where should those facilitators live
public async Task OnAutoFunctionInvocationAsync(AutoFunctionInvocationContext context, Func<AutoFunctionInvocationContext, Task> next)
{
await next(context);

if (context.Result.ValueType == typeof(RestApiOperationResponse))
{
var openApiResponse = context.Result.GetValue<RestApiOperationResponse>();
if (openApiResponse?.ExpectedSchema is not null)
{
openApiResponse.ExpectedSchema = null;
}
}
}
}
}
3 changes: 3 additions & 0 deletions dotnet/samples/Concepts/Concepts.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,9 @@
<Content Include="Resources\Plugins\RepairServicePlugin\repair-service.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Resources\DeclarativeAgents\*">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Resources\*">
Expand Down
9 changes: 7 additions & 2 deletions dotnet/samples/Concepts/Plugins/CopilotAgentBasedPlugins.cs
Original file line number Diff line number Diff line change
Expand Up @@ -87,9 +87,8 @@ private void WriteSampleHeadingToConsole(string pluginToTest, string functionToT
Console.WriteLine($"======== Calling Plugin Function: {pluginToTest}.{functionToTest} with parameters {arguments?.Select(x => x.Key + " = " + x.Value).Aggregate((x, y) => x + ", " + y)} ========");
Console.WriteLine();
}
private async Task AddCopilotAgentPluginsAsync(Kernel kernel, params string[] pluginNames)
internal static async Task<CopilotAgentPluginParameters> GetAuthenticationParametersAsync()
{
#pragma warning disable SKEXP0050
if (TestConfiguration.MSGraph.Scopes is null)
{
throw new InvalidOperationException("Missing Scopes configuration for Microsoft Graph API.");
Expand Down Expand Up @@ -131,6 +130,12 @@ private async Task AddCopilotAgentPluginsAsync(Kernel kernel, params string[] pl
{ "https://api.nasa.gov/planetary", nasaOpenApiFunctionExecutionParameters }
}
};
return apiManifestPluginParameters;
}
private async Task AddCopilotAgentPluginsAsync(Kernel kernel, params string[] pluginNames)
{
#pragma warning disable SKEXP0050
var apiManifestPluginParameters = await GetAuthenticationParametersAsync().ConfigureAwait(false);
var manifestLookupDirectory = Path.Combine(Directory.GetCurrentDirectory(), "..", "..", "..", "Resources", "Plugins", "CopilotAgentPlugins");

foreach (var pluginName in pluginNames)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"$schema": "https://aka.ms/json-schemas/copilot/declarative-agent/v1.0/schema.json",
"version": "v1.0",
"instructions": "$[file('scheduling-assistant-instructions.txt')]",
"name": "SchedulingAssistant",
"description": "This agent helps you schedule meetings and send messages.",
"actions": [
{
"id": "CalendarPlugin",
"file": "../Plugins/CopilotAgentPlugins/CalendarPlugin/calendar-apiplugin.json"
},
{
"id": "MessagesPlugin",
"file": "../Plugins/CopilotAgentPlugins/MessagesPlugin/messages-apiplugin.json"
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
You are a personal assistant to the user. You help recap the last received emails, the upcoming meetings, and reply to any emails upon user's request. You can only use the calendar and messages plugins. Whenever you make HTTP REST request, you MUST select the fewest fields you need from the API to ensure a great user experience. If you need to select the body field, you MUST select the bodyPreview field instead.
4 changes: 2 additions & 2 deletions dotnet/src/Agents/Abstractions/Agent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ public abstract class Agent
/// <summary>
/// The identifier of the agent (optional).
/// </summary>
/// <reamarks>
/// <remarks>
/// Default to a random guid value, but may be overridden.
/// </reamarks>
/// </remarks>
public string Id { get; init; } = Guid.NewGuid().ToString();

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,12 +79,12 @@ public static async Task<KernelPlugin> CreatePluginFromCopilotAgentPluginAsync(

var results = await PluginManifestDocument.LoadAsync(CopilotAgentFileJsonContents, new ReaderOptions
{
ValidationRules = new() // Disable validation rules
ValidationRules = [] // Disable validation rules
}).ConfigureAwait(false);

if (!results.IsValid)
{
var messages = results.Problems.Select(p => p.Message).Aggregate((a, b) => $"{a}, {b}");
var messages = results.Problems.Select(static p => p.Message).Aggregate(static (a, b) => $"{a}, {b}");
throw new InvalidOperationException($"Error loading the manifest: {messages}");
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
// Copyright (c) Microsoft. All rights reserved.

using System;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Plugins.Manifest;
using Microsoft.SemanticKernel.Agents;
using Microsoft.SemanticKernel.Plugins.OpenApi;
using Microsoft.SemanticKernel.Plugins.OpenApi.Extensions;

namespace Microsoft.SemanticKernel;

/// <summary>
/// Provides extension methods for loading and managing declarative agents and their Copilot Agent Plugins.
/// </summary>
public static class DeclarativeAgentExtensions
{
/// <summary>
/// Creates a chat completion agent from a declarative agent manifest asynchronously.
/// </summary>
/// <typeparam name="T">The type of the agent to create.</typeparam>
/// <param name="kernel">The kernel instance.</param>
/// <param name="filePath">The file path of the declarative agent manifest.</param>
/// <param name="pluginParameters">Optional parameters for the Copilot Agent Plugin setup.</param>
/// <param name="promptExecutionSettings">Optional prompt execution settings. Ensure you enable function calling.</param>
/// <param name="cancellationToken">Optional cancellation token.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the created chat completion agent.</returns>
public static async Task<T> CreateChatCompletionAgentFromDeclarativeAgentManifestAsync<T>(
this Kernel kernel,
string filePath,
CopilotAgentPluginParameters? pluginParameters = null,
PromptExecutionSettings? promptExecutionSettings = default,
CancellationToken cancellationToken = default)
where T : KernelAgent, new()
{
Verify.NotNull(kernel);
Verify.NotNullOrWhiteSpace(filePath);

var loggerFactory = kernel.LoggerFactory;
var logger = loggerFactory.CreateLogger(typeof(DeclarativeAgentExtensions)) ?? NullLogger.Instance;
using var declarativeAgentFileJsonContents = DocumentLoader.LoadDocumentFromFilePathAsStream(filePath,
logger);

var results = await DCManifestDocument.LoadAsync(declarativeAgentFileJsonContents, new ReaderOptions
{
ValidationRules = [] // Disable validation rules
}).ConfigureAwait(false);

if (!results.IsValid)
{
var messages = results.Problems.Select(static p => p.Message).Aggregate(static (a, b) => $"{a}, {b}");
throw new InvalidOperationException($"Error loading the manifest: {messages}");
}

var document = results.Document ?? throw new InvalidOperationException("Error loading the manifest");
var manifestDirectory = Path.GetDirectoryName(filePath);
document.Instructions = await GetEffectiveInstructionsAsync(manifestDirectory, document.Instructions, logger, cancellationToken).ConfigureAwait(false);

var agent = new T
{
Name = document.Name,
Instructions = document.Instructions,
Kernel = kernel,
Arguments = new KernelArguments(promptExecutionSettings ?? new PromptExecutionSettings()
{
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(),
}),
Description = document.Description,
LoggerFactory = loggerFactory,
Id = string.IsNullOrEmpty(document.Id) ? Guid.NewGuid().ToString() : document.Id!,
};

if (document.Capabilities is { Count: > 0 })
{
logger.LogWarning("Importing capabilities from declarative agent is not supported in semantic kernel.");
}

if (document.Actions is { Count: > 0 })
{
logger.LogInformation("Importing {ActionsCount} actions from declarative agent.", document.Actions.Count);
await Task.WhenAll(document.Actions.Select(action => ImportCAPFromActionAsync(action, manifestDirectory, kernel, pluginParameters, logger, cancellationToken))).ConfigureAwait(false);
}
return agent;
}
private static async Task ImportCAPFromActionAsync(DCAction action, string? manifestDirectory, Kernel kernel, CopilotAgentPluginParameters? pluginParameters, ILogger logger, CancellationToken cancellationToken)
{
try
{
var capManifestPath = GetFullPath(manifestDirectory, action.File);
logger.LogInformation("Importing action {ActionName} from declarative agent from path {Path}.", action.Id, capManifestPath);
await kernel.ImportPluginFromCopilotAgentPluginAsync(action.Id, capManifestPath, pluginParameters, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex) when (ex is FileNotFoundException or InvalidOperationException)
SergeyMenshykh marked this conversation as resolved.
Show resolved Hide resolved
{
logger.LogError(ex, "Error importing action {ActionName} from declarative agent.", action.Id);
}
catch (Exception ex)
{
logger.LogError(ex, "Error importing action {ActionName} from declarative agent.", action.Id);
throw;
}
}
private static async Task<string?> GetEffectiveInstructionsAsync(string? manifestFilePath, string? source, ILogger logger, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(source) ||
!source!.StartsWith("$[file('", StringComparison.OrdinalIgnoreCase) ||
!source.EndsWith("')]", StringComparison.OrdinalIgnoreCase))
{
return source;
}
#if NETCOREAPP3_0_OR_GREATER
var filePath = source[8..^3];
#else
var filePath = source.Substring(8, source.Length - 11);
#endif
filePath = GetFullPath(manifestFilePath, filePath);
return await DocumentLoader.LoadDocumentFromFilePathAsync(filePath, logger, cancellationToken).ConfigureAwait(false);
}
private static string GetFullPath(string? manifestDirectory, string relativeOrAbsolutePath)
{
return !Path.IsPathRooted(relativeOrAbsolutePath) && !relativeOrAbsolutePath.StartsWith("http", StringComparison.OrdinalIgnoreCase)
? string.IsNullOrEmpty(manifestDirectory)
? throw new InvalidOperationException("Invalid manifest file path.")
: Path.Combine(manifestDirectory, relativeOrAbsolutePath)
: relativeOrAbsolutePath;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,6 @@
<ItemGroup>
<ProjectReference Include="..\..\SemanticKernel.Core\SemanticKernel.Core.csproj" />
<ProjectReference Include="..\Functions.OpenApi\Functions.OpenApi.csproj" />
<ProjectReference Include="..\..\Agents\Abstractions\Agents.Abstractions.csproj"/>
crickman marked this conversation as resolved.
Show resolved Hide resolved
</ItemGroup>
</Project>
Loading