AgentSkillsCN

add-plugin-claude

插件脚手架生成器——按照项目模式,快速生成完整的插件实现。当用户说“为 [功能] 添加插件”、“创建 [名称] 插件”或“搭建新插件”时,此工具将大显身手。

SKILL.md
--- frontmatter
name: add-plugin-claude
description: Plugin Scaffolding Generator - Creates complete plugin implementations following project patterns. Use when user says "add a plugin for [feature]", "create [Name]Plugin", or "scaffold a new plugin".

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:

  1. Plugin name and purpose

    • e.g. "Calendar", "Calculator", "Timer"
    • Use PascalCase; plugin will be [Name]Plugin or Nested[Name]Plugin
    • Ask: "What does this plugin do?" to understand the purpose
  2. 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)
  3. 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
  4. 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-provider skill to generate providers
    • Provider is needed when plugin requires external data or I/O operations

File Generation

For Base Plugin

Generate the following files:

  1. 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
  2. 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
  3. 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
  4. Registration: Update src/AnimalKernel/Core/StandardKernel.cs

    • Add plugin instantiation in AddPlugins method
    • Add using statement for the plugin namespace
    • Track disposable plugins if needed

For Nested Kernel Plugin

Generate the following files:

  1. Plugin Class: src/Plugins/NestedKernel/SK/Nested[Name]Plugin.cs

    • Namespace: AnimalAI.Plugins.NestedKernel.SK
    • Follow exact pattern from NestedStockPlugin.cs
    • Implement IDisposable and IInternalCallTracker
    • Include all standard fields (kernel, logger, provider, tracking)
    • Implement lazy kernel initialization
    • Create AddPlugins private method to register internal tools
    • Implement Ask[Name]Agent KernelFunction
  2. 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
  3. Provider Files (if requested via create-provider skill)

  4. Test Cases: Add to src/IntegrationTesterApp/TestCaseDefinitions.cs

  5. Registration: Update src/AnimalKernel/Core/StandardKernel.cs

    • Add plugin instantiation in AddNestedKernelPlugins method
    • Register plugin instance for tracking
    • Add using statement for the plugin namespace

Base Plugin Template

csharp
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

csharp
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:

csharp
// 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:

csharp
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:

csharp
using AnimalAI.Plugins.Base.SK;

For Nested Kernel Plugin

Add to AddNestedKernelPlugins method in src/AnimalKernel/Core/StandardKernel.cs:

csharp
// 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:

csharp
using AnimalAI.Plugins.NestedKernel.SK;

If using provider pattern with conditional fake/real:

csharp
// 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

  1. Gather Information - Use AskUserQuestion to collect all plugin details
  2. Generate Provider (if needed) - Invoke create-provider skill
  3. Generate Plugin Class - Write the plugin file using appropriate template
  4. Generate Internal Plugin - For nested plugins, create the base plugin too
  5. Add Test Cases - Edit TestCaseDefinitions.cs to add test cases
  6. Register Plugin - Edit StandardKernel.cs to register the plugin
  7. Add Using Statements - Ensure all necessary using statements are added
  8. 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 #region comments to organize code sections
  • For nested plugins, always implement IDisposable and IInternalCallTracker
  • Follow the exact pattern from existing plugins (NestedStockPlugin, WeatherPlugin, etc.)
  • Test cases should have clear names and appropriate tags for filtering

Reference Implementations

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