|
2 | 2 |
|
3 | 3 | from typing import Literal |
4 | 4 |
|
5 | | -from langchain_core.messages import AIMessage, AnyMessage, ToolCall |
6 | | -from uipath.agent.react import END_EXECUTION_TOOL, RAISE_ERROR_TOOL |
7 | | - |
8 | 5 | from ..exceptions import AgentNodeRoutingException |
9 | | -from .types import AgentGraphNode, AgentGraphState |
10 | | -from .utils import count_consecutive_thinking_messages |
11 | | - |
12 | | -FLOW_CONTROL_TOOLS = [END_EXECUTION_TOOL.name, RAISE_ERROR_TOOL.name] |
13 | | - |
14 | | - |
15 | | -def __filter_control_flow_tool_calls( |
16 | | - tool_calls: list[ToolCall], |
17 | | -) -> list[ToolCall]: |
18 | | - """Remove control flow tools when multiple tool calls exist.""" |
19 | | - if len(tool_calls) <= 1: |
20 | | - return tool_calls |
21 | | - |
22 | | - return [tc for tc in tool_calls if tc.get("name") not in FLOW_CONTROL_TOOLS] |
23 | | - |
24 | | - |
25 | | -def __has_control_flow_tool(tool_calls: list[ToolCall]) -> bool: |
26 | | - """Check if any tool call is of a control flow tool.""" |
27 | | - return any(tc.get("name") in FLOW_CONTROL_TOOLS for tc in tool_calls) |
28 | | - |
29 | | - |
30 | | -def __validate_last_message_is_AI(messages: list[AnyMessage]) -> AIMessage: |
31 | | - """Validate and return last message from state. |
32 | | -
|
33 | | - Raises: |
34 | | - AgentNodeRoutingException: If messages are empty or last message is not AIMessage |
35 | | - """ |
36 | | - if not messages: |
37 | | - raise AgentNodeRoutingException( |
38 | | - "No messages in state - cannot route after agent" |
39 | | - ) |
40 | | - |
41 | | - last_message = messages[-1] |
42 | | - if not isinstance(last_message, AIMessage): |
43 | | - raise AgentNodeRoutingException( |
44 | | - f"Last message is not AIMessage (type: {type(last_message).__name__}) - cannot route after agent" |
45 | | - ) |
46 | | - |
47 | | - return last_message |
| 6 | +from .types import ( |
| 7 | + FLOW_CONTROL_TOOLS, |
| 8 | + AgentGraphNode, |
| 9 | + AgentGraphState, |
| 10 | +) |
| 11 | +from .utils import find_latest_ai_message |
48 | 12 |
|
49 | 13 |
|
50 | | -def create_route_agent(thinking_messages_limit: int = 0): |
51 | | - """Create a routing function configured with thinking_messages_limit. |
52 | | -
|
53 | | - Args: |
54 | | - thinking_messages_limit: Max consecutive thinking messages before error |
| 14 | +def create_route_agent(): |
| 15 | + """Create a routing function for LangGraph conditional edges. |
55 | 16 |
|
56 | 17 | Returns: |
57 | 18 | Routing function for LangGraph conditional edges |
58 | 19 | """ |
59 | 20 |
|
60 | 21 | def route_agent( |
61 | 22 | state: AgentGraphState, |
62 | | - ) -> list[str] | Literal[AgentGraphNode.AGENT, AgentGraphNode.TERMINATE]: |
63 | | - """Route after agent: handles all routing logic including control flow detection. |
| 23 | + ) -> str | Literal[AgentGraphNode.AGENT, AgentGraphNode.TERMINATE]: |
| 24 | + """Route after agent: looks at current tool call index and routes to corresponding tool node. |
64 | 25 |
|
65 | 26 | Routing logic: |
66 | | - 1. If multiple tool calls exist, filter out control flow tools (EndExecution, RaiseError) |
67 | | - 2. If control flow tool(s) remain, route to TERMINATE |
68 | | - 3. If regular tool calls remain, route to specific tool nodes (return list of tool names) |
69 | | - 4. If no tool calls, handle consecutive completions |
| 27 | + 1. If current_tool_call_index is None, route back to LLM |
| 28 | + 2. If current_tool_call_index is set, route to the corresponding tool node |
| 29 | + 3. Handle control flow tools for termination |
70 | 30 |
|
71 | 31 | Returns: |
72 | | - - list[str]: Tool node names for parallel execution |
73 | | - - AgentGraphNode.AGENT: For consecutive completions |
| 32 | + - str: Tool node name for single tool execution |
| 33 | + - AgentGraphNode.AGENT: When no current tool call index |
74 | 34 | - AgentGraphNode.TERMINATE: For control flow termination |
75 | 35 |
|
76 | 36 | Raises: |
77 | | - AgentNodeRoutingException: When encountering unexpected state (empty messages, non-AIMessage, or excessive completions) |
| 37 | + AgentNodeRoutingException: When encountering unexpected state |
78 | 38 | """ |
| 39 | + current_index = state.current_tool_call_index |
| 40 | + |
| 41 | + # no tool call in progress, route back to LLM |
| 42 | + if current_index is None: |
| 43 | + return AgentGraphNode.AGENT |
| 44 | + |
79 | 45 | messages = state.messages |
80 | | - last_message = __validate_last_message_is_AI(messages) |
81 | 46 |
|
82 | | - tool_calls = list(last_message.tool_calls) if last_message.tool_calls else [] |
83 | | - tool_calls = __filter_control_flow_tool_calls(tool_calls) |
| 47 | + if not messages: |
| 48 | + raise AgentNodeRoutingException( |
| 49 | + "No messages in state - cannot route after agent" |
| 50 | + ) |
84 | 51 |
|
85 | | - if tool_calls and __has_control_flow_tool(tool_calls): |
86 | | - return AgentGraphNode.TERMINATE |
| 52 | + latest_ai_message = find_latest_ai_message(messages) |
87 | 53 |
|
88 | | - if tool_calls: |
89 | | - return [tc["name"] for tc in tool_calls] |
| 54 | + if latest_ai_message is None: |
| 55 | + raise AgentNodeRoutingException( |
| 56 | + "No AIMessage found in messages - cannot route after agent" |
| 57 | + ) |
90 | 58 |
|
91 | | - consecutive_thinking_messages = count_consecutive_thinking_messages(messages) |
| 59 | + tool_calls = ( |
| 60 | + list(latest_ai_message.tool_calls) if latest_ai_message.tool_calls else [] |
| 61 | + ) |
92 | 62 |
|
93 | | - if consecutive_thinking_messages > thinking_messages_limit: |
| 63 | + if current_index >= len(tool_calls): |
94 | 64 | raise AgentNodeRoutingException( |
95 | | - f"Agent exceeded consecutive completions limit without producing tool calls " |
96 | | - f"(completions: {consecutive_thinking_messages}, max: {thinking_messages_limit}). " |
97 | | - f"This should not happen as tool_choice='required' is enforced at the limit." |
| 65 | + f"Current tool call index {current_index} exceeds available tool calls ({len(tool_calls)})" |
98 | 66 | ) |
99 | 67 |
|
100 | | - if last_message.content: |
101 | | - return AgentGraphNode.AGENT |
| 68 | + current_tool_call = tool_calls[current_index] |
| 69 | + tool_name = current_tool_call["name"] |
102 | 70 |
|
103 | | - raise AgentNodeRoutingException( |
104 | | - f"Agent produced empty response without tool calls " |
105 | | - f"(completions: {consecutive_thinking_messages}, has_content: False)" |
106 | | - ) |
| 71 | + # handle control flow tools for termination |
| 72 | + if tool_name in FLOW_CONTROL_TOOLS: |
| 73 | + return AgentGraphNode.TERMINATE |
| 74 | + |
| 75 | + return tool_name |
107 | 76 |
|
108 | 77 | return route_agent |
0 commit comments