aethergraph 0.1.0a1__py3-none-any.whl → 0.1.0a2__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 +293 -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 +190 -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.0a2.dist-info}/METADATA +138 -31
  249. aethergraph-0.1.0a2.dist-info/RECORD +356 -0
  250. aethergraph-0.1.0a2.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.0a2.dist-info}/WHEEL +0 -0
  265. {aethergraph-0.1.0a1.dist-info → aethergraph-0.1.0a2.dist-info}/licenses/LICENSE +0 -0
  266. {aethergraph-0.1.0a1.dist-info → aethergraph-0.1.0a2.dist-info}/licenses/NOTICE +0 -0
  267. {aethergraph-0.1.0a1.dist-info → aethergraph-0.1.0a2.dist-info}/top_level.txt +0 -0
@@ -15,45 +15,180 @@ class MCPService:
15
15
  """
16
16
 
17
17
  def __init__(self, clients: dict[str, MCPClientProtocol] | None = None, *, secrets=None):
18
+ """
19
+ Initialize the MCPService with optional clients and secrets provider.
20
+
21
+ Examples:
22
+ Basic usage with no clients:
23
+ ```python
24
+ service = MCPService()
25
+ ```
26
+
27
+ With pre-registered clients:
28
+ ```python
29
+ service = MCPService(clients={"default": my_client})
30
+ ```
31
+
32
+ Args:
33
+ clients: Optional dictionary mapping names to MCPClientProtocol instances.
34
+ secrets: Optional secrets provider (not implemented here).
35
+ """
18
36
  self._clients: dict[str, MCPClientProtocol] = clients or {}
19
37
  self._secrets = secrets # optional (Secrets provider) Not implemented here
20
38
 
21
39
  # ---- registration ----
22
40
  def register(self, name: str, client: MCPClientProtocol) -> None:
41
+ """
42
+ Register a new MCP client under a given name.
43
+
44
+ Examples:
45
+ ```python
46
+ context.mcp().register("myserver", my_client)
47
+ ```
48
+
49
+ Args:
50
+ name: The name to register the client under.
51
+ client: The MCPClientProtocol instance to register.
52
+ """
23
53
  self._clients[name] = client
24
54
 
25
55
  def remove(self, name: str) -> None:
56
+ """
57
+ Remove a registered MCP client by name.
58
+
59
+ Examples:
60
+ ```python
61
+ context.mcp().remove("myserver")
62
+ ```
63
+
64
+ Args:
65
+ name: The name of the client to remove.
66
+ """
26
67
  self._clients.pop(name, None)
27
68
 
28
69
  def has(self, name: str) -> bool:
70
+ """
71
+ Check if a client with the given name is registered.
72
+
73
+ Examples:
74
+ ```python
75
+ if context.mcp().has("default"):
76
+ print("Client exists")
77
+ ```
78
+
79
+ Args:
80
+ name: The name to check.
81
+
82
+ Returns:
83
+ bool: True if the client exists, False otherwise.
84
+ """
29
85
  return name in self._clients
30
86
 
31
87
  def names(self) -> list[str]:
88
+ """
89
+ Get a list of all registered client names.
90
+
91
+ Examples:
92
+ ```python
93
+ names = context.mcp().names()
94
+ ```
95
+
96
+ Returns:
97
+ list[str]: List of registered client names.
98
+ """
32
99
  return list(self._clients.keys())
33
100
 
34
101
  def list_clients(self) -> list[str]:
102
+ """
103
+ List all registered client names.
104
+
105
+ Examples:
106
+ ```python
107
+ clients = context.mcp().list_clients()
108
+ ```
109
+
110
+ Returns:
111
+ list[str]: List of registered client names.
112
+ """
35
113
  return list(self._clients.keys())
36
114
 
37
115
  def get(self, name: str = "default") -> MCPClientProtocol:
116
+ """
117
+ Retrieve a registered MCP client by name.
118
+
119
+ Examples:
120
+ ```python
121
+ client = context.mcp().get("default")
122
+ ```
123
+
124
+ Args:
125
+ name: The name of the client to retrieve.
126
+
127
+ Returns:
128
+ MCPClientProtocol: The registered client.
129
+
130
+ Raises:
131
+ KeyError: If the client is not found.
132
+ """
38
133
  if name not in self._clients:
39
134
  raise KeyError(f"Unknown MCP server '{name}'")
40
135
  return self._clients[name]
41
136
 
42
137
  # ---- lifecycle ----
43
138
  async def open(self, name: str) -> None:
139
+ """
140
+ Open the connection for a specific MCP client.
141
+
142
+ Examples:
143
+ ```python
144
+ await context.mcp().open("default")
145
+ ```
146
+
147
+ Args:
148
+ name: The name of the client to open.
149
+ """
44
150
  await self.get(name).open()
45
151
 
46
152
  async def close(self, name: str) -> None:
153
+ """
154
+ Close the connection for a specific MCP client.
155
+
156
+ Examples:
157
+ ```python
158
+ await context.mcp().close("default")
159
+ ```
160
+
161
+ Args:
162
+ name: The name of the client to close.
163
+ """
47
164
  try:
48
165
  await self.get(name).close()
49
166
  except Exception:
50
167
  logger.warning(f"Failed to close MCP client '{name}'")
51
168
 
52
169
  async def open_all(self) -> None:
170
+ """
171
+ Open all registered MCP client connections.
172
+
173
+ Examples:
174
+ ```python
175
+ await context.mcp().open_all()
176
+ ```
177
+
178
+ """
53
179
  for n in self._clients:
54
180
  await self._clients[n].open()
55
181
 
56
182
  async def close_all(self) -> None:
183
+ """
184
+ Close all registered MCP client connections.
185
+
186
+ Examples:
187
+ ```python
188
+ await context.mcp().close_all()
189
+ ```
190
+
191
+ """
57
192
  for n in self._clients:
58
193
  try:
59
194
  await self._clients[n].close()
@@ -64,29 +199,103 @@ class MCPService:
64
199
  async def call(
65
200
  self, name: str, tool: str, params: dict[str, Any] | None = None
66
201
  ) -> dict[str, Any]:
202
+ """
203
+ Call a tool on a specific MCP client, opening the connection if needed.
204
+
205
+ Examples:
206
+ ```python
207
+ result = await context.mcp().call("default", "sum", {"a": 1, "b": 2})
208
+ ```
209
+
210
+ Args:
211
+ name: The name of the client to use.
212
+ tool: The tool name to call.
213
+ params: Optional dictionary of parameters for the tool.
214
+
215
+ Returns:
216
+ dict[str, Any]: The result from the tool call.
217
+ """
67
218
  # lazy-open on first use; clients themselves also lazy-reconnect
68
219
  c = self.get(name)
69
220
  await c.open()
70
221
  return await c.call(tool, params or {})
71
222
 
72
223
  async def list_tools(self, name: str) -> list[MCPTool]:
224
+ """
225
+ List all tools available on a specific MCP client.
226
+
227
+ Examples:
228
+ ```python
229
+ tools = await context.mcp().list_tools("default")
230
+ ```
231
+
232
+ Args:
233
+ name: The name of the client.
234
+
235
+ Returns:
236
+ list[MCPTool]: List of available tools.
237
+ """
73
238
  c = self.get(name)
74
239
  await c.open()
75
240
  return await c.list_tools()
76
241
 
77
242
  async def list_resources(self, name: str) -> list[MCPResource]:
243
+ """
244
+ List all resources available on a specific MCP client.
245
+
246
+ Examples:
247
+ ```python
248
+ resources = await context.mcp().list_resources("default")
249
+ ```
250
+
251
+ Args:
252
+ name: The name of the client.
253
+
254
+ Returns:
255
+ list[MCPResource]: List of available resources.
256
+ """
78
257
  c = self.get(name)
79
258
  await c.open()
80
259
  return await c.list_resources()
81
260
 
82
261
  async def read_resource(self, name: str, uri: str) -> dict[str, Any]:
262
+ """
263
+ Read a resource from a specific MCP client.
264
+
265
+ Examples:
266
+ ```python
267
+ data = await context.mcp().read_resource("default", "resource://foo/bar")
268
+ ```
269
+
270
+ Args:
271
+ name: The name of the client.
272
+ uri: The URI of the resource to read.
273
+
274
+ Returns:
275
+ dict[str, Any]: The resource data.
276
+ """
83
277
  c = self.get(name)
84
278
  await c.open()
85
279
  return await c.read_resource(uri)
86
280
 
87
281
  # ---- optional secrets helpers ----
88
282
  def set_header(self, name: str, key: str, value: str) -> None:
89
- """For ws clients: set/override a header at runtime (demo/notebook UX)."""
283
+ """
284
+ Set or override a header for a websocket client at runtime.
285
+
286
+ Examples:
287
+ ```python
288
+ context.mcp().set_header("default", "Authorization", "Bearer token")
289
+ ```
290
+
291
+ Args:
292
+ name: The name of the client.
293
+ key: The header key.
294
+ value: The header value.
295
+
296
+ Raises:
297
+ RuntimeError: If the client does not support headers.
298
+ """
90
299
  c = self.get(name)
91
300
  # duck-typing for ws client
92
301
  if hasattr(c, "headers") and isinstance(c.headers, dict): # type: ignore[attr-defined]
@@ -95,6 +304,21 @@ class MCPService:
95
304
  raise RuntimeError(f"MCP '{name}' does not support headers")
96
305
 
97
306
  def persist_secret(self, secret_name: str, value: str) -> None:
307
+ """
308
+ Persist a secret using the configured secrets provider.
309
+
310
+ Examples:
311
+ ```python
312
+ context.mcp().persist_secret("API_KEY", "my-secret-value")
313
+ ```
314
+
315
+ Args:
316
+ secret_name: The name of the secret.
317
+ value: The value to persist.
318
+
319
+ Raises:
320
+ RuntimeError: If the secrets provider is not writable.
321
+ """
98
322
  if not self._secrets or not hasattr(self._secrets, "set"):
99
323
  raise RuntimeError("Secrets provider is not writable")
100
324
  self._secrets.set(secret_name, value) # type: ignore
@@ -7,13 +7,48 @@ from aethergraph.contracts.services.mcp import MCPClientProtocol, MCPResource, M
7
7
 
8
8
 
9
9
  class StdioMCPClient(MCPClientProtocol):
10
+ """
11
+ Initialize the MCP client service to communicate with a subprocess over stdio using JSON-RPC 2.0.
12
+
13
+ This class launches a subprocess (typically an MCP server), manages its lifecycle, and provides
14
+ asynchronous methods to interact with it using JSON-RPC 2.0 over standard input/output streams.
15
+ It handles command execution, environment setup, request/response serialization, and concurrency
16
+ control for safe multi-call usage.
17
+
18
+ Examples:
19
+ Basic usage with default environment:
20
+ ```python
21
+ from aethergraph.services.mcp import StdioMCPClient
22
+ client = StdioMCPClient(["python", "mcp_server.py"])
23
+ await client.open()
24
+ tools = await client.list_tools()
25
+ await client.close()
26
+ ```
27
+
28
+ Custom environment and timeout:
29
+ ```python
30
+ from aethergraph.services.mcp import StdioMCPClient
31
+ client = StdioMCPClient(
32
+ ["python", "mcp_server.py"],
33
+ env={"MY_ENV_VAR": "value"},
34
+ timeout=30.0
35
+ )
36
+ ```
37
+
38
+ Args:
39
+ cmd: Command to start the MCP server subprocess (list of str).
40
+ env: Optional dictionary of environment variables for the subprocess.
41
+ timeout: Timeout in seconds for each RPC call.
42
+
43
+ Returns:
44
+ None: Initializes the StdioMCPClient instance.
45
+
46
+ Notes:
47
+ - The subprocess should adhere to the JSON-RPC 2.0 specification over stdio.
48
+ - Ensure proper error handling in the subprocess to avoid deadlocks.
49
+ """
50
+
10
51
  def __init__(self, cmd: list[str], env: dict[str, str] | None = None, timeout: float = 60.0):
11
- """MCP client that talks to a subprocess over stdio using JSON-RPC 2.0.
12
- Args:
13
- cmd: Command to start the MCP server subprocess (list of str).
14
- env: Optional environment variables to set for the subprocess.
15
- timeout: Timeout in seconds for each RPC call.
16
- """
17
52
  self.cmd, self.env, self.timeout = cmd, env or {}, timeout
18
53
  self.proc = None
19
54
  self._id = 0
@@ -3,15 +3,57 @@ from __future__ import annotations
3
3
  import asyncio
4
4
  import contextlib
5
5
  import json
6
- from typing import Any
6
+ from typing import TYPE_CHECKING, Any
7
7
 
8
8
  import websockets
9
- from websockets.client import WebSocketClientProtocol
9
+
10
+ if TYPE_CHECKING:
11
+ # only imported for static type checkers; no runtime import, no warning
12
+ from websockets.client import WebSocketClientProtocol
10
13
 
11
14
  from aethergraph.contracts.services.mcp import MCPClientProtocol, MCPResource, MCPTool
12
15
 
13
16
 
14
17
  class WsMCPClient(MCPClientProtocol):
18
+ """
19
+ Initialize the WebSocket MCP client with URL, headers, and connection parameters.
20
+ This class manages a WebSocket connection to an MCP (Modular Control Protocol) server,
21
+ handling connection lifecycle, pinging for keepalive, and concurrency for sending JSON-RPC
22
+ requests. It provides methods to list tools, call tools, list resources, and read resources
23
+ via the MCP protocol.
24
+
25
+ Examples:
26
+ Basic usage with default headers:
27
+ ```python
28
+ from aethergraph.services.mcp import WsMCPClient
29
+ client = WsMCPClient("wss://mcp.example.com/ws")
30
+ await client.open()
31
+ tools = await client.list_tools()
32
+ await client.close()
33
+ ```
34
+
35
+ Custom headers and ping interval:
36
+ ```python
37
+ from aethergraph.services.mcp import WsMCPClient
38
+ client = WsMCPClient(
39
+ "wss://mcp.example.com/ws",
40
+ headers={"Authorization": "Bearer <token>"},
41
+ ping_interval=10.0,
42
+ timeout=30.0
43
+ await client.open()
44
+ ```
45
+
46
+ Args:
47
+ url: The WebSocket URL of the MCP server (e.g., "wss://mcp.example.com/ws").
48
+ headers: Optional dictionary of additional HTTP headers to include in the WebSocket handshake.
49
+ timeout: Maximum time (in seconds) to wait for connection and responses.
50
+ ping_interval: Interval (in seconds) between WebSocket ping frames for keepalive.
51
+ ping_timeout: Maximum time (in seconds) to wait for a ping response before considering the connection dead.
52
+
53
+ Returns:
54
+ None: Initializes the WsMCPClient instance and prepares internal state.
55
+ """
56
+
15
57
  def __init__(
16
58
  self,
17
59
  url: str,
File without changes
@@ -0,0 +1,234 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Iterable
4
+ import json
5
+ from typing import Any
6
+
7
+ from aethergraph.contracts.services.llm import LLMClientProtocol
8
+ from aethergraph.contracts.services.memory import Distiller, Event, HotLog, Indices, Persistence
9
+ from aethergraph.contracts.storage.doc_store import DocStore
10
+
11
+ # metering
12
+ from aethergraph.core.runtime.runtime_metering import current_meter_context, current_metering
13
+ from aethergraph.services.memory.facade.utils import now_iso, stable_event_id
14
+ from aethergraph.services.memory.utils import _summary_doc_id
15
+
16
+
17
+ class LLMLongTermSummarizer(Distiller):
18
+ """
19
+ LLM-based long-term summarizer.
20
+
21
+ Flow:
22
+ 1) Pull recent events from HotLog.
23
+ 2) Filter by kind/tag/signal.
24
+ 3) Build a prompt that shows the most important events as a transcript.
25
+ 4) Call LLM to generate a structured summary.
26
+ 5) Save summary JSON via Persistence.save_json(uri).
27
+ 6) Emit a long_term_summary Event pointing to summary_uri.
28
+
29
+ This is complementary to RAG:
30
+ - LLM distiller compresses sequences into a digest.
31
+ - RAG uses many such digests + raw docs for retrieval.
32
+ """
33
+
34
+ def __init__(
35
+ self,
36
+ *,
37
+ llm: LLMClientProtocol,
38
+ summary_kind: str = "long_term_summary",
39
+ summary_tag: str = "session",
40
+ include_kinds: list[str] | None = None,
41
+ include_tags: list[str] | None = None,
42
+ max_events: int = 200,
43
+ min_signal: float = 0.0,
44
+ model: str | None = None,
45
+ ):
46
+ self.llm = llm
47
+ self.summary_kind = summary_kind
48
+ self.summary_tag = summary_tag
49
+ self.include_kinds = include_kinds
50
+ self.include_tags = include_tags
51
+ self.max_events = max_events
52
+ self.min_signal = min_signal
53
+ self.model = model # optional model override
54
+
55
+ def _filter_events(self, events: Iterable[Event]) -> list[Event]:
56
+ out: list[Event] = []
57
+ kinds = set(self.include_kinds) if self.include_kinds else None
58
+ tags = set(self.include_tags) if self.include_tags else None
59
+
60
+ for e in events:
61
+ if kinds is not None and e.kind not in kinds:
62
+ continue
63
+ if tags is not None:
64
+ if not e.tags:
65
+ continue
66
+ if not tags.issubset(set(e.tags)):
67
+ continue
68
+ if (e.signal or 0.0) < self.min_signal:
69
+ continue
70
+ out.append(e)
71
+ return out
72
+
73
+ def _build_prompt(self, events: list[Event]) -> list[dict[str, str]]:
74
+ """
75
+ Convert events into a chat-style context for summarization.
76
+
77
+ We keep it model-agnostic: a list of {role, content} messages.
78
+ """
79
+ lines: list[str] = []
80
+
81
+ for e in events:
82
+ role = e.stage or e.kind or "event"
83
+ if e.text:
84
+ lines.append(f"[{role}] {e.text}")
85
+
86
+ transcript = "\n".join(lines)
87
+
88
+ system = (
89
+ "You are a log summarizer for an agent's memory. "
90
+ "Given a chronological transcript of events, produce a concise summary "
91
+ "of what happened, key themes, important user facts, and open TODOs."
92
+ )
93
+
94
+ user = (
95
+ "Here is the recent event transcript:\n\n"
96
+ f"{transcript}\n\n"
97
+ "Return a JSON object with keys: "
98
+ "`summary` (string), "
99
+ "`key_facts` (list of strings), "
100
+ "`open_loops` (list of strings)."
101
+ "Do not use markdown or include explanations or context outside the JSON."
102
+ )
103
+
104
+ return [
105
+ {"role": "system", "content": system},
106
+ {"role": "user", "content": user},
107
+ ]
108
+
109
+ async def distill(
110
+ self,
111
+ run_id: str,
112
+ timeline_id: str,
113
+ scope_id: str = None,
114
+ *,
115
+ hotlog: HotLog,
116
+ persistence: Persistence,
117
+ indices: Indices,
118
+ docs: DocStore,
119
+ **kw: Any,
120
+ ) -> dict[str, Any]:
121
+ # 1) fetch more events than needed, then filter
122
+ raw = await hotlog.recent(timeline_id, kinds=None, limit=self.max_events * 2)
123
+ kept = self._filter_events(raw)
124
+ if not kept:
125
+ return {}
126
+
127
+ kept = kept[-self.max_events :]
128
+ first_ts = kept[0].ts
129
+ last_ts = kept[-1].ts
130
+
131
+ # 2) Build prompt and call LLM
132
+ messages = self._build_prompt(kept)
133
+
134
+ # LLMClientProtocol: assume chat(...) returns (text, usage)
135
+ summary_json_str, usage = await self.llm.chat(
136
+ messages,
137
+ )
138
+
139
+ # 3) Parse LLM JSON response
140
+ try:
141
+ payload = json.loads(summary_json_str)
142
+ except Exception:
143
+ payload = {
144
+ "summary": summary_json_str,
145
+ "key_facts": [],
146
+ "open_loops": [],
147
+ }
148
+ ts = now_iso()
149
+
150
+ summary_obj = {
151
+ "type": self.summary_kind,
152
+ "version": 1,
153
+ "run_id": run_id,
154
+ "scope_id": scope_id or run_id,
155
+ "summary_tag": self.summary_tag,
156
+ "ts": ts,
157
+ "time_window": {"from": first_ts, "to": last_ts},
158
+ "num_events": len(kept),
159
+ "source_event_ids": [e.event_id for e in kept],
160
+ "summary": payload.get("summary", ""),
161
+ "key_facts": payload.get("key_facts", []),
162
+ "open_loops": payload.get("open_loops", []),
163
+ "llm_usage": usage,
164
+ "llm_model": self.llm.model if hasattr(self.llm, "model") else None,
165
+ }
166
+
167
+ scope = scope_id or run_id
168
+ doc_id = _summary_doc_id(scope, self.summary_tag, ts)
169
+ await docs.put(doc_id, summary_obj)
170
+
171
+ # 4) Emit summary Event with preview + uri in data
172
+ text = summary_obj["summary"] or ""
173
+ preview = text[:2000] + (" …[truncated]" if len(text) > 2000 else "")
174
+
175
+ evt = Event(
176
+ event_id="",
177
+ ts=ts,
178
+ run_id=run_id,
179
+ scope_id=scope,
180
+ kind=self.summary_kind,
181
+ stage="summary_llm",
182
+ text=preview,
183
+ tags=["summary", "llm", self.summary_tag],
184
+ data={
185
+ "summary_doc_id": doc_id,
186
+ "summary_tag": self.summary_tag,
187
+ "time_window": summary_obj["time_window"],
188
+ "num_events": len(kept),
189
+ },
190
+ metrics={"num_events": len(kept)},
191
+ severity=2,
192
+ signal=0.7,
193
+ )
194
+
195
+ evt.event_id = stable_event_id(
196
+ {
197
+ "ts": ts,
198
+ "run_id": run_id,
199
+ "kind": self.summary_kind,
200
+ "summary_tag": self.summary_tag,
201
+ "preview": preview[:200],
202
+ }
203
+ )
204
+
205
+ await hotlog.append(timeline_id, evt, ttl_s=7 * 24 * 3600, limit=1000)
206
+ await persistence.append_event(timeline_id, evt)
207
+
208
+ # Metering: record summary event
209
+ try:
210
+ meter = current_metering()
211
+ ctx = current_meter_context.get()
212
+ user_id = ctx.get("user_id")
213
+ org_id = ctx.get("org_id")
214
+
215
+ await meter.record_event(
216
+ user_id=user_id,
217
+ org_id=org_id,
218
+ run_id=run_id,
219
+ scope_id=scope,
220
+ kind=f"memory.{self.summary_kind}", # e.g. "memory.long_term_summary"
221
+ )
222
+ except Exception:
223
+ import logging
224
+
225
+ logger = logging.getLogger("aethergraph.services.memory.distillers.llm_long_term")
226
+ logger.error("Failed to record metering event for long_term_summary")
227
+
228
+ return {
229
+ "summary_doc_id": doc_id,
230
+ "summary_kind": self.summary_kind,
231
+ "summary_tag": self.summary_tag,
232
+ "time_window": summary_obj["time_window"],
233
+ "num_events": len(kept),
234
+ }