model-compose 0.0.17__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 (118) hide show
  1. {model_compose-0.0.17 → model_compose-0.1.0}/PKG-INFO +1 -1
  2. {model_compose-0.0.17 → model_compose-0.1.0}/pyproject.toml +1 -1
  3. {model_compose-0.0.17 → model_compose-0.1.0}/src/mindor/core/component/engine/context.py +3 -3
  4. {model_compose-0.0.17 → model_compose-0.1.0}/src/mindor/core/component/engine/http_client.py +21 -21
  5. {model_compose-0.0.17 → model_compose-0.1.0}/src/mindor/core/controller/engine/http_server.py +1 -1
  6. {model_compose-0.0.17 → model_compose-0.1.0}/src/mindor/core/controller/webui/gradio.py +21 -7
  7. {model_compose-0.0.17 → model_compose-0.1.0}/src/mindor/core/controller/webui/schema.py +21 -21
  8. {model_compose-0.0.17 → model_compose-0.1.0}/src/mindor/core/listener/engine/http_callback.py +16 -16
  9. {model_compose-0.0.17 → model_compose-0.1.0}/src/mindor/core/utils/http_client.py +3 -4
  10. model_compose-0.1.0/src/mindor/core/utils/image.py +14 -0
  11. model_compose-0.1.0/src/mindor/core/utils/streaming.py +63 -0
  12. {model_compose-0.0.17 → model_compose-0.1.0}/src/mindor/core/utils/template.py +28 -22
  13. {model_compose-0.0.17 → model_compose-0.1.0}/src/mindor/core/workflow/context.py +3 -3
  14. {model_compose-0.0.17 → model_compose-0.1.0}/src/mindor/core/workflow/job.py +2 -2
  15. {model_compose-0.0.17 → model_compose-0.1.0}/src/mindor/dsl/schema/workflow.py +26 -2
  16. {model_compose-0.0.17 → model_compose-0.1.0}/src/model_compose.egg-info/PKG-INFO +1 -1
  17. model_compose-0.0.17/src/mindor/core/utils/image.py +0 -9
  18. model_compose-0.0.17/src/mindor/core/utils/streaming.py +0 -28
  19. {model_compose-0.0.17 → model_compose-0.1.0}/LICENSE +0 -0
  20. {model_compose-0.0.17 → model_compose-0.1.0}/README.md +0 -0
  21. {model_compose-0.0.17 → model_compose-0.1.0}/setup.cfg +0 -0
  22. {model_compose-0.0.17 → model_compose-0.1.0}/src/mindor/__init__.py +0 -0
  23. {model_compose-0.0.17 → model_compose-0.1.0}/src/mindor/cli/__init__.py +0 -0
  24. {model_compose-0.0.17 → model_compose-0.1.0}/src/mindor/cli/compose.py +0 -0
  25. {model_compose-0.0.17 → model_compose-0.1.0}/src/mindor/core/__init__.py +0 -0
  26. {model_compose-0.0.17 → model_compose-0.1.0}/src/mindor/core/component/__init__.py +0 -0
  27. {model_compose-0.0.17 → model_compose-0.1.0}/src/mindor/core/component/component.py +0 -0
  28. {model_compose-0.0.17 → model_compose-0.1.0}/src/mindor/core/component/engine/__init__.py +0 -0
  29. {model_compose-0.0.17 → model_compose-0.1.0}/src/mindor/core/component/engine/base.py +0 -0
  30. {model_compose-0.0.17 → model_compose-0.1.0}/src/mindor/core/component/engine/http_server.py +0 -0
  31. {model_compose-0.0.17 → model_compose-0.1.0}/src/mindor/core/compose/__init__.py +0 -0
  32. {model_compose-0.0.17 → model_compose-0.1.0}/src/mindor/core/compose/compose.py +0 -0
  33. {model_compose-0.0.17 → model_compose-0.1.0}/src/mindor/core/compose/manager.py +0 -0
  34. {model_compose-0.0.17 → model_compose-0.1.0}/src/mindor/core/controller/__init__.py +0 -0
  35. {model_compose-0.0.17 → model_compose-0.1.0}/src/mindor/core/controller/controller.py +0 -0
  36. {model_compose-0.0.17 → model_compose-0.1.0}/src/mindor/core/controller/engine/__init__.py +0 -0
  37. {model_compose-0.0.17 → model_compose-0.1.0}/src/mindor/core/controller/engine/base.py +0 -0
  38. {model_compose-0.0.17 → model_compose-0.1.0}/src/mindor/core/controller/engine/mcp_server.py +0 -0
  39. {model_compose-0.0.17 → model_compose-0.1.0}/src/mindor/core/controller/runner/__init__.py +0 -0
  40. {model_compose-0.0.17 → model_compose-0.1.0}/src/mindor/core/controller/runner/client.py +0 -0
  41. {model_compose-0.0.17 → model_compose-0.1.0}/src/mindor/core/controller/runner/http_client.py +0 -0
  42. {model_compose-0.0.17 → model_compose-0.1.0}/src/mindor/core/controller/runner/mcp_client.py +0 -0
  43. {model_compose-0.0.17 → model_compose-0.1.0}/src/mindor/core/controller/runner/runner.py +0 -0
  44. {model_compose-0.0.17 → model_compose-0.1.0}/src/mindor/core/controller/webui/__init__.py +0 -0
  45. {model_compose-0.0.17 → model_compose-0.1.0}/src/mindor/core/controller/webui/webui.py +0 -0
  46. {model_compose-0.0.17 → model_compose-0.1.0}/src/mindor/core/gateway/__init__.py +0 -0
  47. {model_compose-0.0.17 → model_compose-0.1.0}/src/mindor/core/gateway/engine/__init__.py +0 -0
  48. {model_compose-0.0.17 → model_compose-0.1.0}/src/mindor/core/gateway/engine/base.py +0 -0
  49. {model_compose-0.0.17 → model_compose-0.1.0}/src/mindor/core/gateway/engine/http_tunnel.py +0 -0
  50. {model_compose-0.0.17 → model_compose-0.1.0}/src/mindor/core/gateway/gateway.py +0 -0
  51. {model_compose-0.0.17 → model_compose-0.1.0}/src/mindor/core/listener/__init__.py +0 -0
  52. {model_compose-0.0.17 → model_compose-0.1.0}/src/mindor/core/listener/engine/__init__.py +0 -0
  53. {model_compose-0.0.17 → model_compose-0.1.0}/src/mindor/core/listener/engine/base.py +0 -0
  54. {model_compose-0.0.17 → model_compose-0.1.0}/src/mindor/core/listener/listener.py +0 -0
  55. {model_compose-0.0.17 → model_compose-0.1.0}/src/mindor/core/runtime/__init__.py +0 -0
  56. {model_compose-0.0.17 → model_compose-0.1.0}/src/mindor/core/runtime/env.py +0 -0
  57. {model_compose-0.0.17 → model_compose-0.1.0}/src/mindor/core/services/__init__.py +0 -0
  58. {model_compose-0.0.17 → model_compose-0.1.0}/src/mindor/core/services/async_service.py +0 -0
  59. {model_compose-0.0.17 → model_compose-0.1.0}/src/mindor/core/utils/__init__.py +0 -0
  60. {model_compose-0.0.17 → model_compose-0.1.0}/src/mindor/core/utils/expiring.py +0 -0
  61. {model_compose-0.0.17 → model_compose-0.1.0}/src/mindor/core/utils/http_request.py +0 -0
  62. {model_compose-0.0.17 → model_compose-0.1.0}/src/mindor/core/utils/time.py +0 -0
  63. {model_compose-0.0.17 → model_compose-0.1.0}/src/mindor/core/utils/workqueue.py +0 -0
  64. {model_compose-0.0.17 → model_compose-0.1.0}/src/mindor/core/workflow/__init__.py +0 -0
  65. {model_compose-0.0.17 → model_compose-0.1.0}/src/mindor/core/workflow/workflow.py +0 -0
  66. {model_compose-0.0.17 → model_compose-0.1.0}/src/mindor/dsl/__init__.py +0 -0
  67. {model_compose-0.0.17 → model_compose-0.1.0}/src/mindor/dsl/loader.py +0 -0
  68. {model_compose-0.0.17 → model_compose-0.1.0}/src/mindor/dsl/schema/__init__.py +0 -0
  69. {model_compose-0.0.17 → model_compose-0.1.0}/src/mindor/dsl/schema/action/__init__.py +0 -0
  70. {model_compose-0.0.17 → model_compose-0.1.0}/src/mindor/dsl/schema/action/action.py +0 -0
  71. {model_compose-0.0.17 → model_compose-0.1.0}/src/mindor/dsl/schema/action/impl/__init__.py +0 -0
  72. {model_compose-0.0.17 → model_compose-0.1.0}/src/mindor/dsl/schema/action/impl/common.py +0 -0
  73. {model_compose-0.0.17 → model_compose-0.1.0}/src/mindor/dsl/schema/action/impl/http_client.py +0 -0
  74. {model_compose-0.0.17 → model_compose-0.1.0}/src/mindor/dsl/schema/action/impl/http_server.py +0 -0
  75. {model_compose-0.0.17 → model_compose-0.1.0}/src/mindor/dsl/schema/action/impl/mcp_client.py +0 -0
  76. {model_compose-0.0.17 → model_compose-0.1.0}/src/mindor/dsl/schema/action/impl/mcp_server.py +0 -0
  77. {model_compose-0.0.17 → model_compose-0.1.0}/src/mindor/dsl/schema/component/__init__.py +0 -0
  78. {model_compose-0.0.17 → model_compose-0.1.0}/src/mindor/dsl/schema/component/component.py +0 -0
  79. {model_compose-0.0.17 → model_compose-0.1.0}/src/mindor/dsl/schema/component/impl/__init__.py +0 -0
  80. {model_compose-0.0.17 → model_compose-0.1.0}/src/mindor/dsl/schema/component/impl/common.py +0 -0
  81. {model_compose-0.0.17 → model_compose-0.1.0}/src/mindor/dsl/schema/component/impl/http_client.py +0 -0
  82. {model_compose-0.0.17 → model_compose-0.1.0}/src/mindor/dsl/schema/component/impl/http_server.py +0 -0
  83. {model_compose-0.0.17 → model_compose-0.1.0}/src/mindor/dsl/schema/component/impl/mcp_client.py +0 -0
  84. {model_compose-0.0.17 → model_compose-0.1.0}/src/mindor/dsl/schema/component/impl/mcp_server.py +0 -0
  85. {model_compose-0.0.17 → model_compose-0.1.0}/src/mindor/dsl/schema/component/impl/types.py +0 -0
  86. {model_compose-0.0.17 → model_compose-0.1.0}/src/mindor/dsl/schema/compose.py +0 -0
  87. {model_compose-0.0.17 → model_compose-0.1.0}/src/mindor/dsl/schema/controller/__init__.py +0 -0
  88. {model_compose-0.0.17 → model_compose-0.1.0}/src/mindor/dsl/schema/controller/controller.py +0 -0
  89. {model_compose-0.0.17 → model_compose-0.1.0}/src/mindor/dsl/schema/controller/impl/__init__.py +0 -0
  90. {model_compose-0.0.17 → model_compose-0.1.0}/src/mindor/dsl/schema/controller/impl/common.py +0 -0
  91. {model_compose-0.0.17 → model_compose-0.1.0}/src/mindor/dsl/schema/controller/impl/http_server.py +0 -0
  92. {model_compose-0.0.17 → model_compose-0.1.0}/src/mindor/dsl/schema/controller/impl/mcp_server.py +0 -0
  93. {model_compose-0.0.17 → model_compose-0.1.0}/src/mindor/dsl/schema/controller/impl/types.py +0 -0
  94. {model_compose-0.0.17 → model_compose-0.1.0}/src/mindor/dsl/schema/controller/impl/webui.py +0 -0
  95. {model_compose-0.0.17 → model_compose-0.1.0}/src/mindor/dsl/schema/gateway/__init__.py +0 -0
  96. {model_compose-0.0.17 → model_compose-0.1.0}/src/mindor/dsl/schema/gateway/gateway.py +0 -0
  97. {model_compose-0.0.17 → model_compose-0.1.0}/src/mindor/dsl/schema/gateway/impl/__init__.py +0 -0
  98. {model_compose-0.0.17 → model_compose-0.1.0}/src/mindor/dsl/schema/gateway/impl/common.py +0 -0
  99. {model_compose-0.0.17 → model_compose-0.1.0}/src/mindor/dsl/schema/gateway/impl/http_tunnel.py +0 -0
  100. {model_compose-0.0.17 → model_compose-0.1.0}/src/mindor/dsl/schema/gateway/impl/types.py +0 -0
  101. {model_compose-0.0.17 → model_compose-0.1.0}/src/mindor/dsl/schema/listener/__init__.py +0 -0
  102. {model_compose-0.0.17 → model_compose-0.1.0}/src/mindor/dsl/schema/listener/impl/__init__.py +0 -0
  103. {model_compose-0.0.17 → model_compose-0.1.0}/src/mindor/dsl/schema/listener/impl/common.py +0 -0
  104. {model_compose-0.0.17 → model_compose-0.1.0}/src/mindor/dsl/schema/listener/impl/http_callback.py +0 -0
  105. {model_compose-0.0.17 → model_compose-0.1.0}/src/mindor/dsl/schema/listener/impl/types.py +0 -0
  106. {model_compose-0.0.17 → model_compose-0.1.0}/src/mindor/dsl/schema/listener/listener.py +0 -0
  107. {model_compose-0.0.17 → model_compose-0.1.0}/src/mindor/dsl/schema/runtime/__init__.py +0 -0
  108. {model_compose-0.0.17 → model_compose-0.1.0}/src/mindor/dsl/schema/runtime/impl/__init__.py +0 -0
  109. {model_compose-0.0.17 → model_compose-0.1.0}/src/mindor/dsl/schema/runtime/impl/common.py +0 -0
  110. {model_compose-0.0.17 → model_compose-0.1.0}/src/mindor/dsl/schema/runtime/impl/docker.py +0 -0
  111. {model_compose-0.0.17 → model_compose-0.1.0}/src/mindor/dsl/schema/runtime/impl/types.py +0 -0
  112. {model_compose-0.0.17 → model_compose-0.1.0}/src/mindor/dsl/schema/runtime/runtime.py +0 -0
  113. {model_compose-0.0.17 → model_compose-0.1.0}/src/model_compose.egg-info/SOURCES.txt +0 -0
  114. {model_compose-0.0.17 → model_compose-0.1.0}/src/model_compose.egg-info/dependency_links.txt +0 -0
  115. {model_compose-0.0.17 → model_compose-0.1.0}/src/model_compose.egg-info/entry_points.txt +0 -0
  116. {model_compose-0.0.17 → model_compose-0.1.0}/src/model_compose.egg-info/requires.txt +0 -0
  117. {model_compose-0.0.17 → model_compose-0.1.0}/src/model_compose.egg-info/top_level.txt +0 -0
  118. {model_compose-0.0.17 → 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.17
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "model-compose"
3
- version = "0.0.17"
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" }
@@ -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":
@@ -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,6 +1,8 @@
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.image import load_image_from_base64
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
4
6
  from .schema import WorkflowSchema
5
7
  import gradio as gr
6
8
 
@@ -42,11 +44,11 @@ class GradioWebUIBuilder:
42
44
 
43
45
  if workflow.output:
44
46
  if isinstance(output, dict):
45
- output = [ self._convert_type(output[variable.name], variable.type, variable.format) for variable in workflow.output ]
47
+ output = [ await self._convert_type(output[variable.name], variable.type, variable.subtype, variable.format) for variable in workflow.output ]
46
48
  output = output[0] if len(output) == 1 else output
47
49
  else:
48
50
  variable = workflow.output[0]
49
- output = self._convert_type(output, variable.type, variable.format)
51
+ output = await self._convert_type(output, variable.type, variable.subtype, variable.format)
50
52
 
51
53
  return output
52
54
 
@@ -84,21 +86,33 @@ class GradioWebUIBuilder:
84
86
  return gr.Textbox(label=label, value=default, info=f"Unsupported type: {variable.type}")
85
87
 
86
88
  def _build_output_component(self, variable: WorkflowVariableConfig) -> gr.Component:
87
- label = variable.name
89
+ label = variable.name or ""
88
90
  info = variable.description or ""
89
-
91
+
90
92
  if variable.type == "string":
91
93
  return gr.Textbox(label=label, interactive=False, show_copy_button=True, info=info)
92
94
 
93
95
  if variable.type == "image":
94
96
  return gr.Image(label=label, interactive=False)
95
97
 
98
+ if variable.type == "audio":
99
+ return gr.Audio(label=label)
100
+
96
101
  return gr.Textbox(label=label, info=f"Unsupported type: {variable.type}")
97
102
 
98
- def _convert_type(self, value: Any, type: str, format: Optional[str]) -> Any:
103
+ async def _convert_type(self, value: Any, type: Optional[str], subtype: Optional[str], format: Optional[str]) -> Any:
99
104
  if type == "image":
100
105
  if format == "base64" and isinstance(value, str):
101
- return load_image_from_base64(value)
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)
102
116
  return None
103
117
 
104
118
  return value
@@ -8,28 +8,28 @@ class WorkflowVariableResolver:
8
8
  def __init__(self):
9
9
  self.patterns: Dict[str, re.Pattern] = {
10
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\|\}]+)))? # type/format
15
- (?:\s*\|\s*([^\}]+))? # default value after `|`
16
- \}""", # }
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
17
  re.VERBOSE,
18
18
  )
19
19
  }
20
20
 
21
21
  def _enumerate_input_variables(self, value: Any, wanted_key: str) -> List[Tuple[str, str, str, Optional[Any]]]:
22
22
  if isinstance(value, str):
23
- variables: List[Tuple[str, str, str, Optional[Any]]] = []
23
+ variables: List[Tuple[str, str, str, str, Optional[Any]]] = []
24
24
 
25
25
  for match in self.patterns["variable"].finditer(value):
26
- key, path, type, format, default = match.group(1, 2, 3, 4, 5)
26
+ key, path, type, subtype, format, default = match.group(1, 2, 3, 4, 5, 6)
27
27
 
28
28
  if type and default:
29
- default = self._convert_type(default, type, format)
29
+ default = self._convert_type(default, type, subtype, format)
30
30
 
31
31
  if key == wanted_key:
32
- variables.append((path, type or "string", format, default))
32
+ variables.append((path, type or "string", subtype, format, default))
33
33
 
34
34
  return variables
35
35
 
@@ -45,16 +45,16 @@ class WorkflowVariableResolver:
45
45
  return []
46
46
 
47
47
  def _enumerate_output_variables(self, key: str, value: Any) -> List[Tuple[str, str, str, Optional[Any]]]:
48
- variables: List[Tuple[str, str, str, Optional[Any]]] = []
48
+ variables: List[Tuple[str, str, str, str, Optional[Any]]] = []
49
49
 
50
50
  if isinstance(value, str):
51
51
  for match in self.patterns["variable"].finditer(value):
52
- type, format, default = match.group(3, 4, 5)
52
+ type, subtype, format, default = match.group(3, 4, 5, 6)
53
53
 
54
54
  if type and default:
55
- default = self._convert_type(default, type, format)
55
+ default = self._convert_type(default, type, subtype, format)
56
56
 
57
- variables.append((key, type or "string", format, default))
57
+ variables.append((key, type or "string", subtype, format, default))
58
58
 
59
59
  return variables
60
60
 
@@ -69,18 +69,18 @@ class WorkflowVariableResolver:
69
69
 
70
70
  return []
71
71
 
72
- def _convert_type(self, value: Any, type: str, format: Optional[str]) -> Any:
72
+ def _convert_type(self, value: Any, type: str, subtype: str, format: Optional[str]) -> Any:
73
73
  if type == "number":
74
74
  return float(value)
75
75
 
76
76
  if type == "integer":
77
77
  return int(value)
78
78
 
79
- if type == "json":
80
- return json.loads(value)
81
-
82
79
  if type == "boolean":
83
80
  return str(value).lower() in [ "true", "1" ]
81
+
82
+ if type == "json":
83
+ return json.loads(value)
84
84
 
85
85
  return value
86
86
 
@@ -100,11 +100,11 @@ class WorkflowInputResolver(WorkflowVariableResolver):
100
100
  else:
101
101
  variables.extend(self._enumerate_input_variables(job.input, "input"))
102
102
 
103
- return [ WorkflowVariableConfig(name=name, type=type, format=format, default=default) for name, type, format, default in set(variables) ]
103
+ return [ WorkflowVariableConfig(name=name, type=type, subtype=subtype, format=format, default=default) for name, type, subtype, format, default in set(variables) ]
104
104
 
105
105
  class WorkflowOutputResolver(WorkflowVariableResolver):
106
106
  def resolve(self, workflow: WorkflowConfig, components: Dict[str, ComponentConfig]) -> List[WorkflowVariableConfig]:
107
- variables: List[Tuple[str, str, Optional[Any]]] = []
107
+ variables: List[Tuple[str, str, str, Optional[Any]]] = []
108
108
 
109
109
  for job_id, job in workflow.jobs.items():
110
110
  if not self._is_terminal_job(workflow, job_id):
@@ -121,7 +121,7 @@ class WorkflowOutputResolver(WorkflowVariableResolver):
121
121
  else:
122
122
  variables.extend(self._enumerate_output_variables(None, job.output))
123
123
 
124
- return [ WorkflowVariableConfig(name=name, type=type, format=format, default=default) for name, type, format, default in set(variables) ]
124
+ return [ WorkflowVariableConfig(name=name, type=type, subtype=subtype, format=format, default=default) for name, type, subtype, format, default in set(variables) ]
125
125
 
126
126
  def _is_terminal_job(self, workflow: WorkflowConfig, job_id: str) -> bool:
127
127
  return all(job_id not in job.depends_on for other_id, job in workflow.jobs.items() if other_id != job_id)
@@ -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()
@@ -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")
@@ -1,54 +1,55 @@
1
- from typing import Callable, Dict, List, Optional, Any
2
- import re, json
1
+ from typing import Callable, Dict, List, Optional, Awaitable, Any
2
+ from .streaming import StreamResource, encode_stream_to_base64
3
+ import re, json, base64
3
4
 
4
5
  class TemplateRenderer:
5
- def __init__(self, source_resolver: Callable[[str], Any]):
6
- self.source_resolver: Callable[[str], Any] = source_resolver
6
+ def __init__(self, source_resolver: Callable[[str], Awaitable[Any]]):
7
+ self.source_resolver: Callable[[str], Awaitable[Any]] = source_resolver
7
8
  self.patterns: Dict[str, re.Pattern] = {
8
9
  "variable": re.compile(
9
- r"""\$\{ # ${
10
- ([a-zA-Z_][^.\s]*) # key: input, env, etc.
11
- (?:\.([^\s\|\}]+))? # path: key, key.path[0], etc.
12
- (?:\s*as\s*([^\s\|\}/]+)(?:/([^\s\|\}]+)))? # type/format
13
- (?:\s*\|\s*([^\}]+))? # default value after `|`
14
- \}""", # }
10
+ r"""\$\{ # ${
11
+ ([a-zA-Z_][^.\s]*) # key: input, env, etc.
12
+ (?:\.([^\s\|\}]+))? # path: key, key.path[0], etc.
13
+ (?:\s*as\s*([^\s\|\}/;]+)(?:/([^\s\|\};]+))?(?:;([^\s\|\}]+))?)? # type/subtype;format
14
+ (?:\s*\|\s*([^\}]+))? # default value after `|`
15
+ \}""", # }
15
16
  re.VERBOSE,
16
17
  ),
17
18
  "keypath": re.compile(r"[-_\w]+|\[\d+\]"),
18
19
  }
19
20
 
20
- def render(self, data: Any) -> Any:
21
- return self._render_element(data)
21
+ async def render(self, data: Any) -> Any:
22
+ return await self._render_element(data)
22
23
 
23
- def _render_element(self, element: Any) -> Any:
24
+ async def _render_element(self, element: Any) -> Any:
24
25
  if isinstance(element, str):
25
- return self._render_text(element)
26
+ return await self._render_text(element)
26
27
 
27
28
  if isinstance(element, dict):
28
- return { key: self._render_element(value) for key, value in element.items() }
29
+ return { key: await self._render_element(value) for key, value in element.items() }
29
30
 
30
31
  if isinstance(element, list):
31
- return [ self._render_element(item) for item in element ]
32
+ return [ await self._render_element(item) for item in element ]
32
33
 
33
34
  return element
34
35
 
35
- def _render_text(self, text: str) -> Any:
36
+ async def _render_text(self, text: str) -> Any:
36
37
  while True:
37
38
  match = self.patterns["variable"].search(text)
38
39
 
39
40
  if not match:
40
41
  break
41
42
 
42
- key, path, type, format, default, variable = (*match.group(1, 2, 3, 4, 5), match.group(0))
43
+ key, path, type, subtype, format, default, variable = (*match.group(1, 2, 3, 4, 5, 6), match.group(0))
43
44
  try:
44
- value = self._resolve_by_path(self.source_resolver(key), path)
45
+ value = self._resolve_by_path(await self.source_resolver(key), path)
45
46
  except Exception:
46
47
  value = None
47
48
 
48
49
  value = default if not value else value
49
50
 
50
- if format and value:
51
- value = self._convert_type(value, type, format)
51
+ if type and value:
52
+ value = await self._convert_type(value, type, subtype, format)
52
53
 
53
54
  if variable == text:
54
55
  return value
@@ -78,7 +79,7 @@ class TemplateRenderer:
78
79
 
79
80
  return current
80
81
 
81
- def _convert_type(self, value: Any, type: str, format: Optional[str]) -> Any:
82
+ async def _convert_type(self, value: Any, type: str, subtype: str, format: Optional[str]) -> Any:
82
83
  if type == "number":
83
84
  return float(value)
84
85
 
@@ -91,4 +92,9 @@ class TemplateRenderer:
91
92
  if type == "boolean":
92
93
  return str(value).lower() in [ "true", "1" ]
93
94
 
95
+ if type == "base64":
96
+ if isinstance(value, StreamResource):
97
+ return await encode_stream_to_base64(value)
98
+ return base64.b64encode(value)
99
+
94
100
  return value
@@ -16,10 +16,10 @@ class WorkflowContext:
16
16
  def register_source(self, key: str, source: Any) -> None:
17
17
  self.sources[key] = source
18
18
 
19
- def render_template(self, data: Dict[str, Any]) -> Any:
20
- return self.renderer.render(data)
19
+ async def render_template(self, data: Dict[str, Any]) -> Any:
20
+ return await self.renderer.render(data)
21
21
 
22
- def _resolve_source(self, key: str) -> Any:
22
+ async def _resolve_source(self, key: str) -> Any:
23
23
  if key in self.sources:
24
24
  return self.sources[key]
25
25
  if key == "input":
@@ -18,10 +18,10 @@ class Job:
18
18
  await component.start()
19
19
 
20
20
  call_id = ulid.ulid()
21
- input = context.render_template(self.config.input) if self.config.input else context.input
21
+ input = (await context.render_template(self.config.input)) if self.config.input else context.input
22
22
  output = await component.run(self.config.action, call_id, input)
23
23
 
24
24
  if output:
25
25
  context.register_source("output", output)
26
26
 
27
- return context.render_template(self.config.output) if self.config.output else output
27
+ return (await context.render_template(self.config.output)) if self.config.output else output
@@ -1,4 +1,5 @@
1
1
  from typing import Type, Union, Literal, Optional, Dict, List, Tuple, Set, Annotated, Any
2
+ from enum import Enum
2
3
  from pydantic import BaseModel, Field
3
4
  from pydantic import model_validator
4
5
  from .component import ComponentConfig
@@ -10,10 +11,33 @@ class JobConfig(BaseModel):
10
11
  output: Optional[Any] = None
11
12
  depends_on: Optional[List[str]] = Field(default_factory=list)
12
13
 
14
+ class WorkflowVariableType(str, Enum):
15
+ # Primitive data types
16
+ STRING = "string"
17
+ TEXT = "text"
18
+ INTEGER = "integer"
19
+ NUMBER = "number"
20
+ BOOLEAN = "boolean"
21
+ JSON = "json"
22
+ # Encoded data
23
+ BASE64 = "base64"
24
+ # Media and files
25
+ IMAGE = "image"
26
+ AUDIO = "audio"
27
+ FILE = "file"
28
+ # UI-related types
29
+ SELECT = "select"
30
+
31
+ class WorkflowVariableFormat(str, Enum):
32
+ BASE64 = "base64"
33
+ URL = "url"
34
+ PATH = "path"
35
+
13
36
  class WorkflowVariableConfig(BaseModel):
14
37
  name: Optional[str] = Field(default=None, description="The name of the variable")
15
- type: Literal[ "string", "base64", "integer", "number", "json", "boolean", "image", "file", "select" ] = Field(..., description="Type of the variable")
16
- format: Optional[Literal[ "base64" ]] = Field(default=None, description="Format of the variable")
38
+ type: WorkflowVariableType = Field(..., description="Type of the variable")
39
+ subtype: Optional[str] = Field(default=None, description="Subtype of the variable")
40
+ format: Optional[WorkflowVariableFormat] = Field(default=None, description="Format of the variable")
17
41
  required: bool = Field(default=False, description="Whether this variable is required")
18
42
  default: Optional[Any] = Field(default=None, description="Default value if not provided")
19
43
  description: Optional[str] = Field(default=None, description="Description of the variable")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: model-compose
3
- Version: 0.0.17
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
@@ -1,9 +0,0 @@
1
- from typing import Optional
2
- from PIL import Image as PILImage
3
- import io, base64
4
-
5
- def load_image_from_base64(value: str) -> Optional[PILImage.Image]:
6
- try:
7
- return PILImage.open(io.BytesIO(base64.b64decode(value)))
8
- except:
9
- return None
@@ -1,28 +0,0 @@
1
- from typing import Optional, AsyncIterator
2
- from abc import ABC, abstractmethod
3
-
4
- class StreamResource(ABC):
5
- def __init__(self, content_type: Optional[str], filename: Optional[str]):
6
- self.content_type = content_type or "application/octet-stream"
7
- self.filename = filename
8
-
9
- async def __aenter__(self):
10
- return self
11
-
12
- async def __aexit__(self, exc_type, exc_val, exc_tb):
13
- await self.close()
14
-
15
- def __aiter__(self):
16
- return self._iterate_stream()
17
-
18
- @abstractmethod
19
- def get_stream(self):
20
- pass
21
-
22
- @abstractmethod
23
- async def close(self):
24
- pass
25
-
26
- @abstractmethod
27
- async def _iterate_stream(self) -> AsyncIterator[bytes]:
28
- pass
File without changes
File without changes
File without changes