writer 0.8.3rc21__py3-none-any.whl → 1.25.1rc1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (265) hide show
  1. writer/ai/__init__.py +277 -28
  2. writer/app_runner.py +236 -32
  3. writer/app_templates/default/.wf/components-blueprints_blueprint-0-0decp3w5erhvl0nw.jsonl +11 -0
  4. writer/app_templates/default/.wf/components-page-0-c0f99a9e-5004-4e75-a6c6-36f17490b134.jsonl +22 -10
  5. writer/app_templates/default/.wf/components-root.jsonl +1 -1
  6. writer/app_templates/default/.wf/components-workflows_root.jsonl +1 -0
  7. writer/app_templates/default/.wf/components-workflows_workflow-0-lfltcky7l1fsm6j2.jsonl +1 -0
  8. writer/app_templates/default/.wf/metadata.json +1 -1
  9. writer/app_templates/default/README.md +3 -0
  10. writer/app_templates/default/main.py +8 -5
  11. writer/app_templates/default/requirements.txt +1 -0
  12. writer/app_templates/default/static/agent_builder_demo.png +0 -0
  13. writer/app_templates/hello/main.py +3 -0
  14. writer/auth.py +7 -2
  15. writer/autogen.py +26 -9
  16. writer/blocks/__init__.py +21 -1
  17. writer/blocks/addtostatelist.py +5 -4
  18. writer/blocks/apitrigger.py +45 -0
  19. writer/blocks/base_block.py +134 -14
  20. writer/blocks/changepage.py +1 -1
  21. writer/blocks/code.py +27 -11
  22. writer/blocks/crontrigger.py +49 -0
  23. writer/blocks/foreach.py +3 -3
  24. writer/blocks/httprequest.py +8 -24
  25. writer/blocks/ifelse.py +71 -0
  26. writer/blocks/logmessage.py +2 -2
  27. writer/blocks/returnvalue.py +2 -2
  28. writer/blocks/runblueprint.py +2 -2
  29. writer/blocks/setstate.py +5 -5
  30. writer/blocks/sharedblueprint.py +86 -0
  31. writer/blocks/writeraddchatmessage.py +45 -7
  32. writer/blocks/writeraddtokg.py +31 -4
  33. writer/blocks/writeraskkg.py +27 -34
  34. writer/blocks/writerchat.py +14 -9
  35. writer/blocks/writerchatreply.py +279 -0
  36. writer/blocks/writerchatreplywithtoolconfig.py +393 -0
  37. writer/blocks/writerclassification.py +42 -5
  38. writer/blocks/writercompletion.py +4 -4
  39. writer/blocks/writerfileapi.py +4 -4
  40. writer/blocks/writerinitchat.py +5 -4
  41. writer/blocks/writerkeyvaluestorage.py +106 -0
  42. writer/blocks/writernocodeapp.py +4 -4
  43. writer/blocks/writerparsepdf.py +8 -6
  44. writer/blocks/writerstructuredoutput.py +105 -0
  45. writer/blocks/writertoolcalling.py +106 -51
  46. writer/blocks/writervision.py +141 -0
  47. writer/blocks/writerwebsearch.py +175 -0
  48. writer/blueprints.py +715 -251
  49. writer/command_line.py +52 -16
  50. writer/core.py +200 -35
  51. writer/core_ui.py +4 -0
  52. writer/evaluator.py +38 -24
  53. writer/journal.py +227 -0
  54. writer/keyvalue_storage.py +93 -0
  55. writer/logs.py +277 -0
  56. writer/serve.py +402 -198
  57. writer/ss_types.py +41 -0
  58. writer/static/assets/BaseMarkdown-Wrvby5J8.js +1 -0
  59. writer/static/assets/BlueprintToolbar-BuXNRxWT.js +1 -0
  60. writer/static/assets/BlueprintToolbar-wpfX0jo_.css +1 -0
  61. writer/static/assets/BuilderApp-PTOI76jZ.js +8 -0
  62. writer/static/assets/BuilderApp-WimUfNZr.css +1 -0
  63. writer/static/assets/BuilderApplicationSelect-DXzy4e_h.js +7 -0
  64. writer/static/assets/BuilderApplicationSelect-XaM1D5fv.css +1 -0
  65. writer/static/assets/BuilderBlueprintLibraryPanel-Ckrhknlh.css +1 -0
  66. writer/static/assets/BuilderBlueprintLibraryPanel-DBDzhTlc.js +1 -0
  67. writer/static/assets/{BuilderEmbeddedCodeEditor-DiDqfWdt.css → BuilderEmbeddedCodeEditor-B0bcjlhk.css} +1 -1
  68. writer/static/assets/{BuilderEmbeddedCodeEditor-CbK-r9w6.js → BuilderEmbeddedCodeEditor-Dn7eDICN.js} +7 -7
  69. writer/static/assets/BuilderGraphSelect-C-LRsO8W.js +7 -0
  70. writer/static/assets/BuilderGraphSelect-D7B61d5s.css +1 -0
  71. writer/static/assets/{BuilderInsertionLabel-CDlWX-mE.js → BuilderInsertionLabel-BhyL9wgn.js} +1 -1
  72. writer/static/assets/{BuilderInsertionOverlay-B7-TsHNC.js → BuilderInsertionOverlay-MkAIVruY.js} +1 -1
  73. writer/static/assets/BuilderJournal-A0LcEwGI.js +7 -0
  74. writer/static/assets/BuilderJournal-DHv3Pvvm.css +1 -0
  75. writer/static/assets/BuilderModelSelect-CdSo_sih.js +7 -0
  76. writer/static/assets/BuilderModelSelect-Dc4IPLp2.css +1 -0
  77. writer/static/assets/BuilderSettings-BDwZBveu.js +16 -0
  78. writer/static/assets/BuilderSettings-lZkOXEYw.css +1 -0
  79. writer/static/assets/BuilderSettingsArtifactAPITriggerDetails-3O6jKBXD.js +4 -0
  80. writer/static/assets/BuilderSettingsArtifactAPITriggerDetails-DnX66iRg.css +1 -0
  81. writer/static/assets/BuilderSettingsDeploySharedBlueprint-BR_3ptsd.js +1 -0
  82. writer/static/assets/BuilderSettingsDeploySharedBlueprint-KJTl8gxP.css +1 -0
  83. writer/static/assets/BuilderSettingsHandlers-CBtEQFSo.js +1 -0
  84. writer/static/assets/BuilderSettingsHandlers-DJPeASfz.css +1 -0
  85. writer/static/assets/BuilderSidebarComponentTree-DLltgas5.js +1 -0
  86. writer/static/assets/BuilderSidebarComponentTree-DYu1F793.css +1 -0
  87. writer/static/assets/BuilderSidebarToolkit-CApZNTAq.js +7 -0
  88. writer/static/assets/BuilderSidebarToolkit-CwqbjRv8.css +1 -0
  89. writer/static/assets/BuilderTemplateEditor-CYSDeWgV.css +1 -0
  90. writer/static/assets/BuilderTemplateEditor-DnRDRcA0.js +87 -0
  91. writer/static/assets/BuilderVault-2vGoV0sx.js +1 -0
  92. writer/static/assets/BuilderVault-Cx6oQSES.css +1 -0
  93. writer/static/assets/ComponentRenderer-72hqvEvI.css +1 -0
  94. writer/static/assets/ComponentRenderer-D4Pj1i3s.js +1 -0
  95. writer/static/assets/SharedCopyClipboardButton-BipJKGtz.css +1 -0
  96. writer/static/assets/SharedCopyClipboardButton-DNI9kLe6.js +1 -0
  97. writer/static/assets/WdsCheckbox-DKvpPA4D.css +1 -0
  98. writer/static/assets/WdsCheckbox-edQcn1cf.js +1 -0
  99. writer/static/assets/WdsDropdownMenu-CzzPN9Wg.css +1 -0
  100. writer/static/assets/WdsDropdownMenu-DQnrRBNV.js +1 -0
  101. writer/static/assets/WdsFieldWrapper-Cmufx5Nj.js +1 -0
  102. writer/static/assets/WdsFieldWrapper-CsemOh8D.css +1 -0
  103. writer/static/assets/WdsTabs-DKj7BqI0.css +1 -0
  104. writer/static/assets/WdsTabs-DcfY_zn5.js +1 -0
  105. writer/static/assets/{art-paper-WsD9P5Lu.svg → art-paper-D70v1WMA.svg} +0 -1
  106. writer/static/assets/{cssMode-6B7VrieQ.js → cssMode-BYq4oZGq.js} +1 -1
  107. writer/static/assets/{freemarker2-Dy54TNCQ.js → freemarker2-CnNourkO.js} +1 -1
  108. writer/static/assets/{handlebars-BnWqX2x5.js → handlebars-Bm22yapJ.js} +1 -1
  109. writer/static/assets/{html-CIuj_eOg.js → html-CAKAfoZF.js} +1 -1
  110. writer/static/assets/{htmlMode-5fUQN2xJ.js → htmlMode-BGZ97n-V.js} +1 -1
  111. writer/static/assets/index-BKNuk68o.css +1 -0
  112. writer/static/assets/index-BQNXU3IR.js +17 -0
  113. writer/static/assets/index-DHXAd5Yn.js +4 -0
  114. writer/static/assets/index-Zki-pfO-.js +8525 -0
  115. writer/static/assets/index.esm-B1ZQtduY.js +17 -0
  116. writer/static/assets/{javascript-PLzaI1wY.js → javascript-X1f02eyK.js} +1 -1
  117. writer/static/assets/{jsonMode-CzZfZ4-D.js → jsonMode-hT0bNgT8.js} +1 -1
  118. writer/static/assets/{liquid-Cy21vWLb.js → liquid-KmCCiJw2.js} +1 -1
  119. writer/static/assets/{mapbox-gl-BjXsUCYi.js → mapbox-gl-C0cyFYYW.js} +1 -1
  120. writer/static/assets/{mdx-Cgik3q5p.js → mdx-DtRFauUw.js} +1 -1
  121. writer/static/assets/pdf-B6-yWJ-Y.js +12 -0
  122. writer/static/assets/pdf.worker.min-CyUfim15.mjs +21 -0
  123. writer/static/assets/{plotly.min-Dk-1ahEu.js → plotly.min-DutuuatZ.js} +1 -1
  124. writer/static/assets/{python-h6gjz_bN.js → python-DVhxg746.js} +1 -1
  125. writer/static/assets/{razor-BQ1k9241.js → razor-DR5Ns_BC.js} +1 -1
  126. writer/static/assets/{tsMode-BaBWt05D.js → tsMode-BNUEZzir.js} +1 -1
  127. writer/static/assets/{typescript-B42-gYGT.js → typescript-CRVt7Hx0.js} +1 -1
  128. writer/static/assets/useBlueprintRun-C00bCxh-.js +1 -0
  129. writer/static/assets/useKeyValueEditor-nDmI7cTJ.js +1 -0
  130. writer/static/assets/useListResources-DLkZhRSJ.js +1 -0
  131. writer/static/assets/{xml-BOez4Prd.js → xml-C_6-t1tb.js} +1 -1
  132. writer/static/assets/{yaml-BQhoEOIz.js → yaml-DIw8G7jk.js} +1 -1
  133. writer/static/components/annotatedtext.svg +3 -3
  134. writer/static/components/avatar.svg +3 -3
  135. writer/static/components/blueprints_addtostatelist.svg +3 -3
  136. writer/static/components/blueprints_apitrigger.svg +4 -0
  137. writer/static/components/blueprints_category_Logic.svg +3 -3
  138. writer/static/components/blueprints_category_Other.svg +3 -3
  139. writer/static/components/blueprints_category_Writer.svg +24 -5
  140. writer/static/components/blueprints_code.svg +6 -6
  141. writer/static/components/blueprints_crontrigger.svg +6 -0
  142. writer/static/components/blueprints_foreach.svg +3 -3
  143. writer/static/components/blueprints_httprequest.svg +6 -6
  144. writer/static/components/blueprints_logmessage.svg +10 -3
  145. writer/static/components/blueprints_parsejson.svg +3 -3
  146. writer/static/components/blueprints_returnvalue.svg +3 -3
  147. writer/static/components/blueprints_runblueprint.svg +3 -3
  148. writer/static/components/blueprints_setstate.svg +3 -3
  149. writer/static/components/blueprints_writeraddchatmessage.svg +14 -6
  150. writer/static/components/blueprints_writeraddtokg.svg +14 -6
  151. writer/static/components/blueprints_writerchatreply.svg +19 -0
  152. writer/static/components/blueprints_writerclassification.svg +23 -8
  153. writer/static/components/blueprints_writercompletion.svg +13 -5
  154. writer/static/components/blueprints_writernocodeapp.svg +13 -3
  155. writer/static/components/button.svg +3 -3
  156. writer/static/components/category_Content.svg +3 -3
  157. writer/static/components/category_Embed.svg +3 -3
  158. writer/static/components/category_Input.svg +4 -4
  159. writer/static/components/category_Layout.svg +6 -6
  160. writer/static/components/category_Other.svg +3 -3
  161. writer/static/components/chatbot.svg +3 -3
  162. writer/static/components/checkboxinput.svg +3 -3
  163. writer/static/components/colorinput.svg +6 -6
  164. writer/static/components/column.svg +3 -3
  165. writer/static/components/columns.svg +3 -3
  166. writer/static/components/dataframe.svg +3 -3
  167. writer/static/components/dateinput.svg +3 -3
  168. writer/static/components/dropdowninput.svg +4 -4
  169. writer/static/components/fileinput.svg +3 -3
  170. writer/static/components/googlemaps.svg +3 -3
  171. writer/static/components/heading.svg +6 -6
  172. writer/static/components/horizontalstack.svg +3 -3
  173. writer/static/components/html.svg +6 -6
  174. writer/static/components/icon.svg +3 -3
  175. writer/static/components/iframe.svg +3 -3
  176. writer/static/components/image.svg +10 -3
  177. writer/static/components/jsonviewer.svg +3 -3
  178. writer/static/components/link.svg +7 -7
  179. writer/static/components/mapbox.svg +3 -3
  180. writer/static/components/message.svg +3 -3
  181. writer/static/components/metric.svg +3 -3
  182. writer/static/components/multiselectinput.svg +3 -3
  183. writer/static/components/numberinput.svg +3 -3
  184. writer/static/components/pagination.svg +3 -3
  185. writer/static/components/pdf.svg +3 -3
  186. writer/static/components/plotlygraph.svg +6 -6
  187. writer/static/components/progressbar.svg +4 -4
  188. writer/static/components/radioinput.svg +3 -3
  189. writer/static/components/rangeinput.svg +3 -3
  190. writer/static/components/ratinginput.svg +3 -3
  191. writer/static/components/repeater.svg +3 -3
  192. writer/static/components/reuse.svg +3 -3
  193. writer/static/components/section.svg +3 -3
  194. writer/static/components/selectinput.svg +4 -4
  195. writer/static/components/separator.svg +3 -3
  196. writer/static/components/sidebar.svg +3 -3
  197. writer/static/components/sliderinput.svg +3 -3
  198. writer/static/components/step.svg +3 -3
  199. writer/static/components/steps.svg +3 -3
  200. writer/static/components/switchinput.svg +3 -3
  201. writer/static/components/tab.svg +3 -3
  202. writer/static/components/tabs.svg +3 -3
  203. writer/static/components/tags.svg +10 -3
  204. writer/static/components/text.svg +3 -3
  205. writer/static/components/textareainput.svg +10 -3
  206. writer/static/components/textinput.svg +3 -3
  207. writer/static/components/timeinput.svg +3 -3
  208. writer/static/components/timer.svg +3 -3
  209. writer/static/components/videoplayer.svg +10 -3
  210. writer/static/components/webcamcapture.svg +3 -3
  211. writer/static/index.html +3 -11
  212. writer/static/status/cancelled.svg +5 -0
  213. writer/static/status/skipped.svg +4 -0
  214. writer/static/status/stopped.svg +4 -0
  215. writer/sync.py +431 -0
  216. writer/ui.py +49 -41
  217. writer/vault.py +48 -0
  218. writer/wf_project.py +5 -5
  219. writer-1.25.1rc1.dist-info/METADATA +92 -0
  220. writer-1.25.1rc1.dist-info/RECORD +382 -0
  221. {writer-0.8.3rc21.dist-info → writer-1.25.1rc1.dist-info}/WHEEL +1 -1
  222. writer/app_templates/default/.wf/components-blueprints_blueprint-0-t84xyhxau9ej3823.jsonl +0 -18
  223. writer/app_templates/default/static/welcome.svg +0 -40
  224. writer/static/assets/BaseMarkdown-BH_nSq9H.js +0 -1
  225. writer/static/assets/BlueprintToolbar-BO-WERxH.css +0 -1
  226. writer/static/assets/BlueprintToolbar-CHWL-rTm.js +0 -1
  227. writer/static/assets/BuilderApp-0XXiQ2l7.js +0 -7
  228. writer/static/assets/BuilderApp-CAhvLO4a.css +0 -1
  229. writer/static/assets/BuilderApplicationSelect-CwzU4F1-.js +0 -7
  230. writer/static/assets/BuilderApplicationSelect-DYYFtqjx.css +0 -1
  231. writer/static/assets/BuilderGraphSelect-CVO4gzts.css +0 -1
  232. writer/static/assets/BuilderGraphSelect-DV5Xy0HK.js +0 -7
  233. writer/static/assets/BuilderInstanceTracker-BECcXNnW.css +0 -1
  234. writer/static/assets/BuilderInstanceTracker-Dr0XhpSH.js +0 -1
  235. writer/static/assets/BuilderModelSelect-QHUGd86u.css +0 -1
  236. writer/static/assets/BuilderModelSelect-ow0XEf2t.js +0 -7
  237. writer/static/assets/BuilderSettings-C2WRfSor.css +0 -1
  238. writer/static/assets/BuilderSettings-D5bjbcWj.js +0 -24
  239. writer/static/assets/BuilderSettingsHandlers-1QLaHR8J.css +0 -1
  240. writer/static/assets/BuilderSettingsHandlers-j1aMAV4J.js +0 -1
  241. writer/static/assets/BuilderSidebarComponentTree-0YajaJke.css +0 -1
  242. writer/static/assets/BuilderSidebarComponentTree-CtaOZfpV.js +0 -1
  243. writer/static/assets/BuilderSidebarPanel-BMJVzhd3.js +0 -1
  244. writer/static/assets/BuilderSidebarPanel-BrLsNxVM.css +0 -1
  245. writer/static/assets/BuilderSidebarToolkit-BbjOOp8E.js +0 -1
  246. writer/static/assets/BuilderSidebarToolkit-BvZDShKD.css +0 -1
  247. writer/static/assets/ComponentRenderer-B76bKRZO.css +0 -1
  248. writer/static/assets/ComponentRenderer-CZs4z773.js +0 -1
  249. writer/static/assets/SharedMoreDropdown-BWKlox8E.css +0 -1
  250. writer/static/assets/SharedMoreDropdown-DKv_HNef.js +0 -7
  251. writer/static/assets/WdsDropdownMenu-C1UyKOJR.css +0 -1
  252. writer/static/assets/WdsDropdownMenu-CGiATY2E.js +0 -1
  253. writer/static/assets/WdsLoaderDots-qdyk2N-2.js +0 -1
  254. writer/static/assets/index-BJMAe9SN.js +0 -8
  255. writer/static/assets/index-CPCeQU9V.css +0 -1
  256. writer/static/assets/index-ChWW_c_j.js +0 -439
  257. writer/static/assets/instancePath-BsbOTTI8.js +0 -1
  258. writer/static/assets/material-symbols-outlined-latin-wght-normal-DuE-q1Ez.woff2 +0 -0
  259. writer/static/assets/useBlueprintRun-CBOvzWTA.js +0 -1
  260. writer/static/assets/useComponentDescription-FHKxu8gg.js +0 -1
  261. writer/static/assets/useListResources-BpMgq7XI.js +0 -1
  262. writer-0.8.3rc21.dist-info/METADATA +0 -117
  263. writer-0.8.3rc21.dist-info/RECORD +0 -342
  264. {writer-0.8.3rc21.dist-info → writer-1.25.1rc1.dist-info}/entry_points.txt +0 -0
  265. {writer-0.8.3rc21.dist-info → writer-1.25.1rc1.dist-info/licenses}/LICENSE.txt +0 -0
writer/journal.py ADDED
@@ -0,0 +1,227 @@
1
+ import contextlib
2
+ import json
3
+ import logging
4
+ from contextvars import ContextVar
5
+ from copy import deepcopy
6
+ from datetime import datetime, timezone
7
+ from typing import TYPE_CHECKING, Any, Dict, Literal, Optional
8
+
9
+ import writer.abstract
10
+ from writer.core import Config
11
+ from writer.keyvalue_storage import writer_kv_storage
12
+
13
+ if TYPE_CHECKING:
14
+ from writer.blueprints import Graph, GraphNode
15
+ from writer.core import Component
16
+
17
+
18
+ logger = logging.getLogger("journal")
19
+
20
+ JOURNAL_KEY_PREFIX = "wf-journal-"
21
+ INIT_LOGS_KEY_PREFIX = "wf-init-logs-"
22
+
23
+ class JournalRecord:
24
+ def __init__(
25
+ self,
26
+ execution_environment: Dict,
27
+ title: str,
28
+ graph: "Graph"
29
+ ):
30
+ from writer import core_ui
31
+
32
+ self.started_at = datetime.now(timezone.utc)
33
+ self.instance_type = "editor" if Config.mode == "edit" else "agent"
34
+
35
+ # Get blueprint_id from the parent of any node in the graph
36
+ # All nodes in a blueprint share the same parent blueprint component
37
+ self.blueprint_id = graph.nodes[0].component.parentId if graph.nodes else None
38
+
39
+ self.execution_environment = execution_environment
40
+ self.trigger = {
41
+ "event": execution_environment.get("context", {}).get("event"),
42
+ "payload": execution_environment.get("payload"),
43
+ "component": {}
44
+ }
45
+
46
+ if self.trigger["event"] == "wf-run-blueprint":
47
+ self.trigger["component"]["type"] = "blueprint"
48
+ self.trigger["component"]["id"] = self.blueprint_id
49
+ blueprint_component = core_ui.current_component_tree().get_component(self.trigger["component"]["id"])
50
+ if blueprint_component is not None:
51
+ self.trigger["component"]["title"] = blueprint_component.content.get("key")
52
+ else:
53
+ self.trigger["component"]["type"] = "block"
54
+ component = graph.get_start_nodes()[0].component
55
+ self.trigger["component"]["id"] = component.id
56
+ self.trigger["component"]["title"] = self._get_block_info(component)["title"]
57
+
58
+ if "API" in title:
59
+ self.trigger["type"] = "API"
60
+ elif "Cron" in title:
61
+ self.trigger["type"] = "Cron"
62
+ elif "UI" in title:
63
+ self.trigger["type"] = "UI"
64
+ else:
65
+ self.trigger["type"] = "On demand"
66
+
67
+ self.graph = graph
68
+ self.block_outputs: Dict[str, Any] = {}
69
+ for graph_node in self.graph.nodes:
70
+ block_info = self._get_block_info(graph_node.component)
71
+ self.block_outputs[graph_node.id] = {
72
+ "component": {
73
+ "type": graph_node.component.type,
74
+ "id": graph_node.component.id,
75
+ "title": block_info["title"],
76
+ "category": block_info["category"]
77
+ },
78
+ "executions": []
79
+ }
80
+
81
+ self.is_runable = True
82
+ self.result: Optional[Literal["success", "error", "stopped"]] = None
83
+
84
+ def _get_block_info(self, component: "Component") -> Dict[str, str]:
85
+ block_title = component.content.get("alias")
86
+ component_definition = writer.abstract.templates.get(component.type)
87
+
88
+ # If component has an alias, use it as title
89
+ if block_title is not None:
90
+ category = "Unknown category"
91
+ if component_definition is not None:
92
+ category = component_definition.writer.get("category", "Unknown category")
93
+ return {
94
+ "title": block_title,
95
+ "category": category
96
+ }
97
+
98
+ # If no component definition found, return defaults
99
+ if component_definition is None:
100
+ return {
101
+ "title": "Unknown block",
102
+ "category": "Unknown category"
103
+ }
104
+
105
+ # Use component definition for both title and category
106
+ return {
107
+ "title": component_definition.writer.get("name", "Unknown block"),
108
+ "category": component_definition.writer.get("category", "Unknown category")
109
+ }
110
+
111
+ def to_dict(self) -> Dict[str, Any]:
112
+ block_outputs = deepcopy(self.block_outputs)
113
+ for graph_node in self.graph.nodes:
114
+ block_outputs[graph_node.id]["executions"].append(self.get_execution_data(graph_node))
115
+
116
+ data = {
117
+ "timestamp": self.started_at.isoformat(),
118
+ "instanceType": self.instance_type,
119
+ "blueprintId": self.blueprint_id,
120
+ "trigger": self.trigger,
121
+ "blockOutputs": block_outputs,
122
+ "result": self.result,
123
+ }
124
+ sanitized_data = self._sanitize_data(data)
125
+ return {
126
+ **sanitized_data,
127
+ "isRunable": self.is_runable,
128
+ }
129
+
130
+ def get_execution_data(self, graph_node: "GraphNode") -> Dict[str, Any]:
131
+ execution_data: Dict[str, Any] = {
132
+ "result": graph_node.result,
133
+ "outcome": graph_node.outcome,
134
+ }
135
+
136
+ # Add timing information if available
137
+ if graph_node.tool:
138
+ if hasattr(graph_node.tool, 'started_at') and graph_node.tool.started_at >= 0:
139
+ execution_data["startedAt"] = graph_node.tool.started_at
140
+ if hasattr(graph_node.tool, 'execution_time_in_seconds') and graph_node.tool.execution_time_in_seconds >= 0:
141
+ execution_data["executionTimeInSeconds"] = graph_node.tool.execution_time_in_seconds
142
+
143
+ # Add captured logs if available
144
+ if hasattr(graph_node.tool, 'captured_stdout') and graph_node.tool.captured_stdout:
145
+ execution_data["stdout"] = graph_node.tool.captured_stdout
146
+ if hasattr(graph_node.tool, 'captured_logs') and graph_node.tool.captured_logs:
147
+ execution_data["logs"] = graph_node.tool.captured_logs
148
+
149
+ # Add error message if available (contains the traceback for errors)
150
+ if hasattr(graph_node.tool, 'message') and graph_node.tool.message:
151
+ execution_data["message"] = graph_node.tool.message
152
+
153
+ return execution_data
154
+
155
+ def _sanitize_data(self, data):
156
+ if data is None:
157
+ return None
158
+
159
+ if isinstance(data, list):
160
+ return [self._sanitize_data(item) for item in data]
161
+ if isinstance(data, dict):
162
+ return {
163
+ k: self._sanitize_data(v)
164
+ for k, v in data.items()
165
+ }
166
+ if isinstance(data, (str, int, float, bool, type(None))):
167
+ return data
168
+
169
+ try:
170
+ return json.loads(json.dumps(data))
171
+ except (TypeError, OverflowError):
172
+ self.is_runable = False
173
+ return f"Can't be displayed in the Journal. Value of type: {str(type(data))}."
174
+
175
+ def construct_key(self) -> str:
176
+ return f"{JOURNAL_KEY_PREFIX}{self.instance_type[0]}-{int(self.started_at.timestamp() * 1000)}"
177
+
178
+ def set_result(self, result: Literal["success", "error", "stopped"]) -> None:
179
+ self.result = result
180
+
181
+ def add_nested_execution(self, nested_record: "JournalRecord") -> None:
182
+ for graph_node in nested_record.graph.nodes:
183
+ if graph_node.id not in self.block_outputs:
184
+ self.block_outputs[graph_node.id] = nested_record.block_outputs[graph_node.id]
185
+ self.block_outputs[graph_node.id]["executions"].append(nested_record.get_execution_data(graph_node))
186
+
187
+ def save(self) -> None:
188
+ if "journal" not in Config.feature_flags or not writer_kv_storage.is_accessible():
189
+ return
190
+ data = self.to_dict()
191
+ writer_kv_storage.save(self.construct_key(), data)
192
+
193
+
194
+ _parent_journal_record: ContextVar[Optional[JournalRecord]] = ContextVar("parent_journal_record", default=None)
195
+ _current_journal_record: ContextVar[Optional[JournalRecord]] = ContextVar("current_journal_record", default=None)
196
+
197
+ @contextlib.contextmanager
198
+ def use_journal_record_context(
199
+ execution_environment: Dict,
200
+ title: str,
201
+ graph: "Graph"
202
+ ):
203
+ parent_record = _parent_journal_record.get()
204
+ current_record = JournalRecord(execution_environment, title, graph)
205
+ _current_journal_record.set(current_record)
206
+ if parent_record is None:
207
+ _parent_journal_record.set(current_record)
208
+
209
+ try:
210
+ yield current_record
211
+ except BaseException as e:
212
+ current_record.set_result("error")
213
+ raise e
214
+ finally:
215
+ _current_journal_record.set(None)
216
+ if parent_record is not None:
217
+ parent_record.add_nested_execution(current_record)
218
+ else:
219
+ try:
220
+ current_record.save()
221
+ except Exception:
222
+ logger.exception("Failed to save a Journal entry")
223
+ _parent_journal_record.set(None)
224
+
225
+
226
+ def get_current_journal_record() -> Optional[JournalRecord]:
227
+ return _current_journal_record.get()
@@ -0,0 +1,93 @@
1
+ import logging
2
+ import os
3
+ from functools import partial
4
+ from typing import Any, Dict, List, Literal, Optional, Protocol
5
+
6
+ import httpx
7
+
8
+ logger = logging.getLogger("kv_storage")
9
+
10
+
11
+ class _WrappedRequestFunc(Protocol):
12
+ def __call__(
13
+ self,
14
+ headers: Dict[str, str],
15
+ timeout: int,
16
+ ) -> httpx.Response: ...
17
+
18
+
19
+ class KeyValueStorage:
20
+ def __init__(self, client: Optional[httpx.Client] = None) -> None:
21
+ base_url = os.getenv("WRITER_BASE_URL")
22
+ self.api_key = os.getenv("WRITER_API_KEY")
23
+ if None in (base_url, self.api_key):
24
+ logger.warning("Missing required environment variables for KV storage access")
25
+ self.api_url = f"{base_url}/v1" if base_url else None
26
+
27
+ self._client = client if client is not None else httpx
28
+
29
+ def _get_agent_ids(self):
30
+ from writer.core import get_session
31
+ current_session = get_session()
32
+
33
+ if current_session:
34
+ headers = current_session.headers or {}
35
+ agent_id = headers.get("x-agent-id") or os.getenv("WRITER_APP_ID")
36
+ org_id = headers.get("x-organization-id") or os.getenv("WRITER_ORG_ID")
37
+ return (agent_id, org_id)
38
+
39
+ agent_id = os.getenv("WRITER_APP_ID")
40
+ org_id = os.getenv("WRITER_ORG_ID")
41
+ return (agent_id, org_id)
42
+
43
+ def get(self, key: str, type_: Literal["data", "secret"]) -> Dict[str, Any]:
44
+ return self._request(partial(self._client.get, url=f"{self.api_url}/agent_{type_}/{key}")).json()
45
+
46
+ def get_data_keys(self) -> List[str]:
47
+ return self._request(partial(self._client.get, url=f"{self.api_url}/agent_data")).json()["keys"]
48
+
49
+ def save(self, key: str, data: Any) -> Dict[str, Any]:
50
+ try:
51
+ return self._create(key, data).json()
52
+ except httpx.HTTPStatusError as e:
53
+ if "already exists" in e.response.text:
54
+ return self._update(key, data).json()
55
+ raise e
56
+
57
+ def _create(self, key: str, data: Any) -> httpx.Response:
58
+ return self._request(partial(self._client.post, url=f"{self.api_url}/agent_data", json={"key": key, "data": data}))
59
+
60
+ def _update(self, key: str, data: Any) -> httpx.Response:
61
+ return self._request(partial(self._client.put, url=f"{self.api_url}/agent_data/{key}", json={"data": data}))
62
+
63
+ def delete(self, key: str) -> Dict[str, str]:
64
+ self._request(partial(self._client.delete, url=f"{self.api_url}/agent_data/{key}"))
65
+ return {"key": key}
66
+
67
+ def _request(self, request_func: _WrappedRequestFunc) -> httpx.Response:
68
+
69
+ agent_id, org_id = self._get_agent_ids()
70
+ if None in (agent_id, org_id):
71
+ raise ValueError("Can't access KV storage. Missing agent id or org id")
72
+
73
+ if None in (self.api_key, self.api_url):
74
+ raise ValueError("Can't access KV storage. Missing required env vars")
75
+
76
+ headers = {
77
+ "Authorization": f"Bearer {self.api_key}",
78
+ "X-Organization-Id": org_id,
79
+ "X-Agent-Id": agent_id,
80
+ }
81
+
82
+ response = request_func(headers=headers, timeout=3)
83
+ response.raise_for_status()
84
+ return response
85
+
86
+ def is_accessible(self) -> bool:
87
+ if None in self._get_agent_ids():
88
+ return False
89
+ if None in (self.api_key, self.api_url):
90
+ return False
91
+ return True
92
+
93
+ writer_kv_storage = KeyValueStorage()
writer/logs.py ADDED
@@ -0,0 +1,277 @@
1
+ import io
2
+ import json
3
+ import logging
4
+ import logging.config
5
+ import os
6
+ from contextlib import contextmanager, redirect_stdout
7
+ from time import time
8
+ from typing import Any, Callable, Dict, List, Optional, Union
9
+
10
+ WRITER_LOG_LEVEL = os.getenv("WRITER_LOG_LEVEL", "INFO")
11
+ WRITER_LOG_FORMAT = os.getenv("WRITER_LOG_FORMAT", "text") # 'text' or 'json'
12
+
13
+ FAILOVER_ROUTING_KEY = "unset"
14
+ FAILOVER_BUFFER = "failover"
15
+
16
+
17
+ def get_routing_key(prefix: Optional[str] = None) -> str:
18
+ try:
19
+ from writer.blueprints import get_current_block
20
+ current_block = get_current_block()
21
+ except RuntimeError:
22
+ current_block = None
23
+
24
+ key = FAILOVER_ROUTING_KEY
25
+ if current_block is not None:
26
+ key = current_block.component.id
27
+ return f"{prefix}-{key}"
28
+
29
+
30
+ class RoutingMap():
31
+ """
32
+ Maintains a map of routing keys to in-memory output buffers (io.StringIO).
33
+ Used for capturing logs or stdout output in different contexts.
34
+ """
35
+
36
+ def __init__(self) -> None:
37
+ # It's not expected that this will be used without context.
38
+ # But just in case a fail-over buffer is provided
39
+ self._buffer_map: Dict[str, io.StringIO] = {
40
+ FAILOVER_BUFFER: io.StringIO(),
41
+ }
42
+
43
+ def get_buffer(self, key: str) -> io.StringIO:
44
+ """
45
+ Retrieve the buffer associated with a routing key.
46
+
47
+ If buffer for the key is not present uses a fail-over buffer
48
+ """
49
+ return self._buffer_map.get(key, self._buffer_map[FAILOVER_BUFFER])
50
+
51
+ def add_buffer(self, key: str) -> io.StringIO:
52
+ """
53
+ Add a new buffer with the given key.
54
+ """
55
+ buffer = io.StringIO()
56
+ self._buffer_map[key] = buffer
57
+ return buffer
58
+
59
+ def remove_buffer(self, key: str) -> None:
60
+ """
61
+ Remove the buffer associated with the specified key.
62
+ """
63
+ self._buffer_map.pop(key, None)
64
+
65
+
66
+ routing_map = RoutingMap()
67
+
68
+
69
+ class RoutingStream(io.StringIO):
70
+ """
71
+ Custom stream that re-routes stdout to the correct io.StringIO buffer
72
+ based on the current context routing key.
73
+ """
74
+
75
+ def write(self, s: str) -> int:
76
+ if s.strip():
77
+ logging.getLogger("stdout").info(s)
78
+ routing_key = get_routing_key(prefix="stdout")
79
+ return routing_map.get_buffer(routing_key).write(s)
80
+
81
+ def getvalue(self) -> str:
82
+ routing_key = get_routing_key(prefix="stdout")
83
+ return routing_map.get_buffer(routing_key).getvalue()
84
+
85
+
86
+ class RoutingHandler(logging.StreamHandler):
87
+ """
88
+ Custom logging handler that re-routes logs to different buffers
89
+ based on the current context routing key.
90
+
91
+ Overwritten methods are mirroring original ones from logging.StreamHandler.
92
+ The only difference is how 'stream' object is acquired
93
+ """
94
+
95
+ def emit(self, record):
96
+ try:
97
+ msg = self.format(record)
98
+ routing_key = get_routing_key(prefix="logging")
99
+ stream = routing_map.get_buffer(routing_key)
100
+ stream.write(msg + self.terminator)
101
+ self.flush()
102
+ except RecursionError:
103
+ raise
104
+ except Exception:
105
+ self.handleError(record)
106
+
107
+ def flush(self):
108
+ routing_key = get_routing_key(prefix="logging")
109
+ stream = routing_map.get_buffer(routing_key)
110
+ with self.lock:
111
+ if stream and hasattr(stream, "flush"):
112
+ stream.flush()
113
+
114
+
115
+ @contextmanager
116
+ def use_stdout_redirect(add_log_entry_func: Union[Callable[[str], None], List[Callable[[str], None]]]):
117
+ """
118
+ Context manager that redirects stdout to a context-specific buffer.
119
+ Supports single callback or list of callbacks for multiple output destinations.
120
+ """
121
+ # Normalize to list of callbacks
122
+ callbacks = [add_log_entry_func] if callable(add_log_entry_func) else add_log_entry_func
123
+
124
+ key = get_routing_key(prefix="stdout")
125
+ buffer = routing_map.add_buffer(key)
126
+ try:
127
+ with redirect_stdout(RoutingStream()):
128
+ yield buffer
129
+ finally:
130
+ routing_map.remove_buffer(key)
131
+ stdout = buffer.getvalue()
132
+ if stdout:
133
+ for callback in callbacks:
134
+ callback(stdout)
135
+
136
+
137
+ @contextmanager
138
+ def use_logging_redirect(add_log_entry_func: Union[Callable[[str], None], List[Callable[[str], None]]]):
139
+ """
140
+ Context manager that redirects logging to a context-specific buffer.
141
+ Supports single callback or list of callbacks for multiple output destinations.
142
+ """
143
+ # Normalize to list of callbacks
144
+ callbacks = [add_log_entry_func] if callable(add_log_entry_func) else add_log_entry_func
145
+
146
+ key = get_routing_key(prefix="logging")
147
+ buffer = routing_map.add_buffer(key)
148
+ try:
149
+ yield buffer
150
+ finally:
151
+ routing_map.remove_buffer(key)
152
+ logs = buffer.getvalue()
153
+ if logs:
154
+ for callback in callbacks:
155
+ callback(logs)
156
+
157
+
158
+ class JSONFormatter(logging.Formatter):
159
+ def format(self, record: logging.LogRecord, **kwargs) -> str:
160
+ try:
161
+ from writer.blueprints import get_current_block
162
+ from writer.core import get_app_process, get_session
163
+ current_block = get_current_block()
164
+ app_process = get_app_process()
165
+ session = get_session()
166
+ except RuntimeError:
167
+ current_block = None
168
+ session = None
169
+ app_process = None
170
+
171
+ data: Dict[str, Any] = {
172
+ "severity": record.levelname.upper(),
173
+ "message": super().format(record),
174
+ "logger_name": record.name,
175
+ "process": {
176
+ "name": record.processName
177
+ },
178
+ "timestamp": time(),
179
+ }
180
+
181
+ if current_block is not None:
182
+ data["component"] = {
183
+ "id": current_block.component.id,
184
+ "type": current_block.component.type
185
+ }
186
+
187
+ if session is not None:
188
+ data["session"] = {
189
+ "id": session.session_id,
190
+ }
191
+
192
+ if app_process is not None:
193
+ data["process"]["mode"] = app_process.mode
194
+
195
+ if isinstance(record.args, dict):
196
+ data.update(record.args)
197
+
198
+ # if record.args[0] is a dict add to the json dict
199
+ if isinstance(record.args, tuple):
200
+ if len(record.args) > 0 and isinstance(record.args[0], dict):
201
+ data.update(record.args[0])
202
+
203
+ try:
204
+ data_as_json = json.dumps(data)
205
+ return data_as_json
206
+ except Exception:
207
+ return '{"unserializable": true}'
208
+
209
+
210
+ def get_handler(format: str = WRITER_LOG_FORMAT, level: str = WRITER_LOG_LEVEL):
211
+ return {
212
+ "level": level,
213
+ "class": "logging.StreamHandler",
214
+ "formatter": format,
215
+ }
216
+
217
+
218
+ def get_logger(level: str = WRITER_LOG_LEVEL):
219
+ return {
220
+ "handlers": ["basic"],
221
+ "level": level,
222
+ "propagate": False,
223
+ }
224
+
225
+
226
+ LOGGING_CONFIG: Dict[str, Any] = {
227
+ "version": 1,
228
+ "disable_existing_loggers": False,
229
+ "formatters": {
230
+ "text": {
231
+ "format": "%(levelname)s - %(name)s - %(message)s",
232
+ },
233
+ "json": {
234
+ "()": JSONFormatter,
235
+ },
236
+ "user": {
237
+ "format": "%(levelname)s - %(message)s"
238
+ }
239
+ },
240
+ "handlers": {
241
+ "basic": get_handler(),
242
+ "stdout": {
243
+ "level": "DEBUG",
244
+ "class": "logging.StreamHandler",
245
+ "formatter": "json" if WRITER_LOG_FORMAT == "json" else None
246
+ },
247
+ "routing": {
248
+ "()": RoutingHandler,
249
+ "formatter": "user",
250
+ }
251
+ },
252
+ "loggers": {
253
+ "root": get_logger(),
254
+ "writer": get_logger(),
255
+ "app": get_logger(),
256
+ "from_app": get_logger(),
257
+ "journal": get_logger(),
258
+ "kv_storage": get_logger(),
259
+ "vault": get_logger(),
260
+ "exec_logger": {
261
+ "handlers": ["basic", "routing"],
262
+ "level": "DEBUG",
263
+ "propagate": False,
264
+ },
265
+ "user_code": {
266
+ "handlers": ["basic", "routing"],
267
+ "level": "DEBUG",
268
+ "propagate": False,
269
+ },
270
+ "stdout": {
271
+ "handlers": ["stdout"],
272
+ "propagate": False,
273
+ }
274
+ }
275
+ }
276
+
277
+ logging.config.dictConfig(LOGGING_CONFIG)