Developing Plugins
This guide covers how to develop plugins for ai_nn_controller using the
plugin_framework module.
A plugin gives control applications a typed, reusable interface to an external service — a time-series database, an ML model registry, a monitoring backend, or anything else multiple apps might need to talk to. Instead of every control application reimplementing its own InfluxDB or MLflow client, a plugin implements that integration once and is declared as a dependency by any app that needs it.
When to Write a Plugin
Write a plugin when you have logic that:
Talks to an external service shared by multiple control applications (a database, a metrics backend, a model store)
Should be swappable independently of any single app (e.g. a “storage” plugin backed by InfluxDB today, something else tomorrow)
Benefits from a single, well-defined lifecycle (
connect()/disconnect()/ health checks) managed by the controller
If your logic is specific to one control application, keep it in that app’s
aic_app.py instead — a plugin is for behaviour shared across apps.
Core Components
Component |
Description |
|---|---|
|
Base class for all plugins. Subclass this and implement |
|
Optional typed mixins that define a conventional method surface for common plugin categories. |
|
Decorator that registers a plugin class with |
|
Runtime registry of loaded plugins, populated by |
Import everything from the package:
from ai_nn_controller.plugin_framework import AicPlugin, aic_plugin
AicPlugin Base Class
Every plugin subclasses AicPlugin and implements its lifecycle hooks:
Method |
Description |
|---|---|
|
Optional but recommended. Initialise the connection to the external service. Called once by |
|
Optional. Release resources. Called at controller shutdown. |
|
Optional. Return |
All plugin methods are classmethod s — plugins are used as class-level
singletons, the same way control applications are.
Typed Mixins
If your plugin fits one of these categories, subclass the mixin instead of
AicPlugin directly — it documents intent and gives a predictable method
surface for consumers:
from ai_nn_controller.plugin_framework import StoragePlugin, aic_plugin
@aic_plugin(name="InfluxStorage", plugin_type="storage")
class InfluxStorage(StoragePlugin):
@classmethod
def connect(cls):
cls._client = make_influx_client(...)
@classmethod
def write(cls, key, value, tags=None):
cls._client.write(key, value, tags or {})
@classmethod
def read(cls, query):
return cls._client.query(query)
StoragePlugin (write/read), ModelRegistryPlugin
(load_model/save_model), and MonitoringPlugin
(push_metric/get_metric) are available. A plugin that doesn’t fit any
of these — like the reference ConsolePlugin below — can subclass
AicPlugin directly and expose whatever methods make sense.
The @aic_plugin Decorator
@aic_plugin(name="ConsolePlugin", plugin_type="generic")
class ConsolePlugin(AicPlugin):
@classmethod
def connect(cls):
print("[ConsolePlugin] connected")
nameis the string control apps use inrequired_pluginsand to look the plugin up viacls.plugins["ConsolePlugin"]. It must be unique — registering a second plugin under the same name raisesRuntimeError.plugin_typeis a free-form category string ("storage","model_registry","monitoring", or"generic").
Worked Reference: console_plugin
The repository ships plugins/console_plugin/ as the minimal reference
implementation — a plugin with no external dependencies that prints
structured messages to stdout instead of calling a real service. Use it as
the template for a new plugin’s file layout.
plugins/console_plugin/aic_plugin.py:
import datetime
from ai_nn_controller.plugin_framework import AicPlugin, aic_plugin
@aic_plugin(name="ConsolePlugin", plugin_type="generic")
class ConsolePlugin(AicPlugin):
"""Prints structured messages to stdout. No external dependencies."""
_prefix: str = "[ConsolePlugin]"
@classmethod
def connect(cls) -> None:
cls._print("INFO", "connected")
@classmethod
def disconnect(cls) -> None:
cls._print("INFO", "disconnected")
@classmethod
def is_healthy(cls) -> bool:
return True
@classmethod
def log(cls, message: str, level: str = "INFO") -> None:
"""Print a free-form message."""
cls._print(level, message)
@classmethod
def log_measurement(cls, node_id: int, data: dict) -> None:
"""Print the latest measurement snapshot for a node."""
cls._print("DATA", f"node={node_id} {data}")
@classmethod
def log_command(cls, command: str, payload: dict) -> None:
"""Print a command that is about to be (or was) sent."""
cls._print("CMD", f"{command} payload={payload}")
@classmethod
def log_event(cls, event: str, **kwargs) -> None:
"""Print a named event with arbitrary keyword metadata."""
extras = " ".join(f"{k}={v}" for k, v in kwargs.items())
cls._print("EVENT", f"{event} {extras}".strip())
@classmethod
def _print(cls, level: str, message: str) -> None:
ts = datetime.datetime.now().isoformat(timespec="seconds")
print(f"{ts} {cls._prefix} [{level}] {message}")
Declaring the Entry Point
Plugins ship as independent, installable Python packages. The controller
discovers them at startup via the ai_nn_controller.plugin_init entry-point
group — the same mechanism used to discover control applications
(ai_nn_controller.app_init, see Developing Control Applications).
plugins/console_plugin/pyproject.toml:
[build-system]
requires = ["setuptools>=61.0", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "console-plugin"
version = "0.1.0"
description = "Console logging plugin for ai_nn_controller"
requires-python = ">=3.9"
dependencies = ["ai_nn_controller>=1.0.0"]
[project.entry-points."ai_nn_controller.plugin_init"]
"console_plugin:aic_plugin" = "ai_nn_controller.plugin_framework.entrypoints:bootstrap_plugin_bundle"
[tool.setuptools]
py-modules = ["aic_plugin"]
The entry-point name ("console_plugin:aic_plugin") is parsed as
"bundle_name:plugin_module" — bootstrap_plugin_bundle imports
plugin_module (default "aic_plugin"), which triggers the
@aic_plugin decorator and registers the class with PluginManager.
Declaring a Plugin Dependency in a Control App
A control application declares which plugins it needs via
required_plugins, then accesses them through cls.plugins:
from ai_nn_controller.decorators.aic_app import aic_app
from ai_nn_controller.AicApp import AicApp
@aic_app(name="MyApp")
class MyApp(AicApp):
required_plugins = ["ConsolePlugin"]
read_measurements = {3: ["gain", "power"]}
control_functions = {}
@classmethod
def process(cls, measurements):
console = cls.plugins["ConsolePlugin"]
latest = measurements.get(3, [{}])[-1]
console.log_measurement(3, latest)
console.log_event("process_tick", app="MyApp")
If ConsolePlugin isn’t installed (or wasn’t loaded via an entry point),
AicController raises RuntimeError at startup listing the missing
plugin and whatever is available — the app never starts with a silently
missing dependency. See Developing Control Applications for the full app-development
guide.
Installation Order
Plugins run inside the control application’s process — they are not separate containers. A control app’s Dockerfile must install packages in this order:
The
ai_nn_controllerframeworkEvery plugin the app requires
The control application itself
FROM python:3.9-slim
# 1. Framework
COPY controller_components/ai_nn_controller/ /tmp/ai_nn_controller/
RUN pip install --no-cache-dir /tmp/ai_nn_controller
# 2. Plugins
COPY plugins/console_plugin/ /tmp/console_plugin/
RUN pip install --no-cache-dir /tmp/console_plugin
# 3. The app itself (triggers the ai_nn_controller.app_init entry point)
COPY control_applications/my_app/ /app_pkg/
RUN pip install --no-cache-dir /app_pkg
WORKDIR /app
COPY control_applications/my_app/ ./
CMD ["python3", "aic_app.py"]
In docker-compose.yml, mount the plugin source alongside the framework
and app so it can be installed at container start (matching the pattern used
for the framework itself):
aic_server:
build:
context: ./
dockerfile: control_applications/my_app/Dockerfile
volumes:
- ./control_applications/my_app/:/app
- ./controller_components/ai_nn_controller/:/ai_nn_controller
- ./plugins/console_plugin/:/console_plugin
command: >
sh -c "pip install --no-cache-dir /ai_nn_controller
&& pip install --no-cache-dir /console_plugin
&& pip install --no-cache-dir /app
&& python3 aic_app.py --verbose"
If a plugin also runs its own external service (e.g. an MLflow server or
InfluxDB), add a dedicated compose service for it and point the plugin’s
connect() at that service via environment variables — the plugin package
itself still installs into the app’s container, only the backend it talks to
is a separate service.
Best Practices
Keep ``connect()`` idempotent: it may run once at controller startup; don’t assume it’s the only time it runs.
Fail loudly on missing config: if a plugin needs credentials or a URL, raise a clear error from
connect()rather than silently degrading.Use a typed mixin when one fits:
StoragePlugin/ModelRegistryPlugin/MonitoringPlugingive consumers a predictable API.One plugin, one external system: don’t bundle unrelated integrations into a single plugin class.
Version your plugin package: plugins are installed as independent packages (e.g.
console-plugin>=0.1.0) with their own version history.
Next Steps
Plugin Framework API (ai_nn_controller.plugin_framework) — full API reference for
AicPlugin,PluginManager, and entry-point loadingCapability Discovery API (ai_nn_controller.plugins) — the capability-discovery registry and app entry-point loader
Developing Control Applications — declaring
required_pluginson a control applicationPlugin Example — a complete worked example