model-compose 0.1.0__tar.gz → 0.1.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.
Files changed (118) hide show
  1. {model_compose-0.1.0 → model_compose-0.1.2}/PKG-INFO +20 -28
  2. {model_compose-0.1.0 → model_compose-0.1.2}/README.md +19 -27
  3. {model_compose-0.1.0 → model_compose-0.1.2}/pyproject.toml +1 -1
  4. {model_compose-0.1.0 → model_compose-0.1.2}/src/mindor/core/component/engine/http_client.py +11 -13
  5. {model_compose-0.1.0 → model_compose-0.1.2}/src/mindor/core/controller/engine/http_server.py +2 -1
  6. {model_compose-0.1.0 → model_compose-0.1.2}/src/mindor/core/controller/webui/gradio.py +53 -19
  7. model_compose-0.1.2/src/mindor/core/controller/webui/schema.py +198 -0
  8. {model_compose-0.1.0 → model_compose-0.1.2}/src/mindor/core/controller/webui/webui.py +2 -1
  9. {model_compose-0.1.0 → model_compose-0.1.2}/src/mindor/core/listener/engine/http_callback.py +4 -3
  10. {model_compose-0.1.0 → model_compose-0.1.2}/src/mindor/core/utils/http_client.py +3 -3
  11. {model_compose-0.1.0 → model_compose-0.1.2}/src/mindor/core/utils/template.py +3 -3
  12. model_compose-0.1.2/src/mindor/core/workflow/job.py +37 -0
  13. {model_compose-0.1.0 → model_compose-0.1.2}/src/mindor/core/workflow/workflow.py +2 -2
  14. {model_compose-0.1.0 → model_compose-0.1.2}/src/mindor/dsl/schema/workflow.py +18 -6
  15. {model_compose-0.1.0 → model_compose-0.1.2}/src/model_compose.egg-info/PKG-INFO +20 -28
  16. model_compose-0.1.0/src/mindor/core/controller/webui/schema.py +0 -135
  17. model_compose-0.1.0/src/mindor/core/workflow/job.py +0 -27
  18. {model_compose-0.1.0 → model_compose-0.1.2}/LICENSE +0 -0
  19. {model_compose-0.1.0 → model_compose-0.1.2}/setup.cfg +0 -0
  20. {model_compose-0.1.0 → model_compose-0.1.2}/src/mindor/__init__.py +0 -0
  21. {model_compose-0.1.0 → model_compose-0.1.2}/src/mindor/cli/__init__.py +0 -0
  22. {model_compose-0.1.0 → model_compose-0.1.2}/src/mindor/cli/compose.py +0 -0
  23. {model_compose-0.1.0 → model_compose-0.1.2}/src/mindor/core/__init__.py +0 -0
  24. {model_compose-0.1.0 → model_compose-0.1.2}/src/mindor/core/component/__init__.py +0 -0
  25. {model_compose-0.1.0 → model_compose-0.1.2}/src/mindor/core/component/component.py +0 -0
  26. {model_compose-0.1.0 → model_compose-0.1.2}/src/mindor/core/component/engine/__init__.py +0 -0
  27. {model_compose-0.1.0 → model_compose-0.1.2}/src/mindor/core/component/engine/base.py +0 -0
  28. {model_compose-0.1.0 → model_compose-0.1.2}/src/mindor/core/component/engine/context.py +0 -0
  29. {model_compose-0.1.0 → model_compose-0.1.2}/src/mindor/core/component/engine/http_server.py +0 -0
  30. {model_compose-0.1.0 → model_compose-0.1.2}/src/mindor/core/compose/__init__.py +0 -0
  31. {model_compose-0.1.0 → model_compose-0.1.2}/src/mindor/core/compose/compose.py +0 -0
  32. {model_compose-0.1.0 → model_compose-0.1.2}/src/mindor/core/compose/manager.py +0 -0
  33. {model_compose-0.1.0 → model_compose-0.1.2}/src/mindor/core/controller/__init__.py +0 -0
  34. {model_compose-0.1.0 → model_compose-0.1.2}/src/mindor/core/controller/controller.py +0 -0
  35. {model_compose-0.1.0 → model_compose-0.1.2}/src/mindor/core/controller/engine/__init__.py +0 -0
  36. {model_compose-0.1.0 → model_compose-0.1.2}/src/mindor/core/controller/engine/base.py +0 -0
  37. {model_compose-0.1.0 → model_compose-0.1.2}/src/mindor/core/controller/engine/mcp_server.py +0 -0
  38. {model_compose-0.1.0 → model_compose-0.1.2}/src/mindor/core/controller/runner/__init__.py +0 -0
  39. {model_compose-0.1.0 → model_compose-0.1.2}/src/mindor/core/controller/runner/client.py +0 -0
  40. {model_compose-0.1.0 → model_compose-0.1.2}/src/mindor/core/controller/runner/http_client.py +0 -0
  41. {model_compose-0.1.0 → model_compose-0.1.2}/src/mindor/core/controller/runner/mcp_client.py +0 -0
  42. {model_compose-0.1.0 → model_compose-0.1.2}/src/mindor/core/controller/runner/runner.py +0 -0
  43. {model_compose-0.1.0 → model_compose-0.1.2}/src/mindor/core/controller/webui/__init__.py +0 -0
  44. {model_compose-0.1.0 → model_compose-0.1.2}/src/mindor/core/gateway/__init__.py +0 -0
  45. {model_compose-0.1.0 → model_compose-0.1.2}/src/mindor/core/gateway/engine/__init__.py +0 -0
  46. {model_compose-0.1.0 → model_compose-0.1.2}/src/mindor/core/gateway/engine/base.py +0 -0
  47. {model_compose-0.1.0 → model_compose-0.1.2}/src/mindor/core/gateway/engine/http_tunnel.py +0 -0
  48. {model_compose-0.1.0 → model_compose-0.1.2}/src/mindor/core/gateway/gateway.py +0 -0
  49. {model_compose-0.1.0 → model_compose-0.1.2}/src/mindor/core/listener/__init__.py +0 -0
  50. {model_compose-0.1.0 → model_compose-0.1.2}/src/mindor/core/listener/engine/__init__.py +0 -0
  51. {model_compose-0.1.0 → model_compose-0.1.2}/src/mindor/core/listener/engine/base.py +0 -0
  52. {model_compose-0.1.0 → model_compose-0.1.2}/src/mindor/core/listener/listener.py +0 -0
  53. {model_compose-0.1.0 → model_compose-0.1.2}/src/mindor/core/runtime/__init__.py +0 -0
  54. {model_compose-0.1.0 → model_compose-0.1.2}/src/mindor/core/runtime/env.py +0 -0
  55. {model_compose-0.1.0 → model_compose-0.1.2}/src/mindor/core/services/__init__.py +0 -0
  56. {model_compose-0.1.0 → model_compose-0.1.2}/src/mindor/core/services/async_service.py +0 -0
  57. {model_compose-0.1.0 → model_compose-0.1.2}/src/mindor/core/utils/__init__.py +0 -0
  58. {model_compose-0.1.0 → model_compose-0.1.2}/src/mindor/core/utils/expiring.py +0 -0
  59. {model_compose-0.1.0 → model_compose-0.1.2}/src/mindor/core/utils/http_request.py +0 -0
  60. {model_compose-0.1.0 → model_compose-0.1.2}/src/mindor/core/utils/image.py +0 -0
  61. {model_compose-0.1.0 → model_compose-0.1.2}/src/mindor/core/utils/streaming.py +0 -0
  62. {model_compose-0.1.0 → model_compose-0.1.2}/src/mindor/core/utils/time.py +0 -0
  63. {model_compose-0.1.0 → model_compose-0.1.2}/src/mindor/core/utils/workqueue.py +0 -0
  64. {model_compose-0.1.0 → model_compose-0.1.2}/src/mindor/core/workflow/__init__.py +0 -0
  65. {model_compose-0.1.0 → model_compose-0.1.2}/src/mindor/core/workflow/context.py +0 -0
  66. {model_compose-0.1.0 → model_compose-0.1.2}/src/mindor/dsl/__init__.py +0 -0
  67. {model_compose-0.1.0 → model_compose-0.1.2}/src/mindor/dsl/loader.py +0 -0
  68. {model_compose-0.1.0 → model_compose-0.1.2}/src/mindor/dsl/schema/__init__.py +0 -0
  69. {model_compose-0.1.0 → model_compose-0.1.2}/src/mindor/dsl/schema/action/__init__.py +0 -0
  70. {model_compose-0.1.0 → model_compose-0.1.2}/src/mindor/dsl/schema/action/action.py +0 -0
  71. {model_compose-0.1.0 → model_compose-0.1.2}/src/mindor/dsl/schema/action/impl/__init__.py +0 -0
  72. {model_compose-0.1.0 → model_compose-0.1.2}/src/mindor/dsl/schema/action/impl/common.py +0 -0
  73. {model_compose-0.1.0 → model_compose-0.1.2}/src/mindor/dsl/schema/action/impl/http_client.py +0 -0
  74. {model_compose-0.1.0 → model_compose-0.1.2}/src/mindor/dsl/schema/action/impl/http_server.py +0 -0
  75. {model_compose-0.1.0 → model_compose-0.1.2}/src/mindor/dsl/schema/action/impl/mcp_client.py +0 -0
  76. {model_compose-0.1.0 → model_compose-0.1.2}/src/mindor/dsl/schema/action/impl/mcp_server.py +0 -0
  77. {model_compose-0.1.0 → model_compose-0.1.2}/src/mindor/dsl/schema/component/__init__.py +0 -0
  78. {model_compose-0.1.0 → model_compose-0.1.2}/src/mindor/dsl/schema/component/component.py +0 -0
  79. {model_compose-0.1.0 → model_compose-0.1.2}/src/mindor/dsl/schema/component/impl/__init__.py +0 -0
  80. {model_compose-0.1.0 → model_compose-0.1.2}/src/mindor/dsl/schema/component/impl/common.py +0 -0
  81. {model_compose-0.1.0 → model_compose-0.1.2}/src/mindor/dsl/schema/component/impl/http_client.py +0 -0
  82. {model_compose-0.1.0 → model_compose-0.1.2}/src/mindor/dsl/schema/component/impl/http_server.py +0 -0
  83. {model_compose-0.1.0 → model_compose-0.1.2}/src/mindor/dsl/schema/component/impl/mcp_client.py +0 -0
  84. {model_compose-0.1.0 → model_compose-0.1.2}/src/mindor/dsl/schema/component/impl/mcp_server.py +0 -0
  85. {model_compose-0.1.0 → model_compose-0.1.2}/src/mindor/dsl/schema/component/impl/types.py +0 -0
  86. {model_compose-0.1.0 → model_compose-0.1.2}/src/mindor/dsl/schema/compose.py +0 -0
  87. {model_compose-0.1.0 → model_compose-0.1.2}/src/mindor/dsl/schema/controller/__init__.py +0 -0
  88. {model_compose-0.1.0 → model_compose-0.1.2}/src/mindor/dsl/schema/controller/controller.py +0 -0
  89. {model_compose-0.1.0 → model_compose-0.1.2}/src/mindor/dsl/schema/controller/impl/__init__.py +0 -0
  90. {model_compose-0.1.0 → model_compose-0.1.2}/src/mindor/dsl/schema/controller/impl/common.py +0 -0
  91. {model_compose-0.1.0 → model_compose-0.1.2}/src/mindor/dsl/schema/controller/impl/http_server.py +0 -0
  92. {model_compose-0.1.0 → model_compose-0.1.2}/src/mindor/dsl/schema/controller/impl/mcp_server.py +0 -0
  93. {model_compose-0.1.0 → model_compose-0.1.2}/src/mindor/dsl/schema/controller/impl/types.py +0 -0
  94. {model_compose-0.1.0 → model_compose-0.1.2}/src/mindor/dsl/schema/controller/impl/webui.py +0 -0
  95. {model_compose-0.1.0 → model_compose-0.1.2}/src/mindor/dsl/schema/gateway/__init__.py +0 -0
  96. {model_compose-0.1.0 → model_compose-0.1.2}/src/mindor/dsl/schema/gateway/gateway.py +0 -0
  97. {model_compose-0.1.0 → model_compose-0.1.2}/src/mindor/dsl/schema/gateway/impl/__init__.py +0 -0
  98. {model_compose-0.1.0 → model_compose-0.1.2}/src/mindor/dsl/schema/gateway/impl/common.py +0 -0
  99. {model_compose-0.1.0 → model_compose-0.1.2}/src/mindor/dsl/schema/gateway/impl/http_tunnel.py +0 -0
  100. {model_compose-0.1.0 → model_compose-0.1.2}/src/mindor/dsl/schema/gateway/impl/types.py +0 -0
  101. {model_compose-0.1.0 → model_compose-0.1.2}/src/mindor/dsl/schema/listener/__init__.py +0 -0
  102. {model_compose-0.1.0 → model_compose-0.1.2}/src/mindor/dsl/schema/listener/impl/__init__.py +0 -0
  103. {model_compose-0.1.0 → model_compose-0.1.2}/src/mindor/dsl/schema/listener/impl/common.py +0 -0
  104. {model_compose-0.1.0 → model_compose-0.1.2}/src/mindor/dsl/schema/listener/impl/http_callback.py +0 -0
  105. {model_compose-0.1.0 → model_compose-0.1.2}/src/mindor/dsl/schema/listener/impl/types.py +0 -0
  106. {model_compose-0.1.0 → model_compose-0.1.2}/src/mindor/dsl/schema/listener/listener.py +0 -0
  107. {model_compose-0.1.0 → model_compose-0.1.2}/src/mindor/dsl/schema/runtime/__init__.py +0 -0
  108. {model_compose-0.1.0 → model_compose-0.1.2}/src/mindor/dsl/schema/runtime/impl/__init__.py +0 -0
  109. {model_compose-0.1.0 → model_compose-0.1.2}/src/mindor/dsl/schema/runtime/impl/common.py +0 -0
  110. {model_compose-0.1.0 → model_compose-0.1.2}/src/mindor/dsl/schema/runtime/impl/docker.py +0 -0
  111. {model_compose-0.1.0 → model_compose-0.1.2}/src/mindor/dsl/schema/runtime/impl/types.py +0 -0
  112. {model_compose-0.1.0 → model_compose-0.1.2}/src/mindor/dsl/schema/runtime/runtime.py +0 -0
  113. {model_compose-0.1.0 → model_compose-0.1.2}/src/model_compose.egg-info/SOURCES.txt +0 -0
  114. {model_compose-0.1.0 → model_compose-0.1.2}/src/model_compose.egg-info/dependency_links.txt +0 -0
  115. {model_compose-0.1.0 → model_compose-0.1.2}/src/model_compose.egg-info/entry_points.txt +0 -0
  116. {model_compose-0.1.0 → model_compose-0.1.2}/src/model_compose.egg-info/requires.txt +0 -0
  117. {model_compose-0.1.0 → model_compose-0.1.2}/src/model_compose.egg-info/top_level.txt +0 -0
  118. {model_compose-0.1.0 → model_compose-0.1.2}/tests/test_cli.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: model-compose
3
- Version: 0.1.0
3
+ Version: 0.1.2
4
4
  Summary: model-compose: Declarative AI Model and Workflow Orchestrator (part of the Mindor project)
5
5
  Author-email: Hanyeol Cho <hanyeol.cho@gmail.com>
6
6
  License-Expression: MIT
@@ -21,41 +21,28 @@ Requires-Dist: gradio
21
21
  Requires-Dist: Pillow
22
22
  Dynamic: license-file
23
23
 
24
- # model-compose: Declarative AI Model and Workflow Orchestrator (_part of the Mindor project_)
24
+ # model-compose: Declarative AI Model and Workflow Orchestrator
25
25
 
26
26
  [![Python Version](https://img.shields.io/badge/python-3.9+-blue.svg)](https://python.org)
27
27
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
28
28
  [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](http://makeapullrequest.com)
29
29
 
30
+ ![Screenshots](docs/images/screenshots.png)
31
+
30
32
  **model-compose** is an open-source, declarative workflow orchestrator inspired by `docker-compose`. It lets you define and run AI model pipelines using simple YAML files — no custom code required. Effortlessly connect external AI services or run local AI models, all within powerful, composable workflows.
31
33
 
32
34
  ---
33
35
 
34
36
  ## ✨ Features
35
37
 
36
- #### 🔧 Declarative by Design
37
- Define complete AI workflows using simple YAML files no complex scripting required.
38
-
39
- #### 🔗 Compose Anything
40
- Combine multiple AI models, APIs, and tools into a single, unified pipeline.
41
-
42
- #### 🧠 Built for Orchestration
43
- Orchestrate multi-step model interactions with ease. Turn individual API calls into maintainable, end-to-end systems using **model-compose**.
44
-
45
- #### 🛠️ Multi-Workflow Support
46
- Define multiple named workflows in one project. Run them by name or set a default for quick execution.
47
-
48
- #### 🧩 Modular Components
49
- Break down logic into reusable components and jobs. Easily plug, swap, and extend them across workflows.
50
-
51
- #### ⚙️ Flexible I/O Routing
52
- Connect inputs and outputs between jobs using clean, scoped variables - no glue code needed.
53
-
54
- #### 🧪 Run Locally, Serve Remotely
55
- Execute workflows from the CLI or expose them as HTTP or MCP endpoints with an optional Web UI.
56
-
57
- #### 📦 Works with Environment Variables
58
- Easily inject secrets and configuration via `.env` files or environment variables to keep your YAML clean and secure.
38
+ - **Declarative by Design:** Define complete AI workflows using simple YAML files—no complex scripting required.
39
+ - **Compose Anything:** Combine multiple AI models, APIs, and tools into a single, unified pipeline.
40
+ - **Built for Orchestration:** Orchestrate multi-step model interactions with ease. Transform individual API calls into maintainable, end-to-end systems.
41
+ - **Multi-Workflow Support:** Define multiple named workflows in one project. Run them by name or set a default for quick execution.
42
+ - **Modular Components:** Break down logic into reusable components and jobs. Easily plug, swap, and extend them across workflows.
43
+ - **Flexible I/O Routing:** Connect inputs and outputs between jobs using clean, scoped variables—no glue code needed.
44
+ - **Run Locally, Serve Remotely:** Execute workflows from the CLI or expose them as HTTP or MCP endpoints with an optional Web UI.
45
+ - **Environment Variable Support:** Easily inject secrets and configuration via `.env` files or environment variables to keep your YAML clean and secure.
59
46
 
60
47
  ---
61
48
 
@@ -214,14 +201,14 @@ listener:
214
201
  port: 8090
215
202
  base_path: /callbacks
216
203
  callbacks:
217
- - path: /chatgpt
204
+ - path: /chat-ai
218
205
  method: POST
219
206
  item: ${body.data}
220
207
  identify_by: ${item.task_id}
221
208
  result: ${item.choices[0].message.content}
222
209
  ```
223
210
 
224
- This listener sets up an HTTP callback endpoint at `http://localhost:8090/callbacks/chatgpt` to receive asynchronous responses from a remote ChatGPT-compatible service. It extracts the incoming data from the request body, identifies the task using `task_id`, and stores the final message content from the first choice in the response.
211
+ This listener sets up an HTTP callback endpoint at http://localhost:8090/callbacks/chat-ai to handle asynchronous responses from an external service that behaves like ChatGPT but supports delayed or push-based results. This is useful when integrating with services that notify results via webhook-style callbacks.
225
212
 
226
213
  #### 🌐 Gateway Example
227
214
 
@@ -234,7 +221,12 @@ gateway:
234
221
 
235
222
  This gateway configuration exposes the local listener defined above to the public internet using an HTTP tunnel powered by ngrok. It forwards incoming traffic from a secure, public URL (e.g., https://abc123.ngrok.io) directly to your local callback endpoint at http://localhost:8090. This is essential when integrating with third-party services that need to push data back to your workflow via webhooks or asynchronous callbacks.
236
223
 
237
- > 📁 For more example model-compose.yml configurations, check the samples directory in the source code.
224
+ > 📁 For more example model-compose.yml configurations, check the [examples directory](examples) in the source code.
225
+
226
+ ---
227
+ ## 🏗 Architecture
228
+
229
+ ![Archtecture Diagram](docs/images/architecture-diagram.png)
238
230
 
239
231
  ---
240
232
 
@@ -1,38 +1,25 @@
1
- # model-compose: Declarative AI Model and Workflow Orchestrator (_part of the Mindor project_)
1
+ # model-compose: Declarative AI Model and Workflow Orchestrator
2
2
 
3
3
  [![Python Version](https://img.shields.io/badge/python-3.9+-blue.svg)](https://python.org)
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
5
  [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](http://makeapullrequest.com)
6
6
 
7
+ ![Screenshots](docs/images/screenshots.png)
8
+
7
9
  **model-compose** is an open-source, declarative workflow orchestrator inspired by `docker-compose`. It lets you define and run AI model pipelines using simple YAML files — no custom code required. Effortlessly connect external AI services or run local AI models, all within powerful, composable workflows.
8
10
 
9
11
  ---
10
12
 
11
13
  ## ✨ Features
12
14
 
13
- #### 🔧 Declarative by Design
14
- Define complete AI workflows using simple YAML files no complex scripting required.
15
-
16
- #### 🔗 Compose Anything
17
- Combine multiple AI models, APIs, and tools into a single, unified pipeline.
18
-
19
- #### 🧠 Built for Orchestration
20
- Orchestrate multi-step model interactions with ease. Turn individual API calls into maintainable, end-to-end systems using **model-compose**.
21
-
22
- #### 🛠️ Multi-Workflow Support
23
- Define multiple named workflows in one project. Run them by name or set a default for quick execution.
24
-
25
- #### 🧩 Modular Components
26
- Break down logic into reusable components and jobs. Easily plug, swap, and extend them across workflows.
27
-
28
- #### ⚙️ Flexible I/O Routing
29
- Connect inputs and outputs between jobs using clean, scoped variables - no glue code needed.
30
-
31
- #### 🧪 Run Locally, Serve Remotely
32
- Execute workflows from the CLI or expose them as HTTP or MCP endpoints with an optional Web UI.
33
-
34
- #### 📦 Works with Environment Variables
35
- Easily inject secrets and configuration via `.env` files or environment variables to keep your YAML clean and secure.
15
+ - **Declarative by Design:** Define complete AI workflows using simple YAML files—no complex scripting required.
16
+ - **Compose Anything:** Combine multiple AI models, APIs, and tools into a single, unified pipeline.
17
+ - **Built for Orchestration:** Orchestrate multi-step model interactions with ease. Transform individual API calls into maintainable, end-to-end systems.
18
+ - **Multi-Workflow Support:** Define multiple named workflows in one project. Run them by name or set a default for quick execution.
19
+ - **Modular Components:** Break down logic into reusable components and jobs. Easily plug, swap, and extend them across workflows.
20
+ - **Flexible I/O Routing:** Connect inputs and outputs between jobs using clean, scoped variables—no glue code needed.
21
+ - **Run Locally, Serve Remotely:** Execute workflows from the CLI or expose them as HTTP or MCP endpoints with an optional Web UI.
22
+ - **Environment Variable Support:** Easily inject secrets and configuration via `.env` files or environment variables to keep your YAML clean and secure.
36
23
 
37
24
  ---
38
25
 
@@ -191,14 +178,14 @@ listener:
191
178
  port: 8090
192
179
  base_path: /callbacks
193
180
  callbacks:
194
- - path: /chatgpt
181
+ - path: /chat-ai
195
182
  method: POST
196
183
  item: ${body.data}
197
184
  identify_by: ${item.task_id}
198
185
  result: ${item.choices[0].message.content}
199
186
  ```
200
187
 
201
- This listener sets up an HTTP callback endpoint at `http://localhost:8090/callbacks/chatgpt` to receive asynchronous responses from a remote ChatGPT-compatible service. It extracts the incoming data from the request body, identifies the task using `task_id`, and stores the final message content from the first choice in the response.
188
+ This listener sets up an HTTP callback endpoint at http://localhost:8090/callbacks/chat-ai to handle asynchronous responses from an external service that behaves like ChatGPT but supports delayed or push-based results. This is useful when integrating with services that notify results via webhook-style callbacks.
202
189
 
203
190
  #### 🌐 Gateway Example
204
191
 
@@ -211,7 +198,12 @@ gateway:
211
198
 
212
199
  This gateway configuration exposes the local listener defined above to the public internet using an HTTP tunnel powered by ngrok. It forwards incoming traffic from a secure, public URL (e.g., https://abc123.ngrok.io) directly to your local callback endpoint at http://localhost:8090. This is essential when integrating with third-party services that need to push data back to your workflow via webhooks or asynchronous callbacks.
213
200
 
214
- > 📁 For more example model-compose.yml configurations, check the samples directory in the source code.
201
+ > 📁 For more example model-compose.yml configurations, check the [examples directory](examples) in the source code.
202
+
203
+ ---
204
+ ## 🏗 Architecture
205
+
206
+ ![Archtecture Diagram](docs/images/architecture-diagram.png)
215
207
 
216
208
  ---
217
209
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "model-compose"
3
- version = "0.1.0"
3
+ version = "0.1.2"
4
4
  description = "model-compose: Declarative AI Model and Workflow Orchestrator (part of the Mindor project)"
5
5
  authors = [
6
6
  { name = "Hanyeol Cho", email = "hanyeol.cho@gmail.com" }
@@ -10,8 +10,9 @@ from datetime import datetime, timezone
10
10
  import asyncio
11
11
 
12
12
  class HttpClientPollingCompletion:
13
- def __init__(self, base_url: Optional[str], config: HttpClientCompletionConfig):
13
+ def __init__(self, base_url: Optional[str], headers: Optional[Dict[str, str]], config: HttpClientCompletionConfig):
14
14
  self.base_url: Optional[str] = base_url
15
+ self.headers: Optional[Dict[str, str]] = headers
15
16
  self.config: HttpClientCompletionConfig = config
16
17
  self.client: HttpClient = HttpClient()
17
18
 
@@ -20,7 +21,7 @@ class HttpClientPollingCompletion:
20
21
  method = await context.render_template(self.config.method)
21
22
  params = await context.render_template(self.config.params)
22
23
  body = await context.render_template(self.config.body)
23
- headers = await context.render_template(self.config.headers)
24
+ headers = await context.render_template({ **self.headers, **self.config.headers })
24
25
 
25
26
  interval = parse_duration(self.config.interval) if self.config.interval else 5.0
26
27
  timeout = parse_duration(self.config.timeout) if self.config.timeout else 300
@@ -30,8 +31,8 @@ class HttpClientPollingCompletion:
30
31
 
31
32
  while datetime.now(timezone.utc) < deadline:
32
33
  response = await self.client.request(url, method, params, body, headers)
33
-
34
34
  context.register_source("result", response)
35
+
35
36
  status = (await context.render_template(self.config.status)) if self.config.status else response["status"]
36
37
 
37
38
  if not status:
@@ -67,8 +68,9 @@ class HttpClientCallbackCompletion:
67
68
  return await future
68
69
 
69
70
  class HttpClientAction:
70
- def __init__(self, base_url: Optional[str], config: HttpClientActionConfig):
71
+ def __init__(self, base_url: Optional[str], headers: Optional[Dict[str, str]], config: HttpClientActionConfig):
71
72
  self.base_url: Optional[str] = base_url
73
+ self.headers: Optional[Dict[str, str]] = headers
72
74
  self.config: HttpClientActionConfig = config
73
75
  self.client: HttpClient = HttpClient()
74
76
 
@@ -77,18 +79,14 @@ class HttpClientAction:
77
79
  method = await context.render_template(self.config.method)
78
80
  params = await context.render_template(self.config.params)
79
81
  body = await context.render_template(self.config.body)
80
- headers = await context.render_template(self.config.headers)
82
+ headers = await context.render_template({ **self.headers, **self.config.headers })
81
83
 
82
84
  response, result = await self.client.request(url, method, params, body, headers), None
83
-
84
- if response:
85
- context.register_source("response", response)
85
+ context.register_source("response", response)
86
86
 
87
87
  if self.config.completion:
88
88
  result = await self._handle_completion(self.config.completion, context)
89
-
90
- if result:
91
- context.register_source("result", result)
89
+ context.register_source("result", result)
92
90
 
93
91
  return (await context.render_template(self.config.output)) if self.config.output else (result or response)
94
92
 
@@ -100,7 +98,7 @@ class HttpClientAction:
100
98
 
101
99
  async def _handle_completion(self, completion: HttpClientCompletionConfig, context: ComponentContext) -> Any:
102
100
  if completion.type == "polling":
103
- return await HttpClientPollingCompletion(self.base_url, completion).run(context)
101
+ return await HttpClientPollingCompletion(self.base_url, self.headers, completion).run(context)
104
102
 
105
103
  if completion.type == "callback":
106
104
  return await HttpClientCallbackCompletion(completion).run(context)
@@ -118,6 +116,6 @@ class HttpClientComponent(ComponentEngine):
118
116
  pass
119
117
 
120
118
  async def _run(self, action: ActionConfig, context: ComponentContext) -> Any:
121
- return await HttpClientAction(self.config.base_url, action).run(context)
119
+ return await HttpClientAction(self.config.base_url, self.config.headers, action).run(context)
122
120
 
123
121
  ComponentEngineMap[ComponentType.HTTP_CLIENT] = HttpClientComponent
@@ -109,7 +109,8 @@ class HttpServerController(ControllerEngine):
109
109
  await self.server.serve()
110
110
 
111
111
  async def _shutdown(self) -> None:
112
- self.server.should_exit = True
112
+ if self.server:
113
+ self.server.should_exit = True
113
114
 
114
115
  async def _parse_workflow_body(self, request: Request) -> WorkflowTaskRequestBody:
115
116
  content_type, _ = parse_options_header(request.headers, "Content-Type")
@@ -1,23 +1,29 @@
1
1
  from typing import Type, Union, Literal, Optional, Dict, List, Tuple, Set, Annotated, Callable, Awaitable, Any
2
- from mindor.dsl.schema.workflow import WorkflowVariableConfig
2
+ from mindor.dsl.schema.workflow import WorkflowVariableConfig, WorkflowVariableGroupConfig
3
3
  from mindor.core.utils.http_client import HttpStreamResource
4
4
  from mindor.core.utils.image import load_image_from_stream
5
5
  from mindor.core.utils.streaming import Base64StreamResource, save_stream_to_temporary_file
6
6
  from .schema import WorkflowSchema
7
7
  import gradio as gr
8
+ import json
9
+
10
+ class ComponentGroup:
11
+ def __init__(self, group: gr.Component, components: List[gr.Component]):
12
+ self.group: gr.Component = group
13
+ self.components: List[gr.Component] = components
8
14
 
9
15
  class GradioWebUIBuilder:
10
16
  def build(self, schema: Dict[str, WorkflowSchema], runner: Callable[[Optional[str], Any], Awaitable[Any]]) -> gr.Blocks:
11
17
  with gr.Blocks() as blocks:
12
18
  for workflow_id, workflow in schema.items():
13
- async def run_workflow(input: Any, workflow_id=workflow_id) -> Any:
19
+ async def _run_workflow(input: Any, workflow_id=workflow_id) -> Any:
14
20
  return await runner(workflow_id, input)
15
21
 
16
22
  if len(schema) > 1:
17
23
  with gr.Tab(label=workflow.name or workflow_id):
18
- self._build_workflow_section(workflow, run_workflow)
24
+ self._build_workflow_section(workflow, _run_workflow)
19
25
  else:
20
- self._build_workflow_section(workflow, run_workflow)
26
+ self._build_workflow_section(workflow, _run_workflow)
21
27
 
22
28
  return blocks
23
29
 
@@ -38,24 +44,16 @@ class GradioWebUIBuilder:
38
44
  if not output_components:
39
45
  output_components = gr.Textbox(label="", lines=8, interactive=False, show_copy_button=True)
40
46
 
41
- async def run_workflow(*args):
42
- input = { variable.name: value for variable, value in zip(workflow.input, args) }
47
+ async def _run_workflow(*args):
48
+ input = { variable.name: value if value != "" else None for variable, value in zip(workflow.input, args) }
43
49
  output = await runner(input)
44
-
45
- if workflow.output:
46
- if isinstance(output, dict):
47
- output = [ await self._convert_type(output[variable.name], variable.type, variable.subtype, variable.format) for variable in workflow.output ]
48
- output = output[0] if len(output) == 1 else output
49
- else:
50
- variable = workflow.output[0]
51
- output = await self._convert_type(output, variable.type, variable.subtype, variable.format)
52
-
53
- return output
50
+ output = await self._flatten_output(output, workflow.output)
51
+ return output[0] if len(output) == 1 else output
54
52
 
55
53
  run_button.click(
56
- fn=run_workflow,
54
+ fn=_run_workflow,
57
55
  inputs=input_components,
58
- outputs=output_components
56
+ outputs=self._flatten_output_components(output_components)
59
57
  )
60
58
 
61
59
  return section
@@ -85,7 +83,16 @@ class GradioWebUIBuilder:
85
83
 
86
84
  return gr.Textbox(label=label, value=default, info=f"Unsupported type: {variable.type}")
87
85
 
88
- def _build_output_component(self, variable: WorkflowVariableConfig) -> gr.Component:
86
+ def _build_output_component(self, variable: Union[WorkflowVariableConfig, WorkflowVariableGroupConfig]) -> Union[gr.Component, List[ComponentGroup]]:
87
+ if isinstance(variable, WorkflowVariableGroupConfig):
88
+ groups: List[ComponentGroup] = []
89
+ for index in range(variable.repeat_count if variable.repeat_count != 0 else 100):
90
+ visible = True if variable.repeat_count != 0 or index == 0 else False
91
+ with gr.Column(visible=visible) as group:
92
+ components = [ self._build_output_component(v) for v in variable.variables ]
93
+ groups.append(ComponentGroup(group, components))
94
+ return groups
95
+
89
96
  label = variable.name or ""
90
97
  info = variable.description or ""
91
98
 
@@ -100,7 +107,34 @@ class GradioWebUIBuilder:
100
107
 
101
108
  return gr.Textbox(label=label, info=f"Unsupported type: {variable.type}")
102
109
 
110
+ def _flatten_output_components(self, components: List[Union[gr.Component, List[ComponentGroup]]]) -> List[gr.Component]:
111
+ flattened = []
112
+ for item in components:
113
+ if isinstance(item, list):
114
+ for group in item:
115
+ flattened.extend(group.components)
116
+ else:
117
+ flattened.append(item)
118
+ return flattened
119
+
120
+ async def _flatten_output(self, output: Any, variables: List[Union[WorkflowVariableConfig, WorkflowVariableGroupConfig]]) -> Any:
121
+ flattened = []
122
+ for variable in variables:
123
+ if isinstance(variable, WorkflowVariableGroupConfig):
124
+ group = output[variable.name] if variable.name else output
125
+ for value in group:
126
+ flattened.extend(await self._flatten_output(value, variable.variables))
127
+ else:
128
+ value = output[variable.name] if variable.name else output
129
+ flattened.append(await self._convert_type(value, variable.type, variable.subtype, variable.format))
130
+ return flattened
131
+
103
132
  async def _convert_type(self, value: Any, type: Optional[str], subtype: Optional[str], format: Optional[str]) -> Any:
133
+ if type == "string":
134
+ if isinstance(value, (dict, list)):
135
+ return json.dumps(value)
136
+ return str(value)
137
+
104
138
  if type == "image":
105
139
  if format == "base64" and isinstance(value, str):
106
140
  return await load_image_from_stream(Base64StreamResource(value), subtype)
@@ -0,0 +1,198 @@
1
+ from typing import Type, Union, Literal, Optional, Dict, List, Tuple, Set, Annotated, Any
2
+ from pydantic import BaseModel
3
+ from mindor.dsl.schema.workflow import WorkflowConfig, WorkflowVariableConfig, WorkflowVariableGroupConfig
4
+ from mindor.dsl.schema.component import ComponentConfig
5
+ import re, json
6
+
7
+ class WorkflowVariable:
8
+ def __init__(self, name: Optional[str], type: str, subtype: Optional[str], format: Optional[str], default: Optional[Any]):
9
+ self.name: Optional[str] = name
10
+ self.type: str = type
11
+ self.subtype: Optional[str] = subtype
12
+ self.format: Optional[str] = format
13
+ self.default: Optional[Any] = default
14
+
15
+ def to_dict(self) -> Dict[str, Any]:
16
+ return {
17
+ "name": self.name,
18
+ "type": self.type,
19
+ "subtype": self.subtype,
20
+ "format": self.format,
21
+ "default": self.default
22
+ }
23
+
24
+ def __eq__(self, other):
25
+ if not isinstance(other, WorkflowVariable):
26
+ return False
27
+ if self.name is None or other.name is None:
28
+ return False
29
+ return self.name == other.name
30
+
31
+ def __hash__(self):
32
+ return hash(self.name) if self.name is not None else id(self)
33
+
34
+ class WorkflowVariableGroup:
35
+ def __init__(self, name: Optional[str], variables: List[WorkflowVariable], repeat_count: int):
36
+ self.name: Optional[str] = name
37
+ self.variables: List[WorkflowVariable] = variables
38
+ self.repeat_count: int = repeat_count
39
+
40
+ class WorkflowVariableResolver:
41
+ def __init__(self):
42
+ self.patterns: Dict[str, re.Pattern] = {
43
+ "variable": re.compile(
44
+ r"""\$\{ # ${
45
+ ([a-zA-Z_][^.\s]*) # key: input, env, etc.
46
+ (?:\.([^\s\|\}]+))? # path: key, key.path[0], etc.
47
+ (?:\s*as\s*([^\s\|\}/;]+)(?:/([^\s\|\};]+))?(?:;([^\s\|\}]+))?)? # type/subtype;format
48
+ (?:\s*\|\s*([^\}]+))? # default value after `|`
49
+ \}""", # }
50
+ re.VERBOSE,
51
+ ),
52
+ "keypath": re.compile(r"[-_\w]+|\[\d+\]"),
53
+ }
54
+
55
+ def _enumerate_input_variables(self, value: Any, wanted_key: str) -> List[WorkflowVariable]:
56
+ if isinstance(value, str):
57
+ variables: List[WorkflowVariable] = []
58
+
59
+ for match in self.patterns["variable"].finditer(value):
60
+ key, path, type, subtype, format, default = match.group(1, 2, 3, 4, 5, 6)
61
+
62
+ if type and default:
63
+ default = self._parse_as_type(default, type)
64
+
65
+ if key == wanted_key:
66
+ variables.append(WorkflowVariable(path, type or "string", subtype, format, default))
67
+
68
+ return variables
69
+
70
+ if isinstance(value, BaseModel):
71
+ return self._enumerate_input_variables(value.model_dump(exclude_none=True), wanted_key)
72
+
73
+ if isinstance(value, dict):
74
+ return sum([ self._enumerate_input_variables(v, wanted_key) for v in value.values() ], [])
75
+
76
+ if isinstance(value, list):
77
+ return sum([ self._enumerate_input_variables(v, wanted_key) for v in value ], [])
78
+
79
+ return []
80
+
81
+ def _enumerate_output_variables(self, name: Optional[str], value: Any) -> List[WorkflowVariable]:
82
+ variables: List[WorkflowVariable] = []
83
+
84
+ if isinstance(value, str):
85
+ for match in self.patterns["variable"].finditer(value):
86
+ key, path, type, subtype, format, default = match.group(1, 2, 3, 4, 5, 6)
87
+
88
+ if type and default:
89
+ default = self._parse_as_type(default, type)
90
+
91
+ variables.append(WorkflowVariable(name, type or "string", subtype, format, default))
92
+
93
+ return variables
94
+
95
+ if isinstance(value, BaseModel):
96
+ return self._enumerate_output_variables(name, value.model_dump(exclude_none=True))
97
+
98
+ if isinstance(value, dict):
99
+ return sum([ self._enumerate_output_variables(f"{name}.{k}" if name else f"{k}", v) for k, v in value.items() ], [])
100
+
101
+ if isinstance(value, list):
102
+ return sum([ self._enumerate_output_variables(f"{name}[{i}]" if name else f"[{i}]", v) for i, v in enumerate(value) ], [])
103
+
104
+ return []
105
+
106
+ def _to_variable_config_list(self, variables: List[Union[WorkflowVariable, WorkflowVariableGroup]]) -> List[Union[WorkflowVariableConfig, WorkflowVariableGroupConfig]]:
107
+ configs: List[Union[WorkflowVariableConfig, WorkflowVariableGroupConfig]] = []
108
+ seen_single: Set[WorkflowVariable] = set()
109
+
110
+ for item in variables:
111
+ if isinstance(item, WorkflowVariableGroup):
112
+ group: List[WorkflowVariableConfig] = []
113
+ seen_in_group: Set[WorkflowVariable] = set()
114
+ for v in item.variables:
115
+ if v not in seen_in_group:
116
+ seen_in_group.add(v)
117
+ group.append(WorkflowVariableConfig(**v.to_dict()))
118
+ configs.append(WorkflowVariableGroupConfig(name=item.name, variables=group, repeat_count=item.repeat_count))
119
+ else:
120
+ if item not in seen_single:
121
+ seen_single.add(item)
122
+ configs.append(WorkflowVariableConfig(**item.to_dict()))
123
+
124
+ return configs
125
+
126
+ def _parse_as_type(self, value: Any, type: str) -> Any:
127
+ if type == "number":
128
+ return float(value)
129
+
130
+ if type == "integer":
131
+ return int(value)
132
+
133
+ if type == "boolean":
134
+ return str(value).lower() in [ "true", "1" ]
135
+
136
+ if type == "json":
137
+ return json.loads(value)
138
+
139
+ return value
140
+
141
+ class WorkflowInputResolver(WorkflowVariableResolver):
142
+ def resolve(self, workflow: WorkflowConfig, components: Dict[str, ComponentConfig]) -> List[WorkflowVariableConfig]:
143
+ variables: List[WorkflowVariable] = []
144
+
145
+ for job in workflow.jobs.values():
146
+ if not job.input or job.input == "${input}":
147
+ action_id = job.action or "__default__"
148
+ if isinstance(job.component, str):
149
+ component: Optional[ComponentConfig] = components[job.component] if job.component in components else None
150
+ if component:
151
+ variables.extend(self._enumerate_input_variables(component.actions[action_id], "input"))
152
+ else:
153
+ variables.extend(self._enumerate_input_variables(job.component.actions[action_id], "input"))
154
+ else:
155
+ variables.extend(self._enumerate_input_variables(job.input, "input"))
156
+
157
+ for value in [ job.repeat_count ]:
158
+ variables.extend(self._enumerate_input_variables(value, "input"))
159
+
160
+ return self._to_variable_config_list(variables)
161
+
162
+ class WorkflowOutputResolver(WorkflowVariableResolver):
163
+ def resolve(self, workflow: WorkflowConfig, components: Dict[str, ComponentConfig]) -> List[Union[WorkflowVariableConfig, WorkflowVariableGroupConfig]]:
164
+ variables: List[Union[WorkflowVariable, WorkflowVariableGroup]] = []
165
+
166
+ for job_id, job in workflow.jobs.items():
167
+ if not self._is_terminal_job(workflow, job_id):
168
+ continue
169
+
170
+ job_variables: List[WorkflowVariable] = variables
171
+ repeat_count: int = job.repeat_count if isinstance(job.repeat_count, int) else 0
172
+
173
+ if repeat_count != 1:
174
+ variables.append(WorkflowVariableGroup(None, job_variables := [], repeat_count))
175
+
176
+ if not job.output or job.output == "${output}":
177
+ action_id = job.action or "__default__"
178
+ if isinstance(job.component, str):
179
+ component: Optional[ComponentConfig] = components[job.component] if job.component in components else None
180
+ if component:
181
+ job_variables.extend(self._enumerate_output_variables(None, component.actions[action_id].output))
182
+ else:
183
+ job_variables.extend(self._enumerate_output_variables(None, job.component.actions[action_id].output))
184
+ else:
185
+ job_variables.extend(self._enumerate_output_variables(None, job.output))
186
+
187
+ return self._to_variable_config_list(variables)
188
+
189
+ def _is_terminal_job(self, workflow: WorkflowConfig, job_id: str) -> bool:
190
+ return all(job_id not in job.depends_on for other_id, job in workflow.jobs.items() if other_id != job_id)
191
+
192
+ class WorkflowSchema:
193
+ def __init__(self, name: str, title: str, description: Optional[str], input: List[WorkflowVariableConfig], output: List[Union[WorkflowVariableConfig, WorkflowVariableGroupConfig]]):
194
+ self.name: str = name
195
+ self.title: str = title
196
+ self.description: Optional[str] = description
197
+ self.input: List[WorkflowVariableConfig] = input
198
+ self.output: List[Union[WorkflowVariableConfig, WorkflowVariableGroupConfig]] = output
@@ -54,7 +54,8 @@ class ControllerWebUI(AsyncService):
54
54
  await self.server.serve()
55
55
 
56
56
  async def _shutdown(self) -> None:
57
- self.server.should_exit = True
57
+ if self.server:
58
+ self.server.should_exit = True
58
59
 
59
60
  async def _run_workflow(self, workflow_id: Optional[str], input: Any) -> Any:
60
61
  return await self.runner.run_workflow(workflow_id, input)
@@ -66,7 +66,7 @@ class HttpCallbackListener(ListenerEngine):
66
66
  )
67
67
 
68
68
  def _make_callback_handler(self, callback: HttpCallbackConfig) -> Callable:
69
- async def handler(request: Request) -> Response:
69
+ async def _handler(request: Request) -> Response:
70
70
  content_type, _ = parse_options_header(request.headers, "Content-Type")
71
71
  body, query = await parse_request_body(request, content_type), request.query_params
72
72
  context: HttpCallbackContext = HttpCallbackContext(body, query, callback.bulk, callback.item)
@@ -85,7 +85,7 @@ class HttpCallbackListener(ListenerEngine):
85
85
 
86
86
  return Response()
87
87
 
88
- return handler
88
+ return _handler
89
89
 
90
90
  async def _is_callback_succeeded(self, callback: HttpCallbackConfig, context: HttpCallbackContext) -> bool:
91
91
  status = (await context.render_template(callback.status)) if callback.status else None
@@ -109,7 +109,8 @@ class HttpCallbackListener(ListenerEngine):
109
109
  await self.server.serve()
110
110
 
111
111
  async def _shutdown(self) -> None:
112
- self.server.should_exit = True
112
+ if self.server:
113
+ self.server.should_exit = True
113
114
 
114
115
  def _get_pending_future(self, id: str) -> Optional[asyncio.Future]:
115
116
  with _pending_futures_lock:
@@ -39,11 +39,11 @@ class HttpClient:
39
39
  headers=headers
40
40
  )
41
41
 
42
- if response.status >= 400:
43
- raise ValueError(f"Request failed with status {response.status}")
44
-
45
42
  content = await self._parse_response_content(session, response)
46
43
 
44
+ if response.status >= 400:
45
+ raise ValueError(f"Request failed with status {response.status}: {content}")
46
+
47
47
  if not isinstance(content, HttpStreamResource):
48
48
  await session.close()
49
49