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.
@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:
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:
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:
@uni.tool(safe=True)
def calculator(expression: str) -> str:
"""Evaluate a mathematical expression."""
Or make it conditional:
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 trafficDownloading model
example-Q4_K_S.gguf - 75%
Text cards¶
Quick notifications with uni.push_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.
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:
@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:
uni.card.onDismiss(() => {
// Perform cleanup (clear timers, close connections, etc.)
});