vellum-ai 0.14.73__py3-none-any.whl → 0.14.75__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 (33) hide show
  1. vellum/client/core/client_wrapper.py +1 -1
  2. vellum/client/core/serialization.py +0 -1
  3. vellum/client/reference.md +9 -1
  4. vellum/client/resources/prompts/client.py +16 -4
  5. vellum/client/types/secret_type_enum.py +3 -1
  6. vellum/workflows/nodes/bases/base.py +0 -15
  7. vellum/workflows/nodes/core/map_node/tests/test_node.py +54 -0
  8. vellum/workflows/nodes/displayable/bases/inline_prompt_node/node.py +2 -2
  9. vellum/workflows/nodes/displayable/bases/utils.py +2 -0
  10. vellum/workflows/nodes/experimental/tool_calling_node/node.py +5 -1
  11. vellum/workflows/nodes/experimental/tool_calling_node/tests/test_node.py +13 -0
  12. vellum/workflows/nodes/experimental/tool_calling_node/utils.py +14 -4
  13. vellum/workflows/outputs/base.py +26 -2
  14. vellum/workflows/state/encoder.py +2 -0
  15. vellum/workflows/types/definition.py +1 -1
  16. vellum/workflows/types/generics.py +5 -0
  17. vellum/workflows/utils/functions.py +3 -3
  18. vellum/workflows/utils/tests/test_functions.py +7 -7
  19. {vellum_ai-0.14.73.dist-info → vellum_ai-0.14.75.dist-info}/METADATA +1 -1
  20. {vellum_ai-0.14.73.dist-info → vellum_ai-0.14.75.dist-info}/RECORD +33 -32
  21. vellum_ee/workflows/display/nodes/vellum/inline_prompt_node.py +36 -7
  22. vellum_ee/workflows/display/nodes/vellum/tests/test_prompt_node.py +102 -0
  23. vellum_ee/workflows/display/tests/workflow_serialization/generic_nodes/test_attributes_serialization.py +90 -0
  24. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_inline_prompt_node_serialization.py +117 -0
  25. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_tool_calling_node_inline_workflow_serialization.py +7 -0
  26. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_tool_calling_node_serialization.py +7 -0
  27. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_tool_calling_node_workflow_deployment_serialization.py +62 -0
  28. vellum_ee/workflows/display/utils/expressions.py +33 -2
  29. vellum_ee/workflows/display/workflows/base_workflow_display.py +20 -6
  30. vellum_ee/workflows/tests/test_display_meta.py +41 -0
  31. {vellum_ai-0.14.73.dist-info → vellum_ai-0.14.75.dist-info}/LICENSE +0 -0
  32. {vellum_ai-0.14.73.dist-info → vellum_ai-0.14.75.dist-info}/WHEEL +0 -0
  33. {vellum_ai-0.14.73.dist-info → vellum_ai-0.14.75.dist-info}/entry_points.txt +0 -0
@@ -240,6 +240,27 @@ def test_serialize_workflow():
240
240
  },
241
241
  },
242
242
  },
243
+ {
244
+ "id": "2b98319f-f43d-42d9-a8b0-b148d5de0a2c",
245
+ "name": "parameters",
246
+ "value": {
247
+ "type": "CONSTANT_VALUE",
248
+ "value": {
249
+ "type": "JSON",
250
+ "value": {
251
+ "stop": [],
252
+ "temperature": 0.0,
253
+ "max_tokens": 4096,
254
+ "top_p": 1.0,
255
+ "top_k": 0,
256
+ "frequency_penalty": 0.0,
257
+ "presence_penalty": 0.0,
258
+ "logit_bias": None,
259
+ "custom_parameters": None,
260
+ },
261
+ },
262
+ },
263
+ },
243
264
  ],
244
265
  },
245
266
  prompt_node,
@@ -437,3 +458,99 @@ def test_serialize_workflow_with_descriptor_blocks():
437
458
  },
438
459
  }
439
460
  ]
461
+
462
+
463
+ def test_serialize_workflow_with_nested_descriptor_blocks():
464
+ """Test that serialization handles BaseDescriptor instances nested in ChatMessageBlock.blocks."""
465
+
466
+ class TestInputs(BaseInputs):
467
+ noun: str
468
+
469
+ class UpstreamNode(BaseNode):
470
+ class Outputs(BaseNode.Outputs):
471
+ results: list
472
+
473
+ def run(self) -> Outputs:
474
+ return self.Outputs(results=["test"])
475
+
476
+ chat_block = ChatMessagePromptBlock(chat_role="SYSTEM", blocks=[JinjaPromptBlock(template="Hello")])
477
+
478
+ class TestInlinePromptNodeWithNestedDescriptorBlocks(InlinePromptNode):
479
+ ml_model = "gpt-4o"
480
+ blocks = [chat_block]
481
+ prompt_inputs = {"noun": TestInputs.noun}
482
+
483
+ object.__setattr__(chat_block, "blocks", [UpstreamNode.Outputs.results[0]])
484
+
485
+ class TestWorkflow(BaseWorkflow[TestInputs, BaseState]):
486
+ graph = UpstreamNode >> TestInlinePromptNodeWithNestedDescriptorBlocks
487
+
488
+ workflow_display = get_workflow_display(workflow_class=TestWorkflow)
489
+ serialized: dict = workflow_display.serialize()
490
+
491
+ prompt_nodes = [node for node in serialized["workflow_raw_data"]["nodes"] if node["type"] == "PROMPT"]
492
+ prompt_node = prompt_nodes[0]
493
+
494
+ blocks = prompt_node["data"]["exec_config"]["prompt_template_block_data"]["blocks"]
495
+ descriptor_blocks = [block for block in blocks if not isinstance(block, dict) or not block.get("block_type")]
496
+ assert len(descriptor_blocks) == 0, "BaseDescriptor blocks should not appear in serialized blocks"
497
+
498
+ blocks_attr = next((attr for attr in prompt_node["attributes"] if attr["name"] == "blocks"), None)
499
+ assert blocks_attr is not None, "blocks attribute should be present when blocks contain nested BaseDescriptor"
500
+ assert blocks_attr["value"]["type"] == "ARRAY_REFERENCE", "blocks attribute should be serialized as ARRAY_REFERENCE"
501
+ assert blocks_attr["value"]["items"] == [
502
+ {
503
+ "entries": [
504
+ {
505
+ "id": "24a203be-3cba-4b20-bc84-9993a476c120",
506
+ "key": "block_type",
507
+ "value": {"type": "CONSTANT_VALUE", "value": {"type": "STRING", "value": "CHAT_MESSAGE"}},
508
+ },
509
+ {
510
+ "id": "c06269e6-f74c-4860-8fa5-22dcbdc89399",
511
+ "key": "state",
512
+ "value": {"type": "CONSTANT_VALUE", "value": {"type": "JSON", "value": None}},
513
+ },
514
+ {
515
+ "id": "dd9c0d43-b931-4dc8-8b3a-a7507ddff0c1",
516
+ "key": "cache_config",
517
+ "value": {"type": "CONSTANT_VALUE", "value": {"type": "JSON", "value": None}},
518
+ },
519
+ {
520
+ "id": "bef22f2b-0b6e-4910-88cc-6df736d2e20e",
521
+ "key": "chat_role",
522
+ "value": {"type": "CONSTANT_VALUE", "value": {"type": "STRING", "value": "SYSTEM"}},
523
+ },
524
+ {
525
+ "id": "c0beec30-f85e-4a78-a3fb-baee54a692f8",
526
+ "key": "chat_source",
527
+ "value": {"type": "CONSTANT_VALUE", "value": {"type": "JSON", "value": None}},
528
+ },
529
+ {
530
+ "id": "f601f4f2-62fe-4697-9fe0-99ca8aa64500",
531
+ "key": "chat_message_unterminated",
532
+ "value": {"type": "CONSTANT_VALUE", "value": {"type": "JSON", "value": None}},
533
+ },
534
+ {
535
+ "id": "ad550008-64e3-44a3-a32a-84ec226db31c",
536
+ "key": "blocks",
537
+ "value": {
538
+ "items": [
539
+ {
540
+ "lhs": {
541
+ "node_id": "9fe5d3a3-7d26-4692-aa2d-e67c673b0c2b",
542
+ "node_output_id": "92f9a1b7-d33b-4f00-b4c2-e6f58150e166",
543
+ "type": "NODE_OUTPUT",
544
+ },
545
+ "operator": "accessField",
546
+ "rhs": {"type": "CONSTANT_VALUE", "value": {"type": "NUMBER", "value": 0.0}},
547
+ "type": "BINARY_EXPRESSION",
548
+ }
549
+ ],
550
+ "type": "ARRAY_REFERENCE",
551
+ },
552
+ },
553
+ ],
554
+ "type": "DICTIONARY_REFERENCE",
555
+ }
556
+ ]
@@ -131,6 +131,8 @@ def test_serialize_workflow():
131
131
  "value": [
132
132
  {
133
133
  "type": "INLINE_WORKFLOW",
134
+ "name": "BasicInlineSubworkflowWorkflow",
135
+ "description": "\n A workflow that gets the weather for a given city and date.\n ", # noqa: E501
134
136
  "exec_config": {
135
137
  "workflow_raw_data": {
136
138
  "nodes": [
@@ -410,6 +412,11 @@ def test_serialize_workflow():
410
412
  "name": "function_configs",
411
413
  "value": {"type": "CONSTANT_VALUE", "value": {"type": "JSON", "value": None}},
412
414
  },
415
+ {
416
+ "id": "1668419e-a193-43a5-8a97-3394e89bf278",
417
+ "name": "max_prompt_iterations",
418
+ "value": {"type": "CONSTANT_VALUE", "value": {"type": "NUMBER", "value": 5.0}},
419
+ },
413
420
  ],
414
421
  "outputs": [
415
422
  {"id": "e62bc785-a914-4066-b79e-8c89a5d0ec6c", "name": "text", "type": "STRING", "value": None},
@@ -131,6 +131,8 @@ def test_serialize_workflow():
131
131
  "value": [
132
132
  {
133
133
  "type": "CODE_EXECUTION",
134
+ "name": "get_current_weather",
135
+ "description": "\n Get the current weather in a given location.\n ",
134
136
  "definition": {
135
137
  "state": None,
136
138
  "cache_config": None,
@@ -183,6 +185,11 @@ def test_serialize_workflow():
183
185
  ],
184
186
  },
185
187
  },
188
+ {
189
+ "id": "1668419e-a193-43a5-8a97-3394e89bf278",
190
+ "name": "max_prompt_iterations",
191
+ "value": {"type": "CONSTANT_VALUE", "value": {"type": "NUMBER", "value": 5.0}},
192
+ },
186
193
  ],
187
194
  "outputs": [
188
195
  {"id": "e62bc785-a914-4066-b79e-8c89a5d0ec6c", "name": "text", "type": "STRING", "value": None},
@@ -0,0 +1,62 @@
1
+ from deepdiff import DeepDiff
2
+
3
+ from vellum_ee.workflows.display.workflows.get_vellum_workflow_display_class import get_workflow_display
4
+
5
+ from tests.workflows.basic_tool_calling_node_workflow_deployment.workflow import (
6
+ BasicToolCallingNodeWorkflowDeploymentWorkflow,
7
+ )
8
+
9
+
10
+ def test_serialize_workflow():
11
+ # GIVEN a Workflow that uses a generic node
12
+ # WHEN we serialize it
13
+ workflow_display = get_workflow_display(workflow_class=BasicToolCallingNodeWorkflowDeploymentWorkflow)
14
+
15
+ serialized_workflow: dict = workflow_display.serialize()
16
+ # THEN we should get a serialized representation of the Workflow
17
+ assert serialized_workflow.keys() == {
18
+ "workflow_raw_data",
19
+ "input_variables",
20
+ "state_variables",
21
+ "output_variables",
22
+ }
23
+
24
+ # AND its input variables should be what we expect
25
+ input_variables = serialized_workflow["input_variables"]
26
+ assert len(input_variables) == 1
27
+
28
+ # AND its output variables should be what we expect
29
+ output_variables = serialized_workflow["output_variables"]
30
+ assert len(output_variables) == 2
31
+ assert not DeepDiff(
32
+ [
33
+ {"id": "3626cfa6-76f8-465e-b156-c809e3a2cee9", "key": "text", "type": "STRING"},
34
+ {"id": "cb82117a-d649-4c3e-9342-c552028fa2ad", "key": "chat_history", "type": "CHAT_HISTORY"},
35
+ ],
36
+ output_variables,
37
+ ignore_order=True,
38
+ )
39
+
40
+ # AND its raw data should be what we expect
41
+ workflow_raw_data = serialized_workflow["workflow_raw_data"]
42
+ tool_calling_node = workflow_raw_data["nodes"][1]
43
+ function_attributes = next(attr for attr in tool_calling_node["attributes"] if attr["name"] == "functions")
44
+ assert function_attributes == {
45
+ "id": "73a94e3c-1935-4308-a68a-ecd5441804b7",
46
+ "name": "functions",
47
+ "value": {
48
+ "type": "CONSTANT_VALUE",
49
+ "value": {
50
+ "type": "JSON",
51
+ "value": [
52
+ {
53
+ "type": "WORKFLOW_DEPLOYMENT",
54
+ "name": "deployment_1",
55
+ "description": "Workflow deployment for deployment_1",
56
+ "deployment": "deployment_1",
57
+ "release_tag": "LATEST",
58
+ }
59
+ ],
60
+ },
61
+ },
62
+ }
@@ -1,5 +1,8 @@
1
+ from dataclasses import asdict, is_dataclass
1
2
  from typing import TYPE_CHECKING, Any, Dict, List, cast
2
3
 
4
+ from pydantic import BaseModel
5
+
3
6
  from vellum.client.types.logical_operator import LogicalOperator
4
7
  from vellum.workflows.descriptors.base import BaseDescriptor
5
8
  from vellum.workflows.expressions.accessor import AccessorExpression
@@ -39,6 +42,7 @@ from vellum.workflows.references.state_value import StateValueReference
39
42
  from vellum.workflows.references.vellum_secret import VellumSecretReference
40
43
  from vellum.workflows.references.workflow_input import WorkflowInputReference
41
44
  from vellum.workflows.types.core import JsonArray, JsonObject
45
+ from vellum.workflows.types.definition import DeploymentDefinition
42
46
  from vellum.workflows.types.generics import is_workflow_class
43
47
  from vellum.workflows.utils.uuids import uuid4_from_hash
44
48
  from vellum_ee.workflows.display.utils.exceptions import UnsupportedSerializationException
@@ -290,6 +294,10 @@ def serialize_value(display_context: "WorkflowDisplayContext", value: Any) -> Js
290
294
  "items": cast(JsonArray, serialized_items), # list[JsonObject] -> JsonArray
291
295
  }
292
296
 
297
+ if is_dataclass(value) and not isinstance(value, type):
298
+ dict_value = asdict(value)
299
+ return serialize_value(display_context, dict_value)
300
+
293
301
  if isinstance(value, dict):
294
302
  serialized_entries: List[Dict[str, Any]] = [
295
303
  {
@@ -321,18 +329,41 @@ def serialize_value(display_context: "WorkflowDisplayContext", value: Any) -> Js
321
329
  from vellum_ee.workflows.display.workflows.get_vellum_workflow_display_class import get_workflow_display
322
330
 
323
331
  workflow_display = get_workflow_display(workflow_class=value)
324
- value = workflow_display.serialize()
332
+ serialized_value: dict = workflow_display.serialize()
333
+ name = serialized_value["workflow_raw_data"]["definition"]["name"]
334
+ description = value.__doc__ or ""
325
335
  return {
326
336
  "type": "CONSTANT_VALUE",
327
337
  "value": {
328
338
  "type": "JSON",
329
339
  "value": {
330
340
  "type": "INLINE_WORKFLOW",
331
- "exec_config": value,
341
+ "name": name,
342
+ "description": description,
343
+ "exec_config": serialized_value,
344
+ },
345
+ },
346
+ }
347
+
348
+ if isinstance(value, DeploymentDefinition):
349
+ return {
350
+ "type": "CONSTANT_VALUE",
351
+ "value": {
352
+ "type": "JSON",
353
+ "value": {
354
+ "type": "WORKFLOW_DEPLOYMENT",
355
+ "name": value.deployment,
356
+ "description": f"Workflow deployment for {value.deployment}",
357
+ "deployment": value.deployment,
358
+ "release_tag": value.release_tag,
332
359
  },
333
360
  },
334
361
  }
335
362
 
363
+ if isinstance(value, BaseModel):
364
+ dict_value = value.model_dump()
365
+ return serialize_value(display_context, dict_value)
366
+
336
367
  if not isinstance(value, BaseDescriptor):
337
368
  vellum_value = primitive_to_vellum_value(value)
338
369
  return {
@@ -690,8 +690,7 @@ class BaseWorkflowDisplay(Generic[WorkflowType]):
690
690
  try:
691
691
  display_module = importlib.import_module(full_workflow_display_module_path)
692
692
  except ModuleNotFoundError:
693
- logger.exception("Failed to import workflow display module: %s", full_workflow_display_module_path)
694
- return None
693
+ return BaseWorkflowDisplay._gather_event_display_context_from_workflow_crawling(module_path, workflow_class)
695
694
 
696
695
  WorkflowDisplayClass: Optional[Type[BaseWorkflowDisplay]] = None
697
696
  for name, definition in display_module.__dict__.items():
@@ -708,11 +707,26 @@ class BaseWorkflowDisplay(Generic[WorkflowType]):
708
707
  WorkflowDisplayClass = definition
709
708
  break
710
709
 
711
- if not WorkflowDisplayClass:
712
- logger.exception("No workflow display class found in module: %s", full_workflow_display_module_path)
713
- return None
710
+ if WorkflowDisplayClass:
711
+ return WorkflowDisplayClass().get_event_display_context()
712
+
713
+ return BaseWorkflowDisplay._gather_event_display_context_from_workflow_crawling(module_path, workflow_class)
714
+
715
+ @staticmethod
716
+ def _gather_event_display_context_from_workflow_crawling(
717
+ module_path: str,
718
+ workflow_class: Optional[Type[BaseWorkflow]] = None,
719
+ ) -> Union[WorkflowEventDisplayContext, None]:
720
+ try:
721
+ if workflow_class is None:
722
+ workflow_class = BaseWorkflow.load_from_module(module_path)
714
723
 
715
- return WorkflowDisplayClass().get_event_display_context()
724
+ workflow_display = get_workflow_display(workflow_class=workflow_class)
725
+ return workflow_display.get_event_display_context()
726
+
727
+ except ModuleNotFoundError:
728
+ logger.exception("Failed to load workflow from module %s", module_path)
729
+ return None
716
730
 
717
731
  def get_event_display_context(self):
718
732
  display_context = self.display_context
@@ -4,6 +4,7 @@ import sys
4
4
  from uuid import uuid4
5
5
 
6
6
  from vellum.workflows import BaseWorkflow
7
+ from vellum.workflows.events.workflow import WorkflowEventDisplayContext
7
8
  from vellum_ee.workflows.display.workflows import BaseWorkflowDisplay
8
9
  from vellum_ee.workflows.server.virtual_file_loader import VirtualFileFinder
9
10
 
@@ -102,3 +103,43 @@ class MyCustomWorkflowDisplay(BaseWorkflowDisplay[MyCustomWorkflow]):
102
103
  assert display_meta.workflow_outputs == {
103
104
  "answer": workflow_output_id,
104
105
  }
106
+
107
+
108
+ def test_gather_event_display_context__workflow_crawling_without_display_module():
109
+ # GIVEN a workflow module without a display module
110
+ files = {
111
+ "__init__.py": "",
112
+ "workflow.py": """\
113
+ from vellum.workflows import BaseWorkflow
114
+ from vellum.workflows.nodes import BaseNode
115
+
116
+ class TestNode(BaseNode):
117
+ class Outputs(BaseNode.Outputs):
118
+ result: str
119
+
120
+ class TestWorkflow(BaseWorkflow):
121
+ graph = TestNode
122
+
123
+ class Outputs(BaseWorkflow.Outputs):
124
+ final_result = TestNode.Outputs.result
125
+ """,
126
+ }
127
+
128
+ namespace = str(uuid4())
129
+
130
+ # AND the virtual file loader is registered
131
+ sys.meta_path.append(VirtualFileFinder(files, namespace))
132
+
133
+ # WHEN the workflow display context is gathered
134
+ display_meta = BaseWorkflowDisplay.gather_event_display_context(namespace)
135
+
136
+ # THEN the workflow display context should be successfully created via workflow crawling
137
+ assert display_meta is not None
138
+ assert isinstance(display_meta, WorkflowEventDisplayContext)
139
+
140
+ # AND the node displays should be populated with the correct node structure
141
+ assert len(display_meta.node_displays) == 1
142
+ node_display = list(display_meta.node_displays.values())[0]
143
+ assert "result" in node_display.output_display
144
+
145
+ assert "final_result" in display_meta.workflow_outputs