model-compose 0.1.1__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 (116) hide show
  1. {model_compose-0.1.1 → model_compose-0.1.2}/PKG-INFO +1 -1
  2. {model_compose-0.1.1 → model_compose-0.1.2}/pyproject.toml +1 -1
  3. {model_compose-0.1.1 → model_compose-0.1.2}/src/mindor/core/component/engine/http_client.py +3 -7
  4. {model_compose-0.1.1 → model_compose-0.1.2}/src/mindor/core/controller/webui/gradio.py +52 -27
  5. {model_compose-0.1.1 → model_compose-0.1.2}/src/mindor/core/controller/webui/schema.py +88 -25
  6. {model_compose-0.1.1 → model_compose-0.1.2}/src/mindor/core/listener/engine/http_callback.py +2 -2
  7. {model_compose-0.1.1 → model_compose-0.1.2}/src/mindor/core/utils/http_client.py +3 -3
  8. {model_compose-0.1.1 → model_compose-0.1.2}/src/mindor/core/utils/template.py +2 -2
  9. {model_compose-0.1.1 → model_compose-0.1.2}/src/mindor/core/workflow/job.py +11 -9
  10. {model_compose-0.1.1 → model_compose-0.1.2}/src/mindor/dsl/schema/workflow.py +13 -2
  11. {model_compose-0.1.1 → model_compose-0.1.2}/src/model_compose.egg-info/PKG-INFO +1 -1
  12. {model_compose-0.1.1 → model_compose-0.1.2}/LICENSE +0 -0
  13. {model_compose-0.1.1 → model_compose-0.1.2}/README.md +0 -0
  14. {model_compose-0.1.1 → model_compose-0.1.2}/setup.cfg +0 -0
  15. {model_compose-0.1.1 → model_compose-0.1.2}/src/mindor/__init__.py +0 -0
  16. {model_compose-0.1.1 → model_compose-0.1.2}/src/mindor/cli/__init__.py +0 -0
  17. {model_compose-0.1.1 → model_compose-0.1.2}/src/mindor/cli/compose.py +0 -0
  18. {model_compose-0.1.1 → model_compose-0.1.2}/src/mindor/core/__init__.py +0 -0
  19. {model_compose-0.1.1 → model_compose-0.1.2}/src/mindor/core/component/__init__.py +0 -0
  20. {model_compose-0.1.1 → model_compose-0.1.2}/src/mindor/core/component/component.py +0 -0
  21. {model_compose-0.1.1 → model_compose-0.1.2}/src/mindor/core/component/engine/__init__.py +0 -0
  22. {model_compose-0.1.1 → model_compose-0.1.2}/src/mindor/core/component/engine/base.py +0 -0
  23. {model_compose-0.1.1 → model_compose-0.1.2}/src/mindor/core/component/engine/context.py +0 -0
  24. {model_compose-0.1.1 → model_compose-0.1.2}/src/mindor/core/component/engine/http_server.py +0 -0
  25. {model_compose-0.1.1 → model_compose-0.1.2}/src/mindor/core/compose/__init__.py +0 -0
  26. {model_compose-0.1.1 → model_compose-0.1.2}/src/mindor/core/compose/compose.py +0 -0
  27. {model_compose-0.1.1 → model_compose-0.1.2}/src/mindor/core/compose/manager.py +0 -0
  28. {model_compose-0.1.1 → model_compose-0.1.2}/src/mindor/core/controller/__init__.py +0 -0
  29. {model_compose-0.1.1 → model_compose-0.1.2}/src/mindor/core/controller/controller.py +0 -0
  30. {model_compose-0.1.1 → model_compose-0.1.2}/src/mindor/core/controller/engine/__init__.py +0 -0
  31. {model_compose-0.1.1 → model_compose-0.1.2}/src/mindor/core/controller/engine/base.py +0 -0
  32. {model_compose-0.1.1 → model_compose-0.1.2}/src/mindor/core/controller/engine/http_server.py +0 -0
  33. {model_compose-0.1.1 → model_compose-0.1.2}/src/mindor/core/controller/engine/mcp_server.py +0 -0
  34. {model_compose-0.1.1 → model_compose-0.1.2}/src/mindor/core/controller/runner/__init__.py +0 -0
  35. {model_compose-0.1.1 → model_compose-0.1.2}/src/mindor/core/controller/runner/client.py +0 -0
  36. {model_compose-0.1.1 → model_compose-0.1.2}/src/mindor/core/controller/runner/http_client.py +0 -0
  37. {model_compose-0.1.1 → model_compose-0.1.2}/src/mindor/core/controller/runner/mcp_client.py +0 -0
  38. {model_compose-0.1.1 → model_compose-0.1.2}/src/mindor/core/controller/runner/runner.py +0 -0
  39. {model_compose-0.1.1 → model_compose-0.1.2}/src/mindor/core/controller/webui/__init__.py +0 -0
  40. {model_compose-0.1.1 → model_compose-0.1.2}/src/mindor/core/controller/webui/webui.py +0 -0
  41. {model_compose-0.1.1 → model_compose-0.1.2}/src/mindor/core/gateway/__init__.py +0 -0
  42. {model_compose-0.1.1 → model_compose-0.1.2}/src/mindor/core/gateway/engine/__init__.py +0 -0
  43. {model_compose-0.1.1 → model_compose-0.1.2}/src/mindor/core/gateway/engine/base.py +0 -0
  44. {model_compose-0.1.1 → model_compose-0.1.2}/src/mindor/core/gateway/engine/http_tunnel.py +0 -0
  45. {model_compose-0.1.1 → model_compose-0.1.2}/src/mindor/core/gateway/gateway.py +0 -0
  46. {model_compose-0.1.1 → model_compose-0.1.2}/src/mindor/core/listener/__init__.py +0 -0
  47. {model_compose-0.1.1 → model_compose-0.1.2}/src/mindor/core/listener/engine/__init__.py +0 -0
  48. {model_compose-0.1.1 → model_compose-0.1.2}/src/mindor/core/listener/engine/base.py +0 -0
  49. {model_compose-0.1.1 → model_compose-0.1.2}/src/mindor/core/listener/listener.py +0 -0
  50. {model_compose-0.1.1 → model_compose-0.1.2}/src/mindor/core/runtime/__init__.py +0 -0
  51. {model_compose-0.1.1 → model_compose-0.1.2}/src/mindor/core/runtime/env.py +0 -0
  52. {model_compose-0.1.1 → model_compose-0.1.2}/src/mindor/core/services/__init__.py +0 -0
  53. {model_compose-0.1.1 → model_compose-0.1.2}/src/mindor/core/services/async_service.py +0 -0
  54. {model_compose-0.1.1 → model_compose-0.1.2}/src/mindor/core/utils/__init__.py +0 -0
  55. {model_compose-0.1.1 → model_compose-0.1.2}/src/mindor/core/utils/expiring.py +0 -0
  56. {model_compose-0.1.1 → model_compose-0.1.2}/src/mindor/core/utils/http_request.py +0 -0
  57. {model_compose-0.1.1 → model_compose-0.1.2}/src/mindor/core/utils/image.py +0 -0
  58. {model_compose-0.1.1 → model_compose-0.1.2}/src/mindor/core/utils/streaming.py +0 -0
  59. {model_compose-0.1.1 → model_compose-0.1.2}/src/mindor/core/utils/time.py +0 -0
  60. {model_compose-0.1.1 → model_compose-0.1.2}/src/mindor/core/utils/workqueue.py +0 -0
  61. {model_compose-0.1.1 → model_compose-0.1.2}/src/mindor/core/workflow/__init__.py +0 -0
  62. {model_compose-0.1.1 → model_compose-0.1.2}/src/mindor/core/workflow/context.py +0 -0
  63. {model_compose-0.1.1 → model_compose-0.1.2}/src/mindor/core/workflow/workflow.py +0 -0
  64. {model_compose-0.1.1 → model_compose-0.1.2}/src/mindor/dsl/__init__.py +0 -0
  65. {model_compose-0.1.1 → model_compose-0.1.2}/src/mindor/dsl/loader.py +0 -0
  66. {model_compose-0.1.1 → model_compose-0.1.2}/src/mindor/dsl/schema/__init__.py +0 -0
  67. {model_compose-0.1.1 → model_compose-0.1.2}/src/mindor/dsl/schema/action/__init__.py +0 -0
  68. {model_compose-0.1.1 → model_compose-0.1.2}/src/mindor/dsl/schema/action/action.py +0 -0
  69. {model_compose-0.1.1 → model_compose-0.1.2}/src/mindor/dsl/schema/action/impl/__init__.py +0 -0
  70. {model_compose-0.1.1 → model_compose-0.1.2}/src/mindor/dsl/schema/action/impl/common.py +0 -0
  71. {model_compose-0.1.1 → model_compose-0.1.2}/src/mindor/dsl/schema/action/impl/http_client.py +0 -0
  72. {model_compose-0.1.1 → model_compose-0.1.2}/src/mindor/dsl/schema/action/impl/http_server.py +0 -0
  73. {model_compose-0.1.1 → model_compose-0.1.2}/src/mindor/dsl/schema/action/impl/mcp_client.py +0 -0
  74. {model_compose-0.1.1 → model_compose-0.1.2}/src/mindor/dsl/schema/action/impl/mcp_server.py +0 -0
  75. {model_compose-0.1.1 → model_compose-0.1.2}/src/mindor/dsl/schema/component/__init__.py +0 -0
  76. {model_compose-0.1.1 → model_compose-0.1.2}/src/mindor/dsl/schema/component/component.py +0 -0
  77. {model_compose-0.1.1 → model_compose-0.1.2}/src/mindor/dsl/schema/component/impl/__init__.py +0 -0
  78. {model_compose-0.1.1 → model_compose-0.1.2}/src/mindor/dsl/schema/component/impl/common.py +0 -0
  79. {model_compose-0.1.1 → model_compose-0.1.2}/src/mindor/dsl/schema/component/impl/http_client.py +0 -0
  80. {model_compose-0.1.1 → model_compose-0.1.2}/src/mindor/dsl/schema/component/impl/http_server.py +0 -0
  81. {model_compose-0.1.1 → model_compose-0.1.2}/src/mindor/dsl/schema/component/impl/mcp_client.py +0 -0
  82. {model_compose-0.1.1 → model_compose-0.1.2}/src/mindor/dsl/schema/component/impl/mcp_server.py +0 -0
  83. {model_compose-0.1.1 → model_compose-0.1.2}/src/mindor/dsl/schema/component/impl/types.py +0 -0
  84. {model_compose-0.1.1 → model_compose-0.1.2}/src/mindor/dsl/schema/compose.py +0 -0
  85. {model_compose-0.1.1 → model_compose-0.1.2}/src/mindor/dsl/schema/controller/__init__.py +0 -0
  86. {model_compose-0.1.1 → model_compose-0.1.2}/src/mindor/dsl/schema/controller/controller.py +0 -0
  87. {model_compose-0.1.1 → model_compose-0.1.2}/src/mindor/dsl/schema/controller/impl/__init__.py +0 -0
  88. {model_compose-0.1.1 → model_compose-0.1.2}/src/mindor/dsl/schema/controller/impl/common.py +0 -0
  89. {model_compose-0.1.1 → model_compose-0.1.2}/src/mindor/dsl/schema/controller/impl/http_server.py +0 -0
  90. {model_compose-0.1.1 → model_compose-0.1.2}/src/mindor/dsl/schema/controller/impl/mcp_server.py +0 -0
  91. {model_compose-0.1.1 → model_compose-0.1.2}/src/mindor/dsl/schema/controller/impl/types.py +0 -0
  92. {model_compose-0.1.1 → model_compose-0.1.2}/src/mindor/dsl/schema/controller/impl/webui.py +0 -0
  93. {model_compose-0.1.1 → model_compose-0.1.2}/src/mindor/dsl/schema/gateway/__init__.py +0 -0
  94. {model_compose-0.1.1 → model_compose-0.1.2}/src/mindor/dsl/schema/gateway/gateway.py +0 -0
  95. {model_compose-0.1.1 → model_compose-0.1.2}/src/mindor/dsl/schema/gateway/impl/__init__.py +0 -0
  96. {model_compose-0.1.1 → model_compose-0.1.2}/src/mindor/dsl/schema/gateway/impl/common.py +0 -0
  97. {model_compose-0.1.1 → model_compose-0.1.2}/src/mindor/dsl/schema/gateway/impl/http_tunnel.py +0 -0
  98. {model_compose-0.1.1 → model_compose-0.1.2}/src/mindor/dsl/schema/gateway/impl/types.py +0 -0
  99. {model_compose-0.1.1 → model_compose-0.1.2}/src/mindor/dsl/schema/listener/__init__.py +0 -0
  100. {model_compose-0.1.1 → model_compose-0.1.2}/src/mindor/dsl/schema/listener/impl/__init__.py +0 -0
  101. {model_compose-0.1.1 → model_compose-0.1.2}/src/mindor/dsl/schema/listener/impl/common.py +0 -0
  102. {model_compose-0.1.1 → model_compose-0.1.2}/src/mindor/dsl/schema/listener/impl/http_callback.py +0 -0
  103. {model_compose-0.1.1 → model_compose-0.1.2}/src/mindor/dsl/schema/listener/impl/types.py +0 -0
  104. {model_compose-0.1.1 → model_compose-0.1.2}/src/mindor/dsl/schema/listener/listener.py +0 -0
  105. {model_compose-0.1.1 → model_compose-0.1.2}/src/mindor/dsl/schema/runtime/__init__.py +0 -0
  106. {model_compose-0.1.1 → model_compose-0.1.2}/src/mindor/dsl/schema/runtime/impl/__init__.py +0 -0
  107. {model_compose-0.1.1 → model_compose-0.1.2}/src/mindor/dsl/schema/runtime/impl/common.py +0 -0
  108. {model_compose-0.1.1 → model_compose-0.1.2}/src/mindor/dsl/schema/runtime/impl/docker.py +0 -0
  109. {model_compose-0.1.1 → model_compose-0.1.2}/src/mindor/dsl/schema/runtime/impl/types.py +0 -0
  110. {model_compose-0.1.1 → model_compose-0.1.2}/src/mindor/dsl/schema/runtime/runtime.py +0 -0
  111. {model_compose-0.1.1 → model_compose-0.1.2}/src/model_compose.egg-info/SOURCES.txt +0 -0
  112. {model_compose-0.1.1 → model_compose-0.1.2}/src/model_compose.egg-info/dependency_links.txt +0 -0
  113. {model_compose-0.1.1 → model_compose-0.1.2}/src/model_compose.egg-info/entry_points.txt +0 -0
  114. {model_compose-0.1.1 → model_compose-0.1.2}/src/model_compose.egg-info/requires.txt +0 -0
  115. {model_compose-0.1.1 → model_compose-0.1.2}/src/model_compose.egg-info/top_level.txt +0 -0
  116. {model_compose-0.1.1 → 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.1
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "model-compose"
3
- version = "0.1.1"
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" }
@@ -31,8 +31,8 @@ class HttpClientPollingCompletion:
31
31
 
32
32
  while datetime.now(timezone.utc) < deadline:
33
33
  response = await self.client.request(url, method, params, body, headers)
34
-
35
34
  context.register_source("result", response)
35
+
36
36
  status = (await context.render_template(self.config.status)) if self.config.status else response["status"]
37
37
 
38
38
  if not status:
@@ -82,15 +82,11 @@ class HttpClientAction:
82
82
  headers = await context.render_template({ **self.headers, **self.config.headers })
83
83
 
84
84
  response, result = await self.client.request(url, method, params, body, headers), None
85
-
86
- if response:
87
- context.register_source("response", response)
85
+ context.register_source("response", response)
88
86
 
89
87
  if self.config.completion:
90
88
  result = await self._handle_completion(self.config.completion, context)
91
-
92
- if result:
93
- context.register_source("result", result)
89
+ context.register_source("result", result)
94
90
 
95
91
  return (await context.render_template(self.config.output)) if self.config.output else (result or response)
96
92
 
@@ -1,5 +1,5 @@
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
@@ -7,18 +7,23 @@ from .schema import WorkflowSchema
7
7
  import gradio as gr
8
8
  import json
9
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
14
+
10
15
  class GradioWebUIBuilder:
11
16
  def build(self, schema: Dict[str, WorkflowSchema], runner: Callable[[Optional[str], Any], Awaitable[Any]]) -> gr.Blocks:
12
17
  with gr.Blocks() as blocks:
13
18
  for workflow_id, workflow in schema.items():
14
- async def run_workflow(input: Any, workflow_id=workflow_id) -> Any:
19
+ async def _run_workflow(input: Any, workflow_id=workflow_id) -> Any:
15
20
  return await runner(workflow_id, input)
16
21
 
17
22
  if len(schema) > 1:
18
23
  with gr.Tab(label=workflow.name or workflow_id):
19
- self._build_workflow_section(workflow, run_workflow)
24
+ self._build_workflow_section(workflow, _run_workflow)
20
25
  else:
21
- self._build_workflow_section(workflow, run_workflow)
26
+ self._build_workflow_section(workflow, _run_workflow)
22
27
 
23
28
  return blocks
24
29
 
@@ -39,27 +44,16 @@ class GradioWebUIBuilder:
39
44
  if not output_components:
40
45
  output_components = gr.Textbox(label="", lines=8, interactive=False, show_copy_button=True)
41
46
 
42
- async def run_workflow(*args):
43
- 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) }
44
49
  output = await runner(input)
45
-
46
- if workflow.output:
47
- if isinstance(output, dict):
48
- outputs = []
49
- for variable in workflow.output:
50
- value = output[variable.name] if variable.name else output
51
- outputs.append(await self._convert_type(value, variable.type, variable.subtype, variable.format))
52
- output = outputs[0] if len(outputs) == 1 else outputs
53
- else:
54
- variable = workflow.output[0]
55
- output = await self._convert_type(output, variable.type, variable.subtype, variable.format)
56
-
57
- return output
50
+ output = await self._flatten_output(output, workflow.output)
51
+ return output[0] if len(output) == 1 else output
58
52
 
59
53
  run_button.click(
60
- fn=run_workflow,
54
+ fn=_run_workflow,
61
55
  inputs=input_components,
62
- outputs=output_components
56
+ outputs=self._flatten_output_components(output_components)
63
57
  )
64
58
 
65
59
  return section
@@ -89,7 +83,16 @@ class GradioWebUIBuilder:
89
83
 
90
84
  return gr.Textbox(label=label, value=default, info=f"Unsupported type: {variable.type}")
91
85
 
92
- 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
+
93
96
  label = variable.name or ""
94
97
  info = variable.description or ""
95
98
 
@@ -104,7 +107,34 @@ class GradioWebUIBuilder:
104
107
 
105
108
  return gr.Textbox(label=label, info=f"Unsupported type: {variable.type}")
106
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
+
107
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
+
108
138
  if type == "image":
109
139
  if format == "base64" and isinstance(value, str):
110
140
  return await load_image_from_stream(Base64StreamResource(value), subtype)
@@ -119,9 +149,4 @@ class GradioWebUIBuilder:
119
149
  return await save_stream_to_temporary_file(value, subtype)
120
150
  return None
121
151
 
122
- if type == "string":
123
- if isinstance(value, (dict, list)):
124
- return json.dumps(value)
125
- return str(value)
126
-
127
152
  return value
@@ -1,8 +1,41 @@
1
1
  from typing import Type, Union, Literal, Optional, Dict, List, Tuple, Set, Annotated, Any
2
2
  from pydantic import BaseModel
3
- from mindor.dsl.schema.workflow import WorkflowConfig, WorkflowVariableConfig
3
+ from mindor.dsl.schema.workflow import WorkflowConfig, WorkflowVariableConfig, WorkflowVariableGroupConfig
4
4
  from mindor.dsl.schema.component import ComponentConfig
5
- import re, json, io
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
6
39
 
7
40
  class WorkflowVariableResolver:
8
41
  def __init__(self):
@@ -15,12 +48,13 @@ class WorkflowVariableResolver:
15
48
  (?:\s*\|\s*([^\}]+))? # default value after `|`
16
49
  \}""", # }
17
50
  re.VERBOSE,
18
- )
51
+ ),
52
+ "keypath": re.compile(r"[-_\w]+|\[\d+\]"),
19
53
  }
20
54
 
21
- def _enumerate_input_variables(self, value: Any, wanted_key: str) -> List[Tuple[str, str, str, Optional[Any]]]:
55
+ def _enumerate_input_variables(self, value: Any, wanted_key: str) -> List[WorkflowVariable]:
22
56
  if isinstance(value, str):
23
- variables: List[Tuple[str, str, str, str, Optional[Any]]] = []
57
+ variables: List[WorkflowVariable] = []
24
58
 
25
59
  for match in self.patterns["variable"].finditer(value):
26
60
  key, path, type, subtype, format, default = match.group(1, 2, 3, 4, 5, 6)
@@ -29,7 +63,7 @@ class WorkflowVariableResolver:
29
63
  default = self._parse_as_type(default, type)
30
64
 
31
65
  if key == wanted_key:
32
- variables.append((path, type or "string", subtype, format, default))
66
+ variables.append(WorkflowVariable(path, type or "string", subtype, format, default))
33
67
 
34
68
  return variables
35
69
 
@@ -44,31 +78,51 @@ class WorkflowVariableResolver:
44
78
 
45
79
  return []
46
80
 
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]]] = []
81
+ def _enumerate_output_variables(self, name: Optional[str], value: Any) -> List[WorkflowVariable]:
82
+ variables: List[WorkflowVariable] = []
49
83
 
50
84
  if isinstance(value, str):
51
85
  for match in self.patterns["variable"].finditer(value):
52
- type, subtype, format, default = match.group(3, 4, 5, 6)
86
+ key, path, type, subtype, format, default = match.group(1, 2, 3, 4, 5, 6)
53
87
 
54
88
  if type and default:
55
89
  default = self._parse_as_type(default, type)
56
90
 
57
- variables.append((key, type or "string", subtype, format, default))
91
+ variables.append(WorkflowVariable(name, type or "string", subtype, format, default))
58
92
 
59
93
  return variables
60
94
 
61
95
  if isinstance(value, BaseModel):
62
- return self._enumerate_output_variables(key, value.model_dump(exclude_none=True))
96
+ return self._enumerate_output_variables(name, value.model_dump(exclude_none=True))
63
97
 
64
98
  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() ], [])
99
+ return sum([ self._enumerate_output_variables(f"{name}.{k}" if name else f"{k}", v) for k, v in value.items() ], [])
66
100
 
67
101
  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) ], [])
102
+ return sum([ self._enumerate_output_variables(f"{name}[{i}]" if name else f"[{i}]", v) for i, v in enumerate(value) ], [])
69
103
 
70
104
  return []
71
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
+
72
126
  def _parse_as_type(self, value: Any, type: str) -> Any:
73
127
  if type == "number":
74
128
  return float(value)
@@ -86,7 +140,7 @@ class WorkflowVariableResolver:
86
140
 
87
141
  class WorkflowInputResolver(WorkflowVariableResolver):
88
142
  def resolve(self, workflow: WorkflowConfig, components: Dict[str, ComponentConfig]) -> List[WorkflowVariableConfig]:
89
- variables: List[Tuple[str, str, Optional[Any]]] = []
143
+ variables: List[WorkflowVariable] = []
90
144
 
91
145
  for job in workflow.jobs.values():
92
146
  if not job.input or job.input == "${input}":
@@ -100,36 +154,45 @@ class WorkflowInputResolver(WorkflowVariableResolver):
100
154
  else:
101
155
  variables.extend(self._enumerate_input_variables(job.input, "input"))
102
156
 
103
- return [ WorkflowVariableConfig(name=name, type=type, subtype=subtype, format=format, default=default) for name, type, subtype, format, default in set(variables) ]
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)
104
161
 
105
162
  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
-
163
+ def resolve(self, workflow: WorkflowConfig, components: Dict[str, ComponentConfig]) -> List[Union[WorkflowVariableConfig, WorkflowVariableGroupConfig]]:
164
+ variables: List[Union[WorkflowVariable, WorkflowVariableGroup]] = []
165
+
109
166
  for job_id, job in workflow.jobs.items():
110
167
  if not self._is_terminal_job(workflow, job_id):
111
168
  continue
112
-
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
+
113
176
  if not job.output or job.output == "${output}":
114
177
  action_id = job.action or "__default__"
115
178
  if isinstance(job.component, str):
116
179
  component: Optional[ComponentConfig] = components[job.component] if job.component in components else None
117
180
  if component:
118
- variables.extend(self._enumerate_output_variables(None, component.actions[action_id].output))
181
+ job_variables.extend(self._enumerate_output_variables(None, component.actions[action_id].output))
119
182
  else:
120
- variables.extend(self._enumerate_output_variables(None, job.component.actions[action_id].output))
183
+ job_variables.extend(self._enumerate_output_variables(None, job.component.actions[action_id].output))
121
184
  else:
122
- variables.extend(self._enumerate_output_variables(None, job.output))
185
+ job_variables.extend(self._enumerate_output_variables(None, job.output))
123
186
 
124
- return [ WorkflowVariableConfig(name=name, type=type, subtype=subtype, format=format, default=default) for name, type, subtype, format, default in set(variables) ]
187
+ return self._to_variable_config_list(variables)
125
188
 
126
189
  def _is_terminal_job(self, workflow: WorkflowConfig, job_id: str) -> bool:
127
190
  return all(job_id not in job.depends_on for other_id, job in workflow.jobs.items() if other_id != job_id)
128
191
 
129
192
  class WorkflowSchema:
130
- def __init__(self, name: str, title: str, description: Optional[str], input: List[WorkflowVariableConfig], output: List[WorkflowVariableConfig]):
193
+ def __init__(self, name: str, title: str, description: Optional[str], input: List[WorkflowVariableConfig], output: List[Union[WorkflowVariableConfig, WorkflowVariableGroupConfig]]):
131
194
  self.name: str = name
132
195
  self.title: str = title
133
196
  self.description: Optional[str] = description
134
197
  self.input: List[WorkflowVariableConfig] = input
135
- self.output: List[WorkflowVariableConfig] = output
198
+ self.output: List[Union[WorkflowVariableConfig, WorkflowVariableGroupConfig]] = output
@@ -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
@@ -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
 
@@ -48,7 +48,7 @@ class TemplateRenderer:
48
48
 
49
49
  value = default if value is None else value
50
50
 
51
- if type and value:
51
+ if type and value is not None:
52
52
  value = await self._convert_type(value, type, subtype, format)
53
53
 
54
54
  if variable == text:
@@ -85,7 +85,7 @@ class TemplateRenderer:
85
85
 
86
86
  if type == "integer":
87
87
  return int(value)
88
-
88
+
89
89
  if type == "json":
90
90
  return json.loads(value)
91
91
 
@@ -3,7 +3,7 @@ from mindor.dsl.schema.workflow import JobConfig
3
3
  from mindor.dsl.schema.component import ComponentConfig
4
4
  from mindor.core.component import ComponentEngine
5
5
  from .context import WorkflowContext
6
- import ulid
6
+ import asyncio, ulid
7
7
 
8
8
  class Job:
9
9
  def __init__(self, id: str, config: JobConfig, component_provider: Callable[[str, Union[ComponentConfig, str]], ComponentEngine]):
@@ -20,16 +20,18 @@ class Job:
20
20
  input = (await context.render_template(self.config.input)) if self.config.input else context.input
21
21
  outputs = []
22
22
 
23
- for _ in range(self.config.repeats):
23
+ async def _run_once():
24
24
  call_id = ulid.ulid()
25
25
  output = await component.run(self.config.action, call_id, input)
26
-
27
- if output:
28
- outputs.append(output)
26
+ context.register_source("output", output)
29
27
 
30
- output = outputs[0] if len(outputs) == 1 else outputs or None
28
+ output = (await context.render_template(self.config.output)) if self.config.output else output
29
+ outputs.append(output)
31
30
 
32
- if output:
33
- context.register_source("output", output)
31
+ repeat_count = (await context.render_template(self.config.repeat_count)) if self.config.repeat_count else None
32
+ await asyncio.gather(*[ _run_once() for _ in range(int(repeat_count or 1)) ])
33
+
34
+ output = outputs[0] if len(outputs) == 1 else outputs or None
35
+ context.register_source("output", output)
34
36
 
35
- return (await context.render_template(self.config.output)) if self.config.output else output
37
+ return output
@@ -1,17 +1,23 @@
1
1
  from typing import Type, Union, Literal, Optional, Dict, List, Tuple, Set, Annotated, Any
2
2
  from enum import Enum
3
3
  from pydantic import BaseModel, Field
4
- from pydantic import model_validator
4
+ from pydantic import model_validator, field_validator
5
5
  from .component import ComponentConfig
6
6
 
7
7
  class JobConfig(BaseModel):
8
8
  component: Optional[Union[str, 'ComponentConfig']] = Field(default="__default__", description="The component to execute. Can be a string identifier or a ComponentConfig object.")
9
9
  action: Optional[str] = Field(default="__default__", description="The action to invoke within the component. Defaults to '__default__'.")
10
- repeats: Optional[int] = Field(default=1, ge=1, description="Number of times to repeat the component execution. Must be at least 1.")
11
10
  input: Optional[Any] = Field(default=None, description="The input data passed to the component. Can be of any type.")
12
11
  output: Optional[Any] = Field(default=None, description="The expected output data from the component. Can be of any type.")
12
+ repeat_count: Optional[Union[int, str]] = Field(default=1, description="Number of times to repeat the component execution. Must be at least 1.")
13
13
  depends_on: Optional[List[str]] = Field(default_factory=list, description="List of job names that this job depends on. Ensures execution order.")
14
14
 
15
+ @field_validator("repeat_count")
16
+ def validate_repeat_count(cls, value):
17
+ if isinstance(value, int) and value < 1:
18
+ raise ValueError("'repeat_count' must be at least 1")
19
+ return value
20
+
15
21
  class WorkflowVariableType(str, Enum):
16
22
  # Primitive data types
17
23
  STRING = "string"
@@ -44,6 +50,11 @@ class WorkflowVariableConfig(BaseModel):
44
50
  description: Optional[str] = Field(default=None, description="Description of the variable")
45
51
  options: Optional[List[Any]] = Field(default=None, description="List of valid options for select type")
46
52
 
53
+ class WorkflowVariableGroupConfig(BaseModel):
54
+ name: Optional[str] = Field(default=None, description="The name of the group of variables")
55
+ variables: List[WorkflowVariableConfig] = Field(default_factory=list, description="List of variables included in this group")
56
+ repeat_count: int = Field(default=1, description="The number of times this group of variables should be repeated")
57
+
47
58
  class WorkflowConfig(BaseModel):
48
59
  name: Optional[str] = None
49
60
  title: Optional[str] = None
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: model-compose
3
- Version: 0.1.1
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
File without changes
File without changes
File without changes