mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
.NET Port Agent Orchestration (#107)
* Checkpoint * Checkpoint * Namespaces * Namespace * Cleanup * Namespace order * Fix sync * Formatting * Formatting * Namespace * Namespace order * Code convention * Naming * Naming * Text handling * Text handling * Namespace * Namespace order * Namespace ordering * Test * ValueTask * net472 * Test fix * Fix namespace (net472) * Namespace * Fix conditional namespace * Fix type expression * Compatibility and cleanup * Sample compatibility * Sample compat * Test compat * modifier order * Simply http-stub * Formating fix for unit-test * Fix test * Real fix * Test clean-up * Update dotnet/src/Microsoft.Agents.Orchestration/Handoff/HandoffOrchestration.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Fix build errors after merging --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Stephen Toub <stoub@microsoft.com>
This commit is contained in:
@@ -26,6 +26,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Microsoft.Agents.Orchestration\Microsoft.Agents.Orchestration.csproj" />
|
||||
<ProjectReference Include="..\..\src\Microsoft.Extensions.AI.Agents\Microsoft.Extensions.AI.Agents.csproj" />
|
||||
<ProjectReference Include="..\..\src\Microsoft.Extensions.AI.Agents.AzureAI\Microsoft.Extensions.AI.Agents.AzureAI.csproj" />
|
||||
</ItemGroup>
|
||||
@@ -36,8 +37,12 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="Resources\*">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Tools\Files\groceries.txt">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Agents.Orchestration;
|
||||
using Microsoft.Agents.Orchestration.Concurrent;
|
||||
using Microsoft.Extensions.AI.Agents;
|
||||
using Microsoft.SemanticKernel.Agents.Runtime.InProcess;
|
||||
|
||||
namespace Orchestration;
|
||||
|
||||
/// <summary>
|
||||
/// Demonstrates how to use the <see cref="ConcurrentOrchestration"/>
|
||||
/// for executing multiple agents on the same task in parallel.
|
||||
/// </summary>
|
||||
public class ConcurrentOrchestration_Intro(ITestOutputHelper output) : OrchestrationSample(output)
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public async Task RunOrchestrationAsync(bool streamedResponse)
|
||||
{
|
||||
// Define the agents
|
||||
ChatClientAgent physicist =
|
||||
this.CreateAgent(
|
||||
instructions: "You are an expert in physics. You answer questions from a physics perspective.",
|
||||
description: "An expert in physics");
|
||||
ChatClientAgent chemist =
|
||||
this.CreateAgent(
|
||||
instructions: "You are an expert in chemistry. You answer questions from a chemistry perspective.",
|
||||
description: "An expert in chemistry");
|
||||
|
||||
// Create a monitor to capturing agent responses (via ResponseCallback)
|
||||
// to display at the end of this sample. (optional)
|
||||
// NOTE: Create your own callback to capture responses in your application or service.
|
||||
OrchestrationMonitor monitor = new();
|
||||
|
||||
// Define the orchestration
|
||||
ConcurrentOrchestration orchestration =
|
||||
new(physicist, chemist)
|
||||
{
|
||||
LoggerFactory = this.LoggerFactory,
|
||||
ResponseCallback = monitor.ResponseCallback,
|
||||
StreamingResponseCallback = streamedResponse ? monitor.StreamingResultCallback : null,
|
||||
};
|
||||
|
||||
// Start the runtime
|
||||
await using InProcessRuntime runtime = new();
|
||||
await runtime.StartAsync();
|
||||
|
||||
// Run the orchestration
|
||||
string input = "What is temperature?";
|
||||
Console.WriteLine($"\n# INPUT: {input}\n");
|
||||
OrchestrationResult<string[]> result = await orchestration.InvokeAsync(input, runtime);
|
||||
|
||||
string[] output = await result.GetValueAsync(TimeSpan.FromSeconds(ResultTimeoutInSeconds));
|
||||
Console.WriteLine($"\n# RESULT:\n{string.Join("\n\n", output.Select(text => $"{text}"))}");
|
||||
|
||||
await runtime.RunUntilIdleAsync();
|
||||
|
||||
this.DisplayHistory(monitor.History);
|
||||
}
|
||||
}
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json;
|
||||
using Microsoft.Agents.Orchestration;
|
||||
using Microsoft.Agents.Orchestration.Concurrent;
|
||||
using Microsoft.Agents.Orchestration.Transforms;
|
||||
using Microsoft.Extensions.AI.Agents;
|
||||
using Microsoft.SemanticKernel.Agents.Runtime.InProcess;
|
||||
using Microsoft.Shared.Samples;
|
||||
|
||||
namespace Orchestration;
|
||||
|
||||
/// <summary>
|
||||
/// Demonstrates how to use the <see cref="ConcurrentOrchestration"/> with structured output.
|
||||
/// </summary>
|
||||
public class ConcurrentOrchestration_With_StructuredOutput(ITestOutputHelper output) : OrchestrationSample(output)
|
||||
{
|
||||
private static readonly JsonSerializerOptions s_options = new() { WriteIndented = true };
|
||||
|
||||
[Fact]
|
||||
public async Task RunOrchestrationAsync()
|
||||
{
|
||||
// Define the agents
|
||||
ChatClientAgent agent1 =
|
||||
this.CreateAgent(
|
||||
instructions: "You are an expert in identifying themes in articles. Given an article, identify the main themes.",
|
||||
description: "An expert in identifying themes in articles");
|
||||
ChatClientAgent agent2 =
|
||||
this.CreateAgent(
|
||||
instructions: "You are an expert in sentiment analysis. Given an article, identify the sentiment.",
|
||||
description: "An expert in sentiment analysis");
|
||||
ChatClientAgent agent3 =
|
||||
this.CreateAgent(
|
||||
instructions: "You are an expert in entity recognition. Given an article, extract the entities.",
|
||||
description: "An expert in entity recognition");
|
||||
|
||||
// Define the orchestration with transform
|
||||
StructuredOutputTransform<Analysis> outputTransform = new(this.CreateChatClient());
|
||||
ConcurrentOrchestration<string, Analysis> orchestration =
|
||||
new(agent1, agent2, agent3)
|
||||
{
|
||||
LoggerFactory = this.LoggerFactory,
|
||||
ResultTransform = outputTransform.TransformAsync,
|
||||
};
|
||||
|
||||
// Start the runtime
|
||||
await using InProcessRuntime runtime = new();
|
||||
await runtime.StartAsync();
|
||||
|
||||
// Run the orchestration
|
||||
const string resourceId = "Hamlet_full_play_summary.txt";
|
||||
string input = Resources.Read(resourceId);
|
||||
Console.WriteLine($"\n# INPUT: @{resourceId}\n");
|
||||
OrchestrationResult<Analysis> result = await orchestration.InvokeAsync(input, runtime);
|
||||
|
||||
Analysis output = await result.GetValueAsync(TimeSpan.FromSeconds(ResultTimeoutInSeconds * 2));
|
||||
Console.WriteLine($"\n# RESULT:\n{JsonSerializer.Serialize(output, s_options)}");
|
||||
|
||||
await runtime.RunUntilIdleAsync();
|
||||
}
|
||||
|
||||
#pragma warning disable CA1812 // Avoid uninstantiated internal classes
|
||||
private sealed class Analysis
|
||||
{
|
||||
public IList<string> Themes { get; set; } = [];
|
||||
public IList<string> Sentiments { get; set; } = [];
|
||||
public IList<string> Entities { get; set; } = [];
|
||||
}
|
||||
#pragma warning restore CA1812 // Avoid uninstantiated internal classes
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Agents.Orchestration;
|
||||
using Microsoft.Agents.Orchestration.GroupChat;
|
||||
using Microsoft.Extensions.AI.Agents;
|
||||
using Microsoft.SemanticKernel.Agents.Runtime.InProcess;
|
||||
|
||||
namespace Orchestration;
|
||||
|
||||
/// <summary>
|
||||
/// Demonstrates how to use the <see cref="GroupChatOrchestration"/> ith a default
|
||||
/// round robin manager for controlling the flow of conversation in a round robin fashion.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Think of the group chat manager as a state machine, with the following possible states:
|
||||
/// - Request for user message
|
||||
/// - Termination, after which the manager will try to filter a result from the conversation
|
||||
/// - Continuation, at which the manager will select the next agent to speak.
|
||||
/// </remarks>
|
||||
public class GroupChatOrchestration_Intro(ITestOutputHelper output) : OrchestrationSample(output)
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public async Task RunOrchestrationAsync(bool streamedResponse)
|
||||
{
|
||||
// Define the agents
|
||||
ChatClientAgent writer =
|
||||
this.CreateAgent(
|
||||
name: "CopyWriter",
|
||||
description: "A copy writer",
|
||||
instructions:
|
||||
"""
|
||||
You are a copywriter with ten years of experience and are known for brevity and a dry humor.
|
||||
The goal is to refine and decide on the single best copy as an expert in the field.
|
||||
Only provide a single proposal per response.
|
||||
You're laser focused on the goal at hand.
|
||||
Don't waste time with chit chat.
|
||||
Consider suggestions when refining an idea.
|
||||
""");
|
||||
ChatClientAgent editor =
|
||||
this.CreateAgent(
|
||||
name: "Reviewer",
|
||||
description: "An editor.",
|
||||
instructions:
|
||||
"""
|
||||
You are an art director who has opinions about copywriting born of a love for David Ogilvy.
|
||||
The goal is to determine if the given copy is acceptable to print.
|
||||
If so, state that it is approved.
|
||||
If not, provide insight on how to refine suggested copy without example.
|
||||
""");
|
||||
|
||||
// Create a monitor to capturing agent responses (via ResponseCallback)
|
||||
// to display at the end of this sample. (optional)
|
||||
// NOTE: Create your own callback to capture responses in your application or service.
|
||||
OrchestrationMonitor monitor = new();
|
||||
// Define the orchestration
|
||||
GroupChatOrchestration orchestration =
|
||||
new(new RoundRobinGroupChatManager()
|
||||
{
|
||||
MaximumInvocationCount = 5
|
||||
},
|
||||
writer,
|
||||
editor)
|
||||
{
|
||||
LoggerFactory = this.LoggerFactory,
|
||||
ResponseCallback = monitor.ResponseCallback,
|
||||
StreamingResponseCallback = streamedResponse ? monitor.StreamingResultCallback : null,
|
||||
};
|
||||
|
||||
// Start the runtime
|
||||
await using InProcessRuntime runtime = new();
|
||||
await runtime.StartAsync();
|
||||
|
||||
string input = "Create a slogon for a new eletric SUV that is affordable and fun to drive.";
|
||||
Console.WriteLine($"\n# INPUT: {input}\n");
|
||||
OrchestrationResult<string> result = await orchestration.InvokeAsync(input, runtime);
|
||||
string text = await result.GetValueAsync(TimeSpan.FromSeconds(ResultTimeoutInSeconds * 3));
|
||||
Console.WriteLine($"\n# RESULT: {text}");
|
||||
|
||||
await runtime.RunUntilIdleAsync();
|
||||
|
||||
this.DisplayHistory(monitor.History);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Agents.Orchestration;
|
||||
using Microsoft.Agents.Orchestration.GroupChat;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.AI.Agents;
|
||||
using Microsoft.SemanticKernel.Agents.Runtime.InProcess;
|
||||
|
||||
namespace Orchestration;
|
||||
|
||||
/// <summary>
|
||||
/// Demonstrates how to use the <see cref="GroupChatOrchestration"/>
|
||||
/// with a group chat manager that uses a chat completion service to
|
||||
/// control the flow of the conversation.
|
||||
/// </summary>
|
||||
public class GroupChatOrchestration_With_AIManager(ITestOutputHelper output) : OrchestrationSample(output)
|
||||
{
|
||||
[Fact]
|
||||
public async Task RunOrchestrationAsync()
|
||||
{
|
||||
// Define the agents
|
||||
ChatClientAgent farmer =
|
||||
this.CreateAgent(
|
||||
name: "Farmer",
|
||||
description: "A rural farmer from Southeast Asia.",
|
||||
instructions:
|
||||
"""
|
||||
You're a farmer from Southeast Asia.
|
||||
Your life is deeply connected to land and family.
|
||||
You value tradition and sustainability.
|
||||
You are in a debate. Feel free to challenge the other participants with respect.
|
||||
""");
|
||||
ChatClientAgent developer =
|
||||
this.CreateAgent(
|
||||
name: "Developer",
|
||||
description: "An urban software developer from the United States.",
|
||||
instructions:
|
||||
"""
|
||||
You're a software developer from the United States.
|
||||
Your life is fast-paced and technology-driven.
|
||||
You value innovation, freedom, and work-life balance.
|
||||
You are in a debate. Feel free to challenge the other participants with respect.
|
||||
""");
|
||||
ChatClientAgent teacher =
|
||||
this.CreateAgent(
|
||||
name: "Teacher",
|
||||
description: "A retired history teacher from Eastern Europe",
|
||||
instructions:
|
||||
"""
|
||||
You're a retired history teacher from Eastern Europe.
|
||||
You bring historical and philosophical perspectives to discussions.
|
||||
You value legacy, learning, and cultural continuity.
|
||||
You are in a debate. Feel free to challenge the other participants with respect.
|
||||
""");
|
||||
ChatClientAgent activist =
|
||||
this.CreateAgent(
|
||||
name: "Activist",
|
||||
description: "A young activist from South America.",
|
||||
instructions:
|
||||
"""
|
||||
You're a young activist from South America.
|
||||
You focus on social justice, environmental rights, and generational change.
|
||||
You are in a debate. Feel free to challenge the other participants with respect.
|
||||
""");
|
||||
ChatClientAgent spiritual =
|
||||
this.CreateAgent(
|
||||
name: "SpiritualLeader",
|
||||
description: "A spiritual leader from the Middle East.",
|
||||
instructions:
|
||||
"""
|
||||
You're a spiritual leader from the Middle East.
|
||||
You provide insights grounded in religion, morality, and community service.
|
||||
You are in a debate. Feel free to challenge the other participants with respect.
|
||||
""");
|
||||
ChatClientAgent artist =
|
||||
this.CreateAgent(
|
||||
name: "Artist",
|
||||
description: "An artist from Africa.",
|
||||
instructions:
|
||||
"""
|
||||
You're an artist from Africa.
|
||||
You view life through creative expression, storytelling, and collective memory.
|
||||
You are in a debate. Feel free to challenge the other participants with respect.
|
||||
""");
|
||||
ChatClientAgent immigrant =
|
||||
this.CreateAgent(
|
||||
name: "Immigrant",
|
||||
description: "An immigrant entrepreneur from Asia living in Canada.",
|
||||
instructions:
|
||||
"""
|
||||
You're an immigrant entrepreneur from Asia living in Canada.
|
||||
You balance trandition with adaption.
|
||||
You focus on family success, risk, and opportunity.
|
||||
You are in a debate. Feel free to challenge the other participants with respect.
|
||||
""");
|
||||
ChatClientAgent doctor =
|
||||
this.CreateAgent(
|
||||
name: "Doctor",
|
||||
description: "A doctor from Scandinavia.",
|
||||
instructions:
|
||||
"""
|
||||
You're a doctor from Scandinavia.
|
||||
Your perspective is shaped by public health, equity, and structured societal support.
|
||||
You are in a debate. Feel free to challenge the other participants with respect.
|
||||
""");
|
||||
|
||||
// Create a monitor to capturing agent responses (via ResponseCallback)
|
||||
// to display at the end of this sample. (optional)
|
||||
// NOTE: Create your own callback to capture responses in your application or service.
|
||||
OrchestrationMonitor monitor = new();
|
||||
|
||||
// Define the orchestration
|
||||
const string topic = "What does a good life mean to you personally?";
|
||||
GroupChatOrchestration orchestration =
|
||||
new(
|
||||
new AIGroupChatManager(
|
||||
topic,
|
||||
this.CreateChatClient())
|
||||
{
|
||||
MaximumInvocationCount = 5
|
||||
},
|
||||
farmer,
|
||||
developer,
|
||||
teacher,
|
||||
activist,
|
||||
spiritual,
|
||||
artist,
|
||||
immigrant,
|
||||
doctor)
|
||||
{
|
||||
LoggerFactory = this.LoggerFactory,
|
||||
ResponseCallback = monitor.ResponseCallback,
|
||||
};
|
||||
|
||||
// Start the runtime
|
||||
await using InProcessRuntime runtime = new();
|
||||
await runtime.StartAsync();
|
||||
|
||||
// Run the orchestration
|
||||
Console.WriteLine($"\n# INPUT: {topic}\n");
|
||||
OrchestrationResult<string> result = await orchestration.InvokeAsync(topic, runtime);
|
||||
string text = await result.GetValueAsync(TimeSpan.FromSeconds(ResultTimeoutInSeconds * 3));
|
||||
Console.WriteLine($"\n# RESULT: {text}");
|
||||
|
||||
await runtime.RunUntilIdleAsync();
|
||||
|
||||
this.DisplayHistory(monitor.History);
|
||||
}
|
||||
|
||||
private sealed class AIGroupChatManager(string topic, IChatClient chatClient) : GroupChatManager
|
||||
{
|
||||
private static class Prompts
|
||||
{
|
||||
public static string Termination(string topic) =>
|
||||
$"""
|
||||
You are mediator that guides a discussion on the topic of '{topic}'.
|
||||
You need to determine if the discussion has reached a conclusion.
|
||||
If you would like to end the discussion, please respond with True. Otherwise, respond with False.
|
||||
""";
|
||||
|
||||
public static string Selection(string topic, string participants) =>
|
||||
$"""
|
||||
You are mediator that guides a discussion on the topic of '{topic}'.
|
||||
You need to select the next participant to speak.
|
||||
Here are the names and descriptions of the participants:
|
||||
{participants}\n
|
||||
Please respond with only the name of the participant you would like to select.
|
||||
""";
|
||||
|
||||
public static string Filter(string topic) =>
|
||||
$"""
|
||||
You are mediator that guides a discussion on the topic of '{topic}'.
|
||||
You have just concluded the discussion.
|
||||
Please summarize the discussion and provide a closing statement.
|
||||
""";
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override ValueTask<GroupChatManagerResult<string>> FilterResults(IReadOnlyCollection<ChatMessage> history, CancellationToken cancellationToken = default) =>
|
||||
this.GetResponseAsync<string>(history, Prompts.Filter(topic), cancellationToken);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override ValueTask<GroupChatManagerResult<string>> SelectNextAgent(IReadOnlyCollection<ChatMessage> history, GroupChatTeam team, CancellationToken cancellationToken = default) =>
|
||||
this.GetResponseAsync<string>(history, Prompts.Selection(topic, team.FormatList()), cancellationToken);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override ValueTask<GroupChatManagerResult<bool>> ShouldRequestUserInput(IReadOnlyCollection<ChatMessage> history, CancellationToken cancellationToken = default) =>
|
||||
new(new GroupChatManagerResult<bool>(false) { Reason = "The AI group chat manager does not request user input." });
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async ValueTask<GroupChatManagerResult<bool>> ShouldTerminate(IReadOnlyCollection<ChatMessage> history, CancellationToken cancellationToken = default)
|
||||
{
|
||||
GroupChatManagerResult<bool> result = await base.ShouldTerminate(history, cancellationToken);
|
||||
if (!result.Value)
|
||||
{
|
||||
result = await this.GetResponseAsync<bool>(history, Prompts.Termination(topic), cancellationToken);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private async ValueTask<GroupChatManagerResult<TValue>> GetResponseAsync<TValue>(IReadOnlyCollection<ChatMessage> history, string prompt, CancellationToken cancellationToken = default)
|
||||
{
|
||||
ChatResponse<GroupChatManagerResult<TValue>> response = await chatClient.GetResponseAsync<GroupChatManagerResult<TValue>>([.. history, new ChatMessage(ChatRole.System, prompt)], new ChatOptions { ToolMode = ChatToolMode.Auto }, useJsonSchemaResponseFormat: true, cancellationToken);
|
||||
return response.Result;
|
||||
}
|
||||
}
|
||||
}
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Agents.Orchestration;
|
||||
using Microsoft.Agents.Orchestration.GroupChat;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.AI.Agents;
|
||||
using Microsoft.SemanticKernel.Agents.Runtime.InProcess;
|
||||
|
||||
namespace Orchestration;
|
||||
|
||||
/// <summary>
|
||||
/// Demonstrates how to use the <see cref="GroupChatOrchestration"/> with human in the loop
|
||||
/// </summary>
|
||||
public class GroupChatOrchestration_With_HumanInTheLoop(ITestOutputHelper output) : OrchestrationSample(output)
|
||||
{
|
||||
[Fact]
|
||||
public async Task RunOrchestrationAsync()
|
||||
{
|
||||
// Define the agents
|
||||
ChatClientAgent writer =
|
||||
this.CreateAgent(
|
||||
name: "CopyWriter",
|
||||
description: "A copy writer",
|
||||
instructions:
|
||||
"""
|
||||
You are a copywriter with ten years of experience and are known for brevity and a dry humor.
|
||||
The goal is to refine and decide on the single best copy as an expert in the field.
|
||||
Only provide a single proposal per response.
|
||||
You're laser focused on the goal at hand.
|
||||
Don't waste time with chit chat.
|
||||
Consider suggestions when refining an idea.
|
||||
""");
|
||||
ChatClientAgent editor =
|
||||
this.CreateAgent(
|
||||
name: "Reviewer",
|
||||
description: "An editor.",
|
||||
instructions:
|
||||
"""
|
||||
You are an art director who has opinions about copywriting born of a love for David Ogilvy.
|
||||
The goal is to determine if the given copy is acceptable to print.
|
||||
If so, state that it is approved.
|
||||
If not, provide insight on how to refine suggested copy without example.
|
||||
""");
|
||||
|
||||
// Create a monitor to capturing agent responses (via ResponseCallback)
|
||||
// to display at the end of this sample. (optional)
|
||||
// NOTE: Create your own callback to capture responses in your application or service.
|
||||
OrchestrationMonitor monitor = new();
|
||||
|
||||
// Define the orchestration
|
||||
GroupChatOrchestration orchestration =
|
||||
new(
|
||||
new CustomRoundRobinGroupChatManager()
|
||||
{
|
||||
MaximumInvocationCount = 5,
|
||||
InteractiveCallback = () =>
|
||||
{
|
||||
ChatMessage input = new(ChatRole.User, "I like it");
|
||||
monitor.History.Add(input);
|
||||
Console.WriteLine($"\n# INPUT: {input.Text}\n");
|
||||
return new ValueTask<ChatMessage>(input);
|
||||
}
|
||||
},
|
||||
writer,
|
||||
editor)
|
||||
{
|
||||
LoggerFactory = this.LoggerFactory,
|
||||
ResponseCallback = monitor.ResponseCallback,
|
||||
};
|
||||
|
||||
// Start the runtime
|
||||
await using InProcessRuntime runtime = new();
|
||||
await runtime.StartAsync();
|
||||
|
||||
// Run the orchestration
|
||||
string input = "Create a slogon for a new eletric SUV that is affordable and fun to drive.";
|
||||
Console.WriteLine($"\n# INPUT: {input}\n");
|
||||
OrchestrationResult<string> result = await orchestration.InvokeAsync(input, runtime);
|
||||
string text = await result.GetValueAsync(TimeSpan.FromSeconds(ResultTimeoutInSeconds * 3));
|
||||
Console.WriteLine($"\n# RESULT: {text}");
|
||||
|
||||
await runtime.RunUntilIdleAsync();
|
||||
|
||||
this.DisplayHistory(monitor.History);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Define a custom group chat manager that enables user input.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// User input is achieved by overriding the default round robin manager
|
||||
/// to allow user input after the reviewer agent's message.
|
||||
/// </remarks>
|
||||
private sealed class CustomRoundRobinGroupChatManager : RoundRobinGroupChatManager
|
||||
{
|
||||
public override ValueTask<GroupChatManagerResult<bool>> ShouldRequestUserInput(IReadOnlyCollection<ChatMessage> history, CancellationToken cancellationToken = default)
|
||||
{
|
||||
string? lastAgent = history.LastOrDefault()?.AuthorName;
|
||||
|
||||
GroupChatManagerResult<bool> result;
|
||||
|
||||
if (lastAgent is null)
|
||||
{
|
||||
result = new GroupChatManagerResult<bool>(false) { Reason = "No agents have spoken yet." };
|
||||
}
|
||||
|
||||
if (lastAgent == "Reviewer")
|
||||
{
|
||||
result = new GroupChatManagerResult<bool>(true) { Reason = "User input is needed after the reviewer's message." };
|
||||
}
|
||||
else
|
||||
{
|
||||
result = new GroupChatManagerResult<bool>(false) { Reason = "User input is not needed until the reviewer's message." };
|
||||
}
|
||||
|
||||
return new ValueTask<GroupChatManagerResult<bool>>(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Agents.Orchestration;
|
||||
using Microsoft.Agents.Orchestration.Handoff;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.AI.Agents;
|
||||
using Microsoft.SemanticKernel.Agents.Runtime.InProcess;
|
||||
|
||||
namespace Orchestration;
|
||||
|
||||
/// <summary>
|
||||
/// Demonstrates how to use the <see cref="HandoffOrchestration"/> that represents
|
||||
/// a customer support triage system.The orchestration consists of 4 agents, each specialized
|
||||
/// in a different area of customer support: triage, refunds, order status, and order returns.
|
||||
/// </summary>
|
||||
public class HandoffOrchestration_Intro(ITestOutputHelper output) : OrchestrationSample(output)
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public async Task RunOrchestrationAsync(bool streamedResponse)
|
||||
{
|
||||
// Define the agents & tools
|
||||
ChatClientAgent triageAgent =
|
||||
this.CreateAgent(
|
||||
instructions: "A customer support agent that triages issues.",
|
||||
name: "TriageAgent",
|
||||
description: "Handle customer requests.");
|
||||
ChatClientAgent statusAgent =
|
||||
this.CreateAgent(
|
||||
name: "OrderStatusAgent",
|
||||
instructions: "Handle order status requests.",
|
||||
description: "A customer support agent that checks order status.",
|
||||
functions: AIFunctionFactory.Create(OrderFunctions.CheckOrderStatus));
|
||||
ChatClientAgent returnAgent =
|
||||
this.CreateAgent(
|
||||
name: "OrderReturnAgent",
|
||||
instructions: "Handle order return requests.",
|
||||
description: "A customer support agent that handles order returns.",
|
||||
functions: AIFunctionFactory.Create(OrderFunctions.ProcessReturn));
|
||||
ChatClientAgent refundAgent =
|
||||
this.CreateAgent(
|
||||
name: "OrderRefundAgent",
|
||||
instructions: "Handle order refund requests.",
|
||||
description: "A customer support agent that handles order refund.",
|
||||
functions: AIFunctionFactory.Create(OrderFunctions.ProcessRefund));
|
||||
|
||||
// Create a monitor to capturing agent responses (via ResponseCallback)
|
||||
// to display at the end of this sample. (optional)
|
||||
// NOTE: Create your own callback to capture responses in your application or service.
|
||||
OrchestrationMonitor monitor = new();
|
||||
// Define user responses for InteractiveCallback (since sample is not interactive)
|
||||
Queue<string> responses = new();
|
||||
string task = "I am a customer that needs help with my orders";
|
||||
responses.Enqueue("I'd like to track the status of my order");
|
||||
responses.Enqueue("My order ID is 123");
|
||||
responses.Enqueue("I want to return another order of mine");
|
||||
responses.Enqueue("Order ID 321");
|
||||
responses.Enqueue("Broken item");
|
||||
responses.Enqueue("No, bye");
|
||||
// Define the orchestration
|
||||
HandoffOrchestration orchestration =
|
||||
new(OrchestrationHandoffs
|
||||
.StartWith(triageAgent)
|
||||
.Add(triageAgent, statusAgent, returnAgent, refundAgent)
|
||||
.Add(statusAgent, triageAgent, "Transfer to this agent if the issue is not status related")
|
||||
.Add(returnAgent, triageAgent, "Transfer to this agent if the issue is not return related")
|
||||
.Add(refundAgent, triageAgent, "Transfer to this agent if the issue is not refund related"),
|
||||
triageAgent,
|
||||
statusAgent,
|
||||
returnAgent,
|
||||
refundAgent)
|
||||
{
|
||||
InteractiveCallback = () =>
|
||||
{
|
||||
string text = responses.Dequeue();
|
||||
ChatMessage input = new(ChatRole.User, text);
|
||||
monitor.History.Add(input);
|
||||
Console.WriteLine($"\n# INPUT: {input.Text}\n");
|
||||
return new(input);
|
||||
},
|
||||
LoggerFactory = this.LoggerFactory,
|
||||
ResponseCallback = monitor.ResponseCallback,
|
||||
StreamingResponseCallback = streamedResponse ? monitor.StreamingResultCallback : null,
|
||||
};
|
||||
|
||||
// Start the runtime
|
||||
await using InProcessRuntime runtime = new();
|
||||
await runtime.StartAsync();
|
||||
|
||||
// Run the orchestration
|
||||
Console.WriteLine($"\n# INPUT:\n{task}\n");
|
||||
OrchestrationResult<string> result = await orchestration.InvokeAsync(task, runtime);
|
||||
|
||||
string text = await result.GetValueAsync(TimeSpan.FromSeconds(300));
|
||||
Console.WriteLine($"\n# RESULT: {text}");
|
||||
|
||||
await runtime.RunUntilIdleAsync();
|
||||
|
||||
this.DisplayHistory(monitor.History);
|
||||
}
|
||||
|
||||
private static class OrderFunctions
|
||||
{
|
||||
public static string CheckOrderStatus(string orderId) => $"Order {orderId} is shipped and will arrive in 2-3 days.";
|
||||
public static string ProcessReturn(string orderId, string reason) => $"Return for order {orderId} has been processed successfully.";
|
||||
public static string ProcessRefund(string orderId, string reason) => $"Refund for order {orderId} has been processed successfully.";
|
||||
}
|
||||
}
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.Agents.Orchestration;
|
||||
using Microsoft.Agents.Orchestration.Handoff;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.AI.Agents;
|
||||
using Microsoft.SemanticKernel.Agents.Runtime.InProcess;
|
||||
|
||||
namespace Orchestration;
|
||||
|
||||
/// <summary>
|
||||
/// Demonstrates how to use the <see cref="HandoffOrchestration"/>.
|
||||
/// </summary>
|
||||
public class HandoffOrchestration_With_StructuredInput(ITestOutputHelper output) : OrchestrationSample(output)
|
||||
{
|
||||
[Fact]
|
||||
public async Task RunOrchestrationAsync()
|
||||
{
|
||||
// Initialize plugin
|
||||
GithubPlugin githubPlugin = new();
|
||||
AIFunction githubAddLabelFunction = AIFunctionFactory.Create(githubPlugin.AddLabels);
|
||||
|
||||
// Define the agents
|
||||
ChatClientAgent triageAgent =
|
||||
this.CreateAgent(
|
||||
instructions: "Given a GitHub issue, triage it.",
|
||||
name: "TriageAgent",
|
||||
description: "An agent that triages GitHub issues");
|
||||
ChatClientAgent pythonAgent =
|
||||
this.CreateAgent(
|
||||
instructions: "You are an agent that handles Python related GitHub issues.",
|
||||
name: "PythonAgent",
|
||||
description: "An agent that handles Python related issues",
|
||||
functions: githubAddLabelFunction);
|
||||
ChatClientAgent dotnetAgent =
|
||||
this.CreateAgent(
|
||||
instructions: "You are an agent that handles .NET related GitHub issues.",
|
||||
name: "DotNetAgent",
|
||||
description: "An agent that handles .NET related issues",
|
||||
functions: githubAddLabelFunction);
|
||||
|
||||
// Create a monitor to capturing agent responses (via ResponseCallback)
|
||||
// to display at the end of this sample. (optional)
|
||||
// NOTE: Create your own callback to capture responses in your application or service.
|
||||
OrchestrationMonitor monitor = new();
|
||||
|
||||
// Define the orchestration
|
||||
HandoffOrchestration<GithubIssue, string> orchestration =
|
||||
new(OrchestrationHandoffs
|
||||
.StartWith(triageAgent)
|
||||
.Add(triageAgent, dotnetAgent, pythonAgent),
|
||||
triageAgent,
|
||||
pythonAgent,
|
||||
dotnetAgent)
|
||||
{
|
||||
LoggerFactory = this.LoggerFactory,
|
||||
ResponseCallback = monitor.ResponseCallback,
|
||||
};
|
||||
|
||||
GithubIssue input =
|
||||
new()
|
||||
{
|
||||
Id = "12345",
|
||||
Title = "Bug: SQLite Error 1: 'ambiguous column name:' when including VectorStoreRecordKey in VectorSearchOptions.Filter",
|
||||
Body =
|
||||
"""
|
||||
Describe the bug
|
||||
When using column names marked as [VectorStoreRecordData(IsFilterable = true)] in VectorSearchOptions.Filter, the query runs correctly.
|
||||
However, using the column name marked as [VectorStoreRecordKey] in VectorSearchOptions.Filter, the query throws exception 'SQLite Error 1: ambiguous column name: StartUTC'.
|
||||
To Reproduce
|
||||
Add a filter for the column marked [VectorStoreRecordKey]. Since that same column exists in both the vec_TestTable and TestTable, the data for both columns cannot be returned.
|
||||
|
||||
Expected behavior
|
||||
The query should explicitly list the vec_TestTable column names to retrieve and should omit the [VectorStoreRecordKey] column since it will be included in the primary TestTable columns.
|
||||
|
||||
Platform
|
||||
Microsoft.SemanticKernel.Connectors.Sqlite v1.46.0-preview
|
||||
|
||||
Additional context
|
||||
Normal DBContext logging shows only normal context queries. Queries run by VectorizedSearchAsync() don't appear in those logs and I could not find a way to enable logging in semantic search so that I could actually see the exact query that is failing. It would have been very useful to see the failing semantic query.
|
||||
""",
|
||||
Labels = []
|
||||
};
|
||||
|
||||
// Start the runtime
|
||||
await using InProcessRuntime runtime = new();
|
||||
await runtime.StartAsync();
|
||||
|
||||
// Run the orchestration
|
||||
Console.WriteLine($"\n# INPUT:\n{input.Id}: {input.Title}\n");
|
||||
OrchestrationResult<string> result = await orchestration.InvokeAsync(input, runtime);
|
||||
string text = await result.GetValueAsync(TimeSpan.FromSeconds(ResultTimeoutInSeconds));
|
||||
Console.WriteLine($"\n# RESULT: {text}");
|
||||
Console.WriteLine($"\n# LABELS: {string.Join(",", githubPlugin.Labels["12345"])}\n");
|
||||
|
||||
await runtime.RunUntilIdleAsync();
|
||||
}
|
||||
|
||||
private sealed class GithubIssue
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public string Id { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("title")]
|
||||
public string Title { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("body")]
|
||||
public string Body { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("labels")]
|
||||
public string[] Labels { get; set; } = [];
|
||||
}
|
||||
|
||||
private sealed class GithubPlugin
|
||||
{
|
||||
public Dictionary<string, string[]> Labels { get; } = [];
|
||||
|
||||
public void AddLabels(string issueId, params string[] labels)
|
||||
{
|
||||
this.Labels[issueId] = labels;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Agents.Orchestration;
|
||||
using Microsoft.Agents.Orchestration.Sequential;
|
||||
using Microsoft.Extensions.AI.Agents;
|
||||
using Microsoft.SemanticKernel.Agents.Runtime.InProcess;
|
||||
|
||||
namespace Orchestration;
|
||||
|
||||
/// <summary>
|
||||
/// Demonstrates how to use the <see cref="SequentialOrchestration"/> for
|
||||
/// executing multiple agents in sequence, i.e.the output of one agent is
|
||||
/// the input to the next agent.
|
||||
/// </summary>
|
||||
public class SequentialOrchestration_Intro(ITestOutputHelper output) : OrchestrationSample(output)
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public async Task RunOrchestrationAsync(bool streamedResponse)
|
||||
{
|
||||
// Define the agents
|
||||
ChatClientAgent analystAgent =
|
||||
this.CreateAgent(
|
||||
name: "Analyst",
|
||||
instructions:
|
||||
"""
|
||||
You are a marketing analyst. Given a product description, identify:
|
||||
- Key features
|
||||
- Target audience
|
||||
- Unique selling points
|
||||
""",
|
||||
description: "A agent that extracts key concepts from a product description.");
|
||||
ChatClientAgent writerAgent =
|
||||
this.CreateAgent(
|
||||
name: "copywriter",
|
||||
instructions:
|
||||
"""
|
||||
You are a marketing copywriter. Given a block of text describing features, audience, and USPs,
|
||||
compose a compelling marketing copy (like a newsletter section) that highlights these points.
|
||||
Output should be short (around 150 words), output just the copy as a single text block.
|
||||
""",
|
||||
description: "An agent that writes a marketing copy based on the extracted concepts.");
|
||||
ChatClientAgent editorAgent =
|
||||
this.CreateAgent(
|
||||
name: "editor",
|
||||
instructions:
|
||||
"""
|
||||
You are an editor. Given the draft copy, correct grammar, improve clarity, ensure consistent tone,
|
||||
give format and make it polished. Output the final improved copy as a single text block.
|
||||
""",
|
||||
description: "An agent that formats and proofreads the marketing copy.");
|
||||
|
||||
// Create a monitor to capturing agent responses (via ResponseCallback)
|
||||
// to display at the end of this sample. (optional)
|
||||
// NOTE: Create your own callback to capture responses in your application or service.
|
||||
OrchestrationMonitor monitor = new();
|
||||
// Define the orchestration
|
||||
SequentialOrchestration orchestration =
|
||||
new(analystAgent, writerAgent, editorAgent)
|
||||
{
|
||||
LoggerFactory = this.LoggerFactory,
|
||||
ResponseCallback = monitor.ResponseCallback,
|
||||
StreamingResponseCallback = streamedResponse ? monitor.StreamingResultCallback : null,
|
||||
};
|
||||
|
||||
// Start the runtime
|
||||
await using InProcessRuntime runtime = new();
|
||||
await runtime.StartAsync();
|
||||
|
||||
// Run the orchestration
|
||||
string input = "An eco-friendly stainless steel water bottle that keeps drinks cold for 24 hours";
|
||||
Console.WriteLine($"\n# INPUT: {input}\n");
|
||||
OrchestrationResult<string> result = await orchestration.InvokeAsync(input, runtime);
|
||||
string text = await result.GetValueAsync(TimeSpan.FromSeconds(ResultTimeoutInSeconds));
|
||||
Console.WriteLine($"\n# RESULT: {text}");
|
||||
|
||||
await runtime.RunUntilIdleAsync();
|
||||
|
||||
this.DisplayHistory(monitor.History);
|
||||
}
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Agents.Orchestration;
|
||||
using Microsoft.Agents.Orchestration.Sequential;
|
||||
using Microsoft.Extensions.AI.Agents;
|
||||
using Microsoft.SemanticKernel.Agents.Runtime.InProcess;
|
||||
|
||||
namespace Orchestration;
|
||||
|
||||
/// <summary>
|
||||
/// Demonstrates how to use cancel a <see cref="SequentialOrchestration"/> while its running.
|
||||
/// </summary>
|
||||
public class SequentialOrchestration_With_Cancellation(ITestOutputHelper output) : OrchestrationSample(output)
|
||||
{
|
||||
[Fact]
|
||||
public async Task RunOrchestrationAsync()
|
||||
{
|
||||
// Define the agents
|
||||
ChatClientAgent agent =
|
||||
this.CreateAgent(
|
||||
"""
|
||||
If the input message is a number, return the number incremented by one.
|
||||
""",
|
||||
description: "A agent that increments numbers.");
|
||||
|
||||
// Define the orchestration
|
||||
SequentialOrchestration orchestration = new(agent) { LoggerFactory = this.LoggerFactory };
|
||||
|
||||
// Start the runtime
|
||||
await using InProcessRuntime runtime = new();
|
||||
await runtime.StartAsync();
|
||||
|
||||
// Run the orchestration
|
||||
string input = "42";
|
||||
Console.WriteLine($"\n# INPUT: {input}\n");
|
||||
|
||||
OrchestrationResult<string> result = await orchestration.InvokeAsync(input, runtime);
|
||||
|
||||
result.Cancel();
|
||||
await Task.Delay(TimeSpan.FromSeconds(3));
|
||||
|
||||
try
|
||||
{
|
||||
string text = await result.GetValueAsync(TimeSpan.FromSeconds(ResultTimeoutInSeconds));
|
||||
Console.WriteLine($"\n# RESULT: {text}");
|
||||
}
|
||||
catch (AggregateException exception)
|
||||
{
|
||||
Console.WriteLine($"\n# CANCELLED: {exception.InnerException?.Message}");
|
||||
}
|
||||
|
||||
await runtime.RunUntilIdleAsync();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
On a dark winter night, a ghost walks the ramparts of Elsinore Castle in Denmark. Discovered first by a pair of watchmen, then by the scholar Horatio, the ghost resembles the recently deceased King Hamlet, whose brother Claudius has inherited the throne and married the king’s widow, Queen Gertrude. When Horatio and the watchmen bring Prince Hamlet, the son of Gertrude and the dead king, to see the ghost, it speaks to him, declaring ominously that it is indeed his father’s spirit, and that he was murdered by none other than Claudius. Ordering Hamlet to seek revenge on the man who usurped his throne and married his wife, the ghost disappears with the dawn.
|
||||
|
||||
Prince Hamlet devotes himself to avenging his father’s death, but, because he is contemplative and thoughtful by nature, he delays, entering into a deep melancholy and even apparent madness. Claudius and Gertrude worry about the prince’s erratic behavior and attempt to discover its cause. They employ a pair of Hamlet’s friends, Rosencrantz and Guildenstern, to watch him. When Polonius, the pompous Lord Chamberlain, suggests that Hamlet may be mad with love for his daughter, Ophelia, Claudius agrees to spy on Hamlet in conversation with the girl. But though Hamlet certainly seems mad, he does not seem to love Ophelia: he orders her to enter a nunnery and declares that he wishes to ban marriages.
|
||||
|
||||
A group of traveling actors comes to Elsinore, and Hamlet seizes upon an idea to test his uncle’s guilt. He will have the players perform a scene closely resembling the sequence by which Hamlet imagines his uncle to have murdered his father, so that if Claudius is guilty, he will surely react. When the moment of the murder arrives in the theater, Claudius leaps up and leaves the room. Hamlet and Horatio agree that this proves his guilt. Hamlet goes to kill Claudius but finds him praying. Since he believes that killing Claudius while in prayer would send Claudius’s soul to heaven, Hamlet considers that it would be an inadequate revenge and decides to wait. Claudius, now frightened of Hamlet’s madness and fearing for his own safety, orders that Hamlet be sent to England at once.
|
||||
|
||||
Hamlet goes to confront his mother, in whose bedchamber Polonius has hidden behind a tapestry. Hearing a noise from behind the tapestry, Hamlet believes the king is hiding there. He draws his sword and stabs through the fabric, killing Polonius. For this crime, he is immediately dispatched to England with Rosencrantz and Guildenstern. However, Claudius’s plan for Hamlet includes more than banishment, as he has given Rosencrantz and Guildenstern sealed orders for the King of England demanding that Hamlet be put to death.
|
||||
|
||||
In the aftermath of her father’s death, Ophelia goes mad with grief and drowns in the river. Polonius’s son, Laertes, who has been staying in France, returns to Denmark in a rage. Claudius convinces him that Hamlet is to blame for his father’s and sister’s deaths. When Horatio and the king receive letters from Hamlet indicating that the prince has returned to Denmark after pirates attacked his ship en route to England, Claudius concocts a plan to use Laertes’ desire for revenge to secure Hamlet’s death. Laertes will fence with Hamlet in innocent sport, but Claudius will poison Laertes’ blade so that if he draws blood, Hamlet will die. As a backup plan, the king decides to poison a goblet, which he will give Hamlet to drink should Hamlet score the first or second hits of the match. Hamlet returns to the vicinity of Elsinore just as Ophelia’s funeral is taking place. Stricken with grief, he attacks Laertes and declares that he had in fact always loved Ophelia. Back at the castle, he tells Horatio that he believes one must be prepared to die, since death can come at any moment. A foolish courtier named Osric arrives on Claudius’s orders to arrange the fencing match between Hamlet and Laertes.
|
||||
|
||||
The sword-fighting begins. Hamlet scores the first hit, but declines to drink from the king’s proffered goblet. Instead, Gertrude takes a drink from it and is swiftly killed by the poison. Laertes succeeds in wounding Hamlet, though Hamlet does not die of the poison immediately. First, Laertes is cut by his own sword’s blade, and, after revealing to Hamlet that Claudius is responsible for the queen’s death, he dies from the blade’s poison. Hamlet then stabs Claudius through with the poisoned sword and forces him to drink down the rest of the poisoned wine. Claudius dies, and Hamlet dies immediately after achieving his revenge.
|
||||
|
||||
At this moment, a Norwegian prince named Fortinbras, who has led an army to Denmark and attacked Poland earlier in the play, enters with ambassadors from England, who report that Rosencrantz and Guildenstern are dead. Fortinbras is stunned by the gruesome sight of the entire royal family lying sprawled on the floor dead. He moves to take power of the kingdom. Horatio, fulfilling Hamlet’s last request, tells him Hamlet’s tragic story. Fortinbras orders that Hamlet be carried away in a manner befitting a fallen soldier.
|
||||
Reference in New Issue
Block a user