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,62 @@
|
|
|
1
|
+
# redirect runtime service imports for clean imports
|
|
2
|
+
|
|
3
|
+
from aethergraph.core.runtime.ad_hoc_context import open_session
|
|
4
|
+
from aethergraph.core.runtime.runtime_services import (
|
|
5
|
+
# logger service helpers
|
|
6
|
+
current_logger_factory,
|
|
7
|
+
current_services,
|
|
8
|
+
ensure_services_installed,
|
|
9
|
+
# channel service helpers
|
|
10
|
+
get_channel_service,
|
|
11
|
+
get_default_channel,
|
|
12
|
+
get_ext_context_service,
|
|
13
|
+
# llm service helpers
|
|
14
|
+
get_llm_service,
|
|
15
|
+
get_mcp_service,
|
|
16
|
+
# general service management
|
|
17
|
+
install_services,
|
|
18
|
+
list_ext_context_services,
|
|
19
|
+
list_mcp_clients,
|
|
20
|
+
register_channel_adapter,
|
|
21
|
+
# external context service helpers
|
|
22
|
+
register_context_service,
|
|
23
|
+
register_llm_client,
|
|
24
|
+
register_mcp_client,
|
|
25
|
+
set_channel_alias,
|
|
26
|
+
set_default_channel,
|
|
27
|
+
# mcp service helpers
|
|
28
|
+
set_mcp_service,
|
|
29
|
+
set_rag_index_backend,
|
|
30
|
+
set_rag_llm_client,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
# general service management
|
|
35
|
+
"install_services",
|
|
36
|
+
"ensure_services_installed",
|
|
37
|
+
"current_services",
|
|
38
|
+
# channel service helpers
|
|
39
|
+
"get_channel_service",
|
|
40
|
+
"set_default_channel",
|
|
41
|
+
"get_default_channel",
|
|
42
|
+
"set_channel_alias",
|
|
43
|
+
"register_channel_adapter",
|
|
44
|
+
# llm service helpers
|
|
45
|
+
"get_llm_service",
|
|
46
|
+
"register_llm_client",
|
|
47
|
+
"set_rag_llm_client",
|
|
48
|
+
"set_rag_index_backend",
|
|
49
|
+
# logger service helpers
|
|
50
|
+
"current_logger_factory",
|
|
51
|
+
# external context service helpers
|
|
52
|
+
"register_context_service",
|
|
53
|
+
"get_ext_context_service",
|
|
54
|
+
"list_ext_context_services",
|
|
55
|
+
# mcp service helpers
|
|
56
|
+
"set_mcp_service",
|
|
57
|
+
"get_mcp_service",
|
|
58
|
+
"register_mcp_client",
|
|
59
|
+
"list_mcp_clients",
|
|
60
|
+
# ad-hoc context
|
|
61
|
+
"open_session",
|
|
62
|
+
]
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
from fastapi import FastAPI
|
|
5
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
6
|
+
|
|
7
|
+
from aethergraph.config.config import AppSettings
|
|
8
|
+
from aethergraph.utils.optdeps import require
|
|
9
|
+
|
|
10
|
+
from ..core.runtime.runtime_services import install_services
|
|
11
|
+
|
|
12
|
+
# channel routes
|
|
13
|
+
from ..services.container.default_container import build_default_container
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def create_app(
|
|
17
|
+
*,
|
|
18
|
+
workspace: str = "./aethergraph_data",
|
|
19
|
+
cfg: Optional["AppSettings"] = None,
|
|
20
|
+
log_level: str = "info",
|
|
21
|
+
) -> FastAPI:
|
|
22
|
+
"""
|
|
23
|
+
Builds the FastAPI app, registers routers, and installs all services
|
|
24
|
+
into app.state.container (and globally via install_services()).
|
|
25
|
+
"""
|
|
26
|
+
app = FastAPI(title="AetherGraph Sidecar", version="0.1")
|
|
27
|
+
|
|
28
|
+
app.add_middleware(
|
|
29
|
+
CORSMiddleware,
|
|
30
|
+
allow_origins=["http://localhost:5173"], # dev UI origin
|
|
31
|
+
allow_credentials=True,
|
|
32
|
+
allow_methods=["*"],
|
|
33
|
+
allow_headers=["*"],
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
# Resolve settings early, so we can conditionally include routers
|
|
37
|
+
settings = cfg or AppSettings()
|
|
38
|
+
app.state.settings = settings
|
|
39
|
+
|
|
40
|
+
# --- Routers (HTTP transports) ---
|
|
41
|
+
# For now, we can just always include; or gate it with a flag like settings.slack.use_webhook.
|
|
42
|
+
# app.include_router(slack_router) # HTTP /slack/events + /slack/interact
|
|
43
|
+
# app.include_router(console_router)
|
|
44
|
+
# app.include_router(telegram_router)
|
|
45
|
+
# app.include_router(webui_router)
|
|
46
|
+
|
|
47
|
+
# override log level in config
|
|
48
|
+
settings.logging.level = log_level
|
|
49
|
+
|
|
50
|
+
# ---- Services container ----
|
|
51
|
+
container = build_default_container(root=workspace, cfg=settings)
|
|
52
|
+
app.state.container = container
|
|
53
|
+
|
|
54
|
+
# install globally so run()/tools see the same services
|
|
55
|
+
install_services(container)
|
|
56
|
+
|
|
57
|
+
# ---- External channel transports (Socket Mode, polling, etc.) ----
|
|
58
|
+
@app.on_event("startup")
|
|
59
|
+
async def start_external_transports():
|
|
60
|
+
slack_cfg = settings.slack
|
|
61
|
+
if (
|
|
62
|
+
slack_cfg
|
|
63
|
+
and slack_cfg.enabled
|
|
64
|
+
and slack_cfg.socket_mode_enabled
|
|
65
|
+
and slack_cfg.bot_token
|
|
66
|
+
and slack_cfg.app_token
|
|
67
|
+
):
|
|
68
|
+
require("slack_sdk", "slack")
|
|
69
|
+
from ..plugins.channel.websockets.slack_ws import SlackSocketModeRunner
|
|
70
|
+
|
|
71
|
+
runner = SlackSocketModeRunner(container=container, settings=settings)
|
|
72
|
+
app.state.slack_socket_runner = runner
|
|
73
|
+
asyncio.create_task(runner.start())
|
|
74
|
+
|
|
75
|
+
# Telegram polling for local / dev
|
|
76
|
+
tg_cfg = settings.telegram
|
|
77
|
+
if tg_cfg and tg_cfg.enabled and tg_cfg.polling_enabled and tg_cfg.bot_token:
|
|
78
|
+
from ..plugins.channel.websockets.telegram_polling import TelegramPollingRunner
|
|
79
|
+
|
|
80
|
+
tg_runner = TelegramPollingRunner(container=container, settings=settings)
|
|
81
|
+
app.state.telegram_polling_runner = tg_runner
|
|
82
|
+
asyncio.create_task(tg_runner.start())
|
|
83
|
+
|
|
84
|
+
return app
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# aethergraph/start.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import asyncio
|
|
5
|
+
import contextlib
|
|
6
|
+
import socket
|
|
7
|
+
import threading
|
|
8
|
+
|
|
9
|
+
import uvicorn
|
|
10
|
+
|
|
11
|
+
from aethergraph.config.context import set_current_settings
|
|
12
|
+
from aethergraph.config.loader import load_settings
|
|
13
|
+
|
|
14
|
+
from ..plugins.channel.routes.webui_routes import install_web_channel
|
|
15
|
+
from .app_factory import create_app
|
|
16
|
+
|
|
17
|
+
_started = False
|
|
18
|
+
_server_thread: threading.Thread | None = None
|
|
19
|
+
_shutdown_flag = threading.Event()
|
|
20
|
+
_url: str | None = None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _pick_free_port(p: int) -> int:
|
|
24
|
+
if p:
|
|
25
|
+
return p
|
|
26
|
+
s = socket.socket()
|
|
27
|
+
s.bind(("", 0))
|
|
28
|
+
port = s.getsockname()[1]
|
|
29
|
+
s.close()
|
|
30
|
+
return port
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _run_uvicorn_in_thread(app, host: str, port: int, log_level: str):
|
|
34
|
+
loop = asyncio.new_event_loop()
|
|
35
|
+
asyncio.set_event_loop(loop)
|
|
36
|
+
server = uvicorn.Server(
|
|
37
|
+
uvicorn.Config(app, host=host, port=port, log_level=log_level, loop="asyncio")
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
async def runner():
|
|
41
|
+
task = asyncio.create_task(server.serve())
|
|
42
|
+
while not _shutdown_flag.is_set():
|
|
43
|
+
await asyncio.sleep(0.2)
|
|
44
|
+
if not server.should_exit:
|
|
45
|
+
server.should_exit = True
|
|
46
|
+
await task
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
loop.run_until_complete(runner())
|
|
50
|
+
finally:
|
|
51
|
+
loop.stop()
|
|
52
|
+
loop.close()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def start_server(
|
|
56
|
+
*,
|
|
57
|
+
workspace: str = "./aethergraph_data",
|
|
58
|
+
host: str = "127.0.0.1",
|
|
59
|
+
port: int = 8000, # 0 = auto free port
|
|
60
|
+
log_level: str = "warning",
|
|
61
|
+
unvicorn_log_level: str = "warning",
|
|
62
|
+
return_container: bool = False,
|
|
63
|
+
) -> str:
|
|
64
|
+
"""
|
|
65
|
+
Start the AetherGraph sidecar server in a background thread and install
|
|
66
|
+
services using the given workspace. Safe to call at top of any script
|
|
67
|
+
or notebook cell (no main() wrapper needed). Returns base URL.
|
|
68
|
+
"""
|
|
69
|
+
global _started, _server_thread, _url
|
|
70
|
+
if _started:
|
|
71
|
+
return _url # type: ignore
|
|
72
|
+
|
|
73
|
+
# Build app (installs services inside create_app)
|
|
74
|
+
cfg = load_settings()
|
|
75
|
+
set_current_settings(cfg)
|
|
76
|
+
|
|
77
|
+
app = create_app(workspace=workspace, cfg=cfg, log_level=log_level)
|
|
78
|
+
|
|
79
|
+
picked_port = _pick_free_port(port)
|
|
80
|
+
t = threading.Thread(
|
|
81
|
+
target=_run_uvicorn_in_thread,
|
|
82
|
+
args=(app, host, picked_port, unvicorn_log_level),
|
|
83
|
+
name="aethergraph-sidecar",
|
|
84
|
+
daemon=True,
|
|
85
|
+
)
|
|
86
|
+
t.start()
|
|
87
|
+
|
|
88
|
+
_server_thread = t
|
|
89
|
+
_started = True
|
|
90
|
+
_url = f"http://{host}:{picked_port}"
|
|
91
|
+
|
|
92
|
+
install_web_channel(app)
|
|
93
|
+
|
|
94
|
+
if return_container:
|
|
95
|
+
return _url, app.state.container
|
|
96
|
+
return _url
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
async def start_server_async(**kw) -> str:
|
|
100
|
+
# Async-friendly wrapper; still uses a thread to avoid clashing with caller loop
|
|
101
|
+
return start_server(**kw)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def stop_server():
|
|
105
|
+
"""Optional: stop the background server (useful in tests)."""
|
|
106
|
+
global _started, _server_thread, _url
|
|
107
|
+
if not _started:
|
|
108
|
+
return
|
|
109
|
+
_shutdown_flag.set()
|
|
110
|
+
if _server_thread and _server_thread.is_alive():
|
|
111
|
+
with contextlib.suppress(Exception):
|
|
112
|
+
_server_thread.join(timeout=5)
|
|
113
|
+
_started = False
|
|
114
|
+
_server_thread = None
|
|
115
|
+
_url = None
|
|
116
|
+
_shutdown_flag.clear()
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
# backward compatibility
|
|
120
|
+
start = start_server
|
|
121
|
+
stop = stop_server
|
|
122
|
+
start_async = start_server_async
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# redirect runtime.Service imports for clean imports
|
|
2
|
+
from aethergraph.core.runtime.base_service import Service
|
|
3
|
+
from aethergraph.services.mcp.http_client import HttpMCPClient
|
|
4
|
+
|
|
5
|
+
# import mcp-related services
|
|
6
|
+
from aethergraph.services.mcp.service import MCPService
|
|
7
|
+
from aethergraph.services.mcp.stdio_client import StdioMCPClient
|
|
8
|
+
from aethergraph.services.mcp.ws_client import WsMCPClient
|
|
9
|
+
|
|
10
|
+
__all__ = ["HttpMCPClient", "MCPService", "Service", "StdioMCPClient", "WsMCPClient"]
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import builtins
|
|
4
|
+
from contextlib import asynccontextmanager
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, Literal
|
|
7
|
+
from urllib.parse import urlparse
|
|
8
|
+
|
|
9
|
+
from aethergraph.contracts.services.artifacts import (
|
|
10
|
+
Artifact,
|
|
11
|
+
AsyncArtifactIndex,
|
|
12
|
+
AsyncArtifactStore,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
from .paths import _from_uri_or_path
|
|
16
|
+
|
|
17
|
+
Scope = Literal["node", "run", "graph", "all"]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ArtifactFacade:
|
|
21
|
+
"""Facade for artifact storage and indexing operations within a specific context.
|
|
22
|
+
Provides async methods to stage, ingest, save, and write artifacts with automatic indexing.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
*,
|
|
28
|
+
run_id: str,
|
|
29
|
+
graph_id: str,
|
|
30
|
+
node_id: str,
|
|
31
|
+
tool_name: str,
|
|
32
|
+
tool_version: str,
|
|
33
|
+
store: AsyncArtifactStore,
|
|
34
|
+
index: AsyncArtifactIndex,
|
|
35
|
+
):
|
|
36
|
+
self.run_id, self.graph_id, self.node_id = run_id, graph_id, node_id
|
|
37
|
+
self.tool_name, self.tool_version = tool_name, tool_version
|
|
38
|
+
self.store, self.index = store, index
|
|
39
|
+
self.last_artifact: Artifact | None = None
|
|
40
|
+
|
|
41
|
+
async def stage(self, ext: str = "") -> str:
|
|
42
|
+
return await self.store.plan_staging_path(ext)
|
|
43
|
+
|
|
44
|
+
async def ingest(
|
|
45
|
+
self,
|
|
46
|
+
staged_path: str,
|
|
47
|
+
*,
|
|
48
|
+
kind: str,
|
|
49
|
+
labels=None,
|
|
50
|
+
metrics=None,
|
|
51
|
+
suggested_uri: str | None = None,
|
|
52
|
+
pin: bool = False,
|
|
53
|
+
):
|
|
54
|
+
a = await self.store.ingest_staged_file(
|
|
55
|
+
staged_path=staged_path,
|
|
56
|
+
kind=kind,
|
|
57
|
+
run_id=self.run_id,
|
|
58
|
+
graph_id=self.graph_id,
|
|
59
|
+
node_id=self.node_id,
|
|
60
|
+
tool_name=self.tool_name,
|
|
61
|
+
tool_version=self.tool_version,
|
|
62
|
+
labels=labels,
|
|
63
|
+
metrics=metrics,
|
|
64
|
+
suggested_uri=suggested_uri,
|
|
65
|
+
pin=pin,
|
|
66
|
+
)
|
|
67
|
+
await self.index.upsert(a)
|
|
68
|
+
await self.index.record_occurrence(a)
|
|
69
|
+
return a
|
|
70
|
+
|
|
71
|
+
async def save(
|
|
72
|
+
self,
|
|
73
|
+
path: str,
|
|
74
|
+
*,
|
|
75
|
+
kind: str,
|
|
76
|
+
labels=None,
|
|
77
|
+
metrics=None,
|
|
78
|
+
suggested_uri: str | None = None,
|
|
79
|
+
pin: bool = False,
|
|
80
|
+
):
|
|
81
|
+
a = await self.store.save_file(
|
|
82
|
+
path=path,
|
|
83
|
+
kind=kind,
|
|
84
|
+
run_id=self.run_id,
|
|
85
|
+
graph_id=self.graph_id,
|
|
86
|
+
node_id=self.node_id,
|
|
87
|
+
tool_name=self.tool_name,
|
|
88
|
+
tool_version=self.tool_version,
|
|
89
|
+
labels=labels,
|
|
90
|
+
metrics=metrics,
|
|
91
|
+
suggested_uri=suggested_uri,
|
|
92
|
+
pin=pin,
|
|
93
|
+
)
|
|
94
|
+
await self.index.upsert(a)
|
|
95
|
+
await self.index.record_occurrence(a)
|
|
96
|
+
self.last_artifact = a
|
|
97
|
+
return a
|
|
98
|
+
|
|
99
|
+
async def save_text(self, payload: str, *, suggested_uri: str | None = None):
|
|
100
|
+
a = await self.store.save_text(payload=payload, suggested_uri=suggested_uri)
|
|
101
|
+
await self.index.upsert(a)
|
|
102
|
+
await self.index.record_occurrence(a)
|
|
103
|
+
self.last_artifact = a
|
|
104
|
+
return a
|
|
105
|
+
|
|
106
|
+
async def save_json(self, payload: dict, *, suggested_uri: str | None = None):
|
|
107
|
+
a = await self.store.save_json(payload=payload, suggested_uri=suggested_uri)
|
|
108
|
+
await self.index.upsert(a)
|
|
109
|
+
await self.index.record_occurrence(a)
|
|
110
|
+
self.last_artifact = a
|
|
111
|
+
return a
|
|
112
|
+
|
|
113
|
+
@asynccontextmanager
|
|
114
|
+
async def writer(self, *, kind: str, planned_ext: str | None = None, pin: bool = False):
|
|
115
|
+
# Use the store's (sync) contextmanager via async wrapper; user writes bytes
|
|
116
|
+
cm = await self.store.open_writer(
|
|
117
|
+
kind=kind,
|
|
118
|
+
run_id=self.run_id,
|
|
119
|
+
graph_id=self.graph_id,
|
|
120
|
+
node_id=self.node_id,
|
|
121
|
+
tool_name=self.tool_name,
|
|
122
|
+
tool_version=self.tool_version,
|
|
123
|
+
planned_ext=planned_ext,
|
|
124
|
+
pin=pin,
|
|
125
|
+
)
|
|
126
|
+
with cm as w:
|
|
127
|
+
yield w
|
|
128
|
+
a = getattr(w, "_artifact", None)
|
|
129
|
+
if a:
|
|
130
|
+
await self.index.upsert(a)
|
|
131
|
+
await self.index.record_occurrence(a)
|
|
132
|
+
self.last_artifact = a
|
|
133
|
+
else:
|
|
134
|
+
self.last_artifact = None
|
|
135
|
+
|
|
136
|
+
async def stage_dir(self, suffix: str = "") -> str:
|
|
137
|
+
return await self.store.plan_staging_dir(suffix)
|
|
138
|
+
|
|
139
|
+
async def ingest_dir(self, staged_dir: str, **kw):
|
|
140
|
+
a = await self.store.ingest_directory(
|
|
141
|
+
staged_dir=staged_dir,
|
|
142
|
+
run_id=self.run_id,
|
|
143
|
+
graph_id=self.graph_id,
|
|
144
|
+
node_id=self.node_id,
|
|
145
|
+
tool_name=self.tool_name,
|
|
146
|
+
tool_version=self.tool_version,
|
|
147
|
+
**kw,
|
|
148
|
+
)
|
|
149
|
+
await self.index.upsert(a)
|
|
150
|
+
await self.index.record_occurrence(a)
|
|
151
|
+
self.last_artifact = a
|
|
152
|
+
return a
|
|
153
|
+
|
|
154
|
+
async def tmp_path(self, suffix: str = "") -> str:
|
|
155
|
+
return await self.store.plan_staging_path(suffix)
|
|
156
|
+
|
|
157
|
+
async def load_bytes(self, uri: str) -> bytes:
|
|
158
|
+
return await self.store.load_bytes(uri)
|
|
159
|
+
|
|
160
|
+
async def load_text(self, uri: str, *, encoding: str = "utf-8", errors: str = "strict") -> str:
|
|
161
|
+
data = await self.store.load_text(uri)
|
|
162
|
+
return data
|
|
163
|
+
|
|
164
|
+
async def load_json(self, uri: str, *, encoding: str = "utf-8", errors: str = "strict") -> Any:
|
|
165
|
+
data = await self.store.load_json(uri, encoding=encoding, errors=errors)
|
|
166
|
+
return data
|
|
167
|
+
|
|
168
|
+
async def load_artifact(self, uri: str) -> Any:
|
|
169
|
+
return await self.store.load_artifact(uri)
|
|
170
|
+
|
|
171
|
+
async def load_artifact_bytes(self, uri: str) -> bytes:
|
|
172
|
+
return await self.store.load_artifact_bytes(uri)
|
|
173
|
+
|
|
174
|
+
# ------- indexing pass-throughs with scoping -------
|
|
175
|
+
async def list(self, *, scope: Scope = "run") -> builtins.list[Artifact]:
|
|
176
|
+
"""
|
|
177
|
+
Quick listing scoped to current run/graph/node by default.
|
|
178
|
+
scope:
|
|
179
|
+
- "node": filter by (run_id, graph_id, node_id)
|
|
180
|
+
- "graph": filter by (run_id, graph_id)
|
|
181
|
+
- "run": filter by (run_id) [default]
|
|
182
|
+
- "all": no implicit filters (dangerous; use sparingly)
|
|
183
|
+
"""
|
|
184
|
+
if scope == "node":
|
|
185
|
+
arts = await self.index.search(
|
|
186
|
+
labels={"graph_id": self.graph_id, "node_id": self.node_id}
|
|
187
|
+
)
|
|
188
|
+
return [a for a in arts if a.run_id == self.run_id]
|
|
189
|
+
if scope == "graph":
|
|
190
|
+
arts = await self.index.search(labels={"graph_id": self.graph_id})
|
|
191
|
+
return [a for a in arts if a.run_id == self.run_id]
|
|
192
|
+
if scope == "run":
|
|
193
|
+
return await self.index.list_for_run(self.run_id)
|
|
194
|
+
if scope == "all":
|
|
195
|
+
return await self.index.search()
|
|
196
|
+
return await self.index.search(labels=self._scope_labels(scope))
|
|
197
|
+
|
|
198
|
+
async def search(
|
|
199
|
+
self,
|
|
200
|
+
*,
|
|
201
|
+
kind: str | None = None,
|
|
202
|
+
labels: dict[str, Any] | None = None,
|
|
203
|
+
metric: str | None = None,
|
|
204
|
+
mode: Literal["max", "min"] | None = None,
|
|
205
|
+
scope: Scope = "run",
|
|
206
|
+
extra_scope_labels: dict[str, Any] | None = None,
|
|
207
|
+
) -> builtins.list[Artifact]:
|
|
208
|
+
"""Pass-through search with automatic scoping."""
|
|
209
|
+
eff_labels = dict(labels or {})
|
|
210
|
+
if scope in ("node", "graph", "project"):
|
|
211
|
+
eff_labels.update(self._scope_labels(scope))
|
|
212
|
+
if extra_scope_labels:
|
|
213
|
+
eff_labels.update(extra_scope_labels)
|
|
214
|
+
# Delegate heavy lifting to the index
|
|
215
|
+
return await self.index.search(kind=kind, labels=eff_labels, metric=metric, mode=mode)
|
|
216
|
+
|
|
217
|
+
async def best(
|
|
218
|
+
self,
|
|
219
|
+
*,
|
|
220
|
+
kind: str,
|
|
221
|
+
metric: str,
|
|
222
|
+
mode: Literal["max", "min"],
|
|
223
|
+
scope: Scope = "run",
|
|
224
|
+
filters: dict[str, Any] | None = None,
|
|
225
|
+
) -> Artifact | None:
|
|
226
|
+
eff_filters = dict(filters or {})
|
|
227
|
+
if scope in ("node", "graph", "project"):
|
|
228
|
+
eff_filters.update(self._scope_labels(scope))
|
|
229
|
+
return await self.index.best(
|
|
230
|
+
kind=kind, metric=metric, mode=mode, filters=eff_filters or None
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
async def pin(self, artifact_id: str, pinned: bool = True) -> None:
|
|
234
|
+
await self.index.pin(artifact_id, pinned)
|
|
235
|
+
|
|
236
|
+
# -------- internal helpers --------
|
|
237
|
+
def _scope_labels(self, scope: Scope) -> dict[str, Any]:
|
|
238
|
+
if scope == "node":
|
|
239
|
+
return {"run_id": self.run_id, "graph_id": self.graph_id, "node_id": self.node_id}
|
|
240
|
+
if scope == "graph":
|
|
241
|
+
return {"run_id": self.run_id, "graph_id": self.graph_id}
|
|
242
|
+
if scope == "run":
|
|
243
|
+
return {"run_id": self.run_id}
|
|
244
|
+
return {} # "all"
|
|
245
|
+
|
|
246
|
+
def _project_id(self) -> str | None:
|
|
247
|
+
# This function is no longer used, but kept for possible future use.
|
|
248
|
+
return getattr(self, "project_id", None)
|
|
249
|
+
|
|
250
|
+
# ---------- convenience: URI -> local path (FS only) ----------
|
|
251
|
+
def to_local_path(self, uri_or_path: str | Path | Artifact, *, must_exist: bool = True) -> str:
|
|
252
|
+
"""
|
|
253
|
+
Return an absolute native path string if input is a file:// URI or local path.
|
|
254
|
+
If given an Artifact, uses artifact.uri.
|
|
255
|
+
If the scheme is not file://, returns the string form unchanged (or raise in strict mode).
|
|
256
|
+
"""
|
|
257
|
+
s = uri_or_path.uri or "" if isinstance(uri_or_path, Artifact) else str(uri_or_path)
|
|
258
|
+
|
|
259
|
+
p = _from_uri_or_path(s).resolve()
|
|
260
|
+
|
|
261
|
+
# If not a file:// (e.g., s3://, http://), _from_uri_or_path returns Path(s);
|
|
262
|
+
# detect that and either pass through or raise for clarity.
|
|
263
|
+
u = urlparse(s)
|
|
264
|
+
if "://" in s and (u.scheme or "").lower() != "file":
|
|
265
|
+
# Not a filesystem artifact; caller likely needs a downloader
|
|
266
|
+
return s # or: raise ValueError("Not a local filesystem URI")
|
|
267
|
+
|
|
268
|
+
if must_exist and not p.exists():
|
|
269
|
+
raise FileNotFoundError(f"Local path not found: {p}")
|
|
270
|
+
return str(p)
|
|
271
|
+
|
|
272
|
+
def to_local_file(self, uri_or_path: str | Path | Artifact, *, must_exist: bool = True) -> str:
|
|
273
|
+
"""Same as to_local_path but asserts it's a file (not a dir)."""
|
|
274
|
+
p = Path(self.to_local_path(uri_or_path, must_exist=must_exist))
|
|
275
|
+
if must_exist and not p.is_file():
|
|
276
|
+
raise IsADirectoryError(f"Expected file, got directory: {p}")
|
|
277
|
+
return str(p)
|
|
278
|
+
|
|
279
|
+
def to_local_dir(self, uri_or_path: str | Path | Artifact, *, must_exist: bool = True) -> str:
|
|
280
|
+
"""Same as to_local_path but asserts it's a directory."""
|
|
281
|
+
p = Path(self.to_local_path(uri_or_path, must_exist=must_exist))
|
|
282
|
+
if must_exist and not p.is_dir():
|
|
283
|
+
raise NotADirectoryError(f"Expected directory, got file: {p}")
|
|
284
|
+
return str(p)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
from .fs_store import FSArtifactStore
|
|
4
|
+
from .jsonl_index import JsonlArtifactIndex
|
|
5
|
+
from .sqlite_index import SqliteArtifactIndex # if present
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def make_artifact_store() -> FSArtifactStore:
|
|
9
|
+
base = os.getenv("ARTIFACTS_DIR", "./artifacts")
|
|
10
|
+
return FSArtifactStore(base)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def make_artifact_index():
|
|
14
|
+
kind = (os.getenv("ARTIFACT_INDEX", "jsonl")).lower()
|
|
15
|
+
if kind == "sqlite":
|
|
16
|
+
path = os.getenv("ARTIFACT_INDEX_SQLITE", "./artifacts/index.sqlite")
|
|
17
|
+
return SqliteArtifactIndex(path)
|
|
18
|
+
path = os.getenv("ARTIFACT_INDEX_JSONL", "./artifacts/index.jsonl")
|
|
19
|
+
return JsonlArtifactIndex(path)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def make_facade_for_node(*, env, node, store=None, index=None):
|
|
23
|
+
store = store or make_artifact_store()
|
|
24
|
+
index = index or make_artifact_index()
|
|
25
|
+
from aethergraph.services.artifacts.facade import ArtifactFacade
|
|
26
|
+
|
|
27
|
+
return ArtifactFacade(
|
|
28
|
+
run_id=env.run_id,
|
|
29
|
+
graph_id=env.graph_id,
|
|
30
|
+
node_id=node.node_id,
|
|
31
|
+
tool_name=node.tool_name,
|
|
32
|
+
tool_version=node.tool_version,
|
|
33
|
+
store=store,
|
|
34
|
+
index=index,
|
|
35
|
+
)
|