Skip to content

Commit f8e1e56

Browse files
authored
.Net: Structured Data Plugin - Query and CRUD Operations (#10858)
### Motivation and Context - Resolves Partially #10099 - Resolves #10733 - Resolves #10734 Developers need a clear example of how to integrate database operations with Semantic Kernel for AI-powered data interactions. The StructuredDataConnector demo provides a practical implementation showing how to leverage SK's plugin architecture with Entity Framework Core for structured data operations. ### Description - Implemented `StructuredDataConnector` demo showcasing database-AI integration - Added `StructuredDataService<TContext>` for handling basic CRUD operations - Created `StructuredDataPluginFactory` for easy plugin creation - Included example using OpenAI chat completion with function calling - Added interactive mode for testing database queries through natural language The demo provides a reference implementation for developers wanting to integrate database operations with SK's AI capabilities.
1 parent 15abebb commit f8e1e56

17 files changed

+1541
-0
lines changed
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
---
2+
status: proposed
3+
contact: rogerbarreto
4+
date: 2025-03-07
5+
deciders: rogerbarreto, markwallace, dmytrostruk, westey-m, sergeymenshykh
6+
---
7+
8+
# Structured Data Plugin Implementation in Semantic Kernel
9+
10+
## Context and Problem Statement
11+
12+
Modern AI applications often need to interact with structured data in databases while leveraging LLM capabilities. As Semantic Kernel's core focuses on AI orchestration, we need a standardized approach to integrate database operations with AI capabilities. This ADR proposes an experimental StructuredDataConnector as an initial solution for database-AI integration, focusing on basic CRUD operations and simple querying.
13+
14+
## Decision Drivers
15+
16+
- Need for initial database integration pattern with SK
17+
- Requirement for basic composable AI and database operations
18+
- Alignment with SK's plugin architecture
19+
- Ability to validate the approach through real-world usage
20+
- Support for strongly-typed schema validation
21+
- Consistent JSON formatting for AI interactions
22+
23+
## Key Benefits
24+
25+
1. **Plugin-Based Architecture**
26+
27+
- Aligns with SK's plugin architecture
28+
- Supports extension methods for common operations
29+
- Leverages KernelJsonSchema for type safety
30+
31+
2. **Structured Data Operations**
32+
33+
- CRUD operations with schema validation
34+
- JSON-based interactions with proper formatting
35+
- Type-safe database operations
36+
37+
3. **Integration Features**
38+
39+
- Built-in JSON schema generation
40+
- Automatic type conversion
41+
- Pretty-printed JSON for better AI interactions
42+
43+
## Implementation Details
44+
45+
The implementation includes:
46+
47+
1. Core Components:
48+
49+
- `StructuredDataService<TContext>`: Base service for database operations
50+
- `StructuredDataServiceExtensions`: Extension methods for CRUD operations
51+
- `StructuredDataPluginFactory`: Factory for creating SK plugins
52+
- Integration with `KernelJsonSchema` for type validation
53+
54+
2. Key Features:
55+
56+
- Automatic schema generation from entity types
57+
- Properly formatted JSON responses
58+
- Extension-based architecture for maintainability
59+
- Support for Entity Framework Core
60+
61+
3. Usage Example:
62+
63+
```csharp
64+
var service = new StructuredDataService<ApplicationDbContext>(dbContext);
65+
var plugin = StructuredDataPluginFactory.CreateStructuredDataPlugin<ApplicationDbContext, MyEntity>(
66+
service,
67+
operations: StructuredDataOperation.Default);
68+
```
69+
70+
## Decision Outcome
71+
72+
Chosen option: TBD:
73+
74+
1. Provides standardized database integration
75+
2. Leverages SK's schema validation capabilities
76+
3. Supports proper JSON formatting for AI interactions
77+
4. Maintains type safety through generated schemas
78+
5. Follows established SK patterns and principles
79+
80+
## More Information
81+
82+
This is an experimental approach that will evolve based on community feedback.

dotnet/Directory.Packages.props

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,11 @@
2020
<PackageVersion Include="Azure.Identity" Version="1.13.2" />
2121
<PackageVersion Include="Azure.Monitor.OpenTelemetry.Exporter" Version="1.3.0" />
2222
<PackageVersion Include="Azure.Search.Documents" Version="11.6.0" />
23+
<PackageVersion Include="Community.OData.Linq" Version="2.1.0" />
2324
<PackageVersion Include="Dapr.Actors" Version="1.14.0" />
2425
<PackageVersion Include="Dapr.Actors.AspNetCore" Version="1.14.0" />
2526
<PackageVersion Include="Dapr.AspNetCore" Version="1.14.0" />
27+
<PackageVersion Include="EntityFramework" Version="6.5.1" />
2628
<PackageVersion Include="FastBertTokenizer" Version="1.0.28" />
2729
<PackageVersion Include="Google.Apis.Auth" Version="1.69.0" />
2830
<PackageVersion Include="mcpdotnet" Version="1.0.1.3" />

dotnet/SK-dotnet.sln

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -485,6 +485,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Agents.Bedrock", "src\Agent
485485
EndProject
486486
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ModelContextProtocol", "samples\Demos\ModelContextProtocol\ModelContextProtocol.csproj", "{B16AC373-3DA8-4505-9510-110347CD635D}"
487487
EndProject
488+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StructuredDataPlugin", "samples\Demos\StructuredDataPlugin\StructuredDataPlugin.csproj", "{BEDAC050-016A-46F4-9173-339C46DFD3ED}"
489+
EndProject
490+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Plugins.StructuredData.EntityFramework", "src\Plugins\Plugins.StructuredData.EntityFramework\Plugins.StructuredData.EntityFramework.csproj", "{0C81C377-3CDC-46A8-BED1-4B50BDA2202E}"
491+
EndProject
488492
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SqlServerIntegrationTests", "src\VectorDataIntegrationTests\SqlServerIntegrationTests\SqlServerIntegrationTests.csproj", "{A5E6193C-8431-4C6E-B674-682CB41EAA0C}"
489493
EndProject
490494
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PineconeIntegrationTests", "src\VectorDataIntegrationTests\PineconeIntegrationTests\PineconeIntegrationTests.csproj", "{E9A74E0C-BC02-4DDD-A487-89847EDF8026}"
@@ -1330,6 +1334,18 @@ Global
13301334
{B16AC373-3DA8-4505-9510-110347CD635D}.Publish|Any CPU.Build.0 = Debug|Any CPU
13311335
{B16AC373-3DA8-4505-9510-110347CD635D}.Release|Any CPU.ActiveCfg = Release|Any CPU
13321336
{B16AC373-3DA8-4505-9510-110347CD635D}.Release|Any CPU.Build.0 = Release|Any CPU
1337+
{BEDAC050-016A-46F4-9173-339C46DFD3ED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
1338+
{BEDAC050-016A-46F4-9173-339C46DFD3ED}.Debug|Any CPU.Build.0 = Debug|Any CPU
1339+
{BEDAC050-016A-46F4-9173-339C46DFD3ED}.Publish|Any CPU.ActiveCfg = Debug|Any CPU
1340+
{BEDAC050-016A-46F4-9173-339C46DFD3ED}.Publish|Any CPU.Build.0 = Debug|Any CPU
1341+
{BEDAC050-016A-46F4-9173-339C46DFD3ED}.Release|Any CPU.ActiveCfg = Release|Any CPU
1342+
{BEDAC050-016A-46F4-9173-339C46DFD3ED}.Release|Any CPU.Build.0 = Release|Any CPU
1343+
{0C81C377-3CDC-46A8-BED1-4B50BDA2202E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
1344+
{0C81C377-3CDC-46A8-BED1-4B50BDA2202E}.Debug|Any CPU.Build.0 = Debug|Any CPU
1345+
{0C81C377-3CDC-46A8-BED1-4B50BDA2202E}.Publish|Any CPU.ActiveCfg = Publish|Any CPU
1346+
{0C81C377-3CDC-46A8-BED1-4B50BDA2202E}.Publish|Any CPU.Build.0 = Publish|Any CPU
1347+
{0C81C377-3CDC-46A8-BED1-4B50BDA2202E}.Release|Any CPU.ActiveCfg = Release|Any CPU
1348+
{0C81C377-3CDC-46A8-BED1-4B50BDA2202E}.Release|Any CPU.Build.0 = Release|Any CPU
13331349
{A5E6193C-8431-4C6E-B674-682CB41EAA0C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
13341350
{A5E6193C-8431-4C6E-B674-682CB41EAA0C}.Debug|Any CPU.Build.0 = Debug|Any CPU
13351351
{A5E6193C-8431-4C6E-B674-682CB41EAA0C}.Publish|Any CPU.ActiveCfg = Debug|Any CPU
@@ -1523,6 +1539,8 @@ Global
15231539
{DAD5FC6A-8CA0-43AC-87E1-032DFBD6B02A} = {3F260A77-B6C9-97FD-1304-4B34DA936CF4}
15241540
{8C658E1E-83C8-4127-B8BF-27A638A45DDD} = {6823CD5E-2ABE-41EB-B865-F86EC13F0CF9}
15251541
{B16AC373-3DA8-4505-9510-110347CD635D} = {5D4C0700-BBB5-418F-A7B2-F392B9A18263}
1542+
{BEDAC050-016A-46F4-9173-339C46DFD3ED} = {5D4C0700-BBB5-418F-A7B2-F392B9A18263}
1543+
{0C81C377-3CDC-46A8-BED1-4B50BDA2202E} = {D6D598DF-C17C-46F4-B2B9-CDE82E2DE132}
15261544
{A5E6193C-8431-4C6E-B674-682CB41EAA0C} = {4F381919-F1BE-47D8-8558-3187ED04A84F}
15271545
{E9A74E0C-BC02-4DDD-A487-89847EDF8026} = {4F381919-F1BE-47D8-8558-3187ED04A84F}
15281546
EndGlobalSection
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// Copyright (c) Microsoft. All rights reserved.
2+
3+
using System.Data.Entity;
4+
using Microsoft.Extensions.Configuration;
5+
6+
namespace StructuredDataPlugin;
7+
8+
/// <summary>
9+
/// Represents a database context for the structured data plugin demo.
10+
/// Inherits from Entity Framework's DbContext to provide database access and management.
11+
/// </summary>
12+
internal sealed class ApplicationDbContext : DbContext
13+
{
14+
/// <summary>
15+
/// Initializes a new instance of the <see cref="ApplicationDbContext"/> class using configuration settings.
16+
/// </summary>
17+
/// <param name="config">The configuration object containing connection string information.</param>
18+
/// <remarks>
19+
/// The connection string is retrieved from the configuration using the pattern "ConnectionStrings:ApplicationDbContext".
20+
/// </remarks>
21+
public ApplicationDbContext(IConfiguration config) :
22+
base(config[$"ConnectionStrings:{nameof(ApplicationDbContext)}"]!)
23+
{
24+
}
25+
26+
/// <summary>
27+
/// Initializes a new instance of the <see cref="ApplicationDbContext"/> class using a direct connection string.
28+
/// </summary>
29+
/// <param name="connectionString">The connection string to the database.</param>
30+
public ApplicationDbContext(string connectionString)
31+
: base(connectionString)
32+
{
33+
}
34+
35+
/// <summary>
36+
/// Configures the database model and its relationships.
37+
/// </summary>
38+
/// <param name="modelBuilder">The builder being used to construct the model for this context.</param>
39+
/// <remarks>
40+
/// This method:
41+
/// - Disables database initialization
42+
/// - Maps the Product entity to the "Products" table
43+
/// - Calls the base configuration
44+
/// </remarks>
45+
protected override void OnModelCreating(DbModelBuilder modelBuilder)
46+
{
47+
Database.SetInitializer<ApplicationDbContext>(null);
48+
modelBuilder.Entity<Product>().ToTable("Products");
49+
base.OnModelCreating(modelBuilder);
50+
}
51+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// Copyright (c) Microsoft. All rights reserved.
2+
3+
using System.ComponentModel;
4+
using System.ComponentModel.DataAnnotations;
5+
using System.ComponentModel.DataAnnotations.Schema;
6+
7+
namespace StructuredDataPlugin;
8+
9+
/// <summary>
10+
/// Represents a product entity in the database.
11+
/// </summary>
12+
public sealed class Product
13+
{
14+
/// <summary>
15+
/// The unique identifier for the product.
16+
/// </summary>
17+
[Description("The unique identifier for the product.")]
18+
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
19+
public Guid? Id { get; set; }
20+
21+
/// <summary>
22+
/// The description of the product.
23+
/// </summary>
24+
[Description("The name of the product.")]
25+
public string? Name { get; set; }
26+
27+
/// <summary>
28+
/// The price of the product.
29+
/// </summary>
30+
[Description("The price of the product in USD.")]
31+
public decimal? Price { get; set; }
32+
33+
/// <summary>
34+
/// The date the product was created.
35+
/// </summary>
36+
[Description("The date the product was created")]
37+
[DatabaseGenerated(DatabaseGeneratedOption.Computed)]
38+
public DateTime? DateCreated { get; set; }
39+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
// Copyright (c) Microsoft. All rights reserved.
2+
3+
using Microsoft.Extensions.Configuration;
4+
using Microsoft.Extensions.DependencyInjection;
5+
using Microsoft.SemanticKernel;
6+
using Microsoft.SemanticKernel.Connectors.OpenAI;
7+
8+
namespace StructuredDataPlugin;
9+
10+
internal sealed class Program
11+
{
12+
internal static async Task Main(string[] args)
13+
{
14+
var serviceCollection = new ServiceCollection()
15+
.AddSingleton<IConfiguration>((sp) => new ConfigurationBuilder()
16+
.AddJsonFile("appsettings.json", optional: true)
17+
.AddEnvironmentVariables()
18+
.AddUserSecrets<Program>()
19+
.Build())
20+
.AddTransient<ApplicationDbContext>()
21+
.AddTransient<StructuredDataService<ApplicationDbContext>>();
22+
23+
var serviceProvider = serviceCollection.BuildServiceProvider();
24+
var config = serviceProvider.GetRequiredService<IConfiguration>();
25+
using var structuredDataService = serviceProvider.GetRequiredService<StructuredDataService<ApplicationDbContext>>();
26+
27+
// Create kernel builder and add OpenAI
28+
var kernelBuilder = Kernel.CreateBuilder()
29+
.AddOpenAIChatCompletion(
30+
modelId: "gpt-4o",
31+
apiKey: config["OpenAI:ApiKey"]!);
32+
33+
// Add the database plugin using the factory with default operations
34+
var databasePlugin = StructuredDataPluginFactory.CreateStructuredDataPlugin<ApplicationDbContext, Product>(
35+
structuredDataService);
36+
37+
kernelBuilder.Plugins.Add(databasePlugin);
38+
39+
// Build the kernel and add the plugin
40+
var kernel = kernelBuilder.Build();
41+
42+
// Create settings for function calling
43+
var settings = new OpenAIPromptExecutionSettings
44+
{
45+
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto()
46+
};
47+
48+
// Example 1: Inserting new records
49+
Console.WriteLine("Creating a new record...");
50+
var insertPrompt = "Insert a new product with name 'Book' and price 29.99";
51+
var insertResult = await kernel.InvokePromptAsync(insertPrompt, new(settings));
52+
Console.WriteLine($"Insert Result: {insertResult}");
53+
54+
// Example 2: Querying data
55+
Console.WriteLine("\nQuerying specific records...");
56+
var queryPrompt = "Find all products under $50";
57+
var queryResult = await kernel.InvokePromptAsync(queryPrompt, new(settings));
58+
Console.WriteLine($"Query Result: {queryResult}");
59+
60+
// Example 3: Updating records
61+
Console.WriteLine("\nUpdating a record...");
62+
var updatePrompt = "Update the price of 'Book' to 39.99 and keep its name";
63+
var updateResult = await kernel.InvokePromptAsync(updatePrompt, new(settings));
64+
Console.WriteLine($"Update Result: {updateResult}");
65+
66+
// Example 3: Updating records
67+
Console.WriteLine("\nDeleting a record...");
68+
var deletePrompt = "Delete the product 'Book'";
69+
var deleteResult = await kernel.InvokePromptAsync(deletePrompt, new(settings));
70+
Console.WriteLine($"Delete Result: {deleteResult}");
71+
72+
// Example 4: Interactive chat-like interaction
73+
Console.WriteLine("\nStarting interactive mode (type 'exit' to quit)");
74+
Console.WriteLine("You can try queries like:");
75+
Console.WriteLine("- Find all products under $50");
76+
Console.WriteLine("- Insert a new product with name 'Table' and price 19.99");
77+
Console.WriteLine("- Update the price of 'Table' to 25.99");
78+
79+
while (true)
80+
{
81+
Console.Write("\nEnter your database query: ");
82+
var userInput = Console.ReadLine();
83+
84+
if (string.IsNullOrEmpty(userInput) || userInput.Equals("exit", StringComparison.OrdinalIgnoreCase))
85+
{
86+
break;
87+
}
88+
89+
var result = await kernel.InvokePromptAsync(userInput, new(settings));
90+
Console.WriteLine($"\nResult: {result}");
91+
}
92+
}
93+
}

0 commit comments

Comments
 (0)