aethergraph 0.1.0a1__py3-none-any.whl → 0.1.0a3__py3-none-any.whl

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 (267) hide show
  1. aethergraph/__init__.py +4 -10
  2. aethergraph/__main__.py +296 -0
  3. aethergraph/api/v1/__init__.py +0 -0
  4. aethergraph/api/v1/agents.py +46 -0
  5. aethergraph/api/v1/apps.py +70 -0
  6. aethergraph/api/v1/artifacts.py +415 -0
  7. aethergraph/api/v1/channels.py +89 -0
  8. aethergraph/api/v1/deps.py +168 -0
  9. aethergraph/api/v1/graphs.py +259 -0
  10. aethergraph/api/v1/identity.py +25 -0
  11. aethergraph/api/v1/memory.py +353 -0
  12. aethergraph/api/v1/misc.py +47 -0
  13. aethergraph/api/v1/pagination.py +29 -0
  14. aethergraph/api/v1/runs.py +568 -0
  15. aethergraph/api/v1/schemas.py +535 -0
  16. aethergraph/api/v1/session.py +323 -0
  17. aethergraph/api/v1/stats.py +201 -0
  18. aethergraph/api/v1/viz.py +152 -0
  19. aethergraph/config/config.py +22 -0
  20. aethergraph/config/loader.py +3 -2
  21. aethergraph/config/storage.py +209 -0
  22. aethergraph/contracts/__init__.py +0 -0
  23. aethergraph/contracts/services/__init__.py +0 -0
  24. aethergraph/contracts/services/artifacts.py +27 -14
  25. aethergraph/contracts/services/memory.py +45 -17
  26. aethergraph/contracts/services/metering.py +129 -0
  27. aethergraph/contracts/services/runs.py +50 -0
  28. aethergraph/contracts/services/sessions.py +87 -0
  29. aethergraph/contracts/services/state_stores.py +3 -0
  30. aethergraph/contracts/services/viz.py +44 -0
  31. aethergraph/contracts/storage/artifact_index.py +88 -0
  32. aethergraph/contracts/storage/artifact_store.py +99 -0
  33. aethergraph/contracts/storage/async_kv.py +34 -0
  34. aethergraph/contracts/storage/blob_store.py +50 -0
  35. aethergraph/contracts/storage/doc_store.py +35 -0
  36. aethergraph/contracts/storage/event_log.py +31 -0
  37. aethergraph/contracts/storage/vector_index.py +48 -0
  38. aethergraph/core/__init__.py +0 -0
  39. aethergraph/core/execution/forward_scheduler.py +13 -2
  40. aethergraph/core/execution/global_scheduler.py +21 -15
  41. aethergraph/core/execution/step_forward.py +10 -1
  42. aethergraph/core/graph/__init__.py +0 -0
  43. aethergraph/core/graph/graph_builder.py +8 -4
  44. aethergraph/core/graph/graph_fn.py +156 -15
  45. aethergraph/core/graph/graph_spec.py +8 -0
  46. aethergraph/core/graph/graphify.py +146 -27
  47. aethergraph/core/graph/node_spec.py +0 -2
  48. aethergraph/core/graph/node_state.py +3 -0
  49. aethergraph/core/graph/task_graph.py +39 -1
  50. aethergraph/core/runtime/__init__.py +0 -0
  51. aethergraph/core/runtime/ad_hoc_context.py +64 -4
  52. aethergraph/core/runtime/base_service.py +28 -4
  53. aethergraph/core/runtime/execution_context.py +13 -15
  54. aethergraph/core/runtime/graph_runner.py +222 -37
  55. aethergraph/core/runtime/node_context.py +510 -6
  56. aethergraph/core/runtime/node_services.py +12 -5
  57. aethergraph/core/runtime/recovery.py +15 -1
  58. aethergraph/core/runtime/run_manager.py +783 -0
  59. aethergraph/core/runtime/run_manager_local.py +204 -0
  60. aethergraph/core/runtime/run_registration.py +2 -2
  61. aethergraph/core/runtime/run_types.py +89 -0
  62. aethergraph/core/runtime/runtime_env.py +136 -7
  63. aethergraph/core/runtime/runtime_metering.py +71 -0
  64. aethergraph/core/runtime/runtime_registry.py +36 -13
  65. aethergraph/core/runtime/runtime_services.py +194 -6
  66. aethergraph/core/tools/builtins/toolset.py +1 -1
  67. aethergraph/core/tools/toolkit.py +5 -0
  68. aethergraph/plugins/agents/default_chat_agent copy.py +90 -0
  69. aethergraph/plugins/agents/default_chat_agent.py +171 -0
  70. aethergraph/plugins/agents/shared.py +81 -0
  71. aethergraph/plugins/channel/adapters/webui.py +112 -112
  72. aethergraph/plugins/channel/routes/webui_routes.py +367 -102
  73. aethergraph/plugins/channel/utils/slack_utils.py +115 -59
  74. aethergraph/plugins/channel/utils/telegram_utils.py +88 -47
  75. aethergraph/plugins/channel/websockets/weibui_ws.py +172 -0
  76. aethergraph/runtime/__init__.py +15 -0
  77. aethergraph/server/app_factory.py +196 -34
  78. aethergraph/server/clients/channel_client.py +202 -0
  79. aethergraph/server/http/channel_http_routes.py +116 -0
  80. aethergraph/server/http/channel_ws_routers.py +45 -0
  81. aethergraph/server/loading.py +117 -0
  82. aethergraph/server/server.py +131 -0
  83. aethergraph/server/server_state.py +240 -0
  84. aethergraph/server/start.py +227 -66
  85. aethergraph/server/ui_static/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  86. aethergraph/server/ui_static/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  87. aethergraph/server/ui_static/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  88. aethergraph/server/ui_static/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  89. aethergraph/server/ui_static/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  90. aethergraph/server/ui_static/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  91. aethergraph/server/ui_static/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  92. aethergraph/server/ui_static/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  93. aethergraph/server/ui_static/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  94. aethergraph/server/ui_static/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  95. aethergraph/server/ui_static/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  96. aethergraph/server/ui_static/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  97. aethergraph/server/ui_static/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  98. aethergraph/server/ui_static/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  99. aethergraph/server/ui_static/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  100. aethergraph/server/ui_static/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  101. aethergraph/server/ui_static/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  102. aethergraph/server/ui_static/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  103. aethergraph/server/ui_static/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  104. aethergraph/server/ui_static/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  105. aethergraph/server/ui_static/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  106. aethergraph/server/ui_static/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  107. aethergraph/server/ui_static/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  108. aethergraph/server/ui_static/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  109. aethergraph/server/ui_static/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  110. aethergraph/server/ui_static/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  111. aethergraph/server/ui_static/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  112. aethergraph/server/ui_static/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  113. aethergraph/server/ui_static/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  114. aethergraph/server/ui_static/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  115. aethergraph/server/ui_static/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  116. aethergraph/server/ui_static/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  117. aethergraph/server/ui_static/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  118. aethergraph/server/ui_static/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  119. aethergraph/server/ui_static/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  120. aethergraph/server/ui_static/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  121. aethergraph/server/ui_static/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  122. aethergraph/server/ui_static/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  123. aethergraph/server/ui_static/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  124. aethergraph/server/ui_static/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  125. aethergraph/server/ui_static/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  126. aethergraph/server/ui_static/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  127. aethergraph/server/ui_static/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  128. aethergraph/server/ui_static/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  129. aethergraph/server/ui_static/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  130. aethergraph/server/ui_static/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  131. aethergraph/server/ui_static/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  132. aethergraph/server/ui_static/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  133. aethergraph/server/ui_static/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  134. aethergraph/server/ui_static/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  135. aethergraph/server/ui_static/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  136. aethergraph/server/ui_static/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  137. aethergraph/server/ui_static/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  138. aethergraph/server/ui_static/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  139. aethergraph/server/ui_static/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  140. aethergraph/server/ui_static/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  141. aethergraph/server/ui_static/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  142. aethergraph/server/ui_static/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  143. aethergraph/server/ui_static/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  144. aethergraph/server/ui_static/assets/index-BR5GtXcZ.css +1 -0
  145. aethergraph/server/ui_static/assets/index-CQ0HZZ83.js +400 -0
  146. aethergraph/server/ui_static/index.html +15 -0
  147. aethergraph/server/ui_static/logo.png +0 -0
  148. aethergraph/services/artifacts/__init__.py +0 -0
  149. aethergraph/services/artifacts/facade.py +1239 -132
  150. aethergraph/services/auth/{dev.py → authn.py} +0 -8
  151. aethergraph/services/auth/authz.py +100 -0
  152. aethergraph/services/channel/__init__.py +0 -0
  153. aethergraph/services/channel/channel_bus.py +19 -1
  154. aethergraph/services/channel/factory.py +13 -1
  155. aethergraph/services/channel/ingress.py +311 -0
  156. aethergraph/services/channel/queue_adapter.py +75 -0
  157. aethergraph/services/channel/session.py +502 -19
  158. aethergraph/services/container/default_container.py +122 -43
  159. aethergraph/services/continuations/continuation.py +6 -0
  160. aethergraph/services/continuations/stores/fs_store.py +19 -0
  161. aethergraph/services/eventhub/event_hub.py +76 -0
  162. aethergraph/services/kv/__init__.py +0 -0
  163. aethergraph/services/kv/ephemeral.py +244 -0
  164. aethergraph/services/llm/__init__.py +0 -0
  165. aethergraph/services/llm/generic_client copy.py +691 -0
  166. aethergraph/services/llm/generic_client.py +1288 -187
  167. aethergraph/services/llm/providers.py +3 -1
  168. aethergraph/services/llm/types.py +47 -0
  169. aethergraph/services/llm/utils.py +284 -0
  170. aethergraph/services/logger/std.py +3 -0
  171. aethergraph/services/mcp/__init__.py +9 -0
  172. aethergraph/services/mcp/http_client.py +38 -0
  173. aethergraph/services/mcp/service.py +225 -1
  174. aethergraph/services/mcp/stdio_client.py +41 -6
  175. aethergraph/services/mcp/ws_client.py +44 -2
  176. aethergraph/services/memory/__init__.py +0 -0
  177. aethergraph/services/memory/distillers/llm_long_term.py +234 -0
  178. aethergraph/services/memory/distillers/llm_meta_summary.py +398 -0
  179. aethergraph/services/memory/distillers/long_term.py +225 -0
  180. aethergraph/services/memory/facade/__init__.py +3 -0
  181. aethergraph/services/memory/facade/chat.py +440 -0
  182. aethergraph/services/memory/facade/core.py +447 -0
  183. aethergraph/services/memory/facade/distillation.py +424 -0
  184. aethergraph/services/memory/facade/rag.py +410 -0
  185. aethergraph/services/memory/facade/results.py +315 -0
  186. aethergraph/services/memory/facade/retrieval.py +139 -0
  187. aethergraph/services/memory/facade/types.py +77 -0
  188. aethergraph/services/memory/facade/utils.py +43 -0
  189. aethergraph/services/memory/facade_dep.py +1539 -0
  190. aethergraph/services/memory/factory.py +9 -3
  191. aethergraph/services/memory/utils.py +10 -0
  192. aethergraph/services/metering/eventlog_metering.py +470 -0
  193. aethergraph/services/metering/noop.py +25 -4
  194. aethergraph/services/rag/__init__.py +0 -0
  195. aethergraph/services/rag/facade.py +279 -23
  196. aethergraph/services/rag/index_factory.py +2 -2
  197. aethergraph/services/rag/node_rag.py +317 -0
  198. aethergraph/services/rate_limit/inmem_rate_limit.py +24 -0
  199. aethergraph/services/registry/__init__.py +0 -0
  200. aethergraph/services/registry/agent_app_meta.py +419 -0
  201. aethergraph/services/registry/registry_key.py +1 -1
  202. aethergraph/services/registry/unified_registry.py +74 -6
  203. aethergraph/services/scope/scope.py +159 -0
  204. aethergraph/services/scope/scope_factory.py +164 -0
  205. aethergraph/services/state_stores/serialize.py +5 -0
  206. aethergraph/services/state_stores/utils.py +2 -1
  207. aethergraph/services/viz/__init__.py +0 -0
  208. aethergraph/services/viz/facade.py +413 -0
  209. aethergraph/services/viz/viz_service.py +69 -0
  210. aethergraph/storage/artifacts/artifact_index_jsonl.py +180 -0
  211. aethergraph/storage/artifacts/artifact_index_sqlite.py +426 -0
  212. aethergraph/storage/artifacts/cas_store.py +422 -0
  213. aethergraph/storage/artifacts/fs_cas.py +18 -0
  214. aethergraph/storage/artifacts/s3_cas.py +14 -0
  215. aethergraph/storage/artifacts/utils.py +124 -0
  216. aethergraph/storage/blob/fs_blob.py +86 -0
  217. aethergraph/storage/blob/s3_blob.py +115 -0
  218. aethergraph/storage/continuation_store/fs_cont.py +283 -0
  219. aethergraph/storage/continuation_store/inmem_cont.py +146 -0
  220. aethergraph/storage/continuation_store/kvdoc_cont.py +261 -0
  221. aethergraph/storage/docstore/fs_doc.py +63 -0
  222. aethergraph/storage/docstore/sqlite_doc.py +31 -0
  223. aethergraph/storage/docstore/sqlite_doc_sync.py +90 -0
  224. aethergraph/storage/eventlog/fs_event.py +136 -0
  225. aethergraph/storage/eventlog/sqlite_event.py +47 -0
  226. aethergraph/storage/eventlog/sqlite_event_sync.py +178 -0
  227. aethergraph/storage/factory.py +432 -0
  228. aethergraph/storage/fs_utils.py +28 -0
  229. aethergraph/storage/graph_state_store/state_store.py +64 -0
  230. aethergraph/storage/kv/inmem_kv.py +103 -0
  231. aethergraph/storage/kv/layered_kv.py +52 -0
  232. aethergraph/storage/kv/sqlite_kv.py +39 -0
  233. aethergraph/storage/kv/sqlite_kv_sync.py +98 -0
  234. aethergraph/storage/memory/event_persist.py +68 -0
  235. aethergraph/storage/memory/fs_persist.py +118 -0
  236. aethergraph/{services/memory/hotlog_kv.py → storage/memory/hotlog.py} +8 -2
  237. aethergraph/{services → storage}/memory/indices.py +31 -7
  238. aethergraph/storage/metering/meter_event.py +55 -0
  239. aethergraph/storage/runs/doc_store.py +280 -0
  240. aethergraph/storage/runs/inmen_store.py +82 -0
  241. aethergraph/storage/runs/sqlite_run_store.py +403 -0
  242. aethergraph/storage/sessions/doc_store.py +183 -0
  243. aethergraph/storage/sessions/inmem_store.py +110 -0
  244. aethergraph/storage/sessions/sqlite_session_store.py +399 -0
  245. aethergraph/storage/vector_index/chroma_index.py +138 -0
  246. aethergraph/storage/vector_index/faiss_index.py +179 -0
  247. aethergraph/storage/vector_index/sqlite_index.py +187 -0
  248. {aethergraph-0.1.0a1.dist-info → aethergraph-0.1.0a3.dist-info}/METADATA +138 -31
  249. aethergraph-0.1.0a3.dist-info/RECORD +356 -0
  250. aethergraph-0.1.0a3.dist-info/entry_points.txt +3 -0
  251. aethergraph/services/artifacts/factory.py +0 -35
  252. aethergraph/services/artifacts/fs_store.py +0 -656
  253. aethergraph/services/artifacts/jsonl_index.py +0 -123
  254. aethergraph/services/artifacts/sqlite_index.py +0 -209
  255. aethergraph/services/memory/distillers/episode.py +0 -116
  256. aethergraph/services/memory/distillers/rolling.py +0 -74
  257. aethergraph/services/memory/facade.py +0 -633
  258. aethergraph/services/memory/persist_fs.py +0 -40
  259. aethergraph/services/rag/index/base.py +0 -27
  260. aethergraph/services/rag/index/faiss_index.py +0 -121
  261. aethergraph/services/rag/index/sqlite_index.py +0 -134
  262. aethergraph-0.1.0a1.dist-info/RECORD +0 -182
  263. aethergraph-0.1.0a1.dist-info/entry_points.txt +0 -2
  264. {aethergraph-0.1.0a1.dist-info → aethergraph-0.1.0a3.dist-info}/WHEEL +0 -0
  265. {aethergraph-0.1.0a1.dist-info → aethergraph-0.1.0a3.dist-info}/licenses/LICENSE +0 -0
  266. {aethergraph-0.1.0a1.dist-info → aethergraph-0.1.0a3.dist-info}/licenses/NOTICE +0 -0
  267. {aethergraph-0.1.0a1.dist-info → aethergraph-0.1.0a3.dist-info}/top_level.txt +0 -0
@@ -1,3 +1,5 @@
1
1
  from typing import Literal
2
2
 
3
- Provider = Literal["openai", "azure", "anthropic", "google", "openrouter", "lmstudio", "ollama"]
3
+ Provider = Literal[
4
+ "openai", "azure", "anthropic", "google", "openrouter", "lmstudio", "ollama", "dummy"
5
+ ] # dummy for testing
@@ -0,0 +1,47 @@
1
+ from dataclasses import dataclass
2
+ from typing import Any, Literal
3
+
4
+ ChatOutputFormat = Literal["text", "json_object", "json_schema"]
5
+
6
+ ImageFormat = Literal["png", "jpeg", "webp"]
7
+ ImageResponseFormat = Literal["b64_json", "url"] # url only for dall-e models typically
8
+
9
+
10
+ @dataclass(frozen=True)
11
+ class JsonSchemaSpec:
12
+ name: str
13
+ schema: dict[str, Any]
14
+ strict: bool = True
15
+
16
+
17
+ @dataclass(frozen=True)
18
+ class ImageInput:
19
+ data: bytes | None = None
20
+ b64: str | None = None # base64 without data: prefix
21
+ mime_type: str | None = None
22
+ url: str | None = None # http(s) url OR provider file_uri
23
+ is_file_uri: bool = False # Gemini file URIs
24
+
25
+
26
+ class LLMUnsupportedFeatureError(RuntimeError):
27
+ def __init__(self, provider: str, model: str | None, feature: str, detail: str | None = None):
28
+ msg = f"Provider '{provider}' / model '{model or '?'}' does not support: {feature}"
29
+ if detail:
30
+ msg += f" ({detail})"
31
+ super().__init__(msg)
32
+
33
+
34
+ @dataclass
35
+ class GeneratedImage:
36
+ # Exactly one of these is typically present.
37
+ b64: str | None = None
38
+ url: str | None = None
39
+ mime_type: str | None = None
40
+ revised_prompt: str | None = None
41
+
42
+
43
+ @dataclass
44
+ class ImageGenerationResult:
45
+ images: list[GeneratedImage]
46
+ usage: dict[str, int] # often empty for image endpoints
47
+ raw: dict[str, Any] | None = None
@@ -0,0 +1,284 @@
1
+ import base64
2
+ from collections.abc import Sequence
3
+ import json
4
+ import re
5
+ from typing import Any, Literal
6
+
7
+ from aethergraph.services.llm.types import ImageInput
8
+
9
+ ChatOutputFormat = Literal["text", "json_object", "json_schema"]
10
+
11
+
12
+ def _is_data_url(s: str) -> bool:
13
+ return isinstance(s, str) and s.startswith("data:") and ";base64," in s
14
+
15
+
16
+ def _image_bytes_to_b64(data: bytes) -> str:
17
+ return base64.b64encode(data).decode("ascii")
18
+
19
+
20
+ def _data_url_to_b64_and_mime(url: str) -> tuple[str, str]:
21
+ head, b64 = url.split(",", 1)
22
+ mime = head.split(";")[0].split(":", 1)[1]
23
+ return b64, mime
24
+
25
+
26
+ def _ensure_b64(img: ImageInput) -> tuple[str, str]:
27
+ if img.b64 and img.mime_type:
28
+ return img.b64, img.mime_type
29
+ if img.url and _is_data_url(img.url):
30
+ return _data_url_to_b64_and_mime(img.url)
31
+ if img.data and img.mime_type:
32
+ import base64
33
+
34
+ return base64.b64encode(img.data).decode("ascii"), img.mime_type
35
+ raise ValueError("ImageInput must have (b64+mime_type) or (data+mime_type) or a data: URL")
36
+
37
+
38
+ def _normalize_messages(messages: Sequence[dict[str, Any]]) -> list[dict[str, Any]]:
39
+ """
40
+ Normalize many common message shapes into:
41
+ {"role": "...", "parts": [{"type":"text","text":...} | {"type":"image","image": ImageInput}]}
42
+ Supports:
43
+ - {"role","content":"text"}
44
+ - ChatCompletions multimodal image_url parts
45
+ - Responses input_image parts
46
+ - Anthropic image source blocks
47
+ """
48
+ out: list[dict[str, Any]] = []
49
+ for m in messages:
50
+ role = (m.get("role") or "user").lower()
51
+ content = m.get("content")
52
+ parts: list[dict[str, Any]] = []
53
+
54
+ if isinstance(content, str):
55
+ parts.append({"type": "text", "text": content})
56
+ elif isinstance(content, list):
57
+ for p in content:
58
+ if not isinstance(p, dict):
59
+ continue
60
+ t = p.get("type")
61
+
62
+ if t in ("text", "input_text", "output_text"):
63
+ parts.append({"type": "text", "text": p.get("text", "")})
64
+ continue
65
+
66
+ if t == "image_url":
67
+ iu = p.get("image_url") or {}
68
+ url = iu.get("url") or p.get("url")
69
+ if isinstance(url, str):
70
+ parts.append({"type": "image", "image": ImageInput(url=url)})
71
+ continue
72
+
73
+ if t == "input_image":
74
+ url = p.get("image_url")
75
+ if isinstance(url, str):
76
+ parts.append({"type": "image", "image": ImageInput(url=url)})
77
+ continue
78
+
79
+ if t == "image":
80
+ src = p.get("source") or {}
81
+ if src.get("type") == "base64":
82
+ parts.append(
83
+ {
84
+ "type": "image",
85
+ "image": ImageInput(
86
+ b64=src.get("data"), mime_type=src.get("media_type")
87
+ ),
88
+ }
89
+ )
90
+ elif src.get("type") == "url":
91
+ parts.append({"type": "image", "image": ImageInput(url=src.get("url"))})
92
+ continue
93
+
94
+ out.append({"role": role, "parts": parts})
95
+ return out
96
+
97
+
98
+ def _has_images(norm: Sequence[dict[str, Any]]) -> bool:
99
+ return any(p.get("type") == "image" for m in norm for p in m.get("parts", []))
100
+
101
+
102
+ def _strip_code_fences(s: str) -> str:
103
+ s = s.strip()
104
+ if s.startswith("```"):
105
+ s = re.sub(r"^```[a-zA-Z0-9_-]*\s*", "", s)
106
+ s = re.sub(r"\s*```$", "", s)
107
+ return s.strip()
108
+
109
+
110
+ def _extract_json_text(text: str) -> str:
111
+ t = _strip_code_fences(text)
112
+ if not t:
113
+ return t
114
+ if t[0] in "{[":
115
+ return t
116
+ m = re.search(r"(\{.*\}|\[.*\])", t, flags=re.DOTALL)
117
+ return m.group(1).strip() if m else t
118
+
119
+
120
+ def _validate_json_schema(obj: Any, schema: dict[str, Any]) -> None:
121
+ try:
122
+ import jsonschema # type: ignore
123
+ except Exception:
124
+ return
125
+ jsonschema.validate(instance=obj, schema=schema)
126
+
127
+
128
+ def _ensure_system_json_directive(
129
+ messages: list[dict[str, Any]], *, schema: dict[str, Any] | None
130
+ ) -> list[dict[str, Any]]:
131
+ directive = "Return ONLY valid JSON. No markdown, no commentary."
132
+ if schema is not None:
133
+ directive += "\nThe JSON MUST conform to this JSON Schema:\n" + json.dumps(
134
+ schema, ensure_ascii=False
135
+ )
136
+ return [{"role": "system", "content": directive}] + list(messages)
137
+
138
+
139
+ def _message_content_has_images(messages: list[dict[str, Any]]) -> bool:
140
+ for m in messages:
141
+ c = m.get("content")
142
+ if isinstance(c, list):
143
+ for p in c:
144
+ if isinstance(p, dict) and p.get("type") in ("image_url", "input_image", "image"):
145
+ return True
146
+ return False
147
+
148
+
149
+ def _to_anthropic_blocks(content: Any) -> list[dict[str, Any]]:
150
+ """
151
+ Accept:
152
+ - str -> [{"type":"text","text":...}]
153
+ - OpenAI multimodal list -> convert data URLs -> anthropic base64 image blocks
154
+ - Anthropic blocks -> passthrough
155
+ """
156
+ if isinstance(content, str):
157
+ return [{"type": "text", "text": content}]
158
+
159
+ if isinstance(content, list):
160
+ blocks: list[dict[str, Any]] = []
161
+ for p in content:
162
+ if not isinstance(p, dict):
163
+ continue
164
+ t = p.get("type")
165
+ if t in ("text", "input_text", "output_text"):
166
+ blocks.append({"type": "text", "text": p.get("text", "")})
167
+ elif t == "image" and "source" in p:
168
+ blocks.append(p)
169
+ elif t in ("image_url", "input_image"):
170
+ url = None
171
+ if t == "image_url":
172
+ url = (p.get("image_url") or {}).get("url") or p.get("url")
173
+ else:
174
+ url = p.get("image_url")
175
+ if isinstance(url, str) and _is_data_url(url):
176
+ b64, mime = _data_url_to_b64_and_mime(url)
177
+ blocks.append(
178
+ {
179
+ "type": "image",
180
+ "source": {"type": "base64", "media_type": mime, "data": b64},
181
+ }
182
+ )
183
+ elif isinstance(url, str):
184
+ raise RuntimeError(
185
+ "Anthropic vision: provide data: URLs (base64) for images (no remote fetch in client)."
186
+ )
187
+ return blocks
188
+
189
+ return [{"type": "text", "text": str(content)}]
190
+
191
+
192
+ def _to_gemini_parts(content: Any) -> list[dict[str, Any]]:
193
+ """
194
+ Gemini REST generateContent supports:
195
+ - {"text": "..."}
196
+ - {"inline_data": {"mime_type": "...", "data": "<base64>"}}
197
+ """
198
+ if isinstance(content, str):
199
+ return [{"text": content}]
200
+
201
+ if isinstance(content, list):
202
+ parts: list[dict[str, Any]] = []
203
+ for p in content:
204
+ if not isinstance(p, dict):
205
+ continue
206
+ t = p.get("type")
207
+ if t in ("text", "input_text", "output_text"):
208
+ parts.append({"text": p.get("text", "")})
209
+ elif t in ("image_url", "input_image"):
210
+ url = None
211
+ if t == "image_url":
212
+ url = (p.get("image_url") or {}).get("url") or p.get("url")
213
+ else:
214
+ url = p.get("image_url")
215
+ if isinstance(url, str) and _is_data_url(url):
216
+ b64, mime = _data_url_to_b64_and_mime(url)
217
+ parts.append({"inline_data": {"mime_type": mime, "data": b64}})
218
+ elif isinstance(url, str):
219
+ raise RuntimeError(
220
+ "Gemini vision: provide data: URLs (base64) or file_uri; remote http(s) URLs not accepted inline."
221
+ )
222
+ return parts
223
+
224
+ return [{"text": str(content)}]
225
+
226
+
227
+ def _normalize_openai_responses_input(messages: list[dict[str, Any]]) -> list[dict[str, Any]]:
228
+ """
229
+ Make OpenAI Responses input robust:
230
+ - If content is str: keep as-is
231
+ - If content is OpenAI chat multimodal parts (text/image_url): convert to input_text/input_image
232
+ - If already input_text/input_image: passthrough
233
+ """
234
+ out: list[dict[str, Any]] = []
235
+ for m in messages:
236
+ role = m.get("role", "user")
237
+ c = m.get("content")
238
+
239
+ if isinstance(c, str):
240
+ out.append({"role": role, "content": c})
241
+ continue
242
+
243
+ if isinstance(c, list):
244
+ blocks: list[dict[str, Any]] = []
245
+ for p in c:
246
+ if not isinstance(p, dict):
247
+ continue
248
+ t = p.get("type")
249
+ if t in ("input_text", "input_image"):
250
+ blocks.append(p)
251
+ elif t in ("text", "output_text"):
252
+ blocks.append({"type": "input_text", "text": p.get("text", "")})
253
+ elif t == "image_url":
254
+ url = (p.get("image_url") or {}).get("url") or p.get("url")
255
+ if isinstance(url, str):
256
+ blocks.append({"type": "input_image", "image_url": url})
257
+ else:
258
+ # ignore unknown part types
259
+ pass
260
+ out.append({"role": role, "content": blocks})
261
+ continue
262
+
263
+ out.append({"role": role, "content": str(c)})
264
+ return out
265
+
266
+
267
+ def _normalize_base_url_no_trailing_slash(url: str) -> str:
268
+ return (url or "").strip().rstrip("/")
269
+
270
+
271
+ def _azure_images_generations_url(endpoint: str, deployment: str, api_version: str) -> str:
272
+ # endpoint example: https://<resource>.openai.azure.com
273
+ ep = _normalize_base_url_no_trailing_slash(endpoint)
274
+ return f"{ep}/openai/deployments/{deployment}/images/generations?api-version={api_version}"
275
+
276
+
277
+ def _guess_mime_from_format(fmt: str) -> str:
278
+ if fmt == "png":
279
+ return "image/png"
280
+ if fmt == "jpeg":
281
+ return "image/jpeg"
282
+ if fmt == "webp":
283
+ return "image/webp"
284
+ return "application/octet-stream"
@@ -123,6 +123,9 @@ class StdLoggerService(LoggerService):
123
123
  def for_inspect(self) -> logging.Logger:
124
124
  return self.for_namespace("inspect")
125
125
 
126
+ def for_channel(self) -> logging.Logger:
127
+ return self.for_namespace("channel")
128
+
126
129
  def for_scheduler(self) -> logging.Logger:
127
130
  return self.for_namespace("scheduler")
128
131
 
@@ -0,0 +1,9 @@
1
+ from .http_client import HttpMCPClient
2
+ from .stdio_client import StdioMCPClient
3
+ from .ws_client import WsMCPClient
4
+
5
+ __all__ = [
6
+ "HttpMCPClient",
7
+ "StdioMCPClient",
8
+ "WsMCPClient",
9
+ ]
@@ -10,6 +10,44 @@ from aethergraph.contracts.services.mcp import MCPClientProtocol, MCPResource, M
10
10
 
11
11
 
12
12
  class HttpMCPClient(MCPClientProtocol):
13
+ """
14
+ Initialize the HTTP client service with base URL, headers, and timeout.
15
+
16
+ This constructor sets up the base URL for all requests, applies default and custom headers,
17
+ and configures the request timeout. It also initializes internal state for the asynchronous
18
+ HTTP client and concurrency control.
19
+
20
+ Examples:
21
+ Basic usage with default headers:
22
+ ```python
23
+ from aethergraph.services.mcp import HttpMCPClient
24
+ client = HttpMCPClient("https://api.example.com")
25
+ ```
26
+
27
+ Custom headers and timeout:
28
+ ```python
29
+ from aethergraph.services.mcp import HttpMCPClient
30
+ client = HttpMCPClient(
31
+ "https://api.example.com",
32
+ headers={"Authorization": "Bearer <token>"},
33
+ timeout=30.0
34
+ )
35
+ ```
36
+
37
+ Args:
38
+ base_url: The root URL for all HTTP requests (e.g., "https://api.example.com").
39
+ headers: Optional dictionary of additional HTTP headers to include with each request.
40
+ The "Content-Type: application/json" header is always set by default.
41
+ timeout: The maximum time (in seconds) to wait for a response before timing out.
42
+
43
+ Returns:
44
+ None: Initializes the HttpMCPClient instance.
45
+
46
+ Notes:
47
+ - Ensure that the base_url does not have a trailing slash; it will be added automatically.
48
+ - The client uses asynchronous HTTP requests for non-blocking operations.
49
+ """
50
+
13
51
  def __init__(
14
52
  self,
15
53
  base_url: str,