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
aethergraph/runtime/__init__.py
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
# redirect runtime service imports for clean imports
|
|
2
2
|
|
|
3
3
|
from aethergraph.core.runtime.ad_hoc_context import open_session
|
|
4
|
+
from aethergraph.core.runtime.run_manager import RunManager
|
|
5
|
+
from aethergraph.core.runtime.run_types import (
|
|
6
|
+
RunImportance,
|
|
7
|
+
RunOrigin,
|
|
8
|
+
RunRecord,
|
|
9
|
+
RunStatus,
|
|
10
|
+
RunVisibility,
|
|
11
|
+
)
|
|
4
12
|
from aethergraph.core.runtime.runtime_services import (
|
|
5
13
|
# logger service helpers
|
|
6
14
|
current_logger_factory,
|
|
@@ -59,4 +67,11 @@ __all__ = [
|
|
|
59
67
|
"list_mcp_clients",
|
|
60
68
|
# ad-hoc context
|
|
61
69
|
"open_session",
|
|
70
|
+
# run manager and types
|
|
71
|
+
"RunManager",
|
|
72
|
+
"RunRecord",
|
|
73
|
+
"RunStatus",
|
|
74
|
+
"RunOrigin",
|
|
75
|
+
"RunImportance",
|
|
76
|
+
"RunVisibility",
|
|
62
77
|
]
|
|
@@ -1,16 +1,40 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
+
from contextlib import asynccontextmanager, suppress
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
2
6
|
from typing import Optional
|
|
3
7
|
|
|
4
8
|
from fastapi import FastAPI
|
|
5
9
|
from fastapi.middleware.cors import CORSMiddleware
|
|
6
|
-
|
|
10
|
+
from fastapi.responses import FileResponse, PlainTextResponse
|
|
11
|
+
from fastapi.staticfiles import StaticFiles
|
|
12
|
+
|
|
13
|
+
from aethergraph.api.v1.agents import router as agents_router
|
|
14
|
+
from aethergraph.api.v1.apps import router as apps_router
|
|
15
|
+
from aethergraph.api.v1.artifacts import router as artifacts_router
|
|
16
|
+
from aethergraph.api.v1.graphs import router as graphs_router
|
|
17
|
+
from aethergraph.api.v1.identity import router as identity_router
|
|
18
|
+
from aethergraph.api.v1.memory import router as memory_router
|
|
19
|
+
from aethergraph.api.v1.misc import router as misc_router
|
|
20
|
+
from aethergraph.api.v1.runs import router as runs_router
|
|
21
|
+
from aethergraph.api.v1.session import router as session_router
|
|
22
|
+
from aethergraph.api.v1.stats import router as stats_router
|
|
23
|
+
from aethergraph.api.v1.viz import router as vis_router
|
|
24
|
+
|
|
25
|
+
# include apis
|
|
7
26
|
from aethergraph.config.config import AppSettings
|
|
8
|
-
from aethergraph.
|
|
27
|
+
from aethergraph.core.runtime.runtime_services import install_services
|
|
9
28
|
|
|
10
|
-
|
|
29
|
+
# import built-in agents and plugins to register them
|
|
30
|
+
from aethergraph.plugins.agents.default_chat_agent import * # noqa: F403
|
|
11
31
|
|
|
12
32
|
# channel routes
|
|
13
|
-
from
|
|
33
|
+
from aethergraph.server.loading import GraphLoader, LoadSpec
|
|
34
|
+
from aethergraph.services.container.default_container import build_default_container
|
|
35
|
+
from aethergraph.utils.optdeps import require
|
|
36
|
+
|
|
37
|
+
logger = logging.getLogger(__name__)
|
|
14
38
|
|
|
15
39
|
|
|
16
40
|
def create_app(
|
|
@@ -23,40 +47,23 @@ def create_app(
|
|
|
23
47
|
Builds the FastAPI app, registers routers, and installs all services
|
|
24
48
|
into app.state.container (and globally via install_services()).
|
|
25
49
|
"""
|
|
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
50
|
|
|
36
|
-
# Resolve settings
|
|
51
|
+
# Resolve settings and container up front so lifespan can capture them
|
|
37
52
|
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
53
|
settings.logging.level = log_level
|
|
49
54
|
|
|
50
|
-
# ---- Services container ----
|
|
51
55
|
container = build_default_container(root=workspace, cfg=settings)
|
|
52
|
-
app.state.container = container
|
|
53
56
|
|
|
54
|
-
|
|
55
|
-
|
|
57
|
+
@asynccontextmanager
|
|
58
|
+
async def lifespan(app: FastAPI):
|
|
59
|
+
# --- Startup: attach settings/container and start external transports ---
|
|
60
|
+
app.state.settings = settings
|
|
61
|
+
app.state.container = container
|
|
62
|
+
|
|
63
|
+
slack_task = None
|
|
64
|
+
tg_task = None
|
|
56
65
|
|
|
57
|
-
|
|
58
|
-
@app.on_event("startup")
|
|
59
|
-
async def start_external_transports():
|
|
66
|
+
# Slack Socket Mode
|
|
60
67
|
slack_cfg = settings.slack
|
|
61
68
|
if (
|
|
62
69
|
slack_cfg
|
|
@@ -70,15 +77,164 @@ def create_app(
|
|
|
70
77
|
|
|
71
78
|
runner = SlackSocketModeRunner(container=container, settings=settings)
|
|
72
79
|
app.state.slack_socket_runner = runner
|
|
73
|
-
asyncio.create_task(runner.start())
|
|
80
|
+
slack_task = asyncio.create_task(runner.start())
|
|
74
81
|
|
|
75
|
-
# Telegram polling
|
|
82
|
+
# Telegram polling
|
|
76
83
|
tg_cfg = settings.telegram
|
|
77
84
|
if tg_cfg and tg_cfg.enabled and tg_cfg.polling_enabled and tg_cfg.bot_token:
|
|
78
85
|
from ..plugins.channel.websockets.telegram_polling import TelegramPollingRunner
|
|
79
86
|
|
|
80
87
|
tg_runner = TelegramPollingRunner(container=container, settings=settings)
|
|
81
88
|
app.state.telegram_polling_runner = tg_runner
|
|
82
|
-
asyncio.create_task(tg_runner.start())
|
|
89
|
+
tg_task = asyncio.create_task(tg_runner.start())
|
|
90
|
+
|
|
91
|
+
try:
|
|
92
|
+
# Hand control back to FastAPI / TestClient
|
|
93
|
+
yield
|
|
94
|
+
finally:
|
|
95
|
+
# --- Shutdown: best-effort cleanup of background tasks ---
|
|
96
|
+
for task in (slack_task, tg_task):
|
|
97
|
+
if task is not None and not task.done():
|
|
98
|
+
task.cancel()
|
|
99
|
+
# swallow cancellation errors
|
|
100
|
+
with suppress(asyncio.CancelledError):
|
|
101
|
+
await task
|
|
102
|
+
|
|
103
|
+
# Create app with lifespan
|
|
104
|
+
app = FastAPI(
|
|
105
|
+
title="AetherGraph Sidecar",
|
|
106
|
+
version="0.1",
|
|
107
|
+
lifespan=lifespan,
|
|
108
|
+
)
|
|
83
109
|
|
|
110
|
+
frontend_dir = Path(__file__).parent / "ui_static"
|
|
111
|
+
if frontend_dir.exists():
|
|
112
|
+
logger.info(f"Serving built frontend UI from {frontend_dir}")
|
|
113
|
+
logger.info("UI will be available at: http://<host>:<port>/ui")
|
|
114
|
+
|
|
115
|
+
# 1) Serve built assets under /ui/assets
|
|
116
|
+
assets_dir = frontend_dir / "assets"
|
|
117
|
+
if assets_dir.exists():
|
|
118
|
+
app.mount(
|
|
119
|
+
"/ui/assets",
|
|
120
|
+
StaticFiles(directory=str(assets_dir)),
|
|
121
|
+
name="ui_assets",
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
index_path = frontend_dir / "index.html"
|
|
125
|
+
|
|
126
|
+
# 2) SPA catch-all: /ui and ANY /ui/... path -> index.html
|
|
127
|
+
@app.get("/ui", include_in_schema=False)
|
|
128
|
+
@app.get("/ui/{full_path:path}", include_in_schema=False)
|
|
129
|
+
async def serve_ui(full_path: str = ""):
|
|
130
|
+
if index_path.exists():
|
|
131
|
+
return FileResponse(index_path)
|
|
132
|
+
return PlainTextResponse(
|
|
133
|
+
"UI bundle not found. Please build the frontend and copy it to ui_static.",
|
|
134
|
+
status_code=501,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
else:
|
|
138
|
+
logger.warning(
|
|
139
|
+
"AetherGraph UI bundle NOT found at %s. "
|
|
140
|
+
"The /ui endpoint will return a 501 until you build and copy it.",
|
|
141
|
+
frontend_dir,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
@app.get("/ui", include_in_schema=False)
|
|
145
|
+
async def ui_not_built():
|
|
146
|
+
return PlainTextResponse(
|
|
147
|
+
"UI bundle not found. Please build the frontend and copy it to ui_static.",
|
|
148
|
+
status_code=501,
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
# CORS
|
|
152
|
+
app.add_middleware(
|
|
153
|
+
CORSMiddleware,
|
|
154
|
+
allow_origins=["http://localhost:5173"], # dev UI origin
|
|
155
|
+
allow_credentials=True,
|
|
156
|
+
allow_methods=["*"],
|
|
157
|
+
allow_headers=["*"],
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
# Routers
|
|
161
|
+
app.include_router(router=runs_router, prefix="/api/v1")
|
|
162
|
+
app.include_router(router=graphs_router, prefix="/api/v1")
|
|
163
|
+
app.include_router(router=artifacts_router, prefix="/api/v1")
|
|
164
|
+
app.include_router(router=memory_router, prefix="/api/v1")
|
|
165
|
+
app.include_router(router=stats_router, prefix="/api/v1")
|
|
166
|
+
app.include_router(router=identity_router, prefix="/api/v1")
|
|
167
|
+
app.include_router(router=misc_router, prefix="/api/v1")
|
|
168
|
+
app.include_router(router=vis_router, prefix="/api/v1")
|
|
169
|
+
app.include_router(router=session_router, prefix="/api/v1")
|
|
170
|
+
app.include_router(router=apps_router, prefix="/api/v1")
|
|
171
|
+
app.include_router(router=agents_router, prefix="/api/v1")
|
|
172
|
+
|
|
173
|
+
# Webui router
|
|
174
|
+
from aethergraph.plugins.channel.routes.webui_routes import router as webui_router
|
|
175
|
+
|
|
176
|
+
app.include_router(router=webui_router, prefix="/api/v1")
|
|
177
|
+
|
|
178
|
+
# Install services globally so run()/tools see the same container
|
|
179
|
+
install_services(container)
|
|
180
|
+
|
|
181
|
+
# Optional: keep these for immediate access before lifespan runs
|
|
182
|
+
app.state.settings = settings
|
|
183
|
+
app.state.container = container
|
|
184
|
+
|
|
185
|
+
return app
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _load_user_graphs_from_env() -> None:
|
|
189
|
+
"""
|
|
190
|
+
Called inside each uvicorn worker to import user graphs based
|
|
191
|
+
on environment variables set by the CLI.
|
|
192
|
+
"""
|
|
193
|
+
modules_str = os.environ.get("AETHERGRAPH_LOAD_MODULES", "")
|
|
194
|
+
paths_str = os.environ.get("AETHERGRAPH_LOAD_PATHS", "")
|
|
195
|
+
project_root_str = os.environ.get("AETHERGRAPH_PROJECT_ROOT", ".")
|
|
196
|
+
strict_str = os.environ.get("AETHERGRAPH_STRICT_LOAD", "0")
|
|
197
|
+
|
|
198
|
+
modules = [m for m in modules_str.split(",") if m]
|
|
199
|
+
paths = [Path(p) for p in paths_str.split(os.pathsep) if p]
|
|
200
|
+
|
|
201
|
+
project_root = Path(project_root_str).resolve()
|
|
202
|
+
strict = strict_str.lower() in ("1", "true", "yes")
|
|
203
|
+
|
|
204
|
+
spec = LoadSpec(
|
|
205
|
+
modules=modules,
|
|
206
|
+
paths=paths,
|
|
207
|
+
project_root=project_root,
|
|
208
|
+
strict=strict,
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
loader = GraphLoader()
|
|
212
|
+
report = loader.load(spec)
|
|
213
|
+
|
|
214
|
+
# Optional: log report.loaded / report.errors here if you like
|
|
215
|
+
print("🚀 [worker] Loaded user graphs:", report.loaded)
|
|
216
|
+
if report.errors:
|
|
217
|
+
for e in report.errors:
|
|
218
|
+
print(f"⚠️ [worker load error] {e.source}: {e.error}")
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def create_app_from_env() -> FastAPI:
|
|
222
|
+
"""
|
|
223
|
+
Factory for uvicorn --reload / workers mode.
|
|
224
|
+
Reads workspace + graph load config from env, imports user graphs,
|
|
225
|
+
then builds the FastAPI app.
|
|
226
|
+
"""
|
|
227
|
+
workspace = os.environ.get("AETHERGRAPH_WORKSPACE", "./aethergraph_data")
|
|
228
|
+
log_level = os.environ.get("AETHERGRAPH_LOG_LEVEL", "warning")
|
|
229
|
+
|
|
230
|
+
# 1) Load user graphs in *this* process
|
|
231
|
+
_load_user_graphs_from_env()
|
|
232
|
+
|
|
233
|
+
# 2) Build the app (your existing factory)
|
|
234
|
+
# If you have a config system, wire it here
|
|
235
|
+
app = create_app(
|
|
236
|
+
workspace=workspace,
|
|
237
|
+
cfg=None, # or AppSettings.from_env(), etc.
|
|
238
|
+
log_level=log_level,
|
|
239
|
+
)
|
|
84
240
|
return app
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import AsyncIterator, Iterable
|
|
4
|
+
import json
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
import websockets
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ChannelClient:
|
|
12
|
+
"""
|
|
13
|
+
Convenience client for talking to a running AetherGraph server from Python.
|
|
14
|
+
|
|
15
|
+
- send_* methods: external -> AG (inbound to AG via /channel/incoming)
|
|
16
|
+
- iter_events(): AG -> external (outbound from AG via /ws/channel)
|
|
17
|
+
|
|
18
|
+
This is intentionally thin; real apps can wrap it with their own abstractions.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
base_url: str,
|
|
24
|
+
*,
|
|
25
|
+
scheme: str = "ext",
|
|
26
|
+
channel_id: str = "default",
|
|
27
|
+
thread_id: str | None = None,
|
|
28
|
+
timeout: float = 100.0,
|
|
29
|
+
api_key: str | None = None, # currently unused
|
|
30
|
+
http_client: httpx.AsyncClient | None = None, # managed externally if provided
|
|
31
|
+
ws_path: str = "/ws/channel", # currently unused
|
|
32
|
+
):
|
|
33
|
+
self.base_url = base_url
|
|
34
|
+
self.scheme = scheme
|
|
35
|
+
self.channel_id = channel_id
|
|
36
|
+
self.thread_id = thread_id
|
|
37
|
+
self.timeout = timeout
|
|
38
|
+
self.api_key = api_key
|
|
39
|
+
self.ws_path = ws_path
|
|
40
|
+
|
|
41
|
+
self._external_client = http_client
|
|
42
|
+
self._client: httpx.AsyncClient | None = None
|
|
43
|
+
self._owns_client = http_client is None
|
|
44
|
+
|
|
45
|
+
# ------------- internal helpers -------------
|
|
46
|
+
@property
|
|
47
|
+
def client(self) -> httpx.AsyncClient:
|
|
48
|
+
if self._external_client is not None:
|
|
49
|
+
return self._external_client
|
|
50
|
+
if self._client is None:
|
|
51
|
+
headers = {}
|
|
52
|
+
if self.api_key:
|
|
53
|
+
headers["Authorization"] = f"Bearer {self.api_key}"
|
|
54
|
+
self._client = httpx.AsyncClient(
|
|
55
|
+
base_url=self.base_url,
|
|
56
|
+
headers=headers,
|
|
57
|
+
timeout=self.timeout,
|
|
58
|
+
)
|
|
59
|
+
return self._client
|
|
60
|
+
|
|
61
|
+
async def aclose(self):
|
|
62
|
+
if self._owns_client and self._client is not None:
|
|
63
|
+
await self._client.aclose()
|
|
64
|
+
self._client = None
|
|
65
|
+
|
|
66
|
+
def _default_thread_id(self, thread_id: str | None) -> str | None:
|
|
67
|
+
return thread_id if thread_id is not None else self.thread_id
|
|
68
|
+
|
|
69
|
+
async def _get_client(self) -> httpx.AsyncClient:
|
|
70
|
+
"""Get or create an httpx.AsyncClient."""
|
|
71
|
+
if self._external_client is not None:
|
|
72
|
+
return self._external_client
|
|
73
|
+
return httpx.AsyncClient(base_url=self.base_url, timeout=self.timeout)
|
|
74
|
+
|
|
75
|
+
# --------- Inbound to AG (HTTP) ---------
|
|
76
|
+
async def send_text(self, text: str, *, meta: dict[str, Any] | None = None) -> httpx.Response:
|
|
77
|
+
"""
|
|
78
|
+
Send a text message into AG via /channel/incoming.
|
|
79
|
+
"""
|
|
80
|
+
url = f"{self.base_url}/channel/incoming"
|
|
81
|
+
payload = {
|
|
82
|
+
"scheme": self.scheme,
|
|
83
|
+
"channel_id": self.channel_id,
|
|
84
|
+
"thread_id": self.thread_id,
|
|
85
|
+
"text": text,
|
|
86
|
+
"meta": meta or {},
|
|
87
|
+
}
|
|
88
|
+
r = await self.client.post(url, json=payload)
|
|
89
|
+
r.raise_for_status()
|
|
90
|
+
return r.json
|
|
91
|
+
|
|
92
|
+
async def send_choice(
|
|
93
|
+
self, choice: str, *, meta: dict[str, Any] | None = None
|
|
94
|
+
) -> httpx.Response:
|
|
95
|
+
"""
|
|
96
|
+
Send a choice/approval response into AG via /channel/incoming.
|
|
97
|
+
"""
|
|
98
|
+
url = f"{self.base_url}/channel/incoming"
|
|
99
|
+
payload = {
|
|
100
|
+
"scheme": self.scheme,
|
|
101
|
+
"channel_id": self.channel_id,
|
|
102
|
+
"thread_id": self.thread_id,
|
|
103
|
+
"choice": choice,
|
|
104
|
+
"meta": meta or {},
|
|
105
|
+
}
|
|
106
|
+
r = await self.client.post(url, json=payload)
|
|
107
|
+
r.raise_for_status()
|
|
108
|
+
return r.json()
|
|
109
|
+
|
|
110
|
+
async def send_text_and_files(
|
|
111
|
+
self,
|
|
112
|
+
text: str | None,
|
|
113
|
+
files: Iterable[dict[str, Any]],
|
|
114
|
+
*,
|
|
115
|
+
meta: dict[str, Any] | None = None,
|
|
116
|
+
):
|
|
117
|
+
"""
|
|
118
|
+
Send a text message with attached files into AG via /channel/incoming.
|
|
119
|
+
|
|
120
|
+
Each file is a dict with keys like:
|
|
121
|
+
- name (str): filename
|
|
122
|
+
- mimetype (str): MIME type
|
|
123
|
+
- size (int): size in bytes
|
|
124
|
+
- url (str): public URL to download the file
|
|
125
|
+
"""
|
|
126
|
+
url = f"{self.base_url}/channel/incoming"
|
|
127
|
+
payload = {
|
|
128
|
+
"scheme": self.scheme,
|
|
129
|
+
"channel_id": self.channel_id,
|
|
130
|
+
"thread_id": self.thread_id,
|
|
131
|
+
"text": text,
|
|
132
|
+
"files": list(files),
|
|
133
|
+
"meta": meta or {},
|
|
134
|
+
}
|
|
135
|
+
r = await self.client.post(url, json=payload)
|
|
136
|
+
r.raise_for_status()
|
|
137
|
+
return r.json()
|
|
138
|
+
|
|
139
|
+
async def resume(
|
|
140
|
+
self,
|
|
141
|
+
run_id: str,
|
|
142
|
+
node_id: str,
|
|
143
|
+
token: str,
|
|
144
|
+
resume_key: str | None = None,
|
|
145
|
+
payload: dict[str, Any] | None = None,
|
|
146
|
+
) -> httpx.Response:
|
|
147
|
+
"""
|
|
148
|
+
Low-level manual resume via /channel/resume.
|
|
149
|
+
"""
|
|
150
|
+
url = f"{self.base_url}/channel/resume"
|
|
151
|
+
body = {
|
|
152
|
+
"run_id": run_id,
|
|
153
|
+
"node_id": node_id,
|
|
154
|
+
"token": token,
|
|
155
|
+
"resume_key": resume_key,
|
|
156
|
+
"payload": payload or {},
|
|
157
|
+
}
|
|
158
|
+
r = await self.client.post(url, json=body)
|
|
159
|
+
r.raise_for_status()
|
|
160
|
+
return r.json()
|
|
161
|
+
|
|
162
|
+
# --------- Outbound from AG (WebSocket) ---------
|
|
163
|
+
async def iter_events(self) -> AsyncIterator[dict[str, Any]]:
|
|
164
|
+
"""
|
|
165
|
+
Receive outbound channel events over a WebSocket.
|
|
166
|
+
|
|
167
|
+
Expected server endpoint: /ws/channel
|
|
168
|
+
|
|
169
|
+
Query params:
|
|
170
|
+
- scheme
|
|
171
|
+
- channel_id
|
|
172
|
+
- thread_id (optional)
|
|
173
|
+
- api_key (optional)
|
|
174
|
+
"""
|
|
175
|
+
# Build ws URL from base_url (http/https -> ws/wss)
|
|
176
|
+
if self.base_url.startswith("https://"):
|
|
177
|
+
ws_base = "wss://" + self.base_url[len("https://") :]
|
|
178
|
+
elif self.base_url.startswith("http://"):
|
|
179
|
+
ws_base = "ws://" + self.base_url[len("http://") :]
|
|
180
|
+
else:
|
|
181
|
+
# assume ws already
|
|
182
|
+
ws_base = self.base_url
|
|
183
|
+
|
|
184
|
+
params = {
|
|
185
|
+
"scheme": self.scheme,
|
|
186
|
+
"channel_id": self.channel_id,
|
|
187
|
+
}
|
|
188
|
+
if self.thread_id:
|
|
189
|
+
params["thread_id"] = self.thread_id
|
|
190
|
+
if self.api_key:
|
|
191
|
+
params["api_key"] = self.api_key
|
|
192
|
+
|
|
193
|
+
query = "&".join(f"{k}={v}" for k, v in params.items())
|
|
194
|
+
url = f"{ws_base}{self.ws_path}?{query}"
|
|
195
|
+
|
|
196
|
+
async with websockets.connect(url) as ws:
|
|
197
|
+
async for msg in ws:
|
|
198
|
+
try:
|
|
199
|
+
data = json.loads(msg)
|
|
200
|
+
except Exception:
|
|
201
|
+
data = {"raw": msg}
|
|
202
|
+
yield data
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from fastapi import APIRouter, HTTPException, Request
|
|
6
|
+
from pydantic import BaseModel
|
|
7
|
+
from starlette.responses import JSONResponse
|
|
8
|
+
|
|
9
|
+
from aethergraph.services.channel.ingress import ChannelIngress, IncomingFile, IncomingMessage
|
|
10
|
+
|
|
11
|
+
router = APIRouter()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# --------- Pydantic models for HTTP request/response ---------
|
|
15
|
+
class HttpIncomingFile(BaseModel):
|
|
16
|
+
id: str | None = None
|
|
17
|
+
name: str | None = None
|
|
18
|
+
mimetype: str | None = None
|
|
19
|
+
size: int | None = None
|
|
20
|
+
url: str | None = None
|
|
21
|
+
uri: str | None = None
|
|
22
|
+
extra: dict[str, Any] | None = None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ChannelIncomingBody(BaseModel):
|
|
26
|
+
"""
|
|
27
|
+
High-level resume via channel (no run_id/node_id/token exposed).
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
scheme: str = "ext"
|
|
31
|
+
channel_id: str
|
|
32
|
+
thread_id: str | None = None
|
|
33
|
+
|
|
34
|
+
text: str | None = None
|
|
35
|
+
files: list[HttpIncomingFile] | None = None
|
|
36
|
+
choice: str | None = None
|
|
37
|
+
meta: dict[str, Any] | None = None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class ChannelManualResumeBody(BaseModel):
|
|
41
|
+
"""
|
|
42
|
+
Low-level resume for power users: explicit run/node/token.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
run_id: str
|
|
46
|
+
node_id: str
|
|
47
|
+
token: str
|
|
48
|
+
payload: dict[str, Any] | None = None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# --------- HTTP route handlers ---------
|
|
52
|
+
@router.post("/channel/incoming")
|
|
53
|
+
async def channel_incoming(body: ChannelIncomingBody, request: Request):
|
|
54
|
+
"""
|
|
55
|
+
Generic inbound message endpoint. Typical UI call looks like:
|
|
56
|
+
|
|
57
|
+
POST /channel/incoming
|
|
58
|
+
{
|
|
59
|
+
"scheme": "ext",
|
|
60
|
+
"channel_id": "user-123",
|
|
61
|
+
"text": "hello",
|
|
62
|
+
"meta": {"foo": "bar"}
|
|
63
|
+
}
|
|
64
|
+
"""
|
|
65
|
+
try:
|
|
66
|
+
container = request.app.state.container
|
|
67
|
+
ingress: ChannelIngress = container.channel_ingress # TODO: wire via default container
|
|
68
|
+
|
|
69
|
+
files = []
|
|
70
|
+
if body.files:
|
|
71
|
+
files = [
|
|
72
|
+
IncomingFile(
|
|
73
|
+
id=f.id,
|
|
74
|
+
name=f.name,
|
|
75
|
+
mimetype=f.mimetype,
|
|
76
|
+
size=f.size,
|
|
77
|
+
url=f.url,
|
|
78
|
+
uri=f.uri,
|
|
79
|
+
extra=f.extra or {},
|
|
80
|
+
)
|
|
81
|
+
for f in body.files
|
|
82
|
+
]
|
|
83
|
+
|
|
84
|
+
ok = await ingress.handle(
|
|
85
|
+
IncomingMessage(
|
|
86
|
+
scheme=body.scheme,
|
|
87
|
+
channel_id=body.channel_id,
|
|
88
|
+
thread_id=body.thread_id,
|
|
89
|
+
text=body.text,
|
|
90
|
+
files=files,
|
|
91
|
+
choice=body.choice,
|
|
92
|
+
meta=body.meta or {},
|
|
93
|
+
)
|
|
94
|
+
)
|
|
95
|
+
return JSONResponse({"ok": True, "resumed": ok})
|
|
96
|
+
except Exception as e:
|
|
97
|
+
raise HTTPException(status_code=500, detail=str(e)) from e
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@router.post("/channel/resume")
|
|
101
|
+
async def channel_resume(body: ChannelManualResumeBody, request: Request):
|
|
102
|
+
"""
|
|
103
|
+
Low-level resume for power users: explicit run/node/token.
|
|
104
|
+
"""
|
|
105
|
+
try:
|
|
106
|
+
container = request.app.state.container
|
|
107
|
+
|
|
108
|
+
await container.resume_router.resume(
|
|
109
|
+
run_id=body.run_id,
|
|
110
|
+
node_id=body.node_id,
|
|
111
|
+
token=body.token,
|
|
112
|
+
payload=body.payload or {},
|
|
113
|
+
)
|
|
114
|
+
return JSONResponse({"ok": True})
|
|
115
|
+
except Exception as e:
|
|
116
|
+
raise HTTPException(status_code=500, detail=str(e)) from e
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
|
|
5
|
+
from fastapi import APIRouter, WebSocket, WebSocketDisconnect
|
|
6
|
+
|
|
7
|
+
router = APIRouter()
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@router.websocket("/ws/channel")
|
|
11
|
+
async def ws_channel(ws: WebSocket):
|
|
12
|
+
"""
|
|
13
|
+
Generic outbound event stream.
|
|
14
|
+
|
|
15
|
+
Client must first send a JSON handshake:
|
|
16
|
+
{"scheme": "ext", "channel_id": "user-123"}
|
|
17
|
+
|
|
18
|
+
Then we stream any events that the queue-based ChannelAdapter
|
|
19
|
+
appends to `outbox://<scheme>:chan/<channel_id>`.
|
|
20
|
+
"""
|
|
21
|
+
await ws.accept()
|
|
22
|
+
|
|
23
|
+
hello = await ws.receive_json()
|
|
24
|
+
scheme = hello.get("scheme") or "ext"
|
|
25
|
+
channel_id = hello["channel_id"]
|
|
26
|
+
|
|
27
|
+
container = ws.app.state.container
|
|
28
|
+
c = container
|
|
29
|
+
|
|
30
|
+
ch_key = f"{scheme}:chan/{channel_id}"
|
|
31
|
+
outbox_key = f"outbox://{ch_key}"
|
|
32
|
+
|
|
33
|
+
last_idx = 0
|
|
34
|
+
try:
|
|
35
|
+
while True:
|
|
36
|
+
await asyncio.sleep(0.25)
|
|
37
|
+
events = await c.kv_hot.list_get(outbox_key) or []
|
|
38
|
+
if last_idx < len(events):
|
|
39
|
+
for ev in events[last_idx:]:
|
|
40
|
+
# ev is a dict produced by our queue-based adapter
|
|
41
|
+
await ws.send_json(ev)
|
|
42
|
+
last_idx = len(events)
|
|
43
|
+
except WebSocketDisconnect:
|
|
44
|
+
# just drop; nothing special needed
|
|
45
|
+
return
|