Skip to content

Commit

Permalink
.Net: Assistants Update - Refine Cleanup / Delete Support (#4054)
Browse files Browse the repository at this point in the history
### Motivation and Context

<!-- Thank you for your contribution to the semantic-kernel repo!
Please help reviewers and future users, providing the following
information:
  1. Why is this change required?
  2. What problem does it solve?
  3. What scenario does it contribute to?
  4. If it fixes an open issue, please link to the issue here.
-->

Need full support for back-end of assistant lifecycle: Delete

### Description

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

- Completed fundamental support for thread and assistant clean-up
(opt-in)
- Cleaned-up extrenous exception arc on model handling
- Renamed `AssistantResponse.Response` to `AssistantResponse.Message`
- Introduced `AssistantException`
- Removed `ConfigureAwait `from samples (not used in other samples)

### Contribution Checklist

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

- [x] The code builds clean without any errors or warnings
- [x] 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
- [x] All unit tests pass, and I have added new tests where possible
- [x] I didn't break anyone 😄
  • Loading branch information
crickman authored Dec 6, 2023
1 parent 89e9dde commit cb9bc6f
Show file tree
Hide file tree
Showing 14 changed files with 236 additions and 90 deletions.
75 changes: 48 additions & 27 deletions dotnet/samples/KernelSyntaxExamples/Example70_Assistant.cs
Original file line number Diff line number Diff line change
Expand Up @@ -125,17 +125,28 @@ private static async Task RunAsFunctionAsync()
.FromTemplate(EmbeddedResource.Read("Assistants.ParrotAssistant.yaml"))
.BuildAsync();

// Invoke assistant plugin function.
KernelArguments arguments = new("Practice makes perfect.");

var kernel = new Kernel();
var result = await kernel.InvokeAsync(assistant.AsPlugin().Single(), arguments);

// Display result
var response = result.GetValue<AssistantResponse>();
Console.WriteLine(
response?.Response ??
$"No response from assistant: {assistant.Id}");
string? threadId = null;
try
{
// Invoke assistant plugin function.
KernelArguments arguments = new("Practice makes perfect.");

var kernel = new Kernel();
var result = await kernel.InvokeAsync(assistant.AsPlugin().Single(), arguments);

// Display result
var response = result.GetValue<AssistantResponse>();
threadId = response?.ThreadId;
Console.WriteLine(
response?.Message ??
$"No response from assistant: {assistant.Id}");
}
finally
{
await Task.WhenAll(
assistant.DeleteThreadAsync(threadId),
assistant.DeleteAsync());
}
}

/// <summary>
Expand All @@ -155,29 +166,39 @@ private static async Task ChatAsync(
var definition = EmbeddedResource.Read(resourcePath);

// Create assistant
var assistant =
IAssistant assistant =
await new AssistantBuilder()
.WithOpenAIChatCompletion(OpenAIFunctionEnabledModel, TestConfiguration.OpenAI.ApiKey)
.FromTemplate(definition)
.WithPlugin(plugin)
.BuildAsync();

// Display assistant identifier.
Console.WriteLine($"[{assistant.Id}]");

// Create chat thread. Note: Thread is not bound to a single assistant.
var thread = await assistant.NewThreadAsync();

// Process each user message and assistant response.
foreach (var message in messages)
IChatThread? thread = null;
try
{
// Add the user message
var messageUser = await thread.AddUserMessageAsync(message).ConfigureAwait(true);
DisplayMessage(messageUser);

// Retrieve the assistant response
var assistantMessages = await thread.InvokeAsync(assistant).ConfigureAwait(true);
DisplayMessages(assistantMessages);
// Display assistant identifier.
Console.WriteLine($"[{assistant.Id}]");

// Create chat thread. Note: Thread is not bound to a single assistant.
thread = await assistant.NewThreadAsync();

// Process each user message and assistant response.
foreach (var message in messages)
{
// Add the user message
var messageUser = await thread.AddUserMessageAsync(message);
DisplayMessage(messageUser);

// Retrieve the assistant response
var assistantMessages = await thread.InvokeAsync(assistant);
DisplayMessages(assistantMessages);
}
}
finally
{
await Task.WhenAll(
thread?.DeleteAsync() ?? Task.CompletedTask,
assistant.DeleteAsync());
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,44 +33,59 @@ public static async Task RunAsync()
return;
}

var plugin = KernelPluginFactory.CreateFromType<MenuPlugin>();
IAssistant? menuAssistant = null;
IAssistant? parrotAssistant = null;
IAssistant? toolAssistant = null;
IChatThread? thread = null;

var menuAssistant =
await new AssistantBuilder()
.WithOpenAIChatCompletion(OpenAIFunctionEnabledModel, TestConfiguration.OpenAI.ApiKey)
.FromTemplate(EmbeddedResource.Read("Assistants.ToolAssistant.yaml"))
.WithDescription("Answer questions about how the menu uses the tool.")
.WithPlugin(plugin)
.BuildAsync();
try
{
var plugin = KernelPluginFactory.CreateFromType<MenuPlugin>();
menuAssistant =
await new AssistantBuilder()
.WithOpenAIChatCompletion(OpenAIFunctionEnabledModel, TestConfiguration.OpenAI.ApiKey)
.FromTemplate(EmbeddedResource.Read("Assistants.ToolAssistant.yaml"))
.WithDescription("Answer questions about how the menu uses the tool.")
.WithPlugin(plugin)
.BuildAsync();

var parrotAssistant =
await new AssistantBuilder()
.WithOpenAIChatCompletion(OpenAIFunctionEnabledModel, TestConfiguration.OpenAI.ApiKey)
.FromTemplate(EmbeddedResource.Read("Assistants.ParrotAssistant.yaml"))
.BuildAsync();
parrotAssistant =
await new AssistantBuilder()
.WithOpenAIChatCompletion(OpenAIFunctionEnabledModel, TestConfiguration.OpenAI.ApiKey)
.FromTemplate(EmbeddedResource.Read("Assistants.ParrotAssistant.yaml"))
.BuildAsync();

var toolAssistant =
await new AssistantBuilder()
.WithOpenAIChatCompletion(OpenAIFunctionEnabledModel, TestConfiguration.OpenAI.ApiKey)
.FromTemplate(EmbeddedResource.Read("Assistants.ToolAssistant.yaml"))
.WithPlugins(new[] { menuAssistant.AsPlugin(), parrotAssistant.AsPlugin() })
.BuildAsync();
toolAssistant =
await new AssistantBuilder()
.WithOpenAIChatCompletion(OpenAIFunctionEnabledModel, TestConfiguration.OpenAI.ApiKey)
.FromTemplate(EmbeddedResource.Read("Assistants.ToolAssistant.yaml"))
.WithPlugins(new[] { menuAssistant.AsPlugin(), parrotAssistant.AsPlugin() })
.BuildAsync();

var messages = new string[]
{
var messages = new string[]
{
"What's on the menu?",
"Can you talk like pirate?",
"Thank you",
};
};

var thread = await toolAssistant.NewThreadAsync();
foreach (var message in messages)
{
var messageUser = await thread.AddUserMessageAsync(message).ConfigureAwait(true);
DisplayMessage(messageUser);
thread = await toolAssistant.NewThreadAsync();
foreach (var message in messages)
{
var messageUser = await thread.AddUserMessageAsync(message);
DisplayMessage(messageUser);

var assistantMessages = await thread.InvokeAsync(toolAssistant).ConfigureAwait(true);
DisplayMessages(assistantMessages);
var assistantMessages = await thread.InvokeAsync(toolAssistant);
DisplayMessages(assistantMessages);
}
}
finally
{
await Task.WhenAll(
thread?.DeleteAsync() ?? Task.CompletedTask,
toolAssistant?.DeleteAsync() ?? Task.CompletedTask,
parrotAssistant?.DeleteAsync() ?? Task.CompletedTask,
menuAssistant?.DeleteAsync() ?? Task.CompletedTask);
}
}

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

using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Experimental.Assistants;
using Microsoft.SemanticKernel.Experimental.Assistants.Exceptions;
using Microsoft.SemanticKernel.Experimental.Assistants.Extensions;
using Xunit;

namespace SemanticKernel.Experimental.Assistants.UnitTests;
Expand Down Expand Up @@ -38,6 +39,6 @@ public static void InvokeInvalidSinglePartTool(string toolName)
var kernel = new Kernel();

//Act & Assert
Assert.Throws<KernelException>(() => kernel.GetAssistantTool(toolName));
Assert.Throws<AssistantException>(() => kernel.GetAssistantTool(toolName));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,7 @@ public static async Task<IAssistant> GetAssistantAsync(
CancellationToken cancellationToken = default)
{
var restContext = new OpenAIRestContext(apiKey);
var resultModel =
await restContext.GetAssistantModelAsync(assistantId, cancellationToken).ConfigureAwait(false) ??
throw new KernelException($"Unexpected failure retrieving assistant: no result. ({assistantId})");
var resultModel = await restContext.GetAssistantModelAsync(assistantId, cancellationToken).ConfigureAwait(false);

return new Assistant(resultModel, restContext, plugins);
}
Expand Down
5 changes: 3 additions & 2 deletions dotnet/src/Experimental/Assistants/AssistantBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.SemanticKernel.Experimental.Assistants.Exceptions;
using Microsoft.SemanticKernel.Experimental.Assistants.Internal;
using Microsoft.SemanticKernel.Experimental.Assistants.Models;
using YamlDotNet.Serialization;
Expand Down Expand Up @@ -41,12 +42,12 @@ public async Task<IAssistant> BuildAsync(CancellationToken cancellationToken = d
{
if (string.IsNullOrWhiteSpace(this._model.Model))
{
throw new KernelException("Model must be defined for assistant.");
throw new AssistantException("Model must be defined for assistant.");
}

if (string.IsNullOrWhiteSpace(this._apiKey))
{
throw new KernelException("ApiKey must be provided for assistant.");
throw new AssistantException("ApiKey must be provided for assistant.");
}

return
Expand Down
2 changes: 1 addition & 1 deletion dotnet/src/Experimental/Assistants/AssistantResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public class AssistantResponse
/// The assistant response.
/// </summary>
[JsonPropertyName("response")]
public string Response { get; set; } = string.Empty;
public string Message { get; set; } = string.Empty;

/// <summary>
/// Instructions from assistant on next steps.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Copyright (c) Microsoft. All rights reserved.

using System;

namespace Microsoft.SemanticKernel.Experimental.Assistants.Exceptions;

/// <summary>
/// Assistant specific <see cref="KernelException"/>.
/// </summary>
public class AssistantException : KernelException
{
/// <summary>
/// Initializes a new instance of the <see cref="AssistantException"/> class.
/// </summary>
public AssistantException()
{
}

/// <summary>
/// Initializes a new instance of the <see cref="AssistantException"/> class with a specified error message.
/// </summary>
/// <param name="message">The error message that explains the reason for the exception.</param>
public AssistantException(string? message) : base(message)
{
}

/// <summary>
/// Initializes a new instance of the <see cref="AssistantException"/> class with a specified error message and a reference to the inner exception that is the cause of this exception.
/// </summary>
/// <param name="message">The error message that explains the reason for the exception.</param>
/// <param name="innerException">The exception that is the cause of the current exception, or a null reference if no inner exception is specified.</param>
public AssistantException(string? message, Exception? innerException) : base(message, innerException)
{
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Copyright (c) Microsoft. All rights reserved.

namespace Microsoft.SemanticKernel.Experimental.Assistants;
using Microsoft.SemanticKernel.Experimental.Assistants.Exceptions;

namespace Microsoft.SemanticKernel.Experimental.Assistants.Extensions;

internal static class AssistantsKernelExtensions
{
Expand All @@ -13,7 +15,7 @@ public static KernelFunction GetAssistantTool(this Kernel kernel, string toolNam
return nameParts.Length switch
{
2 => kernel.Plugins.GetFunction(nameParts[0], nameParts[1]),
_ => throw new KernelException($"Unknown tool: {toolName}"),
_ => throw new AssistantException($"Unknown tool: {toolName}"),
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.SemanticKernel.Experimental.Assistants.Exceptions;
using Microsoft.SemanticKernel.Experimental.Assistants.Internal;

namespace Microsoft.SemanticKernel.Experimental.Assistants;
Expand All @@ -27,13 +28,17 @@ private static async Task<TResult> ExecuteGetAsync<TResult>(
using var response = await context.GetHttpClient().SendAsync(request, cancellationToken).ConfigureAwait(false);
if (!response.IsSuccessStatusCode)
{
throw new KernelException($"Unexpected failure: {response.StatusCode} [{url}]");
throw new AssistantException($"Unexpected failure: {response.StatusCode} [{url}]");
}

string responseBody = await response.Content.ReadAsStringAsync().ConfigureAwait(false);

// Common case is for failure exception to be raised by REST invocation.
// Null result is a logical possibility, but unlikely edge case.
// Might occur due to model alignment issues over time.
return
JsonSerializer.Deserialize<TResult>(responseBody) ??
throw new KernelException($"Null result processing: {typeof(TResult).Name}");
throw new AssistantException($"Null result processing: {typeof(TResult).Name}");
}

private static Task<TResult> ExecutePostAsync<TResult>(
Expand All @@ -58,13 +63,13 @@ private static async Task<TResult> ExecutePostAsync<TResult>(
using var response = await context.GetHttpClient().SendAsync(request, cancellationToken).ConfigureAwait(false);
if (!response.IsSuccessStatusCode)
{
throw new KernelException($"Unexpected failure: {response.StatusCode} [{url}]");
throw new AssistantException($"Unexpected failure: {response.StatusCode} [{url}]");
}

string responseBody = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
return
JsonSerializer.Deserialize<TResult>(responseBody) ??
throw new KernelException($"Null result processing: {typeof(TResult).Name}");
throw new AssistantException($"Null result processing: {typeof(TResult).Name}");
}

private static async Task ExecuteDeleteAsync(
Expand All @@ -80,7 +85,7 @@ private static async Task ExecuteDeleteAsync(
using var response = await context.GetHttpClient().SendAsync(request, cancellationToken).ConfigureAwait(false);
if (!response.IsSuccessStatusCode)
{
throw new KernelException($"Unexpected failure: {response.StatusCode} [{url}]");
throw new AssistantException($"Unexpected failure: {response.StatusCode} [{url}]");
}
}
}
14 changes: 14 additions & 0 deletions dotnet/src/Experimental/Assistants/IAssistant.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,4 +76,18 @@ public interface IAssistant
/// <param name="id">The id of the existing chat thread.</param>
/// <param name="cancellationToken">A cancellation token</param>
Task<IChatThread> GetThreadAsync(string id, CancellationToken cancellationToken = default);

/// <summary>
/// Deletes an existing assistant chat thread.
/// </summary>
/// <param name="id">The id of the existing chat thread. Allows for null-fallthrough to simplify caller patterns.</param>
/// <param name="cancellationToken">A cancellation token</param>
Task DeleteThreadAsync(string? id, CancellationToken cancellationToken = default);

/// <summary>
/// Delete current assistant. Terminal state - Unable to perform any
/// subsequent actions.
/// </summary>
/// <param name="cancellationToken">A cancellation token</param>
Task DeleteAsync(CancellationToken cancellationToken = default);
}
5 changes: 3 additions & 2 deletions dotnet/src/Experimental/Assistants/IChatThread.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,9 @@ public interface IChatThread
Task<IEnumerable<IChatMessage>> InvokeAsync(IAssistant assistant, CancellationToken cancellationToken = default);

/// <summary>
/// Delete existing thread.
/// Delete current thread. Terminal state - Unable to perform any
/// subsequent actions.
/// </summary>
/// <param name="cancellationToken">A cancellation token</param>
Task DeleteThreadAsync(CancellationToken cancellationToken = default);
Task DeleteAsync(CancellationToken cancellationToken = default);
}
Loading

0 comments on commit cb9bc6f

Please sign in to comment.