Skip to content

Extending Jarvis

Jarvis uses a plugin architecture built on abstract Python interfaces (ABCs). Every extension point follows the same pattern:

  1. Implement an abstract interface (e.g., IJarvisCommand, IJarvisAgent)
  2. Place the file in the corresponding package directory
  3. Done --- the discovery system finds it automatically via Python reflection

There is no registration step, no config file to edit, no decorator to apply. Drop a Python file in the right directory and it is live.

How It Works

Under the hood, Jarvis uses Python's pkgutil and importlib to scan package directories at startup. It finds every class that implements the expected interface, instantiates it, validates any required secrets, and registers it. This happens automatically for commands, agents, device managers, device families, and prompt providers.

For STT, TTS, and wake response providers, discovery is manual --- you specify the provider in your node's config file. But the implementation pattern is identical: subclass the ABC, implement the required methods, and you are done.

Extension Points

Extension Point Interface Package Directory Runs On Discovery
Commands IJarvisCommand commands/ Node CommandDiscoveryService
Agents IJarvisAgent agents/ Node AgentDiscoveryService
Prompt Providers IJarvisPromptProvider app/core/prompt_providers/ Command Center PromptProviderFactory
Device Managers IJarvisDeviceManager device_managers/ Node DeviceManagerDiscoveryService
Device Protocols DeviceProtocol device_families/ Node DeviceFamilyDiscoveryService
STT Providers IJarvisSpeechToTextProvider stt_providers/ Node Manual config
TTS Providers IJarvisTextToSpeechProvider tts_providers/ Node Manual config
Wake Response Providers IJarvisWakeResponseProvider wake_response_providers/ Node Manual config

Architecture Diagram

The following diagram shows where each extension point runs and how the pieces connect:

graph TB
    subgraph Node ["Node (jarvis-node-setup)"]
        direction TB
        CMD["Commands<br/><small>IJarvisCommand</small>"]
        AGT["Agents<br/><small>IJarvisAgent</small>"]
        DM["Device Managers<br/><small>IJarvisDeviceManager</small>"]
        DF["Device Families<br/><small>DeviceProtocol</small>"]
        STT["STT Providers<br/><small>IJarvisSpeechToTextProvider</small>"]
        TTS["TTS Providers<br/><small>IJarvisTextToSpeechProvider</small>"]
        WR["Wake Responses<br/><small>IJarvisWakeResponseProvider</small>"]

        CDS["CommandDiscoveryService"]
        ADS["AgentDiscoveryService"]
        DMDS["DeviceManagerDiscoveryService"]
        DFDS["DeviceFamilyDiscoveryService"]

        CDS -->|scans| CMD
        ADS -->|scans| AGT
        DMDS -->|scans| DM
        DFDS -->|scans| DF
    end

    subgraph CC ["Command Center (jarvis-command-center)"]
        direction TB
        PP["Prompt Providers<br/><small>IJarvisPromptProvider</small>"]
        PPF["PromptProviderFactory"]
        PPF -->|scans| PP
    end

    Node -- "voice/text commands" --> CC
    CC -- "parsed intent + args" --> Node

Common Patterns

Most extension points share a set of common patterns that make the plugin system consistent and predictable.

Required Secrets

Many plugins need API keys or credentials. The required_secrets property declares what a plugin needs, and the discovery system validates availability before registering it:

@property
def required_secrets(self) -> list[str]:
    return ["GOVEE_API_KEY"]

def validate_secrets(self) -> bool:
    """Return True if all required secrets are available."""
    for secret in self.required_secrets:
        if not self.secret_service.get_secret(secret):
            return False
    return True

If secrets are missing, the plugin is skipped with a log warning rather than crashing the service. This allows optional plugins to coexist with required ones.

The name Property

Every plugin has a name property that serves as its unique identifier. This is how the system refers to plugins in logs, configuration, and the command registry:

@property
def name(self) -> str:
    return "get_weather"

Graceful Failure

All discovery services catch ImportError and other exceptions during scanning. If a plugin file has an unmet dependency (e.g., a pip package not installed), the file is skipped and a warning is logged. This means you can have plugins with optional dependencies without breaking the system.

Thread Safety

Discovery services use threading.RLock for thread-safe access to the plugin registry. This is important because the background refresh thread (used by CommandDiscoveryService) can update the registry while other threads are reading it.

Getting Started

The fastest way to extend Jarvis is to add a new command. See the Commands guide for a step-by-step walkthrough.

For deeper understanding of how plugins are found and loaded, see the Discovery System documentation.