Tutorial: Build a Dice Roll Command¶
This tutorial walks you through building a complete command from scratch. By the end, you will have a working roll_dice command that responds to voice input like "Roll two dice" or "Roll a d20".
Prerequisites: A working jarvis-node-setup installation. See the Getting Started guide.
Step 1: Create the File¶
Create a new file at jarvis-node-setup/commands/dice_command.py:
from typing import List
from core.ijarvis_command import IJarvisCommand, CommandExample
from core.ijarvis_parameter import JarvisParameter
from core.ijarvis_secret import IJarvisSecret
from core.command_response import CommandResponse
from core.request_information import RequestInformation
These are the standard imports every command needs.
Step 2: Define the Class and Required Properties¶
class DiceCommand(IJarvisCommand):
@property
def command_name(self) -> str:
return "roll_dice"
@property
def description(self) -> str:
return "Roll one or more dice with a configurable number of sides. Default is one six-sided die."
@property
def keywords(self) -> List[str]:
return ["roll", "dice", "die", "d20", "random", "d6"]
The command_name is the unique identifier. The description tells the LLM when to use this command. The keywords help with fuzzy matching during command discovery.
Step 3: Define Parameters¶
Our dice command takes two optional parameters:
@property
def parameters(self) -> List[JarvisParameter]:
return [
JarvisParameter(
"sides",
"int",
required=False,
description="Number of sides on each die (default 6)",
default="6",
),
JarvisParameter(
"count",
"int",
required=False,
description="Number of dice to roll (default 1)",
default="1",
),
]
Both parameters are optional with sensible defaults. The LLM sees these as the tool's parameter schema and extracts values from the voice command.
Step 4: Declare Secrets¶
This command does not need any API keys or configuration:
If your command needed an API key, you would return JarvisSecret objects here. See the API Integration Tutorial for that pattern.
Step 5: Write Prompt Examples¶
Prompt examples teach the LLM how to parse voice commands into parameters. Keep this list concise -- these go into every system prompt.
def generate_prompt_examples(self) -> List[CommandExample]:
return [
CommandExample(
voice_command="Roll a die",
expected_parameters={},
is_primary=True,
),
CommandExample(
voice_command="Roll two dice",
expected_parameters={"count": 2},
),
CommandExample(
voice_command="Roll a d20",
expected_parameters={"sides": 20},
),
CommandExample(
voice_command="Roll 3 twelve-sided dice",
expected_parameters={"sides": 12, "count": 3},
),
]
The is_primary=True example is used for one-shot inference. Only one example can be primary.
Notice that the first example has empty expected_parameters -- when the user says just "Roll a die", no parameters need to be extracted because the defaults handle it.
Step 6: Write Adapter Examples¶
Adapter examples are used for LoRA fine-tuning. They should be more varied and cover edge cases:
def generate_adapter_examples(self) -> List[CommandExample]:
return [
CommandExample("Roll a die", {}, is_primary=True),
CommandExample("Roll a dice", {}),
CommandExample("Roll the dice", {}),
CommandExample("Throw a die", {}),
CommandExample("Roll two dice", {"count": 2}),
CommandExample("Roll 5 dice", {"count": 5}),
CommandExample("Roll a d20", {"sides": 20}),
CommandExample("Roll a twenty-sided die", {"sides": 20}),
CommandExample("Roll a d12", {"sides": 12}),
CommandExample("Roll 4 d6", {"sides": 6, "count": 4}),
CommandExample("Roll 3 twelve-sided dice", {"sides": 12, "count": 3}),
CommandExample("Roll 2 twenty-sided dice", {"sides": 20, "count": 2}),
CommandExample("Roll 2d8", {"sides": 8, "count": 2}),
CommandExample("Give me a random number", {"sides": 6}),
]
More examples = better adapter training. Cover casual phrasings, shorthand notations, and spoken-out numbers.
Step 7: Implement run()¶
This is where the actual logic lives:
import random
def run(self, request_info: RequestInformation, **kwargs) -> CommandResponse:
sides = int(kwargs.get("sides", 6))
count = int(kwargs.get("count", 1))
# Validate inputs
if sides < 2:
return CommandResponse.error_response(
error_details="A die must have at least 2 sides",
)
if count < 1 or count > 100:
return CommandResponse.error_response(
error_details="You can roll between 1 and 100 dice",
)
# Roll the dice
rolls = [random.randint(1, sides) for _ in range(count)]
total = sum(rolls)
return CommandResponse.final_response(
context_data={
"rolls": rolls,
"total": total,
"sides": sides,
"count": count,
"message": f"Rolled {count}d{sides}: {rolls} (total: {total})",
}
)
Key points:
- Extract parameters from
**kwargswith defaults - Validate inputs and return
error_responsefor bad values - Return
final_responsebecause dice rolls don't need follow-up conversation - The
context_data["message"]is what the LLM uses to generate the spoken response
Complete File¶
Here is the full dice_command.py:
import random
from typing import List
from core.ijarvis_command import IJarvisCommand, CommandExample
from core.ijarvis_parameter import JarvisParameter
from core.ijarvis_secret import IJarvisSecret
from core.command_response import CommandResponse
from core.request_information import RequestInformation
class DiceCommand(IJarvisCommand):
@property
def command_name(self) -> str:
return "roll_dice"
@property
def description(self) -> str:
return "Roll one or more dice with a configurable number of sides. Default is one six-sided die."
@property
def keywords(self) -> List[str]:
return ["roll", "dice", "die", "d20", "random", "d6"]
@property
def parameters(self) -> List[JarvisParameter]:
return [
JarvisParameter(
"sides", "int", required=False,
description="Number of sides on each die (default 6)",
default="6",
),
JarvisParameter(
"count", "int", required=False,
description="Number of dice to roll (default 1)",
default="1",
),
]
@property
def required_secrets(self) -> List[IJarvisSecret]:
return []
def generate_prompt_examples(self) -> List[CommandExample]:
return [
CommandExample("Roll a die", {}, is_primary=True),
CommandExample("Roll two dice", {"count": 2}),
CommandExample("Roll a d20", {"sides": 20}),
CommandExample("Roll 3 twelve-sided dice", {"sides": 12, "count": 3}),
]
def generate_adapter_examples(self) -> List[CommandExample]:
return [
CommandExample("Roll a die", {}, is_primary=True),
CommandExample("Roll a dice", {}),
CommandExample("Roll the dice", {}),
CommandExample("Throw a die", {}),
CommandExample("Roll two dice", {"count": 2}),
CommandExample("Roll 5 dice", {"count": 5}),
CommandExample("Roll a d20", {"sides": 20}),
CommandExample("Roll a twenty-sided die", {"sides": 20}),
CommandExample("Roll a d12", {"sides": 12}),
CommandExample("Roll 4 d6", {"sides": 6, "count": 4}),
CommandExample("Roll 3 twelve-sided dice", {"sides": 12, "count": 3}),
CommandExample("Roll 2 twenty-sided dice", {"sides": 20, "count": 2}),
CommandExample("Roll 2d8", {"sides": 8, "count": 2}),
CommandExample("Give me a random number", {"sides": 6}),
]
def run(self, request_info: RequestInformation, **kwargs) -> CommandResponse:
sides = int(kwargs.get("sides", 6))
count = int(kwargs.get("count", 1))
if sides < 2:
return CommandResponse.error_response(
error_details="A die must have at least 2 sides",
)
if count < 1 or count > 100:
return CommandResponse.error_response(
error_details="You can roll between 1 and 100 dice",
)
rolls = [random.randint(1, sides) for _ in range(count)]
total = sum(rolls)
return CommandResponse.final_response(
context_data={
"rolls": rolls,
"total": total,
"sides": sides,
"count": count,
"message": f"Rolled {count}d{sides}: {rolls} (total: {total})",
}
)
Step 8: Install the Command¶
Run the install script to seed the secrets database:
Since this command has no secrets, this just registers it. For commands with secrets, this creates the empty secret rows in the database.
Step 9: Test It¶
E2E Testing¶
Add test cases to test_command_parsing.py and run:
# List all tests to find your command
python test_command_parsing.py -l
# Run tests for your command
python test_command_parsing.py -c roll_dice
Unit Testing¶
You can also test run() directly:
from commands.dice_command import DiceCommand
from core.request_information import RequestInformation
cmd = DiceCommand()
request = RequestInformation(voice_command="Roll 2d6", conversation_id="test")
response = cmd.run(request, sides=6, count=2)
assert response.success
assert len(response.context_data["rolls"]) == 2
assert all(1 <= r <= 6 for r in response.context_data["rolls"])
See Testing Commands for comprehensive testing guidance.
What's Next¶
- Add parameters with enums for constrained values
- Add API integration for commands that call external services
- Add OAuth authentication for commands that need user authorization
- Learn about response patterns for interactive follow-up flows