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.
- aethergraph/__init__.py +4 -10
- aethergraph/__main__.py +296 -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 +196 -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.0a3.dist-info}/METADATA +138 -31
- aethergraph-0.1.0a3.dist-info/RECORD +356 -0
- aethergraph-0.1.0a3.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.0a3.dist-info}/WHEEL +0 -0
- {aethergraph-0.1.0a1.dist-info → aethergraph-0.1.0a3.dist-info}/licenses/LICENSE +0 -0
- {aethergraph-0.1.0a1.dist-info → aethergraph-0.1.0a3.dist-info}/licenses/NOTICE +0 -0
- {aethergraph-0.1.0a1.dist-info → aethergraph-0.1.0a3.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
|
+
import json
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import sqlite3
|
|
7
|
+
import threading
|
|
8
|
+
import time
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
"""
|
|
12
|
+
This is not used in the main codebase; only used by async wrapper SqliteEventLog.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class SQLiteEventLogSync:
|
|
17
|
+
def __init__(self, path: str):
|
|
18
|
+
path_obj = Path(path)
|
|
19
|
+
path_obj.parent.mkdir(parents=True, exist_ok=True)
|
|
20
|
+
|
|
21
|
+
self._db = sqlite3.connect(
|
|
22
|
+
str(path_obj),
|
|
23
|
+
check_same_thread=False,
|
|
24
|
+
isolation_level=None,
|
|
25
|
+
)
|
|
26
|
+
self._lock = threading.RLock()
|
|
27
|
+
self._initialize_db()
|
|
28
|
+
|
|
29
|
+
def _initialize_db(self) -> None:
|
|
30
|
+
self._db.execute(
|
|
31
|
+
"""
|
|
32
|
+
CREATE TABLE IF NOT EXISTS events (
|
|
33
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
34
|
+
ts REAL NOT NULL,
|
|
35
|
+
scope_id TEXT,
|
|
36
|
+
kind TEXT,
|
|
37
|
+
tags_json TEXT,
|
|
38
|
+
payload TEXT NOT NULL,
|
|
39
|
+
-- new tenant / dimension columns
|
|
40
|
+
user_id TEXT,
|
|
41
|
+
org_id TEXT,
|
|
42
|
+
run_id TEXT,
|
|
43
|
+
session_id TEXT
|
|
44
|
+
)
|
|
45
|
+
"""
|
|
46
|
+
)
|
|
47
|
+
# Migration for existing DBs
|
|
48
|
+
cols = {row[1] for row in self._db.execute("PRAGMA table_info(events)").fetchall()}
|
|
49
|
+
if "user_id" not in cols:
|
|
50
|
+
self._db.execute("ALTER TABLE events ADD COLUMN user_id TEXT")
|
|
51
|
+
if "org_id" not in cols:
|
|
52
|
+
self._db.execute("ALTER TABLE events ADD COLUMN org_id TEXT")
|
|
53
|
+
if "run_id" not in cols:
|
|
54
|
+
self._db.execute("ALTER TABLE events ADD COLUMN run_id TEXT")
|
|
55
|
+
if "session_id" not in cols:
|
|
56
|
+
self._db.execute("ALTER TABLE events ADD COLUMN session_id TEXT")
|
|
57
|
+
|
|
58
|
+
# Existing indexes
|
|
59
|
+
self._db.execute("CREATE INDEX IF NOT EXISTS idx_events_scope ON events(scope_id)")
|
|
60
|
+
self._db.execute("CREATE INDEX IF NOT EXISTS idx_events_kind ON events(kind)")
|
|
61
|
+
self._db.execute("CREATE INDEX IF NOT EXISTS idx_events_ts ON events(ts)")
|
|
62
|
+
|
|
63
|
+
# tenant-aware indexes
|
|
64
|
+
self._db.execute("CREATE INDEX IF NOT EXISTS idx_events_user_ts ON events(user_id, ts)")
|
|
65
|
+
self._db.execute("CREATE INDEX IF NOT EXISTS idx_events_org_ts ON events(org_id, ts)")
|
|
66
|
+
self._db.execute("CREATE INDEX IF NOT EXISTS idx_events_run_ts ON events(run_id, ts)")
|
|
67
|
+
|
|
68
|
+
def append(self, evt: dict) -> None:
|
|
69
|
+
row = dict(evt)
|
|
70
|
+
|
|
71
|
+
ts = row.get("ts")
|
|
72
|
+
if isinstance(ts, datetime):
|
|
73
|
+
ts = ts.timestamp()
|
|
74
|
+
elif isinstance(ts, int | float):
|
|
75
|
+
ts = float(ts)
|
|
76
|
+
elif isinstance(ts, str):
|
|
77
|
+
# Handle ISO 8601 timestamps like '2025-11-27T19:48:09.758687+00:00' or ...Z
|
|
78
|
+
try:
|
|
79
|
+
s = ts.replace("Z", "+00:00") if ts.endswith("Z") else ts
|
|
80
|
+
dt = datetime.fromisoformat(s)
|
|
81
|
+
if dt.tzinfo is None:
|
|
82
|
+
dt = dt.replace(tzinfo=timezone.utc)
|
|
83
|
+
ts = dt.timestamp()
|
|
84
|
+
except Exception:
|
|
85
|
+
# Fallback: current time if we can't parse
|
|
86
|
+
ts = time.time()
|
|
87
|
+
|
|
88
|
+
if ts is None:
|
|
89
|
+
ts = time.time()
|
|
90
|
+
|
|
91
|
+
scope_id = row.get("scope_id")
|
|
92
|
+
kind = row.get("kind")
|
|
93
|
+
tags = row.get("tags") or []
|
|
94
|
+
tags_json = json.dumps(tags, ensure_ascii=False)
|
|
95
|
+
|
|
96
|
+
# tenant & run dims (not all events will have these fields. Chat events can just use session_id to retrieve info after optional authentication)
|
|
97
|
+
user_id = row.get("user_id")
|
|
98
|
+
org_id = row.get("org_id")
|
|
99
|
+
run_id = row.get("run_id")
|
|
100
|
+
session_id = row.get("session_id")
|
|
101
|
+
|
|
102
|
+
# Optionally overwrite the ts in the payload to the normalized float
|
|
103
|
+
row["ts"] = ts
|
|
104
|
+
payload = json.dumps(row, ensure_ascii=False)
|
|
105
|
+
|
|
106
|
+
with self._lock:
|
|
107
|
+
self._db.execute(
|
|
108
|
+
"""
|
|
109
|
+
INSERT INTO events (ts, scope_id, kind, tags_json, payload, user_id, org_id, run_id, session_id)
|
|
110
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
111
|
+
""",
|
|
112
|
+
(ts, scope_id, kind, tags_json, payload, user_id, org_id, run_id, session_id),
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
def query(
|
|
116
|
+
self,
|
|
117
|
+
*,
|
|
118
|
+
scope_id: str | None = None,
|
|
119
|
+
since: datetime | None = None,
|
|
120
|
+
until: datetime | None = None,
|
|
121
|
+
kinds: list[str] | None = None,
|
|
122
|
+
limit: int | None = None,
|
|
123
|
+
tags: list[str] | None = None,
|
|
124
|
+
offset: int = 0,
|
|
125
|
+
user_id: str | None = None,
|
|
126
|
+
org_id: str | None = None,
|
|
127
|
+
) -> list[dict]:
|
|
128
|
+
where: list[str] = []
|
|
129
|
+
params: list[Any] = []
|
|
130
|
+
|
|
131
|
+
if scope_id is not None:
|
|
132
|
+
where.append("scope_id = ?")
|
|
133
|
+
params.append(scope_id)
|
|
134
|
+
|
|
135
|
+
if since is not None:
|
|
136
|
+
where.append("ts >= ?")
|
|
137
|
+
params.append(since.timestamp())
|
|
138
|
+
|
|
139
|
+
if until is not None:
|
|
140
|
+
where.append("ts <= ?")
|
|
141
|
+
params.append(until.timestamp())
|
|
142
|
+
|
|
143
|
+
if kinds:
|
|
144
|
+
where.append(f"kind IN ({', '.join('?' for _ in kinds)})")
|
|
145
|
+
params.extend(kinds)
|
|
146
|
+
|
|
147
|
+
# Tenant-level filters for metering
|
|
148
|
+
if user_id is not None:
|
|
149
|
+
where.append("user_id = ?")
|
|
150
|
+
params.append(user_id)
|
|
151
|
+
if org_id is not None:
|
|
152
|
+
where.append("org_id = ?")
|
|
153
|
+
params.append(org_id)
|
|
154
|
+
|
|
155
|
+
sql = "SELECT payload, tags_json FROM events"
|
|
156
|
+
if where:
|
|
157
|
+
sql += " WHERE " + " AND ".join(where)
|
|
158
|
+
sql += " ORDER BY ts ASC"
|
|
159
|
+
|
|
160
|
+
with self._lock:
|
|
161
|
+
rows = self._db.execute(sql, params).fetchall()
|
|
162
|
+
|
|
163
|
+
tags_set = set(tags or [])
|
|
164
|
+
filtered: list[dict] = []
|
|
165
|
+
for payload_str, tags_json in rows:
|
|
166
|
+
evt = json.loads(payload_str)
|
|
167
|
+
if tags:
|
|
168
|
+
row_tags = set(json.loads(tags_json) or [])
|
|
169
|
+
if not row_tags.issuperset(tags_set):
|
|
170
|
+
continue
|
|
171
|
+
filtered.append(evt)
|
|
172
|
+
|
|
173
|
+
if offset:
|
|
174
|
+
filtered = filtered[offset:]
|
|
175
|
+
if limit is not None:
|
|
176
|
+
filtered = filtered[:limit]
|
|
177
|
+
|
|
178
|
+
return filtered
|
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from aethergraph.config.config import AppSettings, ContinuationStoreSettings
|
|
5
|
+
from aethergraph.contracts.services.continuations import AsyncContinuationStore
|
|
6
|
+
from aethergraph.contracts.services.kv import AsyncKV
|
|
7
|
+
from aethergraph.contracts.services.memory import HotLog, Indices, Persistence
|
|
8
|
+
from aethergraph.contracts.services.runs import RunStore
|
|
9
|
+
from aethergraph.contracts.services.state_stores import GraphStateStore
|
|
10
|
+
from aethergraph.contracts.storage.artifact_index import AsyncArtifactIndex
|
|
11
|
+
from aethergraph.contracts.storage.artifact_store import AsyncArtifactStore
|
|
12
|
+
from aethergraph.contracts.storage.doc_store import DocStore
|
|
13
|
+
from aethergraph.contracts.storage.event_log import EventLog
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def build_doc_store(cfg: AppSettings) -> DocStore:
|
|
17
|
+
"""
|
|
18
|
+
Global DocStore factory, used by:
|
|
19
|
+
- Memory persistence (EventLogPersistence)
|
|
20
|
+
- RAG
|
|
21
|
+
- Continuations (if you choose to share it)
|
|
22
|
+
- Anything else that wants "document-ish" JSON blobs.
|
|
23
|
+
"""
|
|
24
|
+
root = Path(cfg.root).resolve()
|
|
25
|
+
dc = cfg.storage.docs
|
|
26
|
+
|
|
27
|
+
if dc.backend == "sqlite":
|
|
28
|
+
from aethergraph.storage.docstore.sqlite_doc import SqliteDocStore
|
|
29
|
+
|
|
30
|
+
path = root / dc.sqlite_path
|
|
31
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
32
|
+
return SqliteDocStore(path=str(path))
|
|
33
|
+
|
|
34
|
+
if dc.backend == "fs":
|
|
35
|
+
from aethergraph.storage.docstore.fs_doc import FSDocStore
|
|
36
|
+
|
|
37
|
+
doc_root = root / dc.fs_dir
|
|
38
|
+
doc_root.mkdir(parents=True, exist_ok=True)
|
|
39
|
+
return FSDocStore(root=str(doc_root))
|
|
40
|
+
|
|
41
|
+
raise ValueError(f"Unknown DocStore backend: {dc.backend!r}")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def build_event_log(cfg: AppSettings, service_name: str | None = None) -> EventLog | None:
|
|
45
|
+
"""
|
|
46
|
+
Global EventLog factory.
|
|
47
|
+
Used by:
|
|
48
|
+
- GraphStateStore (if you want)
|
|
49
|
+
- Memory (EventLogPersistence)
|
|
50
|
+
- Continuations audit (optional)
|
|
51
|
+
"""
|
|
52
|
+
root = Path(cfg.root).resolve()
|
|
53
|
+
ec = cfg.storage.eventlog
|
|
54
|
+
|
|
55
|
+
if ec.backend == "none":
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
if ec.backend == "sqlite":
|
|
59
|
+
from aethergraph.storage.eventlog.sqlite_event import SqliteEventLog
|
|
60
|
+
|
|
61
|
+
# If you use a different DB file per service, you get isolation between services,
|
|
62
|
+
# but lose global querying and may have more files to manage.
|
|
63
|
+
# If you use a single DB file, all services share the same event log table(s).
|
|
64
|
+
path = root / ec.sqlite_path # You could do: root / f"{service_name}_{ec.sqlite_path}"
|
|
65
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
66
|
+
return SqliteEventLog(path=str(path))
|
|
67
|
+
|
|
68
|
+
if ec.backend == "fs":
|
|
69
|
+
from aethergraph.storage.eventlog.fs_event import FSEventLog
|
|
70
|
+
|
|
71
|
+
ev_root = root / ec.fs_dir if not service_name else root / ec.fs_dir / service_name
|
|
72
|
+
ev_root.mkdir(parents=True, exist_ok=True)
|
|
73
|
+
return FSEventLog(root=str(ev_root))
|
|
74
|
+
|
|
75
|
+
raise ValueError(f"Unknown EventLog backend: {ec.backend!r}")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def build_kv_store(cfg: AppSettings, *, extra_prefix: str = "") -> AsyncKV:
|
|
79
|
+
"""
|
|
80
|
+
Global KV factory.
|
|
81
|
+
|
|
82
|
+
extra_prefix lets subsystems (memory, continuations, etc.) add their own
|
|
83
|
+
namespace on top of the global storage.kv.prefix.
|
|
84
|
+
"""
|
|
85
|
+
root = Path(cfg.root).resolve()
|
|
86
|
+
kc = cfg.storage.kv
|
|
87
|
+
|
|
88
|
+
full_prefix = f"{kc.prefix}{extra_prefix}"
|
|
89
|
+
|
|
90
|
+
if kc.backend == "sqlite":
|
|
91
|
+
from aethergraph.storage.kv.sqlite_kv import SqliteKV
|
|
92
|
+
|
|
93
|
+
path = root / kc.sqlite_path
|
|
94
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
95
|
+
return SqliteKV(path=str(path), prefix=full_prefix)
|
|
96
|
+
|
|
97
|
+
if kc.backend == "inmem":
|
|
98
|
+
from aethergraph.storage.kv.inmem_kv import InMemoryKV
|
|
99
|
+
|
|
100
|
+
return InMemoryKV(prefix=full_prefix)
|
|
101
|
+
|
|
102
|
+
raise ValueError(f"Unknown KV backend: {kc.backend!r}")
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def build_artifact_store(cfg: AppSettings) -> AsyncArtifactStore:
|
|
106
|
+
"""
|
|
107
|
+
Decide which artifact store backend to use based on AppSettings.storage.artifacts.
|
|
108
|
+
"""
|
|
109
|
+
art_cfg = cfg.storage.artifacts
|
|
110
|
+
root = os.path.abspath(cfg.root)
|
|
111
|
+
|
|
112
|
+
if art_cfg.backend == "fs":
|
|
113
|
+
from aethergraph.storage.artifacts.fs_cas import FSArtifactStore
|
|
114
|
+
|
|
115
|
+
base_dir = os.path.join(root, art_cfg.fs.base_dir)
|
|
116
|
+
return FSArtifactStore(base_dir=base_dir)
|
|
117
|
+
|
|
118
|
+
if art_cfg.backend == "s3":
|
|
119
|
+
from aethergraph.storage.artifacts.s3_cas import (
|
|
120
|
+
S3ArtifactStore, # late import to avoid boto3 dependency if unused
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
if not art_cfg.s3.bucket:
|
|
124
|
+
raise ValueError("S3 backend selected, but STORAGE__ARTIFACTS__S3__BUCKET is empty")
|
|
125
|
+
|
|
126
|
+
staging_dir = art_cfg.s3.staging_dir
|
|
127
|
+
if not staging_dir:
|
|
128
|
+
staging_dir = os.path.join(root, ".aethergraph_tmp", "artifacts")
|
|
129
|
+
return S3ArtifactStore(
|
|
130
|
+
bucket=art_cfg.s3.bucket,
|
|
131
|
+
prefix=art_cfg.s3.prefix,
|
|
132
|
+
staging_dir=staging_dir,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
raise ValueError(f"Unknown artifacts backend: {art_cfg.backend!r}")
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def build_artifact_index(cfg: AppSettings) -> AsyncArtifactIndex:
|
|
139
|
+
idx_cfg = cfg.storage.artifact_index
|
|
140
|
+
root = os.path.abspath(cfg.root)
|
|
141
|
+
|
|
142
|
+
if idx_cfg.backend == "jsonl":
|
|
143
|
+
from aethergraph.storage.artifacts.artifact_index_jsonl import JsonlArtifactIndex
|
|
144
|
+
|
|
145
|
+
path = os.path.join(root, idx_cfg.jsonl.path)
|
|
146
|
+
occ = (
|
|
147
|
+
os.path.join(root, idx_cfg.jsonl.occurrences_path)
|
|
148
|
+
if idx_cfg.jsonl.occurrences_path
|
|
149
|
+
else None
|
|
150
|
+
)
|
|
151
|
+
return JsonlArtifactIndex(path=path, occurrences_path=occ)
|
|
152
|
+
|
|
153
|
+
if idx_cfg.backend == "sqlite":
|
|
154
|
+
from aethergraph.storage.artifacts.artifact_index_sqlite import SqliteArtifactIndex
|
|
155
|
+
|
|
156
|
+
path = os.path.join(root, idx_cfg.sqlite.path)
|
|
157
|
+
return SqliteArtifactIndex(path=path)
|
|
158
|
+
|
|
159
|
+
raise ValueError(f"Unknown artifact index backend: {idx_cfg.backend!r}")
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def build_graph_state_store(cfg: AppSettings) -> GraphStateStore:
|
|
163
|
+
from aethergraph.storage.graph_state_store.state_store import GraphStateStoreImpl
|
|
164
|
+
|
|
165
|
+
gs_cfg = cfg.storage.graph_state
|
|
166
|
+
|
|
167
|
+
if gs_cfg.backend == "fs":
|
|
168
|
+
from aethergraph.storage.docstore.fs_doc import FSDocStore
|
|
169
|
+
from aethergraph.storage.eventlog.fs_event import FSEventLog
|
|
170
|
+
|
|
171
|
+
base = os.path.join(cfg.root, gs_cfg.fs_root)
|
|
172
|
+
docs = FSDocStore(os.path.join(base, "docs"))
|
|
173
|
+
log = FSEventLog(os.path.join(base, "events"))
|
|
174
|
+
elif gs_cfg.backend == "sqlite":
|
|
175
|
+
from aethergraph.storage.docstore.sqlite_doc import SqliteDocStore
|
|
176
|
+
from aethergraph.storage.eventlog.sqlite_event import SqliteEventLog
|
|
177
|
+
|
|
178
|
+
db_path = os.path.join(cfg.root, gs_cfg.sqlite_path)
|
|
179
|
+
docs = SqliteDocStore(db_path)
|
|
180
|
+
log = SqliteEventLog(db_path)
|
|
181
|
+
else:
|
|
182
|
+
raise ValueError(f"Unknown graph_state backend: {gs_cfg.backend!r}")
|
|
183
|
+
|
|
184
|
+
return GraphStateStoreImpl(doc_store=docs, event_log=log)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def build_run_store(cfg: AppSettings) -> RunStore:
|
|
188
|
+
"""
|
|
189
|
+
Factory for RunStore:
|
|
190
|
+
|
|
191
|
+
- "memory": InMemoryRunStore (no persistence)
|
|
192
|
+
- "fs": DocRunStore on top of FSDocStore
|
|
193
|
+
- "sqlite": DocRunStore on top of SqliteDocStore
|
|
194
|
+
"""
|
|
195
|
+
rs_cfg = cfg.storage.runs
|
|
196
|
+
|
|
197
|
+
if rs_cfg.backend == "memory":
|
|
198
|
+
from aethergraph.storage.runs.inmen_store import InMemoryRunStore
|
|
199
|
+
|
|
200
|
+
return InMemoryRunStore()
|
|
201
|
+
|
|
202
|
+
if rs_cfg.backend == "fs":
|
|
203
|
+
from aethergraph.storage.docstore.fs_doc import FSDocStore
|
|
204
|
+
from aethergraph.storage.runs.doc_store import DocRunStore
|
|
205
|
+
|
|
206
|
+
base = os.path.join(cfg.root, rs_cfg.fs_root)
|
|
207
|
+
docs = FSDocStore(base)
|
|
208
|
+
return DocRunStore(
|
|
209
|
+
docs, prefix="run-"
|
|
210
|
+
) # use "run-" prefix to avoid OS path issues on Windows
|
|
211
|
+
|
|
212
|
+
if rs_cfg.backend == "sqlite":
|
|
213
|
+
from aethergraph.storage.runs.sqlite_run_store import SQLiteRunStore
|
|
214
|
+
|
|
215
|
+
db_path = os.path.join(cfg.root, rs_cfg.sqlite_path)
|
|
216
|
+
return SQLiteRunStore(path=db_path)
|
|
217
|
+
|
|
218
|
+
raise ValueError(f"Unknown run storage backend: {rs_cfg.backend!r}")
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def build_session_store(cfg: AppSettings):
|
|
222
|
+
"""
|
|
223
|
+
Factory for SessionStore:
|
|
224
|
+
|
|
225
|
+
- "memory": InMemorySessionStore (no persistence)
|
|
226
|
+
- "fs": DocSessionStore on top of FSDocStore
|
|
227
|
+
- "sqlite": DocSessionStore on top of SqliteDocStore
|
|
228
|
+
"""
|
|
229
|
+
ss_cfg = cfg.storage.sessions
|
|
230
|
+
|
|
231
|
+
if ss_cfg.backend == "memory":
|
|
232
|
+
# If you want pure dict-backed like your original snippet, keep it.
|
|
233
|
+
# Otherwise you can also implement InMemoryDocStore + DocSessionStore.
|
|
234
|
+
from aethergraph.storage.sessions.inmem_store import InMemorySessionStore
|
|
235
|
+
|
|
236
|
+
return InMemorySessionStore()
|
|
237
|
+
|
|
238
|
+
if ss_cfg.backend == "fs":
|
|
239
|
+
from aethergraph.storage.docstore.fs_doc import FSDocStore
|
|
240
|
+
from aethergraph.storage.sessions.doc_store import DocSessionStore
|
|
241
|
+
|
|
242
|
+
base = os.path.join(cfg.root, ss_cfg.fs_root)
|
|
243
|
+
docs = FSDocStore(base)
|
|
244
|
+
return DocSessionStore(docs, prefix="session-") # windows-safe
|
|
245
|
+
|
|
246
|
+
if ss_cfg.backend == "sqlite":
|
|
247
|
+
from aethergraph.storage.sessions.sqlite_session_store import SQLiteSessionStore
|
|
248
|
+
|
|
249
|
+
db_path = os.path.join(cfg.root, ss_cfg.sqlite_path)
|
|
250
|
+
return SQLiteSessionStore(path=db_path)
|
|
251
|
+
raise ValueError(f"Unknown session storage backend: {ss_cfg.backend!r}")
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _secret_bytes(secret_key: str) -> bytes:
|
|
255
|
+
# simple default; support hex/env later if needed
|
|
256
|
+
return secret_key.encode("utf-8")
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _build_kvdoc_cont_store(
|
|
260
|
+
root: Path,
|
|
261
|
+
cfg: ContinuationStoreSettings,
|
|
262
|
+
secret: bytes,
|
|
263
|
+
) -> AsyncContinuationStore:
|
|
264
|
+
kvdoc = cfg.kvdoc
|
|
265
|
+
from aethergraph.storage.continuation_store.kvdoc_cont import KVDocContinuationStore
|
|
266
|
+
|
|
267
|
+
# ---- DocStore ----
|
|
268
|
+
if kvdoc.doc_store_backend == "sqlite":
|
|
269
|
+
from aethergraph.storage.docstore.sqlite_doc import SqliteDocStore
|
|
270
|
+
|
|
271
|
+
doc_path = root / kvdoc.sqlite_doc_store_path
|
|
272
|
+
doc_path.parent.mkdir(parents=True, exist_ok=True)
|
|
273
|
+
doc_store: DocStore = SqliteDocStore(path=str(doc_path))
|
|
274
|
+
elif kvdoc.doc_store_backend == "fs":
|
|
275
|
+
from aethergraph.storage.docstore.fs_doc import FSDocStore
|
|
276
|
+
|
|
277
|
+
doc_dir = root / kvdoc.fs_doc_store_dir
|
|
278
|
+
doc_dir.mkdir(parents=True, exist_ok=True)
|
|
279
|
+
doc_store = FSDocStore(root=str(doc_dir))
|
|
280
|
+
else:
|
|
281
|
+
raise ValueError(f"Unknown doc_store_backend: {kvdoc.doc_store_backend}")
|
|
282
|
+
|
|
283
|
+
# ---- KV ----
|
|
284
|
+
if kvdoc.kv_backend == "sqlite":
|
|
285
|
+
from aethergraph.storage.kv.sqlite_kv import SqliteKV
|
|
286
|
+
|
|
287
|
+
kv_path = root / kvdoc.sqlite_kv_path
|
|
288
|
+
kv_path.parent.mkdir(parents=True, exist_ok=True)
|
|
289
|
+
kv: AsyncKV = SqliteKV(path=str(kv_path), prefix=f"{cfg.namespace}:")
|
|
290
|
+
elif kvdoc.kv_backend == "inmem":
|
|
291
|
+
from aethergraph.storage.kv.inmem_kv import InMemoryKV
|
|
292
|
+
|
|
293
|
+
kv = InMemoryKV(prefix=f"{cfg.namespace}:")
|
|
294
|
+
else:
|
|
295
|
+
raise ValueError(f"Unknown kv_backend: {kvdoc.kv_backend}")
|
|
296
|
+
|
|
297
|
+
# ---- EventLog (optional) ----
|
|
298
|
+
event_log: EventLog | None
|
|
299
|
+
if kvdoc.eventlog_backend == "none":
|
|
300
|
+
event_log = None
|
|
301
|
+
elif kvdoc.eventlog_backend == "sqlite":
|
|
302
|
+
from aethergraph.storage.eventlog.sqlite_event import SqliteEventLog
|
|
303
|
+
|
|
304
|
+
ev_path = root / kvdoc.sqlite_eventlog_path
|
|
305
|
+
ev_path.parent.mkdir(parents=True, exist_ok=True)
|
|
306
|
+
event_log = SqliteEventLog(path=str(ev_path))
|
|
307
|
+
elif kvdoc.eventlog_backend == "fs":
|
|
308
|
+
from aethergraph.storage.eventlog.fs_event import FSEventLog
|
|
309
|
+
|
|
310
|
+
ev_dir = root / kvdoc.fs_eventlog_dir
|
|
311
|
+
ev_dir.mkdir(parents=True, exist_ok=True)
|
|
312
|
+
event_log = FSEventLog(root=str(ev_dir))
|
|
313
|
+
else:
|
|
314
|
+
raise ValueError(f"Unknown eventlog_backend: {kvdoc.eventlog_backend}")
|
|
315
|
+
|
|
316
|
+
return KVDocContinuationStore(
|
|
317
|
+
doc_store=doc_store,
|
|
318
|
+
kv=kv,
|
|
319
|
+
event_log=event_log,
|
|
320
|
+
secret=secret,
|
|
321
|
+
namespace=cfg.namespace,
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def build_continuation_store(cfg: AppSettings) -> AsyncContinuationStore:
|
|
326
|
+
"""
|
|
327
|
+
High-level factory used by your runtime builder.
|
|
328
|
+
|
|
329
|
+
Mirrors `build_artifact_store(cfg)` in style.
|
|
330
|
+
"""
|
|
331
|
+
root = Path(cfg.root).resolve()
|
|
332
|
+
cont_cfg: ContinuationStoreSettings = cfg.storage.continuation
|
|
333
|
+
secret = _secret_bytes(cont_cfg.secret_key)
|
|
334
|
+
|
|
335
|
+
if cont_cfg.backend == "memory":
|
|
336
|
+
from aethergraph.services.continuations.stores.inmem_store import InMemoryContinuationStore
|
|
337
|
+
|
|
338
|
+
return InMemoryContinuationStore(secret=secret)
|
|
339
|
+
|
|
340
|
+
if cont_cfg.backend == "fs":
|
|
341
|
+
from aethergraph.services.continuations.stores.fs_store import FSContinuationStore
|
|
342
|
+
|
|
343
|
+
# Keep old FS behavior for people who rely on on-disk layout.
|
|
344
|
+
fs_root = root / cont_cfg.fs.root
|
|
345
|
+
fs_root.parent.mkdir(parents=True, exist_ok=True)
|
|
346
|
+
return FSContinuationStore(root=fs_root, secret=secret)
|
|
347
|
+
|
|
348
|
+
if cont_cfg.backend == "kvdoc":
|
|
349
|
+
return _build_kvdoc_cont_store(root, cont_cfg, secret)
|
|
350
|
+
|
|
351
|
+
raise ValueError(f"Unknown continuation backend: {cont_cfg.backend}")
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def build_vector_index(cfg: AppSettings):
|
|
355
|
+
"""
|
|
356
|
+
Build a VectorIndex based on cfg.storage.vector_index.
|
|
357
|
+
"""
|
|
358
|
+
vcfg = cfg.storage.vector_index
|
|
359
|
+
root = os.path.abspath(cfg.root)
|
|
360
|
+
|
|
361
|
+
if vcfg.backend == "sqlite":
|
|
362
|
+
from aethergraph.storage.vector_index.sqlite_index import SQLiteVectorIndex
|
|
363
|
+
|
|
364
|
+
index_root = os.path.join(root, vcfg.sqlite.dir)
|
|
365
|
+
return SQLiteVectorIndex(root=index_root)
|
|
366
|
+
|
|
367
|
+
if vcfg.backend == "faiss":
|
|
368
|
+
from aethergraph.storage.vector_index.faiss_index import FAISSVectorIndex
|
|
369
|
+
|
|
370
|
+
index_root = os.path.join(root, vcfg.faiss.dir)
|
|
371
|
+
return FAISSVectorIndex(root=index_root, dim=vcfg.faiss.dim)
|
|
372
|
+
|
|
373
|
+
if vcfg.backend == "chroma":
|
|
374
|
+
try:
|
|
375
|
+
import chromadb
|
|
376
|
+
except ImportError as e:
|
|
377
|
+
chromadb = None # type: ignore
|
|
378
|
+
raise RuntimeError("Chroma backend requires `chromadb` to be installed.") from e
|
|
379
|
+
from aethergraph.storage.vector_index.chroma_index import ChromaVectorIndex
|
|
380
|
+
|
|
381
|
+
if chromadb is None:
|
|
382
|
+
raise RuntimeError(
|
|
383
|
+
"Chroma backend selected, but `chromadb` is not installed. "
|
|
384
|
+
"Install it with `pip install chromadb`."
|
|
385
|
+
)
|
|
386
|
+
persist_dir = os.path.join(root, vcfg.chroma.persist_dir)
|
|
387
|
+
client = chromadb.PersistentClient(path=persist_dir)
|
|
388
|
+
return ChromaVectorIndex(
|
|
389
|
+
client=client,
|
|
390
|
+
collection_prefix=vcfg.chroma.collection_prefix,
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
raise ValueError(f"Unknown vector index backend: {vcfg.backend!r}")
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def build_memory_persistence(cfg: AppSettings) -> Persistence:
|
|
397
|
+
mp = cfg.storage.memory.persistence
|
|
398
|
+
root = cfg.root
|
|
399
|
+
|
|
400
|
+
if mp.backend == "fs":
|
|
401
|
+
from aethergraph.storage.memory.fs_persist import FSPersistence
|
|
402
|
+
|
|
403
|
+
return FSPersistence(base_dir=root)
|
|
404
|
+
|
|
405
|
+
if mp.backend == "eventlog":
|
|
406
|
+
from aethergraph.storage.memory.event_persist import EventLogPersistence
|
|
407
|
+
|
|
408
|
+
docs = build_doc_store(cfg)
|
|
409
|
+
log = build_event_log(cfg)
|
|
410
|
+
if log is None:
|
|
411
|
+
raise ValueError("memory.persistence.backend=eventlog requires a non-none EventLog")
|
|
412
|
+
return EventLogPersistence(
|
|
413
|
+
log=log,
|
|
414
|
+
docs=docs,
|
|
415
|
+
uri_prefix=mp.uri_prefix,
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
raise ValueError(f"Unknown memory persistence backend: {mp.backend!r}")
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def build_memory_hotlog(cfg: AppSettings) -> HotLog:
|
|
422
|
+
from aethergraph.storage.memory.hotlog import KVHotLog
|
|
423
|
+
|
|
424
|
+
kv = build_kv_store(cfg, extra_prefix="mem:hot:")
|
|
425
|
+
return KVHotLog(kv=kv)
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def build_memory_indices(cfg: AppSettings) -> Indices:
|
|
429
|
+
from aethergraph.storage.memory.indices import KVIndices
|
|
430
|
+
|
|
431
|
+
kv = build_kv_store(cfg, extra_prefix="mem:idx:")
|
|
432
|
+
return KVIndices(kv=kv, hot_ttl_s=cfg.storage.memory.indices.ttl_s)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from urllib.parse import unquote, urlparse
|
|
4
|
+
from urllib.request import url2pathname
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
async def to_thread(fn, *a, **k):
|
|
8
|
+
return await asyncio.to_thread(fn, *a, **k)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _to_file_uri(path_str: str) -> str:
|
|
12
|
+
"""Canonical RFC-8089 file URI (file:///C:/..., forward slashes)."""
|
|
13
|
+
return Path(path_str).resolve().as_uri()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _from_uri_or_path(s: str) -> Path:
|
|
17
|
+
"""Robustly turn a file:// URI or plain path into a local Path."""
|
|
18
|
+
if "://" not in s:
|
|
19
|
+
return Path(s)
|
|
20
|
+
u = urlparse(s)
|
|
21
|
+
if (u.scheme or "").lower() != "file":
|
|
22
|
+
raise ValueError(f"Unsupported URI scheme: {u.scheme}")
|
|
23
|
+
# if u.netloc:
|
|
24
|
+
# raw = f"//{u.netloc}{u.path}" # UNC: file://server/share/...
|
|
25
|
+
# else:
|
|
26
|
+
# raw = u.path # Local drive: file:///C:/...
|
|
27
|
+
raw = f"//{u.netloc}{u.path}" if u.netloc else u.path
|
|
28
|
+
return Path(url2pathname(unquote(raw)))
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import time
|
|
2
|
+
|
|
3
|
+
from aethergraph.contracts.services.state_stores import GraphSnapshot, GraphStateStore, StateEvent
|
|
4
|
+
from aethergraph.contracts.storage.doc_store import DocStore
|
|
5
|
+
from aethergraph.contracts.storage.event_log import EventLog
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class GraphStateStoreImpl(GraphStateStore):
|
|
9
|
+
"""
|
|
10
|
+
Generic GraphStateStore implementation that combines a DocStore for snapshots
|
|
11
|
+
- DocStore for storing GraphSnapshot documents
|
|
12
|
+
- EventLog for storing StateEvent logs
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def __init__(self, *, doc_store: "DocStore", event_log: "EventLog"):
|
|
16
|
+
self._docs = doc_store
|
|
17
|
+
self._log = event_log
|
|
18
|
+
|
|
19
|
+
def _snapshot_id(self, run_id: str) -> str:
|
|
20
|
+
return f"graph_state/{run_id}/latest"
|
|
21
|
+
|
|
22
|
+
async def save_snapshot(self, snap: GraphSnapshot) -> None:
|
|
23
|
+
# TODO: consider add history of snapshots by rev/timestamp
|
|
24
|
+
# e.g. hist_id = f"graph_state/{run_id}/rev_{snap.rev:08d}_{int(snap.created_at)}"
|
|
25
|
+
# self._docs.put(hist_id, snap.__dict__)
|
|
26
|
+
# but this is not needed for retrieval of latest snapshot
|
|
27
|
+
await self._docs.put(self._snapshot_id(snap.run_id), snap.__dict__)
|
|
28
|
+
|
|
29
|
+
async def load_latest_snapshot(self, run_id) -> GraphSnapshot | None:
|
|
30
|
+
# The saved snapshot is always the latest so just fetch by fixed id
|
|
31
|
+
doc = await self._docs.get(self._snapshot_id(run_id))
|
|
32
|
+
return GraphSnapshot(**doc) if doc else None
|
|
33
|
+
|
|
34
|
+
async def append_event(self, ev: StateEvent) -> None:
|
|
35
|
+
# standard event log append
|
|
36
|
+
payload = ev.__dict__.copy()
|
|
37
|
+
payload.setdefault("scope_id", ev.run_id)
|
|
38
|
+
payload.setdefault("kind", "graph_state")
|
|
39
|
+
payload.setdefault("ts", time.time())
|
|
40
|
+
await self._log.append(payload)
|
|
41
|
+
|
|
42
|
+
async def load_events_since(self, run_id, from_rev) -> list[StateEvent]:
|
|
43
|
+
rows = await self._log.query(
|
|
44
|
+
scope_id=run_id,
|
|
45
|
+
kinds=["graph_state"],
|
|
46
|
+
# from_rev filter will be applied below
|
|
47
|
+
)
|
|
48
|
+
out = []
|
|
49
|
+
for row in rows:
|
|
50
|
+
if row.get("rev", -1) > from_rev:
|
|
51
|
+
out.append(StateEvent(**row))
|
|
52
|
+
return out
|
|
53
|
+
|
|
54
|
+
async def list_run_ids(self, graph_id: str | None = None) -> list[str]:
|
|
55
|
+
# Basic version: ask DocStore for all ids and filter. This is sufficient for local/file-based stores.
|
|
56
|
+
# In cloud implementations, this should be optimized with proper indexing
|
|
57
|
+
ids = await self._docs.list()
|
|
58
|
+
runs: set[str] = set()
|
|
59
|
+
for doc_id in ids:
|
|
60
|
+
if not doc_id.startswith("graph_state/"):
|
|
61
|
+
continue
|
|
62
|
+
_, run_id, *_ = doc_id.split("/")
|
|
63
|
+
runs.add(run_id)
|
|
64
|
+
return sorted(runs)
|