model-compose 0.1.3__tar.gz → 0.1.5__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.3 → model_compose-0.1.5}/PKG-INFO +1 -1
  2. {model_compose-0.1.3 → model_compose-0.1.5}/pyproject.toml +1 -1
  3. {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/core/component/engine/context.py +2 -2
  4. {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/core/component/engine/http_client.py +1 -1
  5. model_compose-0.1.5/src/mindor/core/controller/runner/http_client.py +68 -0
  6. {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/core/controller/webui/gradio.py +69 -34
  7. {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/core/controller/webui/schema.py +27 -17
  8. {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/core/listener/engine/http_callback.py +3 -3
  9. model_compose-0.1.5/src/mindor/core/utils/http_client.py +97 -0
  10. {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/core/utils/http_request.py +39 -4
  11. {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/core/utils/streaming.py +8 -2
  12. {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/core/utils/template.py +34 -11
  13. {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/core/workflow/context.py +2 -2
  14. {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/core/workflow/job.py +1 -1
  15. {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/dsl/schema/workflow.py +2 -1
  16. {model_compose-0.1.3 → model_compose-0.1.5}/src/model_compose.egg-info/PKG-INFO +1 -1
  17. model_compose-0.1.3/src/mindor/core/controller/runner/http_client.py +0 -26
  18. model_compose-0.1.3/src/mindor/core/utils/http_client.py +0 -73
  19. {model_compose-0.1.3 → model_compose-0.1.5}/LICENSE +0 -0
  20. {model_compose-0.1.3 → model_compose-0.1.5}/README.md +0 -0
  21. {model_compose-0.1.3 → model_compose-0.1.5}/setup.cfg +0 -0
  22. {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/__init__.py +0 -0
  23. {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/cli/__init__.py +0 -0
  24. {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/cli/compose.py +0 -0
  25. {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/core/__init__.py +0 -0
  26. {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/core/component/__init__.py +0 -0
  27. {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/core/component/component.py +0 -0
  28. {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/core/component/engine/__init__.py +0 -0
  29. {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/core/component/engine/base.py +0 -0
  30. {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/core/component/engine/http_server.py +0 -0
  31. {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/core/compose/__init__.py +0 -0
  32. {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/core/compose/compose.py +0 -0
  33. {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/core/compose/manager.py +0 -0
  34. {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/core/controller/__init__.py +0 -0
  35. {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/core/controller/controller.py +0 -0
  36. {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/core/controller/engine/__init__.py +0 -0
  37. {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/core/controller/engine/base.py +0 -0
  38. {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/core/controller/engine/http_server.py +0 -0
  39. {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/core/controller/engine/mcp_server.py +0 -0
  40. {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/core/controller/runner/__init__.py +0 -0
  41. {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/core/controller/runner/client.py +0 -0
  42. {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/core/controller/runner/mcp_client.py +0 -0
  43. {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/core/controller/runner/runner.py +0 -0
  44. {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/core/controller/webui/__init__.py +0 -0
  45. {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/core/controller/webui/webui.py +0 -0
  46. {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/core/gateway/__init__.py +0 -0
  47. {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/core/gateway/engine/__init__.py +0 -0
  48. {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/core/gateway/engine/base.py +0 -0
  49. {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/core/gateway/engine/http_tunnel.py +0 -0
  50. {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/core/gateway/gateway.py +0 -0
  51. {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/core/listener/__init__.py +0 -0
  52. {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/core/listener/engine/__init__.py +0 -0
  53. {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/core/listener/engine/base.py +0 -0
  54. {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/core/listener/listener.py +0 -0
  55. {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/core/runtime/__init__.py +0 -0
  56. {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/core/runtime/env.py +0 -0
  57. {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/core/services/__init__.py +0 -0
  58. {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/core/services/async_service.py +0 -0
  59. {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/core/utils/__init__.py +0 -0
  60. {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/core/utils/expiring.py +0 -0
  61. {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/core/utils/image.py +0 -0
  62. {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/core/utils/time.py +0 -0
  63. {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/core/utils/workqueue.py +0 -0
  64. {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/core/workflow/__init__.py +0 -0
  65. {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/core/workflow/workflow.py +0 -0
  66. {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/dsl/__init__.py +0 -0
  67. {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/dsl/loader.py +0 -0
  68. {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/dsl/schema/__init__.py +0 -0
  69. {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/dsl/schema/action/__init__.py +0 -0
  70. {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/dsl/schema/action/action.py +0 -0
  71. {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/dsl/schema/action/impl/__init__.py +0 -0
  72. {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/dsl/schema/action/impl/common.py +0 -0
  73. {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/dsl/schema/action/impl/http_client.py +0 -0
  74. {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/dsl/schema/action/impl/http_server.py +0 -0
  75. {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/dsl/schema/action/impl/mcp_client.py +0 -0
  76. {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/dsl/schema/action/impl/mcp_server.py +0 -0
  77. {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/dsl/schema/component/__init__.py +0 -0
  78. {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/dsl/schema/component/component.py +0 -0
  79. {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/dsl/schema/component/impl/__init__.py +0 -0
  80. {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/dsl/schema/component/impl/common.py +0 -0
  81. {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/dsl/schema/component/impl/http_client.py +0 -0
  82. {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/dsl/schema/component/impl/http_server.py +0 -0
  83. {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/dsl/schema/component/impl/mcp_client.py +0 -0
  84. {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/dsl/schema/component/impl/mcp_server.py +0 -0
  85. {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/dsl/schema/component/impl/types.py +0 -0
  86. {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/dsl/schema/compose.py +0 -0
  87. {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/dsl/schema/controller/__init__.py +0 -0
  88. {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/dsl/schema/controller/controller.py +0 -0
  89. {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/dsl/schema/controller/impl/__init__.py +0 -0
  90. {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/dsl/schema/controller/impl/common.py +0 -0
  91. {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/dsl/schema/controller/impl/http_server.py +0 -0
  92. {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/dsl/schema/controller/impl/mcp_server.py +0 -0
  93. {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/dsl/schema/controller/impl/types.py +0 -0
  94. {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/dsl/schema/controller/impl/webui.py +0 -0
  95. {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/dsl/schema/gateway/__init__.py +0 -0
  96. {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/dsl/schema/gateway/gateway.py +0 -0
  97. {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/dsl/schema/gateway/impl/__init__.py +0 -0
  98. {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/dsl/schema/gateway/impl/common.py +0 -0
  99. {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/dsl/schema/gateway/impl/http_tunnel.py +0 -0
  100. {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/dsl/schema/gateway/impl/types.py +0 -0
  101. {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/dsl/schema/listener/__init__.py +0 -0
  102. {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/dsl/schema/listener/impl/__init__.py +0 -0
  103. {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/dsl/schema/listener/impl/common.py +0 -0
  104. {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/dsl/schema/listener/impl/http_callback.py +0 -0
  105. {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/dsl/schema/listener/impl/types.py +0 -0
  106. {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/dsl/schema/listener/listener.py +0 -0
  107. {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/dsl/schema/runtime/__init__.py +0 -0
  108. {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/dsl/schema/runtime/impl/__init__.py +0 -0
  109. {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/dsl/schema/runtime/impl/common.py +0 -0
  110. {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/dsl/schema/runtime/impl/docker.py +0 -0
  111. {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/dsl/schema/runtime/impl/types.py +0 -0
  112. {model_compose-0.1.3 → model_compose-0.1.5}/src/mindor/dsl/schema/runtime/runtime.py +0 -0
  113. {model_compose-0.1.3 → model_compose-0.1.5}/src/model_compose.egg-info/SOURCES.txt +0 -0
  114. {model_compose-0.1.3 → model_compose-0.1.5}/src/model_compose.egg-info/dependency_links.txt +0 -0
  115. {model_compose-0.1.3 → model_compose-0.1.5}/src/model_compose.egg-info/entry_points.txt +0 -0
  116. {model_compose-0.1.3 → model_compose-0.1.5}/src/model_compose.egg-info/requires.txt +0 -0
  117. {model_compose-0.1.3 → model_compose-0.1.5}/src/model_compose.egg-info/top_level.txt +0 -0
  118. {model_compose-0.1.3 → model_compose-0.1.5}/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.3
3
+ Version: 0.1.5
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.3"
3
+ version = "0.1.5"
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" }
@@ -14,8 +14,8 @@ class ComponentContext:
14
14
  def register_source(self, key: str, source: Any) -> None:
15
15
  self.sources[key] = source
16
16
 
17
- async def render_template(self, data: Dict[str, Any]) -> Any:
18
- return await self.renderer.render(data)
17
+ async def render_template(self, data: Dict[str, Any], ignore_files: bool = False) -> Any:
18
+ return await self.renderer.render(data, ignore_files)
19
19
 
20
20
  async def _resolve_source(self, key: str) -> Any:
21
21
  if key in self.sources:
@@ -88,7 +88,7 @@ class HttpClientAction:
88
88
  result = await self._handle_completion(self.config.completion, context)
89
89
  context.register_source("result", result)
90
90
 
91
- return (await context.render_template(self.config.output)) if self.config.output else (result or response)
91
+ return (await context.render_template(self.config.output, ignore_files=True)) if self.config.output else (result or response)
92
92
 
93
93
  async def _resolve_request_url(self, context: ComponentContext) -> str:
94
94
  if self.base_url and self.config.path:
@@ -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,14 @@
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 HttpClient, HttpStreamResource
3
+ from mindor.core.utils.streaming import StreamResource, Base64StreamResource
4
+ from mindor.core.utils.streaming import save_stream_to_temporary_file
5
+ from mindor.core.utils.http_request import create_upload_file
6
+ from mindor.core.utils.http_client import HttpClient
4
7
  from mindor.core.utils.image import load_image_from_stream
5
- from mindor.core.utils.streaming import Base64StreamResource, save_stream_to_temporary_file
6
8
  from .schema import WorkflowSchema
9
+ from starlette.datastructures import UploadFile
7
10
  import gradio as gr
8
- import json
11
+ import json, os
9
12
 
10
13
  class ComponentGroup:
11
14
  def __init__(self, group: gr.Component, components: List[gr.Component]):
@@ -45,9 +48,9 @@ class GradioWebUIBuilder:
45
48
  output_components = gr.Textbox(label="", lines=8, interactive=False, show_copy_button=True)
46
49
 
47
50
  async def _run_workflow(*args):
48
- input = { variable.name: value if value != "" else None for variable, value in zip(workflow.input, args) }
51
+ input = await self._build_input_values(args, workflow.input)
49
52
  output = await runner(input)
50
- output = await self._flatten_output(output, workflow.output)
53
+ output = await self._flatten_output_values(output, workflow.output)
51
54
  return output[0] if len(output) == 1 else output
52
55
 
53
56
  run_button.click(
@@ -59,11 +62,11 @@ class GradioWebUIBuilder:
59
62
  return section
60
63
 
61
64
  def _build_input_component(self, variable: WorkflowVariableConfig) -> gr.Component:
62
- label = variable.name + (" *" if variable.required else "") + (f" (default: {variable.default})" if variable.default else "")
65
+ label = (variable.name or "") + (" *" if variable.required else "") + (f" (default: {variable.default})" if variable.default else "")
63
66
  info = variable.description or ""
64
67
  default = variable.default
65
68
 
66
- if variable.type == "string":
69
+ if variable.type == "string" or variable.format in [ "base64", "url" ]:
67
70
  return gr.Textbox(label=label, value="", info=info)
68
71
 
69
72
  if variable.type == "number":
@@ -78,8 +81,11 @@ class GradioWebUIBuilder:
78
81
  if variable.type == "image":
79
82
  return gr.Image(label=label, type="filepath")
80
83
 
84
+ if variable.type == "audio":
85
+ return gr.Audio(label=label, type="filepath")
86
+
81
87
  if variable.type == "video":
82
- return gr.Video(label=label)
88
+ return gr.Video(label=label, type="filepath")
83
89
 
84
90
  if variable.type == "file":
85
91
  return gr.File(label=label)
@@ -88,6 +94,25 @@ class GradioWebUIBuilder:
88
94
  return gr.Dropdown(choices=variable.options or [], label=label, value=default, info=info)
89
95
 
90
96
  return gr.Textbox(label=label, value=default, info=f"Unsupported type: {variable.type}")
97
+
98
+ async def _build_input_values(self, arguments: List[Any], variables: List[WorkflowVariableConfig]) -> Any:
99
+ if len(variables) == 1 and not variables[0].name:
100
+ return arguments[0]
101
+
102
+ input: Dict[str, Any] = {}
103
+
104
+ for value, variable in zip(arguments, variables):
105
+ input[variable.name] = await self._convert_input_value(value, variable.type, variable.subtype, variable.format, variable.internal)
106
+
107
+ return input
108
+
109
+ async def _convert_input_value(self, value: Any, type: Optional[str], subtype: Optional[str], format: Optional[str], internal: bool) -> Any:
110
+ if type in [ "image", "audio", "video", "file" ] and (not internal or not format):
111
+ if internal and format and format != "path":
112
+ value = await self._save_value_to_temporary_file(value, subtype, format)
113
+ return create_upload_file(value, type, subtype)
114
+
115
+ return value if value != "" else None
91
116
 
92
117
  def _build_output_component(self, variable: Union[WorkflowVariableConfig, WorkflowVariableGroupConfig]) -> Union[gr.Component, List[ComponentGroup]]:
93
118
  if isinstance(variable, WorkflowVariableGroupConfig):
@@ -102,15 +127,15 @@ class GradioWebUIBuilder:
102
127
  label = variable.name or ""
103
128
  info = variable.description or ""
104
129
 
105
- if variable.type == "string":
130
+ if variable.type in [ "string", "base64" ]:
106
131
  return gr.Textbox(label=label, interactive=False, show_copy_button=True, info=info)
107
-
132
+
108
133
  if variable.type == "image":
109
134
  return gr.Image(label=label, interactive=False)
110
-
135
+
111
136
  if variable.type == "audio":
112
137
  return gr.Audio(label=label)
113
-
138
+
114
139
  if variable.type == "video":
115
140
  return gr.Video(label=label)
116
141
 
@@ -126,40 +151,50 @@ class GradioWebUIBuilder:
126
151
  flattened.append(item)
127
152
  return flattened
128
153
 
129
- async def _flatten_output(self, output: Any, variables: List[Union[WorkflowVariableConfig, WorkflowVariableGroupConfig]]) -> Any:
154
+ async def _flatten_output_values(self, output: Any, variables: List[Union[WorkflowVariableConfig, WorkflowVariableGroupConfig]]) -> Any:
130
155
  flattened = []
131
156
  for variable in variables:
132
157
  if isinstance(variable, WorkflowVariableGroupConfig):
133
158
  group = output[variable.name] if variable.name else output
134
159
  for value in group:
135
- flattened.extend(await self._flatten_output(value, variable.variables))
160
+ flattened.extend(await self._flatten_output_values(value, variable.variables))
136
161
  else:
137
162
  value = output[variable.name] if variable.name else output
138
- flattened.append(await self._convert_type(value, variable.type, variable.subtype, variable.format))
163
+ flattened.append(await self._convert_output_value(value, variable.type, variable.subtype, variable.format, variable.internal))
139
164
  return flattened
140
165
 
141
- async def _convert_type(self, value: Any, type: Optional[str], subtype: Optional[str], format: Optional[str]) -> Any:
166
+ async def _convert_output_value(self, value: Any, type: Optional[str], subtype: Optional[str], format: Optional[str], internal: bool) -> Any:
142
167
  if type == "string":
143
- if isinstance(value, (dict, list)):
144
- return json.dumps(value)
145
- return str(value)
146
-
168
+ return json.dumps(value) if isinstance(value, (dict, list)) else str(value)
169
+
147
170
  if type == "image":
148
- if format == "url" and isinstance(value, str):
149
- return await load_image_from_stream(await HttpClient().request(value), subtype)
150
- if format == "base64" and isinstance(value, str):
151
- return await load_image_from_stream(Base64StreamResource(value), subtype)
152
- if isinstance(value, HttpStreamResource):
153
- return await load_image_from_stream(value, subtype)
154
- return None
171
+ return await self._load_image_from_value(value, subtype, format)
155
172
 
156
173
  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)
159
- if format == "base64" and isinstance(value, str):
160
- return await save_stream_to_temporary_file(Base64StreamResource(value), subtype)
161
- if isinstance(value, HttpStreamResource):
162
- return await save_stream_to_temporary_file(value, subtype)
163
- return None
174
+ return await self._save_value_to_temporary_file(value, subtype, format)
164
175
 
165
176
  return value
177
+
178
+ async def _load_image_from_value(self, value: Any, subtype: Optional[str], format: Optional[str]) -> Optional[str]:
179
+ if format == "base64" and isinstance(value, str):
180
+ return await load_image_from_stream(Base64StreamResource(value), subtype)
181
+
182
+ if format == "url" and isinstance(value, str):
183
+ return await load_image_from_stream(await HttpClient().request(value), subtype)
184
+
185
+ if isinstance(value, StreamResource):
186
+ return await load_image_from_stream(value, subtype)
187
+
188
+ return None
189
+
190
+ async def _save_value_to_temporary_file(self, value: Any, subtype: Optional[str], format: Optional[str]) -> Optional[str]:
191
+ if format == "base64" and isinstance(value, str):
192
+ return await save_stream_to_temporary_file(Base64StreamResource(value), subtype)
193
+
194
+ if format == "url" and isinstance(value, str):
195
+ return await save_stream_to_temporary_file(await HttpClient().request(value), subtype)
196
+
197
+ if isinstance(value, StreamResource):
198
+ return await save_stream_to_temporary_file(value, subtype)
199
+
200
+ return None
@@ -5,12 +5,13 @@ from mindor.dsl.schema.component import ComponentConfig
5
5
  import re, json
6
6
 
7
7
  class WorkflowVariable:
8
- def __init__(self, name: Optional[str], type: str, subtype: Optional[str], format: Optional[str], default: Optional[Any]):
8
+ def __init__(self, name: Optional[str], type: str, subtype: Optional[str], format: Optional[str], default: Optional[Any], internal: bool = False):
9
9
  self.name: Optional[str] = name
10
10
  self.type: str = type
11
11
  self.subtype: Optional[str] = subtype
12
12
  self.format: Optional[str] = format
13
13
  self.default: Optional[Any] = default
14
+ self.internal: bool = internal
14
15
 
15
16
  def to_dict(self) -> Dict[str, Any]:
16
17
  return {
@@ -18,7 +19,8 @@ class WorkflowVariable:
18
19
  "type": self.type,
19
20
  "subtype": self.subtype,
20
21
  "format": self.format,
21
- "default": self.default
22
+ "default": self.default,
23
+ "internal": self.internal
22
24
  }
23
25
 
24
26
  def __eq__(self, other):
@@ -52,7 +54,7 @@ class WorkflowVariableResolver:
52
54
  "keypath": re.compile(r"[-_\w]+|\[\d+\]"),
53
55
  }
54
56
 
55
- def _enumerate_input_variables(self, value: Any, wanted_key: str) -> List[WorkflowVariable]:
57
+ def _enumerate_input_variables(self, value: Any, wanted_key: str, internal: bool = False) -> List[WorkflowVariable]:
56
58
  if isinstance(value, str):
57
59
  variables: List[WorkflowVariable] = []
58
60
 
@@ -63,22 +65,22 @@ class WorkflowVariableResolver:
63
65
  default = self._parse_as_type(default, type)
64
66
 
65
67
  if key == wanted_key:
66
- variables.append(WorkflowVariable(path, type or "string", subtype, format, default))
68
+ variables.append(WorkflowVariable(path, type or "string", subtype, format, default, internal))
67
69
 
68
70
  return variables
69
71
 
70
72
  if isinstance(value, BaseModel):
71
- return self._enumerate_input_variables(value.model_dump(exclude_none=True), wanted_key)
73
+ return self._enumerate_input_variables(value.model_dump(exclude_none=True), wanted_key, internal)
72
74
 
73
75
  if isinstance(value, dict):
74
- return sum([ self._enumerate_input_variables(v, wanted_key) for v in value.values() ], [])
76
+ return sum([ self._enumerate_input_variables(v, wanted_key, internal) for v in value.values() ], [])
75
77
 
76
78
  if isinstance(value, list):
77
- return sum([ self._enumerate_input_variables(v, wanted_key) for v in value ], [])
79
+ return sum([ self._enumerate_input_variables(v, wanted_key, internal) for v in value ], [])
78
80
 
79
81
  return []
80
82
 
81
- def _enumerate_output_variables(self, name: Optional[str], value: Any) -> List[WorkflowVariable]:
83
+ def _enumerate_output_variables(self, name: Optional[str], value: Any, internal: bool = False) -> List[WorkflowVariable]:
82
84
  variables: List[WorkflowVariable] = []
83
85
 
84
86
  if isinstance(value, str):
@@ -88,18 +90,18 @@ class WorkflowVariableResolver:
88
90
  if type and default:
89
91
  default = self._parse_as_type(default, type)
90
92
 
91
- variables.append(WorkflowVariable(name, type or "string", subtype, format, default))
93
+ variables.append(WorkflowVariable(name, type or "string", subtype, format, default, internal))
92
94
 
93
95
  return variables
94
96
 
95
97
  if isinstance(value, BaseModel):
96
- return self._enumerate_output_variables(name, value.model_dump(exclude_none=True))
98
+ return self._enumerate_output_variables(name, value.model_dump(exclude_none=True), internal)
97
99
 
98
100
  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() ], [])
101
+ return sum([ self._enumerate_output_variables(f"{name}.{k}" if name else f"{k}", v, internal) for k, v in value.items() ], [])
100
102
 
101
103
  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) ], [])
104
+ return sum([ self._enumerate_output_variables(f"{name}[{i}]" if name else f"[{i}]", v, internal) for i, v in enumerate(value) ], [])
103
105
 
104
106
  return []
105
107
 
@@ -126,7 +128,7 @@ class WorkflowVariableResolver:
126
128
  def _to_variable_config(self, variable: WorkflowVariable) -> WorkflowVariableConfig:
127
129
  config_dict = variable.to_dict()
128
130
 
129
- if variable.type == "select" and variable.subtype:
131
+ if variable.type in [ "image", "audio", "video", "file", "select" ] and variable.subtype:
130
132
  config_dict["options"] = variable.subtype.split(",")
131
133
 
132
134
  return WorkflowVariableConfig(**config_dict)
@@ -156,9 +158,13 @@ class WorkflowInputVariableResolver(WorkflowVariableResolver):
156
158
  if isinstance(job.component, str):
157
159
  component: Optional[ComponentConfig] = components[job.component] if job.component in components else None
158
160
  if component:
159
- variables.extend(self._enumerate_input_variables(component.actions[action_id], "input"))
161
+ action = component.actions[action_id] if action_id in component.actions else None
162
+ if action:
163
+ variables.extend(self._enumerate_input_variables(action, "input", internal=True))
160
164
  else:
161
- variables.extend(self._enumerate_input_variables(job.component.actions[action_id], "input"))
165
+ action = job.component.actions[action_id] if action_id in job.component.actions else None
166
+ if action:
167
+ variables.extend(self._enumerate_input_variables(action, "input", internal=True))
162
168
  else:
163
169
  variables.extend(self._enumerate_input_variables(job.input, "input"))
164
170
 
@@ -186,9 +192,13 @@ class WorkflowOutputVariableResolver(WorkflowVariableResolver):
186
192
  if isinstance(job.component, str):
187
193
  component: Optional[ComponentConfig] = components[job.component] if job.component in components else None
188
194
  if component:
189
- job_variables.extend(self._enumerate_output_variables(None, component.actions[action_id].output))
195
+ action = component.actions[action_id] if action_id in component.actions else None
196
+ if action:
197
+ job_variables.extend(self._enumerate_output_variables(None, action.output, internal=True))
190
198
  else:
191
- job_variables.extend(self._enumerate_output_variables(None, job.component.actions[action_id].output))
199
+ action = job.component.actions[action_id] if action_id in job.component.actions else None
200
+ if action:
201
+ job_variables.extend(self._enumerate_output_variables(None, action.output, internal=True))
192
202
  else:
193
203
  job_variables.extend(self._enumerate_output_variables(None, job.output))
194
204
 
@@ -25,8 +25,8 @@ class HttpCallbackContext:
25
25
  async for item in self._items():
26
26
  yield HttpCallbackContext(item, self.query, False, None)
27
27
 
28
- async def render_template(self, template: str) -> Any:
29
- return await self.renderer.render(template)
28
+ async def render_template(self, template: str, ignore_files: bool = True) -> Any:
29
+ return await self.renderer.render(template, ignore_files)
30
30
 
31
31
  async def _items(self) -> List[Any]:
32
32
  item = await self.renderer.render(self.item) if self.item else self.body
@@ -78,7 +78,7 @@ class HttpCallbackListener(ListenerEngine):
78
78
 
79
79
  if future:
80
80
  if succeeded:
81
- future.set_result((await item.render_template(callback.result)) if callback.result else item.body)
81
+ future.set_result((await item.render_template(callback.result, ignore_files=True)) 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)
@@ -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, os
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:])
@@ -84,3 +101,21 @@ def parse_options_header(headers: Dict[str, str], header_name: str) -> Tuple[str
84
101
  options = {}
85
102
 
86
103
  return value.strip().lower(), options
104
+
105
+ def create_upload_file(path: str, type: Optional[str], subtype: Optional[str]) -> UploadFile:
106
+ file, filename = open(path, "rb"), os.path.basename(path)
107
+ content_type = guess_file_content_type(filename, type, subtype)
108
+ headers = {
109
+ "Content-Type": content_type,
110
+ "Content-Disposition": f'form-data; filename="{filename}"'
111
+ }
112
+
113
+ return UploadFile(file=file, filename=filename, headers=headers)
114
+
115
+ def guess_file_content_type(filename: str, type: Optional[str], subtype: Optional[str]) -> str:
116
+ subtype = filename.split(".")[-1] if not subtype else subtype
117
+
118
+ if type in [ "image", "audio", "video" ] and subtype:
119
+ return f"{type}/{subtype}"
120
+
121
+ return "application/octet-stream"
@@ -1,7 +1,8 @@
1
1
  from typing import Optional, AsyncIterator
2
2
  from abc import ABC, abstractmethod
3
3
  from tempfile import NamedTemporaryFile
4
- import io, base64
4
+ from aiofiles.threadpool.binary import AsyncBufferedReader
5
+ import aiofiles, io, base64
5
6
 
6
7
  class StreamResource(ABC):
7
8
  def __init__(self, content_type: Optional[str], filename: Optional[str]):
@@ -29,7 +30,12 @@ class Base64StreamResource(StreamResource):
29
30
  def __init__(self, encoded: str, content_type: Optional[str] = None, filename: Optional[str] = None):
30
31
  super().__init__(content_type, filename)
31
32
 
32
- self.stream: Optional[io.BytesIO] = io.BytesIO(base64.b64decode(encoded))
33
+ self.encoded: str = encoded
34
+ self.stream: Optional[io.BytesIO] = None
35
+
36
+ async def __aenter__(self):
37
+ self.stream = io.BytesIO(base64.b64decode(self.encoded))
38
+ return self
33
39
 
34
40
  async def close(self):
35
41
  self.stream.close()