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,259 @@
1
+ # /graphs
2
+
3
+
4
+ from typing import Annotated, Any
5
+
6
+ from fastapi import APIRouter, Depends, HTTPException, Query
7
+
8
+ from aethergraph.core.graph.graph_fn import GraphFunction
9
+ from aethergraph.core.graph.task_graph import TaskGraph
10
+ from aethergraph.core.runtime.runtime_registry import current_registry
11
+ from aethergraph.services.registry.unified_registry import UnifiedRegistry
12
+
13
+ from .deps import RequestIdentity, get_identity
14
+ from .schemas import GraphDetail, GraphListItem
15
+
16
+ router = APIRouter(tags=["graphs"])
17
+
18
+
19
+ GRAPH_NS = "graph"
20
+ GRAPHFN_NS = "graphfn"
21
+
22
+
23
+ def _is_task_graph(obj: Any) -> bool:
24
+ if isinstance(obj, TaskGraph):
25
+ return True
26
+ # Fallback check -- used in tests
27
+ return hasattr(obj, "spec")
28
+
29
+
30
+ def _is_graph_function(obj: Any) -> bool:
31
+ if isinstance(obj, GraphFunction):
32
+ return True
33
+ # Fallback check -- used in tests
34
+ return hasattr(obj, "fn") and hasattr(obj, "name")
35
+
36
+
37
+ @router.get("/graphs", response_model=list[GraphListItem])
38
+ async def list_graphs(
39
+ flow_id: Annotated[str | None, Query()] = None,
40
+ identity: RequestIdentity = Depends(get_identity), # noqa: B008
41
+ ) -> list[GraphListItem]:
42
+ """
43
+ List available graphs (TaskGraphs and GraphFunctions).
44
+
45
+ Optional:
46
+ - flow_id: filter to graphs whose registry metadata has this flow_id.
47
+ """
48
+ reg: UnifiedRegistry = current_registry()
49
+
50
+ items: list[GraphListItem] = []
51
+
52
+ # ---- 1) Static TaskGraphs (ns="graph") ----
53
+ latest_graphs = reg.list(nspace=GRAPH_NS)
54
+ for key, version in latest_graphs.items():
55
+ ns, name = key.split(":", 1)
56
+ if ns != GRAPH_NS:
57
+ continue
58
+
59
+ graph_obj = reg.get_graph(name=name, version=version)
60
+ spec = getattr(graph_obj, "spec", None)
61
+
62
+ meta = reg.get_meta(nspace=GRAPH_NS, name=name, version=version) or {}
63
+ meta_flow_id: str | None = meta.get("flow_id")
64
+ meta_entrypoint: bool = bool(meta.get("entrypoint", False))
65
+ meta_tags = list(meta.get("tags", []))
66
+
67
+ # flow filter
68
+ if flow_id is not None and meta_flow_id != flow_id:
69
+ continue
70
+
71
+ if spec is None:
72
+ items.append(
73
+ GraphListItem(
74
+ graph_id=name,
75
+ name=name,
76
+ description=None,
77
+ inputs=[],
78
+ outputs=[],
79
+ tags=meta_tags or ["graph"],
80
+ kind="graph",
81
+ flow_id=meta_flow_id,
82
+ entrypoint=meta_entrypoint,
83
+ )
84
+ )
85
+ continue
86
+
87
+ inputs = list(spec.io.required.keys()) + list(spec.io.optional.keys())
88
+ outputs = list(spec.io.outputs.keys())
89
+
90
+ desc = spec.meta.get("description") if hasattr(spec, "meta") else None
91
+ spec_tags = list(spec.meta.get("tags", [])) if hasattr(spec, "meta") else []
92
+
93
+ tags = meta_tags or spec_tags or ["graph"]
94
+
95
+ items.append(
96
+ GraphListItem(
97
+ graph_id=name,
98
+ name=name,
99
+ description=desc,
100
+ inputs=inputs,
101
+ outputs=outputs,
102
+ tags=tags,
103
+ kind="graph",
104
+ flow_id=meta_flow_id,
105
+ entrypoint=meta_entrypoint,
106
+ )
107
+ )
108
+
109
+ # ---- 2) Imperative GraphFunctions (ns="graphfn") ----
110
+ latest_graphfns = reg.list(nspace=GRAPHFN_NS)
111
+ for key, version in latest_graphfns.items():
112
+ ns, name = key.split(":", 1)
113
+ if ns != GRAPHFN_NS:
114
+ continue
115
+
116
+ gf = reg.get_graphfn(name=name, version=version)
117
+ if not _is_graph_function(gf):
118
+ continue
119
+
120
+ meta = reg.get_meta(nspace=GRAPHFN_NS, name=name, version=version) or {}
121
+ meta_flow_id: str | None = meta.get("flow_id")
122
+ meta_entrypoint: bool = bool(meta.get("entrypoint", False))
123
+ meta_tags = list(meta.get("tags", []))
124
+
125
+ if flow_id is not None and meta_flow_id != flow_id:
126
+ continue
127
+
128
+ inputs = list(getattr(gf, "inputs", []) or [])
129
+ outputs = list(getattr(gf, "outputs", []) or [])
130
+ desc = getattr(gf, "description", None)
131
+
132
+ items.append(
133
+ GraphListItem(
134
+ graph_id=name,
135
+ name=name,
136
+ description=desc,
137
+ inputs=inputs,
138
+ outputs=outputs,
139
+ tags=meta_tags or ["graphfn"],
140
+ kind="graphfn",
141
+ flow_id=meta_flow_id,
142
+ entrypoint=meta_entrypoint,
143
+ )
144
+ )
145
+
146
+ return items
147
+
148
+
149
+ @router.get("/graphs/{graph_id}", response_model=GraphDetail)
150
+ async def get_graph_detail(
151
+ graph_id: str,
152
+ identity: RequestIdentity = Depends(get_identity), # noqa: B008
153
+ ) -> GraphDetail:
154
+ """
155
+ Get detailed information about a specific graph (structure only).
156
+ """
157
+ reg: UnifiedRegistry = current_registry()
158
+
159
+ # 1) Try TaskGraph
160
+ try:
161
+ graph_obj = reg.get_graph(name=graph_id, version=None)
162
+ spec = getattr(graph_obj, "spec", None)
163
+ meta = reg.get_meta(nspace=GRAPH_NS, name=graph_id, version=None) or {}
164
+
165
+ flow_id = meta.get("flow_id")
166
+ entrypoint = bool(meta.get("entrypoint", False))
167
+ meta_tags = list(meta.get("tags", []))
168
+
169
+ if spec is None:
170
+ return GraphDetail(
171
+ graph_id=graph_id,
172
+ name=graph_id,
173
+ description=None,
174
+ inputs=[],
175
+ outputs=[],
176
+ tags=meta_tags or ["graph"],
177
+ kind="graph",
178
+ flow_id=flow_id,
179
+ entrypoint=entrypoint,
180
+ nodes=[],
181
+ edges=[],
182
+ )
183
+
184
+ # ---- Nodes from TaskNodeSpec ----
185
+ nodes_list: list[dict[str, Any]] = []
186
+ for node_id, node_spec in spec.nodes.items():
187
+ node_info: dict[str, Any] = {
188
+ "id": node_id,
189
+ "type": str(getattr(node_spec, "type", "")),
190
+ "tool_name": getattr(node_spec, "tool_name", None),
191
+ "tool_version": getattr(node_spec, "tool_version", None),
192
+ "expected_inputs": list(getattr(node_spec, "expected_input_keys", []) or []),
193
+ "expected_outputs": list(getattr(node_spec, "expected_output_keys", []) or []),
194
+ "output_keys": list(getattr(node_spec, "output_keys", []) or []),
195
+ }
196
+ nodes_list.append(node_info)
197
+
198
+ # ---- Edges from dependencies ----
199
+ edge_set: set[tuple[str, str]] = set()
200
+ for node_id, node_spec in spec.nodes.items():
201
+ for dep_id in getattr(node_spec, "dependencies", []):
202
+ edge_set.add((str(dep_id), str(node_id)))
203
+
204
+ edges_list: list[dict[str, Any]] = [
205
+ {"source": src, "target": dst} for (src, dst) in sorted(edge_set)
206
+ ]
207
+
208
+ inputs = list(spec.io.required.keys()) + list(spec.io.optional.keys())
209
+ outputs = list(spec.io.outputs.keys())
210
+ desc = spec.meta.get("description") if hasattr(spec, "meta") else None
211
+ spec_tags = list(spec.meta.get("tags", [])) if hasattr(spec, "meta") else []
212
+
213
+ tags = meta_tags or spec_tags or ["graph"]
214
+
215
+ return GraphDetail(
216
+ graph_id=graph_id,
217
+ name=graph_id,
218
+ description=desc,
219
+ inputs=inputs,
220
+ outputs=outputs,
221
+ tags=tags,
222
+ kind="graph",
223
+ flow_id=flow_id,
224
+ entrypoint=entrypoint,
225
+ nodes=nodes_list,
226
+ edges=edges_list,
227
+ )
228
+
229
+ except KeyError:
230
+ pass
231
+
232
+ # 2) Try GraphFunction
233
+ try:
234
+ gf = reg.get_graphfn(name=graph_id, version=None)
235
+ except KeyError as e:
236
+ raise HTTPException(status_code=404, detail="Graph not found") from e
237
+
238
+ meta = reg.get_meta(nspace=GRAPHFN_NS, name=graph_id, version=None) or {}
239
+ flow_id = meta.get("flow_id")
240
+ entrypoint = bool(meta.get("entrypoint", False))
241
+ meta_tags = list(meta.get("tags", []))
242
+
243
+ inputs = list(getattr(gf, "inputs", []) or [])
244
+ outputs = list(getattr(gf, "outputs", []) or [])
245
+ desc = getattr(gf, "description", None)
246
+
247
+ return GraphDetail(
248
+ graph_id=graph_id,
249
+ name=graph_id,
250
+ description=desc,
251
+ inputs=inputs,
252
+ outputs=outputs,
253
+ tags=meta_tags or ["graphfn"],
254
+ kind="graphfn",
255
+ flow_id=flow_id,
256
+ entrypoint=entrypoint,
257
+ nodes=[], # GraphFunction has no static node DAG
258
+ edges=[],
259
+ )
@@ -0,0 +1,25 @@
1
+ from fastapi import APIRouter, Depends
2
+ from pydantic import BaseModel
3
+
4
+ from aethergraph.api.v1.deps import RequestIdentity, get_identity
5
+
6
+ router = APIRouter()
7
+
8
+
9
+ class IdentityResponse(BaseModel):
10
+ mode: str
11
+ user_id: str | None
12
+ org_id: str | None
13
+ roles: list[str]
14
+ client_id: str | None
15
+
16
+
17
+ @router.get("/whoami", response_model=IdentityResponse)
18
+ def whoami(identity: RequestIdentity = Depends(get_identity)): # noqa: B008
19
+ return IdentityResponse(
20
+ mode=identity.mode,
21
+ user_id=identity.user_id,
22
+ org_id=identity.org_id,
23
+ roles=identity.roles,
24
+ client_id=identity.client_id,
25
+ )
@@ -0,0 +1,353 @@
1
+ # memory-related inspection
2
+
3
+ from contextlib import suppress
4
+ from datetime import datetime
5
+ from typing import Annotated, Any
6
+
7
+ from fastapi import APIRouter, Depends, Query
8
+
9
+ from aethergraph.api.v1.pagination import decode_cursor, encode_cursor
10
+ from aethergraph.contracts.services.memory import Event
11
+ from aethergraph.core.runtime.runtime_services import current_services
12
+
13
+ from .deps import RequestIdentity, get_identity
14
+ from .schemas import (
15
+ MemoryEvent,
16
+ MemoryEventListResponse,
17
+ MemorySearchHit,
18
+ MemorySearchRequest,
19
+ MemorySearchResponse,
20
+ MemorySummaryEntry,
21
+ MemorySummaryListResponse,
22
+ )
23
+
24
+ # NOTE: since hotlog is bounded in memory, it is fine to filter and rank in-memory for now.
25
+ # In future, if we need to process large volumes of memory data, we should look into changing the
26
+ # backend memory storage to support indexed queries (not changing the API contracts).
27
+
28
+ router = APIRouter(tags=["memory"])
29
+
30
+
31
+ # ------------ helpers / stubs ------------ #
32
+ def _parse_ts(ts: str) -> datetime:
33
+ """Parse ISO8601 timestamp string to datetime."""
34
+ if ts.endswith("Z"):
35
+ ts = ts[:-1] + "+00:00"
36
+ return datetime.fromisoformat(ts)
37
+
38
+
39
+ def _event_to_api_event(evt: Event) -> MemoryEvent:
40
+ created_at = _parse_ts(evt.ts)
41
+
42
+ data: dict[str, Any] | None = None
43
+ if evt.data is not None:
44
+ data = evt.data
45
+ elif evt.text:
46
+ data = {"text": evt.text}
47
+
48
+ # Fallback: if old events had no scope_id, use run_id so UI still works.
49
+ scope = evt.scope_id or evt.run_id
50
+
51
+ return MemoryEvent(
52
+ event_id=evt.event_id,
53
+ scope_id=scope,
54
+ kind=evt.kind,
55
+ tags=evt.tags or [],
56
+ created_at=created_at,
57
+ data=data or {},
58
+ )
59
+
60
+
61
+ def _doc_to_summary_entry(doc_id: str, doc: dict[str, Any]) -> MemorySummaryEntry:
62
+ ts_str = doc.get("ts") or doc.get("created_at") or ""
63
+ created_at = _parse_ts(ts_str) if ts_str else datetime.utcnow()
64
+
65
+ tw = doc.get("time_window") or {} # expected to have 'from' and 'to'
66
+ from_str = tw.get("from") or tw.get("start") or ""
67
+ to_str = tw.get("to") or tw.get("end") or ""
68
+ time_from = _parse_ts(from_str) if from_str else created_at
69
+ time_to = _parse_ts(to_str) if to_str else created_at
70
+
71
+ # prefer 'summary' field, fallback to text if present
72
+ text = doc.get("summary") or doc.get("text") or ""
73
+
74
+ # Strip out the core fields from metadata
75
+ meta_keys = {
76
+ "summary",
77
+ "text",
78
+ "scope_id",
79
+ "run_id",
80
+ "summary_tag",
81
+ "ts",
82
+ "time_window",
83
+ }
84
+ metadata = {k: v for k, v in doc.items() if k not in meta_keys}
85
+
86
+ return MemorySummaryEntry(
87
+ summary_id=doc_id,
88
+ scope_id=doc.get("scope_id") or doc.get("run_id") or "",
89
+ summary_tag=doc.get("summary_tag"),
90
+ created_at=created_at,
91
+ time_from=time_from,
92
+ time_to=time_to,
93
+ text=text,
94
+ metadata=metadata,
95
+ )
96
+
97
+
98
+ def _string_score(haystack: str, needle: str) -> float:
99
+ """
100
+ Very simple scoring: 0.0 if no match, 1.0 if case-insensitive substring match.
101
+ Placeholder until a real semantic index is wired in.
102
+ """
103
+ if not needle:
104
+ return 0.0
105
+ h = haystack.lower()
106
+ n = needle.lower()
107
+ return 1.0 if n in h else 0.0
108
+
109
+
110
+ # ------------ API endpoints ------------ #
111
+ @router.get("/memory/events", response_model=MemoryEventListResponse)
112
+ async def list_memory_events(
113
+ scope_id: str,
114
+ kinds: Annotated[
115
+ str | None, Query(description="Comma-separated list of kinds to filter")
116
+ ] = None, # noqa: B008
117
+ tags: Annotated[str | None, Query(description="Comma-separated list of tags to filter")] = None, # noqa: B008
118
+ after: Annotated[datetime | None, Query()] = None, # noqa: B008
119
+ before: Annotated[datetime | None, Query()] = None, # noqa: B008
120
+ cursor: Annotated[str | None, Query()] = None, # noqa: B008
121
+ limit: Annotated[int, Query(ge=1, le=200)] = 50, # noqa: B008
122
+ identity: RequestIdentity = Depends(get_identity), # noqa: B008
123
+ ) -> MemoryEventListResponse:
124
+ """
125
+ List raw memory events for a scope.
126
+
127
+ Currently:
128
+ - Treats `scope_id` as the underlying `run_id` used by HotLog/Persistence.
129
+ - Reads from HotLog only (recent in-memory events).
130
+ - Applies basic filters by kind, tag, and time.
131
+
132
+ TODO:
133
+ - Integrate with a long-term event store (Persistence queries).
134
+ - Implement cursor-based pagination.
135
+ - Optionally map scope_id → multiple runs.
136
+ - Filter by identity.user_id / org_id when multi-tenant.
137
+
138
+ NOTE:
139
+ - Currently reads from HotLog only (recent in-memory events),
140
+ NOT the long-term persistence/event log.
141
+ - Fetches up to hot_limit+10 and applies filters + cursor (offset) in Python.
142
+ - Pagination is therefore limited to the hot buffer; older events are not visible.
143
+ In the future, we may want to:
144
+ - Integrate with EventLog-based persistence for full history,
145
+ - Move filtering + pagination closer to the store layer.
146
+ """
147
+ container = current_services()
148
+ mem_factory = getattr(container, "memory_factory", None)
149
+ if mem_factory is None:
150
+ # No memory configured
151
+ return MemoryEventListResponse(events=[], next_cursor=None)
152
+
153
+ hotlog = mem_factory.hotlog
154
+
155
+ # Parse filters
156
+ kinds_list: list[str] | None = None
157
+ if kinds:
158
+ kinds_list = [k.strip() for k in kinds.split(",") if k.strip()]
159
+ tags_list: list[str] | None = None
160
+ if tags:
161
+ tags_list = [t.strip() for t in tags.split(",") if t.strip()]
162
+
163
+ # Fetch slightly more than limit to determine if there's a next page and
164
+ # we can filter in python
165
+ raw_events: list[Event] = await hotlog.recent(
166
+ scope_id,
167
+ kinds=kinds_list,
168
+ limit=mem_factory.hot_limit + 10,
169
+ )
170
+
171
+ filtered: list[Event] = []
172
+ for evt in raw_events:
173
+ dt = _parse_ts(evt.ts)
174
+ if after and dt <= after:
175
+ continue
176
+ if before and dt >= before:
177
+ continue
178
+ if tags_list:
179
+ evt_tags = evt.tags or []
180
+ if not any(t in evt_tags for t in tags_list):
181
+ continue
182
+ filtered.append(evt)
183
+
184
+ # Apply offset and limit
185
+ offset = decode_cursor(cursor)
186
+ page = filtered[offset : offset + limit]
187
+ api_events = [_event_to_api_event(e) for e in page]
188
+
189
+ next_cursor = encode_cursor(offset + limit) if len(filtered) > offset + limit else None
190
+ return MemoryEventListResponse(events=api_events, next_cursor=next_cursor)
191
+
192
+
193
+ @router.get("/memory/summaries", response_model=MemorySummaryListResponse)
194
+ async def list_memory_summaries(
195
+ scope_id: Annotated[str, Query()],
196
+ summary_tag: Annotated[str | None, Query()] = None,
197
+ cursor: Annotated[str | None, Query()] = None,
198
+ limit: Annotated[int, Query(ge=1, le=200)] = 50,
199
+ identity: RequestIdentity = Depends(get_identity), # noqa: B008
200
+ ) -> MemorySummaryListResponse:
201
+ """
202
+ List long-term memory summaries for a scope.
203
+
204
+ Currently:
205
+ - Scans the DocStore (memory_factory.docs) and filters docs where:
206
+ doc["scope_id"] == scope_id
207
+ and (summary_tag is None or doc["summary_tag"] == summary_tag)
208
+ - Converts each summary doc into MemorySummaryEntry.
209
+
210
+ TODO:
211
+ - Avoid full scan for large DocStores (add indexed queries).
212
+ - Implement cursor-based pagination.
213
+ - Optionally filter by identity.user_id / org_id.
214
+ """
215
+ container = current_services()
216
+ mem_factory = getattr(container, "memory_factory", None)
217
+ if mem_factory is None:
218
+ return MemorySummaryListResponse(summaries=[], next_cursor=None)
219
+
220
+ docs = mem_factory.docs
221
+
222
+ # DocStore.list() returns a list of doc_ids; we load and filter them
223
+ try:
224
+ doc_ids = await docs.list()
225
+ except TypeError:
226
+ # If the concrete DocStore doesn't support list(), return empty
227
+ return MemorySummaryListResponse(summaries=[], next_cursor=None)
228
+
229
+ entries: list[MemorySummaryEntry] = []
230
+
231
+ for doc_id in doc_ids:
232
+ doc = await docs.get(doc_id)
233
+ if not doc:
234
+ continue
235
+
236
+ if doc.get("scope_id") != scope_id:
237
+ continue
238
+
239
+ if summary_tag is not None and doc.get("summary_tag") != summary_tag:
240
+ continue
241
+
242
+ entries.append(_doc_to_summary_entry(doc_id, doc))
243
+
244
+ # Sort by created_at descending
245
+ entries.sort(key=lambda e: e.created_at, reverse=True)
246
+
247
+ if len(entries) > limit:
248
+ entries = entries[:limit]
249
+ return MemorySummaryListResponse(summaries=entries, next_cursor=None)
250
+
251
+
252
+ @router.post("/memory/search", response_model=MemorySearchResponse)
253
+ async def search_memory(
254
+ req: MemorySearchRequest,
255
+ identity: RequestIdentity = Depends(get_identity), # noqa: B008
256
+ ) -> MemorySearchResponse:
257
+ """
258
+ Semantic/keyword memory search.
259
+
260
+ Current behavior:
261
+ - Uses a naive substring match over:
262
+ • recent HotLog events for the given scope_id (treated as run_id)
263
+ • all summary docs for that scope_id
264
+ - Returns MemorySearchHit with either `event` or `summary` populated.
265
+
266
+ TODO:
267
+ - Plug into a real semantic index / RAG backend (mem_factory.rag_facade).
268
+ - Support more advanced filters (kinds, tags, summary_tag) on the request.
269
+ """
270
+ container = current_services()
271
+ mem_factory = getattr(container, "memory_factory", None)
272
+ if mem_factory is None:
273
+ return MemorySearchResponse(hits=[])
274
+
275
+ scope_id = req.scope_id or ""
276
+ query = req.query or ""
277
+ top_k = getattr(req, "top_k", 10) or 10
278
+
279
+ hotlog = mem_factory.hotlog
280
+ docs = mem_factory.docs
281
+
282
+ hits: list[MemorySearchHit] = []
283
+
284
+ # 1) Search recent HotLog events
285
+ if scope_id:
286
+ raw_events: list[Event] = await hotlog.recent(
287
+ scope_id,
288
+ kinds=None,
289
+ limit=mem_factory.hot_limit,
290
+ )
291
+ for evt in raw_events:
292
+ text_parts: list[str] = []
293
+ if evt.text:
294
+ text_parts.append(evt.text)
295
+ if evt.data:
296
+ with suppress(Exception):
297
+ text_parts.append(str(evt.data))
298
+ haystack = " ".join(text_parts)
299
+ score = _string_score(haystack, query)
300
+ if score <= 0.0:
301
+ continue
302
+
303
+ api_evt = _event_to_api_event(evt)
304
+ hits.append(
305
+ MemorySearchHit(
306
+ score=score,
307
+ event=api_evt,
308
+ summary=None,
309
+ )
310
+ )
311
+
312
+ # 2) Search summary docs
313
+ try:
314
+ doc_ids = await docs.list()
315
+ except TypeError:
316
+ doc_ids = []
317
+
318
+ for doc_id in doc_ids:
319
+ doc = await docs.get(doc_id)
320
+ if not doc:
321
+ continue
322
+
323
+ if scope_id and doc.get("scope_id") != scope_id:
324
+ continue
325
+
326
+ text_parts: list[str] = []
327
+ if doc.get("summary"):
328
+ text_parts.append(str(doc.get("summary")))
329
+
330
+ if doc.get("key_facts"):
331
+ with suppress(Exception):
332
+ text_parts.append(" ".join(map(str, doc["key_facts"])))
333
+
334
+ haystack = " ".join(text_parts)
335
+ score = _string_score(haystack, query)
336
+ if score <= 0.0:
337
+ continue
338
+
339
+ summary_entry = _doc_to_summary_entry(doc_id, doc)
340
+ hits.append(
341
+ MemorySearchHit(
342
+ score=score,
343
+ event=None,
344
+ summary=summary_entry,
345
+ )
346
+ )
347
+
348
+ # Sort by score (desc) and truncate
349
+ hits.sort(key=lambda h: h.score, reverse=True)
350
+ if len(hits) > top_k:
351
+ hits = hits[:top_k]
352
+
353
+ return MemorySearchResponse(hits=hits)
@@ -0,0 +1,47 @@
1
+ # /health /config
2
+
3
+
4
+ from fastapi import APIRouter, Depends
5
+
6
+ from .deps import RequestIdentity, get_identity
7
+ from .schemas import ConfigLLMProvider, ConfigResponse, HealthResponse
8
+
9
+ router = APIRouter(tags=["misc"])
10
+
11
+
12
+ @router.get("/health", response_model=HealthResponse)
13
+ async def health_check() -> HealthResponse:
14
+ """
15
+ Simple health check endpoint.
16
+ """
17
+ # TODO: optionally include deeper checks (DB, Redis, etc.)
18
+ return HealthResponse(status="ok", version="0.1.0a1")
19
+
20
+
21
+ @router.get("/config", response_model=ConfigResponse)
22
+ async def config_info(
23
+ identity: RequestIdentity = Depends(get_identity), # noqa: B008
24
+ ) -> ConfigResponse:
25
+ """
26
+ Return sanitized config info that's safe for UI.
27
+
28
+ TODO:
29
+ - Read from AppSettings.
30
+ - Mask secrets; only expose high-level info.
31
+ """
32
+ # Stub example
33
+ return ConfigResponse(
34
+ version="0.1.0a1",
35
+ storage_backends={
36
+ "memory": "fs_jsonl",
37
+ "artifacts": "fs",
38
+ },
39
+ llm_providers=[
40
+ ConfigLLMProvider(name="openai", model="gpt-4o-mini", enabled=True),
41
+ ],
42
+ features={
43
+ "ws_channels": True,
44
+ "artifact_search": True,
45
+ "memory_search": True,
46
+ },
47
+ )