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

Tools & cards

Defining tools

Tools are functions the LLM can call. Their return values are serialized to JSON and passed back to the model. Tools can be written as Python functions or custom schemas.

Python function

Use the @uni.tool decorator. The tool schema is auto-generated from the function name, parameters, and docstring.

Basic tool definition
@uni.tool
def get_weather(location: str) -> dict:
    """
    Get current weather for a location.

    Args:
        location: City name or coordinates

    Returns:
        Weather data including temperature and conditions
    """
    return {
        "location": location,
        "temperature": 22,
        "conditions": "Sunny"
    }

Custom schema

For dynamic tools (like those loaded from an MCP server), use uni.register_tool with a custom JSON schema:

Custom schema registration
config = uni.get_config()
locations = config.plugin.get("locations", list, ["Amsterdam", "London"])

uni.register_tool(
    schema={
        "name": "get_weather",
        "description": "Get current weather for a location.",
        "parameters": {
            "type": "object",
            "properties": {
                "location": {
                    "type": "string",
                    "enum": locations
                }
            },
            "required": ["location"]
        }
    },
    handler=lambda params: {"temperature": 22}
)

Design tips

Tool schemas should work well with language models, including smaller local ones. Avoid parameters that require precise computation:

  • Absolute date/times (LLMs can't do reliable calendar math)
  • Units requiring conversion (minutes to seconds, etc.)

What not to do

A tool like update_timer(name, new_seconds) forces the LLM to convert "2 hours and 40 minutes" into seconds. That's error-prone.

A better design accepts natural language, letting your code handle the parsing:

LLM-friendly tool design
def update_timer(name: str, action: str, duration: str):
    """
    Update a running timer.

    Args:
        name: Timer name
        action: One of: "add", "subtract", "set"
        duration: Natural language (e.g., "2 hours 30 minutes")
    """

Built-in helpers

The SDK includes uni.parse_datetime and uni.parse_duration for handling natural language time inputs.

Safety

External data sources (emails, web content) can contain prompt injection attacks. By default, tools require user approval before execution.

Mark safe tools explicitly:

Marking a tool as safe
@uni.tool(safe=True)
def calculator(expression: str) -> str:
    """Evaluate a mathematical expression."""

Or make it conditional:

Conditional safety
def is_recent_search_result(url: str):
    return url in search_result_urls

@uni.tool(safe=is_recent_search_result)
def web_fetch(url: str) -> str:
    """Read a webpage."""

If using uni.register_tool, that also has a safe parameter.

Response time

UNI focuses on real-time interaction. If a tool takes longer than 5 seconds, it times out. For long-running operations, start a thread and use uni.send_impulse to deliver results. See Impulses.

Cards

Cards push rich content to connected devices. Use them for notifications, information displays, or interactive widgets. Users can swipe to dismiss.

Estimated commute

About 23 minutes to work

Medium traffic

Downloading model

example-Q4_K_S.gguf - 75%

Text cards

Quick notifications with uni.push_text_card:

Simple text card
uni.push_text_card(
    title="Estimated commute",
    message="About 23 minutes to work",
    footer="Medium traffic",  # optional
    timeout="30s"  # auto-dismiss
)

To send to all connected devices, add all_devices=True. To update/replace an existing card, pass its id.

HTML cards

For custom layouts, use uni.push_card with HTML (Jinja2), CSS, and JavaScript. Cards render in a shadow DOM with the UNI CSS theme.

Custom HTML card with progress
uni.push_card(
    html="""
    <div class="card">
        <h4>{{ title }}</h4>
        <progress value="{{ progress }}" max="100"></progress>
        <p>{{ status }}</p>
    </div>
    """,
    html_values={
        "title": "Processing",
        "progress": 75,
        "status": "Almost done..."
    },
    script="""
    const progress = uni.card.element.querySelector('progress');
    let value = 75;
    setInterval(() => {
        value = Math.min(100, value + 5);
        progress.value = value;
        if (value >= 100) uni.card.dismiss();
    }, 1000);
    """
)

Avoid XSS

Don't use f-strings for dynamic values. Always use the html_values dictionary and Jinja2 templating.

Dismissal

Cards can be dismissed by users (swipe), automatically (timeout), or programmatically with uni.dismiss_card(card_id). To handle dismissal in your plugin:

Handling card dismissal
@uni.on_dismiss
def handle_dismiss(event: uni.CardDismissEvent):
    print(f"Card {event.card_id} was dismissed")

In the card's JavaScript, register a cleanup callback:

Client-side cleanup
uni.card.onDismiss(() => {
  // Perform cleanup (clear timers, close connections, etc.)
});