model-compose 0.0.16__tar.gz → 0.1.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 (119) hide show
  1. {model_compose-0.0.16 → model_compose-0.1.0}/PKG-INFO +2 -1
  2. {model_compose-0.0.16 → model_compose-0.1.0}/pyproject.toml +3 -2
  3. {model_compose-0.0.16 → model_compose-0.1.0}/src/mindor/core/component/engine/context.py +3 -3
  4. {model_compose-0.0.16 → model_compose-0.1.0}/src/mindor/core/component/engine/http_client.py +21 -21
  5. {model_compose-0.0.16 → model_compose-0.1.0}/src/mindor/core/controller/engine/http_server.py +5 -5
  6. {model_compose-0.0.16 → model_compose-0.1.0}/src/mindor/core/controller/webui/gradio.py +35 -19
  7. model_compose-0.1.0/src/mindor/core/controller/webui/schema.py +135 -0
  8. {model_compose-0.0.16 → model_compose-0.1.0}/src/mindor/core/listener/engine/http_callback.py +16 -16
  9. {model_compose-0.0.16 → model_compose-0.1.0}/src/mindor/core/utils/http_client.py +3 -4
  10. {model_compose-0.0.16 → model_compose-0.1.0}/src/mindor/core/utils/http_request.py +1 -2
  11. model_compose-0.1.0/src/mindor/core/utils/image.py +14 -0
  12. model_compose-0.1.0/src/mindor/core/utils/streaming.py +63 -0
  13. model_compose-0.1.0/src/mindor/core/utils/template.py +100 -0
  14. {model_compose-0.0.16 → model_compose-0.1.0}/src/mindor/core/workflow/context.py +3 -3
  15. {model_compose-0.0.16 → model_compose-0.1.0}/src/mindor/core/workflow/job.py +2 -2
  16. {model_compose-0.0.16 → model_compose-0.1.0}/src/mindor/dsl/schema/controller/impl/http_server.py +1 -0
  17. {model_compose-0.0.16 → model_compose-0.1.0}/src/mindor/dsl/schema/workflow.py +27 -2
  18. {model_compose-0.0.16 → model_compose-0.1.0}/src/model_compose.egg-info/PKG-INFO +2 -1
  19. {model_compose-0.0.16 → model_compose-0.1.0}/src/model_compose.egg-info/SOURCES.txt +1 -0
  20. {model_compose-0.0.16 → model_compose-0.1.0}/src/model_compose.egg-info/requires.txt +1 -0
  21. model_compose-0.0.16/src/mindor/core/controller/webui/schema.py +0 -115
  22. model_compose-0.0.16/src/mindor/core/utils/streaming.py +0 -28
  23. model_compose-0.0.16/src/mindor/core/utils/template.py +0 -94
  24. {model_compose-0.0.16 → model_compose-0.1.0}/LICENSE +0 -0
  25. {model_compose-0.0.16 → model_compose-0.1.0}/README.md +0 -0
  26. {model_compose-0.0.16 → model_compose-0.1.0}/setup.cfg +0 -0
  27. {model_compose-0.0.16 → model_compose-0.1.0}/src/mindor/__init__.py +0 -0
  28. {model_compose-0.0.16 → model_compose-0.1.0}/src/mindor/cli/__init__.py +0 -0
  29. {model_compose-0.0.16 → model_compose-0.1.0}/src/mindor/cli/compose.py +0 -0
  30. {model_compose-0.0.16 → model_compose-0.1.0}/src/mindor/core/__init__.py +0 -0
  31. {model_compose-0.0.16 → model_compose-0.1.0}/src/mindor/core/component/__init__.py +0 -0
  32. {model_compose-0.0.16 → model_compose-0.1.0}/src/mindor/core/component/component.py +0 -0
  33. {model_compose-0.0.16 → model_compose-0.1.0}/src/mindor/core/component/engine/__init__.py +0 -0
  34. {model_compose-0.0.16 → model_compose-0.1.0}/src/mindor/core/component/engine/base.py +0 -0
  35. {model_compose-0.0.16 → model_compose-0.1.0}/src/mindor/core/component/engine/http_server.py +0 -0
  36. {model_compose-0.0.16 → model_compose-0.1.0}/src/mindor/core/compose/__init__.py +0 -0
  37. {model_compose-0.0.16 → model_compose-0.1.0}/src/mindor/core/compose/compose.py +0 -0
  38. {model_compose-0.0.16 → model_compose-0.1.0}/src/mindor/core/compose/manager.py +0 -0
  39. {model_compose-0.0.16 → model_compose-0.1.0}/src/mindor/core/controller/__init__.py +0 -0
  40. {model_compose-0.0.16 → model_compose-0.1.0}/src/mindor/core/controller/controller.py +0 -0
  41. {model_compose-0.0.16 → model_compose-0.1.0}/src/mindor/core/controller/engine/__init__.py +0 -0
  42. {model_compose-0.0.16 → model_compose-0.1.0}/src/mindor/core/controller/engine/base.py +0 -0
  43. {model_compose-0.0.16 → model_compose-0.1.0}/src/mindor/core/controller/engine/mcp_server.py +0 -0
  44. {model_compose-0.0.16 → model_compose-0.1.0}/src/mindor/core/controller/runner/__init__.py +0 -0
  45. {model_compose-0.0.16 → model_compose-0.1.0}/src/mindor/core/controller/runner/client.py +0 -0
  46. {model_compose-0.0.16 → model_compose-0.1.0}/src/mindor/core/controller/runner/http_client.py +0 -0
  47. {model_compose-0.0.16 → model_compose-0.1.0}/src/mindor/core/controller/runner/mcp_client.py +0 -0
  48. {model_compose-0.0.16 → model_compose-0.1.0}/src/mindor/core/controller/runner/runner.py +0 -0
  49. {model_compose-0.0.16 → model_compose-0.1.0}/src/mindor/core/controller/webui/__init__.py +0 -0
  50. {model_compose-0.0.16 → model_compose-0.1.0}/src/mindor/core/controller/webui/webui.py +0 -0
  51. {model_compose-0.0.16 → model_compose-0.1.0}/src/mindor/core/gateway/__init__.py +0 -0
  52. {model_compose-0.0.16 → model_compose-0.1.0}/src/mindor/core/gateway/engine/__init__.py +0 -0
  53. {model_compose-0.0.16 → model_compose-0.1.0}/src/mindor/core/gateway/engine/base.py +0 -0
  54. {model_compose-0.0.16 → model_compose-0.1.0}/src/mindor/core/gateway/engine/http_tunnel.py +0 -0
  55. {model_compose-0.0.16 → model_compose-0.1.0}/src/mindor/core/gateway/gateway.py +0 -0
  56. {model_compose-0.0.16 → model_compose-0.1.0}/src/mindor/core/listener/__init__.py +0 -0
  57. {model_compose-0.0.16 → model_compose-0.1.0}/src/mindor/core/listener/engine/__init__.py +0 -0
  58. {model_compose-0.0.16 → model_compose-0.1.0}/src/mindor/core/listener/engine/base.py +0 -0
  59. {model_compose-0.0.16 → model_compose-0.1.0}/src/mindor/core/listener/listener.py +0 -0
  60. {model_compose-0.0.16 → model_compose-0.1.0}/src/mindor/core/runtime/__init__.py +0 -0
  61. {model_compose-0.0.16 → model_compose-0.1.0}/src/mindor/core/runtime/env.py +0 -0
  62. {model_compose-0.0.16 → model_compose-0.1.0}/src/mindor/core/services/__init__.py +0 -0
  63. {model_compose-0.0.16 → model_compose-0.1.0}/src/mindor/core/services/async_service.py +0 -0
  64. {model_compose-0.0.16 → model_compose-0.1.0}/src/mindor/core/utils/__init__.py +0 -0
  65. {model_compose-0.0.16 → model_compose-0.1.0}/src/mindor/core/utils/expiring.py +0 -0
  66. {model_compose-0.0.16 → model_compose-0.1.0}/src/mindor/core/utils/time.py +0 -0
  67. {model_compose-0.0.16 → model_compose-0.1.0}/src/mindor/core/utils/workqueue.py +0 -0
  68. {model_compose-0.0.16 → model_compose-0.1.0}/src/mindor/core/workflow/__init__.py +0 -0
  69. {model_compose-0.0.16 → model_compose-0.1.0}/src/mindor/core/workflow/workflow.py +0 -0
  70. {model_compose-0.0.16 → model_compose-0.1.0}/src/mindor/dsl/__init__.py +0 -0
  71. {model_compose-0.0.16 → model_compose-0.1.0}/src/mindor/dsl/loader.py +0 -0
  72. {model_compose-0.0.16 → model_compose-0.1.0}/src/mindor/dsl/schema/__init__.py +0 -0
  73. {model_compose-0.0.16 → model_compose-0.1.0}/src/mindor/dsl/schema/action/__init__.py +0 -0
  74. {model_compose-0.0.16 → model_compose-0.1.0}/src/mindor/dsl/schema/action/action.py +0 -0
  75. {model_compose-0.0.16 → model_compose-0.1.0}/src/mindor/dsl/schema/action/impl/__init__.py +0 -0
  76. {model_compose-0.0.16 → model_compose-0.1.0}/src/mindor/dsl/schema/action/impl/common.py +0 -0
  77. {model_compose-0.0.16 → model_compose-0.1.0}/src/mindor/dsl/schema/action/impl/http_client.py +0 -0
  78. {model_compose-0.0.16 → model_compose-0.1.0}/src/mindor/dsl/schema/action/impl/http_server.py +0 -0
  79. {model_compose-0.0.16 → model_compose-0.1.0}/src/mindor/dsl/schema/action/impl/mcp_client.py +0 -0
  80. {model_compose-0.0.16 → model_compose-0.1.0}/src/mindor/dsl/schema/action/impl/mcp_server.py +0 -0
  81. {model_compose-0.0.16 → model_compose-0.1.0}/src/mindor/dsl/schema/component/__init__.py +0 -0
  82. {model_compose-0.0.16 → model_compose-0.1.0}/src/mindor/dsl/schema/component/component.py +0 -0
  83. {model_compose-0.0.16 → model_compose-0.1.0}/src/mindor/dsl/schema/component/impl/__init__.py +0 -0
  84. {model_compose-0.0.16 → model_compose-0.1.0}/src/mindor/dsl/schema/component/impl/common.py +0 -0
  85. {model_compose-0.0.16 → model_compose-0.1.0}/src/mindor/dsl/schema/component/impl/http_client.py +0 -0
  86. {model_compose-0.0.16 → model_compose-0.1.0}/src/mindor/dsl/schema/component/impl/http_server.py +0 -0
  87. {model_compose-0.0.16 → model_compose-0.1.0}/src/mindor/dsl/schema/component/impl/mcp_client.py +0 -0
  88. {model_compose-0.0.16 → model_compose-0.1.0}/src/mindor/dsl/schema/component/impl/mcp_server.py +0 -0
  89. {model_compose-0.0.16 → model_compose-0.1.0}/src/mindor/dsl/schema/component/impl/types.py +0 -0
  90. {model_compose-0.0.16 → model_compose-0.1.0}/src/mindor/dsl/schema/compose.py +0 -0
  91. {model_compose-0.0.16 → model_compose-0.1.0}/src/mindor/dsl/schema/controller/__init__.py +0 -0
  92. {model_compose-0.0.16 → model_compose-0.1.0}/src/mindor/dsl/schema/controller/controller.py +0 -0
  93. {model_compose-0.0.16 → model_compose-0.1.0}/src/mindor/dsl/schema/controller/impl/__init__.py +0 -0
  94. {model_compose-0.0.16 → model_compose-0.1.0}/src/mindor/dsl/schema/controller/impl/common.py +0 -0
  95. {model_compose-0.0.16 → model_compose-0.1.0}/src/mindor/dsl/schema/controller/impl/mcp_server.py +0 -0
  96. {model_compose-0.0.16 → model_compose-0.1.0}/src/mindor/dsl/schema/controller/impl/types.py +0 -0
  97. {model_compose-0.0.16 → model_compose-0.1.0}/src/mindor/dsl/schema/controller/impl/webui.py +0 -0
  98. {model_compose-0.0.16 → model_compose-0.1.0}/src/mindor/dsl/schema/gateway/__init__.py +0 -0
  99. {model_compose-0.0.16 → model_compose-0.1.0}/src/mindor/dsl/schema/gateway/gateway.py +0 -0
  100. {model_compose-0.0.16 → model_compose-0.1.0}/src/mindor/dsl/schema/gateway/impl/__init__.py +0 -0
  101. {model_compose-0.0.16 → model_compose-0.1.0}/src/mindor/dsl/schema/gateway/impl/common.py +0 -0
  102. {model_compose-0.0.16 → model_compose-0.1.0}/src/mindor/dsl/schema/gateway/impl/http_tunnel.py +0 -0
  103. {model_compose-0.0.16 → model_compose-0.1.0}/src/mindor/dsl/schema/gateway/impl/types.py +0 -0
  104. {model_compose-0.0.16 → model_compose-0.1.0}/src/mindor/dsl/schema/listener/__init__.py +0 -0
  105. {model_compose-0.0.16 → model_compose-0.1.0}/src/mindor/dsl/schema/listener/impl/__init__.py +0 -0
  106. {model_compose-0.0.16 → model_compose-0.1.0}/src/mindor/dsl/schema/listener/impl/common.py +0 -0
  107. {model_compose-0.0.16 → model_compose-0.1.0}/src/mindor/dsl/schema/listener/impl/http_callback.py +0 -0
  108. {model_compose-0.0.16 → model_compose-0.1.0}/src/mindor/dsl/schema/listener/impl/types.py +0 -0
  109. {model_compose-0.0.16 → model_compose-0.1.0}/src/mindor/dsl/schema/listener/listener.py +0 -0
  110. {model_compose-0.0.16 → model_compose-0.1.0}/src/mindor/dsl/schema/runtime/__init__.py +0 -0
  111. {model_compose-0.0.16 → model_compose-0.1.0}/src/mindor/dsl/schema/runtime/impl/__init__.py +0 -0
  112. {model_compose-0.0.16 → model_compose-0.1.0}/src/mindor/dsl/schema/runtime/impl/common.py +0 -0
  113. {model_compose-0.0.16 → model_compose-0.1.0}/src/mindor/dsl/schema/runtime/impl/docker.py +0 -0
  114. {model_compose-0.0.16 → model_compose-0.1.0}/src/mindor/dsl/schema/runtime/impl/types.py +0 -0
  115. {model_compose-0.0.16 → model_compose-0.1.0}/src/mindor/dsl/schema/runtime/runtime.py +0 -0
  116. {model_compose-0.0.16 → model_compose-0.1.0}/src/model_compose.egg-info/dependency_links.txt +0 -0
  117. {model_compose-0.0.16 → model_compose-0.1.0}/src/model_compose.egg-info/entry_points.txt +0 -0
  118. {model_compose-0.0.16 → model_compose-0.1.0}/src/model_compose.egg-info/top_level.txt +0 -0
  119. {model_compose-0.0.16 → model_compose-0.1.0}/tests/test_cli.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: model-compose
3
- Version: 0.0.16
3
+ Version: 0.1.0
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
@@ -18,6 +18,7 @@ Requires-Dist: uvicorn
18
18
  Requires-Dist: pyngrok
19
19
  Requires-Dist: ulid
20
20
  Requires-Dist: gradio
21
+ Requires-Dist: Pillow
21
22
  Dynamic: license-file
22
23
 
23
24
  # model-compose: Declarative AI Model and Workflow Orchestrator (_part of the Mindor project_)
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "model-compose"
3
- version = "0.0.16"
3
+ version = "0.1.0"
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" }
@@ -18,7 +18,8 @@ dependencies = [
18
18
  "uvicorn",
19
19
  "pyngrok",
20
20
  "ulid",
21
- "gradio"
21
+ "gradio",
22
+ "Pillow"
22
23
  ]
23
24
 
24
25
  [project.scripts]
@@ -14,10 +14,10 @@ class ComponentContext:
14
14
  def register_source(self, key: str, source: Any) -> None:
15
15
  self.sources[key] = source
16
16
 
17
- def render_template(self, data: Dict[str, Any]) -> Any:
18
- return self.renderer.render(data)
17
+ async def render_template(self, data: Dict[str, Any]) -> Any:
18
+ return await self.renderer.render(data)
19
19
 
20
- def _resolve_source(self, key: str) -> Any:
20
+ async def _resolve_source(self, key: str) -> Any:
21
21
  if key in self.sources:
22
22
  return self.sources[key]
23
23
  if key == "input":
@@ -16,11 +16,11 @@ class HttpClientPollingCompletion:
16
16
  self.client: HttpClient = HttpClient()
17
17
 
18
18
  async def run(self, context: ComponentContext) -> Any:
19
- url = self._resolve_request_url(context)
20
- method = context.render_template(self.config.method)
21
- params = context.render_template(self.config.params)
22
- body = context.render_template(self.config.body)
23
- headers = context.render_template(self.config.headers)
19
+ url = await self._resolve_request_url(context)
20
+ method = await context.render_template(self.config.method)
21
+ params = await context.render_template(self.config.params)
22
+ body = await context.render_template(self.config.body)
23
+ headers = await context.render_template(self.config.headers)
24
24
 
25
25
  interval = parse_duration(self.config.interval) if self.config.interval else 5.0
26
26
  timeout = parse_duration(self.config.timeout) if self.config.timeout else 300
@@ -32,35 +32,35 @@ class HttpClientPollingCompletion:
32
32
  response = await self.client.request(url, method, params, body, headers)
33
33
 
34
34
  context.register_source("result", response)
35
- status = context.render_template(self.config.status) if self.config.status else response["status"]
35
+ status = (await context.render_template(self.config.status)) if self.config.status else response["status"]
36
36
 
37
37
  if not status:
38
38
  raise RuntimeError(f"Polling failed: no status found in response.")
39
39
 
40
40
  for success_when in self.config.success_when if self.config.success_when else [ "completed" ]:
41
- if status == context.render_template(success_when):
41
+ if status == await context.render_template(success_when):
42
42
  return response
43
43
 
44
44
  for fail_when in self.config.fail_when if self.config.fail_when else [ "failed" ]:
45
- if status == context.render_template(fail_when):
45
+ if status == await context.render_template(fail_when):
46
46
  raise RuntimeError(f"Polling failed: status '{status}' matched a failure condition.")
47
47
 
48
48
  await asyncio.sleep(interval.total_seconds())
49
49
 
50
50
  raise TimeoutError(f"Polling timed out after {timeout}.")
51
51
 
52
- def _resolve_request_url(self, context: ComponentContext) -> str:
52
+ async def _resolve_request_url(self, context: ComponentContext) -> str:
53
53
  if self.base_url and self.config.path:
54
- return context.render_template(self.base_url) + context.render_template(self.config.path)
54
+ return await context.render_template(self.base_url) + await context.render_template(self.config.path)
55
55
 
56
- return context.render_template(self.config.endpoint)
56
+ return await context.render_template(self.config.endpoint)
57
57
 
58
58
  class HttpClientCallbackCompletion:
59
59
  def __init__(self, config: HttpClientCompletionConfig):
60
60
  self.config: HttpClientCompletionConfig = config
61
61
 
62
62
  async def run(self, context: ComponentContext) -> Any:
63
- callback_id = context.render_template(self.config.wait_for)
63
+ callback_id = await context.render_template(self.config.wait_for)
64
64
  future: asyncio.Future = asyncio.get_event_loop().create_future()
65
65
  HttpCallbackListener.register_pending_future(callback_id, future)
66
66
 
@@ -73,11 +73,11 @@ class HttpClientAction:
73
73
  self.client: HttpClient = HttpClient()
74
74
 
75
75
  async def run(self, context: ComponentContext) -> Any:
76
- url = self._resolve_request_url(context)
77
- method = context.render_template(self.config.method)
78
- params = context.render_template(self.config.params)
79
- body = context.render_template(self.config.body)
80
- headers = context.render_template(self.config.headers)
76
+ url = await self._resolve_request_url(context)
77
+ method = await context.render_template(self.config.method)
78
+ params = await context.render_template(self.config.params)
79
+ body = await context.render_template(self.config.body)
80
+ headers = await context.render_template(self.config.headers)
81
81
 
82
82
  response, result = await self.client.request(url, method, params, body, headers), None
83
83
 
@@ -90,13 +90,13 @@ class HttpClientAction:
90
90
  if result:
91
91
  context.register_source("result", result)
92
92
 
93
- return context.render_template(self.config.output) if self.config.output else (result or response)
93
+ return (await context.render_template(self.config.output)) if self.config.output else (result or response)
94
94
 
95
- def _resolve_request_url(self, context: ComponentContext) -> str:
95
+ async def _resolve_request_url(self, context: ComponentContext) -> str:
96
96
  if self.base_url and self.config.path:
97
- return context.render_template(self.base_url) + context.render_template(self.config.path)
97
+ return await context.render_template(self.base_url) + await context.render_template(self.config.path)
98
98
 
99
- return context.render_template(self.config.endpoint)
99
+ return await context.render_template(self.config.endpoint)
100
100
 
101
101
  async def _handle_completion(self, completion: HttpClientCompletionConfig, context: ComponentContext) -> Any:
102
102
  if completion.type == "polling":
@@ -61,10 +61,10 @@ class HttpServerController(ControllerEngine):
61
61
 
62
62
  def _configure_server(self):
63
63
  self.app.add_middleware(
64
- CORSMiddleware,
65
- allow_origins=["*"],
66
- allow_credentials=True,
67
- allow_methods=["*"],
64
+ CORSMiddleware,
65
+ allow_origins=[self.config.origins],
66
+ allow_credentials=True,
67
+ allow_methods=["*"],
68
68
  allow_headers=["*"],
69
69
  )
70
70
 
@@ -148,7 +148,7 @@ class HttpServerController(ControllerEngine):
148
148
  resource,
149
149
  media_type=resource.content_type,
150
150
  headers=self._build_stream_resource_headers(resource),
151
- background=BackgroundTask(_close_stream())
151
+ background=BackgroundTask(_close_stream)
152
152
  )
153
153
 
154
154
  def _build_stream_resource_headers(self, resource: StreamResource) -> Dict[str, str]:
@@ -1,20 +1,11 @@
1
1
  from typing import Type, Union, Literal, Optional, Dict, List, Tuple, Set, Annotated, Callable, Awaitable, Any
2
2
  from mindor.dsl.schema.workflow import WorkflowVariableConfig
3
+ from mindor.core.utils.http_client import HttpStreamResource
4
+ from mindor.core.utils.image import load_image_from_stream
5
+ from mindor.core.utils.streaming import Base64StreamResource, save_stream_to_temporary_file
3
6
  from .schema import WorkflowSchema
4
7
  import gradio as gr
5
8
 
6
- mermaid_code = """
7
- ```mermaid
8
- graph LR
9
- A[Start] --> B[Process];
10
- B --> C{Decision};
11
- C -->|Yes| D[Do something];
12
- C -->|No| E[Do something else];
13
- D --> F[End];
14
- E --> F;
15
- ```
16
- """
17
-
18
9
  class GradioWebUIBuilder:
19
10
  def build(self, schema: Dict[str, WorkflowSchema], runner: Callable[[Optional[str], Any], Awaitable[Any]]) -> gr.Blocks:
20
11
  with gr.Blocks() as blocks:
@@ -37,8 +28,6 @@ class GradioWebUIBuilder:
37
28
  if workflow.description:
38
29
  gr.Markdown(f"📝 {workflow.description}")
39
30
 
40
- gr.Markdown(mermaid_code)
41
-
42
31
  gr.Markdown("#### 📥 Input Parameters")
43
32
  input_components = [ self._build_input_component(variable) for variable in workflow.input ]
44
33
  run_button = gr.Button("🚀 Run Workflow", variant="primary")
@@ -53,9 +42,13 @@ class GradioWebUIBuilder:
53
42
  input = { variable.name: value for variable, value in zip(workflow.input, args) }
54
43
  output = await runner(input)
55
44
 
56
- if workflow.output and isinstance(output, dict):
57
- output = [ output[variable.name] for variable in workflow.output ]
58
- output = output[0] if len(output) == 1 else output
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)
59
52
 
60
53
  return output
61
54
 
@@ -93,10 +86,33 @@ class GradioWebUIBuilder:
93
86
  return gr.Textbox(label=label, value=default, info=f"Unsupported type: {variable.type}")
94
87
 
95
88
  def _build_output_component(self, variable: WorkflowVariableConfig) -> gr.Component:
96
- label = variable.name
89
+ label = variable.name or ""
97
90
  info = variable.description or ""
98
-
91
+
99
92
  if variable.type == "string":
100
93
  return gr.Textbox(label=label, interactive=False, show_copy_button=True, info=info)
101
94
 
95
+ if variable.type == "image":
96
+ return gr.Image(label=label, interactive=False)
97
+
98
+ if variable.type == "audio":
99
+ return gr.Audio(label=label)
100
+
102
101
  return gr.Textbox(label=label, info=f"Unsupported type: {variable.type}")
102
+
103
+ async def _convert_type(self, value: Any, type: Optional[str], subtype: Optional[str], format: Optional[str]) -> Any:
104
+ if type == "image":
105
+ if format == "base64" and isinstance(value, str):
106
+ return await load_image_from_stream(Base64StreamResource(value), subtype)
107
+ if isinstance(value, HttpStreamResource):
108
+ return await load_image_from_stream(value, subtype)
109
+ return None
110
+
111
+ if type == "audio":
112
+ if format == "base64" and isinstance(value, str):
113
+ return await save_stream_to_temporary_file(Base64StreamResource(value), subtype)
114
+ if isinstance(value, HttpStreamResource):
115
+ return await save_stream_to_temporary_file(value, subtype)
116
+ return None
117
+
118
+ return value
@@ -0,0 +1,135 @@
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
4
+ from mindor.dsl.schema.component import ComponentConfig
5
+ import re, json, io
6
+
7
+ class WorkflowVariableResolver:
8
+ def __init__(self):
9
+ self.patterns: Dict[str, re.Pattern] = {
10
+ "variable": re.compile(
11
+ r"""\$\{ # ${
12
+ ([a-zA-Z_][^.\s]*) # key: input, env, etc.
13
+ (?:\.([^\s\|\}]+))? # path: key, key.path[0], etc.
14
+ (?:\s*as\s*([^\s\|\}/;]+)(?:/([^\s\|\};]+))?(?:;([^\s\|\}]+))?)? # type/subtype;format
15
+ (?:\s*\|\s*([^\}]+))? # default value after `|`
16
+ \}""", # }
17
+ re.VERBOSE,
18
+ )
19
+ }
20
+
21
+ def _enumerate_input_variables(self, value: Any, wanted_key: str) -> List[Tuple[str, str, str, Optional[Any]]]:
22
+ if isinstance(value, str):
23
+ variables: List[Tuple[str, str, str, str, Optional[Any]]] = []
24
+
25
+ for match in self.patterns["variable"].finditer(value):
26
+ key, path, type, subtype, format, default = match.group(1, 2, 3, 4, 5, 6)
27
+
28
+ if type and default:
29
+ default = self._convert_type(default, type, subtype, format)
30
+
31
+ if key == wanted_key:
32
+ variables.append((path, type or "string", subtype, format, default))
33
+
34
+ return variables
35
+
36
+ if isinstance(value, BaseModel):
37
+ return self._enumerate_input_variables(value.model_dump(exclude_none=True), wanted_key)
38
+
39
+ if isinstance(value, dict):
40
+ return sum([ self._enumerate_input_variables(v, wanted_key) for v in value.values() ], [])
41
+
42
+ if isinstance(value, list):
43
+ return sum([ self._enumerate_input_variables(v, wanted_key) for v in value ], [])
44
+
45
+ return []
46
+
47
+ def _enumerate_output_variables(self, key: str, value: Any) -> List[Tuple[str, str, str, Optional[Any]]]:
48
+ variables: List[Tuple[str, str, str, str, Optional[Any]]] = []
49
+
50
+ if isinstance(value, str):
51
+ for match in self.patterns["variable"].finditer(value):
52
+ type, subtype, format, default = match.group(3, 4, 5, 6)
53
+
54
+ if type and default:
55
+ default = self._convert_type(default, type, subtype, format)
56
+
57
+ variables.append((key, type or "string", subtype, format, default))
58
+
59
+ return variables
60
+
61
+ if isinstance(value, BaseModel):
62
+ return self._enumerate_output_variables(key, value.model_dump(exclude_none=True))
63
+
64
+ if isinstance(value, dict):
65
+ return sum([ self._enumerate_output_variables(f"{key}.{k}" if key else f"{k}", v) for k, v in value.items() ], [])
66
+
67
+ if isinstance(value, list):
68
+ return sum([ self._enumerate_output_variables(f"{key}[{i}]" if key else f"[{i}]", v) for i, v in enumerate(value) ], [])
69
+
70
+ return []
71
+
72
+ def _convert_type(self, value: Any, type: str, subtype: str, format: Optional[str]) -> Any:
73
+ if type == "number":
74
+ return float(value)
75
+
76
+ if type == "integer":
77
+ return int(value)
78
+
79
+ if type == "boolean":
80
+ return str(value).lower() in [ "true", "1" ]
81
+
82
+ if type == "json":
83
+ return json.loads(value)
84
+
85
+ return value
86
+
87
+ class WorkflowInputResolver(WorkflowVariableResolver):
88
+ def resolve(self, workflow: WorkflowConfig, components: Dict[str, ComponentConfig]) -> List[WorkflowVariableConfig]:
89
+ variables: List[Tuple[str, str, Optional[Any]]] = []
90
+
91
+ for job in workflow.jobs.values():
92
+ if not job.input or job.input == "${input}":
93
+ action_id = job.action or "__default__"
94
+ if isinstance(job.component, str):
95
+ component: Optional[ComponentConfig] = components[job.component] if job.component in components else None
96
+ if component:
97
+ variables.extend(self._enumerate_input_variables(component.actions[action_id], "input"))
98
+ else:
99
+ variables.extend(self._enumerate_input_variables(job.component.actions[action_id], "input"))
100
+ else:
101
+ variables.extend(self._enumerate_input_variables(job.input, "input"))
102
+
103
+ return [ WorkflowVariableConfig(name=name, type=type, subtype=subtype, format=format, default=default) for name, type, subtype, format, default in set(variables) ]
104
+
105
+ class WorkflowOutputResolver(WorkflowVariableResolver):
106
+ def resolve(self, workflow: WorkflowConfig, components: Dict[str, ComponentConfig]) -> List[WorkflowVariableConfig]:
107
+ variables: List[Tuple[str, str, str, Optional[Any]]] = []
108
+
109
+ for job_id, job in workflow.jobs.items():
110
+ if not self._is_terminal_job(workflow, job_id):
111
+ continue
112
+
113
+ if not job.output or job.output == "${output}":
114
+ action_id = job.action or "__default__"
115
+ if isinstance(job.component, str):
116
+ component: Optional[ComponentConfig] = components[job.component] if job.component in components else None
117
+ if component:
118
+ variables.extend(self._enumerate_output_variables(None, component.actions[action_id].output))
119
+ else:
120
+ variables.extend(self._enumerate_output_variables(None, job.component.actions[action_id].output))
121
+ else:
122
+ variables.extend(self._enumerate_output_variables(None, job.output))
123
+
124
+ return [ WorkflowVariableConfig(name=name, type=type, subtype=subtype, format=format, default=default) for name, type, subtype, format, default in set(variables) ]
125
+
126
+ def _is_terminal_job(self, workflow: WorkflowConfig, job_id: str) -> bool:
127
+ return all(job_id not in job.depends_on for other_id, job in workflow.jobs.items() if other_id != job_id)
128
+
129
+ class WorkflowSchema:
130
+ def __init__(self, name: str, title: str, description: Optional[str], input: List[WorkflowVariableConfig], output: List[WorkflowVariableConfig]):
131
+ self.name: str = name
132
+ self.title: str = title
133
+ self.description: Optional[str] = description
134
+ self.input: List[WorkflowVariableConfig] = input
135
+ self.output: List[WorkflowVariableConfig] = output
@@ -1,4 +1,4 @@
1
- from typing import Type, Union, Literal, Optional, Dict, List, Tuple, Set, Annotated, Callable, Iterator, Any
1
+ from typing import Type, Union, Literal, Optional, Dict, List, Tuple, Set, Annotated, Callable, AsyncIterator, Any
2
2
 
3
3
  from mindor.dsl.schema.listener import HttpCallbackListenerConfig, HttpCallbackConfig
4
4
  from mindor.core.utils.http_request import parse_request_body, parse_options_header
@@ -21,15 +21,15 @@ class HttpCallbackContext:
21
21
  self.item: Optional[str] = item
22
22
  self.renderer: TemplateRenderer = TemplateRenderer(self._resolve_source)
23
23
 
24
- def items(self) -> Iterator["HttpCallbackContext"]:
25
- for item in self._items():
24
+ async def items(self) -> AsyncIterator["HttpCallbackContext"]:
25
+ async for item in self._items():
26
26
  yield HttpCallbackContext(item, self.query, False, None)
27
27
 
28
- def render_template(self, template: str) -> Any:
29
- return self.renderer.render(template)
28
+ async def render_template(self, template: str) -> Any:
29
+ return await self.renderer.render(template)
30
30
 
31
- def _items(self) -> List[Any]:
32
- item = self.renderer.render(self.item) if self.item else self.body
31
+ async def _items(self) -> List[Any]:
32
+ item = await self.renderer.render(self.item) if self.item else self.body
33
33
 
34
34
  if self.bulk:
35
35
  if not isinstance(item, list):
@@ -38,7 +38,7 @@ class HttpCallbackContext:
38
38
 
39
39
  return [ item ]
40
40
 
41
- def _resolve_source(self, key: str) -> Any:
41
+ async def _resolve_source(self, key: str) -> Any:
42
42
  if key == "body" or key == "item":
43
43
  return self.body
44
44
  if key == "query":
@@ -70,15 +70,15 @@ class HttpCallbackListener(ListenerEngine):
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)
73
- succeeded: bool = self._is_callback_succeeded(callback, context)
73
+ succeeded: bool = await self._is_callback_succeeded(callback, context)
74
74
 
75
- for item in context.items():
76
- callback_id = item.render_template(callback.identify_by)
75
+ async for item in context.items():
76
+ callback_id = await item.render_template(callback.identify_by)
77
77
  future: asyncio.Future = self._get_pending_future(callback_id)
78
78
 
79
79
  if future:
80
80
  if succeeded:
81
- future.set_result(item.render_template(callback.result) if callback.result else item.body)
81
+ future.set_result((await item.render_template(callback.result)) if callback.result else item.body)
82
82
  else:
83
83
  future.set_exception(RuntimeError(f"Task failed for '{callback_id}'"))
84
84
  self._remove_pending_future(callback_id)
@@ -87,14 +87,14 @@ class HttpCallbackListener(ListenerEngine):
87
87
 
88
88
  return handler
89
89
 
90
- def _is_callback_succeeded(self, callback: HttpCallbackConfig, context: HttpCallbackContext) -> bool:
91
- status = context.render_template(callback.status) if callback.status else None
90
+ async def _is_callback_succeeded(self, callback: HttpCallbackConfig, context: HttpCallbackContext) -> bool:
91
+ status = (await context.render_template(callback.status)) if callback.status else None
92
92
 
93
93
  if status:
94
- if callback.success_when and status == context.render_template(callback.success_when):
94
+ if callback.success_when and status == await context.render_template(callback.success_when):
95
95
  return True
96
96
 
97
- if callback.fail_when and status == context.render_template(callback.fail_when):
97
+ if callback.fail_when and status == await context.render_template(callback.fail_when):
98
98
  return False
99
99
 
100
100
  return True
@@ -6,17 +6,16 @@ from urllib.parse import urlencode
6
6
  import aiohttp, json
7
7
 
8
8
  class HttpStreamResource(StreamResource):
9
- def __init__(self, session: aiohttp.ClientSession, stream: aiohttp.StreamReader, content_type: Optional[str], filename: Optional[str]):
9
+ def __init__(self, session: aiohttp.ClientSession, stream: aiohttp.StreamReader, content_type: Optional[str] = None, filename: Optional[str] = None):
10
10
  super().__init__(content_type, filename)
11
11
 
12
12
  self.session: aiohttp.ClientSession = session
13
13
  self.stream: aiohttp.StreamReader = stream
14
14
 
15
- def get_stream(self):
16
- return self.stream
17
-
18
15
  async def close(self):
19
16
  await self.session.close()
17
+ self.session = None
18
+ self.stream = None
20
19
 
21
20
  async def _iterate_stream(self) -> AsyncIterator[bytes]:
22
21
  _, buffer_size = self.stream.get_read_buffer_limits()
@@ -16,8 +16,7 @@ class NestedFormParser:
16
16
  data = {}
17
17
 
18
18
  for form_key, value in form.items():
19
- keys = self._parse_key_path(form_key)
20
- current = data
19
+ keys, current = self._parse_key_path(form_key), data
21
20
  for index, key in enumerate(keys):
22
21
  if index == len(keys) - 1:
23
22
  if key.endswith("[]"):
@@ -0,0 +1,14 @@
1
+ from typing import Optional
2
+ from .streaming import StreamResource
3
+ from PIL import Image as PILImage
4
+ import io, base64
5
+
6
+ async def load_image_from_stream(stream: StreamResource, extension: Optional[str]) -> Optional[PILImage.Image]:
7
+ try:
8
+ data = bytearray()
9
+ async with stream:
10
+ async for chunk in stream:
11
+ data.extend(chunk)
12
+ return PILImage.open(io.BytesIO(data))
13
+ except Exception:
14
+ return None
@@ -0,0 +1,63 @@
1
+ from typing import Optional, AsyncIterator
2
+ from abc import ABC, abstractmethod
3
+ from tempfile import NamedTemporaryFile
4
+ import io, base64
5
+
6
+ class StreamResource(ABC):
7
+ def __init__(self, content_type: Optional[str], filename: Optional[str]):
8
+ self.content_type = content_type or "application/octet-stream"
9
+ self.filename = filename
10
+
11
+ async def __aenter__(self):
12
+ return self
13
+
14
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
15
+ await self.close()
16
+
17
+ def __aiter__(self):
18
+ return self._iterate_stream()
19
+
20
+ @abstractmethod
21
+ async def close(self):
22
+ pass
23
+
24
+ @abstractmethod
25
+ async def _iterate_stream(self) -> AsyncIterator[bytes]:
26
+ pass
27
+
28
+ class Base64StreamResource(StreamResource):
29
+ def __init__(self, encoded: str, content_type: Optional[str] = None, filename: Optional[str] = None):
30
+ super().__init__(content_type, filename)
31
+
32
+ self.stream: Optional[io.BytesIO] = io.BytesIO(base64.b64decode(encoded))
33
+
34
+ async def close(self):
35
+ self.stream.close()
36
+ self.stream = None
37
+
38
+ async def _iterate_stream(self) -> AsyncIterator[bytes]:
39
+ while True:
40
+ chunk = self.stream.read(8192)
41
+ if not chunk:
42
+ break
43
+ yield chunk
44
+
45
+ async def save_stream_to_temporary_file(stream: StreamResource, extension: Optional[str]) -> Optional[str]:
46
+ try:
47
+ file = NamedTemporaryFile(suffix=f".{extension}" if extension else None, delete=False)
48
+ async with stream:
49
+ async for chunk in stream:
50
+ file.write(chunk)
51
+ file.flush()
52
+ file.close()
53
+ return file.name
54
+ except Exception:
55
+ return None
56
+
57
+ async def encode_stream_to_base64(stream: StreamResource) -> str:
58
+ buffer = io.BytesIO()
59
+ async with stream:
60
+ async for chunk in stream:
61
+ buffer.write(chunk)
62
+ buffer.seek(0)
63
+ return base64.b64encode(buffer.read()).decode("utf-8")