vellum-ai 0.14.25__py3-none-any.whl → 0.14.27__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.
Files changed (46) hide show
  1. vellum/__init__.py +6 -4
  2. vellum/client/__init__.py +4 -0
  3. vellum/client/core/client_wrapper.py +1 -1
  4. vellum/client/core/jsonable_encoder.py +1 -1
  5. vellum/client/resources/__init__.py +2 -2
  6. vellum/client/resources/prompts/__init__.py +2 -0
  7. vellum/client/resources/prompts/client.py +197 -0
  8. vellum/client/resources/workflows/__init__.py +0 -3
  9. vellum/client/resources/workflows/client.py +0 -9
  10. vellum/client/types/__init__.py +4 -2
  11. vellum/client/types/deployment_release_tag_read.py +7 -1
  12. vellum/client/types/prompt_exec_config.py +37 -0
  13. vellum/client/types/{release.py → release_tag_release.py} +1 -1
  14. vellum/client/types/workflow_release_tag_read.py +2 -2
  15. vellum/client/types/workflow_release_tag_workflow_deployment_history_item.py +3 -10
  16. vellum/{types/release.py → resources/prompts/__init__.py} +1 -1
  17. vellum/resources/{workflows/types/__init__.py → prompts/client.py} +1 -1
  18. vellum/{resources/workflows/types/workflows_pull_request_format.py → types/prompt_exec_config.py} +1 -1
  19. vellum/types/release_tag_release.py +3 -0
  20. vellum/workflows/events/types.py +10 -7
  21. vellum/workflows/nodes/displayable/bases/inline_prompt_node/node.py +2 -4
  22. vellum/workflows/nodes/displayable/bases/prompt_deployment_node.py +2 -4
  23. vellum/workflows/nodes/displayable/conftest.py +117 -0
  24. vellum/workflows/nodes/displayable/guardrail_node/node.py +10 -11
  25. vellum/workflows/nodes/displayable/guardrail_node/test_node.py +38 -0
  26. vellum/workflows/nodes/displayable/inline_prompt_node/tests/test_node.py +49 -0
  27. vellum/workflows/nodes/displayable/prompt_deployment_node/tests/test_node.py +49 -0
  28. vellum/workflows/nodes/displayable/subworkflow_deployment_node/node.py +2 -5
  29. vellum/workflows/nodes/displayable/subworkflow_deployment_node/tests/test_node.py +63 -0
  30. vellum/workflows/references/workflow_input.py +3 -0
  31. vellum/workflows/runner/runner.py +2 -0
  32. {vellum_ai-0.14.25.dist-info → vellum_ai-0.14.27.dist-info}/METADATA +1 -1
  33. {vellum_ai-0.14.25.dist-info → vellum_ai-0.14.27.dist-info}/RECORD +44 -40
  34. vellum_ee/workflows/display/base.py +13 -7
  35. vellum_ee/workflows/display/tests/workflow_serialization/generic_nodes/conftest.py +11 -10
  36. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_default_state_serialization.py +1 -1
  37. vellum_ee/workflows/display/types.py +5 -9
  38. vellum_ee/workflows/display/vellum.py +9 -4
  39. vellum_ee/workflows/display/workflows/base_workflow_display.py +20 -21
  40. vellum_ee/workflows/display/workflows/vellum_workflow_display.py +7 -35
  41. vellum_ee/workflows/tests/test_server.py +54 -0
  42. vellum/client/resources/workflows/types/__init__.py +0 -5
  43. vellum/client/resources/workflows/types/workflows_pull_request_format.py +0 -5
  44. {vellum_ai-0.14.25.dist-info → vellum_ai-0.14.27.dist-info}/LICENSE +0 -0
  45. {vellum_ai-0.14.25.dist-info → vellum_ai-0.14.27.dist-info}/WHEEL +0 -0
  46. {vellum_ai-0.14.25.dist-info → vellum_ai-0.14.27.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,117 @@
1
+ import pytest
2
+ from uuid import UUID
3
+
4
+ from vellum.workflows.events.types import (
5
+ CodeResourceDefinition,
6
+ NodeParentContext,
7
+ WorkflowDeploymentParentContext,
8
+ WorkflowParentContext,
9
+ )
10
+
11
+
12
+ @pytest.fixture
13
+ def mock_complex_parent_context():
14
+ # TODO: We were able to confirm that this parent context caused our serialization to hang, but don't know why yet.
15
+ # We should try to reduce this example further to isolate a minimal example that reproduces the issue.
16
+ return NodeParentContext(
17
+ span_id=UUID("d697f8c8-b363-4154-8469-eb4f9fb5e445"),
18
+ parent=WorkflowParentContext(
19
+ span_id=UUID("a0c68884-22c3-4ac9-8476-8747884d80e1"),
20
+ parent=NodeParentContext(
21
+ span_id=UUID("46163407-71f7-40f2-9f66-872d4b338fcc"),
22
+ parent=WorkflowParentContext(
23
+ span_id=UUID("0ddf01e7-d0c3-426c-af27-d8bfb22fcdd5"),
24
+ parent=NodeParentContext(
25
+ span_id=UUID("79a6c926-b5f3-4ede-b2f8-4bb6f0c086ba"),
26
+ parent=WorkflowParentContext(
27
+ span_id=UUID("530a56fe-90fd-4f4c-b905-457975fb3e10"),
28
+ parent=WorkflowDeploymentParentContext(
29
+ span_id=UUID("3e10a8c2-558c-4ef7-926d-7b79ebc7cba9"),
30
+ parent=NodeParentContext(
31
+ span_id=UUID("a3cd4086-c0b9-4dff-88f3-3e2191b8a2a7"),
32
+ parent=WorkflowParentContext(
33
+ span_id=UUID("c2ba7577-8d24-49b1-aa92-b9ace8244090"),
34
+ workflow_definition=CodeResourceDefinition(
35
+ id=UUID("2e2d5c56-49b7-48b5-82fa-e80e72768b9c"),
36
+ name="Workflow",
37
+ module=["e81a6124-2c57-4c39-938c-ab6059059ff2", "workflow"],
38
+ ),
39
+ ),
40
+ node_definition=CodeResourceDefinition(
41
+ id=UUID("23d25675-f377-4450-916f-39ebee5c8ea9"),
42
+ name="SubworkflowDeployment",
43
+ module=[
44
+ "e81a6124-2c57-4c39-938c-ab6059059ff2",
45
+ "nodes",
46
+ "subworkflow_deployment",
47
+ ],
48
+ ),
49
+ ),
50
+ deployment_id=UUID("cfc99610-2869-4506-b106-3fd7ce0bbb15"),
51
+ deployment_name="my-deployment",
52
+ deployment_history_item_id=UUID("13f31aae-29fd-4066-a4ec-c7687faebae3"),
53
+ release_tag_id=UUID("2d03987a-dcb5-49b9-981e-5e871c8f5d97"),
54
+ release_tag_name="LATEST",
55
+ external_id=None,
56
+ metadata=None,
57
+ workflow_version_id=UUID("7eaae816-b5f3-436d-8597-e8c3e4a32958"),
58
+ ),
59
+ workflow_definition=CodeResourceDefinition(
60
+ id=UUID("2e2d5c56-49b7-48b5-82fa-e80e72768b9c"),
61
+ name="Workflow",
62
+ module=["3e10a8c2-558c-4ef7-926d-7b79ebc7cba9", "workflow"],
63
+ ),
64
+ ),
65
+ node_definition=CodeResourceDefinition(
66
+ id=UUID("42c8adc2-a0d6-499e-81a4-e2e02d7beba9"),
67
+ name="MyNode",
68
+ module=[
69
+ "3e10a8c2-558c-4ef7-926d-7b79ebc7cba9",
70
+ "nodes",
71
+ "my_node",
72
+ ],
73
+ ),
74
+ ),
75
+ workflow_definition=CodeResourceDefinition(
76
+ id=UUID("b8563da0-7fd4-42e0-a75e-9ef037fca5a1"),
77
+ name="MyNodeWorkflow",
78
+ module=[
79
+ "3e10a8c2-558c-4ef7-926d-7b79ebc7cba9",
80
+ "nodes",
81
+ "my_node",
82
+ "workflow",
83
+ ],
84
+ ),
85
+ ),
86
+ node_definition=CodeResourceDefinition(
87
+ id=UUID("d44aee53-3b6e-41fd-8b7a-908cb2c77821"),
88
+ name="RetryNode",
89
+ module=[
90
+ "3e10a8c2-558c-4ef7-926d-7b79ebc7cba9",
91
+ "nodes",
92
+ "my_node",
93
+ "nodes",
94
+ "my_prompt",
95
+ "MyPrompt",
96
+ "<adornment>",
97
+ ],
98
+ ),
99
+ ),
100
+ workflow_definition=CodeResourceDefinition(
101
+ id=UUID("568a28dd-7134-436e-a5f4-790675212b51"),
102
+ name="Subworkflow",
103
+ module=["vellum", "workflows", "nodes", "utils"],
104
+ ),
105
+ ),
106
+ node_definition=CodeResourceDefinition(
107
+ id=UUID("86a34e5c-2652-49f0-9f9e-c653cf70029a"),
108
+ name="MyPrompt",
109
+ module=[
110
+ "3e10a8c2-558c-4ef7-926d-7b79ebc7cba9",
111
+ "nodes",
112
+ "my_node",
113
+ "nodes",
114
+ "my_prompt",
115
+ ],
116
+ ),
117
+ )
@@ -52,19 +52,18 @@ class GuardrailNode(BaseNode[StateType], Generic[StateType]):
52
52
  message="Metric execution must have one output named 'score' with type 'float'",
53
53
  code=WorkflowErrorCode.INVALID_OUTPUTS,
54
54
  )
55
-
56
- log = metric_outputs.get("log")
57
-
58
- if log is not None and not isinstance(log, str):
59
- raise NodeException(
60
- message="Metric execution log output must be of type 'str'",
61
- code=WorkflowErrorCode.INVALID_OUTPUTS,
62
- )
63
- if log:
64
- metric_outputs.pop("log")
65
-
66
55
  metric_outputs.pop("score")
67
56
 
57
+ if "log" in metric_outputs:
58
+ log = metric_outputs.pop("log") or ""
59
+ if not isinstance(log, str):
60
+ raise NodeException(
61
+ message="Metric execution log output must be of type 'str'",
62
+ code=WorkflowErrorCode.INVALID_OUTPUTS,
63
+ )
64
+ else:
65
+ log = None
66
+
68
67
  return self.Outputs(score=score, log=log, **metric_outputs)
69
68
 
70
69
  def _compile_metric_inputs(self) -> List[MetricDefinitionInput]:
@@ -0,0 +1,38 @@
1
+ import pytest
2
+
3
+ from vellum import TestSuiteRunMetricNumberOutput
4
+ from vellum.client.types.metric_definition_execution import MetricDefinitionExecution
5
+ from vellum.client.types.test_suite_run_metric_string_output import TestSuiteRunMetricStringOutput
6
+ from vellum.workflows.nodes.displayable.guardrail_node.node import GuardrailNode
7
+
8
+
9
+ @pytest.mark.parametrize("log_value", [None, ""], ids=["None", "Empty"])
10
+ def test_run_guardrail_node__empty_log(vellum_client, log_value):
11
+ """Confirm that we can successfully invoke a Guardrail Node"""
12
+
13
+ # GIVEN a Guardrail Node
14
+ class MyGuard(GuardrailNode):
15
+ metric_definition = "example_metric_definition"
16
+ metric_inputs = {}
17
+
18
+ # AND we know that the guardrail node will return a blank log
19
+ mock_metric_execution = MetricDefinitionExecution(
20
+ outputs=[
21
+ TestSuiteRunMetricNumberOutput(
22
+ name="score",
23
+ value=0.6,
24
+ ),
25
+ TestSuiteRunMetricStringOutput(
26
+ name="log",
27
+ value=log_value,
28
+ ),
29
+ ],
30
+ )
31
+ vellum_client.metric_definitions.execute_metric_definition.return_value = mock_metric_execution
32
+
33
+ # WHEN we run the Guardrail Node
34
+ outputs = MyGuard().run()
35
+
36
+ # THEN the workflow should have completed successfully
37
+ assert outputs.score == 0.6
38
+ assert outputs.log == ""
@@ -1,8 +1,11 @@
1
1
  import pytest
2
2
  from dataclasses import dataclass
3
+ import json
3
4
  from uuid import uuid4
4
5
  from typing import Any, Iterator, List
5
6
 
7
+ from httpx import Response
8
+
6
9
  from vellum.client.core.api_error import ApiError
7
10
  from vellum.client.core.pydantic_utilities import UniversalBaseModel
8
11
  from vellum.client.types.chat_message import ChatMessage
@@ -17,6 +20,7 @@ from vellum.client.types.prompt_output import PromptOutput
17
20
  from vellum.client.types.prompt_request_chat_history_input import PromptRequestChatHistoryInput
18
21
  from vellum.client.types.prompt_request_json_input import PromptRequestJsonInput
19
22
  from vellum.client.types.string_vellum_value import StringVellumValue
23
+ from vellum.workflows.context import execution_context
20
24
  from vellum.workflows.errors.types import WorkflowErrorCode
21
25
  from vellum.workflows.exceptions import NodeException
22
26
  from vellum.workflows.nodes.displayable.inline_prompt_node.node import InlinePromptNode
@@ -230,3 +234,48 @@ def test_inline_prompt_node__chat_history_inputs(vellum_adhoc_prompt_client):
230
234
  ),
231
235
  ]
232
236
  assert mock_api.call_args.kwargs["input_variables"][0].type == "CHAT_HISTORY"
237
+
238
+
239
+ @pytest.mark.timeout(5)
240
+ def test_inline_prompt_node__parent_context(mock_httpx_transport, mock_complex_parent_context):
241
+ # GIVEN a prompt node
242
+ class MyNode(InlinePromptNode):
243
+ ml_model = "gpt-4o"
244
+ blocks = []
245
+ prompt_inputs = {}
246
+
247
+ # AND a known response from the httpx client
248
+ expected_outputs: List[PromptOutput] = [
249
+ StringVellumValue(value="Test"),
250
+ ]
251
+ execution_id = str(uuid4())
252
+ events: List[ExecutePromptEvent] = [
253
+ InitiatedExecutePromptEvent(execution_id=execution_id),
254
+ FulfilledExecutePromptEvent(
255
+ execution_id=execution_id,
256
+ outputs=expected_outputs,
257
+ ),
258
+ ]
259
+ text = "\n".join(e.model_dump_json() for e in events)
260
+
261
+ mock_httpx_transport.handle_request.return_value = Response(
262
+ status_code=200,
263
+ text=text,
264
+ )
265
+
266
+ # WHEN the node is run with the complex parent context
267
+ trace_id = uuid4()
268
+ with execution_context(
269
+ parent_context=mock_complex_parent_context,
270
+ trace_id=trace_id,
271
+ ):
272
+ outputs = list(MyNode().run())
273
+
274
+ # THEN the last output is as expected
275
+ assert outputs[-1].value == "Test"
276
+
277
+ # AND the prompt is executed with the correct execution context
278
+ call_request_args = mock_httpx_transport.handle_request.call_args_list[0][0][0]
279
+ request_execution_context = json.loads(call_request_args.read().decode("utf-8"))["execution_context"]
280
+ assert request_execution_context["trace_id"] == str(trace_id)
281
+ assert request_execution_context["parent_context"]
@@ -1,7 +1,10 @@
1
1
  import pytest
2
+ import json
2
3
  from uuid import uuid4
3
4
  from typing import Any, Iterator, List
4
5
 
6
+ from httpx import Response
7
+
5
8
  from vellum.client.types.chat_history_input_request import ChatHistoryInputRequest
6
9
  from vellum.client.types.chat_message import ChatMessage
7
10
  from vellum.client.types.chat_message_request import ChatMessageRequest
@@ -9,7 +12,9 @@ from vellum.client.types.execute_prompt_event import ExecutePromptEvent
9
12
  from vellum.client.types.fulfilled_execute_prompt_event import FulfilledExecutePromptEvent
10
13
  from vellum.client.types.initiated_execute_prompt_event import InitiatedExecutePromptEvent
11
14
  from vellum.client.types.json_input_request import JsonInputRequest
15
+ from vellum.client.types.prompt_output import PromptOutput
12
16
  from vellum.client.types.string_vellum_value import StringVellumValue
17
+ from vellum.workflows.context import execution_context
13
18
  from vellum.workflows.nodes.displayable.prompt_deployment_node.node import PromptDeploymentNode
14
19
 
15
20
 
@@ -94,3 +99,47 @@ def test_run_node__any_array_input(vellum_client):
94
99
  assert call_kwargs["inputs"] == [
95
100
  JsonInputRequest(name="fruits", value=["apple", "banana", "cherry"]),
96
101
  ]
102
+
103
+
104
+ @pytest.mark.timeout(5)
105
+ def test_prompt_deployment_node__parent_context_serialization(mock_httpx_transport, mock_complex_parent_context):
106
+ # GIVEN a prompt deployment node
107
+ class MyNode(PromptDeploymentNode):
108
+ deployment = "example_prompt_deployment"
109
+ prompt_inputs = {}
110
+
111
+ # AND a known response from the httpx client
112
+ expected_outputs: List[PromptOutput] = [
113
+ StringVellumValue(value="Test"),
114
+ ]
115
+ execution_id = str(uuid4())
116
+ events: List[ExecutePromptEvent] = [
117
+ InitiatedExecutePromptEvent(execution_id=execution_id),
118
+ FulfilledExecutePromptEvent(
119
+ execution_id=execution_id,
120
+ outputs=expected_outputs,
121
+ ),
122
+ ]
123
+ text = "\n".join(e.model_dump_json() for e in events)
124
+
125
+ mock_httpx_transport.handle_request.return_value = Response(
126
+ status_code=200,
127
+ text=text,
128
+ )
129
+
130
+ # WHEN the node is run with a complex parent context
131
+ trace_id = uuid4()
132
+ with execution_context(
133
+ parent_context=mock_complex_parent_context,
134
+ trace_id=trace_id,
135
+ ):
136
+ outputs = list(MyNode().run())
137
+
138
+ # THEN the last output is as expected
139
+ assert outputs[-1].value == "Test"
140
+
141
+ # AND the prompt is executed with the correct execution context
142
+ call_request_args = mock_httpx_transport.handle_request.call_args_list[0][0][0]
143
+ request_execution_context = json.loads(call_request_args.read().decode("utf-8"))["execution_context"]
144
+ assert request_execution_context["trace_id"] == str(trace_id)
145
+ assert request_execution_context["parent_context"]
@@ -122,13 +122,10 @@ class SubworkflowDeploymentNode(BaseNode[StateType], Generic[StateType]):
122
122
  return compiled_inputs
123
123
 
124
124
  def run(self) -> Iterator[BaseOutput]:
125
- current_context = get_execution_context()
126
- parent_context = (
127
- current_context.parent_context.model_dump(mode="json") if current_context.parent_context else None
128
- )
125
+ execution_context = get_execution_context()
129
126
  request_options = self.request_options or RequestOptions()
130
127
  request_options["additional_body_parameters"] = {
131
- "execution_context": {"parent_context": parent_context, "trace_id": current_context.trace_id},
128
+ "execution_context": execution_context.model_dump(mode="json"),
132
129
  **request_options.get("additional_body_parameters", {}),
133
130
  }
134
131
 
@@ -1,8 +1,11 @@
1
1
  import pytest
2
2
  from datetime import datetime
3
+ import json
3
4
  from uuid import uuid4
4
5
  from typing import Any, Iterator, List
5
6
 
7
+ from httpx import Response
8
+
6
9
  from vellum.client.core.api_error import ApiError
7
10
  from vellum.client.types.chat_message import ChatMessage
8
11
  from vellum.client.types.chat_message_request import ChatMessageRequest
@@ -13,6 +16,7 @@ from vellum.client.types.workflow_request_json_input_request import WorkflowRequ
13
16
  from vellum.client.types.workflow_request_number_input_request import WorkflowRequestNumberInputRequest
14
17
  from vellum.client.types.workflow_result_event import WorkflowResultEvent
15
18
  from vellum.client.types.workflow_stream_event import WorkflowStreamEvent
19
+ from vellum.workflows.context import execution_context
16
20
  from vellum.workflows.errors import WorkflowErrorCode
17
21
  from vellum.workflows.exceptions import NodeException
18
22
  from vellum.workflows.nodes.displayable.subworkflow_deployment_node.node import SubworkflowDeploymentNode
@@ -405,3 +409,62 @@ def test_subworkflow_deployment_node__immediate_api_error__node_exception(vellum
405
409
  # THEN the node raises the correct NodeException
406
410
  assert e.value.code == WorkflowErrorCode.INVALID_INPUTS
407
411
  assert e.value.message == "Not found"
412
+
413
+
414
+ @pytest.mark.timeout(5)
415
+ def test_prompt_deployment_node__parent_context_serialization(mock_httpx_transport, mock_complex_parent_context):
416
+ # GIVEN a prompt deployment node
417
+ class MyNode(SubworkflowDeploymentNode):
418
+ deployment = "example_subworkflow_deployment"
419
+ subworkflow_inputs = {}
420
+
421
+ # AND a known response from the httpx client
422
+ execution_id = str(uuid4())
423
+ events: List[WorkflowStreamEvent] = [
424
+ WorkflowExecutionWorkflowResultEvent(
425
+ execution_id=execution_id,
426
+ data=WorkflowResultEvent(
427
+ id=str(uuid4()),
428
+ state="INITIATED",
429
+ ts=datetime.now(),
430
+ ),
431
+ ),
432
+ WorkflowExecutionWorkflowResultEvent(
433
+ execution_id=execution_id,
434
+ data=WorkflowResultEvent(
435
+ id=str(uuid4()),
436
+ state="FULFILLED",
437
+ ts=datetime.now(),
438
+ outputs=[
439
+ WorkflowOutputString(
440
+ id=str(uuid4()),
441
+ name="final-output_copy", # Note the hyphen here
442
+ value="Test",
443
+ )
444
+ ],
445
+ ),
446
+ ),
447
+ ]
448
+ text = "\n".join(e.model_dump_json() for e in events)
449
+
450
+ mock_httpx_transport.handle_request.return_value = Response(
451
+ status_code=200,
452
+ text=text,
453
+ )
454
+
455
+ # WHEN the node is run with a complex parent context
456
+ trace_id = uuid4()
457
+ with execution_context(
458
+ parent_context=mock_complex_parent_context,
459
+ trace_id=trace_id,
460
+ ):
461
+ outputs = list(MyNode().run())
462
+
463
+ # THEN the last output is as expected
464
+ assert outputs[-1].value == "Test"
465
+
466
+ # AND the prompt is executed with the correct execution context
467
+ call_request_args = mock_httpx_transport.handle_request.call_args_list[0][0][0]
468
+ request_execution_context = json.loads(call_request_args.read().decode("utf-8"))["execution_context"]
469
+ assert request_execution_context["trace_id"] == str(trace_id)
470
+ assert request_execution_context["parent_context"]
@@ -35,6 +35,9 @@ class WorkflowInputReference(BaseDescriptor[_InputType], Generic[_InputType]):
35
35
  if state.meta.parent:
36
36
  return self.resolve(state.meta.parent)
37
37
 
38
+ if type(None) in self.types:
39
+ return cast(_InputType, None)
40
+
38
41
  raise NodeException(f"Missing required Workflow input: {self._name}", code=WorkflowErrorCode.INVALID_INPUTS)
39
42
 
40
43
  def __repr__(self) -> str:
@@ -321,6 +321,7 @@ class WorkflowRunner(Generic[StateType]):
321
321
  )
322
322
  )
323
323
  except NodeException as e:
324
+ logger.info(e)
324
325
  self._workflow_event_inner_queue.put(
325
326
  NodeExecutionRejectedEvent(
326
327
  trace_id=node.state.meta.trace_id,
@@ -333,6 +334,7 @@ class WorkflowRunner(Generic[StateType]):
333
334
  )
334
335
  )
335
336
  except WorkflowInitializationException as e:
337
+ logger.info(e)
336
338
  self._workflow_event_inner_queue.put(
337
339
  NodeExecutionRejectedEvent(
338
340
  trace_id=node.state.meta.trace_id,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: vellum-ai
3
- Version: 0.14.25
3
+ Version: 0.14.27
4
4
  Summary:
5
5
  License: MIT
6
6
  Requires-Python: >=3.9,<4.0