Add Plugin - Complete Plugin Scaffolding Generator
Generates a complete plugin implementation following AnimalAL-v1 project patterns, including plugin class, providers (if requested), tests, and registration in StandardKernel.
Discovery (Ask First)
Before generating files, gather:
- •
Plugin name and purpose
- •e.g. "Calendar", "Calculator", "Timer"
- •Use PascalCase; plugin will be
[Name]PluginorNested[Name]Plugin - •Ask: "What does this plugin do?" to understand the purpose
- •
Plugin type
- •Ask: "What type of plugin?"
- •Options:
- •Base - Simple plugin with direct KernelFunctions (e.g., MathPlugin, WeatherPlugin)
- •Nested Kernel - Plugin with its own internal kernel and toolset (e.g., NestedStockPlugin)
- •Nested MCP - Plugin that wraps an MCP server (for future use)
- •
Functions/Methods needed
- •Ask: "What functions should this plugin have?"
- •For each function:
- •Function name (e.g., "GetEvents", "AddEvent", "Calculate")
- •Description
- •Parameters (name, type, description)
- •Return type (prefer
Task<string>for async)
- •For Nested plugins: typically one main function like
Ask[Name]Agent
- •
Provider pattern
- •Ask: "Does this plugin need a data provider?" (Yes/No)
- •If Yes:
- •Ask: "Which provider implementations?" (Interface only / Interface + Fake / Interface + Real / Both)
- •Invoke the
create-providerskill to generate providers
- •Provider is needed when plugin requires external data or I/O operations
File Generation
For Base Plugin
Generate the following files:
- •
Plugin Class:
src/Plugins/Base/SK/[Name]Plugin.cs- •Namespace:
AnimalAI.Plugins.Base.SK - •Include logger field and constructor
- •Include provider field if using provider pattern
- •Implement KernelFunctions based on user specification
- •Namespace:
- •
Provider Files (if requested via create-provider skill):
- •Interface:
src/Plugins/Providers/Interfaces/I[Name]DataProvider.cs - •Fake:
src/Plugins/Providers/Fake/Fake[Name]DataProvider.cs - •Real:
src/Plugins/Providers/Real/Real[Name]DataProvider.cs
- •Interface:
- •
Test Cases: Add to
src/IntegrationTesterApp/TestCaseDefinitions.cs- •Add 2-3 test cases for the plugin functions
- •Include positive test cases and at least one error handling test
- •
Registration: Update
src/AnimalKernel/Core/StandardKernel.cs- •Add plugin instantiation in
AddPluginsmethod - •Add using statement for the plugin namespace
- •Track disposable plugins if needed
- •Add plugin instantiation in
For Nested Kernel Plugin
Generate the following files:
- •
Plugin Class:
src/Plugins/NestedKernel/SK/Nested[Name]Plugin.cs- •Namespace:
AnimalAI.Plugins.NestedKernel.SK - •Follow exact pattern from NestedStockPlugin.cs
- •Implement
IDisposableandIInternalCallTracker - •Include all standard fields (kernel, logger, provider, tracking)
- •Implement lazy kernel initialization
- •Create
AddPluginsprivate method to register internal tools - •Implement
Ask[Name]AgentKernelFunction
- •Namespace:
- •
Internal Plugin (for nested kernel to use):
src/Plugins/Base/SK/[Name]Plugin.cs- •This is the actual tool plugin that the nested kernel uses
- •Follow base plugin pattern
- •
Provider Files (if requested via create-provider skill)
- •
Test Cases: Add to
src/IntegrationTesterApp/TestCaseDefinitions.cs - •
Registration: Update
src/AnimalKernel/Core/StandardKernel.cs- •Add plugin instantiation in
AddNestedKernelPluginsmethod - •Register plugin instance for tracking
- •Add using statement for the plugin namespace
- •Add plugin instantiation in
Base Plugin Template
using System.ComponentModel;
using AnimalAI.Plugins.Providers;
using Microsoft.Extensions.Logging;
using Microsoft.SemanticKernel;
namespace AnimalAI.Plugins.Base.SK;
/// <summary>
/// [Brief description of plugin purpose].
/// </summary>
public class [Name]Plugin
{
#region Logger Fields
private readonly ILogger<[Name]Plugin>? _logger;
#endregion Logger Fields
#region Provider Fields
private readonly I[Name]DataProvider _dataProvider;
#endregion Provider Fields
#region Constructors
public [Name]Plugin(ILoggerFactory? loggerFactory = null, I[Name]DataProvider? dataProvider = null)
{
_dataProvider = dataProvider ?? new Fake[Name]DataProvider();
_logger = loggerFactory?.CreateLogger<[Name]Plugin>();
}
#endregion Constructors
#region Kernel Functions
[KernelFunction]
[Description("[Function description]")]
public async Task<string> [FunctionName](
[Description("[Parameter description]")] string paramName)
{
_logger?.LogInformation("🔧 [FUNCTION CALL] [Name]Plugin.[FunctionName](paramName={ParamName})", paramName);
try
{
var result = await _dataProvider.[ProviderMethod]Async(paramName);
if (result == null)
{
var error = $"[Error message]";
_logger?.LogWarning("⚠️ [FUNCTION RESULT] [Name]Plugin.[FunctionName] = {Error}", error);
return error;
}
var response = $"[Format result]";
_logger?.LogInformation("✅ [FUNCTION RESULT] [Name]Plugin.[FunctionName] = {Result}", response);
return response;
}
catch (Exception ex)
{
var error = $"Error in [FunctionName]: {ex.Message}";
_logger?.LogError(ex, "❌ [FUNCTION ERROR] [Name]Plugin.[FunctionName]");
return error;
}
}
#endregion Kernel Functions
}
Note: If provider pattern is NOT used, remove Provider Fields region and directly implement logic in the function.
Nested Kernel Plugin Template
using System.ComponentModel;
using System.Text;
using AnimalAI.Plugins.Base.SK;
using AnimalAI.Plugins.Providers;
using AnimalAI.Utilities;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.ChatCompletion;
using Microsoft.SemanticKernel.Connectors.OpenAI;
namespace AnimalAI.Plugins.NestedKernel.SK;
/// <summary>
/// Plugin that creates an internal kernel with its own toolset ([Name]Plugin).
/// Delegates prompts to the internal kernel which can call [list main functions].
/// ChatHistory is injected via KernelArguments through IAutoFunctionInvocationFilter.
/// </summary>
public class Nested[Name]Plugin : IDisposable, IInternalCallTracker
{
#region Kernel Fields
private readonly string _defaultEndpoint;
private readonly string _defaultModelId;
private readonly Lazy<Kernel> _internalKernel;
#endregion Kernel Fields
#region Logger Fields
private readonly ILogger<Nested[Name]Plugin>? _logger;
private readonly ILoggerFactory? _loggerFactory;
#endregion Logger Fields
#region Provider Fields
private readonly I[Name]DataProvider _dataProvider;
#endregion Provider Fields
#region Tracking Fields
private readonly List<InternalFunctionCall> _internalCalls = new();
private readonly object _internalCallsLock = new();
#endregion Tracking Fields
#region Other Fields
private readonly ServiceProvider _serviceProvider;
#endregion Other Fields
#region Constructors
public Nested[Name]Plugin(ILoggerFactory? loggerFactory = null, string defaultEndpoint = KernelConstants.DefaultEndpoint, string defaultModelId = KernelConstants.DefaultModelId, I[Name]DataProvider? dataProvider = null)
{
_loggerFactory = loggerFactory;
_logger = loggerFactory?.CreateLogger<Nested[Name]Plugin>();
_defaultEndpoint = defaultEndpoint;
_defaultModelId = defaultModelId;
_dataProvider = dataProvider ?? new Fake[Name]DataProvider();
// Create service provider for logging
var services = new ServiceCollection();
services.AddLogging(builder =>
{
if (_loggerFactory != null)
{
builder.ClearProviders();
builder.SetMinimumLevel(LogLevel.Information);
}
else
{
builder.ClearProviders();
builder.SetMinimumLevel(LogLevel.Warning);
}
});
_serviceProvider = services.BuildServiceProvider();
// Lazy initialization of internal kernel
_internalKernel = new Lazy<Kernel>(() => CreateInternalKernel());
}
private Kernel CreateInternalKernel()
{
var builder = Kernel.CreateBuilder();
#pragma warning disable SKEXP0010
builder.AddOpenAIChatCompletion(
modelId: _defaultModelId,
endpoint: new Uri(_defaultEndpoint),
apiKey: "not-needed");
#pragma warning restore SKEXP0010
var loggerFactory = _loggerFactory ?? _serviceProvider.GetRequiredService<ILoggerFactory>();
AddPlugins(builder, loggerFactory);
var kernel = builder.Build();
// Add internal function call tracker
kernel.AutoFunctionInvocationFilters.Add(new InternalFunctionCallTracker(this, loggerFactory));
return kernel;
}
#endregion Constructors
#region AddPlugins Function
private void AddPlugins(IKernelBuilder builder, ILoggerFactory loggerFactory)
{
var [lowerName]Plugin = new [Name]Plugin(loggerFactory, _dataProvider);
builder.Plugins.AddFromObject([lowerName]Plugin, "[Name]Plugin");
}
#endregion AddPlugins Function
#region Kernel Functions
[KernelFunction]
[Description("[When to use this agent and what it can do]. Use this WHENEVER [trigger conditions]. The returned response MUST be used in your answer to the user.")]
[return: Description("The response from the [name] agent that must be relayed to the user")]
public async Task<string> Ask[Name]Agent(
[Description("The prompt or question to process [about what]")] string prompt,
ChatHistory? ChatHistory = null)
{
_logger?.LogInformation("🔧 [FUNCTION CALL] Nested[Name]Plugin.Ask[Name]Agent(prompt={Prompt})", prompt);
try
{
var internalKernel = _internalKernel.Value;
var settings = new OpenAIPromptExecutionSettings
{
ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions
};
var chat = internalKernel.GetRequiredService<IChatCompletionService>();
var history = new ChatHistory();
history.AddSystemMessage(KernelConstants.SubKernelSystemMessage);
var parentHistory = ChatHistory;
if (parentHistory != null && parentHistory.Count > 0)
{
// INTENTIONAL: Use foreach loop with implicit filtering to preserve
// the original ordering of messages in the conversation history
foreach (var message in parentHistory)
{
if (message.Role != AuthorRole.System)
{
history.Add(message);
}
}
}
history.AddUserMessage(prompt);
var responseBuilder = new StringBuilder();
await foreach (var chunk in chat.GetStreamingChatMessageContentsAsync(history, settings, internalKernel))
{
if (chunk.Content != null)
{
responseBuilder.Append(chunk.Content);
}
}
var response = responseBuilder.ToString();
_logger?.LogInformation("✅ [FUNCTION RESULT] Nested[Name]Plugin.Ask[Name]Agent = {Result}", response);
return response;
}
catch (Exception ex)
{
var error = $"Error calling SubKernel: {ex.Message}";
_logger?.LogError(ex, "❌ [FUNCTION ERROR] Nested[Name]Plugin.Ask[Name]Agent");
return error;
}
}
#endregion Kernel Functions
#region Internal Call Tracking
public void TrackInternalCall(string functionName, Dictionary<string, object?> parameters, string? result)
{
lock (_internalCallsLock)
{
_internalCalls.Add(new InternalFunctionCall
{
FunctionName = functionName,
Parameters = parameters,
Result = result
});
}
}
public List<InternalFunctionCall> GetAndClearInternalCalls()
{
lock (_internalCallsLock)
{
var calls = new List<InternalFunctionCall>(_internalCalls);
_internalCalls.Clear();
return calls;
}
}
#endregion Internal Call Tracking
#region Dispose Functions
public void Dispose()
{
if (_internalKernel.IsValueCreated)
{
// Kernel does not implement IDisposable
}
_serviceProvider.Dispose();
}
#endregion Dispose Functions
}
Test Case Template
Add to src/IntegrationTesterApp/TestCaseDefinitions.cs in the GetAllTestCases() method:
// For Nested Plugin:
new()
{
Name = "[Name]Agent [Test Scenario]",
Prompt = "[Test prompt]",
ExpectedToolsToCall = { "[Name]Agent.Ask[Name]Agent" },
ExpectedToolsNotToCall = { "StockAgent.AskStockAgent", "WeatherAgent.AskWeatherAgent" },
ResponseMustContain = { },
ResponseMustContainAny = { "[keyword1]", "[keyword2]", "[keyword3]" },
Tags = { "[plugin-tag]" }
},
// For Base Plugin:
new()
{
Name = "[Name]Plugin [Function] Test",
Prompt = "[Test prompt]",
ExpectedToolsToCall = { "[Name]Plugin.[FunctionName]" },
ExpectedToolsNotToCall = { "StockAgent.AskStockAgent", "WeatherAgent.AskWeatherAgent" },
ResponseMustContain = { },
ResponseMustContainAny = { "[expected keyword]" },
Tags = { "[plugin-tag]" }
},
StandardKernel Registration
For Base Plugin
Add to AddPlugins method in src/AnimalKernel/Core/StandardKernel.cs:
private void AddPlugins(IKernelBuilder builder)
{
// Register the [Name] plugin
var [lowerName]Plugin = new [Name]Plugin(_loggerFactory);
builder.Plugins.AddFromObject([lowerName]Plugin, "[Name]Plugin");
}
Add using statement at top:
using AnimalAI.Plugins.Base.SK;
For Nested Kernel Plugin
Add to AddNestedKernelPlugins method in src/AnimalKernel/Core/StandardKernel.cs:
// Register the Nested[Name]Plugin plugin (internal kernel with [Name] toolset) var nested[Name]Plugin = new Nested[Name]Plugin(_loggerFactory, _endpoint, _modelId); builder.Plugins.AddFromObject(nested[Name]Plugin, "[Name]Agent"); _pluginInstances["[Name]Agent"] = nested[Name]Plugin;
Add using statement at top:
using AnimalAI.Plugins.NestedKernel.SK;
If using provider pattern with conditional fake/real:
// Register the Nested[Name]Plugin plugin
I[Name]DataProvider [lowerName]Provider = string.IsNullOrEmpty(KernelConstants.[Name]ConfigKey)
? new Fake[Name]DataProvider()
: new Real[Name]DataProvider(KernelConstants.[Name]ConfigKey);
var nested[Name]Plugin = new Nested[Name]Plugin(_loggerFactory, _endpoint, _modelId, [lowerName]Provider);
builder.Plugins.AddFromObject(nested[Name]Plugin, "[Name]Agent");
_pluginInstances["[Name]Agent"] = nested[Name]Plugin;
Workflow Steps
- •Gather Information - Use AskUserQuestion to collect all plugin details
- •Generate Provider (if needed) - Invoke
create-providerskill - •Generate Plugin Class - Write the plugin file using appropriate template
- •Generate Internal Plugin - For nested plugins, create the base plugin too
- •Add Test Cases - Edit TestCaseDefinitions.cs to add test cases
- •Register Plugin - Edit StandardKernel.cs to register the plugin
- •Add Using Statements - Ensure all necessary using statements are added
- •Summary - Provide summary of files created and next steps
Important Notes
- •Follow existing naming conventions (PascalCase for classes, camelCase for variables)
- •Use consistent logging patterns with emojis (🔧 for calls, ✅ for success, ❌ for errors)
- •All async methods should return
Task<string>by default - •Logger should always be optional (
ILoggerFactory? loggerFactory = null) - •Provider should default to Fake implementation when null
- •Use
#regioncomments to organize code sections - •For nested plugins, always implement
IDisposableandIInternalCallTracker - •Follow the exact pattern from existing plugins (NestedStockPlugin, WeatherPlugin, etc.)
- •Test cases should have clear names and appropriate tags for filtering
Reference Implementations
- •Base Plugin: WeatherPlugin.cs
- •Nested Plugin: NestedStockPlugin.cs
- •Test Cases: TestCaseDefinitions.cs
- •Registration: StandardKernel.cs
- •Provider Pattern: Use
/create-providerskill
Checklist Before Delivering
- • Plugin class created with correct namespace and naming
- • Provider files created if requested (via create-provider skill)
- • Test cases added to TestCaseDefinitions.cs
- • Plugin registered in StandardKernel.cs
- • Using statements added to StandardKernel.cs
- • All files follow project conventions (regions, logging, error handling)
- • For nested plugins: IDisposable and IInternalCallTracker implemented
- • Summary provided with list of files created and next steps
- • Remind user to run tests with appropriate tag filter