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,170 @@
|
|
|
1
|
+
from vellum.client.core.api_error import ApiError
|
|
2
|
+
from vellum.workflows.errors.types import WorkflowErrorCode
|
|
3
|
+
from vellum.workflows.events.node import NodeExecutionInitiatedEvent, NodeExecutionRejectedEvent
|
|
4
|
+
from vellum.workflows.inputs.base import BaseInputs
|
|
5
|
+
from vellum.workflows.nodes.bases.base import BaseNode
|
|
6
|
+
from vellum.workflows.state.base import BaseState
|
|
7
|
+
from vellum.workflows.workflows.base import BaseWorkflow
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def test_workflow_runner__handles_400_api_error_with_integration_details():
|
|
11
|
+
"""
|
|
12
|
+
Tests that WorkflowRunner handles 400 API errors with integration details
|
|
13
|
+
as INVALID_INPUTS errors.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
# GIVEN a node that raises an ApiError with status_code 400 and integration details
|
|
17
|
+
class TestInputs(BaseInputs):
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
class TestState(BaseState):
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
class TestNode(BaseNode[TestState]):
|
|
24
|
+
class Outputs(BaseNode.Outputs):
|
|
25
|
+
result: str
|
|
26
|
+
|
|
27
|
+
def run(self) -> "TestNode.Outputs":
|
|
28
|
+
raise ApiError(
|
|
29
|
+
status_code=400,
|
|
30
|
+
body={
|
|
31
|
+
"message": "Invalid request to integration",
|
|
32
|
+
"integration": {
|
|
33
|
+
"name": "test_integration",
|
|
34
|
+
"provider": "test_provider",
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
class TestWorkflow(BaseWorkflow[TestInputs, TestState]):
|
|
40
|
+
graph = TestNode
|
|
41
|
+
|
|
42
|
+
class Outputs(BaseWorkflow.Outputs):
|
|
43
|
+
result: str
|
|
44
|
+
|
|
45
|
+
workflow = TestWorkflow()
|
|
46
|
+
|
|
47
|
+
# WHEN we run the node
|
|
48
|
+
events = list(workflow.run_node(node=TestNode))
|
|
49
|
+
|
|
50
|
+
# THEN we should get a rejected event with INVALID_INPUTS error code
|
|
51
|
+
assert len(events) == 2
|
|
52
|
+
assert isinstance(events[0], NodeExecutionInitiatedEvent)
|
|
53
|
+
assert isinstance(events[1], NodeExecutionRejectedEvent)
|
|
54
|
+
|
|
55
|
+
# AND the error should have the correct code and message
|
|
56
|
+
rejected_event = events[1]
|
|
57
|
+
assert rejected_event.body.error.code == WorkflowErrorCode.INVALID_INPUTS
|
|
58
|
+
assert rejected_event.body.error.message == "Invalid request to integration"
|
|
59
|
+
|
|
60
|
+
# AND the raw_data should contain the integration details
|
|
61
|
+
assert rejected_event.body.error.raw_data == {
|
|
62
|
+
"integration": {
|
|
63
|
+
"name": "test_integration",
|
|
64
|
+
"provider": "test_provider",
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_workflow_runner__handles_403_api_error_with_integration_details():
|
|
70
|
+
"""
|
|
71
|
+
Tests that WorkflowRunner handles 403 API errors with integration details correctly.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
# GIVEN a node that raises an ApiError with status_code 403 and integration details
|
|
75
|
+
class TestInputs(BaseInputs):
|
|
76
|
+
pass
|
|
77
|
+
|
|
78
|
+
class TestState(BaseState):
|
|
79
|
+
pass
|
|
80
|
+
|
|
81
|
+
class TestNode(BaseNode[TestState]):
|
|
82
|
+
class Outputs(BaseNode.Outputs):
|
|
83
|
+
result: str
|
|
84
|
+
|
|
85
|
+
def run(self) -> "TestNode.Outputs":
|
|
86
|
+
raise ApiError(
|
|
87
|
+
status_code=403,
|
|
88
|
+
body={
|
|
89
|
+
"message": "You must authenticate with this integration",
|
|
90
|
+
"integration": {
|
|
91
|
+
"name": "test_integration",
|
|
92
|
+
"provider": "test_provider",
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
class TestWorkflow(BaseWorkflow[TestInputs, TestState]):
|
|
98
|
+
graph = TestNode
|
|
99
|
+
|
|
100
|
+
class Outputs(BaseWorkflow.Outputs):
|
|
101
|
+
result: str
|
|
102
|
+
|
|
103
|
+
workflow = TestWorkflow()
|
|
104
|
+
|
|
105
|
+
# WHEN we run the node
|
|
106
|
+
events = list(workflow.run_node(node=TestNode))
|
|
107
|
+
|
|
108
|
+
# THEN we should get a rejected event with INTEGRATION_CREDENTIALS_UNAVAILABLE error code
|
|
109
|
+
assert len(events) == 2
|
|
110
|
+
assert isinstance(events[0], NodeExecutionInitiatedEvent)
|
|
111
|
+
assert isinstance(events[1], NodeExecutionRejectedEvent)
|
|
112
|
+
|
|
113
|
+
# AND the error should have the correct code and message
|
|
114
|
+
rejected_event = events[1]
|
|
115
|
+
assert rejected_event.body.error.code == WorkflowErrorCode.INTEGRATION_CREDENTIALS_UNAVAILABLE
|
|
116
|
+
assert rejected_event.body.error.message == "You must authenticate with this integration"
|
|
117
|
+
|
|
118
|
+
# AND the raw_data should contain the integration details
|
|
119
|
+
assert rejected_event.body.error.raw_data == {
|
|
120
|
+
"integration": {
|
|
121
|
+
"name": "test_integration",
|
|
122
|
+
"provider": "test_provider",
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def test_workflow_runner__handles_400_api_error_without_integration_details():
|
|
128
|
+
"""
|
|
129
|
+
Tests that WorkflowRunner handles 400 API errors without integration details
|
|
130
|
+
as generic errors (not INTEGRATION_CREDENTIALS_UNAVAILABLE).
|
|
131
|
+
"""
|
|
132
|
+
|
|
133
|
+
# GIVEN a node that raises an ApiError with status_code 400 but no integration details
|
|
134
|
+
class TestInputs(BaseInputs):
|
|
135
|
+
pass
|
|
136
|
+
|
|
137
|
+
class TestState(BaseState):
|
|
138
|
+
pass
|
|
139
|
+
|
|
140
|
+
class TestNode(BaseNode[TestState]):
|
|
141
|
+
class Outputs(BaseNode.Outputs):
|
|
142
|
+
result: str
|
|
143
|
+
|
|
144
|
+
def run(self) -> "TestNode.Outputs":
|
|
145
|
+
raise ApiError(
|
|
146
|
+
status_code=400,
|
|
147
|
+
body={
|
|
148
|
+
"message": "Invalid request parameters",
|
|
149
|
+
},
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
class TestWorkflow(BaseWorkflow[TestInputs, TestState]):
|
|
153
|
+
graph = TestNode
|
|
154
|
+
|
|
155
|
+
class Outputs(BaseWorkflow.Outputs):
|
|
156
|
+
result: str
|
|
157
|
+
|
|
158
|
+
workflow = TestWorkflow()
|
|
159
|
+
|
|
160
|
+
# WHEN we run the node
|
|
161
|
+
events = list(workflow.run_node(node=TestNode))
|
|
162
|
+
|
|
163
|
+
# THEN we should get a rejected event
|
|
164
|
+
assert len(events) == 2
|
|
165
|
+
assert isinstance(events[0], NodeExecutionInitiatedEvent)
|
|
166
|
+
assert isinstance(events[1], NodeExecutionRejectedEvent)
|
|
167
|
+
|
|
168
|
+
# AND the error should NOT have INTEGRATION_CREDENTIALS_UNAVAILABLE code
|
|
169
|
+
rejected_event = events[1]
|
|
170
|
+
assert rejected_event.body.error.code != WorkflowErrorCode.INTEGRATION_CREDENTIALS_UNAVAILABLE
|
vellum/workflows/sandbox.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from typing import Any, Dict, Generic, Optional, Sequence,
|
|
1
|
+
from typing import Any, Dict, Generic, Optional, Sequence, Union
|
|
2
2
|
|
|
3
3
|
import dotenv
|
|
4
4
|
|
|
@@ -53,12 +53,12 @@ class WorkflowSandboxRunner(Generic[WorkflowType]):
|
|
|
53
53
|
selected_inputs = self._inputs[index]
|
|
54
54
|
|
|
55
55
|
raw_inputs: Union[BaseInputs, Dict[str, Any]]
|
|
56
|
-
|
|
56
|
+
trigger_value: Optional[BaseTrigger] = None
|
|
57
57
|
node_output_mocks = None
|
|
58
58
|
if isinstance(selected_inputs, DatasetRow):
|
|
59
59
|
raw_inputs = selected_inputs.inputs
|
|
60
|
-
|
|
61
|
-
node_output_mocks = selected_inputs.
|
|
60
|
+
trigger_value = selected_inputs.workflow_trigger
|
|
61
|
+
node_output_mocks = selected_inputs.mocks
|
|
62
62
|
else:
|
|
63
63
|
raw_inputs = selected_inputs
|
|
64
64
|
|
|
@@ -69,10 +69,7 @@ class WorkflowSandboxRunner(Generic[WorkflowType]):
|
|
|
69
69
|
else:
|
|
70
70
|
inputs_for_stream = raw_inputs
|
|
71
71
|
|
|
72
|
-
trigger_instance: Optional[BaseTrigger] =
|
|
73
|
-
if trigger_class is not None:
|
|
74
|
-
# Instantiate the trigger with the inputs
|
|
75
|
-
trigger_instance = trigger_class(**raw_inputs) if isinstance(raw_inputs, dict) else trigger_class()
|
|
72
|
+
trigger_instance: Optional[BaseTrigger] = trigger_value
|
|
76
73
|
|
|
77
74
|
events = self._workflow.stream(
|
|
78
75
|
inputs=inputs_for_stream,
|
|
@@ -97,3 +94,5 @@ class WorkflowSandboxRunner(Generic[WorkflowType]):
|
|
|
97
94
|
elif event.name == "node.execution.rejected":
|
|
98
95
|
self._logger.debug(f"Error: {event.error}")
|
|
99
96
|
self._logger.error(f"Failed to run Node: {event.node_definition.__name__}")
|
|
97
|
+
elif event.name == "workflow.execution.rejected":
|
|
98
|
+
self._logger.error(f"Workflow rejected! Error: {event.error}")
|
vellum/workflows/state/base.py
CHANGED
|
@@ -5,7 +5,7 @@ from dataclasses import field
|
|
|
5
5
|
from datetime import datetime
|
|
6
6
|
import logging
|
|
7
7
|
from queue import Queue
|
|
8
|
-
from threading import
|
|
8
|
+
from threading import RLock
|
|
9
9
|
from uuid import UUID, uuid4
|
|
10
10
|
from typing import (
|
|
11
11
|
TYPE_CHECKING,
|
|
@@ -25,6 +25,7 @@ from typing import (
|
|
|
25
25
|
from typing_extensions import dataclass_transform
|
|
26
26
|
|
|
27
27
|
from pydantic import GetCoreSchemaHandler, ValidationInfo, field_serializer, field_validator
|
|
28
|
+
from pydantic.fields import FieldInfo
|
|
28
29
|
from pydantic_core import core_schema
|
|
29
30
|
|
|
30
31
|
from vellum.client.core.pydantic_utilities import UniversalBaseModel
|
|
@@ -53,10 +54,14 @@ logger = logging.getLogger(__name__)
|
|
|
53
54
|
class _Snapshottable:
|
|
54
55
|
_snapshot_callback: Callable[[Optional[StateDelta]], None]
|
|
55
56
|
_path: str
|
|
57
|
+
_lock: Optional[RLock]
|
|
56
58
|
|
|
57
|
-
def __bind__(
|
|
59
|
+
def __bind__(
|
|
60
|
+
self, path: str, snapshot_callback: Callable[[Optional[StateDelta]], None], lock: Optional[RLock] = None
|
|
61
|
+
) -> None:
|
|
58
62
|
self._snapshot_callback = snapshot_callback
|
|
59
63
|
self._path = path
|
|
64
|
+
self._lock = lock
|
|
60
65
|
|
|
61
66
|
|
|
62
67
|
@dataclass_transform(kw_only_default=True)
|
|
@@ -65,7 +70,9 @@ class _BaseStateMeta(type):
|
|
|
65
70
|
if not name.startswith("_"):
|
|
66
71
|
instance = vars(cls).get(name, undefined)
|
|
67
72
|
types = infer_types(cls, name)
|
|
68
|
-
return StateValueReference(
|
|
73
|
+
return StateValueReference(
|
|
74
|
+
name=name, types=types, instance=instance, state_class=cast(Type["BaseState"], cls)
|
|
75
|
+
)
|
|
69
76
|
|
|
70
77
|
return super().__getattribute__(name)
|
|
71
78
|
|
|
@@ -86,7 +93,11 @@ class _BaseStateMeta(type):
|
|
|
86
93
|
|
|
87
94
|
class _SnapshottableDict(dict, _Snapshottable):
|
|
88
95
|
def __setitem__(self, key: Any, value: Any) -> None:
|
|
89
|
-
|
|
96
|
+
if self._lock:
|
|
97
|
+
with self._lock:
|
|
98
|
+
super().__setitem__(key, value)
|
|
99
|
+
else:
|
|
100
|
+
super().__setitem__(key, value)
|
|
90
101
|
self._snapshot_callback(SetStateDelta(name=f"{self._path}.{key}", delta=value))
|
|
91
102
|
|
|
92
103
|
def __deepcopy__(self, memo: Any) -> "_SnapshottableDict":
|
|
@@ -96,19 +107,27 @@ class _SnapshottableDict(dict, _Snapshottable):
|
|
|
96
107
|
y[deepcopy(key, memo)] = deepcopy(value, memo)
|
|
97
108
|
|
|
98
109
|
y = _SnapshottableDict(y)
|
|
99
|
-
y.__bind__(self._path, self._snapshot_callback)
|
|
110
|
+
y.__bind__(self._path, self._snapshot_callback, self._lock)
|
|
100
111
|
memo[id(self)] = y
|
|
101
112
|
return y
|
|
102
113
|
|
|
103
114
|
|
|
104
115
|
class _SnapshottableList(list, _Snapshottable):
|
|
105
116
|
def __setitem__(self, index: Union[SupportsIndex, slice], value: Any) -> None:
|
|
106
|
-
|
|
117
|
+
if self._lock:
|
|
118
|
+
with self._lock:
|
|
119
|
+
super().__setitem__(index, value)
|
|
120
|
+
else:
|
|
121
|
+
super().__setitem__(index, value)
|
|
107
122
|
if isinstance(index, int):
|
|
108
123
|
self._snapshot_callback(SetStateDelta(name=f"{self._path}.{index}", delta=value))
|
|
109
124
|
|
|
110
125
|
def append(self, value: Any) -> None:
|
|
111
|
-
|
|
126
|
+
if self._lock:
|
|
127
|
+
with self._lock:
|
|
128
|
+
super().append(value)
|
|
129
|
+
else:
|
|
130
|
+
super().append(value)
|
|
112
131
|
self._snapshot_callback(AppendStateDelta(name=self._path, delta=value))
|
|
113
132
|
|
|
114
133
|
def __deepcopy__(self, memo: Any) -> "_SnapshottableList":
|
|
@@ -119,27 +138,33 @@ class _SnapshottableList(list, _Snapshottable):
|
|
|
119
138
|
append(deepcopy(a, memo))
|
|
120
139
|
|
|
121
140
|
y = _SnapshottableList(y)
|
|
122
|
-
y.__bind__(self._path, self._snapshot_callback)
|
|
141
|
+
y.__bind__(self._path, self._snapshot_callback, self._lock)
|
|
123
142
|
memo[id(self)] = y
|
|
124
143
|
return y
|
|
125
144
|
|
|
126
145
|
|
|
127
|
-
def _make_snapshottable(
|
|
146
|
+
def _make_snapshottable(
|
|
147
|
+
path: str,
|
|
148
|
+
value: Any,
|
|
149
|
+
snapshot_callback: Callable[[Optional[StateDelta]], None],
|
|
150
|
+
lock: Optional[RLock] = None,
|
|
151
|
+
) -> Any:
|
|
128
152
|
"""
|
|
129
153
|
Edits any value to make it snapshottable on edit. Made as a separate function from `BaseState` to
|
|
130
154
|
avoid namespace conflicts with subclasses.
|
|
131
155
|
"""
|
|
132
156
|
if isinstance(value, _Snapshottable):
|
|
157
|
+
value.__bind__(path, snapshot_callback, lock)
|
|
133
158
|
return value
|
|
134
159
|
|
|
135
160
|
if isinstance(value, dict):
|
|
136
161
|
snapshottable_dict = _SnapshottableDict(value)
|
|
137
|
-
snapshottable_dict.__bind__(path, snapshot_callback)
|
|
162
|
+
snapshottable_dict.__bind__(path, snapshot_callback, lock)
|
|
138
163
|
return snapshottable_dict
|
|
139
164
|
|
|
140
165
|
if isinstance(value, list):
|
|
141
166
|
snapshottable_list = _SnapshottableList(value)
|
|
142
|
-
snapshottable_list.__bind__(path, snapshot_callback)
|
|
167
|
+
snapshottable_list.__bind__(path, snapshot_callback, lock)
|
|
143
168
|
return snapshottable_list
|
|
144
169
|
|
|
145
170
|
return value
|
|
@@ -307,10 +332,10 @@ class StateMeta(UniversalBaseModel):
|
|
|
307
332
|
id: UUID = field(default_factory=uuid4_default_factory)
|
|
308
333
|
span_id: UUID = field(default_factory=uuid4_default_factory)
|
|
309
334
|
updated_ts: datetime = field(default_factory=default_datetime_factory)
|
|
310
|
-
|
|
335
|
+
trigger_attributes: Optional[Dict[TriggerAttributeReference, Any]] = field(default=None)
|
|
336
|
+
workflow_inputs: Optional[BaseInputs] = field(default=None)
|
|
311
337
|
external_inputs: Dict[ExternalInputReference, Any] = field(default_factory=dict)
|
|
312
338
|
node_outputs: Dict[OutputReference, Any] = field(default_factory=dict)
|
|
313
|
-
trigger_attributes: Dict[TriggerAttributeReference, Any] = field(default_factory=dict)
|
|
314
339
|
node_execution_cache: NodeExecutionCache = field(default_factory=NodeExecutionCache)
|
|
315
340
|
parent: Optional["BaseState"] = None
|
|
316
341
|
__snapshot_callback__: Optional[Callable[[Optional[StateDelta]], None]] = field(init=False, default=None)
|
|
@@ -318,10 +343,27 @@ class StateMeta(UniversalBaseModel):
|
|
|
318
343
|
def model_post_init(self, context: Any) -> None:
|
|
319
344
|
self.__snapshot_callback__ = None
|
|
320
345
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
self.
|
|
324
|
-
|
|
346
|
+
# Auto-populate workflow_inputs with defaults only if trigger_attributes is None
|
|
347
|
+
# (trigger_attributes being set indicates a trigger-based workflow where we don't want default inputs)
|
|
348
|
+
if self.workflow_inputs is None and self.trigger_attributes is None:
|
|
349
|
+
workflow_definition = (
|
|
350
|
+
self.workflow_definition if is_workflow_class(self.workflow_definition) else import_workflow_class()
|
|
351
|
+
)
|
|
352
|
+
try:
|
|
353
|
+
inputs_class = workflow_definition.get_inputs_class()
|
|
354
|
+
self.workflow_inputs = inputs_class()
|
|
355
|
+
except Exception:
|
|
356
|
+
self.workflow_inputs = BaseInputs()
|
|
357
|
+
|
|
358
|
+
def add_snapshot_callback(
|
|
359
|
+
self, callback: Callable[[Optional[StateDelta]], None], lock: Optional[RLock] = None
|
|
360
|
+
) -> None:
|
|
361
|
+
self.node_outputs = _make_snapshottable("meta.node_outputs", self.node_outputs, callback, lock)
|
|
362
|
+
self.external_inputs = _make_snapshottable("meta.external_inputs", self.external_inputs, callback, lock)
|
|
363
|
+
if self.trigger_attributes is not None:
|
|
364
|
+
self.trigger_attributes = _make_snapshottable(
|
|
365
|
+
"meta.trigger_attributes", self.trigger_attributes, callback, lock
|
|
366
|
+
)
|
|
325
367
|
self.__snapshot_callback__ = callback
|
|
326
368
|
|
|
327
369
|
def __setattr__(self, name: str, value: Any) -> None:
|
|
@@ -358,8 +400,10 @@ class StateMeta(UniversalBaseModel):
|
|
|
358
400
|
|
|
359
401
|
@field_serializer("trigger_attributes")
|
|
360
402
|
def serialize_trigger_attributes(
|
|
361
|
-
self, trigger_attributes: Dict[TriggerAttributeReference, Any], _info: Any
|
|
403
|
+
self, trigger_attributes: Optional[Dict[TriggerAttributeReference, Any]], _info: Any
|
|
362
404
|
) -> Dict[str, Any]:
|
|
405
|
+
if trigger_attributes is None:
|
|
406
|
+
return {}
|
|
363
407
|
return {str(descriptor.id): value for descriptor, value in trigger_attributes.items()}
|
|
364
408
|
|
|
365
409
|
@field_validator("node_outputs", mode="before")
|
|
@@ -452,11 +496,10 @@ class StateMeta(UniversalBaseModel):
|
|
|
452
496
|
@field_validator("workflow_inputs", mode="before")
|
|
453
497
|
@classmethod
|
|
454
498
|
def deserialize_workflow_inputs(cls, workflow_inputs: Any, info: ValidationInfo):
|
|
455
|
-
|
|
499
|
+
context = info.context if isinstance(info.context, dict) else {}
|
|
500
|
+
workflow_definition = context.get("workflow_definition") or cls._get_workflow(info)
|
|
456
501
|
|
|
457
502
|
if workflow_definition:
|
|
458
|
-
if workflow_inputs is None:
|
|
459
|
-
return workflow_definition.get_inputs_class()()
|
|
460
503
|
if isinstance(workflow_inputs, dict):
|
|
461
504
|
return workflow_definition.get_inputs_class()(**workflow_inputs)
|
|
462
505
|
|
|
@@ -538,7 +581,7 @@ class StateMeta(UniversalBaseModel):
|
|
|
538
581
|
class BaseState(metaclass=_BaseStateMeta):
|
|
539
582
|
meta: StateMeta = field(init=False)
|
|
540
583
|
|
|
541
|
-
__lock__:
|
|
584
|
+
__lock__: RLock = field(init=False)
|
|
542
585
|
__is_quiet__: bool = field(init=False)
|
|
543
586
|
__is_atomic__: bool = field(init=False)
|
|
544
587
|
__snapshot_callback__: Callable[["BaseState", List[StateDelta]], None] = field(init=False)
|
|
@@ -549,16 +592,23 @@ class BaseState(metaclass=_BaseStateMeta):
|
|
|
549
592
|
self.__is_atomic__ = False
|
|
550
593
|
self.__snapshot_callback__ = lambda state, deltas: None
|
|
551
594
|
self.__deltas__ = []
|
|
552
|
-
self.__lock__ =
|
|
595
|
+
self.__lock__ = RLock()
|
|
553
596
|
|
|
554
597
|
self.meta = meta or StateMeta()
|
|
555
|
-
self.meta.add_snapshot_callback(self.__snapshot__)
|
|
598
|
+
self.meta.add_snapshot_callback(self.__snapshot__, self.__lock__)
|
|
556
599
|
|
|
557
600
|
# Make all class attribute values snapshottable
|
|
558
601
|
for name, value in self.__class__.__dict__.items():
|
|
559
602
|
if not name.startswith("_") and name != "meta":
|
|
603
|
+
if isinstance(value, FieldInfo):
|
|
604
|
+
if value.default_factory is not None:
|
|
605
|
+
value = cast(Callable[[], Any], value.default_factory)()
|
|
606
|
+
elif hasattr(value, "default") and value.default is not ...:
|
|
607
|
+
value = value.default
|
|
608
|
+
else:
|
|
609
|
+
continue
|
|
560
610
|
# Bypass __is_quiet__ instead of `setattr`
|
|
561
|
-
snapshottable_value = _make_snapshottable(name, value, self.__snapshot__)
|
|
611
|
+
snapshottable_value = _make_snapshottable(name, value, self.__snapshot__, self.__lock__)
|
|
562
612
|
super().__setattr__(name, snapshottable_value)
|
|
563
613
|
|
|
564
614
|
for name, value in kwargs.items():
|
|
@@ -570,11 +620,17 @@ class BaseState(metaclass=_BaseStateMeta):
|
|
|
570
620
|
new_state = deepcopy_with_exclusions(
|
|
571
621
|
self,
|
|
572
622
|
exclusions={
|
|
573
|
-
"__lock__":
|
|
623
|
+
"__lock__": RLock(),
|
|
574
624
|
},
|
|
575
625
|
memo=memo,
|
|
576
626
|
)
|
|
577
|
-
new_state.meta.add_snapshot_callback(new_state.__snapshot__)
|
|
627
|
+
new_state.meta.add_snapshot_callback(new_state.__snapshot__, new_state.__lock__)
|
|
628
|
+
|
|
629
|
+
for name, value in list(vars(new_state).items()):
|
|
630
|
+
if not name.startswith("_") and name != "meta":
|
|
631
|
+
rebound_value = _make_snapshottable(name, value, new_state.__snapshot__, new_state.__lock__)
|
|
632
|
+
object.__setattr__(new_state, name, rebound_value)
|
|
633
|
+
|
|
578
634
|
return new_state
|
|
579
635
|
|
|
580
636
|
def __repr__(self) -> str:
|
|
@@ -615,8 +671,9 @@ class BaseState(metaclass=_BaseStateMeta):
|
|
|
615
671
|
super().__setattr__(name, delta)
|
|
616
672
|
return
|
|
617
673
|
|
|
618
|
-
snapshottable_value = _make_snapshottable(name, delta, self.__snapshot__)
|
|
619
|
-
|
|
674
|
+
snapshottable_value = _make_snapshottable(name, delta, self.__snapshot__, self.__lock__)
|
|
675
|
+
with self.__lock__:
|
|
676
|
+
super().__setattr__(name, snapshottable_value)
|
|
620
677
|
self.meta.updated_ts = datetime_now()
|
|
621
678
|
self.__snapshot__(SetStateDelta(name=name, delta=delta))
|
|
622
679
|
|
|
@@ -662,7 +719,9 @@ class BaseState(metaclass=_BaseStateMeta):
|
|
|
662
719
|
return
|
|
663
720
|
|
|
664
721
|
try:
|
|
665
|
-
|
|
722
|
+
with self.__lock__:
|
|
723
|
+
state_copy = deepcopy(self)
|
|
724
|
+
self.__snapshot_callback__(state_copy, self.__deltas__)
|
|
666
725
|
except Exception:
|
|
667
726
|
logger.exception("Failed to snapshot Workflow state.")
|
|
668
727
|
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
from dataclasses import dataclass
|
|
2
2
|
from functools import cached_property
|
|
3
3
|
import json
|
|
4
|
+
import logging
|
|
4
5
|
from queue import Queue
|
|
6
|
+
import traceback
|
|
5
7
|
from uuid import UUID, uuid4
|
|
6
|
-
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Type
|
|
8
|
+
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Type
|
|
7
9
|
|
|
8
10
|
from vellum import Vellum, __version__
|
|
11
|
+
from vellum.client.types import SeverityEnum
|
|
9
12
|
from vellum.workflows.context import ExecutionContext, get_execution_context, set_execution_context
|
|
10
|
-
from vellum.workflows.events.
|
|
13
|
+
from vellum.workflows.events.node import NodeExecutionLogBody, NodeExecutionLogEvent
|
|
14
|
+
from vellum.workflows.events.types import ExternalParentContext, NodeParentContext
|
|
11
15
|
from vellum.workflows.nodes.mocks import MockNodeExecution, MockNodeExecutionArg
|
|
12
16
|
from vellum.workflows.outputs.base import BaseOutputs
|
|
13
17
|
from vellum.workflows.references.constant import ConstantValueReference
|
|
@@ -21,6 +25,8 @@ if TYPE_CHECKING:
|
|
|
21
25
|
from vellum.workflows.state.base import BaseState
|
|
22
26
|
from vellum.workflows.workflows.base import BaseWorkflow
|
|
23
27
|
|
|
28
|
+
logger = logging.getLogger(__name__)
|
|
29
|
+
|
|
24
30
|
|
|
25
31
|
@dataclass
|
|
26
32
|
class WorkflowDeploymentMetadata:
|
|
@@ -43,6 +49,7 @@ class WorkflowContext:
|
|
|
43
49
|
generated_files: Optional[dict[str, str]] = None,
|
|
44
50
|
namespace: Optional[str] = None,
|
|
45
51
|
store_class: Optional[Type[Store]] = None,
|
|
52
|
+
event_max_size: Optional[int] = None,
|
|
46
53
|
):
|
|
47
54
|
self._vellum_client = vellum_client
|
|
48
55
|
self._event_queue: Optional[Queue["WorkflowEvent"]] = None
|
|
@@ -50,6 +57,7 @@ class WorkflowContext:
|
|
|
50
57
|
self._execution_context = get_execution_context()
|
|
51
58
|
self._namespace = namespace
|
|
52
59
|
self._store_class = store_class if store_class is not None else Store
|
|
60
|
+
self._event_max_size = event_max_size
|
|
53
61
|
|
|
54
62
|
if execution_context is not None:
|
|
55
63
|
self._execution_context.trace_id = execution_context.trace_id
|
|
@@ -93,6 +101,10 @@ class WorkflowContext:
|
|
|
93
101
|
def store_class(self) -> Type[Store]:
|
|
94
102
|
return self._store_class
|
|
95
103
|
|
|
104
|
+
@property
|
|
105
|
+
def event_max_size(self) -> Optional[int]:
|
|
106
|
+
return self._event_max_size
|
|
107
|
+
|
|
96
108
|
@property
|
|
97
109
|
def monitoring_url(self) -> Optional[str]:
|
|
98
110
|
"""
|
|
@@ -140,6 +152,61 @@ class WorkflowContext:
|
|
|
140
152
|
# For custom domains, assume the same pattern: api.* -> app.*
|
|
141
153
|
return api_url.replace("api.", "app.", 1)
|
|
142
154
|
|
|
155
|
+
def emit_log_event(
|
|
156
|
+
self,
|
|
157
|
+
severity: SeverityEnum,
|
|
158
|
+
message: str,
|
|
159
|
+
attributes: Optional[Dict[str, Any]] = None,
|
|
160
|
+
exc_info: Optional[bool] = None,
|
|
161
|
+
) -> None:
|
|
162
|
+
"""Emit a log event for a particular node.
|
|
163
|
+
|
|
164
|
+
This is in active development and may have breaking changes.
|
|
165
|
+
"""
|
|
166
|
+
from vellum.workflows.nodes.bases import BaseNode
|
|
167
|
+
|
|
168
|
+
if self._event_queue is None:
|
|
169
|
+
return
|
|
170
|
+
|
|
171
|
+
execution_context = get_execution_context()
|
|
172
|
+
parent_context = execution_context.parent_context
|
|
173
|
+
while parent_context:
|
|
174
|
+
if isinstance(parent_context, NodeParentContext):
|
|
175
|
+
break
|
|
176
|
+
parent_context = parent_context.parent
|
|
177
|
+
|
|
178
|
+
if not isinstance(parent_context, NodeParentContext):
|
|
179
|
+
return
|
|
180
|
+
|
|
181
|
+
try:
|
|
182
|
+
node_class = parent_context.node_definition.decode()
|
|
183
|
+
except Exception:
|
|
184
|
+
logger.exception("Failed to decode node definition.")
|
|
185
|
+
return
|
|
186
|
+
|
|
187
|
+
if not isinstance(node_class, type) or not issubclass(node_class, BaseNode):
|
|
188
|
+
logger.warning("Node definition is not a subclass of BaseNode.")
|
|
189
|
+
return
|
|
190
|
+
|
|
191
|
+
if exc_info:
|
|
192
|
+
attributes = {
|
|
193
|
+
**(attributes or {}),
|
|
194
|
+
"exc_info": traceback.format_exc(),
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
self._event_queue.put(
|
|
198
|
+
NodeExecutionLogEvent(
|
|
199
|
+
trace_id=execution_context.trace_id,
|
|
200
|
+
span_id=parent_context.span_id,
|
|
201
|
+
body=NodeExecutionLogBody(
|
|
202
|
+
node_definition=node_class,
|
|
203
|
+
severity=severity,
|
|
204
|
+
message=message,
|
|
205
|
+
attributes=attributes,
|
|
206
|
+
),
|
|
207
|
+
)
|
|
208
|
+
)
|
|
209
|
+
|
|
143
210
|
def _emit_subworkflow_event(self, event: "WorkflowEvent") -> None:
|
|
144
211
|
if self._event_queue:
|
|
145
212
|
self._event_queue.put(event)
|
|
@@ -147,6 +214,9 @@ class WorkflowContext:
|
|
|
147
214
|
def _register_event_queue(self, event_queue: Queue["WorkflowEvent"]) -> None:
|
|
148
215
|
self._event_queue = event_queue
|
|
149
216
|
|
|
217
|
+
def _register_event_max_size(self, event_max_size: Optional[int]) -> None:
|
|
218
|
+
self._event_max_size = event_max_size
|
|
219
|
+
|
|
150
220
|
def _register_node_output_mocks(self, node_output_mocks: MockNodeExecutionArg) -> None:
|
|
151
221
|
for mock in node_output_mocks:
|
|
152
222
|
if isinstance(mock, MockNodeExecution):
|
|
@@ -290,10 +360,11 @@ class WorkflowContext:
|
|
|
290
360
|
return None
|
|
291
361
|
|
|
292
362
|
@classmethod
|
|
293
|
-
def create_from(cls, context):
|
|
363
|
+
def create_from(cls, context: "WorkflowContext") -> "WorkflowContext":
|
|
294
364
|
return cls(
|
|
295
365
|
vellum_client=context.vellum_client,
|
|
296
366
|
generated_files=context.generated_files,
|
|
297
367
|
namespace=context.namespace,
|
|
298
368
|
store_class=context.store_class,
|
|
369
|
+
event_max_size=context.event_max_size,
|
|
299
370
|
)
|