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.
Files changed (267) hide show
  1. aethergraph/__init__.py +4 -10
  2. aethergraph/__main__.py +293 -0
  3. aethergraph/api/v1/__init__.py +0 -0
  4. aethergraph/api/v1/agents.py +46 -0
  5. aethergraph/api/v1/apps.py +70 -0
  6. aethergraph/api/v1/artifacts.py +415 -0
  7. aethergraph/api/v1/channels.py +89 -0
  8. aethergraph/api/v1/deps.py +168 -0
  9. aethergraph/api/v1/graphs.py +259 -0
  10. aethergraph/api/v1/identity.py +25 -0
  11. aethergraph/api/v1/memory.py +353 -0
  12. aethergraph/api/v1/misc.py +47 -0
  13. aethergraph/api/v1/pagination.py +29 -0
  14. aethergraph/api/v1/runs.py +568 -0
  15. aethergraph/api/v1/schemas.py +535 -0
  16. aethergraph/api/v1/session.py +323 -0
  17. aethergraph/api/v1/stats.py +201 -0
  18. aethergraph/api/v1/viz.py +152 -0
  19. aethergraph/config/config.py +22 -0
  20. aethergraph/config/loader.py +3 -2
  21. aethergraph/config/storage.py +209 -0
  22. aethergraph/contracts/__init__.py +0 -0
  23. aethergraph/contracts/services/__init__.py +0 -0
  24. aethergraph/contracts/services/artifacts.py +27 -14
  25. aethergraph/contracts/services/memory.py +45 -17
  26. aethergraph/contracts/services/metering.py +129 -0
  27. aethergraph/contracts/services/runs.py +50 -0
  28. aethergraph/contracts/services/sessions.py +87 -0
  29. aethergraph/contracts/services/state_stores.py +3 -0
  30. aethergraph/contracts/services/viz.py +44 -0
  31. aethergraph/contracts/storage/artifact_index.py +88 -0
  32. aethergraph/contracts/storage/artifact_store.py +99 -0
  33. aethergraph/contracts/storage/async_kv.py +34 -0
  34. aethergraph/contracts/storage/blob_store.py +50 -0
  35. aethergraph/contracts/storage/doc_store.py +35 -0
  36. aethergraph/contracts/storage/event_log.py +31 -0
  37. aethergraph/contracts/storage/vector_index.py +48 -0
  38. aethergraph/core/__init__.py +0 -0
  39. aethergraph/core/execution/forward_scheduler.py +13 -2
  40. aethergraph/core/execution/global_scheduler.py +21 -15
  41. aethergraph/core/execution/step_forward.py +10 -1
  42. aethergraph/core/graph/__init__.py +0 -0
  43. aethergraph/core/graph/graph_builder.py +8 -4
  44. aethergraph/core/graph/graph_fn.py +156 -15
  45. aethergraph/core/graph/graph_spec.py +8 -0
  46. aethergraph/core/graph/graphify.py +146 -27
  47. aethergraph/core/graph/node_spec.py +0 -2
  48. aethergraph/core/graph/node_state.py +3 -0
  49. aethergraph/core/graph/task_graph.py +39 -1
  50. aethergraph/core/runtime/__init__.py +0 -0
  51. aethergraph/core/runtime/ad_hoc_context.py +64 -4
  52. aethergraph/core/runtime/base_service.py +28 -4
  53. aethergraph/core/runtime/execution_context.py +13 -15
  54. aethergraph/core/runtime/graph_runner.py +222 -37
  55. aethergraph/core/runtime/node_context.py +510 -6
  56. aethergraph/core/runtime/node_services.py +12 -5
  57. aethergraph/core/runtime/recovery.py +15 -1
  58. aethergraph/core/runtime/run_manager.py +783 -0
  59. aethergraph/core/runtime/run_manager_local.py +204 -0
  60. aethergraph/core/runtime/run_registration.py +2 -2
  61. aethergraph/core/runtime/run_types.py +89 -0
  62. aethergraph/core/runtime/runtime_env.py +136 -7
  63. aethergraph/core/runtime/runtime_metering.py +71 -0
  64. aethergraph/core/runtime/runtime_registry.py +36 -13
  65. aethergraph/core/runtime/runtime_services.py +194 -6
  66. aethergraph/core/tools/builtins/toolset.py +1 -1
  67. aethergraph/core/tools/toolkit.py +5 -0
  68. aethergraph/plugins/agents/default_chat_agent copy.py +90 -0
  69. aethergraph/plugins/agents/default_chat_agent.py +171 -0
  70. aethergraph/plugins/agents/shared.py +81 -0
  71. aethergraph/plugins/channel/adapters/webui.py +112 -112
  72. aethergraph/plugins/channel/routes/webui_routes.py +367 -102
  73. aethergraph/plugins/channel/utils/slack_utils.py +115 -59
  74. aethergraph/plugins/channel/utils/telegram_utils.py +88 -47
  75. aethergraph/plugins/channel/websockets/weibui_ws.py +172 -0
  76. aethergraph/runtime/__init__.py +15 -0
  77. aethergraph/server/app_factory.py +190 -34
  78. aethergraph/server/clients/channel_client.py +202 -0
  79. aethergraph/server/http/channel_http_routes.py +116 -0
  80. aethergraph/server/http/channel_ws_routers.py +45 -0
  81. aethergraph/server/loading.py +117 -0
  82. aethergraph/server/server.py +131 -0
  83. aethergraph/server/server_state.py +240 -0
  84. aethergraph/server/start.py +227 -66
  85. aethergraph/server/ui_static/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  86. aethergraph/server/ui_static/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  87. aethergraph/server/ui_static/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  88. aethergraph/server/ui_static/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  89. aethergraph/server/ui_static/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  90. aethergraph/server/ui_static/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  91. aethergraph/server/ui_static/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  92. aethergraph/server/ui_static/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  93. aethergraph/server/ui_static/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  94. aethergraph/server/ui_static/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  95. aethergraph/server/ui_static/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  96. aethergraph/server/ui_static/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  97. aethergraph/server/ui_static/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  98. aethergraph/server/ui_static/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  99. aethergraph/server/ui_static/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  100. aethergraph/server/ui_static/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  101. aethergraph/server/ui_static/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  102. aethergraph/server/ui_static/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  103. aethergraph/server/ui_static/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  104. aethergraph/server/ui_static/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  105. aethergraph/server/ui_static/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  106. aethergraph/server/ui_static/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  107. aethergraph/server/ui_static/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  108. aethergraph/server/ui_static/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  109. aethergraph/server/ui_static/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  110. aethergraph/server/ui_static/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  111. aethergraph/server/ui_static/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  112. aethergraph/server/ui_static/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  113. aethergraph/server/ui_static/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  114. aethergraph/server/ui_static/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  115. aethergraph/server/ui_static/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  116. aethergraph/server/ui_static/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  117. aethergraph/server/ui_static/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  118. aethergraph/server/ui_static/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  119. aethergraph/server/ui_static/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  120. aethergraph/server/ui_static/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  121. aethergraph/server/ui_static/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  122. aethergraph/server/ui_static/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  123. aethergraph/server/ui_static/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  124. aethergraph/server/ui_static/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  125. aethergraph/server/ui_static/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  126. aethergraph/server/ui_static/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  127. aethergraph/server/ui_static/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  128. aethergraph/server/ui_static/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  129. aethergraph/server/ui_static/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  130. aethergraph/server/ui_static/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  131. aethergraph/server/ui_static/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  132. aethergraph/server/ui_static/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  133. aethergraph/server/ui_static/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  134. aethergraph/server/ui_static/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  135. aethergraph/server/ui_static/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  136. aethergraph/server/ui_static/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  137. aethergraph/server/ui_static/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  138. aethergraph/server/ui_static/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  139. aethergraph/server/ui_static/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  140. aethergraph/server/ui_static/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  141. aethergraph/server/ui_static/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  142. aethergraph/server/ui_static/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  143. aethergraph/server/ui_static/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  144. aethergraph/server/ui_static/assets/index-BR5GtXcZ.css +1 -0
  145. aethergraph/server/ui_static/assets/index-CQ0HZZ83.js +400 -0
  146. aethergraph/server/ui_static/index.html +15 -0
  147. aethergraph/server/ui_static/logo.png +0 -0
  148. aethergraph/services/artifacts/__init__.py +0 -0
  149. aethergraph/services/artifacts/facade.py +1239 -132
  150. aethergraph/services/auth/{dev.py → authn.py} +0 -8
  151. aethergraph/services/auth/authz.py +100 -0
  152. aethergraph/services/channel/__init__.py +0 -0
  153. aethergraph/services/channel/channel_bus.py +19 -1
  154. aethergraph/services/channel/factory.py +13 -1
  155. aethergraph/services/channel/ingress.py +311 -0
  156. aethergraph/services/channel/queue_adapter.py +75 -0
  157. aethergraph/services/channel/session.py +502 -19
  158. aethergraph/services/container/default_container.py +122 -43
  159. aethergraph/services/continuations/continuation.py +6 -0
  160. aethergraph/services/continuations/stores/fs_store.py +19 -0
  161. aethergraph/services/eventhub/event_hub.py +76 -0
  162. aethergraph/services/kv/__init__.py +0 -0
  163. aethergraph/services/kv/ephemeral.py +244 -0
  164. aethergraph/services/llm/__init__.py +0 -0
  165. aethergraph/services/llm/generic_client copy.py +691 -0
  166. aethergraph/services/llm/generic_client.py +1288 -187
  167. aethergraph/services/llm/providers.py +3 -1
  168. aethergraph/services/llm/types.py +47 -0
  169. aethergraph/services/llm/utils.py +284 -0
  170. aethergraph/services/logger/std.py +3 -0
  171. aethergraph/services/mcp/__init__.py +9 -0
  172. aethergraph/services/mcp/http_client.py +38 -0
  173. aethergraph/services/mcp/service.py +225 -1
  174. aethergraph/services/mcp/stdio_client.py +41 -6
  175. aethergraph/services/mcp/ws_client.py +44 -2
  176. aethergraph/services/memory/__init__.py +0 -0
  177. aethergraph/services/memory/distillers/llm_long_term.py +234 -0
  178. aethergraph/services/memory/distillers/llm_meta_summary.py +398 -0
  179. aethergraph/services/memory/distillers/long_term.py +225 -0
  180. aethergraph/services/memory/facade/__init__.py +3 -0
  181. aethergraph/services/memory/facade/chat.py +440 -0
  182. aethergraph/services/memory/facade/core.py +447 -0
  183. aethergraph/services/memory/facade/distillation.py +424 -0
  184. aethergraph/services/memory/facade/rag.py +410 -0
  185. aethergraph/services/memory/facade/results.py +315 -0
  186. aethergraph/services/memory/facade/retrieval.py +139 -0
  187. aethergraph/services/memory/facade/types.py +77 -0
  188. aethergraph/services/memory/facade/utils.py +43 -0
  189. aethergraph/services/memory/facade_dep.py +1539 -0
  190. aethergraph/services/memory/factory.py +9 -3
  191. aethergraph/services/memory/utils.py +10 -0
  192. aethergraph/services/metering/eventlog_metering.py +470 -0
  193. aethergraph/services/metering/noop.py +25 -4
  194. aethergraph/services/rag/__init__.py +0 -0
  195. aethergraph/services/rag/facade.py +279 -23
  196. aethergraph/services/rag/index_factory.py +2 -2
  197. aethergraph/services/rag/node_rag.py +317 -0
  198. aethergraph/services/rate_limit/inmem_rate_limit.py +24 -0
  199. aethergraph/services/registry/__init__.py +0 -0
  200. aethergraph/services/registry/agent_app_meta.py +419 -0
  201. aethergraph/services/registry/registry_key.py +1 -1
  202. aethergraph/services/registry/unified_registry.py +74 -6
  203. aethergraph/services/scope/scope.py +159 -0
  204. aethergraph/services/scope/scope_factory.py +164 -0
  205. aethergraph/services/state_stores/serialize.py +5 -0
  206. aethergraph/services/state_stores/utils.py +2 -1
  207. aethergraph/services/viz/__init__.py +0 -0
  208. aethergraph/services/viz/facade.py +413 -0
  209. aethergraph/services/viz/viz_service.py +69 -0
  210. aethergraph/storage/artifacts/artifact_index_jsonl.py +180 -0
  211. aethergraph/storage/artifacts/artifact_index_sqlite.py +426 -0
  212. aethergraph/storage/artifacts/cas_store.py +422 -0
  213. aethergraph/storage/artifacts/fs_cas.py +18 -0
  214. aethergraph/storage/artifacts/s3_cas.py +14 -0
  215. aethergraph/storage/artifacts/utils.py +124 -0
  216. aethergraph/storage/blob/fs_blob.py +86 -0
  217. aethergraph/storage/blob/s3_blob.py +115 -0
  218. aethergraph/storage/continuation_store/fs_cont.py +283 -0
  219. aethergraph/storage/continuation_store/inmem_cont.py +146 -0
  220. aethergraph/storage/continuation_store/kvdoc_cont.py +261 -0
  221. aethergraph/storage/docstore/fs_doc.py +63 -0
  222. aethergraph/storage/docstore/sqlite_doc.py +31 -0
  223. aethergraph/storage/docstore/sqlite_doc_sync.py +90 -0
  224. aethergraph/storage/eventlog/fs_event.py +136 -0
  225. aethergraph/storage/eventlog/sqlite_event.py +47 -0
  226. aethergraph/storage/eventlog/sqlite_event_sync.py +178 -0
  227. aethergraph/storage/factory.py +432 -0
  228. aethergraph/storage/fs_utils.py +28 -0
  229. aethergraph/storage/graph_state_store/state_store.py +64 -0
  230. aethergraph/storage/kv/inmem_kv.py +103 -0
  231. aethergraph/storage/kv/layered_kv.py +52 -0
  232. aethergraph/storage/kv/sqlite_kv.py +39 -0
  233. aethergraph/storage/kv/sqlite_kv_sync.py +98 -0
  234. aethergraph/storage/memory/event_persist.py +68 -0
  235. aethergraph/storage/memory/fs_persist.py +118 -0
  236. aethergraph/{services/memory/hotlog_kv.py → storage/memory/hotlog.py} +8 -2
  237. aethergraph/{services → storage}/memory/indices.py +31 -7
  238. aethergraph/storage/metering/meter_event.py +55 -0
  239. aethergraph/storage/runs/doc_store.py +280 -0
  240. aethergraph/storage/runs/inmen_store.py +82 -0
  241. aethergraph/storage/runs/sqlite_run_store.py +403 -0
  242. aethergraph/storage/sessions/doc_store.py +183 -0
  243. aethergraph/storage/sessions/inmem_store.py +110 -0
  244. aethergraph/storage/sessions/sqlite_session_store.py +399 -0
  245. aethergraph/storage/vector_index/chroma_index.py +138 -0
  246. aethergraph/storage/vector_index/faiss_index.py +179 -0
  247. aethergraph/storage/vector_index/sqlite_index.py +187 -0
  248. {aethergraph-0.1.0a1.dist-info → aethergraph-0.1.0a2.dist-info}/METADATA +138 -31
  249. aethergraph-0.1.0a2.dist-info/RECORD +356 -0
  250. aethergraph-0.1.0a2.dist-info/entry_points.txt +3 -0
  251. aethergraph/services/artifacts/factory.py +0 -35
  252. aethergraph/services/artifacts/fs_store.py +0 -656
  253. aethergraph/services/artifacts/jsonl_index.py +0 -123
  254. aethergraph/services/artifacts/sqlite_index.py +0 -209
  255. aethergraph/services/memory/distillers/episode.py +0 -116
  256. aethergraph/services/memory/distillers/rolling.py +0 -74
  257. aethergraph/services/memory/facade.py +0 -633
  258. aethergraph/services/memory/persist_fs.py +0 -40
  259. aethergraph/services/rag/index/base.py +0 -27
  260. aethergraph/services/rag/index/faiss_index.py +0 -121
  261. aethergraph/services/rag/index/sqlite_index.py +0 -134
  262. aethergraph-0.1.0a1.dist-info/RECORD +0 -182
  263. aethergraph-0.1.0a1.dist-info/entry_points.txt +0 -2
  264. {aethergraph-0.1.0a1.dist-info → aethergraph-0.1.0a2.dist-info}/WHEEL +0 -0
  265. {aethergraph-0.1.0a1.dist-info → aethergraph-0.1.0a2.dist-info}/licenses/LICENSE +0 -0
  266. {aethergraph-0.1.0a1.dist-info → aethergraph-0.1.0a2.dist-info}/licenses/NOTICE +0 -0
  267. {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 os
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, Request, UploadFile, WebSocket, WebSocketDisconnect
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.plugins.channel.adapters.webui import WebChannelAdapter, WebSessionHub
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
- webui_router = APIRouter()
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
- def _hub(app) -> WebSessionHub:
21
- return getattr(app.state, HUB_ATTR)
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
- def _uploads_dir(app) -> str:
25
- return getattr(app.state, UPLOAD_DIR_ATTR)
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
- async def send_json(payload: dict):
34
- await ws.send_json(payload)
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
- hub = _hub(ws.app)
37
- await hub.attach(session_id, send_json)
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
- while True:
41
- msg = await ws.receive_json()
42
- # Expect inbound: {"type": "resume", "run_id": ..., "node_id": ..., "token": ..., "payload": {...}}
43
- t = (msg or {}).get("type")
44
- if t == "resume":
45
- c = ws.app.state.container
46
- # basic token verification happens in ResumeRouter
47
- await c.resume_router.resume(
48
- run_id=msg["run_id"],
49
- node_id=msg["node_id"],
50
- token=msg["token"],
51
- payload=msg.get("payload") or {},
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
- # ------- HTTP resume fallback (for InputDock before WS ready) -------
61
- class ResumeBody(BaseModel):
62
- run_id: str
63
- node_id: str
64
- token: str
65
- payload: dict
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
- # ------- Uploads -------
76
- @webui_router.post("/api/web/upload")
77
- async def upload_files(request: Request, files: list[UploadFile] = None):
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
- Save to <workspace>/web_uploads/<session_or_any>/... and return FileRef[]:
80
- [{url, filename, size, mime}]
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
- if files is None:
84
- files = File(...)
85
- root = _uploads_dir(request.app)
86
- os.makedirs(root, exist_ok=True)
87
-
88
- out = []
89
- for f in files:
90
- target = os.path.join(root, f.filename)
91
- with open(target, "wb") as w:
92
- w.write(await f.read())
93
- url = f"/api/web/files/{f.filename}"
94
- out.append(
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
- "url": url,
97
- "filename": f.filename,
98
- "mime": f.content_type or "application/octet-stream",
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
- @webui_router.get("/api/web/files/{filename}")
105
- async def serve_uploaded(request: Request, filename: str):
106
- root = _uploads_dir(request.app)
107
- path = os.path.join(root, filename)
108
- if not os.path.exists(path):
109
- return JSONResponse({"error": "not found"}, status_code=404)
110
- return FileResponse(path, filename=filename)
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
- # ------- Integration helper -------
114
- def install_web_channel(app: Any):
115
- """
116
- 1) Creates a WebSessionHub
117
- 2) Registers WebChannelAdapter under prefix 'web' in ChannelBus
118
- 3) Sets default channel to 'web:session/<uuid>'
119
- 4) Ensures upload dir exists
120
- """
121
- # 1) Hub
122
- hub = WebSessionHub()
123
- setattr(app.state, HUB_ATTR, hub)
124
-
125
- # 2) Adapter registration
126
- container = app.state.container
127
- web_adapter = WebChannelAdapter(hub)
128
- container.channels.adapters["web"] = web_adapter
129
-
130
- # 3) Keep default as console unless you want to swap globally:
131
- # container.channels.set_default_channel_key("web:session/dev-local")
132
-
133
- # 4) Upload dir
134
- updir = os.path.join(container.root, "web_uploads")
135
- os.makedirs(updir, exist_ok=True)
136
- setattr(app.state, UPLOAD_DIR_ATTR, updir)
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
+ )