mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
.NET Workflows - Update structure of samples (#645)
* Updated * Typos * Update readme fwiw
This commit is contained in:
@@ -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
|
||||
```
|
||||
+8
@@ -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. |
|
||||
|
||||
Reference in New Issue
Block a user