model-compose 0.0.2__tar.gz → 0.0.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.0.2 → model_compose-0.0.3}/PKG-INFO +6 -2
- {model_compose-0.0.2 → model_compose-0.0.3}/pyproject.toml +5 -3
- model_compose-0.0.3/src/mindor/cli/compose.py +180 -0
- model_compose-0.0.3/src/mindor/core/component/__init__.py +1 -0
- model_compose-0.0.3/src/mindor/core/component/component.py +40 -0
- model_compose-0.0.3/src/mindor/core/component/engine/__init__.py +4 -0
- model_compose-0.0.3/src/mindor/core/component/engine/base.py +123 -0
- model_compose-0.0.3/src/mindor/core/component/engine/context.py +24 -0
- model_compose-0.0.3/src/mindor/core/component/engine/http_client.py +87 -0
- model_compose-0.0.3/src/mindor/core/component/engine/http_server.py +19 -0
- model_compose-0.0.3/src/mindor/core/compose/__init__.py +1 -0
- model_compose-0.0.3/src/mindor/core/compose/compose.py +18 -0
- model_compose-0.0.3/src/mindor/core/compose/manager.py +28 -0
- model_compose-0.0.3/src/mindor/core/controller/__init__.py +2 -0
- model_compose-0.0.3/src/mindor/core/controller/controller.py +11 -0
- model_compose-0.0.3/src/mindor/core/controller/engine/__init__.py +3 -0
- model_compose-0.0.3/src/mindor/core/controller/engine/base.py +152 -0
- model_compose-0.0.3/src/mindor/core/controller/engine/http_server.py +74 -0
- model_compose-0.0.3/src/mindor/core/controller/engine/mcp_server.py +17 -0
- model_compose-0.0.3/src/mindor/core/runtime/env.py +21 -0
- model_compose-0.0.3/src/mindor/core/utils/expiring.py +35 -0
- model_compose-0.0.3/src/mindor/core/utils/template.py +59 -0
- model_compose-0.0.3/src/mindor/core/utils/workqueue.py +56 -0
- model_compose-0.0.3/src/mindor/core/workflow/__init__.py +1 -0
- model_compose-0.0.3/src/mindor/core/workflow/context.py +27 -0
- model_compose-0.0.3/src/mindor/core/workflow/job.py +25 -0
- model_compose-0.0.3/src/mindor/core/workflow/workflow.py +156 -0
- model_compose-0.0.3/src/mindor/dsl/loader.py +39 -0
- model_compose-0.0.3/src/mindor/dsl/schema/action/__init__.py +1 -0
- model_compose-0.0.3/src/mindor/dsl/schema/action/action.py +13 -0
- model_compose-0.0.3/src/mindor/dsl/schema/action/impl/__init__.py +5 -0
- model_compose-0.0.3/src/mindor/dsl/schema/action/impl/common.py +6 -0
- model_compose-0.0.3/src/mindor/dsl/schema/action/impl/http_client.py +42 -0
- model_compose-0.0.3/src/mindor/dsl/schema/action/impl/http_server.py +8 -0
- model_compose-0.0.3/src/mindor/dsl/schema/action/impl/mcp_client.py +7 -0
- model_compose-0.0.3/src/mindor/dsl/schema/action/impl/mcp_server.py +8 -0
- model_compose-0.0.3/src/mindor/dsl/schema/component/__init__.py +1 -0
- model_compose-0.0.3/src/mindor/dsl/schema/component/component.py +13 -0
- model_compose-0.0.3/src/mindor/dsl/schema/component/impl/__init__.py +5 -0
- model_compose-0.0.3/src/mindor/dsl/schema/component/impl/common.py +9 -0
- model_compose-0.0.3/src/mindor/dsl/schema/component/impl/http_client.py +33 -0
- model_compose-0.0.3/src/mindor/dsl/schema/component/impl/http_server.py +11 -0
- model_compose-0.0.3/src/mindor/dsl/schema/component/impl/mcp_client.py +10 -0
- model_compose-0.0.3/src/mindor/dsl/schema/component/impl/mcp_server.py +9 -0
- model_compose-0.0.3/src/mindor/dsl/schema/component/impl/types.py +7 -0
- model_compose-0.0.3/src/mindor/dsl/schema/compose.py +28 -0
- model_compose-0.0.3/src/mindor/dsl/schema/controller/__init__.py +1 -0
- model_compose-0.0.3/src/mindor/dsl/schema/controller/controller.py +11 -0
- model_compose-0.0.3/src/mindor/dsl/schema/controller/impl/__init__.py +3 -0
- model_compose-0.0.3/src/mindor/dsl/schema/controller/impl/common.py +9 -0
- model_compose-0.0.3/src/mindor/dsl/schema/controller/impl/http_server.py +9 -0
- model_compose-0.0.3/src/mindor/dsl/schema/controller/impl/mcp_server.py +8 -0
- model_compose-0.0.3/src/mindor/dsl/schema/controller/impl/types.py +5 -0
- model_compose-0.0.3/src/mindor/dsl/schema/runtime/__init__.py +1 -0
- model_compose-0.0.3/src/mindor/dsl/schema/runtime/impl/__init__.py +2 -0
- model_compose-0.0.3/src/mindor/dsl/schema/runtime/impl/common.py +6 -0
- model_compose-0.0.3/src/mindor/dsl/schema/runtime/impl/docker.py +7 -0
- model_compose-0.0.3/src/mindor/dsl/schema/runtime/impl/types.py +4 -0
- model_compose-0.0.3/src/mindor/dsl/schema/runtime/runtime.py +10 -0
- model_compose-0.0.3/src/mindor/dsl/schema/workflow.py +25 -0
- {model_compose-0.0.2 → model_compose-0.0.3}/src/model_compose.egg-info/PKG-INFO +6 -2
- model_compose-0.0.3/src/model_compose.egg-info/SOURCES.txt +75 -0
- model_compose-0.0.3/src/model_compose.egg-info/entry_points.txt +2 -0
- model_compose-0.0.3/src/model_compose.egg-info/requires.txt +9 -0
- model_compose-0.0.2/src/mindor/cli/commands/down.py +0 -6
- model_compose-0.0.2/src/mindor/cli/commands/run.py +0 -9
- model_compose-0.0.2/src/mindor/cli/commands/up.py +0 -6
- model_compose-0.0.2/src/mindor/cli/main.py +0 -15
- model_compose-0.0.2/src/mindor/core/controller.py +0 -5
- model_compose-0.0.2/src/mindor/core/workflow.py +0 -4
- model_compose-0.0.2/src/mindor/dsl/parser.py +0 -0
- model_compose-0.0.2/src/model_compose.egg-info/SOURCES.txt +0 -24
- model_compose-0.0.2/src/model_compose.egg-info/entry_points.txt +0 -2
- model_compose-0.0.2/src/model_compose.egg-info/requires.txt +0 -5
- {model_compose-0.0.2 → model_compose-0.0.3}/LICENSE +0 -0
- {model_compose-0.0.2 → model_compose-0.0.3}/README.md +0 -0
- {model_compose-0.0.2 → model_compose-0.0.3}/setup.cfg +0 -0
- {model_compose-0.0.2 → model_compose-0.0.3}/src/mindor/__init__.py +0 -0
- {model_compose-0.0.2 → model_compose-0.0.3}/src/mindor/cli/__init__.py +0 -0
- {model_compose-0.0.2/src/mindor/cli/commands → model_compose-0.0.3/src/mindor/core}/__init__.py +0 -0
- {model_compose-0.0.2/src/mindor/core → model_compose-0.0.3/src/mindor/core/runtime}/__init__.py +0 -0
- {model_compose-0.0.2/src/mindor/dsl → model_compose-0.0.3/src/mindor/core/utils}/__init__.py +0 -0
- /model_compose-0.0.2/src/mindor/core/model.py → /model_compose-0.0.3/src/mindor/dsl/__init__.py +0 -0
- /model_compose-0.0.2/src/mindor/dsl/loader.py → /model_compose-0.0.3/src/mindor/dsl/schema/__init__.py +0 -0
- {model_compose-0.0.2 → model_compose-0.0.3}/src/model_compose.egg-info/dependency_links.txt +0 -0
- {model_compose-0.0.2 → model_compose-0.0.3}/src/model_compose.egg-info/top_level.txt +0 -0
- {model_compose-0.0.2 → model_compose-0.0.3}/tests/test_cli.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: model-compose
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.3
|
|
4
4
|
Summary: model-compose: Declarative AI Model and Workflow Orchestrator (from Mindor)
|
|
5
5
|
Author-email: Hanyeol Cho <hanyeol.cho@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -11,7 +11,11 @@ Requires-Dist: click
|
|
|
11
11
|
Requires-Dist: pyyaml
|
|
12
12
|
Requires-Dist: pydantic
|
|
13
13
|
Requires-Dist: python-dotenv
|
|
14
|
-
Requires-Dist:
|
|
14
|
+
Requires-Dist: aiohttp
|
|
15
|
+
Requires-Dist: requests
|
|
16
|
+
Requires-Dist: fastapi
|
|
17
|
+
Requires-Dist: uvicorn
|
|
18
|
+
Requires-Dist: ulid
|
|
15
19
|
Dynamic: license-file
|
|
16
20
|
|
|
17
21
|
# model-compose: Declarative AI Model and Workflow Orchestrator (from Mindor)
|
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "model-compose"
|
|
3
|
-
version = "0.0.
|
|
3
|
+
version = "0.0.3"
|
|
4
4
|
description = "model-compose: Declarative AI Model and Workflow Orchestrator (from Mindor)"
|
|
5
5
|
authors = [
|
|
6
6
|
{ name = "Hanyeol Cho", email = "hanyeol.cho@gmail.com" }
|
|
7
7
|
]
|
|
8
8
|
readme = "README.md"
|
|
9
9
|
license = "MIT"
|
|
10
|
-
dependencies = [
|
|
10
|
+
dependencies = [
|
|
11
|
+
"click", "pyyaml", "pydantic", "python-dotenv", "aiohttp", "requests", "fastapi", "uvicorn", "ulid"
|
|
12
|
+
]
|
|
11
13
|
|
|
12
14
|
[project.scripts]
|
|
13
|
-
model-compose = "mindor.cli.
|
|
15
|
+
model-compose = "mindor.cli.compose:compose_command"
|
|
14
16
|
|
|
15
17
|
[project.urls]
|
|
16
18
|
"Homepage" = "https://github.com/hanyeol/model-compose"
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import click
|
|
2
|
+
import json
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import asyncio
|
|
5
|
+
|
|
6
|
+
from mindor.dsl.loader import load_compose_config
|
|
7
|
+
from mindor.core.runtime.env import load_env_files
|
|
8
|
+
from mindor.core.compose import *
|
|
9
|
+
|
|
10
|
+
@click.group()
|
|
11
|
+
@click.option(
|
|
12
|
+
"--file", "-f", "config_files", multiple=True,
|
|
13
|
+
type=click.Path(exists=True, dir_okay=False, path_type=Path),
|
|
14
|
+
help="Compose configuration files",
|
|
15
|
+
)
|
|
16
|
+
@click.pass_context
|
|
17
|
+
def compose_command(ctx: click.Context, config_files: List[Path]) -> None:
|
|
18
|
+
"""model-compose (from Mindor)"""
|
|
19
|
+
ctx.ensure_object(dict)
|
|
20
|
+
ctx.obj["config_files"] = list(config_files)
|
|
21
|
+
|
|
22
|
+
@click.command(name="up")
|
|
23
|
+
@click.option("-d", "--detach", is_flag=True, help="Run in detached mode.")
|
|
24
|
+
@click.option(
|
|
25
|
+
"--env-file", "env_files", multiple=True,
|
|
26
|
+
type=click.Path(exists=True, dir_okay=False, path_type=Path),
|
|
27
|
+
required=False,
|
|
28
|
+
help="Path to a .env file containing environment variables.",
|
|
29
|
+
)
|
|
30
|
+
@click.option(
|
|
31
|
+
"--env", "-e", "env_data", multiple=True,
|
|
32
|
+
help="Environment variable in the form KEY=VALUE. Repeatable.",
|
|
33
|
+
)
|
|
34
|
+
@click.pass_context
|
|
35
|
+
def up_command(
|
|
36
|
+
ctx: click.Context,
|
|
37
|
+
detach: bool,
|
|
38
|
+
env_files: List[Path],
|
|
39
|
+
env_data: List[str]
|
|
40
|
+
) -> None:
|
|
41
|
+
config_files = ctx.obj.get("config_files", [])
|
|
42
|
+
async def _async_command():
|
|
43
|
+
try:
|
|
44
|
+
config = load_compose_config(".", config_files)
|
|
45
|
+
env = load_env_files(".", env_files or [])
|
|
46
|
+
await launch_services(config, detach, env)
|
|
47
|
+
except Exception as e:
|
|
48
|
+
click.echo(f"❌ {e}", err=True)
|
|
49
|
+
asyncio.run(_async_command())
|
|
50
|
+
|
|
51
|
+
@click.command(name="down")
|
|
52
|
+
@click.option(
|
|
53
|
+
"--env-file", "env_files", multiple=True,
|
|
54
|
+
type=click.Path(exists=True, dir_okay=False, path_type=Path),
|
|
55
|
+
required=False,
|
|
56
|
+
help="Path to a .env file containing environment variables.",
|
|
57
|
+
)
|
|
58
|
+
@click.option(
|
|
59
|
+
"--env", "-e", "env_data", multiple=True,
|
|
60
|
+
help="Environment variable in the form KEY=VALUE. Repeatable.",
|
|
61
|
+
)
|
|
62
|
+
@click.pass_context
|
|
63
|
+
def down_command(
|
|
64
|
+
ctx: click.Context,
|
|
65
|
+
env_files: List[Path],
|
|
66
|
+
env_data: List[str],
|
|
67
|
+
) -> None:
|
|
68
|
+
config_files = ctx.obj.get("config_files", [])
|
|
69
|
+
async def _async_command():
|
|
70
|
+
try:
|
|
71
|
+
config = load_compose_config(".", config_files)
|
|
72
|
+
env = load_env_files(".", env_files or [])
|
|
73
|
+
await shutdown_services(config, env)
|
|
74
|
+
except Exception as e:
|
|
75
|
+
click.echo(f"❌ {e}", err=True)
|
|
76
|
+
asyncio.run(_async_command())
|
|
77
|
+
|
|
78
|
+
@click.command(name="start")
|
|
79
|
+
@click.option(
|
|
80
|
+
"--env-file", "env_files", multiple=True,
|
|
81
|
+
type=click.Path(exists=True, dir_okay=False, path_type=Path),
|
|
82
|
+
required=False,
|
|
83
|
+
help="Path to a .env file containing environment variables.",
|
|
84
|
+
)
|
|
85
|
+
@click.option(
|
|
86
|
+
"--env", "-e", "env_data", multiple=True,
|
|
87
|
+
help="Environment variable in the form KEY=VALUE. Repeatable.",
|
|
88
|
+
)
|
|
89
|
+
@click.pass_context
|
|
90
|
+
def start_command(
|
|
91
|
+
ctx: click.Context,
|
|
92
|
+
env_files: List[Path],
|
|
93
|
+
env_data: List[str],
|
|
94
|
+
) -> None:
|
|
95
|
+
config_files = ctx.obj.get("config_files", [])
|
|
96
|
+
async def _async_command():
|
|
97
|
+
try:
|
|
98
|
+
config = load_compose_config(".", config_files)
|
|
99
|
+
env = load_env_files(".", env_files or [])
|
|
100
|
+
await start_services(config, env)
|
|
101
|
+
except Exception as e:
|
|
102
|
+
click.echo(f"❌ {e}", err=True)
|
|
103
|
+
asyncio.run(_async_command())
|
|
104
|
+
|
|
105
|
+
@click.command(name="stop")
|
|
106
|
+
@click.option(
|
|
107
|
+
"--env-file", "env_files", multiple=True,
|
|
108
|
+
type=click.Path(exists=True, dir_okay=False, path_type=Path),
|
|
109
|
+
required=False,
|
|
110
|
+
help="Path to a .env file containing environment variables.",
|
|
111
|
+
)
|
|
112
|
+
@click.option(
|
|
113
|
+
"--env", "-e", "env_data", multiple=True,
|
|
114
|
+
help="Environment variable in the form KEY=VALUE. Repeatable.",
|
|
115
|
+
)
|
|
116
|
+
@click.pass_context
|
|
117
|
+
def stop_command(
|
|
118
|
+
ctx: click.Context,
|
|
119
|
+
env_files: List[Path],
|
|
120
|
+
env_data: List[str],
|
|
121
|
+
) -> None:
|
|
122
|
+
config_files = ctx.obj.get("config_files", [])
|
|
123
|
+
async def _async_command():
|
|
124
|
+
try:
|
|
125
|
+
config = load_compose_config(".", config_files)
|
|
126
|
+
env = load_env_files(".", env_files or [])
|
|
127
|
+
await stop_services(config, env)
|
|
128
|
+
except Exception as e:
|
|
129
|
+
click.echo(f"❌ {e}", err=True)
|
|
130
|
+
asyncio.run(_async_command())
|
|
131
|
+
|
|
132
|
+
@click.command(name="run")
|
|
133
|
+
@click.argument("workflow", required=False)
|
|
134
|
+
@click.option(
|
|
135
|
+
"--input", "-i", "input_json",
|
|
136
|
+
type=str,
|
|
137
|
+
required=False,
|
|
138
|
+
help="JSON input string for the workflow",
|
|
139
|
+
)
|
|
140
|
+
@click.option(
|
|
141
|
+
"--env-file", "env_files", multiple=True,
|
|
142
|
+
type=click.Path(exists=True, dir_okay=False, path_type=Path),
|
|
143
|
+
required=False,
|
|
144
|
+
help="Path to a .env file containing environment variables.",
|
|
145
|
+
)
|
|
146
|
+
@click.option(
|
|
147
|
+
"--env", "-e", "env_data", multiple=True,
|
|
148
|
+
help="Environment variable in the form KEY=VALUE. Repeatable.",
|
|
149
|
+
)
|
|
150
|
+
@click.pass_context
|
|
151
|
+
def run_command(
|
|
152
|
+
ctx: click.Context,
|
|
153
|
+
workflow: Optional[str],
|
|
154
|
+
input_json: Optional[str],
|
|
155
|
+
env_files: List[Path],
|
|
156
|
+
env_data: List[str],
|
|
157
|
+
) -> None:
|
|
158
|
+
config_files = ctx.obj.get("config_files", [])
|
|
159
|
+
async def _async_command():
|
|
160
|
+
try:
|
|
161
|
+
config = load_compose_config(".", config_files)
|
|
162
|
+
env = load_env_files(".", env_files or [])
|
|
163
|
+
input = json.loads(input_json) if input_json else {}
|
|
164
|
+
state = await run_workflow(config, workflow, input, env)
|
|
165
|
+
click.echo(json.dumps(
|
|
166
|
+
state.error if state.error else state.output,
|
|
167
|
+
indent=2,
|
|
168
|
+
ensure_ascii=False
|
|
169
|
+
))
|
|
170
|
+
except json.JSONDecodeError:
|
|
171
|
+
click.echo("❌ Invalid JSON provided for --input", err=True)
|
|
172
|
+
except Exception as e:
|
|
173
|
+
click.echo(f"❌ {e}", err=True)
|
|
174
|
+
asyncio.run(_async_command())
|
|
175
|
+
|
|
176
|
+
compose_command.add_command(up_command)
|
|
177
|
+
compose_command.add_command(down_command)
|
|
178
|
+
compose_command.add_command(start_command)
|
|
179
|
+
compose_command.add_command(stop_command)
|
|
180
|
+
compose_command.add_command(run_command)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .component import *
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from typing import Type, Union, Literal, Optional, Dict, List, Tuple, Set, Annotated, Any
|
|
2
|
+
from mindor.dsl.schema.component import ComponentConfig
|
|
3
|
+
from .engine import BaseComponent, ComponentEngineMap
|
|
4
|
+
|
|
5
|
+
ComponentInstances: Dict[str, BaseComponent] = {}
|
|
6
|
+
|
|
7
|
+
class ComponentResolver:
|
|
8
|
+
def __init__(self, components: Dict[str, ComponentConfig]):
|
|
9
|
+
self.components: Dict[str, ComponentConfig] = components
|
|
10
|
+
|
|
11
|
+
def resolve(self, component_id: Optional[str]) -> Tuple[str, ComponentConfig]:
|
|
12
|
+
component_id = component_id or self._find_default_id(self.components)
|
|
13
|
+
|
|
14
|
+
if not component_id in self.components:
|
|
15
|
+
raise ValueError(f"Component not found: {component_id}")
|
|
16
|
+
|
|
17
|
+
return component_id, self.components[component_id]
|
|
18
|
+
|
|
19
|
+
def _find_default_id(self, components: Dict[str, ComponentConfig]) -> str:
|
|
20
|
+
default_ids = [ component_id for component_id, component in components.items() if component.default ]
|
|
21
|
+
|
|
22
|
+
if len(default_ids) > 1:
|
|
23
|
+
raise ValueError("Multiple components have default: true")
|
|
24
|
+
|
|
25
|
+
if not default_ids and "__default__" not in components:
|
|
26
|
+
raise ValueError("No default component defined.")
|
|
27
|
+
|
|
28
|
+
return default_ids[0] if default_ids else "__default__"
|
|
29
|
+
|
|
30
|
+
def create_component(id: str, config: ComponentConfig, env: Dict[str, str], daemon: bool) -> BaseComponent:
|
|
31
|
+
try:
|
|
32
|
+
component = ComponentInstances[id] if id in ComponentInstances else None
|
|
33
|
+
|
|
34
|
+
if not component:
|
|
35
|
+
component = ComponentEngineMap[config.type](id, config, env, daemon)
|
|
36
|
+
ComponentInstances[id] = component
|
|
37
|
+
|
|
38
|
+
return component
|
|
39
|
+
except KeyError:
|
|
40
|
+
raise ValueError(f"Unsupported component type: {config.type}")
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from typing import Type, Union, Literal, Optional, Dict, List, Tuple, Set, Annotated, Callable, Any
|
|
3
|
+
|
|
4
|
+
from mindor.dsl.schema.component import ComponentConfig, ComponentType
|
|
5
|
+
from mindor.dsl.schema.action import ActionConfig
|
|
6
|
+
from mindor.core.utils.workqueue import WorkQueue
|
|
7
|
+
from .context import ComponentContext
|
|
8
|
+
|
|
9
|
+
from threading import Thread
|
|
10
|
+
import asyncio
|
|
11
|
+
|
|
12
|
+
class ActionResolver:
|
|
13
|
+
def __init__(self, actions: Dict[str, ActionConfig]):
|
|
14
|
+
self.actions = actions
|
|
15
|
+
|
|
16
|
+
def resolve(self, action_id: Optional[str]) -> Tuple[str, ActionConfig]:
|
|
17
|
+
action_id = action_id or self._find_default_id(self.actions)
|
|
18
|
+
|
|
19
|
+
if not action_id in self.actions:
|
|
20
|
+
raise ValueError(f"Action not found: {action_id}")
|
|
21
|
+
|
|
22
|
+
return action_id, self.actions[action_id]
|
|
23
|
+
|
|
24
|
+
def _find_default_id(self, actions: Dict[str, ActionConfig]) -> str:
|
|
25
|
+
default_ids = [ action_id for action_id, action in actions.items() if action.default ]
|
|
26
|
+
|
|
27
|
+
if len(default_ids) > 1:
|
|
28
|
+
raise ValueError("Multiple actions have default: true")
|
|
29
|
+
|
|
30
|
+
if not default_ids and "__default__" not in actions:
|
|
31
|
+
raise ValueError("No default action defined.")
|
|
32
|
+
|
|
33
|
+
return default_ids[0] if default_ids else "__default__"
|
|
34
|
+
|
|
35
|
+
class BaseComponent(ABC):
|
|
36
|
+
def __init__(self, id: str, config: ComponentConfig, env: Dict[str, str], daemon: bool):
|
|
37
|
+
self.id: str = id
|
|
38
|
+
self.config: ComponentConfig = config
|
|
39
|
+
self.env: Dict[str, str] = env
|
|
40
|
+
self.daemon: bool = daemon
|
|
41
|
+
|
|
42
|
+
self.queue: Optional[WorkQueue] = None
|
|
43
|
+
self.thread: Optional[Thread] = None
|
|
44
|
+
self.thread_loop: Optional[asyncio.AbstractEventLoop] = None
|
|
45
|
+
self.daemon_task: Optional[asyncio.Task] = None
|
|
46
|
+
self.started: bool = False
|
|
47
|
+
|
|
48
|
+
if self.config.max_concurrent_count > 0:
|
|
49
|
+
self.queue = WorkQueue(self.config.max_concurrent_count, self._run)
|
|
50
|
+
|
|
51
|
+
async def start(self, background: bool = False) -> None:
|
|
52
|
+
if background:
|
|
53
|
+
def _start_in_thread():
|
|
54
|
+
self.thread_loop = asyncio.new_event_loop()
|
|
55
|
+
asyncio.set_event_loop(self.thread_loop)
|
|
56
|
+
self.thread_loop.run_until_complete(self._start())
|
|
57
|
+
|
|
58
|
+
self.thread = Thread(target=_start_in_thread)
|
|
59
|
+
self.thread.start()
|
|
60
|
+
else:
|
|
61
|
+
await self._start()
|
|
62
|
+
|
|
63
|
+
async def stop(self) -> None:
|
|
64
|
+
if self.thread:
|
|
65
|
+
future = asyncio.run_coroutine_threadsafe(self._stop(), self.thread_loop)
|
|
66
|
+
future.result()
|
|
67
|
+
self.thread_loop.close()
|
|
68
|
+
self.thread_loop = None
|
|
69
|
+
self.thread.join()
|
|
70
|
+
self.thread = None
|
|
71
|
+
else:
|
|
72
|
+
await self._stop()
|
|
73
|
+
|
|
74
|
+
async def wait_until_stopped(self) -> None:
|
|
75
|
+
if self.thread:
|
|
76
|
+
self.thread.join()
|
|
77
|
+
|
|
78
|
+
if self.daemon_task:
|
|
79
|
+
await self.daemon_task
|
|
80
|
+
|
|
81
|
+
async def run(self, action_id: Union[str, None], input: Dict[str, Any]) -> Dict[str, Any]:
|
|
82
|
+
_, action = ActionResolver(self.config.actions).resolve(action_id)
|
|
83
|
+
context = ComponentContext(input, self.env)
|
|
84
|
+
|
|
85
|
+
if self.queue:
|
|
86
|
+
return await (await self.queue.schedule(action, context))
|
|
87
|
+
|
|
88
|
+
return await self._run(action, context)
|
|
89
|
+
|
|
90
|
+
async def _start(self) -> None:
|
|
91
|
+
if self.queue:
|
|
92
|
+
await self.queue.start()
|
|
93
|
+
|
|
94
|
+
self.started = True
|
|
95
|
+
|
|
96
|
+
if self.daemon:
|
|
97
|
+
if not self.thread:
|
|
98
|
+
self.daemon_task = asyncio.create_task(self._serve())
|
|
99
|
+
else:
|
|
100
|
+
await self._serve()
|
|
101
|
+
|
|
102
|
+
async def _stop(self) -> None:
|
|
103
|
+
if self.queue:
|
|
104
|
+
await self.queue.stop()
|
|
105
|
+
|
|
106
|
+
if self.daemon:
|
|
107
|
+
await self._shutdown()
|
|
108
|
+
|
|
109
|
+
self.started = False
|
|
110
|
+
|
|
111
|
+
@abstractmethod
|
|
112
|
+
async def _serve(self) -> None:
|
|
113
|
+
pass
|
|
114
|
+
|
|
115
|
+
@abstractmethod
|
|
116
|
+
async def _shutdown(self) -> None:
|
|
117
|
+
pass
|
|
118
|
+
|
|
119
|
+
@abstractmethod
|
|
120
|
+
async def _run(self, action: ActionConfig, context: ComponentContext) -> Dict[str, Any]:
|
|
121
|
+
pass
|
|
122
|
+
|
|
123
|
+
ComponentEngineMap: Dict[ComponentType, Type[BaseComponent]] = {}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from typing import Type, Union, Literal, Optional, Dict, List, Tuple, Set, Annotated, Any
|
|
2
|
+
from mindor.core.utils.template import TemplateRenderer
|
|
3
|
+
|
|
4
|
+
class ComponentContext:
|
|
5
|
+
def __init__(self, input: Dict[str, Any], env: Dict[str, str]):
|
|
6
|
+
self.input: Dict[str, Any] = input
|
|
7
|
+
self.env: Dict[str, str] = env
|
|
8
|
+
self.sources: Dict[str, Any] = {}
|
|
9
|
+
self.renderer: TemplateRenderer = TemplateRenderer(self._resolve_source)
|
|
10
|
+
|
|
11
|
+
def register_source(self, key: str, source: Any) -> None:
|
|
12
|
+
self.sources[key] = source
|
|
13
|
+
|
|
14
|
+
def render_template(self, data: Dict[str, Any]) -> Any:
|
|
15
|
+
return self.renderer.render(data)
|
|
16
|
+
|
|
17
|
+
def _resolve_source(self, key: str) -> Any:
|
|
18
|
+
if key in self.sources:
|
|
19
|
+
return self.sources[key]
|
|
20
|
+
if key == "input":
|
|
21
|
+
return self.input
|
|
22
|
+
if key == "env":
|
|
23
|
+
return self.env
|
|
24
|
+
raise KeyError(f"Unknown source: {key}")
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
from typing import Type, Union, Literal, Optional, Dict, List, Tuple, Set, Annotated, Any
|
|
2
|
+
from mindor.dsl.schema.component import HttpClientComponentConfig, HttpClientActionConfig, HttpUrl
|
|
3
|
+
from .base import BaseComponent, ComponentType, ComponentEngineMap, ActionConfig
|
|
4
|
+
from .context import ComponentContext
|
|
5
|
+
|
|
6
|
+
from requests.structures import CaseInsensitiveDict
|
|
7
|
+
from urllib.parse import urlencode, urljoin
|
|
8
|
+
import aiohttp, json
|
|
9
|
+
|
|
10
|
+
class HttpClientAction:
|
|
11
|
+
def __init__(self, base_url: Union[str, None], config: HttpClientActionConfig):
|
|
12
|
+
self.base_url: Union[HttpUrl, None] = base_url
|
|
13
|
+
self.config: HttpClientActionConfig = config
|
|
14
|
+
|
|
15
|
+
async def run(self, context: ComponentContext) -> Dict[str, Any]:
|
|
16
|
+
url = self._resolve_request_url(context)
|
|
17
|
+
method = context.render_template(self.config.method)
|
|
18
|
+
params = context.render_template(self.config.params)
|
|
19
|
+
body = context.render_template(self.config.body)
|
|
20
|
+
headers = context.render_template(self.config.headers)
|
|
21
|
+
|
|
22
|
+
response = await self._request(url, method, params, body, headers)
|
|
23
|
+
|
|
24
|
+
if not self.config.output:
|
|
25
|
+
return response or {}
|
|
26
|
+
|
|
27
|
+
if response:
|
|
28
|
+
context.register_source("response", response)
|
|
29
|
+
|
|
30
|
+
return context.render_template(self.config.output)
|
|
31
|
+
|
|
32
|
+
async def _request(self, url: str, method: str, params: Optional[Dict[str, Any]], body: Optional[Dict[str, Any]], headers: Optional[Dict[str, str]]) -> Union[dict, str, bytes]:
|
|
33
|
+
async with aiohttp.ClientSession() as session:
|
|
34
|
+
async with session.request(
|
|
35
|
+
method,
|
|
36
|
+
url,
|
|
37
|
+
params=params,
|
|
38
|
+
data=self._serialize_request_body(body, headers) if body is not None else None,
|
|
39
|
+
headers=headers
|
|
40
|
+
) as response:
|
|
41
|
+
if response.status >= 400:
|
|
42
|
+
raise ValueError(f"Request failed with status {response.status}")
|
|
43
|
+
|
|
44
|
+
return await self._deserialize_response_body(response)
|
|
45
|
+
|
|
46
|
+
def _resolve_request_url(self, context: ComponentContext) -> str:
|
|
47
|
+
if self.base_url and self.config.path:
|
|
48
|
+
return context.render_template(str(self.base_url)) + context.render_template(self.config.path)
|
|
49
|
+
|
|
50
|
+
return context.render_template(str(self.config.endpoint))
|
|
51
|
+
|
|
52
|
+
def _serialize_request_body(self, body: Dict[str, Any], headers: Optional[Dict[str, str]]) -> str:
|
|
53
|
+
content_type = CaseInsensitiveDict(headers or {}).get("Content-Type", "").lower()
|
|
54
|
+
|
|
55
|
+
if content_type == "application/json":
|
|
56
|
+
return json.dumps(body)
|
|
57
|
+
|
|
58
|
+
if content_type == "application/x-www-form-urlencoded":
|
|
59
|
+
return urlencode(body)
|
|
60
|
+
|
|
61
|
+
return str(body)
|
|
62
|
+
|
|
63
|
+
async def _deserialize_response_body(self, response: aiohttp.ClientResponse) -> Union[dict, str, bytes]:
|
|
64
|
+
content_type = response.headers.get("Content-Type", "").lower()
|
|
65
|
+
|
|
66
|
+
if content_type == "application/json":
|
|
67
|
+
return await response.json()
|
|
68
|
+
|
|
69
|
+
if content_type.startswith("text/"):
|
|
70
|
+
return await response.text()
|
|
71
|
+
|
|
72
|
+
return await response.read()
|
|
73
|
+
|
|
74
|
+
class HttpClientComponent(BaseComponent):
|
|
75
|
+
def __init__(self, id: str, config: HttpClientComponentConfig, env: Dict[str, str], daemon: bool):
|
|
76
|
+
super().__init__(id, config, env, daemon)
|
|
77
|
+
|
|
78
|
+
async def _serve(self) -> None:
|
|
79
|
+
pass
|
|
80
|
+
|
|
81
|
+
async def _shutdown(self) -> None:
|
|
82
|
+
pass
|
|
83
|
+
|
|
84
|
+
async def _run(self, action: ActionConfig, context: ComponentContext) -> Dict[str, Any]:
|
|
85
|
+
return await HttpClientAction(self.config.base_url, action).run(context)
|
|
86
|
+
|
|
87
|
+
ComponentEngineMap[ComponentType.HTTP_CLIENT] = HttpClientComponent
|
|
@@ -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 HttpServerComponentConfig
|
|
3
|
+
from .base import BaseComponent, ComponentType, ComponentEngineMap, ActionConfig
|
|
4
|
+
from .context import ComponentContext
|
|
5
|
+
|
|
6
|
+
class HttpServerComponent(BaseComponent):
|
|
7
|
+
def __init__(self, id: str, config: HttpServerComponentConfig, 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) -> Dict[str, Any]:
|
|
17
|
+
return {}
|
|
18
|
+
|
|
19
|
+
ComponentEngineMap[ComponentType.HTTP_SERVER] = HttpServerComponent
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .compose import *
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from typing import Type, Union, Literal, Optional, Dict, List, Tuple, Set, Annotated, Any
|
|
2
|
+
from mindor.dsl.schema.compose import ComposeConfig
|
|
3
|
+
from .manager import ComposeManager, TaskState
|
|
4
|
+
|
|
5
|
+
async def launch_services(config: ComposeConfig, detach: bool, env: Dict[str, str]):
|
|
6
|
+
await ComposeManager(config, env, daemon=True).launch_services(detach=detach)
|
|
7
|
+
|
|
8
|
+
async def shutdown_services(config: ComposeConfig, env: Dict[str, str]):
|
|
9
|
+
await ComposeManager(config, env, daemon=False).shutdown_services()
|
|
10
|
+
|
|
11
|
+
async def start_services(config: ComposeConfig, detach: bool, env: Dict[str, str]):
|
|
12
|
+
await ComposeManager(config, env, daemon=False).start_services()
|
|
13
|
+
|
|
14
|
+
async def stop_services(config: ComposeConfig, env: Dict[str, str]):
|
|
15
|
+
await ComposeManager(config, env, daemon=False).stop_services()
|
|
16
|
+
|
|
17
|
+
async def run_workflow(config: ComposeConfig, workflow_id: Optional[str], input: Dict[str, Any], env: Dict[str, str]) -> TaskState:
|
|
18
|
+
return await ComposeManager(config, env, daemon=False).run_workflow(workflow_id, input)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from typing import Type, Union, Literal, Optional, Dict, List, Tuple, Set, Annotated, Any
|
|
2
|
+
from mindor.dsl.schema.compose import ComposeConfig
|
|
3
|
+
from mindor.core.controller import BaseController, TaskState, create_controller
|
|
4
|
+
|
|
5
|
+
class ComposeManager:
|
|
6
|
+
def __init__(self, config: ComposeConfig, env: Dict[str, str], daemon: bool = True):
|
|
7
|
+
self.config: ComposeConfig = config
|
|
8
|
+
self.controller: BaseController = create_controller(self.config.controller, self.config.components, self.config.workflows, env, daemon)
|
|
9
|
+
|
|
10
|
+
async def launch_services(self, detach: bool, background: bool = False):
|
|
11
|
+
await self.controller.start(background=background)
|
|
12
|
+
await self.controller.wait_until_stopped()
|
|
13
|
+
|
|
14
|
+
async def shutdown_services(self):
|
|
15
|
+
await self.controller.stop()
|
|
16
|
+
|
|
17
|
+
async def start_services(self, background: bool = False):
|
|
18
|
+
await self.controller.start(background=background)
|
|
19
|
+
await self.controller.wait_until_stopped()
|
|
20
|
+
|
|
21
|
+
async def stop_services(self):
|
|
22
|
+
await self.controller.stop()
|
|
23
|
+
|
|
24
|
+
async def run_workflow(self, workflow_id: Optional[str], input: Dict[str, Any]) -> TaskState:
|
|
25
|
+
if not self.controller.started:
|
|
26
|
+
await self.controller.start()
|
|
27
|
+
|
|
28
|
+
return await self.controller.run_workflow(workflow_id, input)
|
|
@@ -0,0 +1,11 @@
|
|
|
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.dsl.schema.component import ComponentConfig
|
|
4
|
+
from mindor.dsl.schema.workflow import WorkflowConfig
|
|
5
|
+
from .engine import BaseController, ControllerEngineMap
|
|
6
|
+
|
|
7
|
+
def create_controller(config: ControllerConfig, components: Dict[str, ComponentConfig], workflows: Dict[str, WorkflowConfig], env: Dict[str, str], daemon: bool) -> BaseController:
|
|
8
|
+
try:
|
|
9
|
+
return ControllerEngineMap[config.type](config, components, workflows, env, daemon)
|
|
10
|
+
except KeyError:
|
|
11
|
+
raise ValueError(f"Unsupported controller type: {config.type}")
|