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.
Files changed (95) hide show
  1. {model_compose-0.0.6/src/model_compose.egg-info → model_compose-0.0.8}/PKG-INFO +1 -1
  2. {model_compose-0.0.6 → model_compose-0.0.8}/pyproject.toml +1 -1
  3. {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/core/component/engine/__init__.py +0 -1
  4. {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/core/component/engine/base.py +7 -61
  5. {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/core/component/engine/http_client.py +24 -6
  6. {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/core/compose/manager.py +1 -1
  7. {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/core/controller/controller.py +3 -2
  8. {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/core/controller/engine/base.py +24 -57
  9. {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/core/controller/engine/http_server.py +45 -38
  10. {model_compose-0.0.6 → 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.6 → 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.6 → model_compose-0.0.8}/src/mindor/core/utils/template.py +12 -9
  18. {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/core/workflow/job.py +2 -2
  19. {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/core/workflow/workflow.py +14 -12
  20. {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/dsl/schema/action/impl/common.py +1 -1
  21. {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/dsl/schema/action/impl/http_client.py +2 -2
  22. {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/dsl/schema/component/impl/http_client.py +1 -1
  23. {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/dsl/schema/compose.py +12 -2
  24. {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/dsl/schema/listener/impl/common.py +1 -0
  25. {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/dsl/schema/listener/impl/http_callback.py +4 -2
  26. {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/dsl/schema/workflow.py +4 -4
  27. {model_compose-0.0.6 → model_compose-0.0.8/src/model_compose.egg-info}/PKG-INFO +1 -1
  28. {model_compose-0.0.6 → model_compose-0.0.8}/src/model_compose.egg-info/SOURCES.txt +2 -0
  29. model_compose-0.0.6/src/mindor/core/listener/engine/base.py +0 -13
  30. model_compose-0.0.6/src/mindor/core/listener/engine/http_callback.py +0 -37
  31. model_compose-0.0.6/src/mindor/dsl/schema/listener/__init__.py +0 -1
  32. {model_compose-0.0.6 → model_compose-0.0.8}/LICENSE +0 -0
  33. {model_compose-0.0.6 → model_compose-0.0.8}/README.md +0 -0
  34. {model_compose-0.0.6 → model_compose-0.0.8}/setup.cfg +0 -0
  35. {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/__init__.py +0 -0
  36. {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/cli/__init__.py +0 -0
  37. {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/cli/compose.py +0 -0
  38. {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/core/__init__.py +0 -0
  39. {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/core/component/__init__.py +0 -0
  40. {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/core/component/component.py +0 -0
  41. {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/core/component/engine/context.py +0 -0
  42. {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/core/component/engine/http_server.py +0 -0
  43. {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/core/compose/__init__.py +0 -0
  44. {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/core/compose/compose.py +0 -0
  45. {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/core/controller/__init__.py +0 -0
  46. {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/core/controller/engine/__init__.py +0 -0
  47. {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/core/listener/engine/__init__.py +0 -0
  48. {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/core/runtime/__init__.py +0 -0
  49. {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/core/runtime/env.py +0 -0
  50. {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/core/utils/__init__.py +0 -0
  51. {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/core/utils/expiring.py +0 -0
  52. {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/core/utils/http_headers.py +0 -0
  53. {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/core/utils/resources.py +0 -0
  54. {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/core/utils/workqueue.py +0 -0
  55. {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/core/workflow/__init__.py +0 -0
  56. {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/core/workflow/context.py +0 -0
  57. {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/dsl/__init__.py +0 -0
  58. {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/dsl/loader.py +0 -0
  59. {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/dsl/schema/__init__.py +0 -0
  60. {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/dsl/schema/action/__init__.py +0 -0
  61. {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/dsl/schema/action/action.py +0 -0
  62. {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/dsl/schema/action/impl/__init__.py +0 -0
  63. {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/dsl/schema/action/impl/http_server.py +0 -0
  64. {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/dsl/schema/action/impl/mcp_client.py +0 -0
  65. {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/dsl/schema/action/impl/mcp_server.py +0 -0
  66. {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/dsl/schema/component/__init__.py +0 -0
  67. {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/dsl/schema/component/component.py +0 -0
  68. {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/dsl/schema/component/impl/__init__.py +0 -0
  69. {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/dsl/schema/component/impl/common.py +0 -0
  70. {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/dsl/schema/component/impl/http_server.py +0 -0
  71. {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/dsl/schema/component/impl/mcp_client.py +0 -0
  72. {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/dsl/schema/component/impl/mcp_server.py +0 -0
  73. {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/dsl/schema/component/impl/types.py +0 -0
  74. {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/dsl/schema/controller/__init__.py +0 -0
  75. {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/dsl/schema/controller/controller.py +0 -0
  76. {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/dsl/schema/controller/impl/__init__.py +0 -0
  77. {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/dsl/schema/controller/impl/common.py +0 -0
  78. {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/dsl/schema/controller/impl/http_server.py +0 -0
  79. {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/dsl/schema/controller/impl/mcp_server.py +0 -0
  80. {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/dsl/schema/controller/impl/types.py +0 -0
  81. {model_compose-0.0.6/src/mindor/core → model_compose-0.0.8/src/mindor/dsl/schema}/listener/__init__.py +0 -0
  82. {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/dsl/schema/listener/impl/__init__.py +0 -0
  83. {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/dsl/schema/listener/impl/types.py +0 -0
  84. {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/dsl/schema/listener/listener.py +0 -0
  85. {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/dsl/schema/runtime/__init__.py +0 -0
  86. {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/dsl/schema/runtime/impl/__init__.py +0 -0
  87. {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/dsl/schema/runtime/impl/common.py +0 -0
  88. {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/dsl/schema/runtime/impl/docker.py +0 -0
  89. {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/dsl/schema/runtime/impl/types.py +0 -0
  90. {model_compose-0.0.6 → model_compose-0.0.8}/src/mindor/dsl/schema/runtime/runtime.py +0 -0
  91. {model_compose-0.0.6 → model_compose-0.0.8}/src/model_compose.egg-info/dependency_links.txt +0 -0
  92. {model_compose-0.0.6 → model_compose-0.0.8}/src/model_compose.egg-info/entry_points.txt +0 -0
  93. {model_compose-0.0.6 → model_compose-0.0.8}/src/model_compose.egg-info/requires.txt +0 -0
  94. {model_compose-0.0.6 → model_compose-0.0.8}/src/model_compose.egg-info/top_level.txt +0 -0
  95. {model_compose-0.0.6 → 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.6
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.6"
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,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, 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
- def __init__(self, session: aiohttp.ClientSession, stream: aiohttp.StreamReader, filename: Optional[str], content_type: Optional[str]):
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
- 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()
@@ -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, filename, content_type)
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 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,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[Dict[str, Any]] = None
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
- if state.status == "failed":
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 JSONResponse(content=TaskResult.from_instance(state).model_dump(exclude_none=True))
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
- if state.status in [ "pending", "processing" ]:
85
- raise HTTPException(status_code=202, detail="Task is still in progress.")
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,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}")