UNI is still under development — Please check back at the end of March
Skip to content

SDK API reference

Introduction

Start with the walkthrough

Read the SDK walkthrough first. This page provides the complete API reference in alphabetical order.

The uni_plugin_sdk module provides the Python API for developing plugins for the UNI AI assistant. Plugins should import this module as: import uni_plugin_sdk as uni

AppConfig

AppConfig(paths: AppPaths | None = None, is_dev: bool = False)

TOML configuration manager that combines default and user settings, providing dot notation access (e.g., 'database.port') and type conversion.

Source code in uni_server/app_config.py
57
58
59
60
61
62
63
64
65
def __init__(self, paths: AppPaths | None = None, is_dev: bool = False):
    self.paths: AppPaths = paths or AppPaths()
    self.is_dev = is_dev
    self.defaults_file = self.paths.resource / "default_settings.toml"
    self.user_file = self.paths.user / "settings.toml"
    self.data: dict[str, Any] = {}
    self._user_doc: TOMLDocument | None = None
    self.plugin = PluginConfig(self)
    self._load()

require

require[T](key: str, type_hint: type[T]) -> T

Retrieve a configuration value or raise an error if not found.

Source code in uni_server/app_config.py
67
68
69
70
71
72
73
74
def require[T](self, key: str, type_hint: type[T]) -> T:
    """
    Retrieve a configuration value or raise an error if not found.
    """
    value = self.get(key, type_hint, None)
    if value is None:
        raise AppConfigError(f"Setting not found: {key}")
    return value

get

get[T](key: str, type_hint: type[T], default: T) -> T

Retrieve a configuration value or return a default if not found.

Source code in uni_server/app_config.py
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
def get[T](self, key: str, type_hint: type[T], default: T) -> T:
    """
    Retrieve a configuration value or return a default if not found.
    """
    value = self.data
    try:
        for part in key.split("."):
            value = value[part]
    except (KeyError, TypeError):
        return default

    if not isinstance(value, type_hint):
        try:
            return type_hint(value)  # type: ignore[call-arg]
        except (ValueError, TypeError):
            raise AppConfigTypeError(
                f"Cannot convert {key}={value} to {type_hint.__name__}"
            ) from None

    return value

set

set(updates: dict[str, Any]) -> None

Set configuration values and write to the user settings file.

config.set({
    "plugin.weather.default_city": "London"
})
Source code in uni_server/app_config.py
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
def set(self, updates: dict[str, Any]) -> None:
    """
    Set configuration values and write to the user settings file.

    ```
    config.set({
        "plugin.weather.default_city": "London"
    })
    ```
    """
    for update_key, update_value in updates.items():
        parent, final = _navigate(self.data, update_key, create_missing=dict)
        parent[final] = update_value

        if self._user_doc is None:
            self._user_doc = tomlkit.document()

        doc_parent, doc_final = _navigate(
            self._user_doc, update_key, create_missing=tomlkit.table
        )
        doc_parent[doc_final] = update_value  # type: ignore[assignment]

    self._write_user_config()

delete

delete(key: str) -> None

Delete a configuration key and write changes to the user settings file.

Source code in uni_server/app_config.py
121
122
123
124
125
126
127
128
129
130
131
132
133
134
def delete(self, key: str) -> None:
    """
    Delete a configuration key and write changes to the user settings file.
    """
    parent, final = _navigate(self.data, key)
    if parent is not None and final in parent:
        del parent[final]

    if self._user_doc is not None:
        doc_parent, doc_final = _navigate(self._user_doc, key)
        if doc_parent is not None and doc_final in doc_parent:
            del doc_parent[doc_final]

    self._write_user_config()

AppPaths

AppPaths(root: Path | None = None)

Standardizes path access and resolution across the application, providing a single source of truth for file and directory locations.

Source code in uni_server/app_paths.py
11
12
13
14
15
16
17
18
19
20
21
22
def __init__(self, root: Path | None = None):
    self.root: Final[Path] = root or Path().absolute()

    self.frontend: Final[Path] = self.root / "uni_frontend" / "build"
    self.static: Final[Path] = self.root / "uni_server" / "static"
    self.resource: Final[Path] = self.root / "resources"
    self.vendor: Final[Path] = self.root / "vendor"
    self.plugins: Final[Path] = self.root / "plugins"

    self.user: Final[Path] = self.root / "user"
    self.user_data: Final[Path] = self.user / "data"
    self.user_logs: Final[Path] = self.user / "logs"

ensure_user_dirs

ensure_user_dirs() -> None

Pre-creates common user directories for user convenience. Application code remains responsible for ensuring directory existence when required.

Source code in uni_server/app_paths.py
24
25
26
27
28
29
30
31
32
33
34
35
def ensure_user_dirs(self) -> None:
    """
    Pre-creates common user directories for user convenience. Application code remains
    responsible for ensuring directory existence when required.
    """
    required_dirs = (
        self.user_data,
        self.user_logs,
    )

    for path in required_dirs:
        path.mkdir(parents=True, exist_ok=True)

user_data_path

user_data_path(subpath: str) -> Path

Resolve a file path in the user data directory.

This directory is used to store persistent data, often encrypted using SecureJSONFile or other solutions from utils/encryption.py.

Parent directories are created if they do not exist.

Source code in uni_server/app_paths.py
37
38
39
40
41
42
43
44
45
46
47
48
def user_data_path(self, subpath: str) -> Path:
    """
    Resolve a file path in the user data directory.

    This directory is used to store persistent data, often encrypted using
    `SecureJSONFile` or other solutions from `utils/encryption.py`.

    Parent directories are created if they do not exist.
    """
    path = self.user_data / subpath
    path.parent.mkdir(parents=True, exist_ok=True)
    return path

MessageChannel

Bases: ABC

Base class for message channel plugins (Discord, Matrix, etc.).

Plugins providing message channels should inherit from this class and register themselves through the @uni.message_channel decorator.

online abstractmethod property

online: bool

Whether the channel is currently ready to send/receive.

send abstractmethod

send(message: str) -> None

Send a message to the user via this channel.

Source code in uni_server/components/channels.py
37
38
39
40
41
42
@abstractmethod
def send(self, message: str) -> None:
    """
    Send a message to the user via this channel.
    """
    ...

approve async

approve(tool_name: str, arguments: dict[str, Any]) -> bool

Request user approval for an unsafe tool call.

Override this to support interactive approval (e.g. emoji reactions). The default denies all requests.

Source code in uni_server/components/channels.py
44
45
46
47
48
49
50
51
async def approve(self, tool_name: str, arguments: dict[str, Any]) -> bool:
    """
    Request user approval for an unsafe tool call.

    Override this to support interactive approval (e.g. emoji reactions).
    The default denies all requests.
    """
    return False

receive

receive(content: str) -> None

Route an incoming user message to UNI's response pipeline.

Source code in uni_server/components/channels.py
53
54
55
56
57
58
59
def receive(self, content: str) -> None:
    """
    Route an incoming user message to UNI's response pipeline.
    """
    from uni_server.app_state import get_uni  # noqa: PLC0415 circular

    get_uni().response_pipeline.on_user_message(content, self._plugin_name)

CurrentDevice

Proxy that resolves the current device's id and name from the active session.

Available as uni.current_device in plugin code.

ToolDescriptor dataclass

ToolDescriptor(schema: dict[str, Any], handler: Callable[..., Any])

Tool registration with explicit schema and handler.

Used for runtime-generated tools where the schema depends on config, available files, or external data sources (e.g., enums built from discovered animations or MCP tools).

AudioEffect

Bases: ABC

Base class for plugin audio effects that transform TTS output.

Effects are stateful and process audio chunk-by-chunk, allowing for streaming transformations like pitch shifting, reverb, or vocoders. State is reset between response fragments.

configure abstractmethod

configure(sample_rate: int, bytes_per_sample: int) -> None

Configure the effect according to the audio format of the active TTS engine.

Parameters:

Name Type Description Default
sample_rate int

Audio sample rate in Hz.

required
bytes_per_sample int

Bytes per audio sample (typically 2 for 16-bit PCM).

required
Source code in uni_server/components/voice/audio_effects.py
17
18
19
20
21
22
23
24
25
26
@abstractmethod
def configure(self, sample_rate: int, bytes_per_sample: int) -> None:
    """
    Configure the effect according to the audio format of the active TTS engine.

    Args:
        sample_rate: Audio sample rate in Hz.
        bytes_per_sample: Bytes per audio sample (typically 2 for 16-bit PCM).
    """
    pass

reset abstractmethod

reset() -> None

Clear accumulated state between response fragments.

Source code in uni_server/components/voice/audio_effects.py
32
33
34
35
36
37
@abstractmethod
def reset(self) -> None:
    """
    Clear accumulated state between response fragments.
    """
    pass

TtsEngine

Bases: ABC

Base interface for text-to-speech providers.

Plugins overriding text-to-speech should inherit from this class and register themselves through the @uni.tts decorator.

WakeWordEngine

Bases: ABC

Base interface for wake word detection providers.

Plugins providing wake word detection should inherit from this class and register themselves through the @uni.wake_word decorator.

CardDismissEvent dataclass

CardDismissEvent(card_id: str)

Fired when a card is dismissed.

DeviceConnectEvent dataclass

DeviceConnectEvent()

Fired when a device connects to the server.

DeviceDisconnectEvent dataclass

DeviceDisconnectEvent()

Fired when a device disconnects from the server.

EmbeddingsModelChangedEvent dataclass

EmbeddingsModelChangedEvent(old_model: str | None, new_model: str | None)

Fired when the embeddings model configuration changes.

IncomingAudioEvent dataclass

IncomingAudioEvent(chunk: bytes, sample_rate: int)

Fired when a new audio chunk is received from the microphone.

LoadEvent dataclass

LoadEvent(timestamp: datetime)

Fired after components load, before the assistant becomes interactive.

MessageEvent dataclass

MessageEvent(message: str, history: list[dict[str, Any]], trigger: Trigger = 'user')

Fired when a new message is being handled.

This is after the user prompt has been modified by user prompt modifiers.

NightlyResetEvent dataclass

NightlyResetEvent(timestamp: datetime, conversation_history: list[dict[str, Any]])

Fired when the nightly sleep cycle runs.

ResponseGeneratedEvent dataclass

ResponseGeneratedEvent(trigger: Trigger, user_prompt: str | None, impulse_data: Any | None, final_prompt: str, full_response: str)

Fired after the LLM completes generating its full response.

Contains all information about the interaction including the trigger, prompts, and complete response. The response has been added to conversation history at this point.

SystemPromptEvent dataclass

SystemPromptEvent(prompt: str)

Fired when the system prompt is updated.

UnloadEvent dataclass

UnloadEvent()

Fired when the system is unloading.

UserPromptEvent dataclass

UserPromptEvent(prompt: str, history: list[dict[str, Any]], channel: str = 'voice')

Fired when a user prompt is received, from any channel.

The channel field indicates the source: "voice" for the local mic/text input, or "message:" for external channels (e.g. "message:discord").

SecureJSONFile

SecureJSONFile(path: str | Path)

Encrypted storage for a single JSON object.

Source code in uni_server/utils/encryption.py
438
439
440
441
def __init__(self, path: str | Path):
    self.path = Path(path)
    self.path.parent.mkdir(parents=True, exist_ok=True)
    self._lock = threading.Lock()

SecureJSONLFile

SecureJSONLFile(path: str | Path)

Encrypted storage for a sequence of JSON objects. Each object is encrypted separately.

Source code in uni_server/utils/encryption.py
463
464
465
466
def __init__(self, path: str | Path):
    self.path = Path(path)
    self.path.parent.mkdir(parents=True, exist_ok=True)
    self._lock = threading.Lock()

read

read() -> list[dict[str, Any]]

Invalid records are logged and skipped.

Source code in uni_server/utils/encryption.py
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
def read(self) -> list[dict[str, Any]]:
    """
    Invalid records are logged and skipped.
    """
    if not self.path.exists():
        return []

    records: list[dict[str, Any]] = []
    with self._lock, self.path.open() as f:
        for line_num, line in enumerate(f, 1):
            try:
                decrypted = decrypt(line.strip())
                records.append(json.loads(decrypted))
            except Exception as e:
                logger.exception(
                    f"Failed to decrypt record {line_num} in {self.path}: {e!s}"
                )
    return records

PluginSocket

Bases: Protocol

Protocol for plugin socket interface.

Provides type hints for the socket object passed to @uni.socket handlers.

on_connect property

on_connect: Callable[[SocketHandler], SocketHandler]

Decorator to register a connect handler.

on_disconnect property

on_disconnect: Callable[[SocketHandler], SocketHandler]

Decorator to register a disconnect handler.

on

on(event: str) -> Callable[[SocketHandler], SocketHandler]

Decorator to register a handler for a socket event.

Source code in uni_plugin_sdk.py
86
87
88
89
90
def on(self, event: str) -> Callable[[SocketHandler], SocketHandler]:
    """
    Decorator to register a handler for a socket event.
    """
    ...

emit

emit(event: str, data: Any = None, to: str | None = None) -> None

Emit an event to connected clients.

Parameters:

Name Type Description Default
event str

Event name

required
data Any

Event data

None
to str | None

Device ID to send to

None
Source code in uni_plugin_sdk.py
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
def emit(
    self,
    event: str,
    data: Any = None,
    to: str | None = None,
) -> None:
    """
    Emit an event to connected clients.

    Args:
        event: Event name
        data: Event data
        to: Device ID to send to
    """
    ...

PluginRouter

PluginRouter()

Bases: APIRouter

Router for plugin routes, mounted at /plugins/<plugin_name>.

Routes at /settings automatically appear in the settings menu. Authentication is applied to all routes via a router-level dependency.

Starlette Mount provides path-prefix and route-name scoping, so templates can use url_for('show_settings') without collisions.

Source code in uni_plugin_sdk.py
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
def __init__(self) -> None:
    from uni_server.web.auth import get_device_token  # noqa: PLC0415 circular

    plugin_name = find_current_plugin_name()
    config = _plugin_state.get_config()
    self._plugin_name = plugin_name
    self._plugin_dir = config.paths.plugins / plugin_name

    # No prefix — Mount provides it at registration time.
    super().__init__(dependencies=[Depends(get_device_token)])
    self._has_settings_page = False

    static_dir = self._plugin_dir / "static"
    if static_dir.is_dir():
        self._add_static_route(static_dir)

redirect

redirect(path: str, status_code: int = 303) -> RedirectResponse

Redirect to a path relative to this plugin's mount point.

Source code in uni_plugin_sdk.py
700
701
702
703
704
705
706
def redirect(self, path: str, status_code: int = 303) -> RedirectResponse:
    """
    Redirect to a path relative to this plugin's mount point.
    """
    return RedirectResponse(
        f"/plugins/{self._plugin_name}{path}", status_code=status_code
    )

prompt_llm

prompt_llm(system_prompt: str, user_prompt: str, formatting: None = None) -> str
prompt_llm(system_prompt: str, user_prompt: str, formatting: FormattingType) -> dict[str, Any]

Run a prompt on the tasks model.

Source code in uni_server/components/llm/utils.py
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
def prompt_llm(
    system_prompt: str,
    user_prompt: str,
    formatting: FormattingType | None = None,
) -> str | dict[str, Any]:
    """
    Run a prompt on the tasks model.
    """
    from uni_server.app_state import get_uni  # noqa: PLC0415 - circular import

    uni = get_uni()

    messages = [
        {"role": "system", "content": textwrap.dedent(system_prompt).strip()},
        {"role": "user", "content": textwrap.dedent(user_prompt).strip()},
    ]

    chunks = list(
        uni.model_router.chat(
            role="tasks",
            messages=messages,
            formatting=formatting,
        )
    )

    if not chunks:
        raise RuntimeError("No response from LLM provider")

    output = "".join(chunk["message"]["content"] for chunk in chunks).strip()

    if formatting:
        return parse_llm_generated_json(output)
    return output

parse_datetime

parse_datetime(text: str, prefer_future: bool = True) -> datetime

Parse natural language datetime strings (e.g., "tomorrow at 3pm", "next Monday").

Source code in uni_server/utils/datetime_parsing.py
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
def parse_datetime(text: str, prefer_future: bool = True) -> datetime:
    """
    Parse natural language datetime strings (e.g., "tomorrow at 3pm", "next Monday").
    """
    settings: dict[str, Any] = {
        "PREFER_DAY_OF_MONTH": "first",  # "December" -> Dec 1st, not Dec 16th
    }
    if prefer_future:
        settings["PREFER_DATES_FROM"] = "future"

    result = dateparser.parse(text, settings=cast(Any, settings))
    if result is None:
        raise ValueError(f"Could not parse '{text}' as a datetime")
    return result

parse_duration

parse_duration(text: str) -> int

Parse natural language duration strings (e.g., "2 hours", "1h 30m", "45s") to seconds.

Source code in uni_server/utils/datetime_parsing.py
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
def parse_duration(text: str) -> int:
    """
    Parse natural language duration strings (e.g., "2 hours", "1h 30m", "45s") to seconds.
    """
    if text.strip().startswith("-"):
        raise ValueError(f"Duration '{text}' must be positive")

    reference_time = datetime(2025, 1, 1, 12, 0, 0)
    future_time = dateparser.parse(
        f"in {text}",
        settings={
            "PREFER_DATES_FROM": "future",
            "PREFER_DAY_OF_MONTH": "first",
            "RELATIVE_BASE": reference_time,
        },
    )
    if future_time is None:
        raise ValueError(f"Could not parse '{text}' as a duration")

    duration_seconds = int((future_time - reference_time).total_seconds())
    if duration_seconds < 0:
        raise ValueError(f"Duration '{text}' must be positive")

    return duration_seconds

serve_file

serve_file(base_dir: Path, relative_path: str) -> FileResponse

Serve a file, ensuring the resolved path stays within base_dir.

Replaces bare FileResponse(base_dir / user_input) which is vulnerable to path traversal attacks. Starlette's FileResponse does not validate that the resolved path stays within the base directory.

Source code in uni_server/utils/fs.py
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
def serve_file(base_dir: Path, relative_path: str) -> FileResponse:
    """
    Serve a file, ensuring the resolved path stays within base_dir.

    Replaces bare FileResponse(base_dir / user_input) which is vulnerable
    to path traversal attacks. Starlette's FileResponse does not validate
    that the resolved path stays within the base directory.
    """
    resolved = (base_dir / relative_path).resolve()
    if not resolved.is_relative_to(base_dir.resolve()):
        raise HTTPException(status_code=404)
    if not resolved.is_file():
        raise HTTPException(status_code=404)
    return FileResponse(resolved)

html_to_markdown

html_to_markdown(html: str, preserve_links: bool = True, truncate_after: int | None = None, body_width: int = 0) -> str

Convert HTML to clean Markdown for LLM consumption.

Strips images and excessive formatting while preserving semantic structure.

Source code in uni_server/utils/html_processing.py
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
def html_to_markdown(
    html: str,
    preserve_links: bool = True,
    truncate_after: int | None = None,
    body_width: int = 0,
) -> str:
    """
    Convert HTML to clean Markdown for LLM consumption.

    Strips images and excessive formatting while preserving semantic structure.
    """
    if not html or not html.strip():
        return ""

    h = html2text.HTML2Text()

    h.ignore_links = not preserve_links
    h.ignore_images = True
    h.ignore_emphasis = False
    h.body_width = body_width
    h.unicode_snob = True
    h.escape_snob = True

    markdown_text = h.handle(html)
    markdown_text = _clean_markdown_text(markdown_text)

    if truncate_after and len(markdown_text) > truncate_after:
        markdown_text = markdown_text[:truncate_after].rstrip() + "...[truncated]"

    return markdown_text

safe_filename

safe_filename(filename: str) -> str

Return a secure version of a filename for safe filesystem storage.

The result can safely be passed to :func:os.path.join. The filename returned is ASCII-only for maximum portability. On Windows the function also avoids special device file names.

safe_filename("My cool movie.mov") 'My_cool_movie.mov' safe_filename("../../../etc/passwd") 'etc_passwd' safe_filename('i contain cool \xfcml\xe4uts.txt') 'i_contain_cool_umlauts.txt'

May return an empty string if the input contains only unsafe characters.

Source code in uni_server/utils/secure_filename.py
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
def safe_filename(filename: str) -> str:
    r"""
    Return a secure version of a filename for safe filesystem storage.

    The result can safely be passed to :func:`os.path.join`. The filename
    returned is ASCII-only for maximum portability. On Windows the function
    also avoids special device file names.

    >>> safe_filename("My cool movie.mov")
    'My_cool_movie.mov'
    >>> safe_filename("../../../etc/passwd")
    'etc_passwd'
    >>> safe_filename('i contain cool \xfcml\xe4uts.txt')
    'i_contain_cool_umlauts.txt'

    May return an empty string if the input contains only unsafe characters.
    """
    filename = unicodedata.normalize("NFKD", filename)
    filename = filename.encode("ascii", "ignore").decode("ascii")

    for sep in os.sep, os.path.altsep:
        if sep:
            filename = filename.replace(sep, " ")

    filename = _filename_ascii_strip_re.sub("", "_".join(filename.split())).strip("._")

    if (
        os.name == "nt"
        and filename
        and filename.split(".")[0].upper() in _windows_device_files
    ):
        filename = f"_{filename}"

    return filename

is_cuda_available

is_cuda_available() -> bool

Check if CUDA is available for PyTorch operations.

Returns:

Type Description
bool

True if CUDA is available and PyTorch was built with CUDA support, False otherwise.

Source code in uni_plugin_sdk.py
123
124
125
126
127
128
129
130
131
132
def is_cuda_available() -> bool:
    """
    Check if CUDA is available for PyTorch operations.

    Returns:
        True if CUDA is available and PyTorch was built with CUDA support, False otherwise.
    """
    import torch  # noqa: PLC0415 - heavy import, only needed here

    return torch.cuda.is_available()

is_hf_authenticated

is_hf_authenticated() -> bool

Check if Hugging Face authentication is active.

Returns:

Type Description
bool

True if authenticated with Hugging Face, False otherwise.

Source code in uni_plugin_sdk.py
140
141
142
143
144
145
146
147
def is_hf_authenticated() -> bool:
    """
    Check if Hugging Face authentication is active.

    Returns:
        True if authenticated with Hugging Face, False otherwise.
    """
    return _plugin_state.is_hf_authenticated()

flag_for_restart

flag_for_restart() -> None

Flag that a restart is needed to apply settings changes.

Call this when the user changes a setting that requires a plugin reload to take effect. This activates the "Restart to apply changes" banner.

Source code in uni_plugin_sdk.py
150
151
152
153
154
155
156
157
def flag_for_restart() -> None:
    """
    Flag that a restart is needed to apply settings changes.

    Call this when the user changes a setting that requires a plugin reload to take
    effect. This activates the "Restart to apply changes" banner.
    """
    restart_state.flag_for_restart()

get_config

get_config() -> AppConfig

Get the central application configuration from /user/settings.toml.

Use config.plugin to access settings specific to the current plugin.

Source code in uni_plugin_sdk.py
160
161
162
163
164
165
166
def get_config() -> AppConfig:
    """
    Get the central application configuration from `/user/settings.toml`.

    Use `config.plugin` to access settings specific to the current plugin.
    """
    return _plugin_state.get_config()

llm_chat

llm_chat(role: str, messages: list[dict[str, Any]], **overrides: Any) -> Iterator[dict[str, Any]]

Stream chat completions from the model assigned to role.

Source code in uni_plugin_sdk.py
169
170
171
172
173
174
175
176
177
178
def llm_chat(
    role: str,
    messages: list[dict[str, Any]],
    **overrides: Any,
) -> Iterator[dict[str, Any]]:
    """
    Stream chat completions from the model assigned to ``role``.
    """
    router = _get_model_router()
    return router.chat(role=role, messages=messages, **overrides)

llm_embeddings

llm_embeddings(text: str) -> list[float]

Generate embeddings using the configured embeddings model.

Source code in uni_plugin_sdk.py
181
182
183
184
185
186
def llm_embeddings(text: str) -> list[float]:
    """
    Generate embeddings using the configured embeddings model.
    """
    router = _get_model_router()
    return router.embeddings(text)

llm_embeddings_model

llm_embeddings_model() -> str | None

Return the configured embeddings model name, or None if not configured.

Source code in uni_plugin_sdk.py
189
190
191
192
193
194
def llm_embeddings_model() -> str | None:
    """
    Return the configured embeddings model name, or None if not configured.
    """
    router = _get_model_router()
    return router.embeddings_model()

cache

cache(ttl: str | None = None, max_size: int | None = None) -> Callable[[Callable[P, R]], CachedFunction[P, R]]

Decorator to cache function results with optional TTL and size limit.

This decorator can be used to effectively rate limit expensive operations by caching their results for a specified duration.

@uni.cache(ttl="5m")
def fetch_weather_data():
    return {"temperature": 22, "conditions": "Sunny"}

@uni.cache(ttl="1h", max_size=100)
def get_user_profile(user_id: str):
    return {"id": user_id, "name": "..."}

Parameters:

Name Type Description Default
ttl str | None

Time-to-live for cached results (e.g., "5m", "1h", None = cache forever)

None
max_size int | None

Maximum number of cached entries (None = unlimited)

None
Source code in uni_plugin_sdk.py
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
def cache(
    ttl: str | None = None,
    max_size: int | None = None,
) -> Callable[[Callable[P, R]], CachedFunction[P, R]]:
    """
    Decorator to cache function results with optional TTL and size limit.

    This decorator can be used to effectively rate limit expensive operations
    by caching their results for a specified duration.

    ```python
    @uni.cache(ttl="5m")
    def fetch_weather_data():
        return {"temperature": 22, "conditions": "Sunny"}

    @uni.cache(ttl="1h", max_size=100)
    def get_user_profile(user_id: str):
        return {"id": user_id, "name": "..."}
    ```

    Args:
        ttl: Time-to-live for cached results (e.g., "5m", "1h", None = cache forever)
        max_size: Maximum number of cached entries (None = unlimited)
    """

    def decorator(func: Callable[P, R]) -> CachedFunction[P, R]:
        cache_data: dict[tuple[Any, ...], tuple[R, float]] = {}
        ttl_seconds = parse_duration(ttl) if ttl is not None else None

        @wraps(func)
        def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
            try:
                key: tuple[Any, ...] = (args, tuple(sorted(kwargs.items())))
                hash(key)
            except TypeError:
                return func(*args, **kwargs)

            if key in cache_data:
                result, timestamp = cache_data[key]
                if ttl_seconds is None or get_time() - timestamp < ttl_seconds:
                    return result
                del cache_data[key]

            if max_size is not None and len(cache_data) >= max_size:
                oldest_key = next(iter(cache_data))
                del cache_data[oldest_key]

            result = func(*args, **kwargs)
            cache_data[key] = (result, get_time())
            return result

        def clear_cache() -> None:
            cache_data.clear()

        wrapper_with_attr = cast(Any, wrapper)
        wrapper_with_attr.clear_cache = clear_cache
        return cast(CachedFunction[P, R], wrapper_with_attr)

    return decorator

socket

socket(handler_func: Callable[..., None]) -> Callable[..., None]

Decorator to register Socket.IO handlers for this plugin.

@uni.socket
def register_socket_handlers(socket):
    @socket.on("game_move")
    def handle_move(data):
        # uni.current_device.id automatically available
        socket.emit("move_result", {"success": True})

    @socket.on_connect
    def handle_connect():
        socket.emit("welcome", {"message": "Connected!"})
Source code in uni_plugin_sdk.py
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
def socket(handler_func: Callable[..., None]) -> Callable[..., None]:
    """
    Decorator to register Socket.IO handlers for this plugin.

    ```python
    @uni.socket
    def register_socket_handlers(socket):
        @socket.on("game_move")
        def handle_move(data):
            # uni.current_device.id automatically available
            socket.emit("move_result", {"success": True})

        @socket.on_connect
        def handle_connect():
            socket.emit("welcome", {"message": "Connected!"})
    ```
    """
    get_uni().plugins.register_socket_handlers(handler_func)
    return handler_func

tool

tool[T: Callable[..., Any]](func: T) -> T
tool[T: Callable[..., Any]](*, safe: bool | Callable[..., bool] = False) -> Callable[[T], T]

Decorator to register a function as a tool for the LLM to use.

Parameters:

Name Type Description Default
func T | None

The tool function (when used without parentheses)

None
safe bool | Callable[..., bool]

Whether the tool is safe to execute without approval. Can be True (always safe), False (always needs approval), or a callable that validates safety based on arguments.

False
# Default: requires approval
@uni.tool
def send_email(to: str, subject: str, body: str): ...

# Always safe
@uni.tool(safe=True)
def calculate(expression: str): ...

# Conditionally safe based on arguments
def is_safe_url(url: str) -> bool:
    return url in get_recent_search_urls()

@uni.tool(safe=is_safe_url)
def fetch_webpage(url: str): ...
Source code in uni_plugin_sdk.py
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
def tool[T: Callable[..., Any]](
    func: T | None = None,
    *,
    safe: bool | Callable[..., bool] = False,
) -> T | Callable[[T], T]:
    """
    Decorator to register a function as a tool for the LLM to use.

    Args:
        func: The tool function (when used without parentheses)
        safe: Whether the tool is safe to execute without approval.
              Can be True (always safe), False (always needs approval),
              or a callable that validates safety based on arguments.

    ```python
    # Default: requires approval
    @uni.tool
    def send_email(to: str, subject: str, body: str): ...

    # Always safe
    @uni.tool(safe=True)
    def calculate(expression: str): ...

    # Conditionally safe based on arguments
    def is_safe_url(url: str) -> bool:
        return url in get_recent_search_urls()

    @uni.tool(safe=is_safe_url)
    def fetch_webpage(url: str): ...
    ```
    """

    def register(f: T) -> T:
        get_uni().plugins.register_tool(f, safe=safe)
        return f

    if func is None:
        return register
    else:
        return register(func)

register_tool

register_tool(schema: dict[str, Any], handler: Callable[..., Any], /, safe: bool | Callable[..., bool] = False) -> None

Register a tool with a custom JSON schema.

Parameters:

Name Type Description Default
schema dict[str, Any]

Dictionary describing the tool JSON schema

required
handler Callable[..., Any]

Function to handle tool calls

required
safe bool | Callable[..., bool]

Whether the tool is safe to execute without approval

False
Source code in uni_plugin_sdk.py
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
def register_tool(
    schema: dict[str, Any],
    handler: Callable[..., Any],
    /,
    safe: bool | Callable[..., bool] = False,
) -> None:
    """
    Register a tool with a custom JSON schema.

    Args:
        schema: Dictionary describing the tool JSON schema
        handler: Function to handle tool calls
        safe: Whether the tool is safe to execute without approval
    """
    get_uni().plugins.register_custom_tool(
        ToolDescriptor(schema=schema, handler=handler),
        safe=safe,
    )

audio_effect

audio_effect(effect: type[AudioEffect] | AudioEffect) -> type[AudioEffect] | AudioEffect

Decorator to register an audio effect for TTS output.

@uni.audio_effect
class MyEffect(uni.AudioEffect):
    ...
Source code in uni_plugin_sdk.py
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
def audio_effect(
    effect: type[AudioEffect] | AudioEffect,
) -> type[AudioEffect] | AudioEffect:
    """
    Decorator to register an audio effect for TTS output.

    ```python
    @uni.audio_effect
    class MyEffect(uni.AudioEffect):
        ...
    ```
    """
    effect_instance = _ensure_instance(effect, AudioEffect)  # type: ignore[type-abstract]
    get_uni().plugins.register_audio_effect(effect_instance)
    return effect

system_prompt_modifier

system_prompt_modifier(modifier: SystemPromptModifierFunction) -> SystemPromptModifierFunction
system_prompt_modifier(*, priority: int = 100) -> Callable[[SystemPromptModifierFunction], SystemPromptModifierFunction]

Decorator to register a function that modifies the system prompt.

@uni.system_prompt_modifier
def my_modifier(event: uni.SystemPromptEvent) -> str:
    return event.prompt + "\n\nAdditional instructions here."
Source code in uni_plugin_sdk.py
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
def system_prompt_modifier(
    modifier: SystemPromptModifierFunction | None = None,
    *,
    priority: int = 100,
) -> (
    SystemPromptModifierFunction
    | Callable[[SystemPromptModifierFunction], SystemPromptModifierFunction]
):
    """
    Decorator to register a function that modifies the system prompt.

    ```python
    @uni.system_prompt_modifier
    def my_modifier(event: uni.SystemPromptEvent) -> str:
        return event.prompt + "\\n\\nAdditional instructions here."
    ```
    """

    def register(
        func: SystemPromptModifierFunction,
    ) -> SystemPromptModifierFunction:
        get_uni().plugins.register_system_prompt_modifier(func, priority)
        return func

    if modifier is None:
        return register
    else:
        return register(modifier)

user_prompt_modifier

user_prompt_modifier(modifier: UserPromptModifierFunction) -> UserPromptModifierFunction
user_prompt_modifier(*, priority: int = 100) -> Callable[[UserPromptModifierFunction], UserPromptModifierFunction]
Decorator to register a function that modifies individual user prompts.

The return value replaces the entire prompt, so include the original:

```python
@uni.user_prompt_modifier
def add_context(event: UserPromptEvent) -> str | None:
    if "weather" in event.prompt:
        return event.prompt + "

Remember to provide current weather info." return None ```

Args:
    modifier: Function that takes a UserPromptEvent and returns modified prompt or None
    priority: Execution priority (lower values run first, default: 100)
Source code in uni_plugin_sdk.py
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
def user_prompt_modifier(
    modifier: UserPromptModifierFunction | None = None,
    *,
    priority: int = 100,
) -> (
    UserPromptModifierFunction
    | Callable[[UserPromptModifierFunction], UserPromptModifierFunction]
):
    """
    Decorator to register a function that modifies individual user prompts.

    The return value replaces the entire prompt, so include the original:

    ```python
    @uni.user_prompt_modifier
    def add_context(event: UserPromptEvent) -> str | None:
        if "weather" in event.prompt:
            return event.prompt + "\nRemember to provide current weather info."
        return None
    ```

    Args:
        modifier: Function that takes a UserPromptEvent and returns modified prompt or None
        priority: Execution priority (lower values run first, default: 100)
    """

    def register(
        func: UserPromptModifierFunction,
    ) -> UserPromptModifierFunction:
        get_uni().plugins.register_user_prompt_modifier(func, priority)
        return func

    if modifier is None:
        return register
    else:
        return register(modifier)

message_context

message_context(func: MessageContextFunction) -> MessageContextFunction
message_context(*, preload: Callable[[], Any] | None = None) -> Callable[[MessageContextFunction], MessageContextFunction]

Decorator to inject ephemeral context for a specific message.

@uni.message_context
def inject_docs(event: MessageEvent) -> str | None:
    if needs_docs(event.message):
        return "Relevant documentation: ..."
    return None

# With preloading
@uni.message_context(preload=lambda: fetch_weather())
def inject_weather(event: MessageEvent, weather_data: dict) -> dict | None:
    if needs_weather(event.message):
        return weather_data  # Can return dict directly
    return None

Parameters:

Name Type Description Default
func MessageContextFunction | None

Function that takes MessageEvent (and preloaded data if applicable)

None
preload Callable[[], Any] | None

Optional function to preload data while user is speaking

None
Source code in uni_plugin_sdk.py
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
def message_context(
    func: MessageContextFunction | None = None,
    *,
    preload: Callable[[], Any] | None = None,
) -> MessageContextFunction | Callable[[MessageContextFunction], MessageContextFunction]:
    """
    Decorator to inject ephemeral context for a specific message.

    ```python
    @uni.message_context
    def inject_docs(event: MessageEvent) -> str | None:
        if needs_docs(event.message):
            return "Relevant documentation: ..."
        return None

    # With preloading
    @uni.message_context(preload=lambda: fetch_weather())
    def inject_weather(event: MessageEvent, weather_data: dict) -> dict | None:
        if needs_weather(event.message):
            return weather_data  # Can return dict directly
        return None
    ```

    Args:
        func: Function that takes MessageEvent (and preloaded data if applicable)
        preload: Optional function to preload data while user is speaking
    """

    def register(
        context_func: MessageContextFunction,
    ) -> MessageContextFunction:
        get_uni().plugins.register_message_context(context_func, preload)
        return context_func

    if func is None:
        return register
    else:
        return register(func)

on_nightly_reset

on_nightly_reset(func: Callable[[NightlyResetEvent], None]) -> Callable[[NightlyResetEvent], None]

Decorator to register a function to be called during nightly reset.

@uni.on_nightly_reset
def consolidate_data(event: NightlyResetEvent) -> None:
    # Process the day's conversation history
    process_history(event.conversation_history)
Source code in uni_plugin_sdk.py
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
def on_nightly_reset(
    func: Callable[[NightlyResetEvent], None],
) -> Callable[[NightlyResetEvent], None]:
    """
    Decorator to register a function to be called during nightly reset.

    ```python
    @uni.on_nightly_reset
    def consolidate_data(event: NightlyResetEvent) -> None:
        # Process the day's conversation history
        process_history(event.conversation_history)
    ```
    """
    get_uni().plugins.events.register_nightly_reset_handler(func)
    return func

tts

tts(engine: type[TtsEngine] | TtsEngine) -> type[TtsEngine] | TtsEngine

Decorator to override the default TTS engine.

@uni.tts
class MyTtsEngine(uni.TtsEngine):
    ...
Source code in uni_plugin_sdk.py
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
def tts(
    engine: type[TtsEngine] | TtsEngine,
) -> type[TtsEngine] | TtsEngine:
    """
    Decorator to override the default TTS engine.

    ```python
    @uni.tts
    class MyTtsEngine(uni.TtsEngine):
        ...
    ```
    """
    tts_instance = _ensure_instance(engine, TtsEngine)  # type: ignore[type-abstract]
    get_uni().plugins.register_tts_engine(tts_instance)
    return engine

wake_word

wake_word(engine: type[WakeWordEngine] | WakeWordEngine) -> type[WakeWordEngine] | WakeWordEngine

Decorator to register a wake word detection engine.

@uni.wake_word
class MyWakeWordEngine(uni.WakeWordEngine):
    ...
Source code in uni_plugin_sdk.py
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
def wake_word(
    engine: type[WakeWordEngine] | WakeWordEngine,
) -> type[WakeWordEngine] | WakeWordEngine:
    """
    Decorator to register a wake word detection engine.

    ```python
    @uni.wake_word
    class MyWakeWordEngine(uni.WakeWordEngine):
        ...
    ```
    """
    engine_instance = _ensure_instance(engine, WakeWordEngine)  # type: ignore[type-abstract]
    get_uni().plugins.register_wake_word_engine(engine_instance)
    return engine

avatar

avatar(module: str) -> Callable[[AvatarConfigFunction], AvatarConfigFunction]

Decorator to register an avatar provider.

@uni.avatar(module="~/avatar.js")
def create_avatar() -> dict[str, Any]:
    # Return a configuration object for the frontend
    return {
        "expressions": ["normal", "happy", "listening", "sleeping"]
    }

If an expressions key is present, those expressions will be triggered automatically during the conversation based on sentiment analysis.

Refer to the plugins documentation for the JS module interface.

Parameters:

Name Type Description Default
module str

Path to the JavaScript module. "~/" = plugin static root

required
Source code in uni_plugin_sdk.py
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
def avatar(module: str) -> Callable[[AvatarConfigFunction], AvatarConfigFunction]:
    """
    Decorator to register an avatar provider.

    ```python
    @uni.avatar(module="~/avatar.js")
    def create_avatar() -> dict[str, Any]:
        # Return a configuration object for the frontend
        return {
            "expressions": ["normal", "happy", "listening", "sleeping"]
        }
    ```

    If an `expressions` key is present, those expressions will be triggered automatically
    during the conversation based on sentiment analysis.

    Refer to the plugins documentation for the JS module interface.

    Args:
        module: Path to the JavaScript module. `"~/"` = plugin static root
    """

    def register(func: AvatarConfigFunction) -> AvatarConfigFunction:
        if module.startswith("~/"):
            plugin_name = find_current_plugin_name()
            module_path = f"/plugins/{plugin_name}/static/{module[2:]}"
            static_path = f"/plugins/{plugin_name}/static"
        else:
            module_path = module
            static_path = ""

        get_uni().plugins.register_avatar(func, module_path, static_path)
        return func

    return register

message_channel

message_channel(cls: type[MessageChannel] | MessageChannel) -> type[MessageChannel] | MessageChannel

Decorator to register a message channel for async communication.

@uni.message_channel
class DiscordChannel(uni.MessageChannel):
    @property
    def online(self) -> bool:
        return self.client.is_ready()

    def send(self, message: str) -> None:
        self.dm_channel.send(message)
Source code in uni_plugin_sdk.py
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
def message_channel(
    cls: type[MessageChannel] | MessageChannel,
) -> type[MessageChannel] | MessageChannel:
    """
    Decorator to register a message channel for async communication.

    ```python
    @uni.message_channel
    class DiscordChannel(uni.MessageChannel):
        @property
        def online(self) -> bool:
            return self.client.is_ready()

        def send(self, message: str) -> None:
            self.dm_channel.send(message)
    ```
    """
    instance = _ensure_instance(cls, MessageChannel)  # type: ignore[type-abstract]
    get_uni().plugins.register_message_channel(instance)
    return cls

router

router(func: Callable[[], APIRouter]) -> APIRouter

Decorator to register a plugin router.

All routes in the router are automatically secured with device authentication.

@uni.router
def register_routes():
    router = uni.PluginRouter()
    # Add routes...
    return router
Source code in uni_plugin_sdk.py
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
def router(func: Callable[[], APIRouter]) -> APIRouter:
    """
    Decorator to register a plugin router.

    All routes in the router are automatically secured with device authentication.

    ```python
    @uni.router
    def register_routes():
        router = uni.PluginRouter()
        # Add routes...
        return router
    ```
    """
    return get_uni().plugins.register_router(func)

push_text_card

push_text_card(title: str, message: str, footer: str | None = None, timeout: str | None = None, *, all_devices: Literal[True], device_id: None = None, id: str | None = None) -> str
push_text_card(title: str, message: str, footer: str | None = None, timeout: str | None = None, *, all_devices: Literal[False] = False, device_id: str | None = None, id: str | None = None) -> str

Push a basic text card to a device.

Parameters:

Name Type Description Default
title str

Title of the card

required
message str

Content of the card

required
footer str | None

Optional footer text

None
timeout str | None

Optional duration string (e.g., "30s", "2m") to auto-dismiss the card

None
all_devices bool

If True, push the card to all devices

False
device_id str | None

Push to a specific device by ID

None
id str | None

Optional ID for the card. If a card with this ID exists, it will be updated

None

Returns:

Type Description
str

The generated card ID (card may not be displayed if no device is active)

Raises:

Type Description
ValueError

If both all_devices and device_id are specified

Source code in uni_plugin_sdk.py
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
def push_text_card(
    title: str,
    message: str,
    footer: str | None = None,
    timeout: str | None = None,
    all_devices: bool = False,
    device_id: str | None = None,
    id: str | None = None,  # noqa: A002
) -> str:
    """
    Push a basic text card to a device.

    Args:
        title: Title of the card
        message: Content of the card
        footer: Optional footer text
        timeout: Optional duration string (e.g., "30s", "2m") to auto-dismiss the card
        all_devices: If True, push the card to all devices
        device_id: Push to a specific device by ID
        id: Optional ID for the card. If a card with this ID exists, it will be updated

    Returns:
        The generated card ID (card may not be displayed if no device is active)

    Raises:
        ValueError: If both all_devices and device_id are specified
    """
    if all_devices and device_id:
        raise ValueError("Cannot specify both all_devices and device_id")

    html = """
        <div class="card">
            <h4>{{title}}</h4>
            <p>{{body}}</p>
            {% if footer %}<small>{{footer}}</small>{% endif %}
        </div>
        """
    values = {"title": title, "body": message, "footer": footer}
    if all_devices:
        return push_card(
            html=html,
            html_values=values,
            timeout=timeout,
            all_devices=True,
            id=id,
        )
    return push_card(
        html=html,
        html_values=values,
        timeout=timeout,
        device_id=device_id,
        id=id,
    )

push_card

push_card(html: str, html_values: dict[str, Any] | None = None, script: str | None = None, style: str | None = None, timeout: str | None = None, *, all_devices: Literal[True], id: str | None = None) -> str
push_card(html: str, html_values: dict[str, Any] | None = None, script: str | None = None, style: str | None = None, timeout: str | None = None, *, all_devices: Literal[False] = False, device_id: str, id: str | None = None) -> str
push_card(html: str, html_values: dict[str, Any] | None = None, script: str | None = None, style: str | None = None, timeout: str | None = None, *, all_devices: Literal[False] = False, device_id: None = None, id: str | None = None) -> str

Push a custom HTML card to a device.

Parameters:

Name Type Description Default
html str

HTML template for the card (supports Jinja2 syntax)

required
html_values dict[str, Any] | None

Optional dictionary of values to render in the HTML

None
script str | None

Optional JavaScript for the card

None
style str | None

Optional CSS for the card

None
timeout str | None

Optional duration string (e.g., "30s", "2m") to auto-dismiss the card

None
all_devices bool

If True, push the card to all devices

False
device_id str | None

Push to a specific device by ID

None
id str | None

Optional ID for the card. If a card with this ID exists, it will be updated

None

Returns:

Type Description
str

The generated card ID (card may not be displayed if no device is active)

Raises:

Type Description
ValueError

If both all_devices and device_id are specified

Source code in uni_plugin_sdk.py
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
def push_card(
    html: str,
    html_values: dict[str, Any] | None = None,
    script: str | None = None,
    style: str | None = None,
    timeout: str | None = None,
    all_devices: bool = False,
    device_id: str | None = None,
    id: str | None = None,  # noqa: A002
) -> str:
    """
    Push a custom HTML card to a device.

    Args:
        html: HTML template for the card (supports Jinja2 syntax)
        html_values: Optional dictionary of values to render in the HTML
        script: Optional JavaScript for the card
        style: Optional CSS for the card
        timeout: Optional duration string (e.g., "30s", "2m") to auto-dismiss the card
        all_devices: If True, push the card to all devices
        device_id: Push to a specific device by ID
        id: Optional ID for the card. If a card with this ID exists, it will be updated

    Returns:
        The generated card ID (card may not be displayed if no device is active)

    Raises:
        ValueError: If both all_devices and device_id are specified
    """
    if all_devices and device_id:
        raise ValueError("Cannot specify both all_devices and device_id")

    plugin_name = find_current_plugin_name()
    timeout_seconds = parse_duration(timeout) if timeout is not None else None
    return get_uni().card_manager.push_card(
        html,
        html_values=html_values,
        script=script,
        style=style,
        timeout=timeout_seconds,
        all_devices=all_devices,
        device_id=device_id,
        card_id=id,
        plugin_name=plugin_name,
    )

dismiss_card

dismiss_card(card_id: str) -> None

Dismiss a card from all devices.

Parameters:

Name Type Description Default
card_id str

The ID of the card to dismiss

required
Source code in uni_plugin_sdk.py
939
940
941
942
943
944
945
946
def dismiss_card(card_id: str) -> None:
    """
    Dismiss a card from all devices.

    Args:
        card_id: The ID of the card to dismiss
    """
    get_uni().card_manager.dismiss_card(card_id)

user_data_path

user_data_path(name: str, create_dirs: bool = True) -> Path

Get a path for plugin data (file or directory).

Parameters:

Name Type Description Default
name str

Path relative to plugin data directory (e.g., "models" or "cache/voice.onnx")

required
create_dirs bool

Whether to create parent directories if they don't exist

True

Returns:

Type Description
Path

Path object

Source code in uni_plugin_sdk.py
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
def user_data_path(name: str, create_dirs: bool = True) -> Path:
    """
    Get a path for plugin data (file or directory).

    Args:
        name: Path relative to plugin data directory (e.g., "models" or "cache/voice.onnx")
        create_dirs: Whether to create parent directories if they don't exist

    Returns:
        Path object
    """
    plugin_name = find_current_plugin_name()
    path = get_config().paths.user_data_path(f"plugin.{plugin_name}/{name}")
    if create_dirs:
        path.parent.mkdir(parents=True, exist_ok=True)
    return path

secure_json_file

secure_json_file(key: str) -> SecureJSONFile

Get a secure JSON file handle for a single configuration object.

Parameters:

Name Type Description Default
key str

Storage key (e.g., "credentials", "state")

required

Returns:

Type Description
SecureJSONFile

SecureJSONFile instance

Source code in uni_plugin_sdk.py
967
968
969
970
971
972
973
974
975
976
977
978
def secure_json_file(key: str) -> SecureJSONFile:
    """
    Get a secure JSON file handle for a single configuration object.

    Args:
        key: Storage key (e.g., "credentials", "state")

    Returns:
        SecureJSONFile instance
    """
    path = user_data_path(f"{key}.json.dat")
    return SecureJSONFile(path)

secure_json_lines_file

secure_json_lines_file(key: str) -> SecureJSONLFile

Get a secure JSONL (JSON Lines) file handle: https://jsonlines.org/

Optimized for appending without needing to re-encrypt existing contents.

Parameters:

Name Type Description Default
key str

Storage key (e.g., "logs", "events")

required

Returns:

Type Description
SecureJSONLFile

SecureJSONLFile instance

Source code in uni_plugin_sdk.py
981
982
983
984
985
986
987
988
989
990
991
992
993
994
def secure_json_lines_file(key: str) -> SecureJSONLFile:
    """
    Get a secure JSONL (JSON Lines) file handle: https://jsonlines.org/

    Optimized for appending without needing to re-encrypt existing contents.

    Args:
        key: Storage key (e.g., "logs", "events")

    Returns:
        SecureJSONLFile instance
    """
    path = user_data_path(f"{key}.jsonl.dat")
    return SecureJSONLFile(path)

run_task

run_task(func: Callable[[], Any]) -> Any

Run a function in a managed background thread.

Tasks are tracked and waited for during system unload. Use this instead of creating raw threads to ensure clean shutdown.

def expensive_computation():
    result = process_data()
    save_result(result)

uni.run_task(expensive_computation)

Parameters:

Name Type Description Default
func Callable[[], Any]

Function to run in background (no arguments)

required

Returns:

Type Description
Any

A Future that can be used to get the result or check completion

Source code in uni_plugin_sdk.py
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
def run_task(func: Callable[[], Any]) -> Any:
    """
    Run a function in a managed background thread.

    Tasks are tracked and waited for during system unload. Use this
    instead of creating raw threads to ensure clean shutdown.

    ```python
    def expensive_computation():
        result = process_data()
        save_result(result)

    uni.run_task(expensive_computation)
    ```

    Args:
        func: Function to run in background (no arguments)

    Returns:
        A Future that can be used to get the result or check completion
    """
    return get_uni().plugins.run_task(func)

check_passphrase

check_passphrase(passphrase: str) -> bool

Check if the given passphrase is valid.

Use this to require a valid passphrase before allowing sensitive operations, like backup/restore.

Parameters:

Name Type Description Default
passphrase str

User-provided passphrase to validate

required
Source code in uni_plugin_sdk.py
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
def check_passphrase(passphrase: str) -> bool:
    """
    Check if the given passphrase is valid.

    Use this to require a valid passphrase before allowing sensitive operations,
    like backup/restore.

    Args:
        passphrase: User-provided passphrase to validate
    """
    return validate_passphrase(get_config(), passphrase)

on_connect

on_connect(func: DeviceConnectHandler) -> DeviceConnectHandler

Decorator to register a function to be called when a device connects.

@uni.on_connect
def handle_connect(event: DeviceConnectEvent):
    uni.push_text_card("Welcome", f"Device {uni.current_device.name} connected")
Source code in uni_plugin_sdk.py
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
def on_connect(func: DeviceConnectHandler) -> DeviceConnectHandler:
    """
    Decorator to register a function to be called when a device connects.

    ```python
    @uni.on_connect
    def handle_connect(event: DeviceConnectEvent):
        uni.push_text_card("Welcome", f"Device {uni.current_device.name} connected")
    ```
    """
    get_uni().plugins.events.register_device_connect_handler(func)
    return func

on_disconnect

on_disconnect(func: DeviceDisconnectHandler) -> DeviceDisconnectHandler

Decorator to register a function to be called when a device disconnects.

@uni.on_disconnect
def handle_disconnect(event: DeviceDisconnectEvent):
    print(f"Device {uni.current_device.name} disconnected")
Source code in uni_plugin_sdk.py
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
def on_disconnect(func: DeviceDisconnectHandler) -> DeviceDisconnectHandler:
    """
    Decorator to register a function to be called when a device disconnects.

    ```python
    @uni.on_disconnect
    def handle_disconnect(event: DeviceDisconnectEvent):
        print(f"Device {uni.current_device.name} disconnected")
    ```
    """
    get_uni().plugins.events.register_device_disconnect_handler(func)
    return func

on_dismiss

on_dismiss(func: CardDismissHandler) -> CardDismissHandler

Decorator to register a function to be called when a card is dismissed.

@uni.on_dismiss
def handle_dismiss(event: CardDismissEvent):
    print(f"Card {event.card_id} was dismissed")
Source code in uni_plugin_sdk.py
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
def on_dismiss(func: CardDismissHandler) -> CardDismissHandler:
    """
    Decorator to register a function to be called when a card is dismissed.

    ```python
    @uni.on_dismiss
    def handle_dismiss(event: CardDismissEvent):
        print(f"Card {event.card_id} was dismissed")
    ```
    """
    plugin_name = find_current_plugin_name()
    get_uni().plugins.events.register_card_dismiss_handler(func, plugin_name)
    return func

on_incoming_audio

on_incoming_audio(func: IncomingAudioHandler) -> IncomingAudioHandler

Decorator to register a function to be called when audio data is received.

@uni.on_incoming_audio
def process_audio(event: IncomingAudioEvent):
    # Process the audio chunk
    audio_data = event.chunk
    sample_rate = event.sample_rate
Source code in uni_plugin_sdk.py
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
def on_incoming_audio(func: IncomingAudioHandler) -> IncomingAudioHandler:
    """
    Decorator to register a function to be called when audio data is received.

    ```python
    @uni.on_incoming_audio
    def process_audio(event: IncomingAudioEvent):
        # Process the audio chunk
        audio_data = event.chunk
        sample_rate = event.sample_rate
    ```
    """
    get_uni().plugins.events.register_incoming_audio_handler(func)
    return func

on_load

on_load(func: LoadHandler) -> LoadHandler

Decorator to register a function called after all components load.

This event fires after all core components (LLM, TTS, STT, etc.) are fully loaded but before the system transitions to 'ready' phase. Plugins can use this to perform initialization that depends on UNI's capabilities or to download dependencies during the loading screen.

@uni.on_load
def initialize_plugin(event: LoadEvent):
    # Download models or data files
    download_required_models()

    # Initialize with core components available
    setup_custom_tools()

    # Pre-warm caches
    preload_frequently_used_data()

Note: The system remains in 'loading' phase while these handlers run. Keep initialization reasonably fast to avoid delaying system readiness.

Source code in uni_plugin_sdk.py
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
def on_load(func: LoadHandler) -> LoadHandler:
    """
    Decorator to register a function called after all components load.

    This event fires after all core components (LLM, TTS, STT, etc.) are fully
    loaded but before the system transitions to 'ready' phase. Plugins can
    use this to perform initialization that depends on UNI's capabilities or
    to download dependencies during the loading screen.

    ```python
    @uni.on_load
    def initialize_plugin(event: LoadEvent):
        # Download models or data files
        download_required_models()

        # Initialize with core components available
        setup_custom_tools()

        # Pre-warm caches
        preload_frequently_used_data()
    ```

    Note: The system remains in 'loading' phase while these handlers run.
    Keep initialization reasonably fast to avoid delaying system readiness.
    """
    get_uni().plugins.events.register_load_handler(func)
    return func

on_unload

on_unload(func: UnloadHandler) -> UnloadHandler

Decorator to register a function called when the system unloads.

This event fires both on user-initiated unload (returning to settings) and on process shutdown. Use it to clean up resources.

@uni.on_unload
def cleanup(event: UnloadEvent):
    # Clean up resources
    save_state()
    close_connections()
Source code in uni_plugin_sdk.py
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
def on_unload(func: UnloadHandler) -> UnloadHandler:
    """
    Decorator to register a function called when the system unloads.

    This event fires both on user-initiated unload (returning to settings)
    and on process shutdown. Use it to clean up resources.

    ```python
    @uni.on_unload
    def cleanup(event: UnloadEvent):
        # Clean up resources
        save_state()
        close_connections()
    ```
    """
    get_uni().plugins.events.register_unload_handler(func)
    return func

on_response_generated

on_response_generated(func: ResponseGeneratedHandler) -> ResponseGeneratedHandler

Decorator to register a function called when LLM completes its response.

This event fires after the full response has been generated and added to conversation history. It contains all information about the interaction including the trigger, prompts, and complete response.

@uni.on_response_generated
def summarize_interaction(event: ResponseGeneratedEvent):
    # - event.trigger: one of "user" or "impulse"
    # - event.user_prompt: original user prompt (if trigger == "user")
    # - event.impulse_data: impulse data (if trigger == "impulse")
    # - event.final_prompt: final processed prompt
    # - event.full_response: full LLM response
    if event.trigger == "user":
        if action_items := extract_todos(event.user_prompt, event.full_response):
            schedule_impulses(action_items)
Source code in uni_plugin_sdk.py
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
def on_response_generated(
    func: ResponseGeneratedHandler,
) -> ResponseGeneratedHandler:
    """
    Decorator to register a function called when LLM completes its response.

    This event fires after the full response has been generated and added to
    conversation history. It contains all information about the interaction
    including the trigger, prompts, and complete response.

    ```python
    @uni.on_response_generated
    def summarize_interaction(event: ResponseGeneratedEvent):
        # - event.trigger: one of "user" or "impulse"
        # - event.user_prompt: original user prompt (if trigger == "user")
        # - event.impulse_data: impulse data (if trigger == "impulse")
        # - event.final_prompt: final processed prompt
        # - event.full_response: full LLM response
        if event.trigger == "user":
            if action_items := extract_todos(event.user_prompt, event.full_response):
                schedule_impulses(action_items)
    ```
    """
    get_uni().plugins.events.register_response_generated_handler(func)
    return func

on_embeddings_model_changed

on_embeddings_model_changed(func: EmbeddingsModelChangedHandler) -> EmbeddingsModelChangedHandler

Decorator to register a function called when the embeddings model changes.

This event fires when the user configures a different embeddings model in settings. Use it to invalidate caches or recompute stored embeddings.

@uni.on_embeddings_model_changed
def handle_model_change(event: EmbeddingsModelChangedEvent):
    # event.old_model: previous model name (or None)
    # event.new_model: new model name (or None)
    if event.new_model:
        recompute_embeddings()
Source code in uni_plugin_sdk.py
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
def on_embeddings_model_changed(
    func: EmbeddingsModelChangedHandler,
) -> EmbeddingsModelChangedHandler:
    """
    Decorator to register a function called when the embeddings model changes.

    This event fires when the user configures a different embeddings model in settings.
    Use it to invalidate caches or recompute stored embeddings.

    ```python
    @uni.on_embeddings_model_changed
    def handle_model_change(event: EmbeddingsModelChangedEvent):
        # event.old_model: previous model name (or None)
        # event.new_model: new model name (or None)
        if event.new_model:
            recompute_embeddings()
    ```
    """
    get_uni().plugins.events.register_embeddings_model_changed_handler(func)
    return func

is_quiet_hours

is_quiet_hours() -> bool

Check whether quiet hours are currently active.

During quiet hours, UNI won't initiate contact — but impulses are still delivered if the user starts a conversation or taps the impulse indicator.

Source code in uni_plugin_sdk.py
1190
1191
1192
1193
1194
1195
1196
1197
def is_quiet_hours() -> bool:
    """
    Check whether quiet hours are currently active.

    During quiet hours, UNI won't initiate contact — but impulses are still delivered if the
    user starts a conversation or taps the impulse indicator.
    """
    return impulse_utils.is_quiet_hours(get_uni().config)

get_quiet_hours_window

get_quiet_hours_window() -> tuple[int, int] | None

Return the configured quiet hours window as (start_hour, end_hour) in 24-hour format, or None if quiet hours are disabled.

Hours use the system's local time and account for DST automatically.

Source code in uni_plugin_sdk.py
1200
1201
1202
1203
1204
1205
1206
1207
def get_quiet_hours_window() -> tuple[int, int] | None:
    """
    Return the configured quiet hours window as (start_hour, end_hour) in 24-hour format,
    or None if quiet hours are disabled.

    Hours use the system's local time and account for DST automatically.
    """
    return impulse_utils.quiet_hours_window(get_uni().config)

send_impulse

send_impulse(type: str, instruction: str, due_after: str = '0s', *, expires_after: str = '0s', data: dict[str, Any] | None = None) -> None

Send an impulse for UNI to act on.

An impulse is like an event that prompts UNI to act proactively. During the due_after period, it waits for a natural conversation. Once due, UNI actively dispatches it.

Parameters:

Name Type Description Default
type str

An event name like "reminder"

required
instruction str

Instruction for the AI on how to act on the impulse

required
due_after str

How long before active dispatch (e.g., "30m", "2h"). "0s" (default) = never dispatch, only consumed by active conversations.

'0s'
expires_after str

Timeout after which the impulse is silently dropped (e.g., "1h"). "0s" (default) = never expires. Time spent in quiet hours is not counted.

'0s'
data dict[str, Any] | None

Additional data fields for the impulse (optional)

None
Source code in uni_plugin_sdk.py
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
def send_impulse(
    type: str,  # noqa: A002
    instruction: str,
    due_after: str = "0s",
    *,
    expires_after: str = "0s",
    data: dict[str, Any] | None = None,
) -> None:
    """
    Send an impulse for UNI to act on.

    An impulse is like an event that prompts UNI to act proactively. During the due_after
    period, it waits for a natural conversation. Once due, UNI actively dispatches it.

    Args:
        type: An event name like "reminder"
        instruction: Instruction for the AI on how to act on the impulse
        due_after: How long before active dispatch (e.g., "30m", "2h").
            "0s" (default) = never dispatch, only consumed by active conversations.
        expires_after: Timeout after which the impulse is silently dropped (e.g., "1h").
            "0s" (default) = never expires. Time spent in quiet hours is not counted.
        data: Additional data fields for the impulse (optional)
    """
    due_after_delta = timedelta(seconds=parse_duration(due_after))
    expires_after_delta = timedelta(seconds=parse_duration(expires_after))

    uni = get_uni()
    impulse = {"type": type, "instruction": instruction, "data": data}
    source = find_current_plugin_name()
    uni.impulse_queue.queue_impulse(
        impulse, due_after_delta, source=source, expires_after=expires_after_delta
    )

job

job(*, interval: str | None = None, daily: str | None = None) -> Callable[[Callable[[], Any]], Callable[[], Any]]

Decorator to create recurring background job.

The job will run according to the specified schedule.

@uni.job(interval="30m")
def refresh_cache():
    update_cached_data()

@uni.job(daily="21:00")
def evening_summary():
    summary = generate_daily_summary()
    uni.send_impulse("reminder", "Share highlights", data={"summary": summary})

Parameters:

Name Type Description Default
interval str | None

Interval string (e.g., "30m", "2h", "45s", "1.5h", "90 minutes")

None
daily str | None

Daily time in 24-hour format ("HH:MM" or "H:MM")

None

Returns:

Type Description
Callable[[Callable[[], Any]], Callable[[], Any]]

Decorator function

Source code in uni_plugin_sdk.py
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
def job(
    *,
    interval: str | None = None,
    daily: str | None = None,
) -> Callable[[Callable[[], Any]], Callable[[], Any]]:
    """
    Decorator to create recurring background job.

    The job will run according to the specified schedule.

    ```python
    @uni.job(interval="30m")
    def refresh_cache():
        update_cached_data()

    @uni.job(daily="21:00")
    def evening_summary():
        summary = generate_daily_summary()
        uni.send_impulse("reminder", "Share highlights", data={"summary": summary})
    ```

    Args:
        interval: Interval string (e.g., "30m", "2h", "45s", "1.5h", "90 minutes")
        daily: Daily time in 24-hour format ("HH:MM" or "H:MM")

    Returns:
        Decorator function
    """
    if (interval is None) == (daily is None):
        raise ValueError("Must specify exactly one of 'interval' or 'daily'")

    def decorator(func: Callable[[], Any]) -> Callable[[], Any]:
        if interval is not None:
            total_seconds = parse_duration(interval)
            get_uni().plugins.register_job(
                func=func,
                interval_seconds=total_seconds,
            )
        elif daily is not None:
            try:
                hour, minute = daily.split(":")
                at_time = time(int(hour), int(minute))
            except (ValueError, AttributeError) as e:
                raise ValueError(f"Time must be in HH:MM format, got: {daily!r}") from e

            get_uni().plugins.register_job(func=func, daily_time=at_time)

        return func

    return decorator