model-compose 0.1.8__tar.gz → 0.1.9__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. {model_compose-0.1.8/src/model_compose.egg-info → model_compose-0.1.9}/PKG-INFO +1 -1
  2. {model_compose-0.1.8 → model_compose-0.1.9}/pyproject.toml +1 -1
  3. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/core/component/engine/http_client.py +63 -32
  4. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/core/controller/engine/http_server.py +3 -2
  5. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/core/controller/engine/mcp_server.py +1 -0
  6. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/core/controller/runner/http_client.py +3 -0
  7. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/core/controller/runner/mcp_client.py +3 -2
  8. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/core/controller/runner/runner.py +10 -1
  9. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/core/controller/webui/gradio.py +2 -2
  10. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/core/controller/webui/webui.py +9 -3
  11. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/core/listener/engine/http_callback.py +8 -8
  12. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/core/utils/http_client.py +50 -23
  13. model_compose-0.1.9/src/mindor/core/utils/http_status.py +20 -0
  14. model_compose-0.1.9/src/mindor/core/utils/mcp_client.py +122 -0
  15. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/core/utils/streaming.py +2 -2
  16. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/core/utils/template.py +1 -1
  17. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/dsl/schema/action/impl/http_client.py +4 -4
  18. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/dsl/schema/component/impl/http_client.py +1 -1
  19. {model_compose-0.1.8 → model_compose-0.1.9/src/model_compose.egg-info}/PKG-INFO +1 -1
  20. {model_compose-0.1.8 → model_compose-0.1.9}/src/model_compose.egg-info/SOURCES.txt +1 -0
  21. model_compose-0.1.8/src/mindor/core/utils/mcp_client.py +0 -4
  22. {model_compose-0.1.8 → model_compose-0.1.9}/LICENSE +0 -0
  23. {model_compose-0.1.8 → model_compose-0.1.9}/README.md +0 -0
  24. {model_compose-0.1.8 → model_compose-0.1.9}/setup.cfg +0 -0
  25. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/__init__.py +0 -0
  26. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/cli/__init__.py +0 -0
  27. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/cli/compose.py +0 -0
  28. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/core/__init__.py +0 -0
  29. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/core/component/__init__.py +0 -0
  30. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/core/component/component.py +0 -0
  31. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/core/component/engine/__init__.py +0 -0
  32. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/core/component/engine/base.py +0 -0
  33. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/core/component/engine/context.py +0 -0
  34. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/core/component/engine/http_server.py +0 -0
  35. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/core/component/engine/mcp_client.py +0 -0
  36. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/core/component/engine/mcp_server.py +0 -0
  37. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/core/component/engine/shell.py +0 -0
  38. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/core/compose/__init__.py +0 -0
  39. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/core/compose/compose.py +0 -0
  40. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/core/compose/manager.py +0 -0
  41. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/core/controller/__init__.py +0 -0
  42. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/core/controller/controller.py +0 -0
  43. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/core/controller/engine/__init__.py +0 -0
  44. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/core/controller/engine/base.py +0 -0
  45. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/core/controller/runner/__init__.py +0 -0
  46. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/core/controller/runner/client.py +0 -0
  47. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/core/controller/webui/__init__.py +0 -0
  48. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/core/gateway/__init__.py +0 -0
  49. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/core/gateway/engine/__init__.py +0 -0
  50. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/core/gateway/engine/base.py +0 -0
  51. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/core/gateway/engine/http_tunnel.py +0 -0
  52. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/core/gateway/engine/ssh_tunnel.py +0 -0
  53. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/core/gateway/gateway.py +0 -0
  54. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/core/listener/__init__.py +0 -0
  55. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/core/listener/engine/__init__.py +0 -0
  56. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/core/listener/engine/base.py +0 -0
  57. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/core/listener/listener.py +0 -0
  58. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/core/runtime/__init__.py +0 -0
  59. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/core/runtime/env.py +0 -0
  60. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/core/services/__init__.py +0 -0
  61. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/core/services/async_service.py +0 -0
  62. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/core/utils/__init__.py +0 -0
  63. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/core/utils/expiring.py +0 -0
  64. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/core/utils/http_request.py +0 -0
  65. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/core/utils/image.py +0 -0
  66. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/core/utils/ssh_client.py +0 -0
  67. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/core/utils/time.py +0 -0
  68. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/core/utils/workqueue.py +0 -0
  69. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/core/workflow/__init__.py +0 -0
  70. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/core/workflow/context.py +0 -0
  71. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/core/workflow/job.py +0 -0
  72. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/core/workflow/schema.py +0 -0
  73. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/core/workflow/workflow.py +0 -0
  74. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/dsl/__init__.py +0 -0
  75. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/dsl/loader.py +0 -0
  76. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/dsl/schema/__init__.py +0 -0
  77. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/dsl/schema/action/__init__.py +0 -0
  78. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/dsl/schema/action/action.py +0 -0
  79. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/dsl/schema/action/impl/__init__.py +0 -0
  80. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/dsl/schema/action/impl/common.py +0 -0
  81. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/dsl/schema/action/impl/http_server.py +0 -0
  82. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/dsl/schema/action/impl/mcp_client.py +0 -0
  83. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/dsl/schema/action/impl/mcp_server.py +0 -0
  84. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/dsl/schema/action/impl/shell.py +0 -0
  85. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/dsl/schema/component/__init__.py +0 -0
  86. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/dsl/schema/component/component.py +0 -0
  87. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/dsl/schema/component/impl/__init__.py +0 -0
  88. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/dsl/schema/component/impl/common.py +0 -0
  89. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/dsl/schema/component/impl/http_server.py +0 -0
  90. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/dsl/schema/component/impl/mcp_client.py +0 -0
  91. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/dsl/schema/component/impl/mcp_server.py +0 -0
  92. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/dsl/schema/component/impl/shell.py +0 -0
  93. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/dsl/schema/component/impl/types.py +0 -0
  94. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/dsl/schema/compose.py +0 -0
  95. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/dsl/schema/controller/__init__.py +0 -0
  96. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/dsl/schema/controller/controller.py +0 -0
  97. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/dsl/schema/controller/impl/__init__.py +0 -0
  98. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/dsl/schema/controller/impl/common.py +0 -0
  99. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/dsl/schema/controller/impl/http_server.py +0 -0
  100. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/dsl/schema/controller/impl/mcp_server.py +0 -0
  101. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/dsl/schema/controller/impl/types.py +0 -0
  102. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/dsl/schema/controller/impl/webui.py +0 -0
  103. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/dsl/schema/gateway/__init__.py +0 -0
  104. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/dsl/schema/gateway/gateway.py +0 -0
  105. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/dsl/schema/gateway/impl/__init__.py +0 -0
  106. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/dsl/schema/gateway/impl/common.py +0 -0
  107. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/dsl/schema/gateway/impl/http_tunnel.py +0 -0
  108. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/dsl/schema/gateway/impl/ssh_tunnel.py +0 -0
  109. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/dsl/schema/gateway/impl/types.py +0 -0
  110. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/dsl/schema/listener/__init__.py +0 -0
  111. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/dsl/schema/listener/impl/__init__.py +0 -0
  112. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/dsl/schema/listener/impl/common.py +0 -0
  113. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/dsl/schema/listener/impl/http_callback.py +0 -0
  114. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/dsl/schema/listener/impl/types.py +0 -0
  115. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/dsl/schema/listener/listener.py +0 -0
  116. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/dsl/schema/runtime/__init__.py +0 -0
  117. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/dsl/schema/runtime/impl/__init__.py +0 -0
  118. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/dsl/schema/runtime/impl/common.py +0 -0
  119. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/dsl/schema/runtime/impl/docker.py +0 -0
  120. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/dsl/schema/runtime/impl/types.py +0 -0
  121. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/dsl/schema/runtime/runtime.py +0 -0
  122. {model_compose-0.1.8 → model_compose-0.1.9}/src/mindor/dsl/schema/workflow.py +0 -0
  123. {model_compose-0.1.8 → model_compose-0.1.9}/src/model_compose.egg-info/dependency_links.txt +0 -0
  124. {model_compose-0.1.8 → model_compose-0.1.9}/src/model_compose.egg-info/entry_points.txt +0 -0
  125. {model_compose-0.1.8 → model_compose-0.1.9}/src/model_compose.egg-info/requires.txt +0 -0
  126. {model_compose-0.1.8 → model_compose-0.1.9}/src/model_compose.egg-info/top_level.txt +0 -0
  127. {model_compose-0.1.8 → model_compose-0.1.9}/tests/test_cli.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: model-compose
3
- Version: 0.1.8
3
+ Version: 0.1.9
4
4
  Summary: model-compose: Declarative AI Model and Workflow Orchestrator (part of the Mindor project)
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.1.8"
3
+ version = "0.1.9"
4
4
  description = "model-compose: Declarative AI Model and Workflow Orchestrator (part of the Mindor project)"
5
5
  authors = [
6
6
  { name = "Hanyeol Cho", email = "hanyeol.cho@gmail.com" }
@@ -1,19 +1,30 @@
1
1
  from typing import Type, Union, Literal, Optional, Dict, List, Tuple, Set, Annotated, AsyncIterator, Any
2
+ from abc import ABC, abstractmethod
2
3
  from mindor.dsl.schema.component import HttpClientComponentConfig
3
4
  from mindor.dsl.schema.action import ActionConfig, HttpClientActionConfig, HttpClientCompletionConfig
4
5
  from mindor.core.listener import HttpCallbackListener
5
6
  from mindor.core.utils.http_client import HttpClient
7
+ from mindor.core.utils.http_status import is_status_code_matched
6
8
  from mindor.core.utils.time import parse_duration
7
9
  from .base import ComponentEngine, ComponentType, ComponentEngineMap
8
10
  from .context import ComponentContext
9
11
  from datetime import datetime, timezone
10
12
  import asyncio
11
13
 
12
- class HttpClientPollingCompletion:
13
- def __init__(self, base_url: Optional[str], headers: Optional[Dict[str, str]], config: HttpClientCompletionConfig):
14
+ class HttpClientCompletion(ABC):
15
+ @abstractmethod
16
+ async def run(self, context: ComponentContext) -> Any:
17
+ pass
18
+
19
+ @abstractmethod
20
+ async def close(self) -> None:
21
+ pass
22
+
23
+ class HttpClientPollingCompletion(HttpClientCompletion):
24
+ def __init__(self, config: HttpClientCompletionConfig, base_url: Optional[str], headers: Optional[Dict[str, str]]):
25
+ self.config: HttpClientCompletionConfig = config
14
26
  self.base_url: Optional[str] = base_url
15
27
  self.headers: Optional[Dict[str, str]] = headers
16
- self.config: HttpClientCompletionConfig = config
17
28
  self.client: HttpClient = HttpClient()
18
29
 
19
30
  async def run(self, context: ComponentContext) -> Any:
@@ -30,33 +41,38 @@ class HttpClientPollingCompletion:
30
41
  await asyncio.sleep(interval.total_seconds())
31
42
 
32
43
  while datetime.now(timezone.utc) < deadline:
33
- response = await self.client.request(url, method, params, body, headers)
44
+ response, status_code = await self.client.request(url, method, params, body, headers, raise_on_error=False)
34
45
  context.register_source("result", response)
35
46
 
36
- status = (await context.render_template(self.config.status)) if self.config.status else response["status"]
37
-
38
- if not status:
47
+ status = (await context.render_template(self.config.status)) if self.config.status else None
48
+ if self.config.status and not status:
39
49
  raise RuntimeError(f"Polling failed: no status found in response.")
40
50
 
41
- for success_when in self.config.success_when if self.config.success_when else [ "completed" ]:
42
- if status == await context.render_template(success_when):
51
+ if status:
52
+ if status in self.config.success_when or []:
43
53
  return response
44
-
45
- for fail_when in self.config.fail_when if self.config.fail_when else [ "failed" ]:
46
- if status == await context.render_template(fail_when):
54
+ if status in self.config.fail_when or []:
47
55
  raise RuntimeError(f"Polling failed: status '{status}' matched a failure condition.")
56
+ else: # use status code
57
+ if is_status_code_matched(status_code, self.config.success_when or []):
58
+ return response
59
+ if is_status_code_matched(status_code, self.config.fail_when or []):
60
+ raise RuntimeError(f"Polling failed: status code '{status_code}' matched a failure condition.")
48
61
 
49
62
  await asyncio.sleep(interval.total_seconds())
50
63
 
51
64
  raise TimeoutError(f"Polling timed out after {timeout}.")
52
65
 
66
+ async def close(self) -> None:
67
+ await self.client.close()
68
+
53
69
  async def _resolve_request_url(self, context: ComponentContext) -> str:
54
70
  if self.base_url and self.config.path:
55
71
  return await context.render_template(self.base_url) + await context.render_template(self.config.path)
56
72
 
57
73
  return await context.render_template(self.config.endpoint)
58
74
 
59
- class HttpClientCallbackCompletion:
75
+ class HttpClientCallbackCompletion(HttpClientCompletion):
60
76
  def __init__(self, config: HttpClientCompletionConfig):
61
77
  self.config: HttpClientCompletionConfig = config
62
78
 
@@ -66,56 +82,71 @@ class HttpClientCallbackCompletion:
66
82
  HttpCallbackListener.register_pending_future(callback_id, future)
67
83
 
68
84
  return await future
85
+
86
+ async def close(self) -> None:
87
+ pass
69
88
 
70
89
  class HttpClientAction:
71
- def __init__(self, base_url: Optional[str], headers: Optional[Dict[str, str]], config: HttpClientActionConfig):
90
+ def __init__(self, config: HttpClientActionConfig, base_url: Optional[str], headers: Optional[Dict[str, str]]):
91
+ self.config: HttpClientActionConfig = config
72
92
  self.base_url: Optional[str] = base_url
73
93
  self.headers: Optional[Dict[str, str]] = headers
74
- self.config: HttpClientActionConfig = config
75
- self.client: HttpClient = HttpClient()
94
+ self.completion: HttpClientCompletion = None
76
95
 
77
- async def run(self, context: ComponentContext) -> Any:
96
+ if self.config.completion:
97
+ self._configure_completion()
98
+
99
+ def _configure_completion(self) -> None:
100
+ if self.config.completion.type == "polling":
101
+ self.completion = HttpClientPollingCompletion(self.config.completion, self.base_url, self.headers)
102
+ return
103
+
104
+ if self.config.completion.type == "callback":
105
+ self.completion = HttpClientCallbackCompletion(self.config.completion)
106
+ return
107
+
108
+ raise ValueError(f"Unsupported http completion type: {self.config.completion.type}")
109
+
110
+ async def run(self, context: ComponentContext, client: HttpClient) -> Any:
78
111
  url = await self._resolve_request_url(context)
79
112
  method = await context.render_template(self.config.method)
80
113
  params = await context.render_template(self.config.params)
81
114
  body = await context.render_template(self.config.body)
82
115
  headers = await context.render_template({ **self.headers, **self.config.headers })
83
116
 
84
- response, result = await self.client.request(url, method, params, body, headers), None
117
+ response, result = await client.request(url, method, params, body, headers), None
85
118
  context.register_source("response", response)
86
119
 
87
- if self.config.completion:
88
- result = await self._handle_completion(self.config.completion, context)
120
+ if self.completion:
121
+ result = await self.completion.run(context)
89
122
  context.register_source("result", result)
90
123
 
91
124
  return (await context.render_template(self.config.output, ignore_files=True)) if self.config.output else (result or response)
92
125
 
126
+ async def close(self) -> None:
127
+ if self.completion:
128
+ await self.completion.close()
129
+
93
130
  async def _resolve_request_url(self, context: ComponentContext) -> str:
94
131
  if self.base_url and self.config.path:
95
132
  return await context.render_template(self.base_url) + await context.render_template(self.config.path)
96
133
 
97
134
  return await context.render_template(self.config.endpoint)
98
135
 
99
- async def _handle_completion(self, completion: HttpClientCompletionConfig, context: ComponentContext) -> Any:
100
- if completion.type == "polling":
101
- return await HttpClientPollingCompletion(self.base_url, self.headers, completion).run(context)
102
-
103
- if completion.type == "callback":
104
- return await HttpClientCallbackCompletion(completion).run(context)
105
-
106
- return None
107
-
108
136
  class HttpClientComponent(ComponentEngine):
109
137
  def __init__(self, id: str, config: HttpClientComponentConfig, env: Dict[str, str], daemon: bool):
110
138
  super().__init__(id, config, env, daemon)
111
139
 
140
+ self.client: HttpClient = None
141
+
112
142
  async def _serve(self) -> None:
113
- pass
143
+ self.client = HttpClient()
114
144
 
115
145
  async def _shutdown(self) -> None:
116
- pass
146
+ await self.client.close()
147
+ self.client = None
117
148
 
118
149
  async def _run(self, action: ActionConfig, context: ComponentContext) -> Any:
119
- return await HttpClientAction(self.config.base_url, self.config.headers, action).run(context)
150
+ return await HttpClientAction(action, self.config.base_url, self.config.headers).run(context, self.client)
120
151
 
121
152
  ComponentEngineMap[ComponentType.HTTP_CLIENT] = HttpClientComponent
@@ -59,7 +59,7 @@ class HttpServerController(ControllerEngine):
59
59
  self._configure_routes()
60
60
  self.app.include_router(self.router, prefix=self.config.base_path)
61
61
 
62
- def _configure_server(self):
62
+ def _configure_server(self) -> None:
63
63
  self.app.add_middleware(
64
64
  CORSMiddleware,
65
65
  allow_origins=[self.config.origins],
@@ -68,7 +68,7 @@ class HttpServerController(ControllerEngine):
68
68
  allow_headers=["*"],
69
69
  )
70
70
 
71
- def _configure_routes(self):
71
+ def _configure_routes(self) -> None:
72
72
  @self.router.post("/workflows")
73
73
  async def run_workflow(
74
74
  request: Request
@@ -107,6 +107,7 @@ class HttpServerController(ControllerEngine):
107
107
  log_level="info"
108
108
  ))
109
109
  await self.server.serve()
110
+ self.server = None
110
111
 
111
112
  async def _shutdown(self) -> None:
112
113
  if self.server:
@@ -150,6 +150,7 @@ class McpServerController(ControllerEngine):
150
150
  log_level="info"
151
151
  ))
152
152
  await self.server.serve()
153
+ self.server = None
153
154
 
154
155
  async def _shutdown(self) -> None:
155
156
  if self.server:
@@ -32,6 +32,9 @@ class HttpControllerClient(ControllerClient):
32
32
 
33
33
  return await self.client.request(url, method, None, body, headers)
34
34
 
35
+ async def close(self) -> None:
36
+ await self.client.close()
37
+
35
38
  def _flatten_for_multipart(self, body: Dict[str, Any], key: str = "") -> List[Tuple[str, Any]]:
36
39
  flattened = []
37
40
 
@@ -7,7 +7,8 @@ class McpControllerClient(ControllerClient):
7
7
  def __init__(self, config: ControllerConfig):
8
8
  super().__init__(config)
9
9
 
10
- self.client: McpClient = McpClient()
11
-
12
10
  async def run_workflow(self, workflow_id: Optional[str], input: Any) -> Any:
13
11
  pass
12
+
13
+ async def close(self) -> None:
14
+ pass
@@ -13,6 +13,10 @@ class ControllerClient(ABC):
13
13
  async def run_workflow(self, workflow_id: Optional[str], input: Any) -> Any:
14
14
  pass
15
15
 
16
+ @abstractmethod
17
+ async def close(self) -> None:
18
+ pass
19
+
16
20
  class ControllerRunner:
17
21
  def __init__(self, config: ControllerConfig):
18
22
  self.config: ControllerConfig = config
@@ -24,10 +28,15 @@ class ControllerRunner:
24
28
  if self.config.type == "http-server":
25
29
  self.client = HttpControllerClient(self.config)
26
30
  return
27
-
31
+
28
32
  if self.config.type == "mcp-server":
29
33
  self.client = McpControllerClient(self.config)
30
34
  return
35
+
36
+ raise ValueError(f"Unsupported controller type: {self.config.type}")
31
37
 
32
38
  async def run_workflow(self, workflow_id: Optional[str], input: Any) -> Any:
33
39
  return await self.client.run_workflow(workflow_id, input)
40
+
41
+ async def close(self) -> None:
42
+ await self.client.close()
@@ -184,7 +184,7 @@ class GradioWebUIBuilder:
184
184
  return await load_image_from_stream(Base64StreamResource(value), subtype)
185
185
 
186
186
  if format == "url" and isinstance(value, str):
187
- return await load_image_from_stream(await HttpClient().request(value), subtype)
187
+ return await load_image_from_stream(await HttpClient.request_once(value), subtype)
188
188
 
189
189
  if isinstance(value, StreamResource):
190
190
  return await load_image_from_stream(value, subtype)
@@ -196,7 +196,7 @@ class GradioWebUIBuilder:
196
196
  return await save_stream_to_temporary_file(Base64StreamResource(value), subtype)
197
197
 
198
198
  if format == "url" and isinstance(value, str):
199
- return await save_stream_to_temporary_file(await HttpClient().request(value), subtype)
199
+ return await save_stream_to_temporary_file(await HttpClient.request_once(value), subtype)
200
200
 
201
201
  if isinstance(value, StreamResource):
202
202
  return await save_stream_to_temporary_file(value, subtype)
@@ -30,7 +30,7 @@ class ControllerWebUI(AsyncService):
30
30
  self.env: Dict[str, str] = env
31
31
  self.server: Optional[uvicorn.Server] = None
32
32
  self.app: FastAPI = FastAPI(openapi_url=None, docs_url=None, redoc_url=None)
33
- self.runner: ControllerRunner = ControllerRunner(controller)
33
+ self.runner: ControllerRunner = None
34
34
 
35
35
  self._configure_driver()
36
36
 
@@ -44,6 +44,7 @@ class ControllerWebUI(AsyncService):
44
44
  return
45
45
 
46
46
  async def _serve(self) -> None:
47
+ self.runner = ControllerRunner(self.controller)
47
48
  self.server = uvicorn.Server(uvicorn.Config(
48
49
  self.app,
49
50
  host=self.config.host,
@@ -51,10 +52,15 @@ class ControllerWebUI(AsyncService):
51
52
  log_level="info"
52
53
  ))
53
54
  await self.server.serve()
55
+ await self.runner.close()
56
+ self.server = None
57
+ self.runner = None
54
58
 
55
59
  async def _shutdown(self) -> None:
56
60
  if self.server:
57
61
  self.server.should_exit = True
58
-
62
+
59
63
  async def _run_workflow(self, workflow_id: Optional[str], input: Any) -> Any:
60
- return await self.runner.run_workflow(workflow_id, input)
64
+ if self.runner:
65
+ return await self.runner.run_workflow(workflow_id, input)
66
+ return None
@@ -22,7 +22,7 @@ class HttpCallbackContext:
22
22
  self.renderer: TemplateRenderer = TemplateRenderer(self._resolve_source)
23
23
 
24
24
  async def items(self) -> AsyncIterator["HttpCallbackContext"]:
25
- async for item in self._items():
25
+ for item in await self._items():
26
26
  yield HttpCallbackContext(item, self.query, False, None)
27
27
 
28
28
  async def render_template(self, template: str, ignore_files: bool = True) -> Any:
@@ -56,7 +56,7 @@ class HttpCallbackListener(ListenerEngine):
56
56
  self._configure_routes()
57
57
  self.app.include_router(self.router, prefix=self.config.base_path)
58
58
 
59
- def _configure_routes(self):
59
+ def _configure_routes(self) -> None:
60
60
  for callback in self.config.callbacks:
61
61
  self.router.add_api_route(
62
62
  path=callback.path,
@@ -69,8 +69,8 @@ class HttpCallbackListener(ListenerEngine):
69
69
  async def _handler(request: Request) -> Response:
70
70
  content_type, _ = parse_options_header(request.headers, "Content-Type")
71
71
  body, query = await parse_request_body(request, content_type), request.query_params
72
- context: HttpCallbackContext = HttpCallbackContext(body, query, callback.bulk, callback.item)
73
- succeeded: bool = await self._is_callback_succeeded(callback, context)
72
+ context = HttpCallbackContext(body, query, callback.bulk, callback.item)
73
+ succeeded = await self._is_callback_succeeded(callback, context)
74
74
 
75
75
  async for item in context.items():
76
76
  callback_id = await item.render_template(callback.identify_by)
@@ -91,13 +91,12 @@ class HttpCallbackListener(ListenerEngine):
91
91
  status = (await context.render_template(callback.status)) if callback.status else None
92
92
 
93
93
  if status:
94
- if callback.success_when and status == await context.render_template(callback.success_when):
94
+ if status in callback.success_when or []:
95
95
  return True
96
-
97
- if callback.fail_when and status == await context.render_template(callback.fail_when):
96
+ if status in callback.fail_when or []:
98
97
  return False
99
98
 
100
- return True
99
+ return True
101
100
 
102
101
  async def _serve(self) -> None:
103
102
  self.server = uvicorn.Server(uvicorn.Config(
@@ -107,6 +106,7 @@ class HttpCallbackListener(ListenerEngine):
107
106
  log_level="info"
108
107
  ))
109
108
  await self.server.serve()
109
+ self.server = None
110
110
 
111
111
  async def _shutdown(self) -> None:
112
112
  if self.server:
@@ -1,4 +1,4 @@
1
- from typing import Optional, Dict, Tuple, AsyncIterator, Any
1
+ from typing import Union, Optional, Dict, Tuple, AsyncIterator, Any
2
2
  from .http_request import build_request_body, parse_options_header
3
3
  from .streaming import StreamResource
4
4
  from requests.structures import CaseInsensitiveDict
@@ -7,20 +7,19 @@ import aiohttp
7
7
  class HttpStreamResource(StreamResource):
8
8
  def __init__(
9
9
  self,
10
- session: aiohttp.ClientSession,
11
- stream: aiohttp.StreamReader,
10
+ response: aiohttp.ClientResponse,
12
11
  content_type: Optional[str] = None,
13
12
  filename: Optional[str] = None
14
13
  ):
15
14
  super().__init__(content_type, filename)
16
15
 
17
- self.session: aiohttp.ClientSession = session
18
- self.stream: aiohttp.StreamReader = stream
16
+ self.response: aiohttp.ClientResponse = response
17
+ self.stream: aiohttp.StreamReader = response.content
19
18
 
20
- async def close(self):
21
- await self.session.close()
22
- self.session = None
23
- self.stream = None
19
+ async def close(self) -> None:
20
+ self.response.close()
21
+ self.response = None
22
+ self.stream = None
24
23
 
25
24
  async def _iterate_stream(self) -> AsyncIterator[bytes]:
26
25
  _, buffer_size = self.stream.get_read_buffer_limits()
@@ -33,34 +32,62 @@ class HttpStreamResource(StreamResource):
33
32
  yield chunk
34
33
 
35
34
  class HttpClient:
35
+ def __init__(self, base_url: Optional[str] = None, headers: Optional[Dict[str, str]] = None):
36
+ self.base_url: Optional[str] = base_url
37
+ self.headers: Optional[Dict[str, str]] = headers
38
+ self.session = aiohttp.ClientSession(self.base_url, headers=self.headers)
39
+
40
+ async def __aenter__(self):
41
+ if self.session is not None:
42
+ self.session = aiohttp.ClientSession(self.base_url, headers=self.headers)
43
+ return self
44
+
45
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
46
+ await self.close()
47
+
36
48
  async def request(
37
49
  self,
38
- url: str,
50
+ url_or_path: str,
39
51
  method: Optional[str] = "GET",
40
52
  params: Optional[Dict[str, Any]] = None,
41
53
  body: Optional[Any] = None,
42
- headers: Optional[Dict[str, str]] = None
43
- ) -> Any:
44
- session = aiohttp.ClientSession()
54
+ headers: Optional[Dict[str, str]] = None,
55
+ raise_on_error: bool = True
56
+ ) -> Union[Any, Tuple[Any, int]]:
57
+ response: aiohttp.ClientResponse = None
45
58
  try:
46
- response = await self._request_with_session(session, url, method, params, body, headers)
47
- content, _ = await self._parse_response_content(session, response)
59
+ response = await self._request_with_session(self.session, url_or_path, method, params, body, headers)
60
+ content, _ = await self._parse_response_content(response)
48
61
 
49
- if response.status >= 400:
62
+ if raise_on_error and response.status >= 400:
50
63
  raise ValueError(f"Request failed with status {response.status}: {content}")
51
64
 
52
65
  if not isinstance(content, HttpStreamResource):
53
- await session.close()
66
+ response.close()
54
67
 
55
- return content
68
+ return content if raise_on_error else (content, response.status)
56
69
  except:
57
- await session.close()
70
+ if response:
71
+ response.close()
58
72
  raise
59
73
 
74
+ async def close(self) -> None:
75
+ if self.session:
76
+ await self.session.close()
77
+ self.session = None
78
+
79
+ @classmethod
80
+ async def request_once(cls, *args, **kwargs):
81
+ instance = cls()
82
+ try:
83
+ return await instance.request(*args, **kwargs)
84
+ finally:
85
+ await instance.close()
86
+
60
87
  async def _request_with_session(
61
88
  self,
62
89
  session: aiohttp.ClientSession,
63
- url: str,
90
+ url_or_path: str,
64
91
  method: str,
65
92
  params: Optional[Dict[str, Any]],
66
93
  body: Optional[Any],
@@ -72,7 +99,7 @@ class HttpClient:
72
99
  headers = CaseInsensitiveDict(headers)
73
100
  headers.pop("Content-Type", None)
74
101
 
75
- return await session.request(method, url, params=params, data=data, headers=headers)
102
+ return await session.request(method, url_or_path, params=params, data=data, headers=headers)
76
103
 
77
104
  def _build_request_body(self, body: Optional[Any], headers: Optional[Dict[str, str]]) -> Tuple[Any, str]:
78
105
  content_type, _ = parse_options_header(headers, "Content-Type")
@@ -82,7 +109,7 @@ class HttpClient:
82
109
 
83
110
  return (body, content_type)
84
111
 
85
- async def _parse_response_content(self, session: aiohttp.ClientSession, response: aiohttp.ClientResponse) -> Tuple[Any, str]:
112
+ async def _parse_response_content(self, response: aiohttp.ClientResponse) -> Tuple[Any, str]:
86
113
  content_type, _ = parse_options_header(response.headers, "Content-Type")
87
114
 
88
115
  if content_type == "application/json":
@@ -94,4 +121,4 @@ class HttpClient:
94
121
  _, disposition = parse_options_header(response.headers, "Content-Disposition")
95
122
  filename = disposition.get("filename")
96
123
 
97
- return (HttpStreamResource(session, response.content, content_type, filename), content_type)
124
+ return (HttpStreamResource(response, content_type, filename), content_type)
@@ -0,0 +1,20 @@
1
+ from typing import Union, List
2
+
3
+ def is_status_code_matched(status_code: int, conditions: List[Union[int, str]]) -> bool:
4
+ for condition in conditions:
5
+ if isinstance(condition, int) or (isinstance(condition, str) and condition.isdigit()):
6
+ if status_code == int(condition):
7
+ return True
8
+ continue
9
+
10
+ if isinstance(condition, str) and condition.endswith("x"):
11
+ try:
12
+ prefix = int(condition.rstrip("x"))
13
+ factor = 10 ** (len(condition) - len(str(prefix)))
14
+ if prefix * factor <= status_code < (prefix + 1) * factor:
15
+ return True
16
+ except ValueError:
17
+ pass
18
+ continue
19
+
20
+ return False
@@ -0,0 +1,122 @@
1
+ from typing import Optional, Dict, Tuple, AsyncIterator, Any
2
+ from mcp.client.streamable_http import streamablehttp_client
3
+ from mcp import ClientSession
4
+
5
+ class McpClient:
6
+ def __init__(self, url: str, headers: Optional[Dict[str, str]] = None):
7
+ self.url: str = url
8
+ self.headers: Optional[Dict[str, str]] = headers
9
+ self.session: Optional[ClientSession] = ClientSession(streamablehttp_client(self.url, headers=self.headers))
10
+ self._initialized = False
11
+
12
+ async def __aenter__(self):
13
+ if self.session is None:
14
+ self.session = ClientSession(streamablehttp_client(self.url, headers=self.headers))
15
+ await self._ensure_initialized()
16
+ return self
17
+
18
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
19
+ await self.close()
20
+
21
+ async def call_tool(self, name: str, arguments: Optional[Dict[str, Any]] = None, raise_on_error: bool = True) -> Any:
22
+ """
23
+ Call a specific tool on the MCP server
24
+
25
+ Args:
26
+ name: The name of the tool to call
27
+ arguments: Arguments to pass to the tool
28
+ raise_on_error: Whether to raise exceptions on error responses
29
+
30
+ Returns:
31
+ The tool execution result
32
+ """
33
+ await self._ensure_initialized()
34
+ try:
35
+ return await self.session.call_tool(name, arguments)
36
+ except:
37
+ pass
38
+
39
+ async def list_tools(self) -> Any:
40
+ """List all available tools from the MCP server"""
41
+ await self._ensure_initialized()
42
+ try:
43
+ return await self.session.list_tools()
44
+ except:
45
+ pass
46
+
47
+ async def read_resource(self, uri: str, raise_on_error: bool = True) -> Any:
48
+ """
49
+ Read a specific resource from the MCP server
50
+
51
+ Args:
52
+ uri: The URI of the resource to read
53
+ raise_on_error: Whether to raise exceptions on error responses
54
+
55
+ Returns:
56
+ The resource content
57
+ """
58
+ await self._ensure_initialized()
59
+ try:
60
+ return await self.session.read_resource(uri)
61
+ except:
62
+ pass
63
+
64
+ async def list_resources(self) -> Any:
65
+ """List all available resources from the MCP server"""
66
+ return await self.session.list_resources()
67
+
68
+ async def get_prompt(self, name: str, arguments: Optional[Dict[str, Any]] = None, raise_on_error: bool = True) -> Any:
69
+ """
70
+ Get a specific prompt from the MCP server
71
+
72
+ Args:
73
+ name: The name of the prompt to get
74
+ arguments: Arguments to pass to the prompt
75
+ raise_on_error: Whether to raise exceptions on error responses
76
+
77
+ Returns:
78
+ The prompt content
79
+ """
80
+ await self._ensure_initialized()
81
+ try:
82
+ return await self.session.get_prompt(name, arguments)
83
+ except:
84
+ pass
85
+
86
+ async def list_prompts(self) -> Any:
87
+ """List all available prompts from the MCP server"""
88
+ await self._ensure_initialized()
89
+ try:
90
+ return await self.session.list_prompts()
91
+ except:
92
+ pass
93
+
94
+ async def ping(self) -> bool:
95
+ """
96
+ Ping the MCP server to check connectivity
97
+
98
+ Returns:
99
+ True if the server is reachable, False otherwise
100
+ """
101
+ await self._ensure_initialized()
102
+ try:
103
+ await self.session.send_ping()
104
+ return True
105
+ except:
106
+ return False
107
+
108
+ async def close(self) -> None:
109
+ """Close the MCP client session"""
110
+ await self._close()
111
+
112
+ async def _ensure_initialized(self):
113
+ if not self._initialized:
114
+ await self.session.__aenter__()
115
+ await self.session.initialize()
116
+ self._initialized = True
117
+
118
+ async def _close(self) -> None:
119
+ if self.session:
120
+ await self.session.__aexit__(None, None, None)
121
+ self.session = None
122
+ self._initialized = False
@@ -19,7 +19,7 @@ class StreamResource(ABC):
19
19
  return self._iterate_stream()
20
20
 
21
21
  @abstractmethod
22
- async def close(self):
22
+ async def close(self) -> None:
23
23
  pass
24
24
 
25
25
  @abstractmethod
@@ -37,7 +37,7 @@ class Base64StreamResource(StreamResource):
37
37
  self.stream = io.BytesIO(base64.b64decode(self.encoded))
38
38
  return self
39
39
 
40
- async def close(self):
40
+ async def close(self) -> None:
41
41
  self.stream.close()
42
42
  self.stream = None
43
43
 
@@ -128,7 +128,7 @@ class TemplateRenderer:
128
128
  return await save_stream_to_temporary_file(Base64StreamResource(value), subtype)
129
129
 
130
130
  if format == "url" and isinstance(value, str):
131
- return await save_stream_to_temporary_file(await HttpClient().request(value), subtype)
131
+ return await save_stream_to_temporary_file(await HttpClient.request_once(value), subtype)
132
132
 
133
133
  if isinstance(value, StreamResource):
134
134
  return await save_stream_to_temporary_file(value, subtype)
@@ -15,8 +15,8 @@ class PollingCompletionConfig(CommonCompletionConfig):
15
15
  body: Optional[Dict[str, Any]] = Field(default_factory=dict)
16
16
  params: Optional[Dict[str, str]] = Field(default_factory=dict)
17
17
  status: Optional[str] = None
18
- success_when: Optional[List[str]] = None
19
- fail_when: Optional[List[str]] = None
18
+ success_when: Optional[List[Union[int, str]]] = None
19
+ fail_when: Optional[List[Union[int, str]]] = None
20
20
  interval: Optional[str] = None
21
21
  timeout: Optional[str] = None
22
22
 
@@ -28,8 +28,8 @@ class PollingCompletionConfig(CommonCompletionConfig):
28
28
 
29
29
  @model_validator(mode="before")
30
30
  def normalize_status_fields(cls, values: Dict[str, Any]):
31
- for key in ("success_when", "fail_when"):
32
- if isinstance(values.get(key), str):
31
+ for key in [ "success_when", "fail_when" ]:
32
+ if isinstance(values.get(key), (int, str)):
33
33
  values[key] = [ values[key] ]
34
34
  return values
35
35
 
@@ -9,7 +9,7 @@ class HttpClientComponentConfig(CommonComponentConfig):
9
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
+
13
13
  @model_validator(mode="before")
14
14
  def inflate_single_action(cls, values: Dict[str, Any]):
15
15
  if "actions" not in values:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: model-compose
3
- Version: 0.1.8
3
+ Version: 0.1.9
4
4
  Summary: model-compose: Declarative AI Model and Workflow Orchestrator (part of the Mindor project)
5
5
  Author-email: Hanyeol Cho <hanyeol.cho@gmail.com>
6
6
  License-Expression: MIT
@@ -51,6 +51,7 @@ src/mindor/core/utils/__init__.py
51
51
  src/mindor/core/utils/expiring.py
52
52
  src/mindor/core/utils/http_client.py
53
53
  src/mindor/core/utils/http_request.py
54
+ src/mindor/core/utils/http_status.py
54
55
  src/mindor/core/utils/image.py
55
56
  src/mindor/core/utils/mcp_client.py
56
57
  src/mindor/core/utils/ssh_client.py
@@ -1,4 +0,0 @@
1
- from typing import Optional, Dict, Tuple, AsyncIterator, Any
2
-
3
- class McpClient:
4
- pass
File without changes
File without changes
File without changes