diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index a221106f594..bbaf9543a1b 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -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 \ No newline at end of file +RUN curl -fsSL https://aka.ms/install-azd.sh | bash + +RUN npm i -g azure-functions-core-tools@4 --unsafe-perm true \ No newline at end of file diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index f1fb5cdc9ea..61446950c64 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -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" ] } } diff --git a/.gitignore b/.gitignore index 85fa5e4e205..af040acbd78 100644 --- a/.gitignore +++ b/.gitignore @@ -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/ \ No newline at end of file +elsa-core/ +sk-azfunc-server/local.settings.json +.azure +temp \ No newline at end of file diff --git a/azure.yaml b/azure.yaml new file mode 100644 index 00000000000..cf9b6d9db7c --- /dev/null +++ b/azure.yaml @@ -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 \ No newline at end of file diff --git a/cli/Models/DevLeadPlanResponse.cs b/cli/Models/DevLeadPlanResponse.cs index a68eacf91ce..3c036d7e9c5 100644 --- a/cli/Models/DevLeadPlanResponse.cs +++ b/cli/Models/DevLeadPlanResponse.cs @@ -3,7 +3,7 @@ public class Subtask { public string subtask { get; set; } - public string LLM_prompt { get; set; } + public string prompt { get; set; } } public class Step diff --git a/cli/Program.cs b/cli/Program.cs index 977cce0ff7f..19b6fd99365 100644 --- a/cli/Program.cs +++ b/cli/Program.cs @@ -92,7 +92,7 @@ public static async Task ChainFunctions(string file, int maxRetry) { try { - implementationResult = await CallFunction(nameof(Developer), Developer.Implement, subtask.LLM_prompt, maxRetry); + implementationResult = await CallFunction(nameof(Developer), Developer.Implement, subtask.prompt, maxRetry); break; } catch (Exception ex) @@ -137,7 +137,7 @@ public static async Task CallFunction(string skillName, string functionNam .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); diff --git a/cli/SandboxSkill.cs b/cli/SandboxSkill.cs index 0cc9f310d49..a44782a13a0 100644 --- a/cli/SandboxSkill.cs +++ b/cli/SandboxSkill.cs @@ -3,17 +3,11 @@ public class SandboxSkill { - [SKFunction("Run a script in Alpine sandbox")] - [SKFunctionInput(Description = "The script to be executed")] - [SKFunctionName("RunInAlpine")] public async Task 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 RunInDotnetAlpineAsync(string input) { return await RunInContainer(input, "mcr.microsoft.com/dotnet/sdk:7.0"); diff --git a/cli/cli.csproj b/cli/cli.csproj index d795b4e68be..5b4aea6bc2f 100644 --- a/cli/cli.csproj +++ b/cli/cli.csproj @@ -13,8 +13,8 @@ - - + + diff --git a/cli/config/KernelConfigExtensions.cs b/cli/config/KernelConfigExtensions.cs deleted file mode 100644 index 6d1d9f2c399..00000000000 --- a/cli/config/KernelConfigExtensions.cs +++ /dev/null @@ -1,27 +0,0 @@ -using Microsoft.SemanticKernel; - -internal static class KernelConfigExtensions -{ - /// - /// Adds a text completion service to the list. It can be either an OpenAI or Azure OpenAI backend service. - /// - /// - /// - /// - 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}"); - } - } -} diff --git a/docs/github-flow-architecture.md b/docs/github-flow-architecture.md new file mode 100644 index 00000000000..04be0b78ab6 --- /dev/null +++ b/docs/github-flow-architecture.md @@ -0,0 +1 @@ +# Azure components diff --git a/docs/github-flow-getting-started.md b/docs/github-flow-getting-started.md new file mode 100644 index 00000000000..e2c67c986d6 --- /dev/null +++ b/docs/github-flow-getting-started.md @@ -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. \ No newline at end of file diff --git a/docs/github-flow.md b/docs/github-flow.md new file mode 100644 index 00000000000..5be43fddaa6 --- /dev/null +++ b/docs/github-flow.md @@ -0,0 +1 @@ +![](/docs/images/github-sk-dev-team.png) \ No newline at end of file diff --git a/docs/images/github-sk-dev-team.png b/docs/images/github-sk-dev-team.png new file mode 100644 index 00000000000..c70e4d87b58 Binary files /dev/null and b/docs/images/github-sk-dev-team.png differ diff --git a/infra/abbreviations.json b/infra/abbreviations.json new file mode 100644 index 00000000000..9ee376cd5e0 --- /dev/null +++ b/infra/abbreviations.json @@ -0,0 +1,11 @@ +{ + "appManagedEnvironments": "cae-", + "containerRegistryRegistries": "cr", + "insightsComponents": "appi-", + "operationalInsightsWorkspaces": "log-", + "portalDashboards": "dash-", + "resourcesResourceGroups": "rg-", + "storageStorageAccounts": "st", + "webServerFarms": "plan-", + "webSitesFunctions": "func-" +} \ No newline at end of file diff --git a/infra/app/sk-func.bicep b/infra/app/sk-func.bicep new file mode 100644 index 00000000000..54a374f607e --- /dev/null +++ b/infra/app/sk-func.bicep @@ -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 diff --git a/infra/core/database/postgresql/flexibleserver.bicep b/infra/core/database/postgresql/flexibleserver.bicep new file mode 100644 index 00000000000..effda63de48 --- /dev/null +++ b/infra/core/database/postgresql/flexibleserver.bicep @@ -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 diff --git a/infra/core/database/qdrant/qdrant-aca.bicep b/infra/core/database/qdrant/qdrant-aca.bicep new file mode 100644 index 00000000000..a5f1573ebff --- /dev/null +++ b/infra/core/database/qdrant/qdrant-aca.bicep @@ -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 + diff --git a/infra/core/host/appservice-appsettings.bicep b/infra/core/host/appservice-appsettings.bicep new file mode 100644 index 00000000000..574342a8147 --- /dev/null +++ b/infra/core/host/appservice-appsettings.bicep @@ -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 +} diff --git a/infra/core/host/appservice.bicep b/infra/core/host/appservice.bicep new file mode 100644 index 00000000000..4dc3e71412c --- /dev/null +++ b/infra/core/host/appservice.bicep @@ -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}' diff --git a/infra/core/host/appserviceplan.bicep b/infra/core/host/appserviceplan.bicep new file mode 100644 index 00000000000..c444f40651d --- /dev/null +++ b/infra/core/host/appserviceplan.bicep @@ -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 diff --git a/infra/core/host/container-app-upsert.bicep b/infra/core/host/container-app-upsert.bicep new file mode 100644 index 00000000000..d9be752b095 --- /dev/null +++ b/infra/core/host/container-app-upsert.bicep @@ -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 diff --git a/infra/core/host/container-app.bicep b/infra/core/host/container-app.bicep new file mode 100644 index 00000000000..7781caa8013 --- /dev/null +++ b/infra/core/host/container-app.bicep @@ -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}' : '' diff --git a/infra/core/host/container-apps-environment.bicep b/infra/core/host/container-apps-environment.bicep new file mode 100644 index 00000000000..f29079a021d --- /dev/null +++ b/infra/core/host/container-apps-environment.bicep @@ -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 diff --git a/infra/core/host/container-apps.bicep b/infra/core/host/container-apps.bicep new file mode 100644 index 00000000000..38f47e068c2 --- /dev/null +++ b/infra/core/host/container-apps.bicep @@ -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 diff --git a/infra/core/host/container-registry.bicep b/infra/core/host/container-registry.bicep new file mode 100644 index 00000000000..02af29925e9 --- /dev/null +++ b/infra/core/host/container-registry.bicep @@ -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 diff --git a/infra/core/host/functions.bicep b/infra/core/host/functions.bicep new file mode 100644 index 00000000000..e5805c70d36 --- /dev/null +++ b/infra/core/host/functions.bicep @@ -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 diff --git a/infra/core/monitor/applicationinsights-dashboard.bicep b/infra/core/monitor/applicationinsights-dashboard.bicep new file mode 100644 index 00000000000..b7af2c1a106 --- /dev/null +++ b/infra/core/monitor/applicationinsights-dashboard.bicep @@ -0,0 +1,1235 @@ +param name string +param applicationInsightsName string +param location string = resourceGroup().location +param tags object = {} + +// 2020-09-01-preview because that is the latest valid version +resource applicationInsightsDashboard 'Microsoft.Portal/dashboards@2020-09-01-preview' = { + name: name + location: location + tags: tags + properties: { + lenses: [ + { + order: 0 + parts: [ + { + position: { + x: 0 + y: 0 + colSpan: 2 + rowSpan: 1 + } + metadata: { + inputs: [ + { + name: 'id' + value: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + } + { + name: 'Version' + value: '1.0' + } + ] + #disable-next-line BCP036 + type: 'Extension/AppInsightsExtension/PartType/AspNetOverviewPinnedPart' + asset: { + idInputName: 'id' + type: 'ApplicationInsights' + } + defaultMenuItemId: 'overview' + } + } + { + position: { + x: 2 + y: 0 + colSpan: 1 + rowSpan: 1 + } + metadata: { + inputs: [ + { + name: 'ComponentId' + value: { + Name: applicationInsights.name + SubscriptionId: subscription().subscriptionId + ResourceGroup: resourceGroup().name + } + } + { + name: 'Version' + value: '1.0' + } + ] + #disable-next-line BCP036 + type: 'Extension/AppInsightsExtension/PartType/ProactiveDetectionAsyncPart' + asset: { + idInputName: 'ComponentId' + type: 'ApplicationInsights' + } + defaultMenuItemId: 'ProactiveDetection' + } + } + { + position: { + x: 3 + y: 0 + colSpan: 1 + rowSpan: 1 + } + metadata: { + inputs: [ + { + name: 'ComponentId' + value: { + Name: applicationInsights.name + SubscriptionId: subscription().subscriptionId + ResourceGroup: resourceGroup().name + } + } + { + name: 'ResourceId' + value: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + } + ] + #disable-next-line BCP036 + type: 'Extension/AppInsightsExtension/PartType/QuickPulseButtonSmallPart' + asset: { + idInputName: 'ComponentId' + type: 'ApplicationInsights' + } + } + } + { + position: { + x: 4 + y: 0 + colSpan: 1 + rowSpan: 1 + } + metadata: { + inputs: [ + { + name: 'ComponentId' + value: { + Name: applicationInsights.name + SubscriptionId: subscription().subscriptionId + ResourceGroup: resourceGroup().name + } + } + { + name: 'TimeContext' + value: { + durationMs: 86400000 + endTime: null + createdTime: '2018-05-04T01:20:33.345Z' + isInitialTime: true + grain: 1 + useDashboardTimeRange: false + } + } + { + name: 'Version' + value: '1.0' + } + ] + #disable-next-line BCP036 + type: 'Extension/AppInsightsExtension/PartType/AvailabilityNavButtonPart' + asset: { + idInputName: 'ComponentId' + type: 'ApplicationInsights' + } + } + } + { + position: { + x: 5 + y: 0 + colSpan: 1 + rowSpan: 1 + } + metadata: { + inputs: [ + { + name: 'ComponentId' + value: { + Name: applicationInsights.name + SubscriptionId: subscription().subscriptionId + ResourceGroup: resourceGroup().name + } + } + { + name: 'TimeContext' + value: { + durationMs: 86400000 + endTime: null + createdTime: '2018-05-08T18:47:35.237Z' + isInitialTime: true + grain: 1 + useDashboardTimeRange: false + } + } + { + name: 'ConfigurationId' + value: '78ce933e-e864-4b05-a27b-71fd55a6afad' + } + ] + #disable-next-line BCP036 + type: 'Extension/AppInsightsExtension/PartType/AppMapButtonPart' + asset: { + idInputName: 'ComponentId' + type: 'ApplicationInsights' + } + } + } + { + position: { + x: 0 + y: 1 + colSpan: 3 + rowSpan: 1 + } + metadata: { + inputs: [] + type: 'Extension/HubsExtension/PartType/MarkdownPart' + settings: { + content: { + settings: { + content: '# Usage' + title: '' + subtitle: '' + } + } + } + } + } + { + position: { + x: 3 + y: 1 + colSpan: 1 + rowSpan: 1 + } + metadata: { + inputs: [ + { + name: 'ComponentId' + value: { + Name: applicationInsights.name + SubscriptionId: subscription().subscriptionId + ResourceGroup: resourceGroup().name + } + } + { + name: 'TimeContext' + value: { + durationMs: 86400000 + endTime: null + createdTime: '2018-05-04T01:22:35.782Z' + isInitialTime: true + grain: 1 + useDashboardTimeRange: false + } + } + ] + #disable-next-line BCP036 + type: 'Extension/AppInsightsExtension/PartType/UsageUsersOverviewPart' + asset: { + idInputName: 'ComponentId' + type: 'ApplicationInsights' + } + } + } + { + position: { + x: 4 + y: 1 + colSpan: 3 + rowSpan: 1 + } + metadata: { + inputs: [] + type: 'Extension/HubsExtension/PartType/MarkdownPart' + settings: { + content: { + settings: { + content: '# Reliability' + title: '' + subtitle: '' + } + } + } + } + } + { + position: { + x: 7 + y: 1 + colSpan: 1 + rowSpan: 1 + } + metadata: { + inputs: [ + { + name: 'ResourceId' + value: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + } + { + name: 'DataModel' + value: { + version: '1.0.0' + timeContext: { + durationMs: 86400000 + createdTime: '2018-05-04T23:42:40.072Z' + isInitialTime: false + grain: 1 + useDashboardTimeRange: false + } + } + isOptional: true + } + { + name: 'ConfigurationId' + value: '8a02f7bf-ac0f-40e1-afe9-f0e72cfee77f' + isOptional: true + } + ] + #disable-next-line BCP036 + type: 'Extension/AppInsightsExtension/PartType/CuratedBladeFailuresPinnedPart' + isAdapter: true + asset: { + idInputName: 'ResourceId' + type: 'ApplicationInsights' + } + defaultMenuItemId: 'failures' + } + } + { + position: { + x: 8 + y: 1 + colSpan: 3 + rowSpan: 1 + } + metadata: { + inputs: [] + type: 'Extension/HubsExtension/PartType/MarkdownPart' + settings: { + content: { + settings: { + content: '# Responsiveness\r\n' + title: '' + subtitle: '' + } + } + } + } + } + { + position: { + x: 11 + y: 1 + colSpan: 1 + rowSpan: 1 + } + metadata: { + inputs: [ + { + name: 'ResourceId' + value: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + } + { + name: 'DataModel' + value: { + version: '1.0.0' + timeContext: { + durationMs: 86400000 + createdTime: '2018-05-04T23:43:37.804Z' + isInitialTime: false + grain: 1 + useDashboardTimeRange: false + } + } + isOptional: true + } + { + name: 'ConfigurationId' + value: '2a8ede4f-2bee-4b9c-aed9-2db0e8a01865' + isOptional: true + } + ] + #disable-next-line BCP036 + type: 'Extension/AppInsightsExtension/PartType/CuratedBladePerformancePinnedPart' + isAdapter: true + asset: { + idInputName: 'ResourceId' + type: 'ApplicationInsights' + } + defaultMenuItemId: 'performance' + } + } + { + position: { + x: 12 + y: 1 + colSpan: 3 + rowSpan: 1 + } + metadata: { + inputs: [] + type: 'Extension/HubsExtension/PartType/MarkdownPart' + settings: { + content: { + settings: { + content: '# Browser' + title: '' + subtitle: '' + } + } + } + } + } + { + position: { + x: 15 + y: 1 + colSpan: 1 + rowSpan: 1 + } + metadata: { + inputs: [ + { + name: 'ComponentId' + value: { + Name: applicationInsights.name + SubscriptionId: subscription().subscriptionId + ResourceGroup: resourceGroup().name + } + } + { + name: 'MetricsExplorerJsonDefinitionId' + value: 'BrowserPerformanceTimelineMetrics' + } + { + name: 'TimeContext' + value: { + durationMs: 86400000 + createdTime: '2018-05-08T12:16:27.534Z' + isInitialTime: false + grain: 1 + useDashboardTimeRange: false + } + } + { + name: 'CurrentFilter' + value: { + eventTypes: [ + 4 + 1 + 3 + 5 + 2 + 6 + 13 + ] + typeFacets: {} + isPermissive: false + } + } + { + name: 'id' + value: { + Name: applicationInsights.name + SubscriptionId: subscription().subscriptionId + ResourceGroup: resourceGroup().name + } + } + { + name: 'Version' + value: '1.0' + } + ] + #disable-next-line BCP036 + type: 'Extension/AppInsightsExtension/PartType/MetricsExplorerBladePinnedPart' + asset: { + idInputName: 'ComponentId' + type: 'ApplicationInsights' + } + defaultMenuItemId: 'browser' + } + } + { + position: { + x: 0 + y: 2 + colSpan: 4 + rowSpan: 3 + } + metadata: { + inputs: [ + { + name: 'options' + value: { + chart: { + metrics: [ + { + resourceMetadata: { + id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + } + name: 'sessions/count' + aggregationType: 5 + namespace: 'microsoft.insights/components/kusto' + metricVisualization: { + displayName: 'Sessions' + color: '#47BDF5' + } + } + { + resourceMetadata: { + id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + } + name: 'users/count' + aggregationType: 5 + namespace: 'microsoft.insights/components/kusto' + metricVisualization: { + displayName: 'Users' + color: '#7E58FF' + } + } + ] + title: 'Unique sessions and users' + visualization: { + chartType: 2 + legendVisualization: { + isVisible: true + position: 2 + hideSubtitle: false + } + axisVisualization: { + x: { + isVisible: true + axisType: 2 + } + y: { + isVisible: true + axisType: 1 + } + } + } + openBladeOnClick: { + openBlade: true + destinationBlade: { + extensionName: 'HubsExtension' + bladeName: 'ResourceMenuBlade' + parameters: { + id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + menuid: 'segmentationUsers' + } + } + } + } + } + } + { + name: 'sharedTimeRange' + isOptional: true + } + ] + #disable-next-line BCP036 + type: 'Extension/HubsExtension/PartType/MonitorChartPart' + settings: {} + } + } + { + position: { + x: 4 + y: 2 + colSpan: 4 + rowSpan: 3 + } + metadata: { + inputs: [ + { + name: 'options' + value: { + chart: { + metrics: [ + { + resourceMetadata: { + id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + } + name: 'requests/failed' + aggregationType: 7 + namespace: 'microsoft.insights/components' + metricVisualization: { + displayName: 'Failed requests' + color: '#EC008C' + } + } + ] + title: 'Failed requests' + visualization: { + chartType: 3 + legendVisualization: { + isVisible: true + position: 2 + hideSubtitle: false + } + axisVisualization: { + x: { + isVisible: true + axisType: 2 + } + y: { + isVisible: true + axisType: 1 + } + } + } + openBladeOnClick: { + openBlade: true + destinationBlade: { + extensionName: 'HubsExtension' + bladeName: 'ResourceMenuBlade' + parameters: { + id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + menuid: 'failures' + } + } + } + } + } + } + { + name: 'sharedTimeRange' + isOptional: true + } + ] + #disable-next-line BCP036 + type: 'Extension/HubsExtension/PartType/MonitorChartPart' + settings: {} + } + } + { + position: { + x: 8 + y: 2 + colSpan: 4 + rowSpan: 3 + } + metadata: { + inputs: [ + { + name: 'options' + value: { + chart: { + metrics: [ + { + resourceMetadata: { + id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + } + name: 'requests/duration' + aggregationType: 4 + namespace: 'microsoft.insights/components' + metricVisualization: { + displayName: 'Server response time' + color: '#00BCF2' + } + } + ] + title: 'Server response time' + visualization: { + chartType: 2 + legendVisualization: { + isVisible: true + position: 2 + hideSubtitle: false + } + axisVisualization: { + x: { + isVisible: true + axisType: 2 + } + y: { + isVisible: true + axisType: 1 + } + } + } + openBladeOnClick: { + openBlade: true + destinationBlade: { + extensionName: 'HubsExtension' + bladeName: 'ResourceMenuBlade' + parameters: { + id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + menuid: 'performance' + } + } + } + } + } + } + { + name: 'sharedTimeRange' + isOptional: true + } + ] + #disable-next-line BCP036 + type: 'Extension/HubsExtension/PartType/MonitorChartPart' + settings: {} + } + } + { + position: { + x: 12 + y: 2 + colSpan: 4 + rowSpan: 3 + } + metadata: { + inputs: [ + { + name: 'options' + value: { + chart: { + metrics: [ + { + resourceMetadata: { + id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + } + name: 'browserTimings/networkDuration' + aggregationType: 4 + namespace: 'microsoft.insights/components' + metricVisualization: { + displayName: 'Page load network connect time' + color: '#7E58FF' + } + } + { + resourceMetadata: { + id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + } + name: 'browserTimings/processingDuration' + aggregationType: 4 + namespace: 'microsoft.insights/components' + metricVisualization: { + displayName: 'Client processing time' + color: '#44F1C8' + } + } + { + resourceMetadata: { + id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + } + name: 'browserTimings/sendDuration' + aggregationType: 4 + namespace: 'microsoft.insights/components' + metricVisualization: { + displayName: 'Send request time' + color: '#EB9371' + } + } + { + resourceMetadata: { + id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + } + name: 'browserTimings/receiveDuration' + aggregationType: 4 + namespace: 'microsoft.insights/components' + metricVisualization: { + displayName: 'Receiving response time' + color: '#0672F1' + } + } + ] + title: 'Average page load time breakdown' + visualization: { + chartType: 3 + legendVisualization: { + isVisible: true + position: 2 + hideSubtitle: false + } + axisVisualization: { + x: { + isVisible: true + axisType: 2 + } + y: { + isVisible: true + axisType: 1 + } + } + } + } + } + } + { + name: 'sharedTimeRange' + isOptional: true + } + ] + #disable-next-line BCP036 + type: 'Extension/HubsExtension/PartType/MonitorChartPart' + settings: {} + } + } + { + position: { + x: 0 + y: 5 + colSpan: 4 + rowSpan: 3 + } + metadata: { + inputs: [ + { + name: 'options' + value: { + chart: { + metrics: [ + { + resourceMetadata: { + id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + } + name: 'availabilityResults/availabilityPercentage' + aggregationType: 4 + namespace: 'microsoft.insights/components' + metricVisualization: { + displayName: 'Availability' + color: '#47BDF5' + } + } + ] + title: 'Average availability' + visualization: { + chartType: 3 + legendVisualization: { + isVisible: true + position: 2 + hideSubtitle: false + } + axisVisualization: { + x: { + isVisible: true + axisType: 2 + } + y: { + isVisible: true + axisType: 1 + } + } + } + openBladeOnClick: { + openBlade: true + destinationBlade: { + extensionName: 'HubsExtension' + bladeName: 'ResourceMenuBlade' + parameters: { + id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + menuid: 'availability' + } + } + } + } + } + } + { + name: 'sharedTimeRange' + isOptional: true + } + ] + #disable-next-line BCP036 + type: 'Extension/HubsExtension/PartType/MonitorChartPart' + settings: {} + } + } + { + position: { + x: 4 + y: 5 + colSpan: 4 + rowSpan: 3 + } + metadata: { + inputs: [ + { + name: 'options' + value: { + chart: { + metrics: [ + { + resourceMetadata: { + id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + } + name: 'exceptions/server' + aggregationType: 7 + namespace: 'microsoft.insights/components' + metricVisualization: { + displayName: 'Server exceptions' + color: '#47BDF5' + } + } + { + resourceMetadata: { + id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + } + name: 'dependencies/failed' + aggregationType: 7 + namespace: 'microsoft.insights/components' + metricVisualization: { + displayName: 'Dependency failures' + color: '#7E58FF' + } + } + ] + title: 'Server exceptions and Dependency failures' + visualization: { + chartType: 2 + legendVisualization: { + isVisible: true + position: 2 + hideSubtitle: false + } + axisVisualization: { + x: { + isVisible: true + axisType: 2 + } + y: { + isVisible: true + axisType: 1 + } + } + } + } + } + } + { + name: 'sharedTimeRange' + isOptional: true + } + ] + #disable-next-line BCP036 + type: 'Extension/HubsExtension/PartType/MonitorChartPart' + settings: {} + } + } + { + position: { + x: 8 + y: 5 + colSpan: 4 + rowSpan: 3 + } + metadata: { + inputs: [ + { + name: 'options' + value: { + chart: { + metrics: [ + { + resourceMetadata: { + id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + } + name: 'performanceCounters/processorCpuPercentage' + aggregationType: 4 + namespace: 'microsoft.insights/components' + metricVisualization: { + displayName: 'Processor time' + color: '#47BDF5' + } + } + { + resourceMetadata: { + id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + } + name: 'performanceCounters/processCpuPercentage' + aggregationType: 4 + namespace: 'microsoft.insights/components' + metricVisualization: { + displayName: 'Process CPU' + color: '#7E58FF' + } + } + ] + title: 'Average processor and process CPU utilization' + visualization: { + chartType: 2 + legendVisualization: { + isVisible: true + position: 2 + hideSubtitle: false + } + axisVisualization: { + x: { + isVisible: true + axisType: 2 + } + y: { + isVisible: true + axisType: 1 + } + } + } + } + } + } + { + name: 'sharedTimeRange' + isOptional: true + } + ] + #disable-next-line BCP036 + type: 'Extension/HubsExtension/PartType/MonitorChartPart' + settings: {} + } + } + { + position: { + x: 12 + y: 5 + colSpan: 4 + rowSpan: 3 + } + metadata: { + inputs: [ + { + name: 'options' + value: { + chart: { + metrics: [ + { + resourceMetadata: { + id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + } + name: 'exceptions/browser' + aggregationType: 7 + namespace: 'microsoft.insights/components' + metricVisualization: { + displayName: 'Browser exceptions' + color: '#47BDF5' + } + } + ] + title: 'Browser exceptions' + visualization: { + chartType: 2 + legendVisualization: { + isVisible: true + position: 2 + hideSubtitle: false + } + axisVisualization: { + x: { + isVisible: true + axisType: 2 + } + y: { + isVisible: true + axisType: 1 + } + } + } + } + } + } + { + name: 'sharedTimeRange' + isOptional: true + } + ] + #disable-next-line BCP036 + type: 'Extension/HubsExtension/PartType/MonitorChartPart' + settings: {} + } + } + { + position: { + x: 0 + y: 8 + colSpan: 4 + rowSpan: 3 + } + metadata: { + inputs: [ + { + name: 'options' + value: { + chart: { + metrics: [ + { + resourceMetadata: { + id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + } + name: 'availabilityResults/count' + aggregationType: 7 + namespace: 'microsoft.insights/components' + metricVisualization: { + displayName: 'Availability test results count' + color: '#47BDF5' + } + } + ] + title: 'Availability test results count' + visualization: { + chartType: 2 + legendVisualization: { + isVisible: true + position: 2 + hideSubtitle: false + } + axisVisualization: { + x: { + isVisible: true + axisType: 2 + } + y: { + isVisible: true + axisType: 1 + } + } + } + } + } + } + { + name: 'sharedTimeRange' + isOptional: true + } + ] + #disable-next-line BCP036 + type: 'Extension/HubsExtension/PartType/MonitorChartPart' + settings: {} + } + } + { + position: { + x: 4 + y: 8 + colSpan: 4 + rowSpan: 3 + } + metadata: { + inputs: [ + { + name: 'options' + value: { + chart: { + metrics: [ + { + resourceMetadata: { + id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + } + name: 'performanceCounters/processIOBytesPerSecond' + aggregationType: 4 + namespace: 'microsoft.insights/components' + metricVisualization: { + displayName: 'Process IO rate' + color: '#47BDF5' + } + } + ] + title: 'Average process I/O rate' + visualization: { + chartType: 2 + legendVisualization: { + isVisible: true + position: 2 + hideSubtitle: false + } + axisVisualization: { + x: { + isVisible: true + axisType: 2 + } + y: { + isVisible: true + axisType: 1 + } + } + } + } + } + } + { + name: 'sharedTimeRange' + isOptional: true + } + ] + #disable-next-line BCP036 + type: 'Extension/HubsExtension/PartType/MonitorChartPart' + settings: {} + } + } + { + position: { + x: 8 + y: 8 + colSpan: 4 + rowSpan: 3 + } + metadata: { + inputs: [ + { + name: 'options' + value: { + chart: { + metrics: [ + { + resourceMetadata: { + id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + } + name: 'performanceCounters/memoryAvailableBytes' + aggregationType: 4 + namespace: 'microsoft.insights/components' + metricVisualization: { + displayName: 'Available memory' + color: '#47BDF5' + } + } + ] + title: 'Average available memory' + visualization: { + chartType: 2 + legendVisualization: { + isVisible: true + position: 2 + hideSubtitle: false + } + axisVisualization: { + x: { + isVisible: true + axisType: 2 + } + y: { + isVisible: true + axisType: 1 + } + } + } + } + } + } + { + name: 'sharedTimeRange' + isOptional: true + } + ] + #disable-next-line BCP036 + type: 'Extension/HubsExtension/PartType/MonitorChartPart' + settings: {} + } + } + ] + } + ] + } +} + +resource applicationInsights 'Microsoft.Insights/components@2020-02-02' existing = { + name: applicationInsightsName +} diff --git a/infra/core/monitor/applicationinsights.bicep b/infra/core/monitor/applicationinsights.bicep new file mode 100644 index 00000000000..9cb814321c1 --- /dev/null +++ b/infra/core/monitor/applicationinsights.bicep @@ -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 diff --git a/infra/core/monitor/loganalytics.bicep b/infra/core/monitor/loganalytics.bicep new file mode 100644 index 00000000000..770544ccab4 --- /dev/null +++ b/infra/core/monitor/loganalytics.bicep @@ -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 diff --git a/infra/core/monitor/monitoring.bicep b/infra/core/monitor/monitoring.bicep new file mode 100644 index 00000000000..a56f9ecb1b4 --- /dev/null +++ b/infra/core/monitor/monitoring.bicep @@ -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 diff --git a/infra/core/storage/storage-account.bicep b/infra/core/storage/storage-account.bicep new file mode 100644 index 00000000000..75461a6542c --- /dev/null +++ b/infra/core/storage/storage-account.bicep @@ -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 diff --git a/infra/main.bicep b/infra/main.bicep new file mode 100644 index 00000000000..fdc51682042 --- /dev/null +++ b/infra/main.bicep @@ -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 + diff --git a/infra/main.parameters.json b/infra/main.parameters.json new file mode 100644 index 00000000000..05edbfaaee1 --- /dev/null +++ b/infra/main.parameters.json @@ -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}" + } + } +} \ No newline at end of file diff --git a/sk-azfunc-server/Activities/IssueActivities.cs b/sk-azfunc-server/Activities/IssueActivities.cs new file mode 100644 index 00000000000..300db30bbfe --- /dev/null +++ b/sk-azfunc-server/Activities/IssueActivities.cs @@ -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 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(); + + 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 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; + } + } +} diff --git a/sk-azfunc-server/Activities/MetadataActivities.cs b/sk-azfunc-server/Activities/MetadataActivities.cs new file mode 100644 index 00000000000..a48d6e91ca9 --- /dev/null +++ b/sk-azfunc-server/Activities/MetadataActivities.cs @@ -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 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(key, key); + var metadata = metadataResponse.Value; + return new OkObjectResult(metadata); + } + + [Function(nameof(SaveMetadata))] + + public static async Task SaveMetadata( + [ActivityTrigger] IssueMetadata metadata, + [TableInput("Metadata", Connection = "AzureWebJobsStorage")] TableClient client, + FunctionContext executionContext) + { + await client.UpsertEntityAsync(metadata); + return metadata; + } + } +} diff --git a/sk-azfunc-server/Activities/PullRequestActivities.cs b/sk-azfunc-server/Activities/PullRequestActivities.cs new file mode 100644 index 00000000000..aa911da576d --- /dev/null +++ b/sk-azfunc-server/Activities/PullRequestActivities.cs @@ -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 logger; + + public PullRequestActivities(IOptions azOptions, GithubService ghService, IHttpClientFactory httpClientFactory, ILogger logger) + { + _azSettings = azOptions.Value; + _ghService = ghService; + _httpClientFactory = httpClientFactory; + this.logger = logger; + } + + [Function(nameof(SaveOutput))] + public async Task 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 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 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 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 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(); + 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 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(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); + } + } + } + } +} \ No newline at end of file diff --git a/sk-azfunc-server/ExecuteFunctionEndpoint.cs b/sk-azfunc-server/ExecuteFunctionEndpoint.cs deleted file mode 100644 index b4850eff299..00000000000 --- a/sk-azfunc-server/ExecuteFunctionEndpoint.cs +++ /dev/null @@ -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 ExecuteFunctionAsync( - [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "skills/{skillName}/functions/{functionName}")] - HttpRequestData requestData, - FunctionContext executionContext, string skillName, string functionName) - { - try - { - var functionRequest = await JsonSerializer.DeserializeAsync(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 CreateResponseAsync(HttpRequestData requestData, HttpStatusCode statusCode, object responseBody) - { - var responseData = requestData.CreateResponse(statusCode); - await responseData.WriteAsJsonAsync(responseBody).ConfigureAwait(false); - return responseData; - } -} diff --git a/sk-azfunc-server/Models/AddToPRRequest.cs b/sk-azfunc-server/Models/AddToPRRequest.cs new file mode 100644 index 00000000000..198907945b8 --- /dev/null +++ b/sk-azfunc-server/Models/AddToPRRequest.cs @@ -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; } +} diff --git a/sk-azfunc-server/Models/CloseIssueRequest.cs b/sk-azfunc-server/Models/CloseIssueRequest.cs new file mode 100644 index 00000000000..be0bc28207b --- /dev/null +++ b/sk-azfunc-server/Models/CloseIssueRequest.cs @@ -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; } +} diff --git a/sk-azfunc-server/Models/ContainerInstanceMetadata.cs b/sk-azfunc-server/Models/ContainerInstanceMetadata.cs new file mode 100644 index 00000000000..eac5d4b8dc2 --- /dev/null +++ b/sk-azfunc-server/Models/ContainerInstanceMetadata.cs @@ -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; } +} diff --git a/sk-azfunc-server/Models/DevLeadPlanResponse.cs b/sk-azfunc-server/Models/DevLeadPlanResponse.cs new file mode 100644 index 00000000000..3c8c6a01c25 --- /dev/null +++ b/sk-azfunc-server/Models/DevLeadPlanResponse.cs @@ -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 subtasks { get; set; } +} + +public class DevLeadPlanResponse +{ + public List steps { get; set; } +} \ No newline at end of file diff --git a/sk-azfunc-server/Models/ErrorResponse.cs b/sk-azfunc-server/Models/ErrorResponse.cs deleted file mode 100644 index 5673ce32896..00000000000 --- a/sk-azfunc-server/Models/ErrorResponse.cs +++ /dev/null @@ -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; -} diff --git a/sk-azfunc-server/Models/ExecuteFunctionRequest.cs b/sk-azfunc-server/Models/ExecuteFunctionRequest.cs deleted file mode 100644 index 126708d04dd..00000000000 --- a/sk-azfunc-server/Models/ExecuteFunctionRequest.cs +++ /dev/null @@ -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 Variables { get; set; } = Enumerable.Empty(); -} - -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; -} diff --git a/sk-azfunc-server/Models/ExecuteFunctionResponse.cs b/sk-azfunc-server/Models/ExecuteFunctionResponse.cs deleted file mode 100644 index 0c3c076d745..00000000000 --- a/sk-azfunc-server/Models/ExecuteFunctionResponse.cs +++ /dev/null @@ -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; } -} diff --git a/sk-azfunc-server/Models/GHCommitRequest.cs b/sk-azfunc-server/Models/GHCommitRequest.cs new file mode 100644 index 00000000000..8d402c7ac9b --- /dev/null +++ b/sk-azfunc-server/Models/GHCommitRequest.cs @@ -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; } + } +} \ No newline at end of file diff --git a/sk-azfunc-server/Models/GHNewBranch.cs b/sk-azfunc-server/Models/GHNewBranch.cs new file mode 100644 index 00000000000..54f13091f23 --- /dev/null +++ b/sk-azfunc-server/Models/GHNewBranch.cs @@ -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; } + } +} \ No newline at end of file diff --git a/sk-azfunc-server/Models/IssueMetadata.cs b/sk-azfunc-server/Models/IssueMetadata.cs new file mode 100644 index 00000000000..6260ba0741b --- /dev/null +++ b/sk-azfunc-server/Models/IssueMetadata.cs @@ -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; } +} diff --git a/sk-azfunc-server/Models/IssueOrchestrationRequest.cs b/sk-azfunc-server/Models/IssueOrchestrationRequest.cs new file mode 100644 index 00000000000..987fd13a200 --- /dev/null +++ b/sk-azfunc-server/Models/IssueOrchestrationRequest.cs @@ -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}"; +} \ No newline at end of file diff --git a/sk-azfunc-server/Models/NewIssueRequest.cs b/sk-azfunc-server/Models/NewIssueRequest.cs new file mode 100644 index 00000000000..0e63c143c36 --- /dev/null +++ b/sk-azfunc-server/Models/NewIssueRequest.cs @@ -0,0 +1,6 @@ +public class NewIssueRequest +{ + public IssueOrchestrationRequest IssueRequest { get; set; } + public string Skill { get; set; } + public string Function { get; set; } +} diff --git a/sk-azfunc-server/Models/NewIssueResponse.cs b/sk-azfunc-server/Models/NewIssueResponse.cs new file mode 100644 index 00000000000..f871d6f5896 --- /dev/null +++ b/sk-azfunc-server/Models/NewIssueResponse.cs @@ -0,0 +1,5 @@ +public class NewIssueResponse +{ + public long Number { get; set; } + public int CommentId { get; set; } +} diff --git a/sk-azfunc-server/Models/RunAndSaveRequest.cs b/sk-azfunc-server/Models/RunAndSaveRequest.cs new file mode 100644 index 00000000000..20202ac8c8c --- /dev/null +++ b/sk-azfunc-server/Models/RunAndSaveRequest.cs @@ -0,0 +1,6 @@ +public class RunAndSaveRequest +{ + public IssueOrchestrationRequest Request { get; set; } + public string InstanceId { get; set; } + +} diff --git a/sk-azfunc-server/Models/RunInSandboxRequest.cs b/sk-azfunc-server/Models/RunInSandboxRequest.cs new file mode 100644 index 00000000000..62c2079f2d1 --- /dev/null +++ b/sk-azfunc-server/Models/RunInSandboxRequest.cs @@ -0,0 +1,8 @@ +namespace SK.DevTeam +{ + public class RunInSandboxRequest + { + public AddToPRRequest PrRequest { get; set; } + public string SanboxOrchestrationId { get; set; } + } +} \ No newline at end of file diff --git a/sk-azfunc-server/Models/SaveOutputRequest.cs b/sk-azfunc-server/Models/SaveOutputRequest.cs new file mode 100644 index 00000000000..9e7bb8507f3 --- /dev/null +++ b/sk-azfunc-server/Models/SaveOutputRequest.cs @@ -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; } + } +} diff --git a/sk-azfunc-server/Models/SkillRequest.cs b/sk-azfunc-server/Models/SkillRequest.cs new file mode 100644 index 00000000000..a8f372054f3 --- /dev/null +++ b/sk-azfunc-server/Models/SkillRequest.cs @@ -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; } + } +} \ No newline at end of file diff --git a/sk-azfunc-server/Models/SkillResponse.cs b/sk-azfunc-server/Models/SkillResponse.cs new file mode 100644 index 00000000000..bb9858ff976 --- /dev/null +++ b/sk-azfunc-server/Models/SkillResponse.cs @@ -0,0 +1,5 @@ +public class SkillResponse +{ + public T Output { get; set; } + public string SuborchestrationId { get; set; } +} \ No newline at end of file diff --git a/sk-azfunc-server/Orchestrators/IssueOrchestration.cs b/sk-azfunc-server/Orchestrators/IssueOrchestration.cs new file mode 100644 index 00000000000..03f24bdbb07 --- /dev/null +++ b/sk-azfunc-server/Orchestrators/IssueOrchestration.cs @@ -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 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(); + string instanceId = await client.ScheduleNewOrchestrationInstanceAsync( + nameof(IssueOrchestration), request); + + logger.LogInformation("Started orchestration with ID = '{instanceId}'.", instanceId); + return ""; + } + + [Function(nameof(IssueOrchestration))] + public static async Task> RunOrchestrator( + [OrchestrationTrigger] TaskOrchestrationContext context, IssueOrchestrationRequest request) + { + var logger = context.CreateReplaySafeLogger(nameof(IssueOrchestration)); + var outputs = new List(); + + var newGHBranchRequest = new GHNewBranch + { + Org = request.Org, + Repo = request.Repo, + Branch = request.Branch, + Number = request.Number + }; + + var newBranch = await context.CallActivityAsync(nameof(PullRequestActivities.CreateBranch), newGHBranchRequest); + + var readmeTask = await context.CallSubOrchestratorAsync(nameof(ReadmeAndSave), new RunAndSaveRequest + { + Request = request, + InstanceId = context.InstanceId + }); + + var newPR = await context.CallActivityAsync(nameof(PullRequestActivities.CreatePR), newGHBranchRequest); + + var planTask = await context.CallSubOrchestratorAsync>(nameof(CreatePlan), request); + var plan = JsonSerializer.Deserialize(planTask.Output); + + var implementationTasks = plan.steps.SelectMany(s => s.subtasks.Select(st => + context.CallSubOrchestratorAsync(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; + } + } +} \ No newline at end of file diff --git a/sk-azfunc-server/Orchestrators/SubIssueOrchestration.cs b/sk-azfunc-server/Orchestrators/SubIssueOrchestration.cs new file mode 100644 index 00000000000..ebacb221091 --- /dev/null +++ b/sk-azfunc-server/Orchestrators/SubIssueOrchestration.cs @@ -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> CallSkill(TaskOrchestrationContext context, SkillRequest request) + { + var newIssueResponse = await context.CallActivityAsync(nameof(IssuesActivities.CreateIssue), new NewIssueRequest + { + IssueRequest = request.IssueRequest, + Skill = request.Skill, + Function = request.Function + }); + + var metadata = await context.CallActivityAsync(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(IssueClosed); + var lastComment = await context.CallActivityAsync(nameof(IssuesActivities.GetLastComment), new IssueOrchestrationRequest + { + Org = request.IssueRequest.Org, + Repo = request.IssueRequest.Repo, + Number = newIssueResponse.Number + }); + + return new SkillResponse { Output = lastComment, SuborchestrationId = context.InstanceId }; + } + + [Function(nameof(CreateReadme))] + public static async Task> 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> 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> 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 ImplementAndSave( + [OrchestrationTrigger] TaskOrchestrationContext context, RunAndSaveRequest request) + { + var implementResult = await context.CallSubOrchestratorAsync>(nameof(Implement), request.Request); + await context.CallSubOrchestratorAsync(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 ReadmeAndSave( + [OrchestrationTrigger] TaskOrchestrationContext context, RunAndSaveRequest request) + { + var readmeResult = await context.CallSubOrchestratorAsync>(nameof(CreateReadme), request.Request); + context.CallSubOrchestratorAsync(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 AddToPR( + [OrchestrationTrigger] TaskOrchestrationContext context, AddToPRRequest request) + { + var saveScriptResponse = await context.CallActivityAsync(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(nameof(PullRequestActivities.RunInSandbox), newRequest); + bool containerTerminated = await context.WaitForExternalEvent(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(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; + } + } +} \ No newline at end of file diff --git a/sk-azfunc-server/Program.cs b/sk-azfunc-server/Program.cs index 4b154eb80b4..a04ed3fe570 100644 --- a/sk-azfunc-server/Program.cs +++ b/sk-azfunc-server/Program.cs @@ -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(_ => s_apiConfigOptions); services.AddTransient((provider) => CreateKernel(provider)); - - + services.AddScoped(); + services.AddScoped(); + services.AddOptions() + .Configure((settings, configuration) => + { + configuration.GetSection("GithubOptions").Bind(settings); + }); + services.AddOptions() + .Configure((settings, configuration) => + { + configuration.GetSection("AzureOptions").Bind(settings); + }); + services.AddOptions() + .Configure((settings, configuration) => + { + configuration.GetSection("OpenAIOptions").Bind(settings); + }); + services.AddOptions() + .Configure((settings, configuration) => + { + configuration.GetSection("QdrantOptions").Bind(settings); + }); + services.AddApplicationInsightsTelemetryWorkerService(); + services.ConfigureFunctionsApplicationInsights(); // return JSON with expected lowercase naming services.Configure(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(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 void Main() private static IKernel CreateKernel(IServiceProvider provider) { - var kernelSettings = KernelSettings.LoadSettings(); + var openAiConfig = provider.GetService>().Value; + var qdrantConfig = provider.GetService>().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()).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()) + .WithAzureChatCompletionService(openAiConfig.DeploymentOrModelId, openAiConfig.Endpoint, openAiConfig.ApiKey, true, openAiConfig.ServiceId, true) + .WithMemory(semanticTextMemory) + .WithConfiguration(kernelConfig).Build(); + } } diff --git a/sk-azfunc-server/README.md b/sk-azfunc-server/README.md index e96323b2f20..a44f1d992b1 100644 --- a/sk-azfunc-server/README.md +++ b/sk-azfunc-server/README.md @@ -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) diff --git a/sk-azfunc-server/Services/GithubService.cs b/sk-azfunc-server/Services/GithubService.cs new file mode 100644 index 00000000000..ce5119c5713 --- /dev/null +++ b/sk-azfunc-server/Services/GithubService.cs @@ -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 ghOptions) + { + _githubSettings = ghOptions.Value; + } + public async Task 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) + }; + } +} \ No newline at end of file diff --git a/sk-azfunc-server/Services/WebHookEventProcessor.cs b/sk-azfunc-server/Services/WebHookEventProcessor.cs new file mode 100644 index 00000000000..ba35597c181 --- /dev/null +++ b/sk-azfunc-server/Services/WebHookEventProcessor.cs @@ -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 _logger; + private readonly GithubService _ghService; + private readonly IHttpClientFactory _httpClientFactory; + + public SKWebHookEventProcessor(IKernel kernel, ILogger 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($"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 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(); + } +} \ No newline at end of file diff --git a/sk-azfunc-server/config/AzureOptions.cs b/sk-azfunc-server/config/AzureOptions.cs new file mode 100644 index 00000000000..393e56d3e76 --- /dev/null +++ b/sk-azfunc-server/config/AzureOptions.cs @@ -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; } +} \ No newline at end of file diff --git a/sk-azfunc-server/config/GithubOptions.cs b/sk-azfunc-server/config/GithubOptions.cs new file mode 100644 index 00000000000..90ed5cdc04c --- /dev/null +++ b/sk-azfunc-server/config/GithubOptions.cs @@ -0,0 +1,6 @@ +public class GithubOptions +{ + public string AppKey { get; set; } + public int AppId { get; set; } + public long InstallationId { get; set; } +} diff --git a/sk-azfunc-server/config/KernelConfigExtensions.cs b/sk-azfunc-server/config/KernelConfigExtensions.cs deleted file mode 100644 index 3c18d79670f..00000000000 --- a/sk-azfunc-server/config/KernelConfigExtensions.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.AI.ChatCompletion; - -internal static class KernelConfigExtensions -{ - /// - /// Adds a text completion service to the list. It can be either an OpenAI or Azure OpenAI backend service. - /// - /// - /// - /// - 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}"); - } - } -} diff --git a/sk-azfunc-server/config/KernelSettings.cs b/sk-azfunc-server/config/KernelSettings.cs deleted file mode 100644 index c3a0de141fc..00000000000 --- a/sk-azfunc-server/config/KernelSettings.cs +++ /dev/null @@ -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; } - - /// - /// Load the kernel settings from settings.json if the file exists and if not attempt to use user secrets. - /// - 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); - } - } - - /// - /// Load the kernel settings from the specified configuration file if it exists. - /// - 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() - ?? throw new InvalidDataException($"Invalid semantic kernel settings in '{configFile}', please provide configuration settings using instructions in the README."); - } - - /// - /// Load the kernel settings from user secrets. - /// - internal static KernelSettings FromUserSecrets() - { - var configuration = new ConfigurationBuilder() - .AddUserSecrets() - .AddEnvironmentVariables() - .Build(); - - return configuration.Get() - ?? throw new InvalidDataException("Invalid semantic kernel settings in user secrets, please provide configuration settings using instructions in the README."); - } -} diff --git a/sk-azfunc-server/config/OpenAIOptions.cs b/sk-azfunc-server/config/OpenAIOptions.cs new file mode 100644 index 00000000000..188f33c4e4c --- /dev/null +++ b/sk-azfunc-server/config/OpenAIOptions.cs @@ -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; } +} \ No newline at end of file diff --git a/sk-azfunc-server/config/QdrantOptions.cs b/sk-azfunc-server/config/QdrantOptions.cs new file mode 100644 index 00000000000..cebef7eb4f3 --- /dev/null +++ b/sk-azfunc-server/config/QdrantOptions.cs @@ -0,0 +1,5 @@ +public class QdrantOptions +{ + public string Endpoint { get; set; } + public int VectorSize { get; set; } +} \ No newline at end of file diff --git a/sk-azfunc-server/config/appsettings-notyet.json b/sk-azfunc-server/config/appsettings-notyet.json deleted file mode 100644 index 87c5d1e6e98..00000000000 --- a/sk-azfunc-server/config/appsettings-notyet.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "serviceType": "AzureOpenAI", - "serviceId": "gpt-4", - "deploymentOrModelId": "gpt-4", - "endpoint": "https://lightspeed-team-shared-openai-eastus.openai.azure.com/", -} \ No newline at end of file diff --git a/sk-azfunc-server/config/appsettings.json.azure-example b/sk-azfunc-server/config/appsettings.json.azure-example deleted file mode 100644 index fb63bb6dd5a..00000000000 --- a/sk-azfunc-server/config/appsettings.json.azure-example +++ /dev/null @@ -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 ..." -} \ No newline at end of file diff --git a/sk-azfunc-server/config/appsettings.json.openai-example b/sk-azfunc-server/config/appsettings.json.openai-example deleted file mode 100644 index f4472a25c9f..00000000000 --- a/sk-azfunc-server/config/appsettings.json.openai-example +++ /dev/null @@ -1,7 +0,0 @@ -{ - "serviceType": "OpenAI", - "serviceId": "text-davinci-003", - "deploymentOrModelId": "text-davinci-003", - "apiKey": "... your OpenAI key ...", - "orgId": "" -} \ No newline at end of file diff --git a/sk-azfunc-server/host.json b/sk-azfunc-server/host.json index 278b52cdee3..6b68371f9dc 100644 --- a/sk-azfunc-server/host.json +++ b/sk-azfunc-server/host.json @@ -7,5 +7,12 @@ "excludedTypes": "Request" } } + }, + "extensions": { + "durableTask": { + "storageProvider": { + "type": "AzureStorage" + } + } } } \ No newline at end of file diff --git a/sk-azfunc-server/local.settings.json b/sk-azfunc-server/local.settings.json index f7a08ea5367..95443230e1e 100644 --- a/sk-azfunc-server/local.settings.json +++ b/sk-azfunc-server/local.settings.json @@ -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" } -} \ No newline at end of file +} + diff --git a/sk-azfunc-server/local.settings.template.json b/sk-azfunc-server/local.settings.template.json new file mode 100644 index 00000000000..905ff383f53 --- /dev/null +++ b/sk-azfunc-server/local.settings.template.json @@ -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" + } +} + diff --git a/sk-azfunc-server/sk-csharp-azure-functions.csproj b/sk-azfunc-server/sk-csharp-azure-functions.csproj index 602415f8641..8bee3639321 100644 --- a/sk-azfunc-server/sk-csharp-azure-functions.csproj +++ b/sk-azfunc-server/sk-csharp-azure-functions.csproj @@ -17,13 +17,26 @@ + + + + - - - - - - + + + + + + + + + + + + + + + diff --git a/skills/DevLead.cs b/skills/DevLead.cs index fd7790399f0..86467c6aa02 100644 --- a/skills/DevLead.cs +++ b/skills/DevLead.cs @@ -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}} diff --git a/skills/skills.csproj b/skills/skills.csproj index eb4501ad295..2cb017c9f4d 100644 --- a/skills/skills.csproj +++ b/skills/skills.csproj @@ -7,6 +7,6 @@ - + diff --git a/util/seed-memory/Program.cs b/util/seed-memory/Program.cs index 73cc3de3fc3..f5d59f4ad87 100644 --- a/util/seed-memory/Program.cs +++ b/util/seed-memory/Program.cs @@ -25,7 +25,7 @@ static async Task Main(string[] args) .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 @@ public static async Task ImportDocumentAsync(IKernel kernel, string filename) 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); + } } } } \ No newline at end of file diff --git a/util/seed-memory/config/KernelSettings.cs b/util/seed-memory/config/KernelSettings.cs index 042945c868e..a21061443ed 100644 --- a/util/seed-memory/config/KernelSettings.cs +++ b/util/seed-memory/config/KernelSettings.cs @@ -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 static KernelSettings FromFile(string configFile = DefaultConfigFile) var configuration = new ConfigurationBuilder() .SetBasePath(System.IO.Directory.GetCurrentDirectory()) .AddJsonFile(configFile, optional: true, reloadOnChange: true) + .AddEnvironmentVariables() .Build(); return configuration.Get() diff --git a/util/seed-memory/config/appsettings.template.json b/util/seed-memory/config/appsettings.template.json new file mode 100644 index 00000000000..3fe9615a4b4 --- /dev/null +++ b/util/seed-memory/config/appsettings.template.json @@ -0,0 +1,9 @@ +{ + "serviceType": "AzureOpenAI", + "serviceId": "", + "deploymentOrModelId": "", + "embeddingDeploymentOrModelId": "", + "endpoint": "", + "apiKey": "", + "qdrantEndoint": "" +} \ No newline at end of file diff --git a/util/seed-memory/seed-memory.csproj b/util/seed-memory/seed-memory.csproj index e36401b3741..500e84ac712 100644 --- a/util/seed-memory/seed-memory.csproj +++ b/util/seed-memory/seed-memory.csproj @@ -12,8 +12,8 @@ - - + +