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.
- vellum/__init__.py +40 -0
- vellum/client/core/client_wrapper.py +2 -2
- vellum/client/core/pydantic_utilities.py +3 -2
- vellum/client/reference.md +16 -0
- vellum/client/resources/workflow_executions/client.py +28 -4
- vellum/client/resources/workflow_executions/raw_client.py +32 -2
- vellum/client/types/__init__.py +40 -0
- vellum/client/types/audio_input_request.py +30 -0
- vellum/client/types/delimiter_chunker_config.py +20 -0
- vellum/client/types/delimiter_chunker_config_request.py +20 -0
- vellum/client/types/delimiter_chunking.py +21 -0
- vellum/client/types/delimiter_chunking_request.py +21 -0
- vellum/client/types/document_index_chunking.py +4 -1
- vellum/client/types/document_index_chunking_request.py +2 -1
- vellum/client/types/document_input_request.py +30 -0
- vellum/client/types/execution_audio_vellum_value.py +31 -0
- vellum/client/types/execution_document_vellum_value.py +31 -0
- vellum/client/types/execution_image_vellum_value.py +31 -0
- vellum/client/types/execution_vellum_value.py +8 -0
- vellum/client/types/execution_video_vellum_value.py +31 -0
- vellum/client/types/image_input_request.py +30 -0
- vellum/client/types/logical_operator.py +1 -0
- vellum/client/types/node_input_compiled_audio_value.py +23 -0
- vellum/client/types/node_input_compiled_document_value.py +23 -0
- vellum/client/types/node_input_compiled_image_value.py +23 -0
- vellum/client/types/node_input_compiled_video_value.py +23 -0
- vellum/client/types/node_input_variable_compiled_value.py +8 -0
- vellum/client/types/prompt_deployment_input_request.py +13 -1
- vellum/client/types/prompt_request_audio_input.py +26 -0
- vellum/client/types/prompt_request_document_input.py +26 -0
- vellum/client/types/prompt_request_image_input.py +26 -0
- vellum/client/types/prompt_request_input.py +13 -1
- vellum/client/types/prompt_request_video_input.py +26 -0
- vellum/client/types/video_input_request.py +30 -0
- vellum/types/audio_input_request.py +3 -0
- vellum/types/delimiter_chunker_config.py +3 -0
- vellum/types/delimiter_chunker_config_request.py +3 -0
- vellum/types/delimiter_chunking.py +3 -0
- vellum/types/delimiter_chunking_request.py +3 -0
- vellum/types/document_input_request.py +3 -0
- vellum/types/execution_audio_vellum_value.py +3 -0
- vellum/types/execution_document_vellum_value.py +3 -0
- vellum/types/execution_image_vellum_value.py +3 -0
- vellum/types/execution_video_vellum_value.py +3 -0
- vellum/types/image_input_request.py +3 -0
- vellum/types/node_input_compiled_audio_value.py +3 -0
- vellum/types/node_input_compiled_document_value.py +3 -0
- vellum/types/node_input_compiled_image_value.py +3 -0
- vellum/types/node_input_compiled_video_value.py +3 -0
- vellum/types/prompt_request_audio_input.py +3 -0
- vellum/types/prompt_request_document_input.py +3 -0
- vellum/types/prompt_request_image_input.py +3 -0
- vellum/types/prompt_request_video_input.py +3 -0
- vellum/types/video_input_request.py +3 -0
- vellum/workflows/context.py +27 -9
- vellum/workflows/events/context.py +53 -78
- vellum/workflows/events/node.py +5 -5
- vellum/workflows/events/relational_threads.py +41 -0
- vellum/workflows/events/tests/test_basic_workflow.py +50 -0
- vellum/workflows/events/tests/test_event.py +9 -0
- vellum/workflows/events/types.py +3 -1
- vellum/workflows/events/workflow.py +12 -1
- vellum/workflows/expressions/contains.py +7 -0
- vellum/workflows/expressions/tests/test_contains.py +175 -0
- vellum/workflows/graph/graph.py +52 -8
- vellum/workflows/graph/tests/test_graph.py +17 -0
- vellum/workflows/integrations/mcp_service.py +35 -5
- vellum/workflows/integrations/tests/test_mcp_service.py +120 -0
- vellum/workflows/nodes/core/error_node/node.py +4 -0
- vellum/workflows/nodes/core/map_node/node.py +7 -0
- vellum/workflows/nodes/core/map_node/tests/test_node.py +19 -0
- vellum/workflows/nodes/core/templating_node/node.py +3 -2
- vellum/workflows/nodes/core/templating_node/tests/test_templating_node.py +129 -0
- vellum/workflows/nodes/displayable/bases/inline_prompt_node/node.py +12 -0
- vellum/workflows/nodes/displayable/bases/inline_prompt_node/tests/test_inline_prompt_node.py +41 -0
- vellum/workflows/nodes/displayable/bases/utils.py +38 -1
- vellum/workflows/nodes/displayable/code_execution_node/utils.py +3 -20
- vellum/workflows/nodes/displayable/final_output_node/node.py +4 -0
- vellum/workflows/nodes/displayable/inline_prompt_node/node.py +3 -26
- vellum/workflows/nodes/displayable/prompt_deployment_node/node.py +3 -25
- vellum/workflows/nodes/displayable/subworkflow_deployment_node/node.py +1 -1
- vellum/workflows/nodes/utils.py +26 -1
- vellum/workflows/ports/node_ports.py +3 -0
- vellum/workflows/ports/port.py +7 -0
- vellum/workflows/state/context.py +35 -4
- vellum/workflows/types/definition.py +1 -0
- vellum/workflows/utils/functions.py +4 -0
- vellum/workflows/utils/tests/test_functions.py +6 -3
- vellum/workflows/utils/uuids.py +15 -0
- {vellum_ai-1.2.2.dist-info → vellum_ai-1.2.4.dist-info}/METADATA +1 -1
- {vellum_ai-1.2.2.dist-info → vellum_ai-1.2.4.dist-info}/RECORD +106 -60
- vellum_cli/__init__.py +6 -0
- vellum_cli/config.py +2 -0
- vellum_cli/push.py +3 -0
- vellum_cli/tests/test_pull.py +2 -0
- vellum_cli/tests/test_push.py +39 -0
- vellum_ee/workflows/display/nodes/vellum/error_node.py +1 -5
- vellum_ee/workflows/display/nodes/vellum/final_output_node.py +1 -5
- vellum_ee/workflows/display/nodes/vellum/tests/test_tool_calling_node.py +2 -0
- vellum_ee/workflows/display/tests/workflow_serialization/test_basic_tool_calling_node_mcp_serialization.py +1 -0
- vellum_ee/workflows/display/utils/events.py +24 -0
- vellum_ee/workflows/display/utils/tests/test_events.py +69 -0
- vellum_ee/workflows/tests/test_server.py +95 -0
- {vellum_ai-1.2.2.dist-info → vellum_ai-1.2.4.dist-info}/LICENSE +0 -0
- {vellum_ai-1.2.2.dist-info → vellum_ai-1.2.4.dist-info}/WHEEL +0 -0
- {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":
|
vellum/workflows/nodes/displayable/bases/inline_prompt_node/tests/test_inline_prompt_node.py
CHANGED
@@ -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
|
-
|
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":
|
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
|
-
|
49
|
-
|
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
|
-
|
52
|
-
|
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)
|
vellum/workflows/nodes/utils.py
CHANGED
@@ -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
|
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:
|
vellum/workflows/ports/port.py
CHANGED
@@ -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(
|
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
|
-
|
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(
|
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": {
|
228
|
+
"properties": {
|
229
|
+
"a": {"type": "integer", "description": "The first number"},
|
230
|
+
"b": {"type": "string"},
|
231
|
+
},
|
229
232
|
"required": ["a", "b"],
|
230
233
|
}
|
231
234
|
},
|
vellum/workflows/utils/uuids.py
CHANGED
@@ -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()
|