Status: Needs Review
This page has not been reviewed for accuracy and completeness. Content may be outdated or contain errors.
Plugin System Overview¶
The cuvis-ai plugin system enables extensibility by allowing external packages to contribute nodes, configurations, and functionality to the core framework. This architecture keeps the core framework lean while enabling unlimited extensibility for domain-specific use cases.
Why Plugins?¶
Benefits¶
- Modularity - Keep core framework lean and focused
- Extensibility - Add custom nodes without modifying core
- Distribution - Share implementations via Git or local paths
- Isolation - Plugin failures don't crash core, session-based isolation
- Versioning - Independent plugin versioning using Git tags
- Reproducibility - Git tag-based distribution ensures deterministic builds
Use Cases¶
- Custom anomaly detection algorithms (e.g., AdaCLIP)
- Domain-specific preprocessing nodes
- Proprietary model integrations
- Third-party tool connectors
- Research experiments without polluting core
Plugin Architecture¶
System Overview¶
graph TD
A[User Application] --> B[NodeRegistry]
B --> C[Built-in Nodes<br/>Class-level Registry]
B --> D[Plugin Manager<br/>Instance-level Registry]
D --> E[Git Plugin Loader]
D --> F[Local Plugin Loader]
E --> G[Plugin Cache<br/>~/.cuvis_plugins/]
F --> H[Local Plugin Path]
G --> I[Plugin Nodes]
H --> I
I --> J[Node Registration]
J --> B
style A fill:#e3f2fd
style B fill:#fff3e0
style C fill:#e8f5e9
style D fill:#fff3e0
style I fill:#f3e5f5
Core Components¶
1. NodeRegistry¶
The central registry managing both built-in and plugin nodes.
Hybrid Architecture:
- Class-level registry: O(1) lookup for built-in nodes via @register decorator
- Instance-level registry: Per-session plugin loading for isolation
- Benefits: No overhead for built-ins, flexible plugin management
from cuvis_ai_core.utils.node_registry import NodeRegistry
# Class-level access (built-in nodes)
BuiltinNode = NodeRegistry.get("MinMaxNormalizer")
# Instance-level access (plugin support)
registry = NodeRegistry()
registry.load_plugin(name="adaclip", config={...})
AdaCLIPNode = NodeRegistry.get("AdaCLIPDetector", instance=registry)
2. Plugin Loaders¶
Two plugin loading mechanisms:
Git Plugin Loader:
- Clones repositories using shallow clone (depth=1)
- Checks out specific Git tags (no branches/commits for reproducibility)
- Caches plugins in ~/.cuvis_plugins/plugin_name@tag/
- Verifies tag matches on subsequent loads
Local Plugin Loader: - Loads plugins from local filesystem paths - Supports both absolute and relative paths - Ideal for development and private plugins
3. Plugin Cache¶
Location: ~/.cuvis_plugins/ (Unix/Linux/macOS) or C:\Users\{username}\.cuvis_plugins\ (Windows)
Structure:
~/.cuvis_plugins/
├── adaclip@v0.1.0/
│ ├── .git/
│ ├── cuvis_ai_adaclip/
│ └── pyproject.toml
├── adaclip@v0.1.1/
│ └── ...
└── custom-detector@v2.0.0/
└── ...
Features: - Automatic cache reuse if tag matches - Tag verification prevents cache corruption - Multiple versions can coexist - Manual cache clearing via API
4. Dependency Management¶
Automatic dependency installation from pyproject.toml:
[project]
name = "cuvis-ai-adaclip"
version = "0.1.1"
dependencies = [
"cuvis-ai-core>=0.1.0",
"torch>=2.9.1",
"transformers>=4.36.0",
"pillow>=10.0.0"
]
Installation Process:
- Extracts dependencies using tomllib (PEP 621 compliant)
- Installs via uv pip install for speed
- 300-second timeout with clear error messages
- Validates pyproject.toml presence
Plugin Structure¶
Required Files¶
my-cuvis-plugin/
├── pyproject.toml # PEP 621 compliant project file (REQUIRED)
├── cuvis_ai_plugin/
│ ├── __init__.py
│ ├── nodes/
│ │ ├── __init__.py
│ │ └── custom_node.py # Your node implementations
│ └── configs/
│ └── default.yaml # Optional configurations
├── tests/
│ └── test_nodes.py
├── README.md
└── .gitignore
Plugin Manifest (plugins.yaml)¶
The manifest defines plugin sources and provided nodes:
plugins:
# Git-based plugin with tagged release
adaclip:
repo: "https://github.com/cubert-hyperspectral/cuvis-ai-adaclip.git"
tag: "v0.1.1"
provides:
- cuvis_ai_adaclip.node.adaclip_node.AdaCLIPDetector
# Pre-release tag support
experimental:
repo: "git@github.com:company/experimental-features.git"
tag: "v2.0.0-beta.1"
provides:
- experimental.features.NewFeatureNode
- experimental.features.HelperNode
# Local development plugin (relative path)
local_dev:
path: "../my-custom-plugin"
provides:
- my_plugin.custom.CustomNode
# Local plugin (absolute path)
production_local:
path: "/absolute/path/to/plugin"
provides:
- production_plugin.MainNode
Manifest Schema¶
Git Plugin Configuration:
{
"repo": str, # SSH or HTTPS URL
"tag": str, # Git tag (e.g., v1.2.3, v0.1.0-alpha)
"provides": List[str] # Fully-qualified class paths
}
Local Plugin Configuration:
Validation Rules¶
- Plugin Names: Must be valid Python identifiers
- Class Paths: Must be fully-qualified (package.module.ClassName)
- Git URLs: Must start with
git@,https://, orhttp:// - Tags: Git tags only (no branches or commit hashes)
- Provides: At least one class path required
- Dependencies: Must have
pyproject.tomlfor Git plugins
Plugin Loading¶
Loading Flow¶
sequenceDiagram
participant User
participant Registry as NodeRegistry
participant Loader as Plugin Loader
participant Cache
participant Git as Git Repository
participant DepMgr as Dependency Manager
User->>Registry: load_plugin(name, config)
Registry->>Loader: Parse config
alt Git Plugin
Loader->>Cache: Check cache for tag
alt Cache Hit
Cache-->>Loader: Return cached path
else Cache Miss
Loader->>Git: Shallow clone (depth=1)
Git-->>Loader: Repository cloned
Loader->>Cache: Store at plugin_name@tag/
end
else Local Plugin
Loader->>Loader: Resolve path (relative/absolute)
end
Loader->>DepMgr: Check pyproject.toml
DepMgr->>DepMgr: Extract dependencies
DepMgr->>DepMgr: uv pip install (300s timeout)
Loader->>Loader: Add to sys.path
Loader->>Loader: Clear module cache
Loader->>Loader: Import node classes
Loader->>Registry: Register nodes
Registry-->>User: Plugin loaded
Single Plugin Load¶
from cuvis_ai_core.utils.node_registry import NodeRegistry
registry = NodeRegistry()
# Load from Git repository
registry.load_plugin(
name="adaclip",
config={
"repo": "https://github.com/cubert-hyperspectral/cuvis-ai-adaclip.git",
"tag": "v0.1.1",
"provides": ["cuvis_ai_adaclip.node.adaclip_node.AdaCLIPDetector"]
}
)
# Load from local path
registry.load_plugin(
name="custom",
config={
"path": "../my-custom-plugin",
"provides": ["my_plugin.nodes.CustomNode"]
}
)
Manifest-Based Load¶
Load multiple plugins from a YAML manifest:
registry = NodeRegistry()
registry.load_plugins("path/to/plugins.yaml")
# All plugins from manifest are now loaded
AdaCLIPDetector = NodeRegistry.get("AdaCLIPDetector", instance=registry)
CustomNode = NodeRegistry.get("CustomNode", instance=registry)
CLI Integration¶
restore-pipeline¶
Restore and run pipelines with plugin support:
# Display pipeline information
uv run restore-pipeline --pipeline-path configs/pipeline/adaclip_baseline.yaml
# Load plugins from manifest
uv run restore-pipeline \
--pipeline-path configs/pipeline/adaclip_baseline.yaml \
--plugins-path examples/adaclip/plugins.yaml
# Run inference with plugins
uv run restore-pipeline \
--pipeline-path configs/pipeline/adaclip_baseline.yaml \
--plugins-path examples/adaclip/plugins.yaml \
--cu3s-file-path data/test_sample.cu3s
# Export pipeline visualization
uv run restore-pipeline \
--pipeline-path configs/pipeline/adaclip_baseline.yaml \
--plugins-path examples/adaclip/plugins.yaml \
--pipeline-vis-ext png
restore-trainrun¶
Restore training runs with plugin support:
# Display trainrun information
uv run restore-trainrun \
--trainrun-path outputs/trained_models/trainrun.yaml
# Re-run training with plugins
uv run restore-trainrun \
--trainrun-path outputs/trained_models/trainrun.yaml \
--mode train \
--override training.optimizer.lr=0.001
# Validation mode
uv run restore-trainrun \
--trainrun-path outputs/trained_models/trainrun.yaml \
--mode validate
Plugin Cache Management¶
Set Custom Cache Directory¶
from cuvis_ai_core.utils.node_registry import NodeRegistry
# Change cache location
NodeRegistry.set_cache_dir("/path/to/custom/cache")
Clear Plugin Cache¶
# Clear all plugin caches
NodeRegistry.clear_plugin_cache()
# Clear specific plugin
NodeRegistry.clear_plugin_cache("adaclip")
Manual Cache Cleanup:
# Unix/Linux/macOS
rm -rf ~/.cuvis_plugins/
# Windows PowerShell
Remove-Item -Recurse -Force $env:USERPROFILE\.cuvis_plugins\
Node Registration¶
Automatic Registration¶
Nodes defined in the manifest's provides list are registered automatically when the plugin loads:
registry = NodeRegistry()
registry.load_plugin(
name="adaclip",
config={
"repo": "https://github.com/cubert-hyperspectral/cuvis-ai-adaclip.git",
"tag": "v0.1.1",
"provides": ["cuvis_ai_adaclip.node.adaclip_node.AdaCLIPDetector"]
}
)
# Node automatically registered and retrievable
AdaCLIPDetector = NodeRegistry.get("AdaCLIPDetector", instance=registry)
Node Retrieval Resolution Order¶
Instance Mode (when registry has plugins loaded):
1. Check instance plugin_registry for class name
2. Check instance plugin_registry for last component of full path
3. Check built-in _builtin_registry (O(1) lookup)
4. Try full import path via importlib
Class Mode (built-ins only): 1. Check built-in registry 2. Try importlib for full paths
# Short name (class name only)
Node = NodeRegistry.get("AdaCLIPDetector", instance=registry)
# Full path
Node = NodeRegistry.get(
"cuvis_ai_adaclip.node.adaclip_node.AdaCLIPDetector",
instance=registry
)
Using Plugin Nodes¶
from cuvis_ai_core.utils.node_registry import NodeRegistry
from cuvis_ai_core.pipeline.pipeline import Pipeline
# Load plugins
registry = NodeRegistry()
registry.load_plugins("plugins.yaml")
# Get node class
AdaCLIPDetector = NodeRegistry.get("AdaCLIPDetector", instance=registry)
# Use in pipeline
pipeline_dict = {
"nodes": [
{
"class_name": "AdaCLIPDetector",
"name": "adaclip_detector",
"params": {
"prompt": "plastic wrapper",
"threshold": 0.5
}
}
],
"edges": [...]
}
pipeline = Pipeline.from_dict(pipeline_dict, node_registry=registry)
Plugin Isolation¶
Session-Based Isolation¶
Each NodeRegistry instance maintains independent plugin registries:
# Session 1
registry1 = NodeRegistry()
registry1.load_plugin(name="adaclip", config={...})
# Session 2 (independent)
registry2 = NodeRegistry()
registry2.load_plugin(name="custom", config={...})
# Sessions don't interfere
assert "AdaCLIPDetector" in registry1.plugin_registry
assert "AdaCLIPDetector" not in registry2.plugin_registry
Error Handling¶
Plugin loading errors don't crash the core:
from cuvis_ai_core.utils.node_registry import NodeRegistry
registry = NodeRegistry()
try:
registry.load_plugin(
name="broken-plugin",
config={"repo": "...", "tag": "v1.0.0", "provides": [...]}
)
except Exception as e:
print(f"Plugin failed to load: {e}")
# Core continues running
Helpful Error Messages¶
When a plugin node is not found, the system provides helpful diagnostics:
Node 'cuvis_ai_adaclip.node.adaclip_node.AdaCLIPDetector' not found in registry.
⚠️ This appears to be an external plugin node!
Did you forget to load plugins?
For CLI usage:
uv run restore-pipeline --pipeline-path <path> --plugins-path plugins.yaml
For Python usage:
registry = NodeRegistry()
registry.load_plugins('path/to/plugins.yaml')
Plugin Versioning¶
Semantic Versioning¶
Plugins should follow semver with Git tags: MAJOR.MINOR.PATCH
- MAJOR: Breaking API changes
- MINOR: New features, backwards compatible
- PATCH: Bug fixes
Examples:
- v1.0.0 - Stable release
- v2.1.3 - Minor version with patches
- v0.1.0-alpha - Pre-release (alpha)
- v2.0.0-beta.1 - Pre-release (beta)
Version Constraints¶
In your plugin's pyproject.toml:
[project]
name = "my-cuvis-plugin"
version = "1.0.0"
dependencies = [
"cuvis-ai-core>=0.1.0,<2.0.0", # SemVer range
"numpy>=1.20.0",
"torch>=2.0.0"
]
Managing Multiple Versions¶
Multiple plugin versions can coexist in cache:
Load specific version:
registry.load_plugin(
name="adaclip",
config={
"repo": "https://github.com/cubert-hyperspectral/cuvis-ai-adaclip.git",
"tag": "v0.1.0", # Specific version
"provides": ["cuvis_ai_adaclip.node.adaclip_node.AdaCLIPDetector"]
}
)
Security Considerations¶
Trust Model¶
- Official plugins: Verified and maintained by cuvis team
- Community plugins: User responsibility to vet and audit
- Private plugins: Under your organization's control
Best Practices¶
- Review plugin source before installing from Git
- Check plugin author and repository reputation
- Use specific tags (not branches) for reproducibility
- Audit dependencies in
pyproject.toml - Test plugins in isolation before production use
- Pin versions in manifests for production
Dependency Security¶
# ✅ Good: Specific versions
[project]
dependencies = [
"torch==2.9.1",
"numpy>=1.20.0,<2.0.0"
]
# ⚠️ Avoid: Unpinned versions in production
[project]
dependencies = [
"torch", # Any version - risky!
"numpy>=1.0.0" # Too broad
]
Plugin Lifecycle¶
Plugin Methods¶
| Method | Purpose |
|---|---|
load_plugin(name, config) |
Load single plugin into instance |
load_plugins(manifest_path) |
Load multiple plugins from YAML |
unload_plugin(name) |
Remove plugin and its nodes |
list_plugins() |
Get loaded plugin names |
clear_plugins() |
Unload all plugins in instance |
set_cache_dir(path) |
Change plugin cache location |
clear_plugin_cache(name) |
Clear cached plugin files |
Plugin Lifecycle States¶
stateDiagram-v2
[*] --> Unloaded
Unloaded --> Downloading: load_plugin (Git)
Unloaded --> Resolving: load_plugin (Local)
Downloading --> Cached: Clone & Checkout
Resolving --> PathResolved: Path Found
Cached --> Installing: Extract Dependencies
PathResolved --> Installing: Extract Dependencies
Installing --> Importing: uv pip install
Importing --> Registered: Import Classes
Registered --> Active: Registration Complete
Active --> Unloaded: unload_plugin()
Active --> [*]: clear_plugins()
Performance Considerations¶
Git Plugin Caching¶
- First load: Full clone (~5-30 seconds depending on repo size)
- Subsequent loads: Cache hit (~<1 second)
- Optimization: Shallow clone with
depth=1reduces clone time
Module Cache Clearing¶
When loading plugins, the system clears Python's module cache to prevent conflicts:
# Clears all modules matching plugin package prefixes
clear_package_modules(["cuvis_ai_adaclip", "my_plugin"])
Impact: - Prevents version conflicts - Ensures fresh imports - Minimal overhead (<100ms)
Dependency Installation¶
- Tool: Uses
uv pip installfor speed (~3-10x faster than pip) - Timeout: 300 seconds
- Caching: Python packages cached by uv/pip
gRPC Plugin Service¶
For remote execution, plugins can be loaded via gRPC:
from cuvis_ai_core.grpc.plugin_service import PluginService
# gRPC session maintains independent NodeRegistry
session.node_registry.load_plugins("plugins.yaml")
# Nodes available in session
nodes = session.list_available_nodes()
Features: - Session-based isolation - Remote plugin loading - Port spec extraction for clients - Plugin info queries
See gRPC Plugin Integration for details.
Troubleshooting¶
Plugin Not Loading¶
Symptom:
Solutions:
1. Check Git URL is correct and accessible
2. Verify tag exists in repository: git ls-remote --tags <repo_url>
3. Ensure pyproject.toml exists in plugin root
4. Check dependencies can be installed
5. Review error logs for specific failure
Node Not Found After Loading¶
Symptom:
Solutions:
1. Verify provides list in manifest includes full class path
2. Check class name spelling matches exactly
3. Ensure plugin loaded successfully (no errors)
4. Use registry.list_plugins() to verify plugin is loaded
5. Try full path: NodeRegistry.get("pkg.module.CustomNode", instance=registry)
Import Errors¶
Symptom:
Solutions:
1. Verify __init__.py files exist in package directories
2. Check class path in provides is correct
3. Ensure all dependencies installed (check pyproject.toml)
4. Try importing manually to diagnose: python -c "from my_plugin.nodes import CustomNode"
Cache Corruption¶
Symptom:
Solutions:
1. Clear plugin cache: NodeRegistry.clear_plugin_cache("my-plugin")
2. Or manually delete: rm -rf ~/.cuvis_plugins/my-plugin@*
3. Reload plugin: registry.load_plugin(...)
Dependency Conflicts¶
Symptom:
Solutions:
1. Update conflicting package: pip install --upgrade <package>
2. Use compatible plugin version
3. Create isolated virtual environment
4. Check pyproject.toml for version constraints
See Also¶
- Plugin Development Guide - Create your own plugins
- Plugin Usage Guide - Use plugins in your workflows
- Node System Deep Dive - Understand node architecture
- Pipeline Lifecycle - Pipeline integration
- gRPC API Reference - Remote plugin loading