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..a8d827b --- /dev/null +++ b/DurableMultiAgentTemplate.Shared/Model/AgentMessageItem.cs @@ -0,0 +1,46 @@ +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); + +/// +/// 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(string Content, + AgentCall? NextAgentCall) : MessageItem(AgentRole.Agent, Content) +{ + public AgentMessageItem(string content) : this(content, null) + { + } +} + +/// +/// Represents the role of an agent. +/// +[JsonConverter(typeof(JsonStringEnumConverter))] +public enum AgentRole +{ + /// + /// Represents a user role. + /// + User, + /// + /// Represents a agent role. + /// + Agent +} 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..19d5c09 100644 --- a/DurableMultiAgentTemplate.Test/Agent/Orchestrator/AgentOrchestratorTest.cs +++ b/DurableMultiAgentTemplate.Test/Agent/Orchestrator/AgentOrchestratorTest.cs @@ -1,10 +1,13 @@ -using DurableMultiAgentTemplate.Agent; +using System.Text.Json; +using DurableMultiAgentTemplate.Agent; 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; @@ -12,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() { @@ -31,11 +40,11 @@ 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 - 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 @@ -44,7 +53,7 @@ public async Task SetCustomStatus_WhenAgentDeciderActivityIsCalled() CollectionAssert.AreEqual( new List() { - new (AgentActivityName.AgentDeciderActivity, reqData) + new (AgentActivityNames.AgentDeciderActivity, JsonSerializer.SerializeToElement(reqData)) }, status.AgentCalls.ToList()); } @@ -60,17 +69,17 @@ 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", [])); // 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 Assert.IsInstanceOfType(orchestratorResult); - Assert.AreEqual("No agent call", orchestratorResult.Content); + Assert.AreEqual("No agent call", orchestratorResult.Item.Content); } [TestMethod] @@ -84,17 +93,17 @@ 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", [])); // 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 Assert.IsInstanceOfType(orchestratorResult); - Assert.AreEqual("No agent call", orchestratorResult.Content); + Assert.AreEqual("No agent call", orchestratorResult.Item.Content); } [TestMethod] @@ -107,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)); @@ -127,13 +136,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 @@ -153,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 @@ -176,13 +185,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,12 +203,12 @@ 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(); + var orchestrator = new AgentOrchestrator(_jsonUtilities); var result = await orchestrator.RunOrchestrator(contextMock.Object); // Assert: Verify the synthesizer request and result @@ -209,7 +218,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 +235,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 +250,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 = []; @@ -255,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 @@ -265,7 +274,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 +294,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,17 +319,17 @@ 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 - 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 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 80% rename from DurableMultiAgentTemplate/Agent/AgentActivityName.cs rename to DurableMultiAgentTemplate/Agent/AgentActivityNames.cs index 21d261b..41630a0 100644 --- a/DurableMultiAgentTemplate/Agent/AgentActivityName.cs +++ b/DurableMultiAgentTemplate/Agent/AgentActivityNames.cs @@ -1,6 +1,6 @@ -namespace DurableMultiAgentTemplate.Agent; +namespace DurableMultiAgentTemplate.Agent; -public static class AgentActivityName +public static class AgentActivityNames { // Orchestrator Agent functions public const string AgentDeciderActivity = nameof(AgentDeciderActivity); @@ -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 CommitReservationAgent = nameof(CommitReservationAgent); +} diff --git a/DurableMultiAgentTemplate/Agent/AgentDecider/AgentDeciderActivity.cs b/DurableMultiAgentTemplate/Agent/AgentDecider/AgentDeciderActivity.cs index 7409c2d..94a8b6a 100644 --- a/DurableMultiAgentTemplate/Agent/AgentDecider/AgentDeciderActivity.cs +++ b/DurableMultiAgentTemplate/Agent/AgentDecider/AgentDeciderActivity.cs @@ -4,36 +4,42 @@ using Microsoft.Extensions.Logging; using OpenAI.Chat; 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, ILogger logger) +public class AgentDeciderActivity(ChatClient chatClient, + AgentDefinitions agentDefinitions, + JsonUtilities jsonUtilities, + 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) + { + var result = await DecideNextAgentCallAsync(messages, lastAgentMessage); + if (result != null) + { + return result; + } + } + 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) @@ -43,7 +49,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, + jsonUtilities.Deserialize(toolCall.FunctionArguments))) ] ); } @@ -58,7 +66,71 @@ 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 argJson = toolCall.FunctionArguments.ToString(); + var prevJson = jsonUtilities.Serialize(nextAgentCall.Arguments); + if (argJson != prevJson) + { + return null; + } + + return new AgentDeciderResult( + IsAgentCall: true, + Content: "", + AgentCalls: [.. chatResultForNextAgentCall.Value + .ToolCalls + .Select(toolCall => new AgentCall( + toolCall.FunctionName, + nextAgentCall.Arguments)) + ] + ); + } + + 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 89d32d5..879ecac 100644 --- a/DurableMultiAgentTemplate/Agent/AgentDecider/AgentDeciderPrompt.cs +++ b/DurableMultiAgentTemplate/Agent/AgentDecider/AgentDeciderPrompt.cs @@ -1,13 +1,25 @@ -namespace DurableMultiAgentTemplate.Agent.AgentDecider; +namespace DurableMultiAgentTemplate.Agent.AgentDecider; internal static class AgentDeciderPrompt { + // Orchestrator Agent functions + public const string SystemPromptForNextAgentCall = """ + あなたは、人々が情報を見つけるのを助ける 旅行 AI アシスタントです。 + アシスタントとして、ユーザーからの問いについて必要なツールを選択してください。 + あなたの知識にないことや、使えるツールがない場合は「わかりません」と答えてください。 + ツールは以下の基準で選択してください。 + - ユーザーが明確にツールを呼び出そうとしている場合はツールを選択してください + - ユーザーがツールを呼び出すために渡す情報を変えようとしている場合は「わかりません」と答えてください。 + - ユーザーがツールの呼び出しを終了しようとしている場合は「わかりません」と答えてください。 + """; + + // Orchestrator Agent functions public const string SystemPrompt = """ - あなたは、人々が情報を見つけるのを助ける 旅行 AI アシスタントです。 - アシスタントとして、ユーザーからの問いについて必要なツールを選択してください。 - あなたの知識にないことや、使えるツールがない場合は「わかりません」と答えてください。 - 使えるツールがあるが、情報が足りない時はユーザーにその情報を質問してください。 - また、旅行以外の話題については答えないでください。 - """; -} \ No newline at end of file + あなたは、人々が情報を見つけるのを助ける 旅行 AI アシスタントです。 + アシスタントとして、ユーザーからの問いについて必要なツールを選択してください。 + あなたの知識にないことや、使えるツールがない場合は「わかりません」と答えてください。 + 使えるツールがあるが、情報が足りない時はユーザーにその情報を質問してください。 + また、旅行以外の話題については答えないでください。 + """; +} diff --git a/DurableMultiAgentTemplate/Agent/AgentDefinition.cs b/DurableMultiAgentTemplate/Agent/AgentDefinition.cs deleted file mode 100644 index e6c2767..0000000 --- a/DurableMultiAgentTemplate/Agent/AgentDefinition.cs +++ /dev/null @@ -1,34 +0,0 @@ -using DurableMultiAgentTemplate.Json; -using DurableMultiAgentTemplate.Model; -using OpenAI.Chat; - -namespace DurableMultiAgentTemplate.Agent; - -//https://learn.microsoft.com/ja-jp/azure/ai-services/openai/how-to/dotnet-migration?tabs=stable -internal class AgentDefinition -{ - 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)); -} diff --git a/DurableMultiAgentTemplate/Agent/GetClimateAgent/GetClimateActivity.cs b/DurableMultiAgentTemplate/Agent/GetClimateAgent/GetClimateActivity.cs deleted file mode 100644 index cb0f101..0000000 --- a/DurableMultiAgentTemplate/Agent/GetClimateAgent/GetClimateActivity.cs +++ /dev/null @@ -1,69 +0,0 @@ -using Microsoft.Azure.Functions.Worker; -using Microsoft.Extensions.Logging; -using OpenAI.Chat; - -namespace DurableMultiAgentTemplate.Agent.GetClimateAgent; - -public class GetClimateActivity(ChatClient chatClient, - ILogger logger) -{ - [Function(AgentActivityName.GetClimateAgent)] - public async Task RunAsync([ActivityTrigger] GetClimateRequest req) - { - if(Random.Shared.Next(0, 10) < 3) - { - logger.LogInformation("Failed to get climate information"); - throw new InvalidOperationException("Failed to get climate information"); - } - - // Simulate a delay - await Task.Delay(3000); - - // This is sample code. Replace this with your own logic. - var result = $""" - {req.Location}の気候は年間を通じて暖かく、**熱帯モンスーン気候**に分類されます。大きく分けて**乾季**と**雨季**があり、それぞれ異なる特徴があります。 - --- - - ### 平均気温 - - **年間を通じて:** 26~30℃程度 - - **日中:** 30℃前後まで上がることが多い。 - - **夜間:** 23~25℃程度で過ごしやすい。 - - --- - - ### 乾季(5月~10月) - - **特徴:** - - 晴れの日が多く、湿度が比較的低い。 - - 海や観光に最適なシーズン。 - - 朝晩は涼しい風が吹き、快適に過ごせる。 - - **おすすめのアクティビティ:** - - ビーチでのリラックス - - ダイビングやサーフィン - - ウブド周辺でのトレッキングや文化体験 - - --- - - ### 雨季(11月~4月) - - **特徴:** - - 短時間のスコールが頻繁に発生。 - - 湿度が高く蒸し暑い。 - - 雨が降ってもその後すぐに晴れることが多い。 - - **おすすめのアクティビティ:** - - 室内スパやリゾート内でのリラクゼーション - - ヒンズー寺院巡りや地元の文化体験 - - 雨季特有の緑が豊かな景色を楽しむ - - --- - - ### 服装のポイント - - **乾季:** 半袖や軽い素材の服装でOK。朝晩の冷えに備えて薄手の上着を用意。 - - **雨季:** 雨具(折りたたみ傘やレインコート)があると便利。速乾性の服装がおすすめ。 - - --- - - {req.Location}は雨季でも旅行を楽しめるよう工夫されているため、いつ訪れても魅力的です。乾季の5月~10月が観光のベストシーズンとされていますが、雨季なら緑豊かな景観と比較的空いている観光地を楽しむことができます。 - """; - - return result; - } -} diff --git a/DurableMultiAgentTemplate/Agent/GetDestinationSuggestAgent/GetDestinationSuggestActivity.cs b/DurableMultiAgentTemplate/Agent/GetDestinationSuggestAgent/GetDestinationSuggestActivity.cs deleted file mode 100644 index c8aa221..0000000 --- a/DurableMultiAgentTemplate/Agent/GetDestinationSuggestAgent/GetDestinationSuggestActivity.cs +++ /dev/null @@ -1,66 +0,0 @@ -using Microsoft.Azure.Functions.Worker; -using Microsoft.Extensions.Logging; -using OpenAI.Chat; - -namespace DurableMultiAgentTemplate.Agent.GetDestinationSuggestAgent; - -public class GetDestinationSuggestActivity(ChatClient chatClient, - ILogger logger) -{ - [Function(AgentActivityName.GetDestinationSuggestAgent)] - public async Task RunAsync([ActivityTrigger] GetDestinationSuggestRequest req) - { - if (Random.Shared.Next(0, 10) < 3) - { - logger.LogInformation("Failed to get destination suggestions"); - throw new InvalidOperationException("Failed to get destination suggestions"); - } - - // Simulate a delay - await Task.Delay(3000); - - // This is sample code. Replace this with your own logic. - var result = $""" - {req.SearchTerm}の条件でおすすめの旅行先を提案します。好みに応じて選んでください。 - ### 国内 - 1. **沖縄本島** - - 透明度の高いビーチ、首里城、美ら海水族館など観光名所が豊富。 - - 冬でも暖かく、リラックスした雰囲気を楽しめる。 - - 2. **石垣島・宮古島** - - 南国らしい美しい自然が広がり、ダイビングやシュノーケリングが人気。 - - 島ならではの郷土料理も楽しめる。 - - 3. **鹿児島・奄美大島** - - 奄美の黒糖焼酎や島唄、特有の自然環境を満喫。 - - 亜熱帯の雰囲気を楽しめる。 - - --- - - ### 海外 - 1. **ハワイ(オアフ島やマウイ島)** - - 年間を通して快適な気温。ビーチリゾートやトレッキングなど多様なアクティビティが可能。 - - 日本語対応も充実していて安心。 - - 2. **タイ(プーケットやクラビ)** - - 手頃な価格で楽しめるリゾート地。温かい気候とタイ料理も魅力。 - - 観光名所や島巡りもおすすめ。 - - 3. **バリ島(インドネシア)** - - 高級リゾートから手軽な宿泊施設まで選択肢が広い。 - - ヒンズー文化と自然が織りなすユニークな雰囲気を堪能。 - - 4. **オーストラリア(ケアンズやゴールドコースト)** - - グレートバリアリーフでの海洋アクティビティが人気。 - - 暖かい気候で自然と都市観光をバランス良く楽しめる。 - - 5. **グアムやサイパン** - - 日本から近く、短期間でも楽しめる南国リゾート。 - - のんびり過ごしたい方に最適。 - - どれも暖かい気候を楽しめる場所です。予算や旅行期間に合わせてお選びください! - """; - - return result; - } -} diff --git a/DurableMultiAgentTemplate/Agent/GetHotelAgent/GetHotelActivity.cs b/DurableMultiAgentTemplate/Agent/GetHotelAgent/GetHotelActivity.cs deleted file mode 100644 index dd04435..0000000 --- a/DurableMultiAgentTemplate/Agent/GetHotelAgent/GetHotelActivity.cs +++ /dev/null @@ -1,73 +0,0 @@ -using Microsoft.Azure.Functions.Worker; -using Microsoft.Extensions.Logging; -using OpenAI.Chat; - -namespace DurableMultiAgentTemplate.Agent.GetHotelAgent; - -public class GetHotelActivity(ChatClient chatClient, - ILogger logger) -{ - [Function(AgentActivityName.GetHotelAgent)] - public async Task Run([ActivityTrigger] GetHotelRequest req) - { - if (Random.Shared.Next(0, 10) < 3) - { - logger.LogInformation("Failed to get hotel information"); - throw new InvalidOperationException("Failed to get hotel information"); - } - - // Simulate a delay - await Task.Delay(3000); - // This is sample code. Replace this with your own logic. - var result = $""" - {req.Location}に以下の4件のホテルがあります。 - --- - - ### 1. **リラ・オアシスホテル(Rila Oasis Hotel)** - - **所在地**: 海沿いのリゾート地 - - **テーマ**: 癒しとウェルネス - - **特徴**: - - 海を望むインフィニティプール - - スパ・ヨガ・瞑想などのウェルネスプログラム - - 地元食材を使ったヘルシーレストラン - - 落ち着いた内装でリラクゼーションを重視 - - --- - - ### 2. **クラウン・スカイタワー(Crown Skytower)** - - **所在地**: 都市部の中心地 - - **テーマ**: モダンでラグジュアリーな都市体験 - - **特徴**: - - 高層階からの夜景が楽しめるラグジュアリールーム - - 最先端の設備を備えたビジネスセンター - - ミシュラン星付きレストラン併設 - - スタイリッシュなデザインとスマートホテル機能 - - --- - - ### 3. **フォレスト・ヒドゥンロッジ(Forest Hidden Lodge)** - - **所在地**: 森に囲まれた山間部 - - **テーマ**: 自然と共に過ごす冒険と安らぎ - - **特徴**: - - 木造のコテージ風宿泊施設 - - トレッキングや星空観察のアクティビティ - - 暖炉付きラウンジと温泉 - - 地元の伝統料理を楽しめるダイニング - - --- - - ### 4. **アルテ・シンフォニア(Arte Sinfonia)** - - **所在地**: 歴史的な街並みの一角 - - **テーマ**: アートと文化の融合 - - **特徴**: - - 地元アーティストの作品を展示したギャラリー併設 - - クラシック音楽のライブ演奏が行われるラウンジ - - アンティーク家具を取り入れたクラシカルな内装 - - 歴史的建築物をリノベーションした趣のある空間 - - --- - """; - - return result; - } -} diff --git a/DurableMultiAgentTemplate/Agent/GetSightseeingSpotAgent/GetSightseeingSpotActivity.cs b/DurableMultiAgentTemplate/Agent/GetSightseeingSpotAgent/GetSightseeingSpotActivity.cs deleted file mode 100644 index 377ab37..0000000 --- a/DurableMultiAgentTemplate/Agent/GetSightseeingSpotAgent/GetSightseeingSpotActivity.cs +++ /dev/null @@ -1,111 +0,0 @@ -using Microsoft.Azure.Functions.Worker; -using Microsoft.Extensions.Logging; -using OpenAI.Chat; - -namespace DurableMultiAgentTemplate.Agent.GetSightseeingSpotAgent; - -public class GetSightseeingSpotActivity(ChatClient chatClient, - ILogger logger) -{ - [Function(AgentActivityName.GetSightseeingSpotAgent)] - public async Task RunAsync([ActivityTrigger] GetSightseeingSpotRequest req) - { - if (Random.Shared.Next(0, 10) < 3) - { - logger.LogInformation("Failed to get sightseeing spot information"); - throw new InvalidOperationException("Failed to get sightseeing spot information"); - } - - // Simulate a delay - await Task.Delay(3000); - - // This is sample code. Replace this with your own logic. - var result = $""" - {req.Location}には美しい自然、歴史的な寺院、ユニークな文化体験が楽しめる観光名所がたくさんあります!以下におすすめの観光スポットをまとめました。 - - --- - - ### 1. **寺院** - #### **タナロット寺院(Pura Tanah Lot)** - - 海の上に建つ寺院で、夕日とともに見える景色が絶景。 - - {req.Location}を代表する観光名所の一つ。 - - #### **ウルワツ寺院(Pura Luhur Uluwatu)** - - 崖の上に位置する寺院。インド洋を見渡す絶景スポット。 - - 夕方には伝統舞踊「ケチャダンス」の公演が行われる。 - - #### **ティルタ・エンプル寺院(Pura Tirta Empul)** - - 聖水が湧き出る寺院で、地元の人々や観光客が浄化の儀式を体験。 - - 神聖な雰囲気を感じることができる。 - - --- - - ### 2. **自然** - #### **ウブドのライステラス(Tegalalang Rice Terrace)** - - 階段状に広がる田んぼの風景が特徴的。 - - 写真撮影やトレッキングに最適。 - - #### **モンキーフォレスト(Sacred Monkey Forest Sanctuary)** - - ウブドにある野生の猿が暮らす自然保護区。 - - 緑豊かな森と寺院が融合した神秘的な場所。 - - #### **キンタマーニ高原とバトゥール山** - - バトゥール火山とカルデラ湖の壮大な景色を楽しめる。 - - トレッキングや朝日鑑賞がおすすめ。 - - --- - - ### 3. **ビーチ** - #### **クタビーチ** - - サーフィン初心者に人気のスポット。 - - 多くのレストランやショップが近くにあり、賑やかな雰囲気。 - - #### **ヌサ・ドゥア** - - 高級リゾートエリアで、静かなビーチと透明度の高い海が魅力。 - - シュノーケリングやジェットスキーなどのマリンスポーツも楽しめる。 - - #### **ジンバランビーチ** - - サンセットディナーが有名で、新鮮なシーフード料理が楽しめる。 - - 夕日を眺めながらの食事は特別な体験。 - - --- - - ### 4. **文化体験** - #### **ウブド王宮(Ubud Palace)** - - 伝統的な建築が美しい王宮で、舞踊のパフォーマンスが行われる。 - - ウブド市場も近くにあり、ショッピングにも最適。 - - #### **伝統舞踊** - - ケチャダンスやレゴンダンスなど、寺院や専用会場で観賞できる。 - - 神話や伝説が題材となった迫力あるパフォーマンス。 - - --- - - ### 5. **アクティビティ** - #### **サファリ&マリンパーク** - - 動物園や水族館が融合したエンターテイメント施設。 - - サファリツアーやアニマルショーが楽しめる。 - - #### **ラフティング(アユン川)** - - 緑に囲まれた川を下る冒険感あふれる体験。 - - 初心者でも楽しめるコースが多い。 - - --- - - ### 6. **近隣の島** - #### **ヌサペニダ島** - - {req.Location}から船でアクセス可能な離島。 - - クリスタルベイやケリンキンビーチの絶景が有名。 - - #### **レンボンガン島** - - マングローブツアーやサンゴ礁シュノーケリングが楽しめる。 - - のんびりとした雰囲気の島。 - - --- - - {req.Location}は多様な楽しみ方ができるため、目的に応じて行き先を選んでみてください! - """; - - return result; - } -} diff --git a/DurableMultiAgentTemplate/Agent/Orchestrator/AgentOrchestrator.cs b/DurableMultiAgentTemplate/Agent/Orchestrator/AgentOrchestrator.cs index 1c0ea9f..e32100a 100644 --- a/DurableMultiAgentTemplate/Agent/Orchestrator/AgentOrchestrator.cs +++ b/DurableMultiAgentTemplate/Agent/Orchestrator/AgentOrchestrator.cs @@ -5,10 +5,14 @@ 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; +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( @@ -27,9 +31,9 @@ public async Task RunOrchestrator( ArgumentNullException.ThrowIfNull(reqData); context.SetCustomStatus(new AgentOrchestratorStatus(AgentOrchestratorStep.AgentDeciderActivity, - [new AgentCall(AgentActivityName.AgentDeciderActivity, reqData)])); + [new AgentCall(AgentActivityNames.AgentDeciderActivity, jsonUtilities.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 +41,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 +53,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 +69,14 @@ public async Task RunOrchestrator( ); context.SetCustomStatus(new AgentOrchestratorStatus(AgentOrchestratorStep.SynthesizerActivity, - [new AgentCall(AgentActivityName.SynthesizerActivity, synthesizerRequest)])); + [new AgentCall(AgentActivityNames.SynthesizerActivity, jsonUtilities.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/SubmitReservationAgent/SubmitReservationActivity.cs b/DurableMultiAgentTemplate/Agent/SubmitReservationAgent/SubmitReservationActivity.cs deleted file mode 100644 index d456769..0000000 --- a/DurableMultiAgentTemplate/Agent/SubmitReservationAgent/SubmitReservationActivity.cs +++ /dev/null @@ -1,27 +0,0 @@ -using Microsoft.Azure.Functions.Worker; -using OpenAI.Chat; - -namespace DurableMultiAgentTemplate.Agent.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/Synthesizer/SynthesizerActivity.cs b/DurableMultiAgentTemplate/Agent/Synthesizer/SynthesizerActivity.cs index e975e7d..89e4de9 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,12 @@ .. 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(chatResult.Value.Content.First().Text, + nextAgentCall), req.CalledAgentNames); } 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/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..2a5759b 100644 --- a/DurableMultiAgentTemplate/Agent/Synthesizer/SynthesizerWithAdditionalInfoActivity.cs +++ b/DurableMultiAgentTemplate/Agent/Synthesizer/SynthesizerWithAdditionalInfoActivity.cs @@ -10,9 +10,11 @@ namespace DurableMultiAgentTemplate.Agent.Synthesizer; -public class SynthesizerWithAdditionalInfoActivity(ChatClient chatClient, ILogger logger) +public class SynthesizerWithAdditionalInfoActivity(ChatClient chatClient, + JsonUtilities jsonUtilities, + ILogger logger) { - [Function(AgentActivityName.SynthesizerWithAdditionalInfoActivity)] + [Function(AgentActivityNames.SynthesizerWithAdditionalInfoActivity)] public async Task Run([ActivityTrigger] SynthesizerRequest req) { logger.LogInformation("Run SynthesizerActivity"); @@ -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,13 +40,13 @@ .. 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"); 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 new file mode 100644 index 0000000..cb7d1b9 --- /dev/null +++ b/DurableMultiAgentTemplate/Agent/Workers/AgentDefinition.cs @@ -0,0 +1,72 @@ +using DurableMultiAgentTemplate.Json; +using DurableMultiAgentTemplate.Model; +using OpenAI.Chat; + +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(JsonUtilities jsonUtilities) +{ + private readonly AgentDefinition _getDestinationSuggestAgent = new( + AgentActivityNames.GetDestinationSuggestAgent, + ChatTool.CreateFunctionTool( + functionName: AgentActivityNames.GetDestinationSuggestAgent, + functionDescription: "希望の行き先に求める条件を自然言語で与えると、おすすめの旅行先を提案します。", + functionParameters: jsonUtilities.GenerateSchemaAsBinaryData(SourceGenerationContext.Default.GetDestinationSuggestRequest)), + false); + + private readonly AgentDefinition _getClimateAgent = new( + AgentActivityNames.GetClimateAgent, + ChatTool.CreateFunctionTool( + functionName: AgentActivityNames.GetClimateAgent, + functionDescription: "指定された場所の気候を取得します。", + functionParameters: jsonUtilities.GenerateSchemaAsBinaryData(SourceGenerationContext.Default.GetClimateRequest)), + false); + + private readonly AgentDefinition _getSightseeingSpotAgent = new( + AgentActivityNames.GetSightseeingSpotAgent, + ChatTool.CreateFunctionTool( + functionName: AgentActivityNames.GetSightseeingSpotAgent, + functionDescription: "指定された場所の観光名所を取得します。", + functionParameters: jsonUtilities.GenerateSchemaAsBinaryData(SourceGenerationContext.Default.GetSightseeingSpotRequest)), + false); + + private readonly AgentDefinition _getHotelAgent = new( + AgentActivityNames.GetHotelAgent, + ChatTool.CreateFunctionTool( + functionName: AgentActivityNames.GetHotelAgent, + functionDescription: "指定された場所のホテルを取得します。", + functionParameters: jsonUtilities.GenerateSchemaAsBinaryData(SourceGenerationContext.Default.GetHotelRequest)), + false); + + private readonly AgentDefinition _submitReservationAgent = new( + AgentActivityNames.SubmitReservationAgent, + ChatTool.CreateFunctionTool( + functionName: AgentActivityNames.SubmitReservationAgent, + functionDescription: "宿泊先の予約を行います。", + functionParameters: jsonUtilities.GenerateSchemaAsBinaryData(SourceGenerationContext.Default.HotelReservationRequest)), + false); + + private readonly AgentDefinition _commitReservationAgent = new( + AgentActivityNames.CommitReservationAgent, + ChatTool.CreateFunctionTool( + functionName: AgentActivityNames.CommitReservationAgent, + functionDescription: "宿泊先の予約の確定を行います。", + functionParameters: jsonUtilities.GenerateSchemaAsBinaryData(SourceGenerationContext.Default.HotelReservationRequest)), + true); + + public AgentDefinition[] AllAgents => + [ + _getDestinationSuggestAgent, + _getClimateAgent, + _getHotelAgent, + _getSightseeingSpotAgent, + _submitReservationAgent, + _commitReservationAgent + ]; + + public AgentDefinition[] GetAgentDefinitions(bool requiresUserConfirmation) => + [.. AllAgents.Where(agent => agent.RequiresUserConfirmation == requiresUserConfirmation)]; +} diff --git a/DurableMultiAgentTemplate/Agent/Workers/GetClimateAgent/GetClimateActivity.cs b/DurableMultiAgentTemplate/Agent/Workers/GetClimateAgent/GetClimateActivity.cs new file mode 100644 index 0000000..525f7ad --- /dev/null +++ b/DurableMultiAgentTemplate/Agent/Workers/GetClimateAgent/GetClimateActivity.cs @@ -0,0 +1,69 @@ +using Microsoft.Azure.Functions.Worker; +using Microsoft.Extensions.Logging; +using OpenAI.Chat; + +namespace DurableMultiAgentTemplate.Agent.Workers.GetClimateAgent; + +public class GetClimateActivity(ChatClient chatClient, + ILogger logger) +{ + [Function(AgentActivityNames.GetClimateAgent)] + public async Task RunAsync([ActivityTrigger] GetClimateRequest req) + { + if(Random.Shared.Next(0, 10) < 3) + { + logger.LogInformation("Failed to get climate information"); + throw new InvalidOperationException("Failed to get climate information"); + } + + // Simulate a delay + await Task.Delay(3000); + + // This is sample code. Replace this with your own logic. + var result = $""" + {req.Location}の気候は年間を通じて暖かく、**熱帯モンスーン気候**に分類されます。大きく分けて**乾季**と**雨季**があり、それぞれ異なる特徴があります。 + --- + + ### 平均気温 + - **年間を通じて:** 26~30℃程度 + - **日中:** 30℃前後まで上がることが多い。 + - **夜間:** 23~25℃程度で過ごしやすい。 + + --- + + ### 乾季(5月~10月) + - **特徴:** + - 晴れの日が多く、湿度が比較的低い。 + - 海や観光に最適なシーズン。 + - 朝晩は涼しい風が吹き、快適に過ごせる。 + - **おすすめのアクティビティ:** + - ビーチでのリラックス + - ダイビングやサーフィン + - ウブド周辺でのトレッキングや文化体験 + + --- + + ### 雨季(11月~4月) + - **特徴:** + - 短時間のスコールが頻繁に発生。 + - 湿度が高く蒸し暑い。 + - 雨が降ってもその後すぐに晴れることが多い。 + - **おすすめのアクティビティ:** + - 室内スパやリゾート内でのリラクゼーション + - ヒンズー寺院巡りや地元の文化体験 + - 雨季特有の緑が豊かな景色を楽しむ + + --- + + ### 服装のポイント + - **乾季:** 半袖や軽い素材の服装でOK。朝晩の冷えに備えて薄手の上着を用意。 + - **雨季:** 雨具(折りたたみ傘やレインコート)があると便利。速乾性の服装がおすすめ。 + + --- + + {req.Location}は雨季でも旅行を楽しめるよう工夫されているため、いつ訪れても魅力的です。乾季の5月~10月が観光のベストシーズンとされていますが、雨季なら緑豊かな景観と比較的空いている観光地を楽しむことができます。 + """; + + return result; + } +} 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/Workers/GetDestinationSuggestAgent/GetDestinationSuggestActivity.cs b/DurableMultiAgentTemplate/Agent/Workers/GetDestinationSuggestAgent/GetDestinationSuggestActivity.cs new file mode 100644 index 0000000..759fea4 --- /dev/null +++ b/DurableMultiAgentTemplate/Agent/Workers/GetDestinationSuggestAgent/GetDestinationSuggestActivity.cs @@ -0,0 +1,66 @@ +using Microsoft.Azure.Functions.Worker; +using Microsoft.Extensions.Logging; +using OpenAI.Chat; + +namespace DurableMultiAgentTemplate.Agent.Workers.GetDestinationSuggestAgent; + +public class GetDestinationSuggestActivity(ChatClient chatClient, + ILogger logger) +{ + [Function(AgentActivityNames.GetDestinationSuggestAgent)] + public async Task RunAsync([ActivityTrigger] GetDestinationSuggestRequest req) + { + if (Random.Shared.Next(0, 10) < 3) + { + logger.LogInformation("Failed to get destination suggestions"); + throw new InvalidOperationException("Failed to get destination suggestions"); + } + + // Simulate a delay + await Task.Delay(3000); + + // This is sample code. Replace this with your own logic. + var result = $""" + {req.SearchTerm}の条件でおすすめの旅行先を提案します。好みに応じて選んでください。 + ### 国内 + 1. **沖縄本島** + - 透明度の高いビーチ、首里城、美ら海水族館など観光名所が豊富。 + - 冬でも暖かく、リラックスした雰囲気を楽しめる。 + + 2. **石垣島・宮古島** + - 南国らしい美しい自然が広がり、ダイビングやシュノーケリングが人気。 + - 島ならではの郷土料理も楽しめる。 + + 3. **鹿児島・奄美大島** + - 奄美の黒糖焼酎や島唄、特有の自然環境を満喫。 + - 亜熱帯の雰囲気を楽しめる。 + + --- + + ### 海外 + 1. **ハワイ(オアフ島やマウイ島)** + - 年間を通して快適な気温。ビーチリゾートやトレッキングなど多様なアクティビティが可能。 + - 日本語対応も充実していて安心。 + + 2. **タイ(プーケットやクラビ)** + - 手頃な価格で楽しめるリゾート地。温かい気候とタイ料理も魅力。 + - 観光名所や島巡りもおすすめ。 + + 3. **バリ島(インドネシア)** + - 高級リゾートから手軽な宿泊施設まで選択肢が広い。 + - ヒンズー文化と自然が織りなすユニークな雰囲気を堪能。 + + 4. **オーストラリア(ケアンズやゴールドコースト)** + - グレートバリアリーフでの海洋アクティビティが人気。 + - 暖かい気候で自然と都市観光をバランス良く楽しめる。 + + 5. **グアムやサイパン** + - 日本から近く、短期間でも楽しめる南国リゾート。 + - のんびり過ごしたい方に最適。 + + どれも暖かい気候を楽しめる場所です。予算や旅行期間に合わせてお選びください! + """; + + return result; + } +} 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/Workers/GetHotelAgent/GetHotelActivity.cs b/DurableMultiAgentTemplate/Agent/Workers/GetHotelAgent/GetHotelActivity.cs new file mode 100644 index 0000000..8a564a5 --- /dev/null +++ b/DurableMultiAgentTemplate/Agent/Workers/GetHotelAgent/GetHotelActivity.cs @@ -0,0 +1,73 @@ +using Microsoft.Azure.Functions.Worker; +using Microsoft.Extensions.Logging; +using OpenAI.Chat; + +namespace DurableMultiAgentTemplate.Agent.Workers.GetHotelAgent; + +public class GetHotelActivity(ChatClient chatClient, + ILogger logger) +{ + [Function(AgentActivityNames.GetHotelAgent)] + public async Task Run([ActivityTrigger] GetHotelRequest req) + { + if (Random.Shared.Next(0, 10) < 3) + { + logger.LogInformation("Failed to get hotel information"); + throw new InvalidOperationException("Failed to get hotel information"); + } + + // Simulate a delay + await Task.Delay(3000); + // This is sample code. Replace this with your own logic. + var result = $""" + {req.Location}に以下の4件のホテルがあります。 + --- + + ### 1. **リラ・オアシスホテル(Rila Oasis Hotel)** + - **所在地**: 海沿いのリゾート地 + - **テーマ**: 癒しとウェルネス + - **特徴**: + - 海を望むインフィニティプール + - スパ・ヨガ・瞑想などのウェルネスプログラム + - 地元食材を使ったヘルシーレストラン + - 落ち着いた内装でリラクゼーションを重視 + + --- + + ### 2. **クラウン・スカイタワー(Crown Skytower)** + - **所在地**: 都市部の中心地 + - **テーマ**: モダンでラグジュアリーな都市体験 + - **特徴**: + - 高層階からの夜景が楽しめるラグジュアリールーム + - 最先端の設備を備えたビジネスセンター + - ミシュラン星付きレストラン併設 + - スタイリッシュなデザインとスマートホテル機能 + + --- + + ### 3. **フォレスト・ヒドゥンロッジ(Forest Hidden Lodge)** + - **所在地**: 森に囲まれた山間部 + - **テーマ**: 自然と共に過ごす冒険と安らぎ + - **特徴**: + - 木造のコテージ風宿泊施設 + - トレッキングや星空観察のアクティビティ + - 暖炉付きラウンジと温泉 + - 地元の伝統料理を楽しめるダイニング + + --- + + ### 4. **アルテ・シンフォニア(Arte Sinfonia)** + - **所在地**: 歴史的な街並みの一角 + - **テーマ**: アートと文化の融合 + - **特徴**: + - 地元アーティストの作品を展示したギャラリー併設 + - クラシック音楽のライブ演奏が行われるラウンジ + - アンティーク家具を取り入れたクラシカルな内装 + - 歴史的建築物をリノベーションした趣のある空間 + + --- + """; + + return result; + } +} 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/Workers/GetSightseeingSpotAgent/GetSightseeingSpotActivity.cs b/DurableMultiAgentTemplate/Agent/Workers/GetSightseeingSpotAgent/GetSightseeingSpotActivity.cs new file mode 100644 index 0000000..3692539 --- /dev/null +++ b/DurableMultiAgentTemplate/Agent/Workers/GetSightseeingSpotAgent/GetSightseeingSpotActivity.cs @@ -0,0 +1,111 @@ +using Microsoft.Azure.Functions.Worker; +using Microsoft.Extensions.Logging; +using OpenAI.Chat; + +namespace DurableMultiAgentTemplate.Agent.Workers.GetSightseeingSpotAgent; + +public class GetSightseeingSpotActivity(ChatClient chatClient, + ILogger logger) +{ + [Function(AgentActivityNames.GetSightseeingSpotAgent)] + public async Task RunAsync([ActivityTrigger] GetSightseeingSpotRequest req) + { + if (Random.Shared.Next(0, 10) < 3) + { + logger.LogInformation("Failed to get sightseeing spot information"); + throw new InvalidOperationException("Failed to get sightseeing spot information"); + } + + // Simulate a delay + await Task.Delay(3000); + + // This is sample code. Replace this with your own logic. + var result = $""" + {req.Location}には美しい自然、歴史的な寺院、ユニークな文化体験が楽しめる観光名所がたくさんあります!以下におすすめの観光スポットをまとめました。 + + --- + + ### 1. **寺院** + #### **タナロット寺院(Pura Tanah Lot)** + - 海の上に建つ寺院で、夕日とともに見える景色が絶景。 + - {req.Location}を代表する観光名所の一つ。 + + #### **ウルワツ寺院(Pura Luhur Uluwatu)** + - 崖の上に位置する寺院。インド洋を見渡す絶景スポット。 + - 夕方には伝統舞踊「ケチャダンス」の公演が行われる。 + + #### **ティルタ・エンプル寺院(Pura Tirta Empul)** + - 聖水が湧き出る寺院で、地元の人々や観光客が浄化の儀式を体験。 + - 神聖な雰囲気を感じることができる。 + + --- + + ### 2. **自然** + #### **ウブドのライステラス(Tegalalang Rice Terrace)** + - 階段状に広がる田んぼの風景が特徴的。 + - 写真撮影やトレッキングに最適。 + + #### **モンキーフォレスト(Sacred Monkey Forest Sanctuary)** + - ウブドにある野生の猿が暮らす自然保護区。 + - 緑豊かな森と寺院が融合した神秘的な場所。 + + #### **キンタマーニ高原とバトゥール山** + - バトゥール火山とカルデラ湖の壮大な景色を楽しめる。 + - トレッキングや朝日鑑賞がおすすめ。 + + --- + + ### 3. **ビーチ** + #### **クタビーチ** + - サーフィン初心者に人気のスポット。 + - 多くのレストランやショップが近くにあり、賑やかな雰囲気。 + + #### **ヌサ・ドゥア** + - 高級リゾートエリアで、静かなビーチと透明度の高い海が魅力。 + - シュノーケリングやジェットスキーなどのマリンスポーツも楽しめる。 + + #### **ジンバランビーチ** + - サンセットディナーが有名で、新鮮なシーフード料理が楽しめる。 + - 夕日を眺めながらの食事は特別な体験。 + + --- + + ### 4. **文化体験** + #### **ウブド王宮(Ubud Palace)** + - 伝統的な建築が美しい王宮で、舞踊のパフォーマンスが行われる。 + - ウブド市場も近くにあり、ショッピングにも最適。 + + #### **伝統舞踊** + - ケチャダンスやレゴンダンスなど、寺院や専用会場で観賞できる。 + - 神話や伝説が題材となった迫力あるパフォーマンス。 + + --- + + ### 5. **アクティビティ** + #### **サファリ&マリンパーク** + - 動物園や水族館が融合したエンターテイメント施設。 + - サファリツアーやアニマルショーが楽しめる。 + + #### **ラフティング(アユン川)** + - 緑に囲まれた川を下る冒険感あふれる体験。 + - 初心者でも楽しめるコースが多い。 + + --- + + ### 6. **近隣の島** + #### **ヌサペニダ島** + - {req.Location}から船でアクセス可能な離島。 + - クリスタルベイやケリンキンビーチの絶景が有名。 + + #### **レンボンガン島** + - マングローブツアーやサンゴ礁シュノーケリングが楽しめる。 + - のんびりとした雰囲気の島。 + + --- + + {req.Location}は多様な楽しみ方ができるため、目的に応じて行き先を選んでみてください! + """; + + return result; + } +} 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/Workers/HotelReservationAgent/HotelReservationActivity.cs b/DurableMultiAgentTemplate/Agent/Workers/HotelReservationAgent/HotelReservationActivity.cs new file mode 100644 index 0000000..6a4d14f --- /dev/null +++ b/DurableMultiAgentTemplate/Agent/Workers/HotelReservationAgent/HotelReservationActivity.cs @@ -0,0 +1,48 @@ +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, JsonUtilities jsonUtilities)//, 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, jsonUtilities.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/SubmitReservationAgent/SubmitReservationRequest.cs b/DurableMultiAgentTemplate/Agent/Workers/HotelReservationAgent/HotelReservationRequest.cs similarity index 77% rename from DurableMultiAgentTemplate/Agent/SubmitReservationAgent/SubmitReservationRequest.cs rename to DurableMultiAgentTemplate/Agent/Workers/HotelReservationAgent/HotelReservationRequest.cs index fdca8be..95657f0 100644 --- a/DurableMultiAgentTemplate/Agent/SubmitReservationAgent/SubmitReservationRequest.cs +++ b/DurableMultiAgentTemplate/Agent/Workers/HotelReservationAgent/HotelReservationRequest.cs @@ -1,8 +1,8 @@ using System.ComponentModel; -namespace DurableMultiAgentTemplate.Agent.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/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/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/Model/SourceGenerationContext.cs b/DurableMultiAgentTemplate/Model/SourceGenerationContext.cs index 7c5a2d0..0f0f3e8 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.HotelReservationAgent; using DurableMultiAgentTemplate.Shared.Model; namespace DurableMultiAgentTemplate.Model; @@ -14,7 +14,7 @@ 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))] diff --git a/DurableMultiAgentTemplate/Program.cs b/DurableMultiAgentTemplate/Program.cs index ae78781..4ca0f98 100644 --- a/DurableMultiAgentTemplate/Program.cs +++ b/DurableMultiAgentTemplate/Program.cs @@ -4,6 +4,8 @@ using Azure; 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; @@ -19,6 +21,8 @@ var configuration = builder.Configuration; builder.Services.Configure(configuration.GetSection("AppConfig")); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); builder.Services .AddAzureClients(clientBuilder =>