Files
semantic-kernel/dotnet/samples/KernelSyntaxExamples/Example75_AgentTools.cs
T
Chris 71666b9dbf .Net - Fix Agent Message Ordering for Multimessage Runs (#4883)
### 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.
-->

Observed agent messages not being provided in the expected order. In
practice they appear reversed, but could be anything depending on JSON
data.

### Description

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

Changed run processing to return message-ids as `IAsyncEnumerable`, in
the order they are generated.

In addition to addressing ordering issue, this also introduces the
ability for the first message to be displayed as soon as it is produced
(for multi-message runs)...even if the last message is added 30 seconds
later. (In the current version, message-ids are return only after all
have been produced.)

Also explored both flavors of `Example75_AgentTools` as separate
entry-points.

### 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 😄
2024-02-06 18:02:47 +00:00

175 lines
5.2 KiB
C#

// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.SemanticKernel.Experimental.Agents;
using Xunit;
using Xunit.Abstractions;
namespace Examples;
// ReSharper disable once InconsistentNaming
/// <summary>
/// Showcase usage of code_interpreter and retrieval tools.
/// </summary>
public sealed class Example75_AgentTools : BaseTest
{
/// <summary>
/// Specific model is required that supports agents and parallel function calling.
/// Currently this is limited to Open AI hosted services.
/// </summary>
private const string OpenAIFunctionEnabledModel = "gpt-4-1106-preview";
// Track agents for clean-up
private readonly List<IAgent> _agents = new();
/// <summary>
/// Show how to utilize code_interpreter tool.
/// </summary>
[Fact]
public async Task RunCodeInterpreterToolAsync()
{
this.WriteLine("======== Using CodeInterpreter tool ========");
if (TestConfiguration.OpenAI.ApiKey == null)
{
this.WriteLine("OpenAI apiKey not found. Skipping example.");
return;
}
var builder =
new AgentBuilder()
.WithOpenAIChatCompletion(OpenAIFunctionEnabledModel, TestConfiguration.OpenAI.ApiKey)
.WithInstructions("Write only code to solve the given problem without comment.");
try
{
var defaultAgent =
Track(
await builder.BuildAsync());
var codeInterpreterAgent =
Track(
await builder.WithCodeInterpreter().BuildAsync());
await ChatAsync(
defaultAgent,
codeInterpreterAgent,
"What is the solution to `3x + 2 = 14`?",
"What is the fibinacci sequence until 101?");
}
finally
{
await Task.WhenAll(this._agents.Select(a => a.DeleteAsync()));
}
}
/// <summary>
/// Show how to utilize retrieval tool.
/// </summary>
[Fact]
public async Task RunRetrievalToolAsync()
{
this.WriteLine("======== Using Retrieval tool ========");
if (TestConfiguration.OpenAI.ApiKey == null)
{
this.WriteLine("OpenAI apiKey not found. Skipping example.");
return;
}
// REQUIRED:
//
// Use `curl` to upload document prior to running example and assign the
// identifier to `fileId`.
//
// Powershell:
// curl https://api.openai.com/v1/files `
// -H "Authorization: Bearer $Env:OPENAI_APIKEY" `
// -F purpose="assistants" `
// -F file="@Resources/travelinfo.txt"
var fileId = "<see comment>";
var defaultAgent =
await new AgentBuilder()
.WithOpenAIChatCompletion(OpenAIFunctionEnabledModel, TestConfiguration.OpenAI.ApiKey)
.BuildAsync();
var retrievalAgent =
await new AgentBuilder()
.WithOpenAIChatCompletion(OpenAIFunctionEnabledModel, TestConfiguration.OpenAI.ApiKey)
.WithRetrieval(fileId)
.BuildAsync();
try
{
await ChatAsync(
defaultAgent,
retrievalAgent,
"Where did sam go?",
"When does the flight leave Seattle?",
"What is the hotel contact info at the destination?");
}
finally
{
await Task.WhenAll(this._agents.Select(a => a.DeleteAsync()));
}
}
/// <summary>
/// Common chat loop used for: RunCodeInterpreterToolAsync and RunRetrievalToolAsync.
/// Processes each question for both "default" and "enabled" agents.
/// </summary>
private async Task ChatAsync(
IAgent defaultAgent,
IAgent enabledAgent,
params string[] questions)
{
foreach (var question in questions)
{
this.WriteLine("\nDEFAULT AGENT:");
await InvokeAgentAsync(defaultAgent, question);
this.WriteLine("\nTOOL ENABLED AGENT:");
await InvokeAgentAsync(enabledAgent, question);
}
async Task InvokeAgentAsync(IAgent agent, string question)
{
await foreach (var message in agent.InvokeAsync(question))
{
string content = message.Content;
foreach (var annotation in message.Annotations)
{
content = content.Replace(annotation.Label, string.Empty, StringComparison.Ordinal);
}
this.WriteLine($"# {message.Role}: {content}");
if (message.Annotations.Count > 0)
{
this.WriteLine("\n# files:");
foreach (var annotation in message.Annotations)
{
this.WriteLine($"* {annotation.FileId}");
}
}
}
this.WriteLine();
}
}
private IAgent Track(IAgent agent)
{
this._agents.Add(agent);
return agent;
}
public Example75_AgentTools(ITestOutputHelper output) : base(output) { }
}