modulex-python 0.1.0__tar.gz → 1.0.0__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.
- {modulex_python-0.1.0 → modulex_python-1.0.0}/.gitignore +13 -1
- modulex_python-1.0.0/CHANGELOG.md +47 -0
- {modulex_python-0.1.0 → modulex_python-1.0.0}/PKG-INFO +90 -51
- {modulex_python-0.1.0 → modulex_python-1.0.0}/README.md +86 -49
- {modulex_python-0.1.0 → modulex_python-1.0.0}/pyproject.toml +11 -2
- {modulex_python-0.1.0 → modulex_python-1.0.0}/src/modulex/__init__.py +12 -2
- {modulex_python-0.1.0 → modulex_python-1.0.0}/src/modulex/_base.py +111 -16
- {modulex_python-0.1.0 → modulex_python-1.0.0}/src/modulex/_client.py +25 -13
- modulex_python-1.0.0/src/modulex/_exceptions.py +295 -0
- modulex_python-1.0.0/src/modulex/_streaming.py +148 -0
- modulex_python-1.0.0/src/modulex/_version.py +5 -0
- {modulex_python-0.1.0 → modulex_python-1.0.0}/src/modulex/resources/api_keys.py +16 -8
- modulex_python-1.0.0/src/modulex/resources/assistant.py +154 -0
- modulex_python-1.0.0/src/modulex/resources/auth.py +50 -0
- {modulex_python-0.1.0 → modulex_python-1.0.0}/src/modulex/resources/chats.py +31 -14
- modulex_python-1.0.0/src/modulex/resources/composer.py +241 -0
- modulex_python-1.0.0/src/modulex/resources/credentials.py +303 -0
- {modulex_python-0.1.0 → modulex_python-1.0.0}/src/modulex/resources/dashboard.py +36 -19
- {modulex_python-0.1.0 → modulex_python-1.0.0}/src/modulex/resources/deployments.py +46 -26
- modulex_python-1.0.0/src/modulex/resources/executions.py +207 -0
- {modulex_python-0.1.0 → modulex_python-1.0.0}/src/modulex/resources/integrations.py +36 -18
- {modulex_python-0.1.0 → modulex_python-1.0.0}/src/modulex/resources/knowledge.py +104 -62
- {modulex_python-0.1.0 → modulex_python-1.0.0}/src/modulex/resources/notifications.py +10 -4
- modulex_python-1.0.0/src/modulex/resources/organizations.py +166 -0
- {modulex_python-0.1.0 → modulex_python-1.0.0}/src/modulex/resources/schedules.py +63 -32
- modulex_python-1.0.0/src/modulex/resources/subscriptions.py +56 -0
- modulex_python-1.0.0/src/modulex/resources/system.py +30 -0
- {modulex_python-0.1.0 → modulex_python-1.0.0}/src/modulex/resources/workflows.py +53 -27
- modulex_python-1.0.0/src/modulex/types/__init__.py +558 -0
- modulex_python-1.0.0/src/modulex/types/_models.py +79 -0
- modulex_python-1.0.0/src/modulex/types/api_keys.py +66 -0
- modulex_python-1.0.0/src/modulex/types/assistant.py +104 -0
- modulex_python-1.0.0/src/modulex/types/auth.py +107 -0
- modulex_python-1.0.0/src/modulex/types/chats.py +83 -0
- modulex_python-1.0.0/src/modulex/types/composer.py +152 -0
- modulex_python-1.0.0/src/modulex/types/credentials.py +120 -0
- modulex_python-1.0.0/src/modulex/types/dashboard.py +255 -0
- modulex_python-1.0.0/src/modulex/types/deployments.py +126 -0
- modulex_python-1.0.0/src/modulex/types/executions.py +135 -0
- modulex_python-1.0.0/src/modulex/types/integrations.py +121 -0
- modulex_python-1.0.0/src/modulex/types/knowledge.py +168 -0
- modulex_python-1.0.0/src/modulex/types/notifications.py +77 -0
- modulex_python-1.0.0/src/modulex/types/organizations.py +147 -0
- modulex_python-1.0.0/src/modulex/types/realtime.py +270 -0
- modulex_python-1.0.0/src/modulex/types/schedules.py +101 -0
- modulex_python-1.0.0/src/modulex/types/subscriptions.py +114 -0
- modulex_python-1.0.0/src/modulex/types/system.py +62 -0
- modulex_python-1.0.0/src/modulex/types/workflows.py +361 -0
- modulex_python-0.1.0/CHANGELOG.md +0 -21
- modulex_python-0.1.0/src/modulex/_compat.py +0 -39
- modulex_python-0.1.0/src/modulex/_exceptions.py +0 -131
- modulex_python-0.1.0/src/modulex/_streaming.py +0 -118
- modulex_python-0.1.0/src/modulex/resources/auth.py +0 -38
- modulex_python-0.1.0/src/modulex/resources/composer.py +0 -134
- modulex_python-0.1.0/src/modulex/resources/credentials.py +0 -197
- modulex_python-0.1.0/src/modulex/resources/executions.py +0 -97
- modulex_python-0.1.0/src/modulex/resources/organizations.py +0 -72
- modulex_python-0.1.0/src/modulex/resources/subscriptions.py +0 -38
- modulex_python-0.1.0/src/modulex/resources/system.py +0 -28
- modulex_python-0.1.0/src/modulex/resources/templates.py +0 -115
- modulex_python-0.1.0/src/modulex/types/__init__.py +0 -294
- modulex_python-0.1.0/src/modulex/types/api_keys.py +0 -19
- modulex_python-0.1.0/src/modulex/types/auth.py +0 -62
- modulex_python-0.1.0/src/modulex/types/chats.py +0 -55
- modulex_python-0.1.0/src/modulex/types/composer.py +0 -27
- modulex_python-0.1.0/src/modulex/types/credentials.py +0 -79
- modulex_python-0.1.0/src/modulex/types/dashboard.py +0 -54
- modulex_python-0.1.0/src/modulex/types/executions.py +0 -104
- modulex_python-0.1.0/src/modulex/types/integrations.py +0 -29
- modulex_python-0.1.0/src/modulex/types/knowledge.py +0 -75
- modulex_python-0.1.0/src/modulex/types/notifications.py +0 -16
- modulex_python-0.1.0/src/modulex/types/organizations.py +0 -43
- modulex_python-0.1.0/src/modulex/types/schedules.py +0 -48
- modulex_python-0.1.0/src/modulex/types/subscriptions.py +0 -59
- modulex_python-0.1.0/src/modulex/types/templates.py +0 -50
- modulex_python-0.1.0/src/modulex/types/workflows.py +0 -253
- {modulex_python-0.1.0 → modulex_python-1.0.0}/CODE_OF_CONDUCT.md +0 -0
- {modulex_python-0.1.0 → modulex_python-1.0.0}/CONTRIBUTING.md +0 -0
- {modulex_python-0.1.0 → modulex_python-1.0.0}/LICENSE +0 -0
- {modulex_python-0.1.0 → modulex_python-1.0.0}/SECURITY.md +0 -0
- {modulex_python-0.1.0 → modulex_python-1.0.0}/src/modulex/_config.py +0 -0
- {modulex_python-0.1.0 → modulex_python-1.0.0}/src/modulex/py.typed +0 -0
- {modulex_python-0.1.0 → modulex_python-1.0.0}/src/modulex/resources/__init__.py +0 -0
- {modulex_python-0.1.0 → modulex_python-1.0.0}/src/modulex/types/shared.py +0 -0
|
@@ -27,4 +27,16 @@ coverage/
|
|
|
27
27
|
*~
|
|
28
28
|
PUBLISHING_GUIDE.md
|
|
29
29
|
SDK_PROMPT_PYTHON.md
|
|
30
|
-
SDK_REFERENCE.md
|
|
30
|
+
SDK_REFERENCE.md
|
|
31
|
+
|
|
32
|
+
# modulex-meta orchestration state (local-only for public repos)
|
|
33
|
+
.mxmeta/
|
|
34
|
+
.claude/
|
|
35
|
+
|
|
36
|
+
# local-only agent / audit / scratch files (must not be published)
|
|
37
|
+
CLAUDE.md
|
|
38
|
+
AGENTS.md
|
|
39
|
+
.cursor/
|
|
40
|
+
.agents/
|
|
41
|
+
big-refactor-audit/
|
|
42
|
+
.venv*/
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to the ModuleX Python SDK will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [1.0.0] - 2026-06-19
|
|
9
|
+
|
|
10
|
+
Major release realigning the SDK with the current platform API. **Breaking.**
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **`assistant` resource** — agentic standard chat (chat/get/list/listen/resume/cancel/status/delete) with HITL.
|
|
15
|
+
- **Composer HITL** — `composer.resume()`, `composer.set_focus()`, `composer.list()`; `composer.chat(llm=...)` now takes a provider-config dict (`ComposerLLMConfig`).
|
|
16
|
+
- **Execution history** — `executions.list_runs()`, `executions.iter_runs()` (typed `AsyncPage`), `executions.get_run()`; `executions.run(idempotency_key=...)` and `attribution_workflow_id`.
|
|
17
|
+
- **Credentials OAuth2** — `initiate_oauth2()`, `refresh_oauth2()`, and `create(oauth_config=...)`.
|
|
18
|
+
- **Organization settings** — `preview_invite()`, `get_settings()`, `set_llm_model_visibility()`, `set_composer_llm()`.
|
|
19
|
+
- **Structured billing errors** — `BillingError` + `PaymentRequiredError`/`QuotaExceededError`/`CreditExhaustedError`/`WalletError` (402/403/429) exposing `code`/`layer`/`key`/`current`/`limit`/`reason`; `RateLimitError` now carries `limit`/`remaining`/`reset`.
|
|
20
|
+
- **Typed responses** — every method returns a Pydantic v2 model (`ModulexModel`); dict-style access still works; unknown backend fields are preserved.
|
|
21
|
+
- Environment-variable config (`MODULEX_API_KEY`/`MODULEX_BASE_URL`/`MODULEX_ORGANIZATION_ID`), `User-Agent` header, `default_headers`, `Idempotency-Key` support.
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
|
|
25
|
+
- **SSE event dispatch fixed** — `event.event` is normalized from `data["type"]`, so workflow/composer/assistant streams dispatch correctly; terminal events stop iteration; heartbeats filtered.
|
|
26
|
+
- `_paginate` now supports page / offset (`has_next`|`has_more`|`total`) / cursor styles and nested `data.<items>` envelopes.
|
|
27
|
+
- All response types migrated from `TypedDict` to Pydantic v2 models, aligned field-by-field with the backend.
|
|
28
|
+
|
|
29
|
+
### Removed
|
|
30
|
+
|
|
31
|
+
- **`templates` resource** (removed from the platform).
|
|
32
|
+
- `system.metrics()` (endpoint no longer exists), composer workflow-`history()` (deprecated), `ApiKeyResponse.is_revoked`, the unused sync `_compat.run_sync` shim, and the removed `/workflows/run` `llm`/`knowledge_config` parameters.
|
|
33
|
+
|
|
34
|
+
## [0.1.0] - 2026-03-09
|
|
35
|
+
|
|
36
|
+
### Added
|
|
37
|
+
|
|
38
|
+
- Initial release of the ModuleX Python SDK
|
|
39
|
+
- Full coverage of all 125 ModuleX API endpoints
|
|
40
|
+
- Async-first client with `httpx`
|
|
41
|
+
- SSE streaming support for workflow execution, chat, and composer events
|
|
42
|
+
- Automatic retry with exponential backoff for transient errors
|
|
43
|
+
- Auto-pagination iterators for list endpoints
|
|
44
|
+
- File upload support for knowledge base documents
|
|
45
|
+
- Complete type definitions for all request/response schemas
|
|
46
|
+
- Exception hierarchy mapping all HTTP error codes
|
|
47
|
+
- Organization ID resolution (per-request override or client default)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: modulex-python
|
|
3
|
-
Version:
|
|
3
|
+
Version: 1.0.0
|
|
4
4
|
Summary: Official Python SDK for the ModuleX AI workflow orchestration platform
|
|
5
5
|
Project-URL: Homepage, https://modulex.dev
|
|
6
6
|
Project-URL: Documentation, https://docs.modulex.dev
|
|
@@ -11,7 +11,7 @@ Author-email: ModuleX <contact@modulex.dev>
|
|
|
11
11
|
License-Expression: MIT
|
|
12
12
|
License-File: LICENSE
|
|
13
13
|
Keywords: ai,modulex,orchestration,sdk,workflow
|
|
14
|
-
Classifier: Development Status ::
|
|
14
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
15
15
|
Classifier: Framework :: AsyncIO
|
|
16
16
|
Classifier: Intended Audience :: Developers
|
|
17
17
|
Classifier: License :: OSI Approved :: MIT License
|
|
@@ -25,6 +25,8 @@ Classifier: Typing :: Typed
|
|
|
25
25
|
Requires-Python: >=3.9
|
|
26
26
|
Requires-Dist: httpx-sse>=0.4
|
|
27
27
|
Requires-Dist: httpx>=0.27
|
|
28
|
+
Requires-Dist: pydantic>=2.7
|
|
29
|
+
Requires-Dist: typing-extensions>=4.10
|
|
28
30
|
Provides-Extra: dev
|
|
29
31
|
Requires-Dist: mypy>=1.13; extra == 'dev'
|
|
30
32
|
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
|
|
@@ -150,43 +152,46 @@ result = await client.executions.run(
|
|
|
150
152
|
workflow_id="workflow-uuid",
|
|
151
153
|
input={"messages": [{"role": "user", "content": "Hello!"}]},
|
|
152
154
|
)
|
|
155
|
+
print(result.run_id) # typed attribute access (responses are Pydantic models)
|
|
153
156
|
|
|
154
|
-
#
|
|
157
|
+
# Safely retry a run without double-execution
|
|
155
158
|
result = await client.executions.run(
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
"model_id": "gpt-4o-mini",
|
|
160
|
-
"temperature": 0.4,
|
|
161
|
-
},
|
|
162
|
-
input={"messages": [{"role": "user", "content": "Hello!"}]},
|
|
159
|
+
workflow_id="workflow-uuid",
|
|
160
|
+
input={"messages": [...]},
|
|
161
|
+
idempotency_key="order-4823", # stable key across retries
|
|
163
162
|
)
|
|
164
163
|
|
|
165
|
-
# Get execution state
|
|
164
|
+
# Get execution state / resume after interrupt / cancel
|
|
166
165
|
state = await client.executions.get_state(thread_id="thread-uuid")
|
|
167
|
-
|
|
168
|
-
# Resume after interrupt
|
|
169
|
-
await client.executions.resume(
|
|
170
|
-
thread_id="thread-uuid",
|
|
171
|
-
run_id="run-uuid",
|
|
172
|
-
resume_value="user input",
|
|
173
|
-
)
|
|
174
|
-
|
|
175
|
-
# Cancel execution
|
|
166
|
+
await client.executions.resume(thread_id="thread-uuid", run_id="run-uuid", resume_value="user input")
|
|
176
167
|
await client.executions.cancel(run_id="run-uuid", reason="No longer needed")
|
|
168
|
+
|
|
169
|
+
# Run history (workflow-runs)
|
|
170
|
+
runs = await client.executions.list_runs(workflow_id="workflow-uuid", limit=50)
|
|
171
|
+
async for run in client.executions.iter_runs(status="succeeded"): # auto-paginates
|
|
172
|
+
print(run.run_id, run.status)
|
|
173
|
+
detail = await client.executions.get_run(run_pk="run-row-id")
|
|
177
174
|
```
|
|
178
175
|
|
|
176
|
+
> Agentic ("direct LLM") chat moved off `/workflows/run` — use `client.assistant.chat(...)` instead.
|
|
177
|
+
|
|
179
178
|
### SSE Streaming
|
|
180
179
|
|
|
180
|
+
The backend carries the event type in the SSE `event:` field for `/chats/stream`, and in the JSON
|
|
181
|
+
`data["type"]` for workflow/composer/assistant streams. The SDK normalizes both, so `event.event`
|
|
182
|
+
always holds the logical type. Streams stop after a terminal event (`done`/`error`/`cancelled`/
|
|
183
|
+
`interrupted`) and heartbeats are filtered by default.
|
|
184
|
+
|
|
181
185
|
```python
|
|
182
186
|
# Listen to workflow execution events
|
|
183
187
|
async for event in client.executions.listen(run_id="run-uuid"):
|
|
184
188
|
if event.event == "node_update":
|
|
185
|
-
print(f"Node {event.data['
|
|
186
|
-
elif event.event == "
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
189
|
+
print(f"Node {event.data['node']}: {event.data.get('output')}")
|
|
190
|
+
elif event.event == "interrupt":
|
|
191
|
+
payload = event.data["data"] # InterruptEventData is nested under data["data"]
|
|
192
|
+
print(f"Needs input: {payload.get('message')}")
|
|
193
|
+
elif event.is_terminal:
|
|
194
|
+
print(f"Stream ended: {event.event}")
|
|
190
195
|
|
|
191
196
|
# Listen to chat list updates
|
|
192
197
|
async for event in client.chats.stream():
|
|
@@ -274,20 +279,6 @@ runs = await client.schedules.list_runs(schedule["id"])
|
|
|
274
279
|
stats = await client.schedules.run_stats(schedule["id"], days=30)
|
|
275
280
|
```
|
|
276
281
|
|
|
277
|
-
### Templates
|
|
278
|
-
|
|
279
|
-
```python
|
|
280
|
-
# Browse templates
|
|
281
|
-
templates = await client.templates.list()
|
|
282
|
-
|
|
283
|
-
# Use a template
|
|
284
|
-
result = await client.templates.use("template-id")
|
|
285
|
-
print(f"Created workflow: {result['workflow']['id']}")
|
|
286
|
-
|
|
287
|
-
# Like a template
|
|
288
|
-
await client.templates.like("template-id")
|
|
289
|
-
```
|
|
290
|
-
|
|
291
282
|
### Deployments
|
|
292
283
|
|
|
293
284
|
```python
|
|
@@ -306,23 +297,48 @@ await client.deployments.deactivate("workflow-uuid")
|
|
|
306
297
|
|
|
307
298
|
### Composer
|
|
308
299
|
|
|
300
|
+
`llm` is a provider config dict ({integration_name, provider_id, model_id, credential_id?}) — pass a
|
|
301
|
+
`ComposerLLMConfig` or an equivalent dict.
|
|
302
|
+
|
|
309
303
|
```python
|
|
310
|
-
|
|
304
|
+
from modulex.types import ComposerLLMConfig, YesNoResponse, user_input_request_from_event
|
|
305
|
+
|
|
311
306
|
result = await client.composer.chat(
|
|
312
307
|
message="Add an LLM node that summarizes the input",
|
|
313
308
|
workflow_id="workflow-uuid",
|
|
314
|
-
llm=
|
|
309
|
+
llm=ComposerLLMConfig(integration_name="anthropic", provider_id="anthropic", model_id="claude-sonnet-4-20250514"),
|
|
315
310
|
)
|
|
316
311
|
|
|
317
|
-
# Listen
|
|
318
|
-
async for event in client.composer.listen(result
|
|
319
|
-
if event.event == "
|
|
320
|
-
|
|
321
|
-
|
|
312
|
+
# Listen, and answer a human-in-the-loop question (HITL) when the run pauses
|
|
313
|
+
async for event in client.composer.listen(result.composer_chat_id, result.run_id):
|
|
314
|
+
if event.event == "user_input_request":
|
|
315
|
+
question = user_input_request_from_event(event.data) # typed UserInputRequest
|
|
316
|
+
await client.composer.resume(
|
|
317
|
+
result.composer_chat_id,
|
|
318
|
+
request_id=question.request_id,
|
|
319
|
+
response=YesNoResponse(answer=True),
|
|
320
|
+
llm={"integration_name": "anthropic", "provider_id": "anthropic", "model_id": "claude-sonnet-4-20250514"},
|
|
321
|
+
) # returns a NEW run_id — re-subscribe with listen() on it
|
|
322
|
+
elif event.is_terminal:
|
|
322
323
|
break
|
|
323
324
|
|
|
324
|
-
|
|
325
|
-
await client.composer.save(result
|
|
325
|
+
chats = await client.composer.list(limit=20) # cursor-paginated
|
|
326
|
+
await client.composer.save(result.composer_chat_id) # or .revert(...)
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
### Assistant (agentic chat)
|
|
330
|
+
|
|
331
|
+
Shares the HITL contract with the composer. All endpoints are available to any org member.
|
|
332
|
+
|
|
333
|
+
```python
|
|
334
|
+
result = await client.assistant.chat("Summarize my latest runs", llm=ComposerLLMConfig(
|
|
335
|
+
integration_name="openai", provider_id="openai", model_id="gpt-4o-mini",
|
|
336
|
+
))
|
|
337
|
+
async for event in client.assistant.listen(result.chat_id, result.run_id):
|
|
338
|
+
if event.event == "response_chunk":
|
|
339
|
+
print(event.data.get("data", {}).get("text", ""), end="")
|
|
340
|
+
elif event.is_terminal:
|
|
341
|
+
break
|
|
326
342
|
```
|
|
327
343
|
|
|
328
344
|
### Other Resources
|
|
@@ -375,7 +391,7 @@ try:
|
|
|
375
391
|
except NotFoundError:
|
|
376
392
|
print("Workflow not found")
|
|
377
393
|
except RateLimitError as e:
|
|
378
|
-
print(f"Rate limited. Retry after {e.retry_after}s")
|
|
394
|
+
print(f"Rate limited. Retry after {e.retry_after}s (limit={e.limit}, remaining={e.remaining})")
|
|
379
395
|
except AuthenticationError:
|
|
380
396
|
print("Invalid API key")
|
|
381
397
|
except ValidationError as e:
|
|
@@ -384,6 +400,20 @@ except ModulexError as e:
|
|
|
384
400
|
print(f"API error ({e.status_code}): {e.message}")
|
|
385
401
|
```
|
|
386
402
|
|
|
403
|
+
Usage/billing denials (quota, credit, wallet) are surfaced structurally via `BillingError` and its
|
|
404
|
+
subclasses, which expose `code`, `layer`, `key`, `current`, `limit`, and `reason`:
|
|
405
|
+
|
|
406
|
+
```python
|
|
407
|
+
from modulex import BillingError, CreditExhaustedError
|
|
408
|
+
|
|
409
|
+
try:
|
|
410
|
+
await client.executions.run(workflow_id="wf")
|
|
411
|
+
except CreditExhaustedError as e: # 402, layer="credit"
|
|
412
|
+
print(f"Out of credits: {e.current}/{e.limit}")
|
|
413
|
+
except BillingError as e: # any quota/credit/wallet denial
|
|
414
|
+
print(f"Denied ({e.layer}/{e.code}): {e.reason}")
|
|
415
|
+
```
|
|
416
|
+
|
|
387
417
|
### Exception Hierarchy
|
|
388
418
|
|
|
389
419
|
| Exception | HTTP Status | Description |
|
|
@@ -391,6 +421,7 @@ except ModulexError as e:
|
|
|
391
421
|
| `ModulexError` | — | Base exception |
|
|
392
422
|
| `BadRequestError` | 400 | Malformed request |
|
|
393
423
|
| `AuthenticationError` | 401 | Invalid/missing auth |
|
|
424
|
+
| `PaymentRequiredError` | 402 | Payment required (billing) |
|
|
394
425
|
| `PermissionError` | 403 | Insufficient permissions |
|
|
395
426
|
| `NotFoundError` | 404 | Resource not found |
|
|
396
427
|
| `ConflictError` | 409 | Resource conflict |
|
|
@@ -399,21 +430,29 @@ except ModulexError as e:
|
|
|
399
430
|
| `InternalError` | 500 | Server error |
|
|
400
431
|
| `ExternalServiceError` | 502 | External service failure |
|
|
401
432
|
| `ServiceUnavailableError` | 503 | Service unavailable |
|
|
433
|
+
| `BillingError` | 402/403/429 | Usage denial (base) — `code`/`layer`/`reason` |
|
|
434
|
+
| `QuotaExceededError` | 403 | Quota exceeded (`layer="quota"`) |
|
|
435
|
+
| `CreditExhaustedError` | 402 | Credit plan exhausted (`layer="credit"`) |
|
|
436
|
+
| `WalletError` | 402 | Wallet overage denied (`layer="wallet"`) |
|
|
402
437
|
| `StreamError` | — | SSE stream error |
|
|
403
438
|
| `TimeoutError` | — | Request timeout |
|
|
404
439
|
|
|
405
440
|
## Type Hints
|
|
406
441
|
|
|
407
|
-
|
|
442
|
+
Responses are **Pydantic v2 models** — use typed attribute access (`result.id`) or, for
|
|
443
|
+
compatibility, dict-style access (`result["id"]`). Unknown fields the backend may add are preserved.
|
|
444
|
+
All models are importable:
|
|
408
445
|
|
|
409
446
|
```python
|
|
447
|
+
from modulex import SSEEvent
|
|
410
448
|
from modulex.types import (
|
|
411
449
|
WorkflowDefinition,
|
|
412
450
|
NodeDefinition,
|
|
413
451
|
EdgeDefinition,
|
|
414
452
|
LLMConfig,
|
|
415
453
|
RunResponse,
|
|
416
|
-
|
|
454
|
+
AsyncPage, # typed auto-pagination (e.g. executions.iter_runs)
|
|
455
|
+
ModulexModel, # base class for all response models
|
|
417
456
|
)
|
|
418
457
|
```
|
|
419
458
|
|
|
@@ -113,43 +113,46 @@ result = await client.executions.run(
|
|
|
113
113
|
workflow_id="workflow-uuid",
|
|
114
114
|
input={"messages": [{"role": "user", "content": "Hello!"}]},
|
|
115
115
|
)
|
|
116
|
+
print(result.run_id) # typed attribute access (responses are Pydantic models)
|
|
116
117
|
|
|
117
|
-
#
|
|
118
|
+
# Safely retry a run without double-execution
|
|
118
119
|
result = await client.executions.run(
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
"model_id": "gpt-4o-mini",
|
|
123
|
-
"temperature": 0.4,
|
|
124
|
-
},
|
|
125
|
-
input={"messages": [{"role": "user", "content": "Hello!"}]},
|
|
120
|
+
workflow_id="workflow-uuid",
|
|
121
|
+
input={"messages": [...]},
|
|
122
|
+
idempotency_key="order-4823", # stable key across retries
|
|
126
123
|
)
|
|
127
124
|
|
|
128
|
-
# Get execution state
|
|
125
|
+
# Get execution state / resume after interrupt / cancel
|
|
129
126
|
state = await client.executions.get_state(thread_id="thread-uuid")
|
|
130
|
-
|
|
131
|
-
# Resume after interrupt
|
|
132
|
-
await client.executions.resume(
|
|
133
|
-
thread_id="thread-uuid",
|
|
134
|
-
run_id="run-uuid",
|
|
135
|
-
resume_value="user input",
|
|
136
|
-
)
|
|
137
|
-
|
|
138
|
-
# Cancel execution
|
|
127
|
+
await client.executions.resume(thread_id="thread-uuid", run_id="run-uuid", resume_value="user input")
|
|
139
128
|
await client.executions.cancel(run_id="run-uuid", reason="No longer needed")
|
|
129
|
+
|
|
130
|
+
# Run history (workflow-runs)
|
|
131
|
+
runs = await client.executions.list_runs(workflow_id="workflow-uuid", limit=50)
|
|
132
|
+
async for run in client.executions.iter_runs(status="succeeded"): # auto-paginates
|
|
133
|
+
print(run.run_id, run.status)
|
|
134
|
+
detail = await client.executions.get_run(run_pk="run-row-id")
|
|
140
135
|
```
|
|
141
136
|
|
|
137
|
+
> Agentic ("direct LLM") chat moved off `/workflows/run` — use `client.assistant.chat(...)` instead.
|
|
138
|
+
|
|
142
139
|
### SSE Streaming
|
|
143
140
|
|
|
141
|
+
The backend carries the event type in the SSE `event:` field for `/chats/stream`, and in the JSON
|
|
142
|
+
`data["type"]` for workflow/composer/assistant streams. The SDK normalizes both, so `event.event`
|
|
143
|
+
always holds the logical type. Streams stop after a terminal event (`done`/`error`/`cancelled`/
|
|
144
|
+
`interrupted`) and heartbeats are filtered by default.
|
|
145
|
+
|
|
144
146
|
```python
|
|
145
147
|
# Listen to workflow execution events
|
|
146
148
|
async for event in client.executions.listen(run_id="run-uuid"):
|
|
147
149
|
if event.event == "node_update":
|
|
148
|
-
print(f"Node {event.data['
|
|
149
|
-
elif event.event == "
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
150
|
+
print(f"Node {event.data['node']}: {event.data.get('output')}")
|
|
151
|
+
elif event.event == "interrupt":
|
|
152
|
+
payload = event.data["data"] # InterruptEventData is nested under data["data"]
|
|
153
|
+
print(f"Needs input: {payload.get('message')}")
|
|
154
|
+
elif event.is_terminal:
|
|
155
|
+
print(f"Stream ended: {event.event}")
|
|
153
156
|
|
|
154
157
|
# Listen to chat list updates
|
|
155
158
|
async for event in client.chats.stream():
|
|
@@ -237,20 +240,6 @@ runs = await client.schedules.list_runs(schedule["id"])
|
|
|
237
240
|
stats = await client.schedules.run_stats(schedule["id"], days=30)
|
|
238
241
|
```
|
|
239
242
|
|
|
240
|
-
### Templates
|
|
241
|
-
|
|
242
|
-
```python
|
|
243
|
-
# Browse templates
|
|
244
|
-
templates = await client.templates.list()
|
|
245
|
-
|
|
246
|
-
# Use a template
|
|
247
|
-
result = await client.templates.use("template-id")
|
|
248
|
-
print(f"Created workflow: {result['workflow']['id']}")
|
|
249
|
-
|
|
250
|
-
# Like a template
|
|
251
|
-
await client.templates.like("template-id")
|
|
252
|
-
```
|
|
253
|
-
|
|
254
243
|
### Deployments
|
|
255
244
|
|
|
256
245
|
```python
|
|
@@ -269,23 +258,48 @@ await client.deployments.deactivate("workflow-uuid")
|
|
|
269
258
|
|
|
270
259
|
### Composer
|
|
271
260
|
|
|
261
|
+
`llm` is a provider config dict ({integration_name, provider_id, model_id, credential_id?}) — pass a
|
|
262
|
+
`ComposerLLMConfig` or an equivalent dict.
|
|
263
|
+
|
|
272
264
|
```python
|
|
273
|
-
|
|
265
|
+
from modulex.types import ComposerLLMConfig, YesNoResponse, user_input_request_from_event
|
|
266
|
+
|
|
274
267
|
result = await client.composer.chat(
|
|
275
268
|
message="Add an LLM node that summarizes the input",
|
|
276
269
|
workflow_id="workflow-uuid",
|
|
277
|
-
llm=
|
|
270
|
+
llm=ComposerLLMConfig(integration_name="anthropic", provider_id="anthropic", model_id="claude-sonnet-4-20250514"),
|
|
278
271
|
)
|
|
279
272
|
|
|
280
|
-
# Listen
|
|
281
|
-
async for event in client.composer.listen(result
|
|
282
|
-
if event.event == "
|
|
283
|
-
|
|
284
|
-
|
|
273
|
+
# Listen, and answer a human-in-the-loop question (HITL) when the run pauses
|
|
274
|
+
async for event in client.composer.listen(result.composer_chat_id, result.run_id):
|
|
275
|
+
if event.event == "user_input_request":
|
|
276
|
+
question = user_input_request_from_event(event.data) # typed UserInputRequest
|
|
277
|
+
await client.composer.resume(
|
|
278
|
+
result.composer_chat_id,
|
|
279
|
+
request_id=question.request_id,
|
|
280
|
+
response=YesNoResponse(answer=True),
|
|
281
|
+
llm={"integration_name": "anthropic", "provider_id": "anthropic", "model_id": "claude-sonnet-4-20250514"},
|
|
282
|
+
) # returns a NEW run_id — re-subscribe with listen() on it
|
|
283
|
+
elif event.is_terminal:
|
|
285
284
|
break
|
|
286
285
|
|
|
287
|
-
|
|
288
|
-
await client.composer.save(result
|
|
286
|
+
chats = await client.composer.list(limit=20) # cursor-paginated
|
|
287
|
+
await client.composer.save(result.composer_chat_id) # or .revert(...)
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
### Assistant (agentic chat)
|
|
291
|
+
|
|
292
|
+
Shares the HITL contract with the composer. All endpoints are available to any org member.
|
|
293
|
+
|
|
294
|
+
```python
|
|
295
|
+
result = await client.assistant.chat("Summarize my latest runs", llm=ComposerLLMConfig(
|
|
296
|
+
integration_name="openai", provider_id="openai", model_id="gpt-4o-mini",
|
|
297
|
+
))
|
|
298
|
+
async for event in client.assistant.listen(result.chat_id, result.run_id):
|
|
299
|
+
if event.event == "response_chunk":
|
|
300
|
+
print(event.data.get("data", {}).get("text", ""), end="")
|
|
301
|
+
elif event.is_terminal:
|
|
302
|
+
break
|
|
289
303
|
```
|
|
290
304
|
|
|
291
305
|
### Other Resources
|
|
@@ -338,7 +352,7 @@ try:
|
|
|
338
352
|
except NotFoundError:
|
|
339
353
|
print("Workflow not found")
|
|
340
354
|
except RateLimitError as e:
|
|
341
|
-
print(f"Rate limited. Retry after {e.retry_after}s")
|
|
355
|
+
print(f"Rate limited. Retry after {e.retry_after}s (limit={e.limit}, remaining={e.remaining})")
|
|
342
356
|
except AuthenticationError:
|
|
343
357
|
print("Invalid API key")
|
|
344
358
|
except ValidationError as e:
|
|
@@ -347,6 +361,20 @@ except ModulexError as e:
|
|
|
347
361
|
print(f"API error ({e.status_code}): {e.message}")
|
|
348
362
|
```
|
|
349
363
|
|
|
364
|
+
Usage/billing denials (quota, credit, wallet) are surfaced structurally via `BillingError` and its
|
|
365
|
+
subclasses, which expose `code`, `layer`, `key`, `current`, `limit`, and `reason`:
|
|
366
|
+
|
|
367
|
+
```python
|
|
368
|
+
from modulex import BillingError, CreditExhaustedError
|
|
369
|
+
|
|
370
|
+
try:
|
|
371
|
+
await client.executions.run(workflow_id="wf")
|
|
372
|
+
except CreditExhaustedError as e: # 402, layer="credit"
|
|
373
|
+
print(f"Out of credits: {e.current}/{e.limit}")
|
|
374
|
+
except BillingError as e: # any quota/credit/wallet denial
|
|
375
|
+
print(f"Denied ({e.layer}/{e.code}): {e.reason}")
|
|
376
|
+
```
|
|
377
|
+
|
|
350
378
|
### Exception Hierarchy
|
|
351
379
|
|
|
352
380
|
| Exception | HTTP Status | Description |
|
|
@@ -354,6 +382,7 @@ except ModulexError as e:
|
|
|
354
382
|
| `ModulexError` | — | Base exception |
|
|
355
383
|
| `BadRequestError` | 400 | Malformed request |
|
|
356
384
|
| `AuthenticationError` | 401 | Invalid/missing auth |
|
|
385
|
+
| `PaymentRequiredError` | 402 | Payment required (billing) |
|
|
357
386
|
| `PermissionError` | 403 | Insufficient permissions |
|
|
358
387
|
| `NotFoundError` | 404 | Resource not found |
|
|
359
388
|
| `ConflictError` | 409 | Resource conflict |
|
|
@@ -362,21 +391,29 @@ except ModulexError as e:
|
|
|
362
391
|
| `InternalError` | 500 | Server error |
|
|
363
392
|
| `ExternalServiceError` | 502 | External service failure |
|
|
364
393
|
| `ServiceUnavailableError` | 503 | Service unavailable |
|
|
394
|
+
| `BillingError` | 402/403/429 | Usage denial (base) — `code`/`layer`/`reason` |
|
|
395
|
+
| `QuotaExceededError` | 403 | Quota exceeded (`layer="quota"`) |
|
|
396
|
+
| `CreditExhaustedError` | 402 | Credit plan exhausted (`layer="credit"`) |
|
|
397
|
+
| `WalletError` | 402 | Wallet overage denied (`layer="wallet"`) |
|
|
365
398
|
| `StreamError` | — | SSE stream error |
|
|
366
399
|
| `TimeoutError` | — | Request timeout |
|
|
367
400
|
|
|
368
401
|
## Type Hints
|
|
369
402
|
|
|
370
|
-
|
|
403
|
+
Responses are **Pydantic v2 models** — use typed attribute access (`result.id`) or, for
|
|
404
|
+
compatibility, dict-style access (`result["id"]`). Unknown fields the backend may add are preserved.
|
|
405
|
+
All models are importable:
|
|
371
406
|
|
|
372
407
|
```python
|
|
408
|
+
from modulex import SSEEvent
|
|
373
409
|
from modulex.types import (
|
|
374
410
|
WorkflowDefinition,
|
|
375
411
|
NodeDefinition,
|
|
376
412
|
EdgeDefinition,
|
|
377
413
|
LLMConfig,
|
|
378
414
|
RunResponse,
|
|
379
|
-
|
|
415
|
+
AsyncPage, # typed auto-pagination (e.g. executions.iter_runs)
|
|
416
|
+
ModulexModel, # base class for all response models
|
|
380
417
|
)
|
|
381
418
|
```
|
|
382
419
|
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "modulex-python"
|
|
7
|
-
version = "
|
|
7
|
+
version = "1.0.0"
|
|
8
8
|
description = "Official Python SDK for the ModuleX AI workflow orchestration platform"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -14,7 +14,7 @@ authors = [
|
|
|
14
14
|
]
|
|
15
15
|
keywords = ["modulex", "ai", "workflow", "orchestration", "sdk"]
|
|
16
16
|
classifiers = [
|
|
17
|
-
"Development Status ::
|
|
17
|
+
"Development Status :: 5 - Production/Stable",
|
|
18
18
|
"Intended Audience :: Developers",
|
|
19
19
|
"License :: OSI Approved :: MIT License",
|
|
20
20
|
"Programming Language :: Python :: 3",
|
|
@@ -29,6 +29,8 @@ classifiers = [
|
|
|
29
29
|
dependencies = [
|
|
30
30
|
"httpx>=0.27",
|
|
31
31
|
"httpx-sse>=0.4",
|
|
32
|
+
"pydantic>=2.7",
|
|
33
|
+
"typing-extensions>=4.10",
|
|
32
34
|
]
|
|
33
35
|
|
|
34
36
|
[project.optional-dependencies]
|
|
@@ -68,6 +70,13 @@ line-length = 120
|
|
|
68
70
|
[tool.ruff.lint]
|
|
69
71
|
select = ["E", "F", "I", "N", "W", "UP"]
|
|
70
72
|
|
|
73
|
+
[tool.ruff.lint.per-file-ignores]
|
|
74
|
+
# Pydantic models evaluate their annotations at runtime to build fields. On
|
|
75
|
+
# Python 3.9 the PEP 604 `X | None` syntax raises TypeError when eval'd, so the
|
|
76
|
+
# response-model files must keep explicit Optional/Union — exempt the whole
|
|
77
|
+
# types/ package from the UP (pyupgrade) modernizers.
|
|
78
|
+
"src/modulex/types/*.py" = ["UP"]
|
|
79
|
+
|
|
71
80
|
[tool.mypy]
|
|
72
81
|
python_version = "3.9"
|
|
73
82
|
strict = true
|
|
@@ -4,19 +4,25 @@ from modulex._client import Modulex
|
|
|
4
4
|
from modulex._exceptions import (
|
|
5
5
|
AuthenticationError,
|
|
6
6
|
BadRequestError,
|
|
7
|
+
BillingError,
|
|
7
8
|
ConflictError,
|
|
9
|
+
CreditExhaustedError,
|
|
8
10
|
ExternalServiceError,
|
|
9
11
|
InternalError,
|
|
10
12
|
ModulexError,
|
|
11
13
|
NotFoundError,
|
|
14
|
+
PaymentRequiredError,
|
|
12
15
|
PermissionError,
|
|
16
|
+
QuotaExceededError,
|
|
13
17
|
RateLimitError,
|
|
14
18
|
ServiceUnavailableError,
|
|
15
19
|
StreamError,
|
|
16
20
|
TimeoutError,
|
|
17
21
|
ValidationError,
|
|
22
|
+
WalletError,
|
|
18
23
|
)
|
|
19
24
|
from modulex._streaming import SSEEvent
|
|
25
|
+
from modulex._version import __version__
|
|
20
26
|
|
|
21
27
|
__all__ = [
|
|
22
28
|
"Modulex",
|
|
@@ -33,7 +39,11 @@ __all__ = [
|
|
|
33
39
|
"ServiceUnavailableError",
|
|
34
40
|
"StreamError",
|
|
35
41
|
"TimeoutError",
|
|
42
|
+
"BillingError",
|
|
43
|
+
"PaymentRequiredError",
|
|
44
|
+
"QuotaExceededError",
|
|
45
|
+
"CreditExhaustedError",
|
|
46
|
+
"WalletError",
|
|
36
47
|
"SSEEvent",
|
|
48
|
+
"__version__",
|
|
37
49
|
]
|
|
38
|
-
|
|
39
|
-
__version__ = "0.1.0"
|