.NET Workflows - Update structure of samples (#645)

* Updated

* Typos

* Update readme fwiw
This commit is contained in:
Chris
2025-09-08 16:16:29 -07:00
committed by GitHub
Unverified
parent 726eb66982
commit 240edb00cd
29 changed files with 451 additions and 19 deletions
+19 -19
View File
@@ -48,29 +48,21 @@
<Folder Name="/Samples/GettingStarted/Telemetry/">
<Project Path="samples/GettingStarted/AgentOpenTelemetry/AgentOpenTelemetry.csproj" />
</Folder>
<Folder Name="/Samples/GettingStarted/Workflow/">
<File Path="samples/GettingStarted/Workflow/README.md" />
<Folder Name="/Samples/GettingStarted/Workflows/">
<File Path="samples/GettingStarted/Workflows/README.md" />
</Folder>
<Folder Name="/Samples/GettingStarted/Workflow/Concurrent/">
<Project Path="samples/GettingStarted/Workflow/Concurrent/Concurrent.csproj" />
<Folder Name="/Samples/GettingStarted/Workflows/Concurrent/">
<Project Path="samples/GettingStarted/Workflows/Concurrent/Concurrent.csproj" />
</Folder>
<Folder Name="/Samples/GettingStarted/Workflow/ConditionalEdges/">
<Project Path="samples/GettingStarted/Workflow/ConditionalEdges/01_EdgeCondition/01_EdgeCondition.csproj" />
<Project Path="samples/GettingStarted/Workflow/ConditionalEdges/02_SwitchCase/02_SwitchCase.csproj" />
<Project Path="samples/GettingStarted/Workflow/ConditionalEdges/03_MultiSelection/03_MultiSelection.csproj" />
<Folder Name="/Samples/GettingStarted/Workflows/ConditionalEdges/">
<Project Path="samples/GettingStarted/Workflows/ConditionalEdges/01_EdgeCondition/01_EdgeCondition.csproj" />
<Project Path="samples/GettingStarted/Workflows/ConditionalEdges/02_SwitchCase/02_SwitchCase.csproj" />
<Project Path="samples/GettingStarted/Workflows/ConditionalEdges/03_MultiSelection/03_MultiSelection.csproj" />
</Folder>
<Folder Name="/Samples/GettingStarted/Workflow/Foundational/">
<Project Path="samples/GettingStarted/Workflow/Foundational/01_ExecutorsAndEdges/01_ExecutorsAndEdges.csproj" />
<Project Path="samples/GettingStarted/Workflow/Foundational/02_Streaming/02_Streaming.csproj" />
<Project Path="samples/GettingStarted/Workflow/Foundational/03_AgentsInWorkflows/03_AgentsInWorkflows.csproj" />
<Folder Name="/Samples/GettingStarted/Workflows/Declarative/">
<Project Path="samples/GettingStarted/Workflows/Declarative/DeclarativeWorkflow.csproj" />
</Folder>
<Folder Name="/Samples/GettingStarted/Workflow/SharedStates/">
<Project Path="samples/GettingStarted/Workflow/SharedStates/SharedStates.csproj" />
</Folder>
<Folder Name="/Samples/DeclarativeWorkflow/">
<Project Path="samples/DeclarativeWorkflow/DeclarativeWorkflow.csproj" />
</Folder>
<Folder Name="/Samples/DeclarativeWorkflow/Example/">
<Folder Name="/Samples/GettingStarted/Workflows/Declarative/Examples/">
<File Path="../workflows/DeepResearch.yaml" />
<File Path="../workflows/HelloWorld.yaml" />
<File Path="../workflows/MathChat.yaml" />
@@ -78,6 +70,14 @@
<File Path="../workflows/README.md" />
<File Path="../workflows/wttr.json" />
</Folder>
<Folder Name="/Samples/GettingStarted/Workflows/Foundational/">
<Project Path="samples/GettingStarted/Workflows/Foundational/01_ExecutorsAndEdges/01_ExecutorsAndEdges.csproj" />
<Project Path="samples/GettingStarted/Workflows/Foundational/02_Streaming/02_Streaming.csproj" />
<Project Path="samples/GettingStarted/Workflows/Foundational/03_AgentsInWorkflows/03_AgentsInWorkflows.csproj" />
</Folder>
<Folder Name="/Samples/GettingStarted/Workflows/SharedStates/">
<Project Path="samples/GettingStarted/Workflows/SharedStates/SharedStates.csproj" />
</Folder>
<Folder Name="/Samples/SemanticKernelMigration/" />
<Folder Name="/Samples/SemanticKernelMigration/AzureAIFoundry/">
<Project Path="samples/SemanticKernelMigration/AzureAIFoundry/Step01_Basics/AzureAIFoundry_Step01_Basics.csproj" />
@@ -0,0 +1,33 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net9.0</TargetFrameworks>
<ProjectsDebugTargetFrameworks>net9.0</ProjectsDebugTargetFrameworks>
<TargetFrameworks Condition="'$(Configuration)' == 'Debug'">$(ProjectsDebugTargetFrameworks)</TargetFrameworks>
<Nullable>enable</Nullable>
<ImplicitUsings>disable</ImplicitUsings>
<UserSecretsId>5ee045b0-aea3-4f08-8d31-32d1a6f8fed0</UserSecretsId>
<NoWarn>$(NoWarn);CA1812</NoWarn>
</PropertyGroup>
<PropertyGroup>
<InjectSharedThrow>true</InjectSharedThrow>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" />
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
<PackageReference Include="Microsoft.Extensions.Logging" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.Workflows\Microsoft.Agents.Workflows.csproj" />
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.Workflows.Declarative\Microsoft.Agents.Workflows.Declarative.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,304 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using Azure.AI.Agents.Persistent;
using Azure.Identity;
using Microsoft.Agents.Workflows;
using Microsoft.Agents.Workflows.Declarative;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Configuration;
namespace Demo.DeclarativeWorkflow;
/// <summary>
/// HOW TO: Create a workflow from a declarative (yaml based) definition.
/// </summary>
/// <remarks>
/// <b>Configuration</b>
/// Define FOUNDRY_PROJECT_ENDPOINT as a user-secret or environment variable that
/// points to your Foundry project endpoint.
/// <b>Usage</b>
/// Provide the path to the workflow definition file as the first argument.
/// All other arguments are intepreted as a queue of inputs.
/// When no input is queued, interactive input is requested from the console.
/// </remarks>
internal sealed class Program
{
public static async Task Main(string[] args)
{
Program program = new(args);
await program.ExecuteAsync();
}
private async Task ExecuteAsync()
{
// Read and parse the declarative workflow.
Notify($"WORKFLOW: Parsing {Path.GetFullPath(this.WorkflowFile)}");
Stopwatch timer = Stopwatch.StartNew();
// Use DeclarativeWorkflowBuilder to build a workflow based on a YAML file.
DeclarativeWorkflowOptions options =
new(new FoundryAgentProvider(this.FoundryEndpoint, new AzureCliCredential()))
{
Configuration = this.Configuration
};
Workflow<string> workflow = DeclarativeWorkflowBuilder.Build<string>(this.WorkflowFile, options);
Notify($"\nWORKFLOW: Defined {timer.Elapsed}");
Notify("\nWORKFLOW: Starting...");
// Run the workflow, just like any other workflow
string input = this.GetWorkflowInput();
StreamingRun run = await InProcessExecution.StreamAsync(workflow, input);
await this.MonitorWorkflowRunAsync(run);
Notify("\nWORKFLOW: Done!");
}
private const string DefaultWorkflow = "HelloWorld.yaml";
private const string ConfigKeyFoundryEndpoint = "FOUNDRY_PROJECT_ENDPOINT";
private static readonly Dictionary<string, string> s_nameCache = [];
private static readonly HashSet<string> s_fileCache = [];
private string WorkflowFile { get; }
private string? WorkflowInput { get; }
private string FoundryEndpoint { get; }
private PersistentAgentsClient FoundryClient { get; }
private IConfiguration Configuration { get; }
private Program(string[] args)
{
this.WorkflowFile = ParseWorkflowFile(args);
this.WorkflowInput = ParseWorkflowInput(args);
this.Configuration = InitializeConfig();
this.FoundryEndpoint = this.Configuration[ConfigKeyFoundryEndpoint] ?? throw new InvalidOperationException($"Undefined configuration setting: {ConfigKeyFoundryEndpoint}");
this.FoundryClient = new PersistentAgentsClient(this.FoundryEndpoint, new AzureCliCredential());
}
private async Task MonitorWorkflowRunAsync(StreamingRun run)
{
string? messageId = null;
await foreach (WorkflowEvent evt in run.WatchStreamAsync().ConfigureAwait(false))
{
if (evt is ExecutorInvokeEvent executorInvoked)
{
Debug.WriteLine($"STEP ENTER #{executorInvoked.ExecutorId}");
}
else if (evt is ExecutorCompleteEvent executorComplete)
{
Debug.WriteLine($"STEP EXIT #{executorComplete.ExecutorId}");
}
else if (evt is ExecutorFailureEvent executorFailure)
{
Debug.WriteLine($"STEP ERROR #{executorFailure.ExecutorId}: {executorFailure.Data?.Message ?? "Unknown"}");
}
else if (evt is ConversationUpdateEvent invokeEvent)
{
Debug.WriteLine($"CONVERSATION: {invokeEvent.Data}");
}
else if (evt is AgentRunUpdateEvent streamEvent)
{
if (!string.Equals(messageId, streamEvent.Update.MessageId, StringComparison.Ordinal))
{
messageId = streamEvent.Update.MessageId;
if (messageId is not null)
{
string? agentId = streamEvent.Update.AuthorName;
if (agentId is not null)
{
if (!s_nameCache.TryGetValue(agentId, out string? realName))
{
PersistentAgent agent = await this.FoundryClient.Administration.GetAgentAsync(agentId);
s_nameCache[agentId] = agent.Name;
realName = agent.Name;
}
agentId = realName;
}
agentId ??= nameof(ChatRole.Assistant);
Console.ForegroundColor = ConsoleColor.Cyan;
Console.Write($"\n{agentId.ToUpperInvariant()}:");
Console.ForegroundColor = ConsoleColor.DarkGray;
Console.WriteLine($" [{messageId}]");
}
}
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)
{
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();
}
}
}
}
private static string ParseWorkflowFile(string[] args)
{
string workflowFile = args.FirstOrDefault() ?? DefaultWorkflow;
if (!File.Exists(workflowFile) && !Path.IsPathFullyQualified(workflowFile))
{
string? repoFolder = GetRepoFolder();
if (repoFolder is not null)
{
workflowFile = Path.Combine(repoFolder, "Workflows", workflowFile);
workflowFile = Path.ChangeExtension(workflowFile, ".yaml");
}
}
if (!File.Exists(workflowFile))
{
throw new InvalidOperationException($"Unable to locate workflow: {Path.GetFullPath(workflowFile)}.");
}
return workflowFile;
static string? GetRepoFolder()
{
DirectoryInfo? current = new(Directory.GetCurrentDirectory());
while (current is not null)
{
if (Directory.Exists(Path.Combine(current.FullName, ".git")))
{
return current.FullName;
}
current = current.Parent;
}
return null;
}
}
private string GetWorkflowInput()
{
string? input = this.WorkflowInput;
try
{
Console.ForegroundColor = ConsoleColor.DarkGreen;
Console.Write("\nINPUT: ");
Console.ForegroundColor = ConsoleColor.White;
if (!string.IsNullOrWhiteSpace(input))
{
Console.WriteLine(input);
return input;
}
while (string.IsNullOrWhiteSpace(input))
{
input = Console.ReadLine();
}
return input.Trim();
}
finally
{
Console.ResetColor();
}
}
private static string? ParseWorkflowInput(string[] args)
{
if (args.Length == 0)
{
return null;
}
string[] workflowInput = [.. args.Skip(1)];
return workflowInput.FirstOrDefault();
}
// Load configuration from user-secrets
private static IConfigurationRoot InitializeConfig() =>
new ConfigurationBuilder()
.AddUserSecrets(Assembly.GetExecutingAssembly())
.AddEnvironmentVariables()
.Build();
private static void Notify(string message)
{
Console.ForegroundColor = ConsoleColor.Cyan;
try
{
Console.WriteLine(message);
}
finally
{
Console.ResetColor();
}
}
private static async ValueTask DownloadFileContentAsync(string filename, BinaryData content)
{
string filePath = Path.Combine(Path.GetTempPath(), Path.GetFileName(filename));
filePath = Path.ChangeExtension(filePath, ".png");
await File.WriteAllBytesAsync(filePath, content.ToArray());
Process.Start(
new ProcessStartInfo
{
FileName = "cmd.exe",
Arguments = $"/C start {filePath}"
});
}
}
@@ -0,0 +1,87 @@
# Summary
This demo showcases the ability to parse a declarative Foundry Workflow file (YAML) to build a `Workflow<>`
be executed using the same pattern as any code-based workflow.
## Configuration
This demo requires configuration to access agents an [Azure Foundry Project](https://learn.microsoft.com/azure/ai-foundry).
#### Settings
We suggest using .NET [Secret Manager](https://learn.microsoft.com/en-us/aspnet/core/security/app-secrets)
to avoid the risk of leaking secrets into the repository, branches and pull requests.
You can also use environment variables if you prefer.
To set your secrets as an environment variable (PowerShell):
```pwsh
$env:FOUNDRY_PROJECT_ENDPOINT="https://..."
```
To set your secrets with .NET Secret Manager:
1. From the root of the repository, navigate the console to the project folder:
```
cd dotnet/samples/GettingStarted/Workflows/Declarative
```
2. Examine existing secret definitions:
```
dotnet user-secrets list
```
3. If needed, perform first time initialization:
```
dotnet user-secrets init
```
4. Define setting that identifies your Azure Foundry Project (endpoint):
```
dotnet user-secrets set "FOUNDRY_PROJECT_ENDPOINT" "https://..."
```
#### Authorization
Use [_Azure CLI_](https://learn.microsoft.com/cli/azure/authenticate-azure-cli) to authorize access to your Azure Foundry Project:
```
az login
az account get-access-token
```
#### Agents
The sample workflows rely on agents defined in your Azure Foundry Project.
To create agents, run the [`Create.ps1`](../../../../../workflows/) script.
This will create the agents used in the sample workflows in your Azure Foundry Project and format a script you can copy and use to configure your environment.
> Note: `Create.ps1` relies upon the `FOUNDRY_PROJECT_ENDPOINT` setting.
## Execution
Run the demo from the console by specifying a path to a declarative (YAML) workflow file.
The repository has example workflows available in the root [`/workflows`](../../../../../workflows) folder.
1. From the root of the repository, navigate the console to the project folder:
```sh
cd dotnet/samples/GettingStarted/Workflows/Declarative
```
2. Run the demo referencing a sample workflow by name:
```sh
dotnet run HelloWorld
```
3. Run the demo with a path to any workflow file:
```sh
dotnet run c:/myworkflows/HelloWorld.yaml
```
@@ -39,3 +39,11 @@ Once completed, please proceed to other samples listed below.
| [Multi-Selection Routing](./ConditionalEdges/03_MultiSelection) | Demonstrates multi-selection routing where one executor can trigger multiple downstream executors |
> These 3 samples build upon each other. It's recommended to explore them in sequence to fully grasp the concepts.
### Declarative Workflows
| Sample | Concepts |
|--------|----------|
| [DeclarativeWorkflow](./DeclarativeWorkflow) | Demonstrates execution of declartive workflows. |