aethergraph 0.1.0a1__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 +49 -0
- aethergraph/config/__init__.py +0 -0
- aethergraph/config/config.py +121 -0
- aethergraph/config/context.py +16 -0
- aethergraph/config/llm.py +26 -0
- aethergraph/config/loader.py +60 -0
- aethergraph/config/runtime.py +9 -0
- aethergraph/contracts/errors/errors.py +44 -0
- aethergraph/contracts/services/artifacts.py +142 -0
- aethergraph/contracts/services/channel.py +72 -0
- aethergraph/contracts/services/continuations.py +23 -0
- aethergraph/contracts/services/eventbus.py +12 -0
- aethergraph/contracts/services/kv.py +24 -0
- aethergraph/contracts/services/llm.py +17 -0
- aethergraph/contracts/services/mcp.py +22 -0
- aethergraph/contracts/services/memory.py +108 -0
- aethergraph/contracts/services/resume.py +28 -0
- aethergraph/contracts/services/state_stores.py +33 -0
- aethergraph/contracts/services/wakeup.py +28 -0
- aethergraph/core/execution/base_scheduler.py +77 -0
- aethergraph/core/execution/forward_scheduler.py +777 -0
- aethergraph/core/execution/global_scheduler.py +634 -0
- aethergraph/core/execution/retry_policy.py +22 -0
- aethergraph/core/execution/step_forward.py +411 -0
- aethergraph/core/execution/step_result.py +18 -0
- aethergraph/core/execution/wait_types.py +72 -0
- aethergraph/core/graph/graph_builder.py +192 -0
- aethergraph/core/graph/graph_fn.py +219 -0
- aethergraph/core/graph/graph_io.py +67 -0
- aethergraph/core/graph/graph_refs.py +154 -0
- aethergraph/core/graph/graph_spec.py +115 -0
- aethergraph/core/graph/graph_state.py +59 -0
- aethergraph/core/graph/graphify.py +128 -0
- aethergraph/core/graph/interpreter.py +145 -0
- aethergraph/core/graph/node_handle.py +33 -0
- aethergraph/core/graph/node_spec.py +46 -0
- aethergraph/core/graph/node_state.py +63 -0
- aethergraph/core/graph/task_graph.py +747 -0
- aethergraph/core/graph/task_node.py +82 -0
- aethergraph/core/graph/utils.py +37 -0
- aethergraph/core/graph/visualize.py +239 -0
- aethergraph/core/runtime/ad_hoc_context.py +61 -0
- aethergraph/core/runtime/base_service.py +153 -0
- aethergraph/core/runtime/bind_adapter.py +42 -0
- aethergraph/core/runtime/bound_memory.py +69 -0
- aethergraph/core/runtime/execution_context.py +220 -0
- aethergraph/core/runtime/graph_runner.py +349 -0
- aethergraph/core/runtime/lifecycle.py +26 -0
- aethergraph/core/runtime/node_context.py +203 -0
- aethergraph/core/runtime/node_services.py +30 -0
- aethergraph/core/runtime/recovery.py +159 -0
- aethergraph/core/runtime/run_registration.py +33 -0
- aethergraph/core/runtime/runtime_env.py +157 -0
- aethergraph/core/runtime/runtime_registry.py +32 -0
- aethergraph/core/runtime/runtime_services.py +224 -0
- aethergraph/core/runtime/wakeup_watcher.py +40 -0
- aethergraph/core/tools/__init__.py +10 -0
- aethergraph/core/tools/builtins/channel_tools.py +194 -0
- aethergraph/core/tools/builtins/toolset.py +134 -0
- aethergraph/core/tools/toolkit.py +510 -0
- aethergraph/core/tools/waitable.py +109 -0
- aethergraph/plugins/channel/__init__.py +0 -0
- aethergraph/plugins/channel/adapters/__init__.py +0 -0
- aethergraph/plugins/channel/adapters/console.py +106 -0
- aethergraph/plugins/channel/adapters/file.py +102 -0
- aethergraph/plugins/channel/adapters/slack.py +285 -0
- aethergraph/plugins/channel/adapters/telegram.py +302 -0
- aethergraph/plugins/channel/adapters/webhook.py +104 -0
- aethergraph/plugins/channel/adapters/webui.py +134 -0
- aethergraph/plugins/channel/routes/__init__.py +0 -0
- aethergraph/plugins/channel/routes/console_routes.py +86 -0
- aethergraph/plugins/channel/routes/slack_routes.py +49 -0
- aethergraph/plugins/channel/routes/telegram_routes.py +26 -0
- aethergraph/plugins/channel/routes/webui_routes.py +136 -0
- aethergraph/plugins/channel/utils/__init__.py +0 -0
- aethergraph/plugins/channel/utils/slack_utils.py +278 -0
- aethergraph/plugins/channel/utils/telegram_utils.py +324 -0
- aethergraph/plugins/channel/websockets/slack_ws.py +68 -0
- aethergraph/plugins/channel/websockets/telegram_polling.py +151 -0
- aethergraph/plugins/mcp/fs_server.py +128 -0
- aethergraph/plugins/mcp/http_server.py +101 -0
- aethergraph/plugins/mcp/ws_server.py +180 -0
- aethergraph/plugins/net/http.py +10 -0
- aethergraph/plugins/utils/data_io.py +359 -0
- aethergraph/runner/__init__.py +5 -0
- aethergraph/runtime/__init__.py +62 -0
- aethergraph/server/__init__.py +3 -0
- aethergraph/server/app_factory.py +84 -0
- aethergraph/server/start.py +122 -0
- aethergraph/services/__init__.py +10 -0
- aethergraph/services/artifacts/facade.py +284 -0
- aethergraph/services/artifacts/factory.py +35 -0
- aethergraph/services/artifacts/fs_store.py +656 -0
- aethergraph/services/artifacts/jsonl_index.py +123 -0
- aethergraph/services/artifacts/paths.py +23 -0
- aethergraph/services/artifacts/sqlite_index.py +209 -0
- aethergraph/services/artifacts/utils.py +124 -0
- aethergraph/services/auth/dev.py +16 -0
- aethergraph/services/channel/channel_bus.py +293 -0
- aethergraph/services/channel/factory.py +44 -0
- aethergraph/services/channel/session.py +511 -0
- aethergraph/services/channel/wait_helpers.py +57 -0
- aethergraph/services/clock/clock.py +9 -0
- aethergraph/services/container/default_container.py +320 -0
- aethergraph/services/continuations/continuation.py +56 -0
- aethergraph/services/continuations/factory.py +34 -0
- aethergraph/services/continuations/stores/fs_store.py +264 -0
- aethergraph/services/continuations/stores/inmem_store.py +95 -0
- aethergraph/services/eventbus/inmem.py +21 -0
- aethergraph/services/features/static.py +10 -0
- aethergraph/services/kv/ephemeral.py +90 -0
- aethergraph/services/kv/factory.py +27 -0
- aethergraph/services/kv/layered.py +41 -0
- aethergraph/services/kv/sqlite_kv.py +128 -0
- aethergraph/services/llm/factory.py +157 -0
- aethergraph/services/llm/generic_client.py +542 -0
- aethergraph/services/llm/providers.py +3 -0
- aethergraph/services/llm/service.py +105 -0
- aethergraph/services/logger/base.py +36 -0
- aethergraph/services/logger/compat.py +50 -0
- aethergraph/services/logger/formatters.py +106 -0
- aethergraph/services/logger/std.py +203 -0
- aethergraph/services/mcp/helpers.py +23 -0
- aethergraph/services/mcp/http_client.py +70 -0
- aethergraph/services/mcp/mcp_tools.py +21 -0
- aethergraph/services/mcp/registry.py +14 -0
- aethergraph/services/mcp/service.py +100 -0
- aethergraph/services/mcp/stdio_client.py +70 -0
- aethergraph/services/mcp/ws_client.py +115 -0
- aethergraph/services/memory/bound.py +106 -0
- aethergraph/services/memory/distillers/episode.py +116 -0
- aethergraph/services/memory/distillers/rolling.py +74 -0
- aethergraph/services/memory/facade.py +633 -0
- aethergraph/services/memory/factory.py +78 -0
- aethergraph/services/memory/hotlog_kv.py +27 -0
- aethergraph/services/memory/indices.py +74 -0
- aethergraph/services/memory/io_helpers.py +72 -0
- aethergraph/services/memory/persist_fs.py +40 -0
- aethergraph/services/memory/resolver.py +152 -0
- aethergraph/services/metering/noop.py +4 -0
- aethergraph/services/prompts/file_store.py +41 -0
- aethergraph/services/rag/chunker.py +29 -0
- aethergraph/services/rag/facade.py +593 -0
- aethergraph/services/rag/index/base.py +27 -0
- aethergraph/services/rag/index/faiss_index.py +121 -0
- aethergraph/services/rag/index/sqlite_index.py +134 -0
- aethergraph/services/rag/index_factory.py +52 -0
- aethergraph/services/rag/parsers/md.py +7 -0
- aethergraph/services/rag/parsers/pdf.py +14 -0
- aethergraph/services/rag/parsers/txt.py +7 -0
- aethergraph/services/rag/utils/hybrid.py +39 -0
- aethergraph/services/rag/utils/make_fs_key.py +62 -0
- aethergraph/services/redactor/simple.py +16 -0
- aethergraph/services/registry/key_parsing.py +44 -0
- aethergraph/services/registry/registry_key.py +19 -0
- aethergraph/services/registry/unified_registry.py +185 -0
- aethergraph/services/resume/multi_scheduler_resume_bus.py +65 -0
- aethergraph/services/resume/router.py +73 -0
- aethergraph/services/schedulers/registry.py +41 -0
- aethergraph/services/secrets/base.py +7 -0
- aethergraph/services/secrets/env.py +8 -0
- aethergraph/services/state_stores/externalize.py +135 -0
- aethergraph/services/state_stores/graph_observer.py +131 -0
- aethergraph/services/state_stores/json_store.py +67 -0
- aethergraph/services/state_stores/resume_policy.py +119 -0
- aethergraph/services/state_stores/serialize.py +249 -0
- aethergraph/services/state_stores/utils.py +91 -0
- aethergraph/services/state_stores/validate.py +78 -0
- aethergraph/services/tracing/noop.py +18 -0
- aethergraph/services/waits/wait_registry.py +91 -0
- aethergraph/services/wakeup/memory_queue.py +57 -0
- aethergraph/services/wakeup/scanner_producer.py +56 -0
- aethergraph/services/wakeup/worker.py +31 -0
- aethergraph/tools/__init__.py +25 -0
- aethergraph/utils/optdeps.py +8 -0
- aethergraph-0.1.0a1.dist-info/METADATA +410 -0
- aethergraph-0.1.0a1.dist-info/RECORD +182 -0
- aethergraph-0.1.0a1.dist-info/WHEEL +5 -0
- aethergraph-0.1.0a1.dist-info/entry_points.txt +2 -0
- aethergraph-0.1.0a1.dist-info/licenses/LICENSE +176 -0
- aethergraph-0.1.0a1.dist-info/licenses/NOTICE +31 -0
- aethergraph-0.1.0a1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
# ---- core services ----
|
|
9
|
+
from aethergraph.config.config import AppSettings
|
|
10
|
+
|
|
11
|
+
# ---- optional services (not used by default) ----
|
|
12
|
+
from aethergraph.contracts.services.llm import LLMClientProtocol
|
|
13
|
+
|
|
14
|
+
# ---- scheduler ---- TODO: move to a separate server to handle scheduling across threads/processes
|
|
15
|
+
from aethergraph.contracts.services.state_stores import GraphStateStore
|
|
16
|
+
from aethergraph.core.execution.global_scheduler import GlobalForwardScheduler
|
|
17
|
+
|
|
18
|
+
# ---- artifact services ----
|
|
19
|
+
from aethergraph.services.artifacts.fs_store import FSArtifactStore # AsyncArtifactStore
|
|
20
|
+
from aethergraph.services.artifacts.jsonl_index import JsonlArtifactIndex # AsyncArtifactIndex
|
|
21
|
+
from aethergraph.services.auth.dev import AllowAllAuthz, DevTokenAuthn
|
|
22
|
+
from aethergraph.services.channel.channel_bus import ChannelBus
|
|
23
|
+
|
|
24
|
+
# ---- channel services ----
|
|
25
|
+
from aethergraph.services.channel.factory import build_bus, make_channel_adapters_from_env
|
|
26
|
+
from aethergraph.services.clock.clock import SystemClock
|
|
27
|
+
from aethergraph.services.continuations.stores.fs_store import (
|
|
28
|
+
FSContinuationStore, # AsyncContinuationStore
|
|
29
|
+
)
|
|
30
|
+
from aethergraph.services.eventbus.inmem import InMemoryEventBus
|
|
31
|
+
|
|
32
|
+
# ---- kv services ----
|
|
33
|
+
from aethergraph.services.kv.ephemeral import EphemeralKV
|
|
34
|
+
from aethergraph.services.kv.sqlite_kv import SQLiteKV
|
|
35
|
+
from aethergraph.services.llm.factory import build_llm_clients
|
|
36
|
+
from aethergraph.services.llm.service import LLMService
|
|
37
|
+
from aethergraph.services.logger.std import LoggingConfig, StdLoggerService
|
|
38
|
+
from aethergraph.services.mcp.service import MCPService
|
|
39
|
+
|
|
40
|
+
# ---- memory services ----
|
|
41
|
+
from aethergraph.services.memory.factory import MemoryFactory
|
|
42
|
+
from aethergraph.services.memory.hotlog_kv import KVHotLog
|
|
43
|
+
from aethergraph.services.memory.indices import KVIndices
|
|
44
|
+
from aethergraph.services.memory.persist_fs import FSPersistence
|
|
45
|
+
from aethergraph.services.metering.noop import NoopMetering
|
|
46
|
+
from aethergraph.services.prompts.file_store import FilePromptStore
|
|
47
|
+
from aethergraph.services.rag.chunker import TextSplitter
|
|
48
|
+
from aethergraph.services.rag.facade import RAGFacade
|
|
49
|
+
|
|
50
|
+
# ---- RAG components ----
|
|
51
|
+
from aethergraph.services.rag.index_factory import create_vector_index
|
|
52
|
+
from aethergraph.services.redactor.simple import RegexRedactor # Simple PII redactor
|
|
53
|
+
from aethergraph.services.registry.unified_registry import UnifiedRegistry
|
|
54
|
+
from aethergraph.services.resume.multi_scheduler_resume_bus import MultiSchedulerResumeBus
|
|
55
|
+
from aethergraph.services.resume.router import ResumeRouter
|
|
56
|
+
from aethergraph.services.schedulers.registry import SchedulerRegistry
|
|
57
|
+
from aethergraph.services.secrets.env import EnvSecrets
|
|
58
|
+
from aethergraph.services.state_stores.json_store import JsonGraphStateStore
|
|
59
|
+
from aethergraph.services.tracing.noop import NoopTracer
|
|
60
|
+
from aethergraph.services.waits.wait_registry import WaitRegistry
|
|
61
|
+
from aethergraph.services.wakeup.memory_queue import ThreadSafeWakeupQueue
|
|
62
|
+
|
|
63
|
+
SERVICE_KEYS = [
|
|
64
|
+
# core
|
|
65
|
+
"registry",
|
|
66
|
+
"logger",
|
|
67
|
+
"clock",
|
|
68
|
+
"channels",
|
|
69
|
+
# continuations and resume
|
|
70
|
+
"cont_store",
|
|
71
|
+
"sched_registry",
|
|
72
|
+
"wait_registry",
|
|
73
|
+
"resume_bus",
|
|
74
|
+
"resume_router",
|
|
75
|
+
"wakeup_queue",
|
|
76
|
+
# storage and artifacts
|
|
77
|
+
"kv_hot",
|
|
78
|
+
"kv_durable",
|
|
79
|
+
"artifacts",
|
|
80
|
+
"artifact_index",
|
|
81
|
+
# memory
|
|
82
|
+
"memory_factory",
|
|
83
|
+
# optional
|
|
84
|
+
"llm",
|
|
85
|
+
"event_bus",
|
|
86
|
+
"prompts",
|
|
87
|
+
"authn",
|
|
88
|
+
"authz",
|
|
89
|
+
"redactor",
|
|
90
|
+
"metering",
|
|
91
|
+
"tracer",
|
|
92
|
+
"secrets",
|
|
93
|
+
]
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@dataclass
|
|
97
|
+
class DefaultContainer:
|
|
98
|
+
# root
|
|
99
|
+
root: str
|
|
100
|
+
|
|
101
|
+
# schedulers
|
|
102
|
+
schedulers: dict[str, Any]
|
|
103
|
+
|
|
104
|
+
# core
|
|
105
|
+
registry: UnifiedRegistry
|
|
106
|
+
logger: StdLoggerService
|
|
107
|
+
clock: SystemClock
|
|
108
|
+
|
|
109
|
+
# channels and interactions
|
|
110
|
+
channels: ChannelBus
|
|
111
|
+
|
|
112
|
+
# continuations and resume
|
|
113
|
+
cont_store: FSContinuationStore
|
|
114
|
+
sched_registry: SchedulerRegistry
|
|
115
|
+
wait_registry: WaitRegistry
|
|
116
|
+
resume_bus: MultiSchedulerResumeBus
|
|
117
|
+
resume_router: ResumeRouter
|
|
118
|
+
wakeup_queue: ThreadSafeWakeupQueue
|
|
119
|
+
state_store: GraphStateStore
|
|
120
|
+
|
|
121
|
+
# storage and artifacts
|
|
122
|
+
kv_hot: EphemeralKV
|
|
123
|
+
kv_durable: SQLiteKV
|
|
124
|
+
artifacts: FSArtifactStore
|
|
125
|
+
artifact_index: JsonlArtifactIndex
|
|
126
|
+
|
|
127
|
+
# memory
|
|
128
|
+
memory_factory: MemoryFactory
|
|
129
|
+
|
|
130
|
+
# optional llm service
|
|
131
|
+
llm: LLMClientProtocol | None = None
|
|
132
|
+
rag: RAGFacade | None = None
|
|
133
|
+
mcp: MCPService | None = None
|
|
134
|
+
|
|
135
|
+
# optional services (not used by default)
|
|
136
|
+
event_bus: InMemoryEventBus | None = None
|
|
137
|
+
prompts: FilePromptStore | None = None
|
|
138
|
+
authn: DevTokenAuthn | None = None
|
|
139
|
+
authz: AllowAllAuthz | None = None
|
|
140
|
+
redactor: RegexRedactor | None = None
|
|
141
|
+
metering: NoopMetering | None = None
|
|
142
|
+
tracer: NoopTracer | None = None
|
|
143
|
+
secrets: EnvSecrets | None = None
|
|
144
|
+
|
|
145
|
+
# extensible services
|
|
146
|
+
ext_services: dict[str, Any] = field(default_factory=dict)
|
|
147
|
+
|
|
148
|
+
# settings -- not a service, but useful to have around
|
|
149
|
+
settings: AppSettings | None = None
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def build_default_container(
|
|
153
|
+
*,
|
|
154
|
+
root: str | None = None,
|
|
155
|
+
cfg: AppSettings | None = None,
|
|
156
|
+
) -> DefaultContainer:
|
|
157
|
+
"""Build the default service container with standard services.
|
|
158
|
+
if "root" is provided, use it as the base directory for storage; else use from cfg/root.
|
|
159
|
+
if cfg is not provided, load from default AppSettings.
|
|
160
|
+
"""
|
|
161
|
+
if cfg is None:
|
|
162
|
+
from aethergraph.config.context import set_current_settings
|
|
163
|
+
from aethergraph.config.loader import load_settings
|
|
164
|
+
|
|
165
|
+
cfg = load_settings()
|
|
166
|
+
set_current_settings(cfg)
|
|
167
|
+
|
|
168
|
+
root = root or cfg.root
|
|
169
|
+
# override root in cfg to match
|
|
170
|
+
cfg.root = root
|
|
171
|
+
|
|
172
|
+
# we use user specified root if provided, else from config/env
|
|
173
|
+
root_p = Path(root).resolve() if root else Path(cfg.root).resolve()
|
|
174
|
+
(root_p / "kv").mkdir(parents=True, exist_ok=True)
|
|
175
|
+
(root_p / "continuations").mkdir(parents=True, exist_ok=True)
|
|
176
|
+
(root_p / "index").mkdir(parents=True, exist_ok=True)
|
|
177
|
+
(root_p / "memory").mkdir(parents=True, exist_ok=True)
|
|
178
|
+
(root_p / "graph_states").mkdir(parents=True, exist_ok=True)
|
|
179
|
+
|
|
180
|
+
# core services
|
|
181
|
+
logger_factory = StdLoggerService.build(
|
|
182
|
+
LoggingConfig.from_cfg(cfg, log_dir=str(root_p / "logs"))
|
|
183
|
+
)
|
|
184
|
+
clock = SystemClock()
|
|
185
|
+
registry = UnifiedRegistry()
|
|
186
|
+
|
|
187
|
+
# continuations and resume
|
|
188
|
+
cont_store = FSContinuationStore(root=str(root_p / "continuations"), secret=os.urandom(32))
|
|
189
|
+
sched_registry = SchedulerRegistry()
|
|
190
|
+
wait_registry = WaitRegistry()
|
|
191
|
+
resume_bus = MultiSchedulerResumeBus(
|
|
192
|
+
registry=sched_registry, store=cont_store, logger=logger_factory.for_run()
|
|
193
|
+
)
|
|
194
|
+
resume_router = ResumeRouter(
|
|
195
|
+
store=cont_store,
|
|
196
|
+
runner=resume_bus,
|
|
197
|
+
logger=logger_factory.for_run(),
|
|
198
|
+
wait_registry=wait_registry,
|
|
199
|
+
)
|
|
200
|
+
wakeup_queue = ThreadSafeWakeupQueue() # TODO: this is a placeholder, not fully implemented
|
|
201
|
+
state_store = JsonGraphStateStore(root=str(root_p / "graph_states"))
|
|
202
|
+
|
|
203
|
+
# global scheduler
|
|
204
|
+
global_sched = GlobalForwardScheduler(
|
|
205
|
+
registry=sched_registry,
|
|
206
|
+
global_max_concurrency=None, # TODO: make configurable
|
|
207
|
+
logger=logger_factory.for_scheduler(),
|
|
208
|
+
)
|
|
209
|
+
schedulers = {
|
|
210
|
+
"global": global_sched,
|
|
211
|
+
"registry": sched_registry,
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
# channels
|
|
215
|
+
channel_adapters = make_channel_adapters_from_env(cfg)
|
|
216
|
+
channels = build_bus(
|
|
217
|
+
channel_adapters,
|
|
218
|
+
default="console:stdin",
|
|
219
|
+
logger=logger_factory.for_run(),
|
|
220
|
+
resume_router=resume_router,
|
|
221
|
+
cont_store=cont_store,
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
# storage and artifacts
|
|
225
|
+
kv_hot = EphemeralKV()
|
|
226
|
+
kv_durable = SQLiteKV(str(root_p / "kv" / "kv.sqlite"))
|
|
227
|
+
artifacts = FSArtifactStore(
|
|
228
|
+
str(root_p / "artifacts")
|
|
229
|
+
) # async wrapper over FileArtifactStoreSync
|
|
230
|
+
artifact_index = JsonlArtifactIndex(str(root_p / "index" / "artifacts.jsonl"))
|
|
231
|
+
|
|
232
|
+
# memory
|
|
233
|
+
hotlog = KVHotLog(kv=kv_hot)
|
|
234
|
+
persistence = FSPersistence(base_dir=str(root_p / "memory"))
|
|
235
|
+
indices = KVIndices(kv=kv_durable, hot_ttl_s=7 * 24 * 3600)
|
|
236
|
+
|
|
237
|
+
# optional services
|
|
238
|
+
secrets = (
|
|
239
|
+
EnvSecrets()
|
|
240
|
+
) # get secrets from env vars -- for local development; in prod, use a proper secrets manager
|
|
241
|
+
llm_clients = build_llm_clients(cfg.llm, secrets) # return {profile: GenericLLMClient}
|
|
242
|
+
llm_service = LLMService(clients=llm_clients) if llm_clients else None
|
|
243
|
+
|
|
244
|
+
rag_cfg = cfg.rag
|
|
245
|
+
vec_index = create_vector_index(
|
|
246
|
+
backend=rag_cfg.backend, index_path=str(root_p / "rag" / "rag_index"), dim=rag_cfg.dim
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
rag_facade = RAGFacade(
|
|
250
|
+
corpus_root=str(root_p / "rag" / "rag_corpora"),
|
|
251
|
+
artifacts=artifacts,
|
|
252
|
+
embed_client=llm_service.get("default"),
|
|
253
|
+
llm_client=llm_service.get("default"),
|
|
254
|
+
index_backend=vec_index,
|
|
255
|
+
chunker=TextSplitter(),
|
|
256
|
+
logger=logger_factory.for_run(),
|
|
257
|
+
)
|
|
258
|
+
mcp = MCPService() # empty MCP service; users can register clients as needed
|
|
259
|
+
|
|
260
|
+
memory_factory = MemoryFactory(
|
|
261
|
+
hotlog=hotlog,
|
|
262
|
+
persistence=persistence,
|
|
263
|
+
indices=indices,
|
|
264
|
+
artifacts=artifacts,
|
|
265
|
+
hot_limit=int(cfg.memory.hot_limit),
|
|
266
|
+
hot_ttl_s=int(cfg.memory.hot_ttl_s),
|
|
267
|
+
default_signal_threshold=float(cfg.memory.signal_threshold),
|
|
268
|
+
logger=logger_factory.for_run(),
|
|
269
|
+
llm_service=llm_service.get("default") if llm_service else None,
|
|
270
|
+
rag_facade=rag_facade,
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
return DefaultContainer(
|
|
274
|
+
root=str(root_p),
|
|
275
|
+
schedulers=schedulers,
|
|
276
|
+
registry=registry,
|
|
277
|
+
logger=logger_factory,
|
|
278
|
+
clock=clock,
|
|
279
|
+
channels=channels,
|
|
280
|
+
cont_store=cont_store,
|
|
281
|
+
sched_registry=sched_registry,
|
|
282
|
+
wait_registry=wait_registry,
|
|
283
|
+
resume_bus=resume_bus,
|
|
284
|
+
resume_router=resume_router,
|
|
285
|
+
wakeup_queue=wakeup_queue,
|
|
286
|
+
kv_hot=kv_hot,
|
|
287
|
+
kv_durable=kv_durable,
|
|
288
|
+
state_store=state_store,
|
|
289
|
+
artifacts=artifacts,
|
|
290
|
+
artifact_index=artifact_index,
|
|
291
|
+
memory_factory=memory_factory,
|
|
292
|
+
llm=llm_service,
|
|
293
|
+
rag=rag_facade,
|
|
294
|
+
mcp=mcp,
|
|
295
|
+
secrets=secrets,
|
|
296
|
+
event_bus=None,
|
|
297
|
+
prompts=None,
|
|
298
|
+
authn=None,
|
|
299
|
+
authz=None,
|
|
300
|
+
redactor=None,
|
|
301
|
+
metering=None,
|
|
302
|
+
tracer=None,
|
|
303
|
+
settings=cfg,
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
# Singleton (used unless the host sets their own)
|
|
308
|
+
DEFAULT_CONTAINER: DefaultContainer | None = None
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def get_container() -> DefaultContainer:
|
|
312
|
+
global DEFAULT_CONTAINER
|
|
313
|
+
if DEFAULT_CONTAINER is None:
|
|
314
|
+
DEFAULT_CONTAINER = build_default_container()
|
|
315
|
+
return DEFAULT_CONTAINER
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def set_container(c: DefaultContainer) -> None:
|
|
319
|
+
global DEFAULT_CONTAINER
|
|
320
|
+
DEFAULT_CONTAINER = c
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True)
|
|
9
|
+
class Correlator:
|
|
10
|
+
"""Platform-agnostic correlation key for continuations."""
|
|
11
|
+
|
|
12
|
+
scheme: str # e.g., "slack", "web", "email"
|
|
13
|
+
channel: str # e.g., channel ID, email address
|
|
14
|
+
thread: str # e.g., thread ID, conversation ID
|
|
15
|
+
message: str # e.g., message ID, timestamp
|
|
16
|
+
|
|
17
|
+
def key(self) -> tuple[str, str, str, str]:
|
|
18
|
+
return (self.scheme, self.channel, self.thread or "", self.message or "")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class Continuation:
|
|
23
|
+
"""Represents a continuation of a process or workflow."""
|
|
24
|
+
|
|
25
|
+
run_id: str
|
|
26
|
+
node_id: str
|
|
27
|
+
kind: str
|
|
28
|
+
token: str
|
|
29
|
+
prompt: str | None = None
|
|
30
|
+
resume_schema: dict | None = None
|
|
31
|
+
deadline: datetime | None = None
|
|
32
|
+
poll: dict | None = None
|
|
33
|
+
next_wakeup_at: datetime | None = None
|
|
34
|
+
attempts: int = 0
|
|
35
|
+
channel: str | None = None
|
|
36
|
+
created_at: datetime = datetime.utcnow()
|
|
37
|
+
closed: bool = False # ← NEW
|
|
38
|
+
payload: dict[str, Any] | None = None # set at creation time
|
|
39
|
+
|
|
40
|
+
def to_dict(self) -> dict[str, Any]:
|
|
41
|
+
return {
|
|
42
|
+
"run_id": self.run_id,
|
|
43
|
+
"node_id": self.node_id,
|
|
44
|
+
"kind": self.kind,
|
|
45
|
+
"token": self.token,
|
|
46
|
+
"prompt": self.prompt,
|
|
47
|
+
"resume_schema": self.resume_schema,
|
|
48
|
+
"deadline": self.deadline.isoformat() if self.deadline else None,
|
|
49
|
+
"poll": self.poll,
|
|
50
|
+
"next_wakeup_at": self.next_wakeup_at.isoformat() if self.next_wakeup_at else None,
|
|
51
|
+
"attempts": self.attempts,
|
|
52
|
+
"channel": self.channel,
|
|
53
|
+
"created_at": self.created_at.isoformat(),
|
|
54
|
+
"closed": self.closed,
|
|
55
|
+
"payload": self.payload,
|
|
56
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
from stores.fs_store import FSContinuationStore
|
|
4
|
+
from stores.inmem_store import InMemoryContinuationStore
|
|
5
|
+
|
|
6
|
+
from aethergraph.contracts.services.continuations import AsyncContinuationStore
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def make_continuation_store() -> AsyncContinuationStore:
|
|
10
|
+
"""Factory to create a continuation store based on environment configuration.
|
|
11
|
+
Returns:
|
|
12
|
+
An instance of AsyncContinuationStore.
|
|
13
|
+
|
|
14
|
+
Currently supports:
|
|
15
|
+
- InMemoryContinuationStore (CONT_STORE="inmem")
|
|
16
|
+
- FSContinuationStore (CONT_STORE="fs")
|
|
17
|
+
|
|
18
|
+
We need env vars:
|
|
19
|
+
- CONT_STORE: "inmem" or "fs" (default: "fs")
|
|
20
|
+
- CONT_SECRET: optional secret for HMAC token generation
|
|
21
|
+
- CONT_ROOT: for FS store, root directory to store continuations (default: "./artifacts/continuations")
|
|
22
|
+
"""
|
|
23
|
+
kind = (os.getenv("CONT_STORE", "fs")).lower()
|
|
24
|
+
secret = os.getenv("CONT_SECRET")
|
|
25
|
+
secret_bytes = secret.encode("utf-8") if secret else os.urandom(32)
|
|
26
|
+
|
|
27
|
+
if kind == "inmem":
|
|
28
|
+
return InMemoryContinuationStore(secret=secret_bytes)
|
|
29
|
+
elif kind == "fs":
|
|
30
|
+
return FSContinuationStore(
|
|
31
|
+
root=os.getenv("CONT_ROOT", "./artifacts/continuations"), secret=secret_bytes
|
|
32
|
+
)
|
|
33
|
+
else:
|
|
34
|
+
raise ValueError(f"Unknown continuation store kind: {kind}")
|
|
@@ -0,0 +1,264 @@
|
|
|
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 ..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 delete(self, run_id: str, node_id: str) -> None:
|
|
99
|
+
def _del():
|
|
100
|
+
p = self._cont_path(run_id, node_id)
|
|
101
|
+
if p.exists():
|
|
102
|
+
p.unlink()
|
|
103
|
+
|
|
104
|
+
await asyncio.to_thread(_del)
|
|
105
|
+
|
|
106
|
+
# ---------- token helpers ----------
|
|
107
|
+
def _write_token_index(self, run_id: str, node_id: str, token: str) -> None:
|
|
108
|
+
with self._lock:
|
|
109
|
+
p = self._token_idx_path(token)
|
|
110
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
111
|
+
p.write_text(
|
|
112
|
+
json.dumps({"run_id": run_id, "node_id": node_id}, indent=2), encoding="utf-8"
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
async def get_by_token(self, token: str) -> Continuation | None:
|
|
116
|
+
def _lookup():
|
|
117
|
+
p = self._token_idx_path(token)
|
|
118
|
+
if not p.exists():
|
|
119
|
+
return None
|
|
120
|
+
ref = json.loads(p.read_text(encoding="utf-8"))
|
|
121
|
+
return ref["run_id"], ref["node_id"]
|
|
122
|
+
|
|
123
|
+
ref = await asyncio.to_thread(_lookup)
|
|
124
|
+
if not ref:
|
|
125
|
+
return None
|
|
126
|
+
run_id, node_id = ref
|
|
127
|
+
return await self.get(run_id, node_id)
|
|
128
|
+
|
|
129
|
+
async def mark_closed(self, token: str) -> None:
|
|
130
|
+
c = await self.get_by_token(token)
|
|
131
|
+
if not c:
|
|
132
|
+
return
|
|
133
|
+
if not c.closed:
|
|
134
|
+
c.closed = True
|
|
135
|
+
await self.save(c)
|
|
136
|
+
|
|
137
|
+
async def verify_token(self, run_id: str, node_id: str, token: str) -> bool:
|
|
138
|
+
c = await self.get(run_id, node_id)
|
|
139
|
+
return bool(c and hmac.compare_digest(token, c.token))
|
|
140
|
+
|
|
141
|
+
# ---------- correlators ----------
|
|
142
|
+
async def bind_correlator(self, *, token: str, corr: Correlator) -> None:
|
|
143
|
+
def _bind():
|
|
144
|
+
scheme, channel, thread, message = corr.key()
|
|
145
|
+
tokens_path = self._corr_tokens_path(scheme, channel, thread, message)
|
|
146
|
+
tokens_path.parent.mkdir(parents=True, exist_ok=True)
|
|
147
|
+
toks: list[str] = []
|
|
148
|
+
if tokens_path.exists():
|
|
149
|
+
try:
|
|
150
|
+
toks = json.loads(tokens_path.read_text(encoding="utf-8"))
|
|
151
|
+
except Exception:
|
|
152
|
+
toks = []
|
|
153
|
+
if token not in toks:
|
|
154
|
+
toks.append(token)
|
|
155
|
+
tokens_path.write_text(json.dumps(toks, indent=2), encoding="utf-8")
|
|
156
|
+
# reverse index
|
|
157
|
+
r = self._rev_idx_path(token)
|
|
158
|
+
r.parent.mkdir(parents=True, exist_ok=True)
|
|
159
|
+
paths = []
|
|
160
|
+
if r.exists():
|
|
161
|
+
try:
|
|
162
|
+
paths = json.loads(r.read_text(encoding="utf-8"))
|
|
163
|
+
except Exception:
|
|
164
|
+
paths = []
|
|
165
|
+
key_path = str(tokens_path.relative_to(self.root))
|
|
166
|
+
if key_path not in paths:
|
|
167
|
+
paths.append(key_path)
|
|
168
|
+
r.write_text(json.dumps(paths, indent=2), encoding="utf-8")
|
|
169
|
+
|
|
170
|
+
await asyncio.to_thread(_bind)
|
|
171
|
+
|
|
172
|
+
async def find_by_correlator(self, *, corr: Correlator) -> Continuation | None:
|
|
173
|
+
def _read_toks():
|
|
174
|
+
scheme, channel, thread, message = corr.key()
|
|
175
|
+
p = self._corr_tokens_path(scheme, channel, thread, message)
|
|
176
|
+
if not p.exists():
|
|
177
|
+
return []
|
|
178
|
+
try:
|
|
179
|
+
return json.loads(p.read_text(encoding="utf-8")) or []
|
|
180
|
+
except Exception:
|
|
181
|
+
return []
|
|
182
|
+
|
|
183
|
+
toks = await asyncio.to_thread(_read_toks)
|
|
184
|
+
for tok in reversed(toks):
|
|
185
|
+
c = await self.get_by_token(tok)
|
|
186
|
+
if c and not c.closed:
|
|
187
|
+
if c.deadline and datetime.now(timezone.utc) > c.deadline.astimezone(timezone.utc):
|
|
188
|
+
continue
|
|
189
|
+
return c
|
|
190
|
+
return None
|
|
191
|
+
|
|
192
|
+
async def last_open(self, *, channel: str, kind: str) -> Continuation | None:
|
|
193
|
+
# Optional slow scan (dev only)
|
|
194
|
+
def _scan():
|
|
195
|
+
waits = []
|
|
196
|
+
runs_path = self.root / "runs"
|
|
197
|
+
if not runs_path.exists():
|
|
198
|
+
return waits
|
|
199
|
+
for run_dir in runs_path.iterdir():
|
|
200
|
+
nodes_dir = run_dir / "nodes"
|
|
201
|
+
if not nodes_dir.exists():
|
|
202
|
+
continue
|
|
203
|
+
for node_dir in nodes_dir.iterdir():
|
|
204
|
+
cont_path = node_dir / "continuation.json"
|
|
205
|
+
if cont_path.exists():
|
|
206
|
+
waits.append(json.loads(cont_path.read_text(encoding="utf-8")))
|
|
207
|
+
return waits
|
|
208
|
+
|
|
209
|
+
waits = await asyncio.to_thread(_scan)
|
|
210
|
+
for raw in reversed(waits):
|
|
211
|
+
if raw.get("closed"):
|
|
212
|
+
continue
|
|
213
|
+
if raw.get("channel") == channel and raw.get("kind") == kind:
|
|
214
|
+
for k in ("deadline", "next_wakeup_at", "created_at"):
|
|
215
|
+
if raw.get(k):
|
|
216
|
+
raw[k] = datetime.fromisoformat(raw[k])
|
|
217
|
+
return Continuation(**raw)
|
|
218
|
+
return None
|
|
219
|
+
|
|
220
|
+
async def list_waits(self) -> list[dict[str, Any]]:
|
|
221
|
+
def _scan():
|
|
222
|
+
out = []
|
|
223
|
+
runs_path = self.root / "runs"
|
|
224
|
+
if not runs_path.exists():
|
|
225
|
+
return out
|
|
226
|
+
for run_dir in runs_path.iterdir():
|
|
227
|
+
nodes_dir = run_dir / "nodes"
|
|
228
|
+
if not nodes_dir.exists():
|
|
229
|
+
continue
|
|
230
|
+
for node_dir in nodes_dir.iterdir():
|
|
231
|
+
cont_path = node_dir / "continuation.json"
|
|
232
|
+
if cont_path.exists():
|
|
233
|
+
out.append(json.loads(cont_path.read_text(encoding="utf-8")))
|
|
234
|
+
return out
|
|
235
|
+
|
|
236
|
+
return await asyncio.to_thread(_scan)
|
|
237
|
+
|
|
238
|
+
async def clear(self) -> None:
|
|
239
|
+
def _clear():
|
|
240
|
+
for sub in ("runs", "index"):
|
|
241
|
+
p = self.root / sub
|
|
242
|
+
if p.exists():
|
|
243
|
+
for root, dirs, files in os.walk(p, topdown=False):
|
|
244
|
+
for f in files:
|
|
245
|
+
try:
|
|
246
|
+
os.remove(Path(root) / f)
|
|
247
|
+
except Exception:
|
|
248
|
+
logger = logging.getLogger(
|
|
249
|
+
"aethergraph.services.continuations.stores.fs_store"
|
|
250
|
+
)
|
|
251
|
+
logger.warning("Failed to remove file: %s", Path(root) / f)
|
|
252
|
+
for d in dirs:
|
|
253
|
+
try:
|
|
254
|
+
os.rmdir(Path(root) / d)
|
|
255
|
+
except Exception:
|
|
256
|
+
logger = logging.getLogger(
|
|
257
|
+
"aethergraph.services.continuations.stores.fs_store"
|
|
258
|
+
)
|
|
259
|
+
logger.warning("Failed to remove dir: %s", Path(root) / d)
|
|
260
|
+
|
|
261
|
+
await asyncio.to_thread(_clear)
|
|
262
|
+
|
|
263
|
+
async def alias_for(self, token: str) -> str | None:
|
|
264
|
+
return token[:24]
|