Official library for building ChatAIze chatbot add-ons.
$ dotnet add package ChatAIze.PluginApiChatAIze.PluginApi is the official SDK for building plugins (add-ons) for ChatAIze.Chatbot.
It builds on top of ChatAIze.Abstractions and provides convenient implementations and helpers for:
ChatbotPlugin (a base implementation of IChatbotPlugin).ChatFunction (+ FunctionParameter for explicit schemas).FunctionAction, FunctionCondition.StringSetting, SelectionSetting, IntegerSetting, BooleanSetting, sections/groups, etc.If you want a working example, start with ChatAIze.PluginApi.ExamplePlugin/MyShop.cs.
net10.0 class library and reference ChatAIze.PluginApi.IPluginLoader (or IAsyncPluginLoader) and return a ChatbotPlugin with Id, Title, and Version.Release and deploy to ChatAIze.Chatbot (upload a single .dll, or copy the whole output folder for plugins with dependencies).net10.0 (matches the current ChatAIze.Chatbot host)..NET 10 SDK (or the SDK required by your target ChatAIze.Chatbot version).dotnet add package ChatAIze.PluginApi
Install-Package ChatAIze.PluginApi
dotnet new classlib -n MyCompany.MyPlugin -f net10.0
cd MyCompany.MyPlugin
dotnet add package ChatAIze.PluginApi
IAsyncPluginLoader,IPluginLoader,IChatbotPlugin (constructed via Activator.CreateInstance).IPluginLoader)using ChatAIze.Abstractions.Plugins;
using ChatAIze.PluginApi;
namespace MyCompany.MyPlugin;
public sealed class MyPluginLoader : IPluginLoader
{
public IChatbotPlugin Load()
{
return new ChatbotPlugin
{
Id = "com.mycompany.myplugin",
Title = "My Plugin",
Description = "Adds custom tools and workflow actions.",
Version = new Version(1, 0, 0)
};
}
}
IAsyncPluginLoader)using ChatAIze.Abstractions.Plugins;
using ChatAIze.PluginApi;
namespace MyCompany.MyPlugin;
public sealed class MyPluginLoader : IAsyncPluginLoader
{
public async ValueTask<IChatbotPlugin> LoadAsync(CancellationToken cancellationToken = default)
{
// Do any async initialization here (warmup, migrations, etc.)
await Task.Delay(10, cancellationToken);
return new ChatbotPlugin
{
Id = "com.mycompany.myplugin",
Title = "My Plugin",
Version = new Version(1, 0, 0)
};
}
}
Tip: ChatbotPlugin defaults to returning its in-memory Settings / Functions / Actions / Conditions collections from the callback properties. You can either populate the collections directly or override the callback properties to return context-dependent definitions.
dotnet build -c Release
ChatAIze.Chatbot loads plugins from .dll files in the plugins/ folder, or via dashboard upload.
.dll)Dashboard → Integrations → Plugins → Upload.
This is the simplest workflow, but it uploads only one file. If your plugin depends on additional assemblies, prefer file-copy deployment.
plugins/ (recommended for plugins with dependencies)Copy at least:
MyCompany.MyPlugin.dllMyCompany.MyPlugin.deps.json.dll files from your output folderinto the chatbot server’s plugins/ directory.
Tip: If you have extra dependencies, the safest approach is to copy everything from bin/Release/net10.0/ (except .pdb if you don’t want symbols).
net10.0 (match the host).IChatbotPlugin.Id and keep it stable (loading a plugin with the same id replaces the previous one).IChatbotPlugin.Version (ChatAIze.Chatbot treats missing versions as invalid).com.mycompany.myplugin:api_key) to avoid collisions with other plugins.AddFunction(MyTool)), and keep tool names unique.IsPreview / IsCommunicationSandboxed before doing side effects (HTTP calls, emails/SMS, writes to external systems).plugins/ (don’t rely on dashboard upload for multi-file plugins).ChatAIze.Chatbot loads plugins with an isolated, unloadable AssemblyLoadContext:
plugins/ directory to a temporary folder before loading, so the original files are not locked.AssemblyLoadContext (so it can be unloaded/replaced).ChatAIze.Abstractions*, ChatAIze.PluginApi*, ChatAIze.Utilities*, and some Microsoft.Extensions.* abstractions)..deps.json (and present in the plugins/ folder).This is why dashboard upload is best for “single dll” plugins, and file-copy deployment is best for plugins with external dependencies.
ChatAIze.Chatbot persists various identifiers, so treat these as stable contracts:
IChatbotPlugin.Id: used to identify and replace already-loaded plugins.ISetting.Id: persisted as a key in the host’s plugin settings store.IChatFunction.Name: becomes a tool name for the model (and must be unique across all installed plugins + integration functions).IFunctionAction.Id / IFunctionCondition.Id: stored in integration function definitions.Tool name matching in the ChatAIze stack uses a tolerant “normalized” comparison (case/spacing/punctuation-insensitive), so treat names like get-order, get order, and GetOrder as equivalent when thinking about collisions.
Recommendation:
com.mycompany.myplugin:..., or a GUID string.IDisposable and/or IAsyncDisposable. The host will attempt to dispose plugins on unload.Every callback can optionally accept a context parameter:
IChatbotContext (used in the dashboard): Settings, Databases, Log(...)IChatContext (per conversation): adds ChatId, User, IsPreview, IsDebugModeOn, IsCommunicationSandboxed, GetPlugin<T>(...)IFunctionContext (tools): adds knowledge search, quick replies, forms, prompt override, status/progressIActionContext (workflow actions): adds action indices/results + placeholder APIsIConditionContext (workflow conditions): currently just IChatContext.dll that returns settings, tools, actions and/or conditions.IChatFunction exposed to the model as a callable tool (LLM function calling).IChatFunction) that executes a workflow of actions/conditions.IFunctionAction step used inside an integration function (not exposed to the model directly).IFunctionCondition gate evaluated before an integration function runs.{placeholder}) produced by actions and injected into later settings as plain-text substitution (supports nested access like {ticket.id} when the placeholder is a JSON object).Tools are IChatFunction instances returned by IChatbotPlugin.FunctionsCallback. They are presented to the language model as callable tools.
using System.ComponentModel;
using ChatAIze.Abstractions.Chat;
using ChatAIze.Abstractions.Plugins;
using ChatAIze.PluginApi;
using Microsoft.Extensions.Logging;
public sealed class MyPluginLoader : IPluginLoader
{
public IChatbotPlugin Load()
{
var plugin = new ChatbotPlugin
{
Id = "com.mycompany.myplugin",
Title = "My Plugin",
Version = new Version(1, 0, 0),
};
plugin.AddFunction(GetOrderStatusAsync);
return plugin;
}
[Description("Gets the status of an order by id.")]
private static async Task<string> GetOrderStatusAsync(
IFunctionContext context,
[Description("Order id shown to the customer.")] string orderId,
CancellationToken cancellationToken = default)
{
var apiKey = await context.Settings.GetAsync("com.mycompany.myplugin:api_key", "", cancellationToken);
if (string.IsNullOrWhiteSpace(apiKey))
{
return "Error: Plugin is not configured (missing api_key).";
}
// Always respect preview/sandbox flags for side effects.
if (context.IsPreview || context.IsCommunicationSandboxed)
{
context.Log(LogLevel.Information, $"(preview) Returning fake status for {orderId}.");
return $"Order {orderId} is shipped. (preview)";
}
// TODO: call your external system here.
return $"Order {orderId} is shipped.";
}
}
parameterName.ToSnakeLower().snake_case (for example GetOrderStatus becomes get_order_status).IFunctionContext is injected by type,CancellationToken is injected by type.Tip: Prefer named methods over lambdas for tools. Some compiler-generated lambda names are not stable/public-friendly and may fail normalization. If you need full control, register a ChatFunction with an explicit Name.
In the ChatAIze stack, schema generation works like this:
IChatFunction.Parameters is non-null, the host uses it as the JSON schema.Important note:
CancellationToken automatically.IFunctionContext.If your callback accepts IFunctionContext, provide an explicit parameter list to avoid showing context as a user-supplied tool input:
using ChatAIze.PluginApi;
var function = new ChatFunction
{
Name = "get_order_status",
Description = "Gets the status of an order by id.",
Callback = GetOrderStatusAsync,
Parameters =
[
new FunctionParameter(typeof(string), "orderId", "Order id shown to the customer.", isRequired: true),
]
};
plugin.AddFunction(function);
If you rely on reflection-based schemas, you can improve the model-visible documentation/constraints using:
DescriptionAttribute on the method and/or parameters[Required], [MinLength], [MaxLength], [StringLength]Example (reflection schema + runtime validation for strings):
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using ChatAIze.Abstractions.Chat;
[Description("Searches the knowledge base.")]
private static async Task<string> SearchAsync(
IFunctionContext context,
[Description("Search query.")] [Required] [MinLength(2)] string query,
CancellationToken cancellationToken = default)
{
var result = await context.SearchKnowledgeAsync(query, cancellationToken: cancellationToken);
return result.ToString();
}
"Error: "."Error:" (case-insensitive)."Error: ..." instead.Set IChatFunction.RequiresDoubleCheck = true to require an extra round-trip where the model is asked to call the tool again to confirm intent.
This behavior is implemented in both the OpenAI and Gemini providers.
Plugins can expose settings that admins configure in the dashboard.
In ChatAIze.Chatbot, your SettingsCallback is invoked while rendering the plugin settings page and when settings change. Keep it fast and return deterministic ids so the host can diff and cache settings trees.
SettingsSection, SettingsGroup, SettingsParagraph (layout-only, do not store a value).StringSetting, SelectionSetting, IntegerSetting, BooleanSetting, DecimalSetting, DateTimeSetting, ListSetting, MapSetting (store a value under ISetting.Id).| Setting | Stored JSON kind | Typical use |
|---|---|---|
StringSetting | string | API keys, URLs, names |
IntegerSetting / DecimalSetting | number | thresholds, limits |
BooleanSetting | true/false | feature toggles |
SelectionSetting | string | “pick one” choices |
DateTimeSetting | string (date/time) | schedules, reminders |
ListSetting | array of strings | tags, allow/deny lists |
MapSetting | object (string → string) | headers, simple key/value configs |
using ChatAIze.Abstractions.Settings;
using ChatAIze.Abstractions.UI;
using ChatAIze.PluginApi.Settings;
plugin.SettingsCallback = _ => ValueTask.FromResult<IReadOnlyCollection<ISetting>>(
[
new SettingsSection(
id: "com.mycompany.myplugin:section.general",
title: "General",
description: "Configuration for My Plugin.",
settings:
[
new StringSetting(
id: "com.mycompany.myplugin:api_key",
title: "API key",
description: "Used to call the Example API.",
textFieldType: TextFieldType.Password,
maxLength: 200),
new SelectionSetting(
id: "com.mycompany.myplugin:region",
title: "Region",
defaultValue: "us",
style: SelectionSettingStyle.Automatic,
choices:
[
new SelectionChoice(value: "us", title: "United States"),
new SelectionChoice(value: "eu", title: "Europe"),
])
])
]);
Notes about ChatAIze.Chatbot UI behavior:
SelectionSettingStyle.Automatic chooses a UI based on the number of choices (≤ 3 segmented, ≤ 6 radio buttons, otherwise a dropdown).SettingsButton callbacks are executed on the server when clicked; the provided CancellationToken is canceled when the settings view is disposed.ListSetting)Use ListSetting when you want the admin to enter a list of strings (tags, keywords, allowed domains, etc.).
ISetting.Id (e.g. ["a", "b", "c"]).Example:
using ChatAIze.PluginApi.Settings;
var allowedDomains = new ListSetting(
id: "com.mycompany.myplugin:allowed_domains",
title: "Allowed email domains",
description: "If set, only users with these email domains are allowed to run certain tools.",
itemPlaceholder: "@example.com",
maxItems: 20,
maxItemLength: 100,
allowDuplicates: false,
isLowercase: true);
Reading a list setting:
var domains = await context.Settings.GetAsync(
"com.mycompany.myplugin:allowed_domains",
defaultValue: new List<string>(),
cancellationToken);
Notes:
AllowDuplicates and IsLowercase are UI hints; host enforcement can vary. Even if you set allowDuplicates: false, still handle duplicates defensively.IntegerSetting + ListSetting).MapSetting)Use MapSetting when you want the admin to enter key/value pairs of strings (headers, labels → URLs, product → price strings, etc.).
ISetting.Id (e.g. {"key":"value"}).Dictionary<string, string>.Example:
using ChatAIze.PluginApi.Settings;
var defaultHeaders = new MapSetting(
id: "com.mycompany.myplugin:default_headers",
title: "Default HTTP headers",
description: "Sent with every outbound request made by this plugin.",
keyPlaceholder: "Header-Name",
valuePlaceholder: "Value",
maxItems: 30,
maxKeyLength: 100,
maxValueLength: 500);
Reading a map setting:
var headers = await context.Settings.GetAsync(
"com.mycompany.myplugin:default_headers",
defaultValue: new Dictionary<string, string>(),
cancellationToken);
// Optional: treat keys case-insensitively for HTTP header usage
var headersNormalized = new Dictionary<string, string>(headers, StringComparer.OrdinalIgnoreCase);
Notes:
MapSetting is string → string. If you want structured values, store JSON in the string values and parse it yourself (and validate carefully), or expose dedicated typed settings.Plugin settings are stored by the host as JSON keyed by ISetting.Id and exposed through IPluginSettings:
var apiKey = await context.Settings.GetAsync("com.mycompany.myplugin:api_key", "", cancellationToken);
Security note: use plugin settings for secrets (API keys, tokens) and avoid hardcoding credentials in your plugin binary or source code. Do not log secret values.
ChatAIze.Chatbot supports “integration functions” configured in the dashboard. These are workflows composed of action steps and optional conditions.
Your plugin can contribute reusable actions via IFunctionAction / FunctionAction.
using ChatAIze.Abstractions.Chat;
using ChatAIze.PluginApi;
using ChatAIze.PluginApi.Settings;
var action = new FunctionAction(
id: "com.mycompany.myplugin:actions.create_ticket",
title: "Create Ticket",
callback: CreateTicketAsync)
{
Description = "Creates a ticket and exposes it as {ticket}.",
Placeholders = ["ticket"]
};
action.AddStringSetting("subject", title: "Subject", maxLength: 200);
action.AddStringSetting("message", title: "Message", editorLines: 4, maxLength: 2000);
plugin.AddAction(action);
static async Task<string> CreateTicketAsync(IActionContext context, string subject, string message, CancellationToken cancellationToken)
{
if (context.IsPreview)
{
context.SetPlaceholder("ticket", new { id = 123, url = "https://example.test/tickets/123" });
return "OK: (preview) ticket created.";
}
// TODO: create the ticket in your external system here.
context.SetPlaceholder("ticket", new { id = 123, url = "https://example.com/tickets/123" });
return "Ticket created.";
}
Actions can use ListSetting and MapSetting as step configuration. This is useful for things like:
If you want the host to bind settings into your delegate parameters, keep the setting ids compatible with C# parameter names (e.g. headers, tags).
var action = new FunctionAction(
id: "com.mycompany.myplugin:actions.send_request",
title: "Send Request",
callback: SendRequestAsync);
action.AddStringSetting("url", title: "URL");
action.AddMapSetting("headers", title: "Headers", keyPlaceholder: "Header", valuePlaceholder: "Value");
action.AddListSetting("tags", title: "Tags", itemPlaceholder: "tag");
plugin.AddAction(action);
static async Task<string> SendRequestAsync(
IActionContext context,
string url,
Dictionary<string, string> headers,
List<string> tags,
CancellationToken cancellationToken)
{
// Use headers/tags here...
return $"OK: sending to {url} with {headers.Count} headers and {tags.Count} tags.";
}
In ChatAIze.Chatbot, placeholders are expanded in action settings before invocation. For list/map values (JSON arrays/objects), substitution happens on the raw JSON text and is reparsed, so placeholders must produce valid JSON after replacement.
In ChatAIze.Chatbot:
IActionContext and/or CancellationToken injected by type.Before calling your action callback, the host expands placeholders in the action settings:
{order_id}, {customer_email}),{ticket.id}, {ticket.url}).Substitution is plain text. For JSON objects/arrays, ChatAIze.Chatbot performs substitution on the raw JSON and reparses it, so replacement must produce valid JSON.
ChatAIze.Chatbot normalizes placeholder ids to snake_case and may suffix them (_2, _3, …) to avoid collisions when multiple actions in a workflow declare the same placeholder id. Use the placeholder names shown in the dashboard for the specific action placement.
"Error: ..." does not automatically mark an action as failed.context.SetActionResult(false, "reason").You can render different settings based on the current placement values via SettingsCallback:
using System.Text.Json;
using ChatAIze.Abstractions.Settings;
using ChatAIze.PluginApi;
using ChatAIze.PluginApi.Settings;
using ChatAIze.Utilities.Extensions;
action.SettingsCallback = (values) =>
{
var settings = new List<ISetting>();
settings.AddSelectionSetting(
id: "mode",
title: "Mode",
defaultValue: "simple",
choices:
[
new SelectionChoice("simple", "Simple"),
new SelectionChoice("advanced", "Advanced"),
]);
var mode = values.TryGetSettingValue("mode", "simple");
if (mode == "advanced")
{
settings.AddIntegerSetting(id: "retries", title: "Retries", defaultValue: 3, minValue: 0, maxValue: 10);
}
return settings;
};
Conditions run before an integration function executes and decide whether it’s allowed.
using ChatAIze.Abstractions.Chat;
using ChatAIze.PluginApi;
using ChatAIze.PluginApi.Settings;
var condition = new FunctionCondition(
id: "com.mycompany.myplugin:conditions.company_email",
title: "Company email required",
callback: (IConditionContext context, string domain) =>
context.User.Email?.EndsWith(domain, StringComparison.OrdinalIgnoreCase) == true
? true
: $"Only {domain} users are allowed.");
condition.AddStringSetting("domain", title: "Email domain", placeholder: "@mycompany.com");
plugin.AddCondition(condition);
Conditions can also use ListSetting / MapSetting for configuration (for example: a list of allowed domains or a map of role → allowed).
In ChatAIze.Chatbot:
true → allowfalse → deny (no reason)Note: conditions run before actions, so they do not have access to action placeholders (only to function parameters and chat/user context).
You can update the current execution status (and optional progress 0–100) via IFunctionContext.SetStatus:
context.SetStatus("Calling external API...", progress: 30);
// ...
context.SetStatus("Done.", progress: 100);
All context types expose Log(...) (wired to the host logging pipeline):
using Microsoft.Extensions.Logging;
context.Log(LogLevel.Information, "Starting order lookup...");
Tip: avoid logging secrets. In ChatAIze.Chatbot, logs can be visible to administrators.
Plugins can prompt the current user with built-in UI dialogs (when supported by the host):
var ok = await context.ShowConfirmationAsync(
title: "Delete account",
message: "Are you sure you want to delete your account?",
yesText: "Delete",
noText: "Cancel",
cancellationToken: cancellationToken);
if (!ok)
{
return "Error: Cancelled by user.";
}
Quick replies are suggested “chips” in the chat UI. A tool or action can update them via IFunctionContext.QuickReplies:
using ChatAIze.PluginApi;
context.QuickReplies.Clear();
context.QuickReplies.Add(new QuickReply("Order", "Where is my order?"));
context.QuickReplies.Add(new QuickReply("Refund", "I want a refund"));
var result = await context.SearchKnowledgeAsync("refund policy", folder: "Support", cancellationToken: cancellationToken);
var content = await context.GetDocumentContentAsync("Refund Policy", cancellationToken);
var lastOrderId = await context.User.GetPropertyAsync("com.mycompany.myplugin:last_order_id", "", cancellationToken);
await context.User.SetPropertyAsync("com.mycompany.myplugin:last_order_id", "12345", cancellationToken);
If your plugin can optionally integrate with another plugin, you can ask the host for a plugin instance by type:
var other = context.GetPlugin<OtherPluginType>(id: "com.other.plugin");
if (other is not null)
{
// Use the optional integration.
}
Avoid hard dependencies on other plugins being present; always handle null.
Plugins can use IDatabaseManager via context.Databases:
using ChatAIze.PluginApi.Databases;
using ChatAIze.Abstractions.Databases.Enums;
var db = await context.Databases.FindDatabaseByTitleAsync("Orders", cancellationToken);
if (db is null)
{
return "Error: Database 'Orders' not found.";
}
var item = await context.Databases.GetFirstItemAsync(
db,
sorting: new DatabaseSorting(property: "created_at", order: SortOrder.Descending),
cancellationToken: cancellationToken,
filters:
[
new DatabaseFilter(property: "order_id", type: FilterType.Equals, value: orderId, options: FilterOptions.None)
]);
IChatFunction.Callback is non-null (tools without callbacks are treated as integration functions and executed by the host’s default callback).AddFunction(Delegate); compiler-generated names can be unstable..dll file. If you use additional dependencies, deploy by copying your full output folder to plugins/..deps.json and dependency .dll files are present.IActionContext.SetActionResult(...)."Error: ..." is just a string result; it does not automatically fail the action.ChatAIze.PluginApi.ExamplePlugin/MyShop.cs demonstrates:
AddFunction)FunctionAction)FunctionCondition)