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,426 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import json
5
+ import os
6
+ import sqlite3
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 SqliteArtifactIndexSync:
14
+ """
15
+ SQLite-based artifact index.
16
+
17
+ - Good for tens/hundreds of thousands of artifacts.
18
+ - Stores labels/metrics as JSON.
19
+ """
20
+
21
+ def __init__(self, path: str):
22
+ os.makedirs(os.path.dirname(path), exist_ok=True)
23
+ self._conn = sqlite3.connect(path, check_same_thread=False)
24
+ self._conn.row_factory = sqlite3.Row
25
+ self._init_schema()
26
+
27
+ def _init_schema(self) -> None:
28
+ cur = self._conn.cursor()
29
+ cur.execute(
30
+ """
31
+ CREATE TABLE IF NOT EXISTS artifacts (
32
+ artifact_id TEXT PRIMARY KEY,
33
+ run_id TEXT,
34
+ graph_id TEXT,
35
+ node_id TEXT,
36
+ tool_name TEXT,
37
+ tool_version TEXT,
38
+ kind TEXT,
39
+ sha256 TEXT,
40
+ bytes INTEGER,
41
+ mime TEXT,
42
+ created_at TEXT,
43
+ labels_json TEXT,
44
+ metrics_json TEXT,
45
+ pinned INTEGER DEFAULT 0,
46
+ -- tenant / scope columns
47
+ org_id TEXT,
48
+ user_id TEXT,
49
+ client_id TEXT,
50
+ app_id TEXT,
51
+ session_id TEXT,
52
+ -- uri columns (may be missing in older DBs)
53
+ uri TEXT,
54
+ preview_uri TEXT
55
+ )
56
+ """
57
+ )
58
+ cur.execute(
59
+ """
60
+ CREATE TABLE IF NOT EXISTS artifact_occurrences (
61
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
62
+ artifact_id TEXT,
63
+ run_id TEXT,
64
+ graph_id TEXT,
65
+ node_id TEXT,
66
+ tool_name TEXT,
67
+ tool_version TEXT,
68
+ created_at TEXT,
69
+ labels_json TEXT
70
+ )
71
+ """
72
+ )
73
+
74
+ # Migration: add uri / preview_uri / tenant columns if missing
75
+ cur.execute("PRAGMA table_info(artifacts)")
76
+ cols = {row["name"] for row in cur.fetchall()}
77
+
78
+ if "uri" not in cols:
79
+ cur.execute("ALTER TABLE artifacts ADD COLUMN uri TEXT")
80
+ if "preview_uri" not in cols:
81
+ cur.execute("ALTER TABLE artifacts ADD COLUMN preview_uri TEXT")
82
+
83
+ # 🔹 NEW tenant columns (for existing DBs)
84
+ if "org_id" not in cols:
85
+ cur.execute("ALTER TABLE artifacts ADD COLUMN org_id TEXT")
86
+ if "user_id" not in cols:
87
+ cur.execute("ALTER TABLE artifacts ADD COLUMN user_id TEXT")
88
+ if "client_id" not in cols:
89
+ cur.execute("ALTER TABLE artifacts ADD COLUMN client_id TEXT")
90
+ if "app_id" not in cols:
91
+ cur.execute("ALTER TABLE artifacts ADD COLUMN app_id TEXT")
92
+ if "session_id" not in cols:
93
+ cur.execute("ALTER TABLE artifacts ADD COLUMN session_id TEXT")
94
+
95
+ # Existing indexes
96
+ cur.execute("CREATE INDEX IF NOT EXISTS idx_artifacts_run ON artifacts(run_id)")
97
+ cur.execute("CREATE INDEX IF NOT EXISTS idx_artifacts_kind ON artifacts(kind)")
98
+ cur.execute("CREATE INDEX IF NOT EXISTS idx_artifacts_sha ON artifacts(sha256)")
99
+ cur.execute(
100
+ "CREATE INDEX IF NOT EXISTS idx_occ_artifact ON artifact_occurrences(artifact_id)"
101
+ )
102
+
103
+ # 🔹 NEW tenant-oriented indexes (tune as needed)
104
+ cur.execute(
105
+ "CREATE INDEX IF NOT EXISTS idx_artifacts_user ON artifacts(user_id, created_at)"
106
+ )
107
+ cur.execute("CREATE INDEX IF NOT EXISTS idx_artifacts_org ON artifacts(org_id, created_at)")
108
+ cur.execute(
109
+ "CREATE INDEX IF NOT EXISTS idx_artifacts_session ON artifacts(session_id, created_at)"
110
+ )
111
+
112
+ self._conn.commit()
113
+
114
+ def upsert(self, a: Artifact) -> None:
115
+ rec = a.to_dict()
116
+ labels_json = json.dumps(rec.get("labels") or {}, ensure_ascii=False)
117
+ metrics_json = json.dumps(rec.get("metrics") or {}, ensure_ascii=False)
118
+
119
+ self._conn.execute(
120
+ """
121
+ INSERT INTO artifacts (
122
+ artifact_id,
123
+ run_id,
124
+ graph_id,
125
+ node_id,
126
+ tool_name,
127
+ tool_version,
128
+ kind,
129
+ sha256,
130
+ bytes,
131
+ mime,
132
+ created_at,
133
+ labels_json,
134
+ metrics_json,
135
+ pinned,
136
+ uri,
137
+ preview_uri,
138
+ org_id,
139
+ user_id,
140
+ client_id,
141
+ app_id,
142
+ session_id
143
+ ) VALUES (
144
+ :artifact_id,
145
+ :run_id,
146
+ :graph_id,
147
+ :node_id,
148
+ :tool_name,
149
+ :tool_version,
150
+ :kind,
151
+ :sha256,
152
+ :bytes,
153
+ :mime,
154
+ :created_at,
155
+ :labels_json,
156
+ :metrics_json,
157
+ :pinned,
158
+ :uri,
159
+ :preview_uri,
160
+ :org_id,
161
+ :user_id,
162
+ :client_id,
163
+ :app_id,
164
+ :session_id
165
+ )
166
+ ON CONFLICT(artifact_id) DO UPDATE SET
167
+ run_id = excluded.run_id,
168
+ graph_id = excluded.graph_id,
169
+ node_id = excluded.node_id,
170
+ tool_name = excluded.tool_name,
171
+ tool_version = excluded.tool_version,
172
+ kind = excluded.kind,
173
+ sha256 = excluded.sha256,
174
+ bytes = excluded.bytes,
175
+ mime = excluded.mime,
176
+ created_at = excluded.created_at,
177
+ labels_json = excluded.labels_json,
178
+ metrics_json = excluded.metrics_json,
179
+ pinned = excluded.pinned,
180
+ uri = excluded.uri,
181
+ preview_uri = excluded.preview_uri,
182
+ org_id = excluded.org_id,
183
+ user_id = excluded.user_id,
184
+ client_id = excluded.client_id,
185
+ app_id = excluded.app_id,
186
+ session_id = excluded.session_id
187
+ """,
188
+ {
189
+ "artifact_id": rec["artifact_id"],
190
+ "run_id": rec.get("run_id"),
191
+ "graph_id": rec.get("graph_id"),
192
+ "node_id": rec.get("node_id"),
193
+ "tool_name": rec.get("tool_name"),
194
+ "tool_version": rec.get("tool_version"),
195
+ "kind": rec.get("kind"),
196
+ "sha256": rec.get("sha256"),
197
+ "bytes": rec.get("bytes"),
198
+ "mime": rec.get("mime"),
199
+ "created_at": rec.get("created_at"),
200
+ "labels_json": labels_json,
201
+ "metrics_json": metrics_json,
202
+ "pinned": int(rec.get("pinned") or 0),
203
+ "uri": rec.get("uri"),
204
+ "preview_uri": rec.get("preview_uri"),
205
+ "org_id": rec.get("org_id"),
206
+ "user_id": rec.get("user_id"),
207
+ "client_id": rec.get("client_id"),
208
+ "app_id": rec.get("app_id"),
209
+ "session_id": rec.get("session_id"),
210
+ },
211
+ )
212
+ self._conn.commit()
213
+
214
+ def list_for_run(self, run_id: str) -> list[Artifact]:
215
+ cur = self._conn.execute(
216
+ "SELECT * FROM artifacts WHERE run_id = ? ORDER BY created_at ASC",
217
+ (run_id,),
218
+ )
219
+ rows = cur.fetchall()
220
+ return [self._row_to_artifact(r) for r in rows]
221
+
222
+ def search(
223
+ self,
224
+ *,
225
+ kind: str | None = None,
226
+ labels: dict[str, Any] | None = None,
227
+ metric: str | None = None,
228
+ mode: Literal["max", "min"] | None = None,
229
+ limit: int | None = None,
230
+ offset: int = 0,
231
+ ) -> list[Artifact]:
232
+ """Search artifacts with optional filtering and ranking.
233
+ Args:
234
+ kind: Optional kind to filter by.
235
+ labels: Optional dict of labels to filter by (exact match).
236
+ metric: Optional metric name to rank by.
237
+ mode: "max" or "min" for metric ranking.
238
+ limit: Optional max number of results to return.
239
+ offset: Number of results to skip from start.
240
+
241
+ Returns:
242
+ List of matching Artifact objects.
243
+
244
+ NOTE: If metric and mode are provided, ranking is done in Python
245
+ after fetching all candidates, which may be slower for large datasets.
246
+ """
247
+ where = []
248
+ params: list[Any] = []
249
+
250
+ if kind:
251
+ where.append("kind = ?")
252
+ params.append(kind)
253
+
254
+ TENANT_KEYS = {
255
+ "org_id": "org_id",
256
+ "user_id": "user_id",
257
+ "client_id": "client_id",
258
+ "app_id": "app_id",
259
+ "session_id": "session_id",
260
+ "run_id": "run_id",
261
+ }
262
+
263
+ if labels:
264
+ for k, v in labels.items():
265
+ if k == "tags":
266
+ tag_list = v if isinstance(v, list) else [v]
267
+ tag_list = [t for t in (t.strip() for t in tag_list) if t]
268
+ if tag_list:
269
+ ors = []
270
+ for t in tag_list:
271
+ ors.append("labels_json LIKE ?")
272
+ params.append(f'%"{k}":%"{t}"%')
273
+ where.append("(" + " OR ".join(ors) + ")")
274
+ continue
275
+
276
+ if k in TENANT_KEYS:
277
+ where.append(f"{TENANT_KEYS[k]} = ?")
278
+ params.append(v)
279
+ continue
280
+
281
+ where.append("labels_json LIKE ?")
282
+ params.append(f'%"{k}": "{v}"%')
283
+
284
+ base_sql = "SELECT * FROM artifacts"
285
+ if where:
286
+ base_sql += " WHERE " + " AND ".join(where)
287
+
288
+ # Fast path: no metric-based ranking → push ORDER + LIMIT/OFFSET to SQL
289
+ if not metric or not mode:
290
+ sql = base_sql + " ORDER BY created_at DESC"
291
+ if limit is not None:
292
+ sql += " LIMIT ? OFFSET ?"
293
+ params.extend([limit, offset])
294
+ elif offset:
295
+ # offset without limit is weird; just add a huge limit for safety
296
+ sql += " LIMIT -1 OFFSET ?"
297
+ params.append(offset)
298
+
299
+ cur = self._conn.execute(sql, params)
300
+ rows = [self._row_to_artifact(r) for r in cur.fetchall()]
301
+ return rows
302
+
303
+ # Slow path: metric sorting in Python (same as before)
304
+ sql = base_sql + " ORDER BY created_at DESC"
305
+ cur = self._conn.execute(sql, params)
306
+ rows = [self._row_to_artifact(r) for r in cur.fetchall()]
307
+
308
+ rows = [a for a in rows if metric in (a.metrics or {})]
309
+ rows.sort(
310
+ key=lambda a: a.metrics[metric],
311
+ reverse=(mode == "max"),
312
+ )
313
+
314
+ if offset:
315
+ rows = rows[offset:]
316
+ if limit is not None:
317
+ rows = rows[:limit]
318
+
319
+ return rows
320
+
321
+ def best(
322
+ self,
323
+ *,
324
+ kind: str,
325
+ metric: str,
326
+ mode: Literal["max", "min"],
327
+ filters: dict[str, Any] | None = None,
328
+ ) -> Artifact | None:
329
+ rows = self.search(
330
+ kind=kind,
331
+ labels=filters,
332
+ metric=metric,
333
+ mode=mode,
334
+ limit=1,
335
+ )
336
+ return rows[0] if rows else None
337
+
338
+ def pin(self, artifact_id: str, pinned: bool = True) -> None:
339
+ self._conn.execute(
340
+ "UPDATE artifacts SET pinned = ? WHERE artifact_id = ?",
341
+ (int(bool(pinned)), artifact_id),
342
+ )
343
+ self._conn.commit()
344
+
345
+ def record_occurrence(self, a: Artifact, extra_labels: dict | None = None) -> None:
346
+ labels = {**(a.labels or {}), **(extra_labels or {})}
347
+ labels_json = json.dumps(labels, ensure_ascii=False)
348
+ self._conn.execute(
349
+ """
350
+ INSERT INTO artifact_occurrences (
351
+ artifact_id, run_id, graph_id, node_id,
352
+ tool_name, tool_version, created_at, labels_json
353
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
354
+ """,
355
+ (
356
+ a.artifact_id,
357
+ a.run_id,
358
+ a.graph_id,
359
+ a.node_id,
360
+ a.tool_name,
361
+ a.tool_version,
362
+ a.created_at,
363
+ labels_json,
364
+ ),
365
+ )
366
+ self._conn.commit()
367
+
368
+ # -------- helpers --------
369
+
370
+ def _row_to_artifact(self, row: sqlite3.Row) -> Artifact:
371
+ labels = json.loads(row["labels_json"] or "{}")
372
+ metrics = json.loads(row["metrics_json"] or "{}")
373
+ return Artifact(
374
+ artifact_id=row["artifact_id"],
375
+ run_id=row["run_id"],
376
+ graph_id=row["graph_id"],
377
+ node_id=row["node_id"],
378
+ tool_name=row["tool_name"],
379
+ tool_version=row["tool_version"],
380
+ kind=row["kind"],
381
+ sha256=row["sha256"],
382
+ bytes=row["bytes"],
383
+ mime=row["mime"],
384
+ created_at=row["created_at"],
385
+ labels=labels,
386
+ metrics=metrics,
387
+ pinned=bool(row["pinned"]),
388
+ uri=row["uri"], # real URI
389
+ preview_uri=row["preview_uri"], # real preview URI (may be None)
390
+ )
391
+
392
+ def get(self, artifact_id: str) -> Artifact | None:
393
+ cur = self._conn.execute(
394
+ "SELECT * FROM artifacts WHERE artifact_id = ?",
395
+ (artifact_id,),
396
+ )
397
+ row = cur.fetchone()
398
+ if row:
399
+ return self._row_to_artifact(row)
400
+ return None
401
+
402
+
403
+ class SqliteArtifactIndex(AsyncArtifactIndex):
404
+ def __init__(self, path: str):
405
+ self._sync = SqliteArtifactIndexSync(path)
406
+
407
+ async def upsert(self, a: Artifact) -> None:
408
+ await asyncio.to_thread(self._sync.upsert, a)
409
+
410
+ async def list_for_run(self, run_id: str) -> list[Artifact]:
411
+ return await asyncio.to_thread(self._sync.list_for_run, run_id)
412
+
413
+ async def search(self, **kwargs) -> list[Artifact]:
414
+ return await asyncio.to_thread(self._sync.search, **kwargs)
415
+
416
+ async def best(self, **kwargs) -> Artifact | None:
417
+ return await asyncio.to_thread(self._sync.best, **kwargs)
418
+
419
+ async def pin(self, artifact_id: str, pinned: bool = True) -> None:
420
+ await asyncio.to_thread(self._sync.pin, artifact_id, pinned)
421
+
422
+ async def record_occurrence(self, a: Artifact, extra_labels: dict | None = None) -> None:
423
+ await asyncio.to_thread(self._sync.record_occurrence, a, extra_labels)
424
+
425
+ async def get(self, artifact_id: str) -> Artifact | None:
426
+ return await asyncio.to_thread(self._sync.get, artifact_id)