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/ai/__init__.py CHANGED
@@ -2,6 +2,7 @@ import json
2
2
  import logging
3
3
  from contextvars import ContextVar
4
4
  from datetime import datetime
5
+ from functools import wraps
5
6
  from typing import (
6
7
  Any,
7
8
  Callable,
@@ -13,6 +14,7 @@ from typing import (
13
14
  Optional,
14
15
  Set,
15
16
  TypedDict,
17
+ TypeVar,
16
18
  Union,
17
19
  cast,
18
20
  )
@@ -20,7 +22,7 @@ from uuid import uuid4
20
22
 
21
23
  from httpx import Timeout
22
24
  from writerai import DefaultHttpxClient, Writer
23
- from writerai._exceptions import WriterError
25
+ from writerai._exceptions import BadRequestError, WriterError
24
26
  from writerai._response import BinaryAPIResponse
25
27
  from writerai._streaming import Stream
26
28
  from writerai._types import Body, Headers, NotGiven, Query
@@ -45,7 +47,14 @@ from writerai.types.applications import (
45
47
  JobRetryResponse,
46
48
  )
47
49
  from writerai.types.applications.application_graphs_response import ApplicationGraphsResponse
48
- from writerai.types.chat_chat_params import GraphData, ToolChoice
50
+ from writerai.types.chat_chat_params import (
51
+ GraphData,
52
+ MessageContentMixedContentImageFragment,
53
+ MessageContentMixedContentImageFragmentImageURL,
54
+ MessageContentMixedContentTextFragment,
55
+ ResponseFormat,
56
+ ToolChoice,
57
+ )
49
58
  from writerai.types.chat_chat_params import Message as WriterAIMessage
50
59
  from writerai.types.chat_completion_message import ChatCompletionMessage
51
60
  from writerai.types.question import Question
@@ -53,14 +62,23 @@ from writerai.types.question_response_chunk import QuestionResponseChunk
53
62
  from writerai.types.shared_params.tool_param import FunctionTool as SDKFunctionTool
54
63
  from writerai.types.shared_params.tool_param import GraphTool as SDKGraphTool
55
64
  from writerai.types.shared_params.tool_param import LlmTool as SDKLlmTool
65
+ from writerai.types.shared_params.tool_param import WebSearchTool as SDKWebSearchTool
56
66
 
57
67
  from writer.core import get_app_process
58
68
 
59
- DEFAULT_CHAT_MODEL = "palmyra-x-004"
60
- DEFAULT_COMPLETION_MODEL = "palmyra-x-004"
69
+ DEFAULT_CHAT_MODEL = "palmyra-x5"
70
+ DEFAULT_COMPLETION_MODEL = "palmyra-x5"
71
+
72
+
73
+ _ai_client: ContextVar[Optional[Writer]] = ContextVar(
74
+ "ai_client", default=None
75
+ )
61
76
 
62
77
 
63
- _ai_client: ContextVar[Optional[Writer]] = ContextVar("ai_client", default=None)
78
+ class ExtendedWebSearchTool(TypedDict, total=False):
79
+ """Extended web search tool that includes all fields supported by the API"""
80
+ type: Literal["web_search"]
81
+ function: Dict[str, Any] # Flexible to support include_raw_content and other fields
64
82
 
65
83
 
66
84
  class APIOptions(TypedDict, total=False):
@@ -75,10 +93,11 @@ class ChatOptions(APIOptions, total=False):
75
93
  tool_choice: ToolChoice
76
94
  tools: Union[
77
95
  Iterable[
78
- Union[SDKGraphTool, SDKFunctionTool, SDKLlmTool]
96
+ Union[SDKGraphTool, SDKFunctionTool, SDKLlmTool, SDKWebSearchTool]
79
97
  ],
80
98
  NotGiven
81
99
  ]
100
+ response_format: Union[ResponseFormat, NotGiven]
82
101
  logprobs: Union[bool, NotGiven]
83
102
  max_tokens: Union[int, NotGiven]
84
103
  n: Union[int, NotGiven]
@@ -166,6 +185,12 @@ class LLMTool(Tool):
166
185
  description: str
167
186
 
168
187
 
188
+ class WebSearchTool(Tool):
189
+ include_domains: Optional[List[str]]
190
+ exclude_domains: Optional[List[str]]
191
+ include_raw_content: Optional[bool]
192
+
193
+
169
194
  def _process_completion_data_chunk(choice: CompletionChunk) -> str:
170
195
  text = choice.value
171
196
  if not text:
@@ -451,7 +476,12 @@ class Graph(SDKWrapper):
451
476
  if description:
452
477
  payload["description"] = description
453
478
  graphs = self._retrieve_graphs_accessor()
454
- response = graphs.update(self.id, **payload, **config)
479
+ response = graphs.update(
480
+ self.id,
481
+ name=payload.get("name", NotGiven()),
482
+ description=payload.get("description", NotGiven()),
483
+ **config
484
+ )
455
485
  Graph.stale_ids.add(self.id)
456
486
  return response
457
487
 
@@ -1013,6 +1043,39 @@ def delete_file(
1013
1043
  return files.delete(file_id, **config)
1014
1044
 
1015
1045
 
1046
+ class GuardrailError(Exception):
1047
+ def __init__(self, name: str, message: str, *args):
1048
+ super().__init__(f"{message}: {name}", *args)
1049
+
1050
+
1051
+ R = TypeVar("R")
1052
+
1053
+
1054
+ def catch_guardrail_error(func: Callable[..., R]) -> Callable[..., R]:
1055
+ @wraps(func)
1056
+ def wrapper(*args: Any, **kwargs: Any) -> R:
1057
+ try:
1058
+ return func(*args, **kwargs)
1059
+ except BadRequestError as e:
1060
+ parsed = e.response.json()
1061
+ errors = parsed.get("errors")
1062
+ if not errors:
1063
+ raise
1064
+
1065
+ extras = parsed.get("extras")
1066
+ if extras is None:
1067
+ raise
1068
+ guardrail_info = extras.get("guardrail_info")
1069
+ if guardrail_info is None:
1070
+ raise
1071
+
1072
+ raise GuardrailError(
1073
+ name=guardrail_info["guardrail_name"],
1074
+ message=errors[0]["description"],
1075
+ ) from None
1076
+
1077
+ return wrapper
1078
+
1016
1079
  class Conversation:
1017
1080
  """
1018
1081
  Manages messages within a conversation flow with an AI system,
@@ -1107,17 +1170,24 @@ class Conversation:
1107
1170
  the `temperature` to 0.7 for this specific call.
1108
1171
 
1109
1172
  """
1173
+ class ContentFragment(TypedDict, total=False):
1174
+ """Content fragment for messages that can contain text or images."""
1175
+ type: Literal["text", "image_url"]
1176
+ text: Optional[str]
1177
+ image_url: Optional[Dict[str, str]]
1178
+
1110
1179
  class Message(TypedDict, total=False):
1111
1180
  """
1112
1181
  Typed dictionary for conversation messages.
1113
1182
 
1114
1183
  :param role: Specifies the sender role.
1115
- :param content: Text content of the message.
1184
+ :param content: Text content of the message or array of content fragments
1185
+ for multimodal messages.
1116
1186
  :param actions: Optional dictionary containing actions
1117
1187
  related to the message.
1118
1188
  """
1119
1189
  role: Literal["system", "assistant", "user", "tool"]
1120
- content: str
1190
+ content: Union[str, List['Conversation.ContentFragment']]
1121
1191
  actions: Optional[dict]
1122
1192
  name: Optional[str]
1123
1193
  tool_call_id: Optional[str]
@@ -1144,13 +1214,44 @@ class Conversation:
1144
1214
  f"Improper message format to add to Conversation: {message}"
1145
1215
  )
1146
1216
 
1147
- if not (
1148
- isinstance(message["content"], str)
1149
- or
1150
- message["content"] is None
1217
+ content = message["content"]
1218
+ if isinstance(content, list):
1219
+ # Validate multimodal content structure
1220
+ for fragment in content:
1221
+ if not isinstance(fragment, dict):
1222
+ raise ValueError(
1223
+ f"Invalid content fragment in message: {message}. "
1224
+ f"Fragments must be dictionaries."
1225
+ )
1226
+ fragment_type = fragment.get("type")
1227
+ if fragment_type not in ["text", "image_url"]:
1228
+ raise ValueError(
1229
+ f"Invalid fragment type '{fragment_type}' in message: {message}. "
1230
+ f"Type must be 'text' or 'image_url'."
1231
+ )
1232
+ if fragment_type == "text" and "text" not in fragment:
1233
+ raise ValueError(
1234
+ f"Text fragment missing 'text' field in message: {message}"
1235
+ )
1236
+ if fragment_type == "image_url" and "image_url" not in fragment:
1237
+ raise ValueError(
1238
+ f"Image fragment missing 'image_url' field in message: {message}"
1239
+ )
1240
+ if fragment_type == "image_url" and not isinstance(fragment.get("image_url"), dict):
1241
+ raise ValueError(
1242
+ f"Image fragment 'image_url' must be a dict in message: {message}"
1243
+ )
1244
+ if fragment_type == "image_url" and "url" not in fragment.get("image_url", {}):
1245
+ raise ValueError(
1246
+ f"Image fragment missing 'url' in 'image_url' in message: {message}"
1247
+ )
1248
+ elif not (
1249
+ isinstance(content, str)
1250
+ or content is None
1151
1251
  ):
1152
1252
  raise ValueError(
1153
- f"Non-string content in message cannot be added: {message}"
1253
+ f"Invalid content format in message: {message}. "
1254
+ f"Content must be a string, None, or array of content fragments."
1154
1255
  )
1155
1256
 
1156
1257
  if message["role"] not in ["system", "assistant", "user", "tool"]:
@@ -1232,7 +1333,24 @@ class Conversation:
1232
1333
  clear_chunk = _clear_chunk_flag(raw_chunk)
1233
1334
  updated_last_message: 'Conversation.Message' = self.messages[-1]
1234
1335
  if "content" in clear_chunk:
1235
- updated_last_message["content"] += clear_chunk.pop("content") or ""
1336
+ new_content = clear_chunk.pop("content") or ""
1337
+ if isinstance(updated_last_message["content"], list):
1338
+ # Handle list content (multimodal)
1339
+ if isinstance(new_content, str) and new_content:
1340
+ # Find the last text fragment and append to it, or create new one
1341
+ last_text_fragment = None
1342
+ for i in range(len(updated_last_message["content"]) - 1, -1, -1):
1343
+ if updated_last_message["content"][i].get("type") == "text":
1344
+ last_text_fragment = updated_last_message["content"][i]
1345
+ break
1346
+
1347
+ if last_text_fragment:
1348
+ last_text_fragment["text"] = (last_text_fragment.get("text") or "") + new_content
1349
+ else:
1350
+ updated_last_message["content"].append({"type": "text", "text": new_content})
1351
+ else:
1352
+ # Handle string content
1353
+ updated_last_message["content"] = str(updated_last_message["content"]) + str(new_content)
1236
1354
 
1237
1355
  if "tool_calls" in clear_chunk:
1238
1356
  # Ensure 'tool_calls' exists in updated_last_message as list
@@ -1290,15 +1408,48 @@ class Conversation:
1290
1408
  Converts a message object stored in Conversation to a Writer AI SDK
1291
1409
  `Message` model, suitable for calls to API.
1292
1410
 
1293
- :param raw_chunk: The data to be merged into the last message.
1411
+ :param message: The message to prepare.
1294
1412
  :raises ValueError: If there are no messages in the conversation
1295
1413
  to merge with.
1296
1414
  """
1297
1415
  if not ("role" in message and "content" in message):
1298
1416
  raise ValueError("Improper message format")
1299
- sdk_message = WriterAIMessage(
1300
- content=message.get("content", None) or "",
1301
- role=message["role"]
1417
+
1418
+ content = message.get("content")
1419
+ if isinstance(content, list):
1420
+ # Handle multimodal content (text + images)
1421
+ # Convert our ContentFragment format to SDK format
1422
+ sdk_content: List[Union[MessageContentMixedContentTextFragment, MessageContentMixedContentImageFragment]] = []
1423
+ for fragment in content:
1424
+ if fragment.get("type") == "text":
1425
+ sdk_content.append({
1426
+ "type": "text",
1427
+ "text": fragment.get("text") or ""
1428
+ })
1429
+ elif fragment.get("type") == "image_url":
1430
+ image_url_data = fragment.get("image_url", {})
1431
+ if isinstance(image_url_data, dict):
1432
+ # Extract the URL from the dict structure
1433
+ url = image_url_data.get("url", "")
1434
+ else:
1435
+ # Assume it's already a URL string
1436
+ url = str(image_url_data) if image_url_data else ""
1437
+
1438
+ image_url_obj: MessageContentMixedContentImageFragmentImageURL = {"url": url}
1439
+ sdk_content.append({
1440
+ "type": "image_url",
1441
+ "image_url": image_url_obj
1442
+ })
1443
+
1444
+ sdk_message = WriterAIMessage(
1445
+ content=sdk_content,
1446
+ role=message["role"]
1447
+ )
1448
+ else:
1449
+ # Handle simple text content
1450
+ sdk_message = WriterAIMessage(
1451
+ content=content or "",
1452
+ role=message["role"]
1302
1453
  )
1303
1454
  if msg_name := message.get("name"):
1304
1455
  sdk_message["name"] = cast(str, msg_name)
@@ -1383,8 +1534,8 @@ class Conversation:
1383
1534
 
1384
1535
  def _prepare_tool(
1385
1536
  self,
1386
- tool_instance: Union['Graph', GraphTool, FunctionTool, LLMTool]
1387
- ) -> Union[SDKGraphTool, SDKFunctionTool, SDKLlmTool]:
1537
+ tool_instance: Union['Graph', GraphTool, FunctionTool, LLMTool, WebSearchTool, dict]
1538
+ ) -> Union[SDKGraphTool, SDKFunctionTool, SDKLlmTool, SDKWebSearchTool]:
1388
1539
  """
1389
1540
  Internal helper function to process a tool instance
1390
1541
  into the required format.
@@ -1624,6 +1775,32 @@ class Conversation:
1624
1775
  }
1625
1776
  }
1626
1777
  )
1778
+ elif tool_type == "web_search":
1779
+ # Return web search tool JSON - SDK format
1780
+ tool_instance = cast(WebSearchTool, tool_instance)
1781
+ function_config: Dict[str, Any] = {}
1782
+
1783
+ # Add required parameters - SDK expects these as lists
1784
+ if "include_domains" in tool_instance:
1785
+ include_domains = tool_instance["include_domains"]
1786
+ if include_domains:
1787
+ function_config["include_domains"] = include_domains
1788
+ if "exclude_domains" in tool_instance:
1789
+ exclude_domains = tool_instance["exclude_domains"]
1790
+ if exclude_domains:
1791
+ function_config["exclude_domains"] = exclude_domains
1792
+ if "include_raw_content" in tool_instance:
1793
+ include_raw_content = tool_instance["include_raw_content"]
1794
+ if include_raw_content is not None:
1795
+ function_config["include_raw_content"] = include_raw_content
1796
+
1797
+ # Return as ExtendedWebSearchTool but ensure SDK compatibility
1798
+ result = ExtendedWebSearchTool({
1799
+ "type": "web_search",
1800
+ "function": function_config
1801
+ })
1802
+ # Cast to SDKWebSearchTool for SDK compatibility while preserving extra fields
1803
+ return cast(SDKWebSearchTool, result)
1627
1804
  else:
1628
1805
  raise ValueError(f"Unsupported tool type: {tool_type}")
1629
1806
 
@@ -1676,6 +1853,37 @@ class Conversation:
1676
1853
  """
1677
1854
  self.__add__({"role": role, "content": message})
1678
1855
 
1856
+ def add_with_images(
1857
+ self,
1858
+ role: Literal["system", "assistant", "user", "tool"],
1859
+ text: Optional[str] = None,
1860
+ image_urls: Optional[List[str]] = None
1861
+ ):
1862
+ """
1863
+ Adds a new multimodal message with text and/or images.
1864
+
1865
+ :param role: The role of the message sender.
1866
+ :param text: Optional text content.
1867
+ :param image_urls: Optional list of image URLs (web URLs or base64 data URLs).
1868
+ """
1869
+ if not text and not image_urls:
1870
+ raise ValueError("At least one of text or image_urls must be provided")
1871
+
1872
+ content: List['Conversation.ContentFragment'] = []
1873
+
1874
+ if text:
1875
+ content.append({"type": "text", "text": text})
1876
+
1877
+ if image_urls:
1878
+ for image_url in image_urls:
1879
+ content.append({
1880
+ "type": "image_url",
1881
+ "image_url": {"url": image_url}
1882
+ })
1883
+
1884
+ self.__add__({"role": role, "content": content})
1885
+
1886
+ @catch_guardrail_error
1679
1887
  def _send_chat_request(
1680
1888
  self,
1681
1889
  request_model: str,
@@ -1701,13 +1909,20 @@ class Conversation:
1701
1909
  f"prepared messages – {prepared_messages}, " +
1702
1910
  f"request_data – {request_data}"
1703
1911
  )
1912
+ tools = request_data.get('tools', NotGiven())
1913
+ tool_choice: Union[ToolChoice, NotGiven]
1914
+ if isinstance(tools, NotGiven):
1915
+ tool_choice = NotGiven()
1916
+ else:
1917
+ tool_choice = request_data.get('tool_choice', cast(ToolChoice, 'auto'))
1704
1918
  return client.chat.chat(
1705
1919
  messages=prepared_messages,
1706
1920
  model=request_model,
1707
1921
  stream=stream,
1708
1922
  logprobs=request_data.get('logprobs', NotGiven()),
1709
- tools=request_data.get('tools', NotGiven()),
1710
- tool_choice=request_data.get('tool_choice', NotGiven()),
1923
+ tools=tools,
1924
+ tool_choice=tool_choice,
1925
+ response_format=request_data.get('response_format', NotGiven()),
1711
1926
  max_tokens=request_data.get('max_tokens', NotGiven()),
1712
1927
  n=request_data.get('n', NotGiven()),
1713
1928
  stop=request_data.get('stop', NotGiven()),
@@ -2124,6 +2339,7 @@ class Conversation:
2124
2339
  ] # can be an instance of tool or a list of instances
2125
2340
  ] = None,
2126
2341
  max_tool_depth: int = 5,
2342
+ response_format: Optional[ResponseFormat] = None
2127
2343
  ) -> 'Conversation.Message':
2128
2344
  """
2129
2345
  Processes the conversation with the current messages and additional
@@ -2134,6 +2350,7 @@ class Conversation:
2134
2350
  :param tools: Optional tools to use for processing.
2135
2351
  :param config: Optional parameters to pass for processing.
2136
2352
  :param max_tool_depth: Maximum depth for tool calls processing.
2353
+ :param response_format: Optional JSON schema used to format the model's output.
2137
2354
  :return: Generated message.
2138
2355
  :raises RuntimeError: If response data was not properly formatted
2139
2356
  to retrieve model text.
@@ -2150,6 +2367,13 @@ class Conversation:
2150
2367
  request_data: ChatOptions = {**config, **self.config}
2151
2368
  if prepared_tools:
2152
2369
  request_data |= {"tools": prepared_tools}
2370
+ if response_format:
2371
+ if not isinstance(response_format, dict):
2372
+ raise ValueError(
2373
+ "Invalid schema for response_format: "
2374
+ f"dictionary required, got {type(response_format)}"
2375
+ )
2376
+ request_data |= {"response_format": response_format}
2153
2377
  request_model = \
2154
2378
  request_data.get("model") or WriterAIManager.use_chat_model()
2155
2379
 
@@ -2184,7 +2408,8 @@ class Conversation:
2184
2408
  List[Union[Graph, GraphTool, FunctionTool]]
2185
2409
  ] # can be an instance of tool or a list of instances
2186
2410
  ] = None,
2187
- max_tool_depth: int = 5
2411
+ max_tool_depth: int = 5,
2412
+ response_format: Optional[ResponseFormat] = None
2188
2413
  ) -> Generator[dict, None, None]:
2189
2414
  """
2190
2415
  Initiates a stream to receive chunks of the model's reply.
@@ -2208,6 +2433,13 @@ class Conversation:
2208
2433
  request_data: ChatOptions = {**config, **self.config}
2209
2434
  if prepared_tools:
2210
2435
  request_data |= {"tools": prepared_tools}
2436
+ if response_format:
2437
+ if not isinstance(response_format, dict):
2438
+ raise ValueError(
2439
+ "Invalid schema for response_format: "
2440
+ f"dictionary required, got {type(response_format)}"
2441
+ )
2442
+ request_data |= {"response_format": response_format}
2211
2443
  request_model = \
2212
2444
  request_data.get("model") or WriterAIManager.use_chat_model()
2213
2445
 
@@ -2341,7 +2573,7 @@ class Apps:
2341
2573
  def generate_content(
2342
2574
  self,
2343
2575
  application_id: str,
2344
- input_dict: Optional[Dict[str, str]] = None,
2576
+ input_dict: Optional[Dict[str, Optional[Union[List[str], str]]]] = None,
2345
2577
  async_job: Optional[bool] = False,
2346
2578
  config: Optional[APIOptions] = None
2347
2579
  ) -> Union[str, JobCreateResponse]:
@@ -2390,11 +2622,19 @@ class Apps:
2390
2622
  ... async_job=True
2391
2623
  ... )
2392
2624
  >>> print(response)
2393
- JobCreateResponse(id="job_456", created_at=datetime(2025, 2, 24, 12, 30, 45), status="in_progress")
2625
+ JobCreateResponse(
2626
+ id="job_456",
2627
+ created_at=datetime(2025, 2, 24, 12, 30, 45),
2628
+ status="in_progress"
2629
+ )
2394
2630
  >>> result = writer.ai.apps.retrieve_job(job_id=response.id)
2395
2631
  >>> if result.status == "completed":
2396
2632
  ... print(result.data)
2397
- {"title": "output", "suggestion": "Climate change refers to long-term shifts in temperatures and weather patterns..."}
2633
+ {
2634
+ "title": "output",
2635
+ "suggestion": "Climate change refers to long-term shifts in "
2636
+ "temperatures and weather patterns..."
2637
+ }
2398
2638
 
2399
2639
  """
2400
2640
 
@@ -2404,9 +2644,16 @@ class Apps:
2404
2644
  inputs = []
2405
2645
 
2406
2646
  for k, v in input_dict.items():
2647
+ # Convert None/empty to []
2648
+ # to avoid API 400 errors on optional inputs
2649
+ if v is None or v == "":
2650
+ value = []
2651
+ else:
2652
+ value = v if isinstance(v, list) else [v]
2653
+
2407
2654
  inputs.append(Input({
2408
2655
  "id": k,
2409
- "value": v if isinstance(v, list) else [v]
2656
+ "value": value
2410
2657
  }))
2411
2658
 
2412
2659
  if not async_job:
@@ -2707,6 +2954,7 @@ class Tools:
2707
2954
  return result.entities
2708
2955
 
2709
2956
 
2957
+ @catch_guardrail_error
2710
2958
  def complete(
2711
2959
  initial_text: str,
2712
2960
  config: Optional['CreateOptions'] = None
@@ -2753,6 +3001,7 @@ def complete(
2753
3001
  f"{response_data}")
2754
3002
 
2755
3003
 
3004
+ @catch_guardrail_error
2756
3005
  def stream_complete(
2757
3006
  initial_text: str,
2758
3007
  config: Optional['CreateOptions'] = None