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/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)
|