vellum-ai 1.1.1__py3-none-any.whl → 1.1.3__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 (85) hide show
  1. vellum/__init__.py +16 -0
  2. vellum/client/README.md +55 -0
  3. vellum/client/__init__.py +66 -507
  4. vellum/client/core/client_wrapper.py +2 -2
  5. vellum/client/core/pydantic_utilities.py +10 -3
  6. vellum/client/raw_client.py +844 -0
  7. vellum/client/reference.md +692 -19
  8. vellum/client/resources/ad_hoc/client.py +23 -180
  9. vellum/client/resources/ad_hoc/raw_client.py +276 -0
  10. vellum/client/resources/container_images/client.py +10 -36
  11. vellum/client/resources/deployments/client.py +16 -62
  12. vellum/client/resources/document_indexes/client.py +16 -72
  13. vellum/client/resources/documents/client.py +8 -30
  14. vellum/client/resources/folder_entities/client.py +4 -8
  15. vellum/client/resources/metric_definitions/client.py +4 -14
  16. vellum/client/resources/ml_models/client.py +2 -8
  17. vellum/client/resources/organizations/client.py +2 -6
  18. vellum/client/resources/prompts/client.py +2 -10
  19. vellum/client/resources/sandboxes/client.py +4 -20
  20. vellum/client/resources/test_suite_runs/client.py +4 -18
  21. vellum/client/resources/test_suites/client.py +11 -86
  22. vellum/client/resources/test_suites/raw_client.py +136 -0
  23. vellum/client/resources/workflow_deployments/client.py +20 -78
  24. vellum/client/resources/workflow_executions/client.py +2 -6
  25. vellum/client/resources/workflow_sandboxes/client.py +2 -10
  26. vellum/client/resources/workflows/client.py +7 -6
  27. vellum/client/resources/workflows/raw_client.py +58 -47
  28. vellum/client/resources/workspace_secrets/client.py +4 -20
  29. vellum/client/resources/workspaces/client.py +2 -6
  30. vellum/client/types/__init__.py +16 -0
  31. vellum/client/types/array_chat_message_content_item.py +4 -2
  32. vellum/client/types/array_chat_message_content_item_request.py +4 -2
  33. vellum/client/types/chat_message_content.py +4 -2
  34. vellum/client/types/chat_message_content_request.py +4 -2
  35. vellum/client/types/node_execution_span.py +2 -0
  36. vellum/client/types/prompt_block.py +4 -2
  37. vellum/client/types/vellum_value.py +4 -2
  38. vellum/client/types/vellum_value_request.py +4 -2
  39. vellum/client/types/vellum_variable_type.py +2 -1
  40. vellum/client/types/vellum_video.py +24 -0
  41. vellum/client/types/vellum_video_request.py +24 -0
  42. vellum/client/types/video_chat_message_content.py +25 -0
  43. vellum/client/types/video_chat_message_content_request.py +25 -0
  44. vellum/client/types/video_prompt_block.py +29 -0
  45. vellum/client/types/video_vellum_value.py +25 -0
  46. vellum/client/types/video_vellum_value_request.py +25 -0
  47. vellum/client/types/workflow_execution_span.py +2 -0
  48. vellum/client/types/workflow_execution_usage_calculation_fulfilled_body.py +22 -0
  49. vellum/prompts/blocks/compilation.py +22 -10
  50. vellum/types/vellum_video.py +3 -0
  51. vellum/types/vellum_video_request.py +3 -0
  52. vellum/types/video_chat_message_content.py +3 -0
  53. vellum/types/video_chat_message_content_request.py +3 -0
  54. vellum/types/video_prompt_block.py +3 -0
  55. vellum/types/video_vellum_value.py +3 -0
  56. vellum/types/video_vellum_value_request.py +3 -0
  57. vellum/types/workflow_execution_usage_calculation_fulfilled_body.py +3 -0
  58. vellum/workflows/events/workflow.py +11 -0
  59. vellum/workflows/graph/graph.py +103 -1
  60. vellum/workflows/graph/tests/test_graph.py +99 -0
  61. vellum/workflows/nodes/bases/base.py +9 -1
  62. vellum/workflows/nodes/displayable/bases/utils.py +4 -2
  63. vellum/workflows/nodes/displayable/tool_calling_node/node.py +19 -18
  64. vellum/workflows/nodes/displayable/tool_calling_node/tests/test_node.py +17 -7
  65. vellum/workflows/nodes/displayable/tool_calling_node/tests/test_utils.py +7 -7
  66. vellum/workflows/nodes/displayable/tool_calling_node/utils.py +47 -80
  67. vellum/workflows/references/environment_variable.py +10 -0
  68. vellum/workflows/runner/runner.py +18 -2
  69. vellum/workflows/state/context.py +101 -12
  70. vellum/workflows/types/definition.py +11 -1
  71. vellum/workflows/types/tests/test_definition.py +19 -0
  72. vellum/workflows/utils/vellum_variables.py +9 -5
  73. vellum/workflows/workflows/base.py +12 -5
  74. {vellum_ai-1.1.1.dist-info → vellum_ai-1.1.3.dist-info}/METADATA +1 -1
  75. {vellum_ai-1.1.1.dist-info → vellum_ai-1.1.3.dist-info}/RECORD +85 -69
  76. vellum_ee/workflows/display/nodes/vellum/code_execution_node.py +1 -1
  77. vellum_ee/workflows/display/nodes/vellum/tests/test_code_execution_node.py +55 -1
  78. vellum_ee/workflows/display/nodes/vellum/tests/test_tool_calling_node.py +15 -52
  79. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_tool_calling_node_mcp_serialization.py +15 -49
  80. vellum_ee/workflows/display/types.py +14 -1
  81. vellum_ee/workflows/display/utils/expressions.py +13 -4
  82. vellum_ee/workflows/display/workflows/base_workflow_display.py +6 -19
  83. {vellum_ai-1.1.1.dist-info → vellum_ai-1.1.3.dist-info}/LICENSE +0 -0
  84. {vellum_ai-1.1.1.dist-info → vellum_ai-1.1.3.dist-info}/WHEEL +0 -0
  85. {vellum_ai-1.1.1.dist-info → vellum_ai-1.1.3.dist-info}/entry_points.txt +0 -0
@@ -47,8 +47,9 @@ class FunctionCallNodeMixin:
47
47
 
48
48
  def _extract_function_arguments(self) -> dict:
49
49
  """Extract arguments from function call output."""
50
- if self.function_call_output and len(self.function_call_output) > 0:
51
- function_call = self.function_call_output[0]
50
+ current_index = getattr(self, "state").current_prompt_output_index
51
+ if self.function_call_output and len(self.function_call_output) > current_index:
52
+ function_call = self.function_call_output[current_index]
52
53
  if function_call.type == "FUNCTION_CALL" and function_call.value is not None:
53
54
  return function_call.value.arguments or {}
54
55
  return {}
@@ -66,7 +67,7 @@ class FunctionCallNodeMixin:
66
67
  state.current_prompt_output_index += 1
67
68
 
68
69
 
69
- class ToolRouterNode(InlinePromptNode[ToolCallingState]):
70
+ class ToolPromptNode(InlinePromptNode[ToolCallingState]):
70
71
  max_prompt_iterations: Optional[int] = 5
71
72
 
72
73
  class Trigger(InlinePromptNode.Trigger):
@@ -115,13 +116,7 @@ class ToolRouterNode(InlinePromptNode[ToolCallingState]):
115
116
  class RouterNode(BaseNode[ToolCallingState]):
116
117
  """Router node that handles routing to function nodes based on outputs."""
117
118
 
118
- class Trigger(BaseNode.Trigger):
119
- merge_behavior = MergeBehavior.AWAIT_ATTRIBUTES
120
-
121
- def run(self) -> Iterator[BaseOutput]:
122
- # Router node doesn't process outputs or create chat messages
123
- # It just handles the routing logic via its ports
124
- yield from []
119
+ pass
125
120
 
126
121
 
127
122
  class DynamicSubworkflowDeploymentNode(SubworkflowDeploymentNode[ToolCallingState], FunctionCallNodeMixin):
@@ -134,8 +129,8 @@ class DynamicSubworkflowDeploymentNode(SubworkflowDeploymentNode[ToolCallingStat
134
129
  # we do in the `__init__` method. Long term, instead of the function_call_output attribute above, we
135
130
  # want to do:
136
131
  # ```python
137
- # subworkflow_inputs = tool_router_node.Outputs.results[0]['value']['arguments'].if_(
138
- # tool_router_node.Outputs.results[0]['type'].equals('FUNCTION_CALL'),
132
+ # subworkflow_inputs = tool_prompt_node.Outputs.results[0]['value']['arguments'].if_(
133
+ # tool_prompt_node.Outputs.results[0]['type'].equals('FUNCTION_CALL'),
139
134
  # {},
140
135
  # )
141
136
  # ```
@@ -255,10 +250,10 @@ class ElseNode(BaseNode[ToolCallingState]):
255
250
 
256
251
  class Ports(BaseNode.Ports):
257
252
  # Redefined in the create_else_node function, but defined here to resolve mypy errors
258
- loop = Port.on_if(
259
- ToolCallingState.current_prompt_output_index.less_than(1)
260
- | ToolCallingState.current_function_calls_processed.greater_than(0)
253
+ loop_to_router = Port.on_if(
254
+ ToolCallingState.current_prompt_output_index.less_than(ToolPromptNode.Outputs.results.length())
261
255
  )
256
+ loop_to_prompt = Port.on_elif(ToolCallingState.current_function_calls_processed.greater_than(0))
262
257
  end = Port.on_else()
263
258
 
264
259
  def run(self) -> BaseNode.Outputs:
@@ -317,46 +312,22 @@ def hydrate_mcp_tool_definitions(server_def: MCPServer) -> List[MCPToolDefinitio
317
312
  return []
318
313
 
319
314
 
320
- def create_tool_router_node(
315
+ def create_tool_prompt_node(
321
316
  ml_model: str,
322
317
  blocks: List[Union[PromptBlock, Dict[str, Any]]],
323
318
  functions: List[Tool],
324
319
  prompt_inputs: Optional[EntityInputsInterface],
325
320
  parameters: PromptParameters,
326
321
  max_prompt_iterations: Optional[int] = None,
327
- ) -> Type[ToolRouterNode]:
322
+ ) -> Type[ToolPromptNode]:
328
323
  if functions and len(functions) > 0:
329
- # Create dynamic ports and convert functions in a single loop
330
- Ports = type("Ports", (), {})
331
324
  prompt_functions: List[Union[Tool, FunctionDefinition]] = []
332
325
 
333
- # Avoid using lambda to capture function_name
334
- # lambda will capture the function_name by reference,
335
- # and if the function_name is changed, the port_condition will also change.
336
- def create_port_condition(fn_name):
337
- return Port.on_if(
338
- LazyReference(
339
- lambda: (
340
- ToolCallingState.current_prompt_output_index.less_than(node.Outputs.results.length())
341
- & node.Outputs.results[ToolCallingState.current_prompt_output_index]["type"].equals(
342
- "FUNCTION_CALL"
343
- )
344
- & node.Outputs.results[ToolCallingState.current_prompt_output_index]["value"]["name"].equals(
345
- fn_name
346
- )
347
- )
348
- )
349
- )
350
-
351
326
  for function in functions:
352
327
  if isinstance(function, ComposioToolDefinition):
353
328
  # Get Composio tool details and hydrate the function definition
354
329
  enhanced_function = _hydrate_composio_tool_definition(function)
355
330
  prompt_functions.append(enhanced_function)
356
- # Create port for this function (using original function for get_function_name)
357
- function_name = get_function_name(function)
358
- port = create_port_condition(function_name)
359
- setattr(Ports, function_name, port)
360
331
  elif isinstance(function, MCPServer):
361
332
  tool_functions: List[MCPToolDefinition] = hydrate_mcp_tool_definitions(function)
362
333
  for tool_function in tool_functions:
@@ -368,19 +339,9 @@ def create_tool_router_node(
368
339
  parameters=tool_function.parameters,
369
340
  )
370
341
  )
371
- port = create_port_condition(name)
372
- setattr(Ports, name, port)
373
342
  else:
374
343
  prompt_functions.append(function)
375
- function_name = get_function_name(function)
376
- port = create_port_condition(function_name)
377
- setattr(Ports, function_name, port)
378
-
379
- # Add the else port for when no function conditions match
380
- setattr(Ports, "default", Port.on_else())
381
344
  else:
382
- # If no functions exist, create a simple Ports class with just a default port
383
- Ports = type("Ports", (), {"default": Port(default=True)})
384
345
  prompt_functions = []
385
346
 
386
347
  # Add a chat history block to blocks only if one doesn't already exist
@@ -416,10 +377,10 @@ def create_tool_router_node(
416
377
  }
417
378
 
418
379
  node = cast(
419
- Type[ToolRouterNode],
380
+ Type[ToolPromptNode],
420
381
  type(
421
- "ToolRouterNode",
422
- (ToolRouterNode,),
382
+ "ToolPromptNode",
383
+ (ToolPromptNode,),
423
384
  {
424
385
  "ml_model": ml_model,
425
386
  "blocks": blocks,
@@ -427,7 +388,6 @@ def create_tool_router_node(
427
388
  "prompt_inputs": node_prompt_inputs,
428
389
  "parameters": parameters,
429
390
  "max_prompt_iterations": max_prompt_iterations,
430
- "Ports": Ports,
431
391
  "__module__": __name__,
432
392
  },
433
393
  ),
@@ -437,25 +397,28 @@ def create_tool_router_node(
437
397
 
438
398
  def create_router_node(
439
399
  functions: List[Tool],
440
- tool_router_node: Type[ToolRouterNode],
400
+ tool_prompt_node: Type[InlinePromptNode[ToolCallingState]],
441
401
  ) -> Type[RouterNode]:
442
- """Create a RouterNode with the same ports as ToolRouterNode."""
402
+ """Create a RouterNode with dynamic ports that route based on tool_prompt_node outputs."""
443
403
 
444
404
  if functions and len(functions) > 0:
445
405
  # Create dynamic ports and convert functions in a single loop
446
406
  Ports = type("Ports", (), {})
447
407
 
408
+ # Avoid using lambda to capture function_name
409
+ # lambda will capture the function_name by reference,
410
+ # and if the function_name is changed, the port_condition will also change.
448
411
  def create_port_condition(fn_name):
449
412
  return Port.on_if(
450
413
  LazyReference(
451
414
  lambda: (
452
415
  ToolCallingState.current_prompt_output_index.less_than(
453
- tool_router_node.Outputs.results.length()
416
+ tool_prompt_node.Outputs.results.length()
454
417
  )
455
- & tool_router_node.Outputs.results[ToolCallingState.current_prompt_output_index]["type"].equals(
418
+ & tool_prompt_node.Outputs.results[ToolCallingState.current_prompt_output_index]["type"].equals(
456
419
  "FUNCTION_CALL"
457
420
  )
458
- & tool_router_node.Outputs.results[ToolCallingState.current_prompt_output_index]["value"][
421
+ & tool_prompt_node.Outputs.results[ToolCallingState.current_prompt_output_index]["value"][
459
422
  "name"
460
423
  ].equals(fn_name)
461
424
  )
@@ -491,6 +454,7 @@ def create_router_node(
491
454
  (RouterNode,),
492
455
  {
493
456
  "Ports": Ports,
457
+ "prompt_outputs": tool_prompt_node.Outputs.results,
494
458
  "__module__": __name__,
495
459
  },
496
460
  ),
@@ -500,7 +464,7 @@ def create_router_node(
500
464
 
501
465
  def create_function_node(
502
466
  function: ToolBase,
503
- tool_router_node: Type[ToolRouterNode],
467
+ tool_prompt_node: Type[ToolPromptNode],
504
468
  ) -> Type[BaseNode]:
505
469
  """
506
470
  Create a FunctionNode class for a given function.
@@ -510,7 +474,7 @@ def create_function_node(
510
474
 
511
475
  Args:
512
476
  function: The function to create a node for
513
- tool_router_node: The tool router node class
477
+ tool_prompt_node: The tool prompt node class
514
478
  """
515
479
  if isinstance(function, DeploymentDefinition):
516
480
  deployment = function.deployment_id or function.deployment_name
@@ -522,7 +486,7 @@ def create_function_node(
522
486
  {
523
487
  "deployment": deployment,
524
488
  "release_tag": release_tag,
525
- "function_call_output": tool_router_node.Outputs.results,
489
+ "function_call_output": tool_prompt_node.Outputs.results,
526
490
  "__module__": __name__,
527
491
  },
528
492
  )
@@ -535,7 +499,7 @@ def create_function_node(
535
499
  (ComposioNode,),
536
500
  {
537
501
  "composio_tool": function,
538
- "function_call_output": tool_router_node.Outputs.results,
502
+ "function_call_output": tool_prompt_node.Outputs.results,
539
503
  "__module__": __name__,
540
504
  },
541
505
  )
@@ -546,7 +510,7 @@ def create_function_node(
546
510
  (DynamicInlineSubworkflowNode,),
547
511
  {
548
512
  "subworkflow": function,
549
- "function_call_output": tool_router_node.Outputs.results,
513
+ "function_call_output": tool_prompt_node.Outputs.results,
550
514
  "__module__": __name__,
551
515
  },
552
516
  )
@@ -556,8 +520,8 @@ def create_function_node(
556
520
  f"FunctionNode_{function.__name__}",
557
521
  (FunctionNode,),
558
522
  {
559
- "function_definition": lambda self, **kwargs: function(**kwargs),
560
- "function_call_output": tool_router_node.Outputs.results,
523
+ "function_definition": lambda self, **kwargs: function(**kwargs), # ← Revert back to lambda
524
+ "function_call_output": tool_prompt_node.Outputs.results,
561
525
  "__module__": __name__,
562
526
  },
563
527
  )
@@ -567,14 +531,14 @@ def create_function_node(
567
531
 
568
532
  def create_mcp_tool_node(
569
533
  tool_def: MCPToolDefinition,
570
- tool_router_node: Type[ToolRouterNode],
534
+ tool_prompt_node: Type[ToolPromptNode],
571
535
  ) -> Type[BaseNode]:
572
536
  node = type(
573
537
  f"MCPNode_{tool_def.name}",
574
538
  (MCPNode,),
575
539
  {
576
540
  "mcp_tool": tool_def,
577
- "function_call_output": tool_router_node.Outputs.results,
541
+ "function_call_output": tool_prompt_node.Outputs.results,
578
542
  "__module__": __name__,
579
543
  },
580
544
  )
@@ -582,22 +546,25 @@ def create_mcp_tool_node(
582
546
 
583
547
 
584
548
  def create_else_node(
585
- tool_router_node: Type[ToolRouterNode],
549
+ tool_prompt_node: Type[ToolPromptNode],
586
550
  ) -> Type[ElseNode]:
587
551
  class Ports(ElseNode.Ports):
588
- loop = Port.on_if(
589
- ToolCallingState.current_prompt_output_index.less_than(tool_router_node.Outputs.results.length())
590
- | ToolCallingState.current_function_calls_processed.greater_than(0)
552
+ loop_to_router = Port.on_if(
553
+ ToolCallingState.current_prompt_output_index.less_than(tool_prompt_node.Outputs.results.length())
591
554
  )
555
+ loop_to_prompt = Port.on_elif(ToolCallingState.current_function_calls_processed.greater_than(0))
592
556
  end = Port.on_else()
593
557
 
594
- node = type(
595
- f"{tool_router_node.__name__}_ElseNode",
596
- (ElseNode,),
597
- {
598
- "Ports": Ports,
599
- "__module__": __name__,
600
- },
558
+ node = cast(
559
+ Type[ElseNode],
560
+ type(
561
+ f"{tool_prompt_node.__name__}_ElseNode",
562
+ (ElseNode,),
563
+ {
564
+ "Ports": Ports,
565
+ "__module__": __name__,
566
+ },
567
+ ),
601
568
  )
602
569
  return node
603
570
 
@@ -15,8 +15,10 @@ class EnvironmentVariableReference(BaseDescriptor[str]):
15
15
  name: str,
16
16
  # DEPRECATED - to be removed in 0.15.0 release
17
17
  default: Optional[str] = None,
18
+ serialize_as_constant: bool = False,
18
19
  ):
19
20
  super().__init__(name=name, types=(str,))
21
+ self._serialize_as_constant = serialize_as_constant
20
22
 
21
23
  def resolve(self, state: "BaseState") -> Any:
22
24
  env_value = os.environ.get(self.name)
@@ -24,3 +26,11 @@ class EnvironmentVariableReference(BaseDescriptor[str]):
24
26
  return env_value
25
27
 
26
28
  return undefined
29
+
30
+ @property
31
+ def serialize_as_constant(self) -> bool:
32
+ return self._serialize_as_constant
33
+
34
+ @serialize_as_constant.setter
35
+ def serialize_as_constant(self, value: bool):
36
+ self._serialize_as_constant = value
@@ -515,8 +515,24 @@ class WorkflowRunner(Generic[StateType]):
515
515
 
516
516
  all_deps = self._dependencies[node_class]
517
517
  node_span_id = node_class.Trigger._queue_node_execution(state, all_deps, invoked_by)
518
- if not node_class.Trigger.should_initiate(state, all_deps, node_span_id):
519
- return
518
+
519
+ try:
520
+ if not node_class.Trigger.should_initiate(state, all_deps, node_span_id):
521
+ return
522
+ except NodeException as e:
523
+ execution = get_execution_context()
524
+
525
+ self._workflow_event_outer_queue.put(
526
+ WorkflowExecutionRejectedEvent(
527
+ trace_id=execution.trace_id,
528
+ span_id=node_span_id,
529
+ body=WorkflowExecutionRejectedBody(
530
+ workflow_definition=self.workflow.__class__,
531
+ error=e.error,
532
+ ),
533
+ )
534
+ )
535
+ raise e
520
536
 
521
537
  execution = get_execution_context()
522
538
  node = node_class(state=state, context=self.workflow.context)
@@ -1,10 +1,12 @@
1
1
  from functools import cached_property
2
+ import inspect
2
3
  from queue import Queue
3
4
  from uuid import uuid4
4
5
  from typing import TYPE_CHECKING, Dict, List, Optional, Type
5
6
 
6
7
  from vellum import Vellum
7
- from vellum.workflows.context import ExecutionContext, get_execution_context
8
+ from vellum.workflows.context import ExecutionContext, get_execution_context, set_execution_context
9
+ from vellum.workflows.emitters.vellum_emitter import VellumEmitter
8
10
  from vellum.workflows.events.types import ExternalParentContext
9
11
  from vellum.workflows.nodes.mocks import MockNodeExecution, MockNodeExecutionArg
10
12
  from vellum.workflows.outputs.base import BaseOutputs
@@ -12,6 +14,7 @@ from vellum.workflows.references.constant import ConstantValueReference
12
14
  from vellum.workflows.vellum_client import create_vellum_client
13
15
 
14
16
  if TYPE_CHECKING:
17
+ from vellum.workflows.emitters.base import BaseWorkflowEmitter
15
18
  from vellum.workflows.events.workflow import WorkflowEvent
16
19
 
17
20
 
@@ -26,17 +29,36 @@ class WorkflowContext:
26
29
  self._vellum_client = vellum_client
27
30
  self._event_queue: Optional[Queue["WorkflowEvent"]] = None
28
31
  self._node_output_mocks_map: Dict[Type[BaseOutputs], List[MockNodeExecution]] = {}
29
- self._execution_context = get_execution_context()
30
-
31
- if execution_context is not None:
32
-
33
- self._execution_context.trace_id = execution_context.trace_id
34
-
35
- if execution_context.parent_context is not None:
36
- self._execution_context.parent_context = execution_context.parent_context
37
-
38
- if self._execution_context.parent_context is None:
39
- self._execution_context.parent_context = ExternalParentContext(span_id=uuid4())
32
+ # Clone the current thread-local execution context to avoid mutating global state
33
+ current_execution_context = get_execution_context()
34
+
35
+ # Resolve parent_context preference: provided > current > new external
36
+ resolved_parent_context = (
37
+ execution_context.parent_context
38
+ if execution_context is not None and execution_context.parent_context is not None
39
+ else current_execution_context.parent_context
40
+ )
41
+ if resolved_parent_context is None:
42
+ resolved_parent_context = ExternalParentContext(span_id=uuid4())
43
+
44
+ # Resolve trace_id preference: provided (if set) > current (if set) > new uuid
45
+ if execution_context is not None and int(execution_context.trace_id) != 0:
46
+ resolved_trace_id = execution_context.trace_id
47
+ elif int(current_execution_context.trace_id) != 0:
48
+ resolved_trace_id = current_execution_context.trace_id
49
+ else:
50
+ resolved_trace_id = uuid4()
51
+
52
+ # Construct a single, resolved execution context for this workflow instance
53
+ self._execution_context = ExecutionContext(
54
+ parent_context=resolved_parent_context,
55
+ trace_id=resolved_trace_id,
56
+ )
57
+
58
+ # Ensure the thread-local context has a parent_context for nodes that read it directly
59
+ if current_execution_context.parent_context is None:
60
+ current_execution_context.parent_context = resolved_parent_context
61
+ set_execution_context(current_execution_context)
40
62
 
41
63
  self._generated_files = generated_files
42
64
 
@@ -59,6 +81,73 @@ class WorkflowContext:
59
81
  def node_output_mocks_map(self) -> Dict[Type[BaseOutputs], List[MockNodeExecution]]:
60
82
  return self._node_output_mocks_map
61
83
 
84
+ @property
85
+ def monitoring_url(self) -> Optional[str]:
86
+ """
87
+ Get the base monitoring URL for this workflow context.
88
+
89
+ Returns the base URL to view executions in Vellum UI.
90
+ """
91
+ # Get UI URL from the Vellum client's API URL
92
+ api_url = self.vellum_client._client_wrapper.get_environment().default
93
+ return self._get_ui_url_from_api_url(api_url)
94
+
95
+ def get_monitoring_url(self, span_id: str) -> Optional[str]:
96
+ """
97
+ Generate the monitoring URL for this workflow execution.
98
+
99
+ Args:
100
+ span_id: The span ID from the workflow execution result.
101
+
102
+ Returns:
103
+ The URL to view execution details in Vellum UI, or None if monitoring is disabled.
104
+ """
105
+ base_url = self.monitoring_url
106
+ if base_url is None:
107
+ return None
108
+
109
+ return f"{base_url}/workflows/executions/{span_id}"
110
+
111
+ def _get_ui_url_from_api_url(self, api_url: str) -> str:
112
+ """
113
+ Convert an API URL to the corresponding UI URL.
114
+
115
+ Args:
116
+ api_url: The API base URL (e.g., https://api.vellum.ai or http://localhost:8000)
117
+
118
+ Returns:
119
+ The corresponding UI URL (e.g., https://app.vellum.ai or http://localhost:3000)
120
+ """
121
+ if "localhost" in api_url:
122
+ # For local development: localhost:8000 (API) -> localhost:3000 (UI)
123
+ return api_url.replace(":8000", ":3000")
124
+ elif "api.vellum.ai" in api_url:
125
+ # For production: api.vellum.ai -> app.vellum.ai
126
+ return api_url.replace("api.vellum.ai", "app.vellum.ai")
127
+ else:
128
+ # For custom domains, assume the same pattern: api.* -> app.*
129
+ return api_url.replace("api.", "app.", 1)
130
+
131
+ def get_emitters_for_workflow(self) -> List["BaseWorkflowEmitter"]:
132
+ """
133
+ Get the default emitters that should be attached to workflows using this context.
134
+
135
+ Returns:
136
+ List of emitters, including VellumEmitter if monitoring is enabled.
137
+ """
138
+ try:
139
+ frame = inspect.currentframe()
140
+ caller = frame.f_back if frame else None
141
+ if caller and "self" in caller.f_locals:
142
+ workflow_instance = caller.f_locals["self"]
143
+ class_level_emitters = getattr(workflow_instance.__class__, "emitters", None)
144
+ if isinstance(class_level_emitters, list) and len(class_level_emitters) > 0:
145
+ return class_level_emitters
146
+ except Exception:
147
+ pass
148
+
149
+ return [VellumEmitter()]
150
+
62
151
  def _emit_subworkflow_event(self, event: "WorkflowEvent") -> None:
63
152
  if self._event_queue:
64
153
  self._event_queue.put(event)
@@ -122,13 +122,23 @@ class MCPServer(UniversalBaseModel):
122
122
  type: Literal["MCP_SERVER"] = "MCP_SERVER"
123
123
  name: str
124
124
  url: str
125
- authorization_type: AuthorizationType = AuthorizationType.BEARER_TOKEN
125
+ authorization_type: Optional[AuthorizationType] = None
126
126
  bearer_token_value: Optional[Union[str, EnvironmentVariableReference]] = None
127
127
  api_key_header_key: Optional[str] = None
128
128
  api_key_header_value: Optional[Union[str, EnvironmentVariableReference]] = None
129
129
 
130
130
  model_config = {"arbitrary_types_allowed": True}
131
131
 
132
+ def __setattr__(self, name: str, value: Any) -> None:
133
+ """Override to automatically set serialization flags for environment variables."""
134
+ super().__setattr__(name, value)
135
+
136
+ if name == "bearer_token_value" and isinstance(value, EnvironmentVariableReference):
137
+ value.serialize_as_constant = True
138
+
139
+ if name == "api_key_header_value" and isinstance(value, EnvironmentVariableReference):
140
+ value.serialize_as_constant = True
141
+
132
142
 
133
143
  class MCPToolDefinition(UniversalBaseModel):
134
144
  name: str
@@ -124,3 +124,22 @@ def test_mcp_tool_definition_creation_api_key():
124
124
  },
125
125
  "required": ["repository_name"],
126
126
  }
127
+
128
+
129
+ def test_mcp_tool_definition_creation_no_authorization():
130
+ """Test that MCPToolDefinition can be created with no authorization."""
131
+ mcp_tool = MCPToolDefinition(
132
+ name="create_repository",
133
+ server=MCPServer(
134
+ name="github",
135
+ url="https://api.githubcopilot.com/mcp/",
136
+ ),
137
+ )
138
+
139
+ assert mcp_tool.name == "create_repository"
140
+ assert mcp_tool.server.name == "github"
141
+ assert mcp_tool.server.url == "https://api.githubcopilot.com/mcp/"
142
+ assert mcp_tool.server.authorization_type is None
143
+ assert mcp_tool.server.bearer_token_value is None
144
+ assert mcp_tool.server.api_key_header_key is None
145
+ assert mcp_tool.server.api_key_header_value is None
@@ -1,7 +1,7 @@
1
1
  import typing
2
2
  from typing import List, Tuple, Type, Union, get_args, get_origin
3
3
 
4
- from vellum import (
4
+ from vellum import ( # VellumVideo,; VellumVideoRequest,
5
5
  ChatMessage,
6
6
  ChatMessageRequest,
7
7
  FunctionCall,
@@ -65,10 +65,12 @@ def primitive_type_to_vellum_variable_type(type_: Union[Type, BaseDescriptor]) -
65
65
  return "NUMBER"
66
66
  elif _is_type_optionally_in(type_, (FunctionCall, FunctionCallRequest)):
67
67
  return "FUNCTION_CALL"
68
- elif _is_type_optionally_in(type_, (VellumImage, VellumImageRequest)):
69
- return "IMAGE"
70
68
  elif _is_type_optionally_in(type_, (VellumAudio, VellumAudioRequest)):
71
69
  return "AUDIO"
70
+ # elif _is_type_optionally_in(type_, (VellumVideo, VellumVideoRequest)):
71
+ # return "VIDEO"
72
+ elif _is_type_optionally_in(type_, (VellumImage, VellumImageRequest)):
73
+ return "IMAGE"
72
74
  elif _is_type_optionally_in(type_, (VellumDocument, VellumDocumentRequest)):
73
75
  return "DOCUMENT"
74
76
  elif _is_type_optionally_in(type_, (VellumError, VellumErrorRequest)):
@@ -101,10 +103,12 @@ def vellum_variable_type_to_openapi_type(vellum_type: VellumVariableType) -> str
101
103
  return "array"
102
104
  elif vellum_type == "FUNCTION_CALL":
103
105
  return "object"
104
- elif vellum_type == "IMAGE":
105
- return "object"
106
106
  elif vellum_type == "AUDIO":
107
107
  return "object"
108
+ elif vellum_type == "VIDEO":
109
+ return "object"
110
+ elif vellum_type == "IMAGE":
111
+ return "object"
108
112
  elif vellum_type == "DOCUMENT":
109
113
  return "object"
110
114
  elif vellum_type == "NULL":
@@ -240,12 +240,16 @@ class BaseWorkflow(Generic[InputsType, StateType], metaclass=_BaseWorkflowMeta):
240
240
  store: Optional[Store] = None,
241
241
  ):
242
242
  self._parent_state = parent_state
243
- self.emitters = emitters or (self.emitters if hasattr(self, "emitters") else [])
244
- self.resolvers = resolvers or (self.resolvers if hasattr(self, "resolvers") else [])
245
243
  self._context = context or WorkflowContext()
244
+ self.emitters = emitters or self._context.get_emitters_for_workflow()
245
+ self.resolvers = resolvers or (self.resolvers if hasattr(self, "resolvers") else [])
246
246
  self._store = store or Store()
247
247
  self._execution_context = self._context.execution_context
248
248
 
249
+ # Register context with all emitters
250
+ for emitter in self.emitters:
251
+ emitter.register_context(self._context)
252
+
249
253
  self.validate()
250
254
 
251
255
  @property
@@ -413,7 +417,7 @@ class BaseWorkflow(Generic[InputsType, StateType], metaclass=_BaseWorkflowMeta):
413
417
  last_event = event
414
418
 
415
419
  if not last_event:
416
- return WorkflowExecutionRejectedEvent(
420
+ rejected_event = WorkflowExecutionRejectedEvent(
417
421
  trace_id=self._execution_context.trace_id,
418
422
  span_id=uuid4(),
419
423
  body=WorkflowExecutionRejectedBody(
@@ -424,9 +428,10 @@ class BaseWorkflow(Generic[InputsType, StateType], metaclass=_BaseWorkflowMeta):
424
428
  workflow_definition=self.__class__,
425
429
  ),
426
430
  )
431
+ return rejected_event
427
432
 
428
433
  if not first_event:
429
- return WorkflowExecutionRejectedEvent(
434
+ rejected_event = WorkflowExecutionRejectedEvent(
430
435
  trace_id=self._execution_context.trace_id,
431
436
  span_id=uuid4(),
432
437
  body=WorkflowExecutionRejectedBody(
@@ -437,6 +442,7 @@ class BaseWorkflow(Generic[InputsType, StateType], metaclass=_BaseWorkflowMeta):
437
442
  workflow_definition=self.__class__,
438
443
  ),
439
444
  )
445
+ return rejected_event
440
446
 
441
447
  if (
442
448
  last_event.name == "workflow.execution.rejected"
@@ -445,7 +451,7 @@ class BaseWorkflow(Generic[InputsType, StateType], metaclass=_BaseWorkflowMeta):
445
451
  ):
446
452
  return last_event
447
453
 
448
- return WorkflowExecutionRejectedEvent(
454
+ rejected_event = WorkflowExecutionRejectedEvent(
449
455
  trace_id=self._execution_context.trace_id,
450
456
  span_id=first_event.span_id,
451
457
  body=WorkflowExecutionRejectedBody(
@@ -456,6 +462,7 @@ class BaseWorkflow(Generic[InputsType, StateType], metaclass=_BaseWorkflowMeta):
456
462
  ),
457
463
  ),
458
464
  )
465
+ return rejected_event
459
466
 
460
467
  def stream(
461
468
  self,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: vellum-ai
3
- Version: 1.1.1
3
+ Version: 1.1.3
4
4
  Summary:
5
5
  License: MIT
6
6
  Requires-Python: >=3.9,<4.0