mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Compare commits
35
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5fa153642e | ||
|
|
29cb87b805 | ||
|
|
350cdfc1cd | ||
|
|
e032fe3993 | ||
|
|
d6fdb91480 | ||
|
|
3730db3e94 | ||
|
|
00a124dae6 | ||
|
|
7e1fd67e76 | ||
|
|
2c9cf6f59d | ||
|
|
7238cde5af | ||
|
|
4cd81fe8e7 | ||
|
|
2f53ce4abd | ||
|
|
9fe4a61dd0 | ||
|
|
a02b82f022 | ||
|
|
8967269d3e | ||
|
|
d16d56b555 | ||
|
|
f17bf0a502 | ||
|
|
1b5e20b5b0 | ||
|
|
2397795c1d | ||
|
|
15afc966ce | ||
|
|
988623e7b8 | ||
|
|
74864f353d | ||
|
|
3d31a4a204 | ||
|
|
7e891fab39 | ||
|
|
c341ee7ed2 | ||
|
|
f5abbc67ae | ||
|
|
a36e183600 | ||
|
|
c2c8ec3d4e | ||
|
|
1c5e607a1f | ||
|
|
334d52f300 | ||
|
|
523127fbf4 | ||
|
|
5902bcb10a | ||
|
|
eb049c43a6 | ||
|
|
362652b966 | ||
|
|
127bf68748 |
@@ -160,6 +160,7 @@ jobs:
|
||||
AzureAI__DeploymentName: ${{ vars.AZUREAI__DEPLOYMENTNAME }}
|
||||
AzureAI__BingConnectionId: ${{ vars.AZUREAI__BINGCONECTIONID }}
|
||||
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT }}
|
||||
FOUNDRY_MEDIA_DEPLOYMENT_NAME: ${{ vars.FOUNDRY_MEDIA_DEPLOYMENT_NAME }}
|
||||
FOUNDRY_MODEL_DEPLOYMENT_NAME: ${{ vars.FOUNDRY_MODEL_DEPLOYMENT_NAME }}
|
||||
FOUNDRY_CONNECTION_GROUNDING_TOOL: ${{ vars.FOUNDRY_CONNECTION_GROUNDING_TOOL }}
|
||||
|
||||
|
||||
@@ -119,22 +119,35 @@ if __name__ == "__main__":
|
||||
|
||||
### Basic Agent - .NET
|
||||
|
||||
Create a simple Agent, using OpenAI Responses, that writes a haiku about the Microsoft Agent Framework
|
||||
|
||||
```c#
|
||||
// dotnet add package Microsoft.Agents.AI.OpenAI --prerelease
|
||||
using System;
|
||||
using OpenAI;
|
||||
|
||||
// Replace the <apikey> with your OpenAI API key.
|
||||
var agent = new OpenAIClient("<apikey>")
|
||||
.GetOpenAIResponseClient("gpt-4o-mini")
|
||||
.CreateAIAgent(name: "HaikuBot", instructions: "You are an upbeat assistant that writes beautifully.");
|
||||
|
||||
Console.WriteLine(await agent.RunAsync("Write a haiku about Microsoft Agent Framework."));
|
||||
```
|
||||
|
||||
Create a simple Agent, using Azure OpenAI Responses with token based auth, that writes a haiku about the Microsoft Agent Framework
|
||||
|
||||
```c#
|
||||
// dotnet add package Microsoft.Agents.AI.OpenAI --prerelease
|
||||
// dotnet add package Azure.AI.OpenAI
|
||||
// dotnet add package Azure.Identity
|
||||
// Use `az login` to authenticate with Azure CLI
|
||||
using System;
|
||||
using Azure.AI.OpenAI;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
using OpenAI;
|
||||
|
||||
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT")!;
|
||||
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME")!;
|
||||
|
||||
var agent = new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential())
|
||||
.GetOpenAIResponseClient(deploymentName)
|
||||
// Replace <resource> and gpt-4o-mini with your Azure OpenAI resource name and deployment name.
|
||||
var agent = new OpenAIClient(
|
||||
new BearerTokenPolicy(new AzureCliCredential(), "https://ai.azure.com/.default"),
|
||||
new OpenAIClientOptions() { Endpoint = new Uri("https://<resource>.openai.azure.com/openai/v1") })
|
||||
.GetOpenAIResponseClient("gpt-4o-mini")
|
||||
.CreateAIAgent(name: "HaikuBot", instructions: "You are an upbeat assistant that writes beautifully.");
|
||||
|
||||
Console.WriteLine(await agent.RunAsync("Write a haiku about Microsoft Agent Framework."));
|
||||
|
||||
@@ -10,58 +10,47 @@
|
||||
<AspireAppHostSdkVersion>9.5.1</AspireAppHostSdkVersion>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<!-- Azure.* -->
|
||||
<PackageVersion Include="Aspire.Azure.AI.OpenAI" Version="9.5.0-preview.1.25474.7" />
|
||||
<!-- Aspire.* -->
|
||||
<PackageVersion Include="Aspire.Azure.AI.OpenAI" Version="9.5.1-preview.1.25502.11" />
|
||||
<PackageVersion Include="Aspire.Hosting.AppHost" Version="$(AspireAppHostSdkVersion)" />
|
||||
<PackageVersion Include="Aspire.Hosting.Azure.CognitiveServices" Version="$(AspireAppHostSdkVersion)" />
|
||||
<PackageVersion Include="Aspire.Microsoft.Azure.Cosmos" Version="$(AspireAppHostSdkVersion)" />
|
||||
<PackageVersion Include="Aspire.Hosting.Testing" Version="$(AspireAppHostSdkVersion)" />
|
||||
<PackageVersion Include="Azure.AI.Agents.Persistent" Version="1.2.0-beta.5" />
|
||||
<PackageVersion Include="Azure.AI.OpenAI" Version="2.3.0-beta.2" />
|
||||
<PackageVersion Include="Azure.Identity" Version="1.16.0" />
|
||||
<PackageVersion Include="Azure.Monitor.OpenTelemetry.Exporter" Version="1.4.0" />
|
||||
<PackageVersion Include="CommunityToolkit.Aspire.OllamaSharp" Version="9.8.0" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="9.0.9" />
|
||||
<PackageVersion Include="Microsoft.Extensions.AI.AzureAIInference" Version="9.9.1-preview.1.25474.6" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Http.Resilience" Version="9.9.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.ServiceDiscovery" Version="$(AspireAppHostSdkVersion)" />
|
||||
<PackageVersion Include="OpenTelemetry.Api" Version="1.12.0" />
|
||||
<PackageVersion Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.12.0" />
|
||||
<PackageVersion Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.12.0" />
|
||||
<PackageVersion Include="OpenTelemetry.Instrumentation.Http" Version="1.12.0" />
|
||||
<PackageVersion Include="OpenTelemetry.Instrumentation.Runtime" Version="1.12.0" />
|
||||
<PackageVersion Include="Microsoft.Azure.Cosmos" Version="3.53.1" />
|
||||
<!-- Newtonsoft (Required by CosmosClient) -->
|
||||
<PackageVersion Include="Newtonsoft.Json" Version="13.0.4" />
|
||||
<PackageVersion Include="Swashbuckle.AspNetCore.SwaggerUI" Version="9.0.4" />
|
||||
<!-- Azure.* -->
|
||||
<PackageVersion Include="Azure.AI.Agents.Persistent" Version="1.2.0-beta.5" />
|
||||
<PackageVersion Include="Azure.AI.OpenAI" Version="2.5.0-beta.1" />
|
||||
<PackageVersion Include="Azure.Identity" Version="1.17.0" />
|
||||
<PackageVersion Include="Azure.Monitor.OpenTelemetry.Exporter" Version="1.4.0" />
|
||||
<!-- System.* -->
|
||||
<PackageVersion Include="System.Linq.Async" Version="6.0.3" />
|
||||
<PackageVersion Include="System.Net.ServerSentEvents" Version="9.0.9" />
|
||||
<PackageVersion Include="System.Text.Json" Version="9.0.9" />
|
||||
<PackageVersion Include="System.CodeDom" Version="9.0.9" />
|
||||
<PackageVersion Include="System.Collections.Immutable" Version="9.0.9" />
|
||||
<PackageVersion Include="System.CommandLine" Version="2.0.0-rc.1.25451.107" />
|
||||
<PackageVersion Include="System.Diagnostics.DiagnosticSource" Version="9.0.9" />
|
||||
<PackageVersion Include="System.Net.ServerSentEvents" Version="9.0.9" />
|
||||
<PackageVersion Include="System.Text.Json" Version="9.0.9" />
|
||||
<PackageVersion Include="System.Threading.Channels" Version="9.0.9" />
|
||||
<PackageVersion Include="System.Threading.Tasks.Extensions" Version="4.6.3" />
|
||||
<PackageVersion Include="Microsoft.Bcl.HashCode" Version="6.0.0" />
|
||||
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="9.0.9" />
|
||||
<!-- OpenTelemetry -->
|
||||
<PackageVersion Include="OpenTelemetry" Version="1.12.0" />
|
||||
<PackageVersion Include="OpenTelemetry.Api" Version="1.12.0" />
|
||||
<PackageVersion Include="OpenTelemetry.Exporter.Console" Version="1.12.0" />
|
||||
<PackageVersion Include="OpenTelemetry.Exporter.InMemory" Version="1.12.0" />
|
||||
<PackageVersion Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.12.0" />
|
||||
<PackageVersion Include="OpenTelemetry.Extensions.Hosting" Version="1.12.0" />
|
||||
<PackageVersion Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.12.0" />
|
||||
<PackageVersion Include="OpenTelemetry.Instrumentation.Http" Version="1.12.0" />
|
||||
<PackageVersion Include="OpenTelemetry.Instrumentation.Runtime" Version="1.12.0" />
|
||||
<!-- Microsoft.AspNetCore.* -->
|
||||
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="9.0.9" />
|
||||
<PackageVersion Include="Swashbuckle.AspNetCore.SwaggerUI" Version="9.0.4" />
|
||||
<!-- Microsoft.Extensions.* -->
|
||||
<PackageVersion Include="Microsoft.Bcl.HashCode" Version="6.0.0" />
|
||||
<PackageVersion Include="Microsoft.Agents.AI" Version="1.0.0-preview.251001.3" />
|
||||
<PackageVersion Include="Microsoft.Agents.AI.Abstractions" Version="1.0.0-preview.251001.3" />
|
||||
<PackageVersion Include="Microsoft.Agents.AI.AzureAI" Version="1.0.0-preview.251001.3" />
|
||||
<PackageVersion Include="Microsoft.Agents.AI.OpenAI" Version="1.0.0-preview.251001.3" />
|
||||
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="9.0.9" />
|
||||
<PackageVersion Include="OpenAI" Version="2.4.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.AI" Version="9.9.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.AI.Abstractions" Version="9.9.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.AI.OpenAI" Version="9.9.0-preview.1.25458.4" />
|
||||
<PackageVersion Include="Microsoft.Extensions.AI.AzureAIInference" Version="9.9.1-preview.1.25474.6" />
|
||||
<PackageVersion Include="Microsoft.Extensions.AI.OpenAI" Version="9.9.1-preview.1.25474.6" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="9.0.9" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.9" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.9" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="9.0.9" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="9.0.9" />
|
||||
@@ -69,29 +58,30 @@
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.9" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.9" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="9.0.9" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Http.Resilience" Version="9.9.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.9" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.9" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging.Console" Version="9.0.9" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging.Testing" Version="9.0.9" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options" Version="9.0.9" />
|
||||
<PackageVersion Include="Microsoft.Extensions.ServiceDiscovery" Version="$(AspireAppHostSdkVersion)" />
|
||||
<!-- Vector Stores -->
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.InMemory" Version="1.65.0-preview" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel" Version="1.65.0" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Agents.Core" Version="1.65.0" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Agents.OpenAI" Version="1.65.0-preview" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Agents.AzureAI" Version="1.65.0-preview" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel" Version="1.66.0" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.InMemory" Version="1.66.0-preview" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Agents.Core" Version="1.66.0" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Agents.OpenAI" Version="1.66.0-preview" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Agents.AzureAI" Version="1.66.0-preview" />
|
||||
<!-- Agent SDKs -->
|
||||
<PackageVersion Include="Microsoft.Agents.CopilotStudio.Client" Version="1.2.41" />
|
||||
<!-- A2A -->
|
||||
<PackageVersion Include="A2A" Version="0.3.1-preview" />
|
||||
<PackageVersion Include="A2A.AspNetCore" Version="0.3.1-preview" />
|
||||
<!-- MCP -->
|
||||
<PackageVersion Include="ModelContextProtocol" Version="0.4.0-preview.1" />
|
||||
<PackageVersion Include="ModelContextProtocol" Version="0.4.0-preview.2" />
|
||||
<!-- Inference SDKs -->
|
||||
<PackageVersion Include="Anthropic.SDK" Version="5.5.3" />
|
||||
<PackageVersion Include="AWSSDK.Extensions.Bedrock.MEAI" Version="4.0.3.5" />
|
||||
<PackageVersion Include="Anthropic.SDK" Version="5.6.0" />
|
||||
<PackageVersion Include="AWSSDK.Extensions.Bedrock.MEAI" Version="4.0.3.6" />
|
||||
<PackageVersion Include="Microsoft.ML.OnnxRuntimeGenAI" Version="0.9.2" />
|
||||
<PackageVersion Include="OllamaSharp" Version="5.4.7" />
|
||||
<PackageVersion Include="OpenAI" Version="2.5.0" />
|
||||
<!-- Identity -->
|
||||
<PackageVersion Include="Microsoft.Identity.Client.Extensions.Msal" Version="4.77.1" />
|
||||
<!-- Workflows -->
|
||||
@@ -99,12 +89,14 @@
|
||||
<PackageVersion Include="Microsoft.Bot.ObjectModel.Json" Version="1.2025.1003.2" />
|
||||
<PackageVersion Include="Microsoft.Bot.ObjectModel.PowerFx" Version="1.2025.1003.2" />
|
||||
<PackageVersion Include="Microsoft.PowerFx.Interpreter" Version="1.4.0" />
|
||||
<!-- Community -->
|
||||
<PackageVersion Include="System.Linq.Async" Version="6.0.3" />
|
||||
<!-- Test -->
|
||||
<PackageVersion Include="FluentAssertions" Version="8.7.0" />
|
||||
<PackageVersion Include="FluentAssertions" Version="8.7.1" />
|
||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.0.0" />
|
||||
<PackageVersion Include="Moq" Version="[4.18.4]" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Agents.Abstractions" Version="1.65.0" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Agents.Yaml" Version="1.65.0-beta" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Agents.Abstractions" Version="1.66.0" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Agents.Yaml" Version="1.66.0-beta" />
|
||||
<PackageVersion Include="xunit" Version="2.9.3" />
|
||||
<PackageVersion Include="xunit.abstractions" Version="2.0.3" />
|
||||
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.3" />
|
||||
|
||||
@@ -113,6 +113,7 @@
|
||||
<Project Path="samples/GettingStarted/Workflows/HumanInTheLoop/HumanInTheLoopBasic/HumanInTheLoopBasic.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/GettingStarted/Workflows/Observability/">
|
||||
<Project Path="samples/GettingStarted/Workflows/Observability/ApplicationInsights/ApplicationInsights.csproj" />
|
||||
<Project Path="samples/GettingStarted/Workflows/Observability/AspireDashboard/AspireDashboard.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/GettingStarted/Workflows/Visualization/">
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
<PropertyGroup>
|
||||
<!-- Central version prefix - applies to all nuget packages. -->
|
||||
<VersionPrefix>1.0.0</VersionPrefix>
|
||||
<PackageVersion Condition="'$(VersionSuffix)' != ''">$(VersionPrefix)-$(VersionSuffix).251007.1</PackageVersion>
|
||||
<PackageVersion Condition="'$(VersionSuffix)' == ''">$(VersionPrefix)-preview.251007.1</PackageVersion>
|
||||
<PackageVersion Condition="'$(VersionSuffix)' != ''">$(VersionPrefix)-$(VersionSuffix).251009.1</PackageVersion>
|
||||
<PackageVersion Condition="'$(VersionSuffix)' == ''">$(VersionPrefix)-preview.251009.1</PackageVersion>
|
||||
<GitTag>1.0.0-preview.251009.1</GitTag>
|
||||
|
||||
<Configurations>Debug;Release;Publish</Configurations>
|
||||
<IsPackable>true</IsPackable>
|
||||
|
||||
+2
-5
@@ -14,9 +14,6 @@ var endpoint = Environment.GetEnvironmentVariable("AZURE_FOUNDRY_OPENAI_ENDPOINT
|
||||
var apiKey = Environment.GetEnvironmentVariable("AZURE_FOUNDRY_OPENAI_APIKEY");
|
||||
var model = Environment.GetEnvironmentVariable("AZURE_FOUNDRY_MODEL_DEPLOYMENT") ?? "Phi-4-mini-instruct";
|
||||
|
||||
const string JokerName = "Joker";
|
||||
const string JokerInstructions = "You are good at telling jokes.";
|
||||
|
||||
// Since we are using the OpenAI Client SDK, we need to override the default endpoint to point to Azure Foundry.
|
||||
var clientOptions = new OpenAIClientOptions() { Endpoint = new Uri(endpoint) };
|
||||
|
||||
@@ -26,8 +23,8 @@ OpenAIClient client = string.IsNullOrWhiteSpace(apiKey)
|
||||
: new OpenAIClient(new ApiKeyCredential(apiKey), clientOptions);
|
||||
|
||||
AIAgent agent = client
|
||||
.GetChatClient(model)
|
||||
.CreateAIAgent(JokerInstructions, JokerName);
|
||||
.GetChatClient(model)
|
||||
.CreateAIAgent(instructions: "You are good at telling jokes.", name: "Joker");
|
||||
|
||||
// Invoke the agent and output the text result.
|
||||
Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate."));
|
||||
|
||||
+1
-4
@@ -10,14 +10,11 @@ using OpenAI;
|
||||
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
|
||||
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
|
||||
|
||||
const string JokerName = "Joker";
|
||||
const string JokerInstructions = "You are good at telling jokes.";
|
||||
|
||||
AIAgent agent = new AzureOpenAIClient(
|
||||
new Uri(endpoint),
|
||||
new AzureCliCredential())
|
||||
.GetChatClient(deploymentName)
|
||||
.CreateAIAgent(JokerInstructions, JokerName);
|
||||
.CreateAIAgent(instructions: "You are good at telling jokes.", name: "Joker");
|
||||
|
||||
// Invoke the agent and output the text result.
|
||||
Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate."));
|
||||
|
||||
+1
-4
@@ -10,14 +10,11 @@ using OpenAI;
|
||||
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
|
||||
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
|
||||
|
||||
const string JokerName = "Joker";
|
||||
const string JokerInstructions = "You are good at telling jokes.";
|
||||
|
||||
AIAgent agent = new AzureOpenAIClient(
|
||||
new Uri(endpoint),
|
||||
new AzureCliCredential())
|
||||
.GetOpenAIResponseClient(deploymentName)
|
||||
.CreateAIAgent(JokerInstructions, JokerName);
|
||||
.CreateAIAgent(instructions: "You are good at telling jokes.", name: "Joker");
|
||||
|
||||
// Invoke the agent and output the text result.
|
||||
Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate."));
|
||||
|
||||
@@ -10,12 +10,9 @@ using Microsoft.ML.OnnxRuntimeGenAI;
|
||||
// E.g. C:\repos\Phi-4-mini-instruct-onnx\cpu_and_mobile\cpu-int4-rtn-block-32-acc-level-4
|
||||
var modelPath = Environment.GetEnvironmentVariable("ONNX_MODEL_PATH") ?? throw new InvalidOperationException("ONNX_MODEL_PATH is not set.");
|
||||
|
||||
const string JokerName = "Joker";
|
||||
const string JokerInstructions = "You are good at telling jokes.";
|
||||
|
||||
// Get a chat client for ONNX and use it to construct an AIAgent.
|
||||
using OnnxRuntimeGenAIChatClient chatClient = new(modelPath);
|
||||
AIAgent agent = chatClient.CreateAIAgent(JokerInstructions, JokerName);
|
||||
AIAgent agent = chatClient.CreateAIAgent(instructions: "You are good at telling jokes.", name: "Joker");
|
||||
|
||||
// Invoke the agent and output the text result.
|
||||
Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate."));
|
||||
|
||||
@@ -9,12 +9,9 @@ using OllamaSharp;
|
||||
var endpoint = Environment.GetEnvironmentVariable("OLLAMA_ENDPOINT") ?? throw new InvalidOperationException("OLLAMA_ENDPOINT is not set.");
|
||||
var modelName = Environment.GetEnvironmentVariable("OLLAMA_MODEL_NAME") ?? throw new InvalidOperationException("OLLAMA_MODEL_NAME is not set.");
|
||||
|
||||
const string JokerName = "Joker";
|
||||
const string JokerInstructions = "You are good at telling jokes.";
|
||||
|
||||
// Get a chat client for Ollama and use it to construct an AIAgent.
|
||||
AIAgent agent = new OllamaApiClient(new Uri(endpoint), modelName)
|
||||
.CreateAIAgent(JokerInstructions, JokerName);
|
||||
.CreateAIAgent(instructions: "You are good at telling jokes.", name: "Joker");
|
||||
|
||||
// Invoke the agent and output the text result.
|
||||
Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate."));
|
||||
|
||||
+1
-4
@@ -8,13 +8,10 @@ using OpenAI;
|
||||
var apiKey = Environment.GetEnvironmentVariable("OPENAI_APIKEY") ?? throw new InvalidOperationException("OPENAI_APIKEY is not set.");
|
||||
var model = Environment.GetEnvironmentVariable("OPENAI_MODEL") ?? "gpt-4o-mini";
|
||||
|
||||
const string JokerName = "Joker";
|
||||
const string JokerInstructions = "You are good at telling jokes.";
|
||||
|
||||
AIAgent agent = new OpenAIClient(
|
||||
apiKey)
|
||||
.GetChatClient(model)
|
||||
.CreateAIAgent(JokerInstructions, JokerName);
|
||||
.CreateAIAgent(instructions: "You are good at telling jokes.", name: "Joker");
|
||||
|
||||
// Invoke the agent and output the text result.
|
||||
Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate."));
|
||||
|
||||
@@ -8,13 +8,10 @@ using OpenAI;
|
||||
var apiKey = Environment.GetEnvironmentVariable("OPENAI_APIKEY") ?? throw new InvalidOperationException("OPENAI_APIKEY is not set.");
|
||||
var model = Environment.GetEnvironmentVariable("OPENAI_MODEL") ?? "gpt-4o-mini";
|
||||
|
||||
const string JokerName = "Joker";
|
||||
const string JokerInstructions = "You are good at telling jokes.";
|
||||
|
||||
AIAgent agent = new OpenAIClient(
|
||||
apiKey)
|
||||
.GetOpenAIResponseClient(model)
|
||||
.CreateAIAgent(JokerInstructions, JokerName);
|
||||
.CreateAIAgent(instructions: "You are good at telling jokes.", name: "Joker");
|
||||
|
||||
// Invoke the agent and output the text result.
|
||||
Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate."));
|
||||
|
||||
+1
-4
@@ -10,12 +10,9 @@ using OpenAI.Chat;
|
||||
var apiKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY") ?? throw new InvalidOperationException("OPENAI_API_KEY is not set.");
|
||||
var model = Environment.GetEnvironmentVariable("OPENAI_MODEL") ?? "gpt-4o-mini";
|
||||
|
||||
const string JokerName = "Joker";
|
||||
const string JokerInstructions = "You are good at telling jokes.";
|
||||
|
||||
AIAgent agent = new OpenAIClient(apiKey)
|
||||
.GetChatClient(model)
|
||||
.CreateAIAgent(JokerInstructions, JokerName);
|
||||
.CreateAIAgent(instructions: "You are good at telling jokes.", name: "Joker");
|
||||
|
||||
UserChatMessage chatMessage = new("Tell me a joke about a pirate.");
|
||||
|
||||
|
||||
@@ -10,14 +10,11 @@ using OpenAI;
|
||||
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
|
||||
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
|
||||
|
||||
const string JokerName = "Joker";
|
||||
const string JokerInstructions = "You are good at telling jokes.";
|
||||
|
||||
AIAgent agent = new AzureOpenAIClient(
|
||||
new Uri(endpoint),
|
||||
new AzureCliCredential())
|
||||
.GetChatClient(deploymentName)
|
||||
.CreateAIAgent(JokerInstructions, JokerName);
|
||||
.GetChatClient(deploymentName)
|
||||
.CreateAIAgent(instructions: "You are good at telling jokes.", name: "Joker");
|
||||
|
||||
// Invoke the agent and output the text result.
|
||||
Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate."));
|
||||
|
||||
@@ -10,14 +10,11 @@ using OpenAI;
|
||||
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
|
||||
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
|
||||
|
||||
const string JokerName = "Joker";
|
||||
const string JokerInstructions = "You are good at telling jokes.";
|
||||
|
||||
AIAgent agent = new AzureOpenAIClient(
|
||||
new Uri(endpoint),
|
||||
new AzureCliCredential())
|
||||
.GetChatClient(deploymentName)
|
||||
.CreateAIAgent(JokerInstructions, JokerName);
|
||||
.GetChatClient(deploymentName)
|
||||
.CreateAIAgent(instructions: "You are good at telling jokes.", name: "Joker");
|
||||
|
||||
// Invoke the agent with a multi-turn conversation, where the context is preserved in the thread object.
|
||||
AgentThread thread = agent.GetNewThread();
|
||||
|
||||
@@ -21,8 +21,8 @@ static string GetWeather([Description("The location to get the weather for.")] s
|
||||
AIAgent agent = new AzureOpenAIClient(
|
||||
new Uri(endpoint),
|
||||
new AzureCliCredential())
|
||||
.GetChatClient(deploymentName)
|
||||
.CreateAIAgent(instructions: "You are a helpful assistant", tools: [AIFunctionFactory.Create(GetWeather)]);
|
||||
.GetChatClient(deploymentName)
|
||||
.CreateAIAgent(instructions: "You are a helpful assistant", tools: [AIFunctionFactory.Create(GetWeather)]);
|
||||
|
||||
// Non-streaming agent interaction with function tools.
|
||||
Console.WriteLine(await agent.RunAsync("What is the weather like in Amsterdam?"));
|
||||
|
||||
+2
-2
@@ -25,8 +25,8 @@ static string GetWeather([Description("The location to get the weather for.")] s
|
||||
AIAgent agent = new AzureOpenAIClient(
|
||||
new Uri(endpoint),
|
||||
new AzureCliCredential())
|
||||
.GetChatClient(deploymentName)
|
||||
.CreateAIAgent(instructions: "You are a helpful assistant", tools: [new ApprovalRequiredAIFunction(AIFunctionFactory.Create(GetWeather))]);
|
||||
.GetChatClient(deploymentName)
|
||||
.CreateAIAgent(instructions: "You are a helpful assistant", tools: [new ApprovalRequiredAIFunction(AIFunctionFactory.Create(GetWeather))]);
|
||||
|
||||
// Call the agent and check if there are any user input requests to handle.
|
||||
AgentThread thread = agent.GetNewThread();
|
||||
|
||||
@@ -11,15 +11,12 @@ using OpenAI;
|
||||
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
|
||||
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
|
||||
|
||||
const string JokerName = "Joker";
|
||||
const string JokerInstructions = "You are good at telling jokes.";
|
||||
|
||||
// Create the agent
|
||||
AIAgent agent = new AzureOpenAIClient(
|
||||
new Uri(endpoint),
|
||||
new AzureCliCredential())
|
||||
.GetChatClient(deploymentName)
|
||||
.CreateAIAgent(JokerInstructions, JokerName);
|
||||
.GetChatClient(deploymentName)
|
||||
.CreateAIAgent(instructions: "You are good at telling jokes.", name: "Joker");
|
||||
|
||||
// Start a new thread for the agent conversation.
|
||||
AgentThread thread = agent.GetNewThread();
|
||||
|
||||
+13
-16
@@ -17,9 +17,6 @@ using SampleApp;
|
||||
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
|
||||
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
|
||||
|
||||
const string JokerName = "Joker";
|
||||
const string JokerInstructions = "You are good at telling jokes.";
|
||||
|
||||
// Create a vector store to store the chat messages in.
|
||||
// Replace this with a vector store implementation of your choice if you want to persist the chat history to disk.
|
||||
VectorStore vectorStore = new InMemoryVectorStore();
|
||||
@@ -28,19 +25,19 @@ VectorStore vectorStore = new InMemoryVectorStore();
|
||||
AIAgent agent = new AzureOpenAIClient(
|
||||
new Uri(endpoint),
|
||||
new AzureCliCredential())
|
||||
.GetChatClient(deploymentName)
|
||||
.CreateAIAgent(new ChatClientAgentOptions
|
||||
{
|
||||
Name = JokerName,
|
||||
Instructions = JokerInstructions,
|
||||
ChatMessageStoreFactory = ctx =>
|
||||
{
|
||||
// Create a new chat message store for this agent that stores the messages in a vector store.
|
||||
// Each thread must get its own copy of the VectorChatMessageStore, since the store
|
||||
// also contains the id that the thread is stored under.
|
||||
return new VectorChatMessageStore(vectorStore, ctx.SerializedState, ctx.JsonSerializerOptions);
|
||||
}
|
||||
});
|
||||
.GetChatClient(deploymentName)
|
||||
.CreateAIAgent(new ChatClientAgentOptions
|
||||
{
|
||||
Instructions = "You are good at telling jokes.",
|
||||
Name = "Joker",
|
||||
ChatMessageStoreFactory = ctx =>
|
||||
{
|
||||
// Create a new chat message store for this agent that stores the messages in a vector store.
|
||||
// Each thread must get its own copy of the VectorChatMessageStore, since the store
|
||||
// also contains the id that the thread is stored under.
|
||||
return new VectorChatMessageStore(vectorStore, ctx.SerializedState, ctx.JsonSerializerOptions);
|
||||
}
|
||||
});
|
||||
|
||||
// Start a new thread for the agent conversation.
|
||||
AgentThread thread = agent.GetNewThread();
|
||||
|
||||
+1
@@ -11,6 +11,7 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.AI.OpenAI" />
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
<PackageReference Include="Azure.Monitor.OpenTelemetry.Exporter" />
|
||||
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
|
||||
<PackageReference Include="OpenTelemetry" />
|
||||
<PackageReference Include="OpenTelemetry.Exporter.Console" />
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
using Azure.AI.OpenAI;
|
||||
using Azure.Identity;
|
||||
using Azure.Monitor.OpenTelemetry.Exporter;
|
||||
using Microsoft.Agents.AI;
|
||||
using OpenAI;
|
||||
using OpenTelemetry;
|
||||
@@ -11,22 +12,24 @@ using OpenTelemetry.Trace;
|
||||
|
||||
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
|
||||
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
|
||||
|
||||
const string JokerName = "Joker";
|
||||
const string JokerInstructions = "You are good at telling jokes.";
|
||||
var applicationInsightsConnectionString = Environment.GetEnvironmentVariable("APPLICATIONINSIGHTS_CONNECTION_STRING");
|
||||
|
||||
// Create TracerProvider with console exporter
|
||||
// This will output the telemetry data to the console.
|
||||
string sourceName = Guid.NewGuid().ToString("N");
|
||||
using var tracerProvider = Sdk.CreateTracerProviderBuilder()
|
||||
var tracerProviderBuilder = Sdk.CreateTracerProviderBuilder()
|
||||
.AddSource(sourceName)
|
||||
.AddConsoleExporter()
|
||||
.Build();
|
||||
.AddConsoleExporter();
|
||||
if (!string.IsNullOrWhiteSpace(applicationInsightsConnectionString))
|
||||
{
|
||||
tracerProviderBuilder.AddAzureMonitorTraceExporter(options => options.ConnectionString = applicationInsightsConnectionString);
|
||||
}
|
||||
using var tracerProvider = tracerProviderBuilder.Build();
|
||||
|
||||
// Create the agent, and enable OpenTelemetry instrumentation.
|
||||
AIAgent agent = new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential())
|
||||
.GetChatClient(deploymentName)
|
||||
.CreateAIAgent(JokerInstructions, JokerName)
|
||||
.CreateAIAgent(instructions: "You are good at telling jokes.", name: "Joker")
|
||||
.AsBuilder()
|
||||
.UseOpenTelemetry(sourceName: sourceName)
|
||||
.Build();
|
||||
|
||||
@@ -18,9 +18,8 @@ var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT
|
||||
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
|
||||
|
||||
// Add agent options to the service collection.
|
||||
const string JokerName = "Joker";
|
||||
const string JokerInstructions = "You are good at telling jokes.";
|
||||
builder.Services.AddSingleton(new ChatClientAgentOptions(JokerInstructions, JokerName));
|
||||
builder.Services.AddSingleton(
|
||||
new ChatClientAgentOptions(instructions: "You are good at telling jokes.", name: "Joker"));
|
||||
|
||||
// Add a chat client to the service collection.
|
||||
builder.Services.AddKeyedChatClient("AzureOpenAI", (sp) => new AzureOpenAIClient(
|
||||
|
||||
@@ -12,18 +12,14 @@ using ModelContextProtocol.Server;
|
||||
var endpoint = Environment.GetEnvironmentVariable("AZURE_FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("AZURE_FOUNDRY_PROJECT_ENDPOINT is not set.");
|
||||
var deploymentName = Environment.GetEnvironmentVariable("AZURE_FOUNDRY_PROJECT_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
|
||||
|
||||
const string JokerName = "Joker";
|
||||
const string JokerDescription = "An agent that tells jokes.";
|
||||
const string JokerInstructions = "You are good at telling jokes, and you always start each joke with 'Aye aye, captain!'.";
|
||||
|
||||
var persistentAgentsClient = new PersistentAgentsClient(endpoint, new AzureCliCredential());
|
||||
|
||||
// Create a server side persistent agent
|
||||
var agentMetadata = await persistentAgentsClient.Administration.CreateAgentAsync(
|
||||
model: deploymentName,
|
||||
name: JokerName,
|
||||
description: JokerDescription,
|
||||
instructions: JokerInstructions);
|
||||
instructions: "You are good at telling jokes, and you always start each joke with 'Aye aye, captain!'.",
|
||||
name: "Joker",
|
||||
description: "An agent that tells jokes.");
|
||||
|
||||
// Retrieve the server side persistent agent as an AIAgent.
|
||||
AIAgent agent = await persistentAgentsClient.GetAIAgentAsync(agentMetadata.Value.Id);
|
||||
|
||||
@@ -31,8 +31,8 @@ AIAgent weatherAgent = new AzureOpenAIClient(
|
||||
AIAgent agent = new AzureOpenAIClient(
|
||||
new Uri(endpoint),
|
||||
new AzureCliCredential())
|
||||
.GetChatClient(deploymentName)
|
||||
.CreateAIAgent(instructions: "You are a helpful assistant who responds in French.", tools: [weatherAgent.AsAIFunction()]);
|
||||
.GetChatClient(deploymentName)
|
||||
.CreateAIAgent(instructions: "You are a helpful assistant who responds in French.", tools: [weatherAgent.AsAIFunction()]);
|
||||
|
||||
// Invoke the agent and output the text result.
|
||||
Console.WriteLine(await agent.RunAsync("What is the weather like in Amsterdam?"));
|
||||
|
||||
@@ -27,16 +27,13 @@ services.AddSingleton<AgentPlugin>(); // The plugin depends on WeatherProvider a
|
||||
|
||||
IServiceProvider serviceProvider = services.BuildServiceProvider();
|
||||
|
||||
const string AgentName = "Assistant";
|
||||
const string AgentInstructions = "You are a helpful assistant that helps people find information.";
|
||||
|
||||
AIAgent agent = new AzureOpenAIClient(
|
||||
new Uri(endpoint),
|
||||
new AzureCliCredential())
|
||||
.GetChatClient(deploymentName)
|
||||
.CreateAIAgent(
|
||||
instructions: AgentInstructions,
|
||||
name: AgentName,
|
||||
.GetChatClient(deploymentName)
|
||||
.CreateAIAgent(
|
||||
instructions: "You are a helpful assistant that helps people find information.",
|
||||
name: "Assistant",
|
||||
tools: [.. serviceProvider.GetRequiredService<AgentPlugin>().AsAITools()],
|
||||
services: serviceProvider); // Pass the service provider to the agent so it will be available to plugin functions to resolve dependencies.
|
||||
|
||||
|
||||
@@ -14,20 +14,17 @@ using OpenAI;
|
||||
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
|
||||
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
|
||||
|
||||
const string JokerName = "Joker";
|
||||
const string JokerInstructions = "You are good at telling jokes.";
|
||||
|
||||
// Construct the agent, and provide a factory to create an in-memory chat message store with a reducer that keeps only the last 2 non-system messages.
|
||||
AIAgent agent = new AzureOpenAIClient(
|
||||
new Uri(endpoint),
|
||||
new AzureCliCredential())
|
||||
.GetChatClient(deploymentName)
|
||||
.CreateAIAgent(new ChatClientAgentOptions
|
||||
{
|
||||
Name = JokerName,
|
||||
Instructions = JokerInstructions,
|
||||
ChatMessageStoreFactory = ctx => new InMemoryChatMessageStore(new MessageCountingChatReducer(2), ctx.SerializedState, ctx.JsonSerializerOptions)
|
||||
});
|
||||
.GetChatClient(deploymentName)
|
||||
.CreateAIAgent(new ChatClientAgentOptions
|
||||
{
|
||||
Instructions = "You are good at telling jokes.",
|
||||
Name = "Joker",
|
||||
ChatMessageStoreFactory = ctx => new InMemoryChatMessageStore(new MessageCountingChatReducer(2), ctx.SerializedState, ctx.JsonSerializerOptions)
|
||||
});
|
||||
|
||||
AgentThread thread = agent.GetNewThread();
|
||||
|
||||
|
||||
+2
-5
@@ -9,9 +9,6 @@ using Microsoft.Agents.AI;
|
||||
var endpoint = Environment.GetEnvironmentVariable("AZURE_FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("AZURE_FOUNDRY_PROJECT_ENDPOINT is not set.");
|
||||
var model = Environment.GetEnvironmentVariable("AZURE_FOUNDRY_PROJECT_MODEL_ID") ?? "gpt-4.1-mini";
|
||||
|
||||
const string AgentName = "MicrosoftLearnAgent";
|
||||
const string AgentInstructions = "You answer questions by searching the Microsoft Learn content only.";
|
||||
|
||||
// Get a client to create/retrieve server side agents with.
|
||||
var persistentAgentsClient = new PersistentAgentsClient(endpoint, new AzureCliCredential());
|
||||
|
||||
@@ -24,8 +21,8 @@ mcpTool.AllowedTools.Add("microsoft_docs_search");
|
||||
// Create a server side persistent agent with the Azure.AI.Agents.Persistent SDK.
|
||||
var agentMetadata = await persistentAgentsClient.Administration.CreateAgentAsync(
|
||||
model: model,
|
||||
name: AgentName,
|
||||
instructions: AgentInstructions,
|
||||
name: "MicrosoftLearnAgent",
|
||||
instructions: "You answer questions by searching the Microsoft Learn content only.",
|
||||
tools: [mcpTool]);
|
||||
|
||||
// Retrieve an already created server side persistent agent as an AIAgent.
|
||||
|
||||
@@ -134,17 +134,17 @@ internal sealed class SloganWriterExecutor
|
||||
this._thread = this._agent.GetNewThread();
|
||||
}
|
||||
|
||||
public async ValueTask<SloganResult> HandleAsync(string message, IWorkflowContext context)
|
||||
public async ValueTask<SloganResult> HandleAsync(string message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var result = await this._agent.RunAsync(message, this._thread);
|
||||
var result = await this._agent.RunAsync(message, this._thread, cancellationToken: cancellationToken);
|
||||
|
||||
var sloganResult = JsonSerializer.Deserialize<SloganResult>(result.Text) ?? throw new InvalidOperationException("Failed to deserialize slogan result.");
|
||||
|
||||
await context.AddEventAsync(new SloganGeneratedEvent(sloganResult));
|
||||
await context.AddEventAsync(new SloganGeneratedEvent(sloganResult), cancellationToken);
|
||||
return sloganResult;
|
||||
}
|
||||
|
||||
public async ValueTask<SloganResult> HandleAsync(FeedbackResult message, IWorkflowContext context)
|
||||
public async ValueTask<SloganResult> HandleAsync(FeedbackResult message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var feedbackMessage = $"""
|
||||
Here is the feedback on your previous slogan:
|
||||
@@ -155,10 +155,10 @@ internal sealed class SloganWriterExecutor
|
||||
Please use this feedback to improve your slogan.
|
||||
""";
|
||||
|
||||
var result = await this._agent.RunAsync(feedbackMessage, this._thread);
|
||||
var result = await this._agent.RunAsync(feedbackMessage, this._thread, cancellationToken: cancellationToken);
|
||||
var sloganResult = JsonSerializer.Deserialize<SloganResult>(result.Text) ?? throw new InvalidOperationException("Failed to deserialize slogan result.");
|
||||
|
||||
await context.AddEventAsync(new SloganGeneratedEvent(sloganResult));
|
||||
await context.AddEventAsync(new SloganGeneratedEvent(sloganResult), cancellationToken);
|
||||
return sloganResult;
|
||||
}
|
||||
}
|
||||
@@ -205,7 +205,7 @@ internal sealed class FeedbackExecutor : ReflectingExecutor<FeedbackExecutor>, I
|
||||
this._thread = this._agent.GetNewThread();
|
||||
}
|
||||
|
||||
public async ValueTask HandleAsync(SloganResult message, IWorkflowContext context)
|
||||
public async ValueTask HandleAsync(SloganResult message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var sloganMessage = $"""
|
||||
Here is a slogan for the task '{message.Task}':
|
||||
@@ -213,24 +213,24 @@ internal sealed class FeedbackExecutor : ReflectingExecutor<FeedbackExecutor>, I
|
||||
Please provide feedback on this slogan, including comments, a rating from 1 to 10, and suggested actions for improvement.
|
||||
""";
|
||||
|
||||
var response = await this._agent.RunAsync(sloganMessage, this._thread);
|
||||
var response = await this._agent.RunAsync(sloganMessage, this._thread, cancellationToken: cancellationToken);
|
||||
var feedback = JsonSerializer.Deserialize<FeedbackResult>(response.Text) ?? throw new InvalidOperationException("Failed to deserialize feedback.");
|
||||
|
||||
await context.AddEventAsync(new FeedbackEvent(feedback));
|
||||
await context.AddEventAsync(new FeedbackEvent(feedback), cancellationToken);
|
||||
|
||||
if (feedback.Rating >= this.MinimumRating)
|
||||
{
|
||||
await context.YieldOutputAsync($"The following slogan was accepted:\n\n{message.Slogan}");
|
||||
await context.YieldOutputAsync($"The following slogan was accepted:\n\n{message.Slogan}", cancellationToken);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._attempts >= this.MaxAttempts)
|
||||
{
|
||||
await context.YieldOutputAsync($"The slogan was rejected after {this.MaxAttempts} attempts. Final slogan:\n\n{message.Slogan}");
|
||||
await context.YieldOutputAsync($"The slogan was rejected after {this.MaxAttempts} attempts. Final slogan:\n\n{message.Slogan}", cancellationToken);
|
||||
return;
|
||||
}
|
||||
|
||||
await context.SendMessageAsync(feedback);
|
||||
await context.SendMessageAsync(feedback, cancellationToken: cancellationToken);
|
||||
this._attempts++;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,13 +51,15 @@ internal static class WorkflowHelper
|
||||
/// </summary>
|
||||
/// <param name="message">The user message to process</param>
|
||||
/// <param name="context">Workflow context for accessing workflow services and adding events</param>
|
||||
public async ValueTask HandleAsync(List<ChatMessage> message, IWorkflowContext context)
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.
|
||||
/// The default is <see cref="CancellationToken.None"/>.</param>
|
||||
public async ValueTask HandleAsync(List<ChatMessage> message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Broadcast the message to all connected agents. Receiving agents will queue
|
||||
// the message but will not start processing until they receive a turn token.
|
||||
await context.SendMessageAsync(message);
|
||||
await context.SendMessageAsync(message, cancellationToken: cancellationToken);
|
||||
// Broadcast the turn token to kick off the agents.
|
||||
await context.SendMessageAsync(new TurnToken(emitEvents: true));
|
||||
await context.SendMessageAsync(new TurnToken(emitEvents: true), cancellationToken: cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,14 +77,16 @@ internal static class WorkflowHelper
|
||||
/// </summary>
|
||||
/// <param name="message">The message from the agent</param>
|
||||
/// <param name="context">Workflow context for accessing workflow services and adding events</param>
|
||||
public async ValueTask HandleAsync(ChatMessage message, IWorkflowContext context)
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.
|
||||
/// The default is <see cref="CancellationToken.None"/>.</param>
|
||||
public async ValueTask HandleAsync(ChatMessage message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
this._messages.Add(message);
|
||||
|
||||
if (this._messages.Count == 2)
|
||||
{
|
||||
var formattedMessages = string.Join(Environment.NewLine, this._messages.Select(m => $"{m.Text}"));
|
||||
await context.YieldOutputAsync(formattedMessages);
|
||||
await context.YieldOutputAsync(formattedMessages, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+12
-12
@@ -69,20 +69,20 @@ internal sealed class GuessNumberExecutor() : ReflectingExecutor<GuessNumberExec
|
||||
|
||||
private int NextGuess => (this.LowerBound + this.UpperBound) / 2;
|
||||
|
||||
public async ValueTask HandleAsync(NumberSignal message, IWorkflowContext context)
|
||||
public async ValueTask HandleAsync(NumberSignal message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
switch (message)
|
||||
{
|
||||
case NumberSignal.Init:
|
||||
await context.SendMessageAsync(this.NextGuess).ConfigureAwait(false);
|
||||
await context.SendMessageAsync(this.NextGuess, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
break;
|
||||
case NumberSignal.Above:
|
||||
this.UpperBound = this.NextGuess - 1;
|
||||
await context.SendMessageAsync(this.NextGuess).ConfigureAwait(false);
|
||||
await context.SendMessageAsync(this.NextGuess, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
break;
|
||||
case NumberSignal.Below:
|
||||
this.LowerBound = this.NextGuess + 1;
|
||||
await context.SendMessageAsync(this.NextGuess).ConfigureAwait(false);
|
||||
await context.SendMessageAsync(this.NextGuess, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -92,14 +92,14 @@ internal sealed class GuessNumberExecutor() : ReflectingExecutor<GuessNumberExec
|
||||
/// This must be overridden to save any state that is needed to resume the executor.
|
||||
/// </summary>
|
||||
protected override ValueTask OnCheckpointingAsync(IWorkflowContext context, CancellationToken cancellationToken = default) =>
|
||||
context.QueueStateUpdateAsync(StateKey, (this.LowerBound, this.UpperBound));
|
||||
context.QueueStateUpdateAsync(StateKey, (this.LowerBound, this.UpperBound), cancellationToken: cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Restore the state of the executor from a checkpoint.
|
||||
/// This must be overridden to restore any state that was saved during checkpointing.
|
||||
/// </summary>
|
||||
protected override async ValueTask OnCheckpointRestoredAsync(IWorkflowContext context, CancellationToken cancellationToken = default) =>
|
||||
(this.LowerBound, this.UpperBound) = await context.ReadStateAsync<(int, int)>(StateKey).ConfigureAwait(false);
|
||||
(this.LowerBound, this.UpperBound) = await context.ReadStateAsync<(int, int)>(StateKey, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -120,20 +120,20 @@ internal sealed class JudgeExecutor() : ReflectingExecutor<JudgeExecutor>("Judge
|
||||
this._targetNumber = targetNumber;
|
||||
}
|
||||
|
||||
public async ValueTask HandleAsync(int message, IWorkflowContext context)
|
||||
public async ValueTask HandleAsync(int message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
this._tries++;
|
||||
if (message == this._targetNumber)
|
||||
{
|
||||
await context.YieldOutputAsync($"{this._targetNumber} found in {this._tries} tries!").ConfigureAwait(false);
|
||||
await context.YieldOutputAsync($"{this._targetNumber} found in {this._tries} tries!", cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
else if (message < this._targetNumber)
|
||||
{
|
||||
await context.SendMessageAsync(NumberSignal.Below).ConfigureAwait(false);
|
||||
await context.SendMessageAsync(NumberSignal.Below, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
await context.SendMessageAsync(NumberSignal.Above).ConfigureAwait(false);
|
||||
await context.SendMessageAsync(NumberSignal.Above, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,12 +142,12 @@ internal sealed class JudgeExecutor() : ReflectingExecutor<JudgeExecutor>("Judge
|
||||
/// This must be overridden to save any state that is needed to resume the executor.
|
||||
/// </summary>
|
||||
protected override ValueTask OnCheckpointingAsync(IWorkflowContext context, CancellationToken cancellationToken = default) =>
|
||||
context.QueueStateUpdateAsync(StateKey, this._tries);
|
||||
context.QueueStateUpdateAsync(StateKey, this._tries, cancellationToken: cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Restore the state of the executor from a checkpoint.
|
||||
/// This must be overridden to restore any state that was saved during checkpointing.
|
||||
/// </summary>
|
||||
protected override async ValueTask OnCheckpointRestoredAsync(IWorkflowContext context, CancellationToken cancellationToken = default) =>
|
||||
this._tries = await context.ReadStateAsync<int>(StateKey).ConfigureAwait(false);
|
||||
this._tries = await context.ReadStateAsync<int>(StateKey, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
+12
-12
@@ -69,20 +69,20 @@ internal sealed class GuessNumberExecutor() : ReflectingExecutor<GuessNumberExec
|
||||
|
||||
private int NextGuess => (this.LowerBound + this.UpperBound) / 2;
|
||||
|
||||
public async ValueTask HandleAsync(NumberSignal message, IWorkflowContext context)
|
||||
public async ValueTask HandleAsync(NumberSignal message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
switch (message)
|
||||
{
|
||||
case NumberSignal.Init:
|
||||
await context.SendMessageAsync(this.NextGuess).ConfigureAwait(false);
|
||||
await context.SendMessageAsync(this.NextGuess, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
break;
|
||||
case NumberSignal.Above:
|
||||
this.UpperBound = this.NextGuess - 1;
|
||||
await context.SendMessageAsync(this.NextGuess).ConfigureAwait(false);
|
||||
await context.SendMessageAsync(this.NextGuess, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
break;
|
||||
case NumberSignal.Below:
|
||||
this.LowerBound = this.NextGuess + 1;
|
||||
await context.SendMessageAsync(this.NextGuess).ConfigureAwait(false);
|
||||
await context.SendMessageAsync(this.NextGuess, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -92,14 +92,14 @@ internal sealed class GuessNumberExecutor() : ReflectingExecutor<GuessNumberExec
|
||||
/// This must be overridden to save any state that is needed to resume the executor.
|
||||
/// </summary>
|
||||
protected override ValueTask OnCheckpointingAsync(IWorkflowContext context, CancellationToken cancellationToken = default) =>
|
||||
context.QueueStateUpdateAsync(StateKey, (this.LowerBound, this.UpperBound));
|
||||
context.QueueStateUpdateAsync(StateKey, (this.LowerBound, this.UpperBound), cancellationToken: cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Restore the state of the executor from a checkpoint.
|
||||
/// This must be overridden to restore any state that was saved during checkpointing.
|
||||
/// </summary>
|
||||
protected override async ValueTask OnCheckpointRestoredAsync(IWorkflowContext context, CancellationToken cancellationToken = default) =>
|
||||
(this.LowerBound, this.UpperBound) = await context.ReadStateAsync<(int, int)>(StateKey).ConfigureAwait(false);
|
||||
(this.LowerBound, this.UpperBound) = await context.ReadStateAsync<(int, int)>(StateKey, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -120,20 +120,20 @@ internal sealed class JudgeExecutor() : ReflectingExecutor<JudgeExecutor>("Judge
|
||||
this._targetNumber = targetNumber;
|
||||
}
|
||||
|
||||
public async ValueTask HandleAsync(int message, IWorkflowContext context)
|
||||
public async ValueTask HandleAsync(int message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
this._tries++;
|
||||
if (message == this._targetNumber)
|
||||
{
|
||||
await context.YieldOutputAsync($"{this._targetNumber} found in {this._tries} tries!").ConfigureAwait(false);
|
||||
await context.YieldOutputAsync($"{this._targetNumber} found in {this._tries} tries!", cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
else if (message < this._targetNumber)
|
||||
{
|
||||
await context.SendMessageAsync(NumberSignal.Below).ConfigureAwait(false);
|
||||
await context.SendMessageAsync(NumberSignal.Below, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
await context.SendMessageAsync(NumberSignal.Above).ConfigureAwait(false);
|
||||
await context.SendMessageAsync(NumberSignal.Above, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,12 +142,12 @@ internal sealed class JudgeExecutor() : ReflectingExecutor<JudgeExecutor>("Judge
|
||||
/// This must be overridden to save any state that is needed to resume the executor.
|
||||
/// </summary>
|
||||
protected override ValueTask OnCheckpointingAsync(IWorkflowContext context, CancellationToken cancellationToken = default) =>
|
||||
context.QueueStateUpdateAsync(StateKey, this._tries);
|
||||
context.QueueStateUpdateAsync(StateKey, this._tries, cancellationToken: cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Restore the state of the executor from a checkpoint.
|
||||
/// This must be overridden to restore any state that was saved during checkpointing.
|
||||
/// </summary>
|
||||
protected override async ValueTask OnCheckpointRestoredAsync(IWorkflowContext context, CancellationToken cancellationToken = default) =>
|
||||
this._tries = await context.ReadStateAsync<int>(StateKey).ConfigureAwait(false);
|
||||
this._tries = await context.ReadStateAsync<int>(StateKey, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
+6
-6
@@ -69,21 +69,21 @@ internal sealed class JudgeExecutor() : ReflectingExecutor<JudgeExecutor>("Judge
|
||||
this._targetNumber = targetNumber;
|
||||
}
|
||||
|
||||
public async ValueTask HandleAsync(int message, IWorkflowContext context)
|
||||
public async ValueTask HandleAsync(int message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
this._tries++;
|
||||
if (message == this._targetNumber)
|
||||
{
|
||||
await context.YieldOutputAsync($"{this._targetNumber} found in {this._tries} tries!")
|
||||
await context.YieldOutputAsync($"{this._targetNumber} found in {this._tries} tries!", cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
else if (message < this._targetNumber)
|
||||
{
|
||||
await context.SendMessageAsync(new SignalWithNumber(NumberSignal.Below, message)).ConfigureAwait(false);
|
||||
await context.SendMessageAsync(new SignalWithNumber(NumberSignal.Below, message), cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
await context.SendMessageAsync(new SignalWithNumber(NumberSignal.Above, message)).ConfigureAwait(false);
|
||||
await context.SendMessageAsync(new SignalWithNumber(NumberSignal.Above, message), cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,12 +92,12 @@ internal sealed class JudgeExecutor() : ReflectingExecutor<JudgeExecutor>("Judge
|
||||
/// This must be overridden to save any state that is needed to resume the executor.
|
||||
/// </summary>
|
||||
protected override ValueTask OnCheckpointingAsync(IWorkflowContext context, CancellationToken cancellationToken = default) =>
|
||||
context.QueueStateUpdateAsync(StateKey, this._tries);
|
||||
context.QueueStateUpdateAsync(StateKey, this._tries, cancellationToken: cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Restore the state of the executor from a checkpoint.
|
||||
/// This must be overridden to restore any state that was saved during checkpointing.
|
||||
/// </summary>
|
||||
protected override async ValueTask OnCheckpointRestoredAsync(IWorkflowContext context, CancellationToken cancellationToken = default) =>
|
||||
this._tries = await context.ReadStateAsync<int>(StateKey).ConfigureAwait(false);
|
||||
this._tries = await context.ReadStateAsync<int>(StateKey, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
@@ -82,14 +82,16 @@ internal sealed class ConcurrentStartExecutor() :
|
||||
/// </summary>
|
||||
/// <param name="message">The user message to process</param>
|
||||
/// <param name="context">Workflow context for accessing workflow services and adding events</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.
|
||||
/// The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A task representing the asynchronous operation</returns>
|
||||
public async ValueTask HandleAsync(string message, IWorkflowContext context)
|
||||
public async ValueTask HandleAsync(string message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Broadcast the message to all connected agents. Receiving agents will queue
|
||||
// the message but will not start processing until they receive a turn token.
|
||||
await context.SendMessageAsync(new ChatMessage(ChatRole.User, message));
|
||||
await context.SendMessageAsync(new ChatMessage(ChatRole.User, message), cancellationToken: cancellationToken);
|
||||
// Broadcast the turn token to kick off the agents.
|
||||
await context.SendMessageAsync(new TurnToken(emitEvents: true));
|
||||
await context.SendMessageAsync(new TurnToken(emitEvents: true), cancellationToken: cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,15 +109,17 @@ internal sealed class ConcurrentAggregationExecutor() :
|
||||
/// </summary>
|
||||
/// <param name="message">The message from the agent</param>
|
||||
/// <param name="context">Workflow context for accessing workflow services and adding events</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.
|
||||
/// The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A task representing the asynchronous operation</returns>
|
||||
public async ValueTask HandleAsync(ChatMessage message, IWorkflowContext context)
|
||||
public async ValueTask HandleAsync(ChatMessage message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
this._messages.Add(message);
|
||||
|
||||
if (this._messages.Count == 2)
|
||||
{
|
||||
var formattedMessages = string.Join(Environment.NewLine, this._messages.Select(m => $"{m.AuthorName}: {m.Text}"));
|
||||
await context.YieldOutputAsync(formattedMessages);
|
||||
await context.YieldOutputAsync(formattedMessages, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.AI.Workflows;
|
||||
using Microsoft.Agents.AI.Workflows.Reflection;
|
||||
@@ -138,7 +139,7 @@ internal sealed class Split(string[] mapperIds, string id) :
|
||||
/// <summary>
|
||||
/// Tokenize input and assign contiguous index ranges to each mapper via shared state.
|
||||
/// </summary>
|
||||
public async ValueTask HandleAsync(string message, IWorkflowContext context)
|
||||
public async ValueTask HandleAsync(string message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Ensure temp directory exists
|
||||
Directory.CreateDirectory(MapReduceConstants.TempDir);
|
||||
@@ -147,7 +148,7 @@ internal sealed class Split(string[] mapperIds, string id) :
|
||||
var wordList = Preprocess(message);
|
||||
|
||||
// Store the tokenized words once so that all mappers can read by index
|
||||
await context.QueueStateUpdateAsync(MapReduceConstants.DataToProcessKey, wordList, scopeName: MapReduceConstants.StateScope);
|
||||
await context.QueueStateUpdateAsync(MapReduceConstants.DataToProcessKey, wordList, scopeName: MapReduceConstants.StateScope, cancellationToken);
|
||||
|
||||
// Divide indices into contiguous slices for each mapper
|
||||
var mapperCount = this._mapperIds.Length;
|
||||
@@ -160,10 +161,10 @@ internal sealed class Split(string[] mapperIds, string id) :
|
||||
var endIndex = i < mapperCount - 1 ? startIndex + chunkSize : wordList.Length;
|
||||
|
||||
// Save the indices under the mapper's Id
|
||||
await context.QueueStateUpdateAsync(this._mapperIds[i], (startIndex, endIndex), scopeName: MapReduceConstants.StateScope);
|
||||
await context.QueueStateUpdateAsync(this._mapperIds[i], (startIndex, endIndex), scopeName: MapReduceConstants.StateScope, cancellationToken);
|
||||
|
||||
// Notify the mapper that data is ready
|
||||
await context.SendMessageAsync(new SplitComplete(), targetId: this._mapperIds[i]);
|
||||
await context.SendMessageAsync(new SplitComplete(), targetId: this._mapperIds[i], cancellationToken);
|
||||
}
|
||||
|
||||
// Process all the chunks
|
||||
@@ -192,10 +193,10 @@ internal sealed class Mapper(string id) : ReflectingExecutor<Mapper>(id), IMessa
|
||||
/// <summary>
|
||||
/// Read the assigned slice, emit (word, 1) pairs, and persist to disk.
|
||||
/// </summary>
|
||||
public async ValueTask HandleAsync(SplitComplete message, IWorkflowContext context)
|
||||
public async ValueTask HandleAsync(SplitComplete message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var dataToProcess = await context.ReadStateAsync<string[]>(MapReduceConstants.DataToProcessKey, scopeName: MapReduceConstants.StateScope);
|
||||
var chunk = await context.ReadStateAsync<(int start, int end)>(this.Id, scopeName: MapReduceConstants.StateScope);
|
||||
var dataToProcess = await context.ReadStateAsync<string[]>(MapReduceConstants.DataToProcessKey, scopeName: MapReduceConstants.StateScope, cancellationToken);
|
||||
var chunk = await context.ReadStateAsync<(int start, int end)>(this.Id, scopeName: MapReduceConstants.StateScope, cancellationToken);
|
||||
|
||||
var results = dataToProcess![chunk.start..chunk.end]
|
||||
.Select(word => (word, 1))
|
||||
@@ -204,9 +205,9 @@ internal sealed class Mapper(string id) : ReflectingExecutor<Mapper>(id), IMessa
|
||||
// Write this mapper's results as simple text lines for easy debugging
|
||||
var filePath = Path.Combine(MapReduceConstants.TempDir, $"map_results_{this.Id}.txt");
|
||||
var lines = results.Select(r => $"{r.word}: {r.Item2}");
|
||||
await File.WriteAllLinesAsync(filePath, lines);
|
||||
await File.WriteAllLinesAsync(filePath, lines, cancellationToken);
|
||||
|
||||
await context.SendMessageAsync(new MapComplete(filePath));
|
||||
await context.SendMessageAsync(new MapComplete(filePath), cancellationToken: cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -224,7 +225,7 @@ internal sealed class Shuffler(string[] reducerIds, string[] mapperIds, string i
|
||||
/// <summary>
|
||||
/// Aggregate mapper outputs and write one partition file per reducer.
|
||||
/// </summary>
|
||||
public async ValueTask HandleAsync(MapComplete message, IWorkflowContext context)
|
||||
public async ValueTask HandleAsync(MapComplete message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
this._mapResults.Add(message);
|
||||
|
||||
@@ -241,9 +242,9 @@ internal sealed class Shuffler(string[] reducerIds, string[] mapperIds, string i
|
||||
// Write one grouped partition for reducer index and notify that reducer
|
||||
var filePath = Path.Combine(MapReduceConstants.TempDir, $"shuffle_results_{index}.txt");
|
||||
var lines = chunk.Select(kvp => $"{kvp.key}: {JsonSerializer.Serialize(kvp.values)}");
|
||||
await File.WriteAllLinesAsync(filePath, lines);
|
||||
await File.WriteAllLinesAsync(filePath, lines, cancellationToken);
|
||||
|
||||
await context.SendMessageAsync(new ShuffleComplete(filePath, this._reducerIds[index]));
|
||||
await context.SendMessageAsync(new ShuffleComplete(filePath, this._reducerIds[index]), cancellationToken: cancellationToken);
|
||||
}
|
||||
|
||||
var tasks = chunks.Select((chunk, i) => ProcessChunkAsync(chunk, i));
|
||||
@@ -318,7 +319,7 @@ internal sealed class Reducer(string id) : ReflectingExecutor<Reducer>(id), IMes
|
||||
/// <summary>
|
||||
/// Read one shuffle partition and reduce it to totals.
|
||||
/// </summary>
|
||||
public async ValueTask HandleAsync(ShuffleComplete message, IWorkflowContext context)
|
||||
public async ValueTask HandleAsync(ShuffleComplete message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (message.ReducerId != this.Id)
|
||||
{
|
||||
@@ -327,7 +328,7 @@ internal sealed class Reducer(string id) : ReflectingExecutor<Reducer>(id), IMes
|
||||
}
|
||||
|
||||
// Read grouped values from the shuffle output
|
||||
var lines = await File.ReadAllLinesAsync(message.FilePath);
|
||||
var lines = await File.ReadAllLinesAsync(message.FilePath, cancellationToken);
|
||||
|
||||
// Sum values per key. Values are serialized JSON arrays like [1, 1, ...]
|
||||
var reducedResults = new Dictionary<string, int>();
|
||||
@@ -345,9 +346,9 @@ internal sealed class Reducer(string id) : ReflectingExecutor<Reducer>(id), IMes
|
||||
// Persist our partition totals
|
||||
var filePath = Path.Combine(MapReduceConstants.TempDir, $"reduced_results_{this.Id}.txt");
|
||||
var outputLines = reducedResults.Select(kvp => $"{kvp.Key}: {kvp.Value}");
|
||||
await File.WriteAllLinesAsync(filePath, outputLines);
|
||||
await File.WriteAllLinesAsync(filePath, outputLines, cancellationToken);
|
||||
|
||||
await context.SendMessageAsync(new ReduceComplete(filePath));
|
||||
await context.SendMessageAsync(new ReduceComplete(filePath), cancellationToken: cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -361,10 +362,10 @@ internal sealed class CompletionExecutor(string id) :
|
||||
/// <summary>
|
||||
/// Collect reducer output file paths and yield final output.
|
||||
/// </summary>
|
||||
public async ValueTask HandleAsync(List<ReduceComplete> message, IWorkflowContext context)
|
||||
public async ValueTask HandleAsync(List<ReduceComplete> message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var filePaths = message.ConvertAll(r => r.FilePath);
|
||||
await context.YieldOutputAsync(filePaths);
|
||||
await context.YieldOutputAsync(filePaths, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+10
-10
@@ -160,7 +160,7 @@ internal sealed class SpamDetectionExecutor : ReflectingExecutor<SpamDetectionEx
|
||||
this._spamDetectionAgent = spamDetectionAgent;
|
||||
}
|
||||
|
||||
public async ValueTask<DetectionResult> HandleAsync(ChatMessage message, IWorkflowContext context)
|
||||
public async ValueTask<DetectionResult> HandleAsync(ChatMessage message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Generate a random email ID and store the email content to the shared state
|
||||
var newEmail = new Email
|
||||
@@ -168,10 +168,10 @@ internal sealed class SpamDetectionExecutor : ReflectingExecutor<SpamDetectionEx
|
||||
EmailId = Guid.NewGuid().ToString("N"),
|
||||
EmailContent = message.Text
|
||||
};
|
||||
await context.QueueStateUpdateAsync(newEmail.EmailId, newEmail, scopeName: EmailStateConstants.EmailStateScope);
|
||||
await context.QueueStateUpdateAsync(newEmail.EmailId, newEmail, scopeName: EmailStateConstants.EmailStateScope, cancellationToken);
|
||||
|
||||
// Invoke the agent
|
||||
var response = await this._spamDetectionAgent.RunAsync(message);
|
||||
var response = await this._spamDetectionAgent.RunAsync(message, cancellationToken: cancellationToken);
|
||||
var detectionResult = JsonSerializer.Deserialize<DetectionResult>(response.Text);
|
||||
|
||||
detectionResult!.EmailId = newEmail.EmailId;
|
||||
@@ -205,7 +205,7 @@ internal sealed class EmailAssistantExecutor : ReflectingExecutor<EmailAssistant
|
||||
this._emailAssistantAgent = emailAssistantAgent;
|
||||
}
|
||||
|
||||
public async ValueTask<EmailResponse> HandleAsync(DetectionResult message, IWorkflowContext context)
|
||||
public async ValueTask<EmailResponse> HandleAsync(DetectionResult message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (message.IsSpam)
|
||||
{
|
||||
@@ -213,11 +213,11 @@ internal sealed class EmailAssistantExecutor : ReflectingExecutor<EmailAssistant
|
||||
}
|
||||
|
||||
// Retrieve the email content from the shared state
|
||||
var email = await context.ReadStateAsync<Email>(message.EmailId, scopeName: EmailStateConstants.EmailStateScope)
|
||||
var email = await context.ReadStateAsync<Email>(message.EmailId, scopeName: EmailStateConstants.EmailStateScope, cancellationToken)
|
||||
?? throw new InvalidOperationException("Email not found.");
|
||||
|
||||
// Invoke the agent
|
||||
var response = await this._emailAssistantAgent.RunAsync(email.EmailContent);
|
||||
var response = await this._emailAssistantAgent.RunAsync(email.EmailContent, cancellationToken: cancellationToken);
|
||||
var emailResponse = JsonSerializer.Deserialize<EmailResponse>(response.Text);
|
||||
|
||||
return emailResponse!;
|
||||
@@ -232,8 +232,8 @@ internal sealed class SendEmailExecutor() : ReflectingExecutor<SendEmailExecutor
|
||||
/// <summary>
|
||||
/// Simulate the sending of an email.
|
||||
/// </summary>
|
||||
public async ValueTask HandleAsync(EmailResponse message, IWorkflowContext context) =>
|
||||
await context.YieldOutputAsync($"Email sent: {message.Response}");
|
||||
public async ValueTask HandleAsync(EmailResponse message, IWorkflowContext context, CancellationToken cancellationToken = default) =>
|
||||
await context.YieldOutputAsync($"Email sent: {message.Response}", cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -244,11 +244,11 @@ internal sealed class HandleSpamExecutor() : ReflectingExecutor<HandleSpamExecut
|
||||
/// <summary>
|
||||
/// Simulate the handling of a spam message.
|
||||
/// </summary>
|
||||
public async ValueTask HandleAsync(DetectionResult message, IWorkflowContext context)
|
||||
public async ValueTask HandleAsync(DetectionResult message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (message.IsSpam)
|
||||
{
|
||||
await context.YieldOutputAsync($"Email marked as spam: {message.Reason}");
|
||||
await context.YieldOutputAsync($"Email marked as spam: {message.Reason}", cancellationToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -185,7 +185,7 @@ internal sealed class SpamDetectionExecutor : ReflectingExecutor<SpamDetectionEx
|
||||
this._spamDetectionAgent = spamDetectionAgent;
|
||||
}
|
||||
|
||||
public async ValueTask<DetectionResult> HandleAsync(ChatMessage message, IWorkflowContext context)
|
||||
public async ValueTask<DetectionResult> HandleAsync(ChatMessage message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Generate a random email ID and store the email content
|
||||
var newEmail = new Email
|
||||
@@ -193,10 +193,10 @@ internal sealed class SpamDetectionExecutor : ReflectingExecutor<SpamDetectionEx
|
||||
EmailId = Guid.NewGuid().ToString("N"),
|
||||
EmailContent = message.Text
|
||||
};
|
||||
await context.QueueStateUpdateAsync(newEmail.EmailId, newEmail, scopeName: EmailStateConstants.EmailStateScope);
|
||||
await context.QueueStateUpdateAsync(newEmail.EmailId, newEmail, scopeName: EmailStateConstants.EmailStateScope, cancellationToken);
|
||||
|
||||
// Invoke the agent
|
||||
var response = await this._spamDetectionAgent.RunAsync(message);
|
||||
var response = await this._spamDetectionAgent.RunAsync(message, cancellationToken: cancellationToken);
|
||||
var detectionResult = JsonSerializer.Deserialize<DetectionResult>(response.Text);
|
||||
|
||||
detectionResult!.EmailId = newEmail.EmailId;
|
||||
@@ -230,7 +230,7 @@ internal sealed class EmailAssistantExecutor : ReflectingExecutor<EmailAssistant
|
||||
this._emailAssistantAgent = emailAssistantAgent;
|
||||
}
|
||||
|
||||
public async ValueTask<EmailResponse> HandleAsync(DetectionResult message, IWorkflowContext context)
|
||||
public async ValueTask<EmailResponse> HandleAsync(DetectionResult message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (message.spamDecision == SpamDecision.Spam)
|
||||
{
|
||||
@@ -238,10 +238,10 @@ internal sealed class EmailAssistantExecutor : ReflectingExecutor<EmailAssistant
|
||||
}
|
||||
|
||||
// Retrieve the email content from the context
|
||||
var email = await context.ReadStateAsync<Email>(message.EmailId, scopeName: EmailStateConstants.EmailStateScope);
|
||||
var email = await context.ReadStateAsync<Email>(message.EmailId, scopeName: EmailStateConstants.EmailStateScope, cancellationToken);
|
||||
|
||||
// Invoke the agent
|
||||
var response = await this._emailAssistantAgent.RunAsync(email!.EmailContent);
|
||||
var response = await this._emailAssistantAgent.RunAsync(email!.EmailContent, cancellationToken: cancellationToken);
|
||||
var emailResponse = JsonSerializer.Deserialize<EmailResponse>(response.Text);
|
||||
|
||||
return emailResponse!;
|
||||
@@ -256,8 +256,8 @@ internal sealed class SendEmailExecutor() : ReflectingExecutor<SendEmailExecutor
|
||||
/// <summary>
|
||||
/// Simulate the sending of an email.
|
||||
/// </summary>
|
||||
public async ValueTask HandleAsync(EmailResponse message, IWorkflowContext context) =>
|
||||
await context.YieldOutputAsync($"Email sent: {message.Response}").ConfigureAwait(false);
|
||||
public async ValueTask HandleAsync(EmailResponse message, IWorkflowContext context, CancellationToken cancellationToken = default) =>
|
||||
await context.YieldOutputAsync($"Email sent: {message.Response}", cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -268,11 +268,11 @@ internal sealed class HandleSpamExecutor() : ReflectingExecutor<HandleSpamExecut
|
||||
/// <summary>
|
||||
/// Simulate the handling of a spam message.
|
||||
/// </summary>
|
||||
public async ValueTask HandleAsync(DetectionResult message, IWorkflowContext context)
|
||||
public async ValueTask HandleAsync(DetectionResult message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (message.spamDecision == SpamDecision.Spam)
|
||||
{
|
||||
await context.YieldOutputAsync($"Email marked as spam: {message.Reason}").ConfigureAwait(false);
|
||||
await context.YieldOutputAsync($"Email marked as spam: {message.Reason}", cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -289,12 +289,12 @@ internal sealed class HandleUncertainExecutor() : ReflectingExecutor<HandleUncer
|
||||
/// <summary>
|
||||
/// Simulate the handling of an uncertain spam decision.
|
||||
/// </summary>
|
||||
public async ValueTask HandleAsync(DetectionResult message, IWorkflowContext context)
|
||||
public async ValueTask HandleAsync(DetectionResult message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (message.spamDecision == SpamDecision.Uncertain)
|
||||
{
|
||||
var email = await context.ReadStateAsync<Email>(message.EmailId, scopeName: EmailStateConstants.EmailStateScope);
|
||||
await context.YieldOutputAsync($"Email marked as uncertain: {message.Reason}. Email content: {email?.EmailContent}");
|
||||
var email = await context.ReadStateAsync<Email>(message.EmailId, scopeName: EmailStateConstants.EmailStateScope, cancellationToken);
|
||||
await context.YieldOutputAsync($"Email marked as uncertain: {message.Reason}. Email content: {email?.EmailContent}", cancellationToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
+21
-21
@@ -241,7 +241,7 @@ internal sealed class EmailAnalysisExecutor : ReflectingExecutor<EmailAnalysisEx
|
||||
this._emailAnalysisAgent = emailAnalysisAgent;
|
||||
}
|
||||
|
||||
public async ValueTask<AnalysisResult> HandleAsync(ChatMessage message, IWorkflowContext context)
|
||||
public async ValueTask<AnalysisResult> HandleAsync(ChatMessage message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Generate a random email ID and store the email content
|
||||
var newEmail = new Email
|
||||
@@ -249,10 +249,10 @@ internal sealed class EmailAnalysisExecutor : ReflectingExecutor<EmailAnalysisEx
|
||||
EmailId = Guid.NewGuid().ToString("N"),
|
||||
EmailContent = message.Text
|
||||
};
|
||||
await context.QueueStateUpdateAsync(newEmail.EmailId, newEmail, scopeName: EmailStateConstants.EmailStateScope);
|
||||
await context.QueueStateUpdateAsync(newEmail.EmailId, newEmail, scopeName: EmailStateConstants.EmailStateScope, cancellationToken);
|
||||
|
||||
// Invoke the agent
|
||||
var response = await this._emailAnalysisAgent.RunAsync(message);
|
||||
var response = await this._emailAnalysisAgent.RunAsync(message, cancellationToken: cancellationToken);
|
||||
var AnalysisResult = JsonSerializer.Deserialize<AnalysisResult>(response.Text);
|
||||
|
||||
AnalysisResult!.EmailId = newEmail.EmailId;
|
||||
@@ -287,7 +287,7 @@ internal sealed class EmailAssistantExecutor : ReflectingExecutor<EmailAssistant
|
||||
this._emailAssistantAgent = emailAssistantAgent;
|
||||
}
|
||||
|
||||
public async ValueTask<EmailResponse> HandleAsync(AnalysisResult message, IWorkflowContext context)
|
||||
public async ValueTask<EmailResponse> HandleAsync(AnalysisResult message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (message.spamDecision == SpamDecision.Spam)
|
||||
{
|
||||
@@ -295,10 +295,10 @@ internal sealed class EmailAssistantExecutor : ReflectingExecutor<EmailAssistant
|
||||
}
|
||||
|
||||
// Retrieve the email content from the context
|
||||
var email = await context.ReadStateAsync<Email>(message.EmailId, scopeName: EmailStateConstants.EmailStateScope);
|
||||
var email = await context.ReadStateAsync<Email>(message.EmailId, scopeName: EmailStateConstants.EmailStateScope, cancellationToken);
|
||||
|
||||
// Invoke the agent
|
||||
var response = await this._emailAssistantAgent.RunAsync(email!.EmailContent);
|
||||
var response = await this._emailAssistantAgent.RunAsync(email!.EmailContent, cancellationToken: cancellationToken);
|
||||
var emailResponse = JsonSerializer.Deserialize<EmailResponse>(response.Text);
|
||||
|
||||
return emailResponse!;
|
||||
@@ -313,8 +313,8 @@ internal sealed class SendEmailExecutor() : ReflectingExecutor<SendEmailExecutor
|
||||
/// <summary>
|
||||
/// Simulate the sending of an email.
|
||||
/// </summary>
|
||||
public async ValueTask HandleAsync(EmailResponse message, IWorkflowContext context) =>
|
||||
await context.YieldOutputAsync($"Email sent: {message.Response}");
|
||||
public async ValueTask HandleAsync(EmailResponse message, IWorkflowContext context, CancellationToken cancellationToken = default) =>
|
||||
await context.YieldOutputAsync($"Email sent: {message.Response}", cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -325,11 +325,11 @@ internal sealed class HandleSpamExecutor() : ReflectingExecutor<HandleSpamExecut
|
||||
/// <summary>
|
||||
/// Simulate the handling of a spam message.
|
||||
/// </summary>
|
||||
public async ValueTask HandleAsync(AnalysisResult message, IWorkflowContext context)
|
||||
public async ValueTask HandleAsync(AnalysisResult message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (message.spamDecision == SpamDecision.Spam)
|
||||
{
|
||||
await context.YieldOutputAsync($"Email marked as spam: {message.Reason}");
|
||||
await context.YieldOutputAsync($"Email marked as spam: {message.Reason}", cancellationToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -346,12 +346,12 @@ internal sealed class HandleUncertainExecutor() : ReflectingExecutor<HandleUncer
|
||||
/// <summary>
|
||||
/// Simulate the handling of an uncertain spam decision.
|
||||
/// </summary>
|
||||
public async ValueTask HandleAsync(AnalysisResult message, IWorkflowContext context)
|
||||
public async ValueTask HandleAsync(AnalysisResult message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (message.spamDecision == SpamDecision.Uncertain)
|
||||
{
|
||||
var email = await context.ReadStateAsync<Email>(message.EmailId, scopeName: EmailStateConstants.EmailStateScope);
|
||||
await context.YieldOutputAsync($"Email marked as uncertain: {message.Reason}. Email content: {email?.EmailContent}");
|
||||
var email = await context.ReadStateAsync<Email>(message.EmailId, scopeName: EmailStateConstants.EmailStateScope, cancellationToken);
|
||||
await context.YieldOutputAsync($"Email marked as uncertain: {message.Reason}. Email content: {email?.EmailContent}", cancellationToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -385,13 +385,13 @@ internal sealed class EmailSummaryExecutor : ReflectingExecutor<EmailSummaryExec
|
||||
this._emailSummaryAgent = emailSummaryAgent;
|
||||
}
|
||||
|
||||
public async ValueTask<AnalysisResult> HandleAsync(AnalysisResult message, IWorkflowContext context)
|
||||
public async ValueTask<AnalysisResult> HandleAsync(AnalysisResult message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Read the email content from the shared states
|
||||
var email = await context.ReadStateAsync<Email>(message.EmailId, scopeName: EmailStateConstants.EmailStateScope);
|
||||
var email = await context.ReadStateAsync<Email>(message.EmailId, scopeName: EmailStateConstants.EmailStateScope, cancellationToken);
|
||||
|
||||
// Invoke the agent
|
||||
var response = await this._emailSummaryAgent.RunAsync(email!.EmailContent);
|
||||
var response = await this._emailSummaryAgent.RunAsync(email!.EmailContent, cancellationToken: cancellationToken);
|
||||
var emailSummary = JsonSerializer.Deserialize<EmailSummary>(response.Text);
|
||||
message.EmailSummary = emailSummary!.Summary;
|
||||
|
||||
@@ -410,17 +410,17 @@ internal sealed class DatabaseEvent(string message) : WorkflowEvent(message) { }
|
||||
/// </summary>
|
||||
internal sealed class DatabaseAccessExecutor() : ReflectingExecutor<DatabaseAccessExecutor>("DatabaseAccessExecutor"), IMessageHandler<AnalysisResult>
|
||||
{
|
||||
public async ValueTask HandleAsync(AnalysisResult message, IWorkflowContext context)
|
||||
public async ValueTask HandleAsync(AnalysisResult message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// 1. Save the email content
|
||||
await context.ReadStateAsync<Email>(message.EmailId, scopeName: EmailStateConstants.EmailStateScope);
|
||||
await Task.Delay(100); // Simulate database access delay
|
||||
await context.ReadStateAsync<Email>(message.EmailId, scopeName: EmailStateConstants.EmailStateScope, cancellationToken);
|
||||
await Task.Delay(100, cancellationToken); // Simulate database access delay
|
||||
|
||||
// 2. Save the analysis result
|
||||
await Task.Delay(100); // Simulate database access delay
|
||||
await Task.Delay(100, cancellationToken); // Simulate database access delay
|
||||
|
||||
// Not using the `WorkflowCompletedEvent` because this is not the end of the workflow.
|
||||
// The end of the workflow is signaled by the `SendEmailExecutor` or the `HandleUnknownExecutor`.
|
||||
await context.AddEventAsync(new DatabaseEvent($"Email {message.EmailId} saved to database."));
|
||||
await context.AddEventAsync(new DatabaseEvent($"Email {message.EmailId} saved to database."), cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,102 +76,103 @@ internal sealed class Program
|
||||
|
||||
string? messageId = null;
|
||||
|
||||
await foreach (WorkflowEvent evt in run.WatchStreamAsync().ConfigureAwait(false))
|
||||
await foreach (WorkflowEvent workflowEvent in run.WatchStreamAsync().ConfigureAwait(false))
|
||||
{
|
||||
if (evt is ExecutorInvokedEvent executorInvoked)
|
||||
switch (workflowEvent)
|
||||
{
|
||||
Debug.WriteLine($"STEP ENTER #{executorInvoked.ExecutorId}");
|
||||
}
|
||||
else if (evt is ExecutorCompletedEvent executorComplete)
|
||||
{
|
||||
Debug.WriteLine($"STEP EXIT #{executorComplete.ExecutorId}");
|
||||
}
|
||||
else if (evt is ExecutorFailedEvent executorFailure)
|
||||
{
|
||||
Debug.WriteLine($"STEP ERROR #{executorFailure.ExecutorId}: {executorFailure.Data?.Message ?? "Unknown"}");
|
||||
}
|
||||
else if (evt is WorkflowErrorEvent workflowError)
|
||||
{
|
||||
Debug.WriteLine("WORKFLOW ERROR");
|
||||
}
|
||||
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($"STEP ENTER #{executorInvoked.ExecutorId}");
|
||||
break;
|
||||
|
||||
if (messageId is not null)
|
||||
case ExecutorCompletedEvent executorComplete:
|
||||
Debug.WriteLine($"STEP EXIT #{executorComplete.ExecutorId}");
|
||||
break;
|
||||
|
||||
case ExecutorFailedEvent executorFailure:
|
||||
Debug.WriteLine($"STEP ERROR #{executorFailure.ExecutorId}: {executorFailure.Data?.Message ?? "Unknown"}");
|
||||
break;
|
||||
|
||||
case WorkflowErrorEvent workflowError:
|
||||
throw workflowError.Data as Exception ?? new InvalidOperationException("Unexpected failure...");
|
||||
|
||||
case ConversationUpdateEvent invokeEvent:
|
||||
Debug.WriteLine($"CONVERSATION: {invokeEvent.Data}");
|
||||
break;
|
||||
|
||||
case AgentRunUpdateEvent streamEvent:
|
||||
if (!string.Equals(messageId, streamEvent.Update.MessageId, StringComparison.Ordinal))
|
||||
{
|
||||
string? agentId = streamEvent.Update.AuthorName;
|
||||
if (agentId is not null)
|
||||
messageId = streamEvent.Update.MessageId;
|
||||
|
||||
if (messageId is not null)
|
||||
{
|
||||
if (!s_nameCache.TryGetValue(agentId, out string? realName))
|
||||
string? agentId = streamEvent.Update.AuthorName;
|
||||
if (agentId is not null)
|
||||
{
|
||||
PersistentAgent agent = await this.FoundryClient.Administration.GetAgentAsync(agentId);
|
||||
s_nameCache[agentId] = agent.Name;
|
||||
realName = agent.Name;
|
||||
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 = 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 && 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)
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.Cyan;
|
||||
Console.WriteLine("ACTIVITY:");
|
||||
Console.ForegroundColor = ConsoleColor.Yellow;
|
||||
Console.WriteLine(messageEvent.Response?.Text.Trim());
|
||||
}
|
||||
else
|
||||
{
|
||||
if (messageEvent.Response.Usage is not null)
|
||||
{
|
||||
agentId ??= nameof(ChatRole.Assistant);
|
||||
Console.ForegroundColor = ConsoleColor.Cyan;
|
||||
Console.Write($"\n{agentId.ToUpperInvariant()}:");
|
||||
Console.ForegroundColor = ConsoleColor.DarkGray;
|
||||
Console.WriteLine($"[Tokens Total: {messageEvent.Response.Usage.TotalTokenCount}, Input: {messageEvent.Response.Usage.InputTokenCount}, Output: {messageEvent.Response.Usage.OutputTokenCount}]");
|
||||
Console.WriteLine($" [{messageId}]");
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
Console.ResetColor();
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
break;
|
||||
|
||||
case AgentRunResponseEvent messageEvent:
|
||||
try
|
||||
{
|
||||
Console.WriteLine();
|
||||
if (messageEvent.Response.AgentId is null)
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.Cyan;
|
||||
Console.WriteLine("ACTIVITY:");
|
||||
Console.ForegroundColor = ConsoleColor.Yellow;
|
||||
Console.WriteLine(messageEvent.Response?.Text.Trim());
|
||||
}
|
||||
else
|
||||
{
|
||||
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();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
// Uncomment this to enable JSON checkpointing to the local file system.
|
||||
#define CHECKPOINT_JSON
|
||||
|
||||
using System.Diagnostics;
|
||||
using System.Reflection;
|
||||
using Azure.AI.Agents.Persistent;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI.Workflows;
|
||||
using Microsoft.Agents.AI.Workflows.Checkpointing;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.Events;
|
||||
using Microsoft.Extensions.AI;
|
||||
@@ -57,8 +61,15 @@ internal sealed class Program
|
||||
// Run the workflow, just like any other workflow
|
||||
string input = this.GetWorkflowInput();
|
||||
|
||||
CheckpointManager checkpointManager = CheckpointManager.CreateInMemory();
|
||||
#if CHECKPOINT_JSON
|
||||
// Use a file-system based JSON checkpoint store to persist checkpoints to disk.
|
||||
DirectoryInfo checkpointFolder = Directory.CreateDirectory(Path.Combine(".", $"chk-{DateTime.Now:YYmmdd-hhMMss-ff}"));
|
||||
CheckpointManager checkpointManager = CheckpointManager.CreateJson(new FileSystemJsonCheckpointStore(checkpointFolder));
|
||||
Checkpointed<StreamingRun> run = await InProcessExecution.StreamAsync(workflow, input, checkpointManager);
|
||||
#else
|
||||
// Use an in-memory checkpoint store that will not persist checkpoints beyond the lifetime of the process.
|
||||
CheckpointManager checkpointManager = CheckpointManager.CreateInMemory();
|
||||
#endif
|
||||
|
||||
bool isComplete = false;
|
||||
InputResponse? response = null;
|
||||
@@ -163,6 +174,9 @@ internal sealed class Program
|
||||
Debug.WriteLine($"STEP ERROR #{executorFailure.ExecutorId}: {executorFailure.Data?.Message ?? "Unknown"}");
|
||||
break;
|
||||
|
||||
case WorkflowErrorEvent workflowError:
|
||||
throw workflowError.Data as Exception ?? new InvalidOperationException("Unexpected failure...");
|
||||
|
||||
case SuperStepCompletedEvent checkpointCompleted:
|
||||
this.LastCheckpoint = checkpointCompleted.CompletionInfo?.Checkpoint;
|
||||
Debug.WriteLine($"CHECKPOINT x{checkpointCompleted.StepNumber} [{this.LastCheckpoint?.CheckpointId ?? "(none)"}]");
|
||||
|
||||
+4
-4
@@ -53,21 +53,21 @@ internal sealed class JudgeExecutor() : ReflectingExecutor<JudgeExecutor>("Judge
|
||||
this._targetNumber = targetNumber;
|
||||
}
|
||||
|
||||
public async ValueTask HandleAsync(int message, IWorkflowContext context)
|
||||
public async ValueTask HandleAsync(int message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
this._tries++;
|
||||
if (message == this._targetNumber)
|
||||
{
|
||||
await context.YieldOutputAsync($"{this._targetNumber} found in {this._tries} tries!")
|
||||
await context.YieldOutputAsync($"{this._targetNumber} found in {this._tries} tries!", cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
else if (message < this._targetNumber)
|
||||
{
|
||||
await context.SendMessageAsync(NumberSignal.Below).ConfigureAwait(false);
|
||||
await context.SendMessageAsync(NumberSignal.Below, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
await context.SendMessageAsync(NumberSignal.Above).ConfigureAwait(false);
|
||||
await context.SendMessageAsync(NumberSignal.Above, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,20 +83,20 @@ internal sealed class GuessNumberExecutor : ReflectingExecutor<GuessNumberExecut
|
||||
|
||||
private int NextGuess => (this.LowerBound + this.UpperBound) / 2;
|
||||
|
||||
public async ValueTask HandleAsync(NumberSignal message, IWorkflowContext context)
|
||||
public async ValueTask HandleAsync(NumberSignal message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
switch (message)
|
||||
{
|
||||
case NumberSignal.Init:
|
||||
await context.SendMessageAsync(this.NextGuess).ConfigureAwait(false);
|
||||
await context.SendMessageAsync(this.NextGuess, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
break;
|
||||
case NumberSignal.Above:
|
||||
this.UpperBound = this.NextGuess - 1;
|
||||
await context.SendMessageAsync(this.NextGuess).ConfigureAwait(false);
|
||||
await context.SendMessageAsync(this.NextGuess, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
break;
|
||||
case NumberSignal.Below:
|
||||
this.LowerBound = this.NextGuess + 1;
|
||||
await context.SendMessageAsync(this.NextGuess).ConfigureAwait(false);
|
||||
await context.SendMessageAsync(this.NextGuess, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -120,21 +120,21 @@ internal sealed class JudgeExecutor : ReflectingExecutor<JudgeExecutor>, IMessag
|
||||
this._targetNumber = targetNumber;
|
||||
}
|
||||
|
||||
public async ValueTask HandleAsync(int message, IWorkflowContext context)
|
||||
public async ValueTask HandleAsync(int message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
this._tries++;
|
||||
if (message == this._targetNumber)
|
||||
{
|
||||
await context.YieldOutputAsync($"{this._targetNumber} found in {this._tries} tries!")
|
||||
await context.YieldOutputAsync($"{this._targetNumber} found in {this._tries} tries!", cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
else if (message < this._targetNumber)
|
||||
{
|
||||
await context.SendMessageAsync(NumberSignal.Below).ConfigureAwait(false);
|
||||
await context.SendMessageAsync(NumberSignal.Below, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
await context.SendMessageAsync(NumberSignal.Above).ConfigureAwait(false);
|
||||
await context.SendMessageAsync(NumberSignal.Above, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.Monitor.OpenTelemetry.Exporter" />
|
||||
<PackageReference Include="OpenTelemetry" />
|
||||
<PackageReference Include="System.Diagnostics.DiagnosticSource" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Workflows\Microsoft.Agents.AI.Workflows.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,101 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Diagnostics;
|
||||
using Azure.Monitor.OpenTelemetry.Exporter;
|
||||
using Microsoft.Agents.AI.Workflows;
|
||||
using Microsoft.Agents.AI.Workflows.Reflection;
|
||||
using OpenTelemetry;
|
||||
using OpenTelemetry.Resources;
|
||||
using OpenTelemetry.Trace;
|
||||
|
||||
namespace WorkflowObservabilitySample;
|
||||
|
||||
/// <summary>
|
||||
/// This sample shows how to enable observability in a workflow and send the traces
|
||||
/// to be visualized in Application Insights.
|
||||
///
|
||||
/// In this example, we create a simple text processing pipeline that:
|
||||
/// 1. Takes input text and converts it to uppercase using an UppercaseExecutor
|
||||
/// 2. Takes the uppercase text and reverses it using a ReverseTextExecutor
|
||||
///
|
||||
/// The executors are connected sequentially, so data flows from one to the next in order.
|
||||
/// For input "Hello, World!", the workflow produces "!DLROW ,OLLEH".
|
||||
/// </summary>
|
||||
public static class Program
|
||||
{
|
||||
private const string SourceName = "Workflow.ApplicationInsightsSample";
|
||||
private static readonly ActivitySource s_activitySource = new(SourceName);
|
||||
|
||||
private static async Task Main()
|
||||
{
|
||||
var applicationInsightsConnectionString = Environment.GetEnvironmentVariable("APPLICATIONINSIGHTS_CONNECTION_STRING") ?? throw new InvalidOperationException("APPLICATIONINSIGHTS_CONNECTION_STRING is not set.");
|
||||
|
||||
var resourceBuilder = ResourceBuilder
|
||||
.CreateDefault()
|
||||
.AddService("WorkflowSample");
|
||||
|
||||
using var traceProvider = Sdk.CreateTracerProviderBuilder()
|
||||
.SetResourceBuilder(resourceBuilder)
|
||||
.AddSource("Microsoft.Agents.AI.Workflows*")
|
||||
.AddSource(SourceName)
|
||||
.AddAzureMonitorTraceExporter(options => options.ConnectionString = applicationInsightsConnectionString)
|
||||
.Build();
|
||||
|
||||
// Start a root activity for the application
|
||||
using var activity = s_activitySource.StartActivity("main");
|
||||
Console.WriteLine($"Operation/Trace ID: {Activity.Current?.TraceId}");
|
||||
|
||||
// Create the executors
|
||||
UppercaseExecutor uppercase = new();
|
||||
ReverseTextExecutor reverse = new();
|
||||
|
||||
// Build the workflow by connecting executors sequentially
|
||||
var workflow = new WorkflowBuilder(uppercase)
|
||||
.AddEdge(uppercase, reverse)
|
||||
.Build();
|
||||
|
||||
// Execute the workflow with input data
|
||||
Run run = await InProcessExecution.RunAsync(workflow, "Hello, World!");
|
||||
foreach (WorkflowEvent evt in run.NewEvents)
|
||||
{
|
||||
if (evt is ExecutorCompletedEvent executorComplete)
|
||||
{
|
||||
Console.WriteLine($"{executorComplete.ExecutorId}: {executorComplete.Data}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// First executor: converts input text to uppercase.
|
||||
/// </summary>
|
||||
internal sealed class UppercaseExecutor() : ReflectingExecutor<UppercaseExecutor>("UppercaseExecutor"), IMessageHandler<string, string>
|
||||
{
|
||||
/// <summary>
|
||||
/// Processes the input message by converting it to uppercase.
|
||||
/// </summary>
|
||||
/// <param name="message">The input text to convert</param>
|
||||
/// <param name="context">Workflow context for accessing workflow services and adding events</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.
|
||||
/// The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>The input text converted to uppercase</returns>
|
||||
public async ValueTask<string> HandleAsync(string message, IWorkflowContext context, CancellationToken cancellationToken = default) =>
|
||||
message.ToUpperInvariant(); // The return value will be sent as a message along an edge to subsequent executors
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Second executor: reverses the input text and completes the workflow.
|
||||
/// </summary>
|
||||
internal sealed class ReverseTextExecutor() : ReflectingExecutor<ReverseTextExecutor>("ReverseTextExecutor"), IMessageHandler<string, string>
|
||||
{
|
||||
/// <summary>
|
||||
/// Processes the input message by reversing the text.
|
||||
/// </summary>
|
||||
/// <param name="message">The input text to reverse</param>
|
||||
/// <param name="context">Workflow context for accessing workflow services and adding events</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.
|
||||
/// The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>The input text reversed</returns>
|
||||
public async ValueTask<string> HandleAsync(string message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
=> new(message.Reverse().ToArray());
|
||||
}
|
||||
@@ -78,8 +78,10 @@ internal sealed class UppercaseExecutor() : ReflectingExecutor<UppercaseExecutor
|
||||
/// </summary>
|
||||
/// <param name="message">The input text to convert</param>
|
||||
/// <param name="context">Workflow context for accessing workflow services and adding events</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.
|
||||
/// The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>The input text converted to uppercase</returns>
|
||||
public async ValueTask<string> HandleAsync(string message, IWorkflowContext context) =>
|
||||
public async ValueTask<string> HandleAsync(string message, IWorkflowContext context, CancellationToken cancellationToken = default) =>
|
||||
message.ToUpperInvariant(); // The return value will be sent as a message along an edge to subsequent executors
|
||||
}
|
||||
|
||||
@@ -93,6 +95,9 @@ internal sealed class ReverseTextExecutor() : ReflectingExecutor<ReverseTextExec
|
||||
/// </summary>
|
||||
/// <param name="message">The input text to reverse</param>
|
||||
/// <param name="context">Workflow context for accessing workflow services and adding events</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.
|
||||
/// The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>The input text reversed</returns>
|
||||
public async ValueTask<string> HandleAsync(string message, IWorkflowContext context) => new string(message.Reverse().ToArray());
|
||||
public async ValueTask<string> HandleAsync(string message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
=> new(message.Reverse().ToArray());
|
||||
}
|
||||
|
||||
@@ -54,13 +54,13 @@ internal static class FileContentStateConstants
|
||||
|
||||
internal sealed class FileReadExecutor() : ReflectingExecutor<FileReadExecutor>("FileReadExecutor"), IMessageHandler<string, string>
|
||||
{
|
||||
public async ValueTask<string> HandleAsync(string message, IWorkflowContext context)
|
||||
public async ValueTask<string> HandleAsync(string message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Read file content from embedded resource
|
||||
string fileContent = Resources.Read(message);
|
||||
// Store file content in a shared state for access by other executors
|
||||
string fileID = Guid.NewGuid().ToString("N");
|
||||
await context.QueueStateUpdateAsync(fileID, fileContent, scopeName: FileContentStateConstants.FileContentStateScope);
|
||||
await context.QueueStateUpdateAsync(fileID, fileContent, scopeName: FileContentStateConstants.FileContentStateScope, cancellationToken);
|
||||
|
||||
return fileID;
|
||||
}
|
||||
@@ -74,10 +74,10 @@ internal sealed class FileStats
|
||||
|
||||
internal sealed class WordCountingExecutor() : ReflectingExecutor<WordCountingExecutor>("WordCountingExecutor"), IMessageHandler<string, FileStats>
|
||||
{
|
||||
public async ValueTask<FileStats> HandleAsync(string message, IWorkflowContext context)
|
||||
public async ValueTask<FileStats> HandleAsync(string message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Retrieve the file content from the shared state
|
||||
var fileContent = await context.ReadStateAsync<string>(message, scopeName: FileContentStateConstants.FileContentStateScope)
|
||||
var fileContent = await context.ReadStateAsync<string>(message, scopeName: FileContentStateConstants.FileContentStateScope, cancellationToken)
|
||||
?? throw new InvalidOperationException("File content state not found");
|
||||
|
||||
int wordCount = fileContent.Split([' ', '\n', '\r'], StringSplitOptions.RemoveEmptyEntries).Length;
|
||||
@@ -86,12 +86,13 @@ internal sealed class WordCountingExecutor() : ReflectingExecutor<WordCountingEx
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class ParagraphCountingExecutor() : ReflectingExecutor<ParagraphCountingExecutor>("ParagraphCountingExecutor"), IMessageHandler<string, FileStats>
|
||||
internal sealed class ParagraphCountingExecutor() : ReflectingExecutor<ParagraphCountingExecutor>("ParagraphCountingExecutor"),
|
||||
IMessageHandler<string, FileStats>
|
||||
{
|
||||
public async ValueTask<FileStats> HandleAsync(string message, IWorkflowContext context)
|
||||
public async ValueTask<FileStats> HandleAsync(string message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Retrieve the file content from the shared state
|
||||
var fileContent = await context.ReadStateAsync<string>(message, scopeName: FileContentStateConstants.FileContentStateScope)
|
||||
var fileContent = await context.ReadStateAsync<string>(message, scopeName: FileContentStateConstants.FileContentStateScope, cancellationToken)
|
||||
?? throw new InvalidOperationException("File content state not found");
|
||||
|
||||
int paragraphCount = fileContent.Split(['\n', '\r'], StringSplitOptions.RemoveEmptyEntries).Length;
|
||||
@@ -104,7 +105,7 @@ internal sealed class AggregationExecutor() : ReflectingExecutor<AggregationExec
|
||||
{
|
||||
private readonly List<FileStats> _messages = [];
|
||||
|
||||
public async ValueTask HandleAsync(FileStats message, IWorkflowContext context)
|
||||
public async ValueTask HandleAsync(FileStats message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
this._messages.Add(message);
|
||||
|
||||
@@ -113,7 +114,7 @@ internal sealed class AggregationExecutor() : ReflectingExecutor<AggregationExec
|
||||
// Aggregate the results from both executors
|
||||
var totalParagraphCount = this._messages.Sum(m => m.ParagraphCount);
|
||||
var totalWordCount = this._messages.Sum(m => m.WordCount);
|
||||
await context.YieldOutputAsync($"Total Paragraphs: {totalParagraphCount}, Total Words: {totalWordCount}");
|
||||
await context.YieldOutputAsync($"Total Paragraphs: {totalParagraphCount}, Total Words: {totalWordCount}", cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+6
-2
@@ -51,8 +51,10 @@ internal sealed class UppercaseExecutor() : ReflectingExecutor<UppercaseExecutor
|
||||
/// </summary>
|
||||
/// <param name="message">The input text to convert</param>
|
||||
/// <param name="context">Workflow context for accessing workflow services and adding events</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.
|
||||
/// The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>The input text converted to uppercase</returns>
|
||||
public async ValueTask<string> HandleAsync(string message, IWorkflowContext context) =>
|
||||
public async ValueTask<string> HandleAsync(string message, IWorkflowContext context, CancellationToken cancellationToken = default) =>
|
||||
message.ToUpperInvariant(); // The return value will be sent as a message along an edge to subsequent executors
|
||||
}
|
||||
|
||||
@@ -66,8 +68,10 @@ internal sealed class ReverseTextExecutor() : ReflectingExecutor<ReverseTextExec
|
||||
/// </summary>
|
||||
/// <param name="message">The input text to reverse</param>
|
||||
/// <param name="context">Workflow context for accessing workflow services and adding events</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.
|
||||
/// The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>The input text reversed</returns>
|
||||
public async ValueTask<string> HandleAsync(string message, IWorkflowContext context)
|
||||
public async ValueTask<string> HandleAsync(string message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Because we do not suppress it, the returned result will be yielded as an output from this executor.
|
||||
return string.Concat(message.Reverse());
|
||||
|
||||
@@ -50,8 +50,10 @@ internal sealed class UppercaseExecutor() : ReflectingExecutor<UppercaseExecutor
|
||||
/// </summary>
|
||||
/// <param name="message">The input text to convert</param>
|
||||
/// <param name="context">Workflow context for accessing workflow services and adding events</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.
|
||||
/// The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>The input text converted to uppercase</returns>
|
||||
public async ValueTask<string> HandleAsync(string message, IWorkflowContext context) =>
|
||||
public async ValueTask<string> HandleAsync(string message, IWorkflowContext context, CancellationToken cancellationToken = default) =>
|
||||
message.ToUpperInvariant(); // The return value will be sent as a message along an edge to subsequent executors
|
||||
}
|
||||
|
||||
@@ -65,8 +67,10 @@ internal sealed class ReverseTextExecutor() : ReflectingExecutor<ReverseTextExec
|
||||
/// </summary>
|
||||
/// <param name="message">The input text to reverse</param>
|
||||
/// <param name="context">Workflow context for accessing workflow services and adding events</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.
|
||||
/// The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>The input text reversed</returns>
|
||||
public async ValueTask<string> HandleAsync(string message, IWorkflowContext context)
|
||||
public async ValueTask<string> HandleAsync(string message, IWorkflowContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
// Because we do not suppress it, the returned result will be yielded as an output from this executor.
|
||||
return string.Concat(message.Reverse());
|
||||
|
||||
@@ -37,7 +37,13 @@ public sealed class AzureAgentProvider(string projectEndpoint, TokenCredential p
|
||||
/// <inheritdoc/>
|
||||
public override async Task<string> CreateConversationAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
PersistentAgentThread conversation = await this.GetAgentsClient().Threads.CreateThreadAsync(cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
PersistentAgentThread conversation =
|
||||
await this.GetAgentsClient().Threads.CreateThreadAsync(
|
||||
messages: null,
|
||||
toolResources: null,
|
||||
metadata: null,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return conversation.Id;
|
||||
}
|
||||
|
||||
@@ -78,6 +84,7 @@ public sealed class AzureAgentProvider(string projectEndpoint, TokenCredential p
|
||||
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())),
|
||||
DataContent dataContent when dataContent.Uri is not null => new MessageInputImageUriBlock(new MessageImageUriParam(dataContent.Uri)),
|
||||
_ => null // Unsupported content type
|
||||
};
|
||||
|
||||
@@ -91,7 +98,7 @@ public sealed class AzureAgentProvider(string projectEndpoint, TokenCredential p
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async Task<AIAgent> GetAgentAsync(string agentId, CancellationToken cancellationToken = default) =>
|
||||
await this.GetAgentsClient().GetAIAgentAsync(agentId, chatOptions: null, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
await this.GetAgentsClient().GetAIAgentAsync(agentId, chatOptions: null, clientFactory: null, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async Task<ChatMessage> GetMessageAsync(string conversationId, string messageId, CancellationToken cancellationToken = default)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.Events;
|
||||
|
||||
/// <summary>
|
||||
@@ -12,6 +14,7 @@ public sealed class InputRequest
|
||||
/// </summary>
|
||||
public string Prompt { get; }
|
||||
|
||||
[JsonConstructor]
|
||||
internal InputRequest(string prompt)
|
||||
{
|
||||
this.Prompt = prompt;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.Events;
|
||||
|
||||
/// <summary>
|
||||
@@ -16,6 +18,7 @@ public sealed class InputResponse
|
||||
/// Initializes a new instance of the <see cref="InputResponse"/> class.
|
||||
/// </summary>
|
||||
/// <param name="value">The response value.</param>
|
||||
[JsonConstructor]
|
||||
public InputResponse(string value)
|
||||
{
|
||||
this.Value = value;
|
||||
|
||||
+19
-3
@@ -4,12 +4,21 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Azure.AI.Agents.Persistent;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.Extensions;
|
||||
|
||||
internal static class AgentProviderExtensions
|
||||
{
|
||||
private static readonly HashSet<Azure.AI.Agents.Persistent.RunStatus> s_failureStatus =
|
||||
[
|
||||
Azure.AI.Agents.Persistent.RunStatus.Failed,
|
||||
Azure.AI.Agents.Persistent.RunStatus.Cancelled,
|
||||
Azure.AI.Agents.Persistent.RunStatus.Cancelling,
|
||||
Azure.AI.Agents.Persistent.RunStatus.Expired,
|
||||
];
|
||||
|
||||
public static async ValueTask<AgentRunResponse> InvokeAgentAsync(
|
||||
this WorkflowAgentProvider agentProvider,
|
||||
string executorId,
|
||||
@@ -51,9 +60,16 @@ internal static class AgentProviderExtensions
|
||||
|
||||
updates.Add(update);
|
||||
|
||||
if (update.RawRepresentation is ChatResponseUpdate chatUpdate &&
|
||||
chatUpdate.RawRepresentation is RunUpdate runUpdate &&
|
||||
s_failureStatus.Contains(runUpdate.Value.Status))
|
||||
{
|
||||
throw new DeclarativeActionException($"Unexpected failure invoking agent, run {runUpdate.Value.Status}: {agent.Name ?? agent.Id} [{runUpdate.Value.Id}/{conversationId}]");
|
||||
}
|
||||
|
||||
if (autoSend)
|
||||
{
|
||||
await context.AddEventAsync(new AgentRunUpdateEvent(executorId, update)).ConfigureAwait(false);
|
||||
await context.AddEventAsync(new AgentRunUpdateEvent(executorId, update), cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,7 +77,7 @@ internal static class AgentProviderExtensions
|
||||
|
||||
if (autoSend)
|
||||
{
|
||||
await context.AddEventAsync(new AgentRunResponseEvent(executorId, response)).ConfigureAwait(false);
|
||||
await context.AddEventAsync(new AgentRunResponseEvent(executorId, response), cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (autoSend && !isWorkflowConversation && workflowConversationId is not null)
|
||||
@@ -87,7 +103,7 @@ internal static class AgentProviderExtensions
|
||||
{
|
||||
conversationId = assignValue;
|
||||
|
||||
await context.QueueConversationUpdateAsync(conversationId).ConfigureAwait(false);
|
||||
await context.QueueConversationUpdateAsync(conversationId, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+7
-2
@@ -131,7 +131,7 @@ internal static class ChatMessageExtensions
|
||||
return
|
||||
contentType switch
|
||||
{
|
||||
AgentMessageContentType.ImageUrl => new UriContent(contentValue, "image/*"),
|
||||
AgentMessageContentType.ImageUrl => GetImageContent(contentValue),
|
||||
AgentMessageContentType.ImageFile => new HostedFileContent(contentValue),
|
||||
_ => new TextContent(contentValue)
|
||||
};
|
||||
@@ -169,7 +169,7 @@ internal static class ChatMessageExtensions
|
||||
yield return
|
||||
contentItem?.GetProperty<StringDataValue>(TypeSchema.Message.Fields.ContentType)?.Value switch
|
||||
{
|
||||
TypeSchema.Message.ContentTypes.ImageUrl => new UriContent(contentValue.Value, "image/*"),
|
||||
TypeSchema.Message.ContentTypes.ImageUrl => GetImageContent(contentValue.Value),
|
||||
TypeSchema.Message.ContentTypes.ImageFile => new HostedFileContent(contentValue.Value),
|
||||
_ => new TextContent(contentValue.Value)
|
||||
};
|
||||
@@ -177,6 +177,11 @@ internal static class ChatMessageExtensions
|
||||
}
|
||||
}
|
||||
|
||||
private static AIContent GetImageContent(string uriText) =>
|
||||
uriText.StartsWith("data:", StringComparison.OrdinalIgnoreCase) ?
|
||||
new DataContent(uriText, "image/*") :
|
||||
new UriContent(uriText, "image/*");
|
||||
|
||||
private static TValue? GetProperty<TValue>(this RecordDataValue record, string name)
|
||||
where TValue : DataValue
|
||||
{
|
||||
|
||||
@@ -148,8 +148,6 @@ internal static class DataValueExtensions
|
||||
|
||||
IEnumerable<KeyValuePair<string, DataValue>> GetFields()
|
||||
{
|
||||
yield return new KeyValuePair<string, DataValue>(TypeSchema.Discriminator, nameof(ExpandoObject).ToDataValue());
|
||||
|
||||
foreach (string key in value.Keys)
|
||||
{
|
||||
yield return new KeyValuePair<string, DataValue>(key, value[key].ToDataValue());
|
||||
@@ -252,7 +250,6 @@ internal static class DataValueExtensions
|
||||
private static Dictionary<string, object?> ToDictionary(this RecordDataValue record)
|
||||
{
|
||||
Dictionary<string, object?> result = [];
|
||||
result[TypeSchema.Discriminator] = nameof(ExpandoObject);
|
||||
foreach (KeyValuePair<string, DataValue> property in record.Properties)
|
||||
{
|
||||
result[property.Key] = property.Value.ToObject();
|
||||
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Dynamic;
|
||||
using System.Linq;
|
||||
using Microsoft.PowerFx.Types;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.Extensions;
|
||||
|
||||
internal static class ExpandoObjectExtensions
|
||||
{
|
||||
public static RecordType ToRecordType(this ExpandoObject value)
|
||||
{
|
||||
RecordType recordType = RecordType.Empty();
|
||||
|
||||
foreach (KeyValuePair<string, object?> property in value)
|
||||
{
|
||||
recordType.Add(property.Key, property.Value.GetFormulaType());
|
||||
}
|
||||
|
||||
return recordType;
|
||||
}
|
||||
|
||||
public static RecordValue ToRecord(this ExpandoObject value) =>
|
||||
FormulaValue.NewRecordFromFields(
|
||||
value.Select(
|
||||
property => new NamedValue(property.Key, property.Value.ToFormula())));
|
||||
}
|
||||
+6
-15
@@ -123,6 +123,8 @@ internal static class FormulaValueExtensions
|
||||
_ => DataType.Unspecified,
|
||||
};
|
||||
|
||||
public static object AsPortable(this FormulaValue? value) => (value?.ToObject()).AsPortable();
|
||||
|
||||
public static string Format(this FormulaValue value) =>
|
||||
value switch
|
||||
{
|
||||
@@ -161,6 +163,10 @@ internal static class FormulaValueExtensions
|
||||
}
|
||||
}
|
||||
}
|
||||
public static RecordValue ToRecord(this Dictionary<string, PortableValue> value) =>
|
||||
FormulaValue.NewRecordFromFields(
|
||||
value.Select(
|
||||
property => new NamedValue(property.Key, property.Value.ToFormula())));
|
||||
|
||||
private static RecordDataType ToDataType(this RecordType record)
|
||||
{
|
||||
@@ -182,21 +188,6 @@ internal static class FormulaValueExtensions
|
||||
return tableType;
|
||||
}
|
||||
|
||||
private static RecordType ToRecordType(this ExpandoObject value)
|
||||
{
|
||||
RecordType recordType = RecordType.Empty();
|
||||
foreach (KeyValuePair<string, object?> property in value)
|
||||
{
|
||||
recordType.Add(property.Key, property.Value.GetFormulaType());
|
||||
}
|
||||
return recordType;
|
||||
}
|
||||
|
||||
private static RecordValue ToRecord(this ExpandoObject value) =>
|
||||
FormulaValue.NewRecordFromFields(
|
||||
value.Select(
|
||||
property => new NamedValue(property.Key, property.Value.ToFormula())));
|
||||
|
||||
private static TableType ToTableType(this IEnumerable value)
|
||||
{
|
||||
foreach (object? element in value)
|
||||
|
||||
+37
-20
@@ -14,23 +14,11 @@ namespace Microsoft.Agents.AI.Workflows.Declarative.Extensions;
|
||||
|
||||
internal static class IWorkflowContextExtensions
|
||||
{
|
||||
public static ValueTask RaiseInvocationEventAsync(this IWorkflowContext context, DialogAction action, string? priorEventId = null) =>
|
||||
context.AddEventAsync(new DeclarativeActionInvokedEvent(action, priorEventId));
|
||||
public static ValueTask RaiseInvocationEventAsync(this IWorkflowContext context, DialogAction action, string? priorEventId = null, CancellationToken cancellationToken = default) =>
|
||||
context.AddEventAsync(new DeclarativeActionInvokedEvent(action, priorEventId), cancellationToken);
|
||||
|
||||
public static ValueTask RaiseCompletionEventAsync(this IWorkflowContext context, DialogAction action) =>
|
||||
context.AddEventAsync(new DeclarativeActionCompletedEvent(action));
|
||||
|
||||
public static ValueTask SendResultMessageAsync(this IWorkflowContext context, string id, object? result = null, CancellationToken cancellationToken = default) =>
|
||||
context.SendMessageAsync(new ActionExecutorResult(id, result));
|
||||
|
||||
public static ValueTask QueueStateResetAsync(this IWorkflowContext context, PropertyPath variablePath) =>
|
||||
context.QueueStateUpdateAsync(Throw.IfNull(variablePath.VariableName), UnassignedValue.Instance, Throw.IfNull(variablePath.NamespaceAlias));
|
||||
|
||||
public static ValueTask QueueStateUpdateAsync<TValue>(this IWorkflowContext context, PropertyPath variablePath, TValue? value) =>
|
||||
context.QueueStateUpdateAsync(Throw.IfNull(variablePath.VariableName), value, Throw.IfNull(variablePath.NamespaceAlias));
|
||||
|
||||
public static ValueTask QueueSystemUpdateAsync<TValue>(this IWorkflowContext context, string key, TValue? value) =>
|
||||
DeclarativeContext(context).QueueSystemUpdateAsync(key, value);
|
||||
public static ValueTask RaiseCompletionEventAsync(this IWorkflowContext context, DialogAction action, CancellationToken cancellationToken = default) =>
|
||||
context.AddEventAsync(new DeclarativeActionCompletedEvent(action), cancellationToken);
|
||||
|
||||
public static FormulaValue ReadState(this IWorkflowContext context, PropertyPath variablePath) =>
|
||||
context.ReadState(Throw.IfNull(variablePath.VariableName), Throw.IfNull(variablePath.NamespaceAlias));
|
||||
@@ -38,18 +26,47 @@ internal static class IWorkflowContextExtensions
|
||||
public static FormulaValue ReadState(this IWorkflowContext context, string key, string? scopeName = null) =>
|
||||
DeclarativeContext(context).State.Get(key, scopeName);
|
||||
|
||||
public static async ValueTask QueueConversationUpdateAsync(this IWorkflowContext context, string conversationId, bool isExternal = false)
|
||||
public static ValueTask SendResultMessageAsync(this IWorkflowContext context, string id, CancellationToken cancellationToken = default) =>
|
||||
context.SendResultMessageAsync(id, result: null, cancellationToken);
|
||||
|
||||
public static ValueTask SendResultMessageAsync(this IWorkflowContext context, string id, object? result, CancellationToken cancellationToken = default) =>
|
||||
context.SendMessageAsync(new ActionExecutorResult(id, result), targetId: null, cancellationToken);
|
||||
|
||||
public static ValueTask QueueStateResetAsync(this IWorkflowContext context, PropertyPath variablePath, CancellationToken cancellationToken = default) =>
|
||||
context.QueueStateUpdateAsync(Throw.IfNull(variablePath.VariableName), UnassignedValue.Instance, Throw.IfNull(variablePath.NamespaceAlias), cancellationToken);
|
||||
|
||||
public static ValueTask QueueStateUpdateAsync<TValue>(this IWorkflowContext context, PropertyPath variablePath, TValue? value, CancellationToken cancellationToken = default) =>
|
||||
context.QueueStateUpdateAsync(Throw.IfNull(variablePath.VariableName), value, Throw.IfNull(variablePath.NamespaceAlias), cancellationToken);
|
||||
|
||||
public static async ValueTask QueueEnvironmentUpdateAsync<TValue>(this IWorkflowContext context, string key, TValue? value, CancellationToken cancellationToken = default)
|
||||
{
|
||||
DeclarativeWorkflowContext declarativeContext = DeclarativeContext(context);
|
||||
await declarativeContext.UpdateStateAsync(key, value, VariableScopeNames.Environment, allowSystem: true, cancellationToken).ConfigureAwait(false);
|
||||
declarativeContext.State.Bind();
|
||||
}
|
||||
|
||||
public static async ValueTask QueueSystemUpdateAsync<TValue>(this IWorkflowContext context, string key, TValue? value, CancellationToken cancellationToken = default)
|
||||
{
|
||||
DeclarativeWorkflowContext declarativeContext = DeclarativeContext(context);
|
||||
await declarativeContext.UpdateStateAsync(key, value, VariableScopeNames.System, allowSystem: true, cancellationToken).ConfigureAwait(false);
|
||||
declarativeContext.State.Bind();
|
||||
}
|
||||
|
||||
public static ValueTask QueueConversationUpdateAsync(this IWorkflowContext context, string conversationId, CancellationToken cancellationToken = default) =>
|
||||
context.QueueConversationUpdateAsync(conversationId, isExternal: false, cancellationToken);
|
||||
|
||||
public static async ValueTask QueueConversationUpdateAsync(this IWorkflowContext context, string conversationId, bool isExternal = false, CancellationToken cancellationToken = default)
|
||||
{
|
||||
RecordValue conversation = (RecordValue)context.ReadState(SystemScope.Names.Conversation, VariableScopeNames.System);
|
||||
|
||||
if (isExternal)
|
||||
{
|
||||
conversation.UpdateField("Id", FormulaValue.New(conversationId));
|
||||
await context.QueueSystemUpdateAsync(SystemScope.Names.Conversation, conversation).ConfigureAwait(false);
|
||||
await context.QueueSystemUpdateAsync(SystemScope.Names.ConversationId, FormulaValue.New(conversationId)).ConfigureAwait(false);
|
||||
await context.QueueSystemUpdateAsync(SystemScope.Names.Conversation, conversation, cancellationToken).ConfigureAwait(false);
|
||||
await context.QueueSystemUpdateAsync(SystemScope.Names.ConversationId, FormulaValue.New(conversationId), cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await context.AddEventAsync(new ConversationUpdateEvent(conversationId) { IsWorkflow = isExternal }).ConfigureAwait(false);
|
||||
await context.AddEventAsync(new ConversationUpdateEvent(conversationId) { IsWorkflow = isExternal }, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public static bool IsWorkflowConversation(
|
||||
|
||||
@@ -7,6 +7,7 @@ using System.Linq;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.Kit;
|
||||
using Microsoft.Bot.ObjectModel;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.PowerFx.Types;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.Extensions;
|
||||
@@ -46,6 +47,56 @@ internal static class ObjectExtensions
|
||||
}
|
||||
}
|
||||
|
||||
public static object AsPortable(this object? value) =>
|
||||
value switch
|
||||
{
|
||||
null => UnassignedValue.Instance,
|
||||
string or
|
||||
bool or
|
||||
int or
|
||||
float or
|
||||
long or
|
||||
decimal or
|
||||
double or
|
||||
DateTime or
|
||||
TimeSpan =>
|
||||
value,
|
||||
ChatMessage messageValue => messageValue.ToRecord().AsPortable(),
|
||||
IDictionary<string, object?> objectValue => objectValue.AsPortable(),
|
||||
IDictionary recordValue => recordValue.AsPortable(),
|
||||
IEnumerable tableValue => tableValue.AsPortable(),
|
||||
_ => throw new DeclarativeModelException($"Unsupported data type: {value.GetType().Name}"),
|
||||
};
|
||||
|
||||
public static object AsPortable(this IDictionary<string, object?> value) => value.ToDictionary(kvp => kvp.Key, kvp => new PortableValue(kvp.Value.AsPortable()));
|
||||
|
||||
public static object AsPortable(this IDictionary value)
|
||||
{
|
||||
return GetEntries().ToDictionary(kvp => kvp.Key, kvp => new PortableValue(kvp.Value.AsPortable()));
|
||||
|
||||
IEnumerable<KeyValuePair<string, object?>> GetEntries()
|
||||
{
|
||||
foreach (string key in value.Keys)
|
||||
{
|
||||
yield return new KeyValuePair<string, object?>(key, value[key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static object AsPortable(this IEnumerable value)
|
||||
{
|
||||
return GetValues().ToArray();
|
||||
|
||||
IEnumerable<PortableValue> GetValues()
|
||||
{
|
||||
IEnumerator enumerator = value.GetEnumerator();
|
||||
while (enumerator.MoveNext())
|
||||
{
|
||||
yield return new PortableValue(enumerator.Current.AsPortable());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static object? ConvertType(this object? sourceValue, VariableType targetType)
|
||||
{
|
||||
if (!targetType.IsValid())
|
||||
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.Kit;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.PowerFx.Types;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.Extensions;
|
||||
|
||||
internal static class PortableValueExtensions
|
||||
{
|
||||
public static FormulaValue ToFormula(this PortableValue value) =>
|
||||
value.TypeId switch
|
||||
{
|
||||
null => FormulaValue.NewBlank(),
|
||||
_ when value.TypeId.IsMatch<UnassignedValue>() => FormulaValue.NewBlank(),
|
||||
_ when value.IsType(out string? stringValue) => FormulaValue.New(stringValue),
|
||||
_ when value.IsSystemType(out bool? boolValue) => FormulaValue.New(boolValue.Value),
|
||||
_ when value.IsSystemType(out int? intValue) => FormulaValue.New(intValue.Value),
|
||||
_ when value.IsSystemType(out long? longValue) => FormulaValue.New(longValue.Value),
|
||||
_ when value.IsSystemType(out decimal? decimalValue) => FormulaValue.New(decimalValue.Value),
|
||||
_ when value.IsSystemType(out float? floatValue) => FormulaValue.New(floatValue.Value),
|
||||
_ when value.IsSystemType(out double? doubleValue) => FormulaValue.New(doubleValue.Value),
|
||||
_ when value.IsParentType(out Dictionary<string, PortableValue>? recordValue) => recordValue.ToRecord(),
|
||||
_ when value.IsParentType(out IDictionary? recordValue) => recordValue.ToRecord(),
|
||||
_ when value.IsType(out PortableValue[]? tableValue) => tableValue.ToTable(),
|
||||
_ when value.IsType(out ChatMessage? messageValue) => messageValue.ToRecord(),
|
||||
_ when value.IsType(out DateTime dateValue) =>
|
||||
dateValue.TimeOfDay == TimeSpan.Zero ?
|
||||
FormulaValue.NewDateOnly(dateValue.Date) :
|
||||
FormulaValue.New(dateValue),
|
||||
_ when value.IsType(out TimeSpan timeValue) => FormulaValue.New(timeValue),
|
||||
_ => throw new DeclarativeModelException($"Unsupported portable type: {value.TypeId.TypeName}"),
|
||||
};
|
||||
|
||||
private static TableValue ToTable(this PortableValue[] values)
|
||||
{
|
||||
FormulaValue[] formulaValues = values.Select(value => value.ToFormula()).ToArray();
|
||||
if (formulaValues[0] is RecordValue recordValue)
|
||||
{
|
||||
return FormulaValue.NewTable(ParseRecordType(recordValue), formulaValues.OfType<RecordValue>());
|
||||
}
|
||||
|
||||
return
|
||||
formulaValues[0] switch
|
||||
{
|
||||
PrimitiveValue<bool> => NewSingleColumnTable<bool>(),
|
||||
PrimitiveValue<string> => NewSingleColumnTable<string>(),
|
||||
PrimitiveValue<int> => NewSingleColumnTable<int>(),
|
||||
PrimitiveValue<long> => NewSingleColumnTable<long>(),
|
||||
PrimitiveValue<float> => NewSingleColumnTable<float>(),
|
||||
PrimitiveValue<decimal> => NewSingleColumnTable<decimal>(),
|
||||
PrimitiveValue<double> => NewSingleColumnTable<double>(),
|
||||
PrimitiveValue<TimeSpan> => NewSingleColumnTable<TimeSpan>(),
|
||||
PrimitiveValue<DateTime> => NewSingleColumnTable<DateTime>(),
|
||||
_ => throw new DeclarativeModelException($"Unsupported table element type: {formulaValues[0].Type.GetType().Name}"),
|
||||
};
|
||||
|
||||
TableValue NewSingleColumnTable<TValue>() =>
|
||||
FormulaValue.NewSingleColumnTable(formulaValues.OfType<PrimitiveValue<TValue>>());
|
||||
}
|
||||
|
||||
private static RecordType ParseRecordType(this RecordValue record)
|
||||
{
|
||||
RecordType recordType = RecordType.Empty();
|
||||
foreach (NamedValue property in record.Fields)
|
||||
{
|
||||
recordType = recordType.Add(property.Name, property.Value.Type);
|
||||
}
|
||||
return recordType;
|
||||
}
|
||||
|
||||
private static bool IsParentType<TValue>(this PortableValue value, [NotNullWhen(true)] out TValue? typedValue)
|
||||
{
|
||||
if (value.TypeId.IsMatchPolymorphic(typeof(TValue)))
|
||||
{
|
||||
return value.Is(out typedValue);
|
||||
}
|
||||
|
||||
typedValue = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool IsSystemType<TValue>(this PortableValue value, [NotNullWhen(true)] out TValue? typedValue) where TValue : struct
|
||||
{
|
||||
if (value.TypeId.IsMatch<TValue>() || value.TypeId.IsMatch(typeof(TValue).UnderlyingSystemType))
|
||||
{
|
||||
return value.Is(out typedValue);
|
||||
}
|
||||
|
||||
typedValue = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool IsType<TValue>(this PortableValue value, [NotNullWhen(true)] out TValue? typedValue)
|
||||
{
|
||||
if (value.TypeId.IsMatch<TValue>())
|
||||
{
|
||||
return value.Is(out typedValue);
|
||||
}
|
||||
|
||||
typedValue = default;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
+4
-4
@@ -61,7 +61,7 @@ internal abstract class DeclarativeActionExecutor : Executor<ActionExecutorResul
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async ValueTask HandleAsync(ActionExecutorResult message, IWorkflowContext context)
|
||||
public override async ValueTask HandleAsync(ActionExecutorResult message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (this.Model.Disabled)
|
||||
{
|
||||
@@ -69,7 +69,7 @@ internal abstract class DeclarativeActionExecutor : Executor<ActionExecutorResul
|
||||
return;
|
||||
}
|
||||
|
||||
await context.RaiseInvocationEventAsync(this.Model, message.ExecutorId).ConfigureAwait(false);
|
||||
await context.RaiseInvocationEventAsync(this.Model, message.ExecutorId, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
try
|
||||
{
|
||||
@@ -78,7 +78,7 @@ internal abstract class DeclarativeActionExecutor : Executor<ActionExecutorResul
|
||||
|
||||
if (this.EmitResultEvent)
|
||||
{
|
||||
await context.SendResultMessageAsync(this.Id, result).ConfigureAwait(false);
|
||||
await context.SendResultMessageAsync(this.Id, result, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (DeclarativeActionException exception)
|
||||
@@ -95,7 +95,7 @@ internal abstract class DeclarativeActionExecutor : Executor<ActionExecutorResul
|
||||
{
|
||||
if (this.IsDiscreteAction)
|
||||
{
|
||||
await context.RaiseCompletionEventAsync(this.Model).ConfigureAwait(false);
|
||||
await context.RaiseCompletionEventAsync(this.Model, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+31
-27
@@ -3,6 +3,7 @@
|
||||
using System.Collections.Frozen;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.Extensions;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.Kit;
|
||||
@@ -32,16 +33,18 @@ internal sealed class DeclarativeWorkflowContext : IWorkflowContext
|
||||
public IReadOnlyDictionary<string, string>? TraceContext => this.Source.TraceContext;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask AddEventAsync(WorkflowEvent workflowEvent) => this.Source.AddEventAsync(workflowEvent);
|
||||
public ValueTask AddEventAsync(WorkflowEvent workflowEvent, CancellationToken cancellationToken = default)
|
||||
=> this.Source.AddEventAsync(workflowEvent, cancellationToken);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask YieldOutputAsync(object output) => this.Source.YieldOutputAsync(output);
|
||||
public ValueTask YieldOutputAsync(object output, CancellationToken cancellationToken = default)
|
||||
=> this.Source.YieldOutputAsync(output, cancellationToken);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask RequestHaltAsync() => this.Source.RequestHaltAsync();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask QueueClearScopeAsync(string? scopeName = null)
|
||||
public async ValueTask QueueClearScopeAsync(string? scopeName = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (scopeName is not null)
|
||||
{
|
||||
@@ -50,12 +53,12 @@ internal sealed class DeclarativeWorkflowContext : IWorkflowContext
|
||||
// Copy keys to array to avoid modifying collection during enumeration.
|
||||
foreach (string key in this.State.Keys(scopeName).ToArray())
|
||||
{
|
||||
await this.UpdateStateAsync(key, UnassignedValue.Instance, scopeName).ConfigureAwait(false);
|
||||
await this.UpdateStateAsync(key, UnassignedValue.Instance, scopeName, allowSystem: false, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
await this.Source.QueueClearScopeAsync(scopeName).ConfigureAwait(false);
|
||||
await this.Source.QueueClearScopeAsync(scopeName, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
this.State.Bind();
|
||||
@@ -63,20 +66,14 @@ internal sealed class DeclarativeWorkflowContext : IWorkflowContext
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask QueueStateUpdateAsync<T>(string key, T? value, string? scopeName = null)
|
||||
public async ValueTask QueueStateUpdateAsync<T>(string key, T? value, string? scopeName = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await this.UpdateStateAsync(key, value, scopeName).ConfigureAwait(false);
|
||||
this.State.Bind();
|
||||
}
|
||||
|
||||
public async ValueTask QueueSystemUpdateAsync<TValue>(string key, TValue? value)
|
||||
{
|
||||
await this.UpdateStateAsync(key, value, VariableScopeNames.System, allowSystem: true).ConfigureAwait(false);
|
||||
await this.UpdateStateAsync(key, value, scopeName, allowSystem: false, cancellationToken).ConfigureAwait(false);
|
||||
this.State.Bind();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<TValue?> ReadStateAsync<TValue>(string key, string? scopeName = null)
|
||||
public async ValueTask<TValue?> ReadStateAsync<TValue>(string key, string? scopeName = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
bool isManagedScope =
|
||||
scopeName is not null && // null scope cannot be managed
|
||||
@@ -86,21 +83,23 @@ internal sealed class DeclarativeWorkflowContext : IWorkflowContext
|
||||
{
|
||||
// Not a managed scope, just pass through. This is valid when a declarative
|
||||
// workflow has been ejected to code (where DeclarativeWorkflowContext is also utilized).
|
||||
_ when !isManagedScope => await this.Source.ReadStateAsync<TValue>(key, scopeName).ConfigureAwait(false),
|
||||
_ when !isManagedScope => await this.Source.ReadStateAsync<TValue>(key, scopeName, cancellationToken).ConfigureAwait(false),
|
||||
// Retrieve formula values directly from the managed state to avoid conversion.
|
||||
_ when typeof(TValue) == typeof(FormulaValue) => (TValue?)(object?)this.State.Get(key, scopeName),
|
||||
// Retrieve native types from the source context to avoid conversion.
|
||||
_ => await this.Source.ReadStateAsync<TValue>(key, scopeName).ConfigureAwait(false),
|
||||
_ => await this.Source.ReadStateAsync<TValue>(key, scopeName, cancellationToken).ConfigureAwait(false),
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask<HashSet<string>> ReadStateKeysAsync(string? scopeName = null) => this.Source.ReadStateKeysAsync(scopeName);
|
||||
public ValueTask<HashSet<string>> ReadStateKeysAsync(string? scopeName = null, CancellationToken cancellationToken = default)
|
||||
=> this.Source.ReadStateKeysAsync(scopeName, cancellationToken);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask SendMessageAsync(object message, string? targetId = null) => this.Source.SendMessageAsync(message, targetId);
|
||||
public ValueTask SendMessageAsync(object message, string? targetId = null, CancellationToken cancellationToken = default)
|
||||
=> this.Source.SendMessageAsync(message, targetId, cancellationToken);
|
||||
|
||||
private ValueTask UpdateStateAsync<T>(string key, T? value, string? scopeName, bool allowSystem = true)
|
||||
public ValueTask UpdateStateAsync<T>(string key, T? value, string? scopeName, bool allowSystem, CancellationToken cancellationToken = default)
|
||||
{
|
||||
bool isManagedScope =
|
||||
scopeName is not null && // null scope cannot be managed
|
||||
@@ -110,7 +109,7 @@ internal sealed class DeclarativeWorkflowContext : IWorkflowContext
|
||||
{
|
||||
// Not a managed scope, just pass through. This is valid when a declarative
|
||||
// workflow has been ejected to code (where DeclarativeWorkflowContext is also utilized).
|
||||
return this.Source.QueueStateUpdateAsync(key, value, scopeName);
|
||||
return this.Source.QueueStateUpdateAsync(key, value, scopeName, cancellationToken);
|
||||
}
|
||||
|
||||
if (!ManagedScopes.Contains(scopeName!) && !allowSystem)
|
||||
@@ -134,7 +133,7 @@ internal sealed class DeclarativeWorkflowContext : IWorkflowContext
|
||||
{
|
||||
this.State.Set(key, FormulaValue.NewBlank(), scopeName);
|
||||
}
|
||||
return this.Source.QueueStateUpdateAsync(key, UnassignedValue.Instance, scopeName);
|
||||
return this.Source.QueueStateUpdateAsync(key, UnassignedValue.Instance, scopeName, cancellationToken);
|
||||
}
|
||||
|
||||
ValueTask QueueFormulaStateAsync(FormulaValue formulaValue)
|
||||
@@ -143,27 +142,32 @@ internal sealed class DeclarativeWorkflowContext : IWorkflowContext
|
||||
{
|
||||
this.State.Set(key, formulaValue, scopeName);
|
||||
}
|
||||
return this.Source.QueueStateUpdateAsync(key, formulaValue.ToObject(), scopeName);
|
||||
|
||||
return this.Source.QueueStateUpdateAsync(key, formulaValue.AsPortable(), scopeName, cancellationToken);
|
||||
}
|
||||
|
||||
ValueTask QueueDataValueStateAsync(DataValue dataValue)
|
||||
{
|
||||
FormulaValue formulaValue = dataValue.ToFormula();
|
||||
|
||||
if (isManagedScope)
|
||||
{
|
||||
FormulaValue formulaValue = dataValue.ToFormula();
|
||||
this.State.Set(key, formulaValue, scopeName);
|
||||
}
|
||||
return this.Source.QueueStateUpdateAsync(key, dataValue.ToObject(), scopeName);
|
||||
|
||||
return this.Source.QueueStateUpdateAsync(key, formulaValue.AsPortable(), scopeName, cancellationToken);
|
||||
}
|
||||
|
||||
ValueTask QueueNativeStateAsync(object? rawValue)
|
||||
ValueTask QueueNativeStateAsync(object rawValue)
|
||||
{
|
||||
FormulaValue formulaValue = rawValue.ToFormula();
|
||||
|
||||
if (isManagedScope)
|
||||
{
|
||||
FormulaValue formulaValue = rawValue.ToFormula();
|
||||
this.State.Set(key, formulaValue, scopeName);
|
||||
}
|
||||
return this.Source.QueueStateUpdateAsync(key, rawValue, scopeName);
|
||||
|
||||
return this.Source.QueueStateUpdateAsync(key, formulaValue.AsPortable(), scopeName, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+7
-6
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.Extensions;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.PowerFx;
|
||||
@@ -24,7 +25,7 @@ internal sealed class DeclarativeWorkflowExecutor<TInput>(
|
||||
return default;
|
||||
}
|
||||
|
||||
public override async ValueTask HandleAsync(TInput message, IWorkflowContext context)
|
||||
public override async ValueTask HandleAsync(TInput message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// No state to restore if we're starting from the beginning.
|
||||
state.SetInitialized();
|
||||
@@ -35,13 +36,13 @@ internal sealed class DeclarativeWorkflowExecutor<TInput>(
|
||||
string? conversationId = options.ConversationId;
|
||||
if (string.IsNullOrWhiteSpace(conversationId))
|
||||
{
|
||||
conversationId = await options.AgentProvider.CreateConversationAsync(cancellationToken: default).ConfigureAwait(false);
|
||||
conversationId = await options.AgentProvider.CreateConversationAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
await declarativeContext.QueueConversationUpdateAsync(conversationId, isExternal: true).ConfigureAwait(false);
|
||||
await declarativeContext.QueueConversationUpdateAsync(conversationId, isExternal: true, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
await options.AgentProvider.CreateMessageAsync(conversationId, input, cancellationToken: default).ConfigureAwait(false);
|
||||
await declarativeContext.SetLastMessageAsync(input).ConfigureAwait(false);
|
||||
ChatMessage inputMessage = await options.AgentProvider.CreateMessageAsync(conversationId, input, cancellationToken).ConfigureAwait(false);
|
||||
await declarativeContext.SetLastMessageAsync(inputMessage).ConfigureAwait(false);
|
||||
|
||||
await context.SendResultMessageAsync(this.Id).ConfigureAwait(false);
|
||||
await context.SendResultMessageAsync(this.Id, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
+6
-5
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Diagnostics;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.Extensions;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.Kit;
|
||||
@@ -11,11 +12,11 @@ namespace Microsoft.Agents.AI.Workflows.Declarative.Interpreter;
|
||||
internal sealed class DelegateActionExecutor(string actionId, WorkflowFormulaState state, DelegateAction<ActionExecutorResult>? action = null, bool emitResult = true)
|
||||
: DelegateActionExecutor<ActionExecutorResult>(actionId, state, action, emitResult)
|
||||
{
|
||||
public override ValueTask HandleAsync(ActionExecutorResult message, IWorkflowContext context)
|
||||
public override ValueTask HandleAsync(ActionExecutorResult message, IWorkflowContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
Debug.WriteLine($"RESULT #{this.Id} - {message.Result ?? "(null)"}");
|
||||
|
||||
return base.HandleAsync(message, context);
|
||||
return base.HandleAsync(message, context, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,16 +40,16 @@ internal class DelegateActionExecutor<TMessage> : Executor<TMessage>, IResettabl
|
||||
return default;
|
||||
}
|
||||
|
||||
public override async ValueTask HandleAsync(TMessage message, IWorkflowContext context)
|
||||
public override async ValueTask HandleAsync(TMessage message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (this._action is not null)
|
||||
{
|
||||
await this._action.Invoke(new DeclarativeWorkflowContext(context, this._state), message, default).ConfigureAwait(false);
|
||||
await this._action.Invoke(new DeclarativeWorkflowContext(context, this._state), message, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (this._emitResult)
|
||||
{
|
||||
await context.SendResultMessageAsync(this.Id).ConfigureAwait(false);
|
||||
await context.SendResultMessageAsync(this.Id, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,12 +73,12 @@ public abstract class ActionExecutor<TMessage> : Executor<TMessage>, IResettable
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async ValueTask HandleAsync(TMessage message, IWorkflowContext context)
|
||||
public override async ValueTask HandleAsync(TMessage message, IWorkflowContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
object? result = await this.ExecuteAsync(new DeclarativeWorkflowContext(context, this._session.State), message, cancellationToken: default).ConfigureAwait(false);
|
||||
Debug.WriteLine($"RESULT #{this.Id} - {result ?? "(null)"}");
|
||||
|
||||
await context.SendResultMessageAsync(this.Id, result).ConfigureAwait(false);
|
||||
await context.SendResultMessageAsync(this.Id, result, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
+4
-3
@@ -132,7 +132,7 @@ public static class IWorkflowContextExtensions
|
||||
/// <returns>The converted value</returns>
|
||||
public static async ValueTask<object?> ConvertValueAsync(this IWorkflowContext context, VariableType targetType, string key, string? scopeName = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
object? sourceValue = await context.ReadStateAsync<object>(key, scopeName).ConfigureAwait(false);
|
||||
object? sourceValue = await context.ReadStateAsync<object>(key, scopeName, cancellationToken).ConfigureAwait(false);
|
||||
return sourceValue.ConvertType(targetType);
|
||||
}
|
||||
|
||||
@@ -143,10 +143,11 @@ public static class IWorkflowContextExtensions
|
||||
/// <param name="context">The workflow execution context used to restore persisted state prior to formatting.</param>
|
||||
/// <param name="key">The key of the state value.</param>
|
||||
/// <param name = "scopeName" > An optional name that specifies the scope to read.If null, the default scope is used.</param>
|
||||
/// <param name="cancellationToken">A token that propagates notification when operation should be canceled.</param>
|
||||
/// <returns>The evaluated list expression</returns>
|
||||
public static async ValueTask<IList<TElement>?> ReadListAsync<TElement>(this IWorkflowContext context, string key, string? scopeName = null)
|
||||
public static async ValueTask<IList<TElement>?> ReadListAsync<TElement>(this IWorkflowContext context, string key, string? scopeName = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
object? value = await context.ReadStateAsync<object>(key, scopeName).ConfigureAwait(false);
|
||||
object? value = await context.ReadStateAsync<object>(key, scopeName, cancellationToken).ConfigureAwait(false);
|
||||
return value.AsList<TElement>();
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ using System.Threading.Tasks;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.Extensions;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.Interpreter;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.PowerFx;
|
||||
using Microsoft.Bot.ObjectModel;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
@@ -55,23 +54,23 @@ public abstract class RootExecutor<TInput> : Executor<TInput>, IResettableExecut
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async ValueTask HandleAsync(TInput message, IWorkflowContext context)
|
||||
public override async ValueTask HandleAsync(TInput message, IWorkflowContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
DeclarativeWorkflowContext declarativeContext = new(context, this._state);
|
||||
await this.ExecuteAsync(message, declarativeContext, cancellationToken: default).ConfigureAwait(false);
|
||||
await this.ExecuteAsync(message, declarativeContext, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
ChatMessage input = (this._inputTransform ?? DefaultInputTransform).Invoke(message);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(this._conversationId))
|
||||
{
|
||||
this._conversationId = await this._agentProvider.CreateConversationAsync(cancellationToken: default).ConfigureAwait(false);
|
||||
this._conversationId = await this._agentProvider.CreateConversationAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
await declarativeContext.QueueConversationUpdateAsync(this._conversationId, isExternal: true).ConfigureAwait(false);
|
||||
await declarativeContext.QueueConversationUpdateAsync(this._conversationId, isExternal: true, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
await this._agentProvider.CreateMessageAsync(this._conversationId, input, cancellationToken: default).ConfigureAwait(false);
|
||||
await declarativeContext.SetLastMessageAsync(input).ConfigureAwait(false);
|
||||
ChatMessage inputMessage = await this._agentProvider.CreateMessageAsync(this._conversationId, input, cancellationToken).ConfigureAwait(false);
|
||||
await declarativeContext.SetLastMessageAsync(inputMessage).ConfigureAwait(false);
|
||||
|
||||
await declarativeContext.SendMessageAsync(new ActionExecutorResult(this.Id)).ConfigureAwait(false);
|
||||
await declarativeContext.SendResultMessageAsync(this.Id, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -94,7 +93,7 @@ public abstract class RootExecutor<TInput> : Executor<TInput>, IResettableExecut
|
||||
{
|
||||
foreach (string variableName in variableNames)
|
||||
{
|
||||
await context.QueueStateUpdateAsync(variableName, GetEnvironmentVariable(variableName), VariableScopeNames.Environment).ConfigureAwait(false);
|
||||
await context.QueueEnvironmentUpdateAsync(variableName, GetEnvironmentVariable(variableName)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
string GetEnvironmentVariable(string name)
|
||||
|
||||
+3
-4
@@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>$(ProjectsTargetFrameworks)</TargetFrameworks>
|
||||
@@ -21,16 +21,15 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.AI.Agents.Persistent" />
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
<PackageReference Include="Microsoft.Bot.ObjectModel" />
|
||||
<PackageReference Include="Microsoft.Bot.ObjectModel.Json" />
|
||||
<PackageReference Include="Microsoft.Bot.ObjectModel.PowerFx" />
|
||||
<PackageReference Include="Microsoft.PowerFx.Interpreter" />
|
||||
<PackageReference Include="System.CodeDom" />
|
||||
<PackageReference Include="System.Collections.Immutable" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" />
|
||||
<PackageReference Include="System.CodeDom" />
|
||||
<PackageReference Include="System.Collections.Immutable" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@ internal sealed class ClearAllVariablesExecutor(ClearAllVariables model, Workflo
|
||||
|
||||
if (scope is not null)
|
||||
{
|
||||
await context.QueueClearScopeAsync(scope).ConfigureAwait(false);
|
||||
await context.QueueClearScopeAsync(scope, cancellationToken).ConfigureAwait(false);
|
||||
Debug.WriteLine(
|
||||
$"""
|
||||
STATE: {this.GetType().Name} [{this.Id}]
|
||||
|
||||
+1
-1
@@ -69,5 +69,5 @@ internal sealed class ConditionGroupExecutor : DeclarativeActionExecutor<Conditi
|
||||
}
|
||||
|
||||
public async ValueTask DoneAsync(IWorkflowContext context, ActionExecutorResult _, CancellationToken cancellationToken) =>
|
||||
await context.RaiseCompletionEventAsync(this.Model).ConfigureAwait(false);
|
||||
await context.RaiseCompletionEventAsync(this.Model, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ internal sealed class CreateConversationExecutor(CreateConversation model, Workf
|
||||
{
|
||||
string conversationId = await agentProvider.CreateConversationAsync(cancellationToken).ConfigureAwait(false);
|
||||
await this.AssignAsync(this.Model.ConversationId?.Path, FormulaValue.New(conversationId), context).ConfigureAwait(false);
|
||||
await context.QueueConversationUpdateAsync(conversationId).ConfigureAwait(false);
|
||||
await context.QueueConversationUpdateAsync(conversationId, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
@@ -68,11 +68,11 @@ internal sealed class ForeachExecutor : DeclarativeActionExecutor<Foreach>
|
||||
{
|
||||
FormulaValue value = this._values[this._index];
|
||||
|
||||
await context.QueueStateUpdateAsync(Throw.IfNull(this.Model.Value), value).ConfigureAwait(false);
|
||||
await context.QueueStateUpdateAsync(Throw.IfNull(this.Model.Value), value, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (this.Model.Index is not null)
|
||||
{
|
||||
await context.QueueStateUpdateAsync(this.Model.Index.Path, FormulaValue.New(this._index)).ConfigureAwait(false);
|
||||
await context.QueueStateUpdateAsync(this.Model.Index.Path, FormulaValue.New(this._index), cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
this._index++;
|
||||
@@ -83,15 +83,15 @@ internal sealed class ForeachExecutor : DeclarativeActionExecutor<Foreach>
|
||||
{
|
||||
try
|
||||
{
|
||||
await context.QueueStateResetAsync(Throw.IfNull(this.Model.Value)).ConfigureAwait(false);
|
||||
await context.QueueStateResetAsync(Throw.IfNull(this.Model.Value), cancellationToken).ConfigureAwait(false);
|
||||
if (this.Model.Index is not null)
|
||||
{
|
||||
await context.QueueStateResetAsync(this.Model.Index).ConfigureAwait(false);
|
||||
await context.QueueStateResetAsync(this.Model.Index, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
await context.RaiseCompletionEventAsync(this.Model).ConfigureAwait(false);
|
||||
await context.RaiseCompletionEventAsync(this.Model, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+8
-8
@@ -65,7 +65,7 @@ internal sealed class QuestionExecutor(Question model, WorkflowFormulaState stat
|
||||
}
|
||||
else
|
||||
{
|
||||
await context.SendResultMessageAsync(this.Id, result: null, cancellationToken).ConfigureAwait(false);
|
||||
await context.SendResultMessageAsync(this.Id, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return default;
|
||||
@@ -75,7 +75,7 @@ internal sealed class QuestionExecutor(Question model, WorkflowFormulaState stat
|
||||
{
|
||||
int count = await this._promptCount.ReadAsync(context).ConfigureAwait(false);
|
||||
InputRequest inputRequest = new(this.FormatPrompt(this.Model.Prompt));
|
||||
await context.SendMessageAsync(inputRequest).ConfigureAwait(false);
|
||||
await context.SendMessageAsync(inputRequest, targetId: null, cancellationToken).ConfigureAwait(false);
|
||||
await this._promptCount.WriteAsync(context, count + 1).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ internal sealed class QuestionExecutor(Question model, WorkflowFormulaState stat
|
||||
if (string.IsNullOrWhiteSpace(message.Value))
|
||||
{
|
||||
string unrecognizedResponse = this.FormatPrompt(this.Model.UnrecognizedPrompt);
|
||||
await context.AddEventAsync(new MessageActivityEvent(unrecognizedResponse.Trim())).ConfigureAwait(false);
|
||||
await context.AddEventAsync(new MessageActivityEvent(unrecognizedResponse.Trim()), cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -97,7 +97,7 @@ internal sealed class QuestionExecutor(Question model, WorkflowFormulaState stat
|
||||
else
|
||||
{
|
||||
string invalidResponse = this.FormatPrompt(this.Model.InvalidPrompt);
|
||||
await context.AddEventAsync(new MessageActivityEvent(invalidResponse.Trim())).ConfigureAwait(false);
|
||||
await context.AddEventAsync(new MessageActivityEvent(invalidResponse.Trim()), cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,13 +109,13 @@ internal sealed class QuestionExecutor(Question model, WorkflowFormulaState stat
|
||||
{
|
||||
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);
|
||||
await context.SendResultMessageAsync(this.Id, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask CompleteAsync(IWorkflowContext context, ActionExecutorResult message, CancellationToken cancellationToken)
|
||||
{
|
||||
await context.RaiseCompletionEventAsync(this.Model).ConfigureAwait(false);
|
||||
await context.RaiseCompletionEventAsync(this.Model, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async ValueTask PromptAsync(IWorkflowContext context, CancellationToken cancellationToken)
|
||||
@@ -128,8 +128,8 @@ internal sealed class QuestionExecutor(Question model, WorkflowFormulaState stat
|
||||
DataValue defaultValue = this.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);
|
||||
await context.AddEventAsync(new MessageActivityEvent(defaultValueResponse.Trim()), cancellationToken).ConfigureAwait(false);
|
||||
await context.SendResultMessageAsync(this.Id, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ internal sealed class ResetVariableExecutor(ResetVariable model, WorkflowFormula
|
||||
protected override async ValueTask<object?> ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(this.Model.Variable, $"{nameof(this.Model)}.{nameof(model.Variable)}");
|
||||
await context.QueueStateResetAsync(this.Model.Variable).ConfigureAwait(false);
|
||||
await context.QueueStateResetAsync(this.Model.Variable, cancellationToken).ConfigureAwait(false);
|
||||
Debug.WriteLine(
|
||||
$"""
|
||||
STATE: {this.GetType().Name} [{this.Id}]
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ internal sealed class SendActivityExecutor(SendActivity model, WorkflowFormulaSt
|
||||
{
|
||||
string activityText = this.Engine.Format(messageActivity.Text).Trim();
|
||||
|
||||
await context.AddEventAsync(new MessageActivityEvent(activityText.Trim())).ConfigureAwait(false);
|
||||
await context.AddEventAsync(new MessageActivityEvent(activityText.Trim()), cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return default;
|
||||
|
||||
+8
-1
@@ -274,6 +274,13 @@ internal sealed class WorkflowExpressionEngine
|
||||
expression.VariableReference?.ToString() :
|
||||
expression.ExpressionText;
|
||||
|
||||
return new(this._engine.Eval(expressionText), SensitivityLevel.None);
|
||||
FormulaValue result = this._engine.Eval(expressionText);
|
||||
|
||||
if (result is ErrorValue errorValue)
|
||||
{
|
||||
throw new DeclarativeActionException(errorValue.Format());
|
||||
}
|
||||
|
||||
return new(result, SensitivityLevel.None);
|
||||
}
|
||||
}
|
||||
|
||||
+12
-7
@@ -2,11 +2,11 @@
|
||||
|
||||
using System.Collections.Frozen;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.Extensions;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.Kit;
|
||||
using Microsoft.Bot.ObjectModel;
|
||||
using Microsoft.PowerFx;
|
||||
using Microsoft.PowerFx.Types;
|
||||
@@ -68,20 +68,25 @@ internal sealed class WorkflowFormulaState
|
||||
return;
|
||||
}
|
||||
|
||||
Stopwatch timer = Stopwatch.StartNew();
|
||||
Debug.WriteLine("RESTORE CHECKPOINT - BEGIN");
|
||||
await Task.WhenAll(RestorableScopes.Select(scopeName => ReadScopeAsync(scopeName))).ConfigureAwait(false);
|
||||
Debug.WriteLine($"RESTORE CHECKPOINT - COMPLETE [{timer.Elapsed}]");
|
||||
|
||||
async Task ReadScopeAsync(string scopeName)
|
||||
{
|
||||
HashSet<string> keys = await context.ReadStateKeysAsync(scopeName).ConfigureAwait(false);
|
||||
HashSet<string> keys = await context.ReadStateKeysAsync(scopeName, cancellationToken).ConfigureAwait(false);
|
||||
foreach (string key in keys)
|
||||
{
|
||||
object? value = await context.ReadStateAsync<object>(key, scopeName).ConfigureAwait(false);
|
||||
if (value is null or UnassignedValue)
|
||||
PortableValue? value = await context.ReadStateAsync<PortableValue>(key, scopeName, cancellationToken).ConfigureAwait(false);
|
||||
if (value is null)
|
||||
{
|
||||
value = FormulaValue.NewBlank();
|
||||
this.Set(key, FormulaValue.NewBlank(), scopeName);
|
||||
continue;
|
||||
}
|
||||
|
||||
this.Set(key, value.ToFormula(), scopeName);
|
||||
FormulaValue formulaValue = value.ToFormula();
|
||||
this.Set(key, formulaValue, scopeName);
|
||||
Debug.WriteLine($"RESTORED: {scopeName}.{key} => {formulaValue.Type}");
|
||||
}
|
||||
|
||||
this.Bind(scopeName);
|
||||
|
||||
@@ -150,12 +150,12 @@ public static partial class AgentWorkflowBuilder
|
||||
|
||||
protected override RouteBuilder ConfigureRoutes(RouteBuilder routeBuilder) =>
|
||||
routeBuilder
|
||||
.AddHandler<string>((message, context) => this._pendingMessages.Add(new(ChatRole.User, message)))
|
||||
.AddHandler<ChatMessage>((message, context) => this._pendingMessages.Add(message))
|
||||
.AddHandler<IEnumerable<ChatMessage>>((messages, _) => this._pendingMessages.AddRange(messages))
|
||||
.AddHandler<ChatMessage[]>((messages, _) => this._pendingMessages.AddRange(messages)) // TODO: Remove once https://github.com/microsoft/agent-framework/issues/782 is addressed
|
||||
.AddHandler<List<ChatMessage>>((messages, _) => this._pendingMessages.AddRange(messages)) // TODO: Remove once https://github.com/microsoft/agent-framework/issues/782 is addressed
|
||||
.AddHandler<TurnToken>(async (token, context) =>
|
||||
.AddHandler<string>((message, _, __) => this._pendingMessages.Add(new(ChatRole.User, message)))
|
||||
.AddHandler<ChatMessage>((message, _, __) => this._pendingMessages.Add(message))
|
||||
.AddHandler<IEnumerable<ChatMessage>>((messages, _, __) => this._pendingMessages.AddRange(messages))
|
||||
.AddHandler<ChatMessage[]>((messages, _, __) => this._pendingMessages.AddRange(messages)) // TODO: Remove once https://github.com/microsoft/agent-framework/issues/782 is addressed
|
||||
.AddHandler<List<ChatMessage>>((messages, _, __) => this._pendingMessages.AddRange(messages)) // TODO: Remove once https://github.com/microsoft/agent-framework/issues/782 is addressed
|
||||
.AddHandler<TurnToken>(async (token, context, cancellationToken) =>
|
||||
{
|
||||
List<ChatMessage> messages = [.. this._pendingMessages];
|
||||
this._pendingMessages.Clear();
|
||||
@@ -163,12 +163,12 @@ public static partial class AgentWorkflowBuilder
|
||||
List<ChatMessage>? roleChanged = ChangeAssistantToUserForOtherParticipants(agent.DisplayName, messages);
|
||||
|
||||
List<AgentRunResponseUpdate> updates = [];
|
||||
await foreach (var update in agent.RunStreamingAsync(messages).ConfigureAwait(false))
|
||||
await foreach (var update in agent.RunStreamingAsync(messages, cancellationToken: cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
updates.Add(update);
|
||||
if (token.EmitEvents is true)
|
||||
{
|
||||
await context.AddEventAsync(new AgentRunUpdateEvent(this.Id, update)).ConfigureAwait(false);
|
||||
await context.AddEventAsync(new AgentRunUpdateEvent(this.Id, update), cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -181,8 +181,8 @@ public static partial class AgentWorkflowBuilder
|
||||
|
||||
messages.AddRange(updates.ToAgentRunResponse().Messages);
|
||||
|
||||
await context.SendMessageAsync(messages).ConfigureAwait(false);
|
||||
await context.SendMessageAsync(token).ConfigureAwait(false);
|
||||
await context.SendMessageAsync(messages, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
await context.SendMessageAsync(token, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
});
|
||||
|
||||
public ValueTask ResetAsync()
|
||||
@@ -199,7 +199,7 @@ public static partial class AgentWorkflowBuilder
|
||||
private sealed class OutputMessagesExecutor() : ChatProtocolExecutor("OutputMessages"), IResettableExecutor
|
||||
{
|
||||
protected override ValueTask TakeTurnAsync(List<ChatMessage> messages, IWorkflowContext context, bool? emitEvents, CancellationToken cancellationToken = default)
|
||||
=> context.YieldOutputAsync(messages);
|
||||
=> context.YieldOutputAsync(messages, cancellationToken);
|
||||
|
||||
ValueTask IResettableExecutor.ResetAsync() => this.ResetAsync();
|
||||
}
|
||||
@@ -209,10 +209,10 @@ public static partial class AgentWorkflowBuilder
|
||||
{
|
||||
protected override RouteBuilder ConfigureRoutes(RouteBuilder routeBuilder) =>
|
||||
routeBuilder
|
||||
.AddHandler<string>((message, context) => context.SendMessageAsync(new ChatMessage(ChatRole.User, message)))
|
||||
.AddHandler<ChatMessage>((message, context) => context.SendMessageAsync(message))
|
||||
.AddHandler<List<ChatMessage>>((messages, context) => context.SendMessageAsync(messages))
|
||||
.AddHandler<TurnToken>((turnToken, context) => context.SendMessageAsync(turnToken));
|
||||
.AddHandler<string>((message, context, cancellationToken) => context.SendMessageAsync(new ChatMessage(ChatRole.User, message), cancellationToken: cancellationToken))
|
||||
.AddHandler<ChatMessage>((message, context, cancellationToken) => context.SendMessageAsync(message, cancellationToken: cancellationToken))
|
||||
.AddHandler<List<ChatMessage>>((messages, context, cancellationToken) => context.SendMessageAsync(messages, cancellationToken: cancellationToken))
|
||||
.AddHandler<TurnToken>((turnToken, context, cancellationToken) => context.SendMessageAsync(turnToken, cancellationToken: cancellationToken));
|
||||
|
||||
public ValueTask ResetAsync() => default;
|
||||
}
|
||||
@@ -224,7 +224,7 @@ public static partial class AgentWorkflowBuilder
|
||||
private sealed class BatchChatMessagesToListExecutor(string id) : ChatProtocolExecutor(id), IResettableExecutor
|
||||
{
|
||||
protected override ValueTask TakeTurnAsync(List<ChatMessage> messages, IWorkflowContext context, bool? emitEvents, CancellationToken cancellationToken = default)
|
||||
=> context.SendMessageAsync(messages);
|
||||
=> context.SendMessageAsync(messages, cancellationToken: cancellationToken);
|
||||
|
||||
ValueTask IResettableExecutor.ResetAsync() => this.ResetAsync();
|
||||
}
|
||||
@@ -256,7 +256,7 @@ public static partial class AgentWorkflowBuilder
|
||||
}
|
||||
|
||||
protected override RouteBuilder ConfigureRoutes(RouteBuilder routeBuilder) =>
|
||||
routeBuilder.AddHandler<List<ChatMessage>>(async (messages, context) =>
|
||||
routeBuilder.AddHandler<List<ChatMessage>>(async (messages, context, cancellationToken) =>
|
||||
{
|
||||
// TODO: https://github.com/microsoft/agent-framework/issues/784
|
||||
// This locking should not be necessary.
|
||||
@@ -273,7 +273,7 @@ public static partial class AgentWorkflowBuilder
|
||||
|
||||
var results = this._allResults;
|
||||
this._allResults = new List<List<ChatMessage>>(this._expectedInputs);
|
||||
await context.YieldOutputAsync(this._aggregator(results)).ConfigureAwait(false);
|
||||
await context.YieldOutputAsync(this._aggregator(results), cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -457,16 +457,17 @@ public static partial class AgentWorkflowBuilder
|
||||
|
||||
protected override RouteBuilder ConfigureRoutes(RouteBuilder routeBuilder) =>
|
||||
routeBuilder
|
||||
.AddHandler<string>((message, context) => this._pendingMessages.Add(new(ChatRole.User, message)))
|
||||
.AddHandler<ChatMessage>((message, context) => this._pendingMessages.Add(message))
|
||||
.AddHandler<IEnumerable<ChatMessage>>((messages, _) => this._pendingMessages.AddRange(messages))
|
||||
.AddHandler<ChatMessage[]>((messages, _) => this._pendingMessages.AddRange(messages)) // TODO: Remove once https://github.com/microsoft/agent-framework/issues/782 is addressed
|
||||
.AddHandler<List<ChatMessage>>((messages, _) => this._pendingMessages.AddRange(messages)) // TODO: Remove once https://github.com/microsoft/agent-framework/issues/782 is addressed
|
||||
.AddHandler<TurnToken>(async (token, context) =>
|
||||
.AddHandler<string>((message, context, _) => this._pendingMessages.Add(new(ChatRole.User, message)))
|
||||
.AddHandler<ChatMessage>((message, context, _) => this._pendingMessages.Add(message))
|
||||
.AddHandler<IEnumerable<ChatMessage>>((messages, _, __) => this._pendingMessages.AddRange(messages))
|
||||
.AddHandler<ChatMessage[]>((messages, _, __) => this._pendingMessages.AddRange(messages)) // TODO: Remove once https://github.com/microsoft/agent-framework/issues/782 is addressed
|
||||
.AddHandler<List<ChatMessage>>((messages, _, __) => this._pendingMessages.AddRange(messages)) // TODO: Remove once https://github.com/microsoft/agent-framework/issues/782 is addressed
|
||||
.AddHandler<TurnToken>(async (token, context, cancellationToken) =>
|
||||
{
|
||||
var messages = new List<ChatMessage>(this._pendingMessages);
|
||||
this._pendingMessages.Clear();
|
||||
await context.SendMessageAsync(new HandoffState(token, null, messages)).ConfigureAwait(false);
|
||||
await context.SendMessageAsync(new HandoffState(token, null, messages), cancellationToken: cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
});
|
||||
|
||||
public ValueTask ResetAsync()
|
||||
@@ -480,8 +481,8 @@ public static partial class AgentWorkflowBuilder
|
||||
private sealed class EndHandoffsExecutor() : Executor("HandoffEnd"), IResettableExecutor
|
||||
{
|
||||
protected override RouteBuilder ConfigureRoutes(RouteBuilder routeBuilder) =>
|
||||
routeBuilder.AddHandler<HandoffState>((handoff, context) =>
|
||||
context.YieldOutputAsync(handoff.Messages));
|
||||
routeBuilder.AddHandler<HandoffState>((handoff, context, cancellationToken) =>
|
||||
context.YieldOutputAsync(handoff.Messages, cancellationToken));
|
||||
|
||||
public ValueTask ResetAsync() => default;
|
||||
}
|
||||
@@ -534,7 +535,7 @@ public static partial class AgentWorkflowBuilder
|
||||
});
|
||||
|
||||
protected override RouteBuilder ConfigureRoutes(RouteBuilder routeBuilder) =>
|
||||
routeBuilder.AddHandler<HandoffState>(async (handoffState, context) =>
|
||||
routeBuilder.AddHandler<HandoffState>(async (handoffState, context, cancellationToken) =>
|
||||
{
|
||||
string? requestedHandoff = null;
|
||||
List<AgentRunResponseUpdate> updates = [];
|
||||
@@ -542,24 +543,31 @@ public static partial class AgentWorkflowBuilder
|
||||
|
||||
List<ChatMessage>? roleChanges = ChangeAssistantToUserForOtherParticipants(this._agent.DisplayName, allMessages);
|
||||
|
||||
await foreach (var update in this._agent.RunStreamingAsync(allMessages, options: this._agentOptions).ConfigureAwait(false))
|
||||
await foreach (var update in this._agent.RunStreamingAsync(allMessages,
|
||||
options: this._agentOptions,
|
||||
cancellationToken: cancellationToken)
|
||||
.ConfigureAwait(false))
|
||||
{
|
||||
await AddUpdateAsync(update).ConfigureAwait(false);
|
||||
await AddUpdateAsync(update, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
foreach (var c in update.Contents)
|
||||
{
|
||||
if (c is FunctionCallContent fcc && this._handoffFunctionNames.Contains(fcc.Name))
|
||||
{
|
||||
requestedHandoff = fcc.Name;
|
||||
await AddUpdateAsync(new AgentRunResponseUpdate
|
||||
{
|
||||
AgentId = this._agent.Id,
|
||||
AuthorName = this._agent.DisplayName,
|
||||
Contents = [new FunctionResultContent(fcc.CallId, "Transferred.")],
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
MessageId = Guid.NewGuid().ToString("N"),
|
||||
Role = ChatRole.Tool,
|
||||
}).ConfigureAwait(false);
|
||||
await AddUpdateAsync(
|
||||
new AgentRunResponseUpdate
|
||||
{
|
||||
AgentId = this._agent.Id,
|
||||
AuthorName = this._agent.DisplayName,
|
||||
Contents = [new FunctionResultContent(fcc.CallId, "Transferred.")],
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
MessageId = Guid.NewGuid().ToString("N"),
|
||||
Role = ChatRole.Tool,
|
||||
},
|
||||
cancellationToken
|
||||
)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -568,14 +576,14 @@ public static partial class AgentWorkflowBuilder
|
||||
|
||||
ResetUserToAssistantForChangedRoles(roleChanges);
|
||||
|
||||
await context.SendMessageAsync(new HandoffState(handoffState.TurnToken, requestedHandoff, allMessages)).ConfigureAwait(false);
|
||||
await context.SendMessageAsync(new HandoffState(handoffState.TurnToken, requestedHandoff, allMessages), cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
|
||||
async Task AddUpdateAsync(AgentRunResponseUpdate update)
|
||||
async Task AddUpdateAsync(AgentRunResponseUpdate update, CancellationToken cancellationToken)
|
||||
{
|
||||
updates.Add(update);
|
||||
if (handoffState.TurnToken.EmitEvents is true)
|
||||
{
|
||||
await context.AddEventAsync(new AgentRunUpdateEvent(this.Id, update)).ConfigureAwait(false);
|
||||
await context.AddEventAsync(new AgentRunUpdateEvent(this.Id, update), cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -623,7 +631,8 @@ public static partial class AgentWorkflowBuilder
|
||||
/// Selects the next agent to participate in the group chat based on the provided chat history and team.
|
||||
/// </summary>
|
||||
/// <param name="history">The chat history to consider.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.
|
||||
/// The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>The next <see cref="AIAgent"/> to speak. This agent must be part of the chat.</returns>
|
||||
protected internal abstract ValueTask<AIAgent> SelectNextAgentAsync(
|
||||
IReadOnlyList<ChatMessage> history,
|
||||
@@ -633,7 +642,8 @@ public static partial class AgentWorkflowBuilder
|
||||
/// Filters the chat history before it's passed to the next agent.
|
||||
/// </summary>
|
||||
/// <param name="history">The chat history to filter.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.
|
||||
/// The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>The filtered chat history.</returns>
|
||||
protected internal virtual ValueTask<IEnumerable<ChatMessage>> UpdateHistoryAsync(
|
||||
IReadOnlyList<ChatMessage> history,
|
||||
@@ -644,7 +654,8 @@ public static partial class AgentWorkflowBuilder
|
||||
/// Determines whether the group chat should be terminated based on the provided chat history and iteration count.
|
||||
/// </summary>
|
||||
/// <param name="history">The chat history to consider.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.
|
||||
/// The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A <see cref="bool"/> indicating whether the chat should be terminated.</returns>
|
||||
protected internal virtual ValueTask<bool> ShouldTerminateAsync(
|
||||
IReadOnlyList<ChatMessage> history,
|
||||
@@ -789,35 +800,35 @@ public static partial class AgentWorkflowBuilder
|
||||
private GroupChatManager? _manager;
|
||||
|
||||
protected override RouteBuilder ConfigureRoutes(RouteBuilder routeBuilder) => routeBuilder
|
||||
.AddHandler<string>((message, context) => this._pendingMessages.Add(new(ChatRole.User, message)))
|
||||
.AddHandler<ChatMessage>((message, context) => this._pendingMessages.Add(message))
|
||||
.AddHandler<IEnumerable<ChatMessage>>((messages, _) => this._pendingMessages.AddRange(messages))
|
||||
.AddHandler<ChatMessage[]>((messages, _) => this._pendingMessages.AddRange(messages)) // TODO: Remove once https://github.com/microsoft/agent-framework/issues/782 is addressed
|
||||
.AddHandler<List<ChatMessage>>((messages, _) => this._pendingMessages.AddRange(messages)) // TODO: Remove once https://github.com/microsoft/agent-framework/issues/782 is addressed
|
||||
.AddHandler<TurnToken>(async (token, context) =>
|
||||
.AddHandler<string>((message, context, _) => this._pendingMessages.Add(new(ChatRole.User, message)))
|
||||
.AddHandler<ChatMessage>((message, context, _) => this._pendingMessages.Add(message))
|
||||
.AddHandler<IEnumerable<ChatMessage>>((messages, _, __) => this._pendingMessages.AddRange(messages))
|
||||
.AddHandler<ChatMessage[]>((messages, _, __) => this._pendingMessages.AddRange(messages)) // TODO: Remove once https://github.com/microsoft/agent-framework/issues/782 is addressed
|
||||
.AddHandler<List<ChatMessage>>((messages, _, __) => this._pendingMessages.AddRange(messages)) // TODO: Remove once https://github.com/microsoft/agent-framework/issues/782 is addressed
|
||||
.AddHandler<TurnToken>(async (token, context, cancellationToken) =>
|
||||
{
|
||||
List<ChatMessage> messages = [.. this._pendingMessages];
|
||||
this._pendingMessages.Clear();
|
||||
|
||||
this._manager ??= this._managerFactory(this._agents);
|
||||
|
||||
if (!await this._manager.ShouldTerminateAsync(messages).ConfigureAwait(false))
|
||||
if (!await this._manager.ShouldTerminateAsync(messages, cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
var filtered = await this._manager.UpdateHistoryAsync(messages).ConfigureAwait(false);
|
||||
var filtered = await this._manager.UpdateHistoryAsync(messages, cancellationToken).ConfigureAwait(false);
|
||||
messages = filtered is null || ReferenceEquals(filtered, messages) ? messages : [.. filtered];
|
||||
|
||||
if (await this._manager.SelectNextAgentAsync(messages).ConfigureAwait(false) is AIAgent nextAgent &&
|
||||
if (await this._manager.SelectNextAgentAsync(messages, cancellationToken).ConfigureAwait(false) is AIAgent nextAgent &&
|
||||
this._agentMap.TryGetValue(nextAgent, out var executor))
|
||||
{
|
||||
this._manager.IterationCount++;
|
||||
await context.SendMessageAsync(messages, executor.Id).ConfigureAwait(false);
|
||||
await context.SendMessageAsync(token, executor.Id).ConfigureAwait(false);
|
||||
await context.SendMessageAsync(messages, executor.Id, cancellationToken).ConfigureAwait(false);
|
||||
await context.SendMessageAsync(token, executor.Id, cancellationToken).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this._manager = null;
|
||||
await context.YieldOutputAsync(messages).ConfigureAwait(false);
|
||||
await context.YieldOutputAsync(messages, cancellationToken).ConfigureAwait(false);
|
||||
});
|
||||
|
||||
public ValueTask ResetAsync()
|
||||
|
||||
@@ -28,7 +28,7 @@ public class AggregatingExecutor<TInput, TAggregate>(string id,
|
||||
private TAggregate? _runningAggregate;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override ValueTask<TAggregate?> HandleAsync(TInput message, IWorkflowContext context)
|
||||
public override ValueTask<TAggregate?> HandleAsync(TInput message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
this._runningAggregate = aggregator(this._runningAggregate, message);
|
||||
return new(this._runningAggregate);
|
||||
@@ -37,7 +37,7 @@ public class AggregatingExecutor<TInput, TAggregate>(string id,
|
||||
/// <inheritdoc/>
|
||||
protected internal override async ValueTask OnCheckpointingAsync(IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await context.QueueStateUpdateAsync(AggregateStateKey, this._runningAggregate).ConfigureAwait(false);
|
||||
await context.QueueStateUpdateAsync(AggregateStateKey, this._runningAggregate, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
|
||||
await base.OnCheckpointingAsync(context, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
@@ -47,6 +47,6 @@ public class AggregatingExecutor<TInput, TAggregate>(string id,
|
||||
{
|
||||
await base.OnCheckpointRestoredAsync(context, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
this._runningAggregate = await context.ReadStateAsync<TAggregate>(AggregateStateKey).ConfigureAwait(false);
|
||||
this._runningAggregate = await context.ReadStateAsync<TAggregate>(AggregateStateKey, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ internal sealed class AsyncBarrier()
|
||||
{
|
||||
private readonly InitLocked<TaskCompletionSource<object>> _completionSource = new();
|
||||
|
||||
public async ValueTask<bool> JoinAsync(CancellationToken cancellation = default)
|
||||
public async ValueTask<bool> JoinAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
this._completionSource.Init(() => new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously));
|
||||
TaskCompletionSource<object> completionSource = this._completionSource.Get()!;
|
||||
@@ -19,10 +19,10 @@ internal sealed class AsyncBarrier()
|
||||
// should not cancel the entire barrier.
|
||||
TaskCompletionSource<object> cancellationSource = new();
|
||||
|
||||
using CancellationTokenRegistration registration = cancellation.Register(() => cancellationSource.SetResult(new()));
|
||||
using CancellationTokenRegistration registration = cancellationToken.Register(() => cancellationSource.SetResult(new()));
|
||||
|
||||
await Task.WhenAny(completionSource.Task, cancellationSource.Task).ConfigureAwait(false);
|
||||
return !cancellation.IsCancellationRequested;
|
||||
return !cancellationToken.IsCancellationRequested;
|
||||
}
|
||||
|
||||
public bool ReleaseBarrier()
|
||||
|
||||
@@ -12,12 +12,13 @@ internal sealed class AsyncCoordinator
|
||||
/// <summary>
|
||||
/// Wait for the Coordination owner to mark the next coordination point, then continue execution.
|
||||
/// </summary>
|
||||
/// <param name="cancellation">A cancellation token that can be used to cancel the wait.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.
|
||||
/// The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>
|
||||
/// A task that represents the asynchronous operation. The task result is <see langword="true"/>
|
||||
/// if the wait was completed; otherwise, for example, if the wait was cancelled, <see langword="false"/>.
|
||||
/// </returns>
|
||||
public async ValueTask<bool> WaitForCoordinationAsync(CancellationToken cancellation = default)
|
||||
public async ValueTask<bool> WaitForCoordinationAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
// There is a chance that we might get a stale barrier that is getting released if there is a
|
||||
// release happening concurrently with this call. This is by design, and should be considered
|
||||
@@ -26,7 +27,7 @@ internal sealed class AsyncCoordinator
|
||||
?? Interlocked.CompareExchange(ref this._coordinationBarrier, new(), null)
|
||||
?? this._coordinationBarrier!; // Re-read after setting
|
||||
|
||||
return await actualBarrier.JoinAsync(cancellation).ConfigureAwait(false);
|
||||
return await actualBarrier.JoinAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -30,19 +30,21 @@ internal abstract class ChatProtocolExecutor(string id, ChatProtocolExecutorOpti
|
||||
{
|
||||
if (this._stringMessageChatRole.HasValue)
|
||||
{
|
||||
routeBuilder = routeBuilder.AddHandler<string>((message, _) => this._pendingMessages.Add(new(this._stringMessageChatRole.Value, message)));
|
||||
routeBuilder = routeBuilder.AddHandler<string>((message, _, __) => this._pendingMessages.Add(new(this._stringMessageChatRole.Value, message)));
|
||||
}
|
||||
|
||||
return routeBuilder.AddHandler<ChatMessage>((message, _) => this._pendingMessages.Add(message))
|
||||
.AddHandler<List<ChatMessage>>((messages, _) => this._pendingMessages.AddRange(messages))
|
||||
// Routing requires exact type matches. The runtime may dispatch either List<ChatMessage> or ChatMessage[].
|
||||
return routeBuilder.AddHandler<ChatMessage>((message, _, __) => this._pendingMessages.Add(message))
|
||||
.AddHandler<List<ChatMessage>>((messages, _, __) => this._pendingMessages.AddRange(messages))
|
||||
.AddHandler<ChatMessage[]>((messages, _, __) => this._pendingMessages.AddRange(messages))
|
||||
.AddHandler<TurnToken>(this.TakeTurnAsync);
|
||||
}
|
||||
|
||||
public async ValueTask TakeTurnAsync(TurnToken token, IWorkflowContext context)
|
||||
public async ValueTask TakeTurnAsync(TurnToken token, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await this.TakeTurnAsync(this._pendingMessages, context, token.EmitEvents).ConfigureAwait(false);
|
||||
await this.TakeTurnAsync(this._pendingMessages, context, token.EmitEvents, cancellationToken).ConfigureAwait(false);
|
||||
this._pendingMessages = [];
|
||||
await context.SendMessageAsync(token).ConfigureAwait(false);
|
||||
await context.SendMessageAsync(token, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
protected abstract ValueTask TakeTurnAsync(List<ChatMessage> messages, IWorkflowContext context, bool? emitEvents, CancellationToken cancellationToken = default);
|
||||
@@ -54,7 +56,7 @@ internal abstract class ChatProtocolExecutor(string id, ChatProtocolExecutorOpti
|
||||
if (this._pendingMessages.Count > 0)
|
||||
{
|
||||
JsonElement messagesValue = this._pendingMessages.Serialize();
|
||||
messagesTask = context.QueueStateUpdateAsync(PendingMessagesStateKey, messagesValue).AsTask();
|
||||
messagesTask = context.QueueStateUpdateAsync(PendingMessagesStateKey, messagesValue, cancellationToken: cancellationToken).AsTask();
|
||||
}
|
||||
|
||||
await messagesTask.ConfigureAwait(false);
|
||||
@@ -62,7 +64,7 @@ internal abstract class ChatProtocolExecutor(string id, ChatProtocolExecutorOpti
|
||||
|
||||
protected internal override async ValueTask OnCheckpointRestoredAsync(IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
JsonElement? messagesValue = await context.ReadStateAsync<JsonElement?>(PendingMessagesStateKey).ConfigureAwait(false);
|
||||
JsonElement? messagesValue = await context.ReadStateAsync<JsonElement?>(PendingMessagesStateKey, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
if (messagesValue.HasValue)
|
||||
{
|
||||
List<ChatMessage> messages = messagesValue.Value.DeserializeMessages();
|
||||
|
||||
+8
-3
@@ -56,7 +56,12 @@ public sealed class FileSystemJsonCheckpointStore : JsonCheckpointStore, IDispos
|
||||
{
|
||||
// read the lines of indexfile and parse them as CheckpointInfos
|
||||
this.CheckpointIndex = [];
|
||||
using StreamReader reader = new(this._indexFile, encoding: Encoding.UTF8, detectEncodingFromByteOrderMarks: false, bufferSize: -1, leaveOpen: true);
|
||||
#if NET
|
||||
const int BufferSize = -1;
|
||||
#else
|
||||
const int BufferSize = 1024;
|
||||
#endif
|
||||
using StreamReader reader = new(this._indexFile, encoding: Encoding.UTF8, detectEncodingFromByteOrderMarks: false, BufferSize, leaveOpen: true);
|
||||
while (reader.ReadLine() is string line)
|
||||
{
|
||||
if (JsonSerializer.Deserialize(line, KeyTypeInfo) is { } info)
|
||||
@@ -65,9 +70,9 @@ public sealed class FileSystemJsonCheckpointStore : JsonCheckpointStore, IDispos
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
catch (Exception exception)
|
||||
{
|
||||
throw new InvalidOperationException($"Could not load store at '{directory.FullName}'. Index corrupted.");
|
||||
throw new InvalidOperationException($"Could not load store at '{directory.FullName}'. Index corrupted.", exception);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -44,19 +44,14 @@ internal sealed class AsyncRunHandle : ICheckpointingHandle, IAsyncDisposable
|
||||
}
|
||||
}
|
||||
|
||||
//private readonly AsyncCoordinator _waitForResponseCoordinator = new();
|
||||
|
||||
//public ValueTask<bool> WaitForNextInputAsync(CancellationToken cancellation = default)
|
||||
// => this._waitForResponseCoordinator.WaitForCoordinationAsync(cancellation);
|
||||
|
||||
public string RunId => this._stepRunner.RunId;
|
||||
|
||||
public IReadOnlyList<CheckpointInfo> Checkpoints => this._checkpointingHandle.Checkpoints;
|
||||
|
||||
public ValueTask<RunStatus> GetStatusAsync(CancellationToken cancellation = default)
|
||||
=> this._eventStream.GetStatusAsync(cancellation);
|
||||
public ValueTask<RunStatus> GetStatusAsync(CancellationToken cancellationToken = default)
|
||||
=> this._eventStream.GetStatusAsync(cancellationToken);
|
||||
|
||||
public async IAsyncEnumerable<WorkflowEvent> TakeEventStreamAsync(bool blockOnPendingRequest, [EnumeratorCancellation] CancellationToken cancellation = default)
|
||||
public async IAsyncEnumerable<WorkflowEvent> TakeEventStreamAsync(bool blockOnPendingRequest, [EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
//Debug.Assert(breakOnHalt);
|
||||
// Enforce single active enumerator (this runs when enumeration begins)
|
||||
@@ -68,7 +63,7 @@ internal sealed class AsyncRunHandle : ICheckpointingHandle, IAsyncDisposable
|
||||
CancellationTokenSource? linked = null;
|
||||
try
|
||||
{
|
||||
linked = CancellationTokenSource.CreateLinkedTokenSource(cancellation, this._endRunSource.Token);
|
||||
linked = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, this._endRunSource.Token);
|
||||
var token = linked.Token;
|
||||
|
||||
// Build the inner stream before the loop so synchronous exceptions still release the gate
|
||||
@@ -92,21 +87,21 @@ internal sealed class AsyncRunHandle : ICheckpointingHandle, IAsyncDisposable
|
||||
}
|
||||
}
|
||||
|
||||
public ValueTask<bool> IsValidInputTypeAsync<T>(CancellationToken cancellation = default)
|
||||
=> this._stepRunner.IsValidInputTypeAsync<T>(cancellation);
|
||||
public ValueTask<bool> IsValidInputTypeAsync<T>(CancellationToken cancellationToken = default)
|
||||
=> this._stepRunner.IsValidInputTypeAsync<T>(cancellationToken);
|
||||
|
||||
public async ValueTask<bool> EnqueueMessageAsync<T>(T message, CancellationToken cancellation = default)
|
||||
public async ValueTask<bool> EnqueueMessageAsync<T>(T message, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (message is ExternalResponse response)
|
||||
{
|
||||
// EnqueueResponseAsync handles signaling
|
||||
await this.EnqueueResponseAsync(response, cancellation)
|
||||
await this.EnqueueResponseAsync(response, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool result = await this._stepRunner.EnqueueMessageAsync(message, cancellation)
|
||||
bool result = await this._stepRunner.EnqueueMessageAsync(message, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
// Signal the run loop that new input is available
|
||||
@@ -115,7 +110,7 @@ internal sealed class AsyncRunHandle : ICheckpointingHandle, IAsyncDisposable
|
||||
return result;
|
||||
}
|
||||
|
||||
public async ValueTask<bool> EnqueueMessageUntypedAsync([NotNull] object message, Type? declaredType = null, CancellationToken cancellation = default)
|
||||
public async ValueTask<bool> EnqueueMessageUntypedAsync([NotNull] object message, Type? declaredType = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (declaredType?.IsInstanceOfType(message) == false)
|
||||
{
|
||||
@@ -125,7 +120,7 @@ internal sealed class AsyncRunHandle : ICheckpointingHandle, IAsyncDisposable
|
||||
if (declaredType != null && typeof(ExternalResponse).IsAssignableFrom(declaredType))
|
||||
{
|
||||
// EnqueueResponseAsync handles signaling
|
||||
await this.EnqueueResponseAsync((ExternalResponse)message, cancellation)
|
||||
await this.EnqueueResponseAsync((ExternalResponse)message, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return true;
|
||||
@@ -133,13 +128,13 @@ internal sealed class AsyncRunHandle : ICheckpointingHandle, IAsyncDisposable
|
||||
else if (declaredType == null && message is ExternalResponse response)
|
||||
{
|
||||
// EnqueueResponseAsync handles signaling
|
||||
await this.EnqueueResponseAsync(response, cancellation)
|
||||
await this.EnqueueResponseAsync(response, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool result = await this._stepRunner.EnqueueMessageUntypedAsync(message, declaredType ?? message.GetType(), cancellation)
|
||||
bool result = await this._stepRunner.EnqueueMessageUntypedAsync(message, declaredType ?? message.GetType(), cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
// Signal the run loop that new input is available
|
||||
@@ -148,9 +143,9 @@ internal sealed class AsyncRunHandle : ICheckpointingHandle, IAsyncDisposable
|
||||
return result;
|
||||
}
|
||||
|
||||
public async ValueTask EnqueueResponseAsync(ExternalResponse response, CancellationToken cancellation = default)
|
||||
public async ValueTask EnqueueResponseAsync(ExternalResponse response, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await this._stepRunner.EnqueueResponseAsync(response, cancellation).ConfigureAwait(false);
|
||||
await this._stepRunner.EnqueueResponseAsync(response, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Signal the run loop that new input is available
|
||||
this.SignalInputToRunLoop();
|
||||
|
||||
@@ -14,33 +14,33 @@ internal static class AsyncRunHandleExtensions
|
||||
return new Checkpointed<TRunType>(run, runHandle);
|
||||
}
|
||||
|
||||
public static async ValueTask<StreamingRun> EnqueueAndStreamAsync<TInput>(this AsyncRunHandle runHandle, TInput input, CancellationToken cancellation = default)
|
||||
public static async ValueTask<StreamingRun> EnqueueAndStreamAsync<TInput>(this AsyncRunHandle runHandle, TInput input, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await runHandle.EnqueueMessageAsync(input, cancellation).ConfigureAwait(false);
|
||||
await runHandle.EnqueueMessageAsync(input, cancellationToken).ConfigureAwait(false);
|
||||
return new(runHandle);
|
||||
}
|
||||
|
||||
public static async ValueTask<StreamingRun> EnqueueUntypedAndStreamAsync(this AsyncRunHandle runHandle, object input, CancellationToken cancellation = default)
|
||||
public static async ValueTask<StreamingRun> EnqueueUntypedAndStreamAsync(this AsyncRunHandle runHandle, object input, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await runHandle.EnqueueMessageUntypedAsync(input, cancellation: cancellation).ConfigureAwait(false);
|
||||
await runHandle.EnqueueMessageUntypedAsync(input, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
return new(runHandle);
|
||||
}
|
||||
|
||||
public static async ValueTask<Run> EnqueueAndRunAsync<TInput>(this AsyncRunHandle runHandle, TInput input, CancellationToken cancellation = default)
|
||||
public static async ValueTask<Run> EnqueueAndRunAsync<TInput>(this AsyncRunHandle runHandle, TInput input, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await runHandle.EnqueueMessageAsync(input, cancellation).ConfigureAwait(false);
|
||||
await runHandle.EnqueueMessageAsync(input, cancellationToken).ConfigureAwait(false);
|
||||
Run run = new(runHandle);
|
||||
|
||||
await run.RunToNextHaltAsync(cancellation).ConfigureAwait(false);
|
||||
await run.RunToNextHaltAsync(cancellationToken).ConfigureAwait(false);
|
||||
return run;
|
||||
}
|
||||
|
||||
public static async ValueTask<Run> EnqueueUntypedAndRunAsync(this AsyncRunHandle runHandle, object input, CancellationToken cancellation = default)
|
||||
public static async ValueTask<Run> EnqueueUntypedAndRunAsync(this AsyncRunHandle runHandle, object input, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await runHandle.EnqueueMessageUntypedAsync(input, cancellation: cancellation).ConfigureAwait(false);
|
||||
await runHandle.EnqueueMessageUntypedAsync(input, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
Run run = new(runHandle);
|
||||
|
||||
await run.RunToNextHaltAsync(cancellation).ConfigureAwait(false);
|
||||
await run.RunToNextHaltAsync(cancellationToken).ConfigureAwait(false);
|
||||
return run;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Threading;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Execution;
|
||||
@@ -26,16 +27,22 @@ internal sealed class CallResult
|
||||
/// </summary>
|
||||
public Exception? Exception { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Indicated whether the call was cancelled (e.g., via a <see cref="CancellationToken"/>).
|
||||
/// </summary>
|
||||
public bool IsCancelled { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether the call was successful. A call is considered successful if it returned
|
||||
/// without throwing an exception.
|
||||
/// </summary>
|
||||
public bool IsSuccess => this.Exception is null;
|
||||
public bool IsSuccess => this.Exception is null && !this.IsCancelled;
|
||||
|
||||
private CallResult(bool isVoid = false)
|
||||
private CallResult(bool isVoid = false, bool isCancelled = false)
|
||||
{
|
||||
// Private constructor to enforce use of static methods.
|
||||
this.IsVoid = isVoid;
|
||||
this.IsCancelled = isCancelled;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -51,6 +58,14 @@ internal sealed class CallResult
|
||||
/// <returns>A <see cref="CallResult"/> indicating the result of the call.</returns>
|
||||
public static CallResult ReturnVoid() => new(isVoid: true);
|
||||
|
||||
/// <summary>
|
||||
/// Create a <see cref="CallResult"/> indicating that the call was cancelled.
|
||||
/// </summary>
|
||||
/// <param name="wasVoid">A boolean specifying whether the call was void (was not expected to return
|
||||
/// a value).</param>
|
||||
/// <returns>A <see cref="CallResult"/> indicating the result of the call.</returns>
|
||||
public static CallResult Cancelled(bool wasVoid) => new(wasVoid, isCancelled: true);
|
||||
|
||||
/// <summary>
|
||||
/// Create a <see cref="CallResult"/> indicating that an exception was raised during the call.
|
||||
/// </summary>
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Execution;
|
||||
|
||||
internal interface IInputCoordinator
|
||||
{
|
||||
ValueTask<bool> WaitForNextInputAsync(CancellationToken cancellation = default);
|
||||
}
|
||||
@@ -15,7 +15,7 @@ internal interface IRunEventStream : IAsyncDisposable
|
||||
// this cannot be cancelled
|
||||
ValueTask StopAsync();
|
||||
|
||||
ValueTask<RunStatus> GetStatusAsync(CancellationToken cancellation = default);
|
||||
ValueTask<RunStatus> GetStatusAsync(CancellationToken cancellationToken = default);
|
||||
|
||||
IAsyncEnumerable<WorkflowEvent> TakeEventStreamAsync(bool blockOnPendingRequest, CancellationToken cancellation = default);
|
||||
IAsyncEnumerable<WorkflowEvent> TakeEventStreamAsync(bool blockOnPendingRequest, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Execution;
|
||||
|
||||
internal interface IRunnerContext : IExternalRequestSink, ISuperStepJoinContext
|
||||
{
|
||||
ValueTask AddEventAsync(WorkflowEvent workflowEvent);
|
||||
ValueTask SendMessageAsync(string sourceId, object message, string? targetId = null);
|
||||
ValueTask AddEventAsync(WorkflowEvent workflowEvent, CancellationToken cancellationToken = default);
|
||||
ValueTask SendMessageAsync(string sourceId, object message, string? targetId = null, CancellationToken cancellationToken = default);
|
||||
|
||||
ValueTask<StepContext> AdvanceAsync();
|
||||
ValueTask<StepContext> AdvanceAsync(CancellationToken cancellationToken = default);
|
||||
IWorkflowContext Bind(string executorId, Dictionary<string, string>? traceContext = null);
|
||||
ValueTask<Executor> EnsureExecutorAsync(string executorId, IStepTracer? tracer);
|
||||
ValueTask<Executor> EnsureExecutorAsync(string executorId, IStepTracer? tracer, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
@@ -10,8 +10,8 @@ internal interface ISuperStepJoinContext
|
||||
{
|
||||
bool WithCheckpointing { get; }
|
||||
|
||||
ValueTask ForwardWorkflowEventAsync(WorkflowEvent workflowEvent, CancellationToken cancellation = default);
|
||||
ValueTask SendMessageAsync<TMessage>(string senderId, [DisallowNull] TMessage message, CancellationToken cancellation = default);
|
||||
ValueTask ForwardWorkflowEventAsync(WorkflowEvent workflowEvent, CancellationToken cancellationToken = default);
|
||||
ValueTask SendMessageAsync<TMessage>(string senderId, [DisallowNull] TMessage message, CancellationToken cancellationToken = default);
|
||||
|
||||
ValueTask AttachSuperstepAsync(ISuperStepRunner superStepRunner, CancellationToken cancellation = default);
|
||||
ValueTask AttachSuperstepAsync(ISuperStepRunner superStepRunner, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
@@ -15,11 +15,11 @@ internal interface ISuperStepRunner
|
||||
bool HasUnservicedRequests { get; }
|
||||
bool HasUnprocessedMessages { get; }
|
||||
|
||||
ValueTask EnqueueResponseAsync(ExternalResponse response, CancellationToken cancellation = default);
|
||||
ValueTask EnqueueResponseAsync(ExternalResponse response, CancellationToken cancellationToken = default);
|
||||
|
||||
ValueTask<bool> IsValidInputTypeAsync<T>(CancellationToken cancellation = default);
|
||||
ValueTask<bool> EnqueueMessageAsync<T>(T message, CancellationToken cancellation = default);
|
||||
ValueTask<bool> EnqueueMessageUntypedAsync(object message, Type declaredType, CancellationToken cancellation = default);
|
||||
ValueTask<bool> IsValidInputTypeAsync<T>(CancellationToken cancellationToken = default);
|
||||
ValueTask<bool> EnqueueMessageAsync<T>(T message, CancellationToken cancellationToken = default);
|
||||
ValueTask<bool> EnqueueMessageUntypedAsync(object message, Type declaredType, CancellationToken cancellationToken = default);
|
||||
|
||||
ConcurrentEventSink OutgoingEvents { get; }
|
||||
|
||||
|
||||
@@ -33,10 +33,10 @@ internal sealed class InputWaiter : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
public Task WaitForInputAsync(CancellationToken cancellation = default) => this.WaitForInputAsync(null, cancellation);
|
||||
public Task WaitForInputAsync(CancellationToken cancellationToken = default) => this.WaitForInputAsync(null, cancellationToken);
|
||||
|
||||
public async Task WaitForInputAsync(TimeSpan? timeout = null, CancellationToken cancellation = default)
|
||||
public async Task WaitForInputAsync(TimeSpan? timeout = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await this._inputSignal.WaitAsync(timeout ?? TimeSpan.FromMilliseconds(-1), cancellation).ConfigureAwait(false);
|
||||
await this._inputSignal.WaitAsync(timeout ?? TimeSpan.FromMilliseconds(-1), cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ internal sealed class LockstepRunEventStream : IRunEventStream
|
||||
|
||||
private readonly ISuperStepRunner _stepRunner;
|
||||
|
||||
public ValueTask<RunStatus> GetStatusAsync(CancellationToken cancellation = default) => new(this.RunStatus);
|
||||
public ValueTask<RunStatus> GetStatusAsync(CancellationToken cancellationToken = default) => new(this.RunStatus);
|
||||
|
||||
public LockstepRunEventStream(ISuperStepRunner stepRunner)
|
||||
{
|
||||
@@ -36,7 +36,7 @@ internal sealed class LockstepRunEventStream : IRunEventStream
|
||||
// No-op for lockstep execution
|
||||
}
|
||||
|
||||
public async IAsyncEnumerable<WorkflowEvent> TakeEventStreamAsync(bool blockOnPendingRequest, [EnumeratorCancellation] CancellationToken cancellation = default)
|
||||
public async IAsyncEnumerable<WorkflowEvent> TakeEventStreamAsync(bool blockOnPendingRequest, [EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
#if NET
|
||||
ObjectDisposedException.ThrowIf(Volatile.Read(ref this._isDisposed) == 1, this);
|
||||
@@ -47,7 +47,7 @@ internal sealed class LockstepRunEventStream : IRunEventStream
|
||||
}
|
||||
#endif
|
||||
|
||||
CancellationTokenSource linkedSource = CancellationTokenSource.CreateLinkedTokenSource(this._stopCancellation.Token, cancellation);
|
||||
CancellationTokenSource linkedSource = CancellationTokenSource.CreateLinkedTokenSource(this._stopCancellation.Token, cancellationToken);
|
||||
|
||||
ConcurrentQueue<WorkflowEvent> eventSink = [];
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.AI.Workflows.Checkpointing;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
@@ -11,12 +12,14 @@ using CatchAllF =
|
||||
System.Func<
|
||||
Microsoft.Agents.AI.Workflows.PortableValue, // message
|
||||
Microsoft.Agents.AI.Workflows.IWorkflowContext, // context
|
||||
System.Threading.CancellationToken, // cancellation
|
||||
System.Threading.Tasks.ValueTask<Microsoft.Agents.AI.Workflows.Execution.CallResult>
|
||||
>;
|
||||
using MessageHandlerF =
|
||||
System.Func<
|
||||
object, // message
|
||||
Microsoft.Agents.AI.Workflows.IWorkflowContext, // context
|
||||
System.Threading.CancellationToken, // cancellation
|
||||
System.Threading.Tasks.ValueTask<Microsoft.Agents.AI.Workflows.Execution.CallResult>
|
||||
>;
|
||||
|
||||
@@ -56,7 +59,7 @@ internal sealed class MessageRouter
|
||||
|
||||
public HashSet<Type> DefaultOutputTypes { get; }
|
||||
|
||||
public async ValueTask<CallResult?> RouteMessageAsync(object message, IWorkflowContext context, bool requireRoute = false)
|
||||
public async ValueTask<CallResult?> RouteMessageAsync(object message, IWorkflowContext context, bool requireRoute = false, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(message);
|
||||
|
||||
@@ -74,13 +77,13 @@ internal sealed class MessageRouter
|
||||
{
|
||||
if (this._typedHandlers.TryGetValue(message.GetType(), out MessageHandlerF? handler))
|
||||
{
|
||||
result = await handler(message, context).ConfigureAwait(false);
|
||||
result = await handler(message, context, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
else if (this.HasCatchAll)
|
||||
{
|
||||
portableValue ??= new PortableValue(message);
|
||||
|
||||
result = await this._catchAllFunc(portableValue, context).ConfigureAwait(false);
|
||||
result = await this._catchAllFunc(portableValue, context, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
|
||||
@@ -99,6 +99,13 @@ internal sealed class StateManager
|
||||
|
||||
public ValueTask<T?> ReadStateAsync<T>(ScopeId scopeId, string key)
|
||||
{
|
||||
if (typeof(T) == typeof(object))
|
||||
{
|
||||
// Reading as object will break across serialize/deserialize boundaries, e.g. checkpointing, distributed runtime, etc.
|
||||
// Disabled pending upstream updates for this change; see https://github.com/microsoft/agent-framework/issues/1369
|
||||
//throw new NotSupportedException("Reading state as 'object' is not supported. Use 'PortableValue' instead for variants.");
|
||||
}
|
||||
|
||||
Throw.IfNullOrEmpty(key);
|
||||
|
||||
UpdateKey stateKey = new(scopeId, key);
|
||||
@@ -116,6 +123,16 @@ internal sealed class StateManager
|
||||
{
|
||||
return new((T?)result.Value);
|
||||
}
|
||||
else if (result.Value == null)
|
||||
{
|
||||
// Technically should only happen if T is nullable, but we don't have the ability to express that
|
||||
// so we cannot `return new((T?)null);` directly.
|
||||
return new((T?)default);
|
||||
}
|
||||
else if (typeof(T) == typeof(PortableValue))
|
||||
{
|
||||
return new((T)(object)new PortableValue(result.Value));
|
||||
}
|
||||
|
||||
throw new InvalidOperationException($"State for key '{key}' in scope '{scopeId}' is not of type '{typeof(T).Name}'.");
|
||||
}
|
||||
|
||||
@@ -51,6 +51,13 @@ internal sealed class StateScope
|
||||
Throw.IfNullOrEmpty(key);
|
||||
if (this._stateData.TryGetValue(key, out PortableValue? value))
|
||||
{
|
||||
if (typeof(T) == typeof(PortableValue) && !value.TypeId.IsMatch(typeof(PortableValue)))
|
||||
{
|
||||
// value is PortableValue, and we do not need to unwrap a PortableValue instance inside of it
|
||||
// Unfortunately we need to cast through object here.
|
||||
return new((T)(object)value);
|
||||
}
|
||||
|
||||
return new(value.As<T>());
|
||||
}
|
||||
|
||||
|
||||
@@ -50,10 +50,10 @@ internal sealed class StreamingRunEventStream : IRunEventStream
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RunLoopAsync(CancellationToken cancellation)
|
||||
private async Task RunLoopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
using CancellationTokenSource errorSource = new();
|
||||
CancellationTokenSource linkedSource = CancellationTokenSource.CreateLinkedTokenSource(errorSource.Token, cancellation);
|
||||
CancellationTokenSource linkedSource = CancellationTokenSource.CreateLinkedTokenSource(errorSource.Token, cancellationToken);
|
||||
|
||||
// Subscribe to events - they will flow directly to the channel as they're raised
|
||||
this._stepRunner.OutgoingEvents.EventRaised += OnEventRaisedAsync;
|
||||
@@ -62,7 +62,7 @@ internal sealed class StreamingRunEventStream : IRunEventStream
|
||||
{
|
||||
// Wait for the first input before starting
|
||||
// The consumer will call EnqueueMessageAsync which signals the run loop
|
||||
await this._inputWaiter.WaitForInputAsync(cancellation: linkedSource.Token).ConfigureAwait(false);
|
||||
await this._inputWaiter.WaitForInputAsync(cancellationToken: linkedSource.Token).ConfigureAwait(false);
|
||||
|
||||
this._runStatus = RunStatus.Running;
|
||||
|
||||
@@ -134,7 +134,7 @@ internal sealed class StreamingRunEventStream : IRunEventStream
|
||||
|
||||
public async IAsyncEnumerable<WorkflowEvent> TakeEventStreamAsync(
|
||||
bool blockOnPendingRequest,
|
||||
[EnumeratorCancellation] CancellationToken cancellation = default)
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Get the current epoch - we'll only respond to completion signals from this epoch or later
|
||||
int myEpoch = Volatile.Read(ref this._completionEpoch) + 1;
|
||||
@@ -143,7 +143,7 @@ internal sealed class StreamingRunEventStream : IRunEventStream
|
||||
// Note: When cancellation is requested, ReadAllAsync may throw OperationCanceledException
|
||||
// or may complete the enumeration. We check IsCancellationRequested explicitly at superstep
|
||||
// boundaries to ensure clean cancellation.
|
||||
await foreach (WorkflowEvent evt in this._eventChannel.Reader.ReadAllAsync(cancellation).ConfigureAwait(false))
|
||||
await foreach (WorkflowEvent evt in this._eventChannel.Reader.ReadAllAsync(cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
// Filter out internal signals used for run loop coordination
|
||||
if (evt is InternalHaltSignal completionSignal)
|
||||
@@ -156,7 +156,7 @@ internal sealed class StreamingRunEventStream : IRunEventStream
|
||||
|
||||
// Check for cancellation at superstep boundaries (before processing completion signal)
|
||||
// This allows consumers to stop reading events cleanly between supersteps
|
||||
if (cancellation.IsCancellationRequested)
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
@@ -186,7 +186,7 @@ internal sealed class StreamingRunEventStream : IRunEventStream
|
||||
yield break;
|
||||
}
|
||||
|
||||
if (cancellation.IsCancellationRequested)
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
@@ -195,7 +195,7 @@ internal sealed class StreamingRunEventStream : IRunEventStream
|
||||
}
|
||||
}
|
||||
|
||||
public ValueTask<RunStatus> GetStatusAsync(CancellationToken cancellation = default)
|
||||
public ValueTask<RunStatus> GetStatusAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Thread-safe read of status (enum is read atomically on most platforms)
|
||||
return new ValueTask<RunStatus>(this._runStatus);
|
||||
|
||||
@@ -90,10 +90,12 @@ public abstract class Executor : IIdentified
|
||||
/// <param name="messageType">The "declared" type of the message (captured when it was being sent). This is
|
||||
/// used to enable routing messages as their base types, in absence of true polymorphic type routing.</param>
|
||||
/// <param name="context">The workflow context in which the executor executes.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.
|
||||
/// The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A ValueTask representing the asynchronous operation, wrapping the output from the executor.</returns>
|
||||
/// <exception cref="NotSupportedException">No handler found for the message type.</exception>
|
||||
/// <exception cref="TargetInvocationException">An exception is generated while handling the message.</exception>
|
||||
public async ValueTask<object?> ExecuteAsync(object message, TypeId messageType, IWorkflowContext context)
|
||||
public async ValueTask<object?> ExecuteAsync(object message, TypeId messageType, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var activity = s_activitySource.StartActivity(ActivityNames.ExecutorProcess, ActivityKind.Internal);
|
||||
activity?.SetTag(Tags.ExecutorId, this.Id)
|
||||
@@ -101,9 +103,9 @@ public abstract class Executor : IIdentified
|
||||
.SetTag(Tags.MessageType, messageType.TypeName)
|
||||
.CreateSourceLinks(context.TraceContext);
|
||||
|
||||
await context.AddEventAsync(new ExecutorInvokedEvent(this.Id, message)).ConfigureAwait(false);
|
||||
await context.AddEventAsync(new ExecutorInvokedEvent(this.Id, message), cancellationToken).ConfigureAwait(false);
|
||||
|
||||
CallResult? result = await this.Router.RouteMessageAsync(message, context, requireRoute: true)
|
||||
CallResult? result = await this.Router.RouteMessageAsync(message, context, requireRoute: true, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
ExecutorEvent executionResult;
|
||||
@@ -116,7 +118,7 @@ public abstract class Executor : IIdentified
|
||||
executionResult = new ExecutorFailedEvent(this.Id, result.Exception);
|
||||
}
|
||||
|
||||
await context.AddEventAsync(executionResult).ConfigureAwait(false);
|
||||
await context.AddEventAsync(executionResult, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (result is null)
|
||||
{
|
||||
@@ -137,11 +139,11 @@ public abstract class Executor : IIdentified
|
||||
// If we had a real return type, raise it as a SendMessage; TODO: Should we have a way to disable this behaviour?
|
||||
if (result.Result is not null && this.Options.AutoSendMessageHandlerResultObject)
|
||||
{
|
||||
await context.SendMessageAsync(result.Result).ConfigureAwait(false);
|
||||
await context.SendMessageAsync(result.Result, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
if (result.Result is not null && this.Options.AutoYieldOutputHandlerResultObject)
|
||||
{
|
||||
await context.YieldOutputAsync(result.Result).ConfigureAwait(false);
|
||||
await context.YieldOutputAsync(result.Result, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return result.Result;
|
||||
@@ -152,7 +154,8 @@ public abstract class Executor : IIdentified
|
||||
/// </summary>
|
||||
/// <param name="context">The workflow context.</param>
|
||||
/// <returns>A ValueTask representing the asynchronous operation.</returns>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.
|
||||
/// The default is <see cref="CancellationToken.None"/>.</param>
|
||||
protected internal virtual ValueTask OnCheckpointingAsync(IWorkflowContext context, CancellationToken cancellationToken = default) => default;
|
||||
|
||||
/// <summary>
|
||||
@@ -160,7 +163,8 @@ public abstract class Executor : IIdentified
|
||||
/// </summary>
|
||||
/// <param name="context">The workflow context.</param>
|
||||
/// <returns>A ValueTask representing the asynchronous operation.</returns>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.
|
||||
/// The default is <see cref="CancellationToken.None"/>.</param>
|
||||
protected internal virtual ValueTask OnCheckpointRestoredAsync(IWorkflowContext context, CancellationToken cancellationToken = default) => default;
|
||||
|
||||
/// <summary>
|
||||
@@ -210,7 +214,7 @@ public abstract class Executor<TInput>(string id, ExecutorOptions? options = nul
|
||||
routeBuilder.AddHandler<TInput>(this.HandleAsync);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public abstract ValueTask HandleAsync(TInput message, IWorkflowContext context);
|
||||
public abstract ValueTask HandleAsync(TInput message, IWorkflowContext context, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -229,5 +233,5 @@ public abstract class Executor<TInput, TOutput>(string id, ExecutorOptions? opti
|
||||
routeBuilder.AddHandler<TInput, TOutput>(this.HandleAsync);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public abstract ValueTask<TOutput> HandleAsync(TInput message, IWorkflowContext context);
|
||||
public abstract ValueTask<TOutput> HandleAsync(TInput message, IWorkflowContext context, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ public class FunctionExecutor<TInput>(string id,
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override ValueTask HandleAsync(TInput message, IWorkflowContext context) => handlerAsync(message, context, default);
|
||||
public override ValueTask HandleAsync(TInput message, IWorkflowContext context, CancellationToken cancellationToken) => handlerAsync(message, context, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of the <see cref="FunctionExecutor{TInput}"/> class.
|
||||
@@ -65,7 +65,7 @@ public class FunctionExecutor<TInput, TOutput>(string id,
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override ValueTask<TOutput> HandleAsync(TInput message, IWorkflowContext context) => handlerAsync(message, context, default);
|
||||
public override ValueTask<TOutput> HandleAsync(TInput message, IWorkflowContext context, CancellationToken cancellationToken) => handlerAsync(message, context, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of the <see cref="FunctionExecutor{TInput,TOutput}"/> class.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows;
|
||||
@@ -15,8 +16,10 @@ public interface IWorkflowContext
|
||||
/// end of the current SuperStep.
|
||||
/// </summary>
|
||||
/// <param name="workflowEvent">The event to be raised.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.
|
||||
/// The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A <see cref="ValueTask"/> representing the asynchronous operation.</returns>
|
||||
ValueTask AddEventAsync(WorkflowEvent workflowEvent);
|
||||
ValueTask AddEventAsync(WorkflowEvent workflowEvent, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Queues a message to be sent to connected executors. The message will be sent during the next SuperStep.
|
||||
@@ -25,8 +28,22 @@ public interface IWorkflowContext
|
||||
/// <param name="targetId">An optional identifier of the target executor. If null, the message is sent to all connected
|
||||
/// executors. If the target executor is not connected from this executor via an edge, it will still not receive the
|
||||
/// message.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.
|
||||
/// The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A <see cref="ValueTask"/> representing the asynchronous operation.</returns>
|
||||
ValueTask SendMessageAsync(object message, string? targetId = null);
|
||||
ValueTask SendMessageAsync(object message, string? targetId = null, CancellationToken cancellationToken = default);
|
||||
|
||||
#if NET // What's the right way to do this so we do not make life a misery for netstandard2.0 targets?
|
||||
// What's the value if they have to still write `cancellationToken: cancellationToken` to skip the targetId parameter?
|
||||
// TODO: Remove this? (Maybe not: NET will eventually be the only target framework, right?)
|
||||
/// <summary>
|
||||
/// Queues a message to be sent to connected executors. The message will be sent during the next SuperStep.
|
||||
/// </summary>
|
||||
/// <param name="message">The message to be sent.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.</param>
|
||||
/// <returns>A <see cref="ValueTask"/> representing the asynchronous operation.</returns>
|
||||
ValueTask SendMessageAsync(object message, CancellationToken cancellationToken) => this.SendMessageAsync(message, null, cancellationToken);
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Adds an output value to the workflow's output queue. These outputs will be bubbled out of the workflow using the
|
||||
@@ -37,8 +54,10 @@ public interface IWorkflowContext
|
||||
/// types of registered message handlers are considered output types, unless otherwise specified using <see cref="ExecutorOptions"/>.
|
||||
/// </remarks>
|
||||
/// <param name="output">The output value to be returned.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.
|
||||
/// The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A <see cref="ValueTask"/> representing the asynchronous operation.</returns>
|
||||
ValueTask YieldOutputAsync(object output);
|
||||
ValueTask YieldOutputAsync(object output, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Adds a request to "halt" workflow execution at the end of the current SuperStep.
|
||||
@@ -54,15 +73,32 @@ public interface IWorkflowContext
|
||||
/// <param name="key">The key of the state value.</param>
|
||||
/// <param name = "scopeName" > An optional name that specifies the scope to read.If null, the default scope is
|
||||
/// used.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.
|
||||
/// The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A <see cref="ValueTask{T}"/> representing the asynchronous operation.</returns>
|
||||
ValueTask<T?> ReadStateAsync<T>(string key, string? scopeName = null);
|
||||
ValueTask<T?> ReadStateAsync<T>(string key, string? scopeName = null, CancellationToken cancellationToken = default);
|
||||
|
||||
#if NET // See above for musings about this construction
|
||||
/// <summary>
|
||||
/// Reads a state value from the workflow's state store. If no scope is provided, the executor's
|
||||
/// default scope is used.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the state value.</typeparam>
|
||||
/// <param name="key">The key of the state value.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.</param>
|
||||
/// <returns>A <see cref="ValueTask{T}"/> representing the asynchronous operation.</returns>
|
||||
ValueTask<T?> ReadStateAsync<T>(string key, CancellationToken cancellationToken) => this.ReadStateAsync<T>(key, null, cancellationToken);
|
||||
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously reads all state keys within the specified scope.
|
||||
/// </summary>
|
||||
/// <param name="scopeName">An optional name that specifies the scope to read. If null, the default scope is
|
||||
/// used.</param>
|
||||
ValueTask<HashSet<string>> ReadStateKeysAsync(string? scopeName = null);
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.
|
||||
/// The default is <see cref="CancellationToken.None"/>.</param>
|
||||
ValueTask<HashSet<string>> ReadStateKeysAsync(string? scopeName = null, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously updates the state of a queue entry identified by the specified key and optional scope.
|
||||
@@ -77,8 +113,27 @@ public interface IWorkflowContext
|
||||
/// implementation.</param>
|
||||
/// <param name="scopeName">An optional name that specifies the scope to update. If null, the default scope is
|
||||
/// used.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.
|
||||
/// The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A ValueTask that represents the asynchronous update operation.</returns>
|
||||
ValueTask QueueStateUpdateAsync<T>(string key, T? value, string? scopeName = null);
|
||||
ValueTask QueueStateUpdateAsync<T>(string key, T? value, string? scopeName = null, CancellationToken cancellationToken = default);
|
||||
|
||||
#if NET // See above for musings about this construction
|
||||
/// <summary>
|
||||
/// Asynchronously updates the state of a queue entry identified by the specified key and optional scope.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Subsequent reads by this executor will result in the new value of the state. Other executors will only see
|
||||
/// the new state starting from the next SuperStep.
|
||||
/// </remarks>
|
||||
/// <typeparam name="T">The type of the value to associate with the queue entry.</typeparam>
|
||||
/// <param name="key">The unique identifier for the queue entry to update. Cannot be null or empty.</param>
|
||||
/// <param name="value">The value to set for the queue entry. If null, the entry's state may be cleared or reset depending on
|
||||
/// implementation.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.</param>
|
||||
/// <returns>A ValueTask that represents the asynchronous update operation.</returns>
|
||||
ValueTask QueueStateUpdateAsync<T>(string key, T? value, CancellationToken cancellationToken) => this.QueueStateUpdateAsync(key, value, null, cancellationToken);
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously clears all state entries within the specified scope.
|
||||
@@ -90,8 +145,25 @@ public interface IWorkflowContext
|
||||
/// see the cleared state starting from the next SuperStep.
|
||||
/// </remarks>
|
||||
/// <param name="scopeName">An optional name that specifies the scope to clear. If null, the default scope is used.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.
|
||||
/// The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A ValueTask that represents the asynchronous clear operation.</returns>
|
||||
ValueTask QueueClearScopeAsync(string? scopeName = null);
|
||||
ValueTask QueueClearScopeAsync(string? scopeName = null, CancellationToken cancellationToken = default);
|
||||
|
||||
#if NET // See above for musings about this construction
|
||||
/// <summary>
|
||||
/// Asynchronously clears all state entries within the specified scope.
|
||||
///
|
||||
/// This semantically equivalent to retrieving all keys in the scope and deleting them one-by-one.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Subsequent reads by this executor will not find any entries in the cleared scope. Other executors will only
|
||||
/// see the cleared state starting from the next SuperStep.
|
||||
/// </remarks>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.</param>
|
||||
/// <returns>A ValueTask that represents the asynchronous clear operation.</returns>
|
||||
ValueTask QueueClearScopeAsync(CancellationToken cancellationToken) => this.QueueClearScopeAsync(null, cancellationToken);
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// The trace context associated with the current message about to be processed by the executor, if any.
|
||||
|
||||
@@ -46,14 +46,14 @@ internal sealed class InProcessRunner : ISuperStepRunner, ICheckpointingHandle
|
||||
public string StartExecutorId { get; }
|
||||
|
||||
private readonly HashSet<Type> _knownValidInputTypes;
|
||||
public async ValueTask<bool> IsValidInputTypeAsync(Type messageType, CancellationToken cancellation = default)
|
||||
public async ValueTask<bool> IsValidInputTypeAsync(Type messageType, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (this._knownValidInputTypes.Contains(messageType))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
Executor startingExecutor = await this.RunContext.EnsureExecutorAsync(this.Workflow.StartExecutorId, tracer: null).ConfigureAwait(false);
|
||||
Executor startingExecutor = await this.RunContext.EnsureExecutorAsync(this.Workflow.StartExecutorId, tracer: null, cancellationToken).ConfigureAwait(false);
|
||||
if (startingExecutor.CanHandle(messageType))
|
||||
{
|
||||
this._knownValidInputTypes.Add(messageType);
|
||||
@@ -63,10 +63,10 @@ internal sealed class InProcessRunner : ISuperStepRunner, ICheckpointingHandle
|
||||
return false;
|
||||
}
|
||||
|
||||
public ValueTask<bool> IsValidInputTypeAsync<T>(CancellationToken cancellation = default)
|
||||
=> this.IsValidInputTypeAsync(typeof(T), cancellation);
|
||||
public ValueTask<bool> IsValidInputTypeAsync<T>(CancellationToken cancellationToken = default)
|
||||
=> this.IsValidInputTypeAsync(typeof(T), cancellationToken);
|
||||
|
||||
public async ValueTask<bool> EnqueueMessageUntypedAsync(object message, Type declaredType, CancellationToken cancellation = default)
|
||||
public async ValueTask<bool> EnqueueMessageUntypedAsync(object message, Type declaredType, CancellationToken cancellationToken = default)
|
||||
{
|
||||
this.RunContext.CheckEnded();
|
||||
Throw.IfNull(message);
|
||||
@@ -78,7 +78,7 @@ internal sealed class InProcessRunner : ISuperStepRunner, ICheckpointingHandle
|
||||
|
||||
// Check that the type of the incoming message is compatible with the starting executor's
|
||||
// input type.
|
||||
if (!await this.IsValidInputTypeAsync(declaredType, cancellation).ConfigureAwait(false))
|
||||
if (!await this.IsValidInputTypeAsync(declaredType, cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -87,13 +87,13 @@ internal sealed class InProcessRunner : ISuperStepRunner, ICheckpointingHandle
|
||||
return true;
|
||||
}
|
||||
|
||||
public ValueTask<bool> EnqueueMessageAsync<T>(T message, CancellationToken cancellation = default)
|
||||
=> this.EnqueueMessageUntypedAsync(Throw.IfNull(message), typeof(T), cancellation);
|
||||
public ValueTask<bool> EnqueueMessageAsync<T>(T message, CancellationToken cancellationToken = default)
|
||||
=> this.EnqueueMessageUntypedAsync(Throw.IfNull(message), typeof(T), cancellationToken);
|
||||
|
||||
public ValueTask<bool> EnqueueMessageAsync(object message, CancellationToken cancellation = default)
|
||||
=> this.EnqueueMessageUntypedAsync(Throw.IfNull(message), message.GetType(), cancellation);
|
||||
public ValueTask<bool> EnqueueMessageUntypedAsync(object message, CancellationToken cancellationToken = default)
|
||||
=> this.EnqueueMessageUntypedAsync(Throw.IfNull(message), message.GetType(), cancellationToken);
|
||||
|
||||
ValueTask ISuperStepRunner.EnqueueResponseAsync(ExternalResponse response, CancellationToken cancellation)
|
||||
ValueTask ISuperStepRunner.EnqueueResponseAsync(ExternalResponse response, CancellationToken cancellationToken)
|
||||
{
|
||||
// TODO: Check that there exists a corresponding input port?
|
||||
return this.RunContext.AddExternalResponseAsync(response);
|
||||
@@ -110,13 +110,13 @@ internal sealed class InProcessRunner : ISuperStepRunner, ICheckpointingHandle
|
||||
private ValueTask RaiseWorkflowEventAsync(WorkflowEvent workflowEvent)
|
||||
=> this.OutgoingEvents.EnqueueAsync(workflowEvent);
|
||||
|
||||
public ValueTask<AsyncRunHandle> BeginStreamAsync(ExecutionMode mode, CancellationToken cancellation = default)
|
||||
public ValueTask<AsyncRunHandle> BeginStreamAsync(ExecutionMode mode, CancellationToken cancellationToken = default)
|
||||
{
|
||||
this.RunContext.CheckEnded();
|
||||
return new(new AsyncRunHandle(this, this, mode));
|
||||
}
|
||||
|
||||
public async ValueTask<AsyncRunHandle> ResumeStreamAsync(ExecutionMode mode, CheckpointInfo fromCheckpoint, CancellationToken cancellation = default)
|
||||
public async ValueTask<AsyncRunHandle> ResumeStreamAsync(ExecutionMode mode, CheckpointInfo fromCheckpoint, CancellationToken cancellationToken = default)
|
||||
{
|
||||
this.RunContext.CheckEnded();
|
||||
Throw.IfNull(fromCheckpoint);
|
||||
@@ -125,7 +125,7 @@ internal sealed class InProcessRunner : ISuperStepRunner, ICheckpointingHandle
|
||||
throw new InvalidOperationException("This runner was not configured with a CheckpointManager, so it cannot restore checkpoints.");
|
||||
}
|
||||
|
||||
await this.RestoreCheckpointAsync(fromCheckpoint, cancellation).ConfigureAwait(false);
|
||||
await this.RestoreCheckpointAsync(fromCheckpoint, cancellationToken).ConfigureAwait(false);
|
||||
return new AsyncRunHandle(this, this, mode);
|
||||
}
|
||||
|
||||
@@ -142,7 +142,7 @@ internal sealed class InProcessRunner : ISuperStepRunner, ICheckpointingHandle
|
||||
return false;
|
||||
}
|
||||
|
||||
StepContext currentStep = await this.RunContext.AdvanceAsync().ConfigureAwait(false);
|
||||
StepContext currentStep = await this.RunContext.AdvanceAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (currentStep.HasMessages ||
|
||||
this.RunContext.HasQueuedExternalDeliveries ||
|
||||
@@ -150,7 +150,7 @@ internal sealed class InProcessRunner : ISuperStepRunner, ICheckpointingHandle
|
||||
{
|
||||
try
|
||||
{
|
||||
await this.RunSuperstepAsync(currentStep).ConfigureAwait(false);
|
||||
await this.RunSuperstepAsync(currentStep, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{ }
|
||||
@@ -165,9 +165,9 @@ internal sealed class InProcessRunner : ISuperStepRunner, ICheckpointingHandle
|
||||
return false;
|
||||
}
|
||||
|
||||
private async ValueTask DeliverMessagesAsync(string receiverId, ConcurrentQueue<MessageEnvelope> envelopes)
|
||||
private async ValueTask DeliverMessagesAsync(string receiverId, ConcurrentQueue<MessageEnvelope> envelopes, CancellationToken cancellationToken)
|
||||
{
|
||||
Executor executor = await this.RunContext.EnsureExecutorAsync(receiverId, this.StepTracer).ConfigureAwait(false);
|
||||
Executor executor = await this.RunContext.EnsureExecutorAsync(receiverId, this.StepTracer, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
this.StepTracer.TraceActivated(receiverId);
|
||||
while (envelopes.TryDequeue(out var envelope))
|
||||
@@ -175,19 +175,20 @@ internal sealed class InProcessRunner : ISuperStepRunner, ICheckpointingHandle
|
||||
await executor.ExecuteAsync(
|
||||
envelope.Message,
|
||||
envelope.MessageType,
|
||||
this.RunContext.Bind(receiverId, envelope.TraceContext)
|
||||
this.RunContext.Bind(receiverId, envelope.TraceContext),
|
||||
cancellationToken
|
||||
).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private async ValueTask RunSuperstepAsync(StepContext currentStep)
|
||||
private async ValueTask RunSuperstepAsync(StepContext currentStep, CancellationToken cancellationToken)
|
||||
{
|
||||
await this.RaiseWorkflowEventAsync(this.StepTracer.Advance(currentStep)).ConfigureAwait(false);
|
||||
|
||||
// Deliver the messages and queue the next step
|
||||
List<Task> receiverTasks =
|
||||
currentStep.QueuedMessages.Keys
|
||||
.Select(receiverId => this.DeliverMessagesAsync(receiverId, currentStep.MessagesFor(receiverId)).AsTask())
|
||||
.Select(receiverId => this.DeliverMessagesAsync(receiverId, currentStep.MessagesFor(receiverId), cancellationToken).AsTask())
|
||||
.ToList();
|
||||
|
||||
// TODO: Should we let the user specify that they want strictly turn-based execution of the edges, vs. concurrent?
|
||||
@@ -202,12 +203,12 @@ internal sealed class InProcessRunner : ISuperStepRunner, ICheckpointingHandle
|
||||
List<Task> subworkflowTasks = new();
|
||||
foreach (ISuperStepRunner subworkflowRunner in this.RunContext.JoinedSubworkflowRunners)
|
||||
{
|
||||
subworkflowTasks.Add(subworkflowRunner.RunSuperStepAsync(CancellationToken.None).AsTask());
|
||||
subworkflowTasks.Add(subworkflowRunner.RunSuperStepAsync(cancellationToken).AsTask());
|
||||
}
|
||||
|
||||
await Task.WhenAll(subworkflowTasks).ConfigureAwait(false);
|
||||
|
||||
await this.CheckpointAsync().ConfigureAwait(false);
|
||||
await this.CheckpointAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
await this.RaiseWorkflowEventAsync(this.StepTracer.Complete(this.RunContext.NextStepHasActions, this.RunContext.HasUnservicedRequests))
|
||||
.ConfigureAwait(false);
|
||||
|
||||
@@ -57,7 +57,7 @@ internal sealed class InProcessRunnerContext : IRunnerContext
|
||||
this.OutgoingEvents = outgoingEvents;
|
||||
}
|
||||
|
||||
public async ValueTask<Executor> EnsureExecutorAsync(string executorId, IStepTracer? tracer)
|
||||
public async ValueTask<Executor> EnsureExecutorAsync(string executorId, IStepTracer? tracer, CancellationToken cancellationToken = default)
|
||||
{
|
||||
this.CheckEnded();
|
||||
Task<Executor> executorTask = this._executors.GetOrAdd(executorId, CreateExecutorAsync);
|
||||
@@ -88,9 +88,9 @@ internal sealed class InProcessRunnerContext : IRunnerContext
|
||||
return await executorTask.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async ValueTask<IEnumerable<Type>> GetStartingExecutorInputTypesAsync(CancellationToken cancellation = default)
|
||||
public async ValueTask<IEnumerable<Type>> GetStartingExecutorInputTypesAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
Executor startingExecutor = await this.EnsureExecutorAsync(this._workflow.StartExecutorId, tracer: null)
|
||||
Executor startingExecutor = await this.EnsureExecutorAsync(this._workflow.StartExecutorId, tracer: null, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return startingExecutor.InputTypes;
|
||||
@@ -145,7 +145,7 @@ internal sealed class InProcessRunnerContext : IRunnerContext
|
||||
public bool HasUnservicedRequests => !this._externalRequests.IsEmpty ||
|
||||
this._joinedSubworkflowRunners.Any(joinedRunner => joinedRunner.HasUnservicedRequests);
|
||||
|
||||
public async ValueTask<StepContext> AdvanceAsync()
|
||||
public async ValueTask<StepContext> AdvanceAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
this.CheckEnded();
|
||||
|
||||
@@ -159,7 +159,7 @@ internal sealed class InProcessRunnerContext : IRunnerContext
|
||||
return Interlocked.Exchange(ref this._nextStep, new StepContext());
|
||||
}
|
||||
|
||||
public ValueTask AddEventAsync(WorkflowEvent workflowEvent)
|
||||
public ValueTask AddEventAsync(WorkflowEvent workflowEvent, CancellationToken cancellationToken = default)
|
||||
{
|
||||
this.CheckEnded();
|
||||
return this.OutgoingEvents.EnqueueAsync(workflowEvent);
|
||||
@@ -168,7 +168,7 @@ internal sealed class InProcessRunnerContext : IRunnerContext
|
||||
private static readonly string s_namespace = typeof(IWorkflowContext).Namespace!;
|
||||
private static readonly ActivitySource s_activitySource = new(s_namespace);
|
||||
|
||||
public async ValueTask SendMessageAsync(string sourceId, object message, string? targetId = null)
|
||||
public async ValueTask SendMessageAsync(string sourceId, object message, string? targetId = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using Activity? activity = s_activitySource.StartActivity(ActivityNames.MessageSend, ActivityKind.Producer);
|
||||
// Create a carrier for trace context propagation
|
||||
@@ -231,19 +231,19 @@ internal sealed class InProcessRunnerContext : IRunnerContext
|
||||
OutputFilter outputFilter,
|
||||
Dictionary<string, string>? traceContext) : IWorkflowContext
|
||||
{
|
||||
public ValueTask AddEventAsync(WorkflowEvent workflowEvent) => RunnerContext.AddEventAsync(workflowEvent);
|
||||
public ValueTask AddEventAsync(WorkflowEvent workflowEvent, CancellationToken cancellationToken = default) => RunnerContext.AddEventAsync(workflowEvent, cancellationToken);
|
||||
|
||||
public ValueTask SendMessageAsync(object message, string? targetId = null)
|
||||
public ValueTask SendMessageAsync(object message, string? targetId = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return RunnerContext.SendMessageAsync(ExecutorId, message, targetId);
|
||||
return RunnerContext.SendMessageAsync(ExecutorId, message, targetId, cancellationToken);
|
||||
}
|
||||
|
||||
public async ValueTask YieldOutputAsync(object output)
|
||||
public async ValueTask YieldOutputAsync(object output, CancellationToken cancellationToken = default)
|
||||
{
|
||||
RunnerContext.CheckEnded();
|
||||
Throw.IfNull(output);
|
||||
|
||||
Executor sourceExecutor = await RunnerContext.EnsureExecutorAsync(ExecutorId, tracer: null).ConfigureAwait(false);
|
||||
Executor sourceExecutor = await RunnerContext.EnsureExecutorAsync(ExecutorId, tracer: null, cancellationToken).ConfigureAwait(false);
|
||||
if (!sourceExecutor.CanOutput(output.GetType()))
|
||||
{
|
||||
throw new InvalidOperationException($"Cannot output object of type {output.GetType().Name}. Expecting one of [{string.Join(", ", sourceExecutor.OutputTypes)}].");
|
||||
@@ -251,22 +251,22 @@ internal sealed class InProcessRunnerContext : IRunnerContext
|
||||
|
||||
if (outputFilter.CanOutput(ExecutorId, output))
|
||||
{
|
||||
await this.AddEventAsync(new WorkflowOutputEvent(output, ExecutorId)).ConfigureAwait(false);
|
||||
await this.AddEventAsync(new WorkflowOutputEvent(output, ExecutorId), cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
public ValueTask RequestHaltAsync() => this.AddEventAsync(new RequestHaltEvent());
|
||||
|
||||
public ValueTask<T?> ReadStateAsync<T>(string key, string? scopeName = null)
|
||||
public ValueTask<T?> ReadStateAsync<T>(string key, string? scopeName = null, CancellationToken cancellationToken = default)
|
||||
=> RunnerContext.StateManager.ReadStateAsync<T>(ExecutorId, scopeName, key);
|
||||
|
||||
public ValueTask<HashSet<string>> ReadStateKeysAsync(string? scopeName = null)
|
||||
public ValueTask<HashSet<string>> ReadStateKeysAsync(string? scopeName = null, CancellationToken cancellationToken = default)
|
||||
=> RunnerContext.StateManager.ReadKeysAsync(ExecutorId, scopeName);
|
||||
|
||||
public ValueTask QueueStateUpdateAsync<T>(string key, T? value, string? scopeName = null)
|
||||
public ValueTask QueueStateUpdateAsync<T>(string key, T? value, string? scopeName = null, CancellationToken cancellationToken = default)
|
||||
=> RunnerContext.StateManager.WriteStateAsync(ExecutorId, scopeName, key, value);
|
||||
|
||||
public ValueTask QueueClearScopeAsync(string? scopeName = null)
|
||||
public ValueTask QueueClearScopeAsync(string? scopeName = null, CancellationToken cancellationToken = default)
|
||||
=> RunnerContext.StateManager.ClearStateAsync(ExecutorId, scopeName);
|
||||
|
||||
public IReadOnlyDictionary<string, string>? TraceContext => traceContext;
|
||||
@@ -274,7 +274,7 @@ internal sealed class InProcessRunnerContext : IRunnerContext
|
||||
|
||||
public bool WithCheckpointing { get; }
|
||||
|
||||
internal Task PrepareForCheckpointAsync(CancellationToken cancellation = default)
|
||||
internal Task PrepareForCheckpointAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
this.CheckEnded();
|
||||
|
||||
@@ -283,7 +283,7 @@ internal sealed class InProcessRunnerContext : IRunnerContext
|
||||
async Task InvokeCheckpointingAsync(Task<Executor> executorTask)
|
||||
{
|
||||
Executor executor = await executorTask.ConfigureAwait(false);
|
||||
await executor.OnCheckpointingAsync(this.Bind(executor.Id), cancellation).ConfigureAwait(false);
|
||||
await executor.OnCheckpointingAsync(this.Bind(executor.Id), cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -320,7 +320,7 @@ internal sealed class InProcessRunnerContext : IRunnerContext
|
||||
{
|
||||
foreach (string requestId in this._externalRequests.Keys)
|
||||
{
|
||||
await this.AddEventAsync(new RequestInfoEvent(this._externalRequests[requestId]))
|
||||
await this.AddEventAsync(new RequestInfoEvent(this._externalRequests[requestId]), cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
@@ -386,7 +386,7 @@ internal sealed class InProcessRunnerContext : IRunnerContext
|
||||
|
||||
public IEnumerable<ISuperStepRunner> JoinedSubworkflowRunners => this._joinedSubworkflowRunners;
|
||||
|
||||
public ValueTask AttachSuperstepAsync(ISuperStepRunner superStepRunner, CancellationToken cancellation = default)
|
||||
public ValueTask AttachSuperstepAsync(ISuperStepRunner superStepRunner, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// This needs to be a thread-safe ordered collection because we can potentially instantiate executors
|
||||
// in parallel, which means multiple sub-workflows could be attaching at the same time.
|
||||
@@ -394,9 +394,9 @@ internal sealed class InProcessRunnerContext : IRunnerContext
|
||||
return default;
|
||||
}
|
||||
|
||||
ValueTask ISuperStepJoinContext.ForwardWorkflowEventAsync(WorkflowEvent workflowEvent, CancellationToken cancellation)
|
||||
=> this.AddEventAsync(workflowEvent);
|
||||
ValueTask ISuperStepJoinContext.ForwardWorkflowEventAsync(WorkflowEvent workflowEvent, CancellationToken cancellationToken)
|
||||
=> this.AddEventAsync(workflowEvent, cancellationToken);
|
||||
|
||||
ValueTask ISuperStepJoinContext.SendMessageAsync<TMessage>(string senderId, [DisallowNull] TMessage message, CancellationToken cancellation)
|
||||
=> this.SendMessageAsync(senderId, Throw.IfNull(message));
|
||||
ValueTask ISuperStepJoinContext.SendMessageAsync<TMessage>(string senderId, [DisallowNull] TMessage message, CancellationToken cancellationToken)
|
||||
=> this.SendMessageAsync(senderId, Throw.IfNull(message), cancellationToken: cancellationToken);
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user