model-compose 0.1.2__tar.gz → 0.1.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. {model_compose-0.1.2 → model_compose-0.1.4}/PKG-INFO +1 -1
  2. {model_compose-0.1.2 → model_compose-0.1.4}/pyproject.toml +1 -1
  3. {model_compose-0.1.2 → model_compose-0.1.4}/src/mindor/core/controller/engine/http_server.py +2 -2
  4. model_compose-0.1.4/src/mindor/core/controller/runner/http_client.py +68 -0
  5. {model_compose-0.1.2 → model_compose-0.1.4}/src/mindor/core/controller/webui/gradio.py +55 -13
  6. {model_compose-0.1.2 → model_compose-0.1.4}/src/mindor/core/controller/webui/schema.py +16 -8
  7. {model_compose-0.1.2 → model_compose-0.1.4}/src/mindor/core/controller/webui/webui.py +3 -3
  8. model_compose-0.1.4/src/mindor/core/utils/http_client.py +97 -0
  9. {model_compose-0.1.2 → model_compose-0.1.4}/src/mindor/core/utils/http_request.py +21 -4
  10. {model_compose-0.1.2 → model_compose-0.1.4}/src/mindor/core/workflow/job.py +2 -2
  11. {model_compose-0.1.2 → model_compose-0.1.4}/src/mindor/dsl/schema/workflow.py +3 -1
  12. {model_compose-0.1.2 → model_compose-0.1.4}/src/model_compose.egg-info/PKG-INFO +1 -1
  13. model_compose-0.1.2/src/mindor/core/controller/runner/http_client.py +0 -26
  14. model_compose-0.1.2/src/mindor/core/utils/http_client.py +0 -75
  15. {model_compose-0.1.2 → model_compose-0.1.4}/LICENSE +0 -0
  16. {model_compose-0.1.2 → model_compose-0.1.4}/README.md +0 -0
  17. {model_compose-0.1.2 → model_compose-0.1.4}/setup.cfg +0 -0
  18. {model_compose-0.1.2 → model_compose-0.1.4}/src/mindor/__init__.py +0 -0
  19. {model_compose-0.1.2 → model_compose-0.1.4}/src/mindor/cli/__init__.py +0 -0
  20. {model_compose-0.1.2 → model_compose-0.1.4}/src/mindor/cli/compose.py +0 -0
  21. {model_compose-0.1.2 → model_compose-0.1.4}/src/mindor/core/__init__.py +0 -0
  22. {model_compose-0.1.2 → model_compose-0.1.4}/src/mindor/core/component/__init__.py +0 -0
  23. {model_compose-0.1.2 → model_compose-0.1.4}/src/mindor/core/component/component.py +0 -0
  24. {model_compose-0.1.2 → model_compose-0.1.4}/src/mindor/core/component/engine/__init__.py +0 -0
  25. {model_compose-0.1.2 → model_compose-0.1.4}/src/mindor/core/component/engine/base.py +0 -0
  26. {model_compose-0.1.2 → model_compose-0.1.4}/src/mindor/core/component/engine/context.py +0 -0
  27. {model_compose-0.1.2 → model_compose-0.1.4}/src/mindor/core/component/engine/http_client.py +0 -0
  28. {model_compose-0.1.2 → model_compose-0.1.4}/src/mindor/core/component/engine/http_server.py +0 -0
  29. {model_compose-0.1.2 → model_compose-0.1.4}/src/mindor/core/compose/__init__.py +0 -0
  30. {model_compose-0.1.2 → model_compose-0.1.4}/src/mindor/core/compose/compose.py +0 -0
  31. {model_compose-0.1.2 → model_compose-0.1.4}/src/mindor/core/compose/manager.py +0 -0
  32. {model_compose-0.1.2 → model_compose-0.1.4}/src/mindor/core/controller/__init__.py +0 -0
  33. {model_compose-0.1.2 → model_compose-0.1.4}/src/mindor/core/controller/controller.py +0 -0
  34. {model_compose-0.1.2 → model_compose-0.1.4}/src/mindor/core/controller/engine/__init__.py +0 -0
  35. {model_compose-0.1.2 → model_compose-0.1.4}/src/mindor/core/controller/engine/base.py +0 -0
  36. {model_compose-0.1.2 → model_compose-0.1.4}/src/mindor/core/controller/engine/mcp_server.py +0 -0
  37. {model_compose-0.1.2 → model_compose-0.1.4}/src/mindor/core/controller/runner/__init__.py +0 -0
  38. {model_compose-0.1.2 → model_compose-0.1.4}/src/mindor/core/controller/runner/client.py +0 -0
  39. {model_compose-0.1.2 → model_compose-0.1.4}/src/mindor/core/controller/runner/mcp_client.py +0 -0
  40. {model_compose-0.1.2 → model_compose-0.1.4}/src/mindor/core/controller/runner/runner.py +0 -0
  41. {model_compose-0.1.2 → model_compose-0.1.4}/src/mindor/core/controller/webui/__init__.py +0 -0
  42. {model_compose-0.1.2 → model_compose-0.1.4}/src/mindor/core/gateway/__init__.py +0 -0
  43. {model_compose-0.1.2 → model_compose-0.1.4}/src/mindor/core/gateway/engine/__init__.py +0 -0
  44. {model_compose-0.1.2 → model_compose-0.1.4}/src/mindor/core/gateway/engine/base.py +0 -0
  45. {model_compose-0.1.2 → model_compose-0.1.4}/src/mindor/core/gateway/engine/http_tunnel.py +0 -0
  46. {model_compose-0.1.2 → model_compose-0.1.4}/src/mindor/core/gateway/gateway.py +0 -0
  47. {model_compose-0.1.2 → model_compose-0.1.4}/src/mindor/core/listener/__init__.py +0 -0
  48. {model_compose-0.1.2 → model_compose-0.1.4}/src/mindor/core/listener/engine/__init__.py +0 -0
  49. {model_compose-0.1.2 → model_compose-0.1.4}/src/mindor/core/listener/engine/base.py +0 -0
  50. {model_compose-0.1.2 → model_compose-0.1.4}/src/mindor/core/listener/engine/http_callback.py +0 -0
  51. {model_compose-0.1.2 → model_compose-0.1.4}/src/mindor/core/listener/listener.py +0 -0
  52. {model_compose-0.1.2 → model_compose-0.1.4}/src/mindor/core/runtime/__init__.py +0 -0
  53. {model_compose-0.1.2 → model_compose-0.1.4}/src/mindor/core/runtime/env.py +0 -0
  54. {model_compose-0.1.2 → model_compose-0.1.4}/src/mindor/core/services/__init__.py +0 -0
  55. {model_compose-0.1.2 → model_compose-0.1.4}/src/mindor/core/services/async_service.py +0 -0
  56. {model_compose-0.1.2 → model_compose-0.1.4}/src/mindor/core/utils/__init__.py +0 -0
  57. {model_compose-0.1.2 → model_compose-0.1.4}/src/mindor/core/utils/expiring.py +0 -0
  58. {model_compose-0.1.2 → model_compose-0.1.4}/src/mindor/core/utils/image.py +0 -0
  59. {model_compose-0.1.2 → model_compose-0.1.4}/src/mindor/core/utils/streaming.py +0 -0
  60. {model_compose-0.1.2 → model_compose-0.1.4}/src/mindor/core/utils/template.py +0 -0
  61. {model_compose-0.1.2 → model_compose-0.1.4}/src/mindor/core/utils/time.py +0 -0
  62. {model_compose-0.1.2 → model_compose-0.1.4}/src/mindor/core/utils/workqueue.py +0 -0
  63. {model_compose-0.1.2 → model_compose-0.1.4}/src/mindor/core/workflow/__init__.py +0 -0
  64. {model_compose-0.1.2 → model_compose-0.1.4}/src/mindor/core/workflow/context.py +0 -0
  65. {model_compose-0.1.2 → model_compose-0.1.4}/src/mindor/core/workflow/workflow.py +0 -0
  66. {model_compose-0.1.2 → model_compose-0.1.4}/src/mindor/dsl/__init__.py +0 -0
  67. {model_compose-0.1.2 → model_compose-0.1.4}/src/mindor/dsl/loader.py +0 -0
  68. {model_compose-0.1.2 → model_compose-0.1.4}/src/mindor/dsl/schema/__init__.py +0 -0
  69. {model_compose-0.1.2 → model_compose-0.1.4}/src/mindor/dsl/schema/action/__init__.py +0 -0
  70. {model_compose-0.1.2 → model_compose-0.1.4}/src/mindor/dsl/schema/action/action.py +0 -0
  71. {model_compose-0.1.2 → model_compose-0.1.4}/src/mindor/dsl/schema/action/impl/__init__.py +0 -0
  72. {model_compose-0.1.2 → model_compose-0.1.4}/src/mindor/dsl/schema/action/impl/common.py +0 -0
  73. {model_compose-0.1.2 → model_compose-0.1.4}/src/mindor/dsl/schema/action/impl/http_client.py +0 -0
  74. {model_compose-0.1.2 → model_compose-0.1.4}/src/mindor/dsl/schema/action/impl/http_server.py +0 -0
  75. {model_compose-0.1.2 → model_compose-0.1.4}/src/mindor/dsl/schema/action/impl/mcp_client.py +0 -0
  76. {model_compose-0.1.2 → model_compose-0.1.4}/src/mindor/dsl/schema/action/impl/mcp_server.py +0 -0
  77. {model_compose-0.1.2 → model_compose-0.1.4}/src/mindor/dsl/schema/component/__init__.py +0 -0
  78. {model_compose-0.1.2 → model_compose-0.1.4}/src/mindor/dsl/schema/component/component.py +0 -0
  79. {model_compose-0.1.2 → model_compose-0.1.4}/src/mindor/dsl/schema/component/impl/__init__.py +0 -0
  80. {model_compose-0.1.2 → model_compose-0.1.4}/src/mindor/dsl/schema/component/impl/common.py +0 -0
  81. {model_compose-0.1.2 → model_compose-0.1.4}/src/mindor/dsl/schema/component/impl/http_client.py +0 -0
  82. {model_compose-0.1.2 → model_compose-0.1.4}/src/mindor/dsl/schema/component/impl/http_server.py +0 -0
  83. {model_compose-0.1.2 → model_compose-0.1.4}/src/mindor/dsl/schema/component/impl/mcp_client.py +0 -0
  84. {model_compose-0.1.2 → model_compose-0.1.4}/src/mindor/dsl/schema/component/impl/mcp_server.py +0 -0
  85. {model_compose-0.1.2 → model_compose-0.1.4}/src/mindor/dsl/schema/component/impl/types.py +0 -0
  86. {model_compose-0.1.2 → model_compose-0.1.4}/src/mindor/dsl/schema/compose.py +0 -0
  87. {model_compose-0.1.2 → model_compose-0.1.4}/src/mindor/dsl/schema/controller/__init__.py +0 -0
  88. {model_compose-0.1.2 → model_compose-0.1.4}/src/mindor/dsl/schema/controller/controller.py +0 -0
  89. {model_compose-0.1.2 → model_compose-0.1.4}/src/mindor/dsl/schema/controller/impl/__init__.py +0 -0
  90. {model_compose-0.1.2 → model_compose-0.1.4}/src/mindor/dsl/schema/controller/impl/common.py +0 -0
  91. {model_compose-0.1.2 → model_compose-0.1.4}/src/mindor/dsl/schema/controller/impl/http_server.py +0 -0
  92. {model_compose-0.1.2 → model_compose-0.1.4}/src/mindor/dsl/schema/controller/impl/mcp_server.py +0 -0
  93. {model_compose-0.1.2 → model_compose-0.1.4}/src/mindor/dsl/schema/controller/impl/types.py +0 -0
  94. {model_compose-0.1.2 → model_compose-0.1.4}/src/mindor/dsl/schema/controller/impl/webui.py +0 -0
  95. {model_compose-0.1.2 → model_compose-0.1.4}/src/mindor/dsl/schema/gateway/__init__.py +0 -0
  96. {model_compose-0.1.2 → model_compose-0.1.4}/src/mindor/dsl/schema/gateway/gateway.py +0 -0
  97. {model_compose-0.1.2 → model_compose-0.1.4}/src/mindor/dsl/schema/gateway/impl/__init__.py +0 -0
  98. {model_compose-0.1.2 → model_compose-0.1.4}/src/mindor/dsl/schema/gateway/impl/common.py +0 -0
  99. {model_compose-0.1.2 → model_compose-0.1.4}/src/mindor/dsl/schema/gateway/impl/http_tunnel.py +0 -0
  100. {model_compose-0.1.2 → model_compose-0.1.4}/src/mindor/dsl/schema/gateway/impl/types.py +0 -0
  101. {model_compose-0.1.2 → model_compose-0.1.4}/src/mindor/dsl/schema/listener/__init__.py +0 -0
  102. {model_compose-0.1.2 → model_compose-0.1.4}/src/mindor/dsl/schema/listener/impl/__init__.py +0 -0
  103. {model_compose-0.1.2 → model_compose-0.1.4}/src/mindor/dsl/schema/listener/impl/common.py +0 -0
  104. {model_compose-0.1.2 → model_compose-0.1.4}/src/mindor/dsl/schema/listener/impl/http_callback.py +0 -0
  105. {model_compose-0.1.2 → model_compose-0.1.4}/src/mindor/dsl/schema/listener/impl/types.py +0 -0
  106. {model_compose-0.1.2 → model_compose-0.1.4}/src/mindor/dsl/schema/listener/listener.py +0 -0
  107. {model_compose-0.1.2 → model_compose-0.1.4}/src/mindor/dsl/schema/runtime/__init__.py +0 -0
  108. {model_compose-0.1.2 → model_compose-0.1.4}/src/mindor/dsl/schema/runtime/impl/__init__.py +0 -0
  109. {model_compose-0.1.2 → model_compose-0.1.4}/src/mindor/dsl/schema/runtime/impl/common.py +0 -0
  110. {model_compose-0.1.2 → model_compose-0.1.4}/src/mindor/dsl/schema/runtime/impl/docker.py +0 -0
  111. {model_compose-0.1.2 → model_compose-0.1.4}/src/mindor/dsl/schema/runtime/impl/types.py +0 -0
  112. {model_compose-0.1.2 → model_compose-0.1.4}/src/mindor/dsl/schema/runtime/runtime.py +0 -0
  113. {model_compose-0.1.2 → model_compose-0.1.4}/src/model_compose.egg-info/SOURCES.txt +0 -0
  114. {model_compose-0.1.2 → model_compose-0.1.4}/src/model_compose.egg-info/dependency_links.txt +0 -0
  115. {model_compose-0.1.2 → model_compose-0.1.4}/src/model_compose.egg-info/entry_points.txt +0 -0
  116. {model_compose-0.1.2 → model_compose-0.1.4}/src/model_compose.egg-info/requires.txt +0 -0
  117. {model_compose-0.1.2 → model_compose-0.1.4}/src/model_compose.egg-info/top_level.txt +0 -0
  118. {model_compose-0.1.2 → model_compose-0.1.4}/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.2
3
+ Version: 0.1.4
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.2"
3
+ version = "0.1.4"
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" }
@@ -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()
@@ -0,0 +1,68 @@
1
+ from typing import Type, Union, Literal, Optional, Dict, List, Tuple, Set, Annotated, Any
2
+ from mindor.dsl.schema.controller import ControllerConfig
3
+ from mindor.core.utils.http_client import HttpClient
4
+ from .runner import ControllerClient
5
+ from starlette.datastructures import UploadFile
6
+
7
+ class HttpControllerClient(ControllerClient):
8
+ def __init__(self, config: ControllerConfig):
9
+ super().__init__(config)
10
+
11
+ self.client: HttpClient = HttpClient()
12
+
13
+ async def run_workflow(self, workflow_id: Optional[str], input: Any) -> Any:
14
+ base_path = self.config.base_path if self.config.base_path else ""
15
+ url = f"http://localhost:{self.config.port}{base_path}/workflows"
16
+ method = "POST"
17
+ body = {
18
+ "workflow_id": workflow_id,
19
+ "input": input,
20
+ "wait_for_completion": True,
21
+ "output_only": True
22
+ }
23
+ headers = {
24
+ "Content-Type": "application/json"
25
+ }
26
+
27
+ if self._contains_upload_file(input):
28
+ body = { key: value for key, value in self._flatten_for_multipart(body) }
29
+ headers = {
30
+ "Content-Type": "multipart/form-data"
31
+ }
32
+
33
+ return await self.client.request(url, method, None, body, headers)
34
+
35
+ def _flatten_for_multipart(self, body: Dict[str, Any], key: str = "") -> List[Tuple[str, Any]]:
36
+ flattened = []
37
+
38
+ for subkey, value in body.items():
39
+ full_key = f"{key}[{subkey}]" if key else subkey
40
+
41
+ if isinstance(value, dict):
42
+ flattened.extend(self._flatten_for_multipart(value, full_key))
43
+ continue
44
+
45
+ if isinstance(value, list):
46
+ for item in value:
47
+ list_key = f"{full_key}[]"
48
+ if isinstance(item, (dict, list)):
49
+ flattened.extend(self._flatten_for_multipart({ list_key: item }))
50
+ else:
51
+ flattened.append((list_key, item))
52
+ continue
53
+
54
+ flattened.append((full_key, value))
55
+
56
+ return flattened
57
+
58
+ def _contains_upload_file(self, value: Any) -> bool:
59
+ if isinstance(value, UploadFile):
60
+ return True
61
+
62
+ if isinstance(value, dict):
63
+ return any(self._contains_upload_file(v) for v in value.values())
64
+
65
+ if isinstance(value, list):
66
+ return any(self._contains_upload_file(v) for v in value)
67
+
68
+ return False
@@ -1,11 +1,12 @@
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, WorkflowVariableGroupConfig
3
- from mindor.core.utils.http_client import HttpStreamResource
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
+ from starlette.datastructures import UploadFile
7
8
  import gradio as gr
8
- import json
9
+ import json, os
9
10
 
10
11
  class ComponentGroup:
11
12
  def __init__(self, group: gr.Component, components: List[gr.Component]):
@@ -29,8 +30,8 @@ class GradioWebUIBuilder:
29
30
 
30
31
  def _build_workflow_section(self, workflow: WorkflowSchema, runner: Callable[[Any], Awaitable[Any]]) -> gr.Column:
31
32
  with gr.Column() as section:
32
- gr.Markdown(f"## **{workflow.title or 'Untitled Workflow'}**")
33
-
33
+ gr.Markdown(f"## **{workflow.title or 'Untitled Workflow'}**")
34
+
34
35
  if workflow.description:
35
36
  gr.Markdown(f"📝 {workflow.description}")
36
37
 
@@ -45,9 +46,9 @@ class GradioWebUIBuilder:
45
46
  output_components = gr.Textbox(label="", lines=8, interactive=False, show_copy_button=True)
46
47
 
47
48
  async def _run_workflow(*args):
48
- input = { variable.name: value if value != "" else None for variable, value in zip(workflow.input, args) }
49
+ input = self._build_input_values(args, workflow.input)
49
50
  output = await runner(input)
50
- output = await self._flatten_output(output, workflow.output)
51
+ output = await self._flatten_output_values(output, workflow.output)
51
52
  return output[0] if len(output) == 1 else output
52
53
 
53
54
  run_button.click(
@@ -74,14 +75,48 @@ class GradioWebUIBuilder:
74
75
 
75
76
  if variable.type == "boolean":
76
77
  return gr.Checkbox(label=label, value=default or False, info=info)
78
+
79
+ if variable.type == "image":
80
+ return gr.Image(label=label, type="filepath")
81
+
82
+ if variable.type == "audio":
83
+ return gr.Audio(label=label, type="filepath")
84
+
85
+ if variable.type == "video":
86
+ return gr.Video(label=label, type="filepath")
77
87
 
78
88
  if variable.type == "file":
79
- return gr.File(label=label, file_types=["*"], info=info)
89
+ return gr.File(label=label)
80
90
 
81
91
  if variable.type == "select":
82
92
  return gr.Dropdown(choices=variable.options or [], label=label, value=default, info=info)
83
-
93
+
84
94
  return gr.Textbox(label=label, value=default, info=f"Unsupported type: {variable.type}")
95
+
96
+ def _build_input_values(self, arguments: List[Any], variables: List[WorkflowVariableConfig]) -> Any:
97
+ if len(variables) == 1 and not variables[0].name:
98
+ return arguments[0]
99
+
100
+ input: Dict[str, Any] = {}
101
+
102
+ for value, variable in zip(arguments, variables):
103
+ input[variable.name] = self._convert_input_value(value, variable)
104
+
105
+ return input
106
+
107
+ def _convert_input_value(self, value: Any, variable: WorkflowVariableConfig) -> Any:
108
+ if variable.type in [ "image", "audio", "video", "file" ]:
109
+ file, filename = open(value, "rb"), os.path.basename(value)
110
+ headers = {
111
+ "Content-Type": self._guess_file_content_type(filename, variable),
112
+ "Content-Disposition": f'form-data; filename="{filename}"'
113
+ }
114
+ return UploadFile(file=file, filename=filename, headers=headers)
115
+
116
+ return value if value != "" else None
117
+
118
+ def _guess_file_content_type(self, filename: str, variable: WorkflowVariableConfig) -> str:
119
+ return "application/octet-stream"
85
120
 
86
121
  def _build_output_component(self, variable: Union[WorkflowVariableConfig, WorkflowVariableGroupConfig]) -> Union[gr.Component, List[ComponentGroup]]:
87
122
  if isinstance(variable, WorkflowVariableGroupConfig):
@@ -98,13 +133,16 @@ class GradioWebUIBuilder:
98
133
 
99
134
  if variable.type == "string":
100
135
  return gr.Textbox(label=label, interactive=False, show_copy_button=True, info=info)
101
-
136
+
102
137
  if variable.type == "image":
103
138
  return gr.Image(label=label, interactive=False)
104
139
 
105
140
  if variable.type == "audio":
106
141
  return gr.Audio(label=label)
107
142
 
143
+ if variable.type == "video":
144
+ return gr.Video(label=label)
145
+
108
146
  return gr.Textbox(label=label, info=f"Unsupported type: {variable.type}")
109
147
 
110
148
  def _flatten_output_components(self, components: List[Union[gr.Component, List[ComponentGroup]]]) -> List[gr.Component]:
@@ -117,13 +155,13 @@ class GradioWebUIBuilder:
117
155
  flattened.append(item)
118
156
  return flattened
119
157
 
120
- async def _flatten_output(self, output: Any, variables: List[Union[WorkflowVariableConfig, WorkflowVariableGroupConfig]]) -> Any:
158
+ async def _flatten_output_values(self, output: Any, variables: List[Union[WorkflowVariableConfig, WorkflowVariableGroupConfig]]) -> Any:
121
159
  flattened = []
122
160
  for variable in variables:
123
161
  if isinstance(variable, WorkflowVariableGroupConfig):
124
162
  group = output[variable.name] if variable.name else output
125
163
  for value in group:
126
- flattened.extend(await self._flatten_output(value, variable.variables))
164
+ flattened.extend(await self._flatten_output_values(value, variable.variables))
127
165
  else:
128
166
  value = output[variable.name] if variable.name else output
129
167
  flattened.append(await self._convert_type(value, variable.type, variable.subtype, variable.format))
@@ -134,15 +172,19 @@ class GradioWebUIBuilder:
134
172
  if isinstance(value, (dict, list)):
135
173
  return json.dumps(value)
136
174
  return str(value)
137
-
175
+
138
176
  if type == "image":
177
+ if format == "url" and isinstance(value, str):
178
+ return await load_image_from_stream(await HttpClient().request(value), subtype)
139
179
  if format == "base64" and isinstance(value, str):
140
180
  return await load_image_from_stream(Base64StreamResource(value), subtype)
141
181
  if isinstance(value, HttpStreamResource):
142
182
  return await load_image_from_stream(value, subtype)
143
183
  return None
144
184
 
145
- if type == "audio":
185
+ if type in [ "audio", "video" ]:
186
+ if format == "url" and isinstance(value, str):
187
+ return await save_stream_to_temporary_file(await HttpClient().request(value), subtype)
146
188
  if format == "base64" and isinstance(value, str):
147
189
  return await save_stream_to_temporary_file(Base64StreamResource(value), subtype)
148
190
  if isinstance(value, HttpStreamResource):
@@ -111,17 +111,25 @@ class WorkflowVariableResolver:
111
111
  if isinstance(item, WorkflowVariableGroup):
112
112
  group: List[WorkflowVariableConfig] = []
113
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()))
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
118
  configs.append(WorkflowVariableGroupConfig(name=item.name, variables=group, repeat_count=item.repeat_count))
119
119
  else:
120
120
  if item not in seen_single:
121
+ configs.append(self._to_variable_config(item))
121
122
  seen_single.add(item)
122
- configs.append(WorkflowVariableConfig(**item.to_dict()))
123
123
 
124
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)
125
133
 
126
134
  def _parse_as_type(self, value: Any, type: str) -> Any:
127
135
  if type == "number":
@@ -138,7 +146,7 @@ class WorkflowVariableResolver:
138
146
 
139
147
  return value
140
148
 
141
- class WorkflowInputResolver(WorkflowVariableResolver):
149
+ class WorkflowInputVariableResolver(WorkflowVariableResolver):
142
150
  def resolve(self, workflow: WorkflowConfig, components: Dict[str, ComponentConfig]) -> List[WorkflowVariableConfig]:
143
151
  variables: List[WorkflowVariable] = []
144
152
 
@@ -154,12 +162,12 @@ class WorkflowInputResolver(WorkflowVariableResolver):
154
162
  else:
155
163
  variables.extend(self._enumerate_input_variables(job.input, "input"))
156
164
 
157
- for value in [ job.repeat_count ]:
165
+ for value in [ job.component, job.action, job.repeat_count ]:
158
166
  variables.extend(self._enumerate_input_variables(value, "input"))
159
167
 
160
168
  return self._to_variable_config_list(variables)
161
169
 
162
- class WorkflowOutputResolver(WorkflowVariableResolver):
170
+ class WorkflowOutputVariableResolver(WorkflowVariableResolver):
163
171
  def resolve(self, workflow: WorkflowConfig, components: Dict[str, ComponentConfig]) -> List[Union[WorkflowVariableConfig, WorkflowVariableGroupConfig]]:
164
172
  variables: List[Union[WorkflowVariable, WorkflowVariableGroup]] = []
165
173
 
@@ -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
@@ -0,0 +1,97 @@
1
+ from typing import Type, Union, Literal, Optional, Dict, List, Tuple, Set, Annotated, AsyncIterator, Any
2
+ from .http_request import build_request_body, parse_options_header
3
+ from .streaming import StreamResource
4
+ from requests.structures import CaseInsensitiveDict
5
+ import aiohttp
6
+
7
+ class HttpStreamResource(StreamResource):
8
+ def __init__(
9
+ self,
10
+ session: aiohttp.ClientSession,
11
+ stream: aiohttp.StreamReader,
12
+ content_type: Optional[str] = None,
13
+ filename: Optional[str] = None
14
+ ):
15
+ super().__init__(content_type, filename)
16
+
17
+ self.session: aiohttp.ClientSession = session
18
+ self.stream: aiohttp.StreamReader = stream
19
+
20
+ async def close(self):
21
+ await self.session.close()
22
+ self.session = None
23
+ self.stream = None
24
+
25
+ async def _iterate_stream(self) -> AsyncIterator[bytes]:
26
+ _, buffer_size = self.stream.get_read_buffer_limits()
27
+ chunk_size = buffer_size or 65536
28
+
29
+ while not self.stream.at_eof():
30
+ chunk = await self.stream.read(chunk_size)
31
+ if not chunk:
32
+ break
33
+ yield chunk
34
+
35
+ class HttpClient:
36
+ async def request(
37
+ self,
38
+ url: str,
39
+ method: Optional[str] = "GET",
40
+ params: Optional[Dict[str, Any]] = None,
41
+ body: Optional[Any] = None,
42
+ headers: Optional[Dict[str, str]] = None
43
+ ) -> Any:
44
+ session = aiohttp.ClientSession()
45
+ try:
46
+ response = await self._request_with_session(session, url, method, params, body, headers)
47
+ content, _ = await self._parse_response_content(session, response)
48
+
49
+ if response.status >= 400:
50
+ raise ValueError(f"Request failed with status {response.status}: {content}")
51
+
52
+ if not isinstance(content, HttpStreamResource):
53
+ await session.close()
54
+
55
+ return content
56
+ except:
57
+ await session.close()
58
+ raise
59
+
60
+ async def _request_with_session(
61
+ self,
62
+ session: aiohttp.ClientSession,
63
+ url: str,
64
+ method: str,
65
+ params: Optional[Dict[str, Any]],
66
+ body: Optional[Any],
67
+ headers: Optional[Dict[str, str]]
68
+ ) -> aiohttp.ClientResponse:
69
+ data, content_type = self._build_request_body(body, headers)
70
+
71
+ if content_type == "multipart/form-data":
72
+ headers = CaseInsensitiveDict(headers)
73
+ headers.pop("Content-Type", None)
74
+
75
+ return await session.request(method, url, params=params, data=data, headers=headers)
76
+
77
+ def _build_request_body(self, body: Optional[Any], headers: Optional[Dict[str, str]]) -> Tuple[Any, str]:
78
+ content_type, _ = parse_options_header(headers, "Content-Type")
79
+
80
+ if content_type and body is not None:
81
+ return (build_request_body(body, content_type), content_type)
82
+
83
+ return (body, content_type)
84
+
85
+ async def _parse_response_content(self, session: aiohttp.ClientSession, response: aiohttp.ClientResponse) -> Tuple[Any, str]:
86
+ content_type, _ = parse_options_header(response.headers, "Content-Type")
87
+
88
+ if content_type == "application/json":
89
+ return (await response.json(), content_type)
90
+
91
+ if content_type.startswith("text/"):
92
+ return (await response.text(), content_type)
93
+
94
+ _, disposition = parse_options_header(response.headers, "Content-Disposition")
95
+ filename = disposition.get("filename")
96
+
97
+ return (HttpStreamResource(session, response.content, content_type, filename), content_type)
@@ -1,9 +1,9 @@
1
1
  from typing import Dict, List, Tuple, Optional, Any
2
2
  from requests.structures import CaseInsensitiveDict
3
- from starlette.datastructures import FormData
3
+ from starlette.datastructures import FormData, UploadFile
4
4
  from urllib.parse import urlencode
5
5
  from fastapi import Request
6
- import re, json
6
+ import aiohttp, re, json
7
7
 
8
8
  _token_pattern = r"([\w!#$%&'*+\-.^_`|~]+)"
9
9
  _quoted_pattern = r'"([^"]*)"'
@@ -37,12 +37,12 @@ class NestedFormParser:
37
37
 
38
38
  return parts
39
39
 
40
- async def serialize_request_body(body: Any, content_type: Optional[str]) -> Any:
40
+ def build_request_body(body: Any, content_type: Optional[str]) -> Any:
41
41
  if content_type == "application/json":
42
42
  return json.dumps(body)
43
43
 
44
44
  if content_type == "multipart/form-data":
45
- return urlencode(body)
45
+ return build_request_form(body)
46
46
 
47
47
  if content_type == "application/x-www-form-urlencoded":
48
48
  return urlencode(body)
@@ -52,6 +52,22 @@ async def serialize_request_body(body: Any, content_type: Optional[str]) -> Any:
52
52
 
53
53
  return body
54
54
 
55
+ def build_request_form(body: Any) -> aiohttp.FormData:
56
+ form = aiohttp.FormData()
57
+
58
+ for key, value in body.items() if isinstance(body, dict) else {}:
59
+ if isinstance(value, UploadFile):
60
+ form.add_field(
61
+ name=key,
62
+ value=value.file,
63
+ filename=value.filename,
64
+ content_type=value.content_type or "application/octet-stream"
65
+ )
66
+ else:
67
+ form.add_field(name=key, value=str(value))
68
+
69
+ return form
70
+
55
71
  async def parse_request_body(request: Request, content_type: str, nested: bool = False) -> Any:
56
72
  if content_type in [ "multipart/form-data", "application/x-www-form-urlencoded" ]:
57
73
  return await parse_request_form(request, nested)
@@ -75,6 +91,7 @@ async def parse_request_form(request: Request, nested: bool = False) -> Any:
75
91
  def parse_options_header(headers: Dict[str, str], header_name: str) -> Tuple[str, Dict[str, str]]:
76
92
  value = CaseInsensitiveDict(headers or {}).get(header_name, "")
77
93
  pos = _firefox_escape_fix_regex.sub("%22", value).find(";")
94
+
78
95
  if pos >= 0:
79
96
  options = {
80
97
  m.group(1).lower(): m.group(2) or m.group(3).replace("%22", "") for m in _option_regex.finditer(value[pos:])
@@ -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()
@@ -22,7 +22,7 @@ class Job:
22
22
 
23
23
  async def _run_once():
24
24
  call_id = ulid.ulid()
25
- output = await component.run(self.config.action, call_id, input)
25
+ output = await component.run(await context.render_template(self.config.action), call_id, input)
26
26
  context.register_source("output", output)
27
27
 
28
28
  output = (await context.render_template(self.config.output)) if self.config.output else output
@@ -31,6 +31,7 @@ class WorkflowVariableType(str, Enum):
31
31
  # Media and files
32
32
  IMAGE = "image"
33
33
  AUDIO = "audio"
34
+ VIDEO = "video"
34
35
  FILE = "file"
35
36
  # UI-related types
36
37
  SELECT = "select"
@@ -39,16 +40,17 @@ class WorkflowVariableFormat(str, Enum):
39
40
  BASE64 = "base64"
40
41
  URL = "url"
41
42
  PATH = "path"
43
+ STREAM = "stream"
42
44
 
43
45
  class WorkflowVariableConfig(BaseModel):
44
46
  name: Optional[str] = Field(default=None, description="The name of the variable")
45
47
  type: WorkflowVariableType = Field(..., description="Type of the variable")
46
48
  subtype: Optional[str] = Field(default=None, description="Subtype of the variable")
47
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")
48
51
  required: bool = Field(default=False, description="Whether this variable is required")
49
52
  default: Optional[Any] = Field(default=None, description="Default value if not provided")
50
53
  description: Optional[str] = Field(default=None, description="Description of the variable")
51
- options: Optional[List[Any]] = Field(default=None, description="List of valid options for select type")
52
54
 
53
55
  class WorkflowVariableGroupConfig(BaseModel):
54
56
  name: Optional[str] = Field(default=None, description="The name of the group of variables")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: model-compose
3
- Version: 0.1.2
3
+ Version: 0.1.4
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,26 +0,0 @@
1
- from typing import Type, Union, Literal, Optional, Dict, List, Tuple, Set, Annotated, Any
2
- from mindor.dsl.schema.controller import ControllerConfig
3
- from mindor.core.utils.http_client import HttpClient
4
- from .runner import ControllerClient
5
-
6
- class HttpControllerClient(ControllerClient):
7
- def __init__(self, config: ControllerConfig):
8
- super().__init__(config)
9
-
10
- self.client: HttpClient = HttpClient()
11
-
12
- async def run_workflow(self, workflow_id: Optional[str], input: Any) -> Any:
13
- base_path = self.config.base_path if self.config.base_path else ""
14
- url = f"http://localhost:{self.config.port}{base_path}/workflows"
15
- method = "POST"
16
- body = {
17
- "workflow_id": workflow_id,
18
- "input": input,
19
- "wait_for_completion": True,
20
- "output_only": True
21
- }
22
- headers = {
23
- "Content-Type": "application/json"
24
- }
25
-
26
- return await self.client.request(url, method, None, body, headers)
@@ -1,75 +0,0 @@
1
- from typing import Type, Union, Literal, Optional, Dict, List, Tuple, Set, Annotated, AsyncIterator, Any
2
- from .http_request import serialize_request_body, parse_options_header
3
- from .streaming import StreamResource
4
-
5
- from urllib.parse import urlencode
6
- import aiohttp, json
7
-
8
- class HttpStreamResource(StreamResource):
9
- def __init__(self, session: aiohttp.ClientSession, stream: aiohttp.StreamReader, content_type: Optional[str] = None, filename: Optional[str] = None):
10
- super().__init__(content_type, filename)
11
-
12
- self.session: aiohttp.ClientSession = session
13
- self.stream: aiohttp.StreamReader = stream
14
-
15
- async def close(self):
16
- await self.session.close()
17
- self.session = None
18
- self.stream = None
19
-
20
- async def _iterate_stream(self) -> AsyncIterator[bytes]:
21
- _, buffer_size = self.stream.get_read_buffer_limits()
22
- chunk_size = buffer_size or 65536
23
-
24
- while not self.stream.at_eof():
25
- chunk = await self.stream.read(chunk_size)
26
- if not chunk:
27
- break
28
- yield chunk
29
-
30
- 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:
32
- session = aiohttp.ClientSession()
33
- try:
34
- response = await session.request(
35
- method,
36
- url,
37
- params=params,
38
- data=await self._serialize_request_body(body, headers),
39
- headers=headers
40
- )
41
-
42
- content = await self._parse_response_content(session, response)
43
-
44
- if response.status >= 400:
45
- raise ValueError(f"Request failed with status {response.status}: {content}")
46
-
47
- if not isinstance(content, HttpStreamResource):
48
- await session.close()
49
-
50
- return content
51
- except:
52
- await session.close()
53
- raise
54
-
55
- async def _serialize_request_body(self, body: Optional[Any], headers: Optional[Dict[str, str]]) -> Any:
56
- content_type, _ = parse_options_header(headers, "Content-Type")
57
-
58
- if content_type and body:
59
- return await serialize_request_body(body, content_type)
60
-
61
- return body
62
-
63
- async def _parse_response_content(self, session: aiohttp.ClientSession, response: aiohttp.ClientResponse) -> Any:
64
- content_type, _ = parse_options_header(response.headers, "Content-Type")
65
-
66
- if content_type == "application/json":
67
- return await response.json()
68
-
69
- if content_type.startswith("text/"):
70
- return await response.text()
71
-
72
- _, disposition = parse_options_header(response.headers, "Content-Disposition")
73
- filename = disposition.get("filename")
74
-
75
- return HttpStreamResource(session, response.content, content_type, filename)
File without changes
File without changes
File without changes