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.
- {model_compose-0.1.1 → model_compose-0.1.3}/PKG-INFO +1 -1
- {model_compose-0.1.1 → model_compose-0.1.3}/pyproject.toml +1 -1
- {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/core/component/engine/http_client.py +3 -7
- {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/core/controller/engine/http_server.py +2 -2
- {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/core/controller/webui/gradio.py +71 -33
- model_compose-0.1.3/src/mindor/core/controller/webui/schema.py +206 -0
- {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/core/controller/webui/webui.py +3 -3
- {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/core/listener/engine/http_callback.py +2 -2
- {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/core/utils/http_client.py +6 -8
- {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/core/utils/template.py +2 -2
- {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/core/workflow/job.py +13 -11
- {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/dsl/schema/workflow.py +16 -3
- {model_compose-0.1.1 → model_compose-0.1.3}/src/model_compose.egg-info/PKG-INFO +1 -1
- model_compose-0.1.1/src/mindor/core/controller/webui/schema.py +0 -135
- {model_compose-0.1.1 → model_compose-0.1.3}/LICENSE +0 -0
- {model_compose-0.1.1 → model_compose-0.1.3}/README.md +0 -0
- {model_compose-0.1.1 → model_compose-0.1.3}/setup.cfg +0 -0
- {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/__init__.py +0 -0
- {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/cli/__init__.py +0 -0
- {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/cli/compose.py +0 -0
- {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/core/__init__.py +0 -0
- {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/core/component/__init__.py +0 -0
- {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/core/component/component.py +0 -0
- {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/core/component/engine/__init__.py +0 -0
- {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/core/component/engine/base.py +0 -0
- {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/core/component/engine/context.py +0 -0
- {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/core/component/engine/http_server.py +0 -0
- {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/core/compose/__init__.py +0 -0
- {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/core/compose/compose.py +0 -0
- {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/core/compose/manager.py +0 -0
- {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/core/controller/__init__.py +0 -0
- {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/core/controller/controller.py +0 -0
- {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/core/controller/engine/__init__.py +0 -0
- {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/core/controller/engine/base.py +0 -0
- {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/core/controller/engine/mcp_server.py +0 -0
- {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/core/controller/runner/__init__.py +0 -0
- {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/core/controller/runner/client.py +0 -0
- {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/core/controller/runner/http_client.py +0 -0
- {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/core/controller/runner/mcp_client.py +0 -0
- {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/core/controller/runner/runner.py +0 -0
- {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/core/controller/webui/__init__.py +0 -0
- {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/core/gateway/__init__.py +0 -0
- {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/core/gateway/engine/__init__.py +0 -0
- {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/core/gateway/engine/base.py +0 -0
- {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/core/gateway/engine/http_tunnel.py +0 -0
- {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/core/gateway/gateway.py +0 -0
- {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/core/listener/__init__.py +0 -0
- {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/core/listener/engine/__init__.py +0 -0
- {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/core/listener/engine/base.py +0 -0
- {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/core/listener/listener.py +0 -0
- {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/core/runtime/__init__.py +0 -0
- {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/core/runtime/env.py +0 -0
- {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/core/services/__init__.py +0 -0
- {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/core/services/async_service.py +0 -0
- {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/core/utils/__init__.py +0 -0
- {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/core/utils/expiring.py +0 -0
- {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/core/utils/http_request.py +0 -0
- {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/core/utils/image.py +0 -0
- {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/core/utils/streaming.py +0 -0
- {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/core/utils/time.py +0 -0
- {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/core/utils/workqueue.py +0 -0
- {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/core/workflow/__init__.py +0 -0
- {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/core/workflow/context.py +0 -0
- {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/core/workflow/workflow.py +0 -0
- {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/dsl/__init__.py +0 -0
- {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/dsl/loader.py +0 -0
- {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/dsl/schema/__init__.py +0 -0
- {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/dsl/schema/action/__init__.py +0 -0
- {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/dsl/schema/action/action.py +0 -0
- {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/dsl/schema/action/impl/__init__.py +0 -0
- {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/dsl/schema/action/impl/common.py +0 -0
- {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/dsl/schema/action/impl/http_client.py +0 -0
- {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/dsl/schema/action/impl/http_server.py +0 -0
- {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/dsl/schema/action/impl/mcp_client.py +0 -0
- {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/dsl/schema/action/impl/mcp_server.py +0 -0
- {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/dsl/schema/component/__init__.py +0 -0
- {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/dsl/schema/component/component.py +0 -0
- {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/dsl/schema/component/impl/__init__.py +0 -0
- {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/dsl/schema/component/impl/common.py +0 -0
- {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/dsl/schema/component/impl/http_client.py +0 -0
- {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/dsl/schema/component/impl/http_server.py +0 -0
- {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/dsl/schema/component/impl/mcp_client.py +0 -0
- {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/dsl/schema/component/impl/mcp_server.py +0 -0
- {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/dsl/schema/component/impl/types.py +0 -0
- {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/dsl/schema/compose.py +0 -0
- {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/dsl/schema/controller/__init__.py +0 -0
- {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/dsl/schema/controller/controller.py +0 -0
- {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/dsl/schema/controller/impl/__init__.py +0 -0
- {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/dsl/schema/controller/impl/common.py +0 -0
- {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/dsl/schema/controller/impl/http_server.py +0 -0
- {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/dsl/schema/controller/impl/mcp_server.py +0 -0
- {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/dsl/schema/controller/impl/types.py +0 -0
- {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/dsl/schema/controller/impl/webui.py +0 -0
- {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/dsl/schema/gateway/__init__.py +0 -0
- {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/dsl/schema/gateway/gateway.py +0 -0
- {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/dsl/schema/gateway/impl/__init__.py +0 -0
- {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/dsl/schema/gateway/impl/common.py +0 -0
- {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/dsl/schema/gateway/impl/http_tunnel.py +0 -0
- {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/dsl/schema/gateway/impl/types.py +0 -0
- {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/dsl/schema/listener/__init__.py +0 -0
- {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/dsl/schema/listener/impl/__init__.py +0 -0
- {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/dsl/schema/listener/impl/common.py +0 -0
- {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/dsl/schema/listener/impl/http_callback.py +0 -0
- {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/dsl/schema/listener/impl/types.py +0 -0
- {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/dsl/schema/listener/listener.py +0 -0
- {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/dsl/schema/runtime/__init__.py +0 -0
- {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/dsl/schema/runtime/impl/__init__.py +0 -0
- {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/dsl/schema/runtime/impl/common.py +0 -0
- {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/dsl/schema/runtime/impl/docker.py +0 -0
- {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/dsl/schema/runtime/impl/types.py +0 -0
- {model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/dsl/schema/runtime/runtime.py +0 -0
- {model_compose-0.1.1 → model_compose-0.1.3}/src/model_compose.egg-info/SOURCES.txt +0 -0
- {model_compose-0.1.1 → model_compose-0.1.3}/src/model_compose.egg-info/dependency_links.txt +0 -0
- {model_compose-0.1.1 → model_compose-0.1.3}/src/model_compose.egg-info/entry_points.txt +0 -0
- {model_compose-0.1.1 → model_compose-0.1.3}/src/model_compose.egg-info/requires.txt +0 -0
- {model_compose-0.1.1 → model_compose-0.1.3}/src/model_compose.egg-info/top_level.txt +0 -0
- {model_compose-0.1.1 → model_compose-0.1.3}/tests/test_cli.py +0 -0
|
@@ -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
|
|
{model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/core/controller/engine/http_server.py
RENAMED
|
@@ -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
|
|
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
|
|
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,
|
|
24
|
+
self._build_workflow_section(workflow, _run_workflow)
|
|
20
25
|
else:
|
|
21
|
-
self._build_workflow_section(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
|
|
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
|
|
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=
|
|
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
|
|
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
|
|
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,
|
|
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=
|
|
72
|
-
output=
|
|
71
|
+
input=WorkflowInputVariableResolver().resolve(workflow, self.components),
|
|
72
|
+
output=WorkflowOutputVariableResolver().resolve(workflow, self.components)
|
|
73
73
|
)
|
|
74
74
|
|
|
75
75
|
return schema
|
{model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/core/listener/engine/http_callback.py
RENAMED
|
@@ -66,7 +66,7 @@ class HttpCallbackListener(ListenerEngine):
|
|
|
66
66
|
)
|
|
67
67
|
|
|
68
68
|
def _make_callback_handler(self, callback: HttpCallbackConfig) -> Callable:
|
|
69
|
-
async def
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
28
|
+
output = (await context.render_template(self.config.output)) if self.config.output else output
|
|
29
|
+
outputs.append(output)
|
|
31
30
|
|
|
32
|
-
if
|
|
33
|
-
|
|
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
|
|
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
|
-
|
|
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,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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/core/controller/runner/http_client.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/dsl/schema/action/impl/http_client.py
RENAMED
|
File without changes
|
{model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/dsl/schema/action/impl/http_server.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/dsl/schema/component/impl/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/dsl/schema/component/impl/http_client.py
RENAMED
|
File without changes
|
{model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/dsl/schema/component/impl/http_server.py
RENAMED
|
File without changes
|
{model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/dsl/schema/component/impl/mcp_client.py
RENAMED
|
File without changes
|
{model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/dsl/schema/component/impl/mcp_server.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/dsl/schema/controller/impl/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/dsl/schema/controller/impl/http_server.py
RENAMED
|
File without changes
|
{model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/dsl/schema/controller/impl/mcp_server.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/dsl/schema/gateway/impl/http_tunnel.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{model_compose-0.1.1 → model_compose-0.1.3}/src/mindor/dsl/schema/listener/impl/http_callback.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|