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) andserver_idscan't be used together.active_server_id,active_host_idandselected_namespacesneedserver_ids. The SDK raisesValueErrorbefore 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:
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.