Merge branch 'elsa3new'

This commit is contained in:
Ryan Sweet 2023-07-18 09:06:39 -07:00
commit 81b84eceec
29 changed files with 1115 additions and 0 deletions

12
.gitignore vendored
View File

@ -482,3 +482,15 @@ $RECYCLE.BIN/
# Vim temporary swap files
*.swp
<<<<<<< HEAD
=======
# SQLite workflows DB
elsa.sqlite.*
# env files
.env
# ignore local elsa-core src
elsa-core/
>>>>>>> elsa3new

6
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,6 @@
{
"recommendations": [
"ms-azuretools.vscode-azurefunctions",
"ms-dotnettools.csharp"
]
}

11
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,11 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Attach to .NET Functions",
"type": "coreclr",
"request": "attach",
"processId": "${command:azureFunctions.pickProcess}"
}
]
}

View File

@ -1,3 +1,12 @@
{
<<<<<<< HEAD
"dotnet.defaultSolution": "sk-dev-team.sln"
=======
"dotnet.defaultSolution": "sk-dev-team.sln",
"azureFunctions.deploySubpath": "sk-azfunc-server/bin/Release/net7.0/publish",
"azureFunctions.projectLanguage": "C#",
"azureFunctions.projectRuntime": "~4",
"debug.internalConsoleOptions": "neverOpen",
"azureFunctions.preDeployTask": "publish (functions)"
>>>>>>> elsa3new
}

81
.vscode/tasks.json vendored Normal file
View File

@ -0,0 +1,81 @@
{
"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",
"dependsOn": "build (functions)",
"options": {
"cwd": "${workspaceFolder}/sk-azfunc-server/bin/Debug/net7.0"
},
"command": "host start",
"isBackground": true,
"problemMatcher": "$func-dotnet-watch"
}
]
}

View File

@ -0,0 +1,240 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Elsa;
using Elsa.Expressions.Models;
using Elsa.Extensions;
using Elsa.Workflows.Core;
using Elsa.Workflows.Core.Contracts;
using Elsa.Workflows.Core.Models;
using Elsa.Workflows.Management.Extensions;
using Elsa.Workflows.Core.Attributes;
using Elsa.Workflows.Core.Models;
using Elsa.Expressions.Models;
using Elsa.Extensions;
using Elsa.Http;
using Microsoft.Extensions.Logging;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Connectors.Memory.Qdrant;
using Microsoft.SemanticKernel.Connectors.AI.OpenAI.TextEmbedding;
using Microsoft.SemanticKernel.Memory;
using Microsoft.SemanticKernel.Orchestration;
using Microsoft.SemanticKernel.Reliability;
using Microsoft.SemanticKernel.SkillDefinition;
using Microsoft.SKDevTeam;
namespace Elsa.SemanticKernel;
//<summary>
// Loads the Semantic Kernel skills and then generates activites for each skill
//</summary>
public class SemanticKernelActivityProvider : IActivityProvider
{
private readonly IActivityFactory _activityFactory;
private readonly IActivityDescriber _activityDescriber;
public SemanticKernelActivityProvider(IActivityFactory activityFactory, IActivityDescriber activityDescriber)
{
_activityFactory = activityFactory;
_activityDescriber = activityDescriber;
}
public async ValueTask<IEnumerable<ActivityDescriptor>> GetDescriptorsAsync(CancellationToken cancellationToken = default)
{
// get the kernel
var kernel = KernelBuilder();
// get a list of skills in the assembly
var skills = LoadSkillsFromAssemblyAsync("skills", kernel);
SKContext context = kernel.CreateNewContext();
var functionsAvailable = context.Skills.GetFunctionsView();
// create activity descriptors for each skilland function
var activities = new List<ActivityDescriptor>();
foreach (KeyValuePair<string, List<FunctionView>> skill in functionsAvailable.SemanticFunctions)
{
Console.WriteLine($"Creating Activities for Skill: {skill.Key}");
foreach (FunctionView func in skill.Value)
{
activities.Add(CreateActivityDescriptorFromSkillAndFunction(func, cancellationToken));
}
}
return activities;
}
/// <summary>
/// Creates an activity descriptor from a skill and function.
/// </summary>
/// <param name="function">The semantic kernel function</param>
/// <param name="cancellationToken">An optional cancellation token.</param>
/// <returns>An activity descriptor.</returns>
private ActivityDescriptor CreateActivityDescriptorFromSkillAndFunction(FunctionView function, CancellationToken cancellationToken = default)
{
// Create a fully qualified type name for the activity
var thisNamespace = GetType().Namespace;
var fullTypeName = $"{thisNamespace}.{function.SkillName}.{function.Name}";
Console.WriteLine($"Creating Activity: {fullTypeName}");
// create inputs from the function parameters - the SemanticKernelSkill activity will be the base for each activity
var inputs = new List<InputDescriptor>();
foreach (var p in function.Parameters) { inputs.Add(CreateInputDescriptorFromSKParameter(p)); }
inputs.Add(CreateInputDescriptor(typeof(string), "SkillName", function.SkillName, "The name of the skill to use (generated, do not change)"));
inputs.Add(CreateInputDescriptor(typeof(string), "FunctionName", function.Name, "The name of the function to use (generated, do not change)"));
inputs.Add(CreateInputDescriptor(typeof(int), "MaxRetries", KernelSettings.DefaultMaxRetries, "Max Retries to contact AI Service"));
return new ActivityDescriptor
{
Kind = ActivityKind.Task,
Category = "Semantic Kernel",
Description = function.Description,
Name = function.Name,
TypeName = fullTypeName,
Namespace = $"{thisNamespace}.{function.SkillName}",
DisplayName = $"{function.SkillName}.{function.Name}",
Inputs = inputs,
Outputs = new[] {new OutputDescriptor()},
Constructor = context =>
{
// The constructor is called when an activity instance of this type is requested.
// Create the activity instance.
var activityInstance = _activityFactory.Create<SemanticKernelSkill>(context);
// Customize the activity type name.
activityInstance.Type = fullTypeName;
// Configure the activity's URL and method properties.
activityInstance.SkillName = new Input<string?>(function.SkillName);
activityInstance.FunctionName = new Input<string?>(function.Name);
return activityInstance;
}
};
}
/// <summary>
/// Creates an input descriptor for a single line string
/// </summary>
/// <param name="name">The name of the input field</param>
/// <param name="description">The description of the input field</param>
private InputDescriptor CreateInputDescriptor(Type inputType, string name, Object defaultValue, string description)
{
var inputDescriptor = new InputDescriptor
{
Description = description,
DefaultValue = defaultValue,
Type = inputType,
Name = name,
DisplayName = name,
IsSynthetic = true, // This is a synthetic property, i.e. it is not part of the activity's .NET type.
IsWrapped = true, // This property is wrapped within an Input<T> object.
UIHint = InputUIHints.SingleLine,
ValueGetter = activity => activity.SyntheticProperties.GetValueOrDefault(name),
ValueSetter = (activity, value) => activity.SyntheticProperties[name] = value!,
};
return inputDescriptor;
}
/// <summary>
/// Creates an input descriptor from an sk funciton parameter definition.
/// </summary>
/// <param name="parameter">The function parameter.</param>
/// <returns>An input descriptor.</returns>
private InputDescriptor CreateInputDescriptorFromSKParameter(ParameterView parameter)
{
var inputDescriptor = new InputDescriptor
{
Description = string.IsNullOrEmpty(parameter.Description) ? parameter.Name : parameter.Description,
DefaultValue = string.IsNullOrEmpty(parameter.DefaultValue) ? string.Empty : parameter.DefaultValue,
Type = typeof(string),
Name = parameter.Name,
DisplayName = parameter.Name,
IsSynthetic = true, // This is a synthetic property, i.e. it is not part of the activity's .NET type.
IsWrapped = true, // This property is wrapped within an Input<T> object.
UIHint = InputUIHints.MultiLine,
ValueGetter = activity => activity.SyntheticProperties.GetValueOrDefault(parameter.Name),
ValueSetter = (activity, value) => activity.SyntheticProperties[parameter.Name] = value!,
};
return inputDescriptor;
}
///<summary>
/// Gets a list of the skills in the assembly
///</summary>
private IEnumerable<string> LoadSkillsFromAssemblyAsync(string assemblyName, IKernel kernel)
{
var skills = new List<string>();
var assembly = Assembly.Load(assemblyName);
Type[] skillTypes = assembly.GetTypes().ToArray();
foreach (Type skillType in skillTypes)
{
if (skillType.Namespace.Equals("Microsoft.SKDevTeam"))
{
skills.Add(skillType.Name);
var functions = skillType.GetFields();
foreach (var function in functions)
{
string field = function.FieldType.ToString();
if (field.Equals("Microsoft.SKDevTeam.SemanticFunctionConfig"))
{
var skillConfig = SemanticFunctionConfig.ForSkillAndFunction(skillType.Name, function.Name);
var skfunc = kernel.CreateSemanticFunction(
skillConfig.PromptTemplate,
skillConfig.Name,
skillConfig.SkillName,
skillConfig.Description,
skillConfig.MaxTokens,
skillConfig.Temperature,
skillConfig.TopP,
skillConfig.PPenalty,
skillConfig.FPenalty);
Console.WriteLine($"SKActivityProvider Added SK function: {skfunc.SkillName}.{skfunc.Name}");
}
}
}
}
return skills;
}
/// <summary>
/// Gets a semantic kernel instance
/// </summary>
/// <returns>Microsoft.SemanticKernel.IKernel</returns>
private IKernel KernelBuilder()
{
var kernelSettings = KernelSettings.LoadSettings();
var kernelConfig = new KernelConfig();
using ILoggerFactory loggerFactory = LoggerFactory.Create(builder =>
{
builder.SetMinimumLevel(kernelSettings.LogLevel ?? LogLevel.Warning);
});
var kernel = new KernelBuilder()
.WithLogger(loggerFactory.CreateLogger<IKernel>())
.WithAzureChatCompletionService(kernelSettings.DeploymentOrModelId, kernelSettings.Endpoint, kernelSettings.ApiKey, true, kernelSettings.ServiceId, true)
.WithConfiguration(kernelConfig)
.Configure(c => c.SetDefaultHttpRetryConfig(new HttpRetryConfig
{
MaxRetryCount = KernelSettings.DefaultMaxRetries,
UseExponentialBackoff = true,
// MinRetryDelay = TimeSpan.FromSeconds(2),
// MaxRetryDelay = TimeSpan.FromSeconds(8),
MaxTotalRetryTime = TimeSpan.FromSeconds(300),
// RetryableStatusCodes = new[] { HttpStatusCode.TooManyRequests, HttpStatusCode.RequestTimeout },
// RetryableExceptions = new[] { typeof(HttpRequestException) }
}))
.Build();
return kernel;
}
}

View File

@ -0,0 +1,245 @@
using Elsa.Extensions;
using Elsa.Workflows.Core;
using Elsa.Workflows.Core.Attributes;
using Elsa.Workflows.Core.Models;
using JetBrains.Annotations;
using Microsoft.Extensions.Logging;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Connectors.Memory.Qdrant;
using Microsoft.SemanticKernel.Connectors.AI.OpenAI.TextEmbedding;
using Microsoft.SemanticKernel.Memory;
using Microsoft.SemanticKernel.Orchestration;
using Microsoft.SemanticKernel.Reliability;
using Microsoft.SemanticKernel.SkillDefinition;
using Microsoft.SKDevTeam;
using System;
using System.Text;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
namespace Elsa.SemanticKernel;
/// <summary>
/// Invoke a Semantic Kernel skill.
/// </summary>
[Activity("Elsa", "Semantic Kernel", "Invoke a Semantic Kernel skill. ", DisplayName = "Generic Semantic Kernel Skill", Kind = ActivityKind.Task)]
[PublicAPI]
public class SemanticKernelSkill : CodeActivity<string>
{
[Input(
Description = "System Prompt",
UIHint = InputUIHints.MultiLine,
DefaultValue = PromptDefaults.SystemPrompt)]
public Input<string> SysPrompt { get; set; } = default!;
[Input(
Description = "User Input Prompt",
UIHint = InputUIHints.MultiLine,
DefaultValue = PromptDefaults.UserPrompt)]
public Input<string> Prompt { get; set; }
[Input(
Description = "Max retries",
UIHint = InputUIHints.SingleLine,
DefaultValue = KernelSettings.DefaultMaxRetries)]
public Input<int> MaxRetries { get; set; }
[Input(
Description = "The skill to invoke from the semantic kernel",
UIHint = InputUIHints.SingleLine,
DefaultValue = "Chat")]
public Input<string> SkillName { get; set; }
[Input(
Description = "The function to invoke from the skill",
UIHint = InputUIHints.SingleLine,
DefaultValue = "ChatCompletion")]
public Input<string> FunctionName { get; set; }
/* [Input(
Description = "Mockup - don't actually call the AI, just output the prompts",
UIHint = InputUIHints.Checkbox,
DefaultValue = false)]
public Input<bool> Mockup { get; set; } */
/// <inheritdoc />
protected override async ValueTask ExecuteAsync(ActivityExecutionContext workflowContext)
{
var test = SkillName.Get(workflowContext);
var skillName = SkillName.Get(workflowContext);
var functionName = FunctionName.Get(workflowContext);
var systemPrompt = SysPrompt.Get(workflowContext);
var maxRetries = MaxRetries.Get(workflowContext);
var prompt = Prompt.Get(workflowContext);
//var mockup = Mockup.Get(workflowContext);
var mockup = false;
string info = ($"#################\nSkill: {skillName}\nFunction: {functionName}\nPrompt: {prompt}\n#################\n\n");
if (mockup)
{
workflowContext.SetResult(info);
}
else
{
// get the kernel
var kernel = KernelBuilder();
// load the skill
var skillConfig = SemanticFunctionConfig.ForSkillAndFunction(skillName, functionName);
var function = kernel.CreateSemanticFunction(skillConfig.PromptTemplate, skillConfig.Name, skillConfig.SkillName,
skillConfig.Description, skillConfig.MaxTokens, skillConfig.Temperature,
skillConfig.TopP, skillConfig.PPenalty, skillConfig.FPenalty);
// set the context (our prompt)
var contextVars = new ContextVariables();
contextVars.Set("input", prompt);
/* var interestingMemories = kernel.Memory.SearchAsync("ImportedMemories", prompt, 2);
var wafContext = "Consider the following contextual snippets:";
await foreach (var memory in interestingMemories)
{
wafContext += $"\n {memory.Metadata.Text}";
} */
//context.Set("wafContext", wafContext);
SKContext answer = await kernel.RunAsync(contextVars, function).ConfigureAwait(false);
string result = answer.Result;
workflowContext.SetResult(result);
}
}
/// <summary>
/// Load the skills into the kernel
/// </summary>
private string ListSkillsInKernel(IKernel kernel)
{
var theSkills = LoadSkillsFromAssemblyAsync("skills", kernel);
SKContext context = kernel.CreateNewContext();
var functionsAvailable = context.Skills.GetFunctionsView();
var list = new StringBuilder();
foreach (KeyValuePair<string, List<FunctionView>> skill in functionsAvailable.SemanticFunctions)
{
Console.WriteLine($"Skill: {skill.Key}");
foreach (FunctionView func in skill.Value)
{
// Function description
if (func.Description != null)
{
list.AppendLine($"// {func.Description}");
}
else
{
Console.WriteLine("{0}.{1} is missing a description", func.SkillName, func.Name);
list.AppendLine($"// Function {func.SkillName}.{func.Name}.");
}
// Function name
list.AppendLine($"{func.SkillName}.{func.Name}");
// Function parameters
foreach (var p in func.Parameters)
{
var description = string.IsNullOrEmpty(p.Description) ? p.Name : p.Description;
var defaultValueString = string.IsNullOrEmpty(p.DefaultValue) ? string.Empty : $" (default value: {p.DefaultValue})";
list.AppendLine($"Parameter \"{p.Name}\": {description} {defaultValueString}");
}
}
}
Console.WriteLine($"List of all skills ----- {list.ToString()}");
return list.ToString();
}
/// <summary>
/// Gets a semantic kernel instance
/// </summary>
/// <returns>Microsoft.SemanticKernel.IKernel</returns>
private IKernel KernelBuilder()
{
var kernelSettings = KernelSettings.LoadSettings();
var kernelConfig = new KernelConfig();
using ILoggerFactory loggerFactory = LoggerFactory.Create(builder =>
{
builder.SetMinimumLevel(kernelSettings.LogLevel ?? LogLevel.Warning);
});
/*
var memoryStore = new QdrantMemoryStore(new QdrantVectorDbClient("http://qdrant", 1536, port: 6333));
var embedingGeneration = new AzureTextEmbeddingGeneration(kernelSettings.EmbeddingDeploymentOrModelId, kernelSettings.Endpoint, kernelSettings.ApiKey);
var semanticTextMemory = new SemanticTextMemory(memoryStore, embedingGeneration);
*/
var kernel = new KernelBuilder()
.WithLogger(loggerFactory.CreateLogger<IKernel>())
.WithAzureChatCompletionService(kernelSettings.DeploymentOrModelId, kernelSettings.Endpoint, kernelSettings.ApiKey, true, kernelSettings.ServiceId, true)
//.WithMemory(semanticTextMemory)
.WithConfiguration(kernelConfig)
.Configure(c => c.SetDefaultHttpRetryConfig(new HttpRetryConfig
{
MaxRetryCount = KernelSettings.DefaultMaxRetries,
UseExponentialBackoff = true,
// MinRetryDelay = TimeSpan.FromSeconds(2),
// MaxRetryDelay = TimeSpan.FromSeconds(8),
MaxTotalRetryTime = TimeSpan.FromSeconds(300),
// RetryableStatusCodes = new[] { HttpStatusCode.TooManyRequests, HttpStatusCode.RequestTimeout },
// RetryableExceptions = new[] { typeof(HttpRequestException) }
}))
.Build();
return kernel;
}
///<summary>
/// Gets a list of the skills in the assembly
///</summary>
private IEnumerable<string> LoadSkillsFromAssemblyAsync(string assemblyName, IKernel kernel)
{
var skills = new List<string>();
var assembly = Assembly.Load(assemblyName);
Type[] skillTypes = assembly.GetTypes().ToArray();
foreach (Type skillType in skillTypes)
{
if (skillType.Namespace.Equals("Microsoft.SKDevTeam"))
{
skills.Add(skillType.Name);
var functions = skillType.GetFields();
foreach (var function in functions)
{
string field = function.FieldType.ToString();
if (field.Equals("Microsoft.SKDevTeam.SemanticFunctionConfig"))
{
var skillConfig = SemanticFunctionConfig.ForSkillAndFunction(skillType.Name, function.Name);
var skfunc = kernel.CreateSemanticFunction(
skillConfig.PromptTemplate,
skillConfig.Name,
skillConfig.SkillName,
skillConfig.Description,
skillConfig.MaxTokens,
skillConfig.Temperature,
skillConfig.TopP,
skillConfig.PPenalty,
skillConfig.FPenalty);
Console.WriteLine($"SK Added function: {skfunc.SkillName}.{skfunc.Name}");
}
}
}
}
return skills;
}
}

View File

@ -4,12 +4,19 @@ using Microsoft.Extensions.Logging;
using System.IO;
using System;
<<<<<<< HEAD
=======
>>>>>>> elsa3new
internal class KernelSettings
{
public const string DefaultConfigFile = "config/appsettings.json";
public const string OpenAI = "OPENAI";
public const string AzureOpenAI = "AZUREOPENAI";
<<<<<<< HEAD
=======
public const int DefaultMaxRetries = 9;
>>>>>>> elsa3new
[JsonPropertyName("serviceType")]
public string ServiceType { get; set; } = string.Empty;

View File

@ -0,0 +1,7 @@
internal static class PromptDefaults {
public const string SystemPrompt = @"You are fulfilling roles on a software development team.
Provide a response to the following prompt, do not provide any additional output.";
public const string UserPrompt = @"Let's build a ToDoList Application!";
}

View File

@ -5,11 +5,23 @@
<Description>
Activities for calling Semantic Kernel SDK
</Description>
<<<<<<< HEAD
<PackageTags>elsa module semantic kerne activities</PackageTags>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Elsa" Version="3.0.0-rc1" />
=======
<PackageTags>elsa module semantic kernel activities</PackageTags>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Elsa" Version="3.0.0-preview.552" />
<PackageReference Include="Elsa.Http" Version="3.0.0-preview.552" />
<PackageReference Include="Elsa.Workflows.Api" Version="3.0.0-preview.552" />
<PackageReference Include="Elsa.Workflows.Core" Version="3.0.0-preview.552" />
<PackageReference Include="Elsa.Workflows.Management" Version="3.0.0-preview.552" />
>>>>>>> elsa3new
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="7.0.0" />

View File

@ -3,7 +3,11 @@
<packageSources>
<clear/>
<<<<<<< HEAD
<add key="NuGet official package source" value="https://api.nuget.org/v3/index.json" />
=======
<!--add key="NuGet official package source" value="https://api.nuget.org/v3/index.json" /-->
>>>>>>> elsa3new
<add key="Jint prereleases" value="https://www.myget.org/F/jint/api/v3/index.json" />
<add key="Elsa prereleases" value="https://f.feedz.io/elsa-workflows/elsa-3/nuget/index.json" />
</packageSources>

136
README.md
View File

@ -1,3 +1,4 @@
<<<<<<< HEAD
# sk-dev-team
# Build a Virtual AI Dev Team using Semantic Kernel Skills
## Status
@ -56,12 +57,141 @@ The system will present a view that facilitates chain-of-thought coordination ac
* Logging service streaming back to azure logs analytics, app insights, and teams channel
* Deployment service actions/pipelines driven
* Azure Dev Skill lean into azure integrations crawl the azure estate to inventory a tenants existing resources to memory and help inform new code. Eg: you have a large azure sql estate? Ok, most likely you want to wire your new app to one of those dbs, etc….
=======
# sk-dev-team
# Build a Virtual AI Dev Team using Semantic Kernel Skills
## Status
This is a nascent project - we will use the README to describe the project's intent - as we build it out we will document what exists and eventually move roadmap/intent to the discussion.
## Trying it out
### Elsa.SemanticKernel
SemanticKernel Activity Provider for Elsa Workflows 3.x
The project supports running [Microsoft Semantic Kernel](https://github.com/microsoft/semantic-kernel) Skills as workflows using [Elsa Workflows](https://v3.elsaworkflows.io). You can build the workflows as .NET code or in the visual designer.
To run the designer:
```bash
> cd WorkflowsApp
> cp .env_example .env
# Edit the .env file to choose your AI model, add your API Endpoint, and secrets.
> bash .env
> dotnet build
> dotnet run
# Open browser to the URI in the console output
```
By Default you can use "admin" and "password" to login. Please review [Workflow Security](https://v3.elsaworkflows.io/docs/installation/aspnet-apps-workflow-server) for into on securing the app, using API tokens, and more.
To [invoke](https://v3.elsaworkflows.io/docs/guides/invoking-workflows) a workflow, first it must be "Published". If your workflow has a trigger activity, you can use that. When your workflow is ready, click the "Publish" button. You can also execute the workflow using the API. Then, find the Workflow Definition ID. From a command line, you can use "curl":
```bash
> curl --location 'https://localhost:5001/elsa/api/workflow-definitions/{workflow_definition_id}/execute' \
--header 'Content-Type: application/json' \
--header 'Authorization: ApiKey {api_key}' \
--data '{
}'
```
Once you have the app runing locally, you can login (admin/password - see the [Elsa Workflows](https://v3.elsaworkflows.io) for info about securing). Then you can click "new workflow" to begin building your workflow with semantic kernel skills.
1. Drag workflow Activity blocks into the designer, and examine the settings.
2. Connect the Activities to specify an order of operations.
3. You can use Workfflow Variables to pass state between activities.
1. Create a Workflow Variable, "MyVariable"
2. Click on the Activity that you want to use to populate the variable.
3. In the Settings box for the Activity, Click "Output"
4. Set the "Output" to the variable chosen.
5. Click the Activity that will use the variable. Click on "Settings".
6. Find the text box representing the variable that you want to populate, in this case usually "input".
7. Click the "..." widget above the text box, and select "javascript"
8. Set the value of the text box to
```javascript
`${getMyVariable()}`
```
9. Run the workflow.
## Via CLI
The easiest way to run the project is in Codespaces. Codespaces will start a qdrant instance for you.
1. Create a new codespace from the *code* button on the main branch.
2. Once the code space setup is finished, from the terminal:
```bash
> cd cli
cli> cp ../WorkflowsApp/.env_example .
# Edit the .env file to choose your AI model, add your API Endpoint, and secrets.
cli> bash .env
cli> dotnet build
cli> dotnet run --file util/ToDoListSamplePrompt.txt do it
```
You will find the output in the *output/* directory.
# Goal
From a natural language specification, set out to integrate a team of AI copilot skills into your teams dev process, either for discrete tasks on an existing repo (unit tests, pipeline expansions, PRs for specific intents), developing a new feature, or even building an application from scratch. Starting from an existing repo and a broad statement of intent, work with multiple AI copilot dev skills, each of which has a different emphasis - from architecture, to task breakdown, to plans for individual tasks, to code output, code review, efficiency, documentation, build, writing tests, setting up pipelines, deployment, integration tests, and then validation.
The system will present a view that facilitates chain-of-thought coordination across multiple trees of reasoning with the dev team skills.
## Proposed UX
* Possible UI: Start with an existing repo (GH or ADO), either populated or empty, and API Keys / config for access once configured / loaded split view between three columns:
* Settings/History/Tasks (allows browsing into each of the chats with a copilot dev team role) | [Central Window Chat interface with Copilot DevTeam] | Repo browsing/editing
* Alternate interface will be via VS Code plugin/other IDE plugins, following the plugin idiom for each IDE
* Settings include teams channel for conversations, repo config and api keys, model config and api keys, and any desired prompt template additions
* CLI: start simple with a CLI that can be passed a file as prompt input and takes optional arguments as to which skills to invoke
* User begins with specifying a repository and then statement of what they want to accomplish, natural language, as simple or as detailed as needed.
* SK DevTeam skill will use dialog to refine the intent as needed, returns a plan, proposes necessary steps
* User approves the plan or gives feedback, requests iteration
* Plan is parceled out to the appropriate further skills
* Eg, for a new app:
* Architecture is passed to DevLead skill gives plan/task breakdown.
* DevLead breaks down tasks into smaller tasks, each of these is fed to a skill to decide if it is a single code module or multiple
* Each module is further fed to a dev lead to break down again or specify a prompt for a coder
* Each code module prompt is fed to a coder
* Each module output from a coder is fed to a code reviewer (with context, specific goals)
* Each reviewer proposes changes, which result in a new prompt for the original coder
* Changes are accepted by the coder
* Each module fed to a builder
* If it doesnt build sent back to review
* (etc)
## Proposed Architecture
* SK Kernel Service ASP.NET Core Service with REST API
* SK Skills:
* PM Skill generates pot, word docs, describing app,
* Designer Skill mockups?
* Architect Skill proposes overall arch
* DevLead Skill proposes task breakdown
* CoderSkill builds code modules for each task
* ReviewerSkill improves code modules
* TestSkill writes tests
* Etc
* Web app: prompt front end and wizard style editor of app
* Build service sandboxes using branches and actions/pipelines 1st draft; Alternate ephemeral build containers
* Logging service streaming back to azure logs analytics, app insights, and teams channel
* Deployment service actions/pipelines driven
* Azure Dev Skill lean into azure integrations crawl the azure estate to inventory a tenants existing resources to memory and help inform new code. Eg: you have a large azure sql estate? Ok, most likely you want to wire your new app to one of those dbs, etc….
>>>>>>> elsa3new
# Contributing
This project welcomes contributions and suggestions. Most contributions require you to agree to a
Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us
<<<<<<< HEAD
the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com.
=======
the rights to use your contribution. For details, visit <https://cla.opensource.microsoft.com>.
>>>>>>> elsa3new
When you submit a pull request, a CLA bot will automatically determine whether you need to provide
a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions
@ -81,9 +211,15 @@ see the [LICENSE](LICENSE) file, and grant you a license to any code in the repo
Microsoft, Windows, Microsoft Azure and/or other Microsoft products and services referenced in the documentation
may be either trademarks or registered trademarks of Microsoft in the United States and/or other countries.
The licenses for this project do not grant you rights to use any Microsoft names, logos, or trademarks.
<<<<<<< HEAD
Microsoft's general trademark guidelines can be found at http://go.microsoft.com/fwlink/?LinkID=254653.
Privacy information can be found at https://privacy.microsoft.com/en-us/
=======
Microsoft's general trademark guidelines can be found at <http://go.microsoft.com/fwlink/?LinkID=254653>.
Privacy information can be found at <https://privacy.microsoft.com/en-us/>
>>>>>>> elsa3new
Microsoft and any contributors reserve all other rights, whether under their respective copyrights, patents,
or trademarks, whether by implication, estoppel or otherwise.

View File

@ -0,0 +1,8 @@
# Replace with your own values
export SERVICETYPE=AzureOpenAI
export SERVICEID=gpt-4
export DEPLOYMENTORMODELID=gpt-4
export EMBEDDINGDEPLOYMENTORMODELID=text-embedding-ada-002
export ENDPOINT="Error - you mus update your OpenAI Endpoint"
export APIKEY="Error - you must update your OpenAPI or Azure API key"

10
WorkflowsApp/NuGet.Config Normal file
View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<clear/>
<!--add key="NuGet official package source" value="https://api.nuget.org/v3/index.json" /-->
<add key="Jint prereleases" value="https://www.myget.org/F/jint/api/v3/index.json" />
<add key="Elsa prereleases" value="https://f.feedz.io/elsa-workflows/elsa-3/nuget/index.json" />
</packageSources>
</configuration>

View File

@ -0,0 +1,22 @@
@page "/"
@{
var serverUrl = $"{Request.Scheme}://{Request.Host}{Request.PathBase}{Request.Path}";
var apiUrl = serverUrl + "elsa/api";
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Elsa Workflows 3.0</title>
<link rel="stylesheet" href="https://rsms.me/inter/inter.css">
<link rel="stylesheet" href="_content/Elsa.Workflows.Designer/elsa-workflows-designer/elsa-workflows-designer.css">
<script src="_content/Elsa.Workflows.Designer/monaco-editor/min/vs/loader.js"></script>
<script type="module" src="_content/Elsa.Workflows.Designer/elsa-workflows-designer/elsa-workflows-designer.esm.js"></script>
</head>
<body>
<elsa-studio server="@apiUrl" monaco-lib-path="/_content/Elsa.Workflows.Designer/monaco-editor/min"></elsa-studio>
</body>
</html>

View File

@ -0,0 +1,2 @@
@namespace WorkflowsApp.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

56
WorkflowsApp/Program.cs Normal file
View File

@ -0,0 +1,56 @@
using Elsa.EntityFrameworkCore.Extensions;
using Elsa.EntityFrameworkCore.Modules.Management;
using Elsa.EntityFrameworkCore.Modules.Runtime;
using Elsa.Extensions;
using Elsa.Workflows.Core.Models;
using Elsa.Identity.Features;
using Elsa.SemanticKernel;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddElsa(elsa =>
{
// Configure management feature to use EF Core.
elsa.UseWorkflowManagement(management => management.UseEntityFrameworkCore(ef => ef.UseSqlite()));
elsa.UseWorkflowRuntime(runtime =>runtime.UseEntityFrameworkCore());
// Expose API endpoints.
elsa.UseWorkflowsApi();
// Add services for HTTP activities and workflow middleware.
elsa.UseHttp();
// Configure identity so that we can create a default admin user.
elsa.UseIdentity(identity =>
{
identity.UseAdminUserProvider();
identity.TokenOptions = options => options.SigningKey = "secret-token-signing-key";
});
// Use default authentication (JWT + API Key).
elsa.UseDefaultAuthentication(auth => auth.UseAdminApiKey());
// Add Semantic Kernel skill.
elsa.AddActivity<SemanticKernelSkill>();
});
// Add dynamic Activity Provider for SK skills.
builder.Services.AddActivityProvider<SemanticKernelActivityProvider>();
// Add Razor pages.
builder.Services.AddRazorPages();
var app = builder.Build();
// Configure the HTTP request pipeline.
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseAuthentication();
app.UseAuthorization();
app.UseWorkflowsApi();
app.UseWorkflows();
app.MapRazorPages();
app.Run();

View File

@ -0,0 +1,37 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:10492",
"sslPort": 44312
}
},
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://localhost:5181",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://localhost:7077;http://localhost:5181",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View File

@ -0,0 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Elsa" Version="3.0.0-preview.552" />
<PackageReference Include="Elsa.EntityFrameworkCore" Version="3.0.0-preview.552" />
<PackageReference Include="Elsa.EntityFrameworkCore.Sqlite" Version="3.0.0-preview.552" />
<PackageReference Include="Elsa.Http" Version="3.0.0-preview.552" />
<PackageReference Include="Elsa.Identity" Version="3.0.0-preview.552" />
<PackageReference Include="Elsa.Workflows.Api" Version="3.0.0-preview.552" />
<PackageReference Include="Elsa.Workflows.Designer" Version="3.0.0-preview.552" /><!--
<PackageReference Include="Elsa.EntityFrameworkCore" Version="3.0.0-rc1" />
<PackageReference Include="Elsa.EntityFrameworkCore.Sqlite" Version="3.0.0-rc1" />
<PackageReference Include="Elsa.Http" Version="3.0.0-rc1" />
<PackageReference Include="Elsa.Identity" Version="3.0.0-rc1" />
<PackageReference Include="Elsa.Workflows.Api" Version="3.0.0-rc1" />
<PackageReference Include="Elsa.Workflows.Designer" Version="3.0.0-rc1" /> -->
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Elsa.SemanticKernel\Elsa.SemanticKernel.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

View File

@ -7,7 +7,12 @@ using Microsoft.SemanticKernel.Connectors.Memory.Qdrant;
using Microsoft.SemanticKernel.Memory;
using Microsoft.SemanticKernel.Orchestration;
using Microsoft.SemanticKernel.Reliability;
<<<<<<< HEAD
using skills;
=======
using Microsoft.SKDevTeam;
>>>>>>> elsa3new
class Program
{
@ -34,7 +39,11 @@ class Program
var pmCommand = new Command("pm", "Commands for the PM team");
var pmReadmeCommand = new Command("readme", "Produce a Readme for a given input");
<<<<<<< HEAD
pmReadmeCommand.SetHandler(async (file, maxRetry) => await CallWithFile<string>(nameof(PM), PM.Readme , file.FullName, maxRetry), fileOption, maxRetryOption);
=======
pmReadmeCommand.SetHandler(async (file, maxRetry) => await CallWithFile<string>(nameof(PM), PM.Readme, file.FullName, maxRetry), fileOption, maxRetryOption);
>>>>>>> elsa3new
var pmBootstrapCommand = new Command("bootstrap", "Bootstrap a project for a given input");
pmBootstrapCommand.SetHandler(async (file, maxRetry) => await CallWithFile<string>(nameof(PM), PM.BootstrapProject, file.FullName, maxRetry), fileOption, maxRetryOption);
@ -51,7 +60,11 @@ class Program
var devPlanCommand = new Command("plan", "Implement the module for a given input");
devPlanCommand.SetHandler(async (file, maxRetry) => await CallWithFile<string>(nameof(Developer), Developer.Implement, file.FullName, maxRetry), fileOption, maxRetryOption);
devCommand.AddCommand(devPlanCommand);
<<<<<<< HEAD
=======
>>>>>>> elsa3new
rootCommand.AddCommand(pmCommand);
rootCommand.AddCommand(devleadCommand);
rootCommand.AddCommand(devCommand);
@ -67,7 +80,11 @@ class Program
Console.WriteLine($"Using output directory: {outputPath}");
<<<<<<< HEAD
var readme = await CallWithFile<string>(nameof(PM), PM.Readme , file, maxRetry);
=======
var readme = await CallWithFile<string>(nameof(PM), PM.Readme, file, maxRetry);
>>>>>>> elsa3new
string readmeFile = Path.Combine(outputPath.FullName, "README.md");
await SaveToFile(readmeFile, readme);
Console.WriteLine($"Saved README to {readmeFile}");
@ -83,12 +100,21 @@ class Program
var implementationTasks = plan.steps.SelectMany(
(step) => step.subtasks.Select(
<<<<<<< HEAD
async (subtask) => {
=======
async (subtask) =>
{
>>>>>>> elsa3new
Console.WriteLine($"Implementing {step.step}-{subtask.subtask}");
var implementationResult = string.Empty;
while (true)
{
<<<<<<< HEAD
try
=======
try
>>>>>>> elsa3new
{
implementationResult = await CallFunction<string>(nameof(Developer), Developer.Implement, subtask.LLM_prompt, maxRetry);
break;
@ -105,7 +131,12 @@ class Program
}
await sandboxSkill.RunInDotnetAlpineAsync(implementationResult);
await SaveToFile(Path.Combine(outputPath.FullName, $"{step.step}-{subtask.subtask}.sh"), implementationResult);
<<<<<<< HEAD
return implementationResult; }));
=======
return implementationResult;
}));
>>>>>>> elsa3new
await Task.WhenAll(implementationTasks);
}
@ -115,8 +146,13 @@ class Program
}
public static async Task<T> CallWithFile<T>(string skillName, string functionName, string filePath, int maxRetry)
<<<<<<< HEAD
{
if(!File.Exists(filePath))
=======
{
if (!File.Exists(filePath))
>>>>>>> elsa3new
throw new FileNotFoundException($"File not found: {filePath}", filePath);
var input = File.ReadAllText(filePath);
return await CallFunction<T>(skillName, functionName, input, maxRetry);
@ -173,10 +209,19 @@ class Program
var answer = await kernel.RunAsync(context, function).ConfigureAwait(false);
var result = typeof(T) != typeof(string) ? JsonSerializer.Deserialize<T>(answer.ToString()) : (T)(object)answer.ToString();
//Console.WriteLine(answer);
<<<<<<< HEAD
=======
>>>>>>> elsa3new
return result;
}
}
public static class PM { public static string Readme = "Readme"; public static string BootstrapProject = "BootstrapProject"; }
<<<<<<< HEAD
public static class DevLead { public static string Plan="Plan"; }
public static class Developer { public static string Implement="Implement"; public static string Improve="Improve";}
=======
public static class DevLead { public static string Plan = "Plan"; }
public static class Developer { public static string Implement = "Implement"; public static string Improve = "Improve"; }
>>>>>>> elsa3new

View File

@ -6,7 +6,12 @@ using Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Attributes;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Orchestration;
using Models;
<<<<<<< HEAD
using skills;
=======
using Microsoft.SKDevTeam;
>>>>>>> elsa3new
public class ExecuteFunctionEndpoint
{
@ -35,6 +40,13 @@ public class ExecuteFunctionEndpoint
try
{
var functionRequest = await JsonSerializer.DeserializeAsync<ExecuteFunctionRequest>(requestData.Body, s_jsonOptions).ConfigureAwait(false);
<<<<<<< HEAD
=======
if (functionRequest == null)
{
return await CreateResponseAsync(requestData, HttpStatusCode.BadRequest, new ErrorResponse() { Message = $"Invalid request body." }).ConfigureAwait(false);
}
>>>>>>> elsa3new
var skillConfig = SemanticFunctionConfig.ForSkillAndFunction(skillName, functionName);
var function = _kernel.CreateSemanticFunction(skillConfig.PromptTemplate, skillConfig.Name, skillConfig.SkillName,

21
skills/Chat.cs Normal file
View File

@ -0,0 +1,21 @@
namespace Microsoft.SKDevTeam;
public static class Chat
{
public static SemanticFunctionConfig ChatCompletion = new SemanticFunctionConfig
{
PromptTemplate = """
You are a helpful assistant. Please complete the prompt as instructed in the Input.
Provide as many references and links as needed to support the accuracy of your answer.
Input: {{$input}}
""",
Name = nameof(ChatCompletion),
SkillName = nameof(Chat),
Description = "Use the Model as a Chatbot.",
MaxTokens = 6500,
Temperature = 0.0,
TopP = 0.0,
PPenalty = 0.0,
FPenalty = 0.0
};
}

29
skills/CodeExplainer.cs Normal file
View File

@ -0,0 +1,29 @@
namespace Microsoft.SKDevTeam;
public static class CodeExplainer {
public static SemanticFunctionConfig Explain = new SemanticFunctionConfig
{
PromptTemplate = """
You are a Software Developer.
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}}
""",
Name = nameof(Explain),
SkillName = nameof(CodeExplainer),
Description = "From a description of a coding task out put the code or scripts necessary to complete the task.",
MaxTokens = 6500,
Temperature = 0.0,
TopP = 0.0,
PPenalty = 0.0,
FPenalty = 0.0
};
}

View File

@ -1,4 +1,8 @@
<<<<<<< HEAD
namespace skills;
=======
namespace Microsoft.SKDevTeam;
>>>>>>> elsa3new
public static class DevLead {
public static SemanticFunctionConfig Plan = new SemanticFunctionConfig
{
@ -9,7 +13,46 @@ public static class DevLead {
For each step or module then break down the steps or subtasks required to complete that step or module.
For each subtask write an LLM prompt that would be used to tell a model to write the coee that will accomplish that subtask. If the subtask involves taking action/running commands tell the model to write the script that will run those commands.
In each LLM prompt restrict the model from outputting other text that is not in the form of code or code comments.
<<<<<<< HEAD
Please output a JSON data structure with a list of steps and a description of each step, and the steps or subtasks that each requires, and the LLM prompts for each subtask.
=======
Please output a JSON array data structure with a list of steps and a description of each step, and the steps or subtasks that each requires, and the LLM prompts for each subtask.
Example:
[
{
"step": "Step 1",
"description": "This is the first step",
"subtasks": [
{
"subtask": "Subtask 1",
"description": "This is the first subtask",
"prompt": "Write the code to do the first subtask"
},
{
"subtask": "Subtask 2",
"description": "This is the second subtask",
"prompt": "Write the code to do the second subtask"
}
]
},
{
"step": "Step 2",
"description": "This is the second step",
"subtasks": [
{
"subtask": "Subtask 1",
"description": "This is the first subtask",
"prompt": "Write the code to do the first subtask"
},
{
"subtask": "Subtask 2",
"description": "This is the second subtask",
"prompt": "Write the code to do the second subtask"
}
]
}
]
>>>>>>> elsa3new
Do not output any other text.
Input: {{$input}}
{{$wafContext}}

View File

@ -1,5 +1,9 @@
<<<<<<< HEAD
namespace skills;
=======
namespace Microsoft.SKDevTeam;
>>>>>>> elsa3new
public static class Developer {
public static SemanticFunctionConfig Implement = new SemanticFunctionConfig
{

View File

@ -1,4 +1,8 @@
<<<<<<< HEAD
namespace skills;
=======
namespace Microsoft.SKDevTeam;
>>>>>>> elsa3new
public static class PM
{
public static SemanticFunctionConfig BootstrapProject = new SemanticFunctionConfig

View File

@ -1,4 +1,8 @@
<<<<<<< HEAD
namespace skills;
=======
namespace Microsoft.SKDevTeam;
>>>>>>> elsa3new
public class SemanticFunctionConfig
{
@ -14,9 +18,17 @@ public class SemanticFunctionConfig
public static SemanticFunctionConfig ForSkillAndFunction(string skillName, string functionName) =>
(skillName, functionName) switch
{
<<<<<<< HEAD
(nameof(PM), nameof(PM.BootstrapProject)) => PM.BootstrapProject,
(nameof(PM), nameof(PM.Readme)) => PM.Readme,
(nameof(DevLead), nameof(DevLead.Plan)) => DevLead.Plan,
=======
(nameof(Chat), nameof(Chat.ChatCompletion)) => Chat.ChatCompletion,
(nameof(PM), nameof(PM.BootstrapProject)) => PM.BootstrapProject,
(nameof(PM), nameof(PM.Readme)) => PM.Readme,
(nameof(DevLead), nameof(DevLead.Plan)) => DevLead.Plan,
(nameof(CodeExplainer), nameof(CodeExplainer.Explain)) => CodeExplainer.Explain,
>>>>>>> elsa3new
(nameof(Developer), nameof(Developer.Implement)) => Developer.Implement,
(nameof(Developer), nameof(Developer.Improve)) => Developer.Improve,
_ => throw new ArgumentException($"Unable to find {skillName}.{functionName}")

View File

@ -4,7 +4,10 @@
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<<<<<<< HEAD
<RootNamespace>skills</RootNamespace>
=======
>>>>>>> elsa3new
</PropertyGroup>
<ItemGroup>