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,115 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
import boto3
|
|
6
|
+
from botocore.config import Config
|
|
7
|
+
|
|
8
|
+
from aethergraph.contracts.storage.blob_store import BlobStore
|
|
9
|
+
from aethergraph.services.artifacts.utils import to_thread
|
|
10
|
+
|
|
11
|
+
"""
|
|
12
|
+
S3BlobStore: BlobStore implementation using AWS S3 as the backend.
|
|
13
|
+
NOTE: This is a stub implementation; not fully tested.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class S3BlobStore(BlobStore):
|
|
18
|
+
def __init__(self, bucket: str, prefix: str = ""):
|
|
19
|
+
self.bucket = bucket
|
|
20
|
+
self.prefix = prefix.strip("/")
|
|
21
|
+
|
|
22
|
+
# Create S3 client with default config; can be customized as needed
|
|
23
|
+
self.s3_client = boto3.client(
|
|
24
|
+
"s3", config=Config(signature_version="s3v4", max_pool_connections=50)
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def base_uri(self) -> str:
|
|
29
|
+
if self.prefix:
|
|
30
|
+
return f"s3://{self.bucket}/{self.prefix}"
|
|
31
|
+
return f"s3://{self.bucket}"
|
|
32
|
+
|
|
33
|
+
def _resolve_key(self, key: str | None, ext: str | None) -> str:
|
|
34
|
+
if key is None:
|
|
35
|
+
import uuid
|
|
36
|
+
|
|
37
|
+
key = uuid.uuid4().hex + (ext or "")
|
|
38
|
+
if self.prefix:
|
|
39
|
+
return f"{self.prefix}/{key.lstrip('/')}"
|
|
40
|
+
return key.lstrip("/")
|
|
41
|
+
|
|
42
|
+
async def put_bytes(
|
|
43
|
+
self,
|
|
44
|
+
data: bytes,
|
|
45
|
+
*,
|
|
46
|
+
key: str | None = None,
|
|
47
|
+
ext: str | None = None,
|
|
48
|
+
mime: str | None = None,
|
|
49
|
+
keep_source: bool = False, # added for interface consistency; not used here
|
|
50
|
+
) -> str:
|
|
51
|
+
key = self._resolve_key(key, ext)
|
|
52
|
+
|
|
53
|
+
def _upload():
|
|
54
|
+
extra = {}
|
|
55
|
+
if mime:
|
|
56
|
+
extra["ContentType"] = mime
|
|
57
|
+
self._client.put_object(
|
|
58
|
+
Bucket=self.bucket,
|
|
59
|
+
Key=key,
|
|
60
|
+
Body=data,
|
|
61
|
+
**extra,
|
|
62
|
+
)
|
|
63
|
+
return f"s3://{self.bucket}/{key}"
|
|
64
|
+
|
|
65
|
+
return await to_thread(_upload)
|
|
66
|
+
|
|
67
|
+
async def put_file(
|
|
68
|
+
self,
|
|
69
|
+
path: str,
|
|
70
|
+
*,
|
|
71
|
+
key: str | None = None,
|
|
72
|
+
mime: str | None = None,
|
|
73
|
+
) -> str:
|
|
74
|
+
ext = os.path.splitext(path)[1]
|
|
75
|
+
key = self._resolve_key(key, ext)
|
|
76
|
+
|
|
77
|
+
def _upload_file():
|
|
78
|
+
extra = {}
|
|
79
|
+
if mime:
|
|
80
|
+
extra["ContentType"] = mime
|
|
81
|
+
self._client.upload_file(
|
|
82
|
+
Filename=os.path.abspath(path),
|
|
83
|
+
Bucket=self.bucket,
|
|
84
|
+
Key=key,
|
|
85
|
+
ExtraArgs=extra or None,
|
|
86
|
+
)
|
|
87
|
+
return f"s3://{self.bucket}/{key}"
|
|
88
|
+
|
|
89
|
+
return await to_thread(_upload_file)
|
|
90
|
+
|
|
91
|
+
async def load_bytes(self, uri: str) -> bytes:
|
|
92
|
+
# assume s3://bucket/prefix...
|
|
93
|
+
# you can parse, or just compute key from uri by stripping base_uri
|
|
94
|
+
if uri.startswith("s3://"):
|
|
95
|
+
_, rest = uri.split("://", 1)
|
|
96
|
+
bucket, key = rest.split("/", 1)
|
|
97
|
+
else:
|
|
98
|
+
bucket = self.bucket
|
|
99
|
+
key = uri
|
|
100
|
+
|
|
101
|
+
def _download():
|
|
102
|
+
obj = self._client.get_object(Bucket=bucket, Key=key)
|
|
103
|
+
return obj["Body"].read()
|
|
104
|
+
|
|
105
|
+
return await to_thread(_download)
|
|
106
|
+
|
|
107
|
+
async def load_text(
|
|
108
|
+
self,
|
|
109
|
+
uri: str,
|
|
110
|
+
*,
|
|
111
|
+
encoding: str = "utf-8",
|
|
112
|
+
errors: str = "strict",
|
|
113
|
+
) -> str:
|
|
114
|
+
data = await self.load_bytes(uri)
|
|
115
|
+
return data.decode(encoding, errors)
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from dataclasses import asdict
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
import hashlib
|
|
7
|
+
import hmac
|
|
8
|
+
import json
|
|
9
|
+
import logging
|
|
10
|
+
import os
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
import re
|
|
13
|
+
import threading
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from aethergraph.services.continuations.continuation import Continuation, Correlator
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class FSContinuationStore: # implements AsyncContinuationStore
|
|
20
|
+
def __init__(self, root: str | Path, secret: bytes):
|
|
21
|
+
self.root = Path(root)
|
|
22
|
+
self.secret = secret
|
|
23
|
+
self._lock = threading.RLock()
|
|
24
|
+
|
|
25
|
+
# ---------- helpers ----------
|
|
26
|
+
def _cont_path(self, run_id: str, node_id: str) -> Path:
|
|
27
|
+
return self.root / "runs" / run_id / "nodes" / node_id / "continuation.json"
|
|
28
|
+
|
|
29
|
+
def _token_idx_path(self, token: str) -> Path:
|
|
30
|
+
return self.root / "index" / "tokens" / f"{token}.json"
|
|
31
|
+
|
|
32
|
+
def _rev_idx_path(self, token: str) -> Path:
|
|
33
|
+
return self.root / "index" / "rev" / f"{token}.json"
|
|
34
|
+
|
|
35
|
+
def _safe(self, s: str) -> str:
|
|
36
|
+
s = (s or "").replace("\\", "_").replace("/", "_").replace(":", "_")
|
|
37
|
+
s = re.sub(r"[^A-Za-z0-9._@-]", "_", s)
|
|
38
|
+
s = re.sub(r"_+", "_", s).strip("._ ") or "x"
|
|
39
|
+
if len(s) > 100:
|
|
40
|
+
h = hashlib.sha1(s.encode()).hexdigest()[:8]
|
|
41
|
+
s = f"{s[:92]}_{h}"
|
|
42
|
+
return s
|
|
43
|
+
|
|
44
|
+
def _corr_dir(self, scheme: str, channel: str, thread: str, message: str) -> Path:
|
|
45
|
+
return (
|
|
46
|
+
self.root
|
|
47
|
+
/ "index"
|
|
48
|
+
/ "corr"
|
|
49
|
+
/ self._safe(scheme)
|
|
50
|
+
/ self._safe(channel)
|
|
51
|
+
/ self._safe(thread)
|
|
52
|
+
/ self._safe(message)
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
def _corr_tokens_path(self, scheme: str, channel: str, thread: str, message: str) -> Path:
|
|
56
|
+
return self._corr_dir(scheme, channel, thread, message) / "tokens.json"
|
|
57
|
+
|
|
58
|
+
def _hmac(self, *parts: str) -> str:
|
|
59
|
+
h = hmac.new(self.secret, digestmod=hashlib.sha256)
|
|
60
|
+
for p in parts:
|
|
61
|
+
h.update(p.encode("utf-8"))
|
|
62
|
+
return h.hexdigest()
|
|
63
|
+
|
|
64
|
+
async def mint_token(self, run_id: str, node_id: str, attempts: int) -> str:
|
|
65
|
+
return self._hmac(run_id, node_id, str(attempts), os.urandom(8).hex())
|
|
66
|
+
|
|
67
|
+
# ---------- core ----------
|
|
68
|
+
async def save(self, cont: Continuation) -> None:
|
|
69
|
+
def _write():
|
|
70
|
+
path = self._cont_path(cont.run_id, cont.node_id)
|
|
71
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
72
|
+
payload = cont.to_dict() if hasattr(cont, "to_dict") else asdict(cont)
|
|
73
|
+
for k in ("deadline", "next_wakeup_at", "created_at"):
|
|
74
|
+
v = payload.get(k)
|
|
75
|
+
if isinstance(v, datetime):
|
|
76
|
+
payload[k] = v.astimezone(timezone.utc).isoformat()
|
|
77
|
+
tmp = path.with_suffix(".tmp")
|
|
78
|
+
tmp.write_text(json.dumps(payload, indent=2), encoding="utf-8")
|
|
79
|
+
tmp.replace(path)
|
|
80
|
+
self._write_token_index(cont.run_id, cont.node_id, cont.token)
|
|
81
|
+
|
|
82
|
+
await asyncio.to_thread(_write)
|
|
83
|
+
|
|
84
|
+
async def get(self, run_id: str, node_id: str) -> Continuation | None:
|
|
85
|
+
def _read():
|
|
86
|
+
path = self._cont_path(run_id, node_id)
|
|
87
|
+
if not path.exists():
|
|
88
|
+
return None
|
|
89
|
+
raw = json.loads(path.read_text(encoding="utf-8"))
|
|
90
|
+
for k in ("deadline", "next_wakeup_at", "created_at"):
|
|
91
|
+
if raw.get(k):
|
|
92
|
+
raw[k] = datetime.fromisoformat(raw[k])
|
|
93
|
+
raw["closed"] = bool(raw.get("closed", False))
|
|
94
|
+
return Continuation(**raw)
|
|
95
|
+
|
|
96
|
+
return await asyncio.to_thread(_read)
|
|
97
|
+
|
|
98
|
+
async def list_cont_by_run(self, run_id: str) -> list[Continuation]:
|
|
99
|
+
def _list():
|
|
100
|
+
out = []
|
|
101
|
+
run_path = self.root / "runs" / run_id / "nodes"
|
|
102
|
+
if not run_path.exists():
|
|
103
|
+
return out
|
|
104
|
+
for node_dir in run_path.iterdir():
|
|
105
|
+
cont_path = node_dir / "continuation.json"
|
|
106
|
+
if cont_path.exists():
|
|
107
|
+
raw = json.loads(cont_path.read_text(encoding="utf-8"))
|
|
108
|
+
for k in ("deadline", "next_wakeup_at", "created_at"):
|
|
109
|
+
if raw.get(k):
|
|
110
|
+
raw[k] = datetime.fromisoformat(raw[k])
|
|
111
|
+
raw["closed"] = bool(raw.get("closed", False))
|
|
112
|
+
out.append(Continuation(**raw))
|
|
113
|
+
return out
|
|
114
|
+
|
|
115
|
+
return await asyncio.to_thread(_list)
|
|
116
|
+
|
|
117
|
+
async def delete(self, run_id: str, node_id: str) -> None:
|
|
118
|
+
def _del():
|
|
119
|
+
p = self._cont_path(run_id, node_id)
|
|
120
|
+
if p.exists():
|
|
121
|
+
p.unlink()
|
|
122
|
+
|
|
123
|
+
await asyncio.to_thread(_del)
|
|
124
|
+
|
|
125
|
+
# ---------- token helpers ----------
|
|
126
|
+
def _write_token_index(self, run_id: str, node_id: str, token: str) -> None:
|
|
127
|
+
with self._lock:
|
|
128
|
+
p = self._token_idx_path(token)
|
|
129
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
130
|
+
p.write_text(
|
|
131
|
+
json.dumps({"run_id": run_id, "node_id": node_id}, indent=2), encoding="utf-8"
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
async def get_by_token(self, token: str) -> Continuation | None:
|
|
135
|
+
def _lookup():
|
|
136
|
+
p = self._token_idx_path(token)
|
|
137
|
+
if not p.exists():
|
|
138
|
+
return None
|
|
139
|
+
ref = json.loads(p.read_text(encoding="utf-8"))
|
|
140
|
+
return ref["run_id"], ref["node_id"]
|
|
141
|
+
|
|
142
|
+
ref = await asyncio.to_thread(_lookup)
|
|
143
|
+
if not ref:
|
|
144
|
+
return None
|
|
145
|
+
run_id, node_id = ref
|
|
146
|
+
return await self.get(run_id, node_id)
|
|
147
|
+
|
|
148
|
+
async def mark_closed(self, token: str) -> None:
|
|
149
|
+
c = await self.get_by_token(token)
|
|
150
|
+
if not c:
|
|
151
|
+
return
|
|
152
|
+
if not c.closed:
|
|
153
|
+
c.closed = True
|
|
154
|
+
await self.save(c)
|
|
155
|
+
|
|
156
|
+
async def verify_token(self, run_id: str, node_id: str, token: str) -> bool:
|
|
157
|
+
c = await self.get(run_id, node_id)
|
|
158
|
+
return bool(c and hmac.compare_digest(token, c.token))
|
|
159
|
+
|
|
160
|
+
# ---------- correlators ----------
|
|
161
|
+
async def bind_correlator(self, *, token: str, corr: Correlator) -> None:
|
|
162
|
+
def _bind():
|
|
163
|
+
scheme, channel, thread, message = corr.key()
|
|
164
|
+
tokens_path = self._corr_tokens_path(scheme, channel, thread, message)
|
|
165
|
+
tokens_path.parent.mkdir(parents=True, exist_ok=True)
|
|
166
|
+
toks: list[str] = []
|
|
167
|
+
if tokens_path.exists():
|
|
168
|
+
try:
|
|
169
|
+
toks = json.loads(tokens_path.read_text(encoding="utf-8"))
|
|
170
|
+
except Exception:
|
|
171
|
+
toks = []
|
|
172
|
+
if token not in toks:
|
|
173
|
+
toks.append(token)
|
|
174
|
+
tokens_path.write_text(json.dumps(toks, indent=2), encoding="utf-8")
|
|
175
|
+
# reverse index
|
|
176
|
+
r = self._rev_idx_path(token)
|
|
177
|
+
r.parent.mkdir(parents=True, exist_ok=True)
|
|
178
|
+
paths = []
|
|
179
|
+
if r.exists():
|
|
180
|
+
try:
|
|
181
|
+
paths = json.loads(r.read_text(encoding="utf-8"))
|
|
182
|
+
except Exception:
|
|
183
|
+
paths = []
|
|
184
|
+
key_path = str(tokens_path.relative_to(self.root))
|
|
185
|
+
if key_path not in paths:
|
|
186
|
+
paths.append(key_path)
|
|
187
|
+
r.write_text(json.dumps(paths, indent=2), encoding="utf-8")
|
|
188
|
+
|
|
189
|
+
await asyncio.to_thread(_bind)
|
|
190
|
+
|
|
191
|
+
async def find_by_correlator(self, *, corr: Correlator) -> Continuation | None:
|
|
192
|
+
def _read_toks():
|
|
193
|
+
scheme, channel, thread, message = corr.key()
|
|
194
|
+
p = self._corr_tokens_path(scheme, channel, thread, message)
|
|
195
|
+
if not p.exists():
|
|
196
|
+
return []
|
|
197
|
+
try:
|
|
198
|
+
return json.loads(p.read_text(encoding="utf-8")) or []
|
|
199
|
+
except Exception:
|
|
200
|
+
return []
|
|
201
|
+
|
|
202
|
+
toks = await asyncio.to_thread(_read_toks)
|
|
203
|
+
for tok in reversed(toks):
|
|
204
|
+
c = await self.get_by_token(tok)
|
|
205
|
+
if c and not c.closed:
|
|
206
|
+
if c.deadline and datetime.now(timezone.utc) > c.deadline.astimezone(timezone.utc):
|
|
207
|
+
continue
|
|
208
|
+
return c
|
|
209
|
+
return None
|
|
210
|
+
|
|
211
|
+
async def last_open(self, *, channel: str, kind: str) -> Continuation | None:
|
|
212
|
+
# Optional slow scan (dev only)
|
|
213
|
+
def _scan():
|
|
214
|
+
waits = []
|
|
215
|
+
runs_path = self.root / "runs"
|
|
216
|
+
if not runs_path.exists():
|
|
217
|
+
return waits
|
|
218
|
+
for run_dir in runs_path.iterdir():
|
|
219
|
+
nodes_dir = run_dir / "nodes"
|
|
220
|
+
if not nodes_dir.exists():
|
|
221
|
+
continue
|
|
222
|
+
for node_dir in nodes_dir.iterdir():
|
|
223
|
+
cont_path = node_dir / "continuation.json"
|
|
224
|
+
if cont_path.exists():
|
|
225
|
+
waits.append(json.loads(cont_path.read_text(encoding="utf-8")))
|
|
226
|
+
return waits
|
|
227
|
+
|
|
228
|
+
waits = await asyncio.to_thread(_scan)
|
|
229
|
+
for raw in reversed(waits):
|
|
230
|
+
if raw.get("closed"):
|
|
231
|
+
continue
|
|
232
|
+
if raw.get("channel") == channel and raw.get("kind") == kind:
|
|
233
|
+
for k in ("deadline", "next_wakeup_at", "created_at"):
|
|
234
|
+
if raw.get(k):
|
|
235
|
+
raw[k] = datetime.fromisoformat(raw[k])
|
|
236
|
+
return Continuation(**raw)
|
|
237
|
+
return None
|
|
238
|
+
|
|
239
|
+
async def list_waits(self) -> list[dict[str, Any]]:
|
|
240
|
+
def _scan():
|
|
241
|
+
out = []
|
|
242
|
+
runs_path = self.root / "runs"
|
|
243
|
+
if not runs_path.exists():
|
|
244
|
+
return out
|
|
245
|
+
for run_dir in runs_path.iterdir():
|
|
246
|
+
nodes_dir = run_dir / "nodes"
|
|
247
|
+
if not nodes_dir.exists():
|
|
248
|
+
continue
|
|
249
|
+
for node_dir in nodes_dir.iterdir():
|
|
250
|
+
cont_path = node_dir / "continuation.json"
|
|
251
|
+
if cont_path.exists():
|
|
252
|
+
out.append(json.loads(cont_path.read_text(encoding="utf-8")))
|
|
253
|
+
return out
|
|
254
|
+
|
|
255
|
+
return await asyncio.to_thread(_scan)
|
|
256
|
+
|
|
257
|
+
async def clear(self) -> None:
|
|
258
|
+
def _clear():
|
|
259
|
+
for sub in ("runs", "index"):
|
|
260
|
+
p = self.root / sub
|
|
261
|
+
if p.exists():
|
|
262
|
+
for root, dirs, files in os.walk(p, topdown=False):
|
|
263
|
+
for f in files:
|
|
264
|
+
try:
|
|
265
|
+
os.remove(Path(root) / f)
|
|
266
|
+
except Exception:
|
|
267
|
+
logger = logging.getLogger(
|
|
268
|
+
"aethergraph.services.continuations.stores.fs_store"
|
|
269
|
+
)
|
|
270
|
+
logger.warning("Failed to remove file: %s", Path(root) / f)
|
|
271
|
+
for d in dirs:
|
|
272
|
+
try:
|
|
273
|
+
os.rmdir(Path(root) / d)
|
|
274
|
+
except Exception:
|
|
275
|
+
logger = logging.getLogger(
|
|
276
|
+
"aethergraph.services.continuations.stores.fs_store"
|
|
277
|
+
)
|
|
278
|
+
logger.warning("Failed to remove dir: %s", Path(root) / d)
|
|
279
|
+
|
|
280
|
+
await asyncio.to_thread(_clear)
|
|
281
|
+
|
|
282
|
+
async def alias_for(self, token: str) -> str | None:
|
|
283
|
+
return token[:24]
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
from dataclasses import asdict
|
|
2
|
+
from datetime import datetime, timezone
|
|
3
|
+
import hashlib
|
|
4
|
+
import hmac
|
|
5
|
+
import os
|
|
6
|
+
import threading
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from aethergraph.contracts.services.continuations import AsyncContinuationStore
|
|
10
|
+
from aethergraph.services.continuations.continuation import Continuation, Correlator
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class InMemoryContinuationStore(AsyncContinuationStore):
|
|
14
|
+
"""
|
|
15
|
+
Process-local, in-memory continuation store.
|
|
16
|
+
|
|
17
|
+
- Thread-safe via RLock (main loop + sidecar can share it).
|
|
18
|
+
- No persistence beyond process lifetime.
|
|
19
|
+
|
|
20
|
+
NOTE: suitable for testing or single-process deployments only.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, *, secret: bytes):
|
|
24
|
+
self._secret = secret
|
|
25
|
+
self._by_id: dict[tuple[str, str], Continuation] = {}
|
|
26
|
+
self._by_token: dict[str, tuple[str, str]] = {}
|
|
27
|
+
self._corr_tokens: dict[str, list[str]] = {}
|
|
28
|
+
self._lock = threading.RLock()
|
|
29
|
+
|
|
30
|
+
# --- helpers ---
|
|
31
|
+
|
|
32
|
+
def _key(self, run_id: str, node_id: str) -> tuple[str, str]:
|
|
33
|
+
return (run_id, node_id)
|
|
34
|
+
|
|
35
|
+
def _corr_key(self, corr: Correlator) -> str:
|
|
36
|
+
scheme, channel, thread, message = corr.key()
|
|
37
|
+
return f"{scheme}:{channel}:{thread}:{message}"
|
|
38
|
+
|
|
39
|
+
def _hmac(self, *parts: str) -> str:
|
|
40
|
+
h = hmac.new(self._secret, digestmod=hashlib.sha256)
|
|
41
|
+
for p in parts:
|
|
42
|
+
h.update(p.encode("utf-8"))
|
|
43
|
+
return h.hexdigest()
|
|
44
|
+
|
|
45
|
+
async def mint_token(self, run_id: str, node_id: str, attempts: int) -> str:
|
|
46
|
+
return self._hmac(run_id, node_id, str(attempts), os.urandom(8).hex())
|
|
47
|
+
|
|
48
|
+
# --- core ---
|
|
49
|
+
|
|
50
|
+
async def save(self, cont: Continuation) -> None:
|
|
51
|
+
with self._lock:
|
|
52
|
+
self._by_id[self._key(cont.run_id, cont.node_id)] = cont
|
|
53
|
+
self._by_token[cont.token] = (cont.run_id, cont.node_id)
|
|
54
|
+
|
|
55
|
+
async def get(self, run_id: str, node_id: str) -> Continuation | None:
|
|
56
|
+
with self._lock:
|
|
57
|
+
return self._by_id.get(self._key(run_id, node_id))
|
|
58
|
+
|
|
59
|
+
async def delete(self, run_id: str, node_id: str) -> None:
|
|
60
|
+
with self._lock:
|
|
61
|
+
key = self._key(run_id, node_id)
|
|
62
|
+
cont = self._by_id.pop(key, None)
|
|
63
|
+
if cont:
|
|
64
|
+
self._by_token.pop(cont.token, None)
|
|
65
|
+
|
|
66
|
+
async def list_cont_by_run(self, run_id: str) -> list[Continuation]:
|
|
67
|
+
with self._lock:
|
|
68
|
+
return [c for (r, _), c in self._by_id.items() if r == run_id]
|
|
69
|
+
|
|
70
|
+
# --- token ---
|
|
71
|
+
|
|
72
|
+
async def get_by_token(self, token: str) -> Continuation | None:
|
|
73
|
+
with self._lock:
|
|
74
|
+
ref = self._by_token.get(token)
|
|
75
|
+
if not ref:
|
|
76
|
+
return None
|
|
77
|
+
run_id, node_id = ref
|
|
78
|
+
return self._by_id.get(self._key(run_id, node_id))
|
|
79
|
+
|
|
80
|
+
async def mark_closed(self, token: str) -> None:
|
|
81
|
+
c = await self.get_by_token(token)
|
|
82
|
+
if not c:
|
|
83
|
+
return
|
|
84
|
+
if not c.closed:
|
|
85
|
+
c.closed = True
|
|
86
|
+
await self.save(c)
|
|
87
|
+
|
|
88
|
+
async def verify_token(self, run_id: str, node_id: str, token: str) -> bool:
|
|
89
|
+
c = await self.get(run_id, node_id)
|
|
90
|
+
return bool(c and hmac.compare_digest(token, c.token))
|
|
91
|
+
|
|
92
|
+
# --- correlators ---
|
|
93
|
+
|
|
94
|
+
async def bind_correlator(self, *, token: str, corr: Correlator) -> None:
|
|
95
|
+
key = self._corr_key(corr)
|
|
96
|
+
with self._lock:
|
|
97
|
+
toks = self._corr_tokens.setdefault(key, [])
|
|
98
|
+
if token not in toks:
|
|
99
|
+
toks.append(token)
|
|
100
|
+
|
|
101
|
+
async def find_by_correlator(self, *, corr: Correlator) -> Continuation | None:
|
|
102
|
+
key = self._corr_key(corr)
|
|
103
|
+
with self._lock:
|
|
104
|
+
toks = list(self._corr_tokens.get(key, []))
|
|
105
|
+
|
|
106
|
+
now = datetime.now(timezone.utc)
|
|
107
|
+
for tok in reversed(toks):
|
|
108
|
+
c = await self.get_by_token(tok)
|
|
109
|
+
if not c or c.closed:
|
|
110
|
+
continue
|
|
111
|
+
if c.deadline and now > c.deadline.astimezone(timezone.utc):
|
|
112
|
+
continue
|
|
113
|
+
return c
|
|
114
|
+
return None
|
|
115
|
+
|
|
116
|
+
# --- scans ---
|
|
117
|
+
|
|
118
|
+
async def last_open(self, *, channel: str, kind: str) -> Continuation | None:
|
|
119
|
+
with self._lock:
|
|
120
|
+
candidates = [
|
|
121
|
+
c
|
|
122
|
+
for c in self._by_id.values()
|
|
123
|
+
if not c.closed and c.channel == channel and c.kind == kind
|
|
124
|
+
]
|
|
125
|
+
if not candidates:
|
|
126
|
+
return None
|
|
127
|
+
|
|
128
|
+
# pick most recent created_at
|
|
129
|
+
def _ts(c: Continuation) -> float:
|
|
130
|
+
dt = c.created_at or datetime.min.replace(tzinfo=timezone.utc)
|
|
131
|
+
return dt.timestamp()
|
|
132
|
+
|
|
133
|
+
return max(candidates, key=_ts)
|
|
134
|
+
|
|
135
|
+
async def list_waits(self) -> list[dict[str, Any]]:
|
|
136
|
+
with self._lock:
|
|
137
|
+
return [asdict(c) for c in self._by_id.values()]
|
|
138
|
+
|
|
139
|
+
async def clear(self) -> None:
|
|
140
|
+
with self._lock:
|
|
141
|
+
self._by_id.clear()
|
|
142
|
+
self._by_token.clear()
|
|
143
|
+
self._corr_tokens.clear()
|
|
144
|
+
|
|
145
|
+
async def alias_for(self, token: str) -> str | None:
|
|
146
|
+
return token[:24]
|