mirror of https://github.com/microsoft/autogen.git
Merge project-oagents into agnext
This commit is contained in:
commit
c5cec3afbd
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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"
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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.");
|
||||
}
|
||||
}
|
|
@ -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!";
|
||||
|
||||
}
|
|
@ -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>
|
|
@ -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>
|
|
@ -0,0 +1,2 @@
|
|||
@namespace WorkflowsApp.Pages
|
||||
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
|
|
@ -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();
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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.
|
|
@ -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
|
||||
};
|
||||
}
|
|
@ -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
|
||||
};
|
||||
}
|
|
@ -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
|
||||
};
|
||||
}
|
|
@ -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}")
|
||||
};
|
||||
}
|
|
@ -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>
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 team’s 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];
|
||||
```
|
|
@ -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: ../../../../
|
|
@ -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: ""
|
|
@ -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"
|
|
@ -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 |
|
@ -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-"
|
||||
}
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
}
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
@ -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
|
||||
}
|
|
@ -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}'
|
|
@ -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
|
|
@ -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
|
|
@ -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}' : ''
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
}
|
|
@ -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
|
|
@ -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
|
|
@ -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}"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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; }
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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; }
|
||||
}
|
|
@ -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.
|
||||
""";
|
||||
}
|
|
@ -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}}
|
||||
""";
|
||||
}
|
|
@ -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; }
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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}}
|
||||
""";
|
||||
}
|
|
@ -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; }
|
||||
}
|
|
@ -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; }
|
||||
}
|
|
@ -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"]
|
|
@ -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; }
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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; }
|
||||
}
|
|
@ -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";
|
||||
}
|
|
@ -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; }
|
||||
}
|
|
@ -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; }
|
||||
}
|
|
@ -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; }
|
||||
}
|
|
@ -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; }
|
||||
}
|
|
@ -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();
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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; }
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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; }
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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; }
|
||||
}
|
|
@ -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.
|
||||
""";
|
||||
}
|
|
@ -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}}
|
||||
""";
|
||||
}
|
|
@ -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; }
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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}}
|
||||
""";
|
||||
}
|
|
@ -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; }
|
||||
}
|
|
@ -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; }
|
||||
}
|
|
@ -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"]
|
|
@ -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}";
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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
Loading…
Reference in New Issue