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
@@ -5,7 +5,9 @@ from typing import Any
5
5
 
6
6
  from aethergraph.contracts.services.artifacts import AsyncArtifactStore # generic protocol
7
7
  from aethergraph.contracts.services.memory import HotLog, Indices, Persistence
8
+ from aethergraph.contracts.storage.doc_store import DocStore
8
9
  from aethergraph.services.memory.facade import MemoryFacade
10
+ from aethergraph.services.scope.scope import Scope
9
11
 
10
12
  """
11
13
  # --- Artifacts (async FS store)
@@ -45,9 +47,10 @@ class MemoryFactory:
45
47
  persistence: Persistence
46
48
  indices: Indices # key-value backed indices for fast lookups, not artifact storage index
47
49
  artifacts: AsyncArtifactStore
50
+ docs: DocStore # document store for RAG
48
51
  hot_limit: int = 1000
49
52
  hot_ttl_s: int = 7 * 24 * 3600
50
- default_signal_threshold: float = 0.25
53
+ default_signal_threshold: float = 0.0
51
54
  logger: Any | None = None
52
55
  llm_service: Any | None = None # LLMService
53
56
  rag_facade: Any | None = None # RAGFacade
@@ -58,16 +61,19 @@ class MemoryFactory:
58
61
  *,
59
62
  graph_id: str | None = None,
60
63
  node_id: str | None = None,
61
- agent_id: str | None = None,
64
+ session_id: str | None = None,
65
+ scope: Scope | None = None,
62
66
  ) -> MemoryFacade:
63
67
  return MemoryFacade(
64
68
  run_id=run_id,
65
69
  graph_id=graph_id,
70
+ session_id=session_id,
66
71
  node_id=node_id,
67
- agent_id=agent_id,
72
+ scope=scope,
68
73
  hotlog=self.hotlog,
69
74
  persistence=self.persistence,
70
75
  indices=self.indices,
76
+ docs=self.docs,
71
77
  artifact_store=self.artifacts,
72
78
  hot_limit=self.hot_limit,
73
79
  hot_ttl_s=self.hot_ttl_s,
@@ -0,0 +1,10 @@
1
+ def _summary_prefix(scope_id: str, summary_tag: str) -> str:
2
+ return f"mem/{scope_id}/summaries/{summary_tag}/"
3
+
4
+
5
+ def _summary_doc_id(scope_id: str, summary_tag: str, ts: str) -> str:
6
+ """
7
+ Build a doc_id for a summary. We assume `ts` is an ISO-ish string
8
+ (e.g. from now_iso()) and rely on lexicographic ordering.
9
+ """
10
+ return f"{_summary_prefix(scope_id, summary_tag)}{ts}"
@@ -0,0 +1,470 @@
1
+ from datetime import datetime, timedelta, timezone
2
+ from typing import Any
3
+
4
+ from aethergraph.contracts.services.metering import MeteringService, MeteringStore
5
+ from aethergraph.services.scope.scope import Scope
6
+
7
+
8
+ class EventLogMeteringService(MeteringService):
9
+ """
10
+ MeteringService implementation backed by a MeteringStore (which itself
11
+ is backed by an EventLog).
12
+
13
+ Behavior notes:
14
+ - All events are tagged with "meter" for easy filtering
15
+ - Events are stored with their original metadata for later retrieval
16
+ - if run_ids is provided in read methods, only events matching those run_ids are returned.
17
+ This is useful for demo/multi-tenant isolation based on client-tagged runs.
18
+ """
19
+
20
+ def __init__(self, store: MeteringStore):
21
+ self._store = store
22
+
23
+ # ---------- helpers -----------
24
+
25
+ @staticmethod
26
+ def _now() -> datetime:
27
+ return datetime.now(timezone.utc)
28
+
29
+ @staticmethod
30
+ def _parse_window(window: str) -> datetime:
31
+ if not window:
32
+ return datetime.min.replace(tzinfo=timezone.utc)
33
+
34
+ unit = window[-1]
35
+ try:
36
+ value = int(window[:-1])
37
+ except ValueError:
38
+ return datetime.min.replace(tzinfo=timezone.utc)
39
+
40
+ if unit == "h":
41
+ delta = timedelta(hours=value)
42
+ elif unit == "d":
43
+ delta = timedelta(days=value)
44
+ else:
45
+ delta = timedelta(0)
46
+
47
+ return datetime.now(timezone.utc) - delta
48
+
49
+ @staticmethod
50
+ def _dims_from_scope(
51
+ scope: Scope | None,
52
+ *,
53
+ user_id: str | None = None,
54
+ org_id: str | None = None,
55
+ run_id: str | None = None,
56
+ graph_id: str | None = None,
57
+ client_id: str | None = None,
58
+ app_id: str | None = None,
59
+ session_id: str | None = None,
60
+ ) -> dict[str, Any]:
61
+ """
62
+ Merge identity/execution dimensions from Scope + explicit overrides.
63
+
64
+ - If scope is None, we just return the explicit values.
65
+ - If scope is present, its metering_dimensions() provide defaults,
66
+ and explicit args win when provided.
67
+ """
68
+ if scope is None:
69
+ return {
70
+ "user_id": user_id,
71
+ "org_id": org_id,
72
+ "run_id": run_id,
73
+ "graph_id": graph_id,
74
+ "client_id": client_id,
75
+ "app_id": app_id,
76
+ "session_id": session_id,
77
+ }
78
+
79
+ s = scope.metering_dimensions()
80
+ return {
81
+ "user_id": user_id if user_id is not None else s.get("user_id"),
82
+ "org_id": org_id if org_id is not None else s.get("org_id"),
83
+ "run_id": run_id if run_id is not None else s.get("run_id"),
84
+ "graph_id": graph_id if graph_id is not None else s.get("graph_id"),
85
+ "client_id": client_id if client_id is not None else s.get("client_id"),
86
+ "app_id": app_id if app_id is not None else s.get("app_id"),
87
+ "session_id": session_id if session_id is not None else s.get("session_id"),
88
+ }
89
+
90
+ async def _query(
91
+ self,
92
+ *,
93
+ window: str,
94
+ kinds: list[str],
95
+ user_id: str | None,
96
+ org_id: str | None,
97
+ run_ids: set[str] | None = None,
98
+ ) -> list[dict[str, Any]]:
99
+ cutoff = self._parse_window(window)
100
+
101
+ # Dev/local: don't restrict by user/org at the store level
102
+ store_user = None if user_id == "local" or org_id == "local" else user_id
103
+ store_org = None if user_id == "local" or org_id == "local" else org_id
104
+
105
+ rows = await self._store.query(
106
+ since=cutoff,
107
+ until=None,
108
+ kinds=kinds,
109
+ limit=None,
110
+ user_id=store_user,
111
+ org_id=store_org,
112
+ )
113
+
114
+ out: list[dict[str, Any]] = []
115
+
116
+ for e in rows:
117
+ if run_ids is not None:
118
+ rid = e.get("run_id")
119
+ if not rid or rid not in run_ids:
120
+ continue
121
+ out.append(e)
122
+ continue
123
+
124
+ # For non-run_ids code paths, we've *already* filtered by user/org in SQL.
125
+ # Keep the local special case (store_user=None/store_org=None) which
126
+ # means "no tenant filter" for dev.
127
+ out.append(e)
128
+
129
+ return out
130
+
131
+ async def _append(self, event: dict[str, Any]) -> None:
132
+ # Ensure ts is always set
133
+ event.setdefault("ts", self._now().isoformat())
134
+ await self._store.append(event)
135
+
136
+ # ---------- record_* methods ----------
137
+
138
+ async def record_llm(
139
+ self,
140
+ *,
141
+ scope: Scope | None = None,
142
+ user_id: str | None = None,
143
+ org_id: str | None = None,
144
+ run_id: str | None = None,
145
+ graph_id: str | None = None,
146
+ client_id: str | None = None,
147
+ app_id: str | None = None,
148
+ session_id: str | None = None,
149
+ model: str,
150
+ provider: str,
151
+ prompt_tokens: int,
152
+ completion_tokens: int,
153
+ latency_ms: int | None = None,
154
+ ) -> None:
155
+ dims = self._dims_from_scope(
156
+ scope,
157
+ user_id=user_id,
158
+ org_id=org_id,
159
+ run_id=run_id,
160
+ graph_id=graph_id,
161
+ client_id=client_id,
162
+ app_id=app_id,
163
+ session_id=session_id,
164
+ )
165
+ await self._append(
166
+ {
167
+ "kind": "meter.llm",
168
+ "user_id": dims["user_id"],
169
+ "org_id": dims["org_id"],
170
+ "client_id": dims["client_id"],
171
+ "app_id": dims["app_id"],
172
+ "session_id": dims["session_id"],
173
+ "run_id": dims["run_id"],
174
+ "graph_id": dims["graph_id"],
175
+ "model": model,
176
+ "provider": provider,
177
+ "prompt_tokens": int(prompt_tokens),
178
+ "completion_tokens": int(completion_tokens),
179
+ "latency_ms": int(latency_ms) if latency_ms is not None else None,
180
+ "tags": ["meter.llm"],
181
+ }
182
+ )
183
+
184
+ async def record_run(
185
+ self,
186
+ *,
187
+ scope: Scope | None = None,
188
+ user_id: str | None = None,
189
+ org_id: str | None = None,
190
+ run_id: str | None = None,
191
+ graph_id: str | None = None,
192
+ client_id: str | None = None,
193
+ app_id: str | None = None,
194
+ session_id: str | None = None,
195
+ status: str | None = None,
196
+ duration_s: float | None = None,
197
+ ) -> None:
198
+ dims = self._dims_from_scope(
199
+ scope,
200
+ user_id=user_id,
201
+ org_id=org_id,
202
+ run_id=run_id,
203
+ graph_id=graph_id,
204
+ client_id=client_id,
205
+ app_id=app_id,
206
+ session_id=session_id,
207
+ )
208
+
209
+ await self._append(
210
+ {
211
+ "kind": "meter.run",
212
+ "user_id": dims["user_id"],
213
+ "org_id": dims["org_id"],
214
+ "client_id": dims["client_id"],
215
+ "app_id": dims["app_id"],
216
+ "session_id": dims["session_id"],
217
+ "run_id": dims["run_id"],
218
+ "graph_id": dims["graph_id"],
219
+ "status": status,
220
+ "duration_s": float(duration_s) if duration_s is not None else None,
221
+ "tags": ["meter.run"],
222
+ }
223
+ )
224
+
225
+ async def record_artifact(
226
+ self,
227
+ *,
228
+ scope: Scope | None = None,
229
+ user_id: str | None = None,
230
+ org_id: str | None = None,
231
+ run_id: str | None = None,
232
+ graph_id: str | None = None,
233
+ client_id: str | None = None,
234
+ app_id: str | None = None,
235
+ session_id: str | None = None,
236
+ kind: str,
237
+ bytes: int,
238
+ pinned: bool = False,
239
+ ) -> None:
240
+ dims = self._dims_from_scope(
241
+ scope,
242
+ user_id=user_id,
243
+ org_id=org_id,
244
+ run_id=run_id,
245
+ graph_id=graph_id,
246
+ client_id=client_id,
247
+ app_id=app_id,
248
+ session_id=session_id,
249
+ )
250
+ await self._append(
251
+ {
252
+ "kind": "meter.artifact",
253
+ "user_id": dims["user_id"],
254
+ "org_id": dims["org_id"],
255
+ "client_id": dims["client_id"],
256
+ "app_id": dims["app_id"],
257
+ "session_id": dims["session_id"],
258
+ "run_id": dims["run_id"],
259
+ "graph_id": dims["graph_id"],
260
+ "artifact_kind": kind,
261
+ "bytes": int(bytes),
262
+ "pinned": bool(pinned),
263
+ "tags": ["meter.artifact"],
264
+ }
265
+ )
266
+
267
+ async def record_event(
268
+ self,
269
+ *,
270
+ scope: Scope | None = None,
271
+ user_id: str | None = None,
272
+ org_id: str | None = None,
273
+ run_id: str | None = None,
274
+ client_id: str | None = None,
275
+ app_id: str | None = None,
276
+ session_id: str | None = None,
277
+ scope_id: str | None = None,
278
+ kind: str,
279
+ ) -> None:
280
+ dims = self._dims_from_scope(
281
+ scope,
282
+ user_id=user_id,
283
+ org_id=org_id,
284
+ run_id=run_id,
285
+ graph_id=None,
286
+ client_id=client_id,
287
+ app_id=app_id,
288
+ session_id=session_id,
289
+ )
290
+
291
+ await self._append(
292
+ {
293
+ "kind": "meter.event",
294
+ "user_id": dims["user_id"],
295
+ "org_id": dims["org_id"],
296
+ "client_id": dims["client_id"],
297
+ "app_id": dims["app_id"],
298
+ "session_id": dims["session_id"],
299
+ "run_id": dims["run_id"],
300
+ "event_kind": kind,
301
+ "scope_id": scope_id,
302
+ "tags": ["meter.event"],
303
+ }
304
+ )
305
+
306
+ # ---------- read methods (unchanged for now) ----------
307
+
308
+ async def get_overview(
309
+ self,
310
+ *,
311
+ user_id: str | None = None,
312
+ org_id: str | None = None,
313
+ window: str = "24h",
314
+ run_ids: set[str] | None = None,
315
+ ) -> dict[str, int]:
316
+ llm = await self._query(
317
+ window=window,
318
+ kinds=["meter.llm"],
319
+ user_id=user_id,
320
+ org_id=org_id,
321
+ run_ids=run_ids,
322
+ )
323
+
324
+ runs = await self._query(
325
+ window=window,
326
+ kinds=["meter.run"],
327
+ user_id=user_id,
328
+ org_id=org_id,
329
+ run_ids=run_ids,
330
+ )
331
+ artifacts = await self._query(
332
+ window=window,
333
+ kinds=["meter.artifact"],
334
+ user_id=user_id,
335
+ org_id=org_id,
336
+ run_ids=run_ids,
337
+ )
338
+ events = await self._query(
339
+ window=window,
340
+ kinds=["meter.event"],
341
+ user_id=user_id,
342
+ org_id=org_id,
343
+ run_ids=run_ids,
344
+ )
345
+
346
+ return {
347
+ "llm_calls": len(llm),
348
+ "llm_prompt_tokens": sum(e.get("prompt_tokens", 0) for e in llm),
349
+ "llm_completion_tokens": sum(e.get("completion_tokens", 0) for e in llm),
350
+ "runs": len(runs),
351
+ "runs_succeeded": sum(1 for e in runs if e.get("status") == "succeeded"),
352
+ "runs_failed": sum(1 for e in runs if e.get("status") == "failed"),
353
+ "artifacts": len(artifacts),
354
+ "artifact_bytes": sum(e.get("bytes", 0) for e in artifacts),
355
+ "events": len(events),
356
+ }
357
+
358
+ async def get_llm_stats(
359
+ self,
360
+ *,
361
+ user_id: str | None = None,
362
+ org_id: str | None = None,
363
+ window: str = "24h",
364
+ run_ids: set[str] | None = None,
365
+ ) -> dict[str, dict[str, int]]:
366
+ rows = await self._query(
367
+ window=window,
368
+ kinds=["meter.llm"],
369
+ user_id=user_id,
370
+ org_id=org_id,
371
+ run_ids=run_ids,
372
+ )
373
+ stats: dict[str, dict[str, int]] = {}
374
+ for e in rows:
375
+ model = e.get("model", "unknown")
376
+ s = stats.setdefault(model, {"calls": 0, "prompt_tokens": 0, "completion_tokens": 0})
377
+ s["calls"] += 1
378
+ s["prompt_tokens"] += int(e.get("prompt_tokens", 0))
379
+ s["completion_tokens"] += int(e.get("completion_tokens", 0))
380
+ return stats
381
+
382
+ async def get_graph_stats(
383
+ self,
384
+ *,
385
+ user_id: str | None = None,
386
+ org_id: str | None = None,
387
+ window: str = "24h",
388
+ run_ids: set[str] | None = None,
389
+ ) -> dict[str, dict[str, int]]:
390
+ rows = await self._query(
391
+ window=window,
392
+ kinds=["meter.run"],
393
+ user_id=user_id,
394
+ org_id=org_id,
395
+ run_ids=run_ids,
396
+ )
397
+ stats: dict[str, dict[str, int]] = {}
398
+ for e in rows:
399
+ graph_id = e.get("graph_id") or "unknown"
400
+ s = stats.setdefault(
401
+ graph_id, {"runs": 0, "succeeded": 0, "failed": 0, "total_duration_s": 0}
402
+ )
403
+ s["runs"] += 1
404
+ if e.get("status") == "succeeded":
405
+ s["succeeded"] += 1
406
+ if e.get("status") == "failed":
407
+ s["failed"] += 1
408
+ s["total_duration_s"] += float(e.get("duration_s", 0.0))
409
+ return stats
410
+
411
+ async def get_artifact_stats(
412
+ self,
413
+ *,
414
+ user_id: str | None = None,
415
+ org_id: str | None = None,
416
+ window: str = "24h",
417
+ run_ids: set[str] | None = None,
418
+ ) -> dict[str, dict[str, int]]:
419
+ rows = await self._query(
420
+ window=window,
421
+ kinds=["meter.artifact"],
422
+ user_id=user_id,
423
+ org_id=org_id,
424
+ run_ids=run_ids,
425
+ )
426
+ stats: dict[str, dict[str, int]] = {}
427
+ for e in rows:
428
+ ak = e.get("artifact_kind") or "unknown"
429
+ s = stats.setdefault(
430
+ ak,
431
+ {"count": 0, "bytes": 0, "pinned_count": 0, "pinned_bytes": 0},
432
+ )
433
+ b = int(e.get("bytes") or 0)
434
+ pinned = bool(e.get("pinned") or False)
435
+
436
+ s["count"] += 1
437
+ s["bytes"] += b
438
+ if pinned:
439
+ s["pinned_count"] += 1
440
+ s["pinned_bytes"] += b
441
+ return stats
442
+
443
+ async def get_memory_stats(
444
+ self,
445
+ *,
446
+ scope_id: str | None = None,
447
+ user_id: str | None = None,
448
+ org_id: str | None = None,
449
+ window: str = "24h",
450
+ run_ids: set[str] | None = None,
451
+ ) -> dict[str, dict[str, int]]:
452
+ rows = await self._query(
453
+ window=window,
454
+ kinds=["meter.event"],
455
+ user_id=user_id,
456
+ org_id=org_id,
457
+ run_ids=run_ids,
458
+ )
459
+ stats: dict[str, dict[str, int]] = {}
460
+ for e in rows:
461
+ if scope_id is not None and e.get("scope_id") != scope_id:
462
+ continue
463
+
464
+ ek = e.get("event_kind", "")
465
+ if not ek.startswith("memory."):
466
+ continue
467
+
468
+ s = stats.setdefault(ek, {"count": 0})
469
+ s["count"] += 1
470
+ return stats
@@ -1,4 +1,25 @@
1
- # services/metering/noop.py
2
- class NoopMetering:
3
- async def incr(self, metric: str, value: float = 1.0, **tags):
4
- return None
1
+ from aethergraph.contracts.services.metering import MeteringService
2
+
3
+
4
+ class NoopMeteringService(MeteringService):
5
+ async def record_llm(self, **kwargs):
6
+ print("NoopMeteringService.record_llm called")
7
+
8
+ async def record_run(self, **kwargs): ...
9
+ async def record_artifact(self, **kwargs): ...
10
+ async def record_event(self, **kwargs): ...
11
+
12
+ async def get_overview(self, **kwargs) -> dict[str, int]:
13
+ return {}
14
+
15
+ async def get_llm_stats(self, **kwargs) -> dict[str, dict[str, int]]:
16
+ return {}
17
+
18
+ async def get_graph_stats(self, **kwargs) -> dict[str, dict[str, int]]:
19
+ return {}
20
+
21
+ async def get_artifact_stats(self, **kwargs) -> dict[str, dict[str, int]]:
22
+ return {}
23
+
24
+ async def get_memory_stats(self, **kwargs) -> dict[str, dict[str, int]]:
25
+ return {}
File without changes