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