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
@@ -1,6 +1,14 @@
1
1
  # redirect runtime service imports for clean imports
2
2
 
3
3
  from aethergraph.core.runtime.ad_hoc_context import open_session
4
+ from aethergraph.core.runtime.run_manager import RunManager
5
+ from aethergraph.core.runtime.run_types import (
6
+ RunImportance,
7
+ RunOrigin,
8
+ RunRecord,
9
+ RunStatus,
10
+ RunVisibility,
11
+ )
4
12
  from aethergraph.core.runtime.runtime_services import (
5
13
  # logger service helpers
6
14
  current_logger_factory,
@@ -59,4 +67,11 @@ __all__ = [
59
67
  "list_mcp_clients",
60
68
  # ad-hoc context
61
69
  "open_session",
70
+ # run manager and types
71
+ "RunManager",
72
+ "RunRecord",
73
+ "RunStatus",
74
+ "RunOrigin",
75
+ "RunImportance",
76
+ "RunVisibility",
62
77
  ]
@@ -1,16 +1,40 @@
1
1
  import asyncio
2
+ from contextlib import asynccontextmanager, suppress
3
+ import logging
4
+ import os
5
+ from pathlib import Path
2
6
  from typing import Optional
3
7
 
4
8
  from fastapi import FastAPI
5
9
  from fastapi.middleware.cors import CORSMiddleware
6
-
10
+ from fastapi.responses import FileResponse, PlainTextResponse
11
+ from fastapi.staticfiles import StaticFiles
12
+
13
+ from aethergraph.api.v1.agents import router as agents_router
14
+ from aethergraph.api.v1.apps import router as apps_router
15
+ from aethergraph.api.v1.artifacts import router as artifacts_router
16
+ from aethergraph.api.v1.graphs import router as graphs_router
17
+ from aethergraph.api.v1.identity import router as identity_router
18
+ from aethergraph.api.v1.memory import router as memory_router
19
+ from aethergraph.api.v1.misc import router as misc_router
20
+ from aethergraph.api.v1.runs import router as runs_router
21
+ from aethergraph.api.v1.session import router as session_router
22
+ from aethergraph.api.v1.stats import router as stats_router
23
+ from aethergraph.api.v1.viz import router as vis_router
24
+
25
+ # include apis
7
26
  from aethergraph.config.config import AppSettings
8
- from aethergraph.utils.optdeps import require
27
+ from aethergraph.core.runtime.runtime_services import install_services
9
28
 
10
- from ..core.runtime.runtime_services import install_services
29
+ # import built-in agents and plugins to register them
30
+ from aethergraph.plugins.agents.default_chat_agent import * # noqa: F403
11
31
 
12
32
  # channel routes
13
- from ..services.container.default_container import build_default_container
33
+ from aethergraph.server.loading import GraphLoader, LoadSpec
34
+ from aethergraph.services.container.default_container import build_default_container
35
+ from aethergraph.utils.optdeps import require
36
+
37
+ logger = logging.getLogger(__name__)
14
38
 
15
39
 
16
40
  def create_app(
@@ -23,40 +47,23 @@ def create_app(
23
47
  Builds the FastAPI app, registers routers, and installs all services
24
48
  into app.state.container (and globally via install_services()).
25
49
  """
26
- app = FastAPI(title="AetherGraph Sidecar", version="0.1")
27
-
28
- app.add_middleware(
29
- CORSMiddleware,
30
- allow_origins=["http://localhost:5173"], # dev UI origin
31
- allow_credentials=True,
32
- allow_methods=["*"],
33
- allow_headers=["*"],
34
- )
35
50
 
36
- # Resolve settings early, so we can conditionally include routers
51
+ # Resolve settings and container up front so lifespan can capture them
37
52
  settings = cfg or AppSettings()
38
- app.state.settings = settings
39
-
40
- # --- Routers (HTTP transports) ---
41
- # For now, we can just always include; or gate it with a flag like settings.slack.use_webhook.
42
- # app.include_router(slack_router) # HTTP /slack/events + /slack/interact
43
- # app.include_router(console_router)
44
- # app.include_router(telegram_router)
45
- # app.include_router(webui_router)
46
-
47
- # override log level in config
48
53
  settings.logging.level = log_level
49
54
 
50
- # ---- Services container ----
51
55
  container = build_default_container(root=workspace, cfg=settings)
52
- app.state.container = container
53
56
 
54
- # install globally so run()/tools see the same services
55
- install_services(container)
57
+ @asynccontextmanager
58
+ async def lifespan(app: FastAPI):
59
+ # --- Startup: attach settings/container and start external transports ---
60
+ app.state.settings = settings
61
+ app.state.container = container
62
+
63
+ slack_task = None
64
+ tg_task = None
56
65
 
57
- # ---- External channel transports (Socket Mode, polling, etc.) ----
58
- @app.on_event("startup")
59
- async def start_external_transports():
66
+ # Slack Socket Mode
60
67
  slack_cfg = settings.slack
61
68
  if (
62
69
  slack_cfg
@@ -70,15 +77,164 @@ def create_app(
70
77
 
71
78
  runner = SlackSocketModeRunner(container=container, settings=settings)
72
79
  app.state.slack_socket_runner = runner
73
- asyncio.create_task(runner.start())
80
+ slack_task = asyncio.create_task(runner.start())
74
81
 
75
- # Telegram polling for local / dev
82
+ # Telegram polling
76
83
  tg_cfg = settings.telegram
77
84
  if tg_cfg and tg_cfg.enabled and tg_cfg.polling_enabled and tg_cfg.bot_token:
78
85
  from ..plugins.channel.websockets.telegram_polling import TelegramPollingRunner
79
86
 
80
87
  tg_runner = TelegramPollingRunner(container=container, settings=settings)
81
88
  app.state.telegram_polling_runner = tg_runner
82
- asyncio.create_task(tg_runner.start())
89
+ tg_task = asyncio.create_task(tg_runner.start())
90
+
91
+ try:
92
+ # Hand control back to FastAPI / TestClient
93
+ yield
94
+ finally:
95
+ # --- Shutdown: best-effort cleanup of background tasks ---
96
+ for task in (slack_task, tg_task):
97
+ if task is not None and not task.done():
98
+ task.cancel()
99
+ # swallow cancellation errors
100
+ with suppress(asyncio.CancelledError):
101
+ await task
102
+
103
+ # Create app with lifespan
104
+ app = FastAPI(
105
+ title="AetherGraph Sidecar",
106
+ version="0.1",
107
+ lifespan=lifespan,
108
+ )
83
109
 
110
+ frontend_dir = Path(__file__).parent / "ui_static"
111
+ if frontend_dir.exists():
112
+ logger.info(f"Serving built frontend UI from {frontend_dir}")
113
+ logger.info("UI will be available at: http://<host>:<port>/ui")
114
+
115
+ # 1) Serve built assets under /ui/assets
116
+ assets_dir = frontend_dir / "assets"
117
+ if assets_dir.exists():
118
+ app.mount(
119
+ "/ui/assets",
120
+ StaticFiles(directory=str(assets_dir)),
121
+ name="ui_assets",
122
+ )
123
+
124
+ index_path = frontend_dir / "index.html"
125
+
126
+ # 2) SPA catch-all: /ui and ANY /ui/... path -> index.html
127
+ @app.get("/ui", include_in_schema=False)
128
+ @app.get("/ui/{full_path:path}", include_in_schema=False)
129
+ async def serve_ui(full_path: str = ""):
130
+ if index_path.exists():
131
+ return FileResponse(index_path)
132
+ return PlainTextResponse(
133
+ "UI bundle not found. Please build the frontend and copy it to ui_static.",
134
+ status_code=501,
135
+ )
136
+
137
+ else:
138
+ logger.warning(
139
+ "AetherGraph UI bundle NOT found at %s. "
140
+ "The /ui endpoint will return a 501 until you build and copy it.",
141
+ frontend_dir,
142
+ )
143
+
144
+ @app.get("/ui", include_in_schema=False)
145
+ async def ui_not_built():
146
+ return PlainTextResponse(
147
+ "UI bundle not found. Please build the frontend and copy it to ui_static.",
148
+ status_code=501,
149
+ )
150
+
151
+ # CORS
152
+ app.add_middleware(
153
+ CORSMiddleware,
154
+ allow_origins=["http://localhost:5173"], # dev UI origin
155
+ allow_credentials=True,
156
+ allow_methods=["*"],
157
+ allow_headers=["*"],
158
+ )
159
+
160
+ # Routers
161
+ app.include_router(router=runs_router, prefix="/api/v1")
162
+ app.include_router(router=graphs_router, prefix="/api/v1")
163
+ app.include_router(router=artifacts_router, prefix="/api/v1")
164
+ app.include_router(router=memory_router, prefix="/api/v1")
165
+ app.include_router(router=stats_router, prefix="/api/v1")
166
+ app.include_router(router=identity_router, prefix="/api/v1")
167
+ app.include_router(router=misc_router, prefix="/api/v1")
168
+ app.include_router(router=vis_router, prefix="/api/v1")
169
+ app.include_router(router=session_router, prefix="/api/v1")
170
+ app.include_router(router=apps_router, prefix="/api/v1")
171
+ app.include_router(router=agents_router, prefix="/api/v1")
172
+
173
+ # Webui router
174
+ from aethergraph.plugins.channel.routes.webui_routes import router as webui_router
175
+
176
+ app.include_router(router=webui_router, prefix="/api/v1")
177
+
178
+ # Install services globally so run()/tools see the same container
179
+ install_services(container)
180
+
181
+ # Optional: keep these for immediate access before lifespan runs
182
+ app.state.settings = settings
183
+ app.state.container = container
184
+
185
+ return app
186
+
187
+
188
+ def _load_user_graphs_from_env() -> None:
189
+ """
190
+ Called inside each uvicorn worker to import user graphs based
191
+ on environment variables set by the CLI.
192
+ """
193
+ modules_str = os.environ.get("AETHERGRAPH_LOAD_MODULES", "")
194
+ paths_str = os.environ.get("AETHERGRAPH_LOAD_PATHS", "")
195
+ project_root_str = os.environ.get("AETHERGRAPH_PROJECT_ROOT", ".")
196
+ strict_str = os.environ.get("AETHERGRAPH_STRICT_LOAD", "0")
197
+
198
+ modules = [m for m in modules_str.split(",") if m]
199
+ paths = [Path(p) for p in paths_str.split(os.pathsep) if p]
200
+
201
+ project_root = Path(project_root_str).resolve()
202
+ strict = strict_str.lower() in ("1", "true", "yes")
203
+
204
+ spec = LoadSpec(
205
+ modules=modules,
206
+ paths=paths,
207
+ project_root=project_root,
208
+ strict=strict,
209
+ )
210
+
211
+ loader = GraphLoader()
212
+ report = loader.load(spec)
213
+
214
+ # Optional: log report.loaded / report.errors here if you like
215
+ print("🚀 [worker] Loaded user graphs:", report.loaded)
216
+ if report.errors:
217
+ for e in report.errors:
218
+ print(f"⚠️ [worker load error] {e.source}: {e.error}")
219
+
220
+
221
+ def create_app_from_env() -> FastAPI:
222
+ """
223
+ Factory for uvicorn --reload / workers mode.
224
+ Reads workspace + graph load config from env, imports user graphs,
225
+ then builds the FastAPI app.
226
+ """
227
+ workspace = os.environ.get("AETHERGRAPH_WORKSPACE", "./aethergraph_data")
228
+ log_level = os.environ.get("AETHERGRAPH_LOG_LEVEL", "warning")
229
+
230
+ # 1) Load user graphs in *this* process
231
+ _load_user_graphs_from_env()
232
+
233
+ # 2) Build the app (your existing factory)
234
+ # If you have a config system, wire it here
235
+ app = create_app(
236
+ workspace=workspace,
237
+ cfg=None, # or AppSettings.from_env(), etc.
238
+ log_level=log_level,
239
+ )
84
240
  return app
@@ -0,0 +1,202 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import AsyncIterator, Iterable
4
+ import json
5
+ from typing import Any
6
+
7
+ import httpx
8
+ import websockets
9
+
10
+
11
+ class ChannelClient:
12
+ """
13
+ Convenience client for talking to a running AetherGraph server from Python.
14
+
15
+ - send_* methods: external -> AG (inbound to AG via /channel/incoming)
16
+ - iter_events(): AG -> external (outbound from AG via /ws/channel)
17
+
18
+ This is intentionally thin; real apps can wrap it with their own abstractions.
19
+ """
20
+
21
+ def __init__(
22
+ self,
23
+ base_url: str,
24
+ *,
25
+ scheme: str = "ext",
26
+ channel_id: str = "default",
27
+ thread_id: str | None = None,
28
+ timeout: float = 100.0,
29
+ api_key: str | None = None, # currently unused
30
+ http_client: httpx.AsyncClient | None = None, # managed externally if provided
31
+ ws_path: str = "/ws/channel", # currently unused
32
+ ):
33
+ self.base_url = base_url
34
+ self.scheme = scheme
35
+ self.channel_id = channel_id
36
+ self.thread_id = thread_id
37
+ self.timeout = timeout
38
+ self.api_key = api_key
39
+ self.ws_path = ws_path
40
+
41
+ self._external_client = http_client
42
+ self._client: httpx.AsyncClient | None = None
43
+ self._owns_client = http_client is None
44
+
45
+ # ------------- internal helpers -------------
46
+ @property
47
+ def client(self) -> httpx.AsyncClient:
48
+ if self._external_client is not None:
49
+ return self._external_client
50
+ if self._client is None:
51
+ headers = {}
52
+ if self.api_key:
53
+ headers["Authorization"] = f"Bearer {self.api_key}"
54
+ self._client = httpx.AsyncClient(
55
+ base_url=self.base_url,
56
+ headers=headers,
57
+ timeout=self.timeout,
58
+ )
59
+ return self._client
60
+
61
+ async def aclose(self):
62
+ if self._owns_client and self._client is not None:
63
+ await self._client.aclose()
64
+ self._client = None
65
+
66
+ def _default_thread_id(self, thread_id: str | None) -> str | None:
67
+ return thread_id if thread_id is not None else self.thread_id
68
+
69
+ async def _get_client(self) -> httpx.AsyncClient:
70
+ """Get or create an httpx.AsyncClient."""
71
+ if self._external_client is not None:
72
+ return self._external_client
73
+ return httpx.AsyncClient(base_url=self.base_url, timeout=self.timeout)
74
+
75
+ # --------- Inbound to AG (HTTP) ---------
76
+ async def send_text(self, text: str, *, meta: dict[str, Any] | None = None) -> httpx.Response:
77
+ """
78
+ Send a text message into AG via /channel/incoming.
79
+ """
80
+ url = f"{self.base_url}/channel/incoming"
81
+ payload = {
82
+ "scheme": self.scheme,
83
+ "channel_id": self.channel_id,
84
+ "thread_id": self.thread_id,
85
+ "text": text,
86
+ "meta": meta or {},
87
+ }
88
+ r = await self.client.post(url, json=payload)
89
+ r.raise_for_status()
90
+ return r.json
91
+
92
+ async def send_choice(
93
+ self, choice: str, *, meta: dict[str, Any] | None = None
94
+ ) -> httpx.Response:
95
+ """
96
+ Send a choice/approval response into AG via /channel/incoming.
97
+ """
98
+ url = f"{self.base_url}/channel/incoming"
99
+ payload = {
100
+ "scheme": self.scheme,
101
+ "channel_id": self.channel_id,
102
+ "thread_id": self.thread_id,
103
+ "choice": choice,
104
+ "meta": meta or {},
105
+ }
106
+ r = await self.client.post(url, json=payload)
107
+ r.raise_for_status()
108
+ return r.json()
109
+
110
+ async def send_text_and_files(
111
+ self,
112
+ text: str | None,
113
+ files: Iterable[dict[str, Any]],
114
+ *,
115
+ meta: dict[str, Any] | None = None,
116
+ ):
117
+ """
118
+ Send a text message with attached files into AG via /channel/incoming.
119
+
120
+ Each file is a dict with keys like:
121
+ - name (str): filename
122
+ - mimetype (str): MIME type
123
+ - size (int): size in bytes
124
+ - url (str): public URL to download the file
125
+ """
126
+ url = f"{self.base_url}/channel/incoming"
127
+ payload = {
128
+ "scheme": self.scheme,
129
+ "channel_id": self.channel_id,
130
+ "thread_id": self.thread_id,
131
+ "text": text,
132
+ "files": list(files),
133
+ "meta": meta or {},
134
+ }
135
+ r = await self.client.post(url, json=payload)
136
+ r.raise_for_status()
137
+ return r.json()
138
+
139
+ async def resume(
140
+ self,
141
+ run_id: str,
142
+ node_id: str,
143
+ token: str,
144
+ resume_key: str | None = None,
145
+ payload: dict[str, Any] | None = None,
146
+ ) -> httpx.Response:
147
+ """
148
+ Low-level manual resume via /channel/resume.
149
+ """
150
+ url = f"{self.base_url}/channel/resume"
151
+ body = {
152
+ "run_id": run_id,
153
+ "node_id": node_id,
154
+ "token": token,
155
+ "resume_key": resume_key,
156
+ "payload": payload or {},
157
+ }
158
+ r = await self.client.post(url, json=body)
159
+ r.raise_for_status()
160
+ return r.json()
161
+
162
+ # --------- Outbound from AG (WebSocket) ---------
163
+ async def iter_events(self) -> AsyncIterator[dict[str, Any]]:
164
+ """
165
+ Receive outbound channel events over a WebSocket.
166
+
167
+ Expected server endpoint: /ws/channel
168
+
169
+ Query params:
170
+ - scheme
171
+ - channel_id
172
+ - thread_id (optional)
173
+ - api_key (optional)
174
+ """
175
+ # Build ws URL from base_url (http/https -> ws/wss)
176
+ if self.base_url.startswith("https://"):
177
+ ws_base = "wss://" + self.base_url[len("https://") :]
178
+ elif self.base_url.startswith("http://"):
179
+ ws_base = "ws://" + self.base_url[len("http://") :]
180
+ else:
181
+ # assume ws already
182
+ ws_base = self.base_url
183
+
184
+ params = {
185
+ "scheme": self.scheme,
186
+ "channel_id": self.channel_id,
187
+ }
188
+ if self.thread_id:
189
+ params["thread_id"] = self.thread_id
190
+ if self.api_key:
191
+ params["api_key"] = self.api_key
192
+
193
+ query = "&".join(f"{k}={v}" for k, v in params.items())
194
+ url = f"{ws_base}{self.ws_path}?{query}"
195
+
196
+ async with websockets.connect(url) as ws:
197
+ async for msg in ws:
198
+ try:
199
+ data = json.loads(msg)
200
+ except Exception:
201
+ data = {"raw": msg}
202
+ yield data
@@ -0,0 +1,116 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from fastapi import APIRouter, HTTPException, Request
6
+ from pydantic import BaseModel
7
+ from starlette.responses import JSONResponse
8
+
9
+ from aethergraph.services.channel.ingress import ChannelIngress, IncomingFile, IncomingMessage
10
+
11
+ router = APIRouter()
12
+
13
+
14
+ # --------- Pydantic models for HTTP request/response ---------
15
+ class HttpIncomingFile(BaseModel):
16
+ id: str | None = None
17
+ name: str | None = None
18
+ mimetype: str | None = None
19
+ size: int | None = None
20
+ url: str | None = None
21
+ uri: str | None = None
22
+ extra: dict[str, Any] | None = None
23
+
24
+
25
+ class ChannelIncomingBody(BaseModel):
26
+ """
27
+ High-level resume via channel (no run_id/node_id/token exposed).
28
+ """
29
+
30
+ scheme: str = "ext"
31
+ channel_id: str
32
+ thread_id: str | None = None
33
+
34
+ text: str | None = None
35
+ files: list[HttpIncomingFile] | None = None
36
+ choice: str | None = None
37
+ meta: dict[str, Any] | None = None
38
+
39
+
40
+ class ChannelManualResumeBody(BaseModel):
41
+ """
42
+ Low-level resume for power users: explicit run/node/token.
43
+ """
44
+
45
+ run_id: str
46
+ node_id: str
47
+ token: str
48
+ payload: dict[str, Any] | None = None
49
+
50
+
51
+ # --------- HTTP route handlers ---------
52
+ @router.post("/channel/incoming")
53
+ async def channel_incoming(body: ChannelIncomingBody, request: Request):
54
+ """
55
+ Generic inbound message endpoint. Typical UI call looks like:
56
+
57
+ POST /channel/incoming
58
+ {
59
+ "scheme": "ext",
60
+ "channel_id": "user-123",
61
+ "text": "hello",
62
+ "meta": {"foo": "bar"}
63
+ }
64
+ """
65
+ try:
66
+ container = request.app.state.container
67
+ ingress: ChannelIngress = container.channel_ingress # TODO: wire via default container
68
+
69
+ files = []
70
+ if body.files:
71
+ files = [
72
+ IncomingFile(
73
+ id=f.id,
74
+ name=f.name,
75
+ mimetype=f.mimetype,
76
+ size=f.size,
77
+ url=f.url,
78
+ uri=f.uri,
79
+ extra=f.extra or {},
80
+ )
81
+ for f in body.files
82
+ ]
83
+
84
+ ok = await ingress.handle(
85
+ IncomingMessage(
86
+ scheme=body.scheme,
87
+ channel_id=body.channel_id,
88
+ thread_id=body.thread_id,
89
+ text=body.text,
90
+ files=files,
91
+ choice=body.choice,
92
+ meta=body.meta or {},
93
+ )
94
+ )
95
+ return JSONResponse({"ok": True, "resumed": ok})
96
+ except Exception as e:
97
+ raise HTTPException(status_code=500, detail=str(e)) from e
98
+
99
+
100
+ @router.post("/channel/resume")
101
+ async def channel_resume(body: ChannelManualResumeBody, request: Request):
102
+ """
103
+ Low-level resume for power users: explicit run/node/token.
104
+ """
105
+ try:
106
+ container = request.app.state.container
107
+
108
+ await container.resume_router.resume(
109
+ run_id=body.run_id,
110
+ node_id=body.node_id,
111
+ token=body.token,
112
+ payload=body.payload or {},
113
+ )
114
+ return JSONResponse({"ok": True})
115
+ except Exception as e:
116
+ raise HTTPException(status_code=500, detail=str(e)) from e
@@ -0,0 +1,45 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+
5
+ from fastapi import APIRouter, WebSocket, WebSocketDisconnect
6
+
7
+ router = APIRouter()
8
+
9
+
10
+ @router.websocket("/ws/channel")
11
+ async def ws_channel(ws: WebSocket):
12
+ """
13
+ Generic outbound event stream.
14
+
15
+ Client must first send a JSON handshake:
16
+ {"scheme": "ext", "channel_id": "user-123"}
17
+
18
+ Then we stream any events that the queue-based ChannelAdapter
19
+ appends to `outbox://<scheme>:chan/<channel_id>`.
20
+ """
21
+ await ws.accept()
22
+
23
+ hello = await ws.receive_json()
24
+ scheme = hello.get("scheme") or "ext"
25
+ channel_id = hello["channel_id"]
26
+
27
+ container = ws.app.state.container
28
+ c = container
29
+
30
+ ch_key = f"{scheme}:chan/{channel_id}"
31
+ outbox_key = f"outbox://{ch_key}"
32
+
33
+ last_idx = 0
34
+ try:
35
+ while True:
36
+ await asyncio.sleep(0.25)
37
+ events = await c.kv_hot.list_get(outbox_key) or []
38
+ if last_idx < len(events):
39
+ for ev in events[last_idx:]:
40
+ # ev is a dict produced by our queue-based adapter
41
+ await ws.send_json(ev)
42
+ last_idx = len(events)
43
+ except WebSocketDisconnect:
44
+ # just drop; nothing special needed
45
+ return