diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx index 8d550c3031..e764f4c936 100644 --- a/dotnet/agent-framework-dotnet.slnx +++ b/dotnet/agent-framework-dotnet.slnx @@ -48,29 +48,21 @@ - - + + - - + + - - - - + + + + - - - - + + - - - - - - - + @@ -78,6 +70,14 @@ + + + + + + + + diff --git a/dotnet/samples/GettingStarted/Workflow/Concurrent/Concurrent.csproj b/dotnet/samples/GettingStarted/Workflows/Concurrent/Concurrent.csproj similarity index 100% rename from dotnet/samples/GettingStarted/Workflow/Concurrent/Concurrent.csproj rename to dotnet/samples/GettingStarted/Workflows/Concurrent/Concurrent.csproj diff --git a/dotnet/samples/GettingStarted/Workflow/Concurrent/Program.cs b/dotnet/samples/GettingStarted/Workflows/Concurrent/Program.cs similarity index 100% rename from dotnet/samples/GettingStarted/Workflow/Concurrent/Program.cs rename to dotnet/samples/GettingStarted/Workflows/Concurrent/Program.cs diff --git a/dotnet/samples/GettingStarted/Workflow/ConditionalEdges/01_EdgeCondition/01_EdgeCondition.csproj b/dotnet/samples/GettingStarted/Workflows/ConditionalEdges/01_EdgeCondition/01_EdgeCondition.csproj similarity index 100% rename from dotnet/samples/GettingStarted/Workflow/ConditionalEdges/01_EdgeCondition/01_EdgeCondition.csproj rename to dotnet/samples/GettingStarted/Workflows/ConditionalEdges/01_EdgeCondition/01_EdgeCondition.csproj diff --git a/dotnet/samples/GettingStarted/Workflow/ConditionalEdges/01_EdgeCondition/Program.cs b/dotnet/samples/GettingStarted/Workflows/ConditionalEdges/01_EdgeCondition/Program.cs similarity index 100% rename from dotnet/samples/GettingStarted/Workflow/ConditionalEdges/01_EdgeCondition/Program.cs rename to dotnet/samples/GettingStarted/Workflows/ConditionalEdges/01_EdgeCondition/Program.cs diff --git a/dotnet/samples/GettingStarted/Workflow/ConditionalEdges/01_EdgeCondition/Resources.cs b/dotnet/samples/GettingStarted/Workflows/ConditionalEdges/01_EdgeCondition/Resources.cs similarity index 100% rename from dotnet/samples/GettingStarted/Workflow/ConditionalEdges/01_EdgeCondition/Resources.cs rename to dotnet/samples/GettingStarted/Workflows/ConditionalEdges/01_EdgeCondition/Resources.cs diff --git a/dotnet/samples/GettingStarted/Workflow/ConditionalEdges/02_SwitchCase/02_SwitchCase.csproj b/dotnet/samples/GettingStarted/Workflows/ConditionalEdges/02_SwitchCase/02_SwitchCase.csproj similarity index 100% rename from dotnet/samples/GettingStarted/Workflow/ConditionalEdges/02_SwitchCase/02_SwitchCase.csproj rename to dotnet/samples/GettingStarted/Workflows/ConditionalEdges/02_SwitchCase/02_SwitchCase.csproj diff --git a/dotnet/samples/GettingStarted/Workflow/ConditionalEdges/02_SwitchCase/Program.cs b/dotnet/samples/GettingStarted/Workflows/ConditionalEdges/02_SwitchCase/Program.cs similarity index 100% rename from dotnet/samples/GettingStarted/Workflow/ConditionalEdges/02_SwitchCase/Program.cs rename to dotnet/samples/GettingStarted/Workflows/ConditionalEdges/02_SwitchCase/Program.cs diff --git a/dotnet/samples/GettingStarted/Workflow/ConditionalEdges/02_SwitchCase/Resources.cs b/dotnet/samples/GettingStarted/Workflows/ConditionalEdges/02_SwitchCase/Resources.cs similarity index 100% rename from dotnet/samples/GettingStarted/Workflow/ConditionalEdges/02_SwitchCase/Resources.cs rename to dotnet/samples/GettingStarted/Workflows/ConditionalEdges/02_SwitchCase/Resources.cs diff --git a/dotnet/samples/GettingStarted/Workflow/ConditionalEdges/03_MultiSelection/03_MultiSelection.csproj b/dotnet/samples/GettingStarted/Workflows/ConditionalEdges/03_MultiSelection/03_MultiSelection.csproj similarity index 100% rename from dotnet/samples/GettingStarted/Workflow/ConditionalEdges/03_MultiSelection/03_MultiSelection.csproj rename to dotnet/samples/GettingStarted/Workflows/ConditionalEdges/03_MultiSelection/03_MultiSelection.csproj diff --git a/dotnet/samples/GettingStarted/Workflow/ConditionalEdges/03_MultiSelection/Program.cs b/dotnet/samples/GettingStarted/Workflows/ConditionalEdges/03_MultiSelection/Program.cs similarity index 100% rename from dotnet/samples/GettingStarted/Workflow/ConditionalEdges/03_MultiSelection/Program.cs rename to dotnet/samples/GettingStarted/Workflows/ConditionalEdges/03_MultiSelection/Program.cs diff --git a/dotnet/samples/GettingStarted/Workflow/ConditionalEdges/03_MultiSelection/Resources.cs b/dotnet/samples/GettingStarted/Workflows/ConditionalEdges/03_MultiSelection/Resources.cs similarity index 100% rename from dotnet/samples/GettingStarted/Workflow/ConditionalEdges/03_MultiSelection/Resources.cs rename to dotnet/samples/GettingStarted/Workflows/ConditionalEdges/03_MultiSelection/Resources.cs diff --git a/dotnet/samples/GettingStarted/Workflows/Declarative/DeclarativeWorkflow.csproj b/dotnet/samples/GettingStarted/Workflows/Declarative/DeclarativeWorkflow.csproj new file mode 100644 index 0000000000..96a9d5df1d --- /dev/null +++ b/dotnet/samples/GettingStarted/Workflows/Declarative/DeclarativeWorkflow.csproj @@ -0,0 +1,33 @@ + + + + Exe + net9.0 + net9.0 + $(ProjectsDebugTargetFrameworks) + enable + disable + 5ee045b0-aea3-4f08-8d31-32d1a6f8fed0 + $(NoWarn);CA1812 + + + + true + + + + + + + + + + + + + + + + + + diff --git a/dotnet/samples/GettingStarted/Workflows/Declarative/Program.cs b/dotnet/samples/GettingStarted/Workflows/Declarative/Program.cs new file mode 100644 index 0000000000..d2301c0311 --- /dev/null +++ b/dotnet/samples/GettingStarted/Workflows/Declarative/Program.cs @@ -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; + +/// +/// HOW TO: Create a workflow from a declarative (yaml based) definition. +/// +/// +/// Configuration +/// Define FOUNDRY_PROJECT_ENDPOINT as a user-secret or environment variable that +/// points to your Foundry project endpoint. +/// Usage +/// 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. +/// +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 workflow = DeclarativeWorkflowBuilder.Build(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 s_nameCache = []; + private static readonly HashSet 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}" + }); + } +} diff --git a/dotnet/samples/GettingStarted/Workflows/Declarative/README.md b/dotnet/samples/GettingStarted/Workflows/Declarative/README.md new file mode 100644 index 0000000000..57101c9b21 --- /dev/null +++ b/dotnet/samples/GettingStarted/Workflows/Declarative/README.md @@ -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 + ``` diff --git a/dotnet/samples/GettingStarted/Workflow/Foundational/01_ExecutorsAndEdges/01_ExecutorsAndEdges.csproj b/dotnet/samples/GettingStarted/Workflows/Foundational/01_ExecutorsAndEdges/01_ExecutorsAndEdges.csproj similarity index 100% rename from dotnet/samples/GettingStarted/Workflow/Foundational/01_ExecutorsAndEdges/01_ExecutorsAndEdges.csproj rename to dotnet/samples/GettingStarted/Workflows/Foundational/01_ExecutorsAndEdges/01_ExecutorsAndEdges.csproj diff --git a/dotnet/samples/GettingStarted/Workflow/Foundational/01_ExecutorsAndEdges/Program.cs b/dotnet/samples/GettingStarted/Workflows/Foundational/01_ExecutorsAndEdges/Program.cs similarity index 100% rename from dotnet/samples/GettingStarted/Workflow/Foundational/01_ExecutorsAndEdges/Program.cs rename to dotnet/samples/GettingStarted/Workflows/Foundational/01_ExecutorsAndEdges/Program.cs diff --git a/dotnet/samples/GettingStarted/Workflow/Foundational/02_Streaming/02_Streaming.csproj b/dotnet/samples/GettingStarted/Workflows/Foundational/02_Streaming/02_Streaming.csproj similarity index 100% rename from dotnet/samples/GettingStarted/Workflow/Foundational/02_Streaming/02_Streaming.csproj rename to dotnet/samples/GettingStarted/Workflows/Foundational/02_Streaming/02_Streaming.csproj diff --git a/dotnet/samples/GettingStarted/Workflow/Foundational/02_Streaming/Program.cs b/dotnet/samples/GettingStarted/Workflows/Foundational/02_Streaming/Program.cs similarity index 100% rename from dotnet/samples/GettingStarted/Workflow/Foundational/02_Streaming/Program.cs rename to dotnet/samples/GettingStarted/Workflows/Foundational/02_Streaming/Program.cs diff --git a/dotnet/samples/GettingStarted/Workflow/Foundational/03_AgentsInWorkflows/03_AgentsInWorkflows.csproj b/dotnet/samples/GettingStarted/Workflows/Foundational/03_AgentsInWorkflows/03_AgentsInWorkflows.csproj similarity index 100% rename from dotnet/samples/GettingStarted/Workflow/Foundational/03_AgentsInWorkflows/03_AgentsInWorkflows.csproj rename to dotnet/samples/GettingStarted/Workflows/Foundational/03_AgentsInWorkflows/03_AgentsInWorkflows.csproj diff --git a/dotnet/samples/GettingStarted/Workflow/Foundational/03_AgentsInWorkflows/Program.cs b/dotnet/samples/GettingStarted/Workflows/Foundational/03_AgentsInWorkflows/Program.cs similarity index 100% rename from dotnet/samples/GettingStarted/Workflow/Foundational/03_AgentsInWorkflows/Program.cs rename to dotnet/samples/GettingStarted/Workflows/Foundational/03_AgentsInWorkflows/Program.cs diff --git a/dotnet/samples/GettingStarted/Workflow/README.md b/dotnet/samples/GettingStarted/Workflows/README.md similarity index 92% rename from dotnet/samples/GettingStarted/Workflow/README.md rename to dotnet/samples/GettingStarted/Workflows/README.md index 05476e7b8b..13dda87e01 100644 --- a/dotnet/samples/GettingStarted/Workflow/README.md +++ b/dotnet/samples/GettingStarted/Workflows/README.md @@ -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. | + diff --git a/dotnet/samples/GettingStarted/Workflow/Resources/Lorem_Ipsum.txt b/dotnet/samples/GettingStarted/Workflows/Resources/Lorem_Ipsum.txt similarity index 100% rename from dotnet/samples/GettingStarted/Workflow/Resources/Lorem_Ipsum.txt rename to dotnet/samples/GettingStarted/Workflows/Resources/Lorem_Ipsum.txt diff --git a/dotnet/samples/GettingStarted/Workflow/Resources/ambiguous_email.txt b/dotnet/samples/GettingStarted/Workflows/Resources/ambiguous_email.txt similarity index 100% rename from dotnet/samples/GettingStarted/Workflow/Resources/ambiguous_email.txt rename to dotnet/samples/GettingStarted/Workflows/Resources/ambiguous_email.txt diff --git a/dotnet/samples/GettingStarted/Workflow/Resources/email.txt b/dotnet/samples/GettingStarted/Workflows/Resources/email.txt similarity index 100% rename from dotnet/samples/GettingStarted/Workflow/Resources/email.txt rename to dotnet/samples/GettingStarted/Workflows/Resources/email.txt diff --git a/dotnet/samples/GettingStarted/Workflow/Resources/spam.txt b/dotnet/samples/GettingStarted/Workflows/Resources/spam.txt similarity index 100% rename from dotnet/samples/GettingStarted/Workflow/Resources/spam.txt rename to dotnet/samples/GettingStarted/Workflows/Resources/spam.txt diff --git a/dotnet/samples/GettingStarted/Workflow/SharedStates/Program.cs b/dotnet/samples/GettingStarted/Workflows/SharedStates/Program.cs similarity index 100% rename from dotnet/samples/GettingStarted/Workflow/SharedStates/Program.cs rename to dotnet/samples/GettingStarted/Workflows/SharedStates/Program.cs diff --git a/dotnet/samples/GettingStarted/Workflow/SharedStates/Resources.cs b/dotnet/samples/GettingStarted/Workflows/SharedStates/Resources.cs similarity index 100% rename from dotnet/samples/GettingStarted/Workflow/SharedStates/Resources.cs rename to dotnet/samples/GettingStarted/Workflows/SharedStates/Resources.cs diff --git a/dotnet/samples/GettingStarted/Workflow/SharedStates/SharedStates.csproj b/dotnet/samples/GettingStarted/Workflows/SharedStates/SharedStates.csproj similarity index 100% rename from dotnet/samples/GettingStarted/Workflow/SharedStates/SharedStates.csproj rename to dotnet/samples/GettingStarted/Workflows/SharedStates/SharedStates.csproj