vellum-ai 1.2.2__py3-none-any.whl → 1.2.4__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 (106) hide show
  1. vellum/__init__.py +40 -0
  2. vellum/client/core/client_wrapper.py +2 -2
  3. vellum/client/core/pydantic_utilities.py +3 -2
  4. vellum/client/reference.md +16 -0
  5. vellum/client/resources/workflow_executions/client.py +28 -4
  6. vellum/client/resources/workflow_executions/raw_client.py +32 -2
  7. vellum/client/types/__init__.py +40 -0
  8. vellum/client/types/audio_input_request.py +30 -0
  9. vellum/client/types/delimiter_chunker_config.py +20 -0
  10. vellum/client/types/delimiter_chunker_config_request.py +20 -0
  11. vellum/client/types/delimiter_chunking.py +21 -0
  12. vellum/client/types/delimiter_chunking_request.py +21 -0
  13. vellum/client/types/document_index_chunking.py +4 -1
  14. vellum/client/types/document_index_chunking_request.py +2 -1
  15. vellum/client/types/document_input_request.py +30 -0
  16. vellum/client/types/execution_audio_vellum_value.py +31 -0
  17. vellum/client/types/execution_document_vellum_value.py +31 -0
  18. vellum/client/types/execution_image_vellum_value.py +31 -0
  19. vellum/client/types/execution_vellum_value.py +8 -0
  20. vellum/client/types/execution_video_vellum_value.py +31 -0
  21. vellum/client/types/image_input_request.py +30 -0
  22. vellum/client/types/logical_operator.py +1 -0
  23. vellum/client/types/node_input_compiled_audio_value.py +23 -0
  24. vellum/client/types/node_input_compiled_document_value.py +23 -0
  25. vellum/client/types/node_input_compiled_image_value.py +23 -0
  26. vellum/client/types/node_input_compiled_video_value.py +23 -0
  27. vellum/client/types/node_input_variable_compiled_value.py +8 -0
  28. vellum/client/types/prompt_deployment_input_request.py +13 -1
  29. vellum/client/types/prompt_request_audio_input.py +26 -0
  30. vellum/client/types/prompt_request_document_input.py +26 -0
  31. vellum/client/types/prompt_request_image_input.py +26 -0
  32. vellum/client/types/prompt_request_input.py +13 -1
  33. vellum/client/types/prompt_request_video_input.py +26 -0
  34. vellum/client/types/video_input_request.py +30 -0
  35. vellum/types/audio_input_request.py +3 -0
  36. vellum/types/delimiter_chunker_config.py +3 -0
  37. vellum/types/delimiter_chunker_config_request.py +3 -0
  38. vellum/types/delimiter_chunking.py +3 -0
  39. vellum/types/delimiter_chunking_request.py +3 -0
  40. vellum/types/document_input_request.py +3 -0
  41. vellum/types/execution_audio_vellum_value.py +3 -0
  42. vellum/types/execution_document_vellum_value.py +3 -0
  43. vellum/types/execution_image_vellum_value.py +3 -0
  44. vellum/types/execution_video_vellum_value.py +3 -0
  45. vellum/types/image_input_request.py +3 -0
  46. vellum/types/node_input_compiled_audio_value.py +3 -0
  47. vellum/types/node_input_compiled_document_value.py +3 -0
  48. vellum/types/node_input_compiled_image_value.py +3 -0
  49. vellum/types/node_input_compiled_video_value.py +3 -0
  50. vellum/types/prompt_request_audio_input.py +3 -0
  51. vellum/types/prompt_request_document_input.py +3 -0
  52. vellum/types/prompt_request_image_input.py +3 -0
  53. vellum/types/prompt_request_video_input.py +3 -0
  54. vellum/types/video_input_request.py +3 -0
  55. vellum/workflows/context.py +27 -9
  56. vellum/workflows/events/context.py +53 -78
  57. vellum/workflows/events/node.py +5 -5
  58. vellum/workflows/events/relational_threads.py +41 -0
  59. vellum/workflows/events/tests/test_basic_workflow.py +50 -0
  60. vellum/workflows/events/tests/test_event.py +9 -0
  61. vellum/workflows/events/types.py +3 -1
  62. vellum/workflows/events/workflow.py +12 -1
  63. vellum/workflows/expressions/contains.py +7 -0
  64. vellum/workflows/expressions/tests/test_contains.py +175 -0
  65. vellum/workflows/graph/graph.py +52 -8
  66. vellum/workflows/graph/tests/test_graph.py +17 -0
  67. vellum/workflows/integrations/mcp_service.py +35 -5
  68. vellum/workflows/integrations/tests/test_mcp_service.py +120 -0
  69. vellum/workflows/nodes/core/error_node/node.py +4 -0
  70. vellum/workflows/nodes/core/map_node/node.py +7 -0
  71. vellum/workflows/nodes/core/map_node/tests/test_node.py +19 -0
  72. vellum/workflows/nodes/core/templating_node/node.py +3 -2
  73. vellum/workflows/nodes/core/templating_node/tests/test_templating_node.py +129 -0
  74. vellum/workflows/nodes/displayable/bases/inline_prompt_node/node.py +12 -0
  75. vellum/workflows/nodes/displayable/bases/inline_prompt_node/tests/test_inline_prompt_node.py +41 -0
  76. vellum/workflows/nodes/displayable/bases/utils.py +38 -1
  77. vellum/workflows/nodes/displayable/code_execution_node/utils.py +3 -20
  78. vellum/workflows/nodes/displayable/final_output_node/node.py +4 -0
  79. vellum/workflows/nodes/displayable/inline_prompt_node/node.py +3 -26
  80. vellum/workflows/nodes/displayable/prompt_deployment_node/node.py +3 -25
  81. vellum/workflows/nodes/displayable/subworkflow_deployment_node/node.py +1 -1
  82. vellum/workflows/nodes/utils.py +26 -1
  83. vellum/workflows/ports/node_ports.py +3 -0
  84. vellum/workflows/ports/port.py +7 -0
  85. vellum/workflows/state/context.py +35 -4
  86. vellum/workflows/types/definition.py +1 -0
  87. vellum/workflows/utils/functions.py +4 -0
  88. vellum/workflows/utils/tests/test_functions.py +6 -3
  89. vellum/workflows/utils/uuids.py +15 -0
  90. {vellum_ai-1.2.2.dist-info → vellum_ai-1.2.4.dist-info}/METADATA +1 -1
  91. {vellum_ai-1.2.2.dist-info → vellum_ai-1.2.4.dist-info}/RECORD +106 -60
  92. vellum_cli/__init__.py +6 -0
  93. vellum_cli/config.py +2 -0
  94. vellum_cli/push.py +3 -0
  95. vellum_cli/tests/test_pull.py +2 -0
  96. vellum_cli/tests/test_push.py +39 -0
  97. vellum_ee/workflows/display/nodes/vellum/error_node.py +1 -5
  98. vellum_ee/workflows/display/nodes/vellum/final_output_node.py +1 -5
  99. vellum_ee/workflows/display/nodes/vellum/tests/test_tool_calling_node.py +2 -0
  100. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_tool_calling_node_mcp_serialization.py +1 -0
  101. vellum_ee/workflows/display/utils/events.py +24 -0
  102. vellum_ee/workflows/display/utils/tests/test_events.py +69 -0
  103. vellum_ee/workflows/tests/test_server.py +95 -0
  104. {vellum_ai-1.2.2.dist-info → vellum_ai-1.2.4.dist-info}/LICENSE +0 -0
  105. {vellum_ai-1.2.2.dist-info → vellum_ai-1.2.4.dist-info}/WHEEL +0 -0
  106. {vellum_ai-1.2.2.dist-info → vellum_ai-1.2.4.dist-info}/entry_points.txt +0 -0
@@ -317,3 +317,132 @@ def test_api_error_templating_node():
317
317
 
318
318
  # THEN the output should be empty string
319
319
  assert outputs.result == ""
320
+
321
+
322
+ @pytest.mark.parametrize(
323
+ "template,inputs,expected_result",
324
+ [
325
+ # String value access
326
+ ("{{ text_input.value }}", {"text_input": "hello world"}, "hello world"),
327
+ # Function call value access
328
+ (
329
+ "{{ func.value.name }}",
330
+ {"func": FunctionCall(name="test_function", arguments={"key": "value"})},
331
+ "test_function",
332
+ ),
333
+ # Array item value access
334
+ ("{{ items[0].value }}", {"items": ["apple"]}, "apple"),
335
+ ],
336
+ ids=["string_value", "function_call_value", "array_item_value"],
337
+ )
338
+ def test_templating_node__value_access_patterns_str(template, inputs, expected_result):
339
+ # GIVEN a templating node that accesses wrapper value properties
340
+ class TemplateNode(TemplatingNode[BaseState, str]):
341
+ pass
342
+
343
+ # Set template and inputs dynamically
344
+ TemplateNode.template = template
345
+ TemplateNode.inputs = inputs
346
+
347
+ # WHEN the node is run
348
+ node = TemplateNode()
349
+ outputs = node.run()
350
+
351
+ # THEN the value is accessible
352
+ assert outputs.result == expected_result
353
+
354
+
355
+ @pytest.mark.parametrize(
356
+ "template,inputs,expected_result",
357
+ [
358
+ # Dict value access
359
+ ("{{ data.value }}", {"data": {"name": "test", "score": 42}}, {"name": "test", "score": 42}),
360
+ # List value access
361
+ ("{{ items.value }}", {"items": ["item1", "item2", "item3"]}, ["item1", "item2", "item3"]),
362
+ ],
363
+ ids=["dict_value", "list_value"],
364
+ )
365
+ def test_templating_node__value_access_patterns_json(template, inputs, expected_result):
366
+ # GIVEN a templating node that accesses wrapper value properties
367
+ class TemplateNode(TemplatingNode[BaseState, Json]):
368
+ pass
369
+
370
+ # Set template and inputs dynamically
371
+ TemplateNode.template = template
372
+ TemplateNode.inputs = inputs
373
+
374
+ # WHEN the node is run
375
+ node = TemplateNode()
376
+ outputs = node.run()
377
+
378
+ # THEN the value is accessible
379
+ assert outputs.result == expected_result
380
+
381
+
382
+ @pytest.mark.parametrize(
383
+ "template,inputs,expected_result",
384
+ [
385
+ # String type access
386
+ ("{{ text_input.type }}", {"text_input": "hello world"}, "STRING"),
387
+ # Function call type access
388
+ ("{{ func.type }}", {"func": FunctionCall(name="test_function", arguments={"key": "value"})}, "FUNCTION_CALL"),
389
+ ],
390
+ ids=["string_type", "function_call_type"],
391
+ )
392
+ def test_templating_node__type_access_patterns(template, inputs, expected_result):
393
+ # GIVEN a templating node that accesses wrapper type properties
394
+ class TemplateNode(TemplatingNode[BaseState, str]):
395
+ pass
396
+
397
+ # Set template and inputs dynamically
398
+ TemplateNode.template = template
399
+ TemplateNode.inputs = inputs
400
+
401
+ # WHEN the node is run
402
+ node = TemplateNode()
403
+ outputs = node.run()
404
+
405
+ # THEN the type is accessible
406
+ assert outputs.result == expected_result
407
+
408
+
409
+ def test_templating_node__nested_dict_access():
410
+ # GIVEN a templating node with nested dict access
411
+ class TemplateNode(TemplatingNode[BaseState, str]):
412
+ template = "{{ data.user.name }}"
413
+ inputs = {"data": {"user": {"name": "John Doe", "age": 30}, "status": "active"}}
414
+
415
+ # WHEN the node is run
416
+ node = TemplateNode()
417
+ outputs = node.run()
418
+
419
+ # THEN nested properties are accessible
420
+ assert outputs.result == "John Doe"
421
+
422
+
423
+ def test_templating_node__list_iteration_wrapper_access():
424
+ # GIVEN a templating node that iterates over list with wrapper access
425
+ class TemplateNode(TemplatingNode[BaseState, str]):
426
+ template = "{% for item in items %}{{ item.value }}{% if not loop.last %},{% endif %}{% endfor %}"
427
+ inputs = {"items": ["apple", "banana", "cherry"]}
428
+
429
+ # WHEN the node is run
430
+ node = TemplateNode()
431
+ outputs = node.run()
432
+
433
+ # THEN list iteration with wrapper access works
434
+ assert outputs.result == "apple,banana,cherry"
435
+
436
+
437
+ def test_templating_node__conditional_type_checking():
438
+ # GIVEN a templating node with conditional type checking
439
+ class TemplateNode(TemplatingNode[BaseState, str]):
440
+ template = "{% if input.type == 'STRING' %}{{ input.value }}{% else %}unknown{% endif %}"
441
+ inputs = {"input": "test string"}
442
+
443
+ # WHEN the node is run
444
+ node = TemplateNode()
445
+ outputs = node.run()
446
+
447
+ # THEN conditional type checking works
448
+ assert outputs.result == "test string"
@@ -28,6 +28,7 @@ from vellum.workflows.errors.types import vellum_error_to_workflow_error
28
28
  from vellum.workflows.events.types import default_serializer
29
29
  from vellum.workflows.exceptions import NodeException
30
30
  from vellum.workflows.nodes.displayable.bases.base_prompt_node import BasePromptNode
31
+ from vellum.workflows.nodes.displayable.bases.utils import process_additional_prompt_outputs
31
32
  from vellum.workflows.outputs import BaseOutput
32
33
  from vellum.workflows.types import MergeBehavior
33
34
  from vellum.workflows.types.definition import DeploymentDefinition
@@ -197,6 +198,17 @@ class BaseInlinePromptNode(BasePromptNode[StateType], Generic[StateType]):
197
198
  elif event.state == "STREAMING":
198
199
  yield BaseOutput(name="results", delta=event.output.value)
199
200
  elif event.state == "FULFILLED":
201
+ if event.meta and event.meta.finish_reason == "LENGTH":
202
+ text_value, json_value = process_additional_prompt_outputs(event.outputs)
203
+ if text_value == "":
204
+ raise NodeException(
205
+ message=(
206
+ "Maximum tokens reached before model could output any content. "
207
+ "Consider increasing the max_tokens Prompt Parameter."
208
+ ),
209
+ code=WorkflowErrorCode.INVALID_OUTPUTS,
210
+ )
211
+
200
212
  outputs = event.outputs
201
213
  yield BaseOutput(name="results", value=event.outputs)
202
214
  elif event.state == "REJECTED":
@@ -20,6 +20,7 @@ from vellum import (
20
20
  )
21
21
  from vellum.client.types.execute_prompt_event import ExecutePromptEvent
22
22
  from vellum.client.types.fulfilled_execute_prompt_event import FulfilledExecutePromptEvent
23
+ from vellum.client.types.fulfilled_prompt_execution_meta import FulfilledPromptExecutionMeta
23
24
  from vellum.client.types.initiated_execute_prompt_event import InitiatedExecutePromptEvent
24
25
  from vellum.client.types.prompt_output import PromptOutput
25
26
  from vellum.client.types.prompt_request_string_input import PromptRequestStringInput
@@ -684,3 +685,43 @@ def test_inline_prompt_node__invalid_function_type():
684
685
  # AND the error should have the correct code and message
685
686
  assert excinfo.value.code == WorkflowErrorCode.INVALID_INPUTS
686
687
  assert "`not_a_function` is not a valid function definition" == str(excinfo.value)
688
+
689
+
690
+ def test_inline_prompt_node__empty_string_output_with_length_finish_reason(vellum_adhoc_prompt_client):
691
+ """
692
+ Tests that InlinePromptNode raises NodeException for empty string output with LENGTH finish_reason.
693
+ """
694
+
695
+ # GIVEN an InlinePromptNode with basic configuration
696
+ class TestNode(InlinePromptNode):
697
+ ml_model = "test-model"
698
+ blocks = []
699
+ prompt_inputs = {}
700
+
701
+ expected_outputs: List[PromptOutput] = [
702
+ StringVellumValue(value=""),
703
+ ]
704
+
705
+ def generate_prompt_events(*args: Any, **kwargs: Any) -> Iterator[ExecutePromptEvent]:
706
+ execution_id = str(uuid4())
707
+ events: List[ExecutePromptEvent] = [
708
+ InitiatedExecutePromptEvent(execution_id=execution_id),
709
+ FulfilledExecutePromptEvent(
710
+ execution_id=execution_id,
711
+ outputs=expected_outputs,
712
+ meta=FulfilledPromptExecutionMeta(finish_reason="LENGTH"),
713
+ ),
714
+ ]
715
+ yield from events
716
+
717
+ vellum_adhoc_prompt_client.adhoc_execute_prompt_stream.side_effect = generate_prompt_events
718
+
719
+ # WHEN the node is run
720
+ node = TestNode()
721
+
722
+ # THEN it should raise a NodeException with INVALID_OUTPUTS error code
723
+ with pytest.raises(NodeException) as excinfo:
724
+ list(node.run())
725
+
726
+ # AND the exception should have the correct error code
727
+ assert excinfo.value.code == WorkflowErrorCode.INVALID_OUTPUTS
@@ -1,7 +1,8 @@
1
1
  import enum
2
2
  import json
3
- from typing import Any, List, Union, cast
3
+ from typing import Any, List, Optional, Tuple, Union, cast
4
4
 
5
+ from vellum import PromptOutput
5
6
  from vellum.client.types.array_vellum_value import ArrayVellumValue
6
7
  from vellum.client.types.array_vellum_value_request import ArrayVellumValueRequest
7
8
  from vellum.client.types.audio_vellum_value import AudioVellumValue
@@ -123,3 +124,39 @@ def primitive_to_vellum_value_request(value: Any) -> VellumValueRequest:
123
124
  raise ValueError(f"Unsupported variable type: {vellum_value.__class__.__name__}")
124
125
 
125
126
  return vellum_value_request_class.model_validate(vellum_value.model_dump())
127
+
128
+
129
+ def process_additional_prompt_outputs(outputs: List[PromptOutput]) -> Tuple[str, Optional[Any]]:
130
+ """
131
+ Process prompt outputs using the same logic as prompt nodes to determine text output.
132
+
133
+ Args:
134
+ outputs: List of prompt outputs to process
135
+
136
+ Returns:
137
+ The text representation of the outputs joined with newlines
138
+ The JSON representation of the outputs
139
+ """
140
+ string_outputs = []
141
+ json_output = None
142
+ for output in outputs:
143
+ if output.value is None:
144
+ continue
145
+
146
+ if output.type == "STRING":
147
+ string_outputs.append(output.value)
148
+ try:
149
+ json_output = json.loads(output.value)
150
+ except (json.JSONDecodeError, TypeError):
151
+ pass
152
+ elif output.type == "JSON":
153
+ string_outputs.append(json.dumps(output.value, indent=4))
154
+ json_output = output.value
155
+ elif output.type == "FUNCTION_CALL":
156
+ string_outputs.append(output.value.model_dump_json(indent=4))
157
+ elif output.type == "THINKING":
158
+ continue
159
+ else:
160
+ string_outputs.append(output.value.message)
161
+
162
+ return "\n".join(string_outputs), json_output
@@ -4,13 +4,10 @@ import sys
4
4
  import traceback
5
5
  from typing import Any, Optional, Tuple, Union
6
6
 
7
- from pydantic import BaseModel
8
-
9
7
  from vellum.workflows.errors.types import WorkflowErrorCode
10
8
  from vellum.workflows.exceptions import NodeException
11
- from vellum.workflows.nodes.utils import cast_to_output_type
9
+ from vellum.workflows.nodes.utils import cast_to_output_type, wrap_inputs_for_backward_compatibility
12
10
  from vellum.workflows.state.context import WorkflowContext
13
- from vellum.workflows.types.code_execution_node_wrappers import ListWrapper, clean_for_dict_wrapper
14
11
  from vellum.workflows.types.core import EntityInputsInterface
15
12
 
16
13
 
@@ -51,23 +48,9 @@ def run_code_inline(
51
48
  print_line = f"{' '.join(str_args)}\n"
52
49
  log_buffer.write(print_line)
53
50
 
54
- def wrap_value(value):
55
- if isinstance(value, list):
56
- return ListWrapper(
57
- [
58
- # Convert VellumValue to dict with its fields
59
- (
60
- item.model_dump()
61
- if isinstance(item, BaseModel)
62
- else clean_for_dict_wrapper(item) if isinstance(item, (dict, list, str)) else item
63
- )
64
- for item in value
65
- ]
66
- )
67
- return clean_for_dict_wrapper(value)
68
-
51
+ wrapped_inputs = wrap_inputs_for_backward_compatibility(inputs)
69
52
  exec_globals = {
70
- "__arg__inputs": {name: wrap_value(value) for name, value in inputs.items()},
53
+ "__arg__inputs": wrapped_inputs,
71
54
  "__arg__out": None,
72
55
  "print": _inline_print,
73
56
  }
@@ -4,6 +4,7 @@ from vellum.workflows.constants import undefined
4
4
  from vellum.workflows.nodes.bases import BaseNode
5
5
  from vellum.workflows.nodes.bases.base import BaseNodeMeta
6
6
  from vellum.workflows.nodes.utils import cast_to_output_type
7
+ from vellum.workflows.ports import NodePorts
7
8
  from vellum.workflows.types import MergeBehavior
8
9
  from vellum.workflows.types.generics import StateType
9
10
  from vellum.workflows.types.utils import get_original_base
@@ -47,6 +48,9 @@ class FinalOutputNode(BaseNode[StateType], Generic[StateType, _OutputType], meta
47
48
  class Trigger(BaseNode.Trigger):
48
49
  merge_behavior = MergeBehavior.AWAIT_ANY
49
50
 
51
+ class Ports(NodePorts):
52
+ pass
53
+
50
54
  class Outputs(BaseNode.Outputs):
51
55
  # We use our mypy plugin to override the _OutputType with the actual output type
52
56
  # for downstream references to this output.
@@ -1,10 +1,10 @@
1
- import json
2
1
  from typing import Any, Dict, Generic, Iterator, Type, Union
3
2
 
4
3
  from vellum.workflows.constants import undefined
5
4
  from vellum.workflows.errors import WorkflowErrorCode
6
5
  from vellum.workflows.exceptions import NodeException
7
6
  from vellum.workflows.nodes.displayable.bases import BaseInlinePromptNode as BaseInlinePromptNode
7
+ from vellum.workflows.nodes.displayable.bases.utils import process_additional_prompt_outputs
8
8
  from vellum.workflows.outputs import BaseOutput
9
9
  from vellum.workflows.types import MergeBehavior
10
10
  from vellum.workflows.types.generics import StateType
@@ -45,31 +45,8 @@ class InlinePromptNode(BaseInlinePromptNode[StateType], Generic[StateType]):
45
45
  code=WorkflowErrorCode.INTERNAL_ERROR,
46
46
  )
47
47
 
48
- string_outputs = []
49
- json_output = None
50
-
51
- for output in outputs:
52
- if output.value is None:
53
- continue
54
-
55
- if output.type == "STRING":
56
- string_outputs.append(output.value)
57
- try:
58
- json_output = json.loads(output.value)
59
- except (json.JSONDecodeError, TypeError):
60
- pass
61
- elif output.type == "JSON":
62
- string_outputs.append(json.dumps(output.value, indent=4))
63
- json_output = output.value
64
- elif output.type == "FUNCTION_CALL":
65
- string_outputs.append(output.value.model_dump_json(indent=4))
66
- elif output.type == "THINKING":
67
- continue
68
- else:
69
- string_outputs.append(output.value.message)
70
-
71
- value = "\n".join(string_outputs)
72
- yield BaseOutput(name="text", value=value)
48
+ text_output, json_output = process_additional_prompt_outputs(outputs)
49
+ yield BaseOutput(name="text", value=text_output)
73
50
 
74
51
  if json_output:
75
52
  yield BaseOutput(name="json", value=json_output)
@@ -1,10 +1,10 @@
1
- import json
2
1
  from typing import Any, Dict, Iterator, Type, Union
3
2
 
4
3
  from vellum.workflows.constants import undefined
5
4
  from vellum.workflows.errors import WorkflowErrorCode
6
5
  from vellum.workflows.exceptions import NodeException
7
6
  from vellum.workflows.nodes.displayable.bases import BasePromptDeploymentNode as BasePromptDeploymentNode
7
+ from vellum.workflows.nodes.displayable.bases.utils import process_additional_prompt_outputs
8
8
  from vellum.workflows.outputs import BaseOutput
9
9
  from vellum.workflows.types import MergeBehavior
10
10
  from vellum.workflows.types.generics import StateType
@@ -48,30 +48,8 @@ class PromptDeploymentNode(BasePromptDeploymentNode[StateType]):
48
48
  code=WorkflowErrorCode.INTERNAL_ERROR,
49
49
  )
50
50
 
51
- string_outputs = []
52
- json_output = None
53
-
54
- for output in outputs:
55
- if output.value is None:
56
- continue
57
-
58
- if output.type == "STRING":
59
- string_outputs.append(output.value)
60
- try:
61
- json_output = json.loads(output.value)
62
- except (json.JSONDecodeError, TypeError):
63
- pass
64
- elif output.type == "JSON":
65
- string_outputs.append(json.dumps(output.value, indent=4))
66
- elif output.type == "FUNCTION_CALL":
67
- string_outputs.append(output.value.model_dump_json(indent=4))
68
- elif output.type == "THINKING":
69
- continue
70
- else:
71
- string_outputs.append(output.value.message)
72
-
73
- value = "\n".join(string_outputs)
74
- yield BaseOutput(name="text", value=value)
51
+ text_output, json_output = process_additional_prompt_outputs(outputs)
52
+ yield BaseOutput(name="text", value=text_output)
75
53
 
76
54
  if json_output:
77
55
  yield BaseOutput(name="json", value=json_output)
@@ -232,7 +232,7 @@ class SubworkflowDeploymentNode(BaseNode[StateType], Generic[StateType]):
232
232
  )
233
233
 
234
234
  resolved_workflow = self._context.resolve_workflow_deployment(
235
- deployment_name=deployment_name, release_tag=self.release_tag
235
+ deployment_name=deployment_name, release_tag=self.release_tag, state=self.state
236
236
  )
237
237
  if resolved_workflow:
238
238
  yield from self._run_resolved_workflow(resolved_workflow)
@@ -15,7 +15,12 @@ from vellum.workflows.nodes import BaseNode
15
15
  from vellum.workflows.nodes.bases.base_adornment_node import BaseAdornmentNode
16
16
  from vellum.workflows.ports.port import Port
17
17
  from vellum.workflows.state.base import BaseState
18
- from vellum.workflows.types.code_execution_node_wrappers import DictWrapper, ListWrapper, StringValueWrapper
18
+ from vellum.workflows.types.code_execution_node_wrappers import (
19
+ DictWrapper,
20
+ ListWrapper,
21
+ StringValueWrapper,
22
+ clean_for_dict_wrapper,
23
+ )
19
24
  from vellum.workflows.types.core import Json
20
25
  from vellum.workflows.types.generics import NodeType
21
26
 
@@ -261,3 +266,23 @@ def cast_to_output_type(result: Any, output_type: Any) -> Any:
261
266
  code=WorkflowErrorCode.INVALID_OUTPUTS,
262
267
  message=f"Expected an output of type '{output_type_name}', but received '{result_type_name}'",
263
268
  )
269
+
270
+
271
+ def wrap_inputs_for_backward_compatibility(inputs: Dict[str, Any]) -> Dict[str, Any]:
272
+ """Wrap inputs with backward-compatible wrapper classes for legacy .value and .type support."""
273
+
274
+ def _wrap_single_value(value: Any) -> Any:
275
+ if isinstance(value, list):
276
+ return ListWrapper(
277
+ [
278
+ (
279
+ item.model_dump()
280
+ if isinstance(item, BaseModel)
281
+ else clean_for_dict_wrapper(item) if isinstance(item, (dict, list, str)) else item
282
+ )
283
+ for item in value
284
+ ]
285
+ )
286
+ return clean_for_dict_wrapper(value)
287
+
288
+ return {name: _wrap_single_value(value) for name, value in inputs.items()}
@@ -40,6 +40,9 @@ class NodePorts(metaclass=_NodePortsMeta):
40
40
 
41
41
  invoked_ports: Set[Port] = set()
42
42
  all_ports = [port for port in self.__class__]
43
+ if not all_ports:
44
+ return set()
45
+
43
46
  enforce_single_invoked_conditional_port = validate_ports(all_ports)
44
47
 
45
48
  for port in all_ports:
@@ -9,6 +9,7 @@ from vellum.workflows.edges.edge import Edge
9
9
  from vellum.workflows.errors.types import WorkflowErrorCode
10
10
  from vellum.workflows.exceptions import NodeException
11
11
  from vellum.workflows.graph import Graph, GraphTarget
12
+ from vellum.workflows.graph.graph import NoPortsNode
12
13
  from vellum.workflows.state.base import BaseState
13
14
  from vellum.workflows.types.core import ConditionType
14
15
 
@@ -66,6 +67,12 @@ class Port:
66
67
  if isinstance(other, Port):
67
68
  return Graph.from_port(self) >> Graph.from_port(other)
68
69
 
70
+ if isinstance(other, NoPortsNode):
71
+ raise ValueError(
72
+ f"Cannot create edge to {other.node_class.__name__} because it has no ports defined. "
73
+ f"Nodes with empty Ports classes cannot be connected to other nodes."
74
+ )
75
+
69
76
  edge = Edge(from_port=self, to_node=other)
70
77
  if edge not in self._edges:
71
78
  self._edges.append(edge)
@@ -4,15 +4,17 @@ from uuid import uuid4
4
4
  from typing import TYPE_CHECKING, Dict, List, Optional, Type
5
5
 
6
6
  from vellum import Vellum
7
- from vellum.workflows.context import ExecutionContext, get_execution_context
7
+ from vellum.workflows.context import ExecutionContext, get_execution_context, set_execution_context
8
8
  from vellum.workflows.events.types import ExternalParentContext
9
9
  from vellum.workflows.nodes.mocks import MockNodeExecution, MockNodeExecutionArg
10
10
  from vellum.workflows.outputs.base import BaseOutputs
11
11
  from vellum.workflows.references.constant import ConstantValueReference
12
+ from vellum.workflows.utils.uuids import generate_workflow_deployment_prefix
12
13
  from vellum.workflows.vellum_client import create_vellum_client
13
14
 
14
15
  if TYPE_CHECKING:
15
16
  from vellum.workflows.events.workflow import WorkflowEvent
17
+ from vellum.workflows.state.base import BaseState
16
18
  from vellum.workflows.workflows.base import BaseWorkflow
17
19
 
18
20
 
@@ -23,11 +25,13 @@ class WorkflowContext:
23
25
  vellum_client: Optional[Vellum] = None,
24
26
  execution_context: Optional[ExecutionContext] = None,
25
27
  generated_files: Optional[dict[str, str]] = None,
28
+ namespace: Optional[str] = None,
26
29
  ):
27
30
  self._vellum_client = vellum_client
28
31
  self._event_queue: Optional[Queue["WorkflowEvent"]] = None
29
32
  self._node_output_mocks_map: Dict[Type[BaseOutputs], List[MockNodeExecution]] = {}
30
33
  self._execution_context = get_execution_context()
34
+ self._namespace = namespace
31
35
 
32
36
  if execution_context is not None:
33
37
  self._execution_context.trace_id = execution_context.trace_id
@@ -36,6 +40,8 @@ class WorkflowContext:
36
40
 
37
41
  if self._execution_context.parent_context is None:
38
42
  self._execution_context.parent_context = ExternalParentContext(span_id=uuid4())
43
+ # Propagate the updated context back to the global execution context
44
+ set_execution_context(self._execution_context)
39
45
 
40
46
  self._generated_files = generated_files
41
47
 
@@ -54,6 +60,10 @@ class WorkflowContext:
54
60
  def generated_files(self) -> Optional[dict[str, str]]:
55
61
  return self._generated_files
56
62
 
63
+ @cached_property
64
+ def namespace(self) -> Optional[str]:
65
+ return self._namespace
66
+
57
67
  @cached_property
58
68
  def node_output_mocks_map(self) -> Dict[Type[BaseOutputs], List[MockNodeExecution]]:
59
69
  return self._node_output_mocks_map
@@ -132,19 +142,40 @@ class WorkflowContext:
132
142
  def _get_all_node_output_mocks(self) -> List[MockNodeExecution]:
133
143
  return [mock for mocks in self._node_output_mocks_map.values() for mock in mocks]
134
144
 
135
- def resolve_workflow_deployment(self, deployment_name: str, release_tag: str) -> Optional["BaseWorkflow"]:
145
+ def resolve_workflow_deployment(
146
+ self, deployment_name: str, release_tag: str, state: "BaseState"
147
+ ) -> Optional["BaseWorkflow"]:
136
148
  """
137
149
  Resolve a workflow deployment by name and release tag.
138
150
 
139
151
  Args:
140
152
  deployment_name: The name of the workflow deployment
141
153
  release_tag: The release tag to resolve
154
+ state: The base state to pass to the workflow
142
155
 
143
156
  Returns:
144
157
  BaseWorkflow instance if found, None otherwise
145
158
  """
146
- return None
159
+ if not self.generated_files or not self.namespace:
160
+ return None
161
+
162
+ expected_prefix = generate_workflow_deployment_prefix(deployment_name, release_tag)
163
+
164
+ workflow_file_key = f"{expected_prefix}/workflow.py"
165
+ if workflow_file_key not in self.generated_files:
166
+ return None
167
+
168
+ try:
169
+ from vellum.workflows.workflows.base import BaseWorkflow
170
+
171
+ WorkflowClass = BaseWorkflow.load_from_module(f"{self.namespace}.{expected_prefix}")
172
+ workflow_instance = WorkflowClass(context=WorkflowContext.create_from(self), parent_state=state)
173
+ return workflow_instance
174
+ except Exception:
175
+ return None
147
176
 
148
177
  @classmethod
149
178
  def create_from(cls, context):
150
- return cls(vellum_client=context.vellum_client, generated_files=context.generated_files)
179
+ return cls(
180
+ vellum_client=context.vellum_client, generated_files=context.generated_files, namespace=context.namespace
181
+ )
@@ -121,6 +121,7 @@ class ComposioToolDefinition(UniversalBaseModel):
121
121
  class MCPServer(UniversalBaseModel):
122
122
  type: Literal["MCP_SERVER"] = "MCP_SERVER"
123
123
  name: str
124
+ description: str = "" # We don't use this field, its for compatibility with UI
124
125
  url: str
125
126
  authorization_type: Optional[AuthorizationType] = None
126
127
  bearer_token_value: Optional[Union[str, EnvironmentVariableReference]] = None
@@ -89,6 +89,10 @@ def compile_annotation(annotation: Optional[Any], defs: dict[str, Any]) -> dict:
89
89
  # Mypy is incorrect here, the `annotation` attribute is defined on `FieldInfo`
90
90
  field_annotation = field.annotation # type: ignore[attr-defined]
91
91
  properties[field_name] = compile_annotation(field_annotation, defs)
92
+
93
+ if hasattr(field, "description") and field.description is not None:
94
+ properties[field_name]["description"] = field.description # type: ignore[attr-defined]
95
+
92
96
  if field.default is PydanticUndefined:
93
97
  required.append(field_name)
94
98
  else:
@@ -4,7 +4,7 @@ from enum import Enum
4
4
  from unittest.mock import Mock
5
5
  from typing import Annotated, Dict, List, Literal, Optional, Tuple, Union
6
6
 
7
- from pydantic import BaseModel
7
+ from pydantic import BaseModel, Field
8
8
 
9
9
  from vellum.client.types.function_definition import FunctionDefinition
10
10
  from vellum.client.types.string_vellum_value import StringVellumValue
@@ -206,7 +206,7 @@ def test_compile_function_definition__dataclasses():
206
206
  def test_compile_function_definition__pydantic():
207
207
  # GIVEN a function with a pydantic model
208
208
  class MyPydanticModel(BaseModel):
209
- a: int
209
+ a: int = Field(description="The first number")
210
210
  b: str
211
211
 
212
212
  def my_function(c: MyPydanticModel):
@@ -225,7 +225,10 @@ def test_compile_function_definition__pydantic():
225
225
  "$defs": {
226
226
  "MyPydanticModel": {
227
227
  "type": "object",
228
- "properties": {"a": {"type": "integer"}, "b": {"type": "string"}},
228
+ "properties": {
229
+ "a": {"type": "integer", "description": "The first number"},
230
+ "b": {"type": "string"},
231
+ },
229
232
  "required": ["a", "b"],
230
233
  }
231
234
  },
@@ -2,6 +2,21 @@ import hashlib
2
2
  from uuid import UUID
3
3
 
4
4
 
5
+ def generate_workflow_deployment_prefix(deployment_name: str, release_tag: str) -> str:
6
+ """
7
+ Generate a workflow deployment prefix from deployment name and release tag.
8
+
9
+ Args:
10
+ deployment_name: The name of the workflow deployment
11
+ release_tag: The release tag to resolve
12
+
13
+ Returns:
14
+ The generated prefix in format vellum_workflow_deployment_{hash}
15
+ """
16
+ expected_hash = str(uuid4_from_hash(f"{deployment_name}|{release_tag}")).replace("-", "_")
17
+ return f"vellum_workflow_deployment_{expected_hash}"
18
+
19
+
5
20
  def uuid4_from_hash(input_str: str) -> UUID:
6
21
  # Create a SHA-256 hash of the input string
7
22
  hash_bytes = hashlib.sha256(input_str.encode()).digest()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: vellum-ai
3
- Version: 1.2.2
3
+ Version: 1.2.4
4
4
  Summary:
5
5
  License: MIT
6
6
  Requires-Python: >=3.9,<4.0