diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 530e27a5744..a39ffcf00cc 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -15,7 +15,8 @@ "ghcr.io/azure/azure-dev/azd:latest": {}, "ghcr.io/devcontainers/features/node:1": {}, "ghcr.io/azure/azure-dev/azd:0": {}, - "ghcr.io/stuartleeks/dev-container-features/dev-tunnels:0": {} + "ghcr.io/stuartleeks/dev-container-features/dev-tunnels:0": {}, + "ghcr.io/dapr/cli/dapr-cli:0": {} }, "postCreateCommand": "bash .devcontainer/startup.sh", "hostRequirements": { @@ -48,10 +49,11 @@ "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" + "ms-azuretools.azure-dev", + "ms-azuretools.vscode-azurefunctions", + "ms-azuretools.vscode-bicep", + "ms-dotnettools.vscode-dotnet-runtime", + "ms-azuretools.vscode-dapr" ] } } diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml index c09e20aedfa..7b1d581f25c 100644 --- a/.devcontainer/docker-compose.yml +++ b/.devcontainer/docker-compose.yml @@ -10,19 +10,21 @@ services: - ..:/workspace:cached # Overrides default command so things don't shut down after the process ends. command: sleep infinity - network_mode: service:cosmos + network_mode: service:qdrant depends_on: - - cosmos - cosmos: - image: mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator - mem_limit: "3g" - cpu_count: 2 - init: true - restart: unless-stopped - environment: - AZURE_COSMOS_EMULATOR_PARTITION_COUNT: "10" - AZURE_COSMOS_EMULATOR_ENABLE_DATA_PERSISTENCE: "true" + - qdrant qdrant: image: qdrant/qdrant ports: - 6333:6333 + redis: + image: redis:alpine + network_mode: service:qdrant + zipkin: + image: openzipkin/zipkin-slim:latest + ports: + - 9411:9411 + dapr-placement: + image: "daprio/dapr" + command: ["./placement", "--port", "50005"] + network_mode: service:qdrant \ No newline at end of file diff --git a/.devcontainer/startup.sh b/.devcontainer/startup.sh index c2c069ec5e9..94a6743641f 100644 --- a/.devcontainer/startup.sh +++ b/.devcontainer/startup.sh @@ -4,5 +4,6 @@ # sudo cp ~/emulatorcert.crt /usr/local/share/ca-certificates/ # sudo update-ca-certificates # sleep 10 -dotnet restore sk-dev-team.sln -# dotnet build util/seed-memory/seed-memory.csproj && dotnet util/seed-memory/bin/Debug/net7.0/seed-memory.dll \ No newline at end of file +dotnet restore Microsoft.AI.Agents.sln + +dapr init --slim \ No newline at end of file diff --git a/.vscode/extensions.json b/.vscode/extensions.json deleted file mode 100644 index bb763007149..00000000000 --- a/.vscode/extensions.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "recommendations": [ - "ms-azuretools.vscode-azurefunctions", - "ms-dotnettools.csharp" - ] -} \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json index f3ac2552498..58dc0dde192 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,18 +1,12 @@ { - "version": "0.2.0", - "configurations": [ - { - "name": "Attach to .NET Functions", - "type": "coreclr", - "request": "attach", - "processId": "${command:azureFunctions.pickProcess}" - }, - { - "name": "Attach to Node Functions", - "type": "node", - "request": "attach", - "port": 9229, - "preLaunchTask": "func: host start" - } - ] + "configurations": [ + { + "name": "C#: Microsoft.AI.DevTeam.Dapr [Default Configuration] with Dapr", + "type": "dotnet", + "request": "launch", + "projectPath": "${workspaceFolder}/samples/gh-flow/src/Microsoft.AI.DevTeam.Dapr/Microsoft.AI.DevTeam.Dapr.csproj", + "preLaunchTask": "dapr-debug", + "postDebugTask": "daprd-down" + } + ] } \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index a13b8792cc4..2892eed21d9 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,11 +1,4 @@ { - "dotnet.defaultSolution": "sk-dev-team.sln", - "azureFunctions.deploySubpath": "src/apps/ingest", - "azureFunctions.projectLanguage": "JavaScript", - "azureFunctions.projectRuntime": "~4", + "dotnet.defaultSolution": "Microsoft.AI.Agents.sln", "debug.internalConsoleOptions": "neverOpen", - "azureFunctions.preDeployTask": "npm prune (functions)", - "azureFunctions.postDeployTask": "npm install (functions)", - "azureFunctions.projectLanguageModel": 4, - "azureFunctions.projectSubpath": "src/apps/ingest" } \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json index ef996066e6b..3d6f923ec6b 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -1,99 +1,19 @@ { - "version": "2.0.0", - "tasks": [ - { - "label": "clean (functions)", - "command": "dotnet", - "args": [ - "clean", - "/property:GenerateFullPaths=true", - "/consoleloggerparameters:NoSummary" - ], - "type": "process", - "problemMatcher": "$msCompile", - "options": { - "cwd": "${workspaceFolder}/sk-azfunc-server" - } - }, - { - "label": "build (functions)", - "command": "dotnet", - "args": [ - "build", - "/property:GenerateFullPaths=true", - "/consoleloggerparameters:NoSummary" - ], - "type": "process", - "dependsOn": "clean (functions)", - "group": { - "kind": "build", - "isDefault": true - }, - "problemMatcher": "$msCompile", - "options": { - "cwd": "${workspaceFolder}/sk-azfunc-server" - } - }, - { - "label": "clean release (functions)", - "command": "dotnet", - "args": [ - "clean", - "--configuration", - "Release", - "/property:GenerateFullPaths=true", - "/consoleloggerparameters:NoSummary" - ], - "type": "process", - "problemMatcher": "$msCompile", - "options": { - "cwd": "${workspaceFolder}/sk-azfunc-server" - } - }, - { - "label": "publish (functions)", - "command": "dotnet", - "args": [ - "publish", - "--configuration", - "Release", - "/property:GenerateFullPaths=true", - "/consoleloggerparameters:NoSummary" - ], - "type": "process", - "dependsOn": "clean release (functions)", - "problemMatcher": "$msCompile", - "options": { - "cwd": "${workspaceFolder}/sk-azfunc-server" - } - }, - { - "type": "func", - "label": "func: host start", - "command": "host start", - "problemMatcher": "$func-node-watch", - "isBackground": true, - "dependsOn": "npm install (functions)", - "options": { - "cwd": "${workspaceFolder}/src/apps/ingest" - } - }, - { - "type": "shell", - "label": "npm install (functions)", - "command": "npm install", - "options": { - "cwd": "${workspaceFolder}/src/apps/ingest" - } - }, - { - "type": "shell", - "label": "npm prune (functions)", - "command": "npm prune --production", - "problemMatcher": [], - "options": { - "cwd": "${workspaceFolder}/src/apps/ingest" - } - } - ] + "version": "2.0.0", + "tasks": [ + { + "appId": "dev-agents", + "grpcPort": 50001, + "httpPort": 3500, + "appPort": 5244, + "componentsPath": "samples/gh-flow/components", + "label": "dapr-debug", + "type": "dapr" + }, + { + "appId": "dev-agents", + "label": "daprd-down", + "type": "daprd-down" + } + ] } \ No newline at end of file diff --git a/Microsoft.AI.Agents.sln b/Microsoft.AI.Agents.sln index a5ad646541b..e9be61c7b02 100644 --- a/Microsoft.AI.Agents.sln +++ b/Microsoft.AI.Agents.sln @@ -1,29 +1,32 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 +Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.0.31903.59 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{7EC6823E-99FB-403F-9941-0CB08C6BFFD4}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{290F9824-BAD3-4703-B9B7-FE9C4BE3A1CF}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AI.Agents", "src\Microsoft.AI.Agents\Microsoft.AI.Agents.csproj", "{160AE89B-16BE-4FB8-8FEC-255E11D6E860}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AI.Agents", "src\Microsoft.AI.Agents\Microsoft.AI.Agents.csproj", "{B0BF1CD6-34E3-4ED4-9B2A-9B8781E9BE20}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{17FFA72B-390A-4BF6-A277-2099897D2132}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AI.Agents.Dapr", "src\Microsoft.AI.Agents.Dapr\Microsoft.AI.Agents.Dapr.csproj", "{1972846E-4C21-4E40-B448-D78B73806BD9}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "gh-flow", "gh-flow", "{0C7D5C39-2067-4EF1-9C10-72A847FF9CB3}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AI.Agents.Orleans", "src\Microsoft.AI.Agents.Orleans\Microsoft.AI.Agents.Orleans.csproj", "{A4AE4656-4919-45E2-9680-C317FBCF7693}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{DADD59B8-4B8B-4F2B-BFEB-0F93B8A6AA55}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{943853E7-513D-45EA-870F-549CFC0AF8E8}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AI.DevTeam", "samples\gh-flow\src\Microsoft.AI.DevTeam\Microsoft.AI.DevTeam.csproj", "{F02CC33B-B479-480B-B886-25ED0E18494A}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "gh-flow", "gh-flow", "{E0E93575-7187-4975-8D72-6F285CD01767}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "seed-memory", "samples\seed-memory\seed-memory.csproj", "{A1EB6145-FE32-45EC-8A1B-A76268B6DFDE}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{50809508-F830-4553-9C4E-C802E0A0F690}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WorkflowsApp", "samples\WorkflowsApp\WorkflowsApp.csproj", "{3EE3F061-CC63-41B0-84BA-C95A256E18A9}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AI.DevTeam", "samples\gh-flow\src\Microsoft.AI.DevTeam\Microsoft.AI.DevTeam.csproj", "{79981945-61F7-4E1A-8949-7808FD75471B}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AI.Agents.Orleans", "src\Microsoft.AI.Agents.Orleans\Microsoft.AI.Agents.Orleans.csproj", "{12381ECC-7ECD-4D13-911B-83A07BB58134}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AI.DevTeam.Dapr", "samples\gh-flow\src\Microsoft.AI.DevTeam.Dapr\Microsoft.AI.DevTeam.Dapr.csproj", "{A7677950-18F1-42FF-8018-870395417465}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AI.Agents.Dapr", "src\Microsoft.AI.Agents.Dapr\Microsoft.AI.Agents.Dapr.csproj", "{6CCA9961-D498-4E59-8781-D94AE8228200}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "marketing", "marketing", "{1FF691E4-E27D-4A7E-861C-4D6291B6EE35}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Marketing", "samples\marketing\src\backend\Marketing.csproj", "{71D20402-E03C-43EA-BFA2-DF16B32D623C}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{4225F3BA-A39D-4680-945E-F2869E98AEA2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Marketing", "samples\marketing\src\backend\Marketing.csproj", "{62F276F3-9184-4908-A7FB-065B4E491BE2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "seed-memory", "samples\gh-flow\src\seed-memory\seed-memory.csproj", "{EF5DF177-F4F2-49D5-9E1C-2E37869238D8}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -31,47 +34,49 @@ Global Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {160AE89B-16BE-4FB8-8FEC-255E11D6E860}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {160AE89B-16BE-4FB8-8FEC-255E11D6E860}.Debug|Any CPU.Build.0 = Debug|Any CPU - {160AE89B-16BE-4FB8-8FEC-255E11D6E860}.Release|Any CPU.ActiveCfg = Release|Any CPU - {160AE89B-16BE-4FB8-8FEC-255E11D6E860}.Release|Any CPU.Build.0 = Release|Any CPU - {F02CC33B-B479-480B-B886-25ED0E18494A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F02CC33B-B479-480B-B886-25ED0E18494A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F02CC33B-B479-480B-B886-25ED0E18494A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F02CC33B-B479-480B-B886-25ED0E18494A}.Release|Any CPU.Build.0 = Release|Any CPU - {A1EB6145-FE32-45EC-8A1B-A76268B6DFDE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A1EB6145-FE32-45EC-8A1B-A76268B6DFDE}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A1EB6145-FE32-45EC-8A1B-A76268B6DFDE}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A1EB6145-FE32-45EC-8A1B-A76268B6DFDE}.Release|Any CPU.Build.0 = Release|Any CPU - {3EE3F061-CC63-41B0-84BA-C95A256E18A9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {3EE3F061-CC63-41B0-84BA-C95A256E18A9}.Debug|Any CPU.Build.0 = Debug|Any CPU - {3EE3F061-CC63-41B0-84BA-C95A256E18A9}.Release|Any CPU.ActiveCfg = Release|Any CPU - {3EE3F061-CC63-41B0-84BA-C95A256E18A9}.Release|Any CPU.Build.0 = Release|Any CPU - {12381ECC-7ECD-4D13-911B-83A07BB58134}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {12381ECC-7ECD-4D13-911B-83A07BB58134}.Debug|Any CPU.Build.0 = Debug|Any CPU - {12381ECC-7ECD-4D13-911B-83A07BB58134}.Release|Any CPU.ActiveCfg = Release|Any CPU - {12381ECC-7ECD-4D13-911B-83A07BB58134}.Release|Any CPU.Build.0 = Release|Any CPU - {6CCA9961-D498-4E59-8781-D94AE8228200}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6CCA9961-D498-4E59-8781-D94AE8228200}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6CCA9961-D498-4E59-8781-D94AE8228200}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6CCA9961-D498-4E59-8781-D94AE8228200}.Release|Any CPU.Build.0 = Release|Any CPU - {71D20402-E03C-43EA-BFA2-DF16B32D623C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {71D20402-E03C-43EA-BFA2-DF16B32D623C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {71D20402-E03C-43EA-BFA2-DF16B32D623C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {71D20402-E03C-43EA-BFA2-DF16B32D623C}.Release|Any CPU.Build.0 = Release|Any CPU + {B0BF1CD6-34E3-4ED4-9B2A-9B8781E9BE20}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B0BF1CD6-34E3-4ED4-9B2A-9B8781E9BE20}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B0BF1CD6-34E3-4ED4-9B2A-9B8781E9BE20}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B0BF1CD6-34E3-4ED4-9B2A-9B8781E9BE20}.Release|Any CPU.Build.0 = Release|Any CPU + {1972846E-4C21-4E40-B448-D78B73806BD9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1972846E-4C21-4E40-B448-D78B73806BD9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1972846E-4C21-4E40-B448-D78B73806BD9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1972846E-4C21-4E40-B448-D78B73806BD9}.Release|Any CPU.Build.0 = Release|Any CPU + {A4AE4656-4919-45E2-9680-C317FBCF7693}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A4AE4656-4919-45E2-9680-C317FBCF7693}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A4AE4656-4919-45E2-9680-C317FBCF7693}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A4AE4656-4919-45E2-9680-C317FBCF7693}.Release|Any CPU.Build.0 = Release|Any CPU + {79981945-61F7-4E1A-8949-7808FD75471B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {79981945-61F7-4E1A-8949-7808FD75471B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {79981945-61F7-4E1A-8949-7808FD75471B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {79981945-61F7-4E1A-8949-7808FD75471B}.Release|Any CPU.Build.0 = Release|Any CPU + {A7677950-18F1-42FF-8018-870395417465}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A7677950-18F1-42FF-8018-870395417465}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A7677950-18F1-42FF-8018-870395417465}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A7677950-18F1-42FF-8018-870395417465}.Release|Any CPU.Build.0 = Release|Any CPU + {62F276F3-9184-4908-A7FB-065B4E491BE2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {62F276F3-9184-4908-A7FB-065B4E491BE2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {62F276F3-9184-4908-A7FB-065B4E491BE2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {62F276F3-9184-4908-A7FB-065B4E491BE2}.Release|Any CPU.Build.0 = Release|Any CPU + {EF5DF177-F4F2-49D5-9E1C-2E37869238D8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EF5DF177-F4F2-49D5-9E1C-2E37869238D8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EF5DF177-F4F2-49D5-9E1C-2E37869238D8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EF5DF177-F4F2-49D5-9E1C-2E37869238D8}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution - {160AE89B-16BE-4FB8-8FEC-255E11D6E860} = {7EC6823E-99FB-403F-9941-0CB08C6BFFD4} - {0C7D5C39-2067-4EF1-9C10-72A847FF9CB3} = {17FFA72B-390A-4BF6-A277-2099897D2132} - {DADD59B8-4B8B-4F2B-BFEB-0F93B8A6AA55} = {0C7D5C39-2067-4EF1-9C10-72A847FF9CB3} - {F02CC33B-B479-480B-B886-25ED0E18494A} = {DADD59B8-4B8B-4F2B-BFEB-0F93B8A6AA55} - {A1EB6145-FE32-45EC-8A1B-A76268B6DFDE} = {17FFA72B-390A-4BF6-A277-2099897D2132} - {3EE3F061-CC63-41B0-84BA-C95A256E18A9} = {17FFA72B-390A-4BF6-A277-2099897D2132} - {12381ECC-7ECD-4D13-911B-83A07BB58134} = {7EC6823E-99FB-403F-9941-0CB08C6BFFD4} - {6CCA9961-D498-4E59-8781-D94AE8228200} = {7EC6823E-99FB-403F-9941-0CB08C6BFFD4} - {71D20402-E03C-43EA-BFA2-DF16B32D623C} = {17FFA72B-390A-4BF6-A277-2099897D2132} + {B0BF1CD6-34E3-4ED4-9B2A-9B8781E9BE20} = {290F9824-BAD3-4703-B9B7-FE9C4BE3A1CF} + {1972846E-4C21-4E40-B448-D78B73806BD9} = {290F9824-BAD3-4703-B9B7-FE9C4BE3A1CF} + {A4AE4656-4919-45E2-9680-C317FBCF7693} = {290F9824-BAD3-4703-B9B7-FE9C4BE3A1CF} + {E0E93575-7187-4975-8D72-6F285CD01767} = {943853E7-513D-45EA-870F-549CFC0AF8E8} + {50809508-F830-4553-9C4E-C802E0A0F690} = {E0E93575-7187-4975-8D72-6F285CD01767} + {79981945-61F7-4E1A-8949-7808FD75471B} = {50809508-F830-4553-9C4E-C802E0A0F690} + {A7677950-18F1-42FF-8018-870395417465} = {50809508-F830-4553-9C4E-C802E0A0F690} + {1FF691E4-E27D-4A7E-861C-4D6291B6EE35} = {943853E7-513D-45EA-870F-549CFC0AF8E8} + {4225F3BA-A39D-4680-945E-F2869E98AEA2} = {1FF691E4-E27D-4A7E-861C-4D6291B6EE35} + {62F276F3-9184-4908-A7FB-065B4E491BE2} = {4225F3BA-A39D-4680-945E-F2869E98AEA2} + {EF5DF177-F4F2-49D5-9E1C-2E37869238D8} = {943853E7-513D-45EA-870F-549CFC0AF8E8} EndGlobalSection EndGlobal diff --git a/samples/gh-flow/components/pubsub.yaml b/samples/gh-flow/components/pubsub.yaml new file mode 100644 index 00000000000..5aec174af9f --- /dev/null +++ b/samples/gh-flow/components/pubsub.yaml @@ -0,0 +1,12 @@ +apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + name: agents-pubsub +spec: + type: pubsub.redis + version: v1 + metadata: + - name: redisHost + value: localhost:6379 + - name: redisPassword + value: "" \ No newline at end of file diff --git a/samples/gh-flow/components/statestore.yaml b/samples/gh-flow/components/statestore.yaml new file mode 100644 index 00000000000..9693888f268 --- /dev/null +++ b/samples/gh-flow/components/statestore.yaml @@ -0,0 +1,14 @@ +apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + name: agents-statestore +spec: + type: state.redis + version: v1 + metadata: + - name: redisHost + value: localhost:6379 + - name: redisPassword + value: "" + - name: actorStateStore + value: "true" diff --git a/samples/gh-flow/src/Microsoft.AI.DevTeam.Dapr/Agents/Architect/Architect.cs b/samples/gh-flow/src/Microsoft.AI.DevTeam.Dapr/Agents/Architect/Architect.cs new file mode 100644 index 00000000000..66812798aa8 --- /dev/null +++ b/samples/gh-flow/src/Microsoft.AI.DevTeam.Dapr/Agents/Architect/Architect.cs @@ -0,0 +1,29 @@ +using Dapr.Actors.Runtime; +using Dapr.Client; +using Microsoft.AI.Agents.Abstractions; +using Microsoft.AI.Agents.Dapr; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Memory; + +namespace Microsoft.AI.DevTeam.Dapr; + + +// The architect has Org+Repo scope and is holding the knowledge of the high level architecture of the project +public class Architect : AiAgent,IDaprAgent +{ + public Architect(ActorHost host, DaprClient client, ISemanticTextMemory memory, Kernel kernel) + : base(host, client, memory, kernel) + { + } + + public override Task HandleEvent(Event item) + { + return Task.CompletedTask; + } +} + +public class ArchitectState +{ + public string FilesTree { get; set; } + public string HighLevelArchitecture { get; set; } +} \ No newline at end of file diff --git a/samples/gh-flow/src/Microsoft.AI.DevTeam.Dapr/Agents/AzureGenie.cs b/samples/gh-flow/src/Microsoft.AI.DevTeam.Dapr/Agents/AzureGenie.cs new file mode 100644 index 00000000000..c42ddaac111 --- /dev/null +++ b/samples/gh-flow/src/Microsoft.AI.DevTeam.Dapr/Agents/AzureGenie.cs @@ -0,0 +1,64 @@ +using Dapr.Actors; +using Dapr.Actors.Runtime; +using Dapr.Client; +using Microsoft.AI.Agents.Abstractions; +using Microsoft.AI.Agents.Dapr; +using Microsoft.AI.DevTeam.Dapr.Events; + +namespace Microsoft.AI.DevTeam.Dapr; + +public class AzureGenie : Agent, IDaprAgent +{ + private readonly IManageAzure _azureService; + + public AzureGenie(ActorHost host,DaprClient client, IManageAzure azureService) : base(host, client) + { + _azureService = azureService; + } + + public override async Task HandleEvent(Event item) + { + switch (item.Type) + { + case nameof(GithubFlowEventType.ReadmeCreated): + { + var context = item.ToGithubContext(); + await Store(context.Org,context.Repo, context.ParentNumber.Value, context.IssueNumber, "readme", "md", "output", item.Data["readme"]); + await PublishEvent(Consts.PubSub, Consts.MainTopic, new Event + { + Type = nameof(GithubFlowEventType.ReadmeStored), + Subject = context.Subject, + Data = context.ToData() + }); + } + + break; + case nameof(GithubFlowEventType.CodeCreated): + { + var context = item.ToGithubContext(); + await Store(context.Org,context.Repo, context.ParentNumber.Value, context.IssueNumber, "run", "sh", "output", item.Data["code"]); + await RunInSandbox(context.Org,context.Repo, context.ParentNumber.Value, context.IssueNumber); + await PublishEvent(Consts.PubSub, Consts.MainTopic, new Event + { + Type = nameof(GithubFlowEventType.SandboxRunCreated), + Subject = context.Subject, + Data = context.ToData() + }); + } + + break; + default: + break; + } + } + + public async Task Store(string org, string repo, long parentIssueNumber, long issueNumber, string filename, string extension, string dir, string output) + { + await _azureService.Store(org, repo, parentIssueNumber, issueNumber, filename, extension, dir, output); + } + + public async Task RunInSandbox(string org, string repo, long parentIssueNumber, long issueNumber) + { + await _azureService.RunInSandbox(org, repo, parentIssueNumber, issueNumber); + } +} diff --git a/samples/gh-flow/src/Microsoft.AI.DevTeam.Dapr/Agents/Developer/Developer.cs b/samples/gh-flow/src/Microsoft.AI.DevTeam.Dapr/Agents/Developer/Developer.cs new file mode 100644 index 00000000000..8648cb60201 --- /dev/null +++ b/samples/gh-flow/src/Microsoft.AI.DevTeam.Dapr/Agents/Developer/Developer.cs @@ -0,0 +1,84 @@ +using Dapr.Actors; +using Dapr.Actors.Runtime; +using Dapr.Client; +using Microsoft.AI.Agents.Abstractions; +using Microsoft.AI.Agents.Dapr; +using Microsoft.AI.DevTeam.Dapr.Events; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Memory; +namespace Microsoft.AI.DevTeam.Dapr; +public class Dev : AiAgent, IDaprAgent +{ + + private readonly ILogger _logger; + + public Dev(ActorHost host, DaprClient client, Kernel kernel, ISemanticTextMemory memory, ILogger logger) + : base(host, client, memory, kernel) + { + _logger = logger; + } + public async override Task HandleEvent(Event item) + { + switch (item.Type) + { + case nameof(GithubFlowEventType.CodeGenerationRequested): + { + var context = item.ToGithubContext(); + var code = await GenerateCode(item.Data["input"]); + var data = context.ToData(); + data["result"] = code; + await PublishEvent(Consts.PubSub, Consts.MainTopic, new Event + { + Type = nameof(GithubFlowEventType.CodeGenerated), + Subject = context.Subject, + Data = data + }); + } + break; + case nameof(GithubFlowEventType.CodeChainClosed): + { + var context = item.ToGithubContext(); + var lastCode = state.History.Last().Message; + var data = context.ToData(); + data["code"] = lastCode; + await PublishEvent(Consts.PubSub, Consts.MainTopic, new Event + { + Type = nameof(GithubFlowEventType.CodeCreated), + Subject = context.Subject, + Data = data + }); + } + break; + default: + break; + } + } + + public async Task GenerateCode(string ask) + { + try + { + // TODO: ask the architect for the high level architecture as well as the files structure of the project + var context = new KernelArguments { ["input"] = AppendChatHistory(ask)}; + var instruction = "Consider the following architectural guidelines:!waf!"; + var enhancedContext = await AddKnowledge(instruction, "waf",context); + return await CallFunction(DeveloperSkills.Implement, enhancedContext); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error generating code"); + return default; + } + } +} + +public class DeveloperState +{ + public string Understanding { get; set; } +} + +public class UnderstandingResult +{ + public string NewUnderstanding { get; set; } + public string Explanation { get; set; } +} \ No newline at end of file diff --git a/samples/gh-flow/src/Microsoft.AI.DevTeam.Dapr/Agents/Developer/DeveloperPrompts.cs b/samples/gh-flow/src/Microsoft.AI.DevTeam.Dapr/Agents/Developer/DeveloperPrompts.cs new file mode 100644 index 00000000000..1e876ad9e27 --- /dev/null +++ b/samples/gh-flow/src/Microsoft.AI.DevTeam.Dapr/Agents/Developer/DeveloperPrompts.cs @@ -0,0 +1,56 @@ + +namespace Microsoft.AI.DevTeam.Dapr; +public static class DeveloperSkills { + public static string Implement = """ + You are a Developer for an application. + Please output the code required to accomplish the task assigned to you below and wrap it in a bash script that creates the files. + Do not use any IDE commands and do not build and run the code. + Make specific choices about implementation. Do not offer a range of options. + Use comments in the code to describe the intent. Do not include other text other than code and code comments. + Input: {{$input}} + {{$waf}} + """; + + public static string Improve = """ + You are a Developer for an application. Your job is to imrove the code that you are given in the input below. + Please output a new version of code that fixes any problems with this version. + If there is an error message in the input you should fix that error in the code. + Wrap the code output up in a bash script that creates the necessary files by overwriting any previous files. + Do not use any IDE commands and do not build and run the code. + Make specific choices about implementation. Do not offer a range of options. + Use comments in the code to describe the intent. Do not include other text other than code and code comments. + Input: {{$input}} + {{$waf}} + """; + + public static string Explain = """ + You are an experienced software developer, with strong experience in Azure and Microsoft technologies. + Extract the key features and capabilities of the code file below, with the intent to build an understanding of an entire code repository. + You can include references or documentation links in your explanation. Also where appropriate please output a list of keywords to describe the code or its capabilities. + Example: + Keywords: Azure, networking, security, authentication + + ===code=== + {{$input}} + ===end-code=== + Only include the points in a bullet point format and DON'T add anything outside of the bulleted list. + Be short and concise. + If the code's purpose is not clear output an error: + Error: The model could not determine the purpose of the code. + """; + + public static string ConsolidateUnderstanding = """ + You are an experienced software developer, with strong experience in Azure and Microsoft technologies. + You are trying to build an understanding of the codebase from code files. This is the current understanding of the project: + ===current-understanding=== + {{$input}} + ===end-current-understanding=== + and this is the new information that surfaced + ===new-understanding=== + {{$newUnderstanding}} + ===end-new-understanding=== + Your job is to update your current understanding with the new information. + Only include the points in a bullet point format and DON'T add anything outside of the bulleted list. + Be short and concise. + """; +} \ No newline at end of file diff --git a/samples/gh-flow/src/Microsoft.AI.DevTeam.Dapr/Agents/DeveloperLead/DevLeadPrompts.cs b/samples/gh-flow/src/Microsoft.AI.DevTeam.Dapr/Agents/DeveloperLead/DevLeadPrompts.cs new file mode 100644 index 00000000000..e5cdba5daf7 --- /dev/null +++ b/samples/gh-flow/src/Microsoft.AI.DevTeam.Dapr/Agents/DeveloperLead/DevLeadPrompts.cs @@ -0,0 +1,51 @@ +namespace Microsoft.AI.DevTeam.Dapr; +public static class DevLeadSkills { + public static string Plan = """ + You are a Dev Lead for an application team, building the application described below. + Please break down the steps and modules required to develop the complete application, describe each step in detail. + Make prescriptive architecture, language, and frameowrk choices, do not provide a range of choices. + For each step or module then break down the steps or subtasks required to complete that step or module. + For each subtask write an LLM prompt that would be used to tell a model to write the coee that will accomplish that subtask. If the subtask involves taking action/running commands tell the model to write the script that will run those commands. + In each LLM prompt restrict the model from outputting other text that is not in the form of code or code comments. + Please output a JSON array data structure, in the precise schema shown below, with a list of steps and a description of each step, and the steps or subtasks that each requires, and the LLM prompts for each subtask. + Example: + { + "steps": [ + { + "step": "1", + "description": "This is the first step", + "subtasks": [ + { + "subtask": "Subtask 1", + "description": "This is the first subtask", + "prompt": "Write the code to do the first subtask" + }, + { + "subtask": "Subtask 2", + "description": "This is the second subtask", + "prompt": "Write the code to do the second subtask" + } + ] + } + ] + } + Do not output any other text. + Do not wrap the JSON in any other text, output the JSON format described above, making sure it's a valid JSON. + Input: {{$input}} + {{$waf}} + """; + + public static string Explain = """ + You are a Dev Lead. + Please explain the code that is in the input below. You can include references or documentation links in your explanation. + Also where appropriate please output a list of keywords to describe the code or its capabilities. + example: + Keywords: Azure, networking, security, authentication + + If the code's purpose is not clear output an error: + Error: The model could not determine the purpose of the code. + + -- + Input: {{$input}} + """; +} diff --git a/samples/gh-flow/src/Microsoft.AI.DevTeam.Dapr/Agents/DeveloperLead/DeveloperLead.cs b/samples/gh-flow/src/Microsoft.AI.DevTeam.Dapr/Agents/DeveloperLead/DeveloperLead.cs new file mode 100644 index 00000000000..bae10596271 --- /dev/null +++ b/samples/gh-flow/src/Microsoft.AI.DevTeam.Dapr/Agents/DeveloperLead/DeveloperLead.cs @@ -0,0 +1,97 @@ +using Dapr.Actors; +using Dapr.Actors.Runtime; +using Dapr.Client; +using Microsoft.AI.Agents.Abstractions; +using Microsoft.AI.Agents.Dapr; +using Microsoft.AI.DevTeam.Dapr.Events; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Memory; + +namespace Microsoft.AI.DevTeam.Dapr; +public class DeveloperLead : AiAgent, IDaprAgent +{ + private readonly ILogger _logger; + + public DeveloperLead(ActorHost host, DaprClient client, Kernel kernel, ISemanticTextMemory memory, ILogger logger) + : base(host, client, memory, kernel) + { + _logger = logger; + } + + public async override Task HandleEvent(Event item) + { + switch (item.Type) + { + case nameof(GithubFlowEventType.DevPlanRequested): + { + var context = item.ToGithubContext(); + var plan = await CreatePlan(item.Data["input"]); + var data = context.ToData(); + data["result"] = plan; + await PublishEvent(Consts.PubSub,Consts.MainTopic, new Event + { + Type = nameof(GithubFlowEventType.DevPlanGenerated), + Subject = context.Subject, + Data = data + }); + } + break; + case nameof(GithubFlowEventType.DevPlanChainClosed): + { + var context = item.ToGithubContext(); + var latestPlan = state.History.Last().Message; + var data = context.ToData(); + data["plan"] = latestPlan; + await PublishEvent(Consts.PubSub,Consts.MainTopic, new Event + { + Type = nameof(GithubFlowEventType.DevPlanCreated), + Subject = context.Subject, + Data = data + }); + } + break; + default: + break; + } + } + public async Task CreatePlan(string ask) + { + try + { + // TODO: Ask the architect for the existing high level architecture + // as well as the file structure + var context = new KernelArguments { ["input"] = AppendChatHistory(ask) }; + var instruction = "Consider the following architectural guidelines:!waf!"; + var enhancedContext = await AddKnowledge(instruction, "waf", context); + return await CallFunction(DevLeadSkills.Plan, enhancedContext); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error creating development plan"); + return default; + } + } +} + +public class DevLeadPlanResponse +{ + public List steps { get; set; } +} + +public class Step +{ + public string description { get; set; } + public string step { get; set; } + public List subtasks { get; set; } +} + +public class Subtask +{ + public string subtask { get; set; } + public string prompt { get; set; } +} + +public class DeveloperLeadState +{ + public string Plan { get; set; } +} \ No newline at end of file diff --git a/samples/gh-flow/src/Microsoft.AI.DevTeam.Dapr/Agents/Hubber.cs b/samples/gh-flow/src/Microsoft.AI.DevTeam.Dapr/Agents/Hubber.cs new file mode 100644 index 00000000000..93285cb6019 --- /dev/null +++ b/samples/gh-flow/src/Microsoft.AI.DevTeam.Dapr/Agents/Hubber.cs @@ -0,0 +1,100 @@ +using System.Text.Json; +using Dapr.Actors; +using Dapr.Actors.Runtime; +using Dapr.Client; +using Microsoft.AI.Agents.Abstractions; +using Microsoft.AI.Agents.Dapr; +using Microsoft.AI.DevTeam.Dapr.Events; + +namespace Microsoft.AI.DevTeam.Dapr; + +public class Hubber : Agent, IDaprAgent +{ + private readonly IManageGithub _ghService; + + public Hubber(ActorHost host, DaprClient client, IManageGithub ghService) :base(host, client) + { + _ghService = ghService; + } + + public override async Task HandleEvent(Event item) + { + switch (item.Type) + { + case nameof(GithubFlowEventType.NewAsk): + { + var context = item.ToGithubContext(); + var pmIssue = await CreateIssue(context.Org, context.Repo , item.Data["input"], "PM.Readme", context.IssueNumber); + var devLeadIssue = await CreateIssue(context.Org, context.Repo , item.Data["input"], "DevLead.Plan", context.IssueNumber); + await PostComment(context.Org, context.Repo, context.IssueNumber, $" - #{pmIssue} - tracks PM.Readme"); + await PostComment(context.Org, context.Repo, context.IssueNumber, $" - #{devLeadIssue} - tracks DevLead.Plan"); + await CreateBranch(context.Org, context.Repo, $"sk-{context.IssueNumber}"); + } + break; + case nameof(GithubFlowEventType.ReadmeGenerated): + case nameof(GithubFlowEventType.DevPlanGenerated): + case nameof(GithubFlowEventType.CodeGenerated): + { + var context = item.ToGithubContext(); + var result = item.Data["result"]; + var contents = string.IsNullOrEmpty(result)? "Sorry, I got tired, can you try again please? ": result; + await PostComment(context.Org,context.Repo, context.IssueNumber, contents); + } + + break; + case nameof(GithubFlowEventType.DevPlanCreated): + { + var context = item.ToGithubContext(); + var plan = JsonSerializer.Deserialize(item.Data["plan"]); + var prompts = plan.steps.SelectMany(s => s.subtasks.Select(st => st.prompt)); + + foreach (var prompt in prompts) + { + var functionName = "Developer.Implement"; + var issue = await CreateIssue(context.Org, context.Repo, prompt, functionName, context.ParentNumber.Value); + var commentBody = $" - #{issue} - tracks {functionName}"; + await PostComment(context.Org, context.Repo, context.ParentNumber.Value, commentBody); + } + } + break; + case nameof(GithubFlowEventType.ReadmeStored): + { + var context = item.ToGithubContext(); + var branch = $"sk-{context.ParentNumber}"; + await CommitToBranch(context.Org, context.Repo, context.ParentNumber.Value, context.IssueNumber, "output", branch); + await CreatePullRequest(context.Org, context.Repo, context.ParentNumber.Value, branch); + } + break; + case nameof(GithubFlowEventType.SandboxRunFinished): + { + var context = item.ToGithubContext(); + var branch = $"sk-{context.ParentNumber}"; + await CommitToBranch(context.Org, context.Repo, context.ParentNumber.Value, context.IssueNumber, "output", branch); + } + break; + default: + break; + } + } + + public async Task CreateIssue(string org, string repo, string input, string function, long parentNumber) + { + return await _ghService.CreateIssue(org, repo, input, function, parentNumber); + } + public async Task PostComment(string org, string repo, long issueNumber, string comment) + { + await _ghService.PostComment(org, repo, issueNumber, comment); + } + public async Task CreateBranch(string org, string repo, string branch) + { + await _ghService.CreateBranch(org, repo, branch); + } + public async Task CreatePullRequest(string org, string repo, long issueNumber, string branch) + { + await _ghService.CreatePR(org, repo, issueNumber, branch); + } + public async Task CommitToBranch(string org, string repo, long parentNumber, long issueNumber, string rootDir, string branch) + { + await _ghService.CommitToBranch(org, repo, parentNumber, issueNumber, rootDir, branch); + } +} \ No newline at end of file diff --git a/samples/gh-flow/src/Microsoft.AI.DevTeam.Dapr/Agents/ProductManager/PMPrompts.cs b/samples/gh-flow/src/Microsoft.AI.DevTeam.Dapr/Agents/ProductManager/PMPrompts.cs new file mode 100644 index 00000000000..a6b727a1bd8 --- /dev/null +++ b/samples/gh-flow/src/Microsoft.AI.DevTeam.Dapr/Agents/ProductManager/PMPrompts.cs @@ -0,0 +1,34 @@ +namespace Microsoft.AI.DevTeam.Dapr; +public static class PMSkills +{ +public static string BootstrapProject = """ + Please write a bash script with the commands that would be required to generate applications as described in the following input. + You may add comments to the script and the generated output but do not add any other text except the bash script. + You may include commands to build the applications but do not run them. + Do not include any git commands. + Input: {{$input}} + {{$waf}} + """; + public static string Readme = """ + You are a program manager on a software development team. You are working on an app described below. + Based on the input below, and any dialog or other context, please output a raw README.MD markdown file documenting the main features of the app and the architecture or code organization. + Do not describe how to create the application. + Write the README as if it were documenting the features and architecture of the application. You may include instructions for how to run the application. + Input: {{$input}} + {{$waf}} + """; + + public static string Explain = """ + You are a Product Manager. + Please explain the code that is in the input below. You can include references or documentation links in your explanation. + Also where appropriate please output a list of keywords to describe the code or its capabilities. + example: + Keywords: Azure, networking, security, authentication + + If the code's purpose is not clear output an error: + Error: The model could not determine the purpose of the code. + + -- + Input: {{$input}} + """; +} diff --git a/samples/gh-flow/src/Microsoft.AI.DevTeam.Dapr/Agents/ProductManager/ProductManager.cs b/samples/gh-flow/src/Microsoft.AI.DevTeam.Dapr/Agents/ProductManager/ProductManager.cs new file mode 100644 index 00000000000..ce08c1d1aa2 --- /dev/null +++ b/samples/gh-flow/src/Microsoft.AI.DevTeam.Dapr/Agents/ProductManager/ProductManager.cs @@ -0,0 +1,78 @@ +using Dapr.Actors; +using Dapr.Actors.Runtime; +using Dapr.Client; +using Microsoft.AI.Agents.Abstractions; +using Microsoft.AI.Agents.Dapr; +using Microsoft.AI.DevTeam.Dapr.Events; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Memory; + +namespace Microsoft.AI.DevTeam.Dapr; + +public class ProductManager : AiAgent, IDaprAgent +{ + private readonly ILogger _logger; + + public ProductManager(ActorHost host, DaprClient client, Kernel kernel, ISemanticTextMemory memory, ILogger logger) + : base(host, client, memory, kernel) + { + _logger = logger; + } + + public async override Task HandleEvent(Event item) + { + switch (item.Type) + { + case nameof(GithubFlowEventType.ReadmeRequested): + { + var context = item.ToGithubContext(); + var readme = await CreateReadme(item.Data["input"]); + var data = context.ToData(); + data["result"]=readme; + await PublishEvent(Consts.PubSub,Consts.MainTopic, new Event { + Type = nameof(GithubFlowEventType.ReadmeGenerated), + Subject = context.Subject, + Data = data + }); + } + break; + case nameof(GithubFlowEventType.ReadmeChainClosed): + { + var context = item.ToGithubContext(); + var lastReadme = state.History.Last().Message; + var data = context.ToData(); + data["readme"] = lastReadme; + await PublishEvent(Consts.PubSub,Consts.MainTopic, new Event { + Type = nameof(GithubFlowEventType.ReadmeCreated), + Subject = context.Subject, + Data = data + }); + } + + break; + default: + break; + } + } + + public async Task CreateReadme(string ask) + { + try + { + var context = new KernelArguments { ["input"] = AppendChatHistory(ask) }; + var instruction = "Consider the following architectural guidelines:!waf!"; + var enhancedContext = await AddKnowledge(instruction, "waf", context); + return await CallFunction(PMSkills.Readme, enhancedContext); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error creating readme"); + return default; + } + } +} + +public class ProductManagerState +{ + public string Capabilities { get; set; } +} \ No newline at end of file diff --git a/samples/gh-flow/src/Microsoft.AI.DevTeam.Dapr/Agents/Sandbox.cs b/samples/gh-flow/src/Microsoft.AI.DevTeam.Dapr/Agents/Sandbox.cs new file mode 100644 index 00000000000..b75ed2ef925 --- /dev/null +++ b/samples/gh-flow/src/Microsoft.AI.DevTeam.Dapr/Agents/Sandbox.cs @@ -0,0 +1,106 @@ +using Dapr.Actors.Runtime; +using Dapr.Client; +using Microsoft.AI.Agents.Abstractions; +using Microsoft.AI.Agents.Dapr; +using Microsoft.AI.DevTeam.Dapr.Events; + +namespace Microsoft.AI.DevTeam.Dapr; +public class Sandbox : Agent, IDaprAgent, IRemindable +{ + private const string ReminderName = "SandboxRunReminder"; + public string StateStore = "agents-statestore"; + private readonly IManageAzure _azService; + + public Sandbox(ActorHost host, DaprClient client, IManageAzure azService) : base(host, client) + { + _azService = azService; + } + + public override async Task HandleEvent(Event item) + { + switch(item.Type) + { + case nameof(GithubFlowEventType.SandboxRunCreated): + { + var context = item.ToGithubContext(); + await ScheduleCommitSandboxRun(context.Org, context.Repo, context.ParentNumber.Value, context.IssueNumber); + } + break; + + default: + break; + } + } + + public async Task ScheduleCommitSandboxRun(string org, string repo, long parentIssueNumber, long issueNumber) + { + await StoreState(org, repo, parentIssueNumber, issueNumber); + await this.RegisterReminderAsync( + ReminderName, + null, + TimeSpan.Zero, + TimeSpan.FromMinutes(1)); + } + + private async Task StoreState(string org, string repo, long parentIssueNumber, long issueNumber) + { + var state = new SandboxMetadata { + Org = org, + Repo = repo, + IssueNumber = issueNumber, + ParentIssueNumber = parentIssueNumber, + IsCompleted = false + }; + await StateManager.SetStateAsync( + StateStore, + state); + } + private async Task Cleanup() + { + var agentState = await StateManager.GetStateAsync(StateStore); + agentState.IsCompleted = true; + await UnregisterReminderAsync(ReminderName); + await StateManager.SetStateAsync( + StateStore, + agentState); + } + + public async Task ReceiveReminderAsync(string reminderName, byte[] state, TimeSpan dueTime, TimeSpan period) + { + var agentState = await StateManager.GetStateAsync(StateStore); + if (!agentState.IsCompleted) + { + var sandboxId = $"sk-sandbox-{agentState.Org}-{agentState.Repo}-{agentState.ParentIssueNumber}-{agentState.IssueNumber}"; + if (await _azService.IsSandboxCompleted(sandboxId)) + { + await _azService.DeleteSandbox(sandboxId); + var data = new Dictionary { + { "org", agentState.Org }, + { "repo", agentState.Repo }, + { "issueNumber", agentState.IssueNumber.ToString() }, + { "parentNumber", agentState.ParentIssueNumber.ToString() } + }; + var subject = $"{agentState.Org}-{agentState.Repo}-{agentState.IssueNumber}"; + await PublishEvent(Consts.PubSub,Consts.MainTopic, new Event { + Type = nameof(GithubFlowEventType.SandboxRunFinished), + Subject = subject, + Data = data + }); + await Cleanup(); + } + } + else + { + await Cleanup(); + } + } +} + +public class SandboxMetadata +{ + public string Org { get; set; } + public string Repo { get; set; } + public long ParentIssueNumber { get; set; } + public long IssueNumber { get; set; } + public bool IsCompleted { get; set; } +} \ No newline at end of file diff --git a/samples/gh-flow/src/Microsoft.AI.DevTeam.Dapr/Dockerfile b/samples/gh-flow/src/Microsoft.AI.DevTeam.Dapr/Dockerfile new file mode 100644 index 00000000000..94041c3fabb --- /dev/null +++ b/samples/gh-flow/src/Microsoft.AI.DevTeam.Dapr/Dockerfile @@ -0,0 +1,23 @@ +FROM mcr.microsoft.com/dotnet/aspnet:7.0 AS base +WORKDIR /app +EXPOSE 5274 +EXPOSE 11111 +EXPOSE 30000 + +ENV ASPNETCORE_URLS=http://+:5274 + +FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build +ARG configuration=Release +COPY . . +RUN dotnet restore "src/apps/gh-flow/gh-flow.csproj" +WORKDIR "/src/apps/gh-flow" +RUN dotnet build "gh-flow.csproj" -c $configuration -o /app/build + +FROM build AS publish +ARG configuration=Release +RUN dotnet publish "gh-flow.csproj" -c $configuration -o /app/publish + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "gh-flow.dll"] \ No newline at end of file diff --git a/samples/gh-flow/src/Microsoft.AI.DevTeam.Dapr/Events/GithubFlowEventType.cs b/samples/gh-flow/src/Microsoft.AI.DevTeam.Dapr/Events/GithubFlowEventType.cs new file mode 100644 index 00000000000..24b165711fc --- /dev/null +++ b/samples/gh-flow/src/Microsoft.AI.DevTeam.Dapr/Events/GithubFlowEventType.cs @@ -0,0 +1,69 @@ +using System.Runtime.Serialization; +using System.Text.Json.Serialization; +using Microsoft.AI.Agents.Abstractions; + +namespace Microsoft.AI.DevTeam.Dapr.Events +{ + public enum GithubFlowEventType + { + NewAsk, + ReadmeChainClosed, + CodeChainClosed, + CodeGenerationRequested, + DevPlanRequested, + ReadmeGenerated, + DevPlanGenerated, + CodeGenerated, + DevPlanChainClosed, + ReadmeRequested, + ReadmeStored, + SandboxRunFinished, + ReadmeCreated, + CodeCreated, + DevPlanCreated, + SandboxRunCreated + } + + public static class EventExtensions + { + public static GithubContext ToGithubContext(this Event evt) + { + return new GithubContext + { + Org = evt.Data["org"], + Repo = evt.Data["repo"], + IssueNumber = long.Parse(evt.Data["issueNumber"]), + ParentNumber = string.IsNullOrEmpty(evt.Data["parentNumber"]) ? default : long.Parse(evt.Data["parentNumber"]) + }; + } + + public static Dictionary ToData(this GithubContext context) + { + return new Dictionary { + { "org", context.Org }, + { "repo", context.Repo }, + { "issueNumber", $"{context.IssueNumber}" }, + { "parentNumber", context.ParentNumber.HasValue? context.ParentNumber.ToString(): default } + }; + } + + + } + + public class GithubContext + { + public string Org { get; set; } + public string Repo { get; set; } + public long IssueNumber { get; set; } + public long? ParentNumber { get; set; } + + public string Subject => $"{Org}-{Repo}-{IssueNumber}"; + } + + [DataContract] + public class EventEnvelope + { + [JsonPropertyName("data")] + public Event Data { get; set; } + } +} \ No newline at end of file diff --git a/samples/gh-flow/src/Microsoft.AI.DevTeam.Dapr/Microsoft.AI.DevTeam.Dapr.csproj b/samples/gh-flow/src/Microsoft.AI.DevTeam.Dapr/Microsoft.AI.DevTeam.Dapr.csproj new file mode 100644 index 00000000000..3d04d93de0f --- /dev/null +++ b/samples/gh-flow/src/Microsoft.AI.DevTeam.Dapr/Microsoft.AI.DevTeam.Dapr.csproj @@ -0,0 +1,44 @@ + + + + + + + + net8.0 + enable + enable + true + true + c073c86e-8483-4956-942f-331fd09172d4 + All + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/gh-flow/src/Microsoft.AI.DevTeam.Dapr/Options/AzureOptions.cs b/samples/gh-flow/src/Microsoft.AI.DevTeam.Dapr/Options/AzureOptions.cs new file mode 100644 index 00000000000..2fcbcab789e --- /dev/null +++ b/samples/gh-flow/src/Microsoft.AI.DevTeam.Dapr/Options/AzureOptions.cs @@ -0,0 +1,22 @@ +using System.ComponentModel.DataAnnotations; + +namespace Microsoft.AI.DevTeam.Dapr; +public class AzureOptions +{ + [Required] + public string SubscriptionId { get; set; } + [Required] + public string Location { get; set; } + [Required] + public string ContainerInstancesResourceGroup { get; set; } + [Required] + public string FilesShareName { get; set; } + [Required] + public string FilesAccountName { get; set; } + [Required] + public string FilesAccountKey { get; set; } + [Required] + public string SandboxImage { get; set; } + public string ManagedIdentity { get; set; } + public string CosmosConnectionString { get; set; } +} \ No newline at end of file diff --git a/samples/gh-flow/src/Microsoft.AI.DevTeam.Dapr/Options/Consts.cs b/samples/gh-flow/src/Microsoft.AI.DevTeam.Dapr/Options/Consts.cs new file mode 100644 index 00000000000..cb97cf30758 --- /dev/null +++ b/samples/gh-flow/src/Microsoft.AI.DevTeam.Dapr/Options/Consts.cs @@ -0,0 +1,8 @@ +namespace Microsoft.AI.DevTeam.Dapr; + +public static class Consts +{ + public const string MainTopic = "DevPersonas"; + public const string PubSub = "agents-pubsub"; + public const string AppId = "dev-agents"; +} diff --git a/samples/gh-flow/src/Microsoft.AI.DevTeam.Dapr/Options/GithubOptions.cs b/samples/gh-flow/src/Microsoft.AI.DevTeam.Dapr/Options/GithubOptions.cs new file mode 100644 index 00000000000..a88480538a1 --- /dev/null +++ b/samples/gh-flow/src/Microsoft.AI.DevTeam.Dapr/Options/GithubOptions.cs @@ -0,0 +1,14 @@ +using System.ComponentModel.DataAnnotations; + +namespace Microsoft.AI.DevTeam.Dapr; +public class GithubOptions +{ + [Required] + public string AppKey { get; set; } + [Required] + public int AppId { get; set; } + [Required] + public long InstallationId { get; set; } + [Required] + public string WebhookSecret { get; set; } +} \ No newline at end of file diff --git a/samples/gh-flow/src/Microsoft.AI.DevTeam.Dapr/Options/OpenAIOptions.cs b/samples/gh-flow/src/Microsoft.AI.DevTeam.Dapr/Options/OpenAIOptions.cs new file mode 100644 index 00000000000..e69ebf59f20 --- /dev/null +++ b/samples/gh-flow/src/Microsoft.AI.DevTeam.Dapr/Options/OpenAIOptions.cs @@ -0,0 +1,18 @@ +using System.ComponentModel.DataAnnotations; + +namespace Microsoft.AI.DevTeam.Dapr; +public class OpenAIOptions +{ + [Required] + public string ServiceType { get; set; } + [Required] + public string ServiceId { get; set; } + [Required] + public string DeploymentOrModelId { get; set; } + [Required] + public string EmbeddingDeploymentOrModelId { get; set; } + [Required] + public string Endpoint { get; set; } + [Required] + public string ApiKey { get; set; } +} diff --git a/samples/gh-flow/src/Microsoft.AI.DevTeam.Dapr/Options/QdrantOptions.cs b/samples/gh-flow/src/Microsoft.AI.DevTeam.Dapr/Options/QdrantOptions.cs new file mode 100644 index 00000000000..ea79a9557a1 --- /dev/null +++ b/samples/gh-flow/src/Microsoft.AI.DevTeam.Dapr/Options/QdrantOptions.cs @@ -0,0 +1,10 @@ +using System.ComponentModel.DataAnnotations; + +namespace Microsoft.AI.DevTeam.Dapr; +public class QdrantOptions +{ + [Required] + public string Endpoint { get; set; } + [Required] + public int VectorSize { get; set; } +} \ No newline at end of file diff --git a/samples/marketing/src/backend/Options/ServiceOptions.cs b/samples/gh-flow/src/Microsoft.AI.DevTeam.Dapr/Options/ServiceOptions.cs similarity index 82% rename from samples/marketing/src/backend/Options/ServiceOptions.cs rename to samples/gh-flow/src/Microsoft.AI.DevTeam.Dapr/Options/ServiceOptions.cs index 01bc1799953..8c3b09c768c 100644 --- a/samples/marketing/src/backend/Options/ServiceOptions.cs +++ b/samples/gh-flow/src/Microsoft.AI.DevTeam.Dapr/Options/ServiceOptions.cs @@ -1,6 +1,6 @@ using System.ComponentModel.DataAnnotations; -namespace Marketing.Options; +namespace Microsoft.AI.DevTeam.Dapr; public class ServiceOptions { private string _ingesterUrl; diff --git a/samples/gh-flow/src/Microsoft.AI.DevTeam.Dapr/Program.cs b/samples/gh-flow/src/Microsoft.AI.DevTeam.Dapr/Program.cs new file mode 100644 index 00000000000..8c938d1813e --- /dev/null +++ b/samples/gh-flow/src/Microsoft.AI.DevTeam.Dapr/Program.cs @@ -0,0 +1,205 @@ +using System.Text.Json; +using Azure; +using Azure.AI.OpenAI; +using Microsoft.Extensions.Options; +using Microsoft.SemanticKernel; +using Octokit.Webhooks; +using Octokit.Webhooks.AspNetCore; +using Azure.Identity; +using Microsoft.Extensions.Azure; +using Microsoft.Extensions.Http.Resilience; +using Microsoft.SemanticKernel.Memory; +using Microsoft.SemanticKernel.Connectors.Qdrant; +using Microsoft.SemanticKernel.Connectors.OpenAI; +using Dapr; +using Dapr.Actors.Client; +using Dapr.Actors; +using Microsoft.AI.DevTeam.Dapr; +using Microsoft.AI.DevTeam.Dapr.Events; + +var builder = WebApplication.CreateBuilder(args); +builder.Services.AddSingleton(); +builder.Services.AddTransient(CreateKernel); +builder.Services.AddTransient(CreateMemory); +builder.Services.AddHttpClient(); + +builder.Services.AddSingleton(s => +{ + var ghOptions = s.GetService>(); + var logger = s.GetService>(); + var ghService = new GithubAuthService(ghOptions, logger); + var client = ghService.GetGitHubClient(); + return client; +}); + + +builder.Services.AddAzureClients(clientBuilder => +{ + clientBuilder.UseCredential(new DefaultAzureCredential()); + clientBuilder.AddArmClient(default); +}); + +builder.Services.AddDaprClient(); + +builder.Services.AddActors( + options => + { + options.UseJsonSerialization = true; + options.JsonSerializerOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web); + options.ReentrancyConfig = new ActorReentrancyConfig { + Enabled = true + }; + options.Actors.RegisterActor(); + options.Actors.RegisterActor(); + options.Actors.RegisterActor(); + options.Actors.RegisterActor(); + options.Actors.RegisterActor(); + options.Actors.RegisterActor(); + }); + +builder.Services.AddSingleton(); + +builder.Services.AddApplicationInsightsTelemetry(); +builder.Services.AddOptions() + .Configure((settings, configuration) => + { + configuration.GetSection(nameof(GithubOptions)).Bind(settings); + }) + .ValidateDataAnnotations() + .ValidateOnStart(); + +builder.Services.AddOptions() + .Configure((settings, configuration) => + { + configuration.GetSection(nameof(AzureOptions)).Bind(settings); + }) + .ValidateDataAnnotations() + .ValidateOnStart(); + +builder.Services.AddOptions() + .Configure((settings, configuration) => + { + configuration.GetSection(nameof(OpenAIOptions)).Bind(settings); + }) + .ValidateDataAnnotations() + .ValidateOnStart(); + +builder.Services.AddOptions() + .Configure((settings, configuration) => + { + configuration.GetSection(nameof(QdrantOptions)).Bind(settings); + }) + .ValidateDataAnnotations() + .ValidateOnStart(); + +builder.Services.AddOptions() + .Configure((settings, configuration) => + { + configuration.GetSection(nameof(ServiceOptions)).Bind(settings); + }) + .ValidateDataAnnotations() + .ValidateOnStart(); + +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); + + +builder.Services.Configure(options => +{ + options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; +}); +var app = builder.Build(); + +app.UseRouting() + .UseEndpoints(endpoints => +{ + var ghOptions = app.Services.GetService>().Value; + endpoints.MapGitHubWebhooks(secret: ghOptions.WebhookSecret); + endpoints.MapActorsHandlers(); + endpoints.MapSubscribeHandler(); +}); + +app.UseCloudEvents(); + +app.MapPost("/developers", [Topic(Consts.PubSub, Consts.MainTopic, + $"(event.type ==\"{nameof(GithubFlowEventType.CodeGenerationRequested)}\") || (event.type ==\"{nameof(GithubFlowEventType.CodeGenerationRequested)}\")", 1)] + async (IActorProxyFactory proxyFactory, EventEnvelope evt) => await HandleEvent(proxyFactory,nameof(Dev), nameof(Dev.HandleEvent), evt)); + +app.MapPost("/devleads", [Topic(Consts.PubSub, Consts.MainTopic, +$"(event.type ==\"{nameof(GithubFlowEventType.DevPlanRequested)}\") || (event.type ==\"{nameof(GithubFlowEventType.DevPlanChainClosed)}\")", 2)] +async (IActorProxyFactory proxyFactory, EventEnvelope evt) => await HandleEvent(proxyFactory, nameof(DeveloperLead), nameof(DeveloperLead.HandleEvent), evt)); + +app.MapPost("/productmanagers", [Topic(Consts.PubSub, Consts.MainTopic, +$"(event.type ==\"{nameof(GithubFlowEventType.ReadmeRequested)}\") || (event.type ==\"{nameof(GithubFlowEventType.ReadmeChainClosed)}\")", 3)] +async (IActorProxyFactory proxyFactory, EventEnvelope evt) => await HandleEvent(proxyFactory, nameof(ProductManager), nameof(ProductManager.HandleEvent), evt)); + +app.MapPost("/hubbers", [Topic(Consts.PubSub, Consts.MainTopic, + $"(event.type ==\"{nameof(GithubFlowEventType.NewAsk)}\") || (event.type ==\"{nameof(GithubFlowEventType.ReadmeGenerated)}\") || (event.type ==\"{nameof(GithubFlowEventType.DevPlanGenerated)}\") || (event.type ==\"{nameof(GithubFlowEventType.CodeGenerated)}\") || (event.type ==\"{nameof(GithubFlowEventType.DevPlanCreated)}\") || (event.type ==\"{nameof(GithubFlowEventType.ReadmeStored)}\") || (event.type ==\"{nameof(GithubFlowEventType.SandboxRunFinished)}\")", 4)] + async (IActorProxyFactory proxyFactory, EventEnvelope evt) => await HandleEvent(proxyFactory, nameof(Hubber), nameof(Hubber.HandleEvent), evt)); + +app.MapPost("/azuregenies", [Topic(Consts.PubSub, Consts.MainTopic, +$"(event.type ==\"{nameof(GithubFlowEventType.ReadmeCreated)}\") || (event.type ==\"{nameof(GithubFlowEventType.CodeCreated)}\")", 5)] +async (IActorProxyFactory proxyFactory, EventEnvelope evt) => await HandleEvent(proxyFactory, nameof(AzureGenie), nameof(AzureGenie.HandleEvent), evt)); + +app.MapPost("/sandboxes", [Topic(Consts.PubSub, Consts.MainTopic,$"(event.type ==\"{nameof(GithubFlowEventType.SandboxRunCreated)}\")", 6)] +async (IActorProxyFactory proxyFactory, EventEnvelope evt) => await HandleEvent(proxyFactory, nameof(Sandbox), nameof(Sandbox.HandleEvent), evt)); + +app.Run(); + +static async Task HandleEvent(IActorProxyFactory proxyFactory, string type, string method, EventEnvelope evt) +{ + try + { + var proxyOptions = new ActorProxyOptions + { + RequestTimeout = Timeout.InfiniteTimeSpan + }; + var proxy = proxyFactory.Create(new ActorId(evt.Data.Subject), type, proxyOptions); + await proxy.InvokeMethodAsync(method, evt.Data); + } + catch (Exception ex) + { + throw; + } +} + +static ISemanticTextMemory CreateMemory(IServiceProvider provider) +{ + var openAiConfig = provider.GetService>().Value; + var qdrantConfig = provider.GetService>().Value; + + var loggerFactory = LoggerFactory.Create(builder => + { + builder + .SetMinimumLevel(LogLevel.Debug) + .AddConsole() + .AddDebug(); + }); + + var memoryBuilder = new MemoryBuilder(); + return memoryBuilder.WithLoggerFactory(loggerFactory) + .WithQdrantMemoryStore(qdrantConfig.Endpoint, qdrantConfig.VectorSize) + .WithAzureOpenAITextEmbeddingGeneration(openAiConfig.EmbeddingDeploymentOrModelId, openAiConfig.Endpoint, openAiConfig.ApiKey) + .Build(); +} + +static Kernel CreateKernel(IServiceProvider provider) +{ + var openAiConfig = provider.GetService>().Value; + var clientOptions = new OpenAIClientOptions(); + clientOptions.Retry.NetworkTimeout = TimeSpan.FromMinutes(5); + var openAIClient = new OpenAIClient(new Uri(openAiConfig.Endpoint), new AzureKeyCredential(openAiConfig.ApiKey), clientOptions); + var builder = Kernel.CreateBuilder(); + builder.Services.AddLogging(c => c.AddConsole().AddDebug().SetMinimumLevel(LogLevel.Debug)); + builder.Services.AddAzureOpenAIChatCompletion(openAiConfig.DeploymentOrModelId, openAIClient); + builder.Services.ConfigureHttpClientDefaults(c => + { + c.AddStandardResilienceHandler().Configure(o => + { + o.Retry.MaxRetryAttempts = 5; + o.Retry.BackoffType = Polly.DelayBackoffType.Exponential; + }); + }); + return builder.Build(); +} \ No newline at end of file diff --git a/samples/gh-flow/src/Microsoft.AI.DevTeam.Dapr/Properties/launchSettings.json b/samples/gh-flow/src/Microsoft.AI.DevTeam.Dapr/Properties/launchSettings.json new file mode 100644 index 00000000000..6970a78f64f --- /dev/null +++ b/samples/gh-flow/src/Microsoft.AI.DevTeam.Dapr/Properties/launchSettings.json @@ -0,0 +1,38 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:59668", + "sslPort": 44354 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:5244", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:7227;http://localhost:5244", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/samples/gh-flow/src/Microsoft.AI.DevTeam.Dapr/Services/AzureService.cs b/samples/gh-flow/src/Microsoft.AI.DevTeam.Dapr/Services/AzureService.cs new file mode 100644 index 00000000000..5e8246b5e99 --- /dev/null +++ b/samples/gh-flow/src/Microsoft.AI.DevTeam.Dapr/Services/AzureService.cs @@ -0,0 +1,160 @@ +using System.Text; +using Azure; +using Azure.Core; +using Azure.ResourceManager; +using Azure.ResourceManager.ContainerInstance; +using Azure.ResourceManager.ContainerInstance.Models; +using Azure.ResourceManager.Resources; +using Azure.Storage.Files.Shares; +using Microsoft.Extensions.Options; + + +namespace Microsoft.AI.DevTeam.Dapr; + +public class AzureService : IManageAzure +{ + private readonly AzureOptions _azSettings; + private readonly ILogger _logger; + private readonly ArmClient _client; + + public AzureService(IOptions azOptions, ILogger logger, ArmClient client) + { + _azSettings = azOptions.Value; + _logger = logger; + _client = client; + } + + public async Task DeleteSandbox(string sandboxId) + { + try + { + var resourceGroupResourceId = ResourceGroupResource.CreateResourceIdentifier(_azSettings.SubscriptionId, _azSettings.ContainerInstancesResourceGroup); + var resourceGroupResource = _client.GetResourceGroupResource(resourceGroupResourceId); + + var collection = resourceGroupResource.GetContainerGroups(); + var containerGroup = await collection.GetAsync(sandboxId); + await containerGroup.Value.DeleteAsync(WaitUntil.Started); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error deleting sandbox"); + throw; + } + + } + + public async Task IsSandboxCompleted(string sandboxId) + { + try + { + var resourceGroupResourceId = ResourceGroupResource.CreateResourceIdentifier(_azSettings.SubscriptionId, _azSettings.ContainerInstancesResourceGroup); + var resourceGroupResource = _client.GetResourceGroupResource(resourceGroupResourceId); + + var collection = resourceGroupResource.GetContainerGroups(); + var containerGroup = await collection.GetAsync(sandboxId); + return containerGroup.Value.Data.ProvisioningState == "Succeeded" + && containerGroup.Value.Data.Containers.First().InstanceView.CurrentState.State == "Terminated"; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error checking sandbox status"); + throw; + } + } + + public async Task RunInSandbox(string org, string repo, long parentIssueNumber, long issueNumber) + { + try + { + var runId = $"sk-sandbox-{org}-{repo}-{parentIssueNumber}-{issueNumber}"; + var resourceGroupResourceId = ResourceGroupResource.CreateResourceIdentifier(_azSettings.SubscriptionId, _azSettings.ContainerInstancesResourceGroup); + var resourceGroupResource = _client.GetResourceGroupResource(resourceGroupResourceId); + var scriptPath = $"/azfiles/output/{org}-{repo}/{parentIssueNumber}/{issueNumber}/run.sh"; + var collection = resourceGroupResource.GetContainerGroups(); + var data = new ContainerGroupData(new AzureLocation(_azSettings.Location), new ContainerInstanceContainer[] + { + new ContainerInstanceContainer(runId,_azSettings.SandboxImage,new ContainerResourceRequirements(new ContainerResourceRequestsContent(1.5,1))) + { + Command = { "/bin/bash", $"{scriptPath}" }, + VolumeMounts = + { + new ContainerVolumeMount("azfiles","/azfiles/") + { + IsReadOnly = false, + } + }, + }}, ContainerInstanceOperatingSystemType.Linux) + { + Volumes = + { + new ContainerVolume("azfiles") + { + AzureFile = new ContainerInstanceAzureFileVolume(_azSettings.FilesShareName,_azSettings.FilesAccountName) + { + StorageAccountKey = _azSettings.FilesAccountKey + }, + }, + }, + RestartPolicy = ContainerGroupRestartPolicy.Never, + Sku = ContainerGroupSku.Standard, + Priority = ContainerGroupPriority.Regular + }; + await collection.CreateOrUpdateAsync(WaitUntil.Completed, runId, data); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error running sandbox"); + throw; + } + + } + + public async Task Store(string org, string repo, long parentIssueNumber, long issueNumber, string filename, string extension, string dir, string output) + { + try + { + var connectionString = $"DefaultEndpointsProtocol=https;AccountName={_azSettings.FilesAccountName};AccountKey={_azSettings.FilesAccountKey};EndpointSuffix=core.windows.net"; + var parentDirName = $"{dir}/{org}-{repo}"; + + var fileName = $"{filename}.{extension}"; + + var share = new ShareClient(connectionString, _azSettings.FilesShareName); + await share.CreateIfNotExistsAsync(); + await share.GetDirectoryClient($"{dir}").CreateIfNotExistsAsync(); ; + + var parentDir = share.GetDirectoryClient(parentDirName); + await parentDir.CreateIfNotExistsAsync(); + + var parentIssueDir = parentDir.GetSubdirectoryClient($"{parentIssueNumber}"); + await parentIssueDir.CreateIfNotExistsAsync(); + + var directory = parentIssueDir.GetSubdirectoryClient($"{issueNumber}"); + await directory.CreateIfNotExistsAsync(); + + var file = directory.GetFileClient(fileName); + // hack to enable script to save files in the same directory + var cwdHack = "#!/bin/bash\n cd $(dirname $0)"; + var contents = extension == "sh" ? output.Replace("#!/bin/bash", cwdHack) : output; + using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(contents))) + { + await file.CreateAsync(stream.Length); + await file.UploadRangeAsync( + new HttpRange(0, stream.Length), + stream); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error storing output"); + throw; + } + } +} + +public interface IManageAzure +{ + Task Store(string org, string repo, long parentIssueNumber, long issueNumber, string filename, string extension, string dir, string output); + Task RunInSandbox(string org, string repo, long parentIssueNumber, long issueNumber); + Task IsSandboxCompleted(string sandboxId); + Task DeleteSandbox(string sandboxId); +} \ No newline at end of file diff --git a/samples/gh-flow/src/Microsoft.AI.DevTeam.Dapr/Services/CodeAnalyzer.cs b/samples/gh-flow/src/Microsoft.AI.DevTeam.Dapr/Services/CodeAnalyzer.cs new file mode 100644 index 00000000000..392d4413730 --- /dev/null +++ b/samples/gh-flow/src/Microsoft.AI.DevTeam.Dapr/Services/CodeAnalyzer.cs @@ -0,0 +1,53 @@ +using System.Text; +using System.Text.Json; +using Microsoft.Extensions.Options; + +namespace Microsoft.AI.DevTeam.Dapr; + +public interface IAnalyzeCode +{ + Task> Analyze(string content); +} +public class CodeAnalyzer : IAnalyzeCode +{ + private readonly ServiceOptions _serviceOptions; + private readonly HttpClient _httpClient; + private readonly ILogger _logger; + + public CodeAnalyzer(IOptions serviceOptions, HttpClient httpClient, ILogger logger) + { + _serviceOptions = serviceOptions.Value; + _httpClient = httpClient; + _logger = logger; + _httpClient.BaseAddress = new Uri(_serviceOptions.IngesterUrl); + + } + public async Task> Analyze(string content) + { + try + { + var request = new CodeAnalysisRequest { Content = content }; + var body = new StringContent(JsonSerializer.Serialize(request), Encoding.UTF8, "application/json"); + var response = await _httpClient.PostAsync("api/AnalyzeCode", body); + var stringResult = await response.Content.ReadAsStringAsync(); + var result = JsonSerializer.Deserialize>(stringResult); + return result; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error analyzing code"); + return Enumerable.Empty(); + } + } +} + +public class CodeAnalysisRequest +{ + public string Content { get; set; } +} + +public class CodeAnalysis +{ + public string Meaning { get; set; } + public string CodeBlock { get; set; } +} diff --git a/samples/gh-flow/src/Microsoft.AI.DevTeam.Dapr/Services/GithubAuthService.cs b/samples/gh-flow/src/Microsoft.AI.DevTeam.Dapr/Services/GithubAuthService.cs new file mode 100644 index 00000000000..12168ace2f1 --- /dev/null +++ b/samples/gh-flow/src/Microsoft.AI.DevTeam.Dapr/Services/GithubAuthService.cs @@ -0,0 +1,68 @@ +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Security.Cryptography; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; +using Octokit; + +namespace Microsoft.AI.DevTeam.Dapr; +public class GithubAuthService +{ + private readonly GithubOptions _githubSettings; + private readonly ILogger _logger; + + public GithubAuthService(IOptions ghOptions, ILogger logger) + { + _githubSettings = ghOptions.Value; + _logger = logger; + } + + public string GenerateJwtToken(string appId, string appKey, int minutes) + { + using var rsa = RSA.Create(); + rsa.ImportFromPem(appKey); + var securityKey = new RsaSecurityKey(rsa); + + var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.RsaSha256); + + var now = DateTime.UtcNow; + var iat = new DateTimeOffset(now).ToUnixTimeSeconds(); + var exp = new DateTimeOffset(now.AddMinutes(minutes)).ToUnixTimeSeconds(); + + var claims = new[] { + new Claim(JwtRegisteredClaimNames.Iat, iat.ToString(), ClaimValueTypes.Integer64), + new Claim(JwtRegisteredClaimNames.Exp, exp.ToString(), ClaimValueTypes.Integer64) + }; + + var token = new JwtSecurityToken( + issuer: appId, + claims: claims, + expires: DateTime.Now.AddMinutes(10), + signingCredentials: credentials + ); + + return new JwtSecurityTokenHandler().WriteToken(token); + } + + public GitHubClient GetGitHubClient() + { + try + { + var jwtToken = GenerateJwtToken(_githubSettings.AppId.ToString(), _githubSettings.AppKey, 10); + var appClient = new GitHubClient(new ProductHeaderValue("SK-DEV-APP")) + { + Credentials = new Credentials(jwtToken, AuthenticationType.Bearer) + }; + var response = appClient.GitHubApps.CreateInstallationToken(_githubSettings.InstallationId).Result; + return new GitHubClient(new ProductHeaderValue($"SK-DEV-APP-Installation{_githubSettings.InstallationId}")) + { + Credentials = new Credentials(response.Token) + }; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error getting GitHub client"); + throw; + } + } +} \ No newline at end of file diff --git a/samples/gh-flow/src/Microsoft.AI.DevTeam.Dapr/Services/GithubService.cs b/samples/gh-flow/src/Microsoft.AI.DevTeam.Dapr/Services/GithubService.cs new file mode 100644 index 00000000000..8aa88d37eb1 --- /dev/null +++ b/samples/gh-flow/src/Microsoft.AI.DevTeam.Dapr/Services/GithubService.cs @@ -0,0 +1,227 @@ +using System.Text; +using Azure.Storage.Files.Shares; +using Microsoft.Extensions.Options; +using Octokit; +using Octokit.Helpers; + + +namespace Microsoft.AI.DevTeam.Dapr; + +public class GithubService : IManageGithub +{ + private readonly GitHubClient _ghClient; + private readonly AzureOptions _azSettings; + private readonly ILogger _logger; + private readonly HttpClient _httpClient; + + public GithubService(IOptions azOptions, GitHubClient ghClient, ILogger logger, HttpClient httpClient) + { + _ghClient = ghClient; + _azSettings = azOptions.Value; + _logger = logger; + _httpClient = httpClient; + } + + public async Task CommitToBranch(string org, string repo, long parentNumber, long issueNumber, string rootDir, string branch) + { + try + { + var connectionString = $"DefaultEndpointsProtocol=https;AccountName={_azSettings.FilesAccountName};AccountKey={_azSettings.FilesAccountKey};EndpointSuffix=core.windows.net"; + + var dirName = $"{rootDir}/{org}-{repo}/{parentNumber}/{issueNumber}"; + var share = new ShareClient(connectionString, _azSettings.FilesShareName); + var directory = share.GetDirectoryClient(dirName); + + var remaining = new Queue(); + remaining.Enqueue(directory); + while (remaining.Count > 0) + { + var dir = remaining.Dequeue(); + await foreach (var item in dir.GetFilesAndDirectoriesAsync()) + { + if (!item.IsDirectory && item.Name != "run.sh") // we don't want the generated script in the PR + { + try + { + var file = dir.GetFileClient(item.Name); + var filePath = file.Path.Replace($"{_azSettings.FilesShareName}/", "") + .Replace($"{dirName}/", ""); + var fileStream = await file.OpenReadAsync(); + using (var reader = new StreamReader(fileStream, Encoding.UTF8)) + { + var value = reader.ReadToEnd(); + + await _ghClient.Repository.Content.CreateFile( + org, repo, filePath, + new CreateFileRequest($"Commit message", value, branch)); // TODO: add more meaningfull commit message + } + } + catch (Exception ex) + { + _logger.LogError(ex, $"Error while uploading file {item.Name}"); + } + } + else if (item.IsDirectory) + { + remaining.Enqueue(dir.GetSubdirectoryClient(item.Name)); + } + } + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error committing to branch"); + throw; + } + } + + public async Task CreateBranch(string org, string repo, string branch) + { + try + { + var ghRepo = await _ghClient.Repository.Get(org, repo); + if (ghRepo.Size == 0) + { + // Create a new file and commit it to the repository + var createChangeSet = await _ghClient.Repository.Content.CreateFile( + org, + repo, + "README.md", + new CreateFileRequest("Initial commit", "# Readme") + ); + } + + await _ghClient.Git.Reference.CreateBranch(org, repo, branch, ghRepo.DefaultBranch); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error creating branch"); + throw; + } + } + + public async Task GetMainLanguage(string org, string repo) + { + try + { + var languages = await _ghClient.Repository.GetAllLanguages(org, repo); + var mainLanguage = languages.OrderByDescending(l => l.NumberOfBytes).First(); + return mainLanguage.Name; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error getting main language"); + throw; + } + } + + public async Task CreateIssue(string org, string repo, string input, string function, long parentNumber) + { + try + { + var newIssue = new NewIssue($"{function} chain for #{parentNumber}") + { + Body = input, + }; + newIssue.Labels.Add(function); + newIssue.Labels.Add($"Parent.{parentNumber}"); + var issue = await _ghClient.Issue.Create(org, repo, newIssue); + return issue.Number; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error creating issue"); + throw; + } + } + + public async Task CreatePR(string org, string repo, long number, string branch) + { + try + { + var ghRepo = await _ghClient.Repository.Get(org, repo); + await _ghClient.PullRequest.Create(org, repo, new NewPullRequest($"New app #{number}", branch, ghRepo.DefaultBranch)); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error creating PR"); + throw; + } + } + + public async Task PostComment(string org, string repo, long issueNumber, string comment) + { + try + { + await _ghClient.Issue.Comment.Create(org, repo, (int)issueNumber, comment); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error posting comment"); + throw; + } + } + + public async Task> GetFiles(string org, string repo, string branch, Func filter) + { + try + { + var items = await _ghClient.Repository.Content.GetAllContentsByRef(org, repo, branch); + return await CollectFiles(org, repo, branch, items, filter); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error getting files"); + throw; + } + } + + private async Task> CollectFiles(string org, string repo, string branch, IReadOnlyList items, Func filter) + { + try + { + var result = new List(); + foreach (var item in items) + { + if (item.Type == ContentType.File && filter(item)) + { + var content = await _httpClient.GetStringAsync(item.DownloadUrl); + result.Add(new FileResponse + { + Name = item.Name, + Content = content + }); + } + else if (item.Type == ContentType.Dir) + { + var subItems = await _ghClient.Repository.Content.GetAllContentsByRef(org, repo, item.Path, branch); + result.AddRange(await CollectFiles(org, repo, branch, subItems, filter)); + } + } + return result; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error collecting files"); + throw; + } + } +} + +public class FileResponse +{ + public string Name { get; set; } + public string Content { get; set; } +} + +public interface IManageGithub +{ + Task CreateIssue(string org, string repo, string input, string function, long parentNumber); + Task CreatePR(string org, string repo, long number, string branch); + Task CreateBranch(string org, string repo, string branch); + Task CommitToBranch(string org, string repo, long parentNumber, long issueNumber, string rootDir, string branch); + + Task PostComment(string org, string repo, long issueNumber, string comment); + Task> GetFiles(string org, string repo, string branch, Func filter); + Task GetMainLanguage(string org, string repo); +} diff --git a/samples/gh-flow/src/Microsoft.AI.DevTeam.Dapr/Services/GithubWebHookProcessor.cs b/samples/gh-flow/src/Microsoft.AI.DevTeam.Dapr/Services/GithubWebHookProcessor.cs new file mode 100644 index 00000000000..0bff68a51d8 --- /dev/null +++ b/samples/gh-flow/src/Microsoft.AI.DevTeam.Dapr/Services/GithubWebHookProcessor.cs @@ -0,0 +1,169 @@ +using Dapr.Client; +using Microsoft.AI.Agents.Abstractions; +using Microsoft.AI.DevTeam.Dapr.Events; +using Octokit.Webhooks; +using Octokit.Webhooks.Events; +using Octokit.Webhooks.Events.IssueComment; +using Octokit.Webhooks.Events.Issues; +using Octokit.Webhooks.Models; + +namespace Microsoft.AI.DevTeam.Dapr; +public sealed class GithubWebHookProcessor : WebhookEventProcessor +{ + private readonly DaprClient _daprClient; + private readonly ILogger _logger; + + public GithubWebHookProcessor(DaprClient daprClient, ILogger logger) + { + _daprClient = daprClient; + _logger = logger; + } + protected override async Task ProcessIssuesWebhookAsync(WebhookHeaders headers, IssuesEvent issuesEvent, IssuesAction action) + { + try + { + _logger.LogInformation("Processing issue event"); + var org = issuesEvent.Repository.Owner.Login; + var repo = issuesEvent.Repository.Name; + var issueNumber = issuesEvent.Issue.Number; + var input = issuesEvent.Issue.Body; + // Assumes the label follows the following convention: Skill.Function example: PM.Readme + // Also, we've introduced the Parent label, that ties the sub-issue with the parent issue + var labels = issuesEvent.Issue.Labels + .Select(l => l.Name.Split('.')) + .Where(parts => parts.Length == 2) + .ToDictionary(parts => parts[0], parts => parts[1]); + var skillName = labels.Keys.Where(k => k != "Parent").FirstOrDefault(); + long? parentNumber = labels.ContainsKey("Parent") ? long.Parse(labels["Parent"]) : null; + + var suffix = $"{org}-{repo}"; + if (issuesEvent.Action == IssuesAction.Opened) + { + _logger.LogInformation("Processing HandleNewAsk"); + await HandleNewAsk(issueNumber, parentNumber, skillName, labels[skillName], input, org, repo); + } + else if (issuesEvent.Action == IssuesAction.Closed && issuesEvent.Issue.User.Type.Value == UserType.Bot) + { + _logger.LogInformation("Processing HandleClosingIssue"); + await HandleClosingIssue(issueNumber, parentNumber, skillName, labels[skillName], org, repo); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Processing issue event"); + throw; + } + } + + protected override async Task ProcessIssueCommentWebhookAsync( + WebhookHeaders headers, + IssueCommentEvent issueCommentEvent, + IssueCommentAction action) + { + try + { + _logger.LogInformation("Processing issue comment event"); + var org = issueCommentEvent.Repository.Owner.Login; + var repo = issueCommentEvent.Repository.Name; + var issueNumber = issueCommentEvent.Issue.Number; + var input = issueCommentEvent.Comment.Body; + // Assumes the label follows the following convention: Skill.Function example: PM.Readme + var labels = issueCommentEvent.Issue.Labels + .Select(l => l.Name.Split('.')) + .Where(parts => parts.Length == 2) + .ToDictionary(parts => parts[0], parts => parts[1]); + var skillName = labels.Keys.Where(k => k != "Parent").FirstOrDefault(); + long? parentNumber = labels.ContainsKey("Parent") ? long.Parse(labels["Parent"]) : null; + var suffix = $"{org}-{repo}"; + // we only respond to non-bot comments + if (issueCommentEvent.Sender.Type.Value != UserType.Bot) + { + await HandleNewAsk(issueNumber, parentNumber, skillName, labels[skillName], input, org, repo); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Processing issue comment event"); + throw; + } + + } + + private async Task HandleClosingIssue(long issueNumber, long? parentNumber, string skillName, string functionName, string org, string repo) + { + var subject = $"{org}-{repo}-{issueNumber}"; + var eventType = (skillName, functionName) switch + { + ("PM", "Readme") => nameof(GithubFlowEventType.ReadmeChainClosed), + ("DevLead", "Plan") => nameof(GithubFlowEventType.DevPlanChainClosed), + ("Developer", "Implement") => nameof(GithubFlowEventType.CodeChainClosed), + _ => nameof(GithubFlowEventType.NewAsk) + }; + var data = new Dictionary + { + { "org", org }, + { "repo", repo }, + { "issueNumber", issueNumber.ToString() }, + { "parentNumber", parentNumber?.ToString()} + }; + + var evt = new Event + { + Type = eventType, + Subject = subject, + Data = data + }; + await PublishEvent(evt); + } + + private async Task HandleNewAsk(long issueNumber, long? parentNumber, string skillName, string functionName, string input, string org, string repo) + { + try + { + _logger.LogInformation("Handling new ask"); + var subject = $"{org}-{repo}-{issueNumber}"; + var eventType = (skillName, functionName) switch + { + ("Do", "It") => nameof(GithubFlowEventType.NewAsk), + ("PM", "Readme") => nameof(GithubFlowEventType.ReadmeRequested), + ("DevLead", "Plan") => nameof(GithubFlowEventType.DevPlanRequested), + ("Developer", "Implement") => nameof(GithubFlowEventType.CodeGenerationRequested), + _ => nameof(GithubFlowEventType.NewAsk) + }; + var data = new Dictionary + { + { "org", org }, + { "repo", repo }, + { "issueNumber", issueNumber.ToString() }, + { "parentNumber", parentNumber?.ToString()}, + { "input" , input} + }; + var evt = new Event + { + Type = eventType, + Subject = subject, + Data = data + }; + await PublishEvent(evt); + } + catch (Exception ex) + { + _logger.LogError(ex, "Handling new ask"); + throw; + } + } + + private async Task PublishEvent(Event evt) + { + var metadata = new Dictionary() { + { "cloudevent.Type", evt.Type }, + { "cloudevent.Subject", evt.Subject }, + { "cloudevent.id", Guid.NewGuid().ToString()} + }; + + await _daprClient.PublishEventAsync(Consts.PubSub, Consts.MainTopic, evt, metadata); + } +} + + + diff --git a/samples/gh-flow/src/Microsoft.AI.DevTeam.Dapr/appsettings.azure.template.json b/samples/gh-flow/src/Microsoft.AI.DevTeam.Dapr/appsettings.azure.template.json new file mode 100644 index 00000000000..2fc698845a9 --- /dev/null +++ b/samples/gh-flow/src/Microsoft.AI.DevTeam.Dapr/appsettings.azure.template.json @@ -0,0 +1,45 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Information", + "Orleans.Streams": "Information" + } + }, + "ApplicationInsights": { + "ConnectionString": "" + }, + "AllowedHosts": "*", + "SANDBOX_IMAGE" : "mcr.microsoft.com/dotnet/sdk:7.0", + "GithubOptions" : { + "AppKey": "", + "AppId": "", + "InstallationId": "", + "WebhookSecret": "" + }, + "AzureOptions" : { + "SubscriptionId":"", + "Location":"", + "ContainerInstancesResourceGroup":"", + "FilesShareName":"", + "FilesAccountName":"", + "FilesAccountKey":"", + "SandboxImage" : "mcr.microsoft.com/dotnet/sdk:7.0", + "ManagedIdentity": "" + }, + "OpenAIOptions" : { + "ServiceType":"AzureOpenAI", + "ServiceId":"gpt-4", + "DeploymentOrModelId":"gpt-4", + "EmbeddingDeploymentOrModelId":"text-embedding-ada-002", + "Endpoint":"", + "ApiKey":"" + }, + "QdrantOptions" : { + "Endpoint" : "http://qdrant:6333", + "VectorSize" : "1536" + }, + "ServiceOptions" : { + "IngesterUrl" : "http://localhost:7071" + } +} diff --git a/samples/gh-flow/src/Microsoft.AI.DevTeam.Dapr/appsettings.local.template.json b/samples/gh-flow/src/Microsoft.AI.DevTeam.Dapr/appsettings.local.template.json new file mode 100644 index 00000000000..2fc698845a9 --- /dev/null +++ b/samples/gh-flow/src/Microsoft.AI.DevTeam.Dapr/appsettings.local.template.json @@ -0,0 +1,45 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Information", + "Orleans.Streams": "Information" + } + }, + "ApplicationInsights": { + "ConnectionString": "" + }, + "AllowedHosts": "*", + "SANDBOX_IMAGE" : "mcr.microsoft.com/dotnet/sdk:7.0", + "GithubOptions" : { + "AppKey": "", + "AppId": "", + "InstallationId": "", + "WebhookSecret": "" + }, + "AzureOptions" : { + "SubscriptionId":"", + "Location":"", + "ContainerInstancesResourceGroup":"", + "FilesShareName":"", + "FilesAccountName":"", + "FilesAccountKey":"", + "SandboxImage" : "mcr.microsoft.com/dotnet/sdk:7.0", + "ManagedIdentity": "" + }, + "OpenAIOptions" : { + "ServiceType":"AzureOpenAI", + "ServiceId":"gpt-4", + "DeploymentOrModelId":"gpt-4", + "EmbeddingDeploymentOrModelId":"text-embedding-ada-002", + "Endpoint":"", + "ApiKey":"" + }, + "QdrantOptions" : { + "Endpoint" : "http://qdrant:6333", + "VectorSize" : "1536" + }, + "ServiceOptions" : { + "IngesterUrl" : "http://localhost:7071" + } +} diff --git a/samples/gh-flow/src/Microsoft.AI.DevTeam/Agents/AzureGenie.cs b/samples/gh-flow/src/Microsoft.AI.DevTeam/Agents/AzureGenie.cs index 15eaf0692f1..421f59d470c 100644 --- a/samples/gh-flow/src/Microsoft.AI.DevTeam/Agents/AzureGenie.cs +++ b/samples/gh-flow/src/Microsoft.AI.DevTeam/Agents/AzureGenie.cs @@ -1,6 +1,7 @@ using Microsoft.AI.Agents.Abstractions; using Microsoft.AI.Agents.Orleans; using Microsoft.AI.DevTeam.Events; +using Newtonsoft.Json.Linq; namespace Microsoft.AI.DevTeam; @@ -21,17 +22,22 @@ public override async Task HandleEvent(Event item) { case nameof(GithubFlowEventType.ReadmeCreated): { - var parentNumber = long.Parse(item.Data["parentNumber"]); - var issueNumber = long.Parse(item.Data["issueNumber"]); - await Store(item.Data["org"], item.Data["repo"], parentNumber, issueNumber, "readme", "md", "output", item.Message); + var data = item.Data; + var parentNumber = long.Parse(data["parentNumber"].ToString()); + var issueNumber = long.Parse(data["issueNumber"].ToString()); + var org = data["org"].ToString(); + var repo = data["repo"].ToString(); + var subject = $"{org}/{repo}/{issueNumber}"; + await Store(org,repo, parentNumber, issueNumber, "readme", "md", "output", data["readme"].ToString()); await PublishEvent(Consts.MainNamespace, this.GetPrimaryKeyString(), new Event { Type = nameof(GithubFlowEventType.ReadmeStored), + Subject = subject, Data = new Dictionary { - { "org", item.Data["org"] }, - { "repo", item.Data["repo"] }, - { "issueNumber", item.Data["issueNumber"] }, - { "parentNumber", item.Data["parentNumber"] } + { "org", org }, + { "repo", repo }, + { "issueNumber", $"{issueNumber}" }, + { "parentNumber", $"{parentNumber}" } } }); } @@ -39,18 +45,23 @@ public override async Task HandleEvent(Event item) break; case nameof(GithubFlowEventType.CodeCreated): { - var parentNumber = long.Parse(item.Data["parentNumber"]); - var issueNumber = long.Parse(item.Data["issueNumber"]); - await Store(item.Data["org"], item.Data["repo"], parentNumber, issueNumber, "run", "sh", "output", item.Message); - await RunInSandbox(item.Data["org"], item.Data["repo"], parentNumber, issueNumber); + var data = item.Data; + var parentNumber = long.Parse(data["parentNumber"].ToString()); + var issueNumber = long.Parse(data["issueNumber"].ToString()); + var org = data["org"].ToString(); + var repo = data["repo"].ToString(); + var subject = $"{org}/{repo}/{issueNumber}"; + await Store(org,repo, parentNumber, issueNumber, "run", "sh", "output", data["code"].ToString()); + await RunInSandbox(org, repo, parentNumber, issueNumber); await PublishEvent(Consts.MainNamespace, this.GetPrimaryKeyString(), new Event { Type = nameof(GithubFlowEventType.SandboxRunCreated), + Subject = subject, Data = new Dictionary { - { "org", item.Data["org"] }, - { "repo", item.Data["repo"] }, - { "issueNumber", item.Data["issueNumber"] }, - { "parentNumber", item.Data["parentNumber"] } + { "org", org }, + { "repo", repo }, + { "issueNumber", $"{issueNumber}" }, + { "parentNumber", $"{parentNumber}" } } }); } diff --git a/samples/gh-flow/src/Microsoft.AI.DevTeam/Agents/Developer/Developer.cs b/samples/gh-flow/src/Microsoft.AI.DevTeam/Agents/Developer/Developer.cs index a9b3187e283..40254e0540c 100644 --- a/samples/gh-flow/src/Microsoft.AI.DevTeam/Agents/Developer/Developer.cs +++ b/samples/gh-flow/src/Microsoft.AI.DevTeam/Agents/Developer/Developer.cs @@ -11,10 +11,10 @@ namespace Microsoft.AI.DevTeam; public class Dev : AiAgent, IDevelopApps { protected override string Namespace => Consts.MainNamespace; - + private readonly ILogger _logger; - public Dev([PersistentState("state", "messages")] IPersistentState> state, Kernel kernel, ISemanticTextMemory memory, ILogger logger) + public Dev([PersistentState("state", "messages")] IPersistentState> state, Kernel kernel, ISemanticTextMemory memory, ILogger logger) : base(state, memory, kernel) { _logger = logger; @@ -25,33 +25,34 @@ public async override Task HandleEvent(Event item) switch (item.Type) { case nameof(GithubFlowEventType.CodeGenerationRequested): - var code = await GenerateCode(item.Message); - await PublishEvent(Consts.MainNamespace, this.GetPrimaryKeyString(), new Event { - Type = nameof(GithubFlowEventType.CodeGenerated), - Data = new Dictionary { - { "org", item.Data["org"] }, - { "repo", item.Data["repo"] }, - { "issueNumber", item.Data["issueNumber"] }, - { "code", code } - }, - Message = code - }); + var context = item.ToGithubContext(); + var code = await GenerateCode(item.Data["input"]); + var data = context.ToData(); + data["result"] = code; + await PublishEvent(Consts.MainNamespace, this.GetPrimaryKeyString(), new Event + { + Type = nameof(GithubFlowEventType.CodeGenerated), + Subject = context.Subject, + Data = data + }); + } + break; case nameof(GithubFlowEventType.CodeChainClosed): - var lastCode = _state.State.History.Last().Message; - await PublishEvent(Consts.MainNamespace, this.GetPrimaryKeyString(), new Event { - Type = nameof(GithubFlowEventType.CodeCreated), - Data = new Dictionary { - { "org", item.Data["org"] }, - { "repo", item.Data["repo"] }, - { "issueNumber", item.Data["issueNumber"] }, - { "code", lastCode }, - { "parentNumber", item.Data["parentNumber"] } - }, - Message = lastCode - }); + var context = item.ToGithubContext(); + var lastCode = _state.State.History.Last().Message; + var data = context.ToData(); + data["code"] = lastCode; + await PublishEvent(Consts.MainNamespace, this.GetPrimaryKeyString(), new Event + { + Type = nameof(GithubFlowEventType.CodeCreated), + Subject = context.Subject, + Data = data + }); + } + break; default: break; @@ -63,9 +64,9 @@ public async Task GenerateCode(string ask) try { // TODO: ask the architect for the high level architecture as well as the files structure of the project - var context = new KernelArguments { ["input"] = AppendChatHistory(ask)}; + var context = new KernelArguments { ["input"] = AppendChatHistory(ask) }; var instruction = "Consider the following architectural guidelines:!waf!"; - var enhancedContext = await AddKnowledge(instruction, "waf",context); + var enhancedContext = await AddKnowledge(instruction, "waf", context); return await CallFunction(DeveloperSkills.Implement, enhancedContext); } catch (Exception ex) diff --git a/samples/gh-flow/src/Microsoft.AI.DevTeam/Agents/Developer/DeveloperPrompts.cs b/samples/gh-flow/src/Microsoft.AI.DevTeam/Agents/Developer/DeveloperPrompts.cs index 1f6baf07f6f..d9d66602075 100644 --- a/samples/gh-flow/src/Microsoft.AI.DevTeam/Agents/Developer/DeveloperPrompts.cs +++ b/samples/gh-flow/src/Microsoft.AI.DevTeam/Agents/Developer/DeveloperPrompts.cs @@ -8,7 +8,7 @@ Please output the code required to accomplish the task assigned to you below and Make specific choices about implementation. Do not offer a range of options. Use comments in the code to describe the intent. Do not include other text other than code and code comments. Input: {{$input}} - {{$wafContext}} + {{$waf}} """; public static string Improve = """ @@ -20,7 +20,7 @@ Wrap the code output up in a bash script that creates the necessary files by ove Make specific choices about implementation. Do not offer a range of options. Use comments in the code to describe the intent. Do not include other text other than code and code comments. Input: {{$input}} - {{$wafContext}} + {{$waf}} """; public static string Explain = """ diff --git a/samples/gh-flow/src/Microsoft.AI.DevTeam/Agents/DeveloperLead/DevLeadPrompts.cs b/samples/gh-flow/src/Microsoft.AI.DevTeam/Agents/DeveloperLead/DevLeadPrompts.cs index 1522ade43d7..2c20812a57a 100644 --- a/samples/gh-flow/src/Microsoft.AI.DevTeam/Agents/DeveloperLead/DevLeadPrompts.cs +++ b/samples/gh-flow/src/Microsoft.AI.DevTeam/Agents/DeveloperLead/DevLeadPrompts.cs @@ -32,7 +32,7 @@ In each LLM prompt restrict the model from outputting other text that is not in Do not output any other text. Do not wrap the JSON in any other text, output the JSON format described above, making sure it's a valid JSON. Input: {{$input}} - {{$wafContext}} + {{$waf}} """; public static string Explain = """ diff --git a/samples/gh-flow/src/Microsoft.AI.DevTeam/Agents/DeveloperLead/DeveloperLead.cs b/samples/gh-flow/src/Microsoft.AI.DevTeam/Agents/DeveloperLead/DeveloperLead.cs index e26ebb210e2..1e55fd84406 100644 --- a/samples/gh-flow/src/Microsoft.AI.DevTeam/Agents/DeveloperLead/DeveloperLead.cs +++ b/samples/gh-flow/src/Microsoft.AI.DevTeam/Agents/DeveloperLead/DeveloperLead.cs @@ -3,6 +3,7 @@ using Microsoft.AI.DevTeam.Events; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Memory; +using Newtonsoft.Json.Linq; using Orleans.Runtime; namespace Microsoft.AI.DevTeam; @@ -23,33 +24,34 @@ public async override Task HandleEvent(Event item) switch (item.Type) { case nameof(GithubFlowEventType.DevPlanRequested): - var plan = await CreatePlan(item.Message); - await PublishEvent(Consts.MainNamespace, this.GetPrimaryKeyString(), new Event { - Type = nameof(GithubFlowEventType.DevPlanGenerated), - Data = new Dictionary { - { "org", item.Data["org"] }, - { "repo", item.Data["repo"] }, - { "issueNumber", item.Data["issueNumber"] }, - { "plan", plan } - }, - Message = plan - }); + var context = item.ToGithubContext(); + var plan = await CreatePlan(item.Data["input"]); + var data = context.ToData(); + data["result"] = plan; + await PublishEvent(Consts.MainNamespace, this.GetPrimaryKeyString(), new Event + { + Type = nameof(GithubFlowEventType.DevPlanGenerated), + Subject = context.Subject, + Data = data + }); + } + break; case nameof(GithubFlowEventType.DevPlanChainClosed): - var latestPlan = _state.State.History.Last().Message; - await PublishEvent(Consts.MainNamespace, this.GetPrimaryKeyString(), new Event { - Type = nameof(GithubFlowEventType.DevPlanCreated), - Data = new Dictionary { - { "org", item.Data["org"] }, - { "repo", item.Data["repo"] }, - { "issueNumber", item.Data["issueNumber"] }, - {"parentNumber", item.Data["parentNumber"]}, - { "plan", latestPlan } - }, - Message = latestPlan - }); + var context = item.ToGithubContext(); + var latestPlan = _state.State.History.Last().Message; + var data = context.ToData(); + data["plan"] = latestPlan; + await PublishEvent(Consts.MainNamespace, this.GetPrimaryKeyString(), new Event + { + Type = nameof(GithubFlowEventType.DevPlanCreated), + Subject = context.Subject, + Data = data + }); + } + break; default: break; diff --git a/samples/gh-flow/src/Microsoft.AI.DevTeam/Agents/Hubber.cs b/samples/gh-flow/src/Microsoft.AI.DevTeam/Agents/Hubber.cs index 8deb38c4cc6..85a495e254f 100644 --- a/samples/gh-flow/src/Microsoft.AI.DevTeam/Agents/Hubber.cs +++ b/samples/gh-flow/src/Microsoft.AI.DevTeam/Agents/Hubber.cs @@ -22,49 +22,52 @@ public override async Task HandleEvent(Event item) { case nameof(GithubFlowEventType.NewAsk): { - var parentNumber = long.Parse(item.Data["issueNumber"]); - var pmIssue = await CreateIssue(item.Data["org"], item.Data["repo"], item.Message, "PM.Readme", parentNumber); - var devLeadIssue = await CreateIssue(item.Data["org"], item.Data["repo"], item.Message, "DevLead.Plan", parentNumber); - await PostComment(item.Data["org"], item.Data["repo"], parentNumber, $" - #{pmIssue} - tracks PM.Readme"); - await PostComment(item.Data["org"], item.Data["repo"], parentNumber, $" - #{devLeadIssue} - tracks DevLead.Plan"); - await CreateBranch(item.Data["org"], item.Data["repo"], $"sk-{parentNumber}"); + var context = item.ToGithubContext(); + var pmIssue = await CreateIssue(context.Org, context.Repo , item.Data["input"], "PM.Readme", context.IssueNumber); + var devLeadIssue = await CreateIssue(context.Org, context.Repo , item.Data["input"], "DevLead.Plan", context.IssueNumber); + await PostComment(context.Org, context.Repo, context.IssueNumber, $" - #{pmIssue} - tracks PM.Readme"); + await PostComment(context.Org, context.Repo, context.IssueNumber, $" - #{devLeadIssue} - tracks DevLead.Plan"); + await CreateBranch(context.Org, context.Repo, $"sk-{context.IssueNumber}"); } break; case nameof(GithubFlowEventType.ReadmeGenerated): case nameof(GithubFlowEventType.DevPlanGenerated): case nameof(GithubFlowEventType.CodeGenerated): - var contents = string.IsNullOrEmpty(item.Message)? "Sorry, I got tired, can you try again please? ": item.Message; - await PostComment(item.Data["org"], item.Data["repo"], long.Parse(item.Data["issueNumber"]), contents); + { + var context = item.ToGithubContext(); + var result = item.Data["result"]; + var contents = string.IsNullOrEmpty(result)? "Sorry, I got tired, can you try again please? ": result; + await PostComment(context.Org,context.Repo, context.IssueNumber, contents); + } break; case nameof(GithubFlowEventType.DevPlanCreated): { + var context = item.ToGithubContext(); var plan = JsonSerializer.Deserialize(item.Data["plan"]); var prompts = plan.steps.SelectMany(s => s.subtasks.Select(st => st.prompt)); - var parentNumber = long.Parse(item.Data["parentNumber"]); + foreach (var prompt in prompts) { var functionName = "Developer.Implement"; - var issue = await CreateIssue(item.Data["org"], item.Data["repo"], prompt, functionName, parentNumber); + var issue = await CreateIssue(context.Org, context.Repo, prompt, functionName, context.ParentNumber.Value); var commentBody = $" - #{issue} - tracks {functionName}"; - await PostComment(item.Data["org"], item.Data["repo"], parentNumber, commentBody); + await PostComment(context.Org, context.Repo, context.ParentNumber.Value, commentBody); } } break; case nameof(GithubFlowEventType.ReadmeStored): { - var parentNumber = long.Parse(item.Data["parentNumber"]); - var issueNumber = long.Parse(item.Data["issueNumber"]); - var branch = $"sk-{parentNumber}"; - await CommitToBranch(item.Data["org"], item.Data["repo"], parentNumber, issueNumber, "output", branch); - await CreatePullRequest(item.Data["org"], item.Data["repo"], parentNumber, branch); + var context = item.ToGithubContext(); + var branch = $"sk-{context.ParentNumber}"; + await CommitToBranch(context.Org, context.Repo, context.ParentNumber.Value, context.IssueNumber, "output", branch); + await CreatePullRequest(context.Org, context.Repo, context.ParentNumber.Value, branch); } break; case nameof(GithubFlowEventType.SandboxRunFinished): { - var parentNumber = long.Parse(item.Data["parentNumber"]); - var issueNumber = long.Parse(item.Data["issueNumber"]); - var branch = $"sk-{parentNumber}"; - await CommitToBranch(item.Data["org"], item.Data["repo"], parentNumber, issueNumber, "output", branch); + var context = item.ToGithubContext(); + var branch = $"sk-{context.ParentNumber}"; + await CommitToBranch(context.Org, context.Repo, context.ParentNumber.Value, context.IssueNumber, "output", branch); } break; default: diff --git a/samples/gh-flow/src/Microsoft.AI.DevTeam/Agents/ProductManager/PMPrompts.cs b/samples/gh-flow/src/Microsoft.AI.DevTeam/Agents/ProductManager/PMPrompts.cs index fdaa156ed43..7683010e418 100644 --- a/samples/gh-flow/src/Microsoft.AI.DevTeam/Agents/ProductManager/PMPrompts.cs +++ b/samples/gh-flow/src/Microsoft.AI.DevTeam/Agents/ProductManager/PMPrompts.cs @@ -7,7 +7,7 @@ Please write a bash script with the commands that would be required to generate You may include commands to build the applications but do not run them. Do not include any git commands. Input: {{$input}} - {{$wafContext}} + {{$waf}} """; public static string Readme = """ You are a program manager on a software development team. You are working on an app described below. @@ -15,7 +15,7 @@ You are a program manager on a software development team. You are working on an Do not describe how to create the application. Write the README as if it were documenting the features and architecture of the application. You may include instructions for how to run the application. Input: {{$input}} - {{$wafContext}} + {{$waf}} """; public static string Explain = """ diff --git a/samples/gh-flow/src/Microsoft.AI.DevTeam/Agents/ProductManager/ProductManager.cs b/samples/gh-flow/src/Microsoft.AI.DevTeam/Agents/ProductManager/ProductManager.cs index 77818534c0f..3ffbc8a59ca 100644 --- a/samples/gh-flow/src/Microsoft.AI.DevTeam/Agents/ProductManager/ProductManager.cs +++ b/samples/gh-flow/src/Microsoft.AI.DevTeam/Agents/ProductManager/ProductManager.cs @@ -24,31 +24,32 @@ public async override Task HandleEvent(Event item) switch (item.Type) { case nameof(GithubFlowEventType.ReadmeRequested): - var readme = await CreateReadme(item.Message); + { + var context = item.ToGithubContext(); + var readme = await CreateReadme(item.Data["input"]); + var data = context.ToData(); + data["result"]=readme; await PublishEvent(Consts.MainNamespace, this.GetPrimaryKeyString(), new Event { Type = nameof(GithubFlowEventType.ReadmeGenerated), - Data = new Dictionary { - { "org", item.Data["org"] }, - { "repo", item.Data["repo"] }, - { "issueNumber", item.Data["issueNumber"] }, - { "readme", readme } - }, - Message = readme + Subject = context.Subject, + Data = data }); + } + break; case nameof(GithubFlowEventType.ReadmeChainClosed): + { + var context = item.ToGithubContext(); var lastReadme = _state.State.History.Last().Message; + var data = context.ToData(); + data["readme"] = lastReadme; await PublishEvent(Consts.MainNamespace, this.GetPrimaryKeyString(), new Event { Type = nameof(GithubFlowEventType.ReadmeCreated), - Data = new Dictionary { - { "org", item.Data["org"] }, - { "repo", item.Data["repo"] }, - { "issueNumber", item.Data["issueNumber"] }, - { "readme", lastReadme }, - { "parentNumber", item.Data["parentNumber"] } - }, - Message = lastReadme + Subject = context.Subject, + Data = data }); + } + break; default: break; diff --git a/samples/gh-flow/src/Microsoft.AI.DevTeam/Agents/Sandbox.cs b/samples/gh-flow/src/Microsoft.AI.DevTeam/Agents/Sandbox.cs index 17e1d2ce850..6185af676aa 100644 --- a/samples/gh-flow/src/Microsoft.AI.DevTeam/Agents/Sandbox.cs +++ b/samples/gh-flow/src/Microsoft.AI.DevTeam/Agents/Sandbox.cs @@ -2,14 +2,13 @@ using Microsoft.AI.Agents.Orleans; using Microsoft.AI.DevTeam.Events; using Orleans.Runtime; -using Orleans.Streams; using Orleans.Timers; namespace Microsoft.AI.DevTeam; [ImplicitStreamSubscription(Consts.MainNamespace)] public class Sandbox : Agent, IRemindable { - protected override string Namespace => Consts.MainNamespace; + protected override string Namespace => Consts.MainNamespace; private const string ReminderName = "SandboxRunReminder"; private readonly IManageAzure _azService; private readonly IReminderRegistry _reminderRegistry; @@ -26,18 +25,18 @@ public Sandbox([PersistentState("state", "messages")] IPersistentState ToData(this GithubContext context) + { + return new Dictionary { + { "org", context.Org }, + { "repo", context.Repo }, + { "issueNumber", $"{context.IssueNumber}" }, + { "parentNumber", context.ParentNumber.HasValue? default: context.ParentNumber.ToString() } + }; + } + + + } + + public class GithubContext + { + public string Org { get; set; } + public string Repo { get; set; } + public long IssueNumber { get; set; } + public long? ParentNumber { get; set; } + + public string Subject => $"{Org}/{Repo}/{IssueNumber}"; + } +} + + diff --git a/samples/gh-flow/src/Microsoft.AI.DevTeam/Microsoft.AI.DevTeam.csproj b/samples/gh-flow/src/Microsoft.AI.DevTeam/Microsoft.AI.DevTeam.csproj index 99bd757bf9c..22c1acdd698 100644 --- a/samples/gh-flow/src/Microsoft.AI.DevTeam/Microsoft.AI.DevTeam.csproj +++ b/samples/gh-flow/src/Microsoft.AI.DevTeam/Microsoft.AI.DevTeam.csproj @@ -17,7 +17,6 @@ - @@ -44,9 +43,7 @@ - - - + diff --git a/samples/gh-flow/src/Microsoft.AI.DevTeam/Program.cs b/samples/gh-flow/src/Microsoft.AI.DevTeam/Program.cs index 64081aae8dd..0cb04c08d72 100644 --- a/samples/gh-flow/src/Microsoft.AI.DevTeam/Program.cs +++ b/samples/gh-flow/src/Microsoft.AI.DevTeam/Program.cs @@ -87,15 +87,16 @@ builder.Host.UseOrleans(siloBuilder => { siloBuilder.UseDashboard(x => x.HostSelf = true); - siloBuilder.Services.AddSerializer( sb => { - sb.AddNewtonsoftJsonSerializer(isSupported: t => true); - }); if (builder.Environment.IsDevelopment()) { siloBuilder.UseLocalhostClustering() .AddMemoryStreams("StreamProvider") .AddMemoryGrainStorage("PubSubStore") .AddMemoryGrainStorage("messages"); + + siloBuilder.UseInMemoryReminderService(); + siloBuilder.UseDashboard(x => x.HostSelf = true); + siloBuilder.UseInMemoryReminderService(); } else @@ -144,7 +145,6 @@ .AddMemoryStreams("StreamProvider") .AddMemoryGrainStorage("PubSubStore"); } - }); builder.Services.Configure(options => diff --git a/samples/gh-flow/src/Microsoft.AI.DevTeam/Services/GithubWebHookProcessor.cs b/samples/gh-flow/src/Microsoft.AI.DevTeam/Services/GithubWebHookProcessor.cs index d415823a90f..e661988b16c 100644 --- a/samples/gh-flow/src/Microsoft.AI.DevTeam/Services/GithubWebHookProcessor.cs +++ b/samples/gh-flow/src/Microsoft.AI.DevTeam/Services/GithubWebHookProcessor.cs @@ -1,5 +1,4 @@ using Microsoft.AI.Agents.Abstractions; -using Microsoft.AI.DevTeam; using Microsoft.AI.DevTeam.Events; using Octokit.Webhooks; using Octokit.Webhooks.Events; @@ -97,8 +96,9 @@ protected override async Task ProcessIssueCommentWebhookAsync( private async Task HandleClosingIssue(long issueNumber, long? parentNumber, string skillName, string functionName, string suffix, string org, string repo) { + var subject = suffix+issueNumber.ToString(); var streamProvider = _client.GetStreamProvider("StreamProvider"); - var streamId = StreamId.Create(Consts.MainNamespace, suffix+issueNumber.ToString()); + var streamId = StreamId.Create(Consts.MainNamespace, subject); var stream = streamProvider.GetStream(streamId); var eventType = (skillName, functionName) switch { @@ -118,6 +118,7 @@ private async Task HandleClosingIssue(long issueNumber, long? parentNumber, stri await stream.OnNextAsync(new Event { Type = eventType, + Subject = subject, Data = data }); } @@ -127,8 +128,9 @@ private async Task HandleNewAsk(long issueNumber, long? parentNumber, string ski try { _logger.LogInformation("Handling new ask"); + var subject = suffix+issueNumber.ToString(); var streamProvider = _client.GetStreamProvider("StreamProvider"); - var streamId = StreamId.Create(Consts.MainNamespace, suffix+issueNumber.ToString()); + var streamId = StreamId.Create(Consts.MainNamespace, subject); var stream = streamProvider.GetStream(streamId); var eventType = (skillName, functionName) switch @@ -144,12 +146,14 @@ private async Task HandleNewAsk(long issueNumber, long? parentNumber, string ski { "org", org }, { "repo", repo }, { "issueNumber", issueNumber.ToString() }, - { "parentNumber", parentNumber?.ToString()} + { "parentNumber", parentNumber?.ToString()}, + { "input", input} + }; await stream.OnNextAsync(new Event { Type = eventType, - Message = input, + Subject = subject, Data = data }); } diff --git a/samples/seed-memory/Dockerfile b/samples/gh-flow/src/seed-memory/Dockerfile similarity index 100% rename from samples/seed-memory/Dockerfile rename to samples/gh-flow/src/seed-memory/Dockerfile diff --git a/samples/seed-memory/Program.cs b/samples/gh-flow/src/seed-memory/Program.cs similarity index 100% rename from samples/seed-memory/Program.cs rename to samples/gh-flow/src/seed-memory/Program.cs diff --git a/samples/seed-memory/README.md b/samples/gh-flow/src/seed-memory/README.md similarity index 100% rename from samples/seed-memory/README.md rename to samples/gh-flow/src/seed-memory/README.md diff --git a/samples/seed-memory/azure-well-architected.pdf b/samples/gh-flow/src/seed-memory/azure-well-architected.pdf similarity index 100% rename from samples/seed-memory/azure-well-architected.pdf rename to samples/gh-flow/src/seed-memory/azure-well-architected.pdf diff --git a/samples/seed-memory/config/KernelSettings.cs b/samples/gh-flow/src/seed-memory/config/KernelSettings.cs similarity index 100% rename from samples/seed-memory/config/KernelSettings.cs rename to samples/gh-flow/src/seed-memory/config/KernelSettings.cs diff --git a/samples/seed-memory/config/appsettings.template.json b/samples/gh-flow/src/seed-memory/config/appsettings.template.json similarity index 100% rename from samples/seed-memory/config/appsettings.template.json rename to samples/gh-flow/src/seed-memory/config/appsettings.template.json diff --git a/samples/seed-memory/seed-memory.csproj b/samples/gh-flow/src/seed-memory/seed-memory.csproj similarity index 100% rename from samples/seed-memory/seed-memory.csproj rename to samples/gh-flow/src/seed-memory/seed-memory.csproj diff --git a/samples/marketing/src/backend/Agents/CommunityManager/CommunityManager.cs b/samples/marketing/src/backend/Agents/CommunityManager/CommunityManager.cs index 6fa5413f816..cd2416bf171 100644 --- a/samples/marketing/src/backend/Agents/CommunityManager/CommunityManager.cs +++ b/samples/marketing/src/backend/Agents/CommunityManager/CommunityManager.cs @@ -36,17 +36,18 @@ public async override Task HandleEvent(Event item) await SendDesignedCreatedEvent(lastMessage, item.Data["UserId"]); break; - case nameof(EventTypes.ArticleCreated): - //var lastCode = _state.State.History.Last().Message; + case nameof(EventTypes.ArticleCreated): + { + var article = item.Data["article"]; - _logger.LogInformation($"[{nameof(GraphicDesigner)}] Event {nameof(EventTypes.ArticleCreated)}. UserMessage: {item.Message}"); + _logger.LogInformation($"[{nameof(GraphicDesigner)}] Event {nameof(EventTypes.ArticleCreated)}. Article: {article}"); - var context = new KernelArguments { ["input"] = AppendChatHistory(item.Message) }; + var context = new KernelArguments { ["input"] = AppendChatHistory(article) }; string socialMediaPost = await CallFunction(CommunityManagerPrompts.WritePost, context); _state.State.Data.WrittenSocialMediaPost = socialMediaPost; await SendDesignedCreatedEvent(socialMediaPost, item.Data["UserId"]); break; - + } default: break; } @@ -59,8 +60,8 @@ private async Task SendDesignedCreatedEvent(string socialMediaPost, string userI Type = nameof(EventTypes.SocialMediaPostCreated), Data = new Dictionary { { "UserId", userId }, - }, - Message = socialMediaPost + { nameof(socialMediaPost), socialMediaPost} + } }); } diff --git a/samples/marketing/src/backend/Agents/GraphicDesigner/GraphicDesigner.cs b/samples/marketing/src/backend/Agents/GraphicDesigner/GraphicDesigner.cs index e3ad64bfad4..8830f6d2602 100644 --- a/samples/marketing/src/backend/Agents/GraphicDesigner/GraphicDesigner.cs +++ b/samples/marketing/src/backend/Agents/GraphicDesigner/GraphicDesigner.cs @@ -42,10 +42,11 @@ public async override Task HandleEvent(Event item) break; case nameof(EventTypes.ArticleCreated): - _logger.LogInformation($"[{nameof(GraphicDesigner)}] Event {nameof(EventTypes.ArticleCreated)}. UserMessage: {item.Message}"); - + //TODO + _logger.LogInformation($"[{nameof(GraphicDesigner)}] Event {nameof(EventTypes.ArticleCreated)}."); + var article = item.Data["article"]; var dallEService = _kernel.GetRequiredService(); - var imageUri = await dallEService.GenerateImageAsync(item.Message, 1024, 1024); + var imageUri = await dallEService.GenerateImageAsync(article, 1024, 1024); _state.State.Data.imageUrl = imageUri; @@ -58,15 +59,15 @@ public async override Task HandleEvent(Event item) } } - private async Task SendDesignedCreatedEvent(string AbsoluteImageUri, string userId) + private async Task SendDesignedCreatedEvent(string imageUri, string userId) { await PublishEvent(Consts.OrleansNamespace, this.GetPrimaryKeyString(), new Event { Type = nameof(EventTypes.GraphicDesignCreated), Data = new Dictionary { { "UserId", userId }, - }, - Message = AbsoluteImageUri + { nameof(imageUri), imageUri} + } }); } } \ No newline at end of file diff --git a/samples/marketing/src/backend/Agents/SignalR/SignalR.cs b/samples/marketing/src/backend/Agents/SignalR/SignalR.cs index ca4af8df56e..8d4ee4ea98d 100644 --- a/samples/marketing/src/backend/Agents/SignalR/SignalR.cs +++ b/samples/marketing/src/backend/Agents/SignalR/SignalR.cs @@ -27,17 +27,17 @@ public async override Task HandleEvent(Event item) switch (item.Type) { case nameof(EventTypes.ArticleCreated): - var writenArticle = item.Message; + var writenArticle = item.Data["article"]; await _signalRClient.SendMessageToSpecificClient(item.Data["UserId"], writenArticle, AgentTypes.Chat); break; case nameof(EventTypes.GraphicDesignCreated): - var imageUrl = item.Message; + var imageUrl = item.Data["imageUri"]; await _signalRClient.SendMessageToSpecificClient(item.Data["UserId"], imageUrl, AgentTypes.GraphicDesigner); break; case nameof(EventTypes.SocialMediaPostCreated): - var post = item.Message; + var post = item.Data["socialMediaPost"]; await _signalRClient.SendMessageToSpecificClient(item.Data["UserId"], post, AgentTypes.CommunityManager); break; diff --git a/samples/marketing/src/backend/Agents/Writer/Writer.cs b/samples/marketing/src/backend/Agents/Writer/Writer.cs index d2811175c10..ba13b151576 100644 --- a/samples/marketing/src/backend/Agents/Writer/Writer.cs +++ b/samples/marketing/src/backend/Agents/Writer/Writer.cs @@ -38,30 +38,31 @@ public async override Task HandleEvent(Event item) break; case nameof(EventTypes.UserChatInput): + { + var userMessage = item.Data["userMessage"]; + _logger.LogInformation($"[{nameof(GraphicDesigner)}] Event {nameof(EventTypes.UserChatInput)}. UserMessage: {userMessage}"); - _logger.LogInformation($"[{nameof(GraphicDesigner)}] Event {nameof(EventTypes.UserChatInput)}. UserMessage: {item.Message}"); - - var context = new KernelArguments { ["input"] = AppendChatHistory(item.Message) }; - string newArticle = await CallFunction(WriterPrompts.Write, context); - - await SendDesignedCreatedEvent(newArticle, item.Data["UserId"]); + var context = new KernelArguments { ["input"] = AppendChatHistory(userMessage) }; + string newArticle = await CallFunction(WriterPrompts.Write, context); - break; + await SendDesignedCreatedEvent(newArticle, item.Data["UserId"]); + break; + } + default: break; } } - private async Task SendDesignedCreatedEvent(string writtenArticle, string userId) + private async Task SendDesignedCreatedEvent(string article, string userId) { await PublishEvent(Consts.OrleansNamespace, this.GetPrimaryKeyString(), new Event { Type = nameof(EventTypes.ArticleCreated), Data = new Dictionary { { "UserId", userId }, - { "UserMessage", writtenArticle }, - }, - Message = writtenArticle + { nameof(article), article }, + } }); } diff --git a/samples/marketing/src/backend/Controller/Articles.cs b/samples/marketing/src/backend/Controller/Articles.cs index fd83028fb9d..78cd09fc3ba 100644 --- a/samples/marketing/src/backend/Controller/Articles.cs +++ b/samples/marketing/src/backend/Controller/Articles.cs @@ -54,7 +54,6 @@ public async Task Put(string UserId, [FromBody] string userMessage) await stream.OnNextAsync(new Event { Type = nameof(EventTypes.UserChatInput), - Message = userMessage, Data = data }); diff --git a/samples/marketing/src/backend/Program.cs b/samples/marketing/src/backend/Program.cs index f9e79925c44..2bba0d21ada 100644 --- a/samples/marketing/src/backend/Program.cs +++ b/samples/marketing/src/backend/Program.cs @@ -10,6 +10,7 @@ using Marketing.SignalRHub; using Marketing.Options; using Marketing; +using Orleans.Serialization; var builder = WebApplication.CreateBuilder(args); builder.Services.AddTransient(CreateKernel); @@ -54,14 +55,6 @@ .ValidateDataAnnotations() .ValidateOnStart(); -builder.Services.AddOptions() - .Configure((settings, configuration) => - { - configuration.GetSection(nameof(ServiceOptions)).Bind(settings); - }) - .ValidateDataAnnotations() - .ValidateOnStart(); - builder.Host.UseOrleans(siloBuilder => { siloBuilder.UseLocalhostClustering() diff --git a/samples/marketing/src/backend/SignalRHub/ArticleHub.cs b/samples/marketing/src/backend/SignalRHub/ArticleHub.cs index c531fc22347..f60876a2a67 100644 --- a/samples/marketing/src/backend/SignalRHub/ArticleHub.cs +++ b/samples/marketing/src/backend/SignalRHub/ArticleHub.cs @@ -41,7 +41,6 @@ public async Task ProcessMessage(FrontEndMessage frontEndMessage, IClusterClient await stream.OnNextAsync(new Event { Type = nameof(EventTypes.UserChatInput), - Message = frontEndMessage.Message, Data = data }); @@ -70,7 +69,6 @@ public async Task ConnectToAgent(string UserId, IClusterClient clusterClient) await stream.OnNextAsync(new Event { Type = nameof(EventTypes.UserConnected), - Message = frontEndMessage.Message, Data = data }); } diff --git a/samples/marketing/src/backend/appsettings.local.template.json b/samples/marketing/src/backend/appsettings.local.template.json index 6104e4e1b2c..68a8effeb05 100644 --- a/samples/marketing/src/backend/appsettings.local.template.json +++ b/samples/marketing/src/backend/appsettings.local.template.json @@ -14,13 +14,15 @@ }, "AllowedHosts": "*", - "SANDBOX_IMAGE": "mcr.microsoft.com/dotnet/sdk:7.0", "OpenAIOptions": { "ChatDeploymentOrModelId": "gpt-4-32", - "EmbeddingDeploymentOrModelId": "text-embedding-ada-002", "ChatEndpoint": "https://.openai.azure.com/", "ChatApiKey": "", + "EmbeddingsDeploymentOrModelId": "text-embedding-ada-002", + "EmbeddingsEndpoint": "https://.openai.azure.com/", + "EmbeddingsApiKey": "", + "ImageDeploymentOrModelId":"dalle3", "ImageEndpoint": "https://.openai.azure.com/", "ImageApiKey": "" }, @@ -28,10 +30,5 @@ "QdrantOptions": { "Endpoint": "http://qdrant:6333", "VectorSize": "1536" - }, - - "ServiceOptions": { - "IngesterUrl": "http://localhost:7071" } - } \ No newline at end of file diff --git a/src/Microsoft.AI.Agents.Dapr/Agent.cs b/src/Microsoft.AI.Agents.Dapr/Agent.cs index 50162e4de19..104bd0ed9f8 100644 --- a/src/Microsoft.AI.Agents.Dapr/Agent.cs +++ b/src/Microsoft.AI.Agents.Dapr/Agent.cs @@ -1,6 +1,27 @@ -namespace Microsoft.AI.Agents.Dapr; +using Dapr.Actors.Runtime; +using Dapr.Client; +using Microsoft.AI.Agents.Abstractions; -public class Agent +namespace Microsoft.AI.Agents.Dapr; + +public abstract class Agent : Actor, IAgent { + private readonly DaprClient daprClient; + + protected Agent(ActorHost host, DaprClient daprClient) : base(host) + { + this.daprClient = daprClient; + } + public abstract Task HandleEvent(Event item); + public async Task PublishEvent(string ns, string id, Event item) + { + var metadata = new Dictionary() { + { "cloudevent.Type", item.Type }, + { "cloudevent.Subject", item.Subject }, + { "cloudevent.id", Guid.NewGuid().ToString()} + }; + + await daprClient.PublishEventAsync(ns, id, item, metadata); + } } diff --git a/src/Microsoft.AI.Agents.Dapr/AiAgent.cs b/src/Microsoft.AI.Agents.Dapr/AiAgent.cs new file mode 100644 index 00000000000..1537defb706 --- /dev/null +++ b/src/Microsoft.AI.Agents.Dapr/AiAgent.cs @@ -0,0 +1,80 @@ +using System.Text; +using Dapr.Actors; +using Dapr.Actors.Runtime; +using Dapr.Client; +using Microsoft.AI.Agents.Abstractions; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Connectors.OpenAI; +using Microsoft.SemanticKernel.Memory; + +namespace Microsoft.AI.Agents.Dapr; + +public abstract class AiAgent : Agent, IAiAgent where T: class, new() +{ + public string StateStore = "agents-statestore"; + public AiAgent(ActorHost host, DaprClient client,ISemanticTextMemory memory, Kernel kernel) + : base(host, client) + { + _memory = memory; + _kernel = kernel; + } + private readonly ISemanticTextMemory _memory; + private readonly Kernel _kernel; + + protected AgentState state; + + + protected override async Task OnActivateAsync() + { + state = await StateManager.GetOrAddStateAsync(StateStore, new AgentState()); + } + + public void AddToHistory(string message, ChatUserType userType) + { + if (state.History == null) state.History = new List(); + state.History.Add(new ChatHistoryItem + { + Message = message, + Order = state.History.Count + 1, + UserType = userType + }); + } + + public string AppendChatHistory(string ask) + { + AddToHistory(ask, ChatUserType.User); + return string.Join("\n", state.History.Select(message => $"{message.UserType}: {message.Message}")); + } + + public virtual async Task CallFunction(string template, KernelArguments arguments, OpenAIPromptExecutionSettings? settings = null) + { + var propmptSettings = (settings == null) ? new OpenAIPromptExecutionSettings { MaxTokens = 18000, Temperature = 0.8, TopP = 1 } + : settings; + var function = _kernel.CreateFunctionFromPrompt(template, propmptSettings); + var result = (await _kernel.InvokeAsync(function, arguments)).ToString(); + AddToHistory(result, ChatUserType.Agent); + await StateManager.SetStateAsync( + StateStore, + state); + return result; + } + + /// + /// Adds knowledge to the + /// + /// The instruction string that uses the value of !index! as a placeholder to inject the data. Example:"Consider the following architectural guidelines: {waf}" + /// Knowledge index + /// The sk arguments, "input" is the argument + /// + public async Task AddKnowledge(string instruction, string index, KernelArguments arguments) + { + var documents = _memory.SearchAsync(index, arguments["input"].ToString(), 5); + var kbStringBuilder = new StringBuilder(); + await foreach (var doc in documents) + { + kbStringBuilder.AppendLine($"{doc.Metadata.Text}"); + } + arguments[index] = instruction.Replace($"!{index}!", $"{kbStringBuilder}"); + return arguments; + } +} diff --git a/src/Microsoft.AI.Agents.Dapr/IDaprAgent.cs b/src/Microsoft.AI.Agents.Dapr/IDaprAgent.cs new file mode 100644 index 00000000000..228359fc627 --- /dev/null +++ b/src/Microsoft.AI.Agents.Dapr/IDaprAgent.cs @@ -0,0 +1,9 @@ +using Dapr.Actors; +using Microsoft.AI.Agents.Abstractions; + +namespace Microsoft.AI.Agents.Dapr; + +public interface IDaprAgent : IActor +{ + Task HandleEvent(Event item); +} \ No newline at end of file diff --git a/src/Microsoft.AI.Agents.Dapr/Microsoft.AI.Agents.Dapr.csproj b/src/Microsoft.AI.Agents.Dapr/Microsoft.AI.Agents.Dapr.csproj index e8bccdc5814..ed1eed20ba7 100644 --- a/src/Microsoft.AI.Agents.Dapr/Microsoft.AI.Agents.Dapr.csproj +++ b/src/Microsoft.AI.Agents.Dapr/Microsoft.AI.Agents.Dapr.csproj @@ -7,11 +7,12 @@ + - - + + diff --git a/src/Microsoft.AI.Agents.Orleans/EventSurrogate.cs b/src/Microsoft.AI.Agents.Orleans/EventSurrogate.cs new file mode 100644 index 00000000000..451cd3391a2 --- /dev/null +++ b/src/Microsoft.AI.Agents.Orleans/EventSurrogate.cs @@ -0,0 +1,32 @@ +using Microsoft.AI.Agents.Abstractions; + +namespace Microsoft.AI.Agents.Orleans; + +[GenerateSerializer] +public struct EventSurrogate +{ + [Id(0)] + public Dictionary Data { get; set; } + [Id(1)] + public string Type { get; set; } + [Id(2)] + public string Subject { get; set; } +} + +[RegisterConverter] +public sealed class EventSurrogateConverter : + IConverter +{ + public Event ConvertFromSurrogate( + in EventSurrogate surrogate) => + new Event { Data = surrogate.Data, Subject = surrogate.Subject, Type = surrogate.Type}; + + public EventSurrogate ConvertToSurrogate( + in Event value) => + new EventSurrogate + { + Data = value.Data, + Type = value.Type, + Subject = value.Subject + }; +} diff --git a/src/Microsoft.AI.Agents/Abstractions/Event.cs b/src/Microsoft.AI.Agents/Abstractions/Event.cs index 098778f6aab..95b64ba2b74 100644 --- a/src/Microsoft.AI.Agents/Abstractions/Event.cs +++ b/src/Microsoft.AI.Agents/Abstractions/Event.cs @@ -1,16 +1,12 @@ -using Orleans; -using Orleans.CodeGeneration; +using System.Runtime.Serialization; namespace Microsoft.AI.Agents.Abstractions { - [GenerateSerializer] + [DataContract] public class Event { - [Id(0)] - public string Message { get; set; } - [Id(1)] public Dictionary Data { get; set; } - [Id(2)] public string Type { get; set; } + public string Subject { get; set; } } } \ No newline at end of file diff --git a/src/Microsoft.AI.Agents/Abstractions/IAgent.cs b/src/Microsoft.AI.Agents/Abstractions/IAgent.cs index aa49238be57..d05c4c37a28 100644 --- a/src/Microsoft.AI.Agents/Abstractions/IAgent.cs +++ b/src/Microsoft.AI.Agents/Abstractions/IAgent.cs @@ -4,4 +4,4 @@ public interface IAgent { Task HandleEvent(Event item); Task PublishEvent(string ns, string id, Event item); -} +} \ No newline at end of file diff --git a/src/Microsoft.AI.Agents/Microsoft.AI.Agents.csproj b/src/Microsoft.AI.Agents/Microsoft.AI.Agents.csproj index 1469d8037c2..7ae7220d856 100644 --- a/src/Microsoft.AI.Agents/Microsoft.AI.Agents.csproj +++ b/src/Microsoft.AI.Agents/Microsoft.AI.Agents.csproj @@ -7,21 +7,7 @@ - - - - - - - - - - - - - - - + \ No newline at end of file