model-compose 0.0.7__tar.gz → 0.0.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.
- {model_compose-0.0.7/src/model_compose.egg-info → model_compose-0.0.8}/PKG-INFO +1 -1
- {model_compose-0.0.7 → model_compose-0.0.8}/pyproject.toml +1 -1
- {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/core/component/engine/__init__.py +0 -1
- {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/core/component/engine/base.py +7 -61
- {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/core/component/engine/http_client.py +22 -4
- {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/core/compose/manager.py +1 -1
- {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/core/controller/controller.py +3 -2
- {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/core/controller/engine/base.py +24 -57
- {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/core/controller/engine/http_server.py +3 -2
- {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/core/controller/engine/mcp_server.py +3 -2
- model_compose-0.0.8/src/mindor/core/listener/__init__.py +2 -0
- model_compose-0.0.8/src/mindor/core/listener/engine/base.py +29 -0
- model_compose-0.0.8/src/mindor/core/listener/engine/http_callback.py +121 -0
- {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/core/listener/listener.py +2 -2
- model_compose-0.0.8/src/mindor/core/services/__init__.py +2 -0
- model_compose-0.0.8/src/mindor/core/services/async_service.py +66 -0
- {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/core/utils/template.py +12 -9
- {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/core/workflow/workflow.py +8 -6
- {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/dsl/schema/action/impl/common.py +1 -1
- {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/dsl/schema/action/impl/http_client.py +2 -2
- {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/dsl/schema/component/impl/http_client.py +1 -1
- {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/dsl/schema/compose.py +12 -2
- {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/dsl/schema/listener/impl/common.py +1 -0
- {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/dsl/schema/listener/impl/http_callback.py +4 -2
- {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/dsl/schema/workflow.py +4 -4
- {model_compose-0.0.7 → model_compose-0.0.8/src/model_compose.egg-info}/PKG-INFO +1 -1
- {model_compose-0.0.7 → model_compose-0.0.8}/src/model_compose.egg-info/SOURCES.txt +2 -0
- model_compose-0.0.7/src/mindor/core/listener/engine/base.py +0 -13
- model_compose-0.0.7/src/mindor/core/listener/engine/http_callback.py +0 -37
- model_compose-0.0.7/src/mindor/dsl/schema/listener/__init__.py +0 -1
- {model_compose-0.0.7 → model_compose-0.0.8}/LICENSE +0 -0
- {model_compose-0.0.7 → model_compose-0.0.8}/README.md +0 -0
- {model_compose-0.0.7 → model_compose-0.0.8}/setup.cfg +0 -0
- {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/__init__.py +0 -0
- {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/cli/__init__.py +0 -0
- {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/cli/compose.py +0 -0
- {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/core/__init__.py +0 -0
- {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/core/component/__init__.py +0 -0
- {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/core/component/component.py +0 -0
- {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/core/component/engine/context.py +0 -0
- {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/core/component/engine/http_server.py +0 -0
- {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/core/compose/__init__.py +0 -0
- {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/core/compose/compose.py +0 -0
- {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/core/controller/__init__.py +0 -0
- {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/core/controller/engine/__init__.py +0 -0
- {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/core/listener/engine/__init__.py +0 -0
- {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/core/runtime/__init__.py +0 -0
- {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/core/runtime/env.py +0 -0
- {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/core/utils/__init__.py +0 -0
- {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/core/utils/expiring.py +0 -0
- {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/core/utils/http_headers.py +0 -0
- {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/core/utils/resources.py +0 -0
- {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/core/utils/workqueue.py +0 -0
- {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/core/workflow/__init__.py +0 -0
- {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/core/workflow/context.py +0 -0
- {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/core/workflow/job.py +0 -0
- {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/dsl/__init__.py +0 -0
- {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/dsl/loader.py +0 -0
- {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/dsl/schema/__init__.py +0 -0
- {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/dsl/schema/action/__init__.py +0 -0
- {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/dsl/schema/action/action.py +0 -0
- {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/dsl/schema/action/impl/__init__.py +0 -0
- {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/dsl/schema/action/impl/http_server.py +0 -0
- {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/dsl/schema/action/impl/mcp_client.py +0 -0
- {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/dsl/schema/action/impl/mcp_server.py +0 -0
- {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/dsl/schema/component/__init__.py +0 -0
- {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/dsl/schema/component/component.py +0 -0
- {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/dsl/schema/component/impl/__init__.py +0 -0
- {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/dsl/schema/component/impl/common.py +0 -0
- {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/dsl/schema/component/impl/http_server.py +0 -0
- {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/dsl/schema/component/impl/mcp_client.py +0 -0
- {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/dsl/schema/component/impl/mcp_server.py +0 -0
- {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/dsl/schema/component/impl/types.py +0 -0
- {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/dsl/schema/controller/__init__.py +0 -0
- {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/dsl/schema/controller/controller.py +0 -0
- {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/dsl/schema/controller/impl/__init__.py +0 -0
- {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/dsl/schema/controller/impl/common.py +0 -0
- {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/dsl/schema/controller/impl/http_server.py +0 -0
- {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/dsl/schema/controller/impl/mcp_server.py +0 -0
- {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/dsl/schema/controller/impl/types.py +0 -0
- {model_compose-0.0.7/src/mindor/core → model_compose-0.0.8/src/mindor/dsl/schema}/listener/__init__.py +0 -0
- {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/dsl/schema/listener/impl/__init__.py +0 -0
- {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/dsl/schema/listener/impl/types.py +0 -0
- {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/dsl/schema/listener/listener.py +0 -0
- {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/dsl/schema/runtime/__init__.py +0 -0
- {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/dsl/schema/runtime/impl/__init__.py +0 -0
- {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/dsl/schema/runtime/impl/common.py +0 -0
- {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/dsl/schema/runtime/impl/docker.py +0 -0
- {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/dsl/schema/runtime/impl/types.py +0 -0
- {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/dsl/schema/runtime/runtime.py +0 -0
- {model_compose-0.0.7 → model_compose-0.0.8}/src/model_compose.egg-info/dependency_links.txt +0 -0
- {model_compose-0.0.7 → model_compose-0.0.8}/src/model_compose.egg-info/entry_points.txt +0 -0
- {model_compose-0.0.7 → model_compose-0.0.8}/src/model_compose.egg-info/requires.txt +0 -0
- {model_compose-0.0.7 → model_compose-0.0.8}/src/model_compose.egg-info/top_level.txt +0 -0
- {model_compose-0.0.7 → model_compose-0.0.8}/tests/test_cli.py +0 -0
|
@@ -3,12 +3,10 @@ from abc import ABC, abstractmethod
|
|
|
3
3
|
|
|
4
4
|
from mindor.dsl.schema.component import ComponentConfig, ComponentType
|
|
5
5
|
from mindor.dsl.schema.action import ActionConfig
|
|
6
|
+
from mindor.core.services import AsyncService
|
|
6
7
|
from mindor.core.utils.workqueue import WorkQueue
|
|
7
8
|
from .context import ComponentContext
|
|
8
9
|
|
|
9
|
-
from threading import Thread
|
|
10
|
-
import asyncio
|
|
11
|
-
|
|
12
10
|
class ActionResolver:
|
|
13
11
|
def __init__(self, actions: Dict[str, ActionConfig]):
|
|
14
12
|
self.actions = actions
|
|
@@ -32,52 +30,17 @@ class ActionResolver:
|
|
|
32
30
|
|
|
33
31
|
return default_ids[0] if default_ids else "__default__"
|
|
34
32
|
|
|
35
|
-
class BaseComponent(
|
|
33
|
+
class BaseComponent(AsyncService):
|
|
36
34
|
def __init__(self, id: str, config: ComponentConfig, env: Dict[str, str], daemon: bool):
|
|
35
|
+
super().__init__(daemon)
|
|
36
|
+
|
|
37
37
|
self.id: str = id
|
|
38
38
|
self.config: ComponentConfig = config
|
|
39
39
|
self.env: Dict[str, str] = env
|
|
40
|
-
|
|
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
|
-
|
|
40
|
+
|
|
48
41
|
if self.config.max_concurrent_count > 0:
|
|
49
42
|
self.queue = WorkQueue(self.config.max_concurrent_count, self._run)
|
|
50
43
|
|
|
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
44
|
async def run(self, action_id: Union[str, None], call_id: str, input: Dict[str, Any]) -> Dict[str, Any]:
|
|
82
45
|
_, action = ActionResolver(self.config.actions).resolve(action_id)
|
|
83
46
|
context = ComponentContext(call_id, input, self.env)
|
|
@@ -91,30 +54,13 @@ class BaseComponent(ABC):
|
|
|
91
54
|
if self.queue:
|
|
92
55
|
await self.queue.start()
|
|
93
56
|
|
|
94
|
-
|
|
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()
|
|
57
|
+
await super()._start()
|
|
101
58
|
|
|
102
59
|
async def _stop(self) -> None:
|
|
103
60
|
if self.queue:
|
|
104
61
|
await self.queue.stop()
|
|
105
62
|
|
|
106
|
-
|
|
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
|
|
63
|
+
await super()._stop()
|
|
118
64
|
|
|
119
65
|
@abstractmethod
|
|
120
66
|
async def _run(self, action: ActionConfig, context: ComponentContext) -> Any:
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
from typing import Type, Union, Literal, Optional, Dict, List, Tuple, Set, Annotated, AsyncIterator, Any
|
|
2
|
-
from mindor.dsl.schema.component import HttpClientComponentConfig
|
|
2
|
+
from mindor.dsl.schema.component import HttpClientComponentConfig
|
|
3
|
+
from mindor.dsl.schema.action import ActionConfig, HttpClientActionConfig, HttpClientCompletionConfig
|
|
4
|
+
from mindor.core.listener import HttpCallbackListener
|
|
3
5
|
from mindor.core.utils.http_headers import parse_options_header, get_header_value
|
|
4
6
|
from mindor.core.utils.resources import StreamResource
|
|
5
|
-
from .base import BaseComponent, ComponentType, ComponentEngineMap
|
|
7
|
+
from .base import BaseComponent, ComponentType, ComponentEngineMap
|
|
6
8
|
from .context import ComponentContext
|
|
7
9
|
|
|
8
10
|
from urllib.parse import urlencode
|
|
9
|
-
import aiohttp, json
|
|
11
|
+
import asyncio, aiohttp, json
|
|
10
12
|
|
|
11
13
|
class HttpStreamResource(StreamResource):
|
|
12
14
|
def __init__(self, session: aiohttp.ClientSession, stream: aiohttp.StreamReader, content_type: Optional[str], filename: Optional[str]):
|
|
@@ -35,6 +37,7 @@ class HttpClientAction:
|
|
|
35
37
|
def __init__(self, base_url: Optional[str], config: HttpClientActionConfig):
|
|
36
38
|
self.base_url: Optional[str] = base_url
|
|
37
39
|
self.config: HttpClientActionConfig = config
|
|
40
|
+
self.future: Optional[asyncio.Future] = None
|
|
38
41
|
|
|
39
42
|
async def run(self, context: ComponentContext) -> Any:
|
|
40
43
|
url = self._resolve_request_url(context)
|
|
@@ -48,7 +51,10 @@ class HttpClientAction:
|
|
|
48
51
|
if response:
|
|
49
52
|
context.register_source("response", response)
|
|
50
53
|
|
|
51
|
-
|
|
54
|
+
if self.config.completion:
|
|
55
|
+
result = await self._handle_completion(self.config.completion, context)
|
|
56
|
+
|
|
57
|
+
return context.render_template(self.config.output) if self.config.output else (result or response)
|
|
52
58
|
|
|
53
59
|
async def _request(self, url: str, method: str, params: Optional[Dict[str, Any]], body: Optional[Any], headers: Optional[Dict[str, str]]) -> Any:
|
|
54
60
|
session = aiohttp.ClientSession()
|
|
@@ -107,6 +113,18 @@ class HttpClientAction:
|
|
|
107
113
|
|
|
108
114
|
return HttpStreamResource(session, response.content, content_type, filename)
|
|
109
115
|
|
|
116
|
+
async def _handle_completion(self, completion: HttpClientCompletionConfig, context: ComponentContext) -> Any:
|
|
117
|
+
if completion.type == "callback":
|
|
118
|
+
future: asyncio.Future = asyncio.get_event_loop().create_future()
|
|
119
|
+
HttpCallbackListener.register_pending_future(context.render_template(completion.wait_for), future)
|
|
120
|
+
result = await future
|
|
121
|
+
if result:
|
|
122
|
+
context.register_source("result", result)
|
|
123
|
+
return result
|
|
124
|
+
|
|
125
|
+
if completion.type == "polling":
|
|
126
|
+
return None
|
|
127
|
+
|
|
110
128
|
class HttpClientComponent(BaseComponent):
|
|
111
129
|
def __init__(self, id: str, config: HttpClientComponentConfig, env: Dict[str, str], daemon: bool):
|
|
112
130
|
super().__init__(id, config, env, daemon)
|
|
@@ -5,7 +5,7 @@ from mindor.core.controller import BaseController, TaskState, create_controller
|
|
|
5
5
|
class ComposeManager:
|
|
6
6
|
def __init__(self, config: ComposeConfig, env: Dict[str, str], daemon: bool = True):
|
|
7
7
|
self.config: ComposeConfig = config
|
|
8
|
-
self.controller: BaseController = create_controller(self.config.controller, self.config.components, self.config.workflows, env, daemon)
|
|
8
|
+
self.controller: BaseController = create_controller(self.config.controller, self.config.components, self.config.listeners, self.config.workflows, env, daemon)
|
|
9
9
|
|
|
10
10
|
async def launch_services(self, detach: bool, background: bool = False):
|
|
11
11
|
await self.controller.start(background=background)
|
|
@@ -1,11 +1,12 @@
|
|
|
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
3
|
from mindor.dsl.schema.component import ComponentConfig
|
|
4
|
+
from mindor.dsl.schema.listener import ListenerConfig
|
|
4
5
|
from mindor.dsl.schema.workflow import WorkflowConfig
|
|
5
6
|
from .engine import BaseController, ControllerEngineMap
|
|
6
7
|
|
|
7
|
-
def create_controller(config: ControllerConfig, components: Dict[str, ComponentConfig], workflows: Dict[str, WorkflowConfig], env: Dict[str, str], daemon: bool) -> BaseController:
|
|
8
|
+
def create_controller(config: ControllerConfig, components: Dict[str, ComponentConfig], listeners: List[ListenerConfig], workflows: Dict[str, WorkflowConfig], env: Dict[str, str], daemon: bool) -> BaseController:
|
|
8
9
|
try:
|
|
9
|
-
return ControllerEngineMap[config.type](config, components, workflows, env, daemon)
|
|
10
|
+
return ControllerEngineMap[config.type](config, components, listeners, workflows, env, daemon)
|
|
10
11
|
except KeyError:
|
|
11
12
|
raise ValueError(f"Unsupported controller type: {config.type}")
|
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
from typing import Type, Union, Literal, Optional, Dict, List, Tuple, Set, Annotated, Any
|
|
2
|
-
from abc import ABC, abstractmethod
|
|
3
2
|
from dataclasses import dataclass
|
|
4
3
|
|
|
5
4
|
from mindor.dsl.schema.controller import ControllerConfig, ControllerType
|
|
6
5
|
from mindor.dsl.schema.component import ComponentConfig
|
|
6
|
+
from mindor.dsl.schema.listener import ListenerConfig
|
|
7
7
|
from mindor.dsl.schema.workflow import WorkflowConfig
|
|
8
|
+
from mindor.core.services import AsyncService
|
|
8
9
|
from mindor.core.component import BaseComponent, create_component
|
|
10
|
+
from mindor.core.listener import BaseListener, create_listener
|
|
9
11
|
from mindor.core.workflow import Workflow, WorkflowResolver, create_workflow
|
|
10
12
|
from mindor.core.utils.workqueue import WorkQueue
|
|
11
13
|
from mindor.core.utils.expiring import ExpiringDict
|
|
12
14
|
|
|
13
|
-
from threading import
|
|
15
|
+
from threading import Lock
|
|
14
16
|
import asyncio, ulid
|
|
15
17
|
|
|
16
18
|
@dataclass
|
|
@@ -20,54 +22,22 @@ class TaskState:
|
|
|
20
22
|
output: Optional[Any] = None
|
|
21
23
|
error: Optional[Any] = None
|
|
22
24
|
|
|
23
|
-
class BaseController(
|
|
24
|
-
def __init__(self, config: ControllerConfig, components: Dict[str, ComponentConfig], workflows: Dict[str, WorkflowConfig], env: Dict[str, str], daemon: bool):
|
|
25
|
+
class BaseController(AsyncService):
|
|
26
|
+
def __init__(self, config: ControllerConfig, components: Dict[str, ComponentConfig], listeners: List[ListenerConfig], workflows: Dict[str, WorkflowConfig], env: Dict[str, str], daemon: bool):
|
|
27
|
+
super().__init__(daemon)
|
|
28
|
+
|
|
25
29
|
self.config: ControllerConfig = config
|
|
26
30
|
self.components: Dict[str, ComponentConfig] = components
|
|
31
|
+
self.listeners: List[ListenerConfig] = listeners
|
|
27
32
|
self.workflows: Dict[str, WorkflowConfig] = workflows
|
|
28
33
|
self.env: Dict[str, str] = env
|
|
29
|
-
self.daemon: bool = daemon
|
|
30
34
|
self.queue: Optional[WorkQueue] = None
|
|
31
|
-
self.thread: Optional[Thread] = None
|
|
32
|
-
self.thread_loop: Optional[asyncio.AbstractEventLoop] = None
|
|
33
|
-
self.daemon_task: Optional[asyncio.Task] = None
|
|
34
35
|
self.task_states: ExpiringDict[TaskState] = ExpiringDict()
|
|
35
36
|
self.task_states_lock: Lock = Lock()
|
|
36
|
-
|
|
37
|
-
|
|
37
|
+
|
|
38
38
|
if self.config.max_concurrent_count > 0:
|
|
39
39
|
self.queue = WorkQueue(self.config.max_concurrent_count, self._run_workflow)
|
|
40
40
|
|
|
41
|
-
async def start(self, background: bool = False) -> None:
|
|
42
|
-
if background:
|
|
43
|
-
def _start_in_thread():
|
|
44
|
-
self.thread_loop = asyncio.new_event_loop()
|
|
45
|
-
asyncio.set_event_loop(self.thread_loop)
|
|
46
|
-
self.thread_loop.run_until_complete(self._start())
|
|
47
|
-
|
|
48
|
-
self.thread = Thread(target=_start_in_thread)
|
|
49
|
-
self.thread.start()
|
|
50
|
-
else:
|
|
51
|
-
await self._start()
|
|
52
|
-
|
|
53
|
-
async def stop(self) -> None:
|
|
54
|
-
if self.thread:
|
|
55
|
-
future = asyncio.run_coroutine_threadsafe(self._stop(), self.thread_loop)
|
|
56
|
-
future.result()
|
|
57
|
-
self.thread_loop.close()
|
|
58
|
-
self.thread_loop = None
|
|
59
|
-
self.thread.join()
|
|
60
|
-
self.thread = None
|
|
61
|
-
else:
|
|
62
|
-
await self._stop()
|
|
63
|
-
|
|
64
|
-
async def wait_until_stopped(self) -> None:
|
|
65
|
-
if self.thread:
|
|
66
|
-
self.thread.join()
|
|
67
|
-
|
|
68
|
-
if self.daemon_task:
|
|
69
|
-
await self.daemon_task
|
|
70
|
-
|
|
71
41
|
async def run_workflow(self, workflow_id: Optional[str], input: Dict[str, Any], wait_for_completion: bool = True) -> TaskState:
|
|
72
42
|
task_id = ulid.ulid()
|
|
73
43
|
state = TaskState(task_id=task_id, status="pending")
|
|
@@ -92,25 +62,21 @@ class BaseController(ABC):
|
|
|
92
62
|
if self.queue:
|
|
93
63
|
await self.queue.start()
|
|
94
64
|
|
|
95
|
-
self.started = True
|
|
96
|
-
|
|
97
65
|
if self.daemon:
|
|
98
66
|
await self._start_components()
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
else:
|
|
103
|
-
await self._serve()
|
|
67
|
+
await self._start_listeners()
|
|
68
|
+
|
|
69
|
+
await super()._start()
|
|
104
70
|
|
|
105
71
|
async def _stop(self) -> None:
|
|
106
72
|
if self.queue:
|
|
107
73
|
await self.queue.stop()
|
|
108
74
|
|
|
109
75
|
if self.daemon:
|
|
76
|
+
await self._stop_listeners()
|
|
110
77
|
await self._stop_components()
|
|
111
|
-
await self._shutdown()
|
|
112
78
|
|
|
113
|
-
|
|
79
|
+
await super()._stop()
|
|
114
80
|
|
|
115
81
|
async def _start_components(self) -> None:
|
|
116
82
|
await asyncio.gather(*[ component.start() for component in self._create_components() ])
|
|
@@ -118,6 +84,12 @@ class BaseController(ABC):
|
|
|
118
84
|
async def _stop_components(self) -> None:
|
|
119
85
|
await asyncio.gather(*[ component.stop() for component in self._create_components() ])
|
|
120
86
|
|
|
87
|
+
async def _start_listeners(self) -> None:
|
|
88
|
+
await asyncio.gather(*[ listener.start() for listener in self._create_listeners() ])
|
|
89
|
+
|
|
90
|
+
async def _stop_listeners(self) -> None:
|
|
91
|
+
await asyncio.gather(*[ listener.stop() for listener in self._create_listeners() ])
|
|
92
|
+
|
|
121
93
|
async def _run_workflow(self, task_id: str, workflow_id: Optional[str], input: Dict[str, Any]) -> TaskState:
|
|
122
94
|
state = TaskState(task_id=task_id, status="processing")
|
|
123
95
|
with self.task_states_lock:
|
|
@@ -138,15 +110,10 @@ class BaseController(ABC):
|
|
|
138
110
|
def _create_components(self) -> List[BaseComponent]:
|
|
139
111
|
return [ create_component(component_id, config, self.env, self.daemon) for component_id, config in self.components.items() ]
|
|
140
112
|
|
|
113
|
+
def _create_listeners(self) -> List[BaseListener]:
|
|
114
|
+
return [ create_listener(config, self.env, self.daemon) for config in self.listeners ]
|
|
115
|
+
|
|
141
116
|
def _create_workflow(self, workflow_id: Optional[str]) -> Workflow:
|
|
142
117
|
return create_workflow(*WorkflowResolver(self.workflows).resolve(workflow_id), self.components, self.env)
|
|
143
118
|
|
|
144
|
-
@abstractmethod
|
|
145
|
-
async def _serve(self) -> None:
|
|
146
|
-
pass
|
|
147
|
-
|
|
148
|
-
@abstractmethod
|
|
149
|
-
async def _shutdown(self) -> None:
|
|
150
|
-
pass
|
|
151
|
-
|
|
152
119
|
ControllerEngineMap: Dict[ControllerType, Type[BaseController]] = {}
|
{model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/core/controller/engine/http_server.py
RENAMED
|
@@ -3,6 +3,7 @@ from typing_extensions import Self
|
|
|
3
3
|
from pydantic import BaseModel
|
|
4
4
|
|
|
5
5
|
from mindor.dsl.schema.controller import HttpServerControllerConfig
|
|
6
|
+
from mindor.dsl.schema.listener import ListenerConfig
|
|
6
7
|
from mindor.dsl.schema.component import ComponentConfig
|
|
7
8
|
from mindor.dsl.schema.workflow import WorkflowConfig
|
|
8
9
|
from mindor.core.utils.resources import StreamResource
|
|
@@ -35,8 +36,8 @@ class TaskResult(BaseModel):
|
|
|
35
36
|
)
|
|
36
37
|
|
|
37
38
|
class HttpServerController(BaseController):
|
|
38
|
-
def __init__(self, config: HttpServerControllerConfig, components: Dict[str, ComponentConfig], workflows: Dict[str, WorkflowConfig], env: Dict[str, str], daemon: bool):
|
|
39
|
-
super().__init__(config, components, workflows, env, daemon)
|
|
39
|
+
def __init__(self, config: HttpServerControllerConfig, components: Dict[str, ComponentConfig], listeners: List[ListenerConfig], workflows: Dict[str, WorkflowConfig], env: Dict[str, str], daemon: bool):
|
|
40
|
+
super().__init__(config, components, listeners, workflows, env, daemon)
|
|
40
41
|
|
|
41
42
|
self.server: Optional[uvicorn.Server] = None
|
|
42
43
|
self.app: FastAPI = FastAPI()
|
|
@@ -1,12 +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 McpServerControllerConfig
|
|
3
|
+
from mindor.dsl.schema.listener import ListenerConfig
|
|
3
4
|
from mindor.dsl.schema.component import ComponentConfig
|
|
4
5
|
from mindor.dsl.schema.workflow import WorkflowConfig
|
|
5
6
|
from .base import BaseController, ControllerType, ControllerEngineMap
|
|
6
7
|
|
|
7
8
|
class McpServerController(BaseController):
|
|
8
|
-
def __init__(self, config: McpServerControllerConfig, components: Dict[str, ComponentConfig], workflows: Dict[str, WorkflowConfig], env: Dict[str, str], daemon: bool):
|
|
9
|
-
super().__init__(config, components, workflows, env, daemon)
|
|
9
|
+
def __init__(self, config: McpServerControllerConfig, components: Dict[str, ComponentConfig], listeners: List[ListenerConfig], workflows: Dict[str, WorkflowConfig], env: Dict[str, str], daemon: bool):
|
|
10
|
+
super().__init__(config, components, listeners, workflows, env, daemon)
|
|
10
11
|
|
|
11
12
|
async def _serve(self) -> None:
|
|
12
13
|
pass
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from typing import Type, Union, Literal, Optional, Dict, List, Tuple, Set, Annotated, Any
|
|
2
|
+
from mindor.dsl.schema.listener import ListenerConfig, ListenerType
|
|
3
|
+
from mindor.core.services import AsyncService
|
|
4
|
+
from mindor.core.utils.workqueue import WorkQueue
|
|
5
|
+
|
|
6
|
+
class BaseListener(AsyncService):
|
|
7
|
+
def __init__(self, config: ListenerConfig, env: Dict[str, str], daemon: bool):
|
|
8
|
+
super().__init__(daemon)
|
|
9
|
+
|
|
10
|
+
self.config: ListenerConfig = config
|
|
11
|
+
self.env: Dict[str, str] = env
|
|
12
|
+
self.queue: Optional[WorkQueue] = None
|
|
13
|
+
|
|
14
|
+
# if self.config.max_concurrent_count > 0:
|
|
15
|
+
# self.queue = WorkQueue(self.config.max_concurrent_count, self._run_workflow)
|
|
16
|
+
|
|
17
|
+
async def _start(self) -> None:
|
|
18
|
+
if self.queue:
|
|
19
|
+
await self.queue.start()
|
|
20
|
+
|
|
21
|
+
await super()._start()
|
|
22
|
+
|
|
23
|
+
async def _stop(self) -> None:
|
|
24
|
+
if self.queue:
|
|
25
|
+
await self.queue.stop()
|
|
26
|
+
|
|
27
|
+
await super()._stop()
|
|
28
|
+
|
|
29
|
+
ListenerEngineMap: Dict[ListenerType, Type[BaseListener]] = {}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
from typing import Type, Union, Literal, Optional, Dict, List, Tuple, Set, Annotated, Callable, Iterator, Any
|
|
2
|
+
|
|
3
|
+
from mindor.dsl.schema.listener import HttpCallbackListenerConfig, HttpCallbackConfig
|
|
4
|
+
from mindor.core.utils.http_headers import parse_options_header, get_header_value
|
|
5
|
+
from mindor.core.utils.template import TemplateRenderer
|
|
6
|
+
from .base import BaseListener, ListenerType, ListenerEngineMap
|
|
7
|
+
|
|
8
|
+
from fastapi import FastAPI, APIRouter, Body, HTTPException, Request
|
|
9
|
+
from fastapi.responses import Response, JSONResponse
|
|
10
|
+
from threading import Lock
|
|
11
|
+
import asyncio, uvicorn
|
|
12
|
+
|
|
13
|
+
_pending_futures: Dict[str, asyncio.Future] = {}
|
|
14
|
+
_pending_futures_lock: Lock = Lock()
|
|
15
|
+
|
|
16
|
+
class HttpCallbackContext:
|
|
17
|
+
def __init__(self, body: Optional[Any], query: Optional[Dict[str, str]], bulk: bool, item_at: Optional[str]):
|
|
18
|
+
self.body: Optional[Any] = body
|
|
19
|
+
self.query: Optional[Dict[str, str]] = query
|
|
20
|
+
self.bulk: bool = bulk
|
|
21
|
+
self.item_at: Optional[str] = item_at
|
|
22
|
+
self.renderer: TemplateRenderer = TemplateRenderer(self._resolve_source)
|
|
23
|
+
|
|
24
|
+
def items(self) -> Iterator["HttpCallbackContext"]:
|
|
25
|
+
for item in self._items():
|
|
26
|
+
yield HttpCallbackContext(item, self.query, False, None)
|
|
27
|
+
|
|
28
|
+
def render_template(self, template: str) -> Any:
|
|
29
|
+
return self.renderer.render(template)
|
|
30
|
+
|
|
31
|
+
def _items(self) -> List[Any]:
|
|
32
|
+
item = self.renderer.render(self.item_at) if self.item_at else self.body
|
|
33
|
+
|
|
34
|
+
if self.bulk:
|
|
35
|
+
if not isinstance(item, list):
|
|
36
|
+
raise ValueError("Expected a list because 'bulk' is True, but got a non-list.")
|
|
37
|
+
return item
|
|
38
|
+
|
|
39
|
+
return [ item ]
|
|
40
|
+
|
|
41
|
+
def _resolve_source(self, key: str) -> Any:
|
|
42
|
+
if key == "body" or key == "item":
|
|
43
|
+
return self.body
|
|
44
|
+
if key == "query":
|
|
45
|
+
return self.query
|
|
46
|
+
raise KeyError(f"Unknown source: {key}")
|
|
47
|
+
|
|
48
|
+
class HttpCallbackListener(BaseListener):
|
|
49
|
+
def __init__(self, config: HttpCallbackListenerConfig, env: Dict[str, str], daemon: bool):
|
|
50
|
+
super().__init__(config, env, daemon)
|
|
51
|
+
|
|
52
|
+
self.server: Optional[uvicorn.Server] = None
|
|
53
|
+
self.app: FastAPI = FastAPI()
|
|
54
|
+
self.router: APIRouter = APIRouter()
|
|
55
|
+
|
|
56
|
+
self._configure_routes()
|
|
57
|
+
self.app.include_router(self.router, prefix=self.config.base_path)
|
|
58
|
+
|
|
59
|
+
def _configure_routes(self):
|
|
60
|
+
for callback in self.config.callbacks:
|
|
61
|
+
self.router.add_api_route(
|
|
62
|
+
path=callback.path,
|
|
63
|
+
endpoint=self._make_callback_handler(callback),
|
|
64
|
+
methods=[callback.method],
|
|
65
|
+
name=f"callback_{callback.path.strip('/').replace('/', '_')}",
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
def _make_callback_handler(self, callback: HttpCallbackConfig) -> Callable:
|
|
69
|
+
async def handler(request: Request) -> Response:
|
|
70
|
+
body, query = await self._parse_request_content(request), request.query_params
|
|
71
|
+
context = HttpCallbackContext(body, query, callback.bulk, callback.item_at)
|
|
72
|
+
|
|
73
|
+
for item in context.items():
|
|
74
|
+
action_id = item.render_template(callback.identify_by)
|
|
75
|
+
future: asyncio.Future = self._get_pending_future(action_id) if isinstance(action_id, str) else None
|
|
76
|
+
|
|
77
|
+
if future:
|
|
78
|
+
future.set_result(item.render_template(callback.result) if callback.result else item.body)
|
|
79
|
+
self._remove_pending_future(action_id)
|
|
80
|
+
|
|
81
|
+
return Response()
|
|
82
|
+
|
|
83
|
+
return handler
|
|
84
|
+
|
|
85
|
+
async def _parse_request_content(self, request: Request) -> Any:
|
|
86
|
+
content_type, _ = parse_options_header(request.headers.get("Content-Type", ""))
|
|
87
|
+
|
|
88
|
+
if content_type == "application/json":
|
|
89
|
+
return await request.json()
|
|
90
|
+
|
|
91
|
+
if content_type.startswith("text/"):
|
|
92
|
+
return str(await request.body())
|
|
93
|
+
|
|
94
|
+
return await request.body()
|
|
95
|
+
|
|
96
|
+
async def _serve(self) -> None:
|
|
97
|
+
self.server = uvicorn.Server(uvicorn.Config(
|
|
98
|
+
self.app,
|
|
99
|
+
host=self.config.host,
|
|
100
|
+
port=self.config.port,
|
|
101
|
+
log_level="info"
|
|
102
|
+
))
|
|
103
|
+
await self.server.serve()
|
|
104
|
+
|
|
105
|
+
async def _shutdown(self) -> None:
|
|
106
|
+
self.server.should_exit = True
|
|
107
|
+
|
|
108
|
+
def _get_pending_future(self, id: str) -> Optional[asyncio.Future]:
|
|
109
|
+
with _pending_futures_lock:
|
|
110
|
+
return _pending_futures.get(id)
|
|
111
|
+
|
|
112
|
+
def _remove_pending_future(self, id: str) -> None:
|
|
113
|
+
with _pending_futures_lock:
|
|
114
|
+
_pending_futures.pop(id, None)
|
|
115
|
+
|
|
116
|
+
@classmethod
|
|
117
|
+
def register_pending_future(cls, id: str, future: asyncio.Future) -> None:
|
|
118
|
+
with _pending_futures_lock:
|
|
119
|
+
_pending_futures[id] = future
|
|
120
|
+
|
|
121
|
+
ListenerEngineMap[ListenerType.HTTP_CALLBACK] = HttpCallbackListener
|
|
@@ -2,8 +2,8 @@ from typing import Type, Union, Literal, Optional, Dict, List, Tuple, Set, Annot
|
|
|
2
2
|
from mindor.dsl.schema.listener import ListenerConfig
|
|
3
3
|
from .engine import BaseListener, ListenerEngineMap
|
|
4
4
|
|
|
5
|
-
def create_listener(config:
|
|
5
|
+
def create_listener(config: ListenerConfig, env: Dict[str, str], daemon: bool) -> BaseListener:
|
|
6
6
|
try:
|
|
7
|
-
return ListenerEngineMap[config.type](config, env)
|
|
7
|
+
return ListenerEngineMap[config.type](config, env, daemon)
|
|
8
8
|
except KeyError:
|
|
9
9
|
raise ValueError(f"Unsupported listener type: {config.type}")
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from typing import Type, Union, Literal, Optional, Dict, List, Tuple, Set, Annotated, Callable, Any
|
|
2
|
+
from abc import ABC, abstractmethod
|
|
3
|
+
from threading import Thread
|
|
4
|
+
import asyncio
|
|
5
|
+
|
|
6
|
+
class AsyncService(ABC):
|
|
7
|
+
def __init__(self, daemon: bool):
|
|
8
|
+
self.daemon: bool = daemon
|
|
9
|
+
self.started: bool = False
|
|
10
|
+
|
|
11
|
+
self.thread: Optional[Thread] = None
|
|
12
|
+
self.thread_loop: Optional[asyncio.AbstractEventLoop] = None
|
|
13
|
+
self.daemon_task: Optional[asyncio.Task] = None
|
|
14
|
+
|
|
15
|
+
async def start(self, background: bool = False) -> None:
|
|
16
|
+
if background:
|
|
17
|
+
def _start_in_thread():
|
|
18
|
+
self.thread_loop = asyncio.new_event_loop()
|
|
19
|
+
asyncio.set_event_loop(self.thread_loop)
|
|
20
|
+
self.thread_loop.run_until_complete(self._start())
|
|
21
|
+
|
|
22
|
+
self.thread = Thread(target=_start_in_thread)
|
|
23
|
+
self.thread.start()
|
|
24
|
+
else:
|
|
25
|
+
await self._start()
|
|
26
|
+
|
|
27
|
+
async def stop(self) -> None:
|
|
28
|
+
if self.thread:
|
|
29
|
+
future = asyncio.run_coroutine_threadsafe(self._stop(), self.thread_loop)
|
|
30
|
+
future.result()
|
|
31
|
+
self.thread_loop.close()
|
|
32
|
+
self.thread_loop = None
|
|
33
|
+
self.thread.join()
|
|
34
|
+
self.thread = None
|
|
35
|
+
else:
|
|
36
|
+
await self._stop()
|
|
37
|
+
|
|
38
|
+
async def wait_until_stopped(self) -> None:
|
|
39
|
+
if self.thread:
|
|
40
|
+
self.thread.join()
|
|
41
|
+
|
|
42
|
+
if self.daemon_task:
|
|
43
|
+
await self.daemon_task
|
|
44
|
+
|
|
45
|
+
async def _start(self) -> None:
|
|
46
|
+
self.started = True
|
|
47
|
+
|
|
48
|
+
if self.daemon:
|
|
49
|
+
if not self.thread:
|
|
50
|
+
self.daemon_task = asyncio.create_task(self._serve())
|
|
51
|
+
else:
|
|
52
|
+
await self._serve()
|
|
53
|
+
|
|
54
|
+
async def _stop(self) -> None:
|
|
55
|
+
if self.daemon:
|
|
56
|
+
await self._shutdown()
|
|
57
|
+
|
|
58
|
+
self.started = False
|
|
59
|
+
|
|
60
|
+
@abstractmethod
|
|
61
|
+
async def _serve(self) -> None:
|
|
62
|
+
pass
|
|
63
|
+
|
|
64
|
+
@abstractmethod
|
|
65
|
+
async def _shutdown(self) -> None:
|
|
66
|
+
pass
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from typing import Callable, Dict, List, Any
|
|
1
|
+
from typing import Callable, Dict, List, Optional, Any
|
|
2
2
|
import re
|
|
3
3
|
|
|
4
4
|
class TemplateRenderer:
|
|
@@ -8,7 +8,7 @@ class TemplateRenderer:
|
|
|
8
8
|
"placeholder": re.compile(
|
|
9
9
|
r"""\$\{ # ${
|
|
10
10
|
([a-zA-Z_][\w\-]*) # key: input, env, etc.
|
|
11
|
-
|
|
11
|
+
(?:\.([^\|\}]+))? # path: key, key.path[0], etc.
|
|
12
12
|
(?:\s*\|\s*([^\}]+))? # default value after `|`
|
|
13
13
|
\}""", # }
|
|
14
14
|
re.VERBOSE,
|
|
@@ -28,19 +28,22 @@ class TemplateRenderer:
|
|
|
28
28
|
return [ self._render_element(item) for item in element ]
|
|
29
29
|
return element
|
|
30
30
|
|
|
31
|
-
def _render_text(self, text: str) ->
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
def _render_text(self, text: str) -> Any:
|
|
32
|
+
match = self.patterns["placeholder"].search(text)
|
|
33
|
+
if match:
|
|
34
|
+
key, path, default, placeholder = match.group(1), match.group(2), match.group(3), match.group(0)
|
|
34
35
|
try:
|
|
35
36
|
source = self.source_resolver(key)
|
|
36
37
|
value = self._resolve_by_path(source, path)
|
|
37
|
-
|
|
38
|
+
if text != placeholder:
|
|
39
|
+
return text.replace(placeholder, value)
|
|
40
|
+
return value
|
|
38
41
|
except Exception:
|
|
39
42
|
return default or ""
|
|
40
|
-
return
|
|
43
|
+
return text
|
|
41
44
|
|
|
42
|
-
def _resolve_by_path(self, source: Any, path: str) -> Any:
|
|
43
|
-
parts: List[str] = self.patterns["keypath"].findall(path)
|
|
45
|
+
def _resolve_by_path(self, source: Any, path: Optional[str]) -> Any:
|
|
46
|
+
parts: List[str] = self.patterns["keypath"].findall(path) if path else []
|
|
44
47
|
current = source
|
|
45
48
|
for part in parts:
|
|
46
49
|
if isinstance(current, dict) and not part.startswith("["):
|
|
@@ -85,7 +85,7 @@ class WorkflowRunner:
|
|
|
85
85
|
running_job_ids: Set[str] = set()
|
|
86
86
|
completed_job_ids: Set[str] = set()
|
|
87
87
|
scheduled_job_tasks: Dict[str, asyncio.Task] = {}
|
|
88
|
-
output: Any =
|
|
88
|
+
output: Any = None
|
|
89
89
|
|
|
90
90
|
while pending_jobs:
|
|
91
91
|
runnable_jobs = [ job for job in pending_jobs.values() if self._can_run_job(job, running_job_ids, completed_job_ids) ]
|
|
@@ -107,8 +107,10 @@ class WorkflowRunner:
|
|
|
107
107
|
context.complete_job(completed_job_id, completed_job_output)
|
|
108
108
|
|
|
109
109
|
if self._is_terminal_job(completed_job_id):
|
|
110
|
-
output
|
|
111
|
-
|
|
110
|
+
if isinstance(output, dict) and isinstance(completed_job_output, dict):
|
|
111
|
+
output.update(completed_job_output)
|
|
112
|
+
else:
|
|
113
|
+
output = completed_job_output
|
|
112
114
|
|
|
113
115
|
running_job_ids.remove(completed_job_id)
|
|
114
116
|
completed_job_ids.add(completed_job_id)
|
|
@@ -136,10 +138,10 @@ class Workflow:
|
|
|
136
138
|
|
|
137
139
|
output = await runner.run(context)
|
|
138
140
|
|
|
139
|
-
if
|
|
140
|
-
|
|
141
|
+
if output:
|
|
142
|
+
context.register_source("output", output)
|
|
141
143
|
|
|
142
|
-
return context.render_template(self.config.output)
|
|
144
|
+
return context.render_template(self.config.output) if self.config.output else output
|
|
143
145
|
|
|
144
146
|
def validate(self) -> None:
|
|
145
147
|
JobGraphValidator(self.config.jobs).validate()
|
|
@@ -2,5 +2,5 @@ from typing import Type, Union, Literal, Optional, Dict, List, Tuple, Set, Annot
|
|
|
2
2
|
from pydantic import BaseModel, Field
|
|
3
3
|
|
|
4
4
|
class CommonActionConfig(BaseModel):
|
|
5
|
-
output: Optional[
|
|
5
|
+
output: Optional[Any] = None
|
|
6
6
|
default: bool = False
|
{model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/dsl/schema/action/impl/http_client.py
RENAMED
|
@@ -22,7 +22,7 @@ class PollingCompletionConfig(CommonCompletionConfig):
|
|
|
22
22
|
timeout: Optional[str] = None
|
|
23
23
|
|
|
24
24
|
@model_validator(mode="before")
|
|
25
|
-
def validate_endpoint_or_path(cls, values):
|
|
25
|
+
def validate_endpoint_or_path(cls, values: Dict[str, Any]):
|
|
26
26
|
if bool(values.get("endpoint")) == bool(values.get("path")):
|
|
27
27
|
raise ValueError("Either 'endpoint' or 'path' must be set, but not both.")
|
|
28
28
|
return values
|
|
@@ -46,7 +46,7 @@ class HttpClientActionConfig(CommonActionConfig):
|
|
|
46
46
|
completion: Optional[HttpClientCompletionConfig] = None
|
|
47
47
|
|
|
48
48
|
@model_validator(mode="before")
|
|
49
|
-
def validate_endpoint_or_path(cls, values):
|
|
49
|
+
def validate_endpoint_or_path(cls, values: Dict[str, Any]):
|
|
50
50
|
if bool(values.get("endpoint")) == bool(values.get("path")):
|
|
51
51
|
raise ValueError("Either 'endpoint' or 'path' must be set, but not both.")
|
|
52
52
|
return values
|
{model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/dsl/schema/component/impl/http_client.py
RENAMED
|
@@ -11,7 +11,7 @@ class HttpClientComponentConfig(CommonComponentConfig):
|
|
|
11
11
|
actions: Optional[Dict[str, HttpClientActionConfig]] = Field(default_factory=dict)
|
|
12
12
|
|
|
13
13
|
@model_validator(mode="before")
|
|
14
|
-
def inflate_single_action(cls, values):
|
|
14
|
+
def inflate_single_action(cls, values: Dict[str, Any]):
|
|
15
15
|
if "actions" not in values:
|
|
16
16
|
action_keys = set(HttpClientActionConfig.model_fields.keys())
|
|
17
17
|
if any(k in values for k in action_keys):
|
|
@@ -4,23 +4,33 @@ from pydantic import model_validator
|
|
|
4
4
|
|
|
5
5
|
from .controller import ControllerConfig
|
|
6
6
|
from .component import ComponentConfig
|
|
7
|
+
from .listener import ListenerConfig
|
|
7
8
|
from .workflow import WorkflowConfig
|
|
8
9
|
|
|
9
10
|
class ComposeConfig(BaseModel):
|
|
10
11
|
controller: ControllerConfig
|
|
11
12
|
components: Dict[str, ComponentConfig] = Field(default_factory=dict)
|
|
13
|
+
listeners: List[ListenerConfig] = Field(default_factory=list)
|
|
12
14
|
workflows: Dict[str, WorkflowConfig] = Field(default_factory=dict)
|
|
13
15
|
|
|
14
16
|
@model_validator(mode="before")
|
|
15
|
-
def inflate_single_component(cls, values):
|
|
17
|
+
def inflate_single_component(cls, values: Dict[str, Any]):
|
|
16
18
|
if "components" not in values:
|
|
17
19
|
component_values = values.pop("component", None)
|
|
18
20
|
if component_values:
|
|
19
21
|
values["components"] = { "__default__": component_values }
|
|
20
22
|
return values
|
|
23
|
+
|
|
24
|
+
@model_validator(mode="before")
|
|
25
|
+
def inflate_single_listener(cls, values: Dict[str, Any]):
|
|
26
|
+
if "listeners" not in values:
|
|
27
|
+
listener_values = values.pop("listener", None)
|
|
28
|
+
if listener_values:
|
|
29
|
+
values["listeners"] = [ listener_values ]
|
|
30
|
+
return values
|
|
21
31
|
|
|
22
32
|
@model_validator(mode="before")
|
|
23
|
-
def inflate_single_workflow(cls, values):
|
|
33
|
+
def inflate_single_workflow(cls, values: Dict[str, Any]):
|
|
24
34
|
if "workflows" not in values:
|
|
25
35
|
workflow_values = values.pop("workflow", None)
|
|
26
36
|
if workflow_values:
|
{model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/dsl/schema/listener/impl/http_callback.py
RENAMED
|
@@ -5,9 +5,11 @@ from .common import ListenerType, CommonListenerConfig
|
|
|
5
5
|
|
|
6
6
|
class HttpCallbackConfig(BaseModel):
|
|
7
7
|
path: str
|
|
8
|
-
method: Literal[ "GET", "POST", "PUT", "DELETE", "PATCH" ] = "
|
|
8
|
+
method: Literal[ "GET", "POST", "PUT", "DELETE", "PATCH" ] = "POST"
|
|
9
9
|
bulk: bool = False
|
|
10
|
+
item_at: Optional[str] = None
|
|
10
11
|
identify_by: str
|
|
12
|
+
result: Optional[Any] = None
|
|
11
13
|
|
|
12
14
|
class HttpCallbackListenerConfig(CommonListenerConfig):
|
|
13
15
|
type: Literal[ListenerType.HTTP_CALLBACK]
|
|
@@ -17,7 +19,7 @@ class HttpCallbackListenerConfig(CommonListenerConfig):
|
|
|
17
19
|
callbacks: Optional[List[HttpCallbackConfig]] = Field(default_factory=list)
|
|
18
20
|
|
|
19
21
|
@model_validator(mode="before")
|
|
20
|
-
def inflate_single_callback(cls, values):
|
|
22
|
+
def inflate_single_callback(cls, values: Dict[str, Any]):
|
|
21
23
|
if "callbacks" not in values:
|
|
22
24
|
callback_keys = set(HttpCallbackConfig.model_fields.keys())
|
|
23
25
|
if any(k in values for k in callback_keys):
|
|
@@ -6,18 +6,18 @@ from .component import ComponentConfig
|
|
|
6
6
|
class JobConfig(BaseModel):
|
|
7
7
|
component: Optional[Union[ str, ComponentConfig ]] = "__default__"
|
|
8
8
|
action: Optional[str] = "__default__"
|
|
9
|
-
input: Optional[
|
|
10
|
-
output: Optional[
|
|
9
|
+
input: Optional[Any] = None
|
|
10
|
+
output: Optional[Any] = None
|
|
11
11
|
depends_on: Optional[List[str]] = Field(default_factory=list)
|
|
12
12
|
|
|
13
13
|
class WorkflowConfig(BaseModel):
|
|
14
14
|
description: Optional[str] = None
|
|
15
15
|
jobs: Optional[Dict[str, JobConfig]] = Field(default_factory=dict)
|
|
16
|
-
output: Optional[
|
|
16
|
+
output: Optional[Any] = None
|
|
17
17
|
default: bool = False
|
|
18
18
|
|
|
19
19
|
@model_validator(mode="before")
|
|
20
|
-
def inflate_single_job(cls, values):
|
|
20
|
+
def inflate_single_job(cls, values: Dict[str, Any]):
|
|
21
21
|
if "jobs" not in values:
|
|
22
22
|
job_keys = set(JobConfig.model_fields.keys())
|
|
23
23
|
if any(k in values for k in job_keys):
|
|
@@ -28,6 +28,8 @@ src/mindor/core/listener/engine/base.py
|
|
|
28
28
|
src/mindor/core/listener/engine/http_callback.py
|
|
29
29
|
src/mindor/core/runtime/__init__.py
|
|
30
30
|
src/mindor/core/runtime/env.py
|
|
31
|
+
src/mindor/core/services/__init__.py
|
|
32
|
+
src/mindor/core/services/async_service.py
|
|
31
33
|
src/mindor/core/utils/__init__.py
|
|
32
34
|
src/mindor/core/utils/expiring.py
|
|
33
35
|
src/mindor/core/utils/http_headers.py
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
from typing import Type, Union, Literal, Optional, Dict, List, Tuple, Set, Annotated, Any
|
|
2
|
-
from abc import ABC, abstractmethod
|
|
3
|
-
from dataclasses import dataclass
|
|
4
|
-
|
|
5
|
-
from mindor.dsl.schema.listener import ListenerConfig, ListenerType
|
|
6
|
-
from mindor.core.listener import BaseListener, create_listener
|
|
7
|
-
|
|
8
|
-
class BaseListener(ABC):
|
|
9
|
-
def __init__(self, config: ListenerConfig, env: Dict[str, str]):
|
|
10
|
-
self.config: ListenerConfig = config
|
|
11
|
-
self.env: Dict[str, str] = env
|
|
12
|
-
|
|
13
|
-
ListenerEngineMap: Dict[ListenerType, Type[BaseListener]] = {}
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
from typing import Type, Union, Literal, Optional, Dict, List, Tuple, Set, Annotated, Any
|
|
2
|
-
from typing_extensions import Self
|
|
3
|
-
from pydantic import BaseModel
|
|
4
|
-
|
|
5
|
-
from mindor.dsl.schema.listener import HttpCallbackListenerConfig
|
|
6
|
-
from .base import BaseListener, ListenerType, ListenerEngineMap
|
|
7
|
-
|
|
8
|
-
from fastapi import FastAPI, APIRouter, Body, HTTPException
|
|
9
|
-
import uvicorn
|
|
10
|
-
|
|
11
|
-
class HttpCallbackListener(BaseListener):
|
|
12
|
-
def __init__(self, config: HttpCallbackListenerConfig, env: Dict[str, str]):
|
|
13
|
-
super().__init__(config, env)
|
|
14
|
-
|
|
15
|
-
self.server: Optional[uvicorn.Server] = None
|
|
16
|
-
self.app: FastAPI = FastAPI()
|
|
17
|
-
self.router: APIRouter = APIRouter()
|
|
18
|
-
|
|
19
|
-
self._configure_routes()
|
|
20
|
-
self.app.include_router(self.router, prefix=self.config.base_path)
|
|
21
|
-
|
|
22
|
-
def _configure_routes(self):
|
|
23
|
-
pass
|
|
24
|
-
|
|
25
|
-
async def _serve(self) -> None:
|
|
26
|
-
self.server = uvicorn.Server(uvicorn.Config(
|
|
27
|
-
self.app,
|
|
28
|
-
host=self.config.host,
|
|
29
|
-
port=self.config.port,
|
|
30
|
-
log_level="info"
|
|
31
|
-
))
|
|
32
|
-
await self.server.serve()
|
|
33
|
-
|
|
34
|
-
async def _shutdown(self) -> None:
|
|
35
|
-
self.server.should_exit = True
|
|
36
|
-
|
|
37
|
-
ListenerEngineMap[ListenerType.HTTP_CALLBACK] = HttpCallbackListener
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
from .listener import *
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/dsl/schema/action/impl/http_server.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/dsl/schema/component/impl/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/dsl/schema/component/impl/http_server.py
RENAMED
|
File without changes
|
{model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/dsl/schema/component/impl/mcp_client.py
RENAMED
|
File without changes
|
{model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/dsl/schema/component/impl/mcp_server.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/dsl/schema/controller/impl/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/dsl/schema/controller/impl/http_server.py
RENAMED
|
File without changes
|
{model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/dsl/schema/controller/impl/mcp_server.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|