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
|
@@ -0,0 +1,912 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
import sys
|
|
3
|
+
from uuid import uuid4
|
|
4
|
+
|
|
5
|
+
from vellum.workflows import BaseInputs, BaseNode, BaseState, BaseWorkflow, MockNodeExecution
|
|
6
|
+
from vellum.workflows.exceptions import NodeException
|
|
7
|
+
from vellum.workflows.expressions.accessor import AccessorExpression
|
|
8
|
+
from vellum.workflows.expressions.add import AddExpression
|
|
9
|
+
from vellum.workflows.expressions.and_ import AndExpression
|
|
10
|
+
from vellum.workflows.expressions.begins_with import BeginsWithExpression
|
|
11
|
+
from vellum.workflows.expressions.between import BetweenExpression
|
|
12
|
+
from vellum.workflows.expressions.coalesce_expression import CoalesceExpression
|
|
13
|
+
from vellum.workflows.expressions.concat import ConcatExpression
|
|
14
|
+
from vellum.workflows.expressions.contains import ContainsExpression
|
|
15
|
+
from vellum.workflows.expressions.does_not_begin_with import DoesNotBeginWithExpression
|
|
16
|
+
from vellum.workflows.expressions.does_not_contain import DoesNotContainExpression
|
|
17
|
+
from vellum.workflows.expressions.does_not_end_with import DoesNotEndWithExpression
|
|
18
|
+
from vellum.workflows.expressions.ends_with import EndsWithExpression
|
|
19
|
+
from vellum.workflows.expressions.in_ import InExpression
|
|
20
|
+
from vellum.workflows.expressions.is_blank import IsBlankExpression
|
|
21
|
+
from vellum.workflows.expressions.is_error import IsErrorExpression
|
|
22
|
+
from vellum.workflows.expressions.is_not_blank import IsNotBlankExpression
|
|
23
|
+
from vellum.workflows.expressions.is_not_null import IsNotNullExpression
|
|
24
|
+
from vellum.workflows.expressions.is_null import IsNullExpression
|
|
25
|
+
from vellum.workflows.expressions.length import LengthExpression
|
|
26
|
+
from vellum.workflows.expressions.minus import MinusExpression
|
|
27
|
+
from vellum.workflows.expressions.not_between import NotBetweenExpression
|
|
28
|
+
from vellum.workflows.expressions.not_in import NotInExpression
|
|
29
|
+
from vellum.workflows.expressions.or_ import OrExpression
|
|
30
|
+
from vellum.workflows.expressions.parse_json import ParseJsonExpression
|
|
31
|
+
from vellum.workflows.nodes.core.inline_subworkflow_node.node import InlineSubworkflowNode
|
|
32
|
+
from vellum.workflows.nodes.core.try_node.node import TryNode
|
|
33
|
+
from vellum.workflows.references.constant import ConstantValueReference
|
|
34
|
+
from vellum.workflows.references.environment_variable import EnvironmentVariableReference
|
|
35
|
+
from vellum.workflows.references.vellum_secret import VellumSecretReference
|
|
36
|
+
from vellum_ee.workflows.display.utils.expressions import base_descriptor_validator
|
|
37
|
+
from vellum_ee.workflows.server.virtual_file_loader import VirtualFileFinder
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_mocks__parse_from_app__descriptors():
|
|
41
|
+
# GIVEN a Base Node
|
|
42
|
+
class StartNode(BaseNode):
|
|
43
|
+
class Outputs(BaseNode.Outputs):
|
|
44
|
+
foo: str
|
|
45
|
+
|
|
46
|
+
# AND workflow inputs
|
|
47
|
+
class Inputs(BaseInputs):
|
|
48
|
+
bar: str
|
|
49
|
+
|
|
50
|
+
# AND workflow state
|
|
51
|
+
class State(BaseState):
|
|
52
|
+
baz: str
|
|
53
|
+
|
|
54
|
+
# AND a workflow class with that Node
|
|
55
|
+
class MyWorkflow(BaseWorkflow[Inputs, State]):
|
|
56
|
+
graph = StartNode
|
|
57
|
+
|
|
58
|
+
class Outputs(BaseWorkflow.Outputs):
|
|
59
|
+
final_value = StartNode.Outputs.foo
|
|
60
|
+
|
|
61
|
+
# AND a mock workflow node execution from the app
|
|
62
|
+
raw_mock_workflow_node_executions = [
|
|
63
|
+
{
|
|
64
|
+
"node_id": str(StartNode.__id__),
|
|
65
|
+
"when_condition": {
|
|
66
|
+
"type": "BINARY_EXPRESSION",
|
|
67
|
+
"operator": ">=",
|
|
68
|
+
"lhs": {
|
|
69
|
+
"type": "EXECUTION_COUNTER",
|
|
70
|
+
"node_id": str(StartNode.__id__),
|
|
71
|
+
},
|
|
72
|
+
"rhs": {
|
|
73
|
+
"type": "CONSTANT_VALUE",
|
|
74
|
+
"value": {
|
|
75
|
+
"type": "NUMBER",
|
|
76
|
+
"value": 1,
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
"then_outputs": {
|
|
81
|
+
"foo": "Hello foo",
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
"node_id": str(StartNode.__id__),
|
|
86
|
+
"when_condition": {
|
|
87
|
+
"type": "BINARY_EXPRESSION",
|
|
88
|
+
"operator": "==",
|
|
89
|
+
"lhs": {
|
|
90
|
+
"type": "WORKFLOW_INPUT",
|
|
91
|
+
"input_variable_id": str(Inputs.bar.id),
|
|
92
|
+
},
|
|
93
|
+
"rhs": {
|
|
94
|
+
"type": "CONSTANT_VALUE",
|
|
95
|
+
"value": {
|
|
96
|
+
"type": "STRING",
|
|
97
|
+
"value": "bar",
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
"then_outputs": {
|
|
102
|
+
"foo": "Hello bar",
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
"node_id": str(StartNode.__id__),
|
|
107
|
+
"when_condition": {
|
|
108
|
+
"type": "BINARY_EXPRESSION",
|
|
109
|
+
"operator": "!=",
|
|
110
|
+
"lhs": {
|
|
111
|
+
"type": "WORKFLOW_STATE",
|
|
112
|
+
"state_variable_id": str(State.baz.id),
|
|
113
|
+
},
|
|
114
|
+
"rhs": {
|
|
115
|
+
"type": "CONSTANT_VALUE",
|
|
116
|
+
"value": {
|
|
117
|
+
"type": "STRING",
|
|
118
|
+
"value": "baz",
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
"then_outputs": {
|
|
123
|
+
"foo": "Hello baz",
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
]
|
|
127
|
+
|
|
128
|
+
# WHEN we parsed the raw data on `MockNodeExecution`
|
|
129
|
+
node_output_mocks = MockNodeExecution.validate_all(
|
|
130
|
+
raw_mock_workflow_node_executions,
|
|
131
|
+
MyWorkflow,
|
|
132
|
+
descriptor_validator=base_descriptor_validator,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
# THEN we get the expected list of MockNodeExecution objects
|
|
136
|
+
assert node_output_mocks
|
|
137
|
+
assert len(node_output_mocks) == 3
|
|
138
|
+
assert node_output_mocks[0] == MockNodeExecution(
|
|
139
|
+
when_condition=StartNode.Execution.count.greater_than_or_equal_to(1),
|
|
140
|
+
then_outputs=StartNode.Outputs(
|
|
141
|
+
foo="Hello foo",
|
|
142
|
+
),
|
|
143
|
+
)
|
|
144
|
+
assert node_output_mocks[1] == MockNodeExecution(
|
|
145
|
+
when_condition=Inputs.bar.equals("bar"),
|
|
146
|
+
then_outputs=StartNode.Outputs(
|
|
147
|
+
foo="Hello bar",
|
|
148
|
+
),
|
|
149
|
+
)
|
|
150
|
+
assert node_output_mocks[2] == MockNodeExecution(
|
|
151
|
+
when_condition=State.baz.does_not_equal("baz"),
|
|
152
|
+
then_outputs=StartNode.Outputs(
|
|
153
|
+
foo="Hello baz",
|
|
154
|
+
),
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def test_mocks__parse_from_app__when_condition_defaults_to_false_without_descriptor_validator():
|
|
159
|
+
"""
|
|
160
|
+
Tests that when_condition defaults to ConstantValueReference(False) when
|
|
161
|
+
descriptor_validator is not provided. This ensures that mocks without a
|
|
162
|
+
descriptor_validator have a valid when_condition that evaluates to False.
|
|
163
|
+
"""
|
|
164
|
+
|
|
165
|
+
# GIVEN a Base Node
|
|
166
|
+
class StartNode(BaseNode):
|
|
167
|
+
class Outputs(BaseNode.Outputs):
|
|
168
|
+
foo: str
|
|
169
|
+
|
|
170
|
+
# AND a workflow class with that Node
|
|
171
|
+
class MyWorkflow(BaseWorkflow):
|
|
172
|
+
graph = StartNode
|
|
173
|
+
|
|
174
|
+
class Outputs(BaseWorkflow.Outputs):
|
|
175
|
+
final_value = StartNode.Outputs.foo
|
|
176
|
+
|
|
177
|
+
# AND a mock workflow node execution with a valid when_condition JSON structure
|
|
178
|
+
raw_mock_workflow_node_executions = [
|
|
179
|
+
{
|
|
180
|
+
"node_id": str(StartNode.__id__),
|
|
181
|
+
"when_condition": {
|
|
182
|
+
"type": "BINARY_EXPRESSION",
|
|
183
|
+
"operator": ">=",
|
|
184
|
+
"lhs": {
|
|
185
|
+
"type": "EXECUTION_COUNTER",
|
|
186
|
+
"node_id": str(StartNode.__id__),
|
|
187
|
+
},
|
|
188
|
+
"rhs": {
|
|
189
|
+
"type": "CONSTANT_VALUE",
|
|
190
|
+
"value": {
|
|
191
|
+
"type": "NUMBER",
|
|
192
|
+
"value": 1,
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
"then_outputs": {
|
|
197
|
+
"foo": "Hello foo",
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
]
|
|
201
|
+
|
|
202
|
+
# WHEN we parse the raw data on `MockNodeExecution` without a descriptor_validator
|
|
203
|
+
node_output_mocks = MockNodeExecution.validate_all(
|
|
204
|
+
raw_mock_workflow_node_executions,
|
|
205
|
+
MyWorkflow,
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
# THEN we get a list of MockNodeExecution objects
|
|
209
|
+
assert node_output_mocks is not None
|
|
210
|
+
assert len(node_output_mocks) == 1
|
|
211
|
+
|
|
212
|
+
# AND the when_condition defaults to ConstantValueReference(False)
|
|
213
|
+
assert node_output_mocks[0].when_condition == ConstantValueReference(False)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def test_mocks__use_node_id_from_display():
|
|
217
|
+
"""
|
|
218
|
+
Tests that validate_all correctly resolves mocks when the node ID is annotated by the display class.
|
|
219
|
+
"""
|
|
220
|
+
|
|
221
|
+
# GIVEN a workflow module with a display class that sets a custom node_id
|
|
222
|
+
display_node_id = uuid4()
|
|
223
|
+
files = {
|
|
224
|
+
"__init__.py": "",
|
|
225
|
+
"workflow.py": """\
|
|
226
|
+
from vellum.workflows import BaseWorkflow
|
|
227
|
+
from vellum.workflows.nodes import BaseNode
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
class StartNode(BaseNode):
|
|
231
|
+
class Outputs(BaseNode.Outputs):
|
|
232
|
+
foo: str
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
class Workflow(BaseWorkflow):
|
|
236
|
+
graph = StartNode
|
|
237
|
+
|
|
238
|
+
class Outputs(BaseWorkflow.Outputs):
|
|
239
|
+
final_value = StartNode.Outputs.foo
|
|
240
|
+
""",
|
|
241
|
+
"display/__init__.py": """\
|
|
242
|
+
# flake8: noqa: F401, F403
|
|
243
|
+
|
|
244
|
+
from .workflow import *
|
|
245
|
+
from .nodes import *
|
|
246
|
+
""",
|
|
247
|
+
"display/workflow.py": """\
|
|
248
|
+
from vellum_ee.workflows.display.workflows import BaseWorkflowDisplay
|
|
249
|
+
from ..workflow import Workflow
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
class WorkflowDisplay(BaseWorkflowDisplay[Workflow]):
|
|
253
|
+
pass
|
|
254
|
+
""",
|
|
255
|
+
"display/nodes/__init__.py": """\
|
|
256
|
+
# flake8: noqa: F401, F403
|
|
257
|
+
|
|
258
|
+
from .start_node import *
|
|
259
|
+
""",
|
|
260
|
+
"display/nodes/start_node.py": f"""\
|
|
261
|
+
from uuid import UUID
|
|
262
|
+
from vellum_ee.workflows.display.nodes.base_node_display import BaseNodeDisplay
|
|
263
|
+
from ...workflow import StartNode
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
class StartNodeDisplay(BaseNodeDisplay[StartNode]):
|
|
267
|
+
node_id = UUID("{display_node_id}")
|
|
268
|
+
""",
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
namespace = str(uuid4())
|
|
272
|
+
|
|
273
|
+
# AND the virtual file loader is registered
|
|
274
|
+
sys.meta_path.append(VirtualFileFinder(files, namespace))
|
|
275
|
+
|
|
276
|
+
# AND the workflow is loaded from the module
|
|
277
|
+
Workflow = BaseWorkflow.load_from_module(namespace)
|
|
278
|
+
StartNode = list(Workflow.get_nodes())[0]
|
|
279
|
+
|
|
280
|
+
# AND a mock workflow node execution using the display-annotated node ID
|
|
281
|
+
raw_mock_workflow_node_executions = [
|
|
282
|
+
{
|
|
283
|
+
"node_id": str(display_node_id),
|
|
284
|
+
"when_condition": {
|
|
285
|
+
"type": "BINARY_EXPRESSION",
|
|
286
|
+
"operator": ">=",
|
|
287
|
+
"lhs": {
|
|
288
|
+
"type": "EXECUTION_COUNTER",
|
|
289
|
+
"node_id": str(display_node_id),
|
|
290
|
+
},
|
|
291
|
+
"rhs": {
|
|
292
|
+
"type": "CONSTANT_VALUE",
|
|
293
|
+
"value": {
|
|
294
|
+
"type": "NUMBER",
|
|
295
|
+
"value": 0,
|
|
296
|
+
},
|
|
297
|
+
},
|
|
298
|
+
},
|
|
299
|
+
"then_outputs": {
|
|
300
|
+
"foo": "Hello",
|
|
301
|
+
},
|
|
302
|
+
},
|
|
303
|
+
]
|
|
304
|
+
|
|
305
|
+
# WHEN we parse the mock workflow node execution
|
|
306
|
+
node_output_mocks = MockNodeExecution.validate_all(
|
|
307
|
+
raw_mock_workflow_node_executions,
|
|
308
|
+
Workflow,
|
|
309
|
+
descriptor_validator=base_descriptor_validator,
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
# THEN we get the expected list of MockNodeExecution objects
|
|
313
|
+
assert node_output_mocks
|
|
314
|
+
assert len(node_output_mocks) == 1
|
|
315
|
+
assert node_output_mocks[0] == MockNodeExecution(
|
|
316
|
+
when_condition=StartNode.Execution.count.greater_than_or_equal_to(0),
|
|
317
|
+
then_outputs=StartNode.Outputs( # type: ignore[call-arg]
|
|
318
|
+
foo="Hello",
|
|
319
|
+
),
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def test_mocks__node_not_found_in_workflow_skips_with_warning(caplog):
|
|
324
|
+
"""
|
|
325
|
+
Tests that when a mock references a node_id that doesn't exist in the workflow,
|
|
326
|
+
the mock is skipped with a warning instead of raising an error.
|
|
327
|
+
"""
|
|
328
|
+
|
|
329
|
+
# GIVEN a Base Node
|
|
330
|
+
class StartNode(BaseNode):
|
|
331
|
+
class Outputs(BaseNode.Outputs):
|
|
332
|
+
foo: str
|
|
333
|
+
|
|
334
|
+
# AND a workflow class with that Node
|
|
335
|
+
class MyWorkflow(BaseWorkflow):
|
|
336
|
+
graph = StartNode
|
|
337
|
+
|
|
338
|
+
class Outputs(BaseWorkflow.Outputs):
|
|
339
|
+
final_value = StartNode.Outputs.foo
|
|
340
|
+
|
|
341
|
+
# AND a mock workflow node execution referencing a non-existent node_id
|
|
342
|
+
non_existent_node_id = uuid4()
|
|
343
|
+
raw_mock_with_missing_node = [
|
|
344
|
+
{
|
|
345
|
+
"node_id": str(non_existent_node_id),
|
|
346
|
+
"when_condition": {
|
|
347
|
+
"type": "BINARY_EXPRESSION",
|
|
348
|
+
"operator": ">=",
|
|
349
|
+
"lhs": {
|
|
350
|
+
"type": "EXECUTION_COUNTER",
|
|
351
|
+
"node_id": str(non_existent_node_id),
|
|
352
|
+
},
|
|
353
|
+
"rhs": {
|
|
354
|
+
"type": "CONSTANT_VALUE",
|
|
355
|
+
"value": {
|
|
356
|
+
"type": "NUMBER",
|
|
357
|
+
"value": 0,
|
|
358
|
+
},
|
|
359
|
+
},
|
|
360
|
+
},
|
|
361
|
+
"then_outputs": {
|
|
362
|
+
"foo": "Hello",
|
|
363
|
+
},
|
|
364
|
+
}
|
|
365
|
+
]
|
|
366
|
+
|
|
367
|
+
# WHEN we parse the mock workflow node execution
|
|
368
|
+
node_output_mocks = MockNodeExecution.validate_all(
|
|
369
|
+
raw_mock_with_missing_node,
|
|
370
|
+
MyWorkflow,
|
|
371
|
+
descriptor_validator=base_descriptor_validator,
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
# THEN we get an empty list (the mock was skipped)
|
|
375
|
+
assert node_output_mocks == []
|
|
376
|
+
|
|
377
|
+
# AND a warning was logged
|
|
378
|
+
assert any(
|
|
379
|
+
f"Skipping mock for node {non_existent_node_id}" in record.message
|
|
380
|
+
and "node not found in workflow MyWorkflow" in record.message
|
|
381
|
+
for record in caplog.records
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def test_base_descriptor_validator__vellum_secret():
|
|
386
|
+
"""
|
|
387
|
+
Tests that VELLUM_SECRET descriptor type is correctly validated.
|
|
388
|
+
"""
|
|
389
|
+
|
|
390
|
+
# GIVEN a simple workflow
|
|
391
|
+
class StartNode(BaseNode):
|
|
392
|
+
class Outputs(BaseNode.Outputs):
|
|
393
|
+
foo: str
|
|
394
|
+
|
|
395
|
+
class MyWorkflow(BaseWorkflow):
|
|
396
|
+
graph = StartNode
|
|
397
|
+
|
|
398
|
+
# AND a VELLUM_SECRET descriptor
|
|
399
|
+
raw_descriptor = {
|
|
400
|
+
"type": "VELLUM_SECRET",
|
|
401
|
+
"vellum_secret_name": "my_secret",
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
# WHEN we validate the descriptor
|
|
405
|
+
result = base_descriptor_validator(raw_descriptor, MyWorkflow)
|
|
406
|
+
|
|
407
|
+
# THEN we get a VellumSecretReference
|
|
408
|
+
assert isinstance(result, VellumSecretReference)
|
|
409
|
+
assert result.name == "my_secret"
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def test_base_descriptor_validator__environment_variable():
|
|
413
|
+
"""
|
|
414
|
+
Tests that ENVIRONMENT_VARIABLE descriptor type is correctly validated.
|
|
415
|
+
"""
|
|
416
|
+
|
|
417
|
+
# GIVEN a simple workflow
|
|
418
|
+
class StartNode(BaseNode):
|
|
419
|
+
class Outputs(BaseNode.Outputs):
|
|
420
|
+
foo: str
|
|
421
|
+
|
|
422
|
+
class MyWorkflow(BaseWorkflow):
|
|
423
|
+
graph = StartNode
|
|
424
|
+
|
|
425
|
+
# AND an ENVIRONMENT_VARIABLE descriptor
|
|
426
|
+
raw_descriptor = {
|
|
427
|
+
"type": "ENVIRONMENT_VARIABLE",
|
|
428
|
+
"environment_variable": "MY_ENV_VAR",
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
# WHEN we validate the descriptor
|
|
432
|
+
result = base_descriptor_validator(raw_descriptor, MyWorkflow)
|
|
433
|
+
|
|
434
|
+
# THEN we get an EnvironmentVariableReference
|
|
435
|
+
assert isinstance(result, EnvironmentVariableReference)
|
|
436
|
+
assert result.name == "MY_ENV_VAR"
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def test_base_descriptor_validator__array_reference():
|
|
440
|
+
"""
|
|
441
|
+
Tests that ARRAY_REFERENCE descriptor type is correctly validated.
|
|
442
|
+
"""
|
|
443
|
+
|
|
444
|
+
# GIVEN a simple workflow
|
|
445
|
+
class StartNode(BaseNode):
|
|
446
|
+
class Outputs(BaseNode.Outputs):
|
|
447
|
+
foo: str
|
|
448
|
+
|
|
449
|
+
class MyWorkflow(BaseWorkflow):
|
|
450
|
+
graph = StartNode
|
|
451
|
+
|
|
452
|
+
# AND an ARRAY_REFERENCE descriptor with constant values
|
|
453
|
+
raw_descriptor = {
|
|
454
|
+
"type": "ARRAY_REFERENCE",
|
|
455
|
+
"items": [
|
|
456
|
+
{"type": "CONSTANT_VALUE", "value": {"type": "STRING", "value": "item1"}},
|
|
457
|
+
{"type": "CONSTANT_VALUE", "value": {"type": "STRING", "value": "item2"}},
|
|
458
|
+
],
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
# WHEN we validate the descriptor
|
|
462
|
+
result = base_descriptor_validator(raw_descriptor, MyWorkflow)
|
|
463
|
+
|
|
464
|
+
# THEN we get a ConstantValueReference containing a list
|
|
465
|
+
assert isinstance(result, ConstantValueReference)
|
|
466
|
+
assert isinstance(result._value, list)
|
|
467
|
+
assert len(result._value) == 2
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
def test_base_descriptor_validator__dictionary_reference():
|
|
471
|
+
"""
|
|
472
|
+
Tests that DICTIONARY_REFERENCE descriptor type is correctly validated.
|
|
473
|
+
"""
|
|
474
|
+
|
|
475
|
+
# GIVEN a simple workflow
|
|
476
|
+
class StartNode(BaseNode):
|
|
477
|
+
class Outputs(BaseNode.Outputs):
|
|
478
|
+
foo: str
|
|
479
|
+
|
|
480
|
+
class MyWorkflow(BaseWorkflow):
|
|
481
|
+
graph = StartNode
|
|
482
|
+
|
|
483
|
+
# AND a DICTIONARY_REFERENCE descriptor
|
|
484
|
+
raw_descriptor = {
|
|
485
|
+
"type": "DICTIONARY_REFERENCE",
|
|
486
|
+
"entries": [
|
|
487
|
+
{
|
|
488
|
+
"id": "entry1",
|
|
489
|
+
"key": "key1",
|
|
490
|
+
"value": {"type": "CONSTANT_VALUE", "value": {"type": "STRING", "value": "value1"}},
|
|
491
|
+
},
|
|
492
|
+
{
|
|
493
|
+
"id": "entry2",
|
|
494
|
+
"key": "key2",
|
|
495
|
+
"value": {"type": "CONSTANT_VALUE", "value": {"type": "NUMBER", "value": 42}},
|
|
496
|
+
},
|
|
497
|
+
],
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
# WHEN we validate the descriptor
|
|
501
|
+
result = base_descriptor_validator(raw_descriptor, MyWorkflow)
|
|
502
|
+
|
|
503
|
+
# THEN we get a ConstantValueReference containing a dict
|
|
504
|
+
assert isinstance(result, ConstantValueReference)
|
|
505
|
+
assert isinstance(result._value, dict)
|
|
506
|
+
assert "key1" in result._value
|
|
507
|
+
assert "key2" in result._value
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
@pytest.mark.parametrize(
|
|
511
|
+
"operator,expected_type",
|
|
512
|
+
[
|
|
513
|
+
("blank", IsBlankExpression),
|
|
514
|
+
("notBlank", IsNotBlankExpression),
|
|
515
|
+
("null", IsNullExpression),
|
|
516
|
+
("notNull", IsNotNullExpression),
|
|
517
|
+
("isError", IsErrorExpression),
|
|
518
|
+
("length", LengthExpression),
|
|
519
|
+
("parseJson", ParseJsonExpression),
|
|
520
|
+
],
|
|
521
|
+
)
|
|
522
|
+
def test_base_descriptor_validator__unary_expression(operator, expected_type):
|
|
523
|
+
"""
|
|
524
|
+
Tests that UNARY_EXPRESSION descriptor types are correctly validated.
|
|
525
|
+
"""
|
|
526
|
+
|
|
527
|
+
# GIVEN a simple workflow
|
|
528
|
+
class StartNode(BaseNode):
|
|
529
|
+
class Outputs(BaseNode.Outputs):
|
|
530
|
+
foo: str
|
|
531
|
+
|
|
532
|
+
class MyWorkflow(BaseWorkflow):
|
|
533
|
+
graph = StartNode
|
|
534
|
+
|
|
535
|
+
# AND a UNARY_EXPRESSION descriptor
|
|
536
|
+
raw_descriptor = {
|
|
537
|
+
"type": "UNARY_EXPRESSION",
|
|
538
|
+
"operator": operator,
|
|
539
|
+
"lhs": {"type": "CONSTANT_VALUE", "value": {"type": "STRING", "value": "test"}},
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
# WHEN we validate the descriptor
|
|
543
|
+
result = base_descriptor_validator(raw_descriptor, MyWorkflow)
|
|
544
|
+
|
|
545
|
+
# THEN we get the expected expression type
|
|
546
|
+
assert isinstance(result, expected_type)
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
@pytest.mark.parametrize(
|
|
550
|
+
"operator,expected_type",
|
|
551
|
+
[
|
|
552
|
+
("between", BetweenExpression),
|
|
553
|
+
("notBetween", NotBetweenExpression),
|
|
554
|
+
],
|
|
555
|
+
)
|
|
556
|
+
def test_base_descriptor_validator__ternary_expression(operator, expected_type):
|
|
557
|
+
"""
|
|
558
|
+
Tests that TERNARY_EXPRESSION descriptor types are correctly validated.
|
|
559
|
+
"""
|
|
560
|
+
|
|
561
|
+
# GIVEN a simple workflow
|
|
562
|
+
class StartNode(BaseNode):
|
|
563
|
+
class Outputs(BaseNode.Outputs):
|
|
564
|
+
foo: str
|
|
565
|
+
|
|
566
|
+
class MyWorkflow(BaseWorkflow):
|
|
567
|
+
graph = StartNode
|
|
568
|
+
|
|
569
|
+
# AND a TERNARY_EXPRESSION descriptor
|
|
570
|
+
raw_descriptor = {
|
|
571
|
+
"type": "TERNARY_EXPRESSION",
|
|
572
|
+
"operator": operator,
|
|
573
|
+
"base": {"type": "CONSTANT_VALUE", "value": {"type": "NUMBER", "value": 5}},
|
|
574
|
+
"lhs": {"type": "CONSTANT_VALUE", "value": {"type": "NUMBER", "value": 1}},
|
|
575
|
+
"rhs": {"type": "CONSTANT_VALUE", "value": {"type": "NUMBER", "value": 10}},
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
# WHEN we validate the descriptor
|
|
579
|
+
result = base_descriptor_validator(raw_descriptor, MyWorkflow)
|
|
580
|
+
|
|
581
|
+
# THEN we get the expected expression type
|
|
582
|
+
assert isinstance(result, expected_type)
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
@pytest.mark.parametrize(
|
|
586
|
+
"operator,expected_type",
|
|
587
|
+
[
|
|
588
|
+
("=", BeginsWithExpression.__bases__[0].__bases__[0]),
|
|
589
|
+
("==", BeginsWithExpression.__bases__[0].__bases__[0]),
|
|
590
|
+
("doesNotContain", DoesNotContainExpression),
|
|
591
|
+
("doesNotBeginWith", DoesNotBeginWithExpression),
|
|
592
|
+
("doesNotEndWith", DoesNotEndWithExpression),
|
|
593
|
+
("in", InExpression),
|
|
594
|
+
("notIn", NotInExpression),
|
|
595
|
+
("and", AndExpression),
|
|
596
|
+
("or", OrExpression),
|
|
597
|
+
("coalesce", CoalesceExpression),
|
|
598
|
+
("+", AddExpression),
|
|
599
|
+
("-", MinusExpression),
|
|
600
|
+
("concat", ConcatExpression),
|
|
601
|
+
],
|
|
602
|
+
)
|
|
603
|
+
def test_base_descriptor_validator__binary_expression_additional_operators(operator, expected_type):
|
|
604
|
+
"""
|
|
605
|
+
Tests that additional BINARY_EXPRESSION operators are correctly validated.
|
|
606
|
+
"""
|
|
607
|
+
|
|
608
|
+
# GIVEN a simple workflow
|
|
609
|
+
class StartNode(BaseNode):
|
|
610
|
+
class Outputs(BaseNode.Outputs):
|
|
611
|
+
foo: str
|
|
612
|
+
|
|
613
|
+
class MyWorkflow(BaseWorkflow):
|
|
614
|
+
graph = StartNode
|
|
615
|
+
|
|
616
|
+
# AND a BINARY_EXPRESSION descriptor with the given operator
|
|
617
|
+
raw_descriptor = {
|
|
618
|
+
"type": "BINARY_EXPRESSION",
|
|
619
|
+
"operator": operator,
|
|
620
|
+
"lhs": {"type": "CONSTANT_VALUE", "value": {"type": "STRING", "value": "left"}},
|
|
621
|
+
"rhs": {"type": "CONSTANT_VALUE", "value": {"type": "STRING", "value": "right"}},
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
# WHEN we validate the descriptor
|
|
625
|
+
result = base_descriptor_validator(raw_descriptor, MyWorkflow)
|
|
626
|
+
|
|
627
|
+
# THEN we get the expected expression type
|
|
628
|
+
assert isinstance(result, expected_type)
|
|
629
|
+
|
|
630
|
+
|
|
631
|
+
def test_base_descriptor_validator__accessor_expression():
|
|
632
|
+
"""
|
|
633
|
+
Tests that accessField operator in BINARY_EXPRESSION is correctly validated.
|
|
634
|
+
"""
|
|
635
|
+
|
|
636
|
+
# GIVEN a simple workflow
|
|
637
|
+
class StartNode(BaseNode):
|
|
638
|
+
class Outputs(BaseNode.Outputs):
|
|
639
|
+
foo: str
|
|
640
|
+
|
|
641
|
+
class MyWorkflow(BaseWorkflow):
|
|
642
|
+
graph = StartNode
|
|
643
|
+
|
|
644
|
+
# AND a BINARY_EXPRESSION descriptor with accessField operator
|
|
645
|
+
raw_descriptor = {
|
|
646
|
+
"type": "BINARY_EXPRESSION",
|
|
647
|
+
"operator": "accessField",
|
|
648
|
+
"lhs": {"type": "CONSTANT_VALUE", "value": {"type": "JSON", "value": {"field": "value"}}},
|
|
649
|
+
"rhs": {"type": "CONSTANT_VALUE", "value": {"type": "STRING", "value": "field"}},
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
# WHEN we validate the descriptor
|
|
653
|
+
result = base_descriptor_validator(raw_descriptor, MyWorkflow)
|
|
654
|
+
|
|
655
|
+
# THEN we get an AccessorExpression
|
|
656
|
+
assert isinstance(result, AccessorExpression)
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
def test_mocks__validate_all__node_nested_in_subworkflow():
|
|
660
|
+
"""
|
|
661
|
+
Tests that MockNodeExecution.validate_all correctly handles mocks for nodes
|
|
662
|
+
nested within a subworkflow node by validating against the inner subworkflow.
|
|
663
|
+
"""
|
|
664
|
+
|
|
665
|
+
# GIVEN a node that will be nested inside a subworkflow
|
|
666
|
+
class NestedNode(BaseNode):
|
|
667
|
+
class Outputs(BaseNode.Outputs):
|
|
668
|
+
result: str
|
|
669
|
+
|
|
670
|
+
def run(self) -> Outputs:
|
|
671
|
+
raise NodeException("This node should be mocked")
|
|
672
|
+
|
|
673
|
+
# AND a subworkflow containing that nested node
|
|
674
|
+
class InnerSubworkflow(BaseWorkflow[BaseInputs, BaseState]):
|
|
675
|
+
graph = NestedNode
|
|
676
|
+
|
|
677
|
+
class Outputs(BaseWorkflow.Outputs):
|
|
678
|
+
inner_result = NestedNode.Outputs.result
|
|
679
|
+
|
|
680
|
+
# AND a subworkflow node that uses the inner subworkflow
|
|
681
|
+
class SubworkflowNode(InlineSubworkflowNode):
|
|
682
|
+
subworkflow = InnerSubworkflow
|
|
683
|
+
|
|
684
|
+
# AND an outer workflow containing the subworkflow node
|
|
685
|
+
class OuterWorkflow(BaseWorkflow):
|
|
686
|
+
graph = SubworkflowNode
|
|
687
|
+
|
|
688
|
+
class Outputs(BaseWorkflow.Outputs):
|
|
689
|
+
final_result = SubworkflowNode.Outputs.inner_result
|
|
690
|
+
|
|
691
|
+
# AND raw mock data for the nested node using the modern data model format
|
|
692
|
+
raw_mock_workflow_node_executions = [
|
|
693
|
+
{
|
|
694
|
+
"node_id": str(NestedNode.__id__),
|
|
695
|
+
"when_condition": {
|
|
696
|
+
"type": "BINARY_EXPRESSION",
|
|
697
|
+
"operator": ">=",
|
|
698
|
+
"lhs": {
|
|
699
|
+
"type": "EXECUTION_COUNTER",
|
|
700
|
+
"node_id": str(NestedNode.__id__),
|
|
701
|
+
},
|
|
702
|
+
"rhs": {
|
|
703
|
+
"type": "CONSTANT_VALUE",
|
|
704
|
+
"value": {
|
|
705
|
+
"type": "NUMBER",
|
|
706
|
+
"value": 0,
|
|
707
|
+
},
|
|
708
|
+
},
|
|
709
|
+
},
|
|
710
|
+
"then_outputs": {
|
|
711
|
+
"result": "mocked_result",
|
|
712
|
+
},
|
|
713
|
+
}
|
|
714
|
+
]
|
|
715
|
+
|
|
716
|
+
# WHEN we call validate_all on the outer workflow
|
|
717
|
+
node_output_mocks = MockNodeExecution.validate_all(
|
|
718
|
+
raw_mock_workflow_node_executions,
|
|
719
|
+
OuterWorkflow,
|
|
720
|
+
descriptor_validator=base_descriptor_validator,
|
|
721
|
+
)
|
|
722
|
+
|
|
723
|
+
# THEN we get a list of MockNodeExecution objects
|
|
724
|
+
assert node_output_mocks is not None
|
|
725
|
+
assert len(node_output_mocks) == 1
|
|
726
|
+
|
|
727
|
+
# AND the mock is correctly parsed with the nested node's outputs
|
|
728
|
+
assert node_output_mocks[0] == MockNodeExecution(
|
|
729
|
+
when_condition=NestedNode.Execution.count.greater_than_or_equal_to(0),
|
|
730
|
+
then_outputs=NestedNode.Outputs(result="mocked_result"),
|
|
731
|
+
)
|
|
732
|
+
|
|
733
|
+
# AND when we run the outer workflow with the mocks
|
|
734
|
+
workflow = OuterWorkflow()
|
|
735
|
+
terminal_event = workflow.run(node_output_mocks=node_output_mocks)
|
|
736
|
+
|
|
737
|
+
# THEN the workflow completes successfully
|
|
738
|
+
assert terminal_event.name == "workflow.execution.fulfilled", terminal_event
|
|
739
|
+
|
|
740
|
+
# AND the output reflects the mocked value from the nested node
|
|
741
|
+
assert terminal_event.outputs.final_result == "mocked_result"
|
|
742
|
+
|
|
743
|
+
|
|
744
|
+
def test_mocks__serialize__try_node_wrapped_node_has_correct_node_id():
|
|
745
|
+
"""
|
|
746
|
+
Tests that when serializing a MockNodeExecution for a node wrapped in a TryNode adornment,
|
|
747
|
+
the serialized node_id matches the inner wrapped node's ID (not the adornment wrapper's ID).
|
|
748
|
+
"""
|
|
749
|
+
|
|
750
|
+
# GIVEN a node wrapped in a TryNode adornment
|
|
751
|
+
@TryNode.wrap()
|
|
752
|
+
class WrappedNode(BaseNode):
|
|
753
|
+
class Outputs(BaseNode.Outputs):
|
|
754
|
+
result: str
|
|
755
|
+
|
|
756
|
+
# AND a workflow that uses the wrapped node
|
|
757
|
+
class MyWorkflow(BaseWorkflow):
|
|
758
|
+
graph = WrappedNode
|
|
759
|
+
|
|
760
|
+
class Outputs(BaseWorkflow.Outputs):
|
|
761
|
+
final_result = WrappedNode.Outputs.result
|
|
762
|
+
|
|
763
|
+
# AND a mock for the wrapped node
|
|
764
|
+
mock = MockNodeExecution(
|
|
765
|
+
when_condition=ConstantValueReference(True),
|
|
766
|
+
then_outputs=WrappedNode.Outputs(result="mocked_result"),
|
|
767
|
+
)
|
|
768
|
+
|
|
769
|
+
# WHEN we serialize the mock
|
|
770
|
+
serialized_mock = mock.model_dump()
|
|
771
|
+
|
|
772
|
+
# THEN the serialized node_id should match the inner wrapped node's ID
|
|
773
|
+
inner_node = WrappedNode.__wrapped_node__
|
|
774
|
+
assert inner_node is not None
|
|
775
|
+
assert serialized_mock["node_id"] == str(inner_node.__id__)
|
|
776
|
+
|
|
777
|
+
# AND the serialized mock should have the correct type
|
|
778
|
+
assert serialized_mock["type"] == "NODE_EXECUTION"
|
|
779
|
+
|
|
780
|
+
# AND the then_outputs should be serialized correctly
|
|
781
|
+
assert serialized_mock["then_outputs"]["result"] == "mocked_result"
|
|
782
|
+
|
|
783
|
+
|
|
784
|
+
def test_mocks__validate_all__try_node_wrapped_node_deserializes_correctly():
|
|
785
|
+
"""
|
|
786
|
+
Tests that validate_all correctly deserializes a mock for a node wrapped in a TryNode adornment.
|
|
787
|
+
"""
|
|
788
|
+
|
|
789
|
+
# GIVEN a node wrapped in a TryNode adornment
|
|
790
|
+
@TryNode.wrap()
|
|
791
|
+
class WrappedNode(BaseNode):
|
|
792
|
+
class Outputs(BaseNode.Outputs):
|
|
793
|
+
result: str
|
|
794
|
+
|
|
795
|
+
# AND a workflow that uses the wrapped node
|
|
796
|
+
class MyWorkflow(BaseWorkflow):
|
|
797
|
+
graph = WrappedNode
|
|
798
|
+
|
|
799
|
+
class Outputs(BaseWorkflow.Outputs):
|
|
800
|
+
final_result = WrappedNode.Outputs.result
|
|
801
|
+
|
|
802
|
+
# AND the inner wrapped node's ID
|
|
803
|
+
inner_node = WrappedNode.__wrapped_node__
|
|
804
|
+
assert inner_node is not None
|
|
805
|
+
|
|
806
|
+
# AND a raw mock workflow node execution using the inner node's ID
|
|
807
|
+
raw_mock_workflow_node_executions = [
|
|
808
|
+
{
|
|
809
|
+
"node_id": str(inner_node.__id__),
|
|
810
|
+
"when_condition": {
|
|
811
|
+
"type": "BINARY_EXPRESSION",
|
|
812
|
+
"operator": ">=",
|
|
813
|
+
"lhs": {
|
|
814
|
+
"type": "EXECUTION_COUNTER",
|
|
815
|
+
"node_id": str(inner_node.__id__),
|
|
816
|
+
},
|
|
817
|
+
"rhs": {
|
|
818
|
+
"type": "CONSTANT_VALUE",
|
|
819
|
+
"value": {
|
|
820
|
+
"type": "NUMBER",
|
|
821
|
+
"value": 1,
|
|
822
|
+
},
|
|
823
|
+
},
|
|
824
|
+
},
|
|
825
|
+
"then_outputs": {
|
|
826
|
+
"result": "mocked_result",
|
|
827
|
+
},
|
|
828
|
+
},
|
|
829
|
+
]
|
|
830
|
+
|
|
831
|
+
# WHEN we parse the raw data on MockNodeExecution
|
|
832
|
+
node_output_mocks = MockNodeExecution.validate_all(
|
|
833
|
+
raw_mock_workflow_node_executions,
|
|
834
|
+
MyWorkflow,
|
|
835
|
+
descriptor_validator=base_descriptor_validator,
|
|
836
|
+
)
|
|
837
|
+
|
|
838
|
+
# THEN we get a list with one MockNodeExecution object
|
|
839
|
+
assert node_output_mocks is not None
|
|
840
|
+
assert len(node_output_mocks) == 1
|
|
841
|
+
|
|
842
|
+
# AND the MockNodeExecution has the correct when_condition
|
|
843
|
+
assert node_output_mocks[0].when_condition == inner_node.Execution.count.greater_than_or_equal_to(1)
|
|
844
|
+
|
|
845
|
+
# AND the then_outputs is the correct type with the expected value
|
|
846
|
+
assert node_output_mocks[0].then_outputs == inner_node.Outputs(result="mocked_result")
|
|
847
|
+
|
|
848
|
+
|
|
849
|
+
def test_mocks__validate_all__ignores_undefined_outputs():
|
|
850
|
+
"""
|
|
851
|
+
Tests that validate_all ignores outputs that are not defined in the node's Outputs class.
|
|
852
|
+
"""
|
|
853
|
+
|
|
854
|
+
# GIVEN a node wrapped in a TryNode adornment
|
|
855
|
+
@TryNode.wrap()
|
|
856
|
+
class WrappedNode(BaseNode):
|
|
857
|
+
class Outputs(BaseNode.Outputs):
|
|
858
|
+
result: str
|
|
859
|
+
|
|
860
|
+
# AND a workflow that uses the wrapped node
|
|
861
|
+
class MyWorkflow(BaseWorkflow):
|
|
862
|
+
graph = WrappedNode
|
|
863
|
+
|
|
864
|
+
class Outputs(BaseWorkflow.Outputs):
|
|
865
|
+
final_result = WrappedNode.Outputs.result
|
|
866
|
+
|
|
867
|
+
# AND the inner wrapped node's ID
|
|
868
|
+
inner_node = WrappedNode.__wrapped_node__
|
|
869
|
+
assert inner_node is not None
|
|
870
|
+
|
|
871
|
+
# AND a raw mock workflow node execution with an undefined "error" output
|
|
872
|
+
raw_mock_workflow_node_executions = [
|
|
873
|
+
{
|
|
874
|
+
"node_id": str(inner_node.__id__),
|
|
875
|
+
"when_condition": {
|
|
876
|
+
"type": "BINARY_EXPRESSION",
|
|
877
|
+
"operator": ">=",
|
|
878
|
+
"lhs": {
|
|
879
|
+
"type": "EXECUTION_COUNTER",
|
|
880
|
+
"node_id": str(inner_node.__id__),
|
|
881
|
+
},
|
|
882
|
+
"rhs": {
|
|
883
|
+
"type": "CONSTANT_VALUE",
|
|
884
|
+
"value": {
|
|
885
|
+
"type": "NUMBER",
|
|
886
|
+
"value": 1,
|
|
887
|
+
},
|
|
888
|
+
},
|
|
889
|
+
},
|
|
890
|
+
"then_outputs": {
|
|
891
|
+
"result": "mocked_result",
|
|
892
|
+
"error": "some error value",
|
|
893
|
+
},
|
|
894
|
+
},
|
|
895
|
+
]
|
|
896
|
+
|
|
897
|
+
# WHEN we parse the raw data on MockNodeExecution
|
|
898
|
+
node_output_mocks = MockNodeExecution.validate_all(
|
|
899
|
+
raw_mock_workflow_node_executions,
|
|
900
|
+
MyWorkflow,
|
|
901
|
+
descriptor_validator=base_descriptor_validator,
|
|
902
|
+
)
|
|
903
|
+
|
|
904
|
+
# THEN we get a list with one MockNodeExecution object
|
|
905
|
+
assert node_output_mocks is not None
|
|
906
|
+
assert len(node_output_mocks) == 1
|
|
907
|
+
|
|
908
|
+
# AND the then_outputs only contains the declared "result" output
|
|
909
|
+
assert node_output_mocks[0].then_outputs == inner_node.Outputs(result="mocked_result")
|
|
910
|
+
|
|
911
|
+
# AND the "error" output was ignored and is not present
|
|
912
|
+
assert not hasattr(node_output_mocks[0].then_outputs, "error")
|