Initial commit
This commit is contained in:
318
skills/langgraph-master/04_tool_integration_tool_node.md
Normal file
318
skills/langgraph-master/04_tool_integration_tool_node.md
Normal file
@@ -0,0 +1,318 @@
|
||||
# Tool Node
|
||||
|
||||
Implementation of nodes that execute tools.
|
||||
|
||||
## ToolNode (Built-in)
|
||||
|
||||
The simplest approach:
|
||||
|
||||
```python
|
||||
from langgraph.prebuilt import ToolNode
|
||||
|
||||
tools = [search_tool, calculator_tool]
|
||||
tool_node = ToolNode(tools)
|
||||
|
||||
# Add to graph
|
||||
builder.add_node("tools", tool_node)
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
ToolNode:
|
||||
1. Extracts `tool_calls` from the last message
|
||||
2. Executes each tool
|
||||
3. Returns results as `ToolMessage`
|
||||
|
||||
```python
|
||||
# Input
|
||||
{
|
||||
"messages": [
|
||||
AIMessage(tool_calls=[
|
||||
{"name": "search", "args": {"query": "weather"}, "id": "1"}
|
||||
])
|
||||
]
|
||||
}
|
||||
|
||||
# ToolNode execution
|
||||
|
||||
# Output
|
||||
{
|
||||
"messages": [
|
||||
ToolMessage(
|
||||
content="Sunny, 25°C",
|
||||
tool_call_id="1"
|
||||
)
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Custom Tool Node
|
||||
|
||||
For finer control:
|
||||
|
||||
```python
|
||||
def custom_tool_node(state: MessagesState):
|
||||
"""Custom tool node"""
|
||||
last_message = state["messages"][-1]
|
||||
tool_results = []
|
||||
|
||||
for tool_call in last_message.tool_calls:
|
||||
# Find the tool
|
||||
tool = tool_map.get(tool_call["name"])
|
||||
|
||||
if not tool:
|
||||
result = f"Tool {tool_call['name']} not found"
|
||||
else:
|
||||
try:
|
||||
# Execute the tool
|
||||
result = tool.invoke(tool_call["args"])
|
||||
except Exception as e:
|
||||
result = f"Error: {str(e)}"
|
||||
|
||||
# Create ToolMessage
|
||||
tool_results.append(
|
||||
ToolMessage(
|
||||
content=str(result),
|
||||
tool_call_id=tool_call["id"]
|
||||
)
|
||||
)
|
||||
|
||||
return {"messages": tool_results}
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Basic Error Handling
|
||||
|
||||
```python
|
||||
def robust_tool_node(state: MessagesState):
|
||||
"""Tool node with error handling"""
|
||||
last_message = state["messages"][-1]
|
||||
tool_results = []
|
||||
|
||||
for tool_call in last_message.tool_calls:
|
||||
try:
|
||||
tool = tool_map[tool_call["name"]]
|
||||
result = tool.invoke(tool_call["args"])
|
||||
|
||||
tool_results.append(
|
||||
ToolMessage(
|
||||
content=str(result),
|
||||
tool_call_id=tool_call["id"]
|
||||
)
|
||||
)
|
||||
|
||||
except KeyError:
|
||||
# Tool not found
|
||||
tool_results.append(
|
||||
ToolMessage(
|
||||
content=f"Error: Tool '{tool_call['name']}' not found",
|
||||
tool_call_id=tool_call["id"]
|
||||
)
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
# Execution error
|
||||
tool_results.append(
|
||||
ToolMessage(
|
||||
content=f"Error executing tool: {str(e)}",
|
||||
tool_call_id=tool_call["id"]
|
||||
)
|
||||
)
|
||||
|
||||
return {"messages": tool_results}
|
||||
```
|
||||
|
||||
### Retry Logic
|
||||
|
||||
```python
|
||||
import time
|
||||
|
||||
def tool_node_with_retry(state: MessagesState, max_retries: int = 3):
|
||||
"""Tool node with retry"""
|
||||
last_message = state["messages"][-1]
|
||||
tool_results = []
|
||||
|
||||
for tool_call in last_message.tool_calls:
|
||||
tool = tool_map[tool_call["name"]]
|
||||
retry_count = 0
|
||||
|
||||
while retry_count < max_retries:
|
||||
try:
|
||||
result = tool.invoke(tool_call["args"])
|
||||
|
||||
tool_results.append(
|
||||
ToolMessage(
|
||||
content=str(result),
|
||||
tool_call_id=tool_call["id"]
|
||||
)
|
||||
)
|
||||
break
|
||||
|
||||
except TransientError as e:
|
||||
retry_count += 1
|
||||
if retry_count >= max_retries:
|
||||
tool_results.append(
|
||||
ToolMessage(
|
||||
content=f"Failed after {max_retries} retries: {str(e)}",
|
||||
tool_call_id=tool_call["id"]
|
||||
)
|
||||
)
|
||||
else:
|
||||
time.sleep(2 ** retry_count) # Exponential backoff
|
||||
|
||||
except Exception as e:
|
||||
# Non-retryable error
|
||||
tool_results.append(
|
||||
ToolMessage(
|
||||
content=f"Error: {str(e)}",
|
||||
tool_call_id=tool_call["id"]
|
||||
)
|
||||
)
|
||||
break
|
||||
|
||||
return {"messages": tool_results}
|
||||
```
|
||||
|
||||
## Conditional Tool Execution
|
||||
|
||||
```python
|
||||
def conditional_tool_node(state: MessagesState, *, store):
|
||||
"""Tool node with permission checking"""
|
||||
user_id = state.get("user_id")
|
||||
user = store.get(("users", user_id), "profile")
|
||||
|
||||
last_message = state["messages"][-1]
|
||||
tool_results = []
|
||||
|
||||
for tool_call in last_message.tool_calls:
|
||||
tool = tool_map[tool_call["name"]]
|
||||
|
||||
# Permission check
|
||||
if not has_permission(user, tool.name):
|
||||
tool_results.append(
|
||||
ToolMessage(
|
||||
content=f"Permission denied for tool '{tool.name}'",
|
||||
tool_call_id=tool_call["id"]
|
||||
)
|
||||
)
|
||||
continue
|
||||
|
||||
# Execute
|
||||
result = tool.invoke(tool_call["args"])
|
||||
tool_results.append(
|
||||
ToolMessage(
|
||||
content=str(result),
|
||||
tool_call_id=tool_call["id"]
|
||||
)
|
||||
)
|
||||
|
||||
return {"messages": tool_results}
|
||||
```
|
||||
|
||||
## Logging Tool Execution
|
||||
|
||||
```python
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def logged_tool_node(state: MessagesState):
|
||||
"""Tool node with logging"""
|
||||
last_message = state["messages"][-1]
|
||||
tool_results = []
|
||||
|
||||
for tool_call in last_message.tool_calls:
|
||||
tool = tool_map[tool_call["name"]]
|
||||
|
||||
logger.info(
|
||||
f"Executing tool: {tool.name}",
|
||||
extra={
|
||||
"tool": tool.name,
|
||||
"args": tool_call["args"],
|
||||
"call_id": tool_call["id"]
|
||||
}
|
||||
)
|
||||
|
||||
try:
|
||||
start = time.time()
|
||||
result = tool.invoke(tool_call["args"])
|
||||
duration = time.time() - start
|
||||
|
||||
logger.info(
|
||||
f"Tool completed: {tool.name}",
|
||||
extra={
|
||||
"tool": tool.name,
|
||||
"duration": duration,
|
||||
"success": True
|
||||
}
|
||||
)
|
||||
|
||||
tool_results.append(
|
||||
ToolMessage(
|
||||
content=str(result),
|
||||
tool_call_id=tool_call["id"]
|
||||
)
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Tool failed: {tool.name}",
|
||||
extra={
|
||||
"tool": tool.name,
|
||||
"error": str(e)
|
||||
},
|
||||
exc_info=True
|
||||
)
|
||||
|
||||
tool_results.append(
|
||||
ToolMessage(
|
||||
content=f"Error: {str(e)}",
|
||||
tool_call_id=tool_call["id"]
|
||||
)
|
||||
)
|
||||
|
||||
return {"messages": tool_results}
|
||||
```
|
||||
|
||||
## Parallel Tool Execution
|
||||
|
||||
```python
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
|
||||
def parallel_tool_node(state: MessagesState):
|
||||
"""Execute tools in parallel"""
|
||||
last_message = state["messages"][-1]
|
||||
|
||||
def execute_tool(tool_call):
|
||||
tool = tool_map[tool_call["name"]]
|
||||
try:
|
||||
result = tool.invoke(tool_call["args"])
|
||||
return ToolMessage(
|
||||
content=str(result),
|
||||
tool_call_id=tool_call["id"]
|
||||
)
|
||||
except Exception as e:
|
||||
return ToolMessage(
|
||||
content=f"Error: {str(e)}",
|
||||
tool_call_id=tool_call["id"]
|
||||
)
|
||||
|
||||
with ThreadPoolExecutor(max_workers=5) as executor:
|
||||
tool_results = list(executor.map(
|
||||
execute_tool,
|
||||
last_message.tool_calls
|
||||
))
|
||||
|
||||
return {"messages": tool_results}
|
||||
```
|
||||
|
||||
## Summary
|
||||
|
||||
ToolNode executes tools and returns results as ToolMessage. You can add error handling, permission checks, logging, and more.
|
||||
|
||||
## Related Pages
|
||||
|
||||
- [04_tool_integration_tool_definition.md](04_tool_integration_tool_definition.md) - Tool definition
|
||||
- [04_tool_integration_command_api.md](04_tool_integration_command_api.md) - Integration with Command API
|
||||
- [05_advanced_features_human_in_the_loop.md](05_advanced_features_human_in_the_loop.md) - Combining with approval flows
|
||||
Reference in New Issue
Block a user