├── .editorconfig ├── .gitattributes ├── .github └── workflows │ ├── dotnet.yml │ └── nugetpush.yml ├── .gitignore ├── README.md ├── UniversalLLMFunctionCaller.sln ├── UniversalLLMFunctionCaller ├── FunctionCall.cs ├── UniversalLLMFunctionCaller.cs ├── UniversalLLMFunctionCaller.csproj └── UniversalLLMFunctionCallerInternalFunctions.cs ├── UniversalLLMFunctionCallerDemo ├── Plugins │ ├── EmailPluginFake.cs │ ├── FileIOPlugin.cs │ ├── MathPlugin.cs │ ├── TextPlugin.cs │ ├── ThrowingEmailPluginFake.cs │ ├── TimePlugin.cs │ └── WaitPlugin.cs ├── Program.cs └── UniversalLLMFunctionCallerDemo.csproj └── UniversalLLMFunctionCallerUnitTests ├── GlobalUsings.cs ├── Plugins ├── EmailPluginFake.cs ├── FileIOPlugin.cs ├── MathPlugin.cs ├── TextPlugin.cs ├── ThrowingEmailPluginFake.cs ├── TimePlugin.cs └── WaitPlugin.cs ├── UnitTest1.cs └── UniversalLLMFunctionCallerUnitTests.csproj /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.cs] 2 | 3 | # SKEXP0054: Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. 4 | dotnet_diagnostic.SKEXP0054.severity = none 5 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.github/workflows/dotnet.yml: -------------------------------------------------------------------------------- 1 | name: .NET 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v3 16 | - name: Setup .NET 17 | uses: actions/setup-dotnet@v3 18 | with: 19 | dotnet-version: 6.0.x 20 | - name: Restore dependencies 21 | run: dotnet restore 22 | - name: Build 23 | run: dotnet build --no-restore 24 | - name: Test 25 | run: dotnet test --no-build --verbosity normal 26 | env: 27 | mistral_key: ${{ secrets.MISTRAL_KEY }} 28 | bing_key: ${{ secrets.BING_KEY }} 29 | -------------------------------------------------------------------------------- /.github/workflows/nugetpush.yml: -------------------------------------------------------------------------------- 1 | name: Manual NuGet Push 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | version: 7 | description: 'NuGet Version' 8 | required: true 9 | default: '1.0.0' 10 | 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | - name: Setup .NET 20 | uses: actions/setup-dotnet@v3 21 | with: 22 | dotnet-version: 6.0.x 23 | - name: Restore dependencies 24 | run: dotnet restore 25 | - name: Build 26 | run: dotnet build --no-restore 27 | - name: Test 28 | run: dotnet test --no-build --verbosity normal 29 | env: 30 | mistral_key: ${{ secrets.MISTRAL_KEY }} 31 | bing_key: ${{ secrets.BING_KEY }} 32 | - name: Pack 33 | run: dotnet pack --configuration Release --no-build -o out 34 | - name: Publish 35 | run: dotnet nuget push out/*.nupkg --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Ww][Ii][Nn]32/ 27 | [Aa][Rr][Mm]/ 28 | [Aa][Rr][Mm]64/ 29 | bld/ 30 | [Bb]in/ 31 | [Oo]bj/ 32 | [Oo]ut/ 33 | [Ll]og/ 34 | [Ll]ogs/ 35 | 36 | # Visual Studio 2015/2017 cache/options directory 37 | .vs/ 38 | # Uncomment if you have tasks that create the project's static files in wwwroot 39 | #wwwroot/ 40 | 41 | # Visual Studio 2017 auto generated files 42 | Generated\ Files/ 43 | 44 | # MSTest test Results 45 | [Tt]est[Rr]esult*/ 46 | [Bb]uild[Ll]og.* 47 | 48 | # NUnit 49 | *.VisualState.xml 50 | TestResult.xml 51 | nunit-*.xml 52 | 53 | # Build Results of an ATL Project 54 | [Dd]ebugPS/ 55 | [Rr]eleasePS/ 56 | dlldata.c 57 | 58 | # Benchmark Results 59 | BenchmarkDotNet.Artifacts/ 60 | 61 | # .NET Core 62 | project.lock.json 63 | project.fragment.lock.json 64 | artifacts/ 65 | 66 | # ASP.NET Scaffolding 67 | ScaffoldingReadMe.txt 68 | 69 | # StyleCop 70 | StyleCopReport.xml 71 | 72 | # Files built by Visual Studio 73 | *_i.c 74 | *_p.c 75 | *_h.h 76 | *.ilk 77 | *.meta 78 | *.obj 79 | *.iobj 80 | *.pch 81 | *.pdb 82 | *.ipdb 83 | *.pgc 84 | *.pgd 85 | *.rsp 86 | *.sbr 87 | *.tlb 88 | *.tli 89 | *.tlh 90 | *.tmp 91 | *.tmp_proj 92 | *_wpftmp.csproj 93 | *.log 94 | *.vspscc 95 | *.vssscc 96 | .builds 97 | *.pidb 98 | *.svclog 99 | *.scc 100 | 101 | # Chutzpah Test files 102 | _Chutzpah* 103 | 104 | # Visual C++ cache files 105 | ipch/ 106 | *.aps 107 | *.ncb 108 | *.opendb 109 | *.opensdf 110 | *.sdf 111 | *.cachefile 112 | *.VC.db 113 | *.VC.VC.opendb 114 | 115 | # Visual Studio profiler 116 | *.psess 117 | *.vsp 118 | *.vspx 119 | *.sap 120 | 121 | # Visual Studio Trace Files 122 | *.e2e 123 | 124 | # TFS 2012 Local Workspace 125 | $tf/ 126 | 127 | # Guidance Automation Toolkit 128 | *.gpState 129 | 130 | # ReSharper is a .NET coding add-in 131 | _ReSharper*/ 132 | *.[Rr]e[Ss]harper 133 | *.DotSettings.user 134 | 135 | # TeamCity is a build add-in 136 | _TeamCity* 137 | 138 | # DotCover is a Code Coverage Tool 139 | *.dotCover 140 | 141 | # AxoCover is a Code Coverage Tool 142 | .axoCover/* 143 | !.axoCover/settings.json 144 | 145 | # Coverlet is a free, cross platform Code Coverage Tool 146 | coverage*.json 147 | coverage*.xml 148 | coverage*.info 149 | 150 | # Visual Studio code coverage results 151 | *.coverage 152 | *.coveragexml 153 | 154 | # NCrunch 155 | _NCrunch_* 156 | .*crunch*.local.xml 157 | nCrunchTemp_* 158 | 159 | # MightyMoose 160 | *.mm.* 161 | AutoTest.Net/ 162 | 163 | # Web workbench (sass) 164 | .sass-cache/ 165 | 166 | # Installshield output folder 167 | [Ee]xpress/ 168 | 169 | # DocProject is a documentation generator add-in 170 | DocProject/buildhelp/ 171 | DocProject/Help/*.HxT 172 | DocProject/Help/*.HxC 173 | DocProject/Help/*.hhc 174 | DocProject/Help/*.hhk 175 | DocProject/Help/*.hhp 176 | DocProject/Help/Html2 177 | DocProject/Help/html 178 | 179 | # Click-Once directory 180 | publish/ 181 | 182 | # Publish Web Output 183 | *.[Pp]ublish.xml 184 | *.azurePubxml 185 | # Note: Comment the next line if you want to checkin your web deploy settings, 186 | # but database connection strings (with potential passwords) will be unencrypted 187 | *.pubxml 188 | *.publishproj 189 | 190 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 191 | # checkin your Azure Web App publish settings, but sensitive information contained 192 | # in these scripts will be unencrypted 193 | PublishScripts/ 194 | 195 | # NuGet Packages 196 | *.nupkg 197 | # NuGet Symbol Packages 198 | *.snupkg 199 | # The packages folder can be ignored because of Package Restore 200 | **/[Pp]ackages/* 201 | # except build/, which is used as an MSBuild target. 202 | !**/[Pp]ackages/build/ 203 | # Uncomment if necessary however generally it will be regenerated when needed 204 | #!**/[Pp]ackages/repositories.config 205 | # NuGet v3's project.json files produces more ignorable files 206 | *.nuget.props 207 | *.nuget.targets 208 | 209 | # Microsoft Azure Build Output 210 | csx/ 211 | *.build.csdef 212 | 213 | # Microsoft Azure Emulator 214 | ecf/ 215 | rcf/ 216 | 217 | # Windows Store app package directories and files 218 | AppPackages/ 219 | BundleArtifacts/ 220 | Package.StoreAssociation.xml 221 | _pkginfo.txt 222 | *.appx 223 | *.appxbundle 224 | *.appxupload 225 | 226 | # Visual Studio cache files 227 | # files ending in .cache can be ignored 228 | *.[Cc]ache 229 | # but keep track of directories ending in .cache 230 | !?*.[Cc]ache/ 231 | 232 | # Others 233 | ClientBin/ 234 | ~$* 235 | *~ 236 | *.dbmdl 237 | *.dbproj.schemaview 238 | *.jfm 239 | *.pfx 240 | *.publishsettings 241 | orleans.codegen.cs 242 | 243 | # Including strong name files can present a security risk 244 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 245 | #*.snk 246 | 247 | # Since there are multiple workflows, uncomment next line to ignore bower_components 248 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 249 | #bower_components/ 250 | 251 | # RIA/Silverlight projects 252 | Generated_Code/ 253 | 254 | # Backup & report files from converting an old project file 255 | # to a newer Visual Studio version. Backup files are not needed, 256 | # because we have git ;-) 257 | _UpgradeReport_Files/ 258 | Backup*/ 259 | UpgradeLog*.XML 260 | UpgradeLog*.htm 261 | ServiceFabricBackup/ 262 | *.rptproj.bak 263 | 264 | # SQL Server files 265 | *.mdf 266 | *.ldf 267 | *.ndf 268 | 269 | # Business Intelligence projects 270 | *.rdl.data 271 | *.bim.layout 272 | *.bim_*.settings 273 | *.rptproj.rsuser 274 | *- [Bb]ackup.rdl 275 | *- [Bb]ackup ([0-9]).rdl 276 | *- [Bb]ackup ([0-9][0-9]).rdl 277 | 278 | # Microsoft Fakes 279 | FakesAssemblies/ 280 | 281 | # GhostDoc plugin setting file 282 | *.GhostDoc.xml 283 | 284 | # Node.js Tools for Visual Studio 285 | .ntvs_analysis.dat 286 | node_modules/ 287 | 288 | # Visual Studio 6 build log 289 | *.plg 290 | 291 | # Visual Studio 6 workspace options file 292 | *.opt 293 | 294 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 295 | *.vbw 296 | 297 | # Visual Studio LightSwitch build output 298 | **/*.HTMLClient/GeneratedArtifacts 299 | **/*.DesktopClient/GeneratedArtifacts 300 | **/*.DesktopClient/ModelManifest.xml 301 | **/*.Server/GeneratedArtifacts 302 | **/*.Server/ModelManifest.xml 303 | _Pvt_Extensions 304 | 305 | # Paket dependency manager 306 | .paket/paket.exe 307 | paket-files/ 308 | 309 | # FAKE - F# Make 310 | .fake/ 311 | 312 | # CodeRush personal settings 313 | .cr/personal 314 | 315 | # Python Tools for Visual Studio (PTVS) 316 | __pycache__/ 317 | *.pyc 318 | 319 | # Cake - Uncomment if you are using it 320 | # tools/** 321 | # !tools/packages.config 322 | 323 | # Tabs Studio 324 | *.tss 325 | 326 | # Telerik's JustMock configuration file 327 | *.jmconfig 328 | 329 | # BizTalk build output 330 | *.btp.cs 331 | *.btm.cs 332 | *.odx.cs 333 | *.xsd.cs 334 | 335 | # OpenCover UI analysis results 336 | OpenCover/ 337 | 338 | # Azure Stream Analytics local run output 339 | ASALocalRun/ 340 | 341 | # MSBuild Binary and Structured Log 342 | *.binlog 343 | 344 | # NVidia Nsight GPU debugger configuration file 345 | *.nvuser 346 | 347 | # MFractors (Xamarin productivity tool) working folder 348 | .mfractor/ 349 | 350 | # Local History for Visual Studio 351 | .localhistory/ 352 | 353 | # BeatPulse healthcheck temp database 354 | healthchecksdb 355 | 356 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 357 | MigrationBackup/ 358 | 359 | # Ionide (cross platform F# VS Code tools) working folder 360 | .ionide/ 361 | 362 | # Fody - auto-generated XML schema 363 | FodyWeavers.xsd -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # UniversalLLMFunctionCaller 2 | ![.NET build](https://github.com/Jenscaasen/UniversalLLMFunctionCaller/actions/workflows/dotnet.yml/badge.svg) 3 | 4 | The UniversalLLMFunctionCaller automatically invokes functions until a given goal or workflow is done. This works without the LLM natively supporting FunctionCalling (OpenAI), which therefore enables other LLM providers as Mistral, Anthropic, Meta, Google and others to also use function calling. 5 | Usage example with Mistral: 6 | 7 | ```C# 8 | IKernelBuilder builder = Kernel.CreateBuilder(); 9 | builder.AddMistralChatCompletion(Environment.GetEnvironmentVariable("mistral_key"), "mistral-small"); 10 | var kernel = builder.Build(); 11 | kernel.ImportPluginFromType("Time"); 12 | kernel.ImportPluginFromType("Math"); 13 | 14 | UniversalLLMFunctionCaller planner = new(kernel); 15 | string ask = "What is the current hour number, plus 5?"; 16 | Console.WriteLine(ask); 17 | string result = await planner.RunAsync(ask); 18 | Console.WriteLine(result); 19 | ``` 20 | 21 | This works with a single ask, but also supports a ChatHistory: 22 | 23 | ```C# 24 | [...] 25 | UniversalLLMFunctionCaller planner = new(kernel); 26 | 27 | ChatHistory history = new ChatHistory(); 28 | history.AddUserMessage("What is the capital of france?"); 29 | history.AddAssistantMessage("Paris"); 30 | history.AddUserMessage("And of Denmark?"); 31 | result = await planner.RunAsync(history); 32 | ``` 33 | Here, the plugin reads the context of the ChatHistory to understand what the actual question is ("What is the capital of Denmark?") instead of just using an out-of-context message ("And of Denmark?") 34 | 35 | According to internal tests, this Planner is both faster and more reliable compared to the Handlebar Planner, that offers a similar functionality out of the box for non-OpenAI LLMs. 36 | Tested with Mistral (nuget: JC.SemanticKernel.Connectors.Mistral). More testing with more use cases and more LLMs needed. Feel free to create issues and do pull requests. 37 | 38 | The Plugins in the Demo and Test Project are taken from the Main Semantic Kernel Repo: https://github.com/microsoft/semantic-kernel 39 | I do not claim ownership or copyright on them. 40 | 41 | ## FAQ 42 | 43 | ### Why don't you use JSON? 44 |
45 | Click to expand! 46 | 47 | The completely made up standard "TextPrompt3000" needs less tokens and is therefore faster and cheaper, especially if you have many Plugins registered. The algorithm relies on retries and telling the LLM their mistakes. This is to mitigate high costs and long runs. 48 | 49 |
50 | 51 | ### It doesn't work with my LLM. What should i do? 52 |
53 | Click to expand! 54 | 55 | Please create an issue on Github and share as many information as possible: what was the task, what plugins were used, what did the LLM respond? 56 | 57 |
58 | 59 | ### Does it realy work with all LLMs? 60 |
61 | Click to expand! 62 | 63 | No. For Mistral, the medium and small models work. The Tiny Model seems to lack a basic understanding of planning, and does not move forward in the process. An undefined minimum of cleverness needs to reside in the LLM for this to work 64 | 65 |
66 | 67 | ### This isn't a planner, it doesn't plan anything! 68 |
69 | Click to expand! 70 | 71 | True. Planner is the closest concept available in Semantic Kernel. There is no planning step to enable the real-time output of plugins to influence the calling of the next plugins. 72 | 73 |
74 | -------------------------------------------------------------------------------- /UniversalLLMFunctionCaller.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.8.34330.188 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UniversalLLMFunctionCaller", "UniversalLLMFunctionCaller\UniversalLLMFunctionCaller.csproj", "{660CAE90-1D47-4468-BE94-6D74C68B3E1F}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UniversalLLMFunctionCallerDemo", "UniversalLLMFunctionCallerDemo\UniversalLLMFunctionCallerDemo.csproj", "{C034FED1-65C8-4EF2-B0A9-54577C8E29CD}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UniversalLLMFunctionCallerUnitTests", "UniversalLLMFunctionCallerUnitTests\UniversalLLMFunctionCallerUnitTests.csproj", "{9803C539-0AA7-4CC0-9145-69A95EE78318}" 11 | EndProject 12 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{F79F6C77-47E2-47A9-9440-787055D5CAF7}" 13 | ProjectSection(SolutionItems) = preProject 14 | .editorconfig = .editorconfig 15 | EndProjectSection 16 | EndProject 17 | Global 18 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 19 | Debug|Any CPU = Debug|Any CPU 20 | Release|Any CPU = Release|Any CPU 21 | EndGlobalSection 22 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 23 | {660CAE90-1D47-4468-BE94-6D74C68B3E1F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 24 | {660CAE90-1D47-4468-BE94-6D74C68B3E1F}.Debug|Any CPU.Build.0 = Debug|Any CPU 25 | {660CAE90-1D47-4468-BE94-6D74C68B3E1F}.Release|Any CPU.ActiveCfg = Release|Any CPU 26 | {660CAE90-1D47-4468-BE94-6D74C68B3E1F}.Release|Any CPU.Build.0 = Release|Any CPU 27 | {C034FED1-65C8-4EF2-B0A9-54577C8E29CD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 28 | {C034FED1-65C8-4EF2-B0A9-54577C8E29CD}.Debug|Any CPU.Build.0 = Debug|Any CPU 29 | {C034FED1-65C8-4EF2-B0A9-54577C8E29CD}.Release|Any CPU.ActiveCfg = Release|Any CPU 30 | {C034FED1-65C8-4EF2-B0A9-54577C8E29CD}.Release|Any CPU.Build.0 = Release|Any CPU 31 | {9803C539-0AA7-4CC0-9145-69A95EE78318}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 32 | {9803C539-0AA7-4CC0-9145-69A95EE78318}.Debug|Any CPU.Build.0 = Debug|Any CPU 33 | {9803C539-0AA7-4CC0-9145-69A95EE78318}.Release|Any CPU.ActiveCfg = Release|Any CPU 34 | {9803C539-0AA7-4CC0-9145-69A95EE78318}.Release|Any CPU.Build.0 = Release|Any CPU 35 | EndGlobalSection 36 | GlobalSection(SolutionProperties) = preSolution 37 | HideSolutionNode = FALSE 38 | EndGlobalSection 39 | GlobalSection(ExtensibilityGlobals) = postSolution 40 | SolutionGuid = {0755AF7B-BBCD-4CEB-8E14-A93F21FC1AB3} 41 | EndGlobalSection 42 | EndGlobal 43 | -------------------------------------------------------------------------------- /UniversalLLMFunctionCaller/FunctionCall.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace JC.SemanticKernel.Planners.UniversalLLMFunctionCaller 8 | { 9 | internal class FunctionCall 10 | { 11 | public string Name { get; set; } 12 | public List Parameters { get; set; } 13 | } 14 | internal class FunctionCallParameter 15 | { 16 | public string Name { get; set; } 17 | public object Value { get; set; } 18 | 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /UniversalLLMFunctionCaller/UniversalLLMFunctionCaller.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging.Abstractions; 2 | using Microsoft.SemanticKernel; 3 | using Microsoft.SemanticKernel.ChatCompletion; 4 | using Microsoft.SemanticKernel.TextGeneration; 5 | using System.Text; 6 | using System.Text.RegularExpressions; 7 | using System.Threading.Tasks; 8 | 9 | namespace JC.SemanticKernel.Planners.UniversalLLMFunctionCaller 10 | { 11 | public class UniversalLLMFunctionCaller 12 | { 13 | private Kernel _kernel; 14 | IChatCompletionService _chatCompletion; 15 | public UniversalLLMFunctionCaller(Kernel kernel) 16 | { 17 | _kernel = kernel; 18 | _chatCompletion = _kernel.GetRequiredService(); 19 | } 20 | 21 | public async Task RunAsync(ChatHistory askHistory) 22 | { 23 | string ask = await GetAskFromHistory(askHistory); 24 | return await RunAsync(ask); 25 | } 26 | 27 | private async Task GetAskFromHistory(ChatHistory askHistory) 28 | { 29 | StringBuilder sb = new StringBuilder(); 30 | var userAndAssistantMessages = askHistory.Where(h => h.Role == AuthorRole.Assistant || h.Role == AuthorRole.User); 31 | foreach(var message in userAndAssistantMessages) 32 | { 33 | sb.AppendLine($"{message.Role.ToString()}: {message.Content}"); 34 | } 35 | 36 | string extractAskFromHistoryPrompt = $@"Look at this dialog between a user and an assistant. 37 | Summarize what the user wants the assistant to do with their last message 38 | ##Start of Conversation## 39 | {sb.ToString()} 40 | ##End of Conversation##"; 41 | 42 | var extractAskResult = await _chatCompletion.GetChatMessageContentAsync(extractAskFromHistoryPrompt); 43 | string ask = extractAskResult.Content; 44 | return ask; 45 | } 46 | 47 | public async Task RunAsync(string task) 48 | { 49 | // Initialize plugins 50 | var plugins = _kernel.Plugins; 51 | var internalPlugin = _kernel.Plugins.AddFromType(); 52 | 53 | // Convert plugins to text 54 | string pluginsAsText = GetTemplatesAsTextPrompt3000(plugins); 55 | 56 | // Initialize function call and chat history 57 | FunctionCall nextFunctionCall = new FunctionCall { Name = "Start" }; 58 | ChatHistory chatHistory = InitializeChatHistory(task); 59 | 60 | // Add new task to chat history 61 | chatHistory.Add(new ChatMessageContent(AuthorRole.User, $"New task: {task}")); 62 | 63 | // Process function calls 64 | for (int iteration = 0; iteration < 10 && nextFunctionCall.Name != "Finished"; iteration++) 65 | { 66 | nextFunctionCall = await GetNextFunctionCallAsync(chatHistory, pluginsAsText); 67 | if (nextFunctionCall == null) throw new Exception("The LLM is not compatible with this approach."); 68 | 69 | // Add function call to chat history 70 | string nextFunctionCallText = GetCallAsTextPrompt3000(nextFunctionCall); 71 | chatHistory.AddAssistantMessage(nextFunctionCallText); 72 | 73 | // Invoke plugin and add response to chat history 74 | string pluginResponse = await InvokePluginAsync(nextFunctionCall); 75 | chatHistory.AddUserMessage(pluginResponse); 76 | } 77 | 78 | // Remove internal plugin 79 | _kernel.Plugins.Remove(internalPlugin); 80 | 81 | // Check if task was completed successfully 82 | if (nextFunctionCall.Name == "Finished") 83 | { 84 | string finalMessage = nextFunctionCall.Parameters[0].Value.ToString(); 85 | return finalMessage; 86 | } 87 | 88 | throw new Exception("LLM could not finish workflow within 10 steps. Consider increasing the number of steps."); 89 | } 90 | 91 | private ChatHistory InitializeChatHistory(string ask) 92 | { 93 | ChatHistory history = new ChatHistory(); 94 | 95 | history.Add(new ChatMessageContent(AuthorRole.User, "New task: Start my spaceship")); 96 | history.Add(new ChatMessageContent(AuthorRole.Assistant, "GetMySpaceshipName()")); 97 | history.Add(new ChatMessageContent(AuthorRole.User, "MSS3000")); 98 | history.Add(new ChatMessageContent(AuthorRole.Assistant, "StartSpaceship(ship_name: \"MSS3000\")")); 99 | history.Add(new ChatMessageContent(AuthorRole.User, "Ship started")); 100 | history.Add(new ChatMessageContent(AuthorRole.Assistant, "Finished(finalmessage: \"The ship with the name of 'MSS3000' was started\")")); 101 | history.Add(new ChatMessageContent(AuthorRole.User, $"New task: {ask}")); 102 | 103 | return history; 104 | } 105 | 106 | private async Task GetNextFunctionCallAsync(ChatHistory history, string pluginsAsText) 107 | { 108 | // Create a copy of the chat history 109 | var copiedHistory = new ChatHistory(history.ToArray().ToList()); 110 | 111 | // Add system message to history if not present 112 | if (copiedHistory[0].Role != AuthorRole.System) 113 | { 114 | string systemMessage = GetLoopSystemMessage(pluginsAsText); 115 | copiedHistory.Insert(0, new ChatMessageContent(AuthorRole.System, systemMessage)); 116 | } 117 | 118 | // Try to get the next function call 119 | for (int retryCount = 0; retryCount < 5; retryCount++) 120 | { 121 | // Get LLM reply and add it to the history 122 | var llmReply = await _chatCompletion.GetChatMessageContentAsync(copiedHistory); 123 | copiedHistory.AddAssistantMessage(llmReply.Content); 124 | 125 | try 126 | { 127 | // Parse and validate the function call 128 | FunctionCall functionCall = ParseTextPrompt3000Call(llmReply); 129 | ValidateFunctionWithKernel(functionCall); 130 | 131 | return functionCall; 132 | } 133 | catch (Exception ex) 134 | { 135 | // Add error message to history and continue 136 | string errorMessage = $"Error: '{ex.Message}'. Please try again without apologizing or explaining. Just follow the previous rules."; 137 | copiedHistory.AddUserMessage(errorMessage); 138 | } 139 | } 140 | 141 | return null; 142 | } 143 | 144 | 145 | private string GetTap1SystemMessage(string pluginsAsTextPrompt3000) 146 | { 147 | string systemPrompt = $@"You are an advisor, that is tasked with finding the next step to fullfil a goal. 148 | Below, you are provided a goal, that needs to be reached, and a chat between a user and a computer, as well as a list of functions that the user could use. 149 | You need to find out what the next step for the user is to reach the goal. You are also provided a list of functions that are in TextPrompt3000 Schema Format. 150 | The TextPrompt3000 Format is defined like this: 151 | {GetTextPrompt300Explanation()} 152 | ##available functions## 153 | {pluginsAsTextPrompt3000} 154 | ##end functions## 155 | 156 | The following rules are very important: 157 | 1) you can only recommend one function and the parameters, not multiple functions 158 | 2) You can only recommend a function that is in the list of available functions 159 | 3) You need to give all parameters for the function 160 | 4) Given the history, the function you recommend needs to be important to get closer towards the goal 161 | 5) Do not wrap functions into each other. Stick to the list of functions, this is not a math problem. Do not use placeholders. 162 | We only need one function, the next one needed. For example, if function A() needs to be used as parameter in function B(), do NOT do B(A()). Instead, 163 | if A wasnt called allready, call A() first. The result will be used in B in a later iteration. 164 | 6) Do not recommend a function that was recently called. Use the output instead. Do not use Placeholders or Functions as parameters for other functions 165 | 7) When all necessary functions are called and the result was presented by the computer system, call the Finished function and present the result 166 | 167 | 168 | If you break any of those rules, a kitten dies. 169 | What function should the user execute next on the computer? Explain your reasoning step by step. 170 | "; 171 | return systemPrompt; 172 | } 173 | private string GetLoopSystemMessage(string pluginsAsTextPrompt3000) 174 | { 175 | string systemPrompt = $@"You are a computer system. You can only speak TextPrompt3000 to make the user call functions, and the user will behave 176 | as a different computer system that answers those functions. 177 | Below, you are provided a goal that needs to be reached, as well as a list of functions that the user could use. 178 | You need to find out what the next step for the user is to reach the goal and recommend a TextPrompt3000 function call. 179 | You are also provided a list of functions that are in TextPrompt3000 Schema Format. 180 | The TextPrompt3000 Format is defined like this: 181 | {GetTextPrompt300Explanation()} 182 | ##available functions## 183 | {pluginsAsTextPrompt3000} 184 | ##end functions## 185 | 186 | The following rules are very important: 187 | 1) you can only recommend one function and the parameters, not multiple functions 188 | 2) You can only recommend a function that is in the list of available functions 189 | 3) You need to give all parameters for the function. Do NOT escape special characters in the name of functions or the names of parameters (dont do aaa\_bbb, just stick to aaa_bbb)! 190 | 4) Given the history, the function you recommend needs to be important to get closer towards the goal 191 | 5) Do not wrap functions into each other. Stick to the list of functions, this is not a math problem. Do not use placeholders. 192 | We only need one function, the next one needed. For example, if function A() needs to be used as parameter in function B(), do NOT do B(A()). Instead, 193 | if A wasnt called allready, call A() first. The result will be used in B in a later iteration. 194 | 6) Do not recommend a function that was recently called. Use the output instead. Do not use Placeholders or Functions as parameters for other functions 195 | 7) Only write a Function Call, do not explain why, do not provide a reasoning. You are limited to writing a function call only! 196 | 8) When all necessary functions are called and the result was presented by the computer system, call the Finished function and present the result 197 | 198 | If you break any of those rules, a kitten dies. 199 | "; 200 | return systemPrompt; 201 | } 202 | 203 | private async Task GetNextFunctionToCallDoubleTappedAsync(ChatHistory tap1history, string pluginsAsTextPrompt3000) 204 | { 205 | if (tap1history[0].Role != AuthorRole.System) { 206 | tap1history.Insert(0, new ChatMessageContent(AuthorRole.System, GetTap1SystemMessage(pluginsAsTextPrompt3000))); 207 | } 208 | 209 | //we are doing multi-tapping here. First get an elaborate answer, and then press that answer into a usable format 210 | var tap1Answer = await _chatCompletion.GetChatMessageContentAsync(tap1history); 211 | string tap2Prompt = @$"You are a computer system. You can only answer with a TextPrompt3000 function, nothing else. 212 | A user is giving you a text. You need to extract a TextPrompt3000 function call from it. 213 | {GetTextPrompt300Explanation()} 214 | You can not say anything else but a function call. Do not explain anything. Do not answer as a schema, including '--' in the end. 215 | Answer as actual function call. The result could look like this 'FunctionA(paramA: content)' or 'FunctionB()' 216 | ##Text from user## 217 | {tap1Answer.Content} 218 | ## 219 | ##Available functions# 220 | {pluginsAsTextPrompt3000} 221 | ## 222 | It is very important that you only return a valid function that you extracted from the users text, no other text. 223 | Do not supply multiple functions, only one, the next necessary one. 224 | It needs to include all necessary parameters. 225 | If you do not do this, a very cute kitten dies."; 226 | 227 | ChatHistory tap2History = new ChatHistory(); 228 | tap2History.AddUserMessage(tap2Prompt); 229 | 230 | 231 | for(int iRetry = 0; iRetry < 5; iRetry++) { 232 | var tap2Answer = await _chatCompletion.GetChatMessageContentsAsync(tap2History); 233 | Console.WriteLine(tap2Answer[0].Content); 234 | tap2History.AddAssistantMessage(tap2Answer[0].Content); 235 | try 236 | { 237 | FunctionCall functionCall = ParseTextPrompt3000Call(tap2Answer[0]); 238 | ValidateFunctionWithKernel(functionCall); 239 | 240 | return functionCall; 241 | } catch (Exception ex) 242 | { 243 | tap2History.AddUserMessage($"Error: '{ex.Message}'. Try again. Do not apologise. Do not explain anything. just follow the rules from earlier"); 244 | } 245 | } 246 | 247 | return null; 248 | } 249 | 250 | private void ValidateFunctionWithKernel(FunctionCall functionCall) 251 | { 252 | // Iterate over each plugin in the kernel 253 | foreach (var plugin in _kernel.Plugins) 254 | { 255 | // Check if the plugin has a function with the same name as the function call 256 | var function = plugin.FirstOrDefault(f => f.Name == functionCall.Name); 257 | if (function != null) 258 | { 259 | // Check if the function has the same parameters as the function call 260 | if (function.Metadata.Parameters.Count == functionCall.Parameters.Count) 261 | { 262 | for (int i = 0; i < function.Metadata.Parameters.Count; i++) 263 | { 264 | if (function.Metadata.Parameters[i].Name != functionCall.Parameters[i].Name) 265 | { 266 | throw new Exception("Parameter " + functionCall.Parameters[i].Name + " does not exist in the function."); 267 | } 268 | } 269 | return; // Exit the function if both function name and parameters match 270 | } 271 | else 272 | { 273 | throw new Exception($"Parameter count does not match for the function."); 274 | } 275 | } 276 | } 277 | 278 | throw new Exception($"Function '{functionCall.Name}' does not exist in the kernel."); 279 | } 280 | 281 | 282 | 283 | private FunctionCall ParseTextPrompt3000Call(ChatMessageContent possibleFunctionCallResult) 284 | { 285 | // Get the content of the first ChatMessageContent 286 | string content = possibleFunctionCallResult.Content; 287 | 288 | // Split the content into function name and parameters 289 | int openParenIndex = content.IndexOf('('); 290 | int closeParenIndex = content.IndexOf(')'); 291 | string functionName = content.Substring(0, openParenIndex); 292 | string parametersContent = content.Substring(openParenIndex + 1, closeParenIndex - openParenIndex - 1); 293 | 294 | parametersContent = RemoveBackslashesOutsideQuotes(parametersContent); 295 | 296 | // Create a new FunctionCall 297 | FunctionCall functionCall = new FunctionCall(); 298 | functionCall.Name = functionName; 299 | functionCall.Parameters = new List(); 300 | 301 | // Check if there are any parameters 302 | if (!string.IsNullOrWhiteSpace(parametersContent)) 303 | { 304 | // Use a regular expression to match the parameters 305 | var matches = Regex.Matches(parametersContent, @"(\w+)\s*:\s*(""([^""]*)""|(\d+(\.\d+)?))"); 306 | 307 | // Parse each parameter 308 | foreach (Match match in matches) 309 | { 310 | // Get the parameter name and value 311 | string parameterName = match.Groups[1].Value; 312 | object parameterValue = match.Groups[3].Success ? match.Groups[3].Value : double.Parse(match.Groups[4].Value); // You might want to convert this to the appropriate type 313 | 314 | 315 | // Create a new FunctionCallParameter and add it to the FunctionCall 316 | FunctionCallParameter functionCallParameter = new FunctionCallParameter(); 317 | functionCallParameter.Name = parameterName; 318 | functionCallParameter.Value = parameterValue; 319 | functionCall.Parameters.Add(functionCallParameter); 320 | } 321 | } 322 | 323 | return functionCall; 324 | } 325 | 326 | 327 | private string RemoveBackslashesOutsideQuotes(string input) 328 | { 329 | StringBuilder result = new StringBuilder(); 330 | bool insideQuotes = false; 331 | 332 | foreach (char c in input) 333 | { 334 | if (c == '"') 335 | { 336 | insideQuotes = !insideQuotes; 337 | } 338 | 339 | if (c != '\\' || insideQuotes) 340 | { 341 | result.Append(c); 342 | } 343 | } 344 | 345 | return result.ToString(); 346 | } 347 | 348 | 349 | 350 | 351 | private string GetTextPrompt300Explanation() 352 | { 353 | return $@"##example of TextPrompt3000## 354 | FunctionName(parameter: value) 355 | ## 356 | In TextPrompt3000, there are also schemas. 357 | ##Example of TextPrompt3000 Schema## 358 | FunctionName(datatype1 parametername1:""parameter1 description"",datatype2 parametername2:""parameter2 description"")--Description of the function 359 | ## 360 | There can be no parameters, one, or many parameters. 361 | For example, a Schema looking like this: 362 | StartSpaceship(string shipName:""The name of the starting ship"", int speed: 100)--Starts a specific spaceship with a specific speed 363 | would be called like this: 364 | StartSpaceship(shipName: ""Enterprise"", speed: 99999) 365 | "; 366 | } 367 | 368 | private string GetCallAsTextPrompt3000(FunctionCall nextFunctionCall) 369 | { 370 | StringBuilder sb = new StringBuilder(); 371 | sb.Append(nextFunctionCall.Name); 372 | sb.Append("("); 373 | for (int i = 0; i < nextFunctionCall.Parameters.Count; i++) 374 | { 375 | if (i > 0) 376 | sb.Append(", "); 377 | sb.Append(nextFunctionCall.Parameters[i].Name); 378 | sb.Append(": \""); 379 | sb.Append(nextFunctionCall.Parameters[i].Value.ToString()); 380 | sb.Append("\""); 381 | } 382 | sb.Append(")"); 383 | return sb.ToString(); 384 | } 385 | 386 | 387 | private async Task InvokePluginAsync(FunctionCall functionCall) 388 | { 389 | List args = new List(); 390 | foreach(var paraam in functionCall.Parameters) 391 | { 392 | args.Add($"{paraam.Name} : {paraam.Value}"); 393 | } 394 | Console.WriteLine($">>invoking {functionCall.Name} with parameters {string.Join(",", args)}"); 395 | // Iterate over each plugin in the kernel 396 | foreach (var plugin in _kernel.Plugins) 397 | { 398 | // Check if the plugin has a function with the same name as the function call 399 | var function = plugin.FirstOrDefault(f => f.Name == functionCall.Name); 400 | if (function != null) 401 | { 402 | // Create a new context for the function call 403 | KernelArguments context = new KernelArguments(); 404 | 405 | // Add the function parameters to the context 406 | foreach (var parameter in functionCall.Parameters) 407 | { 408 | context[parameter.Name] = parameter.Value; 409 | } 410 | 411 | // Invoke the function 412 | var result = await function.InvokeAsync(_kernel,context); 413 | 414 | Console.WriteLine($">>Result: {result.ToString()}"); 415 | return result.ToString(); 416 | } 417 | } 418 | 419 | throw new Exception("Function does not exist in the kernel."); 420 | } 421 | 422 | 423 | private string GetTemplatesAsTextPrompt3000(KernelPluginCollection plugins) 424 | { 425 | StringBuilder sb = new StringBuilder(); 426 | foreach (var plugin in plugins) 427 | { 428 | foreach (var functionimpl in plugin) 429 | { 430 | var function = functionimpl.Metadata; 431 | sb.Append(function.Name); 432 | sb.Append("("); 433 | for (int i = 0; i < function.Parameters.Count; i++) 434 | { 435 | if (i > 0) 436 | sb.Append(", "); 437 | sb.Append(function.Parameters[i].ParameterType.Name); 438 | sb.Append(" "); 439 | sb.Append(function.Parameters[i].Name); 440 | sb.Append(": \""); 441 | sb.Append(function.Parameters[i].Description); 442 | sb.Append("\""); 443 | } 444 | sb.Append(")"); 445 | sb.Append("--"); 446 | sb.Append(function.Description); 447 | sb.AppendLine(); 448 | } 449 | } 450 | return sb.ToString(); 451 | } 452 | 453 | 454 | } 455 | } 456 | -------------------------------------------------------------------------------- /UniversalLLMFunctionCaller/UniversalLLMFunctionCaller.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | JC.SemanticKernel.Planners.UniversalLLMFunctionCaller 8 | JC.SemanticKernel.Planners.UniversalLLMFunctionCaller 9 | 0.1.0 10 | Jens Caasen 11 | Jens Caasen 12 | A planner that integrates into Semantic Kernel to enable function calling on all Chat based LLMs (Mistral, Bard, Claude, LLama etc) 13 | https://github.com/Jenscaasen/UniversalLLMFunctionCaller 14 | https://github.com/Jenscaasen/UniversalLLMFunctionCaller 15 | llm, semantic kernel, planner, function calling, ai 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /UniversalLLMFunctionCaller/UniversalLLMFunctionCallerInternalFunctions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.SemanticKernel; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.ComponentModel; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | namespace JC.SemanticKernel.Planners.UniversalLLMFunctionCaller 10 | { 11 | internal class UniversalLLMFunctionCallerInternalFunctions 12 | { 13 | [KernelFunction, Description("Call this when the workflow is done and there are no more functions to call")] 14 | public string Finished( 15 | [Description("Wrap up what was done and what the result is, be concise")] string finalmessage 16 | ) 17 | { 18 | return string.Empty; 19 | //no actual implementation, for internal routing only 20 | } 21 | [KernelFunction, Description("Gets the name of the spaceship of the user")] 22 | public string GetMySpaceshipName( ) 23 | { 24 | return "MSS3000"; 25 | } 26 | [KernelFunction, Description("Starts a Spaceship")] 27 | public void StartSpaceship( 28 | [Description("The name of the spaceship to start")] string ship_name 29 | ) 30 | { 31 | //no actual implementation, for internal routing only 32 | } 33 | 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /UniversalLLMFunctionCallerDemo/Plugins/EmailPluginFake.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | 3 | using System.ComponentModel; 4 | using System.Threading.Tasks; 5 | using Microsoft.Extensions.Logging; 6 | using Microsoft.SemanticKernel; 7 | 8 | #pragma warning disable CA1812 // Uninstantiated internal types 9 | 10 | namespace SemanticKernel.IntegrationTests.Fakes; 11 | 12 | internal sealed class EmailPluginFake 13 | { 14 | [KernelFunction, Description("Given an email address and message body, send an email")] 15 | public Task SendEmailAsync( 16 | [Description("The body of the email message to send.")] string input = "", 17 | [Description("The email address to send email to.")] string? email_address = "default@email.com") 18 | { 19 | email_address ??= string.Empty; 20 | return Task.FromResult($"Sent email to: {email_address}. Body: {input}"); 21 | } 22 | 23 | [KernelFunction, Description("Lookup an email address for a person given a name")] 24 | public Task GetEmailAddressAsync( 25 | ILogger logger, 26 | [Description("The name of the person to email.")] string? input = null) 27 | { 28 | if (string.IsNullOrEmpty(input)) 29 | { 30 | logger.LogTrace("Returning hard coded email for {0}", input); 31 | return Task.FromResult("johndoe1234@example.com"); 32 | } 33 | 34 | logger.LogTrace("Returning dynamic email for {0}", input); 35 | return Task.FromResult($"{input}@example.com"); 36 | } 37 | 38 | [KernelFunction, Description("Write a short poem for an e-mail")] 39 | public Task WritePoemAsync( 40 | [Description("The topic of the poem.")] string input) 41 | { 42 | return Task.FromResult($"Roses are red, violets are blue, {input} is hard, so is this test."); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /UniversalLLMFunctionCallerDemo/Plugins/FileIOPlugin.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | 3 | using System; 4 | using System.ComponentModel; 5 | using System.IO; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | using Microsoft.SemanticKernel; 9 | 10 | namespace UniversalLLMFunctionCallerDemo.Plugins; 11 | 12 | /// 13 | /// Read and write from a file. 14 | /// 15 | public sealed class FileIOPlugin 16 | { 17 | /// 18 | /// Read a file 19 | /// 20 | /// 21 | /// {{file.readAsync $path }} => "hello world" 22 | /// 23 | /// Source file 24 | /// File content 25 | [KernelFunction, Description("Read a file")] 26 | public async Task ReadAsync([Description("Source file")] string path) 27 | { 28 | using var reader = File.OpenText(path); 29 | return await reader.ReadToEndAsync().ConfigureAwait(false); 30 | } 31 | 32 | /// 33 | /// Write a file 34 | /// 35 | /// 36 | /// {{file.writeAsync}} 37 | /// 38 | /// The destination file path 39 | /// The file content to write 40 | /// An awaitable task 41 | [KernelFunction, Description("Write a file")] 42 | public async Task WriteAsync( 43 | [Description("Destination file")] string path, 44 | [Description("File content")] string content) 45 | { 46 | byte[] text = Encoding.UTF8.GetBytes(content); 47 | if (File.Exists(path) && File.GetAttributes(path).HasFlag(FileAttributes.ReadOnly)) 48 | { 49 | // Most environments will throw this with OpenWrite, but running inside docker on Linux will not. 50 | throw new UnauthorizedAccessException($"File is read-only: {path}"); 51 | } 52 | 53 | using var writer = File.OpenWrite(path); 54 | await writer.WriteAsync(text, 0, text.Length).ConfigureAwait(false); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /UniversalLLMFunctionCallerDemo/Plugins/MathPlugin.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | 3 | using System.ComponentModel; 4 | using Microsoft.SemanticKernel; 5 | 6 | namespace UniversalLLMFunctionCallerDemo.Plugins; 7 | 8 | /// 9 | /// MathPlugin provides a set of functions to make Math calculations. 10 | /// 11 | public sealed class MathPlugin 12 | { 13 | /// 14 | /// Returns the addition result of initial and amount values provided. 15 | /// 16 | /// Initial value to which to add the specified amount. 17 | /// The amount to add as a string. 18 | /// The resulting sum as a string. 19 | [KernelFunction, Description("Adds an amount to a value")] 20 | [return: Description("The sum")] 21 | public int Add( 22 | [Description("The value to add")] int value, 23 | [Description("Amount to add")] int amount) => 24 | value + amount; 25 | 26 | /// 27 | /// Returns the subtraction result of initial and amount values provided. 28 | /// 29 | /// Initial value from which to subtract the specified amount. 30 | /// The amount to subtract as a string. 31 | /// The resulting subtraction as a string. 32 | [KernelFunction, Description("Subtracts an amount from a value")] 33 | [return: Description("The difference")] 34 | public int Subtract( 35 | [Description("The value to subtract")] int value, 36 | [Description("Amount to subtract")] int amount) => 37 | value - amount; 38 | } 39 | -------------------------------------------------------------------------------- /UniversalLLMFunctionCallerDemo/Plugins/TextPlugin.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | 3 | using System.ComponentModel; 4 | using System.Globalization; 5 | using Microsoft.SemanticKernel; 6 | 7 | namespace UniversalLLMFunctionCallerDemo.Plugins; 8 | 9 | /// 10 | /// TextPlugin provides a set of functions to manipulate strings. 11 | /// 12 | public sealed class TextPlugin 13 | { 14 | /// 15 | /// Trim whitespace from the start and end of a string. 16 | /// 17 | /// The string to trim. 18 | /// The trimmed string. 19 | [KernelFunction, Description("Trim whitespace from the start and end of a string.")] 20 | public string Trim(string input) => input.Trim(); 21 | 22 | /// 23 | /// Trim whitespace from the start of a string. 24 | /// 25 | /// The string to trim. 26 | /// The trimmed string. 27 | [KernelFunction, Description("Trim whitespace from the start of a string.")] 28 | public string TrimStart(string input) => input.TrimStart(); 29 | 30 | /// 31 | /// Trim whitespace from the end of a string. 32 | /// 33 | /// The string to trim. 34 | /// The trimmed string. 35 | [KernelFunction, Description("Trim whitespace from the end of a string.")] 36 | public string TrimEnd(string input) => input.TrimEnd(); 37 | 38 | /// 39 | /// Convert a string to uppercase. 40 | /// 41 | /// The string to convert. 42 | /// An object that supplies culture-specific casing rules. 43 | /// The converted string. 44 | [KernelFunction, Description("Convert a string to uppercase.")] 45 | public string Uppercase(string input, CultureInfo? cultureInfo = null) => input.ToUpper(cultureInfo); 46 | 47 | /// 48 | /// Convert a string to lowercase. 49 | /// 50 | /// The string to convert. 51 | /// An object that supplies culture-specific casing rules. 52 | /// The converted string. 53 | [KernelFunction, Description("Convert a string to lowercase.")] 54 | public string Lowercase(string input, CultureInfo? cultureInfo = null) => input.ToLower(cultureInfo); 55 | 56 | /// 57 | /// Get the length of a string. Returns 0 if null or empty 58 | /// 59 | /// The string to get length. 60 | /// The length size of string (0) if null or empty. 61 | [KernelFunction, Description("Get the length of a string.")] 62 | public int Length(string input) => input?.Length ?? 0; 63 | 64 | /// 65 | /// Concatenate two strings into one 66 | /// 67 | /// First input to concatenate with 68 | /// Second input to concatenate with 69 | /// Concatenation result from both inputs. 70 | [KernelFunction, Description("Concat two strings into one.")] 71 | public string Concat( 72 | [Description("First input to concatenate with")] string input, 73 | [Description("Second input to concatenate with")] string input2) => 74 | string.Concat(input, input2); 75 | 76 | /// 77 | /// Echo the input string. Useful for capturing plan input for use in multiple functions. 78 | /// 79 | /// Input string to echo. 80 | /// The input string. 81 | [KernelFunction, Description("Echo the input string. Useful for capturing plan input for use in multiple functions.")] 82 | public string Echo( 83 | [Description("Input string to echo.")] string text) 84 | { 85 | return text; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /UniversalLLMFunctionCallerDemo/Plugins/ThrowingEmailPluginFake.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | 3 | using System; 4 | using System.ComponentModel; 5 | using System.Threading.Tasks; 6 | using Microsoft.SemanticKernel; 7 | 8 | #pragma warning disable CA1812 // Uninstantiated internal types 9 | 10 | namespace SemanticKernel.IntegrationTests.Fakes; 11 | 12 | internal sealed class ThrowingEmailPluginFake 13 | { 14 | [KernelFunction, Description("Given an email address and message body, send an email")] 15 | public Task SendEmailAsync( 16 | [Description("The body of the email message to send.")] string input = "", 17 | [Description("The email address to send email to.")] string? email_address = "default@email.com") 18 | { 19 | // Throw a non-critical exception for testing 20 | throw new ArgumentException($"Failed to send email to {email_address}"); 21 | } 22 | 23 | [KernelFunction, Description("Write a short poem for an e-mail")] 24 | public Task WritePoemAsync( 25 | [Description("The topic of the poem.")] string input) 26 | { 27 | return Task.FromResult($"Roses are red, violets are blue, {input} is hard, so is this test."); 28 | } 29 | 30 | [KernelFunction, Description("Write a joke for an e-mail")] 31 | public Task WriteJokeAsync() 32 | { 33 | // Throw a critical exception for testing 34 | throw new InvalidProgramException(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /UniversalLLMFunctionCallerDemo/Plugins/TimePlugin.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | 3 | using System; 4 | using System.ComponentModel; 5 | using Microsoft.SemanticKernel; 6 | 7 | namespace UniversalLLMFunctionCallerDemo.Plugins; 8 | 9 | /// 10 | /// TimePlugin provides a set of functions to get the current time and date. 11 | /// 12 | /// 13 | /// Note: the time represents the time on the hw/vm/machine where the kernel is running. 14 | /// TODO: import and use user's timezone 15 | /// 16 | public sealed class TimePlugin 17 | { 18 | /// 19 | /// Get the current date 20 | /// 21 | /// 22 | /// {{time.date}} => Sunday, 12 January, 2031 23 | /// 24 | /// The current date 25 | [KernelFunction, Description("Get the current date")] 26 | public string Date(IFormatProvider? formatProvider = null) => 27 | // Example: Sunday, 12 January, 2025 28 | DateTimeOffset.Now.ToString("D", formatProvider); 29 | 30 | /// 31 | /// Get the current date 32 | /// 33 | /// 34 | /// {{time.today}} => Sunday, 12 January, 2031 35 | /// 36 | /// The current date 37 | [KernelFunction, Description("Get the current date")] 38 | public string Today(IFormatProvider? formatProvider = null) => 39 | // Example: Sunday, 12 January, 2025 40 | Date(formatProvider); 41 | 42 | /// 43 | /// Get the current date and time in the local time zone" 44 | /// 45 | /// 46 | /// {{time.now}} => Sunday, January 12, 2025 9:15 PM 47 | /// 48 | /// The current date and time in the local time zone 49 | [KernelFunction, Description("Get the current date and time in the local time zone")] 50 | public string Now(IFormatProvider? formatProvider = null) => 51 | // Sunday, January 12, 2025 9:15 PM 52 | DateTimeOffset.Now.ToString("f", formatProvider); 53 | 54 | /// 55 | /// Get the current UTC date and time 56 | /// 57 | /// 58 | /// {{time.utcNow}} => Sunday, January 13, 2025 5:15 AM 59 | /// 60 | /// The current UTC date and time 61 | [KernelFunction, Description("Get the current UTC date and time")] 62 | public string UtcNow(IFormatProvider? formatProvider = null) => 63 | // Sunday, January 13, 2025 5:15 AM 64 | DateTimeOffset.UtcNow.ToString("f", formatProvider); 65 | 66 | /// 67 | /// Get the current time 68 | /// 69 | /// 70 | /// {{time.time}} => 09:15:07 PM 71 | /// 72 | /// The current time 73 | [KernelFunction, Description("Get the current time")] 74 | public string Time(IFormatProvider? formatProvider = null) => 75 | // Example: 09:15:07 PM 76 | DateTimeOffset.Now.ToString("hh:mm:ss tt", formatProvider); 77 | 78 | /// 79 | /// Get the current year 80 | /// 81 | /// 82 | /// {{time.year}} => 2025 83 | /// 84 | /// The current year 85 | [KernelFunction, Description("Get the current year")] 86 | public string Year(IFormatProvider? formatProvider = null) => 87 | // Example: 2025 88 | DateTimeOffset.Now.ToString("yyyy", formatProvider); 89 | 90 | /// 91 | /// Get the current month name 92 | /// 93 | /// 94 | /// {time.month}} => January 95 | /// 96 | /// The current month name 97 | [KernelFunction, Description("Get the current month name")] 98 | public string Month(IFormatProvider? formatProvider = null) => 99 | // Example: January 100 | DateTimeOffset.Now.ToString("MMMM", formatProvider); 101 | 102 | /// 103 | /// Get the current month number 104 | /// 105 | /// 106 | /// {{time.monthNumber}} => 01 107 | /// 108 | /// The current month number 109 | [KernelFunction, Description("Get the current month number")] 110 | public string MonthNumber(IFormatProvider? formatProvider = null) => 111 | // Example: 01 112 | DateTimeOffset.Now.ToString("MM", formatProvider); 113 | 114 | /// 115 | /// Get the current day of the month 116 | /// 117 | /// 118 | /// {{time.day}} => 12 119 | /// 120 | /// The current day of the month 121 | [KernelFunction, Description("Get the current day of the month")] 122 | public string Day(IFormatProvider? formatProvider = null) => 123 | // Example: 12 124 | DateTimeOffset.Now.ToString("dd", formatProvider); 125 | 126 | /// 127 | /// Get the date a provided number of days in the past 128 | /// 129 | /// The date the provided number of days before today 130 | [KernelFunction] 131 | [Description("Get the date offset by a provided number of days from today")] 132 | public string DaysAgo([Description("The number of days to offset from today")] double input, IFormatProvider? formatProvider = null) => 133 | DateTimeOffset.Now.AddDays(-input).ToString("D", formatProvider); 134 | 135 | /// 136 | /// Get the current day of the week 137 | /// 138 | /// 139 | /// {{time.dayOfWeek}} => Sunday 140 | /// 141 | /// The current day of the week 142 | [KernelFunction, Description("Get the current day of the week")] 143 | public string DayOfWeek(IFormatProvider? formatProvider = null) => 144 | // Example: Sunday 145 | DateTimeOffset.Now.ToString("dddd", formatProvider); 146 | 147 | /// 148 | /// Get the current clock hour 149 | /// 150 | /// 151 | /// {{time.hour}} => 9 PM 152 | /// 153 | /// The current clock hour 154 | [KernelFunction, Description("Get the current clock hour")] 155 | public string Hour(IFormatProvider? formatProvider = null) => 156 | // Example: 9 PM 157 | DateTimeOffset.Now.ToString("h tt", formatProvider); 158 | 159 | /// 160 | /// Get the current clock 24-hour number 161 | /// 162 | /// 163 | /// {{time.hourNumber}} => 21 164 | /// 165 | /// The current clock 24-hour number 166 | [KernelFunction, Description("Get the current clock 24-hour number")] 167 | public string HourNumber(IFormatProvider? formatProvider = null) => 168 | // Example: 21 169 | DateTimeOffset.Now.ToString("HH", formatProvider); 170 | 171 | /// 172 | /// Get the date of the previous day matching the supplied day name 173 | /// 174 | /// 175 | /// {{time.lastMatchingDay $dayName}} => Sunday, 7 May, 2023 176 | /// 177 | /// The date of the last instance of this day name 178 | /// dayName is not a recognized name of a day of the week 179 | [KernelFunction] 180 | [Description("Get the date of the last day matching the supplied week day name in English. Example: Che giorno era 'Martedi' scorso -> dateMatchingLastDayName 'Tuesday' => Tuesday, 16 May, 2023")] 181 | public string DateMatchingLastDayName( 182 | [Description("The day name to match")] DayOfWeek input, 183 | IFormatProvider? formatProvider = null) 184 | { 185 | DateTimeOffset dateTime = DateTimeOffset.Now; 186 | 187 | // Walk backwards from the previous day for up to a week to find the matching day 188 | for (int i = 1; i <= 7; ++i) 189 | { 190 | dateTime = dateTime.AddDays(-1); 191 | if (dateTime.DayOfWeek == input) 192 | { 193 | break; 194 | } 195 | } 196 | 197 | return dateTime.ToString("D", formatProvider); 198 | } 199 | 200 | /// 201 | /// Get the minutes on the current hour 202 | /// 203 | /// 204 | /// {{time.minute}} => 15 205 | /// 206 | /// The minutes on the current hour 207 | [KernelFunction, Description("Get the minutes on the current hour")] 208 | public string Minute(IFormatProvider? formatProvider = null) => 209 | // Example: 15 210 | DateTimeOffset.Now.ToString("mm", formatProvider); 211 | 212 | /// 213 | /// Get the seconds on the current minute 214 | /// 215 | /// 216 | /// {{time.second}} => 7 217 | /// 218 | /// The seconds on the current minute 219 | [KernelFunction, Description("Get the seconds on the current minute")] 220 | public string Second(IFormatProvider? formatProvider = null) => 221 | // Example: 07 222 | DateTimeOffset.Now.ToString("ss", formatProvider); 223 | 224 | /// 225 | /// Get the local time zone offset from UTC 226 | /// 227 | /// 228 | /// {{time.timeZoneOffset}} => -08:00 229 | /// 230 | /// The local time zone offset from UTC 231 | [KernelFunction, Description("Get the local time zone offset from UTC")] 232 | public string TimeZoneOffset(IFormatProvider? formatProvider = null) => 233 | // Example: -08:00 234 | DateTimeOffset.Now.ToString("%K", formatProvider); 235 | 236 | /// 237 | /// Get the local time zone name 238 | /// 239 | /// 240 | /// {{time.timeZoneName}} => PST 241 | /// 242 | /// 243 | /// Note: this is the "current" timezone and it can change over the year, e.g. from PST to PDT 244 | /// 245 | /// The local time zone name 246 | [KernelFunction, Description("Get the local time zone name")] 247 | public string TimeZoneName() => 248 | // Example: PST 249 | // Note: this is the "current" timezone and it can change over the year, e.g. from PST to PDT 250 | TimeZoneInfo.Local.DisplayName; 251 | } 252 | -------------------------------------------------------------------------------- /UniversalLLMFunctionCallerDemo/Plugins/WaitPlugin.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | 3 | using System; 4 | using System.ComponentModel; 5 | using System.Threading.Tasks; 6 | using Microsoft.Extensions.DependencyInjection; 7 | using Microsoft.SemanticKernel; 8 | 9 | namespace UniversalLLMFunctionCallerDemo.Plugins; 10 | 11 | /// 12 | /// WaitPlugin provides a set of functions to wait before making the rest of operations. 13 | /// 14 | public sealed class WaitPlugin 15 | { 16 | private readonly TimeProvider _timeProvider; 17 | 18 | /// 19 | /// Initializes a new instance of the class. 20 | /// 21 | public WaitPlugin() : this(null) { } 22 | 23 | /// 24 | /// Initializes a new instance of the class. 25 | /// 26 | /// An optional time provider. If not provided, a default time provider will be used. 27 | [ActivatorUtilitiesConstructor] 28 | public WaitPlugin(TimeProvider? timeProvider = null) => 29 | _timeProvider = timeProvider ?? TimeProvider.System; 30 | 31 | } 32 | -------------------------------------------------------------------------------- /UniversalLLMFunctionCallerDemo/Program.cs: -------------------------------------------------------------------------------- 1 | using JC.SemanticKernel.Planners.UniversalLLMFunctionCaller; 2 | using Microsoft.SemanticKernel; 3 | using SemanticKernel.IntegrationTests.Fakes; 4 | using UniversalLLMFunctionCallerDemo.Plugins; 5 | 6 | namespace UniversalLLMFunctionCallerDemo 7 | { 8 | internal class Program 9 | { 10 | static async Task Main(string[] args) 11 | { 12 | IKernelBuilder builder = Kernel.CreateBuilder(); 13 | builder.AddMistralChatCompletion(Environment.GetEnvironmentVariable("mistral_key"), "mistral-small"); 14 | var kernel = builder.Build(); 15 | kernel.ImportPluginFromType("Time"); 16 | kernel.ImportPluginFromType("Math"); 17 | kernel.ImportPluginFromType("Email"); 18 | 19 | UniversalLLMFunctionCaller planner = new(kernel); 20 | string ask = "What is the current hour number, plus 5?"; 21 | Console.WriteLine(ask); 22 | string result = await planner.RunAsync("What is the current hour number, plus 5?"); 23 | Console.WriteLine(result); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /UniversalLLMFunctionCallerDemo/UniversalLLMFunctionCallerDemo.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net8.0 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /UniversalLLMFunctionCallerUnitTests/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | global using Microsoft.VisualStudio.TestTools.UnitTesting; -------------------------------------------------------------------------------- /UniversalLLMFunctionCallerUnitTests/Plugins/EmailPluginFake.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | 3 | using System.ComponentModel; 4 | using System.Threading.Tasks; 5 | using Microsoft.Extensions.Logging; 6 | using Microsoft.SemanticKernel; 7 | using DescriptionAttribute = System.ComponentModel.DescriptionAttribute; 8 | 9 | #pragma warning disable CA1812 // Uninstantiated internal types 10 | 11 | namespace SemanticKernel.IntegrationTests.Fakes; 12 | 13 | internal sealed class EmailPluginFake 14 | { 15 | [KernelFunction, Description("Given an email address and message body, send an email")] 16 | public Task SendEmailAsync( 17 | [Description("The body of the email message to send.")] string input = "", 18 | [Description("The email address to send email to.")] string? email_address = "default@email.com") 19 | { 20 | email_address ??= string.Empty; 21 | return Task.FromResult($"Sent email to: {email_address}. Body: {input}"); 22 | } 23 | 24 | [KernelFunction, Description("Lookup an email address for a person given a name")] 25 | public Task GetEmailAddressAsync( 26 | ILogger logger, 27 | [Description("The name of the person to email.")] string? input = null) 28 | { 29 | if (string.IsNullOrEmpty(input)) 30 | { 31 | logger.LogTrace("Returning hard coded email for {0}", input); 32 | return Task.FromResult("c"); 33 | } 34 | 35 | logger.LogTrace("Returning dynamic email for {0}", input); 36 | return Task.FromResult($"{input}@example.com"); 37 | } 38 | 39 | [KernelFunction, Description("Write a short poem for an e-mail")] 40 | public Task WritePoemAsync( 41 | [Description("The topic of the poem.")] string input) 42 | { 43 | return Task.FromResult($"Roses are red, violets are blue, {input} is hard, so is this test."); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /UniversalLLMFunctionCallerUnitTests/Plugins/FileIOPlugin.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | 3 | using System; 4 | using System.ComponentModel; 5 | using System.IO; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | using Microsoft.SemanticKernel; 9 | 10 | using DescriptionAttribute = System.ComponentModel.DescriptionAttribute; 11 | namespace UniversalLLMFunctionCallerDemo.Plugins; 12 | 13 | /// 14 | /// Read and write from a file. 15 | /// 16 | public sealed class FileIOPlugin 17 | { 18 | /// 19 | /// Read a file 20 | /// 21 | /// 22 | /// {{file.readAsync $path }} => "hello world" 23 | /// 24 | /// Source file 25 | /// File content 26 | [KernelFunction, Description("Read a file")] 27 | public async Task ReadAsync([Description("Source file")] string path) 28 | { 29 | using var reader = File.OpenText(path); 30 | return await reader.ReadToEndAsync().ConfigureAwait(false); 31 | } 32 | 33 | /// 34 | /// Write a file 35 | /// 36 | /// 37 | /// {{file.writeAsync}} 38 | /// 39 | /// The destination file path 40 | /// The file content to write 41 | /// An awaitable task 42 | [KernelFunction, Description("Write a file")] 43 | public async Task WriteAsync( 44 | [Description("Destination file")] string path, 45 | [Description("File content")] string content) 46 | { 47 | byte[] text = Encoding.UTF8.GetBytes(content); 48 | if (File.Exists(path) && File.GetAttributes(path).HasFlag(FileAttributes.ReadOnly)) 49 | { 50 | // Most environments will throw this with OpenWrite, but running inside docker on Linux will not. 51 | throw new UnauthorizedAccessException($"File is read-only: {path}"); 52 | } 53 | 54 | using var writer = File.OpenWrite(path); 55 | await writer.WriteAsync(text, 0, text.Length).ConfigureAwait(false); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /UniversalLLMFunctionCallerUnitTests/Plugins/MathPlugin.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | 3 | using System.ComponentModel; 4 | using Microsoft.SemanticKernel; 5 | 6 | using DescriptionAttribute = System.ComponentModel.DescriptionAttribute; 7 | namespace UniversalLLMFunctionCallerDemo.Plugins; 8 | 9 | /// 10 | /// MathPlugin provides a set of functions to make Math calculations. 11 | /// 12 | public sealed class MathPlugin 13 | { 14 | /// 15 | /// Returns the addition result of initial and amount values provided. 16 | /// 17 | /// Initial value to which to add the specified amount. 18 | /// The amount to add as a string. 19 | /// The resulting sum as a string. 20 | [KernelFunction, Description("Adds an amount to a value")] 21 | [return: Description("The sum")] 22 | public int Add( 23 | [Description("The value to add")] int value, 24 | [Description("Amount to add")] int amount) => 25 | value + amount; 26 | 27 | /// 28 | /// Returns the subtraction result of initial and amount values provided. 29 | /// 30 | /// Initial value from which to subtract the specified amount. 31 | /// The amount to subtract as a string. 32 | /// The resulting subtraction as a string. 33 | [KernelFunction, Description("Subtracts an amount from a value")] 34 | [return: Description("The difference")] 35 | public int Subtract( 36 | [Description("The value to subtract")] int value, 37 | [Description("Amount to subtract")] int amount) => 38 | value - amount; 39 | } 40 | -------------------------------------------------------------------------------- /UniversalLLMFunctionCallerUnitTests/Plugins/TextPlugin.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | 3 | using System.ComponentModel; 4 | using System.Globalization; 5 | using Microsoft.SemanticKernel; 6 | 7 | using DescriptionAttribute = System.ComponentModel.DescriptionAttribute; 8 | namespace UniversalLLMFunctionCallerDemo.Plugins; 9 | 10 | /// 11 | /// TextPlugin provides a set of functions to manipulate strings. 12 | /// 13 | public sealed class TextPlugin 14 | { 15 | /// 16 | /// Trim whitespace from the start and end of a string. 17 | /// 18 | /// The string to trim. 19 | /// The trimmed string. 20 | [KernelFunction, Description("Trim whitespace from the start and end of a string.")] 21 | public string Trim(string input) => input.Trim(); 22 | 23 | /// 24 | /// Trim whitespace from the start of a string. 25 | /// 26 | /// The string to trim. 27 | /// The trimmed string. 28 | [KernelFunction, Description("Trim whitespace from the start of a string.")] 29 | public string TrimStart(string input) => input.TrimStart(); 30 | 31 | /// 32 | /// Trim whitespace from the end of a string. 33 | /// 34 | /// The string to trim. 35 | /// The trimmed string. 36 | [KernelFunction, Description("Trim whitespace from the end of a string.")] 37 | public string TrimEnd(string input) => input.TrimEnd(); 38 | 39 | /// 40 | /// Convert a string to uppercase. 41 | /// 42 | /// The string to convert. 43 | /// An object that supplies culture-specific casing rules. 44 | /// The converted string. 45 | [KernelFunction, Description("Convert a string to uppercase.")] 46 | public string Uppercase(string input, CultureInfo? cultureInfo = null) => input.ToUpper(cultureInfo); 47 | 48 | /// 49 | /// Convert a string to lowercase. 50 | /// 51 | /// The string to convert. 52 | /// An object that supplies culture-specific casing rules. 53 | /// The converted string. 54 | [KernelFunction, Description("Convert a string to lowercase.")] 55 | public string Lowercase(string input, CultureInfo? cultureInfo = null) => input.ToLower(cultureInfo); 56 | 57 | /// 58 | /// Get the length of a string. Returns 0 if null or empty 59 | /// 60 | /// The string to get length. 61 | /// The length size of string (0) if null or empty. 62 | [KernelFunction, Description("Get the length of a string.")] 63 | public int Length(string input) => input?.Length ?? 0; 64 | 65 | /// 66 | /// Concatenate two strings into one 67 | /// 68 | /// First input to concatenate with 69 | /// Second input to concatenate with 70 | /// Concatenation result from both inputs. 71 | [KernelFunction, Description("Concat two strings into one.")] 72 | public string Concat( 73 | [Description("First input to concatenate with")] string input, 74 | [Description("Second input to concatenate with")] string input2) => 75 | string.Concat(input, input2); 76 | 77 | /// 78 | /// Echo the input string. Useful for capturing plan input for use in multiple functions. 79 | /// 80 | /// Input string to echo. 81 | /// The input string. 82 | [KernelFunction, Description("Echo the input string. Useful for capturing plan input for use in multiple functions.")] 83 | public string Echo( 84 | [Description("Input string to echo.")] string text) 85 | { 86 | return text; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /UniversalLLMFunctionCallerUnitTests/Plugins/ThrowingEmailPluginFake.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | 3 | using System; 4 | using System.ComponentModel; 5 | using System.Threading.Tasks; 6 | using Microsoft.SemanticKernel; 7 | 8 | using DescriptionAttribute = System.ComponentModel.DescriptionAttribute; 9 | #pragma warning disable CA1812 // Uninstantiated internal types 10 | 11 | namespace SemanticKernel.IntegrationTests.Fakes; 12 | 13 | internal sealed class ThrowingEmailPluginFake 14 | { 15 | [KernelFunction, Description("Given an email address and message body, send an email")] 16 | public Task SendEmailAsync( 17 | [Description("The body of the email message to send.")] string input = "", 18 | [Description("The email address to send email to.")] string? email_address = "default@email.com") 19 | { 20 | // Throw a non-critical exception for testing 21 | throw new ArgumentException($"Failed to send email to {email_address}"); 22 | } 23 | 24 | [KernelFunction, Description("Write a short poem for an e-mail")] 25 | public Task WritePoemAsync( 26 | [Description("The topic of the poem.")] string input) 27 | { 28 | return Task.FromResult($"Roses are red, violets are blue, {input} is hard, so is this test."); 29 | } 30 | 31 | [KernelFunction, Description("Write a joke for an e-mail")] 32 | public Task WriteJokeAsync() 33 | { 34 | // Throw a critical exception for testing 35 | throw new InvalidProgramException(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /UniversalLLMFunctionCallerUnitTests/Plugins/TimePlugin.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | 3 | using System; 4 | using System.ComponentModel; 5 | using Microsoft.SemanticKernel; 6 | 7 | using DescriptionAttribute = System.ComponentModel.DescriptionAttribute; 8 | namespace UniversalLLMFunctionCallerDemo.Plugins; 9 | 10 | /// 11 | /// TimePlugin provides a set of functions to get the current time and date. 12 | /// 13 | /// 14 | /// Note: the time represents the time on the hw/vm/machine where the kernel is running. 15 | /// TODO: import and use user's timezone 16 | /// 17 | public sealed class TimePlugin 18 | { 19 | /// 20 | /// Get the current date 21 | /// 22 | /// 23 | /// {{time.date}} => Sunday, 12 January, 2031 24 | /// 25 | /// The current date 26 | [KernelFunction, Description("Get the current date")] 27 | public string Date(IFormatProvider? formatProvider = null) => 28 | // Example: Sunday, 12 January, 2025 29 | DateTimeOffset.Now.ToString("D", formatProvider); 30 | 31 | /// 32 | /// Get the current date 33 | /// 34 | /// 35 | /// {{time.today}} => Sunday, 12 January, 2031 36 | /// 37 | /// The current date 38 | [KernelFunction, Description("Get the current date")] 39 | public string Today(IFormatProvider? formatProvider = null) => 40 | // Example: Sunday, 12 January, 2025 41 | Date(formatProvider); 42 | 43 | /// 44 | /// Get the current date and time in the local time zone" 45 | /// 46 | /// 47 | /// {{time.now}} => Sunday, January 12, 2025 9:15 PM 48 | /// 49 | /// The current date and time in the local time zone 50 | [KernelFunction, Description("Get the current date and time in the local time zone")] 51 | public string Now(IFormatProvider? formatProvider = null) => 52 | // Sunday, January 12, 2025 9:15 PM 53 | DateTimeOffset.Now.ToString("f", formatProvider); 54 | 55 | /// 56 | /// Get the current UTC date and time 57 | /// 58 | /// 59 | /// {{time.utcNow}} => Sunday, January 13, 2025 5:15 AM 60 | /// 61 | /// The current UTC date and time 62 | [KernelFunction, Description("Get the current UTC date and time")] 63 | public string UtcNow(IFormatProvider? formatProvider = null) => 64 | // Sunday, January 13, 2025 5:15 AM 65 | DateTimeOffset.UtcNow.ToString("f", formatProvider); 66 | 67 | /// 68 | /// Get the current time 69 | /// 70 | /// 71 | /// {{time.time}} => 09:15:07 PM 72 | /// 73 | /// The current time 74 | [KernelFunction, Description("Get the current time")] 75 | public string Time(IFormatProvider? formatProvider = null) => 76 | // Example: 09:15:07 PM 77 | DateTimeOffset.Now.ToString("hh:mm:ss tt", formatProvider); 78 | 79 | /// 80 | /// Get the current year 81 | /// 82 | /// 83 | /// {{time.year}} => 2025 84 | /// 85 | /// The current year 86 | [KernelFunction, Description("Get the current year")] 87 | public string Year(IFormatProvider? formatProvider = null) => 88 | // Example: 2025 89 | DateTimeOffset.Now.ToString("yyyy", formatProvider); 90 | 91 | /// 92 | /// Get the current month name 93 | /// 94 | /// 95 | /// {time.month}} => January 96 | /// 97 | /// The current month name 98 | [KernelFunction, Description("Get the current month name")] 99 | public string Month(IFormatProvider? formatProvider = null) => 100 | // Example: January 101 | DateTimeOffset.Now.ToString("MMMM", formatProvider); 102 | 103 | /// 104 | /// Get the current month number 105 | /// 106 | /// 107 | /// {{time.monthNumber}} => 01 108 | /// 109 | /// The current month number 110 | [KernelFunction, Description("Get the current month number")] 111 | public string MonthNumber(IFormatProvider? formatProvider = null) => 112 | // Example: 01 113 | DateTimeOffset.Now.ToString("MM", formatProvider); 114 | 115 | /// 116 | /// Get the current day of the month 117 | /// 118 | /// 119 | /// {{time.day}} => 12 120 | /// 121 | /// The current day of the month 122 | [KernelFunction, Description("Get the current day of the month")] 123 | public string Day(IFormatProvider? formatProvider = null) => 124 | // Example: 12 125 | DateTimeOffset.Now.ToString("dd", formatProvider); 126 | 127 | /// 128 | /// Get the date a provided number of days in the past 129 | /// 130 | /// The date the provided number of days before today 131 | [KernelFunction] 132 | [Description("Get the date offset by a provided number of days from today")] 133 | public string DaysAgo([Description("The number of days to offset from today")] double input, IFormatProvider? formatProvider = null) => 134 | DateTimeOffset.Now.AddDays(-input).ToString("D", formatProvider); 135 | 136 | /// 137 | /// Get the current day of the week 138 | /// 139 | /// 140 | /// {{time.dayOfWeek}} => Sunday 141 | /// 142 | /// The current day of the week 143 | [KernelFunction, Description("Get the current day of the week")] 144 | public string DayOfWeek(IFormatProvider? formatProvider = null) => 145 | // Example: Sunday 146 | DateTimeOffset.Now.ToString("dddd", formatProvider); 147 | 148 | /// 149 | /// Get the current clock hour 150 | /// 151 | /// 152 | /// {{time.hour}} => 9 PM 153 | /// 154 | /// The current clock hour 155 | [KernelFunction, Description("Get the current clock hour")] 156 | public string Hour(IFormatProvider? formatProvider = null) => 157 | // Example: 9 PM 158 | DateTimeOffset.Now.ToString("h tt", formatProvider); 159 | 160 | /// 161 | /// Get the current clock 24-hour number 162 | /// 163 | /// 164 | /// {{time.hourNumber}} => 21 165 | /// 166 | /// The current clock 24-hour number 167 | [KernelFunction, Description("Get the current clock 24-hour number")] 168 | public string HourNumber(IFormatProvider? formatProvider = null) => 169 | // Example: 21 170 | DateTimeOffset.Now.ToString("HH", formatProvider); 171 | 172 | /// 173 | /// Get the date of the previous day matching the supplied day name 174 | /// 175 | /// 176 | /// {{time.lastMatchingDay $dayName}} => Sunday, 7 May, 2023 177 | /// 178 | /// The date of the last instance of this day name 179 | /// dayName is not a recognized name of a day of the week 180 | [KernelFunction] 181 | [Description("Get the date of the last day matching the supplied week day name in English. Example: Che giorno era 'Martedi' scorso -> dateMatchingLastDayName 'Tuesday' => Tuesday, 16 May, 2023")] 182 | public string DateMatchingLastDayName( 183 | [Description("The day name to match")] DayOfWeek input, 184 | IFormatProvider? formatProvider = null) 185 | { 186 | DateTimeOffset dateTime = DateTimeOffset.Now; 187 | 188 | // Walk backwards from the previous day for up to a week to find the matching day 189 | for (int i = 1; i <= 7; ++i) 190 | { 191 | dateTime = dateTime.AddDays(-1); 192 | if (dateTime.DayOfWeek == input) 193 | { 194 | break; 195 | } 196 | } 197 | 198 | return dateTime.ToString("D", formatProvider); 199 | } 200 | 201 | /// 202 | /// Get the minutes on the current hour 203 | /// 204 | /// 205 | /// {{time.minute}} => 15 206 | /// 207 | /// The minutes on the current hour 208 | [KernelFunction, Description("Get the minutes on the current hour")] 209 | public string Minute(IFormatProvider? formatProvider = null) => 210 | // Example: 15 211 | DateTimeOffset.Now.ToString("mm", formatProvider); 212 | 213 | /// 214 | /// Get the seconds on the current minute 215 | /// 216 | /// 217 | /// {{time.second}} => 7 218 | /// 219 | /// The seconds on the current minute 220 | [KernelFunction, Description("Get the seconds on the current minute")] 221 | public string Second(IFormatProvider? formatProvider = null) => 222 | // Example: 07 223 | DateTimeOffset.Now.ToString("ss", formatProvider); 224 | 225 | /// 226 | /// Get the local time zone offset from UTC 227 | /// 228 | /// 229 | /// {{time.timeZoneOffset}} => -08:00 230 | /// 231 | /// The local time zone offset from UTC 232 | [KernelFunction, Description("Get the local time zone offset from UTC")] 233 | public string TimeZoneOffset(IFormatProvider? formatProvider = null) => 234 | // Example: -08:00 235 | DateTimeOffset.Now.ToString("%K", formatProvider); 236 | 237 | /// 238 | /// Get the local time zone name 239 | /// 240 | /// 241 | /// {{time.timeZoneName}} => PST 242 | /// 243 | /// 244 | /// Note: this is the "current" timezone and it can change over the year, e.g. from PST to PDT 245 | /// 246 | /// The local time zone name 247 | [KernelFunction, Description("Get the local time zone name")] 248 | public string TimeZoneName() => 249 | // Example: PST 250 | // Note: this is the "current" timezone and it can change over the year, e.g. from PST to PDT 251 | TimeZoneInfo.Local.DisplayName; 252 | } 253 | -------------------------------------------------------------------------------- /UniversalLLMFunctionCallerUnitTests/Plugins/WaitPlugin.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | 3 | using System; 4 | using System.ComponentModel; 5 | using System.Threading.Tasks; 6 | using Microsoft.Extensions.DependencyInjection; 7 | using Microsoft.SemanticKernel; 8 | 9 | namespace UniversalLLMFunctionCallerDemo.Plugins; 10 | 11 | /// 12 | /// WaitPlugin provides a set of functions to wait before making the rest of operations. 13 | /// 14 | public sealed class WaitPlugin 15 | { 16 | private readonly TimeProvider _timeProvider; 17 | 18 | /// 19 | /// Initializes a new instance of the class. 20 | /// 21 | public WaitPlugin() : this(null) { } 22 | 23 | /// 24 | /// Initializes a new instance of the class. 25 | /// 26 | /// An optional time provider. If not provided, a default time provider will be used. 27 | [ActivatorUtilitiesConstructor] 28 | public WaitPlugin(TimeProvider? timeProvider = null) => 29 | _timeProvider = timeProvider ?? TimeProvider.System; 30 | 31 | } 32 | -------------------------------------------------------------------------------- /UniversalLLMFunctionCallerUnitTests/UnitTest1.cs: -------------------------------------------------------------------------------- 1 | using JC.SemanticKernel.Planners.UniversalLLMFunctionCaller; 2 | using Microsoft.SemanticKernel; 3 | using Microsoft.SemanticKernel.Plugins.Web.Bing; 4 | using Microsoft.SemanticKernel.Plugins.Web; 5 | using SemanticKernel.IntegrationTests.Fakes; 6 | using UniversalLLMFunctionCallerDemo.Plugins; 7 | using Google.Apis.CustomSearchAPI.v1.Data; 8 | using Microsoft.SemanticKernel.ChatCompletion; 9 | 10 | namespace JC.SemanticKernel.Planners.UniversalLLMFunctionCaller.UniversalLLMFunctionCallerUnitTests 11 | { 12 | [TestClass] 13 | public class UniversalLLMFunctionCallerUnitTests 14 | { 15 | [TestMethod] 16 | public async Task TestFromChatHistory() 17 | { 18 | IKernelBuilder builder = Kernel.CreateBuilder(); 19 | builder.AddMistralChatCompletion(Environment.GetEnvironmentVariable("mistral_key"), "mistral-small"); 20 | var kernel = builder.Build(); 21 | string bingApiKey = Environment.GetEnvironmentVariable("bing_key"); 22 | var bingConnector = new BingConnector(bingApiKey); 23 | var webSearchEnginePlugin = new WebSearchEnginePlugin(bingConnector); 24 | kernel.ImportPluginFromObject(webSearchEnginePlugin, "WebSearch"); 25 | 26 | UniversalLLMFunctionCaller planner = new(kernel); 27 | 28 | ChatHistory history = new ChatHistory(); 29 | history.AddSystemMessage("You are a web searching assistant. You solve everything by searching the web for information"); 30 | history.AddUserMessage("What is the capital of france?"); 31 | 32 | string result = await planner.RunAsync(history); 33 | history.AddAssistantMessage(result); 34 | history.AddUserMessage("And of Denmark?"); 35 | result = await planner.RunAsync(history); 36 | Assert.IsTrue(result.Contains("Copenhagen")); 37 | } 38 | 39 | [TestMethod] 40 | public async Task ConvertStringSingleStepTestAsync() 41 | { 42 | IKernelBuilder builder = Kernel.CreateBuilder(); 43 | builder.AddMistralChatCompletion(Environment.GetEnvironmentVariable("mistral_key"), "mistral-small"); 44 | var kernel = builder.Build(); 45 | kernel.ImportPluginFromType("Text"); 46 | 47 | UniversalLLMFunctionCaller planner = new(kernel); 48 | string ask = "Make this text upper case: hello i want to grow please"; 49 | 50 | string result = await planner.RunAsync(ask); 51 | Assert.IsTrue(result.Contains("HELLO")); 52 | } 53 | [TestMethod] 54 | public async Task AddHoursMultiStepTestAsync() 55 | { 56 | IKernelBuilder builder = Kernel.CreateBuilder(); 57 | builder.AddMistralChatCompletion(Environment.GetEnvironmentVariable("mistral_key"), "mistral-small"); 58 | var kernel = builder.Build(); 59 | kernel.ImportPluginFromType("Time"); 60 | kernel.ImportPluginFromType("Math"); 61 | 62 | UniversalLLMFunctionCaller planner = new(kernel); 63 | string ask = "What is the current hour number, plus 5?"; 64 | 65 | string result = await planner.RunAsync(ask); 66 | string correctAnswer = (DateTime.Now.Hour + 5).ToString(); 67 | var containsCorrectAnswer = result.Contains(correctAnswer); 68 | Assert.IsTrue(containsCorrectAnswer); 69 | } 70 | 71 | [TestMethod] 72 | public async Task WebSearchTest() 73 | { 74 | IKernelBuilder builder = Kernel.CreateBuilder(); 75 | builder.AddMistralChatCompletion(Environment.GetEnvironmentVariable("mistral_key"), "mistral-small"); 76 | var kernel = builder.Build(); 77 | string bingApiKey = Environment.GetEnvironmentVariable("bing_key"); 78 | var bingConnector = new BingConnector(bingApiKey); 79 | var webSearchEnginePlugin = new WebSearchEnginePlugin(bingConnector); 80 | kernel.ImportPluginFromObject(webSearchEnginePlugin, "WebSearch"); 81 | 82 | UniversalLLMFunctionCaller planner = new(kernel); 83 | string ask = "What is the tallest mountain on Earth? How tall is it in meters?"; 84 | 85 | string result = await planner.RunAsync(ask); 86 | 87 | Assert.IsTrue(result.Contains("Everest") && (result.Contains("8848") || result.Contains("8.848") || result.Contains("8,848"))); 88 | } 89 | [TestMethod] 90 | public async Task CalcAndMailTest() 91 | { 92 | IKernelBuilder builder = Kernel.CreateBuilder(); 93 | builder.AddMistralChatCompletion(Environment.GetEnvironmentVariable("mistral_key"), "mistral-small"); 94 | 95 | var kernel = builder.Build(); 96 | 97 | kernel.ImportPluginFromType("Math"); 98 | kernel.ImportPluginFromType("Email"); 99 | 100 | UniversalLLMFunctionCaller planner = new(kernel); 101 | string ask = "What is 387 minus 22? Email the solution to John and Mary. " + 102 | "Tell me where the mail was sent to (the e-mail adress used) and what was wriiten in it."; 103 | 104 | string result = await planner.RunAsync(ask); 105 | 106 | Assert.IsTrue(result.Contains("@example.com") && (result.Contains("365") )); 107 | } 108 | } 109 | } -------------------------------------------------------------------------------- /UniversalLLMFunctionCallerUnitTests/UniversalLLMFunctionCallerUnitTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | false 9 | true 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | --------------------------------------------------------------------------------