writer 0.8.3rc4__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 (378) hide show
  1. writer/__init__.py +1 -1
  2. writer/abstract.py +1 -1
  3. writer/{ai.py → ai/__init__.py} +867 -163
  4. writer/app_runner.py +596 -241
  5. writer/app_templates/default/.wf/components-blueprints_blueprint-0-0decp3w5erhvl0nw.jsonl +11 -0
  6. writer/app_templates/default/.wf/components-blueprints_root.jsonl +1 -0
  7. writer/app_templates/default/.wf/components-page-0-c0f99a9e-5004-4e75-a6c6-36f17490b134.jsonl +27 -0
  8. writer/app_templates/default/.wf/components-root.jsonl +1 -0
  9. writer/app_templates/default/.wf/components-workflows_root.jsonl +1 -0
  10. writer/app_templates/default/.wf/components-workflows_workflow-0-lfltcky7l1fsm6j2.jsonl +1 -0
  11. writer/app_templates/default/.wf/metadata.json +3 -0
  12. writer/app_templates/default/README.md +3 -0
  13. writer/app_templates/default/main.py +16 -0
  14. writer/app_templates/default/requirements.txt +1 -0
  15. writer/app_templates/default/static/README.md +8 -0
  16. writer/app_templates/default/static/agent_builder_demo.png +0 -0
  17. writer/app_templates/default/static/favicon.png +0 -0
  18. writer/app_templates/hello/.wf/components-blueprints_blueprint-0-t84xyhxau9ej3823.jsonl +18 -0
  19. writer/app_templates/hello/.wf/components-blueprints_root.jsonl +1 -0
  20. writer/app_templates/hello/.wf/components-page-0-c0f99a9e-5004-4e75-a6c6-36f17490b134.jsonl +15 -0
  21. writer/app_templates/hello/.wf/components-root.jsonl +1 -0
  22. writer/app_templates/hello/.wf/metadata.json +3 -0
  23. writer/app_templates/hello/main.py +16 -0
  24. writer/app_templates/hello/static/README.md +8 -0
  25. writer/app_templates/hello/static/favicon.png +0 -0
  26. writer/app_templates/hello/static/welcome.svg +40 -0
  27. writer/auth.py +7 -2
  28. writer/autogen.py +352 -0
  29. writer/blocks/__init__.py +51 -17
  30. writer/blocks/addtostatelist.py +10 -9
  31. writer/blocks/apitrigger.py +45 -0
  32. writer/blocks/base_block.py +332 -21
  33. writer/blocks/base_trigger.py +14 -0
  34. writer/blocks/calleventhandler.py +39 -35
  35. writer/blocks/changepage.py +48 -0
  36. writer/blocks/code.py +102 -0
  37. writer/blocks/crontrigger.py +49 -0
  38. writer/blocks/foreach.py +70 -53
  39. writer/blocks/httprequest.py +112 -99
  40. writer/blocks/ifelse.py +71 -0
  41. writer/blocks/logmessage.py +34 -39
  42. writer/blocks/parsejson.py +30 -29
  43. writer/blocks/returnvalue.py +7 -7
  44. writer/blocks/runblueprint.py +63 -0
  45. writer/blocks/setstate.py +43 -33
  46. writer/blocks/sharedblueprint.py +86 -0
  47. writer/blocks/uieventtrigger.py +49 -0
  48. writer/blocks/writeraddchatmessage.py +50 -12
  49. writer/blocks/writeraddtokg.py +38 -11
  50. writer/blocks/writeraskkg.py +123 -0
  51. writer/blocks/writerchat.py +80 -61
  52. writer/blocks/writerchatreply.py +279 -0
  53. writer/blocks/writerchatreplywithtoolconfig.py +393 -0
  54. writer/blocks/writerclassification.py +78 -39
  55. writer/blocks/writercompletion.py +49 -44
  56. writer/blocks/writerfileapi.py +85 -0
  57. writer/blocks/writerinitchat.py +24 -12
  58. writer/blocks/writerkeyvaluestorage.py +106 -0
  59. writer/blocks/writernocodeapp.py +35 -37
  60. writer/blocks/writerparsepdf.py +73 -0
  61. writer/blocks/writerstructuredoutput.py +105 -0
  62. writer/blocks/writertoolcalling.py +251 -0
  63. writer/blocks/writervision.py +141 -0
  64. writer/blocks/writerwebsearch.py +175 -0
  65. writer/blueprints.py +839 -0
  66. writer/command_line.py +52 -16
  67. writer/core.py +562 -290
  68. writer/core_ui.py +6 -2
  69. writer/evaluator.py +98 -46
  70. writer/journal.py +227 -0
  71. writer/keyvalue_storage.py +93 -0
  72. writer/logs.py +277 -0
  73. writer/serve.py +625 -327
  74. writer/ss_types.py +101 -12
  75. writer/static/assets/Arrow.dom-GBJpMYQS.js +1 -0
  76. writer/static/assets/BaseMarkdown-Wrvby5J8.js +1 -0
  77. writer/static/assets/BlueprintToolbar-BuXNRxWT.js +1 -0
  78. writer/static/assets/BlueprintToolbar-wpfX0jo_.css +1 -0
  79. writer/static/assets/BuilderApp-PTOI76jZ.js +8 -0
  80. writer/static/assets/BuilderApp-WimUfNZr.css +1 -0
  81. writer/static/assets/BuilderApplicationSelect-DXzy4e_h.js +7 -0
  82. writer/static/assets/BuilderApplicationSelect-XaM1D5fv.css +1 -0
  83. writer/static/assets/BuilderBlueprintLibraryPanel-Ckrhknlh.css +1 -0
  84. writer/static/assets/BuilderBlueprintLibraryPanel-DBDzhTlc.js +1 -0
  85. writer/static/assets/BuilderEmbeddedCodeEditor-B0bcjlhk.css +1 -0
  86. writer/static/assets/BuilderEmbeddedCodeEditor-Dn7eDICN.js +726 -0
  87. writer/static/assets/BuilderGraphSelect-C-LRsO8W.js +7 -0
  88. writer/static/assets/BuilderGraphSelect-D7B61d5s.css +1 -0
  89. writer/static/assets/BuilderInsertionLabel-BhyL9wgn.js +1 -0
  90. writer/static/assets/BuilderInsertionLabel-_YS5WPfq.css +1 -0
  91. writer/static/assets/BuilderInsertionOverlay-D2XS0ij9.css +1 -0
  92. writer/static/assets/BuilderInsertionOverlay-MkAIVruY.js +1 -0
  93. writer/static/assets/BuilderJournal-A0LcEwGI.js +7 -0
  94. writer/static/assets/BuilderJournal-DHv3Pvvm.css +1 -0
  95. writer/static/assets/BuilderModelSelect-CdSo_sih.js +7 -0
  96. writer/static/assets/BuilderModelSelect-Dc4IPLp2.css +1 -0
  97. writer/static/assets/BuilderSettings-BDwZBveu.js +16 -0
  98. writer/static/assets/BuilderSettings-lZkOXEYw.css +1 -0
  99. writer/static/assets/BuilderSettingsArtifactAPITriggerDetails-3O6jKBXD.js +4 -0
  100. writer/static/assets/BuilderSettingsArtifactAPITriggerDetails-DnX66iRg.css +1 -0
  101. writer/static/assets/BuilderSettingsDeploySharedBlueprint-BR_3ptsd.js +1 -0
  102. writer/static/assets/BuilderSettingsDeploySharedBlueprint-KJTl8gxP.css +1 -0
  103. writer/static/assets/BuilderSettingsHandlers-CBtEQFSo.js +1 -0
  104. writer/static/assets/BuilderSettingsHandlers-DJPeASfz.css +1 -0
  105. writer/static/assets/BuilderSidebarComponentTree-DLltgas5.js +1 -0
  106. writer/static/assets/BuilderSidebarComponentTree-DYu1F793.css +1 -0
  107. writer/static/assets/BuilderSidebarToolkit-CApZNTAq.js +7 -0
  108. writer/static/assets/BuilderSidebarToolkit-CwqbjRv8.css +1 -0
  109. writer/static/assets/BuilderTemplateEditor-CYSDeWgV.css +1 -0
  110. writer/static/assets/BuilderTemplateEditor-DnRDRcA0.js +87 -0
  111. writer/static/assets/BuilderVault-2vGoV0sx.js +1 -0
  112. writer/static/assets/BuilderVault-Cx6oQSES.css +1 -0
  113. writer/static/assets/ComponentRenderer-72hqvEvI.css +1 -0
  114. writer/static/assets/ComponentRenderer-D4Pj1i3s.js +1 -0
  115. writer/static/assets/SharedCopyClipboardButton-BipJKGtz.css +1 -0
  116. writer/static/assets/SharedCopyClipboardButton-DNI9kLe6.js +1 -0
  117. writer/static/assets/WdsCheckbox-DKvpPA4D.css +1 -0
  118. writer/static/assets/WdsCheckbox-edQcn1cf.js +1 -0
  119. writer/static/assets/WdsDropdownMenu-CzzPN9Wg.css +1 -0
  120. writer/static/assets/WdsDropdownMenu-DQnrRBNV.js +1 -0
  121. writer/static/assets/WdsFieldWrapper-Cmufx5Nj.js +1 -0
  122. writer/static/assets/WdsFieldWrapper-CsemOh8D.css +1 -0
  123. writer/static/assets/WdsTabs-DKj7BqI0.css +1 -0
  124. writer/static/assets/WdsTabs-DcfY_zn5.js +1 -0
  125. writer/static/assets/abap-D8nrxEjS.js +6 -0
  126. writer/static/assets/apex-BrXDlLUW.js +6 -0
  127. writer/static/assets/art-paper-D70v1WMA.svg +180 -0
  128. writer/static/assets/azcli-CElzELwZ.js +6 -0
  129. writer/static/assets/bat-CUsyEhik.js +6 -0
  130. writer/static/assets/bicep-BtxyJn6H.js +7 -0
  131. writer/static/assets/cameligo-ClBCoF8h.js +6 -0
  132. writer/static/assets/clojure-B9TqLHAk.js +6 -0
  133. writer/static/assets/codicon-BA2IlpFX.ttf +0 -0
  134. writer/static/assets/coffee-DYsfeylR.js +6 -0
  135. writer/static/assets/cpp-VVGvvgir.js +6 -0
  136. writer/static/assets/csharp-Z6z2stHy.js +6 -0
  137. writer/static/assets/csp-DgZoLDI1.js +6 -0
  138. writer/static/assets/css-KqQ96-gC.js +8 -0
  139. writer/static/assets/css.worker-DvNUQFd1.js +84 -0
  140. writer/static/assets/cssMode-BYq4oZGq.js +9 -0
  141. writer/static/assets/cypher-CYoSlgTu.js +6 -0
  142. writer/static/assets/dart-BGDl7St1.js +6 -0
  143. writer/static/assets/dockerfile-CuCtxA7T.js +6 -0
  144. writer/static/assets/ecl-BCTFAUpS.js +6 -0
  145. writer/static/assets/editor.worker-BVwmgLrR.js +11 -0
  146. writer/static/assets/elixir-C7hRTYZ9.js +6 -0
  147. writer/static/assets/flow9-Bi_qi707.js +6 -0
  148. writer/static/assets/freemarker2-CnNourkO.js +8 -0
  149. writer/static/assets/fsharp-CxaaEKKi.js +6 -0
  150. writer/static/assets/go-DUImKuGY.js +6 -0
  151. writer/static/assets/graphql-D5sGVkLV.js +6 -0
  152. writer/static/assets/handlebars-Bm22yapJ.js +6 -0
  153. writer/static/assets/hcl-zD_CCkZ1.js +6 -0
  154. writer/static/assets/html-CAKAfoZF.js +6 -0
  155. writer/static/assets/html.worker-BJMlcbMU.js +458 -0
  156. writer/static/assets/htmlMode-BGZ97n-V.js +9 -0
  157. writer/static/assets/index-5u5REPT4.js +16 -0
  158. writer/static/assets/index-BKNuk68o.css +1 -0
  159. writer/static/assets/index-BQNXU3IR.js +17 -0
  160. writer/static/assets/index-BQr1pfrb.js +1 -0
  161. writer/static/assets/index-DHXAd5Yn.js +4 -0
  162. writer/static/assets/index-Zki-pfO-.js +8525 -0
  163. writer/static/assets/index.esm-B1ZQtduY.js +17 -0
  164. writer/static/assets/ini-8kKHd4ZL.js +6 -0
  165. writer/static/assets/java-De1axCfe.js +6 -0
  166. writer/static/assets/javascript-X1f02eyK.js +6 -0
  167. writer/static/assets/json.worker-BwvX8PuZ.js +42 -0
  168. writer/static/assets/jsonMode-hT0bNgT8.js +11 -0
  169. writer/static/assets/julia-D3ApGBxz.js +6 -0
  170. writer/static/assets/kotlin-GbSrCElU.js +6 -0
  171. writer/static/assets/less-DNUaDNdz.js +7 -0
  172. writer/static/assets/lexon-Bg9QKxBu.js +6 -0
  173. writer/static/assets/liquid-KmCCiJw2.js +6 -0
  174. writer/static/assets/lua-Crkvc3mc.js +6 -0
  175. writer/static/assets/m3-DsrzVyM1.js +6 -0
  176. writer/static/assets/mapbox-gl-C0cyFYYW.js +2329 -0
  177. writer/static/assets/markdown-CY5IOZuu.js +6 -0
  178. writer/static/assets/marked.esm-273vDTCT.js +45 -0
  179. writer/static/assets/mdx-DtRFauUw.js +6 -0
  180. writer/static/assets/mips-BE8RsGBA.js +6 -0
  181. writer/static/assets/msdax-N5ajIiFQ.js +6 -0
  182. writer/static/assets/mysql-DRxbB97D.js +6 -0
  183. writer/static/assets/objective-c-BHUZy23s.js +6 -0
  184. writer/static/assets/pascal-BemVzBTY.js +6 -0
  185. writer/static/assets/pascaligo-BACCcnx_.js +6 -0
  186. writer/static/assets/pdf-B6-yWJ-Y.js +12 -0
  187. writer/static/assets/pdf.worker.min-CyUfim15.mjs +21 -0
  188. writer/static/assets/perl-CuU66Ptk.js +6 -0
  189. writer/static/assets/pgsql-CQ6TMH2r.js +6 -0
  190. writer/static/assets/php-BvyzZa65.js +6 -0
  191. writer/static/assets/pla-DrIuu9u1.js +6 -0
  192. writer/static/assets/plotly.min-DutuuatZ.js +4030 -0
  193. writer/static/assets/poppins-latin-300-italic-4WBEAciR.woff +0 -0
  194. writer/static/assets/poppins-latin-300-italic-EWCPeN2Y.woff2 +0 -0
  195. writer/static/assets/poppins-latin-300-normal-DCNuMXUj.woff +0 -0
  196. writer/static/assets/poppins-latin-300-normal-Dku2WoCh.woff2 +0 -0
  197. writer/static/assets/poppins-latin-400-italic-B4GYq972.woff2 +0 -0
  198. writer/static/assets/poppins-latin-400-italic-BPejoDS-.woff +0 -0
  199. writer/static/assets/poppins-latin-400-normal-BOb3E3N0.woff +0 -0
  200. writer/static/assets/poppins-latin-400-normal-cpxAROuN.woff2 +0 -0
  201. writer/static/assets/poppins-latin-500-italic-Ce_qjtl5.woff +0 -0
  202. writer/static/assets/poppins-latin-500-italic-o28Otv0U.woff2 +0 -0
  203. writer/static/assets/poppins-latin-500-normal-C8OXljZJ.woff2 +0 -0
  204. writer/static/assets/poppins-latin-500-normal-DGXqpDMm.woff +0 -0
  205. writer/static/assets/poppins-latin-600-italic-BhOZippK.woff +0 -0
  206. writer/static/assets/poppins-latin-600-italic-CZ4wqKBi.woff2 +0 -0
  207. writer/static/assets/poppins-latin-600-normal-BJdTmd5m.woff +0 -0
  208. writer/static/assets/poppins-latin-600-normal-zEkxB9Mr.woff2 +0 -0
  209. writer/static/assets/poppins-latin-700-italic-CW91C-LJ.woff +0 -0
  210. writer/static/assets/poppins-latin-700-italic-RKf6esGj.woff2 +0 -0
  211. writer/static/assets/poppins-latin-700-normal-BVuQR_eA.woff +0 -0
  212. writer/static/assets/poppins-latin-700-normal-Qrb0O0WB.woff2 +0 -0
  213. writer/static/assets/poppins-latin-ext-300-italic-CBzyU4Pf.woff +0 -0
  214. writer/static/assets/poppins-latin-ext-300-italic-DdDvTq5-.woff2 +0 -0
  215. writer/static/assets/poppins-latin-ext-300-normal-7Zg2msWE.woff2 +0 -0
  216. writer/static/assets/poppins-latin-ext-300-normal-C9p7gvmA.woff +0 -0
  217. writer/static/assets/poppins-latin-ext-400-italic-BiCGV3eO.woff2 +0 -0
  218. writer/static/assets/poppins-latin-ext-400-italic-gsPYOGqV.woff +0 -0
  219. writer/static/assets/poppins-latin-ext-400-normal-CIpeJEZw.woff2 +0 -0
  220. writer/static/assets/poppins-latin-ext-400-normal-Ce_uWq1Z.woff +0 -0
  221. writer/static/assets/poppins-latin-ext-500-italic-CwrTHwbn.woff2 +0 -0
  222. writer/static/assets/poppins-latin-ext-500-italic-jdc8Bv4M.woff +0 -0
  223. writer/static/assets/poppins-latin-ext-500-normal-Bl1-S02S.woff +0 -0
  224. writer/static/assets/poppins-latin-ext-500-normal-H4Q0z8D2.woff2 +0 -0
  225. writer/static/assets/poppins-latin-ext-600-italic-BqeDa496.woff2 +0 -0
  226. writer/static/assets/poppins-latin-ext-600-italic-C7MQPb_A.woff +0 -0
  227. writer/static/assets/poppins-latin-ext-600-normal-Cn4C8475.woff2 +0 -0
  228. writer/static/assets/poppins-latin-ext-600-normal-DB6FJURc.woff +0 -0
  229. writer/static/assets/poppins-latin-ext-700-italic-BAdhB_WS.woff2 +0 -0
  230. writer/static/assets/poppins-latin-ext-700-italic-WKTwQMp8.woff +0 -0
  231. writer/static/assets/poppins-latin-ext-700-normal-CE2WFKmF.woff +0 -0
  232. writer/static/assets/poppins-latin-ext-700-normal-DDaViAzG.woff2 +0 -0
  233. writer/static/assets/postiats-BR_hrfni.js +6 -0
  234. writer/static/assets/powerquery-CKDUeRmd.js +6 -0
  235. writer/static/assets/powershell-Dsa4rhA_.js +6 -0
  236. writer/static/assets/protobuf-CGsvhooB.js +7 -0
  237. writer/static/assets/pug-D2p3uOX2.js +6 -0
  238. writer/static/assets/python-DVhxg746.js +6 -0
  239. writer/static/assets/qsharp-B7F3HtPF.js +6 -0
  240. writer/static/assets/r-3aLoi2fs.js +6 -0
  241. writer/static/assets/razor-DR5Ns_BC.js +6 -0
  242. writer/static/assets/redis-jqFeRM5s.js +6 -0
  243. writer/static/assets/redshift-BriwQgXR.js +6 -0
  244. writer/static/assets/restructuredtext-hbBFZ0w9.js +6 -0
  245. writer/static/assets/ruby-ByThyB2Q.js +6 -0
  246. writer/static/assets/rust-DIEZMp5R.js +6 -0
  247. writer/static/assets/sb-C6Gjjw_x.js +6 -0
  248. writer/static/assets/scala-DZNw3jJB.js +6 -0
  249. writer/static/assets/scheme-55eqh71t.js +6 -0
  250. writer/static/assets/scss-D-OVkc4F.js +8 -0
  251. writer/static/assets/serialization-DJC7NP0N.js +20 -0
  252. writer/static/assets/shell-DSpi8_qN.js +6 -0
  253. writer/static/assets/solidity-BHddiNFS.js +6 -0
  254. writer/static/assets/sophia-D6taVZFb.js +6 -0
  255. writer/static/assets/sparql-LA0C7mUc.js +6 -0
  256. writer/static/assets/sql-C3-3IcFM.js +6 -0
  257. writer/static/assets/st-C4g7059C.js +6 -0
  258. writer/static/assets/swift-DNI1vH3h.js +8 -0
  259. writer/static/assets/systemverilog-DL_FVbcQ.js +6 -0
  260. writer/static/assets/tcl-DVJXmIwd.js +6 -0
  261. writer/static/assets/ts.worker-CwG1rUES.js +37021 -0
  262. writer/static/assets/tsMode-BNUEZzir.js +16 -0
  263. writer/static/assets/twig-BVWDLtw5.js +6 -0
  264. writer/static/assets/typescript-CRVt7Hx0.js +6 -0
  265. writer/static/assets/useBlueprintRun-C00bCxh-.js +1 -0
  266. writer/static/assets/useKeyValueEditor-nDmI7cTJ.js +1 -0
  267. writer/static/assets/useListResources-DLkZhRSJ.js +1 -0
  268. writer/static/assets/vb-Btz91-7U.js +6 -0
  269. writer/static/assets/vega-embed.module-SNP5iNdJ.js +201 -0
  270. writer/static/assets/wgsl-D8V_buCG.js +303 -0
  271. writer/static/assets/xml-C_6-t1tb.js +6 -0
  272. writer/static/assets/yaml-DIw8G7jk.js +6 -0
  273. writer/static/components/annotatedtext.svg +4 -0
  274. writer/static/components/avatar.svg +4 -0
  275. writer/static/components/blueprints_addtostatelist.svg +4 -0
  276. writer/static/components/blueprints_apitrigger.svg +4 -0
  277. writer/static/components/blueprints_calleventhandler.svg +9 -0
  278. writer/static/components/blueprints_category_Logic.svg +4 -0
  279. writer/static/components/blueprints_category_Other.svg +4 -0
  280. writer/static/components/blueprints_category_Triggers.svg +4 -0
  281. writer/static/components/blueprints_category_Writer.svg +25 -0
  282. writer/static/components/blueprints_code.svg +9 -0
  283. writer/static/components/blueprints_crontrigger.svg +6 -0
  284. writer/static/components/blueprints_foreach.svg +4 -0
  285. writer/static/components/blueprints_httprequest.svg +11 -0
  286. writer/static/components/blueprints_logmessage.svg +11 -0
  287. writer/static/components/blueprints_parsejson.svg +4 -0
  288. writer/static/components/blueprints_returnvalue.svg +4 -0
  289. writer/static/components/blueprints_runblueprint.svg +4 -0
  290. writer/static/components/blueprints_setstate.svg +4 -0
  291. writer/static/components/blueprints_uieventtrigger.svg +4 -0
  292. writer/static/components/blueprints_writeraddchatmessage.svg +19 -0
  293. writer/static/components/blueprints_writeraddtokg.svg +19 -0
  294. writer/static/components/blueprints_writerchat.svg +11 -0
  295. writer/static/components/blueprints_writerchatreply.svg +19 -0
  296. writer/static/components/blueprints_writerclassification.svg +24 -0
  297. writer/static/components/blueprints_writercompletion.svg +14 -0
  298. writer/static/components/blueprints_writerinitchat.svg +11 -0
  299. writer/static/components/blueprints_writernocodeapp.svg +14 -0
  300. writer/static/components/button.svg +4 -0
  301. writer/static/components/category_Content.svg +4 -0
  302. writer/static/components/category_Embed.svg +4 -0
  303. writer/static/components/category_Input.svg +5 -0
  304. writer/static/components/category_Layout.svg +9 -0
  305. writer/static/components/category_Other.svg +4 -0
  306. writer/static/components/chatbot.svg +4 -0
  307. writer/static/components/checkboxinput.svg +4 -0
  308. writer/static/components/colorinput.svg +11 -0
  309. writer/static/components/column.svg +4 -0
  310. writer/static/components/columns.svg +4 -0
  311. writer/static/components/dataframe.svg +4 -0
  312. writer/static/components/dateinput.svg +4 -0
  313. writer/static/components/dropdowninput.svg +5 -0
  314. writer/static/components/fileinput.svg +4 -0
  315. writer/static/components/googlemaps.svg +4 -0
  316. writer/static/components/header.svg +4 -0
  317. writer/static/components/heading.svg +9 -0
  318. writer/static/components/horizontalstack.svg +4 -0
  319. writer/static/components/html.svg +9 -0
  320. writer/static/components/icon.svg +4 -0
  321. writer/static/components/iframe.svg +4 -0
  322. writer/static/components/image.svg +11 -0
  323. writer/static/components/jsonviewer.svg +4 -0
  324. writer/static/components/link.svg +12 -0
  325. writer/static/components/mapbox.svg +4 -0
  326. writer/static/components/message.svg +4 -0
  327. writer/static/components/metric.svg +4 -0
  328. writer/static/components/multiselectinput.svg +4 -0
  329. writer/static/components/numberinput.svg +4 -0
  330. writer/static/components/page.svg +50 -0
  331. writer/static/components/pagination.svg +4 -0
  332. writer/static/components/pdf.svg +4 -0
  333. writer/static/components/plotlygraph.svg +7 -0
  334. writer/static/components/progressbar.svg +5 -0
  335. writer/static/components/radioinput.svg +4 -0
  336. writer/static/components/rangeinput.svg +4 -0
  337. writer/static/components/ratinginput.svg +4 -0
  338. writer/static/components/repeater.svg +4 -0
  339. writer/static/components/reuse.svg +4 -0
  340. writer/static/components/section.svg +4 -0
  341. writer/static/components/selectinput.svg +5 -0
  342. writer/static/components/separator.svg +4 -0
  343. writer/static/components/sidebar.svg +4 -0
  344. writer/static/components/sliderinput.svg +4 -0
  345. writer/static/components/step.svg +4 -0
  346. writer/static/components/steps.svg +4 -0
  347. writer/static/components/switchinput.svg +4 -0
  348. writer/static/components/tab.svg +4 -0
  349. writer/static/components/tabs.svg +4 -0
  350. writer/static/components/tags.svg +11 -0
  351. writer/static/components/text.svg +4 -0
  352. writer/static/components/textareainput.svg +11 -0
  353. writer/static/components/textinput.svg +4 -0
  354. writer/static/components/timeinput.svg +4 -0
  355. writer/static/components/timer.svg +4 -0
  356. writer/static/components/vegalitechart.svg +7 -0
  357. writer/static/components/videoplayer.svg +11 -0
  358. writer/static/components/webcamcapture.svg +4 -0
  359. writer/static/favicon.png +0 -0
  360. writer/static/index.html +84 -0
  361. writer/static/status/cancelled.svg +5 -0
  362. writer/static/status/error.svg +5 -0
  363. writer/static/status/skipped.svg +4 -0
  364. writer/static/status/stopped.svg +4 -0
  365. writer/static/status/success.svg +4 -0
  366. writer/sync.py +431 -0
  367. writer/ui.py +2268 -0
  368. writer/vault.py +48 -0
  369. writer/wf_project.py +90 -66
  370. writer-1.25.1rc1.dist-info/METADATA +92 -0
  371. writer-1.25.1rc1.dist-info/RECORD +382 -0
  372. {writer-0.8.3rc4.dist-info → writer-1.25.1rc1.dist-info}/WHEEL +1 -1
  373. writer/blocks/runworkflow.py +0 -59
  374. writer/workflows.py +0 -183
  375. writer-0.8.3rc4.dist-info/METADATA +0 -117
  376. writer-0.8.3rc4.dist-info/RECORD +0 -44
  377. {writer-0.8.3rc4.dist-info → writer-1.25.1rc1.dist-info}/entry_points.txt +0 -0
  378. {writer-0.8.3rc4.dist-info → writer-1.25.1rc1.dist-info/licenses}/LICENSE.txt +0 -0
writer/serve.py CHANGED
@@ -1,4 +1,5 @@
1
1
  import asyncio
2
+ import base64
2
3
  import html
3
4
  import importlib.util
4
5
  import io
@@ -9,27 +10,44 @@ import os
9
10
  import os.path
10
11
  import pathlib
11
12
  import socket
13
+ import tempfile
12
14
  import textwrap
13
15
  import time
14
16
  import typing
15
- from contextlib import asynccontextmanager
17
+ from contextlib import asynccontextmanager, suppress
16
18
  from importlib.machinery import ModuleSpec
17
- 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
+ )
18
32
  from urllib.parse import urlsplit
19
33
 
34
+ import orjson
20
35
  import uvicorn
21
- from fastapi import FastAPI, HTTPException, Request, Response
22
- from fastapi.responses import FileResponse, JSONResponse
36
+ from fastapi import FastAPI, File, HTTPException, Request, Response, UploadFile
37
+ from fastapi.responses import FileResponse, JSONResponse, StreamingResponse
23
38
  from fastapi.routing import Mount
24
39
  from fastapi.staticfiles import StaticFiles
25
40
  from pydantic import ValidationError
26
41
  from starlette.websockets import WebSocket, WebSocketDisconnect, WebSocketState
27
42
 
28
- from writer import VERSION, abstract, crypto
43
+ from writer import VERSION, abstract
44
+ from writer.ai import Graph
29
45
  from writer.app_runner import AppRunner
30
46
  from writer.ss_types import (
31
47
  AppProcessServerResponse,
48
+ AutogenRequestBody,
32
49
  ComponentUpdateRequestPayload,
50
+ DeleteDataRequestBody,
33
51
  EventResponsePayload,
34
52
  HashRequestPayload,
35
53
  HashRequestResponsePayload,
@@ -38,6 +56,8 @@ from writer.ss_types import (
38
56
  InitResponseBodyRun,
39
57
  InitSessionRequestPayload,
40
58
  InitSessionResponsePayload,
59
+ RetrieveDataRequestBody,
60
+ RetrieveDataResponseBody,
41
61
  ServeMode,
42
62
  StateEnquiryResponsePayload,
43
63
  WriterEvent,
@@ -48,119 +68,40 @@ from writer.ss_types import (
48
68
  if typing.TYPE_CHECKING:
49
69
  from .auth import Auth, Unauthorized
50
70
 
51
- MAX_WEBSOCKET_MESSAGE_SIZE = 201*1024*1024
52
- logging.getLogger().setLevel(logging.INFO)
53
-
54
-
55
- class JobVault:
56
-
57
- SCHEMES:List[str] = []
58
- job_vault_implementations: List[Type["JobVault"]] = []
59
-
60
- def __init__(self):
61
- self.counter = 0
62
- self.vault = {}
63
-
64
- def generate_job_id(self):
65
- self.counter += 1
66
- return str(self.counter)
67
-
68
- def set(self, job_id: str, value: Any):
69
- self.vault[job_id] = value
70
-
71
- def get(self, job_id: str):
72
- return self.vault.get(job_id)
73
-
74
- @classmethod
75
- def register(cls, klass: Type["JobVault"]):
76
- cls.job_vault_implementations.insert(0, klass)
77
-
78
- @classmethod
79
- def _get_matching_implementation(cls, connection_string):
80
- for job_vault_implementation in cls.job_vault_implementations:
81
- for scheme in job_vault_implementation.SCHEMES:
82
- if connection_string.startswith(scheme):
83
- return job_vault_implementation
84
-
85
- @classmethod
86
- def create_vault(cls):
87
- connection_string = os.getenv("WRITER_PERSISTENT_STORE")
88
- if not connection_string:
89
- return cls()
90
-
91
- matching_implementation = cls._get_matching_implementation(connection_string)
92
- if not matching_implementation:
93
- supported_schemes = [scheme for implementation in JobVault.job_vault_implementations for scheme in implementation.SCHEMES]
94
- supported_schemes_msg = ", ".join(supported_schemes)
95
- logging.error(f"No matching implementation found for { connection_string }. Falling back to in-memory JobVault. \
96
- Supported schemes: {supported_schemes_msg}.")
97
- return cls()
98
-
99
- try:
100
- return matching_implementation()
101
- except Exception as e:
102
- logging.error(f"There was an error connecting to { connection_string }. Falling back to in-memory JobVault. {repr(e)}")
103
- return cls()
104
-
105
-
106
- class RedisJobVault(JobVault):
107
-
108
- SCHEMES = ["redis://", "rediss://", "redis-socket://", "redis-sentinel://"]
109
- DEFAULT_TTL = 86400
110
-
111
- def __init__(self):
112
- import redis # type: ignore
113
- super().__init__()
114
- redis_connection_string = os.getenv("WRITER_PERSISTENT_STORE")
115
- self.redis_client = redis.from_url(redis_connection_string, decode_responses=True, socket_timeout=30)
116
- self.counter_key = "job_counter"
117
- if not self.redis_client.exists(self.counter_key):
118
- self.redis_client.set(self.counter_key, 0)
119
-
120
- def generate_job_id(self):
121
- job_id = self.redis_client.incr(self.counter_key)
122
- return str(job_id)
123
-
124
- def set(self, job_id: str, value: Any):
125
- ttl = RedisJobVault.DEFAULT_TTL
126
- env_ttl = os.getenv("WRITER_PERSISTENT_STORE_TTL")
127
- if env_ttl is not None:
128
- ttl = int(env_ttl)
129
- json_str = json.dumps(value)
130
- self.redis_client.set(f"job:{job_id}", json_str, ex=ttl)
131
-
132
- def get(self, job_id: str):
133
- json_str = self.redis_client.get(f"job:{job_id}")
134
- if not json_str:
135
- return None
136
- return json.loads(json_str)
71
+ MAX_WEBSOCKET_MESSAGE_SIZE = 201 * 1024 * 1024
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"))
137
74
 
138
75
 
139
76
  class WriterState(typing.Protocol):
140
77
  app_runner: AppRunner
141
78
  writer_app: bool
142
- job_vault: JobVault
143
79
  is_server_static_mounted: bool
144
- meta: Union[Dict[str, Any], Callable[[], Dict[str, Any]]] # meta tags for SEO
145
- opengraph_tags: Union[Dict[str, Any], Callable[[], Dict[str, Any]]] # opengraph tags for social networks integration (facebook, discord)
146
- title: Union[str, Callable[[], str]] # title of the page, default: "Writer Framework"
80
+ meta: Union[Dict[str, Any], Callable[[], Dict[str, Any]]] # meta tags for SEO
81
+ opengraph_tags: Union[
82
+ Dict[str, Any], Callable[[], Dict[str, Any]]
83
+ ] # opengraph tags for social networks integration (facebook, discord)
84
+ title: Union[str, Callable[[], str]] # title of the page, default: "Writer Framework"
85
+
147
86
 
148
87
  class WriterAsgi(typing.Protocol):
149
88
  state: WriterState
150
89
 
90
+
151
91
  class WriterFastAPI(FastAPI, WriterAsgi): # type: ignore
152
92
  pass
153
93
 
94
+
154
95
  app: WriterFastAPI = cast(WriterFastAPI, None)
155
96
 
97
+
156
98
  def get_asgi_app(
157
- user_app_path: str,
158
- serve_mode: ServeMode,
159
- enable_remote_edit: bool = False,
160
- enable_server_setup: bool = True,
161
- on_load: Optional[Callable] = None,
162
- on_shutdown: Optional[Callable] = None,
163
- enable_jobs_api: bool = False
99
+ user_app_path: str,
100
+ serve_mode: ServeMode,
101
+ enable_remote_edit: bool = False,
102
+ enable_server_setup: bool = True,
103
+ on_load: Optional[Callable] = None,
104
+ on_shutdown: Optional[Callable] = None
164
105
  ) -> WriterFastAPI:
165
106
  """
166
107
  Builds an ASGI server that can be injected into another ASGI application
@@ -183,6 +124,7 @@ def get_asgi_app(
183
124
 
184
125
  _fix_mimetype()
185
126
  app_runner = AppRunner(user_app_path, serve_mode)
127
+ pending_tasks: Set[asyncio.Task] = set()
186
128
 
187
129
  @asynccontextmanager
188
130
  async def lifespan(asgi_app: FastAPI):
@@ -191,9 +133,11 @@ def get_asgi_app(
191
133
  app_runner.hook_to_running_event_loop()
192
134
  app_runner.load()
193
135
 
194
- if on_load is not None \
195
- and hasattr(asgi_app.state, 'is_server_static_mounted') \
196
- and asgi_app.state.is_server_static_mounted:
136
+ if (
137
+ on_load is not None
138
+ and hasattr(asgi_app.state, "is_server_static_mounted")
139
+ and asgi_app.state.is_server_static_mounted
140
+ ):
197
141
  on_load()
198
142
 
199
143
  try:
@@ -201,6 +145,13 @@ def get_asgi_app(
201
145
  except asyncio.CancelledError:
202
146
  pass
203
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
+
204
155
  app_runner.shut_down()
205
156
  if on_shutdown is not None:
206
157
  on_shutdown()
@@ -217,10 +168,12 @@ def get_asgi_app(
217
168
  extensions_path = pathlib.Path(user_app_path) / "extensions"
218
169
  if not extensions_path.exists():
219
170
  return []
220
- filtered_files = [f for f in extensions_path.rglob(
221
- "*") if f.suffix.lower() in (".js", ".css") and f.is_file()]
222
- relative_paths = [f.relative_to(
223
- extensions_path).as_posix() for f in filtered_files]
171
+ filtered_files = [
172
+ f
173
+ for f in extensions_path.rglob("*")
174
+ if f.suffix.lower() in (".js", ".css") and f.is_file()
175
+ ]
176
+ relative_paths = [f.relative_to(extensions_path).as_posix() for f in filtered_files]
224
177
  return relative_paths
225
178
 
226
179
  cached_extension_paths = _get_extension_paths()
@@ -247,7 +200,8 @@ def get_asgi_app(
247
200
  userFunctions=payload.userFunctions,
248
201
  extensionPaths=cached_extension_paths,
249
202
  featureFlags=payload.featureFlags,
250
- abstractTemplates=abstract.templates
203
+ abstractTemplates=abstract.templates,
204
+ writerApplication=payload.writerApplication,
251
205
  )
252
206
 
253
207
  def _get_edit_starter_pack(payload: InitSessionResponsePayload):
@@ -264,16 +218,117 @@ def get_asgi_app(
264
218
  sourceFiles=app_runner.source_files,
265
219
  extensionPaths=cached_extension_paths,
266
220
  featureFlags=payload.featureFlags,
267
- abstractTemplates=abstract.templates
221
+ abstractTemplates=abstract.templates,
222
+ writerApplication=payload.writerApplication,
268
223
  )
269
224
 
270
225
  @app.get("/api/health")
271
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
+
272
245
  return {"status": "ok"}
273
246
 
274
- @app.post("/api/init")
275
- async def init(initBody: InitRequestBody, request: Request, response: Response) -> Union[InitResponseBodyRun, InitResponseBodyEdit]:
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
+
286
+ @app.post("/api/autogen")
287
+ async def autogen(requestBody: AutogenRequestBody, request: Request):
288
+ import writer.autogen
289
+ agent_token_header = request.headers.get('x-agent-token')
290
+
291
+ return writer.autogen.generate_blueprint(
292
+ requestBody.description,
293
+ agent_token_header
294
+ )
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
276
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
+
328
+ @app.post("/api/init")
329
+ async def init(
330
+ initBody: InitRequestBody, request: Request, response: Response
331
+ ) -> Union[InitResponseBodyRun, InitResponseBodyEdit]:
277
332
  """
278
333
  Handles session init and provides a "starter pack" to the frontend.
279
334
  """
@@ -284,18 +339,20 @@ def get_asgi_app(
284
339
  wrong_origin_message += "To circumvent this protection, use the --enable-remote-edit flag if running via command line."
285
340
  logging.error(wrong_origin_message, origin_header)
286
341
  raise HTTPException(
287
- status_code=403, detail="Incorrect origin. Only local origins are allowed.")
342
+ status_code=403, detail="Incorrect origin. Only local origins are allowed."
343
+ )
288
344
 
289
345
  session_id = request.cookies.get("session")
290
346
  if session_id is not None:
291
347
  initBody.proposedSessionId = session_id
292
348
 
293
-
294
- app_response = await app_runner.init_session(InitSessionRequestPayload(
295
- cookies=dict(request.cookies),
296
- headers=dict(request.headers),
297
- proposedSessionId=initBody.proposedSessionId
298
- ))
349
+ app_response = await app_runner.init_session(
350
+ InitSessionRequestPayload(
351
+ cookies=dict(request.cookies),
352
+ headers=dict(request.headers),
353
+ proposedSessionId=initBody.proposedSessionId,
354
+ )
355
+ )
299
356
 
300
357
  status = app_response.status
301
358
 
@@ -319,7 +376,7 @@ def get_asgi_app(
319
376
 
320
377
  async def _get_payload_as_json(request: Request):
321
378
  payload = None
322
- body = await request.body()
379
+ body = await request.body()
323
380
  if not body:
324
381
  return None
325
382
  try:
@@ -327,108 +384,285 @@ def get_asgi_app(
327
384
  except json.JSONDecodeError:
328
385
  raise HTTPException(status_code=400, detail="Cannot parse the payload.")
329
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
+ ]
414
+
415
+ return JSONResponse(content=blueprints)
416
+
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
+ ]
438
+
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
330
464
 
331
- @app.post("/api/job/workflow/{workflow_key}")
332
- async def create_workflow_job(workflow_key: str, request: Request, response: Response):
333
- if not enable_jobs_api:
334
- raise HTTPException(status_code=404)
465
+ # --- Blueprint discovery logic ---
335
466
 
336
- crypto.verify_message_authorization_signature(f"create_job_{workflow_key}", request)
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
337
472
 
338
- def serialize_result(data):
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
339
479
  if isinstance(data, list):
340
480
  return [serialize_result(item) for item in data]
341
481
  if isinstance(data, dict):
342
- return {k : serialize_result(v) for k, v in data.items()}
343
- if isinstance(data, (str, int, float, bool, type(None))):
344
- return data
482
+ return {k: serialize_result(v) for k, v in data.items()}
345
483
  try:
346
484
  return json.loads(json.dumps(data))
347
485
  except (TypeError, OverflowError):
348
- 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"
349
493
 
350
- def update_job(job_id: str, job_info: dict):
351
- current_job_info = app.state.job_vault.get(job_id)
352
- if not current_job_info:
353
- raise RuntimeError("Job not found.")
354
- merged_info = current_job_info | { "finished_at": int(time.time()) } | job_info
355
- app.state.job_vault.set(job_id, merged_info)
494
+ async def format_keepalive() -> str:
495
+ # Send a SSE comment line as keep-alive (spec compliant)
496
+ return ": keep-alive\n\n"
356
497
 
357
- def job_done_callback(task: asyncio.Task, job_id: str):
498
+ # --- The main worker logic that produces events ---
499
+
500
+ async def event_logic(queue: asyncio.Queue):
358
501
  try:
359
- apsr: Optional[AppProcessServerResponse] = task.result()
360
- if apsr is None or apsr.status != "ok":
361
- 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
+ }))
362
521
  return
363
- result = None
522
+
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
529
+
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
+ )
574
+
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...")}))
576
+
577
+ # Await blueprint execution with timeout protection
578
+ apsr = await asyncio.wait_for(task, timeout=BLUEPRINT_API_EXECUTION_TIMEOUT_SECONDS)
579
+
580
+ await queue.put(await format_event("status", {"status": "processing", "msg": "Processing blueprint result..."}))
581
+
582
+ if not apsr or apsr.status != "ok":
583
+ raise RuntimeError("Blueprint execution failed.")
584
+
364
585
  if apsr.payload and apsr.payload.result:
365
- result = apsr.payload.result.get("result")
366
- update_job(job_id, {
367
- "status": "complete",
368
- "result": serialize_result(result)
369
- })
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
+ }))
604
+
370
605
  except Exception as e:
371
- update_job(job_id, {"status": "error"})
372
- raise e
373
-
374
- app_response = await app_runner.init_session(InitSessionRequestPayload(
375
- cookies=dict(request.cookies),
376
- headers=dict(request.headers),
377
- proposedSessionId=None
378
- ))
379
-
380
- if not app_response or not app_response.payload:
381
- raise HTTPException(status_code=500, detail="Cannot initialize session.")
382
- session_id = app_response.payload.sessionId
383
- is_session_ok = await app_runner.check_session(session_id)
384
- if not is_session_ok:
385
- raise HTTPException(status_code=500, detail="Cannot initialize session.")
386
-
387
- loop = asyncio.get_running_loop()
388
- task = loop.create_task(app_runner.handle_event(
389
- session_id, WriterEvent(
390
- type="wf-builtin-run",
391
- isSafe=True,
392
- handler=f"$runWorkflow_{workflow_key}",
393
- payload=await _get_payload_as_json(request)
394
- )))
395
-
396
- job_id = app.state.job_vault.generate_job_id()
397
- app.state.job_vault.set(job_id, {
398
- "id": job_id,
399
- "status": "in progress",
400
- "created_at": int(time.time())
401
- })
402
- task.add_done_callback(lambda t: job_done_callback(t, job_id))
403
- return {
404
- "id": job_id,
405
- "token": crypto.get_hash(f"get_job_{job_id}")
406
- }
407
-
408
- @app.get("/api/job/{job_id}")
409
- async def get_workflow_job(job_id: str, request: Request, response: Response):
410
- if not enable_jobs_api:
411
- raise HTTPException(status_code=404)
412
-
413
- crypto.verify_message_authorization_signature(f"get_job_{job_id}", request)
414
- job = app.state.job_vault.get(job_id)
415
-
416
- if not job:
417
- return JSONResponse(status_code=404, content={
418
- "id": job_id,
419
- "status": "not found"
420
- })
421
-
422
- status_code = 200
423
- if job.get("status") == "error":
424
- status_code = 400
425
-
426
- return JSONResponse(status_code=status_code, content=job)
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")
614
+
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
+ )
427
655
 
428
656
  # Streaming
429
657
 
430
- async def _stream_session_init(websocket: WebSocket):
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)
431
664
 
665
+ async def _stream_session_init(websocket: WebSocket):
432
666
  """
433
667
  Waits for the client to provide a session id to initialise the stream.
434
668
  Returns the session id received.
@@ -439,8 +673,7 @@ def get_asgi_app(
439
673
  req_message_raw = await websocket.receive_json()
440
674
 
441
675
  try:
442
- req_message = WriterWebsocketIncoming.model_validate(
443
- req_message_raw)
676
+ req_message = WriterWebsocketIncoming.model_validate(req_message_raw)
444
677
  except ValidationError:
445
678
  logging.error("Incorrect incoming request.")
446
679
  return
@@ -450,20 +683,16 @@ def get_asgi_app(
450
683
  return session_id
451
684
 
452
685
  async def _stream_incoming_requests(websocket: WebSocket, session_id: str):
453
-
454
686
  """
455
- Handles incoming requests from client.
687
+ Handles incoming requests from client.
456
688
  """
457
689
 
458
- pending_tasks: Set[asyncio.Task] = set()
459
-
460
690
  try:
461
691
  while True:
462
692
  req_message_raw = await websocket.receive_json()
463
693
 
464
694
  try:
465
- req_message = WriterWebsocketIncoming.model_validate(
466
- req_message_raw)
695
+ req_message = WriterWebsocketIncoming.model_validate(req_message_raw)
467
696
  except ValidationError:
468
697
  logging.error("Incorrect incoming request.")
469
698
  break
@@ -476,183 +705,205 @@ def get_asgi_app(
476
705
 
477
706
  if req_message.type == "event":
478
707
  new_task = asyncio.create_task(
479
- _handle_incoming_event(websocket, session_id, req_message))
708
+ _handle_incoming_event(websocket, session_id, req_message)
709
+ )
480
710
  elif req_message.type == "keepAlive":
481
711
  new_task = asyncio.create_task(
482
- _handle_keep_alive_message(websocket, session_id, req_message))
712
+ _handle_keep_alive_message(websocket, session_id, req_message)
713
+ )
483
714
  elif req_message.type == "stateEnquiry":
484
715
  new_task = asyncio.create_task(
485
- _handle_state_enquiry_message(websocket, session_id, req_message))
716
+ _handle_state_enquiry_message(websocket, session_id, req_message)
717
+ )
486
718
  elif serve_mode == "edit" and req_message.type == "hashRequest":
487
719
  new_task = asyncio.create_task(
488
- _handle_hash_request(websocket, session_id, req_message))
720
+ _handle_hash_request(websocket, session_id, req_message)
721
+ )
489
722
  elif serve_mode == "edit":
490
723
  new_task = asyncio.create_task(
491
- _handle_incoming_edit_message(websocket, session_id, req_message))
724
+ _handle_incoming_edit_message(websocket, session_id, req_message)
725
+ )
492
726
 
493
727
  if new_task:
494
728
  pending_tasks.add(new_task)
495
729
  new_task.add_done_callback(pending_tasks.discard)
496
730
  except WebSocketDisconnect:
497
- pass
731
+ return
498
732
  except asyncio.CancelledError:
499
733
  raise
500
- finally:
501
- # Cancel pending tasks
502
-
503
- for pending_task in pending_tasks.copy():
504
- pending_task.cancel()
505
- try:
506
- await pending_task
507
- except asyncio.CancelledError:
508
- pass
509
734
 
510
- async def _handle_incoming_event(websocket: WebSocket, session_id: str, req_message: WriterWebsocketIncoming):
735
+ async def _handle_incoming_event(
736
+ websocket: WebSocket, session_id: str, req_message: WriterWebsocketIncoming
737
+ ):
511
738
  response = WriterWebsocketOutgoing(
512
739
  messageType=f"{req_message.type}Response",
513
740
  trackingId=req_message.trackingId,
514
- payload=None
741
+ payload=None,
515
742
  )
516
743
 
517
- # Allows for global events if in edit mode (such as "Run workflow" for previewing a workflow)
744
+ # Allows for global events if in edit mode (such as "Run blueprint" for previewing a blueprint)
518
745
 
519
746
  is_safe = serve_mode == "edit"
520
747
  res_payload: Optional[Dict[str, Any]] = None
521
748
  apsr: Optional[AppProcessServerResponse] = None
522
749
  apsr = await app_runner.handle_event(
523
- session_id, WriterEvent(
750
+ session_id,
751
+ WriterEvent(
524
752
  type=req_message.payload.get("type"),
525
753
  handler=req_message.payload.get("handler"),
526
754
  isSafe=is_safe,
527
755
  instancePath=req_message.payload.get("instancePath"),
528
- payload=req_message.payload.get("payload")
529
- ))
756
+ payload=req_message.payload.get("payload"),
757
+ ),
758
+ )
530
759
  if apsr is not None and apsr.payload is not None:
531
- res_payload = typing.cast(
532
- EventResponsePayload, apsr.payload).model_dump()
760
+ res_payload = typing.cast(EventResponsePayload, apsr.payload).model_dump()
533
761
  if res_payload is not None:
534
762
  response.payload = res_payload
535
- await websocket.send_json(response.model_dump())
763
+ await _send_json_or_queue(session_id, response.model_dump(), websocket)
536
764
 
537
- async def _handle_incoming_edit_message(websocket: WebSocket, session_id: str, req_message: WriterWebsocketIncoming):
765
+ async def _handle_incoming_edit_message(
766
+ websocket: WebSocket, session_id: str, req_message: WriterWebsocketIncoming
767
+ ):
538
768
  response = WriterWebsocketOutgoing(
539
769
  messageType=f"{req_message.type}Response",
540
770
  trackingId=req_message.trackingId,
541
- payload=None
771
+ payload=None,
542
772
  )
543
773
  if req_message.type == "componentUpdate":
544
774
  await app_runner.update_components(
545
- session_id, ComponentUpdateRequestPayload(
546
- components=req_message.payload["components"]
547
- ))
775
+ session_id,
776
+ ComponentUpdateRequestPayload(components=req_message.payload["components"]),
777
+ )
778
+ await app_runner.queue_announcement_async(
779
+ "componentUpdate", req_message.payload["components"], session_id
780
+ )
781
+ elif req_message.type == "collaborationPing":
782
+ await app_runner.queue_announcement_async(
783
+ "collaborationUpdate", req_message.payload, exclude_session_id=session_id
784
+ )
548
785
  elif req_message.type == "codeSaveRequest":
549
786
  app_runner.save_code(
550
- session_id, req_message.payload["code"], req_message.payload["path"])
787
+ session_id, req_message.payload["code"], req_message.payload["path"]
788
+ )
551
789
  elif req_message.type == "codeUpdate":
552
790
  app_runner.update_code(session_id, req_message.payload["code"])
553
791
  elif req_message.type == "loadSourceFile":
554
- path = os.path.join(*req_message.payload['path'])
792
+ path = os.path.join(*req_message.payload["path"])
555
793
  try:
556
- response.payload = { "content": app_runner.load_persisted_script(path) }
794
+ response.payload = {"content": app_runner.load_persisted_script(path)}
557
795
  except FileNotFoundError as error:
558
796
  logging.warning(f"could not load script at {path}", error)
559
797
  response.payload = {"error": str(error)}
560
- elif req_message.type == "createSourceFile":
561
- path = os.path.join(*req_message.payload['path'])
798
+ elif req_message.type == "createSourceFile":
799
+ path = os.path.join(*req_message.payload["path"])
562
800
  try:
563
801
  app_runner.create_persisted_script(path)
564
802
  except Exception as error:
565
803
  response.payload = {"error": str(error)}
566
- elif req_message.type == "deleteSourceFile":
567
- path = os.path.join(*req_message.payload['path'])
804
+ elif req_message.type == "deleteSourceFile":
805
+ path = os.path.join(*req_message.payload["path"])
568
806
  try:
569
807
  app_runner.delete_persisted_script(path)
570
808
  except Exception as error:
571
809
  response.payload = {"error": str(error)}
572
- elif req_message.type == "renameSourceFile":
573
- from_path = os.path.join(*req_message.payload['from'])
574
- to_path = os.path.join(*req_message.payload['to'])
810
+ elif req_message.type == "renameSourceFile":
811
+ from_path = os.path.join(*req_message.payload["from"])
812
+ to_path = os.path.join(*req_message.payload["to"])
575
813
  try:
576
814
  app_runner.rename_persisted_script(from_path, to_path)
577
815
  except Exception as error:
578
816
  response.payload = {"error": str(error)}
817
+ elif req_message.type == "listResources":
818
+ res = await app_runner.list_resources(session_id, req_message.payload["resource_type"])
819
+ response.payload = res.payload
820
+ elif req_message.type == "uploadSourceFile":
821
+ path = os.path.join(*req_message.payload["path"])
579
822
 
580
- await websocket.send_json(response.model_dump())
823
+ try:
824
+ content = base64.b64decode(req_message.payload["content"])
825
+ app_runner.create_persisted_script(path, content)
826
+ response.payload = {"sourceFiles": app_runner.source_files}
827
+ except Exception as error:
828
+ response.payload = {"error": str(error)}
829
+ elif req_message.type == "writerVaultUpdate":
830
+ await app_runner.writer_vault_refresh(session_id)
581
831
 
582
- async def _handle_keep_alive_message(websocket: WebSocket, session_id: str, req_message: WriterWebsocketIncoming):
832
+ await _send_json_or_queue(session_id, response.model_dump(), websocket)
833
+
834
+ async def _handle_keep_alive_message(
835
+ websocket: WebSocket, session_id: str, req_message: WriterWebsocketIncoming
836
+ ):
583
837
  response = WriterWebsocketOutgoing(
584
- messageType="keepAliveResponse",
585
- trackingId=req_message.trackingId,
586
- payload=None
838
+ messageType="keepAliveResponse", trackingId=req_message.trackingId, payload=None
587
839
  )
588
- await websocket.send_json(response.model_dump())
840
+ await _send_json_or_queue(session_id, response.model_dump(), websocket)
589
841
 
590
- async def _handle_state_enquiry_message(websocket: WebSocket, session_id: str, req_message: WriterWebsocketIncoming):
842
+ async def _handle_state_enquiry_message(
843
+ websocket: WebSocket, session_id: str, req_message: WriterWebsocketIncoming
844
+ ):
591
845
  response = WriterWebsocketOutgoing(
592
846
  messageType=f"{req_message.type}Response",
593
847
  trackingId=req_message.trackingId,
594
- payload=None
848
+ payload=None,
595
849
  )
596
850
  res_payload: Optional[Dict[str, Any]] = None
597
851
  apsr: Optional[AppProcessServerResponse] = None
598
852
  apsr = await app_runner.handle_state_enquiry(session_id)
599
853
  if apsr is not None and apsr.payload is not None:
600
- res_payload = typing.cast(
601
- StateEnquiryResponsePayload, apsr.payload).model_dump()
854
+ res_payload = typing.cast(StateEnquiryResponsePayload, apsr.payload).model_dump()
602
855
  if res_payload is not None:
603
856
  response.payload = res_payload
604
- await websocket.send_json(response.model_dump())
857
+ await _send_json_or_queue(session_id, response.model_dump(), websocket)
605
858
 
606
- async def _handle_hash_request(websocket: WebSocket, session_id: str, req_message: WriterWebsocketIncoming):
859
+ async def _handle_hash_request(
860
+ websocket: WebSocket, session_id: str, req_message: WriterWebsocketIncoming
861
+ ):
607
862
  response = WriterWebsocketOutgoing(
608
863
  messageType=f"{req_message.type}Response",
609
864
  trackingId=req_message.trackingId,
610
- payload=None
865
+ payload=None,
611
866
  )
612
867
  apsr: Optional[AppProcessServerResponse] = None
613
- apsr = await app_runner.handle_hash_request(session_id, HashRequestPayload(
614
- message=req_message.payload.get("message", "")
615
- ))
868
+ apsr = await app_runner.handle_hash_request(
869
+ session_id, HashRequestPayload(message=req_message.payload.get("message", ""))
870
+ )
616
871
  if apsr is not None and apsr.payload is not None:
617
- response.payload = typing.cast(
618
- HashRequestResponsePayload, apsr.payload).model_dump()
619
- await websocket.send_json(response.model_dump())
620
-
621
- async def _stream_outgoing_announcements(websocket: WebSocket):
872
+ response.payload = typing.cast(HashRequestResponsePayload, apsr.payload).model_dump()
873
+ await _send_json_or_queue(session_id, response.model_dump(), websocket)
622
874
 
875
+ async def _stream_outgoing_announcements(websocket: WebSocket, session_id: str):
623
876
  """
624
- Handles outgoing communications to client (announcements).
877
+ Handles outgoing communications to the client (announcements).
625
878
  """
626
879
 
627
- if app_runner.code_update_condition is None:
628
- raise ValueError("Code update condition not set.")
880
+ WEBSOCKET_CODE_UPDATE_CODE = 4001
881
+ session_queue: asyncio.Queue = asyncio.Queue()
882
+ app_runner.announcement_queues[session_id] = session_queue
629
883
 
630
- await app_runner.code_update_condition.acquire()
631
884
  try:
632
- await app_runner.code_update_condition.wait()
633
- finally:
634
- app_runner.code_update_condition.release()
635
-
636
- announcement = WriterWebsocketOutgoing(
637
- messageType="announcement",
638
- trackingId=-1,
639
- payload={
640
- "announce": "codeUpdate"
641
- }
642
- )
643
-
644
- if websocket.application_state == WebSocketState.DISCONNECTED:
645
- return
646
-
647
- try:
648
- await websocket.send_json(announcement.dict())
649
- except (WebSocketDisconnect):
885
+ while True:
886
+ announcement_data = await session_queue.get()
887
+ announcement = WriterWebsocketOutgoing(
888
+ messageType="announcement", trackingId=-1, payload=announcement_data
889
+ )
890
+ if websocket.application_state == WebSocketState.CONNECTED:
891
+ await websocket.send_json(announcement.dict())
892
+ if announcement_data.get("type") == "codeUpdate":
893
+ await websocket.close(WEBSOCKET_CODE_UPDATE_CODE, "Code update.")
894
+ return
895
+ except WebSocketDisconnect:
650
896
  pass
897
+ except asyncio.CancelledError:
898
+ raise
899
+ finally:
900
+ if app_runner.announcement_queues.get(session_id) is None:
901
+ return
902
+ del app_runner.announcement_queues[session_id]
651
903
 
652
904
  @app.websocket("/api/stream")
653
905
  async def stream(websocket: WebSocket):
654
-
655
- """ Initialises incoming and outgoing communications on the stream. """
906
+ """Initialises incoming and outgoing communications on the stream."""
656
907
 
657
908
  await websocket.accept()
658
909
 
@@ -670,10 +921,17 @@ def get_asgi_app(
670
921
  if not is_session_ok:
671
922
  await websocket.close(code=1008) # Invalid permissions
672
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
673
932
 
674
- task1 = asyncio.create_task(
675
- _stream_incoming_requests(websocket, session_id))
676
- task2 = asyncio.create_task(_stream_outgoing_announcements(websocket))
933
+ task1 = asyncio.create_task(_stream_incoming_requests(websocket, session_id))
934
+ task2 = asyncio.create_task(_stream_outgoing_announcements(websocket, session_id))
677
935
 
678
936
  try:
679
937
  await asyncio.wait((task1, task2), return_when=asyncio.FIRST_COMPLETED)
@@ -693,7 +951,9 @@ def get_asgi_app(
693
951
 
694
952
  user_app_extensions_path = pathlib.Path(user_app_path) / "extensions"
695
953
  if user_app_extensions_path.exists():
696
- app.mount("/extensions", StaticFiles(directory=str(user_app_extensions_path)), name="extensions")
954
+ app.mount(
955
+ "/extensions", StaticFiles(directory=str(user_app_extensions_path)), name="extensions"
956
+ )
697
957
 
698
958
  server_path = pathlib.Path(__file__)
699
959
  server_static_path = server_path.parent / "static"
@@ -715,14 +975,10 @@ def get_asgi_app(
715
975
  )
716
976
  )
717
977
 
718
- JobVault.register(RedisJobVault)
719
-
720
978
  # Return
721
979
  if enable_server_setup is True:
722
980
  _execute_server_setup_hook(user_app_path)
723
981
 
724
- app.state.job_vault = JobVault.create_vault()
725
-
726
982
  return app
727
983
 
728
984
 
@@ -750,17 +1006,29 @@ def print_route_message(run_name: str, port: int, host: str):
750
1006
  GREEN_TOKEN = "\033[92m"
751
1007
  END_TOKEN = "\033[0m"
752
1008
 
753
- print(f"{run_name} is available at:{END_TOKEN}{GREEN_TOKEN} http://{host}:{port}{END_TOKEN}", flush=True)
1009
+ print(
1010
+ f"{run_name} is available at:{END_TOKEN}{GREEN_TOKEN} http://{host}:{port}{END_TOKEN}",
1011
+ flush=True,
1012
+ )
1013
+
754
1014
 
755
1015
  def register_auth(
756
- auth: 'Auth',
1016
+ auth: "Auth",
757
1017
  callback: Optional[Callable[[Request, str, dict], None]] = None,
758
- unauthorized_action: Optional[Callable[[Request, 'Unauthorized'], Response]] = None
1018
+ unauthorized_action: Optional[Callable[[Request, "Unauthorized"], Response]] = None,
759
1019
  ):
760
1020
  auth.register(app, callback=callback, unauthorized_action=unauthorized_action)
761
1021
 
762
- def serve(app_path: str, mode: ServeMode, port: Optional[int], host, enable_remote_edit=False, enable_server_setup=False, enable_jobs_api=False):
763
- """ Initialises the web server. """
1022
+
1023
+ def serve(
1024
+ app_path: str,
1025
+ mode: ServeMode,
1026
+ port: Optional[int],
1027
+ host,
1028
+ enable_remote_edit=False,
1029
+ enable_server_setup=False
1030
+ ):
1031
+ """Initialises the web server."""
764
1032
 
765
1033
  print_init_message()
766
1034
 
@@ -773,17 +1041,23 @@ def serve(app_path: str, mode: ServeMode, port: Optional[int], host, enable_remo
773
1041
  when Writer Framework is launched with the run command.
774
1042
  """
775
1043
  if port is None:
776
- mode_allowed_ports = {
777
- 'run': (3005, 3099),
778
- 'edit': (4005, 4099)
779
- }
1044
+ mode_allowed_ports = {"run": (3005, 3099), "edit": (4005, 4099)}
780
1045
 
781
1046
  port = _next_localhost_available_port(mode_allowed_ports[mode])
782
1047
 
783
1048
  enable_server_setup = mode == "run" or enable_server_setup
784
- app = get_asgi_app(app_path, mode, enable_remote_edit, on_load=on_load, enable_server_setup=enable_server_setup, enable_jobs_api=enable_jobs_api)
1049
+ app = get_asgi_app(
1050
+ app_path,
1051
+ mode,
1052
+ enable_remote_edit,
1053
+ on_load=on_load,
1054
+ enable_server_setup=enable_server_setup
1055
+ )
785
1056
  log_level = "warning"
786
- uvicorn.run(app, host=host, port=port, log_level=log_level, ws_max_size=MAX_WEBSOCKET_MESSAGE_SIZE)
1057
+ uvicorn.run(
1058
+ app, host=host, port=port, log_level=log_level, ws_max_size=MAX_WEBSOCKET_MESSAGE_SIZE
1059
+ )
1060
+
787
1061
 
788
1062
  @asynccontextmanager
789
1063
  async def lifespan(app: FastAPI):
@@ -814,10 +1088,11 @@ async def lifespan(app: FastAPI):
814
1088
  async with _lifespan_invoke(writer_lifespans, app):
815
1089
  yield
816
1090
 
1091
+
817
1092
  def configure_webpage_metadata(
818
1093
  title: Union[str, Callable[[], str]] = "Writer Framework",
819
1094
  meta: Optional[Union[Dict[str, Any], Callable[[], Dict[str, Any]]]] = None,
820
- opengraph_tags: Optional[Union[Dict[str, Any], Callable[[], Dict[str, Any]]]] = None
1095
+ opengraph_tags: Optional[Union[Dict[str, Any], Callable[[], Dict[str, Any]]]] = None,
821
1096
  ):
822
1097
  """
823
1098
  Configures the page header for SEO and social networks from `server_setup` module.
@@ -912,6 +1187,7 @@ async def _lifespan_invoke(context: list, app: FastAPI):
912
1187
  else:
913
1188
  yield
914
1189
 
1190
+
915
1191
  def _fix_mimetype():
916
1192
  """
917
1193
  Fixes mimetypes for .js files. This is needed for the webserver to serve .js files correctly.
@@ -920,13 +1196,14 @@ def _fix_mimetype():
920
1196
  if js_mimetype[0] != "text/javascript":
921
1197
  mimetypes.add_type("text/javascript", ".js")
922
1198
 
1199
+
923
1200
  def _mount_server_static_path(app: FastAPI, server_static_path: pathlib.Path) -> None:
924
1201
  """
925
1202
  Unitarily declares the files and folders present in "/static" directory of source code.
926
1203
 
927
1204
  We avoid the general declaration as below. This declaration limit the ability of a developper to
928
1205
  declare it's own route.
929
-
1206
+
930
1207
  >>> asgi_app.mount("/", StaticFiles(directory=str(server_static_path), html=True), name="server_static")
931
1208
 
932
1209
  Writer Framework routes remain priority. A developer cannot come and overload them.
@@ -937,6 +1214,7 @@ def _mount_server_static_path(app: FastAPI, server_static_path: pathlib.Path) ->
937
1214
  if f.is_dir():
938
1215
  app.mount(f"/{f.name}", StaticFiles(directory=f), name=f"server_static_{f}")
939
1216
 
1217
+
940
1218
  def _mount_render_index_html(app: FastAPI, server_static_path: pathlib.Path):
941
1219
  """
942
1220
  Serves the main page with the title that has been configured.
@@ -945,29 +1223,45 @@ def _mount_render_index_html(app: FastAPI, server_static_path: pathlib.Path):
945
1223
  :param server_static_path:
946
1224
  :return:
947
1225
  """
1226
+
948
1227
  def _render_index_html():
949
- with io.open(server_static_path.joinpath('index.html'), 'r', encoding='utf-8') as f:
1228
+ with io.open(server_static_path.joinpath("index.html"), "r", encoding="utf-8") as f:
950
1229
  index_html = f.read()
951
1230
  if hasattr(app.state, "title"):
952
- index_html = index_html.replace("<title>Writer Framework</title>", f"<title>{html.escape(app.state.title)}</title>")
1231
+ index_html = index_html.replace(
1232
+ "<title>Writer Framework</title>",
1233
+ f"<title>{html.escape(app.state.title)}</title>",
1234
+ )
953
1235
 
954
1236
  if hasattr(app.state, "meta"):
955
1237
  meta = app.state.meta() if callable(app.state.meta) else app.state.meta
956
- meta_tags = "\n".join([f'<meta name="{k}" content="{html.escape(v)}">' for k, v in meta.items()])
1238
+ meta_tags = "\n".join(
1239
+ [f'<meta name="{k}" content="{html.escape(v)}">' for k, v in meta.items()]
1240
+ )
957
1241
  index_html = index_html.replace("<!-- {{ meta }} -->", meta_tags)
958
1242
  else:
959
1243
  index_html = index_html.replace("<!-- {{ meta }} -->", "")
960
1244
 
961
1245
  if hasattr(app.state, "opengraph_tags"):
962
- opengraph_tags = app.state.opengraph_tags() if callable(app.state.opengraph_tags) else app.state.opengraph_tags
963
- opengraph_tags = "\n".join([f'<meta property="{k}" content="{html.escape(v)}">' for k, v in opengraph_tags.items()])
1246
+ opengraph_tags = (
1247
+ app.state.opengraph_tags()
1248
+ if callable(app.state.opengraph_tags)
1249
+ else app.state.opengraph_tags
1250
+ )
1251
+ opengraph_tags = "\n".join(
1252
+ [
1253
+ f'<meta property="{k}" content="{html.escape(v)}">'
1254
+ for k, v in opengraph_tags.items()
1255
+ ]
1256
+ )
964
1257
  index_html = index_html.replace("<!-- {{ opengraph_tags }} -->", opengraph_tags)
965
1258
  else:
966
1259
  index_html = index_html.replace("<!-- {{ opengraph_tags }} -->", "")
967
1260
 
968
1261
  return Response(content=index_html, media_type="text/html")
969
1262
 
970
- return app.get('/')(_render_index_html)
1263
+ return app.get("/")(_render_index_html)
1264
+
971
1265
 
972
1266
  def app_runner(asgi_app: WriterFastAPI) -> AppRunner:
973
1267
  return asgi_app.state.app_runner
@@ -986,7 +1280,7 @@ def wf_root_static_assets() -> List[pathlib.Path]:
986
1280
  all_static_assets: List[pathlib.Path] = []
987
1281
  server_path = pathlib.Path(__file__)
988
1282
  server_static_path = server_path.parent / "static"
989
- for f in server_static_path.glob('*'):
1283
+ for f in server_static_path.glob("*"):
990
1284
  all_static_assets.append(f)
991
1285
 
992
1286
  return all_static_assets
@@ -999,7 +1293,9 @@ def _execute_server_setup_hook(user_app_path: str) -> None:
999
1293
  """
1000
1294
  server_setup_path = os.path.join(user_app_path, "server_setup.py")
1001
1295
  if os.path.isfile(server_setup_path):
1002
- spec = cast(ModuleSpec, importlib.util.spec_from_file_location("server_setup", server_setup_path))
1296
+ spec = cast(
1297
+ ModuleSpec, importlib.util.spec_from_file_location("server_setup", server_setup_path)
1298
+ )
1003
1299
  module = importlib.util.module_from_spec(spec)
1004
1300
  spec.loader.exec_module(module) # type: ignore
1005
1301
 
@@ -1015,9 +1311,11 @@ def _next_localhost_available_port(port_range: Tuple[int, int]) -> int:
1015
1311
  for port in range(port_range[0], port_range[1]):
1016
1312
  sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
1017
1313
  sock.settimeout(1)
1018
- result = sock.connect_ex(('127.0.0.1', port))
1314
+ result = sock.connect_ex(("127.0.0.1", port))
1019
1315
  sock.close()
1020
1316
  if result != 0:
1021
1317
  return port
1022
1318
 
1023
- raise OSError(f"No free port found to start the server between {port_range[0]} and {port_range[1]} .")
1319
+ raise OSError(
1320
+ f"No free port found to start the server between {port_range[0]} and {port_range[1]} ."
1321
+ )