Skip to content

Commit

Permalink
.Net: Change Agents.Abstractions to depend on SemanticKernel.Abstract…
Browse files Browse the repository at this point in the history
…ions instead of SemanticKernel.Core (#10574)

### Motivation and Context

Closes #10571 

### Description

<!-- Describe your changes, the overall approach, the underlying design.
These notes will help understanding how your code works. Thanks! -->

### Contribution Checklist

<!-- Before submitting this PR, please make sure: -->

- [ ] The code builds clean without any errors or warnings
- [ ] The PR follows the [SK Contribution
Guidelines](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md)
and the [pre-submission formatting
script](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md#development-scripts)
raises no violations
- [ ] All unit tests pass, and I have added new tests where possible
- [ ] I didn't break anyone 😄
  • Loading branch information
markwallace-microsoft authored Feb 20, 2025
1 parent 99cbd45 commit 5c7e759
Show file tree
Hide file tree
Showing 15 changed files with 145 additions and 49 deletions.
8 changes: 5 additions & 3 deletions dotnet/samples/Concepts/Agents/ChatCompletion_Templating.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,9 @@ await InvokeChatCompletionAgentWithTemplateAsync(
"""
Write a one verse poem on the requested topic in the style of {{$style}}.
Always state the requested style of the poem.
""");
""",
PromptTemplateConfig.SemanticKernelTemplateFormat,
new KernelPromptTemplateFactory());
}

[Fact]
Expand Down Expand Up @@ -79,8 +81,8 @@ Always state the requested style of the poem.

private async Task InvokeChatCompletionAgentWithTemplateAsync(
string instructionTemplate,
string? templateFormat = null,
IPromptTemplateFactory? templateFactory = null)
string templateFormat,
IPromptTemplateFactory templateFactory)
{
// Define the agent
PromptTemplateConfig templateConfig =
Expand Down
4 changes: 3 additions & 1 deletion dotnet/samples/Concepts/Agents/OpenAIAssistant_Templating.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,9 @@ await InvokeAssistantAgentWithTemplateAsync(
"""
Write a one verse poem on the requested topic in the styles of {{$style}}.
Always state the requested style of the poem.
""");
""",
PromptTemplateConfig.SemanticKernelTemplateFormat,
new KernelPromptTemplateFactory());
}

[Fact]
Expand Down
3 changes: 2 additions & 1 deletion dotnet/samples/GettingStartedWithAgents/Step01_Agent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,11 @@ public async Task UseTemplateForChatCompletionAgentAsync()
// Define the agent
string generateStoryYaml = EmbeddedResource.Read("GenerateStory.yaml");
PromptTemplateConfig templateConfig = KernelFunctionYaml.ToPromptTemplateConfig(generateStoryYaml);
KernelPromptTemplateFactory templateFactory = new();

// Instructions, Name and Description properties defined via the config.
ChatCompletionAgent agent =
new(templateConfig)
new(templateConfig, templateFactory)
{
Kernel = this.CreateKernelWithChatCompletion(),
Arguments =
Expand Down
3 changes: 2 additions & 1 deletion dotnet/samples/GettingStartedWithAgents/Step02_Plugins.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,12 @@ public async Task UseChatCompletionWithTemplateExecutionSettingsAsync()
// Read the template resource
string autoInvokeYaml = EmbeddedResource.Read("AutoInvokeTools.yaml");
PromptTemplateConfig templateConfig = KernelFunctionYaml.ToPromptTemplateConfig(autoInvokeYaml);
KernelPromptTemplateFactory templateFactory = new();

// Define the agent:
// Execution-settings with auto-invocation of plugins defined via the config.
ChatCompletionAgent agent =
new(templateConfig)
new(templateConfig, templateFactory)
{
Kernel = this.CreateKernelWithChatCompletion()
};
Expand Down
2 changes: 1 addition & 1 deletion dotnet/src/Agents/Abstractions/Agents.Abstractions.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\SemanticKernel.Core\SemanticKernel.Core.csproj" />
<ProjectReference Include="..\..\SemanticKernel.Abstractions\SemanticKernel.Abstractions.csproj" />
</ItemGroup>

<ItemGroup>
Expand Down
20 changes: 6 additions & 14 deletions dotnet/src/Agents/Abstractions/KernelAgent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,6 @@ public abstract class KernelAgent : Agent
/// <summary>
/// Gets the instructions for the agent (optional).
/// </summary>
/// <remarks>
/// Instructions can be formatted in "semantic-kernel" template format (<see cref="KernelPromptTemplateFactory"/>).
/// </remarks>
public string? Instructions { get; init; }

/// <summary>
Expand All @@ -39,7 +36,7 @@ public abstract class KernelAgent : Agent
/// <summary>
/// Gets or sets a prompt template based on the agent instructions.
/// </summary>
public IPromptTemplate? Template { get; protected set; }
protected IPromptTemplate? Template { get; set; }

/// <inheritdoc/>
protected override ILoggerFactory ActiveLoggerFactory => this.LoggerFactory ?? this.Kernel.LoggerFactory;
Expand All @@ -53,19 +50,14 @@ public abstract class KernelAgent : Agent
/// <returns>The formatted system instructions for the agent.</returns>
protected async Task<string?> FormatInstructionsAsync(Kernel kernel, KernelArguments? arguments, CancellationToken cancellationToken)
{
// If <see cref="Template"/> is not set, default instructions may be treated as "semantic-kernel" template.
if (this.Template == null)
// Use the provided template as the instructions
if (this.Template is not null)
{
if (string.IsNullOrWhiteSpace(this.Instructions))
{
return null;
}

KernelPromptTemplateFactory templateFactory = new(this.LoggerFactory);
this.Template = templateFactory.Create(new PromptTemplateConfig(this.Instructions!));
return await this.Template.RenderAsync(kernel, arguments, cancellationToken).ConfigureAwait(false);
}

return await this.Template.RenderAsync(kernel, arguments, cancellationToken).ConfigureAwait(false);
// Use the instructions as-is
return this.Instructions;
}

/// <summary>
Expand Down
10 changes: 6 additions & 4 deletions dotnet/src/Agents/AzureAI/AzureAIAgent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,23 +54,25 @@ public static class Tools
/// </summary>
/// <param name="model">The agent model definition.</param>
/// <param name="client">An <see cref="AgentsClient"/> instance.</param>
/// <param name="templateConfig">The prompt template configuration.</param>
/// <param name="templateFactory">An optional template factory.</param>
public AzureAIAgent(
Azure.AI.Projects.Agent model,
AgentsClient client,
PromptTemplateConfig? templateConfig = null,
IPromptTemplateFactory? templateFactory = null)
{
this.Client = client;
this.Definition = model;
this.Description = this.Definition.Description;
this.Id = this.Definition.Id;
this.Name = this.Definition.Name;
this.Instructions = this.Definition.Instructions;
this.Instructions = templateConfig?.Template ?? this.Definition.Instructions;

if (templateFactory != null)
if (templateConfig is not null)
{
PromptTemplateConfig templateConfig = new(this.Instructions);
this.Template = templateFactory.Create(templateConfig);
this.Template = templateFactory?.Create(templateConfig)
?? throw new KernelException($"Invalid prompt template factory {templateFactory} for format {templateConfig.TemplateFormat}");
}
}

Expand Down
1 change: 1 addition & 0 deletions dotnet/src/Agents/Core/Agents.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@

<ItemGroup>
<ProjectReference Include="..\Abstractions\Agents.Abstractions.csproj" />
<ProjectReference Include="..\..\SemanticKernel.Core\SemanticKernel.Core.csproj" />
</ItemGroup>

<ItemGroup>
Expand Down
13 changes: 4 additions & 9 deletions dotnet/src/Agents/Core/ChatCompletionAgent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,19 +32,16 @@ public ChatCompletionAgent() { }
/// a <see cref="PromptTemplateConfig"/>.
/// </summary>
/// <param name="templateConfig">The prompt template configuration.</param>
/// <param name="templateFactory">An optional factory to produce the <see cref="IPromptTemplate"/> for the agent.</param>
/// <remarks>
/// When a template factory argument isn't provided, the default <see cref="KernelPromptTemplateFactory"/> is used.
/// </remarks>
/// <param name="templateFactory">The prompt template factory used to produce the <see cref="IPromptTemplate"/> for the agent.</param>
public ChatCompletionAgent(
PromptTemplateConfig templateConfig,
IPromptTemplateFactory? templateFactory = null)
IPromptTemplateFactory templateFactory)
{
this.Name = templateConfig.Name;
this.Description = templateConfig.Description;
this.Instructions = templateConfig.Template;
this.Arguments = new(templateConfig.ExecutionSettings.Values);
this.Template = templateFactory?.Create(templateConfig);
this.Template = templateFactory.Create(templateConfig);
}

/// <summary>
Expand Down Expand Up @@ -99,12 +96,10 @@ protected override Task<AgentChannel> RestoreChannelAsync(string channelState, C

internal static (IChatCompletionService service, PromptExecutionSettings? executionSettings) GetChatCompletionService(Kernel kernel, KernelArguments? arguments)
{
// Need to provide a KernelFunction to the service selector as a container for the execution-settings.
KernelFunction nullPrompt = KernelFunctionFactory.CreateFromPrompt("placeholder", arguments?.ExecutionSettings?.Values);
(IChatCompletionService chatCompletionService, PromptExecutionSettings? executionSettings) =
kernel.ServiceSelector.SelectAIService<IChatCompletionService>(
kernel,
nullPrompt,
arguments?.ExecutionSettings,
arguments ?? []);

return (chatCompletionService, executionSettings);
Expand Down
12 changes: 7 additions & 5 deletions dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,10 @@ public OpenAIAssistantAgent(
this.Name = this.Definition.Name;
this.Instructions = templateConfig?.Template ?? this.Definition.Instructions;

if (templateConfig != null)
if (templateConfig is not null)
{
this.Template = templateFactory?.Create(templateConfig);
this.Template = templateFactory?.Create(templateConfig)
?? throw new KernelException($"Invalid prompt template factory {templateFactory} for format {templateConfig.TemplateFormat}");
}

if (plugins != null)
Expand Down Expand Up @@ -101,7 +102,7 @@ public OpenAIAssistantAgent(
/// <param name="kernel">The <see cref="Kernel"/> containing services, plugins, and other state for use throughout the operation.</param>
/// <param name="defaultArguments">Required arguments that provide default template parameters, including any <see cref="PromptExecutionSettings"/>.</param>
/// <param name="templateConfig">The prompt template configuration.</param>
/// <param name="templateFactory">An optional factory to produce the <see cref="IPromptTemplate"/> for the agent.</param>
/// <param name="templateFactory">An prompt template factory to produce the <see cref="IPromptTemplate"/> for the agent.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>An <see cref="OpenAIAssistantAgent"/> instance.</returns>
[Obsolete("Use the OpenAI.Assistants.AssistantClient to create an assistant (CreateAssistantFromTemplateAsync).")]
Expand All @@ -111,7 +112,7 @@ public static async Task<OpenAIAssistantAgent> CreateFromTemplateAsync(
Kernel kernel,
KernelArguments defaultArguments,
PromptTemplateConfig templateConfig,
IPromptTemplateFactory? templateFactory = null,
IPromptTemplateFactory templateFactory,
CancellationToken cancellationToken = default)
{
// Validate input
Expand All @@ -120,9 +121,10 @@ public static async Task<OpenAIAssistantAgent> CreateFromTemplateAsync(
Verify.NotNull(clientProvider, nameof(clientProvider));
Verify.NotNull(capabilities, nameof(capabilities));
Verify.NotNull(templateConfig, nameof(templateConfig));
Verify.NotNull(templateFactory, nameof(templateFactory));

// Ensure template is valid (avoid failure after posting assistant creation)
IPromptTemplate? template = templateFactory?.Create(templateConfig);
IPromptTemplate template = templateFactory.Create(templateConfig);

// Create the client
AssistantClient client = clientProvider.Client.GetAssistantClient();
Expand Down
1 change: 1 addition & 0 deletions dotnet/src/Agents/UnitTests/Agents.UnitTests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\Extensions\PromptTemplates.Handlebars\PromptTemplates.Handlebars.csproj" />
<ProjectReference Include="..\Abstractions\Agents.Abstractions.csproj" />
<ProjectReference Include="..\..\Connectors\Connectors.AzureOpenAI\Connectors.AzureOpenAI.csproj" />
<ProjectReference Include="..\Core\Agents.Core.csproj" />
Expand Down
34 changes: 28 additions & 6 deletions dotnet/src/Agents/UnitTests/Core/ChatCompletionAgentTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public void VerifyChatCompletionAgentDefinition()
[Fact]
public void VerifyChatCompletionAgentTemplate()
{
PromptTemplateConfig config =
PromptTemplateConfig promptConfig =
new()
{
Name = "TestName",
Expand All @@ -73,16 +73,38 @@ public void VerifyChatCompletionAgentTemplate()
},
}
};
KernelPromptTemplateFactory templateFactory = new();

// Arrange
ChatCompletionAgent agent = new(config);
ChatCompletionAgent agent = new(promptConfig, templateFactory);

// Assert
Assert.NotNull(agent.Id);
Assert.Equal(config.Template, agent.Instructions);
Assert.Equal(config.Description, agent.Description);
Assert.Equal(config.Name, agent.Name);
Assert.Equal(config.ExecutionSettings, agent.Arguments.ExecutionSettings);
Assert.Equal(promptConfig.Template, agent.Instructions);
Assert.Equal(promptConfig.Description, agent.Description);
Assert.Equal(promptConfig.Name, agent.Name);
Assert.Equal(promptConfig.ExecutionSettings, agent.Arguments.ExecutionSettings);
}

/// <summary>
/// Verify throws <see cref="KernelException"/> when invalid <see cref="IPromptTemplateFactory"/> is provided.
/// </summary>
[Fact]
public void VerifyThrowsForInvalidTemplateFactory()
{
// Arrange
PromptTemplateConfig promptConfig =
new()
{
Name = "TestName",
Description = "TestDescription",
Template = "TestInstructions",
TemplateFormat = "handlebars",
};
KernelPromptTemplateFactory templateFactory = new();

// Act and Assert
Assert.Throws<KernelException>(() => new ChatCompletionAgent(promptConfig, templateFactory));
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
using Microsoft.SemanticKernel.Agents;
using Microsoft.SemanticKernel.Agents.OpenAI;
using Microsoft.SemanticKernel.ChatCompletion;
using Microsoft.SemanticKernel.PromptTemplates.Handlebars;
using OpenAI.Assistants;
using Xunit;

Expand Down Expand Up @@ -76,11 +77,9 @@ public async Task VerifyOpenAIAssistantAgentCreationDefaultTemplateAsync()

OpenAIAssistantCapabilities capabilities = new("testmodel");

// Act and Assert
await this.VerifyAgentTemplateAsync(capabilities, templateConfig);

// Act and Assert
await this.VerifyAgentTemplateAsync(capabilities, templateConfig, new KernelPromptTemplateFactory());
await Assert.ThrowsAsync<KernelException>(async () => await this.VerifyAgentTemplateAsync(capabilities, templateConfig, new HandlebarsPromptTemplateFactory()));
}

/// <summary>
Expand Down Expand Up @@ -734,7 +733,7 @@ await OpenAIAssistantAgent.CreateAsync(
private async Task VerifyAgentTemplateAsync(
OpenAIAssistantCapabilities capabilities,
PromptTemplateConfig templateConfig,
IPromptTemplateFactory? templateFactory = null)
IPromptTemplateFactory templateFactory)
{
this.SetupResponse(HttpStatusCode.OK, capabilities, templateConfig);

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

using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace Microsoft.SemanticKernel;

/// <summary>
/// Represents a kernel function that performs no operation.
/// </summary>
[RequiresUnreferencedCode("Uses reflection to handle various aspects of the function creation and invocation, making it incompatible with AOT scenarios.")]
[RequiresDynamicCode("Uses reflection to handle various aspects of the function creation and invocation, making it incompatible with AOT scenarios.")]
internal sealed class KernelFunctionNoop : KernelFunction
{
/// <summary>
/// Creates a new instance of the <see cref="KernelFunctionNoop"/> class.
/// </summary>
/// <param name="executionSettings">Option: Prompt execution settings.</param>
internal KernelFunctionNoop(IReadOnlyDictionary<string, PromptExecutionSettings>? executionSettings) :
base($"Function_{Guid.NewGuid():N}", string.Empty, [], null, executionSettings?.ToDictionary(static kv => kv.Key, static kv => kv.Value))
{
}

/// <inheritdoc/>
public override KernelFunction Clone(string pluginName)
{
Dictionary<string, PromptExecutionSettings>? executionSettings = this.ExecutionSettings?.ToDictionary(kv => kv.Key, kv => kv.Value);
return new KernelFunctionNoop(executionSettings);
}

/// <inheritdoc/>
protected override ValueTask<FunctionResult> InvokeCoreAsync(Kernel kernel, KernelArguments arguments, CancellationToken cancellationToken)
{
return new(new FunctionResult(this));
}

/// <inheritdoc/>
protected override IAsyncEnumerable<TResult> InvokeStreamingCoreAsync<TResult>(Kernel kernel, KernelArguments arguments, CancellationToken cancellationToken)
{
return AsyncEnumerable.Empty<TResult>();
}
}
Loading

0 comments on commit 5c7e759

Please sign in to comment.