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
@@ -0,0 +1,413 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Sequence
4
+ from dataclasses import dataclass
5
+ from typing import Any
6
+
7
+ from aethergraph.contracts.services.viz import VizEvent, VizMode
8
+ from aethergraph.services.artifacts.facade import Artifact, ArtifactFacade
9
+ from aethergraph.services.scope.scope import Scope
10
+ from aethergraph.services.viz.viz_service import VizService
11
+
12
+
13
+ @dataclass
14
+ class VizFacade:
15
+ """
16
+ High-level facade for visualization operations within a given Scope.
17
+
18
+ - Wraps VizService and ArtifactFacade.
19
+ - Knows about Scope to auto-fill provenance and tenant fields.
20
+
21
+ Usage pattern in ctx.viz:
22
+ # Scalars
23
+ await ctx.viz.scalar("loss", step=iter, value=float(loss), figure_id="metrics")
24
+
25
+ # Matrix (small heatmap)
26
+ await ctx.viz.matrix("field_map", step=iter, matrix=field_2d, figure_id="fields")
27
+
28
+ # Image (pre-rendered PNG)
29
+ artifact = await ctx.artifacts.save_file(path="frame_17.png", kind="image")
30
+ await ctx.viz.image_from_artifact("design_shape", step=17, artifact=artifact, figure_id="design")
31
+ """
32
+
33
+ run_id: str
34
+ graph_id: str
35
+ node_id: str
36
+ tool_name: str
37
+ tool_version: str
38
+
39
+ viz_service: VizService
40
+ scope: Scope | None = None
41
+ artifacts: ArtifactFacade | None = None
42
+
43
+ # ------- internal helpers -------
44
+ def _scope_dims(self) -> dict[str, Any]:
45
+ if not self.scope:
46
+ return {}
47
+ return self.scope.metering_dimensions()
48
+
49
+ def _apply_scope(self, evt: VizEvent) -> VizEvent:
50
+ dims = self._scope_dims()
51
+ evt.org_id = evt.org_id or dims.get("org_id")
52
+ evt.user_id = evt.user_id or dims.get("user_id")
53
+ evt.client_id = evt.client_id or dims.get("client_id")
54
+ evt.app_id = evt.app_id or dims.get("app_id")
55
+ evt.session_id = evt.session_id or dims.get("session_id")
56
+ return evt
57
+
58
+ # ------- public API -------
59
+ async def scalar(
60
+ self,
61
+ track_id: str,
62
+ *,
63
+ step: int,
64
+ value: float,
65
+ figure_id: str | None = None,
66
+ mode: VizMode = "append",
67
+ meta: dict[str, Any] | None = None,
68
+ tags: list[str] | None = None,
69
+ ) -> None:
70
+ """
71
+ Record a single scalar value for visualization in the Aethergraph UI.
72
+
73
+ This method standardizes the event format, auto-fills provenance fields,
74
+ and dispatches the scalar data to the configured storage backend.
75
+
76
+ Examples:
77
+ Basic usage to log a loss metric:
78
+ ```python
79
+ await context.viz().scalar("loss", step=iteration, value=loss)
80
+ ```
81
+
82
+ Logging a scalar with extra metadata and custom tags:
83
+ ```python
84
+ await context.viz().scalar(
85
+ "accuracy",
86
+ step=42,
87
+ value=0.98,
88
+ figure_id="metrics",
89
+ meta={"model": "resnet"},
90
+ tags=["experiment:baseline"]
91
+ )
92
+ ```
93
+
94
+ Args:
95
+ track_id: Unique identifier for the scalar track (e.g., "loss").
96
+ step: Integer step or iteration number for the data point.
97
+ value: The scalar value to record (float).
98
+ figure_id: Optional figure grouping for UI display.
99
+ mode: Storage mode, typically "append".
100
+ meta: Optional dictionary of extra metadata.
101
+ tags: Optional list of string labels. The tag "type:scalar" is automatically appended.
102
+
103
+ Returns:
104
+ None. The event is persisted for later visualization.
105
+ """
106
+ evt = VizEvent(
107
+ run_id=self.run_id,
108
+ graph_id=self.graph_id,
109
+ node_id=self.node_id,
110
+ tool_name=self.tool_name,
111
+ tool_version=self.tool_version,
112
+ track_id=track_id,
113
+ figure_id=figure_id,
114
+ viz_kind="scalar",
115
+ step=step,
116
+ mode=mode,
117
+ value=float(value),
118
+ meta=meta,
119
+ tags=(tags or []) + ["type:scalar"],
120
+ )
121
+ evt = self._apply_scope(evt)
122
+ await self.viz_service.append(evt)
123
+
124
+ async def vector(
125
+ self,
126
+ track_id: str,
127
+ *,
128
+ step: int,
129
+ values: Sequence[float],
130
+ figure_id: str | None = None,
131
+ mode: VizMode = "append",
132
+ meta: dict[str, Any] | None = None,
133
+ tags: list[str] | None = None,
134
+ ) -> None:
135
+ """
136
+ Record a single vector (1D array) for visualization in the Aethergraph UI.
137
+
138
+ This method standardizes the event format, auto-fills provenance fields,
139
+ and dispatches the vector data to the configured storage backend.
140
+
141
+ Examples:
142
+ Basic usage to log a vector:
143
+ ```python
144
+ await context.viz().vector("embedding", step=iteration, values=[0.1, 0.2, 0.3])
145
+ ```
146
+
147
+ Logging a vector with extra metadata and custom tags:
148
+ ```python
149
+ await context.viz().vector(
150
+ "features",
151
+ step=42,
152
+ values=[1.0, 2.5, 3.7],
153
+ figure_id="feature_tracks",
154
+ meta={"source": "encoder"},
155
+ tags=["experiment:baseline"]
156
+ )
157
+ ```
158
+
159
+ Args:
160
+ track_id: Unique identifier for the vector track (e.g., "embedding").
161
+ step: Integer step or iteration number for the data point.
162
+ values: Sequence of float values representing the vector.
163
+ figure_id: Optional figure grouping for UI display.
164
+ mode: Storage mode, typically "append".
165
+ meta: Optional dictionary of extra metadata.
166
+ tags: Optional list of string labels. The tag "type:vector" is automatically appended.
167
+
168
+ Returns:
169
+ None. The event is persisted for later visualization.
170
+ """
171
+ evt = VizEvent(
172
+ run_id=self.run_id,
173
+ graph_id=self.graph_id,
174
+ node_id=self.node_id,
175
+ tool_name=self.tool_name,
176
+ tool_version=self.tool_version,
177
+ track_id=track_id,
178
+ figure_id=figure_id,
179
+ viz_kind="vector",
180
+ step=step,
181
+ mode=mode,
182
+ vector=[float(v) for v in values],
183
+ meta=meta,
184
+ tags=(tags or []) + ["type:vector"],
185
+ )
186
+ evt = self._apply_scope(evt)
187
+ await self.viz_service.append(evt)
188
+
189
+ async def matrix(
190
+ self,
191
+ track_id: str,
192
+ *,
193
+ step: int,
194
+ matrix: Sequence[Sequence[float]],
195
+ figure_id: str | None = None,
196
+ mode: VizMode = "append",
197
+ meta: dict[str, Any] | None = None,
198
+ tags: list[str] | None = None,
199
+ ) -> None:
200
+ """
201
+ Record a single matrix (2D array) for visualization in the Aethergraph UI.
202
+
203
+ This method standardizes the event format, auto-fills provenance fields,
204
+ and dispatches the matrix data to the configured storage backend.
205
+
206
+ Examples:
207
+ Basic usage to log a matrix:
208
+ ```python
209
+ await context.viz().matrix("confusion", step=iteration, matrix=[[1, 2], [3, 4]])
210
+ ```
211
+
212
+ Logging a matrix with extra metadata and custom tags:
213
+ ```python
214
+ await context.viz().matrix(
215
+ "heatmap",
216
+ step=42,
217
+ matrix=[[0.1, 0.2], [0.3, 0.4]],
218
+ figure_id="metrics",
219
+ meta={"source": "model"},
220
+ tags=["experiment:baseline"]
221
+ )
222
+ ```
223
+
224
+ Args:
225
+ track_id: Unique identifier for the matrix track (e.g., "confusion").
226
+ step: Integer step or iteration number for the data point.
227
+ matrix: Sequence of sequences of float values representing the 2D matrix.
228
+ figure_id: Optional figure grouping for UI display.
229
+ mode: Storage mode, typically "append".
230
+ meta: Optional dictionary of extra metadata.
231
+ tags: Optional list of string labels. The tag "matrix" is automatically appended.
232
+
233
+ Returns:
234
+ None. The event is persisted for later visualization.
235
+ """
236
+ # Convert to plain list[list[float]]
237
+ m = [[float(x) for x in row] for row in matrix]
238
+ evt = VizEvent(
239
+ run_id=self.run_id,
240
+ graph_id=self.graph_id,
241
+ node_id=self.node_id,
242
+ tool_name=self.tool_name,
243
+ tool_version=self.tool_version,
244
+ track_id=track_id,
245
+ figure_id=figure_id,
246
+ viz_kind="matrix",
247
+ step=step,
248
+ mode=mode,
249
+ matrix=m,
250
+ meta=meta,
251
+ tags=(tags or []) + ["matrix"],
252
+ )
253
+ evt = self._apply_scope(evt)
254
+ await self.viz_service.append(evt)
255
+
256
+ async def image_from_artifact(
257
+ self,
258
+ track_id: str,
259
+ *,
260
+ step: int,
261
+ artifact: Artifact,
262
+ figure_id: str | None = None,
263
+ mode: VizMode = "append",
264
+ meta: dict[str, Any] | None = None,
265
+ tags: list[str] | None = None,
266
+ ) -> None:
267
+ """
268
+ Record a reference to an existing image Artifact for visualization in the Aethergraph UI.
269
+
270
+ This method standardizes the event format, auto-fills provenance fields,
271
+ and dispatches the image reference to the configured storage backend.
272
+
273
+ Examples:
274
+ Basic usage to log an image artifact:
275
+ ```python
276
+ await context.viz().image_from_artifact(
277
+ "design_shape",
278
+ step=17,
279
+ artifact=artifact,
280
+ figure_id="design"
281
+ )
282
+ ```
283
+
284
+ Logging an image with extra metadata and custom tags:
285
+ ```python
286
+ await context.viz().image_from_artifact(
287
+ "output_frame",
288
+ step=42,
289
+ artifact=artifact,
290
+ figure_id="frames",
291
+ meta={"source": "simulation"},
292
+ tags=["experiment:baseline"]
293
+ )
294
+ ```
295
+
296
+ Args:
297
+ track_id: Unique identifier for the image track (e.g., "design_shape").
298
+ step: Integer step or iteration number for the data point.
299
+ artifact: The Artifact object referencing the stored image.
300
+ figure_id: Optional figure grouping for UI display.
301
+ mode: Storage mode, typically "append".
302
+ meta: Optional dictionary of extra metadata.
303
+ tags: Optional list of string labels. The tag "image" is automatically appended.
304
+
305
+ Returns:
306
+ None. The event is persisted for later visualization.
307
+ """
308
+ evt = VizEvent(
309
+ run_id=self.run_id,
310
+ graph_id=self.graph_id,
311
+ node_id=self.node_id,
312
+ tool_name=self.tool_name,
313
+ tool_version=self.tool_version,
314
+ track_id=track_id,
315
+ figure_id=figure_id,
316
+ viz_kind="image",
317
+ step=step,
318
+ mode=mode,
319
+ artifact_id=artifact.artifact_id,
320
+ meta=meta,
321
+ tags=(tags or []) + ["image"],
322
+ )
323
+ evt = self._apply_scope(evt)
324
+ await self.viz_service.append(evt)
325
+
326
+ async def image_from_bytes(
327
+ self,
328
+ track_id: str,
329
+ *,
330
+ step: int,
331
+ data: bytes,
332
+ mime: str = "image/png",
333
+ kind: str = "image",
334
+ figure_id: str | None = None,
335
+ mode: VizMode = "append",
336
+ labels: dict[str, Any] | None = None,
337
+ meta: dict[str, Any] | None = None,
338
+ tags: list[str] | None = None,
339
+ ) -> Artifact:
340
+ """
341
+ Save image bytes as an Artifact and log a visualization event.
342
+
343
+ This convenience method is accessed via `context.viz().image_from_bytes(...)` and is used by the Aethergraph UI to persist image data to storage. It stores the image as an Artifact using the configured ArtifactFacade, then logs a visualization event referencing the saved artifact.
344
+
345
+ Examples:
346
+ Saving a PNG image to the current visualization track:
347
+ ```python
348
+ await context.viz().image_from_bytes(
349
+ track_id="experiment-123",
350
+ step=42,
351
+ data=image_bytes,
352
+ mime="image/png",
353
+ labels={"type": "output", "stage": "inference"},
354
+ tags=["result", "png"]
355
+ )
356
+ ```
357
+
358
+ Saving an image with custom metadata and figure association:
359
+ ```python
360
+ await context.viz().image_from_bytes(
361
+ track_id="demo-track",
362
+ step=7,
363
+ data=img_bytes,
364
+ figure_id="fig-1",
365
+ meta={"caption": "Sample output"},
366
+ mode="replace"
367
+ )
368
+ ```
369
+
370
+ Args:
371
+ track_id: The identifier for the visualization track to associate with the image.
372
+ step: The step or index within the track for this image.
373
+ data: Raw image bytes to be saved.
374
+ mime: The MIME type of the image (default: "image/png").
375
+ kind: The artifact kind (default: "image").
376
+ figure_id: Optional identifier for the figure this image belongs to.
377
+ mode: Visualization mode, e.g., "append" or "replace".
378
+ labels: Optional dictionary of labels to attach to the artifact.
379
+ meta: Optional dictionary of metadata for the visualization event.
380
+ tags: Optional list of string tags for categorization.
381
+
382
+ Returns:
383
+ Artifact: The persisted `Artifact` object representing the saved image.
384
+
385
+ Notes:
386
+ - This method requires that `self.artifacts` is set to an `ArtifactFacade` instance.
387
+ - The saved artifact is automatically linked to the visualization event.
388
+ - To change the name of the saved artifact in UI, use `labels` to set a "filename" label.
389
+ """
390
+ if not self.artifacts:
391
+ raise RuntimeError("VizFacade.image_from_bytes requires an ArtifactFacade")
392
+
393
+ # Save artifact using writer() so we get proper metering + labels
394
+
395
+ # Use ArtifactFacade.writer to store the image
396
+ async with self.artifacts.writer(kind=kind, planned_ext=".png") as w:
397
+ w.write(data)
398
+ if labels:
399
+ w.add_labels(labels)
400
+ art = self.artifacts.last_artifact
401
+ if not art:
402
+ raise RuntimeError("Artifact writer did not produce an artifact")
403
+
404
+ await self.image_from_artifact(
405
+ track_id=track_id,
406
+ step=step,
407
+ artifact=art,
408
+ figure_id=figure_id,
409
+ mode=mode,
410
+ meta=meta,
411
+ tags=tags,
412
+ )
413
+ return art
@@ -0,0 +1,69 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import asdict
4
+ from datetime import datetime, timezone
5
+ from typing import Any
6
+
7
+ from aethergraph.contracts.services.viz import VizEvent, VizKind
8
+ from aethergraph.contracts.storage.event_log import EventLog
9
+
10
+
11
+ class VizService:
12
+ """
13
+ Low-level service to append/query visualization events.
14
+
15
+ - Uses EventLog as the underlying storage.
16
+ - Does NOT know about NodeContext or Scope; that's the Facade's job.
17
+ """
18
+
19
+ def __init__(self, event_log: EventLog):
20
+ self._log = event_log
21
+
22
+ async def append(self, evt: VizEvent) -> None:
23
+ now = datetime.now(timezone.utc).isoformat()
24
+ if not evt.created_at:
25
+ evt.created_at = now
26
+
27
+ payload = asdict(evt)
28
+ await self._log.append(
29
+ {
30
+ "kind": "viz",
31
+ "scope_id": evt.run_id,
32
+ "ts": evt.created_at,
33
+ "data": payload,
34
+ "tags": (evt.tags or []) + [f"track:{evt.track_id}"],
35
+ }
36
+ )
37
+
38
+ async def query_run(
39
+ self,
40
+ run_id: str,
41
+ *,
42
+ kinds: list[VizKind] | None = None,
43
+ since: datetime | None = None,
44
+ until: datetime | None = None,
45
+ limit: int | None = None,
46
+ offset: int = 0,
47
+ ) -> list[dict[str, Any]]:
48
+ """
49
+ Raw fetch of viz events for a given run.
50
+ Returns raw event dicts as stored in EventLog.
51
+ The API layer can normalize/group them for the frontend.
52
+ """
53
+ rows = await self._log.query(
54
+ scope_id=run_id,
55
+ since=since,
56
+ until=until,
57
+ kinds=["viz"],
58
+ limit=limit,
59
+ offset=offset,
60
+ )
61
+ # Optionally filter by viz_kind inside data
62
+ if kinds:
63
+ out: list[dict[str, Any]] = []
64
+ for r in rows:
65
+ data = r.get("data") or {}
66
+ if data.get("viz_kind") in kinds:
67
+ out.append(r)
68
+ return out
69
+ return rows
@@ -0,0 +1,180 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import json
5
+ import os
6
+ import threading
7
+ from typing import Any, Literal
8
+
9
+ from aethergraph.contracts.services.artifacts import Artifact
10
+ from aethergraph.contracts.storage.artifact_index import AsyncArtifactIndex
11
+
12
+
13
+ class JsonlArtifactIndexSync:
14
+ """
15
+ Simple JSONL-based artifact index for small/medium scale.
16
+ Not suitable for millions of artifacts due to linear scans.
17
+ """
18
+
19
+ def __init__(self, path: str, occurrences_path: str | None = None):
20
+ self.path = path
21
+ self.occ_path = occurrences_path or (os.path.splitext(path)[0] + "_occurrences.jsonl")
22
+ os.makedirs(os.path.dirname(path), exist_ok=True)
23
+
24
+ self._by_id: dict[str, dict[str, Any]] = {}
25
+ self._lock = threading.Lock()
26
+
27
+ if os.path.exists(self.path):
28
+ with open(self.path, encoding="utf-8") as f:
29
+ for line in f:
30
+ if not line.strip():
31
+ continue
32
+ rec = json.loads(line)
33
+ self._by_id[rec["artifact_id"]] = rec
34
+
35
+ # -------- core operations --------
36
+
37
+ def upsert(self, a: Artifact) -> None:
38
+ with self._lock:
39
+ rec = a.to_dict()
40
+ self._by_id[a.artifact_id] = rec
41
+ with open(self.path, "a", encoding="utf-8") as f:
42
+ f.write(json.dumps(rec) + "\n")
43
+
44
+ def list_for_run(self, run_id: str) -> list[Artifact]:
45
+ return [Artifact(**r) for r in self._by_id.values() if r.get("run_id") == run_id]
46
+
47
+ def search(
48
+ self,
49
+ *,
50
+ kind: str | None = None,
51
+ labels: dict[str, Any] | None = None,
52
+ metric: str | None = None,
53
+ mode: Literal["max", "min"] | None = None,
54
+ limit: int | None = None,
55
+ offset: int = 0,
56
+ ) -> list[Artifact]:
57
+ # NOTE: JSONL index keeps all artifacts in memory (_by_id.values()) and
58
+ # performs filtering / sorting in Python, then applies offset + limit.
59
+ # This is intended for small/medium local installs and tests only.
60
+ rows = list(self._by_id.values())
61
+
62
+ if kind:
63
+ rows = [r for r in rows if r.get("kind") == kind]
64
+
65
+ # Treat tenant keys as top-level fields, not labels
66
+ TENANT_KEYS = {
67
+ "org_id",
68
+ "user_id",
69
+ "client_id",
70
+ "app_id",
71
+ "session_id",
72
+ "run_id",
73
+ }
74
+
75
+ if labels:
76
+ for k, v in labels.items():
77
+ if k in TENANT_KEYS:
78
+ # Match against top-level JSON fields
79
+ rows = [r for r in rows if r.get(k) == v]
80
+ continue
81
+
82
+ # Normal label filters
83
+ if isinstance(v, list):
84
+ rows = [
85
+ r
86
+ for r in rows
87
+ if isinstance(r.get("labels", {}).get(k), list)
88
+ and set(v).issubset(set(r["labels"][k]))
89
+ ]
90
+ else:
91
+ rows = [r for r in rows if r.get("labels", {}).get(k) == v]
92
+
93
+ if metric and mode:
94
+ rows = [r for r in rows if metric in r.get("metrics", {})]
95
+ rows.sort(
96
+ key=lambda r: r["metrics"][metric],
97
+ reverse=(mode == "max"),
98
+ )
99
+
100
+ if offset > 0:
101
+ rows = rows[offset:]
102
+
103
+ if limit is not None:
104
+ rows = rows[:limit]
105
+
106
+ return [Artifact(**r) for r in rows]
107
+
108
+ def best(
109
+ self,
110
+ *,
111
+ kind: str,
112
+ metric: str,
113
+ mode: Literal["max", "min"],
114
+ filters: dict[str, Any] | None = None,
115
+ ) -> Artifact | None:
116
+ rows = self.search(
117
+ kind=kind,
118
+ labels=filters,
119
+ metric=metric,
120
+ mode=mode,
121
+ limit=1,
122
+ )
123
+ return rows[0] if rows else None
124
+
125
+ def pin(self, artifact_id: str, pinned: bool = True) -> None:
126
+ with self._lock:
127
+ if artifact_id not in self._by_id:
128
+ return
129
+ rec = self._by_id[artifact_id]
130
+ rec["pinned"] = bool(pinned)
131
+ self._by_id[artifact_id] = rec
132
+ with open(self.path, "a", encoding="utf-8") as f:
133
+ f.write(json.dumps(rec) + "\n")
134
+
135
+ def record_occurrence(self, a: Artifact, extra_labels: dict | None = None) -> None:
136
+ row = {
137
+ "artifact_id": a.artifact_id,
138
+ "run_id": a.run_id,
139
+ "graph_id": a.graph_id,
140
+ "node_id": a.node_id,
141
+ "tool_name": a.tool_name,
142
+ "tool_version": a.tool_version,
143
+ "created_at": a.created_at,
144
+ "labels": {**(a.labels or {}), **(extra_labels or {})},
145
+ }
146
+ with open(self.occ_path, "a", encoding="utf-8") as f:
147
+ f.write(json.dumps(row) + "\n")
148
+
149
+ def get(self, artifact_id: str) -> Artifact | None:
150
+ if artifact_id in self._by_id:
151
+ return Artifact(**self._by_id[artifact_id])
152
+ return None
153
+
154
+
155
+ class JsonlArtifactIndex(AsyncArtifactIndex):
156
+ """Async wrapper for JsonlArtifactIndexSync using asyncio.to_thread."""
157
+
158
+ def __init__(self, path: str, occurrences_path: str | None = None):
159
+ self._sync = JsonlArtifactIndexSync(path, occurrences_path)
160
+
161
+ async def upsert(self, a: Artifact) -> None:
162
+ await asyncio.to_thread(self._sync.upsert, a)
163
+
164
+ async def list_for_run(self, run_id: str) -> list[Artifact]:
165
+ return await asyncio.to_thread(self._sync.list_for_run, run_id)
166
+
167
+ async def search(self, **kwargs) -> list[Artifact]:
168
+ return await asyncio.to_thread(self._sync.search, **kwargs)
169
+
170
+ async def best(self, **kwargs) -> Artifact | None:
171
+ return await asyncio.to_thread(self._sync.best, **kwargs)
172
+
173
+ async def pin(self, artifact_id: str, pinned: bool = True) -> None:
174
+ await asyncio.to_thread(self._sync.pin, artifact_id, pinned)
175
+
176
+ async def record_occurrence(self, a: Artifact, extra_labels: dict | None = None) -> None:
177
+ await asyncio.to_thread(self._sync.record_occurrence, a, extra_labels)
178
+
179
+ async def get(self, artifact_id: str) -> Artifact | None:
180
+ return await asyncio.to_thread(self._sync.get, artifact_id)