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,117 @@
1
+ from __future__ import annotations
2
+
3
+ from contextlib import contextmanager, suppress
4
+ from dataclasses import dataclass, field
5
+ import hashlib
6
+ import importlib
7
+ import importlib.util
8
+ from pathlib import Path
9
+ import sys
10
+ import traceback
11
+ from typing import Any
12
+
13
+
14
+ @dataclass
15
+ class LoadSpec:
16
+ modules: list[str] = field(default_factory=list) # ["my_pkg.graphs"]
17
+ paths: list[Path] = field(default_factory=list) # [Path("./my_graphs.py")]
18
+ project_root: Path | None = None # for sys.path injection
19
+ strict: bool = True # raise on first error
20
+
21
+
22
+ @dataclass
23
+ class LoadError:
24
+ source: str # module or path
25
+ error: str # error message
26
+ traceback: str | None = None # optional traceback
27
+
28
+
29
+ @dataclass
30
+ class LoadReport:
31
+ loaded: list[str] = field(default_factory=list) # successfully loaded modules/paths
32
+ errors: list[LoadError] = field(default_factory=list) # errors encountered during loading
33
+ meta: dict[str, Any] = field(default_factory=dict) # additional metadata
34
+
35
+
36
+ @contextmanager
37
+ def _temp_sys_path(root: Path | None):
38
+ if not root:
39
+ yield
40
+ return
41
+ if isinstance(root, str):
42
+ root = Path(root)
43
+ root_str = str(root.resolve())
44
+ already = root_str in sys.path
45
+ if not already:
46
+ sys.path.insert(0, root_str)
47
+ try:
48
+ yield
49
+ finally:
50
+ if not already:
51
+ # remove first occurrence in case user also added it
52
+ with suppress(ValueError):
53
+ sys.path.remove(root_str)
54
+
55
+
56
+ def _stable_module_name_for_path(path: Path) -> str:
57
+ # stable across runs for the same absolute path
58
+ h = hashlib.sha1(str(path.resolve()).encode("utf-8")).hexdigest()[:12]
59
+ return f"aethergraph_userfile_{h}"
60
+
61
+
62
+ class GraphLoader:
63
+ def __init__(self):
64
+ self.last_report: LoadReport | None = None
65
+
66
+ def load(self, spec: LoadSpec) -> LoadReport:
67
+ report = LoadReport()
68
+ with _temp_sys_path(spec.project_root):
69
+ # 1) import modules
70
+ for mod in spec.modules:
71
+ try:
72
+ importlib.import_module(mod)
73
+ report.loaded.append(f"module:{mod}")
74
+ except Exception as e:
75
+ report.errors.append(
76
+ LoadError(
77
+ source=f"module:{mod}",
78
+ error=repr(e),
79
+ traceback=traceback.format_exc(),
80
+ )
81
+ )
82
+ if spec.strict:
83
+ self.last_report = report
84
+ raise
85
+
86
+ # 2) import paths
87
+ for p in spec.paths:
88
+ try:
89
+ if isinstance(p, str):
90
+ p = Path(p)
91
+ path = p.resolve()
92
+ name = _stable_module_name_for_path(path)
93
+ # Re-import strategy: if already imported, do nothing (Phase 1 design)
94
+ if name in sys.modules:
95
+ report.loaded.append(f"path:{path} (cached)")
96
+ continue
97
+
98
+ spec_obj = importlib.util.spec_from_file_location(name, str(path))
99
+ if spec_obj is None or spec_obj.loader is None:
100
+ raise ImportError(f"Cannot load spec for path: {path}")
101
+ module = importlib.util.module_from_spec(spec_obj)
102
+ sys.modules[name] = module
103
+ spec_obj.loader.exec_module(module) # decorators @graphify etc. run here
104
+ report.loaded.append(f"path:{path}")
105
+ except Exception as e:
106
+ report.errors.append(
107
+ LoadError(
108
+ source=f"path:{p}",
109
+ error=repr(e),
110
+ traceback=traceback.format_exc(),
111
+ )
112
+ )
113
+ if spec.strict:
114
+ self.last_report = report
115
+ raise
116
+ self.last_report = report
117
+ return report
@@ -0,0 +1,131 @@
1
+ # aethergraph/server.py
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ from collections.abc import Sequence
6
+
7
+ from fastapi import FastAPI
8
+ import uvicorn
9
+
10
+ from aethergraph.config.context import set_current_settings
11
+ from aethergraph.config.loader import load_settings
12
+ from aethergraph.server.app_factory import create_app
13
+
14
+
15
+ def build_arg_parser() -> argparse.ArgumentParser:
16
+ parser = argparse.ArgumentParser(
17
+ prog="aethergraph-server",
18
+ description="Run the AetherGraph HTTP/WS server.",
19
+ )
20
+
21
+ parser.add_argument(
22
+ "--host",
23
+ default="0.0.0.0",
24
+ help="Host interface to bind (default: 0.0.0.0).",
25
+ )
26
+ parser.add_argument(
27
+ "--port",
28
+ type=int,
29
+ default=8745,
30
+ help="Port to bind (default: 8745).",
31
+ )
32
+ parser.add_argument(
33
+ "--workspace",
34
+ default="./aethergraph_data",
35
+ help="Workspace directory for AG data (default: ./aethergraph_data).",
36
+ )
37
+ parser.add_argument(
38
+ "--log-level",
39
+ dest="app_log_level",
40
+ default="info",
41
+ choices=["debug", "info", "warning", "error"],
42
+ help="Application log level (default: info).",
43
+ )
44
+ parser.add_argument(
45
+ "--uvicorn-log-level",
46
+ dest="uvicorn_log_level",
47
+ default="info",
48
+ choices=["critical", "error", "warning", "info", "debug", "trace"],
49
+ help="Uvicorn log level (default: info).",
50
+ )
51
+ parser.add_argument(
52
+ "--reload",
53
+ action="store_true",
54
+ help="Enable auto-reload (dev mode).",
55
+ )
56
+ return parser
57
+
58
+
59
+ def app_factory() -> FastAPI:
60
+ """
61
+ Factory for uvicorn's --factory mode.
62
+
63
+ Reads settings, installs them globally, builds the container and app.
64
+ """
65
+ cfg = load_settings()
66
+ set_current_settings(cfg)
67
+
68
+ app = create_app(
69
+ workspace=cfg.workspace.root if hasattr(cfg, "workspace") else "./aethergraph_data",
70
+ cfg=cfg,
71
+ log_level=cfg.logging.level if hasattr(cfg, "logging") else "info",
72
+ )
73
+ return app
74
+
75
+
76
+ def main(argv=None) -> None:
77
+ import argparse
78
+
79
+ import uvicorn
80
+
81
+ parser = argparse.ArgumentParser()
82
+ parser.add_argument("--host", default="127.0.0.1")
83
+ parser.add_argument("--port", type=int, default=8000)
84
+ parser.add_argument("--reload", action="store_true")
85
+ parser.add_argument("--uvicorn-log-level", default="info")
86
+ args = parser.parse_args(argv)
87
+
88
+ uvicorn.run(
89
+ "aethergraph.server.server:app_factory", # <- note :app_factory
90
+ host=args.host,
91
+ port=args.port,
92
+ log_level=args.uvicorn_log_level,
93
+ reload=args.reload,
94
+ factory=True,
95
+ )
96
+
97
+
98
+ def main_old(argv: Sequence[str] | None = None) -> None:
99
+ """
100
+ Entry point for running AetherGraph as a long-lived server.
101
+
102
+ Example:
103
+ python -m aethergraph.server --host 0.0.0.0 --port 8745
104
+ """
105
+ parser = build_arg_parser()
106
+ args = parser.parse_args(argv)
107
+
108
+ # 1) Load and install settings (same as sidecar)
109
+ cfg = load_settings()
110
+ set_current_settings(cfg)
111
+
112
+ # 2) Build the FastAPI app with your existing factory
113
+ app = create_app(
114
+ workspace=args.workspace,
115
+ cfg=cfg,
116
+ log_level=args.app_log_level,
117
+ )
118
+
119
+ # 3) Run uvicorn in this process (no threads, daemon-style)
120
+ # This blocks until the server is stopped.
121
+ uvicorn.run(
122
+ app,
123
+ host=args.host,
124
+ port=args.port,
125
+ log_level=args.uvicorn_log_level,
126
+ reload=args.reload,
127
+ )
128
+
129
+
130
+ if __name__ == "__main__":
131
+ main()
@@ -0,0 +1,240 @@
1
+ from __future__ import annotations
2
+
3
+ from contextlib import contextmanager, suppress
4
+ import json
5
+ import os
6
+ from pathlib import Path
7
+ import socket
8
+ import time
9
+ from typing import Any
10
+
11
+ STATE_DIR_NAME = ".aethergraph"
12
+ STATE_FILE_NAME = "server.json"
13
+ LOCK_FILE_NAME = "server.lock"
14
+
15
+
16
+ class PortInUseError(RuntimeError):
17
+ def __init__(self, host: str, port: int):
18
+ super().__init__(f"Port {host}:{port} is already in use by another process.")
19
+ self.host = host
20
+ self.port = port
21
+
22
+
23
+ def _state_dir(workspace: str | Path) -> Path:
24
+ return Path(workspace).resolve() / STATE_DIR_NAME
25
+
26
+
27
+ def state_file_path(workspace: str | Path) -> Path:
28
+ return _state_dir(workspace) / STATE_FILE_NAME
29
+
30
+
31
+ def lock_file_path(workspace: str | Path) -> Path:
32
+ return _state_dir(workspace) / LOCK_FILE_NAME
33
+
34
+
35
+ def ensure_state_dir(workspace: str | Path) -> Path:
36
+ d = _state_dir(workspace)
37
+ d.mkdir(parents=True, exist_ok=True)
38
+ return d
39
+
40
+
41
+ @contextmanager
42
+ def workspace_lock(workspace: str | Path, timeout_s: float = 10.0, poll_s: float = 0.1):
43
+ """
44
+ Cross-platform file lock:
45
+ - Windows: msvcrt.locking
46
+ - Unix: fcntl.flock
47
+
48
+ Ensures only one server starts per workspace at a time.
49
+ """
50
+ ensure_state_dir(workspace)
51
+ lp = lock_file_path(workspace)
52
+ f = open(lp, "a+") # noqa: SIM115 # keep handle open to hold the lock
53
+
54
+ start = time.time()
55
+ while True:
56
+ try:
57
+ if os.name == "nt":
58
+ import msvcrt # type: ignore
59
+
60
+ # lock 1 byte
61
+ f.seek(0)
62
+ msvcrt.locking(f.fileno(), msvcrt.LK_NBLCK, 1)
63
+ else:
64
+ import fcntl # type: ignore
65
+
66
+ fcntl.flock(f.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
67
+ break
68
+ except OSError as e:
69
+ if time.time() - start > timeout_s:
70
+ f.close()
71
+ raise TimeoutError(f"Timed out acquiring lock for workspace: {workspace}") from e
72
+ time.sleep(poll_s)
73
+
74
+ try:
75
+ yield
76
+ finally:
77
+ try:
78
+ if os.name == "nt":
79
+ import msvcrt # type: ignore
80
+
81
+ f.seek(0)
82
+ msvcrt.locking(f.fileno(), msvcrt.LK_UNLCK, 1)
83
+ else:
84
+ import fcntl # type: ignore
85
+
86
+ fcntl.flock(f.fileno(), fcntl.LOCK_UN)
87
+ finally:
88
+ f.close()
89
+
90
+
91
+ def _tcp_ping(host: str, port: int, timeout_s: float = 0.25) -> bool:
92
+ try:
93
+ with socket.create_connection((host, port), timeout=timeout_s):
94
+ return True
95
+ except OSError:
96
+ return False
97
+
98
+
99
+ def _pid_alive(pid: int) -> bool:
100
+ """
101
+ Cross-platform check if a PID is alive.
102
+
103
+ On Unix: uses os.kill(pid, 0).
104
+ On Windows: uses OpenProcess + GetExitCodeProcess.
105
+ """
106
+ if pid <= 0:
107
+ return False
108
+
109
+ if os.name == "nt":
110
+ # Windows: use Win32 API instead of os.kill(pid, 0),
111
+ # which can give WinError 87 / SystemError behavior.
112
+ import ctypes
113
+ from ctypes import wintypes
114
+
115
+ PROCESS_QUERY_LIMITED_INFORMATION = 0x1000
116
+ STILL_ACTIVE = 259
117
+
118
+ handle = ctypes.windll.kernel32.OpenProcess(
119
+ PROCESS_QUERY_LIMITED_INFORMATION,
120
+ False,
121
+ wintypes.DWORD(pid),
122
+ )
123
+ if not handle:
124
+ return False
125
+
126
+ try:
127
+ exit_code = wintypes.DWORD()
128
+ if not ctypes.windll.kernel32.GetExitCodeProcess(handle, ctypes.byref(exit_code)):
129
+ # Failed to query exit code -> assume not alive
130
+ return False
131
+ return exit_code.value == STILL_ACTIVE
132
+ finally:
133
+ ctypes.windll.kernel32.CloseHandle(handle)
134
+
135
+ # POSIX: classic trick
136
+ try:
137
+ os.kill(pid, 0)
138
+ except OSError:
139
+ return False
140
+ else:
141
+ return True
142
+
143
+
144
+ def read_server_state(workspace: str | Path) -> dict[str, Any] | None:
145
+ p = state_file_path(workspace)
146
+ if not p.exists():
147
+ return None
148
+ try:
149
+ return json.loads(p.read_text(encoding="utf-8"))
150
+ except Exception:
151
+ return None
152
+
153
+
154
+ def write_server_state(workspace: str | Path, state: dict[str, Any]) -> None:
155
+ ensure_state_dir(workspace)
156
+ p = state_file_path(workspace)
157
+ tmp = p.with_suffix(".json.tmp")
158
+ tmp.write_text(json.dumps(state, indent=2), encoding="utf-8")
159
+ tmp.replace(p)
160
+
161
+
162
+ def clear_server_state(workspace: str | Path) -> None:
163
+ p = state_file_path(workspace)
164
+ if p.exists():
165
+ with suppress(Exception):
166
+ p.unlink()
167
+
168
+
169
+ def get_running_url_if_any(workspace: str | Path) -> str | None:
170
+ """
171
+ Returns URL if server.json exists AND it looks like *our* server is alive.
172
+
173
+ If the port is in use by another process (PID dead but TCP ping works),
174
+ raises PortInUseError so the caller can show a clearer message.
175
+ """
176
+ st = read_server_state(workspace)
177
+ if not st:
178
+ return None
179
+
180
+ host = st.get("host")
181
+ port = st.get("port")
182
+ url = st.get("url")
183
+ pid = st.get("pid")
184
+
185
+ if not isinstance(host, str) or not isinstance(url, str) or not isinstance(port, int):
186
+ return None
187
+ if not isinstance(pid, int):
188
+ pid = -1
189
+
190
+ pid_alive = pid > 0 and _pid_alive(pid)
191
+ port_alive = _tcp_ping(host, port)
192
+
193
+ # Case 1: PID + port both alive -> this really looks like our server
194
+ if pid_alive and port_alive:
195
+ return url
196
+
197
+ # Case 2: PID dead but port alive -> someone else is using that port
198
+ if (not pid_alive) and port_alive:
199
+ # our server isn't there anymore, but the port is taken
200
+ clear_server_state(workspace) # stale state; don't reuse
201
+ raise PortInUseError(host, port)
202
+
203
+ # Case 3: both dead -> stale file
204
+ if (not pid_alive) and (not port_alive):
205
+ clear_server_state(workspace)
206
+ return None
207
+
208
+ # Case 4: PID alive but port not responding.
209
+ # This can happen briefly if the process is starting up or shutting down.
210
+ # For CLI UX, it's usually fine to treat it as "running" and let the user retry if needed.
211
+ if pid_alive and not port_alive:
212
+ return url
213
+
214
+ # Fallback: be conservative and say "no running server"
215
+ return None
216
+
217
+
218
+ def pick_free_port(requested: int) -> int:
219
+ """
220
+ Port selection strategy:
221
+
222
+ - If requested != 0: respect the user's choice exactly.
223
+ - If requested == 0: try our preferred dev ports first (8745–8748),
224
+ and if all are taken, fall back to an OS-assigned ephemeral port.
225
+ """
226
+ if requested != 0:
227
+ return requested
228
+
229
+ # Preferred AetherGraph dev ports – unlikely to collide with Jupyter, mkdocs, etc.
230
+ preferred_ports = (8745, 8746, 8747, 8748)
231
+
232
+ for port in preferred_ports:
233
+ # Only 127.0.0.1 is relevant here; the server binding uses the real host later.
234
+ if not _tcp_ping("127.0.0.1", port):
235
+ return port
236
+
237
+ # All preferred ports taken – fall back to OS-assigned free port
238
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
239
+ s.bind(("127.0.0.1", 0))
240
+ return int(s.getsockname()[1])