| """ |
| Plugin Registry β Drop-in extension system for backends, tools, callbacks, and evaluators. |
| |
| Makes the framework extensible without editing core code: |
| - Register new LLM backends |
| - Register new tools |
| - Register new callbacks |
| - Register new SLM models |
| - Register new evaluation metrics |
| |
| Adding a new component = 1 file + 1 register() call. |
| |
| Extension points: |
| BackendRegistry β LLM/SLM backends |
| ToolRegistry β Already exists in tools.py, re-exported here |
| CallbackRegistry β Observability callbacks |
| ModelRegistry β SLM model definitions (extends SLM_REGISTRY) |
| EmbeddingRegistry β Shared embedding backends (deduplicates embed logic) |
| |
| Usage: |
| # In your extension file: |
| from purpose_agent.registry import backend_registry, model_registry |
| |
| backend_registry.register("my_backend", MyCustomBackend) |
| model_registry.register("my-slm", ollama_name="my-model:latest", context_window=32768, description="My custom SLM") |
| |
| # Then use it: |
| backend = backend_registry.create("my_backend", model="my-model") |
| slm = model_registry.create_backend("my-slm") |
| """ |
|
|
| from __future__ import annotations |
|
|
| import logging |
| import math |
| from typing import Any, Callable, Type |
|
|
| logger = logging.getLogger(__name__) |
|
|
|
|
| |
| |
| |
|
|
| class PluginRegistry: |
| """ |
| Generic registry for named plugins. |
| |
| Supports: |
| - Register by name + class/factory |
| - Create instances by name |
| - List available plugins |
| - Discover plugins via entry points (future) |
| """ |
|
|
| def __init__(self, kind: str): |
| self.kind = kind |
| self._plugins: dict[str, Type | Callable] = {} |
| self._metadata: dict[str, dict[str, Any]] = {} |
|
|
| def register( |
| self, name: str, cls_or_factory: Type | Callable, **metadata |
| ) -> "PluginRegistry": |
| """Register a plugin by name.""" |
| self._plugins[name] = cls_or_factory |
| self._metadata[name] = metadata |
| logger.debug(f"{self.kind} registry: registered '{name}'") |
| return self |
|
|
| def create(self, name: str, **kwargs) -> Any: |
| """Create an instance of a registered plugin.""" |
| if name not in self._plugins: |
| available = ", ".join(self._plugins.keys()) |
| raise ValueError( |
| f"Unknown {self.kind} '{name}'. Available: {available}" |
| ) |
| return self._plugins[name](**kwargs) |
|
|
| def get_class(self, name: str) -> Type | Callable | None: |
| """Get the class/factory without instantiating.""" |
| return self._plugins.get(name) |
|
|
| def list(self) -> list[dict[str, Any]]: |
| """List all registered plugins with metadata.""" |
| return [ |
| {"name": name, **self._metadata.get(name, {})} |
| for name in self._plugins |
| ] |
|
|
| def names(self) -> list[str]: |
| return list(self._plugins.keys()) |
|
|
| def __contains__(self, name: str) -> bool: |
| return name in self._plugins |
|
|
| def __len__(self) -> int: |
| return len(self._plugins) |
|
|
|
|
| |
| |
| |
|
|
| class EmbeddingBackend: |
| """ |
| Abstract embedding backend. Swap in sentence-transformers, OpenAI, etc. |
| |
| Default: lightweight trigram hashing (no dependencies, fast, approximate). |
| """ |
|
|
| def __init__(self, dim: int = 128): |
| self.dim = dim |
|
|
| def embed(self, text: str) -> list[float]: |
| """Compute embedding for text. Override for real embeddings.""" |
| vec = [0.0] * self.dim |
| text_lower = text.lower() |
| for i in range(len(text_lower) - 2): |
| trigram = text_lower[i:i + 3] |
| h = hash(trigram) % self.dim |
| vec[h] += 1.0 |
| magnitude = math.sqrt(sum(x * x for x in vec)) |
| if magnitude > 0: |
| vec = [x / magnitude for x in vec] |
| return vec |
|
|
| @staticmethod |
| def cosine_similarity(a: list[float], b: list[float]) -> float: |
| if not a or not b or len(a) != len(b): |
| return 0.0 |
| dot = sum(x * y for x, y in zip(a, b)) |
| mag_a = math.sqrt(sum(x * x for x in a)) |
| mag_b = math.sqrt(sum(x * x for x in b)) |
| if mag_a == 0 or mag_b == 0: |
| return 0.0 |
| return dot / (mag_a * mag_b) |
|
|
|
|
| |
| default_embedding = EmbeddingBackend(dim=128) |
|
|
|
|
| |
| |
| |
|
|
| |
| backend_registry = PluginRegistry("Backend") |
|
|
| |
| callback_registry = PluginRegistry("Callback") |
|
|
| |
| model_registry = PluginRegistry("Model") |
|
|
|
|
| def _register_defaults(): |
| """Register built-in plugins. Called once at import time.""" |
| |
| from purpose_agent.llm_backend import ( |
| MockLLMBackend, HFInferenceBackend, OpenAICompatibleBackend, |
| ) |
| backend_registry.register("mock", MockLLMBackend, description="Deterministic mock for testing") |
| backend_registry.register("hf_inference", HFInferenceBackend, description="HuggingFace Inference Providers") |
| backend_registry.register("openai", OpenAICompatibleBackend, description="OpenAI-compatible API") |
|
|
| from purpose_agent.slm_backends import OllamaBackend, LlamaCppBackend, SLM_REGISTRY |
| backend_registry.register("ollama", OllamaBackend, description="Local Ollama serving") |
| backend_registry.register("llama_cpp", LlamaCppBackend, description="Direct llama-cpp-python") |
|
|
| |
| for key, (ollama_name, ctx, desc) in SLM_REGISTRY.items(): |
| model_registry.register( |
| key, |
| lambda host="http://localhost:11434", _m=ollama_name, _c=ctx: OllamaBackend( |
| model=_m, host=host, context_window=_c, compress_prompts=True, |
| ), |
| ollama_name=ollama_name, |
| context_window=ctx, |
| description=desc, |
| ) |
|
|
| |
| from purpose_agent.observability import LoggingCallback, MetricsCollector, JSONFileCallback |
| callback_registry.register("logging", LoggingCallback, description="Log all events") |
| callback_registry.register("metrics", MetricsCollector, description="Collect aggregate metrics") |
| callback_registry.register("jsonfile", JSONFileCallback, description="Write events to JSONL file") |
|
|
|
|
| |
| _register_defaults() |
|
|