Skip to content

Command Center Client

A package that needs the household's LLM, or needs to call the server, uses two helpers from the Command SDK:

Helper What it does
llm_chat_text(prompt) Sends prompt to the server's LLM and returns its text reply (a str)
cc_post(path, json) POSTs json to a server route as the node and returns the decoded JSON reply (a dict)

Both are plain functions, never raise, and return None when there is no answer. The device the package runs on registers the backend behind them at startup, so a package never needs a URL, a key or an HTTP client.

Where they work

Device llm_chat_text cc_post
Pi with the Python node Yes Yes, any server route, authenticated as the node
Computer with the desktop agent No: returns None No: returns None
Pi with the Go voice node Yes Only /api/v0/node/... routes

On a computer the desktop agent registers neither backend, so a command there gets the SDK's safe default (None). Write every caller with a fallback. See Where your code runs.

Ask the LLM

from jarvis_command_sdk import llm_chat_text

text = llm_chat_text("Write one cheerful sentence about Monday mornings.")
if text is None:
    text = "Happy Monday."  # no LLM, or the call failed

The prompt goes to the server's live model as a single system message, at temperature 0 (POST /api/v0/node/llm/chat). The reply is plain text.

Structured answers

There is no schema-aware variant. Ask for JSON in the prompt and parse it yourself, keeping a fallback for a reply that doesn't parse:

import json

from jarvis_command_sdk import llm_chat_text


def parse_city(question: str) -> dict | None:
    reply = llm_chat_text(
        "Answer with JSON only, as {\"name\": str, \"country\": str}. "
        f"Question: {question}"
    )
    if reply is None:
        return None
    start, end = reply.find("{"), reply.rfind("}")
    if start == -1 or end <= start:
        return None
    try:
        return json.loads(reply[start : end + 1])
    except json.JSONDecodeError:
        return None

Call the server

cc_post takes a path on the server and a JSON body. The routes made for packages are the node plugin API under /api/v0/node/, for example:

Route Body
/api/v0/node/push-notification title, body, optional priority, category, user_id, dedupe_key
/api/v0/node/llm/chat What llm_chat_text uses
from jarvis_command_sdk import cc_post

resp = cc_post(
    "/api/v0/node/push-notification",
    {"title": "Laundry", "body": "The dryer is done.", "category": "reminder"},
)
if resp is None:
    pass  # no backend, or the request failed

For the inbox, timers and alerts, prefer the SDK's own helpers (JarvisInbox, timer_set, alerts_pending and the rest; see the Command SDK) over raw routes.

Testing

In tests, and while Pantry validates a package, no backend is registered and both helpers return None. To test the success path, register a fake:

from jarvis_command_sdk import LLMBackend, set_llm_backend


class FakeLLM(LLMBackend):
    def chat_text(self, prompt: str) -> str | None:
        return '{"name": "Paris", "country": "France"}'


set_llm_backend(FakeLLM())
# ... run the code under test ...
set_llm_backend(None)

CCHttpBackend and set_cc_backend do the same for cc_post.

The node's own client

The Python node talks to the server through JarvisCommandCenterClient (clients/jarvis_command_center_client.py in jarvis-node-setup). It is node-internal: the voice pipeline (send_command*, start_conversation, tool results) and the backends above are built on it. It is not in the SDK, a computer doesn't have it, and its constructor needs the server URL from the node's service discovery. Packages use llm_chat_text and cc_post instead.

Some of its methods call routes jarvisd doesn't have, and return None there: lightweight_chat (/api/v0/lightweight/chat), train_tool_router, train_node_adapter and get_adapter_job_status. Tool-router and adapter training were cut from jarvisd.