Skip to content

Commit 38af7ba

Browse files
committed
feat: support native catch exception node in agentspec adapter
1 parent 9e838e8 commit 38af7ba

File tree

6 files changed

+724
-55
lines changed

6 files changed

+724
-55
lines changed

docs/wayflowcore/source/core/changelog.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,12 @@ New features
6262
Improvements
6363
^^^^^^^^^^^^
6464

65+
* **Support for native Agent Spec CatchExceptionNode:**
66+
67+
WayFlow flows using the CatchExceptionStep now convert to the native Agent Spec
68+
CatchExceptionNode when compatible (i.e., when catching all exceptions).
69+
70+
6571
* **Support list, dict and tuple output types in MCP tools:**
6672

6773
MCP tools now support non-string output types (list of string, dictionary of strings

wayflowcore/src/wayflowcore/datastore/inmemory.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,10 @@
55
# (UPL) 1.0 (LICENSE-UPL or https://oss.oracle.com/licenses/upl), at your option.
66

77
from logging import getLogger
8-
from typing import Any, Dict, List, Optional, Union, cast, overload
8+
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union, cast, overload
99
from warnings import warn
1010

11-
import numpy as np
12-
import pandas as pd
13-
11+
from wayflowcore._utils.lazy_loader import LazyLoader
1412
from wayflowcore.component import Component
1513
from wayflowcore.datastore._datatable import Datatable
1614
from wayflowcore.datastore._utils import (
@@ -24,6 +22,16 @@
2422
from wayflowcore.serialization.context import DeserializationContext, SerializationContext
2523
from wayflowcore.serialization.serializer import SerializableObject, serialize_to_dict
2624

25+
if TYPE_CHECKING:
26+
# Important: do not move these imports out of the TYPE_CHECKING
27+
# Otherwise, importing the module when they are not installed would lead to an import error.
28+
import numpy as np
29+
import pandas as pd
30+
else:
31+
np = LazyLoader("numpy")
32+
pd = LazyLoader("pandas")
33+
34+
2735
logger = getLogger(__name__)
2836

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

wayflowcore/src/wayflowcore/serialization/_builtins_deserialization_plugin.py

Lines changed: 68 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@
3333
from pyagentspec.flows.nodes.agentnode import AgentNode as AgentSpecAgentNode
3434
from pyagentspec.flows.nodes.apinode import ApiNode as AgentSpecApiNode
3535
from pyagentspec.flows.nodes.branchingnode import BranchingNode as AgentSpecBranchingNode
36+
from pyagentspec.flows.nodes.catchexceptionnode import (
37+
CatchExceptionNode as AgentSpecCatchExceptionNode,
38+
)
3639
from pyagentspec.flows.nodes.endnode import EndNode as AgentSpecEndNode
3740
from pyagentspec.flows.nodes.flownode import FlowNode as AgentSpecFlowNode
3841
from pyagentspec.flows.nodes.llmnode import LlmNode as AgentSpecLlmNode
@@ -115,7 +118,9 @@
115118
from wayflowcore.agentspec.components import (
116119
PluginVllmEmbeddingConfig as AgentSpecPluginVllmEmbeddingConfig,
117120
)
118-
from wayflowcore.agentspec.components import all_deserialization_plugin
121+
from wayflowcore.agentspec.components import (
122+
all_deserialization_plugin,
123+
)
119124
from wayflowcore.agentspec.components.agent import ExtendedAgent as AgentSpecExtendedAgent
120125
from wayflowcore.agentspec.components.contextprovider import (
121126
PluginConstantContextProvider as AgentSpecPluginConstantContextProvider,
@@ -359,6 +364,7 @@
359364
from wayflowcore.outputparser import PythonToolOutputParser as RuntimePythonToolOutputParser
360365
from wayflowcore.outputparser import RegexOutputParser as RuntimeRegexOutputParser
361366
from wayflowcore.outputparser import RegexPattern as RuntimeRegexPattern
367+
from wayflowcore.property import AnyProperty as RuntimeAnyProperty
362368
from wayflowcore.property import JsonSchemaParam
363369
from wayflowcore.property import ListProperty as RuntimeListProperty
364370
from wayflowcore.property import Property as RuntimeProperty
@@ -997,10 +1003,18 @@ def _find_property(properties: List[AgentSpecProperty], name: str) -> AgentSpecP
9971003
conversion_context.convert(edge, tool_registry, converted_components)
9981004
for edge in data_flow_connections or []
9991005
]
1000-
control_flow_edges: List[RuntimeControlFlowEdge] = [
1001-
conversion_context.convert(edge, tool_registry, converted_components)
1002-
for edge in agentspec_component.control_flow_connections
1003-
]
1006+
control_flow_edges: List[RuntimeControlFlowEdge] = []
1007+
for edge in agentspec_component.control_flow_connections:
1008+
if (
1009+
isinstance(edge.from_node, AgentSpecCatchExceptionNode)
1010+
and edge.from_branch == AgentSpecCatchExceptionNode.CAUGHT_EXCEPTION_BRANCH
1011+
):
1012+
# we need to rename the branch used in the CatchExceptionNode
1013+
edge.from_branch = RuntimeCatchExceptionStep.DEFAULT_EXCEPTION_BRANCH
1014+
control_flow_edges.append(
1015+
conversion_context.convert(edge, tool_registry, converted_components)
1016+
)
1017+
10041018
for step in steps.values():
10051019
for branch in step.get_branches():
10061020
edge_exists = any(
@@ -1359,6 +1373,55 @@ def _find_property(properties: List[AgentSpecProperty], name: str) -> AgentSpecP
13591373
except_on=agentspec_component.except_on,
13601374
**self._get_rt_nodes_arguments(agentspec_component, metadata_info),
13611375
)
1376+
elif isinstance(agentspec_component, AgentSpecCatchExceptionNode):
1377+
# Standard CatchExceptionNode from Agent Spec does not expose catch_all_exceptions
1378+
# and except_on fields
1379+
# Also, the output of the catch exception node might be renamed,
1380+
# so we have to use output mapping when needed
1381+
rt_nodes_arguments = self._get_node_arguments(agentspec_component, metadata_info)
1382+
if agentspec_component.outputs:
1383+
subflow_outputs_titles = {
1384+
p.title for p in agentspec_component.subflow.outputs or []
1385+
}
1386+
caught_exception_property = next(
1387+
(
1388+
p
1389+
for p in agentspec_component.outputs
1390+
if p.title not in subflow_outputs_titles
1391+
),
1392+
None,
1393+
)
1394+
if caught_exception_property is None:
1395+
raise ValueError(
1396+
f"Internal error: Agent Spec CatchExceptionNode '{agentspec_component.name}' "
1397+
"is missing a output for the exception info. Make sure the pyagentspec "
1398+
"component is successfully validated."
1399+
)
1400+
if (
1401+
caught_exception_property.title
1402+
!= RuntimeCatchExceptionStep.EXCEPTION_PAYLOAD_OUTPUT_NAME
1403+
):
1404+
# there is no output mapping by default. We add one to handle renaming
1405+
rt_nodes_arguments["output_mapping"] = {
1406+
RuntimeCatchExceptionStep.EXCEPTION_PAYLOAD_OUTPUT_NAME: caught_exception_property.title
1407+
}
1408+
1409+
# we need to add a default output property for the exception name
1410+
rt_nodes_arguments["output_descriptors"].append(
1411+
RuntimeAnyProperty(
1412+
name=RuntimeCatchExceptionStep.EXCEPTION_NAME_OUTPUT_NAME,
1413+
default_value="",
1414+
)
1415+
)
1416+
1417+
return RuntimeCatchExceptionStep(
1418+
flow=conversion_context.convert(
1419+
agentspec_component.subflow, tool_registry, converted_components
1420+
),
1421+
catch_all_exceptions=True,
1422+
except_on=None,
1423+
**rt_nodes_arguments,
1424+
)
13621425
elif isinstance(agentspec_component, AgentSpecPluginRegexNode):
13631426
regex_pattern = self._regex_pattern_to_runtime(agentspec_component.regex_pattern)
13641427
if not (

0 commit comments

Comments
 (0)