model-compose 0.0.4__tar.gz → 0.0.6__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 (91) hide show
  1. {model_compose-0.0.4/src/model_compose.egg-info → model_compose-0.0.6}/PKG-INFO +1 -1
  2. {model_compose-0.0.4 → model_compose-0.0.6}/pyproject.toml +1 -1
  3. {model_compose-0.0.4 → model_compose-0.0.6}/src/mindor/core/component/engine/__init__.py +1 -1
  4. {model_compose-0.0.4 → model_compose-0.0.6}/src/mindor/core/component/engine/base.py +4 -4
  5. {model_compose-0.0.4 → model_compose-0.0.6}/src/mindor/core/component/engine/context.py +5 -1
  6. model_compose-0.0.6/src/mindor/core/component/engine/http_client.py +123 -0
  7. {model_compose-0.0.4 → model_compose-0.0.6}/src/mindor/core/component/engine/http_server.py +1 -1
  8. {model_compose-0.0.4 → model_compose-0.0.6}/src/mindor/core/controller/engine/base.py +2 -2
  9. {model_compose-0.0.4 → model_compose-0.0.6}/src/mindor/core/controller/engine/http_server.py +51 -11
  10. model_compose-0.0.6/src/mindor/core/listener/__init__.py +1 -0
  11. model_compose-0.0.6/src/mindor/core/listener/engine/__init__.py +2 -0
  12. model_compose-0.0.6/src/mindor/core/listener/engine/base.py +13 -0
  13. model_compose-0.0.6/src/mindor/core/listener/engine/http_callback.py +37 -0
  14. model_compose-0.0.6/src/mindor/core/listener/listener.py +9 -0
  15. model_compose-0.0.6/src/mindor/core/utils/http_headers.py +24 -0
  16. model_compose-0.0.6/src/mindor/core/utils/resources.py +28 -0
  17. {model_compose-0.0.4 → model_compose-0.0.6}/src/mindor/core/workflow/context.py +5 -1
  18. {model_compose-0.0.4 → model_compose-0.0.6}/src/mindor/core/workflow/job.py +8 -6
  19. {model_compose-0.0.4 → model_compose-0.0.6}/src/mindor/core/workflow/workflow.py +6 -5
  20. {model_compose-0.0.4 → model_compose-0.0.6}/src/mindor/dsl/schema/action/impl/http_client.py +22 -6
  21. {model_compose-0.0.4 → model_compose-0.0.6}/src/mindor/dsl/schema/action/impl/http_server.py +2 -2
  22. {model_compose-0.0.4 → model_compose-0.0.6}/src/mindor/dsl/schema/action/impl/mcp_client.py +2 -2
  23. {model_compose-0.0.4 → model_compose-0.0.6}/src/mindor/dsl/schema/action/impl/mcp_server.py +3 -3
  24. {model_compose-0.0.4 → model_compose-0.0.6}/src/mindor/dsl/schema/component/component.py +1 -1
  25. {model_compose-0.0.4 → model_compose-0.0.6}/src/mindor/dsl/schema/component/impl/common.py +1 -1
  26. {model_compose-0.0.4 → model_compose-0.0.6}/src/mindor/dsl/schema/component/impl/http_client.py +6 -6
  27. {model_compose-0.0.4 → model_compose-0.0.6}/src/mindor/dsl/schema/component/impl/http_server.py +2 -2
  28. {model_compose-0.0.4 → model_compose-0.0.6}/src/mindor/dsl/schema/component/impl/mcp_client.py +3 -3
  29. {model_compose-0.0.4 → model_compose-0.0.6}/src/mindor/dsl/schema/component/impl/mcp_server.py +2 -2
  30. {model_compose-0.0.4 → model_compose-0.0.6}/src/mindor/dsl/schema/controller/controller.py +1 -1
  31. {model_compose-0.0.4 → model_compose-0.0.6}/src/mindor/dsl/schema/controller/impl/http_server.py +2 -2
  32. {model_compose-0.0.4 → model_compose-0.0.6}/src/mindor/dsl/schema/controller/impl/mcp_server.py +2 -2
  33. model_compose-0.0.6/src/mindor/dsl/schema/listener/__init__.py +1 -0
  34. model_compose-0.0.6/src/mindor/dsl/schema/listener/impl/__init__.py +2 -0
  35. model_compose-0.0.6/src/mindor/dsl/schema/listener/impl/common.py +7 -0
  36. model_compose-0.0.6/src/mindor/dsl/schema/listener/impl/http_callback.py +25 -0
  37. model_compose-0.0.6/src/mindor/dsl/schema/listener/impl/types.py +4 -0
  38. model_compose-0.0.6/src/mindor/dsl/schema/listener/listener.py +10 -0
  39. {model_compose-0.0.4 → model_compose-0.0.6}/src/mindor/dsl/schema/runtime/impl/docker.py +2 -2
  40. {model_compose-0.0.4 → model_compose-0.0.6}/src/mindor/dsl/schema/runtime/runtime.py +1 -1
  41. {model_compose-0.0.4 → model_compose-0.0.6/src/model_compose.egg-info}/PKG-INFO +1 -1
  42. {model_compose-0.0.4 → model_compose-0.0.6}/src/model_compose.egg-info/SOURCES.txt +13 -0
  43. model_compose-0.0.4/src/mindor/core/component/engine/http_client.py +0 -87
  44. {model_compose-0.0.4 → model_compose-0.0.6}/LICENSE +0 -0
  45. {model_compose-0.0.4 → model_compose-0.0.6}/README.md +0 -0
  46. {model_compose-0.0.4 → model_compose-0.0.6}/setup.cfg +0 -0
  47. {model_compose-0.0.4 → model_compose-0.0.6}/src/mindor/__init__.py +0 -0
  48. {model_compose-0.0.4 → model_compose-0.0.6}/src/mindor/cli/__init__.py +0 -0
  49. {model_compose-0.0.4 → model_compose-0.0.6}/src/mindor/cli/compose.py +0 -0
  50. {model_compose-0.0.4 → model_compose-0.0.6}/src/mindor/core/__init__.py +0 -0
  51. {model_compose-0.0.4 → model_compose-0.0.6}/src/mindor/core/component/__init__.py +0 -0
  52. {model_compose-0.0.4 → model_compose-0.0.6}/src/mindor/core/component/component.py +0 -0
  53. {model_compose-0.0.4 → model_compose-0.0.6}/src/mindor/core/compose/__init__.py +0 -0
  54. {model_compose-0.0.4 → model_compose-0.0.6}/src/mindor/core/compose/compose.py +0 -0
  55. {model_compose-0.0.4 → model_compose-0.0.6}/src/mindor/core/compose/manager.py +0 -0
  56. {model_compose-0.0.4 → model_compose-0.0.6}/src/mindor/core/controller/__init__.py +0 -0
  57. {model_compose-0.0.4 → model_compose-0.0.6}/src/mindor/core/controller/controller.py +0 -0
  58. {model_compose-0.0.4 → model_compose-0.0.6}/src/mindor/core/controller/engine/__init__.py +0 -0
  59. {model_compose-0.0.4 → model_compose-0.0.6}/src/mindor/core/controller/engine/mcp_server.py +0 -0
  60. {model_compose-0.0.4 → model_compose-0.0.6}/src/mindor/core/runtime/__init__.py +0 -0
  61. {model_compose-0.0.4 → model_compose-0.0.6}/src/mindor/core/runtime/env.py +0 -0
  62. {model_compose-0.0.4 → model_compose-0.0.6}/src/mindor/core/utils/__init__.py +0 -0
  63. {model_compose-0.0.4 → model_compose-0.0.6}/src/mindor/core/utils/expiring.py +0 -0
  64. {model_compose-0.0.4 → model_compose-0.0.6}/src/mindor/core/utils/template.py +0 -0
  65. {model_compose-0.0.4 → model_compose-0.0.6}/src/mindor/core/utils/workqueue.py +0 -0
  66. {model_compose-0.0.4 → model_compose-0.0.6}/src/mindor/core/workflow/__init__.py +0 -0
  67. {model_compose-0.0.4 → model_compose-0.0.6}/src/mindor/dsl/__init__.py +0 -0
  68. {model_compose-0.0.4 → model_compose-0.0.6}/src/mindor/dsl/loader.py +1 -1
  69. {model_compose-0.0.4 → model_compose-0.0.6}/src/mindor/dsl/schema/__init__.py +0 -0
  70. {model_compose-0.0.4 → model_compose-0.0.6}/src/mindor/dsl/schema/action/__init__.py +0 -0
  71. {model_compose-0.0.4 → model_compose-0.0.6}/src/mindor/dsl/schema/action/action.py +1 -1
  72. {model_compose-0.0.4 → model_compose-0.0.6}/src/mindor/dsl/schema/action/impl/__init__.py +0 -0
  73. {model_compose-0.0.4 → model_compose-0.0.6}/src/mindor/dsl/schema/action/impl/common.py +1 -1
  74. {model_compose-0.0.4 → model_compose-0.0.6}/src/mindor/dsl/schema/component/__init__.py +0 -0
  75. {model_compose-0.0.4 → model_compose-0.0.6}/src/mindor/dsl/schema/component/impl/__init__.py +0 -0
  76. {model_compose-0.0.4 → model_compose-0.0.6}/src/mindor/dsl/schema/component/impl/types.py +0 -0
  77. {model_compose-0.0.4 → model_compose-0.0.6}/src/mindor/dsl/schema/compose.py +1 -1
  78. {model_compose-0.0.4 → model_compose-0.0.6}/src/mindor/dsl/schema/controller/__init__.py +0 -0
  79. {model_compose-0.0.4 → model_compose-0.0.6}/src/mindor/dsl/schema/controller/impl/__init__.py +0 -0
  80. {model_compose-0.0.4 → model_compose-0.0.6}/src/mindor/dsl/schema/controller/impl/common.py +1 -1
  81. {model_compose-0.0.4 → model_compose-0.0.6}/src/mindor/dsl/schema/controller/impl/types.py +0 -0
  82. {model_compose-0.0.4 → model_compose-0.0.6}/src/mindor/dsl/schema/runtime/__init__.py +0 -0
  83. {model_compose-0.0.4 → model_compose-0.0.6}/src/mindor/dsl/schema/runtime/impl/__init__.py +0 -0
  84. {model_compose-0.0.4 → model_compose-0.0.6}/src/mindor/dsl/schema/runtime/impl/common.py +1 -1
  85. {model_compose-0.0.4 → model_compose-0.0.6}/src/mindor/dsl/schema/runtime/impl/types.py +0 -0
  86. {model_compose-0.0.4 → model_compose-0.0.6}/src/mindor/dsl/schema/workflow.py +1 -1
  87. {model_compose-0.0.4 → model_compose-0.0.6}/src/model_compose.egg-info/dependency_links.txt +0 -0
  88. {model_compose-0.0.4 → model_compose-0.0.6}/src/model_compose.egg-info/entry_points.txt +0 -0
  89. {model_compose-0.0.4 → model_compose-0.0.6}/src/model_compose.egg-info/requires.txt +0 -0
  90. {model_compose-0.0.4 → model_compose-0.0.6}/src/model_compose.egg-info/top_level.txt +0 -0
  91. {model_compose-0.0.4 → model_compose-0.0.6}/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.4
3
+ Version: 0.0.6
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.4"
3
+ version = "0.0.6"
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,4 @@
1
1
  from .base import *
2
+ from .stream import *
2
3
  from .http_server import *
3
4
  from .http_client import *
4
-
@@ -1,5 +1,5 @@
1
- from abc import ABC, abstractmethod
2
1
  from typing import Type, Union, Literal, Optional, Dict, List, Tuple, Set, Annotated, Callable, Any
2
+ 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
@@ -78,9 +78,9 @@ class BaseComponent(ABC):
78
78
  if self.daemon_task:
79
79
  await self.daemon_task
80
80
 
81
- async def run(self, action_id: Union[str, None], input: Dict[str, Any]) -> Dict[str, Any]:
81
+ async def run(self, action_id: Union[str, None], call_id: str, input: Dict[str, Any]) -> Dict[str, Any]:
82
82
  _, action = ActionResolver(self.config.actions).resolve(action_id)
83
- context = ComponentContext(input, self.env)
83
+ context = ComponentContext(call_id, input, self.env)
84
84
 
85
85
  if self.queue:
86
86
  return await (await self.queue.schedule(action, context))
@@ -117,7 +117,7 @@ class BaseComponent(ABC):
117
117
  pass
118
118
 
119
119
  @abstractmethod
120
- async def _run(self, action: ActionConfig, context: ComponentContext) -> Dict[str, Any]:
120
+ async def _run(self, action: ActionConfig, context: ComponentContext) -> Any:
121
121
  pass
122
122
 
123
123
  ComponentEngineMap: Dict[ComponentType, Type[BaseComponent]] = {}
@@ -2,9 +2,11 @@ from typing import Type, Union, Literal, Optional, Dict, List, Tuple, Set, Annot
2
2
  from mindor.core.utils.template import TemplateRenderer
3
3
 
4
4
  class ComponentContext:
5
- def __init__(self, input: Dict[str, Any], env: Dict[str, str]):
5
+ def __init__(self, call_id: str, input: Dict[str, Any], env: Dict[str, str]):
6
+ self.call_id: str = call_id
6
7
  self.input: Dict[str, Any] = input
7
8
  self.env: Dict[str, str] = env
9
+ self.context: Dict[str, Any] = { "call_id": call_id }
8
10
  self.sources: Dict[str, Any] = {}
9
11
  self.renderer: TemplateRenderer = TemplateRenderer(self._resolve_source)
10
12
 
@@ -21,4 +23,6 @@ class ComponentContext:
21
23
  return self.input
22
24
  if key == "env":
23
25
  return self.env
26
+ if key == "context":
27
+ return self.context
24
28
  raise KeyError(f"Unknown source: {key}")
@@ -0,0 +1,123 @@
1
+ from typing import Type, Union, Literal, Optional, Dict, List, Tuple, Set, Annotated, AsyncIterator, Any
2
+ from mindor.dsl.schema.component import HttpClientComponentConfig, HttpClientActionConfig
3
+ from mindor.core.utils.http_headers import parse_options_header, get_header_value
4
+ from mindor.core.utils.resources import StreamResource
5
+ from .base import BaseComponent, ComponentType, ComponentEngineMap, ActionConfig
6
+ from .context import ComponentContext
7
+
8
+ from urllib.parse import urlencode
9
+ import aiohttp, json
10
+
11
+ class HttpStreamResource(StreamResource):
12
+ def __init__(self, session: aiohttp.ClientSession, stream: aiohttp.StreamReader, filename: Optional[str], content_type: Optional[str]):
13
+ super().__init__(content_type, filename)
14
+
15
+ self.session: aiohttp.ClientSession = session
16
+ self.stream: aiohttp.StreamReader = stream
17
+
18
+ def get_stream(self):
19
+ return self.stream
20
+
21
+ async def close(self):
22
+ await self.session.close()
23
+
24
+ async def _iterate_stream(self) -> AsyncIterator[bytes]:
25
+ _, buffer_size = self.stream.get_read_buffer_limits()
26
+ chunk_size = buffer_size or 65536
27
+
28
+ while not self.stream.at_eof():
29
+ chunk = await self.stream.read(chunk_size)
30
+ if not chunk:
31
+ break
32
+ yield chunk
33
+
34
+ class HttpClientAction:
35
+ def __init__(self, base_url: Optional[str], config: HttpClientActionConfig):
36
+ self.base_url: Optional[str] = base_url
37
+ self.config: HttpClientActionConfig = config
38
+
39
+ async def run(self, context: ComponentContext) -> Any:
40
+ url = self._resolve_request_url(context)
41
+ method = context.render_template(self.config.method)
42
+ params = context.render_template(self.config.params)
43
+ body = context.render_template(self.config.body)
44
+ headers = context.render_template(self.config.headers)
45
+
46
+ response = await self._request(url, method, params, body, headers)
47
+
48
+ if response:
49
+ context.register_source("response", response)
50
+
51
+ return context.render_template(self.config.output) if self.config.output else response
52
+
53
+ async def _request(self, url: str, method: str, params: Optional[Dict[str, Any]], body: Optional[Any], headers: Optional[Dict[str, str]]) -> Any:
54
+ session = aiohttp.ClientSession()
55
+ try:
56
+ response = await session.request(
57
+ method,
58
+ url,
59
+ params=params,
60
+ data=self._serialize_request_body(body, headers) if body is not None else None,
61
+ headers=headers
62
+ )
63
+
64
+ if response.status >= 400:
65
+ raise ValueError(f"Request failed with status {response.status}")
66
+
67
+ content = await self._parse_response_content(session, response)
68
+
69
+ if isinstance(content, HttpStreamResource):
70
+ pass
71
+ else:
72
+ await session.close()
73
+
74
+ return content
75
+ except:
76
+ await session.close()
77
+ raise
78
+
79
+ def _resolve_request_url(self, context: ComponentContext) -> str:
80
+ if self.base_url and self.config.path:
81
+ return context.render_template(str(self.base_url)) + context.render_template(self.config.path)
82
+
83
+ return context.render_template(str(self.config.endpoint))
84
+
85
+ def _serialize_request_body(self, body: Any, headers: Optional[Dict[str, str]]) -> Any:
86
+ content_type, _ = parse_options_header(get_header_value(headers, "Content-Type", ""))
87
+
88
+ if content_type == "application/x-www-form-urlencoded":
89
+ return urlencode(body)
90
+
91
+ if isinstance(body, (str, bytes)):
92
+ return body
93
+
94
+ return json.dumps(body)
95
+
96
+ async def _parse_response_content(self, session: aiohttp.ClientSession, response: aiohttp.ClientResponse) -> Any:
97
+ content_type, _ = parse_options_header(response.headers.get("Content-Type", ""))
98
+
99
+ if content_type == "application/json":
100
+ return await response.json()
101
+
102
+ if content_type.startswith("text/"):
103
+ return await response.text()
104
+
105
+ _, disposition = parse_options_header(response.headers.get("Content-Disposition", ""))
106
+ filename = disposition.get("filename")
107
+
108
+ return HttpStreamResource(session, response.content, filename, content_type)
109
+
110
+ class HttpClientComponent(BaseComponent):
111
+ def __init__(self, id: str, config: HttpClientComponentConfig, env: Dict[str, str], daemon: bool):
112
+ super().__init__(id, config, env, daemon)
113
+
114
+ async def _serve(self) -> None:
115
+ pass
116
+
117
+ async def _shutdown(self) -> None:
118
+ pass
119
+
120
+ async def _run(self, action: ActionConfig, context: ComponentContext) -> Any:
121
+ return await HttpClientAction(self.config.base_url, action).run(context)
122
+
123
+ ComponentEngineMap[ComponentType.HTTP_CLIENT] = HttpClientComponent
@@ -13,7 +13,7 @@ class HttpServerComponent(BaseComponent):
13
13
  async def _shutdown(self) -> None:
14
14
  pass
15
15
 
16
- async def _run(self, action: ActionConfig, context: ComponentContext) -> Dict[str, Any]:
16
+ async def _run(self, action: ActionConfig, context: ComponentContext) -> Any:
17
17
  return {}
18
18
 
19
19
  ComponentEngineMap[ComponentType.HTTP_SERVER] = HttpServerComponent
@@ -1,6 +1,6 @@
1
+ from typing import Type, Union, Literal, Optional, Dict, List, Tuple, Set, Annotated, Any
1
2
  from abc import ABC, abstractmethod
2
3
  from dataclasses import dataclass
3
- from typing import Type, Union, Literal, Optional, Dict, List, Tuple, Set, Annotated, Any
4
4
 
5
5
  from mindor.dsl.schema.controller import ControllerConfig, ControllerType
6
6
  from mindor.dsl.schema.component import ComponentConfig
@@ -125,7 +125,7 @@ class BaseController(ABC):
125
125
 
126
126
  try:
127
127
  workflow = create_workflow(*WorkflowResolver(self.workflows).resolve(workflow_id), self.components, self.env)
128
- output = await workflow.run(input)
128
+ output = await workflow.run(task_id, input)
129
129
  state = TaskState(task_id=task_id, status="completed", output=output)
130
130
  except Exception as e:
131
131
  state = TaskState(task_id=task_id, status="failed", error=str(e))
@@ -1,19 +1,23 @@
1
- from pydantic import BaseModel
2
1
  from typing import Type, Union, Literal, Optional, Dict, List, Tuple, Set, Annotated, Any
3
2
  from typing_extensions import Self
3
+ from pydantic import BaseModel
4
4
 
5
5
  from mindor.dsl.schema.controller import HttpServerControllerConfig
6
6
  from mindor.dsl.schema.component import ComponentConfig
7
7
  from mindor.dsl.schema.workflow import WorkflowConfig
8
+ from mindor.core.utils.resources import StreamResource
8
9
  from .base import BaseController, ControllerType, ControllerEngineMap, TaskState
9
10
 
10
11
  from fastapi import FastAPI, APIRouter, Body, HTTPException
11
- import uvicorn
12
+ from fastapi.responses import JSONResponse, StreamingResponse
13
+ from starlette.background import BackgroundTask
14
+ import asyncio, uvicorn
12
15
 
13
16
  class WorkflowTaskRequestBody(BaseModel):
14
17
  workflow_id: Optional[str] = None
15
18
  input: Optional[Dict[str, Any]] = None
16
19
  wait_for_completion: bool = True
20
+ output_only: bool = False
17
21
 
18
22
  class TaskResult(BaseModel):
19
23
  task_id: str
@@ -42,30 +46,66 @@ class HttpServerController(BaseController):
42
46
  self.app.include_router(self.router, prefix=self.config.base_path)
43
47
 
44
48
  def _configure_routes(self):
45
- @self.router.post("/workflows", response_model=TaskResult, response_model_exclude_none=True)
49
+ @self.router.post("/workflows")
46
50
  async def run_workflow(
47
51
  body: WorkflowTaskRequestBody = Body(...)
48
52
  ):
49
53
  state = await self.run_workflow(body.workflow_id, body.input, body.wait_for_completion)
50
54
 
51
- return TaskResult.from_instance(state)
55
+ 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
70
+
71
+ return JSONResponse(content=TaskResult.from_instance(state).model_dump(exclude_none=True))
52
72
 
53
- @self.router.get("/tasks/{task_id}", response_model=TaskResult, response_model_exclude_none=True)
73
+ @self.router.get("/tasks/{task_id}")
54
74
  async def get_task_state(
55
- task_id: str
75
+ task_id: str,
76
+ output_only: bool = False
56
77
  ):
57
78
  state = self.get_task_state(task_id)
58
79
 
59
80
  if not state:
60
- raise HTTPException(status_code=404, detail="Task not found")
81
+ raise HTTPException(status_code=404, detail="Task not found.")
61
82
 
62
- return TaskResult.from_instance(state)
83
+ 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))
63
103
 
64
104
  async def _serve(self) -> None:
65
105
  self.server = uvicorn.Server(uvicorn.Config(
66
- self.app,
67
- host=self.config.host,
68
- port=self.config.port,
106
+ self.app,
107
+ host=self.config.host,
108
+ port=self.config.port,
69
109
  log_level="info"
70
110
  ))
71
111
  await self.server.serve()
@@ -0,0 +1 @@
1
+ from .listener import *
@@ -0,0 +1,2 @@
1
+ from .base import *
2
+ from .http_callback import *
@@ -0,0 +1,13 @@
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]] = {}
@@ -0,0 +1,37 @@
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
@@ -0,0 +1,9 @@
1
+ from typing import Type, Union, Literal, Optional, Dict, List, Tuple, Set, Annotated, Any
2
+ from mindor.dsl.schema.listener import ListenerConfig
3
+ from .engine import BaseListener, ListenerEngineMap
4
+
5
+ def create_listener(config: ControllerConfig, env: Dict[str, str]) -> BaseListener:
6
+ try:
7
+ return ListenerEngineMap[config.type](config, env)
8
+ except KeyError:
9
+ raise ValueError(f"Unsupported listener type: {config.type}")
@@ -0,0 +1,24 @@
1
+ from typing import Dict, Tuple, Optional
2
+ from requests.structures import CaseInsensitiveDict
3
+ import re
4
+
5
+ _token_pattern = r"([\w!#$%&'*+\-.^_`|~]+)"
6
+ _quoted_pattern = r'"([^"]*)"'
7
+ _option_regex = re.compile(fr";\s*{_token_pattern}=(?:{_token_pattern}|{_quoted_pattern})", re.ASCII)
8
+ _firefox_escape_fix = re.compile(r'\\"(?!; |\s*$)')
9
+
10
+ def parse_options_header(value: str) -> Tuple[str, Dict[str, str]]:
11
+ value = _firefox_escape_fix.sub("%22", value)
12
+ pos = value.find(";")
13
+ if pos >= 0:
14
+ options = {
15
+ m.group(1).lower(): m.group(2) or m.group(3).replace("%22", "") for m in _option_regex.finditer(value[pos:])
16
+ }
17
+ value = value[:pos]
18
+ else:
19
+ options = {}
20
+
21
+ return value.strip().lower(), options
22
+
23
+ def get_header_value(headers: Dict[str, str], header: str, default_value: Optional[str] = None) -> Optional[str]:
24
+ return CaseInsensitiveDict(headers or {}).get(header, default_value)
@@ -0,0 +1,28 @@
1
+ from typing import Optional, AsyncIterator
2
+ from abc import ABC, abstractmethod
3
+
4
+ class StreamResource(ABC):
5
+ def __init__(self, content_type: Optional[str], filename: Optional[str]):
6
+ self.content_type = content_type or "application/octet-stream"
7
+ self.filename = filename
8
+
9
+ async def __aenter__(self):
10
+ return self
11
+
12
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
13
+ await self.close()
14
+
15
+ def __aiter__(self):
16
+ return self._iterate_stream()
17
+
18
+ @abstractmethod
19
+ def get_stream(self):
20
+ pass
21
+
22
+ @abstractmethod
23
+ async def close(self):
24
+ pass
25
+
26
+ @abstractmethod
27
+ async def _iterate_stream(self) -> AsyncIterator[bytes]:
28
+ pass
@@ -2,9 +2,11 @@ from typing import Type, Union, Literal, Optional, Dict, List, Tuple, Set, Annot
2
2
  from mindor.core.utils.template import TemplateRenderer
3
3
 
4
4
  class WorkflowContext:
5
- def __init__(self, input: Dict[str, Any], env: Dict[str, str]):
5
+ def __init__(self, task_id: str, input: Dict[str, Any], env: Dict[str, str]):
6
+ self.task_id: str = task_id
6
7
  self.input: Dict[str, Any] = input
7
8
  self.env: Dict[str, str] = env
9
+ self.context: Dict[str, Any] = { "task_id": task_id }
8
10
  self.sources: Dict[str, Any] = { "jobs": {} }
9
11
  self.renderer = TemplateRenderer(self._resolve_source)
10
12
 
@@ -24,4 +26,6 @@ class WorkflowContext:
24
26
  return self.input
25
27
  if key == "env":
26
28
  return self.env
29
+ if key == "context":
30
+ return self.context
27
31
  raise KeyError(f"Unknown source: {key}")
@@ -3,6 +3,7 @@ from mindor.dsl.schema.workflow import JobConfig
3
3
  from mindor.dsl.schema.component import ComponentConfig
4
4
  from mindor.core.component import BaseComponent
5
5
  from .context import WorkflowContext
6
+ import ulid
6
7
 
7
8
  class Job:
8
9
  def __init__(self, id: str, config: JobConfig, component_provider: Callable[[Union[ComponentConfig, str]], BaseComponent]):
@@ -10,16 +11,17 @@ class Job:
10
11
  self.config: JobConfig = config
11
12
  self.component_provider: Callable[[Union[ComponentConfig, str]], BaseComponent] = component_provider
12
13
 
13
- async def run(self, context: WorkflowContext) -> Dict[str, Any]:
14
+ async def run(self, context: WorkflowContext) -> Any:
14
15
  component = self.component_provider(self.config.component)
15
16
 
16
17
  if not component.started:
17
18
  await component.start()
18
19
 
19
- input = context.render_template(self.config.input)
20
- output = await component.run(self.config.action, input)
20
+ call_id = ulid.ulid()
21
+ input = context.render_template(self.config.input) if self.config.output else context.input
22
+ output = await component.run(self.config.action, call_id, input)
21
23
 
22
- context.register_source("output", output)
23
- output = context.render_template(self.config.output)
24
+ if output:
25
+ context.register_source("output", output)
24
26
 
25
- return output
27
+ return context.render_template(self.config.output) if self.config.output else output
@@ -80,12 +80,12 @@ class WorkflowRunner:
80
80
  self.jobs: Dict[str, JobConfig] = jobs
81
81
  self.component_provider: Callable[[Union[ComponentConfig, str]], BaseComponent] = component_provider
82
82
 
83
- async def run(self, context: WorkflowContext) -> Dict[str, Any]:
83
+ async def run(self, context: WorkflowContext) -> Any:
84
84
  pending_jobs: Dict[str, Job] = { job_id: Job(job_id, job, self.component_provider) for job_id, job in self.jobs.items() }
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: Dict[str, Any] = {}
88
+ output: Any = {}
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,7 +107,8 @@ 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.update(completed_job_output)
110
+ output = completed_job_output
111
+ # output.update(completed_job_output)
111
112
 
112
113
  running_job_ids.remove(completed_job_id)
113
114
  completed_job_ids.add(completed_job_id)
@@ -129,8 +130,8 @@ class Workflow:
129
130
  self.components: Dict[str, ComponentConfig] = components
130
131
  self.env: Dict[str, str] = env
131
132
 
132
- async def run(self, input: Dict[str, Any]) -> Dict[str, Any]:
133
- context = WorkflowContext(input, self.env)
133
+ async def run(self, task_id: str, input: Dict[str, Any]) -> Any:
134
+ context = WorkflowContext(task_id, input, self.env)
134
135
  runner = WorkflowRunner(self.config.jobs, self._create_component)
135
136
 
136
137
  output = await runner.run(context)
@@ -1,10 +1,18 @@
1
- from pydantic import BaseModel, Field, HttpUrl
2
- from pydantic import model_validator
3
1
  from typing import Type, Union, Literal, Optional, Dict, List, Tuple, Set, Annotated, Any
2
+ from pydantic import BaseModel, Field
3
+ from pydantic import model_validator
4
4
  from .common import CommonActionConfig
5
5
 
6
- class HttpClientActionCompletionConfig(CommonActionConfig):
7
- endpoint: Optional[HttpUrl] = None
6
+ class CommonCompletionConfig(BaseModel):
7
+ type: Literal[ "callback", "polling" ]
8
+
9
+ class CallbackCompletionConfig(CommonCompletionConfig):
10
+ type: Literal["callback"]
11
+ wait_for: str
12
+
13
+ class PollingCompletionConfig(CommonCompletionConfig):
14
+ type: Literal["polling"]
15
+ endpoint: Optional[str] = None
8
16
  path: Optional[str] = None
9
17
  method: Literal[ "GET", "POST", "PUT", "DELETE", "PATCH" ] = "GET"
10
18
  headers: Optional[Dict[str, str]] = Field(default_factory=dict)
@@ -19,15 +27,23 @@ class HttpClientActionCompletionConfig(CommonActionConfig):
19
27
  raise ValueError("Either 'endpoint' or 'path' must be set, but not both.")
20
28
  return values
21
29
 
30
+ HttpClientCompletionConfig = Annotated[
31
+ Union[
32
+ CallbackCompletionConfig,
33
+ PollingCompletionConfig
34
+ ],
35
+ Field(discriminator="type")
36
+ ]
37
+
22
38
  class HttpClientActionConfig(CommonActionConfig):
23
- endpoint: Optional[HttpUrl] = None
39
+ endpoint: Optional[str] = None
24
40
  path: Optional[str] = None
25
41
  method: Literal[ "GET", "POST", "PUT", "DELETE", "PATCH" ] = "POST"
26
42
  headers: Optional[Dict[str, str]] = Field(default_factory=dict)
27
43
  body: Optional[Dict[str, Any]] = Field(default_factory=dict)
28
44
  params: Optional[Dict[str, str]] = Field(default_factory=dict)
29
45
  deferred: bool = False
30
- completion: Optional[HttpClientActionCompletionConfig] = None
46
+ completion: Optional[HttpClientCompletionConfig] = None
31
47
 
32
48
  @model_validator(mode="before")
33
49
  def validate_endpoint_or_path(cls, values):
@@ -1,6 +1,6 @@
1
- from pydantic import BaseModel, Field, HttpUrl
2
- from pydantic import model_validator
3
1
  from typing import Type, Union, Literal, Optional, Dict, List, Tuple, Set, Annotated, Any
2
+ from pydantic import BaseModel, Field
3
+ from pydantic import model_validator
4
4
  from .common import CommonActionConfig
5
5
 
6
6
  class HttpServerActionConfig(CommonActionConfig):
@@ -1,6 +1,6 @@
1
- from pydantic import BaseModel, Field, HttpUrl
2
- from pydantic import model_validator
3
1
  from typing import Type, Union, Literal, Optional, Dict, List, Tuple, Set, Annotated, Any
2
+ from pydantic import BaseModel, Field
3
+ from pydantic import model_validator
4
4
  from .common import CommonActionConfig
5
5
 
6
6
  class McpClientActionConfig(CommonActionConfig):
@@ -1,8 +1,8 @@
1
- from pydantic import BaseModel, Field, HttpUrl
2
- from pydantic import model_validator
3
1
  from typing import Type, Union, Literal, Optional, Dict, List, Tuple, Set, Annotated, Any
2
+ from pydantic import BaseModel, Field
3
+ from pydantic import model_validator
4
4
  from .common import CommonActionConfig
5
5
 
6
6
  class McpServerActionConfig(CommonActionConfig):
7
7
  def __init__(self):
8
- super.__init__(self)
8
+ super().__init__()
@@ -1,5 +1,5 @@
1
- from pydantic import BaseModel, Field, HttpUrl
2
1
  from typing import Type, Union, Literal, Optional, Dict, List, Tuple, Set, Annotated, Any
2
+ from pydantic import BaseModel, Field
3
3
  from .impl import *
4
4
 
5
5
  ComponentConfig = Annotated[
@@ -1,5 +1,5 @@
1
- from pydantic import BaseModel, Field, HttpUrl
2
1
  from typing import Type, Union, Literal, Optional, Dict, List, Tuple, Set, Annotated, Any
2
+ from pydantic import BaseModel, Field
3
3
  from .types import ComponentType
4
4
 
5
5
  class CommonComponentConfig(BaseModel):
@@ -1,12 +1,12 @@
1
- from pydantic import BaseModel, Field, HttpUrl
2
- from pydantic import model_validator
3
1
  from typing import Type, Union, Literal, Optional, Dict, List, Tuple, Set, Annotated, Any
2
+ from pydantic import BaseModel, Field
3
+ from pydantic import model_validator
4
4
  from mindor.dsl.schema.action import HttpClientActionConfig
5
5
  from .common import ComponentType, CommonComponentConfig
6
6
 
7
7
  class HttpClientComponentConfig(CommonComponentConfig):
8
8
  type: Literal[ComponentType.HTTP_CLIENT]
9
- base_url: Optional[HttpUrl] = None
9
+ base_url: Optional[str] = None
10
10
  headers: Optional[Dict[str, Any]] = Field(default_factory=dict)
11
11
  actions: Optional[Dict[str, HttpClientActionConfig]] = Field(default_factory=dict)
12
12
 
@@ -27,7 +27,7 @@ class HttpClientComponentConfig(CommonComponentConfig):
27
27
 
28
28
  @model_validator(mode="after")
29
29
  def validate_baseurl_with_completion(self):
30
- for action_id, action in self.actions.items():
31
- if action.completion and action.completion.path and not self.base_url:
32
- raise ValueError(f"Completion for action '{action_id}' uses 'path' but 'base_url' is not set in the component.")
30
+ # for action_id, action in self.actions.items():
31
+ # if action.completion and action.completion.path and not self.base_url:
32
+ # raise ValueError(f"Completion for action '{action_id}' uses 'path' but 'base_url' is not set in the component.")
33
33
  return self
@@ -1,6 +1,6 @@
1
- from pydantic import BaseModel, Field, HttpUrl
2
- from pydantic import model_validator
3
1
  from typing import Type, Union, Literal, Optional, Dict, List, Tuple, Set, Annotated, Any
2
+ from pydantic import BaseModel, Field
3
+ from pydantic import model_validator
4
4
  from mindor.dsl.schema.action import HttpServerActionConfig
5
5
  from .common import ComponentType, CommonComponentConfig
6
6
 
@@ -1,10 +1,10 @@
1
- from pydantic import BaseModel, Field, HttpUrl
2
- from pydantic import model_validator
3
1
  from typing import Type, Union, Literal, Optional, Dict, List, Tuple, Set, Annotated, Any
2
+ from pydantic import BaseModel, Field
3
+ from pydantic import model_validator
4
4
  from mindor.dsl.schema.action import McpClientActionConfig
5
5
  from .common import ComponentType, CommonComponentConfig
6
6
 
7
7
  class McpClientComponentConfig(CommonComponentConfig):
8
8
  type: Literal[ComponentType.MCP_CLIENT]
9
- endpoint: HttpUrl
9
+ endpoint: Optional[str] = None
10
10
  actions: Optional[Dict[str, McpClientActionConfig]] = Field(default_factory=dict)
@@ -1,6 +1,6 @@
1
- from pydantic import BaseModel, Field, HttpUrl
2
- from pydantic import model_validator
3
1
  from typing import Type, Union, Literal, Optional, Dict, List, Tuple, Set, Annotated, Any
2
+ from pydantic import BaseModel, Field
3
+ from pydantic import model_validator
4
4
  from mindor.dsl.schema.action import McpServerActionConfig
5
5
  from .common import ComponentType, CommonComponentConfig
6
6
 
@@ -1,5 +1,5 @@
1
- from pydantic import BaseModel, Field, HttpUrl
2
1
  from typing import Type, Union, Literal, Optional, Dict, List, Tuple, Set, Annotated, Any
2
+ from pydantic import BaseModel, Field
3
3
  from .impl import *
4
4
 
5
5
  ControllerConfig = Annotated[
@@ -1,6 +1,6 @@
1
- from pydantic import BaseModel, Field, HttpUrl
2
- from pydantic import model_validator
3
1
  from typing import Type, Union, Literal, Optional, Dict, List, Tuple, Set, Annotated, Any
2
+ from pydantic import BaseModel, Field
3
+ from pydantic import model_validator
4
4
  from .common import ControllerType, CommonControllerConfig
5
5
 
6
6
  class HttpServerControllerConfig(CommonControllerConfig):
@@ -1,6 +1,6 @@
1
- from pydantic import BaseModel, Field, HttpUrl
2
- from pydantic import model_validator
3
1
  from typing import Type, Union, Literal, Optional, Dict, List, Tuple, Set, Annotated, Any
2
+ from pydantic import BaseModel, Field
3
+ from pydantic import model_validator
4
4
  from .common import ControllerType, CommonControllerConfig
5
5
 
6
6
  class McpServerControllerConfig(CommonControllerConfig):
@@ -0,0 +1 @@
1
+ from .listener import *
@@ -0,0 +1,2 @@
1
+ from .common import *
2
+ from .http_callback import *
@@ -0,0 +1,7 @@
1
+ from typing import Type, Union, Literal, Optional, Dict, List, Tuple, Set, Annotated, Any
2
+ from pydantic import BaseModel, Field
3
+ from .types import ListenerType
4
+
5
+ class CommonListenerConfig(BaseModel):
6
+ type: ListenerType
7
+ runtime: Literal[ "docker", "native" ] = "native"
@@ -0,0 +1,25 @@
1
+ from typing import Type, Union, Literal, Optional, Dict, List, Tuple, Set, Annotated, Any
2
+ from pydantic import BaseModel, Field
3
+ from pydantic import model_validator
4
+ from .common import ListenerType, CommonListenerConfig
5
+
6
+ class HttpCallbackConfig(BaseModel):
7
+ path: str
8
+ method: Literal[ "GET", "POST", "PUT", "DELETE", "PATCH" ] = "GET"
9
+ bulk: bool = False
10
+ identify_by: str
11
+
12
+ class HttpCallbackListenerConfig(CommonListenerConfig):
13
+ type: Literal[ListenerType.HTTP_CALLBACK]
14
+ host: Optional[str] = "0.0.0.0"
15
+ port: Optional[int] = 8090
16
+ base_path: Optional[str] = None
17
+ callbacks: Optional[List[HttpCallbackConfig]] = Field(default_factory=list)
18
+
19
+ @model_validator(mode="before")
20
+ def inflate_single_callback(cls, values):
21
+ if "callbacks" not in values:
22
+ callback_keys = set(HttpCallbackConfig.model_fields.keys())
23
+ if any(k in values for k in callback_keys):
24
+ values["callbacks"] = [ { k: values.pop(k) for k in callback_keys if k in values } ]
25
+ return values
@@ -0,0 +1,4 @@
1
+ from enum import Enum
2
+
3
+ class ListenerType(str, Enum):
4
+ HTTP_CALLBACK = "http-callback"
@@ -0,0 +1,10 @@
1
+ from typing import Type, Union, Literal, Optional, Dict, List, Tuple, Set, Annotated, Any
2
+ from pydantic import BaseModel, Field
3
+ from .impl import *
4
+
5
+ ListenerConfig = Annotated[
6
+ Union[
7
+ HttpCallbackListenerConfig
8
+ ],
9
+ Field(discriminator="type")
10
+ ]
@@ -1,6 +1,6 @@
1
- from pydantic import BaseModel, Field, HttpUrl
2
- from pydantic import model_validator
3
1
  from typing import Type, Union, Literal, Optional, Dict, List, Tuple, Set, Annotated, Any
2
+ from pydantic import BaseModel, Field
3
+ from pydantic import model_validator
4
4
  from .common import RuntimeType, CommonRumtimeConfig
5
5
 
6
6
  class DockerRuntimeConfig(CommonRumtimeConfig):
@@ -1,5 +1,5 @@
1
- from pydantic import BaseModel, Field, HttpUrl
2
1
  from typing import Type, Union, Literal, Optional, Dict, List, Tuple, Set, Annotated, Any
2
+ from pydantic import BaseModel, Field
3
3
  from .impl import *
4
4
 
5
5
  RuntimeConfig = Annotated[
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: model-compose
3
- Version: 0.0.4
3
+ Version: 0.0.6
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
@@ -21,10 +21,17 @@ src/mindor/core/controller/engine/__init__.py
21
21
  src/mindor/core/controller/engine/base.py
22
22
  src/mindor/core/controller/engine/http_server.py
23
23
  src/mindor/core/controller/engine/mcp_server.py
24
+ src/mindor/core/listener/__init__.py
25
+ src/mindor/core/listener/listener.py
26
+ src/mindor/core/listener/engine/__init__.py
27
+ src/mindor/core/listener/engine/base.py
28
+ src/mindor/core/listener/engine/http_callback.py
24
29
  src/mindor/core/runtime/__init__.py
25
30
  src/mindor/core/runtime/env.py
26
31
  src/mindor/core/utils/__init__.py
27
32
  src/mindor/core/utils/expiring.py
33
+ src/mindor/core/utils/http_headers.py
34
+ src/mindor/core/utils/resources.py
28
35
  src/mindor/core/utils/template.py
29
36
  src/mindor/core/utils/workqueue.py
30
37
  src/mindor/core/workflow/__init__.py
@@ -60,6 +67,12 @@ src/mindor/dsl/schema/controller/impl/common.py
60
67
  src/mindor/dsl/schema/controller/impl/http_server.py
61
68
  src/mindor/dsl/schema/controller/impl/mcp_server.py
62
69
  src/mindor/dsl/schema/controller/impl/types.py
70
+ src/mindor/dsl/schema/listener/__init__.py
71
+ src/mindor/dsl/schema/listener/listener.py
72
+ src/mindor/dsl/schema/listener/impl/__init__.py
73
+ src/mindor/dsl/schema/listener/impl/common.py
74
+ src/mindor/dsl/schema/listener/impl/http_callback.py
75
+ src/mindor/dsl/schema/listener/impl/types.py
63
76
  src/mindor/dsl/schema/runtime/__init__.py
64
77
  src/mindor/dsl/schema/runtime/runtime.py
65
78
  src/mindor/dsl/schema/runtime/impl/__init__.py
@@ -1,87 +0,0 @@
1
- from typing import Type, Union, Literal, Optional, Dict, List, Tuple, Set, Annotated, Any
2
- from mindor.dsl.schema.component import HttpClientComponentConfig, HttpClientActionConfig, HttpUrl
3
- from .base import BaseComponent, ComponentType, ComponentEngineMap, ActionConfig
4
- from .context import ComponentContext
5
-
6
- from requests.structures import CaseInsensitiveDict
7
- from urllib.parse import urlencode, urljoin
8
- import aiohttp, json
9
-
10
- class HttpClientAction:
11
- def __init__(self, base_url: Union[str, None], config: HttpClientActionConfig):
12
- self.base_url: Union[HttpUrl, None] = base_url
13
- self.config: HttpClientActionConfig = config
14
-
15
- async def run(self, context: ComponentContext) -> Dict[str, Any]:
16
- url = self._resolve_request_url(context)
17
- method = context.render_template(self.config.method)
18
- params = context.render_template(self.config.params)
19
- body = context.render_template(self.config.body)
20
- headers = context.render_template(self.config.headers)
21
-
22
- response = await self._request(url, method, params, body, headers)
23
-
24
- if not self.config.output:
25
- return response or {}
26
-
27
- if response:
28
- context.register_source("response", response)
29
-
30
- return context.render_template(self.config.output)
31
-
32
- async def _request(self, url: str, method: str, params: Optional[Dict[str, Any]], body: Optional[Dict[str, Any]], headers: Optional[Dict[str, str]]) -> Union[dict, str, bytes]:
33
- async with aiohttp.ClientSession() as session:
34
- async with session.request(
35
- method,
36
- url,
37
- params=params,
38
- data=self._serialize_request_body(body, headers) if body is not None else None,
39
- headers=headers
40
- ) as response:
41
- if response.status >= 400:
42
- raise ValueError(f"Request failed with status {response.status}")
43
-
44
- return await self._deserialize_response_body(response)
45
-
46
- def _resolve_request_url(self, context: ComponentContext) -> str:
47
- if self.base_url and self.config.path:
48
- return context.render_template(str(self.base_url)) + context.render_template(self.config.path)
49
-
50
- return context.render_template(str(self.config.endpoint))
51
-
52
- def _serialize_request_body(self, body: Dict[str, Any], headers: Optional[Dict[str, str]]) -> str:
53
- content_type = CaseInsensitiveDict(headers or {}).get("Content-Type", "").lower()
54
-
55
- if content_type == "application/json":
56
- return json.dumps(body)
57
-
58
- if content_type == "application/x-www-form-urlencoded":
59
- return urlencode(body)
60
-
61
- return str(body)
62
-
63
- async def _deserialize_response_body(self, response: aiohttp.ClientResponse) -> Union[dict, str, bytes]:
64
- content_type = response.headers.get("Content-Type", "").lower()
65
-
66
- if content_type == "application/json":
67
- return await response.json()
68
-
69
- if content_type.startswith("text/"):
70
- return await response.text()
71
-
72
- return await response.read()
73
-
74
- class HttpClientComponent(BaseComponent):
75
- def __init__(self, id: str, config: HttpClientComponentConfig, env: Dict[str, str], daemon: bool):
76
- super().__init__(id, config, env, daemon)
77
-
78
- async def _serve(self) -> None:
79
- pass
80
-
81
- async def _shutdown(self) -> None:
82
- pass
83
-
84
- async def _run(self, action: ActionConfig, context: ComponentContext) -> Dict[str, Any]:
85
- return await HttpClientAction(self.config.base_url, action).run(context)
86
-
87
- ComponentEngineMap[ComponentType.HTTP_CLIENT] = HttpClientComponent
File without changes
File without changes
File without changes
@@ -1,5 +1,5 @@
1
- from pydantic import BaseModel, ValidationError
2
1
  from typing import Union, List, Type
2
+ from pydantic import BaseModel, ValidationError
3
3
  from pathlib import Path
4
4
  import yaml
5
5
 
@@ -1,5 +1,5 @@
1
- from pydantic import BaseModel, Field
2
1
  from typing import Type, Union, Literal, Optional, Dict, List, Tuple, Set, Annotated, Any
2
+ from pydantic import BaseModel, Field
3
3
  from .impl import *
4
4
 
5
5
  ActionConfig = Annotated[
@@ -1,5 +1,5 @@
1
- from pydantic import BaseModel, Field
2
1
  from typing import Type, Union, Literal, Optional, Dict, List, Tuple, Set, Annotated, Any
2
+ from pydantic import BaseModel, Field
3
3
 
4
4
  class CommonActionConfig(BaseModel):
5
5
  output: Optional[Dict[str, Any]] = Field(default_factory=dict)
@@ -1,6 +1,6 @@
1
+ from typing import Type, Union, Literal, Optional, Dict, List, Tuple, Set, Annotated, Any
1
2
  from pydantic import BaseModel, Field
2
3
  from pydantic import model_validator
3
- from typing import Type, Union, Literal, Optional, Dict, List, Tuple, Set, Annotated, Any
4
4
 
5
5
  from .controller import ControllerConfig
6
6
  from .component import ComponentConfig
@@ -1,5 +1,5 @@
1
- from pydantic import BaseModel, Field
2
1
  from typing import Type, Union, Literal, Optional, Dict, List, Tuple, Set, Annotated, Any
2
+ from pydantic import BaseModel, Field
3
3
  from .types import ControllerType
4
4
 
5
5
  class CommonControllerConfig(BaseModel):
@@ -1,5 +1,5 @@
1
- from pydantic import BaseModel, Field
2
1
  from typing import Type, Union, Literal, Optional, Dict, List, Tuple, Set, Annotated, Any
2
+ from pydantic import BaseModel, Field
3
3
  from .types import RuntimeType
4
4
 
5
5
  class CommonRumtimeConfig(BaseModel):
@@ -1,6 +1,6 @@
1
+ from typing import Type, Union, Literal, Optional, Dict, List, Tuple, Set, Annotated, Any
1
2
  from pydantic import BaseModel, Field
2
3
  from pydantic import model_validator
3
- from typing import Type, Union, Literal, Optional, Dict, List, Tuple, Set, Annotated, Any
4
4
  from .component import ComponentConfig
5
5
 
6
6
  class JobConfig(BaseModel):