From b4d1b8936abe50997f4135f7acd8403666fd5d90 Mon Sep 17 00:00:00 2001 From: Kazuki Ota Date: Tue, 18 Mar 2025 09:02:11 +0900 Subject: [PATCH 1/7] init From b0c9a9f58b42e10d610f5ed019286ac28a08d3a5 Mon Sep 17 00:00:00 2001 From: Kazuki Ota Date: Tue, 18 Mar 2025 10:10:17 +0900 Subject: [PATCH 2/7] =?UTF-8?q?Worker=20=E3=82=A8=E3=83=BC=E3=82=B8?= =?UTF-8?q?=E3=82=A7=E3=83=B3=E3=83=88=E3=81=8C=E5=A4=9A=E3=81=8F=E3=81=AA?= =?UTF-8?q?=E3=81=A3=E3=81=A6=E3=81=8D=E3=81=9F=E3=81=AE=E3=81=A7=E3=83=95?= =?UTF-8?q?=E3=82=A9=E3=83=AB=E3=83=80=E3=83=BC=E3=82=92=E5=88=86=E9=9B=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Agent/AgentDecider/AgentDeciderActivity.cs | 1 + .../Agent/AgentDecider/AgentDeciderPrompt.cs | 16 ++++++++-------- .../Agent/Synthesizer/SynthesizerPrompt.cs | 18 +++++++++--------- .../Agent/{ => Workers}/AgentDefinition.cs | 4 ++-- .../GetClimateAgent/GetClimateActivity.cs | 2 +- .../GetClimateAgent/GetClimateRequest.cs | 2 +- .../GetDestinationSuggestActivity.cs | 2 +- .../GetDestinationSuggestRequest.cs | 2 +- .../GetHotelAgent/GetHotelActivity.cs | 2 +- .../GetHotelAgent/GetHotelRequest.cs | 2 +- .../GetSightseeingSpotActivity.cs | 2 +- .../GetSightseeingSpotRequest.cs | 2 +- .../SubmitReservationActivity.cs | 2 +- .../SubmitReservationRequest.cs | 2 +- .../Model/SourceGenerationContext.cs | 10 +++++----- 15 files changed, 35 insertions(+), 34 deletions(-) rename DurableMultiAgentTemplate/Agent/{ => Workers}/AgentDefinition.cs (95%) rename DurableMultiAgentTemplate/Agent/{ => Workers}/GetClimateAgent/GetClimateActivity.cs (97%) rename DurableMultiAgentTemplate/Agent/{ => Workers}/GetClimateAgent/GetClimateRequest.cs (72%) rename DurableMultiAgentTemplate/Agent/{ => Workers}/GetDestinationSuggestAgent/GetDestinationSuggestActivity.cs (97%) rename DurableMultiAgentTemplate/Agent/{ => Workers}/GetDestinationSuggestAgent/GetDestinationSuggestRequest.cs (68%) rename DurableMultiAgentTemplate/Agent/{ => Workers}/GetHotelAgent/GetHotelActivity.cs (97%) rename DurableMultiAgentTemplate/Agent/{ => Workers}/GetHotelAgent/GetHotelRequest.cs (73%) rename DurableMultiAgentTemplate/Agent/{ => Workers}/GetSightseeingSpotAgent/GetSightseeingSpotActivity.cs (98%) rename DurableMultiAgentTemplate/Agent/{ => Workers}/GetSightseeingSpotAgent/GetSightseeingSpotRequest.cs (71%) rename DurableMultiAgentTemplate/Agent/{ => Workers}/SubmitReservationAgent/SubmitReservationActivity.cs (91%) rename DurableMultiAgentTemplate/Agent/{ => Workers}/SubmitReservationAgent/SubmitReservationRequest.cs (85%) diff --git a/DurableMultiAgentTemplate/Agent/AgentDecider/AgentDeciderActivity.cs b/DurableMultiAgentTemplate/Agent/AgentDecider/AgentDeciderActivity.cs index 7409c2d..86da8b5 100644 --- a/DurableMultiAgentTemplate/Agent/AgentDecider/AgentDeciderActivity.cs +++ b/DurableMultiAgentTemplate/Agent/AgentDecider/AgentDeciderActivity.cs @@ -4,6 +4,7 @@ using Microsoft.Extensions.Logging; using OpenAI.Chat; using DurableMultiAgentTemplate.Shared.Model; +using DurableMultiAgentTemplate.Agent.Workers; namespace DurableMultiAgentTemplate.Agent.AgentDecider; diff --git a/DurableMultiAgentTemplate/Agent/AgentDecider/AgentDeciderPrompt.cs b/DurableMultiAgentTemplate/Agent/AgentDecider/AgentDeciderPrompt.cs index 89d32d5..fc154f9 100644 --- a/DurableMultiAgentTemplate/Agent/AgentDecider/AgentDeciderPrompt.cs +++ b/DurableMultiAgentTemplate/Agent/AgentDecider/AgentDeciderPrompt.cs @@ -1,13 +1,13 @@ -namespace DurableMultiAgentTemplate.Agent.AgentDecider; +namespace DurableMultiAgentTemplate.Agent.AgentDecider; internal static class AgentDeciderPrompt { // Orchestrator Agent functions public const string SystemPrompt = """ - あなたは、人々が情報を見つけるのを助ける 旅行 AI アシスタントです。 - アシスタントとして、ユーザーからの問いについて必要なツールを選択してください。 - あなたの知識にないことや、使えるツールがない場合は「わかりません」と答えてください。 - 使えるツールがあるが、情報が足りない時はユーザーにその情報を質問してください。 - また、旅行以外の話題については答えないでください。 - """; -} \ No newline at end of file + あなたは、人々が情報を見つけるのを助ける 旅行 AI アシスタントです。 + アシスタントとして、ユーザーからの問いについて必要なツールを選択してください。 + あなたの知識にないことや、使えるツールがない場合は「わかりません」と答えてください。 + 使えるツールがあるが、情報が足りない時はユーザーにその情報を質問してください。 + また、旅行以外の話題については答えないでください。 + """; +} diff --git a/DurableMultiAgentTemplate/Agent/Synthesizer/SynthesizerPrompt.cs b/DurableMultiAgentTemplate/Agent/Synthesizer/SynthesizerPrompt.cs index d2a491a..e4a40fb 100644 --- a/DurableMultiAgentTemplate/Agent/Synthesizer/SynthesizerPrompt.cs +++ b/DurableMultiAgentTemplate/Agent/Synthesizer/SynthesizerPrompt.cs @@ -1,14 +1,14 @@ -namespace DurableMultiAgentTemplate.Agent.Synthesizer; +namespace DurableMultiAgentTemplate.Agent.Synthesizer; internal static class SynthesizerPrompt { // Orchestrator Agent functions public const string SystemPrompt = """ - あなたは、ユーザーの質問に対して答えを作成する役割を持っています。 - - ユーザーの質問に対して**以下の参考情報のみを用いて**回答を生成してください。 - - 一部分のみ回答できる場合はその部分のみ回答してください。 - - 質問内容に対して全く情報がない場合は「情報がありません」と回答してください。 - - 回答は見やすく簡潔に。Markdown形式で記述することができます。 - # 参考情報 - """; -} \ No newline at end of file + あなたは、ユーザーの質問に対して答えを作成する役割を持っています。 + - ユーザーの質問に対して**以下の参考情報のみを用いて**回答を生成してください。 + - 一部分のみ回答できる場合はその部分のみ回答してください。 + - 質問内容に対して全く情報がない場合は「情報がありません」と回答してください。 + - 回答は見やすく簡潔に。Markdown形式で記述することができます。 + # 参考情報 + """; +} diff --git a/DurableMultiAgentTemplate/Agent/AgentDefinition.cs b/DurableMultiAgentTemplate/Agent/Workers/AgentDefinition.cs similarity index 95% rename from DurableMultiAgentTemplate/Agent/AgentDefinition.cs rename to DurableMultiAgentTemplate/Agent/Workers/AgentDefinition.cs index e6c2767..bca5b5d 100644 --- a/DurableMultiAgentTemplate/Agent/AgentDefinition.cs +++ b/DurableMultiAgentTemplate/Agent/Workers/AgentDefinition.cs @@ -1,8 +1,8 @@ -using DurableMultiAgentTemplate.Json; +using DurableMultiAgentTemplate.Json; using DurableMultiAgentTemplate.Model; using OpenAI.Chat; -namespace DurableMultiAgentTemplate.Agent; +namespace DurableMultiAgentTemplate.Agent.Workers; //https://learn.microsoft.com/ja-jp/azure/ai-services/openai/how-to/dotnet-migration?tabs=stable internal class AgentDefinition diff --git a/DurableMultiAgentTemplate/Agent/GetClimateAgent/GetClimateActivity.cs b/DurableMultiAgentTemplate/Agent/Workers/GetClimateAgent/GetClimateActivity.cs similarity index 97% rename from DurableMultiAgentTemplate/Agent/GetClimateAgent/GetClimateActivity.cs rename to DurableMultiAgentTemplate/Agent/Workers/GetClimateAgent/GetClimateActivity.cs index cb0f101..6fc77eb 100644 --- a/DurableMultiAgentTemplate/Agent/GetClimateAgent/GetClimateActivity.cs +++ b/DurableMultiAgentTemplate/Agent/Workers/GetClimateAgent/GetClimateActivity.cs @@ -2,7 +2,7 @@ using Microsoft.Extensions.Logging; using OpenAI.Chat; -namespace DurableMultiAgentTemplate.Agent.GetClimateAgent; +namespace DurableMultiAgentTemplate.Agent.Workers.GetClimateAgent; public class GetClimateActivity(ChatClient chatClient, ILogger logger) diff --git a/DurableMultiAgentTemplate/Agent/GetClimateAgent/GetClimateRequest.cs b/DurableMultiAgentTemplate/Agent/Workers/GetClimateAgent/GetClimateRequest.cs similarity index 72% rename from DurableMultiAgentTemplate/Agent/GetClimateAgent/GetClimateRequest.cs rename to DurableMultiAgentTemplate/Agent/Workers/GetClimateAgent/GetClimateRequest.cs index 255b8b2..1fe28b1 100644 --- a/DurableMultiAgentTemplate/Agent/GetClimateAgent/GetClimateRequest.cs +++ b/DurableMultiAgentTemplate/Agent/Workers/GetClimateAgent/GetClimateRequest.cs @@ -1,6 +1,6 @@ using System.ComponentModel; -namespace DurableMultiAgentTemplate.Agent.GetClimateAgent; +namespace DurableMultiAgentTemplate.Agent.Workers.GetClimateAgent; public record GetClimateRequest( [property: Description("場所の名前。例: ボストン, 東京、フランス")] diff --git a/DurableMultiAgentTemplate/Agent/GetDestinationSuggestAgent/GetDestinationSuggestActivity.cs b/DurableMultiAgentTemplate/Agent/Workers/GetDestinationSuggestAgent/GetDestinationSuggestActivity.cs similarity index 97% rename from DurableMultiAgentTemplate/Agent/GetDestinationSuggestAgent/GetDestinationSuggestActivity.cs rename to DurableMultiAgentTemplate/Agent/Workers/GetDestinationSuggestAgent/GetDestinationSuggestActivity.cs index c8aa221..d5aca08 100644 --- a/DurableMultiAgentTemplate/Agent/GetDestinationSuggestAgent/GetDestinationSuggestActivity.cs +++ b/DurableMultiAgentTemplate/Agent/Workers/GetDestinationSuggestAgent/GetDestinationSuggestActivity.cs @@ -2,7 +2,7 @@ using Microsoft.Extensions.Logging; using OpenAI.Chat; -namespace DurableMultiAgentTemplate.Agent.GetDestinationSuggestAgent; +namespace DurableMultiAgentTemplate.Agent.Workers.GetDestinationSuggestAgent; public class GetDestinationSuggestActivity(ChatClient chatClient, ILogger logger) diff --git a/DurableMultiAgentTemplate/Agent/GetDestinationSuggestAgent/GetDestinationSuggestRequest.cs b/DurableMultiAgentTemplate/Agent/Workers/GetDestinationSuggestAgent/GetDestinationSuggestRequest.cs similarity index 68% rename from DurableMultiAgentTemplate/Agent/GetDestinationSuggestAgent/GetDestinationSuggestRequest.cs rename to DurableMultiAgentTemplate/Agent/Workers/GetDestinationSuggestAgent/GetDestinationSuggestRequest.cs index 75efc12..19c369f 100644 --- a/DurableMultiAgentTemplate/Agent/GetDestinationSuggestAgent/GetDestinationSuggestRequest.cs +++ b/DurableMultiAgentTemplate/Agent/Workers/GetDestinationSuggestAgent/GetDestinationSuggestRequest.cs @@ -1,6 +1,6 @@ using System.ComponentModel; -namespace DurableMultiAgentTemplate.Agent.GetDestinationSuggestAgent; +namespace DurableMultiAgentTemplate.Agent.Workers.GetDestinationSuggestAgent; public record GetDestinationSuggestRequest( [property: Description("行き先に求める希望の条件")] diff --git a/DurableMultiAgentTemplate/Agent/GetHotelAgent/GetHotelActivity.cs b/DurableMultiAgentTemplate/Agent/Workers/GetHotelAgent/GetHotelActivity.cs similarity index 97% rename from DurableMultiAgentTemplate/Agent/GetHotelAgent/GetHotelActivity.cs rename to DurableMultiAgentTemplate/Agent/Workers/GetHotelAgent/GetHotelActivity.cs index dd04435..1135ab0 100644 --- a/DurableMultiAgentTemplate/Agent/GetHotelAgent/GetHotelActivity.cs +++ b/DurableMultiAgentTemplate/Agent/Workers/GetHotelAgent/GetHotelActivity.cs @@ -2,7 +2,7 @@ using Microsoft.Extensions.Logging; using OpenAI.Chat; -namespace DurableMultiAgentTemplate.Agent.GetHotelAgent; +namespace DurableMultiAgentTemplate.Agent.Workers.GetHotelAgent; public class GetHotelActivity(ChatClient chatClient, ILogger logger) diff --git a/DurableMultiAgentTemplate/Agent/GetHotelAgent/GetHotelRequest.cs b/DurableMultiAgentTemplate/Agent/Workers/GetHotelAgent/GetHotelRequest.cs similarity index 73% rename from DurableMultiAgentTemplate/Agent/GetHotelAgent/GetHotelRequest.cs rename to DurableMultiAgentTemplate/Agent/Workers/GetHotelAgent/GetHotelRequest.cs index 4cb77ff..3abb29e 100644 --- a/DurableMultiAgentTemplate/Agent/GetHotelAgent/GetHotelRequest.cs +++ b/DurableMultiAgentTemplate/Agent/Workers/GetHotelAgent/GetHotelRequest.cs @@ -1,6 +1,6 @@ using System.ComponentModel; -namespace DurableMultiAgentTemplate.Agent.GetHotelAgent; +namespace DurableMultiAgentTemplate.Agent.Workers.GetHotelAgent; public record GetHotelRequest( [property: Description("場所の名前。例: ボストン, 東京、フランス")] diff --git a/DurableMultiAgentTemplate/Agent/GetSightseeingSpotAgent/GetSightseeingSpotActivity.cs b/DurableMultiAgentTemplate/Agent/Workers/GetSightseeingSpotAgent/GetSightseeingSpotActivity.cs similarity index 98% rename from DurableMultiAgentTemplate/Agent/GetSightseeingSpotAgent/GetSightseeingSpotActivity.cs rename to DurableMultiAgentTemplate/Agent/Workers/GetSightseeingSpotAgent/GetSightseeingSpotActivity.cs index 377ab37..acef8c3 100644 --- a/DurableMultiAgentTemplate/Agent/GetSightseeingSpotAgent/GetSightseeingSpotActivity.cs +++ b/DurableMultiAgentTemplate/Agent/Workers/GetSightseeingSpotAgent/GetSightseeingSpotActivity.cs @@ -2,7 +2,7 @@ using Microsoft.Extensions.Logging; using OpenAI.Chat; -namespace DurableMultiAgentTemplate.Agent.GetSightseeingSpotAgent; +namespace DurableMultiAgentTemplate.Agent.Workers.GetSightseeingSpotAgent; public class GetSightseeingSpotActivity(ChatClient chatClient, ILogger logger) diff --git a/DurableMultiAgentTemplate/Agent/GetSightseeingSpotAgent/GetSightseeingSpotRequest.cs b/DurableMultiAgentTemplate/Agent/Workers/GetSightseeingSpotAgent/GetSightseeingSpotRequest.cs similarity index 71% rename from DurableMultiAgentTemplate/Agent/GetSightseeingSpotAgent/GetSightseeingSpotRequest.cs rename to DurableMultiAgentTemplate/Agent/Workers/GetSightseeingSpotAgent/GetSightseeingSpotRequest.cs index 84a1640..31d7c2b 100644 --- a/DurableMultiAgentTemplate/Agent/GetSightseeingSpotAgent/GetSightseeingSpotRequest.cs +++ b/DurableMultiAgentTemplate/Agent/Workers/GetSightseeingSpotAgent/GetSightseeingSpotRequest.cs @@ -1,6 +1,6 @@ using System.ComponentModel; -namespace DurableMultiAgentTemplate.Agent.GetSightseeingSpotAgent; +namespace DurableMultiAgentTemplate.Agent.Workers.GetSightseeingSpotAgent; public record GetSightseeingSpotRequest( [property: Description("場所の名前。例: ボストン, 東京、フランス")] diff --git a/DurableMultiAgentTemplate/Agent/SubmitReservationAgent/SubmitReservationActivity.cs b/DurableMultiAgentTemplate/Agent/Workers/SubmitReservationAgent/SubmitReservationActivity.cs similarity index 91% rename from DurableMultiAgentTemplate/Agent/SubmitReservationAgent/SubmitReservationActivity.cs rename to DurableMultiAgentTemplate/Agent/Workers/SubmitReservationAgent/SubmitReservationActivity.cs index d456769..0b5346b 100644 --- a/DurableMultiAgentTemplate/Agent/SubmitReservationAgent/SubmitReservationActivity.cs +++ b/DurableMultiAgentTemplate/Agent/Workers/SubmitReservationAgent/SubmitReservationActivity.cs @@ -1,7 +1,7 @@ using Microsoft.Azure.Functions.Worker; using OpenAI.Chat; -namespace DurableMultiAgentTemplate.Agent.SubmitReservationAgent; +namespace DurableMultiAgentTemplate.Agent.Workers.SubmitReservationAgent; public class SubmitReservationActivity(ChatClient chatClient)//, CosmosClient cosmosClient) { diff --git a/DurableMultiAgentTemplate/Agent/SubmitReservationAgent/SubmitReservationRequest.cs b/DurableMultiAgentTemplate/Agent/Workers/SubmitReservationAgent/SubmitReservationRequest.cs similarity index 85% rename from DurableMultiAgentTemplate/Agent/SubmitReservationAgent/SubmitReservationRequest.cs rename to DurableMultiAgentTemplate/Agent/Workers/SubmitReservationAgent/SubmitReservationRequest.cs index fdca8be..2cbf6bc 100644 --- a/DurableMultiAgentTemplate/Agent/SubmitReservationAgent/SubmitReservationRequest.cs +++ b/DurableMultiAgentTemplate/Agent/Workers/SubmitReservationAgent/SubmitReservationRequest.cs @@ -1,6 +1,6 @@ using System.ComponentModel; -namespace DurableMultiAgentTemplate.Agent.SubmitReservationAgent; +namespace DurableMultiAgentTemplate.Agent.Workers.SubmitReservationAgent; public record SubmitReservationRequest( [property: Description("行き先のホテルの名前。")] diff --git a/DurableMultiAgentTemplate/Model/SourceGenerationContext.cs b/DurableMultiAgentTemplate/Model/SourceGenerationContext.cs index 7c5a2d0..e542fd4 100644 --- a/DurableMultiAgentTemplate/Model/SourceGenerationContext.cs +++ b/DurableMultiAgentTemplate/Model/SourceGenerationContext.cs @@ -1,10 +1,10 @@ using System.Text.Json.Serialization; -using DurableMultiAgentTemplate.Agent.GetClimateAgent; -using DurableMultiAgentTemplate.Agent.GetDestinationSuggestAgent; -using DurableMultiAgentTemplate.Agent.GetHotelAgent; -using DurableMultiAgentTemplate.Agent.GetSightseeingSpotAgent; -using DurableMultiAgentTemplate.Agent.SubmitReservationAgent; using DurableMultiAgentTemplate.Agent.Synthesizer; +using DurableMultiAgentTemplate.Agent.Workers.GetClimateAgent; +using DurableMultiAgentTemplate.Agent.Workers.GetDestinationSuggestAgent; +using DurableMultiAgentTemplate.Agent.Workers.GetHotelAgent; +using DurableMultiAgentTemplate.Agent.Workers.GetSightseeingSpotAgent; +using DurableMultiAgentTemplate.Agent.Workers.SubmitReservationAgent; using DurableMultiAgentTemplate.Shared.Model; namespace DurableMultiAgentTemplate.Model; From d83d8a148a0718f6b413c8c7d1dd0deb208038d9 Mon Sep 17 00:00:00 2001 From: Kazuki Ota Date: Tue, 18 Mar 2025 10:44:13 +0900 Subject: [PATCH 3/7] =?UTF-8?q?HumanInTheLoop=20=E3=82=92=E8=BF=BD?= =?UTF-8?q?=E5=8A=A0=EF=BC=88=E5=8B=95=E3=81=8B=E3=81=AA=E3=81=84=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Agent/AgentActivityName.cs | 5 ++- .../Agent/Workers/AgentDefinition.cs | 8 +++- .../HumanInTheLoopActivity.cs | 37 +++++++++++++++++++ .../HumanInTheLoopPrompt.cs | 12 ++++++ .../HumanInTheLoopRequest.cs | 7 ++++ .../Model/SourceGenerationContext.cs | 2 + 6 files changed, 68 insertions(+), 3 deletions(-) create mode 100644 DurableMultiAgentTemplate/Agent/Workers/HumanInTheLoopAgent/HumanInTheLoopActivity.cs create mode 100644 DurableMultiAgentTemplate/Agent/Workers/HumanInTheLoopAgent/HumanInTheLoopPrompt.cs create mode 100644 DurableMultiAgentTemplate/Agent/Workers/HumanInTheLoopAgent/HumanInTheLoopRequest.cs diff --git a/DurableMultiAgentTemplate/Agent/AgentActivityName.cs b/DurableMultiAgentTemplate/Agent/AgentActivityName.cs index 21d261b..22340ec 100644 --- a/DurableMultiAgentTemplate/Agent/AgentActivityName.cs +++ b/DurableMultiAgentTemplate/Agent/AgentActivityName.cs @@ -1,4 +1,4 @@ -namespace DurableMultiAgentTemplate.Agent; +namespace DurableMultiAgentTemplate.Agent; public static class AgentActivityName { @@ -13,4 +13,5 @@ public static class AgentActivityName public const string GetSightseeingSpotAgent = nameof(GetSightseeingSpotAgent); public const string GetHotelAgent = nameof(GetHotelAgent); public const string SubmitReservationAgent = nameof(SubmitReservationAgent); -} \ No newline at end of file + public const string HumanInTheLoopAgent = nameof(HumanInTheLoopAgent); +} diff --git a/DurableMultiAgentTemplate/Agent/Workers/AgentDefinition.cs b/DurableMultiAgentTemplate/Agent/Workers/AgentDefinition.cs index bca5b5d..34ea944 100644 --- a/DurableMultiAgentTemplate/Agent/Workers/AgentDefinition.cs +++ b/DurableMultiAgentTemplate/Agent/Workers/AgentDefinition.cs @@ -1,5 +1,6 @@ using DurableMultiAgentTemplate.Json; using DurableMultiAgentTemplate.Model; +using Microsoft.Azure.Cosmos.Core.Networking; using OpenAI.Chat; namespace DurableMultiAgentTemplate.Agent.Workers; @@ -29,6 +30,11 @@ internal class AgentDefinition public static readonly ChatTool SubmitReservationAgent = ChatTool.CreateFunctionTool( functionName: AgentActivityName.SubmitReservationAgent, - functionDescription: "宿泊先の予約を行います。", + functionDescription: "宿泊先の予約を行います。予約には利用者の最終確認が必要です。", functionParameters: JsonSchemaGenerator.GenerateSchemaAsBinaryData(SourceGenerationContext.Default.SubmitReservationRequest)); + + public static readonly ChatTool HumanInTheLoopAgent = ChatTool.CreateFunctionTool( + functionName: AgentActivityName.HumanInTheLoopAgent, + functionDescription: "利用者に最終確認を行います。", + functionParameters: JsonSchemaGenerator.GenerateSchemaAsBinaryData(SourceGenerationContext.Default.HumanInTheLoopRequest)); } diff --git a/DurableMultiAgentTemplate/Agent/Workers/HumanInTheLoopAgent/HumanInTheLoopActivity.cs b/DurableMultiAgentTemplate/Agent/Workers/HumanInTheLoopAgent/HumanInTheLoopActivity.cs new file mode 100644 index 0000000..a3eb675 --- /dev/null +++ b/DurableMultiAgentTemplate/Agent/Workers/HumanInTheLoopAgent/HumanInTheLoopActivity.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using DurableMultiAgentTemplate.Shared.Model; +using Microsoft.Azure.Functions.Worker; +using Microsoft.Extensions.Logging; +using OpenAI.Chat; + +namespace DurableMultiAgentTemplate.Agent.Workers.HumanInTheLoopAgent; + +public class HumanInTheLoopActivity(ILogger logger, + ChatClient chatClient) +{ + [Function(nameof(HumanInTheLoopActivity))] + public async Task RunAsync([ActivityTrigger] HumanInTheLoopRequest req) + { + logger.LogInformation("HumanInTheLoopActivity called"); + ChatMessage[] allMessages = [ + new SystemChatMessage(HumanInTheLoopPrompt.SystemPrompt), + new UserChatMessage(req.UserRequest), + ]; + + var chatResult = await chatClient.CompleteChatAsync( + allMessages + ); + + if (chatResult.Value.FinishReason == ChatFinishReason.Stop) + { + return chatResult.Value.Content.First().Text; + } + + logger.LogError("Failed to generate a confirmation message for the user, the finish reason is {finishReason}.", chatResult.Value.FinishReason); + throw new InvalidOperationException("Failed to generate a confirmation message for the user"); + } +} diff --git a/DurableMultiAgentTemplate/Agent/Workers/HumanInTheLoopAgent/HumanInTheLoopPrompt.cs b/DurableMultiAgentTemplate/Agent/Workers/HumanInTheLoopAgent/HumanInTheLoopPrompt.cs new file mode 100644 index 0000000..8e8f177 --- /dev/null +++ b/DurableMultiAgentTemplate/Agent/Workers/HumanInTheLoopAgent/HumanInTheLoopPrompt.cs @@ -0,0 +1,12 @@ +namespace DurableMultiAgentTemplate.Agent.Workers.HumanInTheLoopAgent; + +internal static class HumanInTheLoopPrompt +{ + public const string SystemPrompt = """ + あなたは、ユーザーがやりたい内容についてユーザーに最終確認を求める役割を持っています。 + - ユーザーがやりたい内容についてユーザーに最終確認を求める問いかけをしてください + - 問いかけはユーザーが「はい」か「いいえ」で答えられる形式にしてください + - 問いかけは簡潔にMarkdown形式で記述してください + - 問いかけにはユーザーがやりたい内容についての情報を含めてください + """; +} diff --git a/DurableMultiAgentTemplate/Agent/Workers/HumanInTheLoopAgent/HumanInTheLoopRequest.cs b/DurableMultiAgentTemplate/Agent/Workers/HumanInTheLoopAgent/HumanInTheLoopRequest.cs new file mode 100644 index 0000000..002ab90 --- /dev/null +++ b/DurableMultiAgentTemplate/Agent/Workers/HumanInTheLoopAgent/HumanInTheLoopRequest.cs @@ -0,0 +1,7 @@ +using System.ComponentModel; + +namespace DurableMultiAgentTemplate.Agent.Workers.HumanInTheLoopAgent; + +public record HumanInTheLoopRequest( + [property: Description("ユーザーが行いたい処理の詳細")] + string UserRequest); diff --git a/DurableMultiAgentTemplate/Model/SourceGenerationContext.cs b/DurableMultiAgentTemplate/Model/SourceGenerationContext.cs index e542fd4..bb61be1 100644 --- a/DurableMultiAgentTemplate/Model/SourceGenerationContext.cs +++ b/DurableMultiAgentTemplate/Model/SourceGenerationContext.cs @@ -4,6 +4,7 @@ using DurableMultiAgentTemplate.Agent.Workers.GetDestinationSuggestAgent; using DurableMultiAgentTemplate.Agent.Workers.GetHotelAgent; using DurableMultiAgentTemplate.Agent.Workers.GetSightseeingSpotAgent; +using DurableMultiAgentTemplate.Agent.Workers.HumanInTheLoopAgent; using DurableMultiAgentTemplate.Agent.Workers.SubmitReservationAgent; using DurableMultiAgentTemplate.Shared.Model; @@ -19,4 +20,5 @@ namespace DurableMultiAgentTemplate.Model; [JsonSerializable(typeof(AdditionalMarkdownInfo))] [JsonSerializable(typeof(AdditionalLinkInfo))] [JsonSerializable(typeof(AgentResponseWithAdditionalInfoFormat))] +[JsonSerializable(typeof(HumanInTheLoopRequest))] internal partial class SourceGenerationContext : JsonSerializerContext; From 26436a14ef54da35f8d76c5c0f33bc5deab0a6b3 Mon Sep 17 00:00:00 2001 From: Kazuki Ota Date: Tue, 18 Mar 2025 10:52:48 +0900 Subject: [PATCH 4/7] =?UTF-8?q?=E4=B8=8D=E8=A6=81=E3=81=AA=E5=90=8D?= =?UTF-8?q?=E5=89=8D=E7=A9=BA=E9=96=93=E3=81=AE=E5=89=8A=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DurableMultiAgentTemplate/Agent/Workers/AgentDefinition.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/DurableMultiAgentTemplate/Agent/Workers/AgentDefinition.cs b/DurableMultiAgentTemplate/Agent/Workers/AgentDefinition.cs index 34ea944..0ddd733 100644 --- a/DurableMultiAgentTemplate/Agent/Workers/AgentDefinition.cs +++ b/DurableMultiAgentTemplate/Agent/Workers/AgentDefinition.cs @@ -1,6 +1,5 @@ using DurableMultiAgentTemplate.Json; using DurableMultiAgentTemplate.Model; -using Microsoft.Azure.Cosmos.Core.Networking; using OpenAI.Chat; namespace DurableMultiAgentTemplate.Agent.Workers; From e9ae5068b4e5e2f93357c59babe5e31cc757b9fc Mon Sep 17 00:00:00 2001 From: Kazuki Ota Date: Tue, 18 Mar 2025 10:52:48 +0900 Subject: [PATCH 5/7] =?UTF-8?q?=E4=B8=8D=E8=A6=81=E3=81=AA=E5=90=8D?= =?UTF-8?q?=E5=89=8D=E7=A9=BA=E9=96=93=E3=81=AE=E5=89=8A=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Components/Chat/ChatThread.razor | 2 +- .../Components/Pages/Home.razor.cs | 8 +- .../Model/ChatMessage.cs | 9 -- .../Model/AgentCall.cs | 7 +- .../Model/AgentMessageItem.cs | 62 +++++++++ .../Model/AgentRequestDto.cs | 2 +- .../Model/AgentRequestMessageItem.cs | 10 -- .../Model/AgentResponseDto.cs | 6 +- .../AgentResponseWithAdditionalInfoDto.cs | 4 +- .../Orchestrator/AgentOrchestratorTest.cs | 51 +++---- .../Synthesizer/SynthesizerActivityTest.cs | 8 +- ...tActivityName.cs => AgentActivityNames.cs} | 4 +- .../AgentDecider/AgentDeciderActivity.cs | 62 ++++++--- .../Agent/AgentDecider/AgentDeciderPrompt.cs | 12 ++ .../Agent/Orchestrator/AgentOrchestrator.cs | 21 +-- .../Agent/Synthesizer/SynthesizerActivity.cs | 11 +- .../Agent/Synthesizer/SynthesizerRequest.cs | 5 +- .../SynthesizerWithAdditionalInfoActivity.cs | 4 +- .../Agent/Workers/AgentDefinition.cs | 94 ++++++++----- .../GetClimateAgent/GetClimateActivity.cs | 70 +++++----- .../GetDestinationSuggestActivity.cs | 64 ++++----- .../Workers/GetHotelAgent/GetHotelActivity.cs | 82 ++++++------ .../GetSightseeingSpotActivity.cs | 126 +++++++++--------- .../HotelReservationActivity.cs | 47 +++++++ .../HotelReservationRequest.cs} | 4 +- .../HumanInTheLoopActivity.cs | 37 ----- .../HumanInTheLoopPrompt.cs | 12 -- .../HumanInTheLoopRequest.cs | 7 - .../SubmitReservationActivity.cs | 27 ---- .../Agent/Workers/WorkerAgentResult.cs | 18 +++ .../AgentRequestMessageItemExtension.cs | 8 +- .../Model/SourceGenerationContext.cs | 6 +- DurableMultiAgentTemplate/Program.cs | 2 + 33 files changed, 501 insertions(+), 391 deletions(-) create mode 100644 DurableMultiAgentTemplate.Shared/Model/AgentMessageItem.cs delete mode 100644 DurableMultiAgentTemplate.Shared/Model/AgentRequestMessageItem.cs rename DurableMultiAgentTemplate/Agent/{AgentActivityName.cs => AgentActivityNames.cs} (86%) create mode 100644 DurableMultiAgentTemplate/Agent/Workers/HotelReservationAgent/HotelReservationActivity.cs rename DurableMultiAgentTemplate/Agent/Workers/{SubmitReservationAgent/SubmitReservationRequest.cs => HotelReservationAgent/HotelReservationRequest.cs} (77%) delete mode 100644 DurableMultiAgentTemplate/Agent/Workers/HumanInTheLoopAgent/HumanInTheLoopActivity.cs delete mode 100644 DurableMultiAgentTemplate/Agent/Workers/HumanInTheLoopAgent/HumanInTheLoopPrompt.cs delete mode 100644 DurableMultiAgentTemplate/Agent/Workers/HumanInTheLoopAgent/HumanInTheLoopRequest.cs delete mode 100644 DurableMultiAgentTemplate/Agent/Workers/SubmitReservationAgent/SubmitReservationActivity.cs create mode 100644 DurableMultiAgentTemplate/Agent/Workers/WorkerAgentResult.cs diff --git a/DurableMultiAgentTemplate.Client/Components/Chat/ChatThread.razor b/DurableMultiAgentTemplate.Client/Components/Chat/ChatThread.razor index 48bb81a..947ba49 100644 --- a/DurableMultiAgentTemplate.Client/Components/Chat/ChatThread.razor +++ b/DurableMultiAgentTemplate.Client/Components/Chat/ChatThread.razor @@ -30,7 +30,7 @@
- @((MarkupString)Markdown.ToHtml(agentChatMessage.Message.Content)) + @((MarkupString)Markdown.ToHtml(agentChatMessage.Message.Item.Content))
使用されたエージェント: @string.Join(", ", agentChatMessage.Message.CalledAgentNames) diff --git a/DurableMultiAgentTemplate.Client/Components/Pages/Home.razor.cs b/DurableMultiAgentTemplate.Client/Components/Pages/Home.razor.cs index 2ee9528..4b50773 100644 --- a/DurableMultiAgentTemplate.Client/Components/Pages/Home.razor.cs +++ b/DurableMultiAgentTemplate.Client/Components/Pages/Home.razor.cs @@ -63,12 +63,8 @@ static string createStatusMessage(AgentOrchestratorStatus status) var getAgentResponseTask = agentChatService.GetAgentResponseAsync(new AgentRequestDto( Messages: [.. _messages.Where(x => x.IsRequestTarget).Select(x => x switch { - UserChatMessage userChatMessage => new AgentRequestMessageItem( - userChatMessage.Role.ToRoleName(), - userChatMessage.Message), - AgentChatMessage agentChatMessage => new AgentRequestMessageItem( - agentChatMessage.Role.ToRoleName(), - agentChatMessage.Message.Content), + UserChatMessage userChatMessage => (MessageItem)new UserMessageItem(userChatMessage.Message), + AgentChatMessage agentChatMessage => agentChatMessage.Message.Item, _ => throw new InvalidOperationException() })], RequireAdditionalInfo: _chatInput.RequireAdditionalInfo), diff --git a/DurableMultiAgentTemplate.Client/Model/ChatMessage.cs b/DurableMultiAgentTemplate.Client/Model/ChatMessage.cs index a6961af..dcc9ed3 100644 --- a/DurableMultiAgentTemplate.Client/Model/ChatMessage.cs +++ b/DurableMultiAgentTemplate.Client/Model/ChatMessage.cs @@ -14,12 +14,3 @@ public enum Role Info } -public static class RoleExtensions -{ - public static string ToRoleName(this Role role) => role switch - { - Role.User => "user", - Role.Assistant => "assistant", - _ => throw new InvalidOperationException() - }; -} diff --git a/DurableMultiAgentTemplate.Shared/Model/AgentCall.cs b/DurableMultiAgentTemplate.Shared/Model/AgentCall.cs index d7526b0..267bb9d 100644 --- a/DurableMultiAgentTemplate.Shared/Model/AgentCall.cs +++ b/DurableMultiAgentTemplate.Shared/Model/AgentCall.cs @@ -1,4 +1,7 @@ -namespace DurableMultiAgentTemplate.Shared.Model; +using System.Text.Json; +using System.Text.Json.Nodes; + +namespace DurableMultiAgentTemplate.Shared.Model; /// @@ -6,4 +9,4 @@ /// /// The name of the agent being called. /// The arguments to be passed to the agent. -public record AgentCall(string AgentName, object Arguments); +public record AgentCall(string AgentName, JsonElement Arguments); diff --git a/DurableMultiAgentTemplate.Shared/Model/AgentMessageItem.cs b/DurableMultiAgentTemplate.Shared/Model/AgentMessageItem.cs new file mode 100644 index 0000000..1237ec6 --- /dev/null +++ b/DurableMultiAgentTemplate.Shared/Model/AgentMessageItem.cs @@ -0,0 +1,62 @@ +using System.Text.Json.Serialization; + +namespace DurableMultiAgentTemplate.Shared.Model; + +/// +/// Represents a message item for an agent request. +/// +/// The role of the agent. +/// The content of the message. +[JsonDerivedType(typeof(UserMessageItem), typeDiscriminator: "user")] +[JsonDerivedType(typeof(AgentMessageItem), typeDiscriminator: "agent")] +public abstract record MessageItem( + AgentRole Role, + string Content); + +public record UserMessageItem(string Content) : MessageItem(AgentRole.User, Content); + +[method: JsonConstructor] +public record AgentMessageItem(AgentMessageType AgentMessageType, + string Content, + AgentCall? NextAgentCall) : MessageItem(AgentRole.Agent, Content) +{ + public AgentMessageItem(string Content) : this(AgentMessageType.Info, Content, null) + { + } + + public AgentMessageItem(string Content, AgentCall NextAgentCall) : this(AgentMessageType.Ask, Content, NextAgentCall) + { + } +} + +/// +/// Represents the role of an agent. +/// +[JsonConverter(typeof(JsonStringEnumConverter))] +public enum AgentRole +{ + /// + /// Represents a user role. + /// + User, + /// + /// Represents a agent role. + /// + Agent +} + +/// +/// Represents a message type for an agent. +/// +[JsonConverter(typeof(JsonStringEnumConverter))] +public enum AgentMessageType +{ + /// + /// Represents a question to the user. + /// + Ask, + /// + /// Represents general information. + /// + Info +} diff --git a/DurableMultiAgentTemplate.Shared/Model/AgentRequestDto.cs b/DurableMultiAgentTemplate.Shared/Model/AgentRequestDto.cs index 8299618..f73190e 100644 --- a/DurableMultiAgentTemplate.Shared/Model/AgentRequestDto.cs +++ b/DurableMultiAgentTemplate.Shared/Model/AgentRequestDto.cs @@ -6,5 +6,5 @@ /// A list of messages associated with the agent request. /// Indicates whether additional information is required for the agent request. public record AgentRequestDto( - List Messages, + List Messages, bool RequireAdditionalInfo = false); diff --git a/DurableMultiAgentTemplate.Shared/Model/AgentRequestMessageItem.cs b/DurableMultiAgentTemplate.Shared/Model/AgentRequestMessageItem.cs deleted file mode 100644 index bf5fe6a..0000000 --- a/DurableMultiAgentTemplate.Shared/Model/AgentRequestMessageItem.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace DurableMultiAgentTemplate.Shared.Model; - -/// -/// Represents a message item for an agent request. -/// -/// The role of the agent. -/// The content of the message. -public record AgentRequestMessageItem( - string Role, - string Content); diff --git a/DurableMultiAgentTemplate.Shared/Model/AgentResponseDto.cs b/DurableMultiAgentTemplate.Shared/Model/AgentResponseDto.cs index 8b7fbb2..cd47696 100644 --- a/DurableMultiAgentTemplate.Shared/Model/AgentResponseDto.cs +++ b/DurableMultiAgentTemplate.Shared/Model/AgentResponseDto.cs @@ -5,14 +5,14 @@ namespace DurableMultiAgentTemplate.Shared.Model; /// /// Represents the response from an agent. /// -/// The content of the response. +/// The content of the response. /// The list of names of the agents that were called. [method: JsonConstructor] public record AgentResponseDto( - string Content, + AgentMessageItem Item, List CalledAgentNames) { - public AgentResponseDto(string content) : this(content, []) + public AgentResponseDto(AgentMessageItem content) : this(content, []) { } } diff --git a/DurableMultiAgentTemplate.Shared/Model/AgentResponseWithAdditionalInfoDto.cs b/DurableMultiAgentTemplate.Shared/Model/AgentResponseWithAdditionalInfoDto.cs index 895c580..ae5f15f 100644 --- a/DurableMultiAgentTemplate.Shared/Model/AgentResponseWithAdditionalInfoDto.cs +++ b/DurableMultiAgentTemplate.Shared/Model/AgentResponseWithAdditionalInfoDto.cs @@ -10,11 +10,11 @@ namespace DurableMultiAgentTemplate.Shared.Model; /// The list of names of the agents that were called. /// The additional information related to the response. [method: JsonConstructor] -public record AgentResponseWithAdditionalInfoDto(string Content, +public record AgentResponseWithAdditionalInfoDto(AgentMessageItem Content, List CalledAgentNames, List AdditionalInfo) : AgentResponseDto(Content, CalledAgentNames) { - public AgentResponseWithAdditionalInfoDto(string content) : this(content, [], []) + public AgentResponseWithAdditionalInfoDto(AgentMessageItem content) : this(content, [], []) { } } diff --git a/DurableMultiAgentTemplate.Test/Agent/Orchestrator/AgentOrchestratorTest.cs b/DurableMultiAgentTemplate.Test/Agent/Orchestrator/AgentOrchestratorTest.cs index fff85ec..27e3ae8 100644 --- a/DurableMultiAgentTemplate.Test/Agent/Orchestrator/AgentOrchestratorTest.cs +++ b/DurableMultiAgentTemplate.Test/Agent/Orchestrator/AgentOrchestratorTest.cs @@ -1,4 +1,5 @@ -using DurableMultiAgentTemplate.Agent; +using System.Text.Json; +using DurableMultiAgentTemplate.Agent; using DurableMultiAgentTemplate.Agent.AgentDecider; using DurableMultiAgentTemplate.Agent.Orchestrator; using DurableMultiAgentTemplate.Agent.Synthesizer; @@ -31,7 +32,7 @@ public async Task SetCustomStatus_WhenAgentDeciderActivityIsCalled() // Simulate cancellation of the activity using var cancellationTokenSource = new CancellationTokenSource(); cancellationTokenSource.Cancel(); - contextMock.Setup(x => x.CallActivityAsync(AgentActivityName.AgentDeciderActivity, reqData, It.IsAny())) + contextMock.Setup(x => x.CallActivityAsync(AgentActivityNames.AgentDeciderActivity, reqData, It.IsAny())) .Returns(Task.FromCanceled(cancellationTokenSource.Token)); // Act: Run the orchestrator and expect a TaskCanceledException @@ -44,7 +45,7 @@ public async Task SetCustomStatus_WhenAgentDeciderActivityIsCalled() CollectionAssert.AreEqual( new List() { - new (AgentActivityName.AgentDeciderActivity, reqData) + new (AgentActivityNames.AgentDeciderActivity, JsonSerializer.SerializeToElement(reqData)) }, status.AgentCalls.ToList()); } @@ -60,7 +61,7 @@ public async Task RunOrchestrator_ShouldReturnAgentResponseDto_WhenNoAgentCallAn contextMock.Setup(x => x.GetInput()) .Returns(reqData); contextMock.Setup(x => x.CallActivityAsync( - AgentActivityName.AgentDeciderActivity, reqData, + AgentActivityNames.AgentDeciderActivity, reqData, It.IsAny())) .ReturnsAsync(new AgentDeciderResult(IsAgentCall: false, Content: "No agent call", [])); @@ -70,7 +71,7 @@ public async Task RunOrchestrator_ShouldReturnAgentResponseDto_WhenNoAgentCallAn // Assert: Verify the result is of type AgentResponseDto and has the expected content Assert.IsInstanceOfType(orchestratorResult); - Assert.AreEqual("No agent call", orchestratorResult.Content); + Assert.AreEqual("No agent call", orchestratorResult.Item.Content); } [TestMethod] @@ -84,7 +85,7 @@ public async Task RunOrchestrator_ShouldReturnAgentResponseWithAdditionalInfoDto contextMock.Setup(x => x.GetInput()) .Returns(reqData); contextMock.Setup(x => x.CallActivityAsync( - AgentActivityName.AgentDeciderActivity, reqData, + AgentActivityNames.AgentDeciderActivity, reqData, It.IsAny())) .ReturnsAsync(new AgentDeciderResult(IsAgentCall: false, Content: "No agent call", [])); @@ -94,7 +95,7 @@ public async Task RunOrchestrator_ShouldReturnAgentResponseWithAdditionalInfoDto // Assert: Verify the result is of type AgentResponseWithAdditionalInfoDto and has the expected content Assert.IsInstanceOfType(orchestratorResult); - Assert.AreEqual("No agent call", orchestratorResult.Content); + Assert.AreEqual("No agent call", orchestratorResult.Item.Content); } [TestMethod] @@ -127,13 +128,13 @@ public async Task SetCustomStatus_WhenWorkerAgentActivityIsCalled() // Setup agent decider result with multiple agent calls List agentCalls = [ - new AgentCall("TestAgent1", "Argument1"), - new AgentCall("TestAgent2", "Argument2") + new AgentCall("TestAgent1", JsonSerializer.SerializeToElement("Argument1")), + new AgentCall("TestAgent2", JsonSerializer.SerializeToElement("Argument2")) ]; var agentDeciderResult = new AgentDeciderResult(true, "Agent call", agentCalls); contextMock.Setup(x => x.CallActivityAsync( - AgentActivityName.AgentDeciderActivity, reqData, It.IsAny())) + AgentActivityNames.AgentDeciderActivity, reqData, It.IsAny())) .ReturnsAsync(agentDeciderResult); // Capture the custom statuses set by the orchestrator @@ -176,13 +177,13 @@ public async Task RunOrchestrator_ShouldCallSynthesizerActivity_WhenAgentCallAnd // Setup agent decider result with multiple agent calls List agentCalls = [ - new AgentCall("TestAgent1", "Argument1"), - new AgentCall("TestAgent2", "Argument2"), + new AgentCall("TestAgent1", JsonSerializer.SerializeToElement("Argument1")), + new AgentCall("TestAgent2", JsonSerializer.SerializeToElement("Argument2")), ]; var agentDeciderResult = new AgentDeciderResult(true, "Agent call", agentCalls); contextMock.Setup(x => x.CallActivityAsync( - AgentActivityName.AgentDeciderActivity, reqData, It.IsAny())) + AgentActivityNames.AgentDeciderActivity, reqData, It.IsAny())) .ReturnsAsync(agentDeciderResult); // Setup agent call results @@ -194,9 +195,9 @@ public async Task RunOrchestrator_ShouldCallSynthesizerActivity_WhenAgentCallAnd // Track synthesizer request SynthesizerRequest? capturedSynthesizerRequest = null; contextMock.Setup(x => x.CallActivityAsync( - AgentActivityName.SynthesizerActivity, It.IsAny(), It.IsAny())) + AgentActivityNames.SynthesizerActivity, It.IsAny(), It.IsAny())) .Callback((_, obj, _) => capturedSynthesizerRequest = obj as SynthesizerRequest) - .ReturnsAsync(new AgentResponseDto("Synthesized result")); + .ReturnsAsync(new AgentResponseDto(new("Synthesized result"))); // Act: Run the orchestrator var orchestrator = new AgentOrchestrator(); @@ -209,7 +210,7 @@ public async Task RunOrchestrator_ShouldCallSynthesizerActivity_WhenAgentCallAnd CollectionAssert.Contains(capturedSynthesizerRequest.AgentCallResult, "Agent2Result"); CollectionAssert.Contains(capturedSynthesizerRequest.CalledAgentNames, "TestAgent1"); CollectionAssert.Contains(capturedSynthesizerRequest.CalledAgentNames, "TestAgent2"); - Assert.AreEqual("Synthesized result", result.Content); + Assert.AreEqual("Synthesized result", result.Item.Content); } [TestMethod] @@ -226,12 +227,12 @@ public async Task RunOrchestrator_ShouldCallSynthesizerWithAdditionalInfoActivit // Setup agent decider result with a single agent call List agentCalls = [ - new AgentCall("TestAgent1", "Argument1") + new AgentCall("TestAgent1", JsonSerializer.SerializeToElement("Argument1")) ]; var agentDeciderResult = new AgentDeciderResult(true, "Agent call", agentCalls); contextMock.Setup(x => x.CallActivityAsync( - AgentActivityName.AgentDeciderActivity, reqData, It.IsAny())) + AgentActivityNames.AgentDeciderActivity, reqData, It.IsAny())) .ReturnsAsync(agentDeciderResult); // Setup agent call result @@ -241,9 +242,9 @@ public async Task RunOrchestrator_ShouldCallSynthesizerWithAdditionalInfoActivit // Track synthesizer request and return response with additional info SynthesizerRequest? capturedSynthesizerRequest = null; contextMock.Setup(x => x.CallActivityAsync( - AgentActivityName.SynthesizerWithAdditionalInfoActivity, It.IsAny(), It.IsAny())) + AgentActivityNames.SynthesizerWithAdditionalInfoActivity, It.IsAny(), It.IsAny())) .Callback((_, obj, _) => capturedSynthesizerRequest = (SynthesizerRequest)obj) - .ReturnsAsync(new AgentResponseWithAdditionalInfoDto("Synthesized result with additional info")); + .ReturnsAsync(new AgentResponseWithAdditionalInfoDto(new("Synthesized result with additional info"))); // Capture the custom statuses set by the orchestrator List statuses = []; @@ -265,7 +266,7 @@ public async Task RunOrchestrator_ShouldCallSynthesizerWithAdditionalInfoActivit CollectionAssert.Contains(capturedSynthesizerRequest.CalledAgentNames, "TestAgent1"); Assert.IsInstanceOfType(result); - Assert.AreEqual("Synthesized result with additional info", result.Content); + Assert.AreEqual("Synthesized result with additional info", result.Item.Content); // Verify SynthesizerActivity status was set Assert.IsTrue(statuses.Count >= 3); @@ -285,11 +286,11 @@ public async Task SetCustomStatus_WhenSynthesizerActivityIsCalled() .Returns(reqData); // Setup agent decider result with a single agent call - List agentCalls = [new AgentCall("TestAgent", "Argument")]; + List agentCalls = [new AgentCall("TestAgent", JsonSerializer.SerializeToElement("Argument"))]; var agentDeciderResult = new AgentDeciderResult(true, "Agent call", agentCalls); contextMock.Setup(x => x.CallActivityAsync( - AgentActivityName.AgentDeciderActivity, reqData, It.IsAny())) + AgentActivityNames.AgentDeciderActivity, reqData, It.IsAny())) .ReturnsAsync(agentDeciderResult); // Setup agent call result @@ -310,7 +311,7 @@ public async Task SetCustomStatus_WhenSynthesizerActivityIsCalled() cancellationTokenSource.Cancel(); contextMock.Setup(x => x.CallActivityAsync( - AgentActivityName.SynthesizerActivity, It.IsAny(), It.IsAny())) + AgentActivityNames.SynthesizerActivity, It.IsAny(), It.IsAny())) .Returns(Task.FromCanceled(cancellationTokenSource.Token)); // Act: Run the orchestrator and expect a TaskCanceledException @@ -320,7 +321,7 @@ public async Task SetCustomStatus_WhenSynthesizerActivityIsCalled() // Assert: Verify the custom status for SynthesizerActivity was set correctly Assert.IsTrue(statuses.Count >= 3); Assert.AreEqual(AgentOrchestratorStep.SynthesizerActivity, statuses[2].Step); - var synthesizerArgs = statuses[2].AgentCalls.Single().Arguments as SynthesizerRequest; + var synthesizerArgs = JsonSerializer.Deserialize(statuses[2].AgentCalls.Single().Arguments); Assert.IsNotNull(synthesizerArgs); Assert.AreEqual(reqData, synthesizerArgs.AgentRequest); CollectionAssert.AreEqual(new[] { "TestAgent" }, synthesizerArgs.CalledAgentNames); diff --git a/DurableMultiAgentTemplate.Test/Agent/Synthesizer/SynthesizerActivityTest.cs b/DurableMultiAgentTemplate.Test/Agent/Synthesizer/SynthesizerActivityTest.cs index d835b6c..ba67f27 100644 --- a/DurableMultiAgentTemplate.Test/Agent/Synthesizer/SynthesizerActivityTest.cs +++ b/DurableMultiAgentTemplate.Test/Agent/Synthesizer/SynthesizerActivityTest.cs @@ -58,15 +58,15 @@ public async Task SynthesizerMethod() どれも暖かい気候を楽しめる場所です。予算や旅行期間に合わせてお選びください! """], - AgentRequest: new AgentRequestDto([new("user", "あったかい場所に行きたいな")]), - CalledAgentNames: [AgentActivityName.GetDestinationSuggestAgent] + AgentRequest: new AgentRequestDto([new UserMessageItem("あったかい場所に行きたいな")]), + CalledAgentNames: [AgentActivityNames.GetDestinationSuggestAgent] ); var agentResponseDto = await synthesizerActivity.Run(synthesizerRequest); Assert.IsNotNull(agentResponseDto); - Assert.IsNotEmpty(agentResponseDto.Content); - Assert.AreEqual(expectedContent, agentResponseDto.Content); + Assert.IsNotNull(agentResponseDto.Item); + Assert.AreEqual(expectedContent, agentResponseDto.Item.Content); Assert.AreEqual(synthesizerRequest.CalledAgentNames, agentResponseDto.CalledAgentNames); } } diff --git a/DurableMultiAgentTemplate/Agent/AgentActivityName.cs b/DurableMultiAgentTemplate/Agent/AgentActivityNames.cs similarity index 86% rename from DurableMultiAgentTemplate/Agent/AgentActivityName.cs rename to DurableMultiAgentTemplate/Agent/AgentActivityNames.cs index 22340ec..41630a0 100644 --- a/DurableMultiAgentTemplate/Agent/AgentActivityName.cs +++ b/DurableMultiAgentTemplate/Agent/AgentActivityNames.cs @@ -1,6 +1,6 @@ namespace DurableMultiAgentTemplate.Agent; -public static class AgentActivityName +public static class AgentActivityNames { // Orchestrator Agent functions public const string AgentDeciderActivity = nameof(AgentDeciderActivity); @@ -13,5 +13,5 @@ public static class AgentActivityName public const string GetSightseeingSpotAgent = nameof(GetSightseeingSpotAgent); public const string GetHotelAgent = nameof(GetHotelAgent); public const string SubmitReservationAgent = nameof(SubmitReservationAgent); - public const string HumanInTheLoopAgent = nameof(HumanInTheLoopAgent); + public const string CommitReservationAgent = nameof(CommitReservationAgent); } diff --git a/DurableMultiAgentTemplate/Agent/AgentDecider/AgentDeciderActivity.cs b/DurableMultiAgentTemplate/Agent/AgentDecider/AgentDeciderActivity.cs index 86da8b5..68b5f5a 100644 --- a/DurableMultiAgentTemplate/Agent/AgentDecider/AgentDeciderActivity.cs +++ b/DurableMultiAgentTemplate/Agent/AgentDecider/AgentDeciderActivity.cs @@ -5,36 +5,54 @@ using OpenAI.Chat; using DurableMultiAgentTemplate.Shared.Model; using DurableMultiAgentTemplate.Agent.Workers; +using System.Text.Json.Nodes; namespace DurableMultiAgentTemplate.Agent.AgentDecider; -public class AgentDeciderActivity(ChatClient chatClient, ILogger logger) +public class AgentDeciderActivity(ChatClient chatClient, AgentDefinitions agentDefinitions, ILogger logger) { - [Function(AgentActivityName.AgentDeciderActivity)] + [Function(AgentActivityNames.AgentDeciderActivity)] public async Task Run([ActivityTrigger] AgentRequestDto reqData) { var messages = reqData.Messages.ConvertToChatMessageArray(); + var lastAgentMessage = (AgentMessageItem?)reqData.Messages.FindLast(x => x.Role == AgentRole.Agent); + if (lastAgentMessage != null && lastAgentMessage.NextAgentCall != null) + { + ChatMessage[] allMessagesForNextAgentCall = [ + new SystemChatMessage(AgentDeciderPrompt.SystemPromptForNextAgentCall), + .. messages, + ]; + var chatResultForNextAgentCall = await chatClient.CompleteChatAsync( + allMessagesForNextAgentCall, + CreateChatOptions(true) + ); + + if (chatResultForNextAgentCall.Value.FinishReason == ChatFinishReason.ToolCalls) + { + return new AgentDeciderResult( + IsAgentCall: true, + Content: "", + AgentCalls: [.. chatResultForNextAgentCall.Value + .ToolCalls + .Select(toolCall => new AgentCall( + toolCall.FunctionName, + JsonSerializer.Deserialize(toolCall.FunctionArguments))) + ] + ); + } + } + logger.LogInformation("Run AgentDeciderActivity"); ChatMessage[] allMessages = [ new SystemChatMessage(AgentDeciderPrompt.SystemPrompt), .. messages, ]; - ChatCompletionOptions options = new() - { - Tools = { - AgentDefinition.GetDestinationSuggestAgent, - AgentDefinition.GetClimateAgent, - AgentDefinition.GetSightseeingSpotAgent, - AgentDefinition.GetHotelAgent, - AgentDefinition.SubmitReservationAgent - } - }; var chatResult = await chatClient.CompleteChatAsync( allMessages, - options + CreateChatOptions(false) ); if (chatResult.Value.FinishReason == ChatFinishReason.ToolCalls) @@ -44,7 +62,9 @@ public async Task Run([ActivityTrigger] AgentRequestDto reqD Content: "", AgentCalls: [.. chatResult.Value .ToolCalls - .Select(toolCall => new AgentCall(toolCall.FunctionName, JsonDocument.Parse(toolCall.FunctionArguments))) + .Select(toolCall => new AgentCall( + toolCall.FunctionName, + JsonSerializer.Deserialize(toolCall.FunctionArguments))) ] ); } @@ -59,7 +79,19 @@ public async Task Run([ActivityTrigger] AgentRequestDto reqD ); } } - + throw new InvalidOperationException("Invalid OpenAI response"); } + + private ChatCompletionOptions CreateChatOptions(bool requiresUserConfirmation) + { + ChatCompletionOptions options = new(); + var agents = agentDefinitions.GetAgentDefinitions(requiresUserConfirmation); + foreach (var agent in agents) + { + options.Tools.Add(agent.ChatTool); + } + + return options; + } } diff --git a/DurableMultiAgentTemplate/Agent/AgentDecider/AgentDeciderPrompt.cs b/DurableMultiAgentTemplate/Agent/AgentDecider/AgentDeciderPrompt.cs index fc154f9..879ecac 100644 --- a/DurableMultiAgentTemplate/Agent/AgentDecider/AgentDeciderPrompt.cs +++ b/DurableMultiAgentTemplate/Agent/AgentDecider/AgentDeciderPrompt.cs @@ -2,6 +2,18 @@ internal static class AgentDeciderPrompt { + // Orchestrator Agent functions + public const string SystemPromptForNextAgentCall = """ + あなたは、人々が情報を見つけるのを助ける 旅行 AI アシスタントです。 + アシスタントとして、ユーザーからの問いについて必要なツールを選択してください。 + あなたの知識にないことや、使えるツールがない場合は「わかりません」と答えてください。 + ツールは以下の基準で選択してください。 + - ユーザーが明確にツールを呼び出そうとしている場合はツールを選択してください + - ユーザーがツールを呼び出すために渡す情報を変えようとしている場合は「わかりません」と答えてください。 + - ユーザーがツールの呼び出しを終了しようとしている場合は「わかりません」と答えてください。 + """; + + // Orchestrator Agent functions public const string SystemPrompt = """ あなたは、人々が情報を見つけるのを助ける 旅行 AI アシスタントです。 diff --git a/DurableMultiAgentTemplate/Agent/Orchestrator/AgentOrchestrator.cs b/DurableMultiAgentTemplate/Agent/Orchestrator/AgentOrchestrator.cs index 1c0ea9f..2f69c36 100644 --- a/DurableMultiAgentTemplate/Agent/Orchestrator/AgentOrchestrator.cs +++ b/DurableMultiAgentTemplate/Agent/Orchestrator/AgentOrchestrator.cs @@ -5,6 +5,9 @@ using DurableMultiAgentTemplate.Shared.Model; using DurableMultiAgentTemplate.Agent.AgentDecider; using DurableMultiAgentTemplate.Agent.Synthesizer; +using System.Text.Json.Nodes; +using System.Text.Json; +using DurableMultiAgentTemplate.Agent.Workers; namespace DurableMultiAgentTemplate.Agent.Orchestrator; @@ -27,9 +30,9 @@ public async Task RunOrchestrator( ArgumentNullException.ThrowIfNull(reqData); context.SetCustomStatus(new AgentOrchestratorStatus(AgentOrchestratorStep.AgentDeciderActivity, - [new AgentCall(AgentActivityName.AgentDeciderActivity, reqData)])); + [new AgentCall(AgentActivityNames.AgentDeciderActivity, JsonSerializer.SerializeToElement(reqData))])); // AgentDecider呼び出し(呼び出すAgentの決定) - var agentDeciderResult = await context.CallActivityAsync(AgentActivityName.AgentDeciderActivity, reqData, DefaultTaskOptions); + var agentDeciderResult = await context.CallActivityAsync(AgentActivityNames.AgentDeciderActivity, reqData, DefaultTaskOptions); // AgentDeciderでエージェントを呼び出さない場合には、そのまま返す if (!agentDeciderResult.IsAgentCall) @@ -37,11 +40,11 @@ public async Task RunOrchestrator( logger.LogInformation("No agent call happened"); if (reqData.RequireAdditionalInfo) { - return new AgentResponseWithAdditionalInfoDto(agentDeciderResult.Content); + return new AgentResponseWithAdditionalInfoDto(new(agentDeciderResult.Content)); } else { - return new AgentResponseDto(agentDeciderResult.Content); + return new AgentResponseDto(new(agentDeciderResult.Content)); } } @@ -49,10 +52,10 @@ public async Task RunOrchestrator( logger.LogInformation("Agent call happened"); context.SetCustomStatus( new AgentOrchestratorStatus(AgentOrchestratorStep.WorkerAgentActivity, agentDeciderResult.AgentCalls)); - var parallelAgentCall = new List>(); + var parallelAgentCall = new List>(); foreach (var agentCall in agentDeciderResult.AgentCalls) { - parallelAgentCall.Add(context.CallActivityAsync(agentCall.AgentName, agentCall.Arguments, DefaultTaskOptions)); + parallelAgentCall.Add(context.CallActivityAsync(agentCall.AgentName, agentCall.Arguments, DefaultTaskOptions)); } await Task.WhenAll(parallelAgentCall); @@ -65,14 +68,14 @@ public async Task RunOrchestrator( ); context.SetCustomStatus(new AgentOrchestratorStatus(AgentOrchestratorStep.SynthesizerActivity, - [new AgentCall(AgentActivityName.SynthesizerActivity, synthesizerRequest)])); + [new AgentCall(AgentActivityNames.SynthesizerActivity, JsonSerializer.SerializeToElement(synthesizerRequest))])); if (reqData.RequireAdditionalInfo) { - return await context.CallActivityAsync(AgentActivityName.SynthesizerWithAdditionalInfoActivity, synthesizerRequest, DefaultTaskOptions); + return await context.CallActivityAsync(AgentActivityNames.SynthesizerWithAdditionalInfoActivity, synthesizerRequest, DefaultTaskOptions); } else { - return await context.CallActivityAsync(AgentActivityName.SynthesizerActivity, synthesizerRequest, DefaultTaskOptions); + return await context.CallActivityAsync(AgentActivityNames.SynthesizerActivity, synthesizerRequest, DefaultTaskOptions); } } } diff --git a/DurableMultiAgentTemplate/Agent/Synthesizer/SynthesizerActivity.cs b/DurableMultiAgentTemplate/Agent/Synthesizer/SynthesizerActivity.cs index e975e7d..514a40e 100644 --- a/DurableMultiAgentTemplate/Agent/Synthesizer/SynthesizerActivity.cs +++ b/DurableMultiAgentTemplate/Agent/Synthesizer/SynthesizerActivity.cs @@ -9,7 +9,7 @@ namespace DurableMultiAgentTemplate.Agent.Synthesizer; public class SynthesizerActivity(ChatClient chatClient, ILogger logger) { - [Function(AgentActivityName.SynthesizerActivity)] + [Function(AgentActivityNames.SynthesizerActivity)] public async Task Run([ActivityTrigger] SynthesizerRequest req) { logger.LogInformation("Run SynthesizerActivity"); @@ -27,8 +27,13 @@ .. req.AgentRequest.Messages.ConvertToChatMessageArray(), if (chatResult.Value.FinishReason == ChatFinishReason.Stop) { - return new AgentResponseDto( - chatResult.Value.Content.First().Text, + var nextAgentCall = req.AgentCallResult + .Select(x => x.NextAgentCall) + .SingleOrDefault(x => x != null); + return new( + new(nextAgentCall == null ? AgentMessageType.Info : AgentMessageType.Ask, + chatResult.Value.Content.First().Text, + nextAgentCall), req.CalledAgentNames); } diff --git a/DurableMultiAgentTemplate/Agent/Synthesizer/SynthesizerRequest.cs b/DurableMultiAgentTemplate/Agent/Synthesizer/SynthesizerRequest.cs index b8d84ea..901a964 100644 --- a/DurableMultiAgentTemplate/Agent/Synthesizer/SynthesizerRequest.cs +++ b/DurableMultiAgentTemplate/Agent/Synthesizer/SynthesizerRequest.cs @@ -1,4 +1,5 @@ -using DurableMultiAgentTemplate.Shared.Model; +using DurableMultiAgentTemplate.Agent.Workers; +using DurableMultiAgentTemplate.Shared.Model; namespace DurableMultiAgentTemplate.Agent.Synthesizer; @@ -9,6 +10,6 @@ namespace DurableMultiAgentTemplate.Agent.Synthesizer; /// The agent request details. /// The names of the called agents. public record SynthesizerRequest( - List AgentCallResult, + List AgentCallResult, AgentRequestDto AgentRequest, List CalledAgentNames); diff --git a/DurableMultiAgentTemplate/Agent/Synthesizer/SynthesizerWithAdditionalInfoActivity.cs b/DurableMultiAgentTemplate/Agent/Synthesizer/SynthesizerWithAdditionalInfoActivity.cs index 4df8df2..0804fed 100644 --- a/DurableMultiAgentTemplate/Agent/Synthesizer/SynthesizerWithAdditionalInfoActivity.cs +++ b/DurableMultiAgentTemplate/Agent/Synthesizer/SynthesizerWithAdditionalInfoActivity.cs @@ -12,7 +12,7 @@ namespace DurableMultiAgentTemplate.Agent.Synthesizer; public class SynthesizerWithAdditionalInfoActivity(ChatClient chatClient, ILogger logger) { - [Function(AgentActivityName.SynthesizerWithAdditionalInfoActivity)] + [Function(AgentActivityNames.SynthesizerWithAdditionalInfoActivity)] public async Task Run([ActivityTrigger] SynthesizerRequest req) { logger.LogInformation("Run SynthesizerActivity"); @@ -44,7 +44,7 @@ .. req.AgentRequest.Messages.ConvertToChatMessageArray(), throw new InvalidOperationException("Failed to deserialize the result"); return new AgentResponseWithAdditionalInfoDto( - res.Content ?? throw new InvalidOperationException("Content is null"), + new(res.Content ?? throw new InvalidOperationException("Content is null")), req.CalledAgentNames, res.AdditionalInfo ?? throw new InvalidOperationException("AdditionalInfo is null")); } diff --git a/DurableMultiAgentTemplate/Agent/Workers/AgentDefinition.cs b/DurableMultiAgentTemplate/Agent/Workers/AgentDefinition.cs index 34ea944..716deff 100644 --- a/DurableMultiAgentTemplate/Agent/Workers/AgentDefinition.cs +++ b/DurableMultiAgentTemplate/Agent/Workers/AgentDefinition.cs @@ -1,40 +1,72 @@ using DurableMultiAgentTemplate.Json; using DurableMultiAgentTemplate.Model; -using Microsoft.Azure.Cosmos.Core.Networking; using OpenAI.Chat; namespace DurableMultiAgentTemplate.Agent.Workers; //https://learn.microsoft.com/ja-jp/azure/ai-services/openai/how-to/dotnet-migration?tabs=stable -internal class AgentDefinition +public record AgentDefinition(string AgentActivityName, ChatTool ChatTool, bool RequiresUserConfirmation); + +public class AgentDefinitions { - public static readonly ChatTool GetDestinationSuggestAgent = ChatTool.CreateFunctionTool( - functionName: AgentActivityName.GetDestinationSuggestAgent, - functionDescription: "希望の行き先に求める条件を自然言語で与えると、おすすめの旅行先を提案します。", - functionParameters: JsonSchemaGenerator.GenerateSchemaAsBinaryData(SourceGenerationContext.Default.GetDestinationSuggestRequest)); - - public static readonly ChatTool GetClimateAgent = ChatTool.CreateFunctionTool( - functionName: AgentActivityName.GetClimateAgent, - functionDescription: "指定された場所の気候を取得します。", - functionParameters: JsonSchemaGenerator.GenerateSchemaAsBinaryData(SourceGenerationContext.Default.GetClimateRequest)); - - public static readonly ChatTool GetSightseeingSpotAgent = ChatTool.CreateFunctionTool( - functionName: AgentActivityName.GetSightseeingSpotAgent, - functionDescription: "指定された場所の観光名所を取得します。", - functionParameters: JsonSchemaGenerator.GenerateSchemaAsBinaryData(SourceGenerationContext.Default.GetSightseeingSpotRequest)); - - public static readonly ChatTool GetHotelAgent = ChatTool.CreateFunctionTool( - functionName: AgentActivityName.GetHotelAgent, - functionDescription: "指定された場所のホテルを取得します。", - functionParameters: JsonSchemaGenerator.GenerateSchemaAsBinaryData(SourceGenerationContext.Default.GetHotelRequest)); - - public static readonly ChatTool SubmitReservationAgent = ChatTool.CreateFunctionTool( - functionName: AgentActivityName.SubmitReservationAgent, - functionDescription: "宿泊先の予約を行います。予約には利用者の最終確認が必要です。", - functionParameters: JsonSchemaGenerator.GenerateSchemaAsBinaryData(SourceGenerationContext.Default.SubmitReservationRequest)); - - public static readonly ChatTool HumanInTheLoopAgent = ChatTool.CreateFunctionTool( - functionName: AgentActivityName.HumanInTheLoopAgent, - functionDescription: "利用者に最終確認を行います。", - functionParameters: JsonSchemaGenerator.GenerateSchemaAsBinaryData(SourceGenerationContext.Default.HumanInTheLoopRequest)); + public static readonly AgentDefinition GetDestinationSuggestAgent = new( + AgentActivityNames.GetDestinationSuggestAgent, + ChatTool.CreateFunctionTool( + functionName: AgentActivityNames.GetDestinationSuggestAgent, + functionDescription: "希望の行き先に求める条件を自然言語で与えると、おすすめの旅行先を提案します。", + functionParameters: JsonSchemaGenerator.GenerateSchemaAsBinaryData(SourceGenerationContext.Default.GetDestinationSuggestRequest)), + false); + + public static readonly AgentDefinition GetClimateAgent = new( + AgentActivityNames.GetClimateAgent, + ChatTool.CreateFunctionTool( + functionName: AgentActivityNames.GetClimateAgent, + functionDescription: "指定された場所の気候を取得します。", + functionParameters: JsonSchemaGenerator.GenerateSchemaAsBinaryData(SourceGenerationContext.Default.GetClimateRequest)), + false); + + public static readonly AgentDefinition GetSightseeingSpotAgent = new( + AgentActivityNames.GetSightseeingSpotAgent, + ChatTool.CreateFunctionTool( + functionName: AgentActivityNames.GetSightseeingSpotAgent, + functionDescription: "指定された場所の観光名所を取得します。", + functionParameters: JsonSchemaGenerator.GenerateSchemaAsBinaryData(SourceGenerationContext.Default.GetSightseeingSpotRequest)), + false); + + public static readonly AgentDefinition GetHotelAgent = new( + AgentActivityNames.GetHotelAgent, + ChatTool.CreateFunctionTool( + functionName: AgentActivityNames.GetHotelAgent, + functionDescription: "指定された場所のホテルを取得します。", + functionParameters: JsonSchemaGenerator.GenerateSchemaAsBinaryData(SourceGenerationContext.Default.GetHotelRequest)), + false); + + public static readonly AgentDefinition SubmitReservationAgent = new( + AgentActivityNames.SubmitReservationAgent, + ChatTool.CreateFunctionTool( + functionName: AgentActivityNames.SubmitReservationAgent, + functionDescription: "宿泊先の予約を行います。", + functionParameters: JsonSchemaGenerator.GenerateSchemaAsBinaryData(SourceGenerationContext.Default.HotelReservationRequest)), + false); + + public static readonly AgentDefinition CommitReservationAgent = new( + AgentActivityNames.CommitReservationAgent, + ChatTool.CreateFunctionTool( + functionName: AgentActivityNames.CommitReservationAgent, + functionDescription: "宿泊先の予約の確定を行います。", + functionParameters: JsonSchemaGenerator.GenerateSchemaAsBinaryData(SourceGenerationContext.Default.HotelReservationRequest)), + true); + + public static AgentDefinition[] AllAgents => new[] + { + GetDestinationSuggestAgent, + GetClimateAgent, + GetHotelAgent, + GetSightseeingSpotAgent, + SubmitReservationAgent, + CommitReservationAgent + }; + + public AgentDefinition[] GetAgentDefinitions(bool requiresUserConfirmation) => + AllAgents.Where(agent => agent.RequiresUserConfirmation == requiresUserConfirmation).ToArray(); } diff --git a/DurableMultiAgentTemplate/Agent/Workers/GetClimateAgent/GetClimateActivity.cs b/DurableMultiAgentTemplate/Agent/Workers/GetClimateAgent/GetClimateActivity.cs index 6fc77eb..525f7ad 100644 --- a/DurableMultiAgentTemplate/Agent/Workers/GetClimateAgent/GetClimateActivity.cs +++ b/DurableMultiAgentTemplate/Agent/Workers/GetClimateAgent/GetClimateActivity.cs @@ -7,8 +7,8 @@ namespace DurableMultiAgentTemplate.Agent.Workers.GetClimateAgent; public class GetClimateActivity(ChatClient chatClient, ILogger logger) { - [Function(AgentActivityName.GetClimateAgent)] - public async Task RunAsync([ActivityTrigger] GetClimateRequest req) + [Function(AgentActivityNames.GetClimateAgent)] + public async Task RunAsync([ActivityTrigger] GetClimateRequest req) { if(Random.Shared.Next(0, 10) < 3) { @@ -21,48 +21,48 @@ public async Task RunAsync([ActivityTrigger] GetClimateRequest req) // This is sample code. Replace this with your own logic. var result = $""" - {req.Location}の気候は年間を通じて暖かく、**熱帯モンスーン気候**に分類されます。大きく分けて**乾季**と**雨季**があり、それぞれ異なる特徴があります。 - --- + {req.Location}の気候は年間を通じて暖かく、**熱帯モンスーン気候**に分類されます。大きく分けて**乾季**と**雨季**があり、それぞれ異なる特徴があります。 + --- - ### 平均気温 - - **年間を通じて:** 26~30℃程度 - - **日中:** 30℃前後まで上がることが多い。 - - **夜間:** 23~25℃程度で過ごしやすい。 + ### 平均気温 + - **年間を通じて:** 26~30℃程度 + - **日中:** 30℃前後まで上がることが多い。 + - **夜間:** 23~25℃程度で過ごしやすい。 - --- + --- - ### 乾季(5月~10月) - - **特徴:** - - 晴れの日が多く、湿度が比較的低い。 - - 海や観光に最適なシーズン。 - - 朝晩は涼しい風が吹き、快適に過ごせる。 - - **おすすめのアクティビティ:** - - ビーチでのリラックス - - ダイビングやサーフィン - - ウブド周辺でのトレッキングや文化体験 + ### 乾季(5月~10月) + - **特徴:** + - 晴れの日が多く、湿度が比較的低い。 + - 海や観光に最適なシーズン。 + - 朝晩は涼しい風が吹き、快適に過ごせる。 + - **おすすめのアクティビティ:** + - ビーチでのリラックス + - ダイビングやサーフィン + - ウブド周辺でのトレッキングや文化体験 - --- + --- - ### 雨季(11月~4月) - - **特徴:** - - 短時間のスコールが頻繁に発生。 - - 湿度が高く蒸し暑い。 - - 雨が降ってもその後すぐに晴れることが多い。 - - **おすすめのアクティビティ:** - - 室内スパやリゾート内でのリラクゼーション - - ヒンズー寺院巡りや地元の文化体験 - - 雨季特有の緑が豊かな景色を楽しむ + ### 雨季(11月~4月) + - **特徴:** + - 短時間のスコールが頻繁に発生。 + - 湿度が高く蒸し暑い。 + - 雨が降ってもその後すぐに晴れることが多い。 + - **おすすめのアクティビティ:** + - 室内スパやリゾート内でのリラクゼーション + - ヒンズー寺院巡りや地元の文化体験 + - 雨季特有の緑が豊かな景色を楽しむ - --- + --- - ### 服装のポイント - - **乾季:** 半袖や軽い素材の服装でOK。朝晩の冷えに備えて薄手の上着を用意。 - - **雨季:** 雨具(折りたたみ傘やレインコート)があると便利。速乾性の服装がおすすめ。 + ### 服装のポイント + - **乾季:** 半袖や軽い素材の服装でOK。朝晩の冷えに備えて薄手の上着を用意。 + - **雨季:** 雨具(折りたたみ傘やレインコート)があると便利。速乾性の服装がおすすめ。 - --- + --- - {req.Location}は雨季でも旅行を楽しめるよう工夫されているため、いつ訪れても魅力的です。乾季の5月~10月が観光のベストシーズンとされていますが、雨季なら緑豊かな景観と比較的空いている観光地を楽しむことができます。 - """; + {req.Location}は雨季でも旅行を楽しめるよう工夫されているため、いつ訪れても魅力的です。乾季の5月~10月が観光のベストシーズンとされていますが、雨季なら緑豊かな景観と比較的空いている観光地を楽しむことができます。 + """; return result; } diff --git a/DurableMultiAgentTemplate/Agent/Workers/GetDestinationSuggestAgent/GetDestinationSuggestActivity.cs b/DurableMultiAgentTemplate/Agent/Workers/GetDestinationSuggestAgent/GetDestinationSuggestActivity.cs index d5aca08..759fea4 100644 --- a/DurableMultiAgentTemplate/Agent/Workers/GetDestinationSuggestAgent/GetDestinationSuggestActivity.cs +++ b/DurableMultiAgentTemplate/Agent/Workers/GetDestinationSuggestAgent/GetDestinationSuggestActivity.cs @@ -7,8 +7,8 @@ namespace DurableMultiAgentTemplate.Agent.Workers.GetDestinationSuggestAgent; public class GetDestinationSuggestActivity(ChatClient chatClient, ILogger logger) { - [Function(AgentActivityName.GetDestinationSuggestAgent)] - public async Task RunAsync([ActivityTrigger] GetDestinationSuggestRequest req) + [Function(AgentActivityNames.GetDestinationSuggestAgent)] + public async Task RunAsync([ActivityTrigger] GetDestinationSuggestRequest req) { if (Random.Shared.Next(0, 10) < 3) { @@ -21,45 +21,45 @@ public async Task RunAsync([ActivityTrigger] GetDestinationSuggestReques // This is sample code. Replace this with your own logic. var result = $""" - {req.SearchTerm}の条件でおすすめの旅行先を提案します。好みに応じて選んでください。 - ### 国内 - 1. **沖縄本島** - - 透明度の高いビーチ、首里城、美ら海水族館など観光名所が豊富。 - - 冬でも暖かく、リラックスした雰囲気を楽しめる。 + {req.SearchTerm}の条件でおすすめの旅行先を提案します。好みに応じて選んでください。 + ### 国内 + 1. **沖縄本島** + - 透明度の高いビーチ、首里城、美ら海水族館など観光名所が豊富。 + - 冬でも暖かく、リラックスした雰囲気を楽しめる。 - 2. **石垣島・宮古島** - - 南国らしい美しい自然が広がり、ダイビングやシュノーケリングが人気。 - - 島ならではの郷土料理も楽しめる。 + 2. **石垣島・宮古島** + - 南国らしい美しい自然が広がり、ダイビングやシュノーケリングが人気。 + - 島ならではの郷土料理も楽しめる。 - 3. **鹿児島・奄美大島** - - 奄美の黒糖焼酎や島唄、特有の自然環境を満喫。 - - 亜熱帯の雰囲気を楽しめる。 + 3. **鹿児島・奄美大島** + - 奄美の黒糖焼酎や島唄、特有の自然環境を満喫。 + - 亜熱帯の雰囲気を楽しめる。 - --- + --- - ### 海外 - 1. **ハワイ(オアフ島やマウイ島)** - - 年間を通して快適な気温。ビーチリゾートやトレッキングなど多様なアクティビティが可能。 - - 日本語対応も充実していて安心。 + ### 海外 + 1. **ハワイ(オアフ島やマウイ島)** + - 年間を通して快適な気温。ビーチリゾートやトレッキングなど多様なアクティビティが可能。 + - 日本語対応も充実していて安心。 - 2. **タイ(プーケットやクラビ)** - - 手頃な価格で楽しめるリゾート地。温かい気候とタイ料理も魅力。 - - 観光名所や島巡りもおすすめ。 + 2. **タイ(プーケットやクラビ)** + - 手頃な価格で楽しめるリゾート地。温かい気候とタイ料理も魅力。 + - 観光名所や島巡りもおすすめ。 - 3. **バリ島(インドネシア)** - - 高級リゾートから手軽な宿泊施設まで選択肢が広い。 - - ヒンズー文化と自然が織りなすユニークな雰囲気を堪能。 + 3. **バリ島(インドネシア)** + - 高級リゾートから手軽な宿泊施設まで選択肢が広い。 + - ヒンズー文化と自然が織りなすユニークな雰囲気を堪能。 - 4. **オーストラリア(ケアンズやゴールドコースト)** - - グレートバリアリーフでの海洋アクティビティが人気。 - - 暖かい気候で自然と都市観光をバランス良く楽しめる。 + 4. **オーストラリア(ケアンズやゴールドコースト)** + - グレートバリアリーフでの海洋アクティビティが人気。 + - 暖かい気候で自然と都市観光をバランス良く楽しめる。 - 5. **グアムやサイパン** - - 日本から近く、短期間でも楽しめる南国リゾート。 - - のんびり過ごしたい方に最適。 + 5. **グアムやサイパン** + - 日本から近く、短期間でも楽しめる南国リゾート。 + - のんびり過ごしたい方に最適。 - どれも暖かい気候を楽しめる場所です。予算や旅行期間に合わせてお選びください! - """; + どれも暖かい気候を楽しめる場所です。予算や旅行期間に合わせてお選びください! + """; return result; } diff --git a/DurableMultiAgentTemplate/Agent/Workers/GetHotelAgent/GetHotelActivity.cs b/DurableMultiAgentTemplate/Agent/Workers/GetHotelAgent/GetHotelActivity.cs index 1135ab0..8a564a5 100644 --- a/DurableMultiAgentTemplate/Agent/Workers/GetHotelAgent/GetHotelActivity.cs +++ b/DurableMultiAgentTemplate/Agent/Workers/GetHotelAgent/GetHotelActivity.cs @@ -7,8 +7,8 @@ namespace DurableMultiAgentTemplate.Agent.Workers.GetHotelAgent; public class GetHotelActivity(ChatClient chatClient, ILogger logger) { - [Function(AgentActivityName.GetHotelAgent)] - public async Task Run([ActivityTrigger] GetHotelRequest req) + [Function(AgentActivityNames.GetHotelAgent)] + public async Task Run([ActivityTrigger] GetHotelRequest req) { if (Random.Shared.Next(0, 10) < 3) { @@ -20,53 +20,53 @@ public async Task Run([ActivityTrigger] GetHotelRequest req) await Task.Delay(3000); // This is sample code. Replace this with your own logic. var result = $""" - {req.Location}に以下の4件のホテルがあります。 - --- + {req.Location}に以下の4件のホテルがあります。 + --- - ### 1. **リラ・オアシスホテル(Rila Oasis Hotel)** - - **所在地**: 海沿いのリゾート地 - - **テーマ**: 癒しとウェルネス - - **特徴**: - - 海を望むインフィニティプール - - スパ・ヨガ・瞑想などのウェルネスプログラム - - 地元食材を使ったヘルシーレストラン - - 落ち着いた内装でリラクゼーションを重視 + ### 1. **リラ・オアシスホテル(Rila Oasis Hotel)** + - **所在地**: 海沿いのリゾート地 + - **テーマ**: 癒しとウェルネス + - **特徴**: + - 海を望むインフィニティプール + - スパ・ヨガ・瞑想などのウェルネスプログラム + - 地元食材を使ったヘルシーレストラン + - 落ち着いた内装でリラクゼーションを重視 - --- + --- - ### 2. **クラウン・スカイタワー(Crown Skytower)** - - **所在地**: 都市部の中心地 - - **テーマ**: モダンでラグジュアリーな都市体験 - - **特徴**: - - 高層階からの夜景が楽しめるラグジュアリールーム - - 最先端の設備を備えたビジネスセンター - - ミシュラン星付きレストラン併設 - - スタイリッシュなデザインとスマートホテル機能 + ### 2. **クラウン・スカイタワー(Crown Skytower)** + - **所在地**: 都市部の中心地 + - **テーマ**: モダンでラグジュアリーな都市体験 + - **特徴**: + - 高層階からの夜景が楽しめるラグジュアリールーム + - 最先端の設備を備えたビジネスセンター + - ミシュラン星付きレストラン併設 + - スタイリッシュなデザインとスマートホテル機能 - --- + --- - ### 3. **フォレスト・ヒドゥンロッジ(Forest Hidden Lodge)** - - **所在地**: 森に囲まれた山間部 - - **テーマ**: 自然と共に過ごす冒険と安らぎ - - **特徴**: - - 木造のコテージ風宿泊施設 - - トレッキングや星空観察のアクティビティ - - 暖炉付きラウンジと温泉 - - 地元の伝統料理を楽しめるダイニング + ### 3. **フォレスト・ヒドゥンロッジ(Forest Hidden Lodge)** + - **所在地**: 森に囲まれた山間部 + - **テーマ**: 自然と共に過ごす冒険と安らぎ + - **特徴**: + - 木造のコテージ風宿泊施設 + - トレッキングや星空観察のアクティビティ + - 暖炉付きラウンジと温泉 + - 地元の伝統料理を楽しめるダイニング - --- + --- - ### 4. **アルテ・シンフォニア(Arte Sinfonia)** - - **所在地**: 歴史的な街並みの一角 - - **テーマ**: アートと文化の融合 - - **特徴**: - - 地元アーティストの作品を展示したギャラリー併設 - - クラシック音楽のライブ演奏が行われるラウンジ - - アンティーク家具を取り入れたクラシカルな内装 - - 歴史的建築物をリノベーションした趣のある空間 + ### 4. **アルテ・シンフォニア(Arte Sinfonia)** + - **所在地**: 歴史的な街並みの一角 + - **テーマ**: アートと文化の融合 + - **特徴**: + - 地元アーティストの作品を展示したギャラリー併設 + - クラシック音楽のライブ演奏が行われるラウンジ + - アンティーク家具を取り入れたクラシカルな内装 + - 歴史的建築物をリノベーションした趣のある空間 - --- - """; + --- + """; return result; } diff --git a/DurableMultiAgentTemplate/Agent/Workers/GetSightseeingSpotAgent/GetSightseeingSpotActivity.cs b/DurableMultiAgentTemplate/Agent/Workers/GetSightseeingSpotAgent/GetSightseeingSpotActivity.cs index acef8c3..3692539 100644 --- a/DurableMultiAgentTemplate/Agent/Workers/GetSightseeingSpotAgent/GetSightseeingSpotActivity.cs +++ b/DurableMultiAgentTemplate/Agent/Workers/GetSightseeingSpotAgent/GetSightseeingSpotActivity.cs @@ -7,8 +7,8 @@ namespace DurableMultiAgentTemplate.Agent.Workers.GetSightseeingSpotAgent; public class GetSightseeingSpotActivity(ChatClient chatClient, ILogger logger) { - [Function(AgentActivityName.GetSightseeingSpotAgent)] - public async Task RunAsync([ActivityTrigger] GetSightseeingSpotRequest req) + [Function(AgentActivityNames.GetSightseeingSpotAgent)] + public async Task RunAsync([ActivityTrigger] GetSightseeingSpotRequest req) { if (Random.Shared.Next(0, 10) < 3) { @@ -21,90 +21,90 @@ public async Task RunAsync([ActivityTrigger] GetSightseeingSpotRequest r // This is sample code. Replace this with your own logic. var result = $""" - {req.Location}には美しい自然、歴史的な寺院、ユニークな文化体験が楽しめる観光名所がたくさんあります!以下におすすめの観光スポットをまとめました。 + {req.Location}には美しい自然、歴史的な寺院、ユニークな文化体験が楽しめる観光名所がたくさんあります!以下におすすめの観光スポットをまとめました。 - --- + --- - ### 1. **寺院** - #### **タナロット寺院(Pura Tanah Lot)** - - 海の上に建つ寺院で、夕日とともに見える景色が絶景。 - - {req.Location}を代表する観光名所の一つ。 + ### 1. **寺院** + #### **タナロット寺院(Pura Tanah Lot)** + - 海の上に建つ寺院で、夕日とともに見える景色が絶景。 + - {req.Location}を代表する観光名所の一つ。 - #### **ウルワツ寺院(Pura Luhur Uluwatu)** - - 崖の上に位置する寺院。インド洋を見渡す絶景スポット。 - - 夕方には伝統舞踊「ケチャダンス」の公演が行われる。 + #### **ウルワツ寺院(Pura Luhur Uluwatu)** + - 崖の上に位置する寺院。インド洋を見渡す絶景スポット。 + - 夕方には伝統舞踊「ケチャダンス」の公演が行われる。 - #### **ティルタ・エンプル寺院(Pura Tirta Empul)** - - 聖水が湧き出る寺院で、地元の人々や観光客が浄化の儀式を体験。 - - 神聖な雰囲気を感じることができる。 + #### **ティルタ・エンプル寺院(Pura Tirta Empul)** + - 聖水が湧き出る寺院で、地元の人々や観光客が浄化の儀式を体験。 + - 神聖な雰囲気を感じることができる。 - --- + --- - ### 2. **自然** - #### **ウブドのライステラス(Tegalalang Rice Terrace)** - - 階段状に広がる田んぼの風景が特徴的。 - - 写真撮影やトレッキングに最適。 + ### 2. **自然** + #### **ウブドのライステラス(Tegalalang Rice Terrace)** + - 階段状に広がる田んぼの風景が特徴的。 + - 写真撮影やトレッキングに最適。 - #### **モンキーフォレスト(Sacred Monkey Forest Sanctuary)** - - ウブドにある野生の猿が暮らす自然保護区。 - - 緑豊かな森と寺院が融合した神秘的な場所。 + #### **モンキーフォレスト(Sacred Monkey Forest Sanctuary)** + - ウブドにある野生の猿が暮らす自然保護区。 + - 緑豊かな森と寺院が融合した神秘的な場所。 - #### **キンタマーニ高原とバトゥール山** - - バトゥール火山とカルデラ湖の壮大な景色を楽しめる。 - - トレッキングや朝日鑑賞がおすすめ。 + #### **キンタマーニ高原とバトゥール山** + - バトゥール火山とカルデラ湖の壮大な景色を楽しめる。 + - トレッキングや朝日鑑賞がおすすめ。 - --- + --- - ### 3. **ビーチ** - #### **クタビーチ** - - サーフィン初心者に人気のスポット。 - - 多くのレストランやショップが近くにあり、賑やかな雰囲気。 + ### 3. **ビーチ** + #### **クタビーチ** + - サーフィン初心者に人気のスポット。 + - 多くのレストランやショップが近くにあり、賑やかな雰囲気。 - #### **ヌサ・ドゥア** - - 高級リゾートエリアで、静かなビーチと透明度の高い海が魅力。 - - シュノーケリングやジェットスキーなどのマリンスポーツも楽しめる。 + #### **ヌサ・ドゥア** + - 高級リゾートエリアで、静かなビーチと透明度の高い海が魅力。 + - シュノーケリングやジェットスキーなどのマリンスポーツも楽しめる。 - #### **ジンバランビーチ** - - サンセットディナーが有名で、新鮮なシーフード料理が楽しめる。 - - 夕日を眺めながらの食事は特別な体験。 + #### **ジンバランビーチ** + - サンセットディナーが有名で、新鮮なシーフード料理が楽しめる。 + - 夕日を眺めながらの食事は特別な体験。 - --- + --- - ### 4. **文化体験** - #### **ウブド王宮(Ubud Palace)** - - 伝統的な建築が美しい王宮で、舞踊のパフォーマンスが行われる。 - - ウブド市場も近くにあり、ショッピングにも最適。 + ### 4. **文化体験** + #### **ウブド王宮(Ubud Palace)** + - 伝統的な建築が美しい王宮で、舞踊のパフォーマンスが行われる。 + - ウブド市場も近くにあり、ショッピングにも最適。 - #### **伝統舞踊** - - ケチャダンスやレゴンダンスなど、寺院や専用会場で観賞できる。 - - 神話や伝説が題材となった迫力あるパフォーマンス。 + #### **伝統舞踊** + - ケチャダンスやレゴンダンスなど、寺院や専用会場で観賞できる。 + - 神話や伝説が題材となった迫力あるパフォーマンス。 - --- + --- - ### 5. **アクティビティ** - #### **サファリ&マリンパーク** - - 動物園や水族館が融合したエンターテイメント施設。 - - サファリツアーやアニマルショーが楽しめる。 + ### 5. **アクティビティ** + #### **サファリ&マリンパーク** + - 動物園や水族館が融合したエンターテイメント施設。 + - サファリツアーやアニマルショーが楽しめる。 - #### **ラフティング(アユン川)** - - 緑に囲まれた川を下る冒険感あふれる体験。 - - 初心者でも楽しめるコースが多い。 + #### **ラフティング(アユン川)** + - 緑に囲まれた川を下る冒険感あふれる体験。 + - 初心者でも楽しめるコースが多い。 - --- + --- - ### 6. **近隣の島** - #### **ヌサペニダ島** - - {req.Location}から船でアクセス可能な離島。 - - クリスタルベイやケリンキンビーチの絶景が有名。 + ### 6. **近隣の島** + #### **ヌサペニダ島** + - {req.Location}から船でアクセス可能な離島。 + - クリスタルベイやケリンキンビーチの絶景が有名。 - #### **レンボンガン島** - - マングローブツアーやサンゴ礁シュノーケリングが楽しめる。 - - のんびりとした雰囲気の島。 + #### **レンボンガン島** + - マングローブツアーやサンゴ礁シュノーケリングが楽しめる。 + - のんびりとした雰囲気の島。 - --- + --- - {req.Location}は多様な楽しみ方ができるため、目的に応じて行き先を選んでみてください! - """; + {req.Location}は多様な楽しみ方ができるため、目的に応じて行き先を選んでみてください! + """; return result; } diff --git a/DurableMultiAgentTemplate/Agent/Workers/HotelReservationAgent/HotelReservationActivity.cs b/DurableMultiAgentTemplate/Agent/Workers/HotelReservationAgent/HotelReservationActivity.cs new file mode 100644 index 0000000..2fecebb --- /dev/null +++ b/DurableMultiAgentTemplate/Agent/Workers/HotelReservationAgent/HotelReservationActivity.cs @@ -0,0 +1,47 @@ +using System.Text.Json; +using Microsoft.Azure.Functions.Worker; +using OpenAI.Chat; + +namespace DurableMultiAgentTemplate.Agent.Workers.HotelReservationAgent; + +public class HotelReservationActivity(ChatClient chatClient)//, CosmosClient cosmosClient) +{ + [Function(AgentActivityNames.SubmitReservationAgent)] + public async Task SubmitReservationRequestAsync([ActivityTrigger] HotelReservationRequest req) + { + // Simulate a delay + await Task.Delay(3000); + + return new( + $""" + 以下の内容で予約を作成してもよろしいでしょうか? + -------------------------------- + ホテル名:{req.Destination} + チェックイン日:{req.CheckIn} + チェックアウト日:{req.CheckOut} + 人数:{req.GuestsCount} 名 + -------------------------------- + """, + new(AgentActivityNames.CommitReservationAgent, JsonSerializer.SerializeToElement(req))); + } + + [Function(AgentActivityNames.CommitReservationAgent)] + public async Task CommitReservationAsync([ActivityTrigger] HotelReservationRequest req) + { + // Simulate a delay + await Task.Delay(3000); + + // This is sample code. Replace this with your own logic. + var result = $""" + 予約を行いました。予約番号は {Guid.NewGuid()} です。 + -------------------------------- + ホテル名:{req.Destination} + チェックイン日:{req.CheckIn} + チェックアウト日:{req.CheckOut} + 人数:{req.GuestsCount} 名 + -------------------------------- + """; + + return result; + } +} diff --git a/DurableMultiAgentTemplate/Agent/Workers/SubmitReservationAgent/SubmitReservationRequest.cs b/DurableMultiAgentTemplate/Agent/Workers/HotelReservationAgent/HotelReservationRequest.cs similarity index 77% rename from DurableMultiAgentTemplate/Agent/Workers/SubmitReservationAgent/SubmitReservationRequest.cs rename to DurableMultiAgentTemplate/Agent/Workers/HotelReservationAgent/HotelReservationRequest.cs index 2cbf6bc..95657f0 100644 --- a/DurableMultiAgentTemplate/Agent/Workers/SubmitReservationAgent/SubmitReservationRequest.cs +++ b/DurableMultiAgentTemplate/Agent/Workers/HotelReservationAgent/HotelReservationRequest.cs @@ -1,8 +1,8 @@ using System.ComponentModel; -namespace DurableMultiAgentTemplate.Agent.Workers.SubmitReservationAgent; +namespace DurableMultiAgentTemplate.Agent.Workers.HotelReservationAgent; -public record SubmitReservationRequest( +public record HotelReservationRequest( [property: Description("行き先のホテルの名前。")] string Destination, [property: Description("チェックイン日。YYYY/MM/DD形式。")] diff --git a/DurableMultiAgentTemplate/Agent/Workers/HumanInTheLoopAgent/HumanInTheLoopActivity.cs b/DurableMultiAgentTemplate/Agent/Workers/HumanInTheLoopAgent/HumanInTheLoopActivity.cs deleted file mode 100644 index a3eb675..0000000 --- a/DurableMultiAgentTemplate/Agent/Workers/HumanInTheLoopAgent/HumanInTheLoopActivity.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using DurableMultiAgentTemplate.Shared.Model; -using Microsoft.Azure.Functions.Worker; -using Microsoft.Extensions.Logging; -using OpenAI.Chat; - -namespace DurableMultiAgentTemplate.Agent.Workers.HumanInTheLoopAgent; - -public class HumanInTheLoopActivity(ILogger logger, - ChatClient chatClient) -{ - [Function(nameof(HumanInTheLoopActivity))] - public async Task RunAsync([ActivityTrigger] HumanInTheLoopRequest req) - { - logger.LogInformation("HumanInTheLoopActivity called"); - ChatMessage[] allMessages = [ - new SystemChatMessage(HumanInTheLoopPrompt.SystemPrompt), - new UserChatMessage(req.UserRequest), - ]; - - var chatResult = await chatClient.CompleteChatAsync( - allMessages - ); - - if (chatResult.Value.FinishReason == ChatFinishReason.Stop) - { - return chatResult.Value.Content.First().Text; - } - - logger.LogError("Failed to generate a confirmation message for the user, the finish reason is {finishReason}.", chatResult.Value.FinishReason); - throw new InvalidOperationException("Failed to generate a confirmation message for the user"); - } -} diff --git a/DurableMultiAgentTemplate/Agent/Workers/HumanInTheLoopAgent/HumanInTheLoopPrompt.cs b/DurableMultiAgentTemplate/Agent/Workers/HumanInTheLoopAgent/HumanInTheLoopPrompt.cs deleted file mode 100644 index 8e8f177..0000000 --- a/DurableMultiAgentTemplate/Agent/Workers/HumanInTheLoopAgent/HumanInTheLoopPrompt.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace DurableMultiAgentTemplate.Agent.Workers.HumanInTheLoopAgent; - -internal static class HumanInTheLoopPrompt -{ - public const string SystemPrompt = """ - あなたは、ユーザーがやりたい内容についてユーザーに最終確認を求める役割を持っています。 - - ユーザーがやりたい内容についてユーザーに最終確認を求める問いかけをしてください - - 問いかけはユーザーが「はい」か「いいえ」で答えられる形式にしてください - - 問いかけは簡潔にMarkdown形式で記述してください - - 問いかけにはユーザーがやりたい内容についての情報を含めてください - """; -} diff --git a/DurableMultiAgentTemplate/Agent/Workers/HumanInTheLoopAgent/HumanInTheLoopRequest.cs b/DurableMultiAgentTemplate/Agent/Workers/HumanInTheLoopAgent/HumanInTheLoopRequest.cs deleted file mode 100644 index 002ab90..0000000 --- a/DurableMultiAgentTemplate/Agent/Workers/HumanInTheLoopAgent/HumanInTheLoopRequest.cs +++ /dev/null @@ -1,7 +0,0 @@ -using System.ComponentModel; - -namespace DurableMultiAgentTemplate.Agent.Workers.HumanInTheLoopAgent; - -public record HumanInTheLoopRequest( - [property: Description("ユーザーが行いたい処理の詳細")] - string UserRequest); diff --git a/DurableMultiAgentTemplate/Agent/Workers/SubmitReservationAgent/SubmitReservationActivity.cs b/DurableMultiAgentTemplate/Agent/Workers/SubmitReservationAgent/SubmitReservationActivity.cs deleted file mode 100644 index 0b5346b..0000000 --- a/DurableMultiAgentTemplate/Agent/Workers/SubmitReservationAgent/SubmitReservationActivity.cs +++ /dev/null @@ -1,27 +0,0 @@ -using Microsoft.Azure.Functions.Worker; -using OpenAI.Chat; - -namespace DurableMultiAgentTemplate.Agent.Workers.SubmitReservationAgent; - -public class SubmitReservationActivity(ChatClient chatClient)//, CosmosClient cosmosClient) -{ - [Function(AgentActivityName.SubmitReservationAgent)] - public async Task RunAsync([ActivityTrigger] SubmitReservationRequest req) - { - // Simulate a delay - await Task.Delay(3000); - - // This is sample code. Replace this with your own logic. - var result = $""" - 予約番号は {Guid.NewGuid()} です。 - -------------------------------- - ホテル名:{req.Destination} - チェックイン日:{req.CheckIn} - チェックアウト日:{req.CheckOut} - 人数:{req.GuestsCount} 名 - -------------------------------- - """; - - return result; - } -} diff --git a/DurableMultiAgentTemplate/Agent/Workers/WorkerAgentResult.cs b/DurableMultiAgentTemplate/Agent/Workers/WorkerAgentResult.cs new file mode 100644 index 0000000..7997c97 --- /dev/null +++ b/DurableMultiAgentTemplate/Agent/Workers/WorkerAgentResult.cs @@ -0,0 +1,18 @@ +using DurableMultiAgentTemplate.Shared.Model; + +namespace DurableMultiAgentTemplate.Agent.Workers; + +/// +/// Represents the result of a worker agent's operation. +/// +/// The content produced by the worker agent. +/// The next agent call to be made, if any. +public record WorkerAgentResult(string Content, + AgentCall? NextAgentCall) +{ + /// + /// Initializes a new instance of the class. + /// + /// The content produced by the worker agent. + public static implicit operator WorkerAgentResult(string content) => new (content, null); +} diff --git a/DurableMultiAgentTemplate/Extension/AgentRequestMessageItemExtension.cs b/DurableMultiAgentTemplate/Extension/AgentRequestMessageItemExtension.cs index ac6d636..4e596a8 100644 --- a/DurableMultiAgentTemplate/Extension/AgentRequestMessageItemExtension.cs +++ b/DurableMultiAgentTemplate/Extension/AgentRequestMessageItemExtension.cs @@ -5,14 +5,14 @@ namespace DurableMultiAgentTemplate.Extension; public static class AgentRequestMessageItemExtension { - public static IEnumerable ConvertToChatMessageArray(this IEnumerable messages) + public static IEnumerable ConvertToChatMessageArray(this IEnumerable messages) { - return messages.Select(m => + return messages.Select(m => { return m.Role switch { - "user" => new UserChatMessage(m.Content), - "assistant" => new AssistantChatMessage(m.Content), + AgentRole.User => new UserChatMessage(m.Content), + AgentRole.Agent => new AssistantChatMessage(m.Content), _ => throw new InvalidOperationException($"You can not set role: {m.Role}") }; }); diff --git a/DurableMultiAgentTemplate/Model/SourceGenerationContext.cs b/DurableMultiAgentTemplate/Model/SourceGenerationContext.cs index bb61be1..0f0f3e8 100644 --- a/DurableMultiAgentTemplate/Model/SourceGenerationContext.cs +++ b/DurableMultiAgentTemplate/Model/SourceGenerationContext.cs @@ -4,8 +4,7 @@ using DurableMultiAgentTemplate.Agent.Workers.GetDestinationSuggestAgent; using DurableMultiAgentTemplate.Agent.Workers.GetHotelAgent; using DurableMultiAgentTemplate.Agent.Workers.GetSightseeingSpotAgent; -using DurableMultiAgentTemplate.Agent.Workers.HumanInTheLoopAgent; -using DurableMultiAgentTemplate.Agent.Workers.SubmitReservationAgent; +using DurableMultiAgentTemplate.Agent.Workers.HotelReservationAgent; using DurableMultiAgentTemplate.Shared.Model; namespace DurableMultiAgentTemplate.Model; @@ -15,10 +14,9 @@ namespace DurableMultiAgentTemplate.Model; [JsonSerializable(typeof(GetDestinationSuggestRequest))] [JsonSerializable(typeof(GetHotelRequest))] [JsonSerializable(typeof(GetSightseeingSpotRequest))] -[JsonSerializable(typeof(SubmitReservationRequest))] +[JsonSerializable(typeof(HotelReservationRequest))] [JsonSerializable(typeof(IAdditionalInfo))] [JsonSerializable(typeof(AdditionalMarkdownInfo))] [JsonSerializable(typeof(AdditionalLinkInfo))] [JsonSerializable(typeof(AgentResponseWithAdditionalInfoFormat))] -[JsonSerializable(typeof(HumanInTheLoopRequest))] internal partial class SourceGenerationContext : JsonSerializerContext; diff --git a/DurableMultiAgentTemplate/Program.cs b/DurableMultiAgentTemplate/Program.cs index ae78781..a09a852 100644 --- a/DurableMultiAgentTemplate/Program.cs +++ b/DurableMultiAgentTemplate/Program.cs @@ -4,6 +4,7 @@ using Azure; using Azure.AI.OpenAI; using Azure.Identity; +using DurableMultiAgentTemplate.Agent.Workers; using DurableMultiAgentTemplate.Model; using Microsoft.Azure.Cosmos; using Microsoft.Azure.Functions.Worker.Builder; @@ -19,6 +20,7 @@ var configuration = builder.Configuration; builder.Services.Configure(configuration.GetSection("AppConfig")); +builder.Services.AddSingleton(); builder.Services .AddAzureClients(clientBuilder => From 26b7227a31b25886b7b123ff112c08f5f2061710 Mon Sep 17 00:00:00 2001 From: Kazuki Ota Date: Tue, 18 Mar 2025 17:23:11 +0900 Subject: [PATCH 6/7] =?UTF-8?q?=E8=A6=81=E3=83=AA=E3=83=95=E3=82=A1?= =?UTF-8?q?=E3=82=AF=E3=82=BF=E3=83=AA=E3=83=B3=E3=82=B0=E3=80=82JsonSeria?= =?UTF-8?q?lizer=20=E3=81=AE=E3=82=AA=E3=83=97=E3=82=B7=E3=83=A7=E3=83=B3?= =?UTF-8?q?=E3=82=92=E5=85=A8=E4=BD=93=E3=81=A7=E4=B8=80=E5=85=83=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AgentDecider/AgentDeciderActivity.cs | 81 ++++++++++++++----- .../HotelReservationActivity.cs | 7 +- 2 files changed, 67 insertions(+), 21 deletions(-) diff --git a/DurableMultiAgentTemplate/Agent/AgentDecider/AgentDeciderActivity.cs b/DurableMultiAgentTemplate/Agent/AgentDecider/AgentDeciderActivity.cs index 68b5f5a..6940874 100644 --- a/DurableMultiAgentTemplate/Agent/AgentDecider/AgentDeciderActivity.cs +++ b/DurableMultiAgentTemplate/Agent/AgentDecider/AgentDeciderActivity.cs @@ -12,6 +12,11 @@ namespace DurableMultiAgentTemplate.Agent.AgentDecider; public class AgentDeciderActivity(ChatClient chatClient, AgentDefinitions agentDefinitions, ILogger logger) { + private static JsonSerializerOptions _jsonSerializerOptions = new() + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + }; + [Function(AgentActivityNames.AgentDeciderActivity)] public async Task Run([ActivityTrigger] AgentRequestDto reqData) { @@ -19,27 +24,10 @@ public async Task Run([ActivityTrigger] AgentRequestDto reqD var lastAgentMessage = (AgentMessageItem?)reqData.Messages.FindLast(x => x.Role == AgentRole.Agent); if (lastAgentMessage != null && lastAgentMessage.NextAgentCall != null) { - ChatMessage[] allMessagesForNextAgentCall = [ - new SystemChatMessage(AgentDeciderPrompt.SystemPromptForNextAgentCall), - .. messages, - ]; - var chatResultForNextAgentCall = await chatClient.CompleteChatAsync( - allMessagesForNextAgentCall, - CreateChatOptions(true) - ); - - if (chatResultForNextAgentCall.Value.FinishReason == ChatFinishReason.ToolCalls) + var result = await DecideNextAgentCallAsync(messages, lastAgentMessage); + if (result != null) { - return new AgentDeciderResult( - IsAgentCall: true, - Content: "", - AgentCalls: [.. chatResultForNextAgentCall.Value - .ToolCalls - .Select(toolCall => new AgentCall( - toolCall.FunctionName, - JsonSerializer.Deserialize(toolCall.FunctionArguments))) - ] - ); + return result; } } @@ -83,6 +71,59 @@ public async Task Run([ActivityTrigger] AgentRequestDto reqD throw new InvalidOperationException("Invalid OpenAI response"); } + private async Task DecideNextAgentCallAsync(IEnumerable messages, AgentMessageItem lastAgentMessageItem) + { + var nextAgentCall = lastAgentMessageItem.NextAgentCall; + if (nextAgentCall == null) + { + return null; + } + + ChatMessage[] allMessagesForNextAgentCall = [ + new SystemChatMessage(AgentDeciderPrompt.SystemPromptForNextAgentCall), + .. messages, + ]; + var chatResultForNextAgentCall = await chatClient.CompleteChatAsync( + allMessagesForNextAgentCall, + CreateChatOptions(true) + ); + + if (chatResultForNextAgentCall.Value.FinishReason != ChatFinishReason.ToolCalls) + { + return null; + } + + if (chatResultForNextAgentCall.Value.ToolCalls.Count != 1) + { + return null; + } + + var toolCall = chatResultForNextAgentCall.Value.ToolCalls[0]; + if (toolCall.FunctionName != nextAgentCall.AgentName) + { + return null; + } + + var argJsonElement = JsonSerializer.Deserialize(toolCall.FunctionArguments); + var argJson = JsonSerializer.Serialize(argJsonElement, _jsonSerializerOptions); + var prevJson = JsonSerializer.Serialize(nextAgentCall.Arguments, _jsonSerializerOptions); + if (argJson != prevJson) + { + return null; + } + + return new AgentDeciderResult( + IsAgentCall: true, + Content: "", + AgentCalls: [.. chatResultForNextAgentCall.Value + .ToolCalls + .Select(toolCall => new AgentCall( + toolCall.FunctionName, + argJsonElement)) + ] + ); + } + private ChatCompletionOptions CreateChatOptions(bool requiresUserConfirmation) { ChatCompletionOptions options = new(); diff --git a/DurableMultiAgentTemplate/Agent/Workers/HotelReservationAgent/HotelReservationActivity.cs b/DurableMultiAgentTemplate/Agent/Workers/HotelReservationAgent/HotelReservationActivity.cs index 2fecebb..d91e26f 100644 --- a/DurableMultiAgentTemplate/Agent/Workers/HotelReservationAgent/HotelReservationActivity.cs +++ b/DurableMultiAgentTemplate/Agent/Workers/HotelReservationAgent/HotelReservationActivity.cs @@ -6,6 +6,11 @@ namespace DurableMultiAgentTemplate.Agent.Workers.HotelReservationAgent; public class HotelReservationActivity(ChatClient chatClient)//, CosmosClient cosmosClient) { + private static JsonSerializerOptions _jsonSerializerOptions = new() + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + }; + [Function(AgentActivityNames.SubmitReservationAgent)] public async Task SubmitReservationRequestAsync([ActivityTrigger] HotelReservationRequest req) { @@ -22,7 +27,7 @@ public async Task SubmitReservationRequestAsync([ActivityTrig 人数:{req.GuestsCount} 名 -------------------------------- """, - new(AgentActivityNames.CommitReservationAgent, JsonSerializer.SerializeToElement(req))); + new(AgentActivityNames.CommitReservationAgent, JsonSerializer.SerializeToElement(req, _jsonSerializerOptions))); } [Function(AgentActivityNames.CommitReservationAgent)] From 7d7e6075c5efa565f6bb6f38470002be221d35e4 Mon Sep 17 00:00:00 2001 From: Kazuki Ota Date: Tue, 18 Mar 2025 20:03:27 +0900 Subject: [PATCH 7/7] =?UTF-8?q?json=20=E3=81=AE=E3=82=B7=E3=83=AA=E3=82=A2?= =?UTF-8?q?=E3=83=A9=E3=82=A4=E3=82=BA=E3=83=BB=E3=83=87=E3=82=B7=E3=83=AA?= =?UTF-8?q?=E3=82=A2=E3=83=A9=E3=82=A4=E3=82=BA=E3=81=AE=E6=9B=B8=E5=BC=8F?= =?UTF-8?q?=E3=82=92=E7=B5=B1=E4=B8=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Model/AgentMessageItem.cs | 30 ++------ .../Orchestrator/AgentOrchestratorTest.cs | 24 ++++-- .../AgentDecider/AgentDeciderActivity.cs | 20 +++-- .../Agent/Orchestrator/AgentOrchestrator.cs | 7 +- .../Agent/Synthesizer/SynthesizerActivity.cs | 3 +- .../SynthesizerWithAdditionalInfoActivity.cs | 8 +- .../Agent/Workers/AgentDefinition.cs | 46 ++++++------ .../HotelReservationActivity.cs | 10 +-- .../Json/JsonSchemaGenerator.cs | 51 ------------- .../Json/JsonUtilities.cs | 75 +++++++++++++++++++ DurableMultiAgentTemplate/Program.cs | 2 + 11 files changed, 145 insertions(+), 131 deletions(-) delete mode 100644 DurableMultiAgentTemplate/Json/JsonSchemaGenerator.cs create mode 100644 DurableMultiAgentTemplate/Json/JsonUtilities.cs diff --git a/DurableMultiAgentTemplate.Shared/Model/AgentMessageItem.cs b/DurableMultiAgentTemplate.Shared/Model/AgentMessageItem.cs index 1237ec6..a8d827b 100644 --- a/DurableMultiAgentTemplate.Shared/Model/AgentMessageItem.cs +++ b/DurableMultiAgentTemplate.Shared/Model/AgentMessageItem.cs @@ -15,16 +15,16 @@ public abstract record MessageItem( public record UserMessageItem(string Content) : MessageItem(AgentRole.User, Content); +/// +/// Represents a message item for an agent response. +/// +/// The content of the message. +/// Represents the next agent to be called. [method: JsonConstructor] -public record AgentMessageItem(AgentMessageType AgentMessageType, - string Content, +public record AgentMessageItem(string Content, AgentCall? NextAgentCall) : MessageItem(AgentRole.Agent, Content) { - public AgentMessageItem(string Content) : this(AgentMessageType.Info, Content, null) - { - } - - public AgentMessageItem(string Content, AgentCall NextAgentCall) : this(AgentMessageType.Ask, Content, NextAgentCall) + public AgentMessageItem(string content) : this(content, null) { } } @@ -44,19 +44,3 @@ public enum AgentRole /// Agent } - -/// -/// Represents a message type for an agent. -/// -[JsonConverter(typeof(JsonStringEnumConverter))] -public enum AgentMessageType -{ - /// - /// Represents a question to the user. - /// - Ask, - /// - /// Represents general information. - /// - Info -} diff --git a/DurableMultiAgentTemplate.Test/Agent/Orchestrator/AgentOrchestratorTest.cs b/DurableMultiAgentTemplate.Test/Agent/Orchestrator/AgentOrchestratorTest.cs index 27e3ae8..19d5c09 100644 --- a/DurableMultiAgentTemplate.Test/Agent/Orchestrator/AgentOrchestratorTest.cs +++ b/DurableMultiAgentTemplate.Test/Agent/Orchestrator/AgentOrchestratorTest.cs @@ -3,9 +3,11 @@ using DurableMultiAgentTemplate.Agent.AgentDecider; using DurableMultiAgentTemplate.Agent.Orchestrator; using DurableMultiAgentTemplate.Agent.Synthesizer; +using DurableMultiAgentTemplate.Json; using DurableMultiAgentTemplate.Shared.Model; using Microsoft.DurableTask; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Moq; namespace DurableMultiAgentTemplate.Test.Agent.Orchestrator; @@ -13,6 +15,12 @@ namespace DurableMultiAgentTemplate.Test.Agent.Orchestrator; [TestClass] public class AgentOrchestratorTest { + private readonly JsonUtilities _jsonUtilities = new(Options.Create(new JsonSerializerOptions() + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + WriteIndented = true + })); + [TestMethod] public async Task SetCustomStatus_WhenAgentDeciderActivityIsCalled() { @@ -36,7 +44,7 @@ public async Task SetCustomStatus_WhenAgentDeciderActivityIsCalled() .Returns(Task.FromCanceled(cancellationTokenSource.Token)); // Act: Run the orchestrator and expect a TaskCanceledException - var orchestrator = new AgentOrchestrator(); + var orchestrator = new AgentOrchestrator(_jsonUtilities); await Assert.ThrowsAsync(async () => await orchestrator.RunOrchestrator(contextMock.Object)); // Assert: Verify the custom status was set correctly @@ -66,7 +74,7 @@ public async Task RunOrchestrator_ShouldReturnAgentResponseDto_WhenNoAgentCallAn .ReturnsAsync(new AgentDeciderResult(IsAgentCall: false, Content: "No agent call", [])); // Act: Run the orchestrator - var orchestrator = new AgentOrchestrator(); + var orchestrator = new AgentOrchestrator(_jsonUtilities); var orchestratorResult = await orchestrator.RunOrchestrator(contextMock.Object); // Assert: Verify the result is of type AgentResponseDto and has the expected content @@ -90,7 +98,7 @@ public async Task RunOrchestrator_ShouldReturnAgentResponseWithAdditionalInfoDto .ReturnsAsync(new AgentDeciderResult(IsAgentCall: false, Content: "No agent call", [])); // Act: Run the orchestrator - var orchestrator = new AgentOrchestrator(); + var orchestrator = new AgentOrchestrator(_jsonUtilities); var orchestratorResult = await orchestrator.RunOrchestrator(contextMock.Object); // Assert: Verify the result is of type AgentResponseWithAdditionalInfoDto and has the expected content @@ -108,7 +116,7 @@ public async Task RunOrchestrator_ShouldThrowArgumentNullException_WhenRequestDa contextMock.Setup(x => x.GetInput()) .Returns((AgentRequestDto)null!); - var orchestrator = new AgentOrchestrator(); + var orchestrator = new AgentOrchestrator(_jsonUtilities); // Act & Assert: Run the orchestrator and expect an ArgumentNullException await Assert.ThrowsExactlyAsync(() => orchestrator.RunOrchestrator(contextMock.Object)); @@ -154,7 +162,7 @@ public async Task SetCustomStatus_WhenWorkerAgentActivityIsCalled() .Returns(Task.FromCanceled(cancellationTokenSource.Token)); // Act: Run the orchestrator and expect a TaskCanceledException - var orchestrator = new AgentOrchestrator(); + var orchestrator = new AgentOrchestrator(_jsonUtilities); await Assert.ThrowsAsync(async () => await orchestrator.RunOrchestrator(contextMock.Object)); // Assert: Verify the custom status for WorkerAgentActivity was set correctly @@ -200,7 +208,7 @@ public async Task RunOrchestrator_ShouldCallSynthesizerActivity_WhenAgentCallAnd .ReturnsAsync(new AgentResponseDto(new("Synthesized result"))); // Act: Run the orchestrator - var orchestrator = new AgentOrchestrator(); + var orchestrator = new AgentOrchestrator(_jsonUtilities); var result = await orchestrator.RunOrchestrator(contextMock.Object); // Assert: Verify the synthesizer request and result @@ -256,7 +264,7 @@ public async Task RunOrchestrator_ShouldCallSynthesizerWithAdditionalInfoActivit }); // Act: Run the orchestrator - var orchestrator = new AgentOrchestrator(); + var orchestrator = new AgentOrchestrator(_jsonUtilities); var result = await orchestrator.RunOrchestrator(contextMock.Object); // Assert: Verify the synthesizer request and result @@ -315,7 +323,7 @@ public async Task SetCustomStatus_WhenSynthesizerActivityIsCalled() .Returns(Task.FromCanceled(cancellationTokenSource.Token)); // Act: Run the orchestrator and expect a TaskCanceledException - var orchestrator = new AgentOrchestrator(); + var orchestrator = new AgentOrchestrator(_jsonUtilities); await Assert.ThrowsAsync(async () => await orchestrator.RunOrchestrator(contextMock.Object)); // Assert: Verify the custom status for SynthesizerActivity was set correctly diff --git a/DurableMultiAgentTemplate/Agent/AgentDecider/AgentDeciderActivity.cs b/DurableMultiAgentTemplate/Agent/AgentDecider/AgentDeciderActivity.cs index 6940874..94a8b6a 100644 --- a/DurableMultiAgentTemplate/Agent/AgentDecider/AgentDeciderActivity.cs +++ b/DurableMultiAgentTemplate/Agent/AgentDecider/AgentDeciderActivity.cs @@ -6,17 +6,16 @@ using DurableMultiAgentTemplate.Shared.Model; using DurableMultiAgentTemplate.Agent.Workers; using System.Text.Json.Nodes; +using DurableMultiAgentTemplate.Json; namespace DurableMultiAgentTemplate.Agent.AgentDecider; -public class AgentDeciderActivity(ChatClient chatClient, AgentDefinitions agentDefinitions, ILogger logger) +public class AgentDeciderActivity(ChatClient chatClient, + AgentDefinitions agentDefinitions, + JsonUtilities jsonUtilities, + ILogger logger) { - private static JsonSerializerOptions _jsonSerializerOptions = new() - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - }; - [Function(AgentActivityNames.AgentDeciderActivity)] public async Task Run([ActivityTrigger] AgentRequestDto reqData) { @@ -52,7 +51,7 @@ public async Task Run([ActivityTrigger] AgentRequestDto reqD .ToolCalls .Select(toolCall => new AgentCall( toolCall.FunctionName, - JsonSerializer.Deserialize(toolCall.FunctionArguments))) + jsonUtilities.Deserialize(toolCall.FunctionArguments))) ] ); } @@ -104,9 +103,8 @@ public async Task Run([ActivityTrigger] AgentRequestDto reqD return null; } - var argJsonElement = JsonSerializer.Deserialize(toolCall.FunctionArguments); - var argJson = JsonSerializer.Serialize(argJsonElement, _jsonSerializerOptions); - var prevJson = JsonSerializer.Serialize(nextAgentCall.Arguments, _jsonSerializerOptions); + var argJson = toolCall.FunctionArguments.ToString(); + var prevJson = jsonUtilities.Serialize(nextAgentCall.Arguments); if (argJson != prevJson) { return null; @@ -119,7 +117,7 @@ public async Task Run([ActivityTrigger] AgentRequestDto reqD .ToolCalls .Select(toolCall => new AgentCall( toolCall.FunctionName, - argJsonElement)) + nextAgentCall.Arguments)) ] ); } diff --git a/DurableMultiAgentTemplate/Agent/Orchestrator/AgentOrchestrator.cs b/DurableMultiAgentTemplate/Agent/Orchestrator/AgentOrchestrator.cs index 2f69c36..e32100a 100644 --- a/DurableMultiAgentTemplate/Agent/Orchestrator/AgentOrchestrator.cs +++ b/DurableMultiAgentTemplate/Agent/Orchestrator/AgentOrchestrator.cs @@ -8,10 +8,11 @@ using System.Text.Json.Nodes; using System.Text.Json; using DurableMultiAgentTemplate.Agent.Workers; +using DurableMultiAgentTemplate.Json; namespace DurableMultiAgentTemplate.Agent.Orchestrator; -public class AgentOrchestrator() +public class AgentOrchestrator(JsonUtilities jsonUtilities) { private static TaskOptions DefaultTaskOptions { get; } = new( new TaskRetryOptions(new RetryPolicy( @@ -30,7 +31,7 @@ public async Task RunOrchestrator( ArgumentNullException.ThrowIfNull(reqData); context.SetCustomStatus(new AgentOrchestratorStatus(AgentOrchestratorStep.AgentDeciderActivity, - [new AgentCall(AgentActivityNames.AgentDeciderActivity, JsonSerializer.SerializeToElement(reqData))])); + [new AgentCall(AgentActivityNames.AgentDeciderActivity, jsonUtilities.SerializeToElement(reqData))])); // AgentDecider呼び出し(呼び出すAgentの決定) var agentDeciderResult = await context.CallActivityAsync(AgentActivityNames.AgentDeciderActivity, reqData, DefaultTaskOptions); @@ -68,7 +69,7 @@ public async Task RunOrchestrator( ); context.SetCustomStatus(new AgentOrchestratorStatus(AgentOrchestratorStep.SynthesizerActivity, - [new AgentCall(AgentActivityNames.SynthesizerActivity, JsonSerializer.SerializeToElement(synthesizerRequest))])); + [new AgentCall(AgentActivityNames.SynthesizerActivity, jsonUtilities.SerializeToElement(synthesizerRequest))])); if (reqData.RequireAdditionalInfo) { return await context.CallActivityAsync(AgentActivityNames.SynthesizerWithAdditionalInfoActivity, synthesizerRequest, DefaultTaskOptions); diff --git a/DurableMultiAgentTemplate/Agent/Synthesizer/SynthesizerActivity.cs b/DurableMultiAgentTemplate/Agent/Synthesizer/SynthesizerActivity.cs index 514a40e..89e4de9 100644 --- a/DurableMultiAgentTemplate/Agent/Synthesizer/SynthesizerActivity.cs +++ b/DurableMultiAgentTemplate/Agent/Synthesizer/SynthesizerActivity.cs @@ -31,8 +31,7 @@ .. req.AgentRequest.Messages.ConvertToChatMessageArray(), .Select(x => x.NextAgentCall) .SingleOrDefault(x => x != null); return new( - new(nextAgentCall == null ? AgentMessageType.Info : AgentMessageType.Ask, - chatResult.Value.Content.First().Text, + new(chatResult.Value.Content.First().Text, nextAgentCall), req.CalledAgentNames); } diff --git a/DurableMultiAgentTemplate/Agent/Synthesizer/SynthesizerWithAdditionalInfoActivity.cs b/DurableMultiAgentTemplate/Agent/Synthesizer/SynthesizerWithAdditionalInfoActivity.cs index 0804fed..2a5759b 100644 --- a/DurableMultiAgentTemplate/Agent/Synthesizer/SynthesizerWithAdditionalInfoActivity.cs +++ b/DurableMultiAgentTemplate/Agent/Synthesizer/SynthesizerWithAdditionalInfoActivity.cs @@ -10,7 +10,9 @@ namespace DurableMultiAgentTemplate.Agent.Synthesizer; -public class SynthesizerWithAdditionalInfoActivity(ChatClient chatClient, ILogger logger) +public class SynthesizerWithAdditionalInfoActivity(ChatClient chatClient, + JsonUtilities jsonUtilities, + ILogger logger) { [Function(AgentActivityNames.SynthesizerWithAdditionalInfoActivity)] public async Task Run([ActivityTrigger] SynthesizerRequest req) @@ -28,7 +30,7 @@ .. req.AgentRequest.Messages.ConvertToChatMessageArray(), { ResponseFormat = ChatResponseFormat.CreateJsonSchemaFormat( "AgentResponseWithAdditionalInfo", - JsonSchemaGenerator.GenerateSchemaAsBinaryData(SourceGenerationContext.Default.AgentResponseWithAdditionalInfoFormat)) + jsonUtilities.GenerateSchemaAsBinaryData(SourceGenerationContext.Default.AgentResponseWithAdditionalInfoFormat)) }; var chatResult = await chatClient.CompleteChatAsync( @@ -38,7 +40,7 @@ .. req.AgentRequest.Messages.ConvertToChatMessageArray(), if (chatResult.Value.FinishReason == ChatFinishReason.Stop) { - var res = JsonSerializer.Deserialize( + var res = jsonUtilities.Deserialize( chatResult.Value.Content.First().Text, SourceGenerationContext.Default.AgentResponseWithAdditionalInfoFormat) ?? throw new InvalidOperationException("Failed to deserialize the result"); diff --git a/DurableMultiAgentTemplate/Agent/Workers/AgentDefinition.cs b/DurableMultiAgentTemplate/Agent/Workers/AgentDefinition.cs index 716deff..cb7d1b9 100644 --- a/DurableMultiAgentTemplate/Agent/Workers/AgentDefinition.cs +++ b/DurableMultiAgentTemplate/Agent/Workers/AgentDefinition.cs @@ -7,66 +7,66 @@ namespace DurableMultiAgentTemplate.Agent.Workers; //https://learn.microsoft.com/ja-jp/azure/ai-services/openai/how-to/dotnet-migration?tabs=stable public record AgentDefinition(string AgentActivityName, ChatTool ChatTool, bool RequiresUserConfirmation); -public class AgentDefinitions +public class AgentDefinitions(JsonUtilities jsonUtilities) { - public static readonly AgentDefinition GetDestinationSuggestAgent = new( + private readonly AgentDefinition _getDestinationSuggestAgent = new( AgentActivityNames.GetDestinationSuggestAgent, ChatTool.CreateFunctionTool( functionName: AgentActivityNames.GetDestinationSuggestAgent, functionDescription: "希望の行き先に求める条件を自然言語で与えると、おすすめの旅行先を提案します。", - functionParameters: JsonSchemaGenerator.GenerateSchemaAsBinaryData(SourceGenerationContext.Default.GetDestinationSuggestRequest)), + functionParameters: jsonUtilities.GenerateSchemaAsBinaryData(SourceGenerationContext.Default.GetDestinationSuggestRequest)), false); - public static readonly AgentDefinition GetClimateAgent = new( + private readonly AgentDefinition _getClimateAgent = new( AgentActivityNames.GetClimateAgent, ChatTool.CreateFunctionTool( functionName: AgentActivityNames.GetClimateAgent, functionDescription: "指定された場所の気候を取得します。", - functionParameters: JsonSchemaGenerator.GenerateSchemaAsBinaryData(SourceGenerationContext.Default.GetClimateRequest)), + functionParameters: jsonUtilities.GenerateSchemaAsBinaryData(SourceGenerationContext.Default.GetClimateRequest)), false); - public static readonly AgentDefinition GetSightseeingSpotAgent = new( + private readonly AgentDefinition _getSightseeingSpotAgent = new( AgentActivityNames.GetSightseeingSpotAgent, ChatTool.CreateFunctionTool( functionName: AgentActivityNames.GetSightseeingSpotAgent, functionDescription: "指定された場所の観光名所を取得します。", - functionParameters: JsonSchemaGenerator.GenerateSchemaAsBinaryData(SourceGenerationContext.Default.GetSightseeingSpotRequest)), + functionParameters: jsonUtilities.GenerateSchemaAsBinaryData(SourceGenerationContext.Default.GetSightseeingSpotRequest)), false); - public static readonly AgentDefinition GetHotelAgent = new( + private readonly AgentDefinition _getHotelAgent = new( AgentActivityNames.GetHotelAgent, ChatTool.CreateFunctionTool( functionName: AgentActivityNames.GetHotelAgent, functionDescription: "指定された場所のホテルを取得します。", - functionParameters: JsonSchemaGenerator.GenerateSchemaAsBinaryData(SourceGenerationContext.Default.GetHotelRequest)), + functionParameters: jsonUtilities.GenerateSchemaAsBinaryData(SourceGenerationContext.Default.GetHotelRequest)), false); - public static readonly AgentDefinition SubmitReservationAgent = new( + private readonly AgentDefinition _submitReservationAgent = new( AgentActivityNames.SubmitReservationAgent, ChatTool.CreateFunctionTool( functionName: AgentActivityNames.SubmitReservationAgent, functionDescription: "宿泊先の予約を行います。", - functionParameters: JsonSchemaGenerator.GenerateSchemaAsBinaryData(SourceGenerationContext.Default.HotelReservationRequest)), + functionParameters: jsonUtilities.GenerateSchemaAsBinaryData(SourceGenerationContext.Default.HotelReservationRequest)), false); - public static readonly AgentDefinition CommitReservationAgent = new( + private readonly AgentDefinition _commitReservationAgent = new( AgentActivityNames.CommitReservationAgent, ChatTool.CreateFunctionTool( functionName: AgentActivityNames.CommitReservationAgent, functionDescription: "宿泊先の予約の確定を行います。", - functionParameters: JsonSchemaGenerator.GenerateSchemaAsBinaryData(SourceGenerationContext.Default.HotelReservationRequest)), + functionParameters: jsonUtilities.GenerateSchemaAsBinaryData(SourceGenerationContext.Default.HotelReservationRequest)), true); - public static AgentDefinition[] AllAgents => new[] - { - GetDestinationSuggestAgent, - GetClimateAgent, - GetHotelAgent, - GetSightseeingSpotAgent, - SubmitReservationAgent, - CommitReservationAgent - }; + public AgentDefinition[] AllAgents => + [ + _getDestinationSuggestAgent, + _getClimateAgent, + _getHotelAgent, + _getSightseeingSpotAgent, + _submitReservationAgent, + _commitReservationAgent + ]; public AgentDefinition[] GetAgentDefinitions(bool requiresUserConfirmation) => - AllAgents.Where(agent => agent.RequiresUserConfirmation == requiresUserConfirmation).ToArray(); + [.. AllAgents.Where(agent => agent.RequiresUserConfirmation == requiresUserConfirmation)]; } diff --git a/DurableMultiAgentTemplate/Agent/Workers/HotelReservationAgent/HotelReservationActivity.cs b/DurableMultiAgentTemplate/Agent/Workers/HotelReservationAgent/HotelReservationActivity.cs index d91e26f..6a4d14f 100644 --- a/DurableMultiAgentTemplate/Agent/Workers/HotelReservationAgent/HotelReservationActivity.cs +++ b/DurableMultiAgentTemplate/Agent/Workers/HotelReservationAgent/HotelReservationActivity.cs @@ -1,16 +1,12 @@ using System.Text.Json; +using DurableMultiAgentTemplate.Json; using Microsoft.Azure.Functions.Worker; using OpenAI.Chat; namespace DurableMultiAgentTemplate.Agent.Workers.HotelReservationAgent; -public class HotelReservationActivity(ChatClient chatClient)//, CosmosClient cosmosClient) +public class HotelReservationActivity(ChatClient chatClient, JsonUtilities jsonUtilities)//, CosmosClient cosmosClient) { - private static JsonSerializerOptions _jsonSerializerOptions = new() - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - }; - [Function(AgentActivityNames.SubmitReservationAgent)] public async Task SubmitReservationRequestAsync([ActivityTrigger] HotelReservationRequest req) { @@ -27,7 +23,7 @@ public async Task SubmitReservationRequestAsync([ActivityTrig 人数:{req.GuestsCount} 名 -------------------------------- """, - new(AgentActivityNames.CommitReservationAgent, JsonSerializer.SerializeToElement(req, _jsonSerializerOptions))); + new(AgentActivityNames.CommitReservationAgent, jsonUtilities.SerializeToElement(req))); } [Function(AgentActivityNames.CommitReservationAgent)] diff --git a/DurableMultiAgentTemplate/Json/JsonSchemaGenerator.cs b/DurableMultiAgentTemplate/Json/JsonSchemaGenerator.cs deleted file mode 100644 index 3b9f8ec..0000000 --- a/DurableMultiAgentTemplate/Json/JsonSchemaGenerator.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System.ComponentModel; -using System.Text.Encodings.Web; -using System.Text.Json; -using System.Text.Json.Nodes; -using System.Text.Json.Schema; -using System.Text.Json.Serialization.Metadata; -using System.Text.Unicode; - -namespace DurableMultiAgentTemplate.Json; - - -/// -/// JsonSchema を生成する。 -/// クラス定義に Description 属性を指定することで JsonSchema にも description を追加する。 -/// -internal static class JsonSchemaGenerator -{ - private static readonly JsonSchemaExporterOptions _jsonSchemaExporterOptions = new() - { - TreatNullObliviousAsNonNullable = true, - // Description を追加する - TransformSchemaNode = (context, schema) => - { - var attributeProvider = context.PropertyInfo is not null ? - context.PropertyInfo.AttributeProvider : - context.TypeInfo.Type; - - var description = (DescriptionAttribute?)attributeProvider?.GetCustomAttributes(false) - .FirstOrDefault(x => x is DescriptionAttribute); - - if (description == null) return schema; - - if (schema is JsonObject jsonObject) - { - jsonObject.Insert(0, "description", description.Description); - } - - return schema; - }, - }; - - private static readonly JsonSerializerOptions _jsonSerializerOptions = new() - { - Encoder = JavaScriptEncoder.Create(UnicodeRanges.All), - }; - - public static string GenerateSchema(JsonTypeInfo type) => - JsonSchemaExporter.GetJsonSchemaAsNode(type, _jsonSchemaExporterOptions).ToJsonString(_jsonSerializerOptions); - public static BinaryData GenerateSchemaAsBinaryData(JsonTypeInfo type) => - BinaryData.FromString(GenerateSchema(type)); -} diff --git a/DurableMultiAgentTemplate/Json/JsonUtilities.cs b/DurableMultiAgentTemplate/Json/JsonUtilities.cs new file mode 100644 index 0000000..1f9b5a5 --- /dev/null +++ b/DurableMultiAgentTemplate/Json/JsonUtilities.cs @@ -0,0 +1,75 @@ +using System.ComponentModel; +using System.Text.Encodings.Web; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Schema; +using System.Text.Json.Serialization.Metadata; +using System.Text.Unicode; +using Microsoft.Extensions.Options; + +namespace DurableMultiAgentTemplate.Json; + + +/// +/// Utilities for JSON. +/// +public class JsonUtilities(IOptions options) +{ + private readonly JsonSerializerOptions _jsonSerializerOptions = options.Value; + private static readonly JsonSchemaExporterOptions _jsonSchemaExporterOptions = new() + { + TreatNullObliviousAsNonNullable = true, + // Description を追加する + TransformSchemaNode = (context, schema) => + { + var attributeProvider = context.PropertyInfo is not null ? + context.PropertyInfo.AttributeProvider : + context.TypeInfo.Type; + + var description = (DescriptionAttribute?)attributeProvider?.GetCustomAttributes(false) + .FirstOrDefault(x => x is DescriptionAttribute); + + if (description == null) return schema; + + if (schema is JsonObject jsonObject) + { + jsonObject.Insert(0, "description", description.Description); + } + + return schema; + }, + }; + + public string GenerateSchema(JsonTypeInfo type) => + JsonSchemaExporter.GetJsonSchemaAsNode(type, _jsonSchemaExporterOptions) + .ToJsonString(_jsonSerializerOptions); + + public BinaryData GenerateSchemaAsBinaryData(JsonTypeInfo type) => + BinaryData.FromString(GenerateSchema(type)); + + public JsonElement SerializeToElement(T value) => + JsonSerializer.SerializeToElement(value, _jsonSerializerOptions); + + public string Serialize(T value) => + JsonSerializer.Serialize(value, _jsonSerializerOptions); + + public T? Deserialize(JsonElement element) => + JsonSerializer.Deserialize(element, _jsonSerializerOptions); + + public T? Deserialize(JsonElement element, + JsonTypeInfo jsonTypeInfo) => + JsonSerializer.Deserialize(element, jsonTypeInfo); + + public T? Deserialize(string json, + JsonTypeInfo jsonTypeInfo) => + JsonSerializer.Deserialize(json, jsonTypeInfo); + + public T? Deserialize(BinaryData json, + JsonTypeInfo jsonTypeInfo) => + JsonSerializer.Deserialize(json, jsonTypeInfo); + + public T? Deserialize(string json) => + JsonSerializer.Deserialize(json, _jsonSerializerOptions); + public T? Deserialize(BinaryData json) => + JsonSerializer.Deserialize(json, _jsonSerializerOptions); +} diff --git a/DurableMultiAgentTemplate/Program.cs b/DurableMultiAgentTemplate/Program.cs index a09a852..4ca0f98 100644 --- a/DurableMultiAgentTemplate/Program.cs +++ b/DurableMultiAgentTemplate/Program.cs @@ -5,6 +5,7 @@ using Azure.AI.OpenAI; using Azure.Identity; using DurableMultiAgentTemplate.Agent.Workers; +using DurableMultiAgentTemplate.Json; using DurableMultiAgentTemplate.Model; using Microsoft.Azure.Cosmos; using Microsoft.Azure.Functions.Worker.Builder; @@ -21,6 +22,7 @@ builder.Services.Configure(configuration.GetSection("AppConfig")); builder.Services.AddSingleton(); +builder.Services.AddSingleton(); builder.Services .AddAzureClients(clientBuilder =>