Skip to content

Plugin Development Guide

Learn how to create custom plugins to extend Open Ticket AI functionality.

Packaging & Naming Requirements

Open Ticket AI discovers plugins by their Python package name and project metadata.

  • Distribution/project name: must start with otai-. This matches the AppConfig.PLUGIN_NAME_PREFIX (otai-) that the runtime uses when computing registry keys.
  • Python package name: use the same words as the distribution name but with underscores ( otai_my_plugin). The loader converts the top-level module name to kebab-case automatically, so otai_my_plugin becomes otai-my-plugin internally.
  • Registry prefix: when a plugin registers injectables it strips the global prefix (otai-) and keeps the remainder (e.g. otai-my-pluginmy-plugin). That portion becomes the registry key prefix when combined with the injectable name, such as my-plugin:MyPipe.
otai-my-plugin/
├── pyproject.toml
├── src/
│   └── otai_my_plugin/
│       ├── __init__.py
│       ├── pipes/
│       │   └── my_pipe.py
│       ├── services/
│       │   └── my_service.py
│       └── plugin_factory.py
└── tests/
    └── unit/
        └── test_my_plugin.py

Entry Point Contract

Open Ticket AI loads plugins via the open_ticket_ai.plugins entry-point group. The entry point must resolve to a callable that accepts an AppConfig instance and returns a Plugin. Subclassing Plugin already satisfies that contract, so expose your subclass directly.

toml
[project.entry-points."open_ticket_ai.plugins"]
my_plugin = "otai_my_plugin.plugin_factory:PluginFactory"

Why reference the class?

  • The loader invokes the target like a factory. Referencing the subclass keeps the wiring declarative—no wrapper function required.
  • The class name clarifies that instantiating the plugin may involve dependency wiring (for example, constructor parameters beyond AppConfig can be injected by the IoC container).

Implementing the PluginFactory Class

All plugins inherit from open_ticket_ai.core.plugins.plugin.Plugin. The base class:

  1. Accepts AppConfig (injected by the loader) in its constructor.
  2. Uses the top-level module name to infer the plugin name and registry prefix.
  3. Calls _get_all_injectables() during on_load and registers each Injectable automatically with the ComponentRegistry using the pattern <plugin-prefix>:<injectable-registry-name>.

The separator between the prefix and the injectable name is AppConfig.REGISTRY_IDENTIFIER_SEPERATOR, which defaults to :.

Override _get_all_injectables() to return every injectable you want to expose. You should not call registry.register(...) yourself inside on_load; the base class does it for you.

python
# src/otai_my_plugin/plugin_factory.py

from open_ticket_ai.core.injectables.injectable import Injectable
from open_ticket_ai.core.plugins.plugin import Plugin

from otai_my_plugin.pipes.my_pipe import MyPipe
from otai_my_plugin.services.my_service import MyService


class PluginFactory(Plugin):
    """Create the plugin instance and declare exported injectables."""

    def _get_all_injectables(self) -> list[type[Injectable]]:
        return [
            MyPipe,
            MyService,
        ]

When the loader instantiates PluginFactory, the base implementation will:

  1. Compute the registry prefix (my-plugin in this example).
  2. Call MyPipe.get_registry_name() and MyService.get_registry_name().
  3. Register each injectable as my-plugin:<registry-name> with the shared ComponentRegistry.

Returning Injectables vs Manual Registration

Prior implementations required a setup(registry) helper that performed manual registration. With the factory pattern above you simply return the list of injectables and let the base class handle the rest. This keeps registration consistent and ensures registry names follow the prefix:Injectable convention automatically.

If you do need to opt out of the automatic behaviour—for example, to register additional aliases—you can still access the registry inside _get_all_injectables() by overriding on_load. For typical use cases, returning the list is sufficient and preferred.

pyproject.toml Essentials

Ensure the metadata aligns with the naming rules and entry-point contract:

toml
[project]
name = "otai-my-plugin"
version = "0.1.0"
description = "Custom plugin for Open Ticket AI"
requires-python = ">=3.13"
dependencies = [
    "open-ticket-ai>=1.0.0,<2.0.0",
]

[project.entry-points."open_ticket_ai.plugins"]
my_plugin = "otai_my_plugin.plugin_factory:PluginFactory"

Use the distribution name (otai-my-plugin) to derive both the module prefix (otai_my_plugin) and registry prefix (my-plugin). Keeping these consistent ensures the loader resolves entry points correctly and produces predictable registry keys.

Packaging, Distribution & Testing

uv build
uv publish

Install your plugin into an Open Ticket AI environment with:

uv pip install otai-my-plugin

Write unit tests alongside your plugin code under tests/unit/ and run them with uv run -m pytest.

Commercial Plugins & Monetization

Open Ticket AI fully supports commercial plugin development with complete licensing freedom. You can create and sell plugins with no restrictions from the core project.

Developer Freedom

  • Choose your license: MIT, GPL, proprietary—your choice
  • Set your pricing: Free, paid, subscription, one-time—you decide
  • Support model: Community support, commercial support, or both
  • No revenue sharing: Keep 100% of your plugin sales
  • No marketplace fees: Currently no official marketplace (coming soon)

Current Status & Future Plans

  • Plugin Listings: Available on the documentation site
  • No Marketplace Yet: There is currently no official plugin marketplace or store
  • Future Plans: A dedicated plugins showcase page is planned with search, categories, and community ratings

Monetization Strategies

Strategy 1: Private PyPI + License Keys

Host your commercial plugin on a private PyPI server (DevPI, Gemfury, AWS CodeArtifact) that customers access with authentication tokens. Implement license validation in your plugin's initialization:

python
import os
from open_ticket_ai.core.plugins.plugin import Plugin
from open_ticket_ai.core.injectables.injectable import Injectable


class LicenseError(Exception):
    """Raised when license validation fails."""
    pass


class MyCommercialPlugin(Plugin):
    """Commercial plugin with license validation."""

    def __init__(self, config):
        # Validate license before plugin initialization
        license_key = os.getenv('MY_PLUGIN_LICENSE_KEY')
        if not license_key or not self._validate_license(license_key):
            raise LicenseError(
                "Valid license key required. "
                "Visit https://myplugin.com for licensing."
            )
        super().__init__(config)

    def _validate_license(self, key: str) -> bool:
        """
        Implement your license validation logic.
        Examples:
        - API call to license server
        - Signature verification
        - Offline validation with cryptographic signatures
        """
        # Your validation logic here
        return self._verify_with_license_server(key)

    def _verify_with_license_server(self, key: str) -> bool:
        """Validate license key against remote server."""
        # Example implementation
        import requests
        try:
            response = requests.post(
                'https://api.myplugin.com/validate',
                json={'license_key': key},
                timeout=5
            )
            return response.status_code == 200 and response.json().get('valid')
        except Exception:
            # Consider grace period for network failures
            return False

    def _get_all_injectables(self) -> list[type[Injectable]]:
        return [MyService, MyPipe]

Benefits:

  • Full control over distribution
  • Secure plugin delivery
  • Customer-specific authentication
  • Private code repository

Drawbacks:

  • Requires maintaining private PyPI infrastructure
  • More complex installation for customers
  • Less discoverable

Strategy 2: Public PyPI + Runtime License Enforcement

Publish your plugin to public PyPI for easy discovery, but implement runtime license validation. Installation is free, but usage requires a valid license:

python
import os
from open_ticket_ai.core.plugins.plugin import Plugin
from open_ticket_ai.core.injectables.injectable import Injectable


class LicenseError(Exception):
    """Raised when license validation fails."""
    pass


class PublicCommercialPlugin(Plugin):
    """Publicly available plugin with runtime license enforcement."""

    def _get_all_injectables(self) -> list[type[Injectable]]:
        """Validate license before exposing injectables."""
        license_key = os.getenv('MY_PLUGIN_LICENSE_KEY')

        if not license_key:
            raise LicenseError(
                "License key required to use this plugin.\n"
                "Set the MY_PLUGIN_LICENSE_KEY environment variable.\n"
                "Purchase a license at https://myplugin.com"
            )

        if not self._verify_license(license_key):
            raise LicenseError(
                "Invalid or expired license key.\n"
                "Visit https://myplugin.com to renew or purchase a license."
            )

        return [MyService, MyPipe]

    def _verify_license(self, key: str) -> bool:
        """Your license verification logic."""
        # Example: Check license format, expiration, signature
        return self._check_license_signature(key)

    def _check_license_signature(self, key: str) -> bool:
        """Verify license key signature (example)."""
        # Implement cryptographic signature verification
        # This allows offline validation
        return True  # Replace with actual verification

Benefits:

  • Easy installation via uv add otai-my-plugin
  • Public discovery on PyPI
  • Simple customer onboarding
  • Trial versions possible (e.g., time-limited grace period)

Drawbacks:

  • Plugin code is publicly visible
  • Potential for license bypass attempts
  • Requires robust license validation

Strategy 3: Freemium Model

Offer a free tier with basic features and charge for premium functionality:

python
import os
from open_ticket_ai.core.plugins.plugin import Plugin
from open_ticket_ai.core.injectables.injectable import Injectable


class FreemiumPlugin(Plugin):
    """Plugin with free and premium features."""

    def _get_all_injectables(self) -> list[type[Injectable]]:
        """Return free injectables always, premium only with license."""
        injectables = [
            # Always available
            BasicPipe,
            BasicService,
        ]

        # Add premium features if licensed
        license_key = os.getenv('MY_PLUGIN_LICENSE_KEY')
        if license_key and self._verify_license(license_key):
            injectables.extend([
                PremiumPipe,
                AdvancedService,
            ])

        return injectables

    def _verify_license(self, key: str) -> bool:
        """Verify premium license."""
        return True  # Your validation logic

License Validation Best Practices

1. Environment Variables

Use environment variables for license keys to keep them out of configuration files:

python
license_key = os.getenv('MY_PLUGIN_LICENSE_KEY')

Users set it in their environment:

bash
export MY_PLUGIN_LICENSE_KEY="your-license-key-here"

2. Clear Error Messages

Provide actionable error messages that guide users to purchase licenses:

python
if not license_key:
    raise LicenseError(
        "Missing license key for my-plugin.\n"
        "\n"
        "To use this commercial plugin:\n"
        "1. Purchase a license at https://myplugin.com\n"
        "2. Set the environment variable: MY_PLUGIN_LICENSE_KEY=<your-key>\n"
        "3. Restart Open Ticket AI\n"
        "\n"
        "For questions, contact support@myplugin.com"
    )

3. Fail Fast

Validate licenses during plugin initialization, not at pipe execution time:

python
# Good: Fails immediately on startup
class MyPlugin(Plugin):
    def __init__(self, config):
        self._validate_license()
        super().__init__(config)


# Bad: Fails when pipe is executed
class BadPipe(Pipe):
    def run(self, context):
        self._validate_license()  # Too late!

4. Offline Support

Consider cached validation for intermittent connectivity:

python
import time
import json
from pathlib import Path


def _verify_license(self, key: str) -> bool:
    """Verify license with grace period for offline scenarios."""
    cache_file = Path.home() / '.my_plugin_license_cache'

    # Try online validation first
    if self._online_validation(key):
        # Cache successful validation
        cache_file.write_text(json.dumps({
            'validated_at': time.time(),
            'key_hash': hashlib.sha256(key.encode()).hexdigest()
        }))
        return True

    # Fall back to cached validation (24-hour grace period)
    if cache_file.exists():
        cache = json.loads(cache_file.read_text())
        age = time.time() - cache['validated_at']
        if age < 86400:  # 24 hours
            return True

    return False

5. Grace Periods & Trials

Allow reasonable trial periods or grace windows:

python
def _verify_license(self, key: str) -> bool:
    """Allow 14-day trial period."""
    if key.startswith('TRIAL-'):
        # Extract trial start date from key
        trial_start = self._parse_trial_date(key)
        days_elapsed = (datetime.now() - trial_start).days

        if days_elapsed <= 14:
            print(f"Trial mode: {14 - days_elapsed} days remaining")
            return True
        else:
            raise LicenseError(
                "Trial period expired. Purchase a license at https://myplugin.com"
            )

    return self._verify_paid_license(key)

Getting Your Plugin Listed

To have your plugin listed on the official plugins page:

  1. Publish to PyPI with the otai- prefix (e.g., otai-my-plugin)
  2. Create documentation describing features, installation, and configuration
  3. Submit a PR to the Open Ticket AI documentation repository with:
    • Plugin name and description
    • Installation instructions
    • Pricing model (free, commercial, freemium)
    • Link to documentation
    • Support contact information
  4. Specify licensing clearly in your plugin's README and PyPI metadata

Example: Complete Commercial Plugin Structure

otai-my-commercial-plugin/
├── pyproject.toml              # Project metadata & dependencies
├── LICENSE                     # Your chosen license (proprietary, etc.)
├── README.md                   # Installation & purchase info
├── src/
│   └── otai_my_commercial_plugin/
│       ├── __init__.py
│       ├── plugin_factory.py   # Plugin with license validation
│       ├── licensing.py        # License validation logic
│       ├── pipes/
│       │   └── my_pipe.py
│       └── services/
│           └── my_service.py
├── tests/
│   └── unit/
│       ├── test_licensing.py   # Test license validation
│       └── test_plugin.py
└── docs/
    ├── installation.md         # How to install & license
    ├── configuration.md        # How to configure
    └── pricing.md             # Pricing & licensing options

Support & Maintenance

As a commercial plugin developer, consider:

  • Documentation: Comprehensive guides for installation, configuration, troubleshooting
  • Support Channels: Email, issue tracker, Discord/Slack community
  • Updates: Regular releases for bug fixes and new features
  • Compatibility: Test against new Open Ticket AI versions
  • Migration Guides: Help users upgrade between versions
  • License Agreement: Provide clear terms of use
  • Privacy Policy: If collecting any data (license validation calls, analytics)
  • Compliance: Ensure your plugin complies with relevant regulations (GDPR, etc.)
  • Intellectual Property: Protect your code and trademarks appropriately

Additional Resources