From 89fe9a9ce3bfaf3eecbc0ffe332037317a27ae80 Mon Sep 17 00:00:00 2001 From: Devis Lucato Date: Fri, 24 Feb 2023 15:38:24 -0800 Subject: [PATCH] First commit, let the journey begin! --- .editorconfig | 255 + .gitattributes | 5 + .github/ISSUE_TEMPLATE/bug_report.md | 32 + .github/ISSUE_TEMPLATE/feature_request.md | 20 + .github/dependabot.yml | 18 + .github/pull_request_template.md | 32 + .github/workflows/codeql-analysis.yml | 66 + .github/workflows/dotnet-ci.yml | 143 + .github/workflows/dotnet-format.yml | 44 + .../workflows/dotnet-integration-tests.yml | 54 + .github/workflows/dotnet-pr.yml | 97 + .../workflows/markdown-link-check-config.json | 19 + .github/workflows/markdown-link-check.yml | 25 + .github/workflows/update-version.sh | 51 + .gitignore | 443 + .vscode/extensions.json | 11 + .vscode/launch.json | 26 + .vscode/settings.json | 66 + .vscode/tasks.json | 161 + CODE_OF_CONDUCT.md | 9 + CONTRIBUTING.md | 97 + GLOSSARY.md | 32 + LICENSE | 21 + PROMPT_TEMPLATE_LANGUAGE.md | 121 + README.md | 113 + SECURITY.md | 41 + build.cmd | 7 + build.sh | 13 + compliance.yml | 79 + dotnet/Directory.Build.props | 18 + dotnet/Directory.Build.targets | 14 + dotnet/Directory.Packages.props | 33 + dotnet/DocFX/.gitignore | 1 + dotnet/DocFX/DocFX.csproj | 17 + dotnet/DocFX/README.md | 29 + dotnet/DocFX/api/.gitignore | 5 + dotnet/DocFX/api/index.md | 1 + dotnet/DocFX/articles/intro.md | 0 dotnet/DocFX/articles/toc.yml | 2 + dotnet/DocFX/docfx.json | 66 + dotnet/DocFX/index.md | 11 + dotnet/DocFX/toc.yml | 5 + dotnet/SK-dotnet.sln | 154 + dotnet/SK-dotnet.sln.DotSettings | 188 + dotnet/common.props | 23 + dotnet/nuget/NUGET.md | 17 + dotnet/nuget/icon.png | Bin 0 -> 15525 bytes dotnet/nuget/nuget-package.props | 43 + .../AI/OpenAICompletionTests.cs | 134 + .../IntegrationTest/IntegrationTests.csproj | 43 + dotnet/src/IntegrationTest/README.md | 60 + dotnet/src/IntegrationTest/RedirectOutput.cs | 24 + .../TestSettings/AzureOpenAIConfiguration.cs | 26 + .../TestSettings/OpenAIConfiguration.cs | 21 + .../IntegrationTest/WebSkill/WebSkillTests.cs | 90 + dotnet/src/IntegrationTest/XunitLogger.cs | 40 + dotnet/src/IntegrationTest/testsettings.json | 16 + .../Connectors.Bing/BingConnector.cs | 103 + .../Connectors.Bing/Connectors.Bing.csproj | 29 + .../Client/MsGraphClientLoggingHandler.cs | 79 + .../Client/MsGraphConfiguration.cs | 55 + .../Connectors.MsGraph.csproj | 30 + .../LocalUserMSALCredentialManager.cs | 114 + .../Connectors.MsGraph/Diagnostics/Ensure.cs | 41 + .../Exceptions/MsGraphConnectorException.cs | 28 + .../MicrosoftToDoConnector.cs | 157 + .../Connectors.MsGraph/OneDriveConnector.cs | 132 + .../OrganizationHierarchyConnector.cs | 57 + .../OutlookCalendarConnector.cs | 55 + .../OutlookMailConnector.cs | 54 + .../Connectors.OpenXml.csproj | 28 + .../Extensions/WordprocessingDocumentEx.cs | 75 + .../LocalFileSystemConnector.cs | 76 + .../WordDocumentConnector.cs | 68 + .../Connectors.Sqlite.Test.csproj | 31 + .../SqliteMemoryStoreTests.cs | 186 + .../Connectors.Sqlite.csproj | 36 + .../Connectors.Sqlite/Database.cs | 134 + .../Connectors.Sqlite/SqliteMemoryStore.cs | 155 + .../Document/DocumentSkillTests.cs | 141 + .../Productivity/CalendarSkillTests.cs | 266 + .../Productivity/CloudDriveSkillTests.cs | 113 + .../Productivity/EmailSkillTests.cs | 107 + .../OrganizationHierarchySkillTests.cs | 81 + .../Productivity/TaskListSkillTests.cs | 132 + .../SemanticKernel.Skills.Test.csproj | 28 + .../Skills.Test/Web/SearchUrlSkillTests.cs | 187 + .../Web/WebSearchEngineSkillTests.cs | 53 + .../Skills.Test/XunitLogger.cs | 40 + .../ICalendarConnector.cs | 21 + .../ICloudDriveConnector.cs | 39 + .../IDocumentConnector.cs | 31 + .../Connectors.Interfaces/IEmailConnector.cs | 28 + .../IFileSystemConnector.cs | 42 + .../IOrganizationHierarchyConnector.cs | 34 + .../ITaskManagementConnector.cs | 52 + .../IWebSearchEngineConnector.cs | 20 + .../Models/CalendarEvent.cs | 61 + .../Models/TaskManagementTask.cs | 51 + .../Models/TaskManagementTaskList.cs | 30 + .../Skills/SemanticKernel.Skills.csproj | 24 + .../ConversationSummarySkill.cs | 112 + .../SemanticFunctionDefinitions.cs | 92 + .../Skills/Skills/Diagnostics/Ensure.cs | 28 + .../Skills/Skills/Document/DocumentSkill.cs | 115 + .../Skills/Productivity/CalendarSkill.cs | 123 + .../Skills/Productivity/CloudDriveSkill.cs | 96 + .../Skills/Skills/Productivity/EmailSkill.cs | 83 + .../OrganizationHierarchySkill.cs | 38 + .../Skills/Productivity/TaskListSkill.cs | 92 + .../Skills/Skills/Web/SearchUrlSkill.cs | 166 + .../Skills/Skills/Web/WebSearchEngineSkill.cs | 34 + .../AI/Embeddings/EmbeddingTests.cs | 63 + .../Configuration/KernelConfigTests.cs | 225 + .../CoreSkills/FileIOSkillTests.cs | 104 + .../CoreSkills/HttpSkillTests.cs | 150 + .../CoreSkills/PlannerSkillTests.cs | 209 + .../CoreSkills/TextSkillTests.cs | 92 + .../CoreSkills/TimeSkillTests.cs | 28 + dotnet/src/SemanticKernel.Test/KernelTests.cs | 178 + .../Memory/Storage/DataEntryTests.cs | 239 + .../Memory/Storage/VolatileDataStoreTests.cs | 193 + .../Memory/VolatileMemoryStoreTests.cs | 251 + .../Orchestration/ContextVariablesTests.cs | 187 + .../Orchestration/SKContextTests.cs | 76 + .../Orchestration/SKFunctionTests1.cs | 262 + .../Orchestration/SKFunctionTests2.cs | 628 ++ .../PassThroughWithoutRetryTests.cs | 44 + .../SemanticTextPartitionerTests.cs | 426 + .../SemanticKernel.Test.csproj | 29 + .../SkillDefinition/FunctionsViewTests.cs | 115 + .../PromptTemplateEngineTests.cs | 337 + .../VectorOperations/VectorOperationTests.cs | 265 + .../VectorOperations/VectorSpanTests.cs | 226 + .../XunitHelpers/ConsoleLogger.cs | 31 + .../XunitHelpers/RedirectOutput.cs | 24 + dotnet/src/SemanticKernel/AI/AIException.cs | 99 + .../AI/CompleteRequestSettings.cs | 77 + .../AI/Embeddings/COSINE_SIMILARITY.md | 60 + .../AI/Embeddings/DOT_PRODUCT.md | 52 + .../AI/Embeddings/EUCLIDEAN_DISTANCE.md | 56 + .../SemanticKernel/AI/Embeddings/Embedding.cs | 186 + .../AI/Embeddings/EmbeddingReadOnlySpan.cs | 115 + .../AI/Embeddings/EmbeddingSpan.cs | 87 + .../AI/Embeddings/IEmbeddingGenerator.cs | 46 + .../AI/Embeddings/IEmbeddingIndex.cs | 53 + .../AI/Embeddings/IEmbeddingWithMetadata.cs | 16 + .../SemanticKernel/AI/Embeddings/README.md | 62 + .../AI/Embeddings/SupportedTypes.cs | 70 + .../CosineSimilarityOperation.cs | 145 + .../VectorOperations/DivideOperation.cs | 81 + .../VectorOperations/DotProductOperation.cs | 313 + .../EuclideanLengthOperation.cs | 48 + .../VectorOperations/MultiplyOperation.cs | 83 + .../VectorOperations/NormalizeOperation.cs | 38 + .../VectorOperations/SpanExtensions.cs | 21 + .../AI/ITextCompletionClient.cs | 19 + .../Clients/AzureOpenAIClientAbstract.cs | 166 + .../AI/OpenAI/Clients/OpenAIClientAbstract.cs | 248 + .../AI/OpenAI/HttpSchema/AzureDeployments.cs | 73 + .../AI/OpenAI/HttpSchema/CompletionRequest.cs | 117 + .../OpenAI/HttpSchema/CompletionResponse.cs | 36 + .../AI/OpenAI/HttpSchema/EmbeddingRequest.cs | 37 + .../AI/OpenAI/HttpSchema/EmbeddingResponse.cs | 36 + .../AI/OpenAI/Services/AzureOpenAIConfig.cs | 52 + .../AI/OpenAI/Services/AzureTextCompletion.cs | 83 + .../AI/OpenAI/Services/AzureTextEmbeddings.cs | 54 + .../AI/OpenAI/Services/OpenAIConfig.cs | 43 + .../OpenAI/Services/OpenAITextCompletion.cs | 79 + .../OpenAI/Services/OpenAITextEmbeddings.cs | 55 + .../Configuration/BackendConfig.cs | 26 + .../Configuration/BackendTypes.cs | 24 + .../Configuration/KernelConfig.cs | 418 + .../SemanticKernel/CoreSkills/FileIOSkill.cs | 53 + .../SemanticKernel/CoreSkills/HttpSkill.cs | 124 + .../SemanticKernel/CoreSkills/PlannerSkill.cs | 278 + .../CoreSkills/SemanticFunctionConstants.cs | 376 + .../CoreSkills/TextMemorySkill.cs | 79 + .../SemanticKernel/CoreSkills/TextSkill.cs | 99 + .../SemanticKernel/CoreSkills/TimeSkill.cs | 254 + .../SemanticKernel/Diagnostics/Exception.cs | 41 + .../Diagnostics/ExceptionExtensions.cs | 27 + .../Diagnostics/ValidationException.cs | 73 + .../src/SemanticKernel/Diagnostics/Verify.cs | 109 + dotnet/src/SemanticKernel/IKernel.cs | 159 + dotnet/src/SemanticKernel/Kernel.cs | 324 + dotnet/src/SemanticKernel/KernelBuilder.cs | 134 + dotnet/src/SemanticKernel/KernelException.cs | 89 + .../ImportSemanticSkillFromDirectory.cs | 102 + .../InlineFunctionsDefinitionExtension.cs | 104 + .../KernelExtensions/MemoryConfiguration.cs | 88 + .../src/SemanticKernel/Memory/IMemoryStore.cs | 15 + .../Memory/ISemanticTextMemory.cs | 79 + .../Memory/MemoryQueryResult.cs | 81 + .../src/SemanticKernel/Memory/MemoryRecord.cs | 99 + .../src/SemanticKernel/Memory/NullMemory.cs | 70 + .../Memory/SemanticTextMemory.cs | 107 + .../Memory/Storage/DataEntry.cs | 300 + .../Memory/Storage/IDataStore.cs | 106 + .../Memory/Storage/VolatileDataStore.cs | 101 + .../Memory/VolatileMemoryStore.cs | 132 + .../Orchestration/ContextVariables.cs | 166 + .../Extensions/ContextVariablesExtensions.cs | 104 + .../Extensions/SKContextExtensions.cs | 53 + .../Orchestration/ISKFunction.cs | 102 + .../SemanticKernel/Orchestration/SKContext.cs | 143 + .../Orchestration/SKFunction.cs | 712 ++ .../Orchestration/SKFunctionExtensions.cs | 120 + .../Planning/FunctionFlowRunner.cs | 282 + dotnet/src/SemanticKernel/Planning/Plan.cs | 109 + .../src/SemanticKernel/Planning/PlanRunner.cs | 183 + .../Planning/PlanningException.cs | 59 + .../Planning/SKContextExtensions.cs | 48 + .../Reliability/IRetryMechanism.cs | 21 + .../Reliability/PassThroughWithoutRetry.cs | 27 + .../SemanticFunctions/IPromptTemplate.cs | 27 + .../Partitioning/FunctionExtensions.cs | 38 + .../Partitioning/SemanticTextPartitioner.cs | 293 + .../SemanticFunctions/PromptTemplate.cs | 96 + .../SemanticFunctions/PromptTemplateConfig.cs | 123 + .../SemanticFunctionConfig.cs | 30 + .../src/SemanticKernel/SemanticKernel.csproj | 44 + .../SkillDefinition/FunctionView.cs | 74 + .../SkillDefinition/FunctionsView.cs | 109 + .../IReadOnlySkillCollection.cs | 82 + .../SkillDefinition/ISkillCollection.cs | 32 + .../SkillDefinition/ParameterView.cs | 67 + .../ReadOnlySkillCollection.cs | 72 + .../SkillDefinition/SKFunctionAttribute.cs | 29 + .../SKFunctionContextParameterAttribute.cs | 59 + .../SKFunctionInputAttribute.cs | 56 + .../SKFunctionNameAttribute.cs | 28 + .../SkillDefinition/SkillCollection.cs | 169 + .../TemplateEngine/Blocks/Block.cs | 42 + .../TemplateEngine/Blocks/BlockTypes.cs | 11 + .../TemplateEngine/Blocks/CodeBlock.cs | 192 + .../TemplateEngine/Blocks/TextBlock.cs | 34 + .../TemplateEngine/Blocks/VarBlock.cs | 88 + .../TemplateEngine/IPromptTemplateEngine.cs | 65 + .../TemplateEngine/PromptTemplateEngine.cs | 258 + .../TemplateEngine/TemplateException.cs | 43 + dotnet/src/SemanticKernel/Text/Json.cs | 36 + .../SemanticKernel/Text/StringExtensions.cs | 15 + samples/apps/.eslingrc.js | 50 + samples/apps/auth-api-webapp-react/.env | 8 + samples/apps/auth-api-webapp-react/README.md | 45 + .../apps/auth-api-webapp-react/package.json | 49 + .../auth-api-webapp-react/public/favicon.ico | Bin 0 -> 17174 bytes .../auth-api-webapp-react/public/index.html | 37 + .../apps/auth-api-webapp-react/src/App.css | 45 + .../apps/auth-api-webapp-react/src/App.tsx | 216 + .../src/components/FunctionProbe.tsx | 60 + .../src/components/InteractWithGraph.tsx | 157 + .../src/components/InteractionButton.tsx | 40 + .../src/components/QuickTipGroup.tsx | 36 + .../src/components/QuickTips.tsx | 50 + .../src/components/ServiceConfig.tsx | 145 + .../src/components/TaskButton.tsx | 92 + .../src/components/YourInfo.tsx | 39 + .../src/hooks/SemanticKernel.ts | 65 + .../src/hooks/useSemanticKernel.ts | 9 + .../apps/auth-api-webapp-react/src/index.tsx | 54 + .../auth-api-webapp-react/src/model/Ask.ts | 12 + .../src/model/AskResult.ts | 7 + .../src/model/KeyConfig.ts | 14 + .../src/ms-symbollockup_signin_light.svg | 1 + .../src/react-app-env.d.ts | 1 + .../apps/auth-api-webapp-react/src/word.png | Bin 0 -> 822 bytes .../apps/auth-api-webapp-react/tsconfig.json | 26 + samples/apps/auth-api-webapp-react/yarn.lock | 9970 +++++++++++++++++ samples/apps/book-creator-webapp-react/.env | 6 + .../apps/book-creator-webapp-react/README.md | 46 + .../book-creator-webapp-react/package.json | 46 + .../public/favicon.ico | Bin 0 -> 17174 bytes .../public/index.html | 37 + .../book-creator-webapp-react/src/App.css | 45 + .../book-creator-webapp-react/src/App.tsx | 205 + .../src/components/CreateBook.tsx | 473 + .../src/components/CreateBookWithPlanner.tsx | 327 + .../src/components/FunctionProbe.tsx | 60 + .../src/components/QuickTipGroup.tsx | 36 + .../src/components/QuickTips.tsx | 50 + .../src/components/ServiceConfig.tsx | 145 + .../src/components/TaskButton.tsx | 80 + .../src/components/TopicCard.tsx | 29 + .../src/components/TopicSelection.tsx | 151 + .../src/hooks/SemanticKernel.ts | 75 + .../src/hooks/TaskRunner.ts | 49 + .../src/hooks/useSemanticKernel.ts | 9 + .../src/hooks/useTaskRunner.ts | 11 + .../book-creator-webapp-react/src/index.tsx | 40 + .../src/model/Ask.ts | 12 + .../src/model/AskResult.ts | 9 + .../src/model/KeyConfig.ts | 16 + .../src/model/Page.ts | 12 + .../src/react-app-env.d.ts | 1 + .../book-creator-webapp-react/tsconfig.json | 26 + .../apps/book-creator-webapp-react/yarn.lock | 9946 ++++++++++++++++ samples/apps/chat-summary-webapp-react/.env | 6 + .../apps/chat-summary-webapp-react/README.md | 57 + .../chat-summary-webapp-react/package.json | 46 + .../public/favicon.ico | Bin 0 -> 17174 bytes .../public/index.html | 37 + .../chat-summary-webapp-react/src/App.css | 45 + .../chat-summary-webapp-react/src/App.tsx | 181 + .../src/components/AISummary.tsx | 184 + .../src/components/FunctionProbe.tsx | 60 + .../src/components/QuickTipGroup.tsx | 36 + .../src/components/QuickTips.tsx | 50 + .../src/components/ServiceConfig.tsx | 145 + .../src/components/chat/ChatHistoryItem.tsx | 37 + .../src/components/chat/ChatInput.tsx | 44 + .../src/components/chat/ChatInteraction.tsx | 81 + .../src/components/chat/ChatThread.ts | 149 + .../src/hooks/SemanticKernel.ts | 65 + .../src/hooks/useSemanticKernel.ts | 9 + .../chat-summary-webapp-react/src/index.tsx | 40 + .../src/model/Ask.ts | 11 + .../src/model/AskResult.ts | 5 + .../src/model/KeyConfig.ts | 16 + .../src/react-app-env.d.ts | 1 + .../chat-summary-webapp-react/tsconfig.json | 26 + .../apps/chat-summary-webapp-react/yarn.lock | 9946 ++++++++++++++++ samples/apps/prettier.config.js | 8 + samples/dotnet/KernelBuilder/GlobalUsings.cs | 14 + .../dotnet/KernelBuilder/KernelBuilder.csproj | 21 + samples/dotnet/KernelBuilder/Program.cs | 132 + .../.vscode/extensions.json | 6 + .../api-azure-function/Config/ApiKeyConfig.cs | 36 + .../Config/CompletionService.cs | 10 + .../dotnet/api-azure-function/Extensions.cs | 165 + .../dotnet/api-azure-function/Model/Ask.cs | 25 + .../api-azure-function/Model/AskResult.cs | 13 + .../dotnet/api-azure-function/Model/Skill.cs | 23 + .../dotnet/api-azure-function/PingEndpoint.cs | 21 + samples/dotnet/api-azure-function/Program.cs | 36 + samples/dotnet/api-azure-function/README.md | 44 + .../SemanticKernelEndpoint.cs | 116 + .../SemanticKernelFactory.cs | 66 + .../SemanticKernelFunction.csproj | 43 + .../TokenAuthenticationProvider.cs | 25 + .../api-azure-function/Utils/RepoFiles.cs | 41 + .../Utils/YourAppException.cs | 12 + samples/dotnet/api-azure-function/host.json | 11 + .../api-azure-function/local.settings.json | 9 + .../AzureOpenAIConfiguration.cs | 23 + .../MsGraphSkillsExample.csproj | 47 + .../graph-api-skills/OpenAIConfiguration.cs | 19 + samples/dotnet/graph-api-skills/Program.cs | 208 + .../Properties/launchSettings.json | 10 + samples/dotnet/graph-api-skills/README.md | 37 + .../dotnet/graph-api-skills/appsettings.json | 35 + .../LoadPromptsFromCloud.csproj | 21 + .../SampleExtension.cs | 62 + .../Example01_NativeFunctions.cs | 21 + .../Example02_Pipeline.cs | 32 + .../Example03_Variables.cs | 33 + .../Example04_BingSkillAndConnector.cs | 38 + ...xample05_CombineLLMPromptsAndNativeCode.cs | 62 + .../Example06_InlineFunctionDefinition.cs | 54 + .../Example07_TemplateLanguage.cs | 42 + .../Example08_RetryMechanism.cs | 38 + .../Example09_FunctionTypes.cs | 253 + ...Example10_DescribeAllSkillsAndFunctions.cs | 199 + .../Example11_WebSearchQueries.cs | 32 + .../Example12_Planning.cs | 509 + .../Example13_ConversationSummarySkill.cs | 273 + .../Example14_Memory.cs | 137 + .../Example15_MemorySkill.cs | 105 + .../KernelSyntaxExamples.csproj | 34 + .../dotnet/kernel-syntax-examples/Program.cs | 58 + .../Reliability/RetryThreeTimesWithBackoff.cs | 37 + .../RepoUtils/ConsoleLogger.cs | 31 + .../kernel-syntax-examples/RepoUtils/Env.cs | 24 + .../RepoUtils/RepoFiles.cs | 41 + .../RepoUtils/YourAppException.cs | 12 + .../Skills/EmailSkill.cs | 29 + .../Skills/StaticTextSkill.cs | 27 + .../Skills/TextSkill.cs | 45 + samples/notebooks/dotnet/0-AI-settings.ipynb | 182 + .../dotnet/1-basic-loading-the-kernel.ipynb | 272 + .../dotnet/2-running-prompts-from-file.ipynb | 268 + .../dotnet/3-semantic-function-inline.ipynb | 489 + .../dotnet/4-context-variables-chat.ipynb | 461 + .../dotnet/5-using-the-planner.ipynb | 510 + .../dotnet/6-memory-and-embeddings.ipynb | 753 ++ .../dotnet/Getting-Started-Notebook.ipynb | 253 + samples/notebooks/dotnet/README.md | 101 + samples/notebooks/dotnet/config/.gitignore | 9 + samples/notebooks/dotnet/config/Settings.cs | 252 + .../dotnet/config/settings.json.azure-example | 6 + .../config/settings.json.openai-example | 6 + .../AssistantShowCalendarEvents/config.json | 18 + .../AssistantShowCalendarEvents/skprompt.txt | 15 + samples/skills/ChatSkill/Chat/config.json | 16 + samples/skills/ChatSkill/Chat/skprompt.txt | 7 + .../skills/ChatSkill/ChatFilter/config.json | 18 + .../skills/ChatSkill/ChatFilter/skprompt.txt | 65 + samples/skills/ChatSkill/ChatGPT/config.json | 15 + samples/skills/ChatSkill/ChatGPT/skprompt.txt | 25 + samples/skills/ChatSkill/ChatUser/config.json | 19 + .../skills/ChatSkill/ChatUser/skprompt.txt | 7 + samples/skills/ChatSkill/ChatV2/config.json | 18 + samples/skills/ChatSkill/ChatV2/skprompt.txt | 23 + .../ChildrensBookSkill/BookIdeas/config.json | 12 + .../ChildrensBookSkill/BookIdeas/skprompt.txt | 4 + .../ChildrensBookSkill/CreateBook/config.json | 12 + .../CreateBook/skprompt.txt | 4 + .../Importance/config.json | 15 + .../Importance/skprompt.txt | 28 + samples/skills/CodingSkill/Code/config.json | 15 + samples/skills/CodingSkill/Code/skprompt.txt | 2 + .../skills/CodingSkill/CodePython/config.json | 12 + .../CodingSkill/CodePython/skprompt.txt | 10 + .../CodingSkill/CommandLinePython/config.json | 18 + .../CommandLinePython/skprompt.txt | 22 + .../skills/CodingSkill/DOSScript/config.json | 20 + .../skills/CodingSkill/DOSScript/skprompt.txt | 19 + .../CodingSkill/EmailSearch/config.json | 18 + .../CodingSkill/EmailSearch/skprompt.txt | 32 + samples/skills/CodingSkill/Entity/config.json | 18 + .../skills/CodingSkill/Entity/skprompt.txt | 8 + samples/skills/FunSkill/Excuses/config.json | 12 + samples/skills/FunSkill/Excuses/skprompt.txt | 6 + samples/skills/FunSkill/Joke/config.json | 12 + samples/skills/FunSkill/Joke/skprompt.txt | 13 + samples/skills/FunSkill/Limerick/config.json | 15 + samples/skills/FunSkill/Limerick/skprompt.txt | 27 + .../AssistantIntent/config.json | 15 + .../AssistantIntent/skprompt.txt | 35 + samples/skills/MiscSkill/Continue/config.json | 15 + .../skills/MiscSkill/Continue/skprompt.txt | 1 + .../OpenApiJsonToHttpClient/config.json | 9 + .../OpenApiJsonToHttpClient/skprompt.txt | 25 + .../QASkill/AssistantResults/config.json | 15 + .../QASkill/AssistantResults/skprompt.txt | 11 + .../skills/QASkill/ContextQuery/config.json | 18 + .../skills/QASkill/ContextQuery/skprompt.txt | 48 + samples/skills/QASkill/Form/config.json | 18 + samples/skills/QASkill/Form/skprompt.txt | 20 + samples/skills/QASkill/QNA/config.json | 15 + samples/skills/QASkill/QNA/skprompt.txt | 27 + samples/skills/QASkill/Question/config.json | 12 + samples/skills/QASkill/Question/skprompt.txt | 27 + .../MakeAbstractReadable/config.json | 15 + .../MakeAbstractReadable/skprompt.txt | 5 + .../skills/SummarizeSkill/Notegen/config.json | 15 + .../SummarizeSkill/Notegen/skprompt.txt | 21 + .../SummarizeSkill/Summarize/config.json | 21 + .../SummarizeSkill/Summarize/skprompt.txt | 23 + .../skills/SummarizeSkill/Topics/config.json | 12 + .../skills/SummarizeSkill/Topics/skprompt.txt | 28 + .../skills/WriterSkill/Acronym/config.json | 15 + .../skills/WriterSkill/Acronym/skprompt.txt | 25 + .../WriterSkill/AcronymGenerator/config.json | 18 + .../WriterSkill/AcronymGenerator/skprompt.txt | 54 + .../WriterSkill/AcronymReverse/config.json | 18 + .../WriterSkill/AcronymReverse/skprompt.txt | 24 + .../skills/WriterSkill/Brainstorm/config.json | 23 + .../WriterSkill/Brainstorm/skprompt.txt | 8 + .../skills/WriterSkill/EmailGen/config.json | 15 + .../skills/WriterSkill/EmailGen/skprompt.txt | 16 + .../skills/WriterSkill/EmailTo/config.json | 15 + .../skills/WriterSkill/EmailTo/skprompt.txt | 31 + .../WriterSkill/NovelChapter/config.json | 37 + .../WriterSkill/NovelChapter/skprompt.txt | 20 + .../NovelChapterWithNotes/config.json | 42 + .../NovelChapterWithNotes/skprompt.txt | 19 + .../WriterSkill/NovelOutline/config.json | 32 + .../WriterSkill/NovelOutline/skprompt.txt | 12 + .../skills/WriterSkill/Rewrite/config.json | 12 + .../skills/WriterSkill/Rewrite/skprompt.txt | 6 + .../skills/WriterSkill/ShortPoem/config.json | 15 + .../skills/WriterSkill/ShortPoem/skprompt.txt | 2 + .../skills/WriterSkill/StoryGen/config.json | 12 + .../skills/WriterSkill/StoryGen/skprompt.txt | 10 + .../skills/WriterSkill/TellMeMore/config.json | 12 + .../WriterSkill/TellMeMore/skprompt.txt | 7 + .../skills/WriterSkill/Translate/config.json | 18 + .../skills/WriterSkill/Translate/skprompt.txt | 6 + .../WriterSkill/TranslateV2/config.json | 15 + .../WriterSkill/TranslateV2/skprompt.txt | 11 + .../TwoSentenceSummary/config.json | 12 + .../TwoSentenceSummary/skprompt.txt | 4 + 484 files changed, 66196 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitattributes create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/dependabot.yml create mode 100644 .github/pull_request_template.md create mode 100644 .github/workflows/codeql-analysis.yml create mode 100644 .github/workflows/dotnet-ci.yml create mode 100644 .github/workflows/dotnet-format.yml create mode 100644 .github/workflows/dotnet-integration-tests.yml create mode 100644 .github/workflows/dotnet-pr.yml create mode 100644 .github/workflows/markdown-link-check-config.json create mode 100644 .github/workflows/markdown-link-check.yml create mode 100755 .github/workflows/update-version.sh create mode 100644 .gitignore create mode 100644 .vscode/extensions.json create mode 100644 .vscode/launch.json create mode 100644 .vscode/settings.json create mode 100644 .vscode/tasks.json create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 GLOSSARY.md create mode 100644 LICENSE create mode 100644 PROMPT_TEMPLATE_LANGUAGE.md create mode 100644 README.md create mode 100644 SECURITY.md create mode 100644 build.cmd create mode 100755 build.sh create mode 100644 compliance.yml create mode 100644 dotnet/Directory.Build.props create mode 100644 dotnet/Directory.Build.targets create mode 100644 dotnet/Directory.Packages.props create mode 100644 dotnet/DocFX/.gitignore create mode 100644 dotnet/DocFX/DocFX.csproj create mode 100644 dotnet/DocFX/README.md create mode 100644 dotnet/DocFX/api/.gitignore create mode 100644 dotnet/DocFX/api/index.md create mode 100644 dotnet/DocFX/articles/intro.md create mode 100644 dotnet/DocFX/articles/toc.yml create mode 100644 dotnet/DocFX/docfx.json create mode 100644 dotnet/DocFX/index.md create mode 100644 dotnet/DocFX/toc.yml create mode 100644 dotnet/SK-dotnet.sln create mode 100644 dotnet/SK-dotnet.sln.DotSettings create mode 100644 dotnet/common.props create mode 100644 dotnet/nuget/NUGET.md create mode 100644 dotnet/nuget/icon.png create mode 100644 dotnet/nuget/nuget-package.props create mode 100644 dotnet/src/IntegrationTest/AI/OpenAICompletionTests.cs create mode 100644 dotnet/src/IntegrationTest/IntegrationTests.csproj create mode 100644 dotnet/src/IntegrationTest/README.md create mode 100644 dotnet/src/IntegrationTest/RedirectOutput.cs create mode 100644 dotnet/src/IntegrationTest/TestSettings/AzureOpenAIConfiguration.cs create mode 100644 dotnet/src/IntegrationTest/TestSettings/OpenAIConfiguration.cs create mode 100644 dotnet/src/IntegrationTest/WebSkill/WebSkillTests.cs create mode 100644 dotnet/src/IntegrationTest/XunitLogger.cs create mode 100644 dotnet/src/IntegrationTest/testsettings.json create mode 100644 dotnet/src/SemanticKernel.Connectors/Connectors.Bing/BingConnector.cs create mode 100644 dotnet/src/SemanticKernel.Connectors/Connectors.Bing/Connectors.Bing.csproj create mode 100644 dotnet/src/SemanticKernel.Connectors/Connectors.MsGraph/Client/MsGraphClientLoggingHandler.cs create mode 100644 dotnet/src/SemanticKernel.Connectors/Connectors.MsGraph/Client/MsGraphConfiguration.cs create mode 100644 dotnet/src/SemanticKernel.Connectors/Connectors.MsGraph/Connectors.MsGraph.csproj create mode 100644 dotnet/src/SemanticKernel.Connectors/Connectors.MsGraph/CredentialManagers/LocalUserMSALCredentialManager.cs create mode 100644 dotnet/src/SemanticKernel.Connectors/Connectors.MsGraph/Diagnostics/Ensure.cs create mode 100644 dotnet/src/SemanticKernel.Connectors/Connectors.MsGraph/Exceptions/MsGraphConnectorException.cs create mode 100644 dotnet/src/SemanticKernel.Connectors/Connectors.MsGraph/MicrosoftToDoConnector.cs create mode 100644 dotnet/src/SemanticKernel.Connectors/Connectors.MsGraph/OneDriveConnector.cs create mode 100644 dotnet/src/SemanticKernel.Connectors/Connectors.MsGraph/OrganizationHierarchyConnector.cs create mode 100644 dotnet/src/SemanticKernel.Connectors/Connectors.MsGraph/OutlookCalendarConnector.cs create mode 100644 dotnet/src/SemanticKernel.Connectors/Connectors.MsGraph/OutlookMailConnector.cs create mode 100644 dotnet/src/SemanticKernel.Connectors/Connectors.OpenXml/Connectors.OpenXml.csproj create mode 100644 dotnet/src/SemanticKernel.Connectors/Connectors.OpenXml/Extensions/WordprocessingDocumentEx.cs create mode 100644 dotnet/src/SemanticKernel.Connectors/Connectors.OpenXml/LocalFileSystemConnector.cs create mode 100644 dotnet/src/SemanticKernel.Connectors/Connectors.OpenXml/WordDocumentConnector.cs create mode 100644 dotnet/src/SemanticKernel.Connectors/Connectors.Sqlite.Test/Connectors.Sqlite.Test.csproj create mode 100644 dotnet/src/SemanticKernel.Connectors/Connectors.Sqlite.Test/SqliteMemoryStoreTests.cs create mode 100644 dotnet/src/SemanticKernel.Connectors/Connectors.Sqlite/Connectors.Sqlite.csproj create mode 100644 dotnet/src/SemanticKernel.Connectors/Connectors.Sqlite/Database.cs create mode 100644 dotnet/src/SemanticKernel.Connectors/Connectors.Sqlite/SqliteMemoryStore.cs create mode 100644 dotnet/src/SemanticKernel.Skills/Skills.Test/Document/DocumentSkillTests.cs create mode 100644 dotnet/src/SemanticKernel.Skills/Skills.Test/Productivity/CalendarSkillTests.cs create mode 100644 dotnet/src/SemanticKernel.Skills/Skills.Test/Productivity/CloudDriveSkillTests.cs create mode 100644 dotnet/src/SemanticKernel.Skills/Skills.Test/Productivity/EmailSkillTests.cs create mode 100644 dotnet/src/SemanticKernel.Skills/Skills.Test/Productivity/OrganizationHierarchySkillTests.cs create mode 100644 dotnet/src/SemanticKernel.Skills/Skills.Test/Productivity/TaskListSkillTests.cs create mode 100644 dotnet/src/SemanticKernel.Skills/Skills.Test/SemanticKernel.Skills.Test.csproj create mode 100644 dotnet/src/SemanticKernel.Skills/Skills.Test/Web/SearchUrlSkillTests.cs create mode 100644 dotnet/src/SemanticKernel.Skills/Skills.Test/Web/WebSearchEngineSkillTests.cs create mode 100644 dotnet/src/SemanticKernel.Skills/Skills.Test/XunitLogger.cs create mode 100644 dotnet/src/SemanticKernel.Skills/Skills/Connectors.Interfaces/ICalendarConnector.cs create mode 100644 dotnet/src/SemanticKernel.Skills/Skills/Connectors.Interfaces/ICloudDriveConnector.cs create mode 100644 dotnet/src/SemanticKernel.Skills/Skills/Connectors.Interfaces/IDocumentConnector.cs create mode 100644 dotnet/src/SemanticKernel.Skills/Skills/Connectors.Interfaces/IEmailConnector.cs create mode 100644 dotnet/src/SemanticKernel.Skills/Skills/Connectors.Interfaces/IFileSystemConnector.cs create mode 100644 dotnet/src/SemanticKernel.Skills/Skills/Connectors.Interfaces/IOrganizationHierarchyConnector.cs create mode 100644 dotnet/src/SemanticKernel.Skills/Skills/Connectors.Interfaces/ITaskManagementConnector.cs create mode 100644 dotnet/src/SemanticKernel.Skills/Skills/Connectors.Interfaces/IWebSearchEngineConnector.cs create mode 100644 dotnet/src/SemanticKernel.Skills/Skills/Connectors.Interfaces/Models/CalendarEvent.cs create mode 100644 dotnet/src/SemanticKernel.Skills/Skills/Connectors.Interfaces/Models/TaskManagementTask.cs create mode 100644 dotnet/src/SemanticKernel.Skills/Skills/Connectors.Interfaces/Models/TaskManagementTaskList.cs create mode 100644 dotnet/src/SemanticKernel.Skills/Skills/SemanticKernel.Skills.csproj create mode 100644 dotnet/src/SemanticKernel.Skills/Skills/Skills/ConversationSummary/ConversationSummarySkill.cs create mode 100644 dotnet/src/SemanticKernel.Skills/Skills/Skills/ConversationSummary/SemanticFunctionDefinitions.cs create mode 100644 dotnet/src/SemanticKernel.Skills/Skills/Skills/Diagnostics/Ensure.cs create mode 100644 dotnet/src/SemanticKernel.Skills/Skills/Skills/Document/DocumentSkill.cs create mode 100644 dotnet/src/SemanticKernel.Skills/Skills/Skills/Productivity/CalendarSkill.cs create mode 100644 dotnet/src/SemanticKernel.Skills/Skills/Skills/Productivity/CloudDriveSkill.cs create mode 100644 dotnet/src/SemanticKernel.Skills/Skills/Skills/Productivity/EmailSkill.cs create mode 100644 dotnet/src/SemanticKernel.Skills/Skills/Skills/Productivity/OrganizationHierarchySkill.cs create mode 100644 dotnet/src/SemanticKernel.Skills/Skills/Skills/Productivity/TaskListSkill.cs create mode 100644 dotnet/src/SemanticKernel.Skills/Skills/Skills/Web/SearchUrlSkill.cs create mode 100644 dotnet/src/SemanticKernel.Skills/Skills/Skills/Web/WebSearchEngineSkill.cs create mode 100644 dotnet/src/SemanticKernel.Test/AI/Embeddings/EmbeddingTests.cs create mode 100644 dotnet/src/SemanticKernel.Test/Configuration/KernelConfigTests.cs create mode 100644 dotnet/src/SemanticKernel.Test/CoreSkills/FileIOSkillTests.cs create mode 100644 dotnet/src/SemanticKernel.Test/CoreSkills/HttpSkillTests.cs create mode 100644 dotnet/src/SemanticKernel.Test/CoreSkills/PlannerSkillTests.cs create mode 100644 dotnet/src/SemanticKernel.Test/CoreSkills/TextSkillTests.cs create mode 100644 dotnet/src/SemanticKernel.Test/CoreSkills/TimeSkillTests.cs create mode 100644 dotnet/src/SemanticKernel.Test/KernelTests.cs create mode 100644 dotnet/src/SemanticKernel.Test/Memory/Storage/DataEntryTests.cs create mode 100644 dotnet/src/SemanticKernel.Test/Memory/Storage/VolatileDataStoreTests.cs create mode 100644 dotnet/src/SemanticKernel.Test/Memory/VolatileMemoryStoreTests.cs create mode 100644 dotnet/src/SemanticKernel.Test/Orchestration/ContextVariablesTests.cs create mode 100644 dotnet/src/SemanticKernel.Test/Orchestration/SKContextTests.cs create mode 100644 dotnet/src/SemanticKernel.Test/Orchestration/SKFunctionTests1.cs create mode 100644 dotnet/src/SemanticKernel.Test/Orchestration/SKFunctionTests2.cs create mode 100644 dotnet/src/SemanticKernel.Test/Reliability/PassThroughWithoutRetryTests.cs create mode 100644 dotnet/src/SemanticKernel.Test/SemanticFunctions/Partitioning/SemanticTextPartitionerTests.cs create mode 100644 dotnet/src/SemanticKernel.Test/SemanticKernel.Test.csproj create mode 100644 dotnet/src/SemanticKernel.Test/SkillDefinition/FunctionsViewTests.cs create mode 100644 dotnet/src/SemanticKernel.Test/TemplateEngine/PromptTemplateEngineTests.cs create mode 100644 dotnet/src/SemanticKernel.Test/VectorOperations/VectorOperationTests.cs create mode 100644 dotnet/src/SemanticKernel.Test/VectorOperations/VectorSpanTests.cs create mode 100644 dotnet/src/SemanticKernel.Test/XunitHelpers/ConsoleLogger.cs create mode 100644 dotnet/src/SemanticKernel.Test/XunitHelpers/RedirectOutput.cs create mode 100644 dotnet/src/SemanticKernel/AI/AIException.cs create mode 100644 dotnet/src/SemanticKernel/AI/CompleteRequestSettings.cs create mode 100644 dotnet/src/SemanticKernel/AI/Embeddings/COSINE_SIMILARITY.md create mode 100644 dotnet/src/SemanticKernel/AI/Embeddings/DOT_PRODUCT.md create mode 100644 dotnet/src/SemanticKernel/AI/Embeddings/EUCLIDEAN_DISTANCE.md create mode 100644 dotnet/src/SemanticKernel/AI/Embeddings/Embedding.cs create mode 100644 dotnet/src/SemanticKernel/AI/Embeddings/EmbeddingReadOnlySpan.cs create mode 100644 dotnet/src/SemanticKernel/AI/Embeddings/EmbeddingSpan.cs create mode 100644 dotnet/src/SemanticKernel/AI/Embeddings/IEmbeddingGenerator.cs create mode 100644 dotnet/src/SemanticKernel/AI/Embeddings/IEmbeddingIndex.cs create mode 100644 dotnet/src/SemanticKernel/AI/Embeddings/IEmbeddingWithMetadata.cs create mode 100644 dotnet/src/SemanticKernel/AI/Embeddings/README.md create mode 100644 dotnet/src/SemanticKernel/AI/Embeddings/SupportedTypes.cs create mode 100644 dotnet/src/SemanticKernel/AI/Embeddings/VectorOperations/CosineSimilarityOperation.cs create mode 100644 dotnet/src/SemanticKernel/AI/Embeddings/VectorOperations/DivideOperation.cs create mode 100644 dotnet/src/SemanticKernel/AI/Embeddings/VectorOperations/DotProductOperation.cs create mode 100644 dotnet/src/SemanticKernel/AI/Embeddings/VectorOperations/EuclideanLengthOperation.cs create mode 100644 dotnet/src/SemanticKernel/AI/Embeddings/VectorOperations/MultiplyOperation.cs create mode 100644 dotnet/src/SemanticKernel/AI/Embeddings/VectorOperations/NormalizeOperation.cs create mode 100644 dotnet/src/SemanticKernel/AI/Embeddings/VectorOperations/SpanExtensions.cs create mode 100644 dotnet/src/SemanticKernel/AI/ITextCompletionClient.cs create mode 100644 dotnet/src/SemanticKernel/AI/OpenAI/Clients/AzureOpenAIClientAbstract.cs create mode 100644 dotnet/src/SemanticKernel/AI/OpenAI/Clients/OpenAIClientAbstract.cs create mode 100644 dotnet/src/SemanticKernel/AI/OpenAI/HttpSchema/AzureDeployments.cs create mode 100644 dotnet/src/SemanticKernel/AI/OpenAI/HttpSchema/CompletionRequest.cs create mode 100644 dotnet/src/SemanticKernel/AI/OpenAI/HttpSchema/CompletionResponse.cs create mode 100644 dotnet/src/SemanticKernel/AI/OpenAI/HttpSchema/EmbeddingRequest.cs create mode 100644 dotnet/src/SemanticKernel/AI/OpenAI/HttpSchema/EmbeddingResponse.cs create mode 100644 dotnet/src/SemanticKernel/AI/OpenAI/Services/AzureOpenAIConfig.cs create mode 100644 dotnet/src/SemanticKernel/AI/OpenAI/Services/AzureTextCompletion.cs create mode 100644 dotnet/src/SemanticKernel/AI/OpenAI/Services/AzureTextEmbeddings.cs create mode 100644 dotnet/src/SemanticKernel/AI/OpenAI/Services/OpenAIConfig.cs create mode 100644 dotnet/src/SemanticKernel/AI/OpenAI/Services/OpenAITextCompletion.cs create mode 100644 dotnet/src/SemanticKernel/AI/OpenAI/Services/OpenAITextEmbeddings.cs create mode 100644 dotnet/src/SemanticKernel/Configuration/BackendConfig.cs create mode 100644 dotnet/src/SemanticKernel/Configuration/BackendTypes.cs create mode 100644 dotnet/src/SemanticKernel/Configuration/KernelConfig.cs create mode 100644 dotnet/src/SemanticKernel/CoreSkills/FileIOSkill.cs create mode 100644 dotnet/src/SemanticKernel/CoreSkills/HttpSkill.cs create mode 100644 dotnet/src/SemanticKernel/CoreSkills/PlannerSkill.cs create mode 100644 dotnet/src/SemanticKernel/CoreSkills/SemanticFunctionConstants.cs create mode 100644 dotnet/src/SemanticKernel/CoreSkills/TextMemorySkill.cs create mode 100644 dotnet/src/SemanticKernel/CoreSkills/TextSkill.cs create mode 100644 dotnet/src/SemanticKernel/CoreSkills/TimeSkill.cs create mode 100644 dotnet/src/SemanticKernel/Diagnostics/Exception.cs create mode 100644 dotnet/src/SemanticKernel/Diagnostics/ExceptionExtensions.cs create mode 100644 dotnet/src/SemanticKernel/Diagnostics/ValidationException.cs create mode 100644 dotnet/src/SemanticKernel/Diagnostics/Verify.cs create mode 100644 dotnet/src/SemanticKernel/IKernel.cs create mode 100644 dotnet/src/SemanticKernel/Kernel.cs create mode 100644 dotnet/src/SemanticKernel/KernelBuilder.cs create mode 100644 dotnet/src/SemanticKernel/KernelException.cs create mode 100644 dotnet/src/SemanticKernel/KernelExtensions/ImportSemanticSkillFromDirectory.cs create mode 100644 dotnet/src/SemanticKernel/KernelExtensions/InlineFunctionsDefinitionExtension.cs create mode 100644 dotnet/src/SemanticKernel/KernelExtensions/MemoryConfiguration.cs create mode 100644 dotnet/src/SemanticKernel/Memory/IMemoryStore.cs create mode 100644 dotnet/src/SemanticKernel/Memory/ISemanticTextMemory.cs create mode 100644 dotnet/src/SemanticKernel/Memory/MemoryQueryResult.cs create mode 100644 dotnet/src/SemanticKernel/Memory/MemoryRecord.cs create mode 100644 dotnet/src/SemanticKernel/Memory/NullMemory.cs create mode 100644 dotnet/src/SemanticKernel/Memory/SemanticTextMemory.cs create mode 100644 dotnet/src/SemanticKernel/Memory/Storage/DataEntry.cs create mode 100644 dotnet/src/SemanticKernel/Memory/Storage/IDataStore.cs create mode 100644 dotnet/src/SemanticKernel/Memory/Storage/VolatileDataStore.cs create mode 100644 dotnet/src/SemanticKernel/Memory/VolatileMemoryStore.cs create mode 100644 dotnet/src/SemanticKernel/Orchestration/ContextVariables.cs create mode 100644 dotnet/src/SemanticKernel/Orchestration/Extensions/ContextVariablesExtensions.cs create mode 100644 dotnet/src/SemanticKernel/Orchestration/Extensions/SKContextExtensions.cs create mode 100644 dotnet/src/SemanticKernel/Orchestration/ISKFunction.cs create mode 100644 dotnet/src/SemanticKernel/Orchestration/SKContext.cs create mode 100644 dotnet/src/SemanticKernel/Orchestration/SKFunction.cs create mode 100644 dotnet/src/SemanticKernel/Orchestration/SKFunctionExtensions.cs create mode 100644 dotnet/src/SemanticKernel/Planning/FunctionFlowRunner.cs create mode 100644 dotnet/src/SemanticKernel/Planning/Plan.cs create mode 100644 dotnet/src/SemanticKernel/Planning/PlanRunner.cs create mode 100644 dotnet/src/SemanticKernel/Planning/PlanningException.cs create mode 100644 dotnet/src/SemanticKernel/Planning/SKContextExtensions.cs create mode 100644 dotnet/src/SemanticKernel/Reliability/IRetryMechanism.cs create mode 100644 dotnet/src/SemanticKernel/Reliability/PassThroughWithoutRetry.cs create mode 100644 dotnet/src/SemanticKernel/SemanticFunctions/IPromptTemplate.cs create mode 100644 dotnet/src/SemanticKernel/SemanticFunctions/Partitioning/FunctionExtensions.cs create mode 100644 dotnet/src/SemanticKernel/SemanticFunctions/Partitioning/SemanticTextPartitioner.cs create mode 100644 dotnet/src/SemanticKernel/SemanticFunctions/PromptTemplate.cs create mode 100644 dotnet/src/SemanticKernel/SemanticFunctions/PromptTemplateConfig.cs create mode 100644 dotnet/src/SemanticKernel/SemanticFunctions/SemanticFunctionConfig.cs create mode 100644 dotnet/src/SemanticKernel/SemanticKernel.csproj create mode 100644 dotnet/src/SemanticKernel/SkillDefinition/FunctionView.cs create mode 100644 dotnet/src/SemanticKernel/SkillDefinition/FunctionsView.cs create mode 100644 dotnet/src/SemanticKernel/SkillDefinition/IReadOnlySkillCollection.cs create mode 100644 dotnet/src/SemanticKernel/SkillDefinition/ISkillCollection.cs create mode 100644 dotnet/src/SemanticKernel/SkillDefinition/ParameterView.cs create mode 100644 dotnet/src/SemanticKernel/SkillDefinition/ReadOnlySkillCollection.cs create mode 100644 dotnet/src/SemanticKernel/SkillDefinition/SKFunctionAttribute.cs create mode 100644 dotnet/src/SemanticKernel/SkillDefinition/SKFunctionContextParameterAttribute.cs create mode 100644 dotnet/src/SemanticKernel/SkillDefinition/SKFunctionInputAttribute.cs create mode 100644 dotnet/src/SemanticKernel/SkillDefinition/SKFunctionNameAttribute.cs create mode 100644 dotnet/src/SemanticKernel/SkillDefinition/SkillCollection.cs create mode 100644 dotnet/src/SemanticKernel/TemplateEngine/Blocks/Block.cs create mode 100644 dotnet/src/SemanticKernel/TemplateEngine/Blocks/BlockTypes.cs create mode 100644 dotnet/src/SemanticKernel/TemplateEngine/Blocks/CodeBlock.cs create mode 100644 dotnet/src/SemanticKernel/TemplateEngine/Blocks/TextBlock.cs create mode 100644 dotnet/src/SemanticKernel/TemplateEngine/Blocks/VarBlock.cs create mode 100644 dotnet/src/SemanticKernel/TemplateEngine/IPromptTemplateEngine.cs create mode 100644 dotnet/src/SemanticKernel/TemplateEngine/PromptTemplateEngine.cs create mode 100644 dotnet/src/SemanticKernel/TemplateEngine/TemplateException.cs create mode 100644 dotnet/src/SemanticKernel/Text/Json.cs create mode 100644 dotnet/src/SemanticKernel/Text/StringExtensions.cs create mode 100644 samples/apps/.eslingrc.js create mode 100644 samples/apps/auth-api-webapp-react/.env create mode 100644 samples/apps/auth-api-webapp-react/README.md create mode 100644 samples/apps/auth-api-webapp-react/package.json create mode 100644 samples/apps/auth-api-webapp-react/public/favicon.ico create mode 100644 samples/apps/auth-api-webapp-react/public/index.html create mode 100644 samples/apps/auth-api-webapp-react/src/App.css create mode 100644 samples/apps/auth-api-webapp-react/src/App.tsx create mode 100644 samples/apps/auth-api-webapp-react/src/components/FunctionProbe.tsx create mode 100644 samples/apps/auth-api-webapp-react/src/components/InteractWithGraph.tsx create mode 100644 samples/apps/auth-api-webapp-react/src/components/InteractionButton.tsx create mode 100644 samples/apps/auth-api-webapp-react/src/components/QuickTipGroup.tsx create mode 100644 samples/apps/auth-api-webapp-react/src/components/QuickTips.tsx create mode 100644 samples/apps/auth-api-webapp-react/src/components/ServiceConfig.tsx create mode 100644 samples/apps/auth-api-webapp-react/src/components/TaskButton.tsx create mode 100644 samples/apps/auth-api-webapp-react/src/components/YourInfo.tsx create mode 100644 samples/apps/auth-api-webapp-react/src/hooks/SemanticKernel.ts create mode 100644 samples/apps/auth-api-webapp-react/src/hooks/useSemanticKernel.ts create mode 100644 samples/apps/auth-api-webapp-react/src/index.tsx create mode 100644 samples/apps/auth-api-webapp-react/src/model/Ask.ts create mode 100644 samples/apps/auth-api-webapp-react/src/model/AskResult.ts create mode 100644 samples/apps/auth-api-webapp-react/src/model/KeyConfig.ts create mode 100644 samples/apps/auth-api-webapp-react/src/ms-symbollockup_signin_light.svg create mode 100644 samples/apps/auth-api-webapp-react/src/react-app-env.d.ts create mode 100644 samples/apps/auth-api-webapp-react/src/word.png create mode 100644 samples/apps/auth-api-webapp-react/tsconfig.json create mode 100644 samples/apps/auth-api-webapp-react/yarn.lock create mode 100644 samples/apps/book-creator-webapp-react/.env create mode 100644 samples/apps/book-creator-webapp-react/README.md create mode 100644 samples/apps/book-creator-webapp-react/package.json create mode 100644 samples/apps/book-creator-webapp-react/public/favicon.ico create mode 100644 samples/apps/book-creator-webapp-react/public/index.html create mode 100644 samples/apps/book-creator-webapp-react/src/App.css create mode 100644 samples/apps/book-creator-webapp-react/src/App.tsx create mode 100644 samples/apps/book-creator-webapp-react/src/components/CreateBook.tsx create mode 100644 samples/apps/book-creator-webapp-react/src/components/CreateBookWithPlanner.tsx create mode 100644 samples/apps/book-creator-webapp-react/src/components/FunctionProbe.tsx create mode 100644 samples/apps/book-creator-webapp-react/src/components/QuickTipGroup.tsx create mode 100644 samples/apps/book-creator-webapp-react/src/components/QuickTips.tsx create mode 100644 samples/apps/book-creator-webapp-react/src/components/ServiceConfig.tsx create mode 100644 samples/apps/book-creator-webapp-react/src/components/TaskButton.tsx create mode 100644 samples/apps/book-creator-webapp-react/src/components/TopicCard.tsx create mode 100644 samples/apps/book-creator-webapp-react/src/components/TopicSelection.tsx create mode 100644 samples/apps/book-creator-webapp-react/src/hooks/SemanticKernel.ts create mode 100644 samples/apps/book-creator-webapp-react/src/hooks/TaskRunner.ts create mode 100644 samples/apps/book-creator-webapp-react/src/hooks/useSemanticKernel.ts create mode 100644 samples/apps/book-creator-webapp-react/src/hooks/useTaskRunner.ts create mode 100644 samples/apps/book-creator-webapp-react/src/index.tsx create mode 100644 samples/apps/book-creator-webapp-react/src/model/Ask.ts create mode 100644 samples/apps/book-creator-webapp-react/src/model/AskResult.ts create mode 100644 samples/apps/book-creator-webapp-react/src/model/KeyConfig.ts create mode 100644 samples/apps/book-creator-webapp-react/src/model/Page.ts create mode 100644 samples/apps/book-creator-webapp-react/src/react-app-env.d.ts create mode 100644 samples/apps/book-creator-webapp-react/tsconfig.json create mode 100644 samples/apps/book-creator-webapp-react/yarn.lock create mode 100644 samples/apps/chat-summary-webapp-react/.env create mode 100644 samples/apps/chat-summary-webapp-react/README.md create mode 100644 samples/apps/chat-summary-webapp-react/package.json create mode 100644 samples/apps/chat-summary-webapp-react/public/favicon.ico create mode 100644 samples/apps/chat-summary-webapp-react/public/index.html create mode 100644 samples/apps/chat-summary-webapp-react/src/App.css create mode 100644 samples/apps/chat-summary-webapp-react/src/App.tsx create mode 100644 samples/apps/chat-summary-webapp-react/src/components/AISummary.tsx create mode 100644 samples/apps/chat-summary-webapp-react/src/components/FunctionProbe.tsx create mode 100644 samples/apps/chat-summary-webapp-react/src/components/QuickTipGroup.tsx create mode 100644 samples/apps/chat-summary-webapp-react/src/components/QuickTips.tsx create mode 100644 samples/apps/chat-summary-webapp-react/src/components/ServiceConfig.tsx create mode 100644 samples/apps/chat-summary-webapp-react/src/components/chat/ChatHistoryItem.tsx create mode 100644 samples/apps/chat-summary-webapp-react/src/components/chat/ChatInput.tsx create mode 100644 samples/apps/chat-summary-webapp-react/src/components/chat/ChatInteraction.tsx create mode 100644 samples/apps/chat-summary-webapp-react/src/components/chat/ChatThread.ts create mode 100644 samples/apps/chat-summary-webapp-react/src/hooks/SemanticKernel.ts create mode 100644 samples/apps/chat-summary-webapp-react/src/hooks/useSemanticKernel.ts create mode 100644 samples/apps/chat-summary-webapp-react/src/index.tsx create mode 100644 samples/apps/chat-summary-webapp-react/src/model/Ask.ts create mode 100644 samples/apps/chat-summary-webapp-react/src/model/AskResult.ts create mode 100644 samples/apps/chat-summary-webapp-react/src/model/KeyConfig.ts create mode 100644 samples/apps/chat-summary-webapp-react/src/react-app-env.d.ts create mode 100644 samples/apps/chat-summary-webapp-react/tsconfig.json create mode 100644 samples/apps/chat-summary-webapp-react/yarn.lock create mode 100644 samples/apps/prettier.config.js create mode 100644 samples/dotnet/KernelBuilder/GlobalUsings.cs create mode 100644 samples/dotnet/KernelBuilder/KernelBuilder.csproj create mode 100644 samples/dotnet/KernelBuilder/Program.cs create mode 100644 samples/dotnet/api-azure-function/.vscode/extensions.json create mode 100644 samples/dotnet/api-azure-function/Config/ApiKeyConfig.cs create mode 100644 samples/dotnet/api-azure-function/Config/CompletionService.cs create mode 100644 samples/dotnet/api-azure-function/Extensions.cs create mode 100644 samples/dotnet/api-azure-function/Model/Ask.cs create mode 100644 samples/dotnet/api-azure-function/Model/AskResult.cs create mode 100644 samples/dotnet/api-azure-function/Model/Skill.cs create mode 100644 samples/dotnet/api-azure-function/PingEndpoint.cs create mode 100644 samples/dotnet/api-azure-function/Program.cs create mode 100644 samples/dotnet/api-azure-function/README.md create mode 100644 samples/dotnet/api-azure-function/SemanticKernelEndpoint.cs create mode 100644 samples/dotnet/api-azure-function/SemanticKernelFactory.cs create mode 100644 samples/dotnet/api-azure-function/SemanticKernelFunction.csproj create mode 100644 samples/dotnet/api-azure-function/TokenAuthenticationProvider.cs create mode 100644 samples/dotnet/api-azure-function/Utils/RepoFiles.cs create mode 100644 samples/dotnet/api-azure-function/Utils/YourAppException.cs create mode 100644 samples/dotnet/api-azure-function/host.json create mode 100644 samples/dotnet/api-azure-function/local.settings.json create mode 100644 samples/dotnet/graph-api-skills/AzureOpenAIConfiguration.cs create mode 100644 samples/dotnet/graph-api-skills/MsGraphSkillsExample.csproj create mode 100644 samples/dotnet/graph-api-skills/OpenAIConfiguration.cs create mode 100644 samples/dotnet/graph-api-skills/Program.cs create mode 100644 samples/dotnet/graph-api-skills/Properties/launchSettings.json create mode 100644 samples/dotnet/graph-api-skills/README.md create mode 100644 samples/dotnet/graph-api-skills/appsettings.json create mode 100644 samples/dotnet/kernel-extension-load-prompts-from-cloud/LoadPromptsFromCloud.csproj create mode 100644 samples/dotnet/kernel-extension-load-prompts-from-cloud/SampleExtension.cs create mode 100644 samples/dotnet/kernel-syntax-examples/Example01_NativeFunctions.cs create mode 100644 samples/dotnet/kernel-syntax-examples/Example02_Pipeline.cs create mode 100644 samples/dotnet/kernel-syntax-examples/Example03_Variables.cs create mode 100644 samples/dotnet/kernel-syntax-examples/Example04_BingSkillAndConnector.cs create mode 100644 samples/dotnet/kernel-syntax-examples/Example05_CombineLLMPromptsAndNativeCode.cs create mode 100644 samples/dotnet/kernel-syntax-examples/Example06_InlineFunctionDefinition.cs create mode 100644 samples/dotnet/kernel-syntax-examples/Example07_TemplateLanguage.cs create mode 100644 samples/dotnet/kernel-syntax-examples/Example08_RetryMechanism.cs create mode 100644 samples/dotnet/kernel-syntax-examples/Example09_FunctionTypes.cs create mode 100644 samples/dotnet/kernel-syntax-examples/Example10_DescribeAllSkillsAndFunctions.cs create mode 100644 samples/dotnet/kernel-syntax-examples/Example11_WebSearchQueries.cs create mode 100644 samples/dotnet/kernel-syntax-examples/Example12_Planning.cs create mode 100644 samples/dotnet/kernel-syntax-examples/Example13_ConversationSummarySkill.cs create mode 100644 samples/dotnet/kernel-syntax-examples/Example14_Memory.cs create mode 100644 samples/dotnet/kernel-syntax-examples/Example15_MemorySkill.cs create mode 100644 samples/dotnet/kernel-syntax-examples/KernelSyntaxExamples.csproj create mode 100644 samples/dotnet/kernel-syntax-examples/Program.cs create mode 100644 samples/dotnet/kernel-syntax-examples/Reliability/RetryThreeTimesWithBackoff.cs create mode 100644 samples/dotnet/kernel-syntax-examples/RepoUtils/ConsoleLogger.cs create mode 100644 samples/dotnet/kernel-syntax-examples/RepoUtils/Env.cs create mode 100644 samples/dotnet/kernel-syntax-examples/RepoUtils/RepoFiles.cs create mode 100644 samples/dotnet/kernel-syntax-examples/RepoUtils/YourAppException.cs create mode 100644 samples/dotnet/kernel-syntax-examples/Skills/EmailSkill.cs create mode 100644 samples/dotnet/kernel-syntax-examples/Skills/StaticTextSkill.cs create mode 100644 samples/dotnet/kernel-syntax-examples/Skills/TextSkill.cs create mode 100644 samples/notebooks/dotnet/0-AI-settings.ipynb create mode 100644 samples/notebooks/dotnet/1-basic-loading-the-kernel.ipynb create mode 100644 samples/notebooks/dotnet/2-running-prompts-from-file.ipynb create mode 100644 samples/notebooks/dotnet/3-semantic-function-inline.ipynb create mode 100644 samples/notebooks/dotnet/4-context-variables-chat.ipynb create mode 100644 samples/notebooks/dotnet/5-using-the-planner.ipynb create mode 100644 samples/notebooks/dotnet/6-memory-and-embeddings.ipynb create mode 100644 samples/notebooks/dotnet/Getting-Started-Notebook.ipynb create mode 100644 samples/notebooks/dotnet/README.md create mode 100644 samples/notebooks/dotnet/config/.gitignore create mode 100644 samples/notebooks/dotnet/config/Settings.cs create mode 100644 samples/notebooks/dotnet/config/settings.json.azure-example create mode 100644 samples/notebooks/dotnet/config/settings.json.openai-example create mode 100644 samples/skills/CalendarSkill/AssistantShowCalendarEvents/config.json create mode 100644 samples/skills/CalendarSkill/AssistantShowCalendarEvents/skprompt.txt create mode 100644 samples/skills/ChatSkill/Chat/config.json create mode 100644 samples/skills/ChatSkill/Chat/skprompt.txt create mode 100644 samples/skills/ChatSkill/ChatFilter/config.json create mode 100644 samples/skills/ChatSkill/ChatFilter/skprompt.txt create mode 100644 samples/skills/ChatSkill/ChatGPT/config.json create mode 100644 samples/skills/ChatSkill/ChatGPT/skprompt.txt create mode 100644 samples/skills/ChatSkill/ChatUser/config.json create mode 100644 samples/skills/ChatSkill/ChatUser/skprompt.txt create mode 100644 samples/skills/ChatSkill/ChatV2/config.json create mode 100644 samples/skills/ChatSkill/ChatV2/skprompt.txt create mode 100644 samples/skills/ChildrensBookSkill/BookIdeas/config.json create mode 100644 samples/skills/ChildrensBookSkill/BookIdeas/skprompt.txt create mode 100644 samples/skills/ChildrensBookSkill/CreateBook/config.json create mode 100644 samples/skills/ChildrensBookSkill/CreateBook/skprompt.txt create mode 100644 samples/skills/ClassificationSkill/Importance/config.json create mode 100644 samples/skills/ClassificationSkill/Importance/skprompt.txt create mode 100644 samples/skills/CodingSkill/Code/config.json create mode 100644 samples/skills/CodingSkill/Code/skprompt.txt create mode 100644 samples/skills/CodingSkill/CodePython/config.json create mode 100644 samples/skills/CodingSkill/CodePython/skprompt.txt create mode 100644 samples/skills/CodingSkill/CommandLinePython/config.json create mode 100644 samples/skills/CodingSkill/CommandLinePython/skprompt.txt create mode 100644 samples/skills/CodingSkill/DOSScript/config.json create mode 100644 samples/skills/CodingSkill/DOSScript/skprompt.txt create mode 100644 samples/skills/CodingSkill/EmailSearch/config.json create mode 100644 samples/skills/CodingSkill/EmailSearch/skprompt.txt create mode 100644 samples/skills/CodingSkill/Entity/config.json create mode 100644 samples/skills/CodingSkill/Entity/skprompt.txt create mode 100644 samples/skills/FunSkill/Excuses/config.json create mode 100644 samples/skills/FunSkill/Excuses/skprompt.txt create mode 100644 samples/skills/FunSkill/Joke/config.json create mode 100644 samples/skills/FunSkill/Joke/skprompt.txt create mode 100644 samples/skills/FunSkill/Limerick/config.json create mode 100644 samples/skills/FunSkill/Limerick/skprompt.txt create mode 100644 samples/skills/IntentDetectionSkill/AssistantIntent/config.json create mode 100644 samples/skills/IntentDetectionSkill/AssistantIntent/skprompt.txt create mode 100644 samples/skills/MiscSkill/Continue/config.json create mode 100644 samples/skills/MiscSkill/Continue/skprompt.txt create mode 100644 samples/skills/OpenApiSkill/OpenApiJsonToHttpClient/config.json create mode 100644 samples/skills/OpenApiSkill/OpenApiJsonToHttpClient/skprompt.txt create mode 100644 samples/skills/QASkill/AssistantResults/config.json create mode 100644 samples/skills/QASkill/AssistantResults/skprompt.txt create mode 100644 samples/skills/QASkill/ContextQuery/config.json create mode 100644 samples/skills/QASkill/ContextQuery/skprompt.txt create mode 100644 samples/skills/QASkill/Form/config.json create mode 100644 samples/skills/QASkill/Form/skprompt.txt create mode 100644 samples/skills/QASkill/QNA/config.json create mode 100644 samples/skills/QASkill/QNA/skprompt.txt create mode 100644 samples/skills/QASkill/Question/config.json create mode 100644 samples/skills/QASkill/Question/skprompt.txt create mode 100644 samples/skills/SummarizeSkill/MakeAbstractReadable/config.json create mode 100644 samples/skills/SummarizeSkill/MakeAbstractReadable/skprompt.txt create mode 100644 samples/skills/SummarizeSkill/Notegen/config.json create mode 100644 samples/skills/SummarizeSkill/Notegen/skprompt.txt create mode 100644 samples/skills/SummarizeSkill/Summarize/config.json create mode 100644 samples/skills/SummarizeSkill/Summarize/skprompt.txt create mode 100644 samples/skills/SummarizeSkill/Topics/config.json create mode 100644 samples/skills/SummarizeSkill/Topics/skprompt.txt create mode 100644 samples/skills/WriterSkill/Acronym/config.json create mode 100644 samples/skills/WriterSkill/Acronym/skprompt.txt create mode 100644 samples/skills/WriterSkill/AcronymGenerator/config.json create mode 100644 samples/skills/WriterSkill/AcronymGenerator/skprompt.txt create mode 100644 samples/skills/WriterSkill/AcronymReverse/config.json create mode 100644 samples/skills/WriterSkill/AcronymReverse/skprompt.txt create mode 100644 samples/skills/WriterSkill/Brainstorm/config.json create mode 100644 samples/skills/WriterSkill/Brainstorm/skprompt.txt create mode 100644 samples/skills/WriterSkill/EmailGen/config.json create mode 100644 samples/skills/WriterSkill/EmailGen/skprompt.txt create mode 100644 samples/skills/WriterSkill/EmailTo/config.json create mode 100644 samples/skills/WriterSkill/EmailTo/skprompt.txt create mode 100644 samples/skills/WriterSkill/NovelChapter/config.json create mode 100644 samples/skills/WriterSkill/NovelChapter/skprompt.txt create mode 100644 samples/skills/WriterSkill/NovelChapterWithNotes/config.json create mode 100644 samples/skills/WriterSkill/NovelChapterWithNotes/skprompt.txt create mode 100644 samples/skills/WriterSkill/NovelOutline/config.json create mode 100644 samples/skills/WriterSkill/NovelOutline/skprompt.txt create mode 100644 samples/skills/WriterSkill/Rewrite/config.json create mode 100644 samples/skills/WriterSkill/Rewrite/skprompt.txt create mode 100644 samples/skills/WriterSkill/ShortPoem/config.json create mode 100644 samples/skills/WriterSkill/ShortPoem/skprompt.txt create mode 100644 samples/skills/WriterSkill/StoryGen/config.json create mode 100644 samples/skills/WriterSkill/StoryGen/skprompt.txt create mode 100644 samples/skills/WriterSkill/TellMeMore/config.json create mode 100644 samples/skills/WriterSkill/TellMeMore/skprompt.txt create mode 100644 samples/skills/WriterSkill/Translate/config.json create mode 100644 samples/skills/WriterSkill/Translate/skprompt.txt create mode 100644 samples/skills/WriterSkill/TranslateV2/config.json create mode 100644 samples/skills/WriterSkill/TranslateV2/skprompt.txt create mode 100644 samples/skills/WriterSkill/TwoSentenceSummary/config.json create mode 100644 samples/skills/WriterSkill/TwoSentenceSummary/skprompt.txt diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000000..79353ef9cc9c --- /dev/null +++ b/.editorconfig @@ -0,0 +1,255 @@ +# To learn more about .editorconfig see https://aka.ms/editorconfigdocs +############################### +# Core EditorConfig Options # +############################### +root = true +# All files +[*] +indent_style = space +end_of_line = lf + +# XML project files +[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}] +indent_size = 2 + +# XML config files +[*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}] +indent_size = 2 + +# YAML config files +[*.{yml,yaml}] +tab_width = 2 +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true + +# JSON config files +[*.json] +tab_width = 2 +indent_size = 2 +insert_final_newline = false +trim_trailing_whitespace = true + +# Typescript files +[*.{ts,tsx}] +insert_final_newline = true +trim_trailing_whitespace = true +tab_width = 4 +indent_size = 4 +file_header_template = Copyright (c) Microsoft. All rights reserved. + +# Stylesheet files +[*.{css,scss,sass,less}] +insert_final_newline = true +trim_trailing_whitespace = true +tab_width = 4 +indent_size = 4 + +# Code files +[*.{cs,csx,vb,vbx}] +tab_width = 4 +indent_size = 4 +insert_final_newline = true +trim_trailing_whitespace = true +charset = utf-8-bom +file_header_template = Copyright (c) Microsoft. All rights reserved. + +############################### +# .NET Coding Conventions # +############################### +[*.{cs,vb}] +# Organize usings +dotnet_sort_system_directives_first = true +# this. preferences +dotnet_style_qualification_for_field = true:error +dotnet_style_qualification_for_property = true:error +dotnet_style_qualification_for_method = true:error +dotnet_style_qualification_for_event = true:error +# Language keywords vs BCL types preferences +dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion +dotnet_style_predefined_type_for_member_access = true:suggestion +# Parentheses preferences +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:suggestion +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:suggestion +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent +# Modifier preferences +dotnet_style_require_accessibility_modifiers = for_non_interface_members:error +dotnet_style_readonly_field = true:suggestion +# Expression-level preferences +dotnet_style_object_initializer = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion +dotnet_style_prefer_inferred_tuple_names = true:suggestion +dotnet_style_prefer_inferred_anonymous_type_member_names = true:silent +dotnet_style_prefer_auto_properties = true:suggestion +dotnet_style_prefer_conditional_expression_over_assignment = true:silent +dotnet_style_prefer_conditional_expression_over_return = true:silent +dotnet_style_prefer_simplified_interpolation = true:suggestion +dotnet_style_operator_placement_when_wrapping = beginning_of_line +dotnet_style_namespace_match_folder = true:suggestion +# Code quality rules +dotnet_code_quality_unused_parameters = all:suggestion + +# Suppressed diagnostics +dotnet_diagnostic.CA1002.severity = none # Change 'List' in '...' to use 'Collection' ... +dotnet_diagnostic.CA1032.severity = none # Add the following constructor to ...Exception: public ... +dotnet_diagnostic.CA1034.severity = none # Do not nest type. Alternatively, change its accessibility so that it is not externally visible +dotnet_diagnostic.CA1062.severity = none # Disable null check, C# already does it for usdotnet_diagnostic.CA1416.severity = silent # Validate platform compatibility +dotnet_diagnostic.CA1303.severity = none # Do not pass literals as localized parameters +dotnet_diagnostic.CS1591.severity = none # Missing XML comment for publicly visible type or member +dotnet_diagnostic.CA1805.severity = none # Member is explicitly initialized to its default value +dotnet_diagnostic.CA1822.severity = none # Member does not access instance data and can be marked as static +dotnet_diagnostic.CA1848.severity = none # For improved performance, use the LoggerMessage delegates +dotnet_diagnostic.CA1852.severity = none # Type '...' can be sealed because it has no subtypes in its containing assembly and is not externally visible +dotnet_diagnostic.CA2007.severity = none # Do not directly await a Task +dotnet_diagnostic.CA2225.severity = none # Operator overloads have named alternates +dotnet_diagnostic.CA2227.severity = none # Change to be read-only by removing the property setter +dotnet_diagnostic.CA2253.severity = none # Named placeholders in the logging message template should not be comprised of only numeric characters + +# Diagnostics elevated as warnings +dotnet_diagnostic.CA1000.severity = warning # Do not declare static members on generic types +dotnet_diagnostic.CA1031.severity = warning # Do not catch general exception types +dotnet_diagnostic.CA1063.severity = warning # Implement IDisposable correctly +dotnet_diagnostic.CA1064.severity = warning # Exceptions should be public +dotnet_diagnostic.CA1508.severity = warning # Avoid dead conditional code +dotnet_diagnostic.CA2000.severity = warning # Call System.IDisposable.Dispose on object before all references to it are out of scope +dotnet_diagnostic.CA2201.severity = warning # Exception type System.Exception is not sufficiently specific + + +############################### +# Naming Conventions # +############################### +# Style Definitions +dotnet_naming_style.pascal_case_style.capitalization = pascal_case +# Use PascalCase for constant fields +dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = error +dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields +dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style +# Define the 'constant_fields' symbol group: +dotnet_naming_symbols.constant_fields.applicable_kinds = field +dotnet_naming_symbols.constant_fields.applicable_accessibilities = * +dotnet_naming_symbols.constant_fields.required_modifiers = const + +# Define the 'private_static_fields' symbol group +dotnet_naming_symbols.private_static_fields.applicable_kinds = field +dotnet_naming_symbols.private_static_fields.applicable_accessibilities = private +dotnet_naming_symbols.private_static_fields.required_modifiers = static +# Define the 'static_underscored' naming style +dotnet_naming_style.static_underscored.capitalization = camel_case +dotnet_naming_style.static_underscored.required_prefix = s_ +# Define the 'private_static_fields_underscored' naming rule +dotnet_naming_rule.private_static_fields_underscored.symbols = private_static_fields +dotnet_naming_rule.private_static_fields_underscored.style = static_underscored +dotnet_naming_rule.private_static_fields_underscored.severity = error + +# Define the 'private_fields' symbol group: +dotnet_naming_symbols.private_fields.applicable_kinds = field +dotnet_naming_symbols.private_fields.applicable_accessibilities = private +# Define the 'underscored' naming style +dotnet_naming_style.underscored.capitalization = camel_case +dotnet_naming_style.underscored.required_prefix = _ +# Define the 'private_fields_underscored' naming rule +dotnet_naming_rule.private_fields_underscored.symbols = private_fields +dotnet_naming_rule.private_fields_underscored.style = underscored +dotnet_naming_rule.private_fields_underscored.severity = error + +# Define the 'any_async_methods' symbol group +dotnet_naming_symbols.any_async_methods.applicable_kinds = method +dotnet_naming_symbols.any_async_methods.applicable_accessibilities = * +dotnet_naming_symbols.any_async_methods.required_modifiers = async +# Define the 'end_in_async' naming style +dotnet_naming_style.end_in_async.required_prefix = +dotnet_naming_style.end_in_async.required_suffix = Async +dotnet_naming_style.end_in_async.capitalization = pascal_case +dotnet_naming_style.end_in_async.word_separator = +# Define the 'async_methods_end_in_async' naming rule +dotnet_naming_rule.async_methods_end_in_async.symbols = any_async_methods +dotnet_naming_rule.async_methods_end_in_async.style = end_in_async +dotnet_naming_rule.async_methods_end_in_async.severity = error +dotnet_style_prefer_simplified_boolean_expressions = true:suggestion +dotnet_style_prefer_compound_assignment = true:suggestion + +############################### +# C# Coding Conventions # +############################### +[*.cs] +# var preferences +csharp_style_var_for_built_in_types = false:none +csharp_style_var_when_type_is_apparent = false:none +csharp_style_var_elsewhere = false:none +# Expression-bodied members +csharp_style_expression_bodied_methods = false:silent +csharp_style_expression_bodied_constructors = false:silent +csharp_style_expression_bodied_operators = false:silent +csharp_style_expression_bodied_properties = true:silent +csharp_style_expression_bodied_indexers = true:silent +csharp_style_expression_bodied_accessors = true:silent +# Pattern matching preferences +csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion +csharp_style_pattern_matching_over_as_with_null_check = true:suggestion +# Null-checking preferences +csharp_style_throw_expression = true:suggestion +csharp_style_conditional_delegate_call = true:suggestion +# Modifier preferences +csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:suggestion +# Expression-level preferences +csharp_prefer_braces = true:error +csharp_style_deconstructed_variable_declaration = true:suggestion +csharp_prefer_simple_default_expression = true:suggestion +csharp_style_prefer_local_over_anonymous_function = true:error +csharp_style_inlined_variable_declaration = true:suggestion + +############################### +# C# Formatting Rules # +############################### +[*.cs] +# New line preferences +csharp_new_line_before_open_brace = all +csharp_new_line_before_else = true +csharp_new_line_before_catch = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_object_initializers = false # Does not work with resharper, forcing code to be on long lines instead of wrapping +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_between_query_expression_clauses = true +# Indentation preferences +csharp_indent_braces = false +csharp_indent_case_contents = true +csharp_indent_case_contents_when_block = false +csharp_indent_switch_labels = true +csharp_indent_labels = flush_left +# Space preferences +csharp_space_after_cast = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_around_binary_operators = before_and_after +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +# Wrapping preferences +csharp_preserve_single_line_statements = true +csharp_preserve_single_line_blocks = true +csharp_using_directive_placement = outside_namespace:warning +csharp_prefer_simple_using_statement = true:suggestion +csharp_style_namespace_declarations = file_scoped:warning +csharp_style_prefer_method_group_conversion = true:silent +csharp_style_prefer_top_level_statements = true:silent +csharp_style_expression_bodied_lambdas = true:silent +csharp_style_expression_bodied_local_functions = false:silent + +############################### +# VB Coding Conventions # +############################### +[*.vb] +trim_trailing_whitespace = true +tab_width = 4 +indent_size = 4 +# Modifier preferences +visual_basic_preferred_modifier_order = Partial,Default,Private,Protected,Public,Friend,NotOverridable,Overridable,MustOverride,Overloads,Overrides,MustInherit,NotInheritable,Static,Shared,Shadows,ReadOnly,WriteOnly,Dim,Const,WithEvents,Widening,Narrowing,Custom,Async:suggestion diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000000..e79a913b6c46 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,5 @@ +# Auto-detect text files, ensure they use LF. +* text=auto eol=lf + +# Bash scripts +*.sh text eol=lf diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 000000000000..aaf733295b80 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,32 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** + - OS: [e.g. Windows] + - IDE: [e.g. Visual Studio, VS Code] + - NuGet Package Version [e.g. 0.1.0] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 000000000000..2d490077748e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + +--- +name: Feature request +about: Suggest an idea for this project + +--- + + + + + + diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000000..37af22678e75 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,18 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: "nuget" + directory: "dotnet/" + schedule: + interval: "weekly" + + - package-ecosystem: "github-actions" + # Workflow files stored in the + # default location of `.github/workflows` + directory: "/" + schedule: + interval: "weekly" diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 000000000000..c4f1fcf72fb3 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,32 @@ +### Motivation and Context + + + +### Description + + + +### Contribution Checklist + + + +- [ ] The code builds clean without any errors or warnings +- [ ] The PR follows SK Contribution Guidelines (https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md) +- [ ] The code follows the .NET coding conventions (https://learn.microsoft.com/dotnet/csharp/fundamentals/coding-style/coding-conventions) verified with `dotnet format` +- [ ] All unit tests pass, and I have added new tests where possible +- [ ] I didn't break anyone :smile: + + diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 000000000000..dbc52d694bd8 --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,66 @@ +# CodeQL is the code analysis engine developed by GitHub to automate security checks. +# The results are shown as code scanning alerts in GitHub. For more details, visit: +# https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/about-code-scanning-with-codeql + +name: "CodeQL" + +on: + push: + branches: [ "main", experimental ] + schedule: + - cron: '17 11 * * 2' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'csharp' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] + # Use only 'java' to analyze code written in Java, Kotlin or both + # Use only 'javascript' to analyze code written in JavaScript, TypeScript or both + # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + + # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v2 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + + # If the Autobuild fails above, remove it and uncomment the following three lines. + # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. + + # - run: | + # echo "Run, Build Application using script" + # ./location_of_script_within_repo/buildscript.sh + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 + with: + category: "/language:${{matrix.language}}" diff --git a/.github/workflows/dotnet-ci.yml b/.github/workflows/dotnet-ci.yml new file mode 100644 index 000000000000..611d0f81115f --- /dev/null +++ b/.github/workflows/dotnet-ci.yml @@ -0,0 +1,143 @@ +# +# This workflow will build and run all unit tests. +# + +name: dotnet-ci + +on: + workflow_dispatch: + push: + branches: [ "main" ] + +permissions: + contents: read + packages: write + +jobs: + build: + strategy: + matrix: + os: [ubuntu-latest, windows-latest] + configuration: [Release, Debug] + runs-on: ${{ matrix.os }} + env: + NUGET_CERT_REVOCATION_MODE: offline + steps: + - uses: actions/checkout@v3 + with: + clean: true + + - name: Setup .NET + uses: actions/setup-dotnet@v3 + with: + dotnet-version: 6.0.x + env: + NUGET_AUTH_TOKEN: ${{ secrets.GPR_READ_TOKEN }} + + - uses: actions/cache@v3 + with: + path: ~/.nuget/packages + # Look to see if there is a cache hit for the corresponding requirements file + key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }} + restore-keys: | + ${{ runner.os }}-nuget + + - name: Update project versions + shell: bash + run: | + allProjectFiles=$(find ./dotnet -type f -name "*.csproj" | tr '\n' ' '); + if [ $? -ne 0 ]; then exit 1; fi + echo "$allProjectFiles" + branchName="${{ github.ref }}" + + buildAndRevisionNumber="${{ github.run_number }}.${{ github.run_attempt }}" + if [ $branchName != "refs/heads/main" ]; then + buildAndRevisionNumber="$buildAndRevisionNumber-dev" + fi + + echo "buildAndRevisionNumber: $buildAndRevisionNumber" + + for file in $allProjectFiles; do + ./.github/workflows/update-version.sh --file $file --buildAndRevisionNumber $buildAndRevisionNumber + done + + - name: Find solutions + shell: bash + run: echo "solutions=$(find ./dotnet -type f -name "*.sln" | tr '\n' ' ')" >> $GITHUB_ENV + + - name: Restore dependencies + shell: bash + run: | + for solution in ${{ env.solutions }}; do + dotnet restore $solution + done + + - name: Build + shell: bash + run: | + for solution in ${{ env.solutions }}; do + dotnet build $solution --no-restore --configuration ${{ matrix.configuration }} + done + + - name: Find unit test projects + shell: bash + run: echo "projects=$(find ./dotnet -type f -name "*.Test.csproj" | tr '\n' ' ')" >> $GITHUB_ENV + + - name: Test + shell: bash + run: | + for project in ${{ env.projects }}; do + dotnet test $project --no-build --verbosity normal --logger trx --results-directory ./TestResults --configuration ${{ matrix.configuration }} + done + + - name: Upload dotnet test results + uses: actions/upload-artifact@v3 + with: + name: dotnet-testresults-${{ matrix.configuration }} + path: ./TestResults + if: ${{ always() }} + + - name: Stage artifacts + shell: bash + run: mkdir ./out; find . | grep "/bin/" | xargs cp -r --parents -t ./out + if: ${{ github.event_name == 'push' }} + + - name: Archive artifacts ${{ matrix.os }}-${{ matrix.configuration }} + uses: actions/upload-artifact@v3 + with: + name: drop-${{ matrix.os }}-${{ matrix.configuration }} + path: ./out + if: ${{ github.event_name == 'push' }} + + publish-nuget: + runs-on: ubuntu-latest + needs: build + continue-on-error: true + if: ${{ github.event_name == 'push' }} + steps: + # Pull all nuget packages (*.nupkg) and the accompanying symbols packages (*.snupkg) + - uses: actions/download-artifact@v3 + with: + name: drop-ubuntu-latest-Release + path: ./out/dotnet/**/*nupkg + + - name: Setup .NET + uses: actions/setup-dotnet@v3 + with: + dotnet-version: 6.0.x + + - name: Find nupkg files + shell: bash + run: echo "nuget_packages=$(find ./out/dotnet -type f -name "*nupkg" | tr '\n' ' ')" >> $GITHUB_ENV + + # Publish NuGet to GitHub Package Repository + # This will publish both the nuget package (*.nupkg) and the accompanying symbols (*.snupkg). + - name: Publish to GitHub package repository + shell: bash + run: | + for nupkg in ${{ env.nuget_packages }}; do + dotnet nuget push $nupkg \ + --api-key ${{ secrets.GITHUB_TOKEN }} \ + --source https://nuget.pkg.github.com/${{ github.repository_owner }} \ + --skip-duplicate + done diff --git a/.github/workflows/dotnet-format.yml b/.github/workflows/dotnet-format.yml new file mode 100644 index 000000000000..07c6d0243513 --- /dev/null +++ b/.github/workflows/dotnet-format.yml @@ -0,0 +1,44 @@ +# +# This workflow runs the dotnet formatter on all c-sharp code. +# + +name: dotnet-format + +on: + workflow_dispatch: + pull_request: + branches: [ "main" ] + paths: + - "dotnet/**" + - "**.cs" + - "**.csproj" + - ".editorconfig" + +jobs: + check-format: + runs-on: ubuntu-latest + + steps: + - name: Setup .NET Core + uses: actions/setup-dotnet@v3 + with: + dotnet-version: '6.0.x' + + - name: Install dotnet-format tool + run: dotnet tool install -g dotnet-format + + - name: Check out code + uses: actions/checkout@v3.3.0 + with: + fetch-depth: 0 + + - name: Run dotnet format + shell: bash + run: | + allProjectFiles=$(find ./ -type f -name "*.csproj" | tr '\n' ' '); + if [ $? -ne 0 ]; then exit 1; fi + echo "$allProjectFiles" + + for file in $allProjectFiles; do + dotnet format $file --verify-no-changes --verbosity detailed + done diff --git a/.github/workflows/dotnet-integration-tests.yml b/.github/workflows/dotnet-integration-tests.yml new file mode 100644 index 000000000000..98514d992101 --- /dev/null +++ b/.github/workflows/dotnet-integration-tests.yml @@ -0,0 +1,54 @@ +# +# This workflow will run all integrations tests. +# + +name: dotnet-integration-tests + +on: + workflow_dispatch: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +jobs: + integration-tests: + strategy: + matrix: + os: [ubuntu-latest] + configuration: [Debug] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v3 + with: + clean: true + + - name: Setup .NET + uses: actions/setup-dotnet@v3 + with: + dotnet-version: 6.0.x + + - name: Find projects + shell: bash + run: echo "projects=$(find ./dotnet -type f -name "*IntegrationTests.csproj" | tr '\n' ' ')" >> $GITHUB_ENV + + - name: Integration Tests + shell: bash + env: # Set Azure credentials secret as an input + AzureOpenAI__Label: azure-text-davinci-003 + AzureOpenAI__DeploymentName: ${{ vars.AZUREOPENAI__DEPLOYMENTNAME }} + AzureOpenAI__Endpoint: ${{ secrets.AZUREOPENAI__ENDPOINT }} + AzureOpenAI__ApiKey: ${{ secrets.AZUREOPENAI__APIKEY }} + Bing__ApiKey: ${{ secrets.BING__APIKEY }} + OpneAI__ApiKey: ${{ secrets.OPENAI__APIKEY }} + run: | + for project in ${{ env.projects }}; do + dotnet test $project --verbosity normal --logger trx --results-directory ./TestResults --configuration ${{ matrix.configuration }} + done + + - name: Upload dotnet test results + uses: actions/upload-artifact@v3 + with: + name: dotnet-testresults-${{ matrix.configuration }} + path: ./TestResults + if: ${{ always() }} diff --git a/.github/workflows/dotnet-pr.yml b/.github/workflows/dotnet-pr.yml new file mode 100644 index 000000000000..83f95e3ef030 --- /dev/null +++ b/.github/workflows/dotnet-pr.yml @@ -0,0 +1,97 @@ +# +# This workflow will build and run all unit tests. +# + +name: dotnet-pr + +on: + workflow_dispatch: + pull_request: + branches: [ "main" ] + +permissions: + contents: read + +jobs: + build: + strategy: + matrix: + os: [ubuntu-latest] + configuration: [Release, Debug] + runs-on: ${{ matrix.os }} + env: + NUGET_CERT_REVOCATION_MODE: offline + steps: + - uses: actions/checkout@v3 + with: + clean: true + + - name: Setup .NET + uses: actions/setup-dotnet@v3 + with: + dotnet-version: 6.0.x + env: + NUGET_AUTH_TOKEN: ${{ secrets.GPR_READ_TOKEN }} + + - uses: actions/cache@v3 + with: + path: ~/.nuget/packages + # Look to see if there is a cache hit for the corresponding requirements file + key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }} + restore-keys: | + ${{ runner.os }}-nuget + + - name: Update project versions + shell: bash + run: | + allProjectFiles=$(find ./dotnet -type f -name "*.csproj" | tr '\n' ' '); + if [ $? -ne 0 ]; then exit 1; fi + echo "$allProjectFiles" + branchName="${{ github.ref }}" + + buildAndRevisionNumber="${{ github.run_number }}.${{ github.run_attempt }}" + if [ $branchName != "refs/heads/main" ]; then + buildAndRevisionNumber="$buildAndRevisionNumber-dev" + fi + + echo "buildAndRevisionNumber: $buildAndRevisionNumber" + + for file in $allProjectFiles; do + ./.github/workflows/update-version.sh --file $file --buildAndRevisionNumber $buildAndRevisionNumber + done + + - name: Find solutions + shell: bash + run: echo "solutions=$(find ./ -type f -name "*.sln" | tr '\n' ' ')" >> $GITHUB_ENV + + - name: Restore dependencies + shell: bash + run: | + for solution in ${{ env.solutions }}; do + dotnet restore $solution + done + + - name: Build + shell: bash + run: | + for solution in ${{ env.solutions }}; do + dotnet build $solution --no-restore --configuration ${{ matrix.configuration }} + done + + - name: Find unit test projects + shell: bash + run: echo "projects=$(find ./dotnet -type f -name "*.Test.csproj" | tr '\n' ' ')" >> $GITHUB_ENV + + - name: Test + shell: bash + run: | + for project in ${{ env.projects }}; do + dotnet test $project --no-build --verbosity normal --logger trx --results-directory ./TestResults --configuration ${{ matrix.configuration }} + done + + - name: Upload dotnet test results + uses: actions/upload-artifact@v3 + with: + name: dotnet-testresults-${{ matrix.configuration }} + path: ./TestResults + if: ${{ always() }} diff --git a/.github/workflows/markdown-link-check-config.json b/.github/workflows/markdown-link-check-config.json new file mode 100644 index 000000000000..4cb77ee2912f --- /dev/null +++ b/.github/workflows/markdown-link-check-config.json @@ -0,0 +1,19 @@ +{ + "ignorePatterns": [ + { "pattern": "/github/" }, + { "pattern": "./actions" }, + { "pattern": "./blob" }, + { "pattern": "./issues" } + ], + "replacementPatterns": [ + { + "pattern": "https://github.com/microsoft/semantic-kernel", + "replacement": "." + } + ], + "timeout": "20s", + "retryOn429": true, + "retryCount": 3, + "fallbackRetryDelay": "30s", + "aliveStatusCodes": [200, 206] +} diff --git a/.github/workflows/markdown-link-check.yml b/.github/workflows/markdown-link-check.yml new file mode 100644 index 000000000000..d50f6f8ee24a --- /dev/null +++ b/.github/workflows/markdown-link-check.yml @@ -0,0 +1,25 @@ +name: Check .md links + +on: + workflow_dispatch: + push: + branches: [ "main" ] + +permissions: + contents: read + +jobs: + markdown-link-check: + runs-on: ubuntu-latest + # check out the latest version of the code + steps: + - uses: actions/checkout@v3 + with: + ref: 'main' + + # Checks the status of hyperlinks in .md files in verbose mode + - name: Check links + uses: gaurav-nelson/github-action-markdown-link-check@v1 + with: + use-verbose-mode: 'yes' + config-file: ".github/workflows/markdown-link-check-config.json" diff --git a/.github/workflows/update-version.sh b/.github/workflows/update-version.sh new file mode 100755 index 000000000000..8ff9748ee7c1 --- /dev/null +++ b/.github/workflows/update-version.sh @@ -0,0 +1,51 @@ +#!/bin/bash + +#!/bin/bash + +POSITIONAL_ARGS=() + +while [[ $# -gt 0 ]]; do + case $1 in + -f|--file) + file="$2" + shift # past argument + shift # past value + ;; + -b|--buildAndRevisionNumber) + buildAndRevisionNumber="$2" + shift # past argument + shift # past value + ;; + -*|--*) + echo "Unknown option $1" + exit 1 + ;; + *) + POSITIONAL_ARGS+=("$1") # save positional arg + shift # past argument + ;; + esac +done + +set -- "${POSITIONAL_ARGS[@]}" # restore positional parameters + +echo "file = ${file}" +echo "buildAndRevisionNumber = ${buildAndRevisionNumber}" + +echo "====${file}===="; +versionString=$(cat $file | grep -i ""); +if [ -n "$versionString" ]; then + echo "Updating version tag..." + content=$(cat $file | sed --expression="s/\([0-9]*.[0-9]*\)<\/Version>/\1.$buildAndRevisionNumber-preview<\/Version>/g"); +else + if [ -n "$(cat $file | grep -i "false")" ]; then + echo "Project is marked as NOT packable - skipping." + continue; + fi + echo "Project is packable - adding version tag..." + content=$(cat $file | sed --expression="s/<\/Project>/1.0.$buildAndRevisionNumber-preview<\/Version><\/PropertyGroup><\/Project>/g"); +fi + +if [ $? -ne 0 ]; then exit 1; fi +echo "$content" && echo "$content" > $file; +if [ $? -ne 0 ]; then exit 1; fi diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000000..6083dcb18e8c --- /dev/null +++ b/.gitignore @@ -0,0 +1,443 @@ +dotnet/.config + +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates +nuget.config + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.tlog +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio 6 auto-generated project file (contains which files were open etc.) +*.vbp + +# Visual Studio 6 workspace and project file (working project files containing files to include in project) +*.dsw +*.dsp + +# Visual Studio 6 technical files +*.ncb +*.aps + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# Visual Studio History (VSHistory) files +.vshistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +# VS Code files for those working on multiple tools +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +# Windows Installer files from build outputs +*.cab +*.msi +*.msix +*.msm +*.msp + +# JetBrains Rider +*.sln.iml +*.tmp +*.log +*.bck +*.tgz +*.tar +*.zip +*.cer +*.crt +*.key + + +appsettings.development.json +testsettings.development.json +config.development.yaml +.DS_Store +.idea/ +node_modules/ +obj/ +bin/ +_dev/ +.dev/ +*.devis.* +.vs/ +*.user +**/.vscode/chrome +**/.vscode/.ropeproject/objectdb +*.pyc +.ipynb_checkpoints +.jython_cache/ +__pycache__/ +.mypy_cache/ +__pypackages__/ +.pdm.toml +global.json + +# doxfx +**/DROP/ +**/TEMP/ +**/packages/ +**/bin/ +**/obj/ +_site diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 000000000000..28789a5a5e14 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,11 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=827846 + // for the documentation about the extensions.json format + "recommendations": [ + "ms-azuretools.vscode-azurefunctions", + "ms-dotnettools.csharp", + "ms-dotnettools.dotnet-interactive-vscode", + "esbenp.prettier-vscode", + "dbaeumer.vscode-eslint" + ] +} \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 000000000000..de411f5e3684 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,26 @@ +{ + "version": "0.2.0", + "configurations": [ + { + // Use IntelliSense to find out which attributes exist for C# debugging + // Use hover for the description of the existing attributes + // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md + "name": ".NET Core Launch (dotnet-kernel-syntax-examples)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build (KernelSyntaxExamples)", + // If you have changed target frameworks, make sure to update the program path. + "program": "${workspaceFolder}/samples/dotnet-kernel-syntax-examples/bin/Debug/net6.0/KernelSyntaxExamples.dll", + "args": [], + "cwd": "${workspaceFolder}/samples/dotnet-kernel-syntax-examples", + // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console + "console": "internalConsole", + "stopAtEntry": false + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach" + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000000..555a45a3e66f --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,66 @@ +{ + "prettier.enable": true, + "css.lint.validProperties": ["composes"], + "editor.defaultFormatter": "ms-dotnettools.csharp", + "editor.formatOnType": true, + "editor.formatOnSave": true, + "editor.formatOnPaste": true, + "editor.codeActionsOnSave": { + "source.fixAll": true + }, + "[csharp]": { "editor.defaultFormatter": "ms-dotnettools.csharp" }, + "editor.bracketPairColorization.enabled": true, + "editor.guides.bracketPairs": "active", + "python.formatting.provider": "autopep8", + "python.formatting.autopep8Args": ["--max-line-length=120"], + "notebook.output.textLineLimit": 500, + "python.analysis.extraPaths": ["./python/src"], + + "javascript.updateImportsOnFileMove.enabled": "always", + "search.exclude": { + "**/node_modules": true, + "**/bower_components": true, + "**/build": true + }, + "[typescript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.codeActionsOnSave": { + "source.organizeImports": true, + "source.fixAll": true + } + }, + "[typescriptreact]": { + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.codeActionsOnSave": { + "source.organizeImports": true, + "source.fixAll": true + } + }, + "typescript.updateImportsOnFileMove.enabled": "always", + "eslint.enable": true, + "eslint.validate": [ + "javascript", + "javascriptreact", + "typescript", + "typescriptreact" + ], + "eslint.lintTask.enable": true, + "eslint.workingDirectories": [ + { + "mode": "auto" + } + ], + "eslint.options": { + "overrideConfigFile": ".eslintrc.js" + }, + "eslint.packageManager": "yarn", + "files.associations": { "*.json": "jsonc" }, + "files.exclude": { + "**/.git": true, + "**/.svn": true, + "**/.hg": true, + "**/CVS": true, + "**/.DS_Store": true, + "**/Thumbs.db": true + } +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 000000000000..ee900c3203d4 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,161 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "build (KernelSyntaxExamples)", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/samples/dotnet-kernel-syntax-examples/KernelSyntaxExamples.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary", + "/property:DebugType=portable" + ], + "problemMatcher": "$msCompile", + "group": "build" + }, + { + "label": "watch (KernelSyntaxExamples)", + "command": "dotnet", + "type": "process", + "args": [ + "watch", + "run", + "--project", + "${workspaceFolder}/samples/dotnet-kernel-syntax-examples/KernelSyntaxExamples.csproj" + ], + "problemMatcher": "$msCompile", + "group": "build" + }, + { + // r# inspect - dotnet jb inspectcode -o="inspectcode.log" --no-build -s=".\SK-dotnet.sln.DotSettings" -f=Text ".\SK-dotnet.sln" + "label": "R# check", + "command": "dotnet", + "type": "process", + "group": "test", + "args": [ + "jb", + "inspectcode", + "-o=inspectcode.log", + "--no-build", + "-s=SK-dotnet.sln.DotSettings", + "-f=Text", + "SK-dotnet.sln" + ], + "options": { + "cwd": "${workspaceFolder}/dotnet" + } + }, + { + // r# cleanup - dotnet jb cleanupcode --no-build -p="Built-in: Reformat Code" -s=SK-dotnet.sln.DotSettings SK-dotnet.sln + "label": "R# cleanup", + "command": "dotnet", + "type": "process", + "group": "test", + "args": [ + "jb", + "cleanupcode", + "--no-build", + "-s=SK-dotnet.sln.DotSettings", + "SK-dotnet.sln" + ], + "options": { + "cwd": "${workspaceFolder}/dotnet" + } + }, + { + "label": "PR - Validate", + "detail": "Runs tasks to validate changes before checking in.", + "group": "test", + "dependsOn": [ + "Build - Semantic-Kernel", + "Test - Semantic-Kernel", + "Run - Kernel-Demo", + "R# check" + ], + "dependsOrder": "sequence" + }, + { + "label": "Build - Semantic-Kernel", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/dotnet/SK-dotnet.sln", + "--configuration", + "Release" + ], + "problemMatcher": "$msCompile", + "group": "build", + "presentation": { + "reveal": "always", + "panel": "shared", + "group": "PR-Validate" + } + }, + { + "label": "Test - Semantic-Kernel", + "command": "dotnet", + "type": "process", + "args": [ + "test", + "${workspaceFolder}/dotnet/src/SemanticKernel.Test/SemanticKernel.Test.csproj" + ], + "problemMatcher": "$msCompile", + "group": "test", + "presentation": { + "reveal": "always", + "panel": "shared", + "group": "PR-Validate" + } + }, + { + "label": "Run - Kernel-Demo", + "command": "dotnet", + "type": "process", + "args": [ + "run", + "--project", + "${workspaceFolder}/samples/dotnet-kernel-syntax-examples/KernelSyntaxExamples.csproj" + ], + "problemMatcher": "$msCompile", + "group": "test", + "presentation": { + "reveal": "always", + "panel": "shared", + "group": "PR-Validate" + } + }, + { + "label": "Run - Starter API Azure Function", + "command": "func", + "type": "shell", + "args": ["start", "--csharp"], + "group": "test", + "options": { + "cwd": "${workspaceFolder}/samples/starter-api-azure-function" + }, + "presentation": { + "reveal": "always", + "panel": "shared", + "group": "Run-Samples" + } + }, + { + "label": "Run - Starter Chat WebApp React", + "command": "yarn", + "type": "shell", + "args": ["start"], + "group": "test", + "options": { + "cwd": "${workspaceFolder}/samples/starter-chat-webapp-react" + }, + "presentation": { + "reveal": "always", + "panel": "shared", + "group": "Run-Samples" + } + } + ] +} diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 000000000000..f9ba8cf65f3e --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,9 @@ +# Microsoft Open Source Code of Conduct + +This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). + +Resources: + +- [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) +- [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) +- Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000000..891f55f01379 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,97 @@ +# Contributing to Semantic Kernel + +You can contribute to Semantic Kernel with issues and pull requests (PRs). Simply filing issues +for problems you encounter is a great way to contribute. Contributing code is greatly appreciated. + +## Reporting Issues + +We always welcome bug reports, API proposals and overall feedback. Here are a few tips on how +you can make reporting your issue as effective as possible. + +### Where to Report + +New issues can be reported in our [list of issues](https://github.com/semantic-kernel/issues). + +Before filing a new issue, please search the list of issues to make sure it does not already exist. + +If you do find an existing issue for what you wanted to report, please include your own feedback +in the discussion. Do consider upvoting (👍 reaction) the original post, as this helps us +prioritize popular issues in our backlog. + +### Writing a Good Bug Report + +Good bug reports make it easier for maintainers to verify and root cause the underlying problem. +The better a bug report, the faster the problem will be resolved. Ideally, a bug report should +contain the following information: + +- A high-level description of the problem. +- A _minimal reproduction_, i.e. the smallest size of code/configuration required to reproduce the wrong behavior. +- A description of the _expected behavior_, contrasted with the _actual behavior_ observed. +- Information on the environment: OS/distribution, CPU architecture, SDK version, etc. +- Additional information, e.g. Is it a regression from previous versions? Are there any known workarounds? + +## Contributing Changes + +Project maintainers will merge accepted code changes from contributors. + +### DOs and DON'Ts + +DO's: + +- **DO** follow the standard .NET [coding style](https://learn.microsoft.com/dotnet/csharp/fundamentals/coding-style/coding-conventions) +- **DO** give priority to the current style of the project or file you're changing if it diverges from the general guidelines. +- **DO** include tests when adding new features. When fixing bugs, start with + adding a test that highlights how the current behavior is broken. +- **DO** keep the discussions focused. When a new or related topic comes up + it's often better to create new issue than to side track the discussion. +- **DO** clearly state on an issue that you are going to take on implementing it. +- **DO** blog and tweet (or whatever) about your contributions, frequently! + +DON'Ts: + +- **DON'T** surprise us with big pull requests. Instead, file an issue and start + a discussion so we can agree on a direction before you invest a large amount of time. +- **DON'T** commit code that you didn't write. If you find code that you think is a good + fit to add to Semantic Kernel, file an issue and start a discussion before proceeding. +- **DON'T** submit PRs that alter licensing related files or headers. If you believe there's + a problem with them, file an issue and we'll be happy to discuss it. +- **DON'T** make new APIs without filing an issue and discussing with us first. + +### Breaking Changes + +Contributions must maintain API signature and behavioral compatibility. Contributions that include +breaking changes will be rejected. Please file an issue to discuss your idea or change if you +believe that a breaking change is warranted. + +### Suggested Workflow + +We use and recommend the following workflow: + +1. Create an issue for your work. + - You can skip this step for trivial changes. + - Reuse an existing issue on the topic, if there is one. + - Get agreement from the team and the community that your proposed change is a good one. + - Clearly state that you are going to take on implementing it, if that's the case. You can + request that the issue be assigned to you. Note: The issue filer and the implementer + don't have to be the same person. +2. Create a personal fork of the repository on GitHub (if you don't already have one). +3. In your fork, create a branch off of main (`git checkout -b mybranch`). + - Name the branch so that it clearly communicates your intentions, such as issue-123 or githubhandle-issue. +4. Make and commit your changes to your branch. +5. Add new tests corresponding to your change, if applicable. +6. Build the repository with your changes. + - Make sure that the builds are clean. + - Make sure that the tests are all passing, including your new tests. +7. Create a PR against the repository's **main** branch. + - State in the description what issue or improvement your change is addressing. + - Verify that all the Continuous Integration checks are passing. +8. Wait for feedback or approval of your changes from the code maintainers. +9. When area owners have signed off, and all checks are green, your PR will be merged. + +### PR - CI Process + +The continuous integration (CI) system will automatically perform the required builds and +run tests (including the ones you are expected to run) for PRs. Builds and test runs must be clean. + +If the CI build fails for any reason, the PR issue will be updated with a link that +can be used to determine the cause of the failure. diff --git a/GLOSSARY.md b/GLOSSARY.md new file mode 100644 index 000000000000..f984bf44d6c2 --- /dev/null +++ b/GLOSSARY.md @@ -0,0 +1,32 @@ +# Glossary ✍ + +To wrap your mind around the concepts we present throughout the kernel, here is a glossary of +commonly used terms + +**Semantic Kernel (SK)** - The orchestrator that fulfills a user's ASK with SK's available SKILLS. + +**Ask**- What a user requests to the Semantic Kernel to help achieve the user's goal. + +- "We make ASKs to the SK" + +**Skill** - A domain-specific collection made available to the SK as a group of finely-tuned functions. + +- "We have a SKILL for using Office better" + +**Function** - A computational machine comprised of Semantic AI and/or native code that's available in a SKILL. + +- "The Office SKILL has many FUNCTIONs" + +**Native Function** - expressed in the conventions of the computing language (Python, C#, Typescript) +and easily integrates with SK + +**Semantic Function** - expressed in natural language in a text file "skprompt.txt" using SK's Prompt +Template. Each semantic function is defined by a unique prompt template file, developed using modern +**prompt engineering** techniques. + +The kernel is designed to encourage **function composition**, allowing users to combine multiple functions +(native and semantic) into a single pipeline. + +

+image +

diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000000..9e841e7a26e4 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ + MIT License + + Copyright (c) Microsoft Corporation. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE diff --git a/PROMPT_TEMPLATE_LANGUAGE.md b/PROMPT_TEMPLATE_LANGUAGE.md new file mode 100644 index 000000000000..b69c75ac6fde --- /dev/null +++ b/PROMPT_TEMPLATE_LANGUAGE.md @@ -0,0 +1,121 @@ +# SK Prompt Template Syntax + +The Semantic Kernel prompt template language is a simple and powerful way to +define and compose AI functions **using plain text**. +You can use it to create natural language prompts, generate responses, extract +information, **invoke other prompts** or perform any other task that can be +expressed with text. + +The language supports three basic features that allow you to (**#1**) include +variables, (**#2**) call external functions, and (**#3**) pass parameters to functions. + +You don't need to write any code or import any external libraries, just use the +curly braces `{{...}}` to embed expressions in your prompts. +Semantic Kernel will parse your template and execute the logic behind it. +This way, you can easily integrate AI into your apps with minimal effort and +maximum flexibility. + +## Variables + +To include a variable value in your text, use the `{{$variableName}}` syntax. +For example, if you have a variable called `name` that holds the user's name, +you can write: + + Hello {{$name}}, welcome to Semantic Kernel! + +This will produce a greeting with the user's name. + +## Function calls + +To call an external function and embed the result in your text, use the +`{{namespace.functionName}}` syntax. +For example, if you have a function called `weather.getForecast` that returns +the weather forecast for a given location, you can write: + + The weather today is {{weather.getForecast}}. + +This will produce a sentence with the weather forecast for the default location +stored in the `input` variable. +The `input` variable is set automatically by the kernel when invoking a function. +For instance, the code above is equivalent to: + + The weather today is {{weather.getForecast $input}}. + +## Function parameters + +To call an external function and pass a parameter to it, use the +`{namespace.functionName $varName}` syntax. +For example, if you want to pass a different input to the weather forecast +function, you can write: + + The weather today in {{$city}} is {weather.getForecast $city}. + The weather today in {{$region}} is {weather.getForecast $region}. + +This will produce two sentences with the weather forecast for two different +locations, using the city stored in the `city` variable and the region name +stored in the `region` variable. + +## Design Principles + +The template language uses of the `$` symbol on purpose, to clearly distinguish +between variables, which are retrieved from local temporary memory, and +functions that retrieve content executing some code. + +Branching features such as "if", "for", and code blocks are not part of SK's +template language. This reflects SK's design principle of using natural language +as much as possible, with a clear separation from conventional programming code. + +By using a simple language, the kernel can also avoid complex parsing and +external dependencies, resulting in a fast and memory efficient processing. + +## Semantic function example + +Here's a very simple example of a semantic function defined with a prompt +template, using the syntax described. + +`== File: skprompt.txt ==` + +``` +My name: {{msgraph.GetMyName}} +My email: {{msgraph.GetMyEmailAddress}} +Recipient: {{$recipient}} +Email to reply to: +========= +{{$sourceEmail}} +========= +Generate a response to the email, to say: {{$input}} + +Include the original email quoted after the response. +``` + +If we were to write that function in C#, it would look something like: + +```csharp +async Task GenResponseToEmailAsync( + string whatToSay, + string recipient, + string sourceEmail) +{ + try { + string name = await this._msgraph.GetMyName(); + } catch { + ... + } + + try { + string email = await this._msgraph.GetMyEmailAddress(); + } catch { + ... + } + + try { + // Use AI to generate an email using the 5 given variables + // Take care of retry logic, tracking AI costs, etc. + string response = await ... + + return response; + } catch { + ... + } +} +``` diff --git a/README.md b/README.md new file mode 100644 index 000000000000..3f27823172a8 --- /dev/null +++ b/README.md @@ -0,0 +1,113 @@ +# Semantic Kernel + +[![dotnet](https://github.com/microsoft/semantic-kernel/actions/workflows/dotnet-ci.yml/badge.svg?branch=main)](https://github.com/microsoft/semantic-kernel/actions/workflows/dotnet-ci.yml) + +This repository is where we (Microsoft) develop the Semantic Kernel product together with you, the community. + +## About Semantic Kernel + +Semantic Kernel is a lightweight SDK that offers a simple, powerful, and consistent programming model mixing +traditional code with AI and "semantic" functions, covering multiple programming languages and platforms. + +Semantic Kernel is designed to support and encapsulate several design patterns from the latest in AI research +such that developers can infuse their applications with complex skills like prompt chaining, recursive reasoning, +summarization, zero/few-shot learning, contextual memory, long-term memory, embeddings, semantic indexing, +planning, and accessing external knowledge stores as well as your own data. + +## Getting Started ⚡ + +To get started with Semantic Kernel, you can clone the semantic-kernel repository: + +```shell +git clone https://github.com/microsoft/semantic-kernel.git +``` + +And then run the [getting started notebook](samples/notebooks/dotnet/Getting-Started-Notebook.ipynb) +or try the samples: + +- [Simple chat summary](samples/apps/chat-summary-webapp-react/README.md) (**Recommended**) +- [Book creator](samples/apps/book-creator-webapp-react/README.md) +- [Authentication and APIs](samples/apps/auth-api-webapp-react/README.md) + +Alternatively, you can also can create a C# app that uses the SDK (see next section). + +**IMPORTANT** - You will need an [Open AI Key](https://openai.com/api/) or +[Azure Open AI Service key](https://learn.microsoft.com/azure/cognitive-services/openai/quickstart?pivots=rest-api) +to get started. + +**IMPORTANT** - There are [software requirements](https://aka.ms/SK-Requirements) +you may need to satisfy for running the samples and notebooks. + +## Use Semantic Kernel in a C# Console App + +Here is a quick example of how to use Semantic Kernel from a C# app. + +Create a new project targeting .NET 3.1 or newer, and add the +`Microsoft.SemanticKernel` nuget package. + +Copy and paste the following code into your project, with your Azure OpenAI +key in hand. If you need to create one, go +[here](https://learn.microsoft.com/azure/cognitive-services/openai/quickstart?pivots=rest-api). + +```csharp +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.KernelExtensions; + +var kernel = Kernel.Builder.Build(); + +kernel.Config.AddAzureOpenAICompletionBackend( + "azure_davinci", // Internal alias + "text-davinci-003", // Azure OpenAI *Deployment ID* + "https://contoso.openai.azure.com/", // Azure OpenAI *Endpoint* + "...your Azure OpenAI Key..." // Azure OpenAI *Key* +); + +string skPrompt = @" +{{$input}} + +Give me the TLDR in 5 words. +"; + +string textToSummarize = @" +1) A robot may not injure a human being or, through inaction, +allow a human being to come to harm. + +2) A robot must obey orders given it by human beings except where +such orders would conflict with the First Law. + +3) A robot must protect its own existence as long as such protection +does not conflict with the First or Second Law. +"; + +var tldrFunction = kernel.CreateSemanticFunction(skPrompt); + +var summary = await kernel.RunAsync(textToSummarize, tldrFunction); + +Console.WriteLine(summary); + +// Output => Protect humans, follow orders, survive. +``` + +## Contributing and Community + +There are many ways in which you can participate in this project: + +- [Contribute](CONTRIBUTING.md) to the project +- Submit [Issues](https://github.com/microsoft/semantic-kernel/issues) +- Join the [Discord community](https://aka.ms/SKDiscord) +- Learn more at the [documentation site](https://aka.ms/SK-Docs) + +## Code of Conduct + +This project has adopted the +[Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). +For more information see the +[Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) +or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) +with any additional questions or comments. + +## License + +Copyright (c) Microsoft Corporation. All rights reserved. + +Licensed under the [MIT](LICENSE) license. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 000000000000..e138ec5d6a77 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,41 @@ + + +## Security + +Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). + +If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/opensource/security/definition), please report it to us as described below. + +## Reporting Security Issues + +**Please do not report security vulnerabilities through public GitHub issues.** + +Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/opensource/security/create-report). + +If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/opensource/security/pgpkey). + +You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://aka.ms/opensource/security/msrc). + +Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: + + * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) + * Full paths of source file(s) related to the manifestation of the issue + * The location of the affected source code (tag/branch/commit or direct URL) + * Any special configuration required to reproduce the issue + * Step-by-step instructions to reproduce the issue + * Proof-of-concept or exploit code (if possible) + * Impact of the issue, including how an attacker might exploit the issue + +This information will help us triage your report more quickly. + +If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/opensource/security/bounty) page for more details about our active programs. + +## Preferred Languages + +We prefer all communications to be in English. + +## Policy + +Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/opensource/security/cvd). + + diff --git a/build.cmd b/build.cmd new file mode 100644 index 000000000000..8d80cf630de1 --- /dev/null +++ b/build.cmd @@ -0,0 +1,7 @@ +@echo off + +cd dotnet + +dotnet build --configuration Release --interactive + +dotnet test --configuration Release --no-build --no-restore --interactive diff --git a/build.sh b/build.sh new file mode 100755 index 000000000000..8b7a77cf025e --- /dev/null +++ b/build.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash + +set -e + +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/" +cd "$HERE" + +cd dotnet + +# Release config triggers also "dotnet format" +dotnet build --configuration Release --interactive + +dotnet test --configuration Release --no-build --no-restore --interactive diff --git a/compliance.yml b/compliance.yml new file mode 100644 index 000000000000..83b24af02001 --- /dev/null +++ b/compliance.yml @@ -0,0 +1,79 @@ +trigger: +- main + +# no PR triggers +pr: none + +pool: + vmImage: 'windows-latest' + +steps: +- task: CredScan@2 + inputs: + toolMajorVersion: 'V2' +- task: ESLint@1 + inputs: + Configuration: 'recommended' + TargetType: 'eslint' + ErrorLevel: 'warn' + +- task: UseDotNet@2 + displayName: 'Use .NET Core SDK 3.1' + inputs: + packageType: 'sdk' + version: '3.1.x' + +# - task: Semmle@0 +# env: +# SYSTEM_ACCESSTOKEN: $(System.AccessToken) +# inputs: +# sourceCodeDirectory: '$(Build.SourcesDirectory)' +# language: 'tsandjs' +# includeNodeModules: true +# querySuite: 'Recommended' +# timeout: '1800' +# ram: '16384' +# addProjectDirToScanningExclusionList: true + +- task: Semmle@1 + env: + SYSTEM_ACCESSTOKEN: $(System.AccessToken) + inputs: + sourceCodeDirectory: '$(Build.SourcesDirectory)' + language: 'csharp' + buildCommandsString: '$(Agent.ToolsDirectory)\dotnet\dotnet.exe restore $(BUILD.SourcesDirectory)\dotnet\SK-dotnet.sln#$(Agent.ToolsDirectory)\dotnet\dotnet.exe build $(BUILD.SourcesDirectory)\dotnet\SK-dotnet.sln' + querySuite: 'Recommended' + timeout: '1800' + ram: '16384' + addProjectDirToScanningExclusionList: true + +# Both versions of Semmle (0 and 1) aren't required +# Either one is fine. The latest version will show up as +# Semmle@1 while older version will be listed as Semmle@0 +# By default, version 1 (@1) will be added to pipeline +# This sample contains both, just for sample purposes + +# Usage of System.AccessToken is only required for uploading +# results to CodeQL servers via variable LGTM.UploadSnapshot = true +# If you want to analyze errors on your own or if LGTM.UploadSnapshot = false, +# then passing this environment variable is not required. + +####################################################### +# Highly Discouraged, only for backward compatibility # +####################################################### +# When code is hosted on GitHub and build pipeline is created on ADO then a PAT token can be used in place of AccessToken as part of environment variable +# SYSTEM_ACCESSTOKEN: $(PATToken) +# Where PATToken is name of the variable and value contains the actual PAT token generated by user + +- task: ComponentGovernanceComponentDetection@0 + inputs: + scanType: 'Register' + verbosity: 'Verbose' + alertWarningLevel: 'High' + +- task: PublishSecurityAnalysisLogs@2 + inputs: + ArtifactName: 'CodeAnalysisLogs' + ArtifactType: 'Container' + AllTools: true + ToolLogsNotFoundAction: 'Standard' diff --git a/dotnet/Directory.Build.props b/dotnet/Directory.Build.props new file mode 100644 index 000000000000..a4884ff7b957 --- /dev/null +++ b/dotnet/Directory.Build.props @@ -0,0 +1,18 @@ + + + + true + true + AllEnabledByDefault + latest + true + 10 + enable + disable + + + + + disable + + \ No newline at end of file diff --git a/dotnet/Directory.Build.targets b/dotnet/Directory.Build.targets new file mode 100644 index 000000000000..b1596003eed4 --- /dev/null +++ b/dotnet/Directory.Build.targets @@ -0,0 +1,14 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props new file mode 100644 index 000000000000..b516aabdd814 --- /dev/null +++ b/dotnet/Directory.Packages.props @@ -0,0 +1,33 @@ + + + + + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/dotnet/DocFX/.gitignore b/dotnet/DocFX/.gitignore new file mode 100644 index 000000000000..89547ba39b26 --- /dev/null +++ b/dotnet/DocFX/.gitignore @@ -0,0 +1 @@ +log.txt \ No newline at end of file diff --git a/dotnet/DocFX/DocFX.csproj b/dotnet/DocFX/DocFX.csproj new file mode 100644 index 000000000000..10ed536a0dad --- /dev/null +++ b/dotnet/DocFX/DocFX.csproj @@ -0,0 +1,17 @@ + + + + Microsoft.SemanticKernel.DocFX + Microsoft.SemanticKernel.DocFX + netstandard2.1 + false + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + diff --git a/dotnet/DocFX/README.md b/dotnet/DocFX/README.md new file mode 100644 index 000000000000..5e767dacc152 --- /dev/null +++ b/dotnet/DocFX/README.md @@ -0,0 +1,29 @@ +# DocFX + +DocFX converts .NET assembly, XML code comment, REST API Swagger files and markdown into rendered +HTML pages, JSON model or PDF files. + +## Links + +- [docfx](https://dotnet.github.io/docfx/index.html) + +## Setup + +- Install [.NET 6 SDK](https://dotnet.microsoft.com/en-us/download) or higher. + +2. Install DocFX + +```bash +dotnet tool install -g docfx +``` + +## Running locally + +To preview the documentation website on your local machine, run `docfx` and point it to the +`docfx.json` file in the `dotnet\DocFX` folder. + +From the root of the repo, run: + +```bash +docfx dotnet\DocFX\docfx.json --serve +``` \ No newline at end of file diff --git a/dotnet/DocFX/api/.gitignore b/dotnet/DocFX/api/.gitignore new file mode 100644 index 000000000000..e8079a3bef9d --- /dev/null +++ b/dotnet/DocFX/api/.gitignore @@ -0,0 +1,5 @@ +############### +# temp file # +############### +*.yml +.manifest diff --git a/dotnet/DocFX/api/index.md b/dotnet/DocFX/api/index.md new file mode 100644 index 000000000000..390bdff56bee --- /dev/null +++ b/dotnet/DocFX/api/index.md @@ -0,0 +1 @@ +# PLACEHOLDER diff --git a/dotnet/DocFX/articles/intro.md b/dotnet/DocFX/articles/intro.md new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/dotnet/DocFX/articles/toc.yml b/dotnet/DocFX/articles/toc.yml new file mode 100644 index 000000000000..ff89ef1fe094 --- /dev/null +++ b/dotnet/DocFX/articles/toc.yml @@ -0,0 +1,2 @@ +- name: Introduction + href: intro.md diff --git a/dotnet/DocFX/docfx.json b/dotnet/DocFX/docfx.json new file mode 100644 index 000000000000..e3b6a418d301 --- /dev/null +++ b/dotnet/DocFX/docfx.json @@ -0,0 +1,66 @@ +{ + "metadata": [ + { + "src": [ + { + "files": [ + "**.csproj" + ], + "exclude": ["**/bin/**", "**/obj/**", "**/*.Test*", "**/*.IntegrationTest*"], + "src": ".." + } + ], + "dest": "api", + "disableGitFeatures": false, + "disableDefaultFilter": false + } + ], + "build": { + "content": [ + { + "files": [ + "api/**.yml", + "api/index.md" + ] + }, + { + "files": [ + "articles/**.md", + "articles/**/toc.yml", + "toc.yml", + "*.md" + ] + } + ], + "resource": [ + { + "files": [ + "images/**" + ] + } + ], + "overwrite": [ + { + "files": [ + "apidoc/**.md" + ], + "exclude": [ + "obj/**", + "_site/**" + ] + } + ], + "dest": "_site", + "globalMetadataFiles": [], + "fileMetadataFiles": [], + "template": [ + "default" + ], + "postProcessors": [], + "markdownEngineName": "markdig", + "noLangKeyword": false, + "keepFileLink": false, + "cleanupCacheHistory": false, + "disableGitFeatures": false + } +} \ No newline at end of file diff --git a/dotnet/DocFX/index.md b/dotnet/DocFX/index.md new file mode 100644 index 000000000000..7ba7c05aa3ac --- /dev/null +++ b/dotnet/DocFX/index.md @@ -0,0 +1,11 @@ +# Semantic Kernel + +[Semantic Kernel](https://github.com/microsoft/semantic-kernel) +(aka "SK") is a lightweight SDK that offers a simple, powerful, and consistent programming +model mixing classical code with AI and "semantic" functions, covering multiple programming +languages and platforms. + +SK is designed to support and encapsulate several design patterns from the latest in AI research such +that developers can infuse their applications with complex skills like prompt chaining, recursive reasoning, +summarization, zero/few-shot learning, contextual memory, long-term memory, embeddings, semantic indexing, +planning, and accessing external knowledge stores as well as your own data. \ No newline at end of file diff --git a/dotnet/DocFX/toc.yml b/dotnet/DocFX/toc.yml new file mode 100644 index 000000000000..55d7e9646860 --- /dev/null +++ b/dotnet/DocFX/toc.yml @@ -0,0 +1,5 @@ +#- name: Articles +# href: articles/ +- name: Api Documentation + href: api/ + homepage: api/index.md diff --git a/dotnet/SK-dotnet.sln b/dotnet/SK-dotnet.sln new file mode 100644 index 000000000000..e4a868b28b6d --- /dev/null +++ b/dotnet/SK-dotnet.sln @@ -0,0 +1,154 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.4.33213.308 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SemanticKernel", "src\SemanticKernel\SemanticKernel.csproj", "{A284C7EB-2248-4A75-B112-F5DCDE65410D}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{831DDCA2-7D2C-4C31-80DB-6BDB3E1F7AE0}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{FA3720F1-C99A-49B2-9577-A940257098BF}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LoadPromptsFromCloud", "..\samples\dotnet\kernel-extension-load-prompts-from-cloud\LoadPromptsFromCloud.csproj", "{A05BF65E-085E-476C-B88A-9DA93F005416}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "KernelSyntaxExamples", "..\samples\dotnet\kernel-syntax-examples\KernelSyntaxExamples.csproj", "{47C6F821-5103-431F-B3B8-A2868A68BB78}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MsGraphSkillsExample", "..\samples\dotnet\graph-api-skills\MsGraphSkillsExample.csproj", "{3EB61E99-C39B-4620-9482-F8DA18E48525}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SemanticKernelFunction", "..\samples\dotnet\api-azure-function\SemanticKernelFunction.csproj", "{34A7F1EF-D243-4160-A413-D713FEABCD94}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IntegrationTests", "src\IntegrationTest\IntegrationTests.csproj", "{E4B777A1-28E1-41BE-96AE-7F3EC61FD5D4}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connectors.OpenXml", "src\SemanticKernel.Connectors\Connectors.OpenXml\Connectors.OpenXml.csproj", "{F94D1938-9DB7-4B24-9FF3-166DDFD96330}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connectors.MsGraph", "src\SemanticKernel.Connectors\Connectors.MsGraph\Connectors.MsGraph.csproj", "{689A5041-BAE7-448F-9BDC-4672E96249AA}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connectors.Bing", "src\SemanticKernel.Connectors\Connectors.Bing\Connectors.Bing.csproj", "{EEA87FBC-4ED5-458C-ABD3-BEAEEB535BAF}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{158A4E5E-AEE0-4D60-83C7-8E089B2D881D}" + ProjectSection(SolutionItems) = preProject + ..\.editorconfig = ..\.editorconfig + ..\.gitignore = ..\.gitignore + Directory.Build.props = Directory.Build.props + Directory.Build.targets = Directory.Build.targets + Directory.Packages.props = Directory.Packages.props + ..\README.md = ..\README.md + EndProjectSection +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SemanticKernel.Test", "src\SemanticKernel.Test\SemanticKernel.Test.csproj", "{37E39C68-5A40-4E63-9D3C-0C66AD98DFCB}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connectors.Sqlite", "src\SemanticKernel.Connectors\Connectors.Sqlite\Connectors.Sqlite.csproj", "{E23E7270-F13D-4620-A115-AA6A8619EE5A}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connectors.Sqlite.Test", "src\SemanticKernel.Connectors\Connectors.Sqlite.Test\Connectors.Sqlite.Test.csproj", "{70C4F01B-9C83-4A62-B731-66A55E1BAC6B}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "skills", "skills", "{9ECD1AA0-75B3-4E25-B0B5-9F0945B64974}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "connectors", "connectors", "{C0F74221-46C8-4ECD-BB0E-67303088C2E6}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SemanticKernel.Skills", "src\SemanticKernel.Skills\Skills\SemanticKernel.Skills.csproj", "{F261D87A-472C-46F7-BC3C-0CF253653755}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SemanticKernel.Skills.Test", "src\SemanticKernel.Skills\Skills.Test\SemanticKernel.Skills.Test.csproj", "{107156B4-5A8B-45C7-97A2-4544D7FA19DE}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "nuget", "nuget", "{F4243136-252A-4459-A7C4-EE8C056D6B0B}" + ProjectSection(SolutionItems) = preProject + nuget\icon.png = nuget\icon.png + nuget\nuget-package.props = nuget\nuget-package.props + nuget\NUGET.md = nuget\NUGET.md + EndProjectSection +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KernelBuilder", "..\samples\dotnet\KernelBuilder\KernelBuilder.csproj", "{A52818AC-57FB-495F-818F-9E1E7BC5618C}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {A284C7EB-2248-4A75-B112-F5DCDE65410D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A284C7EB-2248-4A75-B112-F5DCDE65410D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A284C7EB-2248-4A75-B112-F5DCDE65410D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A284C7EB-2248-4A75-B112-F5DCDE65410D}.Release|Any CPU.Build.0 = Release|Any CPU + {A05BF65E-085E-476C-B88A-9DA93F005416}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A05BF65E-085E-476C-B88A-9DA93F005416}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A05BF65E-085E-476C-B88A-9DA93F005416}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A05BF65E-085E-476C-B88A-9DA93F005416}.Release|Any CPU.Build.0 = Release|Any CPU + {47C6F821-5103-431F-B3B8-A2868A68BB78}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {47C6F821-5103-431F-B3B8-A2868A68BB78}.Debug|Any CPU.Build.0 = Debug|Any CPU + {47C6F821-5103-431F-B3B8-A2868A68BB78}.Release|Any CPU.ActiveCfg = Release|Any CPU + {47C6F821-5103-431F-B3B8-A2868A68BB78}.Release|Any CPU.Build.0 = Release|Any CPU + {3EB61E99-C39B-4620-9482-F8DA18E48525}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3EB61E99-C39B-4620-9482-F8DA18E48525}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3EB61E99-C39B-4620-9482-F8DA18E48525}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3EB61E99-C39B-4620-9482-F8DA18E48525}.Release|Any CPU.Build.0 = Release|Any CPU + {34A7F1EF-D243-4160-A413-D713FEABCD94}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {34A7F1EF-D243-4160-A413-D713FEABCD94}.Debug|Any CPU.Build.0 = Debug|Any CPU + {34A7F1EF-D243-4160-A413-D713FEABCD94}.Release|Any CPU.ActiveCfg = Release|Any CPU + {34A7F1EF-D243-4160-A413-D713FEABCD94}.Release|Any CPU.Build.0 = Release|Any CPU + {E4B777A1-28E1-41BE-96AE-7F3EC61FD5D4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E4B777A1-28E1-41BE-96AE-7F3EC61FD5D4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E4B777A1-28E1-41BE-96AE-7F3EC61FD5D4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E4B777A1-28E1-41BE-96AE-7F3EC61FD5D4}.Release|Any CPU.Build.0 = Release|Any CPU + {F94D1938-9DB7-4B24-9FF3-166DDFD96330}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F94D1938-9DB7-4B24-9FF3-166DDFD96330}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F94D1938-9DB7-4B24-9FF3-166DDFD96330}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F94D1938-9DB7-4B24-9FF3-166DDFD96330}.Release|Any CPU.Build.0 = Release|Any CPU + {689A5041-BAE7-448F-9BDC-4672E96249AA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {689A5041-BAE7-448F-9BDC-4672E96249AA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {689A5041-BAE7-448F-9BDC-4672E96249AA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {689A5041-BAE7-448F-9BDC-4672E96249AA}.Release|Any CPU.Build.0 = Release|Any CPU + {EEA87FBC-4ED5-458C-ABD3-BEAEEB535BAF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EEA87FBC-4ED5-458C-ABD3-BEAEEB535BAF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EEA87FBC-4ED5-458C-ABD3-BEAEEB535BAF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EEA87FBC-4ED5-458C-ABD3-BEAEEB535BAF}.Release|Any CPU.Build.0 = Release|Any CPU + {37E39C68-5A40-4E63-9D3C-0C66AD98DFCB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {37E39C68-5A40-4E63-9D3C-0C66AD98DFCB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {37E39C68-5A40-4E63-9D3C-0C66AD98DFCB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {37E39C68-5A40-4E63-9D3C-0C66AD98DFCB}.Release|Any CPU.Build.0 = Release|Any CPU + {E23E7270-F13D-4620-A115-AA6A8619EE5A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E23E7270-F13D-4620-A115-AA6A8619EE5A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E23E7270-F13D-4620-A115-AA6A8619EE5A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E23E7270-F13D-4620-A115-AA6A8619EE5A}.Release|Any CPU.Build.0 = Release|Any CPU + {70C4F01B-9C83-4A62-B731-66A55E1BAC6B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {70C4F01B-9C83-4A62-B731-66A55E1BAC6B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {70C4F01B-9C83-4A62-B731-66A55E1BAC6B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {70C4F01B-9C83-4A62-B731-66A55E1BAC6B}.Release|Any CPU.Build.0 = Release|Any CPU + {F261D87A-472C-46F7-BC3C-0CF253653755}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F261D87A-472C-46F7-BC3C-0CF253653755}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F261D87A-472C-46F7-BC3C-0CF253653755}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F261D87A-472C-46F7-BC3C-0CF253653755}.Release|Any CPU.Build.0 = Release|Any CPU + {107156B4-5A8B-45C7-97A2-4544D7FA19DE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {107156B4-5A8B-45C7-97A2-4544D7FA19DE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {107156B4-5A8B-45C7-97A2-4544D7FA19DE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {107156B4-5A8B-45C7-97A2-4544D7FA19DE}.Release|Any CPU.Build.0 = Release|Any CPU + {A52818AC-57FB-495F-818F-9E1E7BC5618C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A52818AC-57FB-495F-818F-9E1E7BC5618C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A52818AC-57FB-495F-818F-9E1E7BC5618C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A52818AC-57FB-495F-818F-9E1E7BC5618C}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {A284C7EB-2248-4A75-B112-F5DCDE65410D} = {831DDCA2-7D2C-4C31-80DB-6BDB3E1F7AE0} + {A05BF65E-085E-476C-B88A-9DA93F005416} = {FA3720F1-C99A-49B2-9577-A940257098BF} + {47C6F821-5103-431F-B3B8-A2868A68BB78} = {FA3720F1-C99A-49B2-9577-A940257098BF} + {3EB61E99-C39B-4620-9482-F8DA18E48525} = {FA3720F1-C99A-49B2-9577-A940257098BF} + {34A7F1EF-D243-4160-A413-D713FEABCD94} = {FA3720F1-C99A-49B2-9577-A940257098BF} + {E4B777A1-28E1-41BE-96AE-7F3EC61FD5D4} = {831DDCA2-7D2C-4C31-80DB-6BDB3E1F7AE0} + {F94D1938-9DB7-4B24-9FF3-166DDFD96330} = {C0F74221-46C8-4ECD-BB0E-67303088C2E6} + {689A5041-BAE7-448F-9BDC-4672E96249AA} = {C0F74221-46C8-4ECD-BB0E-67303088C2E6} + {EEA87FBC-4ED5-458C-ABD3-BEAEEB535BAF} = {C0F74221-46C8-4ECD-BB0E-67303088C2E6} + {37E39C68-5A40-4E63-9D3C-0C66AD98DFCB} = {831DDCA2-7D2C-4C31-80DB-6BDB3E1F7AE0} + {E23E7270-F13D-4620-A115-AA6A8619EE5A} = {C0F74221-46C8-4ECD-BB0E-67303088C2E6} + {70C4F01B-9C83-4A62-B731-66A55E1BAC6B} = {C0F74221-46C8-4ECD-BB0E-67303088C2E6} + {9ECD1AA0-75B3-4E25-B0B5-9F0945B64974} = {831DDCA2-7D2C-4C31-80DB-6BDB3E1F7AE0} + {C0F74221-46C8-4ECD-BB0E-67303088C2E6} = {831DDCA2-7D2C-4C31-80DB-6BDB3E1F7AE0} + {F261D87A-472C-46F7-BC3C-0CF253653755} = {9ECD1AA0-75B3-4E25-B0B5-9F0945B64974} + {107156B4-5A8B-45C7-97A2-4544D7FA19DE} = {9ECD1AA0-75B3-4E25-B0B5-9F0945B64974} + {F4243136-252A-4459-A7C4-EE8C056D6B0B} = {158A4E5E-AEE0-4D60-83C7-8E089B2D881D} + {A52818AC-57FB-495F-818F-9E1E7BC5618C} = {FA3720F1-C99A-49B2-9577-A940257098BF} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {FBDC56A3-86AD-4323-AA0F-201E59123B83} + EndGlobalSection +EndGlobal diff --git a/dotnet/SK-dotnet.sln.DotSettings b/dotnet/SK-dotnet.sln.DotSettings new file mode 100644 index 000000000000..cdda35171d85 --- /dev/null +++ b/dotnet/SK-dotnet.sln.DotSettings @@ -0,0 +1,188 @@ + + True + True + FullFormat + True + True + True + True + True + SOLUTION + False + SUGGESTION + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + SUGGESTION + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + True + Field, Property, Event, Method + True + True + True + NEXT_LINE + True + True + True + True + True + True + 1 + 1 + True + True + True + ALWAYS + True + True + False + 160 + True + Copyright (c) Microsoft. All rights reserved. + AI + AIGPT + AMQP + API + CORS + HMAC + HTTP + IOS + JSON + JWT + MQTT + MS + MSAL + OID + OS + SK + SKHTTP + SSL + TTL + UI + UID + URL + YAML + False + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AA_BB"><ExtraRule Prefix="" Suffix="" Style="aaBb" /></Policy> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="" Suffix="Async" Style="AaBb" /></Policy> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb"><ExtraRule Prefix="" Suffix="Async" Style="aaBb" /></Policy> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="" Suffix="Async" Style="AaBb" /></Policy> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb"><ExtraRule Prefix="" Suffix="Async" Style="aaBb" /></Policy> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="_" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="s_" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="" Suffix="Async" Style="AaBb" /></Policy> + + 2 + False + True + True + True + True + True + True + True + True + True + True + True + False + TRACE + 8201 + x64 + True + True + False + True + guid() + 0 + True + True + False + False + True + 2.0 + InCSharpFile + aaa + True + [Fact] +public void It$SOMENAME$() +{ + // Arrange + + // Act + + // Assert + +} + True + True + MSFT copyright + True + 2.0 + InCSharpFile + copy + // Copyright (c) Microsoft. All rights reserved. + + True + True + True + True + True + True + True + True + True + DO_NOT_SHOW + True + True + True + \ No newline at end of file diff --git a/dotnet/common.props b/dotnet/common.props new file mode 100644 index 000000000000..d4266c12909d --- /dev/null +++ b/dotnet/common.props @@ -0,0 +1,23 @@ + + + true + true + AllEnabledByDefault + latest + true + 10 + enable + disable + + + + + disable + + + + + + + \ No newline at end of file diff --git a/dotnet/nuget/NUGET.md b/dotnet/nuget/NUGET.md new file mode 100644 index 000000000000..542c2d8246db --- /dev/null +++ b/dotnet/nuget/NUGET.md @@ -0,0 +1,17 @@ +## About Semantic Kernel + +Semantic Kernel is a lightweight SDK that offers a simple, powerful, and consistent programming +model mixing classical code with AI and "semantic" functions, covering multiple programming +languages and platforms. + +Semantic Kernel is designed to support and encapsulate several design patterns from the latest +in AI research such that developers can infuse their applications with complex skills like prompt +chaining, recursive reasoning, summarization, zero/few-shot learning, contextual memory, +long-term memory, embeddings, semantic indexing, planning, and accessing external knowledge +stores as well as your own data. + +## Getting Started ⚡ + +- Check out the [GitHub repository](https://github.com/microsoft/semantic-kernel) for the latest updates +- Join the [Discord community](https://aka.ms/SKDiscord) +- Learn more at the [documentation site](https://aka.ms/SK-Docs) diff --git a/dotnet/nuget/icon.png b/dotnet/nuget/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..3862f148d4c56a92867623dda958bed1a15620ac GIT binary patch literal 15525 zcmeHM30zah)}KI-RS`(AC~Xi>5Qm*qJ z{6F@||I0^BZSFA!>aa}*-n+vBjCE0{@N(EcUSZm>tPx zh9oAkILx?^L=Gzf;-I!ubu_M^!1&?VPV%dI?XLXMx~i=$0K0a*uD^~kG>YUnDr1a) zM>Z*SRwAH8K#7170VM*j5wN$lb8)qu?`r2_0xM4T@C*K|4S>bK1K5C%0UJcZ!UQ28 z5q?;J1M6|4cN};p4^Rlcc3TvOv%!^4k58N%63z}~%?)G6(NjV;(Cy~h(!l~GWkX0< z42xqD%8HDRceCg_%d;?v4tKK%a$0V?e1iuoDtc*Z0?R*jML<|;OxXNz3&ef0)`Aq* zl(-FXEKZ0?N?dGwqHBtq>FDgPur6<=o4PMZ2#;`Gxy0*_5%A8<^bc8*lauErJIrM# zMAGf%&!11XwWr(L+rSYviJRg%At^TTiDrLHu!NNumJq#x6U~k{kxv*B%1+|AnIDWNg@%7g>hQ2vLT^Y)h#f^N zJdDnYWyP`LIf>AQc63wv|H7K?3&O)(InkV0*2~2g#d7{NJI6-LpLdN7iH~$MO|c1Q zMT8{9a!lR-Eb@K{dTg}&%L-f@Qc_b>a{>LeP_P#MnmYg2gd8Kz&j0AvP%iqJ0b8+gO%+1e+Jj=H;7_yi zUuXZgNGedqN$MJ!TH0{Hsi{DPq^hbyR+UQ)M`yr(K-N>!pJ}&fl7U}{y6HxW{g(U# z8fJ@+pQo;D5STlJCT!KzGMx6F(ezofEiC6)IXcaAcA4+$vBcAhvDAB+e?Z`>pifr^ zhru8+GAep~A}8sK(JAUhY^7L6(cTfMopm1n-L?o8T^&$b)mukJz zY@A+tP%jlSS(U6V*Ndc*42!BBS#74>B>hEx>LD8qOzpR5P!{JOIDTH!%weT~8k*3c zWoYimpVco{ZB(;=OtG#1lxD9Kd#%?)prcBH##7Y;2)NsrzWV5~(v(Rj4ujbnK;=%- zAT70&0Gjb<2(W0s2LV(L5@33HIeK-~2!57_f4Tk;id;V^=ANs2k&}}8+1HPm2~H^A zrU~hCIE=OjaDQW9vnrp-CMD@&S_N2)RSq{AdCHQl%NwAdna&gJdCnCY-9rRczWv3x zeoG?0%{3cW^TwTadwy^Sy?LQWh~{TpZ6v^#r#~aWkGBsHV9QE0m!|mOkuuBS0z85i zRE&My>s6t+Uex#pS$>R@+(BDO6|M8{3F-zl-J9LmMF3N2e)$_1Ac{(^4)A+nf!%bk zV2GzZ&Si>xlIkQnJv0&3Le%ialh=##AO;~rsmRsp&h-SSU2_brkNkuHHKoYqJjG}4 z2U60!3E-ex-hg|Km<{>+gmTW>ukg;&BtW%iSh`m}UzE|Y$t=R#z^9VMaxwYKzMW~) zoA02V1}p-6<_~Q~&F!b--fcO&-wl!pP_&Mha2Y*w4{bs7F`qRI`d$GKGkEqJeqEy|rSHSH*QoetZXlx$jeGg`*=lnK!D~yyI|*HGP;S z+JEGsGw=QK@3eut%fQULb@RQ+4qqy>kMc+I_|X3^MlbTv)U4UaQ(P2 zeCz2pkv;(+QxqjR5mo%C5lBq$6HdPSgejFt&m3JE-o|bWo zK1O=lSSfB5<*|drZLPI^XAVJ|D2NW3ry$1alYTFH2=I}#dF_ZJPf>KoF>=Jr%W zK10*AtDhE*5xtU@ooUi&A=<+K?%=%&^V}hyKvs0~SCazBQ+>_eFW+&jfX~IgWor?j zIruIC7_*W;k7-OP^S*}p^+*p~_T5X%^z)?3c4iof0~L4Lnnqrybs7QIwh>^l(@NeC zPx>9CncSMKIhF^bD1M$`qLNwKjSOi1l>o=)K=b8X7D20+_g!KRYn~-Q1AUFFFSa$7 z_dEyfGCf`mX>jeM{vg@bcmfE0ntH#h8?oZ@AzwaXZc4A)5>=FgJGh5i7Uh76iuFNJ zBmrj4sO1eCo`efr`>ch-)geG4o96#@jN@hKeg!^gd@pqIKj`v(4N z3BJ{OMh6os;8D@$*?k09QWg7Ft1~OZkjG_c7S0g7$apg7u2z&|ExIY*S(IZ5CMs_z zmzZ=X-GKnRa`{ZDb}-u2dF*#}X$!rdG3b$ZF^>lsg4en4#LC7p_h?Vs0JRFz5sG2KtK>~zaG5G=7M1c%}4iGx2UCA94QgRFANa4}_6We{{ z<4EalUjgqb{(i-|>d2atex9fi*M6cMBX6gUS1rEM_bYV3@0>LOLSlSn#=BcK6Cmvn zZiz>26y=XRfpmHB><(>rup(~*tJdI)PxFSLghI1GYjH2r06E=_X+UVaws=xLAx%)! zdwwV&fXP0(q=PHbIKtkJ4y?ce3Gn$oo=kt}#9Pds*}a1R38nZnG3~;sk;0oI!yLZ>(Q9Z!vjAiygN&5UC}Andpn^0Wnvl!IO$b zq+gw2!;EgEpmD05sqA()f?m%{P$A1YiU<>ezEG9*7K)0+2d;E5+P;NUiR3QL3|v2k zwX254c&mzMXVO2$+e!o`MLBb(Pv|e8AEA|{PV8^*-+qhLr9U~yu9t|Io#>RNR;ELH zFm#+d>h?~wTSnW*tLD7c;sPSJY^6f4cd7*cW?63yS_NP26x7iLTPhuZT={*lTle}= zMcP`mj&)bq2z@*Ekq{@tePXt7B5wiPRfl}v;&30l&F(x7j8p;$ppE76A4+dLP1lCL zvw!d4%n3Hk=tdf^+|%dd-$iSibw?_h9eGc)Y1?1EXMf0;`AxdTf#c>~4>wtq&MU9!`~v$B=A9Adm+NtuZLr`OVIWYQ9c7R4W`LRoIXq~53}N8AF?&!BDabO(4Nl3 z0%_wW48RW|Vjz^PYIk`8Lqt%F>)Ghy zn=6xXl`A;a_YFPA&+o3pQUS{q+XGB$+O@*#5B8r+Fs*cXc>RLOwr1(d;_OeZd(1H+ z`_JBMl6x?Jx1QJP@7jUut{9`$7PpT7^n@<*66Q(NR-^6qXckS7+f5T{!Kkt(9I}Qf z@^;LVXv4n6^89R9G)-tI?}PlCKA7=~s`$Xj#Ya6uNWQEVt#v+#)?3|xd@I1@G-fY_ zG!87pRV_p-;db+ey}__o#b7kpFOc`1D#Xb!nR@Xv?Uru!%%N27Rh*CTXWT)$s4PhT zYw}I2FzXzYZw~uxVBZ!^90u-+a}GD_KFw~MDyj1AL^p89sxfv>-dH`5F_Wgnst`BR+e>8JEBwW@#}=4bDnq7ngeNdUYV#Clq+{maGh}M8XQn(yRDk+} zMk2deQiD3Wi?SPgP%|ks`52R4 z;kV2+GF&Qr#^#Km3nY7R(sUhIXJ5jk0z21juK=qlXF@APCn4wwR4(7MBqj zIgoCIUM)U`Ho<61VpA^GbtC}iBKj4x06AxiUOd#p^LX+QzM$(0Bok&z^61BU&H1(g z=O}bYrjG9jzIK$Yj$Unnub(C%O=1_*a06{SjDCYiR?hE7YPiK%UTIWO4m5&pjS>qb z7D`?y_lVM8D1E2$Ju>DU`(O4>JysqdO6%BjulGT^(-Eq}eDbHu5(huXd;t$)vXVP^ z62l}e2KEtPdqs~+sO3OKMn)HIR=VxtAg|>{ZzOWdxM>lLjr%U4!VjM#B9b!!3>cU- z4<_IBCyv0xkKBeWXWq9&U8OhilI<{yN|18J#sqLoT&q5Q98GWx3xX%Vw_&ie*#PZ# z%m~dMtT^YceD1#q7urv64F%^c>;7W>g=jq!lcC!U0qLt}c=Izt^nA`fxLHacB(?%=GEV%HpaqVOdfvhl~jK3tV2@*5}KD;0L8r7~|;3nTr45kW5e zSw*N-1(U+$(PM@&C}c3;rf@$rf(+=geZ+;WJ9?}cF#AwYa;$)dJ9y9TRX@WQz34{m zr49Q%qoCN%3P6q~mI@LM83$pK!pHEq+^vfKl$q*KJ|^(~ddy + + true + true + + + + + Microsoft + Microsoft + Semantic Kernel + Empowers app owners to integrate cutting-edge LLM technology quickly and easily into their apps. + AI, Artificial Intelligence, SDK + + + MIT + © Microsoft Corporation. All rights reserved. + https://aka.ms/semantic-kernel + https://github.com/microsoft/semantic-kernel + true + + + icon.png + icon.png + NUGET.md + + + true + snupkg + + + + + + + + + + + + + + \ No newline at end of file diff --git a/dotnet/src/IntegrationTest/AI/OpenAICompletionTests.cs b/dotnet/src/IntegrationTest/AI/OpenAICompletionTests.cs new file mode 100644 index 000000000000..b7cdadcf4c32 --- /dev/null +++ b/dotnet/src/IntegrationTest/AI/OpenAICompletionTests.cs @@ -0,0 +1,134 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using System.Threading.Tasks; +using IntegrationTests.TestSettings; +using Microsoft.Extensions.Configuration; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.KernelExtensions; +using Microsoft.SemanticKernel.Orchestration; +using Xunit; +using Xunit.Abstractions; + +namespace IntegrationTests.AI; + +public sealed class OpenAICompletionTests : IDisposable +{ + private readonly IConfigurationRoot _configuration; + + public OpenAICompletionTests(ITestOutputHelper output) + { + this._logger = new XunitLogger(output); + this._testOutputHelper = new RedirectOutput(output); + Console.SetOut(this._testOutputHelper); + + // Load configuration + this._configuration = new ConfigurationBuilder() + .AddJsonFile(path: "testsettings.json", optional: false, reloadOnChange: true) + .AddJsonFile(path: "testsettings.development.json", optional: true, reloadOnChange: true) + .AddEnvironmentVariables() + .AddUserSecrets() + .Build(); + } + + [Theory(Skip = "OpenAI will often throttle requests. This test is for manual verification.")] + [InlineData("Where is the most famous fish market in Seattle, Washington, USA?", "Pike Place Market")] + public async Task OpenAITestAsync(string prompt, string expectedAnswerContains) + { + // Arrange + IKernel target = Kernel.Builder.WithLogger(this._logger).Build(); + + OpenAIConfiguration? openAIConfiguration = this._configuration.GetSection("OpenAI").Get(); + Assert.NotNull(openAIConfiguration); + + target.Config.AddOpenAICompletionBackend( + label: openAIConfiguration.Label, + modelId: openAIConfiguration.ModelId, + apiKey: openAIConfiguration.ApiKey); + + target.Config.SetDefaultCompletionBackend(openAIConfiguration.Label); + + IDictionary skill = GetSkill("ChatSkill", target); + + // Act + SKContext actual = await target.RunAsync(prompt, skill["Chat"]); + + // Assert + Assert.Contains(expectedAnswerContains, actual.Result, StringComparison.InvariantCultureIgnoreCase); + } + + [Theory] + [InlineData("Where is the most famous fish market in Seattle, Washington, USA?", "Pike Place")] + public async Task AzureOpenAITestAsync(string prompt, string expectedAnswerContains) + { + // Arrange + IKernel target = Kernel.Builder.WithLogger(this._logger).Build(); + + // OpenAIConfiguration? openAIConfiguration = this._configuration.GetSection("OpenAI").Get(); + // Assert.NotNull(openAIConfiguration); + AzureOpenAIConfiguration? azureOpenAIConfiguration = this._configuration.GetSection("AzureOpenAI").Get(); + Assert.NotNull(azureOpenAIConfiguration); + + target.Config.AddAzureOpenAICompletionBackend( + label: azureOpenAIConfiguration.Label, + deploymentName: azureOpenAIConfiguration.DeploymentName, + endpoint: azureOpenAIConfiguration.Endpoint, + apiKey: azureOpenAIConfiguration.ApiKey); + + target.Config.SetDefaultCompletionBackend(azureOpenAIConfiguration.Label); + + IDictionary skill = GetSkill("ChatSkill", target); + + // Act + SKContext actual = await target.RunAsync(prompt, skill["Chat"]); + + // Assert + Assert.Empty(actual.LastErrorDescription); + Assert.False(actual.ErrorOccurred); + Assert.Contains(expectedAnswerContains, actual.Result, StringComparison.InvariantCultureIgnoreCase); + } + + private static IDictionary GetSkill(string skillName, IKernel target) + { + string? currentAssemblyDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + if (string.IsNullOrWhiteSpace(currentAssemblyDirectory)) + { + throw new InvalidOperationException("Unable to determine current assembly directory."); + } + + string skillParentDirectory = Path.GetFullPath(Path.Combine(currentAssemblyDirectory, "../../../../../../samples/skills")); + + IDictionary skill = target.ImportSemanticSkillFromDirectory(skillParentDirectory, skillName); + return skill; + } + + #region internals + + private readonly XunitLogger _logger; + private readonly RedirectOutput _testOutputHelper; + + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + ~OpenAICompletionTests() + { + this.Dispose(false); + } + + private void Dispose(bool disposing) + { + if (disposing) + { + this._logger.Dispose(); + this._testOutputHelper.Dispose(); + } + } + + #endregion +} diff --git a/dotnet/src/IntegrationTest/IntegrationTests.csproj b/dotnet/src/IntegrationTest/IntegrationTests.csproj new file mode 100644 index 000000000000..9af09b808655 --- /dev/null +++ b/dotnet/src/IntegrationTest/IntegrationTests.csproj @@ -0,0 +1,43 @@ + + + + IntegrationTests + IntegrationTests + net6.0 + false + + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + PreserveNewest + + + PreserveNewest + + + + diff --git a/dotnet/src/IntegrationTest/README.md b/dotnet/src/IntegrationTest/README.md new file mode 100644 index 000000000000..6c42496ffd40 --- /dev/null +++ b/dotnet/src/IntegrationTest/README.md @@ -0,0 +1,60 @@ +# Azure/OpenAI Skill Integration Tests + +## Requirements + +1. **Azure OpenAI**: go to the [Azure OpenAI Quickstart](https://learn.microsoft.com/en-us/azure/cognitive-services/openai/quickstart) + and deploy an instance of Azure OpenAI, deploy a model like "text-davinci-003" find your Endpoint and API key. +2. **OpenAI**: go to [OpenAI](https://openai.com/api/) to register and procure your API key. +3. **Azure Bing Web Search API**: go to [Bing Web Seach API](https://www.microsoft.com/en-us/bing/apis/bing-web-search-api) + and select `Try Now` to get started. + +## Setup + +1. Create a `testsettings.development.json` file next to `testsettings.json`. This file will be ignored by git, + the content will not end up in pull requests, so it's safe for personal settings. Keep the file safe. +2. Edit `testsettings.development.json` and + 1. set you Azure OpenAI and OpenAI keys and settings found in Azure portal and OpenAI website. + 2. set the `Bing:ApiKey` using the API key you can find in the Azure portal. + +For example: + +```json +{ + "OpenAI": { + "Label": "text-davinci-003", + "ModelId": "text-davinci-003", + "ApiKey": "sk-...." + }, + "AzureOpenAI": { + "Label": "azure-text-davinci-003", + "DeploymentName": "text-davinci-003", + "Endpoint": "https://contoso.openai.azure.com/", + "ApiKey": "...." + }, + "Bing": { + "ApiKey": "...." + } +} +``` + +3. (Optional) You may also set the test settings in your environment variables. The environment variables will override the settings in the `testsettings.development.json` file. When setting environment variables, use a double underscore (i.e. "\_\_") to delineate between parent and child properties. For example: + + - bash: + + ```bash + export OpenAI__ApiKey="sk-...." + export AzureOpenAI__ApiKey="...." + export AzureOpenAI__DeploymentName="azure-text-davinci-003" + export AzureOpenAI__Endpoint="https://contoso.openai.azure.com/" + export Bing__ApiKey="...." + ``` + + - PowerShell: + + ```ps + $env:OpenAI__ApiKey = "sk-...." + $env:AzureOpenAI__ApiKey = "...." + $env:AzureOpenAI__DeploymentName = "azure-text-davinci-003" + $env:AzureOpenAI__Endpoint = "https://contoso.openai.azure.com/" + $env:Bing__ApiKey = "...." + ``` diff --git a/dotnet/src/IntegrationTest/RedirectOutput.cs b/dotnet/src/IntegrationTest/RedirectOutput.cs new file mode 100644 index 000000000000..f3fadf13e8d2 --- /dev/null +++ b/dotnet/src/IntegrationTest/RedirectOutput.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.IO; +using System.Text; +using Xunit.Abstractions; + +namespace IntegrationTests; + +public class RedirectOutput : TextWriter +{ + private readonly ITestOutputHelper _output; + + public RedirectOutput(ITestOutputHelper output) + { + this._output = output; + } + + public override Encoding Encoding { get; } = Encoding.UTF8; + + public override void WriteLine(string? value) + { + this._output.WriteLine(value); + } +} diff --git a/dotnet/src/IntegrationTest/TestSettings/AzureOpenAIConfiguration.cs b/dotnet/src/IntegrationTest/TestSettings/AzureOpenAIConfiguration.cs new file mode 100644 index 000000000000..751ac908d9fc --- /dev/null +++ b/dotnet/src/IntegrationTest/TestSettings/AzureOpenAIConfiguration.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Diagnostics.CodeAnalysis; + +namespace IntegrationTests.TestSettings; + +[SuppressMessage("Performance", "CA1812:Internal class that is apparently never instantiated", + Justification = "Configuration classes are instantiated through IConfiguration.")] +internal sealed class AzureOpenAIConfiguration +{ + public string Label { get; set; } + + public string DeploymentName { get; set; } + + public string Endpoint { get; set; } + + public string ApiKey { get; set; } + + public AzureOpenAIConfiguration(string label, string deploymentName, string endpoint, string apiKey) + { + this.Label = label; + this.DeploymentName = deploymentName; + this.Endpoint = endpoint; + this.ApiKey = apiKey; + } +} diff --git a/dotnet/src/IntegrationTest/TestSettings/OpenAIConfiguration.cs b/dotnet/src/IntegrationTest/TestSettings/OpenAIConfiguration.cs new file mode 100644 index 000000000000..fdb0c0f3facd --- /dev/null +++ b/dotnet/src/IntegrationTest/TestSettings/OpenAIConfiguration.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Diagnostics.CodeAnalysis; + +namespace IntegrationTests.TestSettings; + +[SuppressMessage("Performance", "CA1812:Internal class that is apparently never instantiated", + Justification = "Configuration classes are instantiated through IConfiguration.")] +internal sealed class OpenAIConfiguration +{ + public string Label { get; set; } + public string ModelId { get; set; } + public string ApiKey { get; set; } + + public OpenAIConfiguration(string label, string modelId, string apiKey) + { + this.Label = label; + this.ModelId = modelId; + this.ApiKey = apiKey; + } +} diff --git a/dotnet/src/IntegrationTest/WebSkill/WebSkillTests.cs b/dotnet/src/IntegrationTest/WebSkill/WebSkillTests.cs new file mode 100644 index 000000000000..53b1cdc7db69 --- /dev/null +++ b/dotnet/src/IntegrationTest/WebSkill/WebSkillTests.cs @@ -0,0 +1,90 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.Configuration; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Connectors.Bing; +using Microsoft.SemanticKernel.Orchestration; +using Microsoft.SemanticKernel.Skills.Web; +using Xunit; +using Xunit.Abstractions; + +namespace IntegrationTests.WebSkill; + +public sealed class WebSkillTests : IDisposable +{ + private readonly string _bingApiKey; + + public WebSkillTests(ITestOutputHelper output) + { + this._logger = new XunitLogger(output); + this._output = output; + + this._testOutputHelper = new RedirectOutput(output); + Console.SetOut(this._testOutputHelper); + + // Load configuration + IConfigurationRoot configuration = new ConfigurationBuilder() + .AddJsonFile(path: "testsettings.json", optional: false, reloadOnChange: true) + .AddJsonFile(path: "testsettings.development.json", optional: true, reloadOnChange: true) + .AddEnvironmentVariables() + .AddUserSecrets() + .Build(); + + string? bingApiKeyCandidate = configuration["Bing:ApiKey"]; + Assert.NotNull(bingApiKeyCandidate); + this._bingApiKey = bingApiKeyCandidate; + } + + [Theory] + [InlineData("What is generally recognized as the tallest building in Seattle, Washington, USA?", "Columbia Center")] + public async Task BingSkillTestAsync(string prompt, string expectedAnswerContains) + { + // Arrange + IKernel kernel = Kernel.Builder.WithLogger(this._logger).Build(); + + using XunitLogger connectorLogger = new(this._output); + using BingConnector connector = new(this._bingApiKey, connectorLogger); + + WebSearchEngineSkill skill = new(connector); + var search = kernel.ImportSkill(skill, "WebSearchEngine"); + + // Act + SKContext result = await kernel.RunAsync( + prompt, + search["SearchAsync"] + ); + + // Assert + Assert.Contains(expectedAnswerContains, result.Result, StringComparison.InvariantCultureIgnoreCase); + } + + #region internals + + private readonly ITestOutputHelper _output; + private readonly XunitLogger _logger; + private readonly RedirectOutput _testOutputHelper; + + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + ~WebSkillTests() + { + this.Dispose(false); + } + + private void Dispose(bool disposing) + { + if (disposing) + { + this._logger.Dispose(); + this._testOutputHelper.Dispose(); + } + } + + #endregion +} diff --git a/dotnet/src/IntegrationTest/XunitLogger.cs b/dotnet/src/IntegrationTest/XunitLogger.cs new file mode 100644 index 000000000000..8505c1cd230b --- /dev/null +++ b/dotnet/src/IntegrationTest/XunitLogger.cs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using Microsoft.Extensions.Logging; +using Xunit.Abstractions; + +namespace IntegrationTests; + +/// +/// A logger that writes to the Xunit test output +/// +internal sealed class XunitLogger : ILogger, IDisposable +{ + private readonly ITestOutputHelper _output; + + public XunitLogger(ITestOutputHelper output) + { + this._output = output; + } + + /// + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + { + this._output.WriteLine(state?.ToString()); + } + + /// + public bool IsEnabled(LogLevel logLevel) => true; + + /// + public IDisposable BeginScope(TState state) where TState : notnull + => this; + + /// + public void Dispose() + { + // This class is marked as disposable to support the BeginScope method. + // However, there is no need to dispose anything. + } +} diff --git a/dotnet/src/IntegrationTest/testsettings.json b/dotnet/src/IntegrationTest/testsettings.json new file mode 100644 index 000000000000..deac31606563 --- /dev/null +++ b/dotnet/src/IntegrationTest/testsettings.json @@ -0,0 +1,16 @@ +{ + "OpenAI": { + "Label": "text-davinci-002", + "ModelId": "text-davinci-002", + "ApiKey": "" + }, + "AzureOpenAI": { + "Label": "azure-text-davinci-002", + "DeploymentName": "", + "Endpoint": "", + "ApiKey": "" + }, + "Bing": { + "ApiKey": "" + } +} \ No newline at end of file diff --git a/dotnet/src/SemanticKernel.Connectors/Connectors.Bing/BingConnector.cs b/dotnet/src/SemanticKernel.Connectors/Connectors.Bing/BingConnector.cs new file mode 100644 index 000000000000..4ca15a4dfb25 --- /dev/null +++ b/dotnet/src/SemanticKernel.Connectors/Connectors.Bing/BingConnector.cs @@ -0,0 +1,103 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Net.Http; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.SemanticKernel.Connectors.Interfaces; + +namespace Microsoft.SemanticKernel.Connectors.Bing; + +/// +/// Bing API connector. +/// +public class BingConnector : IWebSearchEngineConnector, IDisposable +{ + private readonly ILogger _logger; + private readonly HttpClientHandler _httpClientHandler; + private readonly HttpClient _httpClient; + + public BingConnector(string apiKey, ILogger? logger = null) + { + this._logger = logger ?? NullLogger.Instance; + this._httpClientHandler = new() { CheckCertificateRevocationList = true }; + this._httpClient = new HttpClient(this._httpClientHandler); + this._httpClient.DefaultRequestHeaders.Add("Ocp-Apim-Subscription-Key", apiKey); + } + + /// + public async Task SearchAsync(string query, CancellationToken cancellationToken = default) + { + Uri uri = new($"https://api.bing.microsoft.com/v7.0/search?q={Uri.EscapeDataString(query)}&count=1"); + + this._logger.LogDebug("Sending request: {0}", uri); + HttpResponseMessage response = await this._httpClient.GetAsync(uri, cancellationToken); + response.EnsureSuccessStatusCode(); + this._logger.LogDebug("Response received: {0}", response.StatusCode); + + string json = await response.Content.ReadAsStringAsync(); + this._logger.LogTrace("Response content received: {0}", json); + + BingSearchResponse? data = JsonSerializer.Deserialize(json); + WebPage? firstResult = data?.WebPages?.Value?.FirstOrDefault(); + + this._logger.LogDebug("Result: {0}, {1}, {2}", + firstResult?.Name, + firstResult?.Url, + firstResult?.Snippet); + + return firstResult?.Snippet ?? string.Empty; + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + this._httpClient.Dispose(); + this._httpClientHandler.Dispose(); + } + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + this.Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + [SuppressMessage("Performance", "CA1812:Internal class that is apparently never instantiated", + Justification = "Class is instantiated through deserialization.")] + private sealed class BingSearchResponse + { + [JsonPropertyName("webPages")] + public WebPages? WebPages { get; set; } + } + + [SuppressMessage("Performance", "CA1812:Internal class that is apparently never instantiated", + Justification = "Class is instantiated through deserialization.")] + private sealed class WebPages + { + [JsonPropertyName("value")] + public WebPage[]? Value { get; set; } + } + + [SuppressMessage("Performance", "CA1812:Internal class that is apparently never instantiated", + Justification = "Class is instantiated through deserialization.")] + private sealed class WebPage + { + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + + [JsonPropertyName("url")] + public string Url { get; set; } = string.Empty; + + [JsonPropertyName("snippet")] + public string Snippet { get; set; } = string.Empty; + } +} diff --git a/dotnet/src/SemanticKernel.Connectors/Connectors.Bing/Connectors.Bing.csproj b/dotnet/src/SemanticKernel.Connectors/Connectors.Bing/Connectors.Bing.csproj new file mode 100644 index 000000000000..369b2b8a8847 --- /dev/null +++ b/dotnet/src/SemanticKernel.Connectors/Connectors.Bing/Connectors.Bing.csproj @@ -0,0 +1,29 @@ + + + + $([System.IO.Path]::GetDirectoryName($([MSBuild]::GetPathOfFileAbove('.gitignore', '$(MSBuildThisFileDirectory)')))) + + + + + Microsoft.SemanticKernel.Connectors.Bing + Microsoft.SemanticKernel.Connectors.Bing + netstandard2.1 + + + + + Microsoft.SemanticKernel.Connectors.Bing + Semantic Kernel - Microsoft Bing Connector + 0.7 + + + + + + + + + + + \ No newline at end of file diff --git a/dotnet/src/SemanticKernel.Connectors/Connectors.MsGraph/Client/MsGraphClientLoggingHandler.cs b/dotnet/src/SemanticKernel.Connectors/Connectors.MsGraph/Client/MsGraphClientLoggingHandler.cs new file mode 100644 index 000000000000..5c174f837f4a --- /dev/null +++ b/dotnet/src/SemanticKernel.Connectors/Connectors.MsGraph/Client/MsGraphClientLoggingHandler.cs @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; + +namespace Microsoft.SemanticKernel.Connectors.MsGraph.Client; + +/// +/// An HTTPClient logging handler for ensuring diagnostic headers for Graph API calls are available. +/// +/// +/// See https://github.com/microsoftgraph/msgraph-sdk-dotnet-core/blob/dev/docs/logging-requests.md +/// +public class MsGraphClientLoggingHandler : DelegatingHandler +{ + // From https://learn.microsoft.com/graph/best-practices-concept#reliability-and-support + private const string ClientRequestIdHeaderName = "client-request-id"; + + private readonly List _headerNamesToLog = new() + { + ClientRequestIdHeaderName, + "request-id", + "x-ms-ags-diagnostic", + "Date" + }; + + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class. + /// + /// The to use for logging. + public MsGraphClientLoggingHandler(ILogger logger) + { + this._logger = logger; + } + + /// + /// Sends an HTTP request to the inner handler to send to the server as an asynchronous operation. + /// + /// The request message. + /// Cancellation token. + /// The task object representing the asynchronous operation. + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + request.Headers.Add(ClientRequestIdHeaderName, Guid.NewGuid().ToString()); + this.LogHttpMessage(request.Headers, request.RequestUri, "REQUEST"); + HttpResponseMessage response = await base.SendAsync(request, cancellationToken); + this.LogHttpMessage(response.Headers, response.RequestMessage.RequestUri, "RESPONSE"); + return response; + } + + /// + /// Log the headers and URI of an HTTP message. + /// + private void LogHttpMessage(HttpHeaders headers, Uri uri, string prefix) + { + if (this._logger.IsEnabled(LogLevel.Debug)) + { + StringBuilder message = new StringBuilder(); + message.AppendLine($"{prefix} {uri}"); + foreach (string headerName in this._headerNamesToLog) + { + if (headers.TryGetValues(headerName, out IEnumerable values)) + { + message.AppendLine($"{headerName}: {string.Join(", ", values)}"); + } + } + + this._logger.LogDebug("{0}", message); + } + } +} diff --git a/dotnet/src/SemanticKernel.Connectors/Connectors.MsGraph/Client/MsGraphConfiguration.cs b/dotnet/src/SemanticKernel.Connectors/Connectors.MsGraph/Client/MsGraphConfiguration.cs new file mode 100644 index 000000000000..9a911c67c3c0 --- /dev/null +++ b/dotnet/src/SemanticKernel.Connectors/Connectors.MsGraph/Client/MsGraphConfiguration.cs @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; + +namespace Microsoft.SemanticKernel.Connectors.MsGraph.Client; + +/// +/// Graph API connector configuration model. +/// +public class MsGraphConfiguration +{ + /// + /// Gets or sets the client ID. + /// + public string ClientId { get; } + + /// + /// Gets or sets the tenant/directory ID. + /// + public string TenantId { get; } + + /// + /// Gets or sets the API permission scopes. + /// + /// + /// Keeping this parameters nullable and out of the constructor is a workaround for + /// nested types not working with IConfigurationSection.Get. + /// See https://github.com/dotnet/runtime/issues/77677 + /// + public IEnumerable Scopes { get; set; } = Enumerable.Empty(); + + /// + /// Gets or sets the redirect URI to use. + /// + public Uri RedirectUri { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The client id. + /// The tenant id. + /// The redirect URI. + public MsGraphConfiguration( + [NotNull] string clientId, + [NotNull] string tenantId, + [NotNull] Uri redirectUri) + { + this.ClientId = clientId; + this.TenantId = tenantId; + this.RedirectUri = redirectUri; + } +} diff --git a/dotnet/src/SemanticKernel.Connectors/Connectors.MsGraph/Connectors.MsGraph.csproj b/dotnet/src/SemanticKernel.Connectors/Connectors.MsGraph/Connectors.MsGraph.csproj new file mode 100644 index 000000000000..b44032642247 --- /dev/null +++ b/dotnet/src/SemanticKernel.Connectors/Connectors.MsGraph/Connectors.MsGraph.csproj @@ -0,0 +1,30 @@ + + + + $([System.IO.Path]::GetDirectoryName($([MSBuild]::GetPathOfFileAbove('.gitignore', '$(MSBuildThisFileDirectory)')))) + + + + + Microsoft.SemanticKernel.Connectors.MsGraph + Microsoft.SemanticKernel.Connectors.MsGraph + netstandard2.1 + + + + + Microsoft.SemanticKernel.Connectors.MsGraph + Semantic Kernel - Microsoft Graph Connector + 0.7 + + + + + + + + + + + + diff --git a/dotnet/src/SemanticKernel.Connectors/Connectors.MsGraph/CredentialManagers/LocalUserMSALCredentialManager.cs b/dotnet/src/SemanticKernel.Connectors/Connectors.MsGraph/CredentialManagers/LocalUserMSALCredentialManager.cs new file mode 100644 index 000000000000..1094bb7843bf --- /dev/null +++ b/dotnet/src/SemanticKernel.Connectors/Connectors.MsGraph/CredentialManagers/LocalUserMSALCredentialManager.cs @@ -0,0 +1,114 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Identity.Client; +using Microsoft.Identity.Client.Extensions.Msal; +using Microsoft.SemanticKernel.Connectors.MsGraph.Diagnostics; + +namespace Microsoft.SemanticKernel.Connectors.MsGraph.CredentialManagers; + +/// +/// Manages acquiring and caching MSAL credentials locally. +/// **NOT for use in services or with shared profile scenarios.** +/// +/// +/// https://learn.microsoft.com/azure/active-directory/develop/msal-net-token-cache-serialization?tabs=desktop +/// +public class LocalUserMSALCredentialManager +{ + /// + /// An in-memory cache of IPublicClientApplications by clientId and tenantId. + /// + private readonly ConcurrentDictionary _publicClientApplications; + + /// + /// Storage properties used by the token cache. + /// + private readonly StorageCreationProperties _storageProperties; + + /// + /// Helper to create and manager the token cache. + /// + private readonly MsalCacheHelper _cacheHelper; + + /// + /// Initializes a new instance of the class. + /// + public LocalUserMSALCredentialManager() + { + this._publicClientApplications = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); + + // Initialize persistent storage for the token cache + const string cacheSchemaName = "com.microsoft.semantickernel.tokencache"; + + this._storageProperties = new StorageCreationPropertiesBuilder("sk.msal.cache", MsalCacheHelper.UserRootDirectory) + .WithMacKeyChain( + serviceName: $"{cacheSchemaName}.service", + accountName: $"{cacheSchemaName}.account") + .WithLinuxKeyring( + schemaName: cacheSchemaName, + collection: MsalCacheHelper.LinuxKeyRingDefaultCollection, + secretLabel: "MSAL token cache for Semantic Kernel skills.", + attribute1: new KeyValuePair("Version", "1"), + attribute2: new KeyValuePair("Product", "SemanticKernel")) + .Build(); + + // TODO: remove sync wait, may cause deadlock, use await or JoinableTaskFactory.Run + this._cacheHelper = MsalCacheHelper.CreateAsync(this._storageProperties) + .GetAwaiter() + .GetResult(); + + this._cacheHelper.VerifyPersistence(); + } + + /// + /// Acquires an access token for the specified client ID, tenant ID, scopes, and redirect URI. + /// + public async Task GetTokenAsync(string clientId, string tenantId, string[] scopes, Uri redirectUri) + { + Ensure.NotNullOrWhitespace(clientId, nameof(clientId)); + Ensure.NotNullOrWhitespace(tenantId, nameof(tenantId)); + Ensure.NotNull(redirectUri, nameof(redirectUri)); + Ensure.NotNull(scopes, nameof(scopes)); + + IPublicClientApplication app = this._publicClientApplications.GetOrAdd( + key: this.PublicClientApplicationsKey(clientId, tenantId), + valueFactory: _ => + { + IPublicClientApplication newPublicApp = PublicClientApplicationBuilder.Create(clientId) + .WithRedirectUri(redirectUri.ToString()) + .WithTenantId(tenantId) + .Build(); + this._cacheHelper.RegisterCache(newPublicApp.UserTokenCache); + return newPublicApp; + }); + + IEnumerable accounts = await app.GetAccountsAsync(); + + AuthenticationResult result; + try + { + result = await app.AcquireTokenSilent(scopes, accounts.FirstOrDefault()) + .ExecuteAsync(); + } + catch (MsalUiRequiredException) + { + // A MsalUiRequiredException happened on AcquireTokenSilent. + // This indicates you need to call AcquireTokenInteractive to acquire a token + result = await app.AcquireTokenInteractive(scopes) + .ExecuteAsync(); + // throws MsalException + } + + return result.AccessToken; + } + + /// + /// Returns a key for the public client application dictionary. + /// + private string PublicClientApplicationsKey(string clientId, string tenantId) => $"{clientId}_{tenantId}"; +} diff --git a/dotnet/src/SemanticKernel.Connectors/Connectors.MsGraph/Diagnostics/Ensure.cs b/dotnet/src/SemanticKernel.Connectors/Connectors.MsGraph/Diagnostics/Ensure.cs new file mode 100644 index 000000000000..f084372ddc7e --- /dev/null +++ b/dotnet/src/SemanticKernel.Connectors/Connectors.MsGraph/Diagnostics/Ensure.cs @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +namespace Microsoft.SemanticKernel.Connectors.MsGraph.Diagnostics; + +/// +/// Internal data validation class. +/// +internal static class Ensure +{ + /// + /// Ensures the given parameter is not null or does not contain only white-space characters. + /// Throws an if the parameter is invalid. + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void NotNullOrWhitespace([NotNull] string parameter, [NotNull] string parameterName) + { + if (string.IsNullOrWhiteSpace(parameter)) + { + throw new ArgumentException($"Parameter '{parameterName}' cannot be null or whitespace.", parameterName); + } + } + + /// + /// Ensures the given parameter is not null. + /// Throws an if the parameter is invalid. + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void NotNull([NotNull] object parameter, [NotNull] string parameterName) + { + if (parameter == null) + { + throw new ArgumentNullException($"Parameter '{parameterName}' cannot be null.", parameterName); + } + } +} diff --git a/dotnet/src/SemanticKernel.Connectors/Connectors.MsGraph/Exceptions/MsGraphConnectorException.cs b/dotnet/src/SemanticKernel.Connectors/Connectors.MsGraph/Exceptions/MsGraphConnectorException.cs new file mode 100644 index 000000000000..b9ce249f9d61 --- /dev/null +++ b/dotnet/src/SemanticKernel.Connectors/Connectors.MsGraph/Exceptions/MsGraphConnectorException.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; + +namespace Microsoft.SemanticKernel.Connectors.MsGraph.Exceptions; + +/// +/// Exception thrown by the MsGraph connectors +/// +public class MsGraphConnectorException : Exception +{ + /// + /// Initializes a new instance of the class. + /// + /// Exception message. + public MsGraphConnectorException(string message) : base(message) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// Exception message. + /// Inner exception. + public MsGraphConnectorException(string message, Exception innerException) : base(message, innerException) + { + } +} diff --git a/dotnet/src/SemanticKernel.Connectors/Connectors.MsGraph/MicrosoftToDoConnector.cs b/dotnet/src/SemanticKernel.Connectors/Connectors.MsGraph/MicrosoftToDoConnector.cs new file mode 100644 index 000000000000..d3e7976220dd --- /dev/null +++ b/dotnet/src/SemanticKernel.Connectors/Connectors.MsGraph/MicrosoftToDoConnector.cs @@ -0,0 +1,157 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Graph; +using Microsoft.SemanticKernel.Connectors.Interfaces; +using Microsoft.SemanticKernel.Connectors.Interfaces.Models; +using Microsoft.SemanticKernel.Connectors.MsGraph.Diagnostics; +using Microsoft.SemanticKernel.Connectors.MsGraph.Exceptions; +using TaskStatus = Microsoft.Graph.TaskStatus; + +namespace Microsoft.SemanticKernel.Connectors.MsGraph; + +/// +/// Connector for Microsoft To-Do API +/// +public class MicrosoftToDoConnector : ITaskManagementConnector +{ + private readonly GraphServiceClient _graphServiceClient; + + /// + /// Initializes a new instance of the class. + /// + /// A graph service client. + public MicrosoftToDoConnector(GraphServiceClient graphServiceClient) + { + this._graphServiceClient = graphServiceClient; + } + + /// + public async Task GetDefaultTaskListAsync(CancellationToken cancellationToken = default) + { + // .Filter("wellknownListName eq 'defaultList'") does not work as expected so we grab all the lists locally and filter them by name. + // GH issue: https://github.com/microsoftgraph/microsoft-graph-docs/issues/17694 + + ITodoListsCollectionPage lists = await this._graphServiceClient.Me + .Todo.Lists + .Request().GetAsync(cancellationToken); + + TodoTaskList? result = lists.SingleOrDefault(list => list.WellknownListName == WellknownListName.DefaultList); + + while (result == null && lists.Count != 0 && lists.NextPageRequest != null) + { + lists = await lists.NextPageRequest.GetAsync(cancellationToken); + result = lists.SingleOrDefault(list => list.WellknownListName == WellknownListName.DefaultList); + } + + if (result == null) + { + throw new MsGraphConnectorException("Could not find default task list."); + } + + return new TaskManagementTaskList(result.Id, result.DisplayName); + } + + /// + public async Task> GetTaskListsAsync(CancellationToken cancellationToken = default) + { + ITodoListsCollectionPage lists = await this._graphServiceClient.Me + .Todo.Lists + .Request().GetAsync(cancellationToken); + + List taskLists = lists.ToList(); + + while (lists.Count != 0 && lists.NextPageRequest != null) + { + lists = await lists.NextPageRequest.GetAsync(cancellationToken); + taskLists.AddRange(lists.ToList()); + } + + return taskLists.Select(list => new TaskManagementTaskList( + id: list.Id, + name: list.DisplayName)); + } + + /// + public async Task> GetTasksAsync(string listId, CancellationToken cancellationToken = default) + { + Ensure.NotNullOrWhitespace(listId, nameof(listId)); + + ITodoTaskListTasksCollectionPage tasksPage = await this._graphServiceClient.Me + .Todo.Lists[listId] + .Tasks.Request().GetAsync(cancellationToken); + + List tasks = tasksPage.ToList(); + + while (tasksPage.Count != 0 && tasksPage.NextPageRequest != null) + { + tasksPage = await tasksPage.NextPageRequest.GetAsync(cancellationToken); + tasks.AddRange(tasksPage.ToList()); + } + + return tasks.Select(task => new TaskManagementTask( + id: task.Id, + title: task.Title, + reminder: task.ReminderDateTime?.DateTime, + due: task.DueDateTime?.DateTime, + isCompleted: task.Status == TaskStatus.Completed)); + } + + /// + public async Task AddTaskAsync(string listId, TaskManagementTask task, CancellationToken cancellationToken = default) + { + Ensure.NotNullOrWhitespace(listId, nameof(listId)); + Ensure.NotNull(task, nameof(task)); + + return ToTaskListTask(await this._graphServiceClient.Me + .Todo.Lists[listId] + .Tasks + .Request().AddAsync(FromTaskListTask(task), cancellationToken)); + } + + /// + public Task DeleteTaskAsync(string listId, string taskId, CancellationToken cancellationToken = default) + { + Ensure.NotNullOrWhitespace(listId, nameof(listId)); + Ensure.NotNullOrWhitespace(taskId, nameof(taskId)); + + return this._graphServiceClient.Me + .Todo.Lists[listId] + .Tasks[taskId] + .Request().DeleteAsync(cancellationToken); + } + + private static TodoTask FromTaskListTask(TaskManagementTask task) + { + Ensure.NotNull(task, nameof(task)); + + return new TodoTask() + { + Title = task.Title, + ReminderDateTime = task.Reminder == null + ? null + : DateTimeTimeZone.FromDateTimeOffset(DateTimeOffset.Parse(task.Reminder, CultureInfo.InvariantCulture.DateTimeFormat)), + DueDateTime = task.Due == null + ? null + : DateTimeTimeZone.FromDateTimeOffset(DateTimeOffset.Parse(task.Due, CultureInfo.InvariantCulture.DateTimeFormat)), + Status = task.IsCompleted ? TaskStatus.Completed : TaskStatus.NotStarted + }; + } + + private static TaskManagementTask ToTaskListTask(TodoTask task) + { + Ensure.NotNull(task, nameof(task)); + + return new TaskManagementTask( + id: task.Id, + title: task.Title, + reminder: task.ReminderDateTime?.DateTime, + due: task.DueDateTime?.DateTime, + isCompleted: task.Status == TaskStatus.Completed); + } +} diff --git a/dotnet/src/SemanticKernel.Connectors/Connectors.MsGraph/OneDriveConnector.cs b/dotnet/src/SemanticKernel.Connectors/Connectors.MsGraph/OneDriveConnector.cs new file mode 100644 index 000000000000..269779eba171 --- /dev/null +++ b/dotnet/src/SemanticKernel.Connectors/Connectors.MsGraph/OneDriveConnector.cs @@ -0,0 +1,132 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.IO; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Graph; +using Microsoft.SemanticKernel.Connectors.Interfaces; +using Microsoft.SemanticKernel.Connectors.MsGraph.Diagnostics; +using Microsoft.SemanticKernel.Connectors.MsGraph.Exceptions; + +namespace Microsoft.SemanticKernel.Connectors.MsGraph; + +/// +/// Connector for OneDrive API +/// +public class OneDriveConnector : ICloudDriveConnector, IFileSystemConnector +{ + private readonly GraphServiceClient _graphServiceClient; + + /// + /// Initializes a new instance of the class. + /// + /// A graph service client. + public OneDriveConnector(GraphServiceClient graphServiceClient) + { + this._graphServiceClient = graphServiceClient; + } + + /// + public async Task GetFileContentStreamAsync(string filePath, CancellationToken cancellationToken = default) + { + Ensure.NotNullOrWhitespace(filePath, nameof(filePath)); + + return await this._graphServiceClient.Me + .Drive.Root + .ItemWithPath(filePath).Content + .Request().GetAsync(cancellationToken); + } + + /// + /// This method is not yet supported for . + public Task GetWriteableFileStreamAsync(string filePath, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + /// + /// This method is not yet supported for . + public Task CreateFileAsync(string filePath, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + /// + public async Task FileExistsAsync(string filePath, CancellationToken cancellationToken = default) + { + Ensure.NotNullOrWhitespace(filePath, nameof(filePath)); + + try + { + await this._graphServiceClient.Me + .Drive.Root + .ItemWithPath(filePath).Request().GetAsync(cancellationToken); + + // If no exception is thrown, the file exists. + return true; + } + catch (ServiceException ex) + { + // If the exception is a 404 Not Found, the file does not exist. + if (ex.StatusCode == HttpStatusCode.NotFound) + { + return false; + } + + // Otherwise, rethrow the exception. + throw; + } + } + + /// + public async Task UploadSmallFileAsync(string filePath, string destinationPath, CancellationToken cancellationToken = default) + { + Ensure.NotNullOrWhitespace(filePath, nameof(filePath)); + Ensure.NotNullOrWhitespace(destinationPath, nameof(destinationPath)); + + filePath = Environment.ExpandEnvironmentVariables(filePath); + + long fileSize = new FileInfo(filePath).Length; + if (fileSize > 4 * 1024 * 1024) + { + throw new IOException("File is too large to upload - function currently only supports files up to 4MB."); + } + + using FileStream fileContentStream = new FileStream(filePath, FileMode.Open, FileAccess.Read); + + GraphResponse response = await this._graphServiceClient.Me + .Drive.Root + .ItemWithPath(destinationPath).Content + .Request().PutResponseAsync(fileContentStream, cancellationToken, HttpCompletionOption.ResponseContentRead); + + response.ToHttpResponseMessage().EnsureSuccessStatusCode(); + } + + /// + public async Task CreateShareLinkAsync(string filePath, string type = "view", string scope = "anonymous", + CancellationToken cancellationToken = default) + { + Ensure.NotNullOrWhitespace(filePath, nameof(filePath)); + Ensure.NotNullOrWhitespace(type, nameof(type)); + Ensure.NotNullOrWhitespace(scope, nameof(scope)); + + GraphResponse response = await this._graphServiceClient.Me + .Drive.Root + .ItemWithPath(filePath) + .CreateLink(type, scope) + .Request().PostResponseAsync(cancellationToken); + + response.ToHttpResponseMessage().EnsureSuccessStatusCode(); + + string? result = (await response.GetResponseObjectAsync()).Link?.WebUrl; + if (string.IsNullOrWhiteSpace(result)) + { + throw new MsGraphConnectorException("Shareable file link was null or whitespace."); + } + + return result; + } +} diff --git a/dotnet/src/SemanticKernel.Connectors/Connectors.MsGraph/OrganizationHierarchyConnector.cs b/dotnet/src/SemanticKernel.Connectors/Connectors.MsGraph/OrganizationHierarchyConnector.cs new file mode 100644 index 000000000000..1840d0d83b71 --- /dev/null +++ b/dotnet/src/SemanticKernel.Connectors/Connectors.MsGraph/OrganizationHierarchyConnector.cs @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Graph; +using Microsoft.SemanticKernel.Connectors.Interfaces; + +namespace Microsoft.SemanticKernel.Connectors.MsGraph; + +/// +/// Connector for Microsoft Graph API for organizational hierarchy. +/// +public class OrganizationHierarchyConnector : IOrganizationHierarchyConnector +{ + private readonly GraphServiceClient _graphServiceClient; + + /// + /// Initializes a new instance of the class. + /// + /// A graph service client. + public OrganizationHierarchyConnector(GraphServiceClient graphServiceClient) + { + this._graphServiceClient = graphServiceClient; + } + + /// + public async Task GetManagerEmailAsync(CancellationToken cancellationToken = default) => + ((User)await this._graphServiceClient.Me + .Manager + .Request().GetAsync(cancellationToken)).UserPrincipalName; + + /// + public async Task GetManagerNameAsync(CancellationToken cancellationToken = default) => + ((User)await this._graphServiceClient.Me + .Manager + .Request().GetAsync(cancellationToken)).DisplayName; + + /// + public async Task> GetDirectReportsEmailAsync(CancellationToken cancellationToken = default) + { + IUserDirectReportsCollectionWithReferencesPage directsPage = await this._graphServiceClient.Me + .DirectReports + .Request().GetAsync(cancellationToken); + + List directs = directsPage.Cast().ToList(); + + while (directs.Count != 0 && directsPage.NextPageRequest != null) + { + directsPage = await directsPage.NextPageRequest.GetAsync(cancellationToken); + directs.AddRange(directsPage.Cast()); + } + + return directs.Select(d => d.UserPrincipalName); + } +} diff --git a/dotnet/src/SemanticKernel.Connectors/Connectors.MsGraph/OutlookCalendarConnector.cs b/dotnet/src/SemanticKernel.Connectors/Connectors.MsGraph/OutlookCalendarConnector.cs new file mode 100644 index 000000000000..c403cd36773b --- /dev/null +++ b/dotnet/src/SemanticKernel.Connectors/Connectors.MsGraph/OutlookCalendarConnector.cs @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Graph; +using Microsoft.Graph.Extensions; +using Microsoft.SemanticKernel.Connectors.Interfaces; +using Microsoft.SemanticKernel.Connectors.Interfaces.Models; + +namespace Microsoft.SemanticKernel.Connectors.MsGraph; + +/// +/// Connector for Outlook Calendar API +/// +public class OutlookCalendarConnector : ICalendarConnector +{ + private readonly GraphServiceClient _graphServiceClient; + + /// + /// Initializes a new instance of the class. + /// + /// A graph service client. + public OutlookCalendarConnector(GraphServiceClient graphServiceClient) + { + this._graphServiceClient = graphServiceClient; + } + + /// + public async Task AddEventAsync(CalendarEvent calendarEvent, CancellationToken cancellationToken = default) + { + Event resultEvent = await this._graphServiceClient.Me.Events.Request().AddAsync(ToGraphEvent(calendarEvent), cancellationToken); + return ToCalendarEvent(resultEvent); + } + + private static Event ToGraphEvent(CalendarEvent calendarEvent) + => new Event() + { + Subject = calendarEvent.Subject, + Body = new ItemBody { Content = calendarEvent.Content, ContentType = BodyType.Html }, + Start = DateTimeTimeZone.FromDateTimeOffset(calendarEvent.Start), + End = DateTimeTimeZone.FromDateTimeOffset(calendarEvent.End), + Location = new Location { DisplayName = calendarEvent.Location }, + Attendees = calendarEvent.Attendees?.Select(a => new Attendee { EmailAddress = new EmailAddress { Address = a } }) + }; + + private static CalendarEvent ToCalendarEvent(Event msGraphEvent) + => new CalendarEvent(msGraphEvent.Subject, msGraphEvent.Start.ToDateTimeOffset(), msGraphEvent.End.ToDateTimeOffset()) + { + Id = msGraphEvent.Id, + Content = msGraphEvent.Body?.Content, + Location = msGraphEvent.Location?.DisplayName, + Attendees = msGraphEvent.Attendees?.Select(a => a.EmailAddress.Address) + }; +} diff --git a/dotnet/src/SemanticKernel.Connectors/Connectors.MsGraph/OutlookMailConnector.cs b/dotnet/src/SemanticKernel.Connectors/Connectors.MsGraph/OutlookMailConnector.cs new file mode 100644 index 000000000000..bf911791cb18 --- /dev/null +++ b/dotnet/src/SemanticKernel.Connectors/Connectors.MsGraph/OutlookMailConnector.cs @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Graph; +using Microsoft.SemanticKernel.Connectors.Interfaces; +using Microsoft.SemanticKernel.Connectors.MsGraph.Diagnostics; + +namespace Microsoft.SemanticKernel.Connectors.MsGraph; + +/// +/// Connector for Outlook Mail API +/// +public class OutlookMailConnector : IEmailConnector +{ + private readonly GraphServiceClient _graphServiceClient; + + /// + /// Initializes a new instance of the class. + /// + /// A graph service client. + public OutlookMailConnector(GraphServiceClient graphServiceClient) + { + this._graphServiceClient = graphServiceClient; + } + + /// + public async Task GetMyEmailAddressAsync(CancellationToken cancellationToken = default) + => (await this._graphServiceClient.Me.Request().GetAsync(cancellationToken)).UserPrincipalName; + + /// + public async Task SendEmailAsync(string subject, string content, string[] recipients, CancellationToken cancellationToken = default) + { + Ensure.NotNullOrWhitespace(subject, nameof(subject)); + Ensure.NotNullOrWhitespace(content, nameof(content)); + Ensure.NotNull(recipients, nameof(recipients)); + + Message message = new Message + { + Subject = subject, + Body = new ItemBody { ContentType = BodyType.Text, Content = content }, + ToRecipients = recipients.Select(recipientAddress => new Recipient + { + EmailAddress = new EmailAddress + { + Address = recipientAddress + } + }) + }; + + await this._graphServiceClient.Me.SendMail(message).Request().PostAsync(cancellationToken); + } +} diff --git a/dotnet/src/SemanticKernel.Connectors/Connectors.OpenXml/Connectors.OpenXml.csproj b/dotnet/src/SemanticKernel.Connectors/Connectors.OpenXml/Connectors.OpenXml.csproj new file mode 100644 index 000000000000..50393473c932 --- /dev/null +++ b/dotnet/src/SemanticKernel.Connectors/Connectors.OpenXml/Connectors.OpenXml.csproj @@ -0,0 +1,28 @@ + + + + $([System.IO.Path]::GetDirectoryName($([MSBuild]::GetPathOfFileAbove('.gitignore', '$(MSBuildThisFileDirectory)')))) + + + + + Microsoft.SemanticKernel.Connectors.OpenXml + Microsoft.SemanticKernel.Connectors.OpenXml + netstandard2.1 + + + + + Microsoft.SemanticKernel.Connectors.OpenXml + Semantic Kernel - OpenXml Connector + 0.7 + + + + + + + + + + diff --git a/dotnet/src/SemanticKernel.Connectors/Connectors.OpenXml/Extensions/WordprocessingDocumentEx.cs b/dotnet/src/SemanticKernel.Connectors/Connectors.OpenXml/Extensions/WordprocessingDocumentEx.cs new file mode 100644 index 000000000000..5bab9f54e435 --- /dev/null +++ b/dotnet/src/SemanticKernel.Connectors/Connectors.OpenXml/Extensions/WordprocessingDocumentEx.cs @@ -0,0 +1,75 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Text; +using DocumentFormat.OpenXml.Packaging; +using DocumentFormat.OpenXml.Wordprocessing; + +namespace Microsoft.SemanticKernel.Connectors.OpenXml.Extensions; + +// Extension methods for DocumentFormat.OpenXml.Packaging.WordprocessingDocument +// Note: the "Wordprocessing" vs "WordProcessing" typo is in the 3P class, we follow the original naming. +internal static class WordprocessingDocumentEx +{ + internal static void Initialize(this WordprocessingDocument wordprocessingDocument) + { + // Add a main document part. + MainDocumentPart mainPart = wordprocessingDocument.AddMainDocumentPart(); + + // Create the document structure. + mainPart.Document = new DocumentFormat.OpenXml.Wordprocessing.Document(); + mainPart.Document.AppendChild(new Body()); + } + + internal static string ReadText(this WordprocessingDocument wordprocessingDocument) + { + StringBuilder sb = new StringBuilder(); + + var mainPart = wordprocessingDocument.MainDocumentPart; + if (mainPart is null) + { + throw new InvalidOperationException("The main document part is missing."); + } + + var body = mainPart.Document.Body; + if (body is null) + { + throw new InvalidOperationException("The document body is missing."); + } + + var paras = body.Descendants(); + if (paras != null) + { + foreach (Paragraph para in paras) + { + sb.AppendLine(para.InnerText); + } + } + + return sb.ToString(); + } + + internal static void AppendText(this WordprocessingDocument wordprocessingDocument, string text) + { + if (text is null) + { + throw new ArgumentNullException(nameof(text)); + } + + MainDocumentPart? mainPart = wordprocessingDocument.MainDocumentPart; + if (mainPart is null) + { + throw new InvalidOperationException("The main document part is missing."); + } + + Body? body = mainPart.Document.Body; + if (body is null) + { + throw new InvalidOperationException("The document body is missing."); + } + + Paragraph para = body.AppendChild(new Paragraph()); + Run run = para.AppendChild(new Run()); + run.AppendChild(new DocumentFormat.OpenXml.Wordprocessing.Text(text)); + } +} diff --git a/dotnet/src/SemanticKernel.Connectors/Connectors.OpenXml/LocalFileSystemConnector.cs b/dotnet/src/SemanticKernel.Connectors/Connectors.OpenXml/LocalFileSystemConnector.cs new file mode 100644 index 000000000000..055ff492a674 --- /dev/null +++ b/dotnet/src/SemanticKernel.Connectors/Connectors.OpenXml/LocalFileSystemConnector.cs @@ -0,0 +1,76 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.SemanticKernel.Connectors.Interfaces; + +namespace Microsoft.SemanticKernel.Connectors.OpenXml; + +/// +/// Connector for local filesystem +/// +public class LocalFileSystemConnector : IFileSystemConnector +{ + /// + /// Get the contents of a file as a read-only stream. + /// + /// Path to file + /// Cancellation token. + /// + /// + /// + /// + /// + /// + /// + /// + /// + public Task GetFileContentStreamAsync(string filePath, CancellationToken cancellationToken = default) + { + return Task.FromResult(File.Open(Environment.ExpandEnvironmentVariables(filePath), FileMode.Open, FileAccess.Read)); + } + + /// + /// Get a writeable stream to a file. + /// + /// Path to file + /// Cancellation token. + /// + /// + /// + /// + /// + /// + /// + /// + /// + public Task GetWriteableFileStreamAsync(string filePath, CancellationToken cancellationToken = default) + { + return Task.FromResult(File.Open(Environment.ExpandEnvironmentVariables(filePath), FileMode.Open, FileAccess.ReadWrite)); + } + + /// + /// Get a writeable stream to a file. + /// + /// Path to file + /// Cancellation token. + /// + /// + /// + /// + /// + /// + /// + public Task CreateFileAsync(string filePath, CancellationToken cancellationToken = default) + { + return Task.FromResult(File.Create(Environment.ExpandEnvironmentVariables(filePath))); + } + + /// + public Task FileExistsAsync(string filePath, CancellationToken cancellationToken = default) + { + return Task.FromResult(File.Exists(Environment.ExpandEnvironmentVariables(filePath))); + } +} diff --git a/dotnet/src/SemanticKernel.Connectors/Connectors.OpenXml/WordDocumentConnector.cs b/dotnet/src/SemanticKernel.Connectors/Connectors.OpenXml/WordDocumentConnector.cs new file mode 100644 index 000000000000..62c9a4f7a2cc --- /dev/null +++ b/dotnet/src/SemanticKernel.Connectors/Connectors.OpenXml/WordDocumentConnector.cs @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.IO; +using DocumentFormat.OpenXml; +using DocumentFormat.OpenXml.Packaging; +using Microsoft.SemanticKernel.Connectors.Interfaces; +using Microsoft.SemanticKernel.Connectors.OpenXml.Extensions; + +namespace Microsoft.SemanticKernel.Connectors.OpenXml; + +/// +/// Connector for Microsoft Word (.docx) files +/// +public class WordDocumentConnector : IDocumentConnector +{ + /// + /// Read all text from the document. + /// + /// Document stream + /// String containing all text from the document. + /// + /// + /// + /// + public string ReadText(Stream stream) + { + using WordprocessingDocument wordprocessingDocument = WordprocessingDocument.Open(stream, false); + return wordprocessingDocument.ReadText(); + } + + /// + /// Initialize a document from the given stream. + /// + /// IO stream + /// + /// + /// + public void Initialize(Stream stream) + { + using (WordprocessingDocument wordprocessingDocument = WordprocessingDocument.Create(stream, WordprocessingDocumentType.Document)) + { + wordprocessingDocument.Initialize(); + } + + // This is a workaround for a bug with the OpenXML SDK [TODO: add bug number] + using (WordprocessingDocument wordprocessingDocument = WordprocessingDocument.Open(stream, false)) { } + } + + /// + /// Append the specified text to the document. This requires read-write permissions. + /// + /// Document stream + /// String of text to write to the document. + /// + /// + /// + /// + public void AppendText(Stream stream, string text) + { + using (WordprocessingDocument wordprocessingDocument = WordprocessingDocument.Open(stream, true)) + { + wordprocessingDocument.AppendText(text); + } + + // This is a workaround for a bug with the OpenXML SDK [TODO: add bug number] + using (WordprocessingDocument wordprocessingDocument = WordprocessingDocument.Open(stream, false)) { } + } +} diff --git a/dotnet/src/SemanticKernel.Connectors/Connectors.Sqlite.Test/Connectors.Sqlite.Test.csproj b/dotnet/src/SemanticKernel.Connectors/Connectors.Sqlite.Test/Connectors.Sqlite.Test.csproj new file mode 100644 index 000000000000..d1c49da78aac --- /dev/null +++ b/dotnet/src/SemanticKernel.Connectors/Connectors.Sqlite.Test/Connectors.Sqlite.Test.csproj @@ -0,0 +1,31 @@ + + + + net6.0 + enable + false + SemanticKernelConnectorsSqliteTests + SemanticKernelConnectorsSqliteTests + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + diff --git a/dotnet/src/SemanticKernel.Connectors/Connectors.Sqlite.Test/SqliteMemoryStoreTests.cs b/dotnet/src/SemanticKernel.Connectors/Connectors.Sqlite.Test/SqliteMemoryStoreTests.cs new file mode 100644 index 000000000000..c80843a52db7 --- /dev/null +++ b/dotnet/src/SemanticKernel.Connectors/Connectors.Sqlite.Test/SqliteMemoryStoreTests.cs @@ -0,0 +1,186 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.IO; +using System.Linq; +using System.Security.Cryptography; +using System.Threading.Tasks; +using Microsoft.SemanticKernel.Connectors.Sqlite; +using Microsoft.SemanticKernel.Memory.Storage; +using Xunit; + +namespace SemanticKernelConnectorsSqliteTests; + +/// +/// Unit tests of . +/// +public class SqliteDataStoreTests : IDisposable +{ + private const string DatabaseFile = "SqliteDataStoreTests.db"; + private SqliteDataStore? _db = null; + private bool _disposedValue; + + public SqliteDataStoreTests() + { + File.Delete(DatabaseFile); + } + + [Fact] + public async Task InitializeDbConnectionSucceedsAsync() + { + this._db ??= await SqliteDataStore.ConnectAsync(DatabaseFile); + // Assert + Assert.NotNull(this._db); + } + + [Fact] + public async Task PutAndRetrieveNoTimestampSucceedsAsync() + { + // Arrange + int rand = RandomNumberGenerator.GetInt32(int.MaxValue); + string collection = "collection" + rand; + string key = "key" + rand; + string value = "value" + rand; + + // Act + this._db ??= await SqliteDataStore.ConnectAsync(DatabaseFile); + await this._db.PutValueAsync(collection, key, value); + + string? actual = await this._db.GetValueAsync(collection, key); + + // Assert + Assert.NotNull(actual); + Assert.Equal(value, actual); + } + + [Fact] + public async Task PutAndRetrieveWithTimestampSucceedsAsync() + { + // Arrange + int rand = RandomNumberGenerator.GetInt32(int.MaxValue); + string collection = "collection" + rand; + string key = "key" + rand; + string value = "value" + rand; + DateTimeOffset timestamp = DateTimeOffset.UtcNow; + + // Act + this._db ??= await SqliteDataStore.ConnectAsync(DatabaseFile); + await this._db.PutValueAsync(collection, key, value, timestamp); + DataEntry? actual = await this._db.GetAsync(collection, key); + + // Assert + Assert.NotNull(actual); + Assert.Equal(value, actual!.Value.Value); + Assert.True(timestamp.Date.Equals(actual!.Value.Timestamp?.Date)); + Assert.True((int)timestamp.TimeOfDay.TotalSeconds == (int?)actual!.Value.Timestamp?.TimeOfDay.TotalSeconds); + } + + [Fact] + public async Task PutAndRetrieveDataEntryWithTimestampSucceedsAsync() + { + // Arrange + int rand = RandomNumberGenerator.GetInt32(int.MaxValue); + string collection = "collection" + rand; + string key = "key" + rand; + string value = "value" + rand; + DateTimeOffset timestamp = DateTimeOffset.UtcNow; + var data = DataEntry.Create(key, value, timestamp); + + // Act + this._db ??= await SqliteDataStore.ConnectAsync(DatabaseFile); + await this._db.PutAsync(collection, data); + DataEntry? actual = await this._db.GetAsync(collection, key); + + // Assert + Assert.NotNull(actual); + Assert.Equal(value, actual!.Value.Value); + Assert.True(timestamp.Date.Equals(actual!.Value.Timestamp?.Date)); + Assert.True((int)timestamp.TimeOfDay.TotalSeconds == (int?)actual!.Value.Timestamp?.TimeOfDay.TotalSeconds); + } + + [Fact] + public async Task PutAndDeleteDataEntrySucceedsAsync() + { + // Arrange + int rand = RandomNumberGenerator.GetInt32(int.MaxValue); + string collection = "collection" + rand; + string key = "key" + rand; + string value = "value" + rand; + var data = DataEntry.Create(key, value, DateTimeOffset.UtcNow); + + // Act + this._db ??= await SqliteDataStore.ConnectAsync(DatabaseFile); + await this._db.PutAsync(collection, data); + await this._db.RemoveAsync(collection, key); + + // Assert + var retrieved = await this._db.GetAsync(collection, key); + Assert.Null(retrieved); + } + + [Fact] + public async Task ListAllDatabaseCollectionsSucceedsAsync() + { + // Arrange + int rand = RandomNumberGenerator.GetInt32(int.MaxValue); + string collection = "collection" + rand; + string key = "key" + rand; + string value = "value" + rand; + + // Act + this._db ??= await SqliteDataStore.ConnectAsync(DatabaseFile); + await this._db.PutValueAsync(collection, key, value); + var collections = this._db.GetCollectionsAsync(); + + // Assert + Assert.NotNull(collections); + Assert.True(await collections.AnyAsync(), "Collections is empty"); + Assert.True(await collections.ContainsAsync(collection), "Collections do not contain the newly-created collection"); + } + + [Fact] + public async Task GetAllSucceedsAsync() + { + // Arrange + int rand = RandomNumberGenerator.GetInt32(int.MaxValue); + string collection = "collection" + rand; + string key = "key" + rand; + string value = "value" + rand; + int quantity = 15; + + // Act + this._db ??= await SqliteDataStore.ConnectAsync(DatabaseFile); + for (int i = 0; i < quantity; i++) + { + await this._db.PutValueAsync(collection, key + i, value); + } + + var getAllResults = this._db.GetAllAsync(collection); + + // Assert + Assert.NotNull(getAllResults); + Assert.True(await getAllResults.AnyAsync(), "Collections is empty"); + Assert.True(await getAllResults.CountAsync() == quantity, "Collections should have 15 entries"); + } + + protected virtual void Dispose(bool disposing) + { + if (!this._disposedValue) + { + if (disposing) + { + this._db?.Dispose(); + File.Delete(DatabaseFile); + } + + this._disposedValue = true; + } + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + this.Dispose(disposing: true); + GC.SuppressFinalize(this); + } +} diff --git a/dotnet/src/SemanticKernel.Connectors/Connectors.Sqlite/Connectors.Sqlite.csproj b/dotnet/src/SemanticKernel.Connectors/Connectors.Sqlite/Connectors.Sqlite.csproj new file mode 100644 index 000000000000..264235524b2c --- /dev/null +++ b/dotnet/src/SemanticKernel.Connectors/Connectors.Sqlite/Connectors.Sqlite.csproj @@ -0,0 +1,36 @@ + + + + $([System.IO.Path]::GetDirectoryName($([MSBuild]::GetPathOfFileAbove('.gitignore', '$(MSBuildThisFileDirectory)')))) + + + + + Microsoft.SemanticKernel.Connectors.Sqlite + Microsoft.SemanticKernel.Connectors.Sqlite + netstandard2.1 + + + + + Microsoft.SemanticKernel.Connectors.Sqlite + Semantic Kernel - SQLite Connector + 0.7 + + + + + + + + + all + + + + + + <_Parameter1>SemanticKernelConnectorsSqliteTests + + + diff --git a/dotnet/src/SemanticKernel.Connectors/Connectors.Sqlite/Database.cs b/dotnet/src/SemanticKernel.Connectors/Connectors.Sqlite/Database.cs new file mode 100644 index 000000000000..0d38a92b0658 --- /dev/null +++ b/dotnet/src/SemanticKernel.Connectors/Connectors.Sqlite/Database.cs @@ -0,0 +1,134 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Data; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Data.Sqlite; + +namespace Microsoft.SemanticKernel.Connectors.Sqlite; + +internal struct DatabaseEntry +{ + public string Key { get; set; } + public string Value { get; set; } + public string? Timestamp { get; set; } +} + +internal static class Database +{ + private const string TableName = "SKDataTable"; + + public static async Task CreateConnectionAsync(string filename, CancellationToken cancel = default) + { + var connection = new SqliteConnection(@"Data Source={filename};"); + await connection.OpenAsync(cancel); + return connection; + } + + public static async Task InsertAsync(this SqliteConnection conn, + string collection, string key, string? value, string? timestamp, CancellationToken cancel = default) + { + await CreateTableAsync(conn, cancel); + + SqliteCommand cmd = conn.CreateCommand(); + cmd.CommandText = $@" + INSERT INTO {TableName}(collection, key, value, timestamp) + VALUES(@collection, @key, @value, @timestamp); "; + cmd.Parameters.AddWithValue("@collection", collection); + cmd.Parameters.AddWithValue("@key", key); + cmd.Parameters.AddWithValue("@value", value ?? string.Empty); + cmd.Parameters.AddWithValue("@timestamp", timestamp ?? string.Empty); + await cmd.ExecuteNonQueryAsync(cancel); + } + + public static async IAsyncEnumerable GetCollectionsAsync(this SqliteConnection conn, + [EnumeratorCancellation] CancellationToken cancel = default) + { + SqliteCommand cmd = conn.CreateCommand(); + cmd.CommandText = $@" + SELECT DISTINCT(collection) + FROM {TableName}"; + + var dataReader = await cmd.ExecuteReaderAsync(cancel); + while (await dataReader.ReadAsync(cancel)) + { + yield return dataReader.GetFieldValue("collection"); + } + } + + public static async IAsyncEnumerable ReadAllAsync(this SqliteConnection conn, + string collection, + [EnumeratorCancellation] CancellationToken cancel = default) + { + SqliteCommand cmd = conn.CreateCommand(); + cmd.CommandText = $@" + SELECT * FROM {TableName} + WHERE collection=@collection"; + cmd.Parameters.AddWithValue("@collection", collection); + + var dataReader = await cmd.ExecuteReaderAsync(cancel); + while (await dataReader.ReadAsync(cancel)) + { + string key = dataReader.GetFieldValue("key"); + string value = dataReader.GetFieldValue("value"); + string timestamp = dataReader.GetFieldValue("timestamp"); + yield return new DatabaseEntry() { Key = key, Value = value, Timestamp = timestamp }; + } + } + + public static async Task ReadAsync(this SqliteConnection conn, + string collection, + string key, + CancellationToken cancel = default) + { + SqliteCommand cmd = conn.CreateCommand(); + cmd.CommandText = $@" + SELECT * FROM {TableName} + WHERE collection=@collection + AND key=@key "; + cmd.Parameters.AddWithValue("@collection", collection); + cmd.Parameters.AddWithValue("@key", key); + + var dataReader = await cmd.ExecuteReaderAsync(cancel); + if (await dataReader.ReadAsync(cancel)) + { + string value = dataReader.GetString(dataReader.GetOrdinal("value")); + string timestamp = dataReader.GetString(dataReader.GetOrdinal("timestamp")); + return new DatabaseEntry() + { + Key = key, + Value = value, + Timestamp = timestamp + }; + } + + return null; + } + + public static Task DeleteAsync(this SqliteConnection conn, string collection, string key, CancellationToken cancel = default) + { + SqliteCommand cmd = conn.CreateCommand(); + cmd.CommandText = $@" + DELETE FROM {TableName} + WHERE collection=@collection + AND key=@key "; + cmd.Parameters.AddWithValue("@collection", collection); + cmd.Parameters.AddWithValue("@key", key); + return cmd.ExecuteNonQueryAsync(cancel); + } + + private static Task CreateTableAsync(SqliteConnection conn, CancellationToken cancel = default) + { + SqliteCommand cmd = conn.CreateCommand(); + cmd.CommandText = $@" + CREATE TABLE IF NOT EXISTS {TableName}( + collection TEXT, + key TEXT, + value TEXT, + timestamp TEXT, + PRIMARY KEY(collection, key))"; + return cmd.ExecuteNonQueryAsync(cancel); + } +} diff --git a/dotnet/src/SemanticKernel.Connectors/Connectors.Sqlite/SqliteMemoryStore.cs b/dotnet/src/SemanticKernel.Connectors/Connectors.Sqlite/SqliteMemoryStore.cs new file mode 100644 index 000000000000..eaf0167e7764 --- /dev/null +++ b/dotnet/src/SemanticKernel.Connectors/Connectors.Sqlite/SqliteMemoryStore.cs @@ -0,0 +1,155 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Runtime.CompilerServices; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Data.Sqlite; +using Microsoft.SemanticKernel.Memory.Storage; + +namespace Microsoft.SemanticKernel.Connectors.Sqlite; + +/// +/// An implementation of backed by a SQLite database. +/// +/// The data is saved to a database file, specified in the constructor. +/// The data persists between subsequent instances. Only one instance may access the file at a time. +/// The caller is responsible for deleting the file. +/// The type of data to be stored in this data store. +public class SqliteDataStore : IDataStore, IDisposable +{ + /// + /// Connect a Sqlite database + /// + /// Path to the database file. If file does not exist, it will be created. + /// Cancellation token + [SuppressMessage("Design", "CA1000:Do not declare static members on generic types", + Justification = "Static factory method used to ensure successful connection.")] + public static async Task> ConnectAsync(string filename, + CancellationToken cancel = default) + { + SqliteConnection dbConnection = await Database.CreateConnectionAsync(filename, cancel); + return new SqliteDataStore(dbConnection); + } + + /// + public IAsyncEnumerable GetCollectionsAsync(CancellationToken cancel = default) + { + return this._dbConnection.GetCollectionsAsync(cancel); + } + + /// + public async IAsyncEnumerable> GetAllAsync(string collection, + [EnumeratorCancellation] CancellationToken cancel = default) + { + await foreach (DatabaseEntry dbEntry in this._dbConnection.ReadAllAsync(collection, cancel)) + { + yield return DataEntry.Create(dbEntry.Key, dbEntry.Value, ParseTimestamp(dbEntry.Timestamp)); + } + } + + /// + public async Task?> GetAsync(string collection, string key, CancellationToken cancel = default) + { + DatabaseEntry? entry = await this._dbConnection.ReadAsync(collection, key, cancel); + if (entry.HasValue) + { + DatabaseEntry dbEntry = entry.Value; + return DataEntry.Create(dbEntry.Key, dbEntry.Value, ParseTimestamp(dbEntry.Timestamp)); + } + + return null; + } + + /// + public async Task> PutAsync(string collection, DataEntry data, CancellationToken cancel = default) + { + await this._dbConnection.InsertAsync(collection, data.Key, data.ValueString, ToTimestampString(data.Timestamp), cancel); + return data; + } + + /// + public Task RemoveAsync(string collection, string key, CancellationToken cancel = default) + { + return this._dbConnection.DeleteAsync(collection, key, cancel); + } + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + this.Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + #region protected ================================================================================ + + protected virtual void Dispose(bool disposing) + { + if (!this._disposedValue) + { + if (disposing) + { + this._dbConnection.Dispose(); + } + + this._disposedValue = true; + } + } + + #endregion + + #region private ================================================================================ + + private readonly SqliteConnection _dbConnection; + private bool _disposedValue; + + /// + /// Constructor + /// + /// DB connection + private SqliteDataStore(SqliteConnection dbConnection) + { + this._dbConnection = dbConnection; + } + + // TODO: never used + private static string? ValueToString(TValue? value) + { + if (value != null) + { + if (typeof(TValue) == typeof(string)) + { + return value.ToString(); + } + + return JsonSerializer.Serialize(value); + } + + return null; + } + + private static string? ToTimestampString(DateTimeOffset? timestamp) + { + return timestamp?.ToString("u", CultureInfo.InvariantCulture); + } + + private static DateTimeOffset? ParseTimestamp(string? str) + { + if (!string.IsNullOrEmpty(str) + && DateTimeOffset.TryParse(str, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out DateTimeOffset timestamp)) + { + return timestamp; + } + + return null; + } + + #endregion +} diff --git a/dotnet/src/SemanticKernel.Skills/Skills.Test/Document/DocumentSkillTests.cs b/dotnet/src/SemanticKernel.Skills/Skills.Test/Document/DocumentSkillTests.cs new file mode 100644 index 000000000000..dce9440589d2 --- /dev/null +++ b/dotnet/src/SemanticKernel.Skills/Skills.Test/Document/DocumentSkillTests.cs @@ -0,0 +1,141 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.SemanticKernel.Connectors.Interfaces; +using Microsoft.SemanticKernel.Memory; +using Microsoft.SemanticKernel.Orchestration; +using Microsoft.SemanticKernel.Skills.Document; +using Moq; +using Xunit; +using static Microsoft.SemanticKernel.Skills.Document.DocumentSkill; + +namespace SemanticKernelSkills.Test.Document; + +public class DocumentSkillTests +{ + private readonly SKContext _context = new(new ContextVariables(), NullMemory.Instance, null, NullLogger.Instance); + + [Fact] + public async Task ReadTextAsyncSucceedsAsync() + { + // Arrange + var expectedText = Guid.NewGuid().ToString(); + var anyFilePath = Guid.NewGuid().ToString(); + + var fileSystemConnectorMock = new Mock(); + fileSystemConnectorMock + .Setup(mock => mock.GetFileContentStreamAsync(It.Is(filePath => filePath.Equals(anyFilePath, StringComparison.Ordinal)), + It.IsAny())) + .ReturnsAsync(Stream.Null); + + var documentConnectorMock = new Mock(); + documentConnectorMock + .Setup(mock => mock.ReadText(It.IsAny())) + .Returns(expectedText); + + var target = new DocumentSkill(documentConnectorMock.Object, fileSystemConnectorMock.Object); + + // Act + string actual = await target.ReadTextAsync(anyFilePath, this._context); + + // Assert + Assert.Equal(expectedText, actual); + Assert.False(this._context.ErrorOccurred); + fileSystemConnectorMock.VerifyAll(); + documentConnectorMock.VerifyAll(); + } + + [Fact] + public async Task AppendTextAsyncFileExistsSucceedsAsync() + { + // Arrange + var anyText = Guid.NewGuid().ToString(); + var anyFilePath = Guid.NewGuid().ToString(); + + var fileSystemConnectorMock = new Mock(); + fileSystemConnectorMock + .Setup(mock => mock.FileExistsAsync(It.Is(filePath => filePath.Equals(anyFilePath, StringComparison.Ordinal)), + It.IsAny())) + .ReturnsAsync(true); + fileSystemConnectorMock + .Setup(mock => mock.GetWriteableFileStreamAsync(It.Is(filePath => filePath.Equals(anyFilePath, StringComparison.Ordinal)), + It.IsAny())) + .ReturnsAsync(Stream.Null); + + var documentConnectorMock = new Mock(); + documentConnectorMock + .Setup(mock => mock.AppendText(It.IsAny(), It.Is(text => text.Equals(anyText, StringComparison.Ordinal)))); + + var target = new DocumentSkill(documentConnectorMock.Object, fileSystemConnectorMock.Object); + + this._context.Variables.Set(Parameters.FilePath, anyFilePath); + + // Act + await target.AppendTextAsync(anyText, this._context); + + // Assert + Assert.False(this._context.ErrorOccurred); + fileSystemConnectorMock.VerifyAll(); + documentConnectorMock.VerifyAll(); + } + + [Fact] + public async Task AppendTextAsyncFileDoesNotExistSucceedsAsync() + { + // Arrange + var anyText = Guid.NewGuid().ToString(); + var anyFilePath = Guid.NewGuid().ToString(); + + var fileSystemConnectorMock = new Mock(); + fileSystemConnectorMock + .Setup(mock => mock.FileExistsAsync(It.Is(filePath => filePath.Equals(anyFilePath, StringComparison.Ordinal)), + It.IsAny())) + .ReturnsAsync(false); + fileSystemConnectorMock + .Setup(mock => mock.CreateFileAsync(It.Is(filePath => filePath.Equals(anyFilePath, StringComparison.Ordinal)), + It.IsAny())) + .ReturnsAsync(Stream.Null); + + var documentConnectorMock = new Mock(); + documentConnectorMock + .Setup(mock => mock.Initialize(It.IsAny())); + documentConnectorMock + .Setup(mock => mock.AppendText(It.IsAny(), It.Is(text => text.Equals(anyText, StringComparison.Ordinal)))); + + var target = new DocumentSkill(documentConnectorMock.Object, fileSystemConnectorMock.Object); + + this._context.Variables.Set(Parameters.FilePath, anyFilePath); + + // Act + await target.AppendTextAsync(anyText, this._context); + + // Assert + Assert.False(this._context.ErrorOccurred); + fileSystemConnectorMock.VerifyAll(); + documentConnectorMock.VerifyAll(); + } + + [Fact] + public async Task AppendTextAsyncNoFilePathFailsAsync() + { + // Arrange + var anyText = Guid.NewGuid().ToString(); + + var fileSystemConnectorMock = new Mock(); + var documentConnectorMock = new Mock(); + + var target = new DocumentSkill(documentConnectorMock.Object, fileSystemConnectorMock.Object); + + // Act + await target.AppendTextAsync(anyText, this._context); + + // Assert + Assert.True(this._context.ErrorOccurred); + fileSystemConnectorMock.Verify(mock => mock.GetWriteableFileStreamAsync(It.IsAny(), It.IsAny()), Times.Never()); + documentConnectorMock.Verify(mock => mock.AppendText(It.IsAny(), It.IsAny()), Times.Never()); + } +} diff --git a/dotnet/src/SemanticKernel.Skills/Skills.Test/Productivity/CalendarSkillTests.cs b/dotnet/src/SemanticKernel.Skills/Skills.Test/Productivity/CalendarSkillTests.cs new file mode 100644 index 000000000000..becc15414327 --- /dev/null +++ b/dotnet/src/SemanticKernel.Skills/Skills.Test/Productivity/CalendarSkillTests.cs @@ -0,0 +1,266 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Globalization; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.SemanticKernel.Connectors.Interfaces; +using Microsoft.SemanticKernel.Connectors.Interfaces.Models; +using Microsoft.SemanticKernel.Memory; +using Microsoft.SemanticKernel.Orchestration; +using Microsoft.SemanticKernel.Skills.Productivity; +using Moq; +using Xunit; +using Xunit.Abstractions; +using static Microsoft.SemanticKernel.Skills.Productivity.CalendarSkill; + +namespace SemanticKernelSkills.Test.Productivity; + +public class CalendarSkillTests : IDisposable +{ + private readonly XunitLogger _logger; + private readonly SKContext _context; + + public CalendarSkillTests(ITestOutputHelper output) + { + this._logger = new XunitLogger(output); + this._context = new SKContext(new ContextVariables(), NullMemory.Instance, null, this._logger); + } + + [Fact] + public async Task AddEventAsyncSucceedsAsync() + { + // Arrange + string anyContent = Guid.NewGuid().ToString(); + string anySubject = Guid.NewGuid().ToString(); + string anyLocation = Guid.NewGuid().ToString(); + DateTimeOffset anyStartTime = DateTimeOffset.Now + TimeSpan.FromDays(1); + DateTimeOffset anyEndTime = DateTimeOffset.Now + TimeSpan.FromDays(1.1); + string[] anyAttendees = new[] { Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString() }; + + CalendarEvent expected = new(anySubject, anyStartTime, anyEndTime) + { + Content = anyContent, + Location = anyLocation, + Attendees = anyAttendees + }; + + Mock connectorMock = new(); + connectorMock.Setup(c => c.AddEventAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(expected); + + CalendarSkill target = new(connectorMock.Object); + + this._context.Variables.Set(Parameters.Start, anyStartTime.ToString(CultureInfo.InvariantCulture.DateTimeFormat)); + this._context.Variables.Set(Parameters.End, anyEndTime.ToString(CultureInfo.InvariantCulture.DateTimeFormat)); + this._context.Variables.Set(Parameters.Location, anyLocation); + this._context.Variables.Set(Parameters.Content, anyContent); + this._context.Variables.Set(Parameters.Attendees, string.Join(";", anyAttendees)); + + // Act + await target.AddEventAsync(anySubject, this._context); + + // Assert + Assert.False(this._context.ErrorOccurred); + connectorMock.VerifyAll(); + } + + [Fact] + public async Task AddEventAsyncWithoutLocationSucceedsAsync() + { + // Arrange + string anyContent = Guid.NewGuid().ToString(); + string anySubject = Guid.NewGuid().ToString(); + DateTimeOffset anyStartTime = DateTimeOffset.Now + TimeSpan.FromDays(1); + DateTimeOffset anyEndTime = DateTimeOffset.Now + TimeSpan.FromDays(1.1); + string[] anyAttendees = new[] { Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString() }; + + CalendarEvent expected = new(anySubject, anyStartTime, anyEndTime) + { + Content = anyContent, + Attendees = anyAttendees + }; + + Mock connectorMock = new(); + connectorMock.Setup(c => c.AddEventAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(expected); + + CalendarSkill target = new(connectorMock.Object); + + this._context.Variables.Set(Parameters.Start, anyStartTime.ToString(CultureInfo.InvariantCulture.DateTimeFormat)); + this._context.Variables.Set(Parameters.End, anyEndTime.ToString(CultureInfo.InvariantCulture.DateTimeFormat)); + this._context.Variables.Set(Parameters.Content, anyContent); + this._context.Variables.Set(Parameters.Attendees, string.Join(";", anyAttendees)); + + // Act + await target.AddEventAsync(anySubject, this._context); + + // Assert + Assert.False(this._context.ErrorOccurred); + connectorMock.VerifyAll(); + } + + [Fact] + public async Task AddEventAsyncWithoutContentSucceedsAsync() + { + // Arrange + string anySubject = Guid.NewGuid().ToString(); + string anyLocation = Guid.NewGuid().ToString(); + DateTimeOffset anyStartTime = DateTimeOffset.Now + TimeSpan.FromDays(1); + DateTimeOffset anyEndTime = DateTimeOffset.Now + TimeSpan.FromDays(1.1); + string[] anyAttendees = new[] { Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString() }; + + CalendarEvent expected = new(anySubject, anyStartTime, anyEndTime) + { + Location = anyLocation, + Attendees = anyAttendees + }; + + Mock connectorMock = new(); + connectorMock.Setup(c => c.AddEventAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(expected); + + CalendarSkill target = new(connectorMock.Object); + + this._context.Variables.Set(Parameters.Start, anyStartTime.ToString(CultureInfo.InvariantCulture.DateTimeFormat)); + this._context.Variables.Set(Parameters.End, anyEndTime.ToString(CultureInfo.InvariantCulture.DateTimeFormat)); + this._context.Variables.Set(Parameters.Location, anyLocation); + this._context.Variables.Set(Parameters.Attendees, string.Join(";", anyAttendees)); + + // Act + await target.AddEventAsync(anySubject, this._context); + + // Assert + Assert.False(this._context.ErrorOccurred); + connectorMock.VerifyAll(); + } + + [Fact] + public async Task AddEventAsyncWithoutAttendeesSucceedsAsync() + { + // Arrange + string anyContent = Guid.NewGuid().ToString(); + string anySubject = Guid.NewGuid().ToString(); + string anyLocation = Guid.NewGuid().ToString(); + DateTimeOffset anyStartTime = DateTimeOffset.Now + TimeSpan.FromDays(1); + DateTimeOffset anyEndTime = DateTimeOffset.Now + TimeSpan.FromDays(1.1); + + CalendarEvent expected = new(anySubject, anyStartTime, anyEndTime) + { + Content = anyContent, + Location = anyLocation + }; + + Mock connectorMock = new(); + connectorMock.Setup(c => c.AddEventAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(expected); + + CalendarSkill target = new(connectorMock.Object); + + this._context.Variables.Set(Parameters.Start, anyStartTime.ToString(CultureInfo.InvariantCulture.DateTimeFormat)); + this._context.Variables.Set(Parameters.End, anyEndTime.ToString(CultureInfo.InvariantCulture.DateTimeFormat)); + this._context.Variables.Set(Parameters.Location, anyLocation); + this._context.Variables.Set(Parameters.Content, anyContent); + + // Act + await target.AddEventAsync(anySubject, this._context); + + // Assert + Assert.False(this._context.ErrorOccurred); + connectorMock.VerifyAll(); + } + + [Fact] + public async Task AddEventAsyncWithoutStartFailsAsync() + { + // Arrange + string anyContent = Guid.NewGuid().ToString(); + string anySubject = Guid.NewGuid().ToString(); + string anyLocation = Guid.NewGuid().ToString(); + DateTimeOffset anyEndTime = DateTimeOffset.Now + TimeSpan.FromDays(1.1); + string[] anyAttendees = new[] { Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString() }; + + Mock connectorMock = new(); + + CalendarSkill target = new(connectorMock.Object); + + this._context.Variables.Set(Parameters.End, anyEndTime.ToString(CultureInfo.InvariantCulture.DateTimeFormat)); + this._context.Variables.Set(Parameters.Location, anyLocation); + this._context.Variables.Set(Parameters.Content, anyContent); + this._context.Variables.Set(Parameters.Attendees, string.Join(";", anyAttendees)); + + // Act + await target.AddEventAsync(anySubject, this._context); + + // Assert + Assert.True(this._context.ErrorOccurred); + } + + [Fact] + public async Task AddEventAsyncWithoutEndFailsAsync() + { + // Arrange + string anyContent = Guid.NewGuid().ToString(); + string anySubject = Guid.NewGuid().ToString(); + string anyLocation = Guid.NewGuid().ToString(); + DateTimeOffset anyStartTime = DateTimeOffset.Now + TimeSpan.FromDays(1); + string[] anyAttendees = new[] { Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString() }; + + Mock connectorMock = new(); + + CalendarSkill target = new(connectorMock.Object); + + this._context.Variables.Set(Parameters.Start, anyStartTime.ToString(CultureInfo.InvariantCulture.DateTimeFormat)); + this._context.Variables.Set(Parameters.Location, anyLocation); + this._context.Variables.Set(Parameters.Content, anyContent); + this._context.Variables.Set(Parameters.Attendees, string.Join(";", anyAttendees)); + + // Act + await target.AddEventAsync(anySubject, this._context); + + // Assert + Assert.True(this._context.ErrorOccurred); + } + + [Fact] + public async Task AddEventAsyncWithoutSubjectFailsAsync() + { + // Arrange + string anyContent = Guid.NewGuid().ToString(); + string anyLocation = Guid.NewGuid().ToString(); + DateTimeOffset anyStartTime = DateTimeOffset.Now + TimeSpan.FromDays(1); + DateTimeOffset anyEndTime = DateTimeOffset.Now + TimeSpan.FromDays(1.1); + string[] anyAttendees = new[] { Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString() }; + + Mock connectorMock = new(); + + CalendarSkill target = new(connectorMock.Object); + + this._context.Variables.Set(Parameters.Start, anyStartTime.ToString(CultureInfo.InvariantCulture.DateTimeFormat)); + this._context.Variables.Set(Parameters.End, anyEndTime.ToString(CultureInfo.InvariantCulture.DateTimeFormat)); + this._context.Variables.Set(Parameters.Location, anyLocation); + this._context.Variables.Set(Parameters.Content, anyContent); + this._context.Variables.Set(Parameters.Attendees, string.Join(";", anyAttendees)); + + // Act + await target.AddEventAsync(string.Empty, this._context); + + // Assert + Assert.True(this._context.ErrorOccurred); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + this._logger.Dispose(); + } + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + this.Dispose(disposing: true); + GC.SuppressFinalize(this); + } +} diff --git a/dotnet/src/SemanticKernel.Skills/Skills.Test/Productivity/CloudDriveSkillTests.cs b/dotnet/src/SemanticKernel.Skills/Skills.Test/Productivity/CloudDriveSkillTests.cs new file mode 100644 index 000000000000..5ec3dc4ed86d --- /dev/null +++ b/dotnet/src/SemanticKernel.Skills/Skills.Test/Productivity/CloudDriveSkillTests.cs @@ -0,0 +1,113 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.SemanticKernel.Connectors.Interfaces; +using Microsoft.SemanticKernel.Memory; +using Microsoft.SemanticKernel.Orchestration; +using Microsoft.SemanticKernel.Skills.Productivity; +using Moq; +using Xunit; +using Xunit.Abstractions; +using static Microsoft.SemanticKernel.Skills.Productivity.CloudDriveSkill; + +namespace SemanticKernelSkills.Test.Productivity; + +public class CloudDriveSkillTests : IDisposable +{ + private readonly XunitLogger _logger; + private readonly SKContext _context; + private bool _disposedValue = false; + + public CloudDriveSkillTests(ITestOutputHelper output) + { + this._logger = new XunitLogger(output); + this._context = new SKContext(new ContextVariables(), NullMemory.Instance, null, this._logger, CancellationToken.None); + } + + [Fact] + public async Task UploadSmallFileAsyncSucceedsAsync() + { + // Arrange + string anyFilePath = Guid.NewGuid().ToString(); + + Mock connectorMock = new Mock(); + connectorMock.Setup(c => c.UploadSmallFileAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(Task.CompletedTask); + + this._context.Variables.Set(Parameters.DestinationPath, Guid.NewGuid().ToString()); + CloudDriveSkill target = new CloudDriveSkill(connectorMock.Object); + + // Act + await target.UploadFileAsync(anyFilePath, this._context); + + // Assert + connectorMock.VerifyAll(); + } + + [Fact] + public async Task CreateLinkAsyncSucceedsAsync() + { + // Arrange + string anyFilePath = Guid.NewGuid().ToString(); + string anyLink = Guid.NewGuid().ToString(); + + Mock connectorMock = new Mock(); + connectorMock.Setup(c => c.CreateShareLinkAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(anyLink); + + CloudDriveSkill target = new CloudDriveSkill(connectorMock.Object); + + // Act + string actual = await target.CreateLinkAsync(anyFilePath, this._context); + + // Assert + Assert.Equal(anyLink, actual); + connectorMock.VerifyAll(); + } + + [Fact] + public async Task GetFileContentAsyncSucceedsAsync() + { + string anyFilePath = Guid.NewGuid().ToString(); + string expectedContent = Guid.NewGuid().ToString(); + using MemoryStream expectedStream = new MemoryStream(Encoding.UTF8.GetBytes(expectedContent)); + + // Arrange + Mock connectorMock = new Mock(); + connectorMock.Setup(c => c.GetFileContentStreamAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(expectedStream); + + CloudDriveSkill target = new CloudDriveSkill(connectorMock.Object); + + // Act + string actual = await target.GetFileContentAsync(anyFilePath, this._context); + + // Assert + Assert.Equal(expectedContent, actual); + connectorMock.VerifyAll(); + } + + protected virtual void Dispose(bool disposing) + { + if (!this._disposedValue) + { + if (disposing) + { + this._logger.Dispose(); + } + + this._disposedValue = true; + } + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + this.Dispose(disposing: true); + GC.SuppressFinalize(this); + } +} diff --git a/dotnet/src/SemanticKernel.Skills/Skills.Test/Productivity/EmailSkillTests.cs b/dotnet/src/SemanticKernel.Skills/Skills.Test/Productivity/EmailSkillTests.cs new file mode 100644 index 000000000000..f18f02c6eae9 --- /dev/null +++ b/dotnet/src/SemanticKernel.Skills/Skills.Test/Productivity/EmailSkillTests.cs @@ -0,0 +1,107 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.SemanticKernel.Connectors.Interfaces; +using Microsoft.SemanticKernel.Memory; +using Microsoft.SemanticKernel.Orchestration; +using Microsoft.SemanticKernel.Skills.Productivity; +using Moq; +using Xunit; +using static Microsoft.SemanticKernel.Skills.Productivity.EmailSkill; + +namespace SemanticKernelSkills.Test.Productivity; + +public class EmailSkillTests +{ + private readonly SKContext _context = new SKContext(new ContextVariables(), NullMemory.Instance, null, NullLogger.Instance); + + [Fact] + public async Task SendEmailAsyncSucceedsAsync() + { + // Arrange + Mock connectorMock = new(); + connectorMock.Setup(c => c.SendEmailAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(Task.CompletedTask); + + EmailSkill target = new(connectorMock.Object); + + string anyContent = Guid.NewGuid().ToString(); + string anySubject = Guid.NewGuid().ToString(); + string anyRecipient = Guid.NewGuid().ToString(); + + this._context.Variables.Set(Parameters.Recipients, anyRecipient); + this._context.Variables.Set(Parameters.Subject, anySubject); + + // Act + await target.SendEmailAsync(anyContent, this._context); + + // Assert + Assert.False(this._context.ErrorOccurred); + connectorMock.VerifyAll(); + } + + [Fact] + public async Task SendEmailAsyncNoRecipientFailsAsync() + { + // Arrange + Mock connectorMock = new(); + EmailSkill target = new(connectorMock.Object); + + string anyContent = Guid.NewGuid().ToString(); + string anySubject = Guid.NewGuid().ToString(); + + this._context.Variables.Set(Parameters.Subject, anySubject); + this._context.Variables.Update(anyContent); + + // Act + await target.SendEmailAsync(anyContent, this._context); + + // Assert + Assert.True(this._context.ErrorOccurred); + connectorMock.VerifyAll(); + } + + [Fact] + public async Task SendEmailAsyncNoSubjectFailsAsync() + { + // Arrange + Mock connectorMock = new(); + EmailSkill target = new(connectorMock.Object); + + string anyContent = Guid.NewGuid().ToString(); + string anyRecipient = Guid.NewGuid().ToString(); + + this._context.Variables.Set(Parameters.Recipients, anyRecipient); + this._context.Variables.Update(anyContent); + + // Act + await target.SendEmailAsync(anyContent, this._context); + + // Assert + Assert.True(this._context.ErrorOccurred); + connectorMock.VerifyAll(); + } + + [Fact] + public async Task GetMyEmailAddressAsyncSucceedsAsync() + { + // Arrange + string anyEmailAddress = Guid.NewGuid().ToString(); + Mock connectorMock = new(); + connectorMock.Setup(c => c.GetMyEmailAddressAsync(It.IsAny())) + .ReturnsAsync(anyEmailAddress); + + EmailSkill target = new(connectorMock.Object); + + // Act + string actual = await target.GetMyEmailAddressAsync(); + + // Assert + Assert.Equal(anyEmailAddress, actual); + Assert.False(this._context.ErrorOccurred); + connectorMock.VerifyAll(); + } +} diff --git a/dotnet/src/SemanticKernel.Skills/Skills.Test/Productivity/OrganizationHierarchySkillTests.cs b/dotnet/src/SemanticKernel.Skills/Skills.Test/Productivity/OrganizationHierarchySkillTests.cs new file mode 100644 index 000000000000..352035813ccb --- /dev/null +++ b/dotnet/src/SemanticKernel.Skills/Skills.Test/Productivity/OrganizationHierarchySkillTests.cs @@ -0,0 +1,81 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.SemanticKernel.Connectors.Interfaces; +using Microsoft.SemanticKernel.Memory; +using Microsoft.SemanticKernel.Orchestration; +using Microsoft.SemanticKernel.Skills.Productivity; +using Moq; +using Xunit; +using Xunit.Abstractions; + +namespace SemanticKernelSkills.Test.Productivity; + +public class OrganizationHierarchySkillTests : IDisposable +{ + private readonly XunitLogger _logger; + private readonly SKContext _context; + private bool _disposedValue = false; + + public OrganizationHierarchySkillTests(ITestOutputHelper output) + { + this._logger = new XunitLogger(output); + this._context = new SKContext(new ContextVariables(), NullMemory.Instance, null, this._logger, CancellationToken.None); + } + + [Fact] + public async Task GetMyManagerEmailAsyncSucceedsAsync() + { + // Arrange + string anyManagerEmail = Guid.NewGuid().ToString(); + Mock connectorMock = new Mock(); + connectorMock.Setup(c => c.GetManagerEmailAsync(It.IsAny())).ReturnsAsync(anyManagerEmail); + OrganizationHierarchySkill target = new OrganizationHierarchySkill(connectorMock.Object); + + // Act + string actual = await target.GetMyManagerEmailAsync(this._context); + + // Assert + Assert.Equal(anyManagerEmail, actual); + connectorMock.VerifyAll(); + } + + [Fact] + public async Task GetMyManagerNameAsyncSucceedsAsync() + { + // Arrange + string anyManagerName = Guid.NewGuid().ToString(); + Mock connectorMock = new Mock(); + connectorMock.Setup(c => c.GetManagerNameAsync(It.IsAny())).ReturnsAsync(anyManagerName); + OrganizationHierarchySkill target = new OrganizationHierarchySkill(connectorMock.Object); + + // Act + string actual = await target.GetMyManagerNameAsync(this._context); + + // Assert + Assert.Equal(anyManagerName, actual); + connectorMock.VerifyAll(); + } + + protected virtual void Dispose(bool disposing) + { + if (!this._disposedValue) + { + if (disposing) + { + this._logger.Dispose(); + } + + this._disposedValue = true; + } + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + this.Dispose(disposing: true); + GC.SuppressFinalize(this); + } +} diff --git a/dotnet/src/SemanticKernel.Skills/Skills.Test/Productivity/TaskListSkillTests.cs b/dotnet/src/SemanticKernel.Skills/Skills.Test/Productivity/TaskListSkillTests.cs new file mode 100644 index 000000000000..cb74801d71ee --- /dev/null +++ b/dotnet/src/SemanticKernel.Skills/Skills.Test/Productivity/TaskListSkillTests.cs @@ -0,0 +1,132 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.SemanticKernel.Connectors.Interfaces; +using Microsoft.SemanticKernel.Connectors.Interfaces.Models; +using Microsoft.SemanticKernel.Memory; +using Microsoft.SemanticKernel.Orchestration; +using Microsoft.SemanticKernel.Skills.Productivity; +using Moq; +using Xunit; +using static Microsoft.SemanticKernel.Skills.Productivity.TaskListSkill; + +namespace SemanticKernelSkills.Test.Productivity; + +public class TaskListSkillTests +{ + private readonly SKContext _context = new SKContext(new ContextVariables(), NullMemory.Instance, null, NullLogger.Instance); + + private readonly TaskManagementTaskList _anyTaskList = new TaskManagementTaskList( + id: Guid.NewGuid().ToString(), + name: Guid.NewGuid().ToString()); + + private readonly TaskManagementTask _anyTask = new TaskManagementTask( + id: Guid.NewGuid().ToString(), + title: Guid.NewGuid().ToString(), + reminder: (DateTimeOffset.Now + TimeSpan.FromDays(1)).ToString("o"), + due: DateTimeOffset.Now.ToString("o"), + isCompleted: false); + + [Fact] + public async Task AddTaskAsyncNoReminderSucceedsAsync() + { + // Arrange + string anyTitle = Guid.NewGuid().ToString(); + + Mock connectorMock = new Mock(); + connectorMock.Setup(c => c.GetDefaultTaskListAsync(It.IsAny())) + .ReturnsAsync(this._anyTaskList); + + connectorMock.Setup(c => c.AddTaskAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(this._anyTask); + + TaskListSkill target = new TaskListSkill(connectorMock.Object); + + // Verify no reminder is set + Assert.False(this._context.Variables.Get(Parameters.Reminder, out _)); + + // Act + await target.AddTaskAsync(anyTitle, this._context); + + // Assert + Assert.False(this._context.ErrorOccurred); + connectorMock.VerifyAll(); + } + + [Fact] + public async Task AddTaskAsyncWithReminderSucceedsAsync() + { + // Arrange + string anyTitle = Guid.NewGuid().ToString(); + + Mock connectorMock = new Mock(); + connectorMock.Setup(c => c.GetDefaultTaskListAsync(It.IsAny())) + .ReturnsAsync(this._anyTaskList); + + connectorMock.Setup(c => c.AddTaskAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(this._anyTask); + + string anyReminder = (DateTimeOffset.Now + TimeSpan.FromHours(1)).ToString("o"); + + TaskListSkill target = new TaskListSkill(connectorMock.Object); + this._context.Variables.Set(Parameters.Reminder, anyReminder); + + // Act + await target.AddTaskAsync(anyTitle, this._context); + + // Assert + Assert.False(this._context.ErrorOccurred); + connectorMock.VerifyAll(); + } + + [Fact] + public async Task AddTaskAsyncNoDefaultTaskListFailsAsync() + { + // Arrange + string anyTitle = Guid.NewGuid().ToString(); + + Mock connectorMock = new Mock(); +#pragma warning disable CS8600 // Converting null literal or possible null value to non-nullable type. + connectorMock.Setup(c => c.GetDefaultTaskListAsync(It.IsAny())) + .ReturnsAsync((TaskManagementTaskList)null); +#pragma warning restore CS8600 // Converting null literal or possible null value to non-nullable type. + + string anyReminder = (DateTimeOffset.Now + TimeSpan.FromHours(1)).ToString("o"); + + TaskListSkill target = new TaskListSkill(connectorMock.Object); + this._context.Variables.Set(Parameters.Reminder, anyReminder); + + // Act + await target.AddTaskAsync(anyTitle, this._context); + + // Assert + Assert.True(this._context.ErrorOccurred); + connectorMock.VerifyAll(); + } + + [Theory] + [InlineData(DayOfWeek.Sunday)] + [InlineData(DayOfWeek.Monday)] + [InlineData(DayOfWeek.Tuesday)] + [InlineData(DayOfWeek.Wednesday)] + [InlineData(DayOfWeek.Thursday)] + [InlineData(DayOfWeek.Friday)] + [InlineData(DayOfWeek.Saturday)] + public void GetNextDayOfWeekIsCorrect(DayOfWeek dayOfWeek) + { + // Arrange + DateTimeOffset today = new DateTimeOffset(DateTime.Today); + TimeSpan timeOfDay = TimeSpan.FromHours(13); + + // Act + DateTimeOffset actual = GetNextDayOfWeek(dayOfWeek, timeOfDay); + + // Assert + Assert.Equal(dayOfWeek, actual.DayOfWeek); + Assert.True(today.ToUnixTimeSeconds() < actual.ToUnixTimeSeconds()); + Assert.Equal(timeOfDay.Hours, actual.Hour); + } +} diff --git a/dotnet/src/SemanticKernel.Skills/Skills.Test/SemanticKernel.Skills.Test.csproj b/dotnet/src/SemanticKernel.Skills/Skills.Test/SemanticKernel.Skills.Test.csproj new file mode 100644 index 000000000000..b8ce2f613b4e --- /dev/null +++ b/dotnet/src/SemanticKernel.Skills/Skills.Test/SemanticKernel.Skills.Test.csproj @@ -0,0 +1,28 @@ + + + + SemanticKernelSkills.Test + SemanticKernelSkills.Test + net6.0 + 10 + enable + disable + false + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + diff --git a/dotnet/src/SemanticKernel.Skills/Skills.Test/Web/SearchUrlSkillTests.cs b/dotnet/src/SemanticKernel.Skills/Skills.Test/Web/SearchUrlSkillTests.cs new file mode 100644 index 000000000000..15e23d0cd26d --- /dev/null +++ b/dotnet/src/SemanticKernel.Skills/Skills.Test/Web/SearchUrlSkillTests.cs @@ -0,0 +1,187 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Text.Encodings.Web; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Skills.Web; +using Xunit; + +namespace SemanticKernelSkills.Test.Web; + +public class SearchUrlSkillTests +{ + private const string AnyInput = ""; + private readonly string _encodedInput = UrlEncoder.Default.Encode(AnyInput); + + [Fact] + public void ItCanBeInstantiated() + { + // Act - Assert no exception occurs + var _ = new SearchUrlSkill(); + } + + [Fact] + public void ItCanBeImported() + { + // Arrange + IKernel kernel = KernelBuilder.Create(); + + // Act - Assert no exception occurs e.g. due to reflection + kernel.ImportSkill(new SearchUrlSkill(), "search"); + } + + [Fact] + public void AmazonSearchUrlSucceeds() + { + // Arrange + var skill = new SearchUrlSkill(); + + // Act + string actual = skill.AmazonSearchUrl(AnyInput); + + // Assert + Assert.Equal($"https://www.amazon.com/s?k={this._encodedInput}", actual); + } + + [Fact] + public void BingSearchUrlSucceeds() + { + // Arrange + var skill = new SearchUrlSkill(); + + // Act + string actual = skill.BingSearchUrl(AnyInput); + + // Assert + Assert.Equal($"https://www.bing.com/search?q={this._encodedInput}", actual); + } + + [Fact] + public void BingImagesSearchUrlSucceeds() + { + // Arrange + var skill = new SearchUrlSkill(); + + // Act + string actual = skill.BingImagesSearchUrl(AnyInput); + + // Assert + Assert.Equal($"https://www.bing.com/images/search?q={this._encodedInput}", actual); + } + + [Fact] + public void BingMapsSearchUrl() + { + // Arrange + var skill = new SearchUrlSkill(); + + // Act + string actual = skill.BingMapsSearchUrl(AnyInput); + + // Assert + Assert.Equal($"https://www.bing.com/maps?q={this._encodedInput}", actual); + } + + [Fact] + public void BingShoppingSearchUrl() + { + // Arrange + var skill = new SearchUrlSkill(); + + // Act + string actual = skill.BingShoppingSearchUrl(AnyInput); + + // Assert + Assert.Equal($"https://www.bing.com/shop?q={this._encodedInput}", actual); + } + + [Fact] + public void BingNewsSearchUrl() + { + // Arrange + var skill = new SearchUrlSkill(); + + // Act + string actual = skill.BingNewsSearchUrl(AnyInput); + + // Assert + Assert.Equal($"https://www.bing.com/news/search?q={this._encodedInput}", actual); + } + + [Fact] + public void BingTravelSearchUrl() + { + // Arrange + var skill = new SearchUrlSkill(); + + // Act + string actual = skill.BingTravelSearchUrl(AnyInput); + + // Assert + Assert.Equal($"https://www.bing.com/travel/search?q={this._encodedInput}", actual); + } + + [Fact] + public void FacebookSearchUrl() + { + // Arrange + var skill = new SearchUrlSkill(); + + // Act + string actual = skill.FacebookSearchUrl(AnyInput); + + // Assert + Assert.Equal($"https://www.facebook.com/search/top/?q={this._encodedInput}", actual); + } + + [Fact] + public void GitHubSearchUrl() + { + // Arrange + var skill = new SearchUrlSkill(); + + // Act + string actual = skill.GitHubSearchUrl(AnyInput); + + // Assert + Assert.Equal($"https://github.com/search?q={this._encodedInput}", actual); + } + + [Fact] + public void LinkedInSearchUrl() + { + // Arrange + var skill = new SearchUrlSkill(); + + // Act + string actual = skill.LinkedInSearchUrl(AnyInput); + + // Assert + Assert.Equal($"https://www.linkedin.com/search/results/index/?keywords={this._encodedInput}", actual); + } + + [Fact] + public void TwitterSearchUrl() + { + // Arrange + var skill = new SearchUrlSkill(); + + // Act + string actual = skill.TwitterSearchUrl(AnyInput); + + // Assert + Assert.Equal($"https://twitter.com/search?q={this._encodedInput}", actual); + } + + [Fact] + public void WikipediaSearchUrl() + { + // Arrange + var skill = new SearchUrlSkill(); + + // Act + string actual = skill.WikipediaSearchUrl(AnyInput); + + // Assert + Assert.Equal($"https://wikipedia.org/w/index.php?search={this._encodedInput}", actual); + } +} diff --git a/dotnet/src/SemanticKernel.Skills/Skills.Test/Web/WebSearchEngineSkillTests.cs b/dotnet/src/SemanticKernel.Skills/Skills.Test/Web/WebSearchEngineSkillTests.cs new file mode 100644 index 000000000000..9597261bb865 --- /dev/null +++ b/dotnet/src/SemanticKernel.Skills/Skills.Test/Web/WebSearchEngineSkillTests.cs @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.SemanticKernel.Connectors.Interfaces; +using Microsoft.SemanticKernel.Memory; +using Microsoft.SemanticKernel.Orchestration; +using Microsoft.SemanticKernel.Skills.Web; +using Moq; +using Xunit; +using Xunit.Abstractions; + +namespace SemanticKernelSkills.Test.Web; + +public sealed class WebSearchEngineSkillTests : IDisposable +{ + private readonly SKContext _context; + private readonly XunitLogger _logger; + + public WebSearchEngineSkillTests(ITestOutputHelper output) + { + this._logger = new XunitLogger(output); + this._context = new SKContext(new ContextVariables(), NullMemory.Instance, null, this._logger); + } + + [Fact] + public async Task SearchAsyncSucceedsAsync() + { + // Arrange + string expected = Guid.NewGuid().ToString(); + + Mock connectorMock = new(); + connectorMock.Setup(c => c.SearchAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(expected); + + WebSearchEngineSkill target = new(connectorMock.Object); + + string anyQuery = Guid.NewGuid().ToString(); + + // Act + await target.SearchAsync(anyQuery, this._context); + + // Assert + Assert.False(this._context.ErrorOccurred); + connectorMock.VerifyAll(); + } + + public void Dispose() + { + this._logger.Dispose(); + } +} diff --git a/dotnet/src/SemanticKernel.Skills/Skills.Test/XunitLogger.cs b/dotnet/src/SemanticKernel.Skills/Skills.Test/XunitLogger.cs new file mode 100644 index 000000000000..bba63b3850f5 --- /dev/null +++ b/dotnet/src/SemanticKernel.Skills/Skills.Test/XunitLogger.cs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using Microsoft.Extensions.Logging; +using Xunit.Abstractions; + +namespace SemanticKernelSkills.Test; + +/// +/// A logger that writes to the Xunit test output +/// +internal sealed class XunitLogger : ILogger, IDisposable +{ + private readonly ITestOutputHelper _output; + + public XunitLogger(ITestOutputHelper output) + { + this._output = output; + } + + /// + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + { + this._output.WriteLine(state?.ToString()); + } + + /// + public bool IsEnabled(LogLevel logLevel) => true; + + /// + public IDisposable BeginScope(TState state) where TState : notnull + => this; + + /// + public void Dispose() + { + // This class is marked as disposable to support the BeginScope method. + // However, there is no need to dispose anything. + } +} diff --git a/dotnet/src/SemanticKernel.Skills/Skills/Connectors.Interfaces/ICalendarConnector.cs b/dotnet/src/SemanticKernel.Skills/Skills/Connectors.Interfaces/ICalendarConnector.cs new file mode 100644 index 000000000000..f61790805004 --- /dev/null +++ b/dotnet/src/SemanticKernel.Skills/Skills/Connectors.Interfaces/ICalendarConnector.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.SemanticKernel.Connectors.Interfaces.Models; + +namespace Microsoft.SemanticKernel.Connectors.Interfaces; + +/// +/// Interface for calendar connections (e.g. Outlook). +/// +public interface ICalendarConnector +{ + /// + /// Add a new event to the user's calendar + /// + /// Event to add. + /// Cancellation token + /// Event that was added. + Task AddEventAsync(CalendarEvent calendarEvent, CancellationToken cancellationToken = default); +} diff --git a/dotnet/src/SemanticKernel.Skills/Skills/Connectors.Interfaces/ICloudDriveConnector.cs b/dotnet/src/SemanticKernel.Skills/Skills/Connectors.Interfaces/ICloudDriveConnector.cs new file mode 100644 index 000000000000..b5b3b6066d22 --- /dev/null +++ b/dotnet/src/SemanticKernel.Skills/Skills/Connectors.Interfaces/ICloudDriveConnector.cs @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.SemanticKernel.Connectors.Interfaces; + +/// +/// Interface for cloud drive connections (e.g. OneDrive). +/// +public interface ICloudDriveConnector +{ + /// + /// Create a shareable link to a file. + /// + /// Path to the file. + /// Type of link to create. + /// Scope of the link. + /// Cancellation token. + /// Shareable link. + Task CreateShareLinkAsync(string filePath, string type = "view", string scope = "anonymous", CancellationToken cancellationToken = default); + + /// + /// Get the content of a file. + /// + /// Path to the remote file. + /// Cancellation token. + Task GetFileContentStreamAsync(string filePath, CancellationToken cancellationToken = default); + + /// + /// Upload a small file (less than 4MB). + /// + /// Path of the local file to upload. + /// Remote path to store the file, which is relative to the root of the OneDrive folder and should begin with the '/' character. + /// Cancellation token. + /// + Task UploadSmallFileAsync(string filePath, string destinationPath, CancellationToken cancellationToken = default); +} diff --git a/dotnet/src/SemanticKernel.Skills/Skills/Connectors.Interfaces/IDocumentConnector.cs b/dotnet/src/SemanticKernel.Skills/Skills/Connectors.Interfaces/IDocumentConnector.cs new file mode 100644 index 000000000000..480e537f99b3 --- /dev/null +++ b/dotnet/src/SemanticKernel.Skills/Skills/Connectors.Interfaces/IDocumentConnector.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.IO; + +namespace Microsoft.SemanticKernel.Connectors.Interfaces; + +/// +/// Interface for document connections (e.g. Microsoft Word) +/// +public interface IDocumentConnector +{ + /// + /// Read all text from the document. + /// + /// Document stream + /// String containing all text from the document. + public string ReadText(Stream stream); + + /// + /// Initialize a document from the given stream. + /// + /// IO stream + public void Initialize(Stream stream); + + /// + /// Append the specified text to the document. + /// + /// Document stream + /// String of text to write to the document. + public void AppendText(Stream stream, string text); +} diff --git a/dotnet/src/SemanticKernel.Skills/Skills/Connectors.Interfaces/IEmailConnector.cs b/dotnet/src/SemanticKernel.Skills/Skills/Connectors.Interfaces/IEmailConnector.cs new file mode 100644 index 000000000000..a6b240be3ff6 --- /dev/null +++ b/dotnet/src/SemanticKernel.Skills/Skills/Connectors.Interfaces/IEmailConnector.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.SemanticKernel.Connectors.Interfaces; + +/// +/// Interface for email connections (e.g. Outlook). +/// +public interface IEmailConnector +{ + /// + /// Get the user's email address. + /// + /// Cancellation token. + /// The user's email address. + Task GetMyEmailAddressAsync(CancellationToken cancellationToken = default); + + /// + /// Send an email to the specified recipients. + /// + /// Email subject. + /// Email content. + /// Email recipients. + /// Cancellation token. + Task SendEmailAsync(string subject, string content, string[] recipients, CancellationToken cancellationToken = default); +} diff --git a/dotnet/src/SemanticKernel.Skills/Skills/Connectors.Interfaces/IFileSystemConnector.cs b/dotnet/src/SemanticKernel.Skills/Skills/Connectors.Interfaces/IFileSystemConnector.cs new file mode 100644 index 000000000000..859c0d7ee8ef --- /dev/null +++ b/dotnet/src/SemanticKernel.Skills/Skills/Connectors.Interfaces/IFileSystemConnector.cs @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.SemanticKernel.Connectors.Interfaces; + +/// +/// Interface for filesystem connections. +/// +public interface IFileSystemConnector +{ + /// + /// Get the contents of a file as a read-only stream. + /// + /// Path to the file. + /// Cancellation token. + public Task GetFileContentStreamAsync(string filePath, CancellationToken cancellationToken = default); + + /// + /// Get a writeable stream to an existing file. + /// + /// Path to file. + /// Cancellation token. + public Task GetWriteableFileStreamAsync(string filePath, CancellationToken cancellationToken = default); + + /// + /// Create a new file and get a writeable stream to it. + /// + /// Path to file. + /// Cancellation token. + public Task CreateFileAsync(string filePath, CancellationToken cancellationToken = default); + + /// + /// Determine whether a file exists at the specified path. + /// + /// Path to file. + /// Cancellation token. + /// True if file exists, false otherwise. + public Task FileExistsAsync(string filePath, CancellationToken cancellationToken = default); +} diff --git a/dotnet/src/SemanticKernel.Skills/Skills/Connectors.Interfaces/IOrganizationHierarchyConnector.cs b/dotnet/src/SemanticKernel.Skills/Skills/Connectors.Interfaces/IOrganizationHierarchyConnector.cs new file mode 100644 index 000000000000..3a63b6f0a155 --- /dev/null +++ b/dotnet/src/SemanticKernel.Skills/Skills/Connectors.Interfaces/IOrganizationHierarchyConnector.cs @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.SemanticKernel.Connectors.Interfaces; + +/// +/// Interface for organization hierarchy connections (e.g. Azure AD). +/// +public interface IOrganizationHierarchyConnector +{ + /// + /// Get the user's direct reports' email addresses. + /// + /// Cancellation token. + /// The user's direct reports' email addresses. + Task> GetDirectReportsEmailAsync(CancellationToken cancellationToken = default); + + /// + /// Get the user's manager's email address. + /// + /// Cancellation token. + /// The user's manager's email address. + Task GetManagerEmailAsync(CancellationToken cancellationToken = default); + + /// + /// Get the user's manager's name. + /// + /// Cancellation token. + /// The user's manager's name. + Task GetManagerNameAsync(CancellationToken cancellationToken = default); +} diff --git a/dotnet/src/SemanticKernel.Skills/Skills/Connectors.Interfaces/ITaskManagementConnector.cs b/dotnet/src/SemanticKernel.Skills/Skills/Connectors.Interfaces/ITaskManagementConnector.cs new file mode 100644 index 000000000000..49a8c379da64 --- /dev/null +++ b/dotnet/src/SemanticKernel.Skills/Skills/Connectors.Interfaces/ITaskManagementConnector.cs @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.SemanticKernel.Connectors.Interfaces.Models; + +namespace Microsoft.SemanticKernel.Connectors.Interfaces; + +/// +/// Interface for task list connections (e.g. Microsoft To-Do). +/// +public interface ITaskManagementConnector +{ + /// + /// Add a task to the specified list. + /// + /// ID of the list in which to add the task. + /// Task to add. + /// Cancellation token. + /// Added task definition. + Task AddTaskAsync(string listId, TaskManagementTask task, CancellationToken cancellationToken = default); + + /// + /// Delete a task from a task list. + /// + /// ID of the list from which to delete the task. + /// ID of the task to delete. + /// Cancellation token. + Task DeleteTaskAsync(string listId, string taskId, CancellationToken cancellationToken = default); + + /// + /// Get the default task list. + /// + /// Cancellation token. + Task GetDefaultTaskListAsync(CancellationToken cancellationToken = default); + + /// + /// Get all the task lists. + /// + /// Cancellation token. + /// All of the user's task lists. + Task> GetTaskListsAsync(CancellationToken cancellationToken = default); + + /// + /// Get the all tasks in a task list. + /// + /// ID of the list from which to get the tasks. + /// Cancellation token. + /// All of the tasks in the specified task list. + Task> GetTasksAsync(string listId, CancellationToken cancellationToken = default); +} diff --git a/dotnet/src/SemanticKernel.Skills/Skills/Connectors.Interfaces/IWebSearchEngineConnector.cs b/dotnet/src/SemanticKernel.Skills/Skills/Connectors.Interfaces/IWebSearchEngineConnector.cs new file mode 100644 index 000000000000..fff9b19076ab --- /dev/null +++ b/dotnet/src/SemanticKernel.Skills/Skills/Connectors.Interfaces/IWebSearchEngineConnector.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.SemanticKernel.Connectors.Interfaces; + +/// +/// Web search engine connector interface. +/// +public interface IWebSearchEngineConnector +{ + /// + /// Execute a web search engine search. + /// + /// Query to search. + /// Cancellation token. + /// First snippet returned from search. + Task SearchAsync(string query, CancellationToken cancellationToken = default); +} diff --git a/dotnet/src/SemanticKernel.Skills/Skills/Connectors.Interfaces/Models/CalendarEvent.cs b/dotnet/src/SemanticKernel.Skills/Skills/Connectors.Interfaces/Models/CalendarEvent.cs new file mode 100644 index 000000000000..a7924cab69e5 --- /dev/null +++ b/dotnet/src/SemanticKernel.Skills/Skills/Connectors.Interfaces/Models/CalendarEvent.cs @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Microsoft.SemanticKernel.Connectors.Interfaces.Models; + +/// +/// Model for a calendar event. +/// +public class CalendarEvent +{ + /// + /// ID of the event. + /// + public string? Id { get; set; } = null; + + /// + /// Subject/title of the event. + /// + public string Subject { get; set; } + + /// + /// Body/content of the event. + /// + public string? Content { get; set; } = null; + + /// + /// Start time of the event. + /// + public DateTimeOffset Start { get; set; } + + /// + /// End time of the event. + /// + public DateTimeOffset End { get; set; } + + /// + /// Location of the event. + /// + public string? Location { get; set; } = null; + + /// + /// Attendees of the event. + /// + public IEnumerable? Attendees { get; set; } = Enumerable.Empty(); + + /// + /// Initializes a new instance of the class. + /// + /// Subject/title of the event. + /// Start time of the event. + /// End time of the event. + public CalendarEvent(string subject, DateTimeOffset start, DateTimeOffset end) + { + this.Subject = subject; + this.Start = start; + this.End = end; + } +} diff --git a/dotnet/src/SemanticKernel.Skills/Skills/Connectors.Interfaces/Models/TaskManagementTask.cs b/dotnet/src/SemanticKernel.Skills/Skills/Connectors.Interfaces/Models/TaskManagementTask.cs new file mode 100644 index 000000000000..7feaaa7a720b --- /dev/null +++ b/dotnet/src/SemanticKernel.Skills/Skills/Connectors.Interfaces/Models/TaskManagementTask.cs @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft. All rights reserved. + +namespace Microsoft.SemanticKernel.Connectors.Interfaces.Models; + +/// +/// Model for a task in a task list. +/// +public class TaskManagementTask +{ + /// + /// ID of the task. + /// + public string Id { get; set; } + + /// + /// Title of the task. + /// + public string Title { get; set; } + + /// + /// Reminder date/time for the task. + /// + public string? Reminder { get; set; } + + /// + /// Task's due date/time. + /// + public string? Due { get; set; } + + /// + /// True if the task is completed, otherwise false. + /// + public bool IsCompleted { get; set; } + + /// + /// Initializes a new instance of the class. + /// + /// ID of the task. + /// Title of the task. + /// Reminder date/time for the task. + /// Task's due date/time. + /// True if the task is completed, otherwise false. + public TaskManagementTask(string id, string title, string? reminder = null, string? due = null, bool isCompleted = false) + { + this.Id = id; + this.Title = title; + this.Reminder = reminder; + this.Due = due; + this.IsCompleted = isCompleted; + } +} diff --git a/dotnet/src/SemanticKernel.Skills/Skills/Connectors.Interfaces/Models/TaskManagementTaskList.cs b/dotnet/src/SemanticKernel.Skills/Skills/Connectors.Interfaces/Models/TaskManagementTaskList.cs new file mode 100644 index 000000000000..ca2d6845e2a6 --- /dev/null +++ b/dotnet/src/SemanticKernel.Skills/Skills/Connectors.Interfaces/Models/TaskManagementTaskList.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft. All rights reserved. + +namespace Microsoft.SemanticKernel.Connectors.Interfaces.Models; + +/// +/// Model for a list of tasks. +/// +public class TaskManagementTaskList +{ + /// + /// ID of the task list. + /// + public string Id { get; set; } + + /// + /// Name of the task list. + /// + public string Name { get; set; } + + /// + /// Initializes a new instance of the class. + /// + /// ID of the task list. + /// Name of the task list. + public TaskManagementTaskList(string id, string name) + { + this.Id = id; + this.Name = name; + } +} diff --git a/dotnet/src/SemanticKernel.Skills/Skills/SemanticKernel.Skills.csproj b/dotnet/src/SemanticKernel.Skills/Skills/SemanticKernel.Skills.csproj new file mode 100644 index 000000000000..8f53f7de42bf --- /dev/null +++ b/dotnet/src/SemanticKernel.Skills/Skills/SemanticKernel.Skills.csproj @@ -0,0 +1,24 @@ + + + + $([System.IO.Path]::GetDirectoryName($([MSBuild]::GetPathOfFileAbove('.gitignore', '$(MSBuildThisFileDirectory)')))) + + + + + Microsoft.SemanticKernel.Skills + Microsoft.SemanticKernel + netstandard2.1 + + + + + Microsoft.SemanticKernel.Skills + Semantic Kernel Skills + 0.7 + + + + + + diff --git a/dotnet/src/SemanticKernel.Skills/Skills/Skills/ConversationSummary/ConversationSummarySkill.cs b/dotnet/src/SemanticKernel.Skills/Skills/Skills/ConversationSummary/ConversationSummarySkill.cs new file mode 100644 index 000000000000..a2d3f922317e --- /dev/null +++ b/dotnet/src/SemanticKernel.Skills/Skills/Skills/ConversationSummary/ConversationSummarySkill.cs @@ -0,0 +1,112 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Threading.Tasks; +using Microsoft.SemanticKernel.KernelExtensions; +using Microsoft.SemanticKernel.Orchestration; +using Microsoft.SemanticKernel.SemanticFunctions.Partitioning; +using Microsoft.SemanticKernel.SkillDefinition; + +namespace Microsoft.SemanticKernel.Skills.ConversationSummary; + +/// +/// Semantic skill that enables conversations summarization. +/// +/// +/// +/// var kernel Kernel.Builder.Build(); +/// kernel.ImportSkill(new ConversationSummarySkill(kernel)); +/// +/// +public class ConversationSummarySkill +{ + /// + /// The max tokens to process in a single semantic function call. + /// + private const int MaxTokens = 1024; + + private readonly ISKFunction _summarizeConversationFunction; + private readonly ISKFunction _conversationActionItemsFunction; + private readonly ISKFunction _conversationTopicsFunction; + + /// + /// Initializes a new instance of the class. + /// + /// Kernel instance + public ConversationSummarySkill(IKernel kernel) + { + this._summarizeConversationFunction = kernel.CreateSemanticFunction( + SemanticFunctionConstants.SummarizeConversationDefinition, + skillName: nameof(ConversationSummarySkill), + description: "Given a section of a conversation transcript, summarize the part of the conversation.", + maxTokens: MaxTokens, + temperature: 0.1, + topP: 0.5); + + this._conversationActionItemsFunction = kernel.CreateSemanticFunction( + SemanticFunctionConstants.GetConversationActionItemsDefinition, + skillName: nameof(ConversationSummarySkill), + description: "Given a section of a conversation transcript, identify action items.", + maxTokens: MaxTokens, + temperature: 0.1, + topP: 0.5); + + this._conversationTopicsFunction = kernel.CreateSemanticFunction( + SemanticFunctionConstants.GetConversationTopicsDefinition, + skillName: nameof(ConversationSummarySkill), + description: "Analyze a conversation transcript and extract key topics worth remembering.", + maxTokens: MaxTokens, + temperature: 0.1, + topP: 0.5); + } + + /// + /// Given a long conversation transcript, summarize the conversation. + /// + /// A long conversation transcript. + /// The SKContext for function execution. + [SKFunction("Given a long conversation transcript, summarize the conversation.")] + [SKFunctionName("SummarizeConversation")] + [SKFunctionInput(Description = "A long conversation transcript.")] + public Task SummarizeConversationAsync(string input, SKContext context) + { + System.Collections.Generic.List lines = SemanticTextPartitioner.SplitPlainTextLines(input, MaxTokens); + System.Collections.Generic.List paragraphs = SemanticTextPartitioner.SplitPlainTextParagraphs(lines, MaxTokens); + + return this._summarizeConversationFunction + .AggregatePartitionedResultsAsync(paragraphs, context); + } + + /// + /// Given a long conversation transcript, identify action items. + /// + /// A long conversation transcript. + /// The SKContext for function execution. + [SKFunction("Given a long conversation transcript, identify action items.")] + [SKFunctionName("GetConversationActionItems")] + [SKFunctionInput(Description = "A long conversation transcript.")] + public Task GetConversationActionItemsAsync(string input, SKContext context) + { + System.Collections.Generic.List lines = SemanticTextPartitioner.SplitPlainTextLines(input, MaxTokens); + System.Collections.Generic.List paragraphs = SemanticTextPartitioner.SplitPlainTextParagraphs(lines, MaxTokens); + + return this._conversationActionItemsFunction + .AggregatePartitionedResultsAsync(paragraphs, context); + } + + /// + /// Given a long conversation transcript, identify topics. + /// + /// A long conversation transcript. + /// The SKContext for function execution. + [SKFunction("Given a long conversation transcript, identify topics worth remembering.")] + [SKFunctionName("GetConversationTopics")] + [SKFunctionInput(Description = "A long conversation transcript.")] + public Task GetConversationTopicsAsync(string input, SKContext context) + { + System.Collections.Generic.List lines = SemanticTextPartitioner.SplitPlainTextLines(input, MaxTokens); + System.Collections.Generic.List paragraphs = SemanticTextPartitioner.SplitPlainTextParagraphs(lines, MaxTokens); + + return this._conversationTopicsFunction + .AggregatePartitionedResultsAsync(paragraphs, context); + } +} diff --git a/dotnet/src/SemanticKernel.Skills/Skills/Skills/ConversationSummary/SemanticFunctionDefinitions.cs b/dotnet/src/SemanticKernel.Skills/Skills/Skills/ConversationSummary/SemanticFunctionDefinitions.cs new file mode 100644 index 000000000000..0b7826c511f6 --- /dev/null +++ b/dotnet/src/SemanticKernel.Skills/Skills/Skills/ConversationSummary/SemanticFunctionDefinitions.cs @@ -0,0 +1,92 @@ +// Copyright (c) Microsoft. All rights reserved. + +namespace Microsoft.SemanticKernel.Skills.ConversationSummary; + +internal static class SemanticFunctionConstants +{ + internal const string SummarizeConversationDefinition = + @"BEGIN CONTENT TO SUMMARIZE: +{{$INPUT}} + +END CONTENT TO SUMMARIZE. + +Summarize the conversation in 'CONTENT TO SUMMARIZE', identifying main points of discussion and any conclusions that were reached. +Do not incorporate other general knowledge. +Summary is in plain text, in complete sentences, with no markup or tags. + +BEGIN SUMMARY: +"; + + internal const string GetConversationActionItemsDefinition = + @"You are an action item extractor. You will be given chat history and need to make note of action items mentioned in the chat. +Extract action items from the content if there are any. If there are no action, return nothing. If a single field is missing, use an empty string. +Return the action items in json. + +Possible statuses for action items are: Open, Closed, In Progress. + +EXAMPLE INPUT WITH ACTION ITEMS: + +John Doe said: ""I will record a demo for the new feature by Friday"" +I said: ""Great, thanks John. We may not use all of it but it's good to get it out there."" + +EXAMPLE OUTPUT: +{ + ""actionItems"": [ + { + ""owner"": ""John Doe"", + ""actionItem"": ""Record a demo for the new feature"", + ""dueDate"": ""Friday"", + ""status"": ""Open"", + ""notes"": """" + } + ] +} + +EXAMPLE INPUT WITHOUT ACTION ITEMS: + +John Doe said: ""Hey I'm going to the store, do you need anything?"" +I said: ""No thanks, I'm good."" + +EXAMPLE OUTPUT: +{ + ""action_items"": [] +} + +CONTENT STARTS HERE. + +{{$INPUT}} + +CONTENT STOPS HERE. + +OUTPUT:"; + + internal const string GetConversationTopicsDefinition = + @"Analyze the following extract taken from a conversation transcript and extract key topics. +- Topics only worth remembering. +- Be brief. Short phrases. +- Can use broken English. +- Conciseness is very important. +- Topics can include names of memories you want to recall. +- NO LONG SENTENCES. SHORT PHRASES. +- Return in JSON +[Input] +My name is Macbeth. I used to be King of Scotland, but I died. My wife's name is Lady Macbeth and we were married for 15 years. We had no children. Our beloved dog Toby McDuff was a famous hunter of rats in the forest. +My tragic story was immortalized by Shakespeare in a play. +[Output] +{ + ""topics"": [ + ""Macbeth"", + ""King of Scotland"", + ""Lady Macbeth"", + ""Dog"", + ""Toby McDuff"", + ""Shakespeare"", + ""Play"", + ""Tragedy"" + ] +} ++++++ +[Input] +{{$INPUT}} +[Output]"; +} diff --git a/dotnet/src/SemanticKernel.Skills/Skills/Skills/Diagnostics/Ensure.cs b/dotnet/src/SemanticKernel.Skills/Skills/Skills/Diagnostics/Ensure.cs new file mode 100644 index 000000000000..d62a7b42b7c2 --- /dev/null +++ b/dotnet/src/SemanticKernel.Skills/Skills/Skills/Diagnostics/Ensure.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +namespace Microsoft.SemanticKernel.Skills.Diagnostics; + +internal static class Ensure +{ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void NotNullOrWhitespace([NotNull] string parameter, [NotNull] string parameterName) + { + if (string.IsNullOrWhiteSpace(parameter)) + { + throw new ArgumentException($"Parameter '{parameterName}' cannot be null or whitespace.", parameterName); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void NotNull([NotNull] object parameter, [NotNull] string parameterName) + { + if (parameter == null) + { + throw new ArgumentNullException($"Parameter '{parameterName}' cannot be null.", parameterName); + } + } +} diff --git a/dotnet/src/SemanticKernel.Skills/Skills/Skills/Document/DocumentSkill.cs b/dotnet/src/SemanticKernel.Skills/Skills/Skills/Document/DocumentSkill.cs new file mode 100644 index 000000000000..2f3de9ce4a51 --- /dev/null +++ b/dotnet/src/SemanticKernel.Skills/Skills/Skills/Document/DocumentSkill.cs @@ -0,0 +1,115 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.IO; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.SemanticKernel.Connectors.Interfaces; +using Microsoft.SemanticKernel.Orchestration; +using Microsoft.SemanticKernel.SkillDefinition; + +namespace Microsoft.SemanticKernel.Skills.Document; + +//********************************************************************************************************************** +// EXAMPLE USAGE +// Option #1: as a standalone C# function +// +// DocumentSkill documentSkill = new(new WordDocumentConnector(), new LocalDriveConnector()); +// string filePath = "PATH_TO_DOCX_FILE.docx"; +// string text = await documentSkill.ReadTextAsync(filePath); +// Console.WriteLine(text); +// +// +// Option #2: with the Semantic Kernel +// +// DocumentSkill documentSkill = new(new WordDocumentConnector(), new LocalDriveConnector()); +// string filePath = "PATH_TO_DOCX_FILE.docx"; +// ISemanticKernel kernel = SemanticKernel.Build(); +// var result = await kernel.RunAsync( +// filePath, +// documentSkill.ReadTextAsync); +// Console.WriteLine(result); +//********************************************************************************************************************** + +/// +/// Skill for interacting with documents (e.g. Microsoft Word) +/// +public class DocumentSkill +{ + /// + /// parameter names. + /// + public static class Parameters + { + /// + /// Document file path. + /// + public const string FilePath = "filePath"; + } + + private readonly IDocumentConnector _documentConnector; + private readonly IFileSystemConnector _fileSystemConnector; + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class. + /// + /// Document connector + /// File system connector + /// Optional logger + public DocumentSkill(IDocumentConnector documentConnector, IFileSystemConnector fileSystemConnector, ILogger? logger = null) + { + this._documentConnector = documentConnector ?? throw new ArgumentNullException(nameof(documentConnector)); + this._fileSystemConnector = fileSystemConnector ?? throw new ArgumentNullException(nameof(fileSystemConnector)); + this._logger = logger ?? new NullLogger(); + } + + /// + /// Read all text from a document, using as the file path. + /// + [SKFunction("Read all text from a document")] + [SKFunctionInput(Description = "Path to the file to read")] + public async Task ReadTextAsync(string filePath, SKContext context) + { + this._logger.LogInformation("Reading text from {0}", filePath); + using var stream = await this._fileSystemConnector.GetFileContentStreamAsync(filePath, context.CancellationToken); + return this._documentConnector.ReadText(stream); + } + + /// + /// Append the text in to a document. If the document doesn't exist, it will be created. + /// + [SKFunction("Append text to a document. If the document doesn't exist, it will be created.")] + [SKFunctionInput(Description = "Text to append")] + [SKFunctionContextParameter(Name = Parameters.FilePath, Description = "Destination file path")] + public async Task AppendTextAsync(string text, SKContext context) + { + if (!context.Variables.Get(Parameters.FilePath, out string filePath)) + { + context.Fail($"Missing variable {Parameters.FilePath}."); + return; + } + + Stream stream; + + // If the document already exists, open it. If not, create it. + var fileExists = await this._fileSystemConnector.FileExistsAsync(filePath); + if (fileExists) + { + this._logger.LogInformation("Opening file at {0}", filePath); + stream = await this._fileSystemConnector.GetWriteableFileStreamAsync(filePath, context.CancellationToken); + } + else + { + this._logger.LogInformation("File does not exist. Creating file at {0}", filePath); + stream = await this._fileSystemConnector.CreateFileAsync(filePath); + this._documentConnector.Initialize(stream); + } + + this._logger.LogInformation("Writing text to {0}", filePath); + this._documentConnector.AppendText(stream, text); + + await stream.DisposeAsync(); + } +} diff --git a/dotnet/src/SemanticKernel.Skills/Skills/Skills/Productivity/CalendarSkill.cs b/dotnet/src/SemanticKernel.Skills/Skills/Skills/Productivity/CalendarSkill.cs new file mode 100644 index 000000000000..37fb44639196 --- /dev/null +++ b/dotnet/src/SemanticKernel.Skills/Skills/Skills/Productivity/CalendarSkill.cs @@ -0,0 +1,123 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Globalization; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.SemanticKernel.Connectors.Interfaces; +using Microsoft.SemanticKernel.Connectors.Interfaces.Models; +using Microsoft.SemanticKernel.Orchestration; +using Microsoft.SemanticKernel.SkillDefinition; +using Microsoft.SemanticKernel.Skills.Diagnostics; + +namespace Microsoft.SemanticKernel.Skills.Productivity; + +/// +/// Skill for calendar operations. +/// +public class CalendarSkill +{ + /// + /// parameter names. + /// + public static class Parameters + { + /// + /// Event start as DateTimeOffset. + /// + public const string Start = "start"; + + /// + /// Event end as DateTimeOffset. + /// + public const string End = "end"; + + /// + /// Event's location. + /// + public const string Location = "location"; + + /// + /// Event's content. + /// + public const string Content = "content"; + + /// + /// Event's attendees, separated by ',' or ';'. + /// + public const string Attendees = "attendees"; + } + + private readonly ICalendarConnector _connector; + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class. + /// + /// Calendar connector. + /// Logger. + public CalendarSkill(ICalendarConnector connector, ILogger? logger = null) + { + Ensure.NotNull(connector, nameof(connector)); + + this._connector = connector; + this._logger = logger ?? new NullLogger(); + } + + /// + /// Add an event to my calendar using as the subject. + /// + [SKFunction("Add an event to my calendar.")] + [SKFunctionInput(Description = "Event subject")] + [SKFunctionContextParameter(Name = Parameters.Start, Description = "Event start date/time as DateTimeOffset")] + [SKFunctionContextParameter(Name = Parameters.End, Description = "Event end date/time as DateTimeOffset")] + [SKFunctionContextParameter(Name = Parameters.Location, Description = "Event location (optional)")] + [SKFunctionContextParameter(Name = Parameters.Content, Description = "Event content/body (optional)")] + [SKFunctionContextParameter(Name = Parameters.Attendees, Description = "Event attendees, separated by ',' or ';'.")] + public async Task AddEventAsync(string subject, SKContext context) + { + ContextVariables memory = context.Variables; + + if (string.IsNullOrWhiteSpace(subject)) + { + context.Fail($"Missing variables input to use as event subject."); + return; + } + + if (!memory.Get(Parameters.Start, out string start)) + { + context.Fail($"Missing variable {Parameters.Start}."); + return; + } + + if (!memory.Get(Parameters.End, out string end)) + { + context.Fail($"Missing variable {Parameters.End}."); + return; + } + + CalendarEvent calendarEvent = new CalendarEvent( + memory.Input, + DateTimeOffset.Parse(start, CultureInfo.InvariantCulture.DateTimeFormat), + DateTimeOffset.Parse(end, CultureInfo.InvariantCulture.DateTimeFormat)); + + if (memory.Get(Parameters.Location, out string location)) + { + calendarEvent.Location = location; + } + + if (memory.Get(Parameters.Content, out string content)) + { + calendarEvent.Content = content; + } + + if (memory.Get(Parameters.Attendees, out string attendees)) + { + calendarEvent.Attendees = attendees.Split(new[] { ',', ';' }, StringSplitOptions.RemoveEmptyEntries); + } + + this._logger.LogInformation("Adding calendar event '{0}'", calendarEvent.Subject); + await this._connector.AddEventAsync(calendarEvent); + } +} diff --git a/dotnet/src/SemanticKernel.Skills/Skills/Skills/Productivity/CloudDriveSkill.cs b/dotnet/src/SemanticKernel.Skills/Skills/Skills/Productivity/CloudDriveSkill.cs new file mode 100644 index 000000000000..1a33918948bc --- /dev/null +++ b/dotnet/src/SemanticKernel.Skills/Skills/Skills/Productivity/CloudDriveSkill.cs @@ -0,0 +1,96 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.IO; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.SemanticKernel.Connectors.Interfaces; +using Microsoft.SemanticKernel.Orchestration; +using Microsoft.SemanticKernel.SkillDefinition; +using Microsoft.SemanticKernel.Skills.Diagnostics; + +namespace Microsoft.SemanticKernel.Skills.Productivity; + +/// +/// Cloud drive skill (e.g. OneDrive). +/// +public class CloudDriveSkill +{ + /// + /// parameter names. + /// + public static class Parameters + { + /// + /// Document file path. + /// + public const string DestinationPath = "destinationPath"; + } + + private readonly ICloudDriveConnector _connector; + private readonly ILogger _logger; + + public CloudDriveSkill(ICloudDriveConnector connector, ILogger? logger = null) + { + Ensure.NotNull(connector, nameof(connector)); + + this._connector = connector; + this._logger = logger ?? new NullLogger(); + } + + /// + /// Get the contents of a file stored in a cloud drive. + /// + [SKFunction("Get the contents of a file in a cloud drive.")] + [SKFunctionInput(Description = "Path to file")] + public async Task GetFileContentAsync(string filePath, SKContext context) + { + this._logger.LogDebug("Getting file content for '{0}'", filePath); + Stream fileContentStream = await this._connector.GetFileContentStreamAsync(filePath, context.CancellationToken); + + using StreamReader sr = new StreamReader(fileContentStream); + string content = await sr.ReadToEndAsync(); + this._logger.LogDebug("File content: {0}", content); + return content; + } + + /// + /// Upload a small file to OneDrive (less than 4MB). + /// + [SKFunction("Upload a small file to OneDrive (less than 4MB).")] + public async Task UploadFileAsync(string filePath, SKContext context) + { + if (!context.Variables.Get(Parameters.DestinationPath, out string destinationPath)) + { + context.Fail($"Missing variable {Parameters.DestinationPath}."); + return; + } + + this._logger.LogDebug("Uploading file '{0}'", filePath); + + // TODO Add support for large file uploads (i.e. upload sessions) + + try + { + await this._connector.UploadSmallFileAsync(filePath, destinationPath, context.CancellationToken); + } + catch (IOException ex) + { + context.Fail(ex.Message, ex); + } + } + + /// + /// Create a sharable link to a file stored in a cloud drive. + /// + [SKFunction("Create a sharable link to a file stored in a cloud drive.")] + [SKFunctionInput(Description = "Path to file")] + public async Task CreateLinkAsync(string filePath, SKContext context) + { + this._logger.LogDebug("Creating link for '{0}'", filePath); + const string type = "view"; // TODO expose this as an SK variable + const string scope = "anonymous"; // TODO expose this as an SK variable + + return await this._connector.CreateShareLinkAsync(filePath, type, scope, context.CancellationToken); + } +} diff --git a/dotnet/src/SemanticKernel.Skills/Skills/Skills/Productivity/EmailSkill.cs b/dotnet/src/SemanticKernel.Skills/Skills/Skills/Productivity/EmailSkill.cs new file mode 100644 index 000000000000..d0c70713a7aa --- /dev/null +++ b/dotnet/src/SemanticKernel.Skills/Skills/Skills/Productivity/EmailSkill.cs @@ -0,0 +1,83 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.SemanticKernel.Connectors.Interfaces; +using Microsoft.SemanticKernel.Orchestration; +using Microsoft.SemanticKernel.SkillDefinition; +using Microsoft.SemanticKernel.Skills.Diagnostics; + +namespace Microsoft.SemanticKernel.Skills.Productivity; + +/// +/// Email skill (e.g. Outlook). +/// +public class EmailSkill +{ + /// + /// parameter names. + /// + public static class Parameters + { + /// + /// Email recipients, separated by ',' or ';'. + /// + public const string Recipients = "recipients"; + + /// + /// Email subject. + /// + public const string Subject = "subject"; + } + + private readonly IEmailConnector _connector; + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class. + /// + /// Email connector. + /// Logger. + public EmailSkill(IEmailConnector connector, ILogger? logger = null) + { + Ensure.NotNull(connector, nameof(connector)); + + this._connector = connector; + this._logger = logger ?? new NullLogger(); + } + + /// + /// Get my email address. + /// + [SKFunction("Gets the email address for me.")] + public async Task GetMyEmailAddressAsync() + => await this._connector.GetMyEmailAddressAsync(); + + /// + /// Send an email using as the body. + /// + [SKFunction("Send an email to one or more recipients.")] + [SKFunctionInput(Description = "Email content/body")] + [SKFunctionContextParameter(Name = Parameters.Recipients, Description = "Recipients of the email, separated by ',' or ';'.")] + [SKFunctionContextParameter(Name = Parameters.Subject, Description = "Subject of the email")] + public async Task SendEmailAsync(string content, SKContext context) + { + if (!context.Variables.Get(Parameters.Recipients, out string recipients)) + { + context.Fail($"Missing variable {Parameters.Recipients}."); + return; + } + + if (!context.Variables.Get(Parameters.Subject, out string subject)) + { + context.Fail($"Missing variable {Parameters.Subject}."); + return; + } + + this._logger.LogInformation("Sending email to '{0}' with subject '{1}'", recipients, subject); + string[] recipientList = recipients.Split(new[] { ',', ';' }, StringSplitOptions.RemoveEmptyEntries); + await this._connector.SendEmailAsync(subject, content, recipientList); + } +} diff --git a/dotnet/src/SemanticKernel.Skills/Skills/Skills/Productivity/OrganizationHierarchySkill.cs b/dotnet/src/SemanticKernel.Skills/Skills/Skills/Productivity/OrganizationHierarchySkill.cs new file mode 100644 index 000000000000..dade607cf8cb --- /dev/null +++ b/dotnet/src/SemanticKernel.Skills/Skills/Skills/Productivity/OrganizationHierarchySkill.cs @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Threading.Tasks; +using Microsoft.SemanticKernel.Connectors.Interfaces; +using Microsoft.SemanticKernel.Orchestration; +using Microsoft.SemanticKernel.SkillDefinition; +using Microsoft.SemanticKernel.Skills.Diagnostics; + +namespace Microsoft.SemanticKernel.Skills.Productivity; + +/// +/// Organizational Hierarchy skill. +/// +public class OrganizationHierarchySkill +{ + private readonly IOrganizationHierarchyConnector _connector; + + public OrganizationHierarchySkill(IOrganizationHierarchyConnector connector) + { + Ensure.NotNull(connector, nameof(connector)); + + this._connector = connector; + } + + /// + /// Get the email of the manager of the current user. + /// + [SKFunction("Get my manager's email address.")] + public async Task GetMyManagerEmailAsync(SKContext context) + => await this._connector.GetManagerEmailAsync(context.CancellationToken); + + /// + /// Get the name of the manager of the current user. + /// + [SKFunction("Get my manager's name.")] + public async Task GetMyManagerNameAsync(SKContext context) + => await this._connector.GetManagerNameAsync(context.CancellationToken); +} diff --git a/dotnet/src/SemanticKernel.Skills/Skills/Skills/Productivity/TaskListSkill.cs b/dotnet/src/SemanticKernel.Skills/Skills/Skills/Productivity/TaskListSkill.cs new file mode 100644 index 000000000000..889f82abdbf1 --- /dev/null +++ b/dotnet/src/SemanticKernel.Skills/Skills/Skills/Productivity/TaskListSkill.cs @@ -0,0 +1,92 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.SemanticKernel.Connectors.Interfaces; +using Microsoft.SemanticKernel.Connectors.Interfaces.Models; +using Microsoft.SemanticKernel.Orchestration; +using Microsoft.SemanticKernel.SkillDefinition; +using Microsoft.SemanticKernel.Skills.Diagnostics; + +namespace Microsoft.SemanticKernel.Skills.Productivity; + +/// +/// Task list skill (e.g. Microsoft To-Do) +/// +public class TaskListSkill +{ + /// + /// parameter names. + /// + public static class Parameters + { + /// + /// Task reminder as DateTimeOffset. + /// + public const string Reminder = "reminder"; + } + + private readonly ITaskManagementConnector _connector; + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class. + /// + /// Task list connector. + /// Logger. + public TaskListSkill(ITaskManagementConnector connector, ILogger? logger = null) + { + Ensure.NotNull(connector, nameof(connector)); + + this._connector = connector; + this._logger = logger ?? new NullLogger(); + } + + /// + /// Calculates an upcoming day of week (e.g. 'next Monday'). + /// + public static DateTimeOffset GetNextDayOfWeek(DayOfWeek dayOfWeek, TimeSpan timeOfDay) + { + DateTimeOffset today = new DateTimeOffset(DateTime.Today); + int nextDayOfWeekOffset = (dayOfWeek - today.DayOfWeek); + if (nextDayOfWeekOffset <= 0) + { + nextDayOfWeekOffset += 7; + } + + DateTimeOffset nextDayOfWeek = today.AddDays(nextDayOfWeekOffset); + DateTimeOffset nextDayOfWeekAtTimeOfDay = nextDayOfWeek.Add(timeOfDay); + + return nextDayOfWeekAtTimeOfDay; + } + + /// + /// Add a task to a To-Do list with an optional reminder. + /// + [SKFunction("Add a task to a task list with an optional reminder.")] + [SKFunctionInput(Description = "Title of the task.")] + [SKFunctionContextParameter(Name = Parameters.Reminder, Description = "Reminder for the task in DateTimeOffset (optional)")] + public async Task AddTaskAsync(string title, SKContext context) + { + TaskManagementTaskList? defaultTaskList = await this._connector.GetDefaultTaskListAsync(context.CancellationToken); + if (defaultTaskList == null) + { + context.Fail("No default task list found."); + return; + } + + TaskManagementTask task = new TaskManagementTask( + id: Guid.NewGuid().ToString(), + title: title); + + if (context.Variables.Get(Parameters.Reminder, out string reminder)) + { + task.Reminder = reminder; + } + + this._logger.LogInformation("Adding task '{0}' to task list '{1}'", task.Title, defaultTaskList.Name); + await this._connector.AddTaskAsync(defaultTaskList.Id, task, context.CancellationToken); + } +} diff --git a/dotnet/src/SemanticKernel.Skills/Skills/Skills/Web/SearchUrlSkill.cs b/dotnet/src/SemanticKernel.Skills/Skills/Skills/Web/SearchUrlSkill.cs new file mode 100644 index 000000000000..3633d0529162 --- /dev/null +++ b/dotnet/src/SemanticKernel.Skills/Skills/Skills/Web/SearchUrlSkill.cs @@ -0,0 +1,166 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Diagnostics.CodeAnalysis; +using System.Text.Encodings.Web; +using Microsoft.SemanticKernel.SkillDefinition; + +namespace Microsoft.SemanticKernel.Skills.Web; + +/// +/// Get search URLs for various websites +/// +[SuppressMessage("Design", "CA1055:URI return values should not be strings", Justification = "Semantic Kernel operates on strings")] +public class SearchUrlSkill +{ + /** + * Amazon Search URLs + */ + /// + /// Get search URL for Amazon + /// + [SKFunction("Return URL for Amazon search query")] + public string AmazonSearchUrl(string query) + { + string encoded = UrlEncoder.Default.Encode(query); + return $"https://www.amazon.com/s?k={encoded}"; + } + + /** + * Bing Search URLs + */ + /// + /// Get search URL for Bing + /// + [SKFunction("Return URL for Bing search query.")] + [SKFunctionInput(Description = "Text to search for")] + public string BingSearchUrl(string query) + { + string encoded = UrlEncoder.Default.Encode(query); + return $"https://www.bing.com/search?q={encoded}"; + } + + /// + /// Get search URL for Bing Images + /// + [SKFunction("Return URL for Bing Images search query.")] + [SKFunctionInput(Description = "Text to search for")] + public string BingImagesSearchUrl(string query) + { + string encoded = UrlEncoder.Default.Encode(query); + return $"https://www.bing.com/images/search?q={encoded}"; + } + + /// + /// Get search URL for Bing Maps + /// + [SKFunction("Return URL for Bing Maps search query.")] + [SKFunctionInput(Description = "Text to search for")] + public string BingMapsSearchUrl(string query) + { + string encoded = UrlEncoder.Default.Encode(query); + return $"https://www.bing.com/maps?q={encoded}"; + } + + /// + /// Get search URL for Bing Shopping + /// + [SKFunction("Return URL for Bing Shopping search query.")] + [SKFunctionInput(Description = "Text to search for")] + public string BingShoppingSearchUrl(string query) + { + string encoded = UrlEncoder.Default.Encode(query); + return $"https://www.bing.com/shop?q={encoded}"; + } + + /// + /// Get search URL for Bing News + /// + [SKFunction("Return URL for Bing News search query.")] + [SKFunctionInput(Description = "Text to search for")] + public string BingNewsSearchUrl(string query) + { + string encoded = UrlEncoder.Default.Encode(query); + return $"https://www.bing.com/news/search?q={encoded}"; + } + + /// + /// Get search URL for Bing Travel + /// + [SKFunction("Return URL for Bing Travel search query.")] + [SKFunctionInput(Description = "Text to search for")] + public string BingTravelSearchUrl(string query) + { + string encoded = UrlEncoder.Default.Encode(query); + return $"https://www.bing.com/travel/search?q={encoded}"; + } + + /** + * Facebook Search URLs + */ + /// + /// Get search URL for Facebook + /// + [SKFunction("Return URL for Facebook search query.")] + [SKFunctionInput(Description = "Text to search for")] + public string FacebookSearchUrl(string query) + { + string encoded = UrlEncoder.Default.Encode(query); + return $"https://www.facebook.com/search/top/?q={encoded}"; + } + + /** + * GitHub Search URLs + */ + /// + /// Get search URL for GitHub + /// + [SKFunction("Return URL for GitHub search query.")] + [SKFunctionInput(Description = "Text to search for")] + public string GitHubSearchUrl(string query) + { + string encoded = UrlEncoder.Default.Encode(query); + return $"https://github.com/search?q={encoded}"; + } + + /** + * LinkedIn Search URLs + */ + /// + /// Get search URL for LinkedIn + /// + [SKFunction("Return URL for LinkedIn search query.")] + [SKFunctionInput(Description = "Text to search for")] + public string LinkedInSearchUrl(string query) + { + string encoded = UrlEncoder.Default.Encode(query); + return $"https://www.linkedin.com/search/results/index/?keywords={encoded}"; + } + + /** + * Twitter Search URLs + */ + /// + /// Get search URL for Twitter + /// + [SKFunction("Return URL for Twitter search query.")] + [SKFunctionInput(Description = "Text to search for")] + public string TwitterSearchUrl(string query) + { + string encoded = UrlEncoder.Default.Encode(query); + return $"https://twitter.com/search?q={encoded}"; + } + + /** + * Wikipedia Search URLs + */ + /// + /// Get search URL for Wikipedia + /// + [SKFunction("Return URL for Wikipedia search query.")] + [SKFunctionInput(Description = "Text to search for")] + public string WikipediaSearchUrl(string query) + { + string encoded = UrlEncoder.Default.Encode(query); + return $"https://wikipedia.org/w/index.php?search={encoded}"; + } +} diff --git a/dotnet/src/SemanticKernel.Skills/Skills/Skills/Web/WebSearchEngineSkill.cs b/dotnet/src/SemanticKernel.Skills/Skills/Skills/Web/WebSearchEngineSkill.cs new file mode 100644 index 000000000000..618aa554fe18 --- /dev/null +++ b/dotnet/src/SemanticKernel.Skills/Skills/Skills/Web/WebSearchEngineSkill.cs @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Threading.Tasks; +using Microsoft.SemanticKernel.Connectors.Interfaces; +using Microsoft.SemanticKernel.Orchestration; +using Microsoft.SemanticKernel.SkillDefinition; + +namespace Microsoft.SemanticKernel.Skills.Web; + +/// +/// Web search engine skill (e.g. Bing) +/// +public class WebSearchEngineSkill +{ + private readonly IWebSearchEngineConnector _connector; + + public WebSearchEngineSkill(IWebSearchEngineConnector connector) + { + this._connector = connector; + } + + [SKFunction("Perform a web search.")] + [SKFunctionInput(Description = "Text to search for")] + public async Task SearchAsync(string query, SKContext context) + { + string result = await this._connector.SearchAsync(query, context.CancellationToken); + if (string.IsNullOrWhiteSpace(result)) + { + context.Fail("Failed to get a response from the web search engine."); + } + + return result; + } +} diff --git a/dotnet/src/SemanticKernel.Test/AI/Embeddings/EmbeddingTests.cs b/dotnet/src/SemanticKernel.Test/AI/Embeddings/EmbeddingTests.cs new file mode 100644 index 000000000000..4903d6a110bc --- /dev/null +++ b/dotnet/src/SemanticKernel.Test/AI/Embeddings/EmbeddingTests.cs @@ -0,0 +1,63 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Linq; +using System.Text.Json; +using Microsoft.SemanticKernel.AI.Embeddings; +using Microsoft.SemanticKernel.Diagnostics; +using Xunit; + +namespace SemanticKernelTests.AI.Embeddings; + +public class EmbeddingTests +{ + // Vector has length of 3, magnitude of 5 + private readonly float[] _vector = new float[] { 0, 3, -4 }; + private readonly float[] _empty = Array.Empty(); + + [Fact] + public void ItThrowsWithNullVector() + { + // Assert + Assert.Throws(() => new Embedding(null!)); + } + + [Fact] + public void ItCreatesEmptyEmbedding() + { + // Arrange + var target = new Embedding(this._empty); + + // Assert + Assert.Empty(target.Vector); + Assert.Equal(0, target.Count); + } + + [Fact] + public void ItCreatesExpectedEmbedding() + { + // Arrange + var target = new Embedding(this._vector); + + // Act + // TODO: copy is never used - bug? + var copy = target; + + // Assert + Assert.True(target.Vector.SequenceEqual(this._vector)); + } + + [Fact] + public void ItSerializesEmbedding() + { + // Arrange + var target = new Embedding(this._vector); + + // Act + string json = JsonSerializer.Serialize(target); + var copy = JsonSerializer.Deserialize>(json); + + // Assert + Assert.True(copy.Vector.SequenceEqual(this._vector)); + } +} diff --git a/dotnet/src/SemanticKernel.Test/Configuration/KernelConfigTests.cs b/dotnet/src/SemanticKernel.Test/Configuration/KernelConfigTests.cs new file mode 100644 index 000000000000..eeb9fef9eafe --- /dev/null +++ b/dotnet/src/SemanticKernel.Test/Configuration/KernelConfigTests.cs @@ -0,0 +1,225 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Linq; +using Microsoft.SemanticKernel.Configuration; +using Microsoft.SemanticKernel.Reliability; +using Moq; +using Xunit; + +namespace SemanticKernelTests.Configuration; + +/// +/// Unit tests of . +/// +public class KernelConfigTests +{ + [Fact] + public void RetryMechanismIsSet() + { + // Arrange + var retry = new PassThroughWithoutRetry(); + var config = new KernelConfig(); + + // Act + config.SetRetryMechanism(retry); + + // Assert + Assert.Equal(retry, config.RetryMechanism); + } + + [Fact] + public void RetryMechanismIsSetWithCustomImplementation() + { + // Arrange + var retry = new Mock(); + var config = new KernelConfig(); + + // Act + config.SetRetryMechanism(retry.Object); + + // Assert + Assert.Equal(retry.Object, config.RetryMechanism); + } + + [Fact] + public void RetryMechanismIsSetToPassThroughWithoutRetryIfNull() + { + // Arrange + var config = new KernelConfig(); + + // Act + config.SetRetryMechanism(null); + + // Assert + Assert.IsType(config.RetryMechanism); + } + + [Fact] + public void RetryMechanismIsSetToPassThroughWithoutRetryIfNotSet() + { + // Arrange + var config = new KernelConfig(); + + // Act + // Assert + Assert.IsType(config.RetryMechanism); + } + + [Fact] + public void ItTellsIfABackendIsAvailable() + { + // Arrange + var target = new KernelConfig(); + target.AddAzureOpenAICompletionBackend("azure", "depl", "https://url", "key"); + target.AddOpenAICompletionBackend("oai", "model", "apikey"); + target.AddAzureOpenAIEmbeddingsBackend("azure", "depl2", "https://url2", "key"); + target.AddOpenAIEmbeddingsBackend("oai2", "model2", "apikey2"); + + // Assert + Assert.True(target.HasCompletionBackend("azure")); + Assert.True(target.HasCompletionBackend("oai")); + Assert.True(target.HasEmbeddingsBackend("azure")); + Assert.True(target.HasEmbeddingsBackend("oai2")); + + Assert.False(target.HasCompletionBackend("azure2")); + Assert.False(target.HasCompletionBackend("oai2")); + Assert.False(target.HasEmbeddingsBackend("azure1")); + Assert.False(target.HasEmbeddingsBackend("oai")); + + Assert.True(target.HasCompletionBackend("azure", + x => x.BackendType == BackendTypes.AzureOpenAI)); + Assert.False(target.HasCompletionBackend("azure", + x => x.BackendType == BackendTypes.OpenAI)); + + Assert.False(target.HasEmbeddingsBackend("oai2", + x => x.BackendType == BackendTypes.AzureOpenAI)); + Assert.True(target.HasEmbeddingsBackend("oai2", + x => x.BackendType == BackendTypes.OpenAI)); + + Assert.True(target.HasCompletionBackend("azure", + x => x.BackendType == BackendTypes.AzureOpenAI && x.AzureOpenAI?.DeploymentName == "depl")); + Assert.False(target.HasCompletionBackend("azure", + x => x.BackendType == BackendTypes.AzureOpenAI && x.AzureOpenAI?.DeploymentName == "nope")); + } + + [Fact] + public void ItCanOverwriteBackends() + { + // Arrange + var target = new KernelConfig(); + + // Act - Assert no exception occurs + target.AddAzureOpenAICompletionBackend("one", "dep", "https://localhost", "key", overwrite: true); + target.AddAzureOpenAICompletionBackend("one", "dep", "https://localhost", "key", overwrite: true); + target.AddOpenAICompletionBackend("one", "model", "key", overwrite: true); + target.AddOpenAICompletionBackend("one", "model", "key", overwrite: true); + target.AddAzureOpenAIEmbeddingsBackend("one", "dep", "https://localhost", "key", overwrite: true); + target.AddAzureOpenAIEmbeddingsBackend("one", "dep", "https://localhost", "key", overwrite: true); + target.AddOpenAIEmbeddingsBackend("one", "model", "key", overwrite: true); + target.AddOpenAIEmbeddingsBackend("one", "model", "key", overwrite: true); + } + + [Fact] + public void ItCanRemoveAllBackends() + { + // Arrange + var target = new KernelConfig(); + target.AddAzureOpenAICompletionBackend("one", "dep", "https://localhost", "key"); + target.AddAzureOpenAICompletionBackend("2", "dep", "https://localhost", "key"); + target.AddOpenAICompletionBackend("3", "model", "key"); + target.AddOpenAICompletionBackend("4", "model", "key"); + target.AddAzureOpenAIEmbeddingsBackend("5", "dep", "https://localhost", "key"); + target.AddAzureOpenAIEmbeddingsBackend("6", "dep", "https://localhost", "key"); + target.AddOpenAIEmbeddingsBackend("7", "model", "key"); + target.AddOpenAIEmbeddingsBackend("8", "model", "key"); + + // Act + target.RemoveAllBackends(); + + // Assert + Assert.Empty(target.GetAllEmbeddingsBackends()); + Assert.Empty(target.GetAllCompletionBackends()); + } + + [Fact] + public void ItCanRemoveAllCompletionBackends() + { + // Arrange + var target = new KernelConfig(); + target.AddAzureOpenAICompletionBackend("one", "dep", "https://localhost", "key"); + target.AddAzureOpenAICompletionBackend("2", "dep", "https://localhost", "key"); + target.AddOpenAICompletionBackend("3", "model", "key"); + target.AddOpenAICompletionBackend("4", "model", "key"); + target.AddAzureOpenAIEmbeddingsBackend("5", "dep", "https://localhost", "key"); + target.AddAzureOpenAIEmbeddingsBackend("6", "dep", "https://localhost", "key"); + target.AddOpenAIEmbeddingsBackend("7", "model", "key"); + target.AddOpenAIEmbeddingsBackend("8", "model", "key"); + + // Act + target.RemoveAllCompletionBackends(); + + // Assert + Assert.Equal(4, target.GetAllEmbeddingsBackends().Count()); + Assert.Empty(target.GetAllCompletionBackends()); + } + + [Fact] + public void ItCanRemoveAllEmbeddingsBackends() + { + // Arrange + var target = new KernelConfig(); + target.AddAzureOpenAICompletionBackend("one", "dep", "https://localhost", "key"); + target.AddAzureOpenAICompletionBackend("2", "dep", "https://localhost", "key"); + target.AddOpenAICompletionBackend("3", "model", "key"); + target.AddOpenAICompletionBackend("4", "model", "key"); + target.AddAzureOpenAIEmbeddingsBackend("5", "dep", "https://localhost", "key"); + target.AddAzureOpenAIEmbeddingsBackend("6", "dep", "https://localhost", "key"); + target.AddOpenAIEmbeddingsBackend("7", "model", "key"); + target.AddOpenAIEmbeddingsBackend("8", "model", "key"); + + // Act + target.RemoveAllEmbeddingBackends(); + + // Assert + Assert.Equal(4, target.GetAllCompletionBackends().Count()); + Assert.Empty(target.GetAllEmbeddingsBackends()); + } + + [Fact] + public void ItCanRemoveOneCompletionBackend() + { + // Arrange + var target = new KernelConfig(); + target.AddAzureOpenAICompletionBackend("1", "dep", "https://localhost", "key"); + target.AddAzureOpenAICompletionBackend("2", "dep", "https://localhost", "key"); + target.AddOpenAICompletionBackend("3", "model", "key"); + Assert.Equal("1", target.DefaultCompletionBackend); + + // Act - Assert + target.RemoveCompletionBackend("1"); + Assert.Equal("2", target.DefaultCompletionBackend); + target.RemoveCompletionBackend("2"); + Assert.Equal("3", target.DefaultCompletionBackend); + target.RemoveCompletionBackend("3"); + Assert.Null(target.DefaultCompletionBackend); + } + + [Fact] + public void ItCanRemoveOneEmbeddingsBackend() + { + // Arrange + var target = new KernelConfig(); + target.AddAzureOpenAIEmbeddingsBackend("1", "dep", "https://localhost", "key"); + target.AddAzureOpenAIEmbeddingsBackend("2", "dep", "https://localhost", "key"); + target.AddOpenAIEmbeddingsBackend("3", "model", "key"); + Assert.Equal("1", target.DefaultEmbeddingsBackend); + + // Act - Assert + target.RemoveEmbeddingsBackend("1"); + Assert.Equal("2", target.DefaultEmbeddingsBackend); + target.RemoveEmbeddingsBackend("2"); + Assert.Equal("3", target.DefaultEmbeddingsBackend); + target.RemoveEmbeddingsBackend("3"); + Assert.Null(target.DefaultEmbeddingsBackend); + } +} diff --git a/dotnet/src/SemanticKernel.Test/CoreSkills/FileIOSkillTests.cs b/dotnet/src/SemanticKernel.Test/CoreSkills/FileIOSkillTests.cs new file mode 100644 index 000000000000..d5827af6f7b7 --- /dev/null +++ b/dotnet/src/SemanticKernel.Test/CoreSkills/FileIOSkillTests.cs @@ -0,0 +1,104 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.IO; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.CoreSkills; +using Microsoft.SemanticKernel.Memory; +using Microsoft.SemanticKernel.Orchestration; +using Xunit; + +namespace SemanticKernelTests.CoreSkills; + +public class FileIOSkillTests +{ + private readonly SKContext _context = new(new ContextVariables(), NullMemory.Instance, null, NullLogger.Instance); + + [Fact] + public void ItCanBeInstantiated() + { + // Act - Assert no exception occurs + _ = new FileIOSkill(); + } + + [Fact] + public void ItCanBeImported() + { + // Arrange + var kernel = KernelBuilder.Create(); + + // Act - Assert no exception occurs e.g. due to reflection + _ = kernel.ImportSkill(new FileIOSkill(), "fileIO"); + } + + [Fact] + public async Task ItCanReadAsync() + { + // Arrange + var skill = new FileIOSkill(); + var path = Path.GetTempFileName(); + File.WriteAllText(path, "hello world"); + + // Act + var result = await skill.ReadAsync(path); + + // Assert + Assert.Equal("hello world", result); + } + + [Fact] + public async Task ItCannotReadAsync() + { + // Arrange + var skill = new FileIOSkill(); + var path = Path.GetTempFileName(); + File.Delete(path); + + // Act + Task Fn() + { + return skill.ReadAsync(path); + } + + // Assert + _ = await Assert.ThrowsAsync(Fn); + } + + [Fact] + public async Task ItCanWriteAsync() + { + // Arrange + var skill = new FileIOSkill(); + var path = Path.GetTempFileName(); + this._context["path"] = path; + this._context["content"] = "hello world"; + + // Act + await skill.WriteAsync(this._context); + + // Assert + Assert.Equal("hello world", await File.ReadAllTextAsync(path)); + } + + [Fact] + public async Task ItCannotWriteAsync() + { + // Arrange + var skill = new FileIOSkill(); + var path = Path.GetTempFileName(); + File.SetAttributes(path, FileAttributes.ReadOnly); + this._context["path"] = path; + this._context["content"] = "hello world"; + + // Act + Task Fn() + { + return skill.WriteAsync(this._context); + } + + // Assert + _ = await Assert.ThrowsAsync(Fn); + } +} diff --git a/dotnet/src/SemanticKernel.Test/CoreSkills/HttpSkillTests.cs b/dotnet/src/SemanticKernel.Test/CoreSkills/HttpSkillTests.cs new file mode 100644 index 000000000000..b944d401f039 --- /dev/null +++ b/dotnet/src/SemanticKernel.Test/CoreSkills/HttpSkillTests.cs @@ -0,0 +1,150 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.CoreSkills; +using Microsoft.SemanticKernel.Memory; +using Microsoft.SemanticKernel.Orchestration; +using Moq; +using Moq.Protected; +using Xunit; + +namespace SemanticKernelTests.CoreSkills; + +public class HttpSkillTests : IDisposable +{ + private readonly SKContext _context = new SKContext(new ContextVariables(), NullMemory.Instance, null, NullLogger.Instance); + private readonly string _content = "hello world"; + private readonly string _uriString = "http://www.example.com"; + + private readonly HttpResponseMessage _response = new() + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent("hello world"), + }; + + [Fact] + public void ItCanBeInstantiated() + { + // Act - Assert no exception occurs + using var skill = new HttpSkill(); + } + + [Fact] + public void ItCanBeImported() + { + // Arrange + var kernel = KernelBuilder.Create(); + using var skill = new HttpSkill(); + + // Act - Assert no exception occurs e.g. due to reflection + kernel.ImportSkill(skill, "http"); + } + + [Fact] + public async Task ItCanGetAsync() + { + // Arrange + var mockHandler = this.CreateMock(); + using var client = new HttpClient(mockHandler.Object); + using var skill = new HttpSkill(client); + + // Act + var result = await skill.GetAsync(this._uriString); + + // Assert + Assert.Equal(this._content, result); + this.VerifyMock(mockHandler, HttpMethod.Get); + } + + [Fact] + public async Task ItCanPostAsync() + { + // Arrange + var mockHandler = this.CreateMock(); + using var client = new HttpClient(mockHandler.Object); + using var skill = new HttpSkill(client); + this._context["body"] = this._content; + + // Act + var result = await skill.PostAsync(this._uriString, this._context); + + // Assert + Assert.Equal(this._content, result); + this.VerifyMock(mockHandler, HttpMethod.Post); + } + + [Fact] + public async Task ItCanPutAsync() + { + // Arrange + var mockHandler = this.CreateMock(); + using var client = new HttpClient(mockHandler.Object); + using var skill = new HttpSkill(client); + this._context["body"] = this._content; + + // Act + var result = await skill.PutAsync(this._uriString, this._context); + + // Assert + Assert.Equal(this._content, result); + this.VerifyMock(mockHandler, HttpMethod.Put); + } + + [Fact] + public async Task ItCanDeleteAsync() + { + // Arrange + var mockHandler = this.CreateMock(); + using var client = new HttpClient(mockHandler.Object); + using var skill = new HttpSkill(client); + + // Act + var result = await skill.DeleteAsync(this._uriString); + + // Assert + Assert.Equal(this._content, result); + this.VerifyMock(mockHandler, HttpMethod.Delete); + } + + private Mock CreateMock() + { + var mockHandler = new Mock(); + mockHandler.Protected() + .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) + .ReturnsAsync(this._response); + return mockHandler; + } + + private void VerifyMock(Mock mockHandler, HttpMethod method) + { + mockHandler.Protected().Verify( + "SendAsync", + Times.Exactly(1), // we expected a single external request + ItExpr.Is(req => + req.Method == method // we expected a POST request + && req.RequestUri == new Uri(this._uriString) // to this uri + ), + ItExpr.IsAny() + ); + } + + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + this._response.Dispose(); + } + } +} diff --git a/dotnet/src/SemanticKernel.Test/CoreSkills/PlannerSkillTests.cs b/dotnet/src/SemanticKernel.Test/CoreSkills/PlannerSkillTests.cs new file mode 100644 index 000000000000..17873f8ac75b --- /dev/null +++ b/dotnet/src/SemanticKernel.Test/CoreSkills/PlannerSkillTests.cs @@ -0,0 +1,209 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Threading.Tasks; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.CoreSkills; +using Microsoft.SemanticKernel.Orchestration; +using Microsoft.SemanticKernel.Orchestration.Extensions; +using Microsoft.SemanticKernel.Planning; +using Xunit; +using Xunit.Abstractions; + +namespace SemanticKernelTests.CoreSkills; + +public class PlannerSkillTests +{ + private readonly ITestOutputHelper _testOutputHelper; + + private const string FunctionFlowRunnerText = @" + +Solve the equation x^2 = 2. + + + + +"; + + private const string GoalText = "Solve the equation x^2 = 2."; + + public PlannerSkillTests(ITestOutputHelper testOutputHelper) + { + this._testOutputHelper = testOutputHelper; + this._testOutputHelper.WriteLine("Tests initialized"); + } + + [Fact] + public void ItCanBeInstantiated() + { + // Arrange + var kernel = KernelBuilder.Create(); + kernel.Config.AddOpenAICompletionBackend("test", "test", "test"); + + // Act - Assert no exception occurs + var _ = new PlannerSkill(kernel); + } + + [Fact] + public void ItCanBeImported() + { + // Arrange + var kernel = KernelBuilder.Create(); + kernel.Config.AddOpenAICompletionBackend("test", "test", "test"); + + // Act - Assert no exception occurs e.g. due to reflection + kernel.ImportSkill(new PlannerSkill(kernel), "planner"); + } + + [Fact] + public async Task ItCanCreatePlanAsync() + { + // Arrange + var kernel = KernelBuilder.Create(); + kernel.Config.AddOpenAICompletionBackend("test", "test", "test"); + var plannerSkill = new PlannerSkill(kernel); + var planner = kernel.ImportSkill(plannerSkill, "planner"); + + // Act + var context = await kernel.RunAsync(GoalText, planner["CreatePlan"]); + + // Assert + var plan = context.Variables.ToPlan(); + Assert.NotNull(plan); + Assert.NotNull(plan.Id); + Assert.Equal(GoalText, plan.Goal); + Assert.StartsWith("\nSolve the equation x^2 = 2.\n", plan.PlanString, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public async Task ItCanExecutePlanTextAsync() + { + // Arrange + var kernel = KernelBuilder.Create(); + kernel.Config.AddOpenAICompletionBackend("test", "test", "test"); + var plannerSkill = kernel.ImportSkill(new PlannerSkill(kernel)); + + // Act + var context = await kernel.RunAsync(FunctionFlowRunnerText, plannerSkill["ExecutePlan"]); + + // Assert + var plan = context.Variables.ToPlan(); + Assert.NotNull(plan); + Assert.NotNull(plan.Id); + + // Since not using Plan or PlanExecution object, this won't be present. + // Maybe we do work to parse this out. Not doing too much though since we might move to json instead of xml. + // Assert.Equal(GoalText, plan.Goal); + } + + [Fact] + public async Task ItCanExecutePlanAsync() + { + // Arrange + var kernel = KernelBuilder.Create(); + kernel.Config.AddOpenAICompletionBackend("test", "test", "test"); + var plannerSkill = kernel.ImportSkill(new PlannerSkill(kernel)); + Plan createdPlan = new() + { + Goal = GoalText, + PlanString = FunctionFlowRunnerText + }; + + // Act + var variables = new ContextVariables(); + variables.UpdateWithPlanEntry(createdPlan); + var context = await kernel.RunAsync(variables, plannerSkill["ExecutePlan"]); + + // Assert + var plan = context.Variables.ToPlan(); + Assert.NotNull(plan); + Assert.NotNull(plan.Id); + Assert.Equal(GoalText, plan.Goal); + } + + [Fact] + public async Task ItCanCreateSkillPlanAsync() + { + // Arrange + var kernel = KernelBuilder.Create(); + kernel.Config.AddOpenAICompletionBackend("test", "test", "test"); + var plannerSkill = kernel.ImportSkill(new PlannerSkill(kernel)); + + // Act + var context = await kernel.RunAsync(GoalText, plannerSkill["CreatePlan"]); + + // Assert + var plan = context.Variables.ToPlan(); + Assert.NotNull(plan); + Assert.NotNull(plan.Id); + Assert.Equal(GoalText, plan.Goal); + Assert.StartsWith("\nSolve the equation x^2 = 2.\n", plan.PlanString, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public async Task ItCanExecutePlanJsonAsync() + { + // Arrange + var kernel = KernelBuilder.Create(); + kernel.Config.AddOpenAICompletionBackend("test", "test", "test"); + var plannerSkill = kernel.ImportSkill(new PlannerSkill(kernel)); + Plan createdPlan = new() + { + Goal = GoalText, + PlanString = FunctionFlowRunnerText + }; + + // Act + var context = await kernel.RunAsync(createdPlan.ToJson(), plannerSkill["ExecutePlan"]); + + // Assert + var plan = context.Variables.ToPlan(); + Assert.NotNull(plan); + Assert.NotNull(plan.Id); + Assert.Equal(GoalText, plan.Goal); + } + + [Fact] + public async Task NoGoalExecutePlanReturnsInvalidResultAsync() + { + // Arrange + var kernel = KernelBuilder.Create(); + kernel.Config.AddOpenAICompletionBackend("test", "test", "test"); + var plannerSkill = kernel.ImportSkill(new PlannerSkill(kernel)); + + // Act + var context = await kernel.RunAsync(GoalText, plannerSkill["ExecutePlan"]); + + // Assert + var plan = context.Variables.ToPlan(); + Assert.NotNull(plan); + Assert.NotNull(plan.Id); + Assert.Equal(string.Empty, plan.Goal); + Assert.Equal(GoalText, plan.PlanString); + Assert.False(plan.IsSuccessful); + Assert.True(plan.IsComplete); + Assert.Contains("No goal found.", plan.Result, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public async Task InvalidPlanExecutePlanReturnsInvalidResultAsync() + { + // Arrange + var kernel = KernelBuilder.Create(); + kernel.Config.AddOpenAICompletionBackend("test", "test", "test"); + var plannerSkill = kernel.ImportSkill(new PlannerSkill(kernel)); + + // Act + var context = await kernel.RunAsync("" + GoalText, plannerSkill["ExecutePlan"]); + + // Assert + var plan = context.Variables.ToPlan(); + Assert.NotNull(plan); + Assert.NotNull(plan.Id); + Assert.Equal(string.Empty, plan.Goal); + Assert.Equal("" + GoalText, plan.PlanString); + Assert.False(plan.IsSuccessful); + Assert.True(plan.IsComplete); + Assert.Contains("Failed to parse plan xml.", plan.Result, StringComparison.OrdinalIgnoreCase); + } +} diff --git a/dotnet/src/SemanticKernel.Test/CoreSkills/TextSkillTests.cs b/dotnet/src/SemanticKernel.Test/CoreSkills/TextSkillTests.cs new file mode 100644 index 000000000000..fe79ecc12bbe --- /dev/null +++ b/dotnet/src/SemanticKernel.Test/CoreSkills/TextSkillTests.cs @@ -0,0 +1,92 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.CoreSkills; +using Xunit; + +namespace SemanticKernelTests.CoreSkills; + +public class TextSkillTests +{ + [Fact] + public void ItCanBeInstantiated() + { + // Act - Assert no exception occurs + var _ = new TextSkill(); + } + + [Fact] + public void ItCanBeImported() + { + // Arrange + var kernel = KernelBuilder.Create(); + + // Act - Assert no exception occurs e.g. due to reflection + kernel.ImportSkill(new TextSkill(), "text"); + } + + [Fact] + public void ItCanTrim() + { + // Arrange + var skill = new TextSkill(); + + // Act + var result = skill.Trim(" hello world "); + + // Assert + Assert.Equal("hello world", result); + } + + [Fact] + public void ItCanTrimStart() + { + // Arrange + var skill = new TextSkill(); + + // Act + var result = skill.TrimStart(" hello world "); + + // Assert + Assert.Equal("hello world ", result); + } + + [Fact] + public void ItCanTrimEnd() + { + // Arrange + var skill = new TextSkill(); + + // Act + var result = skill.TrimEnd(" hello world "); + + // Assert + Assert.Equal(" hello world", result); + } + + [Fact] + public void ItCanUppercase() + { + // Arrange + var skill = new TextSkill(); + + // Act + var result = skill.Uppercase("hello world"); + + // Assert + Assert.Equal("HELLO WORLD", result); + } + + [Fact] + public void ItCanLowercase() + { + // Arrange + var skill = new TextSkill(); + + // Act + var result = skill.Lowercase("HELLO WORLD"); + + // Assert + Assert.Equal("hello world", result); + } +} diff --git a/dotnet/src/SemanticKernel.Test/CoreSkills/TimeSkillTests.cs b/dotnet/src/SemanticKernel.Test/CoreSkills/TimeSkillTests.cs new file mode 100644 index 000000000000..ec47b1835499 --- /dev/null +++ b/dotnet/src/SemanticKernel.Test/CoreSkills/TimeSkillTests.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.CoreSkills; +using Xunit; + +namespace SemanticKernelTests.CoreSkills; + +// TODO: allow clock injection and test all functions +public class TimeSkillTests +{ + [Fact] + public void ItCanBeInstantiated() + { + // Act - Assert no exception occurs + var _ = new TimeSkill(); + } + + [Fact] + public void ItCanBeImported() + { + // Arrange + var kernel = KernelBuilder.Create(); + + // Act - Assert no exception occurs e.g. due to reflection + kernel.ImportSkill(new TimeSkill(), "time"); + } +} diff --git a/dotnet/src/SemanticKernel.Test/KernelTests.cs b/dotnet/src/SemanticKernel.Test/KernelTests.cs new file mode 100644 index 000000000000..8295a9804d42 --- /dev/null +++ b/dotnet/src/SemanticKernel.Test/KernelTests.cs @@ -0,0 +1,178 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.KernelExtensions; +using Microsoft.SemanticKernel.Orchestration; +using Microsoft.SemanticKernel.Orchestration.Extensions; +using Microsoft.SemanticKernel.SkillDefinition; +using Xunit; + +// ReSharper disable StringLiteralTypo + +namespace SemanticKernelTests; + +public class KernelTests +{ + [Fact] + public void ItProvidesAccessToFunctionsViaSkillCollection() + { + // Arrange + var kernel = KernelBuilder.Create(); + kernel.Config.AddOpenAICompletionBackend("x", "y", "z"); + + var nativeSkill = new MySkill(); + kernel.CreateSemanticFunction(promptTemplate: "Tell me a joke", functionName: "joker", skillName: "jk", description: "Nice fun"); + kernel.ImportSkill(nativeSkill, "mySk"); + + // Act + FunctionsView data = kernel.Skills.GetFunctionsView(); + + // Assert - 3 functions, var name is not case sensitive + Assert.True(data.IsSemantic("jk", "joker")); + Assert.True(data.IsSemantic("JK", "JOKER")); + Assert.False(data.IsNative("jk", "joker")); + Assert.False(data.IsNative("JK", "JOKER")); + Assert.True(data.IsNative("mySk", "sayhello")); + Assert.True(data.IsNative("MYSK", "SayHello")); + Assert.True(data.IsNative("mySk", "ReadSkillCollectionAsync")); + Assert.True(data.IsNative("MYSK", "readskillcollectionasync")); + Assert.Single(data.SemanticFunctions["Jk"]); + Assert.Equal(3, data.NativeFunctions["mySk"].Count); + } + + [Fact] + public async Task ItProvidesAccessToFunctionsViaSKContextAsync() + { + // Arrange + var kernel = KernelBuilder.Create(); + kernel.Config.AddOpenAICompletionBackend("x", "y", "z"); + + var nativeSkill = new MySkill(); + kernel.CreateSemanticFunction("Tell me a joke", functionName: "joker", skillName: "jk", description: "Nice fun"); + var skill = kernel.ImportSkill(nativeSkill, "mySk"); + + // Act + SKContext result = await kernel.RunAsync(skill["ReadSkillCollectionAsync"]); + + // Assert - 3 functions, var name is not case sensitive + Assert.Equal("Nice fun", result["jk.joker"]); + Assert.Equal("Nice fun", result["JK.JOKER"]); + Assert.Equal("Just say hello", result["mySk.sayhello"]); + Assert.Equal("Just say hello", result["mySk.SayHello"]); + Assert.Equal("Export info.", result["mySk.ReadSkillCollectionAsync"]); + Assert.Equal("Export info.", result["mysk.readskillcollectionasync"]); + } + + [Fact] + public async Task RunAsyncDoesNotRunWhenCancelledAsync() + { + // Arrange + var kernel = KernelBuilder.Create(); + var nativeSkill = new MySkill(); + var skill = kernel.ImportSkill(nativeSkill, "mySk"); + + using CancellationTokenSource cts = new CancellationTokenSource(); + cts.Cancel(); + + // Act + SKContext result = await kernel.RunAsync(cts.Token, skill["GetAnyValue"]); + + // Assert + Assert.True(string.IsNullOrEmpty(result.Result)); + Assert.True(result.ErrorOccurred); + Assert.True(result.LastException is OperationCanceledException); + } + + [Fact] + public async Task RunAsyncRunsWhenNotCancelledAsync() + { + // Arrange + var kernel = KernelBuilder.Create(); + var nativeSkill = new MySkill(); + kernel.ImportSkill(nativeSkill, "mySk"); + + using CancellationTokenSource cts = new CancellationTokenSource(); + + // Act + SKContext result = await kernel.RunAsync(cts.Token, kernel.Func("mySk", "GetAnyValue")); + + // Assert + Assert.False(string.IsNullOrEmpty(result.Result)); + Assert.False(result.ErrorOccurred); + Assert.False(result.LastException is OperationCanceledException); + } + + [Fact] + public void ItImportsSkillsNotCaseSensitive() + { + // Act + IDictionary skill = KernelBuilder.Create().ImportSkill(new MySkill(), "test"); + + // Assert + Assert.Equal(3, skill.Count); + Assert.True(skill.ContainsKey("GetAnyValue")); + Assert.True(skill.ContainsKey("getanyvalue")); + Assert.True(skill.ContainsKey("GETANYVALUE")); + } + + [Fact] + public void ItAllowsToImportSkillsInTheGlobalNamespace() + { + // Arrange + var kernel = KernelBuilder.Create(); + + // Act + IDictionary skill = kernel.ImportSkill(new MySkill()); + + // Assert + Assert.Equal(3, skill.Count); + Assert.True(kernel.Skills.HasNativeFunction("GetAnyValue")); + } + + public class MySkill + { + [SKFunction("Return any value.")] + public string GetAnyValue() + { + return Guid.NewGuid().ToString(); + } + + [SKFunction("Just say hello")] + public void SayHello() + { + Console.WriteLine("Hello folks!"); + } + + [SKFunction("Export info.")] + public async Task ReadSkillCollectionAsync(SKContext context) + { + await Task.Delay(0); + + context.ThrowIfSkillCollectionNotSet(); + + FunctionsView procMem = context.Skills!.GetFunctionsView(); + + foreach (KeyValuePair> list in procMem.SemanticFunctions) + { + foreach (FunctionView f in list.Value) + { + context[$"{list.Key}.{f.Name}"] = f.Description; + } + } + + foreach (KeyValuePair> list in procMem.NativeFunctions) + { + foreach (FunctionView f in list.Value) + { + context[$"{list.Key}.{f.Name}"] = f.Description; + } + } + + return context; + } + } +} diff --git a/dotnet/src/SemanticKernel.Test/Memory/Storage/DataEntryTests.cs b/dotnet/src/SemanticKernel.Test/Memory/Storage/DataEntryTests.cs new file mode 100644 index 000000000000..a6fcbd8c7986 --- /dev/null +++ b/dotnet/src/SemanticKernel.Test/Memory/Storage/DataEntryTests.cs @@ -0,0 +1,239 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using Microsoft.SemanticKernel.Diagnostics; +using Microsoft.SemanticKernel.Memory.Storage; +using Xunit; + +namespace SemanticKernelTests.Memory.Storage; + +/// +/// Unit tests of . +/// +public class DataEntryTests +{ + [Fact] + public void ItCannotHaveNullKey() + { + Assert.Throws(() => DataEntry.Create(null!, "test_value")); + } + + [Fact] + public void ItCannotHaveEmptyKeyName() + { + Assert.Throws(() => DataEntry.Create(string.Empty, "test_value")); + } + + [Fact] + public void ItWillSetNullValueTypeInputToNonNullValueInt() + { + // Arrange + var target = DataEntry.Create("test_key", null!); + + // Assert + Assert.Equal("test_key", target.Key); + Assert.Equal(0, target.Value); + Assert.True(target.HasValue); + } + + [Fact] + public void ItWillSetNullValueTypeInputToNonNullValueFloat() + { + // Arrange + var target = DataEntry.Create("test_key", null!); + + // Assert + Assert.Equal("test_key", target.Key); + Assert.Equal(0.0F, target.Value); + Assert.True(target.HasValue); + } + + [Fact] + public void ItWillSetNullValueTypeInputToNonNullValueDouble() + { + // Arrange + var target = DataEntry.Create("test_key", null!); + + // Assert + Assert.Equal("test_key", target.Key); + Assert.Equal(0.0, target.Value); + Assert.True(target.HasValue); + } + + [Fact] + public void ItWillSetNullValueTypeInputToNonNullValueBool() + { + // Arrange + var target = DataEntry.Create("test_key", null!); + + // Assert + Assert.Equal("test_key", target.Key); + Assert.False(target.Value); + Assert.True(target.HasValue); + } + + [Fact] + public void ItWillSetNullReferenceTypeInputToNullString() + { + // Arrange + var target = DataEntry.Create("test_key", null!); + + // Assert + Assert.Equal("test_key", target.Key); + Assert.Null(target.Value); + Assert.False(target.HasValue); + } + + [Fact] + public void ItWillSetNullReferenceTypeInputToNullObject() + { + // Arrange + var target = DataEntry.Create("test_key", null!); + + // Assert + Assert.Equal("test_key", target.Key); + Assert.Null(target.Value); + Assert.False(target.HasValue); + } + + [Fact] + public void ItWillSetNullReferenceTypeInputToNullDynamic() + { + // Arrange + var target = DataEntry.Create("test_key", null!); + + // Assert + Assert.Equal("test_key", target.Key); + Assert.Null(target.Value); + Assert.False(target.HasValue); + } + + [Fact] + public void ItCanCreateMemoryEntryWithNoTimestamp() + { + // Arrange + var target = DataEntry.Create("test_key", "test_value"); + + // Assert + Assert.Equal("test_key", target.Key); + Assert.Equal("test_value", target.Value); + Assert.True(target.HasValue); + Assert.False(target.Timestamp.HasValue); + Assert.Null(target.Timestamp); + } + + [Fact] + public void ItCanCreateMemoryEntryWithTimestamp() + { + // Arrange + var target = DataEntry.Create("test_key", 10, new DateTimeOffset(2020, 1, 1, 0, 0, 0, TimeSpan.Zero)); + + // Assert + Assert.Equal("test_key", target.Key); + Assert.Equal(10, target.Value); + Assert.True(target.HasValue); + Assert.True(target.Timestamp.HasValue); + Assert.NotNull(target.Timestamp); + } + + [Fact] + public void ItCanSetValue() + { + // Arrange + var target = DataEntry.Create("test_key", "test_value"); + + // Act + target.Value = "new_value"; + + // Assert + Assert.Equal("new_value", target.Value); + Assert.True(target.HasValue); + } + + [Fact] + public void ItCanSetTimestamp() + { + // Arrange + var target = DataEntry.Create("test_key", 10); + + // Act + target.Timestamp = new DateTimeOffset(2020, 1, 1, 0, 0, 0, TimeSpan.Zero); + + // Assert + Assert.Equal(new DateTimeOffset(2020, 1, 1, 0, 0, 0, TimeSpan.Zero), target.Timestamp); + Assert.True(target.HasValue); + } + + [Fact] + public void ItCanCheckForEquality() + { + // Arrange + var target = DataEntry.Create("test_key", 10); + + // Assert + Assert.Equal(target, target); + Assert.Equal(DataEntry.Create("test_key", 10), target); + Assert.NotEqual(DataEntry.Create("test_key", 9), target); + Assert.True(DataEntry.Create("test_key", 10) == target); + Assert.True(DataEntry.Create("test_key", 9) != target); + } + + [Fact] + public void ItCanHashTheCollectionInformation() + { + // Arrange + var entry = DataEntry.Create("test_key", 10.875F); + + // Act + var target = entry.GetHashCode(); + + // Assert + Assert.IsType(target); + Assert.True(target != 0); + } + + [Fact] + public void ItCanSerializeObjectToJson() + { + // Arrange + var entry = DataEntry.Create("test_key", 10.875F); + + // Act + var target = entry.ToString(); + + // Assert + Assert.IsType(target); + Assert.Contains("\"key\":\"test_key\"", target, StringComparison.Ordinal); + Assert.Contains("\"value\":10.875", target, StringComparison.Ordinal); + Assert.Contains("\"timestamp\":null", target, StringComparison.Ordinal); + } + + [Fact] + public void ItCanDeserializeStringToObject() + { + // Arrange + var entry = DataEntry.Create("test_key", 128.5F); + + // Act + var json = entry.ToString(); + var result = DataEntry.TryParse(json, out DataEntry? target); + + // Assert + Assert.True(result); + Assert.Equal(entry, target); + } + + [Fact] + public void ItWillReturnFalseIfDeserializationStringIsNotObjectString() + { + // Arrange + var badString = "abcdefghijklmnopqrstuv"; + + // Act + var result = DataEntry.TryParse(badString, out DataEntry? target); + + // Assert + Assert.False(result); + Assert.Null(target); + } +} diff --git a/dotnet/src/SemanticKernel.Test/Memory/Storage/VolatileDataStoreTests.cs b/dotnet/src/SemanticKernel.Test/Memory/Storage/VolatileDataStoreTests.cs new file mode 100644 index 000000000000..503984811787 --- /dev/null +++ b/dotnet/src/SemanticKernel.Test/Memory/Storage/VolatileDataStoreTests.cs @@ -0,0 +1,193 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.SemanticKernel.Memory.Storage; +using Xunit; + +namespace SemanticKernelTests.Memory.Storage; + +/// +/// Unit tests of . +/// +public class VolatileDataStoreTests +{ + private readonly VolatileDataStore _db; + + public VolatileDataStoreTests() + { + this._db = new(); + } + + [Fact] + public void ItSucceedsInitialization() + { + // Assert + Assert.NotNull(this._db); + } + +#pragma warning disable CA5394 // Random is an insecure random number generator + [Fact] + public async Task ItWillPutAndRetrieveNoTimestampAsync() + { + // Arrange + int rand = Random.Shared.Next(); + string collection = "collection" + rand; + string key = "key" + rand; + string value = "value" + rand; + + // Act + await this._db.PutValueAsync(collection, key, value); + + var actual = await this._db.GetValueAsync(collection, key); + + // Assert + Assert.NotNull(actual); + Assert.Equal(value, actual); + } + + [Fact] + public async Task ItWillPutAndRetrieveWithTimestampAsync() + { + // Arrange + int rand = Random.Shared.Next(); + string collection = "collection" + rand; + string key = "key" + rand; + string value = "value" + rand; + DateTimeOffset timestamp = DateTimeOffset.UtcNow; + + // Act + await this._db.PutValueAsync(collection, key, value, timestamp); + var actual = await this._db.GetAsync(collection, key); + + // Assert + Assert.NotNull(actual); + Assert.Equal(value, actual!.Value.Value); + Assert.True(timestamp.Date.Equals(actual!.Value.Timestamp?.Date)); + Assert.True((int)timestamp.TimeOfDay.TotalSeconds == (int?)actual!.Value.Timestamp?.TimeOfDay.TotalSeconds); + } + + [Fact] + public async Task ItWillPutAndRetrieveDataEntryWithTimestampAsync() + { + // Arrange + int rand = Random.Shared.Next(); + string collection = "collection" + rand; + string key = "key" + rand; + string value = "value" + rand; + DateTimeOffset timestamp = DateTimeOffset.UtcNow; + var data = DataEntry.Create(key, value, timestamp); + + // Act + var placed = await this._db.PutAsync(collection, data); + DataEntry? actual = await this._db.GetAsync(collection, key); + + // Assert + Assert.NotNull(actual); + Assert.True(placed.Equals(data)); + Assert.Equal(value, actual!.Value.Value); + Assert.True(timestamp.Date.Equals(actual!.Value.Timestamp?.Date)); + Assert.True((int)timestamp.TimeOfDay.TotalSeconds == (int?)actual!.Value.Timestamp?.TimeOfDay.TotalSeconds); + } + + [Fact] + public async Task ItWillPutAndDeleteDataEntryAsync() + { + // Arrange + int rand = Random.Shared.Next(); + string collection = "collection" + rand; + string key = "key" + rand; + string value = "value" + rand; + var data = DataEntry.Create(key, value, DateTimeOffset.UtcNow); + + // Act + await this._db.PutAsync(collection, data); + await this._db.RemoveAsync(collection, key); + var attempt = await this._db.GetAsync(collection, key); + + // Assert + Assert.Null(attempt); + } + +#pragma warning disable CA1851 // Possible multiple enumerations of 'IEnumerable' collection + [Fact] + public async Task ItWillListAllDatabaseCollectionsAsync() + { + // Arrange + int rand = Random.Shared.Next(); + string collection = "collection" + rand; + string key = "key" + rand; + string value = "value" + rand; + + // Act + await this._db.PutValueAsync(collection, key, value); + var collections = this._db.GetCollectionsAsync().ToEnumerable(); + + // Assert + Assert.NotNull(collections); + Assert.True(collections.Any(), "Collections is empty"); + Assert.True(collections.Contains(collection), "Collections do not contain the newly-created collection"); + } + + [Fact] + public async Task ItWillGetAllCollectionEntriesAsync() + { + // Arrange + int rand = Random.Shared.Next(); + string collection = "collection" + rand; + string key = "key" + rand; + string value = "value" + rand; + + // Act + for (int i = 0; i < 15; i++) + { + await this._db.PutValueAsync(collection, key + i, value); + } + + var getAllResults = this._db.GetAllAsync(collection).ToEnumerable(); + + // Assert + Assert.NotNull(getAllResults); + Assert.True(getAllResults.Any(), "Collections collection empty"); + Assert.True(getAllResults.Count() == 15, "Collections collection should have 15 entries"); + } + + [Fact] + public async Task ItWillRetrieveNothingIfKeyDoesNotExistAsync() + { + // Arrange + int rand = Random.Shared.Next(); + string collection = "collection" + rand; + string key = "key"; + string value = "value"; + + // Act + await this._db.PutValueAsync(collection, key, value); + var attempt = await this._db.GetAsync(collection, key + "1"); + + // Assert + Assert.Null(attempt); + } + + [Fact] + public async Task ItWillOverwriteExistingValueAsync() + { + // Arrange + int rand = Random.Shared.Next(); + string collection = "collection" + rand; + string key = "key"; + string value1 = "value1"; + string value2 = "value2"; + + // Act + await this._db.PutValueAsync(collection, key, value1); + await this._db.PutValueAsync(collection, key, value2); + var actual = await this._db.GetAsync(collection, key); + + // Assert + Assert.NotNull(actual); + Assert.NotEqual(value1, actual!.Value.Value); + Assert.Equal(value2, actual!.Value.Value); + } +} diff --git a/dotnet/src/SemanticKernel.Test/Memory/VolatileMemoryStoreTests.cs b/dotnet/src/SemanticKernel.Test/Memory/VolatileMemoryStoreTests.cs new file mode 100644 index 000000000000..a2a492305a26 --- /dev/null +++ b/dotnet/src/SemanticKernel.Test/Memory/VolatileMemoryStoreTests.cs @@ -0,0 +1,251 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.SemanticKernel.AI.Embeddings; +using Microsoft.SemanticKernel.Memory; +using Microsoft.SemanticKernel.Memory.Storage; +using Xunit; + +namespace SemanticKernelTests.Memory; + +internal class DoubleEmbeddingWithBasicMetadata : IEmbeddingWithMetadata +{ + public Embedding Embedding { get; } + + public string Metadata { get; } + + public DoubleEmbeddingWithBasicMetadata(Embedding embedding, string metadata) + { + this.Embedding = embedding; + this.Metadata = metadata; + } +} + +public class VolatileMemoryStoreTests +{ + private readonly VolatileMemoryStore _db; + + public VolatileMemoryStoreTests() + { + this._db = new(); + } + + [Fact] + public void InitializeDbConnectionSucceeds() + { + // Assert + Assert.NotNull(this._db); + } + +#pragma warning disable CA5394 // Random is an insecure random number generator + [Fact] + public async Task PutAndRetrieveNoTimestampSucceedsAsync() + { + // Arrange + int rand = Random.Shared.Next(); + string collection = "collection" + rand; + string key = "key"; + var embedding = new Embedding(new double[] { 1, 1, 1 }); + var memory = new DoubleEmbeddingWithBasicMetadata(embedding, "1 1 1"); + + // Act + await this._db.PutValueAsync(collection, key, memory); + var actual = await this._db.GetValueAsync(collection, key); + + // Assert + Assert.NotNull(actual); + Assert.Equal(memory, actual); + } + + [Fact] + public async Task PutAndRetrieveWithTimestampSucceedsAsync() + { + // Arrange + int rand = Random.Shared.Next(); + string collection = "collection" + rand; + string key = "key"; + var embedding = new Embedding(new double[] { 1, 2, 3 }); + var memory = new DoubleEmbeddingWithBasicMetadata(embedding, "1 2 3"); + DateTimeOffset timestamp = DateTimeOffset.UtcNow; + + // Act + await this._db.PutValueAsync(collection, key, memory, timestamp); + var actual = await this._db.GetAsync(collection, key); + + // Assert + Assert.NotNull(actual); + Assert.Equal(memory, actual!.Value.Value); + Assert.True(timestamp.Date.Equals(actual!.Value.Timestamp?.Date)); + Assert.True((int)timestamp.TimeOfDay.TotalSeconds == (int?)actual!.Value.Timestamp?.TimeOfDay.TotalSeconds); + } + + [Fact] + public async Task PutAndRetrieveDataEntryWithTimestampSucceedsAsync() + { + // Arrange + int rand = Random.Shared.Next(); + string collection = "collection" + rand; + string key = "key"; + var embedding = new Embedding(new double[] { 3, 2, 1 }); + var memory = new DoubleEmbeddingWithBasicMetadata(embedding, "3 2 1"); + DateTimeOffset timestamp = DateTimeOffset.UtcNow; + var data = new DataEntry>(key, memory, timestamp); + + // Act + await this._db.PutAsync(collection, data); + DataEntry>? actual = await this._db.GetAsync(collection, key); + + // Assert + Assert.NotNull(actual); + Assert.Equal(memory, actual!.Value.Value); + Assert.True(timestamp.Date.Equals(actual!.Value.Timestamp?.Date)); + Assert.True((int)timestamp.TimeOfDay.TotalSeconds == (int?)actual!.Value.Timestamp?.TimeOfDay.TotalSeconds); + } + + [Fact] + public async Task PutAndDeleteDataEntrySucceedsAsync() + { + // Arrange + int rand = Random.Shared.Next(); + string collection = "collection" + rand; + string key = "key"; + var embedding = new Embedding(new double[] { -1, -1, -1 }); + var memory = new DoubleEmbeddingWithBasicMetadata(embedding, "-1 -1 -1"); + var data = new DataEntry>(key, memory); + + // Act + await this._db.PutAsync(collection, data); + await this._db.RemoveAsync(collection, key); + + // Assert + Assert.Null(await this._db.GetAsync(collection, key)); + } + +#pragma warning disable CA1851 // Possible multiple enumerations of 'IEnumerable' collection + [Fact] + public async Task ListAllDatabaseCollectionsSucceedsAsync() + { + // Arrange + int rand = Random.Shared.Next(); + string collection = "collection" + rand; + string key = "key"; + var embedding = new Embedding(new double[] { 0, 0, 0 }); + var memory = new DoubleEmbeddingWithBasicMetadata(embedding, "0 0 0"); + + // Act + await this._db.PutValueAsync(collection, key, memory); + var collections = this._db.GetCollectionsAsync().ToEnumerable(); + + // Assert + Assert.NotNull(collections); + Assert.True(collections.Any(), "Collections is empty"); + Assert.True(collections.Contains(collection), "Collections do not contain the newly-created collection"); + } + + [Fact] + public async Task GetAllSucceedsAsync() + { + // Arrange + int rand = Random.Shared.Next(); + string collection = "collection" + rand; + string key = "key"; + var embedding = new Embedding(new double[] { 0, 0, 0 }); + var memory = new DoubleEmbeddingWithBasicMetadata(embedding, "0 0 0"); + + // Act + for (int i = 0; i < 15; i++) + { + await this._db.PutValueAsync(collection, key + i, memory); + } + + var getAllResults = this._db.GetAllAsync(collection).ToEnumerable(); + + // Assert + Assert.NotNull(getAllResults); + Assert.True(getAllResults.Any(), "Collections are empty"); + Assert.True(getAllResults.Count() == 15, "Collections should have 15 entries"); + } + + [Fact] + public async Task GetNearestAsyncReturnsExpectedNoMinScoreAsync() + { + // Arrange + var compareEmbedding = new Embedding(new double[] { 1, 1, 1 }); + int rand = Random.Shared.Next(); + string collection = "collection" + rand; + int topN = 4; + + string key = "key" + Random.Shared.Next(); + var memory = new DoubleEmbeddingWithBasicMetadata(new Embedding(new double[] { 1, 1, 1 }), "1 ,1 ,1"); + await this._db.PutValueAsync(collection, key, memory); + + key = "key" + Random.Shared.Next(); + memory = new DoubleEmbeddingWithBasicMetadata(new Embedding(new double[] { -1, -1, -1 }), "-1 ,-1 ,-1"); + await this._db.PutValueAsync(collection, key, memory); + + key = "key" + Random.Shared.Next(); + memory = new DoubleEmbeddingWithBasicMetadata(new Embedding(new double[] { 1, 2, 3 }), "1 ,2 ,3"); + await this._db.PutValueAsync(collection, key, memory); + + key = "key" + Random.Shared.Next(); + memory = new DoubleEmbeddingWithBasicMetadata(new Embedding(new double[] { -1, -2, -3 }), "-1 ,-2 ,-3"); + await this._db.PutValueAsync(collection, key, memory); + + key = "key" + Random.Shared.Next(); + memory = new DoubleEmbeddingWithBasicMetadata(new Embedding(new double[] { 1, -1, 2 }), "1 ,-1 ,2"); + await this._db.PutValueAsync(collection, key, memory); + + // Act + var topNResults = this._db.GetNearestMatchesAsync(collection, compareEmbedding, limit: topN, minRelevanceScore: -1).ToEnumerable().ToArray(); + + // Assert + Assert.Equal(topN, topNResults.Length); + for (int i = 0; i < topN - 1; i++) + { + int compare = topNResults[i].Item2.CompareTo(topNResults[i + 1].Item2); + Assert.True(compare >= 0); + } + } + + [Fact] + public async Task GetNearestAsyncReturnsExpectedWithMinScoreAsync() + { + // Arrange + var compareEmbedding = new Embedding(new double[] { 1, 1, 1 }); + int rand = Random.Shared.Next(); + string collection = "collection" + rand; + int topN = 4; + + string key = "key" + Random.Shared.Next(); + var memory = new DoubleEmbeddingWithBasicMetadata(new Embedding(new double[] { 1, 1, 1 }), "1 ,1 ,1"); + await this._db.PutValueAsync(collection, key, memory); + + key = "key" + Random.Shared.Next(); + memory = new DoubleEmbeddingWithBasicMetadata(new Embedding(new double[] { -1, -1, -1 }), "-1 ,-1 ,-1"); + await this._db.PutValueAsync(collection, key, memory); + + key = "key" + Random.Shared.Next(); + memory = new DoubleEmbeddingWithBasicMetadata(new Embedding(new double[] { 1, 2, 3 }), "1 ,2 ,3"); + await this._db.PutValueAsync(collection, key, memory); + + key = "key" + Random.Shared.Next(); + memory = new DoubleEmbeddingWithBasicMetadata(new Embedding(new double[] { -1, -2, -3 }), "-1 ,-2 ,-3"); + await this._db.PutValueAsync(collection, key, memory); + + key = "key" + Random.Shared.Next(); + memory = new DoubleEmbeddingWithBasicMetadata(new Embedding(new double[] { 1, -1, 2 }), "1 ,-1 ,2"); + await this._db.PutValueAsync(collection, key, memory); + + // Act + var topNResults = this._db.GetNearestMatchesAsync(collection, compareEmbedding, limit: topN, minRelevanceScore: 0.75).ToEnumerable().ToArray(); + + // Assert + for (int i = 0; i < topNResults.Length; i++) + { + int compare = topNResults[i].Item2.CompareTo(0.75); + Assert.True(compare >= 0); + } + } +} diff --git a/dotnet/src/SemanticKernel.Test/Orchestration/ContextVariablesTests.cs b/dotnet/src/SemanticKernel.Test/Orchestration/ContextVariablesTests.cs new file mode 100644 index 000000000000..1fc9a93f0a7d --- /dev/null +++ b/dotnet/src/SemanticKernel.Test/Orchestration/ContextVariablesTests.cs @@ -0,0 +1,187 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.SemanticKernel.Orchestration; +using Xunit; + +namespace SemanticKernelTests.Orchestration; + +/// +/// Unit tests of . +/// +public class ContextVariablesTests +{ + [Fact] + public void EnumerationOfContextVariableVariablesSucceeds() + { + // Arrange + string firstName = Guid.NewGuid().ToString(); + string firstValue = Guid.NewGuid().ToString(); + string secondName = Guid.NewGuid().ToString(); + string secondValue = Guid.NewGuid().ToString(); + + // Act + ContextVariables target = new ContextVariables(); + target.Set(firstName, firstValue); + target.Set(secondName, secondValue); + + // Assert + var items = target.ToArray(); + + Assert.Single(items.Where(i => i.Key == firstName && i.Value == firstValue)); + Assert.Single(items.Where(i => i.Key == secondName && i.Value == secondValue)); + } + + [Fact] + public void IndexGetAfterIndexSetSucceeds() + { + // Arrange + string anyName = Guid.NewGuid().ToString(); + string anyValue = Guid.NewGuid().ToString(); + ContextVariables target = new ContextVariables(); + + // Act + target[anyName] = anyValue; + + // Assert + Assert.Equal(anyValue, target[anyName]); + } + + [Fact] + public void IndexGetWithoutSetThrowsKeyNotFoundException() + { + // Arrange + string anyName = Guid.NewGuid().ToString(); + ContextVariables target = new ContextVariables(); + + // Act,Assert + Assert.Throws(() => target[anyName]); + } + + [Fact] + public void IndexSetAfterIndexSetSucceeds() + { + // Arrange + string anyName = Guid.NewGuid().ToString(); + string anyValue = Guid.NewGuid().ToString(); + ContextVariables target = new ContextVariables(); + + // Act + target[anyName] = anyValue; + target[anyName] = anyValue; + + // Assert + Assert.Equal(anyValue, target[anyName]); + } + + [Fact] + public void IndexSetWithoutGetSucceeds() + { + // Arrange + string anyName = Guid.NewGuid().ToString(); + string anyValue = Guid.NewGuid().ToString(); + ContextVariables target = new ContextVariables(); + + // Act + target[anyName] = anyValue; + + // Assert + Assert.Equal(anyValue, target[anyName]); + } + + [Fact] + public void SetAfterIndexSetSucceeds() + { + // Arrange + string anyName = Guid.NewGuid().ToString(); + string anyContent = Guid.NewGuid().ToString(); + ContextVariables target = new ContextVariables(); + + // Act + target[anyName] = anyContent; + target.Set(anyName, anyContent); + + // Assert + Assert.True(target.Get(anyName, out string _)); + } + + [Fact] + public void SetAfterSetSucceeds() + { + // Arrange + string anyName = Guid.NewGuid().ToString(); + string anyContent = Guid.NewGuid().ToString(); + ContextVariables target = new ContextVariables(); + + // Act + target.Set(anyName, anyContent); + target.Set(anyName, anyContent); + + // Assert + Assert.True(target.Get(anyName, out string _)); + } + + [Fact] + public void SetBeforeIndexSetSucceeds() + { + // Arrange + string anyName = Guid.NewGuid().ToString(); + string anyContent = Guid.NewGuid().ToString(); + ContextVariables target = new ContextVariables(); + + // Act + target.Set(anyName, anyContent); + target[anyName] = anyContent; + + // Assert + Assert.True(target.Get(anyName, out string _)); + } + + [Fact] + public void SetBeforeSetSucceeds() + { + // Arrange + string anyName = Guid.NewGuid().ToString(); + string anyContent = Guid.NewGuid().ToString(); + ContextVariables target = new ContextVariables(); + + // Act + target.Set(anyName, anyContent); + target.Set(anyName, anyContent); + + // Assert + Assert.True(target.Get(anyName, out string _)); + } + + [Fact] + public void SetWithoutGetSucceeds() + { + // Arrange + string anyName = Guid.NewGuid().ToString(); + string anyContent = Guid.NewGuid().ToString(); + ContextVariables target = new ContextVariables(); + + // Act + target.Set(anyName, anyContent); + + // Assert + Assert.True(target.Get(anyName, out string _)); + } + + [Fact] + public void SetWithoutLabelSucceeds() + { + // Arrange + string anyName = Guid.NewGuid().ToString(); + string anyContent = Guid.NewGuid().ToString(); + ContextVariables target = new ContextVariables(); + + // Act + target.Set(anyName, anyContent); + + // Assert + Assert.True(target.Get(anyName, out string _)); + } +} diff --git a/dotnet/src/SemanticKernel.Test/Orchestration/SKContextTests.cs b/dotnet/src/SemanticKernel.Test/Orchestration/SKContextTests.cs new file mode 100644 index 000000000000..565b2bb9c145 --- /dev/null +++ b/dotnet/src/SemanticKernel.Test/Orchestration/SKContextTests.cs @@ -0,0 +1,76 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Memory; +using Microsoft.SemanticKernel.Orchestration; +using Microsoft.SemanticKernel.SkillDefinition; +using Moq; +using Xunit; + +namespace SemanticKernelTests.Orchestration; + +public class SKContextTests +{ + private readonly Mock _skills; + private readonly Mock _log; + + public SKContextTests() + { + this._skills = new Mock(); + this._log = new Mock(); + } + + [Fact] + public void ItHasHelpersForContextVariables() + { + // Arrange + var variables = new ContextVariables(); + var target = new SKContext(variables, NullMemory.Instance, this._skills.Object, this._log.Object); + variables.Set("foo1", "bar1"); + + // Act + target["foo2"] = "bar2"; + target["INPUT"] = Guid.NewGuid().ToString("N"); + + // Assert + Assert.Equal("bar1", target["foo1"]); + Assert.Equal("bar1", target.Variables["foo1"]); + Assert.Equal("bar2", target["foo2"]); + Assert.Equal("bar2", target.Variables["foo2"]); + Assert.Equal(target["INPUT"], target.Result); + Assert.Equal(target["INPUT"], target.ToString()); + Assert.Equal(target["INPUT"], target.Variables.Input); + Assert.Equal(target["INPUT"], target.Variables.ToString()); + } + + [Fact] + public async Task ItHasHelpersForSkillCollectionAsync() + { + // Arrange + IDictionary skill = KernelBuilder.Create().ImportSkill(new Parrot(), "test"); + this._skills.Setup(x => x.GetNativeFunction("func")).Returns(skill["say"]); + var target = new SKContext(new ContextVariables(), NullMemory.Instance, this._skills.Object, this._log.Object); + Assert.NotNull(target.Skills); + + // Act + var say = target.Skills.GetNativeFunction("func"); + SKContext result = await say.InvokeAsync("ciao"); + + // Assert + Assert.Equal("ciao", result.Result); + } + + private class Parrot + { + [SKFunction("say something")] + // ReSharper disable once UnusedMember.Local + public string Say(string text) + { + return text; + } + } +} diff --git a/dotnet/src/SemanticKernel.Test/Orchestration/SKFunctionTests1.cs b/dotnet/src/SemanticKernel.Test/Orchestration/SKFunctionTests1.cs new file mode 100644 index 000000000000..d56aecd929ee --- /dev/null +++ b/dotnet/src/SemanticKernel.Test/Orchestration/SKFunctionTests1.cs @@ -0,0 +1,262 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Orchestration; +using Microsoft.SemanticKernel.SkillDefinition; +using SemanticKernelTests.XunitHelpers; +using Xunit; +using Xunit.Abstractions; + +namespace SemanticKernelTests.Orchestration; + +[SuppressMessage("", "CA1812:internal class apparently never instantiated", Justification = "Classes required by tests")] +public sealed class SKFunctionTests1 : IDisposable +{ + private readonly RedirectOutput _testOutputHelper; + + public SKFunctionTests1(ITestOutputHelper testOutputHelper) + { + this._testOutputHelper = new RedirectOutput(testOutputHelper); + Console.SetOut(this._testOutputHelper); + } + + [Fact] + public void ItDoesntThrowForValidFunctions() + { + // Arrange + var skillInstance = new LocalExampleSkill(); + MethodInfo[] methods = skillInstance.GetType() + .GetMethods(BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public | BindingFlags.InvokeMethod) + .Where(m => m.Name != "GetType" && m.Name != "Equals" && m.Name != "GetHashCode" && m.Name != "ToString") + .ToArray(); + + IEnumerable functions = from method in methods select SKFunction.FromNativeMethod(method, skillInstance, "skill"); + List result = (from function in functions where function != null select function).ToList(); + + // Act - Assert that no exception occurs and functions are not null + Assert.Equal(26, methods.Length); + Assert.Equal(26, result.Count); + foreach (var method in methods) + { + ISKFunction? func = SKFunction.FromNativeMethod(method, skillInstance, "skill"); + Assert.NotNull(func); + } + } + + [Fact] + public void ItThrowsForInvalidFunctions() + { + // Arrange + var instance = new InvalidSkill(); + MethodInfo[] methods = instance.GetType() + .GetMethods(BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public | BindingFlags.InvokeMethod) + .Where(m => m.Name != "GetType" && m.Name != "Equals" && m.Name != "GetHashCode") + .ToArray(); + + // Act - Assert that no exception occurs + var count = 0; + foreach (var method in methods) + { + try + { + SKFunction.FromNativeMethod(method, instance, "skill"); + } + catch (KernelException e) when (e.ErrorCode == KernelException.ErrorCodes.FunctionTypeNotSupported) + { + count++; + } + } + + // Assert + Assert.Equal(3, count); + } + + public void Dispose() + { + this._testOutputHelper.Dispose(); + } + + public class InvalidSkill + { + [SKFunction("one")] + public void Invalid1(string x, string y) + { + } + + [SKFunction("two")] + public void Invalid2(SKContext cx, string y) + { + } + + [SKFunction("three")] + public void Invalid3(string y, int n) + { + } + } + + public class LocalExampleSkill + { + [SKFunction("one")] + public void Type01() + { + } + + [SKFunction("two")] + public string Type02() + { + return ""; + } + + [SKFunction("two2")] + public string? Type02Nullable() + { + return null; + } + + [SKFunction("three")] + public async Task Type03Async() + { + await Task.Delay(0); + return ""; + } + + [SKFunction("three2")] + public async Task Type03NullableAsync() + { + await Task.Delay(0); + return null; + } + + [SKFunction("four")] + public void Type04(SKContext context) + { + } + + [SKFunction("four2")] + public void Type04Nullable(SKContext? context) + { + } + + [SKFunction("five")] + public string Type05(SKContext context) + { + return ""; + } + + [SKFunction("five2")] + public string? Type05Nullable(SKContext? context) + { + return null; + } + + [SKFunction("six")] + public async Task Type06Async(SKContext context) + { + await Task.Delay(0); + return ""; + } + + [SKFunction("seven")] + public async Task Type07Async(SKContext context) + { + await Task.Delay(0); + return context; + } + + [SKFunction("eight")] + public void Type08(string x) + { + } + + [SKFunction("eight2")] + public void Type08Nullable(string? x) + { + } + + [SKFunction("nine")] + public string Type09(string x) + { + return ""; + } + + [SKFunction("nine2")] + public string? Type09Nullable(string? x = null) + { + return ""; + } + + [SKFunction("ten")] + public async Task Type10Async(string x) + { + await Task.Delay(0); + return ""; + } + + [SKFunction("ten2")] + public async Task Type10NullableAsync(string? x) + { + await Task.Delay(0); + return ""; + } + + [SKFunction("eleven")] + public void Type11(string x, SKContext context) + { + } + + [SKFunction("eleven2")] + public void Type11Nullable(string? x = null, SKContext? context = null) + { + } + + [SKFunction("twelve")] + public string Type12(string x, SKContext context) + { + return ""; + } + + [SKFunction("thirteen")] + public async Task Type13Async(string x, SKContext context) + { + await Task.Delay(0); + return ""; + } + + [SKFunction("fourteen")] + public async Task Type14Async(string x, SKContext context) + { + await Task.Delay(0); + return context; + } + + [SKFunction("fifteen")] + public async Task Type15Async(string x) + { + await Task.Delay(0); + } + + [SKFunction("sixteen")] + public async Task Type16Async(SKContext context) + { + await Task.Delay(0); + } + + [SKFunction("seventeen")] + public async Task Type17Async(string x, SKContext context) + { + await Task.Delay(0); + } + + [SKFunction("eighteen")] + public async Task Type18Async() + { + await Task.Delay(0); + } + } +} diff --git a/dotnet/src/SemanticKernel.Test/Orchestration/SKFunctionTests2.cs b/dotnet/src/SemanticKernel.Test/Orchestration/SKFunctionTests2.cs new file mode 100644 index 000000000000..86b77f4fa5da --- /dev/null +++ b/dotnet/src/SemanticKernel.Test/Orchestration/SKFunctionTests2.cs @@ -0,0 +1,628 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.SemanticKernel.Memory; +using Microsoft.SemanticKernel.Orchestration; +using Microsoft.SemanticKernel.SkillDefinition; +using Moq; +using SemanticKernelTests.XunitHelpers; +using Xunit; +using Xunit.Abstractions; + +namespace SemanticKernelTests.Orchestration; + +[SuppressMessage("", "CA1812:internal class apparently never instantiated", Justification = "Classes required by tests")] +public sealed class SKFunctionTests2 : IDisposable +{ + private readonly RedirectOutput _testOutputHelper; + private readonly Mock _log; + private readonly Mock _skills; + + private static string s_expected = string.Empty; + private static string s_canary = string.Empty; + + public SKFunctionTests2(ITestOutputHelper testOutputHelper) + { + this._testOutputHelper = new RedirectOutput(testOutputHelper); + Console.SetOut(this._testOutputHelper); + + this._log = new Mock(); + this._skills = new Mock(); + + s_canary = ""; + s_expected = Guid.NewGuid().ToString("D"); + } + + [Fact] + public async Task ItSupportsType1Async() + { + // Arrange + [SKFunction("Test")] + [SKFunctionName("Test")] + static void Test() + { + s_canary = s_expected; + } + + var context = this.MockContext(""); + + // Act + var function = SKFunction.FromNativeMethod(Method(Test), log: this._log.Object); + Assert.NotNull(function); + SKContext result = await function.InvokeAsync(context); + + // Assert + Assert.False(result.ErrorOccurred); + this.VerifyFunctionTypeMatch(1); + Assert.Equal(s_expected, s_canary); + } + + [Fact] + public async Task ItSupportsType2Async() + { + // Arrange + [SKFunction("Test")] + [SKFunctionName("Test")] + static string Test() + { + s_canary = s_expected; + return s_expected; + } + + var context = this.MockContext(""); + + // Act + var function = SKFunction.FromNativeMethod(Method(Test), log: this._log.Object); + Assert.NotNull(function); + SKContext result = await function.InvokeAsync(context); + + // Assert + Assert.False(result.ErrorOccurred); + this.VerifyFunctionTypeMatch(2); + Assert.Equal(s_expected, s_canary); + Assert.Equal(s_expected, result.Result); + Assert.Equal(s_expected, context.Result); + } + + [Fact] + public async Task ItSupportsType3Async() + { + // Arrange + [SKFunction("Test")] + [SKFunctionName("Test")] + static Task Test() + { + s_canary = s_expected; + return Task.FromResult(s_expected); + } + + var context = this.MockContext(""); + + // Act + var function = SKFunction.FromNativeMethod(Method(Test), log: this._log.Object); + Assert.NotNull(function); + SKContext result = await function.InvokeAsync(context); + + // Assert + Assert.False(result.ErrorOccurred); + this.VerifyFunctionTypeMatch(3); + Assert.Equal(s_expected, s_canary); + Assert.Equal(s_expected, context.Result); + Assert.Equal(s_expected, result.Result); + } + + [Fact] + public async Task ItSupportsType4Async() + { + // Arrange + [SKFunction("Test")] + [SKFunctionName("Test")] + static void Test(SKContext cx) + { + s_canary = s_expected; + cx["canary"] = s_expected; + } + + var context = this.MockContext("xy"); + context["someVar"] = "qz"; + + // Act + var function = SKFunction.FromNativeMethod(Method(Test), log: this._log.Object); + Assert.NotNull(function); + SKContext result = await function.InvokeAsync(context); + + // Assert + Assert.False(result.ErrorOccurred); + this.VerifyFunctionTypeMatch(4); + Assert.Equal(s_expected, s_canary); + Assert.Equal(s_expected, context["canary"]); + } + + [Fact] + public async Task ItSupportsType5Async() + { + // Arrange + [SKFunction("Test")] + [SKFunctionName("Test")] + static string Test(SKContext cx) + { + s_canary = cx["someVar"]; + return "abc"; + } + + var context = this.MockContext(""); + context["someVar"] = s_expected; + + // Act + var function = SKFunction.FromNativeMethod(Method(Test), log: this._log.Object); + Assert.NotNull(function); + SKContext result = await function.InvokeAsync(context); + + // Assert + Assert.False(result.ErrorOccurred); + this.VerifyFunctionTypeMatch(5); + Assert.Equal(s_expected, s_canary); + Assert.Equal("abc", context.Result); + } + + [Fact] + public async Task ItSupportsType5NullableAsync() + { + // Arrange + [SKFunction("Test")] + [SKFunctionName("Test")] + string? Test(SKContext cx) + { + s_canary = cx["someVar"]; + return "abc"; + } + + var context = this.MockContext(""); + context["someVar"] = s_expected; + + // Act + var function = SKFunction.FromNativeMethod(Method(Test), log: this._log.Object); + Assert.NotNull(function); + SKContext result = await function.InvokeAsync(context); + + // Assert + Assert.False(result.ErrorOccurred); + this.VerifyFunctionTypeMatch(5); + Assert.Equal(s_expected, s_canary); + Assert.Equal("abc", context.Result); + } + + [Fact] + public async Task ItSupportsType6Async() + { + // Arrange + [SKFunction("Test")] + [SKFunctionName("Test")] + Task Test(SKContext cx) + { + s_canary = s_expected; + cx.Variables["canary"] = s_expected; + return Task.FromResult(s_expected); + } + + var context = this.MockContext(""); + + // Act + var function = SKFunction.FromNativeMethod(Method(Test), log: this._log.Object); + Assert.NotNull(function); + SKContext result = await function.InvokeAsync(context); + + // Assert + Assert.False(result.ErrorOccurred); + this.VerifyFunctionTypeMatch(6); + Assert.Equal(s_expected, s_canary); + Assert.Equal(s_canary, context.Result); + Assert.Equal(s_expected, context["canary"]); + } + + [Fact] + public async Task ItSupportsType7Async() + { + // Arrange + [SKFunction("Test")] + [SKFunctionName("Test")] + Task Test(SKContext cx) + { + s_canary = s_expected; + cx.Variables.Update("foo"); + cx["canary"] = s_expected; + return Task.FromResult(cx); + } + + var context = this.MockContext(""); + + // Act + var function = SKFunction.FromNativeMethod(Method(Test), log: this._log.Object); + Assert.NotNull(function); + SKContext result = await function.InvokeAsync(context); + + // Assert + Assert.False(result.ErrorOccurred); + this.VerifyFunctionTypeMatch(7); + Assert.Equal(s_expected, s_canary); + Assert.Equal(s_expected, context["canary"]); + Assert.Equal("foo", context.Result); + } + + [Fact] + public async Task ItSupportsType8Async() + { + // Arrange + [SKFunction("Test")] + [SKFunctionName("Test")] + void Test(string input) + { + s_canary = s_expected + input; + } + + var context = this.MockContext(".blah"); + + // Act + var function = SKFunction.FromNativeMethod(Method(Test), log: this._log.Object); + Assert.NotNull(function); + SKContext result = await function.InvokeAsync(context); + + // Assert + Assert.False(result.ErrorOccurred); + this.VerifyFunctionTypeMatch(8); + Assert.Equal(s_expected + ".blah", s_canary); + } + + [Fact] + public async Task ItSupportsType9Async() + { + // Arrange + [SKFunction("Test")] + [SKFunctionName("Test")] + string Test(string input) + { + s_canary = s_expected; + return "foo-bar"; + } + + var context = this.MockContext(""); + + // Act + var function = SKFunction.FromNativeMethod(Method(Test), log: this._log.Object); + Assert.NotNull(function); + SKContext result = await function.InvokeAsync(context); + + // Assert + Assert.False(result.ErrorOccurred); + this.VerifyFunctionTypeMatch(9); + Assert.Equal(s_expected, s_canary); + Assert.Equal("foo-bar", context.Result); + } + + [Fact] + public async Task ItSupportsType10Async() + { + // Arrange + [SKFunction("Test")] + [SKFunctionName("Test")] + Task Test(string input) + { + s_canary = s_expected; + return Task.FromResult("hello there"); + } + + var context = this.MockContext(""); + + // Act + var function = SKFunction.FromNativeMethod(Method(Test), log: this._log.Object); + Assert.NotNull(function); + SKContext result = await function.InvokeAsync(context); + + // Assert + Assert.False(result.ErrorOccurred); + this.VerifyFunctionTypeMatch(10); + Assert.Equal(s_expected, s_canary); + Assert.Equal("hello there", context.Result); + } + + [Fact] + public async Task ItSupportsType11Async() + { + // Arrange + [SKFunction("Test")] + [SKFunctionName("Test")] + void Test(string input, SKContext cx) + { + s_canary = s_expected; + cx.Variables.Update("x y z"); + cx["canary"] = s_expected; + } + + var context = this.MockContext(""); + + // Act + var function = SKFunction.FromNativeMethod(Method(Test), log: this._log.Object); + Assert.NotNull(function); + SKContext result = await function.InvokeAsync(context); + + // Assert + Assert.False(result.ErrorOccurred); + this.VerifyFunctionTypeMatch(11); + Assert.Equal(s_expected, s_canary); + Assert.Equal(s_expected, context["canary"]); + Assert.Equal("x y z", context.Result); + } + + [Fact] + public async Task ItSupportsType12Async() + { + // Arrange + [SKFunction("Test")] + [SKFunctionName("Test")] + static string Test(string input, SKContext cx) + { + s_canary = s_expected; + cx["canary"] = s_expected; + cx.Variables.Update("x y z"); + // This value should overwrite "x y z" + return "new data"; + } + + var context = this.MockContext(""); + + // Act + var function = SKFunction.FromNativeMethod(Method(Test), log: this._log.Object); + Assert.NotNull(function); + SKContext result = await function.InvokeAsync(context); + + // Assert + Assert.False(result.ErrorOccurred); + this.VerifyFunctionTypeMatch(12); + Assert.Equal(s_expected, s_canary); + Assert.Equal(s_expected, context["canary"]); + Assert.Equal("new data", context.Result); + } + + [Fact] + public async Task ItSupportsType13Async() + { + // Arrange + [SKFunction("Test")] + [SKFunctionName("Test")] + static Task Test(string input, SKContext cx) + { + s_canary = s_expected; + cx["canary"] = s_expected; + cx.Variables.Update("x y z"); + // This value should overwrite "x y z" + return Task.FromResult("new data"); + } + + var context = this.MockContext(""); + + // Act + var function = SKFunction.FromNativeMethod(Method(Test), log: this._log.Object); + Assert.NotNull(function); + SKContext result = await function.InvokeAsync(context); + + // Assert + Assert.False(result.ErrorOccurred); + this.VerifyFunctionTypeMatch(13); + Assert.Equal(s_expected, s_canary); + Assert.Equal(s_expected, context["canary"]); + Assert.Equal("new data", context.Result); + } + + [Fact] + public async Task ItSupportsType14Async() + { + // Arrange + [SKFunction("Test")] + [SKFunctionName("Test")] + static Task Test(string input, SKContext cx) + { + s_canary = s_expected; + cx["canary"] = s_expected; + cx.Variables.Update("x y z"); + + // This value should overwrite "x y z". Contexts are merged. + var newCx = new SKContext( + new ContextVariables(input), + NullMemory.Instance, + new Mock().Object, + NullLogger.Instance); + + newCx.Variables.Update("new data"); + newCx["canary2"] = "222"; + + return Task.FromResult(newCx); + } + + var oldContext = this.MockContext(""); + oldContext["legacy"] = "something"; + + // Act + var function = SKFunction.FromNativeMethod(Method(Test), log: this._log.Object); + Assert.NotNull(function); + SKContext newContext = await function.InvokeAsync(oldContext); + + // Assert + Assert.False(oldContext.ErrorOccurred); + Assert.False(newContext.ErrorOccurred); + this.VerifyFunctionTypeMatch(14); + + Assert.Equal(s_expected, s_canary); + + Assert.True(oldContext.Variables.ContainsKey("canary")); + Assert.False(oldContext.Variables.ContainsKey("canary2")); + + Assert.False(newContext.Variables.ContainsKey("canary")); + Assert.True(newContext.Variables.ContainsKey("canary2")); + + Assert.Equal(s_expected, oldContext["canary"]); + Assert.Equal("222", newContext["canary2"]); + + Assert.True(oldContext.Variables.ContainsKey("legacy")); + Assert.False(newContext.Variables.ContainsKey("legacy")); + + Assert.Equal("x y z", oldContext.Result); + Assert.Equal("new data", newContext.Result); + } + + [Fact] + public async Task ItSupportsType15Async() + { + // Arrange + [SKFunction("Test")] + [SKFunctionName("Test")] + static Task Test(string input) + { + s_canary = s_expected; + return Task.CompletedTask; + } + + var context = this.MockContext(""); + + // Act + var function = SKFunction.FromNativeMethod(Method(Test), log: this._log.Object); + Assert.NotNull(function); + SKContext result = await function.InvokeAsync(context); + + // Assert + Assert.False(result.ErrorOccurred); + this.VerifyFunctionTypeMatch(15); + Assert.Equal(s_expected, s_canary); + } + + [Fact] + public async Task ItSupportsType16Async() + { + // Arrange + [SKFunction("Test")] + [SKFunctionName("Test")] + static Task Test(SKContext cx) + { + s_canary = s_expected; + cx["canary"] = s_expected; + cx.Variables.Update("x y z"); + return Task.CompletedTask; + } + + var context = this.MockContext(""); + + // Act + var function = SKFunction.FromNativeMethod(Method(Test), log: this._log.Object); + Assert.NotNull(function); + SKContext result = await function.InvokeAsync(context); + + // Assert + Assert.False(result.ErrorOccurred); + this.VerifyFunctionTypeMatch(16); + Assert.Equal(s_expected, s_canary); + Assert.Equal(s_expected, context["canary"]); + Assert.Equal("x y z", context.Result); + } + + [Fact] + public async Task ItSupportsType17Async() + { + // Arrange + [SKFunction("Test")] + [SKFunctionName("Test")] + static Task Test(string input, SKContext cx) + { + s_canary = s_expected; + cx["canary"] = s_expected; + cx.Variables.Update(input + "x y z"); + return Task.CompletedTask; + } + + var context = this.MockContext("input:"); + + // Act + var function = SKFunction.FromNativeMethod(Method(Test), log: this._log.Object); + Assert.NotNull(function); + SKContext result = await function.InvokeAsync(context); + + // Assert + Assert.False(result.ErrorOccurred); + this.VerifyFunctionTypeMatch(17); + Assert.Equal(s_expected, s_canary); + Assert.Equal(s_expected, context["canary"]); + Assert.Equal("input:x y z", context.Result); + } + + [Fact] + public async Task ItSupportsType18Async() + { + // Arrange + [SKFunction("Test")] + [SKFunctionName("Test")] + static Task Test() + { + s_canary = s_expected; + return Task.CompletedTask; + } + + var context = this.MockContext(""); + + // Act + var function = SKFunction.FromNativeMethod(Method(Test), log: this._log.Object); + Assert.NotNull(function); + SKContext result = await function.InvokeAsync(context); + + // Assert + Assert.False(result.ErrorOccurred); + this.VerifyFunctionTypeMatch(18); + Assert.Equal(s_expected, s_canary); + } + + public void Dispose() + { + this._testOutputHelper.Dispose(); + } + + private static MethodInfo Method(Delegate method) + { + return method.Method; + } + + private SKContext MockContext(string input) + { + return new SKContext( + new ContextVariables(input), + NullMemory.Instance, + this._skills.Object, + this._log.Object); + } + + private void VerifyFunctionTypeMatch(int typeNumber) + { +#pragma warning disable CS8620 + // Verify that the expected function has been called + this._log.Verify(logger => logger.Log( + It.Is(logLevel => logLevel == LogLevel.Trace), + It.Is(eventId => eventId.Id == typeNumber), + It.Is((@object, @type) => true), + It.IsAny(), + It.IsAny>()), + Times.Once); + + // Verify that unexpected functions have not been called (e.g. missing a return statement) + // TODO: check for "Executing function type" instead of using "eventId.Id != 0" + // Note: eventId.Id != 0 is used to avoid catching other Log.Trace events and failing the tests + this._log.Verify(logger => logger.Log( + It.Is(logLevel => logLevel == LogLevel.Trace), + It.Is(eventId => eventId.Id != 0 && eventId.Id != typeNumber), + It.Is((@object, @type) => true), + It.IsAny(), + It.IsAny>()), + Times.Never); +#pragma warning restore CS8620 + } +} diff --git a/dotnet/src/SemanticKernel.Test/Reliability/PassThroughWithoutRetryTests.cs b/dotnet/src/SemanticKernel.Test/Reliability/PassThroughWithoutRetryTests.cs new file mode 100644 index 000000000000..2fdc8607f37f --- /dev/null +++ b/dotnet/src/SemanticKernel.Test/Reliability/PassThroughWithoutRetryTests.cs @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel.AI; +using Microsoft.SemanticKernel.Reliability; +using Moq; +using Xunit; + +namespace SemanticKernelTests.Reliability; + +public class PassThroughWithoutRetryTests +{ + [Fact] + public async Task ItDoesNotRetryOnExceptionAsync() + { + // Arrange + var retry = new PassThroughWithoutRetry(); + var action = new Mock>(); + action.Setup(a => a()).Throws(new AIException(AIException.ErrorCodes.Throttling, "Throttling Test")); + + // Act + await Assert.ThrowsAsync(() => retry.ExecuteWithRetryAsync(action.Object, Mock.Of())); + + // Assert + action.Verify(a => a(), Times.Once); + } + + [Fact] + public async Task NoExceptionNoRetryAsync() + { + // Arrange + var log = new Mock(); + var retry = new PassThroughWithoutRetry(); + var action = new Mock>(); + + // Act + await retry.ExecuteWithRetryAsync(action.Object, log.Object); + + // Assert + action.Verify(a => a(), Times.Once); + } +} diff --git a/dotnet/src/SemanticKernel.Test/SemanticFunctions/Partitioning/SemanticTextPartitionerTests.cs b/dotnet/src/SemanticKernel.Test/SemanticFunctions/Partitioning/SemanticTextPartitionerTests.cs new file mode 100644 index 000000000000..eb5a9d95d8b2 --- /dev/null +++ b/dotnet/src/SemanticKernel.Test/SemanticFunctions/Partitioning/SemanticTextPartitionerTests.cs @@ -0,0 +1,426 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using Microsoft.SemanticKernel.SemanticFunctions.Partitioning; +using SemanticKernelTests.XunitHelpers; +using Xunit; +using Xunit.Abstractions; + +namespace SemanticKernelTests.SemanticFunctions.Partitioning; + +public sealed class SemanticTextPartitionerTests : IDisposable +{ + private readonly RedirectOutput _testOutputHelper; + + public SemanticTextPartitionerTests(ITestOutputHelper testOutputHelper) + { + this._testOutputHelper = new RedirectOutput(testOutputHelper); + Console.SetOut(this._testOutputHelper); + } + + [Fact] + public void CanSplitPlainTextLines() + { + const string input = "This is a test of the emergency broadcast system. This is only a test."; + var expected = new[] + { + "This is a test of the emergency broadcast system.", + "This is only a test." + }; + + var result = SemanticTextPartitioner.SplitPlainTextLines(input, 15); + + Assert.Equal(expected, result); + } + + [Fact] + public void CanSplitMarkdownParagraphs() + { + List input = new() + { + "This is a test of the emergency broadcast system. This is only a test.", + "We repeat, this is only a test. A unit test." + }; + var expected = new[] + { + "This is a test of the emergency broadcast system.", + "This is only a test.", + "We repeat, this is only a test. A unit test." + }; + + var result = SemanticTextPartitioner.SplitMarkdownParagraphs(input, 13); + + Assert.Equal(expected, result); + } + + [Fact] + public void CanSplitTextParagraphs() + { + List input = new() + { + "This is a test of the emergency broadcast system. This is only a test.", + "We repeat, this is only a test. A unit test." + }; + + var expected = new[] + { + "This is a test of the emergency broadcast system.", + "This is only a test.", + "We repeat, this is only a test. A unit test." + }; + + var result = SemanticTextPartitioner.SplitPlainTextParagraphs(input, 13); + + Assert.Equal(expected, result); + } + + [Fact] + public void CanSplitMarkDownLines() + { + const string input = "This is a test of the emergency broadcast system. This is only a test."; + var expected = new[] + { + "This is a test of the emergency broadcast system.", + "This is only a test." + }; + + var result = SemanticTextPartitioner.SplitMarkDownLines(input, 15); + + Assert.Equal(expected, result); + } + + [Fact] + public void CanSplitTextParagraphsWithEmptyInput() + { + List input = new(); + + var expected = new List(); + + var result = SemanticTextPartitioner.SplitPlainTextParagraphs(input, 13); + + Assert.Equal(expected, result); + } + + [Fact] + public void CanSplitMarkdownParagraphsWithEmptyInput() + { + List input = new(); + + var expected = new List(); + + var result = SemanticTextPartitioner.SplitMarkdownParagraphs(input, 13); + + Assert.Equal(expected, result); + } + + [Fact] + public void CanSplitTextParagraphsEvenly() + { + List input = new() + { + "This is a test of the emergency broadcast system. This is only a test.", + "We repeat, this is only a test. A unit test.", + "A small note. And another. And once again. Seriously, this is the end. We're finished. All set. Bye.", + "Done." + }; + + var expected = new[] + { + "This is a test of the emergency broadcast system.", + "This is only a test.", + "We repeat, this is only a test. A unit test.", + "A small note. And another. And once again.", + "Seriously, this is the end. We're finished. All set. Bye. Done." + }; + + var result = SemanticTextPartitioner.SplitPlainTextParagraphs(input, 15); + + Assert.Equal(expected, result); + } + + // a plaintext example that splits on \r or \n + [Fact] + public void CanSplitTextParagraphsOnNewlines() + { + List input = new() + { + "This is a test of the emergency broadcast system\r\nThis is only a test", + "We repeat this is only a test\nA unit test", + "A small note\nAnd another\r\nAnd once again\rSeriously this is the end\nWe're finished\nAll set\nBye\n", + "Done" + }; + + var expected = new[] + { + "This is a test of the emergency broadcast system", + "This is only a test", + "We repeat this is only a test\nA unit test", + "A small note\nAnd another\nAnd once again", + "Seriously this is the end\nWe're finished\nAll set\nBye Done", + }; + + var result = SemanticTextPartitioner.SplitPlainTextParagraphs(input, 15); + + Assert.Equal(expected, result); + } + + // a plaintext example that splits on ? or ! + [Fact] + public void CanSplitTextParagraphsOnPunctuation() + { + List input = new() + { + "This is a test of the emergency broadcast system. This is only a test", + "We repeat, this is only a test? A unit test", + "A small note! And another? And once again! Seriously, this is the end. We're finished. All set. Bye.", + "Done." + }; + + var expected = new[] + { + "This is a test of the emergency broadcast system.", + "This is only a test", + "We repeat, this is only a test? A unit test", + "A small note! And another? And once again!", + "Seriously, this is the end.", + $"We're finished. All set. Bye.{Environment.NewLine}Done.", + }; + + var result = SemanticTextPartitioner.SplitPlainTextParagraphs(input, 15); + + Assert.Equal(expected, result); + } + + // a plaintext example that splits on ; + [Fact] + public void CanSplitTextParagraphsOnSemicolons() + { + List input = new() + { + "This is a test of the emergency broadcast system; This is only a test", + "We repeat; this is only a test; A unit test", + "A small note; And another; And once again; Seriously, this is the end; We're finished; All set; Bye.", + "Done." + }; + + var expected = new[] + { + "This is a test of the emergency broadcast system;", + "This is only a test", + "We repeat; this is only a test; A unit test", + "A small note; And another; And once again;", + "Seriously, this is the end; We're finished; All set; Bye. Done.", + }; + + var result = SemanticTextPartitioner.SplitPlainTextParagraphs(input, 15); + + Assert.Equal(expected, result); + } + + // a plaintext example that splits on : + [Fact] + public void CanSplitTextParagraphsOnColons() + { + List input = new() + { + "This is a test of the emergency broadcast system: This is only a test", + "We repeat: this is only a test: A unit test", + "A small note: And another: And once again: Seriously, this is the end: We're finished: All set: Bye.", + "Done." + }; + + var expected = new[] + { + "This is a test of the emergency broadcast system:", + "This is only a test", + "We repeat: this is only a test: A unit test", + "A small note: And another: And once again:", + "Seriously, this is the end: We're finished: All set: Bye. Done.", + }; + + var result = SemanticTextPartitioner.SplitPlainTextParagraphs(input, 15); + + Assert.Equal(expected, result); + } + + // a plaintext example that splits on , + [Fact] + public void CanSplitTextParagraphsOnCommas() + { + List input = new() + { + "This is a test of the emergency broadcast system, This is only a test", + "We repeat, this is only a test, A unit test", + "A small note, And another, And once again, Seriously, this is the end, We're finished, All set, Bye.", + "Done." + }; + + var expected = new[] + { + "This is a test of the emergency broadcast system,", + "This is only a test", + "We repeat, this is only a test, A unit test", + "A small note, And another, And once again, Seriously,", + $"this is the end, We're finished, All set, Bye.{Environment.NewLine}Done.", + }; + + var result = SemanticTextPartitioner.SplitPlainTextParagraphs(input, 15); + + Assert.Equal(expected, result); + } + + // a plaintext example that splits on ) or ] or } + [Fact] + public void CanSplitTextParagraphsOnClosingBrackets() + { + List input = new() + { + "This is a test of the emergency broadcast system) This is only a test", + "We repeat) this is only a test) A unit test", + "A small note] And another) And once again] Seriously this is the end} We're finished} All set} Bye.", + "Done." + }; + + var expected = new[] + { + "This is a test of the emergency broadcast system)", + "This is only a test", + "We repeat) this is only a test) A unit test", + "A small note] And another) And once again]", + "Seriously this is the end} We're finished} All set} Bye. Done.", + }; + + var result = SemanticTextPartitioner.SplitPlainTextParagraphs(input, 15); + + Assert.Equal(expected, result); + } + + // a plaintext example that splits on ' ' + [Fact] + public void CanSplitTextParagraphsOnSpaces() + { + List input = new() + { + "This is a test of the emergency broadcast system This is only a test", + "We repeat this is only a test A unit test", + "A small note And another And once again Seriously this is the end We're finished All set Bye.", + "Done." + }; + + var expected = new[] + { + "This is a test of the emergency", + "broadcast system This is only a test", + "We repeat this is only a test A unit test", + "A small note And another And once again Seriously", + $"this is the end We're finished All set Bye.{Environment.NewLine}Done.", + }; + + var result = SemanticTextPartitioner.SplitPlainTextParagraphs(input, 15); + + Assert.Equal(expected, result); + } + + // a plaintext example that splits on '-' + [Fact] + public void CanSplitTextParagraphsOnHyphens() + { + List input = new() + { + "This is a test of the emergency broadcast system-This is only a test", + "We repeat-this is only a test-A unit test", + "A small note-And another-And once again-Seriously, this is the end-We're finished-All set-Bye.", + "Done." + }; + + var expected = new[] + { + "This is a test of the emergency", + "broadcast system-This is only a test", + "We repeat-this is only a test-A unit test", + "A small note-And another-And once again-Seriously,", + $"this is the end-We're finished-All set-Bye.{Environment.NewLine}Done.", + }; + + var result = SemanticTextPartitioner.SplitPlainTextParagraphs(input, 15); + + Assert.Equal(expected, result); + } + + // a plaintext example that does not have any of the above characters + [Fact] + public void CanSplitTextParagraphsWithNoDelimiters() + { + List input = new() + { + "Thisisatestoftheemergencybroadcastsystem", + "Thisisonlyatest", + "WerepeatthisisonlyatestAunittest", + "AsmallnoteAndanotherAndonceagain", + "SeriouslythisistheendWe'refinishedAllsetByeDoneThisOneWillBeSplitToMeetTheLimit", + }; + + var expected = new[] + { + $"Thisisatestoftheemergencybroadcastsystem{Environment.NewLine}Thisisonlyatest", + "WerepeatthisisonlyatestAunittest", + "AsmallnoteAndanotherAndonceagain", + "SeriouslythisistheendWe'refinishedAllse", + "tByeDoneThisOneWillBeSplitToMeetTheLimit", + }; + + var result = SemanticTextPartitioner.SplitPlainTextParagraphs(input, 15); + + Assert.Equal(expected, result); + } + + // a markdown example that splits on . + + // a markdown example that splits on ? or ! + + // a markdown example that splits on ; + + // a markdown example that splits on : + + // a markdown example that splits on , + + // a markdown example that splits on ) or ] or } + + // a markdown example that splits on ' ' + + // a markdown example that splits on '-' + + // a markdown example that splits on '\r' or '\n' + [Fact] + public void CanSplitMarkdownParagraphsOnNewlines() + { + List input = new() + { + "This_is_a_test_of_the_emergency_broadcast_system\r\nThis_is_only_a_test", + "We_repeat_this_is_only_a_test\nA_unit_test", + "A_small_note\nAnd_another\r\nAnd_once_again\rSeriously_this_is_the_end\nWe're_finished\nAll_set\nBye\n", + "Done" + }; + + var expected = new[] + { + "This_is_a_test_of_the_emergency_broadcast_system", + "This_is_only_a_test", + "We_repeat_this_is_only_a_test\nA_unit_test", + "A_small_note\nAnd_another\nAnd_once_again", + "Seriously_this_is_the_end\nWe're_finished\nAll_set\nBye Done", + }; + + var result = SemanticTextPartitioner.SplitMarkdownParagraphs(input, 15); + + Assert.Equal(expected, result); + } + + // a markdown example that does not have any of the above characters + + public void Dispose() + { + this._testOutputHelper.Dispose(); + } +} diff --git a/dotnet/src/SemanticKernel.Test/SemanticKernel.Test.csproj b/dotnet/src/SemanticKernel.Test/SemanticKernel.Test.csproj new file mode 100644 index 000000000000..df186b02808c --- /dev/null +++ b/dotnet/src/SemanticKernel.Test/SemanticKernel.Test.csproj @@ -0,0 +1,29 @@ + + + + SemanticKernelTests + SemanticKernelTests + net6.0 + false + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + diff --git a/dotnet/src/SemanticKernel.Test/SkillDefinition/FunctionsViewTests.cs b/dotnet/src/SemanticKernel.Test/SkillDefinition/FunctionsViewTests.cs new file mode 100644 index 000000000000..c15ab2ec6669 --- /dev/null +++ b/dotnet/src/SemanticKernel.Test/SkillDefinition/FunctionsViewTests.cs @@ -0,0 +1,115 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Linq; +using System.Runtime; +using Microsoft.SemanticKernel.SkillDefinition; +using Xunit; + +namespace SemanticKernelTests.SkillDefinition; + +public class FunctionsViewTests +{ + [Fact] + public void ItIsEmptyByDefault() + { + // Act + var target = new FunctionsView(); + + // Assert + Assert.Empty(target.SemanticFunctions); + Assert.Empty(target.NativeFunctions); + } + + [Fact] + public void ItProvidesCorrectNativeFunctionInfo() + { + // Arrange + var target = new FunctionsView() + .AddFunction(new FunctionView("f1", "s1", "", new List(), false)) + .AddFunction(new FunctionView("f2", "s1", "", new List(), false)) + .AddFunction(new FunctionView("f1", "s2", "", new List(), false)); + + // Assert + Assert.Equal(2, target.NativeFunctions.Count); + Assert.Equal(2, target.NativeFunctions["s1"].Count); + Assert.Single(target.NativeFunctions["s2"]); + Assert.True(target.IsNative("s1", "f1")); + Assert.True(target.IsNative("s1", "f2")); + Assert.True(target.IsNative("s2", "f1")); + Assert.False(target.IsNative("s5", "f5")); + Assert.False(target.IsSemantic("s1", "f1")); + } + + [Fact] + public void ItProvidesCorrectSemanticFunctionInfo() + { + // Arrange + var target = new FunctionsView() + .AddFunction(new FunctionView("f1", "s1", "", new List(), true)) + .AddFunction(new FunctionView("f2", "s1", "", new List(), true)) + .AddFunction(new FunctionView("f1", "s2", "", new List(), true)); + + // Assert + Assert.Equal(2, target.SemanticFunctions.Count); + Assert.Equal(2, target.SemanticFunctions["s1"].Count); + Assert.Single(target.SemanticFunctions["s2"]); + Assert.True(target.IsSemantic("s1", "f1")); + Assert.True(target.IsSemantic("s1", "f2")); + Assert.True(target.IsSemantic("s2", "f1")); + Assert.False(target.IsSemantic("s5", "f5")); + Assert.False(target.IsNative("s1", "f1")); + } + + [Fact] + public void ItThrowsOnConflict() + { + // Arrange + var target = new FunctionsView() + .AddFunction(new FunctionView("f1", "s1", "", new List(), true)) + .AddFunction(new FunctionView("f1", "s1", "", new List(), false)); + + // Assert + Assert.Throws(() => target.IsSemantic("s1", "f1")); + Assert.Throws(() => target.IsNative("s1", "f1")); + } + + [Fact] + public void ItReturnsFunctionParams() + { + // Arrange + var params1 = new List + { + new("p1", "param 1", "default 1"), + new("p2", "param 2", "default 2") + }; + var params2 = new List + { + new("p3", "param 3", "default 3"), + new("p4", "param 4", "default 4") + }; + var target = new FunctionsView() + .AddFunction(new FunctionView("semFun", "s1", "", params1, true)) + .AddFunction(new FunctionView("natFun", "s1", "", params2, false)); + + // Act + List semFun = target.SemanticFunctions["s1"]; + List natFun = target.NativeFunctions["s1"]; + + // Assert + Assert.Single(semFun); + Assert.Single(natFun); + Assert.Equal("p1", semFun.First().Parameters[0].Name); + Assert.Equal("p2", semFun.First().Parameters[1].Name); + Assert.Equal("p3", natFun.First().Parameters[0].Name); + Assert.Equal("p4", natFun.First().Parameters[1].Name); + Assert.Equal("param 1", semFun.First().Parameters[0].Description); + Assert.Equal("param 2", semFun.First().Parameters[1].Description); + Assert.Equal("param 3", natFun.First().Parameters[0].Description); + Assert.Equal("param 4", natFun.First().Parameters[1].Description); + Assert.Equal("default 1", semFun.First().Parameters[0].DefaultValue); + Assert.Equal("default 2", semFun.First().Parameters[1].DefaultValue); + Assert.Equal("default 3", natFun.First().Parameters[0].DefaultValue); + Assert.Equal("default 4", natFun.First().Parameters[1].DefaultValue); + } +} diff --git a/dotnet/src/SemanticKernel.Test/TemplateEngine/PromptTemplateEngineTests.cs b/dotnet/src/SemanticKernel.Test/TemplateEngine/PromptTemplateEngineTests.cs new file mode 100644 index 000000000000..6db0285a433d --- /dev/null +++ b/dotnet/src/SemanticKernel.Test/TemplateEngine/PromptTemplateEngineTests.cs @@ -0,0 +1,337 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Reflection; +using System.Threading.Tasks; +using Microsoft.SemanticKernel.Memory; +using Microsoft.SemanticKernel.Orchestration; +using Microsoft.SemanticKernel.SkillDefinition; +using Microsoft.SemanticKernel.TemplateEngine; +using Microsoft.SemanticKernel.TemplateEngine.Blocks; +using Moq; +using SemanticKernelTests.XunitHelpers; +using Xunit; +using Xunit.Abstractions; + +namespace SemanticKernelTests.TemplateEngine; + +public sealed class TemplateEngineTests : IDisposable +{ + private readonly IPromptTemplateEngine _target; + private readonly ContextVariables _variables; + private readonly Mock _skills; + private readonly RedirectOutput _testOutputHelper; + + public TemplateEngineTests(ITestOutputHelper testOutputHelper) + { + this._testOutputHelper = new RedirectOutput(testOutputHelper); + Console.SetOut(this._testOutputHelper); + + this._target = new PromptTemplateEngine(ConsoleLogger.Log); + this._variables = new ContextVariables(Guid.NewGuid().ToString("X")); + this._skills = new Mock(); + } + + [Fact] + public void ItTokenizesEdgeCases1() + { + // Arrange + var template = "}}{{{ {$a}}}} {{b}}x}}"; + + // Act + var blocks = this._target.ExtractBlocks(template, false); + + // Assert + Assert.Equal(5, blocks.Count); + + Assert.Equal("}}{", blocks[0].Content); + Assert.Equal(BlockTypes.Text, blocks[0].Type); + + Assert.Equal("{$a", blocks[1].Content); + Assert.Equal(BlockTypes.Code, blocks[1].Type); + + Assert.Equal("}} ", blocks[2].Content); + Assert.Equal(BlockTypes.Text, blocks[2].Type); + + Assert.Equal("b", blocks[3].Content); + Assert.Equal(BlockTypes.Code, blocks[3].Type); + + Assert.Equal("x}}", blocks[4].Content); + Assert.Equal(BlockTypes.Text, blocks[4].Type); + } + + [Fact] + public void ItTokenizesEdgeCases2() + { + // Arrange + var template = "}}{{{{$a}}}} {{b}}$x}}"; + + // Act + var blocks = this._target.ExtractBlocks(template); + + // Assert + Assert.Equal(5, blocks.Count); + + Assert.Equal("}}{{", blocks[0].Content); + Assert.Equal(BlockTypes.Text, blocks[0].Type); + + Assert.Equal("$a", blocks[1].Content); + Assert.Equal(BlockTypes.Variable, blocks[1].Type); + + Assert.Equal("}} ", blocks[2].Content); + Assert.Equal(BlockTypes.Text, blocks[2].Type); + + Assert.Equal("b", blocks[3].Content); + Assert.Equal(BlockTypes.Code, blocks[3].Type); + + Assert.Equal("$x}}", blocks[4].Content); + Assert.Equal(BlockTypes.Text, blocks[4].Type); + } + + [Fact] + public void ItTokenizesAClassicPrompt() + { + // Arrange + var template = "this is a {{ $prompt }} with {{$some}} variables " + + "and {{function $calls}} that {{ also $use $variables }}"; + + // Act + var blocks = this._target.ExtractBlocks(template, true); + + // Assert + Assert.Equal(8, blocks.Count); + + Assert.Equal("this is a ", blocks[0].Content); + Assert.Equal(BlockTypes.Text, blocks[0].Type); + + Assert.Equal("$prompt", blocks[1].Content); + Assert.Equal(BlockTypes.Variable, blocks[1].Type); + + Assert.Equal(" with ", blocks[2].Content); + Assert.Equal(BlockTypes.Text, blocks[2].Type); + + Assert.Equal("$some", blocks[3].Content); + Assert.Equal(BlockTypes.Variable, blocks[3].Type); + + Assert.Equal(" variables and ", blocks[4].Content); + Assert.Equal(BlockTypes.Text, blocks[4].Type); + + Assert.Equal("function $calls", blocks[5].Content); + Assert.Equal(BlockTypes.Code, blocks[5].Type); + + Assert.Equal(" that ", blocks[6].Content); + Assert.Equal(BlockTypes.Text, blocks[6].Type); + + Assert.Equal("also $use $variables", blocks[7].Content); + Assert.Equal(BlockTypes.Code, blocks[7].Type); + } + + [Theory] + [InlineData(null, 1)] + [InlineData("", 1)] + [InlineData("}}{{a}} {{b}}x", 5)] + [InlineData("}}{{ -a}} {{b}}x", 5)] + [InlineData("}}{{ -a\n}} {{b}}x", 5)] + [InlineData("}}{{ -a\n} } {{b}}x", 3)] + public void ItTokenizesTheRightTokenCount(string? template, int blockCount) + { + // Act + var blocks = this._target.ExtractBlocks(template, false); + + // Assert + Assert.Equal(blockCount, blocks.Count); + } + + [Fact] + public void ItRendersVariables() + { + // Arrange + var template = "{$x11} This {$a} is {$_a} a {{$x11}} test {{$x11}} " + + "template {{foo}}{{bar $a}}{{baz $_a}}{{yay $x11}}"; + + // Act + var blocks = this._target.ExtractBlocks(template); + var updatedBlocks = this._target.RenderVariables(blocks, this._variables); + + // Assert + Assert.Equal(9, blocks.Count); + Assert.Equal(9, updatedBlocks.Count); + + Assert.Equal("$x11", blocks[1].Content); + Assert.Equal("", updatedBlocks[1].Content); + Assert.Equal(BlockTypes.Variable, blocks[1].Type); + Assert.Equal(BlockTypes.Text, updatedBlocks[1].Type); + + Assert.Equal("$x11", blocks[3].Content); + Assert.Equal("", updatedBlocks[3].Content); + Assert.Equal(BlockTypes.Variable, blocks[3].Type); + Assert.Equal(BlockTypes.Text, updatedBlocks[3].Type); + + Assert.Equal("foo", blocks[5].Content); + Assert.Equal("foo", updatedBlocks[5].Content); + Assert.Equal(BlockTypes.Code, blocks[5].Type); + Assert.Equal(BlockTypes.Code, updatedBlocks[5].Type); + + Assert.Equal("bar $a", blocks[6].Content); + Assert.Equal("bar $a", updatedBlocks[6].Content); + Assert.Equal(BlockTypes.Code, blocks[6].Type); + Assert.Equal(BlockTypes.Code, updatedBlocks[6].Type); + + Assert.Equal("baz $_a", blocks[7].Content); + Assert.Equal("baz $_a", updatedBlocks[7].Content); + Assert.Equal(BlockTypes.Code, blocks[7].Type); + Assert.Equal(BlockTypes.Code, updatedBlocks[7].Type); + + Assert.Equal("yay $x11", blocks[8].Content); + Assert.Equal("yay $x11", updatedBlocks[8].Content); + Assert.Equal(BlockTypes.Code, blocks[8].Type); + Assert.Equal(BlockTypes.Code, updatedBlocks[8].Type); + + // Arrange + this._variables.Set("x11", "x11 value"); + this._variables.Set("a", "a value"); + this._variables.Set("_a", "_a value"); + + // Act + blocks = this._target.ExtractBlocks(template); + updatedBlocks = this._target.RenderVariables(blocks, this._variables); + + // Assert + Assert.Equal(9, blocks.Count); + Assert.Equal(9, updatedBlocks.Count); + + Assert.Equal("$x11", blocks[1].Content); + Assert.Equal("x11 value", updatedBlocks[1].Content); + Assert.Equal(BlockTypes.Variable, blocks[1].Type); + Assert.Equal(BlockTypes.Text, updatedBlocks[1].Type); + + Assert.Equal("$x11", blocks[3].Content); + Assert.Equal("x11 value", updatedBlocks[3].Content); + Assert.Equal(BlockTypes.Variable, blocks[3].Type); + Assert.Equal(BlockTypes.Text, updatedBlocks[3].Type); + + Assert.Equal("foo", blocks[5].Content); + Assert.Equal("foo", updatedBlocks[5].Content); + Assert.Equal(BlockTypes.Code, blocks[5].Type); + Assert.Equal(BlockTypes.Code, updatedBlocks[5].Type); + + Assert.Equal("bar $a", blocks[6].Content); + Assert.Equal("bar $a", updatedBlocks[6].Content); + Assert.Equal(BlockTypes.Code, blocks[6].Type); + Assert.Equal(BlockTypes.Code, updatedBlocks[6].Type); + + Assert.Equal("baz $_a", blocks[7].Content); + Assert.Equal("baz $_a", updatedBlocks[7].Content); + Assert.Equal(BlockTypes.Code, blocks[7].Type); + Assert.Equal(BlockTypes.Code, updatedBlocks[7].Type); + + Assert.Equal("yay $x11", blocks[8].Content); + Assert.Equal("yay $x11", updatedBlocks[8].Content); + Assert.Equal(BlockTypes.Code, blocks[8].Type); + Assert.Equal(BlockTypes.Code, updatedBlocks[8].Type); + } + + [Fact] + public async Task ItRendersCodeUsingInputAsync() + { + // Arrange + [SKFunction("test")] + [SKFunctionName("test")] + static string MyFunctionAsync(SKContext cx) + { + Console.WriteLine($"MyFunction call received, input: {cx.Variables.Input}"); + return $"F({cx.Variables.Input})"; + } + + var func = SKFunction.FromNativeMethod(Method(MyFunctionAsync)); + Assert.NotNull(func); + + this._variables.Update("INPUT-BAR"); + var template = "foo-{{function}}-baz"; + this._skills.Setup(x => x.HasNativeFunction("function")).Returns(true); + this._skills.Setup(x => x.GetNativeFunction("function")).Returns(func); + var context = this.MockContext(); + + // Act + var result = await this._target.RenderAsync(template, context); + + // Assert + Assert.Equal("foo-F(INPUT-BAR)-baz", result); + } + + [Fact] + public async Task ItRendersCodeUsingVariablesAsync() + { + // Arrange + [SKFunction("test")] + [SKFunctionName("test")] + static string MyFunctionAsync(SKContext cx) + { + Console.WriteLine($"MyFunction call received, input: {cx.Variables.Input}"); + return $"F({cx.Variables.Input})"; + } + + var func = SKFunction.FromNativeMethod(Method(MyFunctionAsync)); + Assert.NotNull(func); + + this._variables.Set("myVar", "BAR"); + var template = "foo-{{function $myVar}}-baz"; + this._skills.Setup(x => x.HasNativeFunction("function")).Returns(true); + this._skills.Setup(x => x.GetNativeFunction("function")).Returns(func); + var context = this.MockContext(); + + // Act + var result = await this._target.RenderAsync(template, context); + + // Assert + Assert.Equal("foo-F(BAR)-baz", result); + } + + [Fact] + public async Task ItRendersAsyncCodeUsingVariablesAsync() + { + // Arrange + [SKFunction("test")] + [SKFunctionName("test")] + static Task MyFunctionAsync(SKContext cx) + { + // Input value should be "BAR" because the variable $myVar is passed in + Console.WriteLine($"MyFunction call received, input: {cx.Variables.Input}"); + return Task.FromResult(cx.Variables.Input); + } + + var func = SKFunction.FromNativeMethod(Method(MyFunctionAsync)); + Assert.NotNull(func); + + this._variables.Set("myVar", "BAR"); + var template = "foo-{{function $myVar}}-baz"; + this._skills.Setup(x => x.HasNativeFunction("function")).Returns(true); + this._skills.Setup(x => x.GetNativeFunction("function")).Returns(func); + var context = this.MockContext(); + + // Act + var result = await this._target.RenderAsync(template, context); + + // Assert + Assert.Equal("foo-BAR-baz", result); + } + + private static MethodInfo Method(Delegate method) + { + return method.Method; + } + + private SKContext MockContext() + { + return new SKContext( + this._variables, + NullMemory.Instance, + this._skills.Object, + ConsoleLogger.Log); + } + + public void Dispose() + { + this._testOutputHelper.Dispose(); + } +} diff --git a/dotnet/src/SemanticKernel.Test/VectorOperations/VectorOperationTests.cs b/dotnet/src/SemanticKernel.Test/VectorOperations/VectorOperationTests.cs new file mode 100644 index 000000000000..269d4ad32afa --- /dev/null +++ b/dotnet/src/SemanticKernel.Test/VectorOperations/VectorOperationTests.cs @@ -0,0 +1,265 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.SemanticKernel.AI.Embeddings; +using Microsoft.SemanticKernel.AI.Embeddings.VectorOperations; +using Xunit; + +namespace SemanticKernelTests.VectorOperations; + +public class VectorOperationTests +{ + private readonly float[] _floatV1 = new float[] { 1.0F, 2.0F, -4.0F, 10.0F }; + private readonly float[] _floatV2 = new float[] { 3.0F, -7.0F, 1.0F, 6.0F }; + + private readonly double[] _doubleV1 = new double[] { 1.0, 2.0, -4.0, 10.0 }; + private readonly double[] _doubleV2 = new double[] { 3.0, -7.0, 1.0, 6.0 }; + + [Fact] + public void ItOnlySupportsFPDataTypes() + { + // Arrange + var target = SupportedTypes.Types; + + // Assert + Assert.Equal(2, target.Count()); + Assert.Contains(typeof(float), target); + Assert.Contains(typeof(double), target); + } + + [Fact] + public void ItComputesCosineSimilarityFloat() + { + // Arrange + var target = this._floatV1.CosineSimilarity(this._floatV2); + + // Assert + Assert.Equal(0.41971841676, target, 5); + } + + [Fact] + public void ItComputesCosineSimilarityDouble() + { + // Arrange + var target = this._doubleV1.CosineSimilarity(this._doubleV2); + + // Assert + Assert.Equal(0.41971841676, target, 5); + } + + [Fact] + public void ItThrowsOnCosineSimilarityWithDifferentLengthVectorsFP() + { + // Arrange + var shortVector = new float[] { -1.0F, 4.0F }; + + // Assert + try + { + shortVector.CosineSimilarity(this._floatV2); + } + catch (ArgumentException target) + { + Assert.IsType(target); + } + } + + [Fact] + public void ItThrowsOnCosineSimilarityWithDifferentLengthVectorsDouble() + { + // Arrange + var shortVector = new double[] { -1.0, 4.0 }; + + // Assert + try + { + shortVector.CosineSimilarity(this._doubleV2); + } + catch (ArgumentException target) + { + Assert.IsType(target); + } + } + + [Fact] + public void ItComputesEuclideanLengthFloat() + { + // Arrange + var target = this._floatV1.EuclideanLength(); + + // Assert + Assert.Equal(11.0, target, 5); + } + + [Fact] + public void ItComputesEuclideanLengthDouble() + { + // Arrange + var target = this._doubleV1.EuclideanLength(); + + // Assert + Assert.Equal(11.0, target, 5); + } + + [Fact] + public void ItComputesDotProductFloat() + { + // Arrange + var target = this._floatV1.DotProduct(this._floatV2); + + // Assert + Assert.Equal(45.0, target, 5); + } + + [Fact] + public void ItComputesDotProductDouble() + { + // Arrange + var target = this._doubleV1.DotProduct(this._doubleV2); + + // Assert + Assert.Equal(45.0, target, 5); + } + + [Fact] + public void ItNormalizesInPlaceFloat() + { + // Arrange + var target = this._floatV1; + target.NormalizeInPlace(); + var expected = new float[] { 0.09090909F, 0.18181819F, -0.3636364F, 0.90909094F }; + + // Assert + Assert.Equal(expected.Length, target.Length); + for (int i = 0; i < expected.Length; i++) + { + Assert.Equal(expected[i], target[i], .00001F); + } + } + + [Fact] + public void ItNormalizesInPlaceDouble() + { + // Arrange + var target = this._doubleV1; + target.NormalizeInPlace(); + var expected = new double[] { 0.09090909, 0.18181819, -0.3636364, 0.90909094 }; + + // Assert + Assert.Equal(expected.Length, target.Length); + for (int i = 0; i < expected.Length; i++) + { + Assert.Equal(expected[i], target[i], .00001); + } + } + + [Fact] + public void ItMultipliesInPlaceFloat() + { + // Arrange + var target = this._floatV1; + target.MultiplyByInPlace(2); + var expected = new float[] { 2.0F, 4.0F, -8.0F, 20.0F }; + + // Assert + Assert.Equal(expected.Length, target.Length); + for (int i = 0; i < expected.Length; i++) + { + Assert.Equal(expected[i], target[i], .00001F); + } + } + + [Fact] + public void ItMultipliesInPlaceDouble() + { + // Arrange + var target = this._doubleV1; + target.MultiplyByInPlace(2); + var expected = new double[] { 2.0, 4.0, -8.0, 20.0 }; + + // Assert + Assert.Equal(expected.Length, target.Length); + for (int i = 0; i < expected.Length; i++) + { + Assert.Equal(expected[i], target[i], .00001); + } + } + + [Fact] + public void ItDividesInPlaceFloat() + { + // Arrange + var target = this._floatV1; + target.DivideByInPlace(2); + var expected = new float[] { 0.5F, 1.0F, -2.0F, 5.0F }; + + // Assert + Assert.Equal(expected.Length, target.Length); + for (int i = 0; i < expected.Length; i++) + { + Assert.Equal(expected[i], target[i], .00001F); + } + } + + [Fact] + public void ItDividesInPlaceDouble() + { + // Arrange + var target = this._doubleV1; + target.DivideByInPlace(2); + var expected = new double[] { 0.5, 1.0, -2.0, 5.0 }; + + // Assert + Assert.Equal(expected.Length, target.Length); + for (int i = 0; i < expected.Length; i++) + { + Assert.Equal(expected[i], target[i], .00001); + } + } + + [Fact] + public void ItProducesExpectedCosineSimilarityResultsFloat() + { + // Arrange + var vectorList = new List(); + var comparisonVector = new float[] { 1.0F, 1.0F, 1.0F, 1.0F }; + vectorList.Add(new float[] { 1.0F, 1.0F, 1.0F, 1.0F }); // identical + vectorList.Add(new float[] { 1.0F, 1.0F, 1.0F, 2.0F }); + vectorList.Add(new float[] { 1.0F, 1.0F, -1.0F, -1.0F }); + vectorList.Add(new float[] { -1.0F, -1.0F, -1.0F, -1.0F }); // least similar + + // Act + var target = vectorList.Select(x => x.CosineSimilarity(comparisonVector)).ToArray(); + + // Assert + Assert.Equal(1.0, target[0]); // identical vectors results in similarity of 1 + Assert.True(target[0] > target[1]); + Assert.True(target[1] > target[2]); + Assert.True(target[2] > target[3]); + Assert.Equal(-1.0, target[3]); // opposing vectors results in similarity of -1 + } + + [Fact] + public void ItProducesExpectedCosineSimilarityResultsDouble() + { + // Arrange + var vectorList = new List(); + var comparisonVector = new double[] { 1.0, 1.0, 1.0, 1.0 }; + vectorList.Add(new double[] { 1.0, 1.0, 1.0, 1.0 }); // identical + vectorList.Add(new double[] { 1.0, 1.0, 1.0, 2.0 }); + vectorList.Add(new double[] { 1.0, 1.0, -1.0, -1.0 }); + vectorList.Add(new double[] { -1.0, -1.0, -1.0, -1.0 }); // least similar + + // Act + var target = vectorList.Select(x => x.CosineSimilarity(comparisonVector)).ToArray(); + + // Assert + Assert.Equal(1.0, target[0]); // identical vectors results in similarity of 1 + Assert.True(target[0] > target[1]); + Assert.True(target[1] > target[2]); + Assert.True(target[2] > target[3]); + Assert.Equal(-1.0, target[3]); // opposing vectors results in similarity of -1 + } +} diff --git a/dotnet/src/SemanticKernel.Test/VectorOperations/VectorSpanTests.cs b/dotnet/src/SemanticKernel.Test/VectorOperations/VectorSpanTests.cs new file mode 100644 index 000000000000..113fc0c24f7b --- /dev/null +++ b/dotnet/src/SemanticKernel.Test/VectorOperations/VectorSpanTests.cs @@ -0,0 +1,226 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using Microsoft.SemanticKernel.AI.Embeddings; +using Xunit; + +namespace SemanticKernelTests.VectorOperations; + +public class VectorSpanTests +{ + private readonly float[] _floatV1 = new float[] { 1.0F, 2.0F, -4.0F, 10.0F }; + private readonly float[] _floatV2 = new float[] { 3.0F, -7.0F, 1.0F, 6.0F }; + + private readonly double[] _doubleV1 = new double[] { 1.0, 2.0, -4.0, 10.0 }; + private readonly double[] _doubleV2 = new double[] { 3.0, -7.0, 1.0, 6.0 }; + + [Fact] + public void ItOnlySupportsFPDataTypes() + { + // Assert + Assert.True(EmbeddingSpan.IsSupported); + Assert.True(EmbeddingSpan.IsSupported); + + Assert.False(EmbeddingSpan.IsSupported); + Assert.False(EmbeddingSpan.IsSupported); + Assert.False(EmbeddingSpan.IsSupported); + Assert.False(EmbeddingSpan.IsSupported); + Assert.False(EmbeddingSpan.IsSupported); + Assert.False(EmbeddingSpan.IsSupported); + Assert.False(EmbeddingSpan.IsSupported); + Assert.False(EmbeddingSpan.IsSupported); + Assert.False(EmbeddingSpan.IsSupported); + Assert.False(EmbeddingSpan.IsSupported); + Assert.False(EmbeddingSpan.IsSupported); + Assert.False(EmbeddingSpan.IsSupported); + Assert.False(EmbeddingSpan.IsSupported); + } + + [Fact] + public void ItCanComputeCosineSimilarityFloats() + { + // Arrange + var vSpan1 = new EmbeddingSpan(this._floatV1); + var vSpan2 = new EmbeddingSpan(this._floatV2); + var target = vSpan1.CosineSimilarity(vSpan2); + + // Assert + Assert.Equal(0.41971841676, target, 5); + } + + [Fact] + public void ItCanComputeCosineSimilarityDouble() + { + // Arrange + var vSpan1 = new EmbeddingSpan(this._doubleV1); + var vSpan2 = new EmbeddingSpan(this._doubleV2); + var target = vSpan1.CosineSimilarity(vSpan2); + + // Assert + Assert.Equal(0.41971841676, target, 5); + } + + [Fact] + public void ItThrowsOnCosineSimilarityWithDifferentLengthVectorsFloat() + { + // Arrange + var vSpan1 = new EmbeddingSpan(this._floatV1); + var vSpan2 = new EmbeddingSpan(new float[] { -1.0F, 4.0F }); + + // Assert + try + { + vSpan1.CosineSimilarity(vSpan2); + } + catch (ArgumentException target) + { + Assert.IsType(target); + } + } + + [Fact] + public void ItThrowsOnCosineSimilarityWithDifferentLengthVectorsDouble() + { + // Arrange + var vSpan1 = new EmbeddingSpan(this._doubleV1); + var vSpan2 = new EmbeddingSpan(new double[] { -1.0, 4.0 }); + + // Assert + try + { + vSpan1.CosineSimilarity(vSpan2); + } + catch (ArgumentException target) + { + Assert.IsType(target); + } + } + + [Fact] + public void ItCanComputeEuclideanLengthFloat() + { + // Arrange + var vSpan1 = new EmbeddingSpan(this._floatV1); + + // Act + var target = vSpan1.EuclideanLength(); + + // Assert + Assert.Equal(11.0, target, 5); + } + + [Fact] + public void ItCanComputeEuclideanLengthDouble() + { + // Arrange + var vSpan1 = new EmbeddingSpan(this._doubleV1); + + // Act + var target = vSpan1.EuclideanLength(); + + // Assert + Assert.Equal(11.0, target, 5); + } + + [Fact] + public void ItCanComputeDotProductFloat() + { + // Arrange + var vSpan1 = new EmbeddingSpan(this._floatV1); + var vSpan2 = new EmbeddingSpan(this._floatV2); + + // Act + var target = vSpan1.Dot(vSpan2); + + // Assert + Assert.Equal(45.0, target, 5); + } + + [Fact] + public void ItCanComputeDotProductDouble() + { + // Arrange + var vSpan1 = new EmbeddingSpan(this._doubleV1); + var vSpan2 = new EmbeddingSpan(this._doubleV2); + + // Act + var target = vSpan1.Dot(vSpan2); + + // Assert + Assert.Equal(45.0, target, 5); + } + + [Fact] + public void ItThrowsOnDotProductWithDifferentLengthVectorsFP() + { + // Arrange + var vSpan1 = new EmbeddingSpan(this._floatV1); + var vSpan2 = new EmbeddingSpan(new float[] { -1.0F, 4.0F }); + + // Assert + try + { + vSpan1.Dot(vSpan2); + } + catch (ArgumentException target) + { + Assert.IsType(target); + } + } + + [Fact] + public void ItThrowsOnDotProductWithDifferentLengthVectorsDouble() + { + // Arrange + var vSpan1 = new EmbeddingSpan(this._doubleV1); + var vSpan2 = new EmbeddingSpan(new double[] { -1.0, 4.0 }); + + // Assert + try + { + vSpan1.Dot(vSpan2); + } + catch (ArgumentException target) + { + Assert.IsType(target); + } + } + + [Fact] + public void ItCanBeNormalizedFloat() + { + // Arrange + var vSpan1 = new EmbeddingSpan(this._floatV1); + + // Act + var target = vSpan1.Normalize(); + var expected = new EmbeddingSpan(new float[] { 0.09090909F, 0.18181819F, -0.3636364F, 0.90909094F }); + + // Assert + Assert.True(target.IsNormalized); + Assert.Equal(vSpan1.Span.Length, target.ReadOnlySpan.Length); + for (int i = 0; i < vSpan1.Span.Length; i++) + { + Assert.Equal(expected.Span[i], target.ReadOnlySpan[i], .00001F); + } + } + + [Fact] + public void ItCanBeNormalizedDouble() + { + // Arrange + var vSpan1 = new EmbeddingSpan(this._doubleV1); + + // Act + var target = vSpan1.Normalize(); + var expected = new EmbeddingSpan(new double[] { 0.09090909, 0.18181819, -0.3636364, 0.90909094 }); + + // Assert + Assert.True(target.IsNormalized); + Assert.Equal(vSpan1.Span.Length, target.ReadOnlySpan.Length); + for (int i = 0; i < vSpan1.Span.Length; i++) + { + Assert.Equal(expected.Span[i], target.ReadOnlySpan[i], .00001); + } + } +} diff --git a/dotnet/src/SemanticKernel.Test/XunitHelpers/ConsoleLogger.cs b/dotnet/src/SemanticKernel.Test/XunitHelpers/ConsoleLogger.cs new file mode 100644 index 000000000000..069acffd8de4 --- /dev/null +++ b/dotnet/src/SemanticKernel.Test/XunitHelpers/ConsoleLogger.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using Microsoft.Extensions.Logging; + +namespace SemanticKernelTests.XunitHelpers; + +/// +/// Basic logger printing to console +/// +internal static class ConsoleLogger +{ + internal static ILogger Log => LogFactory.CreateLogger(); + + private static ILoggerFactory LogFactory => s_loggerFactory.Value; + private static readonly Lazy s_loggerFactory = new(LogBuilder); + + private static ILoggerFactory LogBuilder() + { + return LoggerFactory.Create(builder => + { + builder.SetMinimumLevel(LogLevel.Trace); + // builder.AddFilter("Microsoft", LogLevel.Trace); + // builder.AddFilter("Microsoft", LogLevel.Debug); + // builder.AddFilter("Microsoft", LogLevel.Information); + // builder.AddFilter("Microsoft", LogLevel.Warning); + // builder.AddFilter("Microsoft", LogLevel.Error); + builder.AddConsole(); + }); + } +} diff --git a/dotnet/src/SemanticKernel.Test/XunitHelpers/RedirectOutput.cs b/dotnet/src/SemanticKernel.Test/XunitHelpers/RedirectOutput.cs new file mode 100644 index 000000000000..96e860e60660 --- /dev/null +++ b/dotnet/src/SemanticKernel.Test/XunitHelpers/RedirectOutput.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.IO; +using System.Text; +using Xunit.Abstractions; + +namespace SemanticKernelTests.XunitHelpers; + +public class RedirectOutput : TextWriter +{ + private readonly ITestOutputHelper _output; + + public RedirectOutput(ITestOutputHelper output) + { + this._output = output; + } + + public override Encoding Encoding { get; } = Encoding.UTF8; + + public override void WriteLine(string? value) + { + this._output.WriteLine(value ?? ""); + } +} diff --git a/dotnet/src/SemanticKernel/AI/AIException.cs b/dotnet/src/SemanticKernel/AI/AIException.cs new file mode 100644 index 000000000000..57c3a10a9bc7 --- /dev/null +++ b/dotnet/src/SemanticKernel/AI/AIException.cs @@ -0,0 +1,99 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using Microsoft.SemanticKernel.Diagnostics; + +namespace Microsoft.SemanticKernel.AI; + +/// +/// AI logic exception +/// +public class AIException : Exception +{ + /// + /// Possible error codes for exceptions + /// + public enum ErrorCodes + { + /// + /// Unknown error. + /// + UnknownError = -1, + + /// + /// No response. + /// + NoResponse, + + /// + /// Access is denied. + /// + AccessDenied, + + /// + /// The request was invalid. + /// + InvalidRequest, + + /// + /// The content of the response was invalid. + /// + InvalidResponseContent, + + /// + /// The request was throttled. + /// + Throttling, + + /// + /// The request timed out. + /// + RequestTimeout, + + /// + /// There was an error in the service. + /// + ServiceError, + + /// + /// The requested model is not available. + /// + ModelNotAvailable, + + /// + /// The supplied configuration was invalid. + /// + InvalidConfiguration, + + /// + /// The function is not supported. + /// + FunctionTypeNotSupported, + } + + /// + /// The exception's error code. + /// + public ErrorCodes ErrorCode { get; set; } + + /// + /// Construct an exception with an error code and message. + /// + /// Error code of the exception. + /// Message of the exception + public AIException(ErrorCodes errCode, string message) : base(errCode, message) + { + this.ErrorCode = errCode; + } + + /// + /// Construct an exception with an error code, message, and existing exception. + /// + /// Error code of the exception. + /// Message of the exception. + /// An exception that was thrown. + public AIException(ErrorCodes errCode, string message, Exception e) : base(errCode, message, e) + { + this.ErrorCode = errCode; + } +} diff --git a/dotnet/src/SemanticKernel/AI/CompleteRequestSettings.cs b/dotnet/src/SemanticKernel/AI/CompleteRequestSettings.cs new file mode 100644 index 000000000000..ac9b0746ac1a --- /dev/null +++ b/dotnet/src/SemanticKernel/AI/CompleteRequestSettings.cs @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using Microsoft.SemanticKernel.SemanticFunctions; + +namespace Microsoft.SemanticKernel.AI; + +/// +/// Settings for a completion request. +/// +public class CompleteRequestSettings +{ + /// + /// Temperature controls the randomness of the completion. The higher the temperature, the more random the completion. + /// + public double Temperature { get; set; } = 0; + + /// + /// TopP controls the diversity of the completion. The higher the TopP, the more diverse the completion. + /// + public double TopP { get; set; } = 0; + + /// + /// Number between -2.0 and 2.0. Positive values penalize new tokens based on whether they appear in the text so far, increasing the model's likelihood to talk about new topics. + /// + public double PresencePenalty { get; set; } = 0; + + /// + /// Number between -2.0 and 2.0. Positive values penalize new tokens based on their existing frequency in the text so far, decreasing the model's likelihood to repeat the same line verbatim. + /// + public double FrequencyPenalty { get; set; } = 0; + + /// + /// The maximum number of tokens to generate in the completion. + /// + public int MaxTokens { get; set; } = 100; + + /// + /// Sequences where the completion will stop generating further tokens. + /// + public IList StopSequences { get; set; } = Array.Empty(); + + /// + /// Update this settings object with the values from another settings object. + /// + /// The config whose values to use + /// Returns this CompleteRequestSettings object + public CompleteRequestSettings UpdateFromCompletionConfig(PromptTemplateConfig.CompletionConfig config) + { + this.Temperature = config.Temperature; + this.TopP = config.TopP; + this.PresencePenalty = config.PresencePenalty; + this.FrequencyPenalty = config.FrequencyPenalty; + this.MaxTokens = config.MaxTokens; + this.StopSequences = config.StopSequences; + return this; + } + + /// + /// Create a new settings object with the values from another settings object. + /// + /// + /// An instance of + public static CompleteRequestSettings FromCompletionConfig(PromptTemplateConfig.CompletionConfig config) + { + return new CompleteRequestSettings + { + Temperature = config.Temperature, + TopP = config.TopP, + PresencePenalty = config.PresencePenalty, + FrequencyPenalty = config.FrequencyPenalty, + MaxTokens = config.MaxTokens, + StopSequences = config.StopSequences, + }; + } +} diff --git a/dotnet/src/SemanticKernel/AI/Embeddings/COSINE_SIMILARITY.md b/dotnet/src/SemanticKernel/AI/Embeddings/COSINE_SIMILARITY.md new file mode 100644 index 000000000000..bf2676a9eb54 --- /dev/null +++ b/dotnet/src/SemanticKernel/AI/Embeddings/COSINE_SIMILARITY.md @@ -0,0 +1,60 @@ +# Cosine Similarity + +Cosine similarity is a measure of the degree of similarity between two vectors in +a multi-dimensional space. It is commonly used in artificial intelligence and natural +language processing to compare [embeddings](README.md), +which are numerical representations of +words or other objects. + +The cosine similarity between two vectors is calculated by taking the +[dot product](DOT_PRODUCT.md) of the two vectors and dividing it by the product +of their magnitudes. This results in a value between -1 and 1, where 1 indicates +that the two vectors are identical, 0 indicates that they are orthogonal +(i.e., have no correlation), and -1 indicates that they are opposite. + +Cosine similarity is particularly useful when working with high-dimensional data +such as word embeddings because it takes into account both the magnitude and direction +of each vector. This makes it more robust than other measures like +[Euclidean distance](EUCLIDEAN_DISTANCE.md), which only considers the magnitude. + +One common use case for cosine similarity is to find similar words based on their +embeddings. For example, given an embedding for "cat", we can use cosine similarity +to find other words with similar embeddings, such as "kitten" or "feline". This +can be useful for tasks like text classification or sentiment analysis where we +want to group together semantically related words. + +Another application of cosine similarity is in recommendation systems. By representing +items (e.g., movies, products) as vectors, we can use cosine similarity to find +items that are similar to each other or to a particular item of interest. This +allows us to make personalized recommendations based on a user's past behavior +or preferences. + +Overall, cosine similarity is an essential tool for developers working with AI +and embeddings. Its ability to capture both magnitude and direction makes it well +suited for high-dimensional data, and its applications in natural language +processing and recommendation systems make it a valuable tool for building +intelligent applications. + +# Applications + +Some examples about cosine similarity applications. + +1. Recommender Systems: Cosine similarity can be used to find similar items or users + in a recommendation system, based on their embedding vectors. + +2. Document Similarity: Cosine similarity can be used to compare the similarity of + two documents by representing them as embedding vectors and calculating the cosine + similarity between them. + +3. Image Recognition: Cosine similarity can be used to compare the embeddings of + two images, which can help with image recognition tasks. + +4. Natural Language Processing: Cosine similarity can be used to measure the semantic + similarity between two sentences or paragraphs by comparing their embedding vectors. + +5. Clustering: Cosine similarity can be used as a distance metric for clustering + algorithms, helping group similar data points together. + +6. Anomaly Detection: Cosine similarity can be used to identify anomalies in a dataset + by finding data points that have a low cosine similarity with other data points in + the dataset. diff --git a/dotnet/src/SemanticKernel/AI/Embeddings/DOT_PRODUCT.md b/dotnet/src/SemanticKernel/AI/Embeddings/DOT_PRODUCT.md new file mode 100644 index 000000000000..3df57d5db07e --- /dev/null +++ b/dotnet/src/SemanticKernel/AI/Embeddings/DOT_PRODUCT.md @@ -0,0 +1,52 @@ +# Dot Product + +Dot product is a mathematical operation that takes two equal-length vectors and +returns a single scalar value. It is also known as the scalar product or inner +product. The dot product of two vectors is calculated by multiplying corresponding +elements of each vector and then summing the results. + +The dot product has many applications in computer science, particularly in artificial +intelligence and machine learning. One common use case for the dot product is to +measure the similarity between two vectors, such as word [embeddings](README.md) +or image embeddings. This can be useful when trying to find similar words or images +in a dataset. + +In AI, the dot product can be used to calculate the +[cosine similarity](COSINE_SIMILARITY.md) between two vectors. Cosine similarity +measures the angle between two vectors, with a smaller angle indicating greater +similarity. This can be useful when working with high-dimensional data where +[Euclidean distance](EUCLIDEAN_DISTANCE.md) may not be an accurate measure of similarity. + +Another application of the dot product in AI is in neural networks, where it can +be used to calculate the weighted sum of inputs to a neuron. This calculation is +essential for forward propagation in neural networks. + +Overall, the dot product is an important operation for software developers working +with AI and embeddings. It provides a simple yet powerful way to measure similarity +between vectors and perform calculations necessary for neural networks. + +# Applications + +Some examples about dot product applications. + +1. Recommender systems: Dot product can be used to measure the similarity between + two vectors representing users or items in a recommender system, helping to identify + which items are most likely to be of interest to a particular user. + +2. Natural Language Processing (NLP): In NLP, dot product can be used to find the + cosine similarity between word embeddings, which is useful for tasks such as + finding synonyms or identifying related words. + +3. Image recognition: Dot product can be used to compare image embeddings, allowing + for more accurate image classification and object detection. + +4. Collaborative filtering: By taking the dot product of user and item embeddings, + collaborative filtering algorithms can predict how much a particular user will + like a particular item. + +5. Clustering: Dot product can be used as a distance metric when clustering data + points in high-dimensional spaces, such as when working with text or image embeddings. + +6. Anomaly detection: By comparing the dot product of an embedding with those of + its nearest neighbors, it is possible to identify data points that are significantly + different from others in their local neighborhood, indicating potential anomalies. diff --git a/dotnet/src/SemanticKernel/AI/Embeddings/EUCLIDEAN_DISTANCE.md b/dotnet/src/SemanticKernel/AI/Embeddings/EUCLIDEAN_DISTANCE.md new file mode 100644 index 000000000000..160f13394c8a --- /dev/null +++ b/dotnet/src/SemanticKernel/AI/Embeddings/EUCLIDEAN_DISTANCE.md @@ -0,0 +1,56 @@ +# Euclidean distance + +Euclidean distance is a mathematical concept that measures the straight-line distance +between two points in a Euclidean space. It is named after the ancient Greek mathematician +Euclid, who is often referred to as the "father of geometry". The formula for calculating +Euclidean distance is based on the Pythagorean theorem and can be expressed as: + + d = √(x2 - x1)² + (y2 - y1)² + +In higher dimensions, this formula can be generalized to: + + d = √(x2 - x1)² + (y2 - y1)² + ... + (zn - zn-1)² + +Euclidean distance has many applications in computer science and artificial intelligence, +particularly when working with [embeddings](README.md). Embeddings are numerical +representations of data that capture the underlying structure and relationships +between different data points. They are commonly used in natural language processing, +computer vision, and recommendation systems. + +When working with embeddings, it is often necessary to measure the similarity or +dissimilarity between different data points. This is where Euclidean distance comes +into play. By calculating the Euclidean distance between two embeddings, we can +determine how similar or dissimilar they are. + +One common use case for Euclidean distance in AI is in clustering algorithms such +as K-means. In this algorithm, data points are grouped together based on their proximity +to one another in a multi-dimensional space. The Euclidean distance between each +point and the centroid of its cluster is used to determine which points belong to +which cluster. + +Another use case for Euclidean distance is in recommendation systems. By calculating +the Euclidean distance between different items' embeddings, we can determine how +similar they are and make recommendations based on that information. + +Overall, Euclidean distance is an essential tool for software developers working +with AI and embeddings. It provides a simple yet powerful way to measure the similarity +or dissimilarity between different data points in a multi-dimensional space. + +# Applications + +Some examples about Euclidean distance applications. + +1. Recommender systems: Euclidean distance can be used to measure the similarity + between items in a recommender system, helping to provide more accurate recommendations. + +2. Image recognition: By calculating the Euclidean distance between image embeddings, + it is possible to identify similar images or detect duplicates. + +3. Natural Language Processing: Measuring the distance between word embeddings can + help with tasks such as semantic similarity and word sense disambiguation. + +4. Clustering: Euclidean distance is commonly used as a metric for clustering algorithms, + allowing them to group similar data points together. + +5. Anomaly detection: By calculating the distance between data points, it is possible + to identify outliers or anomalies in a dataset. diff --git a/dotnet/src/SemanticKernel/AI/Embeddings/Embedding.cs b/dotnet/src/SemanticKernel/AI/Embeddings/Embedding.cs new file mode 100644 index 000000000000..e208f30edb50 --- /dev/null +++ b/dotnet/src/SemanticKernel/AI/Embeddings/Embedding.cs @@ -0,0 +1,186 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Text.Json.Serialization; +using Microsoft.SemanticKernel.Diagnostics; + +namespace Microsoft.SemanticKernel.AI.Embeddings; + +/// +/// Represents a strongly typed vector of numeric data. +/// +/// +public readonly struct Embedding : IEquatable> + where TEmbedding : unmanaged +{ + /// + /// An empty instance. + /// + [SuppressMessage("Design", "CA1000:Do not declare static members on generic types", Justification = "Static empty struct instance.")] + public static Embedding Empty { get; } = new Embedding(Array.Empty()); + + /// + /// Initializes a new instance of the class that contains numeric elements copied from the specified collection. + /// + /// Type is unsupported. + /// A null vector is passed in. + public Embedding() + : this(Array.Empty()) + { + } + + /// + /// Initializes a new instance of the class that contains numeric elements copied from the specified collection. + /// + /// The source data. + /// An unsupported type is used as TEmbedding. + /// A null vector is passed in. + [JsonConstructor] + public Embedding(IEnumerable vector) + { + Verify.NotNull(vector, nameof(vector)); + + if (!IsSupported) + { + throw new NotSupportedException($"Embeddings do not support type '{typeof(TEmbedding).Name}'. " + + $"Supported types include: [ {string.Join(", ", Embedding.SupportedTypes.Select(t => t.Name).ToList())} ]"); + } + + // Create a local, protected copy + this._vector = vector.ToArray(); + } + + /// + /// Gets the vector as a + /// + [JsonPropertyName("vector")] + public IEnumerable Vector => this._vector.AsEnumerable(); + + /// + /// Gets a value that indicates whether is supported. + /// + [JsonIgnore] + [SuppressMessage("Design", "CA1000:Do not declare static members on generic types", Justification = "Following 'IsSupported' pattern of System.Numerics.")] + public static bool IsSupported => Embedding.SupportedTypes.Contains(typeof(TEmbedding)); + + /// + /// true if the vector is empty. + /// + [JsonIgnore] + public bool IsEmpty => this._vector.Length == 0; + + /// + /// The number of elements in the vector. + /// + [JsonIgnore] + public int Count => this._vector.Length; + + /// + /// Gets the vector as a read-only span. + /// + public ReadOnlySpan AsReadOnlySpan() + { + return new(this._vector); + } + + /// + /// Serves as the default hash function. + /// + /// A hash code for the current object. + public override int GetHashCode() + { + return this._vector.GetHashCode(); + } + + /// + /// Determines whether two object instances are equal. + /// + /// The object to compare with the current object. + /// true if the specified object is equal to the current object; otherwise, false. + public override bool Equals(object obj) + { + return (obj is Embedding other) && this.Equals(other); + } + + /// + /// Compares two embeddings for equality. + /// + /// The to compare with the current object. + /// >true if the specified object is equal to the current object; otherwise, false. + public bool Equals(Embedding other) + { + return this._vector.Equals(other._vector); + } + + /// + /// Compares two embeddings for equality. + /// + /// The left . + /// The right . + /// true if the embeddings contain identical data; false otherwise + public static bool operator ==(Embedding left, Embedding right) + { + return left.Equals(right); + } + + /// + /// Compares two embeddings for inequality. + /// + /// The left . + /// The right . + /// true if the embeddings do not contain identical data; false otherwise + public static bool operator !=(Embedding left, Embedding right) + { + return !(left == right); + } + + /// + /// Implicit creation of an object from an array of data.> + /// + /// An array of data. + public static explicit operator Embedding(TEmbedding[] vector) + { + return new Embedding(vector); + } + + /// + /// Implicit creation of an array of type from a . + /// + /// Source . + /// A clone of the underlying data. + public static explicit operator TEmbedding[](Embedding embedding) + { + return (TEmbedding[])embedding._vector.Clone(); + } + + /// + /// Implicit creation of an from a . + /// + /// Source . + /// A clone of the underlying data. + public static explicit operator ReadOnlySpan(Embedding embedding) + { + return (TEmbedding[])embedding._vector.Clone(); + } + + #region private ================================================================================ + + private readonly TEmbedding[] _vector; + + #endregion +} + +/// +/// Static class containing the supported types for . +/// +public static class Embedding +{ + /// + /// Types supported by the struct. + /// + public static readonly Type[] SupportedTypes = { typeof(float), typeof(double) }; +} diff --git a/dotnet/src/SemanticKernel/AI/Embeddings/EmbeddingReadOnlySpan.cs b/dotnet/src/SemanticKernel/AI/Embeddings/EmbeddingReadOnlySpan.cs new file mode 100644 index 000000000000..5279483143ef --- /dev/null +++ b/dotnet/src/SemanticKernel/AI/Embeddings/EmbeddingReadOnlySpan.cs @@ -0,0 +1,115 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Diagnostics.CodeAnalysis; +using Microsoft.SemanticKernel.AI.Embeddings.VectorOperations; + +namespace Microsoft.SemanticKernel.AI.Embeddings; + +/// +/// A view of a vector that allows for low-level, optimized, read-only mathematical operations. +/// +/// The unmanaged data type (, currently supported). +public ref struct EmbeddingReadOnlySpan + where TEmbedding : unmanaged +{ + /// + /// Constructor + /// + /// A a vector of contiguous, unmanaged data. + /// Indicates whether the data was pre-normalized. + /// + /// This does not verified that the data is normalized, nor make any guarantees that it remains so, + /// as the data can be modified at its source. The parameter simply + /// directs these operations to perform faster if the data is known to be normalized. + /// + public EmbeddingReadOnlySpan(ReadOnlySpan vector, bool isNormalized = false) + { + SupportedTypes.VerifyTypeSupported(typeof(TEmbedding)); + + this.ReadOnlySpan = vector; + this.IsNormalized = isNormalized; + } + + /// + /// Constructor + /// + /// A vector of contiguous, unmanaged data. + /// Indicates whether the data was pre-normalized. + /// + /// This does not verified that the data is normalized, nor make any guarantees that it remains so, + /// as the data can be modified at its source. The parameter simply + /// directs these operations to perform faster if the data is known to be normalized. + /// + public EmbeddingReadOnlySpan(TEmbedding[] vector, bool isNormalized = false) + : this(vector.AsReadOnlySpan(), isNormalized) + { + } + + /// + /// Constructor + /// + /// A vector of contiguous, unmanaged data. + /// Indicates whether the data was pre-normalized. + /// + /// This does not verified that the data is normalized, nor make any guarantees that it remains so, + /// as the data can be modified at its source. The parameter simply + /// directs these operations to perform faster if the data is known to be normalized. + /// + public EmbeddingReadOnlySpan(EmbeddingSpan span, bool isNormalized = false) + : this(span.Span.AsReadOnlySpan(), isNormalized) + { + } + + /// + /// Gets the underlying of unmanaged data. + /// + public ReadOnlySpan ReadOnlySpan { get; internal set; } + + /// + /// True if the data was specified to be normalized at construction. + /// + public bool IsNormalized { get; internal set; } + + /// + /// Calculates the dot product of this vector with another. + /// + /// The second vector. + /// The dot product as a + public double Dot(EmbeddingReadOnlySpan other) + { + return this.ReadOnlySpan.DotProduct(other.ReadOnlySpan); + } + + /// + /// Calculates the Euclidean length of this vector. + /// + /// The Euclidean length as a + public double EuclideanLength() + { + return this.ReadOnlySpan.EuclideanLength(); + } + + /// + /// Calculates the cosine similarity of this vector with another. + /// + /// The second vector. + /// The cosine similarity as a . + public double CosineSimilarity(EmbeddingReadOnlySpan other) + { + if (this.IsNormalized && other.IsNormalized) + { + // Because Normalized embeddings already have normalized lengths, cosine similarity is much + // faster - just a dot product. Don't have to compute lengths, square roots, etc. + return this.Dot(other); + } + + return this.ReadOnlySpan.CosineSimilarity(other.ReadOnlySpan); + } + + /// + /// Gets a value that indicates whether is supported. + /// + [SuppressMessage("Design", "CA1000:Do not declare static members on generic types", Justification = "Following 'IsSupported' pattern of System.Numerics.")] + public static bool IsSupported => SupportedTypes.IsSupported(typeof(TEmbedding)); +} diff --git a/dotnet/src/SemanticKernel/AI/Embeddings/EmbeddingSpan.cs b/dotnet/src/SemanticKernel/AI/Embeddings/EmbeddingSpan.cs new file mode 100644 index 000000000000..4f4a0cee882c --- /dev/null +++ b/dotnet/src/SemanticKernel/AI/Embeddings/EmbeddingSpan.cs @@ -0,0 +1,87 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Diagnostics.CodeAnalysis; +using Microsoft.SemanticKernel.AI.Embeddings.VectorOperations; + +namespace Microsoft.SemanticKernel.AI.Embeddings; + +/// +/// A view of a vector that allows for low-level, optimized, read-write mathematical operations. +/// +/// The unmanaged data type (, currently supported). +public ref struct EmbeddingSpan + where TEmbedding : unmanaged +{ + /// + /// Constructor + /// + /// A a vector of contiguous, unmanaged data. + public EmbeddingSpan(Span vector) + { + SupportedTypes.VerifyTypeSupported(typeof(TEmbedding)); + + this.Span = vector; + } + + /// + /// Constructor + /// + /// A vector of contiguous, unmanaged data. + public EmbeddingSpan(TEmbedding[] vector) + : this(vector.AsSpan()) + { + } + + /// + /// Gets the underlying of unmanaged data. + /// + public Span Span { get; internal set; } + + /// + /// Normalizes the underlying vector in-place, such that the Euclidean length is 1. + /// + /// A with 'IsNormalized' set to true. + public EmbeddingReadOnlySpan Normalize() + { + this.Span.NormalizeInPlace(); + return new EmbeddingReadOnlySpan(this.Span, true); + } + + /// + /// Calculates the dot product of this vector with another. + /// + /// The second vector. + /// The dot product as a + public double Dot(EmbeddingSpan other) + { + return this.Span.DotProduct(other.Span); + } + + /// + /// Calculates the Euclidean length of this vector. + /// + /// The Euclidean length as a + public double EuclideanLength() + { + return this.Span.EuclideanLength(); + } + + /// + /// Calculates the cosine similarity of this vector with another. + /// + /// The second vector. + /// The cosine similarity as a . + /// This operation can be performed much faster if the vectors are known to be normalized, by + /// converting to a with constructor parameter 'isNormalized' true. + public double CosineSimilarity(EmbeddingSpan other) + { + return this.Span.CosineSimilarity(other.Span); + } + + /// + /// Gets a value that indicates whether is supported. + /// + [SuppressMessage("Design", "CA1000:Do not declare static members on generic types", Justification = "Following 'IsSupported' pattern of System.Numerics.")] + public static bool IsSupported => SupportedTypes.IsSupported(typeof(TEmbedding)); +} diff --git a/dotnet/src/SemanticKernel/AI/Embeddings/IEmbeddingGenerator.cs b/dotnet/src/SemanticKernel/AI/Embeddings/IEmbeddingGenerator.cs new file mode 100644 index 000000000000..7939d80a4222 --- /dev/null +++ b/dotnet/src/SemanticKernel/AI/Embeddings/IEmbeddingGenerator.cs @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.SemanticKernel.Diagnostics; + +namespace Microsoft.SemanticKernel.AI.Embeddings; + +/// +/// Represents a generator of embeddings. +/// +/// The type from which embeddings will be generated. +/// The numeric type of the embedding data. +public interface IEmbeddingGenerator + where TEmbedding : unmanaged +{ + /// + /// Generates an embedding from the given . + /// + /// List of strings to generate embeddings for + /// List of embeddings + Task>> GenerateEmbeddingsAsync(IList data); +} + +/// +/// Provides a collection of static methods for operating on objects. +/// +public static class EmbeddingGeneratorExtensions +{ + /// + /// Generates an embedding from the given . + /// + /// The type from which embeddings will be generated. + /// The numeric type of the embedding data. + /// The embedding generator. + /// A value from which an will be generated. + /// A list of structs representing the input . + public static async Task> GenerateEmbeddingAsync + (this IEmbeddingGenerator generator, TValue value) + where TEmbedding : unmanaged + { + Verify.NotNull(generator, "Embeddings generator cannot be NULL"); + return (await generator.GenerateEmbeddingsAsync(new[] { value })).FirstOrDefault(); + } +} diff --git a/dotnet/src/SemanticKernel/AI/Embeddings/IEmbeddingIndex.cs b/dotnet/src/SemanticKernel/AI/Embeddings/IEmbeddingIndex.cs new file mode 100644 index 000000000000..c914d95cd471 --- /dev/null +++ b/dotnet/src/SemanticKernel/AI/Embeddings/IEmbeddingIndex.cs @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.SemanticKernel.Diagnostics; + +namespace Microsoft.SemanticKernel.AI.Embeddings; + +/// +/// Represents an searchable index of structs. +/// +/// The data type of the embedding. +public interface IEmbeddingIndex + where TEmbedding : unmanaged +{ + /// + /// Gets the nearest matches to the . + /// + /// The storage collection to search. + /// The input to use as the search. + /// The max number of results to return. + /// The minimum score to consider in the distance calculation. + /// A tuple consisting of the and the similarity score as a . + IAsyncEnumerable<(IEmbeddingWithMetadata, double)> GetNearestMatchesAsync( + string collection, + Embedding embedding, + int limit = 1, + double minRelevanceScore = 0.0); +} + +/// +/// Common extension methods for objects. +/// +public static class EmbeddingIndexExtensions +{ + /// + /// Searches the index for the nearest match to the . + /// + public static async Task<(IEmbeddingWithMetadata, double)> GetNearestMatchAsync(this IEmbeddingIndex index, + string collection, + Embedding embedding, + double minScore = 0.0) + where TEmbedding : unmanaged + { + Verify.NotNull(index, "Embedding index cannot be NULL"); + await foreach (var match in index.GetNearestMatchesAsync(collection, embedding, 1, minScore)) + { + return match; + } + + return default; + } +} diff --git a/dotnet/src/SemanticKernel/AI/Embeddings/IEmbeddingWithMetadata.cs b/dotnet/src/SemanticKernel/AI/Embeddings/IEmbeddingWithMetadata.cs new file mode 100644 index 000000000000..8a498438d71a --- /dev/null +++ b/dotnet/src/SemanticKernel/AI/Embeddings/IEmbeddingWithMetadata.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft. All rights reserved. + +namespace Microsoft.SemanticKernel.AI.Embeddings; + +/// +/// Represents an object that has an . +/// +/// The embedding data type. +public interface IEmbeddingWithMetadata + where TEmbedding : unmanaged +{ + /// + /// Gets the . + /// + Embedding Embedding { get; } +} diff --git a/dotnet/src/SemanticKernel/AI/Embeddings/README.md b/dotnet/src/SemanticKernel/AI/Embeddings/README.md new file mode 100644 index 000000000000..a63f024b5f59 --- /dev/null +++ b/dotnet/src/SemanticKernel/AI/Embeddings/README.md @@ -0,0 +1,62 @@ +# Embeddings + +Embeddings are a powerful tool for software developers working with artificial intelligence +and natural language processing. They allow computers to understand the meaning of +words in a more sophisticated way, by representing them as high-dimensional vectors +rather than simple strings of characters. + +Embeddings work by mapping each word in a vocabulary to a point in a high-dimensional +space. This space is designed so that words with similar meanings are located near each other. +This allows algorithms to identify relationships between words, such as synonyms or +antonyms, without needing explicit rules or human supervision. + +One popular method for creating embeddings is +Word2Vec [[1]](https://arxiv.org/abs/1301.3781)[[2]](https://arxiv.org/abs/1310.4546), +which uses neural networks to learn the relationships between words from large amounts +of text data. Other methods include [GloVe](https://nlp.stanford.edu/projects/glove/) +and [FastText](https://research.facebook.com/downloads/fasttext/). These methods +all have different strengths and weaknesses, but they share the common goal of creating +meaningful representations of words that can be used in machine learning models. + +Embeddings can be used in many different applications, including sentiment analysis, +document classification, and recommendation systems. They are particularly useful +when working with unstructured text data where traditional methods like bag-of-words +models struggle. + +Software developers can use pre-trained embedding model, or train their one with their +own custom datasets. Pre-trained embedding models have been trained on large amounts +of data and can be used out-of-the-box for many applications. Custom embedding models +may be necessary when working with specialized vocabularies or domain-specific language. + +Overall, embeddings are an essential tool for software developers working with AI +and natural language processing. They provide a powerful way to represent and understand +the meaning of words in a computationally efficient manner. + +## Applications + +Some examples about embeddings applications. + +1. Recommender systems: Embeddings can be used to represent the items in a recommender + system, allowing for more accurate recommendations based on similarity between items. + +2. Natural Language Processing (NLP): Embeddings can be used to represent words or + sentences in NLP tasks such as sentiment analysis, named entity recognition, and + text classification. + +3. Image recognition: Embeddings can be used to represent images in computer vision + tasks such as object detection and image classification. + +4. Anomaly detection: Embeddings can be used to represent data points in high-dimensional + datasets, making it easier to identify outliers or anomalous data points. + +5. Graph analysis: Embeddings can be used to represent nodes in a graph, allowing + for more efficient graph analysis and visualization. + +6. Personalization: Embeddings can be used to represent users in personalized recommendation + systems or personalized search engines. + +## Vector Operations used with Embeddings + + - [Cosine Similarity](COSINE_SIMILARITY.md) + - [Dot Product](DOT_PRODUCT.md) + - [Euclidean Distance](EUCLIDEAN_DISTANCE.md) diff --git a/dotnet/src/SemanticKernel/AI/Embeddings/SupportedTypes.cs b/dotnet/src/SemanticKernel/AI/Embeddings/SupportedTypes.cs new file mode 100644 index 000000000000..f69f79cbd102 --- /dev/null +++ b/dotnet/src/SemanticKernel/AI/Embeddings/SupportedTypes.cs @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; + +namespace Microsoft.SemanticKernel.AI.Embeddings; + +/// +/// Static class to identify which data types are supported by the vector operations. +/// +public static class SupportedTypes +{ + /// + /// Determines whether a specified type is supported by the vector operations. + /// + /// The to check. + /// true if the vector operations support this type. + public static bool IsSupported(Type type) + { + return s_types.Contains(type); + } + + /// + /// The collection of types supported by the vector operations. + /// + public static IEnumerable Types => s_types; + + /// + /// Checks if a specified type is supported by the vector operations. + /// + /// The type to check.s + /// Caller member name. + /// Throws if type is not supported. + internal static void VerifyTypeSupported(Type type, [CallerMemberName] string caller = "") + { + if (!IsSupported(type)) + { + ThrowTypeNotSupported(type, caller); + } + } + + #region internal ================================================================================ + + /// + /// Throws type not supported exception. + /// + /// The type to check.s + /// Caller member name. + /// Throws if type is not supported. + internal static void ThrowTypeNotSupported(Type type, [CallerMemberName] string caller = "") + { + throw new NotSupportedException($"Type '{type.Name}' not supported by {caller}. " + + $"Supported types include: [ {ToString()} ]"); + } + + #endregion + + #region private ================================================================================ + + private static readonly Type[] s_types = { typeof(float), typeof(double) }; + + private static new string ToString() + { + return string.Join(", ", s_types.Select(t => t.Name).ToList()); + } + + #endregion +} diff --git a/dotnet/src/SemanticKernel/AI/Embeddings/VectorOperations/CosineSimilarityOperation.cs b/dotnet/src/SemanticKernel/AI/Embeddings/VectorOperations/CosineSimilarityOperation.cs new file mode 100644 index 000000000000..2544421b9f50 --- /dev/null +++ b/dotnet/src/SemanticKernel/AI/Embeddings/VectorOperations/CosineSimilarityOperation.cs @@ -0,0 +1,145 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Runtime.InteropServices; + +namespace Microsoft.SemanticKernel.AI.Embeddings.VectorOperations; + +/// +/// Extension methods to calculate the cosine similarity between two vectors. +/// +/// +/// https://en.wikipedia.org/wiki/Cosine_similarity +/// +public static class CosineSimilarityOperation +{ + /// + /// Calculate the cosine similarity between two vectors of type . + /// + /// The unmanaged data type (, currently supported). + /// The first vector. + /// The second vector. + public static double CosineSimilarity(this ReadOnlySpan x, ReadOnlySpan y) + where TNumber : unmanaged + { + if (typeof(TNumber) == typeof(float)) + { + ReadOnlySpan floatSpanX = MemoryMarshal.Cast(x); + ReadOnlySpan floatSpanY = MemoryMarshal.Cast(y); + return CosineSimilarityImplementation(floatSpanX, floatSpanY); + } + else if (typeof(TNumber) == typeof(double)) + { + ReadOnlySpan doubleSpanX = MemoryMarshal.Cast(x); + ReadOnlySpan doubleSpanY = MemoryMarshal.Cast(y); + return CosineSimilarityImplementation(doubleSpanX, doubleSpanY); + } + + SupportedTypes.ThrowTypeNotSupported(typeof(TNumber)); + return default; + } + + /// + /// Calculate the cosine similarity between two vectors of type . + /// + /// The unmanaged data type (, currently supported). + /// The first vector. + /// The second vector. + public static double CosineSimilarity(this Span x, Span y) + where TNumber : unmanaged + { + return x.AsReadOnlySpan().CosineSimilarity(y.AsReadOnlySpan()); + } + + /// + /// Calculate the cosine similarity between two vectors of type . + /// + /// The unmanaged data type (, currently supported). + /// The first vector. + /// The second vector. + public static double CosineSimilarity(this TNumber[] x, TNumber[] y) + where TNumber : unmanaged + { + return x.AsReadOnlySpan().CosineSimilarity(y.AsReadOnlySpan()); + } + + #region private ================================================================================ + + private static unsafe double CosineSimilarityImplementation(ReadOnlySpan x, ReadOnlySpan y) + { + if (x.Length != y.Length) + { + throw new ArgumentException("Array lengths must be equal"); + } + + double dotSum = 0; + double lenXSum = 0; + double lenYSum = 0; + fixed (double* pxBuffer = x) + { + fixed (double* pyBuffer = y) + { + double* px = pxBuffer; + double* pxMax = px + x.Length; + double* py = pyBuffer; + while (px < pxMax) + { + double xVal = *px; + double yVal = *py; + // Dot product + dotSum += xVal * yVal; + // For magnitude of x + lenXSum += xVal * xVal; + // For magnitude of y + lenYSum += yVal * yVal; + ++px; + ++py; + } + + // Cosine Similarity of X, Y + // Sum(X * Y) / |X| * |Y| + return dotSum / (Math.Sqrt(lenXSum) * Math.Sqrt(lenYSum)); + } + } + } + + private static unsafe double CosineSimilarityImplementation(ReadOnlySpan x, ReadOnlySpan y) + { + if (x.Length != y.Length) + { + throw new ArgumentException("Array lengths must be equal"); + } + + double dotSum = 0; + double lenXSum = 0; + double lenYSum = 0; + fixed (float* pxBuffer = x) + { + fixed (float* pyBuffer = y) + { + float* px = pxBuffer; + float* pxMax = px + x.Length; + float* py = pyBuffer; + while (px < pxMax) + { + float xVal = *px; + float yVal = *py; + // Dot product + dotSum += xVal * yVal; + // For magnitude of x + lenXSum += xVal * xVal; + // For magnitude of y + lenYSum += yVal * yVal; + ++px; + ++py; + } + + // Cosine Similarity of X, Y + // Sum(X * Y) / |X| * |Y| + return dotSum / (Math.Sqrt(lenXSum) * Math.Sqrt(lenYSum)); + } + } + } + + #endregion +} diff --git a/dotnet/src/SemanticKernel/AI/Embeddings/VectorOperations/DivideOperation.cs b/dotnet/src/SemanticKernel/AI/Embeddings/VectorOperations/DivideOperation.cs new file mode 100644 index 000000000000..e9af7930a9b9 --- /dev/null +++ b/dotnet/src/SemanticKernel/AI/Embeddings/VectorOperations/DivideOperation.cs @@ -0,0 +1,81 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Runtime.InteropServices; + +namespace Microsoft.SemanticKernel.AI.Embeddings.VectorOperations; + +/// +/// Extension methods for vector division. +/// +public static class DivideOperation +{ + /// + /// Divide all elements of of type by . + /// + /// The unmanaged data type (, currently supported). + /// The data vector + /// The value to divide by. + public static void DivideByInPlace(this Span span, double divisor) + where TNumber : unmanaged + { + if (typeof(TNumber) == typeof(float)) + { + Span floatSpan = MemoryMarshal.Cast(span); + DivideByInPlaceImplementation(floatSpan, (float)divisor); + } + else if (typeof(TNumber) == typeof(double)) + { + Span doubleSpan = MemoryMarshal.Cast(span); + DivideByInPlaceImplementation(doubleSpan, divisor); + } + else + { + SupportedTypes.ThrowTypeNotSupported(typeof(TNumber)); + } + } + + /// + /// Divide all elements of an array of type by . + /// + /// The unmanaged data type (, currently supported). + /// The data vector + /// The value to divide by. + public static void DivideByInPlace(this TNumber[] vector, double divisor) + where TNumber : unmanaged + { + vector.AsSpan().DivideByInPlace(divisor); + } + + #region private ================================================================================ + + private static unsafe void DivideByInPlaceImplementation(Span x, float divisor) + { + fixed (float* pxBuffer = x) + { + float* px = pxBuffer; + float* pxMax = px + x.Length; + while (px < pxMax) + { + *px = *px / divisor; + px++; + } + } + } + + private static unsafe void DivideByInPlaceImplementation(Span x, double divisor) + { + fixed (double* pxBuffer = x) + { + double* px = pxBuffer; + double* pxMax = px + x.Length; + while (px < pxMax) + { + *px = *px / divisor; + px++; + } + } + } + + #endregion +} diff --git a/dotnet/src/SemanticKernel/AI/Embeddings/VectorOperations/DotProductOperation.cs b/dotnet/src/SemanticKernel/AI/Embeddings/VectorOperations/DotProductOperation.cs new file mode 100644 index 000000000000..1b5a63486269 --- /dev/null +++ b/dotnet/src/SemanticKernel/AI/Embeddings/VectorOperations/DotProductOperation.cs @@ -0,0 +1,313 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Runtime.InteropServices; + +namespace Microsoft.SemanticKernel.AI.Embeddings.VectorOperations; + +/// +/// Extension methods for vector dot product. +/// +/// +/// https://en.wikipedia.org/wiki/Dot_product +/// +public static class DotProductOperation +{ + /// + /// Calculate the dot products of two vectors of type . + /// + /// The unmanaged data type (, currently supported). + /// The first vector. + /// The second vector. + /// The dot product as a . + public static double DotProduct(this ReadOnlySpan x, ReadOnlySpan y) + where TNumber : unmanaged + { + if (typeof(TNumber) == typeof(float)) + { + ReadOnlySpan floatSpanX = MemoryMarshal.Cast(x); + ReadOnlySpan floatSpanY = MemoryMarshal.Cast(y); + return DotProductImplementation(floatSpanX, floatSpanY); + } + else if (typeof(TNumber) == typeof(double)) + { + ReadOnlySpan doubleSpanX = MemoryMarshal.Cast(x); + ReadOnlySpan doubleSpanY = MemoryMarshal.Cast(y); + return DotProductImplementation(doubleSpanX, doubleSpanY); + } + + SupportedTypes.ThrowTypeNotSupported(typeof(TNumber)); + return default; + } + + /// + /// Calculate the dot products of two vectors of type . + /// + /// The unmanaged data type (, currently supported). + /// The first vector. + /// The second vector. + /// The dot product as a . + public static double DotProduct(this Span x, Span y) + where TNumber : unmanaged + { + return x.AsReadOnlySpan().DotProduct(y.AsReadOnlySpan()); + } + + /// + /// Calculate the dot products of two vectors of type . + /// + /// The unmanaged data type (, currently supported). + /// The first vector. + /// The second vector. + /// The dot product as a . + public static double DotProduct(this TNumber[] x, TNumber[] y) + where TNumber : unmanaged + { + return x.AsReadOnlySpan().DotProduct(y.AsReadOnlySpan()); + } + + #region private ================================================================================ + + private static unsafe double DotProductImplementation(ReadOnlySpan x, ReadOnlySpan y) + { + if (x.Length != y.Length) + { + throw new ArgumentException("Array lengths must be equal"); + } + + if (x.Length % 4 == 0) + { + return DotProduct_Len4(x, y); + } + + if (x.Length % 2 == 0) + { + return DotProduct_Len2(x, y); + } + + // Vanilla Dot Product + fixed (double* pxBuffer = x) + { + fixed (double* pyBuffer = y) + { + double dotSum = 0; + double* px = pxBuffer; + double* pxMax = px + x.Length; + double* py = pyBuffer; + while (px < pxMax) + { + // Dot product + dotSum += *px * *py; + ++px; + ++py; + } + + return dotSum; + } + } + } + + private static unsafe double DotProductImplementation(ReadOnlySpan x, ReadOnlySpan y) + { + if (x.Length != y.Length) + { + throw new ArgumentException("Array lengths must be equal"); + } + + if (x.Length % 4 == 0) + { + return DotProduct_Len4(x, y); + } + + if (x.Length % 2 == 0) + { + // Twice as fast + return DotProduct_Len2(x, y); + } + + // Vanilla dot product + double dotSum = 0; + fixed (float* pxBuffer = x) + { + fixed (float* pyBuffer = y) + { + float* px = pxBuffer; + float* pxMax = px + x.Length; + float* py = pyBuffer; + while (px < pxMax) + { + // Dot product + dotSum += *px * *py; + ++px; + ++py; + } + + return dotSum; + } + } + } + + /// + /// Unrolled Dot Product for even length arrays. Should typically be twice as fast. + /// + /// Accumulates to . + private static unsafe double DotProduct_Len2(ReadOnlySpan x, ReadOnlySpan y) + { + if (x.Length != y.Length) + { + throw new ArgumentException("Array lengths must be equal"); + } + + if (x.Length % 4 != 0) + { + throw new ArgumentException("Array length must be a multiple of 2"); + } + + double dotSum1 = 0; + double dotSum2 = 0; + fixed (float* pxBuffer = x) + { + fixed (float* pyBuffer = y) + { + float* px = pxBuffer; + float* pxMax = px + x.Length; + float* py = pyBuffer; + while (px < pxMax) + { + // Dot product + dotSum1 += *px * *py; + dotSum2 += *(px + 1) * *(py + 1); + px += 2; + py += 2; + } + + return dotSum1 + dotSum2; + } + } + } + + /// + /// Unrolled Dot Product for length of multiple of 4. + /// + /// Accumulates to . + private static unsafe double DotProduct_Len4(ReadOnlySpan x, ReadOnlySpan y) + { + if (x.Length != y.Length) + { + throw new ArgumentException("Array lengths must be equal"); + } + + if (x.Length % 4 != 0) + { + throw new ArgumentException("Array length must be a multiple of 4"); + } + + double dotSum1 = 0; + double dotSum2 = 0; + double dotSum3 = 0; + double dotSum4 = 0; + fixed (float* pxBuffer = x) + { + fixed (float* pyBuffer = y) + { + float* px = pxBuffer; + float* pxMax = px + x.Length; + float* py = pyBuffer; + while (px < pxMax) + { + // Dot product + dotSum1 += *px * *py; + dotSum2 += *(px + 1) * *(py + 1); + dotSum3 += *(px + 2) * *(py + 2); + dotSum4 += *(px + 3) * *(py + 3); + px += 4; + py += 4; + } + + return dotSum1 + dotSum2 + dotSum3 + dotSum4; + } + } + } + + /// + /// Unrolled Dot Product for even length arrays. Should typically be twice as fast. + /// + private static unsafe double DotProduct_Len2(ReadOnlySpan x, ReadOnlySpan y) + { + if (x.Length != y.Length) + { + throw new ArgumentException("Array lengths must be equal"); + } + + if (x.Length % 4 != 0) + { + throw new ArgumentException("Array length must be a multiple of 2"); + } + + fixed (double* pxBuffer = x) + { + fixed (double* pyBuffer = y) + { + double dotSum1 = 0; + double dotSum2 = 0; + double* px = pxBuffer; + double* pxMax = px + x.Length; + double* py = pyBuffer; + while (px < pxMax) + { + // Dot product + dotSum1 += *px * *py; + dotSum2 += *(px + 1) * *(py + 1); + px += 2; + py += 2; + } + + return dotSum1 + dotSum2; + } + } + } + + /// + /// Unrolled Dot Product for length of multiple of 4. + /// + private static unsafe double DotProduct_Len4(ReadOnlySpan x, ReadOnlySpan y) + { + if (x.Length != y.Length) + { + throw new ArgumentException("Array lengths must be equal"); + } + + if (x.Length % 4 != 0) + { + throw new ArgumentException("Array length must be a multiple of 4"); + } + + fixed (double* pxBuffer = x) + { + fixed (double* pyBuffer = y) + { + double dotSum1 = 0; + double dotSum2 = 0; + double dotSum3 = 0; + double dotSum4 = 0; + double* px = pxBuffer; + double* pxMax = px + x.Length; + double* py = pyBuffer; + while (px < pxMax) + { + // Dot product + dotSum1 += *px * *py; + dotSum2 += *(px + 1) * *(py + 1); + dotSum3 += *(px + 2) * *(py + 2); + dotSum4 += *(px + 3) * *(py + 3); + px += 4; + py += 4; + } + + return dotSum1 + dotSum2 + dotSum3 + dotSum4; + } + } + } + + #endregion +} diff --git a/dotnet/src/SemanticKernel/AI/Embeddings/VectorOperations/EuclideanLengthOperation.cs b/dotnet/src/SemanticKernel/AI/Embeddings/VectorOperations/EuclideanLengthOperation.cs new file mode 100644 index 000000000000..431a56a392e5 --- /dev/null +++ b/dotnet/src/SemanticKernel/AI/Embeddings/VectorOperations/EuclideanLengthOperation.cs @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; + +namespace Microsoft.SemanticKernel.AI.Embeddings.VectorOperations; + +/// +/// Extension methods to calculate the Euclidean length of a vector. +/// +public static class EuclideanLengthOperation +{ + /// + /// Calculate the Euclidean length of a vector of type . + /// + /// The unmanaged data type (, currently supported). + /// The vector. + /// Euclidean length as a + public static double EuclideanLength(this ReadOnlySpan x) + where TNumber : unmanaged + { + return Math.Sqrt(x.DotProduct(x)); + } + + /// + /// Calculate the Euclidean length of a vector of type . + /// + /// The unmanaged data type (, currently supported). + /// The vector. + /// Euclidean length as a + public static double EuclideanLength(this Span x) + where TNumber : unmanaged + { + var readOnly = x.AsReadOnlySpan(); + return readOnly.EuclideanLength(); + } + + /// + /// Calculate the Euclidean length of a vector of type . + /// + /// The unmanaged data type (, currently supported). + /// The vector. + /// Euclidean length as a + public static double EuclideanLength(this TNumber[] vector) + where TNumber : unmanaged + { + return vector.AsReadOnlySpan().EuclideanLength(); + } +} diff --git a/dotnet/src/SemanticKernel/AI/Embeddings/VectorOperations/MultiplyOperation.cs b/dotnet/src/SemanticKernel/AI/Embeddings/VectorOperations/MultiplyOperation.cs new file mode 100644 index 000000000000..979e12c4542b --- /dev/null +++ b/dotnet/src/SemanticKernel/AI/Embeddings/VectorOperations/MultiplyOperation.cs @@ -0,0 +1,83 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Runtime.InteropServices; + +namespace Microsoft.SemanticKernel.AI.Embeddings.VectorOperations; + +/// +/// Extension methods to multiply a vector by a scalar. +/// +public static class MultiplyOperation +{ + /// + /// Multiplies all elements of a vector by the scalar in-place. + /// Does not allocate new memory. + /// + /// The unmanaged data type (, currently supported). + /// The input vector. + /// The scalar. + public static void MultiplyByInPlace(this Span vector, double multiplier) + where TNumber : unmanaged + { + if (typeof(TNumber) == typeof(float)) + { + Span floatSpan = MemoryMarshal.Cast(vector); + MultiplyByInPlaceImplementation(floatSpan, (float)multiplier); + } + else if (typeof(TNumber) == typeof(double)) + { + Span doubleSpan = MemoryMarshal.Cast(vector); + MultiplyByInPlaceImplementation(doubleSpan, multiplier); + } + else + { + SupportedTypes.ThrowTypeNotSupported(typeof(TNumber)); + } + } + + /// + /// Multiplies all elements of a vector by the scalar in-place. + /// Does not allocate new memory. + /// + /// The unmanaged data type (, currently supported). + /// The input vector. + /// The scalar. + public static void MultiplyByInPlace(this TNumber[] vector, double multiplier) + where TNumber : unmanaged + { + vector.AsSpan().MultiplyByInPlace(multiplier); + } + + #region private ================================================================================ + + private static unsafe void MultiplyByInPlaceImplementation(Span x, float multiplier) + { + fixed (float* pxBuffer = x) + { + float* px = pxBuffer; + float* pxMax = px + x.Length; + while (px < pxMax) + { + *px = *px * multiplier; + px++; + } + } + } + + private static unsafe void MultiplyByInPlaceImplementation(Span x, double multiplier) + { + fixed (double* pxBuffer = x) + { + double* px = pxBuffer; + double* pxMax = px + x.Length; + while (px < pxMax) + { + *px = *px * multiplier; + px++; + } + } + } + + #endregion +} diff --git a/dotnet/src/SemanticKernel/AI/Embeddings/VectorOperations/NormalizeOperation.cs b/dotnet/src/SemanticKernel/AI/Embeddings/VectorOperations/NormalizeOperation.cs new file mode 100644 index 000000000000..3fb2a9fa448d --- /dev/null +++ b/dotnet/src/SemanticKernel/AI/Embeddings/VectorOperations/NormalizeOperation.cs @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; + +namespace Microsoft.SemanticKernel.AI.Embeddings.VectorOperations; + +/// +/// Extension methods to normalize a vector. +/// +/// +/// https://en.wikipedia.org/wiki/Unit_vector +/// +public static class NormalizeOperation +{ + /// + /// Normalizes a vector in-place by dividing all elements by the scalar Euclidean length. + /// The resulting length will be 1.0. Does not allocate new memory. + /// + /// The unmanaged data type (, currently supported). + /// The input vector. + public static void NormalizeInPlace(this Span vector) + where TNumber : unmanaged + { + vector.DivideByInPlace(vector.EuclideanLength()); + } + + /// + /// Normalizes a vector in-place by dividing all elements by the scalar Euclidean length. + /// The resulting length will be 1.0. Does not allocate new memory. + /// + /// The unmanaged data type (, currently supported). + /// The input vector. + public static void NormalizeInPlace(this TNumber[] vector) + where TNumber : unmanaged + { + vector.AsSpan().NormalizeInPlace(); + } +} diff --git a/dotnet/src/SemanticKernel/AI/Embeddings/VectorOperations/SpanExtensions.cs b/dotnet/src/SemanticKernel/AI/Embeddings/VectorOperations/SpanExtensions.cs new file mode 100644 index 000000000000..97013681f500 --- /dev/null +++ b/dotnet/src/SemanticKernel/AI/Embeddings/VectorOperations/SpanExtensions.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; + +namespace Microsoft.SemanticKernel.AI.Embeddings.VectorOperations; + +/// +/// Extension methods to convert from array and to . +/// +internal static class SpanExtensions +{ + internal static ReadOnlySpan AsReadOnlySpan(this TNumber[] vector) + { + return new ReadOnlySpan(vector); + } + + internal static ReadOnlySpan AsReadOnlySpan(this Span span) + { + return (ReadOnlySpan)span; + } +} diff --git a/dotnet/src/SemanticKernel/AI/ITextCompletionClient.cs b/dotnet/src/SemanticKernel/AI/ITextCompletionClient.cs new file mode 100644 index 000000000000..a4118375d8db --- /dev/null +++ b/dotnet/src/SemanticKernel/AI/ITextCompletionClient.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Threading.Tasks; + +namespace Microsoft.SemanticKernel.AI; + +/// +/// Interface for text completion clients. +/// +public interface ITextCompletionClient +{ + /// + /// Creates a completion for the prompt and settings. + /// + /// The prompt to complete. + /// Request settings for the completion API + /// Text generated by the remote model + public Task CompleteAsync(string text, CompleteRequestSettings requestSettings); +} diff --git a/dotnet/src/SemanticKernel/AI/OpenAI/Clients/AzureOpenAIClientAbstract.cs b/dotnet/src/SemanticKernel/AI/OpenAI/Clients/AzureOpenAIClientAbstract.cs new file mode 100644 index 000000000000..32746084f6c6 --- /dev/null +++ b/dotnet/src/SemanticKernel/AI/OpenAI/Clients/AzureOpenAIClientAbstract.cs @@ -0,0 +1,166 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Concurrent; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel.AI.OpenAI.HttpSchema; +using Microsoft.SemanticKernel.Text; + +namespace Microsoft.SemanticKernel.AI.OpenAI.Clients; + +/// +/// An abstract Azure OpenAI Client. +/// +[System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1054:URI-like parameters should not be strings", Justification = "OpenAI users use strings")] +public abstract class AzureOpenAIClientAbstract : OpenAIClientAbstract +{ + /// + /// Default Azure OpenAI REST API version + /// + protected const string DefaultAzureAPIVersion = "2022-12-01"; + + /// + /// Azure OpenAI API version + /// + protected string AzureOpenAIApiVersion + { + get { return this._azureOpenAIApiVersion; } + set + { + if (string.IsNullOrWhiteSpace(value)) + { + throw new AIException( + AIException.ErrorCodes.InvalidConfiguration, + "Invalid Azure OpenAI API version, the value is empty"); + } + + this._azureOpenAIApiVersion = value; + } + } + + /// + /// Azure endpoint of your models + /// + protected string Endpoint { get; set; } = string.Empty; + + /// + /// Construct an AzureOpenAIClientAbstract object + /// + /// Logger + protected AzureOpenAIClientAbstract(ILogger? log = null) : base(log) + { + } + + /// + /// Returns the deployment name of the model ID + /// + /// Azure OpenAI Model ID + /// Name of the deployment for the model ID + /// AIException thrown during request. + protected async Task GetDeploymentNameAsync(string modelId) + { + string fullModelId = this.Endpoint + ":" + modelId; + + // If the value is a deployment name + if (s_deploymentToModel.ContainsKey(fullModelId)) + { + return modelId; + } + + // If the value is a model ID present in the cache + if (s_modelToDeployment.TryGetValue(fullModelId, out string modelIdCached)) + { + return modelIdCached; + } + + // If the cache has already been warmed up + string modelsAvailable; + if (s_deploymentsCached.ContainsKey(this.Endpoint)) + { + modelsAvailable = string.Join(", ", s_modelToDeployment.Keys); + throw new AIException( + AIException.ErrorCodes.ModelNotAvailable, + $"Model '{modelId}' not available on {this.Endpoint}. " + + $"Available models: {modelsAvailable}. Deploy the model and restart the application."); + } + + await this.CacheDeploymentsAsync(); + + if (s_modelToDeployment.TryGetValue(fullModelId, out string modelIdAfterCache)) + { + return modelIdAfterCache; + } + + modelsAvailable = string.Join(", ", s_modelToDeployment.Keys); + throw new AIException( + AIException.ErrorCodes.ModelNotAvailable, + $"Model '{modelId}' not available on {this.Endpoint}. " + + $"Available models: {modelsAvailable}. Deploy the model and restart the application."); + } + + /// + /// Caches the list of deployments in Azure OpenAI. + /// + /// An async task + /// AIException thrown during the request. + protected async Task CacheDeploymentsAsync() + { + var url = $"{this.Endpoint}/openai/deployments?api-version={this.AzureOpenAIApiVersion}"; + HttpResponseMessage response = await this.HTTPClient.GetAsync(url); + if (!response.IsSuccessStatusCode) + { + throw new AIException( + AIException.ErrorCodes.ModelNotAvailable, + $"Unable to fetch the list of model deployments from Azure. Status code: {response.StatusCode}"); + } + + string json = await response.Content.ReadAsStringAsync(); + + lock (s_deploymentToModel) + { + try + { + var data = Json.Deserialize(json); + if (data == null) + { + throw new AIException( + AIException.ErrorCodes.InvalidResponseContent, + "Model not available. Unable to fetch the list of models."); + } + + foreach (var deployment in data.Deployments) + { + if (!deployment.IsAvailableDeployment() || string.IsNullOrEmpty(deployment.ModelName) || + string.IsNullOrEmpty(deployment.DeploymentName)) + { + continue; + } + + s_deploymentToModel[this.Endpoint + ":" + deployment.DeploymentName] = deployment.ModelName; + s_modelToDeployment[this.Endpoint + ":" + deployment.ModelName] = deployment.DeploymentName; + } + } + catch (Exception e) when (e is not AIException) + { + throw new AIException( + AIException.ErrorCodes.UnknownError, + "Model not available. Unable to fetch the list of models.", e); + } + } + + s_deploymentsCached[this.Endpoint] = true; + } + + #region private ================================================================================ + + // Caching Azure details across multiple instances so we don't have to use "deployment names" + private static readonly ConcurrentDictionary s_deploymentsCached = new(); + private static readonly ConcurrentDictionary s_deploymentToModel = new(); + private static readonly ConcurrentDictionary s_modelToDeployment = new(); + + private string _azureOpenAIApiVersion = DefaultAzureAPIVersion; + + #endregion +} diff --git a/dotnet/src/SemanticKernel/AI/OpenAI/Clients/OpenAIClientAbstract.cs b/dotnet/src/SemanticKernel/AI/OpenAI/Clients/OpenAIClientAbstract.cs new file mode 100644 index 000000000000..82c09e41d250 --- /dev/null +++ b/dotnet/src/SemanticKernel/AI/OpenAI/Clients/OpenAIClientAbstract.cs @@ -0,0 +1,248 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Mime; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.SemanticKernel.AI.Embeddings; +using Microsoft.SemanticKernel.AI.OpenAI.HttpSchema; +using Microsoft.SemanticKernel.Text; + +namespace Microsoft.SemanticKernel.AI.OpenAI.Clients; + +/// +/// An abstract OpenAI Client. +/// +[System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1054:URI-like parameters should not be strings", Justification = "OpenAI users use strings")] +public abstract class OpenAIClientAbstract : IDisposable +{ + /// + /// Logger + /// + protected ILogger Log { get; } = NullLogger.Instance; + + /// + /// HTTP client + /// + protected HttpClient HTTPClient { get; } + + private readonly HttpClientHandler _httpClientHandler; + + internal OpenAIClientAbstract(ILogger? log = null) + { + if (log != null) { this.Log = log; } + + // TODO: allow injection of retry logic, e.g. Polly + this._httpClientHandler = new() { CheckCertificateRevocationList = true }; + this.HTTPClient = new HttpClient(this._httpClientHandler); + this.HTTPClient.DefaultRequestHeaders.Add("User-Agent", HTTPUseragent); + } + + /// + /// Asynchronously sends a completion request for the prompt + /// + /// URL for the completion request API + /// Prompt to complete + /// The completed text + /// AIException thrown during the request. + protected async Task ExecuteCompleteRequestAsync(string url, string requestBody) + { + try + { + this.Log.LogDebug("Sending completion request to {0}: {1}", url, requestBody); + + var result = await this.ExecutePostRequestAsync(url, requestBody); + if (result.Completions.Count < 1) + { + throw new AIException( + AIException.ErrorCodes.InvalidResponseContent, + "Completions not found"); + } + + return result.Completions.First().Text; + } + catch (Exception e) when (e is not AIException) + { + throw new AIException( + AIException.ErrorCodes.UnknownError, + $"Something went wrong: {e.Message}", e); + } + } + + /// + /// Asynchronously sends an embedding request for the text. + /// + /// + /// + /// + /// + protected async Task>> ExecuteEmbeddingRequestAsync(string url, string requestBody) + { + try + { + var result = await this.ExecutePostRequestAsync(url, requestBody); + if (result.Embeddings.Count < 1) + { + throw new AIException( + AIException.ErrorCodes.InvalidResponseContent, + "Embeddings not found"); + } + + return result.Embeddings.Select(e => new Embedding(e.Values.ToArray())).ToList(); + } + catch (Exception e) when (e is not AIException) + { + throw new AIException( + AIException.ErrorCodes.UnknownError, + $"Something went wrong: {e.Message}", e); + } + } + + /// + /// Explicit finalizer called by IDisposable + /// + public void Dispose() + { + this.Dispose(true); + // Request CL runtime not to call the finalizer - reduce cost of GC + GC.SuppressFinalize(this); + } + + /// + /// Overridable finalizer for concrete classes + /// + /// + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + this.HTTPClient.Dispose(); + this._httpClientHandler.Dispose(); + } + } + + #region private ================================================================================ + + // HTTP user agent sent to remote endpoints + private const string HTTPUseragent = "Microsoft Semantic Kernel"; + + private async Task ExecutePostRequestAsync(string url, string requestBody) + { + string responseJson; + + try + { + using HttpContent content = new StringContent(requestBody, Encoding.UTF8, MediaTypeNames.Application.Json); + HttpResponseMessage response = await this.HTTPClient.PostAsync(url, content); + + if (response == null) + { + throw new AIException(AIException.ErrorCodes.NoResponse, "Empty response"); + } + + this.Log.LogTrace("HTTP response: {0} {1}", (int)response.StatusCode, response.StatusCode.ToString("G")); + + responseJson = await response.Content.ReadAsStringAsync(); + if (!response.IsSuccessStatusCode) + { + switch (response.StatusCode) + { + case HttpStatusCode.BadRequest: + case HttpStatusCode.MethodNotAllowed: + case HttpStatusCode.NotFound: + case HttpStatusCode.NotAcceptable: + case HttpStatusCode.Conflict: + case HttpStatusCode.Gone: + case HttpStatusCode.LengthRequired: + case HttpStatusCode.PreconditionFailed: + case HttpStatusCode.RequestEntityTooLarge: + case HttpStatusCode.RequestUriTooLong: + case HttpStatusCode.UnsupportedMediaType: + case HttpStatusCode.RequestedRangeNotSatisfiable: + case HttpStatusCode.ExpectationFailed: + case HttpStatusCode.MisdirectedRequest: + case HttpStatusCode.UnprocessableEntity: + case HttpStatusCode.Locked: + case HttpStatusCode.FailedDependency: + case HttpStatusCode.UpgradeRequired: + case HttpStatusCode.PreconditionRequired: + case HttpStatusCode.RequestHeaderFieldsTooLarge: + case HttpStatusCode.HttpVersionNotSupported: + throw new AIException( + AIException.ErrorCodes.InvalidRequest, + $"The request is not valid, HTTP status: {response.StatusCode:G}"); + + case HttpStatusCode.Unauthorized: + case HttpStatusCode.Forbidden: + case HttpStatusCode.ProxyAuthenticationRequired: + case HttpStatusCode.UnavailableForLegalReasons: + case HttpStatusCode.NetworkAuthenticationRequired: + throw new AIException( + AIException.ErrorCodes.AccessDenied, + $"The request is not authorized, HTTP status: {response.StatusCode:G}"); + + case HttpStatusCode.RequestTimeout: + throw new AIException( + AIException.ErrorCodes.RequestTimeout, + $"The request timed out, HTTP status: {response.StatusCode:G}"); + + case HttpStatusCode.TooManyRequests: + throw new AIException( + AIException.ErrorCodes.Throttling, + $"Too many requests, HTTP status: {response.StatusCode:G}"); + + case HttpStatusCode.InternalServerError: + case HttpStatusCode.NotImplemented: + case HttpStatusCode.BadGateway: + case HttpStatusCode.ServiceUnavailable: + case HttpStatusCode.GatewayTimeout: + case HttpStatusCode.InsufficientStorage: + throw new AIException( + AIException.ErrorCodes.ServiceError, + $"The service failed to process the request, HTTP status: {response.StatusCode:G}"); + + default: + throw new AIException( + AIException.ErrorCodes.UnknownError, + $"Unexpected HTTP response, status: {response.StatusCode:G}"); + } + } + } + catch (Exception e) when (e is not AIException) + { + throw new AIException( + AIException.ErrorCodes.UnknownError, + $"Something went wrong: {e.Message}", e); + } + + try + { + var result = Json.Deserialize(responseJson); + if (result != null) { return result; } + + throw new AIException( + AIException.ErrorCodes.InvalidResponseContent, + "Response JSON parse error"); + } + catch (Exception e) when (e is not AIException) + { + throw new AIException( + AIException.ErrorCodes.UnknownError, + $"Something went wrong: {e.Message}", e); + } + } + + // C# finalizer + ~OpenAIClientAbstract() + { + this.Dispose(false); + } + + #endregion +} diff --git a/dotnet/src/SemanticKernel/AI/OpenAI/HttpSchema/AzureDeployments.cs b/dotnet/src/SemanticKernel/AI/OpenAI/HttpSchema/AzureDeployments.cs new file mode 100644 index 000000000000..693c0710810b --- /dev/null +++ b/dotnet/src/SemanticKernel/AI/OpenAI/HttpSchema/AzureDeployments.cs @@ -0,0 +1,73 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Text.Json.Serialization; + +namespace Microsoft.SemanticKernel.AI.OpenAI.HttpSchema; + +/// +/// Azure OpenAI deployment schema +/// +public sealed class AzureDeployments +{ + /// + /// An Azure OpenAI deployment + /// + public class AzureDeployment + { + private string _status = string.Empty; + private string _type = string.Empty; + + /// + /// Azure deployment name + /// + [JsonPropertyName("id")] + public string DeploymentName { get; set; } = string.Empty; + + /// + /// Model Name + /// + [JsonPropertyName("model")] + public string ModelName { get; set; } = string.Empty; + + /// + /// Status of the deployment + /// + [JsonPropertyName("status")] + [SuppressMessage("Globalization", "CA1308:Normalize strings to uppercase", Justification = "Azure API expects lowercase")] + public string Status + { + get => this._status; + // ReSharper disable once ConditionalAccessQualifierIsNonNullableAccordingToAPIContract + set => this._status = value?.ToLowerInvariant().Trim() ?? string.Empty; + } + + /// + /// Type of the deployment + /// + [JsonPropertyName("object")] + [SuppressMessage("Globalization", "CA1308:Normalize strings to uppercase", Justification = "Azure API expects lowercase")] + public string Type + { + get => this._type; + // ReSharper disable once ConditionalAccessQualifierIsNonNullableAccordingToAPIContract + set => this._type = value?.ToLowerInvariant().Trim() ?? string.Empty; + } + + /// + /// Returns true if the deployment is active. + /// + /// Returns true if the deployment is active. + public bool IsAvailableDeployment() + { + return this.Type == "deployment" && this.Status == "succeeded"; + } + } + + /// + /// List of Azure OpenAI deployments + /// + [JsonPropertyName("data")] + public IList Deployments { get; set; } = new List(); +} diff --git a/dotnet/src/SemanticKernel/AI/OpenAI/HttpSchema/CompletionRequest.cs b/dotnet/src/SemanticKernel/AI/OpenAI/HttpSchema/CompletionRequest.cs new file mode 100644 index 000000000000..d257d86b473b --- /dev/null +++ b/dotnet/src/SemanticKernel/AI/OpenAI/HttpSchema/CompletionRequest.cs @@ -0,0 +1,117 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Text.Json.Serialization; + +namespace Microsoft.SemanticKernel.AI.OpenAI.HttpSchema; + +/// +/// Completion Request +/// +public abstract class CompletionRequest +{ + /// + /// What sampling temperature to use. Higher values means the model will take more risks. Try 0.9 for more creative + /// applications, and 0 (argmax sampling) for ones with a well-defined answer. It is generally recommend to use this + /// or "TopP" but not both. + /// + + [JsonPropertyName("temperature")] + [JsonPropertyOrder(1)] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public double? Temperature { get; set; } + + /// + /// An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of + /// the tokens with top_p probability mass. So 0.1 means only the tokens comprising the top 10% probability mass are + /// considered. It is generally recommend to use this or "Temperature" but not both. + /// + [JsonPropertyName("top_p")] + [JsonPropertyOrder(2)] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public double? TopP { get; set; } + + /// + /// Number between -2.0 and 2.0. Positive values penalize new tokens based on whether they appear in the text so + /// far, increasing the model's likelihood to talk about new topics. + /// + [JsonPropertyName("presence_penalty")] + [JsonPropertyOrder(3)] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public double? PresencePenalty { get; set; } + + /// + /// Number between -2.0 and 2.0. Positive values penalize new tokens based on their existing frequency in the text + /// so far, decreasing the model's likelihood to repeat the same line verbatim. + /// + [JsonPropertyName("frequency_penalty")] + [JsonPropertyOrder(4)] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public double? FrequencyPenalty { get; set; } + + /// + /// How many tokens to complete to. Can return fewer if a stop sequence is hit. + /// The token count of your prompt plus max_tokens cannot exceed the model's context length. Most models have a + /// context length of 2048 tokens (except for the newest models, which support 4096). + /// + [JsonPropertyName("max_tokens")] + [JsonPropertyOrder(5)] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public int? MaxTokens { get; set; } = 16; + + /// + /// Up to 4 sequences where the API will stop generating further tokens. + /// The returned text will not contain the stop sequence. + /// Type: string or array of strings + /// + [JsonPropertyName("stop")] + [JsonPropertyOrder(6)] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public object? Stop { get; set; } + + /// + /// How many different choices to request for each prompt. + /// Note: Because this parameter generates many completions, it can quickly consume your token quota. + /// + [JsonPropertyName("n")] + [JsonPropertyOrder(7)] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public int? NumChoices { get; set; } = 1; + + /// + /// Generates best_of completions server-side and returns the "best" + /// (the one with the highest log probability per token). + /// When used with NumChoices, BestOf controls the number of candidate completions and NumChoices specifies + /// how many to return. BestOf must be greater than NumChoices + /// + [JsonPropertyName("best_of")] + [JsonPropertyOrder(8)] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public int? BestOf { get; set; } = 1; + + /// + /// The prompt(s) to generate completions for, encoded as a string, array of strings, array of tokens, or array of token arrays + /// + [JsonPropertyName("prompt")] + [JsonPropertyOrder(100)] + public string Prompt { get; set; } = string.Empty; +} + +/// +/// OpenAI Completion Request +/// +public sealed class OpenAICompletionRequest : CompletionRequest +{ + /// + /// ID of the model to use. + /// + [JsonPropertyName("model")] + [JsonPropertyOrder(-1)] + public string? Model { get; set; } +} + +/// +/// Azure OpenAI Completion Request +/// +public sealed class AzureCompletionRequest : CompletionRequest +{ +} diff --git a/dotnet/src/SemanticKernel/AI/OpenAI/HttpSchema/CompletionResponse.cs b/dotnet/src/SemanticKernel/AI/OpenAI/HttpSchema/CompletionResponse.cs new file mode 100644 index 000000000000..0c095baf0fae --- /dev/null +++ b/dotnet/src/SemanticKernel/AI/OpenAI/HttpSchema/CompletionResponse.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace Microsoft.SemanticKernel.AI.OpenAI.HttpSchema; + +/// +/// Completion Response +/// +public sealed class CompletionResponse +{ + /// + /// A choice of completion response + /// + public sealed class Choice + { + /// + /// The completed text from the completion request. + /// + [JsonPropertyName("text")] + public string Text { get; set; } = string.Empty; + + /// + /// Index of the choice + /// + [JsonPropertyName("index")] + public int Index { get; set; } = 0; + } + + /// + /// List of possible completions. + /// + [JsonPropertyName("choices")] + public IList Completions { get; set; } = new List(); +} diff --git a/dotnet/src/SemanticKernel/AI/OpenAI/HttpSchema/EmbeddingRequest.cs b/dotnet/src/SemanticKernel/AI/OpenAI/HttpSchema/EmbeddingRequest.cs new file mode 100644 index 000000000000..c2d94eabf7a7 --- /dev/null +++ b/dotnet/src/SemanticKernel/AI/OpenAI/HttpSchema/EmbeddingRequest.cs @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace Microsoft.SemanticKernel.AI.OpenAI.HttpSchema; + +/// +/// A request to create embedding vector representing input text +/// +public abstract class EmbeddingRequest +{ + /// + /// Input to embed + /// + [JsonPropertyName("input")] + public IList Input { get; set; } = new List(); +} + +/// +/// An OpenAI embedding request +/// +public sealed class OpenAIEmbeddingRequest : EmbeddingRequest +{ + /// + /// Embedding model ID + /// + [JsonPropertyName("model")] + public string Model { get; set; } = string.Empty; +} + +/// +/// An Azure OpenAI embedding request +/// +public sealed class AzureEmbeddingRequest : EmbeddingRequest +{ +} diff --git a/dotnet/src/SemanticKernel/AI/OpenAI/HttpSchema/EmbeddingResponse.cs b/dotnet/src/SemanticKernel/AI/OpenAI/HttpSchema/EmbeddingResponse.cs new file mode 100644 index 000000000000..0169a4069957 --- /dev/null +++ b/dotnet/src/SemanticKernel/AI/OpenAI/HttpSchema/EmbeddingResponse.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace Microsoft.SemanticKernel.AI.OpenAI.HttpSchema; + +/// +/// A response from an embedding request +/// +public sealed class EmbeddingResponse +{ + /// + /// A single embedding vector + /// + public sealed class EmbeddingResponseIndex + { + /// + /// The embedding vector + /// + [JsonPropertyName("embedding")] + public IList Values { get; set; } = new List(); + + /// + /// Index of the embedding vector + /// + [JsonPropertyName("index")] + public int Index { get; set; } + } + + /// + /// A list of embeddings + /// + [JsonPropertyName("data")] + public IList Embeddings { get; set; } = new List(); +} diff --git a/dotnet/src/SemanticKernel/AI/OpenAI/Services/AzureOpenAIConfig.cs b/dotnet/src/SemanticKernel/AI/OpenAI/Services/AzureOpenAIConfig.cs new file mode 100644 index 000000000000..3e4856732e8c --- /dev/null +++ b/dotnet/src/SemanticKernel/AI/OpenAI/Services/AzureOpenAIConfig.cs @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.SemanticKernel.Diagnostics; + +namespace Microsoft.SemanticKernel.AI.OpenAI.Services; + +/// +/// Azure OpenAI configuration. +/// TODO: support for AAD auth. +/// +public sealed class AzureOpenAIConfig +{ + /// + /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource + /// + public string DeploymentName { get; set; } + + /// + /// Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart + /// + public string Endpoint { get; set; } + + /// + /// Azure OpenAI API key, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart + /// + public string APIKey { get; set; } + + /// + /// Azure OpenAI API version, see https://learn.microsoft.com/azure/cognitive-services/openai/reference + /// + public string APIVersion { get; set; } + + /// + /// Creates a new AzureOpenAIConfig with supplied values. + /// + /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource + /// Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart + /// Azure OpenAI API key, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart + /// Azure OpenAI API version, see https://learn.microsoft.com/azure/cognitive-services/openai/reference + public AzureOpenAIConfig(string deploymentName, string endpoint, string apiKey, string apiVersion) + { + Verify.NotEmpty(deploymentName, "The deployment name is empty"); + Verify.NotEmpty(endpoint, "The endpoint is empty"); + Verify.StartsWith(endpoint, "https://", "The endpoint URL must start with https://"); + Verify.NotEmpty(apiKey, "The API key is empty"); + + this.DeploymentName = deploymentName; + this.Endpoint = endpoint; + this.APIKey = apiKey; + this.APIVersion = apiVersion; + } +} diff --git a/dotnet/src/SemanticKernel/AI/OpenAI/Services/AzureTextCompletion.cs b/dotnet/src/SemanticKernel/AI/OpenAI/Services/AzureTextCompletion.cs new file mode 100644 index 000000000000..38e8055a47c4 --- /dev/null +++ b/dotnet/src/SemanticKernel/AI/OpenAI/Services/AzureTextCompletion.cs @@ -0,0 +1,83 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel.AI.OpenAI.Clients; +using Microsoft.SemanticKernel.AI.OpenAI.HttpSchema; +using Microsoft.SemanticKernel.Diagnostics; +using Microsoft.SemanticKernel.Text; + +namespace Microsoft.SemanticKernel.AI.OpenAI.Services; + +/// +/// Azure OpenAI text completion client. +/// +public sealed class AzureTextCompletion : AzureOpenAIClientAbstract, ITextCompletionClient +{ + /// + /// Creates a new AzureTextCompletion client instance + /// + /// Azure OpenAI model ID or deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource + /// Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart + /// Azure OpenAI API key, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart + /// Azure OpenAI API version, see https://learn.microsoft.com/azure/cognitive-services/openai/reference + /// Application logger + public AzureTextCompletion(string modelId, string endpoint, string apiKey, string apiVersion, ILogger? log = null) + : base(log) + { + Verify.NotEmpty(modelId, "The ID cannot be empty, you must provide a Model ID or a Deployment name."); + this._modelId = modelId; + + Verify.NotEmpty(endpoint, "The Azure endpoint cannot be empty"); + Verify.StartsWith(endpoint, "https://", "The Azure OpenAI endpoint must start with 'https://'"); + this.Endpoint = endpoint.TrimEnd('/'); + + Verify.NotEmpty(apiKey, "The Azure API key cannot be empty"); + this.HTTPClient.DefaultRequestHeaders.Add("api-key", apiKey); + + this.AzureOpenAIApiVersion = apiVersion; + } + + /// + /// Creates a completion for the provided prompt and parameters + /// + /// Text to complete + /// Request settings for the completion API + /// The completed text. + /// AIException thrown during the request + public async Task CompleteAsync(string text, CompleteRequestSettings requestSettings) + { + Verify.NotNull(requestSettings, "Completion settings cannot be empty"); + + var deploymentName = await this.GetDeploymentNameAsync(this._modelId); + var url = $"{this.Endpoint}/openai/deployments/{deploymentName}/completions?api-version={this.AzureOpenAIApiVersion}"; + + this.Log.LogDebug("Sending Azure OpenAI completion request to {0}", url); + + if (requestSettings.MaxTokens < 1) + { + throw new AIException( + AIException.ErrorCodes.InvalidRequest, + $"MaxTokens {requestSettings.MaxTokens} is not valid, the value must be greater than zero"); + } + + var requestBody = Json.Serialize(new AzureCompletionRequest + { + Prompt = text, + Temperature = requestSettings.Temperature, + TopP = requestSettings.TopP, + PresencePenalty = requestSettings.PresencePenalty, + FrequencyPenalty = requestSettings.FrequencyPenalty, + MaxTokens = requestSettings.MaxTokens, + Stop = requestSettings.StopSequences is { Count: > 0 } ? requestSettings.StopSequences : null, + }); + + return await this.ExecuteCompleteRequestAsync(url, requestBody); + } + + #region private ================================================================================ + + private readonly string _modelId; + + #endregion +} diff --git a/dotnet/src/SemanticKernel/AI/OpenAI/Services/AzureTextEmbeddings.cs b/dotnet/src/SemanticKernel/AI/OpenAI/Services/AzureTextEmbeddings.cs new file mode 100644 index 000000000000..ae201187ff04 --- /dev/null +++ b/dotnet/src/SemanticKernel/AI/OpenAI/Services/AzureTextEmbeddings.cs @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel.AI.Embeddings; +using Microsoft.SemanticKernel.AI.OpenAI.Clients; +using Microsoft.SemanticKernel.AI.OpenAI.HttpSchema; +using Microsoft.SemanticKernel.Diagnostics; +using Microsoft.SemanticKernel.Text; + +namespace Microsoft.SemanticKernel.AI.OpenAI.Services; + +/// +/// Azure OpenAI text embedding service. +/// +public sealed class AzureTextEmbeddings : AzureOpenAIClientAbstract, IEmbeddingGenerator +{ + private readonly string _modelId; + + /// + /// Creates a new AzureTextEmbeddings client instance + /// + /// Azure OpenAI model ID or deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource + /// Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart + /// Azure OpenAI API key, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart + /// Azure OpenAI API version, see https://learn.microsoft.com/azure/cognitive-services/openai/reference + /// Application logger + public AzureTextEmbeddings(string modelId, string endpoint, string apiKey, string apiVersion, ILogger? log = null) + : base(log) + { + Verify.NotEmpty(modelId, "The ID cannot be empty, you must provide a Model ID or a Deployment name."); + this._modelId = modelId; + + Verify.NotEmpty(endpoint, "The Azure endpoint cannot be empty"); + Verify.StartsWith(endpoint, "https://", "The Azure OpenAI endpoint must start with 'https://'"); + this.Endpoint = endpoint.TrimEnd('/'); + + Verify.NotEmpty(apiKey, "The Azure API key cannot be empty"); + this.HTTPClient.DefaultRequestHeaders.Add("api-key", apiKey); + + this.AzureOpenAIApiVersion = apiVersion; + } + + /// + public async Task>> GenerateEmbeddingsAsync(IList data) + { + var deploymentName = await this.GetDeploymentNameAsync(this._modelId); + var url = $"{this.Endpoint}/openai/deployments/{deploymentName}/embeddings?api-version={this.AzureOpenAIApiVersion}"; + var requestBody = Json.Serialize(new AzureEmbeddingRequest { Input = data }); + + return await this.ExecuteEmbeddingRequestAsync(url, requestBody); + } +} diff --git a/dotnet/src/SemanticKernel/AI/OpenAI/Services/OpenAIConfig.cs b/dotnet/src/SemanticKernel/AI/OpenAI/Services/OpenAIConfig.cs new file mode 100644 index 000000000000..c25270531e12 --- /dev/null +++ b/dotnet/src/SemanticKernel/AI/OpenAI/Services/OpenAIConfig.cs @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.SemanticKernel.Diagnostics; + +namespace Microsoft.SemanticKernel.AI.OpenAI.Services; + +/// +/// OpenAI configuration. +/// TODO: allow overriding endpoint. +/// +public sealed class OpenAIConfig +{ + /// + /// OpenAI model name, see https://platform.openai.com/docs/models + /// + public string ModelId { get; } + + /// + /// OpenAI API key, see https://platform.openai.com/account/api-keys + /// + public string APIKey { get; } + + /// + /// OpenAI organization id. This is usually optional unless your account belongs to multiple organizations. + /// + public string? OrgId { get; } + + /// + /// Creates a new OpenAIConfig with supplied values. + /// + /// OpenAI model name, see https://platform.openai.com/docs/models + /// OpenAI API key, see https://platform.openai.com/account/api-keys + /// OpenAI organization id. This is usually optional unless your account belongs to multiple organizations. + public OpenAIConfig(string modelId, string apiKey, string? orgId) + { + Verify.NotEmpty(modelId, "The model ID is empty"); + Verify.NotEmpty(apiKey, "The API key is empty"); + + this.ModelId = modelId; + this.APIKey = apiKey; + this.OrgId = orgId; + } +} diff --git a/dotnet/src/SemanticKernel/AI/OpenAI/Services/OpenAITextCompletion.cs b/dotnet/src/SemanticKernel/AI/OpenAI/Services/OpenAITextCompletion.cs new file mode 100644 index 000000000000..00f6553f7ef2 --- /dev/null +++ b/dotnet/src/SemanticKernel/AI/OpenAI/Services/OpenAITextCompletion.cs @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Net.Http.Headers; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel.AI.OpenAI.Clients; +using Microsoft.SemanticKernel.AI.OpenAI.HttpSchema; +using Microsoft.SemanticKernel.Diagnostics; +using Microsoft.SemanticKernel.Text; + +namespace Microsoft.SemanticKernel.AI.OpenAI.Services; + +/// +/// OpenAI text completion service. +/// +public sealed class OpenAITextCompletion : OpenAIClientAbstract, ITextCompletionClient +{ + // 3P OpenAI REST API endpoint + private const string OpenaiEndpoint = "https://api.openai.com/v1"; + + private readonly string _modelId; + + /// + /// Creates a new OpenAITextCompletion with supplied values. + /// + /// OpenAI model name, see https://platform.openai.com/docs/models + /// OpenAI API key, see https://platform.openai.com/account/api-keys + /// OpenAI organization id. This is usually optional unless your account belongs to multiple organizations. + /// Logger + public OpenAITextCompletion(string modelId, string apiKey, string? organization = null, ILogger? log = null) : + base(log) + { + Verify.NotEmpty(modelId, "The OpenAI model ID cannot be empty"); + this._modelId = modelId; + + Verify.NotEmpty(apiKey, "The OpenAI API key cannot be empty"); + this.HTTPClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiKey); + + if (!string.IsNullOrEmpty(organization)) + { + this.HTTPClient.DefaultRequestHeaders.Add("OpenAI-Organization", organization); + } + } + + /// + /// Creates a new completion for the prompt and settings. + /// + /// The prompt to complete. + /// Request settings for the completion API + /// The completed text + /// AIException thrown during the request + public async Task CompleteAsync(string text, CompleteRequestSettings requestSettings) + { + Verify.NotNull(requestSettings, "Completion settings cannot be empty"); + + var url = $"{OpenaiEndpoint}/engines/{this._modelId}/completions"; + this.Log.LogDebug("Sending OpenAI completion request to {0}", url); + + if (requestSettings.MaxTokens < 1) + { + throw new AIException( + AIException.ErrorCodes.InvalidRequest, + $"MaxTokens {requestSettings.MaxTokens} is not valid, the value must be greater than zero"); + } + + var requestBody = Json.Serialize(new OpenAICompletionRequest + { + Prompt = text, + Temperature = requestSettings.Temperature, + TopP = requestSettings.TopP, + PresencePenalty = requestSettings.PresencePenalty, + FrequencyPenalty = requestSettings.FrequencyPenalty, + MaxTokens = requestSettings.MaxTokens, + Stop = requestSettings.StopSequences is { Count: > 0 } ? requestSettings.StopSequences : null, + }); + + return await this.ExecuteCompleteRequestAsync(url, requestBody); + } +} diff --git a/dotnet/src/SemanticKernel/AI/OpenAI/Services/OpenAITextEmbeddings.cs b/dotnet/src/SemanticKernel/AI/OpenAI/Services/OpenAITextEmbeddings.cs new file mode 100644 index 000000000000..6506f4710620 --- /dev/null +++ b/dotnet/src/SemanticKernel/AI/OpenAI/Services/OpenAITextEmbeddings.cs @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Net.Http.Headers; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel.AI.Embeddings; +using Microsoft.SemanticKernel.AI.OpenAI.Clients; +using Microsoft.SemanticKernel.AI.OpenAI.HttpSchema; +using Microsoft.SemanticKernel.Diagnostics; +using Microsoft.SemanticKernel.Text; + +namespace Microsoft.SemanticKernel.AI.OpenAI.Services; + +/// +/// Client to OpenAI.com embedding endpoint, used to generate embeddings. +/// +public sealed class OpenAITextEmbeddings : OpenAIClientAbstract, IEmbeddingGenerator +{ + // 3P OpenAI REST API endpoint + private const string OpenaiEndpoint = "https://api.openai.com/v1"; + private const string OpenaiEmbeddingEndpoint = $"{OpenaiEndpoint}/embeddings"; + + private readonly string _modelId; + + /// + /// Create an instance of OpenAI embeddings endpoint client + /// + /// OpenAI embedding model name + /// OpenAI API Key + /// Optional OpenAI organization ID, usually required only if your account belongs to multiple organizations + /// Application logger + public OpenAITextEmbeddings(string modelId, string apiKey, string? organization = null, ILogger? log = null) + : base(log) + { + Verify.NotEmpty(modelId, "The OpenAI model ID cannot be empty"); + this._modelId = modelId; + + Verify.NotEmpty(apiKey, "The OpenAI API key cannot be empty"); + this.HTTPClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiKey); + + if (!string.IsNullOrEmpty(organization)) + { + this.HTTPClient.DefaultRequestHeaders.Add("OpenAI-Organization", organization); + } + } + + /// + public async Task>> GenerateEmbeddingsAsync(IList data) + { + var requestBody = Json.Serialize(new OpenAIEmbeddingRequest { Model = this._modelId, Input = data, }); + + return await this.ExecuteEmbeddingRequestAsync(OpenaiEmbeddingEndpoint, requestBody); + } +} diff --git a/dotnet/src/SemanticKernel/Configuration/BackendConfig.cs b/dotnet/src/SemanticKernel/Configuration/BackendConfig.cs new file mode 100644 index 000000000000..5fec063146e7 --- /dev/null +++ b/dotnet/src/SemanticKernel/Configuration/BackendConfig.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.SemanticKernel.AI.OpenAI.Services; + +namespace Microsoft.SemanticKernel.Configuration; + +/// +/// Backend configuration. +/// +public sealed class BackendConfig +{ + /// + /// Backend type. + /// + public BackendTypes BackendType { get; set; } = BackendTypes.Unknown; + + /// + /// Azure OpenAI configuration. + /// + public AzureOpenAIConfig? AzureOpenAI { get; set; } = null; + + /// + /// OpenAI configuration. + /// + public OpenAIConfig? OpenAI { get; set; } = null; +} diff --git a/dotnet/src/SemanticKernel/Configuration/BackendTypes.cs b/dotnet/src/SemanticKernel/Configuration/BackendTypes.cs new file mode 100644 index 000000000000..871d6e8e8588 --- /dev/null +++ b/dotnet/src/SemanticKernel/Configuration/BackendTypes.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft. All rights reserved. + +namespace Microsoft.SemanticKernel.Configuration; + +/// +/// Backend types. +/// +public enum BackendTypes +{ + /// + /// Unknown. + /// + Unknown = -1, + + /// + /// Azure OpenAI. + /// + AzureOpenAI = 0, + + /// + /// OpenAI. + /// + OpenAI = 1, +} diff --git a/dotnet/src/SemanticKernel/Configuration/KernelConfig.cs b/dotnet/src/SemanticKernel/Configuration/KernelConfig.cs new file mode 100644 index 000000000000..387726fd31be --- /dev/null +++ b/dotnet/src/SemanticKernel/Configuration/KernelConfig.cs @@ -0,0 +1,418 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.SemanticKernel.AI.OpenAI.Services; +using Microsoft.SemanticKernel.Diagnostics; +using Microsoft.SemanticKernel.Reliability; + +namespace Microsoft.SemanticKernel.Configuration; + +/// +/// Semantic kernel configuration. +/// +public sealed class KernelConfig +{ + /// + /// Global retry logic used for all the backends + /// + public IRetryMechanism RetryMechanism { get => this._retryMechanism; } + + /// + /// Adds an Azure OpenAI backend to the list. + /// See https://learn.microsoft.com/azure/cognitive-services/openai for service details. + /// + /// An identifier used to map semantic functions to backend, + /// decoupling prompts configurations from the actual model used + /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource + /// Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart + /// Azure OpenAI API key, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart + /// Azure OpenAI API version, see https://learn.microsoft.com/azure/cognitive-services/openai/reference + /// Whether to overwrite an existing configuration if the same name exists + /// Self instance + public KernelConfig AddAzureOpenAICompletionBackend( + string label, string deploymentName, string endpoint, string apiKey, string apiVersion = "2022-12-01", bool overwrite = false) + { + Verify.NotEmpty(label, "The backend name is empty"); + + if (!overwrite && this.CompletionBackends.ContainsKey(label)) + { + throw new KernelException( + KernelException.ErrorCodes.InvalidBackendConfiguration, + $"The completion backend cannot be added twice: {label}"); + } + + this.CompletionBackends[label] = new BackendConfig + { + BackendType = BackendTypes.AzureOpenAI, AzureOpenAI = new AzureOpenAIConfig(deploymentName, endpoint, apiKey, apiVersion) + }; + + if (this.CompletionBackends.Count == 1) + { + this._defaultCompletionBackend = label; + } + + return this; + } + + /// + /// Adds the OpenAI completion backend to the list. + /// See https://platform.openai.com/docs for service details. + /// + /// An identifier used to map semantic functions to backend, + /// decoupling prompts configurations from the actual model used + /// OpenAI model name, see https://platform.openai.com/docs/models + /// OpenAI API key, see https://platform.openai.com/account/api-keys + /// OpenAI organization id. This is usually optional unless your account belongs to multiple organizations. + /// Whether to overwrite an existing configuration if the same name exists + /// Self instance + public KernelConfig AddOpenAICompletionBackend( + string label, string modelId, string apiKey, string? orgId = null, bool overwrite = false) + { + Verify.NotEmpty(label, "The backend name is empty"); + + if (!overwrite && this.CompletionBackends.ContainsKey(label)) + { + throw new KernelException( + KernelException.ErrorCodes.InvalidBackendConfiguration, + $"The completion backend cannot be added twice: {label}"); + } + + this.CompletionBackends[label] = new BackendConfig + { + BackendType = BackendTypes.OpenAI, + OpenAI = new OpenAIConfig(modelId, apiKey, orgId) + }; + + if (this.CompletionBackends.Count == 1) + { + this._defaultCompletionBackend = label; + } + + return this; + } + + /// + /// Adds an Azure OpenAI embeddings backend to the list. + /// See https://learn.microsoft.com/azure/cognitive-services/openai for service details. + /// + /// An identifier used to map semantic functions to backend, + /// decoupling prompts configurations from the actual model used + /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource + /// Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart + /// Azure OpenAI API key, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart + /// Azure OpenAI API version, see https://learn.microsoft.com/azure/cognitive-services/openai/reference + /// Whether to overwrite an existing configuration if the same name exists + /// Self instance + public KernelConfig AddAzureOpenAIEmbeddingsBackend( + string label, string deploymentName, string endpoint, string apiKey, string apiVersion = "2022-12-01", bool overwrite = false) + { + Verify.NotEmpty(label, "The backend name is empty"); + + if (!overwrite && this.EmbeddingsBackends.ContainsKey(label)) + { + throw new KernelException( + KernelException.ErrorCodes.InvalidBackendConfiguration, + $"The embeddings backend cannot be added twice: {label}"); + } + + this.EmbeddingsBackends[label] = new BackendConfig + { + BackendType = BackendTypes.AzureOpenAI, + AzureOpenAI = new AzureOpenAIConfig(deploymentName, endpoint, apiKey, apiVersion) + }; + + if (this.EmbeddingsBackends.Count == 1) + { + this._defaultEmbeddingsBackend = label; + } + + return this; + } + + /// + /// Adds the OpenAI embeddings backend to the list. + /// See https://platform.openai.com/docs for service details. + /// + /// An identifier used to map semantic functions to backend, + /// decoupling prompts configurations from the actual model used + /// OpenAI model name, see https://platform.openai.com/docs/models + /// OpenAI API key, see https://platform.openai.com/account/api-keys + /// OpenAI organization id. This is usually optional unless your account belongs to multiple organizations. + /// Whether to overwrite an existing configuration if the same name exists + /// Self instance + public KernelConfig AddOpenAIEmbeddingsBackend( + string label, string modelId, string apiKey, string? orgId = null, bool overwrite = false) + { + Verify.NotEmpty(label, "The backend name is empty"); + + if (!overwrite && this.EmbeddingsBackends.ContainsKey(label)) + { + throw new KernelException( + KernelException.ErrorCodes.InvalidBackendConfiguration, + $"The embeddings backend cannot be added twice: {label}"); + } + + this.EmbeddingsBackends[label] = new BackendConfig + { + BackendType = BackendTypes.OpenAI, + OpenAI = new OpenAIConfig(modelId, apiKey, orgId) + }; + + if (this.EmbeddingsBackends.Count == 1) + { + this._defaultEmbeddingsBackend = label; + } + + return this; + } + + /// + /// Check whether a given completion backend is in the configuration. + /// + /// Name of completion backend to look for. + /// Optional condition that must be met for a backend to be deemed present. + /// true when a completion backend matching the giving label is present, false otherwise. + public bool HasCompletionBackend(string label, Func? condition = null) + { + return condition == null + ? this.CompletionBackends.ContainsKey(label) + : this.CompletionBackends.Any(x => x.Key == label && condition(x.Value)); + } + + /// + /// Check whether a given embeddings backend is in the configuration. + /// + /// Name of embeddings backend to look for. + /// Optional condition that must be met for a backend to be deemed present. + /// true when an embeddings backend matching the giving label is present, false otherwise. + public bool HasEmbeddingsBackend(string label, Func? condition = null) + { + return condition == null + ? this.EmbeddingsBackends.ContainsKey(label) + : this.EmbeddingsBackends.Any(x => x.Key == label && condition(x.Value)); + } + + /// + /// Set the retry mechanism to use for the kernel. + /// + /// Retry mechanism to use. + /// The updated kernel configuration. + public KernelConfig SetRetryMechanism(IRetryMechanism? retryMechanism = null) + { + this._retryMechanism = retryMechanism ?? new PassThroughWithoutRetry(); + return this; + } + + /// + /// Set the default completion backend to use for the kernel. + /// + /// Label of completion backend to use. + /// The updated kernel configuration. + /// Thrown if the requested backend doesn't exist. + public KernelConfig SetDefaultCompletionBackend(string label) + { + if (!this.CompletionBackends.ContainsKey(label)) + { + throw new KernelException( + KernelException.ErrorCodes.BackendNotFound, + $"The completion backend doesn't exist: {label}"); + } + + this._defaultCompletionBackend = label; + return this; + } + + /// + /// Default completion backend. + /// + public string? DefaultCompletionBackend => this._defaultCompletionBackend; + + /// + /// Set the default embeddings backend to use for the kernel. + /// + /// Label of embeddings backend to use. + /// The updated kernel configuration. + /// Thrown if the requested backend doesn't exist. + public KernelConfig SetDefaultEmbeddingsBackend(string label) + { + if (!this.EmbeddingsBackends.ContainsKey(label)) + { + throw new KernelException( + KernelException.ErrorCodes.BackendNotFound, + $"The embeddings backend doesn't exist: {label}"); + } + + this._defaultEmbeddingsBackend = label; + return this; + } + + /// + /// Default embeddings backend. + /// + public string? DefaultEmbeddingsBackend => this._defaultEmbeddingsBackend; + + /// + /// Get the backend configuration matching the given completion backend label. + /// + /// Label of backend desired. + /// The backend configuration matching the given label. + /// Thrown when no suitable backend is found. + public BackendConfig GetCompletionBackend(string? label) + { + if (string.IsNullOrEmpty(label)) + { + if (this._defaultCompletionBackend == null) + { + throw new KernelException( + KernelException.ErrorCodes.BackendNotFound, + $"Completion backend not found: {label}. No default backend available."); + } + + return this.CompletionBackends[this._defaultCompletionBackend]; + } + + if (this.CompletionBackends.TryGetValue(label, out BackendConfig value)) + { + return value; + } + + if (this._defaultCompletionBackend != null) + { + return this.CompletionBackends[this._defaultCompletionBackend]; + } + + throw new KernelException( + KernelException.ErrorCodes.BackendNotFound, + $"Completion backend not found: {label}. No default backend available."); + } + + /// + /// Get the backend configuration matching the given embeddings backend label. + /// + /// Label of backend desired. + /// The backend configuration matching the given label. + /// Thrown when no suitable backend is found. + public BackendConfig GetEmbeddingsBackend(string? label) + { + if (string.IsNullOrEmpty(label)) + { + if (this._defaultEmbeddingsBackend == null) + { + throw new KernelException( + KernelException.ErrorCodes.BackendNotFound, + $"Embeddings backend not found: {label}. No default backend available."); + } + + return this.EmbeddingsBackends[this._defaultEmbeddingsBackend]; + } + + if (this.EmbeddingsBackends.TryGetValue(label, out BackendConfig value)) + { + return value; + } + + if (this._defaultEmbeddingsBackend != null) + { + return this.EmbeddingsBackends[this._defaultEmbeddingsBackend]; + } + + throw new KernelException( + KernelException.ErrorCodes.BackendNotFound, + $"Embeddings backend not found: {label}. No default backend available."); + } + + /// + /// Get all completion backends. + /// + /// IEnumerable of all completion backends in the kernel configuration. + public IEnumerable GetAllCompletionBackends() + { + return this.CompletionBackends.Select(x => x.Value); + } + + /// + /// Get all embeddings backends. + /// + /// IEnumerable of all embeddings backends in the kernel configuration. + public IEnumerable GetAllEmbeddingsBackends() + { + return this.EmbeddingsBackends.Select(x => x.Value); + } + + /// + /// Remove the completion backend with the given label. + /// + /// Label of backend to remove. + /// The updated kernel configuration. + public KernelConfig RemoveCompletionBackend(string label) + { + this.CompletionBackends.Remove(label); + if (this._defaultCompletionBackend == label) + { + this._defaultCompletionBackend = this.CompletionBackends.Keys.FirstOrDefault(); + } + + return this; + } + + /// + /// Remove the embeddings backend with the given label. + /// + /// Label of backend to remove. + /// The updated kernel configuration. + public KernelConfig RemoveEmbeddingsBackend(string label) + { + this.EmbeddingsBackends.Remove(label); + if (this._defaultEmbeddingsBackend == label) + { + this._defaultEmbeddingsBackend = this.EmbeddingsBackends.Keys.FirstOrDefault(); + } + + return this; + } + + /// + /// Remove all completion backends. + /// + /// The updated kernel configuration. + public KernelConfig RemoveAllCompletionBackends() + { + this.CompletionBackends.Clear(); + this._defaultCompletionBackend = null; + return this; + } + + /// + /// Remove all embeddings backends. + /// + /// The updated kernel configuration. + public KernelConfig RemoveAllEmbeddingBackends() + { + this.EmbeddingsBackends.Clear(); + this._defaultEmbeddingsBackend = null; + return this; + } + + /// + /// Remove all backends. + /// + /// The updated kernel configuration. + public KernelConfig RemoveAllBackends() + { + this.RemoveAllCompletionBackends(); + this.RemoveAllEmbeddingBackends(); + return this; + } + + #region private + + private Dictionary CompletionBackends { get; set; } = new(); + private Dictionary EmbeddingsBackends { get; set; } = new(); + private string? _defaultCompletionBackend; + private string? _defaultEmbeddingsBackend; + private IRetryMechanism _retryMechanism = new PassThroughWithoutRetry(); + + #endregion +} diff --git a/dotnet/src/SemanticKernel/CoreSkills/FileIOSkill.cs b/dotnet/src/SemanticKernel/CoreSkills/FileIOSkill.cs new file mode 100644 index 000000000000..bcd0a68f00ce --- /dev/null +++ b/dotnet/src/SemanticKernel/CoreSkills/FileIOSkill.cs @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.IO; +using System.Threading.Tasks; +using Microsoft.SemanticKernel.Orchestration; +using Microsoft.SemanticKernel.SkillDefinition; + +namespace Microsoft.SemanticKernel.CoreSkills; + +/// +/// Read and write from a file. +/// +/// +/// Usage: kernel.ImportSkill("file", new FileIOSkill()); +/// Examples: +/// {{file.readAsync $path }} => "hello world" +/// {{file.writeAsync}} +/// +public class FileIOSkill +{ + /// + /// Read a file + /// + /// + /// {{file.readAsync $path }} => "hello world" + /// + /// Source file + /// File content + [SKFunction("Read a file")] + [SKFunctionInput(Description = "Source file")] + public Task ReadAsync(string path) + { + return File.ReadAllTextAsync(path); + } + + /// + /// Write a file + /// + /// + /// {{file.writeAsync}} + /// + /// + /// Contains the 'path' for the Destination file and 'content' of the file to write. + /// + /// An awaitable task + [SKFunction("Write a file")] + [SKFunctionContextParameter(Name = "path", Description = "Destination file")] + [SKFunctionContextParameter(Name = "content", Description = "File content")] + public Task WriteAsync(SKContext context) + { + return File.WriteAllTextAsync(context["path"], context["content"]); + } +} diff --git a/dotnet/src/SemanticKernel/CoreSkills/HttpSkill.cs b/dotnet/src/SemanticKernel/CoreSkills/HttpSkill.cs new file mode 100644 index 000000000000..1911cd57baf9 --- /dev/null +++ b/dotnet/src/SemanticKernel/CoreSkills/HttpSkill.cs @@ -0,0 +1,124 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.SemanticKernel.Orchestration; +using Microsoft.SemanticKernel.SkillDefinition; + +namespace Microsoft.SemanticKernel.CoreSkills; + +/// +/// A skill that provides HTTP functionality. +/// +/// +/// Usage: kernel.ImportSkill("http", new HttpSkill()); +/// Examples: +/// SKContext["url"] = "https://www.bing.com" +/// {{http.getAsync $url}} +/// {{http.postAsync $url}} +/// {{http.putAsync $url}} +/// {{http.deleteAsync $url}} +/// +[System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1054:URI-like parameters should not be strings", + Justification = "Semantic Kernel operates on strings")] +public class HttpSkill : IDisposable +{ + private readonly HttpClientHandler? _httpClientHandler; + private readonly HttpClient _client; + + /// + /// Initializes a new instance of the class. + /// + public HttpSkill() + { + this._httpClientHandler = new() { CheckCertificateRevocationList = true }; + this._client = new HttpClient(this._httpClientHandler); + } + + /// + /// Initializes a new instance of the class. + /// + /// The HTTP client to use. + public HttpSkill(HttpClient client) + { + this._httpClientHandler = null; + this._client = client; + } + + /// + /// Sends an HTTP GET request to the specified URI and returns the response body as a string. + /// + /// URI of the request + /// The response body as a string. + [SKFunction("Makes a GET request to a uri")] + public async Task GetAsync(string uri) + { + var response = await this._client.GetAsync(uri); + var content = response.Content; + return await content.ReadAsStringAsync(); + } + + /// + /// Sends an HTTP POST request to the specified URI and returns the response body as a string. + /// + /// URI of the request + /// Contains the body of the request + /// The response body as a string. + [SKFunction("Makes a POST request to a uri")] + [SKFunctionContextParameter(Name = "body", Description = "The body of the request")] + public async Task PostAsync(string uri, SKContext context) + { + using var httpContent = new StringContent(context["body"]); + var response = await this._client.PostAsync(uri, httpContent); + var content = response.Content; + return await content.ReadAsStringAsync(); + } + + /// + /// Sends an HTTP PUT request to the specified URI and returns the response body as a string. + /// + /// URI of the request + /// Contains the body of the request + /// The response body as a string. + [SKFunction("Makes a PUT request to a uri")] + [SKFunctionContextParameter(Name = "body", Description = "The body of the request")] + public async Task PutAsync(string uri, SKContext context) + { + using var httpContent = new StringContent(context["body"]); + var response = await this._client.PutAsync(uri, httpContent); + var content = response.Content; + return await content.ReadAsStringAsync(); + } + + /// + /// Sends an HTTP DELETE request to the specified URI and returns the response body as a string. + /// + /// URI of the request + /// The response body as a string. + [SKFunction("Makes a DELETE request to a uri")] + public async Task DeleteAsync(string uri) + { + var response = await this._client.DeleteAsync(uri); + var content = response.Content; + return await content.ReadAsStringAsync(); + } + + /// + /// Disposes resources + /// + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + this._httpClientHandler?.Dispose(); + this._client.Dispose(); + } + } +} diff --git a/dotnet/src/SemanticKernel/CoreSkills/PlannerSkill.cs b/dotnet/src/SemanticKernel/CoreSkills/PlannerSkill.cs new file mode 100644 index 000000000000..10a9842b6050 --- /dev/null +++ b/dotnet/src/SemanticKernel/CoreSkills/PlannerSkill.cs @@ -0,0 +1,278 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel.Diagnostics; +using Microsoft.SemanticKernel.KernelExtensions; +using Microsoft.SemanticKernel.Orchestration; +using Microsoft.SemanticKernel.Orchestration.Extensions; +using Microsoft.SemanticKernel.Planning; +using Microsoft.SemanticKernel.SkillDefinition; + +namespace Microsoft.SemanticKernel.CoreSkills; + +/// +/// Semantic skill that creates and executes plans. +/// +/// Usage: +/// var kernel = SemanticKernel.Build(ConsoleLogger.Log); +/// kernel.ImportSkill("planner", new PlannerSkill(kernel)); +/// +/// +public class PlannerSkill +{ + /// + /// The name to use when creating semantic functions that are restricted from the PlannerSkill plans + /// + private const string RestrictedSkillName = "PlannerSkill_Excluded"; + + /// + /// the skills to exclude from the skill collection + /// + private static readonly List s_excludedSkills = new() { RestrictedSkillName }; + + /// + /// the functions to exclude from the skill collection + /// + private static readonly List s_excludedFunctions = new() { "CreatePlan", "ExecutePlan" }; + + /// + /// the function flow runner, which executes plans that leverage functions + /// + private readonly FunctionFlowRunner _functionFlowRunner; + + /// + /// the bucket semantic function, which takes a list of items and buckets them into a number of buckets + /// + private readonly ISKFunction _bucketFunction; + + /// + /// the function flow semantic function, which takes a goal and creates an xml plan that can be executed + /// + private readonly ISKFunction _functionFlowFunction; + + /// + /// Initializes a new instance of the class. + /// + /// The kernel to use + /// The maximum number of tokens to use for the semantic functions + public PlannerSkill(IKernel kernel, int maxTokens = 1024) + { + this._functionFlowRunner = new(kernel); + + this._bucketFunction = kernel.CreateSemanticFunction( + promptTemplate: SemanticFunctionConstants.BucketFunctionDefinition, + skillName: RestrictedSkillName, + maxTokens: maxTokens, + temperature: 0.0); + + this._functionFlowFunction = kernel.CreateSemanticFunction( + promptTemplate: SemanticFunctionConstants.FunctionFlowFunctionDefinition, + skillName: RestrictedSkillName, + description: "Given a request or command or goal generate a step by step plan to " + + "fulfill the request using functions. This ability is also known as decision making and function flow", + maxTokens: maxTokens, + temperature: 0.0, + stopSequences: new[] { "", " + +[AVAILABLE FUNCTIONS] + Summarize : + description: summarize input text + inputs: + - $input: the text to summarize + TranslateTo: + description: translate the input to another language + inputs: + - $input: the text to translate + - $translate_to_language: the language to translate to + LookupContactEmail: + description: looks up the a contact and retrieves their email address + inputs: + - $input: the name to look up + EmailTo: + decription: email the input text to a recipient + inputs: + - $input: the text to email + - $recipient: the recipient's email address. Multiple addresses may be included if separated by ';'. +[END AVAILABLE FUNCTIONS] + + +Summarize an input, translate to french, and e-mail to John Doe + + + + + + + + +[AVAILABLE FUNCTIONS] + Summarize : + description: summarize input text + inputs: + - $input: the text to summarize + NovelOutline : + description: summarize input text + inputs: + - $input: the text to summarize + EmailTo: + decription: email the input text to a recipient + inputs: + - $input: the text to email + - $recipient: the recipient's email address. Multiple addresses may be included if separated by ';'. +[END AVAILABLE FUNCTIONS] + +Create an outline for a children's book with 3 chapters about a group of kids in a club and then summarize it. + + + + + +The following is an incorrect example, because it creates a new input value for both NovelOutline and EmailTo that is not directly sourced from the goal. + +[AVAILABLE FUNCTIONS] + NovelOutline : + description: summarize input text + inputs: + - $input: the text to summarize + EmailTo: + decription: email the input text to a recipient + inputs: + - $input: the text to email + - $recipient: the recipient's email address. Multiple addresses may be included if separated by ';'. +[END AVAILABLE FUNCTIONS] + +Create an outline for a children's book with 3 chapters about a group of kids in a club. + + + + + +End of examples. + +[AVAILABLE FUNCTIONS] +{{$available_functions}} +[END AVAILABLE FUNCTIONS] + +{{$input}} +"; + + internal const string BucketFunctionDefinition = + @"1. Given an output of a function, bucket the output into a list of results. + +Examples: +[CONTENT] +Result 1 +This is the first result. +Result 2 +This is the second result. It's doubled! +Result 3 +This is the third and final result. Truly astonishing. +[END CONTENT] + +EXPECTED BUCKETS: + +Result: +{""buckets"": [""Result 1 +This is the first result."", ""Result 2 +This is the second result. It's doubled!"", ""Result 3 +This is the third and final result. Truly astonishing.""]} + +End examples. + +[CONTENT] +{{$input}} +[END CONTENT] + +EXPECTED BUCKETS: {{$bucketCount}} + +Result: +"; + + internal const string ProblemSolverFunctionDefinition = + @"Create a plan as a list, step by step, to answer the request or goal given. +To create a plan, follow these steps: +1. Identify the request or goal to be achieved. +2. Break down the request into smaller tasks and steps. +3. If the goal has a ""use"" parameter, use those functions with the exact name given. +4. Use experience and logic to determine the steps and tasks needed. +5. Provide a detailed decision-making process for each step. +6. Avoid generic instructions and add as many details as possible. +7. Present the solution using precise XML syntax with ""solution"" and ""step"" tags. +8. Ensure that all tags are closed. +9. Append an ""END"" comment at the end of the plan. +10. If you don't know how to fulfill a request, use the ""noSolution"" tag. +11. Use a computer, browser, apps, and external services to fulfill the goal. +12. Ensure that all goals are fulfilled from a computer. + +Here some good examples: + + +what time is it? + + + + Get current location + Find the time zone for the location in the variables + Get the current time for the time zone in the variables + + + +what time is it? + + + + + Get the current time for time zone in the variables + + + +what time is it? + + + + + + Get the current time from the variables + + + +how long till Christmas? + + + + + + + Get the current date from the variables + Calculate days from ""current date"" to ""December 25"" + + + +Get user's location + + + + Search for the user location in variables + If the user location is unknown ask the user: What is your location? + + + +Get user's location + + + + + Get the location from the variables + If the user location is unknown ask the user to teach you how to find the value + + + +Find my time zone + + + + + Get the location from the variables + If the user location is unknown ask the user: What is your location? + Find the timezone for given location + If the user timezone is unknown ask the user to teach you how to find the value + + + +summarize last week emails + + + + Find the current time and date + Get all emails from given time to time minus 7 days + Summarize the email in variables + + + +Get the current date and time + + + + Find the current date and time + Get date and time from the variables + + + +Get the current date and time + + + + + + + + Get date and time from the variables + + + +how long until my wife's birthday? + + + + + + + + Search for wife's birthday in memory + If the previous step is empty ask the user: when is your wife's birthday? + + + +Search for wife's birthday in memory + + + + Find name of wife in variables + If the wife name is unknown ask the user + Search for wife's birthday in Facebook using the name in memory + Search for wife's birthday in Teams conversations filtering messages by name and using the name in memory + Search for wife's birthday in Emails filtering messages by name and using the name in memory + If the birthday cannot be found tell the user, ask the user to teach you how to find the value + + + +Search for gift ideas + + + + Find topics of interest from personal conversations + Find topics of interest from personal emails + Search Amazon for gifts including topics in the variables + + + +Count from 1 to 5 + + + + Create a counter variable in memory with value 1 + Show the value of the counter variable + If the counter variable is 5 stop + Increment the counter variable + + + +foo bar + + + + Sorry I don't know how to help with that + + +The following is an incorrect example, because the solution uses a skill not listed in the 'use' attribute. + + +do something + + + + + + +End of examples. + + +{{$SKILLS_MANUAL}} + + + +{{$INPUT}} + +"; + + internal const string SolveNextStepFunctionDefinition = + @"{{$INPUT}} + +Update the plan above: +* If there are steps in the solution, then: + ** use the variables to execute the first step + ** if the variables contains a result, replace it with the result of the first step, otherwise store the result in the variables + ** Remove the first step. +* Keep the XML syntax correct, with a new line after the goal. +* Emit only XML. +* If the list of steps is empty, answer the goal using information in the variables, putting the solution inside the solution tag. +* Append at the end. +END OF INSTRUCTIONS. + +Possible updated plan: +"; +} diff --git a/dotnet/src/SemanticKernel/CoreSkills/TextMemorySkill.cs b/dotnet/src/SemanticKernel/CoreSkills/TextMemorySkill.cs new file mode 100644 index 000000000000..cd343e33d38b --- /dev/null +++ b/dotnet/src/SemanticKernel/CoreSkills/TextMemorySkill.cs @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Globalization; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel.Diagnostics; +using Microsoft.SemanticKernel.Memory; +using Microsoft.SemanticKernel.Orchestration; +using Microsoft.SemanticKernel.SkillDefinition; + +namespace Microsoft.SemanticKernel.CoreSkills; + +/// +/// TextMemorySkill provides a skill to recall information from the long or short term memory. +/// +/// +/// Usage: kernel.ImportSkill("memory", new TextMemorySkill()); +/// Examples: +/// SKContext["input"] = "what is the capital of France?" +/// {{memory.recall $input }} => "Paris" +/// +public class TextMemorySkill +{ + /// + /// Name of the context parameter used to specify which memory collection to use. + /// + public const string CollectionParam = "collection"; + + /// + /// Name of the context parameter used to specify memory search relevance score. + /// + public const string RelevanceParam = "relevance"; + + private const string DefaultCollection = "generic"; + private const string DefaultRelevance = "0.75"; + + /// + /// Recall a fact from the long term memory + /// + /// + /// SKContext["input"] = "what is the capital of France?" + /// {{memory.recall $input }} => "Paris" + /// + /// The information to retrieve + /// Contains the 'collection' to search for information and 'relevance' score + [SKFunction("Recall a fact from the long term memory")] + [SKFunctionName("Recall")] + [SKFunctionInput(Description = "The information to retrieve")] + [SKFunctionContextParameter(Name = CollectionParam, Description = "Memories collection where to search for information", DefaultValue = DefaultCollection)] + [SKFunctionContextParameter(Name = RelevanceParam, Description = "The relevance score, from 0.0 to 1.0, where 1.0 means perfect match", + DefaultValue = DefaultRelevance)] + public async Task RecallAsync(string ask, SKContext context) + { + var collection = context.Variables.ContainsKey(CollectionParam) ? context[CollectionParam] : DefaultCollection; + Verify.NotEmpty(collection, "Memory collection not defined"); + + var relevance = context.Variables.ContainsKey(RelevanceParam) ? context[RelevanceParam] : DefaultRelevance; + if (string.IsNullOrWhiteSpace(relevance)) { relevance = DefaultRelevance; } + + context.Log.LogTrace("Searching memory for '{0}', collection '{1}', relevance '{2}'", ask, collection, relevance); + + // TODO: support locales, e.g. "0.7" and "0,7" must both work + MemoryQueryResult? memory = await context.Memory + .SearchAsync(collection, ask, limit: 1, minRelevanceScore: float.Parse(relevance, CultureInfo.InvariantCulture)) + .FirstOrDefaultAsync(); + + if (memory == null) + { + context.Log.LogWarning("Memory not found in collection: {0}", collection); + } + else + { + context.Log.LogTrace("Memory found (collection: {0})", collection); + } + + return memory != null ? memory.Text : string.Empty; + } +} diff --git a/dotnet/src/SemanticKernel/CoreSkills/TextSkill.cs b/dotnet/src/SemanticKernel/CoreSkills/TextSkill.cs new file mode 100644 index 000000000000..080a77f43294 --- /dev/null +++ b/dotnet/src/SemanticKernel/CoreSkills/TextSkill.cs @@ -0,0 +1,99 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.SemanticKernel.SkillDefinition; + +namespace Microsoft.SemanticKernel.CoreSkills; + +/// +/// TextSkill provides a set of functions to manipulate strings. +/// +/// +/// Usage: kernel.ImportSkill("text", new TextSkill()); +/// +/// Examples: +/// SKContext["input"] = " hello world " +/// {{text.trim $input}} => "hello world" +/// {{text.trimStart $input} => "hello world " +/// {{text.trimEnd $input} => " hello world" +/// SKContext["input"] = "hello world" +/// {{text.uppercase $input}} => "HELLO WORLD" +/// SKContext["input"] = "HELLO WORLD" +/// {{text.lowercase $input}} => "hello world" +/// +public class TextSkill +{ + /// + /// Trim whitespace from the start and end of a string. + /// + /// + /// SKContext["input"] = " hello world " + /// {{text.trim $input}} => "hello world" + /// + /// The string to trim. + /// The trimmed string. + [SKFunction("Trim whitespace from the start and end of a string.")] + public string Trim(string text) + { + return text.Trim(); + } + + /// + /// Trim whitespace from the start of a string. + /// + /// + /// SKContext["input"] = " hello world " + /// {{text.trimStart $input} => "hello world " + /// + /// The string to trim. + /// The trimmed string. + [SKFunction("Trim whitespace from the start of a string.")] + public string TrimStart(string text) + { + return text.TrimStart(); + } + + /// + /// Trim whitespace from the end of a string. + /// + /// + /// SKContext["input"] = " hello world " + /// {{text.trimEnd $input} => " hello world" + /// + /// The string to trim. + /// The trimmed string. + [SKFunction("Trim whitespace from the end of a string.")] + public string TrimEnd(string text) + { + return text.TrimEnd(); + } + + /// + /// Convert a string to uppercase. + /// + /// + /// SKContext["input"] = "hello world" + /// {{text.uppercase $input}} => "HELLO WORLD" + /// + /// The string to convert. + /// The converted string. + [SKFunction("Convert a string to uppercase.")] + public string Uppercase(string text) + { + return text.ToUpper(System.Globalization.CultureInfo.CurrentCulture); + } + + /// + /// Convert a string to lowercase. + /// + /// + /// SKContext["input"] = "HELLO WORLD" + /// {{text.lowercase $input}} => "hello world" + /// + /// The string to convert. + /// The converted string. + [SKFunction("Convert a string to lowercase.")] + public string Lowercase(string text) + { + return text.ToLower(System.Globalization.CultureInfo.CurrentCulture); + } +} diff --git a/dotnet/src/SemanticKernel/CoreSkills/TimeSkill.cs b/dotnet/src/SemanticKernel/CoreSkills/TimeSkill.cs new file mode 100644 index 000000000000..35ce186d061d --- /dev/null +++ b/dotnet/src/SemanticKernel/CoreSkills/TimeSkill.cs @@ -0,0 +1,254 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Globalization; +using Microsoft.SemanticKernel.SkillDefinition; + +namespace Microsoft.SemanticKernel.CoreSkills; + +/// +/// TimeSkill provides a set of functions to get the current time and date. +/// +/// +/// Usage: kernel.ImportSkill("time", new TimeSkill()); +/// Examples: +/// {{time.date}} => Sunday, 12 January, 2031 +/// {{time.today}} => Sunday, 12 January, 2031 +/// {{time.now}} => Sunday, January 12, 2031 9:15 PM +/// {{time.utcNow}} => Sunday, January 13, 2031 5:15 AM +/// {{time.time}} => 09:15:07 PM +/// {{time.year}} => 2031 +/// {{time.month}} => January +/// {{time.monthNumber}} => 01 +/// {{time.day}} => 12 +/// {{time.dayOfMonth}} => 12 +/// {{time.dayOfWeek}} => Sunday +/// {{time.hour}} => 9 PM +/// {{time.hourNumber}} => 21 +/// {{time.minute}} => 15 +/// {{time.minutes}} => 15 +/// {{time.second}} => 7 +/// {{time.seconds}} => 7 +/// {{time.timeZoneOffset}} => -08:00 +/// {{time.timeZoneName}} => PST +/// +/// +/// Note: the time represents the time on the hw/vm/machine where the kernel is running. +/// TODO: import and use user's timezone +/// +public class TimeSkill +{ + /// + /// Get the current date + /// + /// + /// {{time.date}} => Sunday, 12 January, 2031 + /// + /// The current date + [SKFunction("Get the current date")] + public string Date() + { + // Example: Sunday, 12 January, 2025 + return DateTimeOffset.Now.ToString("D", CultureInfo.CurrentCulture); + } + + /// + /// Get the current date and time in the local time zone" + /// + /// + /// {{time.now}} => Sunday, January 12, 2025 9:15 PM + /// + /// The current date and time in the local time zone + [SKFunction("Get the current date and time in the local time zone")] + public string Now() + { + // Sunday, January 12, 2025 9:15 PM + return DateTimeOffset.Now.ToString("f", CultureInfo.CurrentCulture); + } + + /// + /// Get the current UTC date and time + /// + /// + /// {{time.utcNow}} => Sunday, January 13, 2025 5:15 AM + /// + /// The current UTC date and time + [SKFunction("Get the current UTC date and time")] + public string UtcNow() + { + // Sunday, January 13, 2025 5:15 AM + return DateTimeOffset.UtcNow.ToString("f", CultureInfo.CurrentCulture); + } + + /// + /// Get the current time + /// + /// + /// {{time.time}} => 09:15:07 PM + /// + /// The current time + [SKFunction("Get the current time")] + public string Time() + { + // Example: 09:15:07 PM + return DateTimeOffset.Now.ToString("hh:mm:ss tt", CultureInfo.CurrentCulture); + } + + /// + /// Get the current year + /// + /// + /// {{time.year}} => 2025 + /// + /// The current year + [SKFunction("Get the current year")] + public string Year() + { + // Example: 2025 + return DateTimeOffset.Now.ToString("yyyy", CultureInfo.CurrentCulture); + } + + /// + /// Get the current month name + /// + /// + /// {time.month}} => January + /// + /// The current month name + [SKFunction("Get the current month name")] + public string Month() + { + // Example: January + return DateTimeOffset.Now.ToString("MMMM", CultureInfo.CurrentCulture); + } + + /// + /// Get the current month number + /// + /// + /// {{time.monthNumber}} => 01 + /// + /// The current month number + [SKFunction("Get the current month number")] + public string MonthNumber() + { + // Example: 01 + return DateTimeOffset.Now.ToString("MM", CultureInfo.CurrentCulture); + } + + /// + /// Get the current day of the month + /// + /// + /// {{time.day}} => 12 + /// + /// The current day of the month + [SKFunction("Get the current day of the month")] + public string Day() + { + // Example: 12 + return DateTimeOffset.Now.ToString("DD", CultureInfo.CurrentCulture); + } + + /// + /// Get the current day of the week + /// + /// + /// {{time.dayOfWeek}} => Sunday + /// + /// The current day of the week + [SKFunction("Get the current day of the week")] + public string DayOfWeek() + { + // Example: Sunday + return DateTimeOffset.Now.ToString("dddd", CultureInfo.CurrentCulture); + } + + /// + /// Get the current clock hour + /// + /// + /// {{time.hour}} => 9 PM + /// + /// The current clock hour + [SKFunction("Get the current clock hour")] + public string Hour() + { + // Example: 9 PM + return DateTimeOffset.Now.ToString("h tt", CultureInfo.CurrentCulture); + } + + /// + /// Get the current clock 24-hour number + /// + /// + /// {{time.hourNumber}} => 21 + /// + /// The current clock 24-hour number + [SKFunction("Get the current clock 24-hour number")] + public string HourNumber() + { + // Example: 21 + return DateTimeOffset.Now.ToString("HH", CultureInfo.CurrentCulture); + } + + /// + /// Get the minutes on the current hour + /// + /// + /// {{time.minute}} => 15 + /// + /// The minutes on the current hour + [SKFunction("Get the minutes on the current hour")] + public string Minute() + { + // Example: 15 + return DateTimeOffset.Now.ToString("m", CultureInfo.CurrentCulture); + } + + /// + /// Get the seconds on the current minute + /// + /// + /// {{time.second}} => 7 + /// + /// The seconds on the current minute + [SKFunction("Get the seconds on the current minute")] + public string Second() + { + // Example: 7 + return DateTimeOffset.Now.ToString("s", CultureInfo.CurrentCulture); + } + + /// + /// Get the local time zone offset from UTC + /// + /// + /// {{time.timeZoneOffset}} => -08:00 + /// + /// The local time zone offset from UTC + [SKFunction("Get the local time zone offset from UTC")] + public string TimeZoneOffset() + { + // Example: -08:00 + return DateTimeOffset.Now.ToString("%K", CultureInfo.CurrentCulture); + } + + /// + /// Get the local time zone name + /// + /// + /// {{time.timeZoneName}} => PST + /// + /// + /// Note: this is the "current" timezone and it can change over the year, e.g. from PST to PDT + /// + /// The local time zone name + [SKFunction("Get the local time zone name")] + public string TimeZoneName() + { + // Example: PST + // Note: this is the "current" timezone and it can change over the year, e.g. from PST to PDT + return TimeZoneInfo.Local.DisplayName; + } +} diff --git a/dotnet/src/SemanticKernel/Diagnostics/Exception.cs b/dotnet/src/SemanticKernel/Diagnostics/Exception.cs new file mode 100644 index 000000000000..e56058b372a2 --- /dev/null +++ b/dotnet/src/SemanticKernel/Diagnostics/Exception.cs @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; + +namespace Microsoft.SemanticKernel.Diagnostics; + +/// +/// Base exception for all SK exceptions +/// +/// Enum type used for the error codes +public abstract class Exception : Exception where TErrorCode : Enum +{ + /// + /// Initializes a new instance of the class. + /// + /// The error type. + /// The message. + protected Exception(TErrorCode errCode, string? message = null) : base(BuildMessage(errCode, message)) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The error type. + /// The message. + /// The inner exception. + protected Exception(TErrorCode errCode, string? message, Exception? innerException) + : base(BuildMessage(errCode, message), innerException) + { + } + + #region private ================================================================================ + + private static string BuildMessage(TErrorCode errorType, string? message) + { + return message != null ? $"{errorType.ToString("G")}: {message}" : errorType.ToString("G"); + } + + #endregion +} diff --git a/dotnet/src/SemanticKernel/Diagnostics/ExceptionExtensions.cs b/dotnet/src/SemanticKernel/Diagnostics/ExceptionExtensions.cs new file mode 100644 index 000000000000..293158ab8bd8 --- /dev/null +++ b/dotnet/src/SemanticKernel/Diagnostics/ExceptionExtensions.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Threading; + +namespace Microsoft.SemanticKernel.Diagnostics; + +/// +/// Exception extension methods. +/// +public static class ExceptionExtensions +{ + /// + /// Check if an exception is of a type that should not be caught by the kernel. + /// + /// Exception. + /// True if is a critical exception and should not be caught. + public static bool IsCriticalException(this Exception ex) + => ex is OutOfMemoryException + or ThreadAbortException + or AccessViolationException + or AppDomainUnloadedException + or BadImageFormatException + or CannotUnloadAppDomainException + or InvalidProgramException + or StackOverflowException; +} diff --git a/dotnet/src/SemanticKernel/Diagnostics/ValidationException.cs b/dotnet/src/SemanticKernel/Diagnostics/ValidationException.cs new file mode 100644 index 000000000000..4227672a97cf --- /dev/null +++ b/dotnet/src/SemanticKernel/Diagnostics/ValidationException.cs @@ -0,0 +1,73 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; + +namespace Microsoft.SemanticKernel.Diagnostics; + +/// +/// Generic validation exception +/// +public class ValidationException : Exception +{ + /// + /// Error codes for . + /// + public enum ErrorCodes + { + /// + /// Unknown error. + /// + UnknownError = -1, + + /// + /// Null value. + /// + NullValue, + + /// + /// Empty value. + /// + EmptyValue, + + /// + /// Out of range. + /// + OutOfRange, + + /// + /// Missing prefix. + /// + MissingPrefix, + + /// + /// Directory not found. + /// + DirectoryNotFound, + } + + /// + /// Gets the error code of the exception. + /// + public ErrorCodes ErrorCode { get; set; } + + /// + /// Initializes a new instance of the class. + /// + /// The error code. + /// The message. + public ValidationException(ErrorCodes errCode, string message) : base(errCode, message) + { + this.ErrorCode = errCode; + } + + /// + /// Initializes a new instance of the class. + /// + /// The error code. + /// The message. + /// The inner exception. + public ValidationException(ErrorCodes errCode, string message, Exception e) : base(errCode, message, e) + { + this.ErrorCode = errCode; + } +} diff --git a/dotnet/src/SemanticKernel/Diagnostics/Verify.cs b/dotnet/src/SemanticKernel/Diagnostics/Verify.cs new file mode 100644 index 000000000000..817bc9402523 --- /dev/null +++ b/dotnet/src/SemanticKernel/Diagnostics/Verify.cs @@ -0,0 +1,109 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Runtime.CompilerServices; +using System.Text.RegularExpressions; +using Microsoft.SemanticKernel.SkillDefinition; + +namespace Microsoft.SemanticKernel.Diagnostics; + +internal static class Verify +{ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void NotNull([NotNull] object? obj, string message) + { + if (obj != null) { return; } + + throw new ValidationException(ValidationException.ErrorCodes.NullValue, message); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void NotEmpty([NotNull] string? str, string message) + { + NotNull(str, message); + if (!string.IsNullOrWhiteSpace(str)) { return; } + + throw new ValidationException(ValidationException.ErrorCodes.EmptyValue, message); + } + + internal static void ValidSkillName([NotNull] string? skillName) + { + NotEmpty(skillName, "The skill name cannot be empty"); + Regex pattern = new("^[0-9A-Za-z_]*$"); + if (!pattern.IsMatch(skillName)) + { + throw new KernelException( + KernelException.ErrorCodes.InvalidFunctionDescription, + "A skill name can contain only latin letters, 0-9 digits, " + + $"and underscore: '{skillName}' is not a valid name."); + } + } + + internal static void ValidFunctionName([NotNull] string? functionName) + { + NotEmpty(functionName, "The function name cannot be empty"); + Regex pattern = new("^[0-9A-Za-z_]*$"); + if (!pattern.IsMatch(functionName)) + { + throw new KernelException( + KernelException.ErrorCodes.InvalidFunctionDescription, + "A function name can contain only latin letters, 0-9 digits, " + + $"and underscore: '{functionName}' is not a valid name."); + } + } + + internal static void ValidFunctionParamName([NotNull] string? functionParamName) + { + NotEmpty(functionParamName, "The function parameter name cannot be empty"); + Regex pattern = new("^[0-9A-Za-z_]*$"); + if (!pattern.IsMatch(functionParamName)) + { + throw new KernelException( + KernelException.ErrorCodes.InvalidFunctionDescription, + "A function parameter name can contain only latin letters, 0-9 digits, " + + $"and underscore: '{functionParamName}' is not a valid name."); + } + } + + internal static void StartsWith(string text, string prefix, string message) + { + NotEmpty(text, "The text to verify cannot be empty"); + NotNull(prefix, "The prefix to verify is empty"); + if (text.StartsWith(prefix, StringComparison.InvariantCultureIgnoreCase)) { return; } + + throw new ValidationException(ValidationException.ErrorCodes.MissingPrefix, message); + } + + internal static void DirectoryExists(string path) + { + if (Directory.Exists(path)) { return; } + + throw new ValidationException( + ValidationException.ErrorCodes.DirectoryNotFound, + $"Directory not found: {path}"); + } + + /// + /// Make sure every function parameter name is unique + /// + /// List of parameters + internal static void ParametersUniqueness(IEnumerable parameters) + { + var x = new HashSet(StringComparer.OrdinalIgnoreCase); + foreach (var p in parameters) + { + if (x.Contains(p.Name)) + { + throw new KernelException( + KernelException.ErrorCodes.InvalidFunctionDescription, + $"The function has two or more parameters with the same name '{p.Name}'"); + } + + NotEmpty(p.Name, "The parameter name is empty"); + x.Add(p.Name); + } + } +} diff --git a/dotnet/src/SemanticKernel/IKernel.cs b/dotnet/src/SemanticKernel/IKernel.cs new file mode 100644 index 000000000000..71a320d83135 --- /dev/null +++ b/dotnet/src/SemanticKernel/IKernel.cs @@ -0,0 +1,159 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel.Configuration; +using Microsoft.SemanticKernel.Memory; +using Microsoft.SemanticKernel.Orchestration; +using Microsoft.SemanticKernel.SemanticFunctions; +using Microsoft.SemanticKernel.SkillDefinition; +using Microsoft.SemanticKernel.TemplateEngine; + +namespace Microsoft.SemanticKernel; + +/// +/// Interface for the semantic kernel. +/// +public interface IKernel +{ + /// + /// Settings required to execute functions, including details about AI dependencies, e.g. endpoints and API keys. + /// + KernelConfig Config { get; } + + /// + /// App logger + /// + ILogger Log { get; } + + /// + /// Semantic memory instance + /// + ISemanticTextMemory Memory { get; } + + /// + /// Reference to the engine rendering prompt templates + /// + IPromptTemplateEngine PromptTemplateEngine { get; } + + /// + /// Reference to the read-only skill collection containing all the imported functions + /// + IReadOnlySkillCollection Skills { get; } + + /// + /// Build and register a function in the internal skill collection, in a global generic skill. + /// + /// Name of the semantic function. The name can contain only alphanumeric chars + underscore. + /// Function configuration, e.g. I/O params, AI settings, localization details, etc. + /// A C# function wrapping AI logic, usually defined with natural language + ISKFunction RegisterSemanticFunction( + string functionName, + SemanticFunctionConfig functionConfig); + + /// + /// Build and register a function in the internal skill collection. + /// + /// Name of the skill containing the function. The name can contain only alphanumeric chars + underscore. + /// Name of the semantic function. The name can contain only alphanumeric chars + underscore. + /// Function configuration, e.g. I/O params, AI settings, localization details, etc. + /// A C# function wrapping AI logic, usually defined with natural language + ISKFunction RegisterSemanticFunction( + string skillName, + string functionName, + SemanticFunctionConfig functionConfig); + + /// + /// Import a set of functions from the given skill. The functions must have the `SKFunction` attribute. + /// Once these functions are imported, the prompt templates can use functions to import content at runtime. + /// + /// Instance of a class containing functions + /// Name of the skill for skill collection and prompt templates. If the value is empty functions are registered in the global namespace. + /// A list of all the semantic functions found in the directory, indexed by function name. + IDictionary ImportSkill(object skillInstance, string skillName = ""); + + /// + /// Set the semantic memory to use + /// + /// Semantic memory instance + void RegisterMemory(ISemanticTextMemory memory); + + /// + /// Run a pipeline composed by synchronous and asynchronous functions. + /// + /// List of functions + /// Result of the function composition + Task RunAsync( + params ISKFunction[] pipeline); + + /// + /// Run a pipeline composed by synchronous and asynchronous functions. + /// + /// Input to process + /// List of functions + /// Result of the function composition + Task RunAsync( + string input, + params ISKFunction[] pipeline); + + /// + /// Run a pipeline composed by synchronous and asynchronous functions. + /// + /// Input to process + /// List of functions + /// Result of the function composition + Task RunAsync( + ContextVariables variables, + params ISKFunction[] pipeline); + + /// + /// Run a pipeline composed by synchronous and asynchronous functions. + /// + /// Cancellation token + /// List of functions + /// Result of the function composition + Task RunAsync( + CancellationToken cancellationToken, + params ISKFunction[] pipeline); + + /// + /// Run a pipeline composed by synchronous and asynchronous functions. + /// + /// Input to process + /// Cancellation token + /// List of functions + /// Result of the function composition + Task RunAsync( + string input, + CancellationToken cancellationToken, + params ISKFunction[] pipeline); + + /// + /// Run a pipeline composed by synchronous and asynchronous functions. + /// + /// Input to process + /// Cancellation token + /// List of functions + /// Result of the function composition + Task RunAsync( + ContextVariables variables, + CancellationToken cancellationToken, + params ISKFunction[] pipeline); + + /// + /// Access registered functions by skill + name. Not case sensitive. + /// The function might be native or semantic, it's up to the caller handling it. + /// + /// Skill name + /// Function name + /// Delegate to execute the function + ISKFunction Func(string skillName, string functionName); + + /// + /// Create a new instance of a context, linked to the kernel internal state. + /// + /// SK context + SKContext CreateNewContext(); +} diff --git a/dotnet/src/SemanticKernel/Kernel.cs b/dotnet/src/SemanticKernel/Kernel.cs new file mode 100644 index 000000000000..34b6053bec95 --- /dev/null +++ b/dotnet/src/SemanticKernel/Kernel.cs @@ -0,0 +1,324 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel.AI; +using Microsoft.SemanticKernel.AI.OpenAI.Services; +using Microsoft.SemanticKernel.Configuration; +using Microsoft.SemanticKernel.Diagnostics; +using Microsoft.SemanticKernel.Memory; +using Microsoft.SemanticKernel.Orchestration; +using Microsoft.SemanticKernel.SemanticFunctions; +using Microsoft.SemanticKernel.SkillDefinition; +using Microsoft.SemanticKernel.TemplateEngine; +using Microsoft.SemanticKernel.Text; + +namespace Microsoft.SemanticKernel; + +/// +/// Semantic kernel class. +/// The kernel provides a skill collection to define native and semantic functions, an orchestrator to execute a list of functions. +/// Semantic functions are automatically rendered and executed using an internal prompt template rendering engine. +/// Future versions will allow to: +/// * customize the rendering engine +/// * include branching logic in the functions pipeline +/// * persist execution state for long running pipelines +/// * distribute pipelines over a network +/// * RPC functions and secure environments, e.g. sandboxing and credentials management +/// * auto-generate pipelines given a higher level goal +/// +public sealed class Kernel : IKernel, IDisposable +{ + /// + public KernelConfig Config => this._config; + + /// + public ILogger Log => this._log; + + /// + public ISemanticTextMemory Memory => this._memory; + + /// + public IReadOnlySkillCollection Skills => this._skillCollection.ReadOnlySkillCollection; + + /// + public IPromptTemplateEngine PromptTemplateEngine => this._promptTemplateEngine; + + /// + /// Return a new instance of the kernel builder, used to build and configure kernel instances. + /// + public static KernelBuilder Builder => new(); + + /// + /// Kernel constructor. See KernelBuilder for an easier and less error prone approach to create kernel instances. + /// + /// + /// + /// + /// + /// + public Kernel( + ISkillCollection skillCollection, + IPromptTemplateEngine promptTemplateEngine, + ISemanticTextMemory memory, + KernelConfig config, + ILogger log) + { + this._log = log; + this._config = config; + this._memory = memory; + this._promptTemplateEngine = promptTemplateEngine; + this._skillCollection = skillCollection; + } + + /// + public ISKFunction RegisterSemanticFunction(string functionName, SemanticFunctionConfig functionConfig) + { + return this.RegisterSemanticFunction(SkillCollection.GlobalSkill, functionName, functionConfig); + } + + /// + public ISKFunction RegisterSemanticFunction(string skillName, string functionName, SemanticFunctionConfig functionConfig) + { + // Future-proofing the name not to contain special chars + Verify.ValidSkillName(skillName); + Verify.ValidFunctionName(functionName); + + ISKFunction function = this.CreateSemanticFunction(skillName, functionName, functionConfig); + this._skillCollection.AddSemanticFunction(function); + + return function; + } + + /// + public IDictionary ImportSkill(object skillInstance, string skillName = "") + { + if (string.IsNullOrWhiteSpace(skillName)) + { + skillName = SkillCollection.GlobalSkill; + this._log.LogTrace("Importing skill {0} in the global namespace", skillInstance.GetType().FullName); + } + else + { + this._log.LogTrace("Importing skill {0}", skillName); + } + + var skill = new Dictionary(StringComparer.OrdinalIgnoreCase); + IEnumerable functions = ImportSkill(skillInstance, skillName, this._log); + foreach (ISKFunction f in functions) + { + f.SetDefaultSkillCollection(this.Skills); + this._skillCollection.AddNativeFunction(f); + skill.Add(f.Name, f); + } + + return skill; + } + + /// + public void RegisterMemory(ISemanticTextMemory memory) + { + this._memory = memory; + } + + /// + public Task RunAsync(params ISKFunction[] pipeline) + => this.RunAsync(new ContextVariables(), pipeline); + + /// + public Task RunAsync(string input, params ISKFunction[] pipeline) + => this.RunAsync(new ContextVariables(input), pipeline); + + /// + public Task RunAsync(ContextVariables variables, params ISKFunction[] pipeline) + => this.RunAsync(variables, CancellationToken.None, pipeline); + + /// + public Task RunAsync(CancellationToken cancellationToken, params ISKFunction[] pipeline) + => this.RunAsync(new ContextVariables(), cancellationToken, pipeline); + + /// + public Task RunAsync(string input, CancellationToken cancellationToken, params ISKFunction[] pipeline) + => this.RunAsync(new ContextVariables(input), cancellationToken, pipeline); + + /// + public async Task RunAsync(ContextVariables variables, CancellationToken cancellationToken, params ISKFunction[] pipeline) + { + var context = new SKContext( + variables, + this._memory, + this._skillCollection.ReadOnlySkillCollection, + this._log, + cancellationToken); + + int pipelineStepCount = -1; + foreach (ISKFunction f in pipeline) + { + if (context.ErrorOccurred) + { + this._log.LogError( + context.LastException, + "Something went wrong in pipeline step {0}:'{1}'", pipelineStepCount, context.LastErrorDescription); + return context; + } + + pipelineStepCount++; + + try + { + cancellationToken.ThrowIfCancellationRequested(); + context = await f.InvokeAsync(context); + + if (context.ErrorOccurred) + { + this._log.LogError("Function call fail during pipeline step {0}: {1}.{2}", pipelineStepCount, f.SkillName, f.Name); + return context; + } + } +#pragma warning disable CA1031 // We need to catch all exceptions to handle the execution state + catch (Exception e) when (!e.IsCriticalException()) + { + this._log.LogError(e, "Something went wrong in pipeline step {0}: {1}.{2}. Error: {3}", pipelineStepCount, f.SkillName, f.Name, e.Message); + context.Fail(e.Message, e); + return context; + } +#pragma warning restore CA1031 + } + + return context; + } + + /// + public ISKFunction Func(string skillName, string functionName) + { + if (this.Skills.HasNativeFunction(skillName, functionName)) + { + return this.Skills.GetNativeFunction(skillName, functionName); + } + + return this.Skills.GetSemanticFunction(skillName, functionName); + } + + /// + public SKContext CreateNewContext() + { + return new SKContext( + new ContextVariables(), + this._memory, + this._skillCollection.ReadOnlySkillCollection, + this._log); + } + + /// + /// Dispose of resources. + /// + public void Dispose() + { + // ReSharper disable once SuspiciousTypeConversion.Global + if (this._memory is IDisposable mem) { mem.Dispose(); } + + // ReSharper disable once SuspiciousTypeConversion.Global + if (this._skillCollection is IDisposable reg) { reg.Dispose(); } + } + + #region private ================================================================================ + + private readonly ILogger _log; + private readonly KernelConfig _config; + private readonly ISkillCollection _skillCollection; + private ISemanticTextMemory _memory; + private readonly IPromptTemplateEngine _promptTemplateEngine; + + private ISKFunction CreateSemanticFunction( + string skillName, + string functionName, + SemanticFunctionConfig functionConfig) + { + if (!functionConfig.PromptTemplateConfig.Type.EqualsIgnoreCase("completion")) + { + throw new AIException( + AIException.ErrorCodes.FunctionTypeNotSupported, + $"Function type not supported: {functionConfig.PromptTemplateConfig}"); + } + + ISKFunction func = SKFunction.FromSemanticConfig(skillName, functionName, functionConfig); + func.RequestSettings.UpdateFromCompletionConfig(functionConfig.PromptTemplateConfig.Completion); + + // Connect the function to the current kernel skill collection, in case the function + // is invoked manually without a context and without a way to find other functions. + func.SetDefaultSkillCollection(this.Skills); + + // TODO: allow to postpone this (e.g. use lazy init), allow to create semantic functions without a default backend + var backend = this._config.GetCompletionBackend(functionConfig.PromptTemplateConfig.DefaultBackends.FirstOrDefault()); + + func.SetAIConfiguration(CompleteRequestSettings.FromCompletionConfig(functionConfig.PromptTemplateConfig.Completion)); + + switch (backend.BackendType) + { + case BackendTypes.AzureOpenAI: + Verify.NotNull(backend.AzureOpenAI, "Azure OpenAI configuration is missing"); + func.SetAIBackend(() => new AzureTextCompletion( + backend.AzureOpenAI.DeploymentName, + backend.AzureOpenAI.Endpoint, + backend.AzureOpenAI.APIKey, + backend.AzureOpenAI.APIVersion, + this._log)); + break; + + case BackendTypes.OpenAI: + Verify.NotNull(backend.OpenAI, "OpenAI configuration is missing"); + func.SetAIBackend(() => new OpenAITextCompletion( + backend.OpenAI.ModelId, + backend.OpenAI.APIKey, + backend.OpenAI.OrgId, + this._log)); + break; + + default: + throw new AIException( + AIException.ErrorCodes.InvalidConfiguration, + $"Unknown/unsupported backend type {backend.BackendType:G}, unable to prepare semantic function. " + + $"Function description: {functionConfig.PromptTemplateConfig.Description}"); + } + + return func; + } + + /// + /// Import a skill into the kernel skill collection, so that semantic functions and pipelines can consume its functions. + /// + /// Skill class instance + /// Skill name, used to group functions under a shared namespace + /// Application logger + /// List of functions imported from the given class instance + private static IList ImportSkill(object skillInstance, string skillName, ILogger log) + { + log.LogTrace("Importing skill name: {0}", skillName); + MethodInfo[] methods = skillInstance.GetType() + .GetMethods(BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public | BindingFlags.InvokeMethod); + log.LogTrace("Methods found {0}", methods.Length); + + // Filter out null functions + IEnumerable functions = from method in methods select SKFunction.FromNativeMethod(method, skillInstance, skillName, log); + List result = (from function in functions where function != null select function).ToList(); + log.LogTrace("Methods imported {0}", result.Count); + + // Fail if two functions have the same name + var uniquenessCheck = new HashSet(from x in result select x.Name, StringComparer.OrdinalIgnoreCase); + if (result.Count > uniquenessCheck.Count) + { + throw new KernelException( + KernelException.ErrorCodes.FunctionOverloadNotSupported, + "Function overloads are not supported, please differentiate function names"); + } + + return result; + } + + #endregion +} diff --git a/dotnet/src/SemanticKernel/KernelBuilder.cs b/dotnet/src/SemanticKernel/KernelBuilder.cs new file mode 100644 index 000000000000..f03663de4946 --- /dev/null +++ b/dotnet/src/SemanticKernel/KernelBuilder.cs @@ -0,0 +1,134 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.SemanticKernel.AI.Embeddings; +using Microsoft.SemanticKernel.Configuration; +using Microsoft.SemanticKernel.Diagnostics; +using Microsoft.SemanticKernel.KernelExtensions; +using Microsoft.SemanticKernel.Memory; +using Microsoft.SemanticKernel.SkillDefinition; +using Microsoft.SemanticKernel.TemplateEngine; + +namespace Microsoft.SemanticKernel; + +/// +/// A builder for Semantic Kernel. +/// TODO: unit tests +/// +public sealed class KernelBuilder +{ + private KernelConfig _config = new KernelConfig(); + private ISemanticTextMemory _memory = NullMemory.Instance; + private ILogger _log = NullLogger.Instance; + private IMemoryStore? _memoryStorage = null; + + /// + /// Create a new kernel instance + /// + /// + public static IKernel Create() + { + var builder = new KernelBuilder(); + return builder.Build(); + } + + /// + /// Build a new kernel instance using the settings passed so far. + /// + /// Kernel instance + public IKernel Build() + { + var instance = new Kernel( + new SkillCollection(this._log), + new PromptTemplateEngine(this._log), + this._memory, + this._config, + this._log + ); + + // TODO: decouple this from 'UseMemory' kernel extension + if (this._memoryStorage != null) + { + instance.UseMemory(this._memoryStorage); + } + + return instance; + } + + /// + /// Add a logger to the kernel to be built. + /// + /// Logger to add. + /// Updated kernel builder including the logger. + public KernelBuilder WithLogger(ILogger log) + { + Verify.NotNull(log, "The logger instance provided is NULL"); + this._log = log; + return this; + } + + /// + /// Add a semantic text memory entity to the kernel to be built. + /// + /// Semantic text memory entity to add. + /// Updated kernel builder including the semantic text memory entity. + public KernelBuilder WithMemory(ISemanticTextMemory memory) + { + Verify.NotNull(memory, "The memory instance provided is NULL"); + this._memory = memory; + return this; + } + + /// + /// Add memory storage to the kernel to be built. + /// + /// Storage to add. + /// Updated kernel builder including the memory storage. + public KernelBuilder WithMemoryStorage(IMemoryStore storage) + { + Verify.NotNull(storage, "The memory instance provided is NULL"); + this._memoryStorage = storage; + return this; + } + + /// + /// Add memory storage and an embedding generator to the kernel to be built. + /// + /// Storage to add. + /// Embedding generator to add. + /// Updated kernel builder including the memory storage and embedding generator. + public KernelBuilder WithMemoryStorageAndEmbeddingGenerator( + IMemoryStore storage, IEmbeddingGenerator embeddingGenerator) + { + Verify.NotNull(storage, "The memory instance provided is NULL"); + Verify.NotNull(embeddingGenerator, "The embedding generator instance provided is NULL"); + this._memory = new SemanticTextMemory(storage, embeddingGenerator); + return this; + } + + /// + /// Use the given configuration with the kernel to be built. + /// + /// Configuration to use. + /// Updated kernel builder including the given configuration. + public KernelBuilder WithConfiguration(KernelConfig config) + { + Verify.NotNull(config, "The configuration instance provided is NULL"); + this._config = config; + return this; + } + + /// + /// Update the configuration using the instructions provided. + /// + /// Action that updates the current configuration. + /// Updated kernel builder including the updated configuration. + public KernelBuilder Configure(Action configure) + { + Verify.NotNull(configure, "The configuration action provided is NULL"); + configure.Invoke(this._config); + return this; + } +} diff --git a/dotnet/src/SemanticKernel/KernelException.cs b/dotnet/src/SemanticKernel/KernelException.cs new file mode 100644 index 000000000000..a44b9a4645f1 --- /dev/null +++ b/dotnet/src/SemanticKernel/KernelException.cs @@ -0,0 +1,89 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using Microsoft.SemanticKernel.Diagnostics; + +namespace Microsoft.SemanticKernel; + +/// +/// Kernel logic exception +/// +public class KernelException : Exception +{ + /// + /// Semantic kernel error codes. + /// + public enum ErrorCodes + { + /// + /// Unknown error. + /// + UnknownError = -1, + + /// + /// Invalid function description. + /// + InvalidFunctionDescription, + + /// + /// Function overload not supported. + /// + FunctionOverloadNotSupported, + + /// + /// Function not available. + /// + FunctionNotAvailable, + + /// + /// Function type not supported. + /// + FunctionTypeNotSupported, + + /// + /// Invalid function type. + /// + InvalidFunctionType, + + /// + /// Invalid backend configuration. + /// + InvalidBackendConfiguration, + + /// + /// Backend not found. + /// + BackendNotFound, + + /// + /// Skill collection not set. + /// + SkillCollectionNotSet, + } + + /// + /// Error code. + /// + public ErrorCodes ErrorCode { get; set; } + + /// + /// Constructor for KernelException. + /// + /// Error code to put in KernelException. + /// Message to put in KernelException. + public KernelException(ErrorCodes errCode, string message) : base(errCode, message) + { + this.ErrorCode = errCode; + } + + /// + /// Constructor for KernelException. + /// + /// Error code to put in KernelException. + /// Message to put in KernelException. + /// Exception to embed in KernelException. + public KernelException(ErrorCodes errCode, string message, Exception e) : base(errCode, message, e) + { + this.ErrorCode = errCode; + } +} diff --git a/dotnet/src/SemanticKernel/KernelExtensions/ImportSemanticSkillFromDirectory.cs b/dotnet/src/SemanticKernel/KernelExtensions/ImportSemanticSkillFromDirectory.cs new file mode 100644 index 000000000000..5f57089ad637 --- /dev/null +++ b/dotnet/src/SemanticKernel/KernelExtensions/ImportSemanticSkillFromDirectory.cs @@ -0,0 +1,102 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.IO; +using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel.Diagnostics; +using Microsoft.SemanticKernel.Orchestration; +using Microsoft.SemanticKernel.SemanticFunctions; +using Microsoft.SemanticKernel.Text; + +namespace Microsoft.SemanticKernel.KernelExtensions; + +/// +/// Class for extensions methods for importing semantic functions from a directory. +/// +public static class ImportSemanticSkillFromDirectoryExtension +{ + /// + /// A kernel extension that allows to load Semantic Functions, defined by prompt templates stored in the filesystem. + /// A skill directory contains a set of subdirectories, one for each semantic function. + /// This extension requires the path of the parent directory (e.g. "d:\skills") and the name of the skill directory + /// (e.g. "OfficeSkill"), which is used also as the "skill name" in the internal skill collection. + /// + /// Note: skill and function names can contain only alphanumeric chars and underscore. + /// + /// Example: + /// D:\skills\ # parentDirectory = "D:\skills" + /// + /// |__ OfficeSkill\ # skillDirectoryName = "SummarizeEmailThread" + /// + /// |__ ScheduleMeeting # semantic function + /// |__ skprompt.txt # prompt template + /// |__ config.json # settings (optional file) + /// + /// |__ SummarizeEmailThread # semantic function + /// |__ skprompt.txt # prompt template + /// |__ config.json # settings (optional file) + /// + /// |__ MergeWordAndExcelDocs # semantic function + /// |__ skprompt.txt # prompt template + /// |__ config.json # settings (optional file) + /// + /// |__ XboxSkill\ # another skill, etc. + /// + /// |__ MessageFriend + /// |__ skprompt.txt + /// |__ config.json + /// |__ LaunchGame + /// |__ skprompt.txt + /// |__ config.json + /// + /// See https://github.com/microsoft/semantic-kernel/tree/main/samples/skills for some skills in our repo. + /// + /// Semantic Kernel instance + /// Directory containing the skill directory, e.g. "d:\myAppSkills" + /// Name of the directory containing the selected skill, e.g. "StrategySkill" + /// A list of all the semantic functions found in the directory, indexed by function name. + public static IDictionary ImportSemanticSkillFromDirectory( + this IKernel kernel, string parentDirectory, string skillDirectoryName) + { + const string CONFIG_FILE = "config.json"; + const string PROMPT_FILE = "skprompt.txt"; + + Verify.ValidSkillName(skillDirectoryName); + var skillDir = Path.Join(parentDirectory, skillDirectoryName); + Verify.DirectoryExists(skillDir); + + var skill = new Dictionary(); + + string[] directories = Directory.GetDirectories(skillDir); + foreach (string dir in directories) + { + var functionName = Path.GetFileName(dir); + + // Continue only if prompt template exists + var promptPath = Path.Join(dir, PROMPT_FILE); + if (!File.Exists(promptPath)) { continue; } + + // Load prompt configuration. Note: the configuration is optional. + var config = new PromptTemplateConfig(); + var configPath = Path.Join(dir, CONFIG_FILE); + if (File.Exists(configPath)) + { + config = PromptTemplateConfig.FromJson(File.ReadAllText(configPath)); + Verify.NotNull(config, $"Invalid prompt template configuration, unable to parse {configPath}"); + } + + kernel.Log.LogTrace("Config {0}: {1}", functionName, config.ToJson()); + + // Load prompt template + var template = new PromptTemplate(File.ReadAllText(promptPath), config, kernel.PromptTemplateEngine); + + // Prepare lambda wrapping AI logic + var functionConfig = new SemanticFunctionConfig(config, template); + + kernel.Log.LogTrace("Registering function {0}.{1} loaded from {2}", skillDirectoryName, functionName, dir); + skill[functionName] = kernel.RegisterSemanticFunction(skillDirectoryName, functionName, functionConfig); + } + + return skill; + } +} diff --git a/dotnet/src/SemanticKernel/KernelExtensions/InlineFunctionsDefinitionExtension.cs b/dotnet/src/SemanticKernel/KernelExtensions/InlineFunctionsDefinitionExtension.cs new file mode 100644 index 000000000000..8019187e67f6 --- /dev/null +++ b/dotnet/src/SemanticKernel/KernelExtensions/InlineFunctionsDefinitionExtension.cs @@ -0,0 +1,104 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.SemanticKernel.Diagnostics; +using Microsoft.SemanticKernel.Orchestration; +using Microsoft.SemanticKernel.SemanticFunctions; + +namespace Microsoft.SemanticKernel.KernelExtensions; + +/// +/// Class for extensions methods to define semantic functions. +/// +public static class InlineFunctionsDefinitionExtension +{ + /// + /// Define a string-to-string semantic function, with no direct support for input context. + /// The function can be referenced in templates and will receive the context, but when invoked programmatically you + /// can only pass in a string in input and receive a string in output. + /// + /// Semantic Kernel instance + /// Plain language definition of the semantic function, using SK template language + /// A name for the given function. The name can be referenced in templates and used by the pipeline planner. + /// Optional skill name, for namespacing and avoid collisions + /// Optional description, useful for the planner + /// Max number of tokens to generate + /// Temperature parameter passed to LLM + /// Top P parameter passed to LLM + /// Presence Penalty parameter passed to LLM + /// Frequency Penalty parameter passed to LLM + /// Strings the LLM will detect to stop generating (before reaching max tokens) + /// A function ready to use + public static ISKFunction CreateSemanticFunction( + this IKernel kernel, + string promptTemplate, + string? functionName = null, + string skillName = "", + string? description = null, + int maxTokens = 256, + double temperature = 0, + double topP = 0, + double presencePenalty = 0, + double frequencyPenalty = 0, + IEnumerable? stopSequences = null) + { + functionName ??= RandomFunctionName(); + + var config = new PromptTemplateConfig + { + Description = description ?? "Generic function, unknown purpose", + Type = "completion", + Completion = new PromptTemplateConfig.CompletionConfig + { + Temperature = temperature, + TopP = topP, + PresencePenalty = presencePenalty, + FrequencyPenalty = frequencyPenalty, + MaxTokens = maxTokens, + StopSequences = stopSequences?.ToList() ?? new List() + } + }; + + return kernel.CreateSemanticFunction( + promptTemplate: promptTemplate, + config: config, + functionName: functionName, + skillName: skillName); + } + + /// + /// Allow to define a semantic function passing in the definition in natural language, i.e. the prompt template. + /// + /// Semantic Kernel instance + /// A name for the given function. The name can be referenced in templates and used by the pipeline planner. + /// Plain language definition of the semantic function, using SK template language + /// An optional skill name, e.g. to namespace functions with the same name. When empty, + /// the function is added to the global namespace, overwriting functions with the same name + /// Optional function settings + /// A function ready to use + public static ISKFunction CreateSemanticFunction( + this IKernel kernel, + string promptTemplate, + PromptTemplateConfig config, + string? functionName = null, + string skillName = "") + { + functionName ??= RandomFunctionName(); + Verify.ValidFunctionName(functionName); + if (!string.IsNullOrEmpty(skillName)) { Verify.ValidSkillName(skillName); } + + var template = new PromptTemplate(promptTemplate, config, kernel.PromptTemplateEngine); + + // Prepare lambda wrapping AI logic + var functionConfig = new SemanticFunctionConfig(config, template); + + // TODO: manage overwrites, potentially error out + return string.IsNullOrEmpty(skillName) + ? kernel.RegisterSemanticFunction(functionName, functionConfig) + : kernel.RegisterSemanticFunction(skillName, functionName, functionConfig); + } + + private static string RandomFunctionName() => "func" + Guid.NewGuid().ToString("N"); +} diff --git a/dotnet/src/SemanticKernel/KernelExtensions/MemoryConfiguration.cs b/dotnet/src/SemanticKernel/KernelExtensions/MemoryConfiguration.cs new file mode 100644 index 000000000000..b964fc8b130b --- /dev/null +++ b/dotnet/src/SemanticKernel/KernelExtensions/MemoryConfiguration.cs @@ -0,0 +1,88 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Diagnostics.CodeAnalysis; +using Microsoft.SemanticKernel.AI; +using Microsoft.SemanticKernel.AI.Embeddings; +using Microsoft.SemanticKernel.AI.OpenAI.Services; +using Microsoft.SemanticKernel.Configuration; +using Microsoft.SemanticKernel.Diagnostics; +using Microsoft.SemanticKernel.Memory; + +namespace Microsoft.SemanticKernel.KernelExtensions; + +/// +/// Kernel extension to configure the semantic memory with custom settings +/// +public static class MemoryConfiguration +{ + /// + /// Set the semantic memory to use the given memory storage. Uses the kernel's default embedding backend. + /// + /// Kernel instance + /// Memory storage + public static void UseMemory(this IKernel kernel, IMemoryStore storage) + { + UseMemory(kernel, kernel.Config.DefaultEmbeddingsBackend, storage); + } + + /// + /// Set the semantic memory to use the given memory storage and embedding backend. + /// + /// Kernel instance + /// Kernel backend for embedding generation + /// Memory storage + [SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", + Justification = "The embeddingGenerator object is disposed by the kernel")] + public static void UseMemory(this IKernel kernel, string? embeddingsBackendName, IMemoryStore storage) + { + Verify.NotEmpty(embeddingsBackendName, "The embedding backend name is empty"); + + BackendConfig embeddingsBackendCfg = kernel.Config.GetEmbeddingsBackend(embeddingsBackendName); + + IEmbeddingGenerator? embeddingGenerator; + + switch (embeddingsBackendCfg.BackendType) + { + case BackendTypes.AzureOpenAI: + Verify.NotNull(embeddingsBackendCfg.AzureOpenAI, "Azure OpenAI configuration is missing"); + embeddingGenerator = new AzureTextEmbeddings( + embeddingsBackendCfg.AzureOpenAI.DeploymentName, + embeddingsBackendCfg.AzureOpenAI.Endpoint, + embeddingsBackendCfg.AzureOpenAI.APIKey, + embeddingsBackendCfg.AzureOpenAI.APIVersion, + kernel.Log); + break; + + case BackendTypes.OpenAI: + Verify.NotNull(embeddingsBackendCfg.OpenAI, "OpenAI configuration is missing"); + embeddingGenerator = new OpenAITextEmbeddings( + embeddingsBackendCfg.OpenAI.ModelId, + embeddingsBackendCfg.OpenAI.APIKey, + embeddingsBackendCfg.OpenAI.OrgId, + kernel.Log); + break; + + default: + throw new AIException( + AIException.ErrorCodes.InvalidConfiguration, + $"Unknown/unsupported backend type {embeddingsBackendCfg.BackendType:G}, unable to prepare semantic memory"); + } + + UseMemory(kernel, embeddingGenerator, storage); + } + + /// + /// Set the semantic memory to use the given memory storage and embedding generator. + /// + /// Kernel instance + /// Embedding generator + /// Memory storage + [SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", Justification = "The embeddingGenerator object is disposed by the kernel")] + public static void UseMemory(this IKernel kernel, IEmbeddingGenerator embeddingGenerator, IMemoryStore storage) + { + Verify.NotNull(storage, "The storage instance provided is NULL"); + Verify.NotNull(embeddingGenerator, "The embedding generator is NULL"); + + kernel.RegisterMemory(new SemanticTextMemory(storage, embeddingGenerator)); + } +} diff --git a/dotnet/src/SemanticKernel/Memory/IMemoryStore.cs b/dotnet/src/SemanticKernel/Memory/IMemoryStore.cs new file mode 100644 index 000000000000..3a361afbaaa1 --- /dev/null +++ b/dotnet/src/SemanticKernel/Memory/IMemoryStore.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.SemanticKernel.AI.Embeddings; +using Microsoft.SemanticKernel.Memory.Storage; + +namespace Microsoft.SemanticKernel.Memory; + +/// +/// An interface for storing and retrieving indexed objects in a datastore. +/// +/// The data type of the embeddings stored datastore. +public interface IMemoryStore : IDataStore>, IEmbeddingIndex + where TEmbedding : unmanaged +{ +} diff --git a/dotnet/src/SemanticKernel/Memory/ISemanticTextMemory.cs b/dotnet/src/SemanticKernel/Memory/ISemanticTextMemory.cs new file mode 100644 index 000000000000..924df9f55d10 --- /dev/null +++ b/dotnet/src/SemanticKernel/Memory/ISemanticTextMemory.cs @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.SemanticKernel.Memory; + +/// +/// An interface semantic memory that creates and recalls memories associated with text. +/// +public interface ISemanticTextMemory +{ + /// + /// Save some information into the semantic memory, keeping a copy of the source information. + /// + /// Collection where to save the information + /// Unique identifier + /// Information to save + /// Optional description + /// Cancellation token + public Task SaveInformationAsync( + string collection, + string text, + string id, + string? description = null, + CancellationToken cancel = default); + + /// + /// Save some information into the semantic memory, keeping only a reference to the source information. + /// + /// Collection where to save the information + /// Information to save + /// Unique identifier, e.g. URL or GUID to the original source + /// Name of the external service, e.g. "MSTeams", "GitHub", "WebSite", "Outlook IMAP", etc. + /// Optional description + /// Cancellation token + public Task SaveReferenceAsync( + string collection, + string text, + string externalId, + string externalSourceName, + string? description = null, + CancellationToken cancel = default); + + /// + /// Fetch a memory by key. + /// For local memories the key is the "id" used when saving the record. + /// For external reference, the key is the "URI" used when saving the record. + /// + /// Collection to search + /// Unique memory record identifier + /// Cancellation token + /// Memory record, or null when nothing is found + public Task GetAsync(string collection, string key, CancellationToken cancel = default); + + /// + /// Find some information in memory + /// + /// Collection to search + /// What to search for + /// How many results to return + /// Minimum relevance score, from 0 to 1, where 1 means exact match. + /// Cancellation token + /// Memories found + public IAsyncEnumerable SearchAsync( + string collection, + string query, + int limit = 1, + double minRelevanceScore = 0.7, + CancellationToken cancel = default); + + /// + /// Gets a group of all available collection names. + /// + /// Cancellation token. + /// A group of collection names. + public Task> GetCollectionsAsync(CancellationToken cancel = default); +} diff --git a/dotnet/src/SemanticKernel/Memory/MemoryQueryResult.cs b/dotnet/src/SemanticKernel/Memory/MemoryQueryResult.cs new file mode 100644 index 000000000000..a35f3eb593cd --- /dev/null +++ b/dotnet/src/SemanticKernel/Memory/MemoryQueryResult.cs @@ -0,0 +1,81 @@ +// Copyright (c) Microsoft. All rights reserved. + +namespace Microsoft.SemanticKernel.Memory; + +/// +/// Copy of metadata associated with a memory entry. +/// +public class MemoryQueryResult +{ + /// + /// Whether the source data used to calculate embeddings are stored in the local + /// storage provider or is available through and external service, such as web site, MS Graph, etc. + /// + public bool IsReference { get; } + + /// + /// A value used to understand which external service owns the data, to avoid storing the information + /// inside the URI. E.g. this could be "MSTeams", "WebSite", "GitHub", etc. + /// + public string ExternalSourceName { get; } + + /// + /// Unique identifier. The format of the value is domain specific, so it can be a URL, a GUID, etc. + /// + public string Id { get; } + + /// + /// Optional entry description, useful for external references when Text is empty. + /// + public string Description { get; } + + /// + /// Source text, available only when the memory is not an external source. + /// + public string Text { get; } + + /// + /// Search relevance, from 0 to 1, where 1 means perfect match. + /// + public double Relevance { get; } + + /// + /// Create new instance + /// + /// Whether the source data used to calculate embeddings are stored in the local + /// storage provider or is available through and external service, such as web site, MS Graph, etc. + /// A value used to understand which external service owns the data, to avoid storing the information + /// inside the Id. E.g. this could be "MSTeams", "WebSite", "GitHub", etc. + /// Unique identifier. The format of the value is domain specific, so it can be a URL, a GUID, etc. + /// Optional title describing the entry, useful for external references when Text is empty. + /// Source text, available only when the memory is not an external source. + /// Search relevance, from 0 to 1, where 1 means perfect match. + public MemoryQueryResult( + bool isReference, + string sourceName, + string id, + string description, + string text, + double relevance) + { + this.IsReference = isReference; + this.ExternalSourceName = sourceName; + this.Id = id; + this.Description = description; + this.Text = text; + this.Relevance = relevance; + } + + internal static MemoryQueryResult FromMemoryRecord( + MemoryRecord rec, + double relevance) + { + return new MemoryQueryResult( + isReference: rec.IsReference, + sourceName: rec.ExternalSourceName, + id: rec.Id, + description: rec.Description, + text: rec.Text, + relevance); + } +} diff --git a/dotnet/src/SemanticKernel/Memory/MemoryRecord.cs b/dotnet/src/SemanticKernel/Memory/MemoryRecord.cs new file mode 100644 index 000000000000..49c2d0d41387 --- /dev/null +++ b/dotnet/src/SemanticKernel/Memory/MemoryRecord.cs @@ -0,0 +1,99 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.SemanticKernel.AI.Embeddings; + +namespace Microsoft.SemanticKernel.Memory; + +/// +/// IMPORTANT: this is a storage schema. Changing the fields will invalidate existing metadata stored in persistent vector DBs. +/// +internal class MemoryRecord : IEmbeddingWithMetadata +{ + /// + /// Whether the source data used to calculate embeddings are stored in the local + /// storage provider or is available through and external service, such as web site, MS Graph, etc. + /// + public bool IsReference { get; private set; } + + /// + /// A value used to understand which external service owns the data, to avoid storing the information + /// inside the URI. E.g. this could be "MSTeams", "WebSite", "GitHub", etc. + /// + public string ExternalSourceName { get; private set; } = string.Empty; + + /// + /// Unique identifier. The format of the value is domain specific, so it can be a URL, a GUID, etc. + /// + public string Id { get; private set; } = string.Empty; + + /// + /// Optional title describing the content. Note: the title is not indexed. + /// + public string Description { get; private set; } = string.Empty; + + /// + /// Source text, available only when the memory is not an external source. + /// + public string Text { get; private set; } = string.Empty; + + /// + /// Source content embeddings. + /// + public Embedding Embedding { get; private set; } + + /// + /// Prepare an instance about a memory which source is stored externally. + /// The universal resource identifies points to the URL (or equivalent) to find the original source. + /// + /// URL (or equivalent) to find the original source + /// Name of the external service, e.g. "MSTeams", "GitHub", "WebSite", "Outlook IMAP", etc. + /// Optional description of the record. Note: the description is not indexed. + /// Source content embeddings + /// Memory record + public static MemoryRecord ReferenceRecord( + string externalId, + string sourceName, + string? description, + Embedding embedding) + { + return new MemoryRecord + { + IsReference = true, + ExternalSourceName = sourceName, + Id = externalId, + Description = description ?? string.Empty, + Embedding = embedding + }; + } + + /// + /// Prepare an instance for a memory stored in the internal storage provider. + /// + /// Resource identifier within the storage provider, e.g. record ID/GUID/incremental counter etc. + /// Full text used to generate the embeddings + /// Optional description of the record. Note: the description is not indexed. + /// Source content embeddings + /// Memory record + public static MemoryRecord LocalRecord( + string id, + string text, + string? description, + Embedding embedding) + { + return new MemoryRecord + { + IsReference = false, + Id = id, + Text = text, + Description = description ?? string.Empty, + Embedding = embedding + }; + } + + /// + /// Block constructor, use or + /// + private MemoryRecord() + { + } +} diff --git a/dotnet/src/SemanticKernel/Memory/NullMemory.cs b/dotnet/src/SemanticKernel/Memory/NullMemory.cs new file mode 100644 index 000000000000..3ddc2b597a03 --- /dev/null +++ b/dotnet/src/SemanticKernel/Memory/NullMemory.cs @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.SemanticKernel.Memory; + +/// +/// Implementation of that stores nothing. +/// +public sealed class NullMemory : ISemanticTextMemory +{ + public static NullMemory Instance { get; } = new NullMemory(); + + /// + public Task SaveInformationAsync( + string collection, + string text, + string id, + string? description = null, + CancellationToken cancel = default) + { + return Task.CompletedTask; + } + + /// + public Task SaveReferenceAsync( + string collection, + string text, + string externalId, + string externalSourceName, + string? description = null, + CancellationToken cancel = default) + { + return Task.CompletedTask; + } + + /// + public Task GetAsync( + string collection, + string key, + CancellationToken cancel = default) + { + return Task.FromResult(null as MemoryQueryResult); + } + + /// + public IAsyncEnumerable SearchAsync( + string collection, + string query, + int limit = 1, + double minRelevanceScore = 0.7, + CancellationToken cancel = default) + { + return AsyncEnumerable.Empty(); + } + + /// + public Task> GetCollectionsAsync( + CancellationToken cancel = default) + { + return Task.FromResult(new List() as IList); + } + + private NullMemory() + { + } +} diff --git a/dotnet/src/SemanticKernel/Memory/SemanticTextMemory.cs b/dotnet/src/SemanticKernel/Memory/SemanticTextMemory.cs new file mode 100644 index 000000000000..f37e253a484b --- /dev/null +++ b/dotnet/src/SemanticKernel/Memory/SemanticTextMemory.cs @@ -0,0 +1,107 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.SemanticKernel.AI.Embeddings; +using Microsoft.SemanticKernel.Memory.Storage; + +namespace Microsoft.SemanticKernel.Memory; + +/// +/// Implementation of ./>. +/// +public sealed class SemanticTextMemory : ISemanticTextMemory, IDisposable +{ + private readonly IEmbeddingGenerator _embeddingGenerator; + private readonly IMemoryStore _storage; + + public SemanticTextMemory( + IMemoryStore storage, + IEmbeddingGenerator embeddingGenerator) + { + this._embeddingGenerator = embeddingGenerator; + this._storage = storage; + } + + /// + public async Task SaveInformationAsync( + string collection, + string text, + string id, + string? description = null, + CancellationToken cancel = default) + { + var embeddings = await this._embeddingGenerator.GenerateEmbeddingAsync(text); + MemoryRecord data = MemoryRecord.LocalRecord(id, text, description, embeddings); + + await this._storage.PutValueAsync(collection, key: id, value: data, cancel: cancel); + } + + /// + public async Task SaveReferenceAsync( + string collection, + string text, + string externalId, + string externalSourceName, + string? description = null, + CancellationToken cancel = default) + { + var embedding = await this._embeddingGenerator.GenerateEmbeddingAsync(text); + var data = MemoryRecord.ReferenceRecord(externalId: externalId, sourceName: externalSourceName, description, embedding); + + await this._storage.PutValueAsync(collection, key: externalId, value: data, cancel: cancel); + } + + /// + public async Task GetAsync( + string collection, + string key, + CancellationToken cancel = default) + { + DataEntry>? record = await this._storage.GetAsync(collection, key, cancel); + + if (record == null || record.Value == null || record.Value.Value == null) { return null; } + + MemoryRecord result = (MemoryRecord)(record.Value.Value); + + return MemoryQueryResult.FromMemoryRecord(result, 1); + } + + /// + public async IAsyncEnumerable SearchAsync( + string collection, + string query, + int limit = 1, + double minRelevanceScore = 0.7, + [EnumeratorCancellation] CancellationToken cancel = default) + { + Embedding queryEmbedding = await this._embeddingGenerator.GenerateEmbeddingAsync(query); + + IAsyncEnumerable<(IEmbeddingWithMetadata, double)> results = this._storage.GetNearestMatchesAsync( + collection, queryEmbedding, limit: limit, minRelevanceScore: minRelevanceScore); + + await foreach ((IEmbeddingWithMetadata, double) result in results.WithCancellation(cancel)) + { + yield return MemoryQueryResult.FromMemoryRecord((MemoryRecord)result.Item1, result.Item2); + } + } + + /// + public async Task> GetCollectionsAsync(CancellationToken cancel = default) + { + return await this._storage.GetCollectionsAsync(cancel).ToListAsync(cancel); + } + + public void Dispose() + { + // ReSharper disable once SuspiciousTypeConversion.Global + if (this._embeddingGenerator is IDisposable emb) { emb.Dispose(); } + + // ReSharper disable once SuspiciousTypeConversion.Global + if (this._storage is IDisposable storage) { storage.Dispose(); } + } +} diff --git a/dotnet/src/SemanticKernel/Memory/Storage/DataEntry.cs b/dotnet/src/SemanticKernel/Memory/Storage/DataEntry.cs new file mode 100644 index 000000000000..d5316287b92c --- /dev/null +++ b/dotnet/src/SemanticKernel/Memory/Storage/DataEntry.cs @@ -0,0 +1,300 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.SemanticKernel.Diagnostics; + +namespace Microsoft.SemanticKernel.Memory.Storage; + +/// +/// A struct containing properties for storage and retrieval of data. +/// +/// The data . +public struct DataEntry : IEquatable> +{ + /// + /// Creates an instance of a . + /// + /// The data key. + /// The data value. + /// The data timestamp. + [JsonConstructor] + public DataEntry(string key, TValue? value, DateTimeOffset? timestamp = null) + { + this.Key = key; + this.Value = value; + this.Timestamp = timestamp; + } + + /// + /// Gets the key of the data. + /// + [JsonPropertyName("key")] + public readonly string Key { get; } + + /// + /// Gets the value of the data. + /// + [JsonPropertyName("value")] + public TValue? Value { get; set; } + + /// + /// Gets the timestamp of the data. + /// + [JsonPropertyName("timestamp")] + public DateTimeOffset? Timestamp { get; set; } = null; + + /// + /// Gets the data value type. + /// + [JsonIgnore] + public Type ValueType => typeof(TValue); + + /// + /// true if the data has a value. + /// + [JsonIgnore] + public bool HasValue => (this.Value != null); + + /// + /// true if the data has a timestamp. + /// + [JsonIgnore] + public bool HasTimestamp => this.Timestamp.HasValue; + + /// + /// The as a . + /// + [JsonIgnore] + public string? ValueString + { + get + { + if (this.ValueType == typeof(string)) + { + return this.Value?.ToString(); + } + + if (this.Value != null) + { + return JsonSerializer.Serialize(this.Value); + } + + return null; + } + } + + /// + /// Compares two objects for equality. + /// + /// The to compare. + /// true if the specified object is equal to the current object; otherwise, false. + public bool Equals(DataEntry other) + { + return (other != null) + && (this.Key == other.Key) + && (this.Value?.Equals(other.Value) == true) + && (this.Timestamp == other.Timestamp); + } + + /// + /// Determines whether two object instances are equal. + /// + /// The object to compare with the current object. + /// true if the specified object is equal to the current object; otherwise, false. + public override bool Equals(object obj) + { + return (obj is DataEntry other) && this.Equals(other); + } + + /// + /// Serves as the default hash function. + /// + /// A hash code for the current object. + public override int GetHashCode() + { + return HashCode.Combine(this.Key, this.Value, this.Timestamp); + } + + /// + /// Returns a string that represents the current object. + /// + /// Returns a string that represents the current object. + public override string ToString() + { + return JsonSerializer.Serialize(this); + } + + /// + /// Parses a object from a serialized JSON string. + /// + /// The source JSON string. + /// The resulting , or null if empty. + /// true if parsing is successful, false otherwise. + [SuppressMessage("Design", "CA1000:Do not declare static members on generic types", Justification = "Parse type from string.")] + [SuppressMessage("Design", "CA1031:Modify to catch a more specific allowed exception type, or rethrow exception", + Justification = "Does not throw an exception by design.")] + public static bool TryParse(string json, [NotNullWhen(true)] out DataEntry? entry) + { + try + { + entry = JsonSerializer.Deserialize>(json); + return true; + } + catch + { + entry = default; + return false; + } + } + + /// + /// Compares two embeddings for equality. + /// + /// The left . + /// The right . + /// true if the embeddings contain identical data. + public static bool operator ==(DataEntry left, DataEntry right) + { + return left.Equals(right); + } + + /// + /// Compares two embeddings for inequality. + /// + /// The left . + /// The right . + /// true if the embeddings do not contain identical data. + public static bool operator !=(DataEntry left, DataEntry right) + { + return !(left == right); + } +} + +/// +/// Provides a collection of static methods for creating, manipulating, and otherwise operating on generic objects. +/// +public static class DataEntry +{ + /// + /// Creates a new object. + /// + /// The data value . + /// The storage key for the data. + /// The data value. + /// The data timestamp. + /// A object. + public static DataEntry Create(string key, TValue? value, DateTimeOffset? timestamp = null) + { + Verify.NotEmpty(key, "Data entry key cannot be NULL"); + + return new DataEntry(key, value, timestamp); + } + + /// + /// Creates a new object from a string value. + /// + /// The data value . + /// The storage key for the data. + /// The data value. + /// The data timestamp. + /// A object. + public static DataEntry Create(string key, string value, DateTimeOffset? timestamp = null) + { + Verify.NotEmpty(key, "Data entry key cannot be NULL"); + + TValue? valueObj = ParseValueAs(value); + return new DataEntry(key, valueObj, timestamp); + } + + /// + /// Parses a object from a serialized JSON string. + /// + /// The data value . + /// A JSON serialized string representing a . + /// Receives a object if successfully parsed. Null otherwise. + /// true if parsing is successful; false otherwise + public static bool TryParse(string json, [NotNullWhen(true)] out DataEntry? entry) + { + return DataEntry.TryParse(json, out entry); + } + + #region private ================================================================================ + + [SuppressMessage("Design", "CA1031:Modify to catch a more specific allowed exception type, or rethrow exception", + Justification = "Does not throw an exception by design.")] + internal static TCastTo? ParseValueAs(string? value) + { + if (!string.IsNullOrWhiteSpace(value)) + { + if (typeof(TCastTo) == typeof(string)) + { + // Force cast (we already know this is type string) + return (TCastTo)Convert.ChangeType(value, typeof(TCastTo), CultureInfo.InvariantCulture); + } + + return JsonSerializer.Deserialize(value); + } + + return default; + } + + // TODO: method never used + private static DataEntry? As(DataEntry data) + { + if (data == null) + { + return default; + } + + if (data.ValueType == typeof(TCastTo)) + { + // To and From types are the same. Just cast to satisfy the compiler. + return (DataEntry)Convert.ChangeType(data, typeof(DataEntry), CultureInfo.InvariantCulture); + } + + if (!data.HasValue) + { + // Data has no 'value' set. Create a new DataEntry with the desired type, and copy over the other properties. + return new DataEntry(data.Key, default, data.Timestamp); + } + + if (data.ValueType == typeof(string)) + { + // Convert from a string value data to another type. Try to deserialize value. + TCastTo? destinationValue = ParseValueAs(data.ValueString); + if (destinationValue == null) + { + // Cast failed. Return null DataEntry. + return default; + } + + return new DataEntry(data.Key, destinationValue, data.Timestamp); + } + + if (typeof(TCastTo) == typeof(string)) + { + // Convert from another value type to string. Use serialized value from ValueString. + // TODO: entry is never used + var entry = new DataEntry(data.Key, data.ValueString, data.Timestamp); + return (DataEntry)Convert.ChangeType(data, typeof(DataEntry), CultureInfo.InvariantCulture); + } + + // Converting between two non-string value types... see if there's a cast available + try + { + TCastTo destValue = (TCastTo)Convert.ChangeType(data.Value, typeof(TCastTo), CultureInfo.InvariantCulture); + return new DataEntry(data.Key, destValue, data.Timestamp); + } + catch (InvalidCastException) + { + // Cast failed. Return null DataEntry. + return default; + } + } + + #endregion +} diff --git a/dotnet/src/SemanticKernel/Memory/Storage/IDataStore.cs b/dotnet/src/SemanticKernel/Memory/Storage/IDataStore.cs new file mode 100644 index 000000000000..a3cdcdd1b2d5 --- /dev/null +++ b/dotnet/src/SemanticKernel/Memory/Storage/IDataStore.cs @@ -0,0 +1,106 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.SemanticKernel.Diagnostics; + +namespace Microsoft.SemanticKernel.Memory.Storage; + +/// +/// An interface for storing and retrieving objects. +/// +/// The type of data to be stored in this store. +public interface IDataStore +{ + /// + /// Gets a group of all available collection names. + /// + /// Cancellation token. + /// A group of collection names. + IAsyncEnumerable GetCollectionsAsync(CancellationToken cancel = default); + + /// + /// Gets all entries within a collection. + /// + /// Collection name. + /// Cancellation token. + /// A collection of data; empty if collection doesn't exist. + IAsyncEnumerable> GetAllAsync(string collection, CancellationToken cancel = default); + + /// + /// Gets a object from a collection and key. + /// + /// Collection name. + /// Item key. + /// Cancellation token. + /// The data, if found. Null otherwise. + Task?> GetAsync(string collection, string key, CancellationToken cancel = default); + + /// + /// Inserts a data entry. Updates if key is already present. + /// + /// Collection name. + /// The object to insert into the data store. + /// Cancellation token. + /// + Task> PutAsync(string collection, DataEntry data, CancellationToken cancel = default); + + /// + /// Removes a data entry from the store. + /// + /// Collection name. + /// Item key. + /// Cancellation token. + /// + Task RemoveAsync(string collection, string key, CancellationToken cancel = default); +}; + +/// +/// Common extension methods for objects. +/// +public static class DataStoreExtensions +{ + /// + /// Gets the data value from a collection and key. + /// + /// The data value type. + /// The data store. + /// The collection within the data store. + /// The key for the data within the collection. + /// Cancellation token. + /// The data value, if found. Null otherwise. + public static async Task GetValueAsync(this IDataStore store, string collection, string key, CancellationToken cancel = default) + { + Verify.NotNull(store, "Data store cannot be NULL"); + + DataEntry? dataEntry = await store.GetAsync(collection, key, cancel); + if (dataEntry != null) + { + return (dataEntry.Value).Value; + } + + return default; + } + + /// + /// Inserts a data entry. Updates if key is already present. + /// + /// The data value type. + /// The data store. + /// The collection within the data store. + /// The key for the data within the collection. + /// The data value. + /// The data timestamp. + /// Cancellation token. + public static Task> PutValueAsync(this IDataStore store, string collection, string key, TValue? value, + DateTimeOffset? timeStamp = null, + CancellationToken cancel = default) + { + Verify.NotNull(store, "Data store cannot be NULL"); + + DataEntry entry = DataEntry.Create(key, value, timeStamp); + return store.PutAsync(collection, entry, cancel); + } +} diff --git a/dotnet/src/SemanticKernel/Memory/Storage/VolatileDataStore.cs b/dotnet/src/SemanticKernel/Memory/Storage/VolatileDataStore.cs new file mode 100644 index 000000000000..c851314d22ac --- /dev/null +++ b/dotnet/src/SemanticKernel/Memory/Storage/VolatileDataStore.cs @@ -0,0 +1,101 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.SemanticKernel.Diagnostics; + +namespace Microsoft.SemanticKernel.Memory.Storage; + +/// +/// A simple volatile memory implementation of an with a backing . +/// +/// This is a transient data structure, the lifetime of which is controlled by the caller. +/// The data does not persist, and is not shared, between instances. +/// The type of data to be stored in this data store. +public class VolatileDataStore : IDataStore +{ + /// + public virtual IAsyncEnumerable GetCollectionsAsync(CancellationToken cancel = default) + { + return this._store.Keys.ToAsyncEnumerable(); + } + + /// + public virtual IAsyncEnumerable> GetAllAsync(string collection, CancellationToken cancel = default) + { + if (this.TryGetCollection(collection, out var collectionDict)) + { + return collectionDict.Values.ToAsyncEnumerable(); + } + + return AsyncEnumerable.Empty>(); + } + + /// + public virtual Task?> GetAsync(string collection, string key, CancellationToken cancel = default) + { + if (this.TryGetCollection(collection, out var collectionDict) + && collectionDict.TryGetValue(key, out var dataEntry)) + { + return Task.FromResult?>(dataEntry); + } + + return Task.FromResult?>(null); + } + + /// + public virtual Task> PutAsync(string collection, DataEntry data, CancellationToken cancel = default) + { + Verify.NotNull(data, "Data entry cannot be NULL"); + + if (this.TryGetCollection(collection, out var collectionDict, create: true)) + { + collectionDict[data.Key] = data; + } + + return Task.FromResult(data); + } + + /// + public virtual Task RemoveAsync(string collection, string key, CancellationToken cancel = default) + { + if (this.TryGetCollection(collection, out var collectionDict)) + { + collectionDict.Remove(key, out DataEntry _); + } + + return Task.CompletedTask; + } + + #region protected ================================================================================ + + protected bool TryGetCollection(string name, [NotNullWhen(true)] out ConcurrentDictionary>? collection, bool create = false) + { + if (this._store.TryGetValue(name, out collection)) + { + return true; + } + + if (create) + { + collection = new ConcurrentDictionary>(); + return this._store.TryAdd(name, collection); + } + + collection = null; + return false; + } + + #endregion + + #region private ================================================================================ + + private readonly ConcurrentDictionary>> _store = new(); + + #endregion +} diff --git a/dotnet/src/SemanticKernel/Memory/VolatileMemoryStore.cs b/dotnet/src/SemanticKernel/Memory/VolatileMemoryStore.cs new file mode 100644 index 000000000000..a69efcb9a1ef --- /dev/null +++ b/dotnet/src/SemanticKernel/Memory/VolatileMemoryStore.cs @@ -0,0 +1,132 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Linq; +using Microsoft.SemanticKernel.AI.Embeddings; +using Microsoft.SemanticKernel.AI.Embeddings.VectorOperations; +using Microsoft.SemanticKernel.Memory.Storage; + +namespace Microsoft.SemanticKernel.Memory; + +/// +/// A simple volatile memory embeddings store. +/// TODO: multiple enumerations +/// +/// Embedding type +public class VolatileMemoryStore : VolatileDataStore>, IMemoryStore + where TEmbedding : unmanaged +{ + /// + public IAsyncEnumerable<(IEmbeddingWithMetadata, double)> GetNearestMatchesAsync( + string collection, + Embedding embedding, + int limit = 1, + double minRelevanceScore = 0) + { + IEnumerable>>? embeddingCollection = null; + if (this.TryGetCollection(collection, out var collectionDict)) + { + embeddingCollection = collectionDict.Values; + } + + if (embeddingCollection == null || !embeddingCollection.Any()) + { + return AsyncEnumerable.Empty<(IEmbeddingWithMetadata, double)>(); + } + + EmbeddingReadOnlySpan embeddingSpan = new(embedding.AsReadOnlySpan()); + + TopNSortedList> sortedEmbeddings = new(limit); + foreach (var item in embeddingCollection) + { + if (item.Value != null) + { + EmbeddingReadOnlySpan itemSpan = new(item.Value.Embedding.AsReadOnlySpan()); + double similarity = embeddingSpan.CosineSimilarity(itemSpan); + if (similarity >= minRelevanceScore) + { + sortedEmbeddings.Add(similarity, item.Value); + } + } + } + + return sortedEmbeddings.Select(x => (x.Value, x.Key)).ToAsyncEnumerable(); + } + + #region private ================================================================================ + + /// + /// Calculates the cosine similarity between an and an + /// + /// The input to be compared. + /// The input to be compared. + /// A tuple consisting of the cosine similarity result. + private (IEmbeddingWithMetadata, double) PairEmbeddingWithSimilarity(Embedding embedding, + IEmbeddingWithMetadata embeddingWithData) + { + var similarity = embedding.Vector.ToArray().CosineSimilarity(embeddingWithData.Embedding.Vector.ToArray()); + return (embeddingWithData, similarity); + } + + /// + /// A sorted list that only keeps the top N items. + /// + /// + private class TopNSortedList : SortedList + { + /// + /// Creates an instance of the class. + /// + /// + public TopNSortedList(int maxSize) + : base(new DescendingDoubleComparer()) + { + this._maxSize = maxSize; + } + + /// + /// Adds a new item to the list. + /// + /// The item's score + /// The item's value + public new void Add(double score, T value) + { + if (this.Count >= this._maxSize) + { + if (score < this.Keys.Last()) + { + // If the key is less than the smallest key in the list, then we don't need to add it. + return; + } + + // Remove the smallest key. + this.RemoveAt(this.Count - 1); + } + + base.Add(score, value); + } + + private readonly int _maxSize; + + private class DescendingDoubleComparer : IComparer + { + public int Compare(double x, double y) + { + int compareResult = Comparer.Default.Compare(x, y); + + // Invert the result for descending order. + return 0 - compareResult; + } + } + } + + #endregion +} + +/// +/// Default constructor for a simple volatile memory embeddings store for embeddings. +/// The default embedding type is . +/// +public class VolatileMemoryStore : VolatileMemoryStore +{ +} diff --git a/dotnet/src/SemanticKernel/Orchestration/ContextVariables.cs b/dotnet/src/SemanticKernel/Orchestration/ContextVariables.cs new file mode 100644 index 000000000000..de7a592303ae --- /dev/null +++ b/dotnet/src/SemanticKernel/Orchestration/ContextVariables.cs @@ -0,0 +1,166 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections; +using System.Collections.Concurrent; +using System.Collections.Generic; +using Microsoft.SemanticKernel.Diagnostics; + +namespace Microsoft.SemanticKernel.Orchestration; + +/// +/// Context Variables is a data structure that holds temporary data while a task is being performed. +/// It is accessed and manipulated by functions in the pipeline. +/// +public sealed class ContextVariables : IEnumerable> +{ + /// + /// In the simplest scenario, the data is an input string, stored here. + /// + public string Input => this._variables[MainKey]; + + /// + /// Constructor for context variables. + /// + /// Optional value for the main variable of the context. + public ContextVariables(string content = "") + { + this._variables[MainKey] = content; + } + + /// + /// Updates the main input text with the new value after a function is complete. + /// + /// The new input value, for the next function in the pipeline, or as a result for the user + /// if the pipeline reached the end. + /// The current instance + public ContextVariables Update(string content) + { + this._variables[MainKey] = content; + return this; + } + + /// + /// Updates all the local data with new data, merging the two datasets. + /// Do not discard old data + /// + /// New data to be merged + /// Whether to merge and keep old data, or replace. False == discard old data. + /// The current instance + public ContextVariables Update(ContextVariables newData, bool merge = true) + { + // If requested, discard old data and keep only the new one. + if (!merge) { this._variables.Clear(); } + + foreach (KeyValuePair varData in newData._variables) + { + this._variables[varData.Key] = varData.Value; + } + + return this; + } + + /// + /// This method allows to store additional data in the context variables, e.g. variables needed by functions in the + /// pipeline. These "variables" are visible also to semantic functions using the "{{varName}}" syntax, allowing + /// to inject more information into prompt templates. + /// + /// Variable name + /// Value to store. If the value is NULL the variable is deleted. + /// TODO: support for more complex data types, and plan for rendering these values into prompt templates. + public void Set(string name, string? value) + { + Verify.NotEmpty(name, "The variable name is empty"); + if (value != null) + { + this._variables[name] = value; + } + else + { + this._variables.TryRemove(name, out _); + } + } + + /// + /// Fetch a variable value from the context variables. + /// + /// Variable name + /// Value + /// Whether the value exists in the context variables + /// TODO: provide additional method that returns the value without using 'out'. + public bool Get(string name, out string value) + { + if (this._variables.TryGetValue(name, out value!)) { return true; } + + value = string.Empty; + return false; + } + + /// + /// Array of all variables in the context variables. + /// + /// The name of the variable. + /// The value of the variable. + public string this[string name] + { + get => this._variables[name]; + set => this._variables[name] = value; + } + + /// + /// Returns true if there is a variable with the given name + /// + /// + /// True if there is a variable with the given name, false otherwise + public bool ContainsKey(string key) + { + return this._variables.ContainsKey(key); + } + + /// + /// Print the processed input, aka the current data after any processing occurred. + /// + /// Processed input, aka result + public override string ToString() + { + return this.Input; + } + + /// + /// Get an enumerator that iterates through the context variables. + /// + /// An enumerator that iterates through the context variables + public IEnumerator> GetEnumerator() + { + return this._variables.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return this._variables.GetEnumerator(); + } + + /// + /// Create a copy of the current instance with a copy of the internal data + /// + /// Copy of the current instance + public ContextVariables Clone() + { + var clone = new ContextVariables(); + foreach (KeyValuePair x in this._variables) + { + clone[x.Key] = x.Value; + } + + return clone; + } + + #region private ================================================================================ + + private const string MainKey = "INPUT"; + + // Important: names are case insensitive + private readonly ConcurrentDictionary _variables = new(StringComparer.InvariantCultureIgnoreCase); + + #endregion +} diff --git a/dotnet/src/SemanticKernel/Orchestration/Extensions/ContextVariablesExtensions.cs b/dotnet/src/SemanticKernel/Orchestration/Extensions/ContextVariablesExtensions.cs new file mode 100644 index 000000000000..53e0004b8a3a --- /dev/null +++ b/dotnet/src/SemanticKernel/Orchestration/Extensions/ContextVariablesExtensions.cs @@ -0,0 +1,104 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Text.Json; +using Microsoft.SemanticKernel.Planning; + +namespace Microsoft.SemanticKernel.Orchestration.Extensions; + +/// +/// Class that holds extension methods for ContextVariables. +/// +public static class ContextVariablesExtensions +{ + /// + /// Simple extension method to turn a string into a instance. + /// + /// The text to transform + /// An instance of + public static ContextVariables ToContextVariables(this string text) + { + return new ContextVariables(text); + } + + /// + /// Simple extension method to update a instance with a Plan instance. + /// + /// The variables to update + /// The Plan to update the with + /// The updated + public static ContextVariables UpdateWithPlanEntry(this ContextVariables vars, Plan plan) + { + vars.Update(plan.ToJson()); + vars.Set(Plan.IdKey, plan.Id); + vars.Set(Plan.GoalKey, plan.Goal); + vars.Set(Plan.PlanKey, plan.PlanString); + vars.Set(Plan.ArgumentsKey, plan.Arguments); + vars.Set(Plan.IsCompleteKey, plan.IsComplete.ToString()); + vars.Set(Plan.IsSuccessfulKey, plan.IsSuccessful.ToString()); + vars.Set(Plan.ResultKey, plan.Result); + vars.Set(Plan.ArgumentsKey, plan.Arguments); + + return vars; + } + + /// + /// Simple extension method to clear the PlanCreation entries from a instance. + /// + /// The to update + public static ContextVariables ClearPlan(this ContextVariables vars) + { + vars.Set(Plan.IdKey, null); + vars.Set(Plan.GoalKey, null); + vars.Set(Plan.PlanKey, null); + vars.Set(Plan.ArgumentsKey, null); + vars.Set(Plan.IsCompleteKey, null); + vars.Set(Plan.IsSuccessfulKey, null); + vars.Set(Plan.ResultKey, null); + return vars; + } + + /// + /// Simple extension method to parse a Plan instance from a instance. + /// + /// The to read + /// An instance of Plan + public static Plan ToPlan(this ContextVariables vars) + { + if (vars.Get(Plan.PlanKey, out string plan)) + { + vars.Get(Plan.ArgumentsKey, out string arguments); + vars.Get(Plan.IdKey, out string id); + vars.Get(Plan.GoalKey, out string goal); + vars.Get(Plan.IsCompleteKey, out string isComplete); + vars.Get(Plan.IsSuccessfulKey, out string isSuccessful); + vars.Get(Plan.ResultKey, out string result); + + return new Plan() + { + Id = id, + Goal = goal, + PlanString = plan, + Arguments = arguments, + IsComplete = !string.IsNullOrEmpty(isComplete) && bool.Parse(isComplete), + IsSuccessful = !string.IsNullOrEmpty(isSuccessful) && bool.Parse(isSuccessful), + Result = result + }; + } + + try + { + return Plan.FromJson(vars.ToString()); + } + catch (ArgumentNullException) + { + } + catch (JsonException) + { + } + + // If Plan.FromJson fails, return a Plan with the current as the plan. + // Validation of that `plan` will be done separately. + return new Plan() { PlanString = vars.ToString() }; + } +} diff --git a/dotnet/src/SemanticKernel/Orchestration/Extensions/SKContextExtensions.cs b/dotnet/src/SemanticKernel/Orchestration/Extensions/SKContextExtensions.cs new file mode 100644 index 000000000000..0c6c7173380d --- /dev/null +++ b/dotnet/src/SemanticKernel/Orchestration/Extensions/SKContextExtensions.cs @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft. All rights reserved. + +namespace Microsoft.SemanticKernel.Orchestration.Extensions; + +internal static class SKContextExtensions +{ + /// + /// Simple extension method to check if a function is registered in the SKContext. + /// + /// The SKContext to check + /// The skill name + /// The function name + /// The registered function, if found + internal static bool IsFunctionRegistered(this SKContext context, string skillName, string functionName, out ISKFunction? registeredFunction) + { + context.ThrowIfSkillCollectionNotSet(); + + if (context.Skills!.HasNativeFunction(skillName, functionName)) + { + registeredFunction = context.Skills.GetNativeFunction(skillName, functionName); + return true; + } + + if (context.Skills.HasNativeFunction(functionName)) + { + registeredFunction = context.Skills.GetNativeFunction(functionName); + return true; + } + + if (context.Skills.HasSemanticFunction(skillName, functionName)) + { + registeredFunction = context.Skills.GetSemanticFunction(skillName, functionName); + return true; + } + + registeredFunction = null; + return false; + } + + /// + /// Ensures the context has a skill collection available + /// + /// SK execution context + internal static void ThrowIfSkillCollectionNotSet(this SKContext context) + { + if (context.Skills == null) + { + throw new KernelException( + KernelException.ErrorCodes.SkillCollectionNotSet, + "Skill collection not found in the context"); + } + } +} diff --git a/dotnet/src/SemanticKernel/Orchestration/ISKFunction.cs b/dotnet/src/SemanticKernel/Orchestration/ISKFunction.cs new file mode 100644 index 000000000000..c19f3c274a55 --- /dev/null +++ b/dotnet/src/SemanticKernel/Orchestration/ISKFunction.cs @@ -0,0 +1,102 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel.AI; +using Microsoft.SemanticKernel.SkillDefinition; + +namespace Microsoft.SemanticKernel.Orchestration; + +/// +/// Semantic Kernel callable function interface +/// +public interface ISKFunction +{ + /// + /// Name of the function. The name is used by the skill collection and in prompt templates e.g. {{skillName.functionName}} + /// + string Name { get; } + + /// + /// Name of the skill containing the function. The name is used by the skill collection and in prompt templates e.g. {{skillName.functionName}} + /// + string SkillName { get; } + + /// + /// Function description. The description is used in combination with embeddings when searching relevant functions. + /// + string Description { get; } + + /// + /// Whether the function is defined using a prompt template. + /// IMPORTANT: native functions might use semantic functions internally, + /// so when this property is False, executing the function might still involve AI calls. + /// + public bool IsSemantic { get; } + + /// + /// AI backend settings + /// + public CompleteRequestSettings RequestSettings { get; } + + /// + /// Returns a description of the function, including parameters. + /// + /// An instance of describing the function + FunctionView Describe(); + + /// + /// Invoke the internal delegate with an explicit string input + /// + /// String input + /// SK context + /// LLM completion settings + /// Application logger + /// Cancellation token + /// The updated context, potentially a new one if context switching is implemented. + Task InvokeAsync( + string input, + SKContext? context = null, + CompleteRequestSettings? settings = null, + ILogger? log = null, + CancellationToken? cancel = null); + + /// + /// Invoke the internal delegate + /// + /// SK context + /// LLM completion settings + /// Application logger + /// Cancellation token + /// The updated context, potentially a new one if context switching is implemented. + Task InvokeAsync( + SKContext? context = null, + CompleteRequestSettings? settings = null, + ILogger? log = null, + CancellationToken? cancel = null); + + /// + /// Set the default skill collection to use when the function is invoked + /// without a context or with a context that doesn't have a collection. + /// + /// Kernel's skill collection + /// Self instance + ISKFunction SetDefaultSkillCollection(IReadOnlySkillCollection skills); + + /// + /// Set the AI backend used by the semantic function, passing a factory method. + /// The factory allows to lazily instantiate the client and to properly handle its disposal. + /// + /// AI backend factory + /// Self instance + ISKFunction SetAIBackend(Func backendFactory); + + /// + /// Set the AI completion settings used with LLM requests + /// + /// LLM completion settings + /// Self instance + ISKFunction SetAIConfiguration(CompleteRequestSettings settings); +} diff --git a/dotnet/src/SemanticKernel/Orchestration/SKContext.cs b/dotnet/src/SemanticKernel/Orchestration/SKContext.cs new file mode 100644 index 000000000000..6a0709d373f4 --- /dev/null +++ b/dotnet/src/SemanticKernel/Orchestration/SKContext.cs @@ -0,0 +1,143 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Threading; +using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel.Diagnostics; +using Microsoft.SemanticKernel.Memory; +using Microsoft.SemanticKernel.SkillDefinition; + +namespace Microsoft.SemanticKernel.Orchestration; + +/// +/// Semantic Kernel context. +/// +public sealed class SKContext +{ + /// + /// Print the processed input, aka the current data after any processing occurred. + /// + /// Processed input, aka result + public string Result => this.Variables.ToString(); + + // /// + // /// Whether an error occurred while executing functions in the pipeline. + // /// + // public bool ErrorOccurred => this.Variables.ErrorOccurred; + + /// + /// Whether an error occurred while executing functions in the pipeline. + /// + public bool ErrorOccurred { get; private set; } + + /// + /// Error details. + /// + public string LastErrorDescription { get; private set; } = string.Empty; + + /// + /// When an error occurs, this is the most recent exception. + /// + public Exception? LastException { get; private set; } + + /// + /// Cancellation token. + /// + public CancellationToken CancellationToken { get; } + + /// + /// Shortcut into user data, access variables by name + /// + /// Variable name + public string this[string name] + { + get => this.Variables[name]; + set => this.Variables[name] = value; + } + + /// + /// Call this method to signal when an error occurs. + /// In the usual scenarios this is also how execution is stopped, e.g. to inform the user or take necessary steps. + /// + /// Error description + /// If available, the exception occurred + /// The current instance + public SKContext Fail(string errorDescription, Exception? exception = null) + { + this.ErrorOccurred = true; + this.LastErrorDescription = errorDescription; + this.LastException = exception; + return this; + } + + /// + /// User variables + /// + public ContextVariables Variables { get; } + + /// + /// Semantic memory + /// + public ISemanticTextMemory Memory { get; internal set; } + + /// + /// Read only skills collection + /// + public IReadOnlySkillCollection? Skills { get; internal set; } + + /// + /// Access registered functions by skill + name. Not case sensitive. + /// The function might be native or semantic, it's up to the caller handling it. + /// + /// Skill name + /// Function name + /// Delegate to execute the function + public ISKFunction Func(string skillName, string functionName) + { + Verify.NotNull(this.Skills, "The skill collection hasn't been set"); + + if (this.Skills.HasNativeFunction(skillName, functionName)) + { + return this.Skills.GetNativeFunction(skillName, functionName); + } + + return this.Skills.GetSemanticFunction(skillName, functionName); + } + + /// + /// App logger + /// + public ILogger Log { get; } + + /// + /// Constructor for the context. + /// + /// Context variables to include in context. + /// Semantic text memory unit to include in context. + /// Skills to include in context. + /// Logger for operations in context. + /// Optional cancellation token for operations in context. + public SKContext( + ContextVariables variables, + ISemanticTextMemory memory, + IReadOnlySkillCollection? skills, + ILogger logger, + CancellationToken cancellationToken = default) + { + this.Variables = variables; + this.Memory = memory; + this.Skills = skills; + this.Log = logger; + this.CancellationToken = cancellationToken; + } + + /// + /// Print the processed input, aka the current data after any processing occurred. + /// If an error occurred, prints the last exception message instead. + /// + /// Processed input, aka result, or last exception message if any + public override string ToString() + { + return this.ErrorOccurred ? $"Error: {this.LastErrorDescription}" : this.Result; + } +} diff --git a/dotnet/src/SemanticKernel/Orchestration/SKFunction.cs b/dotnet/src/SemanticKernel/Orchestration/SKFunction.cs new file mode 100644 index 000000000000..6b6d2d15c160 --- /dev/null +++ b/dotnet/src/SemanticKernel/Orchestration/SKFunction.cs @@ -0,0 +1,712 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.SemanticKernel.AI; +using Microsoft.SemanticKernel.Diagnostics; +using Microsoft.SemanticKernel.Memory; +using Microsoft.SemanticKernel.SemanticFunctions; +using Microsoft.SemanticKernel.SkillDefinition; + +namespace Microsoft.SemanticKernel.Orchestration; + +/// +/// Standard Semantic Kernel callable function. +/// SKFunction is used to extend one C# , , , +/// with additional methods required by the kernel. +/// +public sealed class SKFunction : ISKFunction, IDisposable +{ + /// + public string Name { get; } + + /// + public string SkillName { get; } + + /// + public string Description { get; } + + /// + public bool IsSemantic { get; } + + /// + public CompleteRequestSettings RequestSettings { get; } = new(); + + /// + /// List of function parameters + /// + public IList Parameters { get; } + + /// + /// Create a native function instance, wrapping a native object method + /// + /// Object containing the method to invoke + /// Signature of the method to invoke + /// SK skill name + /// Application logger + /// SK function instance + public static ISKFunction? FromNativeMethod( + MethodInfo methodSignature, + object? methodContainerInstance = null, + string skillName = "", + ILogger? log = null) + { + if (string.IsNullOrWhiteSpace(skillName)) { skillName = SkillCollection.GlobalSkill; } + + MethodDetails methodDetails = GetMethodDetails(methodSignature, methodContainerInstance, log); + + // If the given method is not a valid SK function + if (!methodDetails.HasSkFunctionAttribute) + { + return null; + } + + return new SKFunction( + delegateType: methodDetails.Type, + delegateFunction: methodDetails.Function, + parameters: methodDetails.Parameters, + skillName: skillName, + functionName: methodDetails.Name, + description: methodDetails.Description, + isSemantic: false, + log: log); + } + + /// + /// Create a native function instance, given a semantic function configuration. + /// + /// Name of the skill to which the function to create belongs. + /// Name of the function to create. + /// Semantic function configuration. + /// Optional logger for the function. + /// SK function instance. + public static ISKFunction FromSemanticConfig( + string skillName, + string functionName, + SemanticFunctionConfig functionConfig, + ILogger? log = null) + { + Verify.NotNull(functionConfig, "Function configuration is empty"); + + async Task LocalFunc( + ITextCompletionClient client, + CompleteRequestSettings requestSettings, + SKContext executionContext) + { + Verify.NotNull(client, "AI LLM backed is empty"); + + try + { + string prompt = await functionConfig.PromptTemplate.RenderAsync(executionContext); + + string completion = await client.CompleteAsync(prompt, requestSettings); + executionContext.Variables.Update(completion); + } +#pragma warning disable CA1031 // We need to catch all exceptions to handle the execution state + catch (Exception e) when (!e.IsCriticalException()) + { + executionContext.Fail(e.Message, e); + } +#pragma warning restore CA1031 + + return executionContext; + } + + return new SKFunction( + delegateType: DelegateTypes.ContextSwitchInSKContextOutTaskSKContext, + delegateFunction: LocalFunc, + parameters: functionConfig.PromptTemplate.GetParameters(), + description: functionConfig.PromptTemplateConfig.Description, + skillName: skillName, + functionName: functionName, + isSemantic: true, + log: log); + } + + /// + public FunctionView Describe() + { + return new FunctionView + { + IsSemantic = this.IsSemantic, + Name = this.Name, + SkillName = this.SkillName, + Description = this.Description, + Parameters = this.Parameters, + }; + } + + /// + public Task InvokeAsync( + string input, + SKContext? context = null, + CompleteRequestSettings? settings = null, + ILogger? log = null, + CancellationToken? cancel = null) + { + if (context == null) + { + var cToken = cancel ?? default; + log ??= NullLogger.Instance; + context = new SKContext( + new ContextVariables(""), + NullMemory.Instance, + this._skillCollection, + log, + cToken); + } + + context.Variables.Update(input); + + return this.InvokeAsync(context, settings, log, cancel); + } + + /// + public Task InvokeAsync( + SKContext? context = null, + CompleteRequestSettings? settings = null, + ILogger? log = null, + CancellationToken? cancel = null) + { + if (context == null) + { + var cToken = cancel ?? default; + log ??= NullLogger.Instance; + context = new SKContext(new ContextVariables(""), NullMemory.Instance, null, log, cToken); + } + + return this.IsSemantic + ? this.InvokeSemanticAsync(context, settings) + : this.InvokeNativeAsync(context); + } + + /// + public ISKFunction SetDefaultSkillCollection(IReadOnlySkillCollection skills) + { + this._skillCollection = skills; + return this; + } + + /// + public ISKFunction SetAIBackend(Func backendFactory) + { + Verify.NotNull(backendFactory, "AI LLM backed factory is empty"); + this.VerifyIsSemantic(); + this._aiBackend = backendFactory.Invoke(); + return this; + } + + /// + public ISKFunction SetAIConfiguration(CompleteRequestSettings settings) + { + Verify.NotNull(settings, "AI LLM request settings are empty"); + this.VerifyIsSemantic(); + this._aiRequestSettings = settings; + return this; + } + + /// + /// Dispose of resources. + /// + public void Dispose() + { + this.ReleaseUnmanagedResources(); + GC.SuppressFinalize(this); + } + + /// + /// Finalizer. + /// + ~SKFunction() + { + this.ReleaseUnmanagedResources(); + } + + #region private + + private readonly DelegateTypes _delegateType; + private readonly Delegate _function; + private readonly ILogger _log; + private IReadOnlySkillCollection? _skillCollection; + private ITextCompletionClient? _aiBackend = null; + private CompleteRequestSettings _aiRequestSettings = new(); + + private struct MethodDetails + { + public bool HasSkFunctionAttribute { get; set; } + public DelegateTypes Type { get; set; } + public Delegate Function { get; set; } + public List Parameters { get; set; } + public string Name { get; set; } + public string Description { get; set; } + } + + private enum DelegateTypes + { + Unknown = 0, + Void = 1, + OutString = 2, + OutTaskString = 3, + InSKContext = 4, + InSKContextOutString = 5, + InSKContextOutTaskString = 6, + ContextSwitchInSKContextOutTaskSKContext = 7, + InString = 8, + InStringOutString = 9, + InStringOutTaskString = 10, + InStringAndContext = 11, + InStringAndContextOutString = 12, + InStringAndContextOutTaskString = 13, + ContextSwitchInStringAndContextOutTaskContext = 14, + InStringOutTask = 15, + InContextOutTask = 16, + InStringAndContextOutTask = 17, + OutTask = 18 + } + + private SKFunction( + DelegateTypes delegateType, + Delegate delegateFunction, + IList parameters, + string skillName, + string functionName, + string description, + bool isSemantic = false, + ILogger? log = null + ) + { + Verify.NotNull(delegateFunction, "The function delegate is empty"); + Verify.ValidSkillName(skillName); + Verify.ValidFunctionName(functionName); + Verify.ParametersUniqueness(parameters); + + this._log = log ?? NullLogger.Instance; + + this._delegateType = delegateType; + this._function = delegateFunction; + this.Parameters = parameters; + + this.IsSemantic = isSemantic; + this.Name = functionName; + this.SkillName = skillName; + this.Description = description; + } + + private void ReleaseUnmanagedResources() + { + if (this._aiBackend is not IDisposable disposable) { return; } + + disposable.Dispose(); + } + + /// + /// Throw an exception if the function is not semantic, use this method when some logic makes sense only for semantic functions. + /// + /// + private void VerifyIsSemantic() + { + if (this.IsSemantic) { return; } + + this._log.LogError("The function is not semantic"); + throw new KernelException( + KernelException.ErrorCodes.InvalidFunctionType, + "Invalid operation, the method requires a semantic function"); + } + + // Run the semantic function + private async Task InvokeSemanticAsync(SKContext context, CompleteRequestSettings? settings) + { + this.VerifyIsSemantic(); + + this.EnsureContextHasSkills(context); + + settings ??= this._aiRequestSettings; + + var callable = (Func>)this._function; + context.Variables.Update((await callable(this._aiBackend, settings, context)).Variables); + return context; + } + + // Run the native function + private async Task InvokeNativeAsync(SKContext context) + { + TraceFunctionTypeCall(this._delegateType, this._log); + + this.EnsureContextHasSkills(context); + + switch (this._delegateType) + { + case DelegateTypes.Void: // 1 + { + var callable = (Action)this._function; + callable(); + return context; + } + + case DelegateTypes.OutString: // 2 + { + var callable = (Func)this._function; + context.Variables.Update(callable()); + return context; + } + + case DelegateTypes.OutTaskString: // 3 + { + var callable = (Func>)this._function; + context.Variables.Update(await callable()); + return context; + } + + case DelegateTypes.InSKContext: // 4 + { + var callable = (Action)this._function; + callable(context); + return context; + } + + case DelegateTypes.InSKContextOutString: // 5 + { + var callable = (Func)this._function; + context.Variables.Update(callable(context)); + return context; + } + + case DelegateTypes.InSKContextOutTaskString: // 6 + { + var callable = (Func>)this._function; + context.Variables.Update(await callable(context)); + return context; + } + + case DelegateTypes.ContextSwitchInSKContextOutTaskSKContext: // 7 + { + var callable = (Func>)this._function; + // Note: Context Switching: allows the function to replace with a new context, e.g. to branch execution path + context = await callable(context); + return context; + } + + case DelegateTypes.InString: + { + var callable = (Action)this._function; // 8 + callable(context.Variables.Input); + return context; + } + + case DelegateTypes.InStringOutString: // 9 + { + var callable = (Func)this._function; + context.Variables.Update(callable(context.Variables.Input)); + return context; + } + + case DelegateTypes.InStringOutTaskString: // 10 + { + var callable = (Func>)this._function; + context.Variables.Update(await callable(context.Variables.Input)); + return context; + } + + case DelegateTypes.InStringAndContext: // 11 + { + var callable = (Action)this._function; + callable(context.Variables.Input, context); + return context; + } + + case DelegateTypes.InStringAndContextOutString: // 12 + { + var callable = (Func)this._function; + context.Variables.Update(callable(context.Variables.Input, context)); + return context; + } + + case DelegateTypes.InStringAndContextOutTaskString: // 13 + { + var callable = (Func>)this._function; + context.Variables.Update(await callable(context.Variables.Input, context)); + return context; + } + + case DelegateTypes.ContextSwitchInStringAndContextOutTaskContext: // 14 + { + var callable = (Func>)this._function; + // Note: Context Switching: allows the function to replace with a new context, e.g. to branch execution path + context = await callable(context.Variables.Input, context); + return context; + } + + case DelegateTypes.InStringOutTask: // 15 + { + var callable = (Func)this._function; + await callable(context.Variables.Input); + return context; + } + + case DelegateTypes.InContextOutTask: // 16 + { + var callable = (Func)this._function; + await callable(context); + return context; + } + + case DelegateTypes.InStringAndContextOutTask: // 17 + { + var callable = (Func)this._function; + await callable(context.Variables.Input, context); + return context; + } + + case DelegateTypes.OutTask: // 18 + { + var callable = (Func)this._function; + await callable(); + return context; + } + + case DelegateTypes.Unknown: + default: + throw new KernelException( + KernelException.ErrorCodes.FunctionTypeNotSupported, + "Invalid function type detected, unable to execute."); + } + } + + private void EnsureContextHasSkills(SKContext context) + { + // If the function is invoked manually, the user might have left out the skill collection + if (context.Skills == null) { context.Skills = this._skillCollection; } + } + + private static MethodDetails GetMethodDetails(MethodInfo methodSignature, object? methodContainerInstance, ILogger? log = null) + { + Verify.NotNull(methodSignature, "Method is NULL"); + + var result = new MethodDetails + { + Name = methodSignature.Name, + Parameters = new List(), + }; + + // SKFunction attribute + SKFunctionAttribute? skFunctionAttribute = methodSignature + .GetCustomAttributes(typeof(SKFunctionAttribute), true) + .Cast() + .FirstOrDefault(); + + result.HasSkFunctionAttribute = skFunctionAttribute != null; + + if (!result.HasSkFunctionAttribute || skFunctionAttribute == null) + { + log?.LogTrace("Method {0} doesn't have SKFunctionAttribute", result.Name); + return result; + } + + result.HasSkFunctionAttribute = true; + + (result.Type, result.Function, bool hasStringParam) = GetDelegateInfo(methodContainerInstance, methodSignature); + + // SKFunctionName attribute + SKFunctionNameAttribute? skFunctionNameAttribute = methodSignature + .GetCustomAttributes(typeof(SKFunctionNameAttribute), true) + .Cast() + .FirstOrDefault(); + + if (skFunctionNameAttribute != null) + { + result.Name = skFunctionNameAttribute.Name; + } + + // SKFunctionInput attribute + SKFunctionInputAttribute? skMainParam = methodSignature + .GetCustomAttributes(typeof(SKFunctionInputAttribute), true) + .Cast() + .FirstOrDefault(); + + // SKFunctionContextParameter attribute + IList skContextParams = methodSignature + .GetCustomAttributes(typeof(SKFunctionContextParameterAttribute), true) + .Cast().ToList(); + + // Handle main string param description, if available/valid + // Note: Using [SKFunctionInput] is optional + if (hasStringParam) + { + result.Parameters.Add(skMainParam != null + ? skMainParam.ToParameterView() // Use the developer description + : new ParameterView { Name = "input", Description = "Input string", DefaultValue = "" }); // Use a default description + } + else if (skMainParam != null) + { + // The developer used [SKFunctionInput] on a function that doesn't support a string input + throw new KernelException( + KernelException.ErrorCodes.InvalidFunctionDescription, + "The function doesn't have a string parameter, do not use " + typeof(SKFunctionInputAttribute)); + } + + // Handle named arg passed via the SKContext object + // Note: "input" is added first to the list, before context params + // Note: Using [SKFunctionContextParameter] is optional + result.Parameters.AddRange(skContextParams.Select(x => x.ToParameterView())); + + // Check for param names conflict + // Note: the name "input" is reserved for the main parameter + Verify.ParametersUniqueness(result.Parameters); + + result.Description = skFunctionAttribute.Description ?? ""; + + log?.LogTrace("Method {0} found", result.Name); + + return result; + } + + // Inspect a method and returns the corresponding delegate and related info + private static (DelegateTypes type, Delegate function, bool hasStringParam) GetDelegateInfo(object? instance, MethodInfo method) + { + if (EqualMethods(instance, method, typeof(Action), out Delegate? funcDelegate)) + { + return (DelegateTypes.Void, funcDelegate, false); + } + + if (EqualMethods(instance, method, typeof(Func), out funcDelegate)) + { + return (DelegateTypes.OutString, funcDelegate, false); + } + + if (EqualMethods(instance, method, typeof(Func>), out funcDelegate!)) + { + return (DelegateTypes.OutTaskString, funcDelegate, false); + } + + if (EqualMethods(instance, method, typeof(Action), out funcDelegate!)) + { + return (DelegateTypes.InSKContext, funcDelegate, false); + } + + if (EqualMethods(instance, method, typeof(Func), out funcDelegate!)) + { + return (DelegateTypes.InSKContextOutString, funcDelegate, false); + } + + if (EqualMethods(instance, method, typeof(Func>), out funcDelegate!)) + { + return (DelegateTypes.InSKContextOutTaskString, funcDelegate, false); + } + + if (EqualMethods(instance, method, typeof(Func>), out funcDelegate!)) + { + return (DelegateTypes.ContextSwitchInSKContextOutTaskSKContext, funcDelegate, false); + } + + // === string input == + + if (EqualMethods(instance, method, typeof(Action), out funcDelegate!)) + { + return (DelegateTypes.InString, funcDelegate, true); + } + + if (EqualMethods(instance, method, typeof(Func), out funcDelegate!)) + { + return (DelegateTypes.InStringOutString, funcDelegate, true); + } + + if (EqualMethods(instance, method, typeof(Func>), out funcDelegate!)) + { + return (DelegateTypes.InStringOutTaskString, funcDelegate, true); + } + + if (EqualMethods(instance, method, typeof(Action), out funcDelegate!)) + { + return (DelegateTypes.InStringAndContext, funcDelegate, true); + } + + if (EqualMethods(instance, method, typeof(Func), out funcDelegate!)) + { + return (DelegateTypes.InStringAndContextOutString, funcDelegate, true); + } + + if (EqualMethods(instance, method, typeof(Func>), out funcDelegate!)) + { + return (DelegateTypes.InStringAndContextOutTaskString, funcDelegate, true); + } + + if (EqualMethods(instance, method, typeof(Func>), out funcDelegate!)) + { + return (DelegateTypes.ContextSwitchInStringAndContextOutTaskContext, funcDelegate, true); + } + + // == Tasks without output == + + if (EqualMethods(instance, method, typeof(Func), out funcDelegate!)) + { + return (DelegateTypes.InStringOutTask, funcDelegate, true); + } + + if (EqualMethods(instance, method, typeof(Func), out funcDelegate!)) + { + return (DelegateTypes.InContextOutTask, funcDelegate, false); + } + + if (EqualMethods(instance, method, typeof(Func), out funcDelegate!)) + { + return (DelegateTypes.InStringAndContextOutTask, funcDelegate, true); + } + + if (EqualMethods(instance, method, typeof(Func), out funcDelegate!)) + { + return (DelegateTypes.OutTask, funcDelegate, false); + } + + // [SKContext DoSomething(SKContext context)] is not supported, use the async form instead. + // If you encounter scenarios that require to interact with the context synchronously, please let us know. + if (EqualMethods(instance, method, typeof(Func), out _)) + { + throw new KernelException( + KernelException.ErrorCodes.FunctionTypeNotSupported, + $"Function {method.Name} has an invalid signature 'Func'. " + + "Please use 'Func>' instead."); + } + + throw new KernelException( + KernelException.ErrorCodes.FunctionTypeNotSupported, + $"Function {method.Name} has an invalid signature not supported by the kernel"); + } + + [SuppressMessage("Maintainability", "CA1508:Avoid dead conditional code", Justification = "Delegate.CreateDelegate result can be null")] + private static bool EqualMethods( + object? instance, + MethodInfo userMethod, + Type delegateDefinition, + [NotNullWhen(true)] out Delegate? result) + { + // Instance methods + if (instance != null) + { + result = Delegate.CreateDelegate(delegateDefinition, instance, userMethod, false); + if (result != null) { return true; } + } + + // Static methods + result = Delegate.CreateDelegate(delegateDefinition, userMethod, false); + + return result != null; + } + + // Internal event to count (and test) that the correct delegates are invoked + private static void TraceFunctionTypeCall(DelegateTypes type, ILogger log) + { + log.Log( + LogLevel.Trace, + new EventId((int)type, $"FuncType{type}"), + "Executing function type {0}: {1}", (int)type, type.ToString("G")); + } + + #endregion +} diff --git a/dotnet/src/SemanticKernel/Orchestration/SKFunctionExtensions.cs b/dotnet/src/SemanticKernel/Orchestration/SKFunctionExtensions.cs new file mode 100644 index 000000000000..05ba3777f8a3 --- /dev/null +++ b/dotnet/src/SemanticKernel/Orchestration/SKFunctionExtensions.cs @@ -0,0 +1,120 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel.AI; +using Microsoft.SemanticKernel.Diagnostics; +using Microsoft.SemanticKernel.Memory; +using Microsoft.SemanticKernel.SkillDefinition; + +namespace Microsoft.SemanticKernel.Orchestration; + +/// +/// Class that holds extension methods for objects implementing ISKFunction. +/// +public static class SKFunctionExtensions +{ + /// + /// Configure the LLM settings used by semantic function. + /// + /// Semantic function + /// Completion settings + /// Self instance + public static ISKFunction UseCompletionSettings(this ISKFunction skFunction, CompleteRequestSettings settings) + { + return skFunction.SetAIConfiguration(settings); + } + + /// + /// Change the LLM Max Tokens configuration + /// + /// Semantic function + /// Tokens count + /// Self instance + public static ISKFunction UseMaxTokens(this ISKFunction skFunction, int maxTokens) + { + skFunction.RequestSettings.MaxTokens = maxTokens; + return skFunction; + } + + /// + /// Change the LLM Temperature configuration + /// + /// Semantic function + /// Temperature value + /// Self instance + public static ISKFunction UseTemperature(this ISKFunction skFunction, double temperature) + { + skFunction.RequestSettings.Temperature = temperature; + return skFunction; + } + + /// + /// Change the Max Tokens configuration + /// + /// Semantic function + /// TopP value + /// Self instance + public static ISKFunction UseTopP(this ISKFunction skFunction, double topP) + { + skFunction.RequestSettings.TopP = topP; + return skFunction; + } + + /// + /// Change the Max Tokens configuration + /// + /// Semantic function + /// Presence penalty value + /// Self instance + public static ISKFunction UsePresencePenalty(this ISKFunction skFunction, double presencePenalty) + { + skFunction.RequestSettings.PresencePenalty = presencePenalty; + return skFunction; + } + + /// + /// Change the Max Tokens configuration + /// + /// Semantic function + /// Frequency penalty value + /// Self instance + public static ISKFunction UseFrequencyPenalty(this ISKFunction skFunction, double frequencyPenalty) + { + skFunction.RequestSettings.FrequencyPenalty = frequencyPenalty; + return skFunction; + } + + /// + /// Execute a function with a custom set of context variables. + /// Use case: template engine: semantic function with custom input variable. + /// + /// Function to execute + /// Custom function input + /// Semantic memory + /// Available skills + /// App logger + /// Cancellation token + /// The temporary context + public static async Task InvokeWithCustomInputAsync(this ISKFunction function, + ContextVariables input, + ISemanticTextMemory memory, + IReadOnlySkillCollection? skills, + ILogger log, + CancellationToken cancellationToken) + { + var tmpContext = new SKContext(input, memory, skills, log, cancellationToken); + try + { + await function.InvokeAsync(tmpContext); + } + catch (Exception ex) when (!ex.IsCriticalException()) + { + tmpContext.Fail(ex.Message, ex); + } + + return tmpContext; + } +} diff --git a/dotnet/src/SemanticKernel/Planning/FunctionFlowRunner.cs b/dotnet/src/SemanticKernel/Planning/FunctionFlowRunner.cs new file mode 100644 index 000000000000..ece51505d0a4 --- /dev/null +++ b/dotnet/src/SemanticKernel/Planning/FunctionFlowRunner.cs @@ -0,0 +1,282 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Xml; +using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel.Diagnostics; +using Microsoft.SemanticKernel.Orchestration; +using Microsoft.SemanticKernel.Orchestration.Extensions; + +namespace Microsoft.SemanticKernel.Planning; + +/// +/// Executes XML plans created by the Function Flow semantic function. +/// +internal class FunctionFlowRunner +{ + /// + /// The tag name used in the plan xml for the user's goal/ask. + /// + internal const string GoalTag = "goal"; + + /// + /// The tag name used in the plan xml for the solution. + /// + internal const string SolutionTag = "plan"; + + /// + /// The tag name used in the plan xml for a step that calls a skill function. + /// + internal const string FunctionTag = "function."; + + /// + /// The attribute tag used in the plan xml for setting the context variable name to set the output of a function to. + /// + internal const string SetContextVariableTag = "setContextVariable"; + + /// + /// The attribute tag used in the plan xml for appending the output of a function to the final result for a plan. + /// + internal const string AppendToResultTag = "appendToResult"; + + internal const string OutputTag = "output"; + + private readonly IKernel _kernel; + + public FunctionFlowRunner(IKernel kernel) + { + this._kernel = kernel; + } + + /// + /// Executes the next step of a plan xml. + /// + /// The context to execute the plan in. + /// The plan xml. + /// The resulting plan xml after executing a step in the plan. + /// + /// Brief overview of how it works: + /// 1. The plan xml is parsed into an XmlDocument. + /// 2. The Goal node is extracted from the plan xml. + /// 3. The Solution node is extracted from the plan xml. + /// 4. The first function node in the Solution node is processed. + /// 5. The resulting plan xml is returned. + /// + /// Thrown when the plan xml is invalid. + public async Task ExecuteXmlPlanAsync(SKContext context, string planPayload) + { + try + { + XmlDocument xmlDoc = new(); + try + { + xmlDoc.LoadXml("" + planPayload + ""); + } + catch (XmlException e) + { + throw new PlanningException(PlanningException.ErrorCodes.InvalidPlan, "Failed to parse plan xml.", e); + } + + // Get the Goal + var (goalTxt, goalXmlString) = GatherGoal(xmlDoc); + + // Get the Solution + XmlNodeList solution = xmlDoc.GetElementsByTagName(SolutionTag); + + // Prepare content for the new plan xml + var solutionContent = new StringBuilder(); + solutionContent.AppendLine($"<{SolutionTag}>"); + + // Use goal as default function {{INPUT}} -- check and see if it's a plan in Input, if so, use goalTxt, otherwise, use the input. + if (!context.Variables.Get("PLAN__INPUT", out var planInput)) + { + // planInput should then be the context.Variables.ToString() only if it's not a plan json + try + { + var plan = Plan.FromJson(context.Variables.ToString()); + planInput = string.IsNullOrEmpty(plan.Goal) ? context.Variables.ToString() : goalTxt; + } + catch (Exception e) when (!e.IsCriticalException()) + { + planInput = context.Variables.ToString(); + } + } + + string functionInput = string.IsNullOrEmpty(planInput) ? goalTxt : planInput; + + // + // Process Solution nodes + // + context.Log.LogDebug("Processing solution"); + + // Process the solution nodes + string stepResults = await this.ProcessNodeListAsync(solution, functionInput, context); + // Add the solution and variable updates to the new plan xml + solutionContent.Append(stepResults) + .AppendLine($""); + // Update the plan xml + var updatedPlan = goalXmlString + solutionContent.Replace("\r\n", "\n"); + updatedPlan = updatedPlan.Trim(); + + context.Variables.Set(Plan.PlanKey, updatedPlan); + context.Variables.Set("PLAN__INPUT", context.Variables.ToString()); + + return context; + } + catch (Exception e) when (!e.IsCriticalException()) + { + context.Log.LogError(e, "Plan execution failed: {0}", e.Message); + throw; + } + } + + private async Task ProcessNodeListAsync(XmlNodeList nodeList, string functionInput, SKContext context) + { + var stepAndTextResults = new StringBuilder(); + var processFunctions = true; + const string INDENT = " "; + foreach (XmlNode o in nodeList) + { + if (o == null) + { + continue; + } + + var parentNodeName = o.Name; + + context.Log.LogTrace("{0}: found node", parentNodeName); + foreach (XmlNode o2 in o.ChildNodes) + { + if (o2.Name == "#text") + { + context.Log.LogTrace("{0}: appending text node", parentNodeName); + if (o2.Value != null) + { + stepAndTextResults.AppendLine(o2.Value.Trim()); + } + + continue; + } + + if (o2.Name.StartsWith(FunctionTag, StringComparison.InvariantCultureIgnoreCase)) + { + var skillFunctionName = o2.Name.Split(FunctionTag)?[1] ?? string.Empty; + context.Log.LogTrace("{0}: found skill node {1}", parentNodeName, skillFunctionName); + GetSkillFunctionNames(skillFunctionName, out var skillName, out var functionName); + if (processFunctions && !string.IsNullOrEmpty(functionName) && context.IsFunctionRegistered(skillName, functionName, out var skillFunction)) + { + Verify.NotNull(functionName, nameof(functionName)); + Verify.NotNull(skillFunction, nameof(skillFunction)); + context.Log.LogTrace("{0}: processing function {1}.{2}", parentNodeName, skillName, functionName); + + var functionVariables = new ContextVariables(functionInput); + var variableTargetName = string.Empty; + var appendToResultName = string.Empty; + if (o2.Attributes is not null) + { + foreach (XmlAttribute attr in o2.Attributes) + { + context.Log.LogTrace("{0}: processing attribute {1}", parentNodeName, attr.ToString()); + if (attr.InnerText.StartsWith("$", StringComparison.InvariantCultureIgnoreCase)) + { + // TODO support lists of parameters like $param1,$param2 or $param1;$param2 + if (context.Variables.Get(attr.InnerText[1..], out var variableReplacement)) + { + functionVariables.Set(attr.Name, variableReplacement); + } + else if (attr.InnerText[1..].Equals(OutputTag, StringComparison.OrdinalIgnoreCase)) + { + // skip + } + } + else if (attr.Name.Equals(SetContextVariableTag, StringComparison.OrdinalIgnoreCase)) + { + variableTargetName = attr.InnerText; + } + else if (attr.Name.Equals(AppendToResultTag, StringComparison.OrdinalIgnoreCase)) + { + appendToResultName = attr.InnerText; + } + else + { + functionVariables.Set(attr.Name, attr.InnerText); + } + } + } + + // capture current keys before running function + var keysToIgnore = functionVariables.Select(x => x.Key).ToList(); + + var result = await this._kernel.RunAsync(functionVariables, skillFunction); + + // If skillFunction is BucketOutputsAsync result + // we need to pass those things back out to context.Variables + if (skillFunctionName.Contains("BucketOutputs", StringComparison.InvariantCultureIgnoreCase)) + { + // copy all values for VariableNames in functionVariables not in keysToIgnore to context.Variables + foreach (var (key, _) in functionVariables) + { + if (!keysToIgnore.Contains(key, StringComparer.InvariantCultureIgnoreCase) && functionVariables.Get(key, out var value)) + { + context.Variables.Set(key, value); + } + } + } + + context.Variables.Update(result.ToString()); + if (!string.IsNullOrEmpty(variableTargetName)) + { + context.Variables.Set(variableTargetName, result.ToString()); + } + + if (!string.IsNullOrEmpty(appendToResultName)) + { + context.Variables.Get(Plan.ResultKey, out var resultsSoFar); + context.Variables.Set(Plan.ResultKey, + string.Join(Environment.NewLine + Environment.NewLine, resultsSoFar, appendToResultName, result.ToString())); + } + + processFunctions = false; + } + else + { + context.Log.LogTrace("{0}: appending function node {1}", parentNodeName, skillFunctionName); + stepAndTextResults.Append(INDENT).AppendLine(o2.OuterXml); + } + + continue; + } + + stepAndTextResults.Append(INDENT).AppendLine(o2.OuterXml); + } + } + + return stepAndTextResults.Replace("\r\n", "\n").ToString(); + } + + private static (string goalTxt, string goalXmlString) GatherGoal(XmlDocument xmlDoc) + { + XmlNodeList goal = xmlDoc.GetElementsByTagName(GoalTag); + if (goal.Count == 0) + { + throw new PlanningException(PlanningException.ErrorCodes.InvalidPlan, "No goal found."); + } + + string goalTxt = goal[0]!.FirstChild!.Value ?? string.Empty; + var goalContent = new StringBuilder(); + goalContent.Append($"<{GoalTag}>") + .Append(goalTxt) + .AppendLine($""); + return (goalTxt.Trim(), goalContent.Replace("\r\n", "\n").ToString().Trim()); + } + + private static void GetSkillFunctionNames(string skillFunctionName, out string skillName, out string functionName) + { + var skillFunctionNameParts = skillFunctionName.Split("."); + skillName = skillFunctionNameParts?.Length > 0 ? skillFunctionNameParts[0] : string.Empty; + functionName = skillFunctionNameParts?.Length > 1 ? skillFunctionNameParts[1] : skillFunctionName; + } +} diff --git a/dotnet/src/SemanticKernel/Planning/Plan.cs b/dotnet/src/SemanticKernel/Planning/Plan.cs new file mode 100644 index 000000000000..0caabfa0c43a --- /dev/null +++ b/dotnet/src/SemanticKernel/Planning/Plan.cs @@ -0,0 +1,109 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Microsoft.SemanticKernel.Planning; + +/// +/// Object that contains details about a plan and its goal and details of its execution. +/// +public class Plan +{ + /// + /// Internal constant string representing the ID key. + /// + internal const string IdKey = "PLAN__ID"; + + /// + /// Internal constant string representing the goal key. + /// + internal const string GoalKey = "PLAN__GOAL"; + + /// + /// Internal constant string representing the plan key. + /// + internal const string PlanKey = "PLAN__PLAN"; + + /// + /// Internal constant string representing the arguments key. + /// + internal const string ArgumentsKey = "PLAN__ARGUMENTS"; + + /// + /// Internal constant string representing the is complete key. + /// + internal const string IsCompleteKey = "PLAN__ISCOMPLETE"; + + /// + /// Internal constant string representing the is successful key. + /// + internal const string IsSuccessfulKey = "PLAN__ISSUCCESSFUL"; + + /// + /// Internal constant string representing the result key. + /// + internal const string ResultKey = "PLAN__RESULT"; + + /// + /// The ID of the plan. + /// Can be used to track creation of a plan and execution over multiple steps. + /// + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; + + /// + /// The goal of the plan. + /// + [JsonPropertyName("goal")] + public string Goal { get; set; } = string.Empty; + + /// + /// The plan details in string. + /// + [JsonPropertyName("plan")] + public string PlanString { get; set; } = string.Empty; + + /// + /// The arguments for the plan. + /// + [JsonPropertyName("arguments")] + public string Arguments { get; set; } = string.Empty; + + /// + /// Flag indicating if the plan is complete. + /// + [JsonPropertyName("is_complete")] + public bool IsComplete { get; set; } + + /// + /// Flag indicating if the plan is successful. + /// + [JsonPropertyName("is_successful")] + public bool IsSuccessful { get; set; } + + /// + /// The result of the plan execution. + /// + [JsonPropertyName("result")] + public string Result { get; set; } = string.Empty; + + /// + /// To help with writing plans to . + /// + /// JSON string representation of the Plan + public string ToJson() + { + return JsonSerializer.Serialize(this); + } + + /// + /// To help with reading plans from . + /// + /// JSON string representation of aPlan + /// An instance of a Plan object. + public static Plan FromJson(string json) + { + return JsonSerializer.Deserialize(json) ?? new Plan(); + } +} diff --git a/dotnet/src/SemanticKernel/Planning/PlanRunner.cs b/dotnet/src/SemanticKernel/Planning/PlanRunner.cs new file mode 100644 index 000000000000..5ae058682170 --- /dev/null +++ b/dotnet/src/SemanticKernel/Planning/PlanRunner.cs @@ -0,0 +1,183 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Text; +using System.Threading.Tasks; +using System.Xml; +using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel.Diagnostics; +using Microsoft.SemanticKernel.Orchestration; + +namespace Microsoft.SemanticKernel.Planning; + +/// +/// Executes plans created by the PlannerSkill/ProblemSolver semantic function. +/// +internal class PlanRunner +{ + /// + /// The tag name used in the plan xml for the user's goal/ask. + /// + private const string GoalTag = "goal"; + + /// + /// The tag name used in the plan xml for the solution. + /// + private const string SolutionTag = "solution"; + + /// + /// The tag name used in the plan xml for a generic step. + /// + private const string StepTag = "step"; + + /// + /// The tag name used in the plan xml for the context variables. + /// + private const string VariablesTag = "variables"; + + private readonly IKernel _kernel; + + public PlanRunner(IKernel kernel) + { + this._kernel = kernel; + } + + /// + /// Executes the next step of a plan xml. + /// + /// The context to execute the plan in. + /// The plan xml. + /// The default step executor to use if a step does not have a skill function. + /// The resulting plan xml after executing a step in the plan. + /// + /// Brief overview of how it works: + /// 1. The plan xml is parsed into a list of goals, variables, arguments, and steps. + /// 2. The first step is executed. + /// 3. The resulting context is converted into a new plan xml. + /// 4. The new plan xml is returned. + /// + /// Thrown when the plan xml is invalid. + public async Task ExecuteXmlPlanAsync(SKContext context, string planPayload, ISKFunction defaultStepExecutor) + { + try + { + XmlDocument xmlDoc = new(); + try + { + xmlDoc.LoadXml("" + planPayload + ""); + } + catch (XmlException e) + { + throw new PlanningException(PlanningException.ErrorCodes.InvalidPlan, "Failed to parse plan xml.", e); + } + + // Get the Goal + var (_, goalXmlString) = GatherGoal(xmlDoc); + + // Get the and Solution + XmlNodeList variables = xmlDoc.GetElementsByTagName(VariablesTag); + XmlNodeList solution = xmlDoc.GetElementsByTagName(SolutionTag); + + // Prepare content for the new plan xml + var variablesContent = new StringBuilder(); + var solutionContent = new StringBuilder(); + variablesContent.AppendLine($"<{VariablesTag}>"); + solutionContent.AppendLine($"<{SolutionTag}>"); + + // + //Process nodes + // + context.Log.LogDebug("Processing context variables"); + // Process the context variables nodes + var stepResults = this.ProcessNodeList(variables, context); + // Add the context variables to the new plan xml + variablesContent.Append(stepResults) + .AppendLine($""); + + // + // Process Solution nodes + // + context.Log.LogDebug("Processing solution"); + + // Process the solution nodes + stepResults = this.ProcessNodeList(solution, context); + // Add the solution and context variables updates to the new plan xml + solutionContent.Append(stepResults) + .AppendLine($""); + + // Update the plan xml + var updatedPlan = goalXmlString + variablesContent + solutionContent; + updatedPlan = updatedPlan.Trim(); + context.Variables.Update(updatedPlan); + + // Otherwise, execute the next step in the plan + var nextPlan = (await this._kernel.RunAsync(context.Variables, defaultStepExecutor)).ToString().Trim(); + + // And return the updated context with the updated plan xml + context.Variables.Update(nextPlan); + return context; + } + catch (Exception e) when (!e.IsCriticalException()) + { + context.Log.LogError(e, "Plan execution failed: {0}", e.Message); + throw; + } + } + + private string ProcessNodeList(XmlNodeList nodeList, SKContext context) + { + var stepAndTextResults = new StringBuilder(); + const string INDENT = " "; + if (nodeList != null) + { + foreach (XmlNode o in nodeList) + { + if (o == null) + { + continue; + } + + var parentNodeName = o.Name; + + context.Log.LogDebug("{0}: found node", parentNodeName); + foreach (XmlNode o2 in o.ChildNodes) + { + if (o2.Name == "#text") + { + context.Log.LogDebug("{0}: appending text node", parentNodeName); + stepAndTextResults.AppendLine(o2.Value.Trim()); + continue; + } + + if (o2.Name == StepTag) + { + context.Log.LogDebug("{0}: appending step node {1}", parentNodeName, o2.OuterXml); + stepAndTextResults.Append(INDENT).AppendLine(o2.OuterXml); + continue; + } + + stepAndTextResults.Append(INDENT).AppendLine(o2.OuterXml); + } + } + } + + return stepAndTextResults.ToString(); + } + + // TODO: goalTxt is never used + private static (string goalTxt, string goalXmlString) GatherGoal(XmlDocument xmlDoc) + { + XmlNodeList goal = xmlDoc.GetElementsByTagName(GoalTag); + if (goal.Count == 0) + { + throw new PlanningException(PlanningException.ErrorCodes.InvalidPlan, "No goal found."); + } + + string goalTxt = goal[0]!.FirstChild!.Value ?? string.Empty; + var goalContent = new StringBuilder(); + goalContent.Append($"<{GoalTag}>") + .Append(goalTxt) + .AppendLine($""); + return (goalTxt, goalContent.ToString()); + } +} diff --git a/dotnet/src/SemanticKernel/Planning/PlanningException.cs b/dotnet/src/SemanticKernel/Planning/PlanningException.cs new file mode 100644 index 000000000000..d33121211e72 --- /dev/null +++ b/dotnet/src/SemanticKernel/Planning/PlanningException.cs @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using Microsoft.SemanticKernel.Diagnostics; + +namespace Microsoft.SemanticKernel.Planning; + +/// +/// Planning exception. +/// +public class PlanningException : Exception +{ + /// + /// Error codes for . + /// + public enum ErrorCodes + { + /// + /// Unknown error. + /// + UnknownError = -1, + + /// + /// Invalid plan. + /// + InvalidPlan = 0, + + /// + /// Invalid configuration. + /// + InvalidConfiguration = 1, + } + + /// + /// Gets the error code of the exception. + /// + public ErrorCodes ErrorCode { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The error code. + /// The message. + public PlanningException(ErrorCodes errCode, string message) : base(errCode, message) + { + this.ErrorCode = errCode; + } + + /// + /// Initializes a new instance of the class. + /// + /// The error code. + /// The message. + /// The inner exception. + public PlanningException(ErrorCodes errCode, string message, Exception e) : base(errCode, message, e) + { + this.ErrorCode = errCode; + } +} diff --git a/dotnet/src/SemanticKernel/Planning/SKContextExtensions.cs b/dotnet/src/SemanticKernel/Planning/SKContextExtensions.cs new file mode 100644 index 000000000000..52d031745b92 --- /dev/null +++ b/dotnet/src/SemanticKernel/Planning/SKContextExtensions.cs @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Linq; +using Microsoft.SemanticKernel.Orchestration; +using Microsoft.SemanticKernel.Orchestration.Extensions; +using Microsoft.SemanticKernel.SkillDefinition; + +namespace Microsoft.SemanticKernel.Planning; + +internal static class SKContextExtensions +{ + internal static string GetFunctionsManual( + this SKContext context, + List? excludedSkills = null, + List? excludedFunctions = null) + { + var functions = context.GetAvailableFunctions(excludedSkills, excludedFunctions); + + return string.Join("\n\n", + functions.Select( + x => + { + var inputs = string.Join("\n", x.Parameters.Select(p => $" -${p.Name}: {p.Description}")); + return $" {x.SkillName}.{x.Name}:\n description: {x.Description}\n inputs:\n{inputs}"; + })); + } + + // TODO: support more strategies, e.g. searching for relevant functions (by goal, by user preferences, etc.) + internal static List GetAvailableFunctions( + this SKContext context, + List? excludedSkills = null, + List? excludedFunctions = null) + { + excludedSkills ??= new(); + excludedFunctions ??= new(); + + context.ThrowIfSkillCollectionNotSet(); + + var functionsView = context.Skills!.GetFunctionsView(); + + return functionsView.SemanticFunctions + .Concat(functionsView.NativeFunctions) + .SelectMany(x => x.Value) + .Where(s => !excludedSkills.Contains(s.SkillName) && !excludedFunctions.Contains(s.Name)) + .ToList(); + } +} diff --git a/dotnet/src/SemanticKernel/Reliability/IRetryMechanism.cs b/dotnet/src/SemanticKernel/Reliability/IRetryMechanism.cs new file mode 100644 index 000000000000..6d7a74521e7a --- /dev/null +++ b/dotnet/src/SemanticKernel/Reliability/IRetryMechanism.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; + +namespace Microsoft.SemanticKernel.Reliability; + +/// +/// Interface for retry mechanisms on AI calls. +/// +public interface IRetryMechanism +{ + /// + /// Executes the given action with retry logic. + /// + /// The action to retry on exception. + /// The logger to use. + /// An awaitable task. + Task ExecuteWithRetryAsync(Func action, ILogger log); +} diff --git a/dotnet/src/SemanticKernel/Reliability/PassThroughWithoutRetry.cs b/dotnet/src/SemanticKernel/Reliability/PassThroughWithoutRetry.cs new file mode 100644 index 000000000000..5eeae2a512a9 --- /dev/null +++ b/dotnet/src/SemanticKernel/Reliability/PassThroughWithoutRetry.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel.Diagnostics; + +namespace Microsoft.SemanticKernel.Reliability; + +/// +/// A retry mechanism that does not retry. +/// +internal class PassThroughWithoutRetry : IRetryMechanism +{ + public async Task ExecuteWithRetryAsync(Func action, ILogger log) + { + try + { + await action(); + } + catch (Exception ex) when (!ex.IsCriticalException()) + { + log.LogWarning(ex, "Error executing action, not retrying"); + throw; + } + } +} diff --git a/dotnet/src/SemanticKernel/SemanticFunctions/IPromptTemplate.cs b/dotnet/src/SemanticKernel/SemanticFunctions/IPromptTemplate.cs new file mode 100644 index 000000000000..295328c0a00d --- /dev/null +++ b/dotnet/src/SemanticKernel/SemanticFunctions/IPromptTemplate.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.SemanticKernel.Orchestration; +using Microsoft.SemanticKernel.SkillDefinition; + +namespace Microsoft.SemanticKernel.SemanticFunctions; + +/// +/// Interface for prompt template. +/// +public interface IPromptTemplate +{ + /// + /// Get the list of parameters required by the template, using configuration and template info. + /// + /// List of parameters + IList GetParameters(); + + /// + /// Render the template using the information in the context + /// + /// Kernel execution context helpers + /// Prompt rendered to string + public Task RenderAsync(SKContext executionContext); +} diff --git a/dotnet/src/SemanticKernel/SemanticFunctions/Partitioning/FunctionExtensions.cs b/dotnet/src/SemanticKernel/SemanticFunctions/Partitioning/FunctionExtensions.cs new file mode 100644 index 000000000000..4cca776884ea --- /dev/null +++ b/dotnet/src/SemanticKernel/SemanticFunctions/Partitioning/FunctionExtensions.cs @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.SemanticKernel.Orchestration; + +namespace Microsoft.SemanticKernel.SemanticFunctions.Partitioning; + +/// +/// Class with extension methods for semantic functions. +/// +public static class FunctionExtensions +{ + /// + /// Extension method to aggregate partitioned results of a semantic function. + /// + /// Semantic Kernel function + /// Input to aggregate. + /// Semantic Kernel context. + /// Aggregated results. + public static async Task AggregatePartitionedResultsAsync( + this ISKFunction func, + List partitionedInput, + SKContext context) + { + var results = new List(); + foreach (var partition in partitionedInput) + { + context.Variables.Update(partition); + context = await func.InvokeAsync(context); + + results.Add(context.Variables.ToString()); + } + + context.Variables.Update(string.Join("\n", results)); + return context; + } +} diff --git a/dotnet/src/SemanticKernel/SemanticFunctions/Partitioning/SemanticTextPartitioner.cs b/dotnet/src/SemanticKernel/SemanticFunctions/Partitioning/SemanticTextPartitioner.cs new file mode 100644 index 000000000000..cd56dffb7aff --- /dev/null +++ b/dotnet/src/SemanticKernel/SemanticFunctions/Partitioning/SemanticTextPartitioner.cs @@ -0,0 +1,293 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Microsoft.SemanticKernel.SemanticFunctions.Partitioning; + +/// +/// Split text in chunks, attempting to leave meaning intact. +/// For plain text, split looking at new lines first, then periods, and so on. +/// For markdown, split looking at punctuation first, and so on. +/// +public static class SemanticTextPartitioner +{ + /// + /// Split plain text into lines. + /// + /// Text to split + /// Maximum number of tokens per line. + /// List of lines. + public static List SplitPlainTextLines(string text, int maxTokensPerLine) + { + return InternalSplitPlaintextLines(text, maxTokensPerLine, true); + } + + /// + /// Split markdown text into lines. + /// + /// Text to split + /// Maximum number of tokens per line. + /// List of lines. + public static List SplitMarkDownLines(string text, int maxTokensPerLine) + { + return InternalSplitMarkdownLines(text, maxTokensPerLine, true); + } + + /// + /// Split plain text into paragraphs. + /// + /// Lines of text. + /// Maximum number of tokens per paragraph. + /// List of paragraphs. + public static List SplitPlainTextParagraphs(List lines, int maxTokensPerParagraph) + { + return InternalSplitTextParagraphs(lines, maxTokensPerParagraph, text => InternalSplitPlaintextLines(text, maxTokensPerParagraph, false)); + } + + /// + /// Split markdown text into paragraphs. + /// + /// Lines of text. + /// Maximum number of tokens per paragraph. + /// List of paragraphs. + public static List SplitMarkdownParagraphs(List lines, int maxTokensPerParagraph) + { + return InternalSplitTextParagraphs(lines, maxTokensPerParagraph, text => InternalSplitMarkdownLines(text, maxTokensPerParagraph, false)); + } + + private static List InternalSplitTextParagraphs(List lines, int maxTokensPerParagraph, Func> longLinesSplitter) + { + if (lines.Count == 0) + { + return new List(); + } + + // Split long lines first + var truncatedLines = new List(); + foreach (var line in lines) + { + truncatedLines.AddRange(longLinesSplitter(line)); + } + + lines = truncatedLines; + + // Group lines in paragraphs + var paragraphs = new List(); + var currentParagraph = new StringBuilder(); + foreach (var line in lines) + { + // "+1" to account for the "new line" added by AppendLine() + if (TokenCount(currentParagraph.ToString()) + TokenCount(line) + 1 >= maxTokensPerParagraph && + currentParagraph.Length > 0) + { + paragraphs.Add(currentParagraph.ToString().Trim()); + currentParagraph.Clear(); + } + + currentParagraph.AppendLine(line); + } + + if (currentParagraph.Length > 0) + { + paragraphs.Add(currentParagraph.ToString().Trim()); + currentParagraph.Clear(); + } + + // distribute text more evenly in the last paragraphs when the last paragraph is too short. + if (paragraphs.Count > 1) + { + var lastParagraph = paragraphs[^1]; + var secondLastParagraph = paragraphs[^2]; + + if (TokenCount(lastParagraph) < maxTokensPerParagraph / 4) + { + var lastParagraphTokens = lastParagraph.Split(' ', StringSplitOptions.RemoveEmptyEntries); + var secondLastParagraphTokens = secondLastParagraph.Split(' ', StringSplitOptions.RemoveEmptyEntries); + + var lastParagraphTokensCount = lastParagraphTokens.Length; + var secondLastParagraphTokensCount = secondLastParagraphTokens.Length; + + if (lastParagraphTokensCount + secondLastParagraphTokensCount <= maxTokensPerParagraph) + { + var newSecondLastParagraph = new StringBuilder(); + for (var i = 0; i < secondLastParagraphTokensCount; i++) + { + newSecondLastParagraph.Append(secondLastParagraphTokens[i]) + .Append(' '); + } + + for (var i = 0; i < lastParagraphTokensCount; i++) + { + newSecondLastParagraph.Append(lastParagraphTokens[i]) + .Append(' '); + } + + paragraphs[^2] = newSecondLastParagraph.ToString().Trim(); + paragraphs.RemoveAt(paragraphs.Count - 1); + } + } + } + + return paragraphs; + } + + private static List InternalSplitPlaintextLines(string text, int maxTokensPerLine, bool trim) + { + text = text.Replace("\r\n", "\n", StringComparison.OrdinalIgnoreCase); + + var splitOptions = new List?> + { + new List { '\n', '\r' }, + new List { '.' }, + new List { '?', '!' }, + new List { ';' }, + new List { ':' }, + new List { ',' }, + new List { ')', ']', '}' }, + new List { ' ' }, + new List { '-' }, + null + }; + + List? result = null; + bool inputWasSplit; + foreach (var splitOption in splitOptions) + { + if (result is null) + { + result = Split(text, maxTokensPerLine, splitOption, trim, out inputWasSplit); + } + else + { + result = Split(result, maxTokensPerLine, splitOption, trim, out inputWasSplit); + } + + if (!inputWasSplit) + { + break; + } + } + + return result ?? new List(); + } + + private static List InternalSplitMarkdownLines(string text, int maxTokensPerLine, bool trim) + { + text = text.Replace("\r\n", "\n", StringComparison.OrdinalIgnoreCase); + + var splitOptions = new List?> + { + new List { '.' }, + new List { '?', '!' }, + new List { ';', }, + new List { ':' }, + new List { ',', }, + new List { ')', ']', '}' }, + new List { ' ' }, + new List { '-' }, + new List { '\n', '\r' }, + null + }; + + List? result = null; + bool inputWasSplit; + foreach (var splitOption in splitOptions) + { + if (result is null) + { + result = Split(text, maxTokensPerLine, splitOption, trim, out inputWasSplit); + } + else + { + result = Split(result, maxTokensPerLine, splitOption, trim, out inputWasSplit); + } + + if (!inputWasSplit) + { + break; + } + } + + return result ?? new List(); + } + + private static List Split(IEnumerable input, int maxTokens, List? separators, bool trim, out bool inputWasSplit) + { + inputWasSplit = false; + var result = new List(); + foreach (string text in input) + { + result.AddRange(Split(text, maxTokens, separators, trim, out bool split)); + inputWasSplit = inputWasSplit || split; + } + + return result; + } + + private static List Split(string input, int maxTokens, List? separators, bool trim, out bool inputWasSplit) + { + inputWasSplit = false; + var asIs = new List { trim ? input.Trim() : input }; + if (TokenCount(input) <= maxTokens) + { + return asIs; + } + + inputWasSplit = true; + var result = new List(); + + int half = input.Length / 2; + int cutPoint = -1; + + if (separators == null || separators.Count == 0) + { + cutPoint = half; + } + else if (input.Any(separators.Contains) && input.Length > 2) + { + for (var index = 0; index < input.Length - 1; index++) + { + if (!separators.Contains(input[index])) + { + continue; + } + + if (Math.Abs(half - index) < Math.Abs(half - cutPoint)) + { + cutPoint = index + 1; + } + } + } + + if (cutPoint > 0) + { + var firstHalf = input[..cutPoint]; + var secondHalf = input[cutPoint..]; + if (trim) + { + firstHalf = firstHalf.Trim(); + secondHalf = secondHalf.Trim(); + } + + // Recursion + result.AddRange(Split(firstHalf, maxTokens, separators, trim, out bool split1)); + result.AddRange(Split(secondHalf, maxTokens, separators, trim, out bool split2)); + + inputWasSplit = split1 || split2; + + return result; + } + + return asIs; + } + + private static int TokenCount(string input) + { + // TODO: partitioning methods should be configurable to allow for different tokenization strategies + // depending on the model to be called. For now, we use an extremely rough estimate. + return input.Length / 4; + } +} diff --git a/dotnet/src/SemanticKernel/SemanticFunctions/PromptTemplate.cs b/dotnet/src/SemanticKernel/SemanticFunctions/PromptTemplate.cs new file mode 100644 index 000000000000..7bf085ba0f69 --- /dev/null +++ b/dotnet/src/SemanticKernel/SemanticFunctions/PromptTemplate.cs @@ -0,0 +1,96 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.SemanticKernel.Orchestration; +using Microsoft.SemanticKernel.SkillDefinition; +using Microsoft.SemanticKernel.TemplateEngine; +using Microsoft.SemanticKernel.TemplateEngine.Blocks; + +namespace Microsoft.SemanticKernel.SemanticFunctions; + +public sealed class PromptTemplate : IPromptTemplate +{ + private readonly string _template; + private readonly IPromptTemplateEngine _templateEngine; + + // ReSharper disable once NotAccessedField.Local + private readonly ILogger _log = NullLogger.Instance; + + // ReSharper disable once NotAccessedField.Local + private readonly PromptTemplateConfig _promptConfig; + + public PromptTemplate(string template, PromptTemplateConfig promptTemplateConfig, IKernel kernel) + : this(template, promptTemplateConfig, kernel.PromptTemplateEngine, kernel.Log) + { + } + + public PromptTemplate( + string template, + PromptTemplateConfig promptTemplateConfig, + IPromptTemplateEngine promptTemplateEngine, + ILogger? log = null) + { + this._template = template; + this._templateEngine = promptTemplateEngine; + this._promptConfig = promptTemplateConfig; + if (log != null) { this._log = log; } + } + + /// + /// Get the list of parameters used by the function, using JSON settings and template variables. + /// TODO: consider caching results - though cache invalidation will add extra complexity + /// + /// List of parameters + public IList GetParameters() + { + var seen = new HashSet(StringComparer.OrdinalIgnoreCase); + + // Parameters from config.json + List result = new(); + foreach (PromptTemplateConfig.InputParameter? p in this._promptConfig.Input.Parameters) + { + if (p == null) { continue; } + + result.Add(new ParameterView + { + Name = p.Name, + Description = p.Description, + DefaultValue = p.DefaultValue + }); + + seen.Add(p.Name); + } + + // Parameters from the template + List listFromTemplate = this._templateEngine.ExtractBlocks(this._template) + .Where(x => x.Type == BlockTypes.Variable) + .Select(x => (VarBlock)x) + .Where(x => x != null) + .ToList(); + + foreach (VarBlock x in listFromTemplate) + { + if (seen.Contains(x.Name)) { continue; } + + result.Add(new ParameterView { Name = x.Name }); + seen.Add(x.Name); + } + + return result; + } + + /// + /// Render the template using the information in the context + /// + /// Kernel execution context helpers + /// Prompt rendered to string + public async Task RenderAsync(SKContext executionContext) + { + return await this._templateEngine.RenderAsync(this._template, executionContext); + } +} diff --git a/dotnet/src/SemanticKernel/SemanticFunctions/PromptTemplateConfig.cs b/dotnet/src/SemanticKernel/SemanticFunctions/PromptTemplateConfig.cs new file mode 100644 index 000000000000..3fb9704567e2 --- /dev/null +++ b/dotnet/src/SemanticKernel/SemanticFunctions/PromptTemplateConfig.cs @@ -0,0 +1,123 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Text.Json.Serialization; +using Microsoft.SemanticKernel.Diagnostics; +using Microsoft.SemanticKernel.Text; + +namespace Microsoft.SemanticKernel.SemanticFunctions; + +public class PromptTemplateConfig +{ + public class CompletionConfig + { + [JsonPropertyName("temperature")] + [JsonPropertyOrder(1)] + public double Temperature { get; set; } = 0.0f; + + [JsonPropertyName("top_p")] + [JsonPropertyOrder(2)] + public double TopP { get; set; } = 0.0f; + + [JsonPropertyName("presence_penalty")] + [JsonPropertyOrder(3)] + public double PresencePenalty { get; set; } = 0.0f; + + [JsonPropertyName("frequency_penalty")] + [JsonPropertyOrder(4)] + public double FrequencyPenalty { get; set; } = 0.0f; + + [JsonPropertyName("max_tokens")] + [JsonPropertyOrder(5)] + public int MaxTokens { get; set; } = 256; + + [JsonPropertyName("stop_sequences")] + [JsonPropertyOrder(6)] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public List StopSequences { get; set; } = new(); + } + + public class InputParameter + { + /// + /// Name of the parameter to pass to the function. + /// e.g. when using "{{$input}}" the name is "input", when using "{{$style}}" the name is "style", etc. + /// + [JsonPropertyName("name")] + [JsonPropertyOrder(1)] + public string Name { get; set; } = string.Empty; + + /// + /// Parameter description for UI apps and planner. Localization is not supported here. + /// + [JsonPropertyName("description")] + [JsonPropertyOrder(2)] + public string Description { get; set; } = string.Empty; + + /// + /// DEfault value when nothing is provided. + /// + [JsonPropertyName("defaultValue")] + [JsonPropertyOrder(3)] + public string DefaultValue { get; set; } = string.Empty; + } + + public class InputConfig + { + [JsonPropertyName("parameters")] + [JsonPropertyOrder(1)] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public List Parameters { get; set; } = new(); + } + + [JsonPropertyName("schema")] + [JsonPropertyOrder(1)] + public int Schema { get; set; } = 1; + + // TODO: use enum + [JsonPropertyName("type")] + [JsonPropertyOrder(2)] + public string Type { get; set; } = "completion"; + + [JsonPropertyName("description")] + [JsonPropertyOrder(3)] + public string Description { get; set; } = string.Empty; + + [JsonPropertyName("completion")] + [JsonPropertyOrder(4)] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public CompletionConfig Completion { get; set; } = new(); + + [JsonPropertyName("default_backends")] + [JsonPropertyOrder(5)] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public List DefaultBackends { get; set; } = new(); + + [JsonPropertyName("input")] + [JsonPropertyOrder(6)] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public InputConfig Input { get; set; } = new(); + + // Remove some default properties to reduce the JSON complexity + public PromptTemplateConfig Compact() + { + if (this.Completion.StopSequences.Count == 0) + { + this.Completion.StopSequences = null!; + } + + if (this.DefaultBackends.Count == 0) + { + this.DefaultBackends = null!; + } + + return this; + } + + public static PromptTemplateConfig FromJson(string json) + { + var result = Json.Deserialize(json); + Verify.NotNull(result, "Unable to deserialize prompt template config. The deserialized returned NULL."); + return result; + } +} diff --git a/dotnet/src/SemanticKernel/SemanticFunctions/SemanticFunctionConfig.cs b/dotnet/src/SemanticKernel/SemanticFunctions/SemanticFunctionConfig.cs new file mode 100644 index 000000000000..e9f19ee4c78a --- /dev/null +++ b/dotnet/src/SemanticKernel/SemanticFunctions/SemanticFunctionConfig.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft. All rights reserved. + +namespace Microsoft.SemanticKernel.SemanticFunctions; + +/// +/// Semantic function configuration. +/// +public sealed class SemanticFunctionConfig +{ + /// + /// Prompt template configuration. + /// + public PromptTemplateConfig PromptTemplateConfig { get; } + + /// + /// Prompt template. + /// + public IPromptTemplate PromptTemplate { get; } + + /// + /// Constructor for SemanticFunctionConfig. + /// + /// Prompt template configuration. + /// Prompt template. + public SemanticFunctionConfig(PromptTemplateConfig config, IPromptTemplate template) + { + this.PromptTemplateConfig = config; + this.PromptTemplate = template; + } +} diff --git a/dotnet/src/SemanticKernel/SemanticKernel.csproj b/dotnet/src/SemanticKernel/SemanticKernel.csproj new file mode 100644 index 000000000000..49e9187ebbbb --- /dev/null +++ b/dotnet/src/SemanticKernel/SemanticKernel.csproj @@ -0,0 +1,44 @@ + + + + $([System.IO.Path]::GetDirectoryName($([MSBuild]::GetPathOfFileAbove('.gitignore', '$(MSBuildThisFileDirectory)')))) + + + + + Microsoft.SemanticKernel + Microsoft.SemanticKernel + netstandard2.1 + true + + + + + Microsoft.SemanticKernel + Semantic Kernel + 0.7 + + + + true + full + bin\Release\netstandard2.1\SemanticKernel.xml + + + + portable + bin\Debug\netstandard2.1\SemanticKernel.xml + + + + + + + + + + + <_Parameter1>SemanticKernelTests + + + \ No newline at end of file diff --git a/dotnet/src/SemanticKernel/SkillDefinition/FunctionView.cs b/dotnet/src/SemanticKernel/SkillDefinition/FunctionView.cs new file mode 100644 index 000000000000..98d18a117bca --- /dev/null +++ b/dotnet/src/SemanticKernel/SkillDefinition/FunctionView.cs @@ -0,0 +1,74 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; + +namespace Microsoft.SemanticKernel.SkillDefinition; + +/// +/// Class used to copy and export data from the skill collection. +/// The data is mutable, but changes do not affect the skill collection. +/// +public sealed class FunctionView +{ + /// + /// Name of the function. The name is used by the skill collection and in prompt templates e.g. {{skillName.functionName}} + /// + public string Name { get; set; } = string.Empty; + + /// + /// Name of the skill containing the function. The name is used by the skill collection and in prompt templates e.g. {{skillName.functionName}} + /// + public string SkillName { get; set; } = string.Empty; + + /// + /// Function description. The description is used in combination with embeddings when searching relevant functions. + /// + public string Description { get; set; } = string.Empty; + + /// + /// Whether the delegate points to a semantic function + /// + public bool IsSemantic { get; set; } + + /// + /// Whether the delegate is an asynchronous function + /// + public bool IsAsynchronous { get; set; } + + /// + /// List of function parameters + /// + public IList Parameters { get; set; } = new List(); + + /// + /// Constructor + /// + public FunctionView() + { + } + + /// + /// Create a function view. + /// + /// Function name + /// Skill name, e.g. the function namespace + /// Function description + /// List of function parameters provided by the skill developer + /// Whether the function is a semantic one (or native is False) + /// Whether the function is async. Note: all semantic functions are async. + public FunctionView( + string name, + string skillName, + string description, + IList parameters, + bool isSemantic, + bool isAsynchronous = true) + { + this.Name = name; + this.SkillName = skillName; + this.Description = description; + this.Parameters = parameters; + this.IsSemantic = isSemantic; + this.IsAsynchronous = isAsynchronous; + } +} diff --git a/dotnet/src/SemanticKernel/SkillDefinition/FunctionsView.cs b/dotnet/src/SemanticKernel/SkillDefinition/FunctionsView.cs new file mode 100644 index 000000000000..a9fc0a1505cd --- /dev/null +++ b/dotnet/src/SemanticKernel/SkillDefinition/FunctionsView.cs @@ -0,0 +1,109 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Runtime; + +namespace Microsoft.SemanticKernel.SkillDefinition; + +/// +/// Class used to copy and export data from the skill collection. +/// The data is mutable, but changes do not affect the skill collection. +/// The class can be used to create custom lists in case your scenario needs to. +/// +public sealed class FunctionsView +{ + /// + /// Collection of semantic skill names and function names, including function parameters. + /// Functions are grouped by skill name. + /// + public ConcurrentDictionary> SemanticFunctions { get; set; } + = new(StringComparer.InvariantCultureIgnoreCase); + + /// + /// Collection of native skill names and function views, including function parameters. + /// Functions are grouped by skill name. + /// + public ConcurrentDictionary> NativeFunctions { get; set; } + = new(StringComparer.InvariantCultureIgnoreCase); + + /// + /// Add a function to the list + /// + /// Function details + /// Current instance + public FunctionsView AddFunction(FunctionView view) + { + if (view.IsSemantic) + { + if (!this.SemanticFunctions.ContainsKey(view.SkillName)) + { + this.SemanticFunctions[view.SkillName] = new(); + } + + this.SemanticFunctions[view.SkillName].Add(view); + } + else + { + if (!this.NativeFunctions.ContainsKey(view.SkillName)) + { + this.NativeFunctions[view.SkillName] = new(); + } + + this.NativeFunctions[view.SkillName].Add(view); + } + + return this; + } + + /// + /// Returns true if the function specified is unique and semantic + /// + /// Skill name + /// Function name + /// True if unique and semantic + /// + public bool IsSemantic(string skillName, string functionName) + { + var sf = this.SemanticFunctions.ContainsKey(skillName) + && this.SemanticFunctions[skillName] + .Any(x => string.Equals(x.Name, functionName, StringComparison.OrdinalIgnoreCase)); + + var nf = this.NativeFunctions.ContainsKey(skillName) + && this.NativeFunctions[skillName] + .Any(x => string.Equals(x.Name, functionName, StringComparison.OrdinalIgnoreCase)); + + if (sf && nf) + { + throw new AmbiguousImplementationException("There are 2 functions with the same name, one native and one semantic"); + } + + return sf; + } + + /// + /// Returns true if the function specified is unique and native + /// + /// Skill name + /// Function name + /// True if unique and native + public bool IsNative(string skillName, string functionName) + { + var sf = this.SemanticFunctions.ContainsKey(skillName) + && this.SemanticFunctions[skillName] + .Any(x => string.Equals(x.Name, functionName, StringComparison.OrdinalIgnoreCase)); + + var nf = this.NativeFunctions.ContainsKey(skillName) + && this.NativeFunctions[skillName] + .Any(x => string.Equals(x.Name, functionName, StringComparison.OrdinalIgnoreCase)); + + if (sf && nf) + { + throw new AmbiguousImplementationException("There are 2 functions with the same name, one native and one semantic"); + } + + return nf; + } +} diff --git a/dotnet/src/SemanticKernel/SkillDefinition/IReadOnlySkillCollection.cs b/dotnet/src/SemanticKernel/SkillDefinition/IReadOnlySkillCollection.cs new file mode 100644 index 000000000000..59fd6d5600a1 --- /dev/null +++ b/dotnet/src/SemanticKernel/SkillDefinition/IReadOnlySkillCollection.cs @@ -0,0 +1,82 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.SemanticKernel.Orchestration; + +namespace Microsoft.SemanticKernel.SkillDefinition; + +/// +/// Read-only skill collection interface. +/// +[System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1710:Identifiers should have correct suffix", Justification = "It is a collection")] +[System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1711:Identifiers should not have incorrect suffix", Justification = "It is a collection")] +public interface IReadOnlySkillCollection +{ + /// + /// Check if the collection contains the specified function in the global skill, regardless of the function type + /// + /// Skill name + /// Function name + /// True if the function exists, false otherwise + bool HasFunction(string skillName, string functionName); + + /// + /// Check if the collection contains the specified function, regardless of the function type + /// + /// Function name + /// True if the function exists, false otherwise + bool HasFunction(string functionName); + + /// + /// Check if a semantic function is registered + /// + /// Function name + /// Skill name + /// True if the function exists + bool HasSemanticFunction(string skillName, string functionName); + + /// + /// Check if a native function is registered + /// + /// Function name + /// Skill name + /// True if the function exists + bool HasNativeFunction(string skillName, string functionName); + + /// + /// Check if a native function is registered in the global skill + /// + /// Function name + /// True if the function exists + bool HasNativeFunction(string functionName); + + /// + /// Return the semantic function delegate stored in the collection + /// + /// Function name + /// Skill name + /// Semantic function delegate + ISKFunction GetSemanticFunction(string skillName, string functionName); + + /// + /// Return the native function delegate stored in the collection + /// + /// Function name + /// Skill name + /// Native function delegate + ISKFunction GetNativeFunction(string skillName, string functionName); + + /// + /// Return the native function delegate stored in the collection + /// + /// Function name + /// Native function delegate + ISKFunction GetNativeFunction(string functionName); + + /// + /// Get all registered functions details, minus the delegates + /// + /// Whether to include semantic functions in the list + /// Whether to include native functions in the list + /// An object containing all the functions details + FunctionsView GetFunctionsView(bool includeSemantic = true, bool includeNative = true); +} diff --git a/dotnet/src/SemanticKernel/SkillDefinition/ISkillCollection.cs b/dotnet/src/SemanticKernel/SkillDefinition/ISkillCollection.cs new file mode 100644 index 000000000000..74b33fb5d89e --- /dev/null +++ b/dotnet/src/SemanticKernel/SkillDefinition/ISkillCollection.cs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.SemanticKernel.Orchestration; + +namespace Microsoft.SemanticKernel.SkillDefinition; + +/// +/// Skill collection interface. +/// +[System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1710:Identifiers should have correct suffix", Justification = "It is a collection")] +[System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1711:Identifiers should not have incorrect suffix", Justification = "It is a collection")] +public interface ISkillCollection : IReadOnlySkillCollection +{ + /// + /// Readonly only access into the collection + /// + IReadOnlySkillCollection ReadOnlySkillCollection { get; } + + /// + /// Add a semantic function to the collection + /// + /// Function delegate + /// Self instance + ISkillCollection AddSemanticFunction(ISKFunction functionInstance); + + /// + /// Add a native function to the collection + /// + /// Wrapped function delegate + /// Self instance + ISkillCollection AddNativeFunction(ISKFunction functionInstance); +} diff --git a/dotnet/src/SemanticKernel/SkillDefinition/ParameterView.cs b/dotnet/src/SemanticKernel/SkillDefinition/ParameterView.cs new file mode 100644 index 000000000000..527db90bdc9f --- /dev/null +++ b/dotnet/src/SemanticKernel/SkillDefinition/ParameterView.cs @@ -0,0 +1,67 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.SemanticKernel.Diagnostics; + +namespace Microsoft.SemanticKernel.SkillDefinition; + +/// +/// Class used to copy and export data from +/// +/// and +/// for planner and related scenarios. +/// +public sealed class ParameterView +{ + private string _name = ""; + + /// + /// Parameter name. Alphanumeric chars + "_" only. + /// + public string Name + { + get + { + return this._name; + } + set + { + Verify.ValidFunctionParamName(value); + this._name = value; + } + } + + /// + /// Parameter description. + /// + public string Description { get; set; } = string.Empty; + + /// + /// Default value when the value is not provided. + /// + public string DefaultValue { get; set; } = string.Empty; + + /// + /// Constructor + /// + public ParameterView() + { + } + + /// + /// Create a function parameter view, using information provided by the skill developer. + /// + /// Parameter name. The name must be alphanumeric (underscore is the only special char allowed). + /// Parameter description + /// Default parameter value, if not provided + public ParameterView( + string name, + string description, + string defaultValue) + { + Verify.ValidFunctionParamName(name); + + this.Name = name; + this.Description = description; + this.DefaultValue = defaultValue; + } +} diff --git a/dotnet/src/SemanticKernel/SkillDefinition/ReadOnlySkillCollection.cs b/dotnet/src/SemanticKernel/SkillDefinition/ReadOnlySkillCollection.cs new file mode 100644 index 000000000000..960ff5060766 --- /dev/null +++ b/dotnet/src/SemanticKernel/SkillDefinition/ReadOnlySkillCollection.cs @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.SemanticKernel.Orchestration; + +namespace Microsoft.SemanticKernel.SkillDefinition; + +/// +/// Access the collection in read-only mode, e.g. allow templates to search and execute functions. +/// +internal class ReadOnlySkillCollection : IReadOnlySkillCollection +{ + private readonly ISkillCollection _skillCollection; + + public ReadOnlySkillCollection(ISkillCollection skillCollection) + { + this._skillCollection = skillCollection; + } + + /// + public bool HasFunction(string skillName, string functionName) + { + return this._skillCollection.HasFunction(skillName, functionName); + } + + /// + public bool HasFunction(string functionName) + { + return this._skillCollection.HasFunction(functionName); + } + + /// + public bool HasSemanticFunction(string skillName, string functionName) + { + return this._skillCollection.HasSemanticFunction(skillName, functionName); + } + + /// + public bool HasNativeFunction(string skillName, string functionName) + { + return this._skillCollection.HasNativeFunction(skillName, functionName); + } + + /// + public bool HasNativeFunction(string functionName) + { + return this._skillCollection.HasNativeFunction(functionName); + } + + /// + public ISKFunction GetSemanticFunction(string skillName, string functionName) + { + return this._skillCollection.GetSemanticFunction(skillName, functionName); + } + + /// + public ISKFunction GetNativeFunction(string skillName, string functionName) + { + return this._skillCollection.GetNativeFunction(skillName, functionName); + } + + /// + public ISKFunction GetNativeFunction(string functionName) + { + return this._skillCollection.GetNativeFunction(functionName); + } + + /// + public FunctionsView GetFunctionsView(bool includeSemantic = true, bool includeNative = true) + { + return this._skillCollection.GetFunctionsView(includeSemantic, includeNative); + } +} diff --git a/dotnet/src/SemanticKernel/SkillDefinition/SKFunctionAttribute.cs b/dotnet/src/SemanticKernel/SkillDefinition/SKFunctionAttribute.cs new file mode 100644 index 000000000000..a63af1604aeb --- /dev/null +++ b/dotnet/src/SemanticKernel/SkillDefinition/SKFunctionAttribute.cs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; + +namespace Microsoft.SemanticKernel.SkillDefinition; + +/// +/// Attribute required to register native functions into the kernel. +/// The registration is required by the prompt templating engine and by the pipeline generator (aka planner). +/// The quality of the description affects the planner ability to reason about complex tasks. +/// The description is used both with LLM prompts and embedding comparisons. +/// +[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] +public sealed class SKFunctionAttribute : Attribute +{ + /// + /// Function description, to be used by the planner to auto-discover functions. + /// + public string Description { get; } + + /// + /// Tag a C# function as a native function available to SK. + /// + /// Function description, to be used by the planner to auto-discover functions. + public SKFunctionAttribute(string description) + { + this.Description = description; + } +} diff --git a/dotnet/src/SemanticKernel/SkillDefinition/SKFunctionContextParameterAttribute.cs b/dotnet/src/SemanticKernel/SkillDefinition/SKFunctionContextParameterAttribute.cs new file mode 100644 index 000000000000..a6d665795a73 --- /dev/null +++ b/dotnet/src/SemanticKernel/SkillDefinition/SKFunctionContextParameterAttribute.cs @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using Microsoft.SemanticKernel.Diagnostics; + +namespace Microsoft.SemanticKernel.SkillDefinition; + +/// +/// Attribute to describe the parameters required by a native function. +/// +/// Note: the class has no ctor, to force the use of setters and keep the attribute use readable +/// e.g. +/// Readable: [SKFunctionContextParameter(Name = "...", Description = "...", DefaultValue = "...")] +/// Not readable: [SKFunctionContextParameter("...", "...", "...")] +/// +[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] +public sealed class SKFunctionContextParameterAttribute : Attribute +{ + private string _name = ""; + + /// + /// Parameter name. Alphanumeric chars + "_" only. + /// + public string Name + { + get { return this._name; } + set + { + Verify.ValidFunctionParamName(value); + this._name = value; + } + } + + /// + /// Parameter description. + /// + public string Description { get; set; } = string.Empty; + + /// + /// Default value when the value is not provided. + /// + public string DefaultValue { get; set; } = string.Empty; + + /// + /// Creates a parameter view, using information from an instance of this class. + /// + /// Parameter view. + public ParameterView ToParameterView() + { + Verify.NotEmpty(this.Name, "The parameter name is missing"); + + return new ParameterView + { + Name = this.Name, + Description = this.Description, + DefaultValue = this.DefaultValue + }; + } +} diff --git a/dotnet/src/SemanticKernel/SkillDefinition/SKFunctionInputAttribute.cs b/dotnet/src/SemanticKernel/SkillDefinition/SKFunctionInputAttribute.cs new file mode 100644 index 000000000000..9b242367ef65 --- /dev/null +++ b/dotnet/src/SemanticKernel/SkillDefinition/SKFunctionInputAttribute.cs @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; + +namespace Microsoft.SemanticKernel.SkillDefinition; + +/// +/// Attribute to describe the main parameter required by a native function, +/// e.g. the first "string" parameter, if the function requires one. +/// +/// +/// The class has no constructor and requires the use of setters for readability. +/// e.g. +/// Readable: [SKFunctionInput(Description = "...", DefaultValue = "...")] +/// Not readable: [SKFunctionInput("...", "...")] +/// +/// +/// +/// // No main parameter here, only context +/// public async Task WriteAsync(SKContext context +/// +/// +/// +/// +/// // "path" is the input parameter +/// [SKFunctionInput("Source file path")] +/// public async Task{string?} ReadAsync(string path, SKContext context +/// +/// +[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] +public sealed class SKFunctionInputAttribute : Attribute +{ + /// + /// Parameter description. + /// + public string Description { get; set; } = string.Empty; + + /// + /// Default value when the value is not provided. + /// + public string DefaultValue { get; set; } = string.Empty; + + /// + /// Creates a parameter view, using information from an instance of this class. + /// + /// Parameter view. + public ParameterView ToParameterView() + { + return new ParameterView + { + Name = "input", + Description = this.Description, + DefaultValue = this.DefaultValue + }; + } +} diff --git a/dotnet/src/SemanticKernel/SkillDefinition/SKFunctionNameAttribute.cs b/dotnet/src/SemanticKernel/SkillDefinition/SKFunctionNameAttribute.cs new file mode 100644 index 000000000000..0badbca51c04 --- /dev/null +++ b/dotnet/src/SemanticKernel/SkillDefinition/SKFunctionNameAttribute.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using Microsoft.SemanticKernel.Diagnostics; + +namespace Microsoft.SemanticKernel.SkillDefinition; + +/// +/// Optional attribute to set the name used for the function in the skill collection. +/// +[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] +public sealed class SKFunctionNameAttribute : Attribute +{ + /// + /// Function name + /// + public string Name { get; } + + /// + /// Tag a C# function as a native function available to SK. + /// + /// Function name + public SKFunctionNameAttribute(string name) + { + Verify.ValidFunctionName(name); + this.Name = name; + } +} diff --git a/dotnet/src/SemanticKernel/SkillDefinition/SkillCollection.cs b/dotnet/src/SemanticKernel/SkillDefinition/SkillCollection.cs new file mode 100644 index 000000000000..81ac939bdaaf --- /dev/null +++ b/dotnet/src/SemanticKernel/SkillDefinition/SkillCollection.cs @@ -0,0 +1,169 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.SemanticKernel.Diagnostics; +using Microsoft.SemanticKernel.Orchestration; + +namespace Microsoft.SemanticKernel.SkillDefinition; + +/// +/// Semantic Kernel default skill collection class. +/// The class holds a list of all the functions, native and semantic, known to the kernel instance. +/// The list is used by the planner and when executing pipelines of function compositions. +/// +[System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1710:Identifiers should have correct suffix", Justification = "It is a collection")] +[System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1711:Identifiers should not have incorrect suffix", Justification = "It is a collection")] +public class SkillCollection : ISkillCollection +{ + internal const string GlobalSkill = "_GLOBAL_FUNCTIONS_"; + + /// + public IReadOnlySkillCollection ReadOnlySkillCollection { get; private set; } + + public SkillCollection(ILogger? log = null) + { + if (log != null) { this._log = log; } + + this.ReadOnlySkillCollection = new ReadOnlySkillCollection(this); + + // Important: names are case insensitive + this._skillCollection = new(StringComparer.InvariantCultureIgnoreCase); + } + + /// + public ISkillCollection AddSemanticFunction(ISKFunction functionInstance) + { + if (!this._skillCollection.ContainsKey(functionInstance.SkillName)) + { + // Important: names are case insensitive + this._skillCollection[functionInstance.SkillName] = new(StringComparer.InvariantCultureIgnoreCase); + } + + this._skillCollection[functionInstance.SkillName][functionInstance.Name] = functionInstance; + + return this; + } + + /// + public ISkillCollection AddNativeFunction(ISKFunction functionInstance) + { + Verify.NotNull(functionInstance, "The function is NULL"); + if (!this._skillCollection.ContainsKey(functionInstance.SkillName)) + { + // Important: names are case insensitive + this._skillCollection[functionInstance.SkillName] = new(StringComparer.InvariantCultureIgnoreCase); + } + + this._skillCollection[functionInstance.SkillName][functionInstance.Name] = functionInstance; + return this; + } + + /// + public bool HasFunction(string skillName, string functionName) + { + return this._skillCollection.ContainsKey(skillName) && + this._skillCollection[skillName].ContainsKey(functionName); + } + + /// + public bool HasFunction(string functionName) + { + return this._skillCollection.ContainsKey(GlobalSkill) && + this._skillCollection[GlobalSkill].ContainsKey(functionName); + } + + /// + public bool HasSemanticFunction(string skillName, string functionName) + { + return this.HasFunction(skillName, functionName) + && this._skillCollection[skillName][functionName].IsSemantic; + } + + /// + public bool HasNativeFunction(string skillName, string functionName) + { + return this.HasFunction(skillName, functionName) + && !this._skillCollection[skillName][functionName].IsSemantic; + } + + /// + public bool HasNativeFunction(string functionName) + { + return this.HasNativeFunction(GlobalSkill, functionName); + } + + /// + public ISKFunction GetSemanticFunction(string skillName, string functionName) + { + if (this.HasSemanticFunction(skillName, functionName)) + { + return this._skillCollection[skillName][functionName]; + } + + this._log.LogError("Function not available: skill:{0} function:{1}", skillName, functionName); + throw new KernelException( + KernelException.ErrorCodes.FunctionNotAvailable, + $"Function not available {skillName}.{functionName}"); + } + + /// + public ISKFunction GetNativeFunction(string skillName, string functionName) + { + if (this.HasNativeFunction(skillName, functionName)) + { + return this._skillCollection[skillName][functionName]; + } + + this._log.LogError("Function not available: skill:{0} function:{1}", skillName, functionName); + throw new KernelException( + KernelException.ErrorCodes.FunctionNotAvailable, + $"Function not available {skillName}.{functionName}"); + } + + /// + public ISKFunction GetNativeFunction(string functionName) + { + return this.GetNativeFunction(GlobalSkill, functionName); + } + + /// + public FunctionsView GetFunctionsView(bool includeSemantic = true, bool includeNative = true) + { + var result = new FunctionsView(); + + if (includeSemantic) + { + foreach (var skill in this._skillCollection) + { + foreach (KeyValuePair f in skill.Value) + { + if (f.Value.IsSemantic) { result.AddFunction(f.Value.Describe()); } + } + } + } + + if (!includeNative) { return result; } + + foreach (var skill in this._skillCollection) + { + foreach (KeyValuePair f in skill.Value) + { + if (!f.Value.IsSemantic) { result.AddFunction(f.Value.Describe()); } + } + } + + return result; + } + + #region private ================================================================================ + + private readonly ILogger _log = NullLogger.Instance; + + private readonly ConcurrentDictionary> _skillCollection; + + #endregion +} diff --git a/dotnet/src/SemanticKernel/TemplateEngine/Blocks/Block.cs b/dotnet/src/SemanticKernel/TemplateEngine/Blocks/Block.cs new file mode 100644 index 000000000000..b17f6ff30788 --- /dev/null +++ b/dotnet/src/SemanticKernel/TemplateEngine/Blocks/Block.cs @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.SemanticKernel.Orchestration; + +namespace Microsoft.SemanticKernel.TemplateEngine.Blocks; + +/// +/// Base class for blocks parsed from a prompt template +/// +public abstract class Block +{ + internal virtual BlockTypes Type => BlockTypes.Undefined; + + internal string Content { get; set; } = string.Empty; + + /// + /// App logger + /// + protected ILogger Log { get; } = NullLogger.Instance; + + /// + /// Base constructor + /// + /// App logger + protected Block(ILogger? log = null) + { + if (log != null) { this.Log = log; } + } + + internal virtual Task RenderCodeAsync(SKContext executionContext) + { + throw new NotImplementedException("This block doesn't support code execution"); + } + + internal abstract bool IsValid(out string error); + + internal abstract string Render(ContextVariables? variables); +} diff --git a/dotnet/src/SemanticKernel/TemplateEngine/Blocks/BlockTypes.cs b/dotnet/src/SemanticKernel/TemplateEngine/Blocks/BlockTypes.cs new file mode 100644 index 000000000000..25fcee63a58b --- /dev/null +++ b/dotnet/src/SemanticKernel/TemplateEngine/Blocks/BlockTypes.cs @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft. All rights reserved. + +namespace Microsoft.SemanticKernel.TemplateEngine.Blocks; + +internal enum BlockTypes +{ + Undefined = 0, + Text = 1, + Code = 2, + Variable = 3, +} diff --git a/dotnet/src/SemanticKernel/TemplateEngine/Blocks/CodeBlock.cs b/dotnet/src/SemanticKernel/TemplateEngine/Blocks/CodeBlock.cs new file mode 100644 index 000000000000..6c591c500c67 --- /dev/null +++ b/dotnet/src/SemanticKernel/TemplateEngine/Blocks/CodeBlock.cs @@ -0,0 +1,192 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel.Orchestration; +using Microsoft.SemanticKernel.Orchestration.Extensions; +using Microsoft.SemanticKernel.SkillDefinition; + +// ReSharper disable TemplateIsNotCompileTimeConstantProblem + +namespace Microsoft.SemanticKernel.TemplateEngine.Blocks; + +internal class CodeBlock : Block +{ + internal override BlockTypes Type => BlockTypes.Code; + + internal CodeBlock(string content, ILogger log) : base(log) + { + this.Content = content; + } + +#pragma warning disable CA2254 // error strings are used also internally, not just for logging + internal override bool IsValid(out string error) + { + error = ""; + + List partsToValidate = this.Content.Split(' ', '\t', '\r', '\n') + .Where(x => !string.IsNullOrEmpty(x.Trim())) + .ToList(); + + for (var index = 0; index < partsToValidate.Count; index++) + { + var part = partsToValidate[index]; + + if (index == 0) // There is only a function name + { + if (VarBlock.HasVarPrefix(part)) + { + error = $"Variables cannot be used as function names [`{part}`]"; + this.Log.LogError(error); + return false; + } + + if (!Regex.IsMatch(part, "^[a-zA-Z0-9_.]*$")) + { + error = $"The function name `{part}` contains invalid characters"; + this.Log.LogError(error); + return false; + } + } + else // The function has parameters + { + if (!VarBlock.HasVarPrefix(part)) + { + error = $"`{part}` is not a valid function parameter: parameters must be variables."; + this.Log.LogError(error); + return false; + } + + if (part.Length < 2) + { + error = $"`{part}` is not a valid variable."; + this.Log.LogError(error); + return false; + } + + if (!VarBlock.IsValidVarName(part.Substring(1))) + { + error = $"`{part}` variable name is not valid."; + this.Log.LogError(error); + return false; + } + } + } + + this._validated = true; + + return true; + } +#pragma warning restore CA2254 + + internal override string Render(ContextVariables? variables) + { + throw new InvalidOperationException( + "Code blocks rendering requires IReadOnlySkillCollection. Incorrect method call."); + } + + internal override async Task RenderCodeAsync(SKContext context) + { + if (!this._validated && !this.IsValid(out var error)) + { + throw new TemplateException(TemplateException.ErrorCodes.SyntaxError, error); + } + + this.Log.LogTrace("Rendering code: `{0}`", this.Content); + + List parts = this.Content.Split(' ', '\t', '\r', '\n') + .Where(x => !string.IsNullOrEmpty(x.Trim())) + .ToList(); + + var functionName = parts[0]; + context.ThrowIfSkillCollectionNotSet(); + if (!this.GetFunctionFromSkillCollection(context.Skills!, functionName, out ISKFunction? function)) + { + this.Log.LogWarning("Function not found `{0}`", functionName); + return ""; + } + + // ReSharper disable once NullCoalescingConditionIsAlwaysNotNullAccordingToAPIContract + // Using $input by default, e.g. when the syntax is {{functionName}} + + // TODO: unit test, verify that all context variables are passed to Render() + ContextVariables variablesClone = context.Variables.Clone(); + if (parts.Count > 1) + { + this.Log.LogTrace("Passing required variable: `{0}`", parts[1]); + // If the code syntax is {{functionName $varName}} use $varName instead of $input + string value = new VarBlock(parts[1], this.Log).Render(variablesClone); + variablesClone.Update(value); + } + + var result = await function.InvokeWithCustomInputAsync( + variablesClone, + context.Memory, + context.Skills, + this.Log, + context.CancellationToken); + + if (result.ErrorOccurred) + { + this.Log.LogError( + "Semantic function references a function `{0}` of incompatible type `{1}`: defaulting to an empty result", + functionName, function.GetType()); + return ""; + } + + return result.Result; + } + + #region private ================================================================================ + + private bool _validated; + + private bool GetFunctionFromSkillCollection(IReadOnlySkillCollection skills, string functionName, + [NotNullWhen(true)] out ISKFunction? function) + { + // Search in the global space (only native functions there) + if (skills.HasNativeFunction(functionName)) + { + function = skills.GetNativeFunction(functionName); + return true; + } + + // If the function contains a skill name... + if (functionName.Contains('.', StringComparison.InvariantCulture)) + { + var functionNameParts = functionName.Split('.'); + if (functionNameParts.Length > 2) + { + this.Log.LogError("Invalid function name `{0}`", functionName); + throw new ArgumentOutOfRangeException( + $"Invalid function name `{functionName}`. " + + "A Function name can contain only one `.` to separate skill name from function name."); + } + + var skillName = functionNameParts[0]; + functionName = functionNameParts[1]; + + if (skills.HasNativeFunction(skillName, functionName)) + { + function = skills.GetNativeFunction(skillName, functionName); + return true; + } + + if (skills.HasSemanticFunction(skillName, functionName)) + { + function = skills.GetSemanticFunction(skillName, functionName); + return true; + } + } + + function = null; + return false; + } + + #endregion +} diff --git a/dotnet/src/SemanticKernel/TemplateEngine/Blocks/TextBlock.cs b/dotnet/src/SemanticKernel/TemplateEngine/Blocks/TextBlock.cs new file mode 100644 index 000000000000..c5e85996d195 --- /dev/null +++ b/dotnet/src/SemanticKernel/TemplateEngine/Blocks/TextBlock.cs @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel.Orchestration; + +namespace Microsoft.SemanticKernel.TemplateEngine.Blocks; + +internal class TextBlock : Block +{ + internal override BlockTypes Type => BlockTypes.Text; + + internal TextBlock(string content, ILogger? log = null) + : base(log) + { + this.Content = content; + } + + internal TextBlock(string text, int startIndex, int stopIndex, ILogger log) + : base(log) + { + this.Content = text.Substring(startIndex, stopIndex - startIndex); + } + + internal override bool IsValid(out string error) + { + error = ""; + return true; + } + + internal override string Render(ContextVariables? variables) + { + return this.Content; + } +} diff --git a/dotnet/src/SemanticKernel/TemplateEngine/Blocks/VarBlock.cs b/dotnet/src/SemanticKernel/TemplateEngine/Blocks/VarBlock.cs new file mode 100644 index 000000000000..0048f45d1e5d --- /dev/null +++ b/dotnet/src/SemanticKernel/TemplateEngine/Blocks/VarBlock.cs @@ -0,0 +1,88 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Text.RegularExpressions; +using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel.Orchestration; + +// ReSharper disable TemplateIsNotCompileTimeConstantProblem + +namespace Microsoft.SemanticKernel.TemplateEngine.Blocks; + +internal class VarBlock : Block +{ + private const char Prefix = '$'; + + internal override BlockTypes Type => BlockTypes.Variable; + + internal string Name => this.VarName(); + + internal VarBlock(string content, ILogger? log = null) : base(log) + { + this.Content = content; + } + +#pragma warning disable CA2254 // error strings are used also internally, not just for logging + internal override bool IsValid(out string error) + { + error = string.Empty; + + if (this.Content[0] != Prefix) + { + error = $"A variable must start with the symbol {Prefix}"; + this.Log.LogError(error); + return false; + } + + if (this.Content.Length < 2) + { + error = "The variable name is empty"; + this.Log.LogError(error); + return false; + } + + var varName = this.VarName(); + if (!Regex.IsMatch(varName, "^[a-zA-Z0-9_]*$")) + { + error = $"The variable name '{varName}' contains invalid characters. " + + "Only alphanumeric chars and underscore are allowed."; + this.Log.LogError(error); + return false; + } + + return true; + } +#pragma warning restore CA2254 + + internal override string Render(ContextVariables? variables) + { + if (variables == null) { return string.Empty; } + + var name = this.VarName(); + if (!string.IsNullOrEmpty(name)) + { + var exists = variables.Get(name, out string value); + if (!exists) { this.Log.LogWarning("Variable `{0}{1}` not found", Prefix, name); } + + return exists ? value : ""; + } + + this.Log.LogError("Variable rendering failed, the variable name is empty"); + throw new TemplateException( + TemplateException.ErrorCodes.SyntaxError, "Variable rendering failed, the variable name is empty."); + } + + internal static bool HasVarPrefix(string text) + { + return !string.IsNullOrEmpty(text) && text.Length > 0 && text[0] == Prefix; + } + + internal static bool IsValidVarName(string text) + { + return Regex.IsMatch(text, "^[a-zA-Z0-9_]*$"); + } + + private string VarName() + { + return this.Content.Length < 2 ? "" : this.Content[1..]; + } +} diff --git a/dotnet/src/SemanticKernel/TemplateEngine/IPromptTemplateEngine.cs b/dotnet/src/SemanticKernel/TemplateEngine/IPromptTemplateEngine.cs new file mode 100644 index 000000000000..6255c5052cc0 --- /dev/null +++ b/dotnet/src/SemanticKernel/TemplateEngine/IPromptTemplateEngine.cs @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.SemanticKernel.Orchestration; +using Microsoft.SemanticKernel.TemplateEngine.Blocks; + +namespace Microsoft.SemanticKernel.TemplateEngine; + +/// +/// Prompt template engine interface. +/// +public interface IPromptTemplateEngine +{ + /// + /// Given a prompt template string, extract all the blocks (text, variables, function calls) + /// + /// Prompt template (see skprompt.txt files) + /// Whether to validate the blocks syntax, or just return the blocks found, which could contain invalid code + /// A list of all the blocks, ie the template tokenized in text, variables and function calls + IList ExtractBlocks( + string? templateText, + bool validate = true); + + /// + /// Given a prompt template, replace the variables with their values and execute the functions replacing their + /// reference with the function result. + /// + /// Prompt template (see skprompt.txt files) + /// Access into the current kernel execution context + /// The prompt template ready to be used for an AI request + Task RenderAsync( + string templateText, + SKContext executionContext); + + /// + /// Given a a list of blocks render each block and compose the final result + /// + /// Template blocks generated by ExtractBlocks + /// Access into the current kernel execution context + /// The prompt template ready to be used for an AI request + Task RenderAsync( + IList blocks, + SKContext executionContext); + + /// + /// Given a list of blocks, render the Variable Blocks, replacing placeholders with the actual value in memory + /// + /// List of blocks, typically all the blocks found in a template + /// Container of all the temporary variables known to the kernel + /// An updated list of blocks where Variable Blocks have rendered to Text Blocks + IList RenderVariables( + IList blocks, + ContextVariables? variables); + + /// + /// Given a list of blocks, render the Code Blocks, executing the functions and replacing placeholders with the functions result + /// + /// List of blocks, typically all the blocks found in a template + /// Access into the current kernel execution context + /// An updated list of blocks where Code Blocks have rendered to Text Blocks + Task> RenderCodeAsync( + IList blocks, + SKContext executionContext); +} diff --git a/dotnet/src/SemanticKernel/TemplateEngine/PromptTemplateEngine.cs b/dotnet/src/SemanticKernel/TemplateEngine/PromptTemplateEngine.cs new file mode 100644 index 000000000000..9ce29b7f2f88 --- /dev/null +++ b/dotnet/src/SemanticKernel/TemplateEngine/PromptTemplateEngine.cs @@ -0,0 +1,258 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.SemanticKernel.Orchestration; +using Microsoft.SemanticKernel.TemplateEngine.Blocks; + +namespace Microsoft.SemanticKernel.TemplateEngine; + +/// +/// Given a prompt, that might contain references to variables and functions: +/// - Get the list of references +/// - Resolve each reference +/// - Variable references are resolved using the context variables +/// - Function references are resolved invoking those functions +/// - Functions can be invoked passing in variables +/// - Functions do not receive the context variables, unless specified using a special variable +/// - Functions can be invoked in order and in parallel so the context variables must be immutable when invoked within the template +/// +public class PromptTemplateEngine : IPromptTemplateEngine +{ + public PromptTemplateEngine(ILogger? log = null) + { + this._log = log ?? NullLogger.Instance; + } + + /// + /// Given a prompt template string, extract all the blocks (text, variables, function calls) + /// + /// Prompt template (see skprompt.txt files) + /// Whether to validate the blocks syntax, or just return the blocks found, which could contain invalid code + /// A list of all the blocks, ie the template tokenized in text, variables and function calls + public IList ExtractBlocks(string? templateText, bool validate = true) + { + this._log.LogTrace("Extracting blocks from template: {0}", templateText); + var blocks = this.TokenizeInternal(templateText); + if (validate) { ValidateBlocksSyntax(blocks); } + + return blocks; + } + + /// + /// Given a prompt template, replace the variables with their values and execute the functions replacing their + /// reference with the function result. + /// + /// Prompt template (see skprompt.txt files) + /// Access into the current kernel execution context + /// The prompt template ready to be used for an AI request + public async Task RenderAsync( + string templateText, + SKContext executionContext) + { + this._log.LogTrace("Rendering string template: {0}", templateText); + var blocks = this.ExtractBlocks(templateText); + return await this.RenderAsync(blocks, executionContext); + } + + /// + /// Given a a list of blocks render each block and compose the final result + /// + /// Template blocks generated by ExtractBlocks + /// Access into the current kernel execution context + /// The prompt template ready to be used for an AI request + public async Task RenderAsync( + IList blocks, + SKContext executionContext) + { + this._log.LogTrace("Rendering list of {0} blocks", blocks.Count); + var result = new StringBuilder(); + foreach (var block in blocks) + { + switch (block.Type) + { + case BlockTypes.Text: + result.Append(block.Content); + break; + + case BlockTypes.Variable: + result.Append(block.Render(executionContext.Variables)); + break; + + case BlockTypes.Code: + result.Append(await block.RenderCodeAsync(executionContext)); + break; + + case BlockTypes.Undefined: + default: + throw new InvalidEnumArgumentException(nameof(blocks), (int)block.Type, typeof(BlockTypes)); + } + } + + this._log.LogDebug("Rendered prompt: {0}", result); + return result.ToString(); + } + + /// + /// Given a list of blocks, render the Variable Blocks, replacing placeholders with the actual value in memory + /// + /// List of blocks, typically all the blocks found in a template + /// Container of all the temporary variables known to the kernel + /// An updated list of blocks where Variable Blocks have rendered to Text Blocks + public IList RenderVariables(IList blocks, ContextVariables? variables) + { + this._log.LogTrace("Rendering variables"); + return blocks.Select(block => block.Type != BlockTypes.Variable + ? block + : new TextBlock(block.Render(variables), this._log)).ToList(); + } + + /// + /// Given a list of blocks, render the Code Blocks, executing the functions and replacing placeholders with the functions result + /// + /// List of blocks, typically all the blocks found in a template + /// Access into the current kernel execution context + /// An updated list of blocks where Code Blocks have rendered to Text Blocks + public async Task> RenderCodeAsync( + IList blocks, + SKContext executionContext) + { + this._log.LogTrace("Rendering code"); + var updatedBlocks = new List(); + foreach (var block in blocks) + { + if (block.Type != BlockTypes.Code) + { + updatedBlocks.Add(block); + } + else + { + var codeResult = await block.RenderCodeAsync(executionContext); + updatedBlocks.Add(new TextBlock(codeResult, this._log)); + } + } + + return updatedBlocks; + } + + #region private ================================================================================ + + private readonly ILogger _log; + + // Blocks delimitation + private const char Starter = '{'; + private const char Ender = '}'; + + private IList TokenizeInternal(string? template) + { + // An empty block consists of 4 chars: "{{}}" + const int EMPTY_CODE_BLOCK_LENGTH = 4; + // A block shorter than 5 chars is either empty or invalid, e.g. "{{ }}" and "{{$}}" + const int MIN_CODE_BLOCK_LENGTH = EMPTY_CODE_BLOCK_LENGTH + 1; + + // Render NULL to "" + if (template == null) + { + return new List { new TextBlock("", this._log) }; + } + + // If the template is "empty" return the content as a text block + if (template.Length < MIN_CODE_BLOCK_LENGTH) + { + return new List { new TextBlock(template, this._log) }; + } + + var blocks = new List(); + + var cursor = 0; + var endOfLastBlock = 0; + + var startPos = 0; + var startFound = false; + + while (cursor < template.Length - 1) + { + // When "{{" is found + if (template[cursor] == Starter && template[cursor + 1] == Starter) + { + startPos = cursor; + startFound = true; + } + // When "}}" is found + else if (startFound && template[cursor] == Ender && template[cursor + 1] == Ender) + { + // If there is plain text between the current var/code block and the previous one, capture that as a TextBlock + if (startPos > endOfLastBlock) + { + blocks.Add(new TextBlock(template, endOfLastBlock, startPos, this._log)); + } + + // Skip ahead to the second "}" of "}}" + cursor++; + + // Extract raw block + var contentWithDelimiters = SubStr(template, startPos, cursor + 1); + + // Remove "{{" and "}}" delimiters and trim empty chars + var contentWithoutDelimiters = contentWithDelimiters + .Substring(2, contentWithDelimiters.Length - EMPTY_CODE_BLOCK_LENGTH).Trim(); + + if (contentWithoutDelimiters.Length == 0) + { + // If what is left is empty, consider the raw block a Text Block + blocks.Add(new TextBlock(contentWithDelimiters, this._log)); + } + else + { + // If the block starts with "$" it's a variable + if (VarBlock.HasVarPrefix(contentWithoutDelimiters)) + { + // Note: validation is delayed to the time VarBlock is rendered + blocks.Add(new VarBlock(contentWithoutDelimiters, this._log)); + } + else + { + // Note: validation is delayed to the time CodeBlock is rendered + blocks.Add(new CodeBlock(contentWithoutDelimiters, this._log)); + } + } + + endOfLastBlock = cursor + 1; + startFound = false; + } + + cursor++; + } + + // If there is something left after the last block, capture it as a TextBlock + if (endOfLastBlock < template.Length) + { + blocks.Add(new TextBlock(template, endOfLastBlock, template.Length, this._log)); + } + + return blocks; + } + + private static string SubStr(string text, int startIndex, int stopIndex) + { + return text.Substring(startIndex, stopIndex - startIndex); + } + + private static void ValidateBlocksSyntax(IList blocks) + { + foreach (var block in blocks) + { + if (!block.IsValid(out var error)) + { + throw new TemplateException(TemplateException.ErrorCodes.SyntaxError, error); + } + } + } + + #endregion +} diff --git a/dotnet/src/SemanticKernel/TemplateEngine/TemplateException.cs b/dotnet/src/SemanticKernel/TemplateEngine/TemplateException.cs new file mode 100644 index 000000000000..185a190969ac --- /dev/null +++ b/dotnet/src/SemanticKernel/TemplateEngine/TemplateException.cs @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.SemanticKernel.Diagnostics; + +namespace Microsoft.SemanticKernel.TemplateEngine; + +/// +/// Template exception. +/// +public class TemplateException : Exception +{ + /// + /// Error codes for . + /// + public enum ErrorCodes + { + /// + /// Unknown. + /// + Unknown = -1, + + /// + /// Syntax error. + /// + SyntaxError = 0, + } + + /// + /// Error code. + /// + public ErrorCodes ErrorCode { get; set; } + + /// + /// Initializes a new instance of the class. + /// + /// The template error type. + /// The exception message. + public TemplateException(ErrorCodes errCode, string? message = null) + : base(errCode, message) + { + this.ErrorCode = errCode; + } +} diff --git a/dotnet/src/SemanticKernel/Text/Json.cs b/dotnet/src/SemanticKernel/Text/Json.cs new file mode 100644 index 000000000000..73ef89835f07 --- /dev/null +++ b/dotnet/src/SemanticKernel/Text/Json.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Text.Json; + +namespace Microsoft.SemanticKernel.Text; + +internal static class Json +{ + internal static string Serialize(object? o) + { + return JsonSerializer.Serialize(o, s_options); + } + + internal static T? Deserialize(string json) + { + return JsonSerializer.Deserialize(json, s_options); + } + + internal static string ToJson(this object o) + { + return JsonSerializer.Serialize(o, s_options); + } + + #region private ================================================================================ + + private static readonly JsonSerializerOptions s_options = new() + { + WriteIndented = true, + MaxDepth = 20, + AllowTrailingCommas = true, + PropertyNameCaseInsensitive = true, + ReadCommentHandling = JsonCommentHandling.Skip, + }; + + #endregion +} diff --git a/dotnet/src/SemanticKernel/Text/StringExtensions.cs b/dotnet/src/SemanticKernel/Text/StringExtensions.cs new file mode 100644 index 000000000000..fcf60d771127 --- /dev/null +++ b/dotnet/src/SemanticKernel/Text/StringExtensions.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.SemanticKernel.Text; + +internal static class StringExtensions +{ + [SuppressMessage("Globalization", "CA1309:Use ordinal StringComparison", Justification = "By design")] + internal static bool EqualsIgnoreCase(this string src, string value) + { + return src.Equals(value, StringComparison.InvariantCultureIgnoreCase); + } +} diff --git a/samples/apps/.eslingrc.js b/samples/apps/.eslingrc.js new file mode 100644 index 000000000000..7b4da7967410 --- /dev/null +++ b/samples/apps/.eslingrc.js @@ -0,0 +1,50 @@ +module.exports = { + env: { + browser: true, + es2021: true, + }, + extends: ['plugin:react/recommended', 'standard-with-typescript'], + ignorePatterns: ['build', '.*.js', '*.config.js', 'node_modules'], + overrides: [], + parserOptions: { + project: './tsconfig.json', + ecmaVersion: 'latest', + sourceType: 'module', + }, + plugins: ['react', '@typescript-eslint', 'import', 'react-hooks', 'react-security'], + rules: { + '@typescript-eslint/brace-style': ['off'], + '@typescript-eslint/space-before-function-paren': [ + 'error', + { anonymous: 'always', named: 'never', asyncArrow: 'always' }, + ], + '@typescript-eslint/semi': ['error', 'always'], + '@typescript-eslint/triple-slash-reference': ['error', { types: 'prefer-import' }], + '@typescript-eslint/indent': ['off'], + '@typescript-eslint/comma-dangle': ['error', 'always-multiline'], + '@typescript-eslint/strict-boolean-expressions': 'off', + '@typescript-eslint/member-delimiter-style': [ + 'error', + { + multiline: { + delimiter: 'semi', + requireLast: true, + }, + singleline: { + delimiter: 'semi', + requireLast: false, + }, + }, + ], + '@typescript-eslint/explicit-function-return-type': 'off', + 'react/jsx-props-no-spreading': 'warn', + 'react-hooks/rules-of-hooks': 'error', + 'react-hooks/exhaustive-deps': 'warn', + 'react/react-in-jsx-scope': 'off', + }, + settings: { + react: { + version: 'detect', + }, + }, +}; diff --git a/samples/apps/auth-api-webapp-react/.env b/samples/apps/auth-api-webapp-react/.env new file mode 100644 index 000000000000..dc1fffca82e0 --- /dev/null +++ b/samples/apps/auth-api-webapp-react/.env @@ -0,0 +1,8 @@ +REACT_APP_GRAPH_CLIENT_ID= +REACT_APP_GRAPH_SCOPES=User.Read,Files.ReadWrite,Tasks.ReadWrite,Mail.Send +REACT_APP_FUNCTION_URI=http://localhost:7071 +REACT_APP_OPEN_AI_KEY= +REACT_APP_OPEN_AI_MODEL= +REACT_APP_AZURE_OPEN_AI_KEY= +REACT_APP_AZURE_OPEN_AI_DEPLOYMENT= +REACT_APP_AZURE_OPEN_AI_ENDPOINT= diff --git a/samples/apps/auth-api-webapp-react/README.md b/samples/apps/auth-api-webapp-react/README.md new file mode 100644 index 000000000000..afe047a41c1d --- /dev/null +++ b/samples/apps/auth-api-webapp-react/README.md @@ -0,0 +1,45 @@ +# Authenticated API’s Sample Learning App + +> [!IMPORTANT] +> This learning sample is for educational purposes only and should not be used in any production +> use case. It is intended to highlight concepts of Semantic Kernel and not any +> architectural / security design practices to be used. + +### Watch the Authenticated API’s Sample Quick Start [Video](https://aka.ms/SK-Samples-AuthAPI-Video) + +## Running the sample + +1. You will need an [Open AI Key](https://openai.com/api/) or + [Azure Open AI Service key](https://learn.microsoft.com/azure/cognitive-services/openai/quickstart) + for this sample +2. Ensure the service API is already running `http://localhost:7071`. If not learn + how to start it [here](../../dotnet/api-azure-function/README.md). +3. You will also need to + [register your application](https://learn.microsoft.com/azure/active-directory/develop/quickstart-register-app) + in the Azure Portal. Follow the steps to register your app + [here](https://learn.microsoft.com/azure/active-directory/develop/quickstart-register-app). + - Select **`single-page application (SPA)`** as platform type, and the Redirect URI will be **`http://localhost:3000`** + - It is recommended you use the **`Personal Microsoft accounts`** account type for this sample. +4. Once registered, copy the **Application (client) ID** from the Azure Portal and paste + the GUID into the **[.env](.env)** file next to `REACT_APP_GRAPH_CLIENT_ID=` (first line of the .env file). +5. **Run** the following command `yarn install` (if you have never run the sample before) + and/or `yarn start` from the command line. +6. A browser will automatically open, otherwise you can navigate to `http://localhost:3000` to use the sample. + +## About the Authenticated API’s Sample + +The Authenticated API’s sample allows you to use authentication to connect to the +Microsoft Graph using your personal account. + +If you don’t have a Microsoft account or do not want to connect to it, +you can review the code to see the patterns needed to call out to APIs. + +The sample highlights connecting to Microsoft Graph and calling APIs for Outlook, OneDrive, and ToDo. +Each function will call Microsoft Graph and/or Open AI to perform the tasks. + +> [!CAUTION] +> Each function will call Open AI which will use tokens that you will be billed for. + +## Next Steps + +Join the [Discord community](https://aka.ms/SKDiscord). diff --git a/samples/apps/auth-api-webapp-react/package.json b/samples/apps/auth-api-webapp-react/package.json new file mode 100644 index 000000000000..8d337c7aebad --- /dev/null +++ b/samples/apps/auth-api-webapp-react/package.json @@ -0,0 +1,49 @@ +{ + "name": "starter-identity-webapp-react", + "version": "0.1.0", + "private": true, + "dependencies": { + "@azure/msal-browser": "^2.33.0", + "@azure/msal-react": "^1.5.3", + "@fluentui/react-components": "^9.15.6", + "msal": "^1.4.17", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "web-vitals": "^3.1.1" + }, + "devDependencies": { + "@testing-library/jest-dom": "^5.14.1", + "@testing-library/react": "^14.0.0", + "@testing-library/user-event": "^14.4.3", + "@types/jest": "^29.4.0", + "@types/node": "^18.14.0", + "@types/react": "^18.0.28", + "@types/react-dom": "^18.0.11", + "react-scripts": "5.0.1", + "typescript": "^4.9.5" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test", + "eject": "react-scripts eject" + }, + "eslintConfig": { + "extends": [ + "react-app", + "react-app/jest" + ] + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + } +} diff --git a/samples/apps/auth-api-webapp-react/public/favicon.ico b/samples/apps/auth-api-webapp-react/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..bfe873eb228f98720fe0ed18c638daa13906958f GIT binary patch literal 17174 zcmeHOJ#Q015PgP|sUltJ%A8D-(o!TUkc{NQ1+uPjPZg!4M<`cP($XUeDk>yUiu?k8 z10_|E2qK~~GdH`@9=;55lyHgjMj7wN?OyiY%vw?RV_+}{(X$}` zkiRp}d#2#VM9RE@(KQaI zUnaWs7TXteX8N); ON)^VQz<8Ga>-ryC<7BY_ literal 0 HcmV?d00001 diff --git a/samples/apps/auth-api-webapp-react/public/index.html b/samples/apps/auth-api-webapp-react/public/index.html new file mode 100644 index 000000000000..ba8b8c439363 --- /dev/null +++ b/samples/apps/auth-api-webapp-react/public/index.html @@ -0,0 +1,37 @@ + + + + + + + + + + + Authentication and API's App + + + + +
+ + + + \ No newline at end of file diff --git a/samples/apps/auth-api-webapp-react/src/App.css b/samples/apps/auth-api-webapp-react/src/App.css new file mode 100644 index 000000000000..2bd4b20d460d --- /dev/null +++ b/samples/apps/auth-api-webapp-react/src/App.css @@ -0,0 +1,45 @@ +body { + padding: 0px; + margin: 0px; +} + +#container { + display: flex; + flex-direction: column; + align-items: stretch; + justify-content: space-between; +} + +#header { + background-color: #9c2153; + width: 100%; + height: 40px; + color: #FFF; + display: flex; +} + +#header h1 { + padding-left: 20px; + align-items: center; + display: flex; +} + +#content { + display: flex; + align-items: stretch; + flex-direction: row; + padding-top: 12px; + gap: 80px; +} + +#main { + display: flex; + align-items: stretch; + flex-direction: column; + gap: 10px; +} + +#tipbar { + background-color: #FAF9F8; + width: 360px; +} \ No newline at end of file diff --git a/samples/apps/auth-api-webapp-react/src/App.tsx b/samples/apps/auth-api-webapp-react/src/App.tsx new file mode 100644 index 000000000000..1ac35776e119 --- /dev/null +++ b/samples/apps/auth-api-webapp-react/src/App.tsx @@ -0,0 +1,216 @@ +// Copyright (c) Microsoft. All rights reserved. + +import { AuthenticatedTemplate, useAccount, useIsAuthenticated, useMsal } from '@azure/msal-react'; +import { Subtitle1, Tab, TabList } from '@fluentui/react-components'; +import { FC, useEffect, useState } from 'react'; +import FunctionProbe from './components/FunctionProbe'; +import InteractWithGraph from './components/InteractWithGraph'; +import QuickTips, { ITipGroup } from './components/QuickTips'; +import ServiceConfig from './components/ServiceConfig'; +import YourInfo from './components/YourInfo'; +import { IKeyConfig } from './model/KeyConfig'; + +const App: FC = () => { + enum AppState { + ProbeForFunction = 0, + YourInfo = 1, + Setup = 2, + InteractWithGraph = 3, + } + + const isAuthenticated = useIsAuthenticated(); + const { instance, accounts } = useMsal(); + const account = useAccount(accounts[0] || {}); + const [appState, setAppState] = useState(AppState.ProbeForFunction); + const [selectedTabValue, setSelectedTabValue] = useState(isAuthenticated ? 'setup' : 'yourinfo'); + const [config, setConfig] = useState(); + + const appStateToTabValueMap = new Map([ + [AppState.Setup, 'setup'], + [AppState.InteractWithGraph, 'interact'], + [AppState.YourInfo, 'yourinfo'], + ]); + const tabValueToAppStateMap = new Map([ + ['setup', AppState.Setup], + ['yourinfo', AppState.YourInfo], + ['interact', AppState.InteractWithGraph], + ]); + + useEffect(() => { + changeAppState(appState); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [appState]); + + useEffect(() => { + if (isAuthenticated) { + setAppState(AppState.Setup); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isAuthenticated]); + + const changeAppState = function (newAppState: AppState) { + setAppState(newAppState); + setSelectedTabValue(appStateToTabValueMap.get(newAppState) ?? 'setup'); + }; + const changeTabValue = function (newTabValue: string) { + setSelectedTabValue(newTabValue); + setAppState(tabValueToAppStateMap.get(newTabValue) ?? AppState.Setup); + }; + + useEffect(() => { + const fetchAsync = async () => { + if (config === undefined || config === null) { + return; + } + + var result = await instance.acquireTokenSilent({ + account: account !== null ? account : undefined, + scopes: (process.env.REACT_APP_GRAPH_SCOPES as string).split(','), + forceRefresh: false, + }); + + config.graphToken = result.accessToken; + setConfig(config); + }; + + fetchAsync(); + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [config]); + + const tips: ITipGroup[] = [ + { + header: 'Useful Resources', + items: [ + { + title: 'Join Discord Community', + uri: 'https://aka.ms/skdiscord', + }, + { + title: 'Read Documentation', + uri: 'https://aka.ms/SKDoc-Auth-API', + }, + ], + }, + { + header: 'Functions used in this sample', + items: [ + { + title: 'Summarize', + uri: 'https://github.com/microsoft/semantic-kernel/tree/main/samples/skills/SummarizeSkill/Summarize', + }, + { + title: 'AppendTextAsync', + uri: 'https://github.com/microsoft/semantic-kernel/blob/main/dotnet/src/SemanticKernel.Skills/Skills/Skills/Document/DocumentSkill.cs#L86', + }, + { + title: 'UploadFileAsync', + uri: 'https://github.com/microsoft/semantic-kernel/blob/main/dotnet/src/SemanticKernel.Skills/Skills/Skills/Productivity/CloudDriveSkill.cs#L61', + }, + { + title: 'CreateLinkAsync', + uri: 'https://github.com/microsoft/semantic-kernel/blob/main/dotnet/src/SemanticKernel.Skills/Skills/Skills/Productivity/CloudDriveSkill.cs#L88', + }, + { + title: 'GetMyEmailAddressAsync', + uri: 'https://github.com/microsoft/semantic-kernel/blob/main/dotnet/src/SemanticKernel.Skills/Skills/Skills/Productivity/EmailSkill.cs#L55', + }, + { + title: 'SendEmailAsync', + uri: 'https://github.com/microsoft/semantic-kernel/blob/main/dotnet/src/SemanticKernel.Skills/Skills/Skills/Productivity/EmailSkill.cs#L65', + }, + { + title: 'AddTaskAsync', + uri: 'https://github.com/microsoft/semantic-kernel/blob/main/dotnet/src/SemanticKernel.Skills/Skills/Skills/Productivity/TaskListSkill.cs#L71', + }, + ], + }, + { + header: 'Local SK URL', + items: [ + { + title: process.env.REACT_APP_FUNCTION_URI as string, + uri: process.env.REACT_APP_FUNCTION_URI as string, + }, + ], + }, + ]; + + return ( +
+ + + {appState === AppState.ProbeForFunction ? ( + setAppState(isAuthenticated ? AppState.Setup : AppState.YourInfo)} + /> + ) : null} +
+
+ {appState === AppState.ProbeForFunction ? null : ( + changeTabValue(data.value as string)} + > + Your Info + + Setup + + + Interact + + + )} +
+ {appState === AppState.YourInfo ? : null} + + {appState === AppState.Setup ? ( + { + setConfig(config); + setAppState(AppState.InteractWithGraph); + }} + /> + ) : null} + + {appState === AppState.InteractWithGraph ? ( + + { + changeAppState(appState - 1); + }} + /> + + ) : null} +
+
+ {appState === AppState.ProbeForFunction ? null : ( +
+ +
+ )} +
+
+ ); +}; + +export default App; diff --git a/samples/apps/auth-api-webapp-react/src/components/FunctionProbe.tsx b/samples/apps/auth-api-webapp-react/src/components/FunctionProbe.tsx new file mode 100644 index 000000000000..549d01aa3995 --- /dev/null +++ b/samples/apps/auth-api-webapp-react/src/components/FunctionProbe.tsx @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft. All rights reserved. + +import { Body1, Spinner, Title3 } from '@fluentui/react-components'; +import { FC, useEffect } from 'react'; + +interface IData { + uri: string; + onFunctionFound: () => void; +} + +const FunctionProbe: FC = ({ uri, onFunctionFound }) => { + useEffect(() => { + const fetchAsync = async () => { + try { + var result = await fetch(`${uri}/api/ping`); + + if (result.ok) { + onFunctionFound(); + } + } catch {} + }; + + fetchAsync(); + }); + + return ( +
+ Looking for your function + + + This sample expects to find the Azure Function from samples/starter-api-azure-function{' '} + running at {uri} + + + Run your Azure Function locally using{' '} + + Visual Studio + + ,{' '} + + Visual Studio Code + {' '} + or from the command line using the{' '} + + Azure Functions Core Tools + + +
+ ); +}; + +export default FunctionProbe; diff --git a/samples/apps/auth-api-webapp-react/src/components/InteractWithGraph.tsx b/samples/apps/auth-api-webapp-react/src/components/InteractWithGraph.tsx new file mode 100644 index 000000000000..85d7afcde6f1 --- /dev/null +++ b/samples/apps/auth-api-webapp-react/src/components/InteractWithGraph.tsx @@ -0,0 +1,157 @@ +// Copyright (c) Microsoft. All rights reserved. + +import { Body1, Button, Image, Textarea, Title3 } from '@fluentui/react-components'; +import React, { FC } from 'react'; +import wordLogo from '../../src/word.png'; +import { useSemanticKernel } from '../hooks/useSemanticKernel'; +import { IKeyConfig } from '../model/KeyConfig'; +import InteractionButton from './InteractionButton'; + +interface IData { + uri: string; + config: IKeyConfig; + onBack: () => void; +} + +const InteractWithGraph: FC = ({ uri, config, onBack }) => { + const sk = useSemanticKernel(uri); + const defaultText = `A glacier is a persistent body of dense ice that is constantly moving under its own weight. A glacier forms where the accumulation of snow exceeds its ablation over many years, often centuries. It acquires distinguishing features, such as crevasses and seracs, as it slowly flows and deforms under stresses induced by its weight. As it moves, it abrades rock and debris from its substrate to create landforms such as cirques, moraines, or fjords. Although a glacier may flow into a body of water, it forms only on land and is distinct from the much thinner sea ice and lake ice that form on the surface of bodies of water.`; + const filename = 'AuthenticationSampleSummary.docx'; + const path = '%temp%\\' + filename; + const destinationPath = '/' + filename; + + const [text, setText] = React.useState(defaultText); + + const runTask1 = async () => { + try { + //get summary + var summary = await sk.invokeAsync(config, { value: text }, 'summarizeskill', 'summarize'); + + //write document + await sk.invokeAsync( + config, + { + value: summary.value, + inputs: [{ key: 'filePath', value: path }], + }, + 'documentskill', + 'appendtextasync', + ); + + //upload to onedrive + await sk.invokeAsync( + config, + { + value: path, + inputs: [{ key: 'destinationPath', value: destinationPath }], + }, + 'clouddriveskill', + 'uploadfileasync', + ); + } catch { + alert('Something went wrong.'); + } + }; + + const runTask2 = async () => { + try { + var shareLink = await sk.invokeAsync( + config, + { value: destinationPath }, + 'clouddriveskill', + 'createlinkasync', + ); + var myEmail = await sk.invokeAsync(config, { value: '' }, 'emailskill', 'getmyemailaddressasync'); + + await sk.invokeAsync( + config, + { + value: `Here's the link: ${shareLink.value}\n\nReminder: Please delete the document on your OneDrive after you finish with this sample app.`, + inputs: [ + { + key: 'recipients', + value: myEmail.value, + }, + { + key: 'subject', + value: 'Semantic Kernel Authentication Sample Project Document Link', + }, + ], + }, + 'emailskill', + 'sendemailasync', + ); + } catch { + alert('Something went wrong.'); + } + }; + + const runTask3 = async () => { + try { + var reminderDate = new Date(); + reminderDate.setDate(reminderDate.getDate() + 3); + + await sk.invokeAsync( + config, + { + value: 'Remind me to follow up re the authentication sample email', + inputs: [ + { + key: 'reminder', + value: reminderDate.toISOString(), + }, + ], + }, + 'tasklistskill', + 'addtaskasync', + ); + } catch { + alert('Something went wrong.'); + } + }; + + return ( +
+ Interact with data and services + + You can interact with data and Microsoft services for your account. Ask questions about your data or ask + for help to complete a task. + + +
+
+ + Sample Doc: {filename} +
+ +