diff --git a/dotnet/Directory.Build.props b/dotnet/Directory.Build.props index 7b4dc5155b..a1bb7cf599 100644 --- a/dotnet/Directory.Build.props +++ b/dotnet/Directory.Build.props @@ -6,7 +6,7 @@ AllEnabledByDefault latest true - 12 + 13 enable disable $(NoWarn);IDE0290;IDE0079;NU5128;CS8002 diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props index 907a58ece8..cc9ddcbdf4 100644 --- a/dotnet/Directory.Packages.props +++ b/dotnet/Directory.Packages.props @@ -37,6 +37,7 @@ + @@ -86,14 +87,16 @@ - - - - + + + + + + diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx index 324d63cb19..2b07e3346d 100644 --- a/dotnet/agent-framework-dotnet.slnx +++ b/dotnet/agent-framework-dotnet.slnx @@ -78,9 +78,9 @@ - + + - @@ -96,8 +96,8 @@ - + @@ -265,6 +265,7 @@ + diff --git a/dotnet/samples/GettingStarted/AgentOpenTelemetry/AgentOpenTelemetry.csproj b/dotnet/samples/GettingStarted/AgentOpenTelemetry/AgentOpenTelemetry.csproj index d552be2c50..a58fa132d2 100644 --- a/dotnet/samples/GettingStarted/AgentOpenTelemetry/AgentOpenTelemetry.csproj +++ b/dotnet/samples/GettingStarted/AgentOpenTelemetry/AgentOpenTelemetry.csproj @@ -3,7 +3,6 @@ Exe net9.0 - 12 enable enable diff --git a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_A2A/Agent_With_A2A.csproj b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_A2A/Agent_With_A2A.csproj index fd2e0b7b80..1604ac6c73 100644 --- a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_A2A/Agent_With_A2A.csproj +++ b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_A2A/Agent_With_A2A.csproj @@ -3,7 +3,6 @@ Exe net9.0 - 12 enable disable diff --git a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_AzureFoundry/Agent_With_AzureFoundry.csproj b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_AzureFoundry/Agent_With_AzureFoundry.csproj index 9da22b3573..b93b40b7dc 100644 --- a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_AzureFoundry/Agent_With_AzureFoundry.csproj +++ b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_AzureFoundry/Agent_With_AzureFoundry.csproj @@ -3,7 +3,6 @@ Exe net9.0 - 12 enable disable diff --git a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_AzureOpenAIChatCompletion/Agent_With_AzureOpenAIChatCompletion.csproj b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_AzureOpenAIChatCompletion/Agent_With_AzureOpenAIChatCompletion.csproj index 3c80c1527e..68b0f8d36b 100644 --- a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_AzureOpenAIChatCompletion/Agent_With_AzureOpenAIChatCompletion.csproj +++ b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_AzureOpenAIChatCompletion/Agent_With_AzureOpenAIChatCompletion.csproj @@ -3,7 +3,6 @@ Exe net9.0 - 12 enable disable diff --git a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_AzureOpenAIResponses/Agent_With_AzureOpenAIResponses.csproj b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_AzureOpenAIResponses/Agent_With_AzureOpenAIResponses.csproj index 3c80c1527e..68b0f8d36b 100644 --- a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_AzureOpenAIResponses/Agent_With_AzureOpenAIResponses.csproj +++ b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_AzureOpenAIResponses/Agent_With_AzureOpenAIResponses.csproj @@ -3,7 +3,6 @@ Exe net9.0 - 12 enable disable diff --git a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_CustomImplementation/Agent_With_CustomImplementation.csproj b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_CustomImplementation/Agent_With_CustomImplementation.csproj index 4fa7edf3fc..f46e9bd63c 100644 --- a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_CustomImplementation/Agent_With_CustomImplementation.csproj +++ b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_CustomImplementation/Agent_With_CustomImplementation.csproj @@ -3,7 +3,6 @@ Exe net9.0 - 12 enable disable diff --git a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_ONNX/Agent_With_ONNX.csproj b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_ONNX/Agent_With_ONNX.csproj index 69320f79c0..6c4004765f 100644 --- a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_ONNX/Agent_With_ONNX.csproj +++ b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_ONNX/Agent_With_ONNX.csproj @@ -3,7 +3,6 @@ Exe net9.0 - 12 enable disable diff --git a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_Ollama/Agent_With_Ollama.csproj b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_Ollama/Agent_With_Ollama.csproj index 44603cc24c..5eee1d6e72 100644 --- a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_Ollama/Agent_With_Ollama.csproj +++ b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_Ollama/Agent_With_Ollama.csproj @@ -3,7 +3,6 @@ Exe net9.0 - 12 enable disable diff --git a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_OpenAIChatCompletion/Agent_With_OpenAIChatCompletion.csproj b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_OpenAIChatCompletion/Agent_With_OpenAIChatCompletion.csproj index bbce14eb4b..27d3e4077f 100644 --- a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_OpenAIChatCompletion/Agent_With_OpenAIChatCompletion.csproj +++ b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_OpenAIChatCompletion/Agent_With_OpenAIChatCompletion.csproj @@ -3,7 +3,6 @@ Exe net9.0 - 12 enable disable diff --git a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_OpenAIResponses/Agent_With_OpenAIResponses.csproj b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_OpenAIResponses/Agent_With_OpenAIResponses.csproj index bbce14eb4b..27d3e4077f 100644 --- a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_OpenAIResponses/Agent_With_OpenAIResponses.csproj +++ b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_OpenAIResponses/Agent_With_OpenAIResponses.csproj @@ -3,7 +3,6 @@ Exe net9.0 - 12 enable disable diff --git a/dotnet/samples/GettingStarted/AgentWithOpenAI/Agent_OpenAI_Step01_Running/Agent_OpenAI_Step01_Running.csproj b/dotnet/samples/GettingStarted/AgentWithOpenAI/Agent_OpenAI_Step01_Running/Agent_OpenAI_Step01_Running.csproj index 718aa6650b..9d84e4babb 100644 --- a/dotnet/samples/GettingStarted/AgentWithOpenAI/Agent_OpenAI_Step01_Running/Agent_OpenAI_Step01_Running.csproj +++ b/dotnet/samples/GettingStarted/AgentWithOpenAI/Agent_OpenAI_Step01_Running/Agent_OpenAI_Step01_Running.csproj @@ -3,7 +3,6 @@ Exe net9.0 - 12 enable disable diff --git a/dotnet/samples/GettingStarted/Agents/Agent_Step01_Running/Agent_Step01_Running.csproj b/dotnet/samples/GettingStarted/Agents/Agent_Step01_Running/Agent_Step01_Running.csproj index 3c80c1527e..68b0f8d36b 100644 --- a/dotnet/samples/GettingStarted/Agents/Agent_Step01_Running/Agent_Step01_Running.csproj +++ b/dotnet/samples/GettingStarted/Agents/Agent_Step01_Running/Agent_Step01_Running.csproj @@ -3,7 +3,6 @@ Exe net9.0 - 12 enable disable diff --git a/dotnet/samples/GettingStarted/Agents/Agent_Step02_MultiturnConversation/Agent_Step02_MultiturnConversation.csproj b/dotnet/samples/GettingStarted/Agents/Agent_Step02_MultiturnConversation/Agent_Step02_MultiturnConversation.csproj index 3c80c1527e..68b0f8d36b 100644 --- a/dotnet/samples/GettingStarted/Agents/Agent_Step02_MultiturnConversation/Agent_Step02_MultiturnConversation.csproj +++ b/dotnet/samples/GettingStarted/Agents/Agent_Step02_MultiturnConversation/Agent_Step02_MultiturnConversation.csproj @@ -3,7 +3,6 @@ Exe net9.0 - 12 enable disable diff --git a/dotnet/samples/GettingStarted/Agents/Agent_Step03_UsingFunctionTools/Agent_Step03_UsingFunctionTools.csproj b/dotnet/samples/GettingStarted/Agents/Agent_Step03_UsingFunctionTools/Agent_Step03_UsingFunctionTools.csproj index 3c80c1527e..68b0f8d36b 100644 --- a/dotnet/samples/GettingStarted/Agents/Agent_Step03_UsingFunctionTools/Agent_Step03_UsingFunctionTools.csproj +++ b/dotnet/samples/GettingStarted/Agents/Agent_Step03_UsingFunctionTools/Agent_Step03_UsingFunctionTools.csproj @@ -3,7 +3,6 @@ Exe net9.0 - 12 enable disable diff --git a/dotnet/samples/GettingStarted/Agents/Agent_Step04_UsingFunctionToolsWithApprovals/Agent_Step04_UsingFunctionToolsWithApprovals.csproj b/dotnet/samples/GettingStarted/Agents/Agent_Step04_UsingFunctionToolsWithApprovals/Agent_Step04_UsingFunctionToolsWithApprovals.csproj index d83018dd29..e6ffdc2794 100644 --- a/dotnet/samples/GettingStarted/Agents/Agent_Step04_UsingFunctionToolsWithApprovals/Agent_Step04_UsingFunctionToolsWithApprovals.csproj +++ b/dotnet/samples/GettingStarted/Agents/Agent_Step04_UsingFunctionToolsWithApprovals/Agent_Step04_UsingFunctionToolsWithApprovals.csproj @@ -3,7 +3,6 @@ Exe net9.0 - 12 enable disable diff --git a/dotnet/samples/GettingStarted/Agents/Agent_Step05_StructuredOutput/Agent_Step05_StructuredOutput.csproj b/dotnet/samples/GettingStarted/Agents/Agent_Step05_StructuredOutput/Agent_Step05_StructuredOutput.csproj index 3c80c1527e..68b0f8d36b 100644 --- a/dotnet/samples/GettingStarted/Agents/Agent_Step05_StructuredOutput/Agent_Step05_StructuredOutput.csproj +++ b/dotnet/samples/GettingStarted/Agents/Agent_Step05_StructuredOutput/Agent_Step05_StructuredOutput.csproj @@ -3,7 +3,6 @@ Exe net9.0 - 12 enable disable diff --git a/dotnet/samples/GettingStarted/Agents/Agent_Step06_PersistedConversations/Agent_Step06_PersistedConversations.csproj b/dotnet/samples/GettingStarted/Agents/Agent_Step06_PersistedConversations/Agent_Step06_PersistedConversations.csproj index 3c80c1527e..68b0f8d36b 100644 --- a/dotnet/samples/GettingStarted/Agents/Agent_Step06_PersistedConversations/Agent_Step06_PersistedConversations.csproj +++ b/dotnet/samples/GettingStarted/Agents/Agent_Step06_PersistedConversations/Agent_Step06_PersistedConversations.csproj @@ -3,7 +3,6 @@ Exe net9.0 - 12 enable disable diff --git a/dotnet/samples/GettingStarted/Agents/Agent_Step07_3rdPartyThreadStorage/Agent_Step07_3rdPartyThreadStorage.csproj b/dotnet/samples/GettingStarted/Agents/Agent_Step07_3rdPartyThreadStorage/Agent_Step07_3rdPartyThreadStorage.csproj index 68c0002c72..b027b80775 100644 --- a/dotnet/samples/GettingStarted/Agents/Agent_Step07_3rdPartyThreadStorage/Agent_Step07_3rdPartyThreadStorage.csproj +++ b/dotnet/samples/GettingStarted/Agents/Agent_Step07_3rdPartyThreadStorage/Agent_Step07_3rdPartyThreadStorage.csproj @@ -3,7 +3,6 @@ Exe net9.0 - 12 enable disable diff --git a/dotnet/samples/GettingStarted/Agents/Agent_Step08_Telemetry/Agent_Step08_Telemetry.csproj b/dotnet/samples/GettingStarted/Agents/Agent_Step08_Telemetry/Agent_Step08_Telemetry.csproj index 96e2dfb566..fe7185c7ee 100644 --- a/dotnet/samples/GettingStarted/Agents/Agent_Step08_Telemetry/Agent_Step08_Telemetry.csproj +++ b/dotnet/samples/GettingStarted/Agents/Agent_Step08_Telemetry/Agent_Step08_Telemetry.csproj @@ -3,7 +3,6 @@ Exe net9.0 - 12 enable disable diff --git a/dotnet/samples/GettingStarted/Agents/Agent_Step09_DependencyInjection/Agent_Step09_DependencyInjection.csproj b/dotnet/samples/GettingStarted/Agents/Agent_Step09_DependencyInjection/Agent_Step09_DependencyInjection.csproj index eaf68b3e73..c9ff416496 100644 --- a/dotnet/samples/GettingStarted/Agents/Agent_Step09_DependencyInjection/Agent_Step09_DependencyInjection.csproj +++ b/dotnet/samples/GettingStarted/Agents/Agent_Step09_DependencyInjection/Agent_Step09_DependencyInjection.csproj @@ -3,7 +3,6 @@ Exe net9.0 - 12 enable disable diff --git a/dotnet/samples/GettingStarted/Agents/Agent_Step10_AsMcpTool/Agent_Step10_AsMcpTool.csproj b/dotnet/samples/GettingStarted/Agents/Agent_Step10_AsMcpTool/Agent_Step10_AsMcpTool.csproj index cdb063750c..009d39f4cf 100644 --- a/dotnet/samples/GettingStarted/Agents/Agent_Step10_AsMcpTool/Agent_Step10_AsMcpTool.csproj +++ b/dotnet/samples/GettingStarted/Agents/Agent_Step10_AsMcpTool/Agent_Step10_AsMcpTool.csproj @@ -3,7 +3,6 @@ Exe net9.0 - 12 enable disable diff --git a/dotnet/samples/GettingStarted/Agents/Agent_Step11_UsingImages/Agent_Step11_UsingImages.csproj b/dotnet/samples/GettingStarted/Agents/Agent_Step11_UsingImages/Agent_Step11_UsingImages.csproj index a438862616..017711102f 100644 --- a/dotnet/samples/GettingStarted/Agents/Agent_Step11_UsingImages/Agent_Step11_UsingImages.csproj +++ b/dotnet/samples/GettingStarted/Agents/Agent_Step11_UsingImages/Agent_Step11_UsingImages.csproj @@ -3,7 +3,6 @@ Exe net9.0 - 12 enable disable diff --git a/dotnet/samples/GettingStarted/Agents/Agent_Step12_AsFunctionTool/Agent_Step12_AsFunctionTool.csproj b/dotnet/samples/GettingStarted/Agents/Agent_Step12_AsFunctionTool/Agent_Step12_AsFunctionTool.csproj index 84ef628db4..3261ff1ebf 100644 --- a/dotnet/samples/GettingStarted/Agents/Agent_Step12_AsFunctionTool/Agent_Step12_AsFunctionTool.csproj +++ b/dotnet/samples/GettingStarted/Agents/Agent_Step12_AsFunctionTool/Agent_Step12_AsFunctionTool.csproj @@ -3,7 +3,6 @@ Exe net9.0 - 12 enable disable diff --git a/dotnet/samples/GettingStarted/ModelContextProtocol/Agent_MCP_Server/Agent_MCP_Server.csproj b/dotnet/samples/GettingStarted/ModelContextProtocol/Agent_MCP_Server/Agent_MCP_Server.csproj index 759eaae572..dce21d16a9 100644 --- a/dotnet/samples/GettingStarted/ModelContextProtocol/Agent_MCP_Server/Agent_MCP_Server.csproj +++ b/dotnet/samples/GettingStarted/ModelContextProtocol/Agent_MCP_Server/Agent_MCP_Server.csproj @@ -3,7 +3,6 @@ Exe net9.0 - 12 enable disable diff --git a/dotnet/samples/GettingStarted/ModelContextProtocol/Agent_MCP_Server_Auth/Agent_MCP_Server_Auth.csproj b/dotnet/samples/GettingStarted/ModelContextProtocol/Agent_MCP_Server_Auth/Agent_MCP_Server_Auth.csproj index d26676a576..e410b2e29a 100644 --- a/dotnet/samples/GettingStarted/ModelContextProtocol/Agent_MCP_Server_Auth/Agent_MCP_Server_Auth.csproj +++ b/dotnet/samples/GettingStarted/ModelContextProtocol/Agent_MCP_Server_Auth/Agent_MCP_Server_Auth.csproj @@ -3,7 +3,6 @@ Exe net9.0 - 12 enable disable diff --git a/dotnet/samples/GettingStarted/Workflows/Agents/CustomAgentExecutors/CustomAgentExecutors.csproj b/dotnet/samples/GettingStarted/Workflows/Agents/CustomAgentExecutors/CustomAgentExecutors.csproj index b1f90ee600..267e25180e 100644 --- a/dotnet/samples/GettingStarted/Workflows/Agents/CustomAgentExecutors/CustomAgentExecutors.csproj +++ b/dotnet/samples/GettingStarted/Workflows/Agents/CustomAgentExecutors/CustomAgentExecutors.csproj @@ -3,7 +3,6 @@ Exe net9.0 - 12 enable disable diff --git a/dotnet/samples/GettingStarted/Workflows/Agents/FoundryAgent/FoundryAgent.csproj b/dotnet/samples/GettingStarted/Workflows/Agents/FoundryAgent/FoundryAgent.csproj index 564e214dac..eac3c76d93 100644 --- a/dotnet/samples/GettingStarted/Workflows/Agents/FoundryAgent/FoundryAgent.csproj +++ b/dotnet/samples/GettingStarted/Workflows/Agents/FoundryAgent/FoundryAgent.csproj @@ -3,7 +3,6 @@ Exe net9.0 - 12 enable disable diff --git a/dotnet/samples/GettingStarted/Workflows/Agents/WorkflowAsAnAgent/WorkflowAsAnAgent.csproj b/dotnet/samples/GettingStarted/Workflows/Agents/WorkflowAsAnAgent/WorkflowAsAnAgent.csproj index b1f90ee600..267e25180e 100644 --- a/dotnet/samples/GettingStarted/Workflows/Agents/WorkflowAsAnAgent/WorkflowAsAnAgent.csproj +++ b/dotnet/samples/GettingStarted/Workflows/Agents/WorkflowAsAnAgent/WorkflowAsAnAgent.csproj @@ -3,7 +3,6 @@ Exe net9.0 - 12 enable disable diff --git a/dotnet/samples/GettingStarted/Workflows/Checkpoint/CheckpointAndRehydrate/CheckpointAndRehydrate.csproj b/dotnet/samples/GettingStarted/Workflows/Checkpoint/CheckpointAndRehydrate/CheckpointAndRehydrate.csproj index bbee070910..897d1d4fd4 100644 --- a/dotnet/samples/GettingStarted/Workflows/Checkpoint/CheckpointAndRehydrate/CheckpointAndRehydrate.csproj +++ b/dotnet/samples/GettingStarted/Workflows/Checkpoint/CheckpointAndRehydrate/CheckpointAndRehydrate.csproj @@ -3,7 +3,6 @@ Exe net9.0 - 12 enable disable diff --git a/dotnet/samples/GettingStarted/Workflows/Checkpoint/CheckpointAndResume/CheckpointAndResume.csproj b/dotnet/samples/GettingStarted/Workflows/Checkpoint/CheckpointAndResume/CheckpointAndResume.csproj index 1025cb45c9..bcca8af014 100644 --- a/dotnet/samples/GettingStarted/Workflows/Checkpoint/CheckpointAndResume/CheckpointAndResume.csproj +++ b/dotnet/samples/GettingStarted/Workflows/Checkpoint/CheckpointAndResume/CheckpointAndResume.csproj @@ -3,7 +3,6 @@ Exe net9.0 - 12 enable disable diff --git a/dotnet/samples/GettingStarted/Workflows/Checkpoint/CheckpointWithHumanInTheLoop/CheckpointWithHumanInTheLoop.csproj b/dotnet/samples/GettingStarted/Workflows/Checkpoint/CheckpointWithHumanInTheLoop/CheckpointWithHumanInTheLoop.csproj index bbee070910..897d1d4fd4 100644 --- a/dotnet/samples/GettingStarted/Workflows/Checkpoint/CheckpointWithHumanInTheLoop/CheckpointWithHumanInTheLoop.csproj +++ b/dotnet/samples/GettingStarted/Workflows/Checkpoint/CheckpointWithHumanInTheLoop/CheckpointWithHumanInTheLoop.csproj @@ -3,7 +3,6 @@ Exe net9.0 - 12 enable disable diff --git a/dotnet/samples/GettingStarted/Workflows/Concurrent/Concurrent.csproj b/dotnet/samples/GettingStarted/Workflows/Concurrent/Concurrent.csproj index a9b76d86ce..694e8a1ea8 100644 --- a/dotnet/samples/GettingStarted/Workflows/Concurrent/Concurrent.csproj +++ b/dotnet/samples/GettingStarted/Workflows/Concurrent/Concurrent.csproj @@ -3,7 +3,6 @@ Exe net9.0 - 12 enable disable diff --git a/dotnet/samples/GettingStarted/Workflows/ConditionalEdges/01_EdgeCondition/01_EdgeCondition.csproj b/dotnet/samples/GettingStarted/Workflows/ConditionalEdges/01_EdgeCondition/01_EdgeCondition.csproj index dcee9dcd7f..492fe95fa6 100644 --- a/dotnet/samples/GettingStarted/Workflows/ConditionalEdges/01_EdgeCondition/01_EdgeCondition.csproj +++ b/dotnet/samples/GettingStarted/Workflows/ConditionalEdges/01_EdgeCondition/01_EdgeCondition.csproj @@ -3,7 +3,6 @@ Exe net9.0 - 12 enable disable diff --git a/dotnet/samples/GettingStarted/Workflows/ConditionalEdges/02_SwitchCase/02_SwitchCase.csproj b/dotnet/samples/GettingStarted/Workflows/ConditionalEdges/02_SwitchCase/02_SwitchCase.csproj index dcee9dcd7f..492fe95fa6 100644 --- a/dotnet/samples/GettingStarted/Workflows/ConditionalEdges/02_SwitchCase/02_SwitchCase.csproj +++ b/dotnet/samples/GettingStarted/Workflows/ConditionalEdges/02_SwitchCase/02_SwitchCase.csproj @@ -3,7 +3,6 @@ Exe net9.0 - 12 enable disable diff --git a/dotnet/samples/GettingStarted/Workflows/ConditionalEdges/03_MultiSelection/03_MultiSelection.csproj b/dotnet/samples/GettingStarted/Workflows/ConditionalEdges/03_MultiSelection/03_MultiSelection.csproj index dcee9dcd7f..492fe95fa6 100644 --- a/dotnet/samples/GettingStarted/Workflows/ConditionalEdges/03_MultiSelection/03_MultiSelection.csproj +++ b/dotnet/samples/GettingStarted/Workflows/ConditionalEdges/03_MultiSelection/03_MultiSelection.csproj @@ -3,7 +3,6 @@ Exe net9.0 - 12 enable disable diff --git a/dotnet/samples/GettingStarted/Workflows/Declarative/DeclarativeWorkflow.csproj b/dotnet/samples/GettingStarted/Workflows/Declarative/DeclarativeWorkflow.csproj index 96a9d5df1d..fa80c24ff2 100644 --- a/dotnet/samples/GettingStarted/Workflows/Declarative/DeclarativeWorkflow.csproj +++ b/dotnet/samples/GettingStarted/Workflows/Declarative/DeclarativeWorkflow.csproj @@ -7,7 +7,6 @@ $(ProjectsDebugTargetFrameworks) enable disable - 5ee045b0-aea3-4f08-8d31-32d1a6f8fed0 $(NoWarn);CA1812 diff --git a/dotnet/samples/GettingStarted/Workflows/Declarative/Program.cs b/dotnet/samples/GettingStarted/Workflows/Declarative/Program.cs index ffacf3123a..914365b779 100644 --- a/dotnet/samples/GettingStarted/Workflows/Declarative/Program.cs +++ b/dotnet/samples/GettingStarted/Workflows/Declarative/Program.cs @@ -11,6 +11,7 @@ using Azure.AI.Agents.Persistent; using Azure.Identity; using Microsoft.Agents.Workflows; using Microsoft.Agents.Workflows.Declarative; +using Microsoft.Agents.Workflows.Declarative.Events; using Microsoft.Extensions.AI; using Microsoft.Extensions.Configuration; @@ -39,17 +40,11 @@ internal sealed class Program private async Task ExecuteAsync() { // Read and parse the declarative workflow. - Notify($"WORKFLOW: Parsing {Path.GetFullPath(this.WorkflowFile)}"); + Notify($"\nWORKFLOW: Parsing {Path.GetFullPath(this.WorkflowFile)}"); Stopwatch timer = Stopwatch.StartNew(); - // Use DeclarativeWorkflowBuilder to build a workflow based on a YAML file. - DeclarativeWorkflowOptions options = - new(new FoundryAgentProvider(this.FoundryEndpoint, new AzureCliCredential())) - { - Configuration = this.Configuration - }; - Workflow workflow = DeclarativeWorkflowBuilder.Build(this.WorkflowFile, options); + Workflow workflow = this.CreateWorkflow(); Notify($"\nWORKFLOW: Defined {timer.Elapsed}"); @@ -57,23 +52,69 @@ internal sealed class Program // Run the workflow, just like any other workflow string input = this.GetWorkflowInput(); - StreamingRun run = await InProcessExecution.StreamAsync(workflow, input); - await this.MonitorWorkflowRunAsync(run); - Notify("\nWORKFLOW: Done!"); + CheckpointManager checkpointManager = new(); + Checkpointed run = await InProcessExecution.StreamAsync(workflow, input, checkpointManager); + + bool isComplete = false; + InputResponse? response = null; + do + { + ExternalRequest? inputRequest = await this.MonitorWorkflowRunAsync(run, response); + if (inputRequest is not null) + { + Notify("\nWORKFLOW: Yield"); + + if (this.LastCheckpoint is null) + { + throw new InvalidOperationException("Checkpoint information missing after external request."); + } + + // Process the external request. + response = HandleExternalRequest(inputRequest); + + // Let's resume on an entirely new workflow instance to demonstrate checkpoint portability. + workflow = this.CreateWorkflow(); + + // Restore the latest checkpoint. + Debug.WriteLine($"RESTORE #{this.LastCheckpoint.CheckpointId}"); + Notify("\nWORKFLOW: Restore"); + run = await InProcessExecution.ResumeStreamAsync(workflow, this.LastCheckpoint, checkpointManager); + } + else + { + isComplete = true; + } + } + while (!isComplete); + + Notify("\nWORKFLOW: Done!\n"); + } + + private Workflow CreateWorkflow() + { + // Use DeclarativeWorkflowBuilder to build a workflow based on a YAML file. + DeclarativeWorkflowOptions options = + new(new AzureAgentProvider(this.FoundryEndpoint, new AzureCliCredential())) + { + Configuration = this.Configuration + }; + Workflow workflow = DeclarativeWorkflowBuilder.Build(this.WorkflowFile, options); + return workflow; } private const string DefaultWorkflow = "HelloWorld.yaml"; private const string ConfigKeyFoundryEndpoint = "FOUNDRY_PROJECT_ENDPOINT"; - private static readonly Dictionary s_nameCache = []; - private static readonly HashSet s_fileCache = []; + private static Dictionary NameCache { get; } = []; + private static HashSet FileCache { get; } = []; private string WorkflowFile { get; } private string? WorkflowInput { get; } private string FoundryEndpoint { get; } private PersistentAgentsClient FoundryClient { get; } private IConfiguration Configuration { get; } + private CheckpointInfo? LastCheckpoint { get; set; } private Program(string[] args) { @@ -86,112 +127,147 @@ internal sealed class Program this.FoundryClient = new PersistentAgentsClient(this.FoundryEndpoint, new AzureCliCredential()); } - private async Task MonitorWorkflowRunAsync(StreamingRun run) + private async Task MonitorWorkflowRunAsync(Checkpointed run, InputResponse? response = null) { string? messageId = null; - await foreach (WorkflowEvent evt in run.WatchStreamAsync().ConfigureAwait(false)) + await foreach (WorkflowEvent workflowEvent in run.Run.WatchStreamAsync().ConfigureAwait(false)) { - if (evt is ExecutorInvokedEvent executorInvoked) + switch (workflowEvent) { - Debug.WriteLine($"EXECUTOR ENTER #{executorInvoked.ExecutorId}"); - } - else if (evt is ExecutorCompletedEvent executorCompleted) - { - Debug.WriteLine($"EXECUTOR EXIT #{executorCompleted.ExecutorId}"); - } - if (evt is DeclarativeActionInvokeEvent actionInvoked) - { - Debug.WriteLine($"ACTION ENTER #{actionInvoked.ActionId} [{actionInvoked.ActionType}]"); - } - else if (evt is DeclarativeActionCompleteEvent actionComplete) - { - Debug.WriteLine($"ACTION EXIT #{actionComplete.ActionId} [{actionComplete.ActionType}]"); - } - else if (evt is ExecutorFailureEvent executorFailure) - { - Debug.WriteLine($"STEP ERROR #{executorFailure.ExecutorId}: {executorFailure.Data?.Message ?? "Unknown"}"); - } - else if (evt is ConversationUpdateEvent invokeEvent) - { - Debug.WriteLine($"CONVERSATION: {invokeEvent.Data}"); - } - else if (evt is AgentRunUpdateEvent streamEvent) - { - if (!string.Equals(messageId, streamEvent.Update.MessageId, StringComparison.Ordinal)) - { - messageId = streamEvent.Update.MessageId; + case ExecutorInvokedEvent executorInvoked: + Debug.WriteLine($"EXECUTOR ENTER #{executorInvoked.ExecutorId}"); + break; - if (messageId is not null) - { - string? agentId = streamEvent.Update.AuthorName; - if (agentId is not null) - { - if (!s_nameCache.TryGetValue(agentId, out string? realName)) - { - PersistentAgent agent = await this.FoundryClient.Administration.GetAgentAsync(agentId); - s_nameCache[agentId] = agent.Name; - realName = agent.Name; - } - agentId = realName; - } - agentId ??= nameof(ChatRole.Assistant); - Console.ForegroundColor = ConsoleColor.Cyan; - Console.Write($"\n{agentId.ToUpperInvariant()}:"); - Console.ForegroundColor = ConsoleColor.DarkGray; - Console.WriteLine($" [{messageId}]"); - } - } + case ExecutorCompletedEvent executorCompleted: + Debug.WriteLine($"EXECUTOR EXIT #{executorCompleted.ExecutorId}"); + break; - ChatResponseUpdate? chatUpdate = streamEvent.Update.RawRepresentation as ChatResponseUpdate; - switch (chatUpdate?.RawRepresentation) - { - case MessageContentUpdate messageUpdate: - string? fileId = messageUpdate.ImageFileId ?? messageUpdate.TextAnnotation?.OutputFileId; - if (fileId is not null && s_fileCache.Add(fileId)) - { - BinaryData content = await this.FoundryClient.Files.GetFileContentAsync(fileId); - await DownloadFileContentAsync(Path.GetFileName(messageUpdate.TextAnnotation?.TextToReplace ?? "response.png"), content); - } - break; - } - try - { - Console.ResetColor(); - Console.Write(streamEvent.Data); - } - finally - { - Console.ResetColor(); - } - } - else if (evt is AgentRunResponseEvent messageEvent) - { - try - { - Console.WriteLine(); - if (messageEvent.Response.AgentId is null) + case DeclarativeActionInvokeEvent actionInvoked: + Debug.WriteLine($"ACTION ENTER #{actionInvoked.ActionId} [{actionInvoked.ActionType}]"); + break; + + case DeclarativeActionCompleteEvent actionComplete: + Debug.WriteLine($"ACTION EXIT #{actionComplete.ActionId} [{actionComplete.ActionType}]"); + break; + + case ExecutorFailureEvent executorFailure: + Debug.WriteLine($"STEP ERROR #{executorFailure.ExecutorId}: {executorFailure.Data?.Message ?? "Unknown"}"); + break; + + case SuperStepCompletedEvent checkpointCompleted: + this.LastCheckpoint = checkpointCompleted.CompletionInfo?.Checkpoint; + Debug.WriteLine($"CHECKPOINT x{checkpointCompleted.StepNumber} [{this.LastCheckpoint?.CheckpointId ?? "(none)"}]"); + break; + + case RequestInfoEvent requestInfo: + Debug.WriteLine($"REQUEST #{requestInfo.Request.RequestId}"); + if (response is not null) { - Console.ForegroundColor = ConsoleColor.Cyan; - Console.WriteLine("ACTIVITY:"); - Console.ForegroundColor = ConsoleColor.Yellow; - Console.WriteLine(messageEvent.Response?.Text.Trim()); + ExternalResponse requestResponse = requestInfo.Request.CreateResponse(response); + await run.Run.SendResponseAsync(requestResponse).ConfigureAwait(false); + response = null; } else { + return requestInfo.Request; + } + break; + + case ConversationUpdateEvent invokeEvent: + Debug.WriteLine($"CONVERSATION: {invokeEvent.Data}"); + break; + + case MessageActivityEvent activityEvent: + Console.ForegroundColor = ConsoleColor.Cyan; + Console.WriteLine("\nACTIVITY:"); + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine(activityEvent.Message.Trim()); + break; + + case AgentRunUpdateEvent streamEvent: + if (!string.Equals(messageId, streamEvent.Update.MessageId, StringComparison.Ordinal)) + { + messageId = streamEvent.Update.MessageId; + + if (messageId is not null) + { + string? agentId = streamEvent.Update.AuthorName; + if (agentId is not null) + { + if (!NameCache.TryGetValue(agentId, out string? realName)) + { + PersistentAgent agent = await this.FoundryClient.Administration.GetAgentAsync(agentId); + NameCache[agentId] = agent.Name; + realName = agent.Name; + } + agentId = realName; + } + agentId ??= nameof(ChatRole.Assistant); + Console.ForegroundColor = ConsoleColor.Cyan; + Console.Write($"\n{agentId.ToUpperInvariant()}:"); + Console.ForegroundColor = ConsoleColor.DarkGray; + Console.WriteLine($" [{messageId}]"); + } + } + + ChatResponseUpdate? chatUpdate = streamEvent.Update.RawRepresentation as ChatResponseUpdate; + switch (chatUpdate?.RawRepresentation) + { + case MessageContentUpdate messageUpdate: + string? fileId = messageUpdate.ImageFileId ?? messageUpdate.TextAnnotation?.OutputFileId; + if (fileId is not null && FileCache.Add(fileId)) + { + BinaryData content = await this.FoundryClient.Files.GetFileContentAsync(fileId); + await DownloadFileContentAsync(Path.GetFileName(messageUpdate.TextAnnotation?.TextToReplace ?? "response.png"), content); + } + break; + } + try + { + Console.ResetColor(); + Console.Write(streamEvent.Data); + } + finally + { + Console.ResetColor(); + } + break; + + case AgentRunResponseEvent messageEvent: + try + { + Console.WriteLine(); if (messageEvent.Response.Usage is not null) { Console.ForegroundColor = ConsoleColor.DarkGray; Console.WriteLine($"[Tokens Total: {messageEvent.Response.Usage.TotalTokenCount}, Input: {messageEvent.Response.Usage.InputTokenCount}, Output: {messageEvent.Response.Usage.OutputTokenCount}]"); } } - } - finally - { - Console.ResetColor(); - } + finally + { + Console.ResetColor(); + } + break; } } + + return default; + } + private static InputResponse HandleExternalRequest(ExternalRequest request) + { + InputRequest? message = request.Data as InputRequest; + string? userInput = null; + do + { + Console.ForegroundColor = ConsoleColor.DarkGreen; + Console.Write($"\n{message?.Prompt ?? "INPUT:"} "); + Console.ForegroundColor = ConsoleColor.White; + userInput = Console.ReadLine(); + } + while (string.IsNullOrWhiteSpace(userInput)); + + return new InputResponse(userInput); } private static string ParseWorkflowFile(string[] args) diff --git a/dotnet/samples/GettingStarted/Workflows/HumanInTheLoop/HumanInTheLoopBasic/HumanInTheLoopBasic.csproj b/dotnet/samples/GettingStarted/Workflows/HumanInTheLoop/HumanInTheLoopBasic/HumanInTheLoopBasic.csproj index bbee070910..897d1d4fd4 100644 --- a/dotnet/samples/GettingStarted/Workflows/HumanInTheLoop/HumanInTheLoopBasic/HumanInTheLoopBasic.csproj +++ b/dotnet/samples/GettingStarted/Workflows/HumanInTheLoop/HumanInTheLoopBasic/HumanInTheLoopBasic.csproj @@ -3,7 +3,6 @@ Exe net9.0 - 12 enable disable diff --git a/dotnet/samples/GettingStarted/Workflows/Loop/Loop.csproj b/dotnet/samples/GettingStarted/Workflows/Loop/Loop.csproj index 75b48d7b65..7a4ee108ed 100644 --- a/dotnet/samples/GettingStarted/Workflows/Loop/Loop.csproj +++ b/dotnet/samples/GettingStarted/Workflows/Loop/Loop.csproj @@ -3,7 +3,6 @@ Exe net9.0 - 12 enable disable diff --git a/dotnet/samples/GettingStarted/Workflows/SharedStates/SharedStates.csproj b/dotnet/samples/GettingStarted/Workflows/SharedStates/SharedStates.csproj index d96f028c4b..e21730816c 100644 --- a/dotnet/samples/GettingStarted/Workflows/SharedStates/SharedStates.csproj +++ b/dotnet/samples/GettingStarted/Workflows/SharedStates/SharedStates.csproj @@ -3,7 +3,6 @@ Exe net9.0 - 12 enable disable diff --git a/dotnet/samples/GettingStarted/Workflows/_Foundational/01_ExecutorsAndEdges/01_ExecutorsAndEdges.csproj b/dotnet/samples/GettingStarted/Workflows/_Foundational/01_ExecutorsAndEdges/01_ExecutorsAndEdges.csproj index bbee070910..897d1d4fd4 100644 --- a/dotnet/samples/GettingStarted/Workflows/_Foundational/01_ExecutorsAndEdges/01_ExecutorsAndEdges.csproj +++ b/dotnet/samples/GettingStarted/Workflows/_Foundational/01_ExecutorsAndEdges/01_ExecutorsAndEdges.csproj @@ -3,7 +3,6 @@ Exe net9.0 - 12 enable disable diff --git a/dotnet/samples/GettingStarted/Workflows/_Foundational/02_Streaming/02_Streaming.csproj b/dotnet/samples/GettingStarted/Workflows/_Foundational/02_Streaming/02_Streaming.csproj index bbee070910..897d1d4fd4 100644 --- a/dotnet/samples/GettingStarted/Workflows/_Foundational/02_Streaming/02_Streaming.csproj +++ b/dotnet/samples/GettingStarted/Workflows/_Foundational/02_Streaming/02_Streaming.csproj @@ -3,7 +3,6 @@ Exe net9.0 - 12 enable disable diff --git a/dotnet/samples/GettingStarted/Workflows/_Foundational/03_AgentsInWorkflows/03_AgentsInWorkflows.csproj b/dotnet/samples/GettingStarted/Workflows/_Foundational/03_AgentsInWorkflows/03_AgentsInWorkflows.csproj index b1f90ee600..267e25180e 100644 --- a/dotnet/samples/GettingStarted/Workflows/_Foundational/03_AgentsInWorkflows/03_AgentsInWorkflows.csproj +++ b/dotnet/samples/GettingStarted/Workflows/_Foundational/03_AgentsInWorkflows/03_AgentsInWorkflows.csproj @@ -3,7 +3,6 @@ Exe net9.0 - 12 enable disable diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/AzureAgentProvider.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/AzureAgentProvider.cs new file mode 100644 index 0000000000..f88b7fa239 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/AzureAgentProvider.cs @@ -0,0 +1,181 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using Azure.AI.Agents.Persistent; +using Azure.Core; +using Azure.Core.Pipeline; +using Microsoft.Extensions.AI; +using Microsoft.Extensions.AI.Agents; + +namespace Microsoft.Agents.Workflows.Declarative; + +/// +/// Provides functionality to interact with Foundry agents within a specified project context. +/// +/// This class is used to retrieve and manage AI agents associated with a Foundry project. It requires a +/// project endpoint and credentials to authenticate requests. +/// The endpoint URL of the Foundry project. This must be a valid, non-null URI pointing to the project. +/// The credentials used to authenticate with the Foundry project. This must be a valid instance of . +/// An optional instance to be used for making HTTP requests. If not provided, a default client will be used. +public sealed class AzureAgentProvider(string projectEndpoint, TokenCredential projectCredentials, HttpClient? httpClient = null) : WorkflowAgentProvider +{ + private static readonly Dictionary s_roleMap = + new() + { + [ChatRole.User.Value.ToUpperInvariant()] = MessageRole.User, + [ChatRole.Assistant.Value.ToUpperInvariant()] = MessageRole.Agent, + [ChatRole.System.Value.ToUpperInvariant()] = new MessageRole(ChatRole.System.Value), + [ChatRole.Tool.Value.ToUpperInvariant()] = new MessageRole(ChatRole.Tool.Value), + }; + + private PersistentAgentsClient? _agentsClient; + + /// + public override async Task CreateConversationAsync(CancellationToken cancellationToken = default) + { + PersistentAgentThread conversation = await this.GetAgentsClient().Threads.CreateThreadAsync(cancellationToken: cancellationToken).ConfigureAwait(false); + return conversation.Id; + } + + /// + public override async Task CreateMessageAsync(string conversationId, ChatMessage conversationMessage, CancellationToken cancellationToken = default) + { + await this.GetAgentsClient().Messages.CreateMessageAsync( + conversationId, + role: s_roleMap[conversationMessage.Role.Value.ToUpperInvariant()], + // TODO: PersistentAgent bug blocks supporting multiple content types: + // https://github.com/Azure/azure-sdk-for-net/issues/52571 + //contentBlocks: GetContent(), + content: conversationMessage.Text, + attachments: null, + metadata: GetMetadata(), + cancellationToken).ConfigureAwait(false); + + Dictionary? GetMetadata() + { + if (conversationMessage.AdditionalProperties is null) + { + return null; + } + + return conversationMessage.AdditionalProperties.ToDictionary(prop => prop.Key, prop => prop.Value?.ToString() ?? string.Empty); + } + + // TODO: PersistentAgent bug blocks supporting multiple content types: + // https://github.com/Azure/azure-sdk-for-net/issues/52571 + //IEnumerable GetContent() + //{ + // foreach (AIContent content in conversationMessage.Contents) + // { + // MessageInputContentBlock? contentBlock = + // content switch + // { + // TextContent textContent => new MessageInputTextBlock(textContent.Text), + // HostedFileContent fileContent => new MessageInputImageFileBlock(new MessageImageFileParam(fileContent.FileId)), + // UriContent uriContent when uriContent.Uri is not null => new MessageInputImageUriBlock(new MessageImageUriParam(uriContent.Uri.ToString())), + // _ => null // Unsupported content type + // }; + + // if (contentBlock is not null) + // { + // yield return contentBlock; + // } + // } + //} + } + + /// + public override async Task GetAgentAsync(string agentId, CancellationToken cancellationToken = default) + { + AIAgent agent = await this.GetAgentsClient().GetAIAgentAsync(agentId, chatOptions: null, cancellationToken).ConfigureAwait(false); + + return agent; + } + + /// + public override async Task GetMessageAsync(string conversationId, string messageId, CancellationToken cancellationToken = default) + { + PersistentThreadMessage message = await this.GetAgentsClient().Messages.GetMessageAsync(conversationId, messageId, cancellationToken).ConfigureAwait(false); + return ToChatMessage(message); + } + + /// + public override async IAsyncEnumerable GetMessagesAsync( + string conversationId, + int? limit = null, + string? after = null, + string? before = null, + bool newestFirst = false, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + ListSortOrder order = newestFirst ? ListSortOrder.Ascending : ListSortOrder.Descending; + await foreach (PersistentThreadMessage message in this.GetAgentsClient().Messages.GetMessagesAsync(conversationId, runId: null, limit, order, after, before, cancellationToken).ConfigureAwait(false)) + { + yield return ToChatMessage(message); + } + } + + private PersistentAgentsClient GetAgentsClient() + { + if (this._agentsClient is null) + { + PersistentAgentsAdministrationClientOptions clientOptions = new(); + + if (httpClient is not null) + { + clientOptions.Transport = new HttpClientTransport(httpClient); + } + + PersistentAgentsClient newClient = new(projectEndpoint, projectCredentials, clientOptions); + + Interlocked.CompareExchange(ref this._agentsClient, newClient, null); + } + + return this._agentsClient; + } + + private static ChatMessage ToChatMessage(PersistentThreadMessage message) + { + return + new ChatMessage(new ChatRole(message.Role.ToString()), [.. GetContent()]) + { + MessageId = message.Id, + CreatedAt = message.CreatedAt, + AdditionalProperties = GetMetadata() + }; + + IEnumerable GetContent() + { + foreach (MessageContent contentItem in message.ContentItems) + { + AIContent? content = + contentItem switch + { + MessageTextContent textContent => new TextContent(textContent.Text), + MessageImageFileContent imageContent => new HostedFileContent(imageContent.FileId), + _ => null // Unsupported content type + }; + + if (content is not null) + { + yield return content; + } + } + } + + AdditionalPropertiesDictionary? GetMetadata() + { + if (message.Metadata is null) + { + return null; + } + + return new AdditionalPropertiesDictionary(message.Metadata.Select(m => new KeyValuePair(m.Key, m.Value))); + } + } +} diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/DeclarativeWorkflowBuilder.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/DeclarativeWorkflowBuilder.cs index 97a7d93328..2225921310 100644 --- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/DeclarativeWorkflowBuilder.cs +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/DeclarativeWorkflowBuilder.cs @@ -2,7 +2,6 @@ using System; using System.IO; -using System.Linq; using Microsoft.Agents.Workflows.Declarative.Extensions; using Microsoft.Agents.Workflows.Declarative.Interpreter; using Microsoft.Agents.Workflows.Declarative.PowerFx; @@ -22,7 +21,7 @@ public static class DeclarativeWorkflowBuilder /// /// The type of the input message /// The path to the workflow. - /// The execution context for the workflow. + /// Configuration options for workflow execution. /// An optional function to transform the input message into a . /// public static Workflow Build( @@ -34,12 +33,13 @@ public static class DeclarativeWorkflowBuilder using StreamReader yamlReader = File.OpenText(workflowFile); return Build(yamlReader, options, inputTransform); } + /// /// Builds a process from the provided YAML definition of a CPS Topic ObjectModel. /// /// The type of the input message /// The reader that provides the workflow object model YAML. - /// The execution context for the workflow. + /// Configuration options for workflow execution. /// An optional function to transform the input message into a . /// The that corresponds with the YAML object model. public static Workflow Build( @@ -56,20 +56,20 @@ public static class DeclarativeWorkflowBuilder throw new DeclarativeModelException($"Unsupported root element: {rootElement.GetType().Name}. Expected an {nameof(AdaptiveDialog)}."); } - string rootId = WorkflowActionVisitor.RootId(workflowElement.BeginDialog?.Id.Value ?? "workflow"); + string rootId = WorkflowActionVisitor.Steps.Root(workflowElement.BeginDialog?.Id.Value); - WorkflowScopes scopes = new(); - scopes.Initialize(WrapWithBot(workflowElement), options.Configuration); - DeclarativeWorkflowState state = new(options.CreateRecalcEngine(), scopes); + WorkflowFormulaState state = new(options.CreateRecalcEngine()); + state.Initialize(workflowElement.WrapWithBot(), options.Configuration); DeclarativeWorkflowExecutor rootExecutor = new(rootId, state, message => inputTransform?.Invoke(message) ?? DefaultTransform(message)); WorkflowActionVisitor visitor = new(rootExecutor, state, options); - WorkflowElementWalker walker = new(rootElement, visitor); + WorkflowElementWalker walker = new(visitor); + walker.Visit(rootElement); - return walker.GetWorkflow(); + return visitor.Complete(); } private static ChatMessage DefaultTransform(object message) => @@ -79,23 +79,4 @@ public static class DeclarativeWorkflowBuilder string stringMessage => new ChatMessage(ChatRole.User, stringMessage), _ => new(ChatRole.User, $"{message}") }; - - // Wrap with bot to ensure schema is set. - private static AdaptiveDialog WrapWithBot(AdaptiveDialog dialog) - { - BotDefinition bot - = new BotDefinition.Builder - { - Components = - { - new DialogComponent.Builder - { - SchemaName = dialog.HasSchemaName ? dialog.SchemaName : "default-schema", - Dialog = new AdaptiveDialog.Builder(dialog), - } - } - }.Build(); - - return bot.Descendants().OfType().First(); - } } diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/DeclarativeWorkflowEvents.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/DeclarativeWorkflowEvents.cs deleted file mode 100644 index 74970f62f1..0000000000 --- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/DeclarativeWorkflowEvents.cs +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using Microsoft.Agents.Workflows.Declarative.Extensions; -using Microsoft.Bot.ObjectModel; - -namespace Microsoft.Agents.Workflows.Declarative; - -/// -/// Event that broadcasts the conversation identifier. -/// -public class ConversationUpdateEvent(string executorid, string conversationId) : ExecutorEvent(executorid, conversationId) -{ - /// - /// The conversation ID associated with the workflow. - /// - public string ConversationId { get; } = conversationId; -} - -/// -/// Event that indicates a declarative action has been invoked. -/// -public class DeclarativeActionInvokeEvent(string actionId, DialogAction action, string? priorActionId) : WorkflowEvent(action) -{ - /// - /// The declarative action id. - /// - public string ActionId => actionId; - - /// - /// The declarative action type name. - /// - public string ActionType => action.GetType().Name; - - /// - /// Identifier of the parent action. - /// - public string? ParentActionId => action.GetParentId(); - - /// - /// Identifier of the previous action. - /// - public string? PriorActionId => priorActionId; -} - -/// -/// Event that indicates a declarative action has completed. -/// -public class DeclarativeActionCompleteEvent(string actionId, DialogAction action) : WorkflowEvent(action) -{ - /// - /// The declarative action identifier. - /// - public string ActionId => actionId; - - /// - /// The declarative action type name. - /// - public string ActionType => action.GetType().Name; - - /// - /// Identifier of the parent action. - /// - public string? ParentActionId => action.GetParentId(); -} diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Entities/EntityExtractionResult.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Entities/EntityExtractionResult.cs new file mode 100644 index 0000000000..96029a7e23 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Entities/EntityExtractionResult.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.PowerFx.Types; + +namespace Microsoft.Agents.Workflows.Declarative.Entities; + +internal sealed record class EntityExtractionResult +{ + public EntityExtractionResult(FormulaValue? value) + { + this.Value = value; + this.ErrorMessage = null; + } + + public EntityExtractionResult(string errorMessage) + { + this.Value = null; + this.ErrorMessage = errorMessage; + } + + public FormulaValue? Value { get; } + public string? ErrorMessage { get; } + public bool IsValid => this.Value is not null; +} diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Entities/EntityExtractor.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Entities/EntityExtractor.cs new file mode 100644 index 0000000000..7ba0ddac0e --- /dev/null +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Entities/EntityExtractor.cs @@ -0,0 +1,173 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Net.Mail; +using System.Text.RegularExpressions; +using Microsoft.Bot.ObjectModel; +using Microsoft.PowerFx.Types; + +namespace Microsoft.Agents.Workflows.Declarative.Entities; + +internal static partial class EntityExtractor +{ + private const string NumberUnitRegExExpression = @"(?[-+]?(?:\d{1,3}(?:,\d{3})+|\d+)(?:\.\d+)?|\d*\.\d+)"; + +#if NET + [GeneratedRegex(NumberUnitRegExExpression, RegexOptions.IgnoreCase)] + private static partial Regex NumberUnitRegex(); +#else + private static Regex NumberUnitRegex() => s_numberUnitRegex; + private static readonly Regex s_numberUnitRegex = new(NumberUnitRegExExpression, RegexOptions.IgnoreCase | RegexOptions.Compiled); +#endif + + public static EntityExtractionResult Parse(EntityReference? entity, string value) => + entity switch + { + null => UndefinedEntity(value), + AgePrebuiltEntity => TryParseNumberUnit(value, "age"), + BooleanPrebuiltEntity => TryParseBoolean(value), + CityPrebuiltEntity => TryParseString(value), + ColorPrebuiltEntity => TryParseString(value), + ContinentPrebuiltEntity => TryParseString(value), + CountryOrRegionPrebuiltEntity => TryParseString(value), + DatePrebuiltEntity => TryParseDate(value), + DateTimeNoTimeZonePrebuiltEntity => TryParseDateTimeNoTimeZone(value), + DateTimePrebuiltEntity => TryParseDateTime(value), + DurationPrebuiltEntity => TryParseDuration(value), + EmailPrebuiltEntity => TryParseEmail(value), + EventPrebuiltEntity => TryParseString(value), + LanguagePrebuiltEntity => TryParseString(value), + MoneyPrebuiltEntity => TryParseNumberUnit(value, "money"), + NumberPrebuiltEntity => TryParseNumber(value), + PercentagePrebuiltEntity => TryParseNumberUnit(value, "percentage"), + PhoneNumberPrebuiltEntity => TryParseString(value), + PointOfInterestPrebuiltEntity => TryParseString(value), + SpeedPrebuiltEntity => TryParseNumberUnit(value, "speed"), + StatePrebuiltEntity => TryParseString(value), + StreetAddressPrebuiltEntity => TryParseString(value), + StringPrebuiltEntity => TryParseString(value), + TemperaturePrebuiltEntity => TryParseNumberUnit(value, "temperature"), + URLPrebuiltEntity => TryParseURL(value), + WeightPrebuiltEntity => TryParseNumberUnit(value, "weight"), + _ => UnsupportedEntity(entity), + }; + + private static EntityExtractionResult TryParseBoolean(string value) + { + if (bool.TryParse(value, out bool parsedValue)) + { + return new EntityExtractionResult(FormulaValue.New(parsedValue)); + } + + return new EntityExtractionResult($"Invalid boolean value: {value}"); + } + + private static EntityExtractionResult TryParseDate(string value) + { + if (DateTime.TryParse(value, out DateTime parsedValue)) + { + return new EntityExtractionResult(FormulaValue.New(parsedValue.Date)); + } + + return new EntityExtractionResult($"Invalid date value: {value}"); + } + + private static EntityExtractionResult TryParseDateTimeNoTimeZone(string value) + { + if (DateTime.TryParse(value, out DateTime parsedValue)) + { + return new EntityExtractionResult( + FormulaValue.New( + DateTime.SpecifyKind(parsedValue, DateTimeKind.Unspecified))); + } + + return new EntityExtractionResult($"Invalid date value: {value}"); + } + + private static EntityExtractionResult TryParseDateTime(string value) + { + if (DateTime.TryParse(value, out DateTime parsedValue)) + { + return new EntityExtractionResult(FormulaValue.New(parsedValue)); + } + + return new EntityExtractionResult($"Invalid date-time value: {value}"); + } + + private static EntityExtractionResult TryParseDuration(string value) + { + if (TimeSpan.TryParse(value, out TimeSpan parsedValue)) + { + return new EntityExtractionResult(FormulaValue.New(parsedValue)); + } + + return new EntityExtractionResult($"Invalid duration value: {value}"); + } + + private static EntityExtractionResult TryParseEmail(string value) + { + try + { + MailAddress parsedValue = new(value); + return new EntityExtractionResult(FormulaValue.New(parsedValue.Address)); + } + catch + { + return new EntityExtractionResult($"Invalid email value: {value}"); + } + } + + private static EntityExtractionResult TryParseNumberUnit(string value, string type) + { + Match m = NumberUnitRegex().Match(value); + if (m.Success) + { + return new EntityExtractionResult(FormulaValue.New(m.Groups[0].Value)); + } + + return new EntityExtractionResult($"Invalid {type} value: {value}"); + } + + private static EntityExtractionResult TryParseNumber(string value) + { + if (double.TryParse(value, out double parsedValue)) + { + return new EntityExtractionResult(FormulaValue.New(parsedValue)); + } + + return new EntityExtractionResult($"Invalid double value: {value}"); + } + + private static EntityExtractionResult TryParseString(string value) + { + if (!string.IsNullOrWhiteSpace(value)) + { + return new EntityExtractionResult(FormulaValue.New(value)); + } + + return new EntityExtractionResult("Empty value"); + } + + private static EntityExtractionResult TryParseURL(string value) + { + if (Uri.TryCreate(value, UriKind.Absolute, out Uri? uriResult)) + { + return new EntityExtractionResult(FormulaValue.New(uriResult.AbsoluteUri)); + } + + return new EntityExtractionResult($"Invalid double value: {value}"); + } + + private static EntityExtractionResult UndefinedEntity(string value) + { + if (string.IsNullOrWhiteSpace(value)) + { + return new EntityExtractionResult(FormulaValue.NewBlank()); + } + + return new EntityExtractionResult(FormulaValue.New(value)); + } + + private static EntityExtractionResult UnsupportedEntity(EntityReference entity) => + new($"Unsupported entity: {entity.GetType().Name}"); +} diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Events/ConversationUpdateEvent.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Events/ConversationUpdateEvent.cs new file mode 100644 index 0000000000..9fa9a12ccc --- /dev/null +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Events/ConversationUpdateEvent.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft. All rights reserved. + +namespace Microsoft.Agents.Workflows.Declarative; + +/// +/// Event that broadcasts the conversation identifier. +/// +public sealed class ConversationUpdateEvent : WorkflowEvent +{ + /// + /// The conversation ID associated with the workflow. + /// + public string ConversationId { get; } + + internal ConversationUpdateEvent(string conversationId) + : base(conversationId) + { + this.ConversationId = conversationId; + } +} diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Events/DeclarativeActionCompleteEvent.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Events/DeclarativeActionCompleteEvent.cs new file mode 100644 index 0000000000..b574be1621 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Events/DeclarativeActionCompleteEvent.cs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.Agents.Workflows.Declarative.Extensions; +using Microsoft.Bot.ObjectModel; + +namespace Microsoft.Agents.Workflows.Declarative; + +/// +/// Event that indicates a declarative action has been invoked. +/// +public sealed class DeclarativeActionInvokeEvent : WorkflowEvent +{ + /// + /// The declarative action id. + /// + public string ActionId { get; } + + /// + /// The declarative action type name. + /// + public string ActionType { get; } + + /// + /// Identifier of the parent action. + /// + public string? ParentActionId { get; } + + /// + /// Identifier of the previous action. + /// + public string? PriorActionId { get; } + + internal DeclarativeActionInvokeEvent(DialogAction action, string? priorActionId) : base(action) + { + this.ActionId = action.GetId(); + this.ActionType = action.GetType().Name; + this.ParentActionId = action.GetParentId(); + this.PriorActionId = priorActionId; + } +} diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Events/DeclarativeActionInvokeEvent.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Events/DeclarativeActionInvokeEvent.cs new file mode 100644 index 0000000000..bb9f676635 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Events/DeclarativeActionInvokeEvent.cs @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.Agents.Workflows.Declarative.Extensions; +using Microsoft.Bot.ObjectModel; + +namespace Microsoft.Agents.Workflows.Declarative; + +/// +/// Event that indicates a declarative action has completed. +/// +public sealed class DeclarativeActionCompleteEvent : WorkflowEvent +{ + /// + /// The declarative action identifier. + /// + public string ActionId { get; } + + /// + /// The declarative action type name. + /// + public string ActionType { get; } + + /// + /// Identifier of the parent action. + /// + public string? ParentActionId { get; } + + internal DeclarativeActionCompleteEvent(DialogAction action) : base(action) + { + this.ActionId = action.GetId(); + this.ActionType = action.GetType().Name; + this.ParentActionId = action.GetParentId(); + } +} diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Events/InputRequest.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Events/InputRequest.cs new file mode 100644 index 0000000000..c2ae30797f --- /dev/null +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Events/InputRequest.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft. All rights reserved. + +namespace Microsoft.Agents.Workflows.Declarative.Events; + +/// +/// Represents a request for user input. +/// +public sealed class InputRequest +{ + /// + /// The prompt message to display to the user. + /// + public string Prompt { get; } + + internal InputRequest(string prompt) + { + this.Prompt = prompt; + } +} diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Events/InputResponse.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Events/InputResponse.cs new file mode 100644 index 0000000000..e72a74b583 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Events/InputResponse.cs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft. All rights reserved. + +namespace Microsoft.Agents.Workflows.Declarative.Events; + +/// +/// Represents a user input response. +/// +public sealed class InputResponse +{ + /// + /// The response value. + /// + public string Value { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The response value. + public InputResponse(string value) + { + this.Value = value; + } +} diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Events/MessageActivityEvent.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Events/MessageActivityEvent.cs new file mode 100644 index 0000000000..1fb786b19f --- /dev/null +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Events/MessageActivityEvent.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft. All rights reserved. + +namespace Microsoft.Agents.Workflows.Declarative; + +/// +/// Event that broadcasts the conversation identifier. +/// +public sealed class MessageActivityEvent : WorkflowEvent +{ + /// + /// The conversation ID associated with the workflow. + /// + public string Message { get; } + + internal MessageActivityEvent(string message) : base(message) + { + this.Message = message; + } +} diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Exceptions/DeclarativeModelException.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Exceptions/DeclarativeModelException.cs index f6b6adfb11..3f5563cb68 100644 --- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Exceptions/DeclarativeModelException.cs +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Exceptions/DeclarativeModelException.cs @@ -7,7 +7,7 @@ namespace Microsoft.Agents.Workflows.Declarative; /// /// Represents an exception that occurs when the declarative model is not supported. /// -public class DeclarativeModelException : DeclarativeWorkflowException +public sealed class DeclarativeModelException : DeclarativeWorkflowException { /// /// Initializes a new instance of the class. diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/ChatMessageExtensions.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/ChatMessageExtensions.cs index 89a65c86b2..34e7efdb80 100644 --- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/ChatMessageExtensions.cs +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/ChatMessageExtensions.cs @@ -1,6 +1,9 @@ // Copyright (c) Microsoft. All rights reserved. +using System; using System.Collections.Generic; +using System.Linq; +using Microsoft.Agents.Workflows.Declarative.PowerFx.Functions; using Microsoft.Bot.ObjectModel; using Microsoft.Extensions.AI; using Microsoft.PowerFx.Types; @@ -9,15 +12,234 @@ namespace Microsoft.Agents.Workflows.Declarative.Extensions; internal static class ChatMessageExtensions { - // ISSUE #485 - Align with message type updated OM is available. public static RecordValue ToRecord(this ChatMessage message) => RecordValue.NewRecordFromFields(message.GetMessageFields()); + public static TableValue ToTable(this IEnumerable messages) => + FormulaValue.NewTable(s_messageRecordType, messages.Select(message => message.ToRecord())); + + public static IEnumerable ToChatMessages(this DataValue messages) + { + if (messages is TableDataValue table) + { + return table.ToChatMessages(); + } + + if (messages is RecordDataValue record) + { + return [record.ToChatMessage()]; + } + + if (messages is StringDataValue text) + { + return [text.ToChatMessage()]; + } + + return []; + } + + public static IEnumerable ToChatMessages(this TableDataValue messages) + { + foreach (DataValue message in messages.Values) + { + if (message is RecordDataValue record) + { + if (record.Properties.Count == 1 && record.Properties.TryGetValue("Value", out DataValue? singleColumn)) + { + record = singleColumn as RecordDataValue ?? record; + } + ChatMessage? convertedMessage = record.ToChatMessage(); + if (convertedMessage is not null) + { + yield return convertedMessage; + } + } + else if (message is StringDataValue text) + { + yield return ToChatMessage(text); + } + } + } + + public static ChatMessage? ToChatMessage(this DataValue message) + { + if (message is RecordDataValue record) + { + return record.ToChatMessage(); + } + + if (message is StringDataValue text) + { + return text.ToChatMessage(); + } + + if (message is BlankDataValue) + { + return null; + } + + throw new DeclarativeActionException($"Unable to convert {message.GetDataType()} to {nameof(ChatMessage)}."); + } + + public static ChatMessage ToChatMessage(this RecordDataValue message) => + new(message.GetRole(), [.. message.GetContent()]) + { + AdditionalProperties = message.GetProperty("metadata").ToMetadata() + }; + + public static ChatMessage ToChatMessage(this StringDataValue message) => new(ChatRole.User, message.Value); + + public static AdditionalPropertiesDictionary? ToMetadata(this RecordDataValue? metadata) + { + if (metadata is null) + { + return null; + } + + AdditionalPropertiesDictionary properties = []; + + foreach (KeyValuePair property in metadata.Properties) + { + properties[property.Key] = property.Value.ToObject(); + } + + return properties; + } + + public static ChatRole ToChatRole(this AgentMessageRole role) => + role switch + { + AgentMessageRole.Agent => ChatRole.Assistant, + AgentMessageRole.User => ChatRole.User, + _ => ChatRole.User + }; + + public static ChatRole ToChatRole(this AgentMessageRole? role) => role?.ToChatRole() ?? ChatRole.User; + + public static AIContent? ToContent(this AgentMessageContentType contentType, string? contentValue) + { + if (string.IsNullOrEmpty(contentValue)) + { + return null; + } + + return + contentType switch + { + AgentMessageContentType.ImageUrl => new UriContent(contentValue, "image/*"), + AgentMessageContentType.ImageFile => new HostedFileContent(contentValue), + _ => new TextContent(contentValue) + }; + } + + private static ChatRole GetRole(this RecordDataValue message) + { + StringDataValue? roleValue = message.GetProperty(TypeSchema.Message.Fields.Role); + if (roleValue is null || string.IsNullOrWhiteSpace(roleValue.Value)) + { + return ChatRole.User; + } + + AgentMessageRole? role = null; + if (Enum.TryParse(roleValue.Value, out AgentMessageRole parsedRole)) + { + role = parsedRole; + } + + return role.ToChatRole(); + } + + private static IEnumerable GetContent(this RecordDataValue message) + { + TableDataValue? content = message.GetProperty(TypeSchema.Message.Fields.Content); + if (content is not null) + { + foreach (RecordDataValue contentItem in content.Values) + { + StringDataValue? contentValue = contentItem?.GetProperty(TypeSchema.Message.Fields.ContentValue); + if (contentValue is null || string.IsNullOrWhiteSpace(contentValue.Value)) + { + continue; + } + yield return + contentItem?.GetProperty(TypeSchema.Message.Fields.ContentType)?.Value switch + { + TypeSchema.Message.ContentTypes.ImageUrl => new UriContent(contentValue.Value, "image/*"), + TypeSchema.Message.ContentTypes.ImageFile => new HostedFileContent(contentValue.Value), + _ => new TextContent(contentValue.Value) + }; + } + } + } + + private static TValue? GetProperty(this RecordDataValue record, string name) + where TValue : DataValue + { + if (record.Properties.TryGetValue(name, out DataValue? value) && value is TValue dataValue) + { + return dataValue; + } + + return null; + } + private static IEnumerable GetMessageFields(this ChatMessage message) { - yield return new NamedValue(nameof(DialogAction.Id), message.MessageId.ToFormulaValue()); - yield return new NamedValue(nameof(ChatMessage.Role), FormulaValue.New(message.Role.Value)); - yield return new NamedValue(nameof(ChatMessage.AuthorName), message.AuthorName.ToFormulaValue()); - yield return new NamedValue(nameof(ChatMessage.Text), message.Text.ToFormulaValue()); + yield return new NamedValue(TypeSchema.Message.Fields.Id, message.MessageId.ToFormula()); + yield return new NamedValue(TypeSchema.Message.Fields.Role, message.Role.Value.ToFormula()); + yield return new NamedValue(TypeSchema.Message.Fields.Author, message.AuthorName.ToFormula()); + yield return new NamedValue(TypeSchema.Message.Fields.Content, FormulaValue.NewTable(s_contentRecordType, message.GetContentRecords())); + yield return new NamedValue(TypeSchema.Message.Fields.Text, message.Text.ToFormula()); + yield return new NamedValue(TypeSchema.Message.Fields.Metadata, message.AdditionalProperties.ToRecord()); } + + private static IEnumerable GetContentRecords(this ChatMessage message) => + message.Contents.Select(content => FormulaValue.NewRecordFromFields(content.GetContentFields())); + + private static IEnumerable GetContentFields(this AIContent content) + { + return + content switch + { + UriContent uriContent => CreateContentRecord(TypeSchema.Message.ContentTypes.ImageUrl, uriContent.Uri.ToString()), + HostedFileContent fileContent => CreateContentRecord(TypeSchema.Message.ContentTypes.ImageFile, fileContent.FileId), + TextContent textContent => CreateContentRecord(TypeSchema.Message.ContentTypes.Text, textContent.Text), + _ => [] + }; + + static IEnumerable CreateContentRecord(string type, string value) + { + yield return new NamedValue(TypeSchema.Message.Fields.ContentType, type.ToFormula()); + yield return new NamedValue(TypeSchema.Message.Fields.ContentValue, value.ToFormula()); + } + } + private static RecordValue ToRecord(this AdditionalPropertiesDictionary? value) + { + return FormulaValue.NewRecordFromFields(GetFields()); + + IEnumerable GetFields() + { + if (value is not null) + { + foreach (string key in value.Keys) + { + yield return new NamedValue(key, value[key].ToFormula()); + } + } + } + } + + private static readonly RecordType s_contentRecordType = + RecordType.Empty() + .Add(TypeSchema.Message.Fields.ContentType, FormulaType.String) + .Add(TypeSchema.Message.Fields.ContentValue, FormulaType.String); + + private static readonly RecordType s_messageRecordType = + RecordType.Empty() + .Add(TypeSchema.Message.Fields.Id, FormulaType.String) + .Add(TypeSchema.Message.Fields.Role, FormulaType.String) + .Add(TypeSchema.Message.Fields.Author, FormulaType.String) + .Add(TypeSchema.Message.Fields.Content, s_contentRecordType.ToTable()) + .Add(TypeSchema.Message.Fields.Text, FormulaType.String) + .Add(TypeSchema.Message.Fields.Metadata, RecordType.Empty()); } diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/DataValueExtensions.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/DataValueExtensions.cs index 81666b6013..7fa82066e1 100644 --- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/DataValueExtensions.cs +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/DataValueExtensions.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. using System.Collections.Generic; +using System.Dynamic; using System.Linq; using Microsoft.Bot.ObjectModel; using Microsoft.PowerFx.Types; @@ -9,11 +10,11 @@ namespace Microsoft.Agents.Workflows.Declarative.Extensions; internal static class DataValueExtensions { - public static FormulaValue ToFormulaValue(this DataValue? value) => + public static FormulaValue ToFormula(this DataValue? value) => value switch { null => FormulaValue.NewBlank(), - BlankDataValue blankValue => BlankValue.NewBlank(), + BlankDataValue => BlankValue.NewBlank(), BooleanDataValue boolValue => FormulaValue.New(boolValue.Value), NumberDataValue numberValue => FormulaValue.New(numberValue.Value), FloatDataValue floatValue => FormulaValue.New(floatValue.Value), @@ -53,12 +54,30 @@ internal static class DataValueExtensions _ => FormulaType.Unknown, }; + public static object? ToObject(this DataValue? value) => + value switch + { + null => null, + BlankDataValue => null, + BooleanDataValue boolValue => boolValue.Value, + NumberDataValue numberValue => numberValue.Value, + FloatDataValue floatValue => floatValue.Value, + StringDataValue stringValue => stringValue.Value, + DateTimeDataValue dateTimeValue => dateTimeValue.Value.DateTime, + DateDataValue dateValue => dateValue.Value, + TimeDataValue timeValue => timeValue.Value, + TableDataValue tableValue => tableValue.Values.Select(value => value.ToObject()).ToArray(), + RecordDataValue recordValue => recordValue.ToDictionary(), + OptionDataValue optionValue => optionValue.Value.Value, + _ => throw new DeclarativeModelException($"Unsupported {nameof(DataValue)} type: {value.GetType().Name}"), + }; + public static FormulaValue NewBlank(this DataType? type) => FormulaValue.NewBlank(type?.ToFormulaType() ?? FormulaType.Blank); public static RecordValue ToRecordValue(this RecordDataValue recordDataValue) => FormulaValue.NewRecordFromFields( recordDataValue.Properties.Select( - property => new NamedValue(property.Key, property.Value.ToFormulaValue()))); + property => new NamedValue(property.Key, property.Value.ToFormula()))); public static RecordType ToRecordType(this RecordDataType record) { @@ -70,6 +89,19 @@ internal static class DataValueExtensions return recordType; } + public static ExpandoObject ToObject(this RecordDataValue recordDataValue) + { + ExpandoObject expandoObject = new(); + + IDictionary dictionary = expandoObject; + foreach (KeyValuePair field in recordDataValue.Properties) + { + dictionary[field.Key] = field.Value?.ToObject(); + } + + return expandoObject; + } + private static RecordType ParseRecordType(this RecordDataValue record) { RecordType recordType = RecordType.Empty(); @@ -79,4 +111,14 @@ internal static class DataValueExtensions } return recordType; } + + private static Dictionary ToDictionary(this RecordDataValue record) + { + Dictionary result = []; + foreach (KeyValuePair property in record.Properties) + { + result[property.Key] = property.Value.ToObject(); + } + return result; + } } diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/DialogBaseExtensions.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/DialogBaseExtensions.cs new file mode 100644 index 0000000000..97b07068e7 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/DialogBaseExtensions.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Linq; +using Microsoft.Bot.ObjectModel; + +namespace Microsoft.Agents.Workflows.Declarative.Extensions; + +internal static class DialogBaseExtensions +{ + public static TDialog WrapWithBot(this TDialog dialog) where TDialog : DialogBase + { + BotDefinition bot + = new BotDefinition.Builder + { + Components = + { + new DialogComponent.Builder + { + SchemaName = dialog.HasSchemaName ? dialog.SchemaName : "default-schema", + Dialog = dialog.ToBuilder(), + } + } + }.Build(); + + return bot.Descendants().OfType().First(); + } +} diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/FormulaValueExtensions.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/FormulaValueExtensions.cs index 568f3ba104..f815486686 100644 --- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/FormulaValueExtensions.cs +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/FormulaValueExtensions.cs @@ -8,6 +8,7 @@ using System.Dynamic; using System.Linq; using System.Text.Json; using System.Text.Json.Nodes; +using Microsoft.Agents.Workflows.Declarative.PowerFx; using Microsoft.Bot.ObjectModel; using Microsoft.PowerFx.Types; using BlankType = Microsoft.PowerFx.Types.BlankType; @@ -20,10 +21,11 @@ internal static class FormulaValueExtensions public static FormulaValue NewBlank(this FormulaType? type) => FormulaValue.NewBlank(type ?? FormulaType.Blank); - public static FormulaValue ToFormulaValue(this object? value) => + public static FormulaValue ToFormula(this object? value) => value switch { null => FormulaValue.NewBlank(), + UnassignedValue => FormulaValue.NewBlank(), FormulaValue formulaValue => formulaValue, bool booleanValue => FormulaValue.New(booleanValue), int decimalValue => FormulaValue.New(decimalValue), @@ -35,8 +37,9 @@ internal static class FormulaValueExtensions DateTime dateonlyValue when dateonlyValue.TimeOfDay == TimeSpan.Zero => FormulaValue.NewDateOnly(dateonlyValue), DateTime datetimeValue => FormulaValue.New(datetimeValue), TimeSpan timeValue => FormulaValue.New(timeValue), - object when value is IEnumerable tableValue => tableValue.ToTable(), ExpandoObject expandoValue => expandoValue.ToRecord(), + object when value is IDictionary dictionaryValue => dictionaryValue.ToRecord(), + object when value is IEnumerable tableValue => tableValue.ToTable(), _ => throw new DeclarativeModelException($"Unsupported variable type: {value.GetType().Name}"), }; @@ -141,7 +144,20 @@ internal static class FormulaValueExtensions TableDataValue.TableFromRecords(value.Rows.Select(row => row.Value.ToRecord()).ToImmutableArray()); public static RecordDataValue ToRecord(this RecordValue value) => - RecordDataValue.RecordFromFields(value.OriginalFields.Select(field => field.GetKeyValuePair()).ToImmutableArray()); + RecordDataValue.RecordFromFields(value.OriginalFields.Select(field => field.GetKeyValuePair())); + + private static RecordValue ToRecord(this IDictionary value) + { + return FormulaValue.NewRecordFromFields(GetFields()); + + IEnumerable GetFields() + { + foreach (string key in value.Keys) + { + yield return new NamedValue(key, value[key].ToFormula()); + } + } + } private static RecordDataType ToDataType(this RecordType record) { @@ -176,48 +192,27 @@ internal static class FormulaValueExtensions private static RecordValue ToRecord(this ExpandoObject value) => FormulaValue.NewRecordFromFields( value.Select( - property => new NamedValue(property.Key, property.Value.ToFormulaValue()))); + property => new NamedValue(property.Key, property.Value.ToFormula()))); private static TableType ToTableType(this IEnumerable value) { - Type valueType = value.GetType(); - Type? elementType = valueType.GetElementType() ?? valueType.GetGenericArguments().FirstOrDefault(); - - if (elementType is not null) + foreach (object? element in value) { - if (elementType != typeof(ExpandoObject)) + if (element is not ExpandoObject expandoElement) { - throw new DeclarativeModelException($"Invalid table element: {elementType.Name}"); + throw new DeclarativeModelException($"Invalid table element: {element.GetType().Name}"); } - foreach (ExpandoObject element in value) - { - return element.ToRecordType().ToTable(); - } + return expandoElement.ToRecordType().ToTable(); // Return first element } return TableType.Empty(); } - private static TableValue ToTable(this IEnumerable value) - { - Type valueType = value.GetType(); - Type? elementType = valueType.GetElementType() ?? valueType.GetGenericArguments().FirstOrDefault(); - - if (elementType is null) - { - return FormulaValue.NewTable(RecordType.EmptySealed()); - } - - if (elementType != typeof(ExpandoObject)) - { - throw new DeclarativeModelException($"Invalid table element: {elementType.Name}"); - } - - List records = [.. value.OfType().Select(element => element.ToRecord())]; - - return FormulaValue.NewTable(value.ToTableType().ToRecord(), records); - } + private static TableValue ToTable(this IEnumerable value) => + FormulaValue.NewTable( + value.ToTableType().ToRecord(), + [.. value.OfType().Select(element => element.ToRecord())]); private static KeyValuePair GetKeyValuePair(this NamedValue value) => new(value.Name, value.Value.ToDataValue()); diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/IWorkflowContextExtensions.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/IWorkflowContextExtensions.cs new file mode 100644 index 0000000000..aa6fc18a98 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/IWorkflowContextExtensions.cs @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Agents.Workflows.Declarative.Interpreter; +using Microsoft.Agents.Workflows.Declarative.PowerFx; +using Microsoft.Bot.ObjectModel; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Agents.Workflows.Declarative.Extensions; + +internal static class IWorkflowContextExtensions +{ + public static ValueTask RaiseInvocationEventAsync(this IWorkflowContext context, DialogAction action, string? priorEventId = null) => + context.AddEventAsync(new DeclarativeActionInvokeEvent(action, priorEventId)); + + public static ValueTask RaiseCompletionEventAsync(this IWorkflowContext context, DialogAction action) => + context.AddEventAsync(new DeclarativeActionCompleteEvent(action)); + + public static ValueTask SendResultMessageAsync(this IWorkflowContext context, string id, object? result = null, CancellationToken cancellationToken = default) => + context.SendMessageAsync(new ExecutorResultMessage(id, result)); + + public static ValueTask QueueStateUpdateAsync(this IWorkflowContext context, PropertyPath variablePath, TValue? value) => + context.QueueStateUpdateAsync(Throw.IfNull(variablePath.VariableName), value, Throw.IfNull(variablePath.VariableScopeName)); + + public static async Task GetStateAsync(this IWorkflowContext context, CancellationToken cancellationToken) + { + if (context is DeclarativeWorkflowContext declarativeContext) + { + return declarativeContext.State; + } + + WorkflowFormulaState state = new(RecalcEngineFactory.Create()); + + await state.RestoreAsync(context, cancellationToken).ConfigureAwait(false); + + return state; + } +} diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/StringExtensions.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/StringExtensions.cs index 0f3a078d2c..83099fddee 100644 --- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/StringExtensions.cs +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/StringExtensions.cs @@ -20,6 +20,6 @@ internal static class StringExtensions return value.Trim(); } - public static FormulaValue ToFormulaValue(this string? value) => + public static FormulaValue ToFormula(this string? value) => string.IsNullOrWhiteSpace(value) ? FormulaValue.NewBlank() : FormulaValue.New(value); } diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/TemplateExtensions.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/TemplateExtensions.cs index 9a41554a18..bf13587a38 100644 --- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/TemplateExtensions.cs +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/TemplateExtensions.cs @@ -10,21 +10,21 @@ namespace Microsoft.Agents.Workflows.Declarative.Extensions; internal static class TemplateExtensions { - public static string? Format(this RecalcEngine engine, IEnumerable template) + public static string Format(this RecalcEngine engine, IEnumerable template) { return string.Concat(template.Select(line => engine.Format(line))); } - public static string? Format(this RecalcEngine engine, TemplateLine? line) + public static string Format(this RecalcEngine engine, TemplateLine? line) { return string.Concat(line?.Segments.Select(segment => engine.Format(segment)) ?? [string.Empty]); } - public static string? Format(this RecalcEngine engine, TemplateSegment segment) + public static string Format(this RecalcEngine engine, TemplateSegment segment) { if (segment is TextSegment textSegment) { - return textSegment.Value; + return textSegment.Value ?? string.Empty; } if (segment is ExpressionSegment expressionSegment) diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DeclarativeActionExecutor.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DeclarativeActionExecutor.cs index 2fa157bcca..44e845e63c 100644 --- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DeclarativeActionExecutor.cs +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DeclarativeActionExecutor.cs @@ -1,12 +1,12 @@ // Copyright (c) Microsoft. All rights reserved. using System; -using System.Collections.Generic; -using System.Collections.Immutable; +using System.Collections.Frozen; using System.Diagnostics; using System.Threading; using System.Threading.Tasks; using Microsoft.Agents.Workflows.Declarative.Extensions; +using Microsoft.Agents.Workflows.Declarative.PowerFx; using Microsoft.Bot.ObjectModel; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; @@ -15,29 +15,24 @@ using Microsoft.Shared.Diagnostics; namespace Microsoft.Agents.Workflows.Declarative.Interpreter; -internal sealed record class DeclarativeExecutorResult(string ExecutorId, object? Result = null); - -internal abstract class DeclarativeActionExecutor(TAction model, DeclarativeWorkflowState state) : - WorkflowActionExecutor(model, state) +internal abstract class DeclarativeActionExecutor(TAction model, WorkflowFormulaState state) : + DeclarativeActionExecutor(model, state) where TAction : DialogAction { public new TAction Model => (TAction)base.Model; } -internal abstract class WorkflowActionExecutor : Executor +internal abstract class DeclarativeActionExecutor : Executor { - public const string RootActionId = "(root)"; - - private static readonly ImmutableHashSet s_mutableScopes = - new HashSet - { - VariableScopeNames.Topic, - VariableScopeNames.Global, - }.ToImmutableHashSet(); + private static readonly FrozenSet s_mutableScopes = + [ + VariableScopeNames.Topic, + VariableScopeNames.Global + ]; private string? _parentId; - protected WorkflowActionExecutor(DialogAction model, DeclarativeWorkflowState state) + protected DeclarativeActionExecutor(DialogAction model, WorkflowFormulaState state) : base(model.Id.Value) { if (!model.HasRequiredProperties) @@ -51,16 +46,18 @@ internal abstract class WorkflowActionExecutor : Executor this._parentId ??= this.Model.GetParentId() ?? RootActionId; + public string ParentId => this._parentId ??= this.Model.GetParentId() ?? WorkflowActionVisitor.Steps.Root(); - internal ILogger Logger { get; set; } = NullLogger.Instance; + internal ILogger Logger { get; set; } = NullLogger.Instance; - protected DeclarativeWorkflowState State { get; } + protected WorkflowFormulaState State { get; } protected virtual bool IsDiscreteAction => true; + protected virtual bool EmitResultEvent => true; + /// - public override async ValueTask HandleAsync(DeclarativeExecutorResult message, IWorkflowContext context) + public override async ValueTask HandleAsync(ExecutorResultMessage message, IWorkflowContext context) { if (this.Model.Disabled) { @@ -68,15 +65,18 @@ internal abstract class WorkflowActionExecutor : Executor ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken = default); - protected async ValueTask AssignAsync(PropertyPath targetPath, FormulaValue result, IWorkflowContext context) + /// + /// Restore the state of the executor from a checkpoint. + /// This must be overridden to restore any state that was saved during checkpointing. + /// + protected override ValueTask OnCheckpointRestoredAsync(IWorkflowContext context, CancellationToken cancellation = default) => + this.State.RestoreAsync(context, cancellation); + + protected async ValueTask AssignAsync(PropertyPath? targetPath, FormulaValue result, IWorkflowContext context) { + if (targetPath is null) + { + return; + } + if (!s_mutableScopes.Contains(Throw.IfNull(targetPath.VariableScopeName))) { throw new DeclarativeModelException($"Invalid scope: {targetPath.VariableScopeName}"); } - await this.State.SetAsync(targetPath, result, context).ConfigureAwait(false); + await context.QueueStateUpdateAsync(targetPath, result).ConfigureAwait(false); #if DEBUG string? resultValue = result.Format(); @@ -114,7 +126,7 @@ internal abstract class WorkflowActionExecutor : Executor context.AddEventAsync(new DeclarativeActionInvokeEvent(this.Id, this.Model, priorEventId)); - - protected ValueTask RaiseCompletionEventAsync(IWorkflowContext context) => context.AddEventAsync(new DeclarativeActionCompleteEvent(this.Id, this.Model)); } diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DeclarativeWorkflowContext.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DeclarativeWorkflowContext.cs new file mode 100644 index 0000000000..6c55282e22 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DeclarativeWorkflowContext.cs @@ -0,0 +1,81 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.Agents.Workflows.Declarative.Extensions; +using Microsoft.Agents.Workflows.Declarative.PowerFx; +using Microsoft.Bot.ObjectModel; +using Microsoft.PowerFx.Types; + +namespace Microsoft.Agents.Workflows.Declarative.Interpreter; + +internal sealed class DeclarativeWorkflowContext : IWorkflowContext +{ + public DeclarativeWorkflowContext(IWorkflowContext source, WorkflowFormulaState state) + { + this.Source = source; + this.State = state; + } + + private IWorkflowContext Source { get; } + public WorkflowFormulaState State { get; } + + /// + public ValueTask AddEventAsync(WorkflowEvent workflowEvent) => this.Source.AddEventAsync(workflowEvent); + + /// + public ValueTask QueueClearScopeAsync(string? scopeName = null) + { + this.State.ResetAll(scopeName); + return this.Source.QueueClearScopeAsync(scopeName); + } + + /// + public async ValueTask QueueStateUpdateAsync(string key, T? value, string? scopeName = null) + { + ValueTask task = value switch + { + null => QueueEmptyStateAsync(), + FormulaValue formulaValue => QueueFormulaStateAsync(formulaValue), + DataValue dataValue => QueueDataValueStateAsync(dataValue), + _ => QueueNativeStateAsync(value), + }; + + await task.ConfigureAwait(false); + + ValueTask QueueEmptyStateAsync() + { + this.State.Set(key, FormulaValue.NewBlank(), scopeName); + return this.Source.QueueStateUpdateAsync(key, UnassignedValue.Instance, scopeName); + } + + ValueTask QueueFormulaStateAsync(FormulaValue formulaValue) + { + this.State.Set(key, formulaValue, scopeName); + return this.Source.QueueStateUpdateAsync(key, formulaValue.ToObject(), scopeName); + } + + ValueTask QueueDataValueStateAsync(DataValue dataValue) + { + FormulaValue formulaValue = dataValue.ToFormula(); + this.State.Set(key, formulaValue, scopeName); + return this.Source.QueueStateUpdateAsync(key, formulaValue.ToObject(), scopeName); + } + + ValueTask QueueNativeStateAsync(object? rawValue) + { + FormulaValue formulaValue = rawValue.ToFormula(); + this.State.Set(key, formulaValue, scopeName); + return this.Source.QueueStateUpdateAsync(key, rawValue, scopeName); + } + } + + /// + public ValueTask ReadStateAsync(string key, string? scopeName = null) => this.Source.ReadStateAsync(key, scopeName); + + /// + public ValueTask> ReadStateKeysAsync(string? scopeName = null) => this.Source.ReadStateKeysAsync(scopeName); + + /// + public ValueTask SendMessageAsync(object message, string? targetId = null) => this.Source.SendMessageAsync(message, targetId); +} diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DeclarativeWorkflowExecutor.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DeclarativeWorkflowExecutor.cs index f65bb7f5f6..a912258157 100644 --- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DeclarativeWorkflowExecutor.cs +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DeclarativeWorkflowExecutor.cs @@ -12,16 +12,19 @@ namespace Microsoft.Agents.Workflows.Declarative.Interpreter; /// internal sealed class DeclarativeWorkflowExecutor( string workflowId, - DeclarativeWorkflowState state, + WorkflowFormulaState state, Func inputTransform) : Executor(workflowId) where TInput : notnull { public override async ValueTask HandleAsync(TInput message, IWorkflowContext context) { - ChatMessage input = inputTransform.Invoke(message); - await state.SetLastMessageAsync(context, input).ConfigureAwait(false); + // No state to restore if we're starting from the beginning. + state.SetInitialized(); - await context.SendMessageAsync(new DeclarativeExecutorResult(this.Id)).ConfigureAwait(false); + ChatMessage input = inputTransform.Invoke(message); + state.SetLastMessage(input); + + await context.SendMessageAsync(new ExecutorResultMessage(this.Id)).ConfigureAwait(false); } } diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DeclarativeWorkflowModel.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DeclarativeWorkflowModel.cs index 1d8f511f55..b78c935020 100644 --- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DeclarativeWorkflowModel.cs +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DeclarativeWorkflowModel.cs @@ -2,8 +2,8 @@ using System; using System.Collections.Generic; -using System.Collections.Immutable; using System.Diagnostics; +using System.Linq; namespace Microsoft.Agents.Workflows.Declarative.Interpreter; @@ -43,7 +43,19 @@ internal sealed class DeclarativeWorkflowModel throw new DeclarativeModelException($"Unresolved parent for {executor.Id}: {parentId}."); } - ModelNode stepNode = this.DefineNode(executor, parentNode, executor.GetType(), completionHandler); + ModelNode stepNode = this.DefineNode(executor, parentNode, completionHandler); + + parentNode.Children.Add(stepNode); + } + + public void AddPort(InputPort port, string parentId) + { + if (!this.Nodes.TryGetValue(parentId, out ModelNode? parentNode)) + { + throw new DeclarativeModelException($"Unresolved parent for {port.Id}: {parentId}."); + } + + ModelNode stepNode = this.DefineNode(port, parentNode); parentNode.Children.Add(stepNode); } @@ -77,7 +89,8 @@ internal sealed class DeclarativeWorkflowModel public void ConnectNodes(WorkflowBuilder workflowBuilder) { - foreach (ModelNode node in this.Nodes.Values.ToImmutableArray()) + // Push `Values` into array to avoid modification during iteration. + foreach (ModelNode node in this.Nodes.Values.ToArray()) { if (node.CompletionHandler is not null) { @@ -96,13 +109,32 @@ internal sealed class DeclarativeWorkflowModel Debug.WriteLine($"> CONNECT: {link.Source.Id} => {link.TargetId}{(link.Condition is null ? string.Empty : " (?)")}"); - workflowBuilder.AddEdge(link.Source.Executor, targetNode.Executor, link.Condition); + workflowBuilder.AddEdge(GetExecutorIsh(link.Source), GetExecutorIsh(targetNode), link.Condition); + } + + ExecutorIsh GetExecutorIsh(ModelNode node) + { + if (node.Port is not null) + { + return node.Port; + } + + return node.Executor; } } - private ModelNode DefineNode(Executor executor, ModelNode? parentNode = null, Type? executorType = null, Action? completionHandler = null) + private ModelNode DefineNode(Executor executor, ModelNode? parentNode = null, Action? completionHandler = null) { - ModelNode stepNode = new(executor, parentNode, executorType, completionHandler); + ModelNode stepNode = new(executor, port: null, parentNode, completionHandler); + + this.Nodes.Add(stepNode.Id, stepNode); + + return stepNode; + } + + private ModelNode DefineNode(InputPort port, ModelNode? parentNode = null) + { + ModelNode stepNode = new(executor: null!, port, parentNode); this.Nodes.Add(stepNode.Id, stepNode); @@ -134,13 +166,15 @@ internal sealed class DeclarativeWorkflowModel return null; } - private sealed class ModelNode(Executor executor, ModelNode? parent = null, Type? executorType = null, Action? completionHandler = null) + private sealed class ModelNode(Executor executor, InputPort? port, ModelNode? parent = null, Action? completionHandler = null) { - public string Id => executor.Id; + public string Id => port?.Id ?? executor.Id; public Executor Executor => executor; - public Type? ExecutorType => executorType; + public InputPort? Port => port; + + public Type? ExecutorType => this.Port?.GetType() ?? this.Executor.GetType(); public ModelNode? Parent { get; } = parent; diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DeclarativeWorkflowState.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DeclarativeWorkflowState.cs deleted file mode 100644 index fbbc77c48d..0000000000 --- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DeclarativeWorkflowState.cs +++ /dev/null @@ -1,103 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Agents.Workflows.Declarative.Extensions; -using Microsoft.Agents.Workflows.Declarative.PowerFx; -using Microsoft.Bot.ObjectModel; -using Microsoft.PowerFx; -using Microsoft.PowerFx.Types; -using Microsoft.Shared.Diagnostics; - -namespace Microsoft.Agents.Workflows.Declarative.Interpreter; - -internal sealed class DeclarativeWorkflowState -{ - private static readonly ImmutableHashSet s_mutableScopes = - new HashSet - { - VariableScopeNames.Topic, - VariableScopeNames.Global, - VariableScopeNames.System, - }.ToImmutableHashSet(); - - private readonly RecalcEngine _engine; - private readonly WorkflowScopes _scopes; - private WorkflowExpressionEngine? _expressionEngine; - private int _isInitialized; - - public DeclarativeWorkflowState(RecalcEngine engine, WorkflowScopes? scopes = null) - { - this._scopes = scopes ?? new WorkflowScopes(); - this._engine = engine; - this._scopes.Bind(this._engine); - } - - public WorkflowExpressionEngine ExpressionEngine => this._expressionEngine ??= new WorkflowExpressionEngine(this._engine); - - public void Reset(PropertyPath variablePath) => - this.Reset(Throw.IfNull(variablePath.VariableScopeName), Throw.IfNull(variablePath.VariableName)); - - public void Reset(string scopeName, string? varName = null) - { - if (string.IsNullOrWhiteSpace(varName)) - { - this._scopes.Clear(scopeName); - } - else - { - this._scopes.Reset(varName, scopeName); - } - - this._scopes.Bind(this._engine, scopeName); - } - - public FormulaValue Get(PropertyPath variablePath) => - this.Get(Throw.IfNull(variablePath.VariableScopeName), Throw.IfNull(variablePath.VariableName)); - - public FormulaValue Get(string scope, string varName) => - this._scopes.Get(varName, scope); - - public ValueTask SetAsync(PropertyPath variablePath, FormulaValue value, IWorkflowContext context) => - this.SetAsync(Throw.IfNull(variablePath.VariableScopeName), Throw.IfNull(variablePath.VariableName), value, context); - - public async ValueTask SetAsync(string scopeName, string varName, FormulaValue value, IWorkflowContext context) - { - if (!s_mutableScopes.Contains(scopeName)) - { - throw new DeclarativeModelException($"Invalid scope: {scopeName}"); - } - - this._scopes.Set(varName, value, scopeName); - this._scopes.Bind(this._engine, scopeName); - - await context.QueueStateUpdateAsync(varName, value.ToObject(), scopeName).ConfigureAwait(false); - } - - public string? Format(IEnumerable template) => this._engine.Format(template); - - public string? Format(TemplateLine? line) => this._engine.Format(line); - - public async ValueTask RestoreAsync(IWorkflowContext context, CancellationToken cancellationToken) - { - if (Interlocked.CompareExchange(ref this._isInitialized, 1, 0) == 1) - { - return; - } - - await Task.WhenAll(s_mutableScopes.Select(scopeName => ReadScopeAsync(scopeName).AsTask())).ConfigureAwait(false); - - async ValueTask ReadScopeAsync(string scopeName) - { - HashSet keys = await context.ReadStateKeysAsync(scopeName).ConfigureAwait(false); - foreach (string key in keys) - { - object? value = await context.ReadStateAsync(key, scopeName).ConfigureAwait(false); - this._scopes.Set(key, value.ToFormulaValue(), scopeName); - } - } - } -} diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DelegateActionExecutor.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DelegateActionExecutor.cs index 59afb516ff..efa4f04643 100644 --- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DelegateActionExecutor.cs +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DelegateActionExecutor.cs @@ -1,29 +1,50 @@ // Copyright (c) Microsoft. All rights reserved. +using System.Diagnostics; using System.Threading; using System.Threading.Tasks; +using Microsoft.Agents.Workflows.Declarative.Extensions; +using Microsoft.Agents.Workflows.Declarative.PowerFx; namespace Microsoft.Agents.Workflows.Declarative.Interpreter; -internal delegate ValueTask DelegateAction(IWorkflowContext context, CancellationToken cancellationToken); +internal delegate ValueTask DelegateAction(IWorkflowContext context, TMessage message, CancellationToken cancellationToken) where TMessage : notnull; -internal sealed class DelegateActionExecutor : Executor +internal sealed class DelegateActionExecutor(string actionId, WorkflowFormulaState state, DelegateAction? action = null, bool emitResult = true) + : DelegateActionExecutor(actionId, state, action, emitResult) { - private readonly DelegateAction? _action; + public override ValueTask HandleAsync(ExecutorResultMessage message, IWorkflowContext context) + { + Debug.WriteLine($"RESULT #{this.Id} - {message.Result ?? "(null)"}"); - public DelegateActionExecutor(string actionId, DelegateAction? action = null) + return base.HandleAsync(message, context); + } +} + +internal class DelegateActionExecutor : Executor where TMessage : notnull +{ + private readonly WorkflowFormulaState _state; + private readonly DelegateAction? _action; + private readonly bool _emitResult; + + public DelegateActionExecutor(string actionId, WorkflowFormulaState state, DelegateAction? action = null, bool emitResult = true) : base(actionId) { + this._state = state; this._action = action; + this._emitResult = emitResult; } - public override async ValueTask HandleAsync(DeclarativeExecutorResult message, IWorkflowContext context) + public override async ValueTask HandleAsync(TMessage message, IWorkflowContext context) { if (this._action is not null) { - await this._action.Invoke(context, default).ConfigureAwait(false); + await this._action.Invoke(new DeclarativeWorkflowContext(context, this._state), message, default).ConfigureAwait(false); } - await context.SendMessageAsync(new DeclarativeExecutorResult(this.Id)).ConfigureAwait(false); + if (this._emitResult) + { + await context.SendResultMessageAsync(this.Id).ConfigureAwait(false); + } } } diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DurableProperty.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DurableProperty.cs new file mode 100644 index 0000000000..fbe02be701 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DurableProperty.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Threading.Tasks; + +namespace Microsoft.Agents.Workflows.Declarative.Interpreter; + +internal sealed class DurableProperty(string name) where TValue : struct +{ + public async ValueTask ReadAsync(IWorkflowContext context) + { + TValue? storedValue = await context.ReadStateAsync(name).ConfigureAwait(false); + return storedValue ?? default; + } + + public ValueTask WriteAsync(IWorkflowContext context, TValue value) => + context.QueueStateUpdateAsync(name, value); +} diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/ExecutorResultMessage.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/ExecutorResultMessage.cs new file mode 100644 index 0000000000..99d3a71984 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/ExecutorResultMessage.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft. All rights reserved. + +namespace Microsoft.Agents.Workflows.Declarative.Interpreter; + +internal sealed record class ExecutorResultMessage(string ExecutorId, object? Result = null) +{ + public static ExecutorResultMessage ThrowIfNot(object? message) + { + if (message is not ExecutorResultMessage executorMessage) + { + throw new DeclarativeActionException($"Unexpected message type: {message?.GetType().Name ?? "(null)"} (Expected: {nameof(ExecutorResultMessage)})"); + } + + return executorMessage; + } +} diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/WorkflowActionVisitor.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/WorkflowActionVisitor.cs index 8434308fc1..7b573c69e2 100644 --- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/WorkflowActionVisitor.cs +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/WorkflowActionVisitor.cs @@ -3,22 +3,33 @@ using System; using System.Diagnostics; using System.Linq; +using Microsoft.Agents.Workflows.Declarative.Events; using Microsoft.Agents.Workflows.Declarative.Extensions; using Microsoft.Agents.Workflows.Declarative.ObjectModel; +using Microsoft.Agents.Workflows.Declarative.PowerFx; using Microsoft.Bot.ObjectModel; namespace Microsoft.Agents.Workflows.Declarative.Interpreter; internal sealed class WorkflowActionVisitor : DialogActionVisitor { + private const string DefaultWorkflowId = "workflow"; + + internal static class Steps + { + public static string Root(string? actionId = null) => $"{actionId ?? DefaultWorkflowId}_{nameof(Root)}"; + + public static string Post(string actionId) => $"{actionId}_{nameof(Post)}"; + } + private readonly WorkflowBuilder _workflowBuilder; private readonly DeclarativeWorkflowModel _workflowModel; private readonly DeclarativeWorkflowOptions _workflowOptions; - private readonly DeclarativeWorkflowState _workflowState; + private readonly WorkflowFormulaState _workflowState; public WorkflowActionVisitor( Executor rootAction, - DeclarativeWorkflowState state, + WorkflowFormulaState state, DeclarativeWorkflowOptions options) { this._workflowBuilder = new WorkflowBuilder(rootAction); @@ -47,10 +58,10 @@ internal sealed class WorkflowActionVisitor : DialogActionVisitor // Handle case where root element is its own parent if (item.Id.Equals(parentId)) { - parentId = RootId(parentId); + parentId = Steps.Root(parentId); } - this.ContinueWith(this.CreateStep(item.Id.Value), parentId, condition: null, CompletionHandler); + this.ContinueWith(new DelegateActionExecutor(item.Id.Value, this._workflowState), parentId, condition: null, CompletionHandler); // Complete the action scope. void CompletionHandler() @@ -59,7 +70,7 @@ internal sealed class WorkflowActionVisitor : DialogActionVisitor { string completionId = this.ContinuationFor(item.Id.Value); // End scope this._workflowModel.AddLinkFromPeer(item.Id.Value, completionId); // Connect with final action - this._workflowModel.AddLink(completionId, PostId(parentId)); // Merge with parent scope + this._workflowModel.AddLink(completionId, Steps.Post(parentId)); // Merge with parent scope } } } @@ -73,7 +84,7 @@ internal sealed class WorkflowActionVisitor : DialogActionVisitor { string stepId = ConditionGroupExecutor.Steps.Item(conditionGroup.Model, item); string parentId = GetParentId(item); - this._workflowModel.AddNode(this.CreateStep(stepId), parentId, CompletionHandler); + this._workflowModel.AddNode(new DelegateActionExecutor(stepId, this._workflowState), parentId, CompletionHandler); base.VisitConditionItem(item); @@ -81,7 +92,7 @@ internal sealed class WorkflowActionVisitor : DialogActionVisitor void CompletionHandler() { string completionId = this.ContinuationFor(stepId, conditionGroup.DoneAsync); // End items - this._workflowModel.AddLink(completionId, PostId(conditionGroup.Id)); // Merge with parent scope + this._workflowModel.AddLink(completionId, Steps.Post(conditionGroup.Id)); // Merge with parent scope // Merge link when no action group is defined if (!item.Actions.Any()) @@ -117,6 +128,7 @@ internal sealed class WorkflowActionVisitor : DialogActionVisitor // Create clean start for else action from prior conditions this.RestartAfter(lastConditionItemId, action.Id); } + // Create conditional link for else action string stepId = ConditionGroupExecutor.Steps.Else(item); this._workflowModel.AddLink(action.Id, stepId, (result) => action.IsElse(result)); @@ -127,10 +139,10 @@ internal sealed class WorkflowActionVisitor : DialogActionVisitor { this.Trace(item); - string parentId = GetParentId(item); - this.ContinueWith(this.CreateStep(item.Id.Value), parentId); - this._workflowModel.AddLink(item.Id.Value, item.ActionId.Value); - this.RestartAfter(item.Id.Value, parentId); + GotoExecutor action = new(item, this._workflowState); + this.ContinueWith(action); + this._workflowModel.AddLink(action.Id, item.ActionId.Value); + this.RestartAfter(action.Id, action.ParentId); } protected override void Visit(Foreach item) @@ -140,17 +152,18 @@ internal sealed class WorkflowActionVisitor : DialogActionVisitor ForeachExecutor action = new(item, this._workflowState); string loopId = ForeachExecutor.Steps.Next(action.Id); this.ContinueWith(action, condition: null, CompletionHandler); // Foreach - this.ContinueWith(this.CreateStep(loopId, action.TakeNextAsync), action.Id); // Loop Increment + this.ContinueWith(new DelegateActionExecutor(loopId, this._workflowState, action.TakeNextAsync), action.Id); // Loop Increment string continuationId = this.ContinuationFor(action.Id, action.ParentId); // Action continuation this._workflowModel.AddLink(loopId, continuationId, (_) => !action.HasValue); - DelegateActionExecutor startAction = this.CreateStep(ForeachExecutor.Steps.Start(action.Id)); // Action start - this._workflowModel.AddNode(startAction, action.Id); - this._workflowModel.AddLink(loopId, startAction.Id, (_) => action.HasValue); + + string startId = ForeachExecutor.Steps.Start(action.Id); + this._workflowModel.AddNode(new DelegateActionExecutor(startId, this._workflowState), action.Id); + this._workflowModel.AddLink(loopId, startId, (_) => action.HasValue); void CompletionHandler() { string endActionsId = ForeachExecutor.Steps.End(action.Id); // Loop continuation - this.ContinueWith(this.CreateStep(endActionsId, action.ResetAsync), action.Id); + this.ContinueWith(new DelegateActionExecutor(endActionsId, this._workflowState, action.ResetAsync), action.Id); this._workflowModel.AddLink(endActionsId, loopId); } } @@ -163,8 +176,8 @@ internal sealed class WorkflowActionVisitor : DialogActionVisitor if (loopExecutor is not null) { string parentId = GetParentId(item); - this.ContinueWith(this.CreateStep(item.Id.Value), parentId); - this._workflowModel.AddLink(item.Id.Value, PostId(loopExecutor.Id)); + this.ContinueWith(new DelegateActionExecutor(item.Id.Value, this._workflowState), parentId); + this._workflowModel.AddLink(item.Id.Value, Steps.Post(loopExecutor.Id)); this.RestartAfter(item.Id.Value, parentId); } } @@ -177,7 +190,7 @@ internal sealed class WorkflowActionVisitor : DialogActionVisitor if (loopExecutor is not null) { string parentId = GetParentId(item); - this.ContinueWith(this.CreateStep(item.Id.Value), parentId); + this.ContinueWith(new DelegateActionExecutor(item.Id.Value, this._workflowState), parentId); this._workflowModel.AddLink(item.Id.Value, ForeachExecutor.Steps.Next(loopExecutor.Id)); this.RestartAfter(item.Id.Value, parentId); } @@ -188,15 +201,77 @@ internal sealed class WorkflowActionVisitor : DialogActionVisitor this.Trace(item); string parentId = GetParentId(item); - this.ContinueWith(this.CreateStep(item.Id.Value), parentId); + this.ContinueWith(new DelegateActionExecutor(item.Id.Value, this._workflowState), parentId); this.RestartAfter(item.Id.Value, parentId); } - protected override void Visit(AnswerQuestionWithAI item) + protected override void Visit(Question item) { this.Trace(item); - this.ContinueWith(new AnswerQuestionWithAIExecutor(item, this._workflowOptions.AgentProvider, this._workflowState)); + string parentId = GetParentId(item); + string actionId = item.GetId(); + string postId = Steps.Post(actionId); + + QuestionExecutor questionExecutor = new(item, this._workflowState); + this.ContinueWith(questionExecutor); + this._workflowModel.AddLink(actionId, postId, message => questionExecutor.IsComplete(message)); + + string prepareId = QuestionExecutor.Steps.Prepare(actionId); + this.ContinueWith(new DelegateActionExecutor(prepareId, this._workflowState, questionExecutor.PrepareResponseAsync, emitResult: false), parentId, message => !questionExecutor.IsComplete(message)); + + string inputId = QuestionExecutor.Steps.Input(actionId); + InputPort inputPort = InputPort.Create(inputId); + this._workflowModel.AddPort(inputPort, parentId); + this._workflowModel.AddLinkFromPeer(parentId, inputId); + + string captureId = QuestionExecutor.Steps.Capture(actionId); + this.ContinueWith(new DelegateActionExecutor(captureId, this._workflowState, questionExecutor.CaptureResponseAsync, emitResult: false), parentId); + + this.ContinueWith(new DelegateActionExecutor(postId, this._workflowState, questionExecutor.CompleteAsync), parentId, message => questionExecutor.IsComplete(message)); + this._workflowModel.AddLink(captureId, prepareId, message => !questionExecutor.IsComplete(message)); + } + + protected override void Visit(CreateConversation item) + { + this.Trace(item); + + this.ContinueWith(new CreateConversationExecutor(item, this._workflowOptions.AgentProvider, this._workflowState)); + } + + protected override void Visit(AddConversationMessage item) + { + this.Trace(item); + + this.ContinueWith(new AddConversationMessageExecutor(item, this._workflowOptions.AgentProvider, this._workflowState)); + } + + protected override void Visit(CopyConversationMessages item) + { + this.Trace(item); + + this.ContinueWith(new CopyConversationMessagesExecutor(item, this._workflowOptions.AgentProvider, this._workflowState)); + } + + protected override void Visit(InvokeAzureAgent item) + { + this.Trace(item); + + this.ContinueWith(new InvokeAzureAgentExecutor(item, this._workflowOptions.AgentProvider, this._workflowState)); + } + + protected override void Visit(RetrieveConversationMessage item) + { + this.Trace(item); + + this.ContinueWith(new RetrieveConversationMessageExecutor(item, this._workflowOptions.AgentProvider, this._workflowState)); + } + + protected override void Visit(RetrieveConversationMessages item) + { + this.Trace(item); + + this.ContinueWith(new RetrieveConversationMessagesExecutor(item, this._workflowOptions.AgentProvider, this._workflowState)); } protected override void Visit(SetVariable item) @@ -206,6 +281,13 @@ internal sealed class WorkflowActionVisitor : DialogActionVisitor this.ContinueWith(new SetVariableExecutor(item, this._workflowState)); } + protected override void Visit(SetMultipleVariables item) + { + this.Trace(item); + + this.ContinueWith(new SetMultipleVariablesExecutor(item, this._workflowState)); + } + protected override void Visit(SetTextVariable item) { this.Trace(item); @@ -257,6 +339,11 @@ internal sealed class WorkflowActionVisitor : DialogActionVisitor #region Not supported + protected override void Visit(AnswerQuestionWithAI item) + { + this.NotSupported(item); + } + protected override void Visit(DeleteActivity item) { this.NotSupported(item); @@ -317,11 +404,6 @@ internal sealed class WorkflowActionVisitor : DialogActionVisitor this.NotSupported(item); } - protected override void Visit(Question item) - { - this.NotSupported(item); - } - protected override void Visit(CSATQuestion item) { this.NotSupported(item); @@ -435,7 +517,7 @@ internal sealed class WorkflowActionVisitor : DialogActionVisitor #endregion private void ContinueWith( - WorkflowActionExecutor executor, + DeclarativeActionExecutor executor, Func? condition = null, Action? completionHandler = null) { @@ -453,32 +535,21 @@ internal sealed class WorkflowActionVisitor : DialogActionVisitor this._workflowModel.AddLinkFromPeer(parentId, executor.Id, condition); } - public static string RootId(string? actionId) => $"root_{actionId ?? "workflow"}"; + private string ContinuationFor(string parentId, DelegateAction? stepAction = null) => this.ContinuationFor(parentId, parentId, stepAction); - private static string PostId(string actionId) => $"{actionId}_Post"; - - private static string GetParentId(BotElement item) => - item.GetParentId() ?? - throw new DeclarativeModelException($"Missing parent ID for action element: {item.GetId()} [{item.GetType().Name}]."); - - private string ContinuationFor(string parentId, DelegateAction? stepAction = null) => this.ContinuationFor(parentId, parentId, stepAction); - - private string ContinuationFor(string actionId, string parentId, DelegateAction? stepAction = null) + private string ContinuationFor(string actionId, string parentId, DelegateAction? stepAction = null) { - actionId = PostId(actionId); - this._workflowModel.AddNode(this.CreateStep(actionId, stepAction), parentId); + actionId = Steps.Post(actionId); + this._workflowModel.AddNode(new DelegateActionExecutor(actionId, this._workflowState, stepAction), parentId); return actionId; } private void RestartAfter(string actionId, string parentId) => - this._workflowModel.AddNode(this.CreateStep($"{actionId}_Continue"), parentId); + this._workflowModel.AddNode(new DelegateActionExecutor($"{actionId}_Continue", this._workflowState), parentId); - private DelegateActionExecutor CreateStep(string actionId, DelegateAction? stepAction = null) - { - DelegateActionExecutor stepExecutor = new(actionId, stepAction); - - return stepExecutor; - } + private static string GetParentId(BotElement item) => + item.GetParentId() ?? + throw new DeclarativeModelException($"Missing parent ID for action element: {item.GetId()} [{item.GetType().Name}]."); private void NotSupported(DialogAction item) { @@ -496,7 +567,7 @@ internal sealed class WorkflowActionVisitor : DialogActionVisitor string? parentId = item.GetParentId(); if (item.Id.Equals(parentId ?? string.Empty)) { - parentId = RootId(parentId); + parentId = Steps.Root(parentId); } Debug.WriteLine($"> VISIT: {new string('\t', this._workflowModel.GetDepth(parentId))}{FormatItem(item)} => {FormatParent(item)}"); } diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/WorkflowElementWalker.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/WorkflowElementWalker.cs index 0769786420..26f37f9102 100644 --- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/WorkflowElementWalker.cs +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/WorkflowElementWalker.cs @@ -6,16 +6,13 @@ namespace Microsoft.Agents.Workflows.Declarative.Interpreter; internal sealed class WorkflowElementWalker : BotElementWalker { - private readonly WorkflowActionVisitor _visitor; + private readonly DialogActionVisitor _visitor; - public WorkflowElementWalker(BotElement rootElement, WorkflowActionVisitor visitor) + public WorkflowElementWalker(DialogActionVisitor visitor) { this._visitor = visitor; - this.Visit(rootElement); } - public Workflow GetWorkflow() => this._visitor.Complete(); - public override bool DefaultVisit(BotElement definition) { if (definition is DialogAction action) diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Microsoft.Agents.Workflows.Declarative.csproj b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Microsoft.Agents.Workflows.Declarative.csproj index 634cd93836..f143816c5a 100644 --- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Microsoft.Agents.Workflows.Declarative.csproj +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Microsoft.Agents.Workflows.Declarative.csproj @@ -27,6 +27,7 @@ + diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/AddConversationMessageExecutor.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/AddConversationMessageExecutor.cs new file mode 100644 index 0000000000..558d58ff2c --- /dev/null +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/AddConversationMessageExecutor.cs @@ -0,0 +1,67 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Agents.Workflows.Declarative.Extensions; +using Microsoft.Agents.Workflows.Declarative.Interpreter; +using Microsoft.Agents.Workflows.Declarative.PowerFx; +using Microsoft.Bot.ObjectModel; +using Microsoft.Extensions.AI; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Agents.Workflows.Declarative.ObjectModel; + +internal sealed class AddConversationMessageExecutor(AddConversationMessage model, WorkflowAgentProvider agentProvider, WorkflowFormulaState state) : + DeclarativeActionExecutor(model, state) +{ + protected override async ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken) + { + StringExpression conversationExpression = Throw.IfNull(this.Model.ConversationId, $"{nameof(this.Model)}.{nameof(this.Model.ConversationId)}"); + string conversationId = this.State.Evaluator.GetValue(conversationExpression).Value; + + ChatMessage newMessage = new(this.GetRole(), [.. this.GetContent()]) { AdditionalProperties = this.GetMetadata() }; + + await agentProvider.CreateMessageAsync(conversationId, newMessage, cancellationToken).ConfigureAwait(false); + + await this.AssignAsync(this.Model.Message?.Path, newMessage.ToRecord(), context).ConfigureAwait(false); + + return default; + } + + private IEnumerable GetContent() + { + foreach (AddConversationMessageContent content in this.Model.Content) + { + AIContent? messageContent = content.Type.Value.ToContent(this.State.Engine.Format(content.Value)); + if (messageContent is not null) + { + yield return messageContent; + } + } + } + + private ChatRole GetRole() + { + if (this.Model.Role is null) + { + return ChatRole.User; + } + + AgentMessageRoleWrapper roleWrapper = this.State.Evaluator.GetValue(this.Model.Role).Value; + + return roleWrapper.Value.ToChatRole(); + } + + private AdditionalPropertiesDictionary? GetMetadata() + { + if (this.Model.Metadata is null) + { + return null; + } + + RecordDataValue? metadataValue = this.State.Evaluator.GetValue(this.Model.Metadata).Value; + + return metadataValue.ToMetadata(); + } +} diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/AnswerQuestionWithAIExecutor.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/AnswerQuestionWithAIExecutor.cs deleted file mode 100644 index 15806e38ac..0000000000 --- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/AnswerQuestionWithAIExecutor.cs +++ /dev/null @@ -1,126 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Agents.Workflows.Declarative.Extensions; -using Microsoft.Agents.Workflows.Declarative.Interpreter; -using Microsoft.Agents.Workflows.Declarative.PowerFx; -using Microsoft.Bot.ObjectModel; -using Microsoft.Bot.ObjectModel.Abstractions; -using Microsoft.Extensions.AI; -using Microsoft.Extensions.AI.Agents; -using Microsoft.PowerFx.Types; -using Microsoft.Shared.Diagnostics; - -namespace Microsoft.Agents.Workflows.Declarative.ObjectModel; - -internal sealed class AnswerQuestionWithAIExecutor(AnswerQuestionWithAI model, WorkflowAgentProvider agentProvider, DeclarativeWorkflowState state) - : DeclarativeActionExecutor(model, state) -{ - protected override async ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken) - { - StringExpression userInputExpression = Throw.IfNull(this.Model.UserInput, $"{nameof(this.Model)}.{nameof(this.Model.UserInput)}"); - - string agentInstructions = this.State.Format(this.Model.AdditionalInstructions) ?? string.Empty; - // ISSUE #485 - Agent identifier embedded in instructions until updated OM is available. - string agentId; - string? additionalInstructions = null; - int delimiterIndex = agentInstructions.IndexOf(','); - if (delimiterIndex < 0) - { - agentId = agentInstructions.Trim(); - } - else - { - agentId = agentInstructions.Substring(0, delimiterIndex).Trim(); - additionalInstructions = agentInstructions.Substring(delimiterIndex + 1).Trim(); - } - - AIAgent agent = await agentProvider.GetAgentAsync(agentId, cancellationToken).ConfigureAwait(false); - - string? userInput = null; - if (this.Model.UserInput is not null) - { - EvaluationResult expressionResult = this.State.ExpressionEngine.GetValue(userInputExpression); - userInput = expressionResult.Value; - } - - ChatClientAgentRunOptions options = - new( - new ChatOptions() - { - Instructions = additionalInstructions, - }); - - FormulaValue conversationValue = - this.Model.AutoSend ? // ISSUE #485: Conversation implicitly managed until updated OM is available. - this.State.GetConversationId() : - this.State.GetInternalConversationId(); - - string? conversationId = null; - if (conversationValue is StringValue stringValue) - { - await AssignConversationId(stringValue.Value).ConfigureAwait(false); - } - - AgentThread agentThread = new() { ConversationId = conversationId }; - IAsyncEnumerable agentUpdates = - !string.IsNullOrWhiteSpace(userInput) ? - agent.RunStreamingAsync(userInput, agentThread, options, cancellationToken) : - agent.RunStreamingAsync(agentThread, options, cancellationToken); - - string? messageId = null; - List agentResponseUpdates = new(0x400); - await foreach (AgentRunResponseUpdate update in agentUpdates.ConfigureAwait(false)) - { - agentResponseUpdates.Add(update); - messageId ??= update.MessageId; - await AssignConversationId(((ChatResponseUpdate?)update.RawRepresentation)?.ConversationId).ConfigureAwait(false); - if (this.Model.AutoSend) - { - await context.AddEventAsync(new AgentRunUpdateEvent(this.Id, update)).ConfigureAwait(false); - } - } - - AgentRunResponse agentResponse = agentResponseUpdates.ToAgentRunResponse(); - - ChatMessage response = agentResponse.Messages.Last(); - await this.State.SetLastMessageAsync(context, response).ConfigureAwait(false); - if (this.Model.AutoSend) - { - await context.AddEventAsync(new AgentRunResponseEvent(this.Id, agentResponse)).ConfigureAwait(false); - } - - // Assign conversation ID if it wasn't already assigned. - if (conversationValue is not StringValue && conversationId is not null) - { - if (this.Model.AutoSend) // ISSUE #485: Conversation implicitly managed until updated OM is available. - { - await this.State.SetConversationIdAsync(context, conversationId).ConfigureAwait(false); - } - else - { - await this.State.SetInternalConversationIdAsync(context, conversationId).ConfigureAwait(false); - } - } - - PropertyPath? variablePath = this.Model.Variable?.Path; - if (variablePath is not null) - { - await this.AssignAsync(variablePath, response.ToRecord(), context).ConfigureAwait(false); - } - - return default; - - async ValueTask AssignConversationId(string? assignValue) - { - if (assignValue != null && conversationId == null) - { - conversationId = assignValue; - await context.AddEventAsync(new ConversationUpdateEvent(this.Id, conversationId)).ConfigureAwait(false); - } - } - } -} diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/ClearAllVariablesExecutor.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/ClearAllVariablesExecutor.cs index 080a985db4..f0fee71ade 100644 --- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/ClearAllVariablesExecutor.cs +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/ClearAllVariablesExecutor.cs @@ -10,19 +10,19 @@ using Microsoft.Bot.ObjectModel.Abstractions; namespace Microsoft.Agents.Workflows.Declarative.ObjectModel; -internal sealed class ClearAllVariablesExecutor(ClearAllVariables model, DeclarativeWorkflowState state) +internal sealed class ClearAllVariablesExecutor(ClearAllVariables model, WorkflowFormulaState state) : DeclarativeActionExecutor(model, state) { protected override ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken) { - EvaluationResult variablesResult = this.State.ExpressionEngine.GetValue(this.Model.Variables); + EvaluationResult variablesResult = this.State.Evaluator.GetValue(this.Model.Variables); variablesResult.Value.Handle(new ScopeHandler(this.Id, this.State)); return default; } - private sealed class ScopeHandler(string executorId, DeclarativeWorkflowState state) : IEnumVariablesToClearHandler + private sealed class ScopeHandler(string executorId, WorkflowFormulaState state) : IEnumVariablesToClearHandler { public void HandleAllGlobalVariables() { @@ -36,7 +36,7 @@ internal sealed class ClearAllVariablesExecutor(ClearAllVariables model, Declara public void HandleConversationScopedVariables() { - this.ClearAll(WorkflowScopes.DefaultScopeName); + this.ClearAll(WorkflowFormulaState.DefaultScopeName); } public void HandleUnknownValue() @@ -51,7 +51,7 @@ internal sealed class ClearAllVariablesExecutor(ClearAllVariables model, Declara private void ClearAll(string scope) { - state.Reset(scope); + state.ResetAll(scope); Debug.WriteLine( $""" STATE: {this.GetType().Name} [{executorId}] diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/ConditionGroupExecutor.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/ConditionGroupExecutor.cs index b8d1662210..01b88ec6d6 100644 --- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/ConditionGroupExecutor.cs +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/ConditionGroupExecutor.cs @@ -3,7 +3,9 @@ using System; using System.Threading; using System.Threading.Tasks; +using Microsoft.Agents.Workflows.Declarative.Extensions; using Microsoft.Agents.Workflows.Declarative.Interpreter; +using Microsoft.Agents.Workflows.Declarative.PowerFx; using Microsoft.Bot.ObjectModel; using Microsoft.Bot.ObjectModel.Abstractions; @@ -26,31 +28,23 @@ internal sealed class ConditionGroupExecutor : DeclarativeActionExecutor model.ElseActions.Id.Value ?? $"{model.Id}_Else"; } - public ConditionGroupExecutor(ConditionGroup model, DeclarativeWorkflowState state) + public ConditionGroupExecutor(ConditionGroup model, WorkflowFormulaState state) : base(model, state) { } protected override bool IsDiscreteAction => false; - public bool IsMatch(ConditionItem conditionItem, object? result) + public bool IsMatch(ConditionItem conditionItem, object? message) { - if (result is not DeclarativeExecutorResult message) - { - return false; - } - - return string.Equals(Steps.Item(this.Model, conditionItem), message.Result as string, StringComparison.Ordinal); + ExecutorResultMessage executorMessage = ExecutorResultMessage.ThrowIfNot(message); + return string.Equals(Steps.Item(this.Model, conditionItem), executorMessage.Result as string, StringComparison.Ordinal); } - public bool IsElse(object? result) + public bool IsElse(object? message) { - if (result is not DeclarativeExecutorResult message) - { - return false; - } - - return string.Equals(Steps.Else(this.Model), message.Result as string, StringComparison.Ordinal); + ExecutorResultMessage executorMessage = ExecutorResultMessage.ThrowIfNot(message); + return string.Equals(Steps.Else(this.Model), executorMessage.Result as string, StringComparison.Ordinal); } #pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously @@ -65,7 +59,7 @@ internal sealed class ConditionGroupExecutor : DeclarativeActionExecutor expressionResult = this.State.ExpressionEngine.GetValue(conditionItem.Condition); + EvaluationResult expressionResult = this.State.Evaluator.GetValue(conditionItem.Condition); if (expressionResult.Value) { return Steps.Item(this.Model, conditionItem); @@ -75,8 +69,8 @@ internal sealed class ConditionGroupExecutor : DeclarativeActionExecutor(model, state) +{ + protected override async ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken) + { + string conversationId = this.State.Evaluator.GetValue(Throw.IfNull(this.Model.ConversationId, $"{nameof(this.Model)}.{nameof(this.Model.ConversationId)}")).Value; + DataValue? inputMessages = this.GetInputMessages(); + + if (inputMessages is not null) + { + foreach (ChatMessage message in inputMessages.ToChatMessages()) + { + await agentProvider.CreateMessageAsync(conversationId, message, cancellationToken).ConfigureAwait(false); + } + } + + return default; + } + + private DataValue? GetInputMessages() + { + DataValue? messages = null; + + if (this.Model.Messages is not null) + { + EvaluationResult expressionResult = this.State.Evaluator.GetValue(this.Model.Messages); + messages = expressionResult.Value; + } + + return messages; + } +} diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/CreateConversationExecutor.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/CreateConversationExecutor.cs new file mode 100644 index 0000000000..042603b69f --- /dev/null +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/CreateConversationExecutor.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Agents.Workflows.Declarative.Interpreter; +using Microsoft.Agents.Workflows.Declarative.PowerFx; +using Microsoft.Bot.ObjectModel; +using Microsoft.PowerFx.Types; + +namespace Microsoft.Agents.Workflows.Declarative.ObjectModel; + +internal sealed class CreateConversationExecutor(CreateConversation model, WorkflowAgentProvider agentProvider, WorkflowFormulaState state) : + DeclarativeActionExecutor(model, state) +{ + protected override async ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken) + { + string conversationId = await agentProvider.CreateConversationAsync(cancellationToken).ConfigureAwait(false); + await this.AssignAsync(this.Model.ConversationId?.Path, FormulaValue.New(conversationId), context).ConfigureAwait(false); + + return default; + } +} diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/EditTableExecutor.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/EditTableExecutor.cs index 0c09b270fe..ade179dbdb 100644 --- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/EditTableExecutor.cs +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/EditTableExecutor.cs @@ -6,6 +6,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Agents.Workflows.Declarative.Extensions; using Microsoft.Agents.Workflows.Declarative.Interpreter; +using Microsoft.Agents.Workflows.Declarative.PowerFx; using Microsoft.Bot.ObjectModel; using Microsoft.Bot.ObjectModel.Abstractions; using Microsoft.PowerFx.Types; @@ -13,7 +14,7 @@ using Microsoft.Shared.Diagnostics; namespace Microsoft.Agents.Workflows.Declarative.ObjectModel; -internal sealed class EditTableExecutor(EditTable model, DeclarativeWorkflowState state) : DeclarativeActionExecutor(model, state) +internal sealed class EditTableExecutor(EditTable model, WorkflowFormulaState state) : DeclarativeActionExecutor(model, state) { protected override async ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken) { @@ -22,7 +23,7 @@ internal sealed class EditTableExecutor(EditTable model, DeclarativeWorkflowStat FormulaValue table = this.State.Get(variablePath); if (table is not TableValue tableValue) { - throw this.Exception($"Require '{variablePath.Format()}' to be a table, not: '{table.GetType().Name}'."); + throw this.Exception($"Require '{variablePath}' to be a table, not: '{table.GetType().Name}'."); } TableChangeType changeType = this.Model.ChangeType.Value; @@ -30,14 +31,14 @@ internal sealed class EditTableExecutor(EditTable model, DeclarativeWorkflowStat { case TableChangeType.Add: ValueExpression addItemValue = Throw.IfNull(this.Model.Value, $"{nameof(this.Model)}.{nameof(this.Model.Value)}"); - EvaluationResult addResult = this.State.ExpressionEngine.GetValue(addItemValue); - RecordValue newRecord = BuildRecord(tableValue.Type.ToRecord(), addResult.Value.ToFormulaValue()); + EvaluationResult addResult = this.State.Evaluator.GetValue(addItemValue); + RecordValue newRecord = BuildRecord(tableValue.Type.ToRecord(), addResult.Value.ToFormula()); await tableValue.AppendAsync(newRecord, cancellationToken).ConfigureAwait(false); await this.AssignAsync(variablePath, newRecord, context).ConfigureAwait(false); break; case TableChangeType.Remove: ValueExpression removeItemValue = Throw.IfNull(this.Model.Value, $"{nameof(this.Model)}.{nameof(this.Model.Value)}"); - EvaluationResult removeResult = this.State.ExpressionEngine.GetValue(removeItemValue); + EvaluationResult removeResult = this.State.Evaluator.GetValue(removeItemValue); if (removeResult.Value is TableDataValue removeItemTable) { await tableValue.RemoveAsync(removeItemTable?.Values.Select(row => row.ToRecordValue()), all: true, cancellationToken).ConfigureAwait(false); diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/EditTableV2Executor.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/EditTableV2Executor.cs index c18e87dce6..3ad45a32c2 100644 --- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/EditTableV2Executor.cs +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/EditTableV2Executor.cs @@ -6,6 +6,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Agents.Workflows.Declarative.Extensions; using Microsoft.Agents.Workflows.Declarative.Interpreter; +using Microsoft.Agents.Workflows.Declarative.PowerFx; using Microsoft.Bot.ObjectModel; using Microsoft.Bot.ObjectModel.Abstractions; using Microsoft.PowerFx.Types; @@ -13,7 +14,7 @@ using Microsoft.Shared.Diagnostics; namespace Microsoft.Agents.Workflows.Declarative.ObjectModel; -internal sealed class EditTableV2Executor(EditTableV2 model, DeclarativeWorkflowState state) : DeclarativeActionExecutor(model, state) +internal sealed class EditTableV2Executor(EditTableV2 model, WorkflowFormulaState state) : DeclarativeActionExecutor(model, state) { protected override async ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken) { @@ -22,15 +23,15 @@ internal sealed class EditTableV2Executor(EditTableV2 model, DeclarativeWorkflow FormulaValue table = this.State.Get(variablePath); if (table is not TableValue tableValue) { - throw this.Exception($"Require '{variablePath.Format()}' to be a table, not: '{table.GetType().Name}'."); + throw this.Exception($"Require '{variablePath}' to be a table, not: '{table.GetType().Name}'."); } EditTableOperation? changeType = this.Model.ChangeType; if (changeType is AddItemOperation addItemOperation) { ValueExpression addItemValue = Throw.IfNull(addItemOperation.Value, $"{nameof(this.Model)}.{nameof(this.Model.ChangeType)}"); - EvaluationResult expressionResult = this.State.ExpressionEngine.GetValue(addItemValue); - RecordValue newRecord = BuildRecord(tableValue.Type.ToRecord(), expressionResult.Value.ToFormulaValue()); + EvaluationResult expressionResult = this.State.Evaluator.GetValue(addItemValue); + RecordValue newRecord = BuildRecord(tableValue.Type.ToRecord(), expressionResult.Value.ToFormula()); await tableValue.AppendAsync(newRecord, cancellationToken).ConfigureAwait(false); await this.AssignAsync(variablePath, newRecord, context).ConfigureAwait(false); } @@ -42,8 +43,8 @@ internal sealed class EditTableV2Executor(EditTableV2 model, DeclarativeWorkflow else if (changeType is RemoveItemOperation removeItemOperation) { ValueExpression removeItemValue = Throw.IfNull(removeItemOperation.Value, $"{nameof(this.Model)}.{nameof(this.Model.ChangeType)}"); - EvaluationResult expressionResult = this.State.ExpressionEngine.GetValue(removeItemValue); - if (expressionResult.Value.ToFormulaValue() is TableValue removeItemTable) + EvaluationResult expressionResult = this.State.Evaluator.GetValue(removeItemValue); + if (expressionResult.Value.ToFormula() is TableValue removeItemTable) { await tableValue.RemoveAsync(removeItemTable?.Rows.Select(row => row.Value), all: true, cancellationToken).ConfigureAwait(false); await this.AssignAsync(variablePath, FormulaValue.NewBlank(), context).ConfigureAwait(false); diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/ForeachExecutor.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/ForeachExecutor.cs index 8b2dcdc135..91d2abf2f4 100644 --- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/ForeachExecutor.cs +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/ForeachExecutor.cs @@ -5,6 +5,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Agents.Workflows.Declarative.Extensions; using Microsoft.Agents.Workflows.Declarative.Interpreter; +using Microsoft.Agents.Workflows.Declarative.PowerFx; using Microsoft.Bot.ObjectModel; using Microsoft.Bot.ObjectModel.Abstractions; using Microsoft.PowerFx.Types; @@ -24,7 +25,7 @@ internal sealed class ForeachExecutor : DeclarativeActionExecutor private int _index; private FormulaValue[] _values; - public ForeachExecutor(Foreach model, DeclarativeWorkflowState state) + public ForeachExecutor(Foreach model, WorkflowFormulaState state) : base(model, state) { this._values = []; @@ -45,40 +46,40 @@ internal sealed class ForeachExecutor : DeclarativeActionExecutor } else { - EvaluationResult expressionResult = this.State.ExpressionEngine.GetValue(this.Model.Items); + EvaluationResult expressionResult = this.State.Evaluator.GetValue(this.Model.Items); if (expressionResult.Value is TableDataValue tableValue) { - this._values = [.. tableValue.Values.Select(value => value.Properties.Values.First().ToFormulaValue())]; + this._values = [.. tableValue.Values.Select(value => value.Properties.Values.First().ToFormula())]; } else { - this._values = [expressionResult.Value.ToFormulaValue()]; + this._values = [expressionResult.Value.ToFormula()]; } } - await this.ResetAsync(context, cancellationToken).ConfigureAwait(false); + await this.ResetAsync(context, null, cancellationToken).ConfigureAwait(false); return default; } - public async ValueTask TakeNextAsync(IWorkflowContext context, CancellationToken cancellationToken) + public async ValueTask TakeNextAsync(IWorkflowContext context, object? _, CancellationToken cancellationToken) { if (this.HasValue = this._index < this._values.Length) { FormulaValue value = this._values[this._index]; - await this.State.SetAsync(Throw.IfNull(this.Model.Value), value, context).ConfigureAwait(false); + await context.QueueStateUpdateAsync(Throw.IfNull(this.Model.Value), value).ConfigureAwait(false); if (this.Model.Index is not null) { - await this.State.SetAsync(this.Model.Index.Path, FormulaValue.New(this._index), context).ConfigureAwait(false); + await context.QueueStateUpdateAsync(this.Model.Index.Path, FormulaValue.New(this._index)).ConfigureAwait(false); } this._index++; } } - public async ValueTask ResetAsync(IWorkflowContext context, CancellationToken cancellationToken) + public async ValueTask ResetAsync(IWorkflowContext context, object? _, CancellationToken cancellationToken) { try { @@ -90,7 +91,7 @@ internal sealed class ForeachExecutor : DeclarativeActionExecutor } finally { - await this.RaiseCompletionEventAsync(context).ConfigureAwait(false); + await context.RaiseCompletionEventAsync(this.Model).ConfigureAwait(false); } } } diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/GotoExecutor.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/GotoExecutor.cs new file mode 100644 index 0000000000..a7ba9bb731 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/GotoExecutor.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Agents.Workflows.Declarative.Interpreter; +using Microsoft.Agents.Workflows.Declarative.PowerFx; +using Microsoft.Bot.ObjectModel; + +namespace Microsoft.Agents.Workflows.Declarative.ObjectModel; + +internal sealed class GotoExecutor(GotoAction model, WorkflowFormulaState state) : + DeclarativeActionExecutor(model, state) +{ + protected override ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken) + { + // No action needed - the edge will be followed automatically + return default; + } +} diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/InvokeAzureAgentExecutor.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/InvokeAzureAgentExecutor.cs new file mode 100644 index 0000000000..689c7d6e1f --- /dev/null +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/InvokeAzureAgentExecutor.cs @@ -0,0 +1,138 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Agents.Workflows.Declarative.Extensions; +using Microsoft.Agents.Workflows.Declarative.Interpreter; +using Microsoft.Agents.Workflows.Declarative.PowerFx; +using Microsoft.Bot.ObjectModel; +using Microsoft.Bot.ObjectModel.Abstractions; +using Microsoft.Extensions.AI; +using Microsoft.Extensions.AI.Agents; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Agents.Workflows.Declarative.ObjectModel; + +internal sealed class InvokeAzureAgentExecutor(InvokeAzureAgent model, WorkflowAgentProvider agentProvider, WorkflowFormulaState state) : + DeclarativeActionExecutor(model, state) +{ + private AzureAgentUsage AgentUsage => Throw.IfNull(this.Model.Agent, $"{nameof(this.Model)}.{nameof(this.Model.Agent)}"); + private AzureAgentInput? AgentInput => this.Model.Input; + private AzureAgentOutput? AgentOutput => this.Model.Output; + + protected override async ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken) + { + string? conversationId = this.GetConversationId(); + string agentName = this.GetAgentName(); + string? additionalInstructions = this.GetAdditionalInstructions(); + bool autoSend = this.GetAutoSendValue(); + DataValue? inputMessages = this.GetInputMessages(); + + AgentRunResponse agentResponse = InvokeAgentAsync().ToEnumerable().ToAgentRunResponse(); + + if (autoSend) + { + await context.AddEventAsync(new AgentRunResponseEvent(this.Id, agentResponse)).ConfigureAwait(false); + } + + ChatMessage response = agentResponse.Messages.Last(); + await this.AssignAsync(this.AgentOutput?.Messages?.Path, response.ToRecord(), context).ConfigureAwait(false); + + return default; + + async IAsyncEnumerable InvokeAgentAsync() + { + AIAgent agent = await agentProvider.GetAgentAsync(agentName, cancellationToken).ConfigureAwait(false); + + ChatClientAgentRunOptions options = + new( + new ChatOptions() + { + Instructions = additionalInstructions, + }); + + AgentThread agentThread = new() { ConversationId = conversationId }; + IAsyncEnumerable agentUpdates = + inputMessages is not null ? + agent.RunStreamingAsync([.. inputMessages.ToChatMessages()], agentThread, options, cancellationToken) : + agent.RunStreamingAsync(agentThread, options, cancellationToken); + + await foreach (AgentRunResponseUpdate update in agentUpdates.ConfigureAwait(false)) + { + await AssignConversationId(((ChatResponseUpdate?)update.RawRepresentation)?.ConversationId).ConfigureAwait(false); + + if (autoSend) + { + await context.AddEventAsync(new AgentRunUpdateEvent(this.Id, update)).ConfigureAwait(false); + } + + yield return update; + } + } + + async ValueTask AssignConversationId(string? assignValue) + { + if (assignValue is not null && conversationId is null) + { + conversationId = assignValue; + this.State.SetConversationId(conversationId); + await context.AddEventAsync(new ConversationUpdateEvent(conversationId)).ConfigureAwait(false); + } + } + } + + private DataValue? GetInputMessages() + { + DataValue? userInput = null; + if (this.AgentInput?.Messages is not null) + { + EvaluationResult expressionResult = this.State.Evaluator.GetValue(this.AgentInput.Messages); + userInput = expressionResult.Value; + } + + return userInput; + } + + private string? GetConversationId() + { + if (this.Model.ConversationId is null) + { + return null; + } + + EvaluationResult conversationIdResult = this.State.Evaluator.GetValue(this.Model.ConversationId); + return conversationIdResult.Value.Length == 0 ? null : conversationIdResult.Value; + } + + private string GetAgentName() => + this.State.Evaluator.GetValue( + Throw.IfNull( + this.AgentUsage.Name, + $"{nameof(this.Model)}.{nameof(this.Model.Agent)}.{nameof(this.Model.Agent.Name)}")).Value; + + private string? GetAdditionalInstructions() + { + string? additionalInstructions = null; + + if (this.AgentInput?.AdditionalInstructions is not null) + { + additionalInstructions = this.State.Engine.Format(this.AgentInput.AdditionalInstructions); + } + + return additionalInstructions; + } + + private bool GetAutoSendValue() + { + if (this.AgentOutput?.AutoSend is null) + { + return true; + } + + EvaluationResult autoSendResult = this.State.Evaluator.GetValue(this.AgentOutput.AutoSend); + + return autoSendResult.Value; + } +} diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/ParseValueExecutor.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/ParseValueExecutor.cs index 377c472a20..26318a81bc 100644 --- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/ParseValueExecutor.cs +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/ParseValueExecutor.cs @@ -7,6 +7,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Agents.Workflows.Declarative.Extensions; using Microsoft.Agents.Workflows.Declarative.Interpreter; +using Microsoft.Agents.Workflows.Declarative.PowerFx; using Microsoft.Bot.ObjectModel; using Microsoft.Bot.ObjectModel.Abstractions; using Microsoft.PowerFx.Types; @@ -14,7 +15,7 @@ using Microsoft.Shared.Diagnostics; namespace Microsoft.Agents.Workflows.Declarative.ObjectModel; -internal sealed class ParseValueExecutor(ParseValue model, DeclarativeWorkflowState state) : +internal sealed class ParseValueExecutor(ParseValue model, WorkflowFormulaState state) : DeclarativeActionExecutor(model, state) { protected override async ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken) @@ -22,13 +23,13 @@ internal sealed class ParseValueExecutor(ParseValue model, DeclarativeWorkflowSt PropertyPath variablePath = Throw.IfNull(this.Model.Variable?.Path, $"{nameof(this.Model)}.{nameof(model.Variable)}"); ValueExpression valueExpression = Throw.IfNull(this.Model.Value, $"{nameof(this.Model)}.{nameof(this.Model.Value)}"); - EvaluationResult expressionResult = this.State.ExpressionEngine.GetValue(valueExpression); + EvaluationResult expressionResult = this.State.Evaluator.GetValue(valueExpression); FormulaValue? parsedResult = null; if (expressionResult.Value is RecordDataValue recordValue) { - parsedResult = recordValue.ToFormulaValue(); + parsedResult = recordValue.ToFormula(); } else if (expressionResult.Value is StringDataValue stringValue) { diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/QuestionExecutor.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/QuestionExecutor.cs new file mode 100644 index 0000000000..1fa2285d36 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/QuestionExecutor.cs @@ -0,0 +1,147 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Agents.Workflows.Declarative.Entities; +using Microsoft.Agents.Workflows.Declarative.Events; +using Microsoft.Agents.Workflows.Declarative.Extensions; +using Microsoft.Agents.Workflows.Declarative.Interpreter; +using Microsoft.Agents.Workflows.Declarative.PowerFx; +using Microsoft.Bot.ObjectModel; +using Microsoft.PowerFx.Types; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Agents.Workflows.Declarative.ObjectModel; + +internal sealed class QuestionExecutor(Question model, WorkflowFormulaState state) : + DeclarativeActionExecutor(model, state) +{ + public static class Steps + { + public static string Prepare(string id) => $"{id}_{nameof(Prepare)}"; + public static string Input(string id) => $"{id}_{nameof(Input)}"; + public static string Capture(string id) => $"{id}_{nameof(Capture)}"; + } + + private readonly DurableProperty _promptCount = new(nameof(_promptCount)); + private readonly DurableProperty _hasExecuted = new(nameof(_hasExecuted)); + + protected override bool IsDiscreteAction => false; + protected override bool EmitResultEvent => false; + + public bool IsComplete(object? message) + { + ExecutorResultMessage executorMessage = ExecutorResultMessage.ThrowIfNot(message); + return executorMessage.Result is null; + } + + protected override async ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken) + { + await this._promptCount.WriteAsync(context, 0).ConfigureAwait(false); + + InitializablePropertyPath variable = Throw.IfNull(this.Model.Variable); + bool hasValue = this.State.Get(variable.Path) is BlankValue; + bool alwaysPrompt = this.State.Evaluator.GetValue(this.Model.AlwaysPrompt).Value; + + bool proceed = !alwaysPrompt || hasValue; + if (proceed) + { + SkipQuestionMode mode = this.State.Evaluator.GetValue(this.Model.SkipQuestionMode).Value; + proceed = + mode switch + { + SkipQuestionMode.SkipOnFirstExecutionIfVariableHasValue => !await this._hasExecuted.ReadAsync(context).ConfigureAwait(false), + SkipQuestionMode.AlwaysSkipIfVariableHasValue => hasValue, + SkipQuestionMode.AlwaysAsk => true, + _ => true, + }; + } + + if (proceed) + { + await this.PromptAsync(context, cancellationToken).ConfigureAwait(false); + } + else + { + await context.SendResultMessageAsync(this.Id, result: null, cancellationToken).ConfigureAwait(false); + } + + return default; + } + + public async ValueTask PrepareResponseAsync(IWorkflowContext context, ExecutorResultMessage message, CancellationToken cancellationToken) + { + int count = await this._promptCount.ReadAsync(context).ConfigureAwait(false); + InputRequest inputRequest = new(this.FormatPrompt(this.Model.Prompt)); + await context.SendMessageAsync(inputRequest).ConfigureAwait(false); + await this._promptCount.WriteAsync(context, count + 1).ConfigureAwait(false); + } + + public async ValueTask CaptureResponseAsync(IWorkflowContext context, InputResponse message, CancellationToken cancellationToken) + { + FormulaValue? extractedValue = null; + if (string.IsNullOrWhiteSpace(message.Value)) + { + string unrecognizedResponse = this.FormatPrompt(this.Model.UnrecognizedPrompt); + await context.AddEventAsync(new MessageActivityEvent(unrecognizedResponse.Trim())).ConfigureAwait(false); + } + else + { + EntityExtractionResult entityResult = EntityExtractor.Parse(this.Model.Entity, message.Value); + if (entityResult.IsValid) + { + extractedValue = entityResult.Value; + } + else + { + string invalidResponse = this.FormatPrompt(this.Model.InvalidPrompt); + await context.AddEventAsync(new MessageActivityEvent(invalidResponse.Trim())).ConfigureAwait(false); + } + } + + if (extractedValue is null) + { + await this.PromptAsync(context, cancellationToken).ConfigureAwait(false); + } + else + { + await this.AssignAsync(this.Model.Variable?.Path, extractedValue, context).ConfigureAwait(false); + await this._hasExecuted.WriteAsync(context, true).ConfigureAwait(false); + await context.SendResultMessageAsync(this.Id, result: null, cancellationToken).ConfigureAwait(false); + } + } + + public async ValueTask CompleteAsync(IWorkflowContext context, ExecutorResultMessage message, CancellationToken cancellationToken) + { + await context.RaiseCompletionEventAsync(this.Model).ConfigureAwait(false); + } + + private async ValueTask PromptAsync(IWorkflowContext context, CancellationToken cancellationToken) + { + long repeatCount = this.State.Evaluator.GetValue(this.Model.RepeatCount).Value; + int actualCount = await this._promptCount.ReadAsync(context).ConfigureAwait(false); + if (actualCount >= repeatCount) + { + ValueExpression defaultValueExpression = Throw.IfNull(this.Model.DefaultValue); + DataValue defaultValue = this.State.Evaluator.GetValue(defaultValueExpression).Value; + await this.AssignAsync(this.Model.Variable?.Path, defaultValue.ToFormula(), context).ConfigureAwait(false); + string defaultValueResponse = this.FormatPrompt(this.Model.DefaultValueResponse); + await context.AddEventAsync(new MessageActivityEvent(defaultValueResponse.Trim())).ConfigureAwait(false); + await context.SendResultMessageAsync(this.Id, result: null, cancellationToken).ConfigureAwait(false); + } + else + { + await context.SendResultMessageAsync(this.Id, result: true, cancellationToken).ConfigureAwait(false); + } + } + + private string FormatPrompt(ActivityTemplateBase? promptTemplate) + { + if (promptTemplate is not MessageActivityTemplate messageActivity) + { + return string.Empty; + } + + return this.State.Engine.Format(messageActivity.Text).Trim(); + } +} diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/ResetVariableExecutor.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/ResetVariableExecutor.cs index 953233762c..e1ade47533 100644 --- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/ResetVariableExecutor.cs +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/ResetVariableExecutor.cs @@ -3,14 +3,14 @@ using System.Diagnostics; using System.Threading; using System.Threading.Tasks; -using Microsoft.Agents.Workflows.Declarative.Extensions; using Microsoft.Agents.Workflows.Declarative.Interpreter; +using Microsoft.Agents.Workflows.Declarative.PowerFx; using Microsoft.Bot.ObjectModel; using Microsoft.Shared.Diagnostics; namespace Microsoft.Agents.Workflows.Declarative.ObjectModel; -internal sealed class ResetVariableExecutor(ResetVariable model, DeclarativeWorkflowState state) : +internal sealed class ResetVariableExecutor(ResetVariable model, WorkflowFormulaState state) : DeclarativeActionExecutor(model, state) { protected override ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken) @@ -21,7 +21,7 @@ internal sealed class ResetVariableExecutor(ResetVariable model, DeclarativeWork Debug.WriteLine( $""" STATE: {this.GetType().Name} [{this.Id}] - NAME: {this.Model.Variable!.Format()} + NAME: {this.Model.Variable} """); return default; diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/RetrieveConversationMessageExecutor.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/RetrieveConversationMessageExecutor.cs new file mode 100644 index 0000000000..782e3aaef6 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/RetrieveConversationMessageExecutor.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Agents.Workflows.Declarative.Extensions; +using Microsoft.Agents.Workflows.Declarative.Interpreter; +using Microsoft.Agents.Workflows.Declarative.PowerFx; +using Microsoft.Bot.ObjectModel; +using Microsoft.Extensions.AI; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Agents.Workflows.Declarative.ObjectModel; + +internal sealed class RetrieveConversationMessageExecutor(RetrieveConversationMessage model, WorkflowAgentProvider agentProvider, WorkflowFormulaState state) : + DeclarativeActionExecutor(model, state) +{ + protected override async ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken) + { + string conversationId = this.State.Evaluator.GetValue(Throw.IfNull(this.Model.ConversationId, $"{nameof(this.Model)}.{nameof(this.Model.ConversationId)}")).Value; + string messageId = this.State.Evaluator.GetValue(Throw.IfNull(this.Model.MessageId, $"{nameof(this.Model)}.{nameof(this.Model.MessageId)}")).Value; + + ChatMessage message = await agentProvider.GetMessageAsync(conversationId, messageId, cancellationToken).ConfigureAwait(false); + + await this.AssignAsync(this.Model.Message?.Path, message.ToRecord(), context).ConfigureAwait(false); + + return default; + } +} diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/RetrieveConversationMessagesExecutor.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/RetrieveConversationMessagesExecutor.cs new file mode 100644 index 0000000000..588f244f08 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/RetrieveConversationMessagesExecutor.cs @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Agents.Workflows.Declarative.Extensions; +using Microsoft.Agents.Workflows.Declarative.Interpreter; +using Microsoft.Agents.Workflows.Declarative.PowerFx; +using Microsoft.Bot.ObjectModel; +using Microsoft.Extensions.AI; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Agents.Workflows.Declarative.ObjectModel; + +internal sealed class RetrieveConversationMessagesExecutor(RetrieveConversationMessages model, WorkflowAgentProvider agentProvider, WorkflowFormulaState state) : + DeclarativeActionExecutor(model, state) +{ + protected override async ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken) + { + string conversationId = this.State.Evaluator.GetValue(Throw.IfNull(this.Model.ConversationId, $"{nameof(this.Model)}.{nameof(this.Model.ConversationId)}")).Value; + + ChatMessage[] messages = await agentProvider.GetMessagesAsync( + conversationId, + limit: this.GetLimit(), + after: this.GetMessage(this.Model.MessageAfter), + before: this.GetMessage(this.Model.MessageBefore), + newestFirst: this.IsDescending(), + cancellationToken).ToArrayAsync(cancellationToken).ConfigureAwait(false); + + await this.AssignAsync(this.Model.Messages?.Path, messages.ToTable(), context).ConfigureAwait(false); + + return default; + } + + private int? GetLimit() + { + if (this.Model.Limit is null) + { + return null; + } + + long limit = this.State.Evaluator.GetValue(this.Model.Limit).Value; + return Convert.ToInt32(Math.Min(limit, 100)); + } + + private string? GetMessage(StringExpression? messagExpression) + { + if (messagExpression is null) + { + return null; + } + + return this.State.Evaluator.GetValue(messagExpression).Value; + } + + private bool IsDescending() + { + if (this.Model.SortOrder is null) + { + return false; + } + + AgentMessageSortOrderWrapper sortOrderWrapper = this.State.Evaluator.GetValue(this.Model.SortOrder).Value; + + return sortOrderWrapper.Value == AgentMessageSortOrder.NewestFirst; + } +} diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/SendActivityExecutor.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/SendActivityExecutor.cs index 2c581668a4..8c78036069 100644 --- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/SendActivityExecutor.cs +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/SendActivityExecutor.cs @@ -1,33 +1,24 @@ // Copyright (c) Microsoft. All rights reserved. -using System.Text; using System.Threading; using System.Threading.Tasks; +using Microsoft.Agents.Workflows.Declarative.Extensions; using Microsoft.Agents.Workflows.Declarative.Interpreter; +using Microsoft.Agents.Workflows.Declarative.PowerFx; using Microsoft.Bot.ObjectModel; -using Microsoft.Extensions.AI; -using Microsoft.Extensions.AI.Agents; namespace Microsoft.Agents.Workflows.Declarative.ObjectModel; -internal sealed class SendActivityExecutor(SendActivity model, DeclarativeWorkflowState state) : +internal sealed class SendActivityExecutor(SendActivity model, WorkflowFormulaState state) : DeclarativeActionExecutor(model, state) { protected override async ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken) { if (this.Model.Activity is MessageActivityTemplate messageActivity) { - StringBuilder templateBuilder = new(); - if (!string.IsNullOrEmpty(messageActivity.Summary)) - { - templateBuilder.AppendLine($"\t{messageActivity.Summary}"); - } + string activityText = this.State.Engine.Format(messageActivity.Text).Trim(); - string? activityText = this.State.Format(messageActivity.Text)?.Trim(); - templateBuilder.AppendLine(activityText); - - AgentRunResponse response = new([new ChatMessage(ChatRole.Assistant, templateBuilder.ToString().Trim())]); - await context.AddEventAsync(new AgentRunResponseEvent(this.Id, response)).ConfigureAwait(false); + await context.AddEventAsync(new MessageActivityEvent(activityText.Trim())).ConfigureAwait(false); } return default; diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/SetMultipleVariablesExecutor.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/SetMultipleVariablesExecutor.cs new file mode 100644 index 0000000000..3e77c6d76b --- /dev/null +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/SetMultipleVariablesExecutor.cs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Agents.Workflows.Declarative.Extensions; +using Microsoft.Agents.Workflows.Declarative.Interpreter; +using Microsoft.Agents.Workflows.Declarative.PowerFx; +using Microsoft.Bot.ObjectModel; +using Microsoft.Bot.ObjectModel.Abstractions; +using Microsoft.PowerFx.Types; + +namespace Microsoft.Agents.Workflows.Declarative.ObjectModel; + +internal sealed class SetMultipleVariablesExecutor(SetMultipleVariables model, WorkflowFormulaState state) + : DeclarativeActionExecutor(model, state) +{ + protected override async ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken) + { + foreach (VariableAssignment assignment in this.Model.Assignments) + { + if (assignment.Variable is null) + { + continue; + } + + if (assignment.Value is null) + { + await this.AssignAsync(assignment.Variable, FormulaValue.NewBlank(), context).ConfigureAwait(false); + } + else + { + EvaluationResult expressionResult = this.State.Evaluator.GetValue(assignment.Value); + + await this.AssignAsync(assignment.Variable, expressionResult.Value.ToFormula(), context).ConfigureAwait(false); + } + } + + return default; + } +} diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/SetTextVariableExecutor.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/SetTextVariableExecutor.cs index 2ee97a7b25..6778c8074d 100644 --- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/SetTextVariableExecutor.cs +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/SetTextVariableExecutor.cs @@ -2,14 +2,16 @@ using System.Threading; using System.Threading.Tasks; +using Microsoft.Agents.Workflows.Declarative.Extensions; using Microsoft.Agents.Workflows.Declarative.Interpreter; +using Microsoft.Agents.Workflows.Declarative.PowerFx; using Microsoft.Bot.ObjectModel; using Microsoft.PowerFx.Types; using Microsoft.Shared.Diagnostics; namespace Microsoft.Agents.Workflows.Declarative.ObjectModel; -internal sealed class SetTextVariableExecutor(SetTextVariable model, DeclarativeWorkflowState state) +internal sealed class SetTextVariableExecutor(SetTextVariable model, WorkflowFormulaState state) : DeclarativeActionExecutor(model, state) { protected override async ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken) @@ -22,7 +24,7 @@ internal sealed class SetTextVariableExecutor(SetTextVariable model, Declarative } else { - FormulaValue expressionResult = FormulaValue.New(this.State.Format(this.Model.Value)); + FormulaValue expressionResult = FormulaValue.New(this.State.Engine.Format(this.Model.Value)); await this.AssignAsync(variablePath, expressionResult, context).ConfigureAwait(false); } diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/SetVariableExecutor.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/SetVariableExecutor.cs index e1c6ec8857..07d00eaa22 100644 --- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/SetVariableExecutor.cs +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/SetVariableExecutor.cs @@ -4,6 +4,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Agents.Workflows.Declarative.Extensions; using Microsoft.Agents.Workflows.Declarative.Interpreter; +using Microsoft.Agents.Workflows.Declarative.PowerFx; using Microsoft.Bot.ObjectModel; using Microsoft.Bot.ObjectModel.Abstractions; using Microsoft.PowerFx.Types; @@ -11,7 +12,7 @@ using Microsoft.Shared.Diagnostics; namespace Microsoft.Agents.Workflows.Declarative.ObjectModel; -internal sealed class SetVariableExecutor(SetVariable model, DeclarativeWorkflowState state) +internal sealed class SetVariableExecutor(SetVariable model, WorkflowFormulaState state) : DeclarativeActionExecutor(model, state) { protected override async ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken) @@ -24,9 +25,9 @@ internal sealed class SetVariableExecutor(SetVariable model, DeclarativeWorkflow } else { - EvaluationResult expressionResult = this.State.ExpressionEngine.GetValue(this.Model.Value); + EvaluationResult expressionResult = this.State.Evaluator.GetValue(this.Model.Value); - await this.AssignAsync(variablePath, expressionResult.Value.ToFormulaValue(), context).ConfigureAwait(false); + await this.AssignAsync(variablePath, expressionResult.Value.ToFormula(), context).ConfigureAwait(false); } return default; diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/PowerFx/Functions/UserMessage.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/PowerFx/Functions/UserMessage.cs new file mode 100644 index 0000000000..fdd8048395 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/PowerFx/Functions/UserMessage.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.Extensions.AI; +using Microsoft.PowerFx; +using Microsoft.PowerFx.Types; + +namespace Microsoft.Agents.Workflows.Declarative.PowerFx.Functions; + +internal sealed class UserMessage : ReflectionFunction +{ + public const string FunctionName = nameof(UserMessage); + + public UserMessage() + : base(FunctionName, FormulaType.String, FormulaType.String) + { } + + public static FormulaValue Execute(StringValue input) => + string.IsNullOrEmpty(input.Value) ? + FormulaValue.NewBlank(RecordType.Empty()) : + FormulaValue.NewRecordFromFields( + new NamedValue(TypeSchema.Message.Fields.Role, FormulaValue.New(ChatRole.User.Value)), + new NamedValue( + TypeSchema.Message.Fields.Content, + FormulaValue.NewTable( + RecordType.Empty() + .Add(TypeSchema.Message.Fields.ContentType, FormulaType.String) + .Add(TypeSchema.Message.Fields.ContentValue, FormulaType.String), + [ + FormulaValue.NewRecordFromFields( + new NamedValue(TypeSchema.Message.Fields.ContentType, FormulaValue.New(TypeSchema.Message.ContentTypes.Text)), + new NamedValue(TypeSchema.Message.Fields.ContentValue, input)) + ] + ) + ) + ); +} diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/PowerFx/RecalcEngineFactory.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/PowerFx/RecalcEngineFactory.cs index f025164eae..b6e7bff333 100644 --- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/PowerFx/RecalcEngineFactory.cs +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/PowerFx/RecalcEngineFactory.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. +using Microsoft.Agents.Workflows.Declarative.PowerFx.Functions; using Microsoft.Bot.ObjectModel; using Microsoft.PowerFx; using Microsoft.PowerFx.Types; @@ -36,6 +37,7 @@ internal static class RecalcEngineFactory } config.EnableSetFunction(); + config.AddFunction(new UserMessage()); return config; } diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/PowerFx/SystemScope.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/PowerFx/SystemScope.cs index 83096bcb13..33f4f4a5bf 100644 --- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/PowerFx/SystemScope.cs +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/PowerFx/SystemScope.cs @@ -1,12 +1,10 @@ // Copyright (c) Microsoft. All rights reserved. using System; +using System.Collections.Frozen; using System.Collections.Generic; -using System.Collections.Immutable; using System.Globalization; -using System.Threading.Tasks; using Microsoft.Agents.Workflows.Declarative.Extensions; -using Microsoft.Agents.Workflows.Declarative.Interpreter; using Microsoft.Bot.ObjectModel; using Microsoft.Bot.ObjectModel.SystemVariables; using Microsoft.Extensions.AI; @@ -16,6 +14,8 @@ namespace Microsoft.Agents.Workflows.Declarative.PowerFx; internal static class SystemScope { + private static readonly RecordValue s_emptyMessage = new ChatMessage(ChatRole.User, string.Empty).ToRecord(); + public static class Names { public const string Activity = nameof(Activity); @@ -31,7 +31,7 @@ internal static class SystemScope public const string UserLanguage = nameof(UserLanguage); } - public static ImmutableHashSet AllNames { get; } = GetNames().ToImmutableHashSet(); + public static FrozenSet AllNames { get; } = GetNames().ToFrozenSet(); public static IEnumerable GetNames() { @@ -48,12 +48,12 @@ internal static class SystemScope yield return Names.UserLanguage; } - public static void InitializeSystem(this WorkflowScopes scopes) + public static void InitializeSystem(this WorkflowFormulaState scopes) { scopes.Set(Names.Activity, RecordValue.Empty(), VariableScopeNames.System); scopes.Set(Names.Bot, RecordValue.Empty(), VariableScopeNames.System); - scopes.Set(Names.LastMessage, FormulaType.String.NewBlank(), VariableScopeNames.System); + scopes.Set(Names.LastMessage, s_emptyMessage, VariableScopeNames.System); Set(Names.LastMessageId); Set(Names.LastMessageText); @@ -95,27 +95,21 @@ internal static class SystemScope } } - public static FormulaValue GetConversationId(this DeclarativeWorkflowState state) => - state.Get(VariableScopeNames.System, Names.ConversationId); + public static FormulaValue GetConversationId(this WorkflowFormulaState state) => + state.Get(Names.ConversationId, VariableScopeNames.System); - public static async ValueTask SetConversationIdAsync(this DeclarativeWorkflowState state, IWorkflowContext context, string conversationId) + public static void SetConversationId(this WorkflowFormulaState state, string conversationId) { - RecordValue conversation = (RecordValue)state.Get(VariableScopeNames.System, Names.Conversation); + RecordValue conversation = (RecordValue)state.Get(Names.Conversation, VariableScopeNames.System); conversation.UpdateField("Id", FormulaValue.New(conversationId)); - await state.SetAsync(VariableScopeNames.System, Names.Conversation, conversation, context).ConfigureAwait(false); - await state.SetAsync(VariableScopeNames.System, Names.ConversationId, FormulaValue.New(conversationId), context).ConfigureAwait(false); + state.Set(Names.Conversation, conversation, VariableScopeNames.System); + state.Set(Names.ConversationId, FormulaValue.New(conversationId), VariableScopeNames.System); } - public static FormulaValue GetInternalConversationId(this DeclarativeWorkflowState state) => - state.Get(VariableScopeNames.System, Names.InternalId); - - public static ValueTask SetInternalConversationIdAsync(this DeclarativeWorkflowState state, IWorkflowContext context, string conversationId) => - state.SetAsync(VariableScopeNames.System, Names.InternalId, FormulaValue.New(conversationId), context); - - public static async ValueTask SetLastMessageAsync(this DeclarativeWorkflowState state, IWorkflowContext context, ChatMessage message) + public static void SetLastMessage(this WorkflowFormulaState state, ChatMessage message) { - await state.SetAsync(VariableScopeNames.System, Names.LastMessage, message.ToRecord(), context).ConfigureAwait(false); - await state.SetAsync(VariableScopeNames.System, Names.LastMessageId, message.MessageId is null ? FormulaValue.NewBlank(FormulaType.String) : FormulaValue.New(message.MessageId), context).ConfigureAwait(false); - await state.SetAsync(VariableScopeNames.System, Names.LastMessageText, FormulaValue.New(message.Text), context).ConfigureAwait(false); + state.Set(Names.LastMessage, message.ToRecord(), VariableScopeNames.System); + state.Set(Names.LastMessageId, message.MessageId is null ? FormulaValue.NewBlank(FormulaType.String) : FormulaValue.New(message.MessageId), VariableScopeNames.System); + state.Set(Names.LastMessageText, FormulaValue.New(message.Text), VariableScopeNames.System); } } diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/PowerFx/TypeSchema.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/PowerFx/TypeSchema.cs new file mode 100644 index 0000000000..21f6770d7b --- /dev/null +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/PowerFx/TypeSchema.cs @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.Bot.ObjectModel; + +namespace Microsoft.Agents.Workflows.Declarative.PowerFx.Functions; + +internal static class TypeSchema +{ + public static class Message + { + public static class Fields + { + public const string Id = nameof(Id); + public const string ConversationId = nameof(ConversationId); + public const string AgentId = nameof(AgentId); + public const string RunId = nameof(RunId); + public const string Role = nameof(Role); + public const string Author = nameof(Author); + public const string Text = nameof(Text); + public const string Content = nameof(Content); + public const string ContentType = nameof(ContentType); + public const string ContentValue = nameof(ContentValue); + public const string Metadata = nameof(Metadata); + } + + public static class ContentTypes + { + public const string Text = nameof(AgentMessageContentType.Text); + public const string ImageUrl = nameof(AgentMessageContentType.ImageUrl); + public const string ImageFile = nameof(AgentMessageContentType.ImageFile); + } + } +} diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/PowerFx/UnassignedValue.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/PowerFx/UnassignedValue.cs new file mode 100644 index 0000000000..11b2e763d2 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/PowerFx/UnassignedValue.cs @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft. All rights reserved. + +namespace Microsoft.Agents.Workflows.Declarative.PowerFx; + +/// +/// Represents the absence of an assigned value for a variable used in an expression. +/// +public sealed record class UnassignedValue +{ + /// + /// A singleton instance of . + /// + public static UnassignedValue Instance { get; } = new(); +} diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/PowerFx/WorkflowDiagnostics.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/PowerFx/WorkflowDiagnostics.cs index 5bf22d92cf..db3c860d54 100644 --- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/PowerFx/WorkflowDiagnostics.cs +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/PowerFx/WorkflowDiagnostics.cs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft. All rights reserved. using System; +using System.Collections.Frozen; +using System.Collections.Generic; using System.Linq; using Microsoft.Agents.Workflows.Declarative.Extensions; using Microsoft.Bot.ObjectModel; @@ -12,11 +14,23 @@ using Microsoft.PowerFx.Types; namespace Microsoft.Agents.Workflows.Declarative.PowerFx; +internal sealed record class WorkflowTypeInfo(FrozenSet EnvironmentVariables, IEnumerable UserVariables); + internal static class WorkflowDiagnostics { private static readonly WorkflowFeatureConfiguration s_semanticFeatureConfig = new(); - public static void Initialize(this WorkflowScopes scopes, TElement workflowElement, IConfiguration? configuration) where TElement : BotElement, IDialogBase + public static WorkflowTypeInfo Describe(this TElement workflowElement) where TElement : BotElement, IDialogBase + { + SemanticModel semanticModel = workflowElement.GetSemanticModel(new PowerFxExpressionChecker(s_semanticFeatureConfig), s_semanticFeatureConfig); + + return + new WorkflowTypeInfo( + semanticModel.GetAllEnvironmentVariablesReferencedInTheBot().ToFrozenSet(), + semanticModel.GetVariables(workflowElement.SchemaName.Value).Where(x => !x.IsSystemVariable).Select(v => v.ToDiagnostic())); + } + + public static void Initialize(this WorkflowFormulaState scopes, TElement workflowElement, IConfiguration? configuration) where TElement : BotElement, IDialogBase { scopes.InitializeSystem(); @@ -25,7 +39,7 @@ internal static class WorkflowDiagnostics scopes.InitializeDefaults(semanticModel, workflowElement.SchemaName.Value); } - private static void InitializeEnvironment(this WorkflowScopes scopes, SemanticModel semanticModel, IConfiguration? configuration) + private static void InitializeEnvironment(this WorkflowFormulaState scopes, SemanticModel semanticModel, IConfiguration? configuration) { foreach (string variableName in semanticModel.GetAllEnvironmentVariablesReferencedInTheBot()) { @@ -35,7 +49,7 @@ internal static class WorkflowDiagnostics } } - private static void InitializeDefaults(this WorkflowScopes scopes, SemanticModel semanticModel, string schemaName) + private static void InitializeDefaults(this WorkflowFormulaState scopes, SemanticModel semanticModel, string schemaName) { foreach (VariableInformationDiagnostic variableDiagnostic in semanticModel.GetVariables(schemaName).Where(x => !x.IsSystemVariable).Select(v => v.ToDiagnostic())) { @@ -44,7 +58,7 @@ internal static class WorkflowDiagnostics continue; } - FormulaValue defaultValue = variableDiagnostic.ConstantValue?.ToFormulaValue() ?? variableDiagnostic.Type.NewBlank(); + FormulaValue defaultValue = variableDiagnostic.ConstantValue?.ToFormula() ?? variableDiagnostic.Type.NewBlank(); if (variableDiagnostic.Path.VariableScopeName?.Equals(VariableScopeNames.System, StringComparison.OrdinalIgnoreCase) ?? false) { @@ -54,7 +68,7 @@ internal static class WorkflowDiagnostics } } - scopes.Set(variableDiagnostic.Path.VariableName, defaultValue, variableDiagnostic.Path.VariableScopeName ?? WorkflowScopes.DefaultScopeName); + scopes.Set(variableDiagnostic.Path.VariableName, defaultValue, variableDiagnostic.Path.VariableScopeName ?? WorkflowFormulaState.DefaultScopeName); } } diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/PowerFx/WorkflowExpressionEngine.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/PowerFx/WorkflowExpressionEngine.cs index 70b594e50d..68fb410aaa 100644 --- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/PowerFx/WorkflowExpressionEngine.cs +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/PowerFx/WorkflowExpressionEngine.cs @@ -13,7 +13,7 @@ using Microsoft.Shared.Diagnostics; namespace Microsoft.Agents.Workflows.Declarative.PowerFx; -internal class WorkflowExpressionEngine : IExpressionEngine +internal class WorkflowExpressionEngine { private readonly RecalcEngine _engine; @@ -22,60 +22,26 @@ internal class WorkflowExpressionEngine : IExpressionEngine this._engine = engine; } - public EvaluationResult GetValue(BoolExpression boolean, WorkflowScopes? state = null) => this.GetValue(boolean, state, this.EvaluateScope); + public EvaluationResult GetValue(BoolExpression boolean) => this.Evaluate(boolean); - public EvaluationResult GetValue(BoolExpression boolean, RecordDataValue state) => this.GetValue(boolean, state, this.EvaluateState); + public EvaluationResult GetValue(StringExpression expression) => this.Evaluate(expression); - public EvaluationResult GetValue(StringExpression expression, WorkflowScopes? state = null) => this.GetValue(expression, state, this.EvaluateScope); + public EvaluationResult GetValue(ValueExpression expression) => this.Evaluate(expression); - public EvaluationResult GetValue(StringExpression expression, RecordDataValue state) => this.GetValue(expression, state, this.EvaluateState); + public EvaluationResult GetValue(IntExpression expression) => this.Evaluate(expression); - public EvaluationResult GetValue(ValueExpression expression, WorkflowScopes? state = null) => this.GetValue(expression, state, this.EvaluateScope); + public EvaluationResult GetValue(NumberExpression expression) => this.Evaluate(expression); - public EvaluationResult GetValue(ValueExpression expression, RecordDataValue state) => this.GetValue(expression, state, this.EvaluateState); + public EvaluationResult GetValue(ObjectExpression expression) where TValue : BotElement => this.Evaluate(expression); - public EvaluationResult GetValue(IntExpression expression, WorkflowScopes? state = null) => this.GetValue(expression, state, this.EvaluateScope); + public ImmutableArray GetValue(ArrayExpression expression) => this.Evaluate(expression).Value; - public EvaluationResult GetValue(IntExpression expression, RecordDataValue state) => this.GetValue(expression, state, this.EvaluateState); + public ImmutableArray GetValue(ArrayExpressionOnly expression) => this.Evaluate(expression).Value; - public EvaluationResult GetValue(NumberExpression expression, WorkflowScopes? state = null) => this.GetValue(expression, state, this.EvaluateScope); + public EvaluationResult GetValue(EnumExpression expression) where TValue : EnumWrapper => + this.Evaluate(expression); - public EvaluationResult GetValue(NumberExpression expression, RecordDataValue state) => this.GetValue(expression, state, this.EvaluateState); - - public EvaluationResult GetValue(ObjectExpression expression, WorkflowScopes? state = null) where TValue : BotElement => this.GetValue(expression, state, this.EvaluateScope); - - public EvaluationResult GetValue(ObjectExpression expression, RecordDataValue state) where TValue : BotElement => this.GetValue(expression, state, this.EvaluateState); - - public ImmutableArray GetValue(ArrayExpression expression, WorkflowScopes? state = null) => this.GetValue(expression, state, this.EvaluateScope).Value; - - public ImmutableArray GetValue(ArrayExpression expression, RecordDataValue state) => this.GetValue(expression, state, this.EvaluateState).Value; - - public ImmutableArray GetValue(ArrayExpressionOnly expression, WorkflowScopes? state = null) => this.GetValue(expression, state, this.EvaluateScope).Value; - - public ImmutableArray GetValue(ArrayExpressionOnly expression, RecordDataValue state) => this.GetValue(expression, state, this.EvaluateState).Value; - - public EvaluationResult GetValue(EnumExpression expression, WorkflowScopes? state = null) where TValue : EnumWrapper => - this.GetValue(expression, state, this.EvaluateScope); - - public EvaluationResult GetValue(EnumExpression expression, RecordDataValue state) where TValue : EnumWrapper => - this.GetValue(expression, state, this.EvaluateState); - - public DialogSchemaName GetValue(DialogExpression expression, RecordDataValue state) - { - throw new NotSupportedException(); - } - - public EvaluationResult GetValue(AdaptiveCardExpression expression, RecordDataValue state) - { - throw new NotSupportedException(); - } - - public EvaluationResult GetValue(FileExpression expression, RecordDataValue state) - { - throw new NotSupportedException(); - } - - private EvaluationResult GetValue(BoolExpression expression, TState state, Func> evaluator) + private EvaluationResult Evaluate(BoolExpression expression) { Throw.IfNull(expression, nameof(expression)); @@ -84,7 +50,7 @@ internal class WorkflowExpressionEngine : IExpressionEngine return new EvaluationResult(expression.LiteralValue, SensitivityLevel.None); } - EvaluationResult expressionResult = evaluator.Invoke(expression, state); + EvaluationResult expressionResult = this.EvaluateScope(expression); if (expressionResult.Value is BlankValue) { @@ -99,7 +65,7 @@ internal class WorkflowExpressionEngine : IExpressionEngine return new EvaluationResult(formulaValue.Value, expressionResult.Sensitivity); } - private EvaluationResult GetValue(StringExpression expression, TState state, Func> evaluator) + private EvaluationResult Evaluate(StringExpression expression) { Throw.IfNull(expression, nameof(expression)); @@ -108,7 +74,7 @@ internal class WorkflowExpressionEngine : IExpressionEngine return new EvaluationResult(expression.LiteralValue, SensitivityLevel.None); } - EvaluationResult expressionResult = evaluator.Invoke(expression, state); + EvaluationResult expressionResult = this.EvaluateScope(expression); if (expressionResult.Value is BlankValue) { @@ -128,7 +94,7 @@ internal class WorkflowExpressionEngine : IExpressionEngine return new EvaluationResult(formulaValue.Value, expressionResult.Sensitivity); } - private EvaluationResult GetValue(IntExpression expression, TState state, Func> evaluator) + private EvaluationResult Evaluate(IntExpression expression) { Throw.IfNull(expression, nameof(expression)); @@ -137,7 +103,7 @@ internal class WorkflowExpressionEngine : IExpressionEngine return new EvaluationResult(expression.LiteralValue, SensitivityLevel.None); } - EvaluationResult expressionResult = evaluator.Invoke(expression, state); + EvaluationResult expressionResult = this.EvaluateScope(expression); if (expressionResult.Value is BlankValue) { @@ -152,7 +118,7 @@ internal class WorkflowExpressionEngine : IExpressionEngine return new EvaluationResult(Convert.ToInt64(formulaValue.Value), expressionResult.Sensitivity); } - private EvaluationResult GetValue(NumberExpression expression, TState state, Func> evaluator) + private EvaluationResult Evaluate(NumberExpression expression) { Throw.IfNull(expression, nameof(expression)); @@ -161,7 +127,7 @@ internal class WorkflowExpressionEngine : IExpressionEngine return new EvaluationResult(expression.LiteralValue, SensitivityLevel.None); } - EvaluationResult expressionResult = evaluator.Invoke(expression, state); + EvaluationResult expressionResult = this.EvaluateScope(expression); if (expressionResult.Value is BlankValue) { @@ -181,7 +147,7 @@ internal class WorkflowExpressionEngine : IExpressionEngine return new EvaluationResult(formulaValue.Value, expressionResult.Sensitivity); } - private EvaluationResult GetValue(ValueExpression expression, TState? state, Func> evaluator) + private EvaluationResult Evaluate(ValueExpression expression) { Throw.IfNull(expression, nameof(expression)); @@ -190,12 +156,12 @@ internal class WorkflowExpressionEngine : IExpressionEngine return new EvaluationResult(expression.LiteralValue ?? BlankDataValue.Instance, SensitivityLevel.None); } - EvaluationResult expressionResult = evaluator.Invoke(expression, state); + EvaluationResult expressionResult = this.EvaluateScope(expression); return new EvaluationResult(expressionResult.Value.ToDataValue(), expressionResult.Sensitivity); } - private EvaluationResult GetValue(EnumExpression expression, TState? state, Func> evaluator) where TValue : EnumWrapper + private EvaluationResult Evaluate(EnumExpression expression) where TValue : EnumWrapper { Throw.IfNull(expression, nameof(expression)); @@ -204,7 +170,7 @@ internal class WorkflowExpressionEngine : IExpressionEngine return new EvaluationResult(expression.LiteralValue, SensitivityLevel.None); } - EvaluationResult expressionResult = evaluator.Invoke(expression, state); + EvaluationResult expressionResult = this.EvaluateScope(expression); return expressionResult.Value switch { @@ -216,7 +182,7 @@ internal class WorkflowExpressionEngine : IExpressionEngine }; } - private EvaluationResult GetValue(ObjectExpression expression, TState state, Func> evaluator) where TValue : BotElement + private EvaluationResult Evaluate(ObjectExpression expression) where TValue : BotElement { Throw.IfNull(expression, nameof(expression)); @@ -225,7 +191,7 @@ internal class WorkflowExpressionEngine : IExpressionEngine return new EvaluationResult(expression.LiteralValue, SensitivityLevel.None); } - EvaluationResult expressionResult = evaluator.Invoke(expression, state); + EvaluationResult expressionResult = this.EvaluateScope(expression); if (expressionResult.Value is BlankValue) { @@ -247,7 +213,7 @@ internal class WorkflowExpressionEngine : IExpressionEngine } } - private EvaluationResult> GetValue(ArrayExpression expression, TState state, Func> evaluator) + private EvaluationResult> Evaluate(ArrayExpression expression) { Throw.IfNull(expression, nameof(expression)); @@ -256,16 +222,16 @@ internal class WorkflowExpressionEngine : IExpressionEngine return new EvaluationResult>(expression.LiteralValue, SensitivityLevel.None); } - EvaluationResult expressionResult = evaluator.Invoke(expression, state); + EvaluationResult expressionResult = this.EvaluateScope(expression); return new EvaluationResult>(ParseArrayResults(expressionResult.Value), expressionResult.Sensitivity); } - private EvaluationResult> GetValue(ArrayExpressionOnly expression, TState state, Func> evaluator) + private EvaluationResult> Evaluate(ArrayExpressionOnly expression) { Throw.IfNull(expression, nameof(expression)); - EvaluationResult expressionResult = evaluator.Invoke(expression, state); + EvaluationResult expressionResult = this.EvaluateScope(expression); return new EvaluationResult>(ParseArrayResults(expressionResult.Value), expressionResult.Sensitivity); } @@ -302,40 +268,11 @@ internal class WorkflowExpressionEngine : IExpressionEngine } } - private EvaluationResult EvaluateState(ExpressionBase expression, RecordDataValue? state) - { - if (state is not null) - { - foreach (KeyValuePair kvp in state.Properties) - { - if (kvp.Value is RecordDataValue scopeRecord) - { - Bind(kvp.Key, scopeRecord.ToRecordValue()); - } - } - } - - return this.Evaluate(expression); - - void Bind(string scopeName, RecordValue stateRecord) - { - this._engine.DeleteFormula(scopeName); - this._engine.UpdateVariable(scopeName, stateRecord); - } - } - - private EvaluationResult EvaluateScope(ExpressionBase expression, WorkflowScopes? state = null) - { - state?.Bind(this._engine); - - return this.Evaluate(expression); - } - - private EvaluationResult Evaluate(ExpressionBase expression) + private EvaluationResult EvaluateScope(ExpressionBase expression) { string? expressionText = expression.IsVariableReference ? - expression.VariableReference?.Format() : + expression.VariableReference?.ToString() : expression.ExpressionText; return new(this._engine.Eval(expressionText), SensitivityLevel.None); diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/PowerFx/WorkflowFormulaState.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/PowerFx/WorkflowFormulaState.cs new file mode 100644 index 0000000000..c067d8bc8d --- /dev/null +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/PowerFx/WorkflowFormulaState.cs @@ -0,0 +1,191 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Frozen; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Agents.Workflows.Declarative.Extensions; +using Microsoft.Bot.ObjectModel; +using Microsoft.PowerFx; +using Microsoft.PowerFx.Types; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Agents.Workflows.Declarative.PowerFx; + +/// +/// Contains all variables scopes for a workflow. +/// +internal sealed class WorkflowFormulaState +{ + // ISSUE #488 - Update default scope for workflows to `Workflow` (instead of `Topic`) + public const string DefaultScopeName = VariableScopeNames.Topic; + + private static readonly FrozenSet s_mutableScopes = + [ + VariableScopeNames.Topic, + VariableScopeNames.Global, + VariableScopeNames.System, + ]; + + private readonly Dictionary _scopes; + private int _isInitialized; + + public RecalcEngine Engine { get; } + + public WorkflowExpressionEngine Evaluator { get; } + + public WorkflowFormulaState(RecalcEngine engine) + { + this.Engine = engine; + this.Evaluator = new WorkflowExpressionEngine(engine); + this._scopes = VariableScopeNames.AllScopes.ToDictionary(scopeName => scopeName, scopeName => new WorkflowScope(scopeName)); + this.Bind(); + } + + public FormulaValue Get(PropertyPath variablePath) => this.Get(Throw.IfNull(variablePath.VariableName), variablePath.VariableScopeName); + + public FormulaValue Get(string variableName, string? scopeName = null) + { + if (this.GetScope(scopeName).TryGetValue(variableName, out FormulaValue? value)) + { + return value; + } + + return FormulaValue.NewBlank(); + } + + public void ResetAll(string? scopeName = null) + { + if (scopeName is not null) + { + this.GetScope(scopeName).ResetAll(); + } + else + { + foreach (string targetScope in VariableScopeNames.AllScopes) + { + this.GetScope(targetScope).ResetAll(); + } + } + + this.Bind(); + } + + public void Reset(PropertyPath variablePath) => this.Reset(Throw.IfNull(variablePath.VariableName), variablePath.VariableScopeName); + + public void Reset(string variableName, string? scopeName = null) + { + this.GetScope(scopeName).Reset(variableName); + this.Bind(); + } + + public void Set(PropertyPath variablePath, FormulaValue value) => this.Set(Throw.IfNull(variablePath.VariableName), value, variablePath.VariableScopeName); + + public void Set(string variableName, FormulaValue value, string? scopeName = null) + { + this.GetScope(scopeName)[variableName] = value; + this.Bind(); + } + + public bool SetInitialized() => Interlocked.CompareExchange(ref this._isInitialized, 1, 0) == 0; + + public async ValueTask RestoreAsync(IWorkflowContext context, CancellationToken cancellationToken) + { + if (!this.SetInitialized()) + { + return; + } + + await Task.WhenAll(s_mutableScopes.Select(scopeName => ReadScopeAsync(scopeName))).ConfigureAwait(false); + + async Task ReadScopeAsync(string scopeName) + { + HashSet keys = await context.ReadStateKeysAsync(scopeName).ConfigureAwait(false); + foreach (string key in keys) + { + object? value = await context.ReadStateAsync(key, scopeName).ConfigureAwait(false); + if (value is null || value is UnassignedValue) + { + value = FormulaValue.NewBlank(); + } + this.Set(key, value.ToFormula(), scopeName); + } + + this.Bind(scopeName); + } + } + + public RecordValue BuildRecord(string scopeName) => this.GetScope(scopeName).BuildRecord(); + + public void Bind(string? targetScope = null) + { + if (targetScope is not null) + { + Bind(targetScope); + } + else + { + foreach (string scopeName in VariableScopeNames.AllScopes) + { + Bind(scopeName); + } + } + + void Bind(string scopeName) + { + RecordValue scopeRecord = this.BuildRecord(scopeName); + this.Engine.DeleteFormula(scopeName); + this.Engine.UpdateVariable(scopeName, scopeRecord); + } + } + + private WorkflowScope GetScope(string? scopeName) + { + scopeName ??= WorkflowFormulaState.DefaultScopeName; + + if (!VariableScopeNames.IsValidName(scopeName)) + { + throw new DeclarativeActionException($"Invalid variable scope name: '{scopeName}'."); + } + + return this._scopes[scopeName]; + } + + /// + /// The set of variables for a specific action scope. + /// + private sealed class WorkflowScope(string scopeName) : Dictionary + { + public string Name => scopeName; + + public void ResetAll() + { + foreach (string variableName in this.Keys.ToArray()) + { + this.Reset(variableName); + } + } + + public void Reset(string variableName) + { + if (this.TryGetValue(variableName, out FormulaValue? value)) + { + this[variableName] = value.Type.NewBlank(); + } + } + + public RecordValue BuildRecord() + { + return FormulaValue.NewRecordFromFields(GetFields()); + + IEnumerable GetFields() + { + foreach (KeyValuePair kvp in this) + { + yield return new NamedValue(kvp.Key, kvp.Value); + } + } + } + } +} diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/PowerFx/WorkflowScopes.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/PowerFx/WorkflowScopes.cs deleted file mode 100644 index b68e76d6f6..0000000000 --- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/PowerFx/WorkflowScopes.cs +++ /dev/null @@ -1,129 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Linq; -using Microsoft.Agents.Workflows.Declarative.Extensions; -using Microsoft.Bot.ObjectModel; -using Microsoft.PowerFx; -using Microsoft.PowerFx.Types; - -namespace Microsoft.Agents.Workflows.Declarative.PowerFx; - -/// -/// Contains all action scopes for a process. -/// -internal sealed class WorkflowScopes -{ - // ISSUE #488 - Update default scope for workflows to `Workflow` (instead of `Topic`) - public const string DefaultScopeName = VariableScopeNames.Topic; - - private readonly ImmutableDictionary _scopes; - - public WorkflowScopes() - { - this._scopes = VariableScopeNames.AllScopes.ToDictionary(scopeName => scopeName, scopeName => new WorkflowScope(scopeName)).ToImmutableDictionary(); - } - - public FormulaValue Get(string variableName, string? scopeName = null) - { - if (this._scopes[scopeName ?? WorkflowScopes.DefaultScopeName].TryGetValue(variableName, out FormulaValue? value)) - { - return value; - } - - return FormulaValue.NewBlank(); - } - - public void Clear(string scopeName) => this._scopes[scopeName].Reset(); - - public void Reset(string variableName, string? scopeName = null) => this._scopes[scopeName ?? WorkflowScopes.DefaultScopeName].Reset(variableName); - - public void Set(string variableName, FormulaValue value, string? scopeName = null) => this._scopes[scopeName ?? WorkflowScopes.DefaultScopeName][variableName] = value; - - public RecordValue BuildRecord(string scopeName) => this._scopes[scopeName].BuildRecord(); - - public RecordDataValue BuildState() - { - return DataValue.RecordFromFields(BuildStateFields()); - - IEnumerable> BuildStateFields() - { - foreach (KeyValuePair kvp in this._scopes) - { - yield return new(kvp.Key, kvp.Value.BuildState()); - } - } - } - - public void Bind(RecalcEngine engine, string? type = null) - { - if (type is not null) - { - Bind(type); - } - else - { - foreach (string scopeName in VariableScopeNames.AllScopes) - { - Bind(scopeName); - } - } - - void Bind(string scopeName) - { - RecordValue scopeRecord = this.BuildRecord(scopeName); - engine.DeleteFormula(scopeName); - engine.UpdateVariable(scopeName, scopeRecord); - } - } - - /// - /// The set of variables for a specific action scope. - /// - private sealed class WorkflowScope(string scopeName) : Dictionary - { - public string Name => scopeName; - - public void Reset() - { - foreach (string variableName in this.Keys.ToArray()) - { - this.Reset(variableName); - } - } - - public void Reset(string variableName) - { - if (this.TryGetValue(variableName, out FormulaValue? value)) - { - this[variableName] = value.Type.NewBlank(); - } - } - - public RecordValue BuildRecord() - { - return FormulaValue.NewRecordFromFields(GetFields()); - - IEnumerable GetFields() - { - foreach (KeyValuePair kvp in this) - { - yield return new NamedValue(kvp.Key, kvp.Value); - } - } - } - - public RecordDataValue BuildState() - { - RecordDataValue.Builder recordBuilder = new(); - - foreach (KeyValuePair kvp in this) - { - recordBuilder.Properties.Add(kvp.Key, kvp.Value.ToDataValue()); - } - - return recordBuilder.Build(); - } - } -} diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/WorkflowAgentProvider.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/WorkflowAgentProvider.cs index 626811c786..a452ee0643 100644 --- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/WorkflowAgentProvider.cs +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/WorkflowAgentProvider.cs @@ -1,11 +1,9 @@ // Copyright (c) Microsoft. All rights reserved. -using System.Net.Http; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -using Azure.AI.Agents.Persistent; -using Azure.Core; -using Azure.Core.Pipeline; +using Microsoft.Extensions.AI; using Microsoft.Extensions.AI.Agents; namespace Microsoft.Agents.Workflows.Declarative; @@ -20,47 +18,48 @@ public abstract class WorkflowAgentProvider /// /// The unique identifier of the AI agent to retrieve. Cannot be null or empty. /// A cancellation token that can be used to cancel the operation. - /// A task that represents the asynchronous operation. The task result contains the associated - /// with the specified . Returns if no agent is found. + /// The task result contains the associated. public abstract Task GetAgentAsync(string agentId, CancellationToken cancellationToken = default); -} - -/// -/// Provides functionality to interact with Foundry agents within a specified project context. -/// -/// This class is used to retrieve and manage AI agents associated with a Foundry project. It requires a -/// project endpoint and credentials to authenticate requests. -/// The endpoint URL of the Foundry project. This must be a valid, non-null URI pointing to the project. -/// The credentials used to authenticate with the Foundry project. This must be a valid instance of . -/// An optional instance to be used for making HTTP requests. If not provided, a default client will be used. -public sealed class FoundryAgentProvider(string projectEndpoint, TokenCredential projectCredentials, HttpClient? httpClient = null) : WorkflowAgentProvider -{ - private PersistentAgentsClient? _agentsClient; - - /// - public override async Task GetAgentAsync(string agentId, CancellationToken cancellationToken = default) - { - AIAgent agent = await this.GetAgentsClient().GetAIAgentAsync(agentId, chatOptions: null, cancellationToken).ConfigureAwait(false); - - return agent; - } - - private PersistentAgentsClient GetAgentsClient() - { - if (this._agentsClient is null) - { - PersistentAgentsAdministrationClientOptions clientOptions = new(); - - if (httpClient is not null) - { - clientOptions.Transport = new HttpClientTransport(httpClient); - } - - PersistentAgentsClient newClient = new(projectEndpoint, projectCredentials, clientOptions); - - Interlocked.CompareExchange(ref this._agentsClient, newClient, null); - } - - return this._agentsClient; - } + + /// + /// Asynchronously creates a new conversation and returns its unique identifier. + /// + /// A cancellation token that can be used to cancel the operation. + /// The conversation identifier + public abstract Task CreateConversationAsync(CancellationToken cancellationToken = default); + + /// + /// Creates a new message in the specified conversation. + /// + /// The identifier of the target conversation. + /// The message being added. + /// A cancellation token that can be used to cancel the operation. + public abstract Task CreateMessageAsync(string conversationId, ChatMessage conversationMessage, CancellationToken cancellationToken = default); + + /// + /// Retrieves a specific message from a conversation. + /// + /// The identifier of the target conversation. + /// The identifier of the target message. + /// A cancellation token that can be used to cancel the operation. + /// The requested message + public abstract Task GetMessageAsync(string conversationId, string messageId, CancellationToken cancellationToken = default); + + /// + /// Retrieves a set of messages from a conversation. + /// + /// The identifier of the target conversation. + /// A limit on the number of objects to be returned. Limit can range between 1 and 100, and the default is 20. + /// A cursor for use in pagination. after is an object ID that defines your place in the list. + /// A cursor for use in pagination. before is an object ID that defines your place in the list. + /// Provide records in descending order when true. + /// A cancellation token that can be used to cancel the operation. + /// The requested messages + public abstract IAsyncEnumerable GetMessagesAsync( + string conversationId, + int? limit = null, + string? after = null, + string? before = null, + bool newestFirst = false, + CancellationToken cancellationToken = default); } diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.CopilotStudio/ActivityProcessor.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.CopilotStudio/ActivityProcessor.cs index e36c57ea78..7ef3119945 100644 --- a/dotnet/src/Microsoft.Extensions.AI.Agents.CopilotStudio/ActivityProcessor.cs +++ b/dotnet/src/Microsoft.Extensions.AI.Agents.CopilotStudio/ActivityProcessor.cs @@ -16,28 +16,20 @@ internal static class ActivityProcessor { await foreach (IActivity activity in activities.ConfigureAwait(false)) { - switch (activity.Type) + // TODO: Prototype a custom AIContent type for CardActions, where the user is instructed to + // pick from a list of actions. + // The activity text doesn't make sense without the actions, as the message + // is often instructing the user to pick from the provided list of actions. + if (!string.IsNullOrWhiteSpace(activity.Text)) { - case "message": - // For streaming scenarios, we sometimes receive intermediate text via "typing" activities, but not always. - // In some cases the response is also returned multiple times via "typing" activities, so the only reliable - // way to get the final response is to wait for a "message" activity. - - // TODO: Prototype a custom AIContent type for CardActions, where the user is instructed to - // pick from a list of actions. - // The activity text doesn't make sense without the actions, as the message - // is often instructing the user to pick from the provided list of actions. + if ((activity.Type == "message" && !streaming) || (activity.Type == "typing" && streaming)) + { yield return CreateChatMessageFromActivity(activity, [new TextContent(activity.Text)]); - break; - case "typing": - case "event": - // TODO: Revisit usage of TextReasoningContent here, to evaluate whether all are really reasoning - // or whether simply an AIContent base type would be more appropriate. - yield return CreateChatMessageFromActivity(activity, [new TextReasoningContent(activity.Text)]); - break; - default: + } + else + { logger.LogWarning("Unknown activity type '{ActivityType}' received.", activity.Type); - break; + } } } } diff --git a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Agents/BasicAgent.yaml b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Agents/BasicAgent.yaml new file mode 100644 index 0000000000..2d41b2c5dd --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Agents/BasicAgent.yaml @@ -0,0 +1,5 @@ +type: foundry_agent +name: BasicAgent +description: Basic agent for integration tests +model: + id: ${AzureAI:ModelDeployment} diff --git a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/DeclarativeWorkflowTest.cs b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/DeclarativeWorkflowTest.cs new file mode 100644 index 0000000000..ad58b2b8d2 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/DeclarativeWorkflowTest.cs @@ -0,0 +1,114 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading.Tasks; +using Azure.Identity; +using Microsoft.Agents.Workflows.Declarative.IntegrationTests.Framework; +using Microsoft.Extensions.AI; +using Microsoft.Extensions.Configuration; +using Shared.IntegrationTests; +using Xunit.Abstractions; + +namespace Microsoft.Agents.Workflows.Declarative.IntegrationTests; + +/// +/// Tests execution of workflow created by . +/// +[Collection("Global")] +public sealed class DeclarativeWorkflowTest(ITestOutputHelper output, AgentFixture agentFixture) : WorkflowTest(output), IClassFixture +{ + [Theory] + [InlineData("SendActivity.yaml", "SendActivity.json")] + [InlineData("InvokeAgent.yaml", "InvokeAgent.json")] + public Task Validate(string workflowFileName, string testcaseFileName) => + this.RunWorkflow(workflowFileName, testcaseFileName); + + private Task RunWorkflow(string workflowFileName, string testcaseFileName) + { + this.Output.WriteLine($"WORKFLOW: {workflowFileName}"); + this.Output.WriteLine($"TESTCASE: {testcaseFileName}"); + + Testcase testcase = ReadTestcase(testcaseFileName); + IConfiguration configuration = InitializeConfig(); + string workflowPath = Path.Combine("Workflows", workflowFileName); + + this.Output.WriteLine($" {testcase.Description}"); + + return + testcase.Setup.Input.Type switch + { + nameof(ChatMessage) => this.RunWorkflow(testcase, workflowPath, configuration), + nameof(String) => this.RunWorkflow(testcase, workflowPath, configuration), + _ => throw new NotSupportedException($"Input type '{testcase.Setup.Input.Type}' is not supported."), + }; + } + + private async Task RunWorkflow( + Testcase testcase, + string workflowPath, + IConfiguration configuration) where TInput : notnull + { + this.Output.WriteLine($"INPUT: {testcase.Setup.Input.Value}"); + + AzureAIConfiguration? foundryConfig = configuration.GetSection("AzureAI").Get(); + Assert.NotNull(foundryConfig); + + IDictionary agentMap = await agentFixture.GetAgentsAsync(foundryConfig); + + IConfiguration workflowConfig = + new ConfigurationBuilder() + .AddInMemoryCollection(agentMap) + .Build(); + + DeclarativeWorkflowOptions workflowOptions = + new(new AzureAgentProvider(foundryConfig.Endpoint, new AzureCliCredential())) + { + Configuration = workflowConfig, + LoggerFactory = this.Output + }; + Workflow workflow = DeclarativeWorkflowBuilder.Build(workflowPath, workflowOptions); + + WorkflowEvents workflowEvents = await WorkflowHarness.RunAsync(workflow, (TInput)GetInput(testcase)); + foreach (DeclarativeActionInvokeEvent actionInvokeEvent in workflowEvents.ActionInvokeEvents) + { + this.Output.WriteLine($"ACTION: {actionInvokeEvent.ActionId} [{actionInvokeEvent.ActionType}]"); + } + + Assert.Equal(testcase.Validation.ActionCount, workflowEvents.ActionInvokeEvents.Count); + Assert.Equal(testcase.Validation.ActionCount, workflowEvents.ActionCompleteEvents.Count); + } + + private static object GetInput(Testcase testcase) where TInput : notnull => + testcase.Setup.Input.Type switch + { + nameof(ChatMessage) => new ChatMessage(ChatRole.User, testcase.Setup.Input.Value), + nameof(String) => testcase.Setup.Input.Value, + _ => throw new NotSupportedException($"Input type '{testcase.Setup.Input.Type}' is not supported."), + }; + + private static Testcase ReadTestcase(string testcaseFileName) + { + using Stream testcaseStream = File.Open(Path.Combine("Testcases", testcaseFileName), FileMode.Open); + Testcase? testcase = JsonSerializer.Deserialize(testcaseStream, s_jsonSerializerOptions); + Assert.NotNull(testcase); + return testcase; + } + + private static IConfigurationRoot InitializeConfig() => + new ConfigurationBuilder() + .AddUserSecrets(Assembly.GetExecutingAssembly()) + .AddEnvironmentVariables() + .Build(); + + private static readonly JsonSerializerOptions s_jsonSerializerOptions = new() + { + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, + WriteIndented = true, + }; +} diff --git a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Framework/AgentFactory.cs b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Framework/AgentFactory.cs new file mode 100644 index 0000000000..f88d4c1f35 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Framework/AgentFactory.cs @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Azure.AI.Agents.Persistent; +using Azure.Identity; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Agents; +using Microsoft.SemanticKernel.Agents.AzureAI; +using Shared.IntegrationTests; + +namespace Microsoft.Agents.Workflows.Declarative.IntegrationTests.Framework; + +#pragma warning disable SKEXP0110 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + +internal static class AgentFactory +{ + public static async Task> CreateAsync(string agentsDirectory, AzureAIConfiguration config, CancellationToken cancellationToken) + { + PersistentAgentsClient clientAgents = new(config.Endpoint, new AzureCliCredential()); + + IKernelBuilder kernelBuilder = Kernel.CreateBuilder(); + kernelBuilder.Services.AddSingleton(clientAgents); + Kernel kernel = kernelBuilder.Build(); + + AzureAIAgentFactory factory = new(); + + Dictionary agentMap = []; + + foreach (string file in Directory.GetFiles(agentsDirectory, "*.yaml")) + { + Debug.WriteLine($"TEST AGENT: Creating - {file}"); + string agentText = File.ReadAllText(file); + + Agent? agent = await factory.CreateAgentFromYamlAsync(agentText, new AgentCreationOptions() { Kernel = kernel }, configuration: null, cancellationToken); + + Assert.NotNull(agent?.Name); + + Debug.WriteLine($"TEST AGENT: {agent.Name} => {agent.Id}"); + agentMap[agent.Name] = agent.Id; + } + + return agentMap.ToImmutableDictionary(); + } +} diff --git a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Framework/AgentFixture.cs b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Framework/AgentFixture.cs new file mode 100644 index 0000000000..fb38bd1279 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Framework/AgentFixture.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Immutable; +using System.Threading; +using System.Threading.Tasks; +using Shared.IntegrationTests; + +namespace Microsoft.Agents.Workflows.Declarative.IntegrationTests.Framework; + +public sealed class AgentFixture : IDisposable +{ + private static ImmutableDictionary? s_agentMap; + + internal async Task> GetAgentsAsync(AzureAIConfiguration config, CancellationToken cancellationToken = default) + { + s_agentMap ??= await AgentFactory.CreateAsync("Agents", config, cancellationToken); + + return s_agentMap; + } + + public void Dispose() + { + GC.SuppressFinalize(this); + } +} diff --git a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Framework/TestOutputAdapter.cs b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Framework/TestOutputAdapter.cs new file mode 100644 index 0000000000..5887c185ea --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Framework/TestOutputAdapter.cs @@ -0,0 +1,73 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using Microsoft.Extensions.Logging; +using Xunit.Abstractions; + +namespace Microsoft.Agents.Workflows.Declarative.IntegrationTests.Framework; + +public sealed class TestOutputAdapter(ITestOutputHelper output) : TextWriter, ILogger, ILoggerFactory +{ + private readonly Stack _scopes = []; + + public override Encoding Encoding { get; } = Encoding.UTF8; + + public void AddProvider(ILoggerProvider provider) => throw new NotSupportedException(); + + public ILogger CreateLogger(string categoryName) => this; + + public bool IsEnabled(LogLevel logLevel) => true; + + public override void WriteLine(object? value = null) => this.SafeWrite($"{value}"); + + public override void WriteLine(string? format, params object?[] arg) => this.SafeWrite(string.Format(format ?? string.Empty, arg)); + + public override void WriteLine(string? value) => this.SafeWrite(value ?? string.Empty); + + public override void Write(object? value = null) => this.SafeWrite($"{value}"); + + public override void Write(char[]? buffer) => this.SafeWrite(new string(buffer)); + + public IDisposable BeginScope(TState state) where TState : notnull + { + this._scopes.Push($"{state}"); + return new LoggerScope(() => this._scopes.Pop()); + } + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + { + string message = formatter(state, exception); + string scope = this._scopes.Count > 0 ? $"[{this._scopes.Peek()}] " : string.Empty; + output.WriteLine($"{scope}{message}"); + } + + private void SafeWrite(string value) + { + try + { + output.WriteLine(value ?? string.Empty); + } + catch (InvalidOperationException exception) when (exception.Message == "There is no currently active test.") + { + // This exception is thrown when the test output is accessed outside of a test context. + // We can ignore it since we are not in a test context. + } + } + + private sealed class LoggerScope(Action action) : IDisposable + { + private bool _disposed; + + public void Dispose() + { + if (!this._disposed) + { + action.Invoke(); + this._disposed = true; + } + } + } +} diff --git a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Framework/Testcase.cs b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Framework/Testcase.cs new file mode 100644 index 0000000000..aa35ddf65c --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Framework/Testcase.cs @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Text.Json.Serialization; + +namespace Microsoft.Agents.Workflows.Declarative.IntegrationTests.Framework; + +public sealed class Testcase +{ + [JsonConstructor] + public Testcase( + string description, + TestcaseSetup setup, + TestcaseValidation validation) + { + this.Description = description; + this.Setup = setup; + this.Validation = validation; + } + + public string Description { get; } + + public TestcaseSetup Setup { get; } + + public TestcaseValidation Validation { get; } +} + +public sealed class TestcaseSetup +{ + [JsonConstructor] + public TestcaseSetup(TestcaseInput input) + { + this.Input = input; + } + public TestcaseInput Input { get; } +} + +public sealed class TestcaseInput +{ + [JsonConstructor] + public TestcaseInput(string type, string value) + { + this.Type = type; + this.Value = value; + } + + public string Type { get; } + public string Value { get; } +} + +public sealed class TestcaseValidation +{ + [JsonConstructor] + public TestcaseValidation(int actionCount) + { + this.ActionCount = actionCount; + } + + public int ActionCount { get; } +} diff --git a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Framework/WorkflowEvents.cs b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Framework/WorkflowEvents.cs new file mode 100644 index 0000000000..163eb19fdf --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Framework/WorkflowEvents.cs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Immutable; +using System.Linq; + +namespace Microsoft.Agents.Workflows.Declarative.IntegrationTests.Framework; + +internal sealed class WorkflowEvents +{ + public WorkflowEvents(ImmutableList workflowEvents) + { + this.Events = workflowEvents; + this.EventCounts = workflowEvents.GroupBy(e => e.GetType()).ToImmutableDictionary(e => e.Key, e => e.Count()); + this.ActionInvokeEvents = workflowEvents.OfType().ToImmutableList(); + this.ActionCompleteEvents = workflowEvents.OfType().ToImmutableList(); + } + + public ImmutableList Events { get; } + public IImmutableDictionary EventCounts { get; } + public ImmutableList ActionInvokeEvents { get; } + public ImmutableList ActionCompleteEvents { get; private set; } +} diff --git a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Framework/WorkflowHarness.cs b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Framework/WorkflowHarness.cs new file mode 100644 index 0000000000..c1b43a76e9 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Framework/WorkflowHarness.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Immutable; +using System.Linq; +using System.Threading.Tasks; + +namespace Microsoft.Agents.Workflows.Declarative.IntegrationTests.Framework; + +internal static class WorkflowHarness +{ + public static async Task RunAsync(Workflow workflow, TInput input) where TInput : notnull + { + StreamingRun run = await InProcessExecution.StreamAsync(workflow, input); + ImmutableList workflowEvents = run.WatchStreamAsync().ToEnumerable().ToImmutableList(); + return new WorkflowEvents(workflowEvents); + } +} diff --git a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Framework/WorkflowTest.cs b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Framework/WorkflowTest.cs new file mode 100644 index 0000000000..afc70f3603 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Framework/WorkflowTest.cs @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using Microsoft.Bot.ObjectModel; +using Xunit.Abstractions; + +namespace Microsoft.Agents.Workflows.Declarative.IntegrationTests.Framework; + +/// +/// Base class for workflow tests. +/// +public abstract class WorkflowTest : IDisposable +{ + public TestOutputAdapter Output { get; } + + protected WorkflowTest(ITestOutputHelper output) + { + this.Output = new TestOutputAdapter(output); + Console.SetOut(this.Output); + } + + public void Dispose() + { + this.Dispose(isDisposing: true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool isDisposing) + { + if (isDisposing) + { + this.Output.Dispose(); + } + } + + internal static string FormatVariablePath(string variableName, string? scope = null) => $"{scope ?? VariableScopeNames.Topic}.{variableName}"; +} diff --git a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Microsoft.Agents.Workflows.Declarative.IntegrationTests.csproj b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Microsoft.Agents.Workflows.Declarative.IntegrationTests.csproj new file mode 100644 index 0000000000..785992170e --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Microsoft.Agents.Workflows.Declarative.IntegrationTests.csproj @@ -0,0 +1,37 @@ + + + + $(ProjectsTargetFrameworks) + $(NoWarn);IDE1006 + + + + true + + + + + + + + + + + + + + + + + + Always + + + Always + + + Always + + + + diff --git a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Testcases/InvokeAgent.json b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Testcases/InvokeAgent.json new file mode 100644 index 0000000000..a48cc30b79 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Testcases/InvokeAgent.json @@ -0,0 +1,12 @@ +{ + "description": "Produce a single response from an agent.", + "setup": { + "input": { + "type": "String", + "value": "Why is the sky blue?" + } + }, + "validation": { + "action_count": 1 + } +} \ No newline at end of file diff --git a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Testcases/SendActivity.json b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Testcases/SendActivity.json new file mode 100644 index 0000000000..a0f6b74752 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Testcases/SendActivity.json @@ -0,0 +1,12 @@ +{ + "description": "Send an activity message .", + "setup": { + "input": { + "type": "String", + "value": "Why is the sky blue?" + } + }, + "validation": { + "action_count": 3 + } +} \ No newline at end of file diff --git a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Workflows/AddMessages.yaml b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Workflows/AddMessages.yaml new file mode 100644 index 0000000000..5a754c9be3 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Workflows/AddMessages.yaml @@ -0,0 +1,49 @@ +# +# This workflow demonstrates a conversation and message manipulation. +# +# Any Foundry Agent may be used to provide the response. +# See: ./setup/QuestionAgent.yaml +# +kind: AdaptiveDialog +beginDialog: + + kind: OnActivity + id: workflow_test + actions: + + - kind: CreateConversation + id: conversation_create1 + conversationId: Topic.FirstConversationId + + - kind: CreateConversation + id: conversation_create2 + conversationId: Topic.SecondConversationId + + - kind: SendActivity + id: sendActivity_conversation + activity: |- + Conversation 1: {Topic.FirstConversationId} + Conversation 2: {Topic.SecondConversationId} + + - kind: AddConversationMessage + id: add_message + message: Topic.MyMessage1 + role: User + conversationId: =Topic.FirstConversationId + content: + - type: Text + value: {System.LastMessage.Text} + + - kind: SendActivity + id: sendActivity_message + activity: |- + Messsage 1: {Topic.MyMessage1} + + - kind: CopyConversationMessages + id: copy_messages + conversationId: =Topic.SecondConversationId + messages: =[Topic.MyMessage1] + + - kind: SendActivity + id: sendActivity_copy + activity: Done! diff --git a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Workflows/GetMessage.yaml b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Workflows/GetMessage.yaml new file mode 100644 index 0000000000..94e8ba4d73 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Workflows/GetMessage.yaml @@ -0,0 +1,23 @@ +# +# This workflow demonstrates a conversation and message manipulation. +# +# Any Foundry Agent may be used to provide the response. +# See: ./setup/QuestionAgent.yaml +# +kind: AdaptiveDialog +beginDialog: + + kind: OnActivity + id: workflow_test + actions: + + - kind: RetrieveConversationMessage + id: get_message + message: Topic.MyMessage + conversationId: thread_T8xIzNrNcPkUkoCEGzxg80Vt + messageId: msg_J4x6YZTDUUWNs60FOUAucldy + + - kind: SendActivity + id: sendActivity_message + activity: |- + {Topic.MyMessage} diff --git a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Workflows/GetMessages.yaml b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Workflows/GetMessages.yaml new file mode 100644 index 0000000000..faaabfb1d1 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Workflows/GetMessages.yaml @@ -0,0 +1,22 @@ +# +# This workflow demonstrates a conversation and message manipulation. +# +# Any Foundry Agent may be used to provide the response. +# See: ./setup/QuestionAgent.yaml +# +kind: AdaptiveDialog +beginDialog: + + kind: OnActivity + id: workflow_test + actions: + + - kind: RetrieveConversationMessages + id: get_message + messages: Topic.MyMessages + conversationId: thread_T8xIzNrNcPkUkoCEGzxg80Vt + + - kind: SendActivity + id: sendActivity_message + activity: |- + {Topic.MyMessages} diff --git a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Workflows/InvokeAgent.yaml b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Workflows/InvokeAgent.yaml new file mode 100644 index 0000000000..ad9a2c3d57 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Workflows/InvokeAgent.yaml @@ -0,0 +1,15 @@ +kind: AdaptiveDialog +beginDialog: + + kind: OnActivity + id: workflow_test + actions: + + - kind: InvokeAzureAgent + id: invoke_agent + agent: + name: =Env.BasicAgent + input: + messages: =[UserMessage(System.LastMessageText)] + output: + messages: Topic.Answer diff --git a/workflows/HelloWorld.yaml b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Workflows/SendActivity.yaml similarity index 86% rename from workflows/HelloWorld.yaml rename to dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Workflows/SendActivity.yaml index 8fc434ed60..9095fe9655 100644 --- a/workflows/HelloWorld.yaml +++ b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Workflows/SendActivity.yaml @@ -7,14 +7,14 @@ kind: AdaptiveDialog beginDialog: kind: OnActivity - id: workflow_demo + id: workflow_test actions: # Capture input - kind: SetVariable id: setvar_userinput variable: Topic.UserInput - value: =System.LastMessageText + value: =System.LastMessage.Text # Capture environment variable - kind: SetVariable @@ -27,4 +27,4 @@ beginDialog: id: sendActivity_demo activity: |- Hello {Global.UserName}, - You said, "{Topic.UserInput}" \ No newline at end of file + You said, "{Topic.UserInput}" diff --git a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/DeclarativeWorkflowTest.cs b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/DeclarativeWorkflowTest.cs index fc7ee92cbf..59d7aa8d5c 100644 --- a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/DeclarativeWorkflowTest.cs +++ b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/DeclarativeWorkflowTest.cs @@ -135,6 +135,8 @@ public sealed class DeclarativeWorkflowTest(ITestOutputHelper output) : Workflow [InlineData("EditTable.yaml", 2, "edit_var")] [InlineData("EditTableV2.yaml", 2, "edit_var")] [InlineData("ParseValue.yaml", 1, "parse_var")] + [InlineData("SendActivity.yaml", 2, "activity_input")] + [InlineData("SetVariable.yaml", 1, "set_var")] [InlineData("SetTextVariable.yaml", 1, "set_text")] [InlineData("ClearAllVariables.yaml", 1, "clear_all")] [InlineData("ResetVariable.yaml", 2, "clear_var")] @@ -168,7 +170,6 @@ public sealed class DeclarativeWorkflowTest(ITestOutputHelper output) : Workflow [InlineData(typeof(InvokeSkillAction.Builder))] [InlineData(typeof(LogCustomTelemetryEvent.Builder))] [InlineData(typeof(OAuthInput.Builder))] - [InlineData(typeof(Question.Builder))] [InlineData(typeof(RecognizeIntent.Builder))] [InlineData(typeof(RepeatDialog.Builder))] [InlineData(typeof(ReplaceDialog.Builder))] @@ -192,17 +193,18 @@ public sealed class DeclarativeWorkflowTest(ITestOutputHelper output) : Workflow BeginDialog = new OnActivity.Builder() { - Id = "workflow", + Id = "anything", Actions = [unsupportedAction] } }; AdaptiveDialog dialog = dialogBuilder.Build(); - WorkflowScopes scopes = new(); + WorkflowFormulaState state = new(RecalcEngineFactory.Create()); Mock mockAgentProvider = new(MockBehavior.Strict); DeclarativeWorkflowOptions options = new(mockAgentProvider.Object); - WorkflowActionVisitor visitor = new(new RootExecutor(), new DeclarativeWorkflowState(RecalcEngineFactory.Create()), options); - WorkflowElementWalker walker = new(dialog, visitor); + WorkflowActionVisitor visitor = new(new RootExecutor(), state, options); + WorkflowElementWalker walker = new(visitor); + walker.Visit(dialog); Assert.True(visitor.HasUnsupportedActions); } @@ -226,7 +228,7 @@ public sealed class DeclarativeWorkflowTest(ITestOutputHelper output) : Workflow private void AssertMessage(string message) { - Assert.Contains(this.WorkflowEvents.OfType(), e => string.Equals(e.Response.Messages[0].Text.Trim(), message, StringComparison.Ordinal)); + Assert.Contains(this.WorkflowEvents.OfType(), e => string.Equals(e.Message.Trim(), message, StringComparison.Ordinal)); } private Task RunWorkflow(string workflowPath) => this.RunWorkflow(workflowPath, string.Empty); @@ -246,7 +248,7 @@ public sealed class DeclarativeWorkflowTest(ITestOutputHelper output) : Workflow { if (workflowEvent is ExecutorInvokedEvent invokeEvent) { - DeclarativeExecutorResult? message = invokeEvent.Data as DeclarativeExecutorResult; + ExecutorResultMessage? message = invokeEvent.Data as ExecutorResultMessage; this.Output.WriteLine($"EXEC: {invokeEvent.ExecutorId} << {message?.ExecutorId ?? "?"} [{message?.Result ?? "-"}]"); } else if (workflowEvent is AgentRunResponseEvent messageEvent) @@ -258,7 +260,7 @@ public sealed class DeclarativeWorkflowTest(ITestOutputHelper output) : Workflow } private sealed class RootExecutor() : - ReflectingExecutor(WorkflowActionVisitor.RootId("workflow")), + ReflectingExecutor(WorkflowActionVisitor.Steps.Root("anything")), IMessageHandler { public async ValueTask HandleAsync(string message, IWorkflowContext context) diff --git a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/Extensions/FormulaValueExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/Extensions/FormulaValueExtensionsTests.cs index bf450b41a2..a2ac6295dc 100644 --- a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/Extensions/FormulaValueExtensionsTests.cs +++ b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/Extensions/FormulaValueExtensionsTests.cs @@ -19,7 +19,7 @@ public class FormulaValueExtensionsTests BooleanDataValue typedValue = Assert.IsType(dataValue); Assert.Equal(formulaValue.Value, typedValue.Value); - BooleanValue formulaCopy = Assert.IsType(dataValue.ToFormulaValue()); + BooleanValue formulaCopy = Assert.IsType(dataValue.ToFormula()); Assert.Equal(typedValue.Value, formulaCopy.Value); Assert.Equal(bool.TrueString, formulaValue.Format()); @@ -35,7 +35,7 @@ public class FormulaValueExtensionsTests StringDataValue typedValue = Assert.IsType(dataValue); Assert.Equal(formulaValue.Value, typedValue.Value); - StringValue formulaCopy = Assert.IsType(typedValue.ToFormulaValue()); + StringValue formulaCopy = Assert.IsType(typedValue.ToFormula()); Assert.Equal(typedValue.Value, formulaCopy.Value); Assert.Equal(formulaValue.Value, formulaValue.Format()); @@ -51,7 +51,7 @@ public class FormulaValueExtensionsTests NumberDataValue typedValue = Assert.IsType(dataValue); Assert.Equal(formulaValue.Value, typedValue.Value); - DecimalValue formulaCopy = Assert.IsType(typedValue.ToFormulaValue()); + DecimalValue formulaCopy = Assert.IsType(typedValue.ToFormula()); Assert.Equal(typedValue.Value, formulaCopy.Value); Assert.Equal("45.3", formulaValue.Format()); @@ -67,7 +67,7 @@ public class FormulaValueExtensionsTests FloatDataValue typedValue = Assert.IsType(dataValue); Assert.Equal(formulaValue.Value, typedValue.Value); - NumberValue formulaCopy = Assert.IsType(typedValue.ToFormulaValue()); + NumberValue formulaCopy = Assert.IsType(typedValue.ToFormula()); Assert.Equal(typedValue.Value, formulaCopy.Value); Assert.Equal("3.1415926535897", formulaValue.Format()); @@ -103,7 +103,7 @@ public class FormulaValueExtensionsTests DateDataValue typedValue = Assert.IsType(dataValue); Assert.Equal(formulaValue.GetConvertedValue(TimeZoneInfo.Utc), typedValue.Value); - DateValue formulaCopy = Assert.IsType(dataValue.ToFormulaValue()); + DateValue formulaCopy = Assert.IsType(dataValue.ToFormula()); Assert.Equal(typedValue.Value, formulaCopy.GetConvertedValue(TimeZoneInfo.Utc)); Assert.Equal($"{timestamp}", formulaValue.Format()); @@ -120,7 +120,7 @@ public class FormulaValueExtensionsTests DateTimeDataValue typedValue = Assert.IsType(dataValue); Assert.Equal(formulaValue.GetConvertedValue(TimeZoneInfo.Utc), typedValue.Value); - DateTimeValue formulaCopy = Assert.IsType(typedValue.ToFormulaValue()); + DateTimeValue formulaCopy = Assert.IsType(typedValue.ToFormula()); Assert.Equal(typedValue.Value, formulaCopy.GetConvertedValue(TimeZoneInfo.Utc)); Assert.Equal($"{timestamp}", formulaValue.Format()); @@ -136,7 +136,7 @@ public class FormulaValueExtensionsTests TimeDataValue typedValue = Assert.IsType(dataValue); Assert.Equal(formulaValue.Value, typedValue.Value); - TimeValue formulaCopy = Assert.IsType(typedValue.ToFormulaValue()); + TimeValue formulaCopy = Assert.IsType(typedValue.ToFormula()); Assert.Equal(typedValue.Value, formulaCopy.Value); Assert.Equal("10:35:00", formulaValue.Format()); @@ -158,7 +158,7 @@ public class FormulaValueExtensionsTests Assert.Contains(property.Key, formulaValue.Fields.Select(field => field.Name)); } - RecordValue formulaCopy = Assert.IsType(dataValue.ToFormulaValue(), exactMatch: false); + RecordValue formulaCopy = Assert.IsType(dataValue.ToFormula(), exactMatch: false); Assert.Equal(formulaCopy.Fields.Count(), dataValue.Properties.Count); foreach (NamedValue field in formulaCopy.Fields) { @@ -174,6 +174,16 @@ public class FormulaValueExtensionsTests } """, formulaValue.Format().Replace(Environment.NewLine, "\n")); + + Dictionary source = + new() + { + ["FieldA"] = 1, + ["FieldB"] = 2, + ["FieldC"] = 3 + }; + FormulaValue formula = source.ToFormula(); + Assert.IsType(formula, exactMatch: false); } [Fact] @@ -188,7 +198,7 @@ public class FormulaValueExtensionsTests TableDataValue dataValue = formulaValue.ToTable(); Assert.Equal(formulaValue.Rows.Count(), dataValue.Values.Length); - TableValue formulaCopy = Assert.IsType(dataValue.ToFormulaValue(), exactMatch: false); + TableValue formulaCopy = Assert.IsType(dataValue.ToFormula(), exactMatch: false); Assert.Equal(formulaCopy.Rows.Count(), dataValue.Values.Length); Assert.Equal( diff --git a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/ObjectModel/ClearAllVariablesExecutorTest.cs b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/ObjectModel/ClearAllVariablesExecutorTest.cs index af3a31c735..7da024f4ce 100644 --- a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/ObjectModel/ClearAllVariablesExecutorTest.cs +++ b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/ObjectModel/ClearAllVariablesExecutorTest.cs @@ -17,7 +17,7 @@ public sealed class ClearAllVariablesExecutorTest(ITestOutputHelper output) : Wo public async Task ClearWorkflowScope() { // Arrange - this.Scopes.Set("NoVar", FormulaValue.New("Old value")); + this.State.Set("NoVar", FormulaValue.New("Old value")); ClearAllVariables model = this.CreateModel( @@ -25,7 +25,7 @@ public sealed class ClearAllVariablesExecutorTest(ITestOutputHelper output) : Wo VariablesToClear.ConversationScopedVariables); // Act - ClearAllVariablesExecutor action = new(model, this.GetState()); + ClearAllVariablesExecutor action = new(model, this.State); await this.Execute(action); // Assert @@ -43,7 +43,7 @@ public sealed class ClearAllVariablesExecutorTest(ITestOutputHelper output) : Wo VariablesToClear.UserScopedVariables); // Act - ClearAllVariablesExecutor action = new(model, this.GetState()); + ClearAllVariablesExecutor action = new(model, this.State); await this.Execute(action); // Assert diff --git a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/ObjectModel/ParseValueExecutorTest.cs b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/ObjectModel/ParseValueExecutorTest.cs index 35f8494d93..032f02cd05 100644 --- a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/ObjectModel/ParseValueExecutorTest.cs +++ b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/ObjectModel/ParseValueExecutorTest.cs @@ -32,7 +32,7 @@ public sealed class ParseValueExecutorTest(ITestOutputHelper output) : WorkflowA @"{ ""key1"": ""val1"" }"); // Act - ParseValueExecutor action = new(model, this.GetState()); + ParseValueExecutor action = new(model, this.State); await this.Execute(action); // Assert @@ -51,7 +51,7 @@ public sealed class ParseValueExecutorTest(ITestOutputHelper output) : WorkflowA "True"); // Act - ParseValueExecutor action = new(model, this.GetState()); + ParseValueExecutor action = new(model, this.State); await this.Execute(action); // Assert @@ -70,7 +70,7 @@ public sealed class ParseValueExecutorTest(ITestOutputHelper output) : WorkflowA "42"); // Act - ParseValueExecutor action = new(model, this.GetState()); + ParseValueExecutor action = new(model, this.State); await this.Execute(action); // Assert @@ -89,7 +89,7 @@ public sealed class ParseValueExecutorTest(ITestOutputHelper output) : WorkflowA "Hello, World!"); // Act - ParseValueExecutor action = new(model, this.GetState()); + ParseValueExecutor action = new(model, this.State); await this.Execute(action); // Assert diff --git a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/ObjectModel/ResetVariableExecutorTest.cs b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/ObjectModel/ResetVariableExecutorTest.cs index d705d4ebb8..b3e8d29b1b 100644 --- a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/ObjectModel/ResetVariableExecutorTest.cs +++ b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/ObjectModel/ResetVariableExecutorTest.cs @@ -17,8 +17,8 @@ public sealed class ResetVariableExecutorTest(ITestOutputHelper output) : Workfl public async Task ResetDefinedValue() { // Arrange - this.Scopes.Set("MyVar1", FormulaValue.New("Value #1")); - this.Scopes.Set("MyVar2", FormulaValue.New("Value #2")); + this.State.Set("MyVar1", FormulaValue.New("Value #1")); + this.State.Set("MyVar2", FormulaValue.New("Value #2")); ResetVariable model = this.CreateModel( @@ -26,7 +26,7 @@ public sealed class ResetVariableExecutorTest(ITestOutputHelper output) : Workfl FormatVariablePath("MyVar1")); // Act - ResetVariableExecutor action = new(model, this.GetState()); + ResetVariableExecutor action = new(model, this.State); await this.Execute(action); // Assert @@ -39,7 +39,7 @@ public sealed class ResetVariableExecutorTest(ITestOutputHelper output) : Workfl public async Task ResetUndefinedValue() { // Arrange - this.Scopes.Set("MyVar1", FormulaValue.New("Value #1")); + this.State.Set("MyVar1", FormulaValue.New("Value #1")); ResetVariable model = this.CreateModel( @@ -47,7 +47,7 @@ public sealed class ResetVariableExecutorTest(ITestOutputHelper output) : Workfl FormatVariablePath("NoVar")); // Act - ResetVariableExecutor action = new(model, this.GetState()); + ResetVariableExecutor action = new(model, this.State); await this.Execute(action); // Assert diff --git a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/ObjectModel/SendActivityExecutorTest.cs b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/ObjectModel/SendActivityExecutorTest.cs index 4337ef2952..8704e80257 100644 --- a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/ObjectModel/SendActivityExecutorTest.cs +++ b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/ObjectModel/SendActivityExecutorTest.cs @@ -22,12 +22,12 @@ public sealed class SendActivityExecutorTest(ITestOutputHelper output) : Workflo "Test activity message"); // Act - SendActivityExecutor action = new(model, this.GetState()); + SendActivityExecutor action = new(model, this.State); WorkflowEvent[] events = await this.Execute(action); // Assert this.VerifyModel(model, action); - Assert.Contains(events, e => e is AgentRunResponseEvent); + Assert.Contains(events, e => e is MessageActivityEvent); } private SendActivity CreateModel(string displayName, string activityMessage, string? summary = null) diff --git a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/ObjectModel/SetTextVariableExecutorTest.cs b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/ObjectModel/SetTextVariableExecutorTest.cs index 6ca809d1dd..50c547fda5 100644 --- a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/ObjectModel/SetTextVariableExecutorTest.cs +++ b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/ObjectModel/SetTextVariableExecutorTest.cs @@ -24,7 +24,7 @@ public sealed class SetTextVariableExecutorTest(ITestOutputHelper output) : Work "Text variable value"); // Act - SetTextVariableExecutor action = new(model, this.GetState()); + SetTextVariableExecutor action = new(model, this.State); await this.Execute(action); // Assert @@ -36,7 +36,7 @@ public sealed class SetTextVariableExecutorTest(ITestOutputHelper output) : Work public async Task UpdateExistingValue() { // Arrange - this.Scopes.Set("TextVar", FormulaValue.New("Old value")); + this.State.Set("TextVar", FormulaValue.New("Old value")); SetTextVariable model = this.CreateModel( @@ -45,7 +45,7 @@ public sealed class SetTextVariableExecutorTest(ITestOutputHelper output) : Work "New value"); // Act - SetTextVariableExecutor action = new(model, this.GetState()); + SetTextVariableExecutor action = new(model, this.State); await this.Execute(action); // Assert diff --git a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/ObjectModel/SetVariableExecutorTest.cs b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/ObjectModel/SetVariableExecutorTest.cs index a6ff29a597..fe9e195e08 100644 --- a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/ObjectModel/SetVariableExecutorTest.cs +++ b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/ObjectModel/SetVariableExecutorTest.cs @@ -17,7 +17,7 @@ public sealed class SetVariableExecutorTest(ITestOutputHelper output) : Workflow public void InvalidModel() { // Arrange, Act, Assert - Assert.Throws(() => new SetVariableExecutor(new SetVariable(), this.GetState())); + Assert.Throws(() => new SetVariableExecutor(new SetVariable(), this.State)); } [Fact] @@ -99,7 +99,7 @@ public sealed class SetVariableExecutorTest(ITestOutputHelper output) : Workflow public async Task SetBooleanVariable() { // Arrange - this.Scopes.Set("Source", FormulaValue.New(true)); + this.State.Set("Source", FormulaValue.New(true)); ValueExpression.Builder expressionBuilder = new(ValueExpression.Variable(PropertyPath.TopicVariable("Source"))); // Act, Assert @@ -114,7 +114,7 @@ public sealed class SetVariableExecutorTest(ITestOutputHelper output) : Workflow public async Task SetNumberVariable() { // Arrange - this.Scopes.Set("Source", FormulaValue.New(321)); + this.State.Set("Source", FormulaValue.New(321)); ValueExpression.Builder expressionBuilder = new(ValueExpression.Variable(PropertyPath.TopicVariable("Source"))); // Act, Assert @@ -129,7 +129,7 @@ public sealed class SetVariableExecutorTest(ITestOutputHelper output) : Workflow public async Task SetStringVariable() { // Arrange - this.Scopes.Set("Source", FormulaValue.New("Test")); + this.State.Set("Source", FormulaValue.New("Test")); ValueExpression.Builder expressionBuilder = new(ValueExpression.Variable(PropertyPath.TopicVariable("Source"))); // Act, Assert @@ -144,7 +144,7 @@ public sealed class SetVariableExecutorTest(ITestOutputHelper output) : Workflow public async Task UpdateExistingValue() { // Arrange - this.Scopes.Set("VarA", FormulaValue.New(33)); + this.State.Set("VarA", FormulaValue.New(33)); // Act, Assert await this.ExecuteTest( @@ -180,10 +180,10 @@ public sealed class SetVariableExecutorTest(ITestOutputHelper output) : Workflow FormatVariablePath(variableName), valueExpression); - this.Scopes.Set(variableName, FormulaValue.New(33)); + this.State.Set(variableName, FormulaValue.New(33)); // Act - SetVariableExecutor action = new(model, this.GetState()); + SetVariableExecutor action = new(model, this.State); await this.Execute(action); // Assert diff --git a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/ObjectModel/WorkflowActionExecutorTest.cs b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/ObjectModel/WorkflowActionExecutorTest.cs index 8d7393864b..26cb44f243 100644 --- a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/ObjectModel/WorkflowActionExecutorTest.cs +++ b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/ObjectModel/WorkflowActionExecutorTest.cs @@ -14,31 +14,29 @@ using Xunit.Abstractions; namespace Microsoft.Agents.Workflows.Declarative.UnitTests.ObjectModel; /// -/// Base test class for implementations. +/// Base test class for implementations. /// public abstract class WorkflowActionExecutorTest(ITestOutputHelper output) : WorkflowTest(output) { - internal WorkflowScopes Scopes { get; } = new(); - - internal DeclarativeWorkflowState GetState() => new(RecalcEngineFactory.Create(), this.Scopes); + internal WorkflowFormulaState State { get; } = new(RecalcEngineFactory.Create()); protected ActionId CreateActionId() => new($"{this.GetType().Name}_{Guid.NewGuid():N}"); protected string FormatDisplayName(string name) => $"{this.GetType().Name}_{name}"; - internal async Task Execute(WorkflowActionExecutor executor) + internal async Task Execute(DeclarativeActionExecutor executor) { TestWorkflowExecutor workflowExecutor = new(); WorkflowBuilder workflowBuilder = new(workflowExecutor); workflowBuilder.AddEdge(workflowExecutor, executor); - StreamingRun run = await InProcessExecution.StreamAsync(workflowBuilder.Build(), this.Scopes); + StreamingRun run = await InProcessExecution.StreamAsync(workflowBuilder.Build(), this.State); WorkflowEvent[] events = await run.WatchStreamAsync().ToArrayAsync(); Assert.Contains(events, e => e is DeclarativeActionInvokeEvent); Assert.Contains(events, e => e is DeclarativeActionCompleteEvent); return events; } - internal void VerifyModel(DialogAction model, WorkflowActionExecutor action) + internal void VerifyModel(DialogAction model, DeclarativeActionExecutor action) { Assert.Equal(model.Id, action.Id); Assert.Equal(model, action.Model); @@ -48,7 +46,7 @@ public abstract class WorkflowActionExecutorTest(ITestOutputHelper output) : Wor internal void VerifyState(string variableName, string scopeName, FormulaValue expectedValue) { - FormulaValue actualValue = this.Scopes.Get(variableName, scopeName); + FormulaValue actualValue = this.State.Get(variableName, scopeName); Assert.Equal(expectedValue.Format(), actualValue.Format()); } @@ -56,7 +54,7 @@ public abstract class WorkflowActionExecutorTest(ITestOutputHelper output) : Wor internal void VerifyUndefined(string variableName, string scopeName) { - Assert.IsType(this.Scopes.Get(variableName, scopeName)); + Assert.IsType(this.State.Get(variableName, scopeName)); } protected TAction AssignParent(DialogAction.Builder actionBuilder) where TAction : DialogAction @@ -76,11 +74,11 @@ public abstract class WorkflowActionExecutorTest(ITestOutputHelper output) : Wor internal sealed class TestWorkflowExecutor() : ReflectingExecutor(nameof(TestWorkflowExecutor)), - IMessageHandler + IMessageHandler { - public async ValueTask HandleAsync(WorkflowScopes message, IWorkflowContext context) + public async ValueTask HandleAsync(WorkflowFormulaState message, IWorkflowContext context) { - await context.SendMessageAsync(new DeclarativeExecutorResult(this.Id)).ConfigureAwait(false); + await context.SendMessageAsync(new ExecutorResultMessage(this.Id)).ConfigureAwait(false); } } } diff --git a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/PowerFx/RecalcEngineFactoryTests.cs b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/PowerFx/RecalcEngineFactoryTests.cs index 690a20614c..e247f0b9bb 100644 --- a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/PowerFx/RecalcEngineFactoryTests.cs +++ b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/PowerFx/RecalcEngineFactoryTests.cs @@ -10,12 +10,12 @@ using Xunit.Abstractions; namespace Microsoft.Agents.Workflows.Declarative.UnitTests.PowerFx; -public class RecalcEngineFactoryTests(ITestOutputHelper output) : RecalcEngineTest(output) +public class RecalcEngineFactoryTests(ITestOutputHelper output) : WorkflowTest(output) { [Fact] public void VariableUpdateTest() { - RecalcEngine engine = this.CreateEngine(); + RecalcEngine engine = RecalcEngineFactory.Create(); FormulaValue evalResult; @@ -60,7 +60,7 @@ public class RecalcEngineFactoryTests(ITestOutputHelper output) : RecalcEngineTe public void DefaultNotNull() { // Act - RecalcEngine engine = this.CreateEngine(); + RecalcEngine engine = RecalcEngineFactory.Create(); // Assert Assert.NotNull(engine); @@ -70,8 +70,8 @@ public class RecalcEngineFactoryTests(ITestOutputHelper output) : RecalcEngineTe public void NewInstanceEachTime() { // Act - RecalcEngine engine1 = this.CreateEngine(); - RecalcEngine engine2 = this.CreateEngine(); + RecalcEngine engine1 = RecalcEngineFactory.Create(); + RecalcEngine engine2 = RecalcEngineFactory.Create(); // Assert Assert.NotNull(engine1); @@ -83,7 +83,7 @@ public class RecalcEngineFactoryTests(ITestOutputHelper output) : RecalcEngineTe public void HasSetFunctionEnabled() { // Arrange - RecalcEngine engine = this.CreateEngine(); + RecalcEngine engine = RecalcEngineFactory.Create(); // Act CheckResult result = engine.Check("1+1"); diff --git a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/PowerFx/RecalcEngineTest.cs b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/PowerFx/RecalcEngineTest.cs index db849323aa..8321ce0d1b 100644 --- a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/PowerFx/RecalcEngineTest.cs +++ b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/PowerFx/RecalcEngineTest.cs @@ -11,7 +11,7 @@ namespace Microsoft.Agents.Workflows.Declarative.UnitTests.PowerFx; /// public abstract class RecalcEngineTest(ITestOutputHelper output) : WorkflowTest(output) { - internal WorkflowScopes Scopes { get; } = new(); + internal WorkflowFormulaState State { get; } = new(RecalcEngineFactory.Create()); - protected RecalcEngine CreateEngine(int maximumExpressionLength = 500) => RecalcEngineFactory.Create(maximumExpressionLength); + protected RecalcEngine Engine => this.State.Engine; } diff --git a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/PowerFx/TemplateExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/PowerFx/TemplateExtensionsTests.cs index cd2f6c05ea..84136e9fab 100644 --- a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/PowerFx/TemplateExtensionsTests.cs +++ b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/PowerFx/TemplateExtensionsTests.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using Microsoft.Agents.Workflows.Declarative.Extensions; using Microsoft.Bot.ObjectModel; -using Microsoft.PowerFx; using Microsoft.PowerFx.Types; using Xunit.Abstractions; @@ -21,10 +20,9 @@ public class TemplateExtensionsTests(ITestOutputHelper output) : RecalcEngineTes TemplateLine.Parse(" "), TemplateLine.Parse("World"), ]; - RecalcEngine engine = this.CreateEngine(); // Act - string? result = engine.Format(template); + string? result = this.Engine.Format(template); // Assert Assert.Equal("Hello World", result); @@ -35,10 +33,9 @@ public class TemplateExtensionsTests(ITestOutputHelper output) : RecalcEngineTes { // Arrange List template = []; - RecalcEngine engine = this.CreateEngine(); // Act - string? result = engine.Format(template); + string? result = this.Engine.Format(template); // Assert Assert.Equal(string.Empty, result); @@ -49,10 +46,9 @@ public class TemplateExtensionsTests(ITestOutputHelper output) : RecalcEngineTes { // Arrange TemplateLine line = TemplateLine.Parse("Test"); - RecalcEngine engine = this.CreateEngine(); // Act - string? result = engine.Format(line); + string? result = this.Engine.Format(line); // Assert Assert.Equal("Test", result); @@ -63,10 +59,9 @@ public class TemplateExtensionsTests(ITestOutputHelper output) : RecalcEngineTes { // Arrange TemplateLine? line = null; - RecalcEngine engine = this.CreateEngine(); // Act - string? result = engine.Format(line); + string? result = this.Engine.Format(line); // Assert Assert.Equal(string.Empty, result); @@ -78,10 +73,9 @@ public class TemplateExtensionsTests(ITestOutputHelper output) : RecalcEngineTes // Arrange TemplateSegment textSegment = TextSegment.FromText("Hello World"); TemplateLine line = new([textSegment]); - RecalcEngine engine = this.CreateEngine(); // Act - string? result = engine.Format(line); + string? result = this.Engine.Format(line); // Assert Assert.Equal("Hello World", result); @@ -93,10 +87,9 @@ public class TemplateExtensionsTests(ITestOutputHelper output) : RecalcEngineTes // Arrange ExpressionSegment expressionSegment = new(ValueExpression.Expression("1 + 1")); TemplateLine line = new([expressionSegment]); - RecalcEngine engine = this.CreateEngine(); // Act - string? result = engine.Format(line); + string? result = this.Engine.Format(line); // Assert Assert.Equal("2", result); @@ -106,14 +99,12 @@ public class TemplateExtensionsTests(ITestOutputHelper output) : RecalcEngineTes public void FormatVariableSegment() { // Arrange - this.Scopes.Set("Source", FormulaValue.New("Hello World")); + this.State.Set("Source", FormulaValue.New("Hello World")); ExpressionSegment expressionSegment = new(ValueExpression.Variable(PropertyPath.TopicVariable("Source"))); TemplateLine line = new([expressionSegment]); - RecalcEngine engine = this.CreateEngine(); - this.Scopes.Bind(engine); // Act - string? result = engine.Format(line); + string? result = this.Engine.Format(line); // Assert Assert.Equal("Hello World", result); @@ -125,10 +116,9 @@ public class TemplateExtensionsTests(ITestOutputHelper output) : RecalcEngineTes // Arrange ExpressionSegment expressionSegment = new(); TemplateLine line = new([expressionSegment]); - RecalcEngine engine = this.CreateEngine(); // Act & Assert - Assert.Throws(() => engine.Format(line)); + Assert.Throws(() => this.Engine.Format(line)); } [Fact] @@ -138,10 +128,9 @@ public class TemplateExtensionsTests(ITestOutputHelper output) : RecalcEngineTes TemplateSegment textSegment = TextSegment.FromText("Hello "); ExpressionSegment expressionSegment = new(ValueExpression.Expression(@"""World""")); TemplateLine line = new([textSegment, expressionSegment]); - RecalcEngine engine = this.CreateEngine(); // Act - string? result = engine.Format(line); + string? result = this.Engine.Format(line); // Assert Assert.Equal("Hello World", result); diff --git a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/PowerFx/WorkflowExpressionEngineTests.cs b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/PowerFx/WorkflowExpressionEngineTests.cs index 53cd9983f5..c12396b395 100644 --- a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/PowerFx/WorkflowExpressionEngineTests.cs +++ b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/PowerFx/WorkflowExpressionEngineTests.cs @@ -7,7 +7,6 @@ using Microsoft.Agents.Workflows.Declarative.PowerFx; using Microsoft.Bot.ObjectModel; using Microsoft.Bot.ObjectModel.Abstractions; using Microsoft.Bot.ObjectModel.Exceptions; -using Microsoft.PowerFx; using Microsoft.PowerFx.Types; using Xunit.Abstractions; @@ -34,39 +33,17 @@ public class WorkflowExpressionEngineTests : RecalcEngineTest public WorkflowExpressionEngineTests(ITestOutputHelper output) : base(output) { - this.Scopes.Set(Variables.GlobalValue, FormulaValue.New(255), VariableScopeNames.Global); - this.Scopes.Set(Variables.BoolValue, FormulaValue.New(true), VariableScopeNames.Topic); - this.Scopes.Set(Variables.StringValue, FormulaValue.New("Hello World"), VariableScopeNames.Topic); - this.Scopes.Set(Variables.IntValue, FormulaValue.New(long.MaxValue), VariableScopeNames.Topic); - this.Scopes.Set(Variables.NumberValue, FormulaValue.New(33.3), VariableScopeNames.Topic); - this.Scopes.Set(Variables.EnumValue, FormulaValue.New(nameof(VariablesToClear.ConversationScopedVariables)), VariableScopeNames.Topic); - this.Scopes.Set(Variables.ObjectValue, ObjectData, VariableScopeNames.Topic); - this.Scopes.Set(Variables.ArrayValue, TableData, VariableScopeNames.Topic); - this.Scopes.Set(Variables.BlankValue, FormulaValue.NewBlank(), VariableScopeNames.Topic); + this.State.Set(Variables.GlobalValue, FormulaValue.New(255), VariableScopeNames.Global); + this.State.Set(Variables.BoolValue, FormulaValue.New(true), VariableScopeNames.Topic); + this.State.Set(Variables.StringValue, FormulaValue.New("Hello World"), VariableScopeNames.Topic); + this.State.Set(Variables.IntValue, FormulaValue.New(long.MaxValue), VariableScopeNames.Topic); + this.State.Set(Variables.NumberValue, FormulaValue.New(33.3), VariableScopeNames.Topic); + this.State.Set(Variables.EnumValue, FormulaValue.New(nameof(VariablesToClear.ConversationScopedVariables)), VariableScopeNames.Topic); + this.State.Set(Variables.ObjectValue, ObjectData, VariableScopeNames.Topic); + this.State.Set(Variables.ArrayValue, TableData, VariableScopeNames.Topic); + this.State.Set(Variables.BlankValue, FormulaValue.NewBlank(), VariableScopeNames.Topic); } - #region Unsupported Expression Tests - - [Fact] - public void AdaptiveCardExpressionGetValueUnsupported() - { - this.EvaluateUnsupportedExpression(expressionEngine => expressionEngine.GetValue(AdaptiveCardExpression.Variable(PropertyPath.TopicVariable(Variables.StringValue)), this.Scopes.BuildState())); - } - - [Fact] - public void DialogExpressionGetValueUnsupported() - { - this.EvaluateUnsupportedExpression(expressionEngine => expressionEngine.GetValue(DialogExpression.Variable(PropertyPath.TopicVariable(Variables.StringValue)), this.Scopes.BuildState())); - } - - [Fact] - public void FileExpressionGetValueUnsupported() - { - this.EvaluateUnsupportedExpression(expressionEngine => expressionEngine.GetValue(FileExpression.Variable(PropertyPath.TopicVariable(Variables.StringValue)), this.Scopes.BuildState())); - } - - #endregion - #region BoolExpression Tests [Fact] @@ -101,16 +78,13 @@ public class WorkflowExpressionEngineTests : RecalcEngineTest expectedValue: false); } - [Theory] - [InlineData(false)] - [InlineData(true)] - public void BoolExpressionGetValueForVariable(bool useState) + [Fact] + public void BoolExpressionGetValueForVariable() { // Arrange, Act & Assert this.EvaluateExpression( BoolExpression.Variable(PropertyPath.TopicVariable(Variables.BoolValue)), - expectedValue: true, - useState); + expectedValue: true); } [Fact] @@ -158,16 +132,13 @@ public class WorkflowExpressionEngineTests : RecalcEngineTest expectedValue: "test"); } - [Theory] - [InlineData(false)] - [InlineData(true)] - public void StringExpressionGetValueForVariable(bool useState) + [Fact] + public void StringExpressionGetValueForVariable() { // Arrange, Act & Assert this.EvaluateExpression( StringExpression.Variable(PropertyPath.TopicVariable(Variables.StringValue)), - expectedValue: "Hello World", - useState); + expectedValue: "Hello World"); } [Fact] @@ -184,7 +155,7 @@ public class WorkflowExpressionEngineTests : RecalcEngineTest { // Arrange RecordValue state = FormulaValue.NewRecordFromFields([new NamedValue("test", FormulaValue.New("value"))]); - this.Scopes.Set("TestRecord", state, VariableScopeNames.Global); + this.State.Set("TestRecord", state, VariableScopeNames.Global); // Arrange, Act & Assert this.EvaluateExpression( @@ -233,16 +204,13 @@ public class WorkflowExpressionEngineTests : RecalcEngineTest expectedValue: 7); } - [Theory] - [InlineData(false)] - [InlineData(true)] - public void IntExpressionGetValueForVariable(bool useState) + [Fact] + public void IntExpressionGetValueForVariable() { // Arrange, Act & Assert this.EvaluateExpression( IntExpression.Variable(PropertyPath.TopicVariable(Variables.IntValue)), - expectedValue: long.MaxValue, - useState); + expectedValue: long.MaxValue); } [Fact] @@ -337,16 +305,13 @@ public class WorkflowExpressionEngineTests : RecalcEngineTest expectedValue: DataValue.Create("test")); } - [Theory] - [InlineData(false)] - [InlineData(true)] - public void DataValueExpressionGetValueForVariable(bool useState) + [Fact] + public void DataValueExpressionGetValueForVariable() { // Arrange, Act & Assert this.EvaluateExpression( ValueExpression.Variable(PropertyPath.TopicVariable(Variables.StringValue)), - expectedValue: DataValue.Create("Hello World"), - useState); + expectedValue: DataValue.Create("Hello World")); } [Fact] @@ -394,16 +359,13 @@ public class WorkflowExpressionEngineTests : RecalcEngineTest expectedValue: VariablesToClear.ConversationScopedVariables); } - [Theory] - [InlineData(false)] - [InlineData(true)] - public void EnumExpressionGetValueForVariable(bool useState) + [Fact] + public void EnumExpressionGetValueForVariable() { // Arrange, Act & Assert this.EvaluateExpression( EnumExpression.Variable(PropertyPath.TopicVariable(Variables.EnumValue)), - expectedValue: VariablesToClear.ConversationScopedVariables, - useState); + expectedValue: VariablesToClear.ConversationScopedVariables); } [Fact] @@ -455,16 +417,13 @@ public class WorkflowExpressionEngineTests : RecalcEngineTest expectedValue: null); } - [Theory] - [InlineData(false)] - [InlineData(true)] - public void ObjectExpressionGetValueForVariable(bool useState) + [Fact] + public void ObjectExpressionGetValueForVariable() { // Arrange, Act & Assert this.EvaluateExpression( ObjectExpression.Variable(PropertyPath.TopicVariable(Variables.ObjectValue)), - expectedValue: ObjectData.ToRecord(), - useState); + expectedValue: ObjectData.ToRecord()); } #endregion @@ -504,16 +463,13 @@ public class WorkflowExpressionEngineTests : RecalcEngineTest expectedValue: []); } - [Theory] - [InlineData(false)] - [InlineData(true)] - public void ArrayExpressionGetValueForVariable(bool useState) + [Fact] + public void ArrayExpressionGetValueForVariable() { // Arrange, Act & Assert this.EvaluateExpression( ArrayExpression.Variable(PropertyPath.TopicVariable(Variables.ArrayValue)), - expectedValue: ["a", "b"], - useState); + expectedValue: ["a", "b"]); } [Fact] @@ -552,16 +508,13 @@ public class WorkflowExpressionEngineTests : RecalcEngineTest expectedValue: []); } - [Theory] - [InlineData(false)] - [InlineData(true)] - public void ArrayExpressionOnlyGetValueForVariable(bool useState) + [Fact] + public void ArrayExpressionOnlyGetValueForVariable() { // Arrange, Act & Assert this.EvaluateExpression( ArrayExpressionOnly.Variable(PropertyPath.TopicVariable(Variables.ArrayValue)), - expectedValue: ["a", "b"], - useState); + expectedValue: ["a", "b"]); } [Fact] @@ -575,85 +528,80 @@ public class WorkflowExpressionEngineTests : RecalcEngineTest #endregion - private EvaluationResult EvaluateExpression(BoolExpression expression, bool expectedValue, bool useState = false, SensitivityLevel expectedSensitivity = SensitivityLevel.None) - => this.EvaluateExpression((evaluator) => useState ? evaluator.GetValue(expression, this.Scopes) : evaluator.GetValue(expression, this.Scopes.BuildState()), expectedValue, expectedSensitivity); + private EvaluationResult EvaluateExpression(BoolExpression expression, bool expectedValue, SensitivityLevel expectedSensitivity = SensitivityLevel.None) + => this.EvaluateExpression((evaluator) => evaluator.GetValue(expression), expectedValue, expectedSensitivity); private void EvaluateInvalidExpression(BoolExpression expression) where TException : Exception - => this.EvaluateInvalidExpression((evaluator) => evaluator.GetValue(expression, this.Scopes)); + => this.EvaluateInvalidExpression((evaluator) => evaluator.GetValue(expression)); - private EvaluationResult EvaluateExpression(StringExpression expression, string expectedValue, bool useState = false, SensitivityLevel expectedSensitivity = SensitivityLevel.None) - => this.EvaluateExpression((evaluator) => useState ? evaluator.GetValue(expression, this.Scopes) : evaluator.GetValue(expression, this.Scopes.BuildState()), expectedValue, expectedSensitivity); + private EvaluationResult EvaluateExpression(StringExpression expression, string expectedValue, SensitivityLevel expectedSensitivity = SensitivityLevel.None) + => this.EvaluateExpression((evaluator) => evaluator.GetValue(expression), expectedValue, expectedSensitivity); private void EvaluateInvalidExpression(StringExpression expression) where TException : Exception - => this.EvaluateInvalidExpression((evaluator) => evaluator.GetValue(expression, this.Scopes)); + => this.EvaluateInvalidExpression((evaluator) => evaluator.GetValue(expression)); - private EvaluationResult EvaluateExpression(IntExpression expression, long expectedValue, bool useState = false, SensitivityLevel expectedSensitivity = SensitivityLevel.None) - => this.EvaluateExpression((evaluator) => useState ? evaluator.GetValue(expression, this.Scopes) : evaluator.GetValue(expression, this.Scopes.BuildState()), expectedValue, expectedSensitivity); + private EvaluationResult EvaluateExpression(IntExpression expression, long expectedValue, SensitivityLevel expectedSensitivity = SensitivityLevel.None) + => this.EvaluateExpression((evaluator) => evaluator.GetValue(expression), expectedValue, expectedSensitivity); private void EvaluateInvalidExpression(IntExpression expression) where TException : Exception - => this.EvaluateInvalidExpression((evaluator) => evaluator.GetValue(expression, this.Scopes)); + => this.EvaluateInvalidExpression((evaluator) => evaluator.GetValue(expression)); - private EvaluationResult EvaluateExpression(NumberExpression expression, double expectedValue, bool useState = false, SensitivityLevel expectedSensitivity = SensitivityLevel.None) - => this.EvaluateExpression((evaluator) => useState ? evaluator.GetValue(expression, this.Scopes) : evaluator.GetValue(expression, this.Scopes.BuildState()), expectedValue, expectedSensitivity); + private EvaluationResult EvaluateExpression(NumberExpression expression, double expectedValue, SensitivityLevel expectedSensitivity = SensitivityLevel.None) + => this.EvaluateExpression((evaluator) => evaluator.GetValue(expression), expectedValue, expectedSensitivity); private void EvaluateInvalidExpression(NumberExpression expression) where TException : Exception - => this.EvaluateInvalidExpression((evaluator) => evaluator.GetValue(expression, this.Scopes)); + => this.EvaluateInvalidExpression((evaluator) => evaluator.GetValue(expression)); - private EvaluationResult EvaluateExpression(ValueExpression expression, DataValue expectedValue, bool useState = false, SensitivityLevel expectedSensitivity = SensitivityLevel.None) - => this.EvaluateExpression((evaluator) => useState ? evaluator.GetValue(expression, this.Scopes) : evaluator.GetValue(expression, this.Scopes.BuildState()), expectedValue, expectedSensitivity); + private EvaluationResult EvaluateExpression(ValueExpression expression, DataValue expectedValue, SensitivityLevel expectedSensitivity = SensitivityLevel.None) + => this.EvaluateExpression((evaluator) => evaluator.GetValue(expression), expectedValue, expectedSensitivity); private void EvaluateInvalidExpression(ValueExpression expression) where TException : Exception - => this.EvaluateInvalidExpression((evaluator) => evaluator.GetValue(expression, this.Scopes)); + => this.EvaluateInvalidExpression((evaluator) => evaluator.GetValue(expression)); - private EvaluationResult EvaluateExpression(EnumExpression expression, TEnum expectedValue, bool useState = false, SensitivityLevel expectedSensitivity = SensitivityLevel.None) + private EvaluationResult EvaluateExpression(EnumExpression expression, TEnum expectedValue, SensitivityLevel expectedSensitivity = SensitivityLevel.None) where TEnum : EnumWrapper - => this.EvaluateExpression((evaluator) => useState ? evaluator.GetValue(expression, this.Scopes) : evaluator.GetValue(expression, this.Scopes.BuildState()), expectedValue, expectedSensitivity); + => this.EvaluateExpression((evaluator) => evaluator.GetValue(expression), expectedValue, expectedSensitivity); private void EvaluateInvalidExpression(EnumExpression expression) where TException : Exception where TEnum : EnumWrapper - => this.EvaluateInvalidExpression((evaluator) => evaluator.GetValue(expression, this.Scopes)); + => this.EvaluateInvalidExpression((evaluator) => evaluator.GetValue(expression)); - private EvaluationResult EvaluateExpression(ObjectExpression expression, TValue? expectedValue, bool useState = false, SensitivityLevel expectedSensitivity = SensitivityLevel.None) + private EvaluationResult EvaluateExpression(ObjectExpression expression, TValue? expectedValue, SensitivityLevel expectedSensitivity = SensitivityLevel.None) where TValue : BotElement - => this.EvaluateExpression((evaluator) => useState ? evaluator.GetValue(expression, this.Scopes) : evaluator.GetValue(expression, this.Scopes.BuildState()), expectedValue, expectedSensitivity); + => this.EvaluateExpression((evaluator) => evaluator.GetValue(expression), expectedValue, expectedSensitivity); private void EvaluateInvalidExpression(ObjectExpression expression) where TException : Exception where TValue : BotElement - => this.EvaluateInvalidExpression((evaluator) => evaluator.GetValue(expression, this.Scopes)); + => this.EvaluateInvalidExpression((evaluator) => evaluator.GetValue(expression)); - private ImmutableArray EvaluateExpression(ArrayExpression expression, TValue[] expectedValue, bool useState = false) - => this.EvaluateArrayExpression((evaluator) => useState ? evaluator.GetValue(expression, this.Scopes) : evaluator.GetValue(expression, this.Scopes.BuildState()), expectedValue); + private ImmutableArray EvaluateExpression(ArrayExpression expression, TValue[] expectedValue) + => this.EvaluateArrayExpression((evaluator) => evaluator.GetValue(expression), expectedValue); private void EvaluateInvalidExpression(ArrayExpression expression) where TException : Exception - => this.EvaluateInvalidExpression((evaluator) => evaluator.GetValue(expression, this.Scopes)); + => this.EvaluateInvalidExpression((evaluator) => evaluator.GetValue(expression)); - private ImmutableArray EvaluateExpression(ArrayExpressionOnly expression, TValue[] expectedValue, bool useState = false) - => this.EvaluateArrayExpression((evaluator) => useState ? evaluator.GetValue(expression, this.Scopes) : evaluator.GetValue(expression, this.Scopes.BuildState()), expectedValue); + private ImmutableArray EvaluateExpression(ArrayExpressionOnly expression, TValue[] expectedValue) + => this.EvaluateArrayExpression((evaluator) => evaluator.GetValue(expression), expectedValue); private void EvaluateInvalidExpression(ArrayExpressionOnly expression) where TException : Exception - => this.EvaluateInvalidExpression((evaluator) => evaluator.GetValue(expression, this.Scopes)); + => this.EvaluateInvalidExpression((evaluator) => evaluator.GetValue(expression)); private EvaluationResult EvaluateExpression( Func> evaluator, TValue? expectedValue, SensitivityLevel expectedSensitivity = SensitivityLevel.None) { - // Arrange - RecalcEngine engine = this.CreateEngine(); - this.Scopes.Bind(engine); - WorkflowExpressionEngine expressionEngine = new(engine); - // Act - EvaluationResult result = evaluator.Invoke(expressionEngine); + EvaluationResult result = evaluator.Invoke(this.State.Evaluator); // Assert Assert.Equal(expectedValue, result.Value); @@ -666,13 +614,8 @@ public class WorkflowExpressionEngineTests : RecalcEngineTest Func> evaluator, TValue[] expectedValue) { - // Arrange - RecalcEngine engine = this.CreateEngine(); - this.Scopes.Bind(engine); - WorkflowExpressionEngine expressionEngine = new(engine); - // Act - ImmutableArray result = evaluator.Invoke(expressionEngine); + ImmutableArray result = evaluator.Invoke(this.State.Evaluator); // Assert Assert.Equal(expectedValue.Length, result.Length); @@ -683,23 +626,7 @@ public class WorkflowExpressionEngineTests : RecalcEngineTest private void EvaluateInvalidExpression(Action evaluator) where TException : Exception { - // Arrange - RecalcEngine engine = this.CreateEngine(); - this.Scopes.Bind(engine); - WorkflowExpressionEngine expressionEngine = new(engine); - - // Act - Assert.Throws(() => evaluator.Invoke(expressionEngine)); - } - - private void EvaluateUnsupportedExpression(Action evaluator) - { - // Arrange - RecalcEngine engine = this.CreateEngine(); - this.Scopes.Bind(engine); - WorkflowExpressionEngine expressionEngine = new(engine); - - // Act - Assert.Throws(() => evaluator.Invoke(expressionEngine)); + // Act & Assert + Assert.Throws(() => evaluator.Invoke(this.State.Evaluator)); } } diff --git a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/PowerFx/WorkflowScopesTests.cs b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/PowerFx/WorkflowScopesTests.cs index c09a448656..606e21c327 100644 --- a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/PowerFx/WorkflowScopesTests.cs +++ b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/PowerFx/WorkflowScopesTests.cs @@ -9,17 +9,16 @@ namespace Microsoft.Agents.Workflows.Declarative.UnitTests.PowerFx; public class WorkflowScopesTests { + internal WorkflowFormulaState State { get; } = new(RecalcEngineFactory.Create()); + [Fact] public void ConstructorInitializesAllScopes() { - // Arrange & Act - WorkflowScopes scopes = new(); - - // Assert - RecordValue envRecord = scopes.BuildRecord(VariableScopeNames.Environment); - RecordValue topicRecord = scopes.BuildRecord(VariableScopeNames.Topic); - RecordValue globalRecord = scopes.BuildRecord(VariableScopeNames.Global); - RecordValue systemRecord = scopes.BuildRecord(VariableScopeNames.System); + // Act & Assert + RecordValue envRecord = this.State.BuildRecord(VariableScopeNames.Environment); + RecordValue topicRecord = this.State.BuildRecord(VariableScopeNames.Topic); + RecordValue globalRecord = this.State.BuildRecord(VariableScopeNames.Global); + RecordValue systemRecord = this.State.BuildRecord(VariableScopeNames.System); Assert.NotNull(envRecord); Assert.NotNull(topicRecord); @@ -30,11 +29,8 @@ public class WorkflowScopesTests [Fact] public void BuildRecordWhenEmpty() { - // Arrange - WorkflowScopes scopes = new(); - // Act - RecordValue record = scopes.BuildRecord(VariableScopeNames.Topic); + RecordValue record = this.State.BuildRecord(VariableScopeNames.Topic); // Assert Assert.NotNull(record); @@ -45,12 +41,11 @@ public class WorkflowScopesTests public void BuildRecordContainsSetValues() { // Arrange - WorkflowScopes scopes = new(); FormulaValue testValue = FormulaValue.New("test"); - scopes.Set("key1", testValue, VariableScopeNames.Topic); + this.State.Set("key1", testValue, VariableScopeNames.Topic); // Act - RecordValue record = scopes.BuildRecord(VariableScopeNames.Topic); + RecordValue record = this.State.BuildRecord(VariableScopeNames.Topic); // Assert Assert.NotNull(record); @@ -63,24 +58,23 @@ public class WorkflowScopesTests public void BuildRecordForAllScopeTypes() { // Arrange - WorkflowScopes scopes = new(); FormulaValue testValue = FormulaValue.New("test"); // Act & Assert - scopes.Set("envKey", testValue, VariableScopeNames.Environment); - RecordValue envRecord = scopes.BuildRecord(VariableScopeNames.Environment); + this.State.Set("envKey", testValue, VariableScopeNames.Environment); + RecordValue envRecord = this.State.BuildRecord(VariableScopeNames.Environment); Assert.Single(envRecord.Fields); - scopes.Set("topicKey", testValue, VariableScopeNames.Topic); - RecordValue topicRecord = scopes.BuildRecord(VariableScopeNames.Topic); + this.State.Set("topicKey", testValue, VariableScopeNames.Topic); + RecordValue topicRecord = this.State.BuildRecord(VariableScopeNames.Topic); Assert.Single(topicRecord.Fields); - scopes.Set("globalKey", testValue, VariableScopeNames.Global); - RecordValue globalRecord = scopes.BuildRecord(VariableScopeNames.Global); + this.State.Set("globalKey", testValue, VariableScopeNames.Global); + RecordValue globalRecord = this.State.BuildRecord(VariableScopeNames.Global); Assert.Single(globalRecord.Fields); - scopes.Set("systemKey", testValue, VariableScopeNames.System); - RecordValue systemRecord = scopes.BuildRecord(VariableScopeNames.System); + this.State.Set("systemKey", testValue, VariableScopeNames.System); + RecordValue systemRecord = this.State.BuildRecord(VariableScopeNames.System); Assert.Single(systemRecord.Fields); } @@ -88,12 +82,11 @@ public class WorkflowScopesTests public void GetWithImplicitScope() { // Arrange - WorkflowScopes scopes = new(); FormulaValue testValue = FormulaValue.New("test"); - scopes.Set("key1", testValue, VariableScopeNames.Topic); + this.State.Set("key1", testValue, VariableScopeNames.Topic); // Act - FormulaValue result = scopes.Get("key1"); + FormulaValue result = this.State.Get("key1"); // Assert Assert.Equal(testValue, result); @@ -103,12 +96,11 @@ public class WorkflowScopesTests public void GetWithSpecifiedScope() { // Arrange - WorkflowScopes scopes = new(); FormulaValue testValue = FormulaValue.New("test"); - scopes.Set("key1", testValue, VariableScopeNames.Global); + this.State.Set("key1", testValue, VariableScopeNames.Global); // Act - FormulaValue result = scopes.Get("key1", VariableScopeNames.Global); + FormulaValue result = this.State.Get("key1", VariableScopeNames.Global); // Assert Assert.Equal(testValue, result); @@ -118,14 +110,13 @@ public class WorkflowScopesTests public void SetDefaultScope() { // Arrange - WorkflowScopes scopes = new(); FormulaValue testValue = FormulaValue.New("test"); // Act - scopes.Set("key1", testValue); + this.State.Set("key1", testValue); // Assert - FormulaValue result = scopes.Get("key1", VariableScopeNames.Topic); + FormulaValue result = this.State.Get("key1", VariableScopeNames.Topic); Assert.Equal(testValue, result); } @@ -133,14 +124,13 @@ public class WorkflowScopesTests public void SetSpecifiedScope() { // Arrange - WorkflowScopes scopes = new(); FormulaValue testValue = FormulaValue.New("test"); // Act - scopes.Set("key1", testValue, VariableScopeNames.System); + this.State.Set("key1", testValue, VariableScopeNames.System); // Assert - FormulaValue result = scopes.Get("key1", VariableScopeNames.System); + FormulaValue result = this.State.Get("key1", VariableScopeNames.System); Assert.Equal(testValue, result); } @@ -148,16 +138,15 @@ public class WorkflowScopesTests public void SetOverwritesExistingValue() { // Arrange - WorkflowScopes scopes = new(); FormulaValue initialValue = FormulaValue.New("initial"); FormulaValue newValue = FormulaValue.New("new"); // Act - scopes.Set("key1", initialValue, VariableScopeNames.Topic); - scopes.Set("key1", newValue, VariableScopeNames.Topic); + this.State.Set("key1", initialValue, VariableScopeNames.Topic); + this.State.Set("key1", newValue, VariableScopeNames.Topic); // Assert - FormulaValue result = scopes.Get("key1", VariableScopeNames.Topic); + FormulaValue result = this.State.Get("key1", VariableScopeNames.Topic); Assert.Equal(newValue, result); } @@ -165,21 +154,20 @@ public class WorkflowScopesTests public void RemoveSpecifiedScope() { // Arrange - WorkflowScopes scopes = new(); FormulaValue testValue = FormulaValue.New("test"); // Act - scopes.Set("key1", testValue); + this.State.Set("key1", testValue); // Assert - FormulaValue result = scopes.Get("key1"); + FormulaValue result = this.State.Get("key1"); Assert.Equal(testValue, result); // Act - scopes.Reset("key1"); + this.State.Reset("key1"); // Assert - FormulaValue resultBlank = scopes.Get("key1"); + FormulaValue resultBlank = this.State.Get("key1"); Assert.IsType(resultBlank); } } diff --git a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/Workflows/Condition.yaml b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/Workflows/Condition.yaml index 5c29fe4a00..99fd2f2ea7 100644 --- a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/Workflows/Condition.yaml +++ b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/Workflows/Condition.yaml @@ -9,7 +9,7 @@ beginDialog: - kind: SetVariable id: setVariable_test variable: Topic.TestValue - value: =Value(System.LastMessage.Text) + value: =System.LastMessageText - kind: ConditionGroup id: conditionGroup_test diff --git a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/Workflows/ConditionElse.yaml b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/Workflows/ConditionElse.yaml index 3f0073439d..35026406f6 100644 --- a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/Workflows/ConditionElse.yaml +++ b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/Workflows/ConditionElse.yaml @@ -9,7 +9,7 @@ beginDialog: - kind: SetVariable id: setVariable_test variable: Topic.TestValue - value: =Value(System.LastMessage.Text) + value: =System.LastMessageText - kind: ConditionGroup id: conditionGroup_test diff --git a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/Workflows/Goto.yaml b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/Workflows/Goto.yaml index 1b300dbe1e..b2c28be457 100644 --- a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/Workflows/Goto.yaml +++ b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/Workflows/Goto.yaml @@ -10,15 +10,15 @@ beginDialog: actionId: end_all - kind: SendActivity - id: sendActivity_1 + id: send_activity_1 activity: NEVER 1! - kind: SendActivity - id: sendActivity_2 + id: send_activity_2 activity: NEVER 2! - kind: SendActivity - id: sendActivity_3 + id: send_activity_3 activity: NEVER 3! - kind: EndConversation diff --git a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/Workflows/SendActivity.yaml b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/Workflows/SendActivity.yaml new file mode 100644 index 0000000000..1b1e1b8883 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/Workflows/SendActivity.yaml @@ -0,0 +1,16 @@ +kind: AdaptiveDialog +beginDialog: + kind: OnActivity + id: my_workflow + type: Message + actions: + + - kind: SetVariable + id: set_input + variable: Topic.TestValue + value: =System.LastMessageText + + - kind: SendActivity + id: activity_input + activity: |- + Input: {Topic.TestValue} diff --git a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/Workflows/SetTextVariable.yaml b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/Workflows/SetTextVariable.yaml index 75d8b99239..8f88186994 100644 --- a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/Workflows/SetTextVariable.yaml +++ b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/Workflows/SetTextVariable.yaml @@ -7,4 +7,4 @@ beginDialog: - kind: SetTextVariable id: set_text variable: Topic.TestVar - value: "Test content" + value: Test content diff --git a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/Workflows/SetVariable.yaml b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/Workflows/SetVariable.yaml new file mode 100644 index 0000000000..5f191db2a5 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/Workflows/SetVariable.yaml @@ -0,0 +1,10 @@ +kind: AdaptiveDialog +beginDialog: + kind: OnActivity + id: my_workflow + type: Message + actions: + - kind: SetVariable + id: set_var + variable: Topic.TestVar + value: =3 diff --git a/python/.env.example b/python/.env.example index 54cb688999..045b3f540e 100644 --- a/python/.env.example +++ b/python/.env.example @@ -17,3 +17,8 @@ AGENT_FRAMEWORK_ENABLE_SENSITIVE_DATA=true AGENT_FRAMEWORK_WORKFLOW_ENABLE_OTEL=true # Mem0 MEM0_API_KEY="" +# Copilot Studio +COPILOTSTUDIOAGENT__ENVIRONMENTID="" +COPILOTSTUDIOAGENT__SCHEMANAME="" +COPILOTSTUDIOAGENT__TENANTID="" +COPILOTSTUDIOAGENT__AGENTAPPID="" diff --git a/python/packages/azure/pyproject.toml b/python/packages/azure/pyproject.toml index c2f2a718f1..6b2d1968c7 100644 --- a/python/packages/azure/pyproject.toml +++ b/python/packages/azure/pyproject.toml @@ -70,7 +70,6 @@ show_error_codes = true warn_unused_ignores = false disallow_incomplete_defs = true disallow_untyped_decorators = true -disallow_any_unimported = true [tool.bandit] targets = ["agent_framework_azure"] diff --git a/python/packages/azure/tests/__init__.py b/python/packages/azure/tests/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/python/packages/copilotstudio/LICENSE b/python/packages/copilotstudio/LICENSE new file mode 100644 index 0000000000..9e841e7a26 --- /dev/null +++ b/python/packages/copilotstudio/LICENSE @@ -0,0 +1,21 @@ + MIT License + + Copyright (c) Microsoft Corporation. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE diff --git a/python/packages/copilotstudio/README.md b/python/packages/copilotstudio/README.md new file mode 100644 index 0000000000..fcf211a919 --- /dev/null +++ b/python/packages/copilotstudio/README.md @@ -0,0 +1,97 @@ +# Get Started with Microsoft Agent Framework Copilot Studio + +Please install this package as the extra for `agent-framework`: + +```bash +pip install agent-framework[copilotstudio] +``` + +## Copilot Studio Agent + +The Copilot Studio agent enables integration with Microsoft Copilot Studio, allowing you to interact with published copilots through the Agent Framework. + +### Prerequisites + +Before using the Copilot Studio agent, you need: + +1. **Copilot Studio Environment**: Access to a Microsoft Copilot Studio environment with a published copilot +2. **App Registration**: An Azure AD App Registration with appropriate permissions for Power Platform API +3. **Environment Configuration**: Set the required environment variables or pass them as parameters + +### Environment Variables + +The following environment variables are used for configuration: + +- `COPILOTSTUDIOAGENT__ENVIRONMENTID` - Your Copilot Studio environment ID +- `COPILOTSTUDIOAGENT__SCHEMANAME` - Your copilot's agent identifier/schema name +- `COPILOTSTUDIOAGENT__AGENTAPPID` - Your App Registration client ID +- `COPILOTSTUDIOAGENT__TENANTID` - Your Azure AD tenant ID + +### Basic Usage Example + +```python +import asyncio +from agent_framework.copilotstudio import CopilotStudioAgent + +async def main(): + # Create agent using environment variables + agent = CopilotStudioAgent() + + # Run a simple query + result = await agent.run("What is the capital of France?") + print(result) + +asyncio.run(main()) +``` + +### Explicit Configuration Example + +```python +import asyncio +import os +from agent_framework.copilotstudio import CopilotStudioAgent, acquire_token +from microsoft_agents.copilotstudio.client import ConnectionSettings, CopilotClient, PowerPlatformCloud, AgentType + +async def main(): + # Acquire authentication token + token = acquire_token( + client_id=os.environ["COPILOTSTUDIOAGENT__AGENTAPPID"], + tenant_id=os.environ["COPILOTSTUDIOAGENT__TENANTID"] + ) + + # Create connection settings + settings = ConnectionSettings( + environment_id=os.environ["COPILOTSTUDIOAGENT__ENVIRONMENTID"], + agent_identifier=os.environ["COPILOTSTUDIOAGENT__SCHEMANAME"], + cloud=PowerPlatformCloud.PROD, + copilot_agent_type=AgentType.PUBLISHED + ) + + # Create client and agent + client = CopilotClient(settings=settings, token=token) + agent = CopilotStudioAgent(client=client) + + # Run a query + result = await agent.run("What is the capital of Italy?") + print(result) + +asyncio.run(main()) +``` + +### Authentication + +The package uses MSAL (Microsoft Authentication Library) for authentication with interactive flows when needed. Ensure your App Registration has: + +- **API Permissions**: Power Platform API permissions (https://api.powerplatform.com/.default) +- **Redirect URIs**: Configured appropriately for your authentication method +- **Public Client Flows**: Enabled if using interactive authentication + +### Examples + +For more comprehensive examples, see the [Copilot Studio examples](https://github.com/microsoft/agent-framework/tree/main/python/samples/getting_started/agents/copilotstudio/) which demonstrate: + +- Basic non-streaming and streaming execution +- Explicit settings and manual token acquisition +- Different authentication patterns +- Error handling and troubleshooting + diff --git a/python/packages/copilotstudio/agent_framework_copilotstudio/__init__.py b/python/packages/copilotstudio/agent_framework_copilotstudio/__init__.py new file mode 100644 index 0000000000..dc892e2f39 --- /dev/null +++ b/python/packages/copilotstudio/agent_framework_copilotstudio/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) Microsoft. All rights reserved. + +import importlib.metadata + +from ._acquire_token import acquire_token +from ._agent import CopilotStudioAgent + +try: + __version__ = importlib.metadata.version(__name__) +except importlib.metadata.PackageNotFoundError: + __version__ = "0.0.0" # Fallback for development mode + +__all__ = ["CopilotStudioAgent", "__version__", "acquire_token"] diff --git a/python/packages/copilotstudio/agent_framework_copilotstudio/_acquire_token.py b/python/packages/copilotstudio/agent_framework_copilotstudio/_acquire_token.py new file mode 100644 index 0000000000..f0334dc7d0 --- /dev/null +++ b/python/packages/copilotstudio/agent_framework_copilotstudio/_acquire_token.py @@ -0,0 +1,94 @@ +# Copyright (c) Microsoft. All rights reserved. +# pyright: reportUnknownMemberType = false +# pyright: reportUnknownVariableType = false +# pyright: reportUnknownArgumentType = false + +import logging +from typing import Any + +from agent_framework.exceptions import ServiceException +from msal import PublicClientApplication + +logger = logging.getLogger(__name__) + +# Default scopes for Power Platform API +DEFAULT_SCOPES = ["https://api.powerplatform.com/.default"] + + +def acquire_token( + client_id: str, + tenant_id: str, + username: str | None = None, + token_cache: Any | None = None, + scopes: list[str] | None = None, +) -> str: + """Acquire an authentication token using MSAL Public Client Application. + + This function attempts to acquire a token silently first (using cached tokens), + and falls back to interactive authentication if needed. + + Args: + client_id: The client ID of the application. + tenant_id: The tenant ID for authentication. + username: Optional username to filter accounts. + token_cache: Optional token cache for storing tokens. + scopes: Optional list of scopes. Defaults to Power Platform API scopes. + + Returns: + The access token string. + + Raises: + ServiceException: If authentication token cannot be acquired. + """ + if not client_id: + raise ServiceException("Client ID is required for token acquisition.") + + if not tenant_id: + raise ServiceException("Tenant ID is required for token acquisition.") + + authority = f"https://login.microsoftonline.com/{tenant_id}" + target_scopes = scopes or DEFAULT_SCOPES + + pca = PublicClientApplication(client_id=client_id, authority=authority, token_cache=token_cache) + + accounts = pca.get_accounts(username=username) + + token: str | None = None + + # Try silent token acquisition first if we have cached accounts + if accounts: + try: + logger.debug("Attempting silent token acquisition") + response = pca.acquire_token_silent(scopes=target_scopes, account=accounts[0]) + if response and "access_token" in response: + token = str(response["access_token"]) # type: ignore[assignment] + logger.debug("Successfully acquired token silently") + elif response and "error" in response: + logger.warning( + "Silent token acquisition failed: %s - %s", response.get("error"), response.get("error_description") + ) + except Exception as ex: + logger.warning("Silent token acquisition failed with exception: %s", ex) + + # Fall back to interactive authentication if silent acquisition failed + if not token: + try: + logger.debug("Attempting interactive token acquisition") + response = pca.acquire_token_interactive(scopes=target_scopes) + if response and "access_token" in response: + token = str(response["access_token"]) # type: ignore[assignment] + logger.debug("Successfully acquired token interactively") + elif response and "error" in response: + logger.error( + "Interactive token acquisition failed: %s - %s", + response.get("error"), + response.get("error_description"), + ) + except Exception as ex: + logger.error("Interactive token acquisition failed with exception: %s", ex) + raise ServiceException(f"Failed to acquire authentication token: {ex}") from ex + + if not token: + raise ServiceException("Authentication token cannot be acquired.") + + return token diff --git a/python/packages/copilotstudio/agent_framework_copilotstudio/_agent.py b/python/packages/copilotstudio/agent_framework_copilotstudio/_agent.py new file mode 100644 index 0000000000..345118c57f --- /dev/null +++ b/python/packages/copilotstudio/agent_framework_copilotstudio/_agent.py @@ -0,0 +1,312 @@ +# Copyright (c) Microsoft. All rights reserved. + +from collections.abc import AsyncIterable +from typing import Any, ClassVar + +from agent_framework import ( + AgentRunResponse, + AgentRunResponseUpdate, + AgentThread, + BaseAgent, + ChatMessage, + Role, + TextContent, +) +from agent_framework._pydantic import AFBaseSettings +from agent_framework.exceptions import ServiceException, ServiceInitializationError +from microsoft_agents.copilotstudio.client import AgentType, ConnectionSettings, CopilotClient, PowerPlatformCloud +from pydantic import ValidationError + +from ._acquire_token import acquire_token + + +class CopilotStudioSettings(AFBaseSettings): + """Copilot Studio model settings. + + The settings are first loaded from environment variables with the prefix 'COPILOTSTUDIOAGENT__'. + If the environment variables are not found, the settings can be loaded from a .env file + with the encoding 'utf-8'. If the settings are not found in the .env file, the settings + are ignored; however, validation will fail alerting that the settings are missing. + + Attributes: + environmentid: Environment ID of environment with the Copilot Studio App.. + (Env var COPILOTSTUDIOAGENT__ENVIRONMENTID) + schemaname: The agent identifier or schema name of the Copilot to use. + (Env var COPILOTSTUDIOAGENT__SCHEMANAME) + agentappid: The app ID of the App Registration used to login. + (Env var COPILOTSTUDIOAGENT__AGENTAPPID) + tenantid: The tenant ID of the App Registration used to login. + (Env var COPILOTSTUDIOAGENT__TENANTID) + Parameters: + env_file_path: If provided, the .env settings are read from this file path location. + env_file_encoding: The encoding of the .env file, defaults to 'utf-8'. + """ + + env_prefix: ClassVar[str] = "COPILOTSTUDIOAGENT__" + + environmentid: str | None = None + schemaname: str | None = None + agentappid: str | None = None + tenantid: str | None = None + + +class CopilotStudioAgent(BaseAgent): + """A Copilot Studio Agent.""" + + client: CopilotClient + settings: ConnectionSettings | None + token: str | None + cloud: PowerPlatformCloud | None + agent_type: AgentType | None + custom_power_platform_cloud: str | None + username: str | None + token_cache: Any | None + scopes: list[str] | None + + def __init__( + self, + client: CopilotClient | None = None, + settings: ConnectionSettings | None = None, + environment_id: str | None = None, + agent_identifier: str | None = None, + client_id: str | None = None, + tenant_id: str | None = None, + token: str | None = None, + cloud: PowerPlatformCloud | None = None, + agent_type: AgentType | None = None, + custom_power_platform_cloud: str | None = None, + username: str | None = None, + token_cache: Any | None = None, + scopes: list[str] | None = None, + env_file_path: str | None = None, + env_file_encoding: str | None = None, + ) -> None: + """Initialize the Copilot Studio Agent. + + Args: + client: Optional pre-configured CopilotClient instance. If not provided, + a new client will be created using the other parameters. + settings: Optional pre-configured ConnectionSettings. If not provided, + settings will be created from the other parameters. + environment_id: Environment ID of the Power Platform environment containing + the Copilot Studio app. Can also be set via COPILOTSTUDIOAGENT__ENVIRONMENTID + environment variable. + agent_identifier: The agent identifier or schema name of the Copilot to use. + Can also be set via COPILOTSTUDIOAGENT__SCHEMANAME environment variable. + client_id: The app ID of the App Registration used for authentication. + Can also be set via COPILOTSTUDIOAGENT__AGENTAPPID environment variable. + tenant_id: The tenant ID of the App Registration used for authentication. + Can also be set via COPILOTSTUDIOAGENT__TENANTID environment variable. + token: Optional pre-acquired authentication token. If not provided, + token acquisition will be attempted using MSAL. + cloud: The Power Platform cloud to use (Public, GCC, etc.). + agent_type: The type of Copilot Studio agent (Copilot, Agent, etc.). + custom_power_platform_cloud: Custom Power Platform cloud URL if using + a custom environment. + username: Optional username for token acquisition. + token_cache: Optional token cache for storing authentication tokens. + scopes: Optional list of authentication scopes. Defaults to Power Platform + API scopes if not provided. + env_file_path: Optional path to .env file for loading configuration. + env_file_encoding: Encoding of the .env file, defaults to 'utf-8'. + + Raises: + ServiceInitializationError: If required configuration is missing or invalid. + """ + if not client: + try: + copilot_studio_settings = CopilotStudioSettings( + environmentid=environment_id, + schemaname=agent_identifier, + agentappid=client_id, + tenantid=tenant_id, + env_file_path=env_file_path, + env_file_encoding=env_file_encoding, + ) + except ValidationError as ex: + raise ServiceInitializationError("Failed to create Copilot Studio settings.", ex) from ex + + if not settings: + if not copilot_studio_settings.environmentid: + raise ServiceInitializationError( + "Copilot Studio environment ID is required. Set via 'environment_id' parameter " + "or 'COPILOTSTUDIOAGENT__ENVIRONMENTID' environment variable." + ) + if not copilot_studio_settings.schemaname: + raise ServiceInitializationError( + "Copilot Studio agent identifier/schema name is required. Set via 'agent_identifier' parameter " + "or 'COPILOTSTUDIOAGENT__SCHEMANAME' environment variable." + ) + + settings = ConnectionSettings( + environment_id=copilot_studio_settings.environmentid, + agent_identifier=copilot_studio_settings.schemaname, + cloud=cloud, + copilot_agent_type=agent_type, + custom_power_platform_cloud=custom_power_platform_cloud, + ) + + if not token: + if not copilot_studio_settings.agentappid: + raise ServiceInitializationError( + "Copilot Studio client ID is required. Set via 'client_id' parameter " + "or 'COPILOTSTUDIOAGENT__AGENTAPPID' environment variable." + ) + + if not copilot_studio_settings.tenantid: + raise ServiceInitializationError( + "Copilot Studio tenant ID is required. Set via 'tenant_id' parameter " + "or 'COPILOTSTUDIOAGENT__TENANTID' environment variable." + ) + + token = acquire_token( + client_id=copilot_studio_settings.agentappid, + tenant_id=copilot_studio_settings.tenantid, + username=username, + token_cache=token_cache, + scopes=scopes, + ) + + client = CopilotClient(settings=settings, token=token) + + super().__init__( + client=client, # type: ignore[reportCallIssue] + settings=settings, # type: ignore[reportCallIssue] + token=token, # type: ignore[reportCallIssue] + cloud=cloud, # type: ignore[reportCallIssue] + agent_type=agent_type, # type: ignore[reportCallIssue] + custom_power_platform_cloud=custom_power_platform_cloud, # type: ignore[reportCallIssue] + username=username, # type: ignore[reportCallIssue] + token_cache=token_cache, # type: ignore[reportCallIssue] + scopes=scopes, # type: ignore[reportCallIssue] + ) + + async def run( + self, + messages: str | ChatMessage | list[str] | list[ChatMessage] | None = None, + *, + thread: AgentThread | None = None, + **kwargs: Any, + ) -> AgentRunResponse: + """Get a response from the agent. + + This method returns the final result of the agent's execution + as a single AgentRunResponse object. The caller is blocked until + the final result is available. + + Note: For streaming responses, use the run_stream method, which returns + intermediate steps and the final result as a stream of AgentRunResponseUpdate + objects. Streaming only the final result is not feasible because the timing of + the final result's availability is unknown, and blocking the caller until then + is undesirable in streaming scenarios. + + Args: + messages: The message(s) to send to the agent. + thread: The conversation thread associated with the message(s). + kwargs: Additional keyword arguments. + + Returns: + An agent response item. + """ + if not thread: + thread = self.get_new_thread() + thread.service_thread_id = await self._start_new_conversation() + + input_messages = self._normalize_messages(messages) + + question = "\n".join([message.text for message in input_messages]) + + activities = self.client.ask_question(question, thread.service_thread_id) + response_messages: list[ChatMessage] = [] + response_id: str | None = None + + response_messages = [message async for message in self._process_activities(activities, streaming=False)] + response_id = response_messages[0].message_id if response_messages else None + + return AgentRunResponse(messages=response_messages, response_id=response_id) + + async def run_stream( + self, + messages: str | ChatMessage | list[str] | list[ChatMessage] | None = None, + *, + thread: AgentThread | None = None, + **kwargs: Any, + ) -> AsyncIterable[AgentRunResponseUpdate]: + """Run the agent as a stream. + + This method will return the intermediate steps and final results of the + agent's execution as a stream of AgentRunResponseUpdate objects to the caller. + + Note: An AgentRunResponseUpdate object contains a chunk of a message. + + Args: + messages: The message(s) to send to the agent. + thread: The conversation thread associated with the message(s). + kwargs: Additional keyword arguments. + + Yields: + An agent response item. + """ + if not thread: + thread = self.get_new_thread() + thread.service_thread_id = await self._start_new_conversation() + + input_messages = self._normalize_messages(messages) + + question = "\n".join([message.text for message in input_messages]) + + activities = self.client.ask_question(question, thread.service_thread_id) + + async for message in self._process_activities(activities, streaming=True): + yield AgentRunResponseUpdate( + role=message.role, + contents=message.contents, + additional_properties=message.additional_properties, + author_name=message.author_name, + raw_representation=message.raw_representation, + response_id=message.message_id, + message_id=message.message_id, + ) + + async def _start_new_conversation(self) -> str: + """Start a new conversation with the Copilot Studio agent. + + Returns: + The conversation ID for the new conversation. + + Raises: + ServiceException: If the conversation could not be started. + """ + conversation_id: str | None = None + + async for activity in self.client.start_conversation(emit_start_conversation_event=True): + if activity and activity.conversation and activity.conversation.id: + conversation_id = activity.conversation.id + + if not conversation_id: + raise ServiceException("Failed to start a new conversation.") + + return conversation_id + + async def _process_activities(self, activities: AsyncIterable[Any], streaming: bool) -> AsyncIterable[ChatMessage]: + """Process activities from the Copilot Studio agent. + + Args: + activities: Stream of activities from the agent. + streaming: Whether to process activities for streaming (typing activities) + or non-streaming (message activities) responses. + + Yields: + ChatMessage objects created from the activities. + """ + async for activity in activities: + if activity.text and ( + (activity.type == "message" and not streaming) or (activity.type == "typing" and streaming) + ): + yield ChatMessage( + role=Role.ASSISTANT, + contents=[TextContent(activity.text)], + author_name=activity.from_property.name if activity.from_property else None, + message_id=activity.id, + raw_representation=activity, + ) diff --git a/python/packages/copilotstudio/pyproject.toml b/python/packages/copilotstudio/pyproject.toml new file mode 100644 index 0000000000..aae8a4c274 --- /dev/null +++ b/python/packages/copilotstudio/pyproject.toml @@ -0,0 +1,93 @@ +[project] +name = "agent-framework-copilotstudio" +description = "Copilot Studio integration for Microsoft Agent Framework." +authors = [{ name = "Microsoft", email = "SK-Support@microsoft.com"}] +readme = "README.md" +requires-python = ">=3.10" +version = "0.1.0b1" +license-files = ["LICENSE"] +urls.homepage = "https://learn.microsoft.com/en-us/semantic-kernel/overview/" +urls.source = "https://github.com/microsoft/agent-framework/tree/main/python" +urls.release_notes = "https://github.com/microsoft/agent-framework/releases?q=tag%3Apython-1&expanded=true" +urls.issues = "https://github.com/microsoft/agent-framework/issues" +classifiers = [ + "License :: OSI Approved :: MIT License", + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Framework :: Pydantic :: 2", + "Typing :: Typed", +] +dependencies = [ + "agent-framework", + "microsoft-agents-copilotstudio-client>=0.3.1", +] + +[tool.uv] +prerelease = "if-necessary-or-explicit" +environments = [ + "sys_platform == 'darwin'", + "sys_platform == 'linux'", + "sys_platform == 'win32'" +] + +[tool.uv-dynamic-versioning] +fallback-version = "0.0.0" +[tool.pytest.ini_options] +testpaths = 'tests' +addopts = "-ra -q -r fEX" +asyncio_mode = "auto" +asyncio_default_fixture_loop_scope = "function" +filterwarnings = [ + "ignore:Support for class-based `config` is deprecated:DeprecationWarning:pydantic.*" +] +timeout = 120 + +[tool.ruff] +extend = "../../pyproject.toml" + +[tool.coverage.run] +omit = [ + "**/__init__.py" +] + +[tool.pyright] +extend = "../../pyproject.toml" +exclude = ['tests'] + +[tool.mypy] +plugins = ['pydantic.mypy'] +strict = true +python_version = "3.10" +ignore_missing_imports = true +disallow_untyped_defs = true +no_implicit_optional = true +check_untyped_defs = true +warn_return_any = true +show_error_codes = true +warn_unused_ignores = false +disallow_incomplete_defs = true +disallow_untyped_decorators = true + +[tool.bandit] +targets = ["agent_framework_copilotstudio"] +exclude_dirs = ["tests"] + +[tool.poe] +executor.type = "uv" +include = "../../shared_tasks.toml" +[tool.poe.tasks] +mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_copilotstudio" +test = "pytest --cov=agent_framework_copilotstudio --cov-report=term-missing:skip-covered tests" + +[tool.uv.build-backend] +module-name = "agent_framework_copilotstudio" +module-root = "" + +[build-system] +requires = ["uv_build>=0.8.2,<0.9.0"] +build-backend = "uv_build" diff --git a/python/packages/copilotstudio/tests/conftest.py b/python/packages/copilotstudio/tests/conftest.py new file mode 100644 index 0000000000..b836a5e71a --- /dev/null +++ b/python/packages/copilotstudio/tests/conftest.py @@ -0,0 +1,91 @@ +# Copyright (c) Microsoft. All rights reserved. + +from typing import Any +from unittest.mock import MagicMock + +import pytest +from microsoft_agents.copilotstudio.client import CopilotClient + + +@pytest.fixture +def exclude_list(request: Any) -> list[str]: + """Fixture that returns a list of environment variables to exclude.""" + return request.param if hasattr(request, "param") else [] + + +@pytest.fixture +def override_env_param_dict(request: Any) -> dict[str, str]: + """Fixture that returns a dict of environment variables to override.""" + return request.param if hasattr(request, "param") else {} + + +@pytest.fixture() +def copilot_studio_unit_test_env(monkeypatch, exclude_list, override_env_param_dict): # type: ignore + """Fixture to set environment variables for CopilotStudioSettings.""" + + if exclude_list is None: + exclude_list = [] + + if override_env_param_dict is None: + override_env_param_dict = {} + + env_vars = { + "COPILOTSTUDIOAGENT__ENVIRONMENTID": "test-environment-id", + "COPILOTSTUDIOAGENT__SCHEMANAME": "test-schema-name", + "COPILOTSTUDIOAGENT__AGENTAPPID": "test-client-id", + "COPILOTSTUDIOAGENT__TENANTID": "test-tenant-id", + } + + env_vars.update(override_env_param_dict) # type: ignore + + for key, value in env_vars.items(): + if key in exclude_list: + monkeypatch.delenv(key, raising=False) # type: ignore + continue + monkeypatch.setenv(key, value) # type: ignore + + return env_vars + + +@pytest.fixture +def mock_copilot_client() -> MagicMock: + """Mock CopilotClient for testing.""" + return MagicMock(spec=CopilotClient) + + +@pytest.fixture +def mock_pca() -> MagicMock: + """Mock PublicClientApplication for testing.""" + mock_pca = MagicMock() + + # Mock successful token response + mock_token_response = { + "access_token": "test-access-token-12345", + "token_type": "Bearer", + "expires_in": 3600, + } + + mock_pca.get_accounts.return_value = [] + mock_pca.acquire_token_interactive.return_value = mock_token_response + mock_pca.acquire_token_silent.return_value = mock_token_response + + return mock_pca + + +@pytest.fixture +def mock_activity() -> MagicMock: + """Mock Activity for testing.""" + mock_activity = MagicMock() + mock_activity.text = "Test response" + mock_activity.type = "message" + mock_activity.id = "test-activity-id" + mock_activity.from_property.name = "Test Bot" + return mock_activity + + +@pytest.fixture +def mock_conversation() -> MagicMock: + """Mock conversation for testing.""" + mock_conversation = MagicMock() + mock_conversation.id = "test-conversation-id" + return mock_conversation diff --git a/python/packages/copilotstudio/tests/test_acquire_token.py b/python/packages/copilotstudio/tests/test_acquire_token.py new file mode 100644 index 0000000000..3aeb6312c3 --- /dev/null +++ b/python/packages/copilotstudio/tests/test_acquire_token.py @@ -0,0 +1,243 @@ +# Copyright (c) Microsoft. All rights reserved. + +from unittest.mock import MagicMock, patch + +import pytest +from agent_framework.exceptions import ServiceException + +from agent_framework_copilotstudio._acquire_token import DEFAULT_SCOPES, acquire_token + + +class TestAcquireToken: + """Test class for token acquisition functionality.""" + + def test_acquire_token_missing_client_id(self) -> None: + """Test that acquire_token raises ServiceException when client_id is missing.""" + with pytest.raises(ServiceException, match="Client ID is required for token acquisition"): + acquire_token(client_id="", tenant_id="test-tenant-id") + + def test_acquire_token_missing_tenant_id(self) -> None: + """Test that acquire_token raises ServiceException when tenant_id is missing.""" + with pytest.raises(ServiceException, match="Tenant ID is required for token acquisition"): + acquire_token(client_id="test-client-id", tenant_id="") + + def test_acquire_token_none_client_id(self) -> None: + """Test that acquire_token raises ServiceException when client_id is None.""" + with pytest.raises(ServiceException, match="Client ID is required for token acquisition"): + acquire_token(client_id=None, tenant_id="test-tenant-id") # type: ignore + + def test_acquire_token_none_tenant_id(self) -> None: + """Test that acquire_token raises ServiceException when tenant_id is None.""" + with pytest.raises(ServiceException, match="Tenant ID is required for token acquisition"): + acquire_token(client_id="test-client-id", tenant_id=None) # type: ignore + + @patch("agent_framework_copilotstudio._acquire_token.PublicClientApplication") + def test_acquire_token_silent_success(self, mock_pca_class: MagicMock) -> None: + """Test successful silent token acquisition.""" + mock_pca = MagicMock() + mock_pca_class.return_value = mock_pca + + mock_account = MagicMock() + mock_pca.get_accounts.return_value = [mock_account] + + mock_token_response = {"access_token": "test-access-token-12345"} + mock_pca.acquire_token_silent.return_value = mock_token_response + + result = acquire_token( + client_id="test-client-id", + tenant_id="test-tenant-id", + ) + + assert result == "test-access-token-12345" + mock_pca_class.assert_called_once_with( + client_id="test-client-id", + authority="https://login.microsoftonline.com/test-tenant-id", + token_cache=None, + ) + mock_pca.get_accounts.assert_called_once_with(username=None) + mock_pca.acquire_token_silent.assert_called_once_with(scopes=DEFAULT_SCOPES, account=mock_account) + + @patch("agent_framework_copilotstudio._acquire_token.PublicClientApplication") + def test_acquire_token_silent_success_with_username(self, mock_pca_class: MagicMock) -> None: + """Test successful silent token acquisition with username.""" + mock_pca = MagicMock() + mock_pca_class.return_value = mock_pca + + mock_account = MagicMock() + mock_pca.get_accounts.return_value = [mock_account] + + mock_token_response = {"access_token": "test-access-token-12345"} + mock_pca.acquire_token_silent.return_value = mock_token_response + + result = acquire_token( + client_id="test-client-id", + tenant_id="test-tenant-id", + username="test-user@example.com", + ) + + assert result == "test-access-token-12345" + mock_pca.get_accounts.assert_called_once_with(username="test-user@example.com") + mock_pca.acquire_token_silent.assert_called_once_with(scopes=DEFAULT_SCOPES, account=mock_account) + + @patch("agent_framework_copilotstudio._acquire_token.PublicClientApplication") + def test_acquire_token_silent_success_with_custom_scopes(self, mock_pca_class: MagicMock) -> None: + """Test successful silent token acquisition with custom scopes.""" + # Setup + mock_pca = MagicMock() + mock_pca_class.return_value = mock_pca + + mock_account = MagicMock() + mock_pca.get_accounts.return_value = [mock_account] + + mock_token_response = {"access_token": "test-access-token-12345"} + mock_pca.acquire_token_silent.return_value = mock_token_response + + custom_scopes = ["https://custom.api.com/.default"] + + result = acquire_token( + client_id="test-client-id", + tenant_id="test-tenant-id", + scopes=custom_scopes, + ) + + assert result == "test-access-token-12345" + mock_pca.acquire_token_silent.assert_called_once_with(scopes=custom_scopes, account=mock_account) + + @patch("agent_framework_copilotstudio._acquire_token.PublicClientApplication") + def test_acquire_token_interactive_success_no_accounts(self, mock_pca_class: MagicMock) -> None: + """Test successful interactive token acquisition when no cached accounts exist.""" + # Setup + mock_pca = MagicMock() + mock_pca_class.return_value = mock_pca + + mock_pca.get_accounts.return_value = [] # No cached accounts + + mock_token_response = {"access_token": "test-interactive-token-67890"} + mock_pca.acquire_token_interactive.return_value = mock_token_response + + result = acquire_token( + client_id="test-client-id", + tenant_id="test-tenant-id", + ) + + assert result == "test-interactive-token-67890" + mock_pca.acquire_token_interactive.assert_called_once_with(scopes=DEFAULT_SCOPES) + + @patch("agent_framework_copilotstudio._acquire_token.PublicClientApplication") + def test_acquire_token_fallback_to_interactive_after_silent_fails(self, mock_pca_class: MagicMock) -> None: + """Test fallback to interactive authentication when silent acquisition fails.""" + mock_pca = MagicMock() + mock_pca_class.return_value = mock_pca + + mock_account = MagicMock() + mock_pca.get_accounts.return_value = [mock_account] + + # Silent acquisition fails with error response + mock_silent_error_response = {"error": "invalid_grant", "error_description": "Token expired"} + mock_pca.acquire_token_silent.return_value = mock_silent_error_response + + # Interactive acquisition succeeds + mock_interactive_response = {"access_token": "test-interactive-token-67890"} + mock_pca.acquire_token_interactive.return_value = mock_interactive_response + + result = acquire_token( + client_id="test-client-id", + tenant_id="test-tenant-id", + ) + + assert result == "test-interactive-token-67890" + mock_pca.acquire_token_silent.assert_called_once_with(scopes=DEFAULT_SCOPES, account=mock_account) + mock_pca.acquire_token_interactive.assert_called_once_with(scopes=DEFAULT_SCOPES) + + @patch("agent_framework_copilotstudio._acquire_token.PublicClientApplication") + def test_acquire_token_fallback_to_interactive_after_silent_exception(self, mock_pca_class: MagicMock) -> None: + """Test fallback to interactive authentication when silent acquisition throws exception.""" + mock_pca = MagicMock() + mock_pca_class.return_value = mock_pca + + mock_account = MagicMock() + mock_pca.get_accounts.return_value = [mock_account] + + # Silent acquisition throws exception + mock_pca.acquire_token_silent.side_effect = Exception("Network error") + + # Interactive acquisition succeeds + mock_interactive_response = {"access_token": "test-interactive-token-67890"} + mock_pca.acquire_token_interactive.return_value = mock_interactive_response + + result = acquire_token( + client_id="test-client-id", + tenant_id="test-tenant-id", + ) + + assert result == "test-interactive-token-67890" + mock_pca.acquire_token_silent.assert_called_once_with(scopes=DEFAULT_SCOPES, account=mock_account) + mock_pca.acquire_token_interactive.assert_called_once_with(scopes=DEFAULT_SCOPES) + + @patch("agent_framework_copilotstudio._acquire_token.PublicClientApplication") + def test_acquire_token_interactive_error_response(self, mock_pca_class: MagicMock) -> None: + """Test that acquire_token handles error responses from interactive authentication.""" + mock_pca = MagicMock() + mock_pca_class.return_value = mock_pca + + mock_pca.get_accounts.return_value = [] # No cached accounts + + # Interactive acquisition returns error + mock_error_response = {"error": "access_denied", "error_description": "User denied consent"} + mock_pca.acquire_token_interactive.return_value = mock_error_response + + with pytest.raises(ServiceException, match="Authentication token cannot be acquired"): + acquire_token( + client_id="test-client-id", + tenant_id="test-tenant-id", + ) + + @patch("agent_framework_copilotstudio._acquire_token.PublicClientApplication") + def test_acquire_token_interactive_exception(self, mock_pca_class: MagicMock) -> None: + """Test that acquire_token handles exceptions from interactive authentication.""" + mock_pca = MagicMock() + mock_pca_class.return_value = mock_pca + + mock_pca.get_accounts.return_value = [] # No cached accounts + + # Interactive acquisition throws exception + mock_pca.acquire_token_interactive.side_effect = Exception("Authentication service unavailable") + + with pytest.raises(ServiceException, match="Failed to acquire authentication token"): + acquire_token( + client_id="test-client-id", + tenant_id="test-tenant-id", + ) + + @patch("agent_framework_copilotstudio._acquire_token.PublicClientApplication") + def test_acquire_token_with_token_cache(self, mock_pca_class: MagicMock) -> None: + """Test acquire_token with custom token cache.""" + mock_pca = MagicMock() + mock_pca_class.return_value = mock_pca + + mock_account = MagicMock() + mock_pca.get_accounts.return_value = [mock_account] + + mock_token_response = {"access_token": "test-cached-token"} + mock_pca.acquire_token_silent.return_value = mock_token_response + + mock_token_cache = MagicMock() + + result = acquire_token( + client_id="test-client-id", + tenant_id="test-tenant-id", + token_cache=mock_token_cache, + ) + + assert result == "test-cached-token" + mock_pca_class.assert_called_once_with( + client_id="test-client-id", + authority="https://login.microsoftonline.com/test-tenant-id", + token_cache=mock_token_cache, + ) + + def test_default_scopes_constant(self) -> None: + """Test that DEFAULT_SCOPES constant is properly defined.""" + assert DEFAULT_SCOPES == ["https://api.powerplatform.com/.default"] + assert isinstance(DEFAULT_SCOPES, list) + assert len(DEFAULT_SCOPES) == 1 diff --git a/python/packages/copilotstudio/tests/test_agent.py b/python/packages/copilotstudio/tests/test_agent.py new file mode 100644 index 0000000000..e2b160610e --- /dev/null +++ b/python/packages/copilotstudio/tests/test_agent.py @@ -0,0 +1,325 @@ +# Copyright (c) Microsoft. All rights reserved. + +from typing import Any +from unittest.mock import MagicMock, patch + +import pytest +from agent_framework import ( + AgentRunResponse, + AgentRunResponseUpdate, + AgentThread, + ChatMessage, + Role, + TextContent, +) +from agent_framework.exceptions import ServiceException, ServiceInitializationError +from microsoft_agents.copilotstudio.client import CopilotClient + +from agent_framework_copilotstudio import CopilotStudioAgent + + +def create_async_generator(items: list[Any]) -> Any: + """Helper to create async generator mock.""" + + async def async_gen() -> Any: + for item in items: + yield item + + return async_gen() + + +class TestCopilotStudioAgent: + """Test cases for CopilotStudioAgent.""" + + @pytest.fixture + def mock_activity(self) -> MagicMock: + activity = MagicMock() + activity.text = "Test response" + activity.type = "message" + activity.id = "test-id" + activity.from_property.name = "Test Bot" + return activity + + @pytest.fixture + def mock_copilot_client(self) -> MagicMock: + return MagicMock(spec=CopilotClient) + + @patch("agent_framework_copilotstudio._acquire_token.acquire_token") + @patch("agent_framework_copilotstudio._agent.CopilotStudioSettings") + def test_init_missing_environment_id(self, mock_settings: MagicMock, mock_acquire_token: MagicMock) -> None: + mock_acquire_token.return_value = "fake-token" + mock_settings.return_value.environmentid = None + mock_settings.return_value.schemaname = "test-bot" + mock_settings.return_value.tenantid = "test-tenant" + mock_settings.return_value.agentappid = "test-client" + + with pytest.raises(ServiceInitializationError, match="environment ID is required"): + CopilotStudioAgent() + + @patch("agent_framework_copilotstudio._acquire_token.acquire_token") + @patch("agent_framework_copilotstudio._agent.CopilotStudioSettings") + def test_init_missing_bot_id(self, mock_settings: MagicMock, mock_acquire_token: MagicMock) -> None: + mock_acquire_token.return_value = "fake-token" + mock_settings.return_value.environmentid = "test-env" + mock_settings.return_value.schemaname = None + mock_settings.return_value.tenantid = "test-tenant" + mock_settings.return_value.agentappid = "test-client" + + with pytest.raises(ServiceInitializationError, match="agent identifier"): + CopilotStudioAgent() + + @patch("agent_framework_copilotstudio._acquire_token.acquire_token") + @patch("agent_framework_copilotstudio._agent.CopilotStudioSettings") + def test_init_missing_tenant_id(self, mock_settings: MagicMock, mock_acquire_token: MagicMock) -> None: + mock_acquire_token.return_value = "fake-token" + mock_settings.return_value.environmentid = "test-env" + mock_settings.return_value.schemaname = "test-bot" + mock_settings.return_value.tenantid = None + mock_settings.return_value.agentappid = "test-client" + + with pytest.raises(ServiceInitializationError, match="tenant ID is required"): + CopilotStudioAgent() + + @patch("agent_framework_copilotstudio._acquire_token.acquire_token") + @patch("agent_framework_copilotstudio._agent.CopilotStudioSettings") + def test_init_missing_client_id(self, mock_settings: MagicMock, mock_acquire_token: MagicMock) -> None: + mock_acquire_token.return_value = "fake-token" + mock_settings.return_value.environmentid = "test-env" + mock_settings.return_value.schemaname = "test-bot" + mock_settings.return_value.tenantid = "test-tenant" + mock_settings.return_value.agentappid = None + + with pytest.raises(ServiceInitializationError, match="client ID is required"): + CopilotStudioAgent() + + def test_init_with_client(self, mock_copilot_client: MagicMock) -> None: + agent = CopilotStudioAgent(client=mock_copilot_client) + assert agent.client == mock_copilot_client + assert agent.id is not None + + @patch("agent_framework_copilotstudio._acquire_token.acquire_token") + def test_init_empty_environment_id(self, mock_acquire_token: MagicMock) -> None: + mock_acquire_token.return_value = "fake-token" + with patch("agent_framework_copilotstudio._agent.CopilotStudioSettings") as mock_settings: + mock_settings.return_value.environmentid = "" + mock_settings.return_value.schemaname = "test-bot" + mock_settings.return_value.tenantid = "test-tenant" + mock_settings.return_value.agentappid = "test-client" + + with pytest.raises(ServiceInitializationError, match="environment ID is required"): + CopilotStudioAgent() + + @patch("agent_framework_copilotstudio._acquire_token.acquire_token") + def test_init_empty_schema_name(self, mock_acquire_token: MagicMock) -> None: + mock_acquire_token.return_value = "fake-token" + with patch("agent_framework_copilotstudio._agent.CopilotStudioSettings") as mock_settings: + mock_settings.return_value.environmentid = "test-env" + mock_settings.return_value.schemaname = "" + mock_settings.return_value.tenantid = "test-tenant" + mock_settings.return_value.agentappid = "test-client" + + with pytest.raises(ServiceInitializationError, match="agent identifier"): + CopilotStudioAgent() + + @pytest.mark.asyncio + async def test_run_with_string_message(self, mock_copilot_client: MagicMock, mock_activity: MagicMock) -> None: + """Test run method with string message.""" + agent = CopilotStudioAgent(client=mock_copilot_client) + + conversation_activity = MagicMock() + conversation_activity.conversation.id = "test-conversation-id" + + mock_copilot_client.start_conversation.return_value = create_async_generator([conversation_activity]) + mock_copilot_client.ask_question.return_value = create_async_generator([mock_activity]) + + response = await agent.run("test message") + + assert isinstance(response, AgentRunResponse) + assert len(response.messages) == 1 + content = response.messages[0].contents[0] + assert isinstance(content, TextContent) + assert content.text == "Test response" + assert response.messages[0].role == Role.ASSISTANT + + @pytest.mark.asyncio + async def test_run_with_chat_message(self, mock_copilot_client: MagicMock, mock_activity: MagicMock) -> None: + """Test run method with ChatMessage.""" + agent = CopilotStudioAgent(client=mock_copilot_client) + + conversation_activity = MagicMock() + conversation_activity.conversation.id = "test-conversation-id" + + mock_copilot_client.start_conversation.return_value = create_async_generator([conversation_activity]) + mock_copilot_client.ask_question.return_value = create_async_generator([mock_activity]) + + chat_message = ChatMessage(role=Role.USER, contents=[TextContent("test message")]) + response = await agent.run(chat_message) + + assert isinstance(response, AgentRunResponse) + assert len(response.messages) == 1 + content = response.messages[0].contents[0] + assert isinstance(content, TextContent) + assert content.text == "Test response" + assert response.messages[0].role == Role.ASSISTANT + + @pytest.mark.asyncio + async def test_run_with_thread(self, mock_copilot_client: MagicMock, mock_activity: MagicMock) -> None: + """Test run method with existing thread.""" + agent = CopilotStudioAgent(client=mock_copilot_client) + thread = AgentThread() + + conversation_activity = MagicMock() + conversation_activity.conversation.id = "test-conversation-id" + + mock_copilot_client.start_conversation.return_value = create_async_generator([conversation_activity]) + mock_copilot_client.ask_question.return_value = create_async_generator([mock_activity]) + + response = await agent.run("test message", thread=thread) + + assert isinstance(response, AgentRunResponse) + assert len(response.messages) == 1 + assert thread.service_thread_id == "test-conversation-id" + + @pytest.mark.asyncio + async def test_run_start_conversation_failure(self, mock_copilot_client: MagicMock) -> None: + """Test run method when conversation start fails.""" + agent = CopilotStudioAgent(client=mock_copilot_client) + + mock_copilot_client.start_conversation.return_value = create_async_generator([]) + + with pytest.raises(ServiceException, match="Failed to start a new conversation"): + await agent.run("test message") + + @pytest.mark.asyncio + async def test_run_stream_with_string_message(self, mock_copilot_client: MagicMock) -> None: + """Test run_stream method with string message.""" + agent = CopilotStudioAgent(client=mock_copilot_client) + + conversation_activity = MagicMock() + conversation_activity.conversation.id = "test-conversation-id" + + typing_activity = MagicMock() + typing_activity.text = "Streaming response" + typing_activity.type = "typing" + typing_activity.id = "test-typing-id" + typing_activity.from_property.name = "Test Bot" + + mock_copilot_client.start_conversation.return_value = create_async_generator([conversation_activity]) + mock_copilot_client.ask_question.return_value = create_async_generator([typing_activity]) + + response_count = 0 + async for response in agent.run_stream("test message"): + assert isinstance(response, AgentRunResponseUpdate) + content = response.contents[0] + assert isinstance(content, TextContent) + assert content.text == "Streaming response" + response_count += 1 + + assert response_count == 1 + + @pytest.mark.asyncio + async def test_run_stream_with_thread(self, mock_copilot_client: MagicMock) -> None: + """Test run_stream method with existing thread.""" + agent = CopilotStudioAgent(client=mock_copilot_client) + thread = AgentThread() + + conversation_activity = MagicMock() + conversation_activity.conversation.id = "test-conversation-id" + + typing_activity = MagicMock() + typing_activity.text = "Streaming response" + typing_activity.type = "typing" + typing_activity.id = "test-typing-id" + typing_activity.from_property.name = "Test Bot" + + mock_copilot_client.start_conversation.return_value = create_async_generator([conversation_activity]) + mock_copilot_client.ask_question.return_value = create_async_generator([typing_activity]) + + response_count = 0 + async for response in agent.run_stream("test message", thread=thread): + assert isinstance(response, AgentRunResponseUpdate) + content = response.contents[0] + assert isinstance(content, TextContent) + assert content.text == "Streaming response" + response_count += 1 + + assert response_count == 1 + assert thread.service_thread_id == "test-conversation-id" + + @pytest.mark.asyncio + async def test_run_stream_no_typing_activity(self, mock_copilot_client: MagicMock) -> None: + """Test run_stream method with non-typing activity.""" + agent = CopilotStudioAgent(client=mock_copilot_client) + + conversation_activity = MagicMock() + conversation_activity.conversation.id = "test-conversation-id" + + message_activity = MagicMock() + message_activity.text = "Message response" + message_activity.type = "message" + message_activity.id = "test-message-id" + + mock_copilot_client.start_conversation.return_value = create_async_generator([conversation_activity]) + mock_copilot_client.ask_question.return_value = create_async_generator([message_activity]) + + response_count = 0 + async for _response in agent.run_stream("test message"): + response_count += 1 + + assert response_count == 0 + + @pytest.mark.asyncio + async def test_run_multiple_activities(self, mock_copilot_client: MagicMock) -> None: + """Test run method with multiple message activities.""" + agent = CopilotStudioAgent(client=mock_copilot_client) + + conversation_activity = MagicMock() + conversation_activity.conversation.id = "test-conversation-id" + + activity1 = MagicMock() + activity1.text = "First response" + activity1.type = "message" + activity1.id = "test-id-1" + activity1.from_property.name = "Test Bot" + + activity2 = MagicMock() + activity2.text = "Second response" + activity2.type = "message" + activity2.id = "test-id-2" + activity2.from_property.name = "Test Bot" + + mock_copilot_client.start_conversation.return_value = create_async_generator([conversation_activity]) + mock_copilot_client.ask_question.return_value = create_async_generator([activity1, activity2]) + + response = await agent.run("test message") + + assert isinstance(response, AgentRunResponse) + assert len(response.messages) == 2 + + @pytest.mark.asyncio + async def test_run_list_of_messages(self, mock_copilot_client: MagicMock, mock_activity: MagicMock) -> None: + """Test run method with list of messages.""" + agent = CopilotStudioAgent(client=mock_copilot_client) + + conversation_activity = MagicMock() + conversation_activity.conversation.id = "test-conversation-id" + + mock_copilot_client.start_conversation.return_value = create_async_generator([conversation_activity]) + mock_copilot_client.ask_question.return_value = create_async_generator([mock_activity]) + + messages = ["Hello", "How are you?"] + response = await agent.run(messages) + + assert isinstance(response, AgentRunResponse) + assert len(response.messages) == 1 + + @pytest.mark.asyncio + async def test_run_stream_start_conversation_failure(self, mock_copilot_client: MagicMock) -> None: + """Test run_stream method when conversation start fails.""" + agent = CopilotStudioAgent(client=mock_copilot_client) + + mock_copilot_client.start_conversation.return_value = create_async_generator([]) + + with pytest.raises(ServiceException, match="Failed to start a new conversation"): + async for _ in agent.run_stream("test message"): + pass diff --git a/python/packages/copilotstudio/tests/test_cross_package.py b/python/packages/copilotstudio/tests/test_cross_package.py new file mode 100644 index 0000000000..228dafc504 --- /dev/null +++ b/python/packages/copilotstudio/tests/test_cross_package.py @@ -0,0 +1,28 @@ +# Copyright (c) Microsoft. All rights reserved. + + +def test_self_through_main() -> None: + try: + from agent_framework.copilotstudio import __version__ + except ImportError: + __version__ = None + + assert __version__ is not None + + +def test_self() -> None: + try: + from agent_framework_copilotstudio import __version__ + except ImportError: + __version__ = None + + assert __version__ is not None + + +def test_agent_framework() -> None: + try: + from agent_framework import __version__ + except ImportError: + __version__ = None + + assert __version__ is not None diff --git a/python/packages/foundry/pyproject.toml b/python/packages/foundry/pyproject.toml index 5949146499..f601069a94 100644 --- a/python/packages/foundry/pyproject.toml +++ b/python/packages/foundry/pyproject.toml @@ -72,7 +72,6 @@ show_error_codes = true warn_unused_ignores = false disallow_incomplete_defs = true disallow_untyped_decorators = true -disallow_any_unimported = true [tool.bandit] targets = ["agent_framework_foundry"] diff --git a/python/packages/foundry/tests/__init.__.py b/python/packages/foundry/tests/__init.__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/python/packages/foundry/tests/test_foundry_chat_client.py b/python/packages/foundry/tests/test_foundry_chat_client.py index 8a7eec03b6..fa69e21b0b 100644 --- a/python/packages/foundry/tests/test_foundry_chat_client.py +++ b/python/packages/foundry/tests/test_foundry_chat_client.py @@ -421,23 +421,6 @@ def test_foundry_chat_client_create_run_options_with_image_content(mock_ai_proje assert len(message.content) == 1 -def test_foundry_chat_client_convert_function_results_to_tool_output(mock_ai_project_client: MagicMock) -> None: - """Test _convert_function_results_to_tool_output method.""" - - chat_client = create_test_foundry_chat_client(mock_ai_project_client) - - function_results = [ - FunctionResultContent(call_id='["run_123", "call_456"]', result="Result 1"), - FunctionResultContent(call_id='["run_123", "call_789"]', result="Result 2"), - ] - - run_id, tool_outputs = chat_client._convert_function_results_to_tool_output(function_results) # type: ignore - - assert run_id == "run_123" - assert tool_outputs is not None - assert len(tool_outputs) == 2 - - def test_foundry_chat_client_convert_function_results_to_tool_output_none(mock_ai_project_client: MagicMock) -> None: """Test _convert_function_results_to_tool_output with None input.""" chat_client = create_test_foundry_chat_client(mock_ai_project_client) @@ -566,30 +549,6 @@ async def test_foundry_chat_client_get_agent_id_or_create_with_run_options( assert "response_format" in call_args -async def test_foundry_chat_client_create_agent_stream_with_tool_results(mock_ai_project_client: MagicMock) -> None: - """Test _create_agent_stream when tool results match active thread run.""" - chat_client = create_test_foundry_chat_client( - mock_ai_project_client, agent_id="test-agent", thread_id="test-thread" - ) - - mock_thread_run = MagicMock() - mock_thread_run.id = "run_123" - mock_thread_run.thread_id = "test-thread" - - with patch.object(chat_client, "_get_active_thread_run", return_value=mock_thread_run): - tool_results = [FunctionResultContent(call_id='["run_123", "call_456"]', result="Result")] - - mock_handler = MagicMock() - mock_ai_project_client.agents.runs.submit_tool_outputs_stream = AsyncMock(return_value=None) - - with patch("agent_framework_foundry._chat_client.AsyncAgentEventHandler", return_value=mock_handler): - stream, thread_id = await chat_client._create_agent_stream("test-thread", "test-agent", {}, tool_results) # type: ignore - - assert stream is mock_handler - assert thread_id == "test-thread" - mock_ai_project_client.agents.runs.submit_tool_outputs_stream.assert_called_once() - - async def test_foundry_chat_client_prepare_thread_cancels_active_run(mock_ai_project_client: MagicMock) -> None: """Test _prepare_thread cancels active thread run when provided.""" chat_client = create_test_foundry_chat_client(mock_ai_project_client, agent_id="test-agent") diff --git a/python/packages/main/agent_framework/_agents.py b/python/packages/main/agent_framework/_agents.py index c1183a75b4..5c59c8bc55 100644 --- a/python/packages/main/agent_framework/_agents.py +++ b/python/packages/main/agent_framework/_agents.py @@ -300,20 +300,7 @@ class BaseAgent(AFBaseModel): ): self._initialize_middleware_pipelines(self.middlewares) - # Normalize messages to ChatMessage list - if messages is None: - normalized_messages: list[ChatMessage] = [] - elif isinstance(messages, str): - normalized_messages = [ChatMessage(role=Role.USER, text=messages)] - elif isinstance(messages, ChatMessage): - normalized_messages = [messages] - elif isinstance(messages, list): - normalized_messages = [] - for msg in messages: - if isinstance(msg, str): - normalized_messages.append(ChatMessage(role=Role.USER, text=msg)) - elif isinstance(msg, ChatMessage): - normalized_messages.append(msg) + normalized_messages = self._normalize_messages(messages) # Execute with middleware if available if self._agent_middleware_pipeline.has_middlewares: @@ -364,20 +351,7 @@ class BaseAgent(AFBaseModel): ): self._initialize_middleware_pipelines(self.middlewares) - # Normalize messages to ChatMessage list - if messages is None: - normalized_messages: list[ChatMessage] = [] - elif isinstance(messages, str): - normalized_messages = [ChatMessage(role=Role.USER, text=messages)] - elif isinstance(messages, ChatMessage): - normalized_messages = [messages] - elif isinstance(messages, list): - normalized_messages = [] - for msg in messages: - if isinstance(msg, str): - normalized_messages.append(ChatMessage(role=Role.USER, text=msg)) - elif isinstance(msg, ChatMessage): - normalized_messages.append(msg) + normalized_messages = self._normalize_messages(messages) # Execute with middleware if available if self._agent_middleware_pipeline.has_middlewares: @@ -403,6 +377,21 @@ class BaseAgent(AFBaseModel): # No middleware, execute directly return self._run_stream_impl(normalized_messages, thread=thread, **kwargs) + def _normalize_messages( + self, + messages: str | ChatMessage | Sequence[str] | Sequence[ChatMessage] | None = None, + ) -> list[ChatMessage]: + if messages is None: + return [] + + if isinstance(messages, str): + return [ChatMessage(role=Role.USER, text=messages)] + + if isinstance(messages, ChatMessage): + return [messages] + + return [ChatMessage(role=Role.USER, text=msg) if isinstance(msg, str) else msg for msg in messages] + # region ChatAgent @@ -1002,21 +991,6 @@ class ChatAgent(BaseAgent): messages.extend(input_messages or []) return thread, messages - def _normalize_messages( - self, - messages: str | ChatMessage | Sequence[str] | Sequence[ChatMessage] | None = None, - ) -> list[ChatMessage]: - if messages is None: - return [] - - if isinstance(messages, str): - return [ChatMessage(role=Role.USER, text=messages)] - - if isinstance(messages, ChatMessage): - return [messages] - - return [ChatMessage(role=Role.USER, text=msg) if isinstance(msg, str) else msg for msg in messages] - def _get_agent_name(self) -> str: return self.name or "UnnamedAgent" diff --git a/python/packages/main/agent_framework/copilotstudio/__init__.py b/python/packages/main/agent_framework/copilotstudio/__init__.py new file mode 100644 index 0000000000..aff666452b --- /dev/null +++ b/python/packages/main/agent_framework/copilotstudio/__init__.py @@ -0,0 +1,24 @@ +# Copyright (c) Microsoft. All rights reserved. + +import importlib +from typing import Any + +PACKAGE_NAME = "agent_framework_copilotstudio" +PACKAGE_EXTRA = "copilotstudio" +_IMPORTS = ["CopilotStudioAgent", "__version__", "acquire_token"] + + +def __getattr__(name: str) -> Any: + if name in _IMPORTS: + try: + return getattr(importlib.import_module(PACKAGE_NAME), name) + except ModuleNotFoundError as exc: + raise ModuleNotFoundError( + f"The '{PACKAGE_EXTRA}' extra is not installed, " + f"please do `pip install agent-framework[{PACKAGE_EXTRA}]`" + ) from exc + raise AttributeError(f"Module {PACKAGE_NAME} has no attribute {name}.") + + +def __dir__() -> list[str]: + return _IMPORTS diff --git a/python/packages/main/agent_framework/copilotstudio/__init__.pyi b/python/packages/main/agent_framework/copilotstudio/__init__.pyi new file mode 100644 index 0000000000..c30a7b3031 --- /dev/null +++ b/python/packages/main/agent_framework/copilotstudio/__init__.pyi @@ -0,0 +1,5 @@ +# Copyright (c) Microsoft. All rights reserved. + +from agent_framework_copilotstudio import CopilotStudioAgent, __version__, acquire_token + +__all__ = ["CopilotStudioAgent", "__version__", "acquire_token"] diff --git a/python/packages/main/agent_framework/workflow/__init__.py b/python/packages/main/agent_framework/workflow/__init__.py index cb8cd56fcf..70f9bf49ea 100644 --- a/python/packages/main/agent_framework/workflow/__init__.py +++ b/python/packages/main/agent_framework/workflow/__init__.py @@ -40,6 +40,7 @@ _IMPORTS = [ "WorkflowAgent", "WorkflowViz", "FileCheckpointStorage", + "ExecutorFailedEvent", "InMemoryCheckpointStorage", "CheckpointStorage", "WorkflowCheckpoint", @@ -56,7 +57,12 @@ _IMPORTS = [ "PlanReviewRequest", "RequestInfoEvent", "StandardMagenticManager", + "WorkflowStatusEvent", + "WorkflowRunState", + "WorkflowErrorDetails", + "WorkflowFailedEvent", "ConcurrentBuilder", + "SequentialBuilder", ] diff --git a/python/packages/main/agent_framework/workflow/__init__.pyi b/python/packages/main/agent_framework/workflow/__init__.pyi index ea88728525..b50c2007a2 100644 --- a/python/packages/main/agent_framework/workflow/__init__.pyi +++ b/python/packages/main/agent_framework/workflow/__init__.pyi @@ -13,6 +13,7 @@ from agent_framework_workflow import ( Executor, ExecutorCompletedEvent, ExecutorEvent, + ExecutorFailedEvent, ExecutorInvokeEvent, FileCheckpointStorage, FunctionExecutor, @@ -29,6 +30,7 @@ from agent_framework_workflow import ( RequestInfoExecutor, RequestInfoMessage, RequestResponse, + SequentialBuilder, StandardMagenticManager, SubWorkflowRequestInfo, SubWorkflowResponse, @@ -38,10 +40,14 @@ from agent_framework_workflow import ( WorkflowCheckpoint, WorkflowCompletedEvent, WorkflowContext, + WorkflowErrorDetails, WorkflowEvent, WorkflowExecutor, + WorkflowFailedEvent, WorkflowRunResult, + WorkflowRunState, WorkflowStartedEvent, + WorkflowStatusEvent, WorkflowViz, __version__, executor, @@ -62,6 +68,7 @@ __all__ = [ "Executor", "ExecutorCompletedEvent", "ExecutorEvent", + "ExecutorFailedEvent", "ExecutorInvokeEvent", "FileCheckpointStorage", "FunctionExecutor", @@ -78,6 +85,7 @@ __all__ = [ "RequestInfoExecutor", "RequestInfoMessage", "RequestResponse", + "SequentialBuilder", "StandardMagenticManager", "SubWorkflowRequestInfo", "SubWorkflowResponse", @@ -87,10 +95,14 @@ __all__ = [ "WorkflowCheckpoint", "WorkflowCompletedEvent", "WorkflowContext", + "WorkflowErrorDetails", "WorkflowEvent", "WorkflowExecutor", + "WorkflowFailedEvent", "WorkflowRunResult", + "WorkflowRunState", "WorkflowStartedEvent", + "WorkflowStatusEvent", "WorkflowViz", "__version__", "executor", diff --git a/python/packages/main/pyproject.toml b/python/packages/main/pyproject.toml index 51c658b03e..b5e1bfd6c8 100644 --- a/python/packages/main/pyproject.toml +++ b/python/packages/main/pyproject.toml @@ -94,7 +94,6 @@ show_error_codes = true warn_unused_ignores = false disallow_incomplete_defs = true disallow_untyped_decorators = true -disallow_any_unimported = true [tool.bandit] targets = ["agent_framework"] diff --git a/python/packages/mem0/agent_framework_mem0/_provider.py b/python/packages/mem0/agent_framework_mem0/_provider.py index 7fb2bbbf65..248f007d66 100644 --- a/python/packages/mem0/agent_framework_mem0/_provider.py +++ b/python/packages/mem0/agent_framework_mem0/_provider.py @@ -6,6 +6,7 @@ from typing import Any, Final from agent_framework import ChatMessage, Context, ContextProvider, TextContent from agent_framework.exceptions import ServiceInitializationError +from mem0 import AsyncMemoryClient from pydantic import PrivateAttr if sys.version_info >= (3, 11): @@ -18,6 +19,7 @@ DEFAULT_CONTEXT_PROMPT: Final[str] = "## Memories\nConsider the following memori class Mem0Provider(ContextProvider): + mem0_client: AsyncMemoryClient api_key: str | None = None application_id: str | None = None agent_id: str | None = None @@ -25,8 +27,6 @@ class Mem0Provider(ContextProvider): user_id: str | None = None scope_to_per_operation_thread_id: bool = False context_prompt: str = DEFAULT_CONTEXT_PROMPT - # Use Any to avoid forward reference issues with AsyncMemoryClient - mem0_client: Any = None _should_close_client: bool = PrivateAttr(default=False) # Track whether we should close client connection @@ -39,7 +39,7 @@ class Mem0Provider(ContextProvider): user_id: str | None = None, scope_to_per_operation_thread_id: bool = False, context_prompt: str = DEFAULT_CONTEXT_PROMPT, - mem0_client: Any = None, + mem0_client: AsyncMemoryClient | None = None, ) -> None: """Initializes a new instance of the Mem0Provider class. @@ -56,8 +56,6 @@ class Mem0Provider(ContextProvider): """ should_close_client = False if mem0_client is None: - from mem0 import AsyncMemoryClient - mem0_client = AsyncMemoryClient(api_key=api_key) should_close_client = True @@ -84,7 +82,7 @@ class Mem0Provider(ContextProvider): async def __aexit__(self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: Any) -> None: """Async context manager exit.""" if self._should_close_client and self.mem0_client: - await self.mem0_client.__aexit__(exc_type, exc_val, exc_tb) + await self.mem0_client.__aexit__(exc_type, exc_val, exc_tb) # type: ignore async def thread_created(self, thread_id: str | None = None) -> None: """Called when a new thread is created. diff --git a/python/packages/mem0/pyproject.toml b/python/packages/mem0/pyproject.toml index 64619181b5..dc3324db79 100644 --- a/python/packages/mem0/pyproject.toml +++ b/python/packages/mem0/pyproject.toml @@ -72,7 +72,6 @@ show_error_codes = true warn_unused_ignores = false disallow_incomplete_defs = true disallow_untyped_decorators = true -disallow_any_unimported = true [tool.bandit] targets = ["agent_framework_mem0"] diff --git a/python/packages/mem0/tests/__init__.py b/python/packages/mem0/tests/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/python/packages/mem0/tests/test_provider.py b/python/packages/mem0/tests/test_provider.py index 812eb8ac2a..05d5563d5f 100644 --- a/python/packages/mem0/tests/test_provider.py +++ b/python/packages/mem0/tests/test_provider.py @@ -9,7 +9,7 @@ from agent_framework.exceptions import ServiceInitializationError from agent_framework.mem0 import Mem0Provider -def test_mem0_provider_import(): +def test_mem0_provider_import() -> None: """Test that Mem0Provider can be imported.""" assert Mem0Provider is not None @@ -17,7 +17,9 @@ def test_mem0_provider_import(): @pytest.fixture def mock_mem0_client() -> AsyncMock: """Create a mock Mem0 AsyncMemoryClient.""" - mock_client = AsyncMock() + from mem0 import AsyncMemoryClient + + mock_client = AsyncMock(spec=AsyncMemoryClient) mock_client.add = AsyncMock() mock_client.search = AsyncMock() mock_client.__aenter__ = AsyncMock(return_value=mock_client) @@ -40,7 +42,7 @@ def sample_messages() -> list[ChatMessage]: class TestMem0ProviderInitialization: """Test initialization and configuration of Mem0Provider.""" - def test_init_with_all_ids(self, mock_mem0_client: AsyncMock): + def test_init_with_all_ids(self, mock_mem0_client: AsyncMock) -> None: """Test initialization with all IDs provided.""" provider = Mem0Provider( user_id="user123", @@ -54,7 +56,7 @@ class TestMem0ProviderInitialization: assert provider.application_id == "app123" assert provider.thread_id == "thread123" - def test_init_without_filters_succeeds(self, mock_mem0_client: AsyncMock): + def test_init_without_filters_succeeds(self, mock_mem0_client: AsyncMock) -> None: """Test that initialization succeeds even without filters (validation happens during invocation).""" provider = Mem0Provider(mem0_client=mock_mem0_client) assert provider.user_id is None @@ -62,13 +64,13 @@ class TestMem0ProviderInitialization: assert provider.application_id is None assert provider.thread_id is None - def test_init_with_custom_context_prompt(self, mock_mem0_client: AsyncMock): + def test_init_with_custom_context_prompt(self, mock_mem0_client: AsyncMock) -> None: """Test initialization with custom context prompt.""" custom_prompt = "## Custom Memories\nConsider these memories:" provider = Mem0Provider(user_id="user123", context_prompt=custom_prompt, mem0_client=mock_mem0_client) assert provider.context_prompt == custom_prompt - def test_init_with_scope_to_per_operation_thread_id(self, mock_mem0_client: AsyncMock): + def test_init_with_scope_to_per_operation_thread_id(self, mock_mem0_client: AsyncMock) -> None: """Test initialization with scope_to_per_operation_thread_id enabled.""" provider = Mem0Provider( user_id="user123", @@ -77,10 +79,12 @@ class TestMem0ProviderInitialization: ) assert provider.scope_to_per_operation_thread_id is True - @patch("mem0.AsyncMemoryClient") - def test_init_creates_default_client_when_none_provided(self, mock_memory_client_class: AsyncMock): + @patch("agent_framework_mem0._provider.AsyncMemoryClient") + def test_init_creates_default_client_when_none_provided(self, mock_memory_client_class: AsyncMock) -> None: """Test that a default client is created when none is provided.""" - mock_client = AsyncMock() + from mem0 import AsyncMemoryClient + + mock_client = AsyncMock(spec=AsyncMemoryClient) mock_memory_client_class.return_value = mock_client provider = Mem0Provider(user_id="user123", api_key="test_api_key") @@ -89,7 +93,7 @@ class TestMem0ProviderInitialization: assert provider.mem0_client == mock_client assert provider._should_close_client is True - def test_init_with_provided_client_should_not_close(self, mock_mem0_client: AsyncMock): + def test_init_with_provided_client_should_not_close(self, mock_mem0_client: AsyncMock) -> None: """Test that provided client should not be closed by provider.""" provider = Mem0Provider(user_id="user123", mem0_client=mock_mem0_client) assert provider._should_close_client is False @@ -98,21 +102,23 @@ class TestMem0ProviderInitialization: class TestMem0ProviderAsyncContextManager: """Test async context manager behavior.""" - async def test_async_context_manager_entry(self, mock_mem0_client: AsyncMock): + async def test_async_context_manager_entry(self, mock_mem0_client: AsyncMock) -> None: """Test async context manager entry returns self.""" provider = Mem0Provider(user_id="user123", mem0_client=mock_mem0_client) async with provider as ctx: assert ctx is provider - async def test_async_context_manager_exit_closes_client_when_should_close(self): + async def test_async_context_manager_exit_closes_client_when_should_close(self) -> None: """Test that async context manager closes client when it should.""" - mock_client = AsyncMock() + from mem0 import AsyncMemoryClient + + mock_client = AsyncMock(spec=AsyncMemoryClient) mock_client.__aenter__ = AsyncMock(return_value=mock_client) mock_client.__aexit__ = AsyncMock() mock_client.async_client = AsyncMock() mock_client.async_client.aclose = AsyncMock() - with patch("mem0.AsyncMemoryClient", return_value=mock_client): + with patch("agent_framework_mem0._provider.AsyncMemoryClient", return_value=mock_client): provider = Mem0Provider(user_id="user123", api_key="test_key") assert provider._should_close_client is True @@ -121,7 +127,7 @@ class TestMem0ProviderAsyncContextManager: mock_client.__aexit__.assert_called_once() - async def test_async_context_manager_exit_does_not_close_provided_client(self, mock_mem0_client: AsyncMock): + async def test_async_context_manager_exit_does_not_close_provided_client(self, mock_mem0_client: AsyncMock) -> None: """Test that async context manager does not close provided client.""" provider = Mem0Provider(user_id="user123", mem0_client=mock_mem0_client) assert provider._should_close_client is False @@ -135,7 +141,7 @@ class TestMem0ProviderAsyncContextManager: class TestMem0ProviderThreadMethods: """Test thread lifecycle methods.""" - async def test_thread_created_sets_per_operation_thread_id(self, mock_mem0_client: AsyncMock): + async def test_thread_created_sets_per_operation_thread_id(self, mock_mem0_client: AsyncMock) -> None: """Test that thread_created sets per-operation thread ID.""" provider = Mem0Provider(user_id="user123", mem0_client=mock_mem0_client) @@ -143,7 +149,7 @@ class TestMem0ProviderThreadMethods: assert provider._per_operation_thread_id == "thread123" - async def test_thread_created_with_existing_thread_id(self, mock_mem0_client: AsyncMock): + async def test_thread_created_with_existing_thread_id(self, mock_mem0_client: AsyncMock) -> None: """Test thread_created when thread ID already exists.""" provider = Mem0Provider(user_id="user123", mem0_client=mock_mem0_client) provider._per_operation_thread_id = "existing_thread" @@ -153,7 +159,7 @@ class TestMem0ProviderThreadMethods: # Should not overwrite existing thread ID assert provider._per_operation_thread_id == "existing_thread" - async def test_thread_created_validation_with_scope_enabled(self, mock_mem0_client: AsyncMock): + async def test_thread_created_validation_with_scope_enabled(self, mock_mem0_client: AsyncMock) -> None: """Test thread_created validation when scope_to_per_operation_thread_id is enabled.""" provider = Mem0Provider( user_id="user123", @@ -169,7 +175,7 @@ class TestMem0ProviderThreadMethods: async def test_messages_adding_sets_per_operation_thread_id( self, mock_mem0_client: AsyncMock, sample_messages: list[ChatMessage] - ): + ) -> None: """Test that messages_adding sets per-operation thread ID.""" provider = Mem0Provider(user_id="user123", mem0_client=mock_mem0_client) @@ -181,7 +187,7 @@ class TestMem0ProviderThreadMethods: class TestMem0ProviderMessagesAdding: """Test messages_adding method.""" - async def test_messages_adding_fails_without_filters(self, mock_mem0_client: AsyncMock): + async def test_messages_adding_fails_without_filters(self, mock_mem0_client: AsyncMock) -> None: """Test that messages_adding fails when no filters are provided.""" provider = Mem0Provider(mem0_client=mock_mem0_client) message = ChatMessage(role=Role.USER, text="Hello!") @@ -191,7 +197,7 @@ class TestMem0ProviderMessagesAdding: assert "At least one of the filters" in str(exc_info.value) - async def test_messages_adding_single_message(self, mock_mem0_client: AsyncMock): + async def test_messages_adding_single_message(self, mock_mem0_client: AsyncMock) -> None: """Test adding a single message.""" provider = Mem0Provider(user_id="user123", mem0_client=mock_mem0_client) message = ChatMessage(role=Role.USER, text="Hello!") @@ -205,7 +211,7 @@ class TestMem0ProviderMessagesAdding: async def test_messages_adding_multiple_messages( self, mock_mem0_client: AsyncMock, sample_messages: list[ChatMessage] - ): + ) -> None: """Test adding multiple messages.""" provider = Mem0Provider(user_id="user123", mem0_client=mock_mem0_client) @@ -220,7 +226,9 @@ class TestMem0ProviderMessagesAdding: ] assert call_args.kwargs["messages"] == expected_messages - async def test_messages_adding_with_agent_id(self, mock_mem0_client: AsyncMock, sample_messages: list[ChatMessage]): + async def test_messages_adding_with_agent_id( + self, mock_mem0_client: AsyncMock, sample_messages: list[ChatMessage] + ) -> None: """Test adding messages with agent_id.""" provider = Mem0Provider(agent_id="agent123", mem0_client=mock_mem0_client) @@ -232,7 +240,7 @@ class TestMem0ProviderMessagesAdding: async def test_messages_adding_with_application_id( self, mock_mem0_client: AsyncMock, sample_messages: list[ChatMessage] - ): + ) -> None: """Test adding messages with application_id in metadata.""" provider = Mem0Provider(user_id="user123", application_id="app123", mem0_client=mock_mem0_client) @@ -243,7 +251,7 @@ class TestMem0ProviderMessagesAdding: async def test_messages_adding_with_scope_to_per_operation_thread_id( self, mock_mem0_client: AsyncMock, sample_messages: list[ChatMessage] - ): + ) -> None: """Test adding messages with scope_to_per_operation_thread_id enabled.""" provider = Mem0Provider( user_id="user123", @@ -260,7 +268,7 @@ class TestMem0ProviderMessagesAdding: async def test_messages_adding_without_scope_uses_base_thread_id( self, mock_mem0_client: AsyncMock, sample_messages: list[ChatMessage] - ): + ) -> None: """Test adding messages without scope uses base thread_id.""" provider = Mem0Provider( user_id="user123", @@ -274,7 +282,7 @@ class TestMem0ProviderMessagesAdding: call_args = mock_mem0_client.add.call_args assert call_args.kwargs["run_id"] == "base_thread" - async def test_messages_adding_filters_empty_messages(self, mock_mem0_client: AsyncMock): + async def test_messages_adding_filters_empty_messages(self, mock_mem0_client: AsyncMock) -> None: """Test that empty or invalid messages are filtered out.""" provider = Mem0Provider(user_id="user123", mem0_client=mock_mem0_client) messages = [ @@ -289,7 +297,7 @@ class TestMem0ProviderMessagesAdding: # Should only include the valid message assert call_args.kwargs["messages"] == [{"role": "user", "content": "Valid message"}] - async def test_messages_adding_skips_when_no_valid_messages(self, mock_mem0_client: AsyncMock): + async def test_messages_adding_skips_when_no_valid_messages(self, mock_mem0_client: AsyncMock) -> None: """Test that mem0 client is not called when no valid messages exist.""" provider = Mem0Provider(user_id="user123", mem0_client=mock_mem0_client) messages = [ @@ -305,7 +313,7 @@ class TestMem0ProviderMessagesAdding: class TestMem0ProviderModelInvoking: """Test model_invoking method.""" - async def test_model_invoking_fails_without_filters(self, mock_mem0_client: AsyncMock): + async def test_model_invoking_fails_without_filters(self, mock_mem0_client: AsyncMock) -> None: """Test that model_invoking fails when no filters are provided.""" provider = Mem0Provider(mem0_client=mock_mem0_client) message = ChatMessage(role=Role.USER, text="What's the weather?") @@ -315,7 +323,7 @@ class TestMem0ProviderModelInvoking: assert "At least one of the filters" in str(exc_info.value) - async def test_model_invoking_single_message(self, mock_mem0_client: AsyncMock): + async def test_model_invoking_single_message(self, mock_mem0_client: AsyncMock) -> None: """Test model_invoking with a single message.""" provider = Mem0Provider(user_id="user123", mem0_client=mock_mem0_client) message = ChatMessage(role=Role.USER, text="What's the weather?") @@ -345,7 +353,7 @@ class TestMem0ProviderModelInvoking: async def test_model_invoking_multiple_messages( self, mock_mem0_client: AsyncMock, sample_messages: list[ChatMessage] - ): + ) -> None: """Test model_invoking with multiple messages.""" provider = Mem0Provider(user_id="user123", mem0_client=mock_mem0_client) @@ -357,7 +365,7 @@ class TestMem0ProviderModelInvoking: expected_query = "Hello, how are you?\nI'm doing well, thank you!\nYou are a helpful assistant" assert call_args.kwargs["query"] == expected_query - async def test_model_invoking_with_agent_id(self, mock_mem0_client: AsyncMock): + async def test_model_invoking_with_agent_id(self, mock_mem0_client: AsyncMock) -> None: """Test model_invoking with agent_id.""" provider = Mem0Provider(agent_id="agent123", mem0_client=mock_mem0_client) message = ChatMessage(role=Role.USER, text="Hello") @@ -370,7 +378,7 @@ class TestMem0ProviderModelInvoking: assert call_args.kwargs["agent_id"] == "agent123" assert call_args.kwargs["user_id"] is None - async def test_model_invoking_with_scope_to_per_operation_thread_id(self, mock_mem0_client: AsyncMock): + async def test_model_invoking_with_scope_to_per_operation_thread_id(self, mock_mem0_client: AsyncMock) -> None: """Test model_invoking with scope_to_per_operation_thread_id enabled.""" provider = Mem0Provider( user_id="user123", @@ -388,7 +396,7 @@ class TestMem0ProviderModelInvoking: call_args = mock_mem0_client.search.call_args assert call_args.kwargs["run_id"] == "operation_thread" - async def test_model_invoking_no_memories_returns_none_instructions(self, mock_mem0_client: AsyncMock): + async def test_model_invoking_no_memories_returns_none_instructions(self, mock_mem0_client: AsyncMock) -> None: """Test that no memories returns context with None instructions.""" provider = Mem0Provider(user_id="user123", mem0_client=mock_mem0_client) message = ChatMessage(role=Role.USER, text="Hello") @@ -400,7 +408,7 @@ class TestMem0ProviderModelInvoking: assert isinstance(context, Context) assert not context.contents - async def test_model_invoking_filters_empty_message_text(self, mock_mem0_client: AsyncMock): + async def test_model_invoking_filters_empty_message_text(self, mock_mem0_client: AsyncMock) -> None: """Test that empty message text is filtered out from query.""" provider = Mem0Provider(user_id="user123", mem0_client=mock_mem0_client) messages = [ @@ -416,7 +424,7 @@ class TestMem0ProviderModelInvoking: call_args = mock_mem0_client.search.call_args assert call_args.kwargs["query"] == "Valid message" - async def test_model_invoking_custom_context_prompt(self, mock_mem0_client: AsyncMock): + async def test_model_invoking_custom_context_prompt(self, mock_mem0_client: AsyncMock) -> None: """Test model_invoking with custom context prompt.""" custom_prompt = "## Custom Context\nRemember these details:" provider = Mem0Provider( @@ -439,7 +447,7 @@ class TestMem0ProviderModelInvoking: class TestMem0ProviderValidation: """Test validation methods.""" - def test_validate_per_operation_thread_id_success(self, mock_mem0_client: AsyncMock): + def test_validate_per_operation_thread_id_success(self, mock_mem0_client: AsyncMock) -> None: """Test successful validation of per-operation thread ID.""" provider = Mem0Provider( user_id="user123", @@ -454,7 +462,7 @@ class TestMem0ProviderValidation: # Should not raise exception for None provider._validate_per_operation_thread_id(None) - def test_validate_per_operation_thread_id_failure(self, mock_mem0_client: AsyncMock): + def test_validate_per_operation_thread_id_failure(self, mock_mem0_client: AsyncMock) -> None: """Test validation failure for conflicting thread IDs.""" provider = Mem0Provider( user_id="user123", @@ -468,7 +476,7 @@ class TestMem0ProviderValidation: assert "can only be used with one thread at a time" in str(exc_info.value) - def test_validate_per_operation_thread_id_disabled_scope(self, mock_mem0_client: AsyncMock): + def test_validate_per_operation_thread_id_disabled_scope(self, mock_mem0_client: AsyncMock) -> None: """Test that validation is skipped when scope is disabled.""" provider = Mem0Provider( user_id="user123", diff --git a/python/packages/runtime/pyproject.toml b/python/packages/runtime/pyproject.toml index 8fddae25d0..34c2980028 100644 --- a/python/packages/runtime/pyproject.toml +++ b/python/packages/runtime/pyproject.toml @@ -69,7 +69,6 @@ show_error_codes = true warn_unused_ignores = false disallow_incomplete_defs = true disallow_untyped_decorators = true -disallow_any_unimported = true [tool.bandit] targets = ["agent_framework_runtime"] diff --git a/python/packages/workflow/agent_framework_workflow/__init__.py b/python/packages/workflow/agent_framework_workflow/__init__.py index 5d3e1352fb..61094b9eb6 100644 --- a/python/packages/workflow/agent_framework_workflow/__init__.py +++ b/python/packages/workflow/agent_framework_workflow/__init__.py @@ -19,11 +19,16 @@ from ._events import ( AgentRunUpdateEvent, ExecutorCompletedEvent, ExecutorEvent, + ExecutorFailedEvent, ExecutorInvokeEvent, RequestInfoEvent, WorkflowCompletedEvent, + WorkflowErrorDetails, WorkflowEvent, + WorkflowFailedEvent, + WorkflowRunState, WorkflowStartedEvent, + WorkflowStatusEvent, ) from ._executor import ( AgentExecutor, @@ -67,6 +72,7 @@ from ._runner_context import ( Message, RunnerContext, ) +from ._sequential import SequentialBuilder from ._validation import ( EdgeDuplicationError, GraphConnectivityError, @@ -100,6 +106,7 @@ __all__ = [ "Executor", "ExecutorCompletedEvent", "ExecutorEvent", + "ExecutorFailedEvent", "ExecutorInvokeEvent", "FileCheckpointStorage", "FunctionExecutor", @@ -131,6 +138,7 @@ __all__ = [ "RequestInfoMessage", "RequestResponse", "RunnerContext", + "SequentialBuilder", "StandardMagenticManager", "SubWorkflowRequestInfo", "SubWorkflowResponse", @@ -142,10 +150,14 @@ __all__ = [ "WorkflowCheckpoint", "WorkflowCompletedEvent", "WorkflowContext", + "WorkflowErrorDetails", "WorkflowEvent", "WorkflowExecutor", + "WorkflowFailedEvent", "WorkflowRunResult", + "WorkflowRunState", "WorkflowStartedEvent", + "WorkflowStatusEvent", "WorkflowValidationError", "WorkflowViz", "__version__", diff --git a/python/packages/workflow/agent_framework_workflow/_agent.py b/python/packages/workflow/agent_framework_workflow/_agent.py index e2c126969d..73a013bb41 100644 --- a/python/packages/workflow/agent_framework_workflow/_agent.py +++ b/python/packages/workflow/agent_framework_workflow/_agent.py @@ -2,7 +2,7 @@ import logging import uuid -from collections.abc import AsyncIterable +from collections.abc import AsyncIterable, Sequence from datetime import datetime from typing import TYPE_CHECKING, Any, ClassVar, TypedDict, cast @@ -206,7 +206,7 @@ class WorkflowAgent(BaseAgent): def _normalize_messages( self, - messages: str | ChatMessage | list[str] | list[ChatMessage] | None = None, + messages: str | ChatMessage | Sequence[str] | Sequence[ChatMessage] | None = None, ) -> list[ChatMessage]: """Normalize input messages to a list of ChatMessage objects.""" if messages is None: @@ -218,7 +218,7 @@ class WorkflowAgent(BaseAgent): if isinstance(messages, ChatMessage): return [messages] - normalized = [] + normalized: list[ChatMessage] = [] for msg in messages: if isinstance(msg, str): normalized.append(ChatMessage(role=Role.USER, contents=[TextContent(text=msg)])) diff --git a/python/packages/workflow/agent_framework_workflow/_events.py b/python/packages/workflow/agent_framework_workflow/_events.py index 67075a617f..a04136eb0e 100644 --- a/python/packages/workflow/agent_framework_workflow/_events.py +++ b/python/packages/workflow/agent_framework_workflow/_events.py @@ -1,5 +1,8 @@ # Copyright (c) Microsoft. All rights reserved. +import traceback as _traceback +from dataclasses import dataclass +from enum import Enum from typing import TYPE_CHECKING, Any from agent_framework import AgentRunResponse, AgentRunResponseUpdate @@ -56,6 +59,108 @@ class WorkflowErrorEvent(WorkflowEvent): return f"{self.__class__.__name__}(exception={self.data})" +class WorkflowRunState(str, Enum): + """Run-level state of a workflow execution. + + Semantics: + - STARTED: Run has been initiated and the workflow context has been created. + This is an initial state before any meaningful work is performed. In this + codebase we emit a dedicated `WorkflowStartedEvent` for telemetry, and + typically advance the status directly to `IN_PROGRESS`. Consumers may + still rely on `STARTED` for state machines that need an explicit pre-work + phase. + + - IN_PROGRESS: The workflow is actively executing (e.g., the initial + message has been delivered to the start executor or a superstep is + running). This status is emitted at the beginning of a run and can be + followed by other statuses as the run progresses. + + - IN_PROGRESS_PENDING_REQUESTS: Active execution while one or more + request-for-information operations are outstanding. New work may still + be scheduled while requests are in flight. + + - IDLE: The workflow is quiescent with no outstanding requests, but has + not yet emitted a terminal result. Rare in practice but provided for + orchestration integrations that distinguish a quiescent state. + + - IDLE_WITH_PENDING_REQUESTS: The workflow is paused awaiting external + input (e.g., emitted a `RequestInfoEvent`). This is a non-terminal + state; the workflow can resume when responses are supplied. + + - COMPLETED: Normal terminal state indicating successful completion. + + - FAILED: Terminal state indicating an error surfaced. Accompanied by a + `WorkflowFailedEvent` with structured error details. + + - CANCELLED: Terminal state indicating the run was cancelled by a caller + or orchestrator. Not currently emitted by default runner paths but + included for integrators/orchestrators that support cancellation. + """ + + STARTED = "STARTED" # Explicit pre-work phase (rarely emitted as status; see note above) + IN_PROGRESS = "IN_PROGRESS" # Active execution is underway + IN_PROGRESS_PENDING_REQUESTS = "IN_PROGRESS_PENDING_REQUESTS" # Active execution with outstanding requests + IDLE = "IDLE" # No active work and no outstanding requests + IDLE_WITH_PENDING_REQUESTS = "IDLE_WITH_PENDING_REQUESTS" # Paused awaiting external responses + COMPLETED = "COMPLETED" # Finished successfully + FAILED = "FAILED" # Finished with an error + CANCELLED = "CANCELLED" # Finished due to cancellation + + +class WorkflowStatusEvent(WorkflowEvent): + """Event indicating a transition in the workflow run state.""" + + def __init__(self, state: WorkflowRunState, data: Any | None = None): + super().__init__(data) + self.state = state + + def __repr__(self) -> str: # pragma: no cover - representation only + return f"{self.__class__.__name__}(state={self.state}, data={self.data!r})" + + +@dataclass +class WorkflowErrorDetails: + """Structured error information to surface in error events/results.""" + + error_type: str + message: str + traceback: str | None = None + executor_id: str | None = None + extra: dict[str, Any] | None = None + + @classmethod + def from_exception( + cls, + exc: BaseException, + *, + executor_id: str | None = None, + extra: dict[str, Any] | None = None, + ) -> "WorkflowErrorDetails": + tb = None + try: + tb = "".join(_traceback.format_exception(type(exc), exc, exc.__traceback__)) + except Exception: + tb = None + return cls( + error_type=exc.__class__.__name__, + message=str(exc), + traceback=tb, + executor_id=executor_id, + extra=extra, + ) + + +class WorkflowFailedEvent(WorkflowEvent): + """Terminal failure event for a workflow run.""" + + def __init__(self, details: WorkflowErrorDetails, data: Any | None = None): + super().__init__(data) + self.details = details + + def __repr__(self) -> str: # pragma: no cover - representation only + return f"{self.__class__.__name__}(details={self.details}, data={self.data!r})" + + class RequestInfoEvent(WorkflowEvent): """Event triggered when a workflow executor requests external information.""" @@ -119,6 +224,17 @@ class ExecutorCompletedEvent(ExecutorEvent): return f"{self.__class__.__name__}(executor_id={self.executor_id})" +class ExecutorFailedEvent(ExecutorEvent): + """Event triggered when an executor handler raises an error.""" + + def __init__(self, executor_id: str, details: WorkflowErrorDetails): + super().__init__(executor_id, details) + self.details = details + + def __repr__(self) -> str: # pragma: no cover - representation only + return f"{self.__class__.__name__}(executor_id={self.executor_id}, details={self.details})" + + class AgentRunUpdateEvent(ExecutorEvent): """Event triggered when an agent is streaming messages.""" diff --git a/python/packages/workflow/agent_framework_workflow/_executor.py b/python/packages/workflow/agent_framework_workflow/_executor.py index 0a7135047f..b4005cd152 100644 --- a/python/packages/workflow/agent_framework_workflow/_executor.py +++ b/python/packages/workflow/agent_framework_workflow/_executor.py @@ -102,7 +102,14 @@ class Executor(AFBaseModel): if self._request_interceptors and message.__class__.__name__ == "SubWorkflowRequestInfo": # Directly handle SubWorkflowRequestInfo await context.add_event(ExecutorInvokeEvent(self.id)) - await self._handle_sub_workflow_request(message, context) + try: + await self._handle_sub_workflow_request(message, context) + except Exception as exc: + # Surface structured executor failure before propagating + from ._events import ExecutorFailedEvent, WorkflowErrorDetails + + await context.add_event(ExecutorFailedEvent(self.id, WorkflowErrorDetails.from_exception(exc))) + raise await context.add_event(ExecutorCompletedEvent(self.id)) return @@ -115,7 +122,14 @@ class Executor(AFBaseModel): if handler is None: raise RuntimeError(f"Executor {self.__class__.__name__} cannot handle message of type {type(message)}.") await context.add_event(ExecutorInvokeEvent(self.id)) - await handler(message, context) + try: + await handler(message, context) + except Exception as exc: + # Surface structured executor failure before propagating + from ._events import ExecutorFailedEvent, WorkflowErrorDetails + + await context.add_event(ExecutorFailedEvent(self.id, WorkflowErrorDetails.from_exception(exc))) + raise await context.add_event(ExecutorCompletedEvent(self.id)) def _discover_handlers(self) -> None: @@ -855,11 +869,10 @@ class AgentExecutor(Executor): ) await ctx.add_event(AgentRunEvent(self.id, response)) - full_conversation: list[ChatMessage] | None = None - if self._cache: - # Construct conversation snapshot = inputs (cache) + agent outputs (agent_run_response.messages). - # Do not mutate response.messages so AgentRunEvent remains clean. - full_conversation = list(self._cache) + list(response.messages) + # Always construct a full conversation snapshot from inputs (cache) + # plus agent outputs (agent_run_response.messages). Do not mutate + # response.messages so AgentRunEvent remains faithful to the raw output. + full_conversation: list[ChatMessage] = list(self._cache) + list(response.messages) agent_response = AgentExecutorResponse(self.id, response, full_conversation=full_conversation) await ctx.send_message(agent_response) @@ -874,41 +887,7 @@ class AgentExecutor(Executor): """ self._cache.extend(request.messages) if request.should_respond: - if self._streaming: - updates: list[AgentRunResponseUpdate] = [] - async for update in self._agent.run_stream( - self._cache, - thread=self._agent_thread, - ): - if not update: - continue - contents = getattr(update, "contents", None) - text_val = getattr(update, "text", "") - has_text_content = False - if contents: - for c in contents: - if getattr(c, "text", None): - has_text_content = True - break - if not (text_val or has_text_content): - continue - updates.append(update) - await ctx.add_event(AgentRunUpdateEvent(self.id, update)) - response = AgentRunResponse.from_agent_run_response_updates(updates) - else: - response = await self._agent.run( - self._cache, - thread=self._agent_thread, - ) - await ctx.add_event(AgentRunEvent(self.id, response)) - - full_conversation: list[ChatMessage] | None = None - if self._cache: - full_conversation = list(self._cache) + list(response.messages) - - agent_response = AgentExecutorResponse(self.id, response, full_conversation=full_conversation) - await ctx.send_message(agent_response) - self._cache.clear() + await self._run_agent_and_emit(ctx) @handler async def from_response(self, prior: AgentExecutorResponse, ctx: WorkflowContext[AgentExecutorResponse]) -> None: diff --git a/python/packages/workflow/agent_framework_workflow/_runner.py b/python/packages/workflow/agent_framework_workflow/_runner.py index bfdca5c27d..05e81619df 100644 --- a/python/packages/workflow/agent_framework_workflow/_runner.py +++ b/python/packages/workflow/agent_framework_workflow/_runner.py @@ -113,8 +113,15 @@ class Runner: # Periodically continue to let iteration advance continue - # Propagate errors from iteration - await iteration_task + # Propagate errors from iteration, but first surface any pending events + try: + await iteration_task + except Exception: + # Make sure failure-related events (like ExecutorFailedEvent) are surfaced + if await self._ctx.has_events(): + for event in await self._ctx.drain_events(): + yield event + raise self._iteration += 1 # Drain any straggler events emitted at tail end diff --git a/python/packages/workflow/agent_framework_workflow/_sequential.py b/python/packages/workflow/agent_framework_workflow/_sequential.py new file mode 100644 index 0000000000..4271d3f7d7 --- /dev/null +++ b/python/packages/workflow/agent_framework_workflow/_sequential.py @@ -0,0 +1,187 @@ +# Copyright (c) Microsoft. All rights reserved. + +"""Sequential builder for agent/executor workflows with shared conversation context. + +This module provides a high-level, agent-focused API to assemble a sequential +workflow where: +- Participants are a sequence of AgentProtocol instances or Executors +- A shared conversation context (list[ChatMessage]) is passed along the chain +- Agents append their assistant messages to the context +- Custom executors can transform or summarize and return a refined context +- The workflow completes with the final context produced by the last participant + +Typical wiring: + input -> _InputToConversation -> participant1 -> (agent? -> _ResponseToConversation) -> ... -> participantN -> _CompleteWithConversation + +Notes: +- Participants can mix AgentProtocol and Executor objects +- Agents are auto-wrapped by WorkflowBuilder as AgentExecutor +- AgentExecutor produces AgentExecutorResponse; _ResponseToConversation converts this to list[ChatMessage] +- Non-agent executors must define a handler that consumes `list[ChatMessage]` and sends back + the updated `list[ChatMessage]` via their workflow context + +Why include the small internal adapter executors? +- Input normalization ("input-conversation"): ensures the workflow always starts with a + `list[ChatMessage]` regardless of whether callers pass a `str`, a single `ChatMessage`, + or a list. This keeps the first hop strongly typed and avoids boilerplate in participants. +- Agent response adaptation ("to-conversation:"): agents (via AgentExecutor) + emit `AgentExecutorResponse`. The adapter converts that to a `list[ChatMessage]` + using `full_conversation` so original prompts aren't lost when chaining. +- Explicit completion ("complete"): emits a `WorkflowCompletedEvent` with the final + conversation list, giving a consistent terminal payload shape for both agents and + custom executors. + +These adapters are first-class executors by design so they are type-checked at edges, +observable (ExecutorInvoke/Completed events), and easily testable/reusable. Their IDs are +deterministic and self-describing (for example, "to-conversation:writer") to reduce event-log +confusion and to mirror how the concurrent builder uses explicit dispatcher/aggregator nodes. +""" # noqa: E501 + +import logging +from collections.abc import Sequence +from typing import Any + +from agent_framework import AgentProtocol, ChatMessage, Role + +from ._events import WorkflowCompletedEvent +from ._executor import ( + AgentExecutor, + AgentExecutorResponse, + Executor, + handler, +) +from ._workflow import Workflow, WorkflowBuilder +from ._workflow_context import WorkflowContext + +logger = logging.getLogger(__name__) + + +class _InputToConversation(Executor): + """Normalizes initial input into a list[ChatMessage] conversation.""" + + @handler + async def from_str(self, prompt: str, ctx: WorkflowContext[list[ChatMessage]]) -> None: + await ctx.send_message([ChatMessage(Role.USER, text=prompt)]) + + @handler + async def from_message(self, message: ChatMessage, ctx: WorkflowContext[list[ChatMessage]]) -> None: # type: ignore[name-defined] + await ctx.send_message([message]) + + @handler + async def from_messages(self, messages: list[ChatMessage], ctx: WorkflowContext[list[ChatMessage]]) -> None: # type: ignore[name-defined] + # Make a copy to avoid mutation downstream + await ctx.send_message(list(messages)) + + +class _ResponseToConversation(Executor): + """Converts AgentExecutorResponse to list[ChatMessage] conversation for chaining.""" + + @handler + async def convert(self, response: AgentExecutorResponse, ctx: WorkflowContext[list[ChatMessage]]) -> None: + # Always use full_conversation; AgentExecutor guarantees it is populated. + if response.full_conversation is None: # Defensive: indicates a contract violation + raise RuntimeError("AgentExecutorResponse.full_conversation missing. AgentExecutor must populate it.") + await ctx.send_message(list(response.full_conversation)) + + +class _CompleteWithConversation(Executor): + """Terminates the workflow by emitting the final conversation context.""" + + @handler + async def complete(self, conversation: list[ChatMessage], ctx: WorkflowContext[Any]) -> None: + await ctx.add_event(WorkflowCompletedEvent(data=list(conversation))) + + +class SequentialBuilder: + r"""High-level builder for sequential agent/executor workflows with shared context. + + - `participants([...])` accepts a list of AgentProtocol (recommended) or Executor + - The workflow wires participants in order, passing a list[ChatMessage] down the chain + - Agents append their assistant messages to the conversation + - Custom executors can transform/summarize and return a list[ChatMessage] + - The final output is the conversation produced by the last participant + + Usage: + ```python + from agent_framework.workflow import SequentialBuilder + + workflow = SequentialBuilder().participants([agent1, agent2, summarizer_exec]).build() + ``` + """ + + def __init__(self) -> None: + self._participants: list[AgentProtocol | Executor] = [] + + def participants(self, participants: Sequence[AgentProtocol | Executor]) -> "SequentialBuilder": + """Define the ordered participants for this sequential workflow. + + Accepts AgentProtocol instances (auto-wrapped as AgentExecutor) or Executor instances. + Raises if empty or duplicates are provided for clarity. + """ + if not participants: + raise ValueError("participants cannot be empty") + + # Defensive duplicate detection + seen_agent_ids: set[int] = set() + seen_executor_ids: set[str] = set() + for p in participants: + if isinstance(p, Executor): + if p.id in seen_executor_ids: + raise ValueError(f"Duplicate executor participant detected: id '{p.id}'") + seen_executor_ids.add(p.id) + else: + # Treat non-Executor as agent-like (AgentProtocol). Structural checks can be brittle at runtime. + pid = id(p) + if pid in seen_agent_ids: + raise ValueError("Duplicate agent participant detected (same agent instance provided twice)") + seen_agent_ids.add(pid) + + self._participants = list(participants) + return self + + def build(self) -> Workflow: + """Build and validate the sequential workflow. + + Wiring pattern: + - _InputToConversation normalizes the initial input into list[ChatMessage] + - For each participant in order: + - If Agent (or AgentExecutor): pass conversation to the agent, then convert response + to conversation via _ResponseToConversation + - Else (custom Executor): pass conversation directly to the executor + - _CompleteWithConversation emits WorkflowCompletedEvent with the final conversation + """ + if not self._participants: + raise ValueError("No participants provided. Call .participants([...]) first.") + + # Internal nodes + input_conv = _InputToConversation(id="input-conversation") + complete = _CompleteWithConversation(id="complete") + + builder = WorkflowBuilder() + builder.set_start_executor(input_conv) + + # Start of the chain is the input normalizer + prior: Executor | AgentProtocol = input_conv + + for p in self._participants: + # Agent-like branch: either explicitly an AgentExecutor or any non-AgentExecutor + if not (isinstance(p, Executor) and not isinstance(p, AgentExecutor)): + # input conversation -> (agent) -> response -> conversation + builder.add_edge(prior, p) + # Give the adapter a deterministic, self-describing id + label: str + label = p.id if isinstance(p, Executor) else getattr(p, "name", None) or p.__class__.__name__ + resp_to_conv = _ResponseToConversation(id=f"to-conversation:{label}") + builder.add_edge(p, resp_to_conv) + prior = resp_to_conv + elif isinstance(p, Executor): + # Custom executor operates on list[ChatMessage] + builder.add_edge(prior, p) + prior = p + else: # pragma: no cover - defensive + raise TypeError(f"Unsupported participant type: {type(p).__name__}") + + # Terminate with the final conversation + builder.add_edge(prior, complete) + + return builder.build() diff --git a/python/packages/workflow/agent_framework_workflow/_workflow.py b/python/packages/workflow/agent_framework_workflow/_workflow.py index 9232149a6a..25aad51e14 100644 --- a/python/packages/workflow/agent_framework_workflow/_workflow.py +++ b/python/packages/workflow/agent_framework_workflow/_workflow.py @@ -25,7 +25,16 @@ from ._edge import ( SwitchCaseEdgeGroupCase, SwitchCaseEdgeGroupDefault, ) -from ._events import RequestInfoEvent, WorkflowCompletedEvent, WorkflowEvent +from ._events import ( + RequestInfoEvent, + WorkflowCompletedEvent, + WorkflowErrorDetails, + WorkflowEvent, + WorkflowFailedEvent, + WorkflowRunState, + WorkflowStartedEvent, + WorkflowStatusEvent, +) from ._executor import AgentExecutor, Executor, RequestInfoExecutor from ._runner import Runner from ._runner_context import CheckpointState, InProcRunnerContext, RunnerContext @@ -43,7 +52,16 @@ logger = logging.getLogger(__name__) class WorkflowRunResult(list[WorkflowEvent]): - """A list of events generated during the workflow execution in non-streaming mode.""" + """A list of events generated during the workflow execution in non-streaming mode. + + Preserves the historical contract that the list contains data-plane events + only (executor invoke/complete, completed, requests), while exposing the + control-plane status timeline via accessors. + """ + + def __init__(self, events: list[WorkflowEvent], status_events: list[WorkflowStatusEvent] | None = None) -> None: + super().__init__(events) + self._status_events: list[WorkflowStatusEvent] = status_events or [] def get_completed_event(self) -> WorkflowCompletedEvent | None: """Get the completed event from the workflow run result. @@ -69,6 +87,23 @@ class WorkflowRunResult(list[WorkflowEvent]): """ return [event for event in self if isinstance(event, RequestInfoEvent)] + def get_final_state(self) -> WorkflowRunState: + """Return the final run state based on explicit status events. + + Returns the last WorkflowStatusEvent.state observed. Raises if none were emitted. + """ + if self._status_events: + return self._status_events[-1].state # type: ignore[return-value] + raise RuntimeError( + "Final state is unknown because no WorkflowStatusEvent was emitted. " + "Ensure your workflow entry points are used (which emit status events) " + "or handle the absence of status explicitly." + ) + + def status_timeline(self) -> list[WorkflowStatusEvent]: + """Return the list of status events emitted during the run (control-plane).""" + return list(self._status_events) + # region Workflow @@ -202,9 +237,15 @@ class Workflow(AFBaseModel): # Create workflow span that encompasses the entire execution with workflow_tracer.create_workflow_run_span(self): + saw_completed = False + saw_request = False + emitted_in_progress_pending = False try: - # Add workflow started event + # Add workflow started event (telemetry + surface state to consumers) workflow_tracer.add_workflow_event("workflow.started") + # Emit explicit start/status events to the stream + yield WorkflowStartedEvent() + yield WorkflowStatusEvent(WorkflowRunState.IN_PROGRESS) # Reset context for a new run if supported if reset_context: @@ -216,11 +257,31 @@ class Workflow(AFBaseModel): # All executor executions happen within workflow span async for event in self._runner.run_until_convergence(): + # Track terminal indicators while forwarding events + if isinstance(event, WorkflowCompletedEvent): + saw_completed = True + elif isinstance(event, RequestInfoEvent): + saw_request = True yield event - # Success + if isinstance(event, RequestInfoEvent) and not emitted_in_progress_pending and not saw_completed: + emitted_in_progress_pending = True + yield WorkflowStatusEvent(WorkflowRunState.IN_PROGRESS_PENDING_REQUESTS) + + # Success path: emit a final status based on observed terminal signals + if saw_completed: + yield WorkflowStatusEvent(WorkflowRunState.COMPLETED) + elif saw_request: + yield WorkflowStatusEvent(WorkflowRunState.IDLE_WITH_PENDING_REQUESTS) + else: + yield WorkflowStatusEvent(WorkflowRunState.IDLE) + workflow_tracer.add_workflow_event("workflow.completed") except Exception as e: + # Surface structured failure details before propagating exception + details = WorkflowErrorDetails.from_exception(e) + yield WorkflowFailedEvent(details) + yield WorkflowStatusEvent(WorkflowRunState.FAILED) workflow_tracer.add_workflow_error_event(e) raise @@ -358,11 +419,12 @@ class Workflow(AFBaseModel): ): yield event - async def run(self, message: Any) -> WorkflowRunResult: + async def run(self, message: Any, *, include_status_events: bool = False) -> WorkflowRunResult: """Run the workflow with the given message. Args: message: The message to be processed by the workflow. + include_status_events: Whether to include WorkflowStatusEvent instances in the result list. Returns: A WorkflowRunResult instance containing a list of events generated during the workflow execution. @@ -377,6 +439,7 @@ class Workflow(AFBaseModel): coalesced: list[WorkflowEvent] = [] # type: ignore[name-defined] pending_updates: list[AgentRunResponseUpdate] = [] pending_executor: str | None = None + status_events: list[WorkflowStatusEvent] = [] def _flush_pending() -> None: nonlocal pending_updates, pending_executor @@ -403,12 +466,22 @@ class Workflow(AFBaseModel): continue # Flush before adding any non-update event _flush_pending() + # Omit WorkflowStartedEvent from non-streaming (telemetry-only) + if isinstance(ev, WorkflowStartedEvent): + continue + # Track status; include inline only if explicitly requested + if isinstance(ev, WorkflowStatusEvent): + status_events.append(ev) + if include_status_events: + coalesced.append(ev) + continue coalesced.append(ev) # Flush any trailing updates _flush_pending() - return WorkflowRunResult(coalesced) + # coalesced already excludes start events; includes status events only if opted in + return WorkflowRunResult(coalesced, status_events) async def run_from_checkpoint( self, @@ -435,7 +508,9 @@ class Workflow(AFBaseModel): events = [ event async for event in self.run_stream_from_checkpoint(checkpoint_id, checkpoint_storage, responses) ] - return WorkflowRunResult(events) + status_events = [e for e in events if isinstance(e, WorkflowStatusEvent)] + filtered_events = [e for e in events if not isinstance(e, (WorkflowStatusEvent, WorkflowStartedEvent))] + return WorkflowRunResult(filtered_events, status_events) async def send_responses(self, responses: dict[str, Any]) -> WorkflowRunResult: """Send responses back to the workflow. @@ -447,7 +522,9 @@ class Workflow(AFBaseModel): A WorkflowRunResult instance containing a list of events generated during the workflow execution. """ events = [event async for event in self.send_responses_streaming(responses)] - return WorkflowRunResult(events) + status_events = [e for e in events if isinstance(e, WorkflowStatusEvent)] + filtered_events = [e for e in events if not isinstance(e, (WorkflowStatusEvent, WorkflowStartedEvent))] + return WorkflowRunResult(filtered_events, status_events) def _get_executor_by_id(self, executor_id: str) -> Executor: """Get an executor by its ID. diff --git a/python/packages/workflow/pyproject.toml b/python/packages/workflow/pyproject.toml index 5ffab1a8cb..4dfece1cb7 100644 --- a/python/packages/workflow/pyproject.toml +++ b/python/packages/workflow/pyproject.toml @@ -74,7 +74,6 @@ show_error_codes = true warn_unused_ignores = false disallow_incomplete_defs = true disallow_untyped_decorators = true -disallow_any_unimported = true [tool.bandit] targets = ["agent_framework_workflow"] diff --git a/python/packages/workflow/tests/test_full_conversation.py b/python/packages/workflow/tests/test_full_conversation.py new file mode 100644 index 0000000000..f1bad2a477 --- /dev/null +++ b/python/packages/workflow/tests/test_full_conversation.py @@ -0,0 +1,160 @@ +# Copyright (c) Microsoft. All rights reserved. + +from collections.abc import AsyncIterable +from typing import Any + +from agent_framework import ( + AgentRunResponse, + AgentRunResponseUpdate, + AgentThread, + BaseAgent, + ChatMessage, + Role, + TextContent, +) +from pydantic import PrivateAttr + +from agent_framework_workflow import ( + AgentExecutor, + SequentialBuilder, + WorkflowBuilder, + WorkflowCompletedEvent, + WorkflowContext, + handler, +) +from agent_framework_workflow._executor import AgentExecutorResponse, Executor + + +class _SimpleAgent(BaseAgent): + """Agent that returns a single assistant message (non-streaming path).""" + + def __init__(self, *, reply_text: str, **kwargs: Any) -> None: + super().__init__(**kwargs) + self._reply_text = reply_text + + async def run( # type: ignore[override] + self, + messages: str | ChatMessage | list[str] | list[ChatMessage] | None = None, + *, + thread: AgentThread | None = None, + **kwargs: Any, + ) -> AgentRunResponse: + return AgentRunResponse(messages=[ChatMessage(role=Role.ASSISTANT, text=self._reply_text)]) + + async def run_stream( # type: ignore[override] + self, + messages: str | ChatMessage | list[str] | list[ChatMessage] | None = None, + *, + thread: AgentThread | None = None, + **kwargs: Any, + ) -> AsyncIterable[AgentRunResponseUpdate]: + # This agent does not support streaming; yield a single complete response + yield AgentRunResponseUpdate(contents=[TextContent(text=self._reply_text)]) + + +class _CaptureFullConversation(Executor): + """Captures AgentExecutorResponse.full_conversation and completes the workflow.""" + + @handler + async def capture(self, response: AgentExecutorResponse, ctx: WorkflowContext[None]) -> None: + full = response.full_conversation + # The AgentExecutor contract guarantees full_conversation is populated. + assert full is not None + await ctx.add_event( + WorkflowCompletedEvent( + data={ + "length": len(full), + "roles": [m.role for m in full], + "texts": [m.text for m in full], + } + ) + ) + + +async def test_agent_executor_populates_full_conversation_non_streaming() -> None: + # Arrange: non-streaming AgentExecutor for deterministic response composition + agent = _SimpleAgent(id="agent1", name="A", reply_text="agent-reply") + agent_exec = AgentExecutor(agent, streaming=False, id="agent1-exec") + capturer = _CaptureFullConversation(id="capture") + + wf = WorkflowBuilder().set_start_executor(agent_exec).add_edge(agent_exec, capturer).build() + + # Act: run with a simple user prompt + completed: WorkflowCompletedEvent | None = None + async for ev in wf.run_stream("hello world"): + if isinstance(ev, WorkflowCompletedEvent): + completed = ev + break + + # Assert: full_conversation contains [user("hello world"), assistant("agent-reply")] + assert completed is not None + payload = completed.data # type: ignore[assignment] + assert isinstance(payload, dict) + assert payload["length"] == 2 + assert payload["roles"][0] == Role.USER and "hello world" in (payload["texts"][0] or "") + assert payload["roles"][1] == Role.ASSISTANT and "agent-reply" in (payload["texts"][1] or "") + + +class _CaptureAgent(BaseAgent): + """Streaming-capable agent that records the messages it received.""" + + _last_messages: list[ChatMessage] = PrivateAttr(default_factory=list) # type: ignore + + def __init__(self, *, reply_text: str, **kwargs: Any) -> None: + super().__init__(**kwargs) + self._reply_text = reply_text + + async def run( # type: ignore[override] + self, + messages: str | ChatMessage | list[str] | list[ChatMessage] | None = None, + *, + thread: AgentThread | None = None, + **kwargs: Any, + ) -> AgentRunResponse: + # Normalize and record messages for verification when running non-streaming + norm: list[ChatMessage] = [] + if messages: + for m in messages: # type: ignore[iteration-over-optional] + if isinstance(m, ChatMessage): + norm.append(m) + elif isinstance(m, str): + norm.append(ChatMessage(role=Role.USER, text=m)) + self._last_messages = norm + return AgentRunResponse(messages=[ChatMessage(role=Role.ASSISTANT, text=self._reply_text)]) + + async def run_stream( # type: ignore[override] + self, + messages: str | ChatMessage | list[str] | list[ChatMessage] | None = None, + *, + thread: AgentThread | None = None, + **kwargs: Any, + ) -> AsyncIterable[AgentRunResponseUpdate]: + # Normalize and record messages for verification when running streaming + norm: list[ChatMessage] = [] + if messages: + for m in messages: # type: ignore[iteration-over-optional] + if isinstance(m, ChatMessage): + norm.append(m) + elif isinstance(m, str): + norm.append(ChatMessage(role=Role.USER, text=m)) + self._last_messages = norm + yield AgentRunResponseUpdate(contents=[TextContent(text=self._reply_text)]) + + +async def test_sequential_adapter_uses_full_conversation() -> None: + # Arrange: two streaming agents; the second records what it receives + a1 = _CaptureAgent(id="agent1", name="A1", reply_text="A1 reply") + a2 = _CaptureAgent(id="agent2", name="A2", reply_text="A2 reply") + + wf = SequentialBuilder().participants([a1, a2]).build() + + # Act + async for ev in wf.run_stream("hello seq"): + if isinstance(ev, WorkflowCompletedEvent): + break + + # Assert: second agent should have seen the user prompt and A1's assistant reply + seen = a2._last_messages # pyright: ignore[reportPrivateUsage] + assert len(seen) == 2 + assert seen[0].role == Role.USER and "hello seq" in (seen[0].text or "") + assert seen[1].role == Role.ASSISTANT and "A1 reply" in (seen[1].text or "") diff --git a/python/packages/workflow/tests/test_sequential.py b/python/packages/workflow/tests/test_sequential.py new file mode 100644 index 0000000000..25ccabab75 --- /dev/null +++ b/python/packages/workflow/tests/test_sequential.py @@ -0,0 +1,106 @@ +# Copyright (c) Microsoft. All rights reserved. + +from collections.abc import AsyncIterable +from typing import Any + +import pytest +from agent_framework import ( + AgentRunResponse, + AgentRunResponseUpdate, + AgentThread, + BaseAgent, + ChatMessage, + Role, + TextContent, +) + +from agent_framework_workflow import ( + Executor, + SequentialBuilder, + WorkflowCompletedEvent, + WorkflowContext, + handler, +) + + +class _EchoAgent(BaseAgent): + """Simple agent that appends a single assistant message with its name.""" + + async def run( # type: ignore[override] + self, + messages: str | ChatMessage | list[str] | list[ChatMessage] | None = None, + *, + thread: AgentThread | None = None, + **kwargs: Any, + ) -> AgentRunResponse: + return AgentRunResponse(messages=[ChatMessage(role=Role.ASSISTANT, text=f"{self.display_name} reply")]) + + async def run_stream( # type: ignore[override] + self, + messages: str | ChatMessage | list[str] | list[ChatMessage] | None = None, + *, + thread: AgentThread | None = None, + **kwargs: Any, + ) -> AsyncIterable[AgentRunResponseUpdate]: + # Minimal async generator with one assistant update + yield AgentRunResponseUpdate(contents=[TextContent(text=f"{self.display_name} reply")]) + + +class _SummarizerExec(Executor): + """Custom executor that summarizes by appending a short assistant message.""" + + @handler + async def summarize(self, conversation: list[ChatMessage], ctx: WorkflowContext[list[ChatMessage]]) -> None: + user_texts = [m.text for m in conversation if m.role == Role.USER] + agents = [m.author_name or m.role for m in conversation if m.role == Role.ASSISTANT] + summary = ChatMessage(role=Role.ASSISTANT, text=f"Summary of users:{len(user_texts)} agents:{len(agents)}") + await ctx.send_message(list(conversation) + [summary]) + + +def test_sequential_builder_rejects_empty_participants() -> None: + with pytest.raises(ValueError): + SequentialBuilder().participants([]) + + +async def test_sequential_agents_append_to_context() -> None: + a1 = _EchoAgent(id="agent1", name="A1") + a2 = _EchoAgent(id="agent2", name="A2") + + wf = SequentialBuilder().participants([a1, a2]).build() + + completed: WorkflowCompletedEvent | None = None + async for ev in wf.run_stream("hello sequential"): + if isinstance(ev, WorkflowCompletedEvent): + completed = ev + break + + assert completed is not None + assert isinstance(completed.data, list) + msgs: list[ChatMessage] = completed.data # type: ignore[assignment] + assert len(msgs) == 3 + assert msgs[0].role == Role.USER and "hello sequential" in msgs[0].text + assert msgs[1].role == Role.ASSISTANT and (msgs[1].author_name == "A1" or True) + assert msgs[2].role == Role.ASSISTANT and (msgs[2].author_name == "A2" or True) + assert "A1 reply" in msgs[1].text + assert "A2 reply" in msgs[2].text + + +async def test_sequential_with_custom_executor_summary() -> None: + a1 = _EchoAgent(id="agent1", name="A1") + summarizer = _SummarizerExec(id="summarizer") + + wf = SequentialBuilder().participants([a1, summarizer]).build() + + completed: WorkflowCompletedEvent | None = None + async for ev in wf.run_stream("topic X"): + if isinstance(ev, WorkflowCompletedEvent): + completed = ev + break + + assert completed is not None + msgs: list[ChatMessage] = completed.data # type: ignore[assignment] + # Expect: [user, A1 reply, summary] + assert len(msgs) == 3 + assert msgs[0].role == Role.USER + assert msgs[1].role == Role.ASSISTANT and "A1 reply" in msgs[1].text + assert msgs[2].role == Role.ASSISTANT and msgs[2].text.startswith("Summary of users:") diff --git a/python/packages/workflow/tests/test_workflow_states.py b/python/packages/workflow/tests/test_workflow_states.py new file mode 100644 index 0000000000..0070afda46 --- /dev/null +++ b/python/packages/workflow/tests/test_workflow_states.py @@ -0,0 +1,137 @@ +# Copyright (c) Microsoft. All rights reserved. + +import pytest +from agent_framework.workflow import ( + Executor, + ExecutorFailedEvent, + RequestInfoEvent, + RequestInfoExecutor, + RequestInfoMessage, + Workflow, + WorkflowBuilder, + WorkflowCompletedEvent, + WorkflowContext, + WorkflowFailedEvent, + WorkflowRunResult, + WorkflowRunState, + WorkflowStatusEvent, + handler, +) + +from agent_framework_workflow import InProcRunnerContext +from agent_framework_workflow._shared_state import SharedState +from agent_framework_workflow._workflow_context import WorkflowContext as WFContext + + +class FailingExecutor(Executor): + """Executor that raises at runtime to test failure signaling.""" + + @handler + async def fail(self, msg: int, ctx: WorkflowContext[None]) -> None: # pragma: no cover - invoked via workflow + raise RuntimeError("boom") + + +async def test_executor_failed_and_workflow_failed_events_streaming(): + failing = FailingExecutor(id="f") + wf: Workflow = WorkflowBuilder().set_start_executor(failing).build() + + events: list[object] = [] + with pytest.raises(RuntimeError, match="boom"): + async for ev in wf.run_stream(0): + events.append(ev) + + # Workflow-level failure and FAILED status should be surfaced + assert any(isinstance(e, WorkflowFailedEvent) for e in events) + status = [e for e in events if isinstance(e, WorkflowStatusEvent)] + assert status and status[-1].state == WorkflowRunState.FAILED + + +async def test_executor_failed_event_emitted_on_direct_execute(): + failing = FailingExecutor(id="f") + ctx = InProcRunnerContext() + wf_ctx: WFContext[None] = WFContext( + executor_id=failing.id, + source_executor_ids=["START"], + shared_state=SharedState(), + runner_context=ctx, + ) + with pytest.raises(RuntimeError, match="boom"): + await failing.execute(0, wf_ctx) + drained = await ctx.drain_events() + assert any(isinstance(e, ExecutorFailedEvent) for e in drained) + + +class Requester(Executor): + """Executor that always requests external info to test idle-with-requests state.""" + + @handler + async def ask(self, _: str, ctx: WorkflowContext[RequestInfoMessage]) -> None: # pragma: no cover + await ctx.send_message(RequestInfoMessage()) + + +async def test_idle_with_pending_requests_status_streaming(): + req = Requester(id="req") + rie = RequestInfoExecutor(id="rie") + wf = WorkflowBuilder().set_start_executor(req).add_edge(req, rie).build() + + events = [ev async for ev in wf.run_stream("start")] # Consume stream fully + + # Ensure a request was emitted + assert any(isinstance(e, RequestInfoEvent) for e in events) + status_events = [e for e in events if isinstance(e, WorkflowStatusEvent)] + assert len(status_events) >= 3 + assert status_events[-2].state == WorkflowRunState.IN_PROGRESS_PENDING_REQUESTS + assert status_events[-1].state == WorkflowRunState.IDLE_WITH_PENDING_REQUESTS + + +class Completer(Executor): + """Executor that completes immediately with provided data for testing.""" + + @handler + async def run(self, msg: str, ctx: WorkflowContext[str]) -> None: # pragma: no cover + await ctx.add_event(WorkflowCompletedEvent(msg)) + + +async def test_completed_status_streaming(): + c = Completer(id="c") + wf = WorkflowBuilder().set_start_executor(c).build() + events = [ev async for ev in wf.run_stream("ok")] # no raise + # Last status should be COMPLETED + status = [e for e in events if isinstance(e, WorkflowStatusEvent)] + assert status and status[-1].state == WorkflowRunState.COMPLETED + + +async def test_non_streaming_final_state_helpers(): + # Completed case + c = Completer(id="c") + wf1 = WorkflowBuilder().set_start_executor(c).build() + result1: WorkflowRunResult = await wf1.run("done") + assert result1.get_final_state() == WorkflowRunState.COMPLETED + + # Idle-with-pending-request case + req = Requester(id="req") + rie = RequestInfoExecutor(id="rie") + wf2 = WorkflowBuilder().set_start_executor(req).add_edge(req, rie).build() + result2: WorkflowRunResult = await wf2.run("start") + assert result2.get_final_state() == WorkflowRunState.IDLE_WITH_PENDING_REQUESTS + + +async def test_run_includes_status_events_completed(): + c = Completer(id="c2") + wf = WorkflowBuilder().set_start_executor(c).build() + result: WorkflowRunResult = await wf.run("ok") + timeline = result.status_timeline() + assert timeline, "Expected status timeline in non-streaming run() results" + assert timeline[-1].state == WorkflowRunState.COMPLETED + + +async def test_run_includes_status_events_idle_with_requests(): + req = Requester(id="req2") + rie = RequestInfoExecutor(id="rie2") + wf = WorkflowBuilder().set_start_executor(req).add_edge(req, rie).build() + result: WorkflowRunResult = await wf.run("start") + timeline = result.status_timeline() + assert timeline, "Expected status timeline in non-streaming run() results" + assert len(timeline) >= 3 + assert timeline[-2].state == WorkflowRunState.IN_PROGRESS_PENDING_REQUESTS + assert timeline[-1].state == WorkflowRunState.IDLE_WITH_PENDING_REQUESTS diff --git a/python/pyproject.toml b/python/pyproject.toml index a891bbe713..1b2e1a456f 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -6,6 +6,7 @@ requires-python = ">=3.10" dependencies = [ "agent-framework", "agent-framework-azure", + "agent-framework-copilotstudio", "agent-framework-foundry", "agent-framework-mem0", "agent-framework-workflow", @@ -63,6 +64,7 @@ exclude = [ "packages/agent_framework_project.egg-info" ] [tool.uv.sources] agent-framework = { workspace = true } agent-framework-azure = { workspace = true } +agent-framework-copilotstudio = { workspace = true } agent-framework-foundry = { workspace = true } agent-framework-mem0 = { workspace = true } agent-framework-runtime = { workspace = true } @@ -164,7 +166,6 @@ show_error_codes = true warn_unused_ignores = false disallow_incomplete_defs = true disallow_untyped_decorators = true -disallow_any_unimported = true [tool.bandit] targets = ["agent_framework"] @@ -193,7 +194,7 @@ build = "python run_tasks_in_packages_if_exists.py build" # combined checks check = ["fmt", "lint", "pyright", "mypy", "test", "markdown-code-lint", "samples-code-check"] pre-commit-check = ["fmt", "lint", "pyright", "markdown-code-lint", "samples-code-check"] -all-tests = "pytest --import-mode=importlib --cov=agent_framework --cov=agent_framework_azure --cov=agent_framework_foundry --cov=agent_framework_mem0 --cov=agent_framework_workflow --cov-report=term-missing:skip-covered packages/azure/tests packages/foundry/tests packages/main/tests packages/mem0/tests packages/workflow/tests" +all-tests = "pytest --import-mode=importlib --cov=agent_framework --cov=agent_framework_azure --cov=agent_framework_copilotstudio --cov=agent_framework_foundry --cov=agent_framework_mem0 --cov=agent_framework_workflow --cov-report=term-missing:skip-covered packages/azure/tests packages/copilotstudio/tests packages/foundry/tests packages/main/tests packages/mem0/tests packages/workflow/tests" [tool.poe.tasks.venv] cmd = "uv venv --clear --python $python" diff --git a/python/samples/getting_started/agents/README.md b/python/samples/getting_started/agents/README.md index 14233b7812..80aa37a533 100644 --- a/python/samples/getting_started/agents/README.md +++ b/python/samples/getting_started/agents/README.md @@ -10,6 +10,12 @@ This folder contains examples demonstrating how to create and use agents with di |--------|-------------| | **[`foundry/`](foundry/)** | Create agents using Azure AI Foundry | +### Microsoft Copilot Studio Examples + +| Folder | Description | +|--------|-------------| +| **[`copilotstudio/`](copilotstudio/)** | Create agents using Microsoft Copilot Studio | + ### Azure OpenAI Examples | Folder | Description | diff --git a/python/samples/getting_started/agents/copilotstudio/README.md b/python/samples/getting_started/agents/copilotstudio/README.md new file mode 100644 index 0000000000..e2503db51a --- /dev/null +++ b/python/samples/getting_started/agents/copilotstudio/README.md @@ -0,0 +1,96 @@ +# Copilot Studio Agent Examples + +This folder contains examples demonstrating how to create and use agents with Microsoft Copilot Studio using the Agent Framework. + +## Prerequisites + +Before running these examples, you need: + +1. **Copilot Studio Environment**: Access to a Microsoft Copilot Studio environment with a published copilot +2. **App Registration**: An Azure AD App Registration with appropriate permissions +3. **Environment Variables**: Set the following environment variables: + - `COPILOTSTUDIOAGENT__ENVIRONMENTID` - Your Copilot Studio environment ID + - `COPILOTSTUDIOAGENT__SCHEMANAME` - Your copilot's agent identifier/schema name + - `COPILOTSTUDIOAGENT__AGENTAPPID` - Your App Registration client ID + - `COPILOTSTUDIOAGENT__TENANTID` - Your Azure AD tenant ID + +## Examples + +| Example | Description | +|---------|-------------| +| **[`copilotstudio_basic.py`](copilotstudio_basic.py)** | Basic non-streaming and streaming execution with simple questions | +| **[`copilotstudio_with_explicit_settings.py`](copilotstudio_with_explicit_settings.py)** | Example with explicit settings and manual token acquisition | + +## Authentication + +The examples use MSAL (Microsoft Authentication Library) for authentication. The first time you run an example, you may need to complete an interactive authentication flow in your browser. + +### App Registration Setup + +Your Azure AD App Registration should have: + +1. **API Permissions**: + - Power Platform API permissions (https://api.powerplatform.com/.default) + - Appropriate delegated permissions for your organization + +2. **Redirect URIs**: + - For public client flows: `http://localhost` + - Configure as appropriate for your authentication method + +3. **Authentication**: + - Enable "Allow public client flows" if using interactive authentication + +## Usage Patterns + +### Basic Usage with Environment Variables + +```python +from agent_framework.copilotstudio import CopilotStudioAgent + +# Uses environment variables for configuration +agent = CopilotStudioAgent() +result = await agent.run("What is the capital of France?") +``` + +### Explicit Configuration + +```python +from agent_framework.copilotstudio import CopilotStudioAgent, acquire_token +from microsoft_agents.copilotstudio.client import ConnectionSettings, CopilotClient, PowerPlatformCloud, AgentType + +# Acquire token manually +token = acquire_token( + client_id="your-client-id", + tenant_id="your-tenant-id" +) + +# Create settings and client +settings = ConnectionSettings( + environment_id="your-environment-id", + agent_identifier="your-agent-schema-name", + cloud=PowerPlatformCloud.PROD, + copilot_agent_type=AgentType.PUBLISHED +) + +client = CopilotClient(settings=settings, token=token) +agent = CopilotStudioAgent(client=client) +``` + +## Troubleshooting + +### Common Issues + +1. **Authentication Errors**: + - Verify your App Registration has correct permissions + - Ensure environment variables are set correctly + - Check that your tenant ID and client ID are valid + +2. **Environment/Agent Not Found**: + - Verify your environment ID is correct + - Ensure your copilot is published and the schema name is correct + - Check that you have access to the specified environment + +3. **Token Acquisition Failures**: + - Interactive authentication may require browser access + - Corporate firewalls may block authentication flows + - Try running with appropriate proxy settings if needed diff --git a/python/samples/getting_started/agents/copilotstudio/copilotstudio_basic.py b/python/samples/getting_started/agents/copilotstudio/copilotstudio_basic.py new file mode 100644 index 0000000000..169f9957cd --- /dev/null +++ b/python/samples/getting_started/agents/copilotstudio/copilotstudio_basic.py @@ -0,0 +1,47 @@ +# Copyright (c) Microsoft. All rights reserved. + +import asyncio + +from agent_framework.copilotstudio import CopilotStudioAgent + +# Environment variables needed: +# COPILOTSTUDIOAGENT__ENVIRONMENTID - Environment ID where your copilot is deployed +# COPILOTSTUDIOAGENT__SCHEMANAME - Agent identifier/schema name of your copilot +# COPILOTSTUDIOAGENT__AGENTAPPID - Client ID for authentication +# COPILOTSTUDIOAGENT__TENANTID - Tenant ID for authentication + + +async def non_streaming_example() -> None: + """Example of non-streaming response (get the complete result at once).""" + print("=== Non-streaming Response Example ===") + + agent = CopilotStudioAgent() + + query = "What is the capital of France?" + print(f"User: {query}") + result = await agent.run(query) + print(f"Agent: {result}\n") + + +async def streaming_example() -> None: + """Example of streaming response (get results as they are generated).""" + print("=== Streaming Response Example ===") + + agent = CopilotStudioAgent() + + query = "What is the capital of Spain?" + print(f"User: {query}") + print("Agent: ", end="", flush=True) + async for chunk in agent.run_stream(query): + if chunk.text: + print(chunk.text, end="", flush=True) + print("\n") + + +async def main() -> None: + await non_streaming_example() + await streaming_example() + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/python/samples/getting_started/agents/copilotstudio/copilotstudio_with_explicit_settings.py b/python/samples/getting_started/agents/copilotstudio/copilotstudio_with_explicit_settings.py new file mode 100644 index 0000000000..46566dfab7 --- /dev/null +++ b/python/samples/getting_started/agents/copilotstudio/copilotstudio_with_explicit_settings.py @@ -0,0 +1,87 @@ +# Copyright (c) Microsoft. All rights reserved. + +import asyncio +import os + +from agent_framework.copilotstudio import CopilotStudioAgent, acquire_token +from microsoft_agents.copilotstudio.client import AgentType, ConnectionSettings, CopilotClient, PowerPlatformCloud + +# Environment variables needed: +# COPILOTSTUDIOAGENT__ENVIRONMENTID - Environment ID where your copilot is deployed +# COPILOTSTUDIOAGENT__SCHEMANAME - Agent identifier/schema name of your copilot +# COPILOTSTUDIOAGENT__AGENTAPPID - Client ID for authentication +# COPILOTSTUDIOAGENT__TENANTID - Tenant ID for authentication + + +async def example_with_connection_settings() -> None: + """Example using explicit ConnectionSettings and CopilotClient.""" + print("=== Copilot Studio Agent with Connection Settings ===") + + # Configuration from environment variables + environment_id = os.environ["COPILOTSTUDIOAGENT__ENVIRONMENTID"] + agent_identifier = os.environ["COPILOTSTUDIOAGENT__SCHEMANAME"] + client_id = os.environ["COPILOTSTUDIOAGENT__AGENTAPPID"] + tenant_id = os.environ["COPILOTSTUDIOAGENT__TENANTID"] + + # Acquire token using the acquire_token function + token = acquire_token( + client_id=client_id, + tenant_id=tenant_id, + ) + + # Create connection settings + settings = ConnectionSettings( + environment_id=environment_id, + agent_identifier=agent_identifier, + cloud=PowerPlatformCloud.PROD, # Or PowerPlatformCloud.GOV, PowerPlatformCloud.HIGH, etc. + copilot_agent_type=AgentType.PUBLISHED, # Or AgentType.PREBUILT + custom_power_platform_cloud=None, # Optional: for custom cloud endpoints + ) + + # Create CopilotClient with explicit settings + client = CopilotClient(settings=settings, token=token) + + # Create agent with explicit client + agent = CopilotStudioAgent(client=client) + + # Run a simple query + query = "What is the capital of Italy?" + print(f"User: {query}") + result = await agent.run(query) + print(f"Agent: {result}") + + +async def example_with_explicit_parameters() -> None: + """Example using CopilotStudioAgent with all parameters explicitly provided.""" + print("\n=== Copilot Studio Agent with All Explicit Parameters ===") + + # Configuration from environment variables + environment_id = os.environ["COPILOTSTUDIOAGENT__ENVIRONMENTID"] + agent_identifier = os.environ["COPILOTSTUDIOAGENT__SCHEMANAME"] + client_id = os.environ["COPILOTSTUDIOAGENT__AGENTAPPID"] + tenant_id = os.environ["COPILOTSTUDIOAGENT__TENANTID"] + + # Create agent with all parameters explicitly + agent = CopilotStudioAgent( + environment_id=environment_id, + agent_identifier=agent_identifier, + client_id=client_id, + tenant_id=tenant_id, + cloud=PowerPlatformCloud.PROD, + agent_type=AgentType.PUBLISHED, + ) + + # Run a simple query + query = "What is the capital of Japan?" + print(f"User: {query}") + result = await agent.run(query) + print(f"Agent: {result}") + + +async def main() -> None: + await example_with_connection_settings() + await example_with_explicit_parameters() + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/python/samples/getting_started/workflow/README.md b/python/samples/getting_started/workflow/README.md index 9fa42bc209..1a52dfe738 100644 --- a/python/samples/getting_started/workflow/README.md +++ b/python/samples/getting_started/workflow/README.md @@ -83,6 +83,8 @@ Once comfortable with these, explore the rest of the samples below. | Concurrent Orchestration (Custom Agent Executors) | [orchestration/concurrent_custom_agent_executors.py](./orchestration/concurrent_custom_agent_executors.py) | Child executors own ChatAgents; concurrent fan-out/fan-in via ConcurrentBuilder | | Magentic Workflow (Multi-Agent) | [orchestration/magentic.py](./orchestration/magentic.py) | Orchestrate multiple agents with Magentic manager and streaming | | Magentic + Human Plan Review | [orchestration/magentic_human_plan_update.py](./orchestration/magentic_human_plan_update.py) | Human reviews/updates the plan before execution | +| Sequential Orchestration (Agents) | [orchestration/sequential_agents.py](./orchestration/sequential_agents.py) | Chain agents sequentially with shared conversation context | +| Sequential Orchestration (Custom Executor) | [orchestration/sequential_custom_executors.py](./orchestration/sequential_custom_executors.py) | Mix agents with a summarizer that appends a compact summary | ### parallelism | Sample | File | Concepts | @@ -110,6 +112,13 @@ Once comfortable with these, explore the rest of the samples below. Notes - Agent-based samples use provider SDKs (Azure/OpenAI, etc.). Ensure credentials are configured, or adapt agents accordingly. +Sequential orchestration uses a few small adapter nodes for plumbing: +- "input-conversation" normalizes input to `list[ChatMessage]` +- "to-conversation:" converts agent responses into the shared conversation +- "complete" publishes the final `WorkflowCompletedEvent` +These may appear in event streams (ExecutorInvoke/Completed). They’re analogous to +concurrent’s dispatcher and aggregator and can be ignored if you only care about agent activity. + ### Environment Variables - **AzureChatClient**: Set Azure OpenAI environment variables as documented [here](https://github.com/microsoft/agent-framework/blob/main/python/samples/getting_started/chat_client/README.md#environment-variables). diff --git a/python/samples/getting_started/workflow/_start-here/step1_executors_and_edges.py b/python/samples/getting_started/workflow/_start-here/step1_executors_and_edges.py index 58b547219b..14c5348575 100644 --- a/python/samples/getting_started/workflow/_start-here/step1_executors_and_edges.py +++ b/python/samples/getting_started/workflow/_start-here/step1_executors_and_edges.py @@ -104,11 +104,14 @@ async def main(): # provides the WorkflowCompletedEvent emitted by the terminal node. events = await workflow.run("hello world") print(events.get_completed_event()) + # Summarize the final run state (e.g., COMPLETED) + print("Final state:", events.get_final_state()) """ Sample Output: WorkflowCompletedEvent(data=DLROW OLLEH) + Final state: WorkflowRunState.COMPLETED """ diff --git a/python/samples/getting_started/workflow/_start-here/step2_agents_in_a_workflow.py b/python/samples/getting_started/workflow/_start-here/step2_agents_in_a_workflow.py index aed9d22056..3d42723a38 100644 --- a/python/samples/getting_started/workflow/_start-here/step2_agents_in_a_workflow.py +++ b/python/samples/getting_started/workflow/_start-here/step2_agents_in_a_workflow.py @@ -59,6 +59,8 @@ async def main(): print(f"{event.executor_id}: {event.data}") print(f"{'=' * 60}\n{events.get_completed_event()}") + # Summarize the final run state (e.g., COMPLETED) + print("Final state:", events.get_final_state()) """ Sample Output: diff --git a/python/samples/getting_started/workflow/_start-here/step3_streaming.py b/python/samples/getting_started/workflow/_start-here/step3_streaming.py index 7f86d3bfb7..0619bd4c8e 100644 --- a/python/samples/getting_started/workflow/_start-here/step3_streaming.py +++ b/python/samples/getting_started/workflow/_start-here/step3_streaming.py @@ -4,7 +4,17 @@ import asyncio from agent_framework import ChatAgent, ChatMessage from agent_framework.azure import AzureChatClient -from agent_framework.workflow import Executor, WorkflowBuilder, WorkflowCompletedEvent, WorkflowContext, handler +from agent_framework.workflow import ( + Executor, + ExecutorFailedEvent, + WorkflowBuilder, + WorkflowCompletedEvent, + WorkflowContext, + WorkflowFailedEvent, + WorkflowRunState, + WorkflowStatusEvent, + handler, +) from azure.identity import AzureCliCredential """ @@ -107,20 +117,41 @@ async def main(): workflow = WorkflowBuilder().set_start_executor(writer).add_edge(writer, reviewer).build() # Run the workflow with the user's initial message and stream events as they occur. - # Events include executor invoke and completion, as well as the terminal WorkflowCompletedEvent. + # In addition to executor events and WorkflowCompletedEvent, this also surfaces run-state and errors. async for event in workflow.run_stream( ChatMessage(role="user", text="Create a slogan for a new electric SUV that is affordable and fun to drive.") ): - print(event) + if isinstance(event, WorkflowStatusEvent): + if event.state == WorkflowRunState.IN_PROGRESS: + print("State: IN_PROGRESS") + elif event.state == WorkflowRunState.COMPLETED: + print("State: COMPLETED") + elif event.state == WorkflowRunState.IN_PROGRESS_PENDING_REQUESTS: + print("State: IN_PROGRESS_PENDING_REQUESTS (requests in flight)") + elif event.state == WorkflowRunState.IDLE: + print("State: IDLE (no active work)") + elif event.state == WorkflowRunState.IDLE_WITH_PENDING_REQUESTS: + print("State: IDLE_WITH_PENDING_REQUESTS (prompt user or UI now)") + else: + print(f"State: {event.state}") + elif isinstance(event, ExecutorFailedEvent): + print(f"Executor failed: {event.executor_id} {event.details.error_type}: {event.details.message}") + elif isinstance(event, WorkflowFailedEvent): + details = event.details + print(f"Workflow failed: {details.error_type}: {details.message}") + else: + print(event) """ Sample Output: + State: IN_PROGRESS ExecutorInvokeEvent(executor_id=writer) ExecutorCompletedEvent(executor_id=writer) ExecutorInvokeEvent(executor_id=reviewer) WorkflowCompletedEvent(data=Drive the Future. Affordable Adventure, Electrified.) ExecutorCompletedEvent(executor_id=reviewer) + State: COMPLETED """ diff --git a/python/samples/getting_started/workflow/human-in-the-loop/guessing_game_with_human_input.py b/python/samples/getting_started/workflow/human-in-the-loop/guessing_game_with_human_input.py index c82e4d666e..ca5c5ec884 100644 --- a/python/samples/getting_started/workflow/human-in-the-loop/guessing_game_with_human_input.py +++ b/python/samples/getting_started/workflow/human-in-the-loop/guessing_game_with_human_input.py @@ -3,12 +3,13 @@ import asyncio from dataclasses import dataclass -from agent_framework import AgentProtocol, ChatMessage, Role +from agent_framework import ChatMessage, Role from agent_framework.azure import AzureChatClient from agent_framework.workflow import ( - AgentExecutor, # Wraps an agent so it can run inside a workflow + AgentExecutor, # Executor that runs the agent AgentExecutorRequest, # Message bundle sent to an AgentExecutor AgentExecutorResponse, # Result returned by an AgentExecutor + Executor, RequestInfoEvent, # Event emitted when human input is requested RequestInfoExecutor, # Special executor that collects human input out of band RequestInfoMessage, # Base class for request payloads sent to RequestInfoExecutor @@ -16,6 +17,8 @@ from agent_framework.workflow import ( WorkflowBuilder, # Fluent builder for assembling the graph WorkflowCompletedEvent, # Terminal event used to finish the workflow WorkflowContext, # Per run context and event bus + WorkflowRunState, # Enum of workflow run states + WorkflowStatusEvent, # Event emitted on run state changes handler, # Decorator to expose an Executor method as a step ) from azure.identity import AzureCliCredential @@ -73,7 +76,7 @@ class GuessOutput(BaseModel): guess: int -class TurnManager(AgentExecutor): +class TurnManager(Executor): """Coordinates turns between the agent and the human. Responsibilities: @@ -82,8 +85,8 @@ class TurnManager(AgentExecutor): - After each human reply, either finish the game or prompt the agent again with feedback. """ - def __init__(self, agent: AgentProtocol, id: str | None = None): - super().__init__(agent, id=id) + def __init__(self, id: str | None = None): + super().__init__(id=id) @handler async def start(self, _: str, ctx: WorkflowContext[AgentExecutorRequest]) -> None: @@ -166,9 +169,10 @@ async def main() -> None: response_format=GuessOutput, ) - # Build a simple loop: TurnManager <-> RequestInfoExecutor. - # TurnManager runs the agent, asks the human, processes feedback, and either finishes or repeats. - turn_manager = TurnManager(agent=agent, id="turn_manager") + # Build a simple loop: TurnManager <-> AgentExecutor <-> RequestInfoExecutor. + # TurnManager coordinates, AgentExecutor runs the model, RequestInfoExecutor gathers human replies. + turn_manager = TurnManager(id="turn_manager") + agent_exec = AgentExecutor(agent=agent, id="agent") # Naming note: # This variable is currently named hitl for historical reasons. The name can feel ambiguous or magical. @@ -179,9 +183,10 @@ async def main() -> None: top_builder = ( WorkflowBuilder() .set_start_executor(turn_manager) - .add_edge(turn_manager, turn_manager) # TurnManager executes its own agent step + .add_edge(turn_manager, agent_exec) # Ask agent to make/adjust a guess + .add_edge(agent_exec, turn_manager) # Agent's response comes back to coordinator .add_edge(turn_manager, hitl) # Ask human for guidance - .add_edge(hitl, turn_manager) # Feed human guidance back to the agent turn manager + .add_edge(hitl, turn_manager) # Feed human guidance back to coordinator ) # Build the workflow (no checkpointing in this minimal sample). @@ -206,6 +211,10 @@ async def main() -> None: stream = ( workflow.send_responses_streaming(pending_responses) if pending_responses else workflow.run_stream("start") ) + # Collect events for this turn. Among these you may see WorkflowStatusEvent + # with state IDLE_WITH_PENDING_REQUESTS when the workflow pauses for + # human input, preceded by IN_PROGRESS_PENDING_REQUESTS as requests are + # emitted. events = [event async for event in stream] pending_responses = None @@ -219,6 +228,22 @@ async def main() -> None: requests.append((event.request_id, event.data.prompt)) # Other events are ignored for brevity. + # Detect run state transitions for a better developer experience. + pending_status = any( + isinstance(e, WorkflowStatusEvent) + and e.state == WorkflowRunState.IN_PROGRESS_PENDING_REQUESTS + for e in events + ) + idle_with_requests = any( + isinstance(e, WorkflowStatusEvent) + and e.state == WorkflowRunState.IDLE_WITH_PENDING_REQUESTS + for e in events + ) + if pending_status: + print("State: IN_PROGRESS_PENDING_REQUESTS (requests outstanding)") + if idle_with_requests: + print("State: IDLE_WITH_PENDING_REQUESTS (awaiting human input)") + # If we have any human requests, prompt the user and prepare responses. if requests and not completed: responses: dict[str, str] = {} @@ -227,7 +252,7 @@ async def main() -> None: print(f"HITL> {prompt}") # Instructional print already appears above. The input line below is the user entry point. # If desired, you can add more guidance here, but keep it concise. - answer = input("Enter higher/lower/correct/exit: ").lower() + answer = input("Enter higher/lower/correct/exit: ").lower() # noqa: ASYNC250 if answer == "exit": print("Exiting...") return diff --git a/python/samples/getting_started/workflow/orchestration/sequential_agents.py b/python/samples/getting_started/workflow/orchestration/sequential_agents.py new file mode 100644 index 0000000000..88aa5ba37a --- /dev/null +++ b/python/samples/getting_started/workflow/orchestration/sequential_agents.py @@ -0,0 +1,80 @@ +# Copyright (c) Microsoft. All rights reserved. + +import asyncio +from typing import Any + +from agent_framework import ChatMessage, Role +from agent_framework.azure import AzureChatClient +from agent_framework.workflow import SequentialBuilder, WorkflowCompletedEvent +from azure.identity import AzureCliCredential + +""" +Sample: Sequential workflow (agent-focused API) with shared conversation context + +Build a high-level sequential workflow using SequentialBuilder and two domain agents. +The shared conversation (list[ChatMessage]) flows through each participant. Each agent +appends its assistant message to the context. The final WorkflowCompletedEvent includes +the final conversation list. + +Note on internal adapters: +- Sequential orchestration includes small adapter nodes for input normalization + ("input-conversation"), agent-response conversion ("to-conversation:"), + and completion ("complete"). These may appear as ExecutorInvoke/Completed events in + the stream—similar to how concurrent orchestration includes a dispatcher/aggregator. + You can safely ignore them when focusing on agent progress. + +Prerequisites: +- Azure OpenAI access configured for AzureChatClient (use az login + env vars) +""" + + +async def main() -> None: + # 1) Create agents + chat_client = AzureChatClient(credential=AzureCliCredential()) + + writer = chat_client.create_agent( + instructions=("You are a concise copywriter. Provide a single, punchy marketing sentence based on the prompt."), + name="writer", + ) + + reviewer = chat_client.create_agent( + instructions=("You are a thoughtful reviewer. Give brief feedback on the previous assistant message."), + name="reviewer", + ) + + # 2) Build sequential workflow: writer -> reviewer + workflow = SequentialBuilder().participants([writer, reviewer]).build() + + # 3) Run and print final conversation + completion: WorkflowCompletedEvent | None = None + async for event in workflow.run_stream("Write a tagline for a budget-friendly eBike."): + if isinstance(event, WorkflowCompletedEvent): + completion = event + + if completion: + print("===== Final Conversation =====") + messages: list[ChatMessage] | Any = completion.data + for i, msg in enumerate(messages, start=1): + name = msg.author_name or ("assistant" if msg.role == Role.ASSISTANT else "user") + print(f"{'-' * 60}\n{i:02d} [{name}]\n{msg.text}") + + """ + Sample Output: + + ===== Final Conversation ===== + ------------------------------------------------------------ + 01 [user] + Write a tagline for a budget-friendly eBike. + ------------------------------------------------------------ + 02 [writer] + Ride farther, spend less—your affordable eBike adventure starts here. + ------------------------------------------------------------ + 03 [reviewer] + This tagline clearly communicates affordability and the benefit of extended travel, making it + appealing to budget-conscious consumers. It has a friendly and motivating tone, though it could + be slightly shorter for more punch. Overall, a strong and effective suggestion! + """ + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/python/samples/getting_started/workflow/orchestration/sequential_custom_executors.py b/python/samples/getting_started/workflow/orchestration/sequential_custom_executors.py new file mode 100644 index 0000000000..b78ea96626 --- /dev/null +++ b/python/samples/getting_started/workflow/orchestration/sequential_custom_executors.py @@ -0,0 +1,91 @@ +# Copyright (c) Microsoft. All rights reserved. + +import asyncio +from typing import Any + +from agent_framework import ChatMessage, Role +from agent_framework.azure import AzureChatClient +from agent_framework.workflow import Executor, SequentialBuilder, WorkflowCompletedEvent, WorkflowContext, handler +from azure.identity import AzureCliCredential + +""" +Sample: Sequential workflow mixing agents and a custom summarizer executor + +This demonstrates how SequentialBuilder chains participants with a shared +conversation context (list[ChatMessage]). An agent produces content; a custom +executor appends a compact summary to the conversation. The final WorkflowCompletedEvent +contains the complete conversation. + +Custom executor contract: +- Provide at least one @handler accepting list[ChatMessage] and a WorkflowContext[list[ChatMessage]] +- Emit the updated conversation via ctx.send_message([...]) + +Note on internal adapters: +- You may see adapter nodes in the event stream such as "input-conversation", + "to-conversation:", and "complete". These provide consistent typing, + conversion of agent responses into the shared conversation, and a single point + for completion—similar to concurrent's dispatcher/aggregator. + +Prerequisites: +- Azure OpenAI access configured for AzureChatClient (use az login + env vars) +""" + + +class Summarizer(Executor): + """Simple summarizer: consumes full conversation and appends an assistant summary.""" + + @handler + async def summarize(self, conversation: list[ChatMessage], ctx: WorkflowContext[list[ChatMessage]]) -> None: + users = sum(1 for m in conversation if m.role == Role.USER) + assistants = sum(1 for m in conversation if m.role == Role.ASSISTANT) + summary = ChatMessage(role=Role.ASSISTANT, text=f"Summary -> users:{users} assistants:{assistants}") + await ctx.send_message(list(conversation) + [summary]) + + +async def main() -> None: + # 1) Create a content agent + chat_client = AzureChatClient(credential=AzureCliCredential()) + content = chat_client.create_agent( + instructions="Produce a concise paragraph answering the user's request.", + name="content", + ) + + # 2) Build sequential workflow: content -> summarizer + summarizer = Summarizer(id="summarizer") + workflow = SequentialBuilder().participants([content, summarizer]).build() + + # 3) Run and print final conversation + completion: WorkflowCompletedEvent | None = None + async for event in workflow.run_stream("Explain the benefits of budget eBikes for commuters."): + if isinstance(event, WorkflowCompletedEvent): + completion = event + + if completion: + print("===== Final Conversation =====") + messages: list[ChatMessage] | Any = completion.data + for i, msg in enumerate(messages, start=1): + name = msg.author_name or ("assistant" if msg.role == Role.ASSISTANT else "user") + print(f"{'-' * 60}\n{i:02d} [{name}]\n{msg.text}") + + """ + Sample Output: + + ------------------------------------------------------------ + 01 [user] + Explain the benefits of budget eBikes for commuters. + ------------------------------------------------------------ + 02 [content] + Budget eBikes offer commuters an affordable, eco-friendly alternative to cars and public transport. + Their electric assistance reduces physical strain and allows riders to cover longer distances quickly, + minimizing travel time and fatigue. Budget models are low-cost to maintain and operate, making them accessible + for a wider range of people. Additionally, eBikes help reduce traffic congestion and carbon emissions, + supporting greener urban environments. Overall, budget eBikes provide cost-effective, efficient, and + sustainable transportation for daily commuting needs. + ------------------------------------------------------------ + 03 [assistant] + Summary -> users:1 assistants:1 + """ + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/python/uv.lock b/python/uv.lock index d6d25353a5..10ba3a3b4e 100644 --- a/python/uv.lock +++ b/python/uv.lock @@ -25,6 +25,7 @@ supported-markers = [ members = [ "agent-framework", "agent-framework-azure", + "agent-framework-copilotstudio", "agent-framework-foundry", "agent-framework-mem0", "agent-framework-project", @@ -111,6 +112,21 @@ requires-dist = [ { name = "azure-identity", specifier = ">=1.13" }, ] +[[package]] +name = "agent-framework-copilotstudio" +version = "0.1.0b1" +source = { editable = "packages/copilotstudio" } +dependencies = [ + { name = "agent-framework", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "microsoft-agents-copilotstudio-client", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] + +[package.metadata] +requires-dist = [ + { name = "agent-framework", editable = "packages/main" }, + { name = "microsoft-agents-copilotstudio-client", specifier = ">=0.3.1" }, +] + [[package]] name = "agent-framework-foundry" version = "0.1.0b1" @@ -152,6 +168,7 @@ source = { virtual = "." } dependencies = [ { name = "agent-framework", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "agent-framework-azure", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "agent-framework-copilotstudio", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "agent-framework-foundry", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "agent-framework-mem0", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "agent-framework-workflow", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -196,6 +213,7 @@ dev = [ requires-dist = [ { name = "agent-framework", editable = "packages/main" }, { name = "agent-framework-azure", editable = "packages/azure" }, + { name = "agent-framework-copilotstudio", editable = "packages/copilotstudio" }, { name = "agent-framework-foundry", editable = "packages/foundry" }, { name = "agent-framework-mem0", editable = "packages/mem0" }, { name = "agent-framework-workflow", editable = "packages/workflow" }, @@ -486,9 +504,9 @@ dependencies = [ { name = "isodate", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/30/db/cdf0ab3cf642ef766d193fadfee4cc8e70867a051c800f9e1091feb76337/azure_ai_agents-1.2.0b4.tar.gz", hash = "sha256:3da41c0f17282dd76bff7d370c714ef2d17140a119a1eb88d8653311835bbe85", size = 383620, upload-time = "2025-09-13T02:04:47.301Z" } +sdist = { url = "https://files.pythonhosted.org/packages/30/db/cdf0ab3cf642ef766d193fadfee4cc8e70867a051c800f9e1091feb76337/azure_ai_agents-1.2.0b4.tar.gz", hash = "sha256:3da41c0f17282dd76bff7d370c714ef2d17140a119a1eb88d8653311835bbe85", size = 383620 } wheels = [ - { url = "https://files.pythonhosted.org/packages/3c/08/571e60c5f76c3ba20e6a99de636335e2480941816344dfad971a2e362c12/azure_ai_agents-1.2.0b4-py3-none-any.whl", hash = "sha256:8cfc4cd19d7d2087858148eba8b9b424a5d30fafbda87ca2d12ea5a066bec051", size = 214705, upload-time = "2025-09-13T02:04:49.171Z" }, + { url = "https://files.pythonhosted.org/packages/3c/08/571e60c5f76c3ba20e6a99de636335e2480941816344dfad971a2e362c12/azure_ai_agents-1.2.0b4-py3-none-any.whl", hash = "sha256:8cfc4cd19d7d2087858148eba8b9b424a5d30fafbda87ca2d12ea5a066bec051", size = 214705 }, ] [[package]] @@ -502,9 +520,9 @@ dependencies = [ { name = "isodate", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bc/16/7a7c978a79f545d62ab4327cd704c22b5d7ade8dcfb58ea193257aebabf9/azure_ai_projects-1.1.0b4.tar.gz", hash = "sha256:39e2f1396270b375069c2d9c82ccfe91c11384eca9f61d59adbc12fb6d6a32ca", size = 147568, upload-time = "2025-09-12T17:35:08.52Z" } +sdist = { url = "https://files.pythonhosted.org/packages/bc/16/7a7c978a79f545d62ab4327cd704c22b5d7ade8dcfb58ea193257aebabf9/azure_ai_projects-1.1.0b4.tar.gz", hash = "sha256:39e2f1396270b375069c2d9c82ccfe91c11384eca9f61d59adbc12fb6d6a32ca", size = 147568 } wheels = [ - { url = "https://files.pythonhosted.org/packages/db/10/8b7bd070e3cc804343dab124ce66a3b7999a72d5be0e49232cbcd1d36e18/azure_ai_projects-1.1.0b4-py3-none-any.whl", hash = "sha256:d8aab84fd7cd7c5937e78141e37ca4473dc5ed6cce2c0490c634418abe14afea", size = 126670, upload-time = "2025-09-12T17:35:10.039Z" }, + { url = "https://files.pythonhosted.org/packages/db/10/8b7bd070e3cc804343dab124ce66a3b7999a72d5be0e49232cbcd1d36e18/azure_ai_projects-1.1.0b4-py3-none-any.whl", hash = "sha256:d8aab84fd7cd7c5937e78141e37ca4473dc5ed6cce2c0490c634418abe14afea", size = 126670 }, ] [[package]] @@ -516,9 +534,9 @@ dependencies = [ { name = "six", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/15/6b/2653adc0f33adba8f11b1903701e6b1c10d34ce5d8e25dfa13a422f832b0/azure_core-1.35.1.tar.gz", hash = "sha256:435d05d6df0fff2f73fb3c15493bb4721ede14203f1ff1382aa6b6b2bdd7e562", size = 345290, upload-time = "2025-09-11T22:58:04.481Z" } +sdist = { url = "https://files.pythonhosted.org/packages/15/6b/2653adc0f33adba8f11b1903701e6b1c10d34ce5d8e25dfa13a422f832b0/azure_core-1.35.1.tar.gz", hash = "sha256:435d05d6df0fff2f73fb3c15493bb4721ede14203f1ff1382aa6b6b2bdd7e562", size = 345290 } wheels = [ - { url = "https://files.pythonhosted.org/packages/27/52/805980aa1ba18282077c484dba634ef0ede1e84eec8be9c92b2e162d0ed6/azure_core-1.35.1-py3-none-any.whl", hash = "sha256:12da0c9e08e48e198f9158b56ddbe33b421477e1dc98c2e1c8f9e254d92c468b", size = 211800, upload-time = "2025-09-11T22:58:06.281Z" }, + { url = "https://files.pythonhosted.org/packages/27/52/805980aa1ba18282077c484dba634ef0ede1e84eec8be9c92b2e162d0ed6/azure_core-1.35.1-py3-none-any.whl", hash = "sha256:12da0c9e08e48e198f9158b56ddbe33b421477e1dc98c2e1c8f9e254d92c468b", size = 211800 }, ] [[package]] @@ -545,9 +563,9 @@ dependencies = [ { name = "msal-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4e/9e/4c9682a286c3c89e437579bd9f64f311020e5125c1321fd3a653166b5716/azure_identity-1.25.0.tar.gz", hash = "sha256:4177df34d684cddc026e6cf684e1abb57767aa9d84e7f2129b080ec45eee7733", size = 278507, upload-time = "2025-09-12T01:30:04.418Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/9e/4c9682a286c3c89e437579bd9f64f311020e5125c1321fd3a653166b5716/azure_identity-1.25.0.tar.gz", hash = "sha256:4177df34d684cddc026e6cf684e1abb57767aa9d84e7f2129b080ec45eee7733", size = 278507 } wheels = [ - { url = "https://files.pythonhosted.org/packages/75/54/81683b6756676a22e037b209695b08008258e603f7e47c56834029c5922a/azure_identity-1.25.0-py3-none-any.whl", hash = "sha256:becaec086bbdf8d1a6aa4fb080c2772a0f824a97d50c29637ec8cc4933f1e82d", size = 190861, upload-time = "2025-09-12T01:30:06.474Z" }, + { url = "https://files.pythonhosted.org/packages/75/54/81683b6756676a22e037b209695b08008258e603f7e47c56834029c5922a/azure_identity-1.25.0-py3-none-any.whl", hash = "sha256:becaec086bbdf8d1a6aa4fb080c2772a0f824a97d50c29637ec8cc4933f1e82d", size = 190861 }, ] [[package]] @@ -1052,7 +1070,7 @@ name = "exceptiongroup" version = "1.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "(python_full_version < '3.13' and sys_platform == 'darwin') or (python_full_version < '3.13' and sys_platform == 'linux') or (python_full_version < '3.13' and sys_platform == 'win32')" }, + { name = "typing-extensions", marker = "(python_full_version < '3.11' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform == 'win32')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" } wheels = [ @@ -1582,73 +1600,72 @@ wheels = [ name = "jiter" version = "0.11.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9d/c0/a3bb4cc13aced219dd18191ea66e874266bd8aa7b96744e495e1c733aa2d/jiter-0.11.0.tar.gz", hash = "sha256:1d9637eaf8c1d6a63d6562f2a6e5ab3af946c66037eb1b894e8fad75422266e4", size = 167094, upload-time = "2025-09-15T09:20:38.212Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/c0/a3bb4cc13aced219dd18191ea66e874266bd8aa7b96744e495e1c733aa2d/jiter-0.11.0.tar.gz", hash = "sha256:1d9637eaf8c1d6a63d6562f2a6e5ab3af946c66037eb1b894e8fad75422266e4", size = 167094 } wheels = [ - { url = "https://files.pythonhosted.org/packages/25/21/7dd1235a19e26979be6098e87e4cced2e061752f3a40a17bbce6dea7fae1/jiter-0.11.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3893ce831e1c0094a83eeaf56c635a167d6fa8cc14393cc14298fd6fdc2a2449", size = 309875, upload-time = "2025-09-15T09:18:48.41Z" }, - { url = "https://files.pythonhosted.org/packages/71/f9/462b54708aa85b135733ccba70529dd68a18511bf367a87c5fd28676c841/jiter-0.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:25c625b9b61b5a8725267fdf867ef2e51b429687f6a4eef211f4612e95607179", size = 316505, upload-time = "2025-09-15T09:18:51.057Z" }, - { url = "https://files.pythonhosted.org/packages/bd/40/14e2eeaac6a47bff27d213834795472355fd39769272eb53cb7aa83d5aa8/jiter-0.11.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd4ca85fb6a62cf72e1c7f5e34ddef1b660ce4ed0886ec94a1ef9777d35eaa1f", size = 337613, upload-time = "2025-09-15T09:18:52.358Z" }, - { url = "https://files.pythonhosted.org/packages/d3/ed/a5f1f8419c92b150a7c7fb5ccba1fb1e192887ad713d780e70874f0ce996/jiter-0.11.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:572208127034725e79c28437b82414028c3562335f2b4f451d98136d0fc5f9cd", size = 361438, upload-time = "2025-09-15T09:18:54.637Z" }, - { url = "https://files.pythonhosted.org/packages/dd/f5/70682c023dfcdd463a53faf5d30205a7d99c51d70d3e303c932d0936e5a2/jiter-0.11.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:494ba627c7f550ad3dabb21862864b8f2216098dc18ff62f37b37796f2f7c325", size = 486180, upload-time = "2025-09-15T09:18:56.158Z" }, - { url = "https://files.pythonhosted.org/packages/7c/39/020d08cbab4eab48142ad88b837c41eb08a15c0767fdb7c0d3265128a44b/jiter-0.11.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8da18a99f58bca3ecc2d2bba99cac000a924e115b6c4f0a2b98f752b6fbf39a", size = 376681, upload-time = "2025-09-15T09:18:57.553Z" }, - { url = "https://files.pythonhosted.org/packages/52/10/b86733f6e594cf51dd142f37c602d8df87c554c5844958deaab0de30eb5d/jiter-0.11.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4ffd3b0fff3fabbb02cc09910c08144db6bb5697a98d227a074401e01ee63dd", size = 348685, upload-time = "2025-09-15T09:18:59.208Z" }, - { url = "https://files.pythonhosted.org/packages/fb/ee/8861665e83a9e703aa5f65fddddb6225428e163e6b0baa95a7f9a8fb9aae/jiter-0.11.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8fe6530aa738a4f7d4e4702aa8f9581425d04036a5f9e25af65ebe1f708f23be", size = 385573, upload-time = "2025-09-15T09:19:00.593Z" }, - { url = "https://files.pythonhosted.org/packages/25/74/05afec03600951f128293813b5a208c9ba1bf587c57a344c05a42a69e1b1/jiter-0.11.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e35d66681c133a03d7e974e7eedae89720fe8ca3bd09f01a4909b86a8adf31f5", size = 516669, upload-time = "2025-09-15T09:19:02.369Z" }, - { url = "https://files.pythonhosted.org/packages/93/d1/2e5bfe147cfbc2a5eef7f73eb75dc5c6669da4fa10fc7937181d93af9495/jiter-0.11.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c59459beca2fbc9718b6f1acb7bfb59ebc3eb4294fa4d40e9cb679dafdcc6c60", size = 508767, upload-time = "2025-09-15T09:19:04.011Z" }, - { url = "https://files.pythonhosted.org/packages/87/50/597f71307e10426b5c082fd05d38c615ddbdd08c3348d8502963307f0652/jiter-0.11.0-cp310-cp310-win32.whl", hash = "sha256:b7b0178417b0dcfc5f259edbc6db2b1f5896093ed9035ee7bab0f2be8854726d", size = 205476, upload-time = "2025-09-15T09:19:05.594Z" }, - { url = "https://files.pythonhosted.org/packages/c7/86/1e5214b3272e311754da26e63edec93a183811d4fc2e0118addec365df8b/jiter-0.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:11df2bf99fb4754abddd7f5d940a48e51f9d11624d6313ca4314145fcad347f0", size = 204708, upload-time = "2025-09-15T09:19:06.955Z" }, - { url = "https://files.pythonhosted.org/packages/38/55/a69fefeef09c2eaabae44b935a1aa81517e49639c0a0c25d861cb18cd7ac/jiter-0.11.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:cb5d9db02979c3f49071fce51a48f4b4e4cf574175fb2b11c7a535fa4867b222", size = 309503, upload-time = "2025-09-15T09:19:08.191Z" }, - { url = "https://files.pythonhosted.org/packages/bd/d5/a6aba9e6551f32f9c127184f398208e4eddb96c59ac065c8a92056089d28/jiter-0.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1dc6a123f3471c4730db7ca8ba75f1bb3dcb6faeb8d46dd781083e7dee88b32d", size = 317688, upload-time = "2025-09-15T09:19:09.918Z" }, - { url = "https://files.pythonhosted.org/packages/bb/f3/5e86f57c1883971cdc8535d0429c2787bf734840a231da30a3be12850562/jiter-0.11.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09858f8d230f031c7b8e557429102bf050eea29c77ad9c34c8fe253c5329acb7", size = 337418, upload-time = "2025-09-15T09:19:11.078Z" }, - { url = "https://files.pythonhosted.org/packages/5e/4f/a71d8a24c2a70664970574a8e0b766663f5ef788f7fe1cc20ee0c016d488/jiter-0.11.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dbe2196c4a0ce760925a74ab4456bf644748ab0979762139626ad138f6dac72d", size = 361423, upload-time = "2025-09-15T09:19:13.286Z" }, - { url = "https://files.pythonhosted.org/packages/8f/e5/b09076f4e7fd9471b91e16f9f3dc7330b161b738f3b39b2c37054a36e26a/jiter-0.11.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5beb56d22b63647bafd0b74979216fdee80c580c0c63410be8c11053860ffd09", size = 486367, upload-time = "2025-09-15T09:19:14.546Z" }, - { url = "https://files.pythonhosted.org/packages/fb/f1/98cb3a36f5e62f80cd860f0179f948d9eab5a316d55d3e1bab98d9767af5/jiter-0.11.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97025d09ef549795d8dc720a824312cee3253c890ac73c621721ddfc75066789", size = 376335, upload-time = "2025-09-15T09:19:15.939Z" }, - { url = "https://files.pythonhosted.org/packages/9f/d8/ec74886497ea393c29dbd7651ddecc1899e86404a6b1f84a3ddab0ab59fd/jiter-0.11.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d50880a6da65d8c23a2cf53c412847d9757e74cc9a3b95c5704a1d1a24667347", size = 348981, upload-time = "2025-09-15T09:19:17.568Z" }, - { url = "https://files.pythonhosted.org/packages/24/93/d22ad7fa3b86ade66c86153ceea73094fc2af8b20c59cb7fceab9fea4704/jiter-0.11.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:452d80a1c86c095a242007bd9fc5d21b8a8442307193378f891cb8727e469648", size = 385797, upload-time = "2025-09-15T09:19:19.121Z" }, - { url = "https://files.pythonhosted.org/packages/c8/bd/e25ff4a4df226e9b885f7cb01ee4b9dc74e3000e612d6f723860d71a1f34/jiter-0.11.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e84e58198d4894668eec2da660ffff60e0f3e60afa790ecc50cb12b0e02ca1d4", size = 516597, upload-time = "2025-09-15T09:19:20.301Z" }, - { url = "https://files.pythonhosted.org/packages/be/fb/beda613db7d93ffa2fdd2683f90f2f5dce8daf4bc2d0d2829e7de35308c6/jiter-0.11.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:df64edcfc5dd5279a791eea52aa113d432c933119a025b0b5739f90d2e4e75f1", size = 508853, upload-time = "2025-09-15T09:19:22.075Z" }, - { url = "https://files.pythonhosted.org/packages/20/64/c5b0d93490634e41e38e2a15de5d54fdbd2c9f64a19abb0f95305b63373c/jiter-0.11.0-cp311-cp311-win32.whl", hash = "sha256:144fc21337d21b1d048f7f44bf70881e1586401d405ed3a98c95a114a9994982", size = 205140, upload-time = "2025-09-15T09:19:23.351Z" }, - { url = "https://files.pythonhosted.org/packages/a1/e6/c347c0e6f5796e97d4356b7e5ff0ce336498b7f4ef848fae621a56f1ccf3/jiter-0.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:b0f32e644d241293b892b1a6dd8f0b9cc029bfd94c97376b2681c36548aabab7", size = 204311, upload-time = "2025-09-15T09:19:24.591Z" }, - { url = "https://files.pythonhosted.org/packages/ba/b5/3009b112b8f673e568ef79af9863d8309a15f0a8cdcc06ed6092051f377e/jiter-0.11.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:2fb7b377688cc3850bbe5c192a6bd493562a0bc50cbc8b047316428fbae00ada", size = 305510, upload-time = "2025-09-15T09:19:25.893Z" }, - { url = "https://files.pythonhosted.org/packages/fe/82/15514244e03b9e71e086bbe2a6de3e4616b48f07d5f834200c873956fb8c/jiter-0.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a1b7cbe3f25bd0d8abb468ba4302a5d45617ee61b2a7a638f63fee1dc086be99", size = 316521, upload-time = "2025-09-15T09:19:27.525Z" }, - { url = "https://files.pythonhosted.org/packages/92/94/7a2e905f40ad2d6d660e00b68d818f9e29fb87ffe82774f06191e93cbe4a/jiter-0.11.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c0a7f0ec81d5b7588c5cade1eb1925b91436ae6726dc2df2348524aeabad5de6", size = 338214, upload-time = "2025-09-15T09:19:28.727Z" }, - { url = "https://files.pythonhosted.org/packages/a8/9c/5791ed5bdc76f12110158d3316a7a3ec0b1413d018b41c5ed399549d3ad5/jiter-0.11.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07630bb46ea2a6b9c6ed986c6e17e35b26148cce2c535454b26ee3f0e8dcaba1", size = 361280, upload-time = "2025-09-15T09:19:30.013Z" }, - { url = "https://files.pythonhosted.org/packages/d4/7f/b7d82d77ff0d2cb06424141000176b53a9e6b16a1125525bb51ea4990c2e/jiter-0.11.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7764f27d28cd4a9cbc61704dfcd80c903ce3aad106a37902d3270cd6673d17f4", size = 487895, upload-time = "2025-09-15T09:19:31.424Z" }, - { url = "https://files.pythonhosted.org/packages/42/44/10a1475d46f1fc1fd5cc2e82c58e7bca0ce5852208e0fa5df2f949353321/jiter-0.11.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1d4a6c4a737d486f77f842aeb22807edecb4a9417e6700c7b981e16d34ba7c72", size = 378421, upload-time = "2025-09-15T09:19:32.746Z" }, - { url = "https://files.pythonhosted.org/packages/9a/5f/0dc34563d8164d31d07bc09d141d3da08157a68dcd1f9b886fa4e917805b/jiter-0.11.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cf408d2a0abd919b60de8c2e7bc5eeab72d4dafd18784152acc7c9adc3291591", size = 347932, upload-time = "2025-09-15T09:19:34.612Z" }, - { url = "https://files.pythonhosted.org/packages/f7/de/b68f32a4fcb7b4a682b37c73a0e5dae32180140cd1caf11aef6ad40ddbf2/jiter-0.11.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cdef53eda7d18e799625023e1e250dbc18fbc275153039b873ec74d7e8883e09", size = 386959, upload-time = "2025-09-15T09:19:35.994Z" }, - { url = "https://files.pythonhosted.org/packages/76/0a/c08c92e713b6e28972a846a81ce374883dac2f78ec6f39a0dad9f2339c3a/jiter-0.11.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:53933a38ef7b551dd9c7f1064f9d7bb235bb3168d0fa5f14f0798d1b7ea0d9c5", size = 517187, upload-time = "2025-09-15T09:19:37.426Z" }, - { url = "https://files.pythonhosted.org/packages/89/b5/4a283bec43b15aad54fcae18d951f06a2ec3f78db5708d3b59a48e9c3fbd/jiter-0.11.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:11840d2324c9ab5162fc1abba23bc922124fedcff0d7b7f85fffa291e2f69206", size = 509461, upload-time = "2025-09-15T09:19:38.761Z" }, - { url = "https://files.pythonhosted.org/packages/34/a5/f8bad793010534ea73c985caaeef8cc22dfb1fedb15220ecdf15c623c07a/jiter-0.11.0-cp312-cp312-win32.whl", hash = "sha256:4f01a744d24a5f2bb4a11657a1b27b61dc038ae2e674621a74020406e08f749b", size = 206664, upload-time = "2025-09-15T09:19:40.096Z" }, - { url = "https://files.pythonhosted.org/packages/ed/42/5823ec2b1469395a160b4bf5f14326b4a098f3b6898fbd327366789fa5d3/jiter-0.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:29fff31190ab3a26de026da2f187814f4b9c6695361e20a9ac2123e4d4378a4c", size = 203520, upload-time = "2025-09-15T09:19:41.798Z" }, - { url = "https://files.pythonhosted.org/packages/97/c4/d530e514d0f4f29b2b68145e7b389cbc7cac7f9c8c23df43b04d3d10fa3e/jiter-0.11.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:4441a91b80a80249f9a6452c14b2c24708f139f64de959943dfeaa6cb915e8eb", size = 305021, upload-time = "2025-09-15T09:19:43.523Z" }, - { url = "https://files.pythonhosted.org/packages/7a/77/796a19c567c5734cbfc736a6f987affc0d5f240af8e12063c0fb93990ffa/jiter-0.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ff85fc6d2a431251ad82dbd1ea953affb5a60376b62e7d6809c5cd058bb39471", size = 314384, upload-time = "2025-09-15T09:19:44.849Z" }, - { url = "https://files.pythonhosted.org/packages/14/9c/824334de0b037b91b6f3fa9fe5a191c83977c7ec4abe17795d3cb6d174cf/jiter-0.11.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5e86126d64706fd28dfc46f910d496923c6f95b395138c02d0e252947f452bd", size = 337389, upload-time = "2025-09-15T09:19:46.094Z" }, - { url = "https://files.pythonhosted.org/packages/a2/95/ed4feab69e6cf9b2176ea29d4ef9d01a01db210a3a2c8a31a44ecdc68c38/jiter-0.11.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4ad8bd82165961867a10f52010590ce0b7a8c53da5ddd8bbb62fef68c181b921", size = 360519, upload-time = "2025-09-15T09:19:47.494Z" }, - { url = "https://files.pythonhosted.org/packages/b5/0c/2ad00f38d3e583caba3909d95b7da1c3a7cd82c0aa81ff4317a8016fb581/jiter-0.11.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b42c2cd74273455ce439fd9528db0c6e84b5623cb74572305bdd9f2f2961d3df", size = 487198, upload-time = "2025-09-15T09:19:49.116Z" }, - { url = "https://files.pythonhosted.org/packages/ea/8b/919b64cf3499b79bdfba6036da7b0cac5d62d5c75a28fb45bad7819e22f0/jiter-0.11.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f0062dab98172dd0599fcdbf90214d0dcde070b1ff38a00cc1b90e111f071982", size = 377835, upload-time = "2025-09-15T09:19:50.468Z" }, - { url = "https://files.pythonhosted.org/packages/29/7f/8ebe15b6e0a8026b0d286c083b553779b4dd63db35b43a3f171b544de91d/jiter-0.11.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb948402821bc76d1f6ef0f9e19b816f9b09f8577844ba7140f0b6afe994bc64", size = 347655, upload-time = "2025-09-15T09:19:51.726Z" }, - { url = "https://files.pythonhosted.org/packages/8e/64/332127cef7e94ac75719dda07b9a472af6158ba819088d87f17f3226a769/jiter-0.11.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:25a5b1110cca7329fd0daf5060faa1234be5c11e988948e4f1a1923b6a457fe1", size = 386135, upload-time = "2025-09-15T09:19:53.075Z" }, - { url = "https://files.pythonhosted.org/packages/20/c8/557b63527442f84c14774159948262a9d4fabb0d61166f11568f22fc60d2/jiter-0.11.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:bf11807e802a214daf6c485037778843fadd3e2ec29377ae17e0706ec1a25758", size = 516063, upload-time = "2025-09-15T09:19:54.447Z" }, - { url = "https://files.pythonhosted.org/packages/86/13/4164c819df4a43cdc8047f9a42880f0ceef5afeb22e8b9675c0528ebdccd/jiter-0.11.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:dbb57da40631c267861dd0090461222060960012d70fd6e4c799b0f62d0ba166", size = 508139, upload-time = "2025-09-15T09:19:55.764Z" }, - { url = "https://files.pythonhosted.org/packages/fa/70/6e06929b401b331d41ddb4afb9f91cd1168218e3371972f0afa51c9f3c31/jiter-0.11.0-cp313-cp313-win32.whl", hash = "sha256:8e36924dad32c48d3c5e188d169e71dc6e84d6cb8dedefea089de5739d1d2f80", size = 206369, upload-time = "2025-09-15T09:19:57.048Z" }, - { url = "https://files.pythonhosted.org/packages/f4/0d/8185b8e15de6dce24f6afae63380e16377dd75686d56007baa4f29723ea1/jiter-0.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:452d13e4fd59698408087235259cebe67d9d49173b4dacb3e8d35ce4acf385d6", size = 202538, upload-time = "2025-09-15T09:19:58.35Z" }, - { url = "https://files.pythonhosted.org/packages/13/3a/d61707803260d59520721fa326babfae25e9573a88d8b7b9cb54c5423a59/jiter-0.11.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:089f9df9f69532d1339e83142438668f52c97cd22ee2d1195551c2b1a9e6cf33", size = 313737, upload-time = "2025-09-15T09:19:59.638Z" }, - { url = "https://files.pythonhosted.org/packages/cd/cc/c9f0eec5d00f2a1da89f6bdfac12b8afdf8d5ad974184863c75060026457/jiter-0.11.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:29ed1fe69a8c69bf0f2a962d8d706c7b89b50f1332cd6b9fbda014f60bd03a03", size = 346183, upload-time = "2025-09-15T09:20:01.442Z" }, - { url = "https://files.pythonhosted.org/packages/a6/87/fc632776344e7aabbab05a95a0075476f418c5d29ab0f2eec672b7a1f0ac/jiter-0.11.0-cp313-cp313t-win_amd64.whl", hash = "sha256:a4d71d7ea6ea8786291423fe209acf6f8d398a0759d03e7f24094acb8ab686ba", size = 204225, upload-time = "2025-09-15T09:20:03.102Z" }, - { url = "https://files.pythonhosted.org/packages/ee/3b/e7f45be7d3969bdf2e3cd4b816a7a1d272507cd0edd2d6dc4b07514f2d9a/jiter-0.11.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:9a6dff27eca70930bdbe4cbb7c1a4ba8526e13b63dc808c0670083d2d51a4a72", size = 304414, upload-time = "2025-09-15T09:20:04.357Z" }, - { url = "https://files.pythonhosted.org/packages/06/32/13e8e0d152631fcc1907ceb4943711471be70496d14888ec6e92034e2caf/jiter-0.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b1ae2a7593a62132c7d4c2abbee80bbbb94fdc6d157e2c6cc966250c564ef774", size = 314223, upload-time = "2025-09-15T09:20:05.631Z" }, - { url = "https://files.pythonhosted.org/packages/0c/7e/abedd5b5a20ca083f778d96bba0d2366567fcecb0e6e34ff42640d5d7a18/jiter-0.11.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b13a431dba4b059e9e43019d3022346d009baf5066c24dcdea321a303cde9f0", size = 337306, upload-time = "2025-09-15T09:20:06.917Z" }, - { url = "https://files.pythonhosted.org/packages/ac/e2/30d59bdc1204c86aa975ec72c48c482fee6633120ee9c3ab755e4dfefea8/jiter-0.11.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:af62e84ca3889604ebb645df3b0a3f3bcf6b92babbff642bd214616f57abb93a", size = 360565, upload-time = "2025-09-15T09:20:08.283Z" }, - { url = "https://files.pythonhosted.org/packages/fe/88/567288e0d2ed9fa8f7a3b425fdaf2cb82b998633c24fe0d98f5417321aa8/jiter-0.11.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c6f3b32bb723246e6b351aecace52aba78adb8eeb4b2391630322dc30ff6c773", size = 486465, upload-time = "2025-09-15T09:20:09.613Z" }, - { url = "https://files.pythonhosted.org/packages/18/6e/7b72d09273214cadd15970e91dd5ed9634bee605176107db21e1e4205eb1/jiter-0.11.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:adcab442f4a099a358a7f562eaa54ed6456fb866e922c6545a717be51dbed7d7", size = 377581, upload-time = "2025-09-15T09:20:10.884Z" }, - { url = "https://files.pythonhosted.org/packages/58/52/4db456319f9d14deed325f70102577492e9d7e87cf7097bda9769a1fcacb/jiter-0.11.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9967c2ab338ee2b2c0102fd379ec2693c496abf71ffd47e4d791d1f593b68e2", size = 347102, upload-time = "2025-09-15T09:20:12.175Z" }, - { url = "https://files.pythonhosted.org/packages/ce/b4/433d5703c38b26083aec7a733eb5be96f9c6085d0e270a87ca6482cbf049/jiter-0.11.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e7d0bed3b187af8b47a981d9742ddfc1d9b252a7235471ad6078e7e4e5fe75c2", size = 386477, upload-time = "2025-09-15T09:20:13.428Z" }, - { url = "https://files.pythonhosted.org/packages/c8/7a/a60bfd9c55b55b07c5c441c5085f06420b6d493ce9db28d069cc5b45d9f3/jiter-0.11.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:f6fe0283e903ebc55f1a6cc569b8c1f3bf4abd026fed85e3ff8598a9e6f982f0", size = 516004, upload-time = "2025-09-15T09:20:14.848Z" }, - { url = "https://files.pythonhosted.org/packages/2e/46/f8363e5ecc179b4ed0ca6cb0a6d3bfc266078578c71ff30642ea2ce2f203/jiter-0.11.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:4ee5821e3d66606b29ae5b497230b304f1376f38137d69e35f8d2bd5f310ff73", size = 507855, upload-time = "2025-09-15T09:20:16.176Z" }, - { url = "https://files.pythonhosted.org/packages/90/33/396083357d51d7ff0f9805852c288af47480d30dd31d8abc74909b020761/jiter-0.11.0-cp314-cp314-win32.whl", hash = "sha256:c2d13ba7567ca8799f17c76ed56b1d49be30df996eb7fa33e46b62800562a5e2", size = 205802, upload-time = "2025-09-15T09:20:17.661Z" }, - { url = "https://files.pythonhosted.org/packages/e7/ab/eb06ca556b2551d41de7d03bf2ee24285fa3d0c58c5f8d95c64c9c3281b1/jiter-0.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:fb4790497369d134a07fc763cc88888c46f734abdd66f9fdf7865038bf3a8f40", size = 313405, upload-time = "2025-09-15T09:20:18.918Z" }, - { url = "https://files.pythonhosted.org/packages/af/22/7ab7b4ec3a1c1f03aef376af11d23b05abcca3fb31fbca1e7557053b1ba2/jiter-0.11.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e2bbf24f16ba5ad4441a9845e40e4ea0cb9eed00e76ba94050664ef53ef4406", size = 347102, upload-time = "2025-09-15T09:20:20.16Z" }, - { url = "https://files.pythonhosted.org/packages/70/f3/ce100253c80063a7b8b406e1d1562657fd4b9b4e1b562db40e68645342fb/jiter-0.11.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:902b43386c04739229076bd1c4c69de5d115553d982ab442a8ae82947c72ede7", size = 336380, upload-time = "2025-09-15T09:20:36.867Z" }, + { url = "https://files.pythonhosted.org/packages/25/21/7dd1235a19e26979be6098e87e4cced2e061752f3a40a17bbce6dea7fae1/jiter-0.11.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3893ce831e1c0094a83eeaf56c635a167d6fa8cc14393cc14298fd6fdc2a2449", size = 309875 }, + { url = "https://files.pythonhosted.org/packages/71/f9/462b54708aa85b135733ccba70529dd68a18511bf367a87c5fd28676c841/jiter-0.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:25c625b9b61b5a8725267fdf867ef2e51b429687f6a4eef211f4612e95607179", size = 316505 }, + { url = "https://files.pythonhosted.org/packages/bd/40/14e2eeaac6a47bff27d213834795472355fd39769272eb53cb7aa83d5aa8/jiter-0.11.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd4ca85fb6a62cf72e1c7f5e34ddef1b660ce4ed0886ec94a1ef9777d35eaa1f", size = 337613 }, + { url = "https://files.pythonhosted.org/packages/d3/ed/a5f1f8419c92b150a7c7fb5ccba1fb1e192887ad713d780e70874f0ce996/jiter-0.11.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:572208127034725e79c28437b82414028c3562335f2b4f451d98136d0fc5f9cd", size = 361438 }, + { url = "https://files.pythonhosted.org/packages/dd/f5/70682c023dfcdd463a53faf5d30205a7d99c51d70d3e303c932d0936e5a2/jiter-0.11.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:494ba627c7f550ad3dabb21862864b8f2216098dc18ff62f37b37796f2f7c325", size = 486180 }, + { url = "https://files.pythonhosted.org/packages/7c/39/020d08cbab4eab48142ad88b837c41eb08a15c0767fdb7c0d3265128a44b/jiter-0.11.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8da18a99f58bca3ecc2d2bba99cac000a924e115b6c4f0a2b98f752b6fbf39a", size = 376681 }, + { url = "https://files.pythonhosted.org/packages/52/10/b86733f6e594cf51dd142f37c602d8df87c554c5844958deaab0de30eb5d/jiter-0.11.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4ffd3b0fff3fabbb02cc09910c08144db6bb5697a98d227a074401e01ee63dd", size = 348685 }, + { url = "https://files.pythonhosted.org/packages/fb/ee/8861665e83a9e703aa5f65fddddb6225428e163e6b0baa95a7f9a8fb9aae/jiter-0.11.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8fe6530aa738a4f7d4e4702aa8f9581425d04036a5f9e25af65ebe1f708f23be", size = 385573 }, + { url = "https://files.pythonhosted.org/packages/25/74/05afec03600951f128293813b5a208c9ba1bf587c57a344c05a42a69e1b1/jiter-0.11.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e35d66681c133a03d7e974e7eedae89720fe8ca3bd09f01a4909b86a8adf31f5", size = 516669 }, + { url = "https://files.pythonhosted.org/packages/93/d1/2e5bfe147cfbc2a5eef7f73eb75dc5c6669da4fa10fc7937181d93af9495/jiter-0.11.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c59459beca2fbc9718b6f1acb7bfb59ebc3eb4294fa4d40e9cb679dafdcc6c60", size = 508767 }, + { url = "https://files.pythonhosted.org/packages/87/50/597f71307e10426b5c082fd05d38c615ddbdd08c3348d8502963307f0652/jiter-0.11.0-cp310-cp310-win32.whl", hash = "sha256:b7b0178417b0dcfc5f259edbc6db2b1f5896093ed9035ee7bab0f2be8854726d", size = 205476 }, + { url = "https://files.pythonhosted.org/packages/c7/86/1e5214b3272e311754da26e63edec93a183811d4fc2e0118addec365df8b/jiter-0.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:11df2bf99fb4754abddd7f5d940a48e51f9d11624d6313ca4314145fcad347f0", size = 204708 }, + { url = "https://files.pythonhosted.org/packages/38/55/a69fefeef09c2eaabae44b935a1aa81517e49639c0a0c25d861cb18cd7ac/jiter-0.11.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:cb5d9db02979c3f49071fce51a48f4b4e4cf574175fb2b11c7a535fa4867b222", size = 309503 }, + { url = "https://files.pythonhosted.org/packages/bd/d5/a6aba9e6551f32f9c127184f398208e4eddb96c59ac065c8a92056089d28/jiter-0.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1dc6a123f3471c4730db7ca8ba75f1bb3dcb6faeb8d46dd781083e7dee88b32d", size = 317688 }, + { url = "https://files.pythonhosted.org/packages/bb/f3/5e86f57c1883971cdc8535d0429c2787bf734840a231da30a3be12850562/jiter-0.11.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09858f8d230f031c7b8e557429102bf050eea29c77ad9c34c8fe253c5329acb7", size = 337418 }, + { url = "https://files.pythonhosted.org/packages/5e/4f/a71d8a24c2a70664970574a8e0b766663f5ef788f7fe1cc20ee0c016d488/jiter-0.11.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dbe2196c4a0ce760925a74ab4456bf644748ab0979762139626ad138f6dac72d", size = 361423 }, + { url = "https://files.pythonhosted.org/packages/8f/e5/b09076f4e7fd9471b91e16f9f3dc7330b161b738f3b39b2c37054a36e26a/jiter-0.11.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5beb56d22b63647bafd0b74979216fdee80c580c0c63410be8c11053860ffd09", size = 486367 }, + { url = "https://files.pythonhosted.org/packages/fb/f1/98cb3a36f5e62f80cd860f0179f948d9eab5a316d55d3e1bab98d9767af5/jiter-0.11.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97025d09ef549795d8dc720a824312cee3253c890ac73c621721ddfc75066789", size = 376335 }, + { url = "https://files.pythonhosted.org/packages/9f/d8/ec74886497ea393c29dbd7651ddecc1899e86404a6b1f84a3ddab0ab59fd/jiter-0.11.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d50880a6da65d8c23a2cf53c412847d9757e74cc9a3b95c5704a1d1a24667347", size = 348981 }, + { url = "https://files.pythonhosted.org/packages/24/93/d22ad7fa3b86ade66c86153ceea73094fc2af8b20c59cb7fceab9fea4704/jiter-0.11.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:452d80a1c86c095a242007bd9fc5d21b8a8442307193378f891cb8727e469648", size = 385797 }, + { url = "https://files.pythonhosted.org/packages/c8/bd/e25ff4a4df226e9b885f7cb01ee4b9dc74e3000e612d6f723860d71a1f34/jiter-0.11.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e84e58198d4894668eec2da660ffff60e0f3e60afa790ecc50cb12b0e02ca1d4", size = 516597 }, + { url = "https://files.pythonhosted.org/packages/be/fb/beda613db7d93ffa2fdd2683f90f2f5dce8daf4bc2d0d2829e7de35308c6/jiter-0.11.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:df64edcfc5dd5279a791eea52aa113d432c933119a025b0b5739f90d2e4e75f1", size = 508853 }, + { url = "https://files.pythonhosted.org/packages/20/64/c5b0d93490634e41e38e2a15de5d54fdbd2c9f64a19abb0f95305b63373c/jiter-0.11.0-cp311-cp311-win32.whl", hash = "sha256:144fc21337d21b1d048f7f44bf70881e1586401d405ed3a98c95a114a9994982", size = 205140 }, + { url = "https://files.pythonhosted.org/packages/a1/e6/c347c0e6f5796e97d4356b7e5ff0ce336498b7f4ef848fae621a56f1ccf3/jiter-0.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:b0f32e644d241293b892b1a6dd8f0b9cc029bfd94c97376b2681c36548aabab7", size = 204311 }, + { url = "https://files.pythonhosted.org/packages/ba/b5/3009b112b8f673e568ef79af9863d8309a15f0a8cdcc06ed6092051f377e/jiter-0.11.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:2fb7b377688cc3850bbe5c192a6bd493562a0bc50cbc8b047316428fbae00ada", size = 305510 }, + { url = "https://files.pythonhosted.org/packages/fe/82/15514244e03b9e71e086bbe2a6de3e4616b48f07d5f834200c873956fb8c/jiter-0.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a1b7cbe3f25bd0d8abb468ba4302a5d45617ee61b2a7a638f63fee1dc086be99", size = 316521 }, + { url = "https://files.pythonhosted.org/packages/92/94/7a2e905f40ad2d6d660e00b68d818f9e29fb87ffe82774f06191e93cbe4a/jiter-0.11.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c0a7f0ec81d5b7588c5cade1eb1925b91436ae6726dc2df2348524aeabad5de6", size = 338214 }, + { url = "https://files.pythonhosted.org/packages/a8/9c/5791ed5bdc76f12110158d3316a7a3ec0b1413d018b41c5ed399549d3ad5/jiter-0.11.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07630bb46ea2a6b9c6ed986c6e17e35b26148cce2c535454b26ee3f0e8dcaba1", size = 361280 }, + { url = "https://files.pythonhosted.org/packages/d4/7f/b7d82d77ff0d2cb06424141000176b53a9e6b16a1125525bb51ea4990c2e/jiter-0.11.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7764f27d28cd4a9cbc61704dfcd80c903ce3aad106a37902d3270cd6673d17f4", size = 487895 }, + { url = "https://files.pythonhosted.org/packages/42/44/10a1475d46f1fc1fd5cc2e82c58e7bca0ce5852208e0fa5df2f949353321/jiter-0.11.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1d4a6c4a737d486f77f842aeb22807edecb4a9417e6700c7b981e16d34ba7c72", size = 378421 }, + { url = "https://files.pythonhosted.org/packages/9a/5f/0dc34563d8164d31d07bc09d141d3da08157a68dcd1f9b886fa4e917805b/jiter-0.11.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cf408d2a0abd919b60de8c2e7bc5eeab72d4dafd18784152acc7c9adc3291591", size = 347932 }, + { url = "https://files.pythonhosted.org/packages/f7/de/b68f32a4fcb7b4a682b37c73a0e5dae32180140cd1caf11aef6ad40ddbf2/jiter-0.11.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cdef53eda7d18e799625023e1e250dbc18fbc275153039b873ec74d7e8883e09", size = 386959 }, + { url = "https://files.pythonhosted.org/packages/76/0a/c08c92e713b6e28972a846a81ce374883dac2f78ec6f39a0dad9f2339c3a/jiter-0.11.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:53933a38ef7b551dd9c7f1064f9d7bb235bb3168d0fa5f14f0798d1b7ea0d9c5", size = 517187 }, + { url = "https://files.pythonhosted.org/packages/89/b5/4a283bec43b15aad54fcae18d951f06a2ec3f78db5708d3b59a48e9c3fbd/jiter-0.11.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:11840d2324c9ab5162fc1abba23bc922124fedcff0d7b7f85fffa291e2f69206", size = 509461 }, + { url = "https://files.pythonhosted.org/packages/34/a5/f8bad793010534ea73c985caaeef8cc22dfb1fedb15220ecdf15c623c07a/jiter-0.11.0-cp312-cp312-win32.whl", hash = "sha256:4f01a744d24a5f2bb4a11657a1b27b61dc038ae2e674621a74020406e08f749b", size = 206664 }, + { url = "https://files.pythonhosted.org/packages/ed/42/5823ec2b1469395a160b4bf5f14326b4a098f3b6898fbd327366789fa5d3/jiter-0.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:29fff31190ab3a26de026da2f187814f4b9c6695361e20a9ac2123e4d4378a4c", size = 203520 }, + { url = "https://files.pythonhosted.org/packages/97/c4/d530e514d0f4f29b2b68145e7b389cbc7cac7f9c8c23df43b04d3d10fa3e/jiter-0.11.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:4441a91b80a80249f9a6452c14b2c24708f139f64de959943dfeaa6cb915e8eb", size = 305021 }, + { url = "https://files.pythonhosted.org/packages/7a/77/796a19c567c5734cbfc736a6f987affc0d5f240af8e12063c0fb93990ffa/jiter-0.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ff85fc6d2a431251ad82dbd1ea953affb5a60376b62e7d6809c5cd058bb39471", size = 314384 }, + { url = "https://files.pythonhosted.org/packages/14/9c/824334de0b037b91b6f3fa9fe5a191c83977c7ec4abe17795d3cb6d174cf/jiter-0.11.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5e86126d64706fd28dfc46f910d496923c6f95b395138c02d0e252947f452bd", size = 337389 }, + { url = "https://files.pythonhosted.org/packages/a2/95/ed4feab69e6cf9b2176ea29d4ef9d01a01db210a3a2c8a31a44ecdc68c38/jiter-0.11.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4ad8bd82165961867a10f52010590ce0b7a8c53da5ddd8bbb62fef68c181b921", size = 360519 }, + { url = "https://files.pythonhosted.org/packages/b5/0c/2ad00f38d3e583caba3909d95b7da1c3a7cd82c0aa81ff4317a8016fb581/jiter-0.11.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b42c2cd74273455ce439fd9528db0c6e84b5623cb74572305bdd9f2f2961d3df", size = 487198 }, + { url = "https://files.pythonhosted.org/packages/ea/8b/919b64cf3499b79bdfba6036da7b0cac5d62d5c75a28fb45bad7819e22f0/jiter-0.11.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f0062dab98172dd0599fcdbf90214d0dcde070b1ff38a00cc1b90e111f071982", size = 377835 }, + { url = "https://files.pythonhosted.org/packages/29/7f/8ebe15b6e0a8026b0d286c083b553779b4dd63db35b43a3f171b544de91d/jiter-0.11.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb948402821bc76d1f6ef0f9e19b816f9b09f8577844ba7140f0b6afe994bc64", size = 347655 }, + { url = "https://files.pythonhosted.org/packages/8e/64/332127cef7e94ac75719dda07b9a472af6158ba819088d87f17f3226a769/jiter-0.11.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:25a5b1110cca7329fd0daf5060faa1234be5c11e988948e4f1a1923b6a457fe1", size = 386135 }, + { url = "https://files.pythonhosted.org/packages/20/c8/557b63527442f84c14774159948262a9d4fabb0d61166f11568f22fc60d2/jiter-0.11.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:bf11807e802a214daf6c485037778843fadd3e2ec29377ae17e0706ec1a25758", size = 516063 }, + { url = "https://files.pythonhosted.org/packages/86/13/4164c819df4a43cdc8047f9a42880f0ceef5afeb22e8b9675c0528ebdccd/jiter-0.11.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:dbb57da40631c267861dd0090461222060960012d70fd6e4c799b0f62d0ba166", size = 508139 }, + { url = "https://files.pythonhosted.org/packages/fa/70/6e06929b401b331d41ddb4afb9f91cd1168218e3371972f0afa51c9f3c31/jiter-0.11.0-cp313-cp313-win32.whl", hash = "sha256:8e36924dad32c48d3c5e188d169e71dc6e84d6cb8dedefea089de5739d1d2f80", size = 206369 }, + { url = "https://files.pythonhosted.org/packages/f4/0d/8185b8e15de6dce24f6afae63380e16377dd75686d56007baa4f29723ea1/jiter-0.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:452d13e4fd59698408087235259cebe67d9d49173b4dacb3e8d35ce4acf385d6", size = 202538 }, + { url = "https://files.pythonhosted.org/packages/13/3a/d61707803260d59520721fa326babfae25e9573a88d8b7b9cb54c5423a59/jiter-0.11.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:089f9df9f69532d1339e83142438668f52c97cd22ee2d1195551c2b1a9e6cf33", size = 313737 }, + { url = "https://files.pythonhosted.org/packages/cd/cc/c9f0eec5d00f2a1da89f6bdfac12b8afdf8d5ad974184863c75060026457/jiter-0.11.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:29ed1fe69a8c69bf0f2a962d8d706c7b89b50f1332cd6b9fbda014f60bd03a03", size = 346183 }, + { url = "https://files.pythonhosted.org/packages/a6/87/fc632776344e7aabbab05a95a0075476f418c5d29ab0f2eec672b7a1f0ac/jiter-0.11.0-cp313-cp313t-win_amd64.whl", hash = "sha256:a4d71d7ea6ea8786291423fe209acf6f8d398a0759d03e7f24094acb8ab686ba", size = 204225 }, + { url = "https://files.pythonhosted.org/packages/ee/3b/e7f45be7d3969bdf2e3cd4b816a7a1d272507cd0edd2d6dc4b07514f2d9a/jiter-0.11.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:9a6dff27eca70930bdbe4cbb7c1a4ba8526e13b63dc808c0670083d2d51a4a72", size = 304414 }, + { url = "https://files.pythonhosted.org/packages/06/32/13e8e0d152631fcc1907ceb4943711471be70496d14888ec6e92034e2caf/jiter-0.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b1ae2a7593a62132c7d4c2abbee80bbbb94fdc6d157e2c6cc966250c564ef774", size = 314223 }, + { url = "https://files.pythonhosted.org/packages/0c/7e/abedd5b5a20ca083f778d96bba0d2366567fcecb0e6e34ff42640d5d7a18/jiter-0.11.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b13a431dba4b059e9e43019d3022346d009baf5066c24dcdea321a303cde9f0", size = 337306 }, + { url = "https://files.pythonhosted.org/packages/ac/e2/30d59bdc1204c86aa975ec72c48c482fee6633120ee9c3ab755e4dfefea8/jiter-0.11.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:af62e84ca3889604ebb645df3b0a3f3bcf6b92babbff642bd214616f57abb93a", size = 360565 }, + { url = "https://files.pythonhosted.org/packages/fe/88/567288e0d2ed9fa8f7a3b425fdaf2cb82b998633c24fe0d98f5417321aa8/jiter-0.11.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c6f3b32bb723246e6b351aecace52aba78adb8eeb4b2391630322dc30ff6c773", size = 486465 }, + { url = "https://files.pythonhosted.org/packages/18/6e/7b72d09273214cadd15970e91dd5ed9634bee605176107db21e1e4205eb1/jiter-0.11.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:adcab442f4a099a358a7f562eaa54ed6456fb866e922c6545a717be51dbed7d7", size = 377581 }, + { url = "https://files.pythonhosted.org/packages/58/52/4db456319f9d14deed325f70102577492e9d7e87cf7097bda9769a1fcacb/jiter-0.11.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9967c2ab338ee2b2c0102fd379ec2693c496abf71ffd47e4d791d1f593b68e2", size = 347102 }, + { url = "https://files.pythonhosted.org/packages/ce/b4/433d5703c38b26083aec7a733eb5be96f9c6085d0e270a87ca6482cbf049/jiter-0.11.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e7d0bed3b187af8b47a981d9742ddfc1d9b252a7235471ad6078e7e4e5fe75c2", size = 386477 }, + { url = "https://files.pythonhosted.org/packages/c8/7a/a60bfd9c55b55b07c5c441c5085f06420b6d493ce9db28d069cc5b45d9f3/jiter-0.11.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:f6fe0283e903ebc55f1a6cc569b8c1f3bf4abd026fed85e3ff8598a9e6f982f0", size = 516004 }, + { url = "https://files.pythonhosted.org/packages/2e/46/f8363e5ecc179b4ed0ca6cb0a6d3bfc266078578c71ff30642ea2ce2f203/jiter-0.11.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:4ee5821e3d66606b29ae5b497230b304f1376f38137d69e35f8d2bd5f310ff73", size = 507855 }, + { url = "https://files.pythonhosted.org/packages/90/33/396083357d51d7ff0f9805852c288af47480d30dd31d8abc74909b020761/jiter-0.11.0-cp314-cp314-win32.whl", hash = "sha256:c2d13ba7567ca8799f17c76ed56b1d49be30df996eb7fa33e46b62800562a5e2", size = 205802 }, + { url = "https://files.pythonhosted.org/packages/e7/ab/eb06ca556b2551d41de7d03bf2ee24285fa3d0c58c5f8d95c64c9c3281b1/jiter-0.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:fb4790497369d134a07fc763cc88888c46f734abdd66f9fdf7865038bf3a8f40", size = 313405 }, + { url = "https://files.pythonhosted.org/packages/af/22/7ab7b4ec3a1c1f03aef376af11d23b05abcca3fb31fbca1e7557053b1ba2/jiter-0.11.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e2bbf24f16ba5ad4441a9845e40e4ea0cb9eed00e76ba94050664ef53ef4406", size = 347102 }, ] [[package]] @@ -1856,9 +1873,9 @@ dependencies = [ { name = "starlette", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "uvicorn", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/95/fd/d6e941a52446198b73e5e4a953441f667f1469aeb06fb382d9f6729d6168/mcp-1.14.0.tar.gz", hash = "sha256:2e7d98b195e08b2abc1dc6191f6f3dc0059604ac13ee6a40f88676274787fac4", size = 454855, upload-time = "2025-09-11T17:40:48.667Z" } +sdist = { url = "https://files.pythonhosted.org/packages/95/fd/d6e941a52446198b73e5e4a953441f667f1469aeb06fb382d9f6729d6168/mcp-1.14.0.tar.gz", hash = "sha256:2e7d98b195e08b2abc1dc6191f6f3dc0059604ac13ee6a40f88676274787fac4", size = 454855 } wheels = [ - { url = "https://files.pythonhosted.org/packages/04/7b/84b0dd4c2c5a499d2c5d63fb7a1224c25fc4c8b6c24623fa7a566471480d/mcp-1.14.0-py3-none-any.whl", hash = "sha256:b2d27feba27b4c53d41b58aa7f4d090ae0cb740cbc4e339af10f8cbe54c4e19d", size = 163805, upload-time = "2025-09-11T17:40:46.891Z" }, + { url = "https://files.pythonhosted.org/packages/04/7b/84b0dd4c2c5a499d2c5d63fb7a1224c25fc4c8b6c24623fa7a566471480d/mcp-1.14.0-py3-none-any.whl", hash = "sha256:b2d27feba27b4c53d41b58aa7f4d090ae0cb740cbc4e339af10f8cbe54c4e19d", size = 163805 }, ] [package.optional-dependencies] @@ -1905,6 +1922,46 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5b/f4/7fb9974bb1f81afb363a6b5a74f64609bf8e021d0cfe486909c6db6a8f3b/mem0ai-0.1.117-py3-none-any.whl", hash = "sha256:12159d6b9fef22a155f9b8b305999c52dfad20a46e7ccbced68677b6f48d4b57", size = 213063, upload-time = "2025-09-03T17:44:10.61Z" }, ] +[[package]] +name = "microsoft-agents-activity" +version = "0.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5a/6a/dfc2fc0316b7dc4f6d24792b4a31a873b026be76792af1e0c3e65f843ef0/microsoft_agents_activity-0.3.1.tar.gz", hash = "sha256:c7567fc30f8e6f2a2d74cd65a1f7f31ade0d7ec9dd94531677d0d7b0648c77ee", size = 44886 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/8b/50ce2243e2900e94358f37009121145bb8224a388d95d704856aa2686667/microsoft_agents_activity-0.3.1-py3-none-any.whl", hash = "sha256:d7fc2e9cf2843ec8d6d42608b808b159a12cbb61e1fc7d7b1aaf29899f20746a", size = 111904 }, +] + +[[package]] +name = "microsoft-agents-copilotstudio-client" +version = "0.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "microsoft-agents-hosting-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/a5/2381ffd14d6a584f9f7ab80c7b6c634f658ea651b38702eb403c930d8396/microsoft_agents_copilotstudio_client-0.3.1.tar.gz", hash = "sha256:c529209241c9d11b7a6e8696f96a3d43121c10b49e44f00e5066f9cf5256f4f3", size = 5024 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/97/35/8b4e9c691f2ce89653007f358519bbadff1fe0d495c3723c9dbbfa962a33/microsoft_agents_copilotstudio_client-0.3.1-py3-none-any.whl", hash = "sha256:cac7485405325b990202452c9c14848cbdb25d13e6cdaf7bd3eca3a5c1fb3989", size = 7420 }, +] + +[[package]] +name = "microsoft-agents-hosting-core" +version = "0.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "azure-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "isodate", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "microsoft-agents-activity", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "pyjwt", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "python-dotenv", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6a/14/a1365e0bab1486c2d16aabeb192ca90715794edf4e68be4815c245884420/microsoft_agents_hosting_core-0.3.1.tar.gz", hash = "sha256:0b76bda10e7a54ff3c86e56cbabaad5ac7a4c2a076c9833af3b2f4c86fa85e89", size = 81137 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f0/1b/543ddaa2daf8593911a02a07a6a78366d4a6a0053ec86a557c19fa97b60e/microsoft_agents_hosting_core-0.3.1-py3-none-any.whl", hash = "sha256:a4b41556b15321b74f539c5a0a89f70955459b7ec57e9e4b24e61bba27f1cbbc", size = 94573 }, +] + [[package]] name = "msal" version = "1.33.0" @@ -2059,39 +2116,39 @@ dependencies = [ { name = "tomli", marker = "(python_full_version < '3.11' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform == 'win32')" }, { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/14/a3/931e09fc02d7ba96da65266884da4e4a8806adcdb8a57faaacc6edf1d538/mypy-1.18.1.tar.gz", hash = "sha256:9e988c64ad3ac5987f43f5154f884747faf62141b7f842e87465b45299eea5a9", size = 3448447, upload-time = "2025-09-11T23:00:47.067Z" } +sdist = { url = "https://files.pythonhosted.org/packages/14/a3/931e09fc02d7ba96da65266884da4e4a8806adcdb8a57faaacc6edf1d538/mypy-1.18.1.tar.gz", hash = "sha256:9e988c64ad3ac5987f43f5154f884747faf62141b7f842e87465b45299eea5a9", size = 3448447 } wheels = [ - { url = "https://files.pythonhosted.org/packages/fc/06/29ea5a34c23938ae93bc0040eb2900eb3f0f2ef4448cc59af37ab3ddae73/mypy-1.18.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2761b6ae22a2b7d8e8607fb9b81ae90bc2e95ec033fd18fa35e807af6c657763", size = 12811535, upload-time = "2025-09-11T22:58:55.399Z" }, - { url = "https://files.pythonhosted.org/packages/a8/40/04c38cb04fa9f1dc224b3e9634021a92c47b1569f1c87dfe6e63168883bb/mypy-1.18.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5b10e3ea7f2eec23b4929a3fabf84505da21034a4f4b9613cda81217e92b74f3", size = 11897559, upload-time = "2025-09-11T22:59:48.041Z" }, - { url = "https://files.pythonhosted.org/packages/46/bf/4c535bd45ea86cebbc1a3b6a781d442f53a4883f322ebd2d442db6444d0b/mypy-1.18.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:261fbfced030228bc0f724d5d92f9ae69f46373bdfd0e04a533852677a11dbea", size = 12507430, upload-time = "2025-09-11T22:59:30.415Z" }, - { url = "https://files.pythonhosted.org/packages/e2/e1/cbefb16f2be078d09e28e0b9844e981afb41f6ffc85beb68b86c6976e641/mypy-1.18.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4dc6b34a1c6875e6286e27d836a35c0d04e8316beac4482d42cfea7ed2527df8", size = 13243717, upload-time = "2025-09-11T22:59:11.297Z" }, - { url = "https://files.pythonhosted.org/packages/65/e8/3e963da63176f16ca9caea7fa48f1bc8766de317cd961528c0391565fd47/mypy-1.18.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1cabb353194d2942522546501c0ff75c4043bf3b63069cb43274491b44b773c9", size = 13492052, upload-time = "2025-09-11T23:00:09.29Z" }, - { url = "https://files.pythonhosted.org/packages/4b/09/d5d70c252a3b5b7530662d145437bd1de15f39fa0b48a27ee4e57d254aa1/mypy-1.18.1-cp310-cp310-win_amd64.whl", hash = "sha256:738b171690c8e47c93569635ee8ec633d2cdb06062f510b853b5f233020569a9", size = 9765846, upload-time = "2025-09-11T22:58:26.198Z" }, - { url = "https://files.pythonhosted.org/packages/32/28/47709d5d9e7068b26c0d5189c8137c8783e81065ad1102b505214a08b548/mypy-1.18.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6c903857b3e28fc5489e54042684a9509039ea0aedb2a619469438b544ae1961", size = 12734635, upload-time = "2025-09-11T23:00:24.983Z" }, - { url = "https://files.pythonhosted.org/packages/7c/12/ee5c243e52497d0e59316854041cf3b3130131b92266d0764aca4dec3c00/mypy-1.18.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2a0c8392c19934c2b6c65566d3a6abdc6b51d5da7f5d04e43f0eb627d6eeee65", size = 11817287, upload-time = "2025-09-11T22:59:07.38Z" }, - { url = "https://files.pythonhosted.org/packages/48/bd/2aeb950151005fe708ab59725afed7c4aeeb96daf844f86a05d4b8ac34f8/mypy-1.18.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f85eb7efa2ec73ef63fc23b8af89c2fe5bf2a4ad985ed2d3ff28c1bb3c317c92", size = 12430464, upload-time = "2025-09-11T22:58:48.084Z" }, - { url = "https://files.pythonhosted.org/packages/71/e8/7a20407aafb488acb5734ad7fb5e8c2ef78d292ca2674335350fa8ebef67/mypy-1.18.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:82ace21edf7ba8af31c3308a61dc72df30500f4dbb26f99ac36b4b80809d7e94", size = 13164555, upload-time = "2025-09-11T23:00:13.803Z" }, - { url = "https://files.pythonhosted.org/packages/e8/c9/5f39065252e033b60f397096f538fb57c1d9fd70a7a490f314df20dd9d64/mypy-1.18.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a2dfd53dfe632f1ef5d161150a4b1f2d0786746ae02950eb3ac108964ee2975a", size = 13359222, upload-time = "2025-09-11T23:00:33.469Z" }, - { url = "https://files.pythonhosted.org/packages/85/b6/d54111ef3c1e55992cd2ec9b8b6ce9c72a407423e93132cae209f7e7ba60/mypy-1.18.1-cp311-cp311-win_amd64.whl", hash = "sha256:320f0ad4205eefcb0e1a72428dde0ad10be73da9f92e793c36228e8ebf7298c0", size = 9760441, upload-time = "2025-09-11T23:00:44.826Z" }, - { url = "https://files.pythonhosted.org/packages/e7/14/1c3f54d606cb88a55d1567153ef3a8bc7b74702f2ff5eb64d0994f9e49cb/mypy-1.18.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:502cde8896be8e638588b90fdcb4c5d5b8c1b004dfc63fd5604a973547367bb9", size = 12911082, upload-time = "2025-09-11T23:00:41.465Z" }, - { url = "https://files.pythonhosted.org/packages/90/83/235606c8b6d50a8eba99773add907ce1d41c068edb523f81eb0d01603a83/mypy-1.18.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7509549b5e41be279afc1228242d0e397f1af2919a8f2877ad542b199dc4083e", size = 11919107, upload-time = "2025-09-11T22:58:40.903Z" }, - { url = "https://files.pythonhosted.org/packages/ca/25/4e2ce00f8d15b99d0c68a2536ad63e9eac033f723439ef80290ec32c1ff5/mypy-1.18.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5956ecaabb3a245e3f34100172abca1507be687377fe20e24d6a7557e07080e2", size = 12472551, upload-time = "2025-09-11T22:58:37.272Z" }, - { url = "https://files.pythonhosted.org/packages/32/bb/92642a9350fc339dd9dcefcf6862d171b52294af107d521dce075f32f298/mypy-1.18.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8750ceb014a96c9890421c83f0db53b0f3b8633e2864c6f9bc0a8e93951ed18d", size = 13340554, upload-time = "2025-09-11T22:59:38.756Z" }, - { url = "https://files.pythonhosted.org/packages/cd/ee/38d01db91c198fb6350025d28f9719ecf3c8f2c55a0094bfbf3ef478cc9a/mypy-1.18.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fb89ea08ff41adf59476b235293679a6eb53a7b9400f6256272fb6029bec3ce5", size = 13530933, upload-time = "2025-09-11T22:59:20.228Z" }, - { url = "https://files.pythonhosted.org/packages/da/8d/6d991ae631f80d58edbf9d7066e3f2a96e479dca955d9a968cd6e90850a3/mypy-1.18.1-cp312-cp312-win_amd64.whl", hash = "sha256:2657654d82fcd2a87e02a33e0d23001789a554059bbf34702d623dafe353eabf", size = 9828426, upload-time = "2025-09-11T23:00:21.007Z" }, - { url = "https://files.pythonhosted.org/packages/e4/ec/ef4a7260e1460a3071628a9277a7579e7da1b071bc134ebe909323f2fbc7/mypy-1.18.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d70d2b5baf9b9a20bc9c730015615ae3243ef47fb4a58ad7b31c3e0a59b5ef1f", size = 12918671, upload-time = "2025-09-11T22:58:29.814Z" }, - { url = "https://files.pythonhosted.org/packages/a1/82/0ea6c3953f16223f0b8eda40c1aeac6bd266d15f4902556ae6e91f6fca4c/mypy-1.18.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b8367e33506300f07a43012fc546402f283c3f8bcff1dc338636affb710154ce", size = 11913023, upload-time = "2025-09-11T23:00:29.049Z" }, - { url = "https://files.pythonhosted.org/packages/ae/ef/5e2057e692c2690fc27b3ed0a4dbde4388330c32e2576a23f0302bc8358d/mypy-1.18.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:913f668ec50c3337b89df22f973c1c8f0b29ee9e290a8b7fe01cc1ef7446d42e", size = 12473355, upload-time = "2025-09-11T23:00:04.544Z" }, - { url = "https://files.pythonhosted.org/packages/98/43/b7e429fc4be10e390a167b0cd1810d41cb4e4add4ae50bab96faff695a3b/mypy-1.18.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a0e70b87eb27b33209fa4792b051c6947976f6ab829daa83819df5f58330c71", size = 13346944, upload-time = "2025-09-11T22:58:23.024Z" }, - { url = "https://files.pythonhosted.org/packages/89/4e/899dba0bfe36bbd5b7c52e597de4cf47b5053d337b6d201a30e3798e77a6/mypy-1.18.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c378d946e8a60be6b6ede48c878d145546fb42aad61df998c056ec151bf6c746", size = 13512574, upload-time = "2025-09-11T22:59:52.152Z" }, - { url = "https://files.pythonhosted.org/packages/f5/f8/7661021a5b0e501b76440454d786b0f01bb05d5c4b125fcbda02023d0250/mypy-1.18.1-cp313-cp313-win_amd64.whl", hash = "sha256:2cd2c1e0f3a7465f22731987fff6fc427e3dcbb4ca5f7db5bbeaff2ff9a31f6d", size = 9837684, upload-time = "2025-09-11T22:58:44.454Z" }, - { url = "https://files.pythonhosted.org/packages/bf/87/7b173981466219eccc64c107cf8e5ab9eb39cc304b4c07df8e7881533e4f/mypy-1.18.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ba24603c58e34dd5b096dfad792d87b304fc6470cbb1c22fd64e7ebd17edcc61", size = 12900265, upload-time = "2025-09-11T22:59:03.4Z" }, - { url = "https://files.pythonhosted.org/packages/ae/cc/b10e65bae75b18a5ac8f81b1e8e5867677e418f0dd2c83b8e2de9ba96ebd/mypy-1.18.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ed36662fb92ae4cb3cacc682ec6656208f323bbc23d4b08d091eecfc0863d4b5", size = 11942890, upload-time = "2025-09-11T23:00:00.607Z" }, - { url = "https://files.pythonhosted.org/packages/39/d4/aeefa07c44d09f4c2102e525e2031bc066d12e5351f66b8a83719671004d/mypy-1.18.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:040ecc95e026f71a9ad7956fea2724466602b561e6a25c2e5584160d3833aaa8", size = 12472291, upload-time = "2025-09-11T22:59:43.425Z" }, - { url = "https://files.pythonhosted.org/packages/c6/07/711e78668ff8e365f8c19735594ea95938bff3639a4c46a905e3ed8ff2d6/mypy-1.18.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:937e3ed86cb731276706e46e03512547e43c391a13f363e08d0fee49a7c38a0d", size = 13318610, upload-time = "2025-09-11T23:00:17.604Z" }, - { url = "https://files.pythonhosted.org/packages/ca/85/df3b2d39339c31d360ce299b418c55e8194ef3205284739b64962f6074e7/mypy-1.18.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1f95cc4f01c0f1701ca3b0355792bccec13ecb2ec1c469e5b85a6ef398398b1d", size = 13513697, upload-time = "2025-09-11T22:58:59.534Z" }, - { url = "https://files.pythonhosted.org/packages/b1/df/462866163c99ea73bb28f0eb4d415c087e30de5d36ee0f5429d42e28689b/mypy-1.18.1-cp314-cp314-win_amd64.whl", hash = "sha256:e4f16c0019d48941220ac60b893615be2f63afedaba6a0801bdcd041b96991ce", size = 9985739, upload-time = "2025-09-11T22:58:51.644Z" }, - { url = "https://files.pythonhosted.org/packages/e0/1d/4b97d3089b48ef3d904c9ca69fab044475bd03245d878f5f0b3ea1daf7ce/mypy-1.18.1-py3-none-any.whl", hash = "sha256:b76a4de66a0ac01da1be14ecc8ae88ddea33b8380284a9e3eae39d57ebcbe26e", size = 2352212, upload-time = "2025-09-11T22:59:26.576Z" }, + { url = "https://files.pythonhosted.org/packages/fc/06/29ea5a34c23938ae93bc0040eb2900eb3f0f2ef4448cc59af37ab3ddae73/mypy-1.18.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2761b6ae22a2b7d8e8607fb9b81ae90bc2e95ec033fd18fa35e807af6c657763", size = 12811535 }, + { url = "https://files.pythonhosted.org/packages/a8/40/04c38cb04fa9f1dc224b3e9634021a92c47b1569f1c87dfe6e63168883bb/mypy-1.18.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5b10e3ea7f2eec23b4929a3fabf84505da21034a4f4b9613cda81217e92b74f3", size = 11897559 }, + { url = "https://files.pythonhosted.org/packages/46/bf/4c535bd45ea86cebbc1a3b6a781d442f53a4883f322ebd2d442db6444d0b/mypy-1.18.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:261fbfced030228bc0f724d5d92f9ae69f46373bdfd0e04a533852677a11dbea", size = 12507430 }, + { url = "https://files.pythonhosted.org/packages/e2/e1/cbefb16f2be078d09e28e0b9844e981afb41f6ffc85beb68b86c6976e641/mypy-1.18.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4dc6b34a1c6875e6286e27d836a35c0d04e8316beac4482d42cfea7ed2527df8", size = 13243717 }, + { url = "https://files.pythonhosted.org/packages/65/e8/3e963da63176f16ca9caea7fa48f1bc8766de317cd961528c0391565fd47/mypy-1.18.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1cabb353194d2942522546501c0ff75c4043bf3b63069cb43274491b44b773c9", size = 13492052 }, + { url = "https://files.pythonhosted.org/packages/4b/09/d5d70c252a3b5b7530662d145437bd1de15f39fa0b48a27ee4e57d254aa1/mypy-1.18.1-cp310-cp310-win_amd64.whl", hash = "sha256:738b171690c8e47c93569635ee8ec633d2cdb06062f510b853b5f233020569a9", size = 9765846 }, + { url = "https://files.pythonhosted.org/packages/32/28/47709d5d9e7068b26c0d5189c8137c8783e81065ad1102b505214a08b548/mypy-1.18.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6c903857b3e28fc5489e54042684a9509039ea0aedb2a619469438b544ae1961", size = 12734635 }, + { url = "https://files.pythonhosted.org/packages/7c/12/ee5c243e52497d0e59316854041cf3b3130131b92266d0764aca4dec3c00/mypy-1.18.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2a0c8392c19934c2b6c65566d3a6abdc6b51d5da7f5d04e43f0eb627d6eeee65", size = 11817287 }, + { url = "https://files.pythonhosted.org/packages/48/bd/2aeb950151005fe708ab59725afed7c4aeeb96daf844f86a05d4b8ac34f8/mypy-1.18.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f85eb7efa2ec73ef63fc23b8af89c2fe5bf2a4ad985ed2d3ff28c1bb3c317c92", size = 12430464 }, + { url = "https://files.pythonhosted.org/packages/71/e8/7a20407aafb488acb5734ad7fb5e8c2ef78d292ca2674335350fa8ebef67/mypy-1.18.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:82ace21edf7ba8af31c3308a61dc72df30500f4dbb26f99ac36b4b80809d7e94", size = 13164555 }, + { url = "https://files.pythonhosted.org/packages/e8/c9/5f39065252e033b60f397096f538fb57c1d9fd70a7a490f314df20dd9d64/mypy-1.18.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a2dfd53dfe632f1ef5d161150a4b1f2d0786746ae02950eb3ac108964ee2975a", size = 13359222 }, + { url = "https://files.pythonhosted.org/packages/85/b6/d54111ef3c1e55992cd2ec9b8b6ce9c72a407423e93132cae209f7e7ba60/mypy-1.18.1-cp311-cp311-win_amd64.whl", hash = "sha256:320f0ad4205eefcb0e1a72428dde0ad10be73da9f92e793c36228e8ebf7298c0", size = 9760441 }, + { url = "https://files.pythonhosted.org/packages/e7/14/1c3f54d606cb88a55d1567153ef3a8bc7b74702f2ff5eb64d0994f9e49cb/mypy-1.18.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:502cde8896be8e638588b90fdcb4c5d5b8c1b004dfc63fd5604a973547367bb9", size = 12911082 }, + { url = "https://files.pythonhosted.org/packages/90/83/235606c8b6d50a8eba99773add907ce1d41c068edb523f81eb0d01603a83/mypy-1.18.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7509549b5e41be279afc1228242d0e397f1af2919a8f2877ad542b199dc4083e", size = 11919107 }, + { url = "https://files.pythonhosted.org/packages/ca/25/4e2ce00f8d15b99d0c68a2536ad63e9eac033f723439ef80290ec32c1ff5/mypy-1.18.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5956ecaabb3a245e3f34100172abca1507be687377fe20e24d6a7557e07080e2", size = 12472551 }, + { url = "https://files.pythonhosted.org/packages/32/bb/92642a9350fc339dd9dcefcf6862d171b52294af107d521dce075f32f298/mypy-1.18.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8750ceb014a96c9890421c83f0db53b0f3b8633e2864c6f9bc0a8e93951ed18d", size = 13340554 }, + { url = "https://files.pythonhosted.org/packages/cd/ee/38d01db91c198fb6350025d28f9719ecf3c8f2c55a0094bfbf3ef478cc9a/mypy-1.18.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fb89ea08ff41adf59476b235293679a6eb53a7b9400f6256272fb6029bec3ce5", size = 13530933 }, + { url = "https://files.pythonhosted.org/packages/da/8d/6d991ae631f80d58edbf9d7066e3f2a96e479dca955d9a968cd6e90850a3/mypy-1.18.1-cp312-cp312-win_amd64.whl", hash = "sha256:2657654d82fcd2a87e02a33e0d23001789a554059bbf34702d623dafe353eabf", size = 9828426 }, + { url = "https://files.pythonhosted.org/packages/e4/ec/ef4a7260e1460a3071628a9277a7579e7da1b071bc134ebe909323f2fbc7/mypy-1.18.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d70d2b5baf9b9a20bc9c730015615ae3243ef47fb4a58ad7b31c3e0a59b5ef1f", size = 12918671 }, + { url = "https://files.pythonhosted.org/packages/a1/82/0ea6c3953f16223f0b8eda40c1aeac6bd266d15f4902556ae6e91f6fca4c/mypy-1.18.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b8367e33506300f07a43012fc546402f283c3f8bcff1dc338636affb710154ce", size = 11913023 }, + { url = "https://files.pythonhosted.org/packages/ae/ef/5e2057e692c2690fc27b3ed0a4dbde4388330c32e2576a23f0302bc8358d/mypy-1.18.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:913f668ec50c3337b89df22f973c1c8f0b29ee9e290a8b7fe01cc1ef7446d42e", size = 12473355 }, + { url = "https://files.pythonhosted.org/packages/98/43/b7e429fc4be10e390a167b0cd1810d41cb4e4add4ae50bab96faff695a3b/mypy-1.18.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a0e70b87eb27b33209fa4792b051c6947976f6ab829daa83819df5f58330c71", size = 13346944 }, + { url = "https://files.pythonhosted.org/packages/89/4e/899dba0bfe36bbd5b7c52e597de4cf47b5053d337b6d201a30e3798e77a6/mypy-1.18.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c378d946e8a60be6b6ede48c878d145546fb42aad61df998c056ec151bf6c746", size = 13512574 }, + { url = "https://files.pythonhosted.org/packages/f5/f8/7661021a5b0e501b76440454d786b0f01bb05d5c4b125fcbda02023d0250/mypy-1.18.1-cp313-cp313-win_amd64.whl", hash = "sha256:2cd2c1e0f3a7465f22731987fff6fc427e3dcbb4ca5f7db5bbeaff2ff9a31f6d", size = 9837684 }, + { url = "https://files.pythonhosted.org/packages/bf/87/7b173981466219eccc64c107cf8e5ab9eb39cc304b4c07df8e7881533e4f/mypy-1.18.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ba24603c58e34dd5b096dfad792d87b304fc6470cbb1c22fd64e7ebd17edcc61", size = 12900265 }, + { url = "https://files.pythonhosted.org/packages/ae/cc/b10e65bae75b18a5ac8f81b1e8e5867677e418f0dd2c83b8e2de9ba96ebd/mypy-1.18.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ed36662fb92ae4cb3cacc682ec6656208f323bbc23d4b08d091eecfc0863d4b5", size = 11942890 }, + { url = "https://files.pythonhosted.org/packages/39/d4/aeefa07c44d09f4c2102e525e2031bc066d12e5351f66b8a83719671004d/mypy-1.18.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:040ecc95e026f71a9ad7956fea2724466602b561e6a25c2e5584160d3833aaa8", size = 12472291 }, + { url = "https://files.pythonhosted.org/packages/c6/07/711e78668ff8e365f8c19735594ea95938bff3639a4c46a905e3ed8ff2d6/mypy-1.18.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:937e3ed86cb731276706e46e03512547e43c391a13f363e08d0fee49a7c38a0d", size = 13318610 }, + { url = "https://files.pythonhosted.org/packages/ca/85/df3b2d39339c31d360ce299b418c55e8194ef3205284739b64962f6074e7/mypy-1.18.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1f95cc4f01c0f1701ca3b0355792bccec13ecb2ec1c469e5b85a6ef398398b1d", size = 13513697 }, + { url = "https://files.pythonhosted.org/packages/b1/df/462866163c99ea73bb28f0eb4d415c087e30de5d36ee0f5429d42e28689b/mypy-1.18.1-cp314-cp314-win_amd64.whl", hash = "sha256:e4f16c0019d48941220ac60b893615be2f63afedaba6a0801bdcd041b96991ce", size = 9985739 }, + { url = "https://files.pythonhosted.org/packages/e0/1d/4b97d3089b48ef3d904c9ca69fab044475bd03245d878f5f0b3ea1daf7ce/mypy-1.18.1-py3-none-any.whl", hash = "sha256:b76a4de66a0ac01da1be14ecc8ae88ddea33b8380284a9e3eae39d57ebcbe26e", size = 2352212 }, ] [[package]] @@ -2527,9 +2584,9 @@ dependencies = [ { name = "opentelemetry-semantic-conventions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "opentelemetry-semantic-conventions-ai", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/76/46/3e59891b433869c731131b5ececdaa3ee1d4a45d19169299b52d1397c8e5/opentelemetry_instrumentation_openai-0.47.1.tar.gz", hash = "sha256:a9ad8d898f8f03581fff1764b605d2c277381ced3083a66e7bddb951b95afb2a", size = 25411, upload-time = "2025-09-14T12:09:19.202Z" } +sdist = { url = "https://files.pythonhosted.org/packages/76/46/3e59891b433869c731131b5ececdaa3ee1d4a45d19169299b52d1397c8e5/opentelemetry_instrumentation_openai-0.47.1.tar.gz", hash = "sha256:a9ad8d898f8f03581fff1764b605d2c277381ced3083a66e7bddb951b95afb2a", size = 25411 } wheels = [ - { url = "https://files.pythonhosted.org/packages/af/4e/ee98a8e9b9d58425ff84aff63d06fdf004727cdac2cfe3a2916a1869a2e2/opentelemetry_instrumentation_openai-0.47.1-py3-none-any.whl", hash = "sha256:2bc426c1324f7e9babee8d2a02d7966562ec993da5d280c597bd29a92997e2ed", size = 35274, upload-time = "2025-09-14T12:08:48.987Z" }, + { url = "https://files.pythonhosted.org/packages/af/4e/ee98a8e9b9d58425ff84aff63d06fdf004727cdac2cfe3a2916a1869a2e2/opentelemetry_instrumentation_openai-0.47.1-py3-none-any.whl", hash = "sha256:2bc426c1324f7e9babee8d2a02d7966562ec993da5d280c597bd29a92997e2ed", size = 35274 }, ] [[package]] @@ -2968,9 +3025,9 @@ dependencies = [ { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "typing-inspection", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ff/5d/09a551ba512d7ca404d785072700d3f6727a02f6f3c24ecfd081c7cf0aa8/pydantic-2.11.9.tar.gz", hash = "sha256:6b8ffda597a14812a7975c90b82a8a2e777d9257aba3453f973acd3c032a18e2", size = 788495, upload-time = "2025-09-13T11:26:39.325Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ff/5d/09a551ba512d7ca404d785072700d3f6727a02f6f3c24ecfd081c7cf0aa8/pydantic-2.11.9.tar.gz", hash = "sha256:6b8ffda597a14812a7975c90b82a8a2e777d9257aba3453f973acd3c032a18e2", size = 788495 } wheels = [ - { url = "https://files.pythonhosted.org/packages/3e/d3/108f2006987c58e76691d5ae5d200dd3e0f532cb4e5fa3560751c3a1feba/pydantic-2.11.9-py3-none-any.whl", hash = "sha256:c42dd626f5cfc1c6950ce6205ea58c93efa406da65f479dcb4029d5934857da2", size = 444855, upload-time = "2025-09-13T11:26:36.909Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d3/108f2006987c58e76691d5ae5d200dd3e0f532cb4e5fa3560751c3a1feba/pydantic-2.11.9-py3-none-any.whl", hash = "sha256:c42dd626f5cfc1c6950ce6205ea58c93efa406da65f479dcb4029d5934857da2", size = 444855 }, ] [[package]] @@ -3088,9 +3145,9 @@ dependencies = [ { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and sys_platform == 'darwin') or (python_full_version >= '3.11' and sys_platform == 'linux') or (python_full_version >= '3.11' and sys_platform == 'win32')" }, { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/00/20/bb50f9de3a6de69e6abd6b087b52fa2418a0418b19597601605f855ad044/pydata_sphinx_theme-0.16.1.tar.gz", hash = "sha256:a08b7f0b7f70387219dc659bff0893a7554d5eb39b59d3b8ef37b8401b7642d7", size = 2412693, upload-time = "2024-12-17T10:53:39.537Z" } +sdist = { url = "https://files.pythonhosted.org/packages/00/20/bb50f9de3a6de69e6abd6b087b52fa2418a0418b19597601605f855ad044/pydata_sphinx_theme-0.16.1.tar.gz", hash = "sha256:a08b7f0b7f70387219dc659bff0893a7554d5eb39b59d3b8ef37b8401b7642d7", size = 2412693 } wheels = [ - { url = "https://files.pythonhosted.org/packages/e2/0d/8ba33fa83a7dcde13eb3c1c2a0c1cc29950a048bfed6d9b0d8b6bd710b4c/pydata_sphinx_theme-0.16.1-py3-none-any.whl", hash = "sha256:225331e8ac4b32682c18fcac5a57a6f717c4e632cea5dd0e247b55155faeccde", size = 6723264, upload-time = "2024-12-17T10:53:35.645Z" }, + { url = "https://files.pythonhosted.org/packages/e2/0d/8ba33fa83a7dcde13eb3c1c2a0c1cc29950a048bfed6d9b0d8b6bd710b4c/pydata_sphinx_theme-0.16.1-py3-none-any.whl", hash = "sha256:225331e8ac4b32682c18fcac5a57a6f717c4e632cea5dd0e247b55155faeccde", size = 6723264 }, ] [[package]] @@ -3156,9 +3213,9 @@ dependencies = [ { name = "pytest", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "typing-extensions", marker = "(python_full_version < '3.13' and sys_platform == 'darwin') or (python_full_version < '3.13' and sys_platform == 'linux') or (python_full_version < '3.13' and sys_platform == 'win32')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/42/86/9e3c5f48f7b7b638b216e4b9e645f54d199d7abbbab7a64a13b4e12ba10f/pytest_asyncio-1.2.0.tar.gz", hash = "sha256:c609a64a2a8768462d0c99811ddb8bd2583c33fd33cf7f21af1c142e824ffb57", size = 50119, upload-time = "2025-09-12T07:33:53.816Z" } +sdist = { url = "https://files.pythonhosted.org/packages/42/86/9e3c5f48f7b7b638b216e4b9e645f54d199d7abbbab7a64a13b4e12ba10f/pytest_asyncio-1.2.0.tar.gz", hash = "sha256:c609a64a2a8768462d0c99811ddb8bd2583c33fd33cf7f21af1c142e824ffb57", size = 50119 } wheels = [ - { url = "https://files.pythonhosted.org/packages/04/93/2fa34714b7a4ae72f2f8dad66ba17dd9a2c793220719e736dda28b7aec27/pytest_asyncio-1.2.0-py3-none-any.whl", hash = "sha256:8e17ae5e46d8e7efe51ab6494dd2010f4ca8dae51652aa3c8d55acf50bfb2e99", size = 15095, upload-time = "2025-09-12T07:33:52.639Z" }, + { url = "https://files.pythonhosted.org/packages/04/93/2fa34714b7a4ae72f2f8dad66ba17dd9a2c793220719e736dda28b7aec27/pytest_asyncio-1.2.0-py3-none-any.whl", hash = "sha256:8e17ae5e46d8e7efe51ab6494dd2010f4ca8dae51652aa3c8d55acf50bfb2e99", size = 15095 }, ] [[package]] @@ -3349,7 +3406,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e6/2f/104c0a3c778d7c2ab8190e9db4f62f0b6957b53c9d87db77c284b69f33ea/pyzmq-27.1.0-cp312-abi3-win32.whl", hash = "sha256:250e5436a4ba13885494412b3da5d518cd0d3a278a1ae640e113c073a5f88edd", size = 559184, upload-time = "2025-09-08T23:08:15.163Z" }, { url = "https://files.pythonhosted.org/packages/fc/7f/a21b20d577e4100c6a41795842028235998a643b1ad406a6d4163ea8f53e/pyzmq-27.1.0-cp312-abi3-win_amd64.whl", hash = "sha256:9ce490cf1d2ca2ad84733aa1d69ce6855372cb5ce9223802450c9b2a7cba0ccf", size = 619480, upload-time = "2025-09-08T23:08:17.192Z" }, { url = "https://files.pythonhosted.org/packages/78/c2/c012beae5f76b72f007a9e91ee9401cb88c51d0f83c6257a03e785c81cc2/pyzmq-27.1.0-cp312-abi3-win_arm64.whl", hash = "sha256:75a2f36223f0d535a0c919e23615fc85a1e23b71f40c7eb43d7b1dedb4d8f15f", size = 552993, upload-time = "2025-09-08T23:08:18.926Z" }, - { url = "https://files.pythonhosted.org/packages/60/cb/84a13459c51da6cec1b7b1dc1a47e6db6da50b77ad7fd9c145842750a011/pyzmq-27.1.0-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:93ad4b0855a664229559e45c8d23797ceac03183c7b6f5b4428152a6b06684a5", size = 1122436, upload-time = "2025-09-08T23:08:20.801Z" }, { url = "https://files.pythonhosted.org/packages/a5/ad/15906493fd40c316377fd8a8f6b1f93104f97a752667763c9b9c1b71d42d/pyzmq-27.1.0-cp313-cp313t-macosx_10_15_universal2.whl", hash = "sha256:e343d067f7b151cfe4eb3bb796a7752c9d369eed007b91231e817071d2c2fec7", size = 1341197, upload-time = "2025-09-08T23:08:24.286Z" }, { url = "https://files.pythonhosted.org/packages/14/1d/d343f3ce13db53a54cb8946594e567410b2125394dafcc0268d8dda027e0/pyzmq-27.1.0-cp313-cp313t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:08363b2011dec81c354d694bdecaef4770e0ae96b9afea70b3f47b973655cc05", size = 897275, upload-time = "2025-09-08T23:08:26.063Z" }, { url = "https://files.pythonhosted.org/packages/69/2d/d83dd6d7ca929a2fc67d2c3005415cdf322af7751d773524809f9e585129/pyzmq-27.1.0-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d54530c8c8b5b8ddb3318f481297441af102517602b569146185fa10b63f4fa9", size = 660469, upload-time = "2025-09-08T23:08:27.623Z" }, @@ -3616,26 +3672,26 @@ wheels = [ name = "ruff" version = "0.13.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6e/1a/1f4b722862840295bcaba8c9e5261572347509548faaa99b2d57ee7bfe6a/ruff-0.13.0.tar.gz", hash = "sha256:5b4b1ee7eb35afae128ab94459b13b2baaed282b1fb0f472a73c82c996c8ae60", size = 5372863, upload-time = "2025-09-10T16:25:37.917Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6e/1a/1f4b722862840295bcaba8c9e5261572347509548faaa99b2d57ee7bfe6a/ruff-0.13.0.tar.gz", hash = "sha256:5b4b1ee7eb35afae128ab94459b13b2baaed282b1fb0f472a73c82c996c8ae60", size = 5372863 } wheels = [ - { url = "https://files.pythonhosted.org/packages/ac/fe/6f87b419dbe166fd30a991390221f14c5b68946f389ea07913e1719741e0/ruff-0.13.0-py3-none-linux_armv6l.whl", hash = "sha256:137f3d65d58ee828ae136a12d1dc33d992773d8f7644bc6b82714570f31b2004", size = 12187826, upload-time = "2025-09-10T16:24:39.5Z" }, - { url = "https://files.pythonhosted.org/packages/e4/25/c92296b1fc36d2499e12b74a3fdb230f77af7bdf048fad7b0a62e94ed56a/ruff-0.13.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:21ae48151b66e71fd111b7d79f9ad358814ed58c339631450c66a4be33cc28b9", size = 12933428, upload-time = "2025-09-10T16:24:43.866Z" }, - { url = "https://files.pythonhosted.org/packages/44/cf/40bc7221a949470307d9c35b4ef5810c294e6cfa3caafb57d882731a9f42/ruff-0.13.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:64de45f4ca5441209e41742d527944635a05a6e7c05798904f39c85bafa819e3", size = 12095543, upload-time = "2025-09-10T16:24:46.638Z" }, - { url = "https://files.pythonhosted.org/packages/f1/03/8b5ff2a211efb68c63a1d03d157e924997ada87d01bebffbd13a0f3fcdeb/ruff-0.13.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2b2c653ae9b9d46e0ef62fc6fbf5b979bda20a0b1d2b22f8f7eb0cde9f4963b8", size = 12312489, upload-time = "2025-09-10T16:24:49.556Z" }, - { url = "https://files.pythonhosted.org/packages/37/fc/2336ef6d5e9c8d8ea8305c5f91e767d795cd4fc171a6d97ef38a5302dadc/ruff-0.13.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4cec632534332062bc9eb5884a267b689085a1afea9801bf94e3ba7498a2d207", size = 11991631, upload-time = "2025-09-10T16:24:53.439Z" }, - { url = "https://files.pythonhosted.org/packages/39/7f/f6d574d100fca83d32637d7f5541bea2f5e473c40020bbc7fc4a4d5b7294/ruff-0.13.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dcd628101d9f7d122e120ac7c17e0a0f468b19bc925501dbe03c1cb7f5415b24", size = 13720602, upload-time = "2025-09-10T16:24:56.392Z" }, - { url = "https://files.pythonhosted.org/packages/fd/c8/a8a5b81d8729b5d1f663348d11e2a9d65a7a9bd3c399763b1a51c72be1ce/ruff-0.13.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:afe37db8e1466acb173bb2a39ca92df00570e0fd7c94c72d87b51b21bb63efea", size = 14697751, upload-time = "2025-09-10T16:24:59.89Z" }, - { url = "https://files.pythonhosted.org/packages/57/f5/183ec292272ce7ec5e882aea74937f7288e88ecb500198b832c24debc6d3/ruff-0.13.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0f96a8d90bb258d7d3358b372905fe7333aaacf6c39e2408b9f8ba181f4b6ef2", size = 14095317, upload-time = "2025-09-10T16:25:03.025Z" }, - { url = "https://files.pythonhosted.org/packages/9f/8d/7f9771c971724701af7926c14dab31754e7b303d127b0d3f01116faef456/ruff-0.13.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94b5e3d883e4f924c5298e3f2ee0f3085819c14f68d1e5b6715597681433f153", size = 13144418, upload-time = "2025-09-10T16:25:06.272Z" }, - { url = "https://files.pythonhosted.org/packages/a8/a6/7985ad1778e60922d4bef546688cd8a25822c58873e9ff30189cfe5dc4ab/ruff-0.13.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:03447f3d18479df3d24917a92d768a89f873a7181a064858ea90a804a7538991", size = 13370843, upload-time = "2025-09-10T16:25:09.965Z" }, - { url = "https://files.pythonhosted.org/packages/64/1c/bafdd5a7a05a50cc51d9f5711da704942d8dd62df3d8c70c311e98ce9f8a/ruff-0.13.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:fbc6b1934eb1c0033da427c805e27d164bb713f8e273a024a7e86176d7f462cf", size = 13321891, upload-time = "2025-09-10T16:25:12.969Z" }, - { url = "https://files.pythonhosted.org/packages/bc/3e/7817f989cb9725ef7e8d2cee74186bf90555279e119de50c750c4b7a72fe/ruff-0.13.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a8ab6a3e03665d39d4a25ee199d207a488724f022db0e1fe4002968abdb8001b", size = 12119119, upload-time = "2025-09-10T16:25:16.621Z" }, - { url = "https://files.pythonhosted.org/packages/58/07/9df080742e8d1080e60c426dce6e96a8faf9a371e2ce22eef662e3839c95/ruff-0.13.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d2a5c62f8ccc6dd2fe259917482de7275cecc86141ee10432727c4816235bc41", size = 11961594, upload-time = "2025-09-10T16:25:19.49Z" }, - { url = "https://files.pythonhosted.org/packages/6a/f4/ae1185349197d26a2316840cb4d6c3fba61d4ac36ed728bf0228b222d71f/ruff-0.13.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:b7b85ca27aeeb1ab421bc787009831cffe6048faae08ad80867edab9f2760945", size = 12933377, upload-time = "2025-09-10T16:25:22.371Z" }, - { url = "https://files.pythonhosted.org/packages/b6/39/e776c10a3b349fc8209a905bfb327831d7516f6058339a613a8d2aaecacd/ruff-0.13.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:79ea0c44a3032af768cabfd9616e44c24303af49d633b43e3a5096e009ebe823", size = 13418555, upload-time = "2025-09-10T16:25:25.681Z" }, - { url = "https://files.pythonhosted.org/packages/46/09/dca8df3d48e8b3f4202bf20b1658898e74b6442ac835bfe2c1816d926697/ruff-0.13.0-py3-none-win32.whl", hash = "sha256:4e473e8f0e6a04e4113f2e1de12a5039579892329ecc49958424e5568ef4f768", size = 12141613, upload-time = "2025-09-10T16:25:28.664Z" }, - { url = "https://files.pythonhosted.org/packages/61/21/0647eb71ed99b888ad50e44d8ec65d7148babc0e242d531a499a0bbcda5f/ruff-0.13.0-py3-none-win_amd64.whl", hash = "sha256:48e5c25c7a3713eea9ce755995767f4dcd1b0b9599b638b12946e892123d1efb", size = 13258250, upload-time = "2025-09-10T16:25:31.773Z" }, - { url = "https://files.pythonhosted.org/packages/e1/a3/03216a6a86c706df54422612981fb0f9041dbb452c3401501d4a22b942c9/ruff-0.13.0-py3-none-win_arm64.whl", hash = "sha256:ab80525317b1e1d38614addec8ac954f1b3e662de9d59114ecbf771d00cf613e", size = 12312357, upload-time = "2025-09-10T16:25:35.595Z" }, + { url = "https://files.pythonhosted.org/packages/ac/fe/6f87b419dbe166fd30a991390221f14c5b68946f389ea07913e1719741e0/ruff-0.13.0-py3-none-linux_armv6l.whl", hash = "sha256:137f3d65d58ee828ae136a12d1dc33d992773d8f7644bc6b82714570f31b2004", size = 12187826 }, + { url = "https://files.pythonhosted.org/packages/e4/25/c92296b1fc36d2499e12b74a3fdb230f77af7bdf048fad7b0a62e94ed56a/ruff-0.13.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:21ae48151b66e71fd111b7d79f9ad358814ed58c339631450c66a4be33cc28b9", size = 12933428 }, + { url = "https://files.pythonhosted.org/packages/44/cf/40bc7221a949470307d9c35b4ef5810c294e6cfa3caafb57d882731a9f42/ruff-0.13.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:64de45f4ca5441209e41742d527944635a05a6e7c05798904f39c85bafa819e3", size = 12095543 }, + { url = "https://files.pythonhosted.org/packages/f1/03/8b5ff2a211efb68c63a1d03d157e924997ada87d01bebffbd13a0f3fcdeb/ruff-0.13.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2b2c653ae9b9d46e0ef62fc6fbf5b979bda20a0b1d2b22f8f7eb0cde9f4963b8", size = 12312489 }, + { url = "https://files.pythonhosted.org/packages/37/fc/2336ef6d5e9c8d8ea8305c5f91e767d795cd4fc171a6d97ef38a5302dadc/ruff-0.13.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4cec632534332062bc9eb5884a267b689085a1afea9801bf94e3ba7498a2d207", size = 11991631 }, + { url = "https://files.pythonhosted.org/packages/39/7f/f6d574d100fca83d32637d7f5541bea2f5e473c40020bbc7fc4a4d5b7294/ruff-0.13.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dcd628101d9f7d122e120ac7c17e0a0f468b19bc925501dbe03c1cb7f5415b24", size = 13720602 }, + { url = "https://files.pythonhosted.org/packages/fd/c8/a8a5b81d8729b5d1f663348d11e2a9d65a7a9bd3c399763b1a51c72be1ce/ruff-0.13.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:afe37db8e1466acb173bb2a39ca92df00570e0fd7c94c72d87b51b21bb63efea", size = 14697751 }, + { url = "https://files.pythonhosted.org/packages/57/f5/183ec292272ce7ec5e882aea74937f7288e88ecb500198b832c24debc6d3/ruff-0.13.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0f96a8d90bb258d7d3358b372905fe7333aaacf6c39e2408b9f8ba181f4b6ef2", size = 14095317 }, + { url = "https://files.pythonhosted.org/packages/9f/8d/7f9771c971724701af7926c14dab31754e7b303d127b0d3f01116faef456/ruff-0.13.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94b5e3d883e4f924c5298e3f2ee0f3085819c14f68d1e5b6715597681433f153", size = 13144418 }, + { url = "https://files.pythonhosted.org/packages/a8/a6/7985ad1778e60922d4bef546688cd8a25822c58873e9ff30189cfe5dc4ab/ruff-0.13.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:03447f3d18479df3d24917a92d768a89f873a7181a064858ea90a804a7538991", size = 13370843 }, + { url = "https://files.pythonhosted.org/packages/64/1c/bafdd5a7a05a50cc51d9f5711da704942d8dd62df3d8c70c311e98ce9f8a/ruff-0.13.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:fbc6b1934eb1c0033da427c805e27d164bb713f8e273a024a7e86176d7f462cf", size = 13321891 }, + { url = "https://files.pythonhosted.org/packages/bc/3e/7817f989cb9725ef7e8d2cee74186bf90555279e119de50c750c4b7a72fe/ruff-0.13.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a8ab6a3e03665d39d4a25ee199d207a488724f022db0e1fe4002968abdb8001b", size = 12119119 }, + { url = "https://files.pythonhosted.org/packages/58/07/9df080742e8d1080e60c426dce6e96a8faf9a371e2ce22eef662e3839c95/ruff-0.13.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d2a5c62f8ccc6dd2fe259917482de7275cecc86141ee10432727c4816235bc41", size = 11961594 }, + { url = "https://files.pythonhosted.org/packages/6a/f4/ae1185349197d26a2316840cb4d6c3fba61d4ac36ed728bf0228b222d71f/ruff-0.13.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:b7b85ca27aeeb1ab421bc787009831cffe6048faae08ad80867edab9f2760945", size = 12933377 }, + { url = "https://files.pythonhosted.org/packages/b6/39/e776c10a3b349fc8209a905bfb327831d7516f6058339a613a8d2aaecacd/ruff-0.13.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:79ea0c44a3032af768cabfd9616e44c24303af49d633b43e3a5096e009ebe823", size = 13418555 }, + { url = "https://files.pythonhosted.org/packages/46/09/dca8df3d48e8b3f4202bf20b1658898e74b6442ac835bfe2c1816d926697/ruff-0.13.0-py3-none-win32.whl", hash = "sha256:4e473e8f0e6a04e4113f2e1de12a5039579892329ecc49958424e5568ef4f768", size = 12141613 }, + { url = "https://files.pythonhosted.org/packages/61/21/0647eb71ed99b888ad50e44d8ec65d7148babc0e242d531a499a0bbcda5f/ruff-0.13.0-py3-none-win_amd64.whl", hash = "sha256:48e5c25c7a3713eea9ce755995767f4dcd1b0b9599b638b12946e892123d1efb", size = 13258250 }, + { url = "https://files.pythonhosted.org/packages/e1/a3/03216a6a86c706df54422612981fb0f9041dbb452c3401501d4a22b942c9/ruff-0.13.0-py3-none-win_arm64.whl", hash = "sha256:ab80525317b1e1d38614addec8ac954f1b3e662de9d59114ecbf771d00cf613e", size = 12312357 }, ] [[package]] @@ -3968,9 +4024,9 @@ dependencies = [ { name = "anyio", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "typing-extensions", marker = "(python_full_version < '3.13' and sys_platform == 'darwin') or (python_full_version < '3.13' and sys_platform == 'linux') or (python_full_version < '3.13' and sys_platform == 'win32')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a7/a5/d6f429d43394057b67a6b5bbe6eae2f77a6bf7459d961fdb224bf206eee6/starlette-0.48.0.tar.gz", hash = "sha256:7e8cee469a8ab2352911528110ce9088fdc6a37d9876926e73da7ce4aa4c7a46", size = 2652949, upload-time = "2025-09-13T08:41:05.699Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a7/a5/d6f429d43394057b67a6b5bbe6eae2f77a6bf7459d961fdb224bf206eee6/starlette-0.48.0.tar.gz", hash = "sha256:7e8cee469a8ab2352911528110ce9088fdc6a37d9876926e73da7ce4aa4c7a46", size = 2652949 } wheels = [ - { url = "https://files.pythonhosted.org/packages/be/72/2db2f49247d0a18b4f1bb9a5a39a0162869acf235f3a96418363947b3d46/starlette-0.48.0-py3-none-any.whl", hash = "sha256:0764ca97b097582558ecb498132ed0c7d942f233f365b86ba37770e026510659", size = 73736, upload-time = "2025-09-13T08:41:03.869Z" }, + { url = "https://files.pythonhosted.org/packages/be/72/2db2f49247d0a18b4f1bb9a5a39a0162869acf235f3a96418363947b3d46/starlette-0.48.0-py3-none-any.whl", hash = "sha256:0764ca97b097582558ecb498132ed0c7d942f233f365b86ba37770e026510659", size = 73736 }, ] [[package]] @@ -4113,26 +4169,26 @@ wheels = [ name = "uv" version = "0.8.17" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6f/4c/c270c6b8ed3e8c7fe38ea0b99df9eff09c332421b93d55a158371f75220e/uv-0.8.17.tar.gz", hash = "sha256:2afd4525a53c8ab3a11a5a15093c503d27da67e76257a649b05e4f0bc2ebb5ae", size = 3615060, upload-time = "2025-09-10T21:51:25.067Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/4c/c270c6b8ed3e8c7fe38ea0b99df9eff09c332421b93d55a158371f75220e/uv-0.8.17.tar.gz", hash = "sha256:2afd4525a53c8ab3a11a5a15093c503d27da67e76257a649b05e4f0bc2ebb5ae", size = 3615060 } wheels = [ - { url = "https://files.pythonhosted.org/packages/b9/7d/bbaa45c88b2c91e02714a8a5c9e787c47e4898bddfdd268569163492ba45/uv-0.8.17-py3-none-linux_armv6l.whl", hash = "sha256:c51c9633ca93ef63c07df2443941e6264efd2819cc9faabfd9fe11899c6a0d6a", size = 20242144, upload-time = "2025-09-10T21:50:18.081Z" }, - { url = "https://files.pythonhosted.org/packages/65/34/609b72034df0c62bcfb0c0ad4b11e2b55e537c0f0817588b5337d3dcca71/uv-0.8.17-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:c28fba6d7bb5c34ade2c8da5000faebe8425a287f42a043ca01ceb24ebc81590", size = 19363081, upload-time = "2025-09-10T21:50:22.731Z" }, - { url = "https://files.pythonhosted.org/packages/b6/bc/9417df48f0c18a9d54c2444096e03f2f56a3534c5b869f50ac620729cbc8/uv-0.8.17-py3-none-macosx_11_0_arm64.whl", hash = "sha256:b009f1ec9e28de00f76814ad66e35aaae82c98a0f24015de51943dcd1c2a1895", size = 17943513, upload-time = "2025-09-10T21:50:25.824Z" }, - { url = "https://files.pythonhosted.org/packages/63/1c/14fd54c852fd592a2b5da4b7960f3bf4a15c7e51eb20eaddabe8c8cca32d/uv-0.8.17-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:84d56ae50ca71aec032577adf9737974554a82a94e52cee57722745656c1d383", size = 19507222, upload-time = "2025-09-10T21:50:29.237Z" }, - { url = "https://files.pythonhosted.org/packages/be/47/f6a68cc310feca37c965bcbd57eb999e023d35eaeda9c9759867bf3ed232/uv-0.8.17-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:85c2140f8553b9a4387a7395dc30cd151ef94046785fe8b198f13f2c380fb39b", size = 19865652, upload-time = "2025-09-10T21:50:32.87Z" }, - { url = "https://files.pythonhosted.org/packages/ab/6a/fdeb2d4a2635a6927c6d549b07177bcaf6ce15bdef58e8253e75c1b70f54/uv-0.8.17-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2076119783e4a6d3c9e25638956cb123f0eabf4d7d407d9661cdf7f84818dcb9", size = 20831760, upload-time = "2025-09-10T21:50:37.803Z" }, - { url = "https://files.pythonhosted.org/packages/d0/4c/bd58b8a76015aa9ac49d6b4e1211ae1ca98a0aade0c49e1a5f645fb5cd38/uv-0.8.17-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:707a55660d302924fdbcb509e63dfec8842e19d35b69bcc17af76c25db15ad6f", size = 22209056, upload-time = "2025-09-10T21:50:41.749Z" }, - { url = "https://files.pythonhosted.org/packages/7e/2e/28f59c00a2ed6532502fb1e27da9394e505fb7b41cc0274475104b43561b/uv-0.8.17-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1824b76911a14aaa9eee65ad9e180e6a4d2d7c86826232c2f28ae86aee56ed0e", size = 21871684, upload-time = "2025-09-10T21:50:45.331Z" }, - { url = "https://files.pythonhosted.org/packages/5a/1d/a8a4fc08de1f767316467e7a1989bb125734b7ed9cd98ce8969386a70653/uv-0.8.17-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bb9b515cc813fb1b08f1e7592f76e437e2fb44945e53cde4fee11dee3b16d0c3", size = 21145154, upload-time = "2025-09-10T21:50:50.388Z" }, - { url = "https://files.pythonhosted.org/packages/8f/35/cb47d2d07a383c07b0e5043c6fe5555f0fd79683c6d7f9760222987c8be9/uv-0.8.17-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b6d30d02fb65193309fc12a20f9e1a9fab67f469d3e487a254ca1145fd06788f", size = 21106619, upload-time = "2025-09-10T21:50:54.5Z" }, - { url = "https://files.pythonhosted.org/packages/6e/93/c310f0153b9dfe79bdd7f7eaef6380a8545c8939dbfc4e6bdee8f3ee7050/uv-0.8.17-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:3941cecd9a6a46d3d4505753912c9cf3e8ae5eea30b9d0813f3656210f8c5d01", size = 19777591, upload-time = "2025-09-10T21:50:57.765Z" }, - { url = "https://files.pythonhosted.org/packages/6c/4f/971d3c84c2f09cf8df4536c33644e6b97e10a259d8630a0c1696c1fa6e94/uv-0.8.17-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:cd0ad366cfe4cbe9212bd660b5b9f3a827ff35a7601cefdac2d153bfc8079eb7", size = 20845039, upload-time = "2025-09-10T21:51:01.167Z" }, - { url = "https://files.pythonhosted.org/packages/4a/29/8ad9038e75cb91f54b81cc933dd14fcfa92fa6f8706117d43d4251a8a662/uv-0.8.17-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:505854bc75c497b95d2c65590291dc820999a4a7d9dfab4f44a9434a6cff7b5f", size = 19820370, upload-time = "2025-09-10T21:51:04.616Z" }, - { url = "https://files.pythonhosted.org/packages/f2/c9/fc8482d1e7dfe187c6e03dcefbac0db41a5dd72aa7b017c0f80f91a04444/uv-0.8.17-py3-none-musllinux_1_1_i686.whl", hash = "sha256:dc479f661da449df37d68b36fdffa641e89fb53ad38c16a5c9f98f3211785b63", size = 20289951, upload-time = "2025-09-10T21:51:08.605Z" }, - { url = "https://files.pythonhosted.org/packages/2d/84/ad878ed045f02aa973be46636c802d494f8270caf5ea8bd04b7bbc68aa23/uv-0.8.17-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:a1d11cd805be6d137ffef4a8227905f87f459031c645ac5031c30a3bcd08abd6", size = 21234644, upload-time = "2025-09-10T21:51:12.429Z" }, - { url = "https://files.pythonhosted.org/packages/f8/03/3fa2641513922988e641050b3adbc87de527f44c2cc8328510703616be6a/uv-0.8.17-py3-none-win32.whl", hash = "sha256:d13a616eb0b2b33c7aa09746cc85860101d595655b58653f0b499af19f33467c", size = 19216757, upload-time = "2025-09-10T21:51:16.021Z" }, - { url = "https://files.pythonhosted.org/packages/1a/c4/0082f437bac162ab95e5a3a389a184c122d45eb5593960aab92fdf80374b/uv-0.8.17-py3-none-win_amd64.whl", hash = "sha256:cf85b84b81b41d57a9b6eeded8473ec06ace8ee959ad0bb57e102b5ad023bd34", size = 21125811, upload-time = "2025-09-10T21:51:19.397Z" }, - { url = "https://files.pythonhosted.org/packages/50/a2/29f57b118b3492c9d5ab1a99ba4906e7d7f8b658881d31bc2c4408d64d07/uv-0.8.17-py3-none-win_arm64.whl", hash = "sha256:64d649a8c4c3732b05dc712544963b004cf733d95fdc5d26f43c5493553ff0a7", size = 19564631, upload-time = "2025-09-10T21:51:22.599Z" }, + { url = "https://files.pythonhosted.org/packages/b9/7d/bbaa45c88b2c91e02714a8a5c9e787c47e4898bddfdd268569163492ba45/uv-0.8.17-py3-none-linux_armv6l.whl", hash = "sha256:c51c9633ca93ef63c07df2443941e6264efd2819cc9faabfd9fe11899c6a0d6a", size = 20242144 }, + { url = "https://files.pythonhosted.org/packages/65/34/609b72034df0c62bcfb0c0ad4b11e2b55e537c0f0817588b5337d3dcca71/uv-0.8.17-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:c28fba6d7bb5c34ade2c8da5000faebe8425a287f42a043ca01ceb24ebc81590", size = 19363081 }, + { url = "https://files.pythonhosted.org/packages/b6/bc/9417df48f0c18a9d54c2444096e03f2f56a3534c5b869f50ac620729cbc8/uv-0.8.17-py3-none-macosx_11_0_arm64.whl", hash = "sha256:b009f1ec9e28de00f76814ad66e35aaae82c98a0f24015de51943dcd1c2a1895", size = 17943513 }, + { url = "https://files.pythonhosted.org/packages/63/1c/14fd54c852fd592a2b5da4b7960f3bf4a15c7e51eb20eaddabe8c8cca32d/uv-0.8.17-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:84d56ae50ca71aec032577adf9737974554a82a94e52cee57722745656c1d383", size = 19507222 }, + { url = "https://files.pythonhosted.org/packages/be/47/f6a68cc310feca37c965bcbd57eb999e023d35eaeda9c9759867bf3ed232/uv-0.8.17-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:85c2140f8553b9a4387a7395dc30cd151ef94046785fe8b198f13f2c380fb39b", size = 19865652 }, + { url = "https://files.pythonhosted.org/packages/ab/6a/fdeb2d4a2635a6927c6d549b07177bcaf6ce15bdef58e8253e75c1b70f54/uv-0.8.17-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2076119783e4a6d3c9e25638956cb123f0eabf4d7d407d9661cdf7f84818dcb9", size = 20831760 }, + { url = "https://files.pythonhosted.org/packages/d0/4c/bd58b8a76015aa9ac49d6b4e1211ae1ca98a0aade0c49e1a5f645fb5cd38/uv-0.8.17-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:707a55660d302924fdbcb509e63dfec8842e19d35b69bcc17af76c25db15ad6f", size = 22209056 }, + { url = "https://files.pythonhosted.org/packages/7e/2e/28f59c00a2ed6532502fb1e27da9394e505fb7b41cc0274475104b43561b/uv-0.8.17-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1824b76911a14aaa9eee65ad9e180e6a4d2d7c86826232c2f28ae86aee56ed0e", size = 21871684 }, + { url = "https://files.pythonhosted.org/packages/5a/1d/a8a4fc08de1f767316467e7a1989bb125734b7ed9cd98ce8969386a70653/uv-0.8.17-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bb9b515cc813fb1b08f1e7592f76e437e2fb44945e53cde4fee11dee3b16d0c3", size = 21145154 }, + { url = "https://files.pythonhosted.org/packages/8f/35/cb47d2d07a383c07b0e5043c6fe5555f0fd79683c6d7f9760222987c8be9/uv-0.8.17-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b6d30d02fb65193309fc12a20f9e1a9fab67f469d3e487a254ca1145fd06788f", size = 21106619 }, + { url = "https://files.pythonhosted.org/packages/6e/93/c310f0153b9dfe79bdd7f7eaef6380a8545c8939dbfc4e6bdee8f3ee7050/uv-0.8.17-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:3941cecd9a6a46d3d4505753912c9cf3e8ae5eea30b9d0813f3656210f8c5d01", size = 19777591 }, + { url = "https://files.pythonhosted.org/packages/6c/4f/971d3c84c2f09cf8df4536c33644e6b97e10a259d8630a0c1696c1fa6e94/uv-0.8.17-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:cd0ad366cfe4cbe9212bd660b5b9f3a827ff35a7601cefdac2d153bfc8079eb7", size = 20845039 }, + { url = "https://files.pythonhosted.org/packages/4a/29/8ad9038e75cb91f54b81cc933dd14fcfa92fa6f8706117d43d4251a8a662/uv-0.8.17-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:505854bc75c497b95d2c65590291dc820999a4a7d9dfab4f44a9434a6cff7b5f", size = 19820370 }, + { url = "https://files.pythonhosted.org/packages/f2/c9/fc8482d1e7dfe187c6e03dcefbac0db41a5dd72aa7b017c0f80f91a04444/uv-0.8.17-py3-none-musllinux_1_1_i686.whl", hash = "sha256:dc479f661da449df37d68b36fdffa641e89fb53ad38c16a5c9f98f3211785b63", size = 20289951 }, + { url = "https://files.pythonhosted.org/packages/2d/84/ad878ed045f02aa973be46636c802d494f8270caf5ea8bd04b7bbc68aa23/uv-0.8.17-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:a1d11cd805be6d137ffef4a8227905f87f459031c645ac5031c30a3bcd08abd6", size = 21234644 }, + { url = "https://files.pythonhosted.org/packages/f8/03/3fa2641513922988e641050b3adbc87de527f44c2cc8328510703616be6a/uv-0.8.17-py3-none-win32.whl", hash = "sha256:d13a616eb0b2b33c7aa09746cc85860101d595655b58653f0b499af19f33467c", size = 19216757 }, + { url = "https://files.pythonhosted.org/packages/1a/c4/0082f437bac162ab95e5a3a389a184c122d45eb5593960aab92fdf80374b/uv-0.8.17-py3-none-win_amd64.whl", hash = "sha256:cf85b84b81b41d57a9b6eeded8473ec06ace8ee959ad0bb57e102b5ad023bd34", size = 21125811 }, + { url = "https://files.pythonhosted.org/packages/50/a2/29f57b118b3492c9d5ab1a99ba4906e7d7f8b658881d31bc2c4408d64d07/uv-0.8.17-py3-none-win_arm64.whl", hash = "sha256:64d649a8c4c3732b05dc712544963b004cf733d95fdc5d26f43c5493553ff0a7", size = 19564631 }, ] [[package]] diff --git a/workflows/DeepResearch.yaml b/workflows/DeepResearch.yaml index d9482d5f6f..9612cc8464 100644 --- a/workflows/DeepResearch.yaml +++ b/workflows/DeepResearch.yaml @@ -81,54 +81,64 @@ beginDialog: id: sendActivity_yFsbRy activity: Analyzing facts... - - kind: AnswerQuestionWithAI + - kind: CreateConversation + id: conversation_1a2b3c + conversationId: Topic.InternalConversationId + + - kind: InvokeAzureAgent id: question_UDoMUw displayName: Get Facts - autoSend: false - variable: Topic.TaskFacts - userInput: =Topic.InputTask - additionalInstructions: |- - {Env.FOUNDRY_AGENT_RESEARCHANALYST}, - In order to help begin addressing the user request, please answer the following pre-survey to the best of your ability. - Keep in mind that you are Ken Jennings-level with trivia, and Mensa-level with puzzles, so there should be a deep well to draw from. + conversationId: =Topic.InternalConversationId + agent: + name: =Env.FOUNDRY_AGENT_RESEARCHANALYST + output: + messages: Topic.TaskFacts + input: + messages: =[UserMessage(Topic.InputTask)] + additionalInstructions: |- + In order to help begin addressing the user request, please answer the following pre-survey to the best of your ability. + Keep in mind that you are Ken Jennings-level with trivia, and Mensa-level with puzzles, so there should be a deep well to draw from. - Here is the pre-survey: + Here is the pre-survey: - 1. Please list any specific facts or figures that are GIVEN in the request itself. It is possible that there are none. - 2. Please list any facts that may need to be looked up, and WHERE SPECIFICALLY they might be found. In some cases, authoritative sources are mentioned in the request itself. - 3. Please list any facts that may need to be derived (e.g., via logical deduction, simulation, or computation) - 4. Please list any facts that are recalled from memory, hunches, well-reasoned guesses, etc. + 1. Please list any specific facts or figures that are GIVEN in the request itself. It is possible that there are none. + 2. Please list any facts that may need to be looked up, and WHERE SPECIFICALLY they might be found. In some cases, authoritative sources are mentioned in the request itself. + 3. Please list any facts that may need to be derived (e.g., via logical deduction, simulation, or computation) + 4. Please list any facts that are recalled from memory, hunches, well-reasoned guesses, etc. - When answering this survey, keep in mind that 'facts' will typically be specific names, dates, statistics, etc. Your answer must only use the headings: + When answering this survey, keep in mind that 'facts' will typically be specific names, dates, statistics, etc. Your answer must only use the headings: - 1. GIVEN OR VERIFIED FACTS - 2. FACTS TO LOOK UP - 3. FACTS TO DERIVE - 4. EDUCATED GUESSES + 1. GIVEN OR VERIFIED FACTS + 2. FACTS TO LOOK UP + 3. FACTS TO DERIVE + 4. EDUCATED GUESSES - DO NOT include any other headings or sections in your response. DO NOT list next steps or plans until asked to do so. + DO NOT include any other headings or sections in your response. DO NOT list next steps or plans until asked to do so. - kind: SendActivity id: sendActivity_yFsbRz activity: Creating a plan... - - kind: AnswerQuestionWithAI + - kind: InvokeAzureAgent id: question_DsBaJU displayName: Create a Plan - autoSend: false - variable: Topic.Plan - userInput: ="" - additionalInstructions: |- - {Env.FOUNDRY_AGENT_RESEARCHMANAGER}, - Your only job is to devise an efficient plan that identifies (by name) how a team member may contribute to addressing the user request. + conversationId: =Topic.InternalConversationId + agent: + name: =Env.FOUNDRY_AGENT_RESEARCHMANAGER + output: + messages: Topic.Plan + input: + messages: =[UserMessage(Topic.InputTask)] + additionalInstructions: |- + Your only job is to devise an efficient plan that identifies (by name) how a team member may contribute to addressing the user request. - Only select the following team which is listed as "- [Name]: [Description]" + Only select the following team which is listed as "- [Name]: [Description]" - {Topic.TeamDescription} + {Topic.TeamDescription} - The plan must be a bullet point list must be in the form "- [AgentName]: [Specific action or task for that agent to perform]" + The plan must be a bullet point list must be in the form "- [AgentName]: [Specific action or task for that agent to perform]" - Remember, there is no requirement to involve the entire team -- only select team member's whose particular expertise is required for this task. + Remember, there is no requirement to involve the entire team -- only select team member's whose particular expertise is required for this task. - kind: SetVariable id: setVariable_Kk2LDL @@ -162,54 +172,57 @@ beginDialog: id: sendActivity_bwNZiM activity: {Topic.TaskInstructions} - - kind: AnswerQuestionWithAI + - kind: InvokeAzureAgent id: question_o3BQkf displayName: Progress Ledger Prompt - autoSend: false - variable: Topic.ProgressLedgerUpdate - userInput: =Topic.AgentResponseText - additionalInstructions: |- - {Env.FOUNDRY_AGENT_RESEARCHMANAGER}, - Recall we are working on the following request: + conversationId: =Topic.InternalConversationId + agent: + name: =Env.FOUNDRY_AGENT_RESEARCHMANAGER + output: + messages: Topic.ProgressLedgerUpdate + input: + messages: =[UserMessage(Topic.AgentResponseText)] + additionalInstructions: |- + Recall we are working on the following request: - {Topic.InputTask} + {Topic.InputTask} - And we have assembled the following team: + And we have assembled the following team: - {Topic.TeamDescription} + {Topic.TeamDescription} - To make progress on the request, please answer the following questions, including necessary reasoning: + To make progress on the request, please answer the following questions, including necessary reasoning: - - Is the request fully satisfied? (True if complete, or False if the original request has yet to be SUCCESSFULLY and FULLY addressed) - - Are we in a loop where we are repeating the same requests and / or getting the same responses from an agent multiple times? Loops can span multiple turns, and can include repeated actions like scrolling up or down more than a handful of times. - - Are we making forward progress? (True if just starting, or recent messages are adding value. False if recent messages show evidence of being stuck in a loop or if there is evidence of significant barriers to success such as the inability to read from a required file) - - Who should speak next? (select from: {Concat(Topic.AvailableAgents, name, ",")}) - - What instruction or question would you give this team member? (Phrase as if speaking directly to them, and include any specific information they may need) + - Is the request fully satisfied? (True if complete, or False if the original request has yet to be SUCCESSFULLY and FULLY addressed) + - Are we in a loop where we are repeating the same requests and / or getting the same responses from an agent multiple times? Loops can span multiple turns, and can include repeated actions like scrolling up or down more than a handful of times. + - Are we making forward progress? (True if just starting, or recent messages are adding value. False if recent messages show evidence of being stuck in a loop or if there is evidence of significant barriers to success such as the inability to read from a required file) + - Who should speak next? (select from: {Concat(Topic.AvailableAgents, name, ",")}) + - What instruction or question would you give this team member? (Phrase as if speaking directly to them, and include any specific information they may need) - Please output an answer in pure JSON format according to the following schema. The JSON object must be parsable as-is. DO NOT OUTPUT ANYTHING OTHER THAN JSON, AND DO NOT DEVIATE FROM THIS SCHEMA: + Please output an answer in pure JSON format according to the following schema. The JSON object must be parsable as-is. DO NOT OUTPUT ANYTHING OTHER THAN JSON, AND DO NOT DEVIATE FROM THIS SCHEMA: - {{ - "is_request_satisfied": {{ - "reason": string, - "answer": boolean - }}, - "is_in_loop": {{ - "reason": string, - "answer": boolean - }}, - "is_progress_being_made": {{ - "reason": string, - "answer": boolean - }}, - "next_speaker": {{ - "reason": string, - "answer": string (select from: {Concat(Topic.AvailableAgents, name, ",")}) - }}, - "instruction_or_question": {{ - "reason": string, - "answer": string - }} - }} + {{ + "is_request_satisfied": {{ + "reason": string, + "answer": boolean + }}, + "is_in_loop": {{ + "reason": string, + "answer": boolean + }}, + "is_progress_being_made": {{ + "reason": string, + "answer": boolean + }}, + "next_speaker": {{ + "reason": string, + "answer": string (select from: {Concat(Topic.AvailableAgents, name, ",")}) + }}, + "instruction_or_question": {{ + "reason": string, + "answer": string + }} + }} - kind: ParseValue id: parse_rNZtlV @@ -266,16 +279,20 @@ beginDialog: id: sendActivity_kdl3mC activity: Completed! {Topic.TypedProgressLedger.is_request_satisfied.reason} - - kind: AnswerQuestionWithAI + - kind: InvokeAzureAgent id: question_Ke3l1d displayName: Generate Response - variable: Topic.FinalResponse - userInput: =Topic.SeedTask - additionalInstructions: |- - {Env.FOUNDRY_AGENT_RESEARCHMANAGER}, - We have completed the task. - Based only on the conversation and without adding any new information, synthesize the result of the conversation as a complete response to the user task. - The user will only every see this last response and not the entire conversation, so please ensure it is complete and self-contained. + conversationId: =System.ConversationId + agent: + name: =Env.FOUNDRY_AGENT_RESEARCHMANAGER + output: + messages: Topic.FinalResponse + input: + messages: =[UserMessage(Topic.SeedTask)] + additionalInstructions: |- + We have completed the task. + Based only on the conversation and without adding any new information, synthesize the result of the conversation as a complete response to the user task. + The user will only every see this last response and not the entire conversation, so please ensure it is complete and self-contained. - kind: EndConversation id: end_SVoNSV @@ -342,45 +359,53 @@ beginDialog: id: sendActivity_cwNZiM activity: Re-analyzing facts... - - kind: AnswerQuestionWithAI + - kind: InvokeAzureAgent id: question_wFJ123 displayName: Get New Facts Prompt - autoSend: false - variable: Topic.TaskFacts - userInput: |- - ="As a reminder, we are working to solve the following task: + conversationId: =Topic.InternalConversationId + agent: + name: =Env.FOUNDRY_AGENT_RESEARCHANALYST + output: + messages: Topic.TaskFacts + input: + messages: |- + =[ + UserMessage( + "As a reminder, we are working to solve the following task: - " & Topic.InputTask - additionalInstructions: |- - {Env.FOUNDRY_AGENT_RESEARCHANALYST}, - It's clear we aren't making as much progress as we would like, but we may have learned something new. - Please rewrite the following fact sheet, updating it to include anything new we have learned that may be helpful. - Example edits can include (but are not limited to) adding new guesses, moving educated guesses to verified facts if appropriate, etc. - Updates may be made to any section of the fact sheet, and more than one section of the fact sheet can be edited. - This is an especially good time to update educated guesses, so please at least add or update one educated guess or hunch, and explain your reasoning. + " & Topic.InputTask) + ] + additionalInstructions: |- + It's clear we aren't making as much progress as we would like, but we may have learned something new. + Please rewrite the following fact sheet, updating it to include anything new we have learned that may be helpful. + Example edits can include (but are not limited to) adding new guesses, moving educated guesses to verified facts if appropriate, etc. + Updates may be made to any section of the fact sheet, and more than one section of the fact sheet can be edited. + This is an especially good time to update educated guesses, so please at least add or update one educated guess or hunch, and explain your reasoning. - Here is the old fact sheet: + Here is the old fact sheet: - {Topic.TaskFacts} + {Topic.TaskFacts} - kind: SendActivity id: sendActivity_dsBaJU activity: Re-analyzing plan... - - - kind: AnswerQuestionWithAI + + - kind: InvokeAzureAgent id: question_uEJ456 displayName: Create new Plan Prompt - autoSend: false - variable: Topic.Plan - userInput: ="" - additionalInstructions: |- - {Env.FOUNDRY_AGENT_RESEARCHMANAGER}, - Please briefly explain what went wrong on this last run (the root cause of the failure), - and then come up with a new plan that takes steps and/or includes hints to overcome prior challenges and especially avoids repeating the same mistakes. - As before, the new plan should be concise, be expressed in bullet-point form, and consider the following team composition - (do not involve any other outside people since we cannot contact anyone else): + conversationId: =Topic.InternalConversationId + agent: + name: =Env.FOUNDRY_AGENT_RESEARCHMANAGER + output: + messages: Topic.Plan + input: + additionalInstructions: |- + Please briefly explain what went wrong on this last run (the root cause of the failure), + and then come up with a new plan that takes steps and/or includes hints to overcome prior challenges and especially avoids repeating the same mistakes. + As before, the new plan should be concise, be expressed in bullet-point form, and consider the following team composition + (do not involve any other outside people since we cannot contact anyone else): - {Topic.TeamDescription} + {Topic.TeamDescription} - kind: SetVariable id: setVariable_jW7tmM @@ -452,13 +477,18 @@ beginDialog: displayName: If next Agent tool Exists actions: - - kind: AnswerQuestionWithAI + - kind: InvokeAzureAgent id: question_orsBf06 - variable: Topic.AgentResponse - userInput: =Topic.SeedTask - additionalInstructions: |- - {First(Topic.NextSpeaker).agentid}, - {Topic.TypedProgressLedger.instruction_or_question.answer} + displayName: Progress Ledger Prompt + conversationId: =System.ConversationId + agent: + name: =First(Topic.NextSpeaker).agentid + output: + messages: Topic.AgentResponse + input: + messages: =[UserMessage(Topic.SeedTask)] + additionalInstructions: |- + {Topic.TypedProgressLedger.instruction_or_question.answer} - kind: SetVariable id: setVariable_XzNrdM diff --git a/workflows/HumanInLoop.yaml b/workflows/HumanInLoop.yaml new file mode 100644 index 0000000000..a1a28d3b61 --- /dev/null +++ b/workflows/HumanInLoop.yaml @@ -0,0 +1,57 @@ +# +# This workflow demonstrates a single agent interaction based on user input. +# +# Any Foundry Agent may be used to provide the response. +# See: ./setup/QuestionAgent.yaml +# +kind: AdaptiveDialog +beginDialog: + + kind: OnActivity + id: workflow_demo + actions: + + # Capture original input + - kind: SetVariable + id: set_project + variable: Topic.OriginalInput + value: =System.LastMessage.Text + + # Request input from user + - kind: Question + id: question_confirm + alwaysPrompt: false + property: Topic.ConfirmedInput + prompt: + kind: Message + text: + - "CONFIRM:" + entity: + kind: StringPrebuiltEntity + + - kind: ConditionGroup + id: check_completion + conditions: + + - condition: =Topic.OriginalInput <> Topic.ConfirmedInput + id: check_confirm + actions: + + - kind: SendActivity + id: sendActivity_mismatch + activity: |- + "{Topic.ConfirmedInput}" does not match the original input of "{Topic.OriginalInput}". Please try again. + + - kind: GotoAction + id: goto_again + actionId: question_confirm + + elseActions: # // %%% REMOVE / BUG + - kind: SendActivity + id: sendActivity_confirmed + activity: |- + You entered: + {Topic.OriginalInput} + + Confirmed input: + {Topic.ConfirmedInput} diff --git a/workflows/Marketing.yaml b/workflows/Marketing.yaml new file mode 100644 index 0000000000..6293a3b616 --- /dev/null +++ b/workflows/Marketing.yaml @@ -0,0 +1,46 @@ +# +# This workflow demonstrates a single agent interaction based on user input. +# +# Any Foundry Agent may be used to provide the response. +# See: ./setup/QuestionAgent.yaml +# +kind: AdaptiveDialog +beginDialog: + + kind: OnActivity + id: workflow_demo + actions: + + - kind: InvokeAzureAgent + id: invoke_analyst + conversationId: =System.ConversationId + agent: + name: =Env.FOUNDRY_AGENT_ANSWER + input: + messages: =[UserMessage(System.LastMessageText)] + additionalInstructions: |- + You are a marketing analyst. Given a product description, identify: + - Key features + - Target audience + - Unique selling points + + - kind: InvokeAzureAgent + id: invoke_writer + conversationId: =System.ConversationId + agent: + name: =Env.FOUNDRY_AGENT_ANSWER + input: + additionalInstructions: |- + You are a marketing copywriter. Given a block of text describing features, audience, and USPs, + compose a compelling marketing copy (like a newsletter section) that highlights these points. + Output should be short (around 150 words), output just the copy as a single text block. + + - kind: InvokeAzureAgent + id: invoke_editor + conversationId: =System.ConversationId + agent: + name: =Env.FOUNDRY_AGENT_ANSWER + input: + additionalInstructions: |- + You are an editor. Given the draft copy, correct grammar, improve clarity, ensure consistent tone, + give format and make it polished. Output the final improved copy as a single text block. \ No newline at end of file diff --git a/workflows/MathChat.yaml b/workflows/MathChat.yaml index 117a241d02..581bc0d5c2 100644 --- a/workflows/MathChat.yaml +++ b/workflows/MathChat.yaml @@ -36,19 +36,25 @@ beginDialog: variable: Topic.Project value: =System.LastMessage.Text - - kind: AnswerQuestionWithAI + - kind: InvokeAzureAgent id: question_student - userInput: =Topic.Project - additionalInstructions: {Env.FOUNDRY_AGENT_STUDENT} + conversationId: =System.ConversationId + agent: + name: =Env.FOUNDRY_AGENT_STUDENT + input: + messages: =[UserMessage(Topic.Project)] - kind: ResetVariable id: reset_project variable: Topic.Project - - kind: AnswerQuestionWithAI + - kind: InvokeAzureAgent id: question_teacher - userInput: ="" - additionalInstructions: {Env.FOUNDRY_AGENT_TEACHER} + conversationId: =System.ConversationId + agent: + name: =Env.FOUNDRY_AGENT_TEACHER + output: + messages: Topic.TeacherResponse - kind: SetVariable id: set_count_increment @@ -59,7 +65,7 @@ beginDialog: id: check_completion conditions: - - condition: =!IsBlank(Find("congratulations", Lower(System.LastMessage.Text))) + - condition: =!IsBlank(Find("CONGRATULATIONS", Upper(Topic.TeacherResponse.Text))) id: check_turn_done actions: diff --git a/workflows/Question.yaml b/workflows/Question.yaml deleted file mode 100644 index 22a9d7b6d4..0000000000 --- a/workflows/Question.yaml +++ /dev/null @@ -1,24 +0,0 @@ -# -# This workflow demonstrates a single agent interaction based on user input. -# -# Any Foundry Agent may be used to provide the response. -# See: ./setup/QuestionAgent.yaml -# -kind: AdaptiveDialog -beginDialog: - - kind: OnActivity - id: workflow_demo - actions: - - # Respond with input - - kind: SendActivity - id: sendActivity_demo - activity: "Working..." - - # Use AI to answer the question - - kind: AnswerQuestionWithAI - id: question_demo - variable: Topic.Answer - userInput: =System.LastMessage.Text - additionalInstructions: {Env.FOUNDRY_AGENT_ANSWER}