Skip to content

Commit b6d5537

Browse files
authored
Merge pull request #1 from jmcentire/feature/configurable-module
Add configurable module infrastructure
2 parents e976ee3 + 7774d7e commit b6d5537

15 files changed

Lines changed: 3840 additions & 16 deletions

src/apprentice/apprentice_class.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -433,6 +433,36 @@ async def close(self):
433433
if self._running:
434434
await self.__aexit__(None, None, None)
435435

436+
async def feedback(self, request_id: str, feedback_type: str, **kwargs) -> None:
437+
"""Record feedback for a previous request. No-op if feedback collector not configured."""
438+
collector = getattr(self, '_feedback_collector', None)
439+
if collector is None:
440+
return
441+
from apprentice.feedback_collector import FeedbackEntry, FeedbackType
442+
entry = FeedbackEntry(
443+
request_id=request_id,
444+
task_name=kwargs.get('task_name', ''),
445+
feedback_type=FeedbackType(feedback_type),
446+
score=kwargs.get('score', 0.0),
447+
edited_output=kwargs.get('edited_output'),
448+
reason=kwargs.get('reason'),
449+
)
450+
collector.record_feedback(entry)
451+
452+
async def observe(self, event) -> None:
453+
"""Record an observation event. No-op if observer not configured."""
454+
observer = getattr(self, '_observer', None)
455+
if observer is None:
456+
return
457+
observer.observe(event)
458+
459+
async def record_action(self, event_id: str, action: dict) -> None:
460+
"""Record an actual action for a previously observed event."""
461+
observer = getattr(self, '_observer', None)
462+
if observer is None:
463+
return
464+
observer.record_action(event_id, action)
465+
436466
async def run(self, task_name: str, input_data: Dict[str, Any]) -> TaskResponse:
437467
"""
438468
Primary public method. Executes a task: routes to appropriate model backend,

src/apprentice/config_loader.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,39 @@ class TrainingDataStoreConfig(BaseModel):
332332
max_examples_per_task: int = Field(default=50000, ge=100, le=10000000)
333333

334334

335+
class PluginEntryConfig(BaseModel):
336+
"""Configuration for a single plugin entry."""
337+
model_config = ConfigDict(frozen=True, strict=True, extra="forbid")
338+
339+
factory: str = Field(min_length=1)
340+
341+
342+
class MiddlewareEntryConfig(BaseModel):
343+
"""Configuration for a single middleware entry in the pipeline."""
344+
model_config = ConfigDict(frozen=True, strict=True, extra="allow")
345+
346+
name: str = Field(min_length=1)
347+
config: Mapping[str, Any] = Field(default_factory=dict)
348+
349+
350+
class FeedbackConfig(BaseModel):
351+
"""Configuration for the feedback collector."""
352+
model_config = ConfigDict(frozen=True, strict=True, extra="forbid")
353+
354+
enabled: bool = False
355+
storage_dir: str = ".apprentice/feedback/"
356+
357+
358+
class ObserverConfig(BaseModel):
359+
"""Configuration for the observer."""
360+
model_config = ConfigDict(frozen=True, strict=True, extra="forbid")
361+
362+
enabled: bool = False
363+
context_window_size: int = Field(default=50, ge=1, le=1000)
364+
shadow_recommendation_rate: float = Field(default=0.1, ge=0.0, le=1.0)
365+
min_context_before_recommending: int = Field(default=10, ge=1)
366+
367+
335368
class ApprenticeConfig(BaseModel):
336369
"""Root configuration model. Frozen and immutable after construction."""
337370
model_config = ConfigDict(frozen=True, strict=True, extra="forbid")
@@ -344,6 +377,13 @@ class ApprenticeConfig(BaseModel):
344377
audit: AuditConfig
345378
training_data: TrainingDataStoreConfig
346379

380+
# New extensibility fields (all optional, backward compatible)
381+
mode: str = Field(default="distillation", pattern=r"^(distillation|copilot|observer)$")
382+
plugins: Optional[Mapping[str, Mapping[str, PluginEntryConfig]]] = None
383+
middleware: Optional[List[MiddlewareEntryConfig]] = None
384+
feedback: Optional[FeedbackConfig] = None
385+
observer: Optional[ObserverConfig] = None
386+
347387
@model_validator(mode="after")
348388
def validate_cross_field_constraints(self) -> "ApprenticeConfig":
349389
"""Performs all cross-field validations."""

src/apprentice/factory.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,7 @@ async def build_from_config(config_path: str) -> Any:
278278
TaskConfig as ACTaskConfig,
279279
ConfidenceThresholds as ACThresholds,
280280
)
281+
from apprentice.plugin_registry import PluginRegistrySet
281282
from apprentice.audit_log import AuditConfig, JsonLinesAuditLogger
282283
from apprentice.budget_manager import BudgetConfig, BudgetManager, PeriodLimit, PeriodType
283284
from apprentice.confidence_engine import ConfidenceEngine, ConfidenceEngineConfig
@@ -306,6 +307,14 @@ async def build_from_config(config_path: str) -> Any:
306307
# 1. Load validated config
307308
cfg = config_loader.load_config(Path(config_path))
308309

310+
# 1.5. Construct Plugin Registry
311+
plugin_registry_set = PluginRegistrySet.with_defaults()
312+
if cfg.plugins:
313+
plugin_registry_set.register_from_config(
314+
{domain: {name: {"factory": entry.factory} for name, entry in plugins.items()}
315+
for domain, plugins in cfg.plugins.items()}
316+
)
317+
309318
# 2. Create directories
310319
base_dir = Path(".apprentice")
311320
base_dir.mkdir(parents=True, exist_ok=True)
@@ -551,5 +560,41 @@ async def build_from_config(config_path: str) -> Any:
551560
apprentice._ft_version_store = ft_version_store
552561
apprentice._model_validator = model_validator
553562
apprentice._real_config = cfg
563+
apprentice._plugin_registry_set = plugin_registry_set
564+
565+
# Construct middleware pipeline if configured
566+
if cfg.middleware:
567+
from apprentice.middleware import MiddlewarePipeline
568+
middleware_registry = plugin_registry_set.get_registry("middleware")
569+
apprentice._middleware_pipeline = MiddlewarePipeline.from_config(
570+
cfg.middleware, middleware_registry,
571+
)
572+
else:
573+
apprentice._middleware_pipeline = None
574+
575+
# Construct feedback collector if configured
576+
if cfg.feedback and cfg.feedback.enabled:
577+
from apprentice.feedback_collector import FeedbackCollector
578+
apprentice._feedback_collector = FeedbackCollector(
579+
storage_dir=cfg.feedback.storage_dir,
580+
enabled=True,
581+
)
582+
else:
583+
apprentice._feedback_collector = None
584+
585+
# Construct observer if configured
586+
if cfg.observer and cfg.observer.enabled:
587+
from apprentice.observer import Observer, ObserverConfig as ObsCfg
588+
apprentice._observer = Observer(ObsCfg(
589+
enabled=True,
590+
context_window_size=cfg.observer.context_window_size,
591+
shadow_recommendation_rate=cfg.observer.shadow_recommendation_rate,
592+
min_context_before_recommending=cfg.observer.min_context_before_recommending,
593+
))
594+
else:
595+
apprentice._observer = None
596+
597+
# Store mode
598+
apprentice._mode = cfg.mode
554599

555600
return apprentice

0 commit comments

Comments
 (0)