Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions dotnet/samples/Demos/StructuredDataPlugin/Product.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,51 @@ public sealed class Product
[DatabaseGenerated(DatabaseGeneratedOption.Computed)]
public DateTime? DateCreated { get; set; }
}

/// <summary>
/// Information the model may provide when creating a product.
/// </summary>
internal sealed class CreateProductRequest
{
/// <summary>
/// Gets or sets the product name.
/// </summary>
[Description("The name of the product.")]
public string? Name { get; set; }

/// <summary>
/// Gets or sets the product price.
/// </summary>
[Description("The price of the product in USD.")]
public decimal? Price { get; set; }
}

/// <summary>
/// Information the model may provide when updating a product.
/// </summary>
internal sealed class UpdateProductRequest
{
/// <summary>
/// Gets or sets the unique product identifier.
/// </summary>
[Description("The unique identifier of the product to update.")]
public Guid Id { get; set; }

/// <summary>
/// Gets or sets the replacement price.
/// </summary>
[Description("The new price of the product in USD.")]
public decimal? Price { get; set; }
}

/// <summary>
/// Information the model may provide when deleting a product.
/// </summary>
internal sealed class DeleteProductRequest
{
/// <summary>
/// Gets or sets the unique product identifier.
/// </summary>
[Description("The unique identifier of the product to delete.")]
public Guid Id { get; set; }
}
33 changes: 27 additions & 6 deletions dotnet/samples/Demos/StructuredDataPlugin/Program.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// Copyright (c) Microsoft. All rights reserved.

using System.Data.Entity;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.SemanticKernel;
Expand Down Expand Up @@ -30,9 +31,29 @@ internal static async Task Main(string[] args)
modelId: "gpt-4o",
apiKey: config["OpenAI:ApiKey"]!);

// Add the database plugin using the factory with default operations
var databasePlugin = StructuredDataPluginFactory.CreateStructuredDataPlugin<ApplicationDbContext, Product>(
structuredDataService);
var selectFunction = structuredDataService.CreateSelectFunction<ApplicationDbContext, Product>();
var insertFunction = structuredDataService.CreateInsertFunction<ApplicationDbContext, Product, CreateProductRequest>(
input => new Product { Name = input.Name, Price = input.Price });
var updateFunction = structuredDataService.CreateUpdateFunction<ApplicationDbContext, Product, UpdateProductRequest>(
async (input, cancellationToken) =>
{
// This application-owned query is the row-scope boundary; real applications can add tenant or owner filters.
return await structuredDataService.Context.Set<Product>()
.SingleOrDefaultAsync(product => product.Id == input.Id, cancellationToken);
},
(input, product) => product.Price = input.Price);
var deleteFunction = structuredDataService.CreateDeleteFunction<ApplicationDbContext, Product, DeleteProductRequest>(
async (input, cancellationToken) =>
{
// This application-owned query is the row-scope boundary; real applications can add tenant or owner filters.
return await structuredDataService.Context.Set<Product>()
.SingleOrDefaultAsync(product => product.Id == input.Id, cancellationToken);
});

var databasePlugin = KernelPluginFactory.CreateFromFunctions(
"ProductDatabasePlugin",
"Allows approved operations against products in the database",
[selectFunction, insertFunction, updateFunction, deleteFunction]);

kernelBuilder.Plugins.Add(databasePlugin);

Expand All @@ -59,13 +80,13 @@ internal static async Task Main(string[] args)

// Example 3: Updating records
Console.WriteLine("\nUpdating a record...");
var updatePrompt = "Update the price of 'Book' to 39.99 and keep its name";
var updatePrompt = "Find the product named 'Book', then update its price to 39.99 using its ID";
var updateResult = await kernel.InvokePromptAsync(updatePrompt, new(settings));
Console.WriteLine($"Update Result: {updateResult}");

// Example 3: Updating records
Console.WriteLine("\nDeleting a record...");
var deletePrompt = "Delete the product 'Book'";
var deletePrompt = "Find the product named 'Book', then delete it using its ID";
var deleteResult = await kernel.InvokePromptAsync(deletePrompt, new(settings));
Console.WriteLine($"Delete Result: {deleteResult}");

Expand All @@ -74,7 +95,7 @@ internal static async Task Main(string[] args)
Console.WriteLine("You can try queries like:");
Console.WriteLine("- Find all products under $50");
Console.WriteLine("- Insert a new product with name 'Table' and price 19.99");
Console.WriteLine("- Update the price of 'Table' to 25.99");
Console.WriteLine("- Find 'Table', then update its price to 25.99 using its ID");

while (true)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,11 @@ namespace Microsoft.SemanticKernel;
public static StructuredDataOperation Delete { get; } = new("Delete");

/// <summary>
/// The default set of supported operations.
/// The default set of supported operations. Mutation operations require explicit application mapping and scoped resolution.
/// </summary>
public static readonly HashSet<StructuredDataOperation> Default =
[
Select,
Insert,
Update,
Delete
Select
];

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Reflection;

namespace Microsoft.SemanticKernel;

Expand All @@ -18,10 +17,13 @@ public static class StructuredDataPluginFactory
/// <typeparam name="TContext">The type of DbContext to use for database operations.</typeparam>
/// <typeparam name="TEntity">The entity type to perform operations on.</typeparam>
/// <param name="service">The structured data service instance to use.</param>
/// <param name="operations">Optional collection of operations to support. Defaults to StructuredDataOperations.Default if not specified.</param>
/// <param name="operations">Optional collection of operations to support. Defaults to query-only operations if not specified.</param>
/// <param name="description">Optional description for the plugin. If not provided, a default description will be generated.</param>
/// <returns>A KernelPlugin instance configured for the specified entity type and operations.</returns>
/// <exception cref="InvalidOperationException">Thrown if the specified operation is not supported by the service.</exception>
/// <exception cref="NotSupportedException">
/// Thrown if a mutation operation is requested without the explicit mapping and scoped resolution required by the safe mutation APIs.
/// </exception>
public static KernelPlugin CreateStructuredDataPlugin<TContext, TEntity>(
StructuredDataService<TContext> service,
IEnumerable<StructuredDataOperation>? operations = null,
Expand All @@ -30,34 +32,29 @@ public static KernelPlugin CreateStructuredDataPlugin<TContext, TEntity>(
where TEntity : class
{
operations ??= StructuredDataOperation.Default;
description ??= $"Allows CRUD operations against the {typeof(TEntity).Name} entity in the database";
description ??= $"Allows queries against the {typeof(TEntity).Name} entity in the database";

var functions = new List<KernelFunction>();
var extensionsType = typeof(StructuredDataServiceExtensions);

foreach (var operation in operations)
{
// Look for Create{Operation}Function method in the extensions
var methodName = $"Create{operation}Function";
var method = extensionsType.GetMethod(methodName, BindingFlags.Public | BindingFlags.Static);

if (method == null)
if (operation == StructuredDataOperation.Select)
{
throw new InvalidOperationException(
$"Operation '{operation}' is not supported. Extension method '{methodName}' not found.");
functions.Add(service.CreateSelectFunction<TContext, TEntity>());
continue;
}

try
{
var genericMethod = method.MakeGenericMethod(typeof(TContext), typeof(TEntity));
var function = (KernelFunction)genericMethod.Invoke(null, new object?[] { service, null })!;
functions.Add(function);
}
catch (Exception ex) when (ex is not InvalidOperationException)
if (operation == StructuredDataOperation.Insert ||
operation == StructuredDataOperation.Update ||
operation == StructuredDataOperation.Delete)
{
throw new InvalidOperationException(
$"Failed to create function for operation '{operation}': {ex.Message}", ex);
// Mutation functions need application-owned request mapping and row scope, which this convenience factory cannot infer.
throw new NotSupportedException(
$"Operation '{operation}' requires an explicitly created safe mutation function. " +
$"Create it with a separate request type, then compose the plugin with {nameof(KernelPluginFactory)}.");
}

throw new InvalidOperationException($"Operation '{operation}' is not supported.");
}

return KernelPluginFactory.CreateFromFunctions(
Expand Down
Loading
Loading