diff --git a/toolkits/zendesk/arcade_zendesk/tools/search_articles.py b/toolkits/zendesk/arcade_zendesk/tools/search_articles.py index b47115be..d07dd437 100644 --- a/toolkits/zendesk/arcade_zendesk/tools/search_articles.py +++ b/toolkits/zendesk/arcade_zendesk/tools/search_articles.py @@ -4,7 +4,7 @@ from typing import Annotated, Any import httpx from arcade_tdk import ToolContext, tool 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.utils import ( @@ -175,53 +175,26 @@ async def search_articles( base_params["sort_order"] = sort_order.value async with httpx.AsyncClient() as client: - try: - headers = { - "Authorization": f"Bearer {auth_token}", - "Content-Type": "application/json", - "Accept": "application/json", - } + headers = { + "Authorization": f"Bearer {auth_token}", + "Content-Type": "application/json", + "Accept": "application/json", + } - data = await fetch_paginated_results( - client=client, - url=url, - headers=headers, - params=base_params, - offset=offset, - limit=limit, + data = await fetch_paginated_results( + client=client, + url=url, + headers=headers, + params=base_params, + offset=offset, + limit=limit, + ) + + if "results" in data: + data["results"] = process_search_results( + data["results"], include_body=include_body, max_body_length=max_article_length ) - if "results" in data: - data["results"] = process_search_results( - data["results"], include_body=include_body, max_body_length=max_article_length - ) + 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 diff --git a/toolkits/zendesk/arcade_zendesk/tools/tickets.py b/toolkits/zendesk/arcade_zendesk/tools/tickets.py index 814917f0..84e7ab6e 100644 --- a/toolkits/zendesk/arcade_zendesk/tools/tickets.py +++ b/toolkits/zendesk/arcade_zendesk/tools/tickets.py @@ -3,7 +3,7 @@ from typing import Annotated, Any import httpx from arcade_tdk import ToolContext, tool 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.utils import fetch_paginated_results, get_zendesk_subdomain @@ -94,68 +94,40 @@ async def list_tickets( # Make the API request async with httpx.AsyncClient() as client: - try: - headers = { - "Authorization": f"Bearer {token}", - "Content-Type": "application/json", - } + headers = { + "Authorization": f"Bearer {token}", + "Content-Type": "application/json", + } - # Use the fetch_paginated_results utility - data = await fetch_paginated_results( - client=client, - url=url, - headers=headers, - params=base_params, - offset=offset, - limit=limit, - ) + # Use the fetch_paginated_results utility + data = await fetch_paginated_results( + client=client, + url=url, + headers=headers, + params=base_params, + offset=offset, + limit=limit, + ) - # Process tickets to add html_url and remove api url - tickets = data.get("results", []) - for ticket in tickets: - if "id" in ticket: - ticket["html_url"] = ( - f"https://{subdomain}.zendesk.com/agent/tickets/{ticket['id']}" - ) - # Remove API url to avoid confusion - if "url" in ticket: - del ticket["url"] + # Process tickets to add html_url and remove api url + tickets = data.get("results", []) + for ticket in tickets: + if "id" in ticket: + ticket["html_url"] = f"https://{subdomain}.zendesk.com/agent/tickets/{ticket['id']}" + # Remove API url to avoid confusion + if "url" in ticket: + del ticket["url"] - # Build the result with consistent structure - result = { - "tickets": tickets, - "count": data.get("count", len(tickets)), - } + # Build the result with consistent structure + result = { + "tickets": tickets, + "count": data.get("count", len(tickets)), + } - # Add next_offset if present - if "next_offset" in data: - 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 + # Add next_offset if present + if "next_offset" in data: + result["next_offset"] = data["next_offset"] + return result @tool( @@ -190,47 +162,23 @@ async def get_ticket_comments( # Make the API request async with httpx.AsyncClient() as client: - try: - headers = { - "Authorization": f"Bearer {token}", - "Content-Type": "application/json", - } + headers = { + "Authorization": f"Bearer {token}", + "Content-Type": "application/json", + } - response = await client.get(url, headers=headers) - _handle_ticket_not_found(response, ticket_id) - response.raise_for_status() + response = await client.get(url, headers=headers) + _handle_ticket_not_found(response, ticket_id) + response.raise_for_status() - data = response.json() - comments = data.get("comments", []) + data = response.json() + comments = data.get("comments", []) - return { - "ticket_id": ticket_id, - "comments": 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 + return { + "ticket_id": ticket_id, + "comments": comments, + "count": len(comments), + } @tool( @@ -265,56 +213,31 @@ async def add_ticket_comment( # Make the API request async with httpx.AsyncClient() as client: - try: - headers = { - "Authorization": f"Bearer {token}", - "Content-Type": "application/json", - } + headers = { + "Authorization": f"Bearer {token}", + "Content-Type": "application/json", + } - response = await client.put(url, headers=headers, json=request_body) - _handle_ticket_not_found(response, ticket_id) - response.raise_for_status() + response = await client.put(url, headers=headers, json=request_body) + _handle_ticket_not_found(response, ticket_id) + response.raise_for_status() - data = response.json() - ticket = data.get("ticket", {}) + data = response.json() + ticket = data.get("ticket", {}) - # Add web interface URL if not present - if "id" in ticket and "html_url" not in ticket: - ticket["html_url"] = f"https://{subdomain}.zendesk.com/agent/tickets/{ticket['id']}" - # Remove API url to avoid confusion - if "url" in ticket: - del ticket["url"] + # Add web interface URL if not present + if "id" in ticket and "html_url" not in ticket: + ticket["html_url"] = f"https://{subdomain}.zendesk.com/agent/tickets/{ticket['id']}" + # Remove API url to avoid confusion + if "url" in ticket: + 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 { - "success": True, - "ticket_id": ticket_id, - "comment_type": "public" if public else "internal", - "ticket": ticket, - } + return { + "success": True, + "ticket_id": ticket_id, + "comment_type": "public" if public else "internal", + "ticket": ticket, + } @tool( @@ -357,58 +280,33 @@ async def mark_ticket_solved( # Make the API request async with httpx.AsyncClient() as client: - try: - headers = { - "Authorization": f"Bearer {token}", - "Content-Type": "application/json", - } + headers = { + "Authorization": f"Bearer {token}", + "Content-Type": "application/json", + } - response = await client.put(url, headers=headers, json=request_body) - _handle_ticket_not_found(response, ticket_id) - response.raise_for_status() + response = await client.put(url, headers=headers, json=request_body) + _handle_ticket_not_found(response, ticket_id) + response.raise_for_status() - data = response.json() - ticket = data.get("ticket", {}) + data = response.json() + ticket = data.get("ticket", {}) - # Add web interface URL if not present - if "id" in ticket and "html_url" not in ticket: - ticket["html_url"] = f"https://{subdomain}.zendesk.com/agent/tickets/{ticket['id']}" - # Remove API url to avoid confusion - if "url" in ticket: - del ticket["url"] + # Add web interface URL if not present + if "id" in ticket and "html_url" not in ticket: + ticket["html_url"] = f"https://{subdomain}.zendesk.com/agent/tickets/{ticket['id']}" + # Remove API url to avoid confusion + if "url" in ticket: + del ticket["url"] - result = { - "success": True, - "ticket_id": ticket_id, - "status": "solved", - "ticket": ticket, - } - if comment_body: - result["comment_added"] = True - result["comment_type"] = "public" if comment_public else "internal" + result = { + "success": True, + "ticket_id": ticket_id, + "status": "solved", + "ticket": ticket, + } + if comment_body: + result["comment_added"] = True + 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 diff --git a/toolkits/zendesk/pyproject.toml b/toolkits/zendesk/pyproject.toml index ea8fb109..a257e0fd 100644 --- a/toolkits/zendesk/pyproject.toml +++ b/toolkits/zendesk/pyproject.toml @@ -4,10 +4,10 @@ build-backend = "hatchling.build" [project] name = "arcade_zendesk" -version = "0.2.0" +version = "0.3.0" requires-python = ">=3.10" dependencies = [ - "arcade-tdk>=2.0.0,<3.0.0", + "arcade-tdk>=2.3.1,<3.0.0", "httpx>=0.25.0,<1.0.0", "beautifulsoup4>=4.0.0,<5" ] @@ -15,8 +15,8 @@ dependencies = [ [project.optional-dependencies] dev = [ - "arcade-ai[evals]>=2.0.0,<3.0.0", - "arcade-serve>=2.0.0,<3.0.0", + "arcade-ai[evals]>=2.2.1,<3.0.0", + "arcade-serve>=2.1.0,<3.0.0", "pytest>=8.3.0,<8.4.0", "pytest-cov>=4.0.0,<4.1.0", "pytest-mock>=3.11.1,<3.12.0", diff --git a/toolkits/zendesk/tests/test_search_articles.py b/toolkits/zendesk/tests/test_search_articles.py index bb357cd2..0cb49d41 100644 --- a/toolkits/zendesk/tests/test_search_articles.py +++ b/toolkits/zendesk/tests/test_search_articles.py @@ -1,6 +1,3 @@ -from unittest.mock import MagicMock - -import httpx import pytest from arcade_tdk.errors import RetryableToolError, ToolExecutionError @@ -312,66 +309,6 @@ class TestSearchArticlesPagination: 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: """Test content processing and formatting."""