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,323 @@
1
+ from datetime import datetime, timezone
2
+
3
+ from fastapi import APIRouter, Depends, HTTPException, Query, Request
4
+
5
+ from aethergraph.api.v1.deps import RequestIdentity, get_identity
6
+ from aethergraph.api.v1.pagination import decode_cursor, encode_cursor
7
+ from aethergraph.api.v1.runs import _extract_app_id_from_tags
8
+ from aethergraph.api.v1.schemas import (
9
+ RunSummary,
10
+ Session,
11
+ SessionChatEvent,
12
+ SessionCreateRequest,
13
+ SessionListResponse,
14
+ SessionRunsResponse,
15
+ SessionUpdateRequest,
16
+ )
17
+ from aethergraph.core.runtime.run_types import RunImportance, RunVisibility, SessionKind
18
+ from aethergraph.core.runtime.runtime_registry import current_registry
19
+ from aethergraph.core.runtime.runtime_services import current_services
20
+
21
+ router = APIRouter(tags=["sessions"])
22
+
23
+
24
+ @router.post("/sessions", response_model=Session)
25
+ async def create_session(
26
+ body: SessionCreateRequest,
27
+ identity: RequestIdentity = Depends(get_identity), # noqa: B008
28
+ ) -> Session:
29
+ """
30
+ Create a new session.
31
+ """
32
+ container = current_services()
33
+ ss = getattr(container, "session_store", None)
34
+ if ss is None:
35
+ raise HTTPException(status_code=500, detail="SessionStore not available")
36
+
37
+ sess = await ss.create(
38
+ kind=body.kind,
39
+ title=body.title,
40
+ external_ref=body.external_ref,
41
+ user_id=identity.user_id,
42
+ org_id=identity.org_id,
43
+ source="webui",
44
+ )
45
+
46
+ return sess
47
+
48
+
49
+ @router.get("/sessions", response_model=SessionListResponse)
50
+ async def list_sessions(
51
+ kind: SessionKind | None = Query(None, description="Filter sessions by kind"), # noqa: B008
52
+ limit: int = Query(50, ge=1, le=1000), # noqa: B008
53
+ cursor: str | None = Query(None), # noqa: B008
54
+ identity: RequestIdentity = Depends(get_identity), # noqa: B008
55
+ ) -> SessionListResponse:
56
+ """
57
+ List sessions for the current user/org, optionally filtered by kind.
58
+ """
59
+ container = current_services()
60
+ ss = getattr(container, "session_store", None)
61
+ if ss is None:
62
+ raise HTTPException(status_code=500, detail="SessionStore not available")
63
+
64
+ offset = decode_cursor(cursor)
65
+
66
+ # Enforce identity for cloud/demo
67
+ if identity.mode in ("cloud", "demo") and identity.user_id is None:
68
+ raise HTTPException(status_code=403, detail="User identity required")
69
+
70
+ sessions = await ss.list_for_user(
71
+ user_id=identity.user_id if identity.mode in ("cloud", "demo") else identity.user_id,
72
+ org_id=identity.org_id if identity.mode in ("cloud", "demo") else identity.org_id,
73
+ kind=kind,
74
+ limit=limit,
75
+ offset=offset,
76
+ )
77
+ # print(f"Listed {len(sessions)} sessions for user_id={identity.user_id} org_id={identity.org_id} offset={offset} limit={limit}")
78
+ # print(f"Sessions: {[s for s in sessions]}")
79
+ next_cursor = encode_cursor(offset + limit) if len(sessions) == limit else None
80
+ return SessionListResponse(items=sessions, next_cursor=next_cursor)
81
+
82
+
83
+ @router.get("/sessions/{session_id}", response_model=Session)
84
+ async def get_session(
85
+ session_id: str,
86
+ identity: RequestIdentity = Depends(get_identity), # noqa: B008
87
+ ) -> Session:
88
+ container = current_services()
89
+ ss = getattr(container, "session_store", None)
90
+ if ss is None:
91
+ raise HTTPException(status_code=500, detail="SessionStore not available")
92
+
93
+ sess = await ss.get(session_id)
94
+ if sess is None:
95
+ raise HTTPException(status_code=404, detail="Session not found")
96
+
97
+ # Optional: enforce that the session belongs to the user/org
98
+ if identity.mode != "local":
99
+ if identity.user_id and sess.user_id is not None and sess.user_id != identity.user_id:
100
+ raise HTTPException(status_code=403, detail="Access denied")
101
+
102
+ if identity.org_id and sess.org_id is not None and sess.org_id != identity.org_id:
103
+ raise HTTPException(status_code=403, detail="Access denied")
104
+ return sess
105
+
106
+
107
+ @router.get("/sessions/{session_id}/runs", response_model=SessionRunsResponse)
108
+ async def get_session_runs(
109
+ session_id: str,
110
+ include_inline: bool = Query(False), # noqa: B008
111
+ identity: RequestIdentity = Depends(get_identity), # noqa: B008
112
+ ) -> SessionRunsResponse:
113
+ container = current_services()
114
+ ss = getattr(container, "session_store", None)
115
+ rm = getattr(container, "run_manager", None)
116
+ if ss is None:
117
+ raise HTTPException(status_code=500, detail="SessionStore not available")
118
+ if rm is None:
119
+ raise HTTPException(status_code=500, detail="RunManager not available")
120
+
121
+ # Make sure the session exists and belongs to this user/org
122
+ sess = await ss.get(session_id)
123
+ if sess is None:
124
+ raise HTTPException(status_code=404, detail="Session not found")
125
+
126
+ if identity.mode != "local":
127
+ if identity.user_id and sess.user_id is not None and sess.user_id != identity.user_id:
128
+ raise HTTPException(status_code=403, detail="Access denied")
129
+ if identity.org_id and sess.org_id is not None and sess.org_id != identity.org_id:
130
+ raise HTTPException(status_code=403, detail="Access denied")
131
+
132
+ # For now, just scan recent runs and filter by session_id
133
+ # Later, we need a dedicated index/query in RunStore
134
+ records = await rm.list_records(
135
+ graph_id=None,
136
+ status=None,
137
+ session_id=session_id,
138
+ flow_id=None,
139
+ limit=1000,
140
+ offset=0,
141
+ )
142
+
143
+ # 🔹 Visibility & importance policy for session views:
144
+ # - Always require importance == normal (ephemeral hidden for now).
145
+ # - If include_inline is False:
146
+ # include only visibility == normal
147
+ # Else:
148
+ # include visibility in {normal, inline}
149
+ visible_states = {RunVisibility.normal}
150
+ if include_inline:
151
+ visible_states.add(RunVisibility.inline)
152
+
153
+ records = [
154
+ rec
155
+ for rec in records
156
+ if rec.visibility in visible_states and rec.importance == RunImportance.normal
157
+ ]
158
+
159
+ reg = getattr(container, "registry", None) or current_registry()
160
+ summaries: list[RunSummary] = []
161
+
162
+ for rec in records:
163
+ # defaults to avoid UnboundLocalError if reg is None
164
+ flow_id: str | None = None
165
+ entrypoint = False
166
+
167
+ if reg is not None:
168
+ if rec.kind == "taskgraph":
169
+ meta = reg.get_meta(nspace="graph", name=rec.graph_id, version=None) or {}
170
+ elif rec.kind == "graphfn":
171
+ meta = reg.get_meta(nspace="graphfn", name=rec.graph_id, version=None) or {}
172
+ else:
173
+ meta = {}
174
+
175
+ flow_id = meta.get("flow_id")
176
+ entrypoint = bool(meta.get("entrypoint", False))
177
+
178
+ # derive app info
179
+ app_id = rec.meta.get("app_id") or _extract_app_id_from_tags(rec.tags)
180
+ app_name = rec.meta.get("app_name")
181
+
182
+ summaries.append(
183
+ RunSummary(
184
+ run_id=rec.run_id,
185
+ graph_id=rec.graph_id,
186
+ status=rec.status,
187
+ started_at=rec.started_at,
188
+ finished_at=rec.finished_at,
189
+ tags=rec.tags,
190
+ user_id=rec.user_id,
191
+ org_id=rec.org_id,
192
+ graph_kind=rec.kind,
193
+ flow_id=flow_id,
194
+ entrypoint=entrypoint,
195
+ meta=rec.meta or {},
196
+ app_id=app_id,
197
+ app_name=app_name,
198
+ session_id=rec.session_id,
199
+ origin=rec.origin,
200
+ visibility=rec.visibility,
201
+ importance=rec.importance,
202
+ agent_id=rec.agent_id,
203
+ )
204
+ )
205
+
206
+ return SessionRunsResponse(items=summaries)
207
+
208
+
209
+ @router.get("/sessions/{session_id}/chat/events", response_model=list[SessionChatEvent])
210
+ async def get_session_chat_events(
211
+ session_id: str,
212
+ request: Request,
213
+ since_ts: float | None = Query(None), # noqa: B008
214
+ identity: RequestIdentity = Depends(get_identity), # noqa: B008
215
+ ) -> list[SessionChatEvent]:
216
+ container = current_services()
217
+ event_log = container.eventlog
218
+
219
+ if event_log is None:
220
+ raise HTTPException(status_code=503, detail="EventLog not available")
221
+
222
+ since_dt: datetime | None = None
223
+ if since_ts is not None:
224
+ since_dt = datetime.fromtimestamp(since_ts, tz=timezone.utc)
225
+
226
+ events = await event_log.query(
227
+ scope_id=session_id,
228
+ since=since_dt,
229
+ kinds=["session_chat"],
230
+ limit=1000,
231
+ )
232
+
233
+ if since_ts is not None:
234
+ # make cursor exclusive -- only return events after since_ts to avoid duplicates
235
+ events = [ev for ev in events if (ev.get("ts") or 0) > since_ts]
236
+
237
+ out: list[SessionChatEvent] = []
238
+ for ev in events:
239
+ payload = ev.get("payload", {}) or {}
240
+ out.append(
241
+ SessionChatEvent(
242
+ id=ev.get("id"),
243
+ session_id=session_id,
244
+ ts=ev.get("ts"),
245
+ type=payload.get("type") or "agent.message",
246
+ text=payload.get("text"),
247
+ buttons=payload.get("buttons", []),
248
+ file=payload.get("file"), # may be None
249
+ files=payload.get("files") or None, # forward list
250
+ meta=payload.get("meta", {}) or {},
251
+ agent_id=payload.get("agent_id"),
252
+ upsert_key=payload.get("upsert_key"), # forward idempotent key
253
+ )
254
+ )
255
+ out.sort(key=lambda e: e.ts)
256
+
257
+ return out
258
+
259
+
260
+ @router.patch("/sessions/{session_id}", response_model=Session)
261
+ async def update_session(
262
+ session_id: str,
263
+ body: SessionUpdateRequest,
264
+ identity: RequestIdentity = Depends(get_identity), # noqa: B008
265
+ ) -> Session:
266
+ container = current_services()
267
+ ss = getattr(container, "session_store", None)
268
+ if ss is None:
269
+ raise HTTPException(status_code=500, detail="SessionStore not available")
270
+
271
+ existing = await ss.get(session_id)
272
+ if existing is None:
273
+ raise HTTPException(status_code=404, detail="Session not found")
274
+
275
+ # Enforce ownership for non-local modes
276
+ if identity.mode != "local":
277
+ if (
278
+ identity.user_id
279
+ and existing.user_id is not None
280
+ and existing.user_id != identity.user_id
281
+ ):
282
+ raise HTTPException(status_code=403, detail="Access denied")
283
+ if identity.org_id and existing.org_id is not None and existing.org_id != identity.org_id:
284
+ raise HTTPException(status_code=403, detail="Access denied")
285
+
286
+ updated = await ss.update(
287
+ session_id,
288
+ title=body.title,
289
+ external_ref=body.external_ref,
290
+ )
291
+ if updated is None:
292
+ # Defensive; shouldn't happen given we already fetched it
293
+ raise HTTPException(status_code=404, detail="Session not found")
294
+
295
+ return updated
296
+
297
+
298
+ @router.delete("/sessions/{session_id}", status_code=204)
299
+ async def delete_session(
300
+ session_id: str,
301
+ identity: RequestIdentity = Depends(get_identity), # noqa: B008
302
+ ) -> None:
303
+ container = current_services()
304
+ ss = getattr(container, "session_store", None)
305
+ if ss is None:
306
+ raise HTTPException(status_code=500, detail="SessionStore not available")
307
+
308
+ existing = await ss.get(session_id)
309
+ if existing is None:
310
+ # 204 for idempotent delete
311
+ return
312
+
313
+ if identity.mode != "local":
314
+ if (
315
+ identity.user_id
316
+ and existing.user_id is not None
317
+ and existing.user_id != identity.user_id
318
+ ):
319
+ raise HTTPException(status_code=403, detail="Access denied")
320
+ if identity.org_id and existing.org_id is not None and existing.org_id != identity.org_id:
321
+ raise HTTPException(status_code=403, detail="Access denied")
322
+
323
+ await ss.delete(session_id)
@@ -0,0 +1,201 @@
1
+ from collections.abc import Iterable
2
+ from typing import Annotated, Any
3
+
4
+ from fastapi import APIRouter, Depends, HTTPException, Query
5
+
6
+ from aethergraph.core.runtime.runtime_services import current_services
7
+
8
+ from .deps import RequestIdentity, get_identity
9
+ from .schemas import (
10
+ ArtifactStats,
11
+ GraphStats,
12
+ GraphStatsEntry,
13
+ LLMStats,
14
+ MemoryStats,
15
+ StatsOverview,
16
+ )
17
+
18
+ router = APIRouter(tags=["stats"])
19
+
20
+
21
+ # This is demo-only; real multi-tenant setups should rely on user_id/org_id instead.
22
+ def _has_client_tag(tags: Iterable[str] | None, client_id: str | None) -> bool:
23
+ if not client_id:
24
+ return True
25
+ if not tags:
26
+ return False
27
+ needle = f"client:{client_id}"
28
+ return any(t == needle for t in tags)
29
+
30
+
31
+ async def _get_run_ids_for_client(
32
+ client_id: str | None,
33
+ limit: int = 500,
34
+ ) -> set[str]:
35
+ """
36
+ TEMP: demo-only helper.
37
+ Look up recent runs and filter by client:<id> tag.
38
+ """
39
+ if not client_id:
40
+ return set()
41
+
42
+ container = current_services()
43
+ rm = getattr(container, "run_manager", None)
44
+ if rm is None:
45
+ return set()
46
+
47
+ records = await rm.list_records(
48
+ graph_id=None,
49
+ status=None,
50
+ flow_id=None,
51
+ limit=limit,
52
+ )
53
+
54
+ return {r.run_id for r in records if _has_client_tag(r.tags, client_id)}
55
+
56
+
57
+ @router.get("/stats/overview", response_model=StatsOverview)
58
+ async def get_stats_overview(
59
+ window: Annotated[str, Query(description="Time window for stats, e.g., '24h', '7d'")] = "24h",
60
+ identity: RequestIdentity = Depends(get_identity), # noqa: B008
61
+ ) -> StatsOverview:
62
+ """
63
+ Get an overview of usage statistics.
64
+
65
+ - **window**: Time window for stats (e.g., "24h", "7d").
66
+ """
67
+ container = current_services()
68
+ meter = getattr(container, "metering", None)
69
+ if meter is None:
70
+ raise HTTPException(status_code=501, detail="Metering service not available")
71
+
72
+ raw: dict[str, int] = await meter.get_overview(
73
+ user_id=identity.user_id if identity and identity.user_id else None,
74
+ org_id=identity.org_id if identity and identity.org_id else None,
75
+ window=window,
76
+ )
77
+ return StatsOverview(**raw)
78
+
79
+
80
+ @router.get("/stats/graphs", response_model=GraphStats)
81
+ async def get_graphs_stats(
82
+ window: Annotated[str, Query(description="Time window for stats, e.g., '24h', '7d'")] = "24h",
83
+ graph_id: Annotated[str | None, Query(description="Optional graph_id filter")] = None,
84
+ identity: RequestIdentity = Depends(get_identity), # noqa: B008
85
+ ) -> GraphStats:
86
+ """
87
+ Get usage statistics for graphs.
88
+
89
+ - **window**: Time window for stats (e.g., "24h", "7d").
90
+ - **graph_id**: Optional filter; if provided, only stats for that graph are returned.
91
+ """
92
+ container = current_services()
93
+ meter = getattr(container, "metering", None)
94
+ if meter is None:
95
+ raise HTTPException(status_code=501, detail="Metering service not available")
96
+
97
+ raw_all: dict[str, dict[str, Any]] = await meter.get_graph_stats(
98
+ user_id=identity.user_id if identity and identity.user_id else None,
99
+ org_id=identity.org_id if identity and identity.org_id else None,
100
+ window=window,
101
+ )
102
+ # raw_all: { "<graph_id>": {"runs":..., "succeeded":..., "failed":..., "total_duration_s":...}, ... }
103
+
104
+ if graph_id is not None:
105
+ # Return only the requested graph, but still in map form
106
+ entry = raw_all.get(graph_id, {})
107
+ filtered: dict[str, dict[str, Any]] = {
108
+ graph_id: {
109
+ "runs": int(entry.get("runs", 0)),
110
+ "succeeded": int(entry.get("succeeded", 0)),
111
+ "failed": int(entry.get("failed", 0)),
112
+ "total_duration_s": float(entry.get("total_duration_s", 0.0)),
113
+ }
114
+ }
115
+ return GraphStats(root={gid: GraphStatsEntry(**vals) for gid, vals in filtered.items()})
116
+
117
+ # Normalize all entries to GraphStatsEntry
118
+ normalized: dict[str, GraphStatsEntry] = {}
119
+ for gid, vals in raw_all.items():
120
+ normalized[gid] = GraphStatsEntry(
121
+ runs=int(vals.get("runs", 0)),
122
+ succeeded=int(vals.get("succeeded", 0)),
123
+ failed=int(vals.get("failed", 0)),
124
+ total_duration_s=float(vals.get("total_duration_s", 0.0)),
125
+ )
126
+
127
+ return GraphStats(root=normalized)
128
+
129
+
130
+ @router.get("/stats/memory", response_model=MemoryStats)
131
+ async def get_memory_stats(
132
+ scope_id: Annotated[str | None, Query(description="Logical memory scope (optional)")] = None,
133
+ window: Annotated[str, Query(description="Time window, e.g., '24h', '7d'")] = "24h",
134
+ identity: RequestIdentity = Depends(get_identity), # noqa: B008
135
+ ) -> MemoryStats:
136
+ """
137
+ Get memory usage statistics.
138
+
139
+ - **scope_id**: Logical memory scope (optional).
140
+ - **window**: Time window for stats (e.g., "24h", "7d").
141
+ """
142
+ container = current_services()
143
+ meter = getattr(container, "metering", None)
144
+
145
+ if meter is None:
146
+ raise HTTPException(status_code=501, detail="Metering service not available")
147
+
148
+ raw: dict[str, dict[str, int]] = await meter.get_memory_stats(
149
+ scope_id=scope_id,
150
+ user_id=identity.user_id if identity and identity.user_id else None,
151
+ org_id=identity.org_id if identity and identity.org_id else None,
152
+ window=window,
153
+ )
154
+ # raw: { "memory.user_msg": {"count": N}, ... }
155
+ return MemoryStats(root=raw)
156
+
157
+
158
+ @router.get("/stats/artifacts", response_model=ArtifactStats)
159
+ async def get_artifacts_stats(
160
+ window: Annotated[str, Query(description="Time window, e.g., '24h', '7d'")] = "24h",
161
+ identity: RequestIdentity = Depends(get_identity), # noqa: B008
162
+ ) -> ArtifactStats:
163
+ """
164
+ Aggregate artifact stats for this user/org: counts, bytes, pinned, etc.
165
+ Backed by MeteringService.get_artifact_stats().
166
+ """
167
+ container = current_services()
168
+ meter = getattr(container, "metering", None)
169
+ if meter is None:
170
+ raise HTTPException(status_code=501, detail="Metering service not available")
171
+
172
+ raw: dict[str, dict[str, int]] = await meter.get_artifact_stats(
173
+ user_id=identity.user_id if identity and identity.user_id else None,
174
+ org_id=identity.org_id if identity and identity.org_id else None,
175
+ window=window,
176
+ )
177
+ # raw: { "json": {"count":..., "bytes":..., "pinned_count":..., "pinned_bytes":...}, ... }
178
+ return ArtifactStats(root=raw)
179
+
180
+
181
+ @router.get("/stats/llm", response_model=LLMStats)
182
+ async def get_stats_llm(
183
+ window: Annotated[str, Query(description="Time window, e.g., '24h', '7d'")] = "24h",
184
+ identity: RequestIdentity = Depends(get_identity), # noqa: B008
185
+ ) -> LLMStats:
186
+ """
187
+ LLM usage stats: tokens, requests, breakdown by provider/model.
188
+ Backed by MeteringService.get_llm_stats().
189
+ """
190
+ container = current_services()
191
+ meter = getattr(container, "metering", None)
192
+ if meter is None:
193
+ raise HTTPException(status_code=501, detail="Metering service not available")
194
+
195
+ raw: dict[str, dict[str, int]] = await meter.get_llm_stats(
196
+ user_id=identity.user_id if identity and identity.user_id else None,
197
+ org_id=identity.org_id if identity and identity.org_id else None,
198
+ window=window,
199
+ )
200
+ # raw: { "gpt-4o-mini": {"calls":..., "prompt_tokens":..., "completion_tokens":...}, ... }
201
+ return LLMStats(root=raw)
@@ -0,0 +1,152 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import datetime
4
+ from typing import Annotated, Any
5
+
6
+ from fastapi import APIRouter, Depends, HTTPException, Query
7
+
8
+ from aethergraph.api.v1.deps import RequestIdentity, get_identity
9
+ from aethergraph.api.v1.schemas import (
10
+ RunVizResponse,
11
+ VizFigure,
12
+ VizKind,
13
+ VizPoint,
14
+ VizTrack,
15
+ )
16
+ from aethergraph.core.runtime.runtime_services import current_services
17
+
18
+ router = APIRouter(tags=["viz"])
19
+
20
+
21
+ @router.get("/runs/{run_id}/viz", response_model=RunVizResponse)
22
+ async def get_run_viz(
23
+ run_id: str,
24
+ viz_kinds: Annotated[
25
+ str | None,
26
+ Query(
27
+ description=(
28
+ "Comma-separated list of viz kinds to include. "
29
+ "Options: scalar,vector,matrix,image. "
30
+ "If omitted, all viz kinds are returned."
31
+ )
32
+ ),
33
+ ] = None,
34
+ identity: RequestIdentity = Depends(get_identity), # noqa: B008
35
+ ) -> RunVizResponse:
36
+ """
37
+ Aggregate visualization data for a run into figures/tracks for the Vis tab.
38
+
39
+ - Uses the EventLog-backed VizService.
40
+ - Enforces demo scoping via RunManager (client_id).
41
+ - Returns structured data (scalars, vectors, matrices, image references),
42
+ not pre-rendered plots.
43
+ """
44
+ container = current_services()
45
+
46
+ viz_service = getattr(container, "viz_service", None)
47
+ rm = getattr(container, "run_manager", None)
48
+ if viz_service is None:
49
+ raise HTTPException(status_code=500, detail="VizService not available")
50
+
51
+ # Demo mode: require RunManager to verify access
52
+ if identity.mode == "demo" and rm is None:
53
+ raise HTTPException(status_code=500, detail="RunManager not available")
54
+
55
+ # Parse viz kinds filter [optional]
56
+ kinds_filter: list[VizKind] | None = None
57
+ if viz_kinds:
58
+ raw = [k.strip().lower() for k in viz_kinds.split(",") if k.strip()]
59
+ allowed: set[str] = {"scalar", "vector", "matrix", "image"}
60
+ bad = [k for k in raw if k not in allowed]
61
+ if bad:
62
+ raise HTTPException(
63
+ status_code=400,
64
+ detail=f"Invalid viz kinds: {', '.join(bad)}; allowed: {', '.join(sorted(allowed))}",
65
+ )
66
+ kinds_filter = raw # type: ignore[assignment]
67
+
68
+ # Query raw viz events for this run from VizService
69
+ rows = await viz_service.query_run(run_id, kinds=kinds_filter)
70
+
71
+ # Group into figures/tracks/points
72
+ # Key: (figure_id, track_id, viz_kind, node_id)
73
+ track_map: dict[tuple[str | None, str, str, str | None], dict[str, Any]] = {}
74
+ for row in rows:
75
+ data: dict[str, Any] = row.get("data") or {}
76
+ viz_kind: str = data.get("viz_kind")
77
+ track_id: str = data.get("track_id")
78
+ figure_id: str | None = data.get("figure_id")
79
+ node_id: str | None = data.get("node_id")
80
+ step = data.get("step")
81
+
82
+ if track_id is None or viz_kind is None or step is None:
83
+ # skip malformed events
84
+ continue
85
+
86
+ key = (figure_id, track_id, viz_kind, node_id)
87
+ agg = track_map.get(key)
88
+ if agg is None:
89
+ agg = {
90
+ "figure_id": figure_id,
91
+ "track_id": track_id,
92
+ "viz_kind": viz_kind,
93
+ "node_id": node_id,
94
+ "mode": data.get("mode", "append"),
95
+ "meta": data.get("meta") or {},
96
+ "points": [],
97
+ }
98
+ track_map[key] = agg
99
+
100
+ # Mode/meta: keep the first one, but allow later events to update if you want
101
+ # For now we just keep existing 'mode' and 'meta' if already set.
102
+
103
+ # Build point
104
+ ts_str: str | None = row.get("ts") or data.get("created_at")
105
+ created_at: datetime | None = None
106
+ if ts_str:
107
+ try:
108
+ created_at = datetime.fromisoformat(ts_str)
109
+ except Exception:
110
+ created_at = None
111
+
112
+ point = VizPoint(
113
+ step=int(step),
114
+ value=data.get("value"),
115
+ vector=data.get("vector"),
116
+ matrix=data.get("matrix"),
117
+ artifact_id=data.get("artifact_id"),
118
+ created_at=created_at,
119
+ )
120
+ agg["points"].append(point)
121
+
122
+ # Build figures from grouping
123
+ figures_map: dict[str | None, list[VizTrack]] = {}
124
+
125
+ for (fig_id, track_id, _, node_id), agg in track_map.items():
126
+ points: list[VizPoint] = agg["points"]
127
+ # Sort points by step (and then by created_at as tiebreaker)
128
+ points.sort(key=lambda p: (p.step, p.created_at or datetime.min))
129
+
130
+ track = VizTrack(
131
+ track_id=track_id,
132
+ figure_id=fig_id,
133
+ node_id=node_id,
134
+ viz_kind=agg["viz_kind"],
135
+ mode=agg["mode"],
136
+ meta=agg["meta"],
137
+ points=points,
138
+ )
139
+
140
+ lst = figures_map.setdefault(fig_id, [])
141
+ lst.append(track)
142
+
143
+ # Sort tracks within each figure by track_id for stability
144
+ figures: list[VizFigure] = []
145
+ for fig_id, tracks in figures_map.items():
146
+ tracks.sort(key=lambda t: t.track_id)
147
+ figures.append(VizFigure(figure_id=fig_id, tracks=tracks))
148
+
149
+ # Sort figures: put named figures first, then the Node/default one
150
+ figures.sort(key=lambda f: (f.figure_id is None, f.figure_id or ""))
151
+
152
+ return RunVizResponse(run_id=run_id, figures=figures)