Plugin-Entwicklungsleitfaden

Komplette Anleitung zur Entwicklung von benutzerdefinierten Plugins für Open Ticket AI, einschließlich Projektstruktur, Entry Points und Best Practices.

Plugin-Entwicklungsleitfaden

Erfahren Sie, wie Sie benutzerdefinierte Plugins erstellen, um die Funktionalität von Open Ticket AI zu erweitern.

Packaging & Namensregeln

Open Ticket AI findet Plugins durch ihren Python-Paketnamen und Projekt-Metadaten.

  • Distribution/Projektname: muss mit otai- beginnen. Dies entspricht dem AppConfig.PLUGIN_NAME_PREFIX (otai-), den die Runtime verwendet, wenn Registry Keys berechnet werden.
  • Python-Paketname: verwenden Sie die gleichen Wörter wie den Distribution-Name, aber mit Unterstrichen ( otai_my_plugin). Der Loader konvertiert den Top-Level-Modulnamen automatisch zu kebab-case, sodass otai_my_plugin intern zu otai-my-plugin wird.
  • Registry Prefix: wenn ein Plugin Injectables registriert, entfernt es den globalen Prefix (otai-) und behält den Rest (z.B. otai-my-pluginmy-plugin). Dieser Teil wird der Registry Key Prefix, wenn er mit dem Injectable-Namen kombiniert wird, wie my-plugin:MyPipe.

Empfohlene Projektstruktur

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 lädt Plugins über die Entry-Point-Gruppe open_ticket_ai.plugins. Der Entry Point muss auf eine Callable resolven, die eine AppConfig Instanz akzeptiert und ein Plugin zurückgibt. Subclassing von Plugin erfüllt diesen Contract bereits, daher können Sie Ihre Subklasse direkt exponieren.

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

Warum die Klasse referenzieren?

  • Der Loader ruft das Ziel wie eine Factory auf. Referenzieren der Subklasse macht die Wiring deklarativ – keine Wrapper-Funktion erforderlich.
  • Der Klassenname verdeutlicht, dass die Instantiierung des Plugins Dependency Wiring involvieren kann (zum Beispiel, Constructor-Parameter neben AppConfig können vom IoC Container injiziert werden).

Implementierung der PluginFactory Klasse

Alle Plugins inheriten von open_ticket_ai.core.plugins.plugin.Plugin. Die Basisklasse:

  1. Akzeptiert AppConfig (injiziert vom Loader) in ihrem Constructor.
  2. Nutzt den Top-Level-Modulnamen, um den Plugin-Namen und Registry Prefix zu inferieren.
  3. Ruft _get_all_injectables() während on_load auf und registriert jedes Injectable automatisch mit dem ComponentRegistry unter dem Pattern <plugin-prefix>:<injectable-registry-name>.

Der Separator zwischen Prefix und Injectable-Namen ist AppConfig.REGISTRY_IDENTIFIER_SEPERATOR, der standardmäßig : ist.

Override _get_all_injectables() um jedes Injectable zurückzugeben, das Sie exponieren möchten. Sie sollten nicht registry.register(...) selbst innerhalb on_load aufrufen; die Basisklasse tut dies für Sie.

# 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):
    """Erstellt die Plugin-Instanz und deklariert exportierte Injectables."""

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

Wenn der Loader PluginFactory instantiert, wird die Basisimplementierung:

  1. Den Registry Prefix berechnen (my-plugin in diesem Beispiel).
  2. MyPipe.get_registry_name() und MyService.get_registry_name() aufrufen.
  3. Jedes Injectable als my-plugin:<registry-name> mit dem shared ComponentRegistry registrieren.

Injectables zurückgeben vs. manuelle Registrierung

Vorherige Implementierungen benötigten einen setup(registry) Helper, der manuelle Registrierung durchführte. Mit dem Factory Pattern oben geben Sie einfach die Liste der Injectables zurück und lassen die Basisklasse den Rest handeln. Dies macht die Registrierung konsistent und stellt sicher, dass Registry Namen automatisch dem prefix:Injectable Convention folgen.

Wenn Sie sich vom automatischen Behavior opt-out müssen – zum Beispiel, um zusätzliche Aliases zu registrieren – können Sie noch innerhalb _get_all_injectables() auf die Registry zugreifen, indem Sie on_load override. Für typische Use Cases ist die Rückgabe der Liste sufficient und preferred.

pyproject.toml Essentials

Stellen Sie sicher, dass die Metadaten mit den Namensregeln und dem Entry-Point Contract align:

[project]
name = "otai-my-plugin"
version = "0.1.0"
description = "Benutzerdefiniertes Plugin für Open Ticket AI"
requires-python = ">=3.13"
dependencies = [
    "isOpen-ticket-ai>=1.0.0,<2.0.0",
]

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

Nutzen Sie den Distribution-Name (otai-my-plugin) um sowohl den Modul Prefix (otai_my_plugin) als auch Registry Prefix (my-plugin) zu derivieren. Diese konsistent zu halten stellt sicher, dass der Loader Entry Points korrekt resolves und predictable Registry Keys produziert.

Packaging, Distribution & Testing

uv build
uv publish

Installieren Sie Ihr Plugin in eine Open Ticket AI Umgebung mit:

uv pip install otai-my-plugin

Schreiben Sie Unit Tests neben Ihrem Plugin Code unter tests/unit/ und führen Sie sie mit uv run -m pytest aus.

Commercial Plugins & Monetization

Open Ticket AI unterstützt Commercial Plugin Development vollständig mit kompletter Licensing Freedom. Sie können Plugins erstellen und verkaufen ohne Restriktionen vom Core Project.

Developer Freedom

  • Choose your license: MIT, GPL, proprietary – Ihre Wahl
  • Set your pricing: Free, paid, subscription, one-time – Sie entscheiden
  • Support model: Community Support, Commercial Support, oder beide
  • No revenue sharing: Keep 100% of your plugin sales
  • No marketplace fees: Currently no official marketplace (coming soon)

Current Status & Future Plans

  • Plugin Listings: Verfügbar auf der Documentation Site
  • No Marketplace Yet: Es gibt aktuell keinen official Plugin Marketplace oder Store
  • Future Plans: Eine dedicated Plugins Showcase Page ist geplant mit Search, Categories und Community Ratings

Monetization Strategies

Strategy 1: Private PyPI + License Keys

Hosten Sie Ihr Commercial Plugin auf einem Private PyPI Server (DevPI, Gemfury, AWS CodeArtifact), den Customers mit Authentication Tokens zugreifen. Implementieren Sie License Validation in Ihrem Plugin’s Initialization:

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[tone[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

Publizieren Sie Ihr Plugin auf Public PyPI für einfache Discovery, aber implementieren Sie Runtime License Validation. Installation ist free, aber Usage benötigt eine valid License:

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[tone[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:

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[tone[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:

license_key = os.getenv('MY_PLUGIN_LICENSE_KEY')

Users set it in their environment:

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

2. Clear Error Messages

Provide actionable error messages that guide users to purchase licenses:

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:

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

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:

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