Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions src/apprentice/apprentice_class.py
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,36 @@ async def close(self):
if self._running:
await self.__aexit__(None, None, None)

async def feedback(self, request_id: str, feedback_type: str, **kwargs) -> None:
"""Record feedback for a previous request. No-op if feedback collector not configured."""
collector = getattr(self, '_feedback_collector', None)
if collector is None:
return
from apprentice.feedback_collector import FeedbackEntry, FeedbackType
entry = FeedbackEntry(
request_id=request_id,
task_name=kwargs.get('task_name', ''),
feedback_type=FeedbackType(feedback_type),
score=kwargs.get('score', 0.0),
edited_output=kwargs.get('edited_output'),
reason=kwargs.get('reason'),
)
collector.record_feedback(entry)

async def observe(self, event) -> None:
"""Record an observation event. No-op if observer not configured."""
observer = getattr(self, '_observer', None)
if observer is None:
return
observer.observe(event)

async def record_action(self, event_id: str, action: dict) -> None:
"""Record an actual action for a previously observed event."""
observer = getattr(self, '_observer', None)
if observer is None:
return
observer.record_action(event_id, action)

async def run(self, task_name: str, input_data: Dict[str, Any]) -> TaskResponse:
"""
Primary public method. Executes a task: routes to appropriate model backend,
Expand Down
40 changes: 40 additions & 0 deletions src/apprentice/config_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,39 @@ class TrainingDataStoreConfig(BaseModel):
max_examples_per_task: int = Field(default=50000, ge=100, le=10000000)


class PluginEntryConfig(BaseModel):
"""Configuration for a single plugin entry."""
model_config = ConfigDict(frozen=True, strict=True, extra="forbid")

factory: str = Field(min_length=1)


class MiddlewareEntryConfig(BaseModel):
"""Configuration for a single middleware entry in the pipeline."""
model_config = ConfigDict(frozen=True, strict=True, extra="allow")

name: str = Field(min_length=1)
config: Mapping[str, Any] = Field(default_factory=dict)


class FeedbackConfig(BaseModel):
"""Configuration for the feedback collector."""
model_config = ConfigDict(frozen=True, strict=True, extra="forbid")

enabled: bool = False
storage_dir: str = ".apprentice/feedback/"


class ObserverConfig(BaseModel):
"""Configuration for the observer."""
model_config = ConfigDict(frozen=True, strict=True, extra="forbid")

enabled: bool = False
context_window_size: int = Field(default=50, ge=1, le=1000)
shadow_recommendation_rate: float = Field(default=0.1, ge=0.0, le=1.0)
min_context_before_recommending: int = Field(default=10, ge=1)


class ApprenticeConfig(BaseModel):
"""Root configuration model. Frozen and immutable after construction."""
model_config = ConfigDict(frozen=True, strict=True, extra="forbid")
Expand All @@ -344,6 +377,13 @@ class ApprenticeConfig(BaseModel):
audit: AuditConfig
training_data: TrainingDataStoreConfig

# New extensibility fields (all optional, backward compatible)
mode: str = Field(default="distillation", pattern=r"^(distillation|copilot|observer)$")
plugins: Optional[Mapping[str, Mapping[str, PluginEntryConfig]]] = None
middleware: Optional[List[MiddlewareEntryConfig]] = None
feedback: Optional[FeedbackConfig] = None
observer: Optional[ObserverConfig] = None

@model_validator(mode="after")
def validate_cross_field_constraints(self) -> "ApprenticeConfig":
"""Performs all cross-field validations."""
Expand Down
45 changes: 45 additions & 0 deletions src/apprentice/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,7 @@ async def build_from_config(config_path: str) -> Any:
TaskConfig as ACTaskConfig,
ConfidenceThresholds as ACThresholds,
)
from apprentice.plugin_registry import PluginRegistrySet
from apprentice.audit_log import AuditConfig, JsonLinesAuditLogger
from apprentice.budget_manager import BudgetConfig, BudgetManager, PeriodLimit, PeriodType
from apprentice.confidence_engine import ConfidenceEngine, ConfidenceEngineConfig
Expand Down Expand Up @@ -306,6 +307,14 @@ async def build_from_config(config_path: str) -> Any:
# 1. Load validated config
cfg = config_loader.load_config(Path(config_path))

# 1.5. Construct Plugin Registry
plugin_registry_set = PluginRegistrySet.with_defaults()
if cfg.plugins:
plugin_registry_set.register_from_config(
{domain: {name: {"factory": entry.factory} for name, entry in plugins.items()}
for domain, plugins in cfg.plugins.items()}
)

# 2. Create directories
base_dir = Path(".apprentice")
base_dir.mkdir(parents=True, exist_ok=True)
Expand Down Expand Up @@ -551,5 +560,41 @@ async def build_from_config(config_path: str) -> Any:
apprentice._ft_version_store = ft_version_store
apprentice._model_validator = model_validator
apprentice._real_config = cfg
apprentice._plugin_registry_set = plugin_registry_set

# Construct middleware pipeline if configured
if cfg.middleware:
from apprentice.middleware import MiddlewarePipeline
middleware_registry = plugin_registry_set.get_registry("middleware")
apprentice._middleware_pipeline = MiddlewarePipeline.from_config(
cfg.middleware, middleware_registry,
)
else:
apprentice._middleware_pipeline = None

# Construct feedback collector if configured
if cfg.feedback and cfg.feedback.enabled:
from apprentice.feedback_collector import FeedbackCollector
apprentice._feedback_collector = FeedbackCollector(
storage_dir=cfg.feedback.storage_dir,
enabled=True,
)
else:
apprentice._feedback_collector = None

# Construct observer if configured
if cfg.observer and cfg.observer.enabled:
from apprentice.observer import Observer, ObserverConfig as ObsCfg
apprentice._observer = Observer(ObsCfg(
enabled=True,
context_window_size=cfg.observer.context_window_size,
shadow_recommendation_rate=cfg.observer.shadow_recommendation_rate,
min_context_before_recommending=cfg.observer.min_context_before_recommending,
))
else:
apprentice._observer = None

# Store mode
apprentice._mode = cfg.mode

return apprentice
Loading