Chapter 6: Plugin Architecture¶
Overview¶
The FCC framework is designed to be extended without modifying its core. The plugin system provides a structured way for third-party packages to contribute personas, engines, templates, scorers, validators, AI providers, governance rules, scenarios, workflows, and event subscribers.
Every plugin type has an abstract base class (ABC), a dedicated entry-point group, and a place in the PluginRegistry discovery pipeline. This chapter explains how plugins are discovered, loaded, validated, and used.
The mind map below groups the ten FCC plugin types into four practical clusters — Content, Engine, Quality, and System — giving a quick map of where third-party extensions attach.
mindmap
root((10 Plugin Types))
Content Plugins
Personas
Contribute YAML definitions
Scenarios
Add simulation scenarios
Workflows
Custom workflow graphs
Templates
Jinja2 template directories
Engine Plugins
Engines
Custom simulation engines
Providers
AI provider clients
Quality Plugins
Scorers
Quality scoring functions
Validators
Validation rules
System Plugins
Governance
Tags and quality gates
Subscribers
Event bus subscribers
The subsequent table restates these groups as their concrete entry-point names and ABC classes so you can match the map to the actual pyproject.toml wiring.
The 10 Plugin Types¶
Each plugin type maps to an entry-point group and an ABC defined in src/fcc/plugins/base.py:
| # | Plugin Type | Entry-Point Group | ABC | Purpose |
|---|---|---|---|---|
| 1 | Personas | fcc.plugins.personas |
PersonaPlugin |
Contribute persona YAML definitions |
| 2 | Engines | fcc.plugins.engines |
EnginePlugin |
Contribute simulation engine classes |
| 3 | Templates | fcc.plugins.templates |
TemplatePlugin |
Contribute Jinja2 template directories |
| 4 | Scorers | fcc.plugins.scorers |
ScorerPlugin |
Contribute quality scoring functions |
| 5 | Validators | fcc.plugins.validators |
ValidatorPlugin |
Contribute validation rules |
| 6 | Providers | fcc.plugins.providers |
AIProviderPlugin |
Contribute AI provider client implementations |
| 7 | Governance | fcc.plugins.governance |
GovernancePlugin |
Contribute tags and quality gates |
| 8 | Scenarios | fcc.plugins.scenarios |
ScenarioPlugin |
Contribute scenario JSON files |
| 9 | Workflows | fcc.plugins.workflows |
WorkflowPlugin |
Contribute workflow graph definitions |
| 10 | Subscribers | fcc.plugins.subscribers |
EventSubscriberPlugin |
Contribute event bus subscribers |
Entry-Point Discovery¶
Plugins are discovered via Python's standard importlib.metadata.entry_points() mechanism. A package registers a plugin by declaring an entry point in its pyproject.toml:
When PluginRegistry.discover() runs, it iterates over all 10 entry-point groups, loads each entry point, instantiates the class, reads its plugin_meta(), and registers the result:
from fcc.plugins.registry import PluginRegistry
registry = PluginRegistry()
result = registry.discover()
print(result.discovered) # total entry points found
print(result.loaded) # successfully loaded
print(result.errors) # loading failures
The discovery process is resilient: a failing plugin is logged and skipped, not propagated. This ensures that a broken third-party package does not prevent the rest of the framework from operating.
Plugin Lifecycle¶
A plugin moves through four stages:
1. Discovery¶
PluginRegistry.discover() scans all entry-point groups and finds callable entry points.
2. Instantiation¶
Each entry point is loaded (ep.load()) and instantiated (plugin_cls()). The instance must have a plugin_meta() method.
3. Registration¶
The PluginMeta returned by plugin_meta() is used to register the plugin. If a plugin ID conflicts with an already-registered plugin, a PluginConflictError is raised.
4. Validation¶
After discovery, registry.validate(plugin_id) checks that the instance is an instance of the expected ABC for its declared plugin type. registry.validate_all() validates every loaded plugin and returns a dict of {plugin_id: [errors]}.
errors = registry.validate_all()
for plugin_id, issues in errors.items():
if issues:
print(f"{plugin_id}: {issues}")
PluginMeta Metadata¶
Every plugin exposes a PluginMeta frozen dataclass:
from fcc.plugins.base import PluginMeta, PluginType
@dataclass(frozen=True)
class PluginMeta:
id: str # unique identifier
name: str # human-readable name
version: str # semver string
plugin_type: PluginType # enum value
description: str = "" # optional description
author: str = "" # optional author
source_package: str = "" # pip package name
tags: tuple[str, ...] = () # classification tags
The id must be globally unique across all loaded plugins. The plugin_type determines which ABC the instance is validated against.
Cross-Plugin Orchestration¶
When multiple plugins contribute personas that need to interact, the CrossPluginOrchestrator in src/fcc/plugins/orchestration.py manages the relationships.
Plugin Dependencies¶
PluginDependency models a dependency between two plugins:
from fcc.plugins.orchestration import PluginDependency, CrossPluginOrchestrator
dep = PluginDependency(
source_plugin="ml_lifecycle",
target_plugin="data_engineering",
dependency_type="provides_metadata",
description="ML lifecycle personas consume data lineage metadata",
)
orchestrator = CrossPluginOrchestrator(dependencies=[dep])
deps = orchestrator.resolve_dependencies("ml_lifecycle")
reverse = orchestrator.reverse_dependencies("data_engineering")
Plugin Interactions¶
PluginInteraction models a persona-to-persona interaction that crosses plugin boundaries:
from fcc.plugins.orchestration import PluginInteraction
interaction = PluginInteraction(
source_persona="DE_PIPE",
source_plugin="data_engineering",
target_persona="ML_TRAIN",
target_plugin="ml_lifecycle",
interaction_type="upstream",
description="Pipeline output feeds model training",
)
orchestrator.add_interaction(interaction)
matrix = orchestrator.get_interaction_matrix()
Ecosystem Health¶
The orchestrator aggregates per-plugin health checks into an EcosystemHealthReport:
from fcc.plugins.orchestration import PluginHealthStatus
statuses = [
PluginHealthStatus(plugin_id="ml_lifecycle", healthy=True, persona_count=9),
PluginHealthStatus(plugin_id="data_engineering", healthy=True, persona_count=6),
]
report = orchestrator.check_health(statuses)
print(report.total_plugins) # 2
print(report.healthy_plugins) # 2
print(report.total_personas) # 15
Writing a Custom Plugin¶
Here is a minimal example of a persona plugin:
from pathlib import Path
from fcc.plugins.base import PersonaPlugin, PluginMeta, PluginType
class MyPersonaPlugin(PersonaPlugin):
"""Contributes custom personas to the FCC registry."""
def plugin_meta(self) -> PluginMeta:
return PluginMeta(
id="my-personas",
name="My Custom Personas",
version="1.0.0",
plugin_type=PluginType.PERSONAS,
description="Domain-specific personas for my project",
author="My Team",
source_package="my-fcc-plugin",
)
def get_persona_paths(self) -> list[Path]:
"""Return paths to YAML files with persona definitions."""
return [Path(__file__).parent / "data" / "personas.yaml"]
def get_dimension_paths(self) -> list[Path]:
"""Optional: return paths to dimension profile YAMLs."""
return [Path(__file__).parent / "data" / "dimensions.yaml"]
def get_cross_reference_paths(self) -> list[Path]:
"""Optional: return paths to cross-reference YAMLs."""
return []
Register it in pyproject.toml:
After installing the package, PluginRegistry.discover() will find and load the plugin automatically.
Plugin Testing¶
Test plugins by instantiating them directly, without entry-point discovery:
def test_my_plugin():
plugin = MyPersonaPlugin()
meta = plugin.plugin_meta()
assert meta.id == "my-personas"
assert meta.plugin_type == PluginType.PERSONAS
paths = plugin.get_persona_paths()
assert len(paths) >= 1
for p in paths:
assert p.exists(), f"Persona YAML not found: {p}"
Accessing Plugins at Runtime¶
After discovery, use the registry to access plugin instances:
from fcc.plugins.base import PluginType
# All persona plugins
persona_plugins = registry.get_plugins(PluginType.PERSONAS)
for plugin in persona_plugins:
for path in plugin.get_persona_paths():
print(f"Loading personas from: {path}")
# Specific plugin by ID
my_plugin = registry.get_plugin("my-personas")
# Metadata for all plugins
all_meta = registry.get_all_meta()
# Count by type
counts = registry.plugins_by_type()
# {"fcc.plugins.personas": 3, "fcc.plugins.engines": 1, ...}
Key Takeaways
- 10 plugin types cover every extension point: personas, engines, templates, scorers, validators, providers, governance, scenarios, workflows, and subscribers.
- Plugins are discovered via
importlib.metadata.entry_points()and registered inPluginRegistry.- Each plugin exposes
PluginMetametadata and implements the ABC for its type.CrossPluginOrchestratormanages inter-plugin dependencies, persona interactions, and ecosystem health.- Writing a plugin requires an ABC implementation, a
PluginMeta, and apyproject.tomlentry point.- Discovery is resilient: broken plugins are logged, not propagated.
Previous: Chapter 5 -- The Workflow System | Next: Chapter 7 -- Event Bus and Observability
Try this in Notebook 06