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,399 @@
1
+ # aethergraph/storage/sqlite_session_store.py
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ from collections.abc import Sequence
7
+ from datetime import datetime, timezone
8
+ import json
9
+ from pathlib import Path
10
+ import sqlite3
11
+ import threading
12
+ from typing import Any
13
+ import uuid
14
+
15
+ from aethergraph.api.v1.schemas import Session
16
+ from aethergraph.contracts.services.sessions import SessionStore
17
+ from aethergraph.core.runtime.run_types import SessionKind
18
+
19
+
20
+ def _dt_to_ts(dt: datetime | None) -> float | None:
21
+ if dt is None:
22
+ return None
23
+ if dt.tzinfo is None:
24
+ dt = dt.replace(tzinfo=timezone.utc)
25
+ return dt.timestamp()
26
+
27
+
28
+ def _parse_dt(val: Any) -> datetime | None:
29
+ if val is None:
30
+ return None
31
+ if isinstance(val, datetime):
32
+ return val
33
+ if isinstance(val, str):
34
+ try:
35
+ return datetime.fromisoformat(val)
36
+ except Exception:
37
+ return None
38
+ if isinstance(val, int | float):
39
+ try:
40
+ return datetime.fromtimestamp(float(val), tz=timezone.utc)
41
+ except Exception:
42
+ return None
43
+ return None
44
+
45
+
46
+ def _session_to_doc(sess: Session) -> dict[str, Any]:
47
+ # Support both Pydantic v1 (.dict) and v2 (.model_dump)
48
+ data = sess.model_dump() if hasattr(sess, "model_dump") else sess.dict()
49
+
50
+ # Normalize datetimes to ISO for JSON
51
+ for key in ("created_at", "updated_at", "last_artifact_at"):
52
+ if isinstance(data.get(key), datetime):
53
+ data[key] = data[key].isoformat()
54
+ return data
55
+
56
+
57
+ def _doc_to_session(doc: dict[str, Any]) -> Session:
58
+ # Convert ISO/ts back to datetime
59
+ for key in ("created_at", "updated_at", "last_artifact_at"):
60
+ if key in doc:
61
+ parsed = _parse_dt(doc[key])
62
+ if parsed is not None:
63
+ doc[key] = parsed
64
+
65
+ # Normalize kind if stored as str
66
+ if "kind" in doc and isinstance(doc["kind"], str):
67
+ try:
68
+ doc["kind"] = SessionKind(doc["kind"])
69
+ except Exception:
70
+ import logging
71
+
72
+ logger = logging.getLogger(__name__)
73
+ logger.warning(f"Unknown SessionKind stored in DB: {doc['kind']}")
74
+
75
+ return Session(**doc)
76
+
77
+
78
+ class SQLiteSessionStoreSync:
79
+ """
80
+ SQLite-backed SessionStore.
81
+
82
+ - Stores full Session as JSON in `data_json`
83
+ - Promotes session_id, kind, user_id, org_id, created_at, updated_at,
84
+ artifact_count, last_artifact_at to columns for fast listing / stats.
85
+ """
86
+
87
+ def __init__(self, path: str):
88
+ path_obj = Path(path)
89
+ path_obj.parent.mkdir(parents=True, exist_ok=True)
90
+
91
+ self._db = sqlite3.connect(
92
+ str(path_obj),
93
+ check_same_thread=False,
94
+ isolation_level=None,
95
+ )
96
+ self._db.execute("PRAGMA journal_mode=WAL;")
97
+ self._db.execute("PRAGMA synchronous=NORMAL;")
98
+
99
+ # Base table
100
+ self._db.execute(
101
+ """
102
+ CREATE TABLE IF NOT EXISTS sessions (
103
+ session_id TEXT PRIMARY KEY,
104
+ data_json TEXT NOT NULL,
105
+ kind TEXT NOT NULL,
106
+ user_id TEXT,
107
+ org_id TEXT,
108
+ created_at REAL NOT NULL,
109
+ updated_at REAL NOT NULL,
110
+ artifact_count INTEGER NOT NULL DEFAULT 0,
111
+ last_artifact_at REAL
112
+ )
113
+ """
114
+ )
115
+
116
+ # Indices
117
+ self._db.execute(
118
+ "CREATE INDEX IF NOT EXISTS idx_sessions_user_updated ON sessions(user_id, updated_at DESC)"
119
+ )
120
+ self._db.execute(
121
+ "CREATE INDEX IF NOT EXISTS idx_sessions_org_updated ON sessions(org_id, updated_at DESC)"
122
+ )
123
+ self._db.execute(
124
+ "CREATE INDEX IF NOT EXISTS idx_sessions_kind_updated ON sessions(kind, updated_at DESC)"
125
+ )
126
+
127
+ self._lock = threading.RLock()
128
+
129
+ # -------- core helpers --------
130
+
131
+ def _upsert(self, sess: Session) -> Session:
132
+ doc = _session_to_doc(sess)
133
+ payload = json.dumps(doc, ensure_ascii=False)
134
+
135
+ created_ts = _dt_to_ts(sess.created_at)
136
+ updated_ts = _dt_to_ts(sess.updated_at)
137
+ last_art_ts = _dt_to_ts(sess.last_artifact_at)
138
+ artifact_count = sess.artifact_count or 0
139
+
140
+ kind_val = sess.kind.value if isinstance(sess.kind, SessionKind) else str(sess.kind)
141
+
142
+ with self._lock:
143
+ self._db.execute(
144
+ """
145
+ INSERT INTO sessions (
146
+ session_id, data_json,
147
+ kind, user_id, org_id,
148
+ created_at, updated_at,
149
+ artifact_count, last_artifact_at
150
+ )
151
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
152
+ ON CONFLICT(session_id) DO UPDATE SET
153
+ data_json = excluded.data_json,
154
+ kind = excluded.kind,
155
+ user_id = excluded.user_id,
156
+ org_id = excluded.org_id,
157
+ created_at = excluded.created_at,
158
+ updated_at = excluded.updated_at,
159
+ artifact_count = excluded.artifact_count,
160
+ last_artifact_at = excluded.last_artifact_at
161
+ """,
162
+ (
163
+ sess.session_id,
164
+ payload,
165
+ kind_val,
166
+ sess.user_id,
167
+ sess.org_id,
168
+ created_ts,
169
+ updated_ts,
170
+ artifact_count,
171
+ last_art_ts,
172
+ ),
173
+ )
174
+ return sess
175
+
176
+ # -------- SessionStore-style API (sync) --------
177
+
178
+ def create(
179
+ self,
180
+ *,
181
+ kind: SessionKind,
182
+ user_id: str | None = None,
183
+ org_id: str | None = None,
184
+ title: str | None = None,
185
+ source: str = "webui",
186
+ external_ref: str | None = None,
187
+ ) -> Session:
188
+ now = datetime.now(timezone.utc)
189
+ sess = Session(
190
+ session_id=str(uuid.uuid4()),
191
+ kind=kind,
192
+ title=title,
193
+ user_id=user_id,
194
+ org_id=org_id,
195
+ source=source,
196
+ external_ref=external_ref,
197
+ created_at=now,
198
+ updated_at=now,
199
+ artifact_count=0,
200
+ last_artifact_at=None,
201
+ )
202
+ return self._upsert(sess)
203
+
204
+ def get(self, session_id: str) -> Session | None:
205
+ with self._lock:
206
+ row = self._db.execute(
207
+ "SELECT data_json FROM sessions WHERE session_id = ?",
208
+ (session_id,),
209
+ ).fetchone()
210
+ if not row:
211
+ return None
212
+ doc = json.loads(row[0])
213
+ return _doc_to_session(doc)
214
+
215
+ def delete(self, session_id: str) -> None:
216
+ with self._lock:
217
+ self._db.execute(
218
+ "DELETE FROM sessions WHERE session_id = ?",
219
+ (session_id,),
220
+ )
221
+
222
+ def list_for_user(
223
+ self,
224
+ *,
225
+ user_id: str | None,
226
+ org_id: str | None = None,
227
+ kind: SessionKind | None = None,
228
+ limit: int = 50,
229
+ offset: int = 0,
230
+ ) -> list[Session]:
231
+ where: list[str] = []
232
+ params: list[Any] = []
233
+
234
+ if user_id is not None:
235
+ where.append("user_id = ?")
236
+ params.append(user_id)
237
+ if org_id is not None:
238
+ where.append("org_id = ?")
239
+ params.append(org_id)
240
+ if kind is not None:
241
+ where.append("kind = ?")
242
+ params.append(kind.value if isinstance(kind, SessionKind) else str(kind))
243
+
244
+ sql = "SELECT data_json FROM sessions"
245
+ if where:
246
+ sql += " WHERE " + " AND ".join(where)
247
+ sql += " ORDER BY updated_at DESC"
248
+
249
+ sql += " LIMIT ? OFFSET ?"
250
+ params.extend([limit, offset])
251
+
252
+ with self._lock:
253
+ rows = self._db.execute(sql, params).fetchall()
254
+
255
+ return [_doc_to_session(json.loads(r[0])) for r in rows]
256
+
257
+ def touch(self, session_id: str, *, updated_at: datetime | None = None) -> None:
258
+ sess = self.get(session_id)
259
+ if not sess:
260
+ return
261
+ sess.updated_at = updated_at or datetime.now(timezone.utc)
262
+ self._upsert(sess)
263
+
264
+ def update(
265
+ self,
266
+ session_id: str,
267
+ *,
268
+ title: str | None = None,
269
+ external_ref: str | None = None,
270
+ ) -> Session | None:
271
+ sess = self.get(session_id)
272
+ if not sess:
273
+ return None
274
+ if title is not None:
275
+ sess.title = title
276
+ if external_ref is not None:
277
+ sess.external_ref = external_ref
278
+ sess.updated_at = datetime.now(timezone.utc)
279
+ return self._upsert(sess)
280
+
281
+ def record_artifact(
282
+ self,
283
+ session_id: str,
284
+ *,
285
+ created_at: datetime | None = None,
286
+ ) -> None:
287
+ """
288
+ Optional API used by ArtifactFacade._record via getattr(..., 'record_artifact').
289
+ Updates artifact_count + last_artifact_at + updated_at.
290
+ """
291
+ sess = self.get(session_id)
292
+ if not sess:
293
+ return
294
+
295
+ ts = created_at or datetime.now(timezone.utc)
296
+
297
+ sess.artifact_count = (sess.artifact_count or 0) + 1
298
+ if sess.last_artifact_at is None or ts > sess.last_artifact_at:
299
+ sess.last_artifact_at = ts
300
+
301
+ # For UI, bump updated_at as well
302
+ if ts > sess.updated_at:
303
+ sess.updated_at = ts
304
+ else:
305
+ sess.updated_at = datetime.now(timezone.utc)
306
+
307
+ self._upsert(sess)
308
+
309
+
310
+ class SQLiteSessionStore(SessionStore):
311
+ """
312
+ Async SessionStore implementation backed by SQLiteSessionStoreSync.
313
+ """
314
+
315
+ def __init__(self, path: str):
316
+ self._sync = SQLiteSessionStoreSync(path)
317
+
318
+ async def create(
319
+ self,
320
+ *,
321
+ kind: SessionKind,
322
+ user_id: str | None = None,
323
+ org_id: str | None = None,
324
+ title: str | None = None,
325
+ source: str = "webui",
326
+ external_ref: str | None = None,
327
+ ) -> Session:
328
+ # Delegate to sync create (which already constructs Session correctly)
329
+ return await asyncio.to_thread(
330
+ self._sync.create,
331
+ kind=kind,
332
+ user_id=user_id,
333
+ org_id=org_id,
334
+ title=title,
335
+ source=source,
336
+ external_ref=external_ref,
337
+ )
338
+
339
+ async def get(self, session_id: str) -> Session | None:
340
+ return await asyncio.to_thread(self._sync.get, session_id)
341
+
342
+ async def list_for_user(
343
+ self,
344
+ *,
345
+ user_id: str | None,
346
+ org_id: str | None = None,
347
+ kind: SessionKind | None = None,
348
+ limit: int = 50,
349
+ offset: int = 0,
350
+ ) -> Sequence[Session]:
351
+ return await asyncio.to_thread(
352
+ self._sync.list_for_user,
353
+ user_id=user_id,
354
+ org_id=org_id,
355
+ kind=kind,
356
+ limit=limit,
357
+ offset=offset,
358
+ )
359
+
360
+ async def touch(
361
+ self,
362
+ session_id: str,
363
+ *,
364
+ updated_at: datetime | None = None,
365
+ ) -> None:
366
+ await asyncio.to_thread(self._sync.touch, session_id, updated_at=updated_at)
367
+
368
+ async def update(
369
+ self,
370
+ session_id: str,
371
+ *,
372
+ title: str | None = None,
373
+ external_ref: str | None = None,
374
+ ) -> Session | None:
375
+ return await asyncio.to_thread(
376
+ self._sync.update,
377
+ session_id,
378
+ title=title,
379
+ external_ref=external_ref,
380
+ )
381
+
382
+ async def delete(self, session_id: str) -> None:
383
+ await asyncio.to_thread(self._sync.delete, session_id)
384
+
385
+ async def record_artifact(
386
+ self,
387
+ session_id: str,
388
+ *,
389
+ created_at: datetime | None = None,
390
+ ) -> None:
391
+ """
392
+ Optional method, called via getattr(..., 'record_artifact', None)
393
+ from ArtifactFacade._record.
394
+ """
395
+ await asyncio.to_thread(
396
+ self._sync.record_artifact,
397
+ session_id,
398
+ created_at=created_at,
399
+ )
@@ -0,0 +1,138 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ from typing import Any
5
+
6
+ from aethergraph.contracts.storage.vector_index import VectorIndex
7
+
8
+ try:
9
+ import chromadb
10
+ except Exception:
11
+ chromadb = None
12
+
13
+ """Adapter over a chromadb.Client.
14
+ NOTE: this is just a stub implementation to show how vector indexes can be adapted.
15
+ We can use the same VectorIndex interface for different backends with adaptation.
16
+
17
+ e.g.
18
+ - QdrantVectorIndex: corpus_id -> Qdrant collection, chunk_id -> point ID, vector + metadata stored per point.
19
+ - PineconeVectorIndex: corpus_id -> Pinecone index, chunk_id -> vector ID, vector + metadata stored per vector.
20
+ - WeaviateVectorIndex: corpus_id -> Weaviate class, chunk_id -> object ID, vector + metadata stored per object.
21
+ """
22
+
23
+
24
+ class ChromaVectorIndex(VectorIndex):
25
+ """
26
+ Adapter over a chromadb.Client.
27
+ Each `corpus_id` is mapped to a Chroma collection:
28
+ collection_name = f"{collection_prefix}{corpus_id}"
29
+ """
30
+
31
+ def __init__(
32
+ self,
33
+ client: chromadb.ClientAPI,
34
+ *,
35
+ collection_prefix: str = "rag_",
36
+ ):
37
+ if chromadb is None:
38
+ raise RuntimeError("ChromaVectorIndex requires `chromadb` to be installed.")
39
+ self._client = client
40
+ self._prefix = collection_prefix
41
+
42
+ def _collection_name(self, corpus_id: str) -> str:
43
+ return f"{self._prefix}{corpus_id}"
44
+
45
+ def _collection(self, corpus_id: str):
46
+ name = self._collection_name(corpus_id)
47
+ return self._client.get_or_create_collection(name=name)
48
+
49
+ async def add(
50
+ self,
51
+ corpus_id: str,
52
+ chunk_ids: list[str],
53
+ vectors: list[list[float]],
54
+ metas: list[dict[str, Any]],
55
+ ) -> None:
56
+ if not chunk_ids:
57
+ return
58
+ col = self._collection(corpus_id)
59
+ # Chroma's API is sync, but fast; you can wrap in to_thread if needed.
60
+ col.upsert(
61
+ ids=chunk_ids, # type: IDs
62
+ embeddings=vectors, # type: Embeddings
63
+ metadatas=metas, # type: Metadatas
64
+ )
65
+
66
+ async def delete(self, corpus_id: str, chunk_ids: list[str] | None = None) -> None:
67
+ name = self._collection_name(corpus_id)
68
+ if chunk_ids is None:
69
+ # Drop whole collection
70
+ try:
71
+ self._client.delete_collection(name=name)
72
+ except Exception as err:
73
+ raise RuntimeError(f"Failed to delete collection '{name}'") from err
74
+ return
75
+
76
+ try:
77
+ col = self._client.get_collection(name=name)
78
+ except Exception:
79
+ return
80
+ col.delete(ids=chunk_ids)
81
+
82
+ async def search(
83
+ self,
84
+ corpus_id: str,
85
+ query_vec: list[float],
86
+ k: int,
87
+ ) -> list[dict[str, Any]]:
88
+ try:
89
+ col = self._collection(corpus_id)
90
+ except Exception:
91
+ return []
92
+
93
+ def _search_sync() -> list[dict[str, Any]]:
94
+ # Chroma's client is sync; do this in a thread so we don't block the loop.
95
+ res = col.query(
96
+ query_embeddings=[query_vec],
97
+ n_results=k,
98
+ )
99
+
100
+ # Chroma returns lists-of-lists
101
+ ids = (res.get("ids") or [[]])[0]
102
+ dists = (res.get("distances") or [[]])[0]
103
+ metas = (res.get("metadatas") or [[]])[0]
104
+
105
+ out: list[dict[str, Any]] = []
106
+ for cid, dist, meta in zip(ids, dists, metas, strict=True):
107
+ # Chroma distance is "smaller is better".
108
+ # We convert to a "score" where larger is better.
109
+ # Any monotone transform is fine; here we use negative distance.
110
+ score = float(-dist)
111
+ out.append(
112
+ {
113
+ "chunk_id": cid,
114
+ "score": score,
115
+ "meta": meta or {},
116
+ }
117
+ )
118
+ return out
119
+
120
+ return await asyncio.to_thread(_search_sync)
121
+
122
+ async def list_corpora(self) -> list[str]:
123
+ cols = self._client.list_collections()
124
+ out = []
125
+ for c in cols:
126
+ name = c.name
127
+ if name.startswith(self._prefix):
128
+ out.append(name[len(self._prefix) :])
129
+ return out
130
+
131
+ async def list_chunks(self, corpus_id: str) -> list[str]:
132
+ try:
133
+ col = self._collection(corpus_id)
134
+ except Exception:
135
+ return []
136
+ # Chroma `get()` can be expensive for huge collections; fine for small/mid-scale.
137
+ res = col.get()
138
+ return list(res.get("ids") or [])
@@ -0,0 +1,179 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ from contextlib import suppress
5
+ from pathlib import Path
6
+ import pickle
7
+ import threading
8
+ from typing import Any
9
+
10
+ import numpy as np
11
+
12
+ from aethergraph.contracts.storage.vector_index import VectorIndex
13
+
14
+ try:
15
+ import faiss # type: ignore
16
+ except Exception:
17
+ faiss = None
18
+
19
+
20
+ class FAISSVectorIndex(VectorIndex):
21
+ """
22
+ Simple FAISS-backed index, one .index + .meta.pkl per corpus_id.
23
+ Uses cosine similarity via normalized vectors (IndexFlatIP).
24
+ """
25
+
26
+ def __init__(self, root: str, dim: int | None = None):
27
+ if faiss is None:
28
+ raise RuntimeError("FAISSVectorIndex requires `faiss` to be installed.")
29
+ self.root = Path(root)
30
+ self.root.mkdir(parents=True, exist_ok=True)
31
+ self.dim = dim # optional default dimension
32
+ self._lock = threading.RLock()
33
+
34
+ def _paths(self, corpus_id: str) -> tuple[Path, Path]:
35
+ base = self.root / corpus_id
36
+ return base.with_suffix(".index"), base.with_suffix(".meta.pkl")
37
+
38
+ def _load_sync(self, corpus_id: str):
39
+ idx_path, meta_path = self._paths(corpus_id)
40
+ if not (idx_path.exists() and meta_path.exists()):
41
+ return None, []
42
+ with self._lock:
43
+ index = faiss.read_index(str(idx_path))
44
+ with meta_path.open("rb") as f:
45
+ metas = pickle.load(f)
46
+ return index, metas
47
+
48
+ def _save_sync(self, corpus_id: str, index, metas: list[dict[str, Any]]) -> None:
49
+ idx_path, meta_path = self._paths(corpus_id)
50
+ self.root.mkdir(parents=True, exist_ok=True)
51
+ with self._lock:
52
+ faiss.write_index(index, str(idx_path))
53
+ with meta_path.open("wb") as f:
54
+ pickle.dump(metas, f)
55
+
56
+ async def add(
57
+ self,
58
+ corpus_id: str,
59
+ chunk_ids: list[str],
60
+ vectors: list[list[float]],
61
+ metas: list[dict[str, Any]],
62
+ ) -> None:
63
+ if faiss is None:
64
+ raise RuntimeError("FAISS not installed")
65
+ if not chunk_ids:
66
+ return
67
+
68
+ vecs = np.asarray(vectors, dtype=np.float32)
69
+ # Normalize for cosine
70
+ norms = np.linalg.norm(vecs, axis=1, keepdims=True) + 1e-9
71
+ vecs = vecs / norms
72
+
73
+ def _add_sync():
74
+ index, old_metas = self._load_sync(corpus_id)
75
+ d = vecs.shape[1]
76
+ if index is None:
77
+ # If dim was provided, optionally sanity-check
78
+ if self.dim is not None and self.dim != d:
79
+ raise ValueError(f"FAISSVectorIndex: dim mismatch {self.dim} vs {d}")
80
+ index = faiss.IndexFlatIP(d)
81
+ old_metas = []
82
+ self.dim = d
83
+
84
+ index.add(vecs)
85
+ for cid, m in zip(chunk_ids, metas, strict=True):
86
+ old_metas.append({"chunk_id": cid, "meta": m})
87
+ self._save_sync(corpus_id, index, old_metas)
88
+
89
+ await asyncio.to_thread(_add_sync)
90
+
91
+ async def delete(self, corpus_id: str, chunk_ids: list[str] | None = None) -> None:
92
+ if chunk_ids is None:
93
+ # Delete entire corpus
94
+ idx_path, meta_path = self._paths(corpus_id)
95
+ for p in (idx_path, meta_path):
96
+ with suppress(Exception):
97
+ p.unlink(missing_ok=True)
98
+ return
99
+
100
+ # Selective delete is tricky with plain FAISS; we’d need to rebuild.
101
+ # For now, keep explicit about limitations:
102
+ async def _delete_sync():
103
+ index, metas = self._load_sync(corpus_id)
104
+ if index is None:
105
+ return
106
+ keep_idxs = [i for i, m in enumerate(metas) if m["chunk_id"] not in set(chunk_ids)]
107
+ if not keep_idxs:
108
+ # Remove whole corpus
109
+ idx_path, meta_path = self._paths(corpus_id)
110
+ for p in (idx_path, meta_path):
111
+ try:
112
+ p.unlink(missing_ok=True)
113
+ except Exception as e:
114
+ import logging
115
+
116
+ logger = logging.getLogger("aethergraph.storage.vector_index.faiss_index")
117
+ logger.error(f"Failed to delete {p}: {e}")
118
+ return
119
+
120
+ # Rebuild index with kept vectors
121
+ # NOTE: we do NOT have original vectors here,
122
+ # so in a minimal implementation we simply raise.
123
+ raise NotImplementedError(
124
+ "FAISSVectorIndex: selective delete requires storing vectors; "
125
+ "either extend metadata to keep vectors, or rebuild from source."
126
+ )
127
+
128
+ await asyncio.to_thread(_delete_sync)
129
+
130
+ async def list_chunks(self, corpus_id: str) -> list[str]:
131
+ def _list_sync() -> list[str]:
132
+ _, metas = self._load_sync(corpus_id)
133
+ return [m["chunk_id"] for m in metas] if metas else []
134
+
135
+ return await asyncio.to_thread(_list_sync)
136
+
137
+ async def list_corpora(self) -> list[str]:
138
+ def _scan() -> list[str]:
139
+ out = []
140
+ for p in self.root.glob("*.meta.pkl"):
141
+ out.append(p.stem) # strip .meta.pkl => corpus_id
142
+ return out
143
+
144
+ return await asyncio.to_thread(_scan)
145
+
146
+ async def search(
147
+ self,
148
+ corpus_id: str,
149
+ query_vec: list[float],
150
+ k: int,
151
+ ) -> list[dict[str, Any]]:
152
+ if faiss is None:
153
+ raise RuntimeError("FAISS not installed")
154
+
155
+ q = np.asarray([query_vec], dtype=np.float32)
156
+ q = q / (np.linalg.norm(q, axis=1, keepdims=True) + 1e-9)
157
+
158
+ def _search_sync() -> list[dict[str, Any]]:
159
+ index, metas = self._load_sync(corpus_id)
160
+ if index is None or not metas:
161
+ return []
162
+ D, I = index.search(q, k) # noqa: E741
163
+ out: list[dict[str, Any]] = []
164
+ scores = D[0].tolist()
165
+ idxs = I[0].tolist()
166
+ for score, idx in zip(scores, idxs, strict=True):
167
+ if idx < 0 or idx >= len(metas):
168
+ continue
169
+ m = metas[idx]
170
+ out.append(
171
+ {
172
+ "chunk_id": m["chunk_id"],
173
+ "score": float(score),
174
+ "meta": m["meta"],
175
+ }
176
+ )
177
+ return out
178
+
179
+ return await asyncio.to_thread(_search_sync)