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.
Files changed (95) hide show
  1. {model_compose-0.0.7/src/model_compose.egg-info → model_compose-0.0.8}/PKG-INFO +1 -1
  2. {model_compose-0.0.7 → model_compose-0.0.8}/pyproject.toml +1 -1
  3. {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/core/component/engine/__init__.py +0 -1
  4. {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/core/component/engine/base.py +7 -61
  5. {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/core/component/engine/http_client.py +22 -4
  6. {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/core/compose/manager.py +1 -1
  7. {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/core/controller/controller.py +3 -2
  8. {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/core/controller/engine/base.py +24 -57
  9. {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/core/controller/engine/http_server.py +3 -2
  10. {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/core/controller/engine/mcp_server.py +3 -2
  11. model_compose-0.0.8/src/mindor/core/listener/__init__.py +2 -0
  12. model_compose-0.0.8/src/mindor/core/listener/engine/base.py +29 -0
  13. model_compose-0.0.8/src/mindor/core/listener/engine/http_callback.py +121 -0
  14. {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/core/listener/listener.py +2 -2
  15. model_compose-0.0.8/src/mindor/core/services/__init__.py +2 -0
  16. model_compose-0.0.8/src/mindor/core/services/async_service.py +66 -0
  17. {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/core/utils/template.py +12 -9
  18. {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/core/workflow/workflow.py +8 -6
  19. {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/dsl/schema/action/impl/common.py +1 -1
  20. {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/dsl/schema/action/impl/http_client.py +2 -2
  21. {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/dsl/schema/component/impl/http_client.py +1 -1
  22. {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/dsl/schema/compose.py +12 -2
  23. {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/dsl/schema/listener/impl/common.py +1 -0
  24. {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/dsl/schema/listener/impl/http_callback.py +4 -2
  25. {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/dsl/schema/workflow.py +4 -4
  26. {model_compose-0.0.7 → model_compose-0.0.8/src/model_compose.egg-info}/PKG-INFO +1 -1
  27. {model_compose-0.0.7 → model_compose-0.0.8}/src/model_compose.egg-info/SOURCES.txt +2 -0
  28. model_compose-0.0.7/src/mindor/core/listener/engine/base.py +0 -13
  29. model_compose-0.0.7/src/mindor/core/listener/engine/http_callback.py +0 -37
  30. model_compose-0.0.7/src/mindor/dsl/schema/listener/__init__.py +0 -1
  31. {model_compose-0.0.7 → model_compose-0.0.8}/LICENSE +0 -0
  32. {model_compose-0.0.7 → model_compose-0.0.8}/README.md +0 -0
  33. {model_compose-0.0.7 → model_compose-0.0.8}/setup.cfg +0 -0
  34. {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/__init__.py +0 -0
  35. {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/cli/__init__.py +0 -0
  36. {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/cli/compose.py +0 -0
  37. {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/core/__init__.py +0 -0
  38. {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/core/component/__init__.py +0 -0
  39. {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/core/component/component.py +0 -0
  40. {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/core/component/engine/context.py +0 -0
  41. {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/core/component/engine/http_server.py +0 -0
  42. {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/core/compose/__init__.py +0 -0
  43. {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/core/compose/compose.py +0 -0
  44. {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/core/controller/__init__.py +0 -0
  45. {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/core/controller/engine/__init__.py +0 -0
  46. {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/core/listener/engine/__init__.py +0 -0
  47. {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/core/runtime/__init__.py +0 -0
  48. {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/core/runtime/env.py +0 -0
  49. {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/core/utils/__init__.py +0 -0
  50. {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/core/utils/expiring.py +0 -0
  51. {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/core/utils/http_headers.py +0 -0
  52. {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/core/utils/resources.py +0 -0
  53. {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/core/utils/workqueue.py +0 -0
  54. {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/core/workflow/__init__.py +0 -0
  55. {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/core/workflow/context.py +0 -0
  56. {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/core/workflow/job.py +0 -0
  57. {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/dsl/__init__.py +0 -0
  58. {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/dsl/loader.py +0 -0
  59. {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/dsl/schema/__init__.py +0 -0
  60. {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/dsl/schema/action/__init__.py +0 -0
  61. {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/dsl/schema/action/action.py +0 -0
  62. {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/dsl/schema/action/impl/__init__.py +0 -0
  63. {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/dsl/schema/action/impl/http_server.py +0 -0
  64. {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/dsl/schema/action/impl/mcp_client.py +0 -0
  65. {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/dsl/schema/action/impl/mcp_server.py +0 -0
  66. {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/dsl/schema/component/__init__.py +0 -0
  67. {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/dsl/schema/component/component.py +0 -0
  68. {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/dsl/schema/component/impl/__init__.py +0 -0
  69. {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/dsl/schema/component/impl/common.py +0 -0
  70. {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/dsl/schema/component/impl/http_server.py +0 -0
  71. {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/dsl/schema/component/impl/mcp_client.py +0 -0
  72. {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/dsl/schema/component/impl/mcp_server.py +0 -0
  73. {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/dsl/schema/component/impl/types.py +0 -0
  74. {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/dsl/schema/controller/__init__.py +0 -0
  75. {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/dsl/schema/controller/controller.py +0 -0
  76. {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/dsl/schema/controller/impl/__init__.py +0 -0
  77. {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/dsl/schema/controller/impl/common.py +0 -0
  78. {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/dsl/schema/controller/impl/http_server.py +0 -0
  79. {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/dsl/schema/controller/impl/mcp_server.py +0 -0
  80. {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/dsl/schema/controller/impl/types.py +0 -0
  81. {model_compose-0.0.7/src/mindor/core → model_compose-0.0.8/src/mindor/dsl/schema}/listener/__init__.py +0 -0
  82. {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/dsl/schema/listener/impl/__init__.py +0 -0
  83. {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/dsl/schema/listener/impl/types.py +0 -0
  84. {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/dsl/schema/listener/listener.py +0 -0
  85. {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/dsl/schema/runtime/__init__.py +0 -0
  86. {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/dsl/schema/runtime/impl/__init__.py +0 -0
  87. {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/dsl/schema/runtime/impl/common.py +0 -0
  88. {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/dsl/schema/runtime/impl/docker.py +0 -0
  89. {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/dsl/schema/runtime/impl/types.py +0 -0
  90. {model_compose-0.0.7 → model_compose-0.0.8}/src/mindor/dsl/schema/runtime/runtime.py +0 -0
  91. {model_compose-0.0.7 → model_compose-0.0.8}/src/model_compose.egg-info/dependency_links.txt +0 -0
  92. {model_compose-0.0.7 → model_compose-0.0.8}/src/model_compose.egg-info/entry_points.txt +0 -0
  93. {model_compose-0.0.7 → model_compose-0.0.8}/src/model_compose.egg-info/requires.txt +0 -0
  94. {model_compose-0.0.7 → model_compose-0.0.8}/src/model_compose.egg-info/top_level.txt +0 -0
  95. {model_compose-0.0.7 → model_compose-0.0.8}/tests/test_cli.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: model-compose
3
- Version: 0.0.7
3
+ Version: 0.0.8
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.8"
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:
@@ -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, HttpClientActionConfig
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, ActionConfig
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
- return context.render_template(self.config.output) if self.config.output else response
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 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 .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_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: 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
+
@@ -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
- \.([^\|\}]+) # path: key, key.path[0], etc.
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) -> str:
32
- def replace(match: re.Match) -> str:
33
- key, path, default = match.group(1), match.group(2), match.group(3)
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
- return str(value if value is not None else (default or ""))
38
+ if text != placeholder:
39
+ return text.replace(placeholder, value)
40
+ return value
38
41
  except Exception:
39
42
  return default or ""
40
- return self.patterns["placeholder"].sub(replace, text)
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 = completed_job_output
111
- # output.update(completed_job_output)
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 not self.config.output:
140
- return output
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[Dict[str, Any]] = Field(default_factory=dict)
5
+ output: Optional[Any] = None
6
6
  default: bool = False
@@ -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
@@ -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:
@@ -5,3 +5,4 @@ from .types import ListenerType
5
5
  class CommonListenerConfig(BaseModel):
6
6
  type: ListenerType
7
7
  runtime: Literal[ "docker", "native" ] = "native"
8
+ max_concurrent_count: int = 0
@@ -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" ] = "GET"
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[Dict[str, Any]] = Field(default_factory=dict)
10
- output: Optional[Dict[str, Any]] = Field(default_factory=dict)
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[Dict[str, Any]] = Field(default_factory=dict)
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):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: model-compose
3
- Version: 0.0.7
3
+ Version: 0.0.8
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
@@ -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