vellum-ai 1.11.2__py3-none-any.whl → 1.13.5__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.

Potentially problematic release.


This version of vellum-ai might be problematic. Click here for more details.

Files changed (275) hide show
  1. vellum/__init__.py +18 -0
  2. vellum/client/README.md +1 -1
  3. vellum/client/core/client_wrapper.py +2 -2
  4. vellum/client/core/force_multipart.py +4 -2
  5. vellum/client/core/http_response.py +1 -1
  6. vellum/client/core/pydantic_utilities.py +7 -4
  7. vellum/client/errors/too_many_requests_error.py +1 -2
  8. vellum/client/reference.md +677 -76
  9. vellum/client/resources/container_images/client.py +299 -0
  10. vellum/client/resources/container_images/raw_client.py +286 -0
  11. vellum/client/resources/documents/client.py +20 -10
  12. vellum/client/resources/documents/raw_client.py +20 -10
  13. vellum/client/resources/events/raw_client.py +4 -4
  14. vellum/client/resources/integration_auth_configs/client.py +2 -0
  15. vellum/client/resources/integration_auth_configs/raw_client.py +2 -0
  16. vellum/client/resources/integration_providers/client.py +28 -2
  17. vellum/client/resources/integration_providers/raw_client.py +24 -0
  18. vellum/client/resources/integrations/client.py +52 -4
  19. vellum/client/resources/integrations/raw_client.py +61 -0
  20. vellum/client/resources/workflow_deployments/client.py +156 -0
  21. vellum/client/resources/workflow_deployments/raw_client.py +334 -0
  22. vellum/client/resources/workflows/client.py +212 -8
  23. vellum/client/resources/workflows/raw_client.py +343 -6
  24. vellum/client/types/__init__.py +18 -0
  25. vellum/client/types/api_actor_type_enum.py +1 -1
  26. vellum/client/types/check_workflow_execution_status_error.py +21 -0
  27. vellum/client/types/check_workflow_execution_status_response.py +29 -0
  28. vellum/client/types/code_execution_package_request.py +21 -0
  29. vellum/client/types/composio_execute_tool_request.py +5 -0
  30. vellum/client/types/composio_tool_definition.py +1 -0
  31. vellum/client/types/container_image_build_config.py +1 -0
  32. vellum/client/types/container_image_container_image_tag.py +1 -0
  33. vellum/client/types/dataset_row_push_request.py +3 -0
  34. vellum/client/types/document_document_to_document_index.py +1 -0
  35. vellum/client/types/integration_name.py +24 -0
  36. vellum/client/types/node_execution_fulfilled_body.py +1 -0
  37. vellum/client/types/node_execution_log_body.py +24 -0
  38. vellum/client/types/node_execution_log_event.py +47 -0
  39. vellum/client/types/prompt_deployment_release_prompt_deployment.py +1 -0
  40. vellum/client/types/runner_config_request.py +24 -0
  41. vellum/client/types/severity_enum.py +5 -0
  42. vellum/client/types/slim_composio_tool_definition.py +1 -0
  43. vellum/client/types/slim_document_document_to_document_index.py +2 -0
  44. vellum/client/types/type_checker_enum.py +5 -0
  45. vellum/client/types/vellum_audio.py +5 -1
  46. vellum/client/types/vellum_audio_request.py +5 -1
  47. vellum/client/types/vellum_document.py +5 -1
  48. vellum/client/types/vellum_document_request.py +5 -1
  49. vellum/client/types/vellum_image.py +5 -1
  50. vellum/client/types/vellum_image_request.py +5 -1
  51. vellum/client/types/vellum_node_execution_event.py +2 -0
  52. vellum/client/types/vellum_variable.py +5 -0
  53. vellum/client/types/vellum_variable_extensions.py +1 -0
  54. vellum/client/types/vellum_variable_type.py +1 -0
  55. vellum/client/types/vellum_video.py +5 -1
  56. vellum/client/types/vellum_video_request.py +5 -1
  57. vellum/client/types/workflow_deployment_release_workflow_deployment.py +1 -0
  58. vellum/client/types/workflow_event.py +2 -0
  59. vellum/client/types/workflow_execution_fulfilled_body.py +1 -0
  60. vellum/client/types/workflow_result_event_output_data_array.py +1 -1
  61. vellum/client/types/workflow_result_event_output_data_chat_history.py +1 -1
  62. vellum/client/types/workflow_result_event_output_data_error.py +1 -1
  63. vellum/client/types/workflow_result_event_output_data_function_call.py +1 -1
  64. vellum/client/types/workflow_result_event_output_data_json.py +1 -1
  65. vellum/client/types/workflow_result_event_output_data_number.py +1 -1
  66. vellum/client/types/workflow_result_event_output_data_search_results.py +1 -1
  67. vellum/client/types/workflow_result_event_output_data_string.py +1 -1
  68. vellum/client/types/workflow_sandbox_execute_node_response.py +8 -0
  69. vellum/plugins/vellum_mypy.py +37 -2
  70. vellum/types/check_workflow_execution_status_error.py +3 -0
  71. vellum/types/check_workflow_execution_status_response.py +3 -0
  72. vellum/types/code_execution_package_request.py +3 -0
  73. vellum/types/node_execution_log_body.py +3 -0
  74. vellum/types/node_execution_log_event.py +3 -0
  75. vellum/types/runner_config_request.py +3 -0
  76. vellum/types/severity_enum.py +3 -0
  77. vellum/types/type_checker_enum.py +3 -0
  78. vellum/types/workflow_sandbox_execute_node_response.py +3 -0
  79. vellum/utils/files/mixin.py +26 -0
  80. vellum/utils/files/tests/test_mixin.py +62 -0
  81. vellum/utils/tests/test_vellum_client.py +95 -0
  82. vellum/utils/uuid.py +19 -2
  83. vellum/utils/vellum_client.py +10 -3
  84. vellum/workflows/__init__.py +7 -1
  85. vellum/workflows/descriptors/base.py +86 -0
  86. vellum/workflows/descriptors/tests/test_utils.py +9 -0
  87. vellum/workflows/errors/tests/__init__.py +0 -0
  88. vellum/workflows/errors/tests/test_types.py +52 -0
  89. vellum/workflows/errors/types.py +1 -0
  90. vellum/workflows/events/node.py +24 -0
  91. vellum/workflows/events/tests/test_event.py +123 -0
  92. vellum/workflows/events/types.py +2 -1
  93. vellum/workflows/events/workflow.py +28 -2
  94. vellum/workflows/expressions/add.py +3 -0
  95. vellum/workflows/expressions/tests/test_add.py +24 -0
  96. vellum/workflows/graph/graph.py +26 -5
  97. vellum/workflows/graph/tests/test_graph.py +228 -1
  98. vellum/workflows/inputs/base.py +22 -6
  99. vellum/workflows/inputs/dataset_row.py +121 -16
  100. vellum/workflows/inputs/tests/test_inputs.py +3 -3
  101. vellum/workflows/integrations/tests/test_vellum_integration_service.py +84 -0
  102. vellum/workflows/integrations/vellum_integration_service.py +12 -1
  103. vellum/workflows/loaders/base.py +2 -0
  104. vellum/workflows/nodes/bases/base.py +37 -16
  105. vellum/workflows/nodes/bases/tests/test_base_node.py +104 -1
  106. vellum/workflows/nodes/core/inline_subworkflow_node/node.py +1 -0
  107. vellum/workflows/nodes/core/inline_subworkflow_node/tests/test_node.py +1 -1
  108. vellum/workflows/nodes/core/map_node/node.py +7 -5
  109. vellum/workflows/nodes/core/map_node/tests/test_node.py +33 -0
  110. vellum/workflows/nodes/core/retry_node/node.py +1 -0
  111. vellum/workflows/nodes/core/try_node/node.py +1 -0
  112. vellum/workflows/nodes/displayable/api_node/node.py +3 -2
  113. vellum/workflows/nodes/displayable/api_node/tests/test_api_node.py +38 -0
  114. vellum/workflows/nodes/displayable/bases/api_node/node.py +1 -1
  115. vellum/workflows/nodes/displayable/bases/base_prompt_node/node.py +18 -1
  116. vellum/workflows/nodes/displayable/bases/inline_prompt_node/node.py +109 -2
  117. vellum/workflows/nodes/displayable/bases/prompt_deployment_node.py +13 -2
  118. vellum/workflows/nodes/displayable/code_execution_node/node.py +9 -15
  119. vellum/workflows/nodes/displayable/code_execution_node/tests/test_node.py +65 -24
  120. vellum/workflows/nodes/displayable/code_execution_node/utils.py +3 -0
  121. vellum/workflows/nodes/displayable/final_output_node/node.py +24 -69
  122. vellum/workflows/nodes/displayable/final_output_node/tests/test_node.py +53 -3
  123. vellum/workflows/nodes/displayable/note_node/node.py +4 -1
  124. vellum/workflows/nodes/displayable/subworkflow_deployment_node/node.py +16 -5
  125. vellum/workflows/nodes/displayable/tests/test_text_prompt_deployment_node.py +47 -0
  126. vellum/workflows/nodes/displayable/tool_calling_node/node.py +74 -34
  127. vellum/workflows/nodes/displayable/tool_calling_node/tests/test_node.py +204 -8
  128. vellum/workflows/nodes/displayable/tool_calling_node/utils.py +92 -71
  129. vellum/workflows/nodes/mocks.py +47 -213
  130. vellum/workflows/nodes/tests/test_mocks.py +0 -177
  131. vellum/workflows/nodes/utils.py +23 -8
  132. vellum/workflows/outputs/base.py +36 -3
  133. vellum/workflows/references/environment_variable.py +1 -11
  134. vellum/workflows/references/lazy.py +8 -0
  135. vellum/workflows/references/state_value.py +24 -1
  136. vellum/workflows/references/tests/test_lazy.py +58 -0
  137. vellum/workflows/references/trigger.py +8 -3
  138. vellum/workflows/references/workflow_input.py +8 -0
  139. vellum/workflows/resolvers/resolver.py +13 -3
  140. vellum/workflows/resolvers/tests/test_resolver.py +31 -0
  141. vellum/workflows/runner/runner.py +159 -14
  142. vellum/workflows/runner/tests/__init__.py +0 -0
  143. vellum/workflows/runner/tests/test_runner.py +170 -0
  144. vellum/workflows/sandbox.py +7 -8
  145. vellum/workflows/state/base.py +89 -30
  146. vellum/workflows/state/context.py +74 -3
  147. vellum/workflows/state/tests/test_state.py +269 -1
  148. vellum/workflows/tests/test_dataset_row.py +8 -7
  149. vellum/workflows/tests/test_sandbox.py +97 -8
  150. vellum/workflows/triggers/__init__.py +2 -1
  151. vellum/workflows/triggers/base.py +160 -28
  152. vellum/workflows/triggers/chat_message.py +141 -0
  153. vellum/workflows/triggers/integration.py +12 -0
  154. vellum/workflows/triggers/manual.py +3 -1
  155. vellum/workflows/triggers/schedule.py +3 -1
  156. vellum/workflows/triggers/tests/test_chat_message.py +257 -0
  157. vellum/workflows/types/core.py +18 -0
  158. vellum/workflows/types/definition.py +6 -13
  159. vellum/workflows/types/generics.py +12 -0
  160. vellum/workflows/types/tests/test_utils.py +12 -0
  161. vellum/workflows/types/utils.py +32 -2
  162. vellum/workflows/types/workflow_metadata.py +124 -0
  163. vellum/workflows/utils/functions.py +152 -16
  164. vellum/workflows/utils/pydantic_schema.py +19 -1
  165. vellum/workflows/utils/tests/test_functions.py +123 -8
  166. vellum/workflows/utils/tests/test_validate.py +79 -0
  167. vellum/workflows/utils/tests/test_vellum_variables.py +62 -2
  168. vellum/workflows/utils/uuids.py +90 -0
  169. vellum/workflows/utils/validate.py +108 -0
  170. vellum/workflows/utils/vellum_variables.py +96 -16
  171. vellum/workflows/workflows/base.py +177 -35
  172. vellum/workflows/workflows/tests/test_base_workflow.py +51 -0
  173. {vellum_ai-1.11.2.dist-info → vellum_ai-1.13.5.dist-info}/METADATA +6 -1
  174. {vellum_ai-1.11.2.dist-info → vellum_ai-1.13.5.dist-info}/RECORD +274 -227
  175. vellum_cli/__init__.py +21 -0
  176. vellum_cli/config.py +16 -2
  177. vellum_cli/pull.py +2 -0
  178. vellum_cli/push.py +23 -10
  179. vellum_cli/tests/conftest.py +8 -13
  180. vellum_cli/tests/test_image_push.py +4 -11
  181. vellum_cli/tests/test_pull.py +83 -68
  182. vellum_cli/tests/test_push.py +251 -2
  183. vellum_ee/assets/node-definitions.json +225 -12
  184. vellum_ee/scripts/generate_node_definitions.py +15 -3
  185. vellum_ee/workflows/display/base.py +4 -3
  186. vellum_ee/workflows/display/nodes/base_node_display.py +44 -11
  187. vellum_ee/workflows/display/nodes/tests/test_base_node_display.py +93 -0
  188. vellum_ee/workflows/display/nodes/types.py +1 -0
  189. vellum_ee/workflows/display/nodes/vellum/__init__.py +0 -2
  190. vellum_ee/workflows/display/nodes/vellum/base_adornment_node.py +5 -2
  191. vellum_ee/workflows/display/nodes/vellum/code_execution_node.py +1 -1
  192. vellum_ee/workflows/display/nodes/vellum/inline_prompt_node.py +10 -2
  193. vellum_ee/workflows/display/nodes/vellum/inline_subworkflow_node.py +17 -14
  194. vellum_ee/workflows/display/nodes/vellum/map_node.py +2 -0
  195. vellum_ee/workflows/display/nodes/vellum/note_node.py +18 -3
  196. vellum_ee/workflows/display/nodes/vellum/subworkflow_deployment_node.py +37 -14
  197. vellum_ee/workflows/display/nodes/vellum/tests/test_code_execution_node.py +62 -2
  198. vellum_ee/workflows/display/nodes/vellum/tests/test_final_output_node.py +136 -0
  199. vellum_ee/workflows/display/nodes/vellum/tests/test_note_node.py +44 -7
  200. vellum_ee/workflows/display/nodes/vellum/tests/test_prompt_node.py +5 -13
  201. vellum_ee/workflows/display/nodes/vellum/tests/test_subworkflow_deployment_node.py +27 -17
  202. vellum_ee/workflows/display/nodes/vellum/tests/test_tool_calling_node.py +145 -22
  203. vellum_ee/workflows/display/nodes/vellum/tests/test_utils.py +107 -2
  204. vellum_ee/workflows/display/nodes/vellum/utils.py +54 -12
  205. vellum_ee/workflows/display/tests/test_base_workflow_display.py +13 -16
  206. vellum_ee/workflows/display/tests/test_json_schema_validation.py +190 -0
  207. vellum_ee/workflows/display/tests/test_mocks.py +912 -0
  208. vellum_ee/workflows/display/tests/workflow_serialization/generic_nodes/test_adornments_serialization.py +14 -2
  209. vellum_ee/workflows/display/tests/workflow_serialization/generic_nodes/test_attributes_serialization.py +109 -0
  210. vellum_ee/workflows/display/tests/workflow_serialization/generic_nodes/test_outputs_serialization.py +3 -0
  211. vellum_ee/workflows/display/tests/workflow_serialization/generic_nodes/test_ports_serialization.py +187 -1
  212. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_api_node_serialization.py +34 -325
  213. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_code_execution_node_serialization.py +42 -393
  214. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_conditional_node_serialization.py +13 -315
  215. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_default_state_serialization.py +2 -122
  216. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_error_node_serialization.py +24 -115
  217. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_generic_node_serialization.py +4 -93
  218. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_guardrail_node_serialization.py +7 -80
  219. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_inline_prompt_node_serialization.py +9 -101
  220. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_inline_subworkflow_serialization.py +77 -308
  221. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_map_node_serialization.py +62 -324
  222. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_merge_node_serialization.py +3 -82
  223. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_prompt_deployment_serialization.py +4 -142
  224. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_search_node_serialization.py +1 -61
  225. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_set_state_node_serialization.py +4 -4
  226. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_subworkflow_deployment_serialization.py +205 -134
  227. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_templating_node_serialization.py +34 -146
  228. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_terminal_node_serialization.py +2 -0
  229. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_tool_calling_node_composio_serialization.py +8 -6
  230. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_tool_calling_node_inline_workflow_serialization.py +137 -266
  231. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_tool_calling_node_inline_workflow_tool_wrapper_serialization.py +84 -0
  232. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_tool_calling_node_mcp_serialization.py +55 -16
  233. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_tool_calling_node_serialization.py +15 -1
  234. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_tool_calling_node_tool_wrapper_serialization.py +71 -0
  235. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_tool_calling_node_vellum_integration_serialization.py +119 -0
  236. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_tool_calling_node_workflow_deployment_serialization.py +1 -1
  237. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_try_node_serialization.py +0 -2
  238. vellum_ee/workflows/display/tests/workflow_serialization/test_chat_message_dict_reference_serialization.py +22 -1
  239. vellum_ee/workflows/display/tests/workflow_serialization/test_chat_message_trigger_serialization.py +412 -0
  240. vellum_ee/workflows/display/tests/workflow_serialization/test_code_tool_node_reference_error.py +106 -0
  241. vellum_ee/workflows/display/tests/workflow_serialization/test_complex_terminal_node_serialization.py +9 -41
  242. vellum_ee/workflows/display/tests/workflow_serialization/test_duplicate_trigger_name_validation.py +208 -0
  243. vellum_ee/workflows/display/tests/workflow_serialization/test_final_output_node_not_referenced_by_workflow_outputs.py +45 -0
  244. vellum_ee/workflows/display/tests/workflow_serialization/test_infinite_loop_validation.py +66 -0
  245. vellum_ee/workflows/display/tests/workflow_serialization/test_int_input_serialization.py +40 -0
  246. vellum_ee/workflows/display/tests/workflow_serialization/test_integration_trigger_serialization.py +8 -14
  247. vellum_ee/workflows/display/tests/workflow_serialization/test_integration_trigger_validation.py +173 -0
  248. vellum_ee/workflows/display/tests/workflow_serialization/test_integration_trigger_with_entrypoint_node_id.py +16 -13
  249. vellum_ee/workflows/display/tests/workflow_serialization/test_list_vellum_document_serialization.py +5 -1
  250. vellum_ee/workflows/display/tests/workflow_serialization/test_manual_trigger_serialization.py +12 -2
  251. vellum_ee/workflows/display/tests/workflow_serialization/test_multi_trigger_same_node_serialization.py +111 -0
  252. vellum_ee/workflows/display/tests/workflow_serialization/test_no_triggers_no_entrypoint_validation.py +64 -0
  253. vellum_ee/workflows/display/tests/workflow_serialization/test_partial_workflow_meta_display_override.py +55 -0
  254. vellum_ee/workflows/display/tests/workflow_serialization/test_sandbox_dataset_mocks_serialization.py +268 -0
  255. vellum_ee/workflows/display/tests/workflow_serialization/test_sandbox_invalid_pdf_data_url.py +49 -0
  256. vellum_ee/workflows/display/tests/workflow_serialization/test_sandbox_validation_errors.py +112 -0
  257. vellum_ee/workflows/display/tests/workflow_serialization/test_scheduled_trigger_serialization.py +25 -16
  258. vellum_ee/workflows/display/tests/workflow_serialization/test_terminal_node_in_unused_graphs_serialization.py +53 -0
  259. vellum_ee/workflows/display/utils/exceptions.py +34 -0
  260. vellum_ee/workflows/display/utils/expressions.py +463 -52
  261. vellum_ee/workflows/display/utils/metadata.py +98 -33
  262. vellum_ee/workflows/display/utils/tests/test_metadata.py +31 -0
  263. vellum_ee/workflows/display/utils/triggers.py +153 -0
  264. vellum_ee/workflows/display/utils/vellum.py +59 -5
  265. vellum_ee/workflows/display/workflows/base_workflow_display.py +656 -254
  266. vellum_ee/workflows/display/workflows/get_vellum_workflow_display_class.py +26 -0
  267. vellum_ee/workflows/display/workflows/tests/test_workflow_display.py +77 -29
  268. vellum_ee/workflows/server/namespaces.py +18 -0
  269. vellum_ee/workflows/tests/test_display_meta.py +2 -0
  270. vellum_ee/workflows/tests/test_serialize_module.py +174 -7
  271. vellum_ee/workflows/tests/test_server.py +0 -3
  272. vellum_ee/workflows/display/nodes/vellum/function_node.py +0 -14
  273. {vellum_ai-1.11.2.dist-info → vellum_ai-1.13.5.dist-info}/LICENSE +0 -0
  274. {vellum_ai-1.11.2.dist-info → vellum_ai-1.13.5.dist-info}/WHEEL +0 -0
  275. {vellum_ai-1.11.2.dist-info → vellum_ai-1.13.5.dist-info}/entry_points.txt +0 -0
@@ -2,8 +2,12 @@ import pytest
2
2
  from copy import deepcopy
3
3
  import json
4
4
  from queue import Queue
5
- from typing import Dict, List, cast
5
+ import threading
6
+ from typing import Any, Dict, List, Optional, cast
6
7
 
8
+ from pydantic import Field
9
+
10
+ from vellum import ChatMessage
7
11
  from vellum.utils.json_encoder import VellumJsonEncoder
8
12
  from vellum.workflows.constants import undefined
9
13
  from vellum.workflows.nodes.bases import BaseNode
@@ -243,3 +247,267 @@ def test_state_deepcopy_handles_undefined_values():
243
247
 
244
248
  # THEN the undefined values are preserved
245
249
  assert deepcopied_state.meta.node_outputs[MockNode.Outputs.baz] == {"foo": undefined}
250
+
251
+
252
+ def test_base_state_initializes_field_with_default_factory():
253
+ """Test that BaseState properly initializes fields with Field(default_factory=...)."""
254
+
255
+ # GIVEN a state class with fields using Field(default_factory=...)
256
+ class TestState(BaseState):
257
+ chat_history: List[str] = Field(default_factory=list)
258
+ items: Dict[str, int] = Field(default_factory=dict)
259
+ counter: int = Field(default_factory=lambda: 0)
260
+
261
+ # WHEN we create a state instance without providing values
262
+ state = TestState()
263
+
264
+ # THEN the fields should be initialized with the factory results, not FieldInfo objects
265
+ assert isinstance(state.chat_history, list)
266
+ assert state.chat_history == []
267
+ assert isinstance(state.items, dict)
268
+ assert state.items == {}
269
+ assert isinstance(state.counter, int)
270
+ assert state.counter == 0
271
+
272
+ # AND we should be able to modify them
273
+ state.chat_history.append("message1")
274
+ state.items["key1"] = 1
275
+ state.counter += 1
276
+
277
+ assert state.chat_history == ["message1"]
278
+ assert state.items == {"key1": 1}
279
+ assert state.counter == 1
280
+
281
+
282
+ def test_base_state_field_with_default_factory_creates_separate_instances():
283
+ """Test that Field(default_factory=...) creates separate instances for each state."""
284
+
285
+ # GIVEN a state class with Field(default_factory=list)
286
+ class TestState(BaseState):
287
+ items: List[str] = Field(default_factory=list)
288
+
289
+ # WHEN we create two state instances
290
+ state1 = TestState()
291
+ state2 = TestState()
292
+
293
+ # THEN they should have separate list instances
294
+ assert state1.items is not state2.items
295
+
296
+ # AND modifying one should not affect the other
297
+ state1.items.append("item1")
298
+ assert state1.items == ["item1"]
299
+ assert state2.items == []
300
+
301
+
302
+ class BlockingValue:
303
+ """A value that blocks during deepcopy until signaled to proceed."""
304
+
305
+ def __init__(self, entered_event: threading.Event, proceed_event: threading.Event):
306
+ self.entered_event = entered_event
307
+ self.proceed_event = proceed_event
308
+
309
+ def __deepcopy__(self, memo: Any) -> "BlockingValue":
310
+ self.entered_event.set()
311
+ self.proceed_event.wait(timeout=5.0)
312
+ return BlockingValue(self.entered_event, self.proceed_event)
313
+
314
+
315
+ def test_state_snapshot__concurrent_mutation_during_deepcopy():
316
+ """Test that concurrent mutations during deepcopy don't cause RuntimeError."""
317
+
318
+ # GIVEN a state with a dict containing a blocking value
319
+ class TestState(BaseState):
320
+ data: Dict[str, Any] = Field(default_factory=dict)
321
+
322
+ state = TestState()
323
+
324
+ entered_event = threading.Event()
325
+ proceed_event = threading.Event()
326
+ state.data["blocking"] = BlockingValue(entered_event, proceed_event)
327
+ state.data["other"] = "value"
328
+
329
+ snapshot_exception: List[Exception] = []
330
+ mutation_completed = threading.Event()
331
+
332
+ def snapshot_thread_fn() -> None:
333
+ try:
334
+ with state.__lock__:
335
+ deepcopy(state)
336
+ except Exception as e:
337
+ snapshot_exception.append(e)
338
+
339
+ def mutation_thread_fn() -> None:
340
+ state.data["new_key"] = "new_value"
341
+ mutation_completed.set()
342
+
343
+ # WHEN we start a snapshot (deepcopy) in one thread
344
+ snapshot_thread = threading.Thread(target=snapshot_thread_fn)
345
+ snapshot_thread.start()
346
+
347
+ # AND wait for the deepcopy to be in progress (blocked on our blocking value)
348
+ entered_event.wait(timeout=5.0)
349
+
350
+ # AND try to mutate the dict from another thread
351
+ mutation_thread = threading.Thread(target=mutation_thread_fn)
352
+ mutation_thread.start()
353
+
354
+ # THEN the mutation should block waiting for the lock (not complete immediately)
355
+ mutation_completed.wait(timeout=0.2)
356
+ mutation_blocked = not mutation_completed.is_set()
357
+
358
+ # AND when we allow the deepcopy to proceed
359
+ proceed_event.set()
360
+ snapshot_thread.join(timeout=5.0)
361
+ mutation_thread.join(timeout=5.0)
362
+
363
+ # THEN the mutation should have been blocked by the lock
364
+ assert mutation_blocked, "Mutation should block while deepcopy holds the lock"
365
+
366
+ # AND no exception should have been raised during snapshot
367
+ assert len(snapshot_exception) == 0, f"Snapshot raised exception: {snapshot_exception}"
368
+
369
+
370
+ def test_state_deepcopy__cloned_state_uses_own_snapshot_callback():
371
+ """Test that deepcopied state's snapshottable containers use the clone's callback."""
372
+
373
+ # GIVEN a state with a snapshottable dict attribute
374
+ original_snapshot_count = 0
375
+ clone_snapshot_count = 0
376
+
377
+ class TestState(BaseState):
378
+ data: Dict[str, int] = Field(default_factory=dict)
379
+
380
+ state = TestState()
381
+ state.data["key1"] = 1
382
+
383
+ def original_callback(state_copy: BaseState, deltas: List[StateDelta]) -> None:
384
+ nonlocal original_snapshot_count
385
+ original_snapshot_count += 1
386
+
387
+ state.__snapshot_callback__ = original_callback
388
+
389
+ # WHEN we deepcopy the state
390
+ cloned_state = deepcopy(state)
391
+
392
+ def clone_callback(state_copy: BaseState, deltas: List[StateDelta]) -> None:
393
+ nonlocal clone_snapshot_count
394
+ clone_snapshot_count += 1
395
+
396
+ cloned_state.__snapshot_callback__ = clone_callback
397
+
398
+ # AND reset counters
399
+ original_snapshot_count = 0
400
+ clone_snapshot_count = 0
401
+
402
+ # AND mutate the cloned state's snapshottable dict
403
+ cloned_state.data["key2"] = 2
404
+
405
+ # THEN only the clone's callback should be invoked
406
+ assert clone_snapshot_count == 1, "Clone's callback should be invoked"
407
+ assert original_snapshot_count == 0, "Original's callback should not be invoked"
408
+
409
+
410
+ def test_state_snapshot__top_level_attribute_assignment_blocks_during_deepcopy():
411
+ """Test that top-level attribute assignments block while deepcopy holds the lock."""
412
+
413
+ # GIVEN a state with a blocking value in a dict attribute
414
+ class TestState(BaseState):
415
+ data: Dict[str, Any] = Field(default_factory=dict)
416
+ counter: int = 0
417
+
418
+ state = TestState()
419
+
420
+ entered_event = threading.Event()
421
+ proceed_event = threading.Event()
422
+ state.data["blocking"] = BlockingValue(entered_event, proceed_event)
423
+
424
+ mutation_completed = threading.Event()
425
+
426
+ def snapshot_thread_fn() -> None:
427
+ with state.__lock__:
428
+ deepcopy(state)
429
+
430
+ def mutation_thread_fn() -> None:
431
+ state.__is_quiet__ = True
432
+ state.counter = 42
433
+ mutation_completed.set()
434
+
435
+ # WHEN we start a snapshot (deepcopy) in one thread
436
+ snapshot_thread = threading.Thread(target=snapshot_thread_fn)
437
+ snapshot_thread.start()
438
+
439
+ # AND wait for the deepcopy to be in progress (blocked on our blocking value)
440
+ entered_event.wait(timeout=5.0)
441
+
442
+ # AND try to assign a top-level attribute from another thread
443
+ mutation_thread = threading.Thread(target=mutation_thread_fn)
444
+ mutation_thread.start()
445
+
446
+ # THEN the mutation should block waiting for the lock (not complete immediately)
447
+ mutation_completed.wait(timeout=0.2)
448
+ mutation_blocked = not mutation_completed.is_set()
449
+
450
+ # AND when we allow the deepcopy to proceed
451
+ proceed_event.set()
452
+ snapshot_thread.join(timeout=5.0)
453
+ mutation_thread.join(timeout=5.0)
454
+
455
+ # THEN the mutation should have been blocked by the lock
456
+ assert mutation_blocked, "Top-level attribute assignment should block while deepcopy holds the lock"
457
+
458
+
459
+ def test_base_state_chat_history_with_default_factory_initializes_to_list():
460
+ """
461
+ Tests that a chat_history state variable with Optional[list[ChatMessage]] = Field(default_factory=list)
462
+ initializes to an empty list instead of None.
463
+ """
464
+
465
+ # GIVEN a state class with chat_history using Field(default_factory=list)
466
+ class TestState(BaseState):
467
+ chat_history: Optional[List[ChatMessage]] = Field(default_factory=list) # type: ignore[arg-type]
468
+
469
+ # WHEN we create a state instance without providing a value
470
+ state = TestState()
471
+
472
+ # THEN the chat_history should be an empty list, not None
473
+ assert state.chat_history is not None
474
+ assert isinstance(state.chat_history, list)
475
+ assert state.chat_history == []
476
+
477
+ # AND we should be able to append ChatMessage objects to it
478
+ chat_history = state.chat_history
479
+ chat_history.append(ChatMessage(role="USER", text="Hello"))
480
+ assert len(chat_history) == 1
481
+ assert chat_history[0].role == "USER"
482
+ assert chat_history[0].text == "Hello"
483
+
484
+
485
+ def test_base_state_chat_history_with_default_factory_creates_separate_instances():
486
+ """
487
+ Tests that Field(default_factory=list) creates separate list instances for each state,
488
+ avoiding the mutable default argument issue.
489
+ """
490
+
491
+ # GIVEN a state class with chat_history using Field(default_factory=list)
492
+ class TestState(BaseState):
493
+ chat_history: Optional[List[ChatMessage]] = Field(default_factory=list) # type: ignore[arg-type]
494
+
495
+ # WHEN we create two state instances
496
+ state1 = TestState()
497
+ state2 = TestState()
498
+
499
+ # THEN they should have separate list instances
500
+ assert state1.chat_history is not state2.chat_history
501
+
502
+ # AND modifying one should not affect the other
503
+ chat_history1 = state1.chat_history
504
+ chat_history2 = state2.chat_history
505
+ assert chat_history1 is not None
506
+ assert chat_history2 is not None
507
+ chat_history1.append(ChatMessage(role="USER", text="Message 1"))
508
+ chat_history2.append(ChatMessage(role="ASSISTANT", text="Message 2"))
509
+
510
+ assert len(chat_history1) == 1
511
+ assert len(chat_history2) == 1
512
+ assert chat_history1[0].text == "Message 1"
513
+ assert chat_history2[0].text == "Message 2"
@@ -152,7 +152,7 @@ def test_dataset_row_with_dict_inputs():
152
152
 
153
153
  def test_dataset_row_with_node_output_mocks():
154
154
  """
155
- Test that DatasetRow can be created with node_output_mocks and properly serialized.
155
+ Test that DatasetRow can be created with mocks and properly serialized.
156
156
  """
157
157
 
158
158
  # GIVEN a node with outputs
@@ -168,7 +168,7 @@ def test_dataset_row_with_node_output_mocks():
168
168
 
169
169
  test_inputs = TestInputs(message="test message")
170
170
 
171
- dataset_row = DatasetRow(label="test_with_mocks", inputs=test_inputs, node_output_mocks=[mock_output])
171
+ dataset_row = DatasetRow(label="test_with_mocks", inputs=test_inputs, mocks=[mock_output])
172
172
 
173
173
  serialized_dict = dataset_row.model_dump()
174
174
 
@@ -176,14 +176,15 @@ def test_dataset_row_with_node_output_mocks():
176
176
  assert serialized_dict["label"] == "test_with_mocks"
177
177
  assert serialized_dict["inputs"]["message"] == "test message"
178
178
 
179
- # AND the node_output_mocks should be present in the serialized dict
180
- assert "node_output_mocks" in serialized_dict
181
- assert serialized_dict["node_output_mocks"] is not None
182
- assert len(serialized_dict["node_output_mocks"]) == 1
179
+ # AND the mocks should be present in the serialized dict
180
+ assert "mocks" in serialized_dict
181
+ assert serialized_dict["mocks"] is not None
182
+ assert len(serialized_dict["mocks"]) == 1
183
183
 
184
184
  # AND the mock output should be serialized as a dict with the correct structure
185
- mock_data = serialized_dict["node_output_mocks"][0]
185
+ mock_data = serialized_dict["mocks"][0]
186
186
  assert mock_data == {
187
+ "type": "NODE_EXECUTION",
187
188
  "node_id": str(DummyNode.__id__),
188
189
  "when_condition": {"type": "CONSTANT_VALUE", "value": {"type": "JSON", "value": True}},
189
190
  "then_outputs": {"result": "mocked output"},
@@ -129,12 +129,15 @@ def test_sandbox_runner_with_workflow_trigger(mock_logger):
129
129
  class Outputs(BaseWorkflow.Outputs):
130
130
  final_output = StartNode.Outputs.result
131
131
 
132
- # AND a dataset with workflow_trigger
132
+ # AND a trigger instance
133
+ trigger_instance = MySchedule(current_run_at=datetime.min, next_run_at=datetime.now())
134
+
135
+ # AND a dataset with workflow_trigger instance
133
136
  dataset = [
134
137
  DatasetRow(
135
138
  label="test_row",
136
139
  inputs={"current_run_at": datetime.min, "next_run_at": datetime.now()},
137
- workflow_trigger=MySchedule,
140
+ workflow_trigger=trigger_instance,
138
141
  ),
139
142
  ]
140
143
 
@@ -151,13 +154,99 @@ def test_sandbox_runner_with_workflow_trigger(mock_logger):
151
154
  "final_output: 0001-01-01 00:00:00",
152
155
  ]
153
156
 
154
- # AND the dataset row should still have the trigger class
155
- assert dataset[0].workflow_trigger == MySchedule
157
+ # AND the dataset row should have the trigger instance
158
+ assert dataset[0].workflow_trigger == trigger_instance
159
+ assert isinstance(dataset[0].workflow_trigger, MySchedule)
160
+
161
+
162
+ def test_sandbox_runner_with_trigger_instance(mock_logger):
163
+ """
164
+ Test that WorkflowSandboxRunner can run with DatasetRow containing trigger instance.
165
+ """
166
+
167
+ # GIVEN we capture the logs to stdout
168
+ logs = []
169
+ mock_logger.return_value.info.side_effect = lambda msg: logs.append(msg)
170
+
171
+ # AND a trigger class
172
+ class MySchedule(ScheduleTrigger):
173
+ class Config(ScheduleTrigger.Config):
174
+ cron = "* * * * *"
175
+ timezone = "UTC"
176
+
177
+ # AND a workflow that uses the trigger
178
+ class StartNode(BaseNode):
179
+ class Outputs(BaseNode.Outputs):
180
+ result = MySchedule.current_run_at
181
+
182
+ class Workflow(BaseWorkflow):
183
+ graph = MySchedule >> StartNode
184
+
185
+ class Outputs(BaseWorkflow.Outputs):
186
+ final_output = StartNode.Outputs.result
187
+
188
+ # AND a trigger instance
189
+ trigger_instance = MySchedule(current_run_at=datetime.min, next_run_at=datetime.now())
190
+
191
+ # AND a dataset with trigger instance
192
+ dataset = [
193
+ DatasetRow(
194
+ label="test_row_with_instance",
195
+ inputs={"current_run_at": datetime.min, "next_run_at": datetime.now()},
196
+ workflow_trigger=trigger_instance,
197
+ ),
198
+ ]
199
+
200
+ # WHEN we run the sandbox with the DatasetRow containing trigger instance
201
+ runner = WorkflowSandboxRunner(workflow=Workflow(), dataset=dataset)
202
+ runner.run()
203
+
204
+ # THEN the workflow should run successfully
205
+ assert logs == [
206
+ "Just started Node: StartNode",
207
+ "Just finished Node: StartNode",
208
+ "Workflow fulfilled!",
209
+ "----------------------------------",
210
+ "final_output: 0001-01-01 00:00:00",
211
+ ]
212
+
213
+ # AND the dataset row should have the trigger instance
214
+ assert dataset[0].workflow_trigger == trigger_instance
215
+ assert isinstance(dataset[0].workflow_trigger, MySchedule)
216
+
217
+
218
+ def test_dataset_row_serialization_with_workflow_trigger():
219
+ """
220
+ Test that DatasetRow serializes workflow_trigger field to workflow_trigger_id.
221
+ """
222
+
223
+ # GIVEN a trigger class
224
+ class MySchedule(ScheduleTrigger):
225
+ class Config(ScheduleTrigger.Config):
226
+ cron = "* * * * *"
227
+ timezone = "UTC"
228
+
229
+ # AND a trigger instance
230
+ trigger_instance = MySchedule(current_run_at=datetime.min, next_run_at=datetime.now())
231
+
232
+ # AND a DatasetRow constructed with workflow_trigger
233
+ dataset_row = DatasetRow(
234
+ label="test_serialization",
235
+ inputs={"foo": "bar"},
236
+ workflow_trigger=trigger_instance,
237
+ )
238
+
239
+ # WHEN we serialize the DatasetRow
240
+ serialized = dataset_row.model_dump()
241
+
242
+ # THEN the serialized dict should contain workflow_trigger_id
243
+ assert "workflow_trigger_id" in serialized
244
+ assert serialized["workflow_trigger_id"] == str(MySchedule.__id__)
156
245
 
157
246
 
158
247
  def test_sandbox_runner_with_node_output_mocks(mock_logger, mocker):
159
248
  """
160
- Tests that WorkflowSandboxRunner passes node_output_mocks from DatasetRow to workflow.stream().
249
+ Tests that WorkflowSandboxRunner passes mocks from DatasetRow to workflow.stream().
161
250
  """
162
251
 
163
252
  class Inputs(BaseInputs):
@@ -175,12 +264,12 @@ def test_sandbox_runner_with_node_output_mocks(mock_logger, mocker):
175
264
 
176
265
  mock_outputs = TestNode.Outputs(result="mocked_result")
177
266
 
178
- # AND a dataset with node_output_mocks
267
+ # AND a dataset with mocks
179
268
  dataset = [
180
269
  DatasetRow(
181
270
  label="test_with_mocks",
182
271
  inputs={"message": "test"},
183
- node_output_mocks=[mock_outputs],
272
+ mocks=[mock_outputs],
184
273
  ),
185
274
  ]
186
275
 
@@ -189,7 +278,7 @@ def test_sandbox_runner_with_node_output_mocks(mock_logger, mocker):
189
278
  stream_mock = MagicMock(return_value=original_stream(inputs=Inputs(message="test")))
190
279
  mocker.patch.object(workflow_instance, "stream", stream_mock)
191
280
 
192
- # WHEN we run the sandbox with the DatasetRow containing node_output_mocks
281
+ # WHEN we run the sandbox with the DatasetRow containing mocks
193
282
  runner = WorkflowSandboxRunner(workflow=workflow_instance, dataset=dataset)
194
283
  runner.run()
195
284
 
@@ -1,6 +1,7 @@
1
1
  from vellum.workflows.triggers.base import BaseTrigger
2
+ from vellum.workflows.triggers.chat_message import ChatMessageTrigger
2
3
  from vellum.workflows.triggers.integration import IntegrationTrigger
3
4
  from vellum.workflows.triggers.manual import ManualTrigger
4
5
  from vellum.workflows.triggers.schedule import ScheduleTrigger
5
6
 
6
- __all__ = ["BaseTrigger", "IntegrationTrigger", "ManualTrigger", "ScheduleTrigger"]
7
+ __all__ = ["BaseTrigger", "ChatMessageTrigger", "IntegrationTrigger", "ManualTrigger", "ScheduleTrigger"]