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
85 changes: 78 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,29 +117,91 @@ This ensures that suppressed parameters are handled gracefully without causing r

The library allows you to use any CLR type or object as a plugin without requiring `KernelFunction` attribute. This enables you to create plugins from existing objects or types, making it easier to integrate with existing codebases.

### CLR Type Discovery and Method Selection

When registering CLR types or objects as plugins, the library follows these rules:

1. **Open Generic Methods**: Open generic methods are filtered out and not exposed as plugin functions.

2. **TAP Method Normalization**: Methods following the Task-based Asynchronous Pattern (TAP) are normalized by:
- Removing any `CancellationToken` parameter
- Removing the `Async` suffix from method names
- Preferred selection order is: method with `CancellationToken` parameter, then method with `Async` suffix

3. **Handling Overloaded Methods**: For methods with the same name but different parameters, you can use `FunctionMetadata.OverrideFunctionName` in your metadata provider to give each overload a unique name:

```csharp
// Handling the Random.Next() overloads
public FunctionMetadata? GetFunctionMetadata(KernelPlugin plugin, KernelFunctionMetadata metadata)
{
return plugin.Name switch
{
"RandomPlugin" => metadata.Name switch
{
"Next" when metadata.Parameters.Count == 0 => new FunctionMetadata(metadata.Name)
{
Description = "Returns a non-negative random integer."
},
"Next" when metadata.Parameters.Count == 1 => new FunctionMetadata(metadata.Name)
{
OverrideFunctionName = "NextWithUpperBound",
Description = "Returns a random integer within a specified range.",
Parameters = [
new ParameterMetadata("maxValue")
{
Description = "The exclusive upper bound of the random number returned."
}
]
},
"Next" when metadata.Parameters.Count == 2 => new FunctionMetadata(metadata.Name)
{
OverrideFunctionName = "NextWithRange",
Description = "Returns a random integer within a specified range.",
Parameters = [
new ParameterMetadata("minValue") { Description = "The inclusive lower bound of the random number returned." },
new ParameterMetadata("maxValue") { Description = "The exclusive upper bound of the random number returned." }
]
},
_ => null
},
_ => null
};
}
```

**Sample Type and Metadata Provider:**

```csharp
public class ShortDate
public class DateTimeWrapper
{
public string ToShortDateString()
{
return DateTime.Now.ToShortDateString();
}

public string ToLongDateString()
{
return DateTime.Now.ToLongDateString();
}

public string CurrentTime()
{
return DateTime.Now.ToString("T");
}
}

public class CustomMetadataProvider : IPluginMetadataProvider
{
public PluginMetadata? GetPluginMetadata(KernelPlugin plugin) =>
plugin.Name == "ShortDatePlugin" ? new PluginMetadata
plugin.Name == "DateTimeWrapper" ? new PluginMetadata
{
Description = "This plugin returns date and time information."
} : null;

public FunctionMetadata? GetFunctionMetadata(KernelPlugin plugin, KernelFunctionMetadata metadata) =>
plugin.Name == "ShortDatePlugin" && metadata.Name == "ToShortDateString" ? new FunctionMetadata(metadata.Name)
plugin.Name == "DateTimeWrapper" && metadata.Name == "ToShortDateString" ? new FunctionMetadata(metadata.Name)
{
Description = "Returns the date in short format."
Description = "Returns the current date in short format (MM/dd/yyyy)."
} : null;
}
```
Expand All @@ -149,15 +211,24 @@ public class CustomMetadataProvider : IPluginMetadataProvider
**Usage Example:**

```csharp
kernelBuilder.Plugins.AddFromClrObjectWithMetadata(new ShortDate(), "ShortDatePlugin");
kernelBuilder.Plugins.AddFromClrObjectWithMetadata(new DateTimeWrapper(), "DateTimeWrapper");
```

### CreateFromClrTypeWithMetadata: Using an existing type to create a plugin

**Usage Example:**

```csharp
kernelBuilder.Plugins.AddFromClrTypeWithMetadata<ShortDate>("ShortDatePlugin");
kernelBuilder.Plugins.AddFromClrTypeWithMetadata<DateTimeWrapper>("DateTimeWrapper");
```

### Example with Azure SDK Client

**Usage Example:**

```csharp
var qc = new QueueClient(new Uri("https://your-storage.queue.core.windows.net/your-queue"), new DefaultAzureCredential());
kernelBuilder.Plugins.AddFromClrObjectWithMetadata(qc, "Queue");
```

## Samples
Expand All @@ -168,7 +239,7 @@ Explore the [`samples`](https://github.com/lsiddiquee/SemanticPluginForge/sample

- **[DefaultValue](https://github.com/lsiddiquee/SemanticPluginForge/samples/DefaultValue/)**: Demonstrates advanced parameter handling including suppression, default values, and context-aware metadata. Shows how to override parameter descriptions and ensure parameters are never resolved from context when suppressed.

- **[UseClrType](https://github.com/lsiddiquee/SemanticPluginForge/samples/UseClrType/)**: Shows how to use existing .NET classes as Semantic Kernel plugins without requiring `KernelFunction` attributes. Demonstrates both type-based and object-based registration approaches.
- **[UseClrType](https://github.com/lsiddiquee/SemanticPluginForge/samples/UseClrType/)**: Shows how to use existing .NET classes as Semantic Kernel plugins without requiring `KernelFunction` attributes. Demonstrates multiple plugin types including custom classes, standard .NET classes like `Random`, and direct Azure SDK integration through `QueueClient`. Features function name overriding for handling method overloads.

- **[AzureAiSearchPlugin](https://github.com/lsiddiquee/SemanticPluginForge/samples/AzureAiSearchPlugin/)**: Comprehensive example showing how to create multiple instances of the same plugin class with different metadata configurations for various data sources. Uses mocked data for learning without external dependencies.

Expand Down
6 changes: 4 additions & 2 deletions samples/FunctionLogger.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ public class FunctionLogger : IAutoFunctionInvocationFilter
public async Task OnAutoFunctionInvocationAsync(AutoFunctionInvocationContext context, Func<AutoFunctionInvocationContext, Task> next)
{
var arguments = context.Arguments?.Select(a => $"{a.Key}={a.Value}") ?? Enumerable.Empty<string>();
Console.WriteLine($"Plugin: {context.Function.PluginName}, Function: {context.Function.Name}({string.Join(',', arguments)})");
Console.WriteLine($"[Invoking] Plugin: {context.Function.PluginName}, Function: {context.Function.Name}({string.Join(',', arguments)})");

await next(context);

Console.WriteLine($"[Invoked] Plugin: {context.Function.PluginName}, Function: {context.Function.Name}({string.Join(',', arguments)}) with result: {context.Result}");
}

public async Task OnFunctionInvocationAsync(FunctionInvocationContext context, Func<FunctionInvocationContext, Task> next)
Expand Down
110 changes: 102 additions & 8 deletions samples/UseClrType/CustomMetadataProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,109 @@

public class CustomMetadataProvider : IPluginMetadataProvider
{
public PluginMetadata? GetPluginMetadata(KernelPlugin plugin) =>
plugin.Name == "ShortDatePlugin" ? new PluginMetadata
public PluginMetadata? GetPluginMetadata(KernelPlugin plugin)
{
Description = "This plugin returns date and time information."
} : null;
return plugin.Name switch
{
"DateTimeWrapper" => new PluginMetadata
{
Description = "This plugin returns date and time information."
},
"RandomPlugin" => new PluginMetadata
{
Description = "This plugin generates random numbers and values."
},
"Queue" => new PluginMetadata
{
Description = "This plugin interacts with Azure Storage Queues."
},
_ => null,
};
}

public FunctionMetadata? GetFunctionMetadata(KernelPlugin plugin, KernelFunctionMetadata metadata) =>
plugin.Name == "ShortDatePlugin" && metadata.Name == "ToShortDateString" ? new FunctionMetadata(metadata.Name)
public FunctionMetadata? GetFunctionMetadata(KernelPlugin plugin, KernelFunctionMetadata metadata)
{
Description = "Returns the date in short format."
} : null;
return plugin.Name switch
{
"DateTimeWrapper" => metadata.Name switch
{
"ToShortDateString" => new FunctionMetadata(metadata.Name)
{
Description = "Returns the current date in short format (MM/dd/yyyy)."
},
"ToLongDateString" => new FunctionMetadata(metadata.Name)
{
Description = "Returns the current date in long format (day of week, month day, year)."
},
"CurrentTime" => new FunctionMetadata(metadata.Name)
{
Description = "Returns the current time in HH:MM:SS format."
},
"CurrentDateTime" => new FunctionMetadata(metadata.Name)
{
Description = "Returns the current date and time in long format."
},
"GetDayOfWeek" => new FunctionMetadata(metadata.Name)
{
Description = "Returns the name of the day of the week for the current date."
},
_ => null
},
"RandomPlugin" => metadata.Name switch
{
"Next" when metadata.Parameters.Count == 0 => new FunctionMetadata(metadata.Name)
{
Description = "Returns a non-negative random integer."
},
"Next" when metadata.Parameters.Count == 1 => new FunctionMetadata(metadata.Name)
{
OverrideFunctionName = "NextWithUpperBound",
Description = "Returns a random integer within a specified range.",
Parameters = [
new ParameterMetadata("maxValue") { Description = "The exclusive upper bound of the random number returned." }
]
},
"Next" when metadata.Parameters.Count == 2 => new FunctionMetadata(metadata.Name)
{
OverrideFunctionName = "NextWithRange",
Description = "Returns a random integer within a specified range.",
Parameters = [
new ParameterMetadata("minValue") { Description = "The inclusive lower bound of the random number returned." },
new ParameterMetadata("maxValue") { Description = "The exclusive upper bound of the random number returned." }
]
},
_ => null
},
"Queue" => metadata.Name switch
{
"ReceiveMessages" when metadata.Parameters.Count == 2 => new FunctionMetadata(metadata.Name)
{
Description = "Retrieves messages from the Azure Storage Queue.",
Parameters = new List<ParameterMetadata>
{
new ParameterMetadata("maxMessages") { Description = "The maximum number of messages to retrieve.", DefaultValue = "5", IsRequired = false },
new ParameterMetadata("visibilityTimeout") { Description = "Specifies the new visibility timeout value, in seconds, relative to server time", DefaultValue = "60", IsRequired = false },
},
},
"PeekMessages" when metadata.Parameters.Count == 1 => new FunctionMetadata(metadata.Name)
{
Description = "Peeks at messages in the Azure Storage Queue without removing them.",
Parameters = new List<ParameterMetadata>
{
new ParameterMetadata("maxMessages") { Description = "The maximum number of messages to peek.", DefaultValue = "5", IsRequired = false },
},
},
"SendMessage" when metadata.Parameters.Count == 1 => new FunctionMetadata(metadata.Name)
{
Description = "Sends a message to the Azure Storage Queue.",
Parameters = new List<ParameterMetadata>
{
new ParameterMetadata("messageText") { Description = "The message to send." },
},
},
_ => null
},
_ => null,
};
}
}
27 changes: 27 additions & 0 deletions samples/UseClrType/DateTimeWrapper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
public class DateTimeWrapper
{
public string ToShortDateString()
{
return DateTime.Now.ToShortDateString();
}

public string ToLongDateString()
{
return DateTime.Now.ToLongDateString();
}

public string CurrentTime()
{
return DateTime.Now.ToString("T");
}

public string CurrentDateTime()
{
return DateTime.Now.ToString("F");
}

public string GetDayOfWeek()
{
return DateTime.Now.DayOfWeek.ToString();
}
}
46 changes: 36 additions & 10 deletions samples/UseClrType/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,54 @@
using SemanticPluginForge.Core;
using Microsoft.Extensions.Configuration;
using System.Reflection;
using Azure.Storage.Queues;
using Azure.Identity;

var builder = Host.CreateApplicationBuilder(args);
builder.Configuration.AddUserSecrets(Assembly.GetExecutingAssembly());
var kernelBuilder = builder.Services.AddKernel();
kernelBuilder.AddAzureOpenAIChatCompletion(
builder.Configuration["AzureOpenAI:ChatDeploymentName"],
builder.Configuration["AzureOpenAI:Endpoint"],
builder.Configuration["AzureOpenAI:ApiKey"]
builder.Configuration["AzureOpenAI:ChatDeploymentName"]!,
builder.Configuration["AzureOpenAI:Endpoint"]!,
builder.Configuration["AzureOpenAI:ApiKey"]!
);

builder.Services.AddSingleton<IPluginMetadataProvider, CustomMetadataProvider>();

// var targetObject = new ShortDate();
// kernelBuilder.Plugins.AddFromClrObjectWithMetadata(targetObject, "ShortDatePlugin");
kernelBuilder.Plugins.AddFromClrTypeWithMetadata<ShortDate>("ShortDatePlugin");
// Add DateTimeWrapper for date/time operations
kernelBuilder.Plugins.AddFromClrTypeWithMetadata<DateTimeWrapper>("DateTimeWrapper");

// Add Random for random number generation
kernelBuilder.Plugins.AddFromClrTypeWithMetadata<Random>("RandomPlugin");

// Add queue client for Azure Storage Queue operations
var qc = new QueueClient(new Uri(builder.Configuration["AzureQueueUri"]!), new DefaultAzureCredential());
kernelBuilder.Plugins.AddFromClrObjectWithMetadata(qc, "Queue");

var host = builder.Build();
var kernel = host.Services.GetRequiredService<Kernel>();
kernel.AutoFunctionInvocationFilters.Add(new FunctionLogger());

var result = await kernel.InvokePromptAsync("What is the date in short format?", arguments: new KernelArguments(new PromptExecutionSettings
while (true)
{
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto()
}));
Console.Write("User: ");
var input = Console.ReadLine()?.Trim();
if (string.IsNullOrWhiteSpace(input) || input.Equals("exit", StringComparison.OrdinalIgnoreCase) || input.Equals("quit", StringComparison.OrdinalIgnoreCase))
{
Console.WriteLine("Exiting...");
break;
}

Console.WriteLine(result);
try
{
var response = await kernel.InvokePromptAsync(input, arguments: new KernelArguments(new PromptExecutionSettings
{
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto()
}));
Console.WriteLine($"Bot: {response}");
}
catch (Exception ex)
{
Console.WriteLine($"Error: {ex.Message}");
}
}
Loading