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.
- aethergraph/__init__.py +4 -10
- aethergraph/__main__.py +293 -0
- aethergraph/api/v1/__init__.py +0 -0
- aethergraph/api/v1/agents.py +46 -0
- aethergraph/api/v1/apps.py +70 -0
- aethergraph/api/v1/artifacts.py +415 -0
- aethergraph/api/v1/channels.py +89 -0
- aethergraph/api/v1/deps.py +168 -0
- aethergraph/api/v1/graphs.py +259 -0
- aethergraph/api/v1/identity.py +25 -0
- aethergraph/api/v1/memory.py +353 -0
- aethergraph/api/v1/misc.py +47 -0
- aethergraph/api/v1/pagination.py +29 -0
- aethergraph/api/v1/runs.py +568 -0
- aethergraph/api/v1/schemas.py +535 -0
- aethergraph/api/v1/session.py +323 -0
- aethergraph/api/v1/stats.py +201 -0
- aethergraph/api/v1/viz.py +152 -0
- aethergraph/config/config.py +22 -0
- aethergraph/config/loader.py +3 -2
- aethergraph/config/storage.py +209 -0
- aethergraph/contracts/__init__.py +0 -0
- aethergraph/contracts/services/__init__.py +0 -0
- aethergraph/contracts/services/artifacts.py +27 -14
- aethergraph/contracts/services/memory.py +45 -17
- aethergraph/contracts/services/metering.py +129 -0
- aethergraph/contracts/services/runs.py +50 -0
- aethergraph/contracts/services/sessions.py +87 -0
- aethergraph/contracts/services/state_stores.py +3 -0
- aethergraph/contracts/services/viz.py +44 -0
- aethergraph/contracts/storage/artifact_index.py +88 -0
- aethergraph/contracts/storage/artifact_store.py +99 -0
- aethergraph/contracts/storage/async_kv.py +34 -0
- aethergraph/contracts/storage/blob_store.py +50 -0
- aethergraph/contracts/storage/doc_store.py +35 -0
- aethergraph/contracts/storage/event_log.py +31 -0
- aethergraph/contracts/storage/vector_index.py +48 -0
- aethergraph/core/__init__.py +0 -0
- aethergraph/core/execution/forward_scheduler.py +13 -2
- aethergraph/core/execution/global_scheduler.py +21 -15
- aethergraph/core/execution/step_forward.py +10 -1
- aethergraph/core/graph/__init__.py +0 -0
- aethergraph/core/graph/graph_builder.py +8 -4
- aethergraph/core/graph/graph_fn.py +156 -15
- aethergraph/core/graph/graph_spec.py +8 -0
- aethergraph/core/graph/graphify.py +146 -27
- aethergraph/core/graph/node_spec.py +0 -2
- aethergraph/core/graph/node_state.py +3 -0
- aethergraph/core/graph/task_graph.py +39 -1
- aethergraph/core/runtime/__init__.py +0 -0
- aethergraph/core/runtime/ad_hoc_context.py +64 -4
- aethergraph/core/runtime/base_service.py +28 -4
- aethergraph/core/runtime/execution_context.py +13 -15
- aethergraph/core/runtime/graph_runner.py +222 -37
- aethergraph/core/runtime/node_context.py +510 -6
- aethergraph/core/runtime/node_services.py +12 -5
- aethergraph/core/runtime/recovery.py +15 -1
- aethergraph/core/runtime/run_manager.py +783 -0
- aethergraph/core/runtime/run_manager_local.py +204 -0
- aethergraph/core/runtime/run_registration.py +2 -2
- aethergraph/core/runtime/run_types.py +89 -0
- aethergraph/core/runtime/runtime_env.py +136 -7
- aethergraph/core/runtime/runtime_metering.py +71 -0
- aethergraph/core/runtime/runtime_registry.py +36 -13
- aethergraph/core/runtime/runtime_services.py +194 -6
- aethergraph/core/tools/builtins/toolset.py +1 -1
- aethergraph/core/tools/toolkit.py +5 -0
- aethergraph/plugins/agents/default_chat_agent copy.py +90 -0
- aethergraph/plugins/agents/default_chat_agent.py +171 -0
- aethergraph/plugins/agents/shared.py +81 -0
- aethergraph/plugins/channel/adapters/webui.py +112 -112
- aethergraph/plugins/channel/routes/webui_routes.py +367 -102
- aethergraph/plugins/channel/utils/slack_utils.py +115 -59
- aethergraph/plugins/channel/utils/telegram_utils.py +88 -47
- aethergraph/plugins/channel/websockets/weibui_ws.py +172 -0
- aethergraph/runtime/__init__.py +15 -0
- aethergraph/server/app_factory.py +190 -34
- aethergraph/server/clients/channel_client.py +202 -0
- aethergraph/server/http/channel_http_routes.py +116 -0
- aethergraph/server/http/channel_ws_routers.py +45 -0
- aethergraph/server/loading.py +117 -0
- aethergraph/server/server.py +131 -0
- aethergraph/server/server_state.py +240 -0
- aethergraph/server/start.py +227 -66
- aethergraph/server/ui_static/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- aethergraph/server/ui_static/assets/index-BR5GtXcZ.css +1 -0
- aethergraph/server/ui_static/assets/index-CQ0HZZ83.js +400 -0
- aethergraph/server/ui_static/index.html +15 -0
- aethergraph/server/ui_static/logo.png +0 -0
- aethergraph/services/artifacts/__init__.py +0 -0
- aethergraph/services/artifacts/facade.py +1239 -132
- aethergraph/services/auth/{dev.py → authn.py} +0 -8
- aethergraph/services/auth/authz.py +100 -0
- aethergraph/services/channel/__init__.py +0 -0
- aethergraph/services/channel/channel_bus.py +19 -1
- aethergraph/services/channel/factory.py +13 -1
- aethergraph/services/channel/ingress.py +311 -0
- aethergraph/services/channel/queue_adapter.py +75 -0
- aethergraph/services/channel/session.py +502 -19
- aethergraph/services/container/default_container.py +122 -43
- aethergraph/services/continuations/continuation.py +6 -0
- aethergraph/services/continuations/stores/fs_store.py +19 -0
- aethergraph/services/eventhub/event_hub.py +76 -0
- aethergraph/services/kv/__init__.py +0 -0
- aethergraph/services/kv/ephemeral.py +244 -0
- aethergraph/services/llm/__init__.py +0 -0
- aethergraph/services/llm/generic_client copy.py +691 -0
- aethergraph/services/llm/generic_client.py +1288 -187
- aethergraph/services/llm/providers.py +3 -1
- aethergraph/services/llm/types.py +47 -0
- aethergraph/services/llm/utils.py +284 -0
- aethergraph/services/logger/std.py +3 -0
- aethergraph/services/mcp/__init__.py +9 -0
- aethergraph/services/mcp/http_client.py +38 -0
- aethergraph/services/mcp/service.py +225 -1
- aethergraph/services/mcp/stdio_client.py +41 -6
- aethergraph/services/mcp/ws_client.py +44 -2
- aethergraph/services/memory/__init__.py +0 -0
- aethergraph/services/memory/distillers/llm_long_term.py +234 -0
- aethergraph/services/memory/distillers/llm_meta_summary.py +398 -0
- aethergraph/services/memory/distillers/long_term.py +225 -0
- aethergraph/services/memory/facade/__init__.py +3 -0
- aethergraph/services/memory/facade/chat.py +440 -0
- aethergraph/services/memory/facade/core.py +447 -0
- aethergraph/services/memory/facade/distillation.py +424 -0
- aethergraph/services/memory/facade/rag.py +410 -0
- aethergraph/services/memory/facade/results.py +315 -0
- aethergraph/services/memory/facade/retrieval.py +139 -0
- aethergraph/services/memory/facade/types.py +77 -0
- aethergraph/services/memory/facade/utils.py +43 -0
- aethergraph/services/memory/facade_dep.py +1539 -0
- aethergraph/services/memory/factory.py +9 -3
- aethergraph/services/memory/utils.py +10 -0
- aethergraph/services/metering/eventlog_metering.py +470 -0
- aethergraph/services/metering/noop.py +25 -4
- aethergraph/services/rag/__init__.py +0 -0
- aethergraph/services/rag/facade.py +279 -23
- aethergraph/services/rag/index_factory.py +2 -2
- aethergraph/services/rag/node_rag.py +317 -0
- aethergraph/services/rate_limit/inmem_rate_limit.py +24 -0
- aethergraph/services/registry/__init__.py +0 -0
- aethergraph/services/registry/agent_app_meta.py +419 -0
- aethergraph/services/registry/registry_key.py +1 -1
- aethergraph/services/registry/unified_registry.py +74 -6
- aethergraph/services/scope/scope.py +159 -0
- aethergraph/services/scope/scope_factory.py +164 -0
- aethergraph/services/state_stores/serialize.py +5 -0
- aethergraph/services/state_stores/utils.py +2 -1
- aethergraph/services/viz/__init__.py +0 -0
- aethergraph/services/viz/facade.py +413 -0
- aethergraph/services/viz/viz_service.py +69 -0
- aethergraph/storage/artifacts/artifact_index_jsonl.py +180 -0
- aethergraph/storage/artifacts/artifact_index_sqlite.py +426 -0
- aethergraph/storage/artifacts/cas_store.py +422 -0
- aethergraph/storage/artifacts/fs_cas.py +18 -0
- aethergraph/storage/artifacts/s3_cas.py +14 -0
- aethergraph/storage/artifacts/utils.py +124 -0
- aethergraph/storage/blob/fs_blob.py +86 -0
- aethergraph/storage/blob/s3_blob.py +115 -0
- aethergraph/storage/continuation_store/fs_cont.py +283 -0
- aethergraph/storage/continuation_store/inmem_cont.py +146 -0
- aethergraph/storage/continuation_store/kvdoc_cont.py +261 -0
- aethergraph/storage/docstore/fs_doc.py +63 -0
- aethergraph/storage/docstore/sqlite_doc.py +31 -0
- aethergraph/storage/docstore/sqlite_doc_sync.py +90 -0
- aethergraph/storage/eventlog/fs_event.py +136 -0
- aethergraph/storage/eventlog/sqlite_event.py +47 -0
- aethergraph/storage/eventlog/sqlite_event_sync.py +178 -0
- aethergraph/storage/factory.py +432 -0
- aethergraph/storage/fs_utils.py +28 -0
- aethergraph/storage/graph_state_store/state_store.py +64 -0
- aethergraph/storage/kv/inmem_kv.py +103 -0
- aethergraph/storage/kv/layered_kv.py +52 -0
- aethergraph/storage/kv/sqlite_kv.py +39 -0
- aethergraph/storage/kv/sqlite_kv_sync.py +98 -0
- aethergraph/storage/memory/event_persist.py +68 -0
- aethergraph/storage/memory/fs_persist.py +118 -0
- aethergraph/{services/memory/hotlog_kv.py → storage/memory/hotlog.py} +8 -2
- aethergraph/{services → storage}/memory/indices.py +31 -7
- aethergraph/storage/metering/meter_event.py +55 -0
- aethergraph/storage/runs/doc_store.py +280 -0
- aethergraph/storage/runs/inmen_store.py +82 -0
- aethergraph/storage/runs/sqlite_run_store.py +403 -0
- aethergraph/storage/sessions/doc_store.py +183 -0
- aethergraph/storage/sessions/inmem_store.py +110 -0
- aethergraph/storage/sessions/sqlite_session_store.py +399 -0
- aethergraph/storage/vector_index/chroma_index.py +138 -0
- aethergraph/storage/vector_index/faiss_index.py +179 -0
- aethergraph/storage/vector_index/sqlite_index.py +187 -0
- {aethergraph-0.1.0a1.dist-info → aethergraph-0.1.0a2.dist-info}/METADATA +138 -31
- aethergraph-0.1.0a2.dist-info/RECORD +356 -0
- aethergraph-0.1.0a2.dist-info/entry_points.txt +3 -0
- aethergraph/services/artifacts/factory.py +0 -35
- aethergraph/services/artifacts/fs_store.py +0 -656
- aethergraph/services/artifacts/jsonl_index.py +0 -123
- aethergraph/services/artifacts/sqlite_index.py +0 -209
- aethergraph/services/memory/distillers/episode.py +0 -116
- aethergraph/services/memory/distillers/rolling.py +0 -74
- aethergraph/services/memory/facade.py +0 -633
- aethergraph/services/memory/persist_fs.py +0 -40
- aethergraph/services/rag/index/base.py +0 -27
- aethergraph/services/rag/index/faiss_index.py +0 -121
- aethergraph/services/rag/index/sqlite_index.py +0 -134
- aethergraph-0.1.0a1.dist-info/RECORD +0 -182
- aethergraph-0.1.0a1.dist-info/entry_points.txt +0 -2
- {aethergraph-0.1.0a1.dist-info → aethergraph-0.1.0a2.dist-info}/WHEEL +0 -0
- {aethergraph-0.1.0a1.dist-info → aethergraph-0.1.0a2.dist-info}/licenses/LICENSE +0 -0
- {aethergraph-0.1.0a1.dist-info → aethergraph-0.1.0a2.dist-info}/licenses/NOTICE +0 -0
- {aethergraph-0.1.0a1.dist-info → aethergraph-0.1.0a2.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])
|