├── .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 | [](https://github.com/kbeaugrand/SemanticKernel.Agents.DatabaseAgent/actions/workflows/build_test.yml)
4 | [](https://github.com/kbeaugrand/SemanticKernel.Agents.DatabaseAgent/actions/workflows/publish.yml)
5 | [](https://img.shields.io/github/v/release/kbeaugrand/SemanticKernel.Agents.DatabaseAgent)
6 | [](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 |
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
--------------------------------------------------------------------------------