writer 0.8.3rc21__py3-none-any.whl → 1.25.1rc1__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 (265) hide show
  1. writer/ai/__init__.py +277 -28
  2. writer/app_runner.py +236 -32
  3. writer/app_templates/default/.wf/components-blueprints_blueprint-0-0decp3w5erhvl0nw.jsonl +11 -0
  4. writer/app_templates/default/.wf/components-page-0-c0f99a9e-5004-4e75-a6c6-36f17490b134.jsonl +22 -10
  5. writer/app_templates/default/.wf/components-root.jsonl +1 -1
  6. writer/app_templates/default/.wf/components-workflows_root.jsonl +1 -0
  7. writer/app_templates/default/.wf/components-workflows_workflow-0-lfltcky7l1fsm6j2.jsonl +1 -0
  8. writer/app_templates/default/.wf/metadata.json +1 -1
  9. writer/app_templates/default/README.md +3 -0
  10. writer/app_templates/default/main.py +8 -5
  11. writer/app_templates/default/requirements.txt +1 -0
  12. writer/app_templates/default/static/agent_builder_demo.png +0 -0
  13. writer/app_templates/hello/main.py +3 -0
  14. writer/auth.py +7 -2
  15. writer/autogen.py +26 -9
  16. writer/blocks/__init__.py +21 -1
  17. writer/blocks/addtostatelist.py +5 -4
  18. writer/blocks/apitrigger.py +45 -0
  19. writer/blocks/base_block.py +134 -14
  20. writer/blocks/changepage.py +1 -1
  21. writer/blocks/code.py +27 -11
  22. writer/blocks/crontrigger.py +49 -0
  23. writer/blocks/foreach.py +3 -3
  24. writer/blocks/httprequest.py +8 -24
  25. writer/blocks/ifelse.py +71 -0
  26. writer/blocks/logmessage.py +2 -2
  27. writer/blocks/returnvalue.py +2 -2
  28. writer/blocks/runblueprint.py +2 -2
  29. writer/blocks/setstate.py +5 -5
  30. writer/blocks/sharedblueprint.py +86 -0
  31. writer/blocks/writeraddchatmessage.py +45 -7
  32. writer/blocks/writeraddtokg.py +31 -4
  33. writer/blocks/writeraskkg.py +27 -34
  34. writer/blocks/writerchat.py +14 -9
  35. writer/blocks/writerchatreply.py +279 -0
  36. writer/blocks/writerchatreplywithtoolconfig.py +393 -0
  37. writer/blocks/writerclassification.py +42 -5
  38. writer/blocks/writercompletion.py +4 -4
  39. writer/blocks/writerfileapi.py +4 -4
  40. writer/blocks/writerinitchat.py +5 -4
  41. writer/blocks/writerkeyvaluestorage.py +106 -0
  42. writer/blocks/writernocodeapp.py +4 -4
  43. writer/blocks/writerparsepdf.py +8 -6
  44. writer/blocks/writerstructuredoutput.py +105 -0
  45. writer/blocks/writertoolcalling.py +106 -51
  46. writer/blocks/writervision.py +141 -0
  47. writer/blocks/writerwebsearch.py +175 -0
  48. writer/blueprints.py +715 -251
  49. writer/command_line.py +52 -16
  50. writer/core.py +200 -35
  51. writer/core_ui.py +4 -0
  52. writer/evaluator.py +38 -24
  53. writer/journal.py +227 -0
  54. writer/keyvalue_storage.py +93 -0
  55. writer/logs.py +277 -0
  56. writer/serve.py +402 -198
  57. writer/ss_types.py +41 -0
  58. writer/static/assets/BaseMarkdown-Wrvby5J8.js +1 -0
  59. writer/static/assets/BlueprintToolbar-BuXNRxWT.js +1 -0
  60. writer/static/assets/BlueprintToolbar-wpfX0jo_.css +1 -0
  61. writer/static/assets/BuilderApp-PTOI76jZ.js +8 -0
  62. writer/static/assets/BuilderApp-WimUfNZr.css +1 -0
  63. writer/static/assets/BuilderApplicationSelect-DXzy4e_h.js +7 -0
  64. writer/static/assets/BuilderApplicationSelect-XaM1D5fv.css +1 -0
  65. writer/static/assets/BuilderBlueprintLibraryPanel-Ckrhknlh.css +1 -0
  66. writer/static/assets/BuilderBlueprintLibraryPanel-DBDzhTlc.js +1 -0
  67. writer/static/assets/{BuilderEmbeddedCodeEditor-DiDqfWdt.css → BuilderEmbeddedCodeEditor-B0bcjlhk.css} +1 -1
  68. writer/static/assets/{BuilderEmbeddedCodeEditor-CbK-r9w6.js → BuilderEmbeddedCodeEditor-Dn7eDICN.js} +7 -7
  69. writer/static/assets/BuilderGraphSelect-C-LRsO8W.js +7 -0
  70. writer/static/assets/BuilderGraphSelect-D7B61d5s.css +1 -0
  71. writer/static/assets/{BuilderInsertionLabel-CDlWX-mE.js → BuilderInsertionLabel-BhyL9wgn.js} +1 -1
  72. writer/static/assets/{BuilderInsertionOverlay-B7-TsHNC.js → BuilderInsertionOverlay-MkAIVruY.js} +1 -1
  73. writer/static/assets/BuilderJournal-A0LcEwGI.js +7 -0
  74. writer/static/assets/BuilderJournal-DHv3Pvvm.css +1 -0
  75. writer/static/assets/BuilderModelSelect-CdSo_sih.js +7 -0
  76. writer/static/assets/BuilderModelSelect-Dc4IPLp2.css +1 -0
  77. writer/static/assets/BuilderSettings-BDwZBveu.js +16 -0
  78. writer/static/assets/BuilderSettings-lZkOXEYw.css +1 -0
  79. writer/static/assets/BuilderSettingsArtifactAPITriggerDetails-3O6jKBXD.js +4 -0
  80. writer/static/assets/BuilderSettingsArtifactAPITriggerDetails-DnX66iRg.css +1 -0
  81. writer/static/assets/BuilderSettingsDeploySharedBlueprint-BR_3ptsd.js +1 -0
  82. writer/static/assets/BuilderSettingsDeploySharedBlueprint-KJTl8gxP.css +1 -0
  83. writer/static/assets/BuilderSettingsHandlers-CBtEQFSo.js +1 -0
  84. writer/static/assets/BuilderSettingsHandlers-DJPeASfz.css +1 -0
  85. writer/static/assets/BuilderSidebarComponentTree-DLltgas5.js +1 -0
  86. writer/static/assets/BuilderSidebarComponentTree-DYu1F793.css +1 -0
  87. writer/static/assets/BuilderSidebarToolkit-CApZNTAq.js +7 -0
  88. writer/static/assets/BuilderSidebarToolkit-CwqbjRv8.css +1 -0
  89. writer/static/assets/BuilderTemplateEditor-CYSDeWgV.css +1 -0
  90. writer/static/assets/BuilderTemplateEditor-DnRDRcA0.js +87 -0
  91. writer/static/assets/BuilderVault-2vGoV0sx.js +1 -0
  92. writer/static/assets/BuilderVault-Cx6oQSES.css +1 -0
  93. writer/static/assets/ComponentRenderer-72hqvEvI.css +1 -0
  94. writer/static/assets/ComponentRenderer-D4Pj1i3s.js +1 -0
  95. writer/static/assets/SharedCopyClipboardButton-BipJKGtz.css +1 -0
  96. writer/static/assets/SharedCopyClipboardButton-DNI9kLe6.js +1 -0
  97. writer/static/assets/WdsCheckbox-DKvpPA4D.css +1 -0
  98. writer/static/assets/WdsCheckbox-edQcn1cf.js +1 -0
  99. writer/static/assets/WdsDropdownMenu-CzzPN9Wg.css +1 -0
  100. writer/static/assets/WdsDropdownMenu-DQnrRBNV.js +1 -0
  101. writer/static/assets/WdsFieldWrapper-Cmufx5Nj.js +1 -0
  102. writer/static/assets/WdsFieldWrapper-CsemOh8D.css +1 -0
  103. writer/static/assets/WdsTabs-DKj7BqI0.css +1 -0
  104. writer/static/assets/WdsTabs-DcfY_zn5.js +1 -0
  105. writer/static/assets/{art-paper-WsD9P5Lu.svg → art-paper-D70v1WMA.svg} +0 -1
  106. writer/static/assets/{cssMode-6B7VrieQ.js → cssMode-BYq4oZGq.js} +1 -1
  107. writer/static/assets/{freemarker2-Dy54TNCQ.js → freemarker2-CnNourkO.js} +1 -1
  108. writer/static/assets/{handlebars-BnWqX2x5.js → handlebars-Bm22yapJ.js} +1 -1
  109. writer/static/assets/{html-CIuj_eOg.js → html-CAKAfoZF.js} +1 -1
  110. writer/static/assets/{htmlMode-5fUQN2xJ.js → htmlMode-BGZ97n-V.js} +1 -1
  111. writer/static/assets/index-BKNuk68o.css +1 -0
  112. writer/static/assets/index-BQNXU3IR.js +17 -0
  113. writer/static/assets/index-DHXAd5Yn.js +4 -0
  114. writer/static/assets/index-Zki-pfO-.js +8525 -0
  115. writer/static/assets/index.esm-B1ZQtduY.js +17 -0
  116. writer/static/assets/{javascript-PLzaI1wY.js → javascript-X1f02eyK.js} +1 -1
  117. writer/static/assets/{jsonMode-CzZfZ4-D.js → jsonMode-hT0bNgT8.js} +1 -1
  118. writer/static/assets/{liquid-Cy21vWLb.js → liquid-KmCCiJw2.js} +1 -1
  119. writer/static/assets/{mapbox-gl-BjXsUCYi.js → mapbox-gl-C0cyFYYW.js} +1 -1
  120. writer/static/assets/{mdx-Cgik3q5p.js → mdx-DtRFauUw.js} +1 -1
  121. writer/static/assets/pdf-B6-yWJ-Y.js +12 -0
  122. writer/static/assets/pdf.worker.min-CyUfim15.mjs +21 -0
  123. writer/static/assets/{plotly.min-Dk-1ahEu.js → plotly.min-DutuuatZ.js} +1 -1
  124. writer/static/assets/{python-h6gjz_bN.js → python-DVhxg746.js} +1 -1
  125. writer/static/assets/{razor-BQ1k9241.js → razor-DR5Ns_BC.js} +1 -1
  126. writer/static/assets/{tsMode-BaBWt05D.js → tsMode-BNUEZzir.js} +1 -1
  127. writer/static/assets/{typescript-B42-gYGT.js → typescript-CRVt7Hx0.js} +1 -1
  128. writer/static/assets/useBlueprintRun-C00bCxh-.js +1 -0
  129. writer/static/assets/useKeyValueEditor-nDmI7cTJ.js +1 -0
  130. writer/static/assets/useListResources-DLkZhRSJ.js +1 -0
  131. writer/static/assets/{xml-BOez4Prd.js → xml-C_6-t1tb.js} +1 -1
  132. writer/static/assets/{yaml-BQhoEOIz.js → yaml-DIw8G7jk.js} +1 -1
  133. writer/static/components/annotatedtext.svg +3 -3
  134. writer/static/components/avatar.svg +3 -3
  135. writer/static/components/blueprints_addtostatelist.svg +3 -3
  136. writer/static/components/blueprints_apitrigger.svg +4 -0
  137. writer/static/components/blueprints_category_Logic.svg +3 -3
  138. writer/static/components/blueprints_category_Other.svg +3 -3
  139. writer/static/components/blueprints_category_Writer.svg +24 -5
  140. writer/static/components/blueprints_code.svg +6 -6
  141. writer/static/components/blueprints_crontrigger.svg +6 -0
  142. writer/static/components/blueprints_foreach.svg +3 -3
  143. writer/static/components/blueprints_httprequest.svg +6 -6
  144. writer/static/components/blueprints_logmessage.svg +10 -3
  145. writer/static/components/blueprints_parsejson.svg +3 -3
  146. writer/static/components/blueprints_returnvalue.svg +3 -3
  147. writer/static/components/blueprints_runblueprint.svg +3 -3
  148. writer/static/components/blueprints_setstate.svg +3 -3
  149. writer/static/components/blueprints_writeraddchatmessage.svg +14 -6
  150. writer/static/components/blueprints_writeraddtokg.svg +14 -6
  151. writer/static/components/blueprints_writerchatreply.svg +19 -0
  152. writer/static/components/blueprints_writerclassification.svg +23 -8
  153. writer/static/components/blueprints_writercompletion.svg +13 -5
  154. writer/static/components/blueprints_writernocodeapp.svg +13 -3
  155. writer/static/components/button.svg +3 -3
  156. writer/static/components/category_Content.svg +3 -3
  157. writer/static/components/category_Embed.svg +3 -3
  158. writer/static/components/category_Input.svg +4 -4
  159. writer/static/components/category_Layout.svg +6 -6
  160. writer/static/components/category_Other.svg +3 -3
  161. writer/static/components/chatbot.svg +3 -3
  162. writer/static/components/checkboxinput.svg +3 -3
  163. writer/static/components/colorinput.svg +6 -6
  164. writer/static/components/column.svg +3 -3
  165. writer/static/components/columns.svg +3 -3
  166. writer/static/components/dataframe.svg +3 -3
  167. writer/static/components/dateinput.svg +3 -3
  168. writer/static/components/dropdowninput.svg +4 -4
  169. writer/static/components/fileinput.svg +3 -3
  170. writer/static/components/googlemaps.svg +3 -3
  171. writer/static/components/heading.svg +6 -6
  172. writer/static/components/horizontalstack.svg +3 -3
  173. writer/static/components/html.svg +6 -6
  174. writer/static/components/icon.svg +3 -3
  175. writer/static/components/iframe.svg +3 -3
  176. writer/static/components/image.svg +10 -3
  177. writer/static/components/jsonviewer.svg +3 -3
  178. writer/static/components/link.svg +7 -7
  179. writer/static/components/mapbox.svg +3 -3
  180. writer/static/components/message.svg +3 -3
  181. writer/static/components/metric.svg +3 -3
  182. writer/static/components/multiselectinput.svg +3 -3
  183. writer/static/components/numberinput.svg +3 -3
  184. writer/static/components/pagination.svg +3 -3
  185. writer/static/components/pdf.svg +3 -3
  186. writer/static/components/plotlygraph.svg +6 -6
  187. writer/static/components/progressbar.svg +4 -4
  188. writer/static/components/radioinput.svg +3 -3
  189. writer/static/components/rangeinput.svg +3 -3
  190. writer/static/components/ratinginput.svg +3 -3
  191. writer/static/components/repeater.svg +3 -3
  192. writer/static/components/reuse.svg +3 -3
  193. writer/static/components/section.svg +3 -3
  194. writer/static/components/selectinput.svg +4 -4
  195. writer/static/components/separator.svg +3 -3
  196. writer/static/components/sidebar.svg +3 -3
  197. writer/static/components/sliderinput.svg +3 -3
  198. writer/static/components/step.svg +3 -3
  199. writer/static/components/steps.svg +3 -3
  200. writer/static/components/switchinput.svg +3 -3
  201. writer/static/components/tab.svg +3 -3
  202. writer/static/components/tabs.svg +3 -3
  203. writer/static/components/tags.svg +10 -3
  204. writer/static/components/text.svg +3 -3
  205. writer/static/components/textareainput.svg +10 -3
  206. writer/static/components/textinput.svg +3 -3
  207. writer/static/components/timeinput.svg +3 -3
  208. writer/static/components/timer.svg +3 -3
  209. writer/static/components/videoplayer.svg +10 -3
  210. writer/static/components/webcamcapture.svg +3 -3
  211. writer/static/index.html +3 -11
  212. writer/static/status/cancelled.svg +5 -0
  213. writer/static/status/skipped.svg +4 -0
  214. writer/static/status/stopped.svg +4 -0
  215. writer/sync.py +431 -0
  216. writer/ui.py +49 -41
  217. writer/vault.py +48 -0
  218. writer/wf_project.py +5 -5
  219. writer-1.25.1rc1.dist-info/METADATA +92 -0
  220. writer-1.25.1rc1.dist-info/RECORD +382 -0
  221. {writer-0.8.3rc21.dist-info → writer-1.25.1rc1.dist-info}/WHEEL +1 -1
  222. writer/app_templates/default/.wf/components-blueprints_blueprint-0-t84xyhxau9ej3823.jsonl +0 -18
  223. writer/app_templates/default/static/welcome.svg +0 -40
  224. writer/static/assets/BaseMarkdown-BH_nSq9H.js +0 -1
  225. writer/static/assets/BlueprintToolbar-BO-WERxH.css +0 -1
  226. writer/static/assets/BlueprintToolbar-CHWL-rTm.js +0 -1
  227. writer/static/assets/BuilderApp-0XXiQ2l7.js +0 -7
  228. writer/static/assets/BuilderApp-CAhvLO4a.css +0 -1
  229. writer/static/assets/BuilderApplicationSelect-CwzU4F1-.js +0 -7
  230. writer/static/assets/BuilderApplicationSelect-DYYFtqjx.css +0 -1
  231. writer/static/assets/BuilderGraphSelect-CVO4gzts.css +0 -1
  232. writer/static/assets/BuilderGraphSelect-DV5Xy0HK.js +0 -7
  233. writer/static/assets/BuilderInstanceTracker-BECcXNnW.css +0 -1
  234. writer/static/assets/BuilderInstanceTracker-Dr0XhpSH.js +0 -1
  235. writer/static/assets/BuilderModelSelect-QHUGd86u.css +0 -1
  236. writer/static/assets/BuilderModelSelect-ow0XEf2t.js +0 -7
  237. writer/static/assets/BuilderSettings-C2WRfSor.css +0 -1
  238. writer/static/assets/BuilderSettings-D5bjbcWj.js +0 -24
  239. writer/static/assets/BuilderSettingsHandlers-1QLaHR8J.css +0 -1
  240. writer/static/assets/BuilderSettingsHandlers-j1aMAV4J.js +0 -1
  241. writer/static/assets/BuilderSidebarComponentTree-0YajaJke.css +0 -1
  242. writer/static/assets/BuilderSidebarComponentTree-CtaOZfpV.js +0 -1
  243. writer/static/assets/BuilderSidebarPanel-BMJVzhd3.js +0 -1
  244. writer/static/assets/BuilderSidebarPanel-BrLsNxVM.css +0 -1
  245. writer/static/assets/BuilderSidebarToolkit-BbjOOp8E.js +0 -1
  246. writer/static/assets/BuilderSidebarToolkit-BvZDShKD.css +0 -1
  247. writer/static/assets/ComponentRenderer-B76bKRZO.css +0 -1
  248. writer/static/assets/ComponentRenderer-CZs4z773.js +0 -1
  249. writer/static/assets/SharedMoreDropdown-BWKlox8E.css +0 -1
  250. writer/static/assets/SharedMoreDropdown-DKv_HNef.js +0 -7
  251. writer/static/assets/WdsDropdownMenu-C1UyKOJR.css +0 -1
  252. writer/static/assets/WdsDropdownMenu-CGiATY2E.js +0 -1
  253. writer/static/assets/WdsLoaderDots-qdyk2N-2.js +0 -1
  254. writer/static/assets/index-BJMAe9SN.js +0 -8
  255. writer/static/assets/index-CPCeQU9V.css +0 -1
  256. writer/static/assets/index-ChWW_c_j.js +0 -439
  257. writer/static/assets/instancePath-BsbOTTI8.js +0 -1
  258. writer/static/assets/material-symbols-outlined-latin-wght-normal-DuE-q1Ez.woff2 +0 -0
  259. writer/static/assets/useBlueprintRun-CBOvzWTA.js +0 -1
  260. writer/static/assets/useComponentDescription-FHKxu8gg.js +0 -1
  261. writer/static/assets/useListResources-BpMgq7XI.js +0 -1
  262. writer-0.8.3rc21.dist-info/METADATA +0 -117
  263. writer-0.8.3rc21.dist-info/RECORD +0 -342
  264. {writer-0.8.3rc21.dist-info → writer-1.25.1rc1.dist-info}/entry_points.txt +0 -0
  265. {writer-0.8.3rc21.dist-info → writer-1.25.1rc1.dist-info/licenses}/LICENSE.txt +0 -0
writer/serve.py CHANGED
@@ -10,29 +10,44 @@ import os
10
10
  import os.path
11
11
  import pathlib
12
12
  import socket
13
+ import tempfile
13
14
  import textwrap
14
15
  import time
15
16
  import typing
16
- from contextlib import asynccontextmanager
17
+ from contextlib import asynccontextmanager, suppress
17
18
  from importlib.machinery import ModuleSpec
18
- from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Type, Union, cast
19
+ from typing import (
20
+ Any,
21
+ AsyncGenerator,
22
+ Callable,
23
+ Dict,
24
+ List,
25
+ Optional,
26
+ Set,
27
+ Tuple,
28
+ Type,
29
+ Union,
30
+ cast,
31
+ )
19
32
  from urllib.parse import urlsplit
20
33
 
34
+ import orjson
21
35
  import uvicorn
22
- from fastapi import FastAPI, HTTPException, Request, Response
23
- from fastapi.responses import FileResponse, JSONResponse
36
+ from fastapi import FastAPI, File, HTTPException, Request, Response, UploadFile
37
+ from fastapi.responses import FileResponse, JSONResponse, StreamingResponse
24
38
  from fastapi.routing import Mount
25
39
  from fastapi.staticfiles import StaticFiles
26
40
  from pydantic import ValidationError
27
41
  from starlette.websockets import WebSocket, WebSocketDisconnect, WebSocketState
28
42
 
29
- from writer import VERSION, abstract, crypto
43
+ from writer import VERSION, abstract
30
44
  from writer.ai import Graph
31
45
  from writer.app_runner import AppRunner
32
46
  from writer.ss_types import (
33
47
  AppProcessServerResponse,
34
48
  AutogenRequestBody,
35
49
  ComponentUpdateRequestPayload,
50
+ DeleteDataRequestBody,
36
51
  EventResponsePayload,
37
52
  HashRequestPayload,
38
53
  HashRequestResponsePayload,
@@ -41,6 +56,8 @@ from writer.ss_types import (
41
56
  InitResponseBodyRun,
42
57
  InitSessionRequestPayload,
43
58
  InitSessionResponsePayload,
59
+ RetrieveDataRequestBody,
60
+ RetrieveDataResponseBody,
44
61
  ServeMode,
45
62
  StateEnquiryResponsePayload,
46
63
  WriterEvent,
@@ -52,104 +69,13 @@ if typing.TYPE_CHECKING:
52
69
  from .auth import Auth, Unauthorized
53
70
 
54
71
  MAX_WEBSOCKET_MESSAGE_SIZE = 201 * 1024 * 1024
55
- logging.getLogger().setLevel(logging.INFO)
56
-
57
-
58
- class JobVault:
59
- SCHEMES: List[str] = []
60
- job_vault_implementations: List[Type["JobVault"]] = []
61
-
62
- def __init__(self):
63
- self.counter = 0
64
- self.vault = {}
65
-
66
- def generate_job_id(self):
67
- self.counter += 1
68
- return str(self.counter)
69
-
70
- def set(self, job_id: str, value: Any):
71
- self.vault[job_id] = value
72
-
73
- def get(self, job_id: str):
74
- return self.vault.get(job_id)
75
-
76
- @classmethod
77
- def register(cls, klass: Type["JobVault"]):
78
- cls.job_vault_implementations.insert(0, klass)
79
-
80
- @classmethod
81
- def _get_matching_implementation(cls, connection_string):
82
- for job_vault_implementation in cls.job_vault_implementations:
83
- for scheme in job_vault_implementation.SCHEMES:
84
- if connection_string.startswith(scheme):
85
- return job_vault_implementation
86
-
87
- @classmethod
88
- def create_vault(cls):
89
- connection_string = os.getenv("WRITER_PERSISTENT_STORE")
90
- if not connection_string:
91
- return cls()
92
-
93
- matching_implementation = cls._get_matching_implementation(connection_string)
94
- if not matching_implementation:
95
- supported_schemes = [
96
- scheme
97
- for implementation in JobVault.job_vault_implementations
98
- for scheme in implementation.SCHEMES
99
- ]
100
- supported_schemes_msg = ", ".join(supported_schemes)
101
- logging.error(f"No matching implementation found for { connection_string }. Falling back to in-memory JobVault. \
102
- Supported schemes: {supported_schemes_msg}.")
103
- return cls()
104
-
105
- try:
106
- return matching_implementation()
107
- except Exception as e:
108
- logging.error(
109
- f"There was an error connecting to { connection_string }. Falling back to in-memory JobVault. {repr(e)}"
110
- )
111
- return cls()
112
-
113
-
114
- class RedisJobVault(JobVault):
115
- SCHEMES = ["redis://", "rediss://", "redis-socket://", "redis-sentinel://"]
116
- DEFAULT_TTL = 86400
117
-
118
- def __init__(self):
119
- import redis # type: ignore
120
-
121
- super().__init__()
122
- redis_connection_string = os.getenv("WRITER_PERSISTENT_STORE")
123
- self.redis_client = redis.from_url(
124
- redis_connection_string, decode_responses=True, socket_timeout=30
125
- )
126
- self.counter_key = "job_counter"
127
- if not self.redis_client.exists(self.counter_key):
128
- self.redis_client.set(self.counter_key, 0)
129
-
130
- def generate_job_id(self):
131
- job_id = self.redis_client.incr(self.counter_key)
132
- return str(job_id)
133
-
134
- def set(self, job_id: str, value: Any):
135
- ttl = RedisJobVault.DEFAULT_TTL
136
- env_ttl = os.getenv("WRITER_PERSISTENT_STORE_TTL")
137
- if env_ttl is not None:
138
- ttl = int(env_ttl)
139
- json_str = json.dumps(value)
140
- self.redis_client.set(f"job:{job_id}", json_str, ex=ttl)
141
-
142
- def get(self, job_id: str):
143
- json_str = self.redis_client.get(f"job:{job_id}")
144
- if not json_str:
145
- return None
146
- return json.loads(json_str)
72
+ BLUEPRINT_API_EXECUTION_TIMEOUT_SECONDS = int(os.getenv("AGENT_BUILDER_BLUEPRINT_API_EXECUTION_TIMEOUT", "600"))
73
+ BLUEPRINT_API_RETRY_TIMEOUT = int(os.getenv("AGENT_BUILDER_BLUEPRINT_API_RETRY_TIMEOUT", "10000"))
147
74
 
148
75
 
149
76
  class WriterState(typing.Protocol):
150
77
  app_runner: AppRunner
151
78
  writer_app: bool
152
- job_vault: JobVault
153
79
  is_server_static_mounted: bool
154
80
  meta: Union[Dict[str, Any], Callable[[], Dict[str, Any]]] # meta tags for SEO
155
81
  opengraph_tags: Union[
@@ -175,8 +101,7 @@ def get_asgi_app(
175
101
  enable_remote_edit: bool = False,
176
102
  enable_server_setup: bool = True,
177
103
  on_load: Optional[Callable] = None,
178
- on_shutdown: Optional[Callable] = None,
179
- enable_jobs_api: bool = False,
104
+ on_shutdown: Optional[Callable] = None
180
105
  ) -> WriterFastAPI:
181
106
  """
182
107
  Builds an ASGI server that can be injected into another ASGI application
@@ -199,6 +124,7 @@ def get_asgi_app(
199
124
 
200
125
  _fix_mimetype()
201
126
  app_runner = AppRunner(user_app_path, serve_mode)
127
+ pending_tasks: Set[asyncio.Task] = set()
202
128
 
203
129
  @asynccontextmanager
204
130
  async def lifespan(asgi_app: FastAPI):
@@ -219,6 +145,13 @@ def get_asgi_app(
219
145
  except asyncio.CancelledError:
220
146
  pass
221
147
 
148
+ for pending_task in pending_tasks.copy():
149
+ pending_task.cancel()
150
+ try:
151
+ await pending_task
152
+ except asyncio.CancelledError:
153
+ pass
154
+
222
155
  app_runner.shut_down()
223
156
  if on_shutdown is not None:
224
157
  on_shutdown()
@@ -291,8 +224,65 @@ def get_asgi_app(
291
224
 
292
225
  @app.get("/api/health")
293
226
  async def health():
227
+ app_runner = app.state.app_runner
228
+
229
+ # Check user app process
230
+ if app_runner.app_process is None or not app_runner.app_process.is_alive():
231
+ return JSONResponse(
232
+ status_code=503,
233
+ content={"status": "error", "message": "User app process is not running"}
234
+ )
235
+
236
+ # Check project saver process (only in edit mode)
237
+ if app_runner.mode == "edit":
238
+ project_saver = app_runner.wf_project_context.write_files_async_process
239
+ if project_saver is None or not project_saver.is_alive():
240
+ return JSONResponse(
241
+ status_code=503,
242
+ content={"status": "error", "message": "Project saver process is not running"}
243
+ )
244
+
294
245
  return {"status": "ok"}
295
246
 
247
+ @app.get("/api/export")
248
+ async def export_zip():
249
+ if serve_mode != "edit":
250
+ raise HTTPException(status_code=403, detail="Invalid mode.")
251
+ exported_zip_stream = app_runner.export_zip()
252
+ return StreamingResponse(
253
+ exported_zip_stream,
254
+ media_type="application/x-zip-compressed",
255
+ headers={
256
+ "Content-Disposition": "attachment; filename=exported_agent.zip"
257
+ }
258
+ )
259
+
260
+ @app.post("/api/import")
261
+ async def import_zip(file: UploadFile = File(...)):
262
+ if serve_mode != "edit":
263
+ raise HTTPException(status_code=403, detail="Invalid mode.")
264
+ if not file.filename or not file.filename.endswith(".zip"):
265
+ raise HTTPException(status_code=400, detail="Only .zip files are supported.")
266
+
267
+ MAX_FILE_SIZE = 200 * 1024 * 1024
268
+
269
+ try:
270
+ with tempfile.NamedTemporaryFile(delete=False) as tmp:
271
+ # Stream file to disk to avoid memory issues
272
+ size = 0
273
+ while chunk := await file.read(8192):
274
+ size += len(chunk)
275
+ if size > MAX_FILE_SIZE:
276
+ tmp.close()
277
+ os.unlink(tmp.name)
278
+ raise HTTPException(status_code=413, detail=f"File too large. Max file size: {MAX_FILE_SIZE}")
279
+ tmp.write(chunk)
280
+ tmp_path = tmp.name
281
+ await app_runner.import_zip(tmp_path)
282
+ os.remove(tmp_path)
283
+ except ValueError:
284
+ raise HTTPException(status_code=400, detail="Invalid upload.")
285
+
296
286
  @app.post("/api/autogen")
297
287
  async def autogen(requestBody: AutogenRequestBody, request: Request):
298
288
  import writer.autogen
@@ -303,6 +293,38 @@ def get_asgi_app(
303
293
  agent_token_header
304
294
  )
305
295
 
296
+ @app.post("/api/data/retrieve")
297
+ async def retrieve_data(requestBody: RetrieveDataRequestBody) -> RetrieveDataResponseBody:
298
+ from writer.keyvalue_storage import writer_kv_storage
299
+
300
+ all_keys = writer_kv_storage.get_data_keys()
301
+
302
+ keys_to_fetch = []
303
+ for key in all_keys:
304
+ if key in requestBody.skip_keys:
305
+ continue
306
+ if requestBody.key_contains and requestBody.key_contains not in key:
307
+ continue
308
+ keys_to_fetch.append(key)
309
+
310
+ async def fetch_value(key: str):
311
+ return key, await asyncio.to_thread(writer_kv_storage.get, key, "data")
312
+
313
+ kv_pairs = await asyncio.gather(*(fetch_value(key) for key in keys_to_fetch))
314
+
315
+ return RetrieveDataResponseBody(result={k: v["data"] for k, v in kv_pairs})
316
+
317
+ @app.post("/api/data/delete")
318
+ async def delete_data(requestBody: DeleteDataRequestBody) -> None:
319
+ from writer.keyvalue_storage import writer_kv_storage
320
+
321
+ async def delete_key(key: str):
322
+ return key, await asyncio.to_thread(writer_kv_storage.delete, key)
323
+
324
+ await asyncio.gather(*(delete_key(key) for key in requestBody.keys))
325
+
326
+ return None
327
+
306
328
  @app.post("/api/init")
307
329
  async def init(
308
330
  initBody: InitRequestBody, request: Request, response: Response
@@ -362,99 +384,284 @@ def get_asgi_app(
362
384
  except json.JSONDecodeError:
363
385
  raise HTTPException(status_code=400, detail="Cannot parse the payload.")
364
386
  return payload
387
+
388
+ def has_api_trigger(app_runner: AppRunner, blueprint_id: str) -> bool:
389
+ # Check if blueprint has at least one API trigger component
390
+ if not app_runner.bmc_components:
391
+ return False
392
+ return any(
393
+ comp["type"] == "blueprints_apitrigger" and comp.get("parentId") == blueprint_id
394
+ for comp in app_runner.bmc_components.values()
395
+ )
396
+
397
+ @app.get("/private/api/blueprints")
398
+ async def get_blueprints(request: Request):
399
+ """
400
+ Returns a list of blueprints available in the agent.
401
+ """
402
+ if not app_runner.bmc_components:
403
+ return JSONResponse(content=[])
404
+
405
+ blueprints = [
406
+ {
407
+ "id": comp["id"],
408
+ "key": comp.get("content", {}).get("key")
409
+ }
410
+ for comp in app_runner.bmc_components.values()
411
+ if comp["type"] == "blueprints_blueprint"
412
+ and has_api_trigger(app_runner, comp["id"])
413
+ ]
365
414
 
366
- @app.post("/api/job/blueprint/{blueprint_key}")
367
- async def create_blueprint_job(blueprint_key: str, request: Request, response: Response):
368
- if not enable_jobs_api:
369
- raise HTTPException(status_code=404)
415
+ return JSONResponse(content=blueprints)
370
416
 
371
- crypto.verify_message_authorization_signature(f"create_job_{blueprint_key}", request)
417
+ @app.get("/private/api/cron-triggers")
418
+ async def get_cron_triggers(request: Request):
419
+ """
420
+ Returns a list of Cron Trigger blocks.
421
+ """
422
+ if not app_runner.bmc_components:
423
+ return JSONResponse(content=[], status_code=200)
424
+
425
+ definition = abstract.templates["blueprints_crontrigger"].writer
426
+
427
+ cron_triggers = [
428
+ {
429
+ "id": comp.get("id"),
430
+ "blueprint_id": comp.get("parentId"),
431
+ "name": comp.get("content", {}).get("alias") or definition["name"],
432
+ "cron_expression": comp.get("content", {}).get("cronExpression", ""),
433
+ "timezone": comp.get("content", {}).get("timezone", "UTC"),
434
+ }
435
+ for comp in app_runner.bmc_components.values()
436
+ if comp.get("type") == "blueprints_crontrigger"
437
+ ]
372
438
 
373
- def serialize_result(data):
439
+ return JSONResponse(content=cron_triggers, status_code=200)
440
+
441
+ @app.post("/private/api/blueprint/{blueprint_id}")
442
+ async def create_blueprint_job(blueprint_id: str, request: Request, response: Response, branch_id: Optional[str] = None):
443
+ # Keep-alive interval for SSE streaming
444
+ KEEPALIVE_INTERVAL = 15
445
+ payload = await _get_payload_as_json(request)
446
+
447
+ # --- Session initialization ---
448
+
449
+ async def init_session_and_validate(
450
+ app_runner: AppRunner,
451
+ cookies: Dict[str, Any],
452
+ headers: Dict[str, Any],
453
+ ) -> str:
454
+ # Initialize session with passed cookies/headers
455
+ sess_resp = await app_runner.init_session(InitSessionRequestPayload(
456
+ cookies=cookies, headers=headers, proposedSessionId=None
457
+ ))
458
+ if not sess_resp or not sess_resp.payload:
459
+ raise RuntimeError("Cannot initialize session.")
460
+ sid = sess_resp.payload.sessionId
461
+ if not await app_runner.check_session(sid):
462
+ raise RuntimeError("Cannot initialize session.")
463
+ return sid
464
+
465
+ # --- Blueprint discovery logic ---
466
+
467
+ def check_blueprint(app_runner: AppRunner, blueprint_id: str) -> bool:
468
+ # Locate blueprint component by its key
469
+ if not app_runner.bmc_components:
470
+ return False
471
+ return blueprint_id in app_runner.bmc_components
472
+
473
+ # --- Result serialization (recursive) ---
474
+
475
+ def serialize_result(data: Any) -> Any:
476
+ # Convert blueprint output into JSON-serializable structure
477
+ if isinstance(data, (str, int, float, bool, type(None))):
478
+ return data
374
479
  if isinstance(data, list):
375
480
  return [serialize_result(item) for item in data]
376
481
  if isinstance(data, dict):
377
482
  return {k: serialize_result(v) for k, v in data.items()}
378
- if isinstance(data, (str, int, float, bool, type(None))):
379
- return data
380
483
  try:
381
484
  return json.loads(json.dumps(data))
382
485
  except (TypeError, OverflowError):
383
- return f"Can't be displayed. Value of type: {str(type(data))}."
486
+ return f"Can't be displayed. Value of type: {type(data)}."
487
+
488
+ # --- SSE formatting utilities ---
489
+
490
+ async def format_event(event_type: str, data: Dict[str, Any]) -> str:
491
+ # Format a proper Server-Sent Event chunk
492
+ return f"event: {event_type}\ndata: {json.dumps(data)}\n\n"
493
+
494
+ async def format_keepalive() -> str:
495
+ # Send a SSE comment line as keep-alive (spec compliant)
496
+ return ": keep-alive\n\n"
384
497
 
385
- def update_job(job_id: str, job_info: dict):
386
- current_job_info = app.state.job_vault.get(job_id)
387
- if not current_job_info:
388
- raise RuntimeError("Job not found.")
389
- merged_info = current_job_info | {"finished_at": int(time.time())} | job_info
390
- app.state.job_vault.set(job_id, merged_info)
498
+ # --- The main worker logic that produces events ---
391
499
 
392
- def job_done_callback(task: asyncio.Task, job_id: str):
500
+ async def event_logic(queue: asyncio.Queue):
393
501
  try:
394
- apsr: Optional[AppProcessServerResponse] = task.result()
395
- if apsr is None or apsr.status != "ok":
396
- update_job(job_id, {"status": "error"})
502
+ await queue.put(await format_event("status", {"status": "in progress", "created_at": int(time.time())}))
503
+ await queue.put(await format_event("status", {"status": "initializing", "msg": "Initializing session..."}))
504
+
505
+ # Validate session & credentials
506
+ session_id = await init_session_and_validate(
507
+ app_runner, dict(request.cookies), dict(request.headers)
508
+ )
509
+
510
+ await queue.put(await format_event("status", {"status": "validating", "msg": "Validating blueprint..."}))
511
+
512
+ if not app_runner.bmc_components:
513
+ raise RuntimeError("No blueprints defined in the agent.")
514
+
515
+ blueprint_exists = check_blueprint(app_runner, blueprint_id)
516
+ if not blueprint_exists:
517
+ await queue.put(await format_event("error", {
518
+ "msg": f"Blueprint '{blueprint_id}' was not found.",
519
+ "finished_at": int(time.time())
520
+ }))
397
521
  return
398
- result = None
399
- if apsr.payload and apsr.payload.result:
400
- result = apsr.payload.result.get("result")
401
- update_job(job_id, {"status": "complete", "result": serialize_result(result)})
402
- except Exception as e:
403
- update_job(job_id, {"status": "error"})
404
- raise e
405
522
 
406
- app_response = await app_runner.init_session(
407
- InitSessionRequestPayload(
408
- cookies=dict(request.cookies), headers=dict(request.headers), proposedSessionId=None
409
- )
410
- )
523
+ if not branch_id and not has_api_trigger(app_runner, blueprint_id):
524
+ await queue.put(await format_event("error", {
525
+ "msg": f"Blueprint '{blueprint_id}' lacks an API trigger.",
526
+ "finished_at": int(time.time())
527
+ }))
528
+ return
411
529
 
412
- if not app_response or not app_response.payload:
413
- raise HTTPException(status_code=500, detail="Cannot initialize session.")
414
- session_id = app_response.payload.sessionId
415
- is_session_ok = await app_runner.check_session(session_id)
416
- if not is_session_ok:
417
- raise HTTPException(status_code=500, detail="Cannot initialize session.")
530
+ if branch_id:
531
+ block = app_runner.bmc_components.get(branch_id)
532
+ if not block:
533
+ await queue.put(await format_event("error", {
534
+ "msg": f"Block '{branch_id}' was not found.",
535
+ "finished_at": int(time.time())
536
+ }))
537
+ return
538
+
539
+ await queue.put(await format_event("status", {"status": "executing", "msg": (f"Executing branch: {branch_id}..." if branch_id else f"Executing blueprint: {blueprint_id}...")}))
540
+
541
+ if branch_id:
542
+ task = asyncio.create_task(
543
+ app_runner.handle_event(
544
+ session_id,
545
+ WriterEvent(
546
+ type="wf-run-blueprint-via-api",
547
+ isSafe=True,
548
+ handler="run_blueprint_via_api",
549
+ payload={
550
+ "blueprint_id": blueprint_id,
551
+ "trigger_type": "Cron",
552
+ "branch_id": branch_id,
553
+ **(payload or {})
554
+ },
555
+ )
556
+ )
557
+ )
558
+ else:
559
+ task = asyncio.create_task(
560
+ app_runner.handle_event(
561
+ session_id,
562
+ WriterEvent(
563
+ type="wf-run-blueprint-via-api",
564
+ isSafe=True,
565
+ handler="run_blueprint_via_api",
566
+ payload={
567
+ "blueprint_id": blueprint_id,
568
+ "trigger_type": "API",
569
+ **(payload or {})
570
+ },
571
+ )
572
+ )
573
+ )
418
574
 
419
- loop = asyncio.get_running_loop()
420
- task = loop.create_task(
421
- app_runner.handle_event(
422
- session_id,
423
- WriterEvent(
424
- type="wf-builtin-run",
425
- isSafe=True,
426
- handler=f"$runBlueprint_{blueprint_key}",
427
- payload=await _get_payload_as_json(request),
428
- ),
429
- )
430
- )
575
+ await queue.put(await format_event("status", {"status": "running", "msg": ("Branch is running. Awaiting output..." if branch_id else "Blueprint is running. Awaiting output...")}))
431
576
 
432
- job_id = app.state.job_vault.generate_job_id()
433
- app.state.job_vault.set(
434
- job_id, {"id": job_id, "status": "in progress", "created_at": int(time.time())}
435
- )
436
- task.add_done_callback(lambda t: job_done_callback(t, job_id))
437
- return {"id": job_id, "token": crypto.get_hash(f"get_job_{job_id}")}
577
+ # Await blueprint execution with timeout protection
578
+ apsr = await asyncio.wait_for(task, timeout=BLUEPRINT_API_EXECUTION_TIMEOUT_SECONDS)
438
579
 
439
- @app.get("/api/job/{job_id}")
440
- async def get_blueprint_job(job_id: str, request: Request, response: Response):
441
- if not enable_jobs_api:
442
- raise HTTPException(status_code=404)
580
+ await queue.put(await format_event("status", {"status": "processing", "msg": "Processing blueprint result..."}))
443
581
 
444
- crypto.verify_message_authorization_signature(f"get_job_{job_id}", request)
445
- job = app.state.job_vault.get(job_id)
582
+ if not apsr or apsr.status != "ok":
583
+ raise RuntimeError("Blueprint execution failed.")
446
584
 
447
- if not job:
448
- return JSONResponse(status_code=404, content={"id": job_id, "status": "not found"})
585
+ if apsr.payload and apsr.payload.result:
586
+ task_status = apsr.payload.result.get("ok", False)
587
+ result = serialize_result(
588
+ apsr.payload.result.get("result")
589
+ )
590
+ else:
591
+ task_status = False
592
+ result = "No result returned from blueprint execution."
593
+
594
+ if not task_status:
595
+ await queue.put(await format_event("error", {
596
+ "msg": result,
597
+ "finished_at": int(time.time())
598
+ }))
599
+ else:
600
+ await queue.put(await format_event("artifact", {
601
+ "artifact": result,
602
+ "finished_at": int(time.time())
603
+ }))
449
604
 
450
- status_code = 200
451
- if job.get("status") == "error":
452
- status_code = 400
605
+ except Exception as e:
606
+ # Bubble up any unexpected error as 'error' SSE event
607
+ await queue.put(await format_event("error", {
608
+ "msg": f"Agent Builder internal error: {str(e)}",
609
+ "finished_at": int(time.time())
610
+ }))
611
+ finally:
612
+ # Always mark stream completion for consumer
613
+ await queue.put("data: [DONE]\n\n")
453
614
 
454
- return JSONResponse(status_code=status_code, content=job)
615
+ # --- The streaming loop that multiplexes events and keep-alives ---
616
+
617
+ async def merged_stream() -> AsyncGenerator[str, None]:
618
+ # Type annotation required by mypy
619
+ queue: asyncio.Queue = asyncio.Queue()
620
+ producer_task = asyncio.create_task(event_logic(queue))
621
+
622
+ yield f"retry: {BLUEPRINT_API_RETRY_TIMEOUT}\n\n"
623
+
624
+ try:
625
+ while True:
626
+ try:
627
+ result = await asyncio.wait_for(
628
+ queue.get(),
629
+ timeout=KEEPALIVE_INTERVAL
630
+ )
631
+ if result == "data: [DONE]\n\n":
632
+ return
633
+ yield result
634
+ except asyncio.TimeoutError:
635
+ yield await format_keepalive()
636
+ except asyncio.CancelledError:
637
+ # Client disconnected, break streaming loop
638
+ break
639
+ finally:
640
+ # Always cancel producer to prevent orphaned task
641
+ producer_task.cancel()
642
+ with suppress(asyncio.CancelledError):
643
+ await producer_task
644
+
645
+ return StreamingResponse(
646
+ merged_stream(),
647
+ media_type="text/event-stream",
648
+ headers={
649
+ "Cache-Control": "no-cache",
650
+ "Connection": "keep-alive",
651
+ "Access-Control-Allow-Origin": "*",
652
+ "Access-Control-Allow-Headers": "Cache-Control",
653
+ },
654
+ )
455
655
 
456
656
  # Streaming
457
657
 
658
+ async def _send_json_or_queue(session_id: str, data: Any, websocket: WebSocket):
659
+ try:
660
+ binary_data = orjson.dumps(data)
661
+ await websocket.send_bytes(binary_data)
662
+ except (RuntimeError, WebSocketDisconnect):
663
+ await app_runner.queue_message(session_id, data)
664
+
458
665
  async def _stream_session_init(websocket: WebSocket):
459
666
  """
460
667
  Waits for the client to provide a session id to initialise the stream.
@@ -480,8 +687,6 @@ def get_asgi_app(
480
687
  Handles incoming requests from client.
481
688
  """
482
689
 
483
- pending_tasks: Set[asyncio.Task] = set()
484
-
485
690
  try:
486
691
  while True:
487
692
  req_message_raw = await websocket.receive_json()
@@ -523,18 +728,9 @@ def get_asgi_app(
523
728
  pending_tasks.add(new_task)
524
729
  new_task.add_done_callback(pending_tasks.discard)
525
730
  except WebSocketDisconnect:
526
- pass
731
+ return
527
732
  except asyncio.CancelledError:
528
733
  raise
529
- finally:
530
- # Cancel pending tasks
531
-
532
- for pending_task in pending_tasks.copy():
533
- pending_task.cancel()
534
- try:
535
- await pending_task
536
- except asyncio.CancelledError:
537
- pass
538
734
 
539
735
  async def _handle_incoming_event(
540
736
  websocket: WebSocket, session_id: str, req_message: WriterWebsocketIncoming
@@ -564,7 +760,7 @@ def get_asgi_app(
564
760
  res_payload = typing.cast(EventResponsePayload, apsr.payload).model_dump()
565
761
  if res_payload is not None:
566
762
  response.payload = res_payload
567
- await websocket.send_json(response.model_dump())
763
+ await _send_json_or_queue(session_id, response.model_dump(), websocket)
568
764
 
569
765
  async def _handle_incoming_edit_message(
570
766
  websocket: WebSocket, session_id: str, req_message: WriterWebsocketIncoming
@@ -582,11 +778,10 @@ def get_asgi_app(
582
778
  await app_runner.queue_announcement_async(
583
779
  "componentUpdate", req_message.payload["components"], session_id
584
780
  )
585
- elif req_message.type == "collaborationUpdate":
586
- pass
587
- # await app_runner.queue_announcement_async(
588
- # "collaborationAnnouncement", req_message.payload, exclude_session_id=session_id
589
- # )
781
+ elif req_message.type == "collaborationPing":
782
+ await app_runner.queue_announcement_async(
783
+ "collaborationUpdate", req_message.payload, exclude_session_id=session_id
784
+ )
590
785
  elif req_message.type == "codeSaveRequest":
591
786
  app_runner.save_code(
592
787
  session_id, req_message.payload["code"], req_message.payload["path"]
@@ -631,8 +826,10 @@ def get_asgi_app(
631
826
  response.payload = {"sourceFiles": app_runner.source_files}
632
827
  except Exception as error:
633
828
  response.payload = {"error": str(error)}
829
+ elif req_message.type == "writerVaultUpdate":
830
+ await app_runner.writer_vault_refresh(session_id)
634
831
 
635
- await websocket.send_json(response.model_dump())
832
+ await _send_json_or_queue(session_id, response.model_dump(), websocket)
636
833
 
637
834
  async def _handle_keep_alive_message(
638
835
  websocket: WebSocket, session_id: str, req_message: WriterWebsocketIncoming
@@ -640,7 +837,7 @@ def get_asgi_app(
640
837
  response = WriterWebsocketOutgoing(
641
838
  messageType="keepAliveResponse", trackingId=req_message.trackingId, payload=None
642
839
  )
643
- await websocket.send_json(response.model_dump())
840
+ await _send_json_or_queue(session_id, response.model_dump(), websocket)
644
841
 
645
842
  async def _handle_state_enquiry_message(
646
843
  websocket: WebSocket, session_id: str, req_message: WriterWebsocketIncoming
@@ -657,7 +854,7 @@ def get_asgi_app(
657
854
  res_payload = typing.cast(StateEnquiryResponsePayload, apsr.payload).model_dump()
658
855
  if res_payload is not None:
659
856
  response.payload = res_payload
660
- await websocket.send_json(response.model_dump())
857
+ await _send_json_or_queue(session_id, response.model_dump(), websocket)
661
858
 
662
859
  async def _handle_hash_request(
663
860
  websocket: WebSocket, session_id: str, req_message: WriterWebsocketIncoming
@@ -673,13 +870,14 @@ def get_asgi_app(
673
870
  )
674
871
  if apsr is not None and apsr.payload is not None:
675
872
  response.payload = typing.cast(HashRequestResponsePayload, apsr.payload).model_dump()
676
- await websocket.send_json(response.model_dump())
873
+ await _send_json_or_queue(session_id, response.model_dump(), websocket)
677
874
 
678
875
  async def _stream_outgoing_announcements(websocket: WebSocket, session_id: str):
679
876
  """
680
877
  Handles outgoing communications to the client (announcements).
681
878
  """
682
879
 
880
+ WEBSOCKET_CODE_UPDATE_CODE = 4001
683
881
  session_queue: asyncio.Queue = asyncio.Queue()
684
882
  app_runner.announcement_queues[session_id] = session_queue
685
883
 
@@ -689,11 +887,15 @@ def get_asgi_app(
689
887
  announcement = WriterWebsocketOutgoing(
690
888
  messageType="announcement", trackingId=-1, payload=announcement_data
691
889
  )
692
- await websocket.send_json(announcement.dict())
890
+ if websocket.application_state == WebSocketState.CONNECTED:
891
+ await websocket.send_json(announcement.dict())
693
892
  if announcement_data.get("type") == "codeUpdate":
893
+ await websocket.close(WEBSOCKET_CODE_UPDATE_CODE, "Code update.")
694
894
  return
695
895
  except WebSocketDisconnect:
696
896
  pass
897
+ except asyncio.CancelledError:
898
+ raise
697
899
  finally:
698
900
  if app_runner.announcement_queues.get(session_id) is None:
699
901
  return
@@ -719,6 +921,14 @@ def get_asgi_app(
719
921
  if not is_session_ok:
720
922
  await websocket.close(code=1008) # Invalid permissions
721
923
  return
924
+
925
+ try:
926
+ queued_messages = await app_runner.retrieve_messages(session_id)
927
+ for message in queued_messages:
928
+ await websocket.send_json(message)
929
+ await app_runner.clear_messages(session_id)
930
+ except (WebSocketDisconnect, RuntimeError):
931
+ return
722
932
 
723
933
  task1 = asyncio.create_task(_stream_incoming_requests(websocket, session_id))
724
934
  task2 = asyncio.create_task(_stream_outgoing_announcements(websocket, session_id))
@@ -765,14 +975,10 @@ def get_asgi_app(
765
975
  )
766
976
  )
767
977
 
768
- JobVault.register(RedisJobVault)
769
-
770
978
  # Return
771
979
  if enable_server_setup is True:
772
980
  _execute_server_setup_hook(user_app_path)
773
981
 
774
- app.state.job_vault = JobVault.create_vault()
775
-
776
982
  return app
777
983
 
778
984
 
@@ -820,8 +1026,7 @@ def serve(
820
1026
  port: Optional[int],
821
1027
  host,
822
1028
  enable_remote_edit=False,
823
- enable_server_setup=False,
824
- enable_jobs_api=False,
1029
+ enable_server_setup=False
825
1030
  ):
826
1031
  """Initialises the web server."""
827
1032
 
@@ -846,8 +1051,7 @@ def serve(
846
1051
  mode,
847
1052
  enable_remote_edit,
848
1053
  on_load=on_load,
849
- enable_server_setup=enable_server_setup,
850
- enable_jobs_api=enable_jobs_api,
1054
+ enable_server_setup=enable_server_setup
851
1055
  )
852
1056
  log_level = "warning"
853
1057
  uvicorn.run(