[READY][PROD-215][toolkits/ZENDESK]updating error handling (#578)
# [PROD-215](https://app.clickup.com/t/9014390315/PROD-215) 🎫 ## Changes - Removed some try catch blocks from Zendesk tools so the default Httpx error adaptor can deal with them. Co-authored-by: Francisco Liberal <francisco@arcade.dev>
This commit is contained in:
parent
be0e0f39d7
commit
b446390acf
4 changed files with 115 additions and 307 deletions
|
|
@ -4,7 +4,7 @@ from typing import Annotated, Any
|
||||||
import httpx
|
import httpx
|
||||||
from arcade_tdk import ToolContext, tool
|
from arcade_tdk import ToolContext, tool
|
||||||
from arcade_tdk.auth import OAuth2
|
from arcade_tdk.auth import OAuth2
|
||||||
from arcade_tdk.errors import RetryableToolError, ToolExecutionError
|
from arcade_tdk.errors import RetryableToolError
|
||||||
|
|
||||||
from arcade_zendesk.enums import ArticleSortBy, SortOrder
|
from arcade_zendesk.enums import ArticleSortBy, SortOrder
|
||||||
from arcade_zendesk.utils import (
|
from arcade_zendesk.utils import (
|
||||||
|
|
@ -175,7 +175,6 @@ async def search_articles(
|
||||||
base_params["sort_order"] = sort_order.value
|
base_params["sort_order"] = sort_order.value
|
||||||
|
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
try:
|
|
||||||
headers = {
|
headers = {
|
||||||
"Authorization": f"Bearer {auth_token}",
|
"Authorization": f"Bearer {auth_token}",
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
|
|
@ -198,30 +197,4 @@ async def search_articles(
|
||||||
|
|
||||||
logger.info(f"Article search returned {data.get('count', 0)} results")
|
logger.info(f"Article search returned {data.get('count', 0)} results")
|
||||||
|
|
||||||
except httpx.HTTPStatusError as e:
|
|
||||||
logger.exception(f"HTTP error during article search: {e.response.status_code}")
|
|
||||||
raise ToolExecutionError(
|
|
||||||
message=f"Failed to search articles: HTTP {e.response.status_code}",
|
|
||||||
developer_message=(
|
|
||||||
f"HTTP {e.response.status_code} error: {e.response.text}. "
|
|
||||||
f"URL: {url}, base_params: {base_params}"
|
|
||||||
),
|
|
||||||
) from e
|
|
||||||
except httpx.TimeoutException as e:
|
|
||||||
logger.exception("Timeout during article search")
|
|
||||||
raise RetryableToolError(
|
|
||||||
message="Request timed out while searching articles.",
|
|
||||||
developer_message=f"Timeout occurred. URL: {url}, base_params: {base_params}",
|
|
||||||
retry_after_ms=5000,
|
|
||||||
) from e
|
|
||||||
except Exception as e:
|
|
||||||
logger.exception("Unexpected error during article search")
|
|
||||||
raise ToolExecutionError(
|
|
||||||
message=f"Failed to search articles: {e!s}",
|
|
||||||
developer_message=(
|
|
||||||
f"Unexpected error: {type(e).__name__}: {e!s}. "
|
|
||||||
f"URL: {url}, base_params: {base_params}"
|
|
||||||
),
|
|
||||||
) from e
|
|
||||||
else:
|
|
||||||
return data
|
return data
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ from typing import Annotated, Any
|
||||||
import httpx
|
import httpx
|
||||||
from arcade_tdk import ToolContext, tool
|
from arcade_tdk import ToolContext, tool
|
||||||
from arcade_tdk.auth import OAuth2
|
from arcade_tdk.auth import OAuth2
|
||||||
from arcade_tdk.errors import RetryableToolError, ToolExecutionError
|
from arcade_tdk.errors import RetryableToolError
|
||||||
|
|
||||||
from arcade_zendesk.enums import SortOrder, TicketStatus
|
from arcade_zendesk.enums import SortOrder, TicketStatus
|
||||||
from arcade_zendesk.utils import fetch_paginated_results, get_zendesk_subdomain
|
from arcade_zendesk.utils import fetch_paginated_results, get_zendesk_subdomain
|
||||||
|
|
@ -94,7 +94,6 @@ async def list_tickets(
|
||||||
|
|
||||||
# Make the API request
|
# Make the API request
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
try:
|
|
||||||
headers = {
|
headers = {
|
||||||
"Authorization": f"Bearer {token}",
|
"Authorization": f"Bearer {token}",
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
|
|
@ -114,9 +113,7 @@ async def list_tickets(
|
||||||
tickets = data.get("results", [])
|
tickets = data.get("results", [])
|
||||||
for ticket in tickets:
|
for ticket in tickets:
|
||||||
if "id" in ticket:
|
if "id" in ticket:
|
||||||
ticket["html_url"] = (
|
ticket["html_url"] = f"https://{subdomain}.zendesk.com/agent/tickets/{ticket['id']}"
|
||||||
f"https://{subdomain}.zendesk.com/agent/tickets/{ticket['id']}"
|
|
||||||
)
|
|
||||||
# Remove API url to avoid confusion
|
# Remove API url to avoid confusion
|
||||||
if "url" in ticket:
|
if "url" in ticket:
|
||||||
del ticket["url"]
|
del ticket["url"]
|
||||||
|
|
@ -130,31 +127,6 @@ async def list_tickets(
|
||||||
# Add next_offset if present
|
# Add next_offset if present
|
||||||
if "next_offset" in data:
|
if "next_offset" in data:
|
||||||
result["next_offset"] = data["next_offset"]
|
result["next_offset"] = data["next_offset"]
|
||||||
|
|
||||||
except httpx.HTTPStatusError as e:
|
|
||||||
raise ToolExecutionError(
|
|
||||||
message=f"Failed to list tickets: HTTP {e.response.status_code}",
|
|
||||||
developer_message=(
|
|
||||||
f"HTTP {e.response.status_code} error: {e.response.text}. "
|
|
||||||
f"URL: {url}, params: {base_params}"
|
|
||||||
),
|
|
||||||
) from e
|
|
||||||
except httpx.TimeoutException as e:
|
|
||||||
raise RetryableToolError(
|
|
||||||
message="Request timed out while listing tickets.",
|
|
||||||
developer_message=f"Timeout occurred. URL: {url}, params: {base_params}",
|
|
||||||
retry_after_ms=5000,
|
|
||||||
additional_prompt_content="Try reducing limit or using more specific filters.",
|
|
||||||
) from e
|
|
||||||
except Exception as e:
|
|
||||||
raise ToolExecutionError(
|
|
||||||
message=f"Failed to list tickets: {e!s}",
|
|
||||||
developer_message=(
|
|
||||||
f"Unexpected error: {type(e).__name__}: {e!s}. "
|
|
||||||
f"URL: {url}, params: {base_params}"
|
|
||||||
),
|
|
||||||
) from e
|
|
||||||
else:
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -190,7 +162,6 @@ async def get_ticket_comments(
|
||||||
|
|
||||||
# Make the API request
|
# Make the API request
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
try:
|
|
||||||
headers = {
|
headers = {
|
||||||
"Authorization": f"Bearer {token}",
|
"Authorization": f"Bearer {token}",
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
|
|
@ -209,29 +180,6 @@ async def get_ticket_comments(
|
||||||
"count": len(comments),
|
"count": len(comments),
|
||||||
}
|
}
|
||||||
|
|
||||||
except RetryableToolError:
|
|
||||||
# Re-raise our custom errors
|
|
||||||
raise
|
|
||||||
except httpx.HTTPStatusError as e:
|
|
||||||
raise ToolExecutionError(
|
|
||||||
message=f"Failed to get ticket comments: HTTP {e.response.status_code}",
|
|
||||||
developer_message=(
|
|
||||||
f"HTTP {e.response.status_code} error: {e.response.text}. URL: {url}"
|
|
||||||
),
|
|
||||||
) from e
|
|
||||||
except httpx.TimeoutException as e:
|
|
||||||
raise RetryableToolError(
|
|
||||||
message="Request timed out while getting ticket comments.",
|
|
||||||
developer_message=f"Timeout occurred. URL: {url}",
|
|
||||||
retry_after_ms=5000,
|
|
||||||
additional_prompt_content="Try again in a few moments.",
|
|
||||||
) from e
|
|
||||||
except Exception as e:
|
|
||||||
raise ToolExecutionError(
|
|
||||||
message=f"Failed to get ticket comments: {e!s}",
|
|
||||||
developer_message=f"Unexpected error: {type(e).__name__}: {e!s}. URL: {url}",
|
|
||||||
) from e
|
|
||||||
|
|
||||||
|
|
||||||
@tool(
|
@tool(
|
||||||
requires_auth=OAuth2(id="zendesk", scopes=["tickets:write"]),
|
requires_auth=OAuth2(id="zendesk", scopes=["tickets:write"]),
|
||||||
|
|
@ -265,7 +213,6 @@ async def add_ticket_comment(
|
||||||
|
|
||||||
# Make the API request
|
# Make the API request
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
try:
|
|
||||||
headers = {
|
headers = {
|
||||||
"Authorization": f"Bearer {token}",
|
"Authorization": f"Bearer {token}",
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
|
|
@ -285,30 +232,6 @@ async def add_ticket_comment(
|
||||||
if "url" in ticket:
|
if "url" in ticket:
|
||||||
del ticket["url"]
|
del ticket["url"]
|
||||||
|
|
||||||
except RetryableToolError:
|
|
||||||
# Re-raise our custom errors
|
|
||||||
raise
|
|
||||||
except httpx.HTTPStatusError as e:
|
|
||||||
raise ToolExecutionError(
|
|
||||||
message=f"Failed to add ticket comment: HTTP {e.response.status_code}",
|
|
||||||
developer_message=(
|
|
||||||
f"HTTP {e.response.status_code} error: {e.response.text}. "
|
|
||||||
f"URL: {url}, body: {request_body}"
|
|
||||||
),
|
|
||||||
) from e
|
|
||||||
except httpx.TimeoutException as e:
|
|
||||||
raise RetryableToolError(
|
|
||||||
message="Request timed out while adding ticket comment.",
|
|
||||||
developer_message=f"Timeout occurred. URL: {url}",
|
|
||||||
retry_after_ms=5000,
|
|
||||||
additional_prompt_content="Try again in a few moments.",
|
|
||||||
) from e
|
|
||||||
except Exception as e:
|
|
||||||
raise ToolExecutionError(
|
|
||||||
message=f"Failed to add ticket comment: {e!s}",
|
|
||||||
developer_message=f"Unexpected error: {type(e).__name__}: {e!s}. URL: {url}",
|
|
||||||
) from e
|
|
||||||
else:
|
|
||||||
return {
|
return {
|
||||||
"success": True,
|
"success": True,
|
||||||
"ticket_id": ticket_id,
|
"ticket_id": ticket_id,
|
||||||
|
|
@ -357,7 +280,6 @@ async def mark_ticket_solved(
|
||||||
|
|
||||||
# Make the API request
|
# Make the API request
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
try:
|
|
||||||
headers = {
|
headers = {
|
||||||
"Authorization": f"Bearer {token}",
|
"Authorization": f"Bearer {token}",
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
|
|
@ -387,28 +309,4 @@ async def mark_ticket_solved(
|
||||||
result["comment_added"] = True
|
result["comment_added"] = True
|
||||||
result["comment_type"] = "public" if comment_public else "internal"
|
result["comment_type"] = "public" if comment_public else "internal"
|
||||||
|
|
||||||
except RetryableToolError:
|
|
||||||
# Re-raise our custom errors
|
|
||||||
raise
|
|
||||||
except httpx.HTTPStatusError as e:
|
|
||||||
raise ToolExecutionError(
|
|
||||||
message=f"Failed to mark ticket as solved: HTTP {e.response.status_code}",
|
|
||||||
developer_message=(
|
|
||||||
f"HTTP {e.response.status_code} error: {e.response.text}. "
|
|
||||||
f"URL: {url}, body: {request_body}"
|
|
||||||
),
|
|
||||||
) from e
|
|
||||||
except httpx.TimeoutException as e:
|
|
||||||
raise RetryableToolError(
|
|
||||||
message="Request timed out while marking ticket as solved.",
|
|
||||||
developer_message=f"Timeout occurred. URL: {url}",
|
|
||||||
retry_after_ms=5000,
|
|
||||||
additional_prompt_content="Try again in a few moments.",
|
|
||||||
) from e
|
|
||||||
except Exception as e:
|
|
||||||
raise ToolExecutionError(
|
|
||||||
message=f"Failed to mark ticket as solved: {e!s}",
|
|
||||||
developer_message=f"Unexpected error: {type(e).__name__}: {e!s}. URL: {url}",
|
|
||||||
) from e
|
|
||||||
else:
|
|
||||||
return result
|
return result
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,10 @@ build-backend = "hatchling.build"
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "arcade_zendesk"
|
name = "arcade_zendesk"
|
||||||
version = "0.2.0"
|
version = "0.3.0"
|
||||||
requires-python = ">=3.10"
|
requires-python = ">=3.10"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arcade-tdk>=2.0.0,<3.0.0",
|
"arcade-tdk>=2.3.1,<3.0.0",
|
||||||
"httpx>=0.25.0,<1.0.0",
|
"httpx>=0.25.0,<1.0.0",
|
||||||
"beautifulsoup4>=4.0.0,<5"
|
"beautifulsoup4>=4.0.0,<5"
|
||||||
]
|
]
|
||||||
|
|
@ -15,8 +15,8 @@ dependencies = [
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
dev = [
|
dev = [
|
||||||
"arcade-ai[evals]>=2.0.0,<3.0.0",
|
"arcade-ai[evals]>=2.2.1,<3.0.0",
|
||||||
"arcade-serve>=2.0.0,<3.0.0",
|
"arcade-serve>=2.1.0,<3.0.0",
|
||||||
"pytest>=8.3.0,<8.4.0",
|
"pytest>=8.3.0,<8.4.0",
|
||||||
"pytest-cov>=4.0.0,<4.1.0",
|
"pytest-cov>=4.0.0,<4.1.0",
|
||||||
"pytest-mock>=3.11.1,<3.12.0",
|
"pytest-mock>=3.11.1,<3.12.0",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,3 @@
|
||||||
from unittest.mock import MagicMock
|
|
||||||
|
|
||||||
import httpx
|
|
||||||
import pytest
|
import pytest
|
||||||
from arcade_tdk.errors import RetryableToolError, ToolExecutionError
|
from arcade_tdk.errors import RetryableToolError, ToolExecutionError
|
||||||
|
|
||||||
|
|
@ -312,66 +309,6 @@ class TestSearchArticlesPagination:
|
||||||
assert "next_offset" not in result # No more results
|
assert "next_offset" not in result # No more results
|
||||||
|
|
||||||
|
|
||||||
class TestSearchArticlesErrors:
|
|
||||||
"""Test error handling scenarios."""
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
"status_code,error_key",
|
|
||||||
[
|
|
||||||
(400, "HTTP 400"),
|
|
||||||
(401, "HTTP 401"),
|
|
||||||
(403, "HTTP 403"),
|
|
||||||
(404, "HTTP 404"),
|
|
||||||
(500, "HTTP 500"),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_http_errors(self, mock_context, mock_httpx_client, status_code, error_key):
|
|
||||||
"""Test handling of HTTP errors."""
|
|
||||||
mock_context.get_secret.return_value = "test-subdomain"
|
|
||||||
|
|
||||||
# Create mock error response
|
|
||||||
error_response = MagicMock()
|
|
||||||
error_response.status_code = status_code
|
|
||||||
error_response.text = f"Error message for {status_code}"
|
|
||||||
error_response.raise_for_status.side_effect = httpx.HTTPStatusError(
|
|
||||||
message=f"HTTP {status_code}", request=MagicMock(), response=error_response
|
|
||||||
)
|
|
||||||
|
|
||||||
mock_httpx_client.get.return_value = error_response
|
|
||||||
|
|
||||||
with pytest.raises(ToolExecutionError) as exc_info:
|
|
||||||
await search_articles(context=mock_context, query="test")
|
|
||||||
|
|
||||||
assert "Failed to search articles" in str(exc_info.value.message)
|
|
||||||
assert f"HTTP {status_code}" in str(exc_info.value.message)
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_timeout_error(self, mock_context, mock_httpx_client):
|
|
||||||
"""Test handling of timeout errors."""
|
|
||||||
mock_context.get_secret.return_value = "test-subdomain"
|
|
||||||
|
|
||||||
mock_httpx_client.get.side_effect = httpx.TimeoutException("Request timed out")
|
|
||||||
|
|
||||||
with pytest.raises(RetryableToolError) as exc_info:
|
|
||||||
await search_articles(context=mock_context, query="test")
|
|
||||||
|
|
||||||
assert "timed out" in str(exc_info.value.message)
|
|
||||||
assert exc_info.value.retry_after_ms == 5000
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_unexpected_error(self, mock_context, mock_httpx_client):
|
|
||||||
"""Test handling of unexpected errors."""
|
|
||||||
mock_context.get_secret.return_value = "test-subdomain"
|
|
||||||
|
|
||||||
mock_httpx_client.get.side_effect = Exception("Unexpected error occurred")
|
|
||||||
|
|
||||||
with pytest.raises(ToolExecutionError) as exc_info:
|
|
||||||
await search_articles(context=mock_context, query="test")
|
|
||||||
|
|
||||||
assert "Unexpected error occurred" in str(exc_info.value.message)
|
|
||||||
|
|
||||||
|
|
||||||
class TestSearchArticlesContentProcessing:
|
class TestSearchArticlesContentProcessing:
|
||||||
"""Test content processing and formatting."""
|
"""Test content processing and formatting."""
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue