Merge project-oagents into agnext

This commit is contained in:
Reuben Bond 2024-06-14 11:53:19 -07:00
commit c5cec3afbd
227 changed files with 24863 additions and 0 deletions

478
dotnet/.editorconfig Normal file
View File

@ -0,0 +1,478 @@
; EditorConfig to support per-solution formatting.
; Use the EditorConfig VS add-in to make this work.
; http://editorconfig.org/
;
; Here are some resources for what's supported for .NET/C#
; https://kent-boogaart.com/blog/editorconfig-reference-for-c-developers
; https://learn.microsoft.com/visualstudio/ide/editorconfig-code-style-settings-reference
;
; Be **careful** editing this because some of the rules don't support adding a severity level
; For instance if you change to `dotnet_sort_system_directives_first = true:warning` (adding `:warning`)
; then the rule will be silently ignored.
; This is the default for the codeline.
root = true
[*]
indent_style = space
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
spelling_exclusion_path = spelling.dic
[*.cs]
indent_size = 4
dotnet_sort_system_directives_first = true
# Don't use this. qualifier
dotnet_style_qualification_for_field = false:suggestion
dotnet_style_qualification_for_property = false:suggestion
# use int x = .. over Int32
dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion
# use int.MaxValue over Int32.MaxValue
dotnet_style_predefined_type_for_member_access = true:suggestion
# Require var all the time.
csharp_style_var_for_built_in_types = true:suggestion
csharp_style_var_when_type_is_apparent = true:suggestion
csharp_style_var_elsewhere = true:suggestion
# Disallow throw expressions.
csharp_style_throw_expression = false:suggestion
# Newline settings
csharp_new_line_before_open_brace = all
csharp_new_line_before_else = true
csharp_new_line_before_catch = true
csharp_new_line_before_finally = true
csharp_new_line_before_members_in_object_initializers = true
csharp_new_line_before_members_in_anonymous_types = true
# Namespace settings
csharp_style_namespace_declarations = file_scoped
# Brace settings
csharp_prefer_braces = true # Prefer curly braces even for one line of code
# name all constant fields using PascalCase
dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = warning
dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields
dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style
dotnet_naming_symbols.constant_fields.applicable_kinds = field
dotnet_naming_symbols.constant_fields.required_modifiers = const
dotnet_naming_style.pascal_case_style.capitalization = pascal_case
# static fields should have s_ prefix
dotnet_naming_rule.static_fields_should_have_prefix.severity = warning
dotnet_naming_rule.static_fields_should_have_prefix.symbols = static_fields
dotnet_naming_rule.static_fields_should_have_prefix.style = static_prefix_style
dotnet_naming_symbols.static_fields.applicable_kinds = field
dotnet_naming_symbols.static_fields.required_modifiers = static
dotnet_naming_symbols.static_fields.applicable_accessibilities = private, internal, private_protected
dotnet_naming_style.static_prefix_style.required_prefix = s_
dotnet_naming_style.static_prefix_style.capitalization = camel_case
# internal and private fields should be _camelCase
dotnet_naming_rule.camel_case_for_private_internal_fields.severity = warning
dotnet_naming_rule.camel_case_for_private_internal_fields.symbols = private_internal_fields
dotnet_naming_rule.camel_case_for_private_internal_fields.style = camel_case_underscore_style
dotnet_naming_symbols.private_internal_fields.applicable_kinds = field
dotnet_naming_symbols.private_internal_fields.applicable_accessibilities = private, internal
dotnet_naming_style.camel_case_underscore_style.required_prefix = _
dotnet_naming_style.camel_case_underscore_style.capitalization = camel_case
[*.{xml,config,*proj,nuspec,props,resx,targets,yml,tasks}]
indent_size = 2
# Xml config files
[*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}]
indent_size = 2
[*.json]
indent_size = 2
[*.{ps1,psm1}]
indent_size = 4
[*.sh]
indent_size = 4
end_of_line = lf
[*.{razor,cshtml}]
charset = utf-8-bom
[*.{cs,vb}]
# SYSLIB1054: Use 'LibraryImportAttribute' instead of 'DllImportAttribute' to generate P/Invoke marshalling code at compile time
dotnet_diagnostic.SYSLIB1054.severity = warning
# CA1018: Mark attributes with AttributeUsageAttribute
dotnet_diagnostic.CA1018.severity = warning
# CA1047: Do not declare protected member in sealed type
dotnet_diagnostic.CA1047.severity = warning
# CA1305: Specify IFormatProvider
dotnet_diagnostic.CA1305.severity = warning
# CA1507: Use nameof to express symbol names
dotnet_diagnostic.CA1507.severity = warning
# CA1510: Use ArgumentNullException throw helper
dotnet_diagnostic.CA1510.severity = warning
# CA1511: Use ArgumentException throw helper
dotnet_diagnostic.CA1511.severity = warning
# CA1512: Use ArgumentOutOfRangeException throw helper
dotnet_diagnostic.CA1512.severity = warning
# CA1513: Use ObjectDisposedException throw helper
dotnet_diagnostic.CA1513.severity = warning
# CA1725: Parameter names should match base declaration
dotnet_diagnostic.CA1725.severity = suggestion
# CA1802: Use literals where appropriate
dotnet_diagnostic.CA1802.severity = warning
# CA1805: Do not initialize unnecessarily
dotnet_diagnostic.CA1805.severity = warning
# CA1810: Do not initialize unnecessarily
dotnet_diagnostic.CA1810.severity = warning
# CA1821: Remove empty Finalizers
dotnet_diagnostic.CA1821.severity = warning
# CA1822: Make member static
dotnet_diagnostic.CA1822.severity = warning
dotnet_code_quality.CA1822.api_surface = private, internal
# CA1823: Avoid unused private fields
dotnet_diagnostic.CA1823.severity = warning
# CA1825: Avoid zero-length array allocations
dotnet_diagnostic.CA1825.severity = warning
# CA1826: Do not use Enumerable methods on indexable collections. Instead use the collection directly
dotnet_diagnostic.CA1826.severity = warning
# CA1827: Do not use Count() or LongCount() when Any() can be used
dotnet_diagnostic.CA1827.severity = warning
# CA1828: Do not use CountAsync() or LongCountAsync() when AnyAsync() can be used
dotnet_diagnostic.CA1828.severity = warning
# CA1829: Use Length/Count property instead of Count() when available
dotnet_diagnostic.CA1829.severity = warning
# CA1830: Prefer strongly-typed Append and Insert method overloads on StringBuilder
dotnet_diagnostic.CA1830.severity = warning
# CA1831: Use AsSpan or AsMemory instead of Range-based indexers when appropriate
dotnet_diagnostic.CA1831.severity = warning
# CA1832: Use AsSpan or AsMemory instead of Range-based indexers when appropriate
dotnet_diagnostic.CA1832.severity = warning
# CA1833: Use AsSpan or AsMemory instead of Range-based indexers when appropriate
dotnet_diagnostic.CA1833.severity = warning
# CA1834: Consider using 'StringBuilder.Append(char)' when applicable
dotnet_diagnostic.CA1834.severity = warning
# CA1835: Prefer the 'Memory'-based overloads for 'ReadAsync' and 'WriteAsync'
dotnet_diagnostic.CA1835.severity = warning
# CA1836: Prefer IsEmpty over Count
dotnet_diagnostic.CA1836.severity = warning
# CA1837: Use 'Environment.ProcessId'
dotnet_diagnostic.CA1837.severity = warning
# CA1838: Avoid 'StringBuilder' parameters for P/Invokes
dotnet_diagnostic.CA1838.severity = warning
# CA1839: Use 'Environment.ProcessPath'
dotnet_diagnostic.CA1839.severity = warning
# CA1840: Use 'Environment.CurrentManagedThreadId'
dotnet_diagnostic.CA1840.severity = warning
# CA1841: Prefer Dictionary.Contains methods
dotnet_diagnostic.CA1841.severity = warning
# CA1842: Do not use 'WhenAll' with a single task
dotnet_diagnostic.CA1842.severity = warning
# CA1843: Do not use 'WaitAll' with a single task
dotnet_diagnostic.CA1843.severity = warning
# CA1844: Provide memory-based overrides of async methods when subclassing 'Stream'
dotnet_diagnostic.CA1844.severity = warning
# CA1845: Use span-based 'string.Concat'
dotnet_diagnostic.CA1845.severity = warning
# CA1846: Prefer AsSpan over Substring
dotnet_diagnostic.CA1846.severity = warning
# CA1847: Use string.Contains(char) instead of string.Contains(string) with single characters
dotnet_diagnostic.CA1847.severity = warning
# CA1852: Seal internal types
dotnet_diagnostic.CA1852.severity = warning
# CA1854: Prefer the IDictionary.TryGetValue(TKey, out TValue) method
dotnet_diagnostic.CA1854.severity = warning
# CA1855: Prefer 'Clear' over 'Fill'
dotnet_diagnostic.CA1855.severity = warning
# CA1856: Incorrect usage of ConstantExpected attribute
dotnet_diagnostic.CA1856.severity = error
# CA1857: A constant is expected for the parameter
dotnet_diagnostic.CA1857.severity = warning
# CA1858: Use 'StartsWith' instead of 'IndexOf'
dotnet_diagnostic.CA1858.severity = warning
# CA2007: Consider calling ConfigureAwait on the awaited task
dotnet_diagnostic.CA2007.severity = warning
# CA2008: Do not create tasks without passing a TaskScheduler
dotnet_diagnostic.CA2008.severity = warning
# CA2009: Do not call ToImmutableCollection on an ImmutableCollection value
dotnet_diagnostic.CA2009.severity = warning
# CA2011: Avoid infinite recursion
dotnet_diagnostic.CA2011.severity = warning
# CA2012: Use ValueTask correctly
dotnet_diagnostic.CA2012.severity = warning
# CA2013: Do not use ReferenceEquals with value types
dotnet_diagnostic.CA2013.severity = warning
# CA2014: Do not use stackalloc in loops.
dotnet_diagnostic.CA2014.severity = warning
# CA2016: Forward the 'CancellationToken' parameter to methods that take one
dotnet_diagnostic.CA2016.severity = warning
# CA2200: Rethrow to preserve stack details
dotnet_diagnostic.CA2200.severity = warning
# CA2201: Do not raise reserved exception types
dotnet_diagnostic.CA2201.severity = warning
# CA2208: Instantiate argument exceptions correctly
dotnet_diagnostic.CA2208.severity = warning
# CA2245: Do not assign a property to itself
dotnet_diagnostic.CA2245.severity = warning
# CA2246: Assigning symbol and its member in the same statement
dotnet_diagnostic.CA2246.severity = warning
# CA2249: Use string.Contains instead of string.IndexOf to improve readability.
dotnet_diagnostic.CA2249.severity = warning
# IDE0005: Remove unnecessary usings
dotnet_diagnostic.IDE0005.severity = warning
# IDE0011: Curly braces to surround blocks of code
dotnet_diagnostic.IDE0011.severity = warning
# IDE0020: Use pattern matching to avoid is check followed by a cast (with variable)
dotnet_diagnostic.IDE0020.severity = warning
# IDE0029: Use coalesce expression (non-nullable types)
dotnet_diagnostic.IDE0029.severity = warning
# IDE0030: Use coalesce expression (nullable types)
dotnet_diagnostic.IDE0030.severity = warning
# IDE0031: Use null propagation
dotnet_diagnostic.IDE0031.severity = warning
# IDE0035: Remove unreachable code
dotnet_diagnostic.IDE0035.severity = warning
# IDE0036: Order modifiers
csharp_preferred_modifier_order = public,private,protected,internal,file,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:suggestion
dotnet_diagnostic.IDE0036.severity = warning
# IDE0038: Use pattern matching to avoid is check followed by a cast (without variable)
dotnet_diagnostic.IDE0038.severity = warning
# IDE0043: Format string contains invalid placeholder
dotnet_diagnostic.IDE0043.severity = warning
# IDE0044: Make field readonly
dotnet_diagnostic.IDE0044.severity = warning
# IDE0051: Remove unused private members
dotnet_diagnostic.IDE0051.severity = warning
# IDE0055: All formatting rules
dotnet_diagnostic.IDE0055.severity = suggestion
# IDE0059: Unnecessary assignment to a value
dotnet_diagnostic.IDE0059.severity = warning
# IDE0060: Remove unused parameter
dotnet_code_quality_unused_parameters = non_public
dotnet_diagnostic.IDE0060.severity = warning
# IDE0062: Make local function static
dotnet_diagnostic.IDE0062.severity = warning
# IDE0073: File header
dotnet_diagnostic.IDE0073.severity = warning
file_header_template = Licensed to the .NET Foundation under one or more agreements.\nThe .NET Foundation licenses this file to you under the MIT license.
# IDE1006: Required naming style
dotnet_diagnostic.IDE1006.severity = warning
# IDE0161: Convert to file-scoped namespace
dotnet_diagnostic.IDE0161.severity = warning
# IDE0200: Lambda expression can be removed
dotnet_diagnostic.IDE0200.severity = warning
# IDE2000: Disallow multiple blank lines
dotnet_style_allow_multiple_blank_lines_experimental = false
dotnet_diagnostic.IDE2000.severity = warning
[{eng/tools/**.cs,**/{test,testassets,samples,Samples,perf,benchmarkapps,scripts,stress}/**.cs,src/Hosting/Server.IntegrationTesting/**.cs,src/Servers/IIS/IntegrationTesting.IIS/**.cs,src/Shared/Http2cat/**.cs,src/Testing/**.cs}]
# CA1018: Mark attributes with AttributeUsageAttribute
dotnet_diagnostic.CA1018.severity = suggestion
# CA1507: Use nameof to express symbol names
dotnet_diagnostic.CA1507.severity = suggestion
# CA1510: Use ArgumentNullException throw helper
dotnet_diagnostic.CA1510.severity = suggestion
# CA1511: Use ArgumentException throw helper
dotnet_diagnostic.CA1511.severity = suggestion
# CA1512: Use ArgumentOutOfRangeException throw helper
dotnet_diagnostic.CA1512.severity = suggestion
# CA1513: Use ObjectDisposedException throw helper
dotnet_diagnostic.CA1513.severity = suggestion
# CA1802: Use literals where appropriate
dotnet_diagnostic.CA1802.severity = suggestion
# CA1805: Do not initialize unnecessarily
dotnet_diagnostic.CA1805.severity = suggestion
# CA1810: Do not initialize unnecessarily
dotnet_diagnostic.CA1810.severity = suggestion
# CA1822: Make member static
dotnet_diagnostic.CA1822.severity = suggestion
# CA1823: Avoid zero-length array allocations
dotnet_diagnostic.CA1825.severity = suggestion
# CA1826: Do not use Enumerable methods on indexable collections. Instead use the collection directly
dotnet_diagnostic.CA1826.severity = suggestion
# CA1827: Do not use Count() or LongCount() when Any() can be used
dotnet_diagnostic.CA1827.severity = suggestion
# CA1829: Use Length/Count property instead of Count() when available
dotnet_diagnostic.CA1829.severity = suggestion
# CA1831: Use AsSpan or AsMemory instead of Range-based indexers when appropriate
dotnet_diagnostic.CA1831.severity = suggestion
# CA1832: Use AsSpan or AsMemory instead of Range-based indexers when appropriate
dotnet_diagnostic.CA1832.severity = suggestion
# CA1833: Use AsSpan or AsMemory instead of Range-based indexers when appropriate
dotnet_diagnostic.CA1833.severity = suggestion
# CA1834: Consider using 'StringBuilder.Append(char)' when applicable
dotnet_diagnostic.CA1834.severity = suggestion
# CA1835: Prefer the 'Memory'-based overloads for 'ReadAsync' and 'WriteAsync'
dotnet_diagnostic.CA1835.severity = suggestion
# CA1837: Use 'Environment.ProcessId'
dotnet_diagnostic.CA1837.severity = suggestion
# CA1838: Avoid 'StringBuilder' parameters for P/Invokes
dotnet_diagnostic.CA1838.severity = suggestion
# CA1841: Prefer Dictionary.Contains methods
dotnet_diagnostic.CA1841.severity = suggestion
# CA1844: Provide memory-based overrides of async methods when subclassing 'Stream'
dotnet_diagnostic.CA1844.severity = suggestion
# CA1845: Use span-based 'string.Concat'
dotnet_diagnostic.CA1845.severity = suggestion
# CA1846: Prefer AsSpan over Substring
dotnet_diagnostic.CA1846.severity = suggestion
# CA1847: Use string.Contains(char) instead of string.Contains(string) with single characters
dotnet_diagnostic.CA1847.severity = suggestion
# CA1852: Seal internal types
dotnet_diagnostic.CA1852.severity = suggestion
# CA1854: Prefer the IDictionary.TryGetValue(TKey, out TValue) method
dotnet_diagnostic.CA1854.severity = suggestion
# CA1855: Prefer 'Clear' over 'Fill'
dotnet_diagnostic.CA1855.severity = suggestion
# CA1856: Incorrect usage of ConstantExpected attribute
dotnet_diagnostic.CA1856.severity = suggestion
# CA1857: A constant is expected for the parameter
dotnet_diagnostic.CA1857.severity = suggestion
# CA1858: Use 'StartsWith' instead of 'IndexOf'
dotnet_diagnostic.CA1858.severity = suggestion
# CA2007: Consider calling ConfigureAwait on the awaited task
dotnet_diagnostic.CA2007.severity = suggestion
# CA2008: Do not create tasks without passing a TaskScheduler
dotnet_diagnostic.CA2008.severity = suggestion
# CA2012: Use ValueTask correctly
dotnet_diagnostic.CA2012.severity = suggestion
# CA2201: Do not raise reserved exception types
dotnet_diagnostic.CA2201.severity = suggestion
# CA2249: Use string.Contains instead of string.IndexOf to improve readability.
dotnet_diagnostic.CA2249.severity = suggestion
# IDE0005: Remove unnecessary usings
dotnet_diagnostic.IDE0005.severity = suggestion
# IDE0020: Use pattern matching to avoid is check followed by a cast (with variable)
dotnet_diagnostic.IDE0020.severity = suggestion
# IDE0029: Use coalesce expression (non-nullable types)
dotnet_diagnostic.IDE0029.severity = suggestion
# IDE0030: Use coalesce expression (nullable types)
dotnet_diagnostic.IDE0030.severity = suggestion
# IDE0031: Use null propagation
dotnet_diagnostic.IDE0031.severity = suggestion
# IDE0038: Use pattern matching to avoid is check followed by a cast (without variable)
dotnet_diagnostic.IDE0038.severity = suggestion
# IDE0044: Make field readonly
dotnet_diagnostic.IDE0044.severity = suggestion
# IDE0051: Remove unused private members
dotnet_diagnostic.IDE0051.severity = suggestion
# IDE0059: Unnecessary assignment to a value
dotnet_diagnostic.IDE0059.severity = suggestion
# IDE0060: Remove unused parameters
dotnet_diagnostic.IDE0060.severity = suggestion
# IDE0062: Make local function static
dotnet_diagnostic.IDE0062.severity = suggestion
# IDE0200: Lambda expression can be removed
dotnet_diagnostic.IDE0200.severity = suggestion
# CA2016: Forward the 'CancellationToken' parameter to methods that take one
dotnet_diagnostic.CA2016.severity = suggestion
# Defaults for content in the shared src/ and shared runtime dir
[{**/Shared/runtime/**.{cs,vb},src/Shared/test/Shared.Tests/runtime/**.{cs,vb},**/microsoft.extensions.hostfactoryresolver.sources/**.{cs,vb}}]
# CA1822: Make member static
dotnet_diagnostic.CA1822.severity = silent
# IDE0011: Use braces
dotnet_diagnostic.IDE0011.severity = silent
# IDE0055: Fix formatting
dotnet_diagnostic.IDE0055.severity = silent
# IDE0060: Remove unused parameters
dotnet_diagnostic.IDE0060.severity = silent
# IDE0062: Make local function static
dotnet_diagnostic.IDE0062.severity = silent
# IDE0161: Convert to file-scoped namespace
dotnet_diagnostic.IDE0161.severity = silent
[{**/Shared/**.cs,**/microsoft.extensions.hostfactoryresolver.sources/**.{cs,vb}}]
# IDE0005: Remove unused usings. Ignore for shared src files since imports for those depend on the projects in which they are included.
dotnet_diagnostic.IDE0005.severity = silent
[{*.razor.cs,src/Aspire.Dashboard/Components/**.cs}]
# CA2007: Consider calling ConfigureAwait on the awaited task
dotnet_diagnostic.CA2007.severity = silent

60
dotnet/.gitattributes vendored Normal file
View File

@ -0,0 +1,60 @@
# Set default behavior to automatically normalize line endings.
* text=auto
# Collapse these files in PRs by default
*.xlf linguist-generated=true
*.lcl linguist-generated=true
*.jpg binary
*.png binary
*.gif binary
# Force bash scripts to always use lf line endings so that if a repo is accessed
# in Unix via a file share from Windows, the scripts will work.
*.in text eol=lf
*.sh text eol=lf
# Likewise, force cmd and batch scripts to always use crlf
*.cmd text eol=crlf
*.bat text eol=crlf
*.cs text=auto diff=csharp
*.vb text=auto
*.resx text=auto
*.c text=auto
*.cpp text=auto
*.cxx text=auto
*.h text=auto
*.hxx text=auto
*.py text=auto
*.rb text=auto
*.java text=auto
*.html text=auto
*.htm text=auto
*.css text=auto
*.scss text=auto
*.sass text=auto
*.less text=auto
*.js text=auto
*.lisp text=auto
*.clj text=auto
*.sql text=auto
*.php text=auto
*.lua text=auto
*.m text=auto
*.asm text=auto
*.erl text=auto
*.fs text=auto
*.fsx text=auto
*.hs text=auto
*.csproj text=auto
*.vbproj text=auto
*.fsproj text=auto
*.dbproj text=auto
*.sln text=auto eol=crlf
# Set linguist language for .h files explicitly based on
# https://github.com/github/linguist/issues/1626#issuecomment-401442069
# this only affects the repo's language statistics
*.h linguist-language=C

146
dotnet/.gitignore vendored Normal file
View File

@ -0,0 +1,146 @@
syntax: glob
### VisualStudio ###
# Tools directory
.dotnet/
.packages/
.tools/
# User-specific files
*.suo
*.user
*.userosscache
*.sln.docstates
# Build results
artifacts/
artifacts_stage_1/
[Dd]ebug/
[Rr]elease/
x64/ !eng/common/cross/x64/
x86/ !eng/common/cross/x86/
[Bb]in/
[Oo]bj/
msbuild.log
msbuild.err
msbuild.wrn
*.binlog
# Visual Studio 2015
.vs/
# Visual Studio 2015 Pre-CTP6
*.sln.ide
*.ide/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
#NUNIT
*.VisualState.xml
TestResult.xml
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# DotCover is a Code Coverage Tool
*.dotCover
# NuGet Packages
*.nuget.props
*.nuget.targets
*.nupkg
**/packages/*
### Windows ###
# Windows image file caches
Thumbs.db
ehthumbs.db
# Folder config file
Desktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msm
*.msp
# Windows shortcuts
*.lnk
### Linux ###
*~
# KDE directory preferences
.directory
### OSX ###
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear on external disk
.Spotlight-V100
.Trashes
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
# vim temporary files
[._]*.s[a-w][a-z]
[._]s[a-w][a-z]
*.un~
Session.vim
.netrwhist
*~
# Visual Studio Code
.vscode/
# Private test configuration and binaries.
config.ps1
**/IISApplications
# Node.js modules
node_modules/
# Python Compile Outputs
*.pyc
# IntelliJ
.idea/
# vscode python env files
.env
# Storage emulator storage files
**/.azurite/*
# Azure Developer CLI files
/playground/**/.gitignore
/playground/**/azure.yaml
/playground/**/next-steps.md

82
dotnet/AGNext.sln Normal file
View File

@ -0,0 +1,82 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "csharp\src", "{290F9824-BAD3-4703-B9B7-FE9C4BE3A1CF}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AI.Agents", "csharp\src\Microsoft.AI.Agents\Microsoft.AI.Agents.csproj", "{B0BF1CD6-34E3-4ED4-9B2A-9B8781E9BE20}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AI.Agents.Dapr", "csharp\src\Microsoft.AI.Agents.Dapr\Microsoft.AI.Agents.Dapr.csproj", "{1972846E-4C21-4E40-B448-D78B73806BD9}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AI.Agents.Orleans", "csharp\src\Microsoft.AI.Agents.Orleans\Microsoft.AI.Agents.Orleans.csproj", "{A4AE4656-4919-45E2-9680-C317FBCF7693}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "examples", "examples\csharp", "{943853E7-513D-45EA-870F-549CFC0AF8E8}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "gh-flow", "gh-flow", "{E0E93575-7187-4975-8D72-6F285CD01767}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "csharp\src", "{50809508-F830-4553-9C4E-C802E0A0F690}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AI.DevTeam", "examples\csharp\gh-flow\src\Microsoft.AI.DevTeam\Microsoft.AI.DevTeam.csproj", "{79981945-61F7-4E1A-8949-7808FD75471B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AI.DevTeam.Dapr", "examples\csharp\gh-flow\src\Microsoft.AI.DevTeam.Dapr\Microsoft.AI.DevTeam.Dapr.csproj", "{A7677950-18F1-42FF-8018-870395417465}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "marketing", "marketing", "{1FF691E4-E27D-4A7E-861C-4D6291B6EE35}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "csharp\src", "{4225F3BA-A39D-4680-945E-F2869E98AEA2}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Marketing", "examples\csharp\marketing\src\backend\Marketing.csproj", "{62F276F3-9184-4908-A7FB-065B4E491BE2}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "seed-memory", "examples\csharp\gh-flow\src\seed-memory\seed-memory.csproj", "{EF5DF177-F4F2-49D5-9E1C-2E37869238D8}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{B0BF1CD6-34E3-4ED4-9B2A-9B8781E9BE20}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B0BF1CD6-34E3-4ED4-9B2A-9B8781E9BE20}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B0BF1CD6-34E3-4ED4-9B2A-9B8781E9BE20}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B0BF1CD6-34E3-4ED4-9B2A-9B8781E9BE20}.Release|Any CPU.Build.0 = Release|Any CPU
{1972846E-4C21-4E40-B448-D78B73806BD9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1972846E-4C21-4E40-B448-D78B73806BD9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1972846E-4C21-4E40-B448-D78B73806BD9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1972846E-4C21-4E40-B448-D78B73806BD9}.Release|Any CPU.Build.0 = Release|Any CPU
{A4AE4656-4919-45E2-9680-C317FBCF7693}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A4AE4656-4919-45E2-9680-C317FBCF7693}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A4AE4656-4919-45E2-9680-C317FBCF7693}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A4AE4656-4919-45E2-9680-C317FBCF7693}.Release|Any CPU.Build.0 = Release|Any CPU
{79981945-61F7-4E1A-8949-7808FD75471B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{79981945-61F7-4E1A-8949-7808FD75471B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{79981945-61F7-4E1A-8949-7808FD75471B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{79981945-61F7-4E1A-8949-7808FD75471B}.Release|Any CPU.Build.0 = Release|Any CPU
{A7677950-18F1-42FF-8018-870395417465}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A7677950-18F1-42FF-8018-870395417465}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A7677950-18F1-42FF-8018-870395417465}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A7677950-18F1-42FF-8018-870395417465}.Release|Any CPU.Build.0 = Release|Any CPU
{62F276F3-9184-4908-A7FB-065B4E491BE2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{62F276F3-9184-4908-A7FB-065B4E491BE2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{62F276F3-9184-4908-A7FB-065B4E491BE2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{62F276F3-9184-4908-A7FB-065B4E491BE2}.Release|Any CPU.Build.0 = Release|Any CPU
{EF5DF177-F4F2-49D5-9E1C-2E37869238D8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{EF5DF177-F4F2-49D5-9E1C-2E37869238D8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EF5DF177-F4F2-49D5-9E1C-2E37869238D8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EF5DF177-F4F2-49D5-9E1C-2E37869238D8}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{B0BF1CD6-34E3-4ED4-9B2A-9B8781E9BE20} = {290F9824-BAD3-4703-B9B7-FE9C4BE3A1CF}
{1972846E-4C21-4E40-B448-D78B73806BD9} = {290F9824-BAD3-4703-B9B7-FE9C4BE3A1CF}
{A4AE4656-4919-45E2-9680-C317FBCF7693} = {290F9824-BAD3-4703-B9B7-FE9C4BE3A1CF}
{E0E93575-7187-4975-8D72-6F285CD01767} = {943853E7-513D-45EA-870F-549CFC0AF8E8}
{50809508-F830-4553-9C4E-C802E0A0F690} = {E0E93575-7187-4975-8D72-6F285CD01767}
{79981945-61F7-4E1A-8949-7808FD75471B} = {50809508-F830-4553-9C4E-C802E0A0F690}
{A7677950-18F1-42FF-8018-870395417465} = {50809508-F830-4553-9C4E-C802E0A0F690}
{1FF691E4-E27D-4A7E-861C-4D6291B6EE35} = {943853E7-513D-45EA-870F-549CFC0AF8E8}
{4225F3BA-A39D-4680-945E-F2869E98AEA2} = {1FF691E4-E27D-4A7E-861C-4D6291B6EE35}
{62F276F3-9184-4908-A7FB-065B4E491BE2} = {4225F3BA-A39D-4680-945E-F2869E98AEA2}
{EF5DF177-F4F2-49D5-9E1C-2E37869238D8} = {943853E7-513D-45EA-870F-549CFC0AF8E8}
EndGlobalSection
EndGlobal

View File

@ -0,0 +1,8 @@
# Replace with your own values
export SERVICETYPE=AzureOpenAI
export SERVICEID=gpt-4
export DEPLOYMENTORMODELID=gpt-4
export EMBEDDINGDEPLOYMENTORMODELID=text-embedding-ada-002
export ENDPOINT="Error - you mus update your OpenAI Endpoint"
export APIKEY="Error - you must update your OpenAPI or Azure API key"

View File

@ -0,0 +1,201 @@
using System.Reflection;
using Azure;
using Azure.AI.OpenAI;
using Elsa.Extensions;
using Elsa.Workflows;
using Elsa.Workflows.Contracts;
using Elsa.Workflows.Models;
using Elsa.Workflows.UIHints;
using Microsoft.Extensions.Http.Resilience;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Connectors.OpenAI;
using Microsoft.SKDevTeam;
namespace Elsa.SemanticKernel;
//<summary>
// Loads the Semantic Kernel skills and then generates activites for each skill
//</summary>
public class SemanticKernelActivityProvider : IActivityProvider
{
private readonly IActivityFactory _activityFactory;
private readonly IActivityDescriber _activityDescriber;
public SemanticKernelActivityProvider(IActivityFactory activityFactory, IActivityDescriber activityDescriber)
{
_activityFactory = activityFactory;
_activityDescriber = activityDescriber;
}
public async ValueTask<IEnumerable<ActivityDescriptor>> GetDescriptorsAsync(CancellationToken cancellationToken = default)
{
// get the kernel
var kernel = KernelBuilder();
// get a list of skills in the assembly
var skills = LoadSkillsFromAssemblyAsync("skills", kernel);
var functionsAvailable = kernel.Plugins.GetFunctionsMetadata();
// create activity descriptors for each skilland function
var activities = new List<ActivityDescriptor>();
foreach (var function in functionsAvailable)
{
Console.WriteLine($"Creating Activities for Plugin: {function.PluginName}");
activities.Add(CreateActivityDescriptorFromSkillAndFunction(function, cancellationToken));
}
return activities;
}
/// <summary>
/// Creates an activity descriptor from a skill and function.
/// </summary>
/// <param name="function">The semantic kernel function</param>
/// <param name="cancellationToken">An optional cancellation token.</param>
/// <returns>An activity descriptor.</returns>
private ActivityDescriptor CreateActivityDescriptorFromSkillAndFunction(KernelFunctionMetadata function, CancellationToken cancellationToken = default)
{
// Create a fully qualified type name for the activity
var thisNamespace = GetType().Namespace;
var fullTypeName = $"{thisNamespace}.{function.PluginName}.{function.Name}";
Console.WriteLine($"Creating Activity: {fullTypeName}");
// create inputs from the function parameters - the SemanticKernelSkill activity will be the base for each activity
var inputs = new List<InputDescriptor>();
foreach (var p in function.Parameters) { inputs.Add(CreateInputDescriptorFromSKParameter(p)); }
inputs.Add(CreateInputDescriptor(typeof(string), "SkillName", function.PluginName, "The name of the skill to use (generated, do not change)"));
inputs.Add(CreateInputDescriptor(typeof(string), "FunctionName", function.Name, "The name of the function to use (generated, do not change)"));
inputs.Add(CreateInputDescriptor(typeof(int), "MaxRetries", KernelSettings.DefaultMaxRetries, "Max Retries to contact AI Service"));
return new ActivityDescriptor
{
Kind = ActivityKind.Task,
Category = "Semantic Kernel",
Description = function.Description,
Name = function.Name,
TypeName = fullTypeName,
Namespace = $"{thisNamespace}.{function.PluginName}",
DisplayName = $"{function.PluginName}.{function.Name}",
Inputs = inputs,
Outputs = new[] {new OutputDescriptor()},
Constructor = context =>
{
// The constructor is called when an activity instance of this type is requested.
// Create the activity instance.
var activityInstance = _activityFactory.Create<SemanticKernelSkill>(context);
// Customize the activity type name.
activityInstance.Type = fullTypeName;
// Configure the activity's URL and method properties.
activityInstance.SkillName = new Input<string?>(function.PluginName);
activityInstance.FunctionName = new Input<string?>(function.Name);
return activityInstance;
}
};
}
/// <summary>
/// Creates an input descriptor for a single line string
/// </summary>
/// <param name="name">The name of the input field</param>
/// <param name="description">The description of the input field</param>
private InputDescriptor CreateInputDescriptor(Type inputType, string name, Object defaultValue, string description)
{
var inputDescriptor = new InputDescriptor
{
Description = description,
DefaultValue = defaultValue,
Type = inputType,
Name = name,
DisplayName = name,
IsSynthetic = true, // This is a synthetic property, i.e. it is not part of the activity's .NET type.
IsWrapped = true, // This property is wrapped within an Input<T> object.
UIHint = InputUIHints.SingleLine,
ValueGetter = activity => activity.SyntheticProperties.GetValueOrDefault(name),
ValueSetter = (activity, value) => activity.SyntheticProperties[name] = value!,
};
return inputDescriptor;
}
/// <summary>
/// Creates an input descriptor from an sk funciton parameter definition.
/// </summary>
/// <param name="parameter">The function parameter.</param>
/// <returns>An input descriptor.</returns>
private InputDescriptor CreateInputDescriptorFromSKParameter(KernelParameterMetadata parameter)
{
var inputDescriptor = new InputDescriptor
{
Description = string.IsNullOrEmpty(parameter.Description) ? parameter.Name : parameter.Description,
DefaultValue = parameter.DefaultValue == null ? string.Empty : parameter.DefaultValue,
Type = typeof(string),
Name = parameter.Name,
DisplayName = parameter.Name,
IsSynthetic = true, // This is a synthetic property, i.e. it is not part of the activity's .NET type.
IsWrapped = true, // This property is wrapped within an Input<T> object.
UIHint = InputUIHints.MultiLine,
ValueGetter = activity => activity.SyntheticProperties.GetValueOrDefault(parameter.Name),
ValueSetter = (activity, value) => activity.SyntheticProperties[parameter.Name] = value!,
};
return inputDescriptor;
}
///<summary>
/// Gets a list of the skills in the assembly
///</summary>
private IEnumerable<string> LoadSkillsFromAssemblyAsync(string assemblyName, Kernel kernel)
{
var skills = new List<string>();
var assembly = Assembly.Load(assemblyName);
Type[] skillTypes = assembly.GetTypes().ToArray();
foreach (Type skillType in skillTypes)
{
if (skillType.Namespace.Equals("Microsoft.AI.DevTeam"))
{
skills.Add(skillType.Name);
var functions = skillType.GetFields();
foreach (var function in functions)
{
string field = function.FieldType.ToString();
if (field.Equals("Microsoft.AI.DevTeam.SemanticFunctionConfig"))
{
var promptTemplate = SemanticFunctionConfig.ForSkillAndFunction(skillType.Name, function.Name);
var skfunc = kernel.CreateFunctionFromPrompt(
promptTemplate.PromptTemplate, new OpenAIPromptExecutionSettings { MaxTokens = 8000, Temperature = 0.4, TopP = 1 });
Console.WriteLine($"SKActivityProvider Added SK function: {skfunc.Metadata.PluginName}.{skfunc.Name}");
}
}
}
}
return skills;
}
/// <summary>
/// Gets a semantic kernel instance
/// </summary>
/// <returns>Microsoft.SemanticKernel.IKernel</returns>
private Kernel KernelBuilder()
{
var kernelSettings = KernelSettings.LoadSettings();
var clientOptions = new OpenAIClientOptions();
clientOptions.Retry.NetworkTimeout = TimeSpan.FromMinutes(5);
var openAIClient = new OpenAIClient(new Uri(kernelSettings.Endpoint), new AzureKeyCredential(kernelSettings.ApiKey), clientOptions);
var builder = Kernel.CreateBuilder();
builder.Services.AddLogging( c=> c.AddConsole().AddDebug().SetMinimumLevel(LogLevel.Debug));
builder.Services.AddAzureOpenAIChatCompletion(kernelSettings.DeploymentOrModelId, openAIClient);
builder.Services.ConfigureHttpClientDefaults(c=>
{
c.AddStandardResilienceHandler().Configure( o=> {
o.Retry.MaxRetryAttempts = 5;
o.Retry.BackoffType = Polly.DelayBackoffType.Exponential;
});
});
return builder.Build();
}
}

View File

@ -0,0 +1,195 @@
using Elsa.Extensions;
using JetBrains.Annotations;
using System.Text;
using System.Reflection;
using Elsa.Workflows;
using Elsa.Workflows.Attributes;
using Elsa.Workflows.UIHints;
using Elsa.Workflows.Models;
using Microsoft.SKDevTeam;
using Microsoft.SemanticKernel.Connectors.OpenAI;
using Microsoft.SemanticKernel;
using Azure.AI.OpenAI;
using Azure;
using Microsoft.Extensions.Http.Resilience;
namespace Elsa.SemanticKernel;
/// <summary>
/// Invoke a Semantic Kernel skill.
/// </summary>
[Activity("Elsa", "Semantic Kernel", "Invoke a Semantic Kernel skill. ", DisplayName = "Generic Semantic Kernel Skill", Kind = ActivityKind.Task)]
[PublicAPI]
public class SemanticKernelSkill : CodeActivity<string>
{
[Input(
Description = "System Prompt",
UIHint = InputUIHints.MultiLine,
DefaultValue = PromptDefaults.SystemPrompt)]
public Input<string> SysPrompt { get; set; } = default!;
[Input(
Description = "User Input Prompt",
UIHint = InputUIHints.MultiLine,
DefaultValue = PromptDefaults.UserPrompt)]
public Input<string> Prompt { get; set; }
[Input(
Description = "Max retries",
UIHint = InputUIHints.SingleLine,
DefaultValue = KernelSettings.DefaultMaxRetries)]
public Input<int> MaxRetries { get; set; }
[Input(
Description = "The skill to invoke from the semantic kernel",
UIHint = InputUIHints.SingleLine,
DefaultValue = "Chat")]
public Input<string> SkillName { get; set; }
[Input(
Description = "The function to invoke from the skill",
UIHint = InputUIHints.SingleLine,
DefaultValue = "ChatCompletion")]
public Input<string> FunctionName { get; set; }
/* [Input(
Description = "Mockup - don't actually call the AI, just output the prompts",
UIHint = InputUIHints.Checkbox,
DefaultValue = false)]
public Input<bool> Mockup { get; set; } */
/// <inheritdoc />
protected override async ValueTask ExecuteAsync(ActivityExecutionContext workflowContext)
{
var test = SkillName.Get(workflowContext);
var skillName = SkillName.Get(workflowContext);
var functionName = FunctionName.Get(workflowContext);
var systemPrompt = SysPrompt.Get(workflowContext);
var maxRetries = MaxRetries.Get(workflowContext);
var prompt = Prompt.Get(workflowContext);
//var mockup = Mockup.Get(workflowContext);
var mockup = false;
string info = ($"#################\nSkill: {skillName}\nFunction: {functionName}\nPrompt: {prompt}\n#################\n\n");
if (mockup)
{
workflowContext.SetResult(info);
}
else
{
// get the kernel
var kernel = KernelBuilder();
// load the skill
var promptTemplate = SemanticFunctionConfig.ForSkillAndFunction(skillName, functionName);
var function = kernel.CreateFunctionFromPrompt(promptTemplate.PromptTemplate, new OpenAIPromptExecutionSettings { MaxTokens = 8000, Temperature = 0.4, TopP = 1 });
// set the context (our prompt)
var arguments = new KernelArguments{
["input"] = prompt
};
var answer = await kernel.InvokeAsync(function, arguments);
workflowContext.SetResult(answer);
}
}
/// <summary>
/// Load the skills into the kernel
/// </summary>
private string ListSkillsInKernel(Kernel kernel)
{
var theSkills = LoadSkillsFromAssemblyAsync("skills", kernel);
var functionsAvailable = kernel.Plugins.GetFunctionsMetadata();
var list = new StringBuilder();
foreach (var function in functionsAvailable)
{
Console.WriteLine($"Skill: {function.PluginName}");
// Function description
if (function.Description != null)
{
list.AppendLine($"// {function.Description}");
}
else
{
Console.WriteLine("{0}.{1} is missing a description", function.PluginName, function.Name);
list.AppendLine($"// Function {function.PluginName}.{function.Name}.");
}
// Function name
list.AppendLine($"{function.PluginName}.{function.Name}");
// Function parameters
foreach (var p in function.Parameters)
{
var description = string.IsNullOrEmpty(p.Description) ? p.Name : p.Description;
var defaultValueString = p.DefaultValue == null ? string.Empty : $" (default value: {p.DefaultValue})";
list.AppendLine($"Parameter \"{p.Name}\": {description} {defaultValueString}");
}
}
Console.WriteLine($"List of all skills ----- {list.ToString()}");
return list.ToString();
}
/// <summary>
/// Gets a semantic kernel instance
/// </summary>
/// <returns>Microsoft.SemanticKernel.IKernel</returns>
private Kernel KernelBuilder()
{
var kernelSettings = KernelSettings.LoadSettings();
var clientOptions = new OpenAIClientOptions();
clientOptions.Retry.NetworkTimeout = TimeSpan.FromMinutes(5);
var openAIClient = new OpenAIClient(new Uri(kernelSettings.Endpoint), new AzureKeyCredential(kernelSettings.ApiKey), clientOptions);
var builder = Kernel.CreateBuilder();
builder.Services.AddLogging( c=> c.AddConsole().AddDebug().SetMinimumLevel(LogLevel.Debug));
builder.Services.AddAzureOpenAIChatCompletion(kernelSettings.DeploymentOrModelId, openAIClient);
builder.Services.ConfigureHttpClientDefaults(c=>
{
c.AddStandardResilienceHandler().Configure( o=> {
o.Retry.MaxRetryAttempts = 5;
o.Retry.BackoffType = Polly.DelayBackoffType.Exponential;
});
});
return builder.Build();
}
///<summary>
/// Gets a list of the skills in the assembly
///</summary>
private IEnumerable<string> LoadSkillsFromAssemblyAsync(string assemblyName, Kernel kernel)
{
var skills = new List<string>();
var assembly = Assembly.Load(assemblyName);
Type[] skillTypes = assembly.GetTypes().ToArray();
foreach (Type skillType in skillTypes)
{
if (skillType.Namespace.Equals("Microsoft.SKDevTeam"))
{
skills.Add(skillType.Name);
var functions = skillType.GetFields();
foreach (var function in functions)
{
string field = function.FieldType.ToString();
if (field.Equals("Microsoft.SKDevTeam.SemanticFunctionConfig"))
{
var prompt = SemanticFunctionConfig.ForSkillAndFunction(skillType.Name, function.Name);
var skfunc = kernel.CreateFunctionFromPrompt(
prompt.PromptTemplate, new OpenAIPromptExecutionSettings { MaxTokens = 8000, Temperature = 0.4, TopP = 1 });
Console.WriteLine($"SK Added function: {skfunc.Metadata.PluginName}.{skfunc.Metadata.Name}");
}
}
}
}
return skills;
}
}

View File

@ -0,0 +1,95 @@
using System.Text.Json.Serialization;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using System.IO;
using System;
internal class KernelSettings
{
public const string DefaultConfigFile = "config/appsettings.json";
public const string OpenAI = "OPENAI";
public const string AzureOpenAI = "AZUREOPENAI";
public const int DefaultMaxRetries = 9;
[JsonPropertyName("serviceType")]
public string ServiceType { get; set; } = string.Empty;
[JsonPropertyName("serviceId")]
public string ServiceId { get; set; } = string.Empty;
[JsonPropertyName("deploymentOrModelId")]
public string DeploymentOrModelId { get; set; } = string.Empty;
[JsonPropertyName("embeddingDeploymentOrModelId")]
public string EmbeddingDeploymentOrModelId { get; set; } = string.Empty;
[JsonPropertyName("endpoint")]
public string Endpoint { get; set; } = string.Empty;
[JsonPropertyName("apiKey")]
public string ApiKey { get; set; } = string.Empty;
[JsonPropertyName("orgId")]
public string OrgId { get; set; } = string.Empty;
[JsonPropertyName("logLevel")]
public LogLevel? LogLevel { get; set; }
/// <summary>
/// Load the kernel settings from settings.json if the file exists and if not attempt to use user secrets.
/// </summary>
internal static KernelSettings LoadSettings()
{
try
{
if (File.Exists(DefaultConfigFile))
{
return FromFile(DefaultConfigFile);
}
Console.WriteLine($"Semantic kernel settings '{DefaultConfigFile}' not found, attempting to load configuration from user secrets.");
return FromUserSecrets();
}
catch (InvalidDataException ide)
{
Console.Error.WriteLine(
"Unable to load semantic kernel settings, please provide configuration settings using instructions in the README.\n" +
"Please refer to: https://github.com/microsoft/semantic-kernel-starters/blob/main/sk-csharp-hello-world/README.md#configuring-the-starter"
);
throw new InvalidOperationException(ide.Message);
}
}
/// <summary>
/// Load the kernel settings from the specified configuration file if it exists.
/// </summary>
internal static KernelSettings FromFile(string configFile = DefaultConfigFile)
{
if (!File.Exists(configFile))
{
throw new FileNotFoundException($"Configuration not found: {configFile}");
}
var configuration = new ConfigurationBuilder()
.SetBasePath(System.IO.Directory.GetCurrentDirectory())
.AddJsonFile(configFile, optional: true, reloadOnChange: true)
.Build();
return configuration.Get<KernelSettings>()
?? throw new InvalidDataException($"Invalid semantic kernel settings in '{configFile}', please provide configuration settings using instructions in the README.");
}
/// <summary>
/// Load the kernel settings from user secrets.
/// </summary>
internal static KernelSettings FromUserSecrets()
{
var configuration = new ConfigurationBuilder()
.AddUserSecrets<KernelSettings>()
.AddEnvironmentVariables()
.Build();
return configuration.Get<KernelSettings>()
?? throw new InvalidDataException("Invalid semantic kernel settings in user secrets, please provide configuration settings using instructions in the README.");
}
}

View File

@ -0,0 +1,7 @@
internal static class PromptDefaults {
public const string SystemPrompt = @"You are fulfilling roles on a software development team.
Provide a response to the following prompt, do not provide any additional output.";
public const string UserPrompt = @"Let's build a ToDoList Application!";
}

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<clear/>
<add key="NuGet official package source" value="https://api.nuget.org/v3/index.json" />
<add key="Jint prereleases" value="https://www.myget.org/F/jint/api/v3/index.json" />
<add key="Elsa prereleases" value="https://f.feedz.io/elsa-workflows/elsa-3/nuget/index.json" />
</packageSources>
</configuration>

View File

@ -0,0 +1,22 @@
@page "/"
@{
var serverUrl = $"{Request.Scheme}://{Request.Host}{Request.PathBase}{Request.Path}";
var apiUrl = serverUrl + "elsa/api";
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Elsa Workflows 3.0</title>
<link rel="stylesheet" href="https://rsms.me/inter/inter.css">
<link rel="stylesheet" href="_content/Elsa.Workflows.Designer/elsa-workflows-designer/elsa-workflows-designer.css">
<script src="_content/Elsa.Workflows.Designer/monaco-editor/min/vs/loader.js"></script>
<script type="module" src="_content/Elsa.Workflows.Designer/elsa-workflows-designer/elsa-workflows-designer.esm.js"></script>
</head>
<body>
<elsa-studio server="@apiUrl" monaco-lib-path="/_content/Elsa.Workflows.Designer/monaco-editor/min"></elsa-studio>
</body>
</html>

View File

@ -0,0 +1,2 @@
@namespace WorkflowsApp.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

View File

@ -0,0 +1,54 @@
using Elsa.EntityFrameworkCore.Extensions;
using Elsa.EntityFrameworkCore.Modules.Management;
using Elsa.EntityFrameworkCore.Modules.Runtime;
using Elsa.Extensions;
using Elsa.SemanticKernel;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddElsa(elsa =>
{
// Configure management feature to use EF Core.
elsa.UseWorkflowManagement(management => management.UseEntityFrameworkCore(ef => ef.UseSqlite()));
elsa.UseWorkflowRuntime(runtime =>runtime.UseEntityFrameworkCore());
// Expose API endpoints.
elsa.UseWorkflowsApi();
// Add services for HTTP activities and workflow middleware.
elsa.UseHttp();
// Configure identity so that we can create a default admin user.
elsa.UseIdentity(identity =>
{
identity.UseAdminUserProvider();
identity.TokenOptions = options => options.SigningKey = "secret-token-signing-key";
});
// Use default authentication (JWT + API Key).
elsa.UseDefaultAuthentication(auth => auth.UseAdminApiKey());
// Add Semantic Kernel skill.
elsa.AddActivity<SemanticKernelSkill>();
});
// Add dynamic Activity Provider for SK skills.
builder.Services.AddActivityProvider<SemanticKernelActivityProvider>();
// Add Razor pages.
builder.Services.AddRazorPages();
var app = builder.Build();
// Configure the HTTP request pipeline.
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseAuthentication();
app.UseAuthorization();
app.UseWorkflowsApi();
app.UseWorkflows();
app.MapRazorPages();
app.Run();

View File

@ -0,0 +1,37 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:10492",
"sslPort": 44312
}
},
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://localhost:5181",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://localhost:7077;http://localhost:5181",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View File

@ -0,0 +1,64 @@
# SemanticKernel Activity Provider for Elsa Workflows 3.x
The project supports running [Microsoft Semantic Kernel](https://github.com/microsoft/semantic-kernel) Skills as workflows using [Elsa Workflows](https://v3.elsaworkflows.io). You can build the workflows as .NET code or in the visual designer.
To run the designer:
```bash
> cd WorkflowsApp
> cp .env_example .env
# Edit the .env file to choose your AI model, add your API Endpoint, and secrets.
> . ./.env
> dotnet build
> dotnet run
# Open browser to the URI in the console output
```
By Default you can use "admin" and "password" to login. Please review [Workflow Security](https://v3.elsaworkflows.io/docs/installation/aspnet-apps-workflow-server) for into on securing the app, using API tokens, and more.
To [invoke](https://v3.elsaworkflows.io/docs/guides/invoking-workflows) a workflow, first it must be "Published". If your workflow has a trigger activity, you can use that. When your workflow is ready, click the "Publish" button. You can also execute the workflow using the API. Then, find the Workflow Definition ID. From a command line, you can use "curl":
```bash
> curl --location 'https://localhost:5001/elsa/api/workflow-definitions/{workflow_definition_id}/execute' \
--header 'Content-Type: application/json' \
--header 'Authorization: ApiKey {api_key}' \
--data '{
}'
```
Once you have the app runing locally, you can login (admin/password - see the [Elsa Workflows](https://v3.elsaworkflows.io) for info about securing). Then you can click "new workflow" to begin building your workflow with semantic kernel skills.
1. Drag workflow Activity blocks into the designer, and examine the settings.
2. Connect the Activities to specify an order of operations.
3. You can use Workfflow Variables to pass state between activities.
1. Create a Workflow Variable, "MyVariable"
2. Click on the Activity that you want to use to populate the variable.
3. In the Settings box for the Activity, Click "Output"
4. Set the "Output" to the variable chosen.
5. Click the Activity that will use the variable. Click on "Settings".
6. Find the text box representing the variable that you want to populate, in this case usually "input".
7. Click the "..." widget above the text box, and select "javascript"
8. Set the value of the text box to
```javascript
`${getMyVariable()}`
```
9. Run the workflow.
## Run via codespaces
The easiest way to run the project is in Codespaces. Codespaces will start a qdrant instance for you.
1. Create a new codespace from the *code* button on the main branch.
2. Once the code space setup is finished, from the terminal:
```bash
> cd cli
cli> cp ../WorkflowsApp/.env_example .
# Edit the .env file to choose your AI model, add your API Endpoint, and secrets.
cli> bash .env
cli> dotnet build
cli> dotnet run --file util/ToDoListSamplePrompt.txt do it
```
You will find the output in the *output/* directory.

View File

@ -0,0 +1,47 @@
namespace Microsoft.SKDevTeam;
public static class DevLead {
public static SemanticFunctionConfig Plan = new SemanticFunctionConfig
{
PromptTemplate = """
You are a Dev Lead for an application team, building the application described below.
Please break down the steps and modules required to develop the complete application, describe each step in detail.
Make prescriptive architecture, language, and frameowrk choices, do not provide a range of choices.
For each step or module then break down the steps or subtasks required to complete that step or module.
For each subtask write an LLM prompt that would be used to tell a model to write the coee that will accomplish that subtask. If the subtask involves taking action/running commands tell the model to write the script that will run those commands.
In each LLM prompt restrict the model from outputting other text that is not in the form of code or code comments.
Please output a JSON array data structure, in the precise schema shown below, with a list of steps and a description of each step, and the steps or subtasks that each requires, and the LLM prompts for each subtask.
Example:
{
"steps": [
{
"step": "1",
"description": "This is the first step",
"subtasks": [
{
"subtask": "Subtask 1",
"description": "This is the first subtask",
"prompt": "Write the code to do the first subtask"
},
{
"subtask": "Subtask 2",
"description": "This is the second subtask",
"prompt": "Write the code to do the second subtask"
}
]
}
]
}
Do not output any other text.
Input: {{$input}}
{{$wafContext}}
""",
Name = nameof(Plan),
SkillName = nameof(DevLead),
Description = "From a simple description of an application output a development plan for building the application.",
MaxTokens = 6500,
Temperature = 0.0,
TopP = 0.0,
PPenalty = 0.0,
FPenalty = 0.0
};
}

View File

@ -0,0 +1,46 @@
namespace Microsoft.SKDevTeam;
public static class Developer {
public static SemanticFunctionConfig Implement = new SemanticFunctionConfig
{
PromptTemplate = """
You are a Developer for an application.
Please output the code required to accomplish the task assigned to you below and wrap it in a bash script that creates the files.
Do not use any IDE commands and do not build and run the code.
Make specific choices about implementation. Do not offer a range of options.
Use comments in the code to describe the intent. Do not include other text other than code and code comments.
Input: {{$input}}
{{$wafContext}}
""",
Name = nameof(Implement),
SkillName = nameof(Developer),
Description = "From a description of a coding task out put the code or scripts necessary to complete the task.",
MaxTokens = 6500,
Temperature = 0.0,
TopP = 0.0,
PPenalty = 0.0,
FPenalty = 0.0
};
public static SemanticFunctionConfig Improve = new SemanticFunctionConfig
{
PromptTemplate = """
You are a Developer for an application. Your job is to imrove the code that you are given in the input below.
Please output a new version of code that fixes any problems with this version.
If there is an error message in the input you should fix that error in the code.
Wrap the code output up in a bash script that creates the necessary files by overwriting any previous files.
Do not use any IDE commands and do not build and run the code.
Make specific choices about implementation. Do not offer a range of options.
Use comments in the code to describe the intent. Do not include other text other than code and code comments.
Input: {{$input}}
{{$wafContext}}
""",
Name = nameof(Improve),
SkillName = nameof(Developer),
Description = "From a description of a coding task out put the code or scripts necessary to complete the task.",
MaxTokens = 6500,
Temperature = 0.0,
TopP = 0.0,
PPenalty = 0.0,
FPenalty = 0.0
};
}

View File

@ -0,0 +1,42 @@
namespace Microsoft.SKDevTeam;
public static class PM
{
public static SemanticFunctionConfig BootstrapProject = new SemanticFunctionConfig
{
PromptTemplate = """
Please write a bash script with the commands that would be required to generate applications as described in the following input.
You may add comments to the script and the generated output but do not add any other text except the bash script.
You may include commands to build the applications but do not run them.
Do not include any git commands.
Input: {{$input}}
{{$wafContext}}
""",
Name = nameof(BootstrapProject),
SkillName = nameof(PM),
Description = "Bootstrap a new project",
MaxTokens = 6500,
Temperature = 0.0,
TopP = 0.0,
PPenalty = 0.0,
FPenalty = 0.0
};
public static SemanticFunctionConfig Readme = new SemanticFunctionConfig
{
PromptTemplate = """
You are a program manager on a software development team. You are working on an app described below.
Based on the input below, and any dialog or other context, please output a raw README.MD markdown file documenting the main features of the app and the architecture or code organization.
Do not describe how to create the application.
Write the README as if it were documenting the features and architecture of the application. You may include instructions for how to run the application.
Input: {{$input}}
{{$wafContext}}
""",
Name = nameof(Readme),
SkillName = nameof(PM),
Description = "From a simple description output a README.md file for a GitHub repository.",
MaxTokens = 6500,
Temperature = 0.0,
TopP = 0.0,
PPenalty = 0.0,
FPenalty = 0.0
};
}

View File

@ -0,0 +1,23 @@
namespace Microsoft.SKDevTeam;
public class SemanticFunctionConfig
{
public string PromptTemplate { get; set; }
public string Name { get; set; }
public string SkillName { get; set; }
public string Description { get; set; }
public int MaxTokens { get; set; }
public double Temperature { get; set; }
public double TopP { get; set; }
public double PPenalty { get; set; }
public double FPenalty { get; set; }
public static SemanticFunctionConfig ForSkillAndFunction(string skillName, string functionName) =>
(skillName, functionName) switch
{
(nameof(PM), nameof(PM.Readme)) => PM.Readme,
(nameof(DevLead), nameof(DevLead.Plan)) => DevLead.Plan,
(nameof(Developer), nameof(Developer.Implement)) => Developer.Implement,
(nameof(Developer), nameof(Developer.Improve)) => Developer.Improve,
_ => throw new ArgumentException($"Unable to find {skillName}.{functionName}")
};
}

View File

@ -0,0 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Elsa.EntityFrameworkCore" Version="3.0.6" />
<PackageReference Include="Elsa.EntityFrameworkCore.Sqlite" Version="3.0.6" />
<PackageReference Include="Elsa.Identity" Version="3.0.6" />
<PackageReference Include="Elsa" Version="3.0.6" />
<PackageReference Include="Elsa.Http" Version="3.0.6" />
<PackageReference Include="Elsa.Workflows.Api" Version="3.0.6" />
<PackageReference Include="Elsa.Workflows.Core" Version="3.0.6" />
<PackageReference Include="Elsa.Workflows.Management" Version="3.0.6" />
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="8.0.0" />
<PackageReference Include="AspNetCore.Authentication.ApiKey" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Http.Resilience" Version="8.0.0" />
<PackageReference Include="Elsa.Workflows.Designer" Version="3.0.0-preview.727" />
<PackageReference Include="Microsoft.SemanticKernel" Version="1.6.2" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

View File

@ -0,0 +1,69 @@
# GitHub Dev Team with AI Agents
Build a Dev Team using event driven agents. This project is an experiment and is not intended to be used in production.
## Background
From a natural language specification, set out to integrate a team of AI agents into your teams dev process, either for discrete tasks on an existing repo (unit tests, pipeline expansions, PRs for specific intents), developing a new feature, or even building an application from scratch. Starting from an existing repo and a broad statement of intent, work with multiple AI agents, each of which has a different emphasis - from architecture, to task breakdown, to plans for individual tasks, to code output, code review, efficiency, documentation, build, writing tests, setting up pipelines, deployment, integration tests, and then validation.
The system will present a view that facilitates chain-of-thought coordination across multiple trees of reasoning with the dev team agents.
## Get it running
Check [the getting started guide](./docs/github-flow-getting-started.md).
## Demo
https://github.com/microsoft/azure-openai-dev-skills-orchestrator/assets/10728102/cafb1546-69ab-4c27-aaf5-1968313d637f
## Solution overview
![General overview](./docs/images/overview.png)
## How it works
* User begins with creating an issue and then stateing what they want to accomplish, natural language, as simple or as detailed as needed.
* Product manager agent will respond with a Readme, which can be iterated upon.
* User approves the readme or gives feedback via issue comments.
* Once the readme is approved, the user closes the issue and the Readme is commited to a PR.
* Developer lead agent responds with a decomposed plan for development, which also can be iterated upon.
* User approves the plan or gives feedback via issue comments.
* Once the readme is approved, the user closes the issue and the plan is used to break down the task to different developer agents.
* Developer agents respond with code, which can be iterated upon.
* User approves the code or gives feedback via issue comments.
* Once the code is approved, the user closes the issue and the code is commited to a PR.
```mermaid
graph TD;
NEA([NewAsk event]) -->|Hubber| NEA1[Creation of PM issue, DevLead issue, and new branch];
RR([ReadmeRequested event]) -->|ProductManager| PM1[Generation of new README];
NEA1 --> RR;
PM1 --> RG([ReadmeGenerated event]);
RG -->|Hubber| RC[Post the readme as a new comment on the issue];
RC --> RCC([ReadmeChainClosed event]);
RCC -->|ProductManager| RCR([ReadmeCreated event]);
RCR --> |AzureGenie| RES[Store Readme in blob storage];
RES --> RES2([ReadmeStored event]);
RES2 --> |Hubber| REC[Readme commited to branch and create new PR];
DPR([DevPlanRequested event]) -->|DeveloperLead| DPG[Generation of new development plan];
NEA1 --> DPR;
DPG --> DPGE([DevPlanGenerated event]);
DPGE -->|Hubber| DPGEC[Posting the plan as a new comment on the issue];
DPGEC --> DPCC([DevPlanChainClosed event]);
DPCC -->|DeveloperLead| DPCE([DevPlanCreated event]);
DPCE --> |Hubber| DPC[Creates a Dev issue for each subtask];
DPC([CodeGenerationRequested event]) -->|Developer| CG[Generation of new code];
CG --> CGE([CodeGenerated event]);
CGE -->|Hubber| CGC[Posting the code as a new comment on the issue];
CGC --> CCCE([CodeChainClosed event]);
CCCE -->|Developer| CCE([CodeCreated event]);
CCE --> |AzureGenie| CS[Store code in blob storage and schedule a run in the sandbox];
CS --> SRC([SandboxRunCreated event]);
SRC --> |Sandbox| SRM[Check every minute if the run finished];
SRM --> SRF([SandboxRunFinished event]);
SRF --> |Hubber| SRCC[Code files commited to branch];
```

View File

@ -0,0 +1,10 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json
name: ai-dev-team
services:
gh-flow:
project: "src/Microsoft.AI.DevTeam"
language: csharp
host: containerapp
docker:
context: ../../../../

View File

@ -0,0 +1,12 @@
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: agents-pubsub
spec:
type: pubsub.redis
version: v1
metadata:
- name: redisHost
value: localhost:6379
- name: redisPassword
value: ""

View File

@ -0,0 +1,14 @@
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: agents-statestore
spec:
type: state.redis
version: v1
metadata:
- name: redisHost
value: localhost:6379
- name: redisPassword
value: ""
- name: actorStateStore
value: "true"

View File

@ -0,0 +1,122 @@
## Prerequisites
- Access to gpt3.5-turbo or preferably gpt4 - [Get access here](https://learn.microsoft.com/en-us/azure/ai-services/openai/overview#how-do-i-get-access-to-azure-openai)
- [Setup a Github app](#how-do-i-setup-the-github-app)
- [Install the Github app](https://docs.github.com/en/apps/using-github-apps/installing-your-own-github-app)
- [Provision the azure resources](#how-do-I-deploy-the-azure-bits)
- [Create labels for the dev team skills](#which-labels-should-i-create)
### How do I setup the Github app?
- [Register a Github app](https://docs.github.com/en/apps/creating-github-apps/registering-a-github-app/registering-a-github-app), with the options listed below:
- Give your App a name and add a description
- Homepage URL: Can be anything (Example: repository URL)
- Add a dummy value for the webhook url, we'll come back to this setting
- Enter a webhook secret, which you'll need later on when filling in the `WebhookSecret` property in the `appsettings.json` file
- Setup the following permissions
- Repository
- Contents - read and write
- Issues - read and write
- Metadata - read only
- Pull requests - read and write
- Subscribe to the following events:
- Issues
- Issue comment
- Allow this app to be installed by any user or organization
- After the app is created, generate a private key, we'll use it later for authentication to Github from the app
### Which labels should I create?
In order for us to know which skill and persona we need to talk with, we are using Labels in Github Issues.
The default bunch of skills and personnas are as follow:
- PM.Readme
- Do.It
- DevLead.Plan
- Developer.Implement
Add them to your repository (They are not there by default).
Once you start adding your own skills, just remember to add the corresponding label to your repository.
## How do I run this locally?
Codespaces are preset for this repo. For codespaces there is a 'free' tier for individual accounts. See: https://github.com/pricing
Start by creating a codespace:
https://docs.github.com/en/codespaces/developing-in-a-codespace/creating-a-codespace-for-a-repository
![Alt text](./images/new-codespace.png)
In this sample's folder there are two files called appsettings.azure.template.json and appsettings.local.template.json. If you run this demo locally, use the local template and if you want to run it within Azure use the Azure template. Rename the selected file to appsettings.json and fill out the config values within the file.
### GitHubOptions
For the GitHubOptions section, you'll need to fill in the following values:
- **AppKey (PrivateKey)**: this is a key generated while creating a GitHub App. If you haven't saved it during creation, you'll need to generate a new one. Go to the settings of your GitHub app, scroll down to "Private keys" and click on "Generate a new private key". It will download a .pem file that contains your App Key. Then copy and paste all the **-----BEGIN RSA PRIVATE KEY---- your key -----END RSA PRIVATE KEY-----** content here, in one line.
- **AppId**: This can be found on the same page where you created your app. Go to the settings of your GitHub app and you can see the App ID at the top of the page.
- **InstallationId**: Access to your GitHub app installation and take note of the number (long type) at the end of the URL (which should be in the following format: https://github.com/settings/installations/installation-id).
- **WebhookSecret**: This is a value that you set when you create your app. In the app settings, go to the "Webhooks" section. Here you can find the "Secret" field where you can set your Webhook Secret.
### AzureOptions
The following fields are required and need to be filled in:
- **SubscriptionId**: The id of the subscription you want to work on.
- **Location**
- **ContainerInstancesResourceGroup**: The name of the resource group where container instances will be deployed.
- **FilesAccountName**: Azure Storage Account name.
- **FilesShareName**: The name of the File Share.
- **FilesAccountKey**: The File Account key.
- **SandboxImage**
In the Explorer tab in VS Code, find the Solution explorer, right click on the `gh-flow` project and click Debug -> Start new instance
![Alt text](./images/solution-explorer.png)
We'll need to expose the running application to the GH App webhooks, for example using [DevTunnels](https://learn.microsoft.com/en-us/azure/developer/dev-tunnels/overview), but any tool like ngrok can also work.
The following commands will create a persistent tunnel, so we need to only do this once:
```bash
TUNNEL_NAME=_name_your_tunnel_here_
devtunnel user login
devtunnel create -a $TUNNEL_NAME
devtunnel port create -p 5244 $TUNNEL_NAME
```
and once we have the tunnel created we can just start forwarding with the following command:
```bash
devtunnel host $TUNNEL_NAME
```
Copy the local address (it will look something like https://your_tunnel_name.euw.devtunnels.ms) and append `/api/github/webhooks` at the end. Using this value, update the Github App's webhook URL and you are ready to go!
Before you go and have the best of times, there is one last thing left to do [load the WAF into the vector DB](#load-the-waf-into-qdrant)
Also, since this project is relying on Orleans for the Agents implementation, there is a [dashboard](https://github.com/OrleansContrib/OrleansDashboard) available at https://yout_tunnel_name.euw.devtunnels.ms/dashboard, with useful metrics and stats related to the running Agents.
## How do I deploy the azure bits?
This sample is setup to use [azd](https://learn.microsoft.com/en-us/azure/developer/azure-developer-cli/overview) to work with the Azure bits. `azd` is installed in the codespace.
Let's start by logging in to Azure using
```bash
azd auth login
```
After we've logged in, we need to create a new environment provision the azure bits.
```bash
ENVIRONMENT=_name_of_your_env
azd env new $ENVIRONMENT
azd provision -e $ENVIRONMENT
```
After the provisioning is done, you can inspect the outputs with the following command
```bash
azd env get-values -e dev
```
As the last step, we also need to [load the WAF into the vector DB](#load-the-waf-into-qdrant)
### Load the WAF into Qdrant.
If you are running the app locally, we have [Qdrant](https://qdrant.tech/) setup in the Codespace and if you are running in Azure, Qdrant is deployed to ACA.
The loader is a project in the `samples` folder, called `seed-memory`. We need to fill in the `appsettings.json` (after renaming `appsettings.template.json` in `appsettings.json`) file in the `config` folder with the OpenAI details and the Qdrant endpoint, then just run the loader with `dotnet run` and you are ready to go.

Binary file not shown.

After

Width:  |  Height:  |  Size: 302 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 341 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

View File

@ -0,0 +1,14 @@
{
"appManagedEnvironments": "cae-",
"containerRegistryRegistries": "cr",
"insightsComponents": "appi-",
"operationalInsightsWorkspaces": "log-",
"portalDashboards": "dash-",
"resourcesResourceGroups": "rg-",
"storageStorageAccounts": "st",
"webServerFarms": "plan-",
"webSitesFunctions": "func-",
"appContainerApps": "ca-",
"managedIdentityUserAssignedIdentities": "id-",
"documentDBDatabaseAccounts":"cosmos-"
}

View File

@ -0,0 +1,46 @@
param accountName string
param location string = resourceGroup().location
param tags object = {}
param containers array = [
{
name: 'reminders'
id: 'reminders'
partitionKey: '/id'
}
{
name: 'persistence'
id: 'persistence'
partitionKey: '/id'
}
{
name: 'clustering'
id: 'clustering'
partitionKey: '/id'
}
]
param databaseName string = ''
param principalIds array = []
// Because databaseName is optional in main.bicep, we make sure the database name is set here.
var defaultDatabaseName = 'Todo'
var actualDatabaseName = !empty(databaseName) ? databaseName : defaultDatabaseName
module cosmos '../core/database/cosmos/sql/cosmos-sql-db.bicep' = {
name: 'cosmos-sql'
params: {
accountName: accountName
location: location
tags: tags
containers: containers
databaseName: actualDatabaseName
principalIds: principalIds
}
}
output accountName string = cosmos.outputs.accountName
output connectionStringKey string = cosmos.outputs.connectionStringKey
output databaseName string = cosmos.outputs.databaseName
output endpoint string = cosmos.outputs.endpoint
output roleDefinitionId string = cosmos.outputs.roleDefinitionId

View File

@ -0,0 +1,170 @@
param name string
param location string = resourceGroup().location
param tags object = {}
param applicationInsightsName string
param identityName string
param serviceName string = 'gh-flow'
param sandboxImage string = 'mcr.microsoft.com/dotnet/sdk:7.0'
param containerAppsEnvironmentName string
param containerRegistryName string
param storageAccountName string
param cosmosAccountName string
@secure()
param githubAppKey string
param githubAppId string
param githubAppInstallationId string
param rgName string
param aciShare string
param openAIServiceType string
param openAIServiceId string
param openAIDeploymentId string
param openAIEmbeddingId string
param openAIEndpoint string
@secure()
param openAIKey string
param qdrantEndpoint string
resource ghFlowIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = {
name: identityName
location: location
}
resource applicationInsights 'Microsoft.Insights/components@2020-02-02' existing = {
name: applicationInsightsName
}
resource storage 'Microsoft.Storage/storageAccounts@2021-09-01' existing = {
name: storageAccountName
}
resource cosmos 'Microsoft.DocumentDB/databaseAccounts@2022-08-15' existing = {
name: cosmosAccountName
}
var contributorRole = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')
var wehbookSecret = uniqueString(resourceGroup().id)
resource rgContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
name: guid(subscription().id, resourceGroup().id, contributorRole)
properties: {
roleDefinitionId: contributorRole
principalType: 'ServicePrincipal'
principalId: app.outputs.identityPrincipalId
}
}
module app '../core/host/container-app.bicep' = {
name: '${serviceName}-ghflow'
params: {
name: name
location: location
tags: union(tags, { 'azd-service-name': serviceName })
identityType: 'UserAssigned'
identityName: ghFlowIdentity.name
containerAppsEnvironmentName: containerAppsEnvironmentName
containerRegistryName: containerRegistryName
containerCpuCoreCount: '2.0'
containerMemory: '4.0Gi'
env: [
{
name: 'APPLICATIONINSIGHTS_CONNECTION_STRING'
value: applicationInsights.properties.ConnectionString
}
{
name: 'SANDBOX_IMAGE'
value: sandboxImage
}
{
name: 'GithubOptions__AppKey'
value: githubAppKey
}
{
name: 'GithubOptions__AppId'
value: githubAppId
}
{
name: 'GithubOptions__InstallationId'
value: githubAppInstallationId
}
{
name: 'GithubOptions__WebhookSecret'
value: wehbookSecret
}
{
name: 'AzureOptions__SubscriptionId'
value: subscription().subscriptionId
}
{
name: 'AzureOptions__Location'
value: location
}
{
name: 'AZURE_CLIENT_ID'
value: ghFlowIdentity.properties.clientId
}
{
name: 'AzureOptions__ContainerInstancesResourceGroup'
value: rgName
}
{
name: 'AzureOptions__FilesAccountKey'
value: storage.listKeys().keys[0].value
}
{
name: 'AzureOptions__FilesShareName'
value: aciShare
}
{
name: 'AzureOptions__FilesAccountName'
value: storageAccountName
}
{
name: 'AzureOptions__CosmosConnectionString'
value: cosmos.listConnectionStrings().connectionStrings[0].connectionString
}
{
name: 'OpenAIOptions__ServiceType'
value: openAIServiceType
}
{
name: 'OpenAIOptions__ServiceId'
value: openAIServiceId
}
{
name: 'OpenAIOptions__DeploymentOrModelId'
value: openAIDeploymentId
}
{
name: 'OpenAIOptions__EmbeddingDeploymentOrModelId'
value: openAIEmbeddingId
}
{
name: 'OpenAIOptions__Endpoint'
value: openAIEndpoint
}
{
name: 'OpenAIOptions__ApiKey'
value: openAIKey
}
{
name: 'QdrantOptions__Endpoint'
value: qdrantEndpoint
}
{
name: 'QdrantOptions__VectorSize'
value: '1536'
}
]
targetPort: 5274
}
}
output SERVICE_TRANSLATE_API_IDENTITY_PRINCIPAL_ID string = app.outputs.identityPrincipalId
output SERVICE_TRANSLATE_API_NAME string = app.outputs.name
output SERVICE_TRANSLATE_API_URI string = app.outputs.uri
output WEBHOOK_SECRET string = wehbookSecret

View File

@ -0,0 +1,37 @@
metadata description = 'Creates an Azure Cosmos DB account.'
param name string
param location string = resourceGroup().location
param tags object = {}
param connectionStringKey string = 'AZURE-COSMOS-CONNECTION-STRING'
@allowed([ 'GlobalDocumentDB', 'MongoDB', 'Parse' ])
param kind string
resource cosmos 'Microsoft.DocumentDB/databaseAccounts@2022-08-15' = {
name: name
kind: kind
location: location
tags: tags
properties: {
consistencyPolicy: { defaultConsistencyLevel: 'Session' }
locations: [
{
locationName: location
failoverPriority: 0
isZoneRedundant: false
}
]
databaseAccountOfferType: 'Standard'
enableAutomaticFailover: false
enableMultipleWriteLocations: false
apiProperties: (kind == 'MongoDB') ? { serverVersion: '4.0' } : {}
capabilities: [ { name: 'EnableServerless' } ]
}
}
output connectionStringKey string = connectionStringKey
output endpoint string = cosmos.properties.documentEndpoint
output id string = cosmos.id
output name string = cosmos.name

View File

@ -0,0 +1,19 @@
metadata description = 'Creates an Azure Cosmos DB for NoSQL account.'
param name string
param location string = resourceGroup().location
param tags object = {}
module cosmos '../../cosmos/cosmos-account.bicep' = {
name: 'cosmos-account'
params: {
name: name
location: location
tags: tags
kind: 'GlobalDocumentDB'
}
}
output connectionStringKey string = cosmos.outputs.connectionStringKey
output endpoint string = cosmos.outputs.endpoint
output id string = cosmos.outputs.id
output name string = cosmos.outputs.name

View File

@ -0,0 +1,72 @@
metadata description = 'Creates an Azure Cosmos DB for NoSQL account with a database.'
param accountName string
param databaseName string
param location string = resourceGroup().location
param tags object = {}
param containers array = []
param principalIds array = []
module cosmos 'cosmos-sql-account.bicep' = {
name: 'cosmos-sql-account'
params: {
name: accountName
location: location
tags: tags
}
}
resource database 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases@2022-05-15' = {
name: '${accountName}/${databaseName}'
properties: {
resource: { id: databaseName }
}
resource list 'containers' = [for container in containers: {
name: container.name
properties: {
resource: {
id: container.id
partitionKey: { paths: [ container.partitionKey ] }
}
options: {}
}
}]
dependsOn: [
cosmos
]
}
module roleDefinition 'cosmos-sql-role-def.bicep' = {
name: 'cosmos-sql-role-definition'
params: {
accountName: accountName
}
dependsOn: [
cosmos
database
]
}
// We need batchSize(1) here because sql role assignments have to be done sequentially
@batchSize(1)
module userRole 'cosmos-sql-role-assign.bicep' = [for principalId in principalIds: if (!empty(principalId)) {
name: 'cosmos-sql-user-role-${uniqueString(principalId)}'
params: {
accountName: accountName
roleDefinitionId: roleDefinition.outputs.id
principalId: principalId
}
dependsOn: [
cosmos
database
]
}]
output accountId string = cosmos.outputs.id
output accountName string = cosmos.outputs.name
output connectionStringKey string = cosmos.outputs.connectionStringKey
output databaseName string = databaseName
output endpoint string = cosmos.outputs.endpoint
output roleDefinitionId string = roleDefinition.outputs.id

View File

@ -0,0 +1,19 @@
metadata description = 'Creates a SQL role assignment under an Azure Cosmos DB account.'
param accountName string
param roleDefinitionId string
param principalId string = ''
resource role 'Microsoft.DocumentDB/databaseAccounts/sqlRoleAssignments@2022-05-15' = {
parent: cosmos
name: guid(roleDefinitionId, principalId, cosmos.id)
properties: {
principalId: principalId
roleDefinitionId: roleDefinitionId
scope: cosmos.id
}
}
resource cosmos 'Microsoft.DocumentDB/databaseAccounts@2022-08-15' existing = {
name: accountName
}

View File

@ -0,0 +1,30 @@
metadata description = 'Creates a SQL role definition under an Azure Cosmos DB account.'
param accountName string
resource roleDefinition 'Microsoft.DocumentDB/databaseAccounts/sqlRoleDefinitions@2022-08-15' = {
parent: cosmos
name: guid(cosmos.id, accountName, 'sql-role')
properties: {
assignableScopes: [
cosmos.id
]
permissions: [
{
dataActions: [
'Microsoft.DocumentDB/databaseAccounts/readMetadata'
'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/items/*'
'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/*'
]
notDataActions: []
}
]
roleName: 'Reader Writer'
type: 'CustomRole'
}
}
resource cosmos 'Microsoft.DocumentDB/databaseAccounts@2022-08-15' existing = {
name: accountName
}
output id string = roleDefinition.id

View File

@ -0,0 +1,64 @@
param name string
param location string = resourceGroup().location
param tags object = {}
param sku object
param storage object
param administratorLogin string
@secure()
param administratorLoginPassword string
param databaseNames array = []
param allowAzureIPsFirewall bool = false
param allowAllIPsFirewall bool = false
param allowedSingleIPs array = []
// PostgreSQL version
param version string
// Latest official version 2022-12-01 does not have Bicep types available
resource postgresServer 'Microsoft.DBforPostgreSQL/flexibleServers@2022-12-01' = {
location: location
tags: tags
name: name
sku: sku
properties: {
version: version
administratorLogin: administratorLogin
administratorLoginPassword: administratorLoginPassword
storage: storage
highAvailability: {
mode: 'Disabled'
}
}
resource database 'databases' = [for name in databaseNames: {
name: name
}]
resource firewall_all 'firewallRules' = if (allowAllIPsFirewall) {
name: 'allow-all-IPs'
properties: {
startIpAddress: '0.0.0.0'
endIpAddress: '255.255.255.255'
}
}
resource firewall_azure 'firewallRules' = if (allowAzureIPsFirewall) {
name: 'allow-all-azure-internal-IPs'
properties: {
startIpAddress: '0.0.0.0'
endIpAddress: '0.0.0.0'
}
}
resource firewall_single 'firewallRules' = [for ip in allowedSingleIPs: {
name: 'allow-single-${replace(ip, '.', '')}'
properties: {
startIpAddress: ip
endIpAddress: ip
}
}]
}
output POSTGRES_DOMAIN_NAME string = postgresServer.properties.fullyQualifiedDomainName

View File

@ -0,0 +1,72 @@
param containerAppsEnvironmentName string
param storageName string
param shareName string
param location string
var storageAccountKey = listKeys(resourceId('Microsoft.Storage/storageAccounts', storageName), '2021-09-01').keys[0].value
resource containerAppsEnvironment 'Microsoft.App/managedEnvironments@2022-11-01-preview' existing = {
name: containerAppsEnvironmentName
}
var mountName = 'qdrantstoragemount'
var volumeName = 'qdrantstoragevol'
resource qdrantstorage 'Microsoft.App/managedEnvironments/storages@2022-11-01-preview' = {
name: '${containerAppsEnvironmentName}/${mountName}'
properties: {
azureFile: {
accountName: storageName
shareName: shareName
accountKey: storageAccountKey
accessMode: 'ReadWrite'
}
}
}
resource qdrant 'Microsoft.App/containerApps@2022-11-01-preview' = {
name: 'qdrant'
location: location
dependsOn:[
qdrantstorage
]
properties: {
environmentId: containerAppsEnvironment.id
configuration: {
ingress: {
external: true
targetPort: 6333
}
}
template: {
containers: [
{
name: 'qdrant'
image: 'qdrant/qdrant'
resources: {
cpu: 1
memory: '2Gi'
}
volumeMounts: [
{
volumeName: volumeName
mountPath: '/qdrant/storage'
}
]
}
]
scale: {
minReplicas: 1
maxReplicas: 1
}
volumes: [
{
name: volumeName
storageName: mountName
storageType: 'AzureFile'
}
]
}
}
}
output fqdn string = qdrant.properties.latestRevisionFqdn

View File

@ -0,0 +1,16 @@
@description('The name of the app service resource within the current resource group scope')
param name string
@description('The app settings to be applied to the app service')
@secure()
param appSettings object
resource appService 'Microsoft.Web/sites@2022-03-01' existing = {
name: name
}
resource settings 'Microsoft.Web/sites/config@2022-03-01' = {
name: 'appsettings'
parent: appService
properties: appSettings
}

View File

@ -0,0 +1,119 @@
param name string
param location string = resourceGroup().location
param tags object = {}
// Reference Properties
param applicationInsightsName string = ''
param appServicePlanId string
param keyVaultName string = ''
param managedIdentity bool
// Runtime Properties
@allowed([
'dotnet', 'dotnetcore', 'dotnet-isolated', 'node', 'python', 'java', 'powershell', 'custom'
])
param runtimeName string
param runtimeNameAndVersion string = '${runtimeName}|${runtimeVersion}'
param runtimeVersion string
// Microsoft.Web/sites Properties
param kind string = 'app,linux'
// Microsoft.Web/sites/config
param allowedOrigins array = []
param alwaysOn bool = true
param appCommandLine string = ''
@secure()
param appSettings object = {}
param clientAffinityEnabled bool = false
param enableOryxBuild bool = contains(kind, 'linux')
param functionAppScaleLimit int = -1
param linuxFxVersion string = runtimeNameAndVersion
param minimumElasticInstanceCount int = -1
param numberOfWorkers int = -1
param scmDoBuildDuringDeployment bool = false
param use32BitWorkerProcess bool = false
param ftpsState string = 'FtpsOnly'
param healthCheckPath string = ''
resource appService 'Microsoft.Web/sites@2022-03-01' = {
name: name
location: location
tags: tags
kind: kind
properties: {
serverFarmId: appServicePlanId
siteConfig: {
alwaysOn: alwaysOn
ftpsState: ftpsState
minTlsVersion: '1.2'
appCommandLine: appCommandLine
numberOfWorkers: numberOfWorkers != -1 ? numberOfWorkers : null
minimumElasticInstanceCount: minimumElasticInstanceCount != -1 ? minimumElasticInstanceCount : null
use32BitWorkerProcess: use32BitWorkerProcess
functionAppScaleLimit: functionAppScaleLimit != -1 ? functionAppScaleLimit : null
healthCheckPath: healthCheckPath
cors: {
allowedOrigins: union([ 'https://portal.azure.com', 'https://ms.portal.azure.com' ], allowedOrigins)
}
}
clientAffinityEnabled: clientAffinityEnabled
httpsOnly: true
}
identity: { type: managedIdentity ? 'SystemAssigned' : 'None' }
resource configLogs 'config' = {
name: 'logs'
properties: {
applicationLogs: { fileSystem: { level: 'Verbose' } }
detailedErrorMessages: { enabled: true }
failedRequestsTracing: { enabled: true }
httpLogs: { fileSystem: { enabled: true, retentionInDays: 1, retentionInMb: 35 } }
}
}
resource basicPublishingCredentialsPoliciesFtp 'basicPublishingCredentialsPolicies' = {
name: 'ftp'
location: location
properties: {
allow: false
}
}
resource basicPublishingCredentialsPoliciesScm 'basicPublishingCredentialsPolicies' = {
name: 'scm'
location: location
properties: {
allow: false
}
}
}
module config 'appservice-appsettings.bicep' = if (!empty(appSettings)) {
name: '${name}-appSettings'
params: {
name: appService.name
appSettings: union(appSettings,
{
SCM_DO_BUILD_DURING_DEPLOYMENT: string(scmDoBuildDuringDeployment)
ENABLE_ORYX_BUILD: string(enableOryxBuild)
},
runtimeName == 'python' && appCommandLine == '' ? { PYTHON_ENABLE_GUNICORN_MULTIWORKERS: 'true'} : {},
!empty(applicationInsightsName) ? { APPLICATIONINSIGHTS_CONNECTION_STRING: applicationInsights.properties.ConnectionString } : {},
!empty(keyVaultName) ? { AZURE_KEY_VAULT_ENDPOINT: keyVault.properties.vaultUri } : {})
}
}
resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' existing = if (!(empty(keyVaultName))) {
name: keyVaultName
}
resource applicationInsights 'Microsoft.Insights/components@2020-02-02' existing = if (!empty(applicationInsightsName)) {
name: applicationInsightsName
}
output identityPrincipalId string = managedIdentity ? appService.identity.principalId : ''
output name string = appService.name
output uri string = 'https://${appService.properties.defaultHostName}'

View File

@ -0,0 +1,21 @@
param name string
param location string = resourceGroup().location
param tags object = {}
param kind string = ''
param reserved bool = true
param sku object
resource appServicePlan 'Microsoft.Web/serverfarms@2022-03-01' = {
name: name
location: location
tags: tags
sku: sku
kind: kind
properties: {
reserved: reserved
}
}
output id string = appServicePlan.id
output name string = appServicePlan.name

View File

@ -0,0 +1,104 @@
param name string
param location string = resourceGroup().location
param tags object = {}
@description('The environment name for the container apps')
param containerAppsEnvironmentName string
@description('The number of CPU cores allocated to a single container instance, e.g., 0.5')
param containerCpuCoreCount string = '0.5'
@description('The maximum number of replicas to run. Must be at least 1.')
@minValue(1)
param containerMaxReplicas int = 10
@description('The amount of memory allocated to a single container instance, e.g., 1Gi')
param containerMemory string = '1.0Gi'
@description('The minimum number of replicas to run. Must be at least 1.')
@minValue(1)
param containerMinReplicas int = 1
@description('The name of the container')
param containerName string = 'main'
@description('The name of the container registry')
param containerRegistryName string = ''
@allowed([ 'http', 'grpc' ])
@description('The protocol used by Dapr to connect to the app, e.g., HTTP or gRPC')
param daprAppProtocol string = 'http'
@description('Enable or disable Dapr for the container app')
param daprEnabled bool = false
@description('The Dapr app ID')
param daprAppId string = containerName
@description('Specifies if the resource already exists')
param exists bool = false
@description('Specifies if Ingress is enabled for the container app')
param ingressEnabled bool = true
@description('The type of identity for the resource')
@allowed([ 'None', 'SystemAssigned', 'UserAssigned' ])
param identityType string = 'None'
@description('The name of the user-assigned identity')
param identityName string = ''
@description('The name of the container image')
param imageName string = ''
@description('The secrets required for the container')
param secrets array = []
@description('The environment variables for the container')
param env array = []
@description('Specifies if the resource ingress is exposed externally')
param external bool = true
@description('The service binds associated with the container')
param serviceBinds array = []
@description('The target port for the container')
param targetPort int = 80
resource existingApp 'Microsoft.App/containerApps@2023-04-01-preview' existing = if (exists) {
name: name
}
module app 'container-app.bicep' = {
name: '${deployment().name}-update'
params: {
name: name
location: location
tags: tags
identityType: identityType
identityName: identityName
ingressEnabled: ingressEnabled
containerName: containerName
containerAppsEnvironmentName: containerAppsEnvironmentName
containerRegistryName: containerRegistryName
containerCpuCoreCount: containerCpuCoreCount
containerMemory: containerMemory
containerMinReplicas: containerMinReplicas
containerMaxReplicas: containerMaxReplicas
daprEnabled: daprEnabled
daprAppId: daprAppId
daprAppProtocol: daprAppProtocol
secrets: secrets
external: external
env: env
imageName: !empty(imageName) ? imageName : exists ? existingApp.properties.template.containers[0].image : ''
targetPort: targetPort
serviceBinds: serviceBinds
}
}
output defaultDomain string = app.outputs.defaultDomain
output imageName string = app.outputs.imageName
output name string = app.outputs.name
output uri string = app.outputs.uri

View File

@ -0,0 +1,161 @@
param name string
param location string = resourceGroup().location
param tags object = {}
@description('Allowed origins')
param allowedOrigins array = []
@description('Name of the environment for container apps')
param containerAppsEnvironmentName string
@description('CPU cores allocated to a single container instance, e.g., 0.5')
param containerCpuCoreCount string = '0.5'
@description('The maximum number of replicas to run. Must be at least 1.')
@minValue(1)
param containerMaxReplicas int = 10
@description('Memory allocated to a single container instance, e.g., 1Gi')
param containerMemory string = '1.0Gi'
@description('The minimum number of replicas to run. Must be at least 1.')
param containerMinReplicas int = 1
@description('The name of the container')
param containerName string = 'main'
@description('The name of the container registry')
param containerRegistryName string = ''
@description('The protocol used by Dapr to connect to the app, e.g., http or grpc')
@allowed([ 'http', 'grpc' ])
param daprAppProtocol string = 'http'
@description('The Dapr app ID')
param daprAppId string = containerName
@description('Enable Dapr')
param daprEnabled bool = false
@description('The environment variables for the container')
param env array = []
@description('Specifies if the resource ingress is exposed externally')
param external bool = true
@description('The name of the user-assigned identity')
param identityName string = ''
@description('The type of identity for the resource')
@allowed([ 'None', 'SystemAssigned', 'UserAssigned' ])
param identityType string = 'None'
@description('The name of the container image')
param imageName string = ''
@description('Specifies if Ingress is enabled for the container app')
param ingressEnabled bool = true
param revisionMode string = 'Single'
@description('The secrets required for the container')
param secrets array = []
@description('The service binds associated with the container')
param serviceBinds array = []
@description('The name of the container apps add-on to use. e.g. redis')
param serviceType string = ''
@description('The target port for the container')
param targetPort int = 80
resource userIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' existing = if (!empty(identityName)) {
name: identityName
}
// Private registry support requires both an ACR name and a User Assigned managed identity
var usePrivateRegistry = !empty(identityName) && !empty(containerRegistryName)
// Automatically set to `UserAssigned` when an `identityName` has been set
var normalizedIdentityType = !empty(identityName) ? 'UserAssigned' : identityType
module containerRegistryAccess '../security/registry-access.bicep' = if (usePrivateRegistry) {
name: '${deployment().name}-registry-access'
params: {
containerRegistryName: containerRegistryName
principalId: usePrivateRegistry ? userIdentity.properties.principalId : ''
}
}
resource app 'Microsoft.App/containerApps@2023-04-01-preview' = {
name: name
location: location
tags: tags
// It is critical that the identity is granted ACR pull access before the app is created
// otherwise the container app will throw a provision error
// This also forces us to use an user assigned managed identity since there would no way to
// provide the system assigned identity with the ACR pull access before the app is created
dependsOn: usePrivateRegistry ? [ containerRegistryAccess ] : []
identity: {
type: normalizedIdentityType
userAssignedIdentities: !empty(identityName) && normalizedIdentityType == 'UserAssigned' ? { '${userIdentity.id}': {} } : null
}
properties: {
managedEnvironmentId: containerAppsEnvironment.id
configuration: {
activeRevisionsMode: revisionMode
ingress: ingressEnabled ? {
external: external
targetPort: targetPort
transport: 'auto'
corsPolicy: {
allowedOrigins: union([ 'https://portal.azure.com', 'https://ms.portal.azure.com' ], allowedOrigins)
}
} : null
dapr: daprEnabled ? {
enabled: true
appId: daprAppId
appProtocol: daprAppProtocol
appPort: ingressEnabled ? targetPort : 0
} : { enabled: false }
secrets: secrets
service: !empty(serviceType) ? { type: serviceType } : null
registries: usePrivateRegistry ? [
{
server: '${containerRegistryName}.azurecr.io'
identity: userIdentity.id
}
] : []
}
template: {
serviceBinds: !empty(serviceBinds) ? serviceBinds : null
containers: [
{
image: !empty(imageName) ? imageName : 'mcr.microsoft.com/azuredocs/containerapps-helloworld:latest'
name: containerName
env: env
resources: {
cpu: json(containerCpuCoreCount)
memory: containerMemory
}
}
]
scale: {
minReplicas: containerMinReplicas
maxReplicas: containerMaxReplicas
}
}
}
}
resource containerAppsEnvironment 'Microsoft.App/managedEnvironments@2023-04-01-preview' existing = {
name: containerAppsEnvironmentName
}
output defaultDomain string = containerAppsEnvironment.properties.defaultDomain
output identityPrincipalId string = normalizedIdentityType == 'None' ? '' : (empty(identityName) ? app.identity.principalId : userIdentity.properties.principalId)
output imageName string = imageName
output name string = app.name
output serviceBind object = !empty(serviceType) ? { serviceId: app.id, name: name } : {}
output uri string = ingressEnabled ? 'https://${app.properties.configuration.ingress.fqdn}' : ''

View File

@ -0,0 +1,40 @@
param name string
param location string = resourceGroup().location
param tags object = {}
@description('Name of the Application Insights resource')
param applicationInsightsName string = ''
@description('Specifies if Dapr is enabled')
param daprEnabled bool = false
@description('Name of the Log Analytics workspace')
param logAnalyticsWorkspaceName string
resource containerAppsEnvironment 'Microsoft.App/managedEnvironments@2023-04-01-preview' = {
name: name
location: location
tags: tags
properties: {
appLogsConfiguration: {
destination: 'log-analytics'
logAnalyticsConfiguration: {
customerId: logAnalyticsWorkspace.properties.customerId
sharedKey: logAnalyticsWorkspace.listKeys().primarySharedKey
}
}
daprAIInstrumentationKey: daprEnabled && !empty(applicationInsightsName) ? applicationInsights.properties.InstrumentationKey : ''
}
}
resource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2022-10-01' existing = {
name: logAnalyticsWorkspaceName
}
resource applicationInsights 'Microsoft.Insights/components@2020-02-02' existing = if (daprEnabled && !empty(applicationInsightsName)) {
name: applicationInsightsName
}
output defaultDomain string = containerAppsEnvironment.properties.defaultDomain
output id string = containerAppsEnvironment.id
output name string = containerAppsEnvironment.name

View File

@ -0,0 +1,37 @@
param name string
param location string = resourceGroup().location
param tags object = {}
param containerAppsEnvironmentName string
param containerRegistryName string
param containerRegistryResourceGroupName string = ''
param logAnalyticsWorkspaceName string
param applicationInsightsName string = ''
module containerAppsEnvironment 'container-apps-environment.bicep' = {
name: '${name}-container-apps-environment'
params: {
name: containerAppsEnvironmentName
location: location
tags: tags
logAnalyticsWorkspaceName: logAnalyticsWorkspaceName
applicationInsightsName: applicationInsightsName
}
}
module containerRegistry 'container-registry.bicep' = {
name: '${name}-container-registry'
scope: !empty(containerRegistryResourceGroupName) ? resourceGroup(containerRegistryResourceGroupName) : resourceGroup()
params: {
name: containerRegistryName
location: location
tags: tags
}
}
output defaultDomain string = containerAppsEnvironment.outputs.defaultDomain
output environmentName string = containerAppsEnvironment.outputs.name
output environmentId string = containerAppsEnvironment.outputs.id
output registryLoginServer string = containerRegistry.outputs.loginServer
output registryName string = containerRegistry.outputs.name

View File

@ -0,0 +1,82 @@
param name string
param location string = resourceGroup().location
param tags object = {}
@description('Indicates whether admin user is enabled')
param adminUserEnabled bool = false
@description('Indicates whether anonymous pull is enabled')
param anonymousPullEnabled bool = false
@description('Indicates whether data endpoint is enabled')
param dataEndpointEnabled bool = false
@description('Encryption settings')
param encryption object = {
status: 'disabled'
}
@description('Options for bypassing network rules')
param networkRuleBypassOptions string = 'AzureServices'
@description('Public network access setting')
param publicNetworkAccess string = 'Enabled'
@description('SKU settings')
param sku object = {
name: 'Basic'
}
@description('Zone redundancy setting')
param zoneRedundancy string = 'Disabled'
@description('The log analytics workspace ID used for logging and monitoring')
param workspaceId string = ''
// 2022-02-01-preview needed for anonymousPullEnabled
resource containerRegistry 'Microsoft.ContainerRegistry/registries@2022-02-01-preview' = {
name: name
location: location
tags: tags
sku: sku
properties: {
adminUserEnabled: adminUserEnabled
anonymousPullEnabled: anonymousPullEnabled
dataEndpointEnabled: dataEndpointEnabled
encryption: encryption
networkRuleBypassOptions: networkRuleBypassOptions
publicNetworkAccess: publicNetworkAccess
zoneRedundancy: zoneRedundancy
}
}
// TODO: Update diagnostics to be its own module
// Blocking issue: https://github.com/Azure/bicep/issues/622
// Unable to pass in a `resource` scope or unable to use string interpolation in resource types
resource diagnostics 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = if (!empty(workspaceId)) {
name: 'registry-diagnostics'
scope: containerRegistry
properties: {
workspaceId: workspaceId
logs: [
{
category: 'ContainerRegistryRepositoryEvents'
enabled: true
}
{
category: 'ContainerRegistryLoginEvents'
enabled: true
}
]
metrics: [
{
category: 'AllMetrics'
enabled: true
timeGrain: 'PT1M'
}
]
}
}
output loginServer string = containerRegistry.properties.loginServer
output name string = containerRegistry.name

View File

@ -0,0 +1,87 @@
param name string
param location string = resourceGroup().location
param tags object = {}
// Reference Properties
param applicationInsightsName string = ''
param appServicePlanId string
param keyVaultName string = ''
param managedIdentity bool
param storageAccountName string
// Runtime Properties
@allowed([
'dotnet', 'dotnetcore', 'dotnet-isolated', 'node', 'python', 'java', 'powershell', 'custom'
])
param runtimeName string
param runtimeNameAndVersion string = '${runtimeName}|${runtimeVersion}'
param runtimeVersion string
// Function Settings
@allowed([
'~4', '~3', '~2', '~1'
])
param extensionVersion string = '~4'
// Microsoft.Web/sites Properties
param kind string = 'functionapp'
// Microsoft.Web/sites/config
param allowedOrigins array = []
param alwaysOn bool = true
param appCommandLine string = ''
@secure()
param appSettings object = {}
param clientAffinityEnabled bool = false
param enableOryxBuild bool = contains(kind, 'linux')
param functionAppScaleLimit int = -1
param linuxFxVersion string = runtimeNameAndVersion
param minimumElasticInstanceCount int = -1
param numberOfWorkers int = -1
param scmDoBuildDuringDeployment bool = true
param use32BitWorkerProcess bool = false
param healthCheckPath string = ''
module functions 'appservice.bicep' = {
name: '${name}-functions'
params: {
name: name
location: location
tags: tags
allowedOrigins: allowedOrigins
alwaysOn: alwaysOn
appCommandLine: appCommandLine
applicationInsightsName: applicationInsightsName
appServicePlanId: appServicePlanId
appSettings: union(appSettings, {
AzureWebJobsStorage: 'DefaultEndpointsProtocol=https;AccountName=${storage.name};AccountKey=${storage.listKeys().keys[0].value};EndpointSuffix=${environment().suffixes.storage}'
FUNCTIONS_EXTENSION_VERSION: extensionVersion
FUNCTIONS_WORKER_RUNTIME: runtimeName
'AzureOptions__FilesAccountKey': storage.listKeys().keys[0].value
})
clientAffinityEnabled: clientAffinityEnabled
enableOryxBuild: enableOryxBuild
functionAppScaleLimit: functionAppScaleLimit
healthCheckPath: healthCheckPath
keyVaultName: keyVaultName
kind: kind
linuxFxVersion: linuxFxVersion
managedIdentity: managedIdentity
minimumElasticInstanceCount: minimumElasticInstanceCount
numberOfWorkers: numberOfWorkers
runtimeName: runtimeName
runtimeVersion: runtimeVersion
runtimeNameAndVersion: runtimeNameAndVersion
scmDoBuildDuringDeployment: scmDoBuildDuringDeployment
use32BitWorkerProcess: use32BitWorkerProcess
}
}
resource storage 'Microsoft.Storage/storageAccounts@2021-09-01' existing = {
name: storageAccountName
}
output identityPrincipalId string = managedIdentity ? functions.outputs.identityPrincipalId : ''
output name string = functions.outputs.name
output uri string = functions.outputs.uri

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,30 @@
param name string
param dashboardName string
param location string = resourceGroup().location
param tags object = {}
param includeDashboard bool = true
param logAnalyticsWorkspaceId string
resource applicationInsights 'Microsoft.Insights/components@2020-02-02' = {
name: name
location: location
tags: tags
kind: 'web'
properties: {
Application_Type: 'web'
WorkspaceResourceId: logAnalyticsWorkspaceId
}
}
module applicationInsightsDashboard 'applicationinsights-dashboard.bicep' = if (includeDashboard) {
name: 'application-insights-dashboard'
params: {
name: dashboardName
location: location
applicationInsightsName: applicationInsights.name
}
}
output connectionString string = applicationInsights.properties.ConnectionString
output instrumentationKey string = applicationInsights.properties.InstrumentationKey
output name string = applicationInsights.name

View File

@ -0,0 +1,21 @@
param name string
param location string = resourceGroup().location
param tags object = {}
resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2021-12-01-preview' = {
name: name
location: location
tags: tags
properties: any({
retentionInDays: 30
features: {
searchVersion: 1
}
sku: {
name: 'PerGB2018'
}
})
}
output id string = logAnalytics.id
output name string = logAnalytics.name

View File

@ -0,0 +1,33 @@
param logAnalyticsName string
param applicationInsightsName string
param applicationInsightsDashboardName string
param location string = resourceGroup().location
param tags object = {}
param includeDashboard bool = true
module logAnalytics 'loganalytics.bicep' = {
name: 'loganalytics'
params: {
name: logAnalyticsName
location: location
tags: tags
}
}
module applicationInsights 'applicationinsights.bicep' = {
name: 'applicationinsights'
params: {
name: applicationInsightsName
location: location
tags: tags
dashboardName: applicationInsightsDashboardName
includeDashboard: includeDashboard
logAnalyticsWorkspaceId: logAnalytics.outputs.id
}
}
output applicationInsightsConnectionString string = applicationInsights.outputs.connectionString
output applicationInsightsInstrumentationKey string = applicationInsights.outputs.instrumentationKey
output applicationInsightsName string = applicationInsights.outputs.name
output logAnalyticsWorkspaceId string = logAnalytics.outputs.id
output logAnalyticsWorkspaceName string = logAnalytics.outputs.name

View File

@ -0,0 +1,19 @@
metadata description = 'Assigns ACR Pull permissions to access an Azure Container Registry.'
param containerRegistryName string
param principalId string
var acrPullRole = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d')
resource aksAcrPull 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
scope: containerRegistry // Use when specifying a scope that is different than the deployment scope
name: guid(subscription().id, resourceGroup().id, principalId, acrPullRole)
properties: {
roleDefinitionId: acrPullRole
principalType: 'ServicePrincipal'
principalId: principalId
}
}
resource containerRegistry 'Microsoft.ContainerRegistry/registries@2022-02-01-preview' existing = {
name: containerRegistryName
}

View File

@ -0,0 +1,76 @@
param name string
param location string = resourceGroup().location
param tags object = {}
@allowed([
'Cool'
'Hot'
'Premium' ])
param accessTier string = 'Hot'
param allowBlobPublicAccess bool = true
param allowCrossTenantReplication bool = true
param allowSharedKeyAccess bool = true
param containers array = []
param defaultToOAuthAuthentication bool = false
param deleteRetentionPolicy object = {}
@allowed([ 'AzureDnsZone', 'Standard' ])
param dnsEndpointType string = 'Standard'
param kind string = 'StorageV2'
param minimumTlsVersion string = 'TLS1_2'
param networkAcls object = {
bypass: 'AzureServices'
defaultAction: 'Allow'
}
@allowed([ 'Enabled', 'Disabled' ])
param publicNetworkAccess string = 'Enabled'
param sku object = { name: 'Standard_LRS' }
param fileShares array = []
param tables array = []
resource storage 'Microsoft.Storage/storageAccounts@2022-05-01' = {
name: name
location: location
tags: tags
kind: kind
sku: sku
properties: {
accessTier: accessTier
allowBlobPublicAccess: allowBlobPublicAccess
allowCrossTenantReplication: allowCrossTenantReplication
allowSharedKeyAccess: allowSharedKeyAccess
defaultToOAuthAuthentication: defaultToOAuthAuthentication
dnsEndpointType: dnsEndpointType
minimumTlsVersion: minimumTlsVersion
networkAcls: networkAcls
publicNetworkAccess: publicNetworkAccess
}
resource blobServices 'blobServices' = if (!empty(containers)) {
name: 'default'
properties: {
deleteRetentionPolicy: deleteRetentionPolicy
}
resource container 'containers' = [for container in containers: {
name: container.name
properties: {
publicAccess: contains(container, 'publicAccess') ? container.publicAccess : 'None'
}
}]
}
resource fileServices 'fileServices' = if (!empty(fileShares)) {
name: 'default'
resource share 'shares' = [for fileShare in fileShares: {
name: fileShare
}]
}
resource tableServices 'tableServices' = if (!empty(tables)) {
name: 'default'
resource table 'tables' = [for table in tables: {
name: table
}]
}
}
output name string = storage.name
output primaryEndpoints object = storage.properties.primaryEndpoints

View File

@ -0,0 +1,160 @@
targetScope = 'subscription'
@minLength(1)
@maxLength(64)
@description('Name of the the environment which is used to generate a short unique hash used in all resources.')
param environmentName string
@minLength(1)
@description('Primary location for all resources')
param location string
@secure()
param githubAppKey string
param githubAppId string
param githubAppInstallationId string
param openAIServiceType string
param openAIServiceId string
param openAIDeploymentId string
param openAIEmbeddingId string
param openAIEndpoint string
@secure()
param openAIKey string
param applicationInsightsDashboardName string = ''
param applicationInsightsName string = ''
param logAnalyticsName string = ''
param resourceGroupName string = ''
param storageAccountName string = ''
param containerAppsEnvironmentName string = ''
param containerRegistryName string = ''
param ghFlowServiceName string = ''
param cosmosAccountName string = ''
var aciShare = 'acishare'
var qdrantShare = 'qdrantshare'
var abbrs = loadJsonContent('./abbreviations.json')
var resourceToken = toLower(uniqueString(subscription().id, environmentName, location))
var tags = { 'azd-env-name': environmentName }
// Organize resources in a resource group
resource rg 'Microsoft.Resources/resourceGroups@2021-04-01' = {
name: !empty(resourceGroupName) ? resourceGroupName : '${abbrs.resourcesResourceGroups}${environmentName}'
location: location
tags: tags
}
module storage './core/storage/storage-account.bicep' = {
name: 'storage'
scope: rg
params: {
name: !empty(storageAccountName) ? storageAccountName : '${abbrs.storageStorageAccounts}${resourceToken}'
location: location
tags: tags
fileShares: [
aciShare
qdrantShare
]
}
}
// Monitor application with Azure Monitor
module monitoring './core/monitor/monitoring.bicep' = {
name: 'monitoring'
scope: rg
params: {
location: location
tags: tags
logAnalyticsName: !empty(logAnalyticsName) ? logAnalyticsName : '${abbrs.operationalInsightsWorkspaces}${resourceToken}'
applicationInsightsName: !empty(applicationInsightsName) ? applicationInsightsName : '${abbrs.insightsComponents}${resourceToken}'
applicationInsightsDashboardName: !empty(applicationInsightsDashboardName) ? applicationInsightsDashboardName : '${abbrs.portalDashboards}${resourceToken}'
}
}
// Container apps host (including container registry)
module containerApps './core/host/container-apps.bicep' = {
name: 'container-apps'
scope: rg
params: {
name: 'app'
location: location
tags: tags
containerAppsEnvironmentName: !empty(containerAppsEnvironmentName) ? containerAppsEnvironmentName : '${abbrs.appManagedEnvironments}${resourceToken}'
containerRegistryName: !empty(containerRegistryName) ? containerRegistryName : '${abbrs.containerRegistryRegistries}${resourceToken}'
logAnalyticsWorkspaceName: monitoring.outputs.logAnalyticsWorkspaceName
applicationInsightsName: monitoring.outputs.applicationInsightsName
}
}
module qdrant './core/database/qdrant/qdrant-aca.bicep' = {
name: 'qdrant-deploy'
scope: rg
params: {
location: location
containerAppsEnvironmentName: containerApps.outputs.environmentName
shareName: qdrantShare
storageName: storage.outputs.name
}
}
// The application database
module cosmos './app/db.bicep' = {
name: 'cosmos'
scope: rg
params: {
accountName: !empty(cosmosAccountName) ? cosmosAccountName : '${abbrs.documentDBDatabaseAccounts}${resourceToken}'
databaseName: 'devteam'
location: location
tags: tags
}
}
module ghFlow './app/gh-flow.bicep' = {
name: 'gh-flow'
scope: rg
params: {
name: !empty(ghFlowServiceName) ? ghFlowServiceName : '${abbrs.appContainerApps}ghflow-${resourceToken}'
location: location
tags: tags
identityName: '${abbrs.managedIdentityUserAssignedIdentities}ghflow-${resourceToken}'
applicationInsightsName: monitoring.outputs.applicationInsightsName
containerAppsEnvironmentName: containerApps.outputs.environmentName
containerRegistryName:containerApps.outputs.registryName
storageAccountName: storage.outputs.name
aciShare: aciShare
githubAppId: githubAppId
githubAppInstallationId: githubAppInstallationId
githubAppKey: githubAppKey
openAIDeploymentId: openAIDeploymentId
openAIEmbeddingId: openAIEmbeddingId
openAIEndpoint: openAIEndpoint
openAIKey: openAIKey
openAIServiceId: openAIServiceId
openAIServiceType: openAIServiceType
qdrantEndpoint: 'https://${qdrant.outputs.fqdn}'
rgName: rg.name
cosmosAccountName: cosmos.outputs.accountName
}
}
// App outputs
output APPLICATIONINSIGHTS_CONNECTION_STRING string = monitoring.outputs.applicationInsightsConnectionString
output AZURE_CONTAINER_ENVIRONMENT_NAME string = containerApps.outputs.environmentName
output AZURE_CONTAINER_REGISTRY_ENDPOINT string = containerApps.outputs.registryLoginServer
output AZURE_CONTAINER_REGISTRY_NAME string = containerApps.outputs.registryName
output AZURE_LOCATION string = location
output AZURE_TENANT_ID string = subscription().tenantId
output AZURE_SUBSCRIPTION_ID string = subscription().subscriptionId
output AZURE_RESOURCE_GROUP_NAME string = rg.name
output AZURE_FILESHARE_NAME string = aciShare
output AZURE_FILESHARE_ACCOUNT_NAME string = storage.outputs.name
output QDRANT_ENDPOINT string = 'https://${qdrant.outputs.fqdn}'
output WEBHOOK_SECRET string = ghFlow.outputs.WEBHOOK_SECRET
// Data outputs
output AZURE_COSMOS_ENDPOINT string = cosmos.outputs.endpoint
output AZURE_COSMOS_CONNECTION_STRING_KEY string = cosmos.outputs.connectionStringKey
output AZURE_COSMOS_DATABASE_NAME string = cosmos.outputs.databaseName

View File

@ -0,0 +1,42 @@
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"environmentName": {
"value": "${AZURE_ENV_NAME}"
},
"location": {
"value": "${AZURE_LOCATION}"
},
"githubAppKey": {
"value": "${GH_APP_KEY}"
},
"githubAppId": {
"value": "${GH_APP_ID}"
},
"githubAppInstallationId": {
"value": "${GH_APP_INST_ID}"
},
"openAIServiceType": {
"value": "${OAI_SERVICE_TYPE}"
},
"openAIServiceId": {
"value": "${OAI_SERVICE_ID}"
},
"openAIDeploymentId": {
"value": "${OAI_DEPLOYMENT_ID}"
},
"openAIEmbeddingId": {
"value": "${OAI_EMBEDDING_ID}"
},
"openAIEndpoint": {
"value": "${OAI_ENDPOINT}"
},
"openAIKey": {
"value": "${OAI_KEY}"
},
"principalId": {
"value": "${AZURE_PRINCIPAL_ID}"
}
}
}

View File

@ -0,0 +1,29 @@
using Dapr.Actors.Runtime;
using Dapr.Client;
using Microsoft.AI.Agents.Abstractions;
using Microsoft.AI.Agents.Dapr;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Memory;
namespace Microsoft.AI.DevTeam.Dapr;
// The architect has Org+Repo scope and is holding the knowledge of the high level architecture of the project
public class Architect : AiAgent<ArchitectState>,IDaprAgent
{
public Architect(ActorHost host, DaprClient client, ISemanticTextMemory memory, Kernel kernel)
: base(host, client, memory, kernel)
{
}
public override Task HandleEvent(Event item)
{
return Task.CompletedTask;
}
}
public class ArchitectState
{
public string FilesTree { get; set; }
public string HighLevelArchitecture { get; set; }
}

View File

@ -0,0 +1,64 @@
using Dapr.Actors;
using Dapr.Actors.Runtime;
using Dapr.Client;
using Microsoft.AI.Agents.Abstractions;
using Microsoft.AI.Agents.Dapr;
using Microsoft.AI.DevTeam.Dapr.Events;
namespace Microsoft.AI.DevTeam.Dapr;
public class AzureGenie : Agent, IDaprAgent
{
private readonly IManageAzure _azureService;
public AzureGenie(ActorHost host,DaprClient client, IManageAzure azureService) : base(host, client)
{
_azureService = azureService;
}
public override async Task HandleEvent(Event item)
{
switch (item.Type)
{
case nameof(GithubFlowEventType.ReadmeCreated):
{
var context = item.ToGithubContext();
await Store(context.Org,context.Repo, context.ParentNumber.Value, context.IssueNumber, "readme", "md", "output", item.Data["readme"]);
await PublishEvent(Consts.PubSub, Consts.MainTopic, new Event
{
Type = nameof(GithubFlowEventType.ReadmeStored),
Subject = context.Subject,
Data = context.ToData()
});
}
break;
case nameof(GithubFlowEventType.CodeCreated):
{
var context = item.ToGithubContext();
await Store(context.Org,context.Repo, context.ParentNumber.Value, context.IssueNumber, "run", "sh", "output", item.Data["code"]);
await RunInSandbox(context.Org,context.Repo, context.ParentNumber.Value, context.IssueNumber);
await PublishEvent(Consts.PubSub, Consts.MainTopic, new Event
{
Type = nameof(GithubFlowEventType.SandboxRunCreated),
Subject = context.Subject,
Data = context.ToData()
});
}
break;
default:
break;
}
}
public async Task Store(string org, string repo, long parentIssueNumber, long issueNumber, string filename, string extension, string dir, string output)
{
await _azureService.Store(org, repo, parentIssueNumber, issueNumber, filename, extension, dir, output);
}
public async Task RunInSandbox(string org, string repo, long parentIssueNumber, long issueNumber)
{
await _azureService.RunInSandbox(org, repo, parentIssueNumber, issueNumber);
}
}

View File

@ -0,0 +1,84 @@
using Dapr.Actors;
using Dapr.Actors.Runtime;
using Dapr.Client;
using Microsoft.AI.Agents.Abstractions;
using Microsoft.AI.Agents.Dapr;
using Microsoft.AI.DevTeam.Dapr.Events;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Memory;
namespace Microsoft.AI.DevTeam.Dapr;
public class Dev : AiAgent<DeveloperState>, IDaprAgent
{
private readonly ILogger<Dev> _logger;
public Dev(ActorHost host, DaprClient client, Kernel kernel, ISemanticTextMemory memory, ILogger<Dev> logger)
: base(host, client, memory, kernel)
{
_logger = logger;
}
public async override Task HandleEvent(Event item)
{
switch (item.Type)
{
case nameof(GithubFlowEventType.CodeGenerationRequested):
{
var context = item.ToGithubContext();
var code = await GenerateCode(item.Data["input"]);
var data = context.ToData();
data["result"] = code;
await PublishEvent(Consts.PubSub, Consts.MainTopic, new Event
{
Type = nameof(GithubFlowEventType.CodeGenerated),
Subject = context.Subject,
Data = data
});
}
break;
case nameof(GithubFlowEventType.CodeChainClosed):
{
var context = item.ToGithubContext();
var lastCode = state.History.Last().Message;
var data = context.ToData();
data["code"] = lastCode;
await PublishEvent(Consts.PubSub, Consts.MainTopic, new Event
{
Type = nameof(GithubFlowEventType.CodeCreated),
Subject = context.Subject,
Data = data
});
}
break;
default:
break;
}
}
public async Task<string> GenerateCode(string ask)
{
try
{
// TODO: ask the architect for the high level architecture as well as the files structure of the project
var context = new KernelArguments { ["input"] = AppendChatHistory(ask)};
var instruction = "Consider the following architectural guidelines:!waf!";
var enhancedContext = await AddKnowledge(instruction, "waf",context);
return await CallFunction(DeveloperSkills.Implement, enhancedContext);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error generating code");
return default;
}
}
}
public class DeveloperState
{
public string Understanding { get; set; }
}
public class UnderstandingResult
{
public string NewUnderstanding { get; set; }
public string Explanation { get; set; }
}

View File

@ -0,0 +1,56 @@
namespace Microsoft.AI.DevTeam.Dapr;
public static class DeveloperSkills {
public static string Implement = """
You are a Developer for an application.
Please output the code required to accomplish the task assigned to you below and wrap it in a bash script that creates the files.
Do not use any IDE commands and do not build and run the code.
Make specific choices about implementation. Do not offer a range of options.
Use comments in the code to describe the intent. Do not include other text other than code and code comments.
Input: {{$input}}
{{$waf}}
""";
public static string Improve = """
You are a Developer for an application. Your job is to imrove the code that you are given in the input below.
Please output a new version of code that fixes any problems with this version.
If there is an error message in the input you should fix that error in the code.
Wrap the code output up in a bash script that creates the necessary files by overwriting any previous files.
Do not use any IDE commands and do not build and run the code.
Make specific choices about implementation. Do not offer a range of options.
Use comments in the code to describe the intent. Do not include other text other than code and code comments.
Input: {{$input}}
{{$waf}}
""";
public static string Explain = """
You are an experienced software developer, with strong experience in Azure and Microsoft technologies.
Extract the key features and capabilities of the code file below, with the intent to build an understanding of an entire code repository.
You can include references or documentation links in your explanation. Also where appropriate please output a list of keywords to describe the code or its capabilities.
Example:
Keywords: Azure, networking, security, authentication
===code===
{{$input}}
===end-code===
Only include the points in a bullet point format and DON'T add anything outside of the bulleted list.
Be short and concise.
If the code's purpose is not clear output an error:
Error: The model could not determine the purpose of the code.
""";
public static string ConsolidateUnderstanding = """
You are an experienced software developer, with strong experience in Azure and Microsoft technologies.
You are trying to build an understanding of the codebase from code files. This is the current understanding of the project:
===current-understanding===
{{$input}}
===end-current-understanding===
and this is the new information that surfaced
===new-understanding===
{{$newUnderstanding}}
===end-new-understanding===
Your job is to update your current understanding with the new information.
Only include the points in a bullet point format and DON'T add anything outside of the bulleted list.
Be short and concise.
""";
}

View File

@ -0,0 +1,51 @@
namespace Microsoft.AI.DevTeam.Dapr;
public static class DevLeadSkills {
public static string Plan = """
You are a Dev Lead for an application team, building the application described below.
Please break down the steps and modules required to develop the complete application, describe each step in detail.
Make prescriptive architecture, language, and frameowrk choices, do not provide a range of choices.
For each step or module then break down the steps or subtasks required to complete that step or module.
For each subtask write an LLM prompt that would be used to tell a model to write the coee that will accomplish that subtask. If the subtask involves taking action/running commands tell the model to write the script that will run those commands.
In each LLM prompt restrict the model from outputting other text that is not in the form of code or code comments.
Please output a JSON array data structure, in the precise schema shown below, with a list of steps and a description of each step, and the steps or subtasks that each requires, and the LLM prompts for each subtask.
Example:
{
"steps": [
{
"step": "1",
"description": "This is the first step",
"subtasks": [
{
"subtask": "Subtask 1",
"description": "This is the first subtask",
"prompt": "Write the code to do the first subtask"
},
{
"subtask": "Subtask 2",
"description": "This is the second subtask",
"prompt": "Write the code to do the second subtask"
}
]
}
]
}
Do not output any other text.
Do not wrap the JSON in any other text, output the JSON format described above, making sure it's a valid JSON.
Input: {{$input}}
{{$waf}}
""";
public static string Explain = """
You are a Dev Lead.
Please explain the code that is in the input below. You can include references or documentation links in your explanation.
Also where appropriate please output a list of keywords to describe the code or its capabilities.
example:
Keywords: Azure, networking, security, authentication
If the code's purpose is not clear output an error:
Error: The model could not determine the purpose of the code.
--
Input: {{$input}}
""";
}

View File

@ -0,0 +1,97 @@
using Dapr.Actors;
using Dapr.Actors.Runtime;
using Dapr.Client;
using Microsoft.AI.Agents.Abstractions;
using Microsoft.AI.Agents.Dapr;
using Microsoft.AI.DevTeam.Dapr.Events;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Memory;
namespace Microsoft.AI.DevTeam.Dapr;
public class DeveloperLead : AiAgent<DeveloperLeadState>, IDaprAgent
{
private readonly ILogger<DeveloperLead> _logger;
public DeveloperLead(ActorHost host, DaprClient client, Kernel kernel, ISemanticTextMemory memory, ILogger<DeveloperLead> logger)
: base(host, client, memory, kernel)
{
_logger = logger;
}
public async override Task HandleEvent(Event item)
{
switch (item.Type)
{
case nameof(GithubFlowEventType.DevPlanRequested):
{
var context = item.ToGithubContext();
var plan = await CreatePlan(item.Data["input"]);
var data = context.ToData();
data["result"] = plan;
await PublishEvent(Consts.PubSub,Consts.MainTopic, new Event
{
Type = nameof(GithubFlowEventType.DevPlanGenerated),
Subject = context.Subject,
Data = data
});
}
break;
case nameof(GithubFlowEventType.DevPlanChainClosed):
{
var context = item.ToGithubContext();
var latestPlan = state.History.Last().Message;
var data = context.ToData();
data["plan"] = latestPlan;
await PublishEvent(Consts.PubSub,Consts.MainTopic, new Event
{
Type = nameof(GithubFlowEventType.DevPlanCreated),
Subject = context.Subject,
Data = data
});
}
break;
default:
break;
}
}
public async Task<string> CreatePlan(string ask)
{
try
{
// TODO: Ask the architect for the existing high level architecture
// as well as the file structure
var context = new KernelArguments { ["input"] = AppendChatHistory(ask) };
var instruction = "Consider the following architectural guidelines:!waf!";
var enhancedContext = await AddKnowledge(instruction, "waf", context);
return await CallFunction(DevLeadSkills.Plan, enhancedContext);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error creating development plan");
return default;
}
}
}
public class DevLeadPlanResponse
{
public List<Step> steps { get; set; }
}
public class Step
{
public string description { get; set; }
public string step { get; set; }
public List<Subtask> subtasks { get; set; }
}
public class Subtask
{
public string subtask { get; set; }
public string prompt { get; set; }
}
public class DeveloperLeadState
{
public string Plan { get; set; }
}

View File

@ -0,0 +1,100 @@
using System.Text.Json;
using Dapr.Actors;
using Dapr.Actors.Runtime;
using Dapr.Client;
using Microsoft.AI.Agents.Abstractions;
using Microsoft.AI.Agents.Dapr;
using Microsoft.AI.DevTeam.Dapr.Events;
namespace Microsoft.AI.DevTeam.Dapr;
public class Hubber : Agent, IDaprAgent
{
private readonly IManageGithub _ghService;
public Hubber(ActorHost host, DaprClient client, IManageGithub ghService) :base(host, client)
{
_ghService = ghService;
}
public override async Task HandleEvent(Event item)
{
switch (item.Type)
{
case nameof(GithubFlowEventType.NewAsk):
{
var context = item.ToGithubContext();
var pmIssue = await CreateIssue(context.Org, context.Repo , item.Data["input"], "PM.Readme", context.IssueNumber);
var devLeadIssue = await CreateIssue(context.Org, context.Repo , item.Data["input"], "DevLead.Plan", context.IssueNumber);
await PostComment(context.Org, context.Repo, context.IssueNumber, $" - #{pmIssue} - tracks PM.Readme");
await PostComment(context.Org, context.Repo, context.IssueNumber, $" - #{devLeadIssue} - tracks DevLead.Plan");
await CreateBranch(context.Org, context.Repo, $"sk-{context.IssueNumber}");
}
break;
case nameof(GithubFlowEventType.ReadmeGenerated):
case nameof(GithubFlowEventType.DevPlanGenerated):
case nameof(GithubFlowEventType.CodeGenerated):
{
var context = item.ToGithubContext();
var result = item.Data["result"];
var contents = string.IsNullOrEmpty(result)? "Sorry, I got tired, can you try again please? ": result;
await PostComment(context.Org,context.Repo, context.IssueNumber, contents);
}
break;
case nameof(GithubFlowEventType.DevPlanCreated):
{
var context = item.ToGithubContext();
var plan = JsonSerializer.Deserialize<DevLeadPlanResponse>(item.Data["plan"]);
var prompts = plan.steps.SelectMany(s => s.subtasks.Select(st => st.prompt));
foreach (var prompt in prompts)
{
var functionName = "Developer.Implement";
var issue = await CreateIssue(context.Org, context.Repo, prompt, functionName, context.ParentNumber.Value);
var commentBody = $" - #{issue} - tracks {functionName}";
await PostComment(context.Org, context.Repo, context.ParentNumber.Value, commentBody);
}
}
break;
case nameof(GithubFlowEventType.ReadmeStored):
{
var context = item.ToGithubContext();
var branch = $"sk-{context.ParentNumber}";
await CommitToBranch(context.Org, context.Repo, context.ParentNumber.Value, context.IssueNumber, "output", branch);
await CreatePullRequest(context.Org, context.Repo, context.ParentNumber.Value, branch);
}
break;
case nameof(GithubFlowEventType.SandboxRunFinished):
{
var context = item.ToGithubContext();
var branch = $"sk-{context.ParentNumber}";
await CommitToBranch(context.Org, context.Repo, context.ParentNumber.Value, context.IssueNumber, "output", branch);
}
break;
default:
break;
}
}
public async Task<int> CreateIssue(string org, string repo, string input, string function, long parentNumber)
{
return await _ghService.CreateIssue(org, repo, input, function, parentNumber);
}
public async Task PostComment(string org, string repo, long issueNumber, string comment)
{
await _ghService.PostComment(org, repo, issueNumber, comment);
}
public async Task CreateBranch(string org, string repo, string branch)
{
await _ghService.CreateBranch(org, repo, branch);
}
public async Task CreatePullRequest(string org, string repo, long issueNumber, string branch)
{
await _ghService.CreatePR(org, repo, issueNumber, branch);
}
public async Task CommitToBranch(string org, string repo, long parentNumber, long issueNumber, string rootDir, string branch)
{
await _ghService.CommitToBranch(org, repo, parentNumber, issueNumber, rootDir, branch);
}
}

View File

@ -0,0 +1,34 @@
namespace Microsoft.AI.DevTeam.Dapr;
public static class PMSkills
{
public static string BootstrapProject = """
Please write a bash script with the commands that would be required to generate applications as described in the following input.
You may add comments to the script and the generated output but do not add any other text except the bash script.
You may include commands to build the applications but do not run them.
Do not include any git commands.
Input: {{$input}}
{{$waf}}
""";
public static string Readme = """
You are a program manager on a software development team. You are working on an app described below.
Based on the input below, and any dialog or other context, please output a raw README.MD markdown file documenting the main features of the app and the architecture or code organization.
Do not describe how to create the application.
Write the README as if it were documenting the features and architecture of the application. You may include instructions for how to run the application.
Input: {{$input}}
{{$waf}}
""";
public static string Explain = """
You are a Product Manager.
Please explain the code that is in the input below. You can include references or documentation links in your explanation.
Also where appropriate please output a list of keywords to describe the code or its capabilities.
example:
Keywords: Azure, networking, security, authentication
If the code's purpose is not clear output an error:
Error: The model could not determine the purpose of the code.
--
Input: {{$input}}
""";
}

View File

@ -0,0 +1,78 @@
using Dapr.Actors;
using Dapr.Actors.Runtime;
using Dapr.Client;
using Microsoft.AI.Agents.Abstractions;
using Microsoft.AI.Agents.Dapr;
using Microsoft.AI.DevTeam.Dapr.Events;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Memory;
namespace Microsoft.AI.DevTeam.Dapr;
public class ProductManager : AiAgent<ProductManagerState>, IDaprAgent
{
private readonly ILogger<ProductManager> _logger;
public ProductManager(ActorHost host, DaprClient client, Kernel kernel, ISemanticTextMemory memory, ILogger<ProductManager> logger)
: base(host, client, memory, kernel)
{
_logger = logger;
}
public async override Task HandleEvent(Event item)
{
switch (item.Type)
{
case nameof(GithubFlowEventType.ReadmeRequested):
{
var context = item.ToGithubContext();
var readme = await CreateReadme(item.Data["input"]);
var data = context.ToData();
data["result"]=readme;
await PublishEvent(Consts.PubSub,Consts.MainTopic, new Event {
Type = nameof(GithubFlowEventType.ReadmeGenerated),
Subject = context.Subject,
Data = data
});
}
break;
case nameof(GithubFlowEventType.ReadmeChainClosed):
{
var context = item.ToGithubContext();
var lastReadme = state.History.Last().Message;
var data = context.ToData();
data["readme"] = lastReadme;
await PublishEvent(Consts.PubSub,Consts.MainTopic, new Event {
Type = nameof(GithubFlowEventType.ReadmeCreated),
Subject = context.Subject,
Data = data
});
}
break;
default:
break;
}
}
public async Task<string> CreateReadme(string ask)
{
try
{
var context = new KernelArguments { ["input"] = AppendChatHistory(ask) };
var instruction = "Consider the following architectural guidelines:!waf!";
var enhancedContext = await AddKnowledge(instruction, "waf", context);
return await CallFunction(PMSkills.Readme, enhancedContext);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error creating readme");
return default;
}
}
}
public class ProductManagerState
{
public string Capabilities { get; set; }
}

View File

@ -0,0 +1,106 @@
using Dapr.Actors.Runtime;
using Dapr.Client;
using Microsoft.AI.Agents.Abstractions;
using Microsoft.AI.Agents.Dapr;
using Microsoft.AI.DevTeam.Dapr.Events;
namespace Microsoft.AI.DevTeam.Dapr;
public class Sandbox : Agent, IDaprAgent, IRemindable
{
private const string ReminderName = "SandboxRunReminder";
public string StateStore = "agents-statestore";
private readonly IManageAzure _azService;
public Sandbox(ActorHost host, DaprClient client, IManageAzure azService) : base(host, client)
{
_azService = azService;
}
public override async Task HandleEvent(Event item)
{
switch(item.Type)
{
case nameof(GithubFlowEventType.SandboxRunCreated):
{
var context = item.ToGithubContext();
await ScheduleCommitSandboxRun(context.Org, context.Repo, context.ParentNumber.Value, context.IssueNumber);
}
break;
default:
break;
}
}
public async Task ScheduleCommitSandboxRun(string org, string repo, long parentIssueNumber, long issueNumber)
{
await StoreState(org, repo, parentIssueNumber, issueNumber);
await this.RegisterReminderAsync(
ReminderName,
null,
TimeSpan.Zero,
TimeSpan.FromMinutes(1));
}
private async Task StoreState(string org, string repo, long parentIssueNumber, long issueNumber)
{
var state = new SandboxMetadata {
Org = org,
Repo = repo,
IssueNumber = issueNumber,
ParentIssueNumber = parentIssueNumber,
IsCompleted = false
};
await StateManager.SetStateAsync(
StateStore,
state);
}
private async Task Cleanup()
{
var agentState = await StateManager.GetStateAsync<SandboxMetadata>(StateStore);
agentState.IsCompleted = true;
await UnregisterReminderAsync(ReminderName);
await StateManager.SetStateAsync(
StateStore,
agentState);
}
public async Task ReceiveReminderAsync(string reminderName, byte[] state, TimeSpan dueTime, TimeSpan period)
{
var agentState = await StateManager.GetStateAsync<SandboxMetadata>(StateStore);
if (!agentState.IsCompleted)
{
var sandboxId = $"sk-sandbox-{agentState.Org}-{agentState.Repo}-{agentState.ParentIssueNumber}-{agentState.IssueNumber}";
if (await _azService.IsSandboxCompleted(sandboxId))
{
await _azService.DeleteSandbox(sandboxId);
var data = new Dictionary<string, string> {
{ "org", agentState.Org },
{ "repo", agentState.Repo },
{ "issueNumber", agentState.IssueNumber.ToString() },
{ "parentNumber", agentState.ParentIssueNumber.ToString() }
};
var subject = $"{agentState.Org}-{agentState.Repo}-{agentState.IssueNumber}";
await PublishEvent(Consts.PubSub,Consts.MainTopic, new Event {
Type = nameof(GithubFlowEventType.SandboxRunFinished),
Subject = subject,
Data = data
});
await Cleanup();
}
}
else
{
await Cleanup();
}
}
}
public class SandboxMetadata
{
public string Org { get; set; }
public string Repo { get; set; }
public long ParentIssueNumber { get; set; }
public long IssueNumber { get; set; }
public bool IsCompleted { get; set; }
}

View File

@ -0,0 +1,23 @@
FROM mcr.microsoft.com/dotnet/aspnet:7.0 AS base
WORKDIR /app
EXPOSE 5274
EXPOSE 11111
EXPOSE 30000
ENV ASPNETCORE_URLS=http://+:5274
FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build
ARG configuration=Release
COPY . .
RUN dotnet restore "src/apps/gh-flow/gh-flow.csproj"
WORKDIR "/src/apps/gh-flow"
RUN dotnet build "gh-flow.csproj" -c $configuration -o /app/build
FROM build AS publish
ARG configuration=Release
RUN dotnet publish "gh-flow.csproj" -c $configuration -o /app/publish
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "gh-flow.dll"]

View File

@ -0,0 +1,69 @@
using System.Runtime.Serialization;
using System.Text.Json.Serialization;
using Microsoft.AI.Agents.Abstractions;
namespace Microsoft.AI.DevTeam.Dapr.Events
{
public enum GithubFlowEventType
{
NewAsk,
ReadmeChainClosed,
CodeChainClosed,
CodeGenerationRequested,
DevPlanRequested,
ReadmeGenerated,
DevPlanGenerated,
CodeGenerated,
DevPlanChainClosed,
ReadmeRequested,
ReadmeStored,
SandboxRunFinished,
ReadmeCreated,
CodeCreated,
DevPlanCreated,
SandboxRunCreated
}
public static class EventExtensions
{
public static GithubContext ToGithubContext(this Event evt)
{
return new GithubContext
{
Org = evt.Data["org"],
Repo = evt.Data["repo"],
IssueNumber = long.Parse(evt.Data["issueNumber"]),
ParentNumber = string.IsNullOrEmpty(evt.Data["parentNumber"]) ? default : long.Parse(evt.Data["parentNumber"])
};
}
public static Dictionary<string, string> ToData(this GithubContext context)
{
return new Dictionary<string, string> {
{ "org", context.Org },
{ "repo", context.Repo },
{ "issueNumber", $"{context.IssueNumber}" },
{ "parentNumber", context.ParentNumber.HasValue? context.ParentNumber.ToString(): default }
};
}
}
public class GithubContext
{
public string Org { get; set; }
public string Repo { get; set; }
public long IssueNumber { get; set; }
public long? ParentNumber { get; set; }
public string Subject => $"{Org}-{Repo}-{IssueNumber}";
}
[DataContract]
public class EventEnvelope
{
[JsonPropertyName("data")]
public Event Data { get; set; }
}
}

View File

@ -0,0 +1,43 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<ServerGarbageCollection>true</ServerGarbageCollection>
<ConcurrentGarbageCollection>true</ConcurrentGarbageCollection>
<UserSecretsId>c073c86e-8483-4956-942f-331fd09172d4</UserSecretsId>
<AnalysisMode>All</AnalysisMode>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Octokit.Webhooks.AspNetCore" Version="2.0.3" />
<PackageReference Include="Octokit" Version="10.0.0" />
<PackageReference Include="Microsoft.SemanticKernel" Version="1.10.0" />
<PackageReference Include="Microsoft.SemanticKernel.Connectors.Qdrant" Version="1.10.0-alpha" />
<PackageReference Include="Microsoft.SemanticKernel.Plugins.Memory" Version="1.10.0-alpha" />
<PackageReference Include="Microsoft.ApplicationInsights.AspNetCore" Version="2.21.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
<PackageReference Include="Microsoft.Extensions.Azure" Version="1.7.2" />
<PackageReference Include="Microsoft.Extensions.Http.Resilience" Version="8.0.0" />
<PackageReference Include="Azure.ResourceManager.ContainerInstance" Version="1.2.0" />
<PackageReference Include="Azure.Storage.Files.Shares" Version="12.16.0-beta.1" />
<PackageReference Include="Azure.Data.Tables" Version="12.8.1" />
<PackageReference Include="Azure.Identity" Version="1.11.0-beta.1" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="7.4.1" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Dapr.Actors.AspNetCore" Version="1.13.0" />
<PackageReference Include="Dapr.AspNetCore" Version="1.13.0" />
<PackageReference Include="CloudNative.CloudEvents.SystemTextJson" Version="2.7.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\..\csharp\src\Microsoft.AI.Agents.Dapr\Microsoft.AI.Agents.Dapr.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,22 @@
using System.ComponentModel.DataAnnotations;
namespace Microsoft.AI.DevTeam.Dapr;
public class AzureOptions
{
[Required]
public string SubscriptionId { get; set; }
[Required]
public string Location { get; set; }
[Required]
public string ContainerInstancesResourceGroup { get; set; }
[Required]
public string FilesShareName { get; set; }
[Required]
public string FilesAccountName { get; set; }
[Required]
public string FilesAccountKey { get; set; }
[Required]
public string SandboxImage { get; set; }
public string ManagedIdentity { get; set; }
public string CosmosConnectionString { get; set; }
}

View File

@ -0,0 +1,8 @@
namespace Microsoft.AI.DevTeam.Dapr;
public static class Consts
{
public const string MainTopic = "DevPersonas";
public const string PubSub = "agents-pubsub";
public const string AppId = "dev-agents";
}

View File

@ -0,0 +1,14 @@
using System.ComponentModel.DataAnnotations;
namespace Microsoft.AI.DevTeam.Dapr;
public class GithubOptions
{
[Required]
public string AppKey { get; set; }
[Required]
public int AppId { get; set; }
[Required]
public long InstallationId { get; set; }
[Required]
public string WebhookSecret { get; set; }
}

View File

@ -0,0 +1,18 @@
using System.ComponentModel.DataAnnotations;
namespace Microsoft.AI.DevTeam.Dapr;
public class OpenAIOptions
{
[Required]
public string ServiceType { get; set; }
[Required]
public string ServiceId { get; set; }
[Required]
public string DeploymentOrModelId { get; set; }
[Required]
public string EmbeddingDeploymentOrModelId { get; set; }
[Required]
public string Endpoint { get; set; }
[Required]
public string ApiKey { get; set; }
}

View File

@ -0,0 +1,10 @@
using System.ComponentModel.DataAnnotations;
namespace Microsoft.AI.DevTeam.Dapr;
public class QdrantOptions
{
[Required]
public string Endpoint { get; set; }
[Required]
public int VectorSize { get; set; }
}

View File

@ -0,0 +1,10 @@
using System.ComponentModel.DataAnnotations;
namespace Microsoft.AI.DevTeam.Dapr;
public class ServiceOptions
{
private string _ingesterUrl;
[Required]
public string IngesterUrl { get; set; }
}

View File

@ -0,0 +1,205 @@
using System.Text.Json;
using Azure;
using Azure.AI.OpenAI;
using Microsoft.Extensions.Options;
using Microsoft.SemanticKernel;
using Octokit.Webhooks;
using Octokit.Webhooks.AspNetCore;
using Azure.Identity;
using Microsoft.Extensions.Azure;
using Microsoft.Extensions.Http.Resilience;
using Microsoft.SemanticKernel.Memory;
using Microsoft.SemanticKernel.Connectors.Qdrant;
using Microsoft.SemanticKernel.Connectors.OpenAI;
using Dapr;
using Dapr.Actors.Client;
using Dapr.Actors;
using Microsoft.AI.DevTeam.Dapr;
using Microsoft.AI.DevTeam.Dapr.Events;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<WebhookEventProcessor, GithubWebHookProcessor>();
builder.Services.AddTransient(CreateKernel);
builder.Services.AddTransient(CreateMemory);
builder.Services.AddHttpClient();
builder.Services.AddSingleton(s =>
{
var ghOptions = s.GetService<IOptions<GithubOptions>>();
var logger = s.GetService<ILogger<GithubAuthService>>();
var ghService = new GithubAuthService(ghOptions, logger);
var client = ghService.GetGitHubClient();
return client;
});
builder.Services.AddAzureClients(clientBuilder =>
{
clientBuilder.UseCredential(new DefaultAzureCredential());
clientBuilder.AddArmClient(default);
});
builder.Services.AddDaprClient();
builder.Services.AddActors(
options =>
{
options.UseJsonSerialization = true;
options.JsonSerializerOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web);
options.ReentrancyConfig = new ActorReentrancyConfig {
Enabled = true
};
options.Actors.RegisterActor<Dev>();
options.Actors.RegisterActor<DeveloperLead>();
options.Actors.RegisterActor<ProductManager>();
options.Actors.RegisterActor<AzureGenie>();
options.Actors.RegisterActor<Hubber>();
options.Actors.RegisterActor<Sandbox>();
});
builder.Services.AddSingleton<GithubAuthService>();
builder.Services.AddApplicationInsightsTelemetry();
builder.Services.AddOptions<GithubOptions>()
.Configure<IConfiguration>((settings, configuration) =>
{
configuration.GetSection(nameof(GithubOptions)).Bind(settings);
})
.ValidateDataAnnotations()
.ValidateOnStart();
builder.Services.AddOptions<AzureOptions>()
.Configure<IConfiguration>((settings, configuration) =>
{
configuration.GetSection(nameof(AzureOptions)).Bind(settings);
})
.ValidateDataAnnotations()
.ValidateOnStart();
builder.Services.AddOptions<OpenAIOptions>()
.Configure<IConfiguration>((settings, configuration) =>
{
configuration.GetSection(nameof(OpenAIOptions)).Bind(settings);
})
.ValidateDataAnnotations()
.ValidateOnStart();
builder.Services.AddOptions<QdrantOptions>()
.Configure<IConfiguration>((settings, configuration) =>
{
configuration.GetSection(nameof(QdrantOptions)).Bind(settings);
})
.ValidateDataAnnotations()
.ValidateOnStart();
builder.Services.AddOptions<ServiceOptions>()
.Configure<IConfiguration>((settings, configuration) =>
{
configuration.GetSection(nameof(ServiceOptions)).Bind(settings);
})
.ValidateDataAnnotations()
.ValidateOnStart();
builder.Services.AddSingleton<IManageAzure, AzureService>();
builder.Services.AddSingleton<IManageGithub, GithubService>();
builder.Services.AddSingleton<IAnalyzeCode, CodeAnalyzer>();
builder.Services.Configure<JsonSerializerOptions>(options =>
{
options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
});
var app = builder.Build();
app.UseRouting()
.UseEndpoints(endpoints =>
{
var ghOptions = app.Services.GetService<IOptions<GithubOptions>>().Value;
endpoints.MapGitHubWebhooks(secret: ghOptions.WebhookSecret);
endpoints.MapActorsHandlers();
endpoints.MapSubscribeHandler();
});
app.UseCloudEvents();
app.MapPost("/developers", [Topic(Consts.PubSub, Consts.MainTopic,
$"(event.type ==\"{nameof(GithubFlowEventType.CodeGenerationRequested)}\") || (event.type ==\"{nameof(GithubFlowEventType.CodeGenerationRequested)}\")", 1)]
async (IActorProxyFactory proxyFactory, EventEnvelope evt) => await HandleEvent(proxyFactory,nameof(Dev), nameof(Dev.HandleEvent), evt));
app.MapPost("/devleads", [Topic(Consts.PubSub, Consts.MainTopic,
$"(event.type ==\"{nameof(GithubFlowEventType.DevPlanRequested)}\") || (event.type ==\"{nameof(GithubFlowEventType.DevPlanChainClosed)}\")", 2)]
async (IActorProxyFactory proxyFactory, EventEnvelope evt) => await HandleEvent(proxyFactory, nameof(DeveloperLead), nameof(DeveloperLead.HandleEvent), evt));
app.MapPost("/productmanagers", [Topic(Consts.PubSub, Consts.MainTopic,
$"(event.type ==\"{nameof(GithubFlowEventType.ReadmeRequested)}\") || (event.type ==\"{nameof(GithubFlowEventType.ReadmeChainClosed)}\")", 3)]
async (IActorProxyFactory proxyFactory, EventEnvelope evt) => await HandleEvent(proxyFactory, nameof(ProductManager), nameof(ProductManager.HandleEvent), evt));
app.MapPost("/hubbers", [Topic(Consts.PubSub, Consts.MainTopic,
$"(event.type ==\"{nameof(GithubFlowEventType.NewAsk)}\") || (event.type ==\"{nameof(GithubFlowEventType.ReadmeGenerated)}\") || (event.type ==\"{nameof(GithubFlowEventType.DevPlanGenerated)}\") || (event.type ==\"{nameof(GithubFlowEventType.CodeGenerated)}\") || (event.type ==\"{nameof(GithubFlowEventType.DevPlanCreated)}\") || (event.type ==\"{nameof(GithubFlowEventType.ReadmeStored)}\") || (event.type ==\"{nameof(GithubFlowEventType.SandboxRunFinished)}\")", 4)]
async (IActorProxyFactory proxyFactory, EventEnvelope evt) => await HandleEvent(proxyFactory, nameof(Hubber), nameof(Hubber.HandleEvent), evt));
app.MapPost("/azuregenies", [Topic(Consts.PubSub, Consts.MainTopic,
$"(event.type ==\"{nameof(GithubFlowEventType.ReadmeCreated)}\") || (event.type ==\"{nameof(GithubFlowEventType.CodeCreated)}\")", 5)]
async (IActorProxyFactory proxyFactory, EventEnvelope evt) => await HandleEvent(proxyFactory, nameof(AzureGenie), nameof(AzureGenie.HandleEvent), evt));
app.MapPost("/sandboxes", [Topic(Consts.PubSub, Consts.MainTopic,$"(event.type ==\"{nameof(GithubFlowEventType.SandboxRunCreated)}\")", 6)]
async (IActorProxyFactory proxyFactory, EventEnvelope evt) => await HandleEvent(proxyFactory, nameof(Sandbox), nameof(Sandbox.HandleEvent), evt));
app.Run();
static async Task HandleEvent(IActorProxyFactory proxyFactory, string type, string method, EventEnvelope evt)
{
try
{
var proxyOptions = new ActorProxyOptions
{
RequestTimeout = Timeout.InfiniteTimeSpan
};
var proxy = proxyFactory.Create(new ActorId(evt.Data.Subject), type, proxyOptions);
await proxy.InvokeMethodAsync(method, evt.Data);
}
catch (Exception ex)
{
throw;
}
}
static ISemanticTextMemory CreateMemory(IServiceProvider provider)
{
var openAiConfig = provider.GetService<IOptions<OpenAIOptions>>().Value;
var qdrantConfig = provider.GetService<IOptions<QdrantOptions>>().Value;
var loggerFactory = LoggerFactory.Create(builder =>
{
builder
.SetMinimumLevel(LogLevel.Debug)
.AddConsole()
.AddDebug();
});
var memoryBuilder = new MemoryBuilder();
return memoryBuilder.WithLoggerFactory(loggerFactory)
.WithQdrantMemoryStore(qdrantConfig.Endpoint, qdrantConfig.VectorSize)
.WithAzureOpenAITextEmbeddingGeneration(openAiConfig.EmbeddingDeploymentOrModelId, openAiConfig.Endpoint, openAiConfig.ApiKey)
.Build();
}
static Kernel CreateKernel(IServiceProvider provider)
{
var openAiConfig = provider.GetService<IOptions<OpenAIOptions>>().Value;
var clientOptions = new OpenAIClientOptions();
clientOptions.Retry.NetworkTimeout = TimeSpan.FromMinutes(5);
var openAIClient = new OpenAIClient(new Uri(openAiConfig.Endpoint), new AzureKeyCredential(openAiConfig.ApiKey), clientOptions);
var builder = Kernel.CreateBuilder();
builder.Services.AddLogging(c => c.AddConsole().AddDebug().SetMinimumLevel(LogLevel.Debug));
builder.Services.AddAzureOpenAIChatCompletion(openAiConfig.DeploymentOrModelId, openAIClient);
builder.Services.ConfigureHttpClientDefaults(c =>
{
c.AddStandardResilienceHandler().Configure(o =>
{
o.Retry.MaxRetryAttempts = 5;
o.Retry.BackoffType = Polly.DelayBackoffType.Exponential;
});
});
return builder.Build();
}

View File

@ -0,0 +1,38 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:59668",
"sslPort": 44354
}
},
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://localhost:5244",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://localhost:7227;http://localhost:5244",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View File

@ -0,0 +1,160 @@
using System.Text;
using Azure;
using Azure.Core;
using Azure.ResourceManager;
using Azure.ResourceManager.ContainerInstance;
using Azure.ResourceManager.ContainerInstance.Models;
using Azure.ResourceManager.Resources;
using Azure.Storage.Files.Shares;
using Microsoft.Extensions.Options;
namespace Microsoft.AI.DevTeam.Dapr;
public class AzureService : IManageAzure
{
private readonly AzureOptions _azSettings;
private readonly ILogger<AzureService> _logger;
private readonly ArmClient _client;
public AzureService(IOptions<AzureOptions> azOptions, ILogger<AzureService> logger, ArmClient client)
{
_azSettings = azOptions.Value;
_logger = logger;
_client = client;
}
public async Task DeleteSandbox(string sandboxId)
{
try
{
var resourceGroupResourceId = ResourceGroupResource.CreateResourceIdentifier(_azSettings.SubscriptionId, _azSettings.ContainerInstancesResourceGroup);
var resourceGroupResource = _client.GetResourceGroupResource(resourceGroupResourceId);
var collection = resourceGroupResource.GetContainerGroups();
var containerGroup = await collection.GetAsync(sandboxId);
await containerGroup.Value.DeleteAsync(WaitUntil.Started);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error deleting sandbox");
throw;
}
}
public async Task<bool> IsSandboxCompleted(string sandboxId)
{
try
{
var resourceGroupResourceId = ResourceGroupResource.CreateResourceIdentifier(_azSettings.SubscriptionId, _azSettings.ContainerInstancesResourceGroup);
var resourceGroupResource = _client.GetResourceGroupResource(resourceGroupResourceId);
var collection = resourceGroupResource.GetContainerGroups();
var containerGroup = await collection.GetAsync(sandboxId);
return containerGroup.Value.Data.ProvisioningState == "Succeeded"
&& containerGroup.Value.Data.Containers.First().InstanceView.CurrentState.State == "Terminated";
}
catch (Exception ex)
{
_logger.LogError(ex, "Error checking sandbox status");
throw;
}
}
public async Task RunInSandbox(string org, string repo, long parentIssueNumber, long issueNumber)
{
try
{
var runId = $"sk-sandbox-{org}-{repo}-{parentIssueNumber}-{issueNumber}";
var resourceGroupResourceId = ResourceGroupResource.CreateResourceIdentifier(_azSettings.SubscriptionId, _azSettings.ContainerInstancesResourceGroup);
var resourceGroupResource = _client.GetResourceGroupResource(resourceGroupResourceId);
var scriptPath = $"/azfiles/output/{org}-{repo}/{parentIssueNumber}/{issueNumber}/run.sh";
var collection = resourceGroupResource.GetContainerGroups();
var data = new ContainerGroupData(new AzureLocation(_azSettings.Location), new ContainerInstanceContainer[]
{
new ContainerInstanceContainer(runId,_azSettings.SandboxImage,new ContainerResourceRequirements(new ContainerResourceRequestsContent(1.5,1)))
{
Command = { "/bin/bash", $"{scriptPath}" },
VolumeMounts =
{
new ContainerVolumeMount("azfiles","/azfiles/")
{
IsReadOnly = false,
}
},
}}, ContainerInstanceOperatingSystemType.Linux)
{
Volumes =
{
new ContainerVolume("azfiles")
{
AzureFile = new ContainerInstanceAzureFileVolume(_azSettings.FilesShareName,_azSettings.FilesAccountName)
{
StorageAccountKey = _azSettings.FilesAccountKey
},
},
},
RestartPolicy = ContainerGroupRestartPolicy.Never,
Sku = ContainerGroupSku.Standard,
Priority = ContainerGroupPriority.Regular
};
await collection.CreateOrUpdateAsync(WaitUntil.Completed, runId, data);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error running sandbox");
throw;
}
}
public async Task Store(string org, string repo, long parentIssueNumber, long issueNumber, string filename, string extension, string dir, string output)
{
try
{
var connectionString = $"DefaultEndpointsProtocol=https;AccountName={_azSettings.FilesAccountName};AccountKey={_azSettings.FilesAccountKey};EndpointSuffix=core.windows.net";
var parentDirName = $"{dir}/{org}-{repo}";
var fileName = $"{filename}.{extension}";
var share = new ShareClient(connectionString, _azSettings.FilesShareName);
await share.CreateIfNotExistsAsync();
await share.GetDirectoryClient($"{dir}").CreateIfNotExistsAsync(); ;
var parentDir = share.GetDirectoryClient(parentDirName);
await parentDir.CreateIfNotExistsAsync();
var parentIssueDir = parentDir.GetSubdirectoryClient($"{parentIssueNumber}");
await parentIssueDir.CreateIfNotExistsAsync();
var directory = parentIssueDir.GetSubdirectoryClient($"{issueNumber}");
await directory.CreateIfNotExistsAsync();
var file = directory.GetFileClient(fileName);
// hack to enable script to save files in the same directory
var cwdHack = "#!/bin/bash\n cd $(dirname $0)";
var contents = extension == "sh" ? output.Replace("#!/bin/bash", cwdHack) : output;
using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(contents)))
{
await file.CreateAsync(stream.Length);
await file.UploadRangeAsync(
new HttpRange(0, stream.Length),
stream);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error storing output");
throw;
}
}
}
public interface IManageAzure
{
Task Store(string org, string repo, long parentIssueNumber, long issueNumber, string filename, string extension, string dir, string output);
Task RunInSandbox(string org, string repo, long parentIssueNumber, long issueNumber);
Task<bool> IsSandboxCompleted(string sandboxId);
Task DeleteSandbox(string sandboxId);
}

View File

@ -0,0 +1,53 @@
using System.Text;
using System.Text.Json;
using Microsoft.Extensions.Options;
namespace Microsoft.AI.DevTeam.Dapr;
public interface IAnalyzeCode
{
Task<IEnumerable<CodeAnalysis>> Analyze(string content);
}
public class CodeAnalyzer : IAnalyzeCode
{
private readonly ServiceOptions _serviceOptions;
private readonly HttpClient _httpClient;
private readonly ILogger<CodeAnalyzer> _logger;
public CodeAnalyzer(IOptions<ServiceOptions> serviceOptions, HttpClient httpClient, ILogger<CodeAnalyzer> logger)
{
_serviceOptions = serviceOptions.Value;
_httpClient = httpClient;
_logger = logger;
_httpClient.BaseAddress = new Uri(_serviceOptions.IngesterUrl);
}
public async Task<IEnumerable<CodeAnalysis>> Analyze(string content)
{
try
{
var request = new CodeAnalysisRequest { Content = content };
var body = new StringContent(JsonSerializer.Serialize(request), Encoding.UTF8, "application/json");
var response = await _httpClient.PostAsync("api/AnalyzeCode", body);
var stringResult = await response.Content.ReadAsStringAsync();
var result = JsonSerializer.Deserialize<IEnumerable<CodeAnalysis>>(stringResult);
return result;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error analyzing code");
return Enumerable.Empty<CodeAnalysis>();
}
}
}
public class CodeAnalysisRequest
{
public string Content { get; set; }
}
public class CodeAnalysis
{
public string Meaning { get; set; }
public string CodeBlock { get; set; }
}

View File

@ -0,0 +1,68 @@
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Security.Cryptography;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using Octokit;
namespace Microsoft.AI.DevTeam.Dapr;
public class GithubAuthService
{
private readonly GithubOptions _githubSettings;
private readonly ILogger<GithubAuthService> _logger;
public GithubAuthService(IOptions<GithubOptions> ghOptions, ILogger<GithubAuthService> logger)
{
_githubSettings = ghOptions.Value;
_logger = logger;
}
public string GenerateJwtToken(string appId, string appKey, int minutes)
{
using var rsa = RSA.Create();
rsa.ImportFromPem(appKey);
var securityKey = new RsaSecurityKey(rsa);
var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.RsaSha256);
var now = DateTime.UtcNow;
var iat = new DateTimeOffset(now).ToUnixTimeSeconds();
var exp = new DateTimeOffset(now.AddMinutes(minutes)).ToUnixTimeSeconds();
var claims = new[] {
new Claim(JwtRegisteredClaimNames.Iat, iat.ToString(), ClaimValueTypes.Integer64),
new Claim(JwtRegisteredClaimNames.Exp, exp.ToString(), ClaimValueTypes.Integer64)
};
var token = new JwtSecurityToken(
issuer: appId,
claims: claims,
expires: DateTime.Now.AddMinutes(10),
signingCredentials: credentials
);
return new JwtSecurityTokenHandler().WriteToken(token);
}
public GitHubClient GetGitHubClient()
{
try
{
var jwtToken = GenerateJwtToken(_githubSettings.AppId.ToString(), _githubSettings.AppKey, 10);
var appClient = new GitHubClient(new ProductHeaderValue("SK-DEV-APP"))
{
Credentials = new Credentials(jwtToken, AuthenticationType.Bearer)
};
var response = appClient.GitHubApps.CreateInstallationToken(_githubSettings.InstallationId).Result;
return new GitHubClient(new ProductHeaderValue($"SK-DEV-APP-Installation{_githubSettings.InstallationId}"))
{
Credentials = new Credentials(response.Token)
};
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting GitHub client");
throw;
}
}
}

View File

@ -0,0 +1,227 @@
using System.Text;
using Azure.Storage.Files.Shares;
using Microsoft.Extensions.Options;
using Octokit;
using Octokit.Helpers;
namespace Microsoft.AI.DevTeam.Dapr;
public class GithubService : IManageGithub
{
private readonly GitHubClient _ghClient;
private readonly AzureOptions _azSettings;
private readonly ILogger<GithubService> _logger;
private readonly HttpClient _httpClient;
public GithubService(IOptions<AzureOptions> azOptions, GitHubClient ghClient, ILogger<GithubService> logger, HttpClient httpClient)
{
_ghClient = ghClient;
_azSettings = azOptions.Value;
_logger = logger;
_httpClient = httpClient;
}
public async Task CommitToBranch(string org, string repo, long parentNumber, long issueNumber, string rootDir, string branch)
{
try
{
var connectionString = $"DefaultEndpointsProtocol=https;AccountName={_azSettings.FilesAccountName};AccountKey={_azSettings.FilesAccountKey};EndpointSuffix=core.windows.net";
var dirName = $"{rootDir}/{org}-{repo}/{parentNumber}/{issueNumber}";
var share = new ShareClient(connectionString, _azSettings.FilesShareName);
var directory = share.GetDirectoryClient(dirName);
var remaining = new Queue<ShareDirectoryClient>();
remaining.Enqueue(directory);
while (remaining.Count > 0)
{
var dir = remaining.Dequeue();
await foreach (var item in dir.GetFilesAndDirectoriesAsync())
{
if (!item.IsDirectory && item.Name != "run.sh") // we don't want the generated script in the PR
{
try
{
var file = dir.GetFileClient(item.Name);
var filePath = file.Path.Replace($"{_azSettings.FilesShareName}/", "")
.Replace($"{dirName}/", "");
var fileStream = await file.OpenReadAsync();
using (var reader = new StreamReader(fileStream, Encoding.UTF8))
{
var value = reader.ReadToEnd();
await _ghClient.Repository.Content.CreateFile(
org, repo, filePath,
new CreateFileRequest($"Commit message", value, branch)); // TODO: add more meaningfull commit message
}
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error while uploading file {item.Name}");
}
}
else if (item.IsDirectory)
{
remaining.Enqueue(dir.GetSubdirectoryClient(item.Name));
}
}
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error committing to branch");
throw;
}
}
public async Task CreateBranch(string org, string repo, string branch)
{
try
{
var ghRepo = await _ghClient.Repository.Get(org, repo);
if (ghRepo.Size == 0)
{
// Create a new file and commit it to the repository
var createChangeSet = await _ghClient.Repository.Content.CreateFile(
org,
repo,
"README.md",
new CreateFileRequest("Initial commit", "# Readme")
);
}
await _ghClient.Git.Reference.CreateBranch(org, repo, branch, ghRepo.DefaultBranch);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error creating branch");
throw;
}
}
public async Task<string> GetMainLanguage(string org, string repo)
{
try
{
var languages = await _ghClient.Repository.GetAllLanguages(org, repo);
var mainLanguage = languages.OrderByDescending(l => l.NumberOfBytes).First();
return mainLanguage.Name;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting main language");
throw;
}
}
public async Task<int> CreateIssue(string org, string repo, string input, string function, long parentNumber)
{
try
{
var newIssue = new NewIssue($"{function} chain for #{parentNumber}")
{
Body = input,
};
newIssue.Labels.Add(function);
newIssue.Labels.Add($"Parent.{parentNumber}");
var issue = await _ghClient.Issue.Create(org, repo, newIssue);
return issue.Number;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error creating issue");
throw;
}
}
public async Task CreatePR(string org, string repo, long number, string branch)
{
try
{
var ghRepo = await _ghClient.Repository.Get(org, repo);
await _ghClient.PullRequest.Create(org, repo, new NewPullRequest($"New app #{number}", branch, ghRepo.DefaultBranch));
}
catch (Exception ex)
{
_logger.LogError(ex, "Error creating PR");
throw;
}
}
public async Task PostComment(string org, string repo, long issueNumber, string comment)
{
try
{
await _ghClient.Issue.Comment.Create(org, repo, (int)issueNumber, comment);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error posting comment");
throw;
}
}
public async Task<IEnumerable<FileResponse>> GetFiles(string org, string repo, string branch, Func<RepositoryContent, bool> filter)
{
try
{
var items = await _ghClient.Repository.Content.GetAllContentsByRef(org, repo, branch);
return await CollectFiles(org, repo, branch, items, filter);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting files");
throw;
}
}
private async Task<IEnumerable<FileResponse>> CollectFiles(string org, string repo, string branch, IReadOnlyList<RepositoryContent> items, Func<RepositoryContent, bool> filter)
{
try
{
var result = new List<FileResponse>();
foreach (var item in items)
{
if (item.Type == ContentType.File && filter(item))
{
var content = await _httpClient.GetStringAsync(item.DownloadUrl);
result.Add(new FileResponse
{
Name = item.Name,
Content = content
});
}
else if (item.Type == ContentType.Dir)
{
var subItems = await _ghClient.Repository.Content.GetAllContentsByRef(org, repo, item.Path, branch);
result.AddRange(await CollectFiles(org, repo, branch, subItems, filter));
}
}
return result;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error collecting files");
throw;
}
}
}
public class FileResponse
{
public string Name { get; set; }
public string Content { get; set; }
}
public interface IManageGithub
{
Task<int> CreateIssue(string org, string repo, string input, string function, long parentNumber);
Task CreatePR(string org, string repo, long number, string branch);
Task CreateBranch(string org, string repo, string branch);
Task CommitToBranch(string org, string repo, long parentNumber, long issueNumber, string rootDir, string branch);
Task PostComment(string org, string repo, long issueNumber, string comment);
Task<IEnumerable<FileResponse>> GetFiles(string org, string repo, string branch, Func<RepositoryContent, bool> filter);
Task<string> GetMainLanguage(string org, string repo);
}

View File

@ -0,0 +1,169 @@
using Dapr.Client;
using Microsoft.AI.Agents.Abstractions;
using Microsoft.AI.DevTeam.Dapr.Events;
using Octokit.Webhooks;
using Octokit.Webhooks.Events;
using Octokit.Webhooks.Events.IssueComment;
using Octokit.Webhooks.Events.Issues;
using Octokit.Webhooks.Models;
namespace Microsoft.AI.DevTeam.Dapr;
public sealed class GithubWebHookProcessor : WebhookEventProcessor
{
private readonly DaprClient _daprClient;
private readonly ILogger<GithubWebHookProcessor> _logger;
public GithubWebHookProcessor(DaprClient daprClient, ILogger<GithubWebHookProcessor> logger)
{
_daprClient = daprClient;
_logger = logger;
}
protected override async Task ProcessIssuesWebhookAsync(WebhookHeaders headers, IssuesEvent issuesEvent, IssuesAction action)
{
try
{
_logger.LogInformation("Processing issue event");
var org = issuesEvent.Repository.Owner.Login;
var repo = issuesEvent.Repository.Name;
var issueNumber = issuesEvent.Issue.Number;
var input = issuesEvent.Issue.Body;
// Assumes the label follows the following convention: Skill.Function example: PM.Readme
// Also, we've introduced the Parent label, that ties the sub-issue with the parent issue
var labels = issuesEvent.Issue.Labels
.Select(l => l.Name.Split('.'))
.Where(parts => parts.Length == 2)
.ToDictionary(parts => parts[0], parts => parts[1]);
var skillName = labels.Keys.Where(k => k != "Parent").FirstOrDefault();
long? parentNumber = labels.ContainsKey("Parent") ? long.Parse(labels["Parent"]) : null;
var suffix = $"{org}-{repo}";
if (issuesEvent.Action == IssuesAction.Opened)
{
_logger.LogInformation("Processing HandleNewAsk");
await HandleNewAsk(issueNumber, parentNumber, skillName, labels[skillName], input, org, repo);
}
else if (issuesEvent.Action == IssuesAction.Closed && issuesEvent.Issue.User.Type.Value == UserType.Bot)
{
_logger.LogInformation("Processing HandleClosingIssue");
await HandleClosingIssue(issueNumber, parentNumber, skillName, labels[skillName], org, repo);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Processing issue event");
throw;
}
}
protected override async Task ProcessIssueCommentWebhookAsync(
WebhookHeaders headers,
IssueCommentEvent issueCommentEvent,
IssueCommentAction action)
{
try
{
_logger.LogInformation("Processing issue comment event");
var org = issueCommentEvent.Repository.Owner.Login;
var repo = issueCommentEvent.Repository.Name;
var issueNumber = issueCommentEvent.Issue.Number;
var input = issueCommentEvent.Comment.Body;
// Assumes the label follows the following convention: Skill.Function example: PM.Readme
var labels = issueCommentEvent.Issue.Labels
.Select(l => l.Name.Split('.'))
.Where(parts => parts.Length == 2)
.ToDictionary(parts => parts[0], parts => parts[1]);
var skillName = labels.Keys.Where(k => k != "Parent").FirstOrDefault();
long? parentNumber = labels.ContainsKey("Parent") ? long.Parse(labels["Parent"]) : null;
var suffix = $"{org}-{repo}";
// we only respond to non-bot comments
if (issueCommentEvent.Sender.Type.Value != UserType.Bot)
{
await HandleNewAsk(issueNumber, parentNumber, skillName, labels[skillName], input, org, repo);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Processing issue comment event");
throw;
}
}
private async Task HandleClosingIssue(long issueNumber, long? parentNumber, string skillName, string functionName, string org, string repo)
{
var subject = $"{org}-{repo}-{issueNumber}";
var eventType = (skillName, functionName) switch
{
("PM", "Readme") => nameof(GithubFlowEventType.ReadmeChainClosed),
("DevLead", "Plan") => nameof(GithubFlowEventType.DevPlanChainClosed),
("Developer", "Implement") => nameof(GithubFlowEventType.CodeChainClosed),
_ => nameof(GithubFlowEventType.NewAsk)
};
var data = new Dictionary<string, string>
{
{ "org", org },
{ "repo", repo },
{ "issueNumber", issueNumber.ToString() },
{ "parentNumber", parentNumber?.ToString()}
};
var evt = new Event
{
Type = eventType,
Subject = subject,
Data = data
};
await PublishEvent(evt);
}
private async Task HandleNewAsk(long issueNumber, long? parentNumber, string skillName, string functionName, string input, string org, string repo)
{
try
{
_logger.LogInformation("Handling new ask");
var subject = $"{org}-{repo}-{issueNumber}";
var eventType = (skillName, functionName) switch
{
("Do", "It") => nameof(GithubFlowEventType.NewAsk),
("PM", "Readme") => nameof(GithubFlowEventType.ReadmeRequested),
("DevLead", "Plan") => nameof(GithubFlowEventType.DevPlanRequested),
("Developer", "Implement") => nameof(GithubFlowEventType.CodeGenerationRequested),
_ => nameof(GithubFlowEventType.NewAsk)
};
var data = new Dictionary<string, string>
{
{ "org", org },
{ "repo", repo },
{ "issueNumber", issueNumber.ToString() },
{ "parentNumber", parentNumber?.ToString()},
{ "input" , input}
};
var evt = new Event
{
Type = eventType,
Subject = subject,
Data = data
};
await PublishEvent(evt);
}
catch (Exception ex)
{
_logger.LogError(ex, "Handling new ask");
throw;
}
}
private async Task PublishEvent(Event evt)
{
var metadata = new Dictionary<string, string>() {
{ "cloudevent.Type", evt.Type },
{ "cloudevent.Subject", evt.Subject },
{ "cloudevent.id", Guid.NewGuid().ToString()}
};
await _daprClient.PublishEventAsync(Consts.PubSub, Consts.MainTopic, evt, metadata);
}
}

View File

@ -0,0 +1,45 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Information",
"Orleans.Streams": "Information"
}
},
"ApplicationInsights": {
"ConnectionString": ""
},
"AllowedHosts": "*",
"SANDBOX_IMAGE" : "mcr.microsoft.com/dotnet/sdk:7.0",
"GithubOptions" : {
"AppKey": "",
"AppId": "",
"InstallationId": "",
"WebhookSecret": ""
},
"AzureOptions" : {
"SubscriptionId":"",
"Location":"",
"ContainerInstancesResourceGroup":"",
"FilesShareName":"",
"FilesAccountName":"",
"FilesAccountKey":"",
"SandboxImage" : "mcr.microsoft.com/dotnet/sdk:7.0",
"ManagedIdentity": ""
},
"OpenAIOptions" : {
"ServiceType":"AzureOpenAI",
"ServiceId":"gpt-4",
"DeploymentOrModelId":"gpt-4",
"EmbeddingDeploymentOrModelId":"text-embedding-ada-002",
"Endpoint":"",
"ApiKey":""
},
"QdrantOptions" : {
"Endpoint" : "http://qdrant:6333",
"VectorSize" : "1536"
},
"ServiceOptions" : {
"IngesterUrl" : "http://localhost:7071"
}
}

View File

@ -0,0 +1,45 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Information",
"Orleans.Streams": "Information"
}
},
"ApplicationInsights": {
"ConnectionString": ""
},
"AllowedHosts": "*",
"SANDBOX_IMAGE" : "mcr.microsoft.com/dotnet/sdk:7.0",
"GithubOptions" : {
"AppKey": "",
"AppId": "",
"InstallationId": "",
"WebhookSecret": ""
},
"AzureOptions" : {
"SubscriptionId":"",
"Location":"",
"ContainerInstancesResourceGroup":"",
"FilesShareName":"",
"FilesAccountName":"",
"FilesAccountKey":"",
"SandboxImage" : "mcr.microsoft.com/dotnet/sdk:7.0",
"ManagedIdentity": ""
},
"OpenAIOptions" : {
"ServiceType":"AzureOpenAI",
"ServiceId":"gpt-4",
"DeploymentOrModelId":"gpt-4",
"EmbeddingDeploymentOrModelId":"text-embedding-ada-002",
"Endpoint":"",
"ApiKey":""
},
"QdrantOptions" : {
"Endpoint" : "http://qdrant:6333",
"VectorSize" : "1536"
},
"ServiceOptions" : {
"IngesterUrl" : "http://localhost:7071"
}
}

View File

@ -0,0 +1,33 @@
using Microsoft.AI.Agents.Abstractions;
using Microsoft.AI.Agents.Orleans;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Memory;
using Orleans.Runtime;
namespace Microsoft.AI.DevTeam;
// The architect has Org+Repo scope and is holding the knowledge of the high level architecture of the project
[ImplicitStreamSubscription(Consts.MainNamespace)]
public class Architect : AiAgent<ArchitectState>
{
protected override string Namespace => Consts.MainNamespace;
public Architect([PersistentState("state", "messages")] IPersistentState<AgentState<ArchitectState>> state, ISemanticTextMemory memory, Kernel kernel)
: base(state, memory, kernel)
{
}
public override Task HandleEvent(Event item)
{
return Task.CompletedTask;
}
}
[GenerateSerializer]
public class ArchitectState
{
[Id(0)]
public string FilesTree { get; set; }
[Id(1)]
public string HighLevelArchitecture { get; set; }
}

View File

@ -0,0 +1,70 @@
using Microsoft.AI.Agents.Abstractions;
using Microsoft.AI.Agents.Orleans;
using Microsoft.AI.DevTeam.Events;
using Microsoft.AI.DevTeam.Extensions;
namespace Microsoft.AI.DevTeam;
[ImplicitStreamSubscription(Consts.MainNamespace)]
public class AzureGenie : Agent
{
protected override string Namespace => Consts.MainNamespace;
private readonly IManageAzure _azureService;
public AzureGenie(IManageAzure azureService)
{
_azureService = azureService;
}
public override async Task HandleEvent(Event item)
{
if (item?.Type is null)
{
throw new ArgumentNullException(nameof(item));
}
switch (item.Type)
{
case nameof(GithubFlowEventType.ReadmeCreated):
{
var context = item.ToGithubContext();
await Store(context.Org,context.Repo, context.ParentNumber.Value, context.IssueNumber, "readme", "md", "output", item.Data["readme"]);
await PublishEvent(Consts.MainNamespace, this.GetPrimaryKeyString(), new Event
{
Type = nameof(GithubFlowEventType.ReadmeStored),
Subject = context.Subject,
Data = context.ToData()
});
break;
}
case nameof(GithubFlowEventType.CodeCreated):
{
var context = item.ToGithubContext();
await Store(context.Org,context.Repo, context.ParentNumber.Value, context.IssueNumber, "run", "sh", "output", item.Data["code"]);
await RunInSandbox(context.Org,context.Repo, context.ParentNumber.Value, context.IssueNumber);
await PublishEvent(Consts.MainNamespace, this.GetPrimaryKeyString(), new Event
{
Type = nameof(GithubFlowEventType.SandboxRunCreated),
Subject = context.Subject,
Data = context.ToData()
});
break;
}
default:
break;
}
}
public async Task Store(string org, string repo, long parentIssueNumber, long issueNumber, string filename, string extension, string dir, string output)
{
await _azureService.Store(org, repo, parentIssueNumber, issueNumber, filename, extension, dir, output);
}
public async Task RunInSandbox(string org, string repo, long parentIssueNumber, long issueNumber)
{
await _azureService.RunInSandbox(org, repo, parentIssueNumber, issueNumber);
}
}

View File

@ -0,0 +1,99 @@
using Microsoft.AI.Agents.Abstractions;
using Microsoft.AI.Agents.Orleans;
using Microsoft.AI.DevTeam.Events;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Memory;
using Orleans.Runtime;
namespace Microsoft.AI.DevTeam;
[ImplicitStreamSubscription(Consts.MainNamespace)]
public class Dev : AiAgent<DeveloperState>, IDevelopApps
{
protected override string Namespace => Consts.MainNamespace;
private readonly ILogger<Dev> _logger;
public Dev([PersistentState("state", "messages")] IPersistentState<AgentState<DeveloperState>> state, Kernel kernel, ISemanticTextMemory memory, ILogger<Dev> logger)
: base(state, memory, kernel)
{
_logger = logger;
}
public async override Task HandleEvent(Event item)
{
switch (item.Type)
{
case nameof(GithubFlowEventType.CodeGenerationRequested):
{
var context = item.ToGithubContext();
var code = await GenerateCode(item.Data["input"]);
var data = context.ToData();
data["result"] = code;
await PublishEvent(Consts.MainNamespace, this.GetPrimaryKeyString(), new Event
{
Type = nameof(GithubFlowEventType.CodeGenerated),
Subject = context.Subject,
Data = data
});
}
break;
case nameof(GithubFlowEventType.CodeChainClosed):
{
var context = item.ToGithubContext();
var lastCode = _state.State.History.Last().Message;
var data = context.ToData();
data["code"] = lastCode;
await PublishEvent(Consts.MainNamespace, this.GetPrimaryKeyString(), new Event
{
Type = nameof(GithubFlowEventType.CodeCreated),
Subject = context.Subject,
Data = data
});
}
break;
default:
break;
}
}
public async Task<string> GenerateCode(string ask)
{
try
{
// TODO: ask the architect for the high level architecture as well as the files structure of the project
var context = new KernelArguments { ["input"] = AppendChatHistory(ask) };
var instruction = "Consider the following architectural guidelines:!waf!";
var enhancedContext = await AddKnowledge(instruction, "waf", context);
return await CallFunction(DeveloperSkills.Implement, enhancedContext);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error generating code");
return default;
}
}
}
[GenerateSerializer]
public class DeveloperState
{
[Id(0)]
public string Understanding { get; set; }
}
public interface IDevelopApps
{
public Task<string> GenerateCode(string ask);
}
[GenerateSerializer]
public class UnderstandingResult
{
[Id(0)]
public string NewUnderstanding { get; set; }
[Id(1)]
public string Explanation { get; set; }
}

View File

@ -0,0 +1,56 @@
namespace Microsoft.AI.DevTeam;
public static class DeveloperSkills {
public static string Implement = """
You are a Developer for an application.
Please output the code required to accomplish the task assigned to you below and wrap it in a bash script that creates the files.
Do not use any IDE commands and do not build and run the code.
Make specific choices about implementation. Do not offer a range of options.
Use comments in the code to describe the intent. Do not include other text other than code and code comments.
Input: {{$input}}
{{$waf}}
""";
public static string Improve = """
You are a Developer for an application. Your job is to imrove the code that you are given in the input below.
Please output a new version of code that fixes any problems with this version.
If there is an error message in the input you should fix that error in the code.
Wrap the code output up in a bash script that creates the necessary files by overwriting any previous files.
Do not use any IDE commands and do not build and run the code.
Make specific choices about implementation. Do not offer a range of options.
Use comments in the code to describe the intent. Do not include other text other than code and code comments.
Input: {{$input}}
{{$waf}}
""";
public static string Explain = """
You are an experienced software developer, with strong experience in Azure and Microsoft technologies.
Extract the key features and capabilities of the code file below, with the intent to build an understanding of an entire code repository.
You can include references or documentation links in your explanation. Also where appropriate please output a list of keywords to describe the code or its capabilities.
Example:
Keywords: Azure, networking, security, authentication
===code===
{{$input}}
===end-code===
Only include the points in a bullet point format and DON'T add anything outside of the bulleted list.
Be short and concise.
If the code's purpose is not clear output an error:
Error: The model could not determine the purpose of the code.
""";
public static string ConsolidateUnderstanding = """
You are an experienced software developer, with strong experience in Azure and Microsoft technologies.
You are trying to build an understanding of the codebase from code files. This is the current understanding of the project:
===current-understanding===
{{$input}}
===end-current-understanding===
and this is the new information that surfaced
===new-understanding===
{{$newUnderstanding}}
===end-new-understanding===
Your job is to update your current understanding with the new information.
Only include the points in a bullet point format and DON'T add anything outside of the bulleted list.
Be short and concise.
""";
}

View File

@ -0,0 +1,51 @@
namespace Microsoft.AI.DevTeam;
public static class DevLeadSkills {
public static string Plan = """
You are a Dev Lead for an application team, building the application described below.
Please break down the steps and modules required to develop the complete application, describe each step in detail.
Make prescriptive architecture, language, and frameowrk choices, do not provide a range of choices.
For each step or module then break down the steps or subtasks required to complete that step or module.
For each subtask write an LLM prompt that would be used to tell a model to write the coee that will accomplish that subtask. If the subtask involves taking action/running commands tell the model to write the script that will run those commands.
In each LLM prompt restrict the model from outputting other text that is not in the form of code or code comments.
Please output a JSON array data structure, in the precise schema shown below, with a list of steps and a description of each step, and the steps or subtasks that each requires, and the LLM prompts for each subtask.
Example:
{
"steps": [
{
"step": "1",
"description": "This is the first step",
"subtasks": [
{
"subtask": "Subtask 1",
"description": "This is the first subtask",
"prompt": "Write the code to do the first subtask"
},
{
"subtask": "Subtask 2",
"description": "This is the second subtask",
"prompt": "Write the code to do the second subtask"
}
]
}
]
}
Do not output any other text.
Do not wrap the JSON in any other text, output the JSON format described above, making sure it's a valid JSON.
Input: {{$input}}
{{$waf}}
""";
public static string Explain = """
You are a Dev Lead.
Please explain the code that is in the input below. You can include references or documentation links in your explanation.
Also where appropriate please output a list of keywords to describe the code or its capabilities.
example:
Keywords: Azure, networking, security, authentication
If the code's purpose is not clear output an error:
Error: The model could not determine the purpose of the code.
--
Input: {{$input}}
""";
}

View File

@ -0,0 +1,120 @@
using Microsoft.AI.Agents.Abstractions;
using Microsoft.AI.Agents.Orleans;
using Microsoft.AI.DevTeam.Events;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Connectors.OpenAI;
using Microsoft.SemanticKernel.Memory;
using Orleans.Runtime;
namespace Microsoft.AI.DevTeam;
[ImplicitStreamSubscription(Consts.MainNamespace)]
public class DeveloperLead : AiAgent<DeveloperLeadState>, ILeadDevelopers
{
protected override string Namespace => Consts.MainNamespace;
private readonly ILogger<DeveloperLead> _logger;
public DeveloperLead([PersistentState("state", "messages")] IPersistentState<AgentState<DeveloperLeadState>> state, Kernel kernel, ISemanticTextMemory memory, ILogger<DeveloperLead> logger)
: base(state, memory, kernel)
{
_logger = logger;
}
public async override Task HandleEvent(Event item)
{
switch (item.Type)
{
case nameof(GithubFlowEventType.DevPlanRequested):
{
var context = item.ToGithubContext();
var plan = await CreatePlan(item.Data["input"]);
var data = context.ToData();
data["result"] = plan;
await PublishEvent(Consts.MainNamespace, this.GetPrimaryKeyString(), new Event
{
Type = nameof(GithubFlowEventType.DevPlanGenerated),
Subject = context.Subject,
Data = data
});
}
break;
case nameof(GithubFlowEventType.DevPlanChainClosed):
{
var context = item.ToGithubContext();
var latestPlan = _state.State.History.Last().Message;
var data = context.ToData();
data["plan"] = latestPlan;
await PublishEvent(Consts.MainNamespace, this.GetPrimaryKeyString(), new Event
{
Type = nameof(GithubFlowEventType.DevPlanCreated),
Subject = context.Subject,
Data = data
});
}
break;
default:
break;
}
}
public async Task<string> CreatePlan(string ask)
{
try
{
// TODO: Ask the architect for the existing high level architecture
// as well as the file structure
var context = new KernelArguments { ["input"] = AppendChatHistory(ask) };
var instruction = "Consider the following architectural guidelines:!waf!";
var enhancedContext = await AddKnowledge(instruction, "waf", context);
var settings = new OpenAIPromptExecutionSettings{
ResponseFormat = "json_object",
MaxTokens = 4096,
Temperature = 0.8,
TopP = 1
};
return await CallFunction(DevLeadSkills.Plan, enhancedContext, settings);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error creating development plan");
return default;
}
}
}
public interface ILeadDevelopers
{
public Task<string> CreatePlan(string ask);
}
[GenerateSerializer]
public class DevLeadPlanResponse
{
[Id(0)]
public List<Step> steps { get; set; }
}
[GenerateSerializer]
public class Step
{
[Id(0)]
public string description { get; set; }
[Id(1)]
public string step { get; set; }
[Id(2)]
public List<Subtask> subtasks { get; set; }
}
[GenerateSerializer]
public class Subtask
{
[Id(0)]
public string subtask { get; set; }
[Id(1)]
public string prompt { get; set; }
}
public class DeveloperLeadState
{
public string Plan { get; set; }
}

View File

@ -0,0 +1,102 @@
using System;
using System.Text.Json;
using Microsoft.AI.Agents.Abstractions;
using Microsoft.AI.Agents.Orleans;
using Microsoft.AI.DevTeam.Events;
namespace Microsoft.AI.DevTeam;
[ImplicitStreamSubscription(Consts.MainNamespace)]
public class Hubber : Agent
{
protected override string Namespace => Consts.MainNamespace;
private readonly IManageGithub _ghService;
public Hubber(IManageGithub ghService)
{
_ghService = ghService;
}
public override async Task HandleEvent(Event item)
{
ArgumentNullException.ThrowIfNull(item);
ArgumentNullException.ThrowIfNull(item.Data);
switch (item.Type)
{
case nameof(GithubFlowEventType.NewAsk):
{
var context = item.ToGithubContext();
var pmIssue = await CreateIssue(context.Org, context.Repo , item.Data["input"], "PM.Readme", context.IssueNumber);
var devLeadIssue = await CreateIssue(context.Org, context.Repo , item.Data["input"], "DevLead.Plan", context.IssueNumber);
await PostComment(context.Org, context.Repo, context.IssueNumber, $" - #{pmIssue} - tracks PM.Readme");
await PostComment(context.Org, context.Repo, context.IssueNumber, $" - #{devLeadIssue} - tracks DevLead.Plan");
await CreateBranch(context.Org, context.Repo, $"sk-{context.IssueNumber}");
}
break;
case nameof(GithubFlowEventType.ReadmeGenerated):
case nameof(GithubFlowEventType.DevPlanGenerated):
case nameof(GithubFlowEventType.CodeGenerated):
{
var context = item.ToGithubContext();
var result = item.Data["result"];
var contents = string.IsNullOrEmpty(result)? "Sorry, I got tired, can you try again please? ": result;
await PostComment(context.Org,context.Repo, context.IssueNumber, contents);
}
break;
case nameof(GithubFlowEventType.DevPlanCreated):
{
var context = item.ToGithubContext();
var plan = JsonSerializer.Deserialize<DevLeadPlanResponse>(item.Data["plan"]);
var prompts = plan.steps.SelectMany(s => s.subtasks.Select(st => st.prompt));
foreach (var prompt in prompts)
{
var functionName = "Developer.Implement";
var issue = await CreateIssue(context.Org, context.Repo, prompt, functionName, context.ParentNumber.Value);
var commentBody = $" - #{issue} - tracks {functionName}";
await PostComment(context.Org, context.Repo, context.ParentNumber.Value, commentBody);
}
}
break;
case nameof(GithubFlowEventType.ReadmeStored):
{
var context = item.ToGithubContext();
var branch = $"sk-{context.ParentNumber}";
await CommitToBranch(context.Org, context.Repo, context.ParentNumber.Value, context.IssueNumber, "output", branch);
await CreatePullRequest(context.Org, context.Repo, context.ParentNumber.Value, branch);
}
break;
case nameof(GithubFlowEventType.SandboxRunFinished):
{
var context = item.ToGithubContext();
var branch = $"sk-{context.ParentNumber}";
await CommitToBranch(context.Org, context.Repo, context.ParentNumber.Value, context.IssueNumber, "output", branch);
}
break;
default:
break;
}
}
public async Task<int> CreateIssue(string org, string repo, string input, string function, long parentNumber)
{
return await _ghService.CreateIssue(org, repo, input, function, parentNumber);
}
public async Task PostComment(string org, string repo, long issueNumber, string comment)
{
await _ghService.PostComment(org, repo, issueNumber, comment);
}
public async Task CreateBranch(string org, string repo, string branch)
{
await _ghService.CreateBranch(org, repo, branch);
}
public async Task CreatePullRequest(string org, string repo, long issueNumber, string branch)
{
await _ghService.CreatePR(org, repo, issueNumber, branch);
}
public async Task CommitToBranch(string org, string repo, long parentNumber, long issueNumber, string rootDir, string branch)
{
await _ghService.CommitToBranch(org, repo, parentNumber, issueNumber, rootDir, branch);
}
}

View File

@ -0,0 +1,34 @@
namespace Microsoft.AI.DevTeam;
public static class PMSkills
{
public static string BootstrapProject = """
Please write a bash script with the commands that would be required to generate applications as described in the following input.
You may add comments to the script and the generated output but do not add any other text except the bash script.
You may include commands to build the applications but do not run them.
Do not include any git commands.
Input: {{$input}}
{{$waf}}
""";
public static string Readme = """
You are a program manager on a software development team. You are working on an app described below.
Based on the input below, and any dialog or other context, please output a raw README.MD markdown file documenting the main features of the app and the architecture or code organization.
Do not describe how to create the application.
Write the README as if it were documenting the features and architecture of the application. You may include instructions for how to run the application.
Input: {{$input}}
{{$waf}}
""";
public static string Explain = """
You are a Product Manager.
Please explain the code that is in the input below. You can include references or documentation links in your explanation.
Also where appropriate please output a list of keywords to describe the code or its capabilities.
example:
Keywords: Azure, networking, security, authentication
If the code's purpose is not clear output an error:
Error: The model could not determine the purpose of the code.
--
Input: {{$input}}
""";
}

View File

@ -0,0 +1,86 @@
using Microsoft.AI.Agents.Abstractions;
using Microsoft.AI.Agents.Orleans;
using Microsoft.AI.DevTeam.Events;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Memory;
using Orleans.Runtime;
namespace Microsoft.AI.DevTeam;
[ImplicitStreamSubscription(Consts.MainNamespace)]
public class ProductManager : AiAgent<ProductManagerState>, IManageProducts
{
protected override string Namespace => Consts.MainNamespace;
private readonly ILogger<ProductManager> _logger;
public ProductManager([PersistentState("state", "messages")] IPersistentState<AgentState<ProductManagerState>> state, Kernel kernel, ISemanticTextMemory memory, ILogger<ProductManager> logger)
: base(state, memory, kernel)
{
_logger = logger;
}
public async override Task HandleEvent(Event item)
{
switch (item.Type)
{
case nameof(GithubFlowEventType.ReadmeRequested):
{
var context = item.ToGithubContext();
var readme = await CreateReadme(item.Data["input"]);
var data = context.ToData();
data["result"]=readme;
await PublishEvent(Consts.MainNamespace, this.GetPrimaryKeyString(), new Event {
Type = nameof(GithubFlowEventType.ReadmeGenerated),
Subject = context.Subject,
Data = data
});
}
break;
case nameof(GithubFlowEventType.ReadmeChainClosed):
{
var context = item.ToGithubContext();
var lastReadme = _state.State.History.Last().Message;
var data = context.ToData();
data["readme"] = lastReadme;
await PublishEvent(Consts.MainNamespace, this.GetPrimaryKeyString(), new Event {
Type = nameof(GithubFlowEventType.ReadmeCreated),
Subject = context.Subject,
Data = data
});
}
break;
default:
break;
}
}
public async Task<string> CreateReadme(string ask)
{
try
{
var context = new KernelArguments { ["input"] = AppendChatHistory(ask)};
var instruction = "Consider the following architectural guidelines:!waf!";
var enhancedContext = await AddKnowledge(instruction, "waf",context);
return await CallFunction(PMSkills.Readme, enhancedContext);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error creating readme");
return default;
}
}
}
public interface IManageProducts
{
public Task<string> CreateReadme(string ask);
}
[GenerateSerializer]
public class ProductManagerState
{
[Id(0)]
public string Capabilities { get; set; }
}

View File

@ -0,0 +1,108 @@
using Microsoft.AI.Agents.Abstractions;
using Microsoft.AI.Agents.Orleans;
using Microsoft.AI.DevTeam.Events;
using Orleans.Runtime;
using Orleans.Timers;
namespace Microsoft.AI.DevTeam;
[ImplicitStreamSubscription(Consts.MainNamespace)]
public class Sandbox : Agent, IRemindable
{
protected override string Namespace => Consts.MainNamespace;
private const string ReminderName = "SandboxRunReminder";
private readonly IManageAzure _azService;
private readonly IReminderRegistry _reminderRegistry;
private IGrainReminder? _reminder;
protected readonly IPersistentState<SandboxMetadata> _state;
public Sandbox([PersistentState("state", "messages")] IPersistentState<SandboxMetadata> state,
IReminderRegistry reminderRegistry, IManageAzure azService)
{
_reminderRegistry = reminderRegistry;
_azService = azService;
_state = state;
}
public override async Task HandleEvent(Event item)
{
switch (item.Type)
{
case nameof(GithubFlowEventType.SandboxRunCreated):
{
var context = item.ToGithubContext();
await ScheduleCommitSandboxRun(context.Org, context.Repo, context.ParentNumber.Value, context.IssueNumber);
break;
}
default:
break;
}
}
public async Task ScheduleCommitSandboxRun(string org, string repo, long parentIssueNumber, long issueNumber)
{
await StoreState(org, repo, parentIssueNumber, issueNumber);
_reminder = await _reminderRegistry.RegisterOrUpdateReminder(
callingGrainId: this.GetGrainId(),
reminderName: ReminderName,
dueTime: TimeSpan.Zero,
period: TimeSpan.FromMinutes(1));
}
async Task IRemindable.ReceiveReminder(string reminderName, TickStatus status)
{
if (!_state.State.IsCompleted)
{
var sandboxId = $"sk-sandbox-{_state.State.Org}-{_state.State.Repo}-{_state.State.ParentIssueNumber}-{_state.State.IssueNumber}".ToLower();
if (await _azService.IsSandboxCompleted(sandboxId))
{
await _azService.DeleteSandbox(sandboxId);
await PublishEvent(Consts.MainNamespace, this.GetPrimaryKeyString(), new Event
{
Type = nameof(GithubFlowEventType.SandboxRunFinished),
Data = new Dictionary<string, string> {
{ "org", _state.State.Org },
{ "repo", _state.State.Repo },
{ "issueNumber", _state.State.IssueNumber.ToString() },
{ "parentNumber", _state.State.ParentIssueNumber.ToString() }
}
});
await Cleanup();
}
}
else
{
await Cleanup();
}
}
private async Task StoreState(string org, string repo, long parentIssueNumber, long issueNumber)
{
_state.State.Org = org;
_state.State.Repo = repo;
_state.State.ParentIssueNumber = parentIssueNumber;
_state.State.IssueNumber = issueNumber;
_state.State.IsCompleted = false;
await _state.WriteStateAsync();
}
private async Task Cleanup()
{
_state.State.IsCompleted = true;
await _reminderRegistry.UnregisterReminder(
this.GetGrainId(), _reminder);
await _state.WriteStateAsync();
}
}
public class SandboxMetadata
{
public string Org { get; set; }
public string Repo { get; set; }
public long ParentIssueNumber { get; set; }
public long IssueNumber { get; set; }
public bool IsCompleted { get; set; }
}

View File

@ -0,0 +1,23 @@
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
WORKDIR /app
EXPOSE 5274
EXPOSE 11111
EXPOSE 30000
ENV ASPNETCORE_URLS=http://+:5274
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
ARG configuration=Release
COPY . .
RUN dotnet restore "samples/gh-flow/src/Microsoft.AI.DevTeam/Microsoft.AI.DevTeam.csproj"
WORKDIR "samples/gh-flow/src/Microsoft.AI.DevTeam"
RUN dotnet build "Microsoft.AI.DevTeam.csproj" -c $configuration -o /app/build
FROM build AS publish
ARG configuration=Release
RUN dotnet publish "Microsoft.AI.DevTeam.csproj" -c $configuration -o /app/publish
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "Microsoft.AI.DevTeam.dll"]

View File

@ -0,0 +1,66 @@
using Microsoft.AI.Agents.Abstractions;
using Microsoft.AI.DevTeam.Extensions;
using System.Globalization;
namespace Microsoft.AI.DevTeam.Events
{
public enum GithubFlowEventType
{
NewAsk,
ReadmeChainClosed,
CodeChainClosed,
CodeGenerationRequested,
DevPlanRequested,
ReadmeGenerated,
DevPlanGenerated,
CodeGenerated,
DevPlanChainClosed,
ReadmeRequested,
ReadmeStored,
SandboxRunFinished,
ReadmeCreated,
CodeCreated,
DevPlanCreated,
SandboxRunCreated
}
public static class EventExtensions
{
public static GithubContext ToGithubContext(this Event evt)
{
ArgumentNullException.ThrowIfNull(evt);
return new GithubContext
{
Org = evt.Data["org"],
Repo = evt.Data["repo"],
IssueNumber = evt.Data.TryParseLong("issueNumber"),
ParentNumber = evt.Data.TryParseLong("parentNumber")
};
}
public static Dictionary<string, string> ToData(this GithubContext context)
{
ArgumentNullException.ThrowIfNull(context);
return new Dictionary<string, string> {
{ "org", context.Org },
{ "repo", context.Repo },
{ "issueNumber", $"{context.IssueNumber}" },
{ "parentNumber", context.ParentNumber.HasValue ? Convert.ToString(context.ParentNumber, CultureInfo.InvariantCulture) : string.Empty }
};
}
}
public class GithubContext
{
public string Org { get; set; }
public string Repo { get; set; }
public long IssueNumber { get; set; }
public long? ParentNumber { get; set; }
public string Subject => $"{Org}/{Repo}/{IssueNumber}";
}
}

View File

@ -0,0 +1,16 @@
namespace Microsoft.AI.DevTeam.Extensions
{
public static class ParseExtensions
{
public static long TryParseLong(this Dictionary<string, string> data, string key)
{
ArgumentNullException.ThrowIfNull(data);
if (data.TryGetValue(key, out string? value) && !string.IsNullOrEmpty(value) && long.TryParse(value, out var result))
{
return result;
}
return default;
}
}
}

View File

@ -0,0 +1,49 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<ServerGarbageCollection>true</ServerGarbageCollection>
<ConcurrentGarbageCollection>true</ConcurrentGarbageCollection>
<UserSecretsId>c073c86e-8483-4956-942f-331fd09172d4</UserSecretsId>
<AnalysisMode>All</AnalysisMode>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Octokit.Webhooks.AspNetCore" Version="2.0.3" />
<PackageReference Include="Octokit" Version="10.0.0" />
<PackageReference Include="Microsoft.SemanticKernel" Version="1.10.0" />
<PackageReference Include="Microsoft.SemanticKernel.Connectors.Qdrant" Version="1.10.0-alpha" />
<PackageReference Include="Microsoft.SemanticKernel.Plugins.Memory" Version="1.10.0-alpha" />
<PackageReference Include="Microsoft.Orleans.Server" Version="8.1.0" />
<PackageReference Include="Microsoft.Orleans.Sdk" Version="8.1.0" />
<PackageReference Include="Microsoft.Orleans.Runtime" Version="8.1.0" />
<PackageReference Include="Microsoft.Orleans.Persistence.Cosmos" Version="8.1.0" />
<PackageReference Include="Microsoft.Orleans.Clustering.Cosmos" Version="8.1.0" />
<PackageReference Include="Microsoft.Orleans.Reminders.Cosmos" Version="8.1.0" />
<PackageReference Include="Microsoft.Orleans.Streaming.EventHubs" Version="8.1.0" />
<PackageReference Include="Microsoft.Orleans.Reminders" Version="8.1.0" />
<PackageReference Include="Microsoft.Orleans.Streaming" Version="8.1.0" />
<PackageReference Include="OrleansDashboard" Version="8.0.0" />
<PackageReference Include="Microsoft.ApplicationInsights.AspNetCore" Version="2.21.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
<PackageReference Include="Microsoft.Extensions.Azure" Version="1.7.2" />
<PackageReference Include="Microsoft.Extensions.Http.Resilience" Version="8.0.0" />
<PackageReference Include="Azure.ResourceManager.ContainerInstance" Version="1.2.0" />
<PackageReference Include="Azure.Storage.Files.Shares" Version="12.16.0-beta.1" />
<PackageReference Include="Azure.Data.Tables" Version="12.8.1" />
<PackageReference Include="Azure.Identity" Version="1.11.0-beta.1" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="7.4.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\..\csharp\src\Microsoft.AI.Agents.Orleans\Microsoft.AI.Agents.Orleans.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,23 @@
using System.ComponentModel.DataAnnotations;
namespace Microsoft.AI.DevTeam;
public class AzureOptions
{
[Required]
public string SubscriptionId { get; set; }
[Required]
public string Location { get; set; }
[Required]
public string ContainerInstancesResourceGroup { get; set; }
[Required]
public string FilesShareName { get; set; }
[Required]
public string FilesAccountName { get; set; }
[Required]
public string FilesAccountKey { get; set; }
[Required]
public string SandboxImage { get; set; }
public string ManagedIdentity { get; set; }
public string CosmosConnectionString { get; set; }
}

Some files were not shown because too many files have changed in this diff Show More