Add service to enable github issues workflow (#1)

* big bang gitub workflows

* add missing settings in local.settings.json

* config refactor

* fix devlead plan response

* swap cosmos to table storage for metadata storage

* unify config via options

* azd-ify WIP

* add qdrant bicep WIP

* working azd provision setup

* consolidate SK version in projects

* replace localhost :)

* add fqdn to options

* httpclient fixes

* add managed identity to the function and assign contrib role

* qdrant endpoint setting

* add container instances cleanup code + wait on termination to upload to Github

* formatting fixes

* add tables in bicep

* local getting started WIP

* add azure setup instructions

* add the load-waf bits

* docs WIP

---------

Co-authored-by: Kosta Petan <Kosta.Petan@microsoft.com>
This commit is contained in:
Kosta Petan
2023-08-28 20:57:56 +02:00
committed by GitHub
parent 69a203ba07
commit d6b917faf4
80 changed files with 3712 additions and 469 deletions

View File

@@ -1,5 +1,7 @@
FROM mcr.microsoft.com/devcontainers/dotnet:0-7.0
# Install the xz-utils package
RUN apt-get update && apt-get install -y xz-utils nodejs
RUN apt-get update && apt-get install -y xz-utils nodejs npm
RUN curl -fsSL https://aka.ms/install-azd.sh | bash
RUN curl -fsSL https://aka.ms/install-azd.sh | bash
RUN npm i -g azure-functions-core-tools@4 --unsafe-perm true

View File

@@ -9,7 +9,8 @@
"features": {
"ghcr.io/devcontainers/features/azure-cli:1": {},
"ghcr.io/devcontainers/features/common-utils:2": {},
"ghcr.io/devcontainers/features/docker-in-docker:2": {}
"ghcr.io/devcontainers/features/docker-in-docker:2": {},
"ghcr.io/azure/azure-dev/azd:latest": {}
},
"postCreateCommand": "bash .devcontainer/startup.sh",
"hostRequirements": {
@@ -39,7 +40,13 @@
"ms-dotnettools.csdevkit",
"Azurite.azurite",
"ms-dotnettools.csharp",
"ms-semantic-kernel.semantic-kernel"
"ms-semantic-kernel.semantic-kernel",
"GitHub.copilot-chat",
"GitHub.vscode-github-actions",
"ms-azuretools.azure-dev",
"ms-azuretools.vscode-azurefunctions",
"ms-azuretools.vscode-bicep",
"ms-dotnettools.vscode-dotnet-runtime"
]
}
}

9
.gitignore vendored
View File

@@ -482,7 +482,9 @@ $RECYCLE.BIN/
# Vim temporary swap files
*.swp
__azurite**
__blob**
__queue**
# SQLite workflows DB
elsa.sqlite.*
@@ -490,4 +492,7 @@ elsa.sqlite.*
.env
# ignore local elsa-core src
elsa-core/
elsa-core/
sk-azfunc-server/local.settings.json
.azure
temp

8
azure.yaml Normal file
View File

@@ -0,0 +1,8 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json
name: sk-dev-team
services:
sk-func:
project: ./sk-azfunc-server
language: dotnet
host: function

View File

@@ -3,7 +3,7 @@ using System.Text.Json.Serialization;
public class Subtask
{
public string subtask { get; set; }
public string LLM_prompt { get; set; }
public string prompt { get; set; }
}
public class Step

View File

@@ -92,7 +92,7 @@ class Program
{
try
{
implementationResult = await CallFunction<string>(nameof(Developer), Developer.Implement, subtask.LLM_prompt, maxRetry);
implementationResult = await CallFunction<string>(nameof(Developer), Developer.Implement, subtask.prompt, maxRetry);
break;
}
catch (Exception ex)
@@ -137,7 +137,7 @@ class Program
.AddConsole()
.AddDebug();
});
var memoryStore = new QdrantMemoryStore(new QdrantVectorDbClient("http://qdrant", 1536, port: 6333));
var memoryStore = new QdrantMemoryStore(new QdrantVectorDbClient("http://qdrant:6333", 1536));
var embedingGeneration = new AzureTextEmbeddingGeneration(kernelSettings.EmbeddingDeploymentOrModelId, kernelSettings.Endpoint, kernelSettings.ApiKey);
var semanticTextMemory = new SemanticTextMemory(memoryStore, embedingGeneration);

View File

@@ -3,17 +3,11 @@ using Microsoft.SemanticKernel.SkillDefinition;
public class SandboxSkill
{
[SKFunction("Run a script in Alpine sandbox")]
[SKFunctionInput(Description = "The script to be executed")]
[SKFunctionName("RunInAlpine")]
public async Task<string> RunInAlpineAsync(string input)
{
return await RunInContainer(input, "alpine");
}
[SKFunction("Run a script in dotnet alpine sandbox")]
[SKFunctionInput(Description = "The script to be executed")]
[SKFunctionName("RunInDotnetAlpine")]
public async Task<string> RunInDotnetAlpineAsync(string input)
{
return await RunInContainer(input, "mcr.microsoft.com/dotnet/sdk:7.0");

View File

@@ -13,8 +13,8 @@
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="7.0.1" />
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
<PackageReference Include="Microsoft.SemanticKernel" Version="0.15.230609.2-preview" />
<PackageReference Include="Microsoft.SemanticKernel.Connectors.Memory.Qdrant" Version="0.15.230609.2-preview" />
<PackageReference Include="Microsoft.SemanticKernel" Version="0.18.230725.3-preview" />
<PackageReference Include="Microsoft.SemanticKernel.Connectors.Memory.Qdrant" Version="0.18.230725.3-preview" />
<PackageReference Include="Testcontainers" Version="3.2.0" />
<ProjectReference Include="..\skills\skills.csproj" />
</ItemGroup>

View File

@@ -1,27 +0,0 @@
using Microsoft.SemanticKernel;
internal static class KernelConfigExtensions
{
/// <summary>
/// Adds a text completion service to the list. It can be either an OpenAI or Azure OpenAI backend service.
/// </summary>
/// <param name="kernelConfig"></param>
/// <param name="kernelSettings"></param>
/// <exception cref="ArgumentException"></exception>
internal static void AddCompletionBackend(this KernelConfig kernelConfig, KernelSettings kernelSettings)
{
switch (kernelSettings.ServiceType.ToUpperInvariant())
{
case KernelSettings.AzureOpenAI:
kernelConfig.AddAzureChatCompletionService(kernelSettings.DeploymentOrModelId, kernelSettings.Endpoint, kernelSettings.ApiKey);
break;
case KernelSettings.OpenAI:
kernelConfig.AddOpenAITextCompletionService(modelId: kernelSettings.DeploymentOrModelId, apiKey: kernelSettings.ApiKey, orgId: kernelSettings.OrgId, serviceId: kernelSettings.ServiceId);
break;
default:
throw new ArgumentException($"Invalid service type value: {kernelSettings.ServiceType}");
}
}
}

View File

@@ -0,0 +1 @@
# Azure components

View File

@@ -0,0 +1,90 @@
## 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)
- [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).
- 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
- Add a dummy value for the webhook url, we'll come back to this setting
- 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 follows:
- PM.Readme
- PM.BootstrapProject
- Do.It
- DevLead.Plan
- Developer.Implement
Once you start adding your own skills, just remember to add the corresponding Label!
## How do I run this locally?
Codespaces are preset for this repo.
Create a codespace and once the codespace is created, make sure to fill in the `local.settings.json` file.
There is a `local.settings.template.json` you can copy and fill in, containing comments on the different config values.
Hit F5 and go to the Ports tab in your codespace, make sure you make the `:7071` port publically visible. [How to share port?](https://docs.github.com/en/codespaces/developing-in-codespaces/forwarding-ports-in-your-codespace?tool=vscode#sharing-a-port-1)
Copy the local address (it will look something like https://foo-bar-7071.preview.app.github.dev) 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)
## How do I deploy this to Azure?
This repo 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 and setup the OpenAI and GithubApp config.
```bash
azd env new dev
azd env set -e dev GH_APP_ID replace_with_gh_app_id
azd env set -e dev GH_APP_INST_ID replace_with_inst_id
azd env set -e dev GH_APP_KEY replace_with_gh_app_key
azd env set -e dev OAI_DEPLOYMENT_ID replace_with_deployment_id
azd env set -e dev OAI_EMBEDDING_ID replace_with_embedding_id
azd env set -e dev OAI_ENDPOINT replace_with_oai_endpoint
azd env set -e dev OAI_KEY replace_with_oai_key
azd env set -e dev OAI_SERVICE_ID replace_with_oai_service_id
azd env set -e dev OAI_SERVICE_TYPE AzureOpenAI
```
Now that we have all that setup, the only thing left to do is run
```
azd up -e dev
```
and wait for the azure components to be provisioned and the app deployed.
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 `util` folder, called `seed-memory`. We need to fill in the `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.

1
docs/github-flow.md Normal file
View File

@@ -0,0 +1 @@
![](/docs/images/github-sk-dev-team.png)

Binary file not shown.

After

Width:  |  Height:  |  Size: 302 KiB

11
infra/abbreviations.json Normal file
View File

@@ -0,0 +1,11 @@
{
"appManagedEnvironments": "cae-",
"containerRegistryRegistries": "cr",
"insightsComponents": "appi-",
"operationalInsightsWorkspaces": "log-",
"portalDashboards": "dash-",
"resourcesResourceGroups": "rg-",
"storageStorageAccounts": "st",
"webServerFarms": "plan-",
"webSitesFunctions": "func-"
}

45
infra/app/sk-func.bicep Normal file
View File

@@ -0,0 +1,45 @@
param name string
param location string = resourceGroup().location
param tags object = {}
param allowedOrigins array = []
param applicationInsightsName string = ''
param appServicePlanId string
@secure()
param appSettings object = {}
param serviceName string = 'sk-func'
param storageAccountName string
module api '../core/host/functions.bicep' = {
name: '${serviceName}-functions-dotnet-isolated-module'
params: {
name: name
location: location
tags: union(tags, { 'azd-service-name': serviceName })
allowedOrigins: allowedOrigins
alwaysOn: false
appSettings: appSettings
applicationInsightsName: applicationInsightsName
appServicePlanId: appServicePlanId
runtimeName: 'dotnet-isolated'
runtimeVersion: '7.0'
storageAccountName: storageAccountName
scmDoBuildDuringDeployment: false
managedIdentity: true
}
}
var contributorRole = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')
resource rgContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
name: guid(subscription().id, resourceGroup().id, contributorRole)
properties: {
roleDefinitionId: contributorRole
principalType: 'ServicePrincipal'
principalId: api.outputs.identityPrincipalId
}
}
output SERVICE_API_IDENTITY_PRINCIPAL_ID string = api.outputs.identityPrincipalId
output SERVICE_API_NAME string = api.outputs.name
output SERVICE_API_URI string = api.outputs.uri

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,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

165
infra/main.bicep Normal file
View File

@@ -0,0 +1,165 @@
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 apiServiceName string = ''
param applicationInsightsDashboardName string = ''
param applicationInsightsName string = ''
param appServicePlanName string = ''
param logAnalyticsName string = ''
param resourceGroupName string = ''
param storageAccountName string = ''
param containerAppsEnvironmentName string = ''
param containerRegistryName string = ''
var aciShare = 'acishare'
var qdrantShare = 'qdrantshare'
var metadataTable = 'Metadata'
var containerMetadataTable = 'ContainersMetadata'
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
]
tables: [
metadataTable
containerMetadataTable
]
}
}
// 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
}
}
// Create an App Service Plan to group applications under the same payment plan and SKU
module appServicePlan './core/host/appserviceplan.bicep' = {
name: 'appserviceplan'
scope: rg
params: {
name: !empty(appServicePlanName) ? appServicePlanName : '${abbrs.webServerFarms}${resourceToken}'
location: location
tags: tags
sku: {
name: 'Y1'
tier: 'Dynamic'
}
}
}
var appName = !empty(apiServiceName) ? apiServiceName : '${abbrs.webSitesFunctions}api-${resourceToken}'
// The application backend
module skfunc './app/sk-func.bicep' = {
name: 'skfunc'
scope: rg
params: {
name: appName
location: location
tags: tags
applicationInsightsName: monitoring.outputs.applicationInsightsName
appServicePlanId: appServicePlan.outputs.id
storageAccountName: storage.outputs.name
appSettings: {
SANDBOX_IMAGE: 'mcr.microsoft.com/dotnet/sdk:7.0'
AzureWebJobsFeatureFlags: 'EnableHttpProxying'
FUNCTIONS_FQDN: 'https://${appName}.azurewebsites.net'
'GithubOptions__AppKey': githubAppKey
'GithubOptions__AppId': githubAppId
'GithubOptions__InstallationId': githubAppInstallationId
'AzureOptions__SubscriptionId': subscription().subscriptionId
'AzureOptions__Location': location
'AzureOptions__ContainerInstancesResourceGroup': rg.name
'AzureOptions__FilesShareName': aciShare
'AzureOptions__FilesAccountName': storage.outputs.name
'OpenAIOptions__ServiceType': openAIServiceType
'OpenAIOptions__ServiceId': openAIServiceId
'OpenAIOptions__DeploymentOrModelId': openAIDeploymentId
'OpenAIOptions__EmbeddingDeploymentOrModelId': openAIEmbeddingId
'OpenAIOptions__Endpoint': openAIEndpoint
'OpenAIOptions__ApiKey': openAIKey
'QdrantOptions__Endpoint':'https://${qdrant.outputs.fqdn}'
'QdrantOptions__VectorSize':'1536'
}
}
}
// App outputs
output APPLICATIONINSIGHTS_CONNECTION_STRING string = monitoring.outputs.applicationInsightsConnectionString
output AZURE_LOCATION string = location
output AZURE_TENANT_ID string = tenant().tenantId

View File

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

View File

@@ -0,0 +1,76 @@
using Microsoft.AspNetCore.Http;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
using Microsoft.DurableTask.Client;
using Octokit;
namespace SK.DevTeam
{
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2007: Do not directly await a Task", Justification = "Durable functions")]
public class IssuesActivities
{
private readonly GithubService _ghService;
public IssuesActivities(GithubService githubService)
{
_ghService = githubService;
}
[Function(nameof(CreateIssue))]
public async Task<NewIssueResponse> CreateIssue([ActivityTrigger] NewIssueRequest request, FunctionContext executionContext)
{
var ghClient = await _ghService.GetGitHubClient();
var newIssue = new NewIssue($"{request.Function} chain for #{request.IssueRequest.Number}")
{
Body = request.IssueRequest.Input,
};
newIssue.Labels.Add($"{request.Skill}.{request.Function}");
var issue = await ghClient.Issue.Create(request.IssueRequest.Org, request.IssueRequest.Repo, newIssue);
var commentBody = $" - [ ] #{issue.Number} - tracks {request.Skill}.{request.Function}";
var comment = await ghClient.Issue.Comment.Create(request.IssueRequest.Org, request.IssueRequest.Repo, (int)request.IssueRequest.Number, commentBody);
return new NewIssueResponse
{
Number = issue.Number,
CommentId = comment.Id
};
}
[Function("CloseSubOrchestration")]
public async Task Close(
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "close")] HttpRequest req,
[DurableClient] DurableTaskClient client)
{
var request = await req.ReadFromJsonAsync<CloseIssueRequest>();
var ghClient = await _ghService.GetGitHubClient();
var comment = await ghClient.Issue.Comment.Get(request.Org, request.Repo, request.CommentId);
var updatedComment = comment.Body.Replace("[ ]", "[x]");
await ghClient.Issue.Comment.Update(request.Org, request.Repo, request.CommentId, updatedComment);
await client.RaiseEventAsync(request.InstanceId, SubIssueOrchestration.IssueClosed, true);
}
[Function(nameof(GetLastComment))]
public async Task<string> GetLastComment([ActivityTrigger] IssueOrchestrationRequest request, FunctionContext executionContext)
{
var ghClient = await _ghService.GetGitHubClient();
var icOptions = new IssueCommentRequest
{
Direction = SortDirection.Descending
};
var apiOptions = new ApiOptions
{
PageCount = 1,
PageSize = 1,
StartPage = 1
};
var comments = await ghClient.Issue.Comment.GetAllForIssue(request.Org, request.Repo, (int)request.Number, icOptions, apiOptions);
return comments.First().Body;
}
}
}

View File

@@ -0,0 +1,34 @@
using Azure.Data.Tables;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.Functions.Worker;
namespace SK.DevTeam
{
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2007: Do not directly await a Task", Justification = "Durable functions")]
public static class MetadataActivities
{
[Function(nameof(GetMetadata))]
public static async Task<IActionResult> GetMetadata(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "metadata/{key}")] HttpRequest req,
[TableInput("Metadata", Connection = "AzureWebJobsStorage")] TableClient client,
FunctionContext executionContext)
{
var key = req.RouteValues["key"].ToString();
var metadataResponse = await client.GetEntityAsync<IssueMetadata>(key, key);
var metadata = metadataResponse.Value;
return new OkObjectResult(metadata);
}
[Function(nameof(SaveMetadata))]
public static async Task<IssueMetadata> SaveMetadata(
[ActivityTrigger] IssueMetadata metadata,
[TableInput("Metadata", Connection = "AzureWebJobsStorage")] TableClient client,
FunctionContext executionContext)
{
await client.UpsertEntityAsync(metadata);
return metadata;
}
}
}

View File

@@ -0,0 +1,240 @@
using System.Text;
using Azure;
using Azure.Core;
using Azure.Data.Tables;
using Azure.Identity;
using Azure.ResourceManager;
using Azure.ResourceManager.ContainerInstance;
using Azure.ResourceManager.ContainerInstance.Models;
using Azure.ResourceManager.Resources;
using Azure.Storage.Files.Shares;
using Microsoft.AspNetCore.Http;
using Microsoft.Azure.Functions.Worker;
using Microsoft.DurableTask.Client;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Octokit;
using Octokit.Helpers;
namespace SK.DevTeam
{
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2007: Do not directly await a Task", Justification = "Durable functions")]
public class PullRequestActivities
{
private readonly AzureOptions _azSettings;
private readonly GithubService _ghService;
private readonly IHttpClientFactory _httpClientFactory;
private readonly ILogger<PullRequestActivities> logger;
public PullRequestActivities(IOptions<AzureOptions> azOptions, GithubService ghService, IHttpClientFactory httpClientFactory, ILogger<PullRequestActivities> logger)
{
_azSettings = azOptions.Value;
_ghService = ghService;
_httpClientFactory = httpClientFactory;
this.logger = logger;
}
[Function(nameof(SaveOutput))]
public async Task<bool> SaveOutput([ActivityTrigger] SaveOutputRequest request, FunctionContext executionContext)
{
var connectionString = $"DefaultEndpointsProtocol=https;AccountName={_azSettings.FilesAccountName};AccountKey={_azSettings.FilesAccountKey};EndpointSuffix=core.windows.net";
var parentDirName = $"{request.Directory}/{request.IssueOrchestrationId}";
var fileName = $"{request.FileName}.{request.Extension}";
var share = new ShareClient(connectionString, _azSettings.FilesShareName);
await share.CreateIfNotExistsAsync();
await share.GetDirectoryClient($"{request.Directory}").CreateIfNotExistsAsync();
var parentDir = share.GetDirectoryClient(parentDirName);
await parentDir.CreateIfNotExistsAsync();
var directory = parentDir.GetSubdirectoryClient(request.SubOrchestrationId);
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 output = request.Extension == "sh" ? request.Output.Replace("#!/bin/bash", cwdHack) : request.Output;
using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(output)))
{
await file.CreateAsync(stream.Length);
await file.UploadRangeAsync(
new HttpRange(0, stream.Length),
stream);
}
return true;
}
[Function(nameof(CreateBranch))]
public async Task<bool> CreateBranch([ActivityTrigger] GHNewBranch request, FunctionContext executionContext)
{
var ghClient = await _ghService.GetGitHubClient();
var repo = await ghClient.Repository.Get(request.Org, request.Repo);
await ghClient.Git.Reference.CreateBranch(request.Org, request.Repo, request.Branch, repo.DefaultBranch);
return true;
}
[Function(nameof(CreatePR))]
public async Task<bool> CreatePR([ActivityTrigger] GHNewBranch request, FunctionContext executionContext)
{
var ghClient = await _ghService.GetGitHubClient();
var repo = await ghClient.Repository.Get(request.Org, request.Repo);
await ghClient.PullRequest.Create(request.Org, request.Repo, new NewPullRequest($"New app #{request.Number}", request.Branch, repo.DefaultBranch));
return true;
}
[Function(nameof(RunInSandbox))]
public async Task<bool> RunInSandbox(
[ActivityTrigger] RunInSandboxRequest request,
[TableInput("ContainersMetadata", Connection = "AzureWebJobsStorage")] TableClient tableClient,
FunctionContext executionContext)
{
var client = new ArmClient(new DefaultAzureCredential());
var containerGroupName = $"sk-sandbox-{request.PrRequest.SubOrchestrationId}";
var containerName = $"sk-sandbox-{request.PrRequest.SubOrchestrationId}";
var image = Environment.GetEnvironmentVariable("SANDBOX_IMAGE", EnvironmentVariableTarget.Process);
var resourceGroupResourceId = ResourceGroupResource.CreateResourceIdentifier(_azSettings.SubscriptionId, _azSettings.ContainerInstancesResourceGroup);
var resourceGroupResource = client.GetResourceGroupResource(resourceGroupResourceId);
var scriptPath = $"/azfiles/output/{request.PrRequest.IssueOrchestrationId}/{request.PrRequest.SubOrchestrationId}/run.sh";
var collection = resourceGroupResource.GetContainerGroups();
var data = new ContainerGroupData(new AzureLocation(_azSettings.Location), new ContainerInstanceContainer[]
{
new ContainerInstanceContainer(containerName,image,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, containerGroupName, data);
var metadata = new ContainerInstanceMetadata
{
PartitionKey = containerGroupName,
RowKey = containerGroupName,
SubOrchestrationId = request.SanboxOrchestrationId,
Processed = false
};
await tableClient.UpsertEntityAsync(metadata);
return true;
}
[Function(nameof(CommitToGithub))]
public async Task<bool> CommitToGithub([ActivityTrigger] GHCommitRequest request, FunctionContext executionContext)
{
var connectionString = $"DefaultEndpointsProtocol=https;AccountName={_azSettings.FilesAccountName};AccountKey={_azSettings.FilesAccountKey};EndpointSuffix=core.windows.net";
var ghClient = await _ghService.GetGitHubClient();
var dirName = $"{request.Directory}/{request.IssueOrchestrationId}/{request.SubOrchestrationId}";
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(
request.Org, request.Repo, filePath,
new CreateFileRequest($"Commit message", value, request.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));
}
}
}
return true;
}
[Function(nameof(Terminated))]
public async Task<ContainerInstanceMetadata> Terminated(
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "container/{name}/terminate")] HttpRequest req,
[TableInput("ContainersMetadata", Connection = "AzureWebJobsStorage")] TableClient tableClient,
[DurableClient] DurableTaskClient client)
{
var containerGroupName = req.RouteValues["name"].ToString();
var metadataResponse = await tableClient.GetEntityAsync<ContainerInstanceMetadata>(containerGroupName, containerGroupName);
var metadata = metadataResponse.Value;
if (!metadata.Processed)
{
await client.RaiseEventAsync(metadata.SubOrchestrationId, SubIssueOrchestration.ContainerTerminated, true);
metadata.Processed = true;
await tableClient.UpdateEntityAsync(metadata, metadata.ETag, TableUpdateMode.Replace);
}
return metadata;
}
[Function(nameof(CleanContainers))]
public async Task CleanContainers(
[TimerTrigger("*/30 * * * * *")] TimerInfo myTimer,
FunctionContext executionContext)
{
var httpClient = _httpClientFactory.CreateClient("FunctionsClient");
var client = new ArmClient(new DefaultAzureCredential());
var resourceGroupResourceId = ResourceGroupResource.CreateResourceIdentifier(_azSettings.SubscriptionId, _azSettings.ContainerInstancesResourceGroup);
var resourceGroupResource = client.GetResourceGroupResource(resourceGroupResourceId);
var collection = resourceGroupResource.GetContainerGroups();
foreach (var cg in collection.GetAll())
{
var c = await cg.GetAsync();
if (c.Value.Data.ProvisioningState == "Succeeded"
&& c.Value.Data.Containers.First().InstanceView.CurrentState.State == "Terminated")
{
await cg.DeleteAsync(WaitUntil.Started);
await httpClient.PostAsync($"container/{cg.Data.Name}/terminate", default);
}
}
}
}
}

View File

@@ -1,75 +0,0 @@
using System.Net;
using System.Text.Json;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
using Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Attributes;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Orchestration;
using Models;
using Microsoft.SKDevTeam;
public class ExecuteFunctionEndpoint
{
private static readonly JsonSerializerOptions s_jsonOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
private readonly IKernel _kernel;
public ExecuteFunctionEndpoint(IKernel kernel)
{
this._kernel = kernel;
}
[Function("ExecuteFunction")]
[OpenApiOperation(operationId: "ExecuteFunction", tags: new[] { "ExecuteFunction" }, Description = "Execute the specified semantic function. Provide skill and function names, plus any variables the function requires.")]
[OpenApiParameter(name: "skillName", Description = "Name of the skill e.g., 'FunSkill'", Required = true)]
[OpenApiParameter(name: "functionName", Description = "Name of the function e.g., 'Excuses'", Required = true)]
[OpenApiRequestBody("application/json", typeof(ExecuteFunctionRequest), Description = "Variables to use when executing the specified function.", Required = true)]
[OpenApiResponseWithBody(statusCode: HttpStatusCode.OK, contentType: "application/json", bodyType: typeof(ExecuteFunctionResponse), Description = "Returns the response from the AI.")]
[OpenApiResponseWithBody(statusCode: HttpStatusCode.BadRequest, contentType: "application/json", bodyType: typeof(ErrorResponse), Description = "Returned if the request body is invalid.")]
[OpenApiResponseWithBody(statusCode: HttpStatusCode.NotFound, contentType: "application/json", bodyType: typeof(ErrorResponse), Description = "Returned if the semantic function could not be found.")]
public async Task<HttpResponseData> ExecuteFunctionAsync(
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "skills/{skillName}/functions/{functionName}")]
HttpRequestData requestData,
FunctionContext executionContext, string skillName, string functionName)
{
try
{
var functionRequest = await JsonSerializer.DeserializeAsync<ExecuteFunctionRequest>(requestData.Body, s_jsonOptions).ConfigureAwait(false);
if (functionRequest == null)
{
return await CreateResponseAsync(requestData, HttpStatusCode.BadRequest, new ErrorResponse() { Message = $"Invalid request body." }).ConfigureAwait(false);
}
var skillConfig = SemanticFunctionConfig.ForSkillAndFunction(skillName, functionName);
var function = _kernel.CreateSemanticFunction(skillConfig.PromptTemplate, skillConfig.Name, skillConfig.SkillName,
skillConfig.Description, skillConfig.MaxTokens, skillConfig.Temperature,
skillConfig.TopP, skillConfig.PPenalty, skillConfig.FPenalty);
var context = new ContextVariables();
foreach (var v in functionRequest.Variables)
{
context.Set(v.Key, v.Value);
}
var result = await this._kernel.RunAsync(context, function).ConfigureAwait(false);
return await CreateResponseAsync(requestData, HttpStatusCode.OK, new ExecuteFunctionResponse() { Response = result.ToString() }).ConfigureAwait(false);
}
catch (Exception ex)
{
// Log the contents of the request
var requestBody = await new StreamReader(requestData.Body).ReadToEndAsync();
Console.WriteLine($"Failed to deserialize request body: {requestBody}. Exception: {ex}");
return await CreateResponseAsync(requestData, HttpStatusCode.BadRequest, new ErrorResponse() { Message = $"Invalid request body." }).ConfigureAwait(false);
}
}
private static async Task<HttpResponseData> CreateResponseAsync(HttpRequestData requestData, HttpStatusCode statusCode, object responseBody)
{
var responseData = requestData.CreateResponse(statusCode);
await responseData.WriteAsJsonAsync(responseBody).ConfigureAwait(false);
return responseData;
}
}

View File

@@ -0,0 +1,10 @@
public class AddToPRRequest
{
public string Output { get; set; }
public string IssueOrchestrationId { get; set; }
public string SubOrchestrationId { get; set; }
public string PrSubOrchestrationId { get; set; }
public string Extension { get; set; }
public bool RunInSandbox { get; set; }
public IssueOrchestrationRequest Request { get; set; }
}

View File

@@ -0,0 +1,7 @@
public class CloseIssueRequest
{
public string InstanceId { get; set; }
public int CommentId { get; set; }
public string Org { get; set; }
public string Repo { get; set; }
}

View File

@@ -0,0 +1,12 @@
using Azure;
using Azure.Data.Tables;
public class ContainerInstanceMetadata : ITableEntity
{
public string PartitionKey { get; set; }
public string RowKey { get; set; }
public string SubOrchestrationId { get; set; }
public bool Processed { get; set; }
public DateTimeOffset? Timestamp { get; set; }
public ETag ETag { get; set; }
}

View File

@@ -0,0 +1,17 @@
public class Subtask
{
public string subtask { get; set; }
public string prompt { get; set; }
}
public class Step
{
public string description { get; set; }
public string step { get; set; }
public List<Subtask> subtasks { get; set; }
}
public class DevLeadPlanResponse
{
public List<Step> steps { get; set; }
}

View File

@@ -1,11 +0,0 @@
using System.Text.Json.Serialization;
using Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Attributes;
namespace Models;
internal class ErrorResponse
{
[JsonPropertyName("message")]
[OpenApiProperty(Description = "The error message.")]
public string Message { get; set; } = string.Empty;
}

View File

@@ -1,23 +0,0 @@
using System.Text.Json.Serialization;
using Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Attributes;
namespace Models;
#pragma warning disable CA1812
internal class ExecuteFunctionRequest
{
[JsonPropertyName("variables")]
[OpenApiProperty(Description = "The variables to pass to the semantic function.")]
public IEnumerable<ExecuteFunctionVariable> Variables { get; set; } = Enumerable.Empty<ExecuteFunctionVariable>();
}
internal class ExecuteFunctionVariable
{
[JsonPropertyName("key")]
[OpenApiProperty(Description = "The variable key.", Default = "input")]
public string Key { get; set; } = string.Empty;
[JsonPropertyName("value")]
[OpenApiProperty(Description = "The variable value.", Default = "Late for school")]
public string Value { get; set; } = string.Empty;
}

View File

@@ -1,11 +0,0 @@
using System.Text.Json.Serialization;
using Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Attributes;
namespace Models;
internal class ExecuteFunctionResponse
{
[JsonPropertyName("response")]
[OpenApiProperty(Description = "The response from the AI.")]
public string? Response { get; set; }
}

View File

@@ -0,0 +1,12 @@
namespace SK.DevTeam
{
public class GHCommitRequest
{
public object IssueOrchestrationId { get; set; }
public object SubOrchestrationId { get; set; }
public string Org { get; set; }
public string Repo { get; set; }
public object Directory { get; set; }
public string Branch { get; set; }
}
}

View File

@@ -0,0 +1,10 @@
namespace SK.DevTeam
{
public class GHNewBranch
{
public string Org { get; set; }
public string Repo { get; set; }
public string Branch { get; set; }
public object Number { get; set; }
}
}

View File

@@ -0,0 +1,19 @@
using Azure;
using Azure.Data.Tables;
public class IssueMetadata : ITableEntity
{
public long? Number { get; set; }
public int? CommentId { get; set; }
public string? InstanceId { get; set; }
public string? Id { get; set; }
public string? Org { get; set; }
public string? Repo { get; set; }
public string PartitionKey { get; set; }
public string RowKey { get; set; }
public DateTimeOffset? Timestamp { get; set; }
public ETag ETag { get; set; }
}

View File

@@ -0,0 +1,8 @@
public class IssueOrchestrationRequest
{
public string Org { get; set; }
public string Repo { get; set; }
public long Number { get; set; }
public string Input { get; set; }
public string Branch => $"sk-{Number}";
}

View File

@@ -0,0 +1,6 @@
public class NewIssueRequest
{
public IssueOrchestrationRequest IssueRequest { get; set; }
public string Skill { get; set; }
public string Function { get; set; }
}

View File

@@ -0,0 +1,5 @@
public class NewIssueResponse
{
public long Number { get; set; }
public int CommentId { get; set; }
}

View File

@@ -0,0 +1,6 @@
public class RunAndSaveRequest
{
public IssueOrchestrationRequest Request { get; set; }
public string InstanceId { get; set; }
}

View File

@@ -0,0 +1,8 @@
namespace SK.DevTeam
{
public class RunInSandboxRequest
{
public AddToPRRequest PrRequest { get; set; }
public string SanboxOrchestrationId { get; set; }
}
}

View File

@@ -0,0 +1,12 @@
namespace SK.DevTeam
{
public class SaveOutputRequest
{
public string IssueOrchestrationId { get; set; }
public string SubOrchestrationId { get; set; }
public string Output { get; set; }
public string Extension { get; set; }
public string Directory { get; set; }
public string FileName { get; set; }
}
}

View File

@@ -0,0 +1,9 @@
namespace SK.DevTeam
{
public class SkillRequest
{
public IssueOrchestrationRequest IssueRequest { get; set; }
public string Skill { get; set; }
public string Function { get; set; }
}
}

View File

@@ -0,0 +1,5 @@
public class SkillResponse<T>
{
public T Output { get; set; }
public string SuborchestrationId { get; set; }
}

View File

@@ -0,0 +1,75 @@
using System.Text.Json;
using Microsoft.AspNetCore.Http;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
using Microsoft.DurableTask;
using Microsoft.DurableTask.Client;
using Microsoft.Extensions.Logging;
using static SK.DevTeam.SubIssueOrchestration;
namespace SK.DevTeam
{
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2007: Do not directly await a Task", Justification = "Durable functions")]
public static class IssueOrchestration
{
[Function("IssueOrchestrationStart")]
public static async Task<string> HttpStart(
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "doit")] HttpRequest req,
[DurableClient] DurableTaskClient client,
FunctionContext executionContext)
{
ILogger logger = executionContext.GetLogger("IssueOrchestration_HttpStart");
var request = await req.ReadFromJsonAsync<IssueOrchestrationRequest>();
string instanceId = await client.ScheduleNewOrchestrationInstanceAsync(
nameof(IssueOrchestration), request);
logger.LogInformation("Started orchestration with ID = '{instanceId}'.", instanceId);
return "";
}
[Function(nameof(IssueOrchestration))]
public static async Task<List<string>> RunOrchestrator(
[OrchestrationTrigger] TaskOrchestrationContext context, IssueOrchestrationRequest request)
{
var logger = context.CreateReplaySafeLogger(nameof(IssueOrchestration));
var outputs = new List<string>();
var newGHBranchRequest = new GHNewBranch
{
Org = request.Org,
Repo = request.Repo,
Branch = request.Branch,
Number = request.Number
};
var newBranch = await context.CallActivityAsync<bool>(nameof(PullRequestActivities.CreateBranch), newGHBranchRequest);
var readmeTask = await context.CallSubOrchestratorAsync<bool>(nameof(ReadmeAndSave), new RunAndSaveRequest
{
Request = request,
InstanceId = context.InstanceId
});
var newPR = await context.CallActivityAsync<bool>(nameof(PullRequestActivities.CreatePR), newGHBranchRequest);
var planTask = await context.CallSubOrchestratorAsync<SkillResponse<string>>(nameof(CreatePlan), request);
var plan = JsonSerializer.Deserialize<DevLeadPlanResponse>(planTask.Output);
var implementationTasks = plan.steps.SelectMany(s => s.subtasks.Select(st =>
context.CallSubOrchestratorAsync<bool>(nameof(ImplementAndSave), new RunAndSaveRequest
{
Request = new IssueOrchestrationRequest
{
Number = request.Number,
Org = request.Org,
Repo = request.Repo,
Input = st.prompt,
},
InstanceId = context.InstanceId
})));
await Task.WhenAll(implementationTasks);
return outputs;
}
}
}

View File

@@ -0,0 +1,154 @@
using Microsoft.Azure.Functions.Worker;
using Microsoft.DurableTask;
using Microsoft.SKDevTeam;
namespace SK.DevTeam
{
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2007: Do not directly await a Task", Justification = "Durable functions")]
public static class SubIssueOrchestration
{
public static string IssueClosed = "IssueClosed";
public static string ContainerTerminated = "ContainerTerminated";
private static async Task<SkillResponse<string>> CallSkill(TaskOrchestrationContext context, SkillRequest request)
{
var newIssueResponse = await context.CallActivityAsync<NewIssueResponse>(nameof(IssuesActivities.CreateIssue), new NewIssueRequest
{
IssueRequest = request.IssueRequest,
Skill = request.Skill,
Function = request.Function
});
var metadata = await context.CallActivityAsync<IssueMetadata>(nameof(MetadataActivities.SaveMetadata), new IssueMetadata
{
Number = newIssueResponse.Number,
InstanceId = context.InstanceId,
Id = Guid.NewGuid().ToString(),
CommentId = newIssueResponse.CommentId,
Org = request.IssueRequest.Org,
Repo = request.IssueRequest.Repo,
PartitionKey = $"{request.IssueRequest.Org}{request.IssueRequest.Repo}{newIssueResponse.Number}",
RowKey = $"{request.IssueRequest.Org}{request.IssueRequest.Repo}{newIssueResponse.Number}",
Timestamp = DateTimeOffset.UtcNow
});
bool issueClosed = await context.WaitForExternalEvent<bool>(IssueClosed);
var lastComment = await context.CallActivityAsync<string>(nameof(IssuesActivities.GetLastComment), new IssueOrchestrationRequest
{
Org = request.IssueRequest.Org,
Repo = request.IssueRequest.Repo,
Number = newIssueResponse.Number
});
return new SkillResponse<string> { Output = lastComment, SuborchestrationId = context.InstanceId };
}
[Function(nameof(CreateReadme))]
public static async Task<SkillResponse<string>> CreateReadme(
[OrchestrationTrigger] TaskOrchestrationContext context, IssueOrchestrationRequest request)
{
return await CallSkill(context, new SkillRequest
{
IssueRequest = request,
Skill = nameof(PM),
Function = nameof(PM.Readme)
});
}
[Function(nameof(CreatePlan))]
public static async Task<SkillResponse<string>> CreatePlan(
[OrchestrationTrigger] TaskOrchestrationContext context, IssueOrchestrationRequest request)
{
return await CallSkill(context, new SkillRequest
{
IssueRequest = request,
Skill = nameof(DevLead),
Function = nameof(DevLead.Plan)
});
}
[Function(nameof(Implement))]
public static async Task<SkillResponse<string>> Implement(
[OrchestrationTrigger] TaskOrchestrationContext context, IssueOrchestrationRequest request)
{
return await CallSkill(context, new SkillRequest
{
IssueRequest = request,
Skill = nameof(Developer),
Function = nameof(Developer.Implement)
});
}
[Function(nameof(ImplementAndSave))]
public static async Task<bool> ImplementAndSave(
[OrchestrationTrigger] TaskOrchestrationContext context, RunAndSaveRequest request)
{
var implementResult = await context.CallSubOrchestratorAsync<SkillResponse<string>>(nameof(Implement), request.Request);
await context.CallSubOrchestratorAsync<string>(nameof(AddToPR), new AddToPRRequest
{
Output = implementResult.Output,
IssueOrchestrationId = request.InstanceId,
SubOrchestrationId = implementResult.SuborchestrationId,
Extension = "sh",
RunInSandbox = true,
Request = request.Request
});
return true;
}
[Function(nameof(ReadmeAndSave))]
public static async Task<bool> ReadmeAndSave(
[OrchestrationTrigger] TaskOrchestrationContext context, RunAndSaveRequest request)
{
var readmeResult = await context.CallSubOrchestratorAsync<SkillResponse<string>>(nameof(CreateReadme), request.Request);
context.CallSubOrchestratorAsync<string>(nameof(AddToPR), new AddToPRRequest
{
Output = readmeResult.Output,
IssueOrchestrationId = request.InstanceId,
SubOrchestrationId = readmeResult.SuborchestrationId,
Extension = "md",
RunInSandbox = false,
Request = request.Request
});
return true;
}
[Function(nameof(AddToPR))]
public static async Task<string> AddToPR(
[OrchestrationTrigger] TaskOrchestrationContext context, AddToPRRequest request)
{
var saveScriptResponse = await context.CallActivityAsync<bool>(nameof(PullRequestActivities.SaveOutput), new SaveOutputRequest
{
Output = request.Output,
IssueOrchestrationId = request.IssueOrchestrationId,
SubOrchestrationId = request.SubOrchestrationId,
Extension = request.Extension,
Directory = "output",
FileName = request.RunInSandbox ? "run" : "readme"
});
if (request.RunInSandbox)
{
var newRequest = new RunInSandboxRequest
{
PrRequest = request,
SanboxOrchestrationId = context.InstanceId
};
var runScriptResponse = await context.CallActivityAsync<bool>(nameof(PullRequestActivities.RunInSandbox), newRequest);
bool containerTerminated = await context.WaitForExternalEvent<bool>(ContainerTerminated);
}
// this is not ideal, as the script might be still running and there might be files that are not yet generated
var commitResponse = await context.CallActivityAsync<bool>(nameof(PullRequestActivities.CommitToGithub), new GHCommitRequest
{
IssueOrchestrationId = request.IssueOrchestrationId,
SubOrchestrationId = request.SubOrchestrationId,
Directory = "output",
Org = request.Request.Org,
Repo = request.Request.Repo,
Branch = request.Request.Branch
});
return default;
}
}
}

View File

@@ -1,13 +1,16 @@
using System.Text.Json;
using Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Abstractions;
using Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Configurations;
using Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Enums;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.OpenApi.Models;
using Microsoft.Extensions.Options;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Connectors.AI.OpenAI.TextEmbedding;
using Microsoft.SemanticKernel.Connectors.Memory.Qdrant;
using Microsoft.SemanticKernel.Memory;
using Octokit.Webhooks;
using Octokit.Webhooks.AzureFunctions;
namespace KernelHttpServer;
@@ -16,25 +19,67 @@ public static class Program
public static void Main()
{
var host = new HostBuilder()
.ConfigureFunctionsWorkerDefaults()
.ConfigureFunctionsWebApplication()
.ConfigureGitHubWebhooks()
.ConfigureAppConfiguration(configuration =>
{
var config = configuration.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("local.settings.json", optional: true, reloadOnChange: true);
.AddJsonFile("local.settings.json", optional: true, reloadOnChange: true)
.AddEnvironmentVariables();
var builtConfig = config.Build();
})
.ConfigureServices(services =>
{
services.AddSingleton<IOpenApiConfigurationOptions>(_ => s_apiConfigOptions);
services.AddTransient((provider) => CreateKernel(provider));
services.AddScoped<GithubService>();
services.AddScoped<WebhookEventProcessor, SKWebHookEventProcessor>();
services.AddOptions<GithubOptions>()
.Configure<IConfiguration>((settings, configuration) =>
{
configuration.GetSection("GithubOptions").Bind(settings);
});
services.AddOptions<AzureOptions>()
.Configure<IConfiguration>((settings, configuration) =>
{
configuration.GetSection("AzureOptions").Bind(settings);
});
services.AddOptions<OpenAIOptions>()
.Configure<IConfiguration>((settings, configuration) =>
{
configuration.GetSection("OpenAIOptions").Bind(settings);
});
services.AddOptions<QdrantOptions>()
.Configure<IConfiguration>((settings, configuration) =>
{
configuration.GetSection("QdrantOptions").Bind(settings);
});
services.AddApplicationInsightsTelemetryWorkerService();
services.ConfigureFunctionsApplicationInsights();
// return JSON with expected lowercase naming
services.Configure<JsonSerializerOptions>(options =>
{
options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
});
services.AddHttpClient("FunctionsClient", client =>
{
var fqdn = Environment.GetEnvironmentVariable("FUNCTIONS_FQDN", EnvironmentVariableTarget.Process);
client.BaseAddress = new Uri($"{fqdn}/api/");
});
})
.ConfigureLogging(logging =>
{
logging.Services.Configure<LoggerFilterOptions>(options =>
{
LoggerFilterRule defaultRule = options.Rules.FirstOrDefault(rule => rule.ProviderName
== "Microsoft.Extensions.Logging.ApplicationInsights.ApplicationInsightsLoggerProvider");
if (defaultRule is not null)
{
options.Rules.Remove(defaultRule);
}
});
})
.Build();
@@ -43,43 +88,27 @@ public static class Program
private static IKernel CreateKernel(IServiceProvider provider)
{
var kernelSettings = KernelSettings.LoadSettings();
var openAiConfig = provider.GetService<IOptions<OpenAIOptions>>().Value;
var qdrantConfig = provider.GetService<IOptions<QdrantOptions>>().Value;
var kernelConfig = new KernelConfig();
kernelConfig.AddCompletionBackend(kernelSettings);
using ILoggerFactory loggerFactory = LoggerFactory.Create(builder =>
{
builder
.SetMinimumLevel(kernelSettings.LogLevel ?? LogLevel.Warning)
.SetMinimumLevel(LogLevel.Debug)
.AddConsole()
.AddDebug();
});
return new KernelBuilder().WithLogger(loggerFactory.CreateLogger<IKernel>()).WithConfiguration(kernelConfig).Build();
}
var memoryStore = new QdrantMemoryStore(new QdrantVectorDbClient(qdrantConfig.Endpoint, qdrantConfig.VectorSize));
var embedingGeneration = new AzureTextEmbeddingGeneration(openAiConfig.EmbeddingDeploymentOrModelId, openAiConfig.Endpoint, openAiConfig.ApiKey);
var semanticTextMemory = new SemanticTextMemory(memoryStore, embedingGeneration);
private static readonly OpenApiConfigurationOptions s_apiConfigOptions = new()
{
Info = new OpenApiInfo()
{
Version = "1.0.0",
Title = "Semantic Kernel Azure Functions Starter",
Description = "Azure Functions starter application for the [Semantic Kernel](https://github.com/microsoft/semantic-kernel).",
Contact = new OpenApiContact()
{
Name = "Issues",
Url = new Uri("https://github.com/microsoft/semantic-kernel-starters/issues"),
},
License = new OpenApiLicense()
{
Name = "MIT",
Url = new Uri("https://github.com/microsoft/semantic-kernel-starters/blob/main/LICENSE"),
}
},
Servers = DefaultOpenApiConfigurationOptions.GetHostNames(),
OpenApiVersion = OpenApiVersionType.V2,
ForceHttps = false,
ForceHttp = false,
};
return new KernelBuilder()
.WithLogger(loggerFactory.CreateLogger<IKernel>())
.WithAzureChatCompletionService(openAiConfig.DeploymentOrModelId, openAiConfig.Endpoint, openAiConfig.ApiKey, true, openAiConfig.ServiceId, true)
.WithMemory(semanticTextMemory)
.WithConfiguration(kernelConfig).Build();
}
}

View File

@@ -1,81 +1,5 @@
# Semantic Kernel Azure Functions Starter
# Demo
The `sk-csharp-azure-functions` Azure Functions application demonstrates how to execute a semantic function.
## How do I get started?
## Prerequisites
- [.NET 6](https://dotnet.microsoft.com/download/dotnet/6.0) is required to run this starter.
- Install the recommended extensions
- [C#](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.csharp)
- [Semantic Kernel Tools](https://marketplace.visualstudio.com/items?itemName=ms-semantic-kernel.semantic-kernel)
## Configuring the starter
The starter can be configured by using either:
- Enter secrets at the command line with [.NET Secret Manager](#using-net-secret-manager)
- Enter secrets in [appsettings.json](#using-appsettingsjson)
For Debugging the console application alone, we suggest using .NET [Secret Manager](https://learn.microsoft.com/en-us/aspnet/core/security/app-secrets) to avoid the risk of leaking secrets into the repository, branches and pull requests.
### Using .NET [Secret Manager](https://learn.microsoft.com/en-us/aspnet/core/security/app-secrets)
Configure an OpenAI endpoint
```powershell
cd sk-csharp-azure-functions
dotnet user-secrets set "serviceType" "OpenAI"
dotnet user-secrets set "serviceId" "text-davinci-003"
dotnet user-secrets set "deploymentOrModelId" "text-davinci-003"
dotnet user-secrets set "apiKey" "... your OpenAI key ..."
```
Configure an Azure OpenAI endpoint
```powershell
cd sk-csharp-azure-functions
dotnet user-secrets set "serviceType" "AzureOpenAI"
dotnet user-secrets set "serviceId" "text-davinci-003"
dotnet user-secrets set "deploymentOrModelId" "text-davinci-003"
dotnet user-secrets set "endpoint" "https:// ... your endpoint ... .openai.azure.com/"
dotnet user-secrets set "apiKey" "... your Azure OpenAI key ..."
```
Configure the Semantic Kernel logging level
```powershell
dotnet user-secrets set "LogLevel" 0
```
Log levels:
- 0 = Trace
- 1 = Debug
- 2 = Information
- 3 = Warning
- 4 = Error
- 5 = Critical
- 6 = None
### Using appsettings.json
Configure an OpenAI endpoint
1. Copy [settings.json.openai-example](./config/appsettings.json.openai-example) to `./config/appsettings.json`
1. Edit the file to add your OpenAI endpoint configuration
Configure an Azure OpenAI endpoint
1. Copy [settings.json.azure-example](./config/appsettings.json.azure-example) to `./config/appsettings.json`
1. Edit the file to add your Azure OpenAI endpoint configuration
## Running the starter
To run the Azure Functions application just hit `F5`.
To build and run the Azure Functions application from a terminal use the following commands:
```powershell
dotnet build
func start --csharp
```
Check - [Getting started](../docs/github-flow-getting-started.md)

View File

@@ -0,0 +1,36 @@
using Microsoft.Extensions.Options;
using Octokit;
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2007: Do not directly await a Task", Justification = "Durable functions")]
public class GithubService
{
private readonly GithubOptions _githubSettings;
public GithubService(IOptions<GithubOptions> ghOptions)
{
_githubSettings = ghOptions.Value;
}
public async Task<GitHubClient> GetGitHubClient()
{
// Use GitHubJwt library to create the GitHubApp Jwt Token using our private certificate PEM file
var generator = new GitHubJwt.GitHubJwtFactory(
new GitHubJwt.StringPrivateKeySource(_githubSettings.AppKey),
new GitHubJwt.GitHubJwtFactoryOptions
{
AppIntegrationId = _githubSettings.AppId, // The GitHub App Id
ExpirationSeconds = 600 // 10 minutes is the maximum time allowed
}
);
var jwtToken = generator.CreateEncodedJwtToken();
var appClient = new GitHubClient(new ProductHeaderValue("SK-DEV-APP"))
{
Credentials = new Credentials(jwtToken, AuthenticationType.Bearer)
};
var response = await appClient.GitHubApps.CreateInstallationToken(_githubSettings.InstallationId);
return new GitHubClient(new ProductHeaderValue($"SK-DEV-APP-Installation{_githubSettings.InstallationId}"))
{
Credentials = new Credentials(response.Token)
};
}
}

View File

@@ -0,0 +1,118 @@
using System.Net.Http.Json;
using System.Text;
using Microsoft.Extensions.Logging;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Orchestration;
using Newtonsoft.Json;
using Octokit.Webhooks;
using Octokit.Webhooks.Events;
using Octokit.Webhooks.Events.IssueComment;
using Octokit.Webhooks.Events.Issues;
using Octokit.Webhooks.Models;
using Microsoft.SKDevTeam;
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2007: Do not directly await a Task", Justification = "Durable functions")]
public class SKWebHookEventProcessor : WebhookEventProcessor
{
private readonly IKernel _kernel;
private readonly ILogger<SKWebHookEventProcessor> _logger;
private readonly GithubService _ghService;
private readonly IHttpClientFactory _httpClientFactory;
public SKWebHookEventProcessor(IKernel kernel, ILogger<SKWebHookEventProcessor> logger, GithubService ghService, IHttpClientFactory httpContextFactory)
{
_kernel = kernel;
_logger = logger;
_ghService = ghService;
_httpClientFactory = httpContextFactory;
}
protected override async Task ProcessIssuesWebhookAsync(WebhookHeaders headers, IssuesEvent issuesEvent, IssuesAction action)
{
var ghClient = await _ghService.GetGitHubClient();
var org = issuesEvent.Organization.Login;
var repo = issuesEvent.Repository.Name;
var issueNumber = issuesEvent.Issue.Number;
var input = issuesEvent.Issue.Body;
if (issuesEvent.Action == IssuesAction.Opened)
{
// Assumes the label follows the following convention: Skill.Function example: PM.Readme
var labels = issuesEvent.Issue.Labels.First().Name.Split(".");
var skillName = labels[0];
var functionName = labels[1];
if (skillName == "Do" && functionName == "It")
{
var issueOrchestrationRequest = new IssueOrchestrationRequest
{
Number = issueNumber,
Org = org,
Repo = repo,
Input = input
};
var content = new StringContent(JsonConvert.SerializeObject(issueOrchestrationRequest), Encoding.UTF8, "application/json");
var httpClient = _httpClientFactory.CreateClient("FunctionsClient");
await httpClient.PostAsync("doit", content);
}
else
{
var result = await RunSkill(skillName, functionName, input);
await ghClient.Issue.Comment.Create(org, repo, (int)issueNumber, result);
}
}
else if (issuesEvent.Action == IssuesAction.Closed && issuesEvent.Issue.User.Type.Value == UserType.Bot)
{
var httpClient = _httpClientFactory.CreateClient("FunctionsClient");
var metadata = await httpClient.GetFromJsonAsync<IssueMetadata>($"metadata/{org}{repo}{issueNumber}");
var closeIssueRequest = new CloseIssueRequest { InstanceId = metadata.InstanceId, CommentId = metadata.CommentId.Value, Org = org, Repo = repo };
var content = new StringContent(JsonConvert.SerializeObject(closeIssueRequest), Encoding.UTF8, "application/json");
_ = await httpClient.PostAsync("close", content);
}
}
protected override async Task ProcessIssueCommentWebhookAsync(
WebhookHeaders headers,
IssueCommentEvent issueCommentEvent,
IssueCommentAction action)
{
// we only resond to non-bot comments
if (issueCommentEvent.Sender.Type.Value != UserType.Bot)
{
var ghClient = await _ghService.GetGitHubClient();
var org = issueCommentEvent.Organization.Login;
var repo = issueCommentEvent.Repository.Name;
var issueId = issueCommentEvent.Issue.Number;
// Assumes the label follows the following convention: Skill.Function example: PM.Readme
var labels = issueCommentEvent.Issue.Labels.First().Name.Split(".");
var skillName = labels[0];
var functionName = labels[1];
var input = issueCommentEvent.Comment.Body;
var result = await RunSkill(skillName, functionName, input);
await ghClient.Issue.Comment.Create(org, repo, (int)issueId, result);
}
}
private async Task<string> RunSkill(string skillName, string functionName, string input)
{
var skillConfig = SemanticFunctionConfig.ForSkillAndFunction(skillName, functionName);
var function = _kernel.CreateSemanticFunction(skillConfig.PromptTemplate, skillConfig.Name, skillConfig.SkillName,
skillConfig.Description, skillConfig.MaxTokens, skillConfig.Temperature,
skillConfig.TopP, skillConfig.PPenalty, skillConfig.FPenalty);
var interestingMemories = _kernel.Memory.SearchAsync("waf-pages", input, 2);
var wafContext = "Consider the following architectural guidelines:";
await foreach (var memory in interestingMemories)
{
wafContext += $"\n {memory.Metadata.Text}";
}
var context = new ContextVariables();
context.Set("input", input);
context.Set("wafContext", wafContext);
var result = await _kernel.RunAsync(context, function);
return result.ToString();
}
}

View File

@@ -0,0 +1,9 @@
public class AzureOptions
{
public string SubscriptionId { get; set; }
public string Location { get; set; }
public string ContainerInstancesResourceGroup { get; set; }
public string FilesShareName { get; set; }
public string FilesAccountName { get; set; }
public string FilesAccountKey { get; set; }
}

View File

@@ -0,0 +1,6 @@
public class GithubOptions
{
public string AppKey { get; set; }
public int AppId { get; set; }
public long InstallationId { get; set; }
}

View File

@@ -1,29 +0,0 @@
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.AI.ChatCompletion;
internal static class KernelConfigExtensions
{
/// <summary>
/// Adds a text completion service to the list. It can be either an OpenAI or Azure OpenAI backend service.
/// </summary>
/// <param name="kernelConfig"></param>
/// <param name="kernelSettings"></param>
/// <exception cref="ArgumentException"></exception>
internal static void AddCompletionBackend(this KernelConfig kernelConfig, KernelSettings kernelSettings)
{
switch (kernelSettings.ServiceType.ToUpperInvariant())
{
case KernelSettings.AzureOpenAI:
//kernelConfig.AddAzureTextCompletionService(deploymentName: kernelSettings.DeploymentOrModelId, endpoint: kernelSettings.Endpoint, apiKey: kernelSettings.ApiKey, serviceId: kernelSettings.ServiceId);
kernelConfig.AddAzureChatCompletionService(kernelSettings.DeploymentOrModelId, kernelSettings.Endpoint, kernelSettings.ApiKey);
break;
case KernelSettings.OpenAI:
kernelConfig.AddOpenAITextCompletionService(modelId: kernelSettings.DeploymentOrModelId, apiKey: kernelSettings.ApiKey, orgId: kernelSettings.OrgId, serviceId: kernelSettings.ServiceId);
break;
default:
throw new ArgumentException($"Invalid service type value: {kernelSettings.ServiceType}");
}
}
}

View File

@@ -1,91 +0,0 @@
using System.Text.Json.Serialization;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
#pragma warning disable CA1812
internal class KernelSettings
{
public const string DefaultConfigFile = "config/appsettings.json";
public const string OpenAI = "OPENAI";
public const string AzureOpenAI = "AZUREOPENAI";
[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("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-azure-functions/README.md#configuring-the-starter"
);
throw new InvalidOperationException(ide.Message);
}
}
/// <summary>
/// Load the kernel settings from the specified configuration file if it exists.
/// </summary>
internal static KernelSettings FromFile(string configFile = DefaultConfigFile)
{
if (!File.Exists(configFile))
{
throw new FileNotFoundException($"Configuration not found: {configFile}");
}
var configuration = new ConfigurationBuilder()
.SetBasePath(System.IO.Directory.GetCurrentDirectory())
.AddJsonFile(configFile, optional: true, reloadOnChange: true)
.Build();
return configuration.Get<KernelSettings>()
?? throw new InvalidDataException($"Invalid semantic kernel settings in '{configFile}', please provide configuration settings using instructions in the README.");
}
/// <summary>
/// Load the kernel settings from user secrets.
/// </summary>
internal static KernelSettings FromUserSecrets()
{
var configuration = new ConfigurationBuilder()
.AddUserSecrets<KernelSettings>()
.AddEnvironmentVariables()
.Build();
return configuration.Get<KernelSettings>()
?? throw new InvalidDataException("Invalid semantic kernel settings in user secrets, please provide configuration settings using instructions in the README.");
}
}

View File

@@ -0,0 +1,9 @@
public class OpenAIOptions
{
public string ServiceType { get; set; }
public string ServiceId { get; set; }
public string DeploymentOrModelId { get; set; }
public string EmbeddingDeploymentOrModelId { get; set; }
public string Endpoint { get; set; }
public string ApiKey { get; set; }
}

View File

@@ -0,0 +1,5 @@
public class QdrantOptions
{
public string Endpoint { get; set; }
public int VectorSize { get; set; }
}

View File

@@ -1,6 +0,0 @@
{
"serviceType": "AzureOpenAI",
"serviceId": "gpt-4",
"deploymentOrModelId": "gpt-4",
"endpoint": "https://lightspeed-team-shared-openai-eastus.openai.azure.com/",
}

View File

@@ -1,7 +0,0 @@
{
"serviceType": "AzureOpenAI",
"serviceId": "text-davinci-003",
"deploymentOrModelId": "text-davinci-003",
"endpoint": "https:// ... your endpoint ... .openai.azure.com/",
"apiKey": "... your Azure OpenAI key ..."
}

View File

@@ -1,7 +0,0 @@
{
"serviceType": "OpenAI",
"serviceId": "text-davinci-003",
"deploymentOrModelId": "text-davinci-003",
"apiKey": "... your OpenAI key ...",
"orgId": ""
}

View File

@@ -7,5 +7,12 @@
"excludedTypes": "Request"
}
}
},
"extensions": {
"durableTask": {
"storageProvider": {
"type": "AzureStorage"
}
}
}
}

View File

@@ -4,6 +4,28 @@
"CORS": "*"
},
"Values": {
"FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated"
"FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
"FUNCTIONS_FQDN": "http://localhost:7071",
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
"AzureWebJobsFeatureFlags": "EnableHttpProxying",
"SANDBOX_IMAGE" : "mcr.microsoft.com/dotnet/sdk:7.0",
"GithubOptions:AppKey": "",
"GithubOptions:AppId": "",
"GithubOptions:InstallationId": "",
"AzureOptions:SubscriptionId":"",
"AzureOptions:Location":"",
"AzureOptions:ContainerInstancesResourceGroup":"",
"AzureOptions:FilesShareName":"",
"AzureOptions:FilesAccountName":"",
"AzureOptions:FilesAccountKey":"",
"OpenAIOptions:ServiceType":"",
"OpenAIOptions:ServiceId":"",
"OpenAIOptions:DeploymentOrModelId":"",
"OpenAIOptions:EmbeddingDeploymentOrModelId":"",
"OpenAIOptions:Endpoint":"",
"OpenAIOptions:ApiKey":"",
"QdrantOptions:Endpoint":"http://qdrant:6333",
"QdrantOptions:VectorSize":"1536"
}
}
}

View File

@@ -0,0 +1,52 @@
{
"IsEncrypted": false,
"Host": {
"CORS": "*"
},
"Values": {
"FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
// For local development, keep the default value
// for Azure deployment, it will be injected as a variable in the bicep template
"FUNCTIONS_FQDN": "localhost:7071",
// For local development, keep the default value
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
"AzureWebJobsFeatureFlags": "EnableHttpProxying",
// This is the container image used as a base for the sandbox
"SANDBOX_IMAGE" : "mcr.microsoft.com/devcontainers/universal:2-linux",
// The private key generated for the Github App
"GithubOptions__AppKey": "",
// The App Id for the Github App
"GithubOptions__AppId": "",
// The instalation ID for the Github App (once installed to a repo or an org)
"GithubOptions__InstallationId": "",
// Azure subscription id
"AzureOptions__SubscriptionId":"",
// Location for the deployed resources in Azure
"AzureOptions__Location":"",
// Resource group in Azure, where ACI sandbox instances are going to be created
"AzureOptions__ContainerInstancesResourceGroup":"",
// Azure storage file share name (doesn't work with Azurite)
"AzureOptions__FilesShareName":"",
// Azure storage file share account name
"AzureOptions__FilesAccountName":"",
// Azure storage file share account key
"AzureOptions__FilesAccountKey":"",
// If using Azure - AzureOpenAI
"OpenAIOptions__ServiceType":"",
// the service id of the OpenAI model you want to use
"OpenAIOptions__ServiceId":"",
// the deployment id of the OpenAI model you want to use
"OpenAIOptions__DeploymentOrModelId":"",
// the embedding deployment id for the semantic memory
"OpenAIOptions__EmbeddingDeploymentOrModelId":"",
// the endpoint for the provisioned OpenAI service
"OpenAIOptions__Endpoint":"",
// the key for the provisioned OpenAI service
"OpenAIOptions__ApiKey":"",
// if using Codespaces, keep the default value
"QdrantOptions__Endpoint":"http://qdrant:6333",
// keep default
"QdrantOptions__VectorSize":"1536"
}
}

View File

@@ -17,13 +17,26 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask" Version="1.0.2" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore" Version="1.0.0-preview4" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.ApplicationInsights" Version="1.0.0" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Timer" Version="4.2.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="6.0.1" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http" Version="3.0.13" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" Version="1.10.0" OutputItemType="Analyzer" />
<PackageReference Include="Microsoft.Azure.Functions.Worker" Version="1.14.1" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.OpenApi" Version="2.0.0-preview2" />
<PackageReference Include="Microsoft.Azure.WebJobs.Extensions.OpenApi" Version="2.0.0-preview2" TreatAsUsed="true" />
<PackageReference Include="Microsoft.SemanticKernel" Version="0.15.230609.2-preview" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http" Version="3.1.0" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" Version="1.14.0" />
<PackageReference Include="Microsoft.Azure.Functions.Worker" Version="1.19.0" />
<PackageReference Include="Microsoft.ApplicationInsights.WorkerService" Version="2.21.0" />
<PackageReference Include="Octokit.Webhooks.AzureFunctions" Version="2.0.1" />
<PackageReference Include="Microsoft.SemanticKernel" Version="0.18.230725.3-preview" />
<PackageReference Include="Microsoft.SemanticKernel.Connectors.Memory.Qdrant" Version="0.18.230725.3-preview" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Storage" Version="5.0.0" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Tables" Version="1.2.0" />
<PackageReference Include="Octokit" Version="6.0.0" />
<PackageReference Include="GitHubJwt" Version="0.0.6" />
<PackageReference Include="Azure.ResourceManager.ContainerInstance" Version="1.2.0-beta.1" />
<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.10.0-beta.1" />
<ProjectReference Include="..\skills\skills.csproj" />
</ItemGroup>

View File

@@ -9,42 +9,28 @@ public static class DevLead {
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 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.
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:
[
{
"step": "Step 1",
"description": "This is the first step",
"subtasks": [
"steps": [
{
"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"
}
]
},
{
"step": "Step 2",
"description": "This is the second 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"
"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}}

View File

@@ -7,6 +7,6 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.SemanticKernel" Version="0.15.230609.2-preview" />
<PackageReference Include="Microsoft.SemanticKernel" Version="0.18.230725.3-preview" />
</ItemGroup>
</Project>

View File

@@ -25,7 +25,7 @@ class Program
.AddDebug();
});
var memoryStore = new QdrantMemoryStore(new QdrantVectorDbClient("http://qdrant", 1536, port: 6333));
var memoryStore = new QdrantMemoryStore(new QdrantVectorDbClient(kernelSettings.QdrantEndpoint, 1536));
var embedingGeneration = new AzureTextEmbeddingGeneration(kernelSettings.EmbeddingDeploymentOrModelId, kernelSettings.Endpoint, kernelSettings.ApiKey);
var semanticTextMemory = new SemanticTextMemory(memoryStore, embedingGeneration);
@@ -45,13 +45,20 @@ class Program
var pages = pdfDocument.GetPages();
foreach (var page in pages)
{
var text = ContentOrderTextExtractor.GetText(page);
var descr = text.Take(100);
await kernel.Memory.SaveInformationAsync(
collection: "waf-pages",
text: text,
id: $"{Guid.NewGuid()}",
description: $"Document: {descr}");
try
{
var text = ContentOrderTextExtractor.GetText(page);
var descr = text.Take(100);
await kernel.Memory.SaveInformationAsync(
collection: "waf-pages",
text: text,
id: $"{Guid.NewGuid()}",
description: $"Document: {descr}");
}
catch(Exception ex)
{
Console.WriteLine(ex.Message);
}
}
}
}

View File

@@ -29,6 +29,9 @@ internal class KernelSettings
[JsonPropertyName("orgId")]
public string OrgId { get; set; } = string.Empty;
[JsonPropertyName("qdrantEndoint")]
public string QdrantEndpoint { get; set; } = string.Empty;
[JsonPropertyName("logLevel")]
public LogLevel? LogLevel { get; set; }
@@ -71,6 +74,7 @@ internal class KernelSettings
var configuration = new ConfigurationBuilder()
.SetBasePath(System.IO.Directory.GetCurrentDirectory())
.AddJsonFile(configFile, optional: true, reloadOnChange: true)
.AddEnvironmentVariables()
.Build();
return configuration.Get<KernelSettings>()

View File

@@ -0,0 +1,9 @@
{
"serviceType": "AzureOpenAI",
"serviceId": "",
"deploymentOrModelId": "",
"embeddingDeploymentOrModelId": "",
"endpoint": "",
"apiKey": "",
"qdrantEndoint": ""
}

View File

@@ -12,8 +12,8 @@
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="7.0.0" />
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
<PackageReference Include="Microsoft.SemanticKernel" Version="0.15.230609.2-preview" />
<PackageReference Include="Microsoft.SemanticKernel.Connectors.Memory.Qdrant" Version="0.15.230609.2-preview" />
<PackageReference Include="Microsoft.SemanticKernel" Version="0.18.230725.3-preview" />
<PackageReference Include="Microsoft.SemanticKernel.Connectors.Memory.Qdrant" Version="0.18.230725.3-preview" />
<PackageReference Include="PdfPig" Version="0.1.8-alpha-20230529-6daa2" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="7.0.1" />
</ItemGroup>