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
@@ -0,0 +1,178 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import datetime, timezone
4
+ import json
5
+ from pathlib import Path
6
+ import sqlite3
7
+ import threading
8
+ import time
9
+ from typing import Any
10
+
11
+ """
12
+ This is not used in the main codebase; only used by async wrapper SqliteEventLog.
13
+ """
14
+
15
+
16
+ class SQLiteEventLogSync:
17
+ def __init__(self, path: str):
18
+ path_obj = Path(path)
19
+ path_obj.parent.mkdir(parents=True, exist_ok=True)
20
+
21
+ self._db = sqlite3.connect(
22
+ str(path_obj),
23
+ check_same_thread=False,
24
+ isolation_level=None,
25
+ )
26
+ self._lock = threading.RLock()
27
+ self._initialize_db()
28
+
29
+ def _initialize_db(self) -> None:
30
+ self._db.execute(
31
+ """
32
+ CREATE TABLE IF NOT EXISTS events (
33
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
34
+ ts REAL NOT NULL,
35
+ scope_id TEXT,
36
+ kind TEXT,
37
+ tags_json TEXT,
38
+ payload TEXT NOT NULL,
39
+ -- new tenant / dimension columns
40
+ user_id TEXT,
41
+ org_id TEXT,
42
+ run_id TEXT,
43
+ session_id TEXT
44
+ )
45
+ """
46
+ )
47
+ # Migration for existing DBs
48
+ cols = {row[1] for row in self._db.execute("PRAGMA table_info(events)").fetchall()}
49
+ if "user_id" not in cols:
50
+ self._db.execute("ALTER TABLE events ADD COLUMN user_id TEXT")
51
+ if "org_id" not in cols:
52
+ self._db.execute("ALTER TABLE events ADD COLUMN org_id TEXT")
53
+ if "run_id" not in cols:
54
+ self._db.execute("ALTER TABLE events ADD COLUMN run_id TEXT")
55
+ if "session_id" not in cols:
56
+ self._db.execute("ALTER TABLE events ADD COLUMN session_id TEXT")
57
+
58
+ # Existing indexes
59
+ self._db.execute("CREATE INDEX IF NOT EXISTS idx_events_scope ON events(scope_id)")
60
+ self._db.execute("CREATE INDEX IF NOT EXISTS idx_events_kind ON events(kind)")
61
+ self._db.execute("CREATE INDEX IF NOT EXISTS idx_events_ts ON events(ts)")
62
+
63
+ # tenant-aware indexes
64
+ self._db.execute("CREATE INDEX IF NOT EXISTS idx_events_user_ts ON events(user_id, ts)")
65
+ self._db.execute("CREATE INDEX IF NOT EXISTS idx_events_org_ts ON events(org_id, ts)")
66
+ self._db.execute("CREATE INDEX IF NOT EXISTS idx_events_run_ts ON events(run_id, ts)")
67
+
68
+ def append(self, evt: dict) -> None:
69
+ row = dict(evt)
70
+
71
+ ts = row.get("ts")
72
+ if isinstance(ts, datetime):
73
+ ts = ts.timestamp()
74
+ elif isinstance(ts, int | float):
75
+ ts = float(ts)
76
+ elif isinstance(ts, str):
77
+ # Handle ISO 8601 timestamps like '2025-11-27T19:48:09.758687+00:00' or ...Z
78
+ try:
79
+ s = ts.replace("Z", "+00:00") if ts.endswith("Z") else ts
80
+ dt = datetime.fromisoformat(s)
81
+ if dt.tzinfo is None:
82
+ dt = dt.replace(tzinfo=timezone.utc)
83
+ ts = dt.timestamp()
84
+ except Exception:
85
+ # Fallback: current time if we can't parse
86
+ ts = time.time()
87
+
88
+ if ts is None:
89
+ ts = time.time()
90
+
91
+ scope_id = row.get("scope_id")
92
+ kind = row.get("kind")
93
+ tags = row.get("tags") or []
94
+ tags_json = json.dumps(tags, ensure_ascii=False)
95
+
96
+ # tenant & run dims (not all events will have these fields. Chat events can just use session_id to retrieve info after optional authentication)
97
+ user_id = row.get("user_id")
98
+ org_id = row.get("org_id")
99
+ run_id = row.get("run_id")
100
+ session_id = row.get("session_id")
101
+
102
+ # Optionally overwrite the ts in the payload to the normalized float
103
+ row["ts"] = ts
104
+ payload = json.dumps(row, ensure_ascii=False)
105
+
106
+ with self._lock:
107
+ self._db.execute(
108
+ """
109
+ INSERT INTO events (ts, scope_id, kind, tags_json, payload, user_id, org_id, run_id, session_id)
110
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
111
+ """,
112
+ (ts, scope_id, kind, tags_json, payload, user_id, org_id, run_id, session_id),
113
+ )
114
+
115
+ def query(
116
+ self,
117
+ *,
118
+ scope_id: str | None = None,
119
+ since: datetime | None = None,
120
+ until: datetime | None = None,
121
+ kinds: list[str] | None = None,
122
+ limit: int | None = None,
123
+ tags: list[str] | None = None,
124
+ offset: int = 0,
125
+ user_id: str | None = None,
126
+ org_id: str | None = None,
127
+ ) -> list[dict]:
128
+ where: list[str] = []
129
+ params: list[Any] = []
130
+
131
+ if scope_id is not None:
132
+ where.append("scope_id = ?")
133
+ params.append(scope_id)
134
+
135
+ if since is not None:
136
+ where.append("ts >= ?")
137
+ params.append(since.timestamp())
138
+
139
+ if until is not None:
140
+ where.append("ts <= ?")
141
+ params.append(until.timestamp())
142
+
143
+ if kinds:
144
+ where.append(f"kind IN ({', '.join('?' for _ in kinds)})")
145
+ params.extend(kinds)
146
+
147
+ # Tenant-level filters for metering
148
+ if user_id is not None:
149
+ where.append("user_id = ?")
150
+ params.append(user_id)
151
+ if org_id is not None:
152
+ where.append("org_id = ?")
153
+ params.append(org_id)
154
+
155
+ sql = "SELECT payload, tags_json FROM events"
156
+ if where:
157
+ sql += " WHERE " + " AND ".join(where)
158
+ sql += " ORDER BY ts ASC"
159
+
160
+ with self._lock:
161
+ rows = self._db.execute(sql, params).fetchall()
162
+
163
+ tags_set = set(tags or [])
164
+ filtered: list[dict] = []
165
+ for payload_str, tags_json in rows:
166
+ evt = json.loads(payload_str)
167
+ if tags:
168
+ row_tags = set(json.loads(tags_json) or [])
169
+ if not row_tags.issuperset(tags_set):
170
+ continue
171
+ filtered.append(evt)
172
+
173
+ if offset:
174
+ filtered = filtered[offset:]
175
+ if limit is not None:
176
+ filtered = filtered[:limit]
177
+
178
+ return filtered
@@ -0,0 +1,432 @@
1
+ import os
2
+ from pathlib import Path
3
+
4
+ from aethergraph.config.config import AppSettings, ContinuationStoreSettings
5
+ from aethergraph.contracts.services.continuations import AsyncContinuationStore
6
+ from aethergraph.contracts.services.kv import AsyncKV
7
+ from aethergraph.contracts.services.memory import HotLog, Indices, Persistence
8
+ from aethergraph.contracts.services.runs import RunStore
9
+ from aethergraph.contracts.services.state_stores import GraphStateStore
10
+ from aethergraph.contracts.storage.artifact_index import AsyncArtifactIndex
11
+ from aethergraph.contracts.storage.artifact_store import AsyncArtifactStore
12
+ from aethergraph.contracts.storage.doc_store import DocStore
13
+ from aethergraph.contracts.storage.event_log import EventLog
14
+
15
+
16
+ def build_doc_store(cfg: AppSettings) -> DocStore:
17
+ """
18
+ Global DocStore factory, used by:
19
+ - Memory persistence (EventLogPersistence)
20
+ - RAG
21
+ - Continuations (if you choose to share it)
22
+ - Anything else that wants "document-ish" JSON blobs.
23
+ """
24
+ root = Path(cfg.root).resolve()
25
+ dc = cfg.storage.docs
26
+
27
+ if dc.backend == "sqlite":
28
+ from aethergraph.storage.docstore.sqlite_doc import SqliteDocStore
29
+
30
+ path = root / dc.sqlite_path
31
+ path.parent.mkdir(parents=True, exist_ok=True)
32
+ return SqliteDocStore(path=str(path))
33
+
34
+ if dc.backend == "fs":
35
+ from aethergraph.storage.docstore.fs_doc import FSDocStore
36
+
37
+ doc_root = root / dc.fs_dir
38
+ doc_root.mkdir(parents=True, exist_ok=True)
39
+ return FSDocStore(root=str(doc_root))
40
+
41
+ raise ValueError(f"Unknown DocStore backend: {dc.backend!r}")
42
+
43
+
44
+ def build_event_log(cfg: AppSettings, service_name: str | None = None) -> EventLog | None:
45
+ """
46
+ Global EventLog factory.
47
+ Used by:
48
+ - GraphStateStore (if you want)
49
+ - Memory (EventLogPersistence)
50
+ - Continuations audit (optional)
51
+ """
52
+ root = Path(cfg.root).resolve()
53
+ ec = cfg.storage.eventlog
54
+
55
+ if ec.backend == "none":
56
+ return None
57
+
58
+ if ec.backend == "sqlite":
59
+ from aethergraph.storage.eventlog.sqlite_event import SqliteEventLog
60
+
61
+ # If you use a different DB file per service, you get isolation between services,
62
+ # but lose global querying and may have more files to manage.
63
+ # If you use a single DB file, all services share the same event log table(s).
64
+ path = root / ec.sqlite_path # You could do: root / f"{service_name}_{ec.sqlite_path}"
65
+ path.parent.mkdir(parents=True, exist_ok=True)
66
+ return SqliteEventLog(path=str(path))
67
+
68
+ if ec.backend == "fs":
69
+ from aethergraph.storage.eventlog.fs_event import FSEventLog
70
+
71
+ ev_root = root / ec.fs_dir if not service_name else root / ec.fs_dir / service_name
72
+ ev_root.mkdir(parents=True, exist_ok=True)
73
+ return FSEventLog(root=str(ev_root))
74
+
75
+ raise ValueError(f"Unknown EventLog backend: {ec.backend!r}")
76
+
77
+
78
+ def build_kv_store(cfg: AppSettings, *, extra_prefix: str = "") -> AsyncKV:
79
+ """
80
+ Global KV factory.
81
+
82
+ extra_prefix lets subsystems (memory, continuations, etc.) add their own
83
+ namespace on top of the global storage.kv.prefix.
84
+ """
85
+ root = Path(cfg.root).resolve()
86
+ kc = cfg.storage.kv
87
+
88
+ full_prefix = f"{kc.prefix}{extra_prefix}"
89
+
90
+ if kc.backend == "sqlite":
91
+ from aethergraph.storage.kv.sqlite_kv import SqliteKV
92
+
93
+ path = root / kc.sqlite_path
94
+ path.parent.mkdir(parents=True, exist_ok=True)
95
+ return SqliteKV(path=str(path), prefix=full_prefix)
96
+
97
+ if kc.backend == "inmem":
98
+ from aethergraph.storage.kv.inmem_kv import InMemoryKV
99
+
100
+ return InMemoryKV(prefix=full_prefix)
101
+
102
+ raise ValueError(f"Unknown KV backend: {kc.backend!r}")
103
+
104
+
105
+ def build_artifact_store(cfg: AppSettings) -> AsyncArtifactStore:
106
+ """
107
+ Decide which artifact store backend to use based on AppSettings.storage.artifacts.
108
+ """
109
+ art_cfg = cfg.storage.artifacts
110
+ root = os.path.abspath(cfg.root)
111
+
112
+ if art_cfg.backend == "fs":
113
+ from aethergraph.storage.artifacts.fs_cas import FSArtifactStore
114
+
115
+ base_dir = os.path.join(root, art_cfg.fs.base_dir)
116
+ return FSArtifactStore(base_dir=base_dir)
117
+
118
+ if art_cfg.backend == "s3":
119
+ from aethergraph.storage.artifacts.s3_cas import (
120
+ S3ArtifactStore, # late import to avoid boto3 dependency if unused
121
+ )
122
+
123
+ if not art_cfg.s3.bucket:
124
+ raise ValueError("S3 backend selected, but STORAGE__ARTIFACTS__S3__BUCKET is empty")
125
+
126
+ staging_dir = art_cfg.s3.staging_dir
127
+ if not staging_dir:
128
+ staging_dir = os.path.join(root, ".aethergraph_tmp", "artifacts")
129
+ return S3ArtifactStore(
130
+ bucket=art_cfg.s3.bucket,
131
+ prefix=art_cfg.s3.prefix,
132
+ staging_dir=staging_dir,
133
+ )
134
+
135
+ raise ValueError(f"Unknown artifacts backend: {art_cfg.backend!r}")
136
+
137
+
138
+ def build_artifact_index(cfg: AppSettings) -> AsyncArtifactIndex:
139
+ idx_cfg = cfg.storage.artifact_index
140
+ root = os.path.abspath(cfg.root)
141
+
142
+ if idx_cfg.backend == "jsonl":
143
+ from aethergraph.storage.artifacts.artifact_index_jsonl import JsonlArtifactIndex
144
+
145
+ path = os.path.join(root, idx_cfg.jsonl.path)
146
+ occ = (
147
+ os.path.join(root, idx_cfg.jsonl.occurrences_path)
148
+ if idx_cfg.jsonl.occurrences_path
149
+ else None
150
+ )
151
+ return JsonlArtifactIndex(path=path, occurrences_path=occ)
152
+
153
+ if idx_cfg.backend == "sqlite":
154
+ from aethergraph.storage.artifacts.artifact_index_sqlite import SqliteArtifactIndex
155
+
156
+ path = os.path.join(root, idx_cfg.sqlite.path)
157
+ return SqliteArtifactIndex(path=path)
158
+
159
+ raise ValueError(f"Unknown artifact index backend: {idx_cfg.backend!r}")
160
+
161
+
162
+ def build_graph_state_store(cfg: AppSettings) -> GraphStateStore:
163
+ from aethergraph.storage.graph_state_store.state_store import GraphStateStoreImpl
164
+
165
+ gs_cfg = cfg.storage.graph_state
166
+
167
+ if gs_cfg.backend == "fs":
168
+ from aethergraph.storage.docstore.fs_doc import FSDocStore
169
+ from aethergraph.storage.eventlog.fs_event import FSEventLog
170
+
171
+ base = os.path.join(cfg.root, gs_cfg.fs_root)
172
+ docs = FSDocStore(os.path.join(base, "docs"))
173
+ log = FSEventLog(os.path.join(base, "events"))
174
+ elif gs_cfg.backend == "sqlite":
175
+ from aethergraph.storage.docstore.sqlite_doc import SqliteDocStore
176
+ from aethergraph.storage.eventlog.sqlite_event import SqliteEventLog
177
+
178
+ db_path = os.path.join(cfg.root, gs_cfg.sqlite_path)
179
+ docs = SqliteDocStore(db_path)
180
+ log = SqliteEventLog(db_path)
181
+ else:
182
+ raise ValueError(f"Unknown graph_state backend: {gs_cfg.backend!r}")
183
+
184
+ return GraphStateStoreImpl(doc_store=docs, event_log=log)
185
+
186
+
187
+ def build_run_store(cfg: AppSettings) -> RunStore:
188
+ """
189
+ Factory for RunStore:
190
+
191
+ - "memory": InMemoryRunStore (no persistence)
192
+ - "fs": DocRunStore on top of FSDocStore
193
+ - "sqlite": DocRunStore on top of SqliteDocStore
194
+ """
195
+ rs_cfg = cfg.storage.runs
196
+
197
+ if rs_cfg.backend == "memory":
198
+ from aethergraph.storage.runs.inmen_store import InMemoryRunStore
199
+
200
+ return InMemoryRunStore()
201
+
202
+ if rs_cfg.backend == "fs":
203
+ from aethergraph.storage.docstore.fs_doc import FSDocStore
204
+ from aethergraph.storage.runs.doc_store import DocRunStore
205
+
206
+ base = os.path.join(cfg.root, rs_cfg.fs_root)
207
+ docs = FSDocStore(base)
208
+ return DocRunStore(
209
+ docs, prefix="run-"
210
+ ) # use "run-" prefix to avoid OS path issues on Windows
211
+
212
+ if rs_cfg.backend == "sqlite":
213
+ from aethergraph.storage.runs.sqlite_run_store import SQLiteRunStore
214
+
215
+ db_path = os.path.join(cfg.root, rs_cfg.sqlite_path)
216
+ return SQLiteRunStore(path=db_path)
217
+
218
+ raise ValueError(f"Unknown run storage backend: {rs_cfg.backend!r}")
219
+
220
+
221
+ def build_session_store(cfg: AppSettings):
222
+ """
223
+ Factory for SessionStore:
224
+
225
+ - "memory": InMemorySessionStore (no persistence)
226
+ - "fs": DocSessionStore on top of FSDocStore
227
+ - "sqlite": DocSessionStore on top of SqliteDocStore
228
+ """
229
+ ss_cfg = cfg.storage.sessions
230
+
231
+ if ss_cfg.backend == "memory":
232
+ # If you want pure dict-backed like your original snippet, keep it.
233
+ # Otherwise you can also implement InMemoryDocStore + DocSessionStore.
234
+ from aethergraph.storage.sessions.inmem_store import InMemorySessionStore
235
+
236
+ return InMemorySessionStore()
237
+
238
+ if ss_cfg.backend == "fs":
239
+ from aethergraph.storage.docstore.fs_doc import FSDocStore
240
+ from aethergraph.storage.sessions.doc_store import DocSessionStore
241
+
242
+ base = os.path.join(cfg.root, ss_cfg.fs_root)
243
+ docs = FSDocStore(base)
244
+ return DocSessionStore(docs, prefix="session-") # windows-safe
245
+
246
+ if ss_cfg.backend == "sqlite":
247
+ from aethergraph.storage.sessions.sqlite_session_store import SQLiteSessionStore
248
+
249
+ db_path = os.path.join(cfg.root, ss_cfg.sqlite_path)
250
+ return SQLiteSessionStore(path=db_path)
251
+ raise ValueError(f"Unknown session storage backend: {ss_cfg.backend!r}")
252
+
253
+
254
+ def _secret_bytes(secret_key: str) -> bytes:
255
+ # simple default; support hex/env later if needed
256
+ return secret_key.encode("utf-8")
257
+
258
+
259
+ def _build_kvdoc_cont_store(
260
+ root: Path,
261
+ cfg: ContinuationStoreSettings,
262
+ secret: bytes,
263
+ ) -> AsyncContinuationStore:
264
+ kvdoc = cfg.kvdoc
265
+ from aethergraph.storage.continuation_store.kvdoc_cont import KVDocContinuationStore
266
+
267
+ # ---- DocStore ----
268
+ if kvdoc.doc_store_backend == "sqlite":
269
+ from aethergraph.storage.docstore.sqlite_doc import SqliteDocStore
270
+
271
+ doc_path = root / kvdoc.sqlite_doc_store_path
272
+ doc_path.parent.mkdir(parents=True, exist_ok=True)
273
+ doc_store: DocStore = SqliteDocStore(path=str(doc_path))
274
+ elif kvdoc.doc_store_backend == "fs":
275
+ from aethergraph.storage.docstore.fs_doc import FSDocStore
276
+
277
+ doc_dir = root / kvdoc.fs_doc_store_dir
278
+ doc_dir.mkdir(parents=True, exist_ok=True)
279
+ doc_store = FSDocStore(root=str(doc_dir))
280
+ else:
281
+ raise ValueError(f"Unknown doc_store_backend: {kvdoc.doc_store_backend}")
282
+
283
+ # ---- KV ----
284
+ if kvdoc.kv_backend == "sqlite":
285
+ from aethergraph.storage.kv.sqlite_kv import SqliteKV
286
+
287
+ kv_path = root / kvdoc.sqlite_kv_path
288
+ kv_path.parent.mkdir(parents=True, exist_ok=True)
289
+ kv: AsyncKV = SqliteKV(path=str(kv_path), prefix=f"{cfg.namespace}:")
290
+ elif kvdoc.kv_backend == "inmem":
291
+ from aethergraph.storage.kv.inmem_kv import InMemoryKV
292
+
293
+ kv = InMemoryKV(prefix=f"{cfg.namespace}:")
294
+ else:
295
+ raise ValueError(f"Unknown kv_backend: {kvdoc.kv_backend}")
296
+
297
+ # ---- EventLog (optional) ----
298
+ event_log: EventLog | None
299
+ if kvdoc.eventlog_backend == "none":
300
+ event_log = None
301
+ elif kvdoc.eventlog_backend == "sqlite":
302
+ from aethergraph.storage.eventlog.sqlite_event import SqliteEventLog
303
+
304
+ ev_path = root / kvdoc.sqlite_eventlog_path
305
+ ev_path.parent.mkdir(parents=True, exist_ok=True)
306
+ event_log = SqliteEventLog(path=str(ev_path))
307
+ elif kvdoc.eventlog_backend == "fs":
308
+ from aethergraph.storage.eventlog.fs_event import FSEventLog
309
+
310
+ ev_dir = root / kvdoc.fs_eventlog_dir
311
+ ev_dir.mkdir(parents=True, exist_ok=True)
312
+ event_log = FSEventLog(root=str(ev_dir))
313
+ else:
314
+ raise ValueError(f"Unknown eventlog_backend: {kvdoc.eventlog_backend}")
315
+
316
+ return KVDocContinuationStore(
317
+ doc_store=doc_store,
318
+ kv=kv,
319
+ event_log=event_log,
320
+ secret=secret,
321
+ namespace=cfg.namespace,
322
+ )
323
+
324
+
325
+ def build_continuation_store(cfg: AppSettings) -> AsyncContinuationStore:
326
+ """
327
+ High-level factory used by your runtime builder.
328
+
329
+ Mirrors `build_artifact_store(cfg)` in style.
330
+ """
331
+ root = Path(cfg.root).resolve()
332
+ cont_cfg: ContinuationStoreSettings = cfg.storage.continuation
333
+ secret = _secret_bytes(cont_cfg.secret_key)
334
+
335
+ if cont_cfg.backend == "memory":
336
+ from aethergraph.services.continuations.stores.inmem_store import InMemoryContinuationStore
337
+
338
+ return InMemoryContinuationStore(secret=secret)
339
+
340
+ if cont_cfg.backend == "fs":
341
+ from aethergraph.services.continuations.stores.fs_store import FSContinuationStore
342
+
343
+ # Keep old FS behavior for people who rely on on-disk layout.
344
+ fs_root = root / cont_cfg.fs.root
345
+ fs_root.parent.mkdir(parents=True, exist_ok=True)
346
+ return FSContinuationStore(root=fs_root, secret=secret)
347
+
348
+ if cont_cfg.backend == "kvdoc":
349
+ return _build_kvdoc_cont_store(root, cont_cfg, secret)
350
+
351
+ raise ValueError(f"Unknown continuation backend: {cont_cfg.backend}")
352
+
353
+
354
+ def build_vector_index(cfg: AppSettings):
355
+ """
356
+ Build a VectorIndex based on cfg.storage.vector_index.
357
+ """
358
+ vcfg = cfg.storage.vector_index
359
+ root = os.path.abspath(cfg.root)
360
+
361
+ if vcfg.backend == "sqlite":
362
+ from aethergraph.storage.vector_index.sqlite_index import SQLiteVectorIndex
363
+
364
+ index_root = os.path.join(root, vcfg.sqlite.dir)
365
+ return SQLiteVectorIndex(root=index_root)
366
+
367
+ if vcfg.backend == "faiss":
368
+ from aethergraph.storage.vector_index.faiss_index import FAISSVectorIndex
369
+
370
+ index_root = os.path.join(root, vcfg.faiss.dir)
371
+ return FAISSVectorIndex(root=index_root, dim=vcfg.faiss.dim)
372
+
373
+ if vcfg.backend == "chroma":
374
+ try:
375
+ import chromadb
376
+ except ImportError as e:
377
+ chromadb = None # type: ignore
378
+ raise RuntimeError("Chroma backend requires `chromadb` to be installed.") from e
379
+ from aethergraph.storage.vector_index.chroma_index import ChromaVectorIndex
380
+
381
+ if chromadb is None:
382
+ raise RuntimeError(
383
+ "Chroma backend selected, but `chromadb` is not installed. "
384
+ "Install it with `pip install chromadb`."
385
+ )
386
+ persist_dir = os.path.join(root, vcfg.chroma.persist_dir)
387
+ client = chromadb.PersistentClient(path=persist_dir)
388
+ return ChromaVectorIndex(
389
+ client=client,
390
+ collection_prefix=vcfg.chroma.collection_prefix,
391
+ )
392
+
393
+ raise ValueError(f"Unknown vector index backend: {vcfg.backend!r}")
394
+
395
+
396
+ def build_memory_persistence(cfg: AppSettings) -> Persistence:
397
+ mp = cfg.storage.memory.persistence
398
+ root = cfg.root
399
+
400
+ if mp.backend == "fs":
401
+ from aethergraph.storage.memory.fs_persist import FSPersistence
402
+
403
+ return FSPersistence(base_dir=root)
404
+
405
+ if mp.backend == "eventlog":
406
+ from aethergraph.storage.memory.event_persist import EventLogPersistence
407
+
408
+ docs = build_doc_store(cfg)
409
+ log = build_event_log(cfg)
410
+ if log is None:
411
+ raise ValueError("memory.persistence.backend=eventlog requires a non-none EventLog")
412
+ return EventLogPersistence(
413
+ log=log,
414
+ docs=docs,
415
+ uri_prefix=mp.uri_prefix,
416
+ )
417
+
418
+ raise ValueError(f"Unknown memory persistence backend: {mp.backend!r}")
419
+
420
+
421
+ def build_memory_hotlog(cfg: AppSettings) -> HotLog:
422
+ from aethergraph.storage.memory.hotlog import KVHotLog
423
+
424
+ kv = build_kv_store(cfg, extra_prefix="mem:hot:")
425
+ return KVHotLog(kv=kv)
426
+
427
+
428
+ def build_memory_indices(cfg: AppSettings) -> Indices:
429
+ from aethergraph.storage.memory.indices import KVIndices
430
+
431
+ kv = build_kv_store(cfg, extra_prefix="mem:idx:")
432
+ return KVIndices(kv=kv, hot_ttl_s=cfg.storage.memory.indices.ttl_s)
@@ -0,0 +1,28 @@
1
+ import asyncio
2
+ from pathlib import Path
3
+ from urllib.parse import unquote, urlparse
4
+ from urllib.request import url2pathname
5
+
6
+
7
+ async def to_thread(fn, *a, **k):
8
+ return await asyncio.to_thread(fn, *a, **k)
9
+
10
+
11
+ def _to_file_uri(path_str: str) -> str:
12
+ """Canonical RFC-8089 file URI (file:///C:/..., forward slashes)."""
13
+ return Path(path_str).resolve().as_uri()
14
+
15
+
16
+ def _from_uri_or_path(s: str) -> Path:
17
+ """Robustly turn a file:// URI or plain path into a local Path."""
18
+ if "://" not in s:
19
+ return Path(s)
20
+ u = urlparse(s)
21
+ if (u.scheme or "").lower() != "file":
22
+ raise ValueError(f"Unsupported URI scheme: {u.scheme}")
23
+ # if u.netloc:
24
+ # raw = f"//{u.netloc}{u.path}" # UNC: file://server/share/...
25
+ # else:
26
+ # raw = u.path # Local drive: file:///C:/...
27
+ raw = f"//{u.netloc}{u.path}" if u.netloc else u.path
28
+ return Path(url2pathname(unquote(raw)))
@@ -0,0 +1,64 @@
1
+ import time
2
+
3
+ from aethergraph.contracts.services.state_stores import GraphSnapshot, GraphStateStore, StateEvent
4
+ from aethergraph.contracts.storage.doc_store import DocStore
5
+ from aethergraph.contracts.storage.event_log import EventLog
6
+
7
+
8
+ class GraphStateStoreImpl(GraphStateStore):
9
+ """
10
+ Generic GraphStateStore implementation that combines a DocStore for snapshots
11
+ - DocStore for storing GraphSnapshot documents
12
+ - EventLog for storing StateEvent logs
13
+ """
14
+
15
+ def __init__(self, *, doc_store: "DocStore", event_log: "EventLog"):
16
+ self._docs = doc_store
17
+ self._log = event_log
18
+
19
+ def _snapshot_id(self, run_id: str) -> str:
20
+ return f"graph_state/{run_id}/latest"
21
+
22
+ async def save_snapshot(self, snap: GraphSnapshot) -> None:
23
+ # TODO: consider add history of snapshots by rev/timestamp
24
+ # e.g. hist_id = f"graph_state/{run_id}/rev_{snap.rev:08d}_{int(snap.created_at)}"
25
+ # self._docs.put(hist_id, snap.__dict__)
26
+ # but this is not needed for retrieval of latest snapshot
27
+ await self._docs.put(self._snapshot_id(snap.run_id), snap.__dict__)
28
+
29
+ async def load_latest_snapshot(self, run_id) -> GraphSnapshot | None:
30
+ # The saved snapshot is always the latest so just fetch by fixed id
31
+ doc = await self._docs.get(self._snapshot_id(run_id))
32
+ return GraphSnapshot(**doc) if doc else None
33
+
34
+ async def append_event(self, ev: StateEvent) -> None:
35
+ # standard event log append
36
+ payload = ev.__dict__.copy()
37
+ payload.setdefault("scope_id", ev.run_id)
38
+ payload.setdefault("kind", "graph_state")
39
+ payload.setdefault("ts", time.time())
40
+ await self._log.append(payload)
41
+
42
+ async def load_events_since(self, run_id, from_rev) -> list[StateEvent]:
43
+ rows = await self._log.query(
44
+ scope_id=run_id,
45
+ kinds=["graph_state"],
46
+ # from_rev filter will be applied below
47
+ )
48
+ out = []
49
+ for row in rows:
50
+ if row.get("rev", -1) > from_rev:
51
+ out.append(StateEvent(**row))
52
+ return out
53
+
54
+ async def list_run_ids(self, graph_id: str | None = None) -> list[str]:
55
+ # Basic version: ask DocStore for all ids and filter. This is sufficient for local/file-based stores.
56
+ # In cloud implementations, this should be optimized with proper indexing
57
+ ids = await self._docs.list()
58
+ runs: set[str] = set()
59
+ for doc_id in ids:
60
+ if not doc_id.startswith("graph_state/"):
61
+ continue
62
+ _, run_id, *_ = doc_id.split("/")
63
+ runs.add(run_id)
64
+ return sorted(runs)