Skip to content

Commit 985a75a

Browse files
authored
fix(batch): Return human-readable results for agent display (#273)
- Add dual output format: human-readable text + structured JSON - Fix misleading 'Tool missing' error messages - Preserve original tool results while adding batch metadata - Enable agents to display individual tool results instead of generic success message - Fix typo: 'Sammple' -> 'Sample' in documentation - Update tests to match new output format Resolves issue where agents only showed 'batch call executed successfully' instead of actual tool results from parallel executions.
1 parent b65dd11 commit 985a75a

File tree

2 files changed

+150
-37
lines changed

2 files changed

+150
-37
lines changed

src/strands_tools/batch.py

Lines changed: 61 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ def batch(tool: ToolUse, **kwargs) -> ToolResult:
7979
- If a tool function is not found or an error occurs, it will be captured in the results.
8080
- This tool is designed to work with agents that support dynamic tool invocation.
8181
82-
Sammple output:
82+
Sample output:
8383
{
8484
"status": "success",
8585
"results": [
@@ -96,41 +96,83 @@ def batch(tool: ToolUse, **kwargs) -> ToolResult:
9696
agent = kwargs.get("agent")
9797
invocations = kwargs.get("invocations", [])
9898
results = []
99+
99100
try:
100101
if not hasattr(agent, "tool") or agent.tool is None:
101102
raise AttributeError("Agent does not have a valid 'tool' attribute.")
103+
102104
for invocation in invocations:
103105
tool_name = invocation.get("name")
104106
arguments = invocation.get("arguments", {})
105107
tool_fn = getattr(agent.tool, tool_name, None)
108+
106109
if callable(tool_fn):
107110
try:
108-
# Only pass JSON-serializable arguments to the tool
111+
# Call the tool function with the provided arguments
109112
result = tool_fn(**arguments)
110-
111-
if result["status"] == "success":
112-
results.append({"json": {"name": tool_name, "status": "success", "result": result}})
113-
else:
114-
results.append(
115-
{"toolUseId": tool_use_id, "status": "error", "content": [{"text": "Tool missing"}]}
116-
)
113+
114+
# Create a consistent result structure
115+
batch_result = {
116+
"name": tool_name,
117+
"status": "success",
118+
"result": result
119+
}
120+
results.append(batch_result)
121+
117122
except Exception as e:
118-
error_msg = f"Error in batch tool: {str(e)}\n{traceback.format_exc()}"
119-
console.print(f"Error in batch tool: {str(e)}")
120-
results.append({"toolUseId": tool_use_id, "status": "error", "content": [{"text": error_msg}]})
121-
else:
122-
results.append(
123-
{
124-
"toolUseId": tool_use_id,
123+
error_msg = f"Error executing tool '{tool_name}': {str(e)}"
124+
console.print(error_msg)
125+
126+
batch_result = {
127+
"name": tool_name,
125128
"status": "error",
126-
"content": [{"text": f"Tool '{tool_name}' not found in agent or tool call failed."}],
129+
"error": str(e),
130+
"traceback": traceback.format_exc()
127131
}
128-
)
132+
results.append(batch_result)
133+
else:
134+
error_msg = f"Tool '{tool_name}' not found in agent"
135+
console.print(error_msg)
136+
137+
batch_result = {
138+
"name": tool_name,
139+
"status": "error",
140+
"error": error_msg
141+
}
142+
results.append(batch_result)
143+
144+
# Create a readable summary for the agent
145+
summary_lines = []
146+
summary_lines.append(f"Batch execution completed with {len(results)} tool(s):")
147+
148+
for result in results:
149+
if result["status"] == "success":
150+
summary_lines.append(f"✓ {result['name']}: Success")
151+
else:
152+
summary_lines.append(f"✗ {result['name']}: Error - {result['error']}")
153+
154+
summary_text = "\n".join(summary_lines)
155+
129156
return {
130157
"toolUseId": tool_use_id,
131158
"status": "success",
132-
"content": results,
159+
"content": [
160+
{
161+
"text": summary_text
162+
},
163+
{
164+
"json": {
165+
"batch_summary": {
166+
"total_tools": len(results),
167+
"successful": len([r for r in results if r["status"] == "success"]),
168+
"failed": len([r for r in results if r["status"] == "error"])
169+
},
170+
"results": results
171+
}
172+
}
173+
]
133174
}
175+
134176
except Exception as e:
135177
error_msg = f"Error in batch tool: {str(e)}\n{traceback.format_exc()}"
136178
console.print(f"Error in batch tool: {str(e)}")

tests/test_batch.py

Lines changed: 89 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,29 @@
88
def mock_agent():
99
"""Fixture to create a mock agent with tools."""
1010
agent = MagicMock()
11-
agent.tool.http_request = MagicMock(return_value={"status": "success", "result": {"ip": "127.0.0.1"}})
12-
agent.tool.use_aws = MagicMock(return_value={"status": "success", "result": {"buckets": ["bucket1", "bucket2"]}})
13-
agent.tool.error_tool = MagicMock(side_effect=Exception("Tool execution failed"))
11+
12+
# Create a mock tool registry that mimics the real agent's tool access pattern
13+
mock_tool_registry = MagicMock()
14+
mock_tool_registry.registry = {
15+
"http_request": MagicMock(return_value={"status": "success", "result": {"ip": "127.0.0.1"}}),
16+
"use_aws": MagicMock(return_value={"status": "success", "result": {"buckets": ["bucket1", "bucket2"]}}),
17+
"error_tool": MagicMock(side_effect=Exception("Tool execution failed"))
18+
}
19+
agent.tool_registry = mock_tool_registry
20+
21+
# Create a custom mock tool object that properly handles getattr
22+
class MockTool:
23+
def __init__(self):
24+
self.http_request = mock_tool_registry.registry["http_request"]
25+
self.use_aws = mock_tool_registry.registry["use_aws"]
26+
self.error_tool = mock_tool_registry.registry["error_tool"]
27+
28+
def __getattr__(self, name):
29+
# Return None for non-existent tools (this will make callable() return False)
30+
return None
31+
32+
agent.tool = MockTool()
33+
1434
return agent
1535

1636

@@ -27,12 +47,26 @@ def test_batch_success(mock_agent):
2747
assert result["toolUseId"] == "mock_tool_id"
2848
assert result["status"] == "success"
2949
assert len(result["content"]) == 2
30-
assert result["content"][0]["json"]["name"] == "http_request"
31-
assert result["content"][0]["json"]["status"] == "success"
32-
assert result["content"][0]["json"]["result"]["result"]["ip"] == "127.0.0.1"
33-
assert result["content"][1]["json"]["name"] == "use_aws"
34-
assert result["content"][1]["json"]["status"] == "success"
35-
assert result["content"][1]["json"]["result"]["result"]["buckets"] == ["bucket1", "bucket2"]
50+
51+
# Check the summary text
52+
assert "Batch execution completed with 2 tool(s):" in result["content"][0]["text"]
53+
assert "✓ http_request: Success" in result["content"][0]["text"]
54+
assert "✓ use_aws: Success" in result["content"][0]["text"]
55+
56+
# Check the JSON results
57+
json_content = result["content"][1]["json"]
58+
assert json_content["batch_summary"]["total_tools"] == 2
59+
assert json_content["batch_summary"]["successful"] == 2
60+
assert json_content["batch_summary"]["failed"] == 0
61+
62+
results = json_content["results"]
63+
assert len(results) == 2
64+
assert results[0]["name"] == "http_request"
65+
assert results[0]["status"] == "success"
66+
assert results[0]["result"]["result"]["ip"] == "127.0.0.1"
67+
assert results[1]["name"] == "use_aws"
68+
assert results[1]["status"] == "success"
69+
assert results[1]["result"]["result"]["buckets"] == ["bucket1", "bucket2"]
3670

3771

3872
def test_batch_missing_tool(mock_agent):
@@ -46,10 +80,23 @@ def test_batch_missing_tool(mock_agent):
4680

4781
assert result["toolUseId"] == "mock_tool_id"
4882
assert result["status"] == "success"
49-
assert len(result["content"]) == 1
50-
assert result["content"][0]["toolUseId"] == "mock_tool_id"
51-
assert result["content"][0]["status"] == "error"
52-
assert "Tool missing" in result["content"][0]["content"][0]["text"]
83+
assert len(result["content"]) == 2
84+
85+
# Check the summary text
86+
assert "Batch execution completed with 1 tool(s):" in result["content"][0]["text"]
87+
assert "✗ non_existent_tool: Error" in result["content"][0]["text"]
88+
89+
# Check the JSON results
90+
json_content = result["content"][1]["json"]
91+
assert json_content["batch_summary"]["total_tools"] == 1
92+
assert json_content["batch_summary"]["successful"] == 0
93+
assert json_content["batch_summary"]["failed"] == 1
94+
95+
results = json_content["results"]
96+
assert len(results) == 1
97+
assert results[0]["name"] == "non_existent_tool"
98+
assert results[0]["status"] == "error"
99+
assert "not found in agent" in results[0]["error"]
53100

54101

55102
def test_batch_tool_error(mock_agent):
@@ -63,10 +110,24 @@ def test_batch_tool_error(mock_agent):
63110

64111
assert result["toolUseId"] == "mock_tool_id"
65112
assert result["status"] == "success"
66-
assert len(result["content"]) == 1
67-
assert result["content"][0]["toolUseId"] == "mock_tool_id"
68-
assert result["content"][0]["status"] == "error"
69-
assert "Error in batch tool" in result["content"][0]["content"][0]["text"]
113+
assert len(result["content"]) == 2
114+
115+
# Check the summary text
116+
assert "Batch execution completed with 1 tool(s):" in result["content"][0]["text"]
117+
assert "✗ error_tool: Error" in result["content"][0]["text"]
118+
119+
# Check the JSON results
120+
json_content = result["content"][1]["json"]
121+
assert json_content["batch_summary"]["total_tools"] == 1
122+
assert json_content["batch_summary"]["successful"] == 0
123+
assert json_content["batch_summary"]["failed"] == 1
124+
125+
results = json_content["results"]
126+
assert len(results) == 1
127+
assert results[0]["name"] == "error_tool"
128+
assert results[0]["status"] == "error"
129+
assert "Tool execution failed" in results[0]["error"]
130+
assert "traceback" in results[0]
70131

71132

72133
def test_batch_no_invocations(mock_agent):
@@ -78,7 +139,17 @@ def test_batch_no_invocations(mock_agent):
78139

79140
assert result["toolUseId"] == "mock_tool_id"
80141
assert result["status"] == "success"
81-
assert len(result["content"]) == 0
142+
assert len(result["content"]) == 2
143+
144+
# Check the summary text
145+
assert "Batch execution completed with 0 tool(s):" in result["content"][0]["text"]
146+
147+
# Check the JSON results
148+
json_content = result["content"][1]["json"]
149+
assert json_content["batch_summary"]["total_tools"] == 0
150+
assert json_content["batch_summary"]["successful"] == 0
151+
assert json_content["batch_summary"]["failed"] == 0
152+
assert len(json_content["results"]) == 0
82153

83154

84155
def test_batch_top_level_error(mock_agent):

0 commit comments

Comments
 (0)