modulex-python 0.1.0__py3-none-any.whl

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 (47) hide show
  1. modulex/__init__.py +39 -0
  2. modulex/_base.py +281 -0
  3. modulex/_client.py +237 -0
  4. modulex/_compat.py +39 -0
  5. modulex/_config.py +26 -0
  6. modulex/_exceptions.py +131 -0
  7. modulex/_streaming.py +118 -0
  8. modulex/py.typed +0 -0
  9. modulex/resources/__init__.py +1 -0
  10. modulex/resources/api_keys.py +39 -0
  11. modulex/resources/auth.py +38 -0
  12. modulex/resources/chats.py +62 -0
  13. modulex/resources/composer.py +134 -0
  14. modulex/resources/credentials.py +197 -0
  15. modulex/resources/dashboard.py +110 -0
  16. modulex/resources/deployments.py +92 -0
  17. modulex/resources/executions.py +97 -0
  18. modulex/resources/integrations.py +110 -0
  19. modulex/resources/knowledge.py +343 -0
  20. modulex/resources/notifications.py +39 -0
  21. modulex/resources/organizations.py +72 -0
  22. modulex/resources/schedules.py +172 -0
  23. modulex/resources/subscriptions.py +38 -0
  24. modulex/resources/system.py +28 -0
  25. modulex/resources/templates.py +115 -0
  26. modulex/resources/workflows.py +156 -0
  27. modulex/types/__init__.py +294 -0
  28. modulex/types/api_keys.py +19 -0
  29. modulex/types/auth.py +62 -0
  30. modulex/types/chats.py +55 -0
  31. modulex/types/composer.py +27 -0
  32. modulex/types/credentials.py +79 -0
  33. modulex/types/dashboard.py +54 -0
  34. modulex/types/executions.py +104 -0
  35. modulex/types/integrations.py +29 -0
  36. modulex/types/knowledge.py +75 -0
  37. modulex/types/notifications.py +16 -0
  38. modulex/types/organizations.py +43 -0
  39. modulex/types/schedules.py +48 -0
  40. modulex/types/shared.py +39 -0
  41. modulex/types/subscriptions.py +59 -0
  42. modulex/types/templates.py +50 -0
  43. modulex/types/workflows.py +253 -0
  44. modulex_python-0.1.0.dist-info/METADATA +435 -0
  45. modulex_python-0.1.0.dist-info/RECORD +47 -0
  46. modulex_python-0.1.0.dist-info/WHEEL +4 -0
  47. modulex_python-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,253 @@
1
+ """Workflow-related type definitions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from typing_extensions import TypedDict
8
+
9
+
10
+ class LLMConfig(TypedDict, total=False):
11
+ """LLM configuration."""
12
+
13
+ integration_name: str
14
+ provider_id: str
15
+ model_id: str
16
+ temperature: float
17
+ credential_id: str | None
18
+
19
+
20
+ class ToolDefinition(TypedDict, total=False):
21
+ """Tool definition for workflow nodes."""
22
+
23
+ integration_name: str
24
+ service_name: str
25
+ credential_id: str | None
26
+ parameter_defaults: dict[str, Any]
27
+ parameter_overrides: dict[str, Any]
28
+
29
+
30
+ class RetryConfig(TypedDict, total=False):
31
+ """Node retry configuration."""
32
+
33
+ max_attempts: int
34
+ initial_interval: float
35
+ backoff_factor: float
36
+
37
+
38
+ class LLMNodeConfig(TypedDict, total=False):
39
+ """LLM node configuration."""
40
+
41
+ llm: LLMConfig
42
+ system_prompt: str
43
+ user_prompt: str
44
+ structured_output_schema: dict[str, Any] | None
45
+
46
+
47
+ class ToolNodeConfig(TypedDict, total=False):
48
+ """Tool node configuration."""
49
+
50
+ tool: ToolDefinition
51
+ input_mapping: dict[str, Any]
52
+
53
+
54
+ class AgentNodeConfig(TypedDict, total=False):
55
+ """Agent node configuration."""
56
+
57
+ llm: LLMConfig
58
+ tools: list[ToolDefinition]
59
+ system_prompt: str
60
+ user_prompt: str
61
+ max_iterations: int
62
+ input_mapping: dict[str, Any]
63
+
64
+
65
+ class ExpressionBranch(TypedDict, total=False):
66
+ """Conditional expression branch."""
67
+
68
+ name: str
69
+ condition: str
70
+ target: str
71
+
72
+
73
+ class LoopConfig(TypedDict, total=False):
74
+ """Loop configuration for conditional nodes."""
75
+
76
+ loop_id: str
77
+ mode: str
78
+ iterations: int
79
+ collection: str
80
+ condition: str
81
+ body_target: str
82
+ body_end: str
83
+ exit_target: str
84
+ max_iterations: int
85
+
86
+
87
+ class ConditionalNodeConfig(TypedDict, total=False):
88
+ """Conditional node configuration."""
89
+
90
+ condition_type: str
91
+ routes: dict[str, str]
92
+ expression_branches: list[ExpressionBranch]
93
+ loop_config: LoopConfig
94
+
95
+
96
+ class InterruptNodeConfig(TypedDict, total=False):
97
+ """Interrupt node configuration."""
98
+
99
+ message: str
100
+ resume_schema: dict[str, Any]
101
+ examples: list[Any]
102
+
103
+
104
+ class TransformerOperation(TypedDict, total=False):
105
+ """Transformer operation."""
106
+
107
+ type: str
108
+ path: str
109
+ condition: str
110
+ template: str
111
+
112
+
113
+ class TransformerNodeConfig(TypedDict, total=False):
114
+ """Transformer node configuration."""
115
+
116
+ source: str
117
+ operations: list[TransformerOperation]
118
+
119
+
120
+ class GuardrailsNodeConfig(TypedDict, total=False):
121
+ """Guardrails node configuration."""
122
+
123
+ rules: list[dict[str, Any]]
124
+ on_violation: str
125
+
126
+
127
+ class KnowledgeNodeConfig(TypedDict, total=False):
128
+ """Knowledge node configuration."""
129
+
130
+ credential_id: str
131
+ provider_type: str
132
+ query: str
133
+ collection_name: str
134
+ top_k: int
135
+ min_score: float
136
+ filters: dict[str, Any]
137
+ embedding_config: dict[str, Any]
138
+
139
+
140
+ class NodeDefinition(TypedDict, total=False):
141
+ """Workflow node definition."""
142
+
143
+ id: str
144
+ type: str
145
+ name: str
146
+ description: str
147
+ enabled: bool
148
+ x: int
149
+ y: int
150
+ retry_config: RetryConfig
151
+ llm_config: LLMNodeConfig
152
+ tool_config: ToolNodeConfig
153
+ agent_config: AgentNodeConfig
154
+ function_config: dict[str, Any]
155
+ conditional_config: ConditionalNodeConfig
156
+ interrupt_config: InterruptNodeConfig
157
+ transformer_config: TransformerNodeConfig
158
+ guardrails_config: GuardrailsNodeConfig
159
+ knowledge_config: KnowledgeNodeConfig
160
+
161
+
162
+ class EdgeDefinition(TypedDict):
163
+ """Workflow edge definition."""
164
+
165
+ source: str
166
+ target: str
167
+
168
+
169
+ class StateField(TypedDict, total=False):
170
+ """State schema field."""
171
+
172
+ type: str
173
+ description: str
174
+ reducer: str
175
+ required: bool
176
+ default: Any
177
+
178
+
179
+ class StateSchema(TypedDict, total=False):
180
+ """Workflow state schema."""
181
+
182
+ fields: dict[str, StateField]
183
+
184
+
185
+ class WorkflowMetadata(TypedDict, total=False):
186
+ """Workflow metadata."""
187
+
188
+ name: str
189
+ description: str
190
+ version: str
191
+ author: str
192
+ tags: list[str]
193
+
194
+
195
+ class WorkflowConfig(TypedDict, total=False):
196
+ """Workflow configuration."""
197
+
198
+ default_llm: LLMConfig
199
+ default_tools: list[ToolDefinition]
200
+ recursion_limit: int
201
+ checkpointing: str
202
+
203
+
204
+ class WorkflowDefinition(TypedDict, total=False):
205
+ """Complete workflow definition schema."""
206
+
207
+ metadata: WorkflowMetadata
208
+ config: WorkflowConfig
209
+ state_schema: StateSchema
210
+ nodes: list[NodeDefinition]
211
+ edges: list[EdgeDefinition]
212
+ entry_point: str
213
+
214
+
215
+ class WorkflowResponse(TypedDict, total=False):
216
+ """Workflow response from API."""
217
+
218
+ id: str
219
+ organization_id: str
220
+ name: str
221
+ description: str
222
+ version: str
223
+ tags: list[str]
224
+ category: str
225
+ status: str
226
+ visibility: str
227
+ workflow_schema: WorkflowDefinition
228
+ input: dict[str, Any]
229
+ config: dict[str, Any]
230
+ edit_version: int
231
+ last_edited_by: str | None
232
+ last_edited_at: str | None
233
+ created_at: str
234
+ updated_at: str
235
+
236
+
237
+ class WorkflowListResponse(TypedDict, total=False):
238
+ """Response from /workflows list."""
239
+
240
+ workflows: list[WorkflowResponse]
241
+ total: int
242
+ page: int
243
+ page_size: int
244
+ total_pages: int
245
+
246
+
247
+ class BuilderDetailsResponse(TypedDict, total=False):
248
+ """Response from /workflows/builder/details."""
249
+
250
+ node_types: dict[str, Any]
251
+ categories: dict[str, Any]
252
+ counts: dict[str, Any]
253
+ cached: bool
@@ -0,0 +1,435 @@
1
+ Metadata-Version: 2.4
2
+ Name: modulex-python
3
+ Version: 0.1.0
4
+ Summary: Official Python SDK for the ModuleX AI workflow orchestration platform
5
+ Project-URL: Homepage, https://modulex.dev
6
+ Project-URL: Documentation, https://docs.modulex.dev
7
+ Project-URL: Repository, https://github.com/ModuleXAI/modulex-python
8
+ Project-URL: Issues, https://github.com/ModuleXAI/modulex-python/issues
9
+ Project-URL: Changelog, https://github.com/ModuleXAI/modulex-python/blob/main/CHANGELOG.md
10
+ Author-email: ModuleX <contact@modulex.dev>
11
+ License-Expression: MIT
12
+ License-File: LICENSE
13
+ Keywords: ai,modulex,orchestration,sdk,workflow
14
+ Classifier: Development Status :: 4 - Beta
15
+ Classifier: Framework :: AsyncIO
16
+ Classifier: Intended Audience :: Developers
17
+ Classifier: License :: OSI Approved :: MIT License
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.9
20
+ Classifier: Programming Language :: Python :: 3.10
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Programming Language :: Python :: 3.13
24
+ Classifier: Typing :: Typed
25
+ Requires-Python: >=3.9
26
+ Requires-Dist: httpx-sse>=0.4
27
+ Requires-Dist: httpx>=0.27
28
+ Provides-Extra: dev
29
+ Requires-Dist: mypy>=1.13; extra == 'dev'
30
+ Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
31
+ Requires-Dist: pytest-cov>=5.0; extra == 'dev'
32
+ Requires-Dist: pytest>=8.0; extra == 'dev'
33
+ Requires-Dist: python-dotenv>=1.0; extra == 'dev'
34
+ Requires-Dist: respx>=0.22; extra == 'dev'
35
+ Requires-Dist: ruff>=0.8; extra == 'dev'
36
+ Description-Content-Type: text/markdown
37
+
38
+ # ModuleX Python SDK
39
+
40
+ The official Python SDK for the [ModuleX](https://modulex.dev) AI workflow orchestration platform.
41
+
42
+ [![CI](https://github.com/ModuleXAI/modulex-python/actions/workflows/ci.yml/badge.svg)](https://github.com/ModuleXAI/modulex-python/actions/workflows/ci.yml)
43
+ [![PyPI](https://img.shields.io/pypi/v/modulex-python)](https://pypi.org/project/modulex-python/)
44
+ [![Python](https://img.shields.io/pypi/pyversions/modulex-python)](https://pypi.org/project/modulex-python/)
45
+ [![License](https://img.shields.io/github/license/ModuleXAI/modulex-python)](LICENSE)
46
+
47
+ ## Installation
48
+
49
+ ```bash
50
+ pip install modulex-python
51
+ ```
52
+
53
+ ## Quick Start
54
+
55
+ ```python
56
+ import asyncio
57
+ from modulex import Modulex
58
+
59
+ async def main():
60
+ async with Modulex(
61
+ api_key="mx_live_...",
62
+ organization_id="your-org-id",
63
+ ) as client:
64
+ # Get current user
65
+ me = await client.auth.me()
66
+ print(f"Hello, {me['username']}!")
67
+
68
+ # List workflows
69
+ workflows = await client.workflows.list(status="active")
70
+ for wf in workflows["workflows"]:
71
+ print(f" {wf['name']}")
72
+
73
+ asyncio.run(main())
74
+ ```
75
+
76
+ ## Authentication
77
+
78
+ Get your API key from the [ModuleX Dashboard](https://app.modulex.dev). Keys use the `mx_live_` prefix.
79
+
80
+ ```python
81
+ from modulex import Modulex
82
+
83
+ # Pass API key directly
84
+ client = Modulex(api_key="mx_live_...")
85
+
86
+ # Or use environment variable
87
+ import os
88
+ client = Modulex(api_key=os.environ["MODULEX_API_KEY"])
89
+ ```
90
+
91
+ ### Organization Context
92
+
93
+ Most endpoints require an organization context. Set it at the client level or override per-request:
94
+
95
+ ```python
96
+ # Set default org for all requests
97
+ client = Modulex(api_key="mx_live_...", organization_id="org-uuid")
98
+
99
+ # Override for a specific request
100
+ workflows = await client.workflows.list(organization_id="other-org-uuid")
101
+ ```
102
+
103
+ ## Configuration
104
+
105
+ ```python
106
+ client = Modulex(
107
+ api_key="mx_live_...",
108
+ organization_id="org-uuid", # Default organization
109
+ base_url="https://api.modulex.dev", # API base URL
110
+ timeout=30.0, # Request timeout (seconds)
111
+ max_retries=3, # Retry count for transient errors
112
+ )
113
+ ```
114
+
115
+ ## Resources
116
+
117
+ ### Workflows
118
+
119
+ ```python
120
+ # List workflows
121
+ workflows = await client.workflows.list(status="active", search="email")
122
+
123
+ # Auto-paginate all workflows
124
+ async for wf in client.workflows.list_all(status="active"):
125
+ print(wf["name"])
126
+
127
+ # Create workflow
128
+ workflow = await client.workflows.create(
129
+ workflow_schema={
130
+ "metadata": {"name": "My Workflow", "version": "1.0"},
131
+ "config": {},
132
+ "state_schema": {"fields": {}},
133
+ "nodes": [],
134
+ "edges": [],
135
+ "entry_point": "start",
136
+ },
137
+ name="My Workflow",
138
+ )
139
+
140
+ # Update & delete
141
+ await client.workflows.update("workflow-id", name="New Name", status="active")
142
+ await client.workflows.delete("workflow-id")
143
+ ```
144
+
145
+ ### Executions
146
+
147
+ ```python
148
+ # Run a workflow
149
+ result = await client.executions.run(
150
+ workflow_id="workflow-uuid",
151
+ input={"messages": [{"role": "user", "content": "Hello!"}]},
152
+ )
153
+
154
+ # Direct LLM call
155
+ 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!"}]},
163
+ )
164
+
165
+ # Get execution state
166
+ 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
176
+ await client.executions.cancel(run_id="run-uuid", reason="No longer needed")
177
+ ```
178
+
179
+ ### SSE Streaming
180
+
181
+ ```python
182
+ # Listen to workflow execution events
183
+ async for event in client.executions.listen(run_id="run-uuid"):
184
+ 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']}")
190
+
191
+ # Listen to chat list updates
192
+ async for event in client.chats.stream():
193
+ if event.event == "chat_list_updated":
194
+ print(f"Chat list changed: {event.data}")
195
+
196
+ # Listen to composer events
197
+ async for event in client.composer.listen("chat-id", "run-id"):
198
+ print(f"{event.event}: {event.data}")
199
+ ```
200
+
201
+ ### Credentials
202
+
203
+ ```python
204
+ # Add an API key credential
205
+ cred = await client.credentials.create(
206
+ integration_name="openai",
207
+ auth_data={"api_key": "sk-..."},
208
+ display_name="Production OpenAI",
209
+ )
210
+
211
+ # Test a credential
212
+ result = await client.credentials.test(cred["credential_id"])
213
+ print(f"Valid: {result['is_valid']}")
214
+
215
+ # List credentials
216
+ creds = await client.credentials.list(integration_name="openai")
217
+
218
+ # Add MCP server
219
+ mcp = await client.credentials.create_mcp_server(
220
+ server_url="https://mcp-server.example.com",
221
+ headers={"Authorization": "Bearer ..."},
222
+ )
223
+ ```
224
+
225
+ ### Knowledge Bases
226
+
227
+ ```python
228
+ # Create a knowledge base
229
+ kb = await client.knowledge.create(
230
+ name="Docs",
231
+ embedding_config={"provider": "openai", "model": "text-embedding-3-small"},
232
+ )
233
+
234
+ # Upload a document
235
+ doc = await client.knowledge.upload_document(
236
+ knowledge_base_id=kb["id"],
237
+ file_path="/path/to/doc.pdf",
238
+ metadata={"department": "engineering"},
239
+ )
240
+
241
+ # Search
242
+ results = await client.knowledge.search(
243
+ knowledge_base_id=kb["id"],
244
+ query="How does deployment work?",
245
+ top_k=5,
246
+ )
247
+
248
+ # Retrieve RAG context
249
+ context = await client.knowledge.retrieve_context(
250
+ knowledge_base_id=kb["id"],
251
+ query="deployment steps",
252
+ max_tokens=2000,
253
+ )
254
+ ```
255
+
256
+ ### Schedules
257
+
258
+ ```python
259
+ # Create a cron schedule
260
+ schedule = await client.schedules.create(
261
+ workflow_id="workflow-uuid",
262
+ name="Daily Report",
263
+ schedule_type="cron",
264
+ cron_expression="0 9 * * 1-5",
265
+ timezone="America/New_York",
266
+ )
267
+
268
+ # Pause/resume
269
+ await client.schedules.pause(schedule["id"])
270
+ await client.schedules.resume(schedule["id"])
271
+
272
+ # View run history
273
+ runs = await client.schedules.list_runs(schedule["id"])
274
+ stats = await client.schedules.run_stats(schedule["id"], days=30)
275
+ ```
276
+
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
+ ### Deployments
292
+
293
+ ```python
294
+ # Deploy a workflow
295
+ deployment = await client.deployments.create(
296
+ workflow_id="workflow-uuid",
297
+ deployment_note="v1.0 release",
298
+ )
299
+
300
+ # Activate a deployment
301
+ await client.deployments.activate("workflow-uuid", deployment["id"])
302
+
303
+ # Deactivate live deployment
304
+ await client.deployments.deactivate("workflow-uuid")
305
+ ```
306
+
307
+ ### Composer
308
+
309
+ ```python
310
+ # Start a composer session
311
+ result = await client.composer.chat(
312
+ message="Add an LLM node that summarizes the input",
313
+ workflow_id="workflow-uuid",
314
+ llm={"integration_name": "anthropic", "model_id": "claude-sonnet-4-20250514"},
315
+ )
316
+
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":
322
+ break
323
+
324
+ # Save or revert changes
325
+ await client.composer.save(result["composer_chat_id"])
326
+ ```
327
+
328
+ ### Other Resources
329
+
330
+ ```python
331
+ # Organizations
332
+ await client.organizations.create(name="My Org")
333
+ await client.organizations.invite("user@example.com", role="member")
334
+ llms = await client.organizations.llms()
335
+
336
+ # Dashboard
337
+ logs = await client.dashboard.logs(category="CREDENTIALS")
338
+ overview = await client.dashboard.analytics_overview()
339
+ users = await client.dashboard.users(search="john")
340
+
341
+ # Subscriptions
342
+ plans = await client.subscriptions.organization_plans()
343
+ billing = await client.subscriptions.organization_billing()
344
+
345
+ # Notifications
346
+ notifications = await client.notifications.list()
347
+
348
+ # Integrations
349
+ integrations = await client.integrations.browse(type="tool")
350
+ providers = await client.integrations.llm_providers()
351
+
352
+ # System
353
+ health = await client.system.health()
354
+ timezones = await client.system.timezones()
355
+
356
+ # API Keys
357
+ key = await client.api_keys.create(name="CI/CD Key")
358
+ await client.api_keys.revoke(key["id"])
359
+ ```
360
+
361
+ ## Error Handling
362
+
363
+ ```python
364
+ from modulex import (
365
+ Modulex,
366
+ ModulexError,
367
+ AuthenticationError,
368
+ NotFoundError,
369
+ RateLimitError,
370
+ ValidationError,
371
+ )
372
+
373
+ try:
374
+ workflow = await client.workflows.get("invalid-id")
375
+ except NotFoundError:
376
+ print("Workflow not found")
377
+ except RateLimitError as e:
378
+ print(f"Rate limited. Retry after {e.retry_after}s")
379
+ except AuthenticationError:
380
+ print("Invalid API key")
381
+ except ValidationError as e:
382
+ print(f"Validation error: {e.message}")
383
+ except ModulexError as e:
384
+ print(f"API error ({e.status_code}): {e.message}")
385
+ ```
386
+
387
+ ### Exception Hierarchy
388
+
389
+ | Exception | HTTP Status | Description |
390
+ |-----------|-------------|-------------|
391
+ | `ModulexError` | — | Base exception |
392
+ | `BadRequestError` | 400 | Malformed request |
393
+ | `AuthenticationError` | 401 | Invalid/missing auth |
394
+ | `PermissionError` | 403 | Insufficient permissions |
395
+ | `NotFoundError` | 404 | Resource not found |
396
+ | `ConflictError` | 409 | Resource conflict |
397
+ | `ValidationError` | 422 | Validation error |
398
+ | `RateLimitError` | 429 | Rate limit exceeded |
399
+ | `InternalError` | 500 | Server error |
400
+ | `ExternalServiceError` | 502 | External service failure |
401
+ | `ServiceUnavailableError` | 503 | Service unavailable |
402
+ | `StreamError` | — | SSE stream error |
403
+ | `TimeoutError` | — | Request timeout |
404
+
405
+ ## Type Hints
406
+
407
+ All types are available for import:
408
+
409
+ ```python
410
+ from modulex.types import (
411
+ WorkflowDefinition,
412
+ NodeDefinition,
413
+ EdgeDefinition,
414
+ LLMConfig,
415
+ RunResponse,
416
+ SSEEvent,
417
+ )
418
+ ```
419
+
420
+ ## Documentation
421
+
422
+ For full API documentation, visit [docs.modulex.dev](https://docs.modulex.dev).
423
+
424
+ ## Contributing
425
+
426
+ Contributions are welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines on:
427
+
428
+ - Setting up the development environment
429
+ - Running tests (unit and integration)
430
+ - Code style and commit conventions
431
+ - Pull request process
432
+
433
+ ## License
434
+
435
+ This project is licensed under the MIT License — see the [LICENSE](LICENSE) file for details.