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