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.
- vellum/__init__.py +18 -0
- vellum/client/README.md +1 -1
- vellum/client/core/client_wrapper.py +2 -2
- vellum/client/core/force_multipart.py +4 -2
- vellum/client/core/http_response.py +1 -1
- vellum/client/core/pydantic_utilities.py +7 -4
- vellum/client/errors/too_many_requests_error.py +1 -2
- vellum/client/reference.md +677 -76
- vellum/client/resources/container_images/client.py +299 -0
- vellum/client/resources/container_images/raw_client.py +286 -0
- vellum/client/resources/documents/client.py +20 -10
- vellum/client/resources/documents/raw_client.py +20 -10
- vellum/client/resources/events/raw_client.py +4 -4
- vellum/client/resources/integration_auth_configs/client.py +2 -0
- vellum/client/resources/integration_auth_configs/raw_client.py +2 -0
- vellum/client/resources/integration_providers/client.py +28 -2
- vellum/client/resources/integration_providers/raw_client.py +24 -0
- vellum/client/resources/integrations/client.py +52 -4
- vellum/client/resources/integrations/raw_client.py +61 -0
- vellum/client/resources/workflow_deployments/client.py +156 -0
- vellum/client/resources/workflow_deployments/raw_client.py +334 -0
- vellum/client/resources/workflows/client.py +212 -8
- vellum/client/resources/workflows/raw_client.py +343 -6
- vellum/client/types/__init__.py +18 -0
- vellum/client/types/api_actor_type_enum.py +1 -1
- vellum/client/types/check_workflow_execution_status_error.py +21 -0
- vellum/client/types/check_workflow_execution_status_response.py +29 -0
- vellum/client/types/code_execution_package_request.py +21 -0
- vellum/client/types/composio_execute_tool_request.py +5 -0
- vellum/client/types/composio_tool_definition.py +1 -0
- vellum/client/types/container_image_build_config.py +1 -0
- vellum/client/types/container_image_container_image_tag.py +1 -0
- vellum/client/types/dataset_row_push_request.py +3 -0
- vellum/client/types/document_document_to_document_index.py +1 -0
- vellum/client/types/integration_name.py +24 -0
- vellum/client/types/node_execution_fulfilled_body.py +1 -0
- vellum/client/types/node_execution_log_body.py +24 -0
- vellum/client/types/node_execution_log_event.py +47 -0
- vellum/client/types/prompt_deployment_release_prompt_deployment.py +1 -0
- vellum/client/types/runner_config_request.py +24 -0
- vellum/client/types/severity_enum.py +5 -0
- vellum/client/types/slim_composio_tool_definition.py +1 -0
- vellum/client/types/slim_document_document_to_document_index.py +2 -0
- vellum/client/types/type_checker_enum.py +5 -0
- vellum/client/types/vellum_audio.py +5 -1
- vellum/client/types/vellum_audio_request.py +5 -1
- vellum/client/types/vellum_document.py +5 -1
- vellum/client/types/vellum_document_request.py +5 -1
- vellum/client/types/vellum_image.py +5 -1
- vellum/client/types/vellum_image_request.py +5 -1
- vellum/client/types/vellum_node_execution_event.py +2 -0
- vellum/client/types/vellum_variable.py +5 -0
- vellum/client/types/vellum_variable_extensions.py +1 -0
- vellum/client/types/vellum_variable_type.py +1 -0
- vellum/client/types/vellum_video.py +5 -1
- vellum/client/types/vellum_video_request.py +5 -1
- vellum/client/types/workflow_deployment_release_workflow_deployment.py +1 -0
- vellum/client/types/workflow_event.py +2 -0
- vellum/client/types/workflow_execution_fulfilled_body.py +1 -0
- vellum/client/types/workflow_result_event_output_data_array.py +1 -1
- vellum/client/types/workflow_result_event_output_data_chat_history.py +1 -1
- vellum/client/types/workflow_result_event_output_data_error.py +1 -1
- vellum/client/types/workflow_result_event_output_data_function_call.py +1 -1
- vellum/client/types/workflow_result_event_output_data_json.py +1 -1
- vellum/client/types/workflow_result_event_output_data_number.py +1 -1
- vellum/client/types/workflow_result_event_output_data_search_results.py +1 -1
- vellum/client/types/workflow_result_event_output_data_string.py +1 -1
- vellum/client/types/workflow_sandbox_execute_node_response.py +8 -0
- vellum/plugins/vellum_mypy.py +37 -2
- vellum/types/check_workflow_execution_status_error.py +3 -0
- vellum/types/check_workflow_execution_status_response.py +3 -0
- vellum/types/code_execution_package_request.py +3 -0
- vellum/types/node_execution_log_body.py +3 -0
- vellum/types/node_execution_log_event.py +3 -0
- vellum/types/runner_config_request.py +3 -0
- vellum/types/severity_enum.py +3 -0
- vellum/types/type_checker_enum.py +3 -0
- vellum/types/workflow_sandbox_execute_node_response.py +3 -0
- vellum/utils/files/mixin.py +26 -0
- vellum/utils/files/tests/test_mixin.py +62 -0
- vellum/utils/tests/test_vellum_client.py +95 -0
- vellum/utils/uuid.py +19 -2
- vellum/utils/vellum_client.py +10 -3
- vellum/workflows/__init__.py +7 -1
- vellum/workflows/descriptors/base.py +86 -0
- vellum/workflows/descriptors/tests/test_utils.py +9 -0
- vellum/workflows/errors/tests/__init__.py +0 -0
- vellum/workflows/errors/tests/test_types.py +52 -0
- vellum/workflows/errors/types.py +1 -0
- vellum/workflows/events/node.py +24 -0
- vellum/workflows/events/tests/test_event.py +123 -0
- vellum/workflows/events/types.py +2 -1
- vellum/workflows/events/workflow.py +28 -2
- vellum/workflows/expressions/add.py +3 -0
- vellum/workflows/expressions/tests/test_add.py +24 -0
- vellum/workflows/graph/graph.py +26 -5
- vellum/workflows/graph/tests/test_graph.py +228 -1
- vellum/workflows/inputs/base.py +22 -6
- vellum/workflows/inputs/dataset_row.py +121 -16
- vellum/workflows/inputs/tests/test_inputs.py +3 -3
- vellum/workflows/integrations/tests/test_vellum_integration_service.py +84 -0
- vellum/workflows/integrations/vellum_integration_service.py +12 -1
- vellum/workflows/loaders/base.py +2 -0
- vellum/workflows/nodes/bases/base.py +37 -16
- vellum/workflows/nodes/bases/tests/test_base_node.py +104 -1
- vellum/workflows/nodes/core/inline_subworkflow_node/node.py +1 -0
- vellum/workflows/nodes/core/inline_subworkflow_node/tests/test_node.py +1 -1
- vellum/workflows/nodes/core/map_node/node.py +7 -5
- vellum/workflows/nodes/core/map_node/tests/test_node.py +33 -0
- vellum/workflows/nodes/core/retry_node/node.py +1 -0
- vellum/workflows/nodes/core/try_node/node.py +1 -0
- vellum/workflows/nodes/displayable/api_node/node.py +3 -2
- vellum/workflows/nodes/displayable/api_node/tests/test_api_node.py +38 -0
- vellum/workflows/nodes/displayable/bases/api_node/node.py +1 -1
- vellum/workflows/nodes/displayable/bases/base_prompt_node/node.py +18 -1
- vellum/workflows/nodes/displayable/bases/inline_prompt_node/node.py +109 -2
- vellum/workflows/nodes/displayable/bases/prompt_deployment_node.py +13 -2
- vellum/workflows/nodes/displayable/code_execution_node/node.py +9 -15
- vellum/workflows/nodes/displayable/code_execution_node/tests/test_node.py +65 -24
- vellum/workflows/nodes/displayable/code_execution_node/utils.py +3 -0
- vellum/workflows/nodes/displayable/final_output_node/node.py +24 -69
- vellum/workflows/nodes/displayable/final_output_node/tests/test_node.py +53 -3
- vellum/workflows/nodes/displayable/note_node/node.py +4 -1
- vellum/workflows/nodes/displayable/subworkflow_deployment_node/node.py +16 -5
- vellum/workflows/nodes/displayable/tests/test_text_prompt_deployment_node.py +47 -0
- vellum/workflows/nodes/displayable/tool_calling_node/node.py +74 -34
- vellum/workflows/nodes/displayable/tool_calling_node/tests/test_node.py +204 -8
- vellum/workflows/nodes/displayable/tool_calling_node/utils.py +92 -71
- vellum/workflows/nodes/mocks.py +47 -213
- vellum/workflows/nodes/tests/test_mocks.py +0 -177
- vellum/workflows/nodes/utils.py +23 -8
- vellum/workflows/outputs/base.py +36 -3
- vellum/workflows/references/environment_variable.py +1 -11
- vellum/workflows/references/lazy.py +8 -0
- vellum/workflows/references/state_value.py +24 -1
- vellum/workflows/references/tests/test_lazy.py +58 -0
- vellum/workflows/references/trigger.py +8 -3
- vellum/workflows/references/workflow_input.py +8 -0
- vellum/workflows/resolvers/resolver.py +13 -3
- vellum/workflows/resolvers/tests/test_resolver.py +31 -0
- vellum/workflows/runner/runner.py +159 -14
- vellum/workflows/runner/tests/__init__.py +0 -0
- vellum/workflows/runner/tests/test_runner.py +170 -0
- vellum/workflows/sandbox.py +7 -8
- vellum/workflows/state/base.py +89 -30
- vellum/workflows/state/context.py +74 -3
- vellum/workflows/state/tests/test_state.py +269 -1
- vellum/workflows/tests/test_dataset_row.py +8 -7
- vellum/workflows/tests/test_sandbox.py +97 -8
- vellum/workflows/triggers/__init__.py +2 -1
- vellum/workflows/triggers/base.py +160 -28
- vellum/workflows/triggers/chat_message.py +141 -0
- vellum/workflows/triggers/integration.py +12 -0
- vellum/workflows/triggers/manual.py +3 -1
- vellum/workflows/triggers/schedule.py +3 -1
- vellum/workflows/triggers/tests/test_chat_message.py +257 -0
- vellum/workflows/types/core.py +18 -0
- vellum/workflows/types/definition.py +6 -13
- vellum/workflows/types/generics.py +12 -0
- vellum/workflows/types/tests/test_utils.py +12 -0
- vellum/workflows/types/utils.py +32 -2
- vellum/workflows/types/workflow_metadata.py +124 -0
- vellum/workflows/utils/functions.py +152 -16
- vellum/workflows/utils/pydantic_schema.py +19 -1
- vellum/workflows/utils/tests/test_functions.py +123 -8
- vellum/workflows/utils/tests/test_validate.py +79 -0
- vellum/workflows/utils/tests/test_vellum_variables.py +62 -2
- vellum/workflows/utils/uuids.py +90 -0
- vellum/workflows/utils/validate.py +108 -0
- vellum/workflows/utils/vellum_variables.py +96 -16
- vellum/workflows/workflows/base.py +177 -35
- vellum/workflows/workflows/tests/test_base_workflow.py +51 -0
- {vellum_ai-1.11.2.dist-info → vellum_ai-1.13.5.dist-info}/METADATA +6 -1
- {vellum_ai-1.11.2.dist-info → vellum_ai-1.13.5.dist-info}/RECORD +274 -227
- vellum_cli/__init__.py +21 -0
- vellum_cli/config.py +16 -2
- vellum_cli/pull.py +2 -0
- vellum_cli/push.py +23 -10
- vellum_cli/tests/conftest.py +8 -13
- vellum_cli/tests/test_image_push.py +4 -11
- vellum_cli/tests/test_pull.py +83 -68
- vellum_cli/tests/test_push.py +251 -2
- vellum_ee/assets/node-definitions.json +225 -12
- vellum_ee/scripts/generate_node_definitions.py +15 -3
- vellum_ee/workflows/display/base.py +4 -3
- vellum_ee/workflows/display/nodes/base_node_display.py +44 -11
- vellum_ee/workflows/display/nodes/tests/test_base_node_display.py +93 -0
- vellum_ee/workflows/display/nodes/types.py +1 -0
- vellum_ee/workflows/display/nodes/vellum/__init__.py +0 -2
- vellum_ee/workflows/display/nodes/vellum/base_adornment_node.py +5 -2
- vellum_ee/workflows/display/nodes/vellum/code_execution_node.py +1 -1
- vellum_ee/workflows/display/nodes/vellum/inline_prompt_node.py +10 -2
- vellum_ee/workflows/display/nodes/vellum/inline_subworkflow_node.py +17 -14
- vellum_ee/workflows/display/nodes/vellum/map_node.py +2 -0
- vellum_ee/workflows/display/nodes/vellum/note_node.py +18 -3
- vellum_ee/workflows/display/nodes/vellum/subworkflow_deployment_node.py +37 -14
- vellum_ee/workflows/display/nodes/vellum/tests/test_code_execution_node.py +62 -2
- vellum_ee/workflows/display/nodes/vellum/tests/test_final_output_node.py +136 -0
- vellum_ee/workflows/display/nodes/vellum/tests/test_note_node.py +44 -7
- vellum_ee/workflows/display/nodes/vellum/tests/test_prompt_node.py +5 -13
- vellum_ee/workflows/display/nodes/vellum/tests/test_subworkflow_deployment_node.py +27 -17
- vellum_ee/workflows/display/nodes/vellum/tests/test_tool_calling_node.py +145 -22
- vellum_ee/workflows/display/nodes/vellum/tests/test_utils.py +107 -2
- vellum_ee/workflows/display/nodes/vellum/utils.py +54 -12
- vellum_ee/workflows/display/tests/test_base_workflow_display.py +13 -16
- vellum_ee/workflows/display/tests/test_json_schema_validation.py +190 -0
- vellum_ee/workflows/display/tests/test_mocks.py +912 -0
- vellum_ee/workflows/display/tests/workflow_serialization/generic_nodes/test_adornments_serialization.py +14 -2
- vellum_ee/workflows/display/tests/workflow_serialization/generic_nodes/test_attributes_serialization.py +109 -0
- vellum_ee/workflows/display/tests/workflow_serialization/generic_nodes/test_outputs_serialization.py +3 -0
- vellum_ee/workflows/display/tests/workflow_serialization/generic_nodes/test_ports_serialization.py +187 -1
- vellum_ee/workflows/display/tests/workflow_serialization/test_basic_api_node_serialization.py +34 -325
- vellum_ee/workflows/display/tests/workflow_serialization/test_basic_code_execution_node_serialization.py +42 -393
- vellum_ee/workflows/display/tests/workflow_serialization/test_basic_conditional_node_serialization.py +13 -315
- vellum_ee/workflows/display/tests/workflow_serialization/test_basic_default_state_serialization.py +2 -122
- vellum_ee/workflows/display/tests/workflow_serialization/test_basic_error_node_serialization.py +24 -115
- vellum_ee/workflows/display/tests/workflow_serialization/test_basic_generic_node_serialization.py +4 -93
- vellum_ee/workflows/display/tests/workflow_serialization/test_basic_guardrail_node_serialization.py +7 -80
- vellum_ee/workflows/display/tests/workflow_serialization/test_basic_inline_prompt_node_serialization.py +9 -101
- vellum_ee/workflows/display/tests/workflow_serialization/test_basic_inline_subworkflow_serialization.py +77 -308
- vellum_ee/workflows/display/tests/workflow_serialization/test_basic_map_node_serialization.py +62 -324
- vellum_ee/workflows/display/tests/workflow_serialization/test_basic_merge_node_serialization.py +3 -82
- vellum_ee/workflows/display/tests/workflow_serialization/test_basic_prompt_deployment_serialization.py +4 -142
- vellum_ee/workflows/display/tests/workflow_serialization/test_basic_search_node_serialization.py +1 -61
- vellum_ee/workflows/display/tests/workflow_serialization/test_basic_set_state_node_serialization.py +4 -4
- vellum_ee/workflows/display/tests/workflow_serialization/test_basic_subworkflow_deployment_serialization.py +205 -134
- vellum_ee/workflows/display/tests/workflow_serialization/test_basic_templating_node_serialization.py +34 -146
- vellum_ee/workflows/display/tests/workflow_serialization/test_basic_terminal_node_serialization.py +2 -0
- vellum_ee/workflows/display/tests/workflow_serialization/test_basic_tool_calling_node_composio_serialization.py +8 -6
- vellum_ee/workflows/display/tests/workflow_serialization/test_basic_tool_calling_node_inline_workflow_serialization.py +137 -266
- vellum_ee/workflows/display/tests/workflow_serialization/test_basic_tool_calling_node_inline_workflow_tool_wrapper_serialization.py +84 -0
- vellum_ee/workflows/display/tests/workflow_serialization/test_basic_tool_calling_node_mcp_serialization.py +55 -16
- vellum_ee/workflows/display/tests/workflow_serialization/test_basic_tool_calling_node_serialization.py +15 -1
- vellum_ee/workflows/display/tests/workflow_serialization/test_basic_tool_calling_node_tool_wrapper_serialization.py +71 -0
- vellum_ee/workflows/display/tests/workflow_serialization/test_basic_tool_calling_node_vellum_integration_serialization.py +119 -0
- vellum_ee/workflows/display/tests/workflow_serialization/test_basic_tool_calling_node_workflow_deployment_serialization.py +1 -1
- vellum_ee/workflows/display/tests/workflow_serialization/test_basic_try_node_serialization.py +0 -2
- vellum_ee/workflows/display/tests/workflow_serialization/test_chat_message_dict_reference_serialization.py +22 -1
- vellum_ee/workflows/display/tests/workflow_serialization/test_chat_message_trigger_serialization.py +412 -0
- vellum_ee/workflows/display/tests/workflow_serialization/test_code_tool_node_reference_error.py +106 -0
- vellum_ee/workflows/display/tests/workflow_serialization/test_complex_terminal_node_serialization.py +9 -41
- vellum_ee/workflows/display/tests/workflow_serialization/test_duplicate_trigger_name_validation.py +208 -0
- vellum_ee/workflows/display/tests/workflow_serialization/test_final_output_node_not_referenced_by_workflow_outputs.py +45 -0
- vellum_ee/workflows/display/tests/workflow_serialization/test_infinite_loop_validation.py +66 -0
- vellum_ee/workflows/display/tests/workflow_serialization/test_int_input_serialization.py +40 -0
- vellum_ee/workflows/display/tests/workflow_serialization/test_integration_trigger_serialization.py +8 -14
- vellum_ee/workflows/display/tests/workflow_serialization/test_integration_trigger_validation.py +173 -0
- vellum_ee/workflows/display/tests/workflow_serialization/test_integration_trigger_with_entrypoint_node_id.py +16 -13
- vellum_ee/workflows/display/tests/workflow_serialization/test_list_vellum_document_serialization.py +5 -1
- vellum_ee/workflows/display/tests/workflow_serialization/test_manual_trigger_serialization.py +12 -2
- vellum_ee/workflows/display/tests/workflow_serialization/test_multi_trigger_same_node_serialization.py +111 -0
- vellum_ee/workflows/display/tests/workflow_serialization/test_no_triggers_no_entrypoint_validation.py +64 -0
- vellum_ee/workflows/display/tests/workflow_serialization/test_partial_workflow_meta_display_override.py +55 -0
- vellum_ee/workflows/display/tests/workflow_serialization/test_sandbox_dataset_mocks_serialization.py +268 -0
- vellum_ee/workflows/display/tests/workflow_serialization/test_sandbox_invalid_pdf_data_url.py +49 -0
- vellum_ee/workflows/display/tests/workflow_serialization/test_sandbox_validation_errors.py +112 -0
- vellum_ee/workflows/display/tests/workflow_serialization/test_scheduled_trigger_serialization.py +25 -16
- vellum_ee/workflows/display/tests/workflow_serialization/test_terminal_node_in_unused_graphs_serialization.py +53 -0
- vellum_ee/workflows/display/utils/exceptions.py +34 -0
- vellum_ee/workflows/display/utils/expressions.py +463 -52
- vellum_ee/workflows/display/utils/metadata.py +98 -33
- vellum_ee/workflows/display/utils/tests/test_metadata.py +31 -0
- vellum_ee/workflows/display/utils/triggers.py +153 -0
- vellum_ee/workflows/display/utils/vellum.py +59 -5
- vellum_ee/workflows/display/workflows/base_workflow_display.py +656 -254
- vellum_ee/workflows/display/workflows/get_vellum_workflow_display_class.py +26 -0
- vellum_ee/workflows/display/workflows/tests/test_workflow_display.py +77 -29
- vellum_ee/workflows/server/namespaces.py +18 -0
- vellum_ee/workflows/tests/test_display_meta.py +2 -0
- vellum_ee/workflows/tests/test_serialize_module.py +174 -7
- vellum_ee/workflows/tests/test_server.py +0 -3
- vellum_ee/workflows/display/nodes/vellum/function_node.py +0 -14
- {vellum_ai-1.11.2.dist-info → vellum_ai-1.13.5.dist-info}/LICENSE +0 -0
- {vellum_ai-1.11.2.dist-info → vellum_ai-1.13.5.dist-info}/WHEEL +0 -0
- {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
|
-
|
|
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
|
|
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,
|
|
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
|
|
180
|
-
assert "
|
|
181
|
-
assert serialized_dict["
|
|
182
|
-
assert len(serialized_dict["
|
|
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["
|
|
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
|
|
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=
|
|
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
|
|
155
|
-
assert dataset[0].workflow_trigger ==
|
|
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
|
|
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
|
|
267
|
+
# AND a dataset with mocks
|
|
179
268
|
dataset = [
|
|
180
269
|
DatasetRow(
|
|
181
270
|
label="test_with_mocks",
|
|
182
271
|
inputs={"message": "test"},
|
|
183
|
-
|
|
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
|
|
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"]
|