splox 0.3.0__tar.gz → 0.3.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- splox-0.3.2/CHANGELOG.md +101 -0
- splox-0.3.2/PKG-INFO +469 -0
- splox-0.3.2/README.md +436 -0
- {splox-0.3.0 → splox-0.3.2}/pyproject.toml +1 -1
- splox-0.3.2/src/splox/__init__.py +110 -0
- {splox-0.3.0 → splox-0.3.2}/src/splox/_client.py +40 -36
- splox-0.3.2/src/splox/_discovery.py +102 -0
- splox-0.3.2/src/splox/_ids.py +70 -0
- splox-0.3.2/src/splox/_interactions.py +172 -0
- splox-0.3.2/src/splox/_mcp.py +426 -0
- splox-0.3.2/src/splox/_models.py +1074 -0
- splox-0.3.2/src/splox/_resources.py +42 -0
- splox-0.3.2/src/splox/_runs.py +714 -0
- splox-0.3.2/src/splox/_transport.py +501 -0
- splox-0.3.2/src/splox/_workflows.py +222 -0
- splox-0.3.2/src/splox/builder/__init__.py +542 -0
- splox-0.3.2/src/splox/evals/__init__.py +558 -0
- splox-0.3.2/src/splox/exceptions.py +229 -0
- splox-0.3.2/tests/integration_test.py +126 -0
- splox-0.3.2/tests/integration_test_mcp.py +124 -0
- splox-0.3.2/tests/integration_test_v2.py +315 -0
- splox-0.3.2/tests/test_builder.py +432 -0
- splox-0.3.2/tests/test_client.py +284 -0
- splox-0.3.2/tests/test_codemode_contract.py +204 -0
- splox-0.3.2/tests/test_discovery.py +194 -0
- splox-0.3.2/tests/test_evals.py +307 -0
- splox-0.3.2/tests/test_exceptions.py +92 -0
- splox-0.3.2/tests/test_ids.py +65 -0
- splox-0.3.2/tests/test_models.py +336 -0
- splox-0.3.2/tests/test_runs.py +637 -0
- splox-0.3.2/tests/test_sse.py +119 -0
- splox-0.3.2/tests/test_workflows.py +388 -0
- splox-0.3.0/PKG-INFO +0 -236
- splox-0.3.0/README.md +0 -203
- splox-0.3.0/src/splox/__init__.py +0 -76
- splox-0.3.0/src/splox/_models.py +0 -730
- splox-0.3.0/src/splox/_resources.py +0 -1029
- splox-0.3.0/src/splox/_transport.py +0 -220
- splox-0.3.0/src/splox/exceptions.py +0 -70
- splox-0.3.0/tests/integration_test.py +0 -322
- splox-0.3.0/tests/test_client.py +0 -434
- splox-0.3.0/tests/test_exceptions.py +0 -43
- splox-0.3.0/tests/test_models.py +0 -217
- splox-0.3.0/tests/test_sse.py +0 -65
- {splox-0.3.0 → splox-0.3.2}/.github/workflows/publish.yml +0 -0
- {splox-0.3.0 → splox-0.3.2}/.github/workflows/test.yml +0 -0
- {splox-0.3.0 → splox-0.3.2}/.gitignore +0 -0
- {splox-0.3.0 → splox-0.3.2}/LICENSE +0 -0
- {splox-0.3.0 → splox-0.3.2}/src/splox/py.typed +0 -0
- {splox-0.3.0 → splox-0.3.2}/tests/__init__.py +0 -0
splox-0.3.2/CHANGELOG.md
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.2.1 — v2 API (breaking)
|
|
4
|
+
|
|
5
|
+
The run-execution surface now targets the Splox Core API v2
|
|
6
|
+
(`/v2/runs`, `/v2/interactions`, RFC 9457 problem+json, cursor pagination,
|
|
7
|
+
durable SSE event journals). Workflow CRUD reads, chats, memory, billing,
|
|
8
|
+
webhooks, LLM and the **entire MCP module are unchanged**.
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- `client.runs` (`Runs` / `AsyncRuns`):
|
|
13
|
+
- `create(workflow_id, input, *, metadata=None, conversation_id=None, workflow_version=None, idempotency_key=None)`
|
|
14
|
+
— returns a bound `Run` handle; `input` accepts a `str` shorthand, a full
|
|
15
|
+
MessageInput dict, or a list of content parts; an `Idempotency-Key`
|
|
16
|
+
(UUIDv4) is generated automatically when omitted; `workflow_id` accepts both
|
|
17
|
+
`wf_...` public ids and raw workflow UUIDs.
|
|
18
|
+
- `get`, `list` (cursor pagination: `{data, page:{has_more, next_cursor}}`,
|
|
19
|
+
filters: `status`, `workflow_id`, `created_after`, `created_before`),
|
|
20
|
+
`cancel` (idempotent), `wait(timeout=, poll_interval=)`.
|
|
21
|
+
- `list_events` (JSON pages) and `stream_events` (SSE with automatic
|
|
22
|
+
`Last-Event-ID` reconnect, at-least-once dedupe by event id, terminates
|
|
23
|
+
after the terminal `run.status_changed` event).
|
|
24
|
+
- `messages`, `outputs`, `tree`, `usage`, `pending_interactions`.
|
|
25
|
+
- `Run` handle methods: `wait`, `cancel`, `events(stream=True|False)`,
|
|
26
|
+
`messages`, `outputs`, `tree`, `usage`, `pending_interactions`, `refresh`.
|
|
27
|
+
- `client.interactions` (`Interactions` / `AsyncInteractions`):
|
|
28
|
+
`list(status=, run_id=, ...)`, `get`, and
|
|
29
|
+
`respond(id, type=..., **fields)` / `respond(id, response={...})` with an
|
|
30
|
+
auto-generated idempotency key.
|
|
31
|
+
- ID helpers: `splox.encode_id(prefix, uuid)` / `splox.decode_id(public_id)`
|
|
32
|
+
(Crockford base32 / ULID alphabet).
|
|
33
|
+
- v2 models: `Run`, `RunFailure`, `RunEvent`, `Message`, `ContentPart`,
|
|
34
|
+
`Output`, `RunTree`, `RunTreeNode`, `RunUsage`, `Interaction`,
|
|
35
|
+
`InteractionResponse`, `PageInfo` and page wrappers (`RunPage`,
|
|
36
|
+
`RunEventPage`, `MessagePage`, `OutputPage`, `InteractionPage`), plus
|
|
37
|
+
`TERMINAL_RUN_STATUSES`.
|
|
38
|
+
- Exceptions: `SploxBadRequestError` (400), `SploxConflictError` (409),
|
|
39
|
+
`SploxValidationError` (422, carries `errors[]` with JSON Pointer names),
|
|
40
|
+
`SploxServerError` (5xx). All API errors now expose `code`, `trace_id` and
|
|
41
|
+
`problem` parsed from RFC 9457 bodies. `SploxRateLimitError.retry_after`
|
|
42
|
+
is populated from `Retry-After`.
|
|
43
|
+
- Transport retries: GETs are retried up to 3 times with exponential backoff
|
|
44
|
+
on connection errors / 429 / 5xx; POSTs are retried only when they carry an
|
|
45
|
+
`Idempotency-Key` header (same key on every attempt).
|
|
46
|
+
- v2 URL resolution: `/v2/...` paths resolve against the server origin
|
|
47
|
+
(base URL with a trailing `/api/v1` or `/v1` stripped), so the default
|
|
48
|
+
base URL keeps working for both API generations.
|
|
49
|
+
|
|
50
|
+
### Changed (BREAKING)
|
|
51
|
+
|
|
52
|
+
- **"Agent" was renamed to "workflow" across the v2 wire contract** and the
|
|
53
|
+
SDK follows: `runs.create(workflow_id=..., workflow_version=...)`,
|
|
54
|
+
`Run.workflow_id`, `runs.list(workflow_id=...)`, public id prefix `wf_`
|
|
55
|
+
(was `agt_`), error pointers `/workflow_id` / `/workflow_version`, and
|
|
56
|
+
codes `workflow_not_runnable` / `workflow_mismatch`.
|
|
57
|
+
- **`client.workflows` lost the run-execution surface.** Removed:
|
|
58
|
+
`workflows.run`, `workflows.listen`, `workflows.get_execution_tree`,
|
|
59
|
+
`workflows.get_history`, `workflows.stop`, `workflows.run_and_wait`
|
|
60
|
+
(sync and async). Replacement: `client.runs.create(...)`,
|
|
61
|
+
`run.events()` / `client.runs.stream_events(...)`, `client.runs.tree(...)`,
|
|
62
|
+
`client.runs.list(...)`, `client.runs.cancel(...)`,
|
|
63
|
+
`client.runs.create(...).wait(...)` respectively. Workflow CRUD reads
|
|
64
|
+
(`list`, `get`, `get_latest_version`, `list_versions`, `get_entry_nodes`)
|
|
65
|
+
and secrets management remain on v1 and are unchanged.
|
|
66
|
+
- **Removed models** (v1 run-execution): `RunResponse`,
|
|
67
|
+
`ExecutionTreeResponse`, `HistoryResponse`, `ExecutionTree`,
|
|
68
|
+
`ExecutionNode`, `ChildExecution`. The v2 equivalents are `Run`, `RunTree`,
|
|
69
|
+
`RunTreeNode` and `RunPage`. (`WorkflowRequest`, `NodeExecution` and
|
|
70
|
+
`SSEEvent` remain for the kept v1 chat streams.)
|
|
71
|
+
- **Removed: the `agents.*` surface entirely** (`client.agents`,
|
|
72
|
+
`Agents`/`AsyncAgents`, `AgentRun`/`AsyncAgentRun`, `AgentResult`/
|
|
73
|
+
`AgentGatherResult`, `AgentError`, `spawn`/`gather`/`stop`) — use
|
|
74
|
+
`client.runs.create(...)` + `run.wait()`/`runs.cancel()` instead; in-run
|
|
75
|
+
subagent spawning is a runtime concern (executor tool), not an SDK API.
|
|
76
|
+
`spawn`'s `SPLOX_RUN_ID`/`parent_run_id` mechanics are gone with it.
|
|
77
|
+
- **Run state names are the v2 state machine**: terminal is
|
|
78
|
+
`succeeded|failed|cancelled` (previously `completed|failed|stopped`);
|
|
79
|
+
cancellation passes through `cancelling`.
|
|
80
|
+
- **Timeouts**: `run.wait()` raises `SploxTimeoutError` (subclass of both
|
|
81
|
+
`SploxError` and `TimeoutError`), so existing `except TimeoutError` code
|
|
82
|
+
keeps working.
|
|
83
|
+
- **500-level responses** now raise `SploxServerError` (a `SploxAPIError`
|
|
84
|
+
subclass) instead of the generic `SploxAPIError`; 400 responses raise
|
|
85
|
+
`SploxBadRequestError`, 409 `SploxConflictError`, 422
|
|
86
|
+
`SploxValidationError`. Code that caught `SploxAPIError` keeps working.
|
|
87
|
+
- GET requests are now retried automatically (previously no retries); disable
|
|
88
|
+
by catching errors yourself — retry semantics never re-send an unkeyed POST.
|
|
89
|
+
|
|
90
|
+
### Unchanged (explicitly)
|
|
91
|
+
|
|
92
|
+
- `client.mcp` — catalog, connections, `execute_tool(mcp_server_id=, tool_slug=, args=)`,
|
|
93
|
+
server tools, OAuth/connection links, `generate_connection_token/link`.
|
|
94
|
+
- `client.chats`, `client.memory`, `client.billing`, `client.llm`,
|
|
95
|
+
`client.events` (webhooks), `SploxClient()`/`AsyncSploxClient()`
|
|
96
|
+
construction and the `SPLOX_API_KEY` / `SPLOX_BASE_URL` env fallbacks.
|
|
97
|
+
- Workflow provisioning reads and workflow secrets management.
|
|
98
|
+
|
|
99
|
+
## 0.0.18
|
|
100
|
+
|
|
101
|
+
- Last release of the v1 run-execution surface.
|
splox-0.3.2/PKG-INFO
ADDED
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: splox
|
|
3
|
+
Version: 0.3.2
|
|
4
|
+
Summary: Official Python SDK for the Splox API — run workflows, manage chats, and monitor execution
|
|
5
|
+
Project-URL: Homepage, https://splox.io
|
|
6
|
+
Project-URL: Documentation, https://docs.splox.io
|
|
7
|
+
Project-URL: Repository, https://github.com/splox-ai/python-sdk
|
|
8
|
+
Project-URL: Issues, https://github.com/splox-ai/python-sdk/issues
|
|
9
|
+
Author-email: Splox <support@splox.io>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: agent,ai,sdk,splox,workflow
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
|
+
Classifier: Typing :: Typed
|
|
24
|
+
Requires-Python: >=3.9
|
|
25
|
+
Requires-Dist: httpx>=0.25.0
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: mypy>=1.0; extra == 'dev'
|
|
28
|
+
Requires-Dist: pytest-asyncio>=0.21; extra == 'dev'
|
|
29
|
+
Requires-Dist: pytest-httpx>=0.30; extra == 'dev'
|
|
30
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
31
|
+
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
32
|
+
Description-Content-Type: text/markdown
|
|
33
|
+
|
|
34
|
+
# Splox Python SDK
|
|
35
|
+
|
|
36
|
+
Official Python SDK for the [Splox API](https://docs.splox.io) — create and observe workflow **runs**, resolve human-in-the-loop **interactions**, browse the MCP catalog, and manage workflows programmatically.
|
|
37
|
+
|
|
38
|
+
> **v0.1.0 is a breaking release.** The run-execution surface moved to the v2 API
|
|
39
|
+
> (`client.runs` / `client.interactions`). Workflow CRUD reads, chats, memory,
|
|
40
|
+
> billing and the **MCP module are unchanged**. See [CHANGELOG.md](CHANGELOG.md)
|
|
41
|
+
> for the full breaking-change list.
|
|
42
|
+
|
|
43
|
+
## Installation
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pip install splox
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Quick Start
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
from splox import SploxClient
|
|
53
|
+
|
|
54
|
+
client = SploxClient(api_key="your-api-key")
|
|
55
|
+
|
|
56
|
+
# Create a run: workflow id ("wf_...") or a raw workflow UUID both work.
|
|
57
|
+
run = client.runs.create(
|
|
58
|
+
"wf_01JAZ6Y5M3Q8F7N2R4T6V9W0XC",
|
|
59
|
+
"Summarize the latest sales report",
|
|
60
|
+
)
|
|
61
|
+
print(run.id, run.status) # run_01... queued
|
|
62
|
+
|
|
63
|
+
# Block until the run finishes (raises SploxTimeoutError on timeout).
|
|
64
|
+
run = run.wait(timeout=300)
|
|
65
|
+
print(run.status) # succeeded | failed | cancelled
|
|
66
|
+
|
|
67
|
+
# Read the transcript and outputs.
|
|
68
|
+
for message in run.messages().data:
|
|
69
|
+
print(f"[{message.role}] {message.text}")
|
|
70
|
+
for output in run.outputs().data:
|
|
71
|
+
print(output.type, output.value)
|
|
72
|
+
|
|
73
|
+
# Usage (includes descendant runs; amount is a decimal string).
|
|
74
|
+
usage = run.usage()
|
|
75
|
+
print(usage.input_tokens, usage.output_tokens, usage.amount, usage.currency)
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Creating runs
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
run = client.runs.create(
|
|
82
|
+
workflow_id, # "wf_..." or raw UUID
|
|
83
|
+
input, # str shorthand, or a full MessageInput dict,
|
|
84
|
+
# or a list of content parts
|
|
85
|
+
metadata={"ticket": "T-123"}, # optional client metadata
|
|
86
|
+
conversation_id="conv_...", # optional: continue a conversation (multi-turn)
|
|
87
|
+
workflow_version=3, # optional: pin a workflow version
|
|
88
|
+
idempotency_key="my-key", # optional: auto-generated UUIDv4 when omitted
|
|
89
|
+
)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
`input` accepts:
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
"What is 2+2?" # text shorthand
|
|
96
|
+
{"role": "user", "content": [{"type": "text", "text": "hi"}]} # full MessageInput
|
|
97
|
+
[{"type": "text", "text": "hi"}, {"type": "json", "value": {"x": 1}}] # parts
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Every `POST /v2/runs` carries an `Idempotency-Key` (auto UUIDv4). Retrying with
|
|
101
|
+
the same key and body returns the same run; the SDK only ever retries POSTs
|
|
102
|
+
that carry an idempotency key.
|
|
103
|
+
|
|
104
|
+
### Streaming events (SSE)
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
# Live event stream: auto-reconnects with Last-Event-ID, dedupes by event id,
|
|
108
|
+
# and ends after the terminal run.status_changed event.
|
|
109
|
+
for event in run.events(): # stream=True is the default
|
|
110
|
+
print(event.sequence, event.type, event.data)
|
|
111
|
+
|
|
112
|
+
# Durable JSON pages instead of a stream:
|
|
113
|
+
page = run.events(stream=False, limit=100)
|
|
114
|
+
for event in page.data:
|
|
115
|
+
print(event.sequence, event.type)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Listing and cancelling
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
page = client.runs.list(status=["running", "waiting"], limit=20)
|
|
122
|
+
for run in page.data:
|
|
123
|
+
print(run.id, run.status)
|
|
124
|
+
if page.page.has_more:
|
|
125
|
+
page = client.runs.list(status=["running", "waiting"], cursor=page.page.next_cursor)
|
|
126
|
+
|
|
127
|
+
run = client.runs.cancel(run_id) # idempotent; terminal runs are returned unchanged
|
|
128
|
+
tree = client.runs.tree(run_id) # run + descendants snapshot
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Human-in-the-loop interactions
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
# Pending questions raised by a run:
|
|
135
|
+
for interaction in run.pending_interactions():
|
|
136
|
+
print(interaction.type, interaction.prompt, interaction.payload)
|
|
137
|
+
|
|
138
|
+
# Respond (variant must match the interaction type):
|
|
139
|
+
client.interactions.respond(interaction.id, type="approval", approved=True)
|
|
140
|
+
client.interactions.respond(interaction.id, type="text", text="blue")
|
|
141
|
+
client.interactions.respond(interaction.id, type="choice", option_ids=["opt_a"])
|
|
142
|
+
client.interactions.respond(interaction.id, type="confirmation", confirmed=True)
|
|
143
|
+
# ...or pass a prebuilt body:
|
|
144
|
+
client.interactions.respond(interaction.id, response={"type": "approval", "approved": False})
|
|
145
|
+
|
|
146
|
+
# Inbox-style listing:
|
|
147
|
+
page = client.interactions.list(status="pending", limit=50)
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Public IDs
|
|
151
|
+
|
|
152
|
+
v2 IDs are `<prefix>_<26-char Crockford base32>` (`run_`, `wf_`, `int_`, ...).
|
|
153
|
+
The SDK accepts raw UUIDs for workflow ids and encodes them client-side:
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
from splox import encode_id, decode_id
|
|
157
|
+
|
|
158
|
+
encode_id("wf", "019f455e-a84c-7d4c-87b0-c951d38bc224") # -> "wf_01KX2NXA2C..."
|
|
159
|
+
decode_id("wf_01KX2NXA2CFN68FC69A79RQGH4") # -> UUID(...)
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## Async Support
|
|
163
|
+
|
|
164
|
+
Every resource has an async twin with the same shape:
|
|
165
|
+
|
|
166
|
+
```python
|
|
167
|
+
import asyncio
|
|
168
|
+
from splox import AsyncSploxClient
|
|
169
|
+
|
|
170
|
+
async def main():
|
|
171
|
+
async with AsyncSploxClient(api_key="your-api-key") as client:
|
|
172
|
+
run = await client.runs.create("wf_01JAZ6Y5M3Q8F7N2R4T6V9W0XC", "Hello!")
|
|
173
|
+
|
|
174
|
+
async for event in run.events(): # SSE with auto-reconnect
|
|
175
|
+
print(event.type, event.data)
|
|
176
|
+
|
|
177
|
+
run = await run.wait(timeout=300)
|
|
178
|
+
page = await run.messages()
|
|
179
|
+
print([m.text for m in page.data])
|
|
180
|
+
|
|
181
|
+
asyncio.run(main())
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
## Error Handling
|
|
185
|
+
|
|
186
|
+
All non-2xx v2 responses are RFC 9457 `problem+json` and map onto a typed
|
|
187
|
+
hierarchy; `code` carries the stable machine-readable error code and
|
|
188
|
+
`trace_id` correlates with server logs.
|
|
189
|
+
|
|
190
|
+
```python
|
|
191
|
+
from splox.exceptions import (
|
|
192
|
+
SploxAPIError, # base for HTTP errors (.status_code/.code/.trace_id/.problem)
|
|
193
|
+
SploxBadRequestError, # 400
|
|
194
|
+
SploxAuthError, # 401
|
|
195
|
+
SploxForbiddenError, # 403
|
|
196
|
+
SploxNotFoundError, # 404
|
|
197
|
+
SploxConflictError, # 409 (idempotency_key_conflict, interaction_not_pending, ...)
|
|
198
|
+
SploxGoneError, # 410 (event_cursor_expired, ...)
|
|
199
|
+
SploxValidationError, # 422 (.errors = [{name, reason, ...}] with JSON Pointers)
|
|
200
|
+
SploxRateLimitError, # 429 (.retry_after)
|
|
201
|
+
SploxServerError, # 5xx
|
|
202
|
+
SploxTimeoutError, # run.wait()/result() timeouts (also a TimeoutError)
|
|
203
|
+
SploxConnectionError, # network failures
|
|
204
|
+
SploxStreamError, # SSE stream gave up reconnecting
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
try:
|
|
208
|
+
run = client.runs.create(workflow_id, "hi")
|
|
209
|
+
except SploxValidationError as e:
|
|
210
|
+
for item in e.errors:
|
|
211
|
+
print(item["name"], item["reason"])
|
|
212
|
+
except SploxRateLimitError as e:
|
|
213
|
+
print(f"Rate limited. Retry after: {e.retry_after}")
|
|
214
|
+
except SploxAPIError as e:
|
|
215
|
+
print(f"API error {e.status_code} ({e.code}): {e.message}")
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
**Retries:** GET requests are retried up to 3 times with exponential backoff on
|
|
219
|
+
connection errors, 429 and 5xx. POSTs are retried only when they carry an
|
|
220
|
+
`Idempotency-Key` (always true for `runs.create` and `interactions.respond`),
|
|
221
|
+
reusing the same key so replays are safe.
|
|
222
|
+
|
|
223
|
+
## Discovery & building an agent
|
|
224
|
+
|
|
225
|
+
Three resource surfaces cover discovery: `client.llm_endpoints`
|
|
226
|
+
(`GET /v2/llm-endpoints`, `.models(id)`) and `client.tool_servers`
|
|
227
|
+
(`GET /v2/tool-servers`, `.tools(id)`). One endpoint carries
|
|
228
|
+
`is_default=True` — the endpoint new agent nodes get automatically; model
|
|
229
|
+
choice is equally optional (omit `text_llm_model` in node data → server
|
|
230
|
+
default).
|
|
231
|
+
|
|
232
|
+
```python
|
|
233
|
+
# 1. LLM endpoints: pick one (or rely on the default).
|
|
234
|
+
endpoints = client.llm_endpoints.list()
|
|
235
|
+
default_ep = next(e for e in endpoints if e.is_default)
|
|
236
|
+
model = client.llm_endpoints.models(default_ep.id)[0] # Model, all operations
|
|
237
|
+
# model.input_schema is the JSON Schema of the agent node's
|
|
238
|
+
# "additional_llm_config"; model.id goes into "text_llm_model".
|
|
239
|
+
|
|
240
|
+
# 2. Tool servers: system servers (kind="system") + your own (kind="user").
|
|
241
|
+
servers = client.tool_servers.list()
|
|
242
|
+
compute = next(s for s in servers if s.id == "system:compute")
|
|
243
|
+
tool_names = [t.name for t in client.tool_servers.tools(compute.id)]
|
|
244
|
+
|
|
245
|
+
# 3. Build a workflow: an agent wired to compute tools via a tool edge.
|
|
246
|
+
wf = client.workflows.create("researcher")
|
|
247
|
+
client.workflows.set_graph(
|
|
248
|
+
wf.id,
|
|
249
|
+
nodes=[
|
|
250
|
+
{"id": "agent", "type": "agent", "data": {
|
|
251
|
+
"system_prompt": "You are a research assistant.",
|
|
252
|
+
# optional — omit for server defaults:
|
|
253
|
+
"text_llm_endpoint_id": default_ep.id,
|
|
254
|
+
"text_llm_model": model.id,
|
|
255
|
+
}},
|
|
256
|
+
{"id": "tools", "type": "tool", "data": {
|
|
257
|
+
"mcp_server_id": compute.id, # "system:compute"
|
|
258
|
+
"allowed_tools": ["compute_exec", "compute_read_file"],
|
|
259
|
+
}},
|
|
260
|
+
],
|
|
261
|
+
edges=[{"source": "agent", "target": "tools"}], # tool edge (inferred)
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
run = client.runs.create(wf.id, "How many CPUs does this sandbox have?").wait()
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### `splox.builder` — declare a graph in Python
|
|
268
|
+
|
|
269
|
+
`splox.builder` turns nested `agent()` / `tool()` specs into one `upsert()`
|
|
270
|
+
call (idempotent by workflow name). Two pure helpers configure the agent
|
|
271
|
+
node's context knobs, mirroring the editor's "Rule-based Context" and
|
|
272
|
+
"Summarization" panels:
|
|
273
|
+
|
|
274
|
+
```python
|
|
275
|
+
from splox.builder import agent, tool, upsert, summarization, context_rule
|
|
276
|
+
|
|
277
|
+
root = agent(
|
|
278
|
+
"assistant",
|
|
279
|
+
"You help the user.",
|
|
280
|
+
tools=[tool("system:compute", allow=["compute_exec"])],
|
|
281
|
+
# Token-based memory: summarize trimmed history instead of dropping it.
|
|
282
|
+
context_memory=summarization(
|
|
283
|
+
"Summarize the conversation so far, keeping open questions.",
|
|
284
|
+
max_tokens=70000, trim_target_percent=30,
|
|
285
|
+
),
|
|
286
|
+
# Rule-based context: each rule fires only when its condition matches
|
|
287
|
+
# (keyword/regex are instant; intent asks the model).
|
|
288
|
+
context_providers=[
|
|
289
|
+
context_rule(text="Today is 2025-01-01.", keywords=["date", "today"]),
|
|
290
|
+
context_rule(
|
|
291
|
+
tool_server="system:compute", tool_name="compute_exec",
|
|
292
|
+
tool_args={"command": "uname -a"},
|
|
293
|
+
intent="the user asks about the machine",
|
|
294
|
+
),
|
|
295
|
+
],
|
|
296
|
+
)
|
|
297
|
+
res = upsert(client, "Assistant", root) # create or new version, one call
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
### `splox.evals` — grade a workflow in code
|
|
301
|
+
|
|
302
|
+
`splox.evals` runs scenario batches and lets you grade them with ordinary
|
|
303
|
+
Python: `run_cases()` fans out `k` attempts per input and folds each run's
|
|
304
|
+
output / tools / usage into a `RunResult`; `judge()` scores text with the
|
|
305
|
+
platform judge workflow; `compare()` diffs two result matrices (pass@k,
|
|
306
|
+
regressions, exact binomial p-value).
|
|
307
|
+
|
|
308
|
+
```python
|
|
309
|
+
from splox.evals import run_cases, judge, compare
|
|
310
|
+
|
|
311
|
+
runs = run_cases(client, res.workflow_id, inputs=["Run uname -sm"], k=3)
|
|
312
|
+
graded = [[r.status == "succeeded" and "compute_exec" in r.tools_used
|
|
313
|
+
for r in case] for case in runs]
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
## Workflow provisioning (v1, unchanged)
|
|
317
|
+
|
|
318
|
+
Workflow CRUD reads remain available for provisioning:
|
|
319
|
+
|
|
320
|
+
```python
|
|
321
|
+
workflows = client.workflows.list(search="Test")
|
|
322
|
+
full = client.workflows.get(workflow_id)
|
|
323
|
+
version = client.workflows.get_latest_version(workflow_id)
|
|
324
|
+
entry_nodes = client.workflows.get_entry_nodes(version.id)
|
|
325
|
+
versions = client.workflows.list_versions(workflow_id)
|
|
326
|
+
# plus workflow secrets management: list_secrets / set_env_secret / ...
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
Chats (`client.chats`), memory (`client.memory`), billing (`client.billing`)
|
|
330
|
+
and webhooks (`client.events`) are also unchanged.
|
|
331
|
+
|
|
332
|
+
## MCP (Model Context Protocol) — unchanged
|
|
333
|
+
|
|
334
|
+
The MCP module is fully supported and **unchanged in this release**: catalog,
|
|
335
|
+
connections, tool execution, OAuth and connection links work exactly as in 0.0.x.
|
|
336
|
+
|
|
337
|
+
### Catalog
|
|
338
|
+
|
|
339
|
+
```python
|
|
340
|
+
# Search the MCP catalog
|
|
341
|
+
catalog = client.mcp.list_catalog(search="github", per_page=10)
|
|
342
|
+
for server in catalog.mcp_servers:
|
|
343
|
+
print(f"{server.name} — {server.url}")
|
|
344
|
+
|
|
345
|
+
# Get featured servers
|
|
346
|
+
featured = client.mcp.list_catalog(featured=True)
|
|
347
|
+
|
|
348
|
+
# Get a single catalog item
|
|
349
|
+
item = client.mcp.get_catalog_item("mcp-server-id")
|
|
350
|
+
print(item.name, item.auth_type)
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
### Connections & tools
|
|
354
|
+
|
|
355
|
+
```python
|
|
356
|
+
conns = client.mcp.list_connections()
|
|
357
|
+
owner_servers = client.mcp.list_connections(scope="owner_user")
|
|
358
|
+
|
|
359
|
+
tools = client.mcp.get_server_tools("mcp-server-id")
|
|
360
|
+
|
|
361
|
+
result = client.mcp.execute_tool(
|
|
362
|
+
mcp_server_id="mcp-server-id",
|
|
363
|
+
tool_slug="list_servers",
|
|
364
|
+
args={"query": "x"},
|
|
365
|
+
)
|
|
366
|
+
print(result.result.content, result.result.structured_content, result.result.is_error)
|
|
367
|
+
|
|
368
|
+
client.mcp.delete_connection("connection-id")
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
### Connection Token & Link
|
|
372
|
+
|
|
373
|
+
```python
|
|
374
|
+
from splox import generate_connection_token, generate_connection_link
|
|
375
|
+
|
|
376
|
+
token = generate_connection_token(
|
|
377
|
+
mcp_server_id="mcp-server-id",
|
|
378
|
+
owner_user_id="owner-user-id",
|
|
379
|
+
end_user_id="end-user-id",
|
|
380
|
+
credentials_encryption_key="your-credentials-encryption-key",
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
link = generate_connection_link(
|
|
384
|
+
base_url="https://app.splox.io",
|
|
385
|
+
mcp_server_id="mcp-server-id",
|
|
386
|
+
owner_user_id="owner-user-id",
|
|
387
|
+
end_user_id="end-user-id",
|
|
388
|
+
credentials_encryption_key="your-credentials-encryption-key",
|
|
389
|
+
)
|
|
390
|
+
# → https://app.splox.io/tools/connect?token=eyJhbG...
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
## Webhooks
|
|
394
|
+
|
|
395
|
+
```python
|
|
396
|
+
from splox import SploxClient
|
|
397
|
+
|
|
398
|
+
client = SploxClient() # No API key needed for webhooks
|
|
399
|
+
|
|
400
|
+
result = client.events.send(
|
|
401
|
+
webhook_id="your-webhook-id",
|
|
402
|
+
payload={"order_id": "12345", "status": "paid"},
|
|
403
|
+
)
|
|
404
|
+
print(result.event_id)
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
## Custom Base URL
|
|
408
|
+
|
|
409
|
+
```python
|
|
410
|
+
client = SploxClient(
|
|
411
|
+
api_key="your-api-key",
|
|
412
|
+
base_url="https://your-self-hosted-instance.com/api/v1",
|
|
413
|
+
)
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
v1 endpoints use the base URL as-is; v2 endpoints (`/v2/...`) are resolved
|
|
417
|
+
against the server origin (the base URL with its `/api/v1` suffix stripped).
|
|
418
|
+
|
|
419
|
+
## API Reference
|
|
420
|
+
|
|
421
|
+
### `SploxClient` / `AsyncSploxClient`
|
|
422
|
+
|
|
423
|
+
| Parameter | Type | Default | Description |
|
|
424
|
+
|-----------|------|---------|-------------|
|
|
425
|
+
| `api_key` | `str \| None` | `SPLOX_API_KEY` env | API authentication token |
|
|
426
|
+
| `base_url` | `str` | `SPLOX_BASE_URL` env, then `https://splox.io/api/v1` | API base URL |
|
|
427
|
+
| `timeout` | `float` | `300.0` | Request timeout in seconds |
|
|
428
|
+
|
|
429
|
+
### `client.runs` (v2)
|
|
430
|
+
|
|
431
|
+
| Method | Description |
|
|
432
|
+
|--------|-------------|
|
|
433
|
+
| `create(workflow_id, input, *, metadata=, conversation_id=, workflow_version=, idempotency_key=)` | Create a run; returns a bound `Run` handle |
|
|
434
|
+
| `get(run_id)` | Get a run |
|
|
435
|
+
| `list(*, status=, workflow_id=, created_after=, created_before=, cursor=, limit=)` | Cursor-paged listing (newest first) |
|
|
436
|
+
| `cancel(run_id)` | Idempotent cancellation |
|
|
437
|
+
| `wait(run_id, *, timeout=, poll_interval=)` | Poll until terminal |
|
|
438
|
+
| `stream_events(run_id, *, cursor=)` | SSE stream with auto-reconnect + dedupe |
|
|
439
|
+
| `list_events(run_id, *, cursor=, limit=)` | Durable JSON event pages |
|
|
440
|
+
| `messages / outputs / tree / usage / pending_interactions` | Run reads |
|
|
441
|
+
|
|
442
|
+
`Run` handles expose the same operations instance-bound: `run.wait()`,
|
|
443
|
+
`run.cancel()`, `run.events()`, `run.messages()`, `run.outputs()`,
|
|
444
|
+
`run.tree()`, `run.usage()`, `run.pending_interactions()`, `run.refresh()`.
|
|
445
|
+
|
|
446
|
+
### `client.interactions` (v2)
|
|
447
|
+
|
|
448
|
+
| Method | Description |
|
|
449
|
+
|--------|-------------|
|
|
450
|
+
| `list(*, status=, run_id=, cursor=, limit=)` | Cursor-paged listing (newest first) |
|
|
451
|
+
| `get(interaction_id)` | Get an interaction |
|
|
452
|
+
| `respond(interaction_id, *, type=, ..., response=, idempotency_key=)` | Resolve a pending interaction |
|
|
453
|
+
|
|
454
|
+
### `client.workflows` (v1 provisioning reads)
|
|
455
|
+
|
|
456
|
+
| Method | Description |
|
|
457
|
+
|--------|-------------|
|
|
458
|
+
| `list(...)` / `get(id)` | List/get workflows |
|
|
459
|
+
| `get_latest_version(id)` / `list_versions(id)` | Version reads |
|
|
460
|
+
| `get_entry_nodes(version_id)` | Entry nodes for a version |
|
|
461
|
+
| `list_secrets / set_env_secret / set_file_secret / delete_secret / ...` | Secrets management |
|
|
462
|
+
|
|
463
|
+
### `client.chats`, `client.memory`, `client.billing`, `client.mcp`
|
|
464
|
+
|
|
465
|
+
Unchanged from 0.0.x — see the sections above.
|
|
466
|
+
|
|
467
|
+
## License
|
|
468
|
+
|
|
469
|
+
MIT
|