model-compose 0.1.6__tar.gz → 0.1.8__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 (126) hide show
  1. {model_compose-0.1.6/src/model_compose.egg-info → model_compose-0.1.8}/PKG-INFO +2 -1
  2. {model_compose-0.1.6 → model_compose-0.1.8}/pyproject.toml +2 -1
  3. {model_compose-0.1.6 → model_compose-0.1.8}/src/mindor/core/component/engine/__init__.py +3 -0
  4. model_compose-0.1.8/src/mindor/core/component/engine/mcp_client.py +19 -0
  5. model_compose-0.1.8/src/mindor/core/component/engine/mcp_server.py +19 -0
  6. model_compose-0.1.8/src/mindor/core/component/engine/shell.py +80 -0
  7. model_compose-0.1.8/src/mindor/core/controller/engine/mcp_server.py +158 -0
  8. {model_compose-0.1.6 → model_compose-0.1.8}/src/mindor/core/controller/runner/mcp_client.py +3 -0
  9. {model_compose-0.1.6 → model_compose-0.1.8}/src/mindor/core/controller/webui/gradio.py +21 -19
  10. {model_compose-0.1.6 → model_compose-0.1.8}/src/mindor/core/controller/webui/webui.py +4 -19
  11. model_compose-0.1.8/src/mindor/core/gateway/engine/ssh_tunnel.py +25 -0
  12. {model_compose-0.1.6 → model_compose-0.1.8}/src/mindor/core/utils/http_client.py +1 -1
  13. model_compose-0.1.8/src/mindor/core/utils/mcp_client.py +4 -0
  14. model_compose-0.1.8/src/mindor/core/utils/ssh_client.py +4 -0
  15. {model_compose-0.1.6 → model_compose-0.1.8}/src/mindor/core/utils/streaming.py +1 -1
  16. {model_compose-0.1.6 → model_compose-0.1.8}/src/mindor/core/utils/template.py +26 -13
  17. {model_compose-0.1.6/src/mindor/core/controller/webui → model_compose-0.1.8/src/mindor/core/workflow}/schema.py +105 -45
  18. {model_compose-0.1.6 → model_compose-0.1.8}/src/mindor/dsl/schema/action/impl/__init__.py +1 -0
  19. model_compose-0.1.8/src/mindor/dsl/schema/action/impl/shell.py +10 -0
  20. {model_compose-0.1.6 → model_compose-0.1.8}/src/mindor/dsl/schema/component/component.py +2 -1
  21. {model_compose-0.1.6 → model_compose-0.1.8}/src/mindor/dsl/schema/component/impl/__init__.py +1 -0
  22. {model_compose-0.1.6 → model_compose-0.1.8}/src/mindor/dsl/schema/component/impl/http_server.py +1 -1
  23. model_compose-0.1.8/src/mindor/dsl/schema/component/impl/shell.py +19 -0
  24. {model_compose-0.1.6 → model_compose-0.1.8}/src/mindor/dsl/schema/component/impl/types.py +1 -0
  25. {model_compose-0.1.6 → model_compose-0.1.8}/src/mindor/dsl/schema/controller/impl/common.py +1 -0
  26. {model_compose-0.1.6 → model_compose-0.1.8}/src/mindor/dsl/schema/controller/impl/mcp_server.py +2 -0
  27. {model_compose-0.1.6 → model_compose-0.1.8}/src/mindor/dsl/schema/gateway/gateway.py +2 -1
  28. {model_compose-0.1.6 → model_compose-0.1.8}/src/mindor/dsl/schema/gateway/impl/__init__.py +1 -0
  29. model_compose-0.1.8/src/mindor/dsl/schema/gateway/impl/ssh_tunnel.py +7 -0
  30. {model_compose-0.1.6 → model_compose-0.1.8}/src/mindor/dsl/schema/gateway/impl/types.py +1 -0
  31. {model_compose-0.1.6 → model_compose-0.1.8}/src/mindor/dsl/schema/workflow.py +38 -27
  32. {model_compose-0.1.6 → model_compose-0.1.8/src/model_compose.egg-info}/PKG-INFO +2 -1
  33. {model_compose-0.1.6 → model_compose-0.1.8}/src/model_compose.egg-info/SOURCES.txt +10 -1
  34. {model_compose-0.1.6 → model_compose-0.1.8}/src/model_compose.egg-info/requires.txt +1 -0
  35. model_compose-0.1.6/src/mindor/core/controller/engine/mcp_server.py +0 -28
  36. {model_compose-0.1.6 → model_compose-0.1.8}/LICENSE +0 -0
  37. {model_compose-0.1.6 → model_compose-0.1.8}/README.md +0 -0
  38. {model_compose-0.1.6 → model_compose-0.1.8}/setup.cfg +0 -0
  39. {model_compose-0.1.6 → model_compose-0.1.8}/src/mindor/__init__.py +0 -0
  40. {model_compose-0.1.6 → model_compose-0.1.8}/src/mindor/cli/__init__.py +0 -0
  41. {model_compose-0.1.6 → model_compose-0.1.8}/src/mindor/cli/compose.py +0 -0
  42. {model_compose-0.1.6 → model_compose-0.1.8}/src/mindor/core/__init__.py +0 -0
  43. {model_compose-0.1.6 → model_compose-0.1.8}/src/mindor/core/component/__init__.py +0 -0
  44. {model_compose-0.1.6 → model_compose-0.1.8}/src/mindor/core/component/component.py +0 -0
  45. {model_compose-0.1.6 → model_compose-0.1.8}/src/mindor/core/component/engine/base.py +0 -0
  46. {model_compose-0.1.6 → model_compose-0.1.8}/src/mindor/core/component/engine/context.py +0 -0
  47. {model_compose-0.1.6 → model_compose-0.1.8}/src/mindor/core/component/engine/http_client.py +0 -0
  48. {model_compose-0.1.6 → model_compose-0.1.8}/src/mindor/core/component/engine/http_server.py +0 -0
  49. {model_compose-0.1.6 → model_compose-0.1.8}/src/mindor/core/compose/__init__.py +0 -0
  50. {model_compose-0.1.6 → model_compose-0.1.8}/src/mindor/core/compose/compose.py +0 -0
  51. {model_compose-0.1.6 → model_compose-0.1.8}/src/mindor/core/compose/manager.py +0 -0
  52. {model_compose-0.1.6 → model_compose-0.1.8}/src/mindor/core/controller/__init__.py +0 -0
  53. {model_compose-0.1.6 → model_compose-0.1.8}/src/mindor/core/controller/controller.py +0 -0
  54. {model_compose-0.1.6 → model_compose-0.1.8}/src/mindor/core/controller/engine/__init__.py +0 -0
  55. {model_compose-0.1.6 → model_compose-0.1.8}/src/mindor/core/controller/engine/base.py +0 -0
  56. {model_compose-0.1.6 → model_compose-0.1.8}/src/mindor/core/controller/engine/http_server.py +0 -0
  57. {model_compose-0.1.6 → model_compose-0.1.8}/src/mindor/core/controller/runner/__init__.py +0 -0
  58. {model_compose-0.1.6 → model_compose-0.1.8}/src/mindor/core/controller/runner/client.py +0 -0
  59. {model_compose-0.1.6 → model_compose-0.1.8}/src/mindor/core/controller/runner/http_client.py +0 -0
  60. {model_compose-0.1.6 → model_compose-0.1.8}/src/mindor/core/controller/runner/runner.py +0 -0
  61. {model_compose-0.1.6 → model_compose-0.1.8}/src/mindor/core/controller/webui/__init__.py +0 -0
  62. {model_compose-0.1.6 → model_compose-0.1.8}/src/mindor/core/gateway/__init__.py +0 -0
  63. {model_compose-0.1.6 → model_compose-0.1.8}/src/mindor/core/gateway/engine/__init__.py +0 -0
  64. {model_compose-0.1.6 → model_compose-0.1.8}/src/mindor/core/gateway/engine/base.py +0 -0
  65. {model_compose-0.1.6 → model_compose-0.1.8}/src/mindor/core/gateway/engine/http_tunnel.py +0 -0
  66. {model_compose-0.1.6 → model_compose-0.1.8}/src/mindor/core/gateway/gateway.py +0 -0
  67. {model_compose-0.1.6 → model_compose-0.1.8}/src/mindor/core/listener/__init__.py +0 -0
  68. {model_compose-0.1.6 → model_compose-0.1.8}/src/mindor/core/listener/engine/__init__.py +0 -0
  69. {model_compose-0.1.6 → model_compose-0.1.8}/src/mindor/core/listener/engine/base.py +0 -0
  70. {model_compose-0.1.6 → model_compose-0.1.8}/src/mindor/core/listener/engine/http_callback.py +0 -0
  71. {model_compose-0.1.6 → model_compose-0.1.8}/src/mindor/core/listener/listener.py +0 -0
  72. {model_compose-0.1.6 → model_compose-0.1.8}/src/mindor/core/runtime/__init__.py +0 -0
  73. {model_compose-0.1.6 → model_compose-0.1.8}/src/mindor/core/runtime/env.py +0 -0
  74. {model_compose-0.1.6 → model_compose-0.1.8}/src/mindor/core/services/__init__.py +0 -0
  75. {model_compose-0.1.6 → model_compose-0.1.8}/src/mindor/core/services/async_service.py +0 -0
  76. {model_compose-0.1.6 → model_compose-0.1.8}/src/mindor/core/utils/__init__.py +0 -0
  77. {model_compose-0.1.6 → model_compose-0.1.8}/src/mindor/core/utils/expiring.py +0 -0
  78. {model_compose-0.1.6 → model_compose-0.1.8}/src/mindor/core/utils/http_request.py +0 -0
  79. {model_compose-0.1.6 → model_compose-0.1.8}/src/mindor/core/utils/image.py +0 -0
  80. {model_compose-0.1.6 → model_compose-0.1.8}/src/mindor/core/utils/time.py +0 -0
  81. {model_compose-0.1.6 → model_compose-0.1.8}/src/mindor/core/utils/workqueue.py +0 -0
  82. {model_compose-0.1.6 → model_compose-0.1.8}/src/mindor/core/workflow/__init__.py +0 -0
  83. {model_compose-0.1.6 → model_compose-0.1.8}/src/mindor/core/workflow/context.py +0 -0
  84. {model_compose-0.1.6 → model_compose-0.1.8}/src/mindor/core/workflow/job.py +0 -0
  85. {model_compose-0.1.6 → model_compose-0.1.8}/src/mindor/core/workflow/workflow.py +0 -0
  86. {model_compose-0.1.6 → model_compose-0.1.8}/src/mindor/dsl/__init__.py +0 -0
  87. {model_compose-0.1.6 → model_compose-0.1.8}/src/mindor/dsl/loader.py +0 -0
  88. {model_compose-0.1.6 → model_compose-0.1.8}/src/mindor/dsl/schema/__init__.py +0 -0
  89. {model_compose-0.1.6 → model_compose-0.1.8}/src/mindor/dsl/schema/action/__init__.py +0 -0
  90. {model_compose-0.1.6 → model_compose-0.1.8}/src/mindor/dsl/schema/action/action.py +0 -0
  91. {model_compose-0.1.6 → model_compose-0.1.8}/src/mindor/dsl/schema/action/impl/common.py +0 -0
  92. {model_compose-0.1.6 → model_compose-0.1.8}/src/mindor/dsl/schema/action/impl/http_client.py +0 -0
  93. {model_compose-0.1.6 → model_compose-0.1.8}/src/mindor/dsl/schema/action/impl/http_server.py +0 -0
  94. {model_compose-0.1.6 → model_compose-0.1.8}/src/mindor/dsl/schema/action/impl/mcp_client.py +0 -0
  95. {model_compose-0.1.6 → model_compose-0.1.8}/src/mindor/dsl/schema/action/impl/mcp_server.py +0 -0
  96. {model_compose-0.1.6 → model_compose-0.1.8}/src/mindor/dsl/schema/component/__init__.py +0 -0
  97. {model_compose-0.1.6 → model_compose-0.1.8}/src/mindor/dsl/schema/component/impl/common.py +0 -0
  98. {model_compose-0.1.6 → model_compose-0.1.8}/src/mindor/dsl/schema/component/impl/http_client.py +0 -0
  99. {model_compose-0.1.6 → model_compose-0.1.8}/src/mindor/dsl/schema/component/impl/mcp_client.py +0 -0
  100. {model_compose-0.1.6 → model_compose-0.1.8}/src/mindor/dsl/schema/component/impl/mcp_server.py +0 -0
  101. {model_compose-0.1.6 → model_compose-0.1.8}/src/mindor/dsl/schema/compose.py +0 -0
  102. {model_compose-0.1.6 → model_compose-0.1.8}/src/mindor/dsl/schema/controller/__init__.py +0 -0
  103. {model_compose-0.1.6 → model_compose-0.1.8}/src/mindor/dsl/schema/controller/controller.py +0 -0
  104. {model_compose-0.1.6 → model_compose-0.1.8}/src/mindor/dsl/schema/controller/impl/__init__.py +0 -0
  105. {model_compose-0.1.6 → model_compose-0.1.8}/src/mindor/dsl/schema/controller/impl/http_server.py +0 -0
  106. {model_compose-0.1.6 → model_compose-0.1.8}/src/mindor/dsl/schema/controller/impl/types.py +0 -0
  107. {model_compose-0.1.6 → model_compose-0.1.8}/src/mindor/dsl/schema/controller/impl/webui.py +0 -0
  108. {model_compose-0.1.6 → model_compose-0.1.8}/src/mindor/dsl/schema/gateway/__init__.py +0 -0
  109. {model_compose-0.1.6 → model_compose-0.1.8}/src/mindor/dsl/schema/gateway/impl/common.py +0 -0
  110. {model_compose-0.1.6 → model_compose-0.1.8}/src/mindor/dsl/schema/gateway/impl/http_tunnel.py +0 -0
  111. {model_compose-0.1.6 → model_compose-0.1.8}/src/mindor/dsl/schema/listener/__init__.py +0 -0
  112. {model_compose-0.1.6 → model_compose-0.1.8}/src/mindor/dsl/schema/listener/impl/__init__.py +0 -0
  113. {model_compose-0.1.6 → model_compose-0.1.8}/src/mindor/dsl/schema/listener/impl/common.py +0 -0
  114. {model_compose-0.1.6 → model_compose-0.1.8}/src/mindor/dsl/schema/listener/impl/http_callback.py +0 -0
  115. {model_compose-0.1.6 → model_compose-0.1.8}/src/mindor/dsl/schema/listener/impl/types.py +0 -0
  116. {model_compose-0.1.6 → model_compose-0.1.8}/src/mindor/dsl/schema/listener/listener.py +0 -0
  117. {model_compose-0.1.6 → model_compose-0.1.8}/src/mindor/dsl/schema/runtime/__init__.py +0 -0
  118. {model_compose-0.1.6 → model_compose-0.1.8}/src/mindor/dsl/schema/runtime/impl/__init__.py +0 -0
  119. {model_compose-0.1.6 → model_compose-0.1.8}/src/mindor/dsl/schema/runtime/impl/common.py +0 -0
  120. {model_compose-0.1.6 → model_compose-0.1.8}/src/mindor/dsl/schema/runtime/impl/docker.py +0 -0
  121. {model_compose-0.1.6 → model_compose-0.1.8}/src/mindor/dsl/schema/runtime/impl/types.py +0 -0
  122. {model_compose-0.1.6 → model_compose-0.1.8}/src/mindor/dsl/schema/runtime/runtime.py +0 -0
  123. {model_compose-0.1.6 → model_compose-0.1.8}/src/model_compose.egg-info/dependency_links.txt +0 -0
  124. {model_compose-0.1.6 → model_compose-0.1.8}/src/model_compose.egg-info/entry_points.txt +0 -0
  125. {model_compose-0.1.6 → model_compose-0.1.8}/src/model_compose.egg-info/top_level.txt +0 -0
  126. {model_compose-0.1.6 → model_compose-0.1.8}/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.6
3
+ Version: 0.1.8
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
@@ -15,6 +15,7 @@ Requires-Dist: aiohttp
15
15
  Requires-Dist: requests
16
16
  Requires-Dist: fastapi
17
17
  Requires-Dist: uvicorn
18
+ Requires-Dist: mcp
18
19
  Requires-Dist: pyngrok
19
20
  Requires-Dist: ulid
20
21
  Requires-Dist: gradio
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "model-compose"
3
- version = "0.1.6"
3
+ version = "0.1.8"
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" }
@@ -16,6 +16,7 @@ dependencies = [
16
16
  "requests",
17
17
  "fastapi",
18
18
  "uvicorn",
19
+ "mcp",
19
20
  "pyngrok",
20
21
  "ulid",
21
22
  "gradio",
@@ -1,3 +1,6 @@
1
1
  from .base import *
2
2
  from .http_server import *
3
3
  from .http_client import *
4
+ from .mcp_server import *
5
+ from .mcp_client import *
6
+ from .shell import *
@@ -0,0 +1,19 @@
1
+ from typing import Type, Union, Literal, Optional, Dict, List, Tuple, Set, Annotated, Any
2
+ from mindor.dsl.schema.component import McpClientComponentConfig
3
+ from .base import ComponentEngine, ComponentType, ComponentEngineMap, ActionConfig
4
+ from .context import ComponentContext
5
+
6
+ class McpClientComponent(ComponentEngine):
7
+ def __init__(self, id: str, config: McpClientComponentConfig, env: Dict[str, str], daemon: bool):
8
+ super().__init__(id, config, env, daemon)
9
+
10
+ async def _serve(self) -> None:
11
+ pass
12
+
13
+ async def _shutdown(self) -> None:
14
+ pass
15
+
16
+ async def _run(self, action: ActionConfig, context: ComponentContext) -> Any:
17
+ return {}
18
+
19
+ ComponentEngineMap[ComponentType.MCP_CLIENT] = McpClientComponent
@@ -0,0 +1,19 @@
1
+ from typing import Type, Union, Literal, Optional, Dict, List, Tuple, Set, Annotated, Any
2
+ from mindor.dsl.schema.component import McpServerComponentConfig
3
+ from .base import ComponentEngine, ComponentType, ComponentEngineMap, ActionConfig
4
+ from .context import ComponentContext
5
+
6
+ class McpServerComponent(ComponentEngine):
7
+ def __init__(self, id: str, config: McpServerComponentConfig, env: Dict[str, str], daemon: bool):
8
+ super().__init__(id, config, env, daemon)
9
+
10
+ async def _serve(self) -> None:
11
+ pass
12
+
13
+ async def _shutdown(self) -> None:
14
+ pass
15
+
16
+ async def _run(self, action: ActionConfig, context: ComponentContext) -> Any:
17
+ return {}
18
+
19
+ ComponentEngineMap[ComponentType.MCP_SERVER] = McpServerComponent
@@ -0,0 +1,80 @@
1
+ from typing import Type, Union, Literal, Optional, Dict, List, Tuple, Set, Annotated, Any
2
+ from mindor.dsl.schema.component import ShellComponentConfig
3
+ from mindor.dsl.schema.action import ActionConfig, ShellActionConfig
4
+ from .base import ComponentEngine, ComponentType, ComponentEngineMap, ActionConfig
5
+ from .context import ComponentContext
6
+ from asyncio.subprocess import Process
7
+ import asyncio, os
8
+
9
+ class ShellAction:
10
+ def __init__(self, base_dir: Optional[str], env: Optional[Dict[str, str]], config: ShellActionConfig):
11
+ self.base_dir: Optional[str] = base_dir
12
+ self.env: Optional[Dict[str, str]] = env
13
+ self.config: ShellActionConfig = config
14
+
15
+ async def run(self, context: ComponentContext) -> Any:
16
+ working_dir = self._resolve_working_directory()
17
+ env = { **(self.env or {}), **(self.config.env or {}) }
18
+
19
+ result = await self._run_command(self.config.command, working_dir, env, self.config.timeout)
20
+ context.register_source("result", result)
21
+
22
+ return (await context.render_template(self.config.output, ignore_files=True)) if self.config.output else result
23
+
24
+ async def _run_command(self, command: List[str], working_dir: str, env: Dict[str, str], timeout: Optional[float]) -> Dict[str, Any]:
25
+ process = await asyncio.create_subprocess_exec(
26
+ *command,
27
+ cwd=working_dir,
28
+ env={**os.environ, **env},
29
+ stdout=asyncio.subprocess.PIPE,
30
+ stderr=asyncio.subprocess.PIPE
31
+ )
32
+
33
+ try:
34
+ stdout, stderr = await asyncio.wait_for(process.communicate(), timeout=timeout)
35
+ except asyncio.TimeoutError:
36
+ if await self._kill_process(process):
37
+ raise RuntimeError(f"Command timed out: {' '.join(command)}")
38
+
39
+ return {
40
+ "stdout": stdout.decode().strip(),
41
+ "stderr": stderr.decode().strip(),
42
+ "exit_code": process.returncode
43
+ }
44
+
45
+ async def _kill_process(self, process: Process) -> bool:
46
+ if process.returncode is None:
47
+ process.kill()
48
+ try:
49
+ await process.wait()
50
+ except Exception as e:
51
+ pass
52
+ return True
53
+ else:
54
+ return False
55
+
56
+ def _resolve_working_directory(self) -> str:
57
+ working_dir = self.config.working_dir
58
+ if working_dir:
59
+ if self.base_dir:
60
+ working_dir = os.path.abspath(os.path.join(self.base_dir, working_dir))
61
+ else:
62
+ working_dir = os.path.abspath(working_dir)
63
+ else:
64
+ working_dir = self.base_dir or os.getcwd()
65
+ return working_dir
66
+
67
+ class ShellComponent(ComponentEngine):
68
+ def __init__(self, id: str, config: ShellComponentConfig, env: Dict[str, str], daemon: bool):
69
+ super().__init__(id, config, env, daemon)
70
+
71
+ async def _serve(self) -> None:
72
+ pass
73
+
74
+ async def _shutdown(self) -> None:
75
+ pass
76
+
77
+ async def _run(self, action: ActionConfig, context: ComponentContext) -> Any:
78
+ return await ShellAction(self.config.base_dir, self.config.env, action).run(context)
79
+
80
+ ComponentEngineMap[ComponentType.SHELL] = ShellComponent
@@ -0,0 +1,158 @@
1
+ from typing import Type, Union, Literal, Optional, Dict, List, Tuple, Set, Annotated, Callable, Awaitable, Any
2
+ from mindor.dsl.schema.controller import McpServerControllerConfig
3
+ from mindor.dsl.schema.component import ComponentConfig
4
+ from mindor.dsl.schema.listener import ListenerConfig
5
+ from mindor.dsl.schema.gateway import GatewayConfig
6
+ from mindor.dsl.schema.workflow import WorkflowConfig, WorkflowVariableConfig
7
+ from mindor.core.workflow.schema import WorkflowSchema, create_workflow_schema
8
+ from mindor.core.utils.streaming import StreamResource
9
+ from .base import ControllerEngine, ControllerType, ControllerEngineMap, TaskState
10
+ from mcp.server.fastmcp.server import FastMCP
11
+ from mcp.types import TextContent, ImageContent, AudioContent
12
+ import uvicorn, re, json
13
+
14
+ _invalid_function_chars_regex = re.compile(r"[^a-zA-Z0-9_]")
15
+
16
+ class WorkflowToolGenerator():
17
+ def generate(self, workflow_id: str, workflow: WorkflowSchema, runner: Callable[[Optional[str], Any], Awaitable[Any]]) -> Tuple[Callable[[Any], Awaitable[Any]], str]:
18
+ async def _run_workflow(input: Any, workflow_id=workflow_id, variables=workflow.output) -> Any:
19
+ return await self._build_output_value(await runner(workflow_id, input), variables)
20
+
21
+ async def _build_input_value(arguments, variables=workflow.input) -> Any:
22
+ return await self._build_input_value(arguments, variables)
23
+
24
+ safe_workflow_id = re.sub(_invalid_function_chars_regex, "_", workflow_id)
25
+ arguments = ",".join([ variable.name or "input" for variable in workflow.input ])
26
+ code = f"async def _run_workflow_{safe_workflow_id}({arguments}): return await _run_workflow(await _build_input_value([{arguments}]))"
27
+ context = { "_run_workflow": _run_workflow, "_build_input_value": _build_input_value }
28
+ exec(compile(code, f"<string>", "exec"), context)
29
+
30
+ return (context[f"_run_workflow_{safe_workflow_id}"], self._generate_description(workflow))
31
+
32
+ async def _build_input_value(self, arguments: List[Any], variables: List[WorkflowVariableConfig]) -> Any:
33
+ input: Dict[str, Any] = {}
34
+
35
+ for value, variable in zip(arguments, variables):
36
+ type, subtype, format = variable.type.value, variable.subtype, variable.format.value if variable.format else None
37
+ input[variable.name or "input"] = await self._convert_input_value(value, type, subtype, format, variable.default)
38
+
39
+ return input
40
+
41
+ async def _convert_input_value(self, value: Any, type: str, subtype: Optional[str], format: Optional[str], default: Optional[Any]) -> Any:
42
+ if type in [ "image", "audio", "video", "file" ]:
43
+ if format and format != "path":
44
+ pass
45
+
46
+ return value if value != "" else None
47
+
48
+ async def _build_output_value(self, state: TaskState, variables: List[WorkflowVariableConfig]) -> List[Union[TextContent, ImageContent, AudioContent]]:
49
+ output: List[Union[TextContent, ImageContent]] = []
50
+
51
+ if state.output:
52
+ for variable in variables:
53
+ type, subtype, format = variable.type.value, variable.subtype, variable.format.value if variable.format else None
54
+ output.append(await self._convert_output_value(state.output, type, subtype, format))
55
+
56
+ return output
57
+
58
+ async def _convert_output_value(self, value: Any, type: str, subtype: Optional[str], format: Optional[str]) -> Union[TextContent, ImageContent, AudioContent]:
59
+ if type in [ "image", "audio", "video", "file" ]:
60
+ if format and format != "path":
61
+ pass
62
+
63
+ if isinstance(value, (dict, list)):
64
+ return TextContent(type="text", text=json.dumps(value))
65
+
66
+ return TextContent(type="text", text=str(value))
67
+
68
+ def _generate_description(self, workflow: WorkflowSchema) -> str:
69
+ lines = []
70
+
71
+ lines.append(workflow.description or workflow.title or "")
72
+ lines.append("")
73
+ lines.append("Args:")
74
+
75
+ for variable in workflow.input:
76
+ name, type = variable.name or "input", self._get_docstring_type(variable)
77
+ description = variable.get_annotation_value("description") or ""
78
+ lines.append(f" {name} ({type}): {description}")
79
+
80
+ lines.append("Returns:")
81
+
82
+ if len(workflow.output) == 1 and not workflow.output[0].name:
83
+ variable = workflow.output[0]
84
+ name, type = variable.name or "", self._get_docstring_type(variable)
85
+ lines.append(f" {type}: {description}")
86
+ else:
87
+ lines.append(f" dict: results containing:")
88
+ for variable in workflow.output:
89
+ name, type = variable.name or "", self._get_docstring_type(variable)
90
+ description = variable.get_annotation_value("description") or ""
91
+ lines.append(f" - {name} ({type}): {description}")
92
+
93
+ return "\n".join(lines)
94
+
95
+ def _get_docstring_type(self, variable: WorkflowVariableConfig) -> str:
96
+ type, subtype, format = variable.type.value, variable.subtype, variable.format.value if variable.format else None
97
+
98
+ if type == "object[]":
99
+ return "list[dict]"
100
+
101
+ if type == "number":
102
+ return "float"
103
+
104
+ if type == "integer":
105
+ return "int"
106
+
107
+ return "str"
108
+
109
+ class McpServerController(ControllerEngine):
110
+ def __init__(
111
+ self,
112
+ config: McpServerControllerConfig,
113
+ components: Dict[str, ComponentConfig],
114
+ listeners: List[ListenerConfig],
115
+ gateways: List[GatewayConfig],
116
+ workflows: Dict[str, WorkflowConfig],
117
+ env: Dict[str, str],
118
+ daemon: bool
119
+ ):
120
+ super().__init__(config, components, listeners, gateways, workflows, env, daemon)
121
+
122
+ self.server: Optional[uvicorn.Server] = None
123
+ self.app: FastMCP = FastMCP(self.config.name, settings={
124
+ "streamable_http_path": self.config.base_path
125
+ })
126
+
127
+ self._configure_tools()
128
+
129
+ def _configure_tools(self) -> None:
130
+ schema = create_workflow_schema(self.workflows, self.components)
131
+
132
+ for workflow_id, workflow in schema.items():
133
+ fn, description = WorkflowToolGenerator().generate(workflow_id, workflow, self._run_workflow_as_tool)
134
+ self.app.add_tool(
135
+ fn=fn,
136
+ name=workflow.name or workflow_id,
137
+ title=workflow.title,
138
+ description=description,
139
+ annotations=None
140
+ )
141
+
142
+ async def _run_workflow_as_tool(self, workflow_id: Optional[str], input: Any) -> TaskState:
143
+ return await self.run_workflow(workflow_id, input, wait_for_completion=True)
144
+
145
+ async def _serve(self) -> None:
146
+ self.server = uvicorn.Server(uvicorn.Config(
147
+ self.app.streamable_http_app(),
148
+ host=self.config.host,
149
+ port=self.config.port,
150
+ log_level="info"
151
+ ))
152
+ await self.server.serve()
153
+
154
+ async def _shutdown(self) -> None:
155
+ if self.server:
156
+ self.server.should_exit = True
157
+
158
+ ControllerEngineMap[ControllerType.MCP_SERVER] = McpServerController
@@ -1,10 +1,13 @@
1
1
  from typing import Type, Union, Literal, Optional, Dict, List, Tuple, Set, Annotated, Any
2
2
  from mindor.dsl.schema.controller import ControllerConfig
3
+ from mindor.core.utils.mcp_client import McpClient
3
4
  from .runner import ControllerClient
4
5
 
5
6
  class McpControllerClient(ControllerClient):
6
7
  def __init__(self, config: ControllerConfig):
7
8
  super().__init__(config)
8
9
 
10
+ self.client: McpClient = McpClient()
11
+
9
12
  async def run_workflow(self, workflow_id: Optional[str], input: Any) -> Any:
10
13
  pass
@@ -1,14 +1,13 @@
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.workflow.schema import WorkflowSchema
3
4
  from mindor.core.utils.streaming import StreamResource, Base64StreamResource
4
5
  from mindor.core.utils.streaming import save_stream_to_temporary_file
5
6
  from mindor.core.utils.http_request import create_upload_file
6
7
  from mindor.core.utils.http_client import HttpClient
7
8
  from mindor.core.utils.image import load_image_from_stream
8
- from .schema import WorkflowSchema
9
- from starlette.datastructures import UploadFile
10
9
  import gradio as gr
11
- import json, os
10
+ import json
12
11
 
13
12
  class ComponentGroup:
14
13
  def __init__(self, group: gr.Component, components: List[gr.Component]):
@@ -48,9 +47,9 @@ class GradioWebUIBuilder:
48
47
  output_components = gr.Textbox(label="", lines=8, interactive=False, show_copy_button=True)
49
48
 
50
49
  async def _run_workflow(*args):
51
- input = await self._build_input_values(args, workflow.input)
50
+ input = await self._build_input_value(args, workflow.input)
52
51
  output = await runner(input)
53
- output = await self._flatten_output_values(output, workflow.output)
52
+ output = await self._flatten_output_value(output, workflow.output)
54
53
  return output[0] if len(output) == 1 else output
55
54
 
56
55
  run_button.click(
@@ -63,18 +62,15 @@ class GradioWebUIBuilder:
63
62
 
64
63
  def _build_input_component(self, variable: WorkflowVariableConfig) -> gr.Component:
65
64
  label = (variable.name or "") + (" *" if variable.required else "") + (f" (default: {variable.default})" if variable.default else "")
66
- info = variable.description or ""
65
+ info = variable.get_annotation_value("description") or ""
67
66
  default = variable.default
68
67
 
69
68
  if variable.type == "string" or variable.format in [ "base64", "url" ]:
70
69
  return gr.Textbox(label=label, value="", info=info)
71
70
 
72
- if variable.type == "number":
73
- return gr.Number(label=label, value="", precision=None, info=info)
74
-
75
- if variable.type == "integer":
76
- return gr.Number(label=label, value="", precision=0, info=info)
77
-
71
+ if variable.type in [ "integer", "number" ]:
72
+ return gr.Number(label=label, value="", info=info)
73
+
78
74
  if variable.type == "boolean":
79
75
  return gr.Checkbox(label=label, value=default or False, info=info)
80
76
 
@@ -95,16 +91,16 @@ class GradioWebUIBuilder:
95
91
 
96
92
  return gr.Textbox(label=label, value=default, info=f"Unsupported type: {variable.type}")
97
93
 
98
- async def _build_input_values(self, arguments: List[Any], variables: List[WorkflowVariableConfig]) -> Any:
94
+ async def _build_input_value(self, arguments: List[Any], variables: List[WorkflowVariableConfig]) -> Any:
99
95
  if len(variables) == 1 and not variables[0].name:
100
- return arguments[0]
96
+ value, variable = arguments[0], variables[0]
97
+ type, subtype, format = variable.type.value, variable.subtype, variable.format.value if variable.format else None
98
+ return await self._convert_input_value(value, type, subtype, format, variable.internal)
101
99
 
102
100
  input: Dict[str, Any] = {}
103
-
104
101
  for value, variable in zip(arguments, variables):
105
102
  type, subtype, format = variable.type.value, variable.subtype, variable.format.value if variable.format else None
106
103
  input[variable.name] = await self._convert_input_value(value, type, subtype, format, variable.internal)
107
-
108
104
  return input
109
105
 
110
106
  async def _convert_input_value(self, value: Any, type: str, subtype: Optional[str], format: Optional[str], internal: bool) -> Any:
@@ -126,11 +122,17 @@ class GradioWebUIBuilder:
126
122
  return groups
127
123
 
128
124
  label = variable.name or ""
129
- info = variable.description or ""
125
+ info = variable.get_annotation_value("description") or ""
130
126
 
131
127
  if variable.type in [ "string", "base64" ]:
132
128
  return gr.Textbox(label=label, interactive=False, show_copy_button=True, info=info)
133
129
 
130
+ if variable.type == "markdown":
131
+ return gr.Markdown(label=label)
132
+
133
+ if variable.type in [ "json", "object[]" ]:
134
+ return gr.JSON(label=label)
135
+
134
136
  if variable.type == "image":
135
137
  return gr.Image(label=label, interactive=False)
136
138
 
@@ -152,13 +154,13 @@ class GradioWebUIBuilder:
152
154
  flattened.append(item)
153
155
  return flattened
154
156
 
155
- async def _flatten_output_values(self, output: Any, variables: List[Union[WorkflowVariableConfig, WorkflowVariableGroupConfig]]) -> Any:
157
+ async def _flatten_output_value(self, output: Any, variables: List[Union[WorkflowVariableConfig, WorkflowVariableGroupConfig]]) -> Any:
156
158
  flattened = []
157
159
  for variable in variables:
158
160
  if isinstance(variable, WorkflowVariableGroupConfig):
159
161
  group = output[variable.name] if variable.name else output
160
162
  for value in group:
161
- flattened.extend(await self._flatten_output_values(value, variable.variables))
163
+ flattened.extend(await self._flatten_output_value(value, variable.variables))
162
164
  else:
163
165
  value = output[variable.name] if variable.name else output
164
166
  type, subtype, format = variable.type.value, variable.subtype, variable.format.value if variable.format else None
@@ -3,14 +3,13 @@ from mindor.dsl.schema.controller import ControllerWebUIConfig, ControllerConfig
3
3
  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
+ from mindor.core.workflow.schema import create_workflow_schema
6
7
  from mindor.core.services import AsyncService
7
- from .schema import WorkflowSchema, WorkflowInputVariableResolver, WorkflowOutputVariableResolver
8
8
  from .gradio import GradioWebUIBuilder
9
9
 
10
- from fastapi import FastAPI, APIRouter, Body, HTTPException
11
- import asyncio, uvicorn
10
+ from fastapi import FastAPI
12
11
  from gradio import mount_gradio_app
13
- import gradio
12
+ import uvicorn, gradio
14
13
 
15
14
  class ControllerWebUI(AsyncService):
16
15
  def __init__(
@@ -38,7 +37,7 @@ class ControllerWebUI(AsyncService):
38
37
  def _configure_driver(self) -> None:
39
38
  if self.config.driver == "gradio":
40
39
  blocks: gradio.Blocks = GradioWebUIBuilder().build(
41
- schema=self._resolve_workflow_schema(),
40
+ schema=create_workflow_schema(self.workflows, self.components),
42
41
  runner=self._run_workflow
43
42
  )
44
43
  self.app = mount_gradio_app(self.app, blocks, path="")
@@ -59,17 +58,3 @@ class ControllerWebUI(AsyncService):
59
58
 
60
59
  async def _run_workflow(self, workflow_id: Optional[str], input: Any) -> Any:
61
60
  return await self.runner.run_workflow(workflow_id, input)
62
-
63
- def _resolve_workflow_schema(self) -> Dict[str, WorkflowSchema]:
64
- schema: Dict[str, WorkflowSchema] = {}
65
-
66
- for workflow_id, workflow in self.workflows.items():
67
- schema[workflow_id] = WorkflowSchema(
68
- name=workflow.name,
69
- title=workflow.title,
70
- description=workflow.description,
71
- input=WorkflowInputVariableResolver().resolve(workflow, self.components),
72
- output=WorkflowOutputVariableResolver().resolve(workflow, self.components)
73
- )
74
-
75
- return schema
@@ -0,0 +1,25 @@
1
+ from typing import Type, Union, Literal, Optional, Dict, List, Tuple, Set, Annotated, Callable, Iterator, Any
2
+ from mindor.dsl.schema.gateway import SshTunnelGatewayConfig
3
+ from .base import GatewayEngine, GatewayType, GatewayEngineMap
4
+ from pyngrok import ngrok
5
+
6
+ class SshTunnelGateway(GatewayEngine):
7
+ def __init__(self, id: str, config: SshTunnelGatewayConfig, env: Dict[str, str], daemon: bool):
8
+ super().__init__(id, config, env, daemon)
9
+
10
+ self.tunnel: Optional[Any] = None
11
+ self.public_url: Optional[str] = None
12
+
13
+ def get_context(self) -> Dict[str, Any]:
14
+ return {
15
+ "public_url": self.public_url,
16
+ "port": self.config.port
17
+ }
18
+
19
+ async def _serve(self) -> None:
20
+ pass
21
+
22
+ async def _shutdown(self) -> None:
23
+ pass
24
+
25
+ GatewayEngineMap[GatewayType.SSH_TUNNEL] = SshTunnelGateway
@@ -1,4 +1,4 @@
1
- from typing import Type, Union, Literal, Optional, Dict, List, Tuple, Set, Annotated, AsyncIterator, Any
1
+ from typing import Optional, Dict, Tuple, AsyncIterator, Any
2
2
  from .http_request import build_request_body, parse_options_header
3
3
  from .streaming import StreamResource
4
4
  from requests.structures import CaseInsensitiveDict
@@ -0,0 +1,4 @@
1
+ from typing import Optional, Dict, Tuple, AsyncIterator, Any
2
+
3
+ class McpClient:
4
+ pass
@@ -0,0 +1,4 @@
1
+ from typing import Optional, Dict, Tuple, AsyncIterator, Any
2
+
3
+ class SshClient:
4
+ pass
@@ -2,7 +2,7 @@ from typing import Optional, AsyncIterator
2
2
  from abc import ABC, abstractmethod
3
3
  from tempfile import NamedTemporaryFile
4
4
  from aiofiles.threadpool.binary import AsyncBufferedReader
5
- import aiofiles, io, base64
5
+ import io, base64
6
6
 
7
7
  class StreamResource(ABC):
8
8
  def __init__(self, content_type: Optional[str], filename: Optional[str]):
@@ -11,12 +11,13 @@ class TemplateRenderer:
11
11
  self.source_resolver: Callable[[str], Awaitable[Any]] = source_resolver
12
12
  self.patterns: Dict[str, re.Pattern] = {
13
13
  "variable": re.compile(
14
- r"""\$\{ # ${
15
- ([a-zA-Z_][^.\s]*) # key: input, env, etc.
16
- (?:\.([^\s\|\}]+))? # path: key, key.path[0], etc.
17
- (?:\s*as\s*([^\s\|\}/;]+)(?:/([^\s\|\};]+))?(?:;([^\s\|\}]+))?)? # type/subtype;format
18
- (?:\s*\|\s*([^\}]+))? # default value after `|`
19
- \}""", # }
14
+ r"""\$\{ # ${
15
+ (?:\s*([a-zA-Z_][^.\s]*)) # key: input, env, etc.
16
+ (?:\.([^\s|}]+))? # path: key, key.path[0], etc.
17
+ (?:\s*as\s*([^\s/;}]+)(?:/([^\s;}]+))?(?:;([^\s}]+))?)? # type/subtype;format
18
+ (?:\s*\|\s*((?:\$\{[^}]+\}|\\[$@{}]|(?!\s*(?:@\(|\$\{)).)+))? # default value after `|`
19
+ (?:\s*(@\(\s*[\w]+\s+(?:\\[$@{}]|(?!\s*\$\{).)+\)))? # annotations
20
+ \s*\}""", # }
20
21
  re.VERBOSE,
21
22
  ),
22
23
  "keypath": re.compile(r"[-_\w]+|\[\d+\]"),
@@ -39,18 +40,19 @@ class TemplateRenderer:
39
40
 
40
41
  async def _render_text(self, text: str, ignore_files: bool) -> Any:
41
42
  while True:
42
- match = self.patterns["variable"].search(text)
43
+ m = self.patterns["variable"].search(text)
43
44
 
44
- if not match:
45
+ if not m:
45
46
  break
46
47
 
47
- key, path, type, subtype, format, default, variable = (*match.group(1, 2, 3, 4, 5, 6), match.group(0))
48
+ key, path, type, subtype, format, default, variable = (*m.group(1, 2, 3, 4, 5, 6), m.group(0))
48
49
  try:
49
50
  value = self._resolve_by_path(await self.source_resolver(key), path)
50
51
  except Exception:
51
52
  value = None
52
53
 
53
- value = default if value is None else value
54
+ if value is None and default is not None:
55
+ value = await self._render_text(default, ignore_files)
54
56
 
55
57
  if type and value is not None:
56
58
  value = await self._convert_value_to_type(value, type, subtype, format, ignore_files)
@@ -89,13 +91,24 @@ class TemplateRenderer:
89
91
 
90
92
  if type == "integer":
91
93
  return int(value)
92
-
93
- if type == "json":
94
- return json.loads(value)
95
94
 
96
95
  if type == "boolean":
97
96
  return str(value).lower() in [ "true", "1" ]
97
+
98
+ if type == "json":
99
+ if isinstance(value, str):
100
+ return json.loads(value)
101
+ return value
98
102
 
103
+ if type == "object[]":
104
+ if isinstance(value, list):
105
+ objects = [ v for v in value if isinstance(v, dict) ]
106
+ if subtype:
107
+ paths = [ (path, path.split(".")[-1]) for path in subtype.split(",") ]
108
+ return [ { key: self._resolve_by_path(obj, path) for path, key in paths } for obj in objects ]
109
+ return objects
110
+ return []
111
+
99
112
  if type == "base64":
100
113
  if isinstance(value, StreamResource):
101
114
  return await encode_stream_to_base64(value)