├── .github ├── CODEOWNERS ├── dependabot.yaml ├── pull_request_template.yml └── workflows │ ├── build_tests.yml │ ├── execute_test_for_model.yml │ └── publish.yml ├── .gitignore ├── LICENSE.md ├── README.md ├── src ├── .dockerignore ├── SemanticKernel.Agents.DatabaseAgent.MCPServer │ ├── AsemblyInfo.cs │ ├── Configuration │ │ ├── AgentSettings.cs │ │ ├── AzureOpenAIConfig.cs │ │ ├── DatabaseSettings.cs │ │ ├── KernelSettings.cs │ │ ├── MCPServerSettings.cs │ │ ├── MemorySettings.cs │ │ ├── OllamaConfig.cs │ │ ├── QdrantMemorySettings.cs │ │ └── SQLiteMemorySettings.cs │ ├── DbConnectionFactory.cs │ ├── Docker.md │ ├── DockerScripts │ │ └── install │ │ │ ├── simbaspark.sh │ │ │ └── void.sh │ ├── Dockerfile │ ├── DotnetTool.md │ ├── Extensions │ │ ├── DatabaseKernelAgentExtension.cs │ │ └── IKernelBuilderExtension.cs │ ├── Internals │ │ └── AgentKernelFactory.cs │ ├── Program.cs │ ├── README.md │ └── SemanticKernel.Agents.DatabaseAgent.MCPServer.csproj ├── SemanticKernel.Agents.DatabaseAgent.QualityAssurance │ ├── Evaluators │ │ ├── QueryRelevancyEvaluation.cs │ │ └── QueryRelevancyEvaluator.cs │ ├── Extensions │ │ └── ServiceCollectionExtension.cs │ ├── Filters │ │ ├── QualityAssuranceFilterOptions.cs │ │ └── QueryRelevancyFilter.cs │ ├── Prompts │ │ └── QuestionExtraction.md │ ├── README.md │ └── SemanticKernel.Agents.DatabaseAgent.QualityAssurance.csproj ├── SemanticKernel.Agents.DatabaseAgent │ ├── .gitattributes │ ├── AgentDefinitionSnippet.cs │ ├── AgentDescriptionResponse.cs │ ├── AgentInstructionsResponse.cs │ ├── AgentNameRespone.cs │ ├── AgentResponse.cs │ ├── AsemblyInfo.cs │ ├── DatabaseAgent.cs │ ├── DatabaseAgentFactory.cs │ ├── DatabasePlugin.cs │ ├── DatabasePluginOptions.cs │ ├── ExplainTableResponse.cs │ ├── Extensions │ │ └── DBConnectionExtension.cs │ ├── ExtractTableNameResponse.cs │ ├── Filters │ │ ├── IQueryExecutionFilter.cs │ │ └── QueryExecutionContext.cs │ ├── IPromptProvider.cs │ ├── Internals │ │ ├── EmbeddedPromptProvider.cs │ │ ├── MarkdownRenderer.cs │ │ ├── QueryExecutor.cs │ │ └── RetryHelper.cs │ ├── Prompts │ │ ├── AgentDescriptionGenerator.md │ │ ├── AgentInstructionsGenerator.md │ │ ├── AgentNameGenerator.md │ │ ├── ExplainTable.md │ │ ├── ExtractTableName.md │ │ └── WriteSQLQuery.md │ ├── README.md │ ├── SemanticKernel.Agents.DatabaseAgent.csproj │ ├── TableDefinitionSnippet.cs │ └── WriteSQLQueryResponse.cs └── SemanticKernel.Plugins.DatabaseAgent.sln └── tests └── SemanticKernel.Agents.DatabaseAgent.Tests ├── AgentFactoryTest.cs ├── Evaluation.cs ├── Properties └── launchSettings.json ├── SemanticKernel.Agents.DatabaseAgent.Tests.csproj ├── appsettings.json └── northwind.db /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Each line is a file pattern followed by one or more owners. 2 | 3 | # These owners will be the default owners for everything in 4 | # the repo. Unless a later match takes precedence. 5 | 6 | * @kbeaugrand -------------------------------------------------------------------------------- /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | # Maintain dependencies for GitHub Actions 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "daily" 8 | # Maintain dependencies for NuGet packages 9 | - package-ecosystem: "nuget" 10 | directory: "/src" 11 | schedule: 12 | interval: "daily" 13 | ignore: 14 | - dependency-name: "System.*" 15 | update-types: ["version-update:semver-major"] 16 | - dependency-name: "Microsoft.Extensions.*" 17 | update-types: ["version-update:semver-major"] 18 | -------------------------------------------------------------------------------- /.github/pull_request_template.yml: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | What's new? 4 | 5 | - 6 | 7 | ## What kind of change does this PR introduce? 8 | 9 | - [ ] Bugfix 10 | - [ ] Feature 11 | - [ ] Code style update (formatting, local variables) 12 | - [ ] Refactoring (no functional changes, no api changes) 13 | - [ ] Build related changes 14 | - [ ] CI related changes 15 | - [ ] Documentation content changes 16 | - [ ] Tests 17 | - [ ] Other -------------------------------------------------------------------------------- /.github/workflows/build_tests.yml: -------------------------------------------------------------------------------- 1 | name: Build & Test 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | jobs: 10 | gpt-4o-mini: 11 | uses: ./.github/workflows/execute_test_for_model.yml 12 | secrets: inherit 13 | with: 14 | continue-on-error: true 15 | tag: gpt-4o-mini 16 | completion: GPT4OMINI 17 | embedding: TEXTEMBEDDINGADA002 18 | 19 | gpt-41-mini: 20 | needs: gpt-4o-mini 21 | uses: ./.github/workflows/execute_test_for_model.yml 22 | secrets: inherit 23 | with: 24 | continue-on-error: true 25 | tag: gpt-4.1-mini 26 | completion: GPT41MINI 27 | embedding: TEXTEMBEDDINGADA002 28 | 29 | devstral: 30 | needs: gpt-41-mini 31 | uses: ./.github/workflows/execute_test_for_model.yml 32 | secrets: inherit 33 | with: 34 | continue-on-error: true 35 | tag: devstral 36 | completion: DEVSTRAL 37 | embedding: NOMICEMBEDTEXT 38 | 39 | phi4: 40 | needs: devstral 41 | uses: ./.github/workflows/execute_test_for_model.yml 42 | secrets: inherit 43 | with: 44 | continue-on-error: true 45 | tag: phi4 46 | completion: PHI4 47 | embedding: NOMICEMBEDTEXT 48 | 49 | qwen2-5-coder: 50 | needs: phi4 51 | uses: ./.github/workflows/execute_test_for_model.yml 52 | secrets: inherit 53 | with: 54 | continue-on-error: true 55 | tag: qwen2.5-coder 56 | completion: QWEN25CODER 57 | embedding: NOMICEMBEDTEXT 58 | 59 | qwen3-8b: 60 | needs: qwen2-5-coder 61 | uses: ./.github/workflows/execute_test_for_model.yml 62 | secrets: inherit 63 | with: 64 | continue-on-error: true 65 | tag: qwen3:8b 66 | completion: QWEN38B 67 | embedding: NOMICEMBEDTEXT 68 | 69 | # llama3-3: 70 | # needs: qwen3-8b 71 | # uses: ./.github/workflows/execute_test_for_model.yml 72 | # secrets: inherit 73 | # with: 74 | # tag: llama3.3 75 | # completion: LLAMA33 76 | # embedding: NOMICEMBEDTEXT 77 | 78 | llama4: 79 | needs: qwen3-8b 80 | uses: ./.github/workflows/execute_test_for_model.yml 81 | secrets: inherit 82 | with: 83 | continue-on-error: true 84 | tag: llama3.3 85 | completion: LLAMA4 86 | embedding: NOMICEMBEDTEXT 87 | -------------------------------------------------------------------------------- /.github/workflows/execute_test_for_model.yml: -------------------------------------------------------------------------------- 1 | name: .NET Test on LLM model 2 | 3 | on: 4 | workflow_call: 5 | inputs: 6 | tag: 7 | required: true 8 | type: string 9 | completion: 10 | required: true 11 | type: string 12 | embedding: 13 | required: true 14 | type: string 15 | continue-on-error: 16 | required: false 17 | type: boolean 18 | default: false 19 | 20 | jobs: 21 | test: 22 | runs-on: ubuntu-latest 23 | name: Test ${{ inputs.tag }} 24 | continue-on-error: ${{ inputs.continue-on-error || 'false' }} 25 | steps: 26 | - uses: actions/checkout@v4 27 | - uses: actions/setup-dotnet@v4 28 | with: 29 | dotnet-version: 8.x 30 | - run: dotnet restore 31 | working-directory: ./src/ 32 | - run: dotnet build --no-restore 33 | working-directory: ./src/ 34 | - run: dotnet test --no-build --verbosity normal 35 | working-directory: ./src/ 36 | env: 37 | DATABASE__CONNECTIONSTRING: ${{ secrets.DATABASE__CONNECTIONSTRING }} 38 | 39 | KERNEL__COMPLETION: ${{ inputs.completion }} 40 | KERNEL__EMBEDDING: ${{ inputs.embedding }} 41 | MEMORY__KIND: volatile 42 | MEMORY__TOPK: 15 43 | MEMORY__MAXTOKENS: 2000 44 | MEMORY__TEMPERATURE: 0.1 45 | MEMORY__TOPP: 0.1 46 | 47 | ## Azure OpenAI Service 48 | SERVICES__GPT4OMINI__TYPE: AzureOpenAI 49 | SERVICES__GPT4OMINI__ENDPOINT: ${{ secrets.AZUREOPENAI_ENDPOINT }} 50 | SERVICES__GPT4OMINI__APIKEY: ${{ secrets.AZUREOPENAI_APIKEY }} 51 | SERVICES__GPT4OMINI__DEPLOYMENT: gpt-4o-mini 52 | SERVICES__GPT41MINI__TYPE: AzureOpenAI 53 | SERVICES__GPT41MINI__ENDPOINT: ${{ secrets.AZUREOPENAI_ENDPOINT }} 54 | SERVICES__GPT41MINI__APIKEY: ${{ secrets.AZUREOPENAI_APIKEY }} 55 | SERVICES__GPT41MINI__DEPLOYMENT: gpt-4.1-mini 56 | SERVICES__TEXTEMBEDDINGADA002__TYPE: AzureOpenAI 57 | SERVICES__TEXTEMBEDDINGADA002__ENDPOINT: ${{ secrets.AZUREOPENAI_ENDPOINT }} 58 | SERVICES__TEXTEMBEDDINGADA002__APIKEY: ${{ secrets.AZUREOPENAI_APIKEY }} 59 | SERVICES__TEXTEMBEDDINGADA002__DEPLOYMENT: text-embedding-ada-002 60 | 61 | ## Qwen Models via Ollama 62 | SERVICES__QWEN25CODER__TYPE: Ollama 63 | SERVICES__QWEN25CODER__ENDPOINT: ${{ secrets.OLLAMA_ENDPOINT }} 64 | SERVICES__QWEN25CODER__MODELID: qwen2.5-coder:7b 65 | SERVICES__QWEN38B__TYPE: Ollama 66 | SERVICES__QWEN38B__ENDPOINT: ${{ secrets.OLLAMA_ENDPOINT }} 67 | SERVICES__QWEN38B__MODELID: qwen3:8b 68 | 69 | ## Mistral Models via Ollama 70 | SERVICES__DEVSTRAL__TYPE: Ollama 71 | SERVICES__DEVSTRAL__ENDPOINT: ${{ secrets.OLLAMA_ENDPOINT }} 72 | SERVICES__DEVSTRAL__MODELID: devstral:24b 73 | 74 | ## Phi Models via Ollama 75 | SERVICES__PHI4__TYPE: Ollama 76 | SERVICES__PHI4__ENDPOINT: ${{ secrets.OLLAMA_ENDPOINT }} 77 | SERVICES__PHI4__MODELID: phi4:14b 78 | 79 | ## Llama Models via Ollama 80 | SERVICES__LLAMA4__TYPE: Ollama 81 | SERVICES__LLAMA4__ENDPOINT: ${{ secrets.OLLAMA_ENDPOINT }} 82 | SERVICES__LLAMA4__MODELID: llama4:scout 83 | 84 | SERVICES__LLAMA33__TYPE: Ollama 85 | SERVICES__LLAMA33__ENDPOINT: ${{ secrets.OLLAMA_ENDPOINT }} 86 | SERVICES__LLAMA33__MODELID: llama3.3:70b 87 | 88 | ## Nomic Embedding Models via Ollama 89 | SERVICES__NOMICEMBEDTEXT__TYPE: Ollama 90 | SERVICES__NOMICEMBEDTEXT__ENDPOINT: ${{ secrets.OLLAMA_ENDPOINT }} 91 | SERVICES__NOMICEMBEDTEXT__MODELID: nomic-embed-text:v1.5 92 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a .NET project 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-net 3 | 4 | name: Create Release 5 | 6 | on: 7 | release: 8 | types: [published] 9 | env: 10 | registry_name: ghcr.io 11 | image_name: database-mcp-server 12 | 13 | jobs: 14 | docker: 15 | name: Build and Push MCP Docker Image 16 | runs-on: ubuntu-latest 17 | strategy: 18 | matrix: 19 | tag: ['Default', 'ODBCSpark'] 20 | include: 21 | - tag: Default 22 | installer: void 23 | image_flavor: | 24 | latest=true 25 | - tag: ODBCSpark 26 | image_name: odbc-spark- 27 | installer: simbaspark 28 | version: '2.9.1' 29 | os: Debian-64bit 30 | build: '1001' 31 | image_flavor: | 32 | latest=false 33 | steps: 34 | - name: Checkout code 35 | uses: actions/checkout@v4 36 | 37 | - name: Docker Login to ACR 38 | # You may pin to the exact commit or the version. 39 | uses: docker/login-action@v3.4.0 40 | with: 41 | registry: ghcr.io 42 | username: ${{ github.actor }} 43 | password: ${{ secrets.GITHUB_TOKEN }} 44 | 45 | - name: Docker meta 46 | id: meta 47 | uses: docker/metadata-action@v5 48 | with: 49 | # list of Docker images to use as base name for tags 50 | images: | 51 | ${{ env.registry_name }}/${{ github.repository_owner }}/${{ env.image_name }} 52 | # generate Docker tags based on the following events/attributes 53 | tags: | 54 | type=schedule,prefix=${{ matrix.image_name }} 55 | type=ref,event=branch,prefix=${{ matrix.image_name }} 56 | type=ref,event=pr,prefix=${{ matrix.image_name }} 57 | type=semver,pattern={{version}},prefix=${{ matrix.image_name }} 58 | type=semver,pattern={{major}}.{{minor}},prefix=${{ matrix.image_name }} 59 | type=semver,pattern={{major}},prefix=${{ matrix.image_name }} 60 | flavor: | 61 | ${{ matrix.image_flavor }} 62 | 63 | - name: Build and push Docker image 64 | uses: docker/build-push-action@v6 65 | with: 66 | context: ./ 67 | file: ./src/SemanticKernel.Agents.DatabaseAgent.MCPServer/Dockerfile 68 | build-args: | 69 | RUNTIME_SDK_INSTALLER=${{ matrix.INSTALLER }} 70 | VERSION=${{ matrix.VERSION }} 71 | OS=${{ matrix.OS }} 72 | BUILD=${{ matrix.BUILD }} 73 | tags: 74 | ${{ steps.meta.outputs.tags }} 75 | push: true 76 | 77 | nuget: 78 | name: Publish NuGet Packages 79 | runs-on: ubuntu-latest 80 | strategy: 81 | matrix: 82 | package: 83 | - SemanticKernel.Agents.DatabaseAgent 84 | - SemanticKernel.Agents.DatabaseAgent.QualityAssurance 85 | - SemanticKernel.Agents.DatabaseAgent.MCPServer 86 | 87 | steps: 88 | - uses: actions/checkout@v4 89 | - name: Setup .NET 90 | uses: actions/setup-dotnet@v4 91 | with: 92 | dotnet-version: 8.x 93 | - name: Restore dependencies 94 | run: dotnet restore 95 | working-directory: ./src/${{ matrix.package }}/ 96 | - name: Build 97 | run: dotnet build --no-restore --configuration Release 98 | working-directory: ./src/${{ matrix.package }}/ 99 | - name: Pack 100 | run: dotnet pack --configuration Release /p:Version=${{ github.event.release.tag_name }} 101 | working-directory: ./src/${{ matrix.package }}/ 102 | - name: Push to NuGet 103 | run: | 104 | dotnet nuget push **/*.nupkg --source nuget.org --api-key ${{ secrets.NUGET_API_KEY }} --skip-duplicate 105 | working-directory: ./src/${{ matrix.package }}/ 106 | -------------------------------------------------------------------------------- /.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 364 | 365 | # Local app settings file 366 | appsettings.*.json 367 | /src/SemanticKernel.Agents.DatabaseAgent.MCPServer/Properties/launchSettings.json 368 | /src/SemanticKernel.Agents.DatabaseAgent.MCPServer/northwind.db 369 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Kevin BEAUGRAND 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Database Agent for Semantic Kernel 2 | 3 | [![Build & Test](https://github.com/kbeaugrand/SemanticKernel.Agents.DatabaseAgent/actions/workflows/build_tests.yml/badge.svg)](https://github.com/kbeaugrand/SemanticKernel.Agents.DatabaseAgent/actions/workflows/build_test.yml) 4 | [![Create Release](https://github.com/kbeaugrand/SemanticKernel.Agents.DatabaseAgent/actions/workflows/publish.yml/badge.svg)](https://github.com/kbeaugrand/SemanticKernel.Agents.DatabaseAgent/actions/workflows/publish.yml) 5 | [![Version](https://img.shields.io/github/v/release/kbeaugrand/SemanticKernel.Agents.DatabaseAgent)](https://img.shields.io/github/v/release/kbeaugrand/SemanticKernel.Agents.DatabaseAgent) 6 | [![License](https://img.shields.io/github/license/kbeaugrand/SemanticKernel.Agents.DatabaseAgent)](https://img.shields.io/github/v/release/kbeaugrand/SemanticKernel.Agents.DatabaseAgent) 7 | 8 | ## Overview 9 | 10 | The Database Agent for Semantic Kernel is a service that provides a database management system (DBMS) for the Semantic Kernel (NL2SQL). The Agent is responsible for managing the storage and retrieval of data from the Semantic Kernel. 11 | This built on top of the [Microsoft's Semantic Kernel](https://github.com/microsoft/semantic-kernel) and Semantic Kernel Memory connectors to memorize database schema and relationships to provide a more efficient and accurate database management system. 12 | 13 | image 14 | 15 | 16 | ## Models Tested 17 | 18 | | Model Name | NL 2 SQL | Quality Insurance | Score | Speed (avg time/op.) | 19 | |---------------------|:---------:|:----------------:|:-------:|:----------------------:| 20 | | gpt-4o-mini | ✅ | ✅ | 90% | Fast (~3sec) | 21 | | phi4:14b | ✅ | ✅ | 90% | Slow (~10sec) | 22 | | llama4:scout | ✅ | ✅ | 90% | Slow (~10sec) | 23 | | gpt-4.1-mini | ✅ | ✅ | 80% | Fast (~3sec) | 24 | | devstral:24b | ✅ | ✅ | 80% | Slow (~10sec) | 25 | | qwen2.5-coder:7b | ⚠️ (WIP) | ⚠️ (WIP) | 50% | Fast (~3sec) | 26 | | qwen3:8b | ⚠️ (WIP) | ⚠️ (WIP) | 50% | Slow (~10sec) | 27 | 28 | > Note: current score is a personal evaluation regarding the test cases with Northwind database and a set of queries. 29 | > development is firstly focused on the gpt-4o-mini model, which is the most performant and accurate model for NL2SQL tasks. 30 | > for the evaluation, the TopP and Temperature parameters are set to 0.1, which is the recommended setting. 31 | 32 | **DICLAIMER** 33 | 34 | Even if the model is marked as tested, it does not mean that it will work for all queries. 35 | 36 | Furthermore, **using LLM agents might lead to risks such as unintended data exposure, security vulnerabilities, and inefficient query execution, potentially compromising system integrity and compliance requirements.** 37 | 38 | ## Getting Started 39 | 40 | ### Prerequisites 41 | 42 | - [.NET 8.0 SDK](https://dotnet.microsoft.com/download/dotnet/8.0) 43 | 44 | ### Installation 45 | 46 | To use the Database Agent for Semantic Kernel, you must first install the package from NuGet. 47 | 48 | ```bash 49 | dotnet add package SemanticKernel.Agents.DatabaseAgent 50 | ``` 51 | 52 | ### Usage 53 | 54 | To use the Database Agent for Semantic Kernel, you must first create an instance of the `DatabaseAgent` class and provide the necessary configuration settings. 55 | 56 | ```csharp 57 | using Microsoft.KernelMemory; 58 | using Microsoft.SemanticKernel; 59 | using Microsoft.SemanticKernel.ChatCompletion; 60 | using SemanticKernel.Agents.DatabaseAgent; 61 | 62 | var kernelBuilder = Kernel.CreateBuilder() 63 | ... 64 | .Build(); 65 | 66 | kernelBuilder.Services.AddSingleton((sp) => 67 | { 68 | // Configure the database connection 69 | return new SqliteConnection(configuration.GetConnectionString("DefaultConnection")); 70 | }); 71 | 72 | var kernel = kernelBuilder.Build(); 73 | 74 | var agent = await DBMSAgentFactory.CreateAgentAsync(kernel); 75 | 76 | // execute the NL2SQL query 77 | var responses = agent.InvokeAsync([new ChatMessageContent { Content = question, Role = AuthorRole.User }], thread: null) 78 | .ConfigureAwait(false); 79 | ``` 80 | 81 | ## Install the MCP Server as a Docker Image 82 | 83 | The database agent MCP server can be run as a Docker image. This allows you to run the server in a containerized environment, making it easy to deploy and manage to expose it SSE (Server-Sent Events) and HTTP endpoints. 84 | 85 | To run the MCP server as a Docker image, you can use the following command: 86 | 87 | ```bash 88 | 89 | docker run -it --rm \ 90 | -p 8080:5000 \ 91 | -e DATABASE_PROVIDER=sqlite \ 92 | -e DATABASE_CONNECTION_STRING="Data Source=northwind.db;Mode=ReadWrite" \ 93 | -e MEMORY_KIND=Volatile \ 94 | -e KERNEL_COMPLETION=gpt4omini \ 95 | -e KERNEL_EMBEDDING=textembeddingada002 \ 96 | -e SERVICES_GPT4OMINI_TYPE=AzureOpenAI \ 97 | -e SERVICES_GPT4OMINI_ENDPOINT=https://xxx.openai.azure.com/ \ 98 | -e SERVICES_GPT4OMINI_AUTH=APIKey \ 99 | -e SERVICES_GPT4OMINI_API_KEY=xxx \ 100 | -e SERVICES_GPT4OMINI_DEPLOYMENT=gpt-4o-mini \ 101 | -e SERVICES_TEXTEMBEDDINGADA002_TYPE=AzureOpenAI \ 102 | -e SERVICES_TEXTEMBEDDINGADA002_ENDPOINT=https://xxx.openai.azure.com/ \ 103 | -e SERVICES_TEXTEMBEDDINGADA002_AUTH=APIKey \ 104 | -e SERVICES_TEXTEMBEDDINGADA002_API_KEY=xxx \ 105 | -e SERVICES_TEXTEMBEDDINGADA002_DEPLOYMENT=text-embedding-ada-002 \ 106 | ghcr.io/kbeaugrand/database-mcp-server 107 | ``` 108 | 109 | ### Behind the scenes 110 | 111 | Here is a simplified sequence diagram of how the Database Agent is constructed using the Semantic Kernel before it can be used: 112 | 113 | ```mermaid 114 | sequenceDiagram 115 | autonumber 116 | participant Client 117 | participant DatabaseAgentFactory 118 | participant SemanticKernel 119 | participant Database 120 | 121 | Client->>DatabaseAgentFactory: CreateAgentAsync(kernel) 122 | DatabaseAgentFactory->>SemanticKernel: Access services (vector store, embedding, prompts) 123 | 124 | DatabaseAgentFactory->>DatabaseAgentFactory: MemorizeAgentSchema() 125 | DatabaseAgentFactory->>Database: Fetch list of tables 126 | loop For each table 127 | DatabaseAgentFactory->>Database: Get structure and data sample 128 | DatabaseAgentFactory->>SemanticKernel: Generate table description 129 | DatabaseAgentFactory->>SemanticKernel: Embed and store definition 130 | end 131 | 132 | DatabaseAgentFactory->>SemanticKernel: Generate agent description 133 | DatabaseAgentFactory->>SemanticKernel: Generate name and instructions 134 | DatabaseAgentFactory->>SemanticKernel: Embed and store agent 135 | 136 | DatabaseAgentFactory-->>Client: Return DatabaseKernelAgent 137 | ``` 138 | 139 | Then, once the agent is created, the client can use it to execute queries. 140 | 141 | ```mermaid 142 | sequenceDiagram 143 | autonumber 144 | participant User 145 | participant DatabasePlugin 146 | participant SemanticKernel 147 | participant Database 148 | 149 | User->>DatabasePlugin: ExecuteQueryAsync(prompt) 150 | 151 | DatabasePlugin->>SemanticKernel: Generate embedding for prompt 152 | SemanticKernel-->>DatabasePlugin: Embedding 153 | 154 | DatabasePlugin->>SemanticKernel: Vector search for related tables 155 | SemanticKernel-->>DatabasePlugin: Matching table definitions 156 | 157 | DatabasePlugin->>SemanticKernel: Generate SQL query (WriteSQLQuery prompt) 158 | SemanticKernel-->>DatabasePlugin: SQL query string 159 | 160 | DatabasePlugin->>DatabasePlugin: Check query filters (optional) 161 | alt Query is allowed 162 | DatabasePlugin->>Database: Execute SQL query 163 | Database-->>DatabasePlugin: Query result 164 | DatabasePlugin-->>User: Markdown-formatted result 165 | else Query is blocked 166 | DatabasePlugin-->>User: Filter message 167 | end 168 | 169 | Note over DatabasePlugin: Logs and error handling during the process 170 | 171 | ``` 172 | 173 | ## Quality insurance 174 | 175 | Using LLM agents to write and execute its own queries into a database might lead to risks such as unintended data exposure, security vulnerabilities, and inefficient query execution, potentially compromising system integrity and compliance requirements. 176 | To mitigate these risks, the Database Agent for Semantic Kernel provides a set of quality assurance features to ensure the safety and reliability of the queries executed by the agent. 177 | 178 | ### Additional Configuration 179 | 180 | First, you must add the ``QualityAssurance`` package for DatabaseAgent to your project. 181 | 182 | ```bash 183 | dotnet add package SemanticKernel.Agents.DatabaseAgent.QualityAssurance 184 | ``` 185 | 186 | Next, you must configure the quality insurance settings for the Database Agent. 187 | ```csharp 188 | kernelBuilder.Services.UseDatabaseAgentQualityAssurance(opts => 189 | { 190 | opts.EnableQueryRelevancyFilter = true; 191 | opts.QueryRelevancyThreshold = .8f; 192 | }); 193 | ``` 194 | 195 | ### Quality Assurance Features 196 | 197 | The Database Agent for Semantic Kernel provides the following quality assurance features: 198 | `QueryRelevancyFilter`: Ensures that only relevant queries are executed by the agent. The filter uses LLM to generate the description of the query that is intended to be executed, then compute the cosine similarity between the user prompt and the generated description. If the similarity score is below the threshold, the query is rejected. 199 | 200 | ### Create a custom quality assurance filter 201 | 202 | You can create a custom quality assurance filter by implementing the `IQueryExecutionFilter` interface and registering it with the DI container. 203 | ```csharp 204 | kernelBuilder.Services.AddTransient(); 205 | 206 | public class CustomQueryExecutionFilter : IQueryExecutionFilter 207 | { 208 | public async Task OnQueryExecutionAsync(QueryExecutionContext context, Func next) 209 | { 210 | // Implement custom query execution logic 211 | return Task.FromResult(true); 212 | } 213 | } 214 | ``` 215 | 216 | ## Contributing 217 | 218 | We welcome contributions to enhance this project. Please fork the repository and submit a pull request with your proposed changes. 219 | 220 | ## License 221 | 222 | This project is licensed under the MIT License. See the [LICENSE](LICENSE.md) file for details. 223 | 224 | ## Acknowledgments 225 | 226 | - [Microsoft's Kernel Memory](https://github.com/microsoft/kernel-memory) for providing the foundational AI service. 227 | - The open-source community for continuous support and contributions. 228 | -------------------------------------------------------------------------------- /src/.dockerignore: -------------------------------------------------------------------------------- 1 | **/.classpath 2 | **/.dockerignore 3 | **/.env 4 | **/.git 5 | **/.gitignore 6 | **/.project 7 | **/.settings 8 | **/.toolstarget 9 | **/.vs 10 | **/.vscode 11 | **/*.*proj.user 12 | **/*.dbmdl 13 | **/*.jfm 14 | **/azds.yaml 15 | **/bin 16 | **/charts 17 | **/docker-compose* 18 | **/Dockerfile* 19 | **/node_modules 20 | **/npm-debug.log 21 | **/obj 22 | **/secrets.dev.yaml 23 | **/values.dev.yaml 24 | LICENSE 25 | README.md 26 | !**/.gitignore 27 | !.git/HEAD 28 | !.git/config 29 | !.git/packed-refs 30 | !.git/refs/heads/** -------------------------------------------------------------------------------- /src/SemanticKernel.Agents.DatabaseAgent.MCPServer/AsemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | using System.Runtime.InteropServices; 3 | 4 | // In SDK-style projects such as this one, several assembly attributes that were historically 5 | // defined in this file are now automatically added during build and populated with 6 | // values defined in project properties. For details of which attributes are included 7 | // and how to customise this process see: https://aka.ms/assembly-info-properties 8 | 9 | 10 | // Setting ComVisible to false makes the types in this assembly not visible to COM 11 | // components. If you need to access a type in this assembly from COM, set the ComVisible 12 | // attribute to true on that type. 13 | 14 | [assembly: ComVisible(false)] 15 | 16 | // The following GUID is for the ID of the typelib if this project is exposed to COM. 17 | 18 | [assembly: Guid("2c8d417e-556b-4dd0-88bf-5ea65adc30f4")] 19 | [assembly: InternalsVisibleTo("SemanticKernel.Agents.DatabaseAgent.Tests")] 20 | -------------------------------------------------------------------------------- /src/SemanticKernel.Agents.DatabaseAgent.MCPServer/Configuration/AgentSettings.cs: -------------------------------------------------------------------------------- 1 | using SemanticKernel.Agents.DatabaseAgent.QualityAssurance.Filters; 2 | 3 | namespace SemanticKernel.Agents.DatabaseAgent.MCPServer.Configuration; 4 | 5 | internal class AgentSettings 6 | { 7 | public required KernelSettings Kernel { get; set; } 8 | 9 | public required QualityAssuranceFilterOptions QualityAssurance { get; set; } 10 | 11 | public required TransportSettings Transport { get; set; } 12 | } 13 | 14 | internal sealed class TransportSettings 15 | { 16 | internal enum TransportType 17 | { 18 | Stdio, 19 | Sse 20 | } 21 | 22 | public TransportType Kind { get; set; } = TransportType.Stdio; 23 | } 24 | -------------------------------------------------------------------------------- /src/SemanticKernel.Agents.DatabaseAgent.MCPServer/Configuration/AzureOpenAIConfig.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace SemanticKernel.Agents.DatabaseAgent.MCPServer.Configuration 4 | { 5 | internal class AzureOpenAIConfig 6 | { 7 | [Required] 8 | public string Deployment { get; set; } = string.Empty; 9 | 10 | [Required] 11 | public string Endpoint { get; set; } = string.Empty; 12 | 13 | [Required] 14 | public string APIKey { get; set; } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/SemanticKernel.Agents.DatabaseAgent.MCPServer/Configuration/DatabaseSettings.cs: -------------------------------------------------------------------------------- 1 | namespace SemanticKernel.Agents.DatabaseAgent.MCPServer.Configuration; 2 | 3 | internal class DatabaseSettings 4 | { 5 | public required string Provider { get; set; } 6 | 7 | public required string ConnectionString { get; set; } 8 | } 9 | -------------------------------------------------------------------------------- /src/SemanticKernel.Agents.DatabaseAgent.MCPServer/Configuration/KernelSettings.cs: -------------------------------------------------------------------------------- 1 | namespace SemanticKernel.Agents.DatabaseAgent.MCPServer.Configuration; 2 | 3 | internal class KernelSettings 4 | { 5 | public required string Completion { get; set; } 6 | 7 | public required string Embedding { get; set; } 8 | } 9 | -------------------------------------------------------------------------------- /src/SemanticKernel.Agents.DatabaseAgent.MCPServer/Configuration/MCPServerSettings.cs: -------------------------------------------------------------------------------- 1 | namespace SemanticKernel.Agents.DatabaseAgent.MCPServer.Configuration; 2 | 3 | internal class MCPServerSettings 4 | { 5 | public required DatabaseSettings Database { get; set; } 6 | 7 | public required MemorySettings Memory { get; set; } 8 | } -------------------------------------------------------------------------------- /src/SemanticKernel.Agents.DatabaseAgent.MCPServer/Configuration/MemorySettings.cs: -------------------------------------------------------------------------------- 1 | namespace SemanticKernel.Agents.DatabaseAgent.MCPServer.Configuration; 2 | 3 | internal class MemorySettings 4 | { 5 | public enum StorageType 6 | { 7 | Volatile, 8 | SQLite, 9 | Qdrant, 10 | } 11 | 12 | public required StorageType Kind { get; set; } 13 | } 14 | -------------------------------------------------------------------------------- /src/SemanticKernel.Agents.DatabaseAgent.MCPServer/Configuration/OllamaConfig.cs: -------------------------------------------------------------------------------- 1 |  2 | 3 | namespace SemanticKernel.Agents.DatabaseAgent.MCPServer.Configuration; 4 | 5 | internal class OllamaConfig 6 | { 7 | public string ModelId { get; set; } 8 | 9 | public string Endpoint { get; set; } 10 | } 11 | -------------------------------------------------------------------------------- /src/SemanticKernel.Agents.DatabaseAgent.MCPServer/Configuration/QdrantMemorySettings.cs: -------------------------------------------------------------------------------- 1 | namespace SemanticKernel.Agents.DatabaseAgent.MCPServer.Configuration 2 | { 3 | internal class QdrantMemorySettings 4 | { 5 | public string Host { get; set; } 6 | 7 | public int Port { get; set; } = 6333; 8 | 9 | public bool Https { get; set; } = false; 10 | 11 | public string? APIKey { get; set; } = null; 12 | 13 | public int Dimensions { get; set; } = 1536; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/SemanticKernel.Agents.DatabaseAgent.MCPServer/Configuration/SQLiteMemorySettings.cs: -------------------------------------------------------------------------------- 1 | namespace SemanticKernel.Agents.DatabaseAgent.MCPServer.Configuration 2 | { 3 | internal class SQLiteMemorySettings 4 | { 5 | public string ConnectionString { get; set; } 6 | 7 | public int Dimensions { get; set; } = 1536; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/SemanticKernel.Agents.DatabaseAgent.MCPServer/DbConnectionFactory.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Data.SqlClient; 2 | using Microsoft.Data.Sqlite; 3 | using MySql.Data.MySqlClient; 4 | using Npgsql; 5 | using Oracle.ManagedDataAccess.Client; 6 | using System.Data.Common; 7 | using System.Data.Odbc; 8 | using System.Data.OleDb; 9 | 10 | namespace SemanticKernel.Agents.DatabaseAgent.MCPServer; 11 | 12 | public class DbConnectionFactory 13 | { 14 | public static DbConnection CreateDbConnection(string connectionString, string providerType) 15 | { 16 | switch (providerType.ToLower()) 17 | { 18 | case "sqlserver": 19 | return new SqlConnection(connectionString); 20 | 21 | case "sqlite": 22 | return new SqliteConnection(connectionString); 23 | 24 | case "mysql": 25 | return new MySqlConnection(connectionString); 26 | 27 | case "postgresql": 28 | return new NpgsqlConnection(connectionString); 29 | 30 | case "oracle": 31 | return new OracleConnection(connectionString); 32 | 33 | case "access": 34 | return new OleDbConnection(connectionString); 35 | 36 | case "odbc": 37 | return new OdbcConnection(connectionString); 38 | 39 | default: 40 | throw new ArgumentException($"Unsupported provider type: {providerType}"); 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /src/SemanticKernel.Agents.DatabaseAgent.MCPServer/Docker.md: -------------------------------------------------------------------------------- 1 | # Database Agent MCP Server 2 | 3 | ## Install the MCP Server as a Docker Image 4 | 5 | The database agent MCP server can be run as a Docker image. This allows you to run the server in a containerized environment, making it easy to deploy and manage to expose it SSE (Server-Sent Events) and HTTP endpoints. 6 | 7 | To run the MCP server as a Docker image, you can use the following command: 8 | 9 | ```bash 10 | 11 | docker run -it --rm \ 12 | -p 8080:5000 \ 13 | -e DATABASE_PROVIDER=sqlite \ 14 | -e DATABASE_CONNECTION_STRING="Data Source=northwind.db;Mode=ReadWrite" \ 15 | -e MEMORY_KIND=Volatile \ 16 | -e KERNEL_COMPLETION=gpt-4o-mini \ 17 | -e KERNEL_EMBEDDING=text-embedding-ada-002 \ 18 | -e SERVICES_GPT_4O_MINI_TYPE=AzureOpenAI \ 19 | -e SERVICES_GPT_4O_MINI_ENDPOINT=https://xxx.openai.azure.com/ \ 20 | -e SERVICES_GPT_4O_MINI_AUTH=APIKey \ 21 | -e SERVICES_GPT_4O_MINI_API_KEY=xxx \ 22 | -e SERVICES_GPT_4O_MINI_DEPLOYMENT=gpt-4o-mini \ 23 | -e SERVICES_TEXT_EMBEDDING_ADA_002_TYPE=AzureOpenAI \ 24 | -e SERVICES_TEXT_EMBEDDING_ADA_002_ENDPOINT=https://xxx.openai.azure.com/ \ 25 | -e SERVICES_TEXT_EMBEDDING_ADA_002_AUTH=APIKey \ 26 | -e SERVICES_TEXT_EMBEDDING_ADA_002_API_KEY=xxx \ 27 | -e SERVICES_TEXT_EMBEDDING_ADA_002_DEPLOYMENT=text-embedding-ada-002 \ 28 | ghcr.io/kbeaugrand/database-mcp-server 29 | ``` -------------------------------------------------------------------------------- /src/SemanticKernel.Agents.DatabaseAgent.MCPServer/DockerScripts/install/simbaspark.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Parameters for version and OS 4 | VERSION=${1:-2.9.1} 5 | OS=${2:-Debian-64bit} 6 | BUILD=${3:-1001} 7 | 8 | # Extract the major version number from the version 9 | MAJOR_VERSION=$(echo $VERSION | cut -d '.' -f 1) 10 | 11 | # Construct the download URL 12 | URL="https://databricks-bi-artifacts.s3.us-east-2.amazonaws.com/simbaspark-drivers/odbc/${VERSION}/SimbaSparkODBC-${VERSION}.${BUILD}-${OS}.zip" 13 | 14 | echo "Downloading Simba Spark ODBC Driver version $VERSION for OS $OS" 15 | echo "Download URL: $URL" 16 | 17 | # Download the package 18 | wget $URL 19 | 20 | # Unzip the package 21 | unzip SimbaSparkODBC-${VERSION}.${BUILD}-${OS}.zip 22 | 23 | # Move the driver to the appropriate directory 24 | mkdir -p /opt/sparkodbc 25 | mv *.deb /opt/sparkodbc 26 | 27 | # Remove the downloaded zip file and extracted directory 28 | rm -rf SimbaSparkODBC-${VERSION}.${BUILD}-${OS}.zip 29 | rm -rf docs 30 | 31 | # Install dependencies 32 | apt-get update && \ 33 | apt-get install -y --no-install-recommends libsasl2-2 libsasl2-modules-gssapi-mit libodbc2 unixodbc \ 34 | && rm -rf /var/lib/apt/lists/* 35 | 36 | # Install the package from deb file 37 | dpkg -i /opt/sparkodbc/*.deb 38 | 39 | # Set environment variables for ODBC configuration 40 | export ODBCINI=/etc/odbc.ini 41 | 42 | # Configure the Simba Spark ODBC driver in the odbcinst.ini 43 | echo "[Simba Spark ODBC Driver]" > /etc/odbcinst.ini 44 | echo "Description=Simba Spark ODBC Driver" >> /etc/odbcinst.ini 45 | echo "Driver=/opt/simba/spark/lib/64/libsparkodbc_sb64.so" >> /etc/odbcinst.ini 46 | echo "UsageCount=1" >> /etc/odbcinst.ini -------------------------------------------------------------------------------- /src/SemanticKernel.Agents.DatabaseAgent.MCPServer/DockerScripts/install/void.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Parameters for version and OS 4 | VERSION=${1:-2.9.1} 5 | OS=${2:-Debian-64bit} 6 | BUILD=${3:-1001} 7 | ARCH=${4:-amd64} 8 | -------------------------------------------------------------------------------- /src/SemanticKernel.Agents.DatabaseAgent.MCPServer/Dockerfile: -------------------------------------------------------------------------------- 1 | # See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging. 2 | 3 | # This stage is used when running from VS in fast mode (Default for Debug configuration) 4 | FROM mcr.microsoft.com/dotnet/aspnet:8.0-noble AS base 5 | WORKDIR /app 6 | 7 | # This stage is used to build the service project 8 | FROM mcr.microsoft.com/dotnet/sdk:8.0-alpine AS build 9 | ARG BUILD_CONFIGURATION=Release 10 | WORKDIR / 11 | COPY . . 12 | RUN dotnet restore "./src/SemanticKernel.Plugins.DatabaseAgent.sln" 13 | RUN dotnet build "./src/SemanticKernel.Plugins.DatabaseAgent.sln" -c $BUILD_CONFIGURATION -o /app/build 14 | 15 | # This stage is used to publish the service project to be copied to the final stage 16 | FROM build AS publish 17 | ARG BUILD_CONFIGURATION=Release 18 | WORKDIR ./src/SemanticKernel.Agents.DatabaseAgent.MCPServer 19 | RUN dotnet publish "./SemanticKernel.Agents.DatabaseAgent.MCPServer.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false 20 | 21 | # This stage is used in production or when running from VS in regular mode (Default when not using the Debug configuration) 22 | FROM base AS final 23 | 24 | ARG RUNTIME_SDK_INSTALLER=void 25 | ARG VERSION= 26 | ARG OS= 27 | ARG BUILD= 28 | 29 | # Install dependencies 30 | RUN apt-get update && \ 31 | apt-get install -y --no-install-recommends wget unzip dos2unix \ 32 | && rm -rf /var/lib/apt/lists/* 33 | 34 | COPY ./src/SemanticKernel.Agents.DatabaseAgent.MCPServer/DockerScripts/install/${RUNTIME_SDK_INSTALLER}.sh /tmp/ 35 | RUN chmod +x /tmp/${RUNTIME_SDK_INSTALLER}.sh && \ 36 | /tmp/${RUNTIME_SDK_INSTALLER}.sh ${VERSION} ${OS} ${BUILD} && \ 37 | rm -rf /tmp/${RUNTIME_SDK_INSTALLER}.sh 38 | 39 | COPY --from=publish /app/publish . 40 | 41 | USER $APP_UID 42 | WORKDIR /app 43 | ENTRYPOINT ["/usr/bin/dotnet", "SemanticKernel.Agents.DatabaseAgent.MCPServer.dll"] -------------------------------------------------------------------------------- /src/SemanticKernel.Agents.DatabaseAgent.MCPServer/DotnetTool.md: -------------------------------------------------------------------------------- 1 | # Database Agent MCP Server 2 | 3 | ## Install the MCP Server as a .NET Core Tool 4 | 5 | To install the MCP server as .NET Core Tool, you first should install the .NET Core SDK. 6 | You can download the .NET Core SDK from the following link: https://dotnet.microsoft.com/download 7 | 8 | After installing the .NET Core SDK, you can install the MCP server tool by running the following command: 9 | 10 | ```bash 11 | dotnet tool install --global SemanticKernel.Agents.DatabaseAgent.MCPServer 12 | ``` 13 | 14 | ## Usage 15 | 16 | To start the MCP server, you can run the following command: 17 | 18 | ```bash 19 | modelcontextprotocol-database-agent --*options* 20 | ``` 21 | 22 | ### Example 23 | 24 | Here is an example of how to start the MCP server with a SQLite database and Azure OpenAI services: 25 | 26 | ```bash 27 | modelcontextprotocol-database-agent \ 28 | --database:Provider=sqlite \ 29 | --database:ConnectionString="Data Source=northwind.db;Mode=ReadWrite" \ 30 | --memory:Kind=Volatile \ 31 | --kernel:Completion=gpt-4o-mini \ 32 | --kernel:Embedding=text-embedding-ada-002 \ 33 | --services:gpt-4o-mini:Type=AzureOpenAI \ 34 | --services:gpt-4o-mini:Endpoint=https://xxx.openai.azure.com/ \ 35 | --services:gpt-4o-mini:Auth=APIKey \ 36 | --services:gpt-4o-mini:APIKey=xxx \ 37 | --services:gpt-4o-mini:Deployment=gpt-4o-mini \ 38 | --services:text-embedding-ada-002:Type=AzureOpenAI \ 39 | --services:text-embedding-ada-002:Endpoint=https://xxx.openai.azure.com/ \ 40 | --services:text-embedding-ada-002:Auth=APIKey \ 41 | --services:text-embedding-ada-002:APIKey=xxx \ 42 | --services:text-embedding-ada-002:Deployment=text-embedding-ada-002 43 | ``` -------------------------------------------------------------------------------- /src/SemanticKernel.Agents.DatabaseAgent.MCPServer/Extensions/DatabaseKernelAgentExtension.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Configuration; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Microsoft.Extensions.Hosting; 4 | using Microsoft.Extensions.Logging; 5 | using Microsoft.Extensions.Logging.Abstractions; 6 | using Microsoft.SemanticKernel; 7 | using Microsoft.SemanticKernel.Agents; 8 | using Microsoft.SemanticKernel.ChatCompletion; 9 | using ModelContextProtocol.Server; 10 | using SemanticKernel.Agents.DatabaseAgent.MCPServer.Configuration; 11 | using System.Text.Json; 12 | using Microsoft.AspNetCore.Builder; 13 | using ModelContextProtocol.Protocol; 14 | 15 | namespace SemanticKernel.Agents.DatabaseAgent.MCPServer.Extensions 16 | { 17 | internal static class DatabaseKernelAgentExtension 18 | { 19 | public static IHost ToMcpServer(this DatabaseKernelAgent agent, IConfiguration configuration) 20 | { 21 | TransportSettings configuredTransport = new(); 22 | var transportConfiguration = configuration.GetSection("Agent:Transport"); 23 | 24 | if (transportConfiguration.Exists()) 25 | transportConfiguration.Bind(configuredTransport); 26 | 27 | switch (configuredTransport.Kind) 28 | { 29 | case TransportSettings.TransportType.Stdio: 30 | var builder = Host.CreateEmptyApplicationBuilder(settings: null); 31 | 32 | var mcpServerBuilder = builder.Services 33 | .AddMcpServer(options => BindMcpServerOptions(agent, options)); 34 | mcpServerBuilder.WithStdioServerTransport(); 35 | 36 | return builder.Build(); 37 | 38 | case TransportSettings.TransportType.Sse: 39 | var webAppOptions = new WebApplicationOptions(); 40 | configuration.Bind(webAppOptions); 41 | 42 | var webAppBuilder = WebApplication.CreateBuilder(webAppOptions); 43 | 44 | webAppBuilder.Logging 45 | .AddConsole(); 46 | 47 | webAppBuilder.Services.AddMcpServer((options) => BindMcpServerOptions(agent, options)); 48 | 49 | var app = webAppBuilder.Build(); 50 | 51 | app.MapMcp(); 52 | 53 | return app; 54 | default: 55 | throw new NotSupportedException($"Transport '{configuredTransport.Kind}' is not supported."); 56 | } 57 | } 58 | 59 | static void BindMcpServerOptions(DatabaseKernelAgent agent, McpServerOptions options) 60 | { 61 | options.ServerInfo = new() { Name = agent.Name!, Version = "1.0.0" }; 62 | options.Capabilities = new() 63 | { 64 | Tools = new() 65 | { 66 | ListToolsHandler = async (context, cancellationToken) => 67 | { 68 | return new ListToolsResult() 69 | { 70 | Tools = [ 71 | new Tool(){ 72 | Name = agent.Name!, 73 | Description = agent.Description, 74 | InputSchema = JsonSerializer.Deserialize(""" 75 | { 76 | "type": "object", 77 | "properties": { 78 | "message": { 79 | "type": "string", 80 | "description": "The user query in natural language." 81 | } 82 | }, 83 | "required": ["message"] 84 | } 85 | """), 86 | } 87 | ] 88 | 89 | }; 90 | }, 91 | CallToolHandler = async (context, cancellationToken) => 92 | { 93 | if (!string.Equals(agent.Name, context.Params?.Name)) 94 | { 95 | throw new InvalidOperationException($"Unknown tool: '{context.Params?.Name}'"); 96 | } 97 | 98 | if (context.Params?.Arguments?.TryGetValue("message", out var message) is not true) 99 | { 100 | throw new InvalidOperationException("Missing required argument 'message'"); 101 | } 102 | 103 | var responses = agent.InvokeAsync(new ChatMessageContent(AuthorRole.User, message.ToString()), thread: null) 104 | .ConfigureAwait(false); 105 | 106 | var callToolResponse = new CallToolResponse(); 107 | 108 | await foreach (var item in responses) 109 | { 110 | callToolResponse.Content.Add(new() 111 | { 112 | Type = "text", 113 | Text = item.Message.Content! 114 | }); 115 | } 116 | 117 | return callToolResponse; 118 | } 119 | } 120 | }; 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/SemanticKernel.Agents.DatabaseAgent.MCPServer/Extensions/IKernelBuilderExtension.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Configuration; 2 | using Microsoft.Extensions.Logging; 3 | using Microsoft.SemanticKernel; 4 | using SemanticKernel.Agents.DatabaseAgent.MCPServer.Configuration; 5 | 6 | namespace SemanticKernel.Agents.DatabaseAgent.MCPServer.Extensions 7 | { 8 | internal static class IKernelBuilderExtension 9 | { 10 | internal static IKernelBuilder AddCompletionServiceFromConfiguration(this IKernelBuilder builder, IConfiguration configuration, string serviceName, ILoggerFactory loggerFactory) 11 | { 12 | var logger = loggerFactory.CreateLogger(nameof(IKernelBuilderExtension)); 13 | 14 | logger.LogInformation("Adding completion service from configuration: {serviceName}", serviceName); 15 | 16 | var service = configuration.GetSection($"services:{serviceName}"); 17 | 18 | if (string.IsNullOrEmpty(service["Type"])) 19 | { 20 | logger.LogError("Service type is not specified for {serviceName}", serviceName); 21 | return builder; 22 | } 23 | 24 | switch (service["Type"]) 25 | { 26 | case "AzureOpenAI": 27 | var azureConfig = service.Get(); 28 | return builder.AddAzureOpenAIChatCompletion(azureConfig.Deployment, azureConfig.Endpoint, azureConfig.APIKey); 29 | #pragma warning disable SKEXP0070 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. 30 | case "Ollama": 31 | var ollamaConfig = service.Get(); 32 | return builder.AddOllamaChatCompletion(ollamaConfig.ModelId, new Uri(ollamaConfig.Endpoint), null); 33 | #pragma warning restore SKEXP0070 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. 34 | 35 | default: 36 | throw new ArgumentException($"Unknown service type: '{service["Type"]}' for {serviceName}"); 37 | } 38 | 39 | } 40 | #pragma warning disable SKEXP0010 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. 41 | #pragma warning disable SKEXP0070 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. 42 | 43 | internal static IKernelBuilder AddTextEmbeddingFromConfiguration(this IKernelBuilder builder, IConfiguration configuration, string serviceName, ILoggerFactory loggerFactory) 44 | { 45 | var logger = loggerFactory.CreateLogger(nameof(IKernelBuilderExtension)); 46 | 47 | logger.LogInformation("Adding text embedding service from configuration: {serviceName}", serviceName); 48 | 49 | var service = configuration.GetSection($"services:{serviceName}"); 50 | 51 | if (string.IsNullOrEmpty(service["Type"])) 52 | { 53 | logger.LogError("Service type is not specified for {serviceName}", serviceName); 54 | return builder; 55 | } 56 | 57 | switch (service["Type"]) 58 | { 59 | case "AzureOpenAI": 60 | var azureConfig = service.Get(); 61 | 62 | return builder.AddAzureOpenAITextEmbeddingGeneration(azureConfig.Deployment, azureConfig.Endpoint, azureConfig.APIKey); 63 | case "Ollama": 64 | var ollamaConfig = service.Get(); 65 | return builder.AddOllamaTextEmbeddingGeneration(ollamaConfig.ModelId, new Uri(ollamaConfig.Endpoint), null); 66 | default: 67 | throw new ArgumentException("Unknown service type"); 68 | } 69 | } 70 | #pragma warning restore SKEXP0070 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. 71 | #pragma warning restore SKEXP0010 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. 72 | 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/SemanticKernel.Agents.DatabaseAgent.MCPServer/Internals/AgentKernelFactory.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Configuration; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Microsoft.Extensions.Logging; 4 | using Microsoft.Extensions.VectorData; 5 | using Microsoft.SemanticKernel; 6 | using Microsoft.SemanticKernel.Connectors.InMemory; 7 | using Microsoft.SemanticKernel.Connectors.Qdrant; 8 | using Microsoft.SemanticKernel.Connectors.SqliteVec; 9 | using Qdrant.Client; 10 | using SemanticKernel.Agents.DatabaseAgent.Internals; 11 | using SemanticKernel.Agents.DatabaseAgent.MCPServer.Configuration; 12 | using SemanticKernel.Agents.DatabaseAgent.MCPServer.Extensions; 13 | 14 | namespace SemanticKernel.Agents.DatabaseAgent.MCPServer.Internals; 15 | 16 | internal static class AgentKernelFactory 17 | { 18 | private static VectorStoreCollectionDefinition GetVectorStoreRecordDefinition(int vectorDimensions = 1536) 19 | { 20 | return new() 21 | { 22 | Properties = new List 23 | { 24 | new VectorStoreDataProperty(nameof(TableDefinitionSnippet.TableName), typeof(string)){ IsIndexed = true }, 25 | new VectorStoreKeyProperty(nameof(TableDefinitionSnippet.Key), typeof(Guid)), 26 | new VectorStoreDataProperty(nameof(TableDefinitionSnippet.Definition), typeof(string)), 27 | new VectorStoreDataProperty(nameof(TableDefinitionSnippet.Description), typeof(string)){ IsFullTextIndexed = true }, 28 | new VectorStoreVectorProperty(nameof(TableDefinitionSnippet.TextEmbedding), typeof(ReadOnlyMemory), dimensions: vectorDimensions) 29 | } 30 | }; 31 | } 32 | 33 | private static VectorStoreCollectionDefinition GetAgentStoreRecordDefinition(int vectorDimensions = 1536) 34 | { 35 | return new() 36 | { 37 | Properties = new List 38 | { 39 | new VectorStoreDataProperty(nameof(AgentDefinitionSnippet.AgentName), typeof(string)){ IsIndexed = true }, 40 | new VectorStoreKeyProperty(nameof(AgentDefinitionSnippet.Key), typeof(Guid)), 41 | new VectorStoreDataProperty(nameof(AgentDefinitionSnippet.Description), typeof(string)){ IsFullTextIndexed = true }, 42 | new VectorStoreDataProperty(nameof(AgentDefinitionSnippet.Instructions), typeof(string)), 43 | new VectorStoreVectorProperty(nameof(AgentDefinitionSnippet.TextEmbedding), typeof(ReadOnlyMemory), dimensions: vectorDimensions) 44 | } 45 | }; 46 | } 47 | 48 | internal static Kernel ConfigureKernel(IConfiguration configuration, ILoggerFactory loggerFactory) 49 | { 50 | var kernelSettings = configuration.GetSection("kernel").Get()!; 51 | var databaseSettings = configuration.GetSection("database").Get()!; 52 | var memorySection = configuration.GetSection("memory"); 53 | 54 | var kernelBuilder = Kernel.CreateBuilder(); 55 | 56 | kernelBuilder.Services 57 | .UseDatabaseAgentQualityAssurance(opts => 58 | { 59 | configuration.GetSection("agent:qualityAssurance").Bind(opts); 60 | }); 61 | 62 | kernelBuilder.Services.AddScoped(sp => DbConnectionFactory.CreateDbConnection(databaseSettings.ConnectionString, databaseSettings.Provider)); 63 | 64 | kernelBuilder.Services.Configure(options => 65 | { 66 | configuration.GetSection("memory") 67 | .Bind(options); 68 | }); 69 | 70 | var memorySettings = memorySection.Get(); 71 | 72 | loggerFactory.CreateLogger(nameof(AgentKernelFactory)) 73 | .LogInformation("Using memory kind {kind}", memorySettings!.Kind); 74 | 75 | switch (memorySettings.Kind) 76 | { 77 | case MemorySettings.StorageType.Volatile: 78 | kernelBuilder.Services.AddInMemoryVectorStoreRecordCollection("agent", options: new() 79 | { 80 | Definition = GetAgentStoreRecordDefinition() 81 | }); 82 | kernelBuilder.Services.AddInMemoryVectorStoreRecordCollection("tables", options: new() 83 | { 84 | Definition = GetVectorStoreRecordDefinition() 85 | }); 86 | break; 87 | case MemorySettings.StorageType.SQLite: 88 | var sqliteSettings = memorySection.Get()!; 89 | kernelBuilder.Services.AddSqliteCollection("agent", 90 | sqliteSettings.ConnectionString, 91 | options: new SqliteCollectionOptions() 92 | { 93 | Definition = GetAgentStoreRecordDefinition(sqliteSettings.Dimensions) 94 | }); 95 | kernelBuilder.Services.AddSqliteCollection("tables", 96 | sqliteSettings.ConnectionString, 97 | options: new SqliteCollectionOptions() 98 | { 99 | Definition = GetVectorStoreRecordDefinition(sqliteSettings.Dimensions) 100 | }); 101 | break; 102 | case MemorySettings.StorageType.Qdrant: 103 | var qdrantSettings = memorySection.Get()!; 104 | 105 | kernelBuilder.Services.AddQdrantCollection("agent", 106 | qdrantSettings.Host, 107 | qdrantSettings.Port, 108 | qdrantSettings.Https, 109 | qdrantSettings.APIKey, 110 | new QdrantCollectionOptions 111 | { 112 | Definition = GetAgentStoreRecordDefinition(qdrantSettings.Dimensions) 113 | }); 114 | kernelBuilder.Services.AddQdrantCollection("tables", 115 | qdrantSettings.Host, 116 | qdrantSettings.Port, 117 | qdrantSettings.Https, 118 | qdrantSettings.APIKey, 119 | new QdrantCollectionOptions 120 | { 121 | Definition = GetVectorStoreRecordDefinition(qdrantSettings.Dimensions) 122 | }); 123 | break; 124 | default: throw new ArgumentException($"Unknown storage type '{memorySection.Get()!.Kind}'"); 125 | } 126 | 127 | _ = kernelBuilder.Services.AddSingleton(); 128 | 129 | 130 | return kernelBuilder 131 | .AddTextEmbeddingFromConfiguration(configuration, kernelSettings.Embedding, loggerFactory) 132 | .AddCompletionServiceFromConfiguration(configuration, kernelSettings.Completion, loggerFactory) 133 | .Build(); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/SemanticKernel.Agents.DatabaseAgent.MCPServer/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Configuration; 2 | using Microsoft.Extensions.Hosting; 3 | using Microsoft.Extensions.Logging; 4 | using Microsoft.Extensions.Logging.Abstractions; 5 | using SemanticKernel.Agents.DatabaseAgent.MCPServer.Extensions; 6 | using SemanticKernel.Agents.DatabaseAgent.MCPServer.Internals; 7 | using ZstdSharp.Unsafe; 8 | 9 | namespace SemanticKernel.Agents.DatabaseAgent.MCPServer; 10 | 11 | internal class Program 12 | { 13 | static async Task Main(string[] args) 14 | { 15 | IConfiguration configuration = new ConfigurationBuilder() 16 | .AddEnvironmentVariables() 17 | .AddCommandLine(args) 18 | .Build(); 19 | 20 | var loggerFactory = LoggerFactory.Create(options => 21 | { 22 | options.AddDebug(); 23 | options.SetMinimumLevel(LogLevel.Debug); 24 | }); 25 | 26 | var kernel = AgentKernelFactory.ConfigureKernel(configuration, loggerFactory); 27 | 28 | var agent = await DatabaseAgentFactory.CreateAgentAsync(kernel, loggerFactory); 29 | 30 | await agent.ToMcpServer(configuration) 31 | .RunAsync(); 32 | 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/SemanticKernel.Agents.DatabaseAgent.MCPServer/README.md: -------------------------------------------------------------------------------- 1 | # Database Agent MCP Server 2 | 3 | The Database Agent MCP Server is a server that listens for incoming connections from the Database Agent and processes the messages sent by the Database Agent. The Database Agent MCP Server is responsible for processing the messages sent by the Database Agent and executing the appropriate actions based on the message type. 4 | 5 | ## Installation 6 | 7 | There are two ways to install the Database Agent MCP Server: 8 | 9 | 1. **As a .NET Core Tool**: You can install the Database Agent MCP Server as a .NET Core tool. 10 | > See the [DotnetTool.md](DotnetTool.md) file for more information on how to install and use the Database Agent MCP Server as a .NET Core tool. 11 | 12 | 1. **As a Docker Image**: You can also run the Database Agent MCP Server as a Docker image and expose it SSE (Server-Sent Events) and HTTP endpoints. To do this, you can use the following command: 13 | > See the [Docker.md](Docker.md) file for more information on how to run the Database Agent MCP Server as a Docker image. 14 | 15 | 16 | ### Options 17 | 18 | All options are passed as command line argument or environment variables. The command line options take precedence over the environment variables. 19 | The following options are available 20 | 21 | #### Global options 22 | 23 | `--database:Provider` 24 | - **Description**: Specifies the database provider (e.g., SQLite, SQL Server, etc.). 25 | - **Type**: `string` 26 | - **Example**: `--database:Provider=sqlite` 27 | 28 | `--database:ConnectionString` 29 | - **Description**: The connection string for connecting to the database. 30 | - **Type**: `string` 31 | - **Example**: `--database:ConnectionString="Data Source=northwind.db;Mode=ReadWrite"` 32 | 33 | ## Agent transport 34 | 35 | You can configure the transport options for the agent by setting the following options: 36 | 37 | `--agent:Transport:Kind` 38 | - **Description**: Defines the kind of transport to be used for the agent (e.g., Stdio, Sse). 39 | - **Type**: `string` 40 | - **Default**: `Stdio` 41 | - **Example**: `--agent:Transport:Kind=Stdio` 42 | 43 | #### Supported database providers 44 | 45 | The following database providers are supported: 46 | 47 | - `sqlite`: SQLite database provider 48 | - `sqlserver`: SQL Server database provider 49 | - `mysql`: MySQL database provider 50 | - `postgresql`: PostgreSQL database provider 51 | - `oracle`: Oracle database provider 52 | - `oledb`: OLE DB database provider 53 | - `odbc`: ODBC database provider. (When using this provider, you need to ensure that the ODBC driver is installed and configured on your system. The connection string format may vary based on the ODBC driver you are using. Refer to the documentation of your specific ODBC driver for the correct connection string format.) 54 | 55 | #### Memory options 56 | 57 | Memory options are used to configure the memory settings for the kernel. 58 | As a default, the memory is set to `Volatile`, which means that the memory is not persisted and will be lost when the kernel is stopped. 59 | 60 | `--memory:Kind` 61 | - **Description**: Defines the kind of memory to be used for the kernel (e.g., Volatile). 62 | - **Type**: `string` 63 | - **Example**: `--memory:Kind=Volatile` 64 | 65 | `--memory:Dimensions` 66 | - **Description**: The number of dimensions for the memory vectors. This is only used when the memory kind is set to a persistent memory provider. 67 | - **Type**: `int` 68 | - **Example**: `--memory:Dimensions=1536` 69 | 70 | `--memory:TopK` 71 | - **Description**: The number of tables to return from the memory. 72 | - **Type**: `int` 73 | - **Example**: `--memory:TopK=5` 74 | 75 | `--memory:MaxTokens` 76 | - **Description**: The maximum number of tokens to be used for the sql query generation. 77 | - **Type**: `int` 78 | - **Example**: `--memory:MaxTokens=1000` 79 | 80 | `--memory:Temperature` 81 | - **Description**: The temperature to be used for the sql query generation. 82 | - **Type**: `float` 83 | - **Example**: `--memory:Temperature=0.5` 84 | 85 | `--memory:TopP` 86 | - **Description**: The top p to be used for the sql query generation. 87 | - **Type**: `float` 88 | - **Example**: `--memory:TopP=0.9` 89 | 90 | You can also set the memory to persist the data in a database. At the moment, the only supported database provider is `sqlite` and `Qdrant` but more providers will be added in the future. 91 | 92 | ##### SQLite options 93 | 94 | `--memory:ConnectionString` 95 | - **Description**: The connection string for connecting to the SQLite database. 96 | - **Type**: `string` 97 | - **Example**: `--memory:SQLite:ConnectionString="Data Source=northwind.db;Mode=ReadWrite"` 98 | 99 | ##### Qdrant options 100 | 101 | `--memory:Host` 102 | - **Description**: The host name or IP address of the Qdrant server. 103 | - **Type**: `string` 104 | - **Example**: `--memory:Host="localhost"` 105 | 106 | `--memory:Port` 107 | - **Description**: The port number of the Qdrant server. 108 | - **Type**: `int` 109 | - **Example**: `--memory:Port=6333` 110 | 111 | `--memory:Https` 112 | - **Description**: Specifies whether to use HTTPS for the connection. 113 | - **Type**: `bool` 114 | - **Default**: `false` 115 | - **Example**: `--memory:Https=true` 116 | 117 | `--memory:ApiKey` 118 | - **Description**: The API key for authenticating with the Qdrant server. 119 | - **Type**: `string` 120 | - **Example**: `--memory:ApiKey="xxx"` 121 | 122 | #### Kernel options 123 | 124 | . `--kernel:Completion` 125 | - **Description**: Defines the completion model used by the kernel and configured in the services section. 126 | - **Type**: `string` 127 | - **Example**: `--kernel:Completion=gpt-4o-mini` 128 | 129 | . `--kernel:Embedding` 130 | - **Description**: Specifies the embedding model for the kernel's embedding operations and configured in the services section. 131 | - **Type**: `string` 132 | - **Example**: `--kernel:Embedding=text-embedding-ada-002` 133 | 134 | #### Services options 135 | 136 | The services options are used to configure the services that are used by the kernel. At this time Azure Open AI and Ollama are supported as backend but more providers will be added in the future. 137 | 138 | `--services::Type` 139 | - **Description**: Specifies the type of service to be used (e.g., AzureOpenAI, Ollama). 140 | - **Type**: `string` 141 | - **Example**: `--services:gpt-4o-mini:Type=AzureOpenAI` 142 | 143 | ##### Azure Open AI 144 | 145 | `--services::Endpoint` 146 | - **Description**: The endpoint URL for the service. 147 | - **Type**: `string` 148 | - **Example**: `--services:gpt-4o-mini:Endpoint="https://xxx.openai.azure.com/"` 149 | 150 | `--services::Auth` 151 | - **Description**: The authentication method for the service (e.g., APIKey). 152 | - **Type**: `string` 153 | - **Example**: `--services:gpt-4o-mini:Auth=APIKey` 154 | 155 | `--services::APIKey` 156 | - **Description**: The API key for authenticating with the service. 157 | - **Type**: `string` 158 | - **Example**: `--services:gpt-4o-mini:APIKey="xxx"` 159 | 160 | `--services::Deployment` 161 | - **Description**: The deployment name for the service. 162 | - **Type**: `string` 163 | - **Example**: `--services:gpt-4o-mini:Deployment="gpt-4o-mini"` 164 | 165 | ##### Ollama 166 | 167 | `--services::ModelId` 168 | - **Description**: The model name for the Ollama service. 169 | - **Type**: `string` 170 | - **Example**: `--services:qwen2.5-coder:ModelId="qwen2.5-coder:latest"` 171 | 172 | `--services::Host` 173 | - **Description**: The host name or IP address of the Ollama server. 174 | - **Type**: `string` 175 | - **Example**: `--services:qwen2.5-coder:Endpoint="http://localhost:11434"` 176 | 177 | ## Quality assurance 178 | 179 | You can set the quality assurance settings by adding these specific configuration options: 180 | 181 | `--agent:QualityAssurance:EnableQueryRelevancyFilter` 182 | - **Description**: Enables or disables the query relevancy filter in the quality assurance process. 183 | - **Type**: `bool` 184 | - **Default**: `true` 185 | - **Example**: `--agent:QualityAssurance:EnableQueryRelevancyFilter=false` 186 | 187 | `--agent:QualityAssurance:QueryRelevancyThreshold` 188 | - **Description**: Enables or disables the query relevancy filter in the quality assurance process. 189 | - **Type**: `bool` 190 | - **Default**: `true` 191 | - **Example**: `--agent:QualityAssurance:QueryRelevancyThreshold=0.9` 192 | 193 | ## Contributing 194 | 195 | Contributions are welcome! For more information, please see the [CONTRIBUTING](../../CONTRIBUTING.md) file. 196 | 197 | ## License 198 | 199 | This project is licensed under the MIT License. See the [LICENSE](../../LICENSE.md) file for details. -------------------------------------------------------------------------------- /src/SemanticKernel.Agents.DatabaseAgent.MCPServer/SemanticKernel.Agents.DatabaseAgent.MCPServer.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | enable 6 | enable 7 | 8 | true 9 | modelcontextprotocol-database-agent 10 | ./nupkg 11 | net8.0 12 | 13 | 14 | 15 | 16 | 17 | Semantic Kernel Database agent MCP server 18 | README.md 19 | 20 | Microsoft's Semantic Kernel NL2SQL agent for databases. 21 | This agent can be used to generate SQL queries from natural language prompts. 22 | 23 | This is the MCP server tool for .NET core CLI. 24 | 25 | Linux 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | Always 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /src/SemanticKernel.Agents.DatabaseAgent.QualityAssurance/Evaluators/QueryRelevancyEvaluation.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace SemanticKernel.Agents.DatabaseAgent.QualityAssurance.Evaluators 4 | { 5 | internal class QueryRelevancyEvaluation 6 | { 7 | [JsonPropertyName("reasoning")] 8 | public string[] Reasoning { get; set; } = []; 9 | 10 | [JsonPropertyName("questions")] 11 | public string[] Questions { get; set; } = []; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/SemanticKernel.Agents.DatabaseAgent.QualityAssurance/Evaluators/QueryRelevancyEvaluator.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.AI; 2 | using Microsoft.SemanticKernel; 3 | using Microsoft.SemanticKernel.ChatCompletion; 4 | using Microsoft.SemanticKernel.Connectors.OpenAI; 5 | using Microsoft.SemanticKernel.Embeddings; 6 | using Microsoft.SemanticKernel.Services; 7 | using SemanticKernel.Agents.DatabaseAgent.Internals; 8 | using SemanticKernel.Agents.DatabaseAgent.QualityAssurance.Evaluators; 9 | using System.Numerics.Tensors; 10 | using System.Text.Json; 11 | 12 | namespace SemanticKernel.Agents.DatabaseAgent.QualityAssurance; 13 | 14 | #pragma warning disable SKEXP0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. 15 | 16 | public class QueryRelevancyEvaluator 17 | { 18 | private readonly Kernel kernel; 19 | 20 | private KernelFunction ExtractQuestion => KernelFunctionFactory.CreateFromPrompt(new EmbeddedPromptProvider().ReadPrompt("QuestionExtraction"), new OpenAIPromptExecutionSettings 21 | { 22 | Temperature = 9.99999993922529E-09, 23 | TopP = 9.99999993922529E-09, 24 | Seed = 0L, 25 | ResponseFormat = AIJsonUtilities.CreateJsonSchema(typeof(QueryRelevancyEvaluation)), 26 | }, "ExtractQuestion"); 27 | 28 | public QueryRelevancyEvaluator(Kernel kernel) 29 | { 30 | this.kernel = kernel; 31 | } 32 | 33 | public async Task EvaluateAsync(string prompt, string tableDefinitions, string query) 34 | { 35 | (var chatCompletionService, var executionSettings) = GetChatCompletionService(kernel, null); 36 | 37 | var result = await ExtractQuestion.InvokeAsync(kernel, new KernelArguments(executionSettings) 38 | { 39 | { "tablesDefinition", tableDefinitions }, 40 | { "query", query } 41 | }); 42 | 43 | var evaluation = JsonSerializer.Deserialize(result.GetValue()!)!; 44 | var embeddedQueries = new List 45 | { 46 | prompt 47 | }; 48 | 49 | embeddedQueries.AddRange(evaluation.Questions); 50 | 51 | IList> embeddings = await kernel.GetRequiredService() 52 | .GenerateEmbeddingsAsync(embeddedQueries, kernel) 53 | .ConfigureAwait(false); 54 | 55 | var promptEmbedding = embeddings.First(); 56 | 57 | return embeddings.Skip(1).Select(c => TensorPrimitives.CosineSimilarity(promptEmbedding.Span, c.Span)).Max(); 58 | } 59 | 60 | internal static (IChatCompletionService service, PromptExecutionSettings?) GetChatCompletionService(Kernel kernel, KernelArguments? arguments) 61 | { 62 | (IChatCompletionService chatCompletionService, PromptExecutionSettings? executionSettings) = 63 | kernel.ServiceSelector.SelectAIService( 64 | kernel, 65 | arguments?.ExecutionSettings, 66 | arguments ?? []); 67 | 68 | return (chatCompletionService, executionSettings); 69 | } 70 | } 71 | 72 | #pragma warning restore SKEXP0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. 73 | -------------------------------------------------------------------------------- /src/SemanticKernel.Agents.DatabaseAgent.QualityAssurance/Extensions/ServiceCollectionExtension.cs: -------------------------------------------------------------------------------- 1 | using SemanticKernel.Agents.DatabaseAgent.Filters; 2 | using SemanticKernel.Agents.DatabaseAgent.QualityAssurance.Filters; 3 | 4 | namespace Microsoft.Extensions.DependencyInjection; 5 | 6 | public static class ServiceCollectionExtension 7 | { 8 | public static IServiceCollection UseDatabaseAgentQualityAssurance( 9 | this IServiceCollection services, 10 | Action? opts = null) 11 | { 12 | if (opts is null) 13 | { 14 | services.Configure(_ => { }); 15 | } 16 | else 17 | { 18 | services.Configure(opts); 19 | } 20 | 21 | services.AddTransient(); 22 | 23 | return services; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/SemanticKernel.Agents.DatabaseAgent.QualityAssurance/Filters/QualityAssuranceFilterOptions.cs: -------------------------------------------------------------------------------- 1 | namespace SemanticKernel.Agents.DatabaseAgent.QualityAssurance.Filters; 2 | 3 | public class QualityAssuranceFilterOptions 4 | { 5 | public bool EnableQueryRelevancyFilter { get; set; } = true; 6 | 7 | public float QueryRelevancyThreshold { get; set; } = .9f; 8 | } 9 | -------------------------------------------------------------------------------- /src/SemanticKernel.Agents.DatabaseAgent.QualityAssurance/Filters/QueryRelevancyFilter.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using Microsoft.Extensions.Logging.Abstractions; 3 | using Microsoft.Extensions.Options; 4 | using SemanticKernel.Agents.DatabaseAgent.Filters; 5 | 6 | namespace SemanticKernel.Agents.DatabaseAgent.QualityAssurance.Filters; 7 | 8 | public class QueryRelevancyFilter : IQueryExecutionFilter 9 | { 10 | private ILogger Logger { get; } 11 | 12 | private QualityAssuranceFilterOptions Options { get; } 13 | 14 | public QueryRelevancyFilter(IOptions options, ILoggerFactory? loggerFactory = null) 15 | { 16 | Logger = (loggerFactory ?? NullLoggerFactory.Instance).CreateLogger(); 17 | Options = options.Value; 18 | } 19 | 20 | public async Task<(bool filtered, string message)> OnQueryExecutionAsync(QueryExecutionContext context, Func> next) 21 | { 22 | if (!this.Options.EnableQueryRelevancyFilter) 23 | { 24 | await next(context); 25 | return (false, string.Empty); 26 | } 27 | 28 | QueryRelevancyEvaluator evaluator = new QueryRelevancyEvaluator(context.Kernel); 29 | 30 | var relevancy = await evaluator.EvaluateAsync(context.Prompt, context.TableDefinitions, context.SQLQuery); 31 | 32 | if (relevancy < Options.QueryRelevancyThreshold) 33 | { 34 | Logger.LogWarning("Query relevancy is below threshold. Skipping query execution."); 35 | return (true, "Generated query doesn't seems relevant, try to modify you prompt to get a better query."); 36 | } 37 | 38 | return await next(context); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/SemanticKernel.Agents.DatabaseAgent.QualityAssurance/Prompts/QuestionExtraction.md: -------------------------------------------------------------------------------- 1 | Analyze the provided table definitions and SQL query to produce a natural language question that corresponds to the query's intent. 2 | 3 | ## Steps 4 | 5 | 1. **Analyze Table Definitions** 6 | - Review the structure, column names, and relationships between tables (if any). 7 | - Note the purpose of each column (e.g., key identifiers, textual fields, or numeric data). 8 | 9 | 2. **Examine the SQL Query** 10 | - Break down the query into components: 11 | - SELECT fields (what data is being retrieved). 12 | - Filtering (WHERE clauses, date ranges, or conditions). 13 | - Aggregations (e.g., COUNT, SUM, AVG). 14 | - Relationships or table joins, if applicable. 15 | 16 | 3. **Determine the Query's Intent** 17 | - Identify what the query aims to achieve based on its structure and components. 18 | - Pay attention to filtering, grouping, or ordering as they affect the final question. 19 | 20 | 4. **Formulate the Natural Language Question** 21 | - Use clear and concise language to explain the query's purpose. 22 | - Ensure details like filters or relationships are reflected in the formulated question to match the query's structure. 23 | 24 | ## Output Format 25 | 26 | Return a JSON object containing: 27 | - `reasoning`: A list of reasoning explanation of how the analysis was performed. 28 | - `questions`: A list of natural language questions variants summarizing the SQL query. 29 | 30 | ## Examples 31 | 32 | ### Example 1 33 | **Input:** 34 | - **Table Definitions:** 35 | ``` 36 | Students(id INT, name VARCHAR, age INT) 37 | Grades(student_id INT, course_id INT, grade CHAR) 38 | ``` 39 | - **SQL Query:** 40 | ```sql 41 | SELECT name FROM Students WHERE age > 18; 42 | ``` 43 | 44 | **Output:** 45 | ```json 46 | { 47 | "reasoning": [ 48 | "The query selects the 'name' column from the Students table and filters rows where the 'age' column has a value greater than 18.", 49 | "This implies the goal is to retrieve the names of students older than 18." 50 | ], 51 | "questions": [ 52 | "What are the names of students who are older than 18?", 53 | "Which students are above the age of 18?", 54 | "List the names of students aged over 18." 55 | ] 56 | } 57 | ``` 58 | 59 | ### Example 2 60 | **Input:** 61 | - **Table Definitions:** 62 | ``` 63 | Employees(id INT, name VARCHAR, department_id INT) 64 | Departments(department_id INT, department_name VARCHAR) 65 | ``` 66 | - **SQL Query:** 67 | ```sql 68 | SELECT department_name, COUNT(*) FROM Employees INNER JOIN Departments ON Employees.department_id = Departments.department_id GROUP BY department_name; 69 | ``` 70 | 71 | **Output:** 72 | ```json 73 | { 74 | "reasoning": [ 75 | "The query counts the number of employees grouped by department name.", 76 | "It joins the Employees and Departments tables based on the department_id column." 77 | ], 78 | "questions": [ 79 | "How many employees are there in each department?", 80 | "What is the count of employees per department?", 81 | "List the number of employees in each department." 82 | ] 83 | } 84 | ``` 85 | 86 | ### Example 3 87 | **Input:** 88 | - **Table Definitions:** 89 | ``` 90 | Orders(order_id INT, customer_id INT, order_date DATE, total_amount FLOAT) 91 | ``` 92 | - **SQL Query:** 93 | ```sql 94 | SELECT COUNT(*) FROM Orders WHERE order_date BETWEEN '2023-01-01' AND '2023-12-31'; 95 | ``` 96 | 97 | **Output:** 98 | ```json 99 | { 100 | "reasoning": [ 101 | "The query counts all rows in the Orders table that have an order_date between January 1, 2023, and December 31, 2023.", 102 | "This indicates the purpose is to determine the number of orders placed during 2023." 103 | ], 104 | "questions": [ 105 | "How many orders were placed in the year 2023?", 106 | "What is the total count of orders made in 2023?", 107 | "Count the number of orders within the year 2023." 108 | ] 109 | } 110 | ``` 111 | 112 | --- 113 | 114 | ## Notes 115 | 116 | 1. Ensure the question reflects query-specific details, such as filters and aggregations. 117 | 2. Use placeholders or realistic examples to aid clarity when constructing answers for variations on SQL queries. 118 | 3. Consider joins or relationships when explaining the connection between tables clearly in reasoning. 119 | 120 | --- 121 | 122 | ### Your Input 123 | **Table Definitions:** 124 | ``` 125 | {{$tablesDefinition}} 126 | ``` 127 | **SQL Query:** 128 | ```sql 129 | {{$query}} 130 | ``` 131 | 132 | Your response should include a JSON object like the examples above. -------------------------------------------------------------------------------- /src/SemanticKernel.Agents.DatabaseAgent.QualityAssurance/README.md: -------------------------------------------------------------------------------- 1 | # DBMS Agent Quality Assurance for Semantic Kernel 2 | 3 | ## Overview 4 | 5 | The Database Agent for Semantic Kernel is a service that provides a database management system (DBMS) for the Semantic Kernel (NL2SQL). The Agent is responsible for managing the storage and retrieval of data from the Semantic Kernel. 6 | This built on top of the [Microsoft's Semantic Kernel](https://github.com/microsoft/semantic-kernel) and leverages the [Microsoft's Kernel Memory](https://github.com/microsoft/kernel-memory) service to memorize database schema and relationships to provide a more efficient and accurate database management system. 7 | 8 | ## Getting Started 9 | 10 | Using LLM agents to write and execute its own queries into a database might lead to risks such as unintended data exposure, security vulnerabilities, and inefficient query execution, potentially compromising system integrity and compliance requirements. 11 | To mitigate these risks, the Database Agent for Semantic Kernel provides a set of quality assurance features to ensure the safety and reliability of the queries executed by the agent. 12 | 13 | ### Additional Configuration 14 | 15 | First, you must add the ``QualityAssurance`` package for DatabaseAgent to your project. 16 | 17 | ```bash 18 | dotnet add package SemanticKernel.Agents.DatabaseAgent.QualityAssurance 19 | ``` 20 | 21 | Next, you must configure the quality insurance settings for the Database Agent. 22 | ```csharp 23 | kernelBuilder.Services.UseDatabaseAgentQualityAssurance(opts => 24 | { 25 | opts.EnableQueryRelevancyFilter = true; 26 | opts.QueryRelevancyThreshold = .8f; 27 | }); 28 | ``` 29 | 30 | ### Quality Assurance Features 31 | 32 | The Database Agent for Semantic Kernel provides the following quality assurance features: 33 | `QueryRelevancyFilter`: Ensures that only relevant queries are executed by the agent. The filter uses LLM to generate the description of the query that is intended to be executed, then compute the cosine similarity between the user prompt and the generated description. If the similarity score is below the threshold, the query is rejected. 34 | 35 | ### Create a custom quality assurance filter 36 | 37 | You can create a custom quality assurance filter by implementing the `IQueryExecutionFilter` interface and registering it with the DI container. 38 | ```csharp 39 | kernelBuilder.Services.AddTransient(); 40 | 41 | public class CustomQueryExecutionFilter : IQueryExecutionFilter 42 | { 43 | public async Task OnQueryExecutionAsync(QueryExecutionContext context, Func next) 44 | { 45 | // Implement custom query execution logic 46 | return Task.FromResult(true); 47 | } 48 | } 49 | ``` 50 | 51 | ### Prerequisites 52 | 53 | - [.NET 8.0 SDK](https://dotnet.microsoft.com/download/dotnet/8.0) 54 | 55 | ## Contributing 56 | 57 | We welcome contributions to enhance this project. Please fork the repository and submit a pull request with your proposed changes. 58 | 59 | ## License 60 | 61 | This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details. 62 | 63 | ## Acknowledgments 64 | 65 | - [Microsoft's Kernel Memory](https://github.com/microsoft/kernel-memory) for providing the foundational AI service. 66 | - The open-source community for continuous support and contributions. 67 | -------------------------------------------------------------------------------- /src/SemanticKernel.Agents.DatabaseAgent.QualityAssurance/SemanticKernel.Agents.DatabaseAgent.QualityAssurance.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/SemanticKernel.Agents.DatabaseAgent/.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 | -------------------------------------------------------------------------------- /src/SemanticKernel.Agents.DatabaseAgent/AgentDefinitionSnippet.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.VectorData; 2 | 3 | namespace SemanticKernel.Agents.DatabaseAgent 4 | { 5 | public sealed class AgentDefinitionSnippet 6 | { 7 | public required string AgentName { get; set; } 8 | 9 | public required Guid Key { get; set; } 10 | 11 | public string Description { get; set; } 12 | 13 | public string Instructions { get; set; } 14 | 15 | public ReadOnlyMemory TextEmbedding { get; set; } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/SemanticKernel.Agents.DatabaseAgent/AgentDescriptionResponse.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace SemanticKernel.Agents.DatabaseAgent; 4 | 5 | internal sealed class AgentDescriptionResponse 6 | { 7 | [JsonPropertyName("description")] 8 | public string Description { get; set; } = string.Empty; 9 | } 10 | -------------------------------------------------------------------------------- /src/SemanticKernel.Agents.DatabaseAgent/AgentInstructionsResponse.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace SemanticKernel.Agents.DatabaseAgent; 4 | 5 | internal sealed class AgentInstructionsResponse 6 | { 7 | [JsonPropertyName("instructions")] 8 | public string Instructions { get; set; } 9 | } 10 | -------------------------------------------------------------------------------- /src/SemanticKernel.Agents.DatabaseAgent/AgentNameRespone.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace SemanticKernel.Agents.DatabaseAgent; 4 | 5 | internal sealed class AgentNameRespone 6 | { 7 | [JsonPropertyName("name")] 8 | public string Name { get; set; } 9 | } 10 | -------------------------------------------------------------------------------- /src/SemanticKernel.Agents.DatabaseAgent/AgentResponse.cs: -------------------------------------------------------------------------------- 1 |  2 | 3 | using System.Text.Json.Serialization; 4 | 5 | namespace SemanticKernel.Agents.DatabaseAgent 6 | { 7 | internal class AgentResponse 8 | { 9 | [JsonPropertyName("thinking")] 10 | public string Tinking { get; set; } = string.Empty; 11 | 12 | [JsonPropertyName("answer")] 13 | public string Answer { get; set; } = string.Empty; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/SemanticKernel.Agents.DatabaseAgent/AsemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | using System.Runtime.InteropServices; 3 | 4 | // In SDK-style projects such as this one, several assembly attributes that were historically 5 | // defined in this file are now automatically added during build and populated with 6 | // values defined in project properties. For details of which attributes are included 7 | // and how to customise this process see: https://aka.ms/assembly-info-properties 8 | 9 | 10 | // Setting ComVisible to false makes the types in this assembly not visible to COM 11 | // components. If you need to access a type in this assembly from COM, set the ComVisible 12 | // attribute to true on that type. 13 | 14 | [assembly: ComVisible(false)] 15 | 16 | // The following GUID is for the ID of the typelib if this project is exposed to COM. 17 | 18 | [assembly: Guid("2c8d417e-556b-4dd0-88bf-5ea65adc30f4")] 19 | [assembly: InternalsVisibleTo("SemanticKernel.Agents.DatabaseAgent.QualityAssurance")] 20 | [assembly: InternalsVisibleTo("SemanticKernel.Agents.DatabaseAgent.MCPServer")] 21 | -------------------------------------------------------------------------------- /src/SemanticKernel.Agents.DatabaseAgent/DatabaseAgent.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using Microsoft.SemanticKernel.ChatCompletion; 3 | using Microsoft.SemanticKernel.Connectors.OpenAI; 4 | using Microsoft.SemanticKernel.Services; 5 | using SemanticKernel.Agents.DatabaseAgent; 6 | using System.Diagnostics.CodeAnalysis; 7 | using System.Runtime.CompilerServices; 8 | using System.Text; 9 | using System.Text.Json; 10 | 11 | namespace Microsoft.SemanticKernel.Agents; 12 | 13 | public sealed class DatabaseKernelAgent : ChatHistoryAgent 14 | { 15 | /// 16 | /// Initializes a new instance of the class. 17 | /// 18 | internal DatabaseKernelAgent() 19 | { 20 | 21 | } 22 | 23 | /// 24 | /// Gets the role used for agent instructions. Defaults to "system". 25 | /// 26 | /// 27 | /// Certain versions of "O*" series (deep reasoning) models require the instructions 28 | /// to be provided as "developer" role. Other versions support neither role and 29 | /// an agent targeting such a model cannot provide instructions. Agent functionality 30 | /// will be dictated entirely by the provided plugins. 31 | /// 32 | public AuthorRole InstructionsRole { get; init; } = AuthorRole.System; 33 | 34 | /// 35 | /// Provides a name for the agent, even if it's the identifier. 36 | /// (since allows null) 37 | /// 38 | /// The target agent 39 | /// The agent name as a non-empty string 40 | public string GetName() => this.Name ?? this.Id; 41 | 42 | /// 43 | /// Provides the display name of the agent. 44 | /// 45 | /// The target agent 46 | /// 47 | /// Currently, it's intended for telemetry purposes only. 48 | /// 49 | public string GetDisplayName() => !string.IsNullOrWhiteSpace(this.Name) ? this.Name! : "UnnamedAgent"; 50 | 51 | /// 52 | public override async IAsyncEnumerable> InvokeAsync( 53 | ICollection messages, 54 | AgentThread? thread = null, 55 | AgentInvokeOptions? options = null, 56 | [EnumeratorCancellation] CancellationToken cancellationToken = default) 57 | { 58 | var chatHistoryAgentThread = await this.EnsureThreadExistsWithMessagesAsync( 59 | messages, 60 | thread, 61 | () => new ChatHistoryAgentThread(), 62 | cancellationToken).ConfigureAwait(false); 63 | 64 | // Invoke Chat Completion with the updated chat history. 65 | var chatHistory = new ChatHistory(); 66 | await foreach (var existingMessage in chatHistoryAgentThread.GetMessagesAsync(cancellationToken).ConfigureAwait(false)) 67 | { 68 | chatHistory.Add(existingMessage); 69 | } 70 | 71 | string agentName = this.GetDisplayName(); 72 | var invokeResults = this.InternalInvokeAsync( 73 | agentName, 74 | chatHistory, 75 | (m) => this.NotifyThreadOfNewMessage(chatHistoryAgentThread, m, cancellationToken), 76 | options?.KernelArguments, 77 | options?.Kernel, 78 | options?.AdditionalInstructions, 79 | cancellationToken); 80 | 81 | // Notify the thread of new messages and return them to the caller. 82 | await foreach (var result in invokeResults.ConfigureAwait(false)) 83 | { 84 | // Do not add function call related messages as they will already be included in the chat history. 85 | if (!result.Items.Any(i => i is FunctionCallContent || i is FunctionResultContent)) 86 | { 87 | await this.NotifyThreadOfNewMessage(chatHistoryAgentThread, result, cancellationToken).ConfigureAwait(false); 88 | } 89 | 90 | yield return new(result, chatHistoryAgentThread); 91 | } 92 | } 93 | 94 | /// 95 | [Obsolete("Use InvokeAsync with AgentThread instead.")] 96 | public override IAsyncEnumerable InvokeAsync( 97 | ChatHistory history, 98 | KernelArguments? arguments = null, 99 | Kernel? kernel = null, 100 | CancellationToken cancellationToken = default) 101 | { 102 | string agentName = this.GetDisplayName(); 103 | 104 | return this.InternalInvokeAsync(agentName, history, (m) => Task.CompletedTask, arguments, kernel, null, cancellationToken); 105 | } 106 | 107 | /// 108 | public override async IAsyncEnumerable> InvokeStreamingAsync( 109 | ICollection messages, 110 | AgentThread? thread = null, 111 | AgentInvokeOptions? options = null, 112 | [EnumeratorCancellation] CancellationToken cancellationToken = default) 113 | { 114 | var chatHistoryAgentThread = await this.EnsureThreadExistsWithMessagesAsync( 115 | messages, 116 | thread, 117 | () => new ChatHistoryAgentThread(), 118 | cancellationToken).ConfigureAwait(false); 119 | 120 | // Invoke Chat Completion with the updated chat history. 121 | var chatHistory = new ChatHistory(); 122 | await foreach (var existingMessage in chatHistoryAgentThread.GetMessagesAsync(cancellationToken).ConfigureAwait(false)) 123 | { 124 | chatHistory.Add(existingMessage); 125 | } 126 | 127 | string agentName = this.GetDisplayName(); 128 | var invokeResults = this.InternalInvokeStreamingAsync( 129 | agentName, 130 | chatHistory, 131 | (newMessage) => this.NotifyThreadOfNewMessage(chatHistoryAgentThread, newMessage, cancellationToken), 132 | options?.KernelArguments, 133 | options?.Kernel, 134 | options?.AdditionalInstructions, 135 | cancellationToken); 136 | 137 | await foreach (var result in invokeResults.ConfigureAwait(false)) 138 | { 139 | yield return new(result, chatHistoryAgentThread); 140 | } 141 | } 142 | 143 | /// 144 | [Obsolete("Use InvokeStreamingAsync with AgentThread instead.")] 145 | public override IAsyncEnumerable InvokeStreamingAsync( 146 | ChatHistory history, 147 | KernelArguments? arguments = null, 148 | Kernel? kernel = null, 149 | CancellationToken cancellationToken = default) 150 | { 151 | string agentName = this.GetDisplayName(); 152 | 153 | return this.InternalInvokeStreamingAsync( 154 | agentName, 155 | history, 156 | (newMessage) => Task.CompletedTask, 157 | arguments, 158 | kernel, 159 | null, 160 | cancellationToken); 161 | } 162 | 163 | /// 164 | [Experimental("SKEXP0110")] 165 | protected override Task RestoreChannelAsync(string channelState, CancellationToken cancellationToken) 166 | { 167 | throw new NotImplementedException(); 168 | } 169 | 170 | internal static (IChatCompletionService service, PromptExecutionSettings? executionSettings) GetChatCompletionService(Kernel kernel, KernelArguments? arguments) 171 | { 172 | (IChatCompletionService chatCompletionService, PromptExecutionSettings? executionSettings) = 173 | kernel.ServiceSelector.SelectAIService( 174 | kernel, 175 | arguments?.ExecutionSettings, 176 | arguments ?? []); 177 | 178 | executionSettings ??= new OpenAIPromptExecutionSettings 179 | { 180 | ResponseFormat = "json_object", 181 | }; 182 | 183 | return (chatCompletionService, executionSettings); 184 | } 185 | 186 | #region private 187 | 188 | private async Task SetupAgentChatHistoryAsync( 189 | IReadOnlyList history, 190 | KernelArguments? arguments, 191 | Kernel kernel, 192 | string? additionalInstructions, 193 | CancellationToken cancellationToken) 194 | { 195 | ChatHistory chat = new ChatHistory(); 196 | string? instructions = await this.RenderInstructionsAsync(kernel, arguments, cancellationToken).ConfigureAwait(false); 197 | 198 | if (!string.IsNullOrWhiteSpace(instructions)) 199 | { 200 | chat.Add(new ChatMessageContent(this.InstructionsRole, instructions) { AuthorName = this.Name }); 201 | } 202 | 203 | if (!string.IsNullOrWhiteSpace(additionalInstructions)) 204 | { 205 | chat.Add(new ChatMessageContent(AuthorRole.System, additionalInstructions) { AuthorName = this.Name }); 206 | } 207 | 208 | chat.Add(new ChatMessageContent(AuthorRole.System, $$$""" 209 | # Output Format 210 | 211 | The output should be a JSON object structured as follows: 212 | 213 | ```json 214 | { 215 | "thinking": "Your step-by-step thought process and reasoning based on the query result.", 216 | "answer": "Your final answer in natural language, addressing the question explicitly." 217 | } 218 | ``` 219 | 220 | # Examples 221 | 222 | **Example 1** 223 | 224 | _Query_: What is the capital of France? 225 | _Query Result_: 226 | | Country | Capital | 227 | |---------------|-------------| 228 | | France | Paris | 229 | | Germany | Berlin | 230 | | Spain | Madrid | 231 | | Italy | Rome | 232 | | Portugal | Lisbon | 233 | | Netherlands | Amsterdam | 234 | | Belgium | Brussels | 235 | | Switzerland | Bern | 236 | | Austria | Vienna | 237 | 238 | 239 | _Output_: 240 | ```json 241 | { 242 | "thinking": "The query result indicates that the capital of France is listed as 'Paris.' Therefore, based on this data, the capital of France is Paris.", 243 | "answer": "The capital of France is Paris." 244 | } 245 | ``` 246 | 247 | **Example 2** 248 | 249 | _Query_: What is the population of New York City? 250 | _Query Result_: 251 | | City | Population | 252 | |----------------|------------| 253 | | New York City | 8,419,600 | 254 | | Los Angeles | 3,979,576 | 255 | | Chicago | 2,693,976 | 256 | | Houston | 2,303,482 | 257 | | Phoenix | 1,563,025 | 258 | 259 | 260 | _Output_: 261 | ```json 262 | { 263 | "thinking": "The query result specifies that the population of New York City is 8,419,600. This directly answers the question about its population.", 264 | "answer": "The population of New York City is 8,419,600." 265 | } 266 | ``` 267 | """) { AuthorName = this.Name }); 268 | 269 | chat.AddRange(history); 270 | 271 | return chat; 272 | } 273 | 274 | private async IAsyncEnumerable InternalInvokeAsync( 275 | string agentName, 276 | ChatHistory history, 277 | Func onNewToolMessage, 278 | KernelArguments? arguments = null, 279 | Kernel? kernel = null, 280 | string? additionalInstructions = null, 281 | [EnumeratorCancellation] CancellationToken cancellationToken = default) 282 | { 283 | kernel ??= this.Kernel; 284 | 285 | var plugin = kernel.CreatePluginFromType(); 286 | 287 | var data = await plugin["ExecuteQuery"].InvokeAsync(kernel, new KernelArguments 288 | { 289 | {"prompt", history.Last(c => c.Role == AuthorRole.User).Content! } 290 | }, cancellationToken: cancellationToken) 291 | .ConfigureAwait(false); 292 | 293 | history.Insert(0, new ChatMessageContent(AuthorRole.System, "Here are the results from the database:") { AuthorName = this.Name }); 294 | history.Insert(1, new ChatMessageContent(AuthorRole.System, data.GetValue()) { AuthorName = this.Name }); 295 | 296 | (IChatCompletionService chatCompletionService, PromptExecutionSettings? executionSettings) = GetChatCompletionService(kernel, arguments); 297 | 298 | ChatHistory chat = await this.SetupAgentChatHistoryAsync(history, arguments, kernel, additionalInstructions, cancellationToken).ConfigureAwait(false); 299 | 300 | int messageCount = chat.Count; 301 | 302 | Type serviceType = chatCompletionService.GetType(); 303 | 304 | IReadOnlyList messages = 305 | await chatCompletionService.GetChatMessageContentsAsync( 306 | chat, 307 | executionSettings, 308 | kernel, 309 | cancellationToken).ConfigureAwait(false); 310 | 311 | // Capture mutated messages related function calling / tools 312 | for (int messageIndex = messageCount; messageIndex < chat.Count; messageIndex++) 313 | { 314 | ChatMessageContent message = chat[messageIndex]; 315 | 316 | message.AuthorName = this.Name; 317 | 318 | history.Add(message); 319 | await onNewToolMessage(message).ConfigureAwait(false); 320 | } 321 | 322 | foreach (ChatMessageContent message in messages) 323 | { 324 | message.AuthorName = this.Name; 325 | 326 | try 327 | { 328 | message.Content = JsonSerializer.Deserialize(message.Content!)!.Answer; 329 | } 330 | catch (JsonException ex) 331 | { 332 | Logger.LogWarning(ex, "Failed to deserialize agent response content to AgentResponse. Content: {Content}", message.Content); 333 | } 334 | 335 | yield return message; 336 | } 337 | } 338 | 339 | private async IAsyncEnumerable InternalInvokeStreamingAsync( 340 | string agentName, 341 | ChatHistory history, 342 | Func onNewMessage, 343 | KernelArguments? arguments = null, 344 | Kernel? kernel = null, 345 | string? additionalInstructions = null, 346 | [EnumeratorCancellation] CancellationToken cancellationToken = default) 347 | { 348 | kernel ??= this.Kernel; 349 | 350 | (IChatCompletionService chatCompletionService, PromptExecutionSettings? executionSettings) = GetChatCompletionService(kernel, arguments); 351 | 352 | ChatHistory chat = await this.SetupAgentChatHistoryAsync(history, arguments, kernel, additionalInstructions, cancellationToken).ConfigureAwait(false); 353 | 354 | int messageCount = chat.Count; 355 | 356 | Type serviceType = chatCompletionService.GetType(); 357 | 358 | IAsyncEnumerable messages = 359 | chatCompletionService.GetStreamingChatMessageContentsAsync( 360 | chat, 361 | executionSettings, 362 | kernel, 363 | cancellationToken); 364 | 365 | AuthorRole? role = null; 366 | StringBuilder builder = new(); 367 | await foreach (StreamingChatMessageContent message in messages.ConfigureAwait(false)) 368 | { 369 | role = message.Role; 370 | message.Role ??= AuthorRole.Assistant; 371 | message.AuthorName = this.Name; 372 | 373 | builder.Append(message.ToString()); 374 | 375 | yield return message; 376 | } 377 | 378 | // Capture mutated messages related function calling / tools 379 | for (int messageIndex = messageCount; messageIndex < chat.Count; messageIndex++) 380 | { 381 | ChatMessageContent message = chat[messageIndex]; 382 | 383 | message.AuthorName = this.Name; 384 | 385 | await onNewMessage(message).ConfigureAwait(false); 386 | history.Add(message); 387 | } 388 | 389 | // Do not duplicate terminated function result to history 390 | if (role != AuthorRole.Tool) 391 | { 392 | await onNewMessage(new(role ?? AuthorRole.Assistant, builder.ToString()) { AuthorName = this.Name }).ConfigureAwait(false); 393 | history.Add(new(role ?? AuthorRole.Assistant, builder.ToString()) { AuthorName = this.Name }); 394 | } 395 | } 396 | 397 | #endregion 398 | } 399 | -------------------------------------------------------------------------------- /src/SemanticKernel.Agents.DatabaseAgent/DatabaseAgentFactory.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.AI; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Microsoft.Extensions.Logging; 4 | using Microsoft.Extensions.Logging.Abstractions; 5 | using Microsoft.Extensions.VectorData; 6 | using Microsoft.SemanticKernel; 7 | using Microsoft.SemanticKernel.Agents; 8 | using Microsoft.SemanticKernel.Connectors.OpenAI; 9 | using Microsoft.SemanticKernel.Embeddings; 10 | using Microsoft.SemanticKernel.PromptTemplates.Handlebars; 11 | using SemanticKernel.Agents.DatabaseAgent.Extensions; 12 | using SemanticKernel.Agents.DatabaseAgent.Internals; 13 | using System.Data; 14 | using System.Data.Common; 15 | using System.Runtime.CompilerServices; 16 | using System.Text; 17 | using System.Text.Json; 18 | 19 | namespace SemanticKernel.Agents.DatabaseAgent; 20 | 21 | public static class DatabaseAgentFactory 22 | { 23 | private static PromptExecutionSettings GetPromptExecutionSettings() => new OpenAIPromptExecutionSettings 24 | { 25 | MaxTokens = 4096, 26 | Temperature = .1E-9, 27 | TopP = .1E-9, 28 | Seed = 0L, 29 | ResponseFormat = AIJsonUtilities.CreateJsonSchema(typeof(T)), 30 | }; 31 | 32 | public static async Task CreateAgentAsync( 33 | Kernel kernel, 34 | ILoggerFactory loggingFactory, 35 | CancellationToken? cancellationToken = null) 36 | { 37 | var vectorStore = kernel.Services.GetService>(); 38 | 39 | if (vectorStore is null) 40 | { 41 | throw new InvalidOperationException("The kernel does not have a vector store for table definitions."); 42 | } 43 | 44 | var agentStore = kernel.Services.GetService>(); 45 | 46 | if (agentStore is null) 47 | { 48 | throw new InvalidOperationException("The kernel does not have a vector store for agent."); 49 | } 50 | 51 | await vectorStore.EnsureCollectionExistsAsync() 52 | .ConfigureAwait(false); 53 | 54 | await agentStore.EnsureCollectionExistsAsync() 55 | .ConfigureAwait(false); 56 | 57 | return await BuildAgentAsync(kernel, loggingFactory, cancellationToken ?? CancellationToken.None) 58 | .ConfigureAwait(false); 59 | } 60 | 61 | private static async Task BuildAgentAsync(Kernel kernel, ILoggerFactory loggingFactory, CancellationToken cancellationToken) 62 | { 63 | var agentKernel = kernel.Clone(); 64 | 65 | var existingDefinition = await kernel.GetRequiredService>() 66 | .GetAsync(Guid.Empty) 67 | .ConfigureAwait(false); 68 | 69 | loggingFactory.CreateLogger() 70 | .LogInformation("Agent definition: {Definition}", existingDefinition); 71 | 72 | if (existingDefinition is not null) 73 | { 74 | return new DatabaseKernelAgent 75 | { 76 | Kernel = agentKernel, 77 | Name = existingDefinition.AgentName, 78 | Description = existingDefinition.Description, 79 | Instructions = existingDefinition.Instructions 80 | }; 81 | } 82 | 83 | loggingFactory.CreateLogger() 84 | .LogInformation("Creating a new agent definition."); 85 | 86 | var tableDescriptions = await MemorizeAgentSchema(kernel, loggingFactory, cancellationToken); 87 | 88 | var promptProvider = kernel.GetRequiredService() ?? new EmbeddedPromptProvider(); 89 | 90 | var agentDescription = await KernelFunctionFactory.CreateFromPrompt(promptProvider.ReadPrompt(AgentPromptConstants.AgentDescriptionGenerator), GetPromptExecutionSettings(), functionName: AgentPromptConstants.AgentDescriptionGenerator) 91 | .InvokeAsync(kernel, new KernelArguments 92 | { 93 | { "tableDefinitions", tableDescriptions } 94 | }) 95 | .ConfigureAwait(false); 96 | 97 | loggingFactory.CreateLogger() 98 | .LogInformation("Agent description: {Description}", agentDescription.GetValue()!); 99 | 100 | var agentName = await KernelFunctionFactory.CreateFromPrompt(promptProvider.ReadPrompt(AgentPromptConstants.AgentNameGenerator), GetPromptExecutionSettings(), functionName: AgentPromptConstants.AgentNameGenerator) 101 | .InvokeAsync(kernel, new KernelArguments 102 | { 103 | { "agentDescription", agentDescription.GetValue()! } 104 | }) 105 | .ConfigureAwait(false); 106 | 107 | loggingFactory.CreateLogger() 108 | .LogInformation("Agent name: {Name}", agentName.GetValue()!); 109 | 110 | var agentInstructions = await KernelFunctionFactory.CreateFromPrompt(promptProvider.ReadPrompt(AgentPromptConstants.AgentInstructionsGenerator), GetPromptExecutionSettings(), functionName: AgentPromptConstants.AgentInstructionsGenerator) 111 | .InvokeAsync(kernel, new KernelArguments 112 | { 113 | { "agentDescription", agentDescription.GetValue()! } 114 | }) 115 | .ConfigureAwait(false); 116 | 117 | loggingFactory.CreateLogger() 118 | .LogInformation("Agent instructions: {Instructions}", agentInstructions.GetValue()!); 119 | 120 | var agentDefinition = new AgentDefinitionSnippet 121 | { 122 | Key = Guid.Empty, 123 | AgentName = JsonSerializer.Deserialize(agentName.GetValue()!)!.Name, 124 | Description = JsonSerializer.Deserialize(agentDescription.GetValue()!)!.Description, 125 | Instructions = JsonSerializer.Deserialize(agentInstructions.GetValue()!)!.Instructions 126 | }; 127 | 128 | agentDefinition.TextEmbedding = await kernel.GetRequiredService() 129 | .GenerateEmbeddingAsync(agentDefinition.Description) 130 | .ConfigureAwait(false); 131 | 132 | await kernel.GetRequiredService>() 133 | .UpsertAsync(agentDefinition, cancellationToken) 134 | .ConfigureAwait(false); 135 | 136 | return new DatabaseKernelAgent() 137 | { 138 | Kernel = agentKernel, 139 | Name = agentDefinition.AgentName, 140 | Description = agentDefinition.Description, 141 | Instructions = agentDefinition.Instructions 142 | }; 143 | } 144 | 145 | private static async Task MemorizeAgentSchema(Kernel kernel, ILoggerFactory loggerFactory, CancellationToken cancellationToken) 146 | { 147 | var stringBuilder = new StringBuilder(); 148 | 149 | var descriptions = GetTablesDescription(kernel, GetTablesAsync(kernel, cancellationToken), loggerFactory, cancellationToken) 150 | .ConfigureAwait(false); 151 | 152 | var embeddingTextGenerator = kernel.GetRequiredService(); 153 | 154 | await foreach (var (tableName, definition, description, dataSample) in descriptions) 155 | { 156 | using var stream = new MemoryStream(Encoding.UTF8.GetBytes(description)); 157 | 158 | await kernel.GetRequiredService>() 159 | .UpsertAsync(new TableDefinitionSnippet 160 | { 161 | Key = Guid.NewGuid(), 162 | TableName = tableName, 163 | Definition = definition, 164 | Description = description, 165 | SampleData = dataSample, 166 | TextEmbedding = await embeddingTextGenerator.GenerateEmbeddingAsync(description, cancellationToken: cancellationToken) 167 | .ConfigureAwait(false) 168 | }) 169 | .ConfigureAwait(false); 170 | 171 | stringBuilder.AppendLine(description); 172 | } 173 | 174 | return stringBuilder.ToString(); 175 | } 176 | 177 | private static async IAsyncEnumerable GetTablesAsync(Kernel kernel, [EnumeratorCancellation] CancellationToken cancellationToken) 178 | { 179 | var connection = kernel.GetRequiredService(); 180 | var promptProvider = kernel.GetRequiredService() ?? new EmbeddedPromptProvider(); 181 | var sqlWriter = KernelFunctionFactory.CreateFromPrompt( 182 | executionSettings: GetPromptExecutionSettings(), 183 | templateFormat: "handlebars", 184 | promptTemplate: promptProvider.ReadPrompt(AgentPromptConstants.WriteSQLQuery), 185 | promptTemplateFactory: new HandlebarsPromptTemplateFactory()); 186 | 187 | var defaultKernelArguments = new KernelArguments 188 | { 189 | { "providerName", connection.GetProviderName() }, 190 | { "tablesDefinitions", "" } 191 | }; 192 | 193 | string previousSQLQuery = null!; 194 | using var reader = await RetryHelper.Try(async (e) => 195 | { 196 | var tablesGenerator = await sqlWriter.InvokeAsync(kernel, new KernelArguments(defaultKernelArguments) 197 | { 198 | { "prompt", "List all tables" }, 199 | { "previousAttempt", previousSQLQuery }, 200 | { "previousException", e }, 201 | }) 202 | .ConfigureAwait(false); 203 | 204 | var response = JsonSerializer.Deserialize(tablesGenerator.GetValue()!)!; 205 | 206 | previousSQLQuery = response.Query; 207 | 208 | return await QueryExecutor.ExecuteSQLAsync(connection, previousSQLQuery, null, cancellationToken) 209 | .ConfigureAwait(false); 210 | }, cancellationToken: cancellationToken) 211 | .ConfigureAwait(false); 212 | 213 | foreach (DataRow row in reader!.Rows) 214 | { 215 | yield return MarkdownRenderer.Render(row); 216 | } 217 | } 218 | 219 | private static async IAsyncEnumerable<(string tableName, string tableDefinition, string tableDescription, string dataSample)> GetTablesDescription(Kernel kernel, IAsyncEnumerable tables, ILoggerFactory loggerFactory, [EnumeratorCancellation] CancellationToken cancellationToken) 220 | { 221 | var connection = kernel.GetRequiredService(); 222 | var promptProvider = kernel.GetRequiredService() ?? new EmbeddedPromptProvider(); 223 | var sqlWriter = KernelFunctionFactory.CreateFromPrompt( 224 | executionSettings: GetPromptExecutionSettings(), 225 | templateFormat: "handlebars", 226 | promptTemplate: promptProvider.ReadPrompt(AgentPromptConstants.WriteSQLQuery), 227 | promptTemplateFactory: new HandlebarsPromptTemplateFactory()); 228 | var extractTableName = KernelFunctionFactory.CreateFromPrompt(promptProvider.ReadPrompt(AgentPromptConstants.ExtractTableName), GetPromptExecutionSettings(), functionName: AgentPromptConstants.ExtractTableName); 229 | var tableDescriptionGenerator = KernelFunctionFactory.CreateFromPrompt(promptProvider.ReadPrompt(AgentPromptConstants.ExplainTable), GetPromptExecutionSettings(), functionName: AgentPromptConstants.ExplainTable); 230 | var defaultKernelArguments = new KernelArguments 231 | { 232 | { "providerName", connection.GetProviderName() } 233 | }; 234 | 235 | StringBuilder sb = new StringBuilder(); 236 | 237 | var logger = loggerFactory?.CreateLogger(nameof(DatabaseAgentFactory)) ?? NullLoggerFactory.Instance.CreateLogger(nameof(DatabaseAgentFactory)); 238 | 239 | await foreach (var item in tables) 240 | { 241 | logger.LogDebug("Processing table: {Table}", item); 242 | 243 | var tableName = await RetryHelper.Try(async (e) => 244 | { 245 | var tableNameResponse = await extractTableName.InvokeAsync(kernel, new KernelArguments(defaultKernelArguments) 246 | { 247 | { "item", item } 248 | }, cancellationToken: cancellationToken) 249 | .ConfigureAwait(false); 250 | 251 | return JsonSerializer.Deserialize(tableNameResponse.GetValue()!)!.TableName; 252 | }, loggerFactory: loggerFactory!, cancellationToken: cancellationToken) 253 | .ConfigureAwait(false); 254 | 255 | logger.LogDebug("Extracted table name: {TableName}", tableName); 256 | 257 | var existingRecordSearch = kernel.GetRequiredService>() 258 | .SearchAsync(await kernel.GetRequiredService() 259 | .GenerateEmbeddingAsync(item) 260 | .ConfigureAwait(false), top: 1) 261 | .ConfigureAwait(false); 262 | 263 | VectorSearchResult existingRecord = null!; 264 | 265 | await foreach (var searchResult in existingRecordSearch) 266 | { 267 | if (searchResult.Record.TableName == tableName) 268 | { 269 | existingRecord = searchResult; 270 | break; 271 | } 272 | } 273 | 274 | if (existingRecord is not null) 275 | { 276 | yield return (tableName!, existingRecord.Record.Definition!, existingRecord.Record.Description!, existingRecord.Record.SampleData!); 277 | continue; 278 | } 279 | 280 | logger.LogDebug("No existing record found for table: {TableName}, generating structure and data sample.", tableName); 281 | 282 | string previousSQLQuery = null!; 283 | var tableDefinition = await RetryHelper.Try(async (e) => 284 | { 285 | var definition = await sqlWriter.InvokeAsync(kernel, new KernelArguments(defaultKernelArguments) 286 | { 287 | { "prompt", $"Show the current structure of '{tableName}'" }, 288 | { "previousAttempt", previousSQLQuery }, 289 | { "previousException", e }, 290 | }, cancellationToken) 291 | .ConfigureAwait(false); 292 | 293 | previousSQLQuery = JsonSerializer.Deserialize(definition.GetValue()!).Query; 294 | 295 | logger.LogDebug("Generated table structure for {TableName}: {Query}", tableName, previousSQLQuery); 296 | 297 | return MarkdownRenderer.Render(await QueryExecutor.ExecuteSQLAsync(connection, previousSQLQuery, null, cancellationToken) 298 | .ConfigureAwait(false)); 299 | }, loggerFactory: loggerFactory!, cancellationToken: cancellationToken) 300 | .ConfigureAwait(false); 301 | 302 | logger.LogDebug("Table definition for {TableName}: {Definition}", tableName, tableDefinition); 303 | 304 | var tableExtract = await RetryHelper.Try(async (e) => 305 | { 306 | var extract = await sqlWriter.InvokeAsync(kernel, new KernelArguments(defaultKernelArguments) 307 | { 308 | { "prompt", $"Get the first 5 rows for '{tableName}'" }, 309 | { "tablesDefinition", tableDefinition }, 310 | { "previousAttempt", previousSQLQuery }, 311 | { "previousException", e }, 312 | }, cancellationToken) 313 | .ConfigureAwait(false); 314 | 315 | previousSQLQuery = JsonSerializer.Deserialize(extract.GetValue()!).Query; 316 | 317 | logger.LogDebug("Generated table data extract for {TableName}: {Query}", tableName, previousSQLQuery); 318 | 319 | return MarkdownRenderer.Render(await QueryExecutor.ExecuteSQLAsync(connection, previousSQLQuery, null, cancellationToken) 320 | .ConfigureAwait(false)); 321 | }, loggerFactory: loggerFactory!, cancellationToken: cancellationToken) 322 | .ConfigureAwait(false); 323 | 324 | logger.LogDebug("Table data extract for {TableName}: {Extract}", tableName, tableExtract); 325 | 326 | var tableExplain = await RetryHelper.Try(async (e) => 327 | { 328 | var description = await tableDescriptionGenerator.InvokeAsync(kernel, new KernelArguments 329 | { 330 | { "tableName", tableName }, 331 | { "tableDefinition", tableDefinition }, 332 | { "tableDataExtract", tableExtract } 333 | }) 334 | .ConfigureAwait(false); 335 | 336 | return JsonSerializer.Deserialize(description.GetValue()!)!; 337 | }, loggerFactory: loggerFactory!, cancellationToken: cancellationToken) 338 | .ConfigureAwait(false); 339 | 340 | var description = $""" 341 | ### {tableName} 342 | 343 | {tableExplain.Definition} 344 | 345 | #### Attributes 346 | 347 | {tableExplain.Attributes} 348 | 349 | #### Relations 350 | 351 | {tableExplain.Relations} 352 | """; 353 | 354 | logger.LogDebug("Generated table description for {TableName}: {Description}", tableName, description); 355 | 356 | yield return (tableName, tableDefinition, description, tableExtract)!; 357 | } 358 | } 359 | } 360 | -------------------------------------------------------------------------------- /src/SemanticKernel.Agents.DatabaseAgent/DatabasePlugin.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.AI; 2 | using Microsoft.Extensions.Logging; 3 | using Microsoft.Extensions.Logging.Abstractions; 4 | using Microsoft.Extensions.Options; 5 | using Microsoft.Extensions.VectorData; 6 | using Microsoft.SemanticKernel; 7 | using Microsoft.SemanticKernel.Connectors.OpenAI; 8 | using Microsoft.SemanticKernel.Embeddings; 9 | using Microsoft.SemanticKernel.PromptTemplates.Handlebars; 10 | using SemanticKernel.Agents.DatabaseAgent.Extensions; 11 | using SemanticKernel.Agents.DatabaseAgent.Filters; 12 | using SemanticKernel.Agents.DatabaseAgent.Internals; 13 | using System.ComponentModel; 14 | using System.Data; 15 | using System.Data.Common; 16 | using System.Text; 17 | using System.Text.Json; 18 | 19 | namespace SemanticKernel.Agents.DatabaseAgent; 20 | 21 | internal sealed class DatabasePlugin 22 | { 23 | private readonly ILogger _log; 24 | private readonly KernelFunction _writeSQLFunction; 25 | 26 | private readonly ILoggerFactory? _loggerFactory; 27 | 28 | private readonly IVectorSearchable _vectorStore; 29 | 30 | private readonly DatabasePluginOptions _options; 31 | 32 | public DatabasePlugin( 33 | IPromptProvider promptProvider, 34 | IOptions options, 35 | IVectorSearchable vectorStore, 36 | ILoggerFactory? loggerFactory = null) 37 | { 38 | this._options = options?.Value ?? throw new ArgumentNullException(nameof(options)); 39 | this._loggerFactory = loggerFactory; 40 | this._log = loggerFactory?.CreateLogger() ?? new NullLogger(); 41 | this._vectorStore = vectorStore; 42 | 43 | this._writeSQLFunction = KernelFunctionFactory.CreateFromPrompt(executionSettings: new OpenAIPromptExecutionSettings 44 | { 45 | MaxTokens = this._options.MaxTokens, 46 | Temperature = this._options.Temperature, 47 | TopP = this._options.TopP, 48 | Seed = 0, 49 | ResponseFormat = AIJsonUtilities.CreateJsonSchema(typeof(WriteSQLQueryResponse)) 50 | }, 51 | templateFormat: "handlebars", 52 | promptTemplate: promptProvider.ReadPrompt(AgentPromptConstants.WriteSQLQuery), 53 | promptTemplateFactory: new HandlebarsPromptTemplateFactory()); 54 | } 55 | 56 | [Description("Execute a query into the database. " + 57 | "The query should be formulate in natural language. " + 58 | "No worry about the schema, I'll look for you in the database.")] 59 | [KernelFunction] 60 | [return: Description("A Markdown representation of the query result.")] 61 | public async Task ExecuteQueryAsync(Kernel kernel, 62 | [Description("The user query in natural language.")] 63 | string prompt, 64 | CancellationToken cancellationToken) 65 | { 66 | try 67 | { 68 | var connection = kernel.GetRequiredService(); 69 | var textEmbeddingService = kernel.GetRequiredService(); 70 | 71 | var embeddings = await textEmbeddingService.GenerateEmbeddingAsync(prompt, cancellationToken: cancellationToken) 72 | .ConfigureAwait(false); 73 | 74 | var relatedTables = this._vectorStore.SearchAsync(embeddings, top: this._options.TopK, cancellationToken: cancellationToken) 75 | .ConfigureAwait(false); 76 | 77 | var tableDefinitionsSb = new StringBuilder(); 78 | 79 | await foreach (var relatedTable in relatedTables) 80 | { 81 | if (relatedTable.Score < this._options.MinScore) 82 | { 83 | this._log.LogInformation("Skipping table {tableName} with score {score} below threshold {minScore}", 84 | relatedTable.Record.TableName, relatedTable.Score, this._options.MinScore); 85 | continue; 86 | } 87 | 88 | tableDefinitionsSb.AppendLine(relatedTable.Record.Description); 89 | tableDefinitionsSb.AppendLine(); 90 | tableDefinitionsSb.AppendLine("---"); 91 | tableDefinitionsSb.AppendLine(); 92 | } 93 | 94 | var tableDefinitions = tableDefinitionsSb.ToString(); 95 | 96 | var sqlQuery = string.Empty; 97 | 98 | using var dataTable = await RetryHelper.Try(async (e) => 99 | { 100 | sqlQuery = await GetSQLQueryStringAsync(kernel, 101 | prompt, tableDefinitions, 102 | cancellationToken: cancellationToken, 103 | previousSQLException: e, 104 | previousSQLQuery: sqlQuery) 105 | .ConfigureAwait(false); 106 | 107 | if (string.IsNullOrWhiteSpace(sqlQuery)) 108 | { 109 | this._log.LogWarning("SQL query is empty for prompt: {prompt}", prompt); 110 | throw new InvalidOperationException("The kernel was unable to generate the expected query."); 111 | } 112 | 113 | this._log.LogInformation("SQL query generated: {sqlQuery}", sqlQuery); 114 | 115 | var queryExecutionContext = new QueryExecutionContext(kernel, prompt, tableDefinitions, sqlQuery, cancellationToken); 116 | 117 | (bool isQueryExecutionFiltered, string filterMessage) = await InvokeFiltersOrQueryAsync(kernel.GetAllServices().ToList(), 118 | _ => 119 | { 120 | return Task.FromResult((false, string.Empty)); 121 | }, 122 | queryExecutionContext) 123 | .ConfigureAwait(false); 124 | 125 | if (isQueryExecutionFiltered) 126 | { 127 | throw new Exception($"Query execution was filtered: {filterMessage}"); 128 | } 129 | 130 | return await QueryExecutor.ExecuteSQLAsync(connection, sqlQuery, this._loggerFactory, cancellationToken) 131 | .ConfigureAwait(false); 132 | }, count: 3, _loggerFactory, cancellationToken) 133 | .ConfigureAwait(false); 134 | 135 | var result = MarkdownRenderer.Render(dataTable); 136 | 137 | this._log.LogInformation("Query result: {result}", result); 138 | 139 | return result; 140 | } 141 | catch (Exception e) 142 | { 143 | this._log.LogError(e, "Error executing query: {prompt}", prompt); 144 | throw; 145 | } 146 | } 147 | 148 | private async Task GetSQLQueryStringAsync(Kernel kernel, 149 | string prompt, 150 | string tablesDefinitions, 151 | string? previousSQLQuery = null, 152 | Exception? previousSQLException = null, 153 | CancellationToken? cancellationToken = null) 154 | { 155 | var arguments = new KernelArguments() 156 | { 157 | { "prompt", prompt }, 158 | { "tablesDefinition", tablesDefinitions }, 159 | { "previousAttempt", previousSQLQuery }, 160 | { "previousException", previousSQLException }, 161 | { "providerName", kernel.GetRequiredService().GetProviderName() } 162 | }; 163 | 164 | this._log.LogInformation("Write SQL query for: {prompt}", prompt); 165 | 166 | var functionResult = await this._writeSQLFunction.InvokeAsync(kernel, arguments, cancellationToken ?? CancellationToken.None) 167 | .ConfigureAwait(false); 168 | 169 | return JsonSerializer.Deserialize(functionResult.GetValue()!)!.Query!; 170 | } 171 | 172 | private static async Task<(bool, string)> InvokeFiltersOrQueryAsync( 173 | List? functionFilters, 174 | Func> functionCallback, 175 | QueryExecutionContext context, 176 | int index = 0) 177 | { 178 | if (functionFilters is { Count: > 0 } && index < functionFilters.Count) 179 | { 180 | return await functionFilters[index].OnQueryExecutionAsync(context, 181 | (context) => InvokeFiltersOrQueryAsync(functionFilters, functionCallback, context, index + 1)).ConfigureAwait(false); 182 | } 183 | else 184 | { 185 | return await functionCallback(context).ConfigureAwait(false); 186 | } 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /src/SemanticKernel.Agents.DatabaseAgent/DatabasePluginOptions.cs: -------------------------------------------------------------------------------- 1 | namespace SemanticKernel.Agents.DatabaseAgent; 2 | 3 | public sealed class DatabasePluginOptions 4 | { 5 | public int TopK { get; set; } = 5; 6 | 7 | public int? MaxTokens { get; set; } = 4096; 8 | 9 | public double? Temperature { get; set; } = .1E-9; 10 | 11 | public double? TopP { get; set; } = .1E-9; 12 | 13 | public double? MinScore { get; set; } = 0.5; 14 | } 15 | -------------------------------------------------------------------------------- /src/SemanticKernel.Agents.DatabaseAgent/ExplainTableResponse.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace SemanticKernel.Agents.DatabaseAgent; 4 | 5 | internal sealed class ExplainTableResponse 6 | { 7 | [JsonPropertyName("tableName")] 8 | public string TableName { get; set; } = string.Empty; 9 | 10 | [JsonPropertyName("attributes")] 11 | public string Attributes { get; set; } = string.Empty; 12 | 13 | [JsonPropertyName("recordSample")] 14 | public string RecordSample { get; set; } = string.Empty; 15 | 16 | [JsonPropertyName("definition")] 17 | public string Definition { get; set; } = string.Empty; 18 | 19 | [JsonPropertyName("relations")] 20 | public string Relations { get; set; } = string.Empty; 21 | } 22 | -------------------------------------------------------------------------------- /src/SemanticKernel.Agents.DatabaseAgent/Extensions/DBConnectionExtension.cs: -------------------------------------------------------------------------------- 1 | using System.Data.Common; 2 | using System.Text.RegularExpressions; 3 | 4 | namespace SemanticKernel.Agents.DatabaseAgent.Extensions; 5 | 6 | internal static class DBConnectionExtension 7 | { 8 | internal static string GetProviderName(this DbConnection connection) 9 | { 10 | string typeName = connection.GetType().FullName; 11 | 12 | return typeName switch 13 | { 14 | string s when s.Contains("SqlClient", StringComparison.OrdinalIgnoreCase) => "SQL Server", 15 | string s when s.Contains("MySqlClient", StringComparison.OrdinalIgnoreCase) => "MySQL", 16 | string s when s.Contains("Npgsql", StringComparison.OrdinalIgnoreCase) => "PostgreSQL", 17 | string s when s.Contains("Oracle", StringComparison.OrdinalIgnoreCase) => "Oracle", 18 | string s when s.Contains("SQLite", StringComparison.OrdinalIgnoreCase) => "SQLite", 19 | string s when s.Contains("OleDb", StringComparison.OrdinalIgnoreCase) => "OLE DB", 20 | string s when s.Contains("Odbc", StringComparison.OrdinalIgnoreCase) => ExtractDriverFromConnectionString(connection.ConnectionString), 21 | _ => "Unknown Provider" 22 | }; 23 | } 24 | 25 | private static string ExtractDriverFromConnectionString(string connectionString) 26 | { 27 | if (string.IsNullOrWhiteSpace(connectionString)) 28 | { 29 | return "Unknown Driver"; 30 | } 31 | 32 | var match = Regex.Match(connectionString, @"(?i)Driver\s*=\s*([^;]+)"); 33 | return match.Success ? match.Groups[1].Value : "Unknown Driver"; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/SemanticKernel.Agents.DatabaseAgent/ExtractTableNameResponse.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace SemanticKernel.Agents.DatabaseAgent 4 | { 5 | internal class ExtractTableNameResponse 6 | { 7 | [JsonPropertyName("thinking")] 8 | public string Thinking { get; set; } = string.Empty; 9 | 10 | [JsonPropertyName("tableName")] 11 | public string? TableName { get; set; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/SemanticKernel.Agents.DatabaseAgent/Filters/IQueryExecutionFilter.cs: -------------------------------------------------------------------------------- 1 | namespace SemanticKernel.Agents.DatabaseAgent.Filters; 2 | 3 | public interface IQueryExecutionFilter 4 | { 5 | public Task<(bool filtered, string message)> OnQueryExecutionAsync(QueryExecutionContext context, Func> next); 6 | } 7 | -------------------------------------------------------------------------------- /src/SemanticKernel.Agents.DatabaseAgent/Filters/QueryExecutionContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.SemanticKernel; 2 | 3 | namespace SemanticKernel.Agents.DatabaseAgent.Filters; 4 | 5 | public class QueryExecutionContext 6 | { 7 | public Kernel Kernel { get; init; } 8 | 9 | public string TableDefinitions { get; init; } 10 | 11 | public string Prompt { get; init; } 12 | 13 | public string SQLQuery { get; init; } 14 | 15 | public CancellationToken CancellationToken { get; init; } 16 | 17 | internal QueryExecutionContext(Kernel kernel, string prompt, string tableDefinitions, string sqlQuery, CancellationToken cancellationToken) 18 | { 19 | Kernel = kernel; 20 | Prompt = prompt; 21 | TableDefinitions = tableDefinitions; 22 | SQLQuery = sqlQuery; 23 | CancellationToken = cancellationToken; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/SemanticKernel.Agents.DatabaseAgent/IPromptProvider.cs: -------------------------------------------------------------------------------- 1 | namespace SemanticKernel.Agents.DatabaseAgent; 2 | 3 | public class AgentPromptConstants 4 | { 5 | public const string AgentDescriptionGenerator = nameof(AgentDescriptionGenerator); 6 | public const string AgentInstructionsGenerator = nameof(AgentInstructionsGenerator); 7 | public const string AgentNameGenerator = nameof(AgentNameGenerator); 8 | public const string ExplainTable = nameof(ExplainTable); 9 | public const string WriteSQLQuery = nameof(WriteSQLQuery); 10 | public const string ExtractTableName = nameof(ExtractTableName); 11 | } 12 | 13 | public interface IPromptProvider 14 | { 15 | public string ReadPrompt(string promptName); 16 | } -------------------------------------------------------------------------------- /src/SemanticKernel.Agents.DatabaseAgent/Internals/EmbeddedPromptProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | 3 | namespace SemanticKernel.Agents.DatabaseAgent.Internals; 4 | 5 | internal sealed class EmbeddedPromptProvider : IPromptProvider 6 | { 7 | public string ReadPrompt(string promptName) 8 | { 9 | var resourceStream = Assembly.GetCallingAssembly() 10 | .GetManifestResourceStream($"{Assembly.GetCallingAssembly().GetName().Name}.Prompts.{promptName}.md"); 11 | 12 | using var reader = new StreamReader(resourceStream!); 13 | var text = reader.ReadToEnd(); 14 | return text; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/SemanticKernel.Agents.DatabaseAgent/Internals/MarkdownRenderer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Data; 4 | using System.Data.Common; 5 | using System.Linq; 6 | using System.Reflection.PortableExecutable; 7 | using System.Text; 8 | using System.Threading.Tasks; 9 | 10 | namespace SemanticKernel.Agents.DatabaseAgent.Internals 11 | { 12 | internal static class MarkdownRenderer 13 | { 14 | public static string Render(DataTable dataTable) 15 | { 16 | var result = new StringBuilder(); 17 | 18 | // Append column names 19 | for (int i = 0; i < dataTable.Columns.Count; i++) 20 | { 21 | result.Append($"| {dataTable.Columns[i].ColumnName} "); 22 | } 23 | result.AppendLine("|"); 24 | 25 | // Append separator 26 | for (int i = 0; i < dataTable.Columns.Count; i++) 27 | { 28 | result.Append("| --- "); 29 | } 30 | result.AppendLine("|"); 31 | 32 | // Append rows 33 | foreach (DataRow row in dataTable.Rows) 34 | { 35 | for (int i = 0; i < dataTable.Columns.Count; i++) 36 | { 37 | result.Append($"| {row[i]} "); 38 | } 39 | result.AppendLine("|"); 40 | } 41 | 42 | result.AppendLine("|"); 43 | 44 | 45 | return result.ToString(); 46 | } 47 | 48 | public static string Render(DataRow data) 49 | { 50 | var result = new StringBuilder(); 51 | 52 | // Append column names 53 | for (int i = 0; i < data.Table.Columns.Count; i++) 54 | { 55 | result.Append($"| {data.Table.Columns[i].ColumnName} "); 56 | } 57 | result.AppendLine("|"); 58 | 59 | // Append separator 60 | for (int i = 0; i < data.Table.Columns.Count; i++) 61 | { 62 | result.Append("| --- "); 63 | } 64 | result.AppendLine("|"); 65 | 66 | for (int i = 0; i < data.Table.Columns.Count; i++) 67 | { 68 | result.Append($"| {data[i]} "); 69 | } 70 | result.AppendLine("|"); 71 | 72 | 73 | return result.ToString(); 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/SemanticKernel.Agents.DatabaseAgent/Internals/QueryExecutor.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using Microsoft.Extensions.Logging.Abstractions; 3 | using System.Data; 4 | using System.Data.Common; 5 | using System.Text; 6 | 7 | namespace SemanticKernel.Agents.DatabaseAgent.Internals; 8 | 9 | internal static class QueryExecutor 10 | { 11 | internal static async Task ExecuteSQLAsync( 12 | DbConnection connection, 13 | string sqlQuery, 14 | ILoggerFactory? loggerFactory, 15 | CancellationToken cancellationToken) 16 | { 17 | var log = loggerFactory?.CreateLogger(nameof(QueryExecutor)) ?? NullLogger.Instance; 18 | 19 | log.LogInformation("SQL Query: {Query}", sqlQuery); 20 | 21 | using var command = connection.CreateCommand(); 22 | 23 | command.CommandText = sqlQuery; 24 | command.Parameters.Clear(); 25 | 26 | var result = new StringBuilder(); 27 | 28 | try 29 | { 30 | await connection.OpenAsync(cancellationToken) 31 | .ConfigureAwait(false); 32 | var reader = await command.ExecuteReaderAsync(cancellationToken) 33 | .ConfigureAwait(false); 34 | 35 | var dataTable = new DataTable(); 36 | 37 | return await SafeLoadToDataTableAsync(reader, cancellationToken); 38 | } 39 | catch (Exception ex) 40 | { 41 | log.LogError(ex, "Error executing SQL Query: {Query}", sqlQuery); 42 | throw; 43 | } 44 | finally 45 | { 46 | connection.Close(); 47 | } 48 | } 49 | 50 | private static async Task SafeLoadToDataTableAsync(DbDataReader reader, CancellationToken cancellationToken) 51 | { 52 | var dataTable = new DataTable(); 53 | 54 | // Build columns using schema info 55 | var schemaTable = await Task.Run(() => reader.GetSchemaTable(), cancellationToken); 56 | if (schemaTable != null) 57 | { 58 | foreach (DataRow row in schemaTable.Rows) 59 | { 60 | var columnName = row["ColumnName"]?.ToString() ?? "Column"; 61 | if (!dataTable.Columns.Contains(columnName)) 62 | { 63 | // Always use object to prevent type mismatch 64 | dataTable.Columns.Add(new DataColumn(columnName, typeof(object))); 65 | } 66 | } 67 | } 68 | 69 | // Read data rows 70 | while (await reader.ReadAsync(cancellationToken).ConfigureAwait(false)) 71 | { 72 | var values = new object[reader.FieldCount]; 73 | reader.GetValues(values); 74 | dataTable.Rows.Add(values); 75 | } 76 | 77 | return dataTable; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/SemanticKernel.Agents.DatabaseAgent/Internals/RetryHelper.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using Microsoft.Extensions.Logging.Abstractions; 3 | 4 | namespace SemanticKernel.Agents.DatabaseAgent.Internals 5 | { 6 | internal static class RetryHelper 7 | { 8 | internal static async Task Try(Func> func, 9 | int count = 5, 10 | ILoggerFactory loggerFactory = null!, 11 | CancellationToken? cancellationToken = null) 12 | { 13 | var token = cancellationToken ?? CancellationToken.None; 14 | 15 | Exception? lastException = null; 16 | 17 | for (int i = 0; i < count; i++) 18 | { 19 | try 20 | { 21 | return await func(lastException) 22 | .ConfigureAwait(false); 23 | } 24 | catch (Exception ex) 25 | { 26 | 27 | if (i == count - 1) 28 | { 29 | (loggerFactory ?? NullLoggerFactory.Instance) 30 | .CreateLogger(nameof(DatabaseAgentFactory)) 31 | .LogWarning(ex, "Failed to execute the function after {Count} attempts.", count); 32 | throw; 33 | } 34 | 35 | lastException = ex; 36 | 37 | await Task.Delay(200, token) 38 | .ConfigureAwait(false); 39 | } 40 | } 41 | throw new InvalidOperationException("Failed to execute the function."); 42 | 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/SemanticKernel.Agents.DatabaseAgent/Prompts/AgentDescriptionGenerator.md: -------------------------------------------------------------------------------- 1 | Create a detailed and appropriate agent description based on the tables it manages. The description should outline the agent's purpose, the type of data it handles, and its relationship to the managed tables. 2 | 3 | # Guidelines 4 | 5 | - **Understand the Context**: Review the provided tables and their structure to infer the agent's primary purpose and operational scope. 6 | - **Details to Include**: 7 | - **Purpose**: What the agent is designed to achieve or manage. 8 | - **Relationships**: Connections or dependencies between the tables that the agent manages. 9 | - **Functionalities**: Optional section describing the agent’s actions, e.g., data retrieval, updates, analytics. 10 | 11 | # Output Format 12 | 13 | The agent description should be a structured paragraph or bulleted list. For instances that require a machine-readable format, the description may be delivered in JSON format: 14 | 15 | [START OF EXAMPLE] 16 | **Input**: 17 | Tables managed: 18 | 1. **Orders**: Tracks customer purchases. 19 | 2. **Products**: Contains product catalog data. 20 | 3. **Customers**: Stores customer profiles and contact information. 21 | 22 | **Output**: 23 | ```json 24 | { 25 | "description": ""Manage customer orders, products, and profiles to facilitate the online sales process.\n### Relationships\nEach order is linked to a corresponding customer and one or more products.\n### Examples of actions supported\nSearch for orders, update product inventory, retrieve and manage customer details.", 26 | 27 | } 28 | ``` 29 | [END OF EXAMPLE] 30 | 31 | # Notes 32 | 33 | - Ensure all agent descriptions align with the context of the managed tables. 34 | - If tables are interdependent (e.g., via foreign keys), clearly articulate the relationships. 35 | - Include examples only when relevant and avoid referencing confidential or sensitive data directly. 36 | 37 | # Let's go! 🚀 38 | 39 | **Input:** 40 | Tables managed: 41 | Agent Description: {{$tableDefinitions}} 42 | 43 | **Output:** -------------------------------------------------------------------------------- /src/SemanticKernel.Agents.DatabaseAgent/Prompts/AgentInstructionsGenerator.md: -------------------------------------------------------------------------------- 1 | Generate clear, markdown-based instructions for an agent’s behavior, detailing its role, capabilities, and tool usage based on a given description. 2 | 3 | # Guidelines 4 | 5 | 1. **Understand the Agent's Role** 6 | - Begin by analyzing the agent's purpose, capabilities, and any constraints as described in the input. 7 | - Identify actionable priorities and key outcomes the agent must achieve based on its purpose. 8 | 9 | 2. **Tool Utilization** 10 | - Detail when and how the agent should effectively use each available tool. 11 | - Include guidance for selecting between tools when multiple options are available. 12 | - Encourage verifying outputs from tools to ensure accuracy and minimize errors. 13 | 14 | 3. **Prioritize Reliability and Transparency** 15 | - Emphasize the importance of producing outputs that are clear, accurate, and well-supported. 16 | - Specify that the response should cite sources when applicable or explain limitations when information is incomplete. 17 | 18 | 4. **Communication Style** 19 | - Instruct the agent to tailor responses to the user's context, including tone, depth of explanation, and language complexity. 20 | - Give examples of how to balance conciseness with completeness for varying tasks. 21 | 22 | 5. **Uncertainty and Limitations** 23 | - Explain how the agent should handle unclear queries or situations where tools or information fall short. 24 | - Encourage follow-up clarification requests to refine user intent. 25 | 26 | # Steps 27 | 28 | 1. Extract the agent’s objectives, capabilities, and any provided tools from the input description. 29 | 2. Define specific guidelines for the agent’s operation, ensuring clarity on its tasks and how its tools should be leveraged. 30 | 3. Address principles for maintaining reliability, user-centered adjustments, and handling ambiguities or limitations. 31 | 4. Format the output as markdown text with appropriate sections such as "Objectives," "Tool Guidelines," "Accuracy," etc. 32 | 33 | # Output Format 34 | 35 | The output should be returned in JSON format as follows: 36 | 37 | ```json 38 | { 39 | "instructions": "" 40 | } 41 | ``` 42 | 43 | # Examples 44 | 45 | ### Example Input: 46 | ```text 47 | The agent is a virtual assistant focused on project management tasks for teams. It can create task lists, assign responsibilities, track deadlines, and generate reports. It has access to a calendar app, task management tool, and basic analytics software. 48 | ``` 49 | 50 | ### Example Output: 51 | ```json 52 | { 53 | "instructions": "You are an experienced project manager for a team.\n\n#### Objectives:\n1. Assist users with organizing tasks, assigning responsibilities, and prioritizing workflows for teams.\n2. Track deadlines and deliver timely reminders to ensure project milestones are met.\n3. Generate clear and concise progress reports for stakeholders.\n\n#### Tool Guidelines:\n- Use the **calendar app** to schedule deadlines, set reminders, and ensure thorough time management.\n- Use the **task management tool** to create detailed to-do lists, assign team members, and monitor task statuses.\n- Use the **analytics software** to generate summaries or visualizations for progress reports.\n\n#### Accuracy and Reliability:\n- Before finalizing outputs, cross-check information to ensure all task details and deadlines are correct.\n- For team assignments or timelines, confirm inputs with available team data or ask for clarification if uncertain.\n\n#### Communication Guidelines:\n- Respond to users with clear, user-friendly language. Adapt responses to the user's familiarity with project management tools.\n- Use structured formatting (e.g., bullet points, numbered lists) for readability when presenting workflows or reports.\n- Adjust detail levels (broad overviews vs. granular analysis) based on user queries.\n\n#### Handling Uncertainty:\n- If user inputs are incomplete, ask for additional details to clarify the task or context.\n- If a tool cannot retrieve specific data (e.g., missing team member information), explain the limitation and suggest steps to resolve it.\n\n#### Example Workflow:\n1. User requests a task list for an upcoming project.\n - Use the task management tool to structure a list.\n - Assign responsibilities based on input or suggest a preliminary assignment plan.\n2. User asks to analyze project progress.\n - Use the analytics tool to generate progress insights, including task completion rates.\n - Highlight any overdue tasks or potential bottlenecks.\n3. User requests an updated project timeline.\n - Modify the calendar with new dates and reminders for any adjusted tasks.\n - Inform the user of the changes and confirm the new schedule." 54 | } 55 | ``` 56 | 57 | (Replace tools, capabilities, and details in examples as needed based on input.) 58 | 59 | # Notes 60 | 61 | - Customize markdown instructions to align directly with the agent’s described role and tools. 62 | - Ensure that tool usage explanations are practical and specific, avoiding ambiguity. 63 | - Highlight steps for maintaining accuracy and handling uncertainties clearly and systematically. 64 | - Do not alter any provided information or descriptions inaccurately—preserve the integrity of given tasks and constraints. -------------------------------------------------------------------------------- /src/SemanticKernel.Agents.DatabaseAgent/Prompts/AgentNameGenerator.md: -------------------------------------------------------------------------------- 1 | Generate a creative and fitting agent name based on the provided agent description. 2 | 3 | - Analyze the agent description to understand its purpose, role, characteristics, and any distinct features. 4 | - Ensure the generated name captures the essence of the agent, reflecting its function, personality, or core attributes as described. 5 | - The name should be concise, memorable, and relevant to the provided description. Avoid overly generic or unrelated names. 6 | - Consider including themes, metaphors, or stylistic elements inspired by the details of the description. 7 | 8 | ## Steps 9 | 10 | 1. Read and understand the agent description. Identify: 11 | - Key purpose or role of the agent. 12 | - Core traits or personality (if provided). 13 | - Any thematic or stylistic hints embedded in the description. 14 | 2. Synthesize the name based on relevant keywords, themes, or concepts. 15 | 3. Test the name for clarity and relevance. 16 | 17 | ## Output Format 18 | 19 | - The agent name should be delivered as a single word or short phrase without any explanation. 20 | 21 | ## Examples 22 | 23 | **Input:** 24 | Agent Description: An AI that provides financial advice and budgeting tips, focusing on simplicity, logic, and trustworthiness. 25 | 26 | **Output:** 27 | ```json 28 | { 29 | "name": "FinGuard" 30 | } 31 | ``` 32 | 33 | --- 34 | 35 | **Input:** 36 | Agent Description: A playful chatbot designed to engage children in learning grammar and vocabulary. 37 | 38 | **Output:** 39 | ```json 40 | { 41 | "name": "WordWhiz" 42 | } 43 | ``` 44 | 45 | ## Notes 46 | 47 | - If no specific details are provided, default to creating names that are functional and versatile. 48 | - For whimsical, thematic, or creative agents, feel free to incorporate imaginative elements into the name. 49 | 50 | # Let's generate some creative agent names! 🚀 51 | 52 | **Input:** 53 | Agent Description: {{$agentDescription}} 54 | 55 | **Output:** 56 | -------------------------------------------------------------------------------- /src/SemanticKernel.Agents.DatabaseAgent/Prompts/ExplainTable.md: -------------------------------------------------------------------------------- 1 | Generate a natural language description explaining the purpose of a database table based on its column names and types. 2 | 3 | ## Steps 4 | 5 | 1. **Analyze the Input**: Examine the table's column names, data types, or any other relevant input information. 6 | 2. **Infer the Context**: 7 | - Identify key fields (e.g., primary key, foreign keys). 8 | - Look for patterns or domain-specific terminologies that suggest the table's purpose. 9 | - Consider how the columns might relate to one another to form a coherent description. 10 | 3. **Generate a Coherent Description**: 11 | - Synthesize the identified context into a concise, human-readable explanation. 12 | - Explain how the table might be used or the kind of data it likely stores. 13 | 4. **Handle Ambiguities**: 14 | - If the purpose of the table is not explicit based on column names alone, provide educated guesses or flag uncertainty. 15 | - Avoid being overly definitive unless there is strong contextual evidence. 16 | 17 | ## Output Format 18 | 19 | The output should be a **short paragraph** in natural language. The description should: 20 | - Begin with a statement clearly indicating the table's likely purpose. 21 | - Optionally elaborate on the relationships or dependencies between columns for additional clarity. 22 | - Avoid excessive technical jargon unless essential to the task. 23 | 24 | ### Json schema 25 | 26 | ```json 27 | { 28 | "tableName": "The table name as described", 29 | "attributes": "A markdown-formatted bullet list describing the columns in the table, including their names, types, and purposes.", 30 | "recordSample": "A markdown-formatted table showing a few example rows from the dataset, illustrating the structure and content of the table.", 31 | "definition": "A concise textual explanation of the table’s purpose and the type of data it represents within the overall data model.", 32 | "relations": "A markdown-formatted table describing relationships between this table and others, including relationship types (e.g., one-to-many) and a short explanation." 33 | } 34 | ``` 35 | 36 | ## Example 37 | 38 | **Input**: 39 | 40 | ``` 41 | Table Definition: 42 | | Column Name | Data Type | Constraints | 43 | |-----------------------|-------------------|-----------------------| 44 | | Book_ID | INT | PRIMARY KEY | 45 | | Title | VARCHAR(255) | NOT NULL | 46 | | Author_ID | INT | FOREIGN KEY | 47 | | Publication_Year | INT | NOT NULL | 48 | | Genre | VARCHAR(50 | FOREIGN KEY | 49 | | ISBN | VARCHAR(13) | UNIQUE | 50 | ``` 51 | 52 | ```Table extract: 53 | | Book_ID | Title | Author | Publication_Year | Genre | ISBN | 54 | |---------|---------------------------|----------------------------|-------------------|-------------------|---------------------| 55 | | 1 | The Little Prince | 1034 | 1943 | FICTION | 978-2-07-061275-8 | 56 | | 2 | 1984 | 732 | 1949 | POLAR | 978-0-452-28423-4 | 57 | | 3 | The Great Gatsby | 64346 | 1925 | FICTION | 978-0-7432-7356-5 | 58 | ``` 59 | 60 | **Output**: 61 | 62 | ```json 63 | { 64 | "tableName": "Book", 65 | "attributes": "- **Book_ID** (primary key): Unique identifier for the book.\n- **Title**: Title of the book.\n- **Author**: Author of the book.\n- **Publication_Year**: Year of publication for the book.\n- **Genre**: Literary genre of the book.\n- **ISBN**: ISBN code of the book.\n\n", 66 | "recordSample:"| Book_ID | Title | Author | Publication_Year | Genre | ISBN |\n|---------|---------------------------|----------------------------|-------------------|-------------------|---------------------|\n| 1 | The Little Prince | Antoine de Saint-Exupéry | 1943 | Fiction | 978-2-07-061275-8 |\n| 2 | 1984 | George Orwell | 1949 | Science Fiction | 978-0-452-28423-4 |\n| 3 | The Great Gatsby | F. Scott Fitzgerald | 1925 | Fiction | 978-0-7432-7356-5 |", 67 | "definition":"This simplified model focuses on managing books in a library. It highlights the key information needed to catalog and search for books." 68 | "relations": "| From Table | To Table | Relation | Description |\n|------------|----------|--------------|---------------------------------------------------------------------|\n| Book | Author | Many-to-One | Each book is written by one author, but an author can write multiple books. |\n| Book | Genre | Many-to-One | Each book belongs to one genre, but a genre can have multiple books. |", 69 | } 70 | ``` 71 | --- 72 | 73 | If the context or purpose is unclear, explicitly note this in the response. 74 | Ensure the table name and columns are clearly defined in the output without any alteration of the original column names or types. 75 | 76 | # Let's Practice! 77 | 78 | **Input**: 79 | ``` 80 | Table Name: 81 | {{$tableName}} 82 | ``` 83 | ``` 84 | Table Definition: 85 | {{$tableDefinition}} 86 | ``` 87 | ``` 88 | Table extract: 89 | {{$tableDataExtract}} 90 | ``` 91 | **Output**: -------------------------------------------------------------------------------- /src/SemanticKernel.Agents.DatabaseAgent/Prompts/ExtractTableName.md: -------------------------------------------------------------------------------- 1 | Extract the table name from a given input, ensuring it is properly formatted for use in SQL queries as per the guidelines below: 2 | 3 | - **Bracket Requirement**: If the table name contains spaces, special characters, or starts with a number, enclose it in square brackets `[]`. 4 | - **Preserve Existing Brackets**: Do not modify names that are already correctly bracketed. 5 | - **Restrict Output**: Do not include any explanation, return only the formatted table name in a JSON object. 6 | - **SQL Compatibility**: Ensure formatting is compatible with the specified provider. 7 | - 8 | # Steps 9 | 10 | 1. Extract the content inside the input. 11 | 2. Inspect the extracted table name for spaces, special characters, or if it begins with a number. 12 | 3. Apply brackets `[]` around the name only if it meets the criteria listed above. 13 | 4. Validate against existing bracket formatting. Do not add brackets if the name is already correctly bracketed. 14 | 5. Return the final table name as a JSON object. 15 | 16 | # Output Format 17 | 18 | Return a JSON object formatted as: 19 | 20 | ```json 21 | { 22 | "thinking": "Your thought process on how you arrived at the table name.", 23 | "tableName": "FormattedTableName" 24 | } 25 | ``` 26 | 27 | # Notes 28 | 29 | - Ensure that the final table name is properly structured for SQL and does not include any additional artifacts from the placeholder. 30 | - Avoid misinterpreting symbols or placeholders; only extract and format the table name. 31 | 32 | # Examples 33 | 34 | **Input**: 35 | ``` 36 | | name | 37 | | --- | 38 | | Products Table | 39 | ``` 40 | **Provider**: `Microsoft SQL Server` 41 | **Output**: 42 | ```json 43 | { 44 | "thinking": "The table name contains spaces, so it needs to be enclosed in brackets for SQL Server compatibility.", 45 | "tableName": "[Products Table]" 46 | } 47 | ``` 48 | 49 | **Input**: 50 | ``` 51 | | name | 52 | | --- | 53 | | Products Table | 54 | ``` 55 | **Provider**: `PostgreSQL` 56 | **Output**: 57 | ```json 58 | { 59 | "thinking": "The table name contains spaces, so it needs to be enclosed in double quotes for PostgreSQL compatibility.", 60 | "tableName": "\"Products Table\"" 61 | } 62 | ``` 63 | 64 | **Input**: 65 | ``` 66 | | name | 67 | | --- | 68 | | Products Table | 69 | ``` 70 | **Provider**: `MySQL` 71 | **Output**: 72 | ```json 73 | { 74 | "thinking": "The table name contains spaces, so it needs to be enclosed in backticks for MySQL compatibility.", 75 | "tableName": "`Products Table`" 76 | } 77 | ``` 78 | 79 | **Input**: 80 | ``` 81 | | name | 82 | | --- | 83 | | ValidTableName | 84 | ``` 85 | **Provider**: `Microsoft SQL Server` 86 | **Output**: 87 | ```json 88 | { 89 | "thinking": "The table name does not require brackets as it is valid without them.", 90 | "tableName": "ValidTableName" 91 | } 92 | ``` 93 | 94 | **Input**: 95 | ``` 96 | | name | 97 | | --- | 98 | | [SpecialName] | 99 | ``` 100 | **Provider**: `SQLite` 101 | **Output**: 102 | ```json 103 | { 104 | "thinking": "The table name is already correctly bracketed."," 105 | "tableName": "[SpecialName]" 106 | } 107 | ``` 108 | 109 | # Let's do this! 110 | 111 | **Input**: `{{$item}}` 112 | **Provider**: `{{$providerName}}`. 113 | **Output**: -------------------------------------------------------------------------------- /src/SemanticKernel.Agents.DatabaseAgent/Prompts/WriteSQLQuery.md: -------------------------------------------------------------------------------- 1 | You are an expert SQL query generator for {{providerName}}. 2 | 3 | {{ #if previousAttempt }} 4 | 5 | Your task is to fix a SQL query based on the provided database schema and the specific requirements of {{providerName}}. 6 | 7 | ### Constraints 8 | - You should never guess or make assumptions about the database structure beyond what is explicitly provided in the `Tables and Columns` section. 9 | - Avoid using any CLI commands and focus solely on generating the SQL query. 10 | 11 | # Steps 12 | 1. Review the provided database schema in the `Tables and Columns` section. 13 | 2. Analyze the provided SQL query to identify errors, inefficiencies, or issues. 14 | 3. Fix the SQL query based on: 15 | - The database schema. 16 | - The specific requirements for {{providerName}}. 17 | 4. Ensure the corrected query adheres to SQL best practices, remains efficient, and aligns with the current database schema. 18 | 19 | # Output Format 20 | Provide the corrected SQL query as plain text. 21 | 22 | # Notes 23 | - Only provide the corrected SQL query—do not include description, commentary, or extraneous information. 24 | - Ensure the output does not deviate from the supplied structure, schema, and requirements. 25 | 26 | # Examples 27 | 28 | **Example 1**: 29 | **DBMS Provider**: MySQL 30 | **Natural Language Query**: "Find all customers with last names starting with 'S'." 31 | **Tables and Columns**: 32 | ### [Customers] 33 | The 'Customers' table is designed to hold essential details about each customer in the database. It includes unique identifiers for customers, their contact information, and names to facilitate communication and querying. 34 | #### Attributes 35 | - **CustomerID**: Unique identifier for each customer in the database. 36 | - **FirstName**: The customer's first name. 37 | - **LastName**: The customer's last name, which can be used for personalized communication. 38 | - **Email**: The customer's email address for contact purposes. 39 | 40 | #### Relations 41 | | From Table | To Table | Relation | Description | 42 | |------------|-----------|--------------|-------------------------------------------------------------------| 43 | | Customers | Orders | One-to-Many | Each customer can have multiple orders linked to their account. | 44 | --- 45 | 46 | Previous Attempt: 47 | ```sql 48 | SELECT * FROM Customers WHERE last_name LIKE 'S%'; 49 | ``` 50 | The error was: 51 | ``` 52 | The column 'last_name' does not exist in the Customers table. 53 | ``` 54 | Generated: 55 | ```json 56 | { 57 | "comments": [ 58 | "Last name was incorrectly referenced as 'last_name' instead of 'LastName'.", 59 | "The wildcard 'S%' is used to match last names starting with 'S'.", 60 | "Query is compatible with MySQL syntax." 61 | ], 62 | "query": "SELECT * FROM Customers WHERE LastName LIKE 'S%';" 63 | } 64 | ``` 65 | 66 | {{else}} 67 | Your task is to create a valid SQL query based on a natural language prompt, considering the provided database schema and the specific requirements of {{providerName}}. 68 | You should never guess or make assumptions about the database structure beyond what is provided in the `Tables and Columns` section. 69 | 70 | You should avoid using any CLI commands, and focus solely on generating the SQL query. 71 | 72 | # Steps 73 | 74 | 1. **Parse Details**: 75 | - Identify and format the table/column names from the provided `Tables and Columns` section. 76 | - Resolve any ambiguities in natural language using the supplied table structures. 77 | 78 | 2. **DBMS Compatibility**: 79 | - Bracket Requirement: If the table name contains spaces, special characters, or starts with a number, enclose it in square brackets `[]`. 80 | - Preserve Existing Brackets: Do not modify names that are already correctly bracketed. 81 | - Ensure that the query adheres strictly to its supported syntax. 82 | 83 | 3. **Handle Special Table and Column Names**: 84 | - Enclose table names in brackets `[]` if they contain spaces, special characters, or start with a number. 85 | - Enclose column names in brackets `[]` if they contain spaces, special characters, or start with a number. 86 | - Ensure that the final table name is properly structured for SQL and does not include any additional artifacts from the placeholder. 87 | - Avoid misinterpreting symbols or placeholders; only extract and format the table name and column name. 88 | 89 | 4. **Write the Query**: 90 | - Translate the natural language into its SQL equivalent, adhering to the DBMS’s rules for joins, filters, aggregations, or other operations. 91 | 92 | 5. **Optimize and Document**: 93 | - Add optional comments explaining assumptions or adjustments for performance, based on the natural language query and the DBMS. 94 | 95 | # Output Format 96 | 97 | The output should be structured as a JSON object: 98 | - **`query`**: The SQL query string, ensuring adherence to DBMS-specific rules. 99 | - **`comments`**: A list of comments explaining: 100 | 1. Any assumptions or constraints applied while translating the natural language query. 101 | 2. Considerations specific to the DBMS type (e.g., syntax adjustments or optimizations). 102 | 103 | # Notes 104 | 105 | - SQL injection concerns or dynamic parameters are not part of this task but should be considered outside this scope. 106 | - Analyze table/column structures for duplicates, edge cases, or potential joins required by the prompt. 107 | 108 | # Examples 109 | 110 | **Example 1**: 111 | **DBMS Provider**: MySQL 112 | **Natural Language Query**: "Find all customers with last names starting with 'S'." 113 | **Tables and Columns**: 114 | ### [Customers] 115 | The 'Customers' table is designed to hold essential details about each customer in the database. It includes unique identifiers for customers, their contact information, and names to facilitate communication and querying. 116 | 117 | #### Attributes 118 | - **CustomerID**: Unique identifier for each customer in the database. 119 | - **FirstName**: The customer's first name. 120 | - **LastName**: The customer's last name, which can be used for personalized communication. 121 | - **Email**: The customer's email address for contact purposes. 122 | 123 | #### Relations 124 | | From Table | To Table | Relation | Description | 125 | |------------|-----------|--------------|-------------------------------------------------------------------| 126 | | Customers | Orders | One-to-Many | Each customer can have multiple orders linked to their account. | 127 | 128 | --- 129 | **Generated**: 130 | ```json 131 | { 132 | "comments": [ 133 | "The wildcard 'S%' is used to match last names starting with 'S'.", 134 | "Query is compatible with MySQL syntax." 135 | ], 136 | "query": "SELECT * FROM Customers WHERE last_name LIKE 'S%';" 137 | } 138 | ``` 139 | 140 | **Example 2**: 141 | **DBMS Provider**: SQL Server 142 | **Natural Language Query**: "Get the total sales by product category." 143 | **Tables and Columns**: 144 | ### [Products] 145 | The 'Products' table is structured to capture comprehensive details about each product available for sale, including its unique identifier, category, and price. This table serves as a central repository for product information. 146 | 147 | #### Attributes 148 | - **ProductID**: Unique identifier for each product in the inventory. 149 | - **Category**: The classification or type of product (e.g., electronics, clothing). 150 | - **Price**: The retail price of the product. 151 | 152 | #### Relations 153 | | From Table | To Table | Relation | Description | 154 | |------------|-----------|--------------|-------------------------------------------------------------------| 155 | | Products | Sales | One-to-Many | Each product can have multiple sales linked to it. | 156 | 157 | --- 158 | 159 | ### [Sales] 160 | The 'Sales' table is organized to maintain records of individual sales transactions, including the products sold and the quantities sold. This table plays a crucial role in sales analytics and inventory management. 161 | 162 | #### Attributes 163 | - **SaleID**: Unique identifier for each sale transaction. 164 | - **ProductID**: Identifier for the product sold, likely a foreign key referencing the Products table. 165 | - **Quantity**: The number of units sold in the transaction. 166 | 167 | #### Relations 168 | | From Table | To Table | Relation | Description | 169 | |------------|-----------|--------------|-------------------------------------------------------------------| 170 | | Sales | Products | Many-to-One | Each sale corresponds to a specific product. | 171 | 172 | --- 173 | **Generated**: 174 | ```json 175 | { 176 | "comments": [ 177 | "'quantity * price' provides total sales for each product.", 178 | "Compatible with SQL Server syntax; brackets used for [Products] table." 179 | ], 180 | "query": "SELECT p.category, SUM(s.quantity * p.price) AS total_sales FROM [Products] p INNER JOIN Sales s ON p.product_id = s.product_id GROUP BY p.category;" 181 | } 182 | ``` 183 | 184 | **Example 3**: 185 | **DBMS Provider**: MySQL 186 | **Natural Language Query**: "List all employees who joined after January 1, 2020."" 187 | **Tables and Columns**: 188 | ### [Employees] 189 | The 'Employees' table is structured to store vital information about employees within an organization. It includes unique identifiers, names, and the date each employee joined the company for effective management and reporting. 190 | 191 | #### Attributes 192 | - **EmployeeID**: Unique identifier for each employee in the organization. 193 | - **FirstName**: The employee's first name, used for identification and personalization. 194 | - **LastName**: The employee's last name, important for formal communication. 195 | - **JoinDate**: The date the employee joined the organization, which can be used to track tenure and employee progress. 196 | 197 | #### Relations 198 | | From Table | To Table | Relation | Description | 199 | |------------|-----------|--------------|-------------------------------------------------------------------| 200 | | Employees | Departments | Many-to-One | Each employee belongs to a specific department within the organization. | 201 | 202 | --- 203 | Generated: 204 | ```json 205 | { 206 | "comments": [ 207 | "The date format is adjusted to MySQL's standard format.", 208 | "Found 'join date' is stored in a DATE type column.", 209 | "Column names with spaces are enclosed in backticks for MySQL compatibility." 210 | ], 211 | "query": "SELECT employee_id, `first name`, `last name` FROM Employees WHERE `join date` > '2020-01-01';" 212 | } 213 | ``` 214 | {{/if}} 215 | 216 | ## IMPORTANT 217 | 218 | - You must ensure that the SQL query is valid and executable in the context of the provided database schema. 219 | - You must never guess or make assumptions about the database structure beyond what is explicitly provided in the `Tables and Columns` section. 220 | - You should avoid using any CLI commands and focus solely on generating the SQL query. 221 | - You should ensure that the query is formatted correctly according to the specific requirements of {{providerName}}. 222 | 223 | ## Let's do it for real 224 | 225 | #### Input: 226 | **DBMS Provider:** {{providerName}} 227 | **Natural Language Query:** "{{prompt}}" 228 | 229 | [BEGIN TABLES AND COLUMNS] 230 | {{tablesDefinition}} 231 | [END TABLES AND COLUMNS] 232 | 233 | {{ #if previousAttempt }} 234 | #### Previous Attempt 235 | 236 | You previously attempted to generate a SQL query for the following prompt, but it encountered an error. 237 | Below is the SQL query you generated: 238 | ```sql 239 | {{ previousAttempt }} 240 | ``` 241 | The error was: 242 | ``` 243 | {{ previousException }} 244 | ``` 245 | 246 | To enhance the new query generation, please analyze the previous attempt and consider the following: 247 | - **Identify the Problem**: Understand the specific reason for the failure (as indicated in the error message). Take note of any SQL syntax errors, missing fields, or logical inconsistencies that led to this error. 248 | - **Utilize the Semantic Model**: Refer to the `Tables and Columns` section, which includes all valid table names and columns relevant to this task. Ensure that your new query explicitly aligns with this model. 249 | - **Adjust the Query**: Modify your approach based on the above points. Focus on constructing a new query that avoids the issues identified in the previous attempt. 250 | 251 | With these considerations in mind, please generate a new SQL query based on the original prompt while integrating your learnings from the previous attempt. 252 | {{/if}} -------------------------------------------------------------------------------- /src/SemanticKernel.Agents.DatabaseAgent/README.md: -------------------------------------------------------------------------------- 1 | # DBMS Agent for Semantic Kernel 2 | 3 | ## Overview 4 | 5 | The Database Agent for Semantic Kernel is a service that provides a database management system (DBMS) for the Semantic Kernel (NL2SQL). The Agent is responsible for managing the storage and retrieval of data from the Semantic Kernel. 6 | This built on top of the [Microsoft's Semantic Kernel](https://github.com/microsoft/semantic-kernel) and leverages the [Microsoft's Kernel Memory](https://github.com/microsoft/kernel-memory) service to memorize database schema and relationships to provide a more efficient and accurate database management system. 7 | 8 | ## Getting Started 9 | 10 | ### Prerequisites 11 | 12 | - [.NET 8.0 SDK](https://dotnet.microsoft.com/download/dotnet/8.0) 13 | 14 | ## Contributing 15 | 16 | We welcome contributions to enhance this project. Please fork the repository and submit a pull request with your proposed changes. 17 | 18 | ## License 19 | 20 | This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details. 21 | 22 | ## Acknowledgments 23 | 24 | - [Microsoft's Kernel Memory](https://github.com/microsoft/kernel-memory) for providing the foundational AI service. 25 | - The open-source community for continuous support and contributions. 26 | -------------------------------------------------------------------------------- /src/SemanticKernel.Agents.DatabaseAgent/SemanticKernel.Agents.DatabaseAgent.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | $(NoWarn);CA2007;IDE1006;SKEXP0001;SKEXP0110;OPENAI001 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | Semantic Kernel Database agent 17 | README.md 18 | 19 | Microsoft's Semantic Kernel NL2SQL agent for databases. 20 | This agent can be used to generate SQL queries from natural language prompts. 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /src/SemanticKernel.Agents.DatabaseAgent/TableDefinitionSnippet.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.VectorData; 2 | 3 | namespace SemanticKernel.Agents.DatabaseAgent 4 | { 5 | public sealed class TableDefinitionSnippet 6 | { 7 | public required string TableName { get; set; } 8 | 9 | public required Guid Key { get; set; } 10 | 11 | public string? Definition { get; set; } 12 | 13 | public string? Description { get; set; } 14 | 15 | public string? SampleData { get; set; } 16 | 17 | public ReadOnlyMemory TextEmbedding { get; set; } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/SemanticKernel.Agents.DatabaseAgent/WriteSQLQueryResponse.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace SemanticKernel.Agents.DatabaseAgent; 5 | 6 | internal sealed class WriteSQLQueryResponse 7 | { 8 | [JsonPropertyName("comments")] 9 | [JsonPropertyOrder(0)] 10 | [Description("A list of comments explaining: \n- Any assumptions or constraints applied while translating the natural language query.\n Considerations specific to the DBMS type (e.g., syntax adjustments or optimizations).")] 11 | public IEnumerable Comments { get; set; } = Enumerable.Empty(); 12 | 13 | [JsonPropertyName("query")] 14 | [JsonRequired] 15 | [Description("The SQL query string, ensuring adherence to DBMS-specific rules")] 16 | [JsonPropertyOrder(1)] 17 | public string Query { get; set; } = string.Empty; 18 | } 19 | -------------------------------------------------------------------------------- /src/SemanticKernel.Plugins.DatabaseAgent.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.13.35825.156 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SemanticKernel.Agents.DatabaseAgent", "SemanticKernel.Agents.DatabaseAgent\SemanticKernel.Agents.DatabaseAgent.csproj", "{0FB959FE-C974-4D07-8D4E-BC7127FC22CE}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8EC462FD-D22E-90A8-E5CE-7E832BA40C5D}" 9 | ProjectSection(SolutionItems) = preProject 10 | ..\.gitignore = ..\.gitignore 11 | ..\LICENSE.md = ..\LICENSE.md 12 | ..\README.md = ..\README.md 13 | EndProjectSection 14 | EndProject 15 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SemanticKernel.Agents.DatabaseAgent.Tests", "..\tests\SemanticKernel.Agents.DatabaseAgent.Tests\SemanticKernel.Agents.DatabaseAgent.Tests.csproj", "{830AF03C-A298-44E3-96C6-6E1A6EAE0C5E}" 16 | EndProject 17 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SemanticKernel.Agents.DatabaseAgent.QualityAssurance", "SemanticKernel.Agents.DatabaseAgent.QualityAssurance\SemanticKernel.Agents.DatabaseAgent.QualityAssurance.csproj", "{ECA0008E-81A8-41CE-9896-08F22A5CCBAF}" 18 | EndProject 19 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SemanticKernel.Agents.DatabaseAgent.MCPServer", "SemanticKernel.Agents.DatabaseAgent.MCPServer\SemanticKernel.Agents.DatabaseAgent.MCPServer.csproj", "{4A993934-CA43-4EB9-A3C6-C0976D705A33}" 20 | EndProject 21 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".github", ".github", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}" 22 | ProjectSection(SolutionItems) = preProject 23 | ..\.github\CODEOWNERS = ..\.github\CODEOWNERS 24 | ..\.github\dependabot.yaml = ..\.github\dependabot.yaml 25 | ..\.github\pull_request_template.yml = ..\.github\pull_request_template.yml 26 | EndProjectSection 27 | EndProject 28 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{4ED0DC8D-A438-4FFC-AD81-98A7E3F3634E}" 29 | ProjectSection(SolutionItems) = preProject 30 | ..\.github\workflows\build_tests.yml = ..\.github\workflows\build_tests.yml 31 | ..\.github\workflows\execute_test_for_model.yml = ..\.github\workflows\execute_test_for_model.yml 32 | ..\.github\workflows\publish.yml = ..\.github\workflows\publish.yml 33 | EndProjectSection 34 | EndProject 35 | Global 36 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 37 | Debug|Any CPU = Debug|Any CPU 38 | Release|Any CPU = Release|Any CPU 39 | EndGlobalSection 40 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 41 | {0FB959FE-C974-4D07-8D4E-BC7127FC22CE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 42 | {0FB959FE-C974-4D07-8D4E-BC7127FC22CE}.Debug|Any CPU.Build.0 = Debug|Any CPU 43 | {0FB959FE-C974-4D07-8D4E-BC7127FC22CE}.Release|Any CPU.ActiveCfg = Release|Any CPU 44 | {0FB959FE-C974-4D07-8D4E-BC7127FC22CE}.Release|Any CPU.Build.0 = Release|Any CPU 45 | {830AF03C-A298-44E3-96C6-6E1A6EAE0C5E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 46 | {830AF03C-A298-44E3-96C6-6E1A6EAE0C5E}.Debug|Any CPU.Build.0 = Debug|Any CPU 47 | {830AF03C-A298-44E3-96C6-6E1A6EAE0C5E}.Release|Any CPU.ActiveCfg = Release|Any CPU 48 | {830AF03C-A298-44E3-96C6-6E1A6EAE0C5E}.Release|Any CPU.Build.0 = Release|Any CPU 49 | {ECA0008E-81A8-41CE-9896-08F22A5CCBAF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 50 | {ECA0008E-81A8-41CE-9896-08F22A5CCBAF}.Debug|Any CPU.Build.0 = Debug|Any CPU 51 | {ECA0008E-81A8-41CE-9896-08F22A5CCBAF}.Release|Any CPU.ActiveCfg = Release|Any CPU 52 | {ECA0008E-81A8-41CE-9896-08F22A5CCBAF}.Release|Any CPU.Build.0 = Release|Any CPU 53 | {4A993934-CA43-4EB9-A3C6-C0976D705A33}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 54 | {4A993934-CA43-4EB9-A3C6-C0976D705A33}.Debug|Any CPU.Build.0 = Debug|Any CPU 55 | {4A993934-CA43-4EB9-A3C6-C0976D705A33}.Release|Any CPU.ActiveCfg = Release|Any CPU 56 | {4A993934-CA43-4EB9-A3C6-C0976D705A33}.Release|Any CPU.Build.0 = Release|Any CPU 57 | EndGlobalSection 58 | GlobalSection(SolutionProperties) = preSolution 59 | HideSolutionNode = FALSE 60 | EndGlobalSection 61 | GlobalSection(NestedProjects) = preSolution 62 | {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} = {8EC462FD-D22E-90A8-E5CE-7E832BA40C5D} 63 | {4ED0DC8D-A438-4FFC-AD81-98A7E3F3634E} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} 64 | EndGlobalSection 65 | GlobalSection(ExtensibilityGlobals) = postSolution 66 | SolutionGuid = {2E9C15E3-18FB-422C-B2F0-B8BD86672D04} 67 | EndGlobalSection 68 | EndGlobal 69 | -------------------------------------------------------------------------------- /tests/SemanticKernel.Agents.DatabaseAgent.Tests/AgentFactoryTest.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Configuration; 2 | using Microsoft.Extensions.Logging; 3 | using Microsoft.SemanticKernel; 4 | using Microsoft.SemanticKernel.Agents; 5 | using Microsoft.SemanticKernel.ChatCompletion; 6 | using Microsoft.SemanticKernel.Connectors.OpenAI; 7 | using Microsoft.SemanticKernel.Embeddings; 8 | using SemanticKernel.Agents.DatabaseAgent.MCPServer.Internals; 9 | using SQLitePCL; 10 | using System.Text.Json; 11 | 12 | [assembly: Parallelizable(ParallelScope.None)] 13 | 14 | #pragma warning disable SKEXP0010 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. 15 | #pragma warning disable SKEXP0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. 16 | 17 | namespace SemanticKernel.Agents.DatabaseAgent.Tests 18 | { 19 | public class AgentFactoryTest 20 | { 21 | private Kernel kernel; 22 | private Agent agent; 23 | private IConfiguration configuration; 24 | 25 | [OneTimeSetUp] 26 | public void Setup() 27 | { 28 | Batteries.Init(); 29 | 30 | configuration = new ConfigurationBuilder() 31 | .AddJsonFile("appsettings.json") 32 | .AddJsonFile("appsettings.Development.json", optional: true) 33 | .AddUserSecrets() 34 | .AddEnvironmentVariables() 35 | .Build(); 36 | 37 | var loggerFactory = LoggerFactory.Create(builder => 38 | { 39 | builder.AddConsole(); 40 | builder.SetMinimumLevel(LogLevel.Debug); 41 | }); 42 | 43 | this.kernel = AgentKernelFactory.ConfigureKernel(configuration, loggerFactory); 44 | 45 | this.agent = DatabaseAgentFactory.CreateAgentAsync(kernel, loggerFactory).Result; 46 | } 47 | 48 | [Test, Order(0)] 49 | public void AgentFactoryCanCreateANewAgent() 50 | { 51 | // Arrange 52 | 53 | // Test 54 | 55 | // Assert 56 | Assert.That(agent, Is.Not.Null); 57 | } 58 | 59 | [Order(1)] 60 | [TestCase("Counts the number of orders placed by each customer in the top 5", 61 | $""" 62 | | CustomerID | OrderCount | 63 | | --- | --- | 64 | | BSBEV | 210 | 65 | | RICAR | 203 | 66 | | LILAS | 203 | 67 | | GOURL | 202 | 68 | | PRINI | 200 | 69 | | 70 | """)] 71 | [TestCase("Compute the units sold for each of the top 5 products by aggregating the quantity values from the order details", 72 | $""" 73 | | ProductName | TotalUnitsSold | 74 | | --- | --- | 75 | | Louisiana Hot Spiced Okra | 206213 | 76 | | Sir Rodney's Marmalade | 205637 | 77 | | Teatime Chocolate Biscuits | 205487 | 78 | | Sirop d'érable | 205005 | 79 | | Gumbär Gummibärchen | 204761 | 80 | | 81 | """)] 82 | [TestCase("Calculate the average unit price of products grouped by their category.", 83 | $""" 84 | | CategoryName | average_unit_price | 85 | | --- | --- | 86 | | Beverages | 37.979166666666664 | 87 | | Condiments | 23.0625 | 88 | | Confections | 25.16 | 89 | | Dairy Products | 28.73 | 90 | | Grains/Cereals | 20.25 | 91 | | Meat/Poultry | 54.00666666666667 | 92 | | Produce | 32.37 | 93 | | Seafood | 20.6825 | 94 | | 95 | """)] 96 | [TestCase("Count how many orders each employee has processed", 97 | $""" 98 | | EmployeeID | OrderCount | 99 | | --- | --- | 100 | | 1 | 1846 | 101 | | 2 | 1771 | 102 | | 3 | 1846 | 103 | | 4 | 1908 | 104 | | 5 | 1804 | 105 | | 6 | 1754 | 106 | | 7 | 1789 | 107 | | 8 | 1798 | 108 | | 9 | 1766 | 109 | | 110 | """)] 111 | [TestCase("List the five products with the highest unit price", 112 | $""" 113 | | ProductName | UnitPrice | 114 | | --- | --- | 115 | | Côte de Blaye | 263.5 | 116 | | Thüringer Rostbratwurst | 123.79 | 117 | | Mishi Kobe Niku | 97 | 118 | | Sir Rodney's Marmalade | 81 | 119 | | Carnarvon Tigers | 62.5 | 120 | | 121 | """)] 122 | [TestCase("Counts how many sales were made with and without a discount", 123 | $""" 124 | | sales_with_discount | sales_without_discount | 125 | | --- | --- | 126 | | 838 | 608445 | 127 | | 128 | """)] 129 | [TestCase("Calculate the total revenue generated from the product 'Chai'", 130 | $""" 131 | | total_revenue | 132 | | --- | 133 | | 3632174.1 | 134 | | 135 | """)] 136 | [TestCase("Counts how many products each supplier provides", 137 | $""" 138 | | CompanyName | ProductCount | 139 | | --- | --- | 140 | | Aux joyeux ecclésiastiques | 2 | 141 | | Bigfoot Breweries | 3 | 142 | | Cooperativa de Quesos 'Las Cabras' | 2 | 143 | | Escargots Nouveaux | 1 | 144 | | Exotic Liquids | 3 | 145 | | Formaggi Fortini s.r.l. | 3 | 146 | | Forêts d'érables | 2 | 147 | | G'day, Mate | 3 | 148 | | Gai pâturage | 2 | 149 | | Grandma Kelly's Homestead | 3 | 150 | | Heli Süßwaren GmbH & Co. KG | 3 | 151 | | Karkki Oy | 3 | 152 | | Leka Trading | 3 | 153 | | Lyngbysild | 2 | 154 | | Ma Maison | 2 | 155 | | Mayumi's | 3 | 156 | | New England Seafood Cannery | 2 | 157 | | New Orleans Cajun Delights | 4 | 158 | | Nord-Ost-Fisch Handelsgesellschaft mbH | 1 | 159 | | Norske Meierier | 3 | 160 | | PB Knäckebröd AB | 2 | 161 | | Pasta Buttini s.r.l. | 2 | 162 | | Pavlova, Ltd. | 5 | 163 | | Plutzer Lebensmittelgroßmärkte AG | 5 | 164 | | Refrescos Americanas LTDA | 1 | 165 | | Specialty Biscuits, Ltd. | 4 | 166 | | Svensk Sjöföda AB | 3 | 167 | | Tokyo Traders | 3 | 168 | | Zaanse Snoepfabriek | 2 | 169 | | 170 | """)] 171 | [TestCase("List employees and their manager", 172 | $""" 173 | | EmployeeName | ManagerName | 174 | | --- | --- | 175 | | Nancy Davolio | Andrew Fuller | 176 | | Andrew Fuller | | 177 | | Janet Leverling | Andrew Fuller | 178 | | Margaret Peacock | Andrew Fuller | 179 | | Steven Buchanan | Andrew Fuller | 180 | | Michael Suyama | Steven Buchanan | 181 | | Robert King | Steven Buchanan | 182 | | Laura Callahan | Andrew Fuller | 183 | | Anne Dodsworth | Steven Buchanan | 184 | | 185 | """)] 186 | [TestCase("Lists the top 5 orders with the most expensive shipping fees", 187 | $""" 188 | | OrderID | Freight | 189 | | --- | --- | 190 | | 13460 | 587 | 191 | | 17596 | 580.75 | 192 | | 13372 | 574.5 | 193 | | 13466 | 568.25 | 194 | | 25679 | 561.25 | 195 | | 196 | """)] 197 | public async Task AgentCanAnswerToDataAsync(string question, string expectedAnswer) 198 | { 199 | // Arrange 200 | var evaluatorKernel = kernel.Clone(); 201 | 202 | var embeddingTextGenerator = evaluatorKernel.GetRequiredService(); 203 | 204 | // Test 205 | var responses = agent.InvokeAsync([new ChatMessageContent { Content = question, Role = AuthorRole.User }], thread: null) 206 | .ConfigureAwait(false); 207 | 208 | // Assert 209 | await foreach (var response in responses) 210 | { 211 | Assert.That(response.Message, Is.Not.Null); 212 | 213 | var evaluator = KernelFunctionFactory.CreateFromPrompt($$$""" 214 | Evaluate the semantic similarity between the expected answer and the actual response to the given question. 215 | 216 | # Guidelines 217 | 218 | - The similarity should be assessed on a scale from 0 to 1, where 0 indicates no similarity and 1 indicates identical meaning. 219 | - Consider both content accuracy and completeness when evaluating, rather than superficial linguistic differences. 220 | - Do not provide any explanation or additional commentary in the output. 221 | 222 | # Steps 223 | 224 | 1. Compare the *Source of Truth* (expected answer) with the *Second Sentence* (actual response). 225 | 2. Analyze the alignment between the meanings conveyed in both sentences, focusing on: 226 | - **Accuracy:** Whether the key facts and information in the expected answer are present in the response. 227 | - **Completeness:** Whether the response adequately covers all major elements provided in the expected answer. 228 | - **Meaning:** Whether the ideas and intended message of the expected answer are preserved. 229 | 3. Assign a numerical score between 0 and 1 based on the degree of similarity: 230 | - **1.0:** Perfect match; the response is semantically identical to the expected answer. 231 | - **0.8 - 0.99:** High similarity; the response conveys almost the exact message but with slight differences in phrasing or some minor omissions. 232 | - **0.5 - 0.79:** Moderate similarity; the response captures general meaning but has significant missing or incorrect components. 233 | - **0.1 - 0.49:** Low similarity; only a small portion of the response aligns with the expected answer. 234 | - **0:** No similarity; the response does not align at all with the expected answer or contradicts it entirely. 235 | 236 | # Output Format 237 | 238 | Return the similarity score as a JSON document in the following format: 239 | 240 | ```json 241 | { 242 | "score": SCORE 243 | } 244 | ``` 245 | 246 | Replace "SCORE" with the evaluated numeric value between 0 and 1. 247 | 248 | # Examples 249 | 250 | **Input:** 251 | Question: What is the capital of France? 252 | Source of truth: Paris. 253 | Second sentence: The capital of France is Paris. 254 | 255 | **Output:** 256 | ```json 257 | { 258 | "score": 1.0 259 | } 260 | ``` 261 | 262 | --- 263 | 264 | **Input:** 265 | Question: What is the capital of France? 266 | Source of truth: Paris. 267 | Second sentence: It might be Rome. 268 | 269 | **Output:** 270 | ```json 271 | { 272 | "score": 0.0 273 | } 274 | ``` 275 | 276 | --- 277 | 278 | **Input:** 279 | Question: What is the process of photosynthesis? 280 | Source of truth: Photosynthesis is a process used by plants to convert sunlight, carbon dioxide, and water into glucose and oxygen. 281 | Second sentence: Photosynthesis is how plants use sunlight to make food, producing oxygen in the process. 282 | 283 | **Output:** 284 | ```json 285 | { 286 | "score": 0.8 287 | } 288 | ``` 289 | 290 | # Notes 291 | 292 | - The evaluation should remain unbiased and focused solely on the semantic similarity between the expected and provided sentences. 293 | - Avoid factoring in linguistic style or grammar unless it changes the meaning. 294 | 295 | # Let's evaluate the similarity between the expected answer and the actual response. 296 | 297 | ## Question: {{{question}}} 298 | ## Source of truth 299 | {{{expectedAnswer}}} 300 | ## Second sentence: 301 | {{{response.Message.Content}}} 302 | """, new OpenAIPromptExecutionSettings 303 | { 304 | MaxTokens = 4096, 305 | Temperature = .1E-9, 306 | TopP = .1E-9, 307 | #pragma warning disable SKEXP0010 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. 308 | ResponseFormat = "json_object" 309 | #pragma warning restore SKEXP0010 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. 310 | }, functionName: AgentPromptConstants.WriteSQLQuery); 311 | 312 | 313 | var evaluationResult = await evaluator.InvokeAsync(kernel) 314 | .ConfigureAwait(false); 315 | 316 | var evaluation = JsonSerializer.Deserialize(evaluationResult.GetValue()!); 317 | 318 | Console.WriteLine($"Score: {evaluation!.Score}"); 319 | Console.WriteLine($"Answer: {response.Message}"); 320 | Console.WriteLine($"Expected: {expectedAnswer}"); 321 | 322 | if (evaluation.Score < .8) 323 | { 324 | Assert.Inconclusive("The answer is not similar enough to the expected answer."); 325 | } 326 | } 327 | } 328 | } 329 | } 330 | #pragma warning restore SKEXP0010 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. 331 | #pragma warning restore SKEXP0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. 332 | -------------------------------------------------------------------------------- /tests/SemanticKernel.Agents.DatabaseAgent.Tests/Evaluation.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace SemanticKernel.Agents.DatabaseAgent.Tests 4 | { 5 | internal class Evaluation 6 | { 7 | [JsonPropertyName("score")] 8 | public float Score { get; set; } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /tests/SemanticKernel.Agents.DatabaseAgent.Tests/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "SemanticKernel.Agents.DatabaseAgent.Tests": { 4 | "commandName": "Project", 5 | "environmentVariables": { 6 | "DATABASE__CONNECTIONSTRING": "$", 7 | "SERVICES__GPT4OMINI__TYPE": "AzureOpenAI", 8 | "SERVICES__GPT4OMINI__ENDPOINT": "$", 9 | "SERVICES__GPT4OMINI__APIKEY": "$", 10 | "SERVICES__GPT4OMINI__DEPLOYMENTNAME": "gpt-4o-mini", 11 | "SERVICES__TEXTEMBEDDINGADA002___TYPE": "AzureOpenAI", 12 | "SERVICES__TEXTEMBEDDINGADA002___ENDPOINT": "$", 13 | "SERVICES__TEXTEMBEDDINGADA002___APIKEY": "$", 14 | "SERVICES__TEXTEMBEDDINGADA002___DEPLOYMENTNAME": "text-embedding-ada-002", 15 | "KERNEL__COMPLETION": "GPT4OMINI", 16 | "KERNEL__EMBEDDING": "TEXTEMBEDDINGADA002", 17 | "MEMORY__KIND": "volatile" 18 | } 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /tests/SemanticKernel.Agents.DatabaseAgent.Tests/SemanticKernel.Agents.DatabaseAgent.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | false 9 | true 10 | 8ff85c4f-a80f-4a87-aca9-33baebf99af3 11 | 12 | 13 | 14 | 15 | all 16 | runtime; build; native; contentfiles; analyzers; buildtransitive 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | all 30 | runtime; build; native; contentfiles; analyzers; buildtransitive 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | PreserveNewest 50 | 51 | 52 | PreserveNewest 53 | 54 | 55 | PreserveNewest 56 | 57 | 58 | Always 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /tests/SemanticKernel.Agents.DatabaseAgent.Tests/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Agent": { 3 | "QualityAssurance": { 4 | "EnableQueryRelevancyFilter": false, 5 | "QueryRelevancyThreshold": 0.85 6 | } 7 | }, 8 | "Database": { 9 | "Provider": "sqlite", 10 | "ConnectionString": "Data Source=northwind.db;Mode=ReadWrite" 11 | }, 12 | "Memory": { 13 | "Kind": "qdrant", 14 | "Host": "localhost", 15 | "Port": 6334, 16 | "Dimensions": 768 17 | }, 18 | "Kernel": { 19 | "Completion": "qwen2.5-coder", 20 | "Embedding": "nomic-embed-text" 21 | }, 22 | "Services": { 23 | "qwen2.5-coder": { 24 | "Type": "Ollama", 25 | "Endpoint": "http://xxx", 26 | "ModelId": "qwen2.5-coder:latest" 27 | }, 28 | "nomic-embed-text": { 29 | "Type": "Ollama", 30 | "Endpoint": "http://xxx", 31 | "ModelId": "nomic-embed-text:latest" 32 | }, 33 | "gpt-4o-mini": { 34 | "Type": "AzureOpenAI", 35 | "Endpoint": "https://xxx.openai.azure.com/", 36 | "APIKey": "xxx", 37 | "Deployment": "gpt-4o-mini" 38 | }, 39 | "text-embedding-ada-002": { 40 | "Type": "AzureOpenAI", 41 | "Endpoint": "https://xxx.azure.com/", 42 | "APIKey": "xxx", 43 | "Deployment": "text-embedding-ada-002" 44 | } 45 | }, 46 | "Logging": { 47 | "LogLevel": { 48 | "Default": "Trace", 49 | "Microsoft": "Warning", 50 | "Microsoft.Hosting.Lifetime": "Information" 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /tests/SemanticKernel.Agents.DatabaseAgent.Tests/northwind.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kbeaugrand/SemanticKernel.Agents.DatabaseAgent/6e43cceb85ba989aaa91918295b764da2705bee7/tests/SemanticKernel.Agents.DatabaseAgent.Tests/northwind.db --------------------------------------------------------------------------------