Skip to content

Commit aaa8762

Browse files
committed
feat: support native catch exception node in agentspec adapter
1 parent 65857f2 commit aaa8762

File tree

9 files changed

+717
-55
lines changed

9 files changed

+717
-55
lines changed

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
26.2.0.dev0
1+
26.2.0.dev2

docs/wayflowcore/source/core/changelog.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,11 @@ New features
112112
Improvements
113113
^^^^^^^^^^^^
114114

115+
* **Support for native Agent Spec CatchExceptionNode:**
116+
117+
WayFlow flows using the CatchExceptionStep now convert to the native Agent Spec
118+
CatchExceptionNode when compatible (i.e., when catching all exceptions).
119+
115120
* **Added ``sensitive_headers`` in components that perform remote calls:**
116121

117122
``ApiCallStep``, ``RemoteTool``, and MCP ``RemoteBaseTransport`` now have a new attribute ``sensitive_headers``.

wayflowcore/constraints/constraints_dev.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,6 @@ PyYAML==6.0.3
88
pydantic==2.12.4
99
httpx==0.28.1
1010
mcp==1.24.0
11-
pyagentspec==26.2.0.dev0 # Main branch of pyagentspec
11+
pyagentspec==26.2.0.dev1 # Main branch of pyagentspec
1212
opentelemetry-api==1.36.0
1313
opentelemetry-sdk==1.36.0

wayflowcore/setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ def read(file_name):
5252
packages=find_packages("src"),
5353
python_requires=">=3.10,<3.15",
5454
install_requires=[
55-
"pyagentspec>=26.1.0",
55+
"pyagentspec>=26.2.0.dev0",
5656
"httpx>0.28.0,<1.0.0", # warning but no vulnerabilities
5757
"numpy>=1.24.3,<3.0.0",
5858
"pandas>=2.0.3,<3.0.0",

wayflowcore/src/wayflowcore/datastore/inmemory.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,10 @@
66

77
import warnings
88
from logging import getLogger
9-
from typing import Any, Dict, List, Optional, Union, cast, overload
10-
11-
import numpy as np
12-
import pandas as pd
9+
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union, cast, overload
1310

1411
from wayflowcore._metadata import MetadataType
12+
from wayflowcore._utils.lazy_loader import LazyLoader
1513
from wayflowcore.datastore._datatable import Datatable
1614
from wayflowcore.datastore._utils import (
1715
check_collection_name,
@@ -35,6 +33,16 @@
3533
from wayflowcore.serialization.context import DeserializationContext, SerializationContext
3634
from wayflowcore.serialization.serializer import serialize_to_dict
3735

36+
if TYPE_CHECKING:
37+
# Important: do not move these imports out of the TYPE_CHECKING
38+
# Otherwise, importing the module when they are not installed would lead to an import error.
39+
import numpy as np
40+
import pandas as pd
41+
else:
42+
np = LazyLoader("numpy")
43+
pd = LazyLoader("pandas")
44+
45+
3846
logger = getLogger(__name__)
3947

4048
_INMEMORY_USER_WARNING = "InMemoryDatastore is for DEVELOPMENT and PROOF-OF-CONCEPT ONLY!"

wayflowcore/src/wayflowcore/serialization/_builtins_deserialization_plugin.py

Lines changed: 65 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@
4646
from pyagentspec.flows.nodes.agentnode import AgentNode as AgentSpecAgentNode
4747
from pyagentspec.flows.nodes.apinode import ApiNode as AgentSpecApiNode
4848
from pyagentspec.flows.nodes.branchingnode import BranchingNode as AgentSpecBranchingNode
49+
from pyagentspec.flows.nodes.catchexceptionnode import (
50+
CatchExceptionNode as AgentSpecCatchExceptionNode,
51+
)
4952
from pyagentspec.flows.nodes.endnode import EndNode as AgentSpecEndNode
5053
from pyagentspec.flows.nodes.flownode import FlowNode as AgentSpecFlowNode
5154
from pyagentspec.flows.nodes.llmnode import LlmNode as AgentSpecLlmNode
@@ -379,6 +382,7 @@
379382
from wayflowcore.outputparser import PythonToolOutputParser as RuntimePythonToolOutputParser
380383
from wayflowcore.outputparser import RegexOutputParser as RuntimeRegexOutputParser
381384
from wayflowcore.outputparser import RegexPattern as RuntimeRegexPattern
385+
from wayflowcore.property import AnyProperty as RuntimeAnyProperty
382386
from wayflowcore.property import JsonSchemaParam
383387
from wayflowcore.property import ListProperty as RuntimeListProperty
384388
from wayflowcore.property import Property as RuntimeProperty
@@ -1105,10 +1109,18 @@ def _find_property(properties: List[AgentSpecProperty], name: str) -> AgentSpecP
11051109
conversion_context.convert(edge, tool_registry, converted_components)
11061110
for edge in data_flow_connections or []
11071111
]
1108-
control_flow_edges: List[RuntimeControlFlowEdge] = [
1109-
conversion_context.convert(edge, tool_registry, converted_components)
1110-
for edge in agentspec_component.control_flow_connections
1111-
]
1112+
control_flow_edges: List[RuntimeControlFlowEdge] = []
1113+
for edge in agentspec_component.control_flow_connections:
1114+
if (
1115+
isinstance(edge.from_node, AgentSpecCatchExceptionNode)
1116+
and edge.from_branch == AgentSpecCatchExceptionNode.CAUGHT_EXCEPTION_BRANCH
1117+
):
1118+
# we need to rename the branch used in the CatchExceptionNode
1119+
edge.from_branch = RuntimeCatchExceptionStep.DEFAULT_EXCEPTION_BRANCH
1120+
control_flow_edges.append(
1121+
conversion_context.convert(edge, tool_registry, converted_components)
1122+
)
1123+
11121124
for step in steps.values():
11131125
for branch in step.get_branches():
11141126
edge_exists = any(
@@ -1482,6 +1494,55 @@ def _find_property(properties: List[AgentSpecProperty], name: str) -> AgentSpecP
14821494
except_on=agentspec_component.except_on,
14831495
**self._get_rt_nodes_arguments(agentspec_component, metadata_info),
14841496
)
1497+
elif isinstance(agentspec_component, AgentSpecCatchExceptionNode):
1498+
# Standard CatchExceptionNode from Agent Spec does not expose catch_all_exceptions
1499+
# and except_on fields
1500+
# Also, the output of the catch exception node might be renamed,
1501+
# so we have to use output mapping when needed
1502+
rt_nodes_arguments = self._get_node_arguments(agentspec_component, metadata_info)
1503+
if agentspec_component.outputs:
1504+
subflow_outputs_titles = {
1505+
p.title for p in agentspec_component.subflow.outputs or []
1506+
}
1507+
caught_exception_property = next(
1508+
(
1509+
p
1510+
for p in agentspec_component.outputs
1511+
if p.title not in subflow_outputs_titles
1512+
),
1513+
None,
1514+
)
1515+
if caught_exception_property is None:
1516+
raise ValueError(
1517+
f"Internal error: Agent Spec CatchExceptionNode '{agentspec_component.name}' "
1518+
"is missing a output for the exception info. Make sure the pyagentspec "
1519+
"component is successfully validated."
1520+
)
1521+
if (
1522+
caught_exception_property.title
1523+
!= RuntimeCatchExceptionStep.EXCEPTION_PAYLOAD_OUTPUT_NAME
1524+
):
1525+
# there is no output mapping by default. We add one to handle renaming
1526+
rt_nodes_arguments["output_mapping"] = {
1527+
RuntimeCatchExceptionStep.EXCEPTION_PAYLOAD_OUTPUT_NAME: caught_exception_property.title
1528+
}
1529+
1530+
# we need to add a default output property for the exception name
1531+
rt_nodes_arguments["output_descriptors"].append(
1532+
RuntimeAnyProperty(
1533+
name=RuntimeCatchExceptionStep.EXCEPTION_NAME_OUTPUT_NAME,
1534+
default_value="",
1535+
)
1536+
)
1537+
1538+
return RuntimeCatchExceptionStep(
1539+
flow=conversion_context.convert(
1540+
agentspec_component.subflow, tool_registry, converted_components
1541+
),
1542+
catch_all_exceptions=True,
1543+
except_on=None,
1544+
**rt_nodes_arguments,
1545+
)
14851546
elif isinstance(agentspec_component, AgentSpecPluginRegexNode):
14861547
regex_pattern = self._regex_pattern_to_runtime(agentspec_component.regex_pattern)
14871548
if not (

0 commit comments

Comments
 (0)