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
|
@@ -5,6 +5,10 @@ from uuid import UUID
|
|
|
5
5
|
from deepdiff import DeepDiff
|
|
6
6
|
|
|
7
7
|
from vellum.client.core.pydantic_utilities import UniversalBaseModel
|
|
8
|
+
from vellum.client.types import (
|
|
9
|
+
NodeExecutionFulfilledEvent as ClientNodeExecutionFulfilledEvent,
|
|
10
|
+
WorkflowExecutionFulfilledEvent as ClientWorkflowExecutionFulfilledEvent,
|
|
11
|
+
)
|
|
8
12
|
from vellum.workflows.constants import undefined
|
|
9
13
|
from vellum.workflows.errors.types import WorkflowError, WorkflowErrorCode
|
|
10
14
|
from vellum.workflows.events.exception_handling import stream_initialization_exception
|
|
@@ -13,6 +17,8 @@ from vellum.workflows.events.node import (
|
|
|
13
17
|
NodeExecutionFulfilledEvent,
|
|
14
18
|
NodeExecutionInitiatedBody,
|
|
15
19
|
NodeExecutionInitiatedEvent,
|
|
20
|
+
NodeExecutionLogBody,
|
|
21
|
+
NodeExecutionLogEvent,
|
|
16
22
|
NodeExecutionStreamingBody,
|
|
17
23
|
NodeExecutionStreamingEvent,
|
|
18
24
|
)
|
|
@@ -37,6 +43,7 @@ from vellum.workflows.state.base import BaseState
|
|
|
37
43
|
from vellum.workflows.types.core import VellumSecret
|
|
38
44
|
from vellum.workflows.utils.uuids import uuid4_from_hash
|
|
39
45
|
from vellum.workflows.workflows.base import BaseWorkflow
|
|
46
|
+
from vellum.workflows.workflows.event_filters import all_workflow_event_filter
|
|
40
47
|
|
|
41
48
|
|
|
42
49
|
class MockInputs(BaseInputs):
|
|
@@ -471,6 +478,41 @@ mock_node_uuid = str(MockNode.__id__)
|
|
|
471
478
|
"links": None,
|
|
472
479
|
},
|
|
473
480
|
),
|
|
481
|
+
(
|
|
482
|
+
NodeExecutionLogEvent(
|
|
483
|
+
id=UUID("123e4567-e89b-12d3-a456-426614174000"),
|
|
484
|
+
timestamp=datetime(2024, 1, 1, 12, 0, 0, tzinfo=timezone.utc),
|
|
485
|
+
trace_id=UUID("123e4567-e89b-12d3-a456-426614174000"),
|
|
486
|
+
span_id=UUID("123e4567-e89b-12d3-a456-426614174000"),
|
|
487
|
+
body=NodeExecutionLogBody(
|
|
488
|
+
node_definition=MockNode,
|
|
489
|
+
attributes={"foo": "bar"},
|
|
490
|
+
severity="INFO",
|
|
491
|
+
message="Test log message",
|
|
492
|
+
),
|
|
493
|
+
),
|
|
494
|
+
{
|
|
495
|
+
"id": "123e4567-e89b-12d3-a456-426614174000",
|
|
496
|
+
"api_version": "2024-10-25",
|
|
497
|
+
"timestamp": "2024-01-01T12:00:00Z",
|
|
498
|
+
"trace_id": "123e4567-e89b-12d3-a456-426614174000",
|
|
499
|
+
"span_id": "123e4567-e89b-12d3-a456-426614174000",
|
|
500
|
+
"name": "node.execution.log",
|
|
501
|
+
"body": {
|
|
502
|
+
"node_definition": {
|
|
503
|
+
"id": mock_node_uuid,
|
|
504
|
+
"name": "MockNode",
|
|
505
|
+
"module": module_root + ["events", "tests", "test_event"],
|
|
506
|
+
"exclude_from_monitoring": False,
|
|
507
|
+
},
|
|
508
|
+
"attributes": {"foo": "bar"},
|
|
509
|
+
"severity": "INFO",
|
|
510
|
+
"message": "Test log message",
|
|
511
|
+
},
|
|
512
|
+
"parent": None,
|
|
513
|
+
"links": None,
|
|
514
|
+
},
|
|
515
|
+
),
|
|
474
516
|
],
|
|
475
517
|
ids=[
|
|
476
518
|
"workflow.execution.initiated",
|
|
@@ -483,6 +525,7 @@ mock_node_uuid = str(MockNode.__id__)
|
|
|
483
525
|
"node.execution.fulfilled",
|
|
484
526
|
"fulfilled_node_with_undefined_outputs",
|
|
485
527
|
"mocked_node",
|
|
528
|
+
"node.execution.log",
|
|
486
529
|
],
|
|
487
530
|
)
|
|
488
531
|
def test_event_serialization(event, expected_json):
|
|
@@ -573,3 +616,83 @@ def test_node_execution_initiated_event_includes_exclude_from_monitoring():
|
|
|
573
616
|
serialized = else_node_event.model_dump(mode="json")
|
|
574
617
|
assert "exclude_from_monitoring" in serialized["body"]["node_definition"]
|
|
575
618
|
assert serialized["body"]["node_definition"]["exclude_from_monitoring"] is True
|
|
619
|
+
|
|
620
|
+
|
|
621
|
+
def test_event_max_size__outputs_redacted_when_exceeds_limit():
|
|
622
|
+
"""
|
|
623
|
+
Tests that event outputs are redacted when serialized size exceeds event_max_size.
|
|
624
|
+
"""
|
|
625
|
+
|
|
626
|
+
# GIVEN a workflow with a node
|
|
627
|
+
workflow = MockWorkflow()
|
|
628
|
+
|
|
629
|
+
# WHEN the workflow is streamed with a very small event_max_size
|
|
630
|
+
events = list(
|
|
631
|
+
workflow.stream(inputs=MockInputs(foo="bar"), event_filter=all_workflow_event_filter, event_max_size=10)
|
|
632
|
+
)
|
|
633
|
+
|
|
634
|
+
# THEN both node and workflow fulfilled events should have redacted outputs
|
|
635
|
+
node_fulfilled_events = [e for e in events if e.name == "node.execution.fulfilled"]
|
|
636
|
+
workflow_fulfilled_events = [e for e in events if e.name == "workflow.execution.fulfilled"]
|
|
637
|
+
|
|
638
|
+
assert len(node_fulfilled_events) > 0, "Expected at least one node fulfilled event"
|
|
639
|
+
assert len(workflow_fulfilled_events) == 1, "Expected exactly one workflow fulfilled event"
|
|
640
|
+
|
|
641
|
+
# AND the node fulfilled event outputs should be empty
|
|
642
|
+
node_serialized = node_fulfilled_events[0].model_dump(mode="json")
|
|
643
|
+
assert node_serialized["body"]["outputs"] == {}
|
|
644
|
+
|
|
645
|
+
# AND the serialized node event should be parseable by the client library
|
|
646
|
+
ClientNodeExecutionFulfilledEvent.model_validate(node_serialized)
|
|
647
|
+
|
|
648
|
+
# AND the workflow fulfilled event outputs should be empty
|
|
649
|
+
workflow_serialized = workflow_fulfilled_events[0].model_dump(mode="json")
|
|
650
|
+
assert workflow_serialized["body"]["outputs"] == {}
|
|
651
|
+
|
|
652
|
+
# AND the serialized workflow event should be parseable by the client library
|
|
653
|
+
ClientWorkflowExecutionFulfilledEvent.model_validate(workflow_serialized)
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
def test_event_max_size__outputs_preserved_when_under_limit():
|
|
657
|
+
"""
|
|
658
|
+
Tests that event outputs are preserved when serialized size is under event_max_size.
|
|
659
|
+
"""
|
|
660
|
+
|
|
661
|
+
# GIVEN a workflow with a node that produces outputs
|
|
662
|
+
class OutputNode(BaseNode):
|
|
663
|
+
class Outputs(BaseNode.Outputs):
|
|
664
|
+
result: str = "hello world"
|
|
665
|
+
|
|
666
|
+
class OutputWorkflow(BaseWorkflow[MockInputs, BaseState]):
|
|
667
|
+
graph = OutputNode
|
|
668
|
+
|
|
669
|
+
class Outputs(BaseWorkflow.Outputs):
|
|
670
|
+
final = OutputNode.Outputs.result
|
|
671
|
+
|
|
672
|
+
workflow = OutputWorkflow()
|
|
673
|
+
|
|
674
|
+
# WHEN the workflow is streamed with a large event_max_size
|
|
675
|
+
events = list(
|
|
676
|
+
workflow.stream(inputs=MockInputs(foo="bar"), event_filter=all_workflow_event_filter, event_max_size=100000)
|
|
677
|
+
)
|
|
678
|
+
|
|
679
|
+
# THEN both node and workflow fulfilled events should have their outputs preserved
|
|
680
|
+
node_fulfilled_events = [e for e in events if e.name == "node.execution.fulfilled"]
|
|
681
|
+
workflow_fulfilled_events = [e for e in events if e.name == "workflow.execution.fulfilled"]
|
|
682
|
+
|
|
683
|
+
assert len(node_fulfilled_events) > 0, "Expected at least one node fulfilled event"
|
|
684
|
+
assert len(workflow_fulfilled_events) == 1, "Expected exactly one workflow fulfilled event"
|
|
685
|
+
|
|
686
|
+
# AND the node fulfilled event outputs should contain the result
|
|
687
|
+
node_serialized = node_fulfilled_events[0].model_dump(mode="json")
|
|
688
|
+
assert node_serialized["body"]["outputs"]["result"] == "hello world"
|
|
689
|
+
|
|
690
|
+
# AND the serialized node event should be parseable by the client library
|
|
691
|
+
ClientNodeExecutionFulfilledEvent.model_validate(node_serialized)
|
|
692
|
+
|
|
693
|
+
# AND the workflow fulfilled event outputs should contain the final output
|
|
694
|
+
workflow_serialized = workflow_fulfilled_events[0].model_dump(mode="json")
|
|
695
|
+
assert workflow_serialized["body"]["outputs"]["final"] == "hello world"
|
|
696
|
+
|
|
697
|
+
# AND the serialized workflow event should be parseable by the client library
|
|
698
|
+
ClientWorkflowExecutionFulfilledEvent.model_validate(workflow_serialized)
|
vellum/workflows/events/types.py
CHANGED
|
@@ -3,7 +3,7 @@ import json
|
|
|
3
3
|
from uuid import UUID, uuid4
|
|
4
4
|
from typing import Annotated, Any, List, Literal, Optional, Union, get_args
|
|
5
5
|
|
|
6
|
-
from pydantic import Field, GetCoreSchemaHandler, Tag, ValidationInfo
|
|
6
|
+
from pydantic import Field, GetCoreSchemaHandler, PrivateAttr, Tag, ValidationInfo
|
|
7
7
|
from pydantic_core import CoreSchema, core_schema
|
|
8
8
|
|
|
9
9
|
from vellum.client.core.pydantic_utilities import UniversalBaseModel
|
|
@@ -186,3 +186,4 @@ class BaseEvent(UniversalBaseModel):
|
|
|
186
186
|
span_id: UUID
|
|
187
187
|
parent: Optional[ParentContext] = None
|
|
188
188
|
links: Optional[List[SpanLink]] = None
|
|
189
|
+
_event_max_size: Optional[int] = PrivateAttr(default=None)
|
|
@@ -1,11 +1,13 @@
|
|
|
1
|
+
import json
|
|
1
2
|
import logging
|
|
2
3
|
from uuid import UUID
|
|
3
4
|
from typing import TYPE_CHECKING, Any, Dict, Generic, Iterable, Literal, Optional, Type, Union, cast
|
|
4
5
|
from typing_extensions import TypeGuard
|
|
5
6
|
|
|
6
|
-
from pydantic import SerializationInfo, field_serializer, field_validator
|
|
7
|
+
from pydantic import Field, SerializationInfo, field_serializer, field_validator, model_serializer
|
|
7
8
|
|
|
8
9
|
from vellum.client.core.pydantic_utilities import UniversalBaseModel
|
|
10
|
+
from vellum.utils.json_encoder import VellumJsonEncoder
|
|
9
11
|
from vellum.workflows.errors import WorkflowError
|
|
10
12
|
from vellum.workflows.outputs.base import BaseOutput
|
|
11
13
|
from vellum.workflows.references import ExternalInputReference
|
|
@@ -16,6 +18,7 @@ from ..triggers import BaseTrigger
|
|
|
16
18
|
from .node import (
|
|
17
19
|
NodeExecutionFulfilledEvent,
|
|
18
20
|
NodeExecutionInitiatedEvent,
|
|
21
|
+
NodeExecutionLogEvent,
|
|
19
22
|
NodeExecutionPausedEvent,
|
|
20
23
|
NodeExecutionRejectedEvent,
|
|
21
24
|
NodeExecutionResumedEvent,
|
|
@@ -111,9 +114,17 @@ class WorkflowExecutionInitiatedBody(_BaseWorkflowExecutionBody, Generic[InputsT
|
|
|
111
114
|
|
|
112
115
|
trigger: Optional[Type[BaseTrigger]] = None
|
|
113
116
|
|
|
117
|
+
# Raw inputs from trigger event data, used to include trigger attributes in serialized inputs.
|
|
118
|
+
# This field is excluded from serialization and only used to merge into the inputs field.
|
|
119
|
+
raw_inputs: Optional[Dict[str, Any]] = Field(default=None, exclude=True)
|
|
120
|
+
|
|
114
121
|
@field_serializer("inputs")
|
|
115
122
|
def serialize_inputs(self, inputs: InputsType, _info: Any) -> Dict[str, Any]:
|
|
116
|
-
|
|
123
|
+
serialized = default_serializer(inputs)
|
|
124
|
+
# Merge raw_inputs (trigger event data) with serialized inputs
|
|
125
|
+
if self.raw_inputs:
|
|
126
|
+
return {**self.raw_inputs, **serialized}
|
|
127
|
+
return serialized
|
|
117
128
|
|
|
118
129
|
@field_serializer("initial_state")
|
|
119
130
|
def serialize_initial_state(self, initial_state: Optional[StateType], _info: Any) -> Optional[Dict[str, Any]]:
|
|
@@ -203,6 +214,20 @@ class WorkflowExecutionFulfilledEvent(_BaseWorkflowEvent, Generic[OutputsType, S
|
|
|
203
214
|
WorkflowExecutionFulfilledBody[OutputsType, StateType], _serialize_body_with_enricher(self, body, info)
|
|
204
215
|
)
|
|
205
216
|
|
|
217
|
+
@model_serializer(mode="plain", when_used="json")
|
|
218
|
+
def serialize_model(self, info: SerializationInfo) -> Dict[str, Any]:
|
|
219
|
+
serialized = super().serialize_model(info)
|
|
220
|
+
|
|
221
|
+
if (
|
|
222
|
+
self._event_max_size is not None
|
|
223
|
+
and len(json.dumps(serialized, cls=VellumJsonEncoder)) > self._event_max_size
|
|
224
|
+
and "body" in serialized
|
|
225
|
+
and isinstance(serialized["body"], dict)
|
|
226
|
+
):
|
|
227
|
+
serialized["body"]["outputs"] = {}
|
|
228
|
+
|
|
229
|
+
return serialized
|
|
230
|
+
|
|
206
231
|
|
|
207
232
|
class WorkflowExecutionRejectedBody(_BaseWorkflowExecutionBody):
|
|
208
233
|
error: WorkflowError
|
|
@@ -287,6 +312,7 @@ GenericWorkflowEvent = Union[
|
|
|
287
312
|
NodeExecutionRejectedEvent,
|
|
288
313
|
NodeExecutionPausedEvent,
|
|
289
314
|
NodeExecutionResumedEvent,
|
|
315
|
+
NodeExecutionLogEvent,
|
|
290
316
|
]
|
|
291
317
|
|
|
292
318
|
WorkflowEvent = Union[
|
|
@@ -38,4 +38,7 @@ class AddExpression(BaseDescriptor[Any], Generic[LHS, RHS]):
|
|
|
38
38
|
if not has_add(lhs):
|
|
39
39
|
raise InvalidExpressionException(f"'{lhs.__class__.__name__}' must support the '+' operator")
|
|
40
40
|
|
|
41
|
+
if isinstance(lhs, list) and not isinstance(rhs, list):
|
|
42
|
+
return lhs + [rhs]
|
|
43
|
+
|
|
41
44
|
return lhs + rhs
|
|
@@ -70,3 +70,27 @@ def test_add_expression_types():
|
|
|
70
70
|
expression = AddExpression(lhs=5, rhs=3)
|
|
71
71
|
|
|
72
72
|
assert expression.types == (object,)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_add_expression_list_plus_non_list():
|
|
76
|
+
"""
|
|
77
|
+
Tests that AddExpression correctly handles list + non-list by wrapping non-list in a list.
|
|
78
|
+
This allows State.chat_history + ChatMessage(...) to work.
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
state = TestState()
|
|
82
|
+
|
|
83
|
+
# GIVEN a list and a non-list value
|
|
84
|
+
my_list = [1, 2, 3]
|
|
85
|
+
single_item = 4
|
|
86
|
+
|
|
87
|
+
# WHEN we create an AddExpression and resolve it
|
|
88
|
+
expression = AddExpression(lhs=my_list, rhs=single_item)
|
|
89
|
+
result = expression.resolve(state)
|
|
90
|
+
|
|
91
|
+
# THEN the result should be the list with the item appended
|
|
92
|
+
assert result == [1, 2, 3, 4]
|
|
93
|
+
assert isinstance(result, list)
|
|
94
|
+
|
|
95
|
+
# AND the original list should be unchanged
|
|
96
|
+
assert my_list == [1, 2, 3]
|
vellum/workflows/graph/graph.py
CHANGED
|
@@ -81,12 +81,16 @@ class Graph:
|
|
|
81
81
|
entrypoints = set()
|
|
82
82
|
edges = OrderedSet[Edge]()
|
|
83
83
|
terminals = set()
|
|
84
|
+
trigger_edges: List[TriggerEdge] = []
|
|
84
85
|
|
|
85
86
|
for target in targets:
|
|
86
87
|
if isinstance(target, Graph):
|
|
87
88
|
entrypoints.update(target._entrypoints)
|
|
88
89
|
edges.update(target._edges)
|
|
89
90
|
terminals.update(target._terminals)
|
|
91
|
+
for trigger_edge in target._trigger_edges:
|
|
92
|
+
if trigger_edge not in trigger_edges:
|
|
93
|
+
trigger_edges.append(trigger_edge)
|
|
90
94
|
elif hasattr(target, "Ports"):
|
|
91
95
|
entrypoints.update({port for port in target.Ports})
|
|
92
96
|
terminals.update({port for port in target.Ports})
|
|
@@ -95,11 +99,14 @@ class Graph:
|
|
|
95
99
|
entrypoints.update({target})
|
|
96
100
|
terminals.update({target})
|
|
97
101
|
|
|
98
|
-
return Graph(entrypoints=entrypoints, edges=list(edges), terminals=terminals)
|
|
102
|
+
return Graph(entrypoints=entrypoints, edges=list(edges), terminals=terminals, trigger_edges=trigger_edges)
|
|
99
103
|
|
|
100
104
|
@staticmethod
|
|
101
105
|
def from_edge(edge: Edge) -> "Graph":
|
|
102
|
-
|
|
106
|
+
terminals: Set[Union["Port", "NoPortsNode"]] = {port for port in edge.to_node.Ports}
|
|
107
|
+
if not terminals:
|
|
108
|
+
terminals = {NoPortsNode(edge.to_node)}
|
|
109
|
+
return Graph(entrypoints={edge.from_port}, edges=[edge], terminals=terminals)
|
|
103
110
|
|
|
104
111
|
@staticmethod
|
|
105
112
|
def from_trigger_edge(edge: TriggerEdge) -> "Graph":
|
|
@@ -166,6 +173,7 @@ class Graph:
|
|
|
166
173
|
|
|
167
174
|
def __rshift__(self, other: GraphTarget) -> "Graph":
|
|
168
175
|
# Check for trigger target (class-level only)
|
|
176
|
+
from vellum.workflows.nodes.bases.base import BaseNode
|
|
169
177
|
from vellum.workflows.triggers.base import BaseTrigger
|
|
170
178
|
|
|
171
179
|
if isinstance(other, type) and issubclass(other, BaseTrigger):
|
|
@@ -197,13 +205,16 @@ class Graph:
|
|
|
197
205
|
midgraph = final_output_node >> set(elem.entrypoints)
|
|
198
206
|
self._extend_edges(midgraph.edges)
|
|
199
207
|
self._extend_edges(elem.edges)
|
|
208
|
+
self._extend_trigger_edges(elem._trigger_edges)
|
|
200
209
|
for other_terminal in elem._terminals:
|
|
201
210
|
new_terminals.add(other_terminal)
|
|
202
211
|
elif hasattr(elem, "Ports"):
|
|
203
212
|
midgraph = final_output_node >> elem
|
|
204
213
|
self._extend_edges(midgraph.edges)
|
|
205
|
-
for
|
|
206
|
-
|
|
214
|
+
other_ports = {port for port in elem.Ports}
|
|
215
|
+
if isinstance(elem, type) and issubclass(elem, BaseNode) and not other_ports:
|
|
216
|
+
other_ports = {NoPortsNode(elem)}
|
|
217
|
+
new_terminals.update(other_ports)
|
|
207
218
|
else:
|
|
208
219
|
# elem is a Port
|
|
209
220
|
midgraph = final_output_node >> elem
|
|
@@ -219,6 +230,7 @@ class Graph:
|
|
|
219
230
|
midgraph = final_output_node >> set(other.entrypoints)
|
|
220
231
|
self._extend_edges(midgraph.edges)
|
|
221
232
|
self._extend_edges(other.edges)
|
|
233
|
+
self._extend_trigger_edges(other._trigger_edges)
|
|
222
234
|
self._terminals = other._terminals
|
|
223
235
|
return self
|
|
224
236
|
|
|
@@ -228,7 +240,11 @@ class Graph:
|
|
|
228
240
|
continue
|
|
229
241
|
subgraph = final_output_node >> other
|
|
230
242
|
self._extend_edges(subgraph.edges)
|
|
231
|
-
|
|
243
|
+
|
|
244
|
+
other_ports = {port for port in other.Ports}
|
|
245
|
+
if isinstance(other, type) and issubclass(other, BaseNode) and not other_ports:
|
|
246
|
+
other_ports = {NoPortsNode(other)}
|
|
247
|
+
self._terminals = other_ports
|
|
232
248
|
return self
|
|
233
249
|
|
|
234
250
|
# other is a Port
|
|
@@ -298,6 +314,11 @@ class Graph:
|
|
|
298
314
|
if edge not in self._edges:
|
|
299
315
|
self._edges.append(edge)
|
|
300
316
|
|
|
317
|
+
def _extend_trigger_edges(self, trigger_edges: List[TriggerEdge]) -> None:
|
|
318
|
+
for trigger_edge in trigger_edges:
|
|
319
|
+
if trigger_edge not in self._trigger_edges:
|
|
320
|
+
self._trigger_edges.append(trigger_edge)
|
|
321
|
+
|
|
301
322
|
def __str__(self) -> str:
|
|
302
323
|
"""
|
|
303
324
|
Return a visual ASCII representation of the graph showing the flow structure.
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import pytest
|
|
2
|
+
from typing import Type
|
|
2
3
|
|
|
3
4
|
from vellum.workflows.edges.edge import Edge
|
|
4
|
-
from vellum.workflows.graph.graph import Graph
|
|
5
|
+
from vellum.workflows.graph.graph import Graph, NoPortsNode
|
|
5
6
|
from vellum.workflows.nodes.bases.base import BaseNode
|
|
7
|
+
from vellum.workflows.nodes.displayable.final_output_node import FinalOutputNode
|
|
6
8
|
from vellum.workflows.ports.port import Port
|
|
9
|
+
from vellum.workflows.state.base import BaseState
|
|
7
10
|
from vellum.workflows.triggers import ManualTrigger
|
|
11
|
+
from vellum.workflows.triggers.schedule import ScheduleTrigger
|
|
8
12
|
|
|
9
13
|
|
|
10
14
|
def test_graph__empty():
|
|
@@ -784,3 +788,226 @@ def test_graph__trigger_then_graph_then_node():
|
|
|
784
788
|
# AND the graph has both nodes
|
|
785
789
|
nodes = list(graph.nodes)
|
|
786
790
|
assert len(nodes) == 2
|
|
791
|
+
|
|
792
|
+
|
|
793
|
+
def test_graph__set_of_trigger_graphs_preserves_trigger_edges():
|
|
794
|
+
"""Test that combining graphs with triggers via a set preserves trigger edges.
|
|
795
|
+
|
|
796
|
+
This tests the fix for Graph.from_set() not propagating _trigger_edges.
|
|
797
|
+
"""
|
|
798
|
+
|
|
799
|
+
# GIVEN a custom scheduled trigger
|
|
800
|
+
class MyScheduledTrigger(ScheduleTrigger):
|
|
801
|
+
class Config:
|
|
802
|
+
cron = "0 9 * * *"
|
|
803
|
+
|
|
804
|
+
# AND two trigger-initiated graphs
|
|
805
|
+
class MyFirstNode(BaseNode):
|
|
806
|
+
pass
|
|
807
|
+
|
|
808
|
+
class MySecondNode(BaseNode):
|
|
809
|
+
pass
|
|
810
|
+
|
|
811
|
+
trigger_graph_a = MyScheduledTrigger >> MyFirstNode
|
|
812
|
+
trigger_graph_b = MyScheduledTrigger >> MySecondNode
|
|
813
|
+
|
|
814
|
+
# WHEN we combine them in a set
|
|
815
|
+
combined_graph = Graph.from_set({trigger_graph_a, trigger_graph_b})
|
|
816
|
+
|
|
817
|
+
# THEN the combined graph has both trigger edges
|
|
818
|
+
trigger_edges = list(combined_graph.trigger_edges)
|
|
819
|
+
assert len(trigger_edges) == 2
|
|
820
|
+
|
|
821
|
+
# AND both edges point to the correct nodes
|
|
822
|
+
target_nodes = {edge.to_node for edge in trigger_edges}
|
|
823
|
+
assert target_nodes == {MyFirstNode, MySecondNode}
|
|
824
|
+
|
|
825
|
+
# AND the graph exposes the trigger
|
|
826
|
+
triggers = list(combined_graph.triggers)
|
|
827
|
+
assert len(triggers) == 1
|
|
828
|
+
assert triggers[0] == MyScheduledTrigger
|
|
829
|
+
|
|
830
|
+
|
|
831
|
+
def test_graph__set_of_trigger_graphs_to_node_preserves_trigger_edges():
|
|
832
|
+
# GIVEN a custom scheduled trigger
|
|
833
|
+
class MyScheduledTrigger(ScheduleTrigger):
|
|
834
|
+
class Config:
|
|
835
|
+
cron = "0 9 * * *"
|
|
836
|
+
|
|
837
|
+
# AND two trigger-initiated graphs
|
|
838
|
+
class MyFirstNode(BaseNode):
|
|
839
|
+
pass
|
|
840
|
+
|
|
841
|
+
class MySecondNode(BaseNode):
|
|
842
|
+
pass
|
|
843
|
+
|
|
844
|
+
class MyThirdNode(BaseNode):
|
|
845
|
+
pass
|
|
846
|
+
|
|
847
|
+
trigger_graph_a = MyScheduledTrigger >> MyFirstNode
|
|
848
|
+
trigger_graph_b = MyScheduledTrigger >> MySecondNode
|
|
849
|
+
|
|
850
|
+
# WHEN we combine them in a set and connect to another node
|
|
851
|
+
combined_graph = {trigger_graph_a, trigger_graph_b} >> MyThirdNode
|
|
852
|
+
|
|
853
|
+
# THEN the combined graph has both trigger edges
|
|
854
|
+
trigger_edges = list(combined_graph.trigger_edges)
|
|
855
|
+
assert len(trigger_edges) == 2
|
|
856
|
+
|
|
857
|
+
# AND both trigger edges point to the correct initial nodes
|
|
858
|
+
trigger_target_nodes = {edge.to_node for edge in trigger_edges}
|
|
859
|
+
assert trigger_target_nodes == {MyFirstNode, MySecondNode}
|
|
860
|
+
|
|
861
|
+
# AND the graph has regular edges to the third node
|
|
862
|
+
regular_edges = list(combined_graph.edges)
|
|
863
|
+
assert len(regular_edges) == 2
|
|
864
|
+
assert all(edge.to_node == MyThirdNode for edge in regular_edges)
|
|
865
|
+
|
|
866
|
+
# AND the graph has all three nodes
|
|
867
|
+
nodes = list(combined_graph.nodes)
|
|
868
|
+
assert len(nodes) == 3
|
|
869
|
+
assert set(nodes) == {MyFirstNode, MySecondNode, MyThirdNode}
|
|
870
|
+
|
|
871
|
+
|
|
872
|
+
def test_graph__graph_rshift_graph_preserves_trigger_edges():
|
|
873
|
+
"""Test that Graph >> Graph preserves trigger edges from the right-hand graph."""
|
|
874
|
+
|
|
875
|
+
# GIVEN a regular graph and a trigger-initiated graph
|
|
876
|
+
class StartNode(BaseNode):
|
|
877
|
+
pass
|
|
878
|
+
|
|
879
|
+
class TriggerNode(BaseNode):
|
|
880
|
+
pass
|
|
881
|
+
|
|
882
|
+
class EndNode(BaseNode):
|
|
883
|
+
pass
|
|
884
|
+
|
|
885
|
+
regular_graph = StartNode >> TriggerNode
|
|
886
|
+
trigger_graph = ManualTrigger >> EndNode
|
|
887
|
+
|
|
888
|
+
# WHEN we combine them with >>
|
|
889
|
+
combined = regular_graph >> trigger_graph
|
|
890
|
+
|
|
891
|
+
# THEN the combined graph has the trigger edge
|
|
892
|
+
trigger_edges = list(combined.trigger_edges)
|
|
893
|
+
assert len(trigger_edges) == 1
|
|
894
|
+
assert trigger_edges[0].to_node == EndNode
|
|
895
|
+
|
|
896
|
+
# AND the graph exposes the trigger
|
|
897
|
+
triggers = list(combined.triggers)
|
|
898
|
+
assert len(triggers) == 1
|
|
899
|
+
|
|
900
|
+
|
|
901
|
+
def test_graph__graph_rshift_set_of_trigger_graphs_preserves_trigger_edges():
|
|
902
|
+
"""Test that Graph >> {TriggerGraph1, TriggerGraph2} preserves trigger edges."""
|
|
903
|
+
|
|
904
|
+
# GIVEN a regular graph and two trigger-initiated graphs
|
|
905
|
+
class StartNode(BaseNode):
|
|
906
|
+
pass
|
|
907
|
+
|
|
908
|
+
class TriggerNodeA(BaseNode):
|
|
909
|
+
pass
|
|
910
|
+
|
|
911
|
+
class TriggerNodeB(BaseNode):
|
|
912
|
+
pass
|
|
913
|
+
|
|
914
|
+
regular_graph = Graph.from_node(StartNode)
|
|
915
|
+
trigger_graph_a = ManualTrigger >> TriggerNodeA
|
|
916
|
+
trigger_graph_b = ManualTrigger >> TriggerNodeB
|
|
917
|
+
|
|
918
|
+
# WHEN we combine them with >> and a set
|
|
919
|
+
combined = regular_graph >> {trigger_graph_a, trigger_graph_b}
|
|
920
|
+
|
|
921
|
+
# THEN the combined graph has both trigger edges
|
|
922
|
+
trigger_edges = list(combined.trigger_edges)
|
|
923
|
+
assert len(trigger_edges) == 2
|
|
924
|
+
|
|
925
|
+
# AND both trigger edges point to the correct nodes
|
|
926
|
+
trigger_target_nodes = {edge.to_node for edge in trigger_edges}
|
|
927
|
+
assert trigger_target_nodes == {TriggerNodeA, TriggerNodeB}
|
|
928
|
+
|
|
929
|
+
|
|
930
|
+
class TestFinalOutputNoPortsNode:
|
|
931
|
+
"""Test that we preserve final output nodes as NoPortsNode when constructing a Graph."""
|
|
932
|
+
|
|
933
|
+
def validate_graph(self, graph: Graph, final_output_node: Type[FinalOutputNode]):
|
|
934
|
+
assert len(list(graph._terminals)) == 1
|
|
935
|
+
node = list(graph._terminals)[0]
|
|
936
|
+
assert isinstance(node, NoPortsNode)
|
|
937
|
+
assert node.node_class == final_output_node
|
|
938
|
+
|
|
939
|
+
def test_from_edge(self):
|
|
940
|
+
# GIVEN
|
|
941
|
+
class StartNode(BaseNode[BaseState]):
|
|
942
|
+
pass
|
|
943
|
+
|
|
944
|
+
class MyFinalOutput(FinalOutputNode[BaseState, str]):
|
|
945
|
+
pass
|
|
946
|
+
|
|
947
|
+
# WHEN
|
|
948
|
+
edge = Edge(from_port=StartNode.Ports.default, to_node=MyFinalOutput)
|
|
949
|
+
graph = Graph.from_edge(edge)
|
|
950
|
+
|
|
951
|
+
# THEN
|
|
952
|
+
self.validate_graph(graph, MyFinalOutput)
|
|
953
|
+
|
|
954
|
+
def test_rshift__node(self):
|
|
955
|
+
# GIVEN
|
|
956
|
+
class StartNode(BaseNode[BaseState]):
|
|
957
|
+
pass
|
|
958
|
+
|
|
959
|
+
class MyFinalOutput(FinalOutputNode[BaseState, str]):
|
|
960
|
+
pass
|
|
961
|
+
|
|
962
|
+
# WHEN
|
|
963
|
+
initial_graph = Graph.from_node(StartNode)
|
|
964
|
+
graph = initial_graph >> MyFinalOutput
|
|
965
|
+
|
|
966
|
+
# THEN
|
|
967
|
+
self.validate_graph(graph, MyFinalOutput)
|
|
968
|
+
|
|
969
|
+
def test_rshift__graph(self):
|
|
970
|
+
# GIVEN
|
|
971
|
+
class StartNode(BaseNode[BaseState]):
|
|
972
|
+
pass
|
|
973
|
+
|
|
974
|
+
class MyFinalOutput(FinalOutputNode[BaseState, str]):
|
|
975
|
+
pass
|
|
976
|
+
|
|
977
|
+
# WHEN
|
|
978
|
+
subgraph = Graph.from_node(MyFinalOutput)
|
|
979
|
+
graph = Graph.from_node(StartNode) >> subgraph
|
|
980
|
+
|
|
981
|
+
# THEN
|
|
982
|
+
self.validate_graph(subgraph, MyFinalOutput)
|
|
983
|
+
self.validate_graph(graph, MyFinalOutput)
|
|
984
|
+
|
|
985
|
+
def test_rshift__set_with_node(self):
|
|
986
|
+
# GIVEN
|
|
987
|
+
class StartNode(BaseNode[BaseState]):
|
|
988
|
+
pass
|
|
989
|
+
|
|
990
|
+
class MyFinalOutput(FinalOutputNode[BaseState, str]):
|
|
991
|
+
pass
|
|
992
|
+
|
|
993
|
+
# WHEN
|
|
994
|
+
graph = Graph.from_node(StartNode) >> {MyFinalOutput}
|
|
995
|
+
|
|
996
|
+
# THEN
|
|
997
|
+
self.validate_graph(graph, MyFinalOutput)
|
|
998
|
+
|
|
999
|
+
def test_rshift__set_with_graph(self):
|
|
1000
|
+
# GIVEN
|
|
1001
|
+
class StartNode(BaseNode[BaseState]):
|
|
1002
|
+
pass
|
|
1003
|
+
|
|
1004
|
+
class MyFinalOutput(FinalOutputNode[BaseState, str]):
|
|
1005
|
+
pass
|
|
1006
|
+
|
|
1007
|
+
# WHEN
|
|
1008
|
+
subgraph = Graph.from_node(MyFinalOutput)
|
|
1009
|
+
graph = Graph.from_node(StartNode) >> {subgraph}
|
|
1010
|
+
|
|
1011
|
+
# THEN
|
|
1012
|
+
self.validate_graph(subgraph, MyFinalOutput)
|
|
1013
|
+
self.validate_graph(graph, MyFinalOutput)
|
vellum/workflows/inputs/base.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from typing import Any, Dict, Iterator, Set, Tuple, Type, Union, get_args, get_origin
|
|
1
|
+
from typing import Any, Callable, Dict, Iterator, Set, Tuple, Type, Union, cast, get_args, get_origin
|
|
2
2
|
from typing_extensions import dataclass_transform
|
|
3
3
|
|
|
4
4
|
from pydantic import GetCoreSchemaHandler
|
|
@@ -10,7 +10,7 @@ from vellum.workflows.errors.types import WorkflowErrorCode
|
|
|
10
10
|
from vellum.workflows.exceptions import WorkflowInitializationException
|
|
11
11
|
from vellum.workflows.references import ExternalInputReference, WorkflowInputReference
|
|
12
12
|
from vellum.workflows.references.input import InputReference
|
|
13
|
-
from vellum.workflows.types.utils import get_class_attr_names, infer_types
|
|
13
|
+
from vellum.workflows.types.utils import coerce_to_declared_type, get_class_attr_names, infer_types
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
@dataclass_transform(kw_only_default=True)
|
|
@@ -97,8 +97,7 @@ class BaseInputs(metaclass=_BaseInputsMeta):
|
|
|
97
97
|
# Check if default is a FieldInfo with default_factory
|
|
98
98
|
if isinstance(default_value, FieldInfo):
|
|
99
99
|
if default_value.default_factory is not None:
|
|
100
|
-
|
|
101
|
-
value = default_value.default_factory() # type: ignore[call-arg]
|
|
100
|
+
value = cast(Callable[[], Any], default_value.default_factory)()
|
|
102
101
|
elif hasattr(default_value, "default") and default_value.default is not ...:
|
|
103
102
|
# Use the default value directly
|
|
104
103
|
value = default_value.default
|
|
@@ -114,7 +113,7 @@ class BaseInputs(metaclass=_BaseInputsMeta):
|
|
|
114
113
|
if value is undefined and not has_default:
|
|
115
114
|
# All fields without defaults must be provided, even if Optional
|
|
116
115
|
raise WorkflowInitializationException(
|
|
117
|
-
message=f"Required input variables {name} should have defined value",
|
|
116
|
+
message=f"Required input variables '{name}' should have defined value",
|
|
118
117
|
code=WorkflowErrorCode.INVALID_INPUTS,
|
|
119
118
|
workflow_definition=self.__class__.__parent_class__,
|
|
120
119
|
)
|
|
@@ -122,13 +121,30 @@ class BaseInputs(metaclass=_BaseInputsMeta):
|
|
|
122
121
|
# Validate that None is not provided for non-Optional fields
|
|
123
122
|
if value is None and not is_optional:
|
|
124
123
|
raise WorkflowInitializationException(
|
|
125
|
-
message=f"Required input variables {name} should have defined value",
|
|
124
|
+
message=f"Required input variables '{name}' should have defined value",
|
|
126
125
|
code=WorkflowErrorCode.INVALID_INPUTS,
|
|
127
126
|
workflow_definition=self.__class__.__parent_class__,
|
|
128
127
|
)
|
|
129
128
|
|
|
130
129
|
# Set the value on the instance (either from kwargs or default)
|
|
131
130
|
if value is not undefined:
|
|
131
|
+
# Coerce the value to the declared type if needed
|
|
132
|
+
# For Optional types, extract the non-None type for coercion
|
|
133
|
+
coercion_type = field_type
|
|
134
|
+
if is_optional:
|
|
135
|
+
non_none_args = [arg for arg in args if arg is not type(None)]
|
|
136
|
+
if len(non_none_args) == 1:
|
|
137
|
+
coercion_type = non_none_args[0]
|
|
138
|
+
|
|
139
|
+
try:
|
|
140
|
+
value = coerce_to_declared_type(value, coercion_type, name)
|
|
141
|
+
except ValueError as e:
|
|
142
|
+
raise WorkflowInitializationException(
|
|
143
|
+
message=str(e),
|
|
144
|
+
code=WorkflowErrorCode.INVALID_INPUTS,
|
|
145
|
+
workflow_definition=self.__class__.__parent_class__,
|
|
146
|
+
)
|
|
147
|
+
|
|
132
148
|
setattr(self, name, value)
|
|
133
149
|
|
|
134
150
|
def __iter__(self) -> Iterator[Tuple[InputReference, Any]]:
|