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
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from fastapi import HTTPException
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def decode_cursor(cursor: str | None) -> int:
|
|
5
|
+
"""
|
|
6
|
+
Turn an opaque cursor string into an integer offset.
|
|
7
|
+
|
|
8
|
+
For now, cursor is just the stringified offest. Later we will
|
|
9
|
+
switch to base64 JSON or keyset pagination without changing
|
|
10
|
+
the endpoints
|
|
11
|
+
"""
|
|
12
|
+
if not cursor:
|
|
13
|
+
return 0
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
return int(cursor)
|
|
17
|
+
except ValueError as e:
|
|
18
|
+
raise HTTPException(status_code=400, detail="Invalid cursor") from e
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def encode_cursor(offset: int) -> str:
|
|
22
|
+
"""
|
|
23
|
+
Turn an integer offset into an opaque cursor string.
|
|
24
|
+
|
|
25
|
+
For now, cursor is just the stringified offest. Later we will
|
|
26
|
+
switch to base64 JSON or keyset pagination without changing
|
|
27
|
+
the endpoints
|
|
28
|
+
"""
|
|
29
|
+
return str(offset)
|
|
@@ -0,0 +1,568 @@
|
|
|
1
|
+
# /runs
|
|
2
|
+
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from fastapi import APIRouter, Depends, HTTPException, Query, Request
|
|
7
|
+
|
|
8
|
+
from aethergraph.api.v1.pagination import decode_cursor, encode_cursor
|
|
9
|
+
from aethergraph.core.runtime.run_manager import RunManager
|
|
10
|
+
from aethergraph.core.runtime.run_types import RunImportance, RunOrigin, RunVisibility
|
|
11
|
+
from aethergraph.core.runtime.runtime_registry import current_registry
|
|
12
|
+
from aethergraph.core.runtime.runtime_services import current_services
|
|
13
|
+
|
|
14
|
+
from .deps import RequestIdentity, enforce_run_rate_limits, get_identity, require_runs_execute
|
|
15
|
+
from .schemas import (
|
|
16
|
+
NodeSnapshot,
|
|
17
|
+
RunChannelEvent,
|
|
18
|
+
RunCreateRequest,
|
|
19
|
+
RunCreateResponse,
|
|
20
|
+
RunListResponse,
|
|
21
|
+
RunSnapshot,
|
|
22
|
+
RunStatus,
|
|
23
|
+
RunSummary,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
router = APIRouter(tags=["runs"])
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@router.post(
|
|
30
|
+
"/graphs/{graph_id}/runs",
|
|
31
|
+
response_model=RunCreateResponse,
|
|
32
|
+
dependencies=[Depends(enforce_run_rate_limits)], # noqa: B008
|
|
33
|
+
)
|
|
34
|
+
async def create_run(
|
|
35
|
+
graph_id: str,
|
|
36
|
+
body: RunCreateRequest,
|
|
37
|
+
identity: RequestIdentity = Depends(require_runs_execute), # noqa: B008
|
|
38
|
+
) -> RunCreateResponse:
|
|
39
|
+
container = current_services()
|
|
40
|
+
rm: RunManager = getattr(container, "run_manager", None)
|
|
41
|
+
if rm is None:
|
|
42
|
+
raise HTTPException(status_code=503, detail="Run manager not configured")
|
|
43
|
+
|
|
44
|
+
app_vis = None
|
|
45
|
+
app_imp = None
|
|
46
|
+
reg = getattr(container, "registry", None) or current_registry()
|
|
47
|
+
if body.app_id and reg is not None:
|
|
48
|
+
app_meta = reg.get_meta(nspace="app", name=body.app_id)
|
|
49
|
+
if app_meta:
|
|
50
|
+
app_vis = app_meta.get("run_visibility")
|
|
51
|
+
app_imp = app_meta.get("run_importance")
|
|
52
|
+
app_vis = RunVisibility(app_vis) if app_vis else None
|
|
53
|
+
app_imp = RunImportance(app_imp) if app_imp else None
|
|
54
|
+
|
|
55
|
+
record = await rm.submit_run(
|
|
56
|
+
graph_id=graph_id,
|
|
57
|
+
inputs=body.inputs or {},
|
|
58
|
+
run_id=body.run_id,
|
|
59
|
+
tags=body.tags,
|
|
60
|
+
identity=identity,
|
|
61
|
+
origin=body.origin or RunOrigin.app,
|
|
62
|
+
visibility=body.visibility or app_vis or RunVisibility.normal,
|
|
63
|
+
importance=body.importance or app_imp or RunImportance.normal,
|
|
64
|
+
agent_id=body.agent_id or None,
|
|
65
|
+
app_id=body.app_id or None,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
return RunCreateResponse(
|
|
69
|
+
run_id=record.run_id,
|
|
70
|
+
graph_id=record.graph_id,
|
|
71
|
+
status=record.status, # typically "running"
|
|
72
|
+
outputs=None,
|
|
73
|
+
has_waits=False, # for now, we don't expose waits on submit
|
|
74
|
+
continuations=[],
|
|
75
|
+
started_at=record.started_at,
|
|
76
|
+
finished_at=record.finished_at,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _extract_app_id_from_tags(tags: list[str]) -> str | None:
|
|
81
|
+
# This is a convention: look for first tag that is not a client/flow tag
|
|
82
|
+
# and return it as app_id
|
|
83
|
+
# NOTE: this is not robust; in real usage, app_id should be stored in RunRecord.meta
|
|
84
|
+
# Only for demo purposes
|
|
85
|
+
for t in tags:
|
|
86
|
+
# skip client / flow tags
|
|
87
|
+
if t.startswith("client:") or t.startswith("flow:"):
|
|
88
|
+
continue
|
|
89
|
+
return t
|
|
90
|
+
return None
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@router.get("/runs", response_model=RunListResponse)
|
|
94
|
+
async def list_runs(
|
|
95
|
+
graph_id: str | None = Query(None), # noqa: B008
|
|
96
|
+
status: RunStatus | None = Query(None), # noqa: B008
|
|
97
|
+
flow_id: str | None = Query(None), # noqa: B008
|
|
98
|
+
cursor: str | None = Query(None), # noqa: B008
|
|
99
|
+
limit: int = Query(20, ge=1, le=100), # noqa: B008
|
|
100
|
+
identity: RequestIdentity = Depends(get_identity), # noqa: B008
|
|
101
|
+
) -> RunListResponse:
|
|
102
|
+
"""
|
|
103
|
+
List recent runs, optionally filterable by graph_id, status, flow_id.
|
|
104
|
+
|
|
105
|
+
Tenant scoping:
|
|
106
|
+
- cloud/demo: filtered by identity.user_id/org_id at the RunStore level.
|
|
107
|
+
- local: currently returns all runs.
|
|
108
|
+
"""
|
|
109
|
+
container = current_services()
|
|
110
|
+
rm = getattr(container, "run_manager", None)
|
|
111
|
+
if rm is None:
|
|
112
|
+
raise HTTPException(status_code=503, detail="Run manager not configured")
|
|
113
|
+
|
|
114
|
+
offset = decode_cursor(cursor)
|
|
115
|
+
|
|
116
|
+
# Enforce identity for cloud/demo (guest demo etc.)
|
|
117
|
+
if identity.mode in ("cloud", "demo") and identity.user_id is None:
|
|
118
|
+
raise HTTPException(status_code=403, detail="User identity required")
|
|
119
|
+
|
|
120
|
+
records = await rm.list_records(
|
|
121
|
+
graph_id=graph_id,
|
|
122
|
+
status=status,
|
|
123
|
+
flow_id=flow_id,
|
|
124
|
+
user_id=identity.user_id if identity.mode in ("cloud", "demo") else None,
|
|
125
|
+
org_id=identity.org_id if identity.mode in ("cloud", "demo") else None,
|
|
126
|
+
limit=limit,
|
|
127
|
+
offset=offset,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
# Still apply UI visibility policy in Python (this is cheap)
|
|
131
|
+
records = [
|
|
132
|
+
rec
|
|
133
|
+
for rec in records
|
|
134
|
+
if rec.visibility == RunVisibility.normal and rec.importance == RunImportance.normal
|
|
135
|
+
]
|
|
136
|
+
|
|
137
|
+
reg = getattr(container, "registry", None) or current_registry()
|
|
138
|
+
summaries: list[RunSummary] = []
|
|
139
|
+
|
|
140
|
+
for rec in records:
|
|
141
|
+
# Graph metadata logic as before
|
|
142
|
+
flow_meta_id: str | None = None
|
|
143
|
+
entrypoint = False
|
|
144
|
+
if reg is not None:
|
|
145
|
+
if rec.kind == "taskgraph":
|
|
146
|
+
meta = reg.get_meta(nspace="graph", name=rec.graph_id, version=None) or {}
|
|
147
|
+
elif rec.kind == "graphfn":
|
|
148
|
+
meta = reg.get_meta(nspace="graphfn", name=rec.graph_id, version=None) or {}
|
|
149
|
+
else:
|
|
150
|
+
meta = {}
|
|
151
|
+
flow_meta_id = meta.get("flow_id")
|
|
152
|
+
entrypoint = bool(meta.get("entrypoint", False))
|
|
153
|
+
|
|
154
|
+
effective_flow_id = rec.meta.get("flow_id") or flow_meta_id
|
|
155
|
+
|
|
156
|
+
app_id = rec.app_id
|
|
157
|
+
app_name = rec.meta.get("app_name")
|
|
158
|
+
|
|
159
|
+
summaries.append(
|
|
160
|
+
RunSummary(
|
|
161
|
+
run_id=rec.run_id,
|
|
162
|
+
graph_id=rec.graph_id,
|
|
163
|
+
status=rec.status,
|
|
164
|
+
started_at=rec.started_at,
|
|
165
|
+
finished_at=rec.finished_at,
|
|
166
|
+
tags=rec.tags,
|
|
167
|
+
user_id=rec.user_id,
|
|
168
|
+
org_id=rec.org_id,
|
|
169
|
+
session_id=rec.session_id or None,
|
|
170
|
+
graph_kind=rec.kind,
|
|
171
|
+
flow_id=effective_flow_id,
|
|
172
|
+
entrypoint=entrypoint,
|
|
173
|
+
meta=rec.meta or {},
|
|
174
|
+
app_id=app_id,
|
|
175
|
+
app_name=app_name,
|
|
176
|
+
agent_id=rec.meta.get("agent_id") or None,
|
|
177
|
+
origin=rec.origin,
|
|
178
|
+
visibility=rec.visibility,
|
|
179
|
+
importance=rec.importance,
|
|
180
|
+
artifact_count=rec.get("artifact_count"),
|
|
181
|
+
last_artifact_at=rec.get("last_artifact_at"),
|
|
182
|
+
)
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
next_cursor = encode_cursor(offset + limit) if len(records) == limit else None
|
|
186
|
+
return RunListResponse(runs=summaries, next_cursor=next_cursor)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
@router.get("/runs/{run_id}", response_model=RunSummary)
|
|
190
|
+
async def get_run(
|
|
191
|
+
run_id: str,
|
|
192
|
+
identity: RequestIdentity = Depends(get_identity), # noqa: B008
|
|
193
|
+
) -> RunSummary:
|
|
194
|
+
"""
|
|
195
|
+
Get high-level summary for a run from RunStore.
|
|
196
|
+
|
|
197
|
+
NOTE: `client_id` is a demo-only soft guard. If provided, we'll 404
|
|
198
|
+
runs that are not tagged with `client:<client_id>`.
|
|
199
|
+
"""
|
|
200
|
+
container = current_services()
|
|
201
|
+
rm = getattr(container, "run_manager", None)
|
|
202
|
+
if rm is None:
|
|
203
|
+
raise HTTPException(status_code=503, detail="Run manager not configured")
|
|
204
|
+
|
|
205
|
+
rec = await rm.get_record(run_id)
|
|
206
|
+
if rec is None:
|
|
207
|
+
raise HTTPException(status_code=404, detail="Run not found")
|
|
208
|
+
|
|
209
|
+
if identity.mode in ("cloud", "demo"):
|
|
210
|
+
user, _ = identity.user_id, identity.org_id
|
|
211
|
+
if user is not None:
|
|
212
|
+
if rec.user_id != user:
|
|
213
|
+
raise HTTPException(status_code=404, detail="Run not found")
|
|
214
|
+
else:
|
|
215
|
+
raise HTTPException(status_code=403, detail="User identity required")
|
|
216
|
+
|
|
217
|
+
reg = getattr(container, "registry", None) or current_registry()
|
|
218
|
+
flow_id: str | None = None
|
|
219
|
+
entrypoint = False
|
|
220
|
+
|
|
221
|
+
if reg is not None:
|
|
222
|
+
if rec.kind == "taskgraph":
|
|
223
|
+
meta = reg.get_meta(nspace="graph", name=rec.graph_id, version=None) or {}
|
|
224
|
+
elif rec.kind == "graphfn":
|
|
225
|
+
meta = reg.get_meta(nspace="graphfn", name=rec.graph_id, version=None) or {}
|
|
226
|
+
else:
|
|
227
|
+
meta = {}
|
|
228
|
+
|
|
229
|
+
flow_id = meta.get("flow_id")
|
|
230
|
+
entrypoint = bool(meta.get("entrypoint", False))
|
|
231
|
+
|
|
232
|
+
app_id = rec.app_id or rec.meta.get("app_id") or _extract_app_id_from_tags(rec.tags)
|
|
233
|
+
app_name = rec.meta.get("app_name")
|
|
234
|
+
agent_id = rec.agent_id or rec.meta.get("agent_id")
|
|
235
|
+
|
|
236
|
+
return RunSummary(
|
|
237
|
+
run_id=rec.run_id,
|
|
238
|
+
graph_id=rec.graph_id,
|
|
239
|
+
status=rec.status,
|
|
240
|
+
started_at=rec.started_at,
|
|
241
|
+
finished_at=rec.finished_at,
|
|
242
|
+
tags=rec.tags,
|
|
243
|
+
user_id=rec.user_id,
|
|
244
|
+
org_id=rec.org_id,
|
|
245
|
+
graph_kind=rec.kind,
|
|
246
|
+
flow_id=flow_id,
|
|
247
|
+
entrypoint=entrypoint,
|
|
248
|
+
meta=rec.meta or {},
|
|
249
|
+
app_id=app_id,
|
|
250
|
+
app_name=app_name,
|
|
251
|
+
agent_id=agent_id,
|
|
252
|
+
session_id=rec.session_id or None,
|
|
253
|
+
origin=rec.origin,
|
|
254
|
+
visibility=rec.visibility,
|
|
255
|
+
importance=rec.importance,
|
|
256
|
+
artifact_count=rec.get("artifact_count"),
|
|
257
|
+
last_artifact_at=rec.get("last_artifact_at"),
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
@router.post("/runs/{run_id}/cancel")
|
|
262
|
+
async def cancel_run(
|
|
263
|
+
run_id: str,
|
|
264
|
+
identity: RequestIdentity = Depends(get_identity), # noqa: B008
|
|
265
|
+
) -> dict:
|
|
266
|
+
"""
|
|
267
|
+
Request run cancellation.
|
|
268
|
+
|
|
269
|
+
TODO:
|
|
270
|
+
- Call runtime/cancellation mechanism.
|
|
271
|
+
"""
|
|
272
|
+
container = current_services()
|
|
273
|
+
rm = getattr(container, "run_manager", None)
|
|
274
|
+
if rm is None:
|
|
275
|
+
raise HTTPException(status_code=503, detail="Run manager not configured")
|
|
276
|
+
await rm.cancel_run(run_id)
|
|
277
|
+
return {"run_id": run_id, "status": "cancellation_requested"}
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _coerce_ts_to_dt(value: Any) -> datetime | None:
|
|
281
|
+
"""
|
|
282
|
+
Accepts:
|
|
283
|
+
- None
|
|
284
|
+
- datetime
|
|
285
|
+
- float / int epoch seconds
|
|
286
|
+
- ISO8601 string
|
|
287
|
+
Returns timezone-aware UTC datetime or None.
|
|
288
|
+
"""
|
|
289
|
+
if value is None:
|
|
290
|
+
return None
|
|
291
|
+
if isinstance(value, datetime):
|
|
292
|
+
# Ensure it's tz-aware; default to UTC if naive.
|
|
293
|
+
if value.tzinfo is None:
|
|
294
|
+
return value.replace(tzinfo=timezone.utc)
|
|
295
|
+
return value
|
|
296
|
+
|
|
297
|
+
# Epoch seconds (int/float)
|
|
298
|
+
if isinstance(value, int | float):
|
|
299
|
+
try:
|
|
300
|
+
return datetime.fromtimestamp(float(value), tz=timezone.utc)
|
|
301
|
+
except Exception:
|
|
302
|
+
return None
|
|
303
|
+
|
|
304
|
+
# ISO string
|
|
305
|
+
if isinstance(value, str):
|
|
306
|
+
try:
|
|
307
|
+
dt = datetime.fromisoformat(value)
|
|
308
|
+
if dt.tzinfo is None:
|
|
309
|
+
dt = dt.replace(tzinfo=timezone.utc)
|
|
310
|
+
return dt
|
|
311
|
+
except Exception:
|
|
312
|
+
return None
|
|
313
|
+
|
|
314
|
+
return None
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def _coerce_node_status(value: Any, fallback: RunStatus) -> RunStatus:
|
|
318
|
+
"""
|
|
319
|
+
Try to convert arbitrary value to RunStatus, else use fallback.
|
|
320
|
+
"""
|
|
321
|
+
if isinstance(value, RunStatus):
|
|
322
|
+
return value
|
|
323
|
+
if isinstance(value, str):
|
|
324
|
+
try:
|
|
325
|
+
if value == "DONE":
|
|
326
|
+
return RunStatus.succeeded
|
|
327
|
+
if value == "FAILED":
|
|
328
|
+
return RunStatus.failed
|
|
329
|
+
if value == "CANCELLED":
|
|
330
|
+
return RunStatus.canceled
|
|
331
|
+
if value == "PENDING":
|
|
332
|
+
return RunStatus.pending
|
|
333
|
+
return RunStatus(value)
|
|
334
|
+
except ValueError:
|
|
335
|
+
# maybe uppercased, etc.
|
|
336
|
+
try:
|
|
337
|
+
return RunStatus(value.lower())
|
|
338
|
+
except Exception:
|
|
339
|
+
pass
|
|
340
|
+
return fallback
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
@router.get("/runs/{run_id}/snapshot", response_model=RunSnapshot)
|
|
344
|
+
async def get_run_snapshot(
|
|
345
|
+
run_id: str,
|
|
346
|
+
identity: RequestIdentity = Depends(get_identity), # noqa: B008
|
|
347
|
+
) -> RunSnapshot:
|
|
348
|
+
"""
|
|
349
|
+
Run snapshot for a single graph within this run.
|
|
350
|
+
|
|
351
|
+
- Uses RunRecord for run-level status.
|
|
352
|
+
- Uses registry metadata for graph_kind, flow_id, entrypoint.
|
|
353
|
+
- Uses state_store (if present) for node-level state.
|
|
354
|
+
- Falls back to TaskGraphSpec or a single pseudo-node.
|
|
355
|
+
"""
|
|
356
|
+
container = current_services()
|
|
357
|
+
|
|
358
|
+
rm = getattr(container, "run_manager", None)
|
|
359
|
+
if rm is None:
|
|
360
|
+
raise HTTPException(status_code=503, detail="Run manager not configured")
|
|
361
|
+
|
|
362
|
+
state_store = getattr(container, "state_store", None)
|
|
363
|
+
|
|
364
|
+
rec = await rm.get_record(run_id)
|
|
365
|
+
if rec is None:
|
|
366
|
+
raise HTTPException(status_code=404, detail="Run not found")
|
|
367
|
+
|
|
368
|
+
graph_id = rec.graph_id
|
|
369
|
+
graph_kind = rec.kind
|
|
370
|
+
|
|
371
|
+
# --- Graph metadata from registry ---
|
|
372
|
+
reg = getattr(container, "registry", None) or current_registry()
|
|
373
|
+
|
|
374
|
+
flow_id: str | None = None
|
|
375
|
+
entrypoint = False
|
|
376
|
+
meta = {}
|
|
377
|
+
|
|
378
|
+
if reg is not None:
|
|
379
|
+
if graph_kind == "taskgraph":
|
|
380
|
+
meta = reg.get_meta(nspace="graph", name=graph_id, version=None) or {}
|
|
381
|
+
elif graph_kind == "graphfn":
|
|
382
|
+
meta = reg.get_meta(nspace="graphfn", name=graph_id, version=None) or {}
|
|
383
|
+
|
|
384
|
+
flow_id = meta.get("flow_id")
|
|
385
|
+
entrypoint = bool(meta.get("entrypoint", False))
|
|
386
|
+
|
|
387
|
+
# --- Load static TaskGraph spec if it exists ---
|
|
388
|
+
spec = None
|
|
389
|
+
if reg is not None:
|
|
390
|
+
try:
|
|
391
|
+
graph_obj = reg.get_graph(name=graph_id, version=None)
|
|
392
|
+
spec = getattr(graph_obj, "spec", None)
|
|
393
|
+
except KeyError:
|
|
394
|
+
spec = None
|
|
395
|
+
|
|
396
|
+
# --- Load latest GraphSnapshot (if we have a state store) ---
|
|
397
|
+
snap = None
|
|
398
|
+
if state_store is not None:
|
|
399
|
+
snap = await state_store.load_latest_snapshot(run_id)
|
|
400
|
+
|
|
401
|
+
nodes_state: dict[str, dict[str, Any]] = {}
|
|
402
|
+
snapshot_edges: list[dict[str, str]] = []
|
|
403
|
+
|
|
404
|
+
if snap is not None and isinstance(snap.state, dict):
|
|
405
|
+
raw_nodes = snap.state.get("nodes") or snap.state.get("node_state") or {}
|
|
406
|
+
if isinstance(raw_nodes, dict):
|
|
407
|
+
nodes_state = {str(k): (v or {}) for k, v in raw_nodes.items()}
|
|
408
|
+
|
|
409
|
+
raw_edges = snap.state.get("edges") or []
|
|
410
|
+
if isinstance(raw_edges, list):
|
|
411
|
+
snapshot_edges = [
|
|
412
|
+
{"source": e.get("from"), "target": e.get("to")}
|
|
413
|
+
for e in raw_edges
|
|
414
|
+
if isinstance(e, dict) and "from" in e and "to" in e
|
|
415
|
+
]
|
|
416
|
+
|
|
417
|
+
# --- Build edges ---
|
|
418
|
+
edges: list[dict[str, str]] = []
|
|
419
|
+
|
|
420
|
+
if snapshot_edges:
|
|
421
|
+
edges = snapshot_edges
|
|
422
|
+
elif spec is not None and getattr(spec, "nodes", None):
|
|
423
|
+
edge_set: set[tuple[str, str]] = set()
|
|
424
|
+
for node_id, node_spec in spec.nodes.items():
|
|
425
|
+
for dep_id in getattr(node_spec, "dependencies", []):
|
|
426
|
+
edge_set.add((str(dep_id), str(node_id)))
|
|
427
|
+
edges = [{"source": src, "target": dst} for (src, dst) in sorted(edge_set)]
|
|
428
|
+
|
|
429
|
+
nodes: list[NodeSnapshot] = []
|
|
430
|
+
|
|
431
|
+
# --- Case 1: TaskGraph with spec (static graph) ---
|
|
432
|
+
if spec is not None and getattr(spec, "nodes", None):
|
|
433
|
+
for node_id, node_spec in spec.nodes.items():
|
|
434
|
+
node_id_str = str(node_id)
|
|
435
|
+
st = nodes_state.get(node_id_str, {})
|
|
436
|
+
|
|
437
|
+
node_status = _coerce_node_status(st.get("status"), fallback=rec.status)
|
|
438
|
+
started_at = _coerce_ts_to_dt(st.get("started_at"))
|
|
439
|
+
finished_at = _coerce_ts_to_dt(st.get("finished_at"))
|
|
440
|
+
outputs = st.get("outputs")
|
|
441
|
+
error = st.get("error")
|
|
442
|
+
|
|
443
|
+
nodes.append(
|
|
444
|
+
NodeSnapshot(
|
|
445
|
+
node_id=node_id_str,
|
|
446
|
+
tool_name=getattr(node_spec, "tool_name", None),
|
|
447
|
+
status=node_status,
|
|
448
|
+
started_at=started_at,
|
|
449
|
+
finished_at=finished_at,
|
|
450
|
+
outputs=outputs,
|
|
451
|
+
error=error,
|
|
452
|
+
)
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
return RunSnapshot(
|
|
456
|
+
run_id=rec.run_id,
|
|
457
|
+
graph_id=graph_id,
|
|
458
|
+
nodes=nodes,
|
|
459
|
+
edges=edges,
|
|
460
|
+
graph_kind=graph_kind,
|
|
461
|
+
flow_id=flow_id,
|
|
462
|
+
entrypoint=entrypoint,
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
# --- Case 2: no spec, but snapshot has nodes (graphfn / dynamic) ---
|
|
466
|
+
if nodes_state:
|
|
467
|
+
for node_id, st in nodes_state.items():
|
|
468
|
+
node_status = _coerce_node_status(st.get("status"), fallback=rec.status)
|
|
469
|
+
started_at = _coerce_ts_to_dt(st.get("started_at"))
|
|
470
|
+
finished_at = _coerce_ts_to_dt(st.get("finished_at"))
|
|
471
|
+
outputs = st.get("outputs")
|
|
472
|
+
error = st.get("error")
|
|
473
|
+
|
|
474
|
+
nodes.append(
|
|
475
|
+
NodeSnapshot(
|
|
476
|
+
node_id=str(node_id),
|
|
477
|
+
tool_name=st.get("tool_name"),
|
|
478
|
+
status=node_status,
|
|
479
|
+
started_at=started_at,
|
|
480
|
+
finished_at=finished_at,
|
|
481
|
+
outputs=outputs,
|
|
482
|
+
error=error,
|
|
483
|
+
)
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
return RunSnapshot(
|
|
487
|
+
run_id=rec.run_id,
|
|
488
|
+
graph_id=graph_id,
|
|
489
|
+
nodes=nodes,
|
|
490
|
+
edges=edges,
|
|
491
|
+
graph_kind=graph_kind,
|
|
492
|
+
flow_id=flow_id,
|
|
493
|
+
entrypoint=entrypoint,
|
|
494
|
+
)
|
|
495
|
+
|
|
496
|
+
# --- Case 3: no spec, no snapshot → single pseudo-node, each node is the graph itself---
|
|
497
|
+
node = NodeSnapshot(
|
|
498
|
+
node_id=graph_id,
|
|
499
|
+
tool_name=None,
|
|
500
|
+
status=rec.status,
|
|
501
|
+
started_at=rec.started_at,
|
|
502
|
+
finished_at=rec.finished_at,
|
|
503
|
+
outputs=None,
|
|
504
|
+
error=rec.error,
|
|
505
|
+
)
|
|
506
|
+
return RunSnapshot(
|
|
507
|
+
run_id=rec.run_id,
|
|
508
|
+
graph_id=graph_id,
|
|
509
|
+
nodes=[node],
|
|
510
|
+
edges=[],
|
|
511
|
+
graph_kind=graph_kind,
|
|
512
|
+
flow_id=flow_id,
|
|
513
|
+
entrypoint=entrypoint,
|
|
514
|
+
)
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
@router.get("/runs/{run_id}/channel/events", response_model=list[RunChannelEvent])
|
|
518
|
+
async def get_run_channel_events(
|
|
519
|
+
run_id: str,
|
|
520
|
+
request: Request,
|
|
521
|
+
since_ts: float | None = None,
|
|
522
|
+
identity: RequestIdentity = Depends(get_identity), # noqa: B008
|
|
523
|
+
):
|
|
524
|
+
"""
|
|
525
|
+
Fetch normalized UI channel events for a run.
|
|
526
|
+
|
|
527
|
+
- Optionally enforces a demo-only `client_id` filter by checking the run's tags.
|
|
528
|
+
- Frontend can poll with `since_ts` for incremental updates.
|
|
529
|
+
"""
|
|
530
|
+
container = request.app.state.container
|
|
531
|
+
event_log = getattr(container, "eventlog", None)
|
|
532
|
+
rm = getattr(container, "run_manager", None)
|
|
533
|
+
|
|
534
|
+
if event_log is None or rm is None:
|
|
535
|
+
raise HTTPException(status_code=503, detail="Event log or run manager not configured")
|
|
536
|
+
|
|
537
|
+
# --- Build the time filter ---
|
|
538
|
+
since_dt: datetime | None = None
|
|
539
|
+
if since_ts is not None:
|
|
540
|
+
since_dt = datetime.fromtimestamp(since_ts, tz=timezone.utc)
|
|
541
|
+
|
|
542
|
+
# Query only this run's channel events
|
|
543
|
+
events = await event_log.query(
|
|
544
|
+
scope_id=run_id,
|
|
545
|
+
since=since_dt,
|
|
546
|
+
kinds=["run_channel"],
|
|
547
|
+
limit=200,
|
|
548
|
+
)
|
|
549
|
+
|
|
550
|
+
out: list[RunChannelEvent] = []
|
|
551
|
+
for e in events:
|
|
552
|
+
payload = e.get("payload", {})
|
|
553
|
+
|
|
554
|
+
ev = RunChannelEvent(
|
|
555
|
+
id=e.get("id"),
|
|
556
|
+
run_id=e.get("scope_id") or run_id,
|
|
557
|
+
type=payload.get("type") or "agent.message",
|
|
558
|
+
text=payload.get("text"),
|
|
559
|
+
buttons=payload.get("buttons") or [],
|
|
560
|
+
file=payload.get("file"),
|
|
561
|
+
meta=payload.get("meta") or {},
|
|
562
|
+
ts=e.get("ts"),
|
|
563
|
+
)
|
|
564
|
+
out.append(ev)
|
|
565
|
+
|
|
566
|
+
# Sort ascending by ts for stable UI
|
|
567
|
+
out.sort(key=lambda ev: ev.ts)
|
|
568
|
+
return out
|