aethergraph 0.1.0a1__py3-none-any.whl → 0.1.0a2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- aethergraph/__init__.py +4 -10
- aethergraph/__main__.py +293 -0
- aethergraph/api/v1/__init__.py +0 -0
- aethergraph/api/v1/agents.py +46 -0
- aethergraph/api/v1/apps.py +70 -0
- aethergraph/api/v1/artifacts.py +415 -0
- aethergraph/api/v1/channels.py +89 -0
- aethergraph/api/v1/deps.py +168 -0
- aethergraph/api/v1/graphs.py +259 -0
- aethergraph/api/v1/identity.py +25 -0
- aethergraph/api/v1/memory.py +353 -0
- aethergraph/api/v1/misc.py +47 -0
- aethergraph/api/v1/pagination.py +29 -0
- aethergraph/api/v1/runs.py +568 -0
- aethergraph/api/v1/schemas.py +535 -0
- aethergraph/api/v1/session.py +323 -0
- aethergraph/api/v1/stats.py +201 -0
- aethergraph/api/v1/viz.py +152 -0
- aethergraph/config/config.py +22 -0
- aethergraph/config/loader.py +3 -2
- aethergraph/config/storage.py +209 -0
- aethergraph/contracts/__init__.py +0 -0
- aethergraph/contracts/services/__init__.py +0 -0
- aethergraph/contracts/services/artifacts.py +27 -14
- aethergraph/contracts/services/memory.py +45 -17
- aethergraph/contracts/services/metering.py +129 -0
- aethergraph/contracts/services/runs.py +50 -0
- aethergraph/contracts/services/sessions.py +87 -0
- aethergraph/contracts/services/state_stores.py +3 -0
- aethergraph/contracts/services/viz.py +44 -0
- aethergraph/contracts/storage/artifact_index.py +88 -0
- aethergraph/contracts/storage/artifact_store.py +99 -0
- aethergraph/contracts/storage/async_kv.py +34 -0
- aethergraph/contracts/storage/blob_store.py +50 -0
- aethergraph/contracts/storage/doc_store.py +35 -0
- aethergraph/contracts/storage/event_log.py +31 -0
- aethergraph/contracts/storage/vector_index.py +48 -0
- aethergraph/core/__init__.py +0 -0
- aethergraph/core/execution/forward_scheduler.py +13 -2
- aethergraph/core/execution/global_scheduler.py +21 -15
- aethergraph/core/execution/step_forward.py +10 -1
- aethergraph/core/graph/__init__.py +0 -0
- aethergraph/core/graph/graph_builder.py +8 -4
- aethergraph/core/graph/graph_fn.py +156 -15
- aethergraph/core/graph/graph_spec.py +8 -0
- aethergraph/core/graph/graphify.py +146 -27
- aethergraph/core/graph/node_spec.py +0 -2
- aethergraph/core/graph/node_state.py +3 -0
- aethergraph/core/graph/task_graph.py +39 -1
- aethergraph/core/runtime/__init__.py +0 -0
- aethergraph/core/runtime/ad_hoc_context.py +64 -4
- aethergraph/core/runtime/base_service.py +28 -4
- aethergraph/core/runtime/execution_context.py +13 -15
- aethergraph/core/runtime/graph_runner.py +222 -37
- aethergraph/core/runtime/node_context.py +510 -6
- aethergraph/core/runtime/node_services.py +12 -5
- aethergraph/core/runtime/recovery.py +15 -1
- aethergraph/core/runtime/run_manager.py +783 -0
- aethergraph/core/runtime/run_manager_local.py +204 -0
- aethergraph/core/runtime/run_registration.py +2 -2
- aethergraph/core/runtime/run_types.py +89 -0
- aethergraph/core/runtime/runtime_env.py +136 -7
- aethergraph/core/runtime/runtime_metering.py +71 -0
- aethergraph/core/runtime/runtime_registry.py +36 -13
- aethergraph/core/runtime/runtime_services.py +194 -6
- aethergraph/core/tools/builtins/toolset.py +1 -1
- aethergraph/core/tools/toolkit.py +5 -0
- aethergraph/plugins/agents/default_chat_agent copy.py +90 -0
- aethergraph/plugins/agents/default_chat_agent.py +171 -0
- aethergraph/plugins/agents/shared.py +81 -0
- aethergraph/plugins/channel/adapters/webui.py +112 -112
- aethergraph/plugins/channel/routes/webui_routes.py +367 -102
- aethergraph/plugins/channel/utils/slack_utils.py +115 -59
- aethergraph/plugins/channel/utils/telegram_utils.py +88 -47
- aethergraph/plugins/channel/websockets/weibui_ws.py +172 -0
- aethergraph/runtime/__init__.py +15 -0
- aethergraph/server/app_factory.py +190 -34
- aethergraph/server/clients/channel_client.py +202 -0
- aethergraph/server/http/channel_http_routes.py +116 -0
- aethergraph/server/http/channel_ws_routers.py +45 -0
- aethergraph/server/loading.py +117 -0
- aethergraph/server/server.py +131 -0
- aethergraph/server/server_state.py +240 -0
- aethergraph/server/start.py +227 -66
- aethergraph/server/ui_static/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- aethergraph/server/ui_static/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- aethergraph/server/ui_static/assets/index-BR5GtXcZ.css +1 -0
- aethergraph/server/ui_static/assets/index-CQ0HZZ83.js +400 -0
- aethergraph/server/ui_static/index.html +15 -0
- aethergraph/server/ui_static/logo.png +0 -0
- aethergraph/services/artifacts/__init__.py +0 -0
- aethergraph/services/artifacts/facade.py +1239 -132
- aethergraph/services/auth/{dev.py → authn.py} +0 -8
- aethergraph/services/auth/authz.py +100 -0
- aethergraph/services/channel/__init__.py +0 -0
- aethergraph/services/channel/channel_bus.py +19 -1
- aethergraph/services/channel/factory.py +13 -1
- aethergraph/services/channel/ingress.py +311 -0
- aethergraph/services/channel/queue_adapter.py +75 -0
- aethergraph/services/channel/session.py +502 -19
- aethergraph/services/container/default_container.py +122 -43
- aethergraph/services/continuations/continuation.py +6 -0
- aethergraph/services/continuations/stores/fs_store.py +19 -0
- aethergraph/services/eventhub/event_hub.py +76 -0
- aethergraph/services/kv/__init__.py +0 -0
- aethergraph/services/kv/ephemeral.py +244 -0
- aethergraph/services/llm/__init__.py +0 -0
- aethergraph/services/llm/generic_client copy.py +691 -0
- aethergraph/services/llm/generic_client.py +1288 -187
- aethergraph/services/llm/providers.py +3 -1
- aethergraph/services/llm/types.py +47 -0
- aethergraph/services/llm/utils.py +284 -0
- aethergraph/services/logger/std.py +3 -0
- aethergraph/services/mcp/__init__.py +9 -0
- aethergraph/services/mcp/http_client.py +38 -0
- aethergraph/services/mcp/service.py +225 -1
- aethergraph/services/mcp/stdio_client.py +41 -6
- aethergraph/services/mcp/ws_client.py +44 -2
- aethergraph/services/memory/__init__.py +0 -0
- aethergraph/services/memory/distillers/llm_long_term.py +234 -0
- aethergraph/services/memory/distillers/llm_meta_summary.py +398 -0
- aethergraph/services/memory/distillers/long_term.py +225 -0
- aethergraph/services/memory/facade/__init__.py +3 -0
- aethergraph/services/memory/facade/chat.py +440 -0
- aethergraph/services/memory/facade/core.py +447 -0
- aethergraph/services/memory/facade/distillation.py +424 -0
- aethergraph/services/memory/facade/rag.py +410 -0
- aethergraph/services/memory/facade/results.py +315 -0
- aethergraph/services/memory/facade/retrieval.py +139 -0
- aethergraph/services/memory/facade/types.py +77 -0
- aethergraph/services/memory/facade/utils.py +43 -0
- aethergraph/services/memory/facade_dep.py +1539 -0
- aethergraph/services/memory/factory.py +9 -3
- aethergraph/services/memory/utils.py +10 -0
- aethergraph/services/metering/eventlog_metering.py +470 -0
- aethergraph/services/metering/noop.py +25 -4
- aethergraph/services/rag/__init__.py +0 -0
- aethergraph/services/rag/facade.py +279 -23
- aethergraph/services/rag/index_factory.py +2 -2
- aethergraph/services/rag/node_rag.py +317 -0
- aethergraph/services/rate_limit/inmem_rate_limit.py +24 -0
- aethergraph/services/registry/__init__.py +0 -0
- aethergraph/services/registry/agent_app_meta.py +419 -0
- aethergraph/services/registry/registry_key.py +1 -1
- aethergraph/services/registry/unified_registry.py +74 -6
- aethergraph/services/scope/scope.py +159 -0
- aethergraph/services/scope/scope_factory.py +164 -0
- aethergraph/services/state_stores/serialize.py +5 -0
- aethergraph/services/state_stores/utils.py +2 -1
- aethergraph/services/viz/__init__.py +0 -0
- aethergraph/services/viz/facade.py +413 -0
- aethergraph/services/viz/viz_service.py +69 -0
- aethergraph/storage/artifacts/artifact_index_jsonl.py +180 -0
- aethergraph/storage/artifacts/artifact_index_sqlite.py +426 -0
- aethergraph/storage/artifacts/cas_store.py +422 -0
- aethergraph/storage/artifacts/fs_cas.py +18 -0
- aethergraph/storage/artifacts/s3_cas.py +14 -0
- aethergraph/storage/artifacts/utils.py +124 -0
- aethergraph/storage/blob/fs_blob.py +86 -0
- aethergraph/storage/blob/s3_blob.py +115 -0
- aethergraph/storage/continuation_store/fs_cont.py +283 -0
- aethergraph/storage/continuation_store/inmem_cont.py +146 -0
- aethergraph/storage/continuation_store/kvdoc_cont.py +261 -0
- aethergraph/storage/docstore/fs_doc.py +63 -0
- aethergraph/storage/docstore/sqlite_doc.py +31 -0
- aethergraph/storage/docstore/sqlite_doc_sync.py +90 -0
- aethergraph/storage/eventlog/fs_event.py +136 -0
- aethergraph/storage/eventlog/sqlite_event.py +47 -0
- aethergraph/storage/eventlog/sqlite_event_sync.py +178 -0
- aethergraph/storage/factory.py +432 -0
- aethergraph/storage/fs_utils.py +28 -0
- aethergraph/storage/graph_state_store/state_store.py +64 -0
- aethergraph/storage/kv/inmem_kv.py +103 -0
- aethergraph/storage/kv/layered_kv.py +52 -0
- aethergraph/storage/kv/sqlite_kv.py +39 -0
- aethergraph/storage/kv/sqlite_kv_sync.py +98 -0
- aethergraph/storage/memory/event_persist.py +68 -0
- aethergraph/storage/memory/fs_persist.py +118 -0
- aethergraph/{services/memory/hotlog_kv.py → storage/memory/hotlog.py} +8 -2
- aethergraph/{services → storage}/memory/indices.py +31 -7
- aethergraph/storage/metering/meter_event.py +55 -0
- aethergraph/storage/runs/doc_store.py +280 -0
- aethergraph/storage/runs/inmen_store.py +82 -0
- aethergraph/storage/runs/sqlite_run_store.py +403 -0
- aethergraph/storage/sessions/doc_store.py +183 -0
- aethergraph/storage/sessions/inmem_store.py +110 -0
- aethergraph/storage/sessions/sqlite_session_store.py +399 -0
- aethergraph/storage/vector_index/chroma_index.py +138 -0
- aethergraph/storage/vector_index/faiss_index.py +179 -0
- aethergraph/storage/vector_index/sqlite_index.py +187 -0
- {aethergraph-0.1.0a1.dist-info → aethergraph-0.1.0a2.dist-info}/METADATA +138 -31
- aethergraph-0.1.0a2.dist-info/RECORD +356 -0
- aethergraph-0.1.0a2.dist-info/entry_points.txt +3 -0
- aethergraph/services/artifacts/factory.py +0 -35
- aethergraph/services/artifacts/fs_store.py +0 -656
- aethergraph/services/artifacts/jsonl_index.py +0 -123
- aethergraph/services/artifacts/sqlite_index.py +0 -209
- aethergraph/services/memory/distillers/episode.py +0 -116
- aethergraph/services/memory/distillers/rolling.py +0 -74
- aethergraph/services/memory/facade.py +0 -633
- aethergraph/services/memory/persist_fs.py +0 -40
- aethergraph/services/rag/index/base.py +0 -27
- aethergraph/services/rag/index/faiss_index.py +0 -121
- aethergraph/services/rag/index/sqlite_index.py +0 -134
- aethergraph-0.1.0a1.dist-info/RECORD +0 -182
- aethergraph-0.1.0a1.dist-info/entry_points.txt +0 -2
- {aethergraph-0.1.0a1.dist-info → aethergraph-0.1.0a2.dist-info}/WHEEL +0 -0
- {aethergraph-0.1.0a1.dist-info → aethergraph-0.1.0a2.dist-info}/licenses/LICENSE +0 -0
- {aethergraph-0.1.0a1.dist-info → aethergraph-0.1.0a2.dist-info}/licenses/NOTICE +0 -0
- {aethergraph-0.1.0a1.dist-info → aethergraph-0.1.0a2.dist-info}/top_level.txt +0 -0
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
# services/auth/dev.py
|
|
2
1
|
class DevTokenAuthn:
|
|
3
2
|
"""Development token authenticator. Accepts any token, returns 'dev' as subject."""
|
|
4
3
|
|
|
@@ -7,10 +6,3 @@ class DevTokenAuthn:
|
|
|
7
6
|
|
|
8
7
|
async def whoami(self, token: str | None) -> dict:
|
|
9
8
|
return {"subject": token or "dev", "roles": ["admin"]}
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
class AllowAllAuthz:
|
|
13
|
-
"""Development authorizer that allows all actions."""
|
|
14
|
-
|
|
15
|
-
async def allow(self, actor: dict, action: str, resource: str) -> bool:
|
|
16
|
-
return True
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import TYPE_CHECKING, Literal, Protocol
|
|
5
|
+
|
|
6
|
+
from fastapi import HTTPException, status
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from aethergraph.api.v1.deps import RequestIdentity
|
|
10
|
+
|
|
11
|
+
AuthZServiceScope = Literal["runs", "artifacts", "graphs", "admin", "system"]
|
|
12
|
+
AuthZServiceAction = Literal["read", "write", "execute", "delete", "admin"]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class AuthZService(Protocol):
|
|
16
|
+
async def authorize(
|
|
17
|
+
self,
|
|
18
|
+
*,
|
|
19
|
+
identity: RequestIdentity,
|
|
20
|
+
scope: AuthZServiceScope,
|
|
21
|
+
action: AuthZServiceAction,
|
|
22
|
+
) -> None:
|
|
23
|
+
"""Authorize the given identity to perform the action within the scope.
|
|
24
|
+
|
|
25
|
+
Raises HTTPException with status 403 if not authorized.
|
|
26
|
+
"""
|
|
27
|
+
...
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class AllowAllAuthz(AuthZService):
|
|
32
|
+
"""
|
|
33
|
+
Default OSS-safe behavior: everything is allowed.
|
|
34
|
+
Useful for local/demo, and as a safe fallback if authz isn't configured.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
async def authorize(
|
|
38
|
+
self,
|
|
39
|
+
*,
|
|
40
|
+
identity: RequestIdentity,
|
|
41
|
+
scope: AuthZServiceScope,
|
|
42
|
+
action: AuthZServiceAction,
|
|
43
|
+
) -> None:
|
|
44
|
+
"""Always allow."""
|
|
45
|
+
return
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass
|
|
49
|
+
class BasicAuthz(AuthZService):
|
|
50
|
+
"""
|
|
51
|
+
Minimal policy based on mode and roles:
|
|
52
|
+
- local: allow everything
|
|
53
|
+
- demo: allow normal operations, block admin/system stuff
|
|
54
|
+
- cloud: allow everything except admin unless role "admin" is present
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
allow_local_admin: bool = False
|
|
58
|
+
|
|
59
|
+
async def authorize(
|
|
60
|
+
self,
|
|
61
|
+
*,
|
|
62
|
+
identity: RequestIdentity,
|
|
63
|
+
scope: AuthZServiceScope,
|
|
64
|
+
action: AuthZServiceAction,
|
|
65
|
+
) -> None:
|
|
66
|
+
# Local dev: basically god-mode
|
|
67
|
+
if identity.mode == "local":
|
|
68
|
+
if self.allow_local_admin:
|
|
69
|
+
return
|
|
70
|
+
if scope != "admin":
|
|
71
|
+
return
|
|
72
|
+
|
|
73
|
+
# Demo mode: no admin / system endpoints
|
|
74
|
+
if identity.mode == "demo":
|
|
75
|
+
if scope in ("admin", "system"):
|
|
76
|
+
raise HTTPException(
|
|
77
|
+
status_code=status.HTTP_403_FORBIDDEN,
|
|
78
|
+
detail="Admin/system operations are disabled in demo mode.",
|
|
79
|
+
)
|
|
80
|
+
return
|
|
81
|
+
|
|
82
|
+
# Cloud mode: basic, role-based admin
|
|
83
|
+
if identity.mode == "cloud":
|
|
84
|
+
if scope in ("admin", "system"):
|
|
85
|
+
if "admin" not in identity.roles:
|
|
86
|
+
raise HTTPException(
|
|
87
|
+
status_code=status.HTTP_403_FORBIDDEN,
|
|
88
|
+
detail="Admin privileges required.",
|
|
89
|
+
)
|
|
90
|
+
return
|
|
91
|
+
|
|
92
|
+
# Non-admin scopes: for now, allow everything.
|
|
93
|
+
# Later you can restrict per-org / per-resource here.
|
|
94
|
+
return
|
|
95
|
+
|
|
96
|
+
# Fallback: if someone invents a new mode and forgets to handle it
|
|
97
|
+
raise HTTPException(
|
|
98
|
+
status_code=status.HTTP_403_FORBIDDEN,
|
|
99
|
+
detail=f"Unknown auth mode: {identity.mode}",
|
|
100
|
+
)
|
|
File without changes
|
|
@@ -212,10 +212,28 @@ class ChannelBus:
|
|
|
212
212
|
"resume_key": resume_key,
|
|
213
213
|
}
|
|
214
214
|
|
|
215
|
+
# Enrich continuation meta with the same context fields we attach
|
|
216
|
+
# on normal channel events (if present on the continuation object).
|
|
217
|
+
session_id = getattr(continuation, "session_id", None)
|
|
218
|
+
if session_id is not None:
|
|
219
|
+
meta.setdefault("session_id", session_id)
|
|
220
|
+
|
|
221
|
+
agent_id = getattr(continuation, "agent_id", None)
|
|
222
|
+
if agent_id is not None:
|
|
223
|
+
meta.setdefault("agent_id", agent_id)
|
|
224
|
+
|
|
225
|
+
app_id = getattr(continuation, "app_id", None)
|
|
226
|
+
if app_id is not None:
|
|
227
|
+
meta.setdefault("app_id", app_id)
|
|
228
|
+
|
|
229
|
+
graph_id = getattr(continuation, "graph_id", None)
|
|
230
|
+
if graph_id is not None:
|
|
231
|
+
meta.setdefault("graph_id", graph_id)
|
|
232
|
+
|
|
215
233
|
# Shape event
|
|
216
234
|
if kind == "user_input":
|
|
217
235
|
silent = False
|
|
218
|
-
if hasattr(continuation, "payload"):
|
|
236
|
+
if hasattr(continuation, "payload") and isinstance(continuation.payload, dict):
|
|
219
237
|
silent = continuation.payload.get("_silent", False)
|
|
220
238
|
|
|
221
239
|
txt = prompt if isinstance(prompt, str) else None
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
# channels/factory.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
2
4
|
import os
|
|
3
5
|
from typing import Any
|
|
4
6
|
|
|
5
7
|
from aethergraph.config.config import AppSettings
|
|
8
|
+
from aethergraph.contracts.storage.event_log import EventLog
|
|
6
9
|
from aethergraph.plugins.channel.adapters.console import ConsoleChannelAdapter
|
|
7
10
|
from aethergraph.plugins.channel.adapters.file import FileChannelAdapter
|
|
8
11
|
from aethergraph.plugins.channel.adapters.slack import SlackChannelAdapter
|
|
@@ -11,7 +14,9 @@ from aethergraph.plugins.channel.adapters.webhook import WebhookChannelAdapter
|
|
|
11
14
|
from aethergraph.services.channel.channel_bus import ChannelBus
|
|
12
15
|
|
|
13
16
|
|
|
14
|
-
def make_channel_adapters_from_env(
|
|
17
|
+
def make_channel_adapters_from_env(
|
|
18
|
+
cfg: AppSettings, event_log: EventLog | None = None
|
|
19
|
+
) -> dict[str, Any]:
|
|
15
20
|
# Always include console adapter
|
|
16
21
|
adapters = {"console": ConsoleChannelAdapter()}
|
|
17
22
|
|
|
@@ -29,6 +34,13 @@ def make_channel_adapters_from_env(cfg: AppSettings) -> dict[str, Any]:
|
|
|
29
34
|
|
|
30
35
|
# include webhook adapter
|
|
31
36
|
adapters["webhook"] = WebhookChannelAdapter()
|
|
37
|
+
|
|
38
|
+
# Always include webui adapter
|
|
39
|
+
from aethergraph.plugins.channel.adapters.webui import WebUIChannelAdapter
|
|
40
|
+
|
|
41
|
+
if event_log is None:
|
|
42
|
+
raise ValueError("event_log must be provided to create WebUIChannelAdapter")
|
|
43
|
+
adapters["ui"] = WebUIChannelAdapter(event_log=event_log)
|
|
32
44
|
return adapters
|
|
33
45
|
|
|
34
46
|
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Iterable
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import aiohttp
|
|
8
|
+
|
|
9
|
+
from aethergraph.services.continuations.continuation import Continuation, Correlator
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class IncomingFile:
|
|
14
|
+
"""
|
|
15
|
+
Generic description of a file coming from an external UI.
|
|
16
|
+
|
|
17
|
+
You can:
|
|
18
|
+
- pre-upload somewhere and pass url/uri, or
|
|
19
|
+
- provide a public url and let AG download + store as artifact.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
id: str | None = None # Optional identifier for the file
|
|
23
|
+
name: str | None = None # Optional name of the file
|
|
24
|
+
mimetype: str | None = None # Optional MIME type of the file
|
|
25
|
+
size: int | None = None # Optional size of the file in bytes
|
|
26
|
+
url: str | None = None # URL where the file is located
|
|
27
|
+
uri: str | None = None # URI where the file is located
|
|
28
|
+
extra: dict[str, Any] = None # Any extra metadata
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class IncomingMessage:
|
|
33
|
+
"""
|
|
34
|
+
Transport-agnostic inbound message shape.
|
|
35
|
+
Used by HTTP/WS handlers and any custom code that wants to resume via channel.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
scheme: str # e.g. "ext", "mychat", "slack-http", etc.
|
|
39
|
+
channel_id: str # Channel identifier
|
|
40
|
+
thread_id: str | None = None # Optional thread/conversation identifier
|
|
41
|
+
|
|
42
|
+
# For ask_text / ask_file continuations
|
|
43
|
+
text: str | None = None # Text content of the message
|
|
44
|
+
files: Iterable[IncomingFile] | None = None # Attached files
|
|
45
|
+
|
|
46
|
+
# For approval
|
|
47
|
+
choice: str | None = None # User's choice/response
|
|
48
|
+
|
|
49
|
+
# Optional structured metadata
|
|
50
|
+
meta: dict[str, Any] | None = None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class ChannelIngress:
|
|
54
|
+
"""
|
|
55
|
+
Canonical entry point for inbound messages from external channels.
|
|
56
|
+
|
|
57
|
+
Typical flow:
|
|
58
|
+
UI -> HTTP/WS -> ChannelIngress.handle(...) -> cont_store + resume_router
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
def __init__(self, *, container, logger=None):
|
|
62
|
+
self.c = container
|
|
63
|
+
# Validate and assign dependencies
|
|
64
|
+
|
|
65
|
+
assert container is not None, "Either provide all dependencies or a container"
|
|
66
|
+
self.artifacts = container.artifacts if hasattr(container, "artifacts") else None
|
|
67
|
+
self.kv_hot = container.kv_hot if hasattr(container, "kv_hot") else None
|
|
68
|
+
self.cont_store = container.cont_store if hasattr(container, "cont_store") else None
|
|
69
|
+
self.resume_router = (
|
|
70
|
+
container.resume_router if hasattr(container, "resume_router") else None
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
if logger is not None:
|
|
74
|
+
self.logger = logger
|
|
75
|
+
else:
|
|
76
|
+
container_logger = getattr(container, "logger", None)
|
|
77
|
+
self.logger = container_logger.for_channel() if container_logger else None
|
|
78
|
+
|
|
79
|
+
def _channel_key(self, scheme: str, channel_id: str) -> str:
|
|
80
|
+
"""
|
|
81
|
+
Build a canonical channel key string from scheme + channel_id.
|
|
82
|
+
|
|
83
|
+
- For the generic "ext" channel, we use "ext:chan/<id>".
|
|
84
|
+
- For Slack/Telegram/etc. we can just use "<scheme>:<channel_id>" so we can
|
|
85
|
+
preserve their existing formats.
|
|
86
|
+
"""
|
|
87
|
+
if scheme == "ext":
|
|
88
|
+
return f"{scheme}:chan/{channel_id}"
|
|
89
|
+
# Slack: channel_id = "team/T:chan/C" => "slack:team/T:chan/C"
|
|
90
|
+
# Telegram: channel_id = "chat/<id>[:topic/<topic_id>]" => "tg:chat/..."
|
|
91
|
+
return f"{scheme}:{channel_id}"
|
|
92
|
+
|
|
93
|
+
def _log(self, level: str, msg: str, **kwargs):
|
|
94
|
+
if not self.logger:
|
|
95
|
+
print(f"[{level.upper()}] {msg} | {kwargs}")
|
|
96
|
+
return
|
|
97
|
+
log_fn = getattr(self.logger, level.lower(), self.logger.info)
|
|
98
|
+
log_fn(msg, extra=kwargs)
|
|
99
|
+
|
|
100
|
+
async def _download_url(self, url: str) -> bytes:
|
|
101
|
+
"""
|
|
102
|
+
Simple downloader for public URLs.
|
|
103
|
+
"""
|
|
104
|
+
async with aiohttp.ClientSession() as sess, sess.get(url) as r:
|
|
105
|
+
r.raise_for_status()
|
|
106
|
+
return await r.read()
|
|
107
|
+
|
|
108
|
+
async def _stage_file(
|
|
109
|
+
self,
|
|
110
|
+
*,
|
|
111
|
+
data: bytes,
|
|
112
|
+
file_id: str | None,
|
|
113
|
+
name: str,
|
|
114
|
+
ch_key: str,
|
|
115
|
+
cont: Continuation,
|
|
116
|
+
) -> str:
|
|
117
|
+
"""
|
|
118
|
+
Write bytes to tmp path, then save via ArtifactStore.save_file(...).
|
|
119
|
+
Returns the Artifact.uri (string).
|
|
120
|
+
"""
|
|
121
|
+
tmp = await self.artifacts.plan_staging_path(planned_ext=f"_{file_id or name}")
|
|
122
|
+
|
|
123
|
+
with open(tmp, "wb") as f:
|
|
124
|
+
f.write(data)
|
|
125
|
+
|
|
126
|
+
run_id = cont.run_id if cont else "ad-hoc"
|
|
127
|
+
node_id = cont.node_id if cont else "channel-ingress"
|
|
128
|
+
|
|
129
|
+
art = await self.artifacts.save_file(
|
|
130
|
+
path=tmp,
|
|
131
|
+
kind="upload",
|
|
132
|
+
run_id=run_id,
|
|
133
|
+
graph_id="channel",
|
|
134
|
+
node_id=node_id,
|
|
135
|
+
tool_name="channel.upload",
|
|
136
|
+
tool_version="0.0.1",
|
|
137
|
+
suggested_uri=None,
|
|
138
|
+
pin=False,
|
|
139
|
+
labels={
|
|
140
|
+
"source": "channel",
|
|
141
|
+
"channel": ch_key,
|
|
142
|
+
"name": name,
|
|
143
|
+
"inbound_file_id": file_id or "",
|
|
144
|
+
},
|
|
145
|
+
metrics=None,
|
|
146
|
+
preview_uri=None,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
saved_uri = getattr(art, "uri", None)
|
|
150
|
+
if not saved_uri:
|
|
151
|
+
self._log(
|
|
152
|
+
"error",
|
|
153
|
+
"Failed to save uploaded file as artifact",
|
|
154
|
+
channel=ch_key,
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
return saved_uri
|
|
158
|
+
|
|
159
|
+
async def _handle_files(
|
|
160
|
+
self,
|
|
161
|
+
msg: IncomingMessage,
|
|
162
|
+
*,
|
|
163
|
+
ch_key: str,
|
|
164
|
+
cont: Continuation,
|
|
165
|
+
) -> list[dict[str, Any]]:
|
|
166
|
+
"""
|
|
167
|
+
Normalize and optionally persist incoming files to artifact store.
|
|
168
|
+
|
|
169
|
+
Returns a list of file_refs that mirror the Slack file_refs shape:
|
|
170
|
+
{id, name, mimetype, size, uri, url, platform, channel_key, ...}
|
|
171
|
+
"""
|
|
172
|
+
if not msg.files:
|
|
173
|
+
return []
|
|
174
|
+
|
|
175
|
+
file_refs: list[dict[str, Any]] = []
|
|
176
|
+
for f in msg.files:
|
|
177
|
+
name = f.name or f.id or "unnamed"
|
|
178
|
+
file_id = f.id or name
|
|
179
|
+
mimetype = f.mimetype or "application/octet-stream"
|
|
180
|
+
size = f.size or 0
|
|
181
|
+
uri = f.uri
|
|
182
|
+
url = f.url
|
|
183
|
+
|
|
184
|
+
# Optional: auto-download if url is provided and no uri
|
|
185
|
+
# this is not executed when we stage files with channel-specific upload handlers that already provide uri
|
|
186
|
+
if (not uri) and url:
|
|
187
|
+
try:
|
|
188
|
+
data_bytes = await self._download_url(url)
|
|
189
|
+
uri = await self._stage_file(
|
|
190
|
+
data=data_bytes,
|
|
191
|
+
file_id=file_id,
|
|
192
|
+
name=name,
|
|
193
|
+
ch_key=ch_key,
|
|
194
|
+
cont=cont,
|
|
195
|
+
)
|
|
196
|
+
except Exception as e:
|
|
197
|
+
self._log("warning", f"Ingress: file download failed: {e}", channel_key=ch_key)
|
|
198
|
+
|
|
199
|
+
ref = {
|
|
200
|
+
"id": file_id,
|
|
201
|
+
"name": name,
|
|
202
|
+
"mimetype": mimetype,
|
|
203
|
+
"size": size,
|
|
204
|
+
"uri": uri,
|
|
205
|
+
"url": url,
|
|
206
|
+
"platform": msg.scheme,
|
|
207
|
+
"channel_key": ch_key,
|
|
208
|
+
}
|
|
209
|
+
if f.extra:
|
|
210
|
+
ref["extra"] = dict(f.extra)
|
|
211
|
+
|
|
212
|
+
file_refs.append(ref)
|
|
213
|
+
|
|
214
|
+
# Append to per-channel inbox, dedup by id
|
|
215
|
+
inbox_key = f"inbox://{ch_key}"
|
|
216
|
+
await self.kv_hot.list_append_unique(
|
|
217
|
+
inbox_key,
|
|
218
|
+
file_refs,
|
|
219
|
+
id_key="id",
|
|
220
|
+
)
|
|
221
|
+
return file_refs
|
|
222
|
+
|
|
223
|
+
async def _find_continuation(
|
|
224
|
+
self, *, scheme: str, ch_key: str, thread_id: str | None
|
|
225
|
+
) -> Continuation | None:
|
|
226
|
+
"""
|
|
227
|
+
Find pending continuation for this channel/thread.
|
|
228
|
+
"""
|
|
229
|
+
cont = None
|
|
230
|
+
if thread_id:
|
|
231
|
+
corr = Correlator(scheme=scheme, channel=ch_key, thread=thread_id, message="")
|
|
232
|
+
cont = await self.cont_store.find_by_correlator(corr=corr)
|
|
233
|
+
|
|
234
|
+
if not cont:
|
|
235
|
+
# Fallback: look for any continuation for this channel
|
|
236
|
+
corr2 = Correlator(scheme=scheme, channel=ch_key, thread="", message="")
|
|
237
|
+
cont = await self.cont_store.find_by_correlator(corr=corr2)
|
|
238
|
+
|
|
239
|
+
return cont
|
|
240
|
+
|
|
241
|
+
# ---- Public method ----
|
|
242
|
+
async def handle(self, msg: IncomingMessage) -> bool:
|
|
243
|
+
"""
|
|
244
|
+
Handle an inbound message and resume a waiting continuation if any.
|
|
245
|
+
|
|
246
|
+
Returns:
|
|
247
|
+
True -> a continuation was found and resumed
|
|
248
|
+
False -> nothing was listening on this channel (fire-and-forget)
|
|
249
|
+
"""
|
|
250
|
+
scheme = msg.scheme
|
|
251
|
+
ch_key = self._channel_key(scheme, msg.channel_id)
|
|
252
|
+
|
|
253
|
+
cont = await self._find_continuation(
|
|
254
|
+
scheme=scheme,
|
|
255
|
+
ch_key=ch_key,
|
|
256
|
+
thread_id=msg.thread_id,
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
# Normalize and persist any attached files
|
|
260
|
+
file_refs = []
|
|
261
|
+
if msg.files:
|
|
262
|
+
file_refs = await self._handle_files(
|
|
263
|
+
msg,
|
|
264
|
+
ch_key=ch_key,
|
|
265
|
+
cont=cont,
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
if not cont:
|
|
269
|
+
# No continuation found, log and return
|
|
270
|
+
self._log(
|
|
271
|
+
"info",
|
|
272
|
+
"Ingress: no continuation found for inbound message",
|
|
273
|
+
channel_key=ch_key,
|
|
274
|
+
)
|
|
275
|
+
return False
|
|
276
|
+
|
|
277
|
+
# Build payload for resumption
|
|
278
|
+
kind = cont.kind
|
|
279
|
+
meta = msg.meta or {}
|
|
280
|
+
|
|
281
|
+
if kind == "approval":
|
|
282
|
+
choice = (msg.choice or (msg.text or "")).strip() or "reject"
|
|
283
|
+
payload: dict[str, Any] = {
|
|
284
|
+
"choice": choice,
|
|
285
|
+
"channel_key": ch_key,
|
|
286
|
+
"thread_id": msg.thread_id,
|
|
287
|
+
"meta": meta,
|
|
288
|
+
}
|
|
289
|
+
elif kind in ("user_files", "user_input_or_files"):
|
|
290
|
+
payload = {
|
|
291
|
+
"text": msg.text or "",
|
|
292
|
+
"files": file_refs,
|
|
293
|
+
"channel_key": ch_key,
|
|
294
|
+
"thread_id": msg.thread_id,
|
|
295
|
+
"meta": meta,
|
|
296
|
+
}
|
|
297
|
+
else:
|
|
298
|
+
payload = {
|
|
299
|
+
"text": msg.text or "",
|
|
300
|
+
"channel_key": ch_key,
|
|
301
|
+
"thread_id": msg.thread_id,
|
|
302
|
+
"meta": meta,
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
await self.resume_router.resume(
|
|
306
|
+
run_id=cont.run_id,
|
|
307
|
+
node_id=cont.node_id,
|
|
308
|
+
token=cont.token,
|
|
309
|
+
payload=payload,
|
|
310
|
+
)
|
|
311
|
+
return True
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from aethergraph.contracts.services.channel import ChannelAdapter, OutEvent
|
|
6
|
+
from aethergraph.services.continuations.continuation import Correlator
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class QueueChannelAdapter(ChannelAdapter):
|
|
10
|
+
"""
|
|
11
|
+
Generic adapter that writes OutEvents into a per-channel outbox in kv_hot.
|
|
12
|
+
|
|
13
|
+
This is meant to be paired with:
|
|
14
|
+
- /ws/channel (to stream events to browser/clients)
|
|
15
|
+
- optional /channel/outbox polling endpoint
|
|
16
|
+
|
|
17
|
+
Capabilities: full superset to avoid downgrades in ChannelBus._smart_fallback.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
# Slack-level capability set; user code can still choose to ignore some fields.
|
|
21
|
+
capabilities: set[str] = {"text", "buttons", "image", "file", "edit", "stream"}
|
|
22
|
+
|
|
23
|
+
def __init__(self, container, *, scheme: str = "ext"):
|
|
24
|
+
self.c = container
|
|
25
|
+
self.scheme = scheme
|
|
26
|
+
|
|
27
|
+
async def send(self, event: OutEvent) -> dict | None:
|
|
28
|
+
"""
|
|
29
|
+
Serialize OutEvent to a JSON-friendly dict and append to the channel outbox.
|
|
30
|
+
|
|
31
|
+
Consumers (WS client, HTTP polling, etc.) can render this however they like.
|
|
32
|
+
"""
|
|
33
|
+
ch_key = event.channel # expected to already look like "ext:chan/<id>" or similar
|
|
34
|
+
outbox_key = f"outbox://{ch_key}"
|
|
35
|
+
|
|
36
|
+
# Minimal normalization; keep as much info as possible for UI.
|
|
37
|
+
payload: dict[str, Any] = {
|
|
38
|
+
"type": event.type,
|
|
39
|
+
"channel": event.channel,
|
|
40
|
+
"text": event.text,
|
|
41
|
+
"meta": event.meta,
|
|
42
|
+
"rich": event.rich,
|
|
43
|
+
"upsert_key": event.upsert_key,
|
|
44
|
+
"file": event.file,
|
|
45
|
+
"buttons": [],
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
# Buttons: flatten for clients (label/value/style/url)
|
|
49
|
+
if event.buttons:
|
|
50
|
+
btns = []
|
|
51
|
+
for b in event.buttons:
|
|
52
|
+
btns.append(
|
|
53
|
+
{
|
|
54
|
+
"label": getattr(b, "label", None),
|
|
55
|
+
"value": getattr(b, "value", None),
|
|
56
|
+
"style": getattr(b, "style", None),
|
|
57
|
+
"url": getattr(b, "url", None),
|
|
58
|
+
}
|
|
59
|
+
)
|
|
60
|
+
payload["buttons"] = btns
|
|
61
|
+
|
|
62
|
+
# simple timestamp if you have a clock service; otherwise omit
|
|
63
|
+
if hasattr(self.c, "clock"):
|
|
64
|
+
payload["ts"] = self.c.clock.now_ts()
|
|
65
|
+
|
|
66
|
+
await self.c.kv_hot.list_append(outbox_key, [payload])
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
"correlator": Correlator(
|
|
70
|
+
scheme=self.scheme,
|
|
71
|
+
channel=ch_key,
|
|
72
|
+
thread=(event.meta or {}).get("thread") or "",
|
|
73
|
+
message=None,
|
|
74
|
+
)
|
|
75
|
+
}
|