Plugin Development Guide
Dieser Inhalt ist noch nicht in deiner Sprache verfügbar.
Plugin Development Guide
Section titled “Plugin Development Guide”Learn how to create custom plugins to extend Open Ticket AI functionality.
Packaging & Naming Requirements
Section titled “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 theAppConfig.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, sootai_my_pluginbecomesotai-my-plugininternally. - Registry prefix: when a plugin registers injectables it strips the global prefix (
otai-) and keeps the remainder (e.g.otai-my-plugin→my-plugin). That portion becomes the registry key prefix when combined with the injectable name, such asmy-plugin:MyPipe.
Recommended Project Layout
Section titled “Recommended Project Layout”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.pyEntry Point Contract
Section titled “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.
[project.entry-points."open_ticket_ai.plugins"]my_plugin = "otai_my_plugin.plugin_factory:PluginFactory"Why reference the class?
Section titled “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
AppConfigcan be injected by the IoC container).
Implementing the PluginFactory Class
Section titled “Implementing the PluginFactory Class”All plugins inherit from open_ticket_ai.core.plugins.plugin.Plugin. The base class:
- Accepts
AppConfig(injected by the loader) in its constructor. - Uses the top-level module name to infer the plugin name and registry prefix.
- Calls
_get_all_injectables()duringon_loadand registers eachInjectableautomatically with theComponentRegistryusing 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.
from open_ticket_ai.core.injectables.injectable import Injectablefrom open_ticket_ai.core.plugins.plugin import Plugin
from otai_my_plugin.pipes.my_pipe import MyPipefrom 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:
- Compute the registry prefix (
my-pluginin this example). - Call
MyPipe.get_registry_name()andMyService.get_registry_name(). - Register each injectable as
my-plugin:<registry-name>with the sharedComponentRegistry.
Returning Injectables vs Manual Registration
Section titled “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
Section titled “pyproject.toml Essentials”Ensure the metadata aligns with the naming rules and entry-point contract:
[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
Section titled “Packaging, Distribution & Testing”uv builduv publishInstall your plugin into an Open Ticket AI environment with:
uv pip install otai-my-pluginWrite unit tests alongside your plugin code under tests/unit/ and run them with
uv run -m pytest.
Commercial Plugins & Monetization
Section titled “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
Section titled “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
Section titled “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
Section titled “Monetization Strategies”Strategy 1: Private PyPI + License Keys
Section titled “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:
import osfrom open_ticket_ai.core.plugins.plugin import Pluginfrom 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
Section titled “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:
import osfrom open_ticket_ai.core.plugins.plugin import Pluginfrom 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 verificationBenefits:
- 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
Section titled “Strategy 3: Freemium Model”Offer a free tier with basic features and charge for premium functionality:
import osfrom open_ticket_ai.core.plugins.plugin import Pluginfrom 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 logicLicense Validation Best Practices
Section titled “License Validation Best Practices”1. Environment Variables
Section titled “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
Section titled “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
Section titled “3. Fail Fast”Validate licenses during plugin initialization, not at pipe execution time:
# Good: Fails immediately on startupclass MyPlugin(Plugin): def __init__(self, config): self._validate_license() super().__init__(config)
# Bad: Fails when pipe is executedclass BadPipe(Pipe): def run(self, context): self._validate_license() # Too late!4. Offline Support
Section titled “4. Offline Support”Consider cached validation for intermittent connectivity:
import timeimport jsonfrom 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 False5. Grace Periods & Trials
Section titled “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
Section titled “Getting Your Plugin Listed”To have your plugin listed on the official plugins page:
- Publish to PyPI with the
otai-prefix (e.g.,otai-my-plugin) - Create documentation describing features, installation, and configuration
- 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
- Specify licensing clearly in your plugin’s README and PyPI metadata
Example: Complete Commercial Plugin Structure
Section titled “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 optionsSupport & Maintenance
Section titled “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
Legal Considerations
Section titled “Legal Considerations”- 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