model-compose 0.0.6__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.6/src/model_compose.egg-info → model_compose-0.0.8}/PKG-INFO +1 -1
- {model_compose-0.0.6 → model_compose-0.0.8}/pyproject.toml +1 -1
- {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/core/component/engine/__init__.py +0 -1
- {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/core/component/engine/base.py +7 -61
- {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/core/component/engine/http_client.py +24 -6
- {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/core/compose/manager.py +1 -1
- {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/core/controller/controller.py +3 -2
- {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/core/controller/engine/base.py +24 -57
- {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/core/controller/engine/http_server.py +45 -38
- {model_compose-0.0.6 → 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.6 → 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.6 → model_compose-0.0.8}/src/mindor/core/utils/template.py +12 -9
- {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/core/workflow/job.py +2 -2
- {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/core/workflow/workflow.py +14 -12
- {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/dsl/schema/action/impl/common.py +1 -1
- {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/dsl/schema/action/impl/http_client.py +2 -2
- {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/dsl/schema/component/impl/http_client.py +1 -1
- {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/dsl/schema/compose.py +12 -2
- {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/dsl/schema/listener/impl/common.py +1 -0
- {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/dsl/schema/listener/impl/http_callback.py +4 -2
- {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/dsl/schema/workflow.py +4 -4
- {model_compose-0.0.6 → model_compose-0.0.8/src/model_compose.egg-info}/PKG-INFO +1 -1
- {model_compose-0.0.6 → model_compose-0.0.8}/src/model_compose.egg-info/SOURCES.txt +2 -0
- model_compose-0.0.6/src/mindor/core/listener/engine/base.py +0 -13
- model_compose-0.0.6/src/mindor/core/listener/engine/http_callback.py +0 -37
- model_compose-0.0.6/src/mindor/dsl/schema/listener/__init__.py +0 -1
- {model_compose-0.0.6 → model_compose-0.0.8}/LICENSE +0 -0
- {model_compose-0.0.6 → model_compose-0.0.8}/README.md +0 -0
- {model_compose-0.0.6 → model_compose-0.0.8}/setup.cfg +0 -0
- {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/__init__.py +0 -0
- {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/cli/__init__.py +0 -0
- {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/cli/compose.py +0 -0
- {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/core/__init__.py +0 -0
- {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/core/component/__init__.py +0 -0
- {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/core/component/component.py +0 -0
- {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/core/component/engine/context.py +0 -0
- {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/core/component/engine/http_server.py +0 -0
- {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/core/compose/__init__.py +0 -0
- {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/core/compose/compose.py +0 -0
- {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/core/controller/__init__.py +0 -0
- {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/core/controller/engine/__init__.py +0 -0
- {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/core/listener/engine/__init__.py +0 -0
- {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/core/runtime/__init__.py +0 -0
- {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/core/runtime/env.py +0 -0
- {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/core/utils/__init__.py +0 -0
- {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/core/utils/expiring.py +0 -0
- {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/core/utils/http_headers.py +0 -0
- {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/core/utils/resources.py +0 -0
- {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/core/utils/workqueue.py +0 -0
- {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/core/workflow/__init__.py +0 -0
- {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/core/workflow/context.py +0 -0
- {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/dsl/__init__.py +0 -0
- {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/dsl/loader.py +0 -0
- {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/dsl/schema/__init__.py +0 -0
- {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/dsl/schema/action/__init__.py +0 -0
- {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/dsl/schema/action/action.py +0 -0
- {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/dsl/schema/action/impl/__init__.py +0 -0
- {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/dsl/schema/action/impl/http_server.py +0 -0
- {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/dsl/schema/action/impl/mcp_client.py +0 -0
- {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/dsl/schema/action/impl/mcp_server.py +0 -0
- {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/dsl/schema/component/__init__.py +0 -0
- {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/dsl/schema/component/component.py +0 -0
- {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/dsl/schema/component/impl/__init__.py +0 -0
- {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/dsl/schema/component/impl/common.py +0 -0
- {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/dsl/schema/component/impl/http_server.py +0 -0
- {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/dsl/schema/component/impl/mcp_client.py +0 -0
- {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/dsl/schema/component/impl/mcp_server.py +0 -0
- {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/dsl/schema/component/impl/types.py +0 -0
- {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/dsl/schema/controller/__init__.py +0 -0
- {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/dsl/schema/controller/controller.py +0 -0
- {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/dsl/schema/controller/impl/__init__.py +0 -0
- {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/dsl/schema/controller/impl/common.py +0 -0
- {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/dsl/schema/controller/impl/http_server.py +0 -0
- {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/dsl/schema/controller/impl/mcp_server.py +0 -0
- {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/dsl/schema/controller/impl/types.py +0 -0
- {model_compose-0.0.6/src/mindor/core → model_compose-0.0.8/src/mindor/dsl/schema}/listener/__init__.py +0 -0
- {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/dsl/schema/listener/impl/__init__.py +0 -0
- {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/dsl/schema/listener/impl/types.py +0 -0
- {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/dsl/schema/listener/listener.py +0 -0
- {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/dsl/schema/runtime/__init__.py +0 -0
- {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/dsl/schema/runtime/impl/__init__.py +0 -0
- {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/dsl/schema/runtime/impl/common.py +0 -0
- {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/dsl/schema/runtime/impl/docker.py +0 -0
- {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/dsl/schema/runtime/impl/types.py +0 -0
- {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/dsl/schema/runtime/runtime.py +0 -0
- {model_compose-0.0.6 → model_compose-0.0.8}/src/model_compose.egg-info/dependency_links.txt +0 -0
- {model_compose-0.0.6 → model_compose-0.0.8}/src/model_compose.egg-info/entry_points.txt +0 -0
- {model_compose-0.0.6 → model_compose-0.0.8}/src/model_compose.egg-info/requires.txt +0 -0
- {model_compose-0.0.6 → model_compose-0.0.8}/src/model_compose.egg-info/top_level.txt +0 -0
- {model_compose-0.0.6 → 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,15 +1,17 @@
|
|
|
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
|
-
def __init__(self, session: aiohttp.ClientSession, stream: aiohttp.StreamReader,
|
|
14
|
+
def __init__(self, session: aiohttp.ClientSession, stream: aiohttp.StreamReader, content_type: Optional[str], filename: Optional[str]):
|
|
13
15
|
super().__init__(content_type, filename)
|
|
14
16
|
|
|
15
17
|
self.session: aiohttp.ClientSession = session
|
|
@@ -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()
|
|
@@ -105,7 +111,19 @@ class HttpClientAction:
|
|
|
105
111
|
_, disposition = parse_options_header(response.headers.get("Content-Disposition", ""))
|
|
106
112
|
filename = disposition.get("filename")
|
|
107
113
|
|
|
108
|
-
return HttpStreamResource(session, response.content,
|
|
114
|
+
return HttpStreamResource(session, response.content, content_type, filename)
|
|
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
|
|
109
127
|
|
|
110
128
|
class HttpClientComponent(BaseComponent):
|
|
111
129
|
def __init__(self, id: str, config: HttpClientComponentConfig, env: Dict[str, str], daemon: bool):
|
|
@@ -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.6 → model_compose-0.0.8}/src/mindor/core/controller/engine/http_server.py
RENAMED
|
@@ -3,19 +3,20 @@ 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
|
|
9
10
|
from .base import BaseController, ControllerType, ControllerEngineMap, TaskState
|
|
10
11
|
|
|
11
12
|
from fastapi import FastAPI, APIRouter, Body, HTTPException
|
|
12
|
-
from fastapi.responses import JSONResponse, StreamingResponse
|
|
13
|
+
from fastapi.responses import Response, JSONResponse, StreamingResponse
|
|
13
14
|
from starlette.background import BackgroundTask
|
|
14
15
|
import asyncio, uvicorn
|
|
15
16
|
|
|
16
17
|
class WorkflowTaskRequestBody(BaseModel):
|
|
17
18
|
workflow_id: Optional[str] = None
|
|
18
|
-
input: Optional[
|
|
19
|
+
input: Optional[Any] = None
|
|
19
20
|
wait_for_completion: bool = True
|
|
20
21
|
output_only: bool = False
|
|
21
22
|
|
|
@@ -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()
|
|
@@ -53,22 +54,9 @@ class HttpServerController(BaseController):
|
|
|
53
54
|
state = await self.run_workflow(body.workflow_id, body.input, body.wait_for_completion)
|
|
54
55
|
|
|
55
56
|
if body.output_only and body.wait_for_completion:
|
|
56
|
-
|
|
57
|
-
raise HTTPException(status_code=500, detail=str(state.error))
|
|
58
|
-
|
|
59
|
-
if isinstance(state.output, StreamResource):
|
|
60
|
-
return StreamingResponse(
|
|
61
|
-
state.output,
|
|
62
|
-
media_type=state.output.content_type,
|
|
63
|
-
headers = {
|
|
64
|
-
"Content-Disposition": f"attachment; filename={state.output.filename}"
|
|
65
|
-
} if state.output.filename else {},
|
|
66
|
-
#background=BackgroundTask(lambda: asyncio.create_task(state.output.close()))
|
|
67
|
-
)
|
|
68
|
-
|
|
69
|
-
return state.output
|
|
57
|
+
return self._render_task_output(state)
|
|
70
58
|
|
|
71
|
-
return
|
|
59
|
+
return self._render_task_state(state)
|
|
72
60
|
|
|
73
61
|
@self.router.get("/tasks/{task_id}")
|
|
74
62
|
async def get_task_state(
|
|
@@ -81,25 +69,9 @@ class HttpServerController(BaseController):
|
|
|
81
69
|
raise HTTPException(status_code=404, detail="Task not found.")
|
|
82
70
|
|
|
83
71
|
if output_only:
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
if state.status == "failed":
|
|
88
|
-
raise HTTPException(status_code=500, detail=str(state.error))
|
|
89
|
-
|
|
90
|
-
if isinstance(state.output, StreamResource):
|
|
91
|
-
return StreamingResponse(
|
|
92
|
-
state.output,
|
|
93
|
-
media_type=state.output.content_type,
|
|
94
|
-
headers = {
|
|
95
|
-
"Content-Disposition": f"attachment; filename={state.output.filename}"
|
|
96
|
-
} if state.output.filename else {},
|
|
97
|
-
# background=BackgroundTask(lambda: asyncio.create_task(state.output.close()))
|
|
98
|
-
)
|
|
99
|
-
|
|
100
|
-
return state.output
|
|
101
|
-
|
|
102
|
-
return JSONResponse(content=TaskResult.from_instance(state).model_dump(exclude_none=True))
|
|
72
|
+
return self._render_task_output(state)
|
|
73
|
+
|
|
74
|
+
return self._render_task_state(state)
|
|
103
75
|
|
|
104
76
|
async def _serve(self) -> None:
|
|
105
77
|
self.server = uvicorn.Server(uvicorn.Config(
|
|
@@ -113,4 +85,39 @@ class HttpServerController(BaseController):
|
|
|
113
85
|
async def _shutdown(self) -> None:
|
|
114
86
|
self.server.should_exit = True
|
|
115
87
|
|
|
88
|
+
def _render_task_output(self, state: TaskState) -> Response:
|
|
89
|
+
if state.status in [ "pending", "processing" ]:
|
|
90
|
+
raise HTTPException(status_code=202, detail="Task is still in progress.")
|
|
91
|
+
|
|
92
|
+
if state.status == "failed":
|
|
93
|
+
raise HTTPException(status_code=500, detail=str(state.error))
|
|
94
|
+
|
|
95
|
+
if isinstance(state.output, StreamResource):
|
|
96
|
+
return self._render_stream_resource(state.output)
|
|
97
|
+
|
|
98
|
+
return JSONResponse(content=state.output)
|
|
99
|
+
|
|
100
|
+
def _render_task_state(self, state: TaskState) -> Response:
|
|
101
|
+
return JSONResponse(content=TaskResult.from_instance(state).model_dump(exclude_none=True))
|
|
102
|
+
|
|
103
|
+
def _render_stream_resource(self, resource: StreamResource) -> Response:
|
|
104
|
+
def _close_stream():
|
|
105
|
+
asyncio.run(resource.close())
|
|
106
|
+
|
|
107
|
+
return StreamingResponse(
|
|
108
|
+
resource,
|
|
109
|
+
media_type=resource.content_type,
|
|
110
|
+
headers=self._build_stream_resource_headers(resource),
|
|
111
|
+
background=BackgroundTask(_close_stream)
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
def _build_stream_resource_headers(self, resource: StreamResource) -> Dict[str, str]:
|
|
115
|
+
headers: Dict[str, str] = { "Cache-Control": "no-cache" }
|
|
116
|
+
|
|
117
|
+
if resource.filename:
|
|
118
|
+
filename = resource.filename.replace('"', '\\"')
|
|
119
|
+
headers["Content-Disposition"] = f'attachment; filename="{filename}"'
|
|
120
|
+
|
|
121
|
+
return headers
|
|
122
|
+
|
|
116
123
|
ControllerEngineMap[ControllerType.HTTP_SERVER] = HttpServerController
|
|
@@ -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}")
|