mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Python: fix(claude): handle API errors in run_stream() method (#3653)
* fix(claude): handle API errors in run_stream() method - Import AssistantMessage and TextBlock from claude_agent_sdk - Check AssistantMessage.error and raise ServiceException with descriptive message - Check ResultMessage.is_error and raise ServiceException with error details - Add tests for error handling in run_stream() Fixes #3652 * fix: add defensive check for message.content before iterating Address PR review feedback - add null check for message.content to prevent potential AttributeError if content is None. * chore: refresh uv.lock * chore: fix import sorting * chore: refresh uv.lock
This commit is contained in:
committed by
GitHub
Unverified
parent
0daa7700c6
commit
9e51e2f0bc
@@ -23,15 +23,16 @@ from agent_framework import (
|
||||
from agent_framework._types import normalize_tools
|
||||
from agent_framework.exceptions import ServiceException, ServiceInitializationError
|
||||
from claude_agent_sdk import (
|
||||
ClaudeAgentOptions as SDKOptions,
|
||||
)
|
||||
from claude_agent_sdk import (
|
||||
AssistantMessage,
|
||||
ClaudeSDKClient,
|
||||
ResultMessage,
|
||||
SdkMcpTool,
|
||||
create_sdk_mcp_server,
|
||||
)
|
||||
from claude_agent_sdk.types import StreamEvent
|
||||
from claude_agent_sdk import (
|
||||
ClaudeAgentOptions as SDKOptions,
|
||||
)
|
||||
from claude_agent_sdk.types import StreamEvent, TextBlock
|
||||
from pydantic import ValidationError
|
||||
|
||||
from ._settings import ClaudeAgentSettings
|
||||
@@ -639,7 +640,33 @@ class ClaudeAgent(BaseAgent, Generic[TOptions]):
|
||||
contents=[Content.from_text_reasoning(text=thinking, raw_representation=message)],
|
||||
raw_representation=message,
|
||||
)
|
||||
elif isinstance(message, AssistantMessage):
|
||||
# Handle AssistantMessage - check for API errors
|
||||
# Note: In streaming mode, the content was already yielded via StreamEvent,
|
||||
# so we only check for errors here, not re-emit content.
|
||||
if message.error:
|
||||
# Map error types to descriptive messages
|
||||
error_messages = {
|
||||
"authentication_failed": "Authentication failed with Claude API",
|
||||
"billing_error": "Billing error with Claude API",
|
||||
"rate_limit": "Rate limit exceeded for Claude API",
|
||||
"invalid_request": "Invalid request to Claude API",
|
||||
"server_error": "Claude API server error",
|
||||
"unknown": "Unknown error from Claude API",
|
||||
}
|
||||
error_msg = error_messages.get(message.error, f"Claude API error: {message.error}")
|
||||
# Extract any error details from content blocks
|
||||
if message.content:
|
||||
for block in message.content:
|
||||
if isinstance(block, TextBlock):
|
||||
error_msg = f"{error_msg}: {block.text}"
|
||||
break
|
||||
raise ServiceException(error_msg)
|
||||
elif isinstance(message, ResultMessage):
|
||||
# Check for errors in result message
|
||||
if message.is_error:
|
||||
error_msg = message.result or "Unknown error from Claude API"
|
||||
raise ServiceException(f"Claude API error: {error_msg}")
|
||||
session_id = message.session_id
|
||||
|
||||
# Update thread with session ID
|
||||
|
||||
@@ -379,6 +379,61 @@ class TestClaudeAgentRunStream:
|
||||
assert updates[0].text == "Streaming "
|
||||
assert updates[1].text == "response"
|
||||
|
||||
async def test_run_stream_raises_on_assistant_message_error(self) -> None:
|
||||
"""Test run_stream raises ServiceException when AssistantMessage has an error."""
|
||||
from agent_framework.exceptions import ServiceException
|
||||
from claude_agent_sdk import AssistantMessage, ResultMessage, TextBlock
|
||||
|
||||
messages = [
|
||||
AssistantMessage(
|
||||
content=[TextBlock(text="Error details from API")],
|
||||
model="claude-sonnet",
|
||||
error="invalid_request",
|
||||
),
|
||||
ResultMessage(
|
||||
subtype="success",
|
||||
duration_ms=100,
|
||||
duration_api_ms=50,
|
||||
is_error=False,
|
||||
num_turns=1,
|
||||
session_id="error-session",
|
||||
),
|
||||
]
|
||||
mock_client = self._create_mock_client(messages)
|
||||
|
||||
with patch("agent_framework_claude._agent.ClaudeSDKClient", return_value=mock_client):
|
||||
agent = ClaudeAgent()
|
||||
with pytest.raises(ServiceException) as exc_info:
|
||||
async for _ in agent.run_stream("Hello"):
|
||||
pass
|
||||
assert "Invalid request to Claude API" in str(exc_info.value)
|
||||
assert "Error details from API" in str(exc_info.value)
|
||||
|
||||
async def test_run_stream_raises_on_result_message_error(self) -> None:
|
||||
"""Test run_stream raises ServiceException when ResultMessage.is_error is True."""
|
||||
from agent_framework.exceptions import ServiceException
|
||||
from claude_agent_sdk import ResultMessage
|
||||
|
||||
messages = [
|
||||
ResultMessage(
|
||||
subtype="error",
|
||||
duration_ms=100,
|
||||
duration_api_ms=50,
|
||||
is_error=True,
|
||||
num_turns=0,
|
||||
session_id="error-session",
|
||||
result="Model 'claude-sonnet-4.5' not found",
|
||||
),
|
||||
]
|
||||
mock_client = self._create_mock_client(messages)
|
||||
|
||||
with patch("agent_framework_claude._agent.ClaudeSDKClient", return_value=mock_client):
|
||||
agent = ClaudeAgent()
|
||||
with pytest.raises(ServiceException) as exc_info:
|
||||
async for _ in agent.run_stream("Hello"):
|
||||
pass
|
||||
assert "Model 'claude-sonnet-4.5' not found" in str(exc_info.value)
|
||||
|
||||
|
||||
# region Test ClaudeAgent Session Management
|
||||
|
||||
|
||||
Reference in New Issue
Block a user