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,136 +1,401 @@
|
|
|
1
|
-
# src/aethergraph/server/webui.py
|
|
2
1
|
from __future__ import annotations
|
|
3
2
|
|
|
4
|
-
import
|
|
3
|
+
import dataclasses
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
|
+
import json
|
|
6
|
+
import shutil
|
|
5
7
|
from typing import Any
|
|
8
|
+
import uuid
|
|
9
|
+
from uuid import uuid4
|
|
6
10
|
|
|
7
|
-
from fastapi import APIRouter, File,
|
|
8
|
-
from fastapi.responses import FileResponse, JSONResponse
|
|
11
|
+
from fastapi import APIRouter, Depends, File, Form, HTTPException, Request, UploadFile
|
|
9
12
|
from pydantic import BaseModel
|
|
13
|
+
from starlette.responses import JSONResponse
|
|
10
14
|
|
|
11
|
-
from aethergraph.
|
|
15
|
+
from aethergraph.api.v1.deps import RequestIdentity, get_identity
|
|
16
|
+
from aethergraph.core.runtime.run_types import RunImportance, RunOrigin, RunVisibility
|
|
17
|
+
from aethergraph.core.runtime.runtime_services import current_services
|
|
18
|
+
from aethergraph.services.artifacts.facade import ArtifactFacade
|
|
19
|
+
from aethergraph.services.channel.ingress import ChannelIngress, IncomingFile, IncomingMessage
|
|
12
20
|
|
|
13
|
-
|
|
21
|
+
router = APIRouter()
|
|
14
22
|
|
|
15
|
-
# ------- runtime singletons (attached in create_app) -------
|
|
16
|
-
HUB_ATTR = "web_session_hub"
|
|
17
|
-
UPLOAD_DIR_ATTR = "web_upload_dir"
|
|
18
23
|
|
|
24
|
+
class RunChannelIncomingBody(BaseModel):
|
|
25
|
+
"""
|
|
26
|
+
Inbound message from AG web UI to a run's channel.
|
|
27
|
+
"""
|
|
19
28
|
|
|
20
|
-
|
|
21
|
-
|
|
29
|
+
text: str | None = None
|
|
30
|
+
files: list[dict[str, Any]] | None = None
|
|
31
|
+
choice: str | None = None
|
|
32
|
+
meta: dict[str, Any] | None = None
|
|
22
33
|
|
|
23
34
|
|
|
24
|
-
|
|
25
|
-
|
|
35
|
+
class SessionChatIncomingBody(BaseModel):
|
|
36
|
+
"""
|
|
37
|
+
Inbound message from AG web UI to a session's chat channel.
|
|
38
|
+
"""
|
|
26
39
|
|
|
40
|
+
text: str | None = None
|
|
41
|
+
files: list[dict[str, Any]] | None = None
|
|
42
|
+
choice: str | None = None
|
|
43
|
+
meta: dict[str, Any] | None = None
|
|
44
|
+
agent_id: str | None = None
|
|
45
|
+
context_refs: list[dict[str, Any]] | None = None
|
|
27
46
|
|
|
28
|
-
# ------- WebSocket endpoint -------
|
|
29
|
-
@webui_router.websocket("/ws/channel/{session_id}")
|
|
30
|
-
async def ws_channel(ws: WebSocket, session_id: str):
|
|
31
|
-
await ws.accept()
|
|
32
47
|
|
|
33
|
-
|
|
34
|
-
|
|
48
|
+
@router.post("/runs/{run_id}/channel/incoming")
|
|
49
|
+
async def run_channel_incoming(
|
|
50
|
+
run_id: str,
|
|
51
|
+
body: RunChannelIncomingBody,
|
|
52
|
+
request: Request,
|
|
53
|
+
) -> JSONResponse:
|
|
54
|
+
"""
|
|
55
|
+
Specialized ingress for AG Web UI.
|
|
35
56
|
|
|
36
|
-
|
|
37
|
-
|
|
57
|
+
UI calls:
|
|
58
|
+
POST /runs/<run_id>/channel/incoming
|
|
59
|
+
{ "text": "hello", "meta": {...} }
|
|
38
60
|
|
|
61
|
+
Backend maps this to ChannelIngress with:
|
|
62
|
+
scheme="ui", channel_id=f"run/{run_id}"
|
|
63
|
+
and logs a `user.message` event into EventLog so the UI can render it.
|
|
64
|
+
"""
|
|
39
65
|
try:
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
66
|
+
container = request.app.state.container # type: ignore
|
|
67
|
+
ingress: ChannelIngress = container.channel_ingress
|
|
68
|
+
event_log = container.eventlog
|
|
69
|
+
|
|
70
|
+
# 1) Normalize files into IncomingFile list (future use)
|
|
71
|
+
files = []
|
|
72
|
+
if body.files:
|
|
73
|
+
for f in body.files:
|
|
74
|
+
files.append(
|
|
75
|
+
IncomingFile(
|
|
76
|
+
id=f.get("id"),
|
|
77
|
+
name=f.get("name"),
|
|
78
|
+
mimetype=f.get("mimetype"),
|
|
79
|
+
size=f.get("size"),
|
|
80
|
+
url=f.get("url"),
|
|
81
|
+
uri=f.get("uri"),
|
|
82
|
+
extra=f.get("extra") or {},
|
|
83
|
+
)
|
|
52
84
|
)
|
|
53
|
-
# optionally handle ping or upload notifications (not required)
|
|
54
|
-
except WebSocketDisconnect:
|
|
55
|
-
pass
|
|
56
|
-
finally:
|
|
57
|
-
await hub.detach(session_id, send_json)
|
|
58
85
|
|
|
86
|
+
# 2) Log the inbound user message **first**
|
|
87
|
+
text = body.text or body.choice or ""
|
|
88
|
+
if text:
|
|
89
|
+
now_ts = datetime.now(timezone.utc).timestamp()
|
|
90
|
+
row = {
|
|
91
|
+
"id": str(uuid4()),
|
|
92
|
+
"ts": now_ts,
|
|
93
|
+
"scope_id": run_id,
|
|
94
|
+
"kind": "run_channel",
|
|
95
|
+
"payload": {
|
|
96
|
+
"type": "user.message",
|
|
97
|
+
"text": text,
|
|
98
|
+
"buttons": [],
|
|
99
|
+
"file": None,
|
|
100
|
+
"meta": {
|
|
101
|
+
**(body.meta or {}),
|
|
102
|
+
"direction": "inbound",
|
|
103
|
+
"role": "user",
|
|
104
|
+
# we don't yet know "resumed" here; can add later if needed
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
}
|
|
108
|
+
await event_log.append(row)
|
|
59
109
|
|
|
60
|
-
#
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
110
|
+
# 3) Now resume any waiting continuation via ChannelIngress
|
|
111
|
+
resumed = await ingress.handle(
|
|
112
|
+
IncomingMessage(
|
|
113
|
+
scheme="ui",
|
|
114
|
+
channel_id=f"run/{run_id}",
|
|
115
|
+
thread_id=None,
|
|
116
|
+
text=body.text,
|
|
117
|
+
files=files,
|
|
118
|
+
choice=body.choice,
|
|
119
|
+
meta=body.meta or {},
|
|
120
|
+
)
|
|
121
|
+
)
|
|
66
122
|
|
|
123
|
+
return JSONResponse({"ok": True, "resumed": resumed})
|
|
124
|
+
except Exception as e:
|
|
125
|
+
raise HTTPException(status_code=500, detail=str(e)) from e
|
|
67
126
|
|
|
68
|
-
@webui_router.post("/api/web/resume")
|
|
69
|
-
async def http_resume(request: Request, body: ResumeBody):
|
|
70
|
-
c = request.app.state.container
|
|
71
|
-
await c.resume_router.resume(body.run_id, body.node_id, body.token, body.payload)
|
|
72
|
-
return {"ok": True}
|
|
73
127
|
|
|
128
|
+
async def _save_upload_as_artifact_deprecated(
|
|
129
|
+
container, upload: UploadFile, session_id: str, identity: RequestIdentity
|
|
130
|
+
) -> str:
|
|
131
|
+
"""
|
|
132
|
+
Streams upload to disk, saves as artifact, returns URI.
|
|
133
|
+
"""
|
|
134
|
+
filename = upload.filename or "unknown"
|
|
135
|
+
ext = ""
|
|
136
|
+
if "." in filename:
|
|
137
|
+
ext = f".{filename.split('.')[-1]}"
|
|
74
138
|
|
|
75
|
-
#
|
|
76
|
-
|
|
77
|
-
|
|
139
|
+
# 1. Plan Staging
|
|
140
|
+
tmp_path = await container.artifacts.plan_staging_path(
|
|
141
|
+
planned_ext=f"_{uuid.uuid4().hex[:6]}{ext}"
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
# 2. Save Bytes
|
|
145
|
+
with open(tmp_path, "wb") as buffer:
|
|
146
|
+
shutil.copyfileobj(upload.file, buffer)
|
|
147
|
+
|
|
148
|
+
# 3. Register Artifact
|
|
149
|
+
artifact = await container.artifacts.save_file(
|
|
150
|
+
path=tmp_path,
|
|
151
|
+
kind="upload",
|
|
152
|
+
run_id=f"session:{session_id}",
|
|
153
|
+
graph_id="chat",
|
|
154
|
+
node_id="user_input",
|
|
155
|
+
tool_name="web.upload",
|
|
156
|
+
tool_version="1.0.0",
|
|
157
|
+
labels={
|
|
158
|
+
"source": "web_chat",
|
|
159
|
+
"original_name": filename,
|
|
160
|
+
"session_id": session_id,
|
|
161
|
+
"content_type": upload.content_type,
|
|
162
|
+
},
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
# Return URI
|
|
166
|
+
return getattr(artifact, "uri", None) or getattr(artifact, "path", None)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
async def _save_upload_as_artifact(
|
|
170
|
+
container: Any,
|
|
171
|
+
upload: UploadFile,
|
|
172
|
+
session_id: str,
|
|
173
|
+
identity: RequestIdentity,
|
|
174
|
+
) -> str:
|
|
78
175
|
"""
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
UI doesn't pass session; we just save under a common folder.
|
|
176
|
+
Streams upload to disk, saves as session-scoped artifact, returns URI.
|
|
177
|
+
Artifacts created here will appear under scope_id = session_id.
|
|
82
178
|
"""
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
179
|
+
filename = upload.filename or "unknown"
|
|
180
|
+
ext = ""
|
|
181
|
+
if "." in filename:
|
|
182
|
+
ext = f".{filename.split('.')[-1]}"
|
|
183
|
+
|
|
184
|
+
# 1. Stage to a temp path
|
|
185
|
+
tmp_path = await container.artifacts.plan_staging_path(
|
|
186
|
+
planned_ext=f"_{uuid.uuid4().hex[:6]}{ext}"
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
with open(tmp_path, "wb") as buffer:
|
|
190
|
+
shutil.copyfileobj(upload.file, buffer)
|
|
191
|
+
|
|
192
|
+
# 2. Build a Scope for this session upload
|
|
193
|
+
scope = None
|
|
194
|
+
if getattr(container, "scope_factory", None):
|
|
195
|
+
scope = container.scope_factory.for_node(
|
|
196
|
+
identity=identity,
|
|
197
|
+
run_id=None,
|
|
198
|
+
graph_id="chat",
|
|
199
|
+
node_id="user_upload",
|
|
200
|
+
session_id=session_id,
|
|
201
|
+
app_id=None,
|
|
202
|
+
tool_name="web.upload",
|
|
203
|
+
tool_version="1.0.0",
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
# 3. Use ArtifactFacade so index gets scope_id = session_id
|
|
207
|
+
artifact_facade = ArtifactFacade(
|
|
208
|
+
run_id=f"session:{session_id}",
|
|
209
|
+
graph_id="chat",
|
|
210
|
+
node_id="user_upload",
|
|
211
|
+
tool_name="web.upload",
|
|
212
|
+
tool_version="1.0.0",
|
|
213
|
+
store=container.artifacts,
|
|
214
|
+
index=container.artifact_index,
|
|
215
|
+
scope=scope,
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
artifact = await artifact_facade.save_file(
|
|
219
|
+
path=tmp_path,
|
|
220
|
+
kind="upload",
|
|
221
|
+
suggested_uri=f"./sessions/{session_id}/uploads/{filename}",
|
|
222
|
+
labels={
|
|
223
|
+
"source": "web_chat",
|
|
224
|
+
"original_name": filename,
|
|
225
|
+
"session_id": session_id,
|
|
226
|
+
"content_type": upload.content_type or "",
|
|
227
|
+
},
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
# Return URI (or local path fallback)
|
|
231
|
+
return getattr(artifact, "uri", None) or getattr(artifact, "path", None)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
@router.post("/sessions/{session_id}/chat/incoming")
|
|
235
|
+
async def session_chat_incoming(
|
|
236
|
+
session_id: str,
|
|
237
|
+
request: Request,
|
|
238
|
+
# Form fields
|
|
239
|
+
text: str = Form(""),
|
|
240
|
+
agent_id: str | None = Form(None), # noqa: B008
|
|
241
|
+
meta_json: str | None = Form(None), # noqa: B008
|
|
242
|
+
context_refs_json: str | None = Form(None), # 🔑 new
|
|
243
|
+
# Files
|
|
244
|
+
files: list[UploadFile] = File(default=[]), # noqa: B008
|
|
245
|
+
# Context
|
|
246
|
+
identity: RequestIdentity = Depends(get_identity), # noqa: B008
|
|
247
|
+
):
|
|
248
|
+
container = current_services()
|
|
249
|
+
ingress = container.channel_ingress
|
|
250
|
+
registry = container.registry
|
|
251
|
+
rm = container.run_manager
|
|
252
|
+
event_log = container.eventlog
|
|
253
|
+
|
|
254
|
+
# 1. Parse meta
|
|
255
|
+
meta: dict[str, Any] = {}
|
|
256
|
+
if meta_json:
|
|
257
|
+
try:
|
|
258
|
+
meta = json.loads(meta_json)
|
|
259
|
+
except json.JSONDecodeError as e:
|
|
260
|
+
raise HTTPException(400, "Invalid meta JSON") from e
|
|
261
|
+
|
|
262
|
+
# 2. Parse context_refs (JSON list)
|
|
263
|
+
context_refs: list[dict[str, Any]] = []
|
|
264
|
+
if context_refs_json:
|
|
265
|
+
try:
|
|
266
|
+
raw = json.loads(context_refs_json)
|
|
267
|
+
if isinstance(raw, list):
|
|
268
|
+
context_refs = raw
|
|
269
|
+
else:
|
|
270
|
+
raise HTTPException(400, "context_refs_json must be a JSON list")
|
|
271
|
+
except json.JSONDecodeError as e:
|
|
272
|
+
raise HTTPException(400, "Invalid context_refs JSON") from e
|
|
273
|
+
|
|
274
|
+
# 3. Process files -> IncomingFile (and save as artifacts)
|
|
275
|
+
incoming_files: list[IncomingFile] = []
|
|
276
|
+
for upload in files:
|
|
277
|
+
uri = await _save_upload_as_artifact(container, upload, session_id, identity)
|
|
278
|
+
incoming_files.append(
|
|
279
|
+
IncomingFile(
|
|
280
|
+
id=str(uuid.uuid4()),
|
|
281
|
+
name=upload.filename,
|
|
282
|
+
mimetype=upload.content_type,
|
|
283
|
+
size=getattr(upload, "size", None),
|
|
284
|
+
url=None,
|
|
285
|
+
uri=uri,
|
|
286
|
+
extra={
|
|
287
|
+
"source": "web_upload",
|
|
288
|
+
"session_id": session_id,
|
|
289
|
+
},
|
|
290
|
+
)
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
# 4. Log event (with files + context_refs in meta)
|
|
294
|
+
if text or incoming_files:
|
|
295
|
+
now_ts = datetime.now(timezone.utc).timestamp()
|
|
296
|
+
files_payload = [dataclasses.asdict(f) for f in incoming_files]
|
|
297
|
+
|
|
298
|
+
log_meta = {
|
|
299
|
+
**meta,
|
|
300
|
+
"direction": "inbound",
|
|
301
|
+
"role": "user",
|
|
302
|
+
}
|
|
303
|
+
if context_refs:
|
|
304
|
+
log_meta["context_refs"] = context_refs
|
|
305
|
+
|
|
306
|
+
await event_log.append(
|
|
95
307
|
{
|
|
96
|
-
"
|
|
97
|
-
"
|
|
98
|
-
"
|
|
308
|
+
"id": str(uuid.uuid4()),
|
|
309
|
+
"ts": now_ts,
|
|
310
|
+
"scope_id": session_id,
|
|
311
|
+
"kind": "session_chat",
|
|
312
|
+
"payload": {
|
|
313
|
+
"type": "user.message",
|
|
314
|
+
"text": text,
|
|
315
|
+
"files": files_payload,
|
|
316
|
+
"meta": log_meta,
|
|
317
|
+
},
|
|
99
318
|
}
|
|
100
319
|
)
|
|
101
|
-
return out
|
|
102
320
|
|
|
321
|
+
# 5. Let ChannelIngress handle / resume continuations
|
|
322
|
+
msg_meta = dict(meta)
|
|
323
|
+
if context_refs:
|
|
324
|
+
msg_meta["context_refs"] = context_refs
|
|
103
325
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
326
|
+
resumed = await ingress.handle(
|
|
327
|
+
IncomingMessage(
|
|
328
|
+
scheme="ui",
|
|
329
|
+
channel_id=f"session/{session_id}",
|
|
330
|
+
thread_id=None,
|
|
331
|
+
text=text,
|
|
332
|
+
files=incoming_files or None,
|
|
333
|
+
meta=msg_meta,
|
|
334
|
+
)
|
|
335
|
+
)
|
|
111
336
|
|
|
337
|
+
# 6. Spawn run if nothing was resumed
|
|
338
|
+
run_id: str | None = None
|
|
339
|
+
if not resumed:
|
|
340
|
+
if agent_id is None:
|
|
341
|
+
# for v1 it is fine to require frontend to specify agent_id
|
|
342
|
+
# later we can derive default agent per session
|
|
343
|
+
raise HTTPException(
|
|
344
|
+
status_code=400,
|
|
345
|
+
detail="agent_id is required when no continuation is resumed",
|
|
346
|
+
)
|
|
112
347
|
|
|
113
|
-
#
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
348
|
+
# Resolve agent meta -> backing graph
|
|
349
|
+
agent_meta = registry.get_meta(nspace="agent", name=agent_id)
|
|
350
|
+
if not agent_meta:
|
|
351
|
+
raise HTTPException(
|
|
352
|
+
status_code=404,
|
|
353
|
+
detail=f"Agent not found: {agent_id}",
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
run_vis_str = agent_meta.get("run_visibility", RunVisibility.inline.value) # default inline
|
|
357
|
+
run_imp_str = agent_meta.get(
|
|
358
|
+
"run_importance", RunImportance.ephemeral.value
|
|
359
|
+
) # default ephemeral
|
|
360
|
+
run_vis = RunVisibility(run_vis_str)
|
|
361
|
+
run_imp = RunImportance(run_imp_str)
|
|
362
|
+
|
|
363
|
+
backing = agent_meta.get("backing", {})
|
|
364
|
+
if backing.get("type") != "graphfn":
|
|
365
|
+
raise HTTPException(
|
|
366
|
+
status_code=400,
|
|
367
|
+
detail=f"Unsupported agent backing type: {backing.get('type')}. Only 'graphfn' is supported in v1.",
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
graph_id = backing["name"]
|
|
371
|
+
# build inputs for the agent graph -- in agent case, we pass message + files
|
|
372
|
+
inputs = {
|
|
373
|
+
"message": text,
|
|
374
|
+
"files": incoming_files,
|
|
375
|
+
"session_id": session_id, # for convenience, we can derive session inside graph too
|
|
376
|
+
"user_meta": meta or {}, # optional user meta
|
|
377
|
+
"context_refs": context_refs or [], # optional context references
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
record = await rm.submit_run(
|
|
381
|
+
graph_id=graph_id,
|
|
382
|
+
inputs=inputs,
|
|
383
|
+
session_id=session_id,
|
|
384
|
+
identity=identity,
|
|
385
|
+
origin=RunOrigin.chat,
|
|
386
|
+
visibility=run_vis,
|
|
387
|
+
importance=run_imp,
|
|
388
|
+
agent_id=agent_id,
|
|
389
|
+
app_id=agent_meta.get("app_id"), # optional, if you attach this
|
|
390
|
+
tags=["session:" + session_id, "agent:" + agent_id],
|
|
391
|
+
)
|
|
392
|
+
run_id = record.run_id
|
|
393
|
+
|
|
394
|
+
return JSONResponse(
|
|
395
|
+
{
|
|
396
|
+
"ok": True,
|
|
397
|
+
"resumed": resumed,
|
|
398
|
+
"run_id": run_id,
|
|
399
|
+
"files_processed": len(incoming_files),
|
|
400
|
+
}
|
|
401
|
+
)
|