model-compose 0.1.1__tar.gz → 0.1.3__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 (117) hide show
  1. {model_compose-0.1.1 → model_compose-0.1.3}/PKG-INFO +1 -1
  2. {model_compose-0.1.1 → model_compose-0.1.3}/pyproject.toml +1 -1
  3. {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/core/component/engine/http_client.py +3 -7
  4. {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/core/controller/engine/http_server.py +2 -2
  5. {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/core/controller/webui/gradio.py +71 -33
  6. model_compose-0.1.3/src/mindor/core/controller/webui/schema.py +206 -0
  7. {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/core/controller/webui/webui.py +3 -3
  8. {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/core/listener/engine/http_callback.py +2 -2
  9. {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/core/utils/http_client.py +6 -8
  10. {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/core/utils/template.py +2 -2
  11. {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/core/workflow/job.py +13 -11
  12. {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/dsl/schema/workflow.py +16 -3
  13. {model_compose-0.1.1 → model_compose-0.1.3}/src/model_compose.egg-info/PKG-INFO +1 -1
  14. model_compose-0.1.1/src/mindor/core/controller/webui/schema.py +0 -135
  15. {model_compose-0.1.1 → model_compose-0.1.3}/LICENSE +0 -0
  16. {model_compose-0.1.1 → model_compose-0.1.3}/README.md +0 -0
  17. {model_compose-0.1.1 → model_compose-0.1.3}/setup.cfg +0 -0
  18. {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/__init__.py +0 -0
  19. {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/cli/__init__.py +0 -0
  20. {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/cli/compose.py +0 -0
  21. {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/core/__init__.py +0 -0
  22. {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/core/component/__init__.py +0 -0
  23. {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/core/component/component.py +0 -0
  24. {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/core/component/engine/__init__.py +0 -0
  25. {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/core/component/engine/base.py +0 -0
  26. {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/core/component/engine/context.py +0 -0
  27. {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/core/component/engine/http_server.py +0 -0
  28. {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/core/compose/__init__.py +0 -0
  29. {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/core/compose/compose.py +0 -0
  30. {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/core/compose/manager.py +0 -0
  31. {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/core/controller/__init__.py +0 -0
  32. {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/core/controller/controller.py +0 -0
  33. {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/core/controller/engine/__init__.py +0 -0
  34. {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/core/controller/engine/base.py +0 -0
  35. {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/core/controller/engine/mcp_server.py +0 -0
  36. {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/core/controller/runner/__init__.py +0 -0
  37. {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/core/controller/runner/client.py +0 -0
  38. {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/core/controller/runner/http_client.py +0 -0
  39. {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/core/controller/runner/mcp_client.py +0 -0
  40. {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/core/controller/runner/runner.py +0 -0
  41. {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/core/controller/webui/__init__.py +0 -0
  42. {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/core/gateway/__init__.py +0 -0
  43. {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/core/gateway/engine/__init__.py +0 -0
  44. {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/core/gateway/engine/base.py +0 -0
  45. {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/core/gateway/engine/http_tunnel.py +0 -0
  46. {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/core/gateway/gateway.py +0 -0
  47. {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/core/listener/__init__.py +0 -0
  48. {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/core/listener/engine/__init__.py +0 -0
  49. {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/core/listener/engine/base.py +0 -0
  50. {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/core/listener/listener.py +0 -0
  51. {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/core/runtime/__init__.py +0 -0
  52. {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/core/runtime/env.py +0 -0
  53. {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/core/services/__init__.py +0 -0
  54. {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/core/services/async_service.py +0 -0
  55. {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/core/utils/__init__.py +0 -0
  56. {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/core/utils/expiring.py +0 -0
  57. {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/core/utils/http_request.py +0 -0
  58. {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/core/utils/image.py +0 -0
  59. {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/core/utils/streaming.py +0 -0
  60. {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/core/utils/time.py +0 -0
  61. {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/core/utils/workqueue.py +0 -0
  62. {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/core/workflow/__init__.py +0 -0
  63. {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/core/workflow/context.py +0 -0
  64. {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/core/workflow/workflow.py +0 -0
  65. {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/dsl/__init__.py +0 -0
  66. {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/dsl/loader.py +0 -0
  67. {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/dsl/schema/__init__.py +0 -0
  68. {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/dsl/schema/action/__init__.py +0 -0
  69. {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/dsl/schema/action/action.py +0 -0
  70. {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/dsl/schema/action/impl/__init__.py +0 -0
  71. {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/dsl/schema/action/impl/common.py +0 -0
  72. {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/dsl/schema/action/impl/http_client.py +0 -0
  73. {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/dsl/schema/action/impl/http_server.py +0 -0
  74. {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/dsl/schema/action/impl/mcp_client.py +0 -0
  75. {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/dsl/schema/action/impl/mcp_server.py +0 -0
  76. {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/dsl/schema/component/__init__.py +0 -0
  77. {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/dsl/schema/component/component.py +0 -0
  78. {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/dsl/schema/component/impl/__init__.py +0 -0
  79. {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/dsl/schema/component/impl/common.py +0 -0
  80. {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/dsl/schema/component/impl/http_client.py +0 -0
  81. {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/dsl/schema/component/impl/http_server.py +0 -0
  82. {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/dsl/schema/component/impl/mcp_client.py +0 -0
  83. {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/dsl/schema/component/impl/mcp_server.py +0 -0
  84. {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/dsl/schema/component/impl/types.py +0 -0
  85. {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/dsl/schema/compose.py +0 -0
  86. {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/dsl/schema/controller/__init__.py +0 -0
  87. {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/dsl/schema/controller/controller.py +0 -0
  88. {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/dsl/schema/controller/impl/__init__.py +0 -0
  89. {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/dsl/schema/controller/impl/common.py +0 -0
  90. {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/dsl/schema/controller/impl/http_server.py +0 -0
  91. {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/dsl/schema/controller/impl/mcp_server.py +0 -0
  92. {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/dsl/schema/controller/impl/types.py +0 -0
  93. {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/dsl/schema/controller/impl/webui.py +0 -0
  94. {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/dsl/schema/gateway/__init__.py +0 -0
  95. {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/dsl/schema/gateway/gateway.py +0 -0
  96. {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/dsl/schema/gateway/impl/__init__.py +0 -0
  97. {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/dsl/schema/gateway/impl/common.py +0 -0
  98. {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/dsl/schema/gateway/impl/http_tunnel.py +0 -0
  99. {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/dsl/schema/gateway/impl/types.py +0 -0
  100. {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/dsl/schema/listener/__init__.py +0 -0
  101. {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/dsl/schema/listener/impl/__init__.py +0 -0
  102. {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/dsl/schema/listener/impl/common.py +0 -0
  103. {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/dsl/schema/listener/impl/http_callback.py +0 -0
  104. {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/dsl/schema/listener/impl/types.py +0 -0
  105. {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/dsl/schema/listener/listener.py +0 -0
  106. {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/dsl/schema/runtime/__init__.py +0 -0
  107. {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/dsl/schema/runtime/impl/__init__.py +0 -0
  108. {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/dsl/schema/runtime/impl/common.py +0 -0
  109. {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/dsl/schema/runtime/impl/docker.py +0 -0
  110. {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/dsl/schema/runtime/impl/types.py +0 -0
  111. {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/dsl/schema/runtime/runtime.py +0 -0
  112. {model_compose-0.1.1 → model_compose-0.1.3}/src/model_compose.egg-info/SOURCES.txt +0 -0
  113. {model_compose-0.1.1 → model_compose-0.1.3}/src/model_compose.egg-info/dependency_links.txt +0 -0
  114. {model_compose-0.1.1 → model_compose-0.1.3}/src/model_compose.egg-info/entry_points.txt +0 -0
  115. {model_compose-0.1.1 → model_compose-0.1.3}/src/model_compose.egg-info/requires.txt +0 -0
  116. {model_compose-0.1.1 → model_compose-0.1.3}/src/model_compose.egg-info/top_level.txt +0 -0
  117. {model_compose-0.1.1 → model_compose-0.1.3}/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.3
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.3"
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
 
@@ -15,7 +15,7 @@ from fastapi import FastAPI, APIRouter, Request, Body, HTTPException
15
15
  from fastapi.middleware.cors import CORSMiddleware
16
16
  from fastapi.responses import Response, JSONResponse, StreamingResponse
17
17
  from starlette.background import BackgroundTask
18
- import asyncio, uvicorn
18
+ import uvicorn
19
19
 
20
20
  class WorkflowTaskRequestBody(BaseModel):
21
21
  workflow_id: Optional[str] = None
@@ -50,7 +50,7 @@ class HttpServerController(ControllerEngine):
50
50
  daemon: bool
51
51
  ):
52
52
  super().__init__(config, components, listeners, gateways, workflows, env, daemon)
53
-
53
+
54
54
  self.server: Optional[uvicorn.Server] = None
55
55
  self.app: FastAPI = FastAPI(openapi_url=None, docs_url=None, redoc_url=None)
56
56
  self.router: APIRouter = APIRouter()
@@ -1,31 +1,36 @@
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
3
- from mindor.core.utils.http_client import HttpStreamResource
2
+ from mindor.dsl.schema.workflow import WorkflowVariableConfig, WorkflowVariableGroupConfig
3
+ from mindor.core.utils.http_client import HttpClient, HttpStreamResource
4
4
  from mindor.core.utils.image import load_image_from_stream
5
5
  from mindor.core.utils.streaming import Base64StreamResource, save_stream_to_temporary_file
6
6
  from .schema import WorkflowSchema
7
7
  import gradio as gr
8
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
 
25
30
  def _build_workflow_section(self, workflow: WorkflowSchema, runner: Callable[[Any], Awaitable[Any]]) -> gr.Column:
26
31
  with gr.Column() as section:
27
- gr.Markdown(f"## **{workflow.title or 'Untitled Workflow'}**")
28
-
32
+ gr.Markdown(f"## **{workflow.title or 'Untitled Workflow'}**")
33
+
29
34
  if workflow.description:
30
35
  gr.Markdown(f"📝 {workflow.description}")
31
36
 
@@ -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
@@ -80,16 +74,31 @@ class GradioWebUIBuilder:
80
74
 
81
75
  if variable.type == "boolean":
82
76
  return gr.Checkbox(label=label, value=default or False, info=info)
77
+
78
+ if variable.type == "image":
79
+ return gr.Image(label=label, type="filepath")
80
+
81
+ if variable.type == "video":
82
+ return gr.Video(label=label)
83
83
 
84
84
  if variable.type == "file":
85
- return gr.File(label=label, file_types=["*"], info=info)
85
+ return gr.File(label=label)
86
86
 
87
87
  if variable.type == "select":
88
88
  return gr.Dropdown(choices=variable.options or [], label=label, value=default, info=info)
89
-
89
+
90
90
  return gr.Textbox(label=label, value=default, info=f"Unsupported type: {variable.type}")
91
91
 
92
- def _build_output_component(self, variable: WorkflowVariableConfig) -> gr.Component:
92
+ def _build_output_component(self, variable: Union[WorkflowVariableConfig, WorkflowVariableGroupConfig]) -> Union[gr.Component, List[ComponentGroup]]:
93
+ if isinstance(variable, WorkflowVariableGroupConfig):
94
+ groups: List[ComponentGroup] = []
95
+ for index in range(variable.repeat_count if variable.repeat_count != 0 else 100):
96
+ visible = True if variable.repeat_count != 0 or index == 0 else False
97
+ with gr.Column(visible=visible) as group:
98
+ components = [ self._build_output_component(v) for v in variable.variables ]
99
+ groups.append(ComponentGroup(group, components))
100
+ return groups
101
+
93
102
  label = variable.name or ""
94
103
  info = variable.description or ""
95
104
 
@@ -102,26 +111,55 @@ class GradioWebUIBuilder:
102
111
  if variable.type == "audio":
103
112
  return gr.Audio(label=label)
104
113
 
114
+ if variable.type == "video":
115
+ return gr.Video(label=label)
116
+
105
117
  return gr.Textbox(label=label, info=f"Unsupported type: {variable.type}")
106
118
 
119
+ def _flatten_output_components(self, components: List[Union[gr.Component, List[ComponentGroup]]]) -> List[gr.Component]:
120
+ flattened = []
121
+ for item in components:
122
+ if isinstance(item, list):
123
+ for group in item:
124
+ flattened.extend(group.components)
125
+ else:
126
+ flattened.append(item)
127
+ return flattened
128
+
129
+ async def _flatten_output(self, output: Any, variables: List[Union[WorkflowVariableConfig, WorkflowVariableGroupConfig]]) -> Any:
130
+ flattened = []
131
+ for variable in variables:
132
+ if isinstance(variable, WorkflowVariableGroupConfig):
133
+ group = output[variable.name] if variable.name else output
134
+ for value in group:
135
+ flattened.extend(await self._flatten_output(value, variable.variables))
136
+ else:
137
+ value = output[variable.name] if variable.name else output
138
+ flattened.append(await self._convert_type(value, variable.type, variable.subtype, variable.format))
139
+ return flattened
140
+
107
141
  async def _convert_type(self, value: Any, type: Optional[str], subtype: Optional[str], format: Optional[str]) -> Any:
142
+ if type == "string":
143
+ if isinstance(value, (dict, list)):
144
+ return json.dumps(value)
145
+ return str(value)
146
+
108
147
  if type == "image":
148
+ if format == "url" and isinstance(value, str):
149
+ return await load_image_from_stream(await HttpClient().request(value), subtype)
109
150
  if format == "base64" and isinstance(value, str):
110
151
  return await load_image_from_stream(Base64StreamResource(value), subtype)
111
152
  if isinstance(value, HttpStreamResource):
112
153
  return await load_image_from_stream(value, subtype)
113
154
  return None
114
155
 
115
- if type == "audio":
156
+ if type in [ "audio", "video" ]:
157
+ if format == "url" and isinstance(value, str):
158
+ return await save_stream_to_temporary_file(await HttpClient().request(value), subtype)
116
159
  if format == "base64" and isinstance(value, str):
117
160
  return await save_stream_to_temporary_file(Base64StreamResource(value), subtype)
118
161
  if isinstance(value, HttpStreamResource):
119
162
  return await save_stream_to_temporary_file(value, subtype)
120
163
  return None
121
164
 
122
- if type == "string":
123
- if isinstance(value, (dict, list)):
124
- return json.dumps(value)
125
- return str(value)
126
-
127
165
  return value
@@ -0,0 +1,206 @@
1
+ from typing import Type, Union, Literal, Optional, Dict, List, Tuple, Set, Annotated, Any
2
+ from pydantic import BaseModel
3
+ from mindor.dsl.schema.workflow import WorkflowConfig, WorkflowVariableConfig, WorkflowVariableGroupConfig
4
+ from mindor.dsl.schema.component import ComponentConfig
5
+ import re, json
6
+
7
+ class WorkflowVariable:
8
+ def __init__(self, name: Optional[str], type: str, subtype: Optional[str], format: Optional[str], default: Optional[Any]):
9
+ self.name: Optional[str] = name
10
+ self.type: str = type
11
+ self.subtype: Optional[str] = subtype
12
+ self.format: Optional[str] = format
13
+ self.default: Optional[Any] = default
14
+
15
+ def to_dict(self) -> Dict[str, Any]:
16
+ return {
17
+ "name": self.name,
18
+ "type": self.type,
19
+ "subtype": self.subtype,
20
+ "format": self.format,
21
+ "default": self.default
22
+ }
23
+
24
+ def __eq__(self, other):
25
+ if not isinstance(other, WorkflowVariable):
26
+ return False
27
+ if self.name is None or other.name is None:
28
+ return False
29
+ return self.name == other.name
30
+
31
+ def __hash__(self):
32
+ return hash(self.name) if self.name is not None else id(self)
33
+
34
+ class WorkflowVariableGroup:
35
+ def __init__(self, name: Optional[str], variables: List[WorkflowVariable], repeat_count: int):
36
+ self.name: Optional[str] = name
37
+ self.variables: List[WorkflowVariable] = variables
38
+ self.repeat_count: int = repeat_count
39
+
40
+ class WorkflowVariableResolver:
41
+ def __init__(self):
42
+ self.patterns: Dict[str, re.Pattern] = {
43
+ "variable": re.compile(
44
+ r"""\$\{ # ${
45
+ ([a-zA-Z_][^.\s]*) # key: input, env, etc.
46
+ (?:\.([^\s\|\}]+))? # path: key, key.path[0], etc.
47
+ (?:\s*as\s*([^\s\|\}/;]+)(?:/([^\s\|\};]+))?(?:;([^\s\|\}]+))?)? # type/subtype;format
48
+ (?:\s*\|\s*([^\}]+))? # default value after `|`
49
+ \}""", # }
50
+ re.VERBOSE,
51
+ ),
52
+ "keypath": re.compile(r"[-_\w]+|\[\d+\]"),
53
+ }
54
+
55
+ def _enumerate_input_variables(self, value: Any, wanted_key: str) -> List[WorkflowVariable]:
56
+ if isinstance(value, str):
57
+ variables: List[WorkflowVariable] = []
58
+
59
+ for match in self.patterns["variable"].finditer(value):
60
+ key, path, type, subtype, format, default = match.group(1, 2, 3, 4, 5, 6)
61
+
62
+ if type and default:
63
+ default = self._parse_as_type(default, type)
64
+
65
+ if key == wanted_key:
66
+ variables.append(WorkflowVariable(path, type or "string", subtype, format, default))
67
+
68
+ return variables
69
+
70
+ if isinstance(value, BaseModel):
71
+ return self._enumerate_input_variables(value.model_dump(exclude_none=True), wanted_key)
72
+
73
+ if isinstance(value, dict):
74
+ return sum([ self._enumerate_input_variables(v, wanted_key) for v in value.values() ], [])
75
+
76
+ if isinstance(value, list):
77
+ return sum([ self._enumerate_input_variables(v, wanted_key) for v in value ], [])
78
+
79
+ return []
80
+
81
+ def _enumerate_output_variables(self, name: Optional[str], value: Any) -> List[WorkflowVariable]:
82
+ variables: List[WorkflowVariable] = []
83
+
84
+ if isinstance(value, str):
85
+ for match in self.patterns["variable"].finditer(value):
86
+ key, path, type, subtype, format, default = match.group(1, 2, 3, 4, 5, 6)
87
+
88
+ if type and default:
89
+ default = self._parse_as_type(default, type)
90
+
91
+ variables.append(WorkflowVariable(name, type or "string", subtype, format, default))
92
+
93
+ return variables
94
+
95
+ if isinstance(value, BaseModel):
96
+ return self._enumerate_output_variables(name, value.model_dump(exclude_none=True))
97
+
98
+ if isinstance(value, dict):
99
+ return sum([ self._enumerate_output_variables(f"{name}.{k}" if name else f"{k}", v) for k, v in value.items() ], [])
100
+
101
+ if isinstance(value, list):
102
+ return sum([ self._enumerate_output_variables(f"{name}[{i}]" if name else f"[{i}]", v) for i, v in enumerate(value) ], [])
103
+
104
+ return []
105
+
106
+ def _to_variable_config_list(self, variables: List[Union[WorkflowVariable, WorkflowVariableGroup]]) -> List[Union[WorkflowVariableConfig, WorkflowVariableGroupConfig]]:
107
+ configs: List[Union[WorkflowVariableConfig, WorkflowVariableGroupConfig]] = []
108
+ seen_single: Set[WorkflowVariable] = set()
109
+
110
+ for item in variables:
111
+ if isinstance(item, WorkflowVariableGroup):
112
+ group: List[WorkflowVariableConfig] = []
113
+ seen_in_group: Set[WorkflowVariable] = set()
114
+ for variable in item.variables:
115
+ if variable not in seen_in_group:
116
+ group.append(self._to_variable_config(variable))
117
+ seen_in_group.add(variable)
118
+ configs.append(WorkflowVariableGroupConfig(name=item.name, variables=group, repeat_count=item.repeat_count))
119
+ else:
120
+ if item not in seen_single:
121
+ configs.append(self._to_variable_config(item))
122
+ seen_single.add(item)
123
+
124
+ return configs
125
+
126
+ def _to_variable_config(self, variable: WorkflowVariable) -> WorkflowVariableConfig:
127
+ config_dict = variable.to_dict()
128
+
129
+ if variable.type == "select" and variable.subtype:
130
+ config_dict["options"] = variable.subtype.split(",")
131
+
132
+ return WorkflowVariableConfig(**config_dict)
133
+
134
+ def _parse_as_type(self, value: Any, type: str) -> Any:
135
+ if type == "number":
136
+ return float(value)
137
+
138
+ if type == "integer":
139
+ return int(value)
140
+
141
+ if type == "boolean":
142
+ return str(value).lower() in [ "true", "1" ]
143
+
144
+ if type == "json":
145
+ return json.loads(value)
146
+
147
+ return value
148
+
149
+ class WorkflowInputVariableResolver(WorkflowVariableResolver):
150
+ def resolve(self, workflow: WorkflowConfig, components: Dict[str, ComponentConfig]) -> List[WorkflowVariableConfig]:
151
+ variables: List[WorkflowVariable] = []
152
+
153
+ for job in workflow.jobs.values():
154
+ if not job.input or job.input == "${input}":
155
+ action_id = job.action or "__default__"
156
+ if isinstance(job.component, str):
157
+ component: Optional[ComponentConfig] = components[job.component] if job.component in components else None
158
+ if component:
159
+ variables.extend(self._enumerate_input_variables(component.actions[action_id], "input"))
160
+ else:
161
+ variables.extend(self._enumerate_input_variables(job.component.actions[action_id], "input"))
162
+ else:
163
+ variables.extend(self._enumerate_input_variables(job.input, "input"))
164
+
165
+ for value in [ job.component, job.action, job.repeat_count ]:
166
+ variables.extend(self._enumerate_input_variables(value, "input"))
167
+
168
+ return self._to_variable_config_list(variables)
169
+
170
+ class WorkflowOutputVariableResolver(WorkflowVariableResolver):
171
+ def resolve(self, workflow: WorkflowConfig, components: Dict[str, ComponentConfig]) -> List[Union[WorkflowVariableConfig, WorkflowVariableGroupConfig]]:
172
+ variables: List[Union[WorkflowVariable, WorkflowVariableGroup]] = []
173
+
174
+ for job_id, job in workflow.jobs.items():
175
+ if not self._is_terminal_job(workflow, job_id):
176
+ continue
177
+
178
+ job_variables: List[WorkflowVariable] = variables
179
+ repeat_count: int = job.repeat_count if isinstance(job.repeat_count, int) else 0
180
+
181
+ if repeat_count != 1:
182
+ variables.append(WorkflowVariableGroup(None, job_variables := [], repeat_count))
183
+
184
+ if not job.output or job.output == "${output}":
185
+ action_id = job.action or "__default__"
186
+ if isinstance(job.component, str):
187
+ component: Optional[ComponentConfig] = components[job.component] if job.component in components else None
188
+ if component:
189
+ job_variables.extend(self._enumerate_output_variables(None, component.actions[action_id].output))
190
+ else:
191
+ job_variables.extend(self._enumerate_output_variables(None, job.component.actions[action_id].output))
192
+ else:
193
+ job_variables.extend(self._enumerate_output_variables(None, job.output))
194
+
195
+ return self._to_variable_config_list(variables)
196
+
197
+ def _is_terminal_job(self, workflow: WorkflowConfig, job_id: str) -> bool:
198
+ return all(job_id not in job.depends_on for other_id, job in workflow.jobs.items() if other_id != job_id)
199
+
200
+ class WorkflowSchema:
201
+ def __init__(self, name: str, title: str, description: Optional[str], input: List[WorkflowVariableConfig], output: List[Union[WorkflowVariableConfig, WorkflowVariableGroupConfig]]):
202
+ self.name: str = name
203
+ self.title: str = title
204
+ self.description: Optional[str] = description
205
+ self.input: List[WorkflowVariableConfig] = input
206
+ self.output: List[Union[WorkflowVariableConfig, WorkflowVariableGroupConfig]] = output
@@ -4,7 +4,7 @@ from mindor.dsl.schema.component import ComponentConfig
4
4
  from mindor.dsl.schema.workflow import WorkflowConfig
5
5
  from mindor.core.controller.runner import ControllerRunner
6
6
  from mindor.core.services import AsyncService
7
- from .schema import WorkflowSchema, WorkflowInputResolver, WorkflowOutputResolver
7
+ from .schema import WorkflowSchema, WorkflowInputVariableResolver, WorkflowOutputVariableResolver
8
8
  from .gradio import GradioWebUIBuilder
9
9
 
10
10
  from fastapi import FastAPI, APIRouter, Body, HTTPException
@@ -68,8 +68,8 @@ class ControllerWebUI(AsyncService):
68
68
  name=workflow.name,
69
69
  title=workflow.title,
70
70
  description=workflow.description,
71
- input=WorkflowInputResolver().resolve(workflow, self.components),
72
- output=WorkflowOutputResolver().resolve(workflow, self.components)
71
+ input=WorkflowInputVariableResolver().resolve(workflow, self.components),
72
+ output=WorkflowOutputVariableResolver().resolve(workflow, self.components)
73
73
  )
74
74
 
75
75
  return schema
@@ -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
@@ -1,9 +1,7 @@
1
1
  from typing import Type, Union, Literal, Optional, Dict, List, Tuple, Set, Annotated, AsyncIterator, Any
2
2
  from .http_request import serialize_request_body, parse_options_header
3
3
  from .streaming import StreamResource
4
-
5
- from urllib.parse import urlencode
6
- import aiohttp, json
4
+ import aiohttp
7
5
 
8
6
  class HttpStreamResource(StreamResource):
9
7
  def __init__(self, session: aiohttp.ClientSession, stream: aiohttp.StreamReader, content_type: Optional[str] = None, filename: Optional[str] = None):
@@ -28,7 +26,7 @@ class HttpStreamResource(StreamResource):
28
26
  yield chunk
29
27
 
30
28
  class HttpClient:
31
- async def request(self, url: str, method: str, params: Optional[Dict[str, Any]], body: Optional[Any], headers: Optional[Dict[str, str]]) -> Any:
29
+ async def request(self, url: str, method: Optional[str] = "GET", params: Optional[Dict[str, Any]] = None, body: Optional[Any] = None, headers: Optional[Dict[str, str]] = None) -> Any:
32
30
  session = aiohttp.ClientSession()
33
31
  try:
34
32
  response = await session.request(
@@ -39,11 +37,11 @@ class HttpClient:
39
37
  headers=headers
40
38
  )
41
39
 
42
- if response.status >= 400:
43
- raise ValueError(f"Request failed with status {response.status}")
44
-
45
40
  content = await self._parse_response_content(session, response)
46
41
 
42
+ if response.status >= 400:
43
+ raise ValueError(f"Request failed with status {response.status}: {content}")
44
+
47
45
  if not isinstance(content, HttpStreamResource):
48
46
  await session.close()
49
47
 
@@ -57,7 +55,7 @@ class HttpClient:
57
55
 
58
56
  if content_type and body:
59
57
  return await serialize_request_body(body, content_type)
60
-
58
+
61
59
  return body
62
60
 
63
61
  async def _parse_response_content(self, session: aiohttp.ClientSession, response: aiohttp.ClientResponse) -> Any:
@@ -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]):
@@ -12,7 +12,7 @@ class Job:
12
12
  self.component_provider: Callable[[str, Union[ComponentConfig, str]], ComponentEngine] = component_provider
13
13
 
14
14
  async def run(self, context: WorkflowContext) -> Any:
15
- component: ComponentEngine = self.component_provider(self.id, self.config.component)
15
+ component: ComponentEngine = self.component_provider(self.id, await context.render_template(self.config.component))
16
16
 
17
17
  if not component.started:
18
18
  await component.start()
@@ -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
- output = await component.run(self.config.action, call_id, input)
26
-
27
- if output:
28
- outputs.append(output)
25
+ output = await component.run(await context.render_template(self.config.action), call_id, input)
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"
@@ -25,6 +31,7 @@ class WorkflowVariableType(str, Enum):
25
31
  # Media and files
26
32
  IMAGE = "image"
27
33
  AUDIO = "audio"
34
+ VIDEO = "video"
28
35
  FILE = "file"
29
36
  # UI-related types
30
37
  SELECT = "select"
@@ -33,16 +40,22 @@ class WorkflowVariableFormat(str, Enum):
33
40
  BASE64 = "base64"
34
41
  URL = "url"
35
42
  PATH = "path"
43
+ STREAM = "stream"
36
44
 
37
45
  class WorkflowVariableConfig(BaseModel):
38
46
  name: Optional[str] = Field(default=None, description="The name of the variable")
39
47
  type: WorkflowVariableType = Field(..., description="Type of the variable")
40
48
  subtype: Optional[str] = Field(default=None, description="Subtype of the variable")
41
49
  format: Optional[WorkflowVariableFormat] = Field(default=None, description="Format of the variable")
50
+ options: Optional[List[str]] = Field(default=None, description="List of valid options for select type")
42
51
  required: bool = Field(default=False, description="Whether this variable is required")
43
52
  default: Optional[Any] = Field(default=None, description="Default value if not provided")
44
53
  description: Optional[str] = Field(default=None, description="Description of the variable")
45
- options: Optional[List[Any]] = Field(default=None, description="List of valid options for select type")
54
+
55
+ class WorkflowVariableGroupConfig(BaseModel):
56
+ name: Optional[str] = Field(default=None, description="The name of the group of variables")
57
+ variables: List[WorkflowVariableConfig] = Field(default_factory=list, description="List of variables included in this group")
58
+ repeat_count: int = Field(default=1, description="The number of times this group of variables should be repeated")
46
59
 
47
60
  class WorkflowConfig(BaseModel):
48
61
  name: 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.3
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,135 +0,0 @@
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._parse_as_type(default, type)
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._parse_as_type(default, type)
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 _parse_as_type(self, value: Any, type: 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
File without changes
File without changes
File without changes