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.
Files changed (84) hide show
  1. {modulex_python-0.1.0 → modulex_python-1.0.0}/.gitignore +13 -1
  2. modulex_python-1.0.0/CHANGELOG.md +47 -0
  3. {modulex_python-0.1.0 → modulex_python-1.0.0}/PKG-INFO +90 -51
  4. {modulex_python-0.1.0 → modulex_python-1.0.0}/README.md +86 -49
  5. {modulex_python-0.1.0 → modulex_python-1.0.0}/pyproject.toml +11 -2
  6. {modulex_python-0.1.0 → modulex_python-1.0.0}/src/modulex/__init__.py +12 -2
  7. {modulex_python-0.1.0 → modulex_python-1.0.0}/src/modulex/_base.py +111 -16
  8. {modulex_python-0.1.0 → modulex_python-1.0.0}/src/modulex/_client.py +25 -13
  9. modulex_python-1.0.0/src/modulex/_exceptions.py +295 -0
  10. modulex_python-1.0.0/src/modulex/_streaming.py +148 -0
  11. modulex_python-1.0.0/src/modulex/_version.py +5 -0
  12. {modulex_python-0.1.0 → modulex_python-1.0.0}/src/modulex/resources/api_keys.py +16 -8
  13. modulex_python-1.0.0/src/modulex/resources/assistant.py +154 -0
  14. modulex_python-1.0.0/src/modulex/resources/auth.py +50 -0
  15. {modulex_python-0.1.0 → modulex_python-1.0.0}/src/modulex/resources/chats.py +31 -14
  16. modulex_python-1.0.0/src/modulex/resources/composer.py +241 -0
  17. modulex_python-1.0.0/src/modulex/resources/credentials.py +303 -0
  18. {modulex_python-0.1.0 → modulex_python-1.0.0}/src/modulex/resources/dashboard.py +36 -19
  19. {modulex_python-0.1.0 → modulex_python-1.0.0}/src/modulex/resources/deployments.py +46 -26
  20. modulex_python-1.0.0/src/modulex/resources/executions.py +207 -0
  21. {modulex_python-0.1.0 → modulex_python-1.0.0}/src/modulex/resources/integrations.py +36 -18
  22. {modulex_python-0.1.0 → modulex_python-1.0.0}/src/modulex/resources/knowledge.py +104 -62
  23. {modulex_python-0.1.0 → modulex_python-1.0.0}/src/modulex/resources/notifications.py +10 -4
  24. modulex_python-1.0.0/src/modulex/resources/organizations.py +166 -0
  25. {modulex_python-0.1.0 → modulex_python-1.0.0}/src/modulex/resources/schedules.py +63 -32
  26. modulex_python-1.0.0/src/modulex/resources/subscriptions.py +56 -0
  27. modulex_python-1.0.0/src/modulex/resources/system.py +30 -0
  28. {modulex_python-0.1.0 → modulex_python-1.0.0}/src/modulex/resources/workflows.py +53 -27
  29. modulex_python-1.0.0/src/modulex/types/__init__.py +558 -0
  30. modulex_python-1.0.0/src/modulex/types/_models.py +79 -0
  31. modulex_python-1.0.0/src/modulex/types/api_keys.py +66 -0
  32. modulex_python-1.0.0/src/modulex/types/assistant.py +104 -0
  33. modulex_python-1.0.0/src/modulex/types/auth.py +107 -0
  34. modulex_python-1.0.0/src/modulex/types/chats.py +83 -0
  35. modulex_python-1.0.0/src/modulex/types/composer.py +152 -0
  36. modulex_python-1.0.0/src/modulex/types/credentials.py +120 -0
  37. modulex_python-1.0.0/src/modulex/types/dashboard.py +255 -0
  38. modulex_python-1.0.0/src/modulex/types/deployments.py +126 -0
  39. modulex_python-1.0.0/src/modulex/types/executions.py +135 -0
  40. modulex_python-1.0.0/src/modulex/types/integrations.py +121 -0
  41. modulex_python-1.0.0/src/modulex/types/knowledge.py +168 -0
  42. modulex_python-1.0.0/src/modulex/types/notifications.py +77 -0
  43. modulex_python-1.0.0/src/modulex/types/organizations.py +147 -0
  44. modulex_python-1.0.0/src/modulex/types/realtime.py +270 -0
  45. modulex_python-1.0.0/src/modulex/types/schedules.py +101 -0
  46. modulex_python-1.0.0/src/modulex/types/subscriptions.py +114 -0
  47. modulex_python-1.0.0/src/modulex/types/system.py +62 -0
  48. modulex_python-1.0.0/src/modulex/types/workflows.py +361 -0
  49. modulex_python-0.1.0/CHANGELOG.md +0 -21
  50. modulex_python-0.1.0/src/modulex/_compat.py +0 -39
  51. modulex_python-0.1.0/src/modulex/_exceptions.py +0 -131
  52. modulex_python-0.1.0/src/modulex/_streaming.py +0 -118
  53. modulex_python-0.1.0/src/modulex/resources/auth.py +0 -38
  54. modulex_python-0.1.0/src/modulex/resources/composer.py +0 -134
  55. modulex_python-0.1.0/src/modulex/resources/credentials.py +0 -197
  56. modulex_python-0.1.0/src/modulex/resources/executions.py +0 -97
  57. modulex_python-0.1.0/src/modulex/resources/organizations.py +0 -72
  58. modulex_python-0.1.0/src/modulex/resources/subscriptions.py +0 -38
  59. modulex_python-0.1.0/src/modulex/resources/system.py +0 -28
  60. modulex_python-0.1.0/src/modulex/resources/templates.py +0 -115
  61. modulex_python-0.1.0/src/modulex/types/__init__.py +0 -294
  62. modulex_python-0.1.0/src/modulex/types/api_keys.py +0 -19
  63. modulex_python-0.1.0/src/modulex/types/auth.py +0 -62
  64. modulex_python-0.1.0/src/modulex/types/chats.py +0 -55
  65. modulex_python-0.1.0/src/modulex/types/composer.py +0 -27
  66. modulex_python-0.1.0/src/modulex/types/credentials.py +0 -79
  67. modulex_python-0.1.0/src/modulex/types/dashboard.py +0 -54
  68. modulex_python-0.1.0/src/modulex/types/executions.py +0 -104
  69. modulex_python-0.1.0/src/modulex/types/integrations.py +0 -29
  70. modulex_python-0.1.0/src/modulex/types/knowledge.py +0 -75
  71. modulex_python-0.1.0/src/modulex/types/notifications.py +0 -16
  72. modulex_python-0.1.0/src/modulex/types/organizations.py +0 -43
  73. modulex_python-0.1.0/src/modulex/types/schedules.py +0 -48
  74. modulex_python-0.1.0/src/modulex/types/subscriptions.py +0 -59
  75. modulex_python-0.1.0/src/modulex/types/templates.py +0 -50
  76. modulex_python-0.1.0/src/modulex/types/workflows.py +0 -253
  77. {modulex_python-0.1.0 → modulex_python-1.0.0}/CODE_OF_CONDUCT.md +0 -0
  78. {modulex_python-0.1.0 → modulex_python-1.0.0}/CONTRIBUTING.md +0 -0
  79. {modulex_python-0.1.0 → modulex_python-1.0.0}/LICENSE +0 -0
  80. {modulex_python-0.1.0 → modulex_python-1.0.0}/SECURITY.md +0 -0
  81. {modulex_python-0.1.0 → modulex_python-1.0.0}/src/modulex/_config.py +0 -0
  82. {modulex_python-0.1.0 → modulex_python-1.0.0}/src/modulex/py.typed +0 -0
  83. {modulex_python-0.1.0 → modulex_python-1.0.0}/src/modulex/resources/__init__.py +0 -0
  84. {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: 0.1.0
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 :: 4 - Beta
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
- # Direct LLM call
157
+ # Safely retry a run without double-execution
155
158
  result = await client.executions.run(
156
- llm={
157
- "integration_name": "openai",
158
- "provider_id": "openai",
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['node_id']}: {event.data['status']}")
186
- elif event.event == "done":
187
- print(f"Completed in {event.data['total_execution_time_ms']}ms")
188
- elif event.event == "error":
189
- print(f"Error: {event.data['error_message']}")
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
- # Start a composer session
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={"integration_name": "anthropic", "model_id": "claude-sonnet-4-20250514"},
309
+ llm=ComposerLLMConfig(integration_name="anthropic", provider_id="anthropic", model_id="claude-sonnet-4-20250514"),
315
310
  )
316
311
 
317
- # Listen to composer events
318
- async for event in client.composer.listen(result["composer_chat_id"], result["run_id"]):
319
- if event.event == "workflow_change":
320
- print(f"Workflow modified: {event.data}")
321
- elif event.event == "done":
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
- # Save or revert changes
325
- await client.composer.save(result["composer_chat_id"])
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
- All types are available for import:
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
- SSEEvent,
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
- # Direct LLM call
118
+ # Safely retry a run without double-execution
118
119
  result = await client.executions.run(
119
- llm={
120
- "integration_name": "openai",
121
- "provider_id": "openai",
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['node_id']}: {event.data['status']}")
149
- elif event.event == "done":
150
- print(f"Completed in {event.data['total_execution_time_ms']}ms")
151
- elif event.event == "error":
152
- print(f"Error: {event.data['error_message']}")
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
- # Start a composer session
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={"integration_name": "anthropic", "model_id": "claude-sonnet-4-20250514"},
270
+ llm=ComposerLLMConfig(integration_name="anthropic", provider_id="anthropic", model_id="claude-sonnet-4-20250514"),
278
271
  )
279
272
 
280
- # Listen to composer events
281
- async for event in client.composer.listen(result["composer_chat_id"], result["run_id"]):
282
- if event.event == "workflow_change":
283
- print(f"Workflow modified: {event.data}")
284
- elif event.event == "done":
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
- # Save or revert changes
288
- await client.composer.save(result["composer_chat_id"])
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
- All types are available for import:
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
- SSEEvent,
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 = "0.1.0"
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 :: 4 - Beta",
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"