model-compose 0.0.7__tar.gz → 0.0.9__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. {model_compose-0.0.7/src/model_compose.egg-info → model_compose-0.0.9}/PKG-INFO +1 -1
  2. {model_compose-0.0.7 → model_compose-0.0.9}/pyproject.toml +1 -1
  3. {model_compose-0.0.7 → model_compose-0.0.9}/src/mindor/core/component/engine/__init__.py +0 -1
  4. {model_compose-0.0.7 → model_compose-0.0.9}/src/mindor/core/component/engine/base.py +7 -61
  5. model_compose-0.0.9/src/mindor/core/component/engine/http_client.py +120 -0
  6. {model_compose-0.0.7 → model_compose-0.0.9}/src/mindor/core/compose/manager.py +1 -1
  7. {model_compose-0.0.7 → model_compose-0.0.9}/src/mindor/core/controller/controller.py +3 -2
  8. {model_compose-0.0.7 → model_compose-0.0.9}/src/mindor/core/controller/engine/base.py +24 -57
  9. {model_compose-0.0.7 → model_compose-0.0.9}/src/mindor/core/controller/engine/http_server.py +3 -2
  10. {model_compose-0.0.7 → model_compose-0.0.9}/src/mindor/core/controller/engine/mcp_server.py +3 -2
  11. model_compose-0.0.9/src/mindor/core/gateway/__init__.py +2 -0
  12. model_compose-0.0.9/src/mindor/core/gateway/engine/__init__.py +2 -0
  13. model_compose-0.0.9/src/mindor/core/gateway/engine/base.py +12 -0
  14. model_compose-0.0.9/src/mindor/core/gateway/engine/http_tunnel.py +9 -0
  15. model_compose-0.0.9/src/mindor/core/gateway/gateway.py +9 -0
  16. model_compose-0.0.9/src/mindor/core/listener/__init__.py +2 -0
  17. model_compose-0.0.9/src/mindor/core/listener/engine/base.py +29 -0
  18. model_compose-0.0.9/src/mindor/core/listener/engine/http_callback.py +121 -0
  19. {model_compose-0.0.7 → model_compose-0.0.9}/src/mindor/core/listener/listener.py +2 -2
  20. model_compose-0.0.9/src/mindor/core/services/__init__.py +2 -0
  21. model_compose-0.0.9/src/mindor/core/services/async_service.py +66 -0
  22. {model_compose-0.0.7/src/mindor/core/component/engine → model_compose-0.0.9/src/mindor/core/utils}/http_client.py +5 -49
  23. {model_compose-0.0.7 → model_compose-0.0.9}/src/mindor/core/utils/template.py +24 -12
  24. model_compose-0.0.9/src/mindor/core/utils/time.py +13 -0
  25. {model_compose-0.0.7 → model_compose-0.0.9}/src/mindor/core/workflow/workflow.py +8 -6
  26. {model_compose-0.0.7 → model_compose-0.0.9}/src/mindor/dsl/schema/action/impl/common.py +1 -1
  27. {model_compose-0.0.7 → model_compose-0.0.9}/src/mindor/dsl/schema/action/impl/http_client.py +13 -10
  28. {model_compose-0.0.7 → model_compose-0.0.9}/src/mindor/dsl/schema/component/impl/http_client.py +1 -1
  29. {model_compose-0.0.7 → model_compose-0.0.9}/src/mindor/dsl/schema/compose.py +12 -2
  30. model_compose-0.0.9/src/mindor/dsl/schema/gateway/__init__.py +1 -0
  31. model_compose-0.0.9/src/mindor/dsl/schema/gateway/gateway.py +10 -0
  32. model_compose-0.0.9/src/mindor/dsl/schema/gateway/impl/__init__.py +2 -0
  33. model_compose-0.0.9/src/mindor/dsl/schema/gateway/impl/common.py +7 -0
  34. model_compose-0.0.9/src/mindor/dsl/schema/gateway/impl/http_tunnel.py +9 -0
  35. model_compose-0.0.9/src/mindor/dsl/schema/gateway/impl/types.py +4 -0
  36. {model_compose-0.0.7 → model_compose-0.0.9}/src/mindor/dsl/schema/listener/impl/common.py +1 -0
  37. {model_compose-0.0.7 → model_compose-0.0.9}/src/mindor/dsl/schema/listener/impl/http_callback.py +4 -2
  38. {model_compose-0.0.7 → model_compose-0.0.9}/src/mindor/dsl/schema/workflow.py +4 -4
  39. {model_compose-0.0.7 → model_compose-0.0.9/src/model_compose.egg-info}/PKG-INFO +1 -1
  40. {model_compose-0.0.7 → model_compose-0.0.9}/src/model_compose.egg-info/SOURCES.txt +15 -0
  41. model_compose-0.0.7/src/mindor/core/listener/engine/base.py +0 -13
  42. model_compose-0.0.7/src/mindor/core/listener/engine/http_callback.py +0 -37
  43. model_compose-0.0.7/src/mindor/dsl/schema/listener/__init__.py +0 -1
  44. {model_compose-0.0.7 → model_compose-0.0.9}/LICENSE +0 -0
  45. {model_compose-0.0.7 → model_compose-0.0.9}/README.md +0 -0
  46. {model_compose-0.0.7 → model_compose-0.0.9}/setup.cfg +0 -0
  47. {model_compose-0.0.7 → model_compose-0.0.9}/src/mindor/__init__.py +0 -0
  48. {model_compose-0.0.7 → model_compose-0.0.9}/src/mindor/cli/__init__.py +0 -0
  49. {model_compose-0.0.7 → model_compose-0.0.9}/src/mindor/cli/compose.py +0 -0
  50. {model_compose-0.0.7 → model_compose-0.0.9}/src/mindor/core/__init__.py +0 -0
  51. {model_compose-0.0.7 → model_compose-0.0.9}/src/mindor/core/component/__init__.py +0 -0
  52. {model_compose-0.0.7 → model_compose-0.0.9}/src/mindor/core/component/component.py +0 -0
  53. {model_compose-0.0.7 → model_compose-0.0.9}/src/mindor/core/component/engine/context.py +0 -0
  54. {model_compose-0.0.7 → model_compose-0.0.9}/src/mindor/core/component/engine/http_server.py +0 -0
  55. {model_compose-0.0.7 → model_compose-0.0.9}/src/mindor/core/compose/__init__.py +0 -0
  56. {model_compose-0.0.7 → model_compose-0.0.9}/src/mindor/core/compose/compose.py +0 -0
  57. {model_compose-0.0.7 → model_compose-0.0.9}/src/mindor/core/controller/__init__.py +0 -0
  58. {model_compose-0.0.7 → model_compose-0.0.9}/src/mindor/core/controller/engine/__init__.py +0 -0
  59. {model_compose-0.0.7 → model_compose-0.0.9}/src/mindor/core/listener/engine/__init__.py +0 -0
  60. {model_compose-0.0.7 → model_compose-0.0.9}/src/mindor/core/runtime/__init__.py +0 -0
  61. {model_compose-0.0.7 → model_compose-0.0.9}/src/mindor/core/runtime/env.py +0 -0
  62. {model_compose-0.0.7 → model_compose-0.0.9}/src/mindor/core/utils/__init__.py +0 -0
  63. {model_compose-0.0.7 → model_compose-0.0.9}/src/mindor/core/utils/expiring.py +0 -0
  64. {model_compose-0.0.7 → model_compose-0.0.9}/src/mindor/core/utils/http_headers.py +0 -0
  65. {model_compose-0.0.7 → model_compose-0.0.9}/src/mindor/core/utils/resources.py +0 -0
  66. {model_compose-0.0.7 → model_compose-0.0.9}/src/mindor/core/utils/workqueue.py +0 -0
  67. {model_compose-0.0.7 → model_compose-0.0.9}/src/mindor/core/workflow/__init__.py +0 -0
  68. {model_compose-0.0.7 → model_compose-0.0.9}/src/mindor/core/workflow/context.py +0 -0
  69. {model_compose-0.0.7 → model_compose-0.0.9}/src/mindor/core/workflow/job.py +0 -0
  70. {model_compose-0.0.7 → model_compose-0.0.9}/src/mindor/dsl/__init__.py +0 -0
  71. {model_compose-0.0.7 → model_compose-0.0.9}/src/mindor/dsl/loader.py +0 -0
  72. {model_compose-0.0.7 → model_compose-0.0.9}/src/mindor/dsl/schema/__init__.py +0 -0
  73. {model_compose-0.0.7 → model_compose-0.0.9}/src/mindor/dsl/schema/action/__init__.py +0 -0
  74. {model_compose-0.0.7 → model_compose-0.0.9}/src/mindor/dsl/schema/action/action.py +0 -0
  75. {model_compose-0.0.7 → model_compose-0.0.9}/src/mindor/dsl/schema/action/impl/__init__.py +0 -0
  76. {model_compose-0.0.7 → model_compose-0.0.9}/src/mindor/dsl/schema/action/impl/http_server.py +0 -0
  77. {model_compose-0.0.7 → model_compose-0.0.9}/src/mindor/dsl/schema/action/impl/mcp_client.py +0 -0
  78. {model_compose-0.0.7 → model_compose-0.0.9}/src/mindor/dsl/schema/action/impl/mcp_server.py +0 -0
  79. {model_compose-0.0.7 → model_compose-0.0.9}/src/mindor/dsl/schema/component/__init__.py +0 -0
  80. {model_compose-0.0.7 → model_compose-0.0.9}/src/mindor/dsl/schema/component/component.py +0 -0
  81. {model_compose-0.0.7 → model_compose-0.0.9}/src/mindor/dsl/schema/component/impl/__init__.py +0 -0
  82. {model_compose-0.0.7 → model_compose-0.0.9}/src/mindor/dsl/schema/component/impl/common.py +0 -0
  83. {model_compose-0.0.7 → model_compose-0.0.9}/src/mindor/dsl/schema/component/impl/http_server.py +0 -0
  84. {model_compose-0.0.7 → model_compose-0.0.9}/src/mindor/dsl/schema/component/impl/mcp_client.py +0 -0
  85. {model_compose-0.0.7 → model_compose-0.0.9}/src/mindor/dsl/schema/component/impl/mcp_server.py +0 -0
  86. {model_compose-0.0.7 → model_compose-0.0.9}/src/mindor/dsl/schema/component/impl/types.py +0 -0
  87. {model_compose-0.0.7 → model_compose-0.0.9}/src/mindor/dsl/schema/controller/__init__.py +0 -0
  88. {model_compose-0.0.7 → model_compose-0.0.9}/src/mindor/dsl/schema/controller/controller.py +0 -0
  89. {model_compose-0.0.7 → model_compose-0.0.9}/src/mindor/dsl/schema/controller/impl/__init__.py +0 -0
  90. {model_compose-0.0.7 → model_compose-0.0.9}/src/mindor/dsl/schema/controller/impl/common.py +0 -0
  91. {model_compose-0.0.7 → model_compose-0.0.9}/src/mindor/dsl/schema/controller/impl/http_server.py +0 -0
  92. {model_compose-0.0.7 → model_compose-0.0.9}/src/mindor/dsl/schema/controller/impl/mcp_server.py +0 -0
  93. {model_compose-0.0.7 → model_compose-0.0.9}/src/mindor/dsl/schema/controller/impl/types.py +0 -0
  94. {model_compose-0.0.7/src/mindor/core → model_compose-0.0.9/src/mindor/dsl/schema}/listener/__init__.py +0 -0
  95. {model_compose-0.0.7 → model_compose-0.0.9}/src/mindor/dsl/schema/listener/impl/__init__.py +0 -0
  96. {model_compose-0.0.7 → model_compose-0.0.9}/src/mindor/dsl/schema/listener/impl/types.py +0 -0
  97. {model_compose-0.0.7 → model_compose-0.0.9}/src/mindor/dsl/schema/listener/listener.py +0 -0
  98. {model_compose-0.0.7 → model_compose-0.0.9}/src/mindor/dsl/schema/runtime/__init__.py +0 -0
  99. {model_compose-0.0.7 → model_compose-0.0.9}/src/mindor/dsl/schema/runtime/impl/__init__.py +0 -0
  100. {model_compose-0.0.7 → model_compose-0.0.9}/src/mindor/dsl/schema/runtime/impl/common.py +0 -0
  101. {model_compose-0.0.7 → model_compose-0.0.9}/src/mindor/dsl/schema/runtime/impl/docker.py +0 -0
  102. {model_compose-0.0.7 → model_compose-0.0.9}/src/mindor/dsl/schema/runtime/impl/types.py +0 -0
  103. {model_compose-0.0.7 → model_compose-0.0.9}/src/mindor/dsl/schema/runtime/runtime.py +0 -0
  104. {model_compose-0.0.7 → model_compose-0.0.9}/src/model_compose.egg-info/dependency_links.txt +0 -0
  105. {model_compose-0.0.7 → model_compose-0.0.9}/src/model_compose.egg-info/entry_points.txt +0 -0
  106. {model_compose-0.0.7 → model_compose-0.0.9}/src/model_compose.egg-info/requires.txt +0 -0
  107. {model_compose-0.0.7 → model_compose-0.0.9}/src/model_compose.egg-info/top_level.txt +0 -0
  108. {model_compose-0.0.7 → model_compose-0.0.9}/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.7
3
+ Version: 0.0.9
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "model-compose"
3
- version = "0.0.7"
3
+ version = "0.0.9"
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" }
@@ -1,4 +1,3 @@
1
1
  from .base import *
2
- from .stream import *
3
2
  from .http_server import *
4
3
  from .http_client import *
@@ -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(ABC):
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
- 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
-
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
- 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()
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
- 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
63
+ await super()._stop()
118
64
 
119
65
  @abstractmethod
120
66
  async def _run(self, action: ActionConfig, context: ComponentContext) -> Any:
@@ -0,0 +1,120 @@
1
+ from typing import Type, Union, Literal, Optional, Dict, List, Tuple, Set, Annotated, AsyncIterator, Any
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
5
+ from mindor.core.utils.http_client import HttpClient
6
+ from mindor.core.utils.time import parse_duration
7
+ from .base import BaseComponent, ComponentType, ComponentEngineMap
8
+ from .context import ComponentContext
9
+ from datetime import datetime, timezone
10
+ import asyncio
11
+
12
+ class HttpClientPollingCompletion:
13
+ def __init__(self, base_url: Optional[str], config: HttpClientCompletionConfig):
14
+ self.base_url: Optional[str] = base_url
15
+ self.config: HttpClientCompletionConfig = config
16
+ self.client: HttpClient = HttpClient()
17
+
18
+ async def run(self, context: ComponentContext) -> Any:
19
+ url = self._resolve_request_url(context)
20
+ method = context.render_template(self.config.method)
21
+ params = context.render_template(self.config.params)
22
+ body = context.render_template(self.config.body)
23
+ headers = context.render_template(self.config.headers)
24
+
25
+ interval = parse_duration(self.config.interval) if self.config.interval else 5.0
26
+ timeout = parse_duration(self.config.timeout) if self.config.timeout else 300
27
+ deadline = datetime.now(timezone.utc) + timeout
28
+
29
+ await asyncio.sleep(interval.total_seconds())
30
+
31
+ while datetime.now(timezone.utc) < deadline:
32
+ response = await self.client.request(url, method, params, body, headers)
33
+
34
+ context.register_source("response", response)
35
+ status = context.render_template(self.config.status) if self.config.status else response["status"]
36
+
37
+ success_when = context.render_template(self.config.success_when) if self.config.success_when else "completed"
38
+ if success_when and status == success_when:
39
+ return response
40
+
41
+ fail_when = context.render_template(self.config.fail_when) if self.config.fail_when else "failed"
42
+ if fail_when and status == fail_when:
43
+ raise RuntimeError(f"Polling failed: status='{status}'.")
44
+
45
+ await asyncio.sleep(interval.total_seconds())
46
+
47
+ raise TimeoutError(f"Polling timed out after {timeout}")
48
+
49
+ def _resolve_request_url(self, context: ComponentContext) -> str:
50
+ if self.base_url and self.config.path:
51
+ return context.render_template(self.base_url) + context.render_template(self.config.path)
52
+
53
+ return context.render_template(self.config.endpoint)
54
+
55
+ class HttpClientCallbackCompletion:
56
+ def __init__(self, config: HttpClientCompletionConfig):
57
+ self.config: HttpClientCompletionConfig = config
58
+
59
+ async def run(self, context: ComponentContext) -> Any:
60
+ callback_id = context.render_template(self.config.wait_for)
61
+ future: asyncio.Future = asyncio.get_event_loop().create_future()
62
+ HttpCallbackListener.register_pending_future(callback_id, future)
63
+
64
+ return await future
65
+
66
+ class HttpClientAction:
67
+ def __init__(self, base_url: Optional[str], config: HttpClientActionConfig):
68
+ self.base_url: Optional[str] = base_url
69
+ self.config: HttpClientActionConfig = config
70
+ self.client: HttpClient = HttpClient()
71
+
72
+ async def run(self, context: ComponentContext) -> Any:
73
+ url = self._resolve_request_url(context)
74
+ method = context.render_template(self.config.method)
75
+ params = context.render_template(self.config.params)
76
+ body = context.render_template(self.config.body)
77
+ headers = context.render_template(self.config.headers)
78
+
79
+ response = await self.client.request(url, method, params, body, headers)
80
+
81
+ if response:
82
+ context.register_source("response", response)
83
+
84
+ if self.config.completion:
85
+ result = await self._handle_completion(self.config.completion, context)
86
+
87
+ if result:
88
+ context.register_source("result", result)
89
+
90
+ return context.render_template(self.config.output) if self.config.output else (result or response)
91
+
92
+ def _resolve_request_url(self, context: ComponentContext) -> str:
93
+ if self.base_url and self.config.path:
94
+ return context.render_template(self.base_url) + context.render_template(self.config.path)
95
+
96
+ return context.render_template(self.config.endpoint)
97
+
98
+ async def _handle_completion(self, completion: HttpClientCompletionConfig, context: ComponentContext) -> Any:
99
+ if completion.type == "polling":
100
+ return await HttpClientPollingCompletion(self.base_url, completion).run(context)
101
+
102
+ if completion.type == "callback":
103
+ return await HttpClientCallbackCompletion(completion).run(context)
104
+
105
+ return None
106
+
107
+ class HttpClientComponent(BaseComponent):
108
+ def __init__(self, id: str, config: HttpClientComponentConfig, env: Dict[str, str], daemon: bool):
109
+ super().__init__(id, config, env, daemon)
110
+
111
+ async def _serve(self) -> None:
112
+ pass
113
+
114
+ async def _shutdown(self) -> None:
115
+ pass
116
+
117
+ async def _run(self, action: ActionConfig, context: ComponentContext) -> Any:
118
+ return await HttpClientAction(self.config.base_url, action).run(context)
119
+
120
+ ComponentEngineMap[ComponentType.HTTP_CLIENT] = HttpClientComponent
@@ -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 Thread, Lock
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(ABC):
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
- self.started: bool = False
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
- if not self.thread:
101
- self.daemon_task = asyncio.create_task(self._serve())
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
- self.started = False
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]] = {}
@@ -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,2 @@
1
+ from .gateway import *
2
+ from .engine import HttpTunnelGateway
@@ -0,0 +1,2 @@
1
+ from .base import *
2
+ from .http_tunnel import *
@@ -0,0 +1,12 @@
1
+ from typing import Type, Union, Literal, Optional, Dict, List, Tuple, Set, Annotated, Any
2
+ from mindor.dsl.schema.gateway import GatewayConfig, GatewayType
3
+ from mindor.core.services import AsyncService
4
+
5
+ class BaseGateway(AsyncService):
6
+ def __init__(self, config: GatewayConfig, env: Dict[str, str], daemon: bool):
7
+ super().__init__(daemon)
8
+
9
+ self.config: GatewayConfig = config
10
+ self.env: Dict[str, str] = env
11
+
12
+ GatewayEngineMap: Dict[GatewayType, Type[BaseGateway]] = {}
@@ -0,0 +1,9 @@
1
+ from typing import Type, Union, Literal, Optional, Dict, List, Tuple, Set, Annotated, Callable, Iterator, Any
2
+ from mindor.dsl.schema.gateway import HttpTunnelGatewayConfig
3
+ from .base import BaseGateway, GatewayType, GatewayEngineMap
4
+
5
+ class HttpTunnelGateway(BaseGateway):
6
+ def __init__(self, config: HttpTunnelGatewayConfig, env: Dict[str, str], daemon: bool):
7
+ super().__init__(config, env, daemon)
8
+
9
+ GatewayEngineMap[GatewayType.HTTP_TUNNEL] = HttpTunnelGateway
@@ -0,0 +1,9 @@
1
+ from typing import Type, Union, Literal, Optional, Dict, List, Tuple, Set, Annotated, Any
2
+ from mindor.dsl.schema.gateway import GatewayConfig
3
+ from .engine import BaseGateway, GatewayEngineMap
4
+
5
+ def create_gateway(config: GatewayConfig, env: Dict[str, str], daemon: bool) -> BaseGateway:
6
+ try:
7
+ return GatewayEngineMap[config.type](config, env, daemon)
8
+ except KeyError:
9
+ raise ValueError(f"Unsupported gateway type: {config.type}")
@@ -0,0 +1,2 @@
1
+ from .listener import *
2
+ from .engine import HttpCallbackListener
@@ -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: Optional[str]):
18
+ self.body: Optional[Any] = body
19
+ self.query: Optional[Dict[str, str]] = query
20
+ self.bulk: bool = bulk
21
+ self.item: Optional[str] = item
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) if self.item 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)
72
+
73
+ for item in context.items():
74
+ callback_id = item.render_template(callback.identify_by)
75
+ future: asyncio.Future = self._get_pending_future(callback_id)
76
+
77
+ if future:
78
+ future.set_result(item.render_template(callback.result) if callback.result else item.body)
79
+ self._remove_pending_future(callback_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: ControllerConfig, env: Dict[str, str]) -> BaseListener:
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,2 @@
1
+ from .async_service import *
2
+