Skip to content

Response Patterns

CommandResponse is the return type of every run() method. It carries the command's output data back through the pipeline to the command center, which uses it to generate a spoken response and update the mobile UI.

CommandResponse Structure

@dataclass
class CommandResponse:
    context_data: dict | None = None       # Raw data for the LLM to generate a spoken response
    success: bool = True                   # Whether the command succeeded
    error_details: str | None = None       # Error message (sets success=False automatically)
    wait_for_input: bool = True            # Whether to keep the conversation open
    clear_history: bool = False            # Clear conversation history before next turn
    metadata: dict | None = None           # Command-specific metadata
    actions: list[IJarvisButton] | None = None  # Interactive buttons
    is_chunked_response: bool = False      # Chunked response flag
    chunk_session_id: str | None = None    # Session ID for chunked responses

Factory Methods

success_response() -- Standard Success

The most common response. The conversation stays open for follow-up by default.

return CommandResponse.success_response(
    context_data={
        "city": "Chicago",
        "temperature": 72,
        "description": "partly cloudy",
        "message": "It's 72 degrees and partly cloudy in Chicago",
    }
)

The LLM uses context_data to generate a natural spoken response. Include a message key as a hint for what to say, plus structured data for the mobile UI.

Signature:

CommandResponse.success_response(
    context_data: dict | None = None,
    wait_for_input: bool = True,
    metadata: dict | None = None,
) -> CommandResponse

error_response() -- Errors

Return when the command fails. The conversation closes by default.

return CommandResponse.error_response(
    error_details="OpenWeather API key is not configured",
    context_data={"error": "missing_api_key"},
)

Signature:

CommandResponse.error_response(
    error_details: str,
    context_data: dict | None = None,
    wait_for_input: bool = False,
) -> CommandResponse

Tips:

  • error_details is a human-friendly message that the LLM uses to generate a spoken error
  • Include context_data for debugging even in errors
  • Set wait_for_input=True if the user can retry (e.g., "City not found, try a different one")

follow_up_response() -- Expects More Input

Return when the command succeeds but expects the user to continue the conversation. Used by the calculator (for chained calculations), chat, and other interactive commands.

return CommandResponse.follow_up_response(
    context_data={
        "result": 42,
        "calculation": "6 * 7 = 42",
        "message": "6 times 7 equals 42",
    }
)

The conversation stays open. The user can say "now multiply that by 2" and the command center will route the follow-up to the same command.

Signature:

CommandResponse.follow_up_response(
    context_data: dict,
    metadata: dict | None = None,
) -> CommandResponse

final_response() -- One-Shot, No Follow-Up

Return when the command is done and no follow-up is expected. The conversation ends.

return CommandResponse.final_response(
    context_data={
        "message": "Timer set for 5 minutes",
        "timer_id": "abc123",
        "duration_seconds": 300,
    }
)

Signature:

CommandResponse.final_response(
    context_data: dict | None = None,
    metadata: dict | None = None,
) -> CommandResponse

chunked_response() -- Large Data

Return when the response is too large for a single message. The user can ask for more with follow-up commands like "continue" or "next page".

return CommandResponse.chunked_response(
    session_id="news-session-abc123",
    context_data={
        "headlines": headlines[:5],
        "total": len(headlines),
        "message": f"Here are the top 5 of {len(headlines)} headlines. Say 'more' for the next batch.",
    }
)

Signature:

CommandResponse.chunked_response(
    session_id: str,
    context_data: dict | None = None,
    metadata: dict | None = None,
) -> CommandResponse

The session_id is used to track the chunked session across turns. On the next turn, the command can use the session ID to resume from where it left off.

validation_error() -- Parameter Validation Failures

Returned automatically by the execute() pipeline when validate_call() produces errors. You rarely call this directly -- it is generated for you.

return CommandResponse.validation_error([
    ValidationResult(
        success=False,
        param_name="entity_id",
        command_name="control_device",
        message="Device 'light.nonexistent' not found",
        valid_values=["light.living_room", "light.kitchen", "light.bedroom"],
    ),
])

The command center receives this and can either:

  • Retry with a corrected value from valid_values
  • Ask the user to clarify

Signature:

CommandResponse.validation_error(
    results: list[ValidationResult],
) -> CommandResponse

Choosing the Right Response

Scenario Factory Method wait_for_input
Weather report success_response() True
Calculator result follow_up_response() True
Timer set final_response() False
Music playing success_response(wait_for_input=False) False
API error error_response() False
News with pagination chunked_response() True
Bad device name validation_error() False
Email sent final_response() False
Email draft preview Response with actions True

Interactive Actions with IJarvisButton

Attach buttons to a response for interactive flows. The mobile app renders these as tappable buttons.

from core.ijarvis_button import IJarvisButton

return CommandResponse(
    context_data={
        "draft": {
            "to": "alice@example.com",
            "subject": "Meeting tomorrow",
            "body": "Hi Alice, are we still on for tomorrow?",
        },
        "message": "Here's your email draft to Alice. Send it or cancel?",
    },
    success=True,
    wait_for_input=True,
    actions=[
        IJarvisButton(
            button_text="Send",
            button_action="send_click",
            button_type="primary",
            button_icon="send",
        ),
        IJarvisButton(
            button_text="Cancel",
            button_action="cancel_click",
            button_type="destructive",
            button_icon="close",
        ),
    ],
)

Button Types

Type Appearance Use Case
"primary" Highlighted / accent color Main action (Send, Confirm)
"secondary" Neutral Alternative action (Edit, Save Draft)
"destructive" Red / warning Dangerous action (Delete, Cancel)

Button Icons

Use MaterialCommunityIcons names:

IJarvisButton("Send", "send_click", "primary", button_icon="send")
IJarvisButton("Edit", "edit_click", "secondary", button_icon="pencil")
IJarvisButton("Delete", "delete_click", "destructive", button_icon="delete")

Handling Button Actions

When the user taps a button, the command center calls your command's handle_action() method:

def handle_action(self, action_name: str, context: dict) -> CommandResponse:
    if action_name == "send_click":
        draft = context.get("draft", {})
        self._send_email(
            to=draft["to"],
            subject=draft["subject"],
            body=draft["body"],
        )
        return CommandResponse.final_response(
            context_data={"message": "Email sent successfully."}
        )

    # The base class handles "cancel_click" automatically
    return super().handle_action(action_name, context)

The context dict contains the context_data from the original response. This is how you pass data from the preview to the action handler (e.g., the email draft).

Default cancel_click Handler

The base class provides a default handler for cancel_click:

# Built-in on JarvisCommandBase -- no need to implement
if action_name == "cancel_click":
    return CommandResponse.final_response(
        context_data={"cancelled": True, "message": "Cancelled."}
    )

context_data Best Practices

context_data is the bridge between your command and the LLM's spoken response. The command center's LLM reads this data and generates natural language.

Always include a message key

context_data={
    "temperature": 72,
    "city": "Chicago",
    "message": "It's 72 degrees in Chicago",  # LLM uses this as a starting point
}

Include structured data for the mobile UI

context_data={
    "emails": [
        {"from": "alice@example.com", "subject": "Meeting", "snippet": "..."},
        {"from": "bob@example.com", "subject": "Lunch?", "snippet": "..."},
    ],
    "total": 5,
    "message": "You have 5 unread emails. The first is from Alice about Meeting.",
}

Keep error context helpful

# Good -- gives the LLM context to explain the problem
context_data={
    "ticker": "INVALID",
    "error": "not_found",
    "message": "I couldn't find a stock with ticker 'INVALID'",
}

# Bad -- no context
context_data=None

The metadata Field

metadata is for command-internal data that should not influence the spoken response. It flows through the pipeline but is not shown to the LLM.

return CommandResponse.success_response(
    context_data={"message": "Timer set for 5 minutes"},
    metadata={
        "timer_id": "internal-uuid",
        "created_at": "2025-01-01T12:00:00Z",
        "node_id": "abc123",
    },
)

The clear_history Flag

Set clear_history=True to reset the conversation context before the next turn. Useful when a command fundamentally changes the conversation state:

return CommandResponse(
    context_data={"message": "Starting a new conversation"},
    success=True,
    clear_history=True,
)