vellum-ai 1.1.2__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.
- vellum/__init__.py +16 -0
- vellum/client/README.md +55 -0
- vellum/client/__init__.py +66 -507
- vellum/client/core/client_wrapper.py +2 -2
- vellum/client/raw_client.py +844 -0
- vellum/client/reference.md +692 -19
- vellum/client/resources/ad_hoc/client.py +23 -180
- vellum/client/resources/ad_hoc/raw_client.py +276 -0
- vellum/client/resources/container_images/client.py +10 -36
- vellum/client/resources/deployments/client.py +16 -62
- vellum/client/resources/document_indexes/client.py +16 -72
- vellum/client/resources/documents/client.py +8 -30
- vellum/client/resources/folder_entities/client.py +4 -8
- vellum/client/resources/metric_definitions/client.py +4 -14
- vellum/client/resources/ml_models/client.py +2 -8
- vellum/client/resources/organizations/client.py +2 -6
- vellum/client/resources/prompts/client.py +2 -10
- vellum/client/resources/sandboxes/client.py +4 -20
- vellum/client/resources/test_suite_runs/client.py +4 -18
- vellum/client/resources/test_suites/client.py +11 -86
- vellum/client/resources/test_suites/raw_client.py +136 -0
- vellum/client/resources/workflow_deployments/client.py +20 -78
- vellum/client/resources/workflow_executions/client.py +2 -6
- vellum/client/resources/workflow_sandboxes/client.py +2 -10
- vellum/client/resources/workflows/client.py +7 -6
- vellum/client/resources/workflows/raw_client.py +58 -47
- vellum/client/resources/workspace_secrets/client.py +4 -20
- vellum/client/resources/workspaces/client.py +2 -6
- vellum/client/types/__init__.py +16 -0
- vellum/client/types/array_chat_message_content_item.py +4 -2
- vellum/client/types/array_chat_message_content_item_request.py +4 -2
- vellum/client/types/chat_message_content.py +4 -2
- vellum/client/types/chat_message_content_request.py +4 -2
- vellum/client/types/node_execution_span.py +2 -0
- vellum/client/types/prompt_block.py +4 -2
- vellum/client/types/vellum_value.py +4 -2
- vellum/client/types/vellum_value_request.py +4 -2
- vellum/client/types/vellum_variable_type.py +2 -1
- vellum/client/types/vellum_video.py +24 -0
- vellum/client/types/vellum_video_request.py +24 -0
- vellum/client/types/video_chat_message_content.py +25 -0
- vellum/client/types/video_chat_message_content_request.py +25 -0
- vellum/client/types/video_prompt_block.py +29 -0
- vellum/client/types/video_vellum_value.py +25 -0
- vellum/client/types/video_vellum_value_request.py +25 -0
- vellum/client/types/workflow_execution_span.py +2 -0
- vellum/client/types/workflow_execution_usage_calculation_fulfilled_body.py +22 -0
- vellum/prompts/blocks/compilation.py +22 -10
- vellum/types/vellum_video.py +3 -0
- vellum/types/vellum_video_request.py +3 -0
- vellum/types/video_chat_message_content.py +3 -0
- vellum/types/video_chat_message_content_request.py +3 -0
- vellum/types/video_prompt_block.py +3 -0
- vellum/types/video_vellum_value.py +3 -0
- vellum/types/video_vellum_value_request.py +3 -0
- vellum/types/workflow_execution_usage_calculation_fulfilled_body.py +3 -0
- vellum/workflows/events/workflow.py +11 -0
- vellum/workflows/graph/graph.py +103 -1
- vellum/workflows/graph/tests/test_graph.py +99 -0
- vellum/workflows/nodes/bases/base.py +9 -1
- vellum/workflows/nodes/displayable/bases/utils.py +4 -2
- vellum/workflows/nodes/displayable/tool_calling_node/node.py +19 -18
- vellum/workflows/nodes/displayable/tool_calling_node/tests/test_node.py +17 -7
- vellum/workflows/nodes/displayable/tool_calling_node/tests/test_utils.py +7 -7
- vellum/workflows/nodes/displayable/tool_calling_node/utils.py +47 -80
- vellum/workflows/references/environment_variable.py +10 -0
- vellum/workflows/runner/runner.py +18 -2
- vellum/workflows/state/context.py +101 -12
- vellum/workflows/types/definition.py +11 -1
- vellum/workflows/types/tests/test_definition.py +19 -0
- vellum/workflows/utils/vellum_variables.py +9 -5
- vellum/workflows/workflows/base.py +12 -5
- {vellum_ai-1.1.2.dist-info → vellum_ai-1.1.3.dist-info}/METADATA +1 -1
- {vellum_ai-1.1.2.dist-info → vellum_ai-1.1.3.dist-info}/RECORD +84 -68
- vellum_ee/workflows/display/nodes/vellum/code_execution_node.py +1 -1
- vellum_ee/workflows/display/nodes/vellum/tests/test_code_execution_node.py +55 -1
- vellum_ee/workflows/display/nodes/vellum/tests/test_tool_calling_node.py +15 -52
- vellum_ee/workflows/display/tests/workflow_serialization/test_basic_tool_calling_node_mcp_serialization.py +15 -49
- vellum_ee/workflows/display/types.py +14 -1
- vellum_ee/workflows/display/utils/expressions.py +13 -4
- vellum_ee/workflows/display/workflows/base_workflow_display.py +6 -19
- {vellum_ai-1.1.2.dist-info → vellum_ai-1.1.3.dist-info}/LICENSE +0 -0
- {vellum_ai-1.1.2.dist-info → vellum_ai-1.1.3.dist-info}/WHEEL +0 -0
- {vellum_ai-1.1.2.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
|
-
|
51
|
-
|
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
|
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
|
-
|
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 =
|
138
|
-
#
|
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
|
-
|
259
|
-
ToolCallingState.current_prompt_output_index.less_than(
|
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
|
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[
|
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[
|
380
|
+
Type[ToolPromptNode],
|
420
381
|
type(
|
421
|
-
"
|
422
|
-
(
|
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
|
-
|
400
|
+
tool_prompt_node: Type[InlinePromptNode[ToolCallingState]],
|
441
401
|
) -> Type[RouterNode]:
|
442
|
-
"""Create a RouterNode with
|
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
|
-
|
416
|
+
tool_prompt_node.Outputs.results.length()
|
454
417
|
)
|
455
|
-
&
|
418
|
+
& tool_prompt_node.Outputs.results[ToolCallingState.current_prompt_output_index]["type"].equals(
|
456
419
|
"FUNCTION_CALL"
|
457
420
|
)
|
458
|
-
&
|
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
|
-
|
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
|
-
|
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":
|
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":
|
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":
|
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":
|
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
|
-
|
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":
|
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
|
-
|
549
|
+
tool_prompt_node: Type[ToolPromptNode],
|
586
550
|
) -> Type[ElseNode]:
|
587
551
|
class Ports(ElseNode.Ports):
|
588
|
-
|
589
|
-
ToolCallingState.current_prompt_output_index.less_than(
|
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 =
|
595
|
-
|
596
|
-
(
|
597
|
-
|
598
|
-
|
599
|
-
|
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
|
-
|
519
|
-
|
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
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
if execution_context.parent_context is not None
|
36
|
-
|
37
|
-
|
38
|
-
if
|
39
|
-
|
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 =
|
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
|
-
|
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
|
-
|
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
|
-
|
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,
|