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
aethergraph/__init__.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
__version__ = "0.1.0a1"
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
# Server
|
|
5
|
+
# Channel buttons
|
|
6
|
+
from .contracts.services.channel import Button
|
|
7
|
+
|
|
8
|
+
# Graphs
|
|
9
|
+
from .core.graph.graph_fn import graph_fn # full-featured graph decorator
|
|
10
|
+
from .core.graph.graphify import graphify # graphify decorator to build TaskGraphs from functions
|
|
11
|
+
from .core.graph.task_graph import (
|
|
12
|
+
TaskGraph, # full task graph object for type checking, serialization, etc.
|
|
13
|
+
)
|
|
14
|
+
from .core.runtime.base_service import Service # base service class for custom services
|
|
15
|
+
|
|
16
|
+
# Runtime
|
|
17
|
+
from .core.runtime.node_context import NodeContext # per-node execution context (run_id)
|
|
18
|
+
|
|
19
|
+
# Tools
|
|
20
|
+
from .core.tools.toolkit import tool
|
|
21
|
+
from .server.start import (
|
|
22
|
+
start_server, # start a local sidecar server
|
|
23
|
+
start_server_async, # async version of start_server
|
|
24
|
+
stop_server, # stop the sidecar server
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
# Server
|
|
29
|
+
"start_server",
|
|
30
|
+
"stop_server",
|
|
31
|
+
"start_server_async",
|
|
32
|
+
# Tools
|
|
33
|
+
"tool",
|
|
34
|
+
"graph_fn",
|
|
35
|
+
"graphify",
|
|
36
|
+
"TaskGraph",
|
|
37
|
+
"RuntimeEnv",
|
|
38
|
+
"NodeContext",
|
|
39
|
+
# Services
|
|
40
|
+
"Service",
|
|
41
|
+
# Channel buttons
|
|
42
|
+
"Button",
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# Setup a default null logger to avoid "No handler found" warnings
|
|
47
|
+
import logging
|
|
48
|
+
|
|
49
|
+
logging.getLogger("aethergraph").addHandler(logging.NullHandler())
|
|
File without changes
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# aethergraph/config.py
|
|
2
|
+
from typing import Literal
|
|
3
|
+
|
|
4
|
+
from pydantic import BaseModel, Field, SecretStr
|
|
5
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
6
|
+
|
|
7
|
+
from .llm import LLMSettings
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class LoggingSettings(BaseModel):
|
|
11
|
+
nspace: str = Field("aethergraph", description="Root logger namespace")
|
|
12
|
+
level: str = Field("INFO", description="Root log level")
|
|
13
|
+
json_logs: bool = Field(False, description="Emit JSON logs")
|
|
14
|
+
enable_queue: bool = Field(default=False, description="Enable async logging via queue")
|
|
15
|
+
|
|
16
|
+
external_level: str = Field("WARNING", description="Level for third-party loggers")
|
|
17
|
+
quiet_loggers: list[str] = Field(
|
|
18
|
+
default_factory=lambda: ["httpx", "faiss", "faiss.loader", "slack_sdk"],
|
|
19
|
+
description="Additional loggers to set to external_level",
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class SlackSettings(BaseModel):
|
|
24
|
+
# Turn Slack integration on/off globally
|
|
25
|
+
enabled: bool = Field(default=False)
|
|
26
|
+
|
|
27
|
+
# Tokens
|
|
28
|
+
bot_token: SecretStr | None = None # xoxb-...
|
|
29
|
+
app_token: SecretStr | None = None # xapp-... (Socket Mode)
|
|
30
|
+
signing_secret: SecretStr | None = None # only needed for HTTP/webhook
|
|
31
|
+
|
|
32
|
+
# Transport mode flags
|
|
33
|
+
#
|
|
34
|
+
# Local / individual default:
|
|
35
|
+
# enabled = true
|
|
36
|
+
# socket_mode_enabled = true
|
|
37
|
+
# webhook_enabled = false
|
|
38
|
+
#
|
|
39
|
+
# Production / webhook default:
|
|
40
|
+
# enabled = true
|
|
41
|
+
# socket_mode_enabled = false (optional)
|
|
42
|
+
# webhook_enabled = true
|
|
43
|
+
|
|
44
|
+
socket_mode_enabled: bool = Field(
|
|
45
|
+
default=True, description="Use Slack Socket Mode (WS outbound) when app_token is set."
|
|
46
|
+
)
|
|
47
|
+
webhook_enabled: bool = Field(
|
|
48
|
+
default=False,
|
|
49
|
+
description="Expose /slack/events & /slack/interact HTTP endpoints for Slack.",
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
# Default routing
|
|
53
|
+
#
|
|
54
|
+
# For simple setups likely only need default_channel_id (+ maybe default_team_id).
|
|
55
|
+
# default_channel_key is the more general 'slack:team/T:chan/C' form.
|
|
56
|
+
# TODO: later we might deprecate the default setting in .env and require explicit channel keys in code.
|
|
57
|
+
default_team_id: str | None = None # e.g. 'T...'
|
|
58
|
+
default_channel_id: str | None = None # e.g. 'C...'
|
|
59
|
+
default_channel_key: str | None = None # e.g. 'slack:team/T...:chan/C...'
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class TelegramSettings(BaseModel):
|
|
63
|
+
enabled: bool = Field(default=False)
|
|
64
|
+
bot_token: SecretStr | None = None
|
|
65
|
+
|
|
66
|
+
# for webhook mode
|
|
67
|
+
webhook_enabled: bool = False
|
|
68
|
+
webhook_secret: SecretStr | None = None # used ONLY for HTTP webhook verification
|
|
69
|
+
|
|
70
|
+
# for local / dev mode
|
|
71
|
+
polling_enabled: bool = True # use getUpdates loop by default for local
|
|
72
|
+
|
|
73
|
+
# default chat key
|
|
74
|
+
default_chat_id: str | None = None
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class ContinuationStoreSettings(BaseModel):
|
|
78
|
+
kind: Literal["fs", "inmem"] = "fs"
|
|
79
|
+
secret: SecretStr | None = None
|
|
80
|
+
root: str = "./artifacts/continuations"
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class MemorySettings(BaseModel):
|
|
84
|
+
hot_limit: int = 1000
|
|
85
|
+
hot_ttl_s: int = 7 * 24 * 3600
|
|
86
|
+
signal_threshold: float = 0.25
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class ChannelSettings(BaseModel):
|
|
90
|
+
# room for Telegram / Console etc.
|
|
91
|
+
default: str = "console:stdin"
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class RAGSettings(BaseModel):
|
|
95
|
+
root: str = "./aethergraph_data/rag" # base dir for rag; should not use it unless customized
|
|
96
|
+
backend: str = "sqlite" # "sqlite" | "faiss"
|
|
97
|
+
index_path: str | None = None # defaults set at runtime if None
|
|
98
|
+
dim: int | None = None # only for faiss; optional
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class AppSettings(BaseSettings):
|
|
102
|
+
model_config = SettingsConfigDict(
|
|
103
|
+
env_prefix="AETHERGRAPH_", env_nested_delimiter="__", extra="ignore", case_sensitive=False
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
# top-level for workspace root
|
|
107
|
+
root: str = "./aethergraph_data"
|
|
108
|
+
|
|
109
|
+
logging: LoggingSettings = LoggingSettings()
|
|
110
|
+
slack: SlackSettings = SlackSettings()
|
|
111
|
+
telegram: TelegramSettings = TelegramSettings()
|
|
112
|
+
llm: LLMSettings = LLMSettings()
|
|
113
|
+
cont: ContinuationStoreSettings = ContinuationStoreSettings()
|
|
114
|
+
memory: MemorySettings = MemorySettings()
|
|
115
|
+
channels: ChannelSettings = ChannelSettings()
|
|
116
|
+
rag: RAGSettings = RAGSettings()
|
|
117
|
+
|
|
118
|
+
# Future fields:
|
|
119
|
+
# authn: ...
|
|
120
|
+
# authz: ...
|
|
121
|
+
# tracer: ...
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from contextvars import ContextVar
|
|
2
|
+
|
|
3
|
+
from .config import AppSettings
|
|
4
|
+
|
|
5
|
+
_current_cfg: ContextVar[AppSettings | None] = ContextVar("current_cfg", default=None)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def set_current_settings(cfg: AppSettings) -> None:
|
|
9
|
+
_current_cfg.set(cfg)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def current_settings() -> AppSettings:
|
|
13
|
+
cfg = _current_cfg.get()
|
|
14
|
+
if cfg is None:
|
|
15
|
+
raise RuntimeError("Settings not installed. Call set_current_settings() at startup.")
|
|
16
|
+
return cfg
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from pydantic import BaseModel, Field, SecretStr
|
|
2
|
+
|
|
3
|
+
from aethergraph.services.llm.providers import Provider
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class LLMProfile(BaseModel):
|
|
7
|
+
provider: Provider = "openai"
|
|
8
|
+
model: str = "gpt-4o-mini"
|
|
9
|
+
embed_model: str | None = None # separate embedding model
|
|
10
|
+
base_url: str | None = None
|
|
11
|
+
timeout: float = 60.0
|
|
12
|
+
|
|
13
|
+
# provider-specific
|
|
14
|
+
azure_deployment: str | None = None
|
|
15
|
+
|
|
16
|
+
# secrets (either direct value or ref name)
|
|
17
|
+
api_key: SecretStr | None = None
|
|
18
|
+
api_key_ref: str | None = Field(
|
|
19
|
+
default=None, description="Name in secret store, e.g. 'OPENAI_API_KEY'"
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class LLMSettings(BaseModel):
|
|
24
|
+
enabled: bool = True
|
|
25
|
+
default: LLMProfile = LLMProfile()
|
|
26
|
+
profiles: dict[str, LLMProfile] = Field(default_factory=dict)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# aethergraph/config_loader.py
|
|
2
|
+
from collections.abc import Iterable
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from .config import AppSettings
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _existing(paths: Iterable[Path]) -> list[Path]:
|
|
11
|
+
return [p for p in paths if p and p.exists()]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def load_settings() -> AppSettings:
|
|
15
|
+
log = logging.getLogger("aethergraph.config.loader")
|
|
16
|
+
|
|
17
|
+
# 1) explicit override
|
|
18
|
+
explicit = os.getenv("AETHERGRAPH_ENV_FILE")
|
|
19
|
+
explicit_path = Path(explicit).expanduser().resolve() if explicit else None
|
|
20
|
+
|
|
21
|
+
# 2) execution context (project) – where user runs `python ...`
|
|
22
|
+
cwd = Path.cwd()
|
|
23
|
+
|
|
24
|
+
# 3) workspace-level (if user sets it)
|
|
25
|
+
workspace = Path(os.getenv("AETHERGRAPH_ROOT", "./aethergraph_data")).expanduser().resolve()
|
|
26
|
+
|
|
27
|
+
# 4) user config dir (~/.config/aethergraph/.env or XDG)
|
|
28
|
+
xdg = Path(os.getenv("XDG_CONFIG_HOME", Path.home() / ".config")).expanduser().resolve()
|
|
29
|
+
user_cfg_env = xdg / "aethergraph" / ".env"
|
|
30
|
+
|
|
31
|
+
# Optional: keep a *repo dev* fallback if running from source
|
|
32
|
+
# (safe; only used if that path actually exists)
|
|
33
|
+
try:
|
|
34
|
+
repo_root = Path(__file__).resolve().parents[3]
|
|
35
|
+
except Exception:
|
|
36
|
+
repo_root = None
|
|
37
|
+
repo_env = (
|
|
38
|
+
(repo_root / ".env").resolve() if (repo_root and (repo_root / ".env").exists()) else None
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
candidates = _existing(
|
|
42
|
+
[
|
|
43
|
+
explicit_path or Path(), # explicit if set
|
|
44
|
+
cwd / ".env",
|
|
45
|
+
cwd / ".env.local",
|
|
46
|
+
workspace / ".env",
|
|
47
|
+
user_cfg_env,
|
|
48
|
+
repo_env if repo_env else Path(), # dev fallback only if exists
|
|
49
|
+
]
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
if explicit and not explicit_path.exists():
|
|
53
|
+
raise FileNotFoundError(f"AETHERGRAPH_ENV_FILE not found: {explicit_path}")
|
|
54
|
+
|
|
55
|
+
if not candidates:
|
|
56
|
+
log.warning("No .env files found; using OS environment variables only.")
|
|
57
|
+
return AppSettings()
|
|
58
|
+
|
|
59
|
+
# Later files override earlier ones
|
|
60
|
+
return AppSettings(_env_file=[str(p) for p in candidates])
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Typed exceptions (ValidationError, MigrationError, etc.)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class AetherGraphError(Exception):
|
|
5
|
+
"""Base class for all AetherGraph errors."""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class NodeContractError(AetherGraphError):
|
|
9
|
+
"""Raised when a TaskNodeRuntime violates its declared input/output contract."""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class MissingInputError(NodeContractError):
|
|
13
|
+
"""Raised when a required input key is missing."""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class MissingOutputError(NodeContractError):
|
|
17
|
+
"""Raised when a required output key is missing."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ExecutionError(AetherGraphError):
|
|
21
|
+
"""Raised when a node’s logic fails during execution."""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class GraphHasPendingWaits(RuntimeError):
|
|
25
|
+
"""Raised when attempting to finalize a graph that has pending waits."""
|
|
26
|
+
|
|
27
|
+
def __init__(
|
|
28
|
+
self, message: str, waiting_nodes: list[str], continuations: list[dict] | None = None
|
|
29
|
+
):
|
|
30
|
+
super().__init__(message)
|
|
31
|
+
self.waiting_nodes = waiting_nodes
|
|
32
|
+
self.continuations = continuations or []
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ResumeIncompatibleSnapshot(RuntimeError):
|
|
36
|
+
"""
|
|
37
|
+
Raised when a snapshot is not allowed for resume under the current policy
|
|
38
|
+
(e.g., contains non-JSON outputs or external refs like __aether_ref__).
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(self, run_id: str, reasons: list[str]):
|
|
42
|
+
super().__init__(f"Resume blocked for run_id={run_id}. " + " / ".join(reasons))
|
|
43
|
+
self.run_id = run_id
|
|
44
|
+
self.reasons = reasons
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from contextlib import AbstractAsyncContextManager
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Any, Protocol
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class Artifact:
|
|
10
|
+
artifact_id: str
|
|
11
|
+
uri: str
|
|
12
|
+
kind: str
|
|
13
|
+
bytes: int
|
|
14
|
+
sha256: str
|
|
15
|
+
mime: str | None
|
|
16
|
+
run_id: str
|
|
17
|
+
graph_id: str
|
|
18
|
+
node_id: str
|
|
19
|
+
tool_name: str
|
|
20
|
+
tool_version: str
|
|
21
|
+
created_at: str
|
|
22
|
+
labels: dict[str, Any]
|
|
23
|
+
metrics: dict[str, Any]
|
|
24
|
+
preview_uri: str | None = None # for rendering previews in UI, not tied to storage
|
|
25
|
+
pinned: bool = False
|
|
26
|
+
|
|
27
|
+
def to_dict(self) -> dict[str, Any]:
|
|
28
|
+
return {
|
|
29
|
+
"artifact_id": self.artifact_id,
|
|
30
|
+
"uri": self.uri,
|
|
31
|
+
"kind": self.kind,
|
|
32
|
+
"bytes": self.bytes,
|
|
33
|
+
"sha256": self.sha256,
|
|
34
|
+
"mime": self.mime,
|
|
35
|
+
"run_id": self.run_id,
|
|
36
|
+
"graph_id": self.graph_id,
|
|
37
|
+
"node_id": self.node_id,
|
|
38
|
+
"tool_name": self.tool_name,
|
|
39
|
+
"tool_version": self.tool_version,
|
|
40
|
+
"created_at": self.created_at,
|
|
41
|
+
"labels": self.labels,
|
|
42
|
+
"metrics": self.metrics,
|
|
43
|
+
"preview_uri": self.preview_uri,
|
|
44
|
+
"pinned": self.pinned,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class AsyncArtifactStore(Protocol):
|
|
49
|
+
async def save_file(
|
|
50
|
+
self,
|
|
51
|
+
*,
|
|
52
|
+
path: str,
|
|
53
|
+
kind: str,
|
|
54
|
+
run_id: str,
|
|
55
|
+
graph_id: str,
|
|
56
|
+
node_id: str,
|
|
57
|
+
tool_name: str,
|
|
58
|
+
tool_version: str,
|
|
59
|
+
suggested_uri: str | None = None,
|
|
60
|
+
pin: bool = False,
|
|
61
|
+
labels: dict | None = None,
|
|
62
|
+
metrics: dict | None = None,
|
|
63
|
+
preview_uri: str | None = None,
|
|
64
|
+
) -> Artifact: ...
|
|
65
|
+
async def open_writer(
|
|
66
|
+
self,
|
|
67
|
+
*,
|
|
68
|
+
kind: str,
|
|
69
|
+
run_id: str,
|
|
70
|
+
graph_id: str,
|
|
71
|
+
node_id: str,
|
|
72
|
+
tool_name: str,
|
|
73
|
+
tool_version: str,
|
|
74
|
+
planned_ext: str | None = None,
|
|
75
|
+
pin: bool = False,
|
|
76
|
+
) -> AbstractAsyncContextManager[Any]: ...
|
|
77
|
+
async def plan_staging_path(self, planned_ext: str = "") -> str: ...
|
|
78
|
+
async def ingest_staged_file(
|
|
79
|
+
self,
|
|
80
|
+
*,
|
|
81
|
+
staged_path: str,
|
|
82
|
+
kind: str,
|
|
83
|
+
run_id: str,
|
|
84
|
+
graph_id: str,
|
|
85
|
+
node_id: str,
|
|
86
|
+
tool_name: str,
|
|
87
|
+
tool_version: str,
|
|
88
|
+
pin: bool = False,
|
|
89
|
+
labels: dict | None = None,
|
|
90
|
+
metrics: dict | None = None,
|
|
91
|
+
preview_uri: str | None = None,
|
|
92
|
+
suggested_uri: str | None = None,
|
|
93
|
+
) -> Artifact: ...
|
|
94
|
+
async def plan_staging_dir(self, suffix: str = "") -> str: ...
|
|
95
|
+
async def ingest_directory(
|
|
96
|
+
self,
|
|
97
|
+
*,
|
|
98
|
+
staged_dir: str,
|
|
99
|
+
kind: str,
|
|
100
|
+
run_id: str,
|
|
101
|
+
graph_id: str,
|
|
102
|
+
node_id: str,
|
|
103
|
+
tool_name: str,
|
|
104
|
+
tool_version: str,
|
|
105
|
+
include: list[str] | None = None,
|
|
106
|
+
exclude: list[str] | None = None,
|
|
107
|
+
index_children: bool = False,
|
|
108
|
+
pin: bool = False,
|
|
109
|
+
labels: dict | None = None,
|
|
110
|
+
metrics: dict | None = None,
|
|
111
|
+
suggested_uri: str | None = None,
|
|
112
|
+
archive: bool = False,
|
|
113
|
+
archive_name: str = "bundle.tar.gz",
|
|
114
|
+
cleanup: bool = True,
|
|
115
|
+
store: str | None = None,
|
|
116
|
+
) -> Artifact: ...
|
|
117
|
+
async def load_artifact(self, uri: str) -> Any: ...
|
|
118
|
+
async def load_artifact_bytes(self, uri: str) -> bytes: ...
|
|
119
|
+
async def load_artifact_dir(self, uri: str) -> str: ...
|
|
120
|
+
async def cleanup_tmp(self, max_age_hours: int = 24) -> None: ...
|
|
121
|
+
async def save_text(self, payload: str, suggested_uri: str | None = None) -> Artifact: ...
|
|
122
|
+
async def save_json(self, payload: dict, suggested_uri: str | None = None) -> Artifact: ...
|
|
123
|
+
@property
|
|
124
|
+
def base_uri(self) -> str: ...
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class AsyncArtifactIndex(Protocol):
|
|
128
|
+
async def upsert(self, a: Artifact) -> None: ...
|
|
129
|
+
async def list_for_run(self, run_id: str) -> list[Artifact]: ...
|
|
130
|
+
async def search(
|
|
131
|
+
self,
|
|
132
|
+
*,
|
|
133
|
+
kind: str | None = None,
|
|
134
|
+
labels: dict | None = None,
|
|
135
|
+
metric: str | None = None,
|
|
136
|
+
mode: str | None = None,
|
|
137
|
+
) -> list[Artifact]: ...
|
|
138
|
+
async def best(
|
|
139
|
+
self, *, kind: str, metric: str, mode: str, filters: dict | None = None
|
|
140
|
+
) -> Artifact | None: ...
|
|
141
|
+
async def pin(self, artifact_id: str, pinned: bool = True) -> None: ...
|
|
142
|
+
async def record_occurrence(self, a: Artifact, extra_labels: dict | None = None) -> None: ...
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Any, Literal, Protocol, TypedDict
|
|
3
|
+
|
|
4
|
+
EventType = Literal[
|
|
5
|
+
"agent.message",
|
|
6
|
+
"agent.message.update", # simple text messages
|
|
7
|
+
"agent.stream.start",
|
|
8
|
+
"agent.stream.delta",
|
|
9
|
+
"agent.stream.end", # streaming messages
|
|
10
|
+
"agent.progress.start",
|
|
11
|
+
"agent.progress.update",
|
|
12
|
+
"agent.progress.end", # progress bar
|
|
13
|
+
"session.need_input",
|
|
14
|
+
"session.need_approval",
|
|
15
|
+
"session.waiting",
|
|
16
|
+
"file.upload",
|
|
17
|
+
"link.buttons", # link preview with buttons
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class FileRef(TypedDict, total=False):
|
|
22
|
+
id: str # platform file id (e.g., Slack file ID)
|
|
23
|
+
name: str # suggested filename
|
|
24
|
+
mimetype: str # MIME type, e.g., "image/png", "application/pdf"
|
|
25
|
+
size: int # size in bytes
|
|
26
|
+
uri: str # URL to download the file (artifact storage or platform URL)
|
|
27
|
+
url_private: str # private URL if applicable (e.g., Slack private URL)
|
|
28
|
+
platform: str # platform name, e.g., "slack", "telegram", "console"
|
|
29
|
+
channel_key: str # normalized channel key where the file was sent, e.g., "slack:team/T:chan/C"
|
|
30
|
+
ts: str | float # timestamp of the file upload
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class Button:
|
|
35
|
+
label: str
|
|
36
|
+
value: str | None = None
|
|
37
|
+
url: str | None = None
|
|
38
|
+
style: Literal["primary", "danger", "default"] | None = None # for slack buttons
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class OutEvent:
|
|
43
|
+
type: EventType # "agent.message", "session.need_input", "session.need_approval", "agent.stream.*"
|
|
44
|
+
channel: str # routing key, e.g., "console:stdout" or "slack:team/T:chan/C[:thread/TS]"
|
|
45
|
+
text: str | None = None
|
|
46
|
+
rich: dict[str, Any] | None = None
|
|
47
|
+
meta: dict[str, Any] | None = None
|
|
48
|
+
# Optional structured extras most adapters can use, e.g., for buttons, attachments, files, etc.
|
|
49
|
+
buttons: dict[str, Button] | None = None # for approvals or link actions
|
|
50
|
+
image: dict[str, Any] | None = None # e.g., {"url": "...", "alt": "...", "title": "..."}
|
|
51
|
+
file: dict[str, Any] | None = (
|
|
52
|
+
None # e.g., {"bytes" b"...", "filename": "...", "mimetype": "..."} or {"url": "...", "filename": "...", "mimetype": "..."}
|
|
53
|
+
)
|
|
54
|
+
upsert_key: str | None = None # for idempotent upserts, e.g., message ID to update same message
|
|
55
|
+
|
|
56
|
+
def to_printable(self) -> str:
|
|
57
|
+
"""Only contains printable parts of the event."""
|
|
58
|
+
return (
|
|
59
|
+
f"Event(type={self.type}, channel={self.channel}, text={self.text}, meta={self.meta})"
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class ChannelAdapter(Protocol):
|
|
64
|
+
# Capabilities helper
|
|
65
|
+
capabilities: set[str] # e.g. {"text", "image", "file", "buttons", "rich"}
|
|
66
|
+
|
|
67
|
+
async def send(self, event: OutEvent) -> None:
|
|
68
|
+
"""
|
|
69
|
+
Send an outgoing event to the appropriate channel.
|
|
70
|
+
E.g., print to console, post to Slack, enqueue in UI, etc.
|
|
71
|
+
"""
|
|
72
|
+
pass
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Protocol
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class AsyncContinuationStore(Protocol):
|
|
7
|
+
async def mint_token(self, run_id: str, node_id: str, attempts: int) -> str: ...
|
|
8
|
+
async def save(self, cont: Any) -> None: ...
|
|
9
|
+
async def get(self, run_id: str, node_id: str) -> Any | None: ...
|
|
10
|
+
async def delete(self, run_id: str, node_id: str) -> None: ...
|
|
11
|
+
|
|
12
|
+
async def get_by_token(self, token: str) -> Any | None: ...
|
|
13
|
+
async def mark_closed(self, token: str) -> None: ...
|
|
14
|
+
async def verify_token(self, run_id: str, node_id: str, token: str) -> bool: ...
|
|
15
|
+
|
|
16
|
+
async def bind_correlator(self, *, token: str, corr: Any) -> None: ...
|
|
17
|
+
async def find_by_correlator(self, *, corr: Any) -> Any | None: ...
|
|
18
|
+
|
|
19
|
+
# Optional helpers
|
|
20
|
+
async def last_open(self, *, channel: str, kind: str) -> Any | None: ...
|
|
21
|
+
async def list_waits(self) -> list[dict[str, Any]]: ...
|
|
22
|
+
async def clear(self) -> None: ...
|
|
23
|
+
async def alias_for(self, token: str) -> str | None: ...
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from collections.abc import Awaitable, Callable
|
|
2
|
+
from typing import Any, Protocol
|
|
3
|
+
|
|
4
|
+
Handler = Callable[[dict[str, Any]], Awaitable[None]]
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class EventBus(Protocol):
|
|
8
|
+
"""Protocol for an event bus service."""
|
|
9
|
+
|
|
10
|
+
async def publish(self, topic: str, event: dict[str, Any]) -> None: ...
|
|
11
|
+
|
|
12
|
+
def subscribe(self, topic: str, handler: Handler) -> None: ...
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Iterable
|
|
4
|
+
from typing import Any, Protocol
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class AsyncKV(Protocol):
|
|
8
|
+
async def get(self, key: str, default: Any = None) -> Any: ...
|
|
9
|
+
async def set(self, key: str, value: Any, *, ttl_s: int | None = None) -> None: ...
|
|
10
|
+
async def delete(self, key: str) -> None: ...
|
|
11
|
+
|
|
12
|
+
# list helpers (JSON list values)
|
|
13
|
+
async def list_append_unique(
|
|
14
|
+
self, key: str, items: list, *, id_key: str = "id", ttl_s: int | None = None
|
|
15
|
+
) -> list: ...
|
|
16
|
+
async def list_pop_all(self, key: str) -> list: ...
|
|
17
|
+
|
|
18
|
+
# optional but useful
|
|
19
|
+
async def mget(self, keys: Iterable[str]) -> dict[str, Any]: ...
|
|
20
|
+
async def mset(self, items: dict[str, Any], *, ttl_s: int | None = None) -> None: ...
|
|
21
|
+
async def incr(self, key: str, amount: int = 1, *, ttl_s: int | None = None) -> int: ...
|
|
22
|
+
async def exists(self, key: str) -> bool: ...
|
|
23
|
+
async def expire(self, key: str, ttl_s: int) -> bool: ...
|
|
24
|
+
async def scan_prefix(self, prefix: str, limit: int = 1000) -> Iterable[str]: ...
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from typing import Any, Protocol
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class LLMClientProtocol(Protocol):
|
|
5
|
+
async def chat(self, messages: list[dict[str, Any]], **kw) -> tuple[str, dict[str, int]]: ...
|
|
6
|
+
async def embed(self, texts: list[str], **kw) -> list[list[float]]: ...
|
|
7
|
+
async def raw(
|
|
8
|
+
self,
|
|
9
|
+
*,
|
|
10
|
+
method: str = "POST",
|
|
11
|
+
path: str | None = None,
|
|
12
|
+
url: str | None = None,
|
|
13
|
+
json: Any | None = None,
|
|
14
|
+
params: dict[str, Any] | None = None,
|
|
15
|
+
headers: dict[str, str] | None = None,
|
|
16
|
+
return_response: bool = False,
|
|
17
|
+
) -> Any: ...
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from typing import Any, TypedDict
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class MCPTool(TypedDict):
|
|
5
|
+
name: str
|
|
6
|
+
description: str | None
|
|
7
|
+
input_schema: dict[str, Any] | None
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class MCPResource(TypedDict):
|
|
11
|
+
uri: str
|
|
12
|
+
mime: str | None
|
|
13
|
+
description: str | None
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class MCPClientProtocol:
|
|
17
|
+
async def open(self): ...
|
|
18
|
+
async def close(self): ...
|
|
19
|
+
async def list_tools(self) -> list[MCPTool]: ...
|
|
20
|
+
async def call(self, tool: str, params: dict[str, Any] | None = None) -> dict[str, Any]: ...
|
|
21
|
+
async def list_resources(self) -> list[MCPResource]: ...
|
|
22
|
+
async def read_resource(self, uri: str) -> dict[str, Any]: ...
|