205 lines
5.2 KiB
Python
205 lines
5.2 KiB
Python
"""
|
|
Dynamic provider loading and composition for the Voice Assistant.
|
|
|
|
Purpose:
|
|
Resolve provider names from the application configuration and
|
|
instantiate implementations that satisfy the corresponding adapter
|
|
contract.
|
|
|
|
Responsibilities:
|
|
- Resolve provider implementations from ProviderCatalog metadata.
|
|
- Import provider classes dynamically.
|
|
- Validate that the loaded implementation matches the expected adapter.
|
|
- Instantiate STT, Agent, TTS, and VAD providers using provider-specific options.
|
|
|
|
Non-responsibilities:
|
|
- Loading YAML files.
|
|
- Reading secrets directly.
|
|
- Managing WebSocket connections.
|
|
- Managing turn state.
|
|
- Executing provider functionality.
|
|
|
|
Architecture:
|
|
AppConfig + ProviderCatalog
|
|
|
|
|
v
|
|
Provider Loader
|
|
|
|
|
v
|
|
STTAdapter / AgentAdapter / TTSAdapter / VADAdapter
|
|
|
|
|
v
|
|
concrete provider
|
|
"""
|
|
|
|
from importlib import import_module
|
|
from typing import Any
|
|
|
|
from voice_assistant.agent.adapter import AgentAdapter
|
|
from voice_assistant.config.models import (
|
|
AppConfig,
|
|
ComponentConfig,
|
|
ProviderCatalog,
|
|
)
|
|
from voice_assistant.stt.adapter import STTAdapter
|
|
from voice_assistant.tts.adapter import TTSAdapter
|
|
from voice_assistant.vad.adapter import VADAdapter
|
|
|
|
|
|
PROVIDER_CONTRACTS: dict[str, type] = {
|
|
"stt": STTAdapter,
|
|
"agent": AgentAdapter,
|
|
"tts": TTSAdapter,
|
|
"vad": VADAdapter,
|
|
}
|
|
|
|
|
|
def load_provider_class(
|
|
catalog: ProviderCatalog,
|
|
component: str,
|
|
provider: str,
|
|
) -> type:
|
|
"""
|
|
Load and validate a provider class from the provider catalog.
|
|
|
|
The catalog stores the import path, while this function ensures that
|
|
the resolved class implements the adapter contract associated with the
|
|
requested component.
|
|
"""
|
|
contract = PROVIDER_CONTRACTS.get(component)
|
|
|
|
if contract is None:
|
|
raise ValueError(
|
|
f"Unsupported provider component: {component!r}."
|
|
)
|
|
|
|
component_providers = catalog.providers.get(component)
|
|
|
|
if component_providers is None:
|
|
raise ValueError(
|
|
f"No providers are registered for component {component!r}."
|
|
)
|
|
|
|
definition = component_providers.get(provider)
|
|
|
|
if definition is None:
|
|
raise ValueError(
|
|
f"Provider {provider!r} is not registered for "
|
|
f"component {component!r}."
|
|
)
|
|
|
|
module_name, class_name = definition.implementation.split(
|
|
":",
|
|
maxsplit=1,
|
|
)
|
|
|
|
try:
|
|
module = import_module(module_name)
|
|
except ImportError as exc:
|
|
raise RuntimeError(
|
|
f"Could not import provider module "
|
|
f"{module_name!r} for {component!r}/{provider!r}."
|
|
) from exc
|
|
|
|
try:
|
|
provider_class = getattr(module, class_name)
|
|
except AttributeError as exc:
|
|
raise RuntimeError(
|
|
f"Provider class {class_name!r} was not found in "
|
|
f"module {module_name!r}."
|
|
) from exc
|
|
|
|
if not isinstance(provider_class, type):
|
|
raise TypeError(
|
|
f"Provider {component!r}/{provider!r} does not resolve "
|
|
f"to a class."
|
|
)
|
|
|
|
if not issubclass(provider_class, contract):
|
|
raise TypeError(
|
|
f"Provider {component!r}/{provider!r} must implement "
|
|
f"{contract.__name__}."
|
|
)
|
|
|
|
return provider_class
|
|
|
|
|
|
def _create_provider(
|
|
catalog: ProviderCatalog,
|
|
component: str,
|
|
config: ComponentConfig,
|
|
) -> Any:
|
|
"""
|
|
Instantiate the provider selected by a component configuration.
|
|
|
|
Provider-specific constructor arguments are kept inside `options`
|
|
so the common configuration model does not need to know the API of
|
|
every vendor.
|
|
"""
|
|
provider_class = load_provider_class(
|
|
catalog=catalog,
|
|
component=component,
|
|
provider=config.provider,
|
|
)
|
|
|
|
try:
|
|
return provider_class(**config.options)
|
|
except TypeError as exc:
|
|
raise TypeError(
|
|
f"Invalid options for provider "
|
|
f"{component!r}/{config.provider!r}: {exc}"
|
|
) from exc
|
|
|
|
|
|
def create_stt_adapter(
|
|
config: AppConfig,
|
|
catalog: ProviderCatalog,
|
|
) -> STTAdapter:
|
|
"""Create the configured STT adapter."""
|
|
return _create_provider(
|
|
catalog=catalog,
|
|
component="stt",
|
|
config=config.stt,
|
|
)
|
|
|
|
|
|
def create_agent_adapter(
|
|
config: AppConfig,
|
|
catalog: ProviderCatalog,
|
|
) -> AgentAdapter:
|
|
"""Create the configured Agent adapter."""
|
|
return _create_provider(
|
|
catalog=catalog,
|
|
component="agent",
|
|
config=config.agent,
|
|
)
|
|
|
|
|
|
def create_tts_adapter(
|
|
config: AppConfig,
|
|
catalog: ProviderCatalog,
|
|
) -> TTSAdapter:
|
|
"""Create the configured TTS adapter."""
|
|
return _create_provider(
|
|
catalog=catalog,
|
|
component="tts",
|
|
config=config.tts,
|
|
)
|
|
|
|
|
|
|
|
|
|
def create_vad_adapter(
|
|
config: AppConfig,
|
|
catalog: ProviderCatalog,
|
|
) -> VADAdapter:
|
|
"""Create the VAD selected by the active environment configuration."""
|
|
if config.vad is None:
|
|
raise ValueError("VAD provider is not configured for this environment.")
|
|
|
|
return _create_provider(
|
|
catalog=catalog,
|
|
component="vad",
|
|
config=config.vad,
|
|
)
|