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.
- writer/__init__.py +1 -1
- writer/abstract.py +1 -1
- writer/{ai.py → ai/__init__.py} +867 -163
- writer/app_runner.py +596 -241
- writer/app_templates/default/.wf/components-blueprints_blueprint-0-0decp3w5erhvl0nw.jsonl +11 -0
- writer/app_templates/default/.wf/components-blueprints_root.jsonl +1 -0
- writer/app_templates/default/.wf/components-page-0-c0f99a9e-5004-4e75-a6c6-36f17490b134.jsonl +27 -0
- writer/app_templates/default/.wf/components-root.jsonl +1 -0
- writer/app_templates/default/.wf/components-workflows_root.jsonl +1 -0
- writer/app_templates/default/.wf/components-workflows_workflow-0-lfltcky7l1fsm6j2.jsonl +1 -0
- writer/app_templates/default/.wf/metadata.json +3 -0
- writer/app_templates/default/README.md +3 -0
- writer/app_templates/default/main.py +16 -0
- writer/app_templates/default/requirements.txt +1 -0
- writer/app_templates/default/static/README.md +8 -0
- writer/app_templates/default/static/agent_builder_demo.png +0 -0
- writer/app_templates/default/static/favicon.png +0 -0
- writer/app_templates/hello/.wf/components-blueprints_blueprint-0-t84xyhxau9ej3823.jsonl +18 -0
- writer/app_templates/hello/.wf/components-blueprints_root.jsonl +1 -0
- writer/app_templates/hello/.wf/components-page-0-c0f99a9e-5004-4e75-a6c6-36f17490b134.jsonl +15 -0
- writer/app_templates/hello/.wf/components-root.jsonl +1 -0
- writer/app_templates/hello/.wf/metadata.json +3 -0
- writer/app_templates/hello/main.py +16 -0
- writer/app_templates/hello/static/README.md +8 -0
- writer/app_templates/hello/static/favicon.png +0 -0
- writer/app_templates/hello/static/welcome.svg +40 -0
- writer/auth.py +7 -2
- writer/autogen.py +352 -0
- writer/blocks/__init__.py +51 -17
- writer/blocks/addtostatelist.py +10 -9
- writer/blocks/apitrigger.py +45 -0
- writer/blocks/base_block.py +332 -21
- writer/blocks/base_trigger.py +14 -0
- writer/blocks/calleventhandler.py +39 -35
- writer/blocks/changepage.py +48 -0
- writer/blocks/code.py +102 -0
- writer/blocks/crontrigger.py +49 -0
- writer/blocks/foreach.py +70 -53
- writer/blocks/httprequest.py +112 -99
- writer/blocks/ifelse.py +71 -0
- writer/blocks/logmessage.py +34 -39
- writer/blocks/parsejson.py +30 -29
- writer/blocks/returnvalue.py +7 -7
- writer/blocks/runblueprint.py +63 -0
- writer/blocks/setstate.py +43 -33
- writer/blocks/sharedblueprint.py +86 -0
- writer/blocks/uieventtrigger.py +49 -0
- writer/blocks/writeraddchatmessage.py +50 -12
- writer/blocks/writeraddtokg.py +38 -11
- writer/blocks/writeraskkg.py +123 -0
- writer/blocks/writerchat.py +80 -61
- writer/blocks/writerchatreply.py +279 -0
- writer/blocks/writerchatreplywithtoolconfig.py +393 -0
- writer/blocks/writerclassification.py +78 -39
- writer/blocks/writercompletion.py +49 -44
- writer/blocks/writerfileapi.py +85 -0
- writer/blocks/writerinitchat.py +24 -12
- writer/blocks/writerkeyvaluestorage.py +106 -0
- writer/blocks/writernocodeapp.py +35 -37
- writer/blocks/writerparsepdf.py +73 -0
- writer/blocks/writerstructuredoutput.py +105 -0
- writer/blocks/writertoolcalling.py +251 -0
- writer/blocks/writervision.py +141 -0
- writer/blocks/writerwebsearch.py +175 -0
- writer/blueprints.py +839 -0
- writer/command_line.py +52 -16
- writer/core.py +562 -290
- writer/core_ui.py +6 -2
- writer/evaluator.py +98 -46
- writer/journal.py +227 -0
- writer/keyvalue_storage.py +93 -0
- writer/logs.py +277 -0
- writer/serve.py +625 -327
- writer/ss_types.py +101 -12
- writer/static/assets/Arrow.dom-GBJpMYQS.js +1 -0
- writer/static/assets/BaseMarkdown-Wrvby5J8.js +1 -0
- writer/static/assets/BlueprintToolbar-BuXNRxWT.js +1 -0
- writer/static/assets/BlueprintToolbar-wpfX0jo_.css +1 -0
- writer/static/assets/BuilderApp-PTOI76jZ.js +8 -0
- writer/static/assets/BuilderApp-WimUfNZr.css +1 -0
- writer/static/assets/BuilderApplicationSelect-DXzy4e_h.js +7 -0
- writer/static/assets/BuilderApplicationSelect-XaM1D5fv.css +1 -0
- writer/static/assets/BuilderBlueprintLibraryPanel-Ckrhknlh.css +1 -0
- writer/static/assets/BuilderBlueprintLibraryPanel-DBDzhTlc.js +1 -0
- writer/static/assets/BuilderEmbeddedCodeEditor-B0bcjlhk.css +1 -0
- writer/static/assets/BuilderEmbeddedCodeEditor-Dn7eDICN.js +726 -0
- writer/static/assets/BuilderGraphSelect-C-LRsO8W.js +7 -0
- writer/static/assets/BuilderGraphSelect-D7B61d5s.css +1 -0
- writer/static/assets/BuilderInsertionLabel-BhyL9wgn.js +1 -0
- writer/static/assets/BuilderInsertionLabel-_YS5WPfq.css +1 -0
- writer/static/assets/BuilderInsertionOverlay-D2XS0ij9.css +1 -0
- writer/static/assets/BuilderInsertionOverlay-MkAIVruY.js +1 -0
- writer/static/assets/BuilderJournal-A0LcEwGI.js +7 -0
- writer/static/assets/BuilderJournal-DHv3Pvvm.css +1 -0
- writer/static/assets/BuilderModelSelect-CdSo_sih.js +7 -0
- writer/static/assets/BuilderModelSelect-Dc4IPLp2.css +1 -0
- writer/static/assets/BuilderSettings-BDwZBveu.js +16 -0
- writer/static/assets/BuilderSettings-lZkOXEYw.css +1 -0
- writer/static/assets/BuilderSettingsArtifactAPITriggerDetails-3O6jKBXD.js +4 -0
- writer/static/assets/BuilderSettingsArtifactAPITriggerDetails-DnX66iRg.css +1 -0
- writer/static/assets/BuilderSettingsDeploySharedBlueprint-BR_3ptsd.js +1 -0
- writer/static/assets/BuilderSettingsDeploySharedBlueprint-KJTl8gxP.css +1 -0
- writer/static/assets/BuilderSettingsHandlers-CBtEQFSo.js +1 -0
- writer/static/assets/BuilderSettingsHandlers-DJPeASfz.css +1 -0
- writer/static/assets/BuilderSidebarComponentTree-DLltgas5.js +1 -0
- writer/static/assets/BuilderSidebarComponentTree-DYu1F793.css +1 -0
- writer/static/assets/BuilderSidebarToolkit-CApZNTAq.js +7 -0
- writer/static/assets/BuilderSidebarToolkit-CwqbjRv8.css +1 -0
- writer/static/assets/BuilderTemplateEditor-CYSDeWgV.css +1 -0
- writer/static/assets/BuilderTemplateEditor-DnRDRcA0.js +87 -0
- writer/static/assets/BuilderVault-2vGoV0sx.js +1 -0
- writer/static/assets/BuilderVault-Cx6oQSES.css +1 -0
- writer/static/assets/ComponentRenderer-72hqvEvI.css +1 -0
- writer/static/assets/ComponentRenderer-D4Pj1i3s.js +1 -0
- writer/static/assets/SharedCopyClipboardButton-BipJKGtz.css +1 -0
- writer/static/assets/SharedCopyClipboardButton-DNI9kLe6.js +1 -0
- writer/static/assets/WdsCheckbox-DKvpPA4D.css +1 -0
- writer/static/assets/WdsCheckbox-edQcn1cf.js +1 -0
- writer/static/assets/WdsDropdownMenu-CzzPN9Wg.css +1 -0
- writer/static/assets/WdsDropdownMenu-DQnrRBNV.js +1 -0
- writer/static/assets/WdsFieldWrapper-Cmufx5Nj.js +1 -0
- writer/static/assets/WdsFieldWrapper-CsemOh8D.css +1 -0
- writer/static/assets/WdsTabs-DKj7BqI0.css +1 -0
- writer/static/assets/WdsTabs-DcfY_zn5.js +1 -0
- writer/static/assets/abap-D8nrxEjS.js +6 -0
- writer/static/assets/apex-BrXDlLUW.js +6 -0
- writer/static/assets/art-paper-D70v1WMA.svg +180 -0
- writer/static/assets/azcli-CElzELwZ.js +6 -0
- writer/static/assets/bat-CUsyEhik.js +6 -0
- writer/static/assets/bicep-BtxyJn6H.js +7 -0
- writer/static/assets/cameligo-ClBCoF8h.js +6 -0
- writer/static/assets/clojure-B9TqLHAk.js +6 -0
- writer/static/assets/codicon-BA2IlpFX.ttf +0 -0
- writer/static/assets/coffee-DYsfeylR.js +6 -0
- writer/static/assets/cpp-VVGvvgir.js +6 -0
- writer/static/assets/csharp-Z6z2stHy.js +6 -0
- writer/static/assets/csp-DgZoLDI1.js +6 -0
- writer/static/assets/css-KqQ96-gC.js +8 -0
- writer/static/assets/css.worker-DvNUQFd1.js +84 -0
- writer/static/assets/cssMode-BYq4oZGq.js +9 -0
- writer/static/assets/cypher-CYoSlgTu.js +6 -0
- writer/static/assets/dart-BGDl7St1.js +6 -0
- writer/static/assets/dockerfile-CuCtxA7T.js +6 -0
- writer/static/assets/ecl-BCTFAUpS.js +6 -0
- writer/static/assets/editor.worker-BVwmgLrR.js +11 -0
- writer/static/assets/elixir-C7hRTYZ9.js +6 -0
- writer/static/assets/flow9-Bi_qi707.js +6 -0
- writer/static/assets/freemarker2-CnNourkO.js +8 -0
- writer/static/assets/fsharp-CxaaEKKi.js +6 -0
- writer/static/assets/go-DUImKuGY.js +6 -0
- writer/static/assets/graphql-D5sGVkLV.js +6 -0
- writer/static/assets/handlebars-Bm22yapJ.js +6 -0
- writer/static/assets/hcl-zD_CCkZ1.js +6 -0
- writer/static/assets/html-CAKAfoZF.js +6 -0
- writer/static/assets/html.worker-BJMlcbMU.js +458 -0
- writer/static/assets/htmlMode-BGZ97n-V.js +9 -0
- writer/static/assets/index-5u5REPT4.js +16 -0
- writer/static/assets/index-BKNuk68o.css +1 -0
- writer/static/assets/index-BQNXU3IR.js +17 -0
- writer/static/assets/index-BQr1pfrb.js +1 -0
- writer/static/assets/index-DHXAd5Yn.js +4 -0
- writer/static/assets/index-Zki-pfO-.js +8525 -0
- writer/static/assets/index.esm-B1ZQtduY.js +17 -0
- writer/static/assets/ini-8kKHd4ZL.js +6 -0
- writer/static/assets/java-De1axCfe.js +6 -0
- writer/static/assets/javascript-X1f02eyK.js +6 -0
- writer/static/assets/json.worker-BwvX8PuZ.js +42 -0
- writer/static/assets/jsonMode-hT0bNgT8.js +11 -0
- writer/static/assets/julia-D3ApGBxz.js +6 -0
- writer/static/assets/kotlin-GbSrCElU.js +6 -0
- writer/static/assets/less-DNUaDNdz.js +7 -0
- writer/static/assets/lexon-Bg9QKxBu.js +6 -0
- writer/static/assets/liquid-KmCCiJw2.js +6 -0
- writer/static/assets/lua-Crkvc3mc.js +6 -0
- writer/static/assets/m3-DsrzVyM1.js +6 -0
- writer/static/assets/mapbox-gl-C0cyFYYW.js +2329 -0
- writer/static/assets/markdown-CY5IOZuu.js +6 -0
- writer/static/assets/marked.esm-273vDTCT.js +45 -0
- writer/static/assets/mdx-DtRFauUw.js +6 -0
- writer/static/assets/mips-BE8RsGBA.js +6 -0
- writer/static/assets/msdax-N5ajIiFQ.js +6 -0
- writer/static/assets/mysql-DRxbB97D.js +6 -0
- writer/static/assets/objective-c-BHUZy23s.js +6 -0
- writer/static/assets/pascal-BemVzBTY.js +6 -0
- writer/static/assets/pascaligo-BACCcnx_.js +6 -0
- writer/static/assets/pdf-B6-yWJ-Y.js +12 -0
- writer/static/assets/pdf.worker.min-CyUfim15.mjs +21 -0
- writer/static/assets/perl-CuU66Ptk.js +6 -0
- writer/static/assets/pgsql-CQ6TMH2r.js +6 -0
- writer/static/assets/php-BvyzZa65.js +6 -0
- writer/static/assets/pla-DrIuu9u1.js +6 -0
- writer/static/assets/plotly.min-DutuuatZ.js +4030 -0
- writer/static/assets/poppins-latin-300-italic-4WBEAciR.woff +0 -0
- writer/static/assets/poppins-latin-300-italic-EWCPeN2Y.woff2 +0 -0
- writer/static/assets/poppins-latin-300-normal-DCNuMXUj.woff +0 -0
- writer/static/assets/poppins-latin-300-normal-Dku2WoCh.woff2 +0 -0
- writer/static/assets/poppins-latin-400-italic-B4GYq972.woff2 +0 -0
- writer/static/assets/poppins-latin-400-italic-BPejoDS-.woff +0 -0
- writer/static/assets/poppins-latin-400-normal-BOb3E3N0.woff +0 -0
- writer/static/assets/poppins-latin-400-normal-cpxAROuN.woff2 +0 -0
- writer/static/assets/poppins-latin-500-italic-Ce_qjtl5.woff +0 -0
- writer/static/assets/poppins-latin-500-italic-o28Otv0U.woff2 +0 -0
- writer/static/assets/poppins-latin-500-normal-C8OXljZJ.woff2 +0 -0
- writer/static/assets/poppins-latin-500-normal-DGXqpDMm.woff +0 -0
- writer/static/assets/poppins-latin-600-italic-BhOZippK.woff +0 -0
- writer/static/assets/poppins-latin-600-italic-CZ4wqKBi.woff2 +0 -0
- writer/static/assets/poppins-latin-600-normal-BJdTmd5m.woff +0 -0
- writer/static/assets/poppins-latin-600-normal-zEkxB9Mr.woff2 +0 -0
- writer/static/assets/poppins-latin-700-italic-CW91C-LJ.woff +0 -0
- writer/static/assets/poppins-latin-700-italic-RKf6esGj.woff2 +0 -0
- writer/static/assets/poppins-latin-700-normal-BVuQR_eA.woff +0 -0
- writer/static/assets/poppins-latin-700-normal-Qrb0O0WB.woff2 +0 -0
- writer/static/assets/poppins-latin-ext-300-italic-CBzyU4Pf.woff +0 -0
- writer/static/assets/poppins-latin-ext-300-italic-DdDvTq5-.woff2 +0 -0
- writer/static/assets/poppins-latin-ext-300-normal-7Zg2msWE.woff2 +0 -0
- writer/static/assets/poppins-latin-ext-300-normal-C9p7gvmA.woff +0 -0
- writer/static/assets/poppins-latin-ext-400-italic-BiCGV3eO.woff2 +0 -0
- writer/static/assets/poppins-latin-ext-400-italic-gsPYOGqV.woff +0 -0
- writer/static/assets/poppins-latin-ext-400-normal-CIpeJEZw.woff2 +0 -0
- writer/static/assets/poppins-latin-ext-400-normal-Ce_uWq1Z.woff +0 -0
- writer/static/assets/poppins-latin-ext-500-italic-CwrTHwbn.woff2 +0 -0
- writer/static/assets/poppins-latin-ext-500-italic-jdc8Bv4M.woff +0 -0
- writer/static/assets/poppins-latin-ext-500-normal-Bl1-S02S.woff +0 -0
- writer/static/assets/poppins-latin-ext-500-normal-H4Q0z8D2.woff2 +0 -0
- writer/static/assets/poppins-latin-ext-600-italic-BqeDa496.woff2 +0 -0
- writer/static/assets/poppins-latin-ext-600-italic-C7MQPb_A.woff +0 -0
- writer/static/assets/poppins-latin-ext-600-normal-Cn4C8475.woff2 +0 -0
- writer/static/assets/poppins-latin-ext-600-normal-DB6FJURc.woff +0 -0
- writer/static/assets/poppins-latin-ext-700-italic-BAdhB_WS.woff2 +0 -0
- writer/static/assets/poppins-latin-ext-700-italic-WKTwQMp8.woff +0 -0
- writer/static/assets/poppins-latin-ext-700-normal-CE2WFKmF.woff +0 -0
- writer/static/assets/poppins-latin-ext-700-normal-DDaViAzG.woff2 +0 -0
- writer/static/assets/postiats-BR_hrfni.js +6 -0
- writer/static/assets/powerquery-CKDUeRmd.js +6 -0
- writer/static/assets/powershell-Dsa4rhA_.js +6 -0
- writer/static/assets/protobuf-CGsvhooB.js +7 -0
- writer/static/assets/pug-D2p3uOX2.js +6 -0
- writer/static/assets/python-DVhxg746.js +6 -0
- writer/static/assets/qsharp-B7F3HtPF.js +6 -0
- writer/static/assets/r-3aLoi2fs.js +6 -0
- writer/static/assets/razor-DR5Ns_BC.js +6 -0
- writer/static/assets/redis-jqFeRM5s.js +6 -0
- writer/static/assets/redshift-BriwQgXR.js +6 -0
- writer/static/assets/restructuredtext-hbBFZ0w9.js +6 -0
- writer/static/assets/ruby-ByThyB2Q.js +6 -0
- writer/static/assets/rust-DIEZMp5R.js +6 -0
- writer/static/assets/sb-C6Gjjw_x.js +6 -0
- writer/static/assets/scala-DZNw3jJB.js +6 -0
- writer/static/assets/scheme-55eqh71t.js +6 -0
- writer/static/assets/scss-D-OVkc4F.js +8 -0
- writer/static/assets/serialization-DJC7NP0N.js +20 -0
- writer/static/assets/shell-DSpi8_qN.js +6 -0
- writer/static/assets/solidity-BHddiNFS.js +6 -0
- writer/static/assets/sophia-D6taVZFb.js +6 -0
- writer/static/assets/sparql-LA0C7mUc.js +6 -0
- writer/static/assets/sql-C3-3IcFM.js +6 -0
- writer/static/assets/st-C4g7059C.js +6 -0
- writer/static/assets/swift-DNI1vH3h.js +8 -0
- writer/static/assets/systemverilog-DL_FVbcQ.js +6 -0
- writer/static/assets/tcl-DVJXmIwd.js +6 -0
- writer/static/assets/ts.worker-CwG1rUES.js +37021 -0
- writer/static/assets/tsMode-BNUEZzir.js +16 -0
- writer/static/assets/twig-BVWDLtw5.js +6 -0
- writer/static/assets/typescript-CRVt7Hx0.js +6 -0
- writer/static/assets/useBlueprintRun-C00bCxh-.js +1 -0
- writer/static/assets/useKeyValueEditor-nDmI7cTJ.js +1 -0
- writer/static/assets/useListResources-DLkZhRSJ.js +1 -0
- writer/static/assets/vb-Btz91-7U.js +6 -0
- writer/static/assets/vega-embed.module-SNP5iNdJ.js +201 -0
- writer/static/assets/wgsl-D8V_buCG.js +303 -0
- writer/static/assets/xml-C_6-t1tb.js +6 -0
- writer/static/assets/yaml-DIw8G7jk.js +6 -0
- writer/static/components/annotatedtext.svg +4 -0
- writer/static/components/avatar.svg +4 -0
- writer/static/components/blueprints_addtostatelist.svg +4 -0
- writer/static/components/blueprints_apitrigger.svg +4 -0
- writer/static/components/blueprints_calleventhandler.svg +9 -0
- writer/static/components/blueprints_category_Logic.svg +4 -0
- writer/static/components/blueprints_category_Other.svg +4 -0
- writer/static/components/blueprints_category_Triggers.svg +4 -0
- writer/static/components/blueprints_category_Writer.svg +25 -0
- writer/static/components/blueprints_code.svg +9 -0
- writer/static/components/blueprints_crontrigger.svg +6 -0
- writer/static/components/blueprints_foreach.svg +4 -0
- writer/static/components/blueprints_httprequest.svg +11 -0
- writer/static/components/blueprints_logmessage.svg +11 -0
- writer/static/components/blueprints_parsejson.svg +4 -0
- writer/static/components/blueprints_returnvalue.svg +4 -0
- writer/static/components/blueprints_runblueprint.svg +4 -0
- writer/static/components/blueprints_setstate.svg +4 -0
- writer/static/components/blueprints_uieventtrigger.svg +4 -0
- writer/static/components/blueprints_writeraddchatmessage.svg +19 -0
- writer/static/components/blueprints_writeraddtokg.svg +19 -0
- writer/static/components/blueprints_writerchat.svg +11 -0
- writer/static/components/blueprints_writerchatreply.svg +19 -0
- writer/static/components/blueprints_writerclassification.svg +24 -0
- writer/static/components/blueprints_writercompletion.svg +14 -0
- writer/static/components/blueprints_writerinitchat.svg +11 -0
- writer/static/components/blueprints_writernocodeapp.svg +14 -0
- writer/static/components/button.svg +4 -0
- writer/static/components/category_Content.svg +4 -0
- writer/static/components/category_Embed.svg +4 -0
- writer/static/components/category_Input.svg +5 -0
- writer/static/components/category_Layout.svg +9 -0
- writer/static/components/category_Other.svg +4 -0
- writer/static/components/chatbot.svg +4 -0
- writer/static/components/checkboxinput.svg +4 -0
- writer/static/components/colorinput.svg +11 -0
- writer/static/components/column.svg +4 -0
- writer/static/components/columns.svg +4 -0
- writer/static/components/dataframe.svg +4 -0
- writer/static/components/dateinput.svg +4 -0
- writer/static/components/dropdowninput.svg +5 -0
- writer/static/components/fileinput.svg +4 -0
- writer/static/components/googlemaps.svg +4 -0
- writer/static/components/header.svg +4 -0
- writer/static/components/heading.svg +9 -0
- writer/static/components/horizontalstack.svg +4 -0
- writer/static/components/html.svg +9 -0
- writer/static/components/icon.svg +4 -0
- writer/static/components/iframe.svg +4 -0
- writer/static/components/image.svg +11 -0
- writer/static/components/jsonviewer.svg +4 -0
- writer/static/components/link.svg +12 -0
- writer/static/components/mapbox.svg +4 -0
- writer/static/components/message.svg +4 -0
- writer/static/components/metric.svg +4 -0
- writer/static/components/multiselectinput.svg +4 -0
- writer/static/components/numberinput.svg +4 -0
- writer/static/components/page.svg +50 -0
- writer/static/components/pagination.svg +4 -0
- writer/static/components/pdf.svg +4 -0
- writer/static/components/plotlygraph.svg +7 -0
- writer/static/components/progressbar.svg +5 -0
- writer/static/components/radioinput.svg +4 -0
- writer/static/components/rangeinput.svg +4 -0
- writer/static/components/ratinginput.svg +4 -0
- writer/static/components/repeater.svg +4 -0
- writer/static/components/reuse.svg +4 -0
- writer/static/components/section.svg +4 -0
- writer/static/components/selectinput.svg +5 -0
- writer/static/components/separator.svg +4 -0
- writer/static/components/sidebar.svg +4 -0
- writer/static/components/sliderinput.svg +4 -0
- writer/static/components/step.svg +4 -0
- writer/static/components/steps.svg +4 -0
- writer/static/components/switchinput.svg +4 -0
- writer/static/components/tab.svg +4 -0
- writer/static/components/tabs.svg +4 -0
- writer/static/components/tags.svg +11 -0
- writer/static/components/text.svg +4 -0
- writer/static/components/textareainput.svg +11 -0
- writer/static/components/textinput.svg +4 -0
- writer/static/components/timeinput.svg +4 -0
- writer/static/components/timer.svg +4 -0
- writer/static/components/vegalitechart.svg +7 -0
- writer/static/components/videoplayer.svg +11 -0
- writer/static/components/webcamcapture.svg +4 -0
- writer/static/favicon.png +0 -0
- writer/static/index.html +84 -0
- writer/static/status/cancelled.svg +5 -0
- writer/static/status/error.svg +5 -0
- writer/static/status/skipped.svg +4 -0
- writer/static/status/stopped.svg +4 -0
- writer/static/status/success.svg +4 -0
- writer/sync.py +431 -0
- writer/ui.py +2268 -0
- writer/vault.py +48 -0
- writer/wf_project.py +90 -66
- writer-1.25.1rc1.dist-info/METADATA +92 -0
- writer-1.25.1rc1.dist-info/RECORD +382 -0
- {writer-0.8.3rc4.dist-info → writer-1.25.1rc1.dist-info}/WHEEL +1 -1
- writer/blocks/runworkflow.py +0 -59
- writer/workflows.py +0 -183
- writer-0.8.3rc4.dist-info/METADATA +0 -117
- writer-0.8.3rc4.dist-info/RECORD +0 -44
- {writer-0.8.3rc4.dist-info → writer-1.25.1rc1.dist-info}/entry_points.txt +0 -0
- {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
|
|
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
|
|
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
|
-
|
|
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]]]
|
|
145
|
-
opengraph_tags: Union[
|
|
146
|
-
|
|
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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
|
195
|
-
|
|
196
|
-
|
|
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 = [
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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.
|
|
275
|
-
async def
|
|
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
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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: {
|
|
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
|
|
351
|
-
|
|
352
|
-
|
|
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
|
-
|
|
498
|
+
# --- The main worker logic that produces events ---
|
|
499
|
+
|
|
500
|
+
async def event_logic(queue: asyncio.Queue):
|
|
358
501
|
try:
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
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
|
-
|
|
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
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
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
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
|
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,
|
|
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
|
|
763
|
+
await _send_json_or_queue(session_id, response.model_dump(), websocket)
|
|
536
764
|
|
|
537
|
-
async def _handle_incoming_edit_message(
|
|
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,
|
|
546
|
-
|
|
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[
|
|
792
|
+
path = os.path.join(*req_message.payload["path"])
|
|
555
793
|
try:
|
|
556
|
-
response.payload = {
|
|
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
|
|
561
|
-
path = os.path.join(*req_message.payload[
|
|
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
|
|
567
|
-
path = os.path.join(*req_message.payload[
|
|
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
|
|
573
|
-
from_path = os.path.join(*req_message.payload[
|
|
574
|
-
to_path = os.path.join(*req_message.payload[
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
840
|
+
await _send_json_or_queue(session_id, response.model_dump(), websocket)
|
|
589
841
|
|
|
590
|
-
async def _handle_state_enquiry_message(
|
|
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
|
|
857
|
+
await _send_json_or_queue(session_id, response.model_dump(), websocket)
|
|
605
858
|
|
|
606
|
-
async def _handle_hash_request(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
628
|
-
|
|
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
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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:
|
|
1016
|
+
auth: "Auth",
|
|
757
1017
|
callback: Optional[Callable[[Request, str, dict], None]] = None,
|
|
758
|
-
unauthorized_action: Optional[Callable[[Request,
|
|
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
|
-
|
|
763
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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 =
|
|
963
|
-
|
|
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(
|
|
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(
|
|
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((
|
|
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(
|
|
1319
|
+
raise OSError(
|
|
1320
|
+
f"No free port found to start the server between {port_range[0]} and {port_range[1]} ."
|
|
1321
|
+
)
|