Skip to content

Python SDK

Use the Python SDK when chats with SARA are part of an application or a Python script. Import everything from skyportalai. The old skyportal package was removed in 0.3.0.

Create a client

from skyportalai import Skyportal

client = Skyportal(
    api_key="YOUR_API_KEY",
    base_url="https://app.skyportal.ai",
    timeout=30.0,
    max_retries=2,
)
Parameter Default Description
api_key SKYPORTALAI_API_KEY API key. Required: without one, the client raises SkyportalError
base_url SKYPORTALAI_BASE_URL, then https://app.skyportal.ai API root. A trailing slash is removed
timeout 30.0 Seconds per request. Must be greater than zero
max_retries 2 Retries for GET requests. Must be zero or more
session a new requests.Session Optional session that you own

Prefer a context manager:

from skyportalai import Skyportal

with Skyportal() as client:
    chat = client.chat.create_chat("Report disk usage", server_id=42)

close() only closes sessions the SDK created. A session you pass in stays yours.

Create and follow a chat

from skyportalai import Skyportal

with Skyportal() as client:
    chat = client.chat.create_chat(
        "Report disk usage and identify the largest directory",
        server_id=42,
    )

    status = chat.wait(timeout=300)
    print(chat.chat_id, status.status)

    for message in chat.messages().messages:
        print(f"{message.role}: {message.content}")

wait() polls once a second while the status is processing or uninitialized. It returns when the chat settles or needs a decision, and raises WaitTimeoutError when timeout runs out. Without timeout, it waits as long as it takes.

Several hosts and Kubernetes namespaces

chat = client.chat.create_chat(
    "Compare NVIDIA driver versions on these hosts",
    server_ids=[42, 43, 44],
    active_server_id=42,
)
chat = client.chat.create_chat(
    "Why are pods restarting in ml-serving?",
    server_ids=[12],
    selected_namespaces={12: ["ml-serving"]},
)
  • server_id (one host) and server_ids can't be used together.
  • active_server_id, active_host_id and selected_namespaces need server_ids. The SDK raises ValueError before sending anything if these rules are broken.
  • selected_namespaces={12: ["__all__"]} gives SARA the whole cluster.

To change the scope between turns, use chat.select_servers():

chat.select_servers([42, 43], active_server_id=43)
chat.select_servers([12], selected_namespaces={12: ["ml-serving", "batch"]})

Leave out selected_namespaces to keep the current namespaces, or pass {} to clear them. An empty server list clears the scope.

Approvals

By default, wait() returns with status == "awaiting_approval" when SARA needs a decision. Handle one approval at a time, then wait again:

from skyportalai import Skyportal

with Skyportal() as client:
    chat = client.chat.create_chat("Check disk usage", server_id=42)
    status = chat.wait()

    while status.status == "awaiting_approval" and status.pending_approvals:
        approval = status.pending_approvals[0]
        print("Type:", approval.type)
        print("Command:", approval.command)

        # Approve only after a person has reviewed the action.
        if input("Approve? [y/N] ").strip().lower() == "y":
            chat.approve(
                approval.approval_id,
                approval_type=approval.type or "bash_command",
                command=approval.command or None,
            )
        else:
            chat.reject(
                approval.approval_id,
                approval_type=approval.type or "bash_command",
                reason="Outside the approved scope",
            )
        status = chat.wait()

You can also pass a callback. It gets each pending approval and returns True to approve, False to reject, or None to leave it pending, in which case wait() returns:

def review(approval):
    print(approval.command)
    return None  # leave it for a person

status = chat.wait(on_approval=review)

Your account's approval mode

Your account has one approval mode shared by the web app, the CLI and the SDK:

client.get_permission_mode()        # "ask" or "autoapprove"
client.set_permission_mode("ask")

Auto-approve

When the mode is "autoapprove" and you don't pass a callback, wait() approves every pending command and plan for you, one at a time, and keeps waiting. That includes destructive commands. Other approval types are returned to you. A callback always takes precedence over the mode, and if the mode can't be read, wait() returns instead of approving. Read Approvals and safety first.

Continue, rescope or cancel

chat.send("Now inspect the previous job logs")
chat.select_server(84)
chat.cancel(reason="No longer required")

See what SARA did

execution = chat.execution_status()
events = chat.events(event_types=["BashCommandApprovalRequestedEvent", "StopEvent"], limit=100)
tool_calls = chat.tool_calls(limit=100)
reasoning = chat.reasoning(limit=100)
evaluations = chat.evaluations()
environment = chat.environment()

These methods return the server's JSON as dictionaries. Keep unknown fields: new ones can appear at any time.

events() accepts after_timestamp, event_types (a list or a comma-separated string) and limit (up to 500). The result has events, has_more and next_timestamp. Pass next_timestamp back as after_timestamp to read the next page. Audit and events describes the event format.

Kubernetes clusters and Ansible playbooks

with open("kubeconfig.yaml") as f:
    cluster = client.kubernetes.connect("ml-cluster", f.read(), environment="Staging")

print(cluster.id, cluster.status, cluster.namespaces)
clusters = client.kubernetes.list()
client.kubernetes.disconnect(cluster.id)
with open("cleanup.yml") as f:
    playbook = client.ansible.create("nightly-cleanup", f.read(), description="Remove old checkpoints")

deployment = client.ansible.deploy(playbook.id, server_id=42)
status = client.chat.wait(deployment.chat_id)

client.ansible also has list(), get(id), update(id, ...) and delete(id). Some kubeconfigs are refused; see Kubernetes.

Methods by chat ID

Every Chat method has a counterpart on client.chat that takes the chat ID:

client.chat.get_status(chat_id)
client.chat.get_messages(chat_id, after_sequence=0, limit=100)
client.chat.send_message(chat_id, "Continue")
client.chat.select_servers(chat_id, [42, 43])
client.chat.wait(chat_id, timeout=300)
client.chat.approve(chat_id, approval_id, approval_type="bash_command", command=command)
client.chat.reject(chat_id, approval_id, approval_type="bash_command", reason=reason)
client.chat.cancel(chat_id, reason="No longer required")
client.chat.get_events(chat_id, after_timestamp=None, event_types=None, limit=100)

Returned types

The models are immutable dataclasses. Each one keeps the full server payload in raw.

Type Main fields
Chat chat_id, raw
ChatStatus status, workflow_type, pending_approvals, raw
PendingApproval approval_id, type, command, plan_id, reason, raw
ApprovalResult success, decision, raw
Message role, content, sequence, raw
MessagesPage messages, has_more, raw
KubernetesCluster id, name, environment, status, api_endpoint, namespaces, connection_verified, raw
AnsiblePlaybook id, name, description, content, created_at, updated_at, raw
AnsibleDeployment chat_id, playbook_id, server_id, status, poll_url, raw
PermissionMode "ask" or "autoapprove"

Message.content is plain text built from the message blocks. The original blocks stay in Message.raw.