From 91fbdcf8a2116ac15333081f14a8955bd2e61bd3 Mon Sep 17 00:00:00 2001 From: Daniel Gerlag Date: Thu, 29 Jan 2026 16:16:13 +0000 Subject: [PATCH 1/3] azure foundry --- .gitignore | 2 + README.md | 1 + WorkflowCore.sln | 497 +++++++++++++++++ docs/azure-ai-foundry.md | 452 ++++++++++++++++ docs/extensions.md | 3 +- docs/samples.md | 7 + mkdocs.yml | 1 + .../WorkflowCore.AI.AzureFoundry/CHANGELOG.md | 92 ++++ .../Interface/IAgentLoopBuilder.cs | 63 +++ .../Interface/IAgentTool.cs | 35 ++ .../Interface/IChatCompletionBuilder.cs | 68 +++ .../Interface/IChatCompletionService.cs | 66 +++ .../Interface/IConversationStore.cs | 31 ++ .../Interface/IEmbeddingService.cs | 49 ++ .../Interface/IHumanReviewBuilder.cs | 54 ++ .../Interface/ISearchService.cs | 43 ++ .../Interface/IToolRegistry.cs | 41 ++ .../Models/AzureFoundryOptions.cs | 58 ++ .../Models/ChatCompletionResult.cs | 38 ++ .../Models/ConversationMessage.cs | 88 +++ .../Models/ConversationThread.cs | 115 ++++ .../Models/ReviewAction.cs | 61 +++ .../Models/SearchResult.cs | 56 ++ .../Models/ToolDefinition.cs | 36 ++ .../Models/ToolResult.cs | 75 +++ .../Primitives/AgentLoop.cs | 209 ++++++++ .../Primitives/AgentLoopStep.cs | 13 + .../Primitives/ChatCompletion.cs | 149 ++++++ .../Primitives/ChatCompletionStep.cs | 13 + .../Primitives/ExecuteTool.cs | 97 ++++ .../Primitives/ExecuteToolStep.cs | 13 + .../Primitives/GenerateEmbedding.cs | 67 +++ .../Primitives/GenerateEmbeddingStep.cs | 13 + .../Primitives/HumanReview.cs | 138 +++++ .../Primitives/HumanReviewStep.cs | 13 + .../Primitives/VectorSearch.cs | 98 ++++ .../Primitives/VectorSearchStep.cs | 13 + .../Properties/AssemblyInfo.cs | 3 + .../ServiceCollectionExtensions.cs | 69 +++ .../StepBuilderExtensions.cs | 114 ++++ .../Services/AgentLoopBuilder.cs | 87 +++ .../Services/AzureFoundryClientFactory.cs | 69 +++ .../Services/ChatCompletionBuilder.cs | 87 +++ .../Services/ChatCompletionService.cs | 156 ++++++ .../Services/EmbeddingService.cs | 68 +++ .../Services/HumanReviewBuilder.cs | 64 +++ .../Services/InMemoryConversationStore.cs | 77 +++ .../Services/SearchService.cs | 133 +++++ .../Services/ToolRegistry.cs | 83 +++ .../WorkflowCore.AI.AzureFoundry.csproj | 35 ++ .../WorkflowCore.AI.AzureFoundry/readme.md | 500 ++++++++++++++++++ .../.env.example | 25 + .../.gitignore | 1 + .../Program.cs | 238 +++++++++ .../README.md | 259 +++++++++ .../Tools/CalculatorTool.cs | 69 +++ .../Tools/WeatherTool.cs | 71 +++ .../WorkflowCore.Sample.AzureFoundry.csproj | 39 ++ .../Workflows/AgentWithToolsWorkflow.cs | 37 ++ .../Workflows/HumanReviewWorkflow.cs | 40 ++ .../Workflows/SimpleChatWorkflow.cs | 29 + .../Workflows/WorkflowData.cs | 47 ++ .../ConversationThreadTests.cs | 110 ++++ .../InMemoryConversationStoreTests.cs | 87 +++ .../ToolRegistryTests.cs | 97 ++++ .../WorkflowCore.AI.AzureFoundry.Tests.csproj | 21 + 66 files changed, 5582 insertions(+), 1 deletion(-) create mode 100644 docs/azure-ai-foundry.md create mode 100644 src/extensions/WorkflowCore.AI.AzureFoundry/CHANGELOG.md create mode 100644 src/extensions/WorkflowCore.AI.AzureFoundry/Interface/IAgentLoopBuilder.cs create mode 100644 src/extensions/WorkflowCore.AI.AzureFoundry/Interface/IAgentTool.cs create mode 100644 src/extensions/WorkflowCore.AI.AzureFoundry/Interface/IChatCompletionBuilder.cs create mode 100644 src/extensions/WorkflowCore.AI.AzureFoundry/Interface/IChatCompletionService.cs create mode 100644 src/extensions/WorkflowCore.AI.AzureFoundry/Interface/IConversationStore.cs create mode 100644 src/extensions/WorkflowCore.AI.AzureFoundry/Interface/IEmbeddingService.cs create mode 100644 src/extensions/WorkflowCore.AI.AzureFoundry/Interface/IHumanReviewBuilder.cs create mode 100644 src/extensions/WorkflowCore.AI.AzureFoundry/Interface/ISearchService.cs create mode 100644 src/extensions/WorkflowCore.AI.AzureFoundry/Interface/IToolRegistry.cs create mode 100644 src/extensions/WorkflowCore.AI.AzureFoundry/Models/AzureFoundryOptions.cs create mode 100644 src/extensions/WorkflowCore.AI.AzureFoundry/Models/ChatCompletionResult.cs create mode 100644 src/extensions/WorkflowCore.AI.AzureFoundry/Models/ConversationMessage.cs create mode 100644 src/extensions/WorkflowCore.AI.AzureFoundry/Models/ConversationThread.cs create mode 100644 src/extensions/WorkflowCore.AI.AzureFoundry/Models/ReviewAction.cs create mode 100644 src/extensions/WorkflowCore.AI.AzureFoundry/Models/SearchResult.cs create mode 100644 src/extensions/WorkflowCore.AI.AzureFoundry/Models/ToolDefinition.cs create mode 100644 src/extensions/WorkflowCore.AI.AzureFoundry/Models/ToolResult.cs create mode 100644 src/extensions/WorkflowCore.AI.AzureFoundry/Primitives/AgentLoop.cs create mode 100644 src/extensions/WorkflowCore.AI.AzureFoundry/Primitives/AgentLoopStep.cs create mode 100644 src/extensions/WorkflowCore.AI.AzureFoundry/Primitives/ChatCompletion.cs create mode 100644 src/extensions/WorkflowCore.AI.AzureFoundry/Primitives/ChatCompletionStep.cs create mode 100644 src/extensions/WorkflowCore.AI.AzureFoundry/Primitives/ExecuteTool.cs create mode 100644 src/extensions/WorkflowCore.AI.AzureFoundry/Primitives/ExecuteToolStep.cs create mode 100644 src/extensions/WorkflowCore.AI.AzureFoundry/Primitives/GenerateEmbedding.cs create mode 100644 src/extensions/WorkflowCore.AI.AzureFoundry/Primitives/GenerateEmbeddingStep.cs create mode 100644 src/extensions/WorkflowCore.AI.AzureFoundry/Primitives/HumanReview.cs create mode 100644 src/extensions/WorkflowCore.AI.AzureFoundry/Primitives/HumanReviewStep.cs create mode 100644 src/extensions/WorkflowCore.AI.AzureFoundry/Primitives/VectorSearch.cs create mode 100644 src/extensions/WorkflowCore.AI.AzureFoundry/Primitives/VectorSearchStep.cs create mode 100644 src/extensions/WorkflowCore.AI.AzureFoundry/Properties/AssemblyInfo.cs create mode 100644 src/extensions/WorkflowCore.AI.AzureFoundry/ServiceExtensions/ServiceCollectionExtensions.cs create mode 100644 src/extensions/WorkflowCore.AI.AzureFoundry/ServiceExtensions/StepBuilderExtensions.cs create mode 100644 src/extensions/WorkflowCore.AI.AzureFoundry/Services/AgentLoopBuilder.cs create mode 100644 src/extensions/WorkflowCore.AI.AzureFoundry/Services/AzureFoundryClientFactory.cs create mode 100644 src/extensions/WorkflowCore.AI.AzureFoundry/Services/ChatCompletionBuilder.cs create mode 100644 src/extensions/WorkflowCore.AI.AzureFoundry/Services/ChatCompletionService.cs create mode 100644 src/extensions/WorkflowCore.AI.AzureFoundry/Services/EmbeddingService.cs create mode 100644 src/extensions/WorkflowCore.AI.AzureFoundry/Services/HumanReviewBuilder.cs create mode 100644 src/extensions/WorkflowCore.AI.AzureFoundry/Services/InMemoryConversationStore.cs create mode 100644 src/extensions/WorkflowCore.AI.AzureFoundry/Services/SearchService.cs create mode 100644 src/extensions/WorkflowCore.AI.AzureFoundry/Services/ToolRegistry.cs create mode 100644 src/extensions/WorkflowCore.AI.AzureFoundry/WorkflowCore.AI.AzureFoundry.csproj create mode 100644 src/extensions/WorkflowCore.AI.AzureFoundry/readme.md create mode 100644 src/samples/WorkflowCore.Sample.AzureFoundry/.env.example create mode 100644 src/samples/WorkflowCore.Sample.AzureFoundry/.gitignore create mode 100644 src/samples/WorkflowCore.Sample.AzureFoundry/Program.cs create mode 100644 src/samples/WorkflowCore.Sample.AzureFoundry/README.md create mode 100644 src/samples/WorkflowCore.Sample.AzureFoundry/Tools/CalculatorTool.cs create mode 100644 src/samples/WorkflowCore.Sample.AzureFoundry/Tools/WeatherTool.cs create mode 100644 src/samples/WorkflowCore.Sample.AzureFoundry/WorkflowCore.Sample.AzureFoundry.csproj create mode 100644 src/samples/WorkflowCore.Sample.AzureFoundry/Workflows/AgentWithToolsWorkflow.cs create mode 100644 src/samples/WorkflowCore.Sample.AzureFoundry/Workflows/HumanReviewWorkflow.cs create mode 100644 src/samples/WorkflowCore.Sample.AzureFoundry/Workflows/SimpleChatWorkflow.cs create mode 100644 src/samples/WorkflowCore.Sample.AzureFoundry/Workflows/WorkflowData.cs create mode 100644 test/WorkflowCore.AI.AzureFoundry.Tests/ConversationThreadTests.cs create mode 100644 test/WorkflowCore.AI.AzureFoundry.Tests/InMemoryConversationStoreTests.cs create mode 100644 test/WorkflowCore.AI.AzureFoundry.Tests/ToolRegistryTests.cs create mode 100644 test/WorkflowCore.AI.AzureFoundry.Tests/WorkflowCore.AI.AzureFoundry.Tests.csproj diff --git a/.gitignore b/.gitignore index 62aadfb80..3ff0a4429 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. +plans + # User-specific files *.suo *.user diff --git a/README.md b/README.md index ab398cc2c..c129fa840 100644 --- a/README.md +++ b/README.md @@ -150,6 +150,7 @@ These are also available as separate Nuget packages. ## Extensions +* [Azure AI Foundry](src/extensions/WorkflowCore.AI.AzureFoundry) * [User (human) workflows](src/extensions/WorkflowCore.Users) diff --git a/WorkflowCore.sln b/WorkflowCore.sln index 25c2016d2..685218c34 100644 --- a/WorkflowCore.sln +++ b/WorkflowCore.sln @@ -158,236 +158,730 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WorkflowCore.Persistence.Or EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WorkflowCore.Tests.Oracle", "test\WorkflowCore.Tests.Oracle\WorkflowCore.Tests.Oracle.csproj", "{A2837F1C-3740-4375-9069-81AE32C867CA}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WorkflowCore.AI.AzureFoundry", "src\extensions\WorkflowCore.AI.AzureFoundry\WorkflowCore.AI.AzureFoundry.csproj", "{A74FFAE5-9788-4C0D-9F77-AF6F6468A1A1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WorkflowCore.AI.AzureFoundry.Tests", "test\WorkflowCore.AI.AzureFoundry.Tests\WorkflowCore.AI.AzureFoundry.Tests.csproj", "{AAB9DDFF-0A0A-43BE-BF00-2BA13ED526C8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WorkflowCore.Sample.AzureFoundry", "src\samples\WorkflowCore.Sample.AzureFoundry\WorkflowCore.Sample.AzureFoundry.csproj", "{D75C8112-6A4D-4A13-BB79-2D23DF66E4CB}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {B7B2EA4D-E7F0-43E2-942A-3A5AA8F57272}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {B7B2EA4D-E7F0-43E2-942A-3A5AA8F57272}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B7B2EA4D-E7F0-43E2-942A-3A5AA8F57272}.Debug|x64.ActiveCfg = Debug|Any CPU + {B7B2EA4D-E7F0-43E2-942A-3A5AA8F57272}.Debug|x64.Build.0 = Debug|Any CPU + {B7B2EA4D-E7F0-43E2-942A-3A5AA8F57272}.Debug|x86.ActiveCfg = Debug|Any CPU + {B7B2EA4D-E7F0-43E2-942A-3A5AA8F57272}.Debug|x86.Build.0 = Debug|Any CPU {B7B2EA4D-E7F0-43E2-942A-3A5AA8F57272}.Release|Any CPU.ActiveCfg = Release|Any CPU {B7B2EA4D-E7F0-43E2-942A-3A5AA8F57272}.Release|Any CPU.Build.0 = Release|Any CPU + {B7B2EA4D-E7F0-43E2-942A-3A5AA8F57272}.Release|x64.ActiveCfg = Release|Any CPU + {B7B2EA4D-E7F0-43E2-942A-3A5AA8F57272}.Release|x64.Build.0 = Release|Any CPU + {B7B2EA4D-E7F0-43E2-942A-3A5AA8F57272}.Release|x86.ActiveCfg = Release|Any CPU + {B7B2EA4D-E7F0-43E2-942A-3A5AA8F57272}.Release|x86.Build.0 = Release|Any CPU {DD26E7B4-9D3A-4E1E-8585-862DB6DE21EB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {DD26E7B4-9D3A-4E1E-8585-862DB6DE21EB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DD26E7B4-9D3A-4E1E-8585-862DB6DE21EB}.Debug|x64.ActiveCfg = Debug|Any CPU + {DD26E7B4-9D3A-4E1E-8585-862DB6DE21EB}.Debug|x64.Build.0 = Debug|Any CPU + {DD26E7B4-9D3A-4E1E-8585-862DB6DE21EB}.Debug|x86.ActiveCfg = Debug|Any CPU + {DD26E7B4-9D3A-4E1E-8585-862DB6DE21EB}.Debug|x86.Build.0 = Debug|Any CPU {DD26E7B4-9D3A-4E1E-8585-862DB6DE21EB}.Release|Any CPU.ActiveCfg = Release|Any CPU {DD26E7B4-9D3A-4E1E-8585-862DB6DE21EB}.Release|Any CPU.Build.0 = Release|Any CPU + {DD26E7B4-9D3A-4E1E-8585-862DB6DE21EB}.Release|x64.ActiveCfg = Release|Any CPU + {DD26E7B4-9D3A-4E1E-8585-862DB6DE21EB}.Release|x64.Build.0 = Release|Any CPU + {DD26E7B4-9D3A-4E1E-8585-862DB6DE21EB}.Release|x86.ActiveCfg = Release|Any CPU + {DD26E7B4-9D3A-4E1E-8585-862DB6DE21EB}.Release|x86.Build.0 = Release|Any CPU {660FEDAB-D085-476B-9E16-73E42F66DB4F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {660FEDAB-D085-476B-9E16-73E42F66DB4F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {660FEDAB-D085-476B-9E16-73E42F66DB4F}.Debug|x64.ActiveCfg = Debug|Any CPU + {660FEDAB-D085-476B-9E16-73E42F66DB4F}.Debug|x64.Build.0 = Debug|Any CPU + {660FEDAB-D085-476B-9E16-73E42F66DB4F}.Debug|x86.ActiveCfg = Debug|Any CPU + {660FEDAB-D085-476B-9E16-73E42F66DB4F}.Debug|x86.Build.0 = Debug|Any CPU {660FEDAB-D085-476B-9E16-73E42F66DB4F}.Release|Any CPU.ActiveCfg = Release|Any CPU {660FEDAB-D085-476B-9E16-73E42F66DB4F}.Release|Any CPU.Build.0 = Release|Any CPU + {660FEDAB-D085-476B-9E16-73E42F66DB4F}.Release|x64.ActiveCfg = Release|Any CPU + {660FEDAB-D085-476B-9E16-73E42F66DB4F}.Release|x64.Build.0 = Release|Any CPU + {660FEDAB-D085-476B-9E16-73E42F66DB4F}.Release|x86.ActiveCfg = Release|Any CPU + {660FEDAB-D085-476B-9E16-73E42F66DB4F}.Release|x86.Build.0 = Release|Any CPU {BC6F28F1-9F47-4FFE-AD06-2B74CB89E76B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {BC6F28F1-9F47-4FFE-AD06-2B74CB89E76B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BC6F28F1-9F47-4FFE-AD06-2B74CB89E76B}.Debug|x64.ActiveCfg = Debug|Any CPU + {BC6F28F1-9F47-4FFE-AD06-2B74CB89E76B}.Debug|x64.Build.0 = Debug|Any CPU + {BC6F28F1-9F47-4FFE-AD06-2B74CB89E76B}.Debug|x86.ActiveCfg = Debug|Any CPU + {BC6F28F1-9F47-4FFE-AD06-2B74CB89E76B}.Debug|x86.Build.0 = Debug|Any CPU {BC6F28F1-9F47-4FFE-AD06-2B74CB89E76B}.Release|Any CPU.ActiveCfg = Release|Any CPU {BC6F28F1-9F47-4FFE-AD06-2B74CB89E76B}.Release|Any CPU.Build.0 = Release|Any CPU + {BC6F28F1-9F47-4FFE-AD06-2B74CB89E76B}.Release|x64.ActiveCfg = Release|Any CPU + {BC6F28F1-9F47-4FFE-AD06-2B74CB89E76B}.Release|x64.Build.0 = Release|Any CPU + {BC6F28F1-9F47-4FFE-AD06-2B74CB89E76B}.Release|x86.ActiveCfg = Release|Any CPU + {BC6F28F1-9F47-4FFE-AD06-2B74CB89E76B}.Release|x86.Build.0 = Release|Any CPU {FB738255-0304-4A25-B256-22E36EDF9507}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {FB738255-0304-4A25-B256-22E36EDF9507}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FB738255-0304-4A25-B256-22E36EDF9507}.Debug|x64.ActiveCfg = Debug|Any CPU + {FB738255-0304-4A25-B256-22E36EDF9507}.Debug|x64.Build.0 = Debug|Any CPU + {FB738255-0304-4A25-B256-22E36EDF9507}.Debug|x86.ActiveCfg = Debug|Any CPU + {FB738255-0304-4A25-B256-22E36EDF9507}.Debug|x86.Build.0 = Debug|Any CPU {FB738255-0304-4A25-B256-22E36EDF9507}.Release|Any CPU.ActiveCfg = Release|Any CPU {FB738255-0304-4A25-B256-22E36EDF9507}.Release|Any CPU.Build.0 = Release|Any CPU + {FB738255-0304-4A25-B256-22E36EDF9507}.Release|x64.ActiveCfg = Release|Any CPU + {FB738255-0304-4A25-B256-22E36EDF9507}.Release|x64.Build.0 = Release|Any CPU + {FB738255-0304-4A25-B256-22E36EDF9507}.Release|x86.ActiveCfg = Release|Any CPU + {FB738255-0304-4A25-B256-22E36EDF9507}.Release|x86.Build.0 = Release|Any CPU {91301F52-E589-499E-97DE-91FA074B790C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {91301F52-E589-499E-97DE-91FA074B790C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {91301F52-E589-499E-97DE-91FA074B790C}.Debug|x64.ActiveCfg = Debug|Any CPU + {91301F52-E589-499E-97DE-91FA074B790C}.Debug|x64.Build.0 = Debug|Any CPU + {91301F52-E589-499E-97DE-91FA074B790C}.Debug|x86.ActiveCfg = Debug|Any CPU + {91301F52-E589-499E-97DE-91FA074B790C}.Debug|x86.Build.0 = Debug|Any CPU {91301F52-E589-499E-97DE-91FA074B790C}.Release|Any CPU.ActiveCfg = Release|Any CPU {91301F52-E589-499E-97DE-91FA074B790C}.Release|Any CPU.Build.0 = Release|Any CPU + {91301F52-E589-499E-97DE-91FA074B790C}.Release|x64.ActiveCfg = Release|Any CPU + {91301F52-E589-499E-97DE-91FA074B790C}.Release|x64.Build.0 = Release|Any CPU + {91301F52-E589-499E-97DE-91FA074B790C}.Release|x86.ActiveCfg = Release|Any CPU + {91301F52-E589-499E-97DE-91FA074B790C}.Release|x86.Build.0 = Release|Any CPU {68883A5C-BD59-404D-A394-18104D6F472C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {68883A5C-BD59-404D-A394-18104D6F472C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {68883A5C-BD59-404D-A394-18104D6F472C}.Debug|x64.ActiveCfg = Debug|Any CPU + {68883A5C-BD59-404D-A394-18104D6F472C}.Debug|x64.Build.0 = Debug|Any CPU + {68883A5C-BD59-404D-A394-18104D6F472C}.Debug|x86.ActiveCfg = Debug|Any CPU + {68883A5C-BD59-404D-A394-18104D6F472C}.Debug|x86.Build.0 = Debug|Any CPU {68883A5C-BD59-404D-A394-18104D6F472C}.Release|Any CPU.ActiveCfg = Release|Any CPU {68883A5C-BD59-404D-A394-18104D6F472C}.Release|Any CPU.Build.0 = Release|Any CPU + {68883A5C-BD59-404D-A394-18104D6F472C}.Release|x64.ActiveCfg = Release|Any CPU + {68883A5C-BD59-404D-A394-18104D6F472C}.Release|x64.Build.0 = Release|Any CPU + {68883A5C-BD59-404D-A394-18104D6F472C}.Release|x86.ActiveCfg = Release|Any CPU + {68883A5C-BD59-404D-A394-18104D6F472C}.Release|x86.Build.0 = Release|Any CPU {FE54AD67-817A-4CC6-A9EF-C9F7A5122CA4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {FE54AD67-817A-4CC6-A9EF-C9F7A5122CA4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FE54AD67-817A-4CC6-A9EF-C9F7A5122CA4}.Debug|x64.ActiveCfg = Debug|Any CPU + {FE54AD67-817A-4CC6-A9EF-C9F7A5122CA4}.Debug|x64.Build.0 = Debug|Any CPU + {FE54AD67-817A-4CC6-A9EF-C9F7A5122CA4}.Debug|x86.ActiveCfg = Debug|Any CPU + {FE54AD67-817A-4CC6-A9EF-C9F7A5122CA4}.Debug|x86.Build.0 = Debug|Any CPU {FE54AD67-817A-4CC6-A9EF-C9F7A5122CA4}.Release|Any CPU.ActiveCfg = Release|Any CPU {FE54AD67-817A-4CC6-A9EF-C9F7A5122CA4}.Release|Any CPU.Build.0 = Release|Any CPU + {FE54AD67-817A-4CC6-A9EF-C9F7A5122CA4}.Release|x64.ActiveCfg = Release|Any CPU + {FE54AD67-817A-4CC6-A9EF-C9F7A5122CA4}.Release|x64.Build.0 = Release|Any CPU + {FE54AD67-817A-4CC6-A9EF-C9F7A5122CA4}.Release|x86.ActiveCfg = Release|Any CPU + {FE54AD67-817A-4CC6-A9EF-C9F7A5122CA4}.Release|x86.Build.0 = Release|Any CPU {1DE96D4F-F2CA-4740-8764-BADD1000040A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {1DE96D4F-F2CA-4740-8764-BADD1000040A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1DE96D4F-F2CA-4740-8764-BADD1000040A}.Debug|x64.ActiveCfg = Debug|Any CPU + {1DE96D4F-F2CA-4740-8764-BADD1000040A}.Debug|x64.Build.0 = Debug|Any CPU + {1DE96D4F-F2CA-4740-8764-BADD1000040A}.Debug|x86.ActiveCfg = Debug|Any CPU + {1DE96D4F-F2CA-4740-8764-BADD1000040A}.Debug|x86.Build.0 = Debug|Any CPU {1DE96D4F-F2CA-4740-8764-BADD1000040A}.Release|Any CPU.ActiveCfg = Release|Any CPU {1DE96D4F-F2CA-4740-8764-BADD1000040A}.Release|Any CPU.Build.0 = Release|Any CPU + {1DE96D4F-F2CA-4740-8764-BADD1000040A}.Release|x64.ActiveCfg = Release|Any CPU + {1DE96D4F-F2CA-4740-8764-BADD1000040A}.Release|x64.Build.0 = Release|Any CPU + {1DE96D4F-F2CA-4740-8764-BADD1000040A}.Release|x86.ActiveCfg = Release|Any CPU + {1DE96D4F-F2CA-4740-8764-BADD1000040A}.Release|x86.Build.0 = Release|Any CPU {9274B938-3996-4FBA-AE2F-0C82009B1116}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {9274B938-3996-4FBA-AE2F-0C82009B1116}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9274B938-3996-4FBA-AE2F-0C82009B1116}.Debug|x64.ActiveCfg = Debug|Any CPU + {9274B938-3996-4FBA-AE2F-0C82009B1116}.Debug|x64.Build.0 = Debug|Any CPU + {9274B938-3996-4FBA-AE2F-0C82009B1116}.Debug|x86.ActiveCfg = Debug|Any CPU + {9274B938-3996-4FBA-AE2F-0C82009B1116}.Debug|x86.Build.0 = Debug|Any CPU {9274B938-3996-4FBA-AE2F-0C82009B1116}.Release|Any CPU.ActiveCfg = Release|Any CPU {9274B938-3996-4FBA-AE2F-0C82009B1116}.Release|Any CPU.Build.0 = Release|Any CPU + {9274B938-3996-4FBA-AE2F-0C82009B1116}.Release|x64.ActiveCfg = Release|Any CPU + {9274B938-3996-4FBA-AE2F-0C82009B1116}.Release|x64.Build.0 = Release|Any CPU + {9274B938-3996-4FBA-AE2F-0C82009B1116}.Release|x86.ActiveCfg = Release|Any CPU + {9274B938-3996-4FBA-AE2F-0C82009B1116}.Release|x86.Build.0 = Release|Any CPU {86BC1E05-E9CE-4E53-B324-885A2FDBCE74}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {86BC1E05-E9CE-4E53-B324-885A2FDBCE74}.Debug|Any CPU.Build.0 = Debug|Any CPU + {86BC1E05-E9CE-4E53-B324-885A2FDBCE74}.Debug|x64.ActiveCfg = Debug|Any CPU + {86BC1E05-E9CE-4E53-B324-885A2FDBCE74}.Debug|x64.Build.0 = Debug|Any CPU + {86BC1E05-E9CE-4E53-B324-885A2FDBCE74}.Debug|x86.ActiveCfg = Debug|Any CPU + {86BC1E05-E9CE-4E53-B324-885A2FDBCE74}.Debug|x86.Build.0 = Debug|Any CPU {86BC1E05-E9CE-4E53-B324-885A2FDBCE74}.Release|Any CPU.ActiveCfg = Release|Any CPU {86BC1E05-E9CE-4E53-B324-885A2FDBCE74}.Release|Any CPU.Build.0 = Release|Any CPU + {86BC1E05-E9CE-4E53-B324-885A2FDBCE74}.Release|x64.ActiveCfg = Release|Any CPU + {86BC1E05-E9CE-4E53-B324-885A2FDBCE74}.Release|x64.Build.0 = Release|Any CPU + {86BC1E05-E9CE-4E53-B324-885A2FDBCE74}.Release|x86.ActiveCfg = Release|Any CPU + {86BC1E05-E9CE-4E53-B324-885A2FDBCE74}.Release|x86.Build.0 = Release|Any CPU {AFAD87C7-B2EE-451E-BA7E-3F5A91358C48}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {AFAD87C7-B2EE-451E-BA7E-3F5A91358C48}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AFAD87C7-B2EE-451E-BA7E-3F5A91358C48}.Debug|x64.ActiveCfg = Debug|Any CPU + {AFAD87C7-B2EE-451E-BA7E-3F5A91358C48}.Debug|x64.Build.0 = Debug|Any CPU + {AFAD87C7-B2EE-451E-BA7E-3F5A91358C48}.Debug|x86.ActiveCfg = Debug|Any CPU + {AFAD87C7-B2EE-451E-BA7E-3F5A91358C48}.Debug|x86.Build.0 = Debug|Any CPU {AFAD87C7-B2EE-451E-BA7E-3F5A91358C48}.Release|Any CPU.ActiveCfg = Release|Any CPU {AFAD87C7-B2EE-451E-BA7E-3F5A91358C48}.Release|Any CPU.Build.0 = Release|Any CPU + {AFAD87C7-B2EE-451E-BA7E-3F5A91358C48}.Release|x64.ActiveCfg = Release|Any CPU + {AFAD87C7-B2EE-451E-BA7E-3F5A91358C48}.Release|x64.Build.0 = Release|Any CPU + {AFAD87C7-B2EE-451E-BA7E-3F5A91358C48}.Release|x86.ActiveCfg = Release|Any CPU + {AFAD87C7-B2EE-451E-BA7E-3F5A91358C48}.Release|x86.Build.0 = Release|Any CPU {8FEAFD74-C304-4F75-BA38-4686BE55C891}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {8FEAFD74-C304-4F75-BA38-4686BE55C891}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8FEAFD74-C304-4F75-BA38-4686BE55C891}.Debug|x64.ActiveCfg = Debug|Any CPU + {8FEAFD74-C304-4F75-BA38-4686BE55C891}.Debug|x64.Build.0 = Debug|Any CPU + {8FEAFD74-C304-4F75-BA38-4686BE55C891}.Debug|x86.ActiveCfg = Debug|Any CPU + {8FEAFD74-C304-4F75-BA38-4686BE55C891}.Debug|x86.Build.0 = Debug|Any CPU {8FEAFD74-C304-4F75-BA38-4686BE55C891}.Release|Any CPU.ActiveCfg = Release|Any CPU {8FEAFD74-C304-4F75-BA38-4686BE55C891}.Release|Any CPU.Build.0 = Release|Any CPU + {8FEAFD74-C304-4F75-BA38-4686BE55C891}.Release|x64.ActiveCfg = Release|Any CPU + {8FEAFD74-C304-4F75-BA38-4686BE55C891}.Release|x64.Build.0 = Release|Any CPU + {8FEAFD74-C304-4F75-BA38-4686BE55C891}.Release|x86.ActiveCfg = Release|Any CPU + {8FEAFD74-C304-4F75-BA38-4686BE55C891}.Release|x86.Build.0 = Release|Any CPU {37B598A8-B054-4ABA-884D-96AEF2511600}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {37B598A8-B054-4ABA-884D-96AEF2511600}.Debug|Any CPU.Build.0 = Debug|Any CPU + {37B598A8-B054-4ABA-884D-96AEF2511600}.Debug|x64.ActiveCfg = Debug|Any CPU + {37B598A8-B054-4ABA-884D-96AEF2511600}.Debug|x64.Build.0 = Debug|Any CPU + {37B598A8-B054-4ABA-884D-96AEF2511600}.Debug|x86.ActiveCfg = Debug|Any CPU + {37B598A8-B054-4ABA-884D-96AEF2511600}.Debug|x86.Build.0 = Debug|Any CPU {37B598A8-B054-4ABA-884D-96AEF2511600}.Release|Any CPU.ActiveCfg = Release|Any CPU {37B598A8-B054-4ABA-884D-96AEF2511600}.Release|Any CPU.Build.0 = Release|Any CPU + {37B598A8-B054-4ABA-884D-96AEF2511600}.Release|x64.ActiveCfg = Release|Any CPU + {37B598A8-B054-4ABA-884D-96AEF2511600}.Release|x64.Build.0 = Release|Any CPU + {37B598A8-B054-4ABA-884D-96AEF2511600}.Release|x86.ActiveCfg = Release|Any CPU + {37B598A8-B054-4ABA-884D-96AEF2511600}.Release|x86.Build.0 = Release|Any CPU {17C270A8-EC88-4883-9318-74BB28EFF508}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {17C270A8-EC88-4883-9318-74BB28EFF508}.Debug|Any CPU.Build.0 = Debug|Any CPU + {17C270A8-EC88-4883-9318-74BB28EFF508}.Debug|x64.ActiveCfg = Debug|Any CPU + {17C270A8-EC88-4883-9318-74BB28EFF508}.Debug|x64.Build.0 = Debug|Any CPU + {17C270A8-EC88-4883-9318-74BB28EFF508}.Debug|x86.ActiveCfg = Debug|Any CPU + {17C270A8-EC88-4883-9318-74BB28EFF508}.Debug|x86.Build.0 = Debug|Any CPU {17C270A8-EC88-4883-9318-74BB28EFF508}.Release|Any CPU.ActiveCfg = Release|Any CPU {17C270A8-EC88-4883-9318-74BB28EFF508}.Release|Any CPU.Build.0 = Release|Any CPU + {17C270A8-EC88-4883-9318-74BB28EFF508}.Release|x64.ActiveCfg = Release|Any CPU + {17C270A8-EC88-4883-9318-74BB28EFF508}.Release|x64.Build.0 = Release|Any CPU + {17C270A8-EC88-4883-9318-74BB28EFF508}.Release|x86.ActiveCfg = Release|Any CPU + {17C270A8-EC88-4883-9318-74BB28EFF508}.Release|x86.Build.0 = Release|Any CPU {0631B4BA-D5DD-4C9E-8842-0D370A3D714A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {0631B4BA-D5DD-4C9E-8842-0D370A3D714A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0631B4BA-D5DD-4C9E-8842-0D370A3D714A}.Debug|x64.ActiveCfg = Debug|Any CPU + {0631B4BA-D5DD-4C9E-8842-0D370A3D714A}.Debug|x64.Build.0 = Debug|Any CPU + {0631B4BA-D5DD-4C9E-8842-0D370A3D714A}.Debug|x86.ActiveCfg = Debug|Any CPU + {0631B4BA-D5DD-4C9E-8842-0D370A3D714A}.Debug|x86.Build.0 = Debug|Any CPU {0631B4BA-D5DD-4C9E-8842-0D370A3D714A}.Release|Any CPU.ActiveCfg = Release|Any CPU {0631B4BA-D5DD-4C9E-8842-0D370A3D714A}.Release|Any CPU.Build.0 = Release|Any CPU + {0631B4BA-D5DD-4C9E-8842-0D370A3D714A}.Release|x64.ActiveCfg = Release|Any CPU + {0631B4BA-D5DD-4C9E-8842-0D370A3D714A}.Release|x64.Build.0 = Release|Any CPU + {0631B4BA-D5DD-4C9E-8842-0D370A3D714A}.Release|x86.ActiveCfg = Release|Any CPU + {0631B4BA-D5DD-4C9E-8842-0D370A3D714A}.Release|x86.Build.0 = Release|Any CPU {9162B6AD-AD06-4C64-9032-0E727643B1B9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {9162B6AD-AD06-4C64-9032-0E727643B1B9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9162B6AD-AD06-4C64-9032-0E727643B1B9}.Debug|x64.ActiveCfg = Debug|Any CPU + {9162B6AD-AD06-4C64-9032-0E727643B1B9}.Debug|x64.Build.0 = Debug|Any CPU + {9162B6AD-AD06-4C64-9032-0E727643B1B9}.Debug|x86.ActiveCfg = Debug|Any CPU + {9162B6AD-AD06-4C64-9032-0E727643B1B9}.Debug|x86.Build.0 = Debug|Any CPU {9162B6AD-AD06-4C64-9032-0E727643B1B9}.Release|Any CPU.ActiveCfg = Release|Any CPU {9162B6AD-AD06-4C64-9032-0E727643B1B9}.Release|Any CPU.Build.0 = Release|Any CPU + {9162B6AD-AD06-4C64-9032-0E727643B1B9}.Release|x64.ActiveCfg = Release|Any CPU + {9162B6AD-AD06-4C64-9032-0E727643B1B9}.Release|x64.Build.0 = Release|Any CPU + {9162B6AD-AD06-4C64-9032-0E727643B1B9}.Release|x86.ActiveCfg = Release|Any CPU + {9162B6AD-AD06-4C64-9032-0E727643B1B9}.Release|x86.Build.0 = Release|Any CPU {58EC09C7-EC0A-4708-9B6F-FBE6243CEB49}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {58EC09C7-EC0A-4708-9B6F-FBE6243CEB49}.Debug|Any CPU.Build.0 = Debug|Any CPU + {58EC09C7-EC0A-4708-9B6F-FBE6243CEB49}.Debug|x64.ActiveCfg = Debug|Any CPU + {58EC09C7-EC0A-4708-9B6F-FBE6243CEB49}.Debug|x64.Build.0 = Debug|Any CPU + {58EC09C7-EC0A-4708-9B6F-FBE6243CEB49}.Debug|x86.ActiveCfg = Debug|Any CPU + {58EC09C7-EC0A-4708-9B6F-FBE6243CEB49}.Debug|x86.Build.0 = Debug|Any CPU {58EC09C7-EC0A-4708-9B6F-FBE6243CEB49}.Release|Any CPU.ActiveCfg = Release|Any CPU {58EC09C7-EC0A-4708-9B6F-FBE6243CEB49}.Release|Any CPU.Build.0 = Release|Any CPU + {58EC09C7-EC0A-4708-9B6F-FBE6243CEB49}.Release|x64.ActiveCfg = Release|Any CPU + {58EC09C7-EC0A-4708-9B6F-FBE6243CEB49}.Release|x64.Build.0 = Release|Any CPU + {58EC09C7-EC0A-4708-9B6F-FBE6243CEB49}.Release|x86.ActiveCfg = Release|Any CPU + {58EC09C7-EC0A-4708-9B6F-FBE6243CEB49}.Release|x86.Build.0 = Release|Any CPU {8C2BD4D2-43EC-4930-9364-CDA938C01803}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {8C2BD4D2-43EC-4930-9364-CDA938C01803}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8C2BD4D2-43EC-4930-9364-CDA938C01803}.Debug|x64.ActiveCfg = Debug|Any CPU + {8C2BD4D2-43EC-4930-9364-CDA938C01803}.Debug|x64.Build.0 = Debug|Any CPU + {8C2BD4D2-43EC-4930-9364-CDA938C01803}.Debug|x86.ActiveCfg = Debug|Any CPU + {8C2BD4D2-43EC-4930-9364-CDA938C01803}.Debug|x86.Build.0 = Debug|Any CPU {8C2BD4D2-43EC-4930-9364-CDA938C01803}.Release|Any CPU.ActiveCfg = Release|Any CPU {8C2BD4D2-43EC-4930-9364-CDA938C01803}.Release|Any CPU.Build.0 = Release|Any CPU + {8C2BD4D2-43EC-4930-9364-CDA938C01803}.Release|x64.ActiveCfg = Release|Any CPU + {8C2BD4D2-43EC-4930-9364-CDA938C01803}.Release|x64.Build.0 = Release|Any CPU + {8C2BD4D2-43EC-4930-9364-CDA938C01803}.Release|x86.ActiveCfg = Release|Any CPU + {8C2BD4D2-43EC-4930-9364-CDA938C01803}.Release|x86.Build.0 = Release|Any CPU {4C4DE624-9D91-484F-8BF7-2D71264EAB8B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {4C4DE624-9D91-484F-8BF7-2D71264EAB8B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4C4DE624-9D91-484F-8BF7-2D71264EAB8B}.Debug|x64.ActiveCfg = Debug|Any CPU + {4C4DE624-9D91-484F-8BF7-2D71264EAB8B}.Debug|x64.Build.0 = Debug|Any CPU + {4C4DE624-9D91-484F-8BF7-2D71264EAB8B}.Debug|x86.ActiveCfg = Debug|Any CPU + {4C4DE624-9D91-484F-8BF7-2D71264EAB8B}.Debug|x86.Build.0 = Debug|Any CPU {4C4DE624-9D91-484F-8BF7-2D71264EAB8B}.Release|Any CPU.ActiveCfg = Release|Any CPU {4C4DE624-9D91-484F-8BF7-2D71264EAB8B}.Release|Any CPU.Build.0 = Release|Any CPU + {4C4DE624-9D91-484F-8BF7-2D71264EAB8B}.Release|x64.ActiveCfg = Release|Any CPU + {4C4DE624-9D91-484F-8BF7-2D71264EAB8B}.Release|x64.Build.0 = Release|Any CPU + {4C4DE624-9D91-484F-8BF7-2D71264EAB8B}.Release|x86.ActiveCfg = Release|Any CPU + {4C4DE624-9D91-484F-8BF7-2D71264EAB8B}.Release|x86.Build.0 = Release|Any CPU {ED5074AF-A09E-4357-A419-FE3476C0FAE7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {ED5074AF-A09E-4357-A419-FE3476C0FAE7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {ED5074AF-A09E-4357-A419-FE3476C0FAE7}.Debug|x64.ActiveCfg = Debug|Any CPU + {ED5074AF-A09E-4357-A419-FE3476C0FAE7}.Debug|x64.Build.0 = Debug|Any CPU + {ED5074AF-A09E-4357-A419-FE3476C0FAE7}.Debug|x86.ActiveCfg = Debug|Any CPU + {ED5074AF-A09E-4357-A419-FE3476C0FAE7}.Debug|x86.Build.0 = Debug|Any CPU {ED5074AF-A09E-4357-A419-FE3476C0FAE7}.Release|Any CPU.ActiveCfg = Release|Any CPU {ED5074AF-A09E-4357-A419-FE3476C0FAE7}.Release|Any CPU.Build.0 = Release|Any CPU + {ED5074AF-A09E-4357-A419-FE3476C0FAE7}.Release|x64.ActiveCfg = Release|Any CPU + {ED5074AF-A09E-4357-A419-FE3476C0FAE7}.Release|x64.Build.0 = Release|Any CPU + {ED5074AF-A09E-4357-A419-FE3476C0FAE7}.Release|x86.ActiveCfg = Release|Any CPU + {ED5074AF-A09E-4357-A419-FE3476C0FAE7}.Release|x86.Build.0 = Release|Any CPU {FBF8D151-A3BF-4EB3-8F80-D71618696362}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {FBF8D151-A3BF-4EB3-8F80-D71618696362}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FBF8D151-A3BF-4EB3-8F80-D71618696362}.Debug|x64.ActiveCfg = Debug|Any CPU + {FBF8D151-A3BF-4EB3-8F80-D71618696362}.Debug|x64.Build.0 = Debug|Any CPU + {FBF8D151-A3BF-4EB3-8F80-D71618696362}.Debug|x86.ActiveCfg = Debug|Any CPU + {FBF8D151-A3BF-4EB3-8F80-D71618696362}.Debug|x86.Build.0 = Debug|Any CPU {FBF8D151-A3BF-4EB3-8F80-D71618696362}.Release|Any CPU.ActiveCfg = Release|Any CPU {FBF8D151-A3BF-4EB3-8F80-D71618696362}.Release|Any CPU.Build.0 = Release|Any CPU + {FBF8D151-A3BF-4EB3-8F80-D71618696362}.Release|x64.ActiveCfg = Release|Any CPU + {FBF8D151-A3BF-4EB3-8F80-D71618696362}.Release|x64.Build.0 = Release|Any CPU + {FBF8D151-A3BF-4EB3-8F80-D71618696362}.Release|x86.ActiveCfg = Release|Any CPU + {FBF8D151-A3BF-4EB3-8F80-D71618696362}.Release|x86.Build.0 = Release|Any CPU {50E1AFAC-0B58-43A8-8F03-3A63AAC681FA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {50E1AFAC-0B58-43A8-8F03-3A63AAC681FA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {50E1AFAC-0B58-43A8-8F03-3A63AAC681FA}.Debug|x64.ActiveCfg = Debug|Any CPU + {50E1AFAC-0B58-43A8-8F03-3A63AAC681FA}.Debug|x64.Build.0 = Debug|Any CPU + {50E1AFAC-0B58-43A8-8F03-3A63AAC681FA}.Debug|x86.ActiveCfg = Debug|Any CPU + {50E1AFAC-0B58-43A8-8F03-3A63AAC681FA}.Debug|x86.Build.0 = Debug|Any CPU {50E1AFAC-0B58-43A8-8F03-3A63AAC681FA}.Release|Any CPU.ActiveCfg = Release|Any CPU {50E1AFAC-0B58-43A8-8F03-3A63AAC681FA}.Release|Any CPU.Build.0 = Release|Any CPU + {50E1AFAC-0B58-43A8-8F03-3A63AAC681FA}.Release|x64.ActiveCfg = Release|Any CPU + {50E1AFAC-0B58-43A8-8F03-3A63AAC681FA}.Release|x64.Build.0 = Release|Any CPU + {50E1AFAC-0B58-43A8-8F03-3A63AAC681FA}.Release|x86.ActiveCfg = Release|Any CPU + {50E1AFAC-0B58-43A8-8F03-3A63AAC681FA}.Release|x86.Build.0 = Release|Any CPU {5E792455-4C4C-460F-849E-50A5DCED454D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {5E792455-4C4C-460F-849E-50A5DCED454D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5E792455-4C4C-460F-849E-50A5DCED454D}.Debug|x64.ActiveCfg = Debug|Any CPU + {5E792455-4C4C-460F-849E-50A5DCED454D}.Debug|x64.Build.0 = Debug|Any CPU + {5E792455-4C4C-460F-849E-50A5DCED454D}.Debug|x86.ActiveCfg = Debug|Any CPU + {5E792455-4C4C-460F-849E-50A5DCED454D}.Debug|x86.Build.0 = Debug|Any CPU {5E792455-4C4C-460F-849E-50A5DCED454D}.Release|Any CPU.ActiveCfg = Release|Any CPU {5E792455-4C4C-460F-849E-50A5DCED454D}.Release|Any CPU.Build.0 = Release|Any CPU + {5E792455-4C4C-460F-849E-50A5DCED454D}.Release|x64.ActiveCfg = Release|Any CPU + {5E792455-4C4C-460F-849E-50A5DCED454D}.Release|x64.Build.0 = Release|Any CPU + {5E792455-4C4C-460F-849E-50A5DCED454D}.Release|x86.ActiveCfg = Release|Any CPU + {5E792455-4C4C-460F-849E-50A5DCED454D}.Release|x86.Build.0 = Release|Any CPU {58D0480F-D05D-4348-86D9-B0A7255700E6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {58D0480F-D05D-4348-86D9-B0A7255700E6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {58D0480F-D05D-4348-86D9-B0A7255700E6}.Debug|x64.ActiveCfg = Debug|Any CPU + {58D0480F-D05D-4348-86D9-B0A7255700E6}.Debug|x64.Build.0 = Debug|Any CPU + {58D0480F-D05D-4348-86D9-B0A7255700E6}.Debug|x86.ActiveCfg = Debug|Any CPU + {58D0480F-D05D-4348-86D9-B0A7255700E6}.Debug|x86.Build.0 = Debug|Any CPU {58D0480F-D05D-4348-86D9-B0A7255700E6}.Release|Any CPU.ActiveCfg = Release|Any CPU {58D0480F-D05D-4348-86D9-B0A7255700E6}.Release|Any CPU.Build.0 = Release|Any CPU + {58D0480F-D05D-4348-86D9-B0A7255700E6}.Release|x64.ActiveCfg = Release|Any CPU + {58D0480F-D05D-4348-86D9-B0A7255700E6}.Release|x64.Build.0 = Release|Any CPU + {58D0480F-D05D-4348-86D9-B0A7255700E6}.Release|x86.ActiveCfg = Release|Any CPU + {58D0480F-D05D-4348-86D9-B0A7255700E6}.Release|x86.Build.0 = Release|Any CPU {BB776411-D279-419F-8697-5C6F52BCD5CD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {BB776411-D279-419F-8697-5C6F52BCD5CD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BB776411-D279-419F-8697-5C6F52BCD5CD}.Debug|x64.ActiveCfg = Debug|Any CPU + {BB776411-D279-419F-8697-5C6F52BCD5CD}.Debug|x64.Build.0 = Debug|Any CPU + {BB776411-D279-419F-8697-5C6F52BCD5CD}.Debug|x86.ActiveCfg = Debug|Any CPU + {BB776411-D279-419F-8697-5C6F52BCD5CD}.Debug|x86.Build.0 = Debug|Any CPU {BB776411-D279-419F-8697-5C6F52BCD5CD}.Release|Any CPU.ActiveCfg = Release|Any CPU {BB776411-D279-419F-8697-5C6F52BCD5CD}.Release|Any CPU.Build.0 = Release|Any CPU + {BB776411-D279-419F-8697-5C6F52BCD5CD}.Release|x64.ActiveCfg = Release|Any CPU + {BB776411-D279-419F-8697-5C6F52BCD5CD}.Release|x64.Build.0 = Release|Any CPU + {BB776411-D279-419F-8697-5C6F52BCD5CD}.Release|x86.ActiveCfg = Release|Any CPU + {BB776411-D279-419F-8697-5C6F52BCD5CD}.Release|x86.Build.0 = Release|Any CPU {F9F8F9CD-01D9-468B-856D-6A87F0762A01}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {F9F8F9CD-01D9-468B-856D-6A87F0762A01}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F9F8F9CD-01D9-468B-856D-6A87F0762A01}.Debug|x64.ActiveCfg = Debug|Any CPU + {F9F8F9CD-01D9-468B-856D-6A87F0762A01}.Debug|x64.Build.0 = Debug|Any CPU + {F9F8F9CD-01D9-468B-856D-6A87F0762A01}.Debug|x86.ActiveCfg = Debug|Any CPU + {F9F8F9CD-01D9-468B-856D-6A87F0762A01}.Debug|x86.Build.0 = Debug|Any CPU {F9F8F9CD-01D9-468B-856D-6A87F0762A01}.Release|Any CPU.ActiveCfg = Release|Any CPU {F9F8F9CD-01D9-468B-856D-6A87F0762A01}.Release|Any CPU.Build.0 = Release|Any CPU + {F9F8F9CD-01D9-468B-856D-6A87F0762A01}.Release|x64.ActiveCfg = Release|Any CPU + {F9F8F9CD-01D9-468B-856D-6A87F0762A01}.Release|x64.Build.0 = Release|Any CPU + {F9F8F9CD-01D9-468B-856D-6A87F0762A01}.Release|x86.ActiveCfg = Release|Any CPU + {F9F8F9CD-01D9-468B-856D-6A87F0762A01}.Release|x86.Build.0 = Release|Any CPU {A4B8E54D-F8FE-4358-AE14-A5354E828938}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A4B8E54D-F8FE-4358-AE14-A5354E828938}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A4B8E54D-F8FE-4358-AE14-A5354E828938}.Debug|x64.ActiveCfg = Debug|Any CPU + {A4B8E54D-F8FE-4358-AE14-A5354E828938}.Debug|x64.Build.0 = Debug|Any CPU + {A4B8E54D-F8FE-4358-AE14-A5354E828938}.Debug|x86.ActiveCfg = Debug|Any CPU + {A4B8E54D-F8FE-4358-AE14-A5354E828938}.Debug|x86.Build.0 = Debug|Any CPU {A4B8E54D-F8FE-4358-AE14-A5354E828938}.Release|Any CPU.ActiveCfg = Release|Any CPU {A4B8E54D-F8FE-4358-AE14-A5354E828938}.Release|Any CPU.Build.0 = Release|Any CPU + {A4B8E54D-F8FE-4358-AE14-A5354E828938}.Release|x64.ActiveCfg = Release|Any CPU + {A4B8E54D-F8FE-4358-AE14-A5354E828938}.Release|x64.Build.0 = Release|Any CPU + {A4B8E54D-F8FE-4358-AE14-A5354E828938}.Release|x86.ActiveCfg = Release|Any CPU + {A4B8E54D-F8FE-4358-AE14-A5354E828938}.Release|x86.Build.0 = Release|Any CPU {AAE2E9F9-37EF-4AE1-A200-D37417C9040C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {AAE2E9F9-37EF-4AE1-A200-D37417C9040C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AAE2E9F9-37EF-4AE1-A200-D37417C9040C}.Debug|x64.ActiveCfg = Debug|Any CPU + {AAE2E9F9-37EF-4AE1-A200-D37417C9040C}.Debug|x64.Build.0 = Debug|Any CPU + {AAE2E9F9-37EF-4AE1-A200-D37417C9040C}.Debug|x86.ActiveCfg = Debug|Any CPU + {AAE2E9F9-37EF-4AE1-A200-D37417C9040C}.Debug|x86.Build.0 = Debug|Any CPU {AAE2E9F9-37EF-4AE1-A200-D37417C9040C}.Release|Any CPU.ActiveCfg = Release|Any CPU {AAE2E9F9-37EF-4AE1-A200-D37417C9040C}.Release|Any CPU.Build.0 = Release|Any CPU + {AAE2E9F9-37EF-4AE1-A200-D37417C9040C}.Release|x64.ActiveCfg = Release|Any CPU + {AAE2E9F9-37EF-4AE1-A200-D37417C9040C}.Release|x64.Build.0 = Release|Any CPU + {AAE2E9F9-37EF-4AE1-A200-D37417C9040C}.Release|x86.ActiveCfg = Release|Any CPU + {AAE2E9F9-37EF-4AE1-A200-D37417C9040C}.Release|x86.Build.0 = Release|Any CPU {77C49ACA-203E-428C-A4DB-114DFE454988}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {77C49ACA-203E-428C-A4DB-114DFE454988}.Debug|Any CPU.Build.0 = Debug|Any CPU + {77C49ACA-203E-428C-A4DB-114DFE454988}.Debug|x64.ActiveCfg = Debug|Any CPU + {77C49ACA-203E-428C-A4DB-114DFE454988}.Debug|x64.Build.0 = Debug|Any CPU + {77C49ACA-203E-428C-A4DB-114DFE454988}.Debug|x86.ActiveCfg = Debug|Any CPU + {77C49ACA-203E-428C-A4DB-114DFE454988}.Debug|x86.Build.0 = Debug|Any CPU {77C49ACA-203E-428C-A4DB-114DFE454988}.Release|Any CPU.ActiveCfg = Release|Any CPU {77C49ACA-203E-428C-A4DB-114DFE454988}.Release|Any CPU.Build.0 = Release|Any CPU + {77C49ACA-203E-428C-A4DB-114DFE454988}.Release|x64.ActiveCfg = Release|Any CPU + {77C49ACA-203E-428C-A4DB-114DFE454988}.Release|x64.Build.0 = Release|Any CPU + {77C49ACA-203E-428C-A4DB-114DFE454988}.Release|x86.ActiveCfg = Release|Any CPU + {77C49ACA-203E-428C-A4DB-114DFE454988}.Release|x86.Build.0 = Release|Any CPU {A2374B7C-4198-40B3-B8FE-FAC3DB3F2539}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A2374B7C-4198-40B3-B8FE-FAC3DB3F2539}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A2374B7C-4198-40B3-B8FE-FAC3DB3F2539}.Debug|x64.ActiveCfg = Debug|Any CPU + {A2374B7C-4198-40B3-B8FE-FAC3DB3F2539}.Debug|x64.Build.0 = Debug|Any CPU + {A2374B7C-4198-40B3-B8FE-FAC3DB3F2539}.Debug|x86.ActiveCfg = Debug|Any CPU + {A2374B7C-4198-40B3-B8FE-FAC3DB3F2539}.Debug|x86.Build.0 = Debug|Any CPU {A2374B7C-4198-40B3-B8FE-FAC3DB3F2539}.Release|Any CPU.ActiveCfg = Release|Any CPU {A2374B7C-4198-40B3-B8FE-FAC3DB3F2539}.Release|Any CPU.Build.0 = Release|Any CPU + {A2374B7C-4198-40B3-B8FE-FAC3DB3F2539}.Release|x64.ActiveCfg = Release|Any CPU + {A2374B7C-4198-40B3-B8FE-FAC3DB3F2539}.Release|x64.Build.0 = Release|Any CPU + {A2374B7C-4198-40B3-B8FE-FAC3DB3F2539}.Release|x86.ActiveCfg = Release|Any CPU + {A2374B7C-4198-40B3-B8FE-FAC3DB3F2539}.Release|x86.Build.0 = Release|Any CPU {6BC66637-B42A-4334-ADFB-DBEC9F29D293}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {6BC66637-B42A-4334-ADFB-DBEC9F29D293}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6BC66637-B42A-4334-ADFB-DBEC9F29D293}.Debug|x64.ActiveCfg = Debug|Any CPU + {6BC66637-B42A-4334-ADFB-DBEC9F29D293}.Debug|x64.Build.0 = Debug|Any CPU + {6BC66637-B42A-4334-ADFB-DBEC9F29D293}.Debug|x86.ActiveCfg = Debug|Any CPU + {6BC66637-B42A-4334-ADFB-DBEC9F29D293}.Debug|x86.Build.0 = Debug|Any CPU {6BC66637-B42A-4334-ADFB-DBEC9F29D293}.Release|Any CPU.ActiveCfg = Release|Any CPU {6BC66637-B42A-4334-ADFB-DBEC9F29D293}.Release|Any CPU.Build.0 = Release|Any CPU + {6BC66637-B42A-4334-ADFB-DBEC9F29D293}.Release|x64.ActiveCfg = Release|Any CPU + {6BC66637-B42A-4334-ADFB-DBEC9F29D293}.Release|x64.Build.0 = Release|Any CPU + {6BC66637-B42A-4334-ADFB-DBEC9F29D293}.Release|x86.ActiveCfg = Release|Any CPU + {6BC66637-B42A-4334-ADFB-DBEC9F29D293}.Release|x86.Build.0 = Release|Any CPU {62A9709E-27DA-42EE-B94F-5AF431D86354}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {62A9709E-27DA-42EE-B94F-5AF431D86354}.Debug|Any CPU.Build.0 = Debug|Any CPU + {62A9709E-27DA-42EE-B94F-5AF431D86354}.Debug|x64.ActiveCfg = Debug|Any CPU + {62A9709E-27DA-42EE-B94F-5AF431D86354}.Debug|x64.Build.0 = Debug|Any CPU + {62A9709E-27DA-42EE-B94F-5AF431D86354}.Debug|x86.ActiveCfg = Debug|Any CPU + {62A9709E-27DA-42EE-B94F-5AF431D86354}.Debug|x86.Build.0 = Debug|Any CPU {62A9709E-27DA-42EE-B94F-5AF431D86354}.Release|Any CPU.ActiveCfg = Release|Any CPU {62A9709E-27DA-42EE-B94F-5AF431D86354}.Release|Any CPU.Build.0 = Release|Any CPU + {62A9709E-27DA-42EE-B94F-5AF431D86354}.Release|x64.ActiveCfg = Release|Any CPU + {62A9709E-27DA-42EE-B94F-5AF431D86354}.Release|x64.Build.0 = Release|Any CPU + {62A9709E-27DA-42EE-B94F-5AF431D86354}.Release|x86.ActiveCfg = Release|Any CPU + {62A9709E-27DA-42EE-B94F-5AF431D86354}.Release|x86.Build.0 = Release|Any CPU {0E3C1496-8E7C-411A-A536-C7C9CE4EED4E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {0E3C1496-8E7C-411A-A536-C7C9CE4EED4E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0E3C1496-8E7C-411A-A536-C7C9CE4EED4E}.Debug|x64.ActiveCfg = Debug|Any CPU + {0E3C1496-8E7C-411A-A536-C7C9CE4EED4E}.Debug|x64.Build.0 = Debug|Any CPU + {0E3C1496-8E7C-411A-A536-C7C9CE4EED4E}.Debug|x86.ActiveCfg = Debug|Any CPU + {0E3C1496-8E7C-411A-A536-C7C9CE4EED4E}.Debug|x86.Build.0 = Debug|Any CPU {0E3C1496-8E7C-411A-A536-C7C9CE4EED4E}.Release|Any CPU.ActiveCfg = Release|Any CPU {0E3C1496-8E7C-411A-A536-C7C9CE4EED4E}.Release|Any CPU.Build.0 = Release|Any CPU + {0E3C1496-8E7C-411A-A536-C7C9CE4EED4E}.Release|x64.ActiveCfg = Release|Any CPU + {0E3C1496-8E7C-411A-A536-C7C9CE4EED4E}.Release|x64.Build.0 = Release|Any CPU + {0E3C1496-8E7C-411A-A536-C7C9CE4EED4E}.Release|x86.ActiveCfg = Release|Any CPU + {0E3C1496-8E7C-411A-A536-C7C9CE4EED4E}.Release|x86.Build.0 = Release|Any CPU {EC497168-5347-4E70-9D9E-9C2F826C1CDF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {EC497168-5347-4E70-9D9E-9C2F826C1CDF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EC497168-5347-4E70-9D9E-9C2F826C1CDF}.Debug|x64.ActiveCfg = Debug|Any CPU + {EC497168-5347-4E70-9D9E-9C2F826C1CDF}.Debug|x64.Build.0 = Debug|Any CPU + {EC497168-5347-4E70-9D9E-9C2F826C1CDF}.Debug|x86.ActiveCfg = Debug|Any CPU + {EC497168-5347-4E70-9D9E-9C2F826C1CDF}.Debug|x86.Build.0 = Debug|Any CPU {EC497168-5347-4E70-9D9E-9C2F826C1CDF}.Release|Any CPU.ActiveCfg = Release|Any CPU {EC497168-5347-4E70-9D9E-9C2F826C1CDF}.Release|Any CPU.Build.0 = Release|Any CPU + {EC497168-5347-4E70-9D9E-9C2F826C1CDF}.Release|x64.ActiveCfg = Release|Any CPU + {EC497168-5347-4E70-9D9E-9C2F826C1CDF}.Release|x64.Build.0 = Release|Any CPU + {EC497168-5347-4E70-9D9E-9C2F826C1CDF}.Release|x86.ActiveCfg = Release|Any CPU + {EC497168-5347-4E70-9D9E-9C2F826C1CDF}.Release|x86.Build.0 = Release|Any CPU {9B7811AC-68D6-4D19-B1E9-65423393ED83}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {9B7811AC-68D6-4D19-B1E9-65423393ED83}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9B7811AC-68D6-4D19-B1E9-65423393ED83}.Debug|x64.ActiveCfg = Debug|Any CPU + {9B7811AC-68D6-4D19-B1E9-65423393ED83}.Debug|x64.Build.0 = Debug|Any CPU + {9B7811AC-68D6-4D19-B1E9-65423393ED83}.Debug|x86.ActiveCfg = Debug|Any CPU + {9B7811AC-68D6-4D19-B1E9-65423393ED83}.Debug|x86.Build.0 = Debug|Any CPU {9B7811AC-68D6-4D19-B1E9-65423393ED83}.Release|Any CPU.ActiveCfg = Release|Any CPU {9B7811AC-68D6-4D19-B1E9-65423393ED83}.Release|Any CPU.Build.0 = Release|Any CPU + {9B7811AC-68D6-4D19-B1E9-65423393ED83}.Release|x64.ActiveCfg = Release|Any CPU + {9B7811AC-68D6-4D19-B1E9-65423393ED83}.Release|x64.Build.0 = Release|Any CPU + {9B7811AC-68D6-4D19-B1E9-65423393ED83}.Release|x86.ActiveCfg = Release|Any CPU + {9B7811AC-68D6-4D19-B1E9-65423393ED83}.Release|x86.Build.0 = Release|Any CPU {0C9617A9-C8B7-45F6-A54A-261A23AC881B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {0C9617A9-C8B7-45F6-A54A-261A23AC881B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0C9617A9-C8B7-45F6-A54A-261A23AC881B}.Debug|x64.ActiveCfg = Debug|Any CPU + {0C9617A9-C8B7-45F6-A54A-261A23AC881B}.Debug|x64.Build.0 = Debug|Any CPU + {0C9617A9-C8B7-45F6-A54A-261A23AC881B}.Debug|x86.ActiveCfg = Debug|Any CPU + {0C9617A9-C8B7-45F6-A54A-261A23AC881B}.Debug|x86.Build.0 = Debug|Any CPU {0C9617A9-C8B7-45F6-A54A-261A23AC881B}.Release|Any CPU.ActiveCfg = Release|Any CPU {0C9617A9-C8B7-45F6-A54A-261A23AC881B}.Release|Any CPU.Build.0 = Release|Any CPU + {0C9617A9-C8B7-45F6-A54A-261A23AC881B}.Release|x64.ActiveCfg = Release|Any CPU + {0C9617A9-C8B7-45F6-A54A-261A23AC881B}.Release|x64.Build.0 = Release|Any CPU + {0C9617A9-C8B7-45F6-A54A-261A23AC881B}.Release|x86.ActiveCfg = Release|Any CPU + {0C9617A9-C8B7-45F6-A54A-261A23AC881B}.Release|x86.Build.0 = Release|Any CPU {42F475BC-95F4-42E1-8CCD-7B9C27487E33}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {42F475BC-95F4-42E1-8CCD-7B9C27487E33}.Debug|Any CPU.Build.0 = Debug|Any CPU + {42F475BC-95F4-42E1-8CCD-7B9C27487E33}.Debug|x64.ActiveCfg = Debug|Any CPU + {42F475BC-95F4-42E1-8CCD-7B9C27487E33}.Debug|x64.Build.0 = Debug|Any CPU + {42F475BC-95F4-42E1-8CCD-7B9C27487E33}.Debug|x86.ActiveCfg = Debug|Any CPU + {42F475BC-95F4-42E1-8CCD-7B9C27487E33}.Debug|x86.Build.0 = Debug|Any CPU {42F475BC-95F4-42E1-8CCD-7B9C27487E33}.Release|Any CPU.ActiveCfg = Release|Any CPU {42F475BC-95F4-42E1-8CCD-7B9C27487E33}.Release|Any CPU.Build.0 = Release|Any CPU + {42F475BC-95F4-42E1-8CCD-7B9C27487E33}.Release|x64.ActiveCfg = Release|Any CPU + {42F475BC-95F4-42E1-8CCD-7B9C27487E33}.Release|x64.Build.0 = Release|Any CPU + {42F475BC-95F4-42E1-8CCD-7B9C27487E33}.Release|x86.ActiveCfg = Release|Any CPU + {42F475BC-95F4-42E1-8CCD-7B9C27487E33}.Release|x86.Build.0 = Release|Any CPU {7EDD9353-F5C2-414C-AE51-4B0F1C5E105A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {7EDD9353-F5C2-414C-AE51-4B0F1C5E105A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7EDD9353-F5C2-414C-AE51-4B0F1C5E105A}.Debug|x64.ActiveCfg = Debug|Any CPU + {7EDD9353-F5C2-414C-AE51-4B0F1C5E105A}.Debug|x64.Build.0 = Debug|Any CPU + {7EDD9353-F5C2-414C-AE51-4B0F1C5E105A}.Debug|x86.ActiveCfg = Debug|Any CPU + {7EDD9353-F5C2-414C-AE51-4B0F1C5E105A}.Debug|x86.Build.0 = Debug|Any CPU {7EDD9353-F5C2-414C-AE51-4B0F1C5E105A}.Release|Any CPU.ActiveCfg = Release|Any CPU {7EDD9353-F5C2-414C-AE51-4B0F1C5E105A}.Release|Any CPU.Build.0 = Release|Any CPU + {7EDD9353-F5C2-414C-AE51-4B0F1C5E105A}.Release|x64.ActiveCfg = Release|Any CPU + {7EDD9353-F5C2-414C-AE51-4B0F1C5E105A}.Release|x64.Build.0 = Release|Any CPU + {7EDD9353-F5C2-414C-AE51-4B0F1C5E105A}.Release|x86.ActiveCfg = Release|Any CPU + {7EDD9353-F5C2-414C-AE51-4B0F1C5E105A}.Release|x86.Build.0 = Release|Any CPU {5E82A137-0954-46A1-8C46-13C00F0E4842}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {5E82A137-0954-46A1-8C46-13C00F0E4842}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5E82A137-0954-46A1-8C46-13C00F0E4842}.Debug|x64.ActiveCfg = Debug|Any CPU + {5E82A137-0954-46A1-8C46-13C00F0E4842}.Debug|x64.Build.0 = Debug|Any CPU + {5E82A137-0954-46A1-8C46-13C00F0E4842}.Debug|x86.ActiveCfg = Debug|Any CPU + {5E82A137-0954-46A1-8C46-13C00F0E4842}.Debug|x86.Build.0 = Debug|Any CPU {5E82A137-0954-46A1-8C46-13C00F0E4842}.Release|Any CPU.ActiveCfg = Release|Any CPU {5E82A137-0954-46A1-8C46-13C00F0E4842}.Release|Any CPU.Build.0 = Release|Any CPU + {5E82A137-0954-46A1-8C46-13C00F0E4842}.Release|x64.ActiveCfg = Release|Any CPU + {5E82A137-0954-46A1-8C46-13C00F0E4842}.Release|x64.Build.0 = Release|Any CPU + {5E82A137-0954-46A1-8C46-13C00F0E4842}.Release|x86.ActiveCfg = Release|Any CPU + {5E82A137-0954-46A1-8C46-13C00F0E4842}.Release|x86.Build.0 = Release|Any CPU {453E260D-DBDC-4DDC-BC9C-CA500CED7897}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {453E260D-DBDC-4DDC-BC9C-CA500CED7897}.Debug|Any CPU.Build.0 = Debug|Any CPU + {453E260D-DBDC-4DDC-BC9C-CA500CED7897}.Debug|x64.ActiveCfg = Debug|Any CPU + {453E260D-DBDC-4DDC-BC9C-CA500CED7897}.Debug|x64.Build.0 = Debug|Any CPU + {453E260D-DBDC-4DDC-BC9C-CA500CED7897}.Debug|x86.ActiveCfg = Debug|Any CPU + {453E260D-DBDC-4DDC-BC9C-CA500CED7897}.Debug|x86.Build.0 = Debug|Any CPU {453E260D-DBDC-4DDC-BC9C-CA500CED7897}.Release|Any CPU.ActiveCfg = Release|Any CPU {453E260D-DBDC-4DDC-BC9C-CA500CED7897}.Release|Any CPU.Build.0 = Release|Any CPU + {453E260D-DBDC-4DDC-BC9C-CA500CED7897}.Release|x64.ActiveCfg = Release|Any CPU + {453E260D-DBDC-4DDC-BC9C-CA500CED7897}.Release|x64.Build.0 = Release|Any CPU + {453E260D-DBDC-4DDC-BC9C-CA500CED7897}.Release|x86.ActiveCfg = Release|Any CPU + {453E260D-DBDC-4DDC-BC9C-CA500CED7897}.Release|x86.Build.0 = Release|Any CPU {DF7F7ECA-1771-40C9-9CD0-AFEFC44E60DE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {DF7F7ECA-1771-40C9-9CD0-AFEFC44E60DE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DF7F7ECA-1771-40C9-9CD0-AFEFC44E60DE}.Debug|x64.ActiveCfg = Debug|Any CPU + {DF7F7ECA-1771-40C9-9CD0-AFEFC44E60DE}.Debug|x64.Build.0 = Debug|Any CPU + {DF7F7ECA-1771-40C9-9CD0-AFEFC44E60DE}.Debug|x86.ActiveCfg = Debug|Any CPU + {DF7F7ECA-1771-40C9-9CD0-AFEFC44E60DE}.Debug|x86.Build.0 = Debug|Any CPU {DF7F7ECA-1771-40C9-9CD0-AFEFC44E60DE}.Release|Any CPU.ActiveCfg = Release|Any CPU {DF7F7ECA-1771-40C9-9CD0-AFEFC44E60DE}.Release|Any CPU.Build.0 = Release|Any CPU + {DF7F7ECA-1771-40C9-9CD0-AFEFC44E60DE}.Release|x64.ActiveCfg = Release|Any CPU + {DF7F7ECA-1771-40C9-9CD0-AFEFC44E60DE}.Release|x64.Build.0 = Release|Any CPU + {DF7F7ECA-1771-40C9-9CD0-AFEFC44E60DE}.Release|x86.ActiveCfg = Release|Any CPU + {DF7F7ECA-1771-40C9-9CD0-AFEFC44E60DE}.Release|x86.Build.0 = Release|Any CPU {3ECEC028-7E2C-4983-B928-26C073B51BB7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {3ECEC028-7E2C-4983-B928-26C073B51BB7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3ECEC028-7E2C-4983-B928-26C073B51BB7}.Debug|x64.ActiveCfg = Debug|Any CPU + {3ECEC028-7E2C-4983-B928-26C073B51BB7}.Debug|x64.Build.0 = Debug|Any CPU + {3ECEC028-7E2C-4983-B928-26C073B51BB7}.Debug|x86.ActiveCfg = Debug|Any CPU + {3ECEC028-7E2C-4983-B928-26C073B51BB7}.Debug|x86.Build.0 = Debug|Any CPU {3ECEC028-7E2C-4983-B928-26C073B51BB7}.Release|Any CPU.ActiveCfg = Release|Any CPU {3ECEC028-7E2C-4983-B928-26C073B51BB7}.Release|Any CPU.Build.0 = Release|Any CPU + {3ECEC028-7E2C-4983-B928-26C073B51BB7}.Release|x64.ActiveCfg = Release|Any CPU + {3ECEC028-7E2C-4983-B928-26C073B51BB7}.Release|x64.Build.0 = Release|Any CPU + {3ECEC028-7E2C-4983-B928-26C073B51BB7}.Release|x86.ActiveCfg = Release|Any CPU + {3ECEC028-7E2C-4983-B928-26C073B51BB7}.Release|x86.Build.0 = Release|Any CPU {435C6263-C6F8-4E93-B417-D861E9C22E18}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {435C6263-C6F8-4E93-B417-D861E9C22E18}.Debug|Any CPU.Build.0 = Debug|Any CPU + {435C6263-C6F8-4E93-B417-D861E9C22E18}.Debug|x64.ActiveCfg = Debug|Any CPU + {435C6263-C6F8-4E93-B417-D861E9C22E18}.Debug|x64.Build.0 = Debug|Any CPU + {435C6263-C6F8-4E93-B417-D861E9C22E18}.Debug|x86.ActiveCfg = Debug|Any CPU + {435C6263-C6F8-4E93-B417-D861E9C22E18}.Debug|x86.Build.0 = Debug|Any CPU {435C6263-C6F8-4E93-B417-D861E9C22E18}.Release|Any CPU.ActiveCfg = Release|Any CPU {435C6263-C6F8-4E93-B417-D861E9C22E18}.Release|Any CPU.Build.0 = Release|Any CPU + {435C6263-C6F8-4E93-B417-D861E9C22E18}.Release|x64.ActiveCfg = Release|Any CPU + {435C6263-C6F8-4E93-B417-D861E9C22E18}.Release|x64.Build.0 = Release|Any CPU + {435C6263-C6F8-4E93-B417-D861E9C22E18}.Release|x86.ActiveCfg = Release|Any CPU + {435C6263-C6F8-4E93-B417-D861E9C22E18}.Release|x86.Build.0 = Release|Any CPU {F6348170-B695-4D97-BAE6-4F0F643F3BEF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {F6348170-B695-4D97-BAE6-4F0F643F3BEF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F6348170-B695-4D97-BAE6-4F0F643F3BEF}.Debug|x64.ActiveCfg = Debug|Any CPU + {F6348170-B695-4D97-BAE6-4F0F643F3BEF}.Debug|x64.Build.0 = Debug|Any CPU + {F6348170-B695-4D97-BAE6-4F0F643F3BEF}.Debug|x86.ActiveCfg = Debug|Any CPU + {F6348170-B695-4D97-BAE6-4F0F643F3BEF}.Debug|x86.Build.0 = Debug|Any CPU {F6348170-B695-4D97-BAE6-4F0F643F3BEF}.Release|Any CPU.ActiveCfg = Release|Any CPU {F6348170-B695-4D97-BAE6-4F0F643F3BEF}.Release|Any CPU.Build.0 = Release|Any CPU + {F6348170-B695-4D97-BAE6-4F0F643F3BEF}.Release|x64.ActiveCfg = Release|Any CPU + {F6348170-B695-4D97-BAE6-4F0F643F3BEF}.Release|x64.Build.0 = Release|Any CPU + {F6348170-B695-4D97-BAE6-4F0F643F3BEF}.Release|x86.ActiveCfg = Release|Any CPU + {F6348170-B695-4D97-BAE6-4F0F643F3BEF}.Release|x86.Build.0 = Release|Any CPU {44644716-0CE8-4837-B189-AB65AE2106AA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {44644716-0CE8-4837-B189-AB65AE2106AA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {44644716-0CE8-4837-B189-AB65AE2106AA}.Debug|x64.ActiveCfg = Debug|Any CPU + {44644716-0CE8-4837-B189-AB65AE2106AA}.Debug|x64.Build.0 = Debug|Any CPU + {44644716-0CE8-4837-B189-AB65AE2106AA}.Debug|x86.ActiveCfg = Debug|Any CPU + {44644716-0CE8-4837-B189-AB65AE2106AA}.Debug|x86.Build.0 = Debug|Any CPU {44644716-0CE8-4837-B189-AB65AE2106AA}.Release|Any CPU.ActiveCfg = Release|Any CPU {44644716-0CE8-4837-B189-AB65AE2106AA}.Release|Any CPU.Build.0 = Release|Any CPU + {44644716-0CE8-4837-B189-AB65AE2106AA}.Release|x64.ActiveCfg = Release|Any CPU + {44644716-0CE8-4837-B189-AB65AE2106AA}.Release|x64.Build.0 = Release|Any CPU + {44644716-0CE8-4837-B189-AB65AE2106AA}.Release|x86.ActiveCfg = Release|Any CPU + {44644716-0CE8-4837-B189-AB65AE2106AA}.Release|x86.Build.0 = Release|Any CPU {78217204-B873-40B9-8875-E3925B2FBCEC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {78217204-B873-40B9-8875-E3925B2FBCEC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {78217204-B873-40B9-8875-E3925B2FBCEC}.Debug|x64.ActiveCfg = Debug|Any CPU + {78217204-B873-40B9-8875-E3925B2FBCEC}.Debug|x64.Build.0 = Debug|Any CPU + {78217204-B873-40B9-8875-E3925B2FBCEC}.Debug|x86.ActiveCfg = Debug|Any CPU + {78217204-B873-40B9-8875-E3925B2FBCEC}.Debug|x86.Build.0 = Debug|Any CPU {78217204-B873-40B9-8875-E3925B2FBCEC}.Release|Any CPU.ActiveCfg = Release|Any CPU {78217204-B873-40B9-8875-E3925B2FBCEC}.Release|Any CPU.Build.0 = Release|Any CPU + {78217204-B873-40B9-8875-E3925B2FBCEC}.Release|x64.ActiveCfg = Release|Any CPU + {78217204-B873-40B9-8875-E3925B2FBCEC}.Release|x64.Build.0 = Release|Any CPU + {78217204-B873-40B9-8875-E3925B2FBCEC}.Release|x86.ActiveCfg = Release|Any CPU + {78217204-B873-40B9-8875-E3925B2FBCEC}.Release|x86.Build.0 = Release|Any CPU {20B98905-08CB-4854-8E2C-A31A078383E9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {20B98905-08CB-4854-8E2C-A31A078383E9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {20B98905-08CB-4854-8E2C-A31A078383E9}.Debug|x64.ActiveCfg = Debug|Any CPU + {20B98905-08CB-4854-8E2C-A31A078383E9}.Debug|x64.Build.0 = Debug|Any CPU + {20B98905-08CB-4854-8E2C-A31A078383E9}.Debug|x86.ActiveCfg = Debug|Any CPU + {20B98905-08CB-4854-8E2C-A31A078383E9}.Debug|x86.Build.0 = Debug|Any CPU {20B98905-08CB-4854-8E2C-A31A078383E9}.Release|Any CPU.ActiveCfg = Release|Any CPU {20B98905-08CB-4854-8E2C-A31A078383E9}.Release|Any CPU.Build.0 = Release|Any CPU + {20B98905-08CB-4854-8E2C-A31A078383E9}.Release|x64.ActiveCfg = Release|Any CPU + {20B98905-08CB-4854-8E2C-A31A078383E9}.Release|x64.Build.0 = Release|Any CPU + {20B98905-08CB-4854-8E2C-A31A078383E9}.Release|x86.ActiveCfg = Release|Any CPU + {20B98905-08CB-4854-8E2C-A31A078383E9}.Release|x86.Build.0 = Release|Any CPU {5BE6D628-B9DB-4C76-AAEB-8F3800509A84}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {5BE6D628-B9DB-4C76-AAEB-8F3800509A84}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5BE6D628-B9DB-4C76-AAEB-8F3800509A84}.Debug|x64.ActiveCfg = Debug|Any CPU + {5BE6D628-B9DB-4C76-AAEB-8F3800509A84}.Debug|x64.Build.0 = Debug|Any CPU + {5BE6D628-B9DB-4C76-AAEB-8F3800509A84}.Debug|x86.ActiveCfg = Debug|Any CPU + {5BE6D628-B9DB-4C76-AAEB-8F3800509A84}.Debug|x86.Build.0 = Debug|Any CPU {5BE6D628-B9DB-4C76-AAEB-8F3800509A84}.Release|Any CPU.ActiveCfg = Release|Any CPU {5BE6D628-B9DB-4C76-AAEB-8F3800509A84}.Release|Any CPU.Build.0 = Release|Any CPU + {5BE6D628-B9DB-4C76-AAEB-8F3800509A84}.Release|x64.ActiveCfg = Release|Any CPU + {5BE6D628-B9DB-4C76-AAEB-8F3800509A84}.Release|x64.Build.0 = Release|Any CPU + {5BE6D628-B9DB-4C76-AAEB-8F3800509A84}.Release|x86.ActiveCfg = Release|Any CPU + {5BE6D628-B9DB-4C76-AAEB-8F3800509A84}.Release|x86.Build.0 = Release|Any CPU {E32CF21A-29CC-46D1-8044-FCC327F2B281}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {E32CF21A-29CC-46D1-8044-FCC327F2B281}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E32CF21A-29CC-46D1-8044-FCC327F2B281}.Debug|x64.ActiveCfg = Debug|Any CPU + {E32CF21A-29CC-46D1-8044-FCC327F2B281}.Debug|x64.Build.0 = Debug|Any CPU + {E32CF21A-29CC-46D1-8044-FCC327F2B281}.Debug|x86.ActiveCfg = Debug|Any CPU + {E32CF21A-29CC-46D1-8044-FCC327F2B281}.Debug|x86.Build.0 = Debug|Any CPU {E32CF21A-29CC-46D1-8044-FCC327F2B281}.Release|Any CPU.ActiveCfg = Release|Any CPU {E32CF21A-29CC-46D1-8044-FCC327F2B281}.Release|Any CPU.Build.0 = Release|Any CPU + {E32CF21A-29CC-46D1-8044-FCC327F2B281}.Release|x64.ActiveCfg = Release|Any CPU + {E32CF21A-29CC-46D1-8044-FCC327F2B281}.Release|x64.Build.0 = Release|Any CPU + {E32CF21A-29CC-46D1-8044-FCC327F2B281}.Release|x86.ActiveCfg = Release|Any CPU + {E32CF21A-29CC-46D1-8044-FCC327F2B281}.Release|x86.Build.0 = Release|Any CPU {51BB7DCD-01DD-453D-A1E7-17E5E3DBB14C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {51BB7DCD-01DD-453D-A1E7-17E5E3DBB14C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {51BB7DCD-01DD-453D-A1E7-17E5E3DBB14C}.Debug|x64.ActiveCfg = Debug|Any CPU + {51BB7DCD-01DD-453D-A1E7-17E5E3DBB14C}.Debug|x64.Build.0 = Debug|Any CPU + {51BB7DCD-01DD-453D-A1E7-17E5E3DBB14C}.Debug|x86.ActiveCfg = Debug|Any CPU + {51BB7DCD-01DD-453D-A1E7-17E5E3DBB14C}.Debug|x86.Build.0 = Debug|Any CPU {51BB7DCD-01DD-453D-A1E7-17E5E3DBB14C}.Release|Any CPU.ActiveCfg = Release|Any CPU {51BB7DCD-01DD-453D-A1E7-17E5E3DBB14C}.Release|Any CPU.Build.0 = Release|Any CPU + {51BB7DCD-01DD-453D-A1E7-17E5E3DBB14C}.Release|x64.ActiveCfg = Release|Any CPU + {51BB7DCD-01DD-453D-A1E7-17E5E3DBB14C}.Release|x64.Build.0 = Release|Any CPU + {51BB7DCD-01DD-453D-A1E7-17E5E3DBB14C}.Release|x86.ActiveCfg = Release|Any CPU + {51BB7DCD-01DD-453D-A1E7-17E5E3DBB14C}.Release|x86.Build.0 = Release|Any CPU {54DE20BA-EBA7-4BF0-9BD9-F03766849716}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {54DE20BA-EBA7-4BF0-9BD9-F03766849716}.Debug|Any CPU.Build.0 = Debug|Any CPU + {54DE20BA-EBA7-4BF0-9BD9-F03766849716}.Debug|x64.ActiveCfg = Debug|Any CPU + {54DE20BA-EBA7-4BF0-9BD9-F03766849716}.Debug|x64.Build.0 = Debug|Any CPU + {54DE20BA-EBA7-4BF0-9BD9-F03766849716}.Debug|x86.ActiveCfg = Debug|Any CPU + {54DE20BA-EBA7-4BF0-9BD9-F03766849716}.Debug|x86.Build.0 = Debug|Any CPU {54DE20BA-EBA7-4BF0-9BD9-F03766849716}.Release|Any CPU.ActiveCfg = Release|Any CPU {54DE20BA-EBA7-4BF0-9BD9-F03766849716}.Release|Any CPU.Build.0 = Release|Any CPU + {54DE20BA-EBA7-4BF0-9BD9-F03766849716}.Release|x64.ActiveCfg = Release|Any CPU + {54DE20BA-EBA7-4BF0-9BD9-F03766849716}.Release|x64.Build.0 = Release|Any CPU + {54DE20BA-EBA7-4BF0-9BD9-F03766849716}.Release|x86.ActiveCfg = Release|Any CPU + {54DE20BA-EBA7-4BF0-9BD9-F03766849716}.Release|x86.Build.0 = Release|Any CPU {1223ED47-3E5E-4960-B70D-DFAF550F6666}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {1223ED47-3E5E-4960-B70D-DFAF550F6666}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1223ED47-3E5E-4960-B70D-DFAF550F6666}.Debug|x64.ActiveCfg = Debug|Any CPU + {1223ED47-3E5E-4960-B70D-DFAF550F6666}.Debug|x64.Build.0 = Debug|Any CPU + {1223ED47-3E5E-4960-B70D-DFAF550F6666}.Debug|x86.ActiveCfg = Debug|Any CPU + {1223ED47-3E5E-4960-B70D-DFAF550F6666}.Debug|x86.Build.0 = Debug|Any CPU {1223ED47-3E5E-4960-B70D-DFAF550F6666}.Release|Any CPU.ActiveCfg = Release|Any CPU {1223ED47-3E5E-4960-B70D-DFAF550F6666}.Release|Any CPU.Build.0 = Release|Any CPU + {1223ED47-3E5E-4960-B70D-DFAF550F6666}.Release|x64.ActiveCfg = Release|Any CPU + {1223ED47-3E5E-4960-B70D-DFAF550F6666}.Release|x64.Build.0 = Release|Any CPU + {1223ED47-3E5E-4960-B70D-DFAF550F6666}.Release|x86.ActiveCfg = Release|Any CPU + {1223ED47-3E5E-4960-B70D-DFAF550F6666}.Release|x86.Build.0 = Release|Any CPU {AF205715-C8B7-42EF-BF14-AFC9E7F27242}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {AF205715-C8B7-42EF-BF14-AFC9E7F27242}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AF205715-C8B7-42EF-BF14-AFC9E7F27242}.Debug|x64.ActiveCfg = Debug|Any CPU + {AF205715-C8B7-42EF-BF14-AFC9E7F27242}.Debug|x64.Build.0 = Debug|Any CPU + {AF205715-C8B7-42EF-BF14-AFC9E7F27242}.Debug|x86.ActiveCfg = Debug|Any CPU + {AF205715-C8B7-42EF-BF14-AFC9E7F27242}.Debug|x86.Build.0 = Debug|Any CPU {AF205715-C8B7-42EF-BF14-AFC9E7F27242}.Release|Any CPU.ActiveCfg = Release|Any CPU {AF205715-C8B7-42EF-BF14-AFC9E7F27242}.Release|Any CPU.Build.0 = Release|Any CPU + {AF205715-C8B7-42EF-BF14-AFC9E7F27242}.Release|x64.ActiveCfg = Release|Any CPU + {AF205715-C8B7-42EF-BF14-AFC9E7F27242}.Release|x64.Build.0 = Release|Any CPU + {AF205715-C8B7-42EF-BF14-AFC9E7F27242}.Release|x86.ActiveCfg = Release|Any CPU + {AF205715-C8B7-42EF-BF14-AFC9E7F27242}.Release|x86.Build.0 = Release|Any CPU {635629BC-9D5C-40C6-BBD0-060550ECE290}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {635629BC-9D5C-40C6-BBD0-060550ECE290}.Debug|Any CPU.Build.0 = Debug|Any CPU + {635629BC-9D5C-40C6-BBD0-060550ECE290}.Debug|x64.ActiveCfg = Debug|Any CPU + {635629BC-9D5C-40C6-BBD0-060550ECE290}.Debug|x64.Build.0 = Debug|Any CPU + {635629BC-9D5C-40C6-BBD0-060550ECE290}.Debug|x86.ActiveCfg = Debug|Any CPU + {635629BC-9D5C-40C6-BBD0-060550ECE290}.Debug|x86.Build.0 = Debug|Any CPU {635629BC-9D5C-40C6-BBD0-060550ECE290}.Release|Any CPU.ActiveCfg = Release|Any CPU {635629BC-9D5C-40C6-BBD0-060550ECE290}.Release|Any CPU.Build.0 = Release|Any CPU + {635629BC-9D5C-40C6-BBD0-060550ECE290}.Release|x64.ActiveCfg = Release|Any CPU + {635629BC-9D5C-40C6-BBD0-060550ECE290}.Release|x64.Build.0 = Release|Any CPU + {635629BC-9D5C-40C6-BBD0-060550ECE290}.Release|x86.ActiveCfg = Release|Any CPU + {635629BC-9D5C-40C6-BBD0-060550ECE290}.Release|x86.Build.0 = Release|Any CPU {A2837F1C-3740-4375-9069-81AE32C867CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A2837F1C-3740-4375-9069-81AE32C867CA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A2837F1C-3740-4375-9069-81AE32C867CA}.Debug|x64.ActiveCfg = Debug|Any CPU + {A2837F1C-3740-4375-9069-81AE32C867CA}.Debug|x64.Build.0 = Debug|Any CPU + {A2837F1C-3740-4375-9069-81AE32C867CA}.Debug|x86.ActiveCfg = Debug|Any CPU + {A2837F1C-3740-4375-9069-81AE32C867CA}.Debug|x86.Build.0 = Debug|Any CPU {A2837F1C-3740-4375-9069-81AE32C867CA}.Release|Any CPU.ActiveCfg = Release|Any CPU {A2837F1C-3740-4375-9069-81AE32C867CA}.Release|Any CPU.Build.0 = Release|Any CPU + {A2837F1C-3740-4375-9069-81AE32C867CA}.Release|x64.ActiveCfg = Release|Any CPU + {A2837F1C-3740-4375-9069-81AE32C867CA}.Release|x64.Build.0 = Release|Any CPU + {A2837F1C-3740-4375-9069-81AE32C867CA}.Release|x86.ActiveCfg = Release|Any CPU + {A2837F1C-3740-4375-9069-81AE32C867CA}.Release|x86.Build.0 = Release|Any CPU + {A74FFAE5-9788-4C0D-9F77-AF6F6468A1A1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A74FFAE5-9788-4C0D-9F77-AF6F6468A1A1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A74FFAE5-9788-4C0D-9F77-AF6F6468A1A1}.Debug|x64.ActiveCfg = Debug|Any CPU + {A74FFAE5-9788-4C0D-9F77-AF6F6468A1A1}.Debug|x64.Build.0 = Debug|Any CPU + {A74FFAE5-9788-4C0D-9F77-AF6F6468A1A1}.Debug|x86.ActiveCfg = Debug|Any CPU + {A74FFAE5-9788-4C0D-9F77-AF6F6468A1A1}.Debug|x86.Build.0 = Debug|Any CPU + {A74FFAE5-9788-4C0D-9F77-AF6F6468A1A1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A74FFAE5-9788-4C0D-9F77-AF6F6468A1A1}.Release|Any CPU.Build.0 = Release|Any CPU + {A74FFAE5-9788-4C0D-9F77-AF6F6468A1A1}.Release|x64.ActiveCfg = Release|Any CPU + {A74FFAE5-9788-4C0D-9F77-AF6F6468A1A1}.Release|x64.Build.0 = Release|Any CPU + {A74FFAE5-9788-4C0D-9F77-AF6F6468A1A1}.Release|x86.ActiveCfg = Release|Any CPU + {A74FFAE5-9788-4C0D-9F77-AF6F6468A1A1}.Release|x86.Build.0 = Release|Any CPU + {AAB9DDFF-0A0A-43BE-BF00-2BA13ED526C8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AAB9DDFF-0A0A-43BE-BF00-2BA13ED526C8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AAB9DDFF-0A0A-43BE-BF00-2BA13ED526C8}.Debug|x64.ActiveCfg = Debug|Any CPU + {AAB9DDFF-0A0A-43BE-BF00-2BA13ED526C8}.Debug|x64.Build.0 = Debug|Any CPU + {AAB9DDFF-0A0A-43BE-BF00-2BA13ED526C8}.Debug|x86.ActiveCfg = Debug|Any CPU + {AAB9DDFF-0A0A-43BE-BF00-2BA13ED526C8}.Debug|x86.Build.0 = Debug|Any CPU + {AAB9DDFF-0A0A-43BE-BF00-2BA13ED526C8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AAB9DDFF-0A0A-43BE-BF00-2BA13ED526C8}.Release|Any CPU.Build.0 = Release|Any CPU + {AAB9DDFF-0A0A-43BE-BF00-2BA13ED526C8}.Release|x64.ActiveCfg = Release|Any CPU + {AAB9DDFF-0A0A-43BE-BF00-2BA13ED526C8}.Release|x64.Build.0 = Release|Any CPU + {AAB9DDFF-0A0A-43BE-BF00-2BA13ED526C8}.Release|x86.ActiveCfg = Release|Any CPU + {AAB9DDFF-0A0A-43BE-BF00-2BA13ED526C8}.Release|x86.Build.0 = Release|Any CPU + {D75C8112-6A4D-4A13-BB79-2D23DF66E4CB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D75C8112-6A4D-4A13-BB79-2D23DF66E4CB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D75C8112-6A4D-4A13-BB79-2D23DF66E4CB}.Debug|x64.ActiveCfg = Debug|Any CPU + {D75C8112-6A4D-4A13-BB79-2D23DF66E4CB}.Debug|x64.Build.0 = Debug|Any CPU + {D75C8112-6A4D-4A13-BB79-2D23DF66E4CB}.Debug|x86.ActiveCfg = Debug|Any CPU + {D75C8112-6A4D-4A13-BB79-2D23DF66E4CB}.Debug|x86.Build.0 = Debug|Any CPU + {D75C8112-6A4D-4A13-BB79-2D23DF66E4CB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D75C8112-6A4D-4A13-BB79-2D23DF66E4CB}.Release|Any CPU.Build.0 = Release|Any CPU + {D75C8112-6A4D-4A13-BB79-2D23DF66E4CB}.Release|x64.ActiveCfg = Release|Any CPU + {D75C8112-6A4D-4A13-BB79-2D23DF66E4CB}.Release|x64.Build.0 = Release|Any CPU + {D75C8112-6A4D-4A13-BB79-2D23DF66E4CB}.Release|x86.ActiveCfg = Release|Any CPU + {D75C8112-6A4D-4A13-BB79-2D23DF66E4CB}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -452,6 +946,9 @@ Global {AF205715-C8B7-42EF-BF14-AFC9E7F27242} = {2EEE6ABD-EE9B-473F-AF2D-6DABB85D7BA2} {635629BC-9D5C-40C6-BBD0-060550ECE290} = {2EEE6ABD-EE9B-473F-AF2D-6DABB85D7BA2} {A2837F1C-3740-4375-9069-81AE32C867CA} = {E6CEAD8D-F565-471E-A0DC-676F54EAEDEB} + {A74FFAE5-9788-4C0D-9F77-AF6F6468A1A1} = {6803696C-B19A-4B27-9193-082A02B6F205} + {AAB9DDFF-0A0A-43BE-BF00-2BA13ED526C8} = {E6CEAD8D-F565-471E-A0DC-676F54EAEDEB} + {D75C8112-6A4D-4A13-BB79-2D23DF66E4CB} = {5080DB09-CBE8-4C45-9957-C3BB7651755E} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {DC0FA8D3-6449-4FDA-BB46-ECF58FAD23B4} diff --git a/docs/azure-ai-foundry.md b/docs/azure-ai-foundry.md new file mode 100644 index 000000000..29e3ed799 --- /dev/null +++ b/docs/azure-ai-foundry.md @@ -0,0 +1,452 @@ +# Azure AI Foundry Extension + +The Azure AI Foundry extension enables building AI-powered, agentic workflows with WorkflowCore. It provides workflow steps for LLM invocation, automatic tool execution, embeddings, vector search, and human-in-the-loop review patterns. + +## Installation + +```bash +dotnet add package WorkflowCore.AI.AzureFoundry +``` + +## Overview + +This extension adds six new workflow step types: + +| Step | Description | +|------|-------------| +| `ChatCompletion` | Invoke LLMs with conversation history | +| `AgentLoop` | Agentic workflows with automatic tool calling | +| `ExecuteTool` | Manual tool execution | +| `GenerateEmbedding` | Create vector embeddings | +| `VectorSearch` | Semantic search with Azure AI Search | +| `HumanReview` | Pause for human approval | + +## Configuration + +### Basic Setup + +```csharp +services.AddWorkflow(); + +services.AddAzureFoundry(options => +{ + options.Endpoint = "https://myresource.services.ai.azure.com"; + options.ApiKey = "your-api-key"; + options.DefaultModel = "gpt-4o"; +}); +``` + +### Configuration Options + +| Option | Type | Description | +|--------|------|-------------| +| `Endpoint` | string | Azure AI Foundry endpoint URL | +| `ApiKey` | string | API key for authentication | +| `Credential` | TokenCredential | Azure AD credential (alternative to ApiKey) | +| `DefaultModel` | string | Default LLM model name | +| `DefaultEmbeddingModel` | string | Default embedding model | +| `DefaultTemperature` | float | Default creativity level (0-1) | +| `DefaultMaxTokens` | int | Default response token limit | +| `SearchEndpoint` | string | Azure AI Search endpoint (optional) | +| `SearchApiKey` | string | Azure AI Search API key (optional) | + +## Chat Completion + +The simplest way to invoke an LLM in your workflow: + +```csharp +public class SimpleChatWorkflow : IWorkflow +{ + public void Build(IWorkflowBuilder builder) + { + builder + .StartWith(context => ExecutionResult.Next()) + .ChatCompletion(cfg => cfg + .SystemPrompt("You are a helpful assistant") + .UserMessage(data => data.Question) + .OutputTo(data => data.Answer)); + } +} +``` + +### With Conversation History + +Enable multi-turn conversations: + +```csharp +.ChatCompletion(cfg => cfg + .SystemPrompt("You are a helpful assistant") + .UserMessage(data => data.Question) + .WithHistory() // Maintains conversation context + .OutputTo(data => data.Answer)); +``` + +## Agentic Workflows + +The `AgentLoop` step enables autonomous AI agents that can use tools to accomplish tasks: + +```csharp +public class SupportAgentWorkflow : IWorkflow +{ + public void Build(IWorkflowBuilder builder) + { + builder + .StartWith(context => ExecutionResult.Next()) + .AgentLoop(cfg => cfg + .SystemPrompt(@"You are a customer support agent. + Use the available tools to help customers. + Always search the knowledge base before answering.") + .Message(data => data.CustomerQuery) + .WithTool() + .WithTool() + .WithTool() + .MaxIterations(10) + .OutputTo(data => data.Response)); + } +} +``` + +### How Agent Loop Works + +1. The LLM receives the user message and tool definitions +2. If the LLM decides to use a tool, it returns a tool call request +3. The step executes the tool and feeds the result back to the LLM +4. This continues until the LLM provides a final response (or max iterations) + +``` +User Message → LLM → Tool Call → Tool Execution → Result → LLM → ... → Final Response +``` + +## Creating Tools + +Tools extend the LLM's capabilities by allowing it to take actions: + +```csharp +public class SearchKnowledgeBase : IAgentTool +{ + private readonly IKnowledgeBaseService _kb; + + public SearchKnowledgeBase(IKnowledgeBaseService kb) + { + _kb = kb; + } + + public string Name => "search_knowledge_base"; + + public string Description => + "Search the knowledge base for articles matching the query"; + + public string ParametersSchema => @"{ + ""type"": ""object"", + ""properties"": { + ""query"": { + ""type"": ""string"", + ""description"": ""Search query"" + }, + ""category"": { + ""type"": ""string"", + ""description"": ""Optional category filter"" + } + }, + ""required"": [""query""] + }"; + + public async Task ExecuteAsync( + string toolCallId, + string arguments, + CancellationToken ct) + { + var args = JsonSerializer.Deserialize(arguments); + var results = await _kb.SearchAsync(args.Query, args.Category, ct); + + if (results.Any()) + { + return ToolResult.Succeeded( + toolCallId, + Name, + JsonSerializer.Serialize(results)); + } + + return ToolResult.Succeeded( + toolCallId, + Name, + "No articles found matching the query."); + } +} +``` + +### Registering Tools + +```csharp +// In your DI setup +services.AddSingleton(); +services.AddSingleton(); + +// After building service provider +var toolRegistry = serviceProvider.GetRequiredService(); +toolRegistry.Register(serviceProvider.GetRequiredService()); +toolRegistry.Register(serviceProvider.GetRequiredService()); +``` + +## Human-in-the-Loop + +For workflows requiring human oversight of AI outputs: + +```csharp +public class ContentReviewWorkflow : IWorkflow +{ + public void Build(IWorkflowBuilder builder) + { + builder + .StartWith(context => ExecutionResult.Next()) + + // Generate content with AI + .ChatCompletion(cfg => cfg + .SystemPrompt("Generate marketing copy for the product") + .UserMessage(data => data.ProductDescription) + .OutputTo(data => data.DraftContent)) + + // Human reviews before publishing + .HumanReview(cfg => cfg + .Content(data => data.DraftContent) + .Reviewer(data => data.AssignedEditor) + .Prompt("Review this AI-generated marketing copy") + .OnApproved(data => data.ApprovedContent) + .OnDecision(data => data.ReviewDecision)) + + // Continue based on decision + .If(data => data.ReviewDecision == ReviewDecision.Approved) + .Do(then => then + .Then() + .Input(step => step.Content, data => data.ApprovedContent)); + } +} +``` + +### Getting the Event Key + +There are two ways to get the event key for completing a review: + +**Option 1: Use the workflow ID (simplest)** + +By default, if you don't provide a `CorrelationId`, the event key equals the workflow ID: + +```csharp +// Start workflow +var workflowId = await host.StartWorkflow("ContentReview", data); + +// Later, complete the review using workflowId as the event key +await host.PublishEvent("HumanReview", workflowId, reviewAction); +``` + +**Option 2: Use a custom correlation ID** + +Provide your own correlation ID (e.g., a ticket ID, request ID) for easier integration: + +```csharp +// In your workflow +.HumanReview(cfg => cfg + .Content(data => data.DraftContent) + .CorrelationId(data => data.TicketId) // Use your own ID + .OnApproved(data => data.ApprovedContent)) + +// Complete the review using your known ID +await host.PublishEvent("HumanReview", "TICKET-12345", reviewAction); +``` + +**Option 3: Capture the event key in workflow data** + +Output the event key to your workflow data for later use: + +```csharp +.HumanReview(cfg => cfg + .Content(data => data.DraftContent) + .OnEventKey(data => data.ReviewEventKey) // Capture the key + .OnApproved(data => data.ApprovedContent)) +``` + +### Completing Reviews + +From your UI or API, publish an event to complete the review: + +```csharp +await workflowHost.PublishEvent( + "HumanReview", + eventKey, // The workflow ID, custom correlation ID, or captured event key + new ReviewAction + { + Decision = ReviewDecision.Approved, + Reviewer = "editor@example.com", + Comments = "Approved with minor edits", + ModifiedContent = "Updated content..." // Optional, for modifications + }); +``` + +## RAG (Retrieval-Augmented Generation) + +Combine vector search with LLM generation for knowledge-grounded responses: + +```csharp +public class RAGWorkflow : IWorkflow +{ + public void Build(IWorkflowBuilder builder) + { + builder + .StartWith(context => ExecutionResult.Next()) + + // Search for relevant documents + .VectorSearch(cfg => cfg + .Input(s => s.Query, data => data.UserQuestion) + .Input(s => s.IndexName, data => "company-docs") + .Input(s => s.TopK, data => 5) + .Output(s => s.Results, data => data.RelevantDocs)) + + // Generate answer grounded in documents + .ChatCompletion(cfg => cfg + .SystemPrompt(data => $@"Answer based on these documents: + {string.Join("\n", data.RelevantDocs.Select(d => d.Content))} + If the answer isn't in the documents, say so.") + .UserMessage(data => data.UserQuestion) + .OutputTo(data => data.Answer)); + } +} +``` + +## Embeddings + +Generate embeddings for semantic search or similarity: + +```csharp +.GenerateEmbedding(cfg => cfg + .Input(s => s.Text, data => data.Document) + .Output(s => s.Embedding, data => data.DocumentVector)); +``` + +## Authentication + +### API Key (Simplest) + +```csharp +options.ApiKey = Environment.GetEnvironmentVariable("AZURE_AI_API_KEY"); +``` + +### Managed Identity (Production) + +```csharp +options.Credential = new ManagedIdentityCredential(); +``` + +### Service Principal + +```csharp +options.Credential = new ClientSecretCredential( + tenantId: "your-tenant-id", + clientId: "your-client-id", + clientSecret: "your-client-secret" +); +``` + +## Best Practices + +### 1. Set Iteration Limits + +Always set `MaxIterations` on `AgentLoop` to prevent runaway costs: + +```csharp +.AgentLoop(cfg => cfg + .MaxIterations(10) // Stop after 10 LLM calls + ...); +``` + +### 2. Write Clear Tool Descriptions + +The LLM uses descriptions to decide when to use tools: + +```csharp +// ❌ Bad +public string Description => "Gets weather"; + +// ✅ Good +public string Description => + "Get the current weather conditions for a specific city. " + + "Returns temperature, humidity, and conditions."; +``` + +### 3. Use System Prompts Effectively + +Guide the agent's behavior with clear instructions: + +```csharp +.AgentLoop(cfg => cfg + .SystemPrompt(@"You are a customer support agent. + + Guidelines: + 1. Always be polite and professional + 2. Search the knowledge base before answering + 3. If you can't help, create a support ticket + 4. Never share sensitive customer data") + ...); +``` + +### 4. Track Token Usage + +Monitor costs by tracking token consumption: + +```csharp +.ChatCompletion(cfg => cfg + ... + .OutputTokensTo(data => data.TokensUsed)); + +// In your application +logger.LogInformation("Request used {Tokens} tokens", data.TokensUsed); +``` + +### 5. Handle Tool Errors Gracefully + +Return meaningful error messages from tools: + +```csharp +public async Task ExecuteAsync(...) +{ + try + { + var result = await DoWork(); + return ToolResult.Succeeded(id, Name, result); + } + catch (NotFoundException) + { + return ToolResult.Succeeded(id, Name, + "No results found. Try a different search query."); + } + catch (Exception ex) + { + logger.LogError(ex, "Tool execution failed"); + return ToolResult.Failed(id, Name, + "An error occurred. Please try again."); + } +} +``` + +## Samples + +See the [sample project](https://github.com/danielgerlag/workflow-core/tree/master/src/samples/WorkflowCore.Sample.AzureFoundry) for complete working examples. + +## Troubleshooting + +### 404 Resource Not Found + +Ensure your endpoint ends correctly: +- Azure AI Foundry: `https://resource.services.ai.azure.com` +- The extension automatically appends `/models` to the endpoint + +### Authentication Errors + +1. Verify your API key or credentials +2. Check that your Azure AD app has the required permissions +3. For managed identity, ensure the identity has access to the AI resource + +### Tool Not Being Called + +1. Check the tool description is clear about when to use it +2. Verify the tool is registered in the `IToolRegistry` +3. Check the tool's `ParametersSchema` is valid JSON Schema diff --git a/docs/extensions.md b/docs/extensions.md index e370f2b81..ceae2a382 100644 --- a/docs/extensions.md +++ b/docs/extensions.md @@ -1,3 +1,4 @@ ## Extensions -* [User (human) workflows](https://github.com/danielgerlag/workflow-core/tree/master/src/extensions/WorkflowCore.Users) \ No newline at end of file +* [User (human) workflows](https://github.com/danielgerlag/workflow-core/tree/master/src/extensions/WorkflowCore.Users) +* [Azure AI Foundry](azure-ai-foundry.md) - AI-powered agentic workflows with LLM invocation, tool execution, and human-in-the-loop patterns \ No newline at end of file diff --git a/docs/samples.md b/docs/samples.md index f69290c57..39f1e360e 100644 --- a/docs/samples.md +++ b/docs/samples.md @@ -35,3 +35,10 @@ [Human(User) Workflow](https://github.com/danielgerlag/workflow-core/tree/master/src/samples/WorkflowCore.Sample08) [Workflow Middleware](https://github.com/danielgerlag/workflow-core/tree/master/src/samples/WorkflowCore.Sample19) + +## AI & Agentic Workflow Samples + +[Azure AI Foundry - Chat, Agents & Tools](https://github.com/danielgerlag/workflow-core/tree/master/src/samples/WorkflowCore.Sample.AzureFoundry) - Interactive sample demonstrating: + - Simple LLM chat completion + - Agentic workflows with automatic tool execution (weather, calculator) + - Human-in-the-loop approval workflows diff --git a/mkdocs.yml b/mkdocs.yml index 57ed12c94..5c2da328d 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -15,5 +15,6 @@ nav: - Elasticsearch plugin: elastic-search.md - Test helpers: test-helpers.md - Extensions: extensions.md + - Azure AI Foundry: azure-ai-foundry.md - Samples: samples.md theme: readthedocs diff --git a/src/extensions/WorkflowCore.AI.AzureFoundry/CHANGELOG.md b/src/extensions/WorkflowCore.AI.AzureFoundry/CHANGELOG.md new file mode 100644 index 000000000..6753b41ca --- /dev/null +++ b/src/extensions/WorkflowCore.AI.AzureFoundry/CHANGELOG.md @@ -0,0 +1,92 @@ +# Changelog + +All notable changes to WorkflowCore.AI.AzureFoundry will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [1.0.0-beta.1] - 2026-01-27 + +### Added + +- **ChatCompletion Step** - Invoke Azure AI models with conversation history support + - Configurable system prompts, temperature, and max tokens + - Automatic conversation history management + - Token usage tracking for cost monitoring + +- **AgentLoop Step** - Agentic workflows with automatic tool execution + - LLM-driven tool selection and invocation + - Configurable iteration limits to prevent runaway loops + - Support for both automatic and manual tool execution modes + - Tool result tracking and debugging + +- **ExecuteTool Step** - Manual tool execution for fine-grained control + - Direct tool invocation by name with JSON arguments + - Error handling with success/failure results + +- **GenerateEmbedding Step** - Vector embedding generation + - Support for Azure AI embedding models + - Configurable model selection + - Token usage tracking + +- **VectorSearch Step** - Semantic search with Azure AI Search + - Vector similarity search + - OData filter support + - Configurable result count (TopK) + +- **HumanReview Step** - Human-in-the-loop approval workflows + - Pause workflow for human review + - Support for approve, reject, and modify actions + - Configurable reviewer assignment and prompts + +- **Tool Framework** + - `IAgentTool` interface for custom tool implementations + - `IToolRegistry` for tool registration and discovery + - JSON Schema parameter definitions for tool calling + - `ToolResult` with success/failure states + +- **Conversation History Management** + - `IConversationStore` abstraction for pluggable storage + - `InMemoryConversationStore` default implementation + - Automatic thread management per workflow execution + - `ConversationMessage` and `ConversationThread` models + +- **Azure AI Foundry Integration** + - Support for Azure AI Foundry (`services.ai.azure.com`) endpoints + - API key and Azure AD authentication + - Configurable default models and parameters + - Azure AI Search integration for RAG scenarios + +- **Fluent Builder API** + - `ChatCompletion()` extension method + - `AgentLoop()` extension method + - `GenerateEmbedding()` extension method + - `VectorSearch()` extension method + - `HumanReview()` extension method + +### Dependencies + +- Azure.AI.Inference 1.0.0-beta.5 +- Azure.AI.Projects 1.0.0-beta.2 +- Azure.Identity 1.13.0 +- Azure.Search.Documents 11.6.0 + +### Notes + +- This is a beta release - APIs may change before 1.0.0 stable +- Requires .NET Standard 2.0 or higher +- Compatible with WorkflowCore 3.x + +--- + +## [Unreleased] + +### Planned Features + +- Streaming response support for real-time output +- Structured output with JSON schema validation +- Vision/multimodal input support +- OpenTelemetry tracing integration +- Rate limiting and retry configuration +- Batch embedding generation +- More conversation store implementations (Redis, SQL, CosmosDB) diff --git a/src/extensions/WorkflowCore.AI.AzureFoundry/Interface/IAgentLoopBuilder.cs b/src/extensions/WorkflowCore.AI.AzureFoundry/Interface/IAgentLoopBuilder.cs new file mode 100644 index 000000000..942979745 --- /dev/null +++ b/src/extensions/WorkflowCore.AI.AzureFoundry/Interface/IAgentLoopBuilder.cs @@ -0,0 +1,63 @@ +using System; +using System.Linq.Expressions; +using WorkflowCore.AI.AzureFoundry.Primitives; +using WorkflowCore.Interface; + +namespace WorkflowCore.AI.AzureFoundry.Interface +{ + /// + /// Builder interface for configuring AgentLoop steps + /// + public interface IAgentLoopBuilder : IStepBuilder + { + /// + /// Set the system prompt + /// + IAgentLoopBuilder SystemPrompt(string prompt); + + /// + /// Set the system prompt from workflow data + /// + IAgentLoopBuilder SystemPrompt(Expression> expression); + + /// + /// Set the user message + /// + IAgentLoopBuilder Message(string message); + + /// + /// Set the user message from workflow data + /// + IAgentLoopBuilder Message(Expression> expression); + + /// + /// Set the model to use + /// + IAgentLoopBuilder Model(string model); + + /// + /// Set maximum iterations + /// + IAgentLoopBuilder MaxIterations(int maxIterations); + + /// + /// Add a tool by type + /// + IAgentLoopBuilder WithTool() where TTool : IAgentTool; + + /// + /// Add a tool by name + /// + IAgentLoopBuilder WithTool(string toolName); + + /// + /// Enable/disable automatic tool execution + /// + IAgentLoopBuilder AutoExecuteTools(bool auto = true); + + /// + /// Output the response to workflow data + /// + IAgentLoopBuilder OutputTo(Expression> expression); + } +} diff --git a/src/extensions/WorkflowCore.AI.AzureFoundry/Interface/IAgentTool.cs b/src/extensions/WorkflowCore.AI.AzureFoundry/Interface/IAgentTool.cs new file mode 100644 index 000000000..57f2dd172 --- /dev/null +++ b/src/extensions/WorkflowCore.AI.AzureFoundry/Interface/IAgentTool.cs @@ -0,0 +1,35 @@ +using System.Threading; +using System.Threading.Tasks; +using WorkflowCore.AI.AzureFoundry.Models; + +namespace WorkflowCore.AI.AzureFoundry.Interface +{ + /// + /// Interface for tools that can be invoked by the LLM + /// + public interface IAgentTool + { + /// + /// Name of the tool (must be unique) + /// + string Name { get; } + + /// + /// Description of what the tool does (used by the LLM to decide when to use it) + /// + string Description { get; } + + /// + /// JSON schema for the tool's parameters + /// + string ParametersSchema { get; } + + /// + /// Execute the tool with the given arguments + /// + /// JSON string containing the tool arguments + /// Cancellation token + /// Tool execution result + Task ExecuteAsync(string toolCallId, string arguments, CancellationToken cancellationToken = default); + } +} diff --git a/src/extensions/WorkflowCore.AI.AzureFoundry/Interface/IChatCompletionBuilder.cs b/src/extensions/WorkflowCore.AI.AzureFoundry/Interface/IChatCompletionBuilder.cs new file mode 100644 index 000000000..19ed2eef4 --- /dev/null +++ b/src/extensions/WorkflowCore.AI.AzureFoundry/Interface/IChatCompletionBuilder.cs @@ -0,0 +1,68 @@ +using System; +using System.Linq.Expressions; +using WorkflowCore.AI.AzureFoundry.Primitives; +using WorkflowCore.Interface; + +namespace WorkflowCore.AI.AzureFoundry.Interface +{ + /// + /// Builder interface for configuring ChatCompletion steps + /// + public interface IChatCompletionBuilder : IStepBuilder + { + /// + /// Set the system prompt + /// + IChatCompletionBuilder SystemPrompt(string prompt); + + /// + /// Set the system prompt from workflow data + /// + IChatCompletionBuilder SystemPrompt(Expression> expression); + + /// + /// Set the user message + /// + IChatCompletionBuilder UserMessage(string message); + + /// + /// Set the user message from workflow data + /// + IChatCompletionBuilder UserMessage(Expression> expression); + + /// + /// Set the model to use + /// + IChatCompletionBuilder Model(string model); + + /// + /// Set the temperature + /// + IChatCompletionBuilder Temperature(float temperature); + + /// + /// Set the max tokens + /// + IChatCompletionBuilder MaxTokens(int maxTokens); + + /// + /// Include conversation history + /// + IChatCompletionBuilder WithHistory(bool include = true); + + /// + /// Set the thread ID for conversation history + /// + IChatCompletionBuilder ThreadId(Expression> expression); + + /// + /// Output the response to workflow data + /// + IChatCompletionBuilder OutputTo(Expression> expression); + + /// + /// Output token usage to workflow data + /// + IChatCompletionBuilder OutputTokensTo(Expression> expression); + } +} diff --git a/src/extensions/WorkflowCore.AI.AzureFoundry/Interface/IChatCompletionService.cs b/src/extensions/WorkflowCore.AI.AzureFoundry/Interface/IChatCompletionService.cs new file mode 100644 index 000000000..96aeee867 --- /dev/null +++ b/src/extensions/WorkflowCore.AI.AzureFoundry/Interface/IChatCompletionService.cs @@ -0,0 +1,66 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using WorkflowCore.AI.AzureFoundry.Models; + +namespace WorkflowCore.AI.AzureFoundry.Interface +{ + /// + /// Service for chat completion operations + /// + public interface IChatCompletionService + { + /// + /// Complete a chat conversation + /// + /// Conversation messages + /// Model to use (null for default) + /// Temperature (null for default) + /// Max tokens (null for default) + /// Available tools for the LLM to call + /// Cancellation token + Task CompleteAsync( + IEnumerable messages, + string model = null, + float? temperature = null, + int? maxTokens = null, + IEnumerable tools = null, + CancellationToken cancellationToken = default); + } + + /// + /// Response from a chat completion request + /// + public class ChatCompletionResponse + { + /// + /// The message generated by the model + /// + public ConversationMessage Message { get; set; } + + /// + /// Reason the completion finished (stop, tool_calls, length, content_filter) + /// + public string FinishReason { get; set; } + + /// + /// Number of tokens in the prompt + /// + public int PromptTokens { get; set; } + + /// + /// Number of tokens in the completion + /// + public int CompletionTokens { get; set; } + + /// + /// Total tokens used + /// + public int TotalTokens => PromptTokens + CompletionTokens; + + /// + /// Model used for the completion + /// + public string Model { get; set; } + } +} diff --git a/src/extensions/WorkflowCore.AI.AzureFoundry/Interface/IConversationStore.cs b/src/extensions/WorkflowCore.AI.AzureFoundry/Interface/IConversationStore.cs new file mode 100644 index 000000000..da3f87a62 --- /dev/null +++ b/src/extensions/WorkflowCore.AI.AzureFoundry/Interface/IConversationStore.cs @@ -0,0 +1,31 @@ +using System.Threading.Tasks; +using WorkflowCore.AI.AzureFoundry.Models; + +namespace WorkflowCore.AI.AzureFoundry.Interface +{ + /// + /// Abstraction for storing and retrieving conversation threads + /// + public interface IConversationStore + { + /// + /// Get a conversation thread by ID + /// + Task GetThreadAsync(string threadId); + + /// + /// Get or create a thread for a workflow execution pointer + /// + Task GetOrCreateThreadAsync(string workflowInstanceId, string executionPointerId); + + /// + /// Save a conversation thread + /// + Task SaveThreadAsync(ConversationThread thread); + + /// + /// Delete a conversation thread + /// + Task DeleteThreadAsync(string threadId); + } +} diff --git a/src/extensions/WorkflowCore.AI.AzureFoundry/Interface/IEmbeddingService.cs b/src/extensions/WorkflowCore.AI.AzureFoundry/Interface/IEmbeddingService.cs new file mode 100644 index 000000000..e6689a334 --- /dev/null +++ b/src/extensions/WorkflowCore.AI.AzureFoundry/Interface/IEmbeddingService.cs @@ -0,0 +1,49 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace WorkflowCore.AI.AzureFoundry.Interface +{ + /// + /// Service for generating embeddings + /// + public interface IEmbeddingService + { + /// + /// Generate an embedding vector for the given text + /// + /// Text to embed + /// Model to use (null for default) + /// Cancellation token + /// Embedding vector + Task GenerateEmbeddingAsync( + string text, + string model = null, + CancellationToken cancellationToken = default); + } + + /// + /// Response from an embedding request + /// + public class EmbeddingResponse + { + /// + /// The embedding vector + /// + public float[] Embedding { get; set; } + + /// + /// Dimensionality of the embedding + /// + public int Dimensions => Embedding?.Length ?? 0; + + /// + /// Model used to generate the embedding + /// + public string Model { get; set; } + + /// + /// Tokens used + /// + public int TokensUsed { get; set; } + } +} diff --git a/src/extensions/WorkflowCore.AI.AzureFoundry/Interface/IHumanReviewBuilder.cs b/src/extensions/WorkflowCore.AI.AzureFoundry/Interface/IHumanReviewBuilder.cs new file mode 100644 index 000000000..910ee0d92 --- /dev/null +++ b/src/extensions/WorkflowCore.AI.AzureFoundry/Interface/IHumanReviewBuilder.cs @@ -0,0 +1,54 @@ +using System; +using System.Linq.Expressions; +using WorkflowCore.AI.AzureFoundry.Models; +using WorkflowCore.AI.AzureFoundry.Primitives; +using WorkflowCore.Interface; + +namespace WorkflowCore.AI.AzureFoundry.Interface +{ + /// + /// Builder interface for configuring HumanReview steps + /// + public interface IHumanReviewBuilder : IStepBuilder + { + /// + /// Set the content to be reviewed + /// + IHumanReviewBuilder Content(Expression> expression); + + /// + /// Set the reviewer + /// + IHumanReviewBuilder Reviewer(Expression> expression); + + /// + /// Set the review prompt/instructions + /// + IHumanReviewBuilder Prompt(string prompt); + + /// + /// Set a custom correlation ID for the event key. + /// This allows you to use a known value (e.g., ticket ID, request ID) + /// to later complete the review via PublishEvent. + /// If not set, defaults to the workflow ID. + /// + IHumanReviewBuilder CorrelationId(Expression> expression); + + /// + /// Output the event key to workflow data. + /// Use this value to later complete the review via: + /// workflowHost.PublishEvent("HumanReview", eventKey, reviewAction) + /// + IHumanReviewBuilder OnEventKey(Expression> expression); + + /// + /// Output the approved content to workflow data + /// + IHumanReviewBuilder OnApproved(Expression> expression); + + /// + /// Output the decision to workflow data + /// + IHumanReviewBuilder OutputDecisionTo(Expression> expression); + } +} diff --git a/src/extensions/WorkflowCore.AI.AzureFoundry/Interface/ISearchService.cs b/src/extensions/WorkflowCore.AI.AzureFoundry/Interface/ISearchService.cs new file mode 100644 index 000000000..80805fd49 --- /dev/null +++ b/src/extensions/WorkflowCore.AI.AzureFoundry/Interface/ISearchService.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using WorkflowCore.AI.AzureFoundry.Models; + +namespace WorkflowCore.AI.AzureFoundry.Interface +{ + /// + /// Service for vector search operations + /// + public interface ISearchService + { + /// + /// Search for documents using a text query (will be embedded automatically) + /// + /// Name of the search index + /// Text query + /// Number of results to return + /// Optional OData filter expression + /// Cancellation token + Task SearchAsync( + string indexName, + string query, + int topK = 5, + string filter = null, + CancellationToken cancellationToken = default); + + /// + /// Search for documents using a pre-computed embedding vector + /// + /// Name of the search index + /// Embedding vector + /// Number of results to return + /// Optional OData filter expression + /// Cancellation token + Task SearchByVectorAsync( + string indexName, + float[] embedding, + int topK = 5, + string filter = null, + CancellationToken cancellationToken = default); + } +} diff --git a/src/extensions/WorkflowCore.AI.AzureFoundry/Interface/IToolRegistry.cs b/src/extensions/WorkflowCore.AI.AzureFoundry/Interface/IToolRegistry.cs new file mode 100644 index 000000000..74eefd8e2 --- /dev/null +++ b/src/extensions/WorkflowCore.AI.AzureFoundry/Interface/IToolRegistry.cs @@ -0,0 +1,41 @@ +using System.Collections.Generic; +using WorkflowCore.AI.AzureFoundry.Models; + +namespace WorkflowCore.AI.AzureFoundry.Interface +{ + /// + /// Registry for agent tools + /// + public interface IToolRegistry + { + /// + /// Register a tool + /// + void Register(IAgentTool tool); + + /// + /// Register a tool by type + /// + void Register() where T : IAgentTool; + + /// + /// Get a tool by name + /// + IAgentTool GetTool(string name); + + /// + /// Get all registered tools + /// + IEnumerable GetAllTools(); + + /// + /// Get tool definitions for all registered tools + /// + IEnumerable GetToolDefinitions(); + + /// + /// Check if a tool is registered + /// + bool HasTool(string name); + } +} diff --git a/src/extensions/WorkflowCore.AI.AzureFoundry/Models/AzureFoundryOptions.cs b/src/extensions/WorkflowCore.AI.AzureFoundry/Models/AzureFoundryOptions.cs new file mode 100644 index 000000000..b3e099341 --- /dev/null +++ b/src/extensions/WorkflowCore.AI.AzureFoundry/Models/AzureFoundryOptions.cs @@ -0,0 +1,58 @@ +using System; +using Azure.Core; + +namespace WorkflowCore.AI.AzureFoundry.Models +{ + public class AzureFoundryOptions + { + /// + /// Azure AI Foundry endpoint URL (e.g., "https://myresource.services.ai.azure.com") + /// + public string Endpoint { get; set; } + + /// + /// Azure AI Foundry project name + /// + public string ProjectName { get; set; } + + /// + /// API key for authentication (if not using Azure credentials) + /// + public string ApiKey { get; set; } + + /// + /// Default model to use for chat completions (e.g., "gpt-4o") + /// + public string DefaultModel { get; set; } = "gpt-4o"; + + /// + /// Default model to use for embeddings (e.g., "text-embedding-3-small") + /// + public string DefaultEmbeddingModel { get; set; } = "text-embedding-3-small"; + + /// + /// Azure credential for authentication. If null and ApiKey is null, DefaultAzureCredential will be used. + /// + public TokenCredential Credential { get; set; } + + /// + /// Default temperature for LLM calls (0.0 - 2.0) + /// + public float DefaultTemperature { get; set; } = 0.7f; + + /// + /// Default maximum tokens for LLM responses + /// + public int DefaultMaxTokens { get; set; } = 4096; + + /// + /// Azure AI Search endpoint for vector search operations + /// + public string SearchEndpoint { get; set; } + + /// + /// Azure AI Search API key (optional, uses DefaultAzureCredential if not provided) + /// + public string SearchApiKey { get; set; } + } +} diff --git a/src/extensions/WorkflowCore.AI.AzureFoundry/Models/ChatCompletionResult.cs b/src/extensions/WorkflowCore.AI.AzureFoundry/Models/ChatCompletionResult.cs new file mode 100644 index 000000000..f6ad7d1e1 --- /dev/null +++ b/src/extensions/WorkflowCore.AI.AzureFoundry/Models/ChatCompletionResult.cs @@ -0,0 +1,38 @@ +namespace WorkflowCore.AI.AzureFoundry.Models +{ + /// + /// Result from a chat completion request + /// + public class ChatCompletionResult + { + /// + /// The generated response text + /// + public string Response { get; set; } + + /// + /// Reason the completion finished + /// + public string FinishReason { get; set; } + + /// + /// Number of tokens in the prompt + /// + public int PromptTokens { get; set; } + + /// + /// Number of tokens in the completion + /// + public int CompletionTokens { get; set; } + + /// + /// Total tokens used + /// + public int TotalTokens => PromptTokens + CompletionTokens; + + /// + /// Model used for the completion + /// + public string Model { get; set; } + } +} diff --git a/src/extensions/WorkflowCore.AI.AzureFoundry/Models/ConversationMessage.cs b/src/extensions/WorkflowCore.AI.AzureFoundry/Models/ConversationMessage.cs new file mode 100644 index 000000000..1cfa2e65e --- /dev/null +++ b/src/extensions/WorkflowCore.AI.AzureFoundry/Models/ConversationMessage.cs @@ -0,0 +1,88 @@ +using System; +using System.Collections.Generic; + +namespace WorkflowCore.AI.AzureFoundry.Models +{ + /// + /// Represents a single message in a conversation thread + /// + public class ConversationMessage + { + /// + /// Unique identifier for this message + /// + public string Id { get; set; } = Guid.NewGuid().ToString(); + + /// + /// Role of the message sender (system, user, assistant, tool) + /// + public MessageRole Role { get; set; } + + /// + /// Text content of the message + /// + public string Content { get; set; } + + /// + /// Name of the tool that produced this message (for tool role) + /// + public string ToolName { get; set; } + + /// + /// Tool call ID this message is responding to (for tool role) + /// + public string ToolCallId { get; set; } + + /// + /// Tool calls requested by the assistant + /// + public IList ToolCalls { get; set; } + + /// + /// When the message was created + /// + public DateTime Timestamp { get; set; } = DateTime.UtcNow; + + /// + /// Additional metadata for the message + /// + public IDictionary Metadata { get; set; } = new Dictionary(); + + /// + /// Token count for this message (if available) + /// + public int? TokenCount { get; set; } + } + + /// + /// Role of a conversation message + /// + public enum MessageRole + { + System, + User, + Assistant, + Tool + } + + /// + /// Represents a tool call request from the LLM + /// + public class ToolCallRequest + { + /// + /// Unique identifier for this tool call + /// + public string Id { get; set; } + + /// + /// Name of the tool to invoke + /// + public string ToolName { get; set; } + + /// + /// JSON arguments for the tool + /// + public string Arguments { get; set; } + } +} diff --git a/src/extensions/WorkflowCore.AI.AzureFoundry/Models/ConversationThread.cs b/src/extensions/WorkflowCore.AI.AzureFoundry/Models/ConversationThread.cs new file mode 100644 index 000000000..cc2410bc6 --- /dev/null +++ b/src/extensions/WorkflowCore.AI.AzureFoundry/Models/ConversationThread.cs @@ -0,0 +1,115 @@ +using System; +using System.Collections.Generic; + +namespace WorkflowCore.AI.AzureFoundry.Models +{ + /// + /// Represents a conversation thread containing multiple messages + /// + public class ConversationThread + { + /// + /// Unique identifier for this conversation thread + /// + public string Id { get; set; } = Guid.NewGuid().ToString(); + + /// + /// Associated workflow instance ID + /// + public string WorkflowInstanceId { get; set; } + + /// + /// Associated execution pointer ID + /// + public string ExecutionPointerId { get; set; } + + /// + /// Messages in the conversation + /// + public IList Messages { get; set; } = new List(); + + /// + /// When the thread was created + /// + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + + /// + /// When the thread was last updated + /// + public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; + + /// + /// Additional metadata for the thread + /// + public IDictionary Metadata { get; set; } = new Dictionary(); + + /// + /// Total token count across all messages + /// + public int TotalTokens { get; set; } + + /// + /// Add a message to the thread + /// + public void AddMessage(ConversationMessage message) + { + Messages.Add(message); + UpdatedAt = DateTime.UtcNow; + if (message.TokenCount.HasValue) + { + TotalTokens += message.TokenCount.Value; + } + } + + /// + /// Add a system message + /// + public void AddSystemMessage(string content) + { + AddMessage(new ConversationMessage + { + Role = MessageRole.System, + Content = content + }); + } + + /// + /// Add a user message + /// + public void AddUserMessage(string content) + { + AddMessage(new ConversationMessage + { + Role = MessageRole.User, + Content = content + }); + } + + /// + /// Add an assistant message + /// + public void AddAssistantMessage(string content, IList toolCalls = null) + { + AddMessage(new ConversationMessage + { + Role = MessageRole.Assistant, + Content = content, + ToolCalls = toolCalls + }); + } + + /// + /// Add a tool response message + /// + public void AddToolMessage(string toolCallId, string toolName, string content) + { + AddMessage(new ConversationMessage + { + Role = MessageRole.Tool, + ToolCallId = toolCallId, + ToolName = toolName, + Content = content + }); + } + } +} diff --git a/src/extensions/WorkflowCore.AI.AzureFoundry/Models/ReviewAction.cs b/src/extensions/WorkflowCore.AI.AzureFoundry/Models/ReviewAction.cs new file mode 100644 index 000000000..27d5d9521 --- /dev/null +++ b/src/extensions/WorkflowCore.AI.AzureFoundry/Models/ReviewAction.cs @@ -0,0 +1,61 @@ +using System; + +namespace WorkflowCore.AI.AzureFoundry.Models +{ + /// + /// Represents a human review action on LLM output + /// + public class ReviewAction + { + /// + /// The decision made by the reviewer + /// + public ReviewDecision Decision { get; set; } + + /// + /// The reviewer's identity + /// + public string Reviewer { get; set; } + + /// + /// Modified content (if the reviewer edited the original) + /// + public string ModifiedContent { get; set; } + + /// + /// Comments from the reviewer + /// + public string Comments { get; set; } + + /// + /// When the review was completed + /// + public DateTime Timestamp { get; set; } = DateTime.UtcNow; + } + + /// + /// Possible decisions for human review + /// + public enum ReviewDecision + { + /// + /// Content approved as-is + /// + Approved, + + /// + /// Content approved with modifications + /// + ApprovedWithChanges, + + /// + /// Content rejected + /// + Rejected, + + /// + /// Request regeneration from LLM + /// + Regenerate + } +} diff --git a/src/extensions/WorkflowCore.AI.AzureFoundry/Models/SearchResult.cs b/src/extensions/WorkflowCore.AI.AzureFoundry/Models/SearchResult.cs new file mode 100644 index 000000000..70f27926b --- /dev/null +++ b/src/extensions/WorkflowCore.AI.AzureFoundry/Models/SearchResult.cs @@ -0,0 +1,56 @@ +using System.Collections.Generic; + +namespace WorkflowCore.AI.AzureFoundry.Models +{ + /// + /// Result from a vector search operation + /// + public class SearchResult + { + /// + /// Unique identifier of the document + /// + public string DocumentId { get; set; } + + /// + /// Relevance score (higher is more relevant) + /// + public double Score { get; set; } + + /// + /// Document content + /// + public string Content { get; set; } + + /// + /// Document title or name + /// + public string Title { get; set; } + + /// + /// Additional fields from the document + /// + public IDictionary Fields { get; set; } = new Dictionary(); + } + + /// + /// Collection of search results + /// + public class SearchResults + { + /// + /// Individual search results + /// + public IList Results { get; set; } = new List(); + + /// + /// Total number of matching documents + /// + public long? TotalCount { get; set; } + + /// + /// The query that produced these results + /// + public string Query { get; set; } + } +} diff --git a/src/extensions/WorkflowCore.AI.AzureFoundry/Models/ToolDefinition.cs b/src/extensions/WorkflowCore.AI.AzureFoundry/Models/ToolDefinition.cs new file mode 100644 index 000000000..cbddf1dcd --- /dev/null +++ b/src/extensions/WorkflowCore.AI.AzureFoundry/Models/ToolDefinition.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; + +namespace WorkflowCore.AI.AzureFoundry.Models +{ + /// + /// Defines a tool that can be invoked by the LLM + /// + public class ToolDefinition + { + /// + /// Name of the tool (must be unique) + /// + public string Name { get; set; } + + /// + /// Description of what the tool does (used by the LLM) + /// + public string Description { get; set; } + + /// + /// JSON schema for the tool's parameters + /// + public string ParametersSchema { get; set; } + + /// + /// Whether the tool requires confirmation before execution + /// + public bool RequiresConfirmation { get; set; } + + /// + /// Type that implements the tool execution + /// + public Type ImplementationType { get; set; } + } +} diff --git a/src/extensions/WorkflowCore.AI.AzureFoundry/Models/ToolResult.cs b/src/extensions/WorkflowCore.AI.AzureFoundry/Models/ToolResult.cs new file mode 100644 index 000000000..4d7bf341d --- /dev/null +++ b/src/extensions/WorkflowCore.AI.AzureFoundry/Models/ToolResult.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; + +namespace WorkflowCore.AI.AzureFoundry.Models +{ + /// + /// Result from executing a tool + /// + public class ToolResult + { + /// + /// Whether the tool executed successfully + /// + public bool Success { get; set; } + + /// + /// Result data from the tool (serialized as string for LLM consumption) + /// + public string Result { get; set; } + + /// + /// Error message if the tool failed + /// + public string Error { get; set; } + + /// + /// The tool call ID this result corresponds to + /// + public string ToolCallId { get; set; } + + /// + /// Name of the tool that was executed + /// + public string ToolName { get; set; } + + /// + /// Execution duration + /// + public TimeSpan Duration { get; set; } + + /// + /// Additional metadata + /// + public IDictionary Metadata { get; set; } = new Dictionary(); + + /// + /// Create a successful result + /// + public static ToolResult Succeeded(string toolCallId, string toolName, string result) + { + return new ToolResult + { + Success = true, + ToolCallId = toolCallId, + ToolName = toolName, + Result = result + }; + } + + /// + /// Create a failed result + /// + public static ToolResult Failed(string toolCallId, string toolName, string error) + { + return new ToolResult + { + Success = false, + ToolCallId = toolCallId, + ToolName = toolName, + Error = error, + Result = $"Error: {error}" + }; + } + } +} diff --git a/src/extensions/WorkflowCore.AI.AzureFoundry/Primitives/AgentLoop.cs b/src/extensions/WorkflowCore.AI.AzureFoundry/Primitives/AgentLoop.cs new file mode 100644 index 000000000..f883d48f2 --- /dev/null +++ b/src/extensions/WorkflowCore.AI.AzureFoundry/Primitives/AgentLoop.cs @@ -0,0 +1,209 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using WorkflowCore.AI.AzureFoundry.Interface; +using WorkflowCore.AI.AzureFoundry.Models; +using WorkflowCore.Interface; +using WorkflowCore.Models; + +namespace WorkflowCore.AI.AzureFoundry.Primitives +{ + /// + /// Step body for running an agent loop (LLM with automatic tool execution) + /// + public class AgentLoop : StepBodyAsync + { + private readonly IChatCompletionService _chatService; + private readonly IToolRegistry _toolRegistry; + private readonly IConversationStore _conversationStore; + + public AgentLoop( + IChatCompletionService chatService, + IToolRegistry toolRegistry, + IConversationStore conversationStore) + { + _chatService = chatService ?? throw new ArgumentNullException(nameof(chatService)); + _toolRegistry = toolRegistry ?? throw new ArgumentNullException(nameof(toolRegistry)); + _conversationStore = conversationStore ?? throw new ArgumentNullException(nameof(conversationStore)); + } + + /// + /// System prompt to set the agent's behavior + /// + public string SystemPrompt { get; set; } + + /// + /// User message to start the agent loop + /// + public string UserMessage { get; set; } + + /// + /// Model to use (optional, uses default if not specified) + /// + public string Model { get; set; } + + /// + /// Temperature for response generation + /// + public float? Temperature { get; set; } + + /// + /// Maximum number of iterations (LLM calls) before stopping + /// + public int MaxIterations { get; set; } = 10; + + /// + /// Whether to run in automatic mode (execute tools automatically) + /// + public bool AutomaticMode { get; set; } = true; + + /// + /// Names of tools available to the agent (uses all registered tools if empty) + /// + public IList AvailableTools { get; set; } = new List(); + + /// + /// Thread ID for conversation history (optional) + /// + public string ThreadId { get; set; } + + // Outputs + + /// + /// Final response from the agent + /// + public string Response { get; set; } + + /// + /// Number of iterations executed + /// + public int IterationsExecuted { get; set; } + + /// + /// Tool calls that were made during the loop + /// + public IList ToolResults { get; set; } = new List(); + + /// + /// Total tokens used across all iterations + /// + public int TotalTokens { get; set; } + + /// + /// Whether the loop completed successfully (vs hitting max iterations) + /// + public bool CompletedSuccessfully { get; set; } + + public override async Task RunAsync(IStepExecutionContext context) + { + var thread = await GetOrCreateThread(context); + + if (!string.IsNullOrEmpty(SystemPrompt) && + (thread.Messages.Count == 0 || thread.Messages[0].Role != MessageRole.System)) + { + thread.AddSystemMessage(SystemPrompt); + } + + thread.AddUserMessage(UserMessage); + + var tools = GetAvailableTools(); + var toolDefinitions = tools.Select(t => new ToolDefinition + { + Name = t.Name, + Description = t.Description, + ParametersSchema = t.ParametersSchema + }).ToList(); + + for (int iteration = 0; iteration < MaxIterations; iteration++) + { + IterationsExecuted = iteration + 1; + + var result = await _chatService.CompleteAsync( + thread.Messages, + Model, + Temperature, + cancellationToken: context.CancellationToken, + tools: toolDefinitions); + + TotalTokens += result.TotalTokens; + thread.AddMessage(result.Message); + + if (result.FinishReason == "stop" || result.Message.ToolCalls == null || !result.Message.ToolCalls.Any()) + { + Response = result.Message.Content; + CompletedSuccessfully = true; + await _conversationStore.SaveThreadAsync(thread); + return ExecutionResult.Next(); + } + + if (!AutomaticMode) + { + Response = result.Message.Content; + await _conversationStore.SaveThreadAsync(thread); + return ExecutionResult.Next(); + } + + foreach (var toolCall in result.Message.ToolCalls) + { + var tool = tools.FirstOrDefault(t => t.Name == toolCall.ToolName); + ToolResult toolResult; + + if (tool == null) + { + toolResult = ToolResult.Failed(toolCall.Id, toolCall.ToolName, $"Tool '{toolCall.ToolName}' not found"); + } + else + { + try + { + toolResult = await tool.ExecuteAsync(toolCall.Id, toolCall.Arguments, context.CancellationToken); + } + catch (Exception ex) + { + toolResult = ToolResult.Failed(toolCall.Id, toolCall.ToolName, ex.Message); + } + } + + ToolResults.Add(toolResult); + thread.AddToolMessage(toolCall.Id, toolCall.ToolName, toolResult.Result); + } + } + + CompletedSuccessfully = false; + Response = thread.Messages.LastOrDefault(m => m.Role == MessageRole.Assistant)?.Content; + await _conversationStore.SaveThreadAsync(thread); + + return ExecutionResult.Next(); + } + + private async Task GetOrCreateThread(IStepExecutionContext context) + { + if (!string.IsNullOrEmpty(ThreadId)) + { + var existing = await _conversationStore.GetThreadAsync(ThreadId); + if (existing != null) + return existing; + } + + var thread = await _conversationStore.GetOrCreateThreadAsync( + context.Workflow.Id, + context.ExecutionPointer.Id); + ThreadId = thread.Id; + return thread; + } + + private IList GetAvailableTools() + { + if (AvailableTools != null && AvailableTools.Any()) + { + return AvailableTools + .Select(name => _toolRegistry.GetTool(name)) + .Where(t => t != null) + .ToList(); + } + + return _toolRegistry.GetAllTools().ToList(); + } + } +} diff --git a/src/extensions/WorkflowCore.AI.AzureFoundry/Primitives/AgentLoopStep.cs b/src/extensions/WorkflowCore.AI.AzureFoundry/Primitives/AgentLoopStep.cs new file mode 100644 index 000000000..d057bcaee --- /dev/null +++ b/src/extensions/WorkflowCore.AI.AzureFoundry/Primitives/AgentLoopStep.cs @@ -0,0 +1,13 @@ +using System; +using WorkflowCore.Models; + +namespace WorkflowCore.AI.AzureFoundry.Primitives +{ + /// + /// WorkflowStep wrapper for AgentLoop + /// + public class AgentLoopStep : WorkflowStep + { + public override Type BodyType => typeof(AgentLoop); + } +} diff --git a/src/extensions/WorkflowCore.AI.AzureFoundry/Primitives/ChatCompletion.cs b/src/extensions/WorkflowCore.AI.AzureFoundry/Primitives/ChatCompletion.cs new file mode 100644 index 000000000..200cda774 --- /dev/null +++ b/src/extensions/WorkflowCore.AI.AzureFoundry/Primitives/ChatCompletion.cs @@ -0,0 +1,149 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using WorkflowCore.AI.AzureFoundry.Interface; +using WorkflowCore.AI.AzureFoundry.Models; +using WorkflowCore.Interface; +using WorkflowCore.Models; + +namespace WorkflowCore.AI.AzureFoundry.Primitives +{ + /// + /// Step body for chat completion operations + /// + public class ChatCompletion : StepBodyAsync + { + private readonly IChatCompletionService _chatService; + private readonly IConversationStore _conversationStore; + + public ChatCompletion(IChatCompletionService chatService, IConversationStore conversationStore) + { + _chatService = chatService ?? throw new ArgumentNullException(nameof(chatService)); + _conversationStore = conversationStore ?? throw new ArgumentNullException(nameof(conversationStore)); + } + + /// + /// System prompt to set the LLM's behavior + /// + public string SystemPrompt { get; set; } + + /// + /// User message to send to the LLM + /// + public string UserMessage { get; set; } + + /// + /// Model to use (optional, uses default if not specified) + /// + public string Model { get; set; } + + /// + /// Temperature for response generation (0.0 - 2.0) + /// + public float? Temperature { get; set; } + + /// + /// Maximum tokens in the response + /// + public int? MaxTokens { get; set; } + + /// + /// Whether to include conversation history from previous steps + /// + public bool IncludeHistory { get; set; } = true; + + /// + /// Thread ID for conversation history (optional) + /// + public string ThreadId { get; set; } + + // Outputs + + /// + /// The generated response text + /// + public string Response { get; set; } + + /// + /// Reason the completion finished + /// + public string FinishReason { get; set; } + + /// + /// Number of tokens used in the prompt + /// + public int PromptTokens { get; set; } + + /// + /// Number of tokens used in the completion + /// + public int CompletionTokens { get; set; } + + /// + /// Total tokens used + /// + public int TotalTokens { get; set; } + + public override async Task RunAsync(IStepExecutionContext context) + { + var messages = new List(); + + if (IncludeHistory && !string.IsNullOrEmpty(ThreadId)) + { + var thread = await _conversationStore.GetThreadAsync(ThreadId); + if (thread != null) + { + messages.AddRange(thread.Messages); + } + } + else if (IncludeHistory) + { + var thread = await _conversationStore.GetOrCreateThreadAsync( + context.Workflow.Id, + context.ExecutionPointer.Id); + messages.AddRange(thread.Messages); + ThreadId = thread.Id; + } + + if (!string.IsNullOrEmpty(SystemPrompt) && (messages.Count == 0 || messages[0].Role != MessageRole.System)) + { + messages.Insert(0, new ConversationMessage + { + Role = MessageRole.System, + Content = SystemPrompt + }); + } + + messages.Add(new ConversationMessage + { + Role = MessageRole.User, + Content = UserMessage + }); + + var result = await _chatService.CompleteAsync( + messages, + Model, + Temperature, + MaxTokens, + cancellationToken: context.CancellationToken); + + Response = result.Message.Content; + FinishReason = result.FinishReason; + PromptTokens = result.PromptTokens; + CompletionTokens = result.CompletionTokens; + TotalTokens = result.PromptTokens + result.CompletionTokens; + + if (IncludeHistory) + { + var thread = await _conversationStore.GetThreadAsync(ThreadId) + ?? await _conversationStore.GetOrCreateThreadAsync(context.Workflow.Id, context.ExecutionPointer.Id); + + thread.AddUserMessage(UserMessage); + thread.AddAssistantMessage(Response); + await _conversationStore.SaveThreadAsync(thread); + } + + return ExecutionResult.Next(); + } + } +} diff --git a/src/extensions/WorkflowCore.AI.AzureFoundry/Primitives/ChatCompletionStep.cs b/src/extensions/WorkflowCore.AI.AzureFoundry/Primitives/ChatCompletionStep.cs new file mode 100644 index 000000000..1f616e94f --- /dev/null +++ b/src/extensions/WorkflowCore.AI.AzureFoundry/Primitives/ChatCompletionStep.cs @@ -0,0 +1,13 @@ +using System; +using WorkflowCore.Models; + +namespace WorkflowCore.AI.AzureFoundry.Primitives +{ + /// + /// WorkflowStep wrapper for ChatCompletion + /// + public class ChatCompletionStep : WorkflowStep + { + public override Type BodyType => typeof(ChatCompletion); + } +} diff --git a/src/extensions/WorkflowCore.AI.AzureFoundry/Primitives/ExecuteTool.cs b/src/extensions/WorkflowCore.AI.AzureFoundry/Primitives/ExecuteTool.cs new file mode 100644 index 000000000..a5b09b5b8 --- /dev/null +++ b/src/extensions/WorkflowCore.AI.AzureFoundry/Primitives/ExecuteTool.cs @@ -0,0 +1,97 @@ +using System; +using System.Threading.Tasks; +using WorkflowCore.AI.AzureFoundry.Interface; +using WorkflowCore.AI.AzureFoundry.Models; +using WorkflowCore.Interface; +using WorkflowCore.Models; + +namespace WorkflowCore.AI.AzureFoundry.Primitives +{ + /// + /// Step body for executing a single tool + /// + public class ExecuteTool : StepBodyAsync + { + private readonly IToolRegistry _toolRegistry; + + public ExecuteTool(IToolRegistry toolRegistry) + { + _toolRegistry = toolRegistry ?? throw new ArgumentNullException(nameof(toolRegistry)); + } + + /// + /// Name of the tool to execute + /// + public string ToolName { get; set; } + + /// + /// Tool call ID (for correlating with LLM tool calls) + /// + public string ToolCallId { get; set; } + + /// + /// JSON arguments for the tool + /// + public string Arguments { get; set; } + + // Outputs + + /// + /// Result from the tool execution + /// + public ToolResult Result { get; set; } + + /// + /// Whether the tool executed successfully + /// + public bool Success { get; set; } + + /// + /// Result string from the tool + /// + public string ResultString { get; set; } + + /// + /// Error message if the tool failed + /// + public string Error { get; set; } + + public override async Task RunAsync(IStepExecutionContext context) + { + if (string.IsNullOrEmpty(ToolName)) + { + throw new InvalidOperationException("ToolName is required"); + } + + var tool = _toolRegistry.GetTool(ToolName); + if (tool == null) + { + Result = ToolResult.Failed(ToolCallId, ToolName, $"Tool '{ToolName}' not found"); + Success = false; + Error = Result.Error; + ResultString = Result.Result; + return ExecutionResult.Next(); + } + + try + { + var startTime = DateTime.UtcNow; + Result = await tool.ExecuteAsync(ToolCallId, Arguments, context.CancellationToken); + Result.Duration = DateTime.UtcNow - startTime; + + Success = Result.Success; + ResultString = Result.Result; + Error = Result.Error; + } + catch (Exception ex) + { + Result = ToolResult.Failed(ToolCallId, ToolName, ex.Message); + Success = false; + Error = ex.Message; + ResultString = Result.Result; + } + + return ExecutionResult.Next(); + } + } +} diff --git a/src/extensions/WorkflowCore.AI.AzureFoundry/Primitives/ExecuteToolStep.cs b/src/extensions/WorkflowCore.AI.AzureFoundry/Primitives/ExecuteToolStep.cs new file mode 100644 index 000000000..7072e7d8c --- /dev/null +++ b/src/extensions/WorkflowCore.AI.AzureFoundry/Primitives/ExecuteToolStep.cs @@ -0,0 +1,13 @@ +using System; +using WorkflowCore.Models; + +namespace WorkflowCore.AI.AzureFoundry.Primitives +{ + /// + /// WorkflowStep wrapper for ExecuteTool + /// + public class ExecuteToolStep : WorkflowStep + { + public override Type BodyType => typeof(ExecuteTool); + } +} diff --git a/src/extensions/WorkflowCore.AI.AzureFoundry/Primitives/GenerateEmbedding.cs b/src/extensions/WorkflowCore.AI.AzureFoundry/Primitives/GenerateEmbedding.cs new file mode 100644 index 000000000..4b0eb81e2 --- /dev/null +++ b/src/extensions/WorkflowCore.AI.AzureFoundry/Primitives/GenerateEmbedding.cs @@ -0,0 +1,67 @@ +using System; +using System.Threading.Tasks; +using WorkflowCore.AI.AzureFoundry.Interface; +using WorkflowCore.Interface; +using WorkflowCore.Models; + +namespace WorkflowCore.AI.AzureFoundry.Primitives +{ + /// + /// Step body for generating embeddings + /// + public class GenerateEmbedding : StepBodyAsync + { + private readonly IEmbeddingService _embeddingService; + + public GenerateEmbedding(IEmbeddingService embeddingService) + { + _embeddingService = embeddingService ?? throw new ArgumentNullException(nameof(embeddingService)); + } + + /// + /// Text to generate embedding for + /// + public string Text { get; set; } + + /// + /// Model to use (optional, uses default if not specified) + /// + public string Model { get; set; } + + // Outputs + + /// + /// The generated embedding vector + /// + public float[] Embedding { get; set; } + + /// + /// Dimensionality of the embedding + /// + public int Dimensions { get; set; } + + /// + /// Tokens used for embedding + /// + public int TokensUsed { get; set; } + + public override async Task RunAsync(IStepExecutionContext context) + { + if (string.IsNullOrEmpty(Text)) + { + throw new InvalidOperationException("Text is required for embedding generation"); + } + + var result = await _embeddingService.GenerateEmbeddingAsync( + Text, + Model, + context.CancellationToken); + + Embedding = result.Embedding; + Dimensions = result.Dimensions; + TokensUsed = result.TokensUsed; + + return ExecutionResult.Next(); + } + } +} diff --git a/src/extensions/WorkflowCore.AI.AzureFoundry/Primitives/GenerateEmbeddingStep.cs b/src/extensions/WorkflowCore.AI.AzureFoundry/Primitives/GenerateEmbeddingStep.cs new file mode 100644 index 000000000..09e570b77 --- /dev/null +++ b/src/extensions/WorkflowCore.AI.AzureFoundry/Primitives/GenerateEmbeddingStep.cs @@ -0,0 +1,13 @@ +using System; +using WorkflowCore.Models; + +namespace WorkflowCore.AI.AzureFoundry.Primitives +{ + /// + /// WorkflowStep wrapper for GenerateEmbedding + /// + public class GenerateEmbeddingStep : WorkflowStep + { + public override Type BodyType => typeof(GenerateEmbedding); + } +} diff --git a/src/extensions/WorkflowCore.AI.AzureFoundry/Primitives/HumanReview.cs b/src/extensions/WorkflowCore.AI.AzureFoundry/Primitives/HumanReview.cs new file mode 100644 index 000000000..cf65cf123 --- /dev/null +++ b/src/extensions/WorkflowCore.AI.AzureFoundry/Primitives/HumanReview.cs @@ -0,0 +1,138 @@ +using System; +using WorkflowCore.AI.AzureFoundry.Models; +using WorkflowCore.Interface; +using WorkflowCore.Models; + +namespace WorkflowCore.AI.AzureFoundry.Primitives +{ + /// + /// Step body for human review of LLM output. + /// + /// To complete a review, publish an event with: + /// - EventName: "HumanReview" + /// - EventKey: The value from the EventKey output property (or your custom CorrelationId if provided) + /// - EventData: A ReviewAction object + /// + public class HumanReview : StepBody + { + public const string EventName = "HumanReview"; + public const string ExtContent = "ContentToReview"; + public const string ExtReviewer = "Reviewer"; + public const string ExtPrompt = "ReviewPrompt"; + public const string ExtEventKey = "EventKey"; + + /// + /// Content to be reviewed + /// + public string Content { get; set; } + + /// + /// Principal/user assigned to review + /// + public string Reviewer { get; set; } + + /// + /// Prompt/instructions for the reviewer + /// + public string ReviewPrompt { get; set; } + + /// + /// Optional custom correlation ID for the event key. + /// If not provided, defaults to "{workflowId}". + /// Use this to correlate reviews with external systems (e.g., ticket ID, request ID). + /// + public string CorrelationId { get; set; } + + // Outputs + + /// + /// The event key to use when publishing the review decision. + /// Store this value to later complete the review via workflowHost.PublishEvent(). + /// + public string EventKey { get; set; } + + /// + /// The review action taken + /// + public ReviewAction ReviewAction { get; set; } + + /// + /// The final approved content (original or modified) + /// + public string ApprovedContent { get; set; } + + /// + /// The decision made by the reviewer + /// + public ReviewDecision Decision { get; set; } + + /// + /// Whether the content was approved (Approved or ApprovedWithChanges) + /// + public bool IsApproved { get; set; } + + /// + /// Comments from the reviewer + /// + public string Comments { get; set; } + + public override ExecutionResult Run(IStepExecutionContext context) + { + if (!context.ExecutionPointer.EventPublished) + { + // Generate the event key - use custom CorrelationId if provided, otherwise use workflowId + EventKey = !string.IsNullOrEmpty(CorrelationId) + ? CorrelationId + : context.Workflow.Id; + + context.ExecutionPointer.ExtensionAttributes[ExtContent] = Content; + context.ExecutionPointer.ExtensionAttributes[ExtReviewer] = Reviewer; + context.ExecutionPointer.ExtensionAttributes[ExtPrompt] = ReviewPrompt; + context.ExecutionPointer.ExtensionAttributes[ExtEventKey] = EventKey; + + var effectiveDate = DateTime.UtcNow; + + return ExecutionResult.WaitForEvent(EventName, EventKey, effectiveDate); + } + + // Restore EventKey from extension attributes for output + if (context.ExecutionPointer.ExtensionAttributes.TryGetValue(ExtEventKey, out var storedKey)) + { + EventKey = storedKey?.ToString(); + } + + if (!(context.ExecutionPointer.EventData is ReviewAction action)) + { + throw new InvalidOperationException("Expected ReviewAction event data"); + } + + ReviewAction = action; + Decision = action.Decision; + Comments = action.Comments; + + switch (action.Decision) + { + case ReviewDecision.Approved: + ApprovedContent = Content; + IsApproved = true; + break; + + case ReviewDecision.ApprovedWithChanges: + ApprovedContent = action.ModifiedContent ?? Content; + IsApproved = true; + break; + + case ReviewDecision.Rejected: + case ReviewDecision.Regenerate: + ApprovedContent = null; + IsApproved = false; + break; + } + + context.ExecutionPointer.ExtensionAttributes["ReviewDecision"] = action.Decision.ToString(); + context.ExecutionPointer.ExtensionAttributes["ReviewedBy"] = action.Reviewer; + + return ExecutionResult.Next(); + } + } +} diff --git a/src/extensions/WorkflowCore.AI.AzureFoundry/Primitives/HumanReviewStep.cs b/src/extensions/WorkflowCore.AI.AzureFoundry/Primitives/HumanReviewStep.cs new file mode 100644 index 000000000..023d09991 --- /dev/null +++ b/src/extensions/WorkflowCore.AI.AzureFoundry/Primitives/HumanReviewStep.cs @@ -0,0 +1,13 @@ +using System; +using WorkflowCore.Models; + +namespace WorkflowCore.AI.AzureFoundry.Primitives +{ + /// + /// WorkflowStep wrapper for HumanReview + /// + public class HumanReviewStep : WorkflowStep + { + public override Type BodyType => typeof(HumanReview); + } +} diff --git a/src/extensions/WorkflowCore.AI.AzureFoundry/Primitives/VectorSearch.cs b/src/extensions/WorkflowCore.AI.AzureFoundry/Primitives/VectorSearch.cs new file mode 100644 index 000000000..4014860bd --- /dev/null +++ b/src/extensions/WorkflowCore.AI.AzureFoundry/Primitives/VectorSearch.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using WorkflowCore.AI.AzureFoundry.Interface; +using WorkflowCore.AI.AzureFoundry.Models; +using WorkflowCore.Interface; +using WorkflowCore.Models; + +namespace WorkflowCore.AI.AzureFoundry.Primitives +{ + /// + /// Step body for vector search operations + /// + public class VectorSearch : StepBodyAsync + { + private readonly ISearchService _searchService; + + public VectorSearch(ISearchService searchService) + { + _searchService = searchService ?? throw new ArgumentNullException(nameof(searchService)); + } + + /// + /// Name of the search index + /// + public string IndexName { get; set; } + + /// + /// Text query (will be embedded automatically) + /// + public string Query { get; set; } + + /// + /// Pre-computed embedding vector (optional, if provided Query is ignored) + /// + public float[] Embedding { get; set; } + + /// + /// Number of results to return + /// + public int TopK { get; set; } = 5; + + /// + /// OData filter expression + /// + public string Filter { get; set; } + + // Outputs + + /// + /// Search results + /// + public IList Results { get; set; } + + /// + /// Total count of matching documents + /// + public long? TotalCount { get; set; } + + public override async Task RunAsync(IStepExecutionContext context) + { + if (string.IsNullOrEmpty(IndexName)) + { + throw new InvalidOperationException("IndexName is required for vector search"); + } + + SearchResults searchResults; + + if (Embedding != null && Embedding.Length > 0) + { + searchResults = await _searchService.SearchByVectorAsync( + IndexName, + Embedding, + TopK, + Filter, + context.CancellationToken); + } + else if (!string.IsNullOrEmpty(Query)) + { + searchResults = await _searchService.SearchAsync( + IndexName, + Query, + TopK, + Filter, + context.CancellationToken); + } + else + { + throw new InvalidOperationException("Either Query or Embedding is required for vector search"); + } + + Results = searchResults.Results; + TotalCount = searchResults.TotalCount; + + return ExecutionResult.Next(); + } + } +} diff --git a/src/extensions/WorkflowCore.AI.AzureFoundry/Primitives/VectorSearchStep.cs b/src/extensions/WorkflowCore.AI.AzureFoundry/Primitives/VectorSearchStep.cs new file mode 100644 index 000000000..e82fc4a06 --- /dev/null +++ b/src/extensions/WorkflowCore.AI.AzureFoundry/Primitives/VectorSearchStep.cs @@ -0,0 +1,13 @@ +using System; +using WorkflowCore.Models; + +namespace WorkflowCore.AI.AzureFoundry.Primitives +{ + /// + /// WorkflowStep wrapper for VectorSearch + /// + public class VectorSearchStep : WorkflowStep + { + public override Type BodyType => typeof(VectorSearch); + } +} diff --git a/src/extensions/WorkflowCore.AI.AzureFoundry/Properties/AssemblyInfo.cs b/src/extensions/WorkflowCore.AI.AzureFoundry/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..ffefcf4c7 --- /dev/null +++ b/src/extensions/WorkflowCore.AI.AzureFoundry/Properties/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("WorkflowCore.AI.AzureFoundry.Tests")] diff --git a/src/extensions/WorkflowCore.AI.AzureFoundry/ServiceExtensions/ServiceCollectionExtensions.cs b/src/extensions/WorkflowCore.AI.AzureFoundry/ServiceExtensions/ServiceCollectionExtensions.cs new file mode 100644 index 000000000..2d52c3758 --- /dev/null +++ b/src/extensions/WorkflowCore.AI.AzureFoundry/ServiceExtensions/ServiceCollectionExtensions.cs @@ -0,0 +1,69 @@ +using System; +using Microsoft.Extensions.DependencyInjection; +using WorkflowCore.AI.AzureFoundry.Interface; +using WorkflowCore.AI.AzureFoundry.Models; +using WorkflowCore.AI.AzureFoundry.Primitives; +using WorkflowCore.AI.AzureFoundry.Services; + +namespace WorkflowCore.AI.AzureFoundry.ServiceExtensions +{ + /// + /// Extension methods for adding Azure AI Foundry services to the DI container + /// + public static class ServiceCollectionExtensions + { + /// + /// Add Azure AI Foundry services to WorkflowCore + /// + public static IServiceCollection AddAzureFoundry( + this IServiceCollection services, + Action configure) + { + if (configure == null) + throw new ArgumentNullException(nameof(configure)); + + services.Configure(configure); + + // Core services + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + // AI services + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + + // Step bodies + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + + return services; + } + + /// + /// Register a tool with the tool registry + /// + public static IServiceCollection AddAgentTool(this IServiceCollection services) + where TTool : class, IAgentTool + { + services.AddTransient(); + services.AddTransient(); + return services; + } + + /// + /// Use a custom conversation store implementation + /// + public static IServiceCollection UseConversationStore(this IServiceCollection services) + where TStore : class, IConversationStore + { + services.AddSingleton(); + return services; + } + } +} diff --git a/src/extensions/WorkflowCore.AI.AzureFoundry/ServiceExtensions/StepBuilderExtensions.cs b/src/extensions/WorkflowCore.AI.AzureFoundry/ServiceExtensions/StepBuilderExtensions.cs new file mode 100644 index 000000000..48e6a9fbd --- /dev/null +++ b/src/extensions/WorkflowCore.AI.AzureFoundry/ServiceExtensions/StepBuilderExtensions.cs @@ -0,0 +1,114 @@ +using System; +using WorkflowCore.AI.AzureFoundry.Interface; +using WorkflowCore.AI.AzureFoundry.Primitives; +using WorkflowCore.AI.AzureFoundry.Services; +using WorkflowCore.Interface; +using WorkflowCore.Models; + +namespace WorkflowCore.Interface +{ + /// + /// Extension methods for adding AI steps to workflows + /// + public static class AzureFoundryStepBuilderExtensions + { + /// + /// Add a chat completion step + /// + public static IChatCompletionBuilder ChatCompletion( + this IStepBuilder builder, + Action> configure = null) + where TStepBody : IStepBody + { + var newStep = new ChatCompletionStep(); + builder.WorkflowBuilder.AddStep(newStep); + var stepBuilder = new ChatCompletionBuilder(builder.WorkflowBuilder, newStep); + + configure?.Invoke(stepBuilder); + + newStep.Name = newStep.Name ?? nameof(ChatCompletion); + builder.Step.Outcomes.Add(new ValueOutcome { NextStep = newStep.Id }); + + return stepBuilder; + } + + /// + /// Add an agent loop step + /// + public static IAgentLoopBuilder AgentLoop( + this IStepBuilder builder, + Action> configure = null) + where TStepBody : IStepBody + { + var newStep = new AgentLoopStep(); + builder.WorkflowBuilder.AddStep(newStep); + var stepBuilder = new AgentLoopBuilder(builder.WorkflowBuilder, newStep); + + configure?.Invoke(stepBuilder); + + newStep.Name = newStep.Name ?? nameof(AgentLoop); + builder.Step.Outcomes.Add(new ValueOutcome { NextStep = newStep.Id }); + + return stepBuilder; + } + + /// + /// Add a human review step + /// + public static IHumanReviewBuilder HumanReview( + this IStepBuilder builder, + Action> configure = null) + where TStepBody : IStepBody + { + var newStep = new HumanReviewStep(); + builder.WorkflowBuilder.AddStep(newStep); + var stepBuilder = new HumanReviewBuilder(builder.WorkflowBuilder, newStep); + + configure?.Invoke(stepBuilder); + + newStep.Name = newStep.Name ?? nameof(HumanReview); + builder.Step.Outcomes.Add(new ValueOutcome { NextStep = newStep.Id }); + + return stepBuilder; + } + + /// + /// Add an embedding generation step + /// + public static IStepBuilder GenerateEmbedding( + this IStepBuilder builder, + Action> configure = null) + where TStepBody : IStepBody + { + var stepBuilder = builder.Then(); + configure?.Invoke(stepBuilder); + return stepBuilder; + } + + /// + /// Add a vector search step + /// + public static IStepBuilder VectorSearch( + this IStepBuilder builder, + Action> configure = null) + where TStepBody : IStepBody + { + var stepBuilder = builder.Then(); + configure?.Invoke(stepBuilder); + return stepBuilder; + } + + /// + /// Add a tool execution step + /// + public static IStepBuilder ExecuteTool( + this IStepBuilder builder, + Action> configure = null) + where TStepBody : IStepBody + { + var stepBuilder = builder.Then(); + configure?.Invoke(stepBuilder); + return stepBuilder; + } + } +} diff --git a/src/extensions/WorkflowCore.AI.AzureFoundry/Services/AgentLoopBuilder.cs b/src/extensions/WorkflowCore.AI.AzureFoundry/Services/AgentLoopBuilder.cs new file mode 100644 index 000000000..48b72f2b9 --- /dev/null +++ b/src/extensions/WorkflowCore.AI.AzureFoundry/Services/AgentLoopBuilder.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using WorkflowCore.AI.AzureFoundry.Interface; +using WorkflowCore.AI.AzureFoundry.Primitives; +using WorkflowCore.Interface; +using WorkflowCore.Models; +using WorkflowCore.Services; + +namespace WorkflowCore.AI.AzureFoundry.Services +{ + /// + /// Builder for AgentLoop steps + /// + public class AgentLoopBuilder : StepBuilder, IAgentLoopBuilder + { + private readonly List _toolNames = new List(); + + public AgentLoopBuilder(IWorkflowBuilder workflowBuilder, WorkflowStep step) + : base(workflowBuilder, step) + { + } + + public IAgentLoopBuilder SystemPrompt(string prompt) + { + Input(s => s.SystemPrompt, d => prompt); + return this; + } + + public IAgentLoopBuilder SystemPrompt(Expression> expression) + { + Input(s => s.SystemPrompt, expression); + return this; + } + + public IAgentLoopBuilder Message(string message) + { + Input(s => s.UserMessage, d => message); + return this; + } + + public IAgentLoopBuilder Message(Expression> expression) + { + Input(s => s.UserMessage, expression); + return this; + } + + public IAgentLoopBuilder Model(string model) + { + Input(s => s.Model, d => model); + return this; + } + + public IAgentLoopBuilder MaxIterations(int maxIterations) + { + Input(s => s.MaxIterations, d => maxIterations); + return this; + } + + public IAgentLoopBuilder WithTool() where TTool : IAgentTool + { + // Tool name will be resolved at runtime + _toolNames.Add(typeof(TTool).Name); + Input(s => s.AvailableTools, d => _toolNames); + return this; + } + + public IAgentLoopBuilder WithTool(string toolName) + { + _toolNames.Add(toolName); + Input(s => s.AvailableTools, d => _toolNames); + return this; + } + + public IAgentLoopBuilder AutoExecuteTools(bool auto = true) + { + Input(s => s.AutomaticMode, d => auto); + return this; + } + + public IAgentLoopBuilder OutputTo(Expression> expression) + { + Output(expression, s => s.Response); + return this; + } + } +} diff --git a/src/extensions/WorkflowCore.AI.AzureFoundry/Services/AzureFoundryClientFactory.cs b/src/extensions/WorkflowCore.AI.AzureFoundry/Services/AzureFoundryClientFactory.cs new file mode 100644 index 000000000..fece6317a --- /dev/null +++ b/src/extensions/WorkflowCore.AI.AzureFoundry/Services/AzureFoundryClientFactory.cs @@ -0,0 +1,69 @@ +using System; +using Azure; +using Azure.AI.Inference; +using Azure.Identity; +using Microsoft.Extensions.Options; +using WorkflowCore.AI.AzureFoundry.Models; + +namespace WorkflowCore.AI.AzureFoundry.Services +{ + /// + /// Factory for creating Azure AI Foundry SDK clients. + /// Supports Azure AI Foundry (services.ai.azure.com) endpoints. + /// + public class AzureFoundryClientFactory + { + private readonly AzureFoundryOptions _options; + + public AzureFoundryClientFactory(IOptions options) + { + _options = options?.Value ?? throw new ArgumentNullException(nameof(options)); + } + + /// + /// Create a ChatCompletionsClient + /// + public ChatCompletionsClient CreateChatClient() + { + var endpoint = BuildEndpoint(); + + if (!string.IsNullOrEmpty(_options.ApiKey)) + { + return new ChatCompletionsClient(endpoint, new AzureKeyCredential(_options.ApiKey)); + } + + var credential = _options.Credential ?? new DefaultAzureCredential(); + return new ChatCompletionsClient(endpoint, credential); + } + + /// + /// Create an EmbeddingsClient + /// + public EmbeddingsClient CreateEmbeddingsClient() + { + var endpoint = BuildEndpoint(); + + if (!string.IsNullOrEmpty(_options.ApiKey)) + { + return new EmbeddingsClient(endpoint, new AzureKeyCredential(_options.ApiKey)); + } + + var credential = _options.Credential ?? new DefaultAzureCredential(); + return new EmbeddingsClient(endpoint, credential); + } + + private Uri BuildEndpoint() + { + var baseEndpoint = _options.Endpoint.TrimEnd('/'); + + // For Azure AI Foundry (services.ai.azure.com), append /models + // The SDK will then call /models/chat/completions or /models/embeddings + if (baseEndpoint.Contains("services.ai.azure.com") && !baseEndpoint.EndsWith("/models")) + { + return new Uri($"{baseEndpoint}/models"); + } + + return new Uri(baseEndpoint); + } + } +} diff --git a/src/extensions/WorkflowCore.AI.AzureFoundry/Services/ChatCompletionBuilder.cs b/src/extensions/WorkflowCore.AI.AzureFoundry/Services/ChatCompletionBuilder.cs new file mode 100644 index 000000000..0244fbf1f --- /dev/null +++ b/src/extensions/WorkflowCore.AI.AzureFoundry/Services/ChatCompletionBuilder.cs @@ -0,0 +1,87 @@ +using System; +using System.Linq.Expressions; +using WorkflowCore.AI.AzureFoundry.Interface; +using WorkflowCore.AI.AzureFoundry.Primitives; +using WorkflowCore.Interface; +using WorkflowCore.Models; +using WorkflowCore.Services; + +namespace WorkflowCore.AI.AzureFoundry.Services +{ + /// + /// Builder for ChatCompletion steps + /// + public class ChatCompletionBuilder : StepBuilder, IChatCompletionBuilder + { + public ChatCompletionBuilder(IWorkflowBuilder workflowBuilder, WorkflowStep step) + : base(workflowBuilder, step) + { + } + + public IChatCompletionBuilder SystemPrompt(string prompt) + { + Input(s => s.SystemPrompt, d => prompt); + return this; + } + + public IChatCompletionBuilder SystemPrompt(Expression> expression) + { + Input(s => s.SystemPrompt, expression); + return this; + } + + public IChatCompletionBuilder UserMessage(string message) + { + Input(s => s.UserMessage, d => message); + return this; + } + + public IChatCompletionBuilder UserMessage(Expression> expression) + { + Input(s => s.UserMessage, expression); + return this; + } + + public IChatCompletionBuilder Model(string model) + { + Input(s => s.Model, d => model); + return this; + } + + public IChatCompletionBuilder Temperature(float temperature) + { + Input(s => s.Temperature, d => temperature); + return this; + } + + public IChatCompletionBuilder MaxTokens(int maxTokens) + { + Input(s => s.MaxTokens, d => maxTokens); + return this; + } + + public IChatCompletionBuilder WithHistory(bool include = true) + { + Input(s => s.IncludeHistory, d => include); + return this; + } + + public IChatCompletionBuilder ThreadId(Expression> expression) + { + Input(s => s.ThreadId, expression); + return this; + } + + public IChatCompletionBuilder OutputTo(Expression> expression) + { + Output(expression, s => s.Response); + return this; + } + + public IChatCompletionBuilder OutputTokensTo(Expression> expression) + { + Output(expression, s => s.TotalTokens); + return this; + } + } +} diff --git a/src/extensions/WorkflowCore.AI.AzureFoundry/Services/ChatCompletionService.cs b/src/extensions/WorkflowCore.AI.AzureFoundry/Services/ChatCompletionService.cs new file mode 100644 index 000000000..e740fbcf1 --- /dev/null +++ b/src/extensions/WorkflowCore.AI.AzureFoundry/Services/ChatCompletionService.cs @@ -0,0 +1,156 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Azure.AI.Inference; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using WorkflowCore.AI.AzureFoundry.Interface; +using WorkflowCore.AI.AzureFoundry.Models; + +namespace WorkflowCore.AI.AzureFoundry.Services +{ + /// + /// Service for chat completion operations using Azure AI Inference + /// + public class ChatCompletionService : IChatCompletionService + { + private readonly AzureFoundryClientFactory _clientFactory; + private readonly AzureFoundryOptions _options; + private readonly ILogger _logger; + + public ChatCompletionService( + AzureFoundryClientFactory clientFactory, + IOptions options, + ILogger logger) + { + _clientFactory = clientFactory ?? throw new ArgumentNullException(nameof(clientFactory)); + _options = options?.Value ?? throw new ArgumentNullException(nameof(options)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + public async Task CompleteAsync( + IEnumerable messages, + string model = null, + float? temperature = null, + int? maxTokens = null, + IEnumerable tools = null, + CancellationToken cancellationToken = default) + { + var chatMessages = messages.Select(ConvertToSdkMessage).ToList(); + + var requestOptions = new ChatCompletionsOptions(chatMessages) + { + Model = model ?? _options.DefaultModel, + Temperature = temperature ?? _options.DefaultTemperature, + MaxTokens = maxTokens ?? _options.DefaultMaxTokens + }; + + if (tools != null && tools.Any()) + { + foreach (var tool in tools) + { + var functionDef = new FunctionDefinition(tool.Name) + { + Description = tool.Description, + Parameters = BinaryData.FromString(tool.ParametersSchema ?? "{}") + }; + requestOptions.Tools.Add(new ChatCompletionsToolDefinition(functionDef)); + } + } + + _logger.LogDebug("Sending chat completion request with {MessageCount} messages", chatMessages.Count); + + var client = _clientFactory.CreateChatClient(); + var response = await client.CompleteAsync(requestOptions, cancellationToken); + var completion = response.Value; + + var responseMessage = new ConversationMessage + { + Role = MessageRole.Assistant, + Content = completion.Content, + TokenCount = completion.Usage?.TotalTokens + }; + + if (completion.ToolCalls != null && completion.ToolCalls.Any()) + { + responseMessage.ToolCalls = completion.ToolCalls + .Select(tc => new ToolCallRequest + { + // Use the SDK-provided ID, but ensure it's not too long (API max is 40 chars) + Id = EnsureValidToolCallId(tc.Id), + ToolName = tc.Function?.Name, + Arguments = tc.Function?.Arguments + }) + .ToList(); + } + + return new ChatCompletionResponse + { + Message = responseMessage, + FinishReason = completion.FinishReason?.ToString() ?? "unknown", + PromptTokens = completion.Usage?.PromptTokens ?? 0, + CompletionTokens = completion.Usage?.CompletionTokens ?? 0, + Model = model ?? _options.DefaultModel + }; + } + + private ChatRequestMessage ConvertToSdkMessage(ConversationMessage message) + { + switch (message.Role) + { + case MessageRole.System: + return new ChatRequestSystemMessage(message.Content); + + case MessageRole.User: + return new ChatRequestUserMessage(message.Content); + + case MessageRole.Assistant: + var assistantMessage = new ChatRequestAssistantMessage(message.Content ?? string.Empty); + if (message.ToolCalls != null) + { + foreach (var toolCall in message.ToolCalls) + { + var validId = EnsureValidToolCallId(toolCall.Id); + _logger.LogDebug("Assistant tool call ID: original={OriginalLength}, truncated={TruncatedLength}", + toolCall.Id?.Length ?? 0, validId?.Length ?? 0); + assistantMessage.ToolCalls.Add(new ChatCompletionsToolCall( + validId, + new FunctionCall(toolCall.ToolName, toolCall.Arguments))); + } + } + return assistantMessage; + + case MessageRole.Tool: + var validToolCallId = EnsureValidToolCallId(message.ToolCallId); + _logger.LogDebug("Tool message tool_call_id: original={OriginalLength}, truncated={TruncatedLength}", + message.ToolCallId?.Length ?? 0, validToolCallId?.Length ?? 0); + // Constructor order is (content, toolCallId) + return new ChatRequestToolMessage(message.Content, validToolCallId); + + default: + throw new ArgumentException($"Unknown message role: {message.Role}"); + } + } + + /// + /// Ensures tool call ID is valid (max 40 characters per API requirement) + /// + private static string EnsureValidToolCallId(string id) + { + if (string.IsNullOrEmpty(id)) + { + return "call_" + Guid.NewGuid().ToString("N").Substring(0, 24); + } + + if (id.Length > 40) + { + return id.Substring(0, 40); + } + + return id; + } + } +} diff --git a/src/extensions/WorkflowCore.AI.AzureFoundry/Services/EmbeddingService.cs b/src/extensions/WorkflowCore.AI.AzureFoundry/Services/EmbeddingService.cs new file mode 100644 index 000000000..a9bbea6c2 --- /dev/null +++ b/src/extensions/WorkflowCore.AI.AzureFoundry/Services/EmbeddingService.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Azure.AI.Inference; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using WorkflowCore.AI.AzureFoundry.Interface; +using WorkflowCore.AI.AzureFoundry.Models; + +namespace WorkflowCore.AI.AzureFoundry.Services +{ + /// + /// Service for generating embeddings using Azure AI Inference + /// + public class EmbeddingService : IEmbeddingService + { + private readonly AzureFoundryClientFactory _clientFactory; + private readonly AzureFoundryOptions _options; + private readonly ILogger _logger; + + public EmbeddingService( + AzureFoundryClientFactory clientFactory, + IOptions options, + ILogger logger) + { + _clientFactory = clientFactory ?? throw new ArgumentNullException(nameof(clientFactory)); + _options = options?.Value ?? throw new ArgumentNullException(nameof(options)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + public async Task GenerateEmbeddingAsync( + string text, + string model = null, + CancellationToken cancellationToken = default) + { + if (string.IsNullOrEmpty(text)) + throw new ArgumentException("Text cannot be null or empty", nameof(text)); + + _logger.LogDebug("Generating embedding for text of length {Length}", text.Length); + + var options = new EmbeddingsOptions(new List { text }) + { + Model = model ?? _options.DefaultEmbeddingModel + }; + var client = _clientFactory.CreateEmbeddingsClient(); + var response = await client.EmbedAsync(options, cancellationToken); + var embedding = response.Value; + + var embeddingItem = embedding.Data.FirstOrDefault(); + float[] vector = null; + if (embeddingItem?.Embedding != null) + { + var bytes = embeddingItem.Embedding.ToArray(); + vector = new float[bytes.Length / sizeof(float)]; + Buffer.BlockCopy(bytes, 0, vector, 0, bytes.Length); + } + + return new EmbeddingResponse + { + Embedding = vector, + Model = model ?? _options.DefaultEmbeddingModel, + TokensUsed = embedding.Usage?.TotalTokens ?? 0 + }; + } + } +} diff --git a/src/extensions/WorkflowCore.AI.AzureFoundry/Services/HumanReviewBuilder.cs b/src/extensions/WorkflowCore.AI.AzureFoundry/Services/HumanReviewBuilder.cs new file mode 100644 index 000000000..0cd7957f4 --- /dev/null +++ b/src/extensions/WorkflowCore.AI.AzureFoundry/Services/HumanReviewBuilder.cs @@ -0,0 +1,64 @@ +using System; +using System.Linq.Expressions; +using WorkflowCore.AI.AzureFoundry.Interface; +using WorkflowCore.AI.AzureFoundry.Models; +using WorkflowCore.AI.AzureFoundry.Primitives; +using WorkflowCore.Interface; +using WorkflowCore.Models; +using WorkflowCore.Services; + +namespace WorkflowCore.AI.AzureFoundry.Services +{ + /// + /// Builder for HumanReview steps + /// + public class HumanReviewBuilder : StepBuilder, IHumanReviewBuilder + { + public HumanReviewBuilder(IWorkflowBuilder workflowBuilder, WorkflowStep step) + : base(workflowBuilder, step) + { + } + + public IHumanReviewBuilder Content(Expression> expression) + { + Input(s => s.Content, expression); + return this; + } + + public IHumanReviewBuilder Reviewer(Expression> expression) + { + Input(s => s.Reviewer, expression); + return this; + } + + public IHumanReviewBuilder Prompt(string prompt) + { + Input(s => s.ReviewPrompt, d => prompt); + return this; + } + + public IHumanReviewBuilder CorrelationId(Expression> expression) + { + Input(s => s.CorrelationId, expression); + return this; + } + + public IHumanReviewBuilder OnEventKey(Expression> expression) + { + Output(expression, s => s.EventKey); + return this; + } + + public IHumanReviewBuilder OnApproved(Expression> expression) + { + Output(expression, s => s.ApprovedContent); + return this; + } + + public IHumanReviewBuilder OutputDecisionTo(Expression> expression) + { + Output(expression, s => s.Decision); + return this; + } + } +} diff --git a/src/extensions/WorkflowCore.AI.AzureFoundry/Services/InMemoryConversationStore.cs b/src/extensions/WorkflowCore.AI.AzureFoundry/Services/InMemoryConversationStore.cs new file mode 100644 index 000000000..c6fca1b75 --- /dev/null +++ b/src/extensions/WorkflowCore.AI.AzureFoundry/Services/InMemoryConversationStore.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Concurrent; +using System.Threading.Tasks; +using WorkflowCore.AI.AzureFoundry.Interface; +using WorkflowCore.AI.AzureFoundry.Models; + +namespace WorkflowCore.AI.AzureFoundry.Services +{ + /// + /// In-memory implementation of conversation store (for development/testing) + /// + public class InMemoryConversationStore : IConversationStore + { + private readonly ConcurrentDictionary _threads = + new ConcurrentDictionary(); + + private readonly ConcurrentDictionary _workflowThreadMap = + new ConcurrentDictionary(); + + public Task GetThreadAsync(string threadId) + { + _threads.TryGetValue(threadId, out var thread); + return Task.FromResult(thread); + } + + public Task GetOrCreateThreadAsync(string workflowInstanceId, string executionPointerId) + { + var key = $"{workflowInstanceId}:{executionPointerId}"; + + if (_workflowThreadMap.TryGetValue(key, out var threadId)) + { + if (_threads.TryGetValue(threadId, out var existingThread)) + { + return Task.FromResult(existingThread); + } + } + + var thread = new ConversationThread + { + WorkflowInstanceId = workflowInstanceId, + ExecutionPointerId = executionPointerId + }; + + _threads[thread.Id] = thread; + _workflowThreadMap[key] = thread.Id; + + return Task.FromResult(thread); + } + + public Task SaveThreadAsync(ConversationThread thread) + { + _threads[thread.Id] = thread; + + if (!string.IsNullOrEmpty(thread.WorkflowInstanceId) && !string.IsNullOrEmpty(thread.ExecutionPointerId)) + { + var key = $"{thread.WorkflowInstanceId}:{thread.ExecutionPointerId}"; + _workflowThreadMap[key] = thread.Id; + } + + return Task.CompletedTask; + } + + public Task DeleteThreadAsync(string threadId) + { + if (_threads.TryRemove(threadId, out var thread)) + { + if (!string.IsNullOrEmpty(thread.WorkflowInstanceId) && !string.IsNullOrEmpty(thread.ExecutionPointerId)) + { + var key = $"{thread.WorkflowInstanceId}:{thread.ExecutionPointerId}"; + _workflowThreadMap.TryRemove(key, out _); + } + } + + return Task.CompletedTask; + } + } +} diff --git a/src/extensions/WorkflowCore.AI.AzureFoundry/Services/SearchService.cs b/src/extensions/WorkflowCore.AI.AzureFoundry/Services/SearchService.cs new file mode 100644 index 000000000..afa683e27 --- /dev/null +++ b/src/extensions/WorkflowCore.AI.AzureFoundry/Services/SearchService.cs @@ -0,0 +1,133 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Azure; +using Azure.Identity; +using Azure.Search.Documents; +using Azure.Search.Documents.Models; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using WorkflowCore.AI.AzureFoundry.Interface; +using WorkflowCore.AI.AzureFoundry.Models; + +namespace WorkflowCore.AI.AzureFoundry.Services +{ + /// + /// Service for vector search operations using Azure AI Search + /// + public class SearchService : ISearchService + { + private readonly IEmbeddingService _embeddingService; + private readonly AzureFoundryOptions _options; + private readonly ILogger _logger; + + public SearchService( + IEmbeddingService embeddingService, + IOptions options, + ILogger logger) + { + _embeddingService = embeddingService ?? throw new ArgumentNullException(nameof(embeddingService)); + _options = options?.Value ?? throw new ArgumentNullException(nameof(options)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + public async Task SearchAsync( + string indexName, + string query, + int topK = 5, + string filter = null, + CancellationToken cancellationToken = default) + { + if (string.IsNullOrEmpty(query)) + throw new ArgumentException("Query cannot be null or empty", nameof(query)); + + _logger.LogDebug("Generating embedding for search query"); + var embeddingResponse = await _embeddingService.GenerateEmbeddingAsync(query, cancellationToken: cancellationToken); + + return await SearchByVectorAsync(indexName, embeddingResponse.Embedding, topK, filter, cancellationToken); + } + + public async Task SearchByVectorAsync( + string indexName, + float[] embedding, + int topK = 5, + string filter = null, + CancellationToken cancellationToken = default) + { + if (string.IsNullOrEmpty(indexName)) + throw new ArgumentException("Index name cannot be null or empty", nameof(indexName)); + + if (embedding == null || embedding.Length == 0) + throw new ArgumentException("Embedding cannot be null or empty", nameof(embedding)); + + if (string.IsNullOrEmpty(_options.SearchEndpoint)) + throw new InvalidOperationException("Search endpoint is not configured"); + + var searchClient = CreateSearchClient(indexName); + + var vectorQuery = new VectorizedQuery(embedding.Select(f => f).ToArray()) + { + KNearestNeighborsCount = topK, + Fields = { "contentVector" } + }; + + var searchOptions = new SearchOptions + { + VectorSearch = new VectorSearchOptions + { + Queries = { vectorQuery } + }, + Size = topK, + Select = { "id", "content", "title" } + }; + + if (!string.IsNullOrEmpty(filter)) + { + searchOptions.Filter = filter; + } + + _logger.LogDebug("Executing vector search on index {IndexName}", indexName); + + var response = await searchClient.SearchAsync(null, searchOptions, cancellationToken); + var results = new SearchResults { Query = "vector search" }; + + await foreach (var result in response.Value.GetResultsAsync()) + { + var searchResult = new SearchResult + { + DocumentId = result.Document.GetString("id"), + Score = result.Score ?? 0, + Content = result.Document.GetString("content"), + Title = result.Document.GetString("title") + }; + + foreach (var field in result.Document) + { + if (field.Key != "id" && field.Key != "content" && field.Key != "title" && field.Key != "contentVector") + { + searchResult.Fields[field.Key] = field.Value; + } + } + + results.Results.Add(searchResult); + } + + results.TotalCount = response.Value.TotalCount; + return results; + } + + private SearchClient CreateSearchClient(string indexName) + { + var endpoint = new Uri(_options.SearchEndpoint); + + if (!string.IsNullOrEmpty(_options.SearchApiKey)) + { + return new SearchClient(endpoint, indexName, new AzureKeyCredential(_options.SearchApiKey)); + } + + return new SearchClient(endpoint, indexName, new DefaultAzureCredential()); + } + } +} diff --git a/src/extensions/WorkflowCore.AI.AzureFoundry/Services/ToolRegistry.cs b/src/extensions/WorkflowCore.AI.AzureFoundry/Services/ToolRegistry.cs new file mode 100644 index 000000000..ffe113698 --- /dev/null +++ b/src/extensions/WorkflowCore.AI.AzureFoundry/Services/ToolRegistry.cs @@ -0,0 +1,83 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Extensions.DependencyInjection; +using WorkflowCore.AI.AzureFoundry.Interface; +using WorkflowCore.AI.AzureFoundry.Models; + +namespace WorkflowCore.AI.AzureFoundry.Services +{ + /// + /// Registry for managing agent tools + /// + public class ToolRegistry : IToolRegistry + { + private readonly ConcurrentDictionary _tools = + new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); + + private readonly ConcurrentDictionary _toolTypes = + new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); + + private readonly IServiceProvider _serviceProvider; + + public ToolRegistry(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + } + + public void Register(IAgentTool tool) + { + if (tool == null) + throw new ArgumentNullException(nameof(tool)); + + if (string.IsNullOrEmpty(tool.Name)) + throw new ArgumentException("Tool name cannot be null or empty", nameof(tool)); + + _tools[tool.Name] = tool; + } + + public void Register() where T : IAgentTool + { + var tool = _serviceProvider.GetRequiredService(); + Register(tool); + _toolTypes[tool.Name] = typeof(T); + } + + public IAgentTool GetTool(string name) + { + if (_tools.TryGetValue(name, out var tool)) + return tool; + + if (_toolTypes.TryGetValue(name, out var type)) + { + tool = (IAgentTool)_serviceProvider.GetRequiredService(type); + _tools[name] = tool; + return tool; + } + + return null; + } + + public IEnumerable GetAllTools() + { + return _tools.Values.ToList(); + } + + public IEnumerable GetToolDefinitions() + { + return _tools.Values.Select(t => new ToolDefinition + { + Name = t.Name, + Description = t.Description, + ParametersSchema = t.ParametersSchema, + ImplementationType = t.GetType() + }).ToList(); + } + + public bool HasTool(string name) + { + return _tools.ContainsKey(name) || _toolTypes.ContainsKey(name); + } + } +} diff --git a/src/extensions/WorkflowCore.AI.AzureFoundry/WorkflowCore.AI.AzureFoundry.csproj b/src/extensions/WorkflowCore.AI.AzureFoundry/WorkflowCore.AI.AzureFoundry.csproj new file mode 100644 index 000000000..7f2bc5260 --- /dev/null +++ b/src/extensions/WorkflowCore.AI.AzureFoundry/WorkflowCore.AI.AzureFoundry.csproj @@ -0,0 +1,35 @@ + + + + Workflow Core extensions for Azure AI Foundry + Daniel Gerlag + netstandard2.0 + WorkflowCore.AI.AzureFoundry + WorkflowCore.AI.AzureFoundry + workflow;.NET;Core;WorkflowCore;AI;Azure;Foundry;LLM;Agent + https://github.com/danielgerlag/workflow-core + https://github.com/danielgerlag/workflow-core/blob/master/LICENSE.md + git + https://github.com/danielgerlag/workflow-core.git + false + false + false + Provides extensions for Workflow Core to integrate Azure AI Foundry capabilities including LLM invocation, agent orchestration, and agentic workflow activities. + 8.0 + + + + + + + + + + + + + + + + + diff --git a/src/extensions/WorkflowCore.AI.AzureFoundry/readme.md b/src/extensions/WorkflowCore.AI.AzureFoundry/readme.md new file mode 100644 index 000000000..298a7815a --- /dev/null +++ b/src/extensions/WorkflowCore.AI.AzureFoundry/readme.md @@ -0,0 +1,500 @@ +# WorkflowCore.AI.AzureFoundry + +[![NuGet](https://img.shields.io/nuget/v/WorkflowCore.AI.AzureFoundry.svg)](https://www.nuget.org/packages/WorkflowCore.AI.AzureFoundry/) + +Azure AI Foundry extension for [WorkflowCore](https://github.com/danielgerlag/workflow-core) - enables building AI-powered, agentic workflows with LLM invocation, automatic tool execution, embeddings, RAG search, and human-in-the-loop review patterns. + +## Table of Contents + +- [Features](#features) +- [Installation](#installation) +- [Quick Start](#quick-start) +- [Configuration](#configuration) +- [Available Steps](#available-steps) + - [ChatCompletion](#chatcompletion) + - [AgentLoop](#agentloop) + - [ExecuteTool](#executetool) + - [GenerateEmbedding](#generateembedding) + - [VectorSearch](#vectorsearch) + - [HumanReview](#humanreview) +- [Creating Custom Tools](#creating-custom-tools) +- [Conversation History](#conversation-history) +- [Authentication](#authentication) +- [Samples](#samples) +- [API Reference](#api-reference) + +## Features + +- **LLM Chat Completion** - Invoke Azure AI models with full conversation history support +- **Agentic Workflows** - Automatic tool-calling loops where the LLM decides which tools to use +- **Tool Execution Framework** - Define and register custom tools that the LLM can invoke +- **Embeddings Generation** - Generate vector embeddings for semantic search and RAG +- **Vector Search** - Integrate with Azure AI Search for similarity search +- **Human-in-the-Loop** - Pause workflows for human review/approval of AI outputs +- **Conversation Persistence** - Automatic conversation history management across workflow steps + +## Installation + +```bash +dotnet add package WorkflowCore.AI.AzureFoundry +``` + +## Quick Start + +```csharp +// 1. Configure services +services.AddWorkflow(); +services.AddAzureFoundry(options => +{ + options.Endpoint = "https://myresource.services.ai.azure.com"; + options.ApiKey = Environment.GetEnvironmentVariable("AZURE_AI_API_KEY"); + options.DefaultModel = "gpt-4o"; +}); + +// 2. Define a workflow with AI steps +public class CustomerSupportWorkflow : IWorkflow +{ + public string Id => "CustomerSupport"; + public int Version => 1; + + public void Build(IWorkflowBuilder builder) + { + builder + .StartWith(context => ExecutionResult.Next()) + .AgentLoop(cfg => cfg + .SystemPrompt("You are a helpful customer support agent.") + .Message(data => data.CustomerQuery) + .WithTool() + .WithTool() + .MaxIterations(5) + .OutputTo(data => data.Response)); + } +} + +// 3. Run the workflow +var workflowId = await host.StartWorkflow("CustomerSupport", new SupportData +{ + CustomerQuery = "How do I reset my password?" +}); +``` + +## Configuration + +### Basic Configuration + +```csharp +services.AddAzureFoundry(options => +{ + // Required: Azure AI Foundry endpoint + options.Endpoint = "https://myresource.services.ai.azure.com"; + + // Authentication (choose one) + options.ApiKey = "your-api-key"; // API key authentication + // OR + options.Credential = new DefaultAzureCredential(); // Azure AD authentication + + // Model configuration + options.DefaultModel = "gpt-4o"; + options.DefaultEmbeddingModel = "text-embedding-3-small"; + options.DefaultTemperature = 0.7f; + options.DefaultMaxTokens = 4096; + + // Azure AI Search (optional, for RAG) + options.SearchEndpoint = "https://mysearch.search.windows.net"; + options.SearchApiKey = "your-search-api-key"; +}); +``` + +### Environment Variables + +The sample project supports `.env` files: + +```bash +AZURE_AI_ENDPOINT=https://myresource.services.ai.azure.com +AZURE_AI_API_KEY=your-api-key +AZURE_AI_DEFAULT_MODEL=gpt-4o +AZURE_AI_PROJECT=myproject +``` + +## Available Steps + +### ChatCompletion + +Simple LLM chat completion with optional conversation history. + +```csharp +builder + .ChatCompletion(cfg => cfg + .SystemPrompt("You are a helpful assistant") + .UserMessage(data => data.UserQuery) + .Model("gpt-4o") // Optional: override default model + .Temperature(0.7f) // Optional: creativity level (0-1) + .MaxTokens(1000) // Optional: response length limit + .WithHistory() // Optional: enable conversation history + .OutputTo(data => data.Response) + .OutputTokensTo(data => data.TokensUsed)); +``` + +**Inputs:** +| Property | Type | Description | +|----------|------|-------------| +| `SystemPrompt` | string | System message defining assistant behavior | +| `UserMessage` | string | User's message/query | +| `Model` | string | Model to use (optional) | +| `Temperature` | float? | Creativity level 0-1 (optional) | +| `MaxTokens` | int? | Maximum response tokens (optional) | + +**Outputs:** +| Property | Type | Description | +|----------|------|-------------| +| `Response` | string | LLM's response text | +| `TokensUsed` | int | Total tokens consumed | +| `FinishReason` | string | Why generation stopped | + +--- + +### AgentLoop + +Agentic workflow with automatic tool execution. The LLM decides which tools to call, the step executes them, and continues until the LLM provides a final response. + +```csharp +builder + .AgentLoop(cfg => cfg + .SystemPrompt("You are an agent with access to tools") + .Message(data => data.UserRequest) + .WithTool() // Register available tools + .WithTool() + .MaxIterations(10) // Prevent infinite loops + .AutoExecuteTools() // Automatically execute tool calls + .OutputTo(data => data.AgentResponse) + .OutputIterationsTo(data => data.IterationsUsed) + .OutputToolResultsTo(data => data.ToolResults)); +``` + +**Inputs:** +| Property | Type | Description | +|----------|------|-------------| +| `SystemPrompt` | string | Agent behavior definition | +| `UserMessage` | string | User's request | +| `MaxIterations` | int | Maximum LLM calls (default: 10) | +| `AutomaticMode` | bool | Auto-execute tools (default: true) | +| `AvailableTools` | IList | Tool names to use (empty = all) | + +**Outputs:** +| Property | Type | Description | +|----------|------|-------------| +| `Response` | string | Final agent response | +| `IterationsExecuted` | int | Number of LLM calls made | +| `ToolResults` | IList | Results from tool executions | +| `CompletedSuccessfully` | bool | True if completed before max iterations | + +--- + +### ExecuteTool + +Manually execute a specific tool (useful for non-automatic tool orchestration). + +```csharp +builder + .ExecuteTool(cfg => cfg + .Input(s => s.ToolName, data => "weather") + .Input(s => s.Arguments, data => JsonSerializer.Serialize(new { city = data.City })) + .Output(s => s.Result, data => data.ToolOutput)); +``` + +--- + +### GenerateEmbedding + +Generate vector embeddings for semantic similarity and RAG applications. + +```csharp +builder + .GenerateEmbedding(cfg => cfg + .Input(s => s.Text, data => data.ContentToEmbed) + .Model("text-embedding-3-small") // Optional: override model + .Output(s => s.Embedding, data => data.EmbeddingVector) + .Output(s => s.TokensUsed, data => data.EmbeddingTokens)); +``` + +**Inputs:** +| Property | Type | Description | +|----------|------|-------------| +| `Text` | string | Text to generate embedding for | +| `Model` | string | Embedding model (optional) | + +**Outputs:** +| Property | Type | Description | +|----------|------|-------------| +| `Embedding` | float[] | Vector embedding array | +| `TokensUsed` | int | Tokens consumed | + +--- + +### VectorSearch + +Search using vector similarity with Azure AI Search. + +```csharp +builder + .VectorSearch(cfg => cfg + .Input(s => s.Query, data => data.SearchQuery) + .Input(s => s.IndexName, data => "knowledge-base") + .Input(s => s.TopK, data => 5) + .Input(s => s.Filter, data => "category eq 'support'") // OData filter + .Output(s => s.Results, data => data.SearchResults)); +``` + +**Inputs:** +| Property | Type | Description | +|----------|------|-------------| +| `Query` | string | Search query text | +| `IndexName` | string | Azure AI Search index name | +| `TopK` | int | Number of results to return | +| `Filter` | string | OData filter expression (optional) | + +**Outputs:** +| Property | Type | Description | +|----------|------|-------------| +| `Results` | IList | Matching documents with scores | + +--- + +### HumanReview + +Pause workflow for human review, approval, or modification of AI-generated content. + +```csharp +builder + .HumanReview(cfg => cfg + .Content(data => data.AIGeneratedContent) + .Reviewer(data => data.AssignedReviewer) + .Prompt("Please review this AI-generated response before sending to customer") + .CorrelationId(data => data.TicketId) // Optional: custom event key + .OnEventKey(data => data.ReviewEventKey) // Optional: capture the event key + .OnApproved(data => data.ApprovedContent) + .OutputDecisionTo(data => data.ReviewDecision)); +``` + +**Inputs:** +| Property | Type | Description | +|----------|------|-------------| +| `Content` | string | The content to be reviewed | +| `Reviewer` | string | Assigned reviewer identifier | +| `ReviewPrompt` | string | Instructions for the reviewer | +| `CorrelationId` | string | Custom event key (optional, defaults to workflowId) | + +**Outputs:** +| Property | Type | Description | +|----------|------|-------------| +| `EventKey` | string | The key to use when completing the review | +| `ApprovedContent` | string | Final approved/modified content | +| `Decision` | ReviewDecision | The reviewer's decision | +| `IsApproved` | bool | Whether content was approved | +| `Comments` | string | Reviewer's comments | + +**Getting the Event Key:** + +There are three ways to get the event key for completing a review: + +1. **Use the workflow ID** (default): If you don't provide a `CorrelationId`, the event key equals the workflow ID +2. **Use a custom correlation ID**: Provide your own ID via `.CorrelationId(data => data.MyId)` +3. **Capture the event key**: Use `.OnEventKey(data => data.ReviewEventKey)` to store it in workflow data + +**Complete a review by publishing an event:** + +```csharp +// Option 1: Use workflow ID (when no CorrelationId was set) +await workflowHost.PublishEvent("HumanReview", workflowId, reviewAction); + +// Option 2: Use your custom correlation ID +await workflowHost.PublishEvent("HumanReview", "TICKET-12345", reviewAction); + +// Option 3: Use the captured event key from workflow data +await workflowHost.PublishEvent("HumanReview", data.ReviewEventKey, reviewAction); +``` + +```csharp +var reviewAction = new ReviewAction +{ + Decision = ReviewDecision.Approved, // or Rejected, ApprovedWithChanges + Reviewer = "john.doe@example.com", + ModifiedContent = "Updated content...", // if modified + Comments = "Looks good!" +}; +``` + +## Creating Custom Tools + +Tools allow the LLM to take actions in your system. Implement `IAgentTool`: + +```csharp +public class WeatherTool : IAgentTool +{ + public string Name => "weather"; + + public string Description => "Get current weather for a city"; + + public string ParametersSchema => @"{ + ""type"": ""object"", + ""properties"": { + ""city"": { + ""type"": ""string"", + ""description"": ""City name"" + } + }, + ""required"": [""city""] + }"; + + private readonly IWeatherService _weatherService; + + public WeatherTool(IWeatherService weatherService) + { + _weatherService = weatherService; + } + + public async Task ExecuteAsync( + string toolCallId, + string arguments, + CancellationToken cancellationToken) + { + try + { + var args = JsonSerializer.Deserialize(arguments); + var weather = await _weatherService.GetWeatherAsync(args.City, cancellationToken); + + return ToolResult.Succeeded(toolCallId, Name, JsonSerializer.Serialize(weather)); + } + catch (Exception ex) + { + return ToolResult.Failed(toolCallId, Name, ex.Message); + } + } +} +``` + +**Register tools in DI:** + +```csharp +// Register tool class +services.AddSingleton(); +services.AddSingleton(); + +// Register with tool registry +var toolRegistry = serviceProvider.GetRequiredService(); +toolRegistry.Register(serviceProvider.GetRequiredService()); +toolRegistry.Register(serviceProvider.GetRequiredService()); +``` + +## Conversation History + +Conversation history is automatically managed per workflow execution using `IConversationStore`. + +### Default In-Memory Store + +```csharp +// Enabled by default - conversations stored in memory +services.AddAzureFoundry(options => { ... }); +``` + +### Custom Store Implementation + +Implement `IConversationStore` for persistent storage (Redis, SQL, CosmosDB, etc.): + +```csharp +public class RedisConversationStore : IConversationStore +{ + public Task GetOrCreateThreadAsync( + string workflowId, string stepId) { ... } + + public Task GetThreadAsync(string threadId) { ... } + + public Task SaveThreadAsync(ConversationThread thread) { ... } + + public Task DeleteThreadAsync(string threadId) { ... } +} + +// Register custom store +services.AddSingleton(); +``` + +## Authentication + +### API Key Authentication (Simplest) + +```csharp +services.AddAzureFoundry(options => +{ + options.Endpoint = "https://myresource.services.ai.azure.com"; + options.ApiKey = Environment.GetEnvironmentVariable("AZURE_AI_API_KEY"); +}); +``` + +### Azure AD Authentication + +```csharp +services.AddAzureFoundry(options => +{ + options.Endpoint = "https://myresource.services.ai.azure.com"; + options.Credential = new DefaultAzureCredential(); + + // Or specific credential types: + // options.Credential = new ManagedIdentityCredential(); + // options.Credential = new ClientSecretCredential(tenantId, clientId, secret); +}); +``` + +## Samples + +See the [sample project](../../samples/WorkflowCore.Sample.AzureFoundry/) for complete working examples: + +| Sample | Description | +|--------|-------------| +| **Simple Chat** | Basic LLM chat completion workflow | +| **Agent with Tools** | Agentic workflow with weather and calculator tools | +| **Human Review** | Human-in-the-loop approval workflow | + +### Running the Sample + +```bash +cd src/samples/WorkflowCore.Sample.AzureFoundry +cp .env.example .env +# Edit .env with your Azure AI credentials +dotnet run +``` + +## API Reference + +### Models + +| Class | Description | +|-------|-------------| +| `AzureFoundryOptions` | Configuration options for the extension | +| `ConversationMessage` | A single message in a conversation | +| `ConversationThread` | A conversation thread with message history | +| `ToolDefinition` | Defines a tool's name, description, and parameters | +| `ToolResult` | Result from tool execution | +| `SearchResult` | A single search result with score and content | +| `ReviewAction` | Human review decision and modifications | + +### Interfaces + +| Interface | Description | +|-----------|-------------| +| `IChatCompletionService` | Service for LLM chat completions | +| `IEmbeddingService` | Service for generating embeddings | +| `ISearchService` | Service for vector search | +| `IAgentTool` | Interface for custom tools | +| `IToolRegistry` | Registry for available tools | +| `IConversationStore` | Storage for conversation history | + +### Enums + +| Enum | Values | +|------|--------| +| `MessageRole` | System, User, Assistant, Tool | +| `ReviewDecision` | Pending, Approved, Rejected, Modified | + +## License + +This extension is part of WorkflowCore and is released under the [MIT License](../../LICENSE.md). diff --git a/src/samples/WorkflowCore.Sample.AzureFoundry/.env.example b/src/samples/WorkflowCore.Sample.AzureFoundry/.env.example new file mode 100644 index 000000000..af86a4792 --- /dev/null +++ b/src/samples/WorkflowCore.Sample.AzureFoundry/.env.example @@ -0,0 +1,25 @@ +# Azure AI Foundry Configuration +# Copy this file to .env and fill in your values + +# Azure AI Foundry / Azure OpenAI endpoint (required) +# Example: https://myresource.openai.azure.com +AZURE_AI_ENDPOINT= + +# API Key for authentication (required if not using Azure AD) +AZURE_AI_API_KEY= + +# Azure AI Foundry project name (optional) +AZURE_AI_PROJECT= + +# Default model for chat completions (optional, defaults to gpt-4o) +# Use your deployed model name, e.g., gpt-4o, gpt-35-turbo +AZURE_AI_DEFAULT_MODEL=gpt-4o + +# Default model for embeddings (optional) +AZURE_AI_EMBEDDING_MODEL=text-embedding-3-small + +# Azure AI Search endpoint (optional, for RAG/vector search) +AZURE_SEARCH_ENDPOINT= + +# Azure AI Search API key (optional) +AZURE_SEARCH_API_KEY= diff --git a/src/samples/WorkflowCore.Sample.AzureFoundry/.gitignore b/src/samples/WorkflowCore.Sample.AzureFoundry/.gitignore new file mode 100644 index 000000000..2eea525d8 --- /dev/null +++ b/src/samples/WorkflowCore.Sample.AzureFoundry/.gitignore @@ -0,0 +1 @@ +.env \ No newline at end of file diff --git a/src/samples/WorkflowCore.Sample.AzureFoundry/Program.cs b/src/samples/WorkflowCore.Sample.AzureFoundry/Program.cs new file mode 100644 index 000000000..1f4e37093 --- /dev/null +++ b/src/samples/WorkflowCore.Sample.AzureFoundry/Program.cs @@ -0,0 +1,238 @@ +using System; +using System.Threading.Tasks; +using DotNetEnv; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using WorkflowCore.AI.AzureFoundry.Interface; +using WorkflowCore.AI.AzureFoundry.ServiceExtensions; +using WorkflowCore.Interface; +using WorkflowCore.Sample.AzureFoundry.Tools; +using WorkflowCore.Sample.AzureFoundry.Workflows; + +namespace WorkflowCore.Sample.AzureFoundry +{ + public class Program + { + public static async Task Main(string[] args) + { + // Load environment variables from .env file + Env.Load(); + + var configuration = new ConfigurationBuilder() + .AddEnvironmentVariables() + .Build(); + + var serviceProvider = ConfigureServices(configuration); + + // Register tools + var toolRegistry = serviceProvider.GetRequiredService(); + toolRegistry.Register(serviceProvider.GetRequiredService()); + toolRegistry.Register(serviceProvider.GetRequiredService()); + + var host = serviceProvider.GetRequiredService(); + + // Register workflows + host.RegisterWorkflow(); + host.RegisterWorkflow(); + host.RegisterWorkflow(); + + host.Start(); + + Console.WriteLine("=== WorkflowCore Azure AI Foundry Sample ==="); + Console.WriteLine(); + Console.WriteLine("Choose a workflow to run:"); + Console.WriteLine("1. Simple Chat Completion"); + Console.WriteLine("2. Agent with Tools (Agentic Loop)"); + Console.WriteLine("3. Human-in-the-Loop Review"); + Console.WriteLine("Q. Quit"); + Console.WriteLine(); + + while (true) + { + Console.Write("Enter choice: "); + var choice = Console.ReadLine()?.Trim().ToUpper(); + + switch (choice) + { + case "1": + await RunSimpleChatWorkflow(host); + break; + case "2": + await RunAgentWithToolsWorkflow(host); + break; + case "3": + await RunHumanReviewWorkflow(host); + break; + case "Q": + host.Stop(); + return; + default: + Console.WriteLine("Invalid choice. Try again."); + break; + } + } + } + + private static async Task RunSimpleChatWorkflow(IWorkflowHost host) + { + Console.WriteLine("Type 'quit' to exit the conversation."); + Console.WriteLine(); + + while (true) + { + Console.Write("You: "); + var message = Console.ReadLine(); + + if (string.IsNullOrWhiteSpace(message) || message.Equals("quit", StringComparison.OrdinalIgnoreCase)) + { + Console.WriteLine("Exiting chat."); + break; + } + + var data = new ChatWorkflowData + { + UserMessage = message + }; + + var workflowId = await host.StartWorkflow("SimpleChatWorkflow", data); + + // Wait a bit for the workflow to complete + await Task.Delay(5000); + + var instance = await host.PersistenceStore.GetWorkflowInstance(workflowId); + var result = instance.Data as ChatWorkflowData; + + Console.WriteLine($"Assistant: {result?.Response ?? "Still processing..."}"); + Console.WriteLine(); + } + } + + private static async Task RunAgentWithToolsWorkflow(IWorkflowHost host) + { + Console.WriteLine("Available tools: weather (get weather for a city), calculator (do math)"); + Console.WriteLine("Type 'quit' to exit the conversation."); + Console.WriteLine(); + + while (true) + { + Console.Write("You: "); + var message = Console.ReadLine(); + + if (string.IsNullOrWhiteSpace(message) || message.Equals("quit", StringComparison.OrdinalIgnoreCase)) + { + Console.WriteLine("Exiting agent conversation."); + break; + } + + var data = new AgentWorkflowData + { + UserRequest = message + }; + + var workflowId = await host.StartWorkflow("AgentWithToolsWorkflow", data); + + // Wait for the agent loop to complete + await Task.Delay(15000); + + var instance = await host.PersistenceStore.GetWorkflowInstance(workflowId); + var result = instance.Data as AgentWorkflowData; + + Console.WriteLine($"Agent: {result?.AgentResponse ?? "Still processing..."}"); + Console.WriteLine(); + } + } + + private static async Task RunHumanReviewWorkflow(IWorkflowHost host) + { + Console.Write("Enter content to generate and review: "); + var topic = Console.ReadLine(); + + var data = new ReviewWorkflowData + { + Topic = topic, + Reviewer = "demo-user" + }; + + var workflowId = await host.StartWorkflow("HumanReviewWorkflow", data); + Console.WriteLine($"Started workflow: {workflowId}"); + + // Wait for AI to generate content + await Task.Delay(5000); + + var instance = await host.PersistenceStore.GetWorkflowInstance(workflowId); + var result = instance.Data as ReviewWorkflowData; + + Console.WriteLine(); + Console.WriteLine("=== Content Generated by AI ==="); + Console.WriteLine(result?.GeneratedContent ?? "Still generating..."); + Console.WriteLine("================================"); + Console.WriteLine(); + Console.WriteLine("To approve, publish a HumanReview event. For this demo, auto-approving..."); + + // In a real app, this would come from a UI or API + // For demo, we auto-approve + await host.PublishEvent( + "HumanReview", + $"{workflowId}.{GetReviewPointerId(instance)}", + new WorkflowCore.AI.AzureFoundry.Models.ReviewAction + { + Decision = WorkflowCore.AI.AzureFoundry.Models.ReviewDecision.Approved, + Reviewer = "demo-user" + }); + + await Task.Delay(2000); + + instance = await host.PersistenceStore.GetWorkflowInstance(workflowId); + result = instance.Data as ReviewWorkflowData; + + Console.WriteLine(); + Console.WriteLine($"Final approved content: {result?.ApprovedContent ?? "Pending..."}"); + Console.WriteLine(); + } + + private static string GetReviewPointerId(WorkflowCore.Models.WorkflowInstance instance) + { + string lastId = null; + foreach (var pointer in instance.ExecutionPointers) + { + if (pointer.StepName == "HumanReview") + return pointer.Id; + lastId = pointer.Id; + } + return lastId; + } + + private static IServiceProvider ConfigureServices(IConfiguration configuration) + { + var services = new ServiceCollection(); + + services.AddLogging(builder => + { + builder.AddConsole(); + builder.SetMinimumLevel(LogLevel.Information); + }); + + services.AddWorkflow(); + + // Configure Azure AI Foundry + services.AddAzureFoundry(options => + { + options.Endpoint = configuration["AZURE_AI_ENDPOINT"] + ?? throw new InvalidOperationException("AZURE_AI_ENDPOINT not configured. Copy .env.example to .env and fill in values."); + options.ApiKey = configuration["AZURE_AI_API_KEY"]; + options.ProjectName = configuration["AZURE_AI_PROJECT"] ?? "default"; + options.DefaultModel = configuration["AZURE_AI_DEFAULT_MODEL"] ?? "gpt-4o"; + options.DefaultEmbeddingModel = configuration["AZURE_AI_EMBEDDING_MODEL"] ?? "text-embedding-3-small"; + options.SearchEndpoint = configuration["AZURE_SEARCH_ENDPOINT"]; + options.SearchApiKey = configuration["AZURE_SEARCH_API_KEY"]; + }); + + // Register tools + services.AddTransient(); + services.AddTransient(); + + return services.BuildServiceProvider(); + } + } +} diff --git a/src/samples/WorkflowCore.Sample.AzureFoundry/README.md b/src/samples/WorkflowCore.Sample.AzureFoundry/README.md new file mode 100644 index 000000000..7fc1a9484 --- /dev/null +++ b/src/samples/WorkflowCore.Sample.AzureFoundry/README.md @@ -0,0 +1,259 @@ +# WorkflowCore Azure AI Foundry Sample + +This sample demonstrates how to use the **WorkflowCore.AI.AzureFoundry** extension to build AI-powered, agentic workflows. + +## Features Demonstrated + +1. **Simple Chat Completion** - Conversational LLM chat with persistent conversation +2. **Agent with Tools** - Autonomous agent that uses tools (weather, calculator) to answer questions +3. **Human-in-the-Loop Review** - AI generates content, human approves/modifies before continuing + +## Prerequisites + +- .NET 8.0 or later +- Azure AI Foundry resource with deployed models (e.g., gpt-4o) +- API Key from your Azure AI resource + +## Setup + +1. **Copy the environment file:** + ```bash + cp .env.example .env + ``` + +2. **Edit `.env` with your Azure AI credentials:** + ```bash + AZURE_AI_ENDPOINT=https://your-resource.services.ai.azure.com + AZURE_AI_API_KEY=your-api-key-here + AZURE_AI_DEFAULT_MODEL=gpt-4o + ``` + + Get your endpoint and API key from the Azure Portal: + - Navigate to your Azure AI Foundry resource + - Go to **Keys and Endpoint** + - Copy the endpoint and one of the keys + +## Running the Sample + +```bash +cd src/samples/WorkflowCore.Sample.AzureFoundry +dotnet run +``` + +You'll see an interactive menu: + +``` +=== WorkflowCore Azure AI Foundry Sample === + +Choose a workflow to run: +1. Simple Chat Completion +2. Agent with Tools (Agentic Loop) +3. Human-in-the-Loop Review +Q. Quit + +Enter choice: +``` + +## Sample Workflows + +### 1. Simple Chat Completion + +A conversational chat loop where you can have a multi-turn conversation with the LLM. + +``` +Enter choice: 1 +Type 'quit' to exit the conversation. + +You: What is the capital of France? +Assistant: The capital of France is Paris. + +You: What's the population? +Assistant: Paris has a population of approximately 2.1 million in the city proper... + +You: quit +``` + +**Workflow code:** +```csharp +builder + .StartWith(context => ExecutionResult.Next()) + .ChatCompletion(cfg => cfg + .SystemPrompt("You are a helpful assistant") + .UserMessage(data => data.UserMessage) + .OutputTo(data => data.Response)); +``` + +### 2. Agent with Tools (Agentic Loop) + +An autonomous agent that can use tools to accomplish tasks. The agent decides when and how to use tools based on your request. + +**Available tools:** +- `weather` - Get current weather for any city +- `calculator` - Perform mathematical calculations + +``` +Enter choice: 2 +Available tools: weather (get weather for a city), calculator (do math) +Type 'quit' to exit the conversation. + +You: What's the weather in Seattle? +Agent: The current weather in Seattle is partly cloudy with a temperature of 31°C (87°F) and a humidity of 84%. + +You: What is 25 * 4 + 10? +Agent: 25 × 4 + 10 = 110 + +You: What's the weather in Tokyo and convert the temperature from Celsius to Fahrenheit +Agent: The weather in Tokyo is sunny with a temperature of 28°C. Converting to Fahrenheit: (28 × 9/5) + 32 = 82.4°F + +You: quit +``` + +**How it works:** +1. You send a request +2. The LLM analyzes your request and decides which tool(s) to use +3. Tools are executed automatically +4. Results are fed back to the LLM +5. The LLM provides a final response using the tool results + +**Workflow code:** +```csharp +builder + .StartWith(context => ExecutionResult.Next()) + .AgentLoop(cfg => cfg + .SystemPrompt(@"You are a helpful assistant with access to tools. + Use the weather tool to get weather information. + Use the calculator tool for math operations. + Always explain what you're doing.") + .Message(data => data.UserRequest) + .WithTool("weather") + .WithTool("calculator") + .MaxIterations(5) + .AutoExecuteTools(true) + .OutputTo(data => data.AgentResponse)); +``` + +### 3. Human-in-the-Loop Review + +Demonstrates workflows that pause for human approval. The AI generates content, then waits for a human to approve, reject, or modify it. + +``` +Enter choice: 3 +Enter content to generate and review: Write a product description for wireless earbuds + +AI Generated Content: +[AI generates a product description] + +Enter your review decision: +1. Approve as-is +2. Approve with modifications +3. Reject + +Enter decision: 1 +Content approved: [approved content is stored] +``` + +**Workflow code:** +```csharp +builder + .StartWith(context => ExecutionResult.Next()) + .ChatCompletion(cfg => cfg + .SystemPrompt("You are a marketing copywriter") + .UserMessage(data => $"Write about: {data.Topic}") + .OutputTo(data => data.GeneratedContent)) + .HumanReview(cfg => cfg + .Content(data => data.GeneratedContent) + .Reviewer(data => data.Reviewer) + .OnApproved(data => data.ApprovedContent)); +``` + +## Creating Custom Tools + +You can extend the agent's capabilities by creating custom tools: + +```csharp +public class StockPriceTool : IAgentTool +{ + public string Name => "stock_price"; + + public string Description => "Get the current stock price for a ticker symbol"; + + public string ParametersSchema => @"{ + ""type"": ""object"", + ""properties"": { + ""ticker"": { + ""type"": ""string"", + ""description"": ""Stock ticker symbol (e.g., MSFT, AAPL)"" + } + }, + ""required"": [""ticker""] + }"; + + private readonly IStockService _stockService; + + public StockPriceTool(IStockService stockService) + { + _stockService = stockService; + } + + public async Task ExecuteAsync( + string toolCallId, + string arguments, + CancellationToken ct) + { + var args = JsonSerializer.Deserialize(arguments); + var price = await _stockService.GetPriceAsync(args.Ticker, ct); + + return ToolResult.Succeeded(toolCallId, Name, + JsonSerializer.Serialize(new { ticker = args.Ticker, price = price })); + } +} +``` + +Register your tool: +```csharp +services.AddSingleton(); +toolRegistry.Register(serviceProvider.GetRequiredService()); +``` + +## Project Structure + +``` +WorkflowCore.Sample.AzureFoundry/ +├── Program.cs # Entry point and service configuration +├── README.md # This file +├── .env.example # Environment variable template +├── Workflows/ +│ ├── WorkflowData.cs # Data classes for all workflows +│ ├── SimpleChatWorkflow.cs # Simple LLM chat workflow +│ ├── AgentWithToolsWorkflow.cs # Agentic workflow with tool calling +│ └── HumanReviewWorkflow.cs # Human-in-the-loop workflow +└── Tools/ + ├── WeatherTool.cs # Simulated weather API tool + └── CalculatorTool.cs # Mathematical calculator tool +``` + +## Troubleshooting + +### "Resource not found" error + +Make sure your endpoint is correct: +- Azure AI Foundry: `https://your-resource.services.ai.azure.com` +- The model name should match a deployed model in your resource + +### Authentication errors + +1. Verify your API key is correct +2. Make sure the key has access to the resource +3. Check that the model is deployed and accessible + +### Tool not being called + +The LLM decides when to use tools based on your request. Try being more specific: +- ❌ "calculator" (too vague) +- ✅ "What is 25 + 15?" (clearly needs calculation) + +## Learn More + +- [WorkflowCore Documentation](https://workflow-core.readthedocs.io) +- [WorkflowCore.AI.AzureFoundry Extension](../../extensions/WorkflowCore.AI.AzureFoundry/) +- [Azure AI Foundry Documentation](https://learn.microsoft.com/azure/ai-services/) diff --git a/src/samples/WorkflowCore.Sample.AzureFoundry/Tools/CalculatorTool.cs b/src/samples/WorkflowCore.Sample.AzureFoundry/Tools/CalculatorTool.cs new file mode 100644 index 000000000..9ddc2b082 --- /dev/null +++ b/src/samples/WorkflowCore.Sample.AzureFoundry/Tools/CalculatorTool.cs @@ -0,0 +1,69 @@ +using System; +using System.Data; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using WorkflowCore.AI.AzureFoundry.Interface; +using WorkflowCore.AI.AzureFoundry.Models; + +namespace WorkflowCore.Sample.AzureFoundry.Tools +{ + /// + /// Sample tool that performs mathematical calculations + /// + public class CalculatorTool : IAgentTool + { + public string Name => "calculator"; + + public string Description => "Perform mathematical calculations. Supports basic arithmetic (+, -, *, /), parentheses, and common math operations."; + + public string ParametersSchema => @"{ + ""type"": ""object"", + ""properties"": { + ""expression"": { + ""type"": ""string"", + ""description"": ""The mathematical expression to evaluate (e.g., '2 + 2', '(10 * 5) / 2', '3.14 * 2')"" + } + }, + ""required"": [""expression""] + }"; + + public Task ExecuteAsync(string toolCallId, string arguments, CancellationToken cancellationToken = default) + { + try + { + var args = JsonSerializer.Deserialize(arguments, new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true + }); + + // Use DataTable.Compute for simple expression evaluation + var table = new DataTable(); + var result = table.Compute(args.Expression, null); + + var response = new + { + expression = args.Expression, + result = Convert.ToDouble(result) + }; + + return Task.FromResult(ToolResult.Succeeded( + toolCallId, + Name, + JsonSerializer.Serialize(response))); + } + catch (Exception ex) + { + return Task.FromResult(ToolResult.Failed( + toolCallId, + Name, + $"Failed to evaluate expression: {ex.Message}")); + } + } + + private class CalculatorArgs + { + public string Expression { get; set; } + } + } +} diff --git a/src/samples/WorkflowCore.Sample.AzureFoundry/Tools/WeatherTool.cs b/src/samples/WorkflowCore.Sample.AzureFoundry/Tools/WeatherTool.cs new file mode 100644 index 000000000..38da38a21 --- /dev/null +++ b/src/samples/WorkflowCore.Sample.AzureFoundry/Tools/WeatherTool.cs @@ -0,0 +1,71 @@ +using System; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using WorkflowCore.AI.AzureFoundry.Interface; +using WorkflowCore.AI.AzureFoundry.Models; + +namespace WorkflowCore.Sample.AzureFoundry.Tools +{ + /// + /// Sample tool that provides weather information (simulated) + /// + public class WeatherTool : IAgentTool + { + public string Name => "weather"; + + public string Description => "Get the current weather for a specified city. Returns temperature, conditions, and humidity."; + + public string ParametersSchema => @"{ + ""type"": ""object"", + ""properties"": { + ""city"": { + ""type"": ""string"", + ""description"": ""The city name to get weather for (e.g., 'London', 'New York')"" + } + }, + ""required"": [""city""] + }"; + + public Task ExecuteAsync(string toolCallId, string arguments, CancellationToken cancellationToken = default) + { + try + { + var args = JsonSerializer.Deserialize(arguments, new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true + }); + + // Simulated weather data + var random = new Random(); + var temp = random.Next(0, 35); + var conditions = new[] { "Sunny", "Cloudy", "Rainy", "Partly Cloudy", "Overcast" }; + var condition = conditions[random.Next(conditions.Length)]; + var humidity = random.Next(30, 90); + + var result = new + { + city = args.City, + temperature_celsius = temp, + temperature_fahrenheit = (temp * 9 / 5) + 32, + conditions = condition, + humidity_percent = humidity + }; + + return Task.FromResult(ToolResult.Succeeded( + toolCallId, + Name, + JsonSerializer.Serialize(result))); + } + catch (Exception ex) + { + return Task.FromResult(ToolResult.Failed(toolCallId, Name, ex.Message)); + } + } + + private class WeatherArgs + { + public string City { get; set; } + } + } +} diff --git a/src/samples/WorkflowCore.Sample.AzureFoundry/WorkflowCore.Sample.AzureFoundry.csproj b/src/samples/WorkflowCore.Sample.AzureFoundry/WorkflowCore.Sample.AzureFoundry.csproj new file mode 100644 index 000000000..e23dcdf1f --- /dev/null +++ b/src/samples/WorkflowCore.Sample.AzureFoundry/WorkflowCore.Sample.AzureFoundry.csproj @@ -0,0 +1,39 @@ + + + + WorkflowCore.Sample.AzureFoundry + Exe + WorkflowCore.Sample.AzureFoundry + false + false + false + + + net8.0 + + + + + + + + + + + + + + + + + + + + PreserveNewest + + + PreserveNewest + + + + diff --git a/src/samples/WorkflowCore.Sample.AzureFoundry/Workflows/AgentWithToolsWorkflow.cs b/src/samples/WorkflowCore.Sample.AzureFoundry/Workflows/AgentWithToolsWorkflow.cs new file mode 100644 index 000000000..4a81051ab --- /dev/null +++ b/src/samples/WorkflowCore.Sample.AzureFoundry/Workflows/AgentWithToolsWorkflow.cs @@ -0,0 +1,37 @@ +using WorkflowCore.AI.AzureFoundry.Interface; +using WorkflowCore.AI.AzureFoundry.Primitives; +using WorkflowCore.Interface; +using WorkflowCore.Models; +using WorkflowCore.Sample.AzureFoundry.Tools; + +namespace WorkflowCore.Sample.AzureFoundry.Workflows +{ + /// + /// Workflow demonstrating an agentic loop with tool execution + /// + public class AgentWithToolsWorkflow : IWorkflow + { + public string Id => "AgentWithToolsWorkflow"; + public int Version => 1; + + public void Build(IWorkflowBuilder builder) + { + builder + .StartWith(context => ExecutionResult.Next()) + .AgentLoop(cfg => cfg + .SystemPrompt(@"You are a helpful assistant with access to tools. +Available tools: +- weather: Get current weather for a city +- calculator: Perform mathematical calculations + +Use the tools when needed to answer user questions accurately. +Always provide a final answer after using tools.") + .Message(data => data.UserRequest) + .WithTool("weather") + .WithTool("calculator") + .MaxIterations(5) + .AutoExecuteTools(true) + .OutputTo(data => data.AgentResponse)); + } + } +} diff --git a/src/samples/WorkflowCore.Sample.AzureFoundry/Workflows/HumanReviewWorkflow.cs b/src/samples/WorkflowCore.Sample.AzureFoundry/Workflows/HumanReviewWorkflow.cs new file mode 100644 index 000000000..43edacef1 --- /dev/null +++ b/src/samples/WorkflowCore.Sample.AzureFoundry/Workflows/HumanReviewWorkflow.cs @@ -0,0 +1,40 @@ +using WorkflowCore.AI.AzureFoundry.Interface; +using WorkflowCore.AI.AzureFoundry.Primitives; +using WorkflowCore.Interface; +using WorkflowCore.Models; + +namespace WorkflowCore.Sample.AzureFoundry.Workflows +{ + /// + /// Workflow demonstrating human-in-the-loop review of AI-generated content + /// + public class HumanReviewWorkflow : IWorkflow + { + public string Id => "HumanReviewWorkflow"; + public int Version => 1; + + public void Build(IWorkflowBuilder builder) + { + builder + .StartWith(context => ExecutionResult.Next()) + + // Step 1: Generate content with AI + .ChatCompletion(cfg => cfg + .SystemPrompt("You are a content writer. Write clear, engaging content on the given topic.") + .UserMessage(data => $"Write a short paragraph about: {data.Topic}") + .MaxTokens(300) + .OutputTo(data => data.GeneratedContent)) + + // Step 2: Wait for human review + // Use CorrelationId to provide a known event key for completing the review + // If not provided, defaults to the workflowId + .HumanReview(cfg => cfg + .Content(data => data.GeneratedContent) + .Reviewer(data => data.Reviewer) + .Prompt("Please review this AI-generated content. Approve, modify, or reject.") + .CorrelationId(data => data.ReviewId) // Use custom correlation ID if provided + .OnApproved(data => data.ApprovedContent) + .OnEventKey(data => data.EventKey)); // Capture the event key for completing the review + } + } +} diff --git a/src/samples/WorkflowCore.Sample.AzureFoundry/Workflows/SimpleChatWorkflow.cs b/src/samples/WorkflowCore.Sample.AzureFoundry/Workflows/SimpleChatWorkflow.cs new file mode 100644 index 000000000..a62e85107 --- /dev/null +++ b/src/samples/WorkflowCore.Sample.AzureFoundry/Workflows/SimpleChatWorkflow.cs @@ -0,0 +1,29 @@ +using WorkflowCore.AI.AzureFoundry.Interface; +using WorkflowCore.AI.AzureFoundry.Primitives; +using WorkflowCore.Interface; +using WorkflowCore.Models; + +namespace WorkflowCore.Sample.AzureFoundry.Workflows +{ + /// + /// Simple workflow demonstrating basic chat completion + /// + public class SimpleChatWorkflow : IWorkflow + { + public string Id => "SimpleChatWorkflow"; + public int Version => 1; + + public void Build(IWorkflowBuilder builder) + { + builder + .StartWith(context => ExecutionResult.Next()) + .ChatCompletion(cfg => cfg + .SystemPrompt("You are a helpful, friendly assistant. Keep responses concise.") + .UserMessage(data => data.UserMessage) + .Temperature(0.7f) + .MaxTokens(500) + .OutputTo(data => data.Response) + .OutputTokensTo(data => data.TokensUsed)); + } + } +} diff --git a/src/samples/WorkflowCore.Sample.AzureFoundry/Workflows/WorkflowData.cs b/src/samples/WorkflowCore.Sample.AzureFoundry/Workflows/WorkflowData.cs new file mode 100644 index 000000000..b05b47f86 --- /dev/null +++ b/src/samples/WorkflowCore.Sample.AzureFoundry/Workflows/WorkflowData.cs @@ -0,0 +1,47 @@ +namespace WorkflowCore.Sample.AzureFoundry.Workflows +{ + /// + /// Data for simple chat workflow + /// + public class ChatWorkflowData + { + public string UserMessage { get; set; } + public string Response { get; set; } + public int TokensUsed { get; set; } + } + + /// + /// Data for agent with tools workflow + /// + public class AgentWorkflowData + { + public string UserRequest { get; set; } + public string AgentResponse { get; set; } + public int IterationsUsed { get; set; } + } + + /// + /// Data for human review workflow + /// + public class ReviewWorkflowData + { + public string Topic { get; set; } + public string Reviewer { get; set; } + public string GeneratedContent { get; set; } + public string ApprovedContent { get; set; } + public bool IsApproved { get; set; } + + /// + /// Optional custom correlation ID for the review. + /// If provided, this will be used as the event key. + /// If not provided, the workflow ID will be used. + /// + public string ReviewId { get; set; } + + /// + /// The event key to use when completing the review. + /// Use this with: workflowHost.PublishEvent("HumanReview", eventKey, reviewAction) + /// + public string EventKey { get; set; } + } +} diff --git a/test/WorkflowCore.AI.AzureFoundry.Tests/ConversationThreadTests.cs b/test/WorkflowCore.AI.AzureFoundry.Tests/ConversationThreadTests.cs new file mode 100644 index 000000000..ba86edb31 --- /dev/null +++ b/test/WorkflowCore.AI.AzureFoundry.Tests/ConversationThreadTests.cs @@ -0,0 +1,110 @@ +using WorkflowCore.AI.AzureFoundry.Models; +using Xunit; +using FluentAssertions; + +namespace WorkflowCore.AI.AzureFoundry.Tests +{ + public class ConversationThreadTests + { + [Fact] + public void AddMessage_ShouldUpdateTimestamp() + { + // Arrange + var thread = new ConversationThread(); + var originalUpdatedAt = thread.UpdatedAt; + + // Act + System.Threading.Thread.Sleep(10); + thread.AddUserMessage("Hello"); + + // Assert + thread.UpdatedAt.Should().BeAfter(originalUpdatedAt); + } + + [Fact] + public void AddSystemMessage_ShouldAddCorrectRole() + { + // Arrange + var thread = new ConversationThread(); + + // Act + thread.AddSystemMessage("You are helpful"); + + // Assert + thread.Messages.Should().ContainSingle(m => + m.Role == MessageRole.System && + m.Content == "You are helpful"); + } + + [Fact] + public void AddUserMessage_ShouldAddCorrectRole() + { + // Arrange + var thread = new ConversationThread(); + + // Act + thread.AddUserMessage("Hello"); + + // Assert + thread.Messages.Should().ContainSingle(m => + m.Role == MessageRole.User && + m.Content == "Hello"); + } + + [Fact] + public void AddAssistantMessage_ShouldAddCorrectRole() + { + // Arrange + var thread = new ConversationThread(); + + // Act + thread.AddAssistantMessage("Hi there!"); + + // Assert + thread.Messages.Should().ContainSingle(m => + m.Role == MessageRole.Assistant && + m.Content == "Hi there!"); + } + + [Fact] + public void AddToolMessage_ShouldAddCorrectRoleAndMetadata() + { + // Arrange + var thread = new ConversationThread(); + + // Act + thread.AddToolMessage("call-123", "search_tool", "results here"); + + // Assert + thread.Messages.Should().ContainSingle(m => + m.Role == MessageRole.Tool && + m.ToolCallId == "call-123" && + m.ToolName == "search_tool" && + m.Content == "results here"); + } + + [Fact] + public void AddMessage_WithTokenCount_ShouldUpdateTotalTokens() + { + // Arrange + var thread = new ConversationThread(); + + // Act + thread.AddMessage(new ConversationMessage + { + Role = MessageRole.User, + Content = "Hello", + TokenCount = 5 + }); + thread.AddMessage(new ConversationMessage + { + Role = MessageRole.Assistant, + Content = "Hi there!", + TokenCount = 10 + }); + + // Assert + thread.TotalTokens.Should().Be(15); + } + } +} diff --git a/test/WorkflowCore.AI.AzureFoundry.Tests/InMemoryConversationStoreTests.cs b/test/WorkflowCore.AI.AzureFoundry.Tests/InMemoryConversationStoreTests.cs new file mode 100644 index 000000000..1d7262598 --- /dev/null +++ b/test/WorkflowCore.AI.AzureFoundry.Tests/InMemoryConversationStoreTests.cs @@ -0,0 +1,87 @@ +using System.Threading.Tasks; +using WorkflowCore.AI.AzureFoundry.Models; +using WorkflowCore.AI.AzureFoundry.Services; +using Xunit; +using FluentAssertions; + +namespace WorkflowCore.AI.AzureFoundry.Tests +{ + public class InMemoryConversationStoreTests + { + private readonly InMemoryConversationStore _store; + + public InMemoryConversationStoreTests() + { + _store = new InMemoryConversationStore(); + } + + [Fact] + public async Task GetOrCreateThreadAsync_ShouldCreateNewThread() + { + // Act + var thread = await _store.GetOrCreateThreadAsync("workflow-1", "pointer-1"); + + // Assert + thread.Should().NotBeNull(); + thread.WorkflowInstanceId.Should().Be("workflow-1"); + thread.ExecutionPointerId.Should().Be("pointer-1"); + thread.Messages.Should().BeEmpty(); + } + + [Fact] + public async Task GetOrCreateThreadAsync_ShouldReturnExistingThread() + { + // Arrange + var firstThread = await _store.GetOrCreateThreadAsync("workflow-1", "pointer-1"); + firstThread.AddUserMessage("Hello"); + await _store.SaveThreadAsync(firstThread); + + // Act + var secondThread = await _store.GetOrCreateThreadAsync("workflow-1", "pointer-1"); + + // Assert + secondThread.Id.Should().Be(firstThread.Id); + secondThread.Messages.Should().HaveCount(1); + } + + [Fact] + public async Task SaveAndGetThread_ShouldPersistMessages() + { + // Arrange + var thread = new ConversationThread + { + WorkflowInstanceId = "workflow-2", + ExecutionPointerId = "pointer-2" + }; + thread.AddSystemMessage("You are a helpful assistant"); + thread.AddUserMessage("Hello"); + thread.AddAssistantMessage("Hi there!"); + + // Act + await _store.SaveThreadAsync(thread); + var retrieved = await _store.GetThreadAsync(thread.Id); + + // Assert + retrieved.Should().NotBeNull(); + retrieved.Messages.Should().HaveCount(3); + retrieved.Messages[0].Role.Should().Be(MessageRole.System); + retrieved.Messages[1].Role.Should().Be(MessageRole.User); + retrieved.Messages[2].Role.Should().Be(MessageRole.Assistant); + } + + [Fact] + public async Task DeleteThreadAsync_ShouldRemoveThread() + { + // Arrange + var thread = await _store.GetOrCreateThreadAsync("workflow-3", "pointer-3"); + await _store.SaveThreadAsync(thread); + + // Act + await _store.DeleteThreadAsync(thread.Id); + var retrieved = await _store.GetThreadAsync(thread.Id); + + // Assert + retrieved.Should().BeNull(); + } + } +} diff --git a/test/WorkflowCore.AI.AzureFoundry.Tests/ToolRegistryTests.cs b/test/WorkflowCore.AI.AzureFoundry.Tests/ToolRegistryTests.cs new file mode 100644 index 000000000..0f4b09d47 --- /dev/null +++ b/test/WorkflowCore.AI.AzureFoundry.Tests/ToolRegistryTests.cs @@ -0,0 +1,97 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using WorkflowCore.AI.AzureFoundry.Interface; +using WorkflowCore.AI.AzureFoundry.Models; +using WorkflowCore.AI.AzureFoundry.Services; +using Xunit; +using FluentAssertions; + +namespace WorkflowCore.AI.AzureFoundry.Tests +{ + public class ToolRegistryTests + { + [Fact] + public void Register_ShouldAddToolToRegistry() + { + // Arrange + var registry = new ToolRegistry(null); + var tool = new TestTool(); + + // Act + registry.Register(tool); + + // Assert + registry.HasTool("test_tool").Should().BeTrue(); + registry.GetTool("test_tool").Should().Be(tool); + } + + [Fact] + public void GetTool_ShouldReturnNullForUnregisteredTool() + { + // Arrange + var registry = new ToolRegistry(null); + + // Act + var tool = registry.GetTool("nonexistent"); + + // Assert + tool.Should().BeNull(); + } + + [Fact] + public void GetAllTools_ShouldReturnAllRegisteredTools() + { + // Arrange + var registry = new ToolRegistry(null); + registry.Register(new TestTool()); + registry.Register(new AnotherTestTool()); + + // Act + var tools = registry.GetAllTools(); + + // Assert + tools.Should().HaveCount(2); + } + + [Fact] + public void GetToolDefinitions_ShouldReturnDefinitionsForAllTools() + { + // Arrange + var registry = new ToolRegistry(null); + registry.Register(new TestTool()); + + // Act + var definitions = registry.GetToolDefinitions(); + + // Assert + definitions.Should().ContainSingle(d => + d.Name == "test_tool" && + d.Description == "A test tool"); + } + + private class TestTool : IAgentTool + { + public string Name => "test_tool"; + public string Description => "A test tool"; + public string ParametersSchema => "{}"; + + public Task ExecuteAsync(string toolCallId, string arguments, CancellationToken cancellationToken = default) + { + return Task.FromResult(ToolResult.Succeeded(toolCallId, Name, "test result")); + } + } + + private class AnotherTestTool : IAgentTool + { + public string Name => "another_tool"; + public string Description => "Another test tool"; + public string ParametersSchema => "{}"; + + public Task ExecuteAsync(string toolCallId, string arguments, CancellationToken cancellationToken = default) + { + return Task.FromResult(ToolResult.Succeeded(toolCallId, Name, "another result")); + } + } + } +} diff --git a/test/WorkflowCore.AI.AzureFoundry.Tests/WorkflowCore.AI.AzureFoundry.Tests.csproj b/test/WorkflowCore.AI.AzureFoundry.Tests/WorkflowCore.AI.AzureFoundry.Tests.csproj new file mode 100644 index 000000000..9a5ec0ac6 --- /dev/null +++ b/test/WorkflowCore.AI.AzureFoundry.Tests/WorkflowCore.AI.AzureFoundry.Tests.csproj @@ -0,0 +1,21 @@ + + + + WorkflowCore.AI.AzureFoundry.Tests + WorkflowCore.AI.AzureFoundry.Tests + true + false + false + false + + + net10.0 + true + + + + + + + + From 7d1db1456cb0c2209ba10d0e101437246ea012cb Mon Sep 17 00:00:00 2001 From: Daniel Gerlag Date: Thu, 29 Jan 2026 16:25:42 +0000 Subject: [PATCH 2/3] test targets --- .github/workflows/dotnet.yml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 5527c4c59..253d321bd 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -18,6 +18,7 @@ jobs: 6.0.x 8.0.x 9.0.x + 10.0.x - name: Restore dependencies run: dotnet restore - name: Build @@ -49,6 +50,7 @@ jobs: 6.0.x 8.0.x 9.0.x + 10.0.x - name: Restore dependencies run: dotnet restore - name: Build @@ -80,7 +82,7 @@ jobs: 6.0.x 8.0.x 9.0.x - - name: Restore dependencies + 10.0.x run: dotnet restore - name: Build run: dotnet build --no-restore @@ -111,6 +113,7 @@ jobs: 6.0.x 8.0.x 9.0.x + 10.0.x - name: Restore dependencies run: dotnet restore - name: Build @@ -142,6 +145,7 @@ jobs: 6.0.x 8.0.x 9.0.x + 10.0.x - name: Restore dependencies run: dotnet restore - name: Build @@ -173,6 +177,7 @@ jobs: 6.0.x 8.0.x 9.0.x + 10.0.x - name: Restore dependencies run: dotnet restore - name: Build @@ -204,6 +209,7 @@ jobs: 6.0.x 8.0.x 9.0.x + 10.0.x - name: Restore dependencies run: dotnet restore - name: Build @@ -235,6 +241,7 @@ jobs: 6.0.x 8.0.x 9.0.x + 10.0.x - name: Restore dependencies run: dotnet restore - name: Build @@ -266,6 +273,7 @@ jobs: 6.0.x 8.0.x 9.0.x + 10.0.x - name: Restore dependencies run: dotnet restore - name: Build From b2359182072b50a8e25352144a583b3e1f904fc5 Mon Sep 17 00:00:00 2001 From: Daniel Gerlag Date: Thu, 29 Jan 2026 16:46:53 +0000 Subject: [PATCH 3/3] tests --- .../WorkflowCore.AI.AzureFoundry.Tests.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/WorkflowCore.AI.AzureFoundry.Tests/WorkflowCore.AI.AzureFoundry.Tests.csproj b/test/WorkflowCore.AI.AzureFoundry.Tests/WorkflowCore.AI.AzureFoundry.Tests.csproj index 9a5ec0ac6..46cc5e004 100644 --- a/test/WorkflowCore.AI.AzureFoundry.Tests/WorkflowCore.AI.AzureFoundry.Tests.csproj +++ b/test/WorkflowCore.AI.AzureFoundry.Tests/WorkflowCore.AI.AzureFoundry.Tests.csproj @@ -9,7 +9,7 @@ false - net10.0 + net8.0 true