refactor: modularity fixes + plugin registry + compiled research
Browse files- purpose_agent/registry.py +191 -0
purpose_agent/registry.py
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Plugin Registry — Drop-in extension system for backends, tools, callbacks, and evaluators.
|
| 3 |
+
|
| 4 |
+
Makes the framework extensible without editing core code:
|
| 5 |
+
- Register new LLM backends
|
| 6 |
+
- Register new tools
|
| 7 |
+
- Register new callbacks
|
| 8 |
+
- Register new SLM models
|
| 9 |
+
- Register new evaluation metrics
|
| 10 |
+
|
| 11 |
+
Adding a new component = 1 file + 1 register() call.
|
| 12 |
+
|
| 13 |
+
Extension points:
|
| 14 |
+
BackendRegistry — LLM/SLM backends
|
| 15 |
+
ToolRegistry — Already exists in tools.py, re-exported here
|
| 16 |
+
CallbackRegistry — Observability callbacks
|
| 17 |
+
ModelRegistry — SLM model definitions (extends SLM_REGISTRY)
|
| 18 |
+
EmbeddingRegistry — Shared embedding backends (deduplicates embed logic)
|
| 19 |
+
|
| 20 |
+
Usage:
|
| 21 |
+
# In your extension file:
|
| 22 |
+
from purpose_agent.registry import backend_registry, model_registry
|
| 23 |
+
|
| 24 |
+
backend_registry.register("my_backend", MyCustomBackend)
|
| 25 |
+
model_registry.register("my-slm", ollama_name="my-model:latest", context_window=32768, description="My custom SLM")
|
| 26 |
+
|
| 27 |
+
# Then use it:
|
| 28 |
+
backend = backend_registry.create("my_backend", model="my-model")
|
| 29 |
+
slm = model_registry.create_backend("my-slm")
|
| 30 |
+
"""
|
| 31 |
+
|
| 32 |
+
from __future__ import annotations
|
| 33 |
+
|
| 34 |
+
import logging
|
| 35 |
+
import math
|
| 36 |
+
from typing import Any, Callable, Type
|
| 37 |
+
|
| 38 |
+
logger = logging.getLogger(__name__)
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
# ---------------------------------------------------------------------------
|
| 42 |
+
# Generic Plugin Registry
|
| 43 |
+
# ---------------------------------------------------------------------------
|
| 44 |
+
|
| 45 |
+
class PluginRegistry:
|
| 46 |
+
"""
|
| 47 |
+
Generic registry for named plugins.
|
| 48 |
+
|
| 49 |
+
Supports:
|
| 50 |
+
- Register by name + class/factory
|
| 51 |
+
- Create instances by name
|
| 52 |
+
- List available plugins
|
| 53 |
+
- Discover plugins via entry points (future)
|
| 54 |
+
"""
|
| 55 |
+
|
| 56 |
+
def __init__(self, kind: str):
|
| 57 |
+
self.kind = kind
|
| 58 |
+
self._plugins: dict[str, Type | Callable] = {}
|
| 59 |
+
self._metadata: dict[str, dict[str, Any]] = {}
|
| 60 |
+
|
| 61 |
+
def register(
|
| 62 |
+
self, name: str, cls_or_factory: Type | Callable, **metadata
|
| 63 |
+
) -> "PluginRegistry":
|
| 64 |
+
"""Register a plugin by name."""
|
| 65 |
+
self._plugins[name] = cls_or_factory
|
| 66 |
+
self._metadata[name] = metadata
|
| 67 |
+
logger.debug(f"{self.kind} registry: registered '{name}'")
|
| 68 |
+
return self
|
| 69 |
+
|
| 70 |
+
def create(self, name: str, **kwargs) -> Any:
|
| 71 |
+
"""Create an instance of a registered plugin."""
|
| 72 |
+
if name not in self._plugins:
|
| 73 |
+
available = ", ".join(self._plugins.keys())
|
| 74 |
+
raise ValueError(
|
| 75 |
+
f"Unknown {self.kind} '{name}'. Available: {available}"
|
| 76 |
+
)
|
| 77 |
+
return self._plugins[name](**kwargs)
|
| 78 |
+
|
| 79 |
+
def get_class(self, name: str) -> Type | Callable | None:
|
| 80 |
+
"""Get the class/factory without instantiating."""
|
| 81 |
+
return self._plugins.get(name)
|
| 82 |
+
|
| 83 |
+
def list(self) -> list[dict[str, Any]]:
|
| 84 |
+
"""List all registered plugins with metadata."""
|
| 85 |
+
return [
|
| 86 |
+
{"name": name, **self._metadata.get(name, {})}
|
| 87 |
+
for name in self._plugins
|
| 88 |
+
]
|
| 89 |
+
|
| 90 |
+
def names(self) -> list[str]:
|
| 91 |
+
return list(self._plugins.keys())
|
| 92 |
+
|
| 93 |
+
def __contains__(self, name: str) -> bool:
|
| 94 |
+
return name in self._plugins
|
| 95 |
+
|
| 96 |
+
def __len__(self) -> int:
|
| 97 |
+
return len(self._plugins)
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
# ---------------------------------------------------------------------------
|
| 101 |
+
# Shared Embedding Utility — deduplicated from ExperienceReplay + ToolRegistry
|
| 102 |
+
# ---------------------------------------------------------------------------
|
| 103 |
+
|
| 104 |
+
class EmbeddingBackend:
|
| 105 |
+
"""
|
| 106 |
+
Abstract embedding backend. Swap in sentence-transformers, OpenAI, etc.
|
| 107 |
+
|
| 108 |
+
Default: lightweight trigram hashing (no dependencies, fast, approximate).
|
| 109 |
+
"""
|
| 110 |
+
|
| 111 |
+
def __init__(self, dim: int = 128):
|
| 112 |
+
self.dim = dim
|
| 113 |
+
|
| 114 |
+
def embed(self, text: str) -> list[float]:
|
| 115 |
+
"""Compute embedding for text. Override for real embeddings."""
|
| 116 |
+
vec = [0.0] * self.dim
|
| 117 |
+
text_lower = text.lower()
|
| 118 |
+
for i in range(len(text_lower) - 2):
|
| 119 |
+
trigram = text_lower[i:i + 3]
|
| 120 |
+
h = hash(trigram) % self.dim
|
| 121 |
+
vec[h] += 1.0
|
| 122 |
+
magnitude = math.sqrt(sum(x * x for x in vec))
|
| 123 |
+
if magnitude > 0:
|
| 124 |
+
vec = [x / magnitude for x in vec]
|
| 125 |
+
return vec
|
| 126 |
+
|
| 127 |
+
@staticmethod
|
| 128 |
+
def cosine_similarity(a: list[float], b: list[float]) -> float:
|
| 129 |
+
if not a or not b or len(a) != len(b):
|
| 130 |
+
return 0.0
|
| 131 |
+
dot = sum(x * y for x, y in zip(a, b))
|
| 132 |
+
mag_a = math.sqrt(sum(x * x for x in a))
|
| 133 |
+
mag_b = math.sqrt(sum(x * x for x in b))
|
| 134 |
+
if mag_a == 0 or mag_b == 0:
|
| 135 |
+
return 0.0
|
| 136 |
+
return dot / (mag_a * mag_b)
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
# Singleton shared instance (override with embedding_backend = SentenceTransformerBackend(...))
|
| 140 |
+
default_embedding = EmbeddingBackend(dim=128)
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
# ---------------------------------------------------------------------------
|
| 144 |
+
# Pre-built Registries
|
| 145 |
+
# ---------------------------------------------------------------------------
|
| 146 |
+
|
| 147 |
+
# Backend registry — for LLM/SLM backends
|
| 148 |
+
backend_registry = PluginRegistry("Backend")
|
| 149 |
+
|
| 150 |
+
# Callback registry — for observability callbacks
|
| 151 |
+
callback_registry = PluginRegistry("Callback")
|
| 152 |
+
|
| 153 |
+
# Model registry — extensible SLM model definitions
|
| 154 |
+
model_registry = PluginRegistry("Model")
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
def _register_defaults():
|
| 158 |
+
"""Register built-in plugins. Called once at import time."""
|
| 159 |
+
# Register built-in backends
|
| 160 |
+
from purpose_agent.llm_backend import (
|
| 161 |
+
MockLLMBackend, HFInferenceBackend, OpenAICompatibleBackend,
|
| 162 |
+
)
|
| 163 |
+
backend_registry.register("mock", MockLLMBackend, description="Deterministic mock for testing")
|
| 164 |
+
backend_registry.register("hf_inference", HFInferenceBackend, description="HuggingFace Inference Providers")
|
| 165 |
+
backend_registry.register("openai", OpenAICompatibleBackend, description="OpenAI-compatible API")
|
| 166 |
+
|
| 167 |
+
from purpose_agent.slm_backends import OllamaBackend, LlamaCppBackend, SLM_REGISTRY
|
| 168 |
+
backend_registry.register("ollama", OllamaBackend, description="Local Ollama serving")
|
| 169 |
+
backend_registry.register("llama_cpp", LlamaCppBackend, description="Direct llama-cpp-python")
|
| 170 |
+
|
| 171 |
+
# Register SLM models from the built-in registry
|
| 172 |
+
for key, (ollama_name, ctx, desc) in SLM_REGISTRY.items():
|
| 173 |
+
model_registry.register(
|
| 174 |
+
key,
|
| 175 |
+
lambda host="http://localhost:11434", _m=ollama_name, _c=ctx: OllamaBackend(
|
| 176 |
+
model=_m, host=host, context_window=_c, compress_prompts=True,
|
| 177 |
+
),
|
| 178 |
+
ollama_name=ollama_name,
|
| 179 |
+
context_window=ctx,
|
| 180 |
+
description=desc,
|
| 181 |
+
)
|
| 182 |
+
|
| 183 |
+
# Register built-in callbacks
|
| 184 |
+
from purpose_agent.observability import LoggingCallback, MetricsCollector, JSONFileCallback
|
| 185 |
+
callback_registry.register("logging", LoggingCallback, description="Log all events")
|
| 186 |
+
callback_registry.register("metrics", MetricsCollector, description="Collect aggregate metrics")
|
| 187 |
+
callback_registry.register("jsonfile", JSONFileCallback, description="Write events to JSONL file")
|
| 188 |
+
|
| 189 |
+
|
| 190 |
+
# Auto-register defaults on import
|
| 191 |
+
_register_defaults()
|