mirror of
https://github.com/microsoft/autogen.git
synced 2026-01-25 12:28:13 -05:00
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:
@@ -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
|
||||
@@ -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
9
.gitignore
vendored
@@ -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
8
azure.yaml
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
1
docs/github-flow-architecture.md
Normal file
1
docs/github-flow-architecture.md
Normal file
@@ -0,0 +1 @@
|
||||
# Azure components
|
||||
90
docs/github-flow-getting-started.md
Normal file
90
docs/github-flow-getting-started.md
Normal 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
1
docs/github-flow.md
Normal file
@@ -0,0 +1 @@
|
||||

|
||||
BIN
docs/images/github-sk-dev-team.png
Normal file
BIN
docs/images/github-sk-dev-team.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 302 KiB |
11
infra/abbreviations.json
Normal file
11
infra/abbreviations.json
Normal 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
45
infra/app/sk-func.bicep
Normal 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
|
||||
64
infra/core/database/postgresql/flexibleserver.bicep
Normal file
64
infra/core/database/postgresql/flexibleserver.bicep
Normal 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
|
||||
72
infra/core/database/qdrant/qdrant-aca.bicep
Normal file
72
infra/core/database/qdrant/qdrant-aca.bicep
Normal 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
|
||||
|
||||
16
infra/core/host/appservice-appsettings.bicep
Normal file
16
infra/core/host/appservice-appsettings.bicep
Normal 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
|
||||
}
|
||||
119
infra/core/host/appservice.bicep
Normal file
119
infra/core/host/appservice.bicep
Normal 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}'
|
||||
21
infra/core/host/appserviceplan.bicep
Normal file
21
infra/core/host/appserviceplan.bicep
Normal 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
|
||||
104
infra/core/host/container-app-upsert.bicep
Normal file
104
infra/core/host/container-app-upsert.bicep
Normal 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
|
||||
161
infra/core/host/container-app.bicep
Normal file
161
infra/core/host/container-app.bicep
Normal 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}' : ''
|
||||
40
infra/core/host/container-apps-environment.bicep
Normal file
40
infra/core/host/container-apps-environment.bicep
Normal 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
|
||||
37
infra/core/host/container-apps.bicep
Normal file
37
infra/core/host/container-apps.bicep
Normal 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
|
||||
82
infra/core/host/container-registry.bicep
Normal file
82
infra/core/host/container-registry.bicep
Normal 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
|
||||
87
infra/core/host/functions.bicep
Normal file
87
infra/core/host/functions.bicep
Normal 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
|
||||
1235
infra/core/monitor/applicationinsights-dashboard.bicep
Normal file
1235
infra/core/monitor/applicationinsights-dashboard.bicep
Normal file
File diff suppressed because it is too large
Load Diff
30
infra/core/monitor/applicationinsights.bicep
Normal file
30
infra/core/monitor/applicationinsights.bicep
Normal 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
|
||||
21
infra/core/monitor/loganalytics.bicep
Normal file
21
infra/core/monitor/loganalytics.bicep
Normal 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
|
||||
33
infra/core/monitor/monitoring.bicep
Normal file
33
infra/core/monitor/monitoring.bicep
Normal 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
|
||||
76
infra/core/storage/storage-account.bicep
Normal file
76
infra/core/storage/storage-account.bicep
Normal 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
165
infra/main.bicep
Normal 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
|
||||
|
||||
42
infra/main.parameters.json
Normal file
42
infra/main.parameters.json
Normal 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}"
|
||||
}
|
||||
}
|
||||
}
|
||||
76
sk-azfunc-server/Activities/IssueActivities.cs
Normal file
76
sk-azfunc-server/Activities/IssueActivities.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
34
sk-azfunc-server/Activities/MetadataActivities.cs
Normal file
34
sk-azfunc-server/Activities/MetadataActivities.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
240
sk-azfunc-server/Activities/PullRequestActivities.cs
Normal file
240
sk-azfunc-server/Activities/PullRequestActivities.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
10
sk-azfunc-server/Models/AddToPRRequest.cs
Normal file
10
sk-azfunc-server/Models/AddToPRRequest.cs
Normal 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; }
|
||||
}
|
||||
7
sk-azfunc-server/Models/CloseIssueRequest.cs
Normal file
7
sk-azfunc-server/Models/CloseIssueRequest.cs
Normal 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; }
|
||||
}
|
||||
12
sk-azfunc-server/Models/ContainerInstanceMetadata.cs
Normal file
12
sk-azfunc-server/Models/ContainerInstanceMetadata.cs
Normal 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; }
|
||||
}
|
||||
17
sk-azfunc-server/Models/DevLeadPlanResponse.cs
Normal file
17
sk-azfunc-server/Models/DevLeadPlanResponse.cs
Normal 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; }
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
12
sk-azfunc-server/Models/GHCommitRequest.cs
Normal file
12
sk-azfunc-server/Models/GHCommitRequest.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
10
sk-azfunc-server/Models/GHNewBranch.cs
Normal file
10
sk-azfunc-server/Models/GHNewBranch.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
19
sk-azfunc-server/Models/IssueMetadata.cs
Normal file
19
sk-azfunc-server/Models/IssueMetadata.cs
Normal 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; }
|
||||
}
|
||||
8
sk-azfunc-server/Models/IssueOrchestrationRequest.cs
Normal file
8
sk-azfunc-server/Models/IssueOrchestrationRequest.cs
Normal 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}";
|
||||
}
|
||||
6
sk-azfunc-server/Models/NewIssueRequest.cs
Normal file
6
sk-azfunc-server/Models/NewIssueRequest.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
public class NewIssueRequest
|
||||
{
|
||||
public IssueOrchestrationRequest IssueRequest { get; set; }
|
||||
public string Skill { get; set; }
|
||||
public string Function { get; set; }
|
||||
}
|
||||
5
sk-azfunc-server/Models/NewIssueResponse.cs
Normal file
5
sk-azfunc-server/Models/NewIssueResponse.cs
Normal file
@@ -0,0 +1,5 @@
|
||||
public class NewIssueResponse
|
||||
{
|
||||
public long Number { get; set; }
|
||||
public int CommentId { get; set; }
|
||||
}
|
||||
6
sk-azfunc-server/Models/RunAndSaveRequest.cs
Normal file
6
sk-azfunc-server/Models/RunAndSaveRequest.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
public class RunAndSaveRequest
|
||||
{
|
||||
public IssueOrchestrationRequest Request { get; set; }
|
||||
public string InstanceId { get; set; }
|
||||
|
||||
}
|
||||
8
sk-azfunc-server/Models/RunInSandboxRequest.cs
Normal file
8
sk-azfunc-server/Models/RunInSandboxRequest.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace SK.DevTeam
|
||||
{
|
||||
public class RunInSandboxRequest
|
||||
{
|
||||
public AddToPRRequest PrRequest { get; set; }
|
||||
public string SanboxOrchestrationId { get; set; }
|
||||
}
|
||||
}
|
||||
12
sk-azfunc-server/Models/SaveOutputRequest.cs
Normal file
12
sk-azfunc-server/Models/SaveOutputRequest.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
9
sk-azfunc-server/Models/SkillRequest.cs
Normal file
9
sk-azfunc-server/Models/SkillRequest.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
5
sk-azfunc-server/Models/SkillResponse.cs
Normal file
5
sk-azfunc-server/Models/SkillResponse.cs
Normal file
@@ -0,0 +1,5 @@
|
||||
public class SkillResponse<T>
|
||||
{
|
||||
public T Output { get; set; }
|
||||
public string SuborchestrationId { get; set; }
|
||||
}
|
||||
75
sk-azfunc-server/Orchestrators/IssueOrchestration.cs
Normal file
75
sk-azfunc-server/Orchestrators/IssueOrchestration.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
154
sk-azfunc-server/Orchestrators/SubIssueOrchestration.cs
Normal file
154
sk-azfunc-server/Orchestrators/SubIssueOrchestration.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
36
sk-azfunc-server/Services/GithubService.cs
Normal file
36
sk-azfunc-server/Services/GithubService.cs
Normal 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)
|
||||
};
|
||||
}
|
||||
}
|
||||
118
sk-azfunc-server/Services/WebHookEventProcessor.cs
Normal file
118
sk-azfunc-server/Services/WebHookEventProcessor.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
9
sk-azfunc-server/config/AzureOptions.cs
Normal file
9
sk-azfunc-server/config/AzureOptions.cs
Normal 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; }
|
||||
}
|
||||
6
sk-azfunc-server/config/GithubOptions.cs
Normal file
6
sk-azfunc-server/config/GithubOptions.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
public class GithubOptions
|
||||
{
|
||||
public string AppKey { get; set; }
|
||||
public int AppId { get; set; }
|
||||
public long InstallationId { get; set; }
|
||||
}
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
9
sk-azfunc-server/config/OpenAIOptions.cs
Normal file
9
sk-azfunc-server/config/OpenAIOptions.cs
Normal 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; }
|
||||
}
|
||||
5
sk-azfunc-server/config/QdrantOptions.cs
Normal file
5
sk-azfunc-server/config/QdrantOptions.cs
Normal file
@@ -0,0 +1,5 @@
|
||||
public class QdrantOptions
|
||||
{
|
||||
public string Endpoint { get; set; }
|
||||
public int VectorSize { get; set; }
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"serviceType": "AzureOpenAI",
|
||||
"serviceId": "gpt-4",
|
||||
"deploymentOrModelId": "gpt-4",
|
||||
"endpoint": "https://lightspeed-team-shared-openai-eastus.openai.azure.com/",
|
||||
}
|
||||
@@ -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 ..."
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"serviceType": "OpenAI",
|
||||
"serviceId": "text-davinci-003",
|
||||
"deploymentOrModelId": "text-davinci-003",
|
||||
"apiKey": "... your OpenAI key ...",
|
||||
"orgId": ""
|
||||
}
|
||||
@@ -7,5 +7,12 @@
|
||||
"excludedTypes": "Request"
|
||||
}
|
||||
}
|
||||
},
|
||||
"extensions": {
|
||||
"durableTask": {
|
||||
"storageProvider": {
|
||||
"type": "AzureStorage"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
52
sk-azfunc-server/local.settings.template.json
Normal file
52
sk-azfunc-server/local.settings.template.json
Normal 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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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}}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>()
|
||||
|
||||
9
util/seed-memory/config/appsettings.template.json
Normal file
9
util/seed-memory/config/appsettings.template.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"serviceType": "AzureOpenAI",
|
||||
"serviceId": "",
|
||||
"deploymentOrModelId": "",
|
||||
"embeddingDeploymentOrModelId": "",
|
||||
"endpoint": "",
|
||||
"apiKey": "",
|
||||
"qdrantEndoint": ""
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user