From 25bfb16936e2fecc10754a4dbb98842e40112f78 Mon Sep 17 00:00:00 2001 From: Denis Shulyaka Date: Sat, 28 Mar 2026 17:40:03 +0300 Subject: [PATCH] Exception translations for Anthropic integration (#166723) --- .../components/anthropic/__init__.py | 16 ++++++- homeassistant/components/anthropic/ai_task.py | 7 ++- homeassistant/components/anthropic/entity.py | 46 +++++++++++++++---- .../components/anthropic/quality_scale.yaml | 2 +- homeassistant/components/anthropic/repairs.py | 8 +++- .../components/anthropic/strings.json | 41 +++++++++++++++++ .../components/anthropic/test_conversation.py | 2 +- tests/components/anthropic/test_init.py | 2 +- 8 files changed, 105 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/anthropic/__init__.py b/homeassistant/components/anthropic/__init__.py index e479c1836ec..9011ad21e42 100644 --- a/homeassistant/components/anthropic/__init__.py +++ b/homeassistant/components/anthropic/__init__.py @@ -45,9 +45,21 @@ async def async_setup_entry(hass: HomeAssistant, entry: AnthropicConfigEntry) -> try: await client.models.list(timeout=10.0) except anthropic.AuthenticationError as err: - raise ConfigEntryAuthFailed(err) from err + raise ConfigEntryAuthFailed( + translation_domain=DOMAIN, + translation_key="api_authentication_error", + translation_placeholders={"message": err.message}, + ) from err except anthropic.AnthropicError as err: - raise ConfigEntryNotReady(err) from err + raise ConfigEntryNotReady( + translation_domain=DOMAIN, + translation_key="api_error", + translation_placeholders={ + "message": err.message + if isinstance(err, anthropic.APIError) + else str(err) + }, + ) from err entry.runtime_data = client diff --git a/homeassistant/components/anthropic/ai_task.py b/homeassistant/components/anthropic/ai_task.py index 8701e28577e..5445b654397 100644 --- a/homeassistant/components/anthropic/ai_task.py +++ b/homeassistant/components/anthropic/ai_task.py @@ -12,6 +12,7 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.util.json import json_loads +from .const import DOMAIN from .entity import AnthropicBaseLLMEntity if TYPE_CHECKING: @@ -60,7 +61,7 @@ class AnthropicTaskEntity( if not isinstance(chat_log.content[-1], conversation.AssistantContent): raise HomeAssistantError( - "Last content in chat log is not an AssistantContent" + translation_domain=DOMAIN, translation_key="response_not_found" ) text = chat_log.content[-1].content or "" @@ -78,7 +79,9 @@ class AnthropicTaskEntity( err, text, ) - raise HomeAssistantError("Error with Claude structured response") from err + raise HomeAssistantError( + translation_domain=DOMAIN, translation_key="json_parse_error" + ) from err return ai_task.GenDataTaskResult( conversation_id=chat_log.conversation_id, diff --git a/homeassistant/components/anthropic/entity.py b/homeassistant/components/anthropic/entity.py index 38a99cc39d9..94c8616d010 100644 --- a/homeassistant/components/anthropic/entity.py +++ b/homeassistant/components/anthropic/entity.py @@ -401,7 +401,11 @@ def _convert_content( messages[-1]["content"] = messages[-1]["content"][0]["text"] else: # Note: We don't pass SystemContent here as it's passed to the API as the prompt - raise HomeAssistantError("Unexpected content type in chat log") + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="unexpected_chat_log_content", + translation_placeholders={"type": type(content).__name__}, + ) return messages, container_id @@ -443,7 +447,9 @@ async def _transform_stream( # noqa: C901 - This is complex, but better to have Each message could contain multiple blocks of the same type. """ if stream is None or not hasattr(stream, "__aiter__"): - raise HomeAssistantError("Expected a stream of messages") + raise HomeAssistantError( + translation_domain=DOMAIN, translation_key="unexpected_stream_object" + ) current_tool_block: ToolUseBlockParam | ServerToolUseBlockParam | None = None current_tool_args: str @@ -605,7 +611,9 @@ async def _transform_stream( # noqa: C901 - This is complex, but better to have chat_log.async_trace(_create_token_stats(input_usage, usage)) content_details.container = response.delta.container if response.delta.stop_reason == "refusal": - raise HomeAssistantError("Potential policy violation detected") + raise HomeAssistantError( + translation_domain=DOMAIN, translation_key="api_refusal" + ) elif isinstance(response, RawMessageStopEvent): if content_details: content_details.delete_empty() @@ -664,7 +672,9 @@ class AnthropicBaseLLMEntity(Entity): system = chat_log.content[0] if not isinstance(system, conversation.SystemContent): - raise HomeAssistantError("First message must be a system message") + raise HomeAssistantError( + translation_domain=DOMAIN, translation_key="system_message_not_found" + ) # System prompt with caching enabled system_prompt: list[TextBlockParam] = [ @@ -754,7 +764,7 @@ class AnthropicBaseLLMEntity(Entity): last_message = messages[-1] if last_message["role"] != "user": raise HomeAssistantError( - "Last message must be a user message to add attachments" + translation_domain=DOMAIN, translation_key="user_message_not_found" ) if isinstance(last_message["content"], str): last_message["content"] = [ @@ -859,11 +869,19 @@ class AnthropicBaseLLMEntity(Entity): except anthropic.AuthenticationError as err: self.entry.async_start_reauth(self.hass) raise HomeAssistantError( - "Authentication error with Anthropic API, reauthentication required" + translation_domain=DOMAIN, + translation_key="api_authentication_error", + translation_placeholders={"message": err.message}, ) from err except anthropic.AnthropicError as err: raise HomeAssistantError( - f"Sorry, I had a problem talking to Anthropic: {err}" + translation_domain=DOMAIN, + translation_key="api_error", + translation_placeholders={ + "message": err.message + if isinstance(err, anthropic.APIError) + else str(err) + }, ) from err if not chat_log.unresponded_tool_results: @@ -883,15 +901,23 @@ async def async_prepare_files_for_prompt( for file_path, mime_type in files: if not file_path.exists(): - raise HomeAssistantError(f"`{file_path}` does not exist") + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="wrong_file_path", + translation_placeholders={"file_path": file_path.as_posix()}, + ) if mime_type is None: mime_type = guess_file_type(file_path)[0] if not mime_type or not mime_type.startswith(("image/", "application/pdf")): raise HomeAssistantError( - "Only images and PDF are supported by the Anthropic API," - f"`{file_path}` is not an image file or PDF" + translation_domain=DOMAIN, + translation_key="wrong_file_type", + translation_placeholders={ + "file_path": file_path.as_posix(), + "mime_type": mime_type or "unknown", + }, ) if mime_type == "image/jpg": mime_type = "image/jpeg" diff --git a/homeassistant/components/anthropic/quality_scale.yaml b/homeassistant/components/anthropic/quality_scale.yaml index 37f605b1532..142285df629 100644 --- a/homeassistant/components/anthropic/quality_scale.yaml +++ b/homeassistant/components/anthropic/quality_scale.yaml @@ -88,7 +88,7 @@ rules: comment: | No entities disabled by default. entity-translations: todo - exception-translations: todo + exception-translations: done icon-translations: done reconfiguration-flow: done repair-issues: done diff --git a/homeassistant/components/anthropic/repairs.py b/homeassistant/components/anthropic/repairs.py index 4594967d379..ac78e690eba 100644 --- a/homeassistant/components/anthropic/repairs.py +++ b/homeassistant/components/anthropic/repairs.py @@ -161,7 +161,9 @@ class ModelDeprecatedRepairFlow(RepairsFlow): is None or (subentry := entry.subentries.get(self._current_subentry_id)) is None ): - raise HomeAssistantError("Subentry not found") + raise HomeAssistantError( + translation_domain=DOMAIN, translation_key="subentry_not_found" + ) updated_data = { **subentry.data, @@ -190,4 +192,6 @@ async def async_create_fix_flow( """Create flow.""" if issue_id == "model_deprecated": return ModelDeprecatedRepairFlow() - raise HomeAssistantError("Unknown issue ID") + raise HomeAssistantError( + translation_domain=DOMAIN, translation_key="unknown_issue_id" + ) diff --git a/homeassistant/components/anthropic/strings.json b/homeassistant/components/anthropic/strings.json index 4e34085a09c..72b15fbe2dd 100644 --- a/homeassistant/components/anthropic/strings.json +++ b/homeassistant/components/anthropic/strings.json @@ -149,6 +149,47 @@ } } }, + "exceptions": { + "api_authentication_error": { + "message": "Authentication error with Anthropic API: {message}. Reauthentication required." + }, + "api_error": { + "message": "Anthropic API error: {message}." + }, + "api_refusal": { + "message": "Potential policy violation detected." + }, + "json_parse_error": { + "message": "Error with Claude structured response." + }, + "response_not_found": { + "message": "Last content in chat log is not an AssistantContent." + }, + "subentry_not_found": { + "message": "Subentry not found." + }, + "system_message_not_found": { + "message": "First message must be a system message." + }, + "unexpected_chat_log_content": { + "message": "Unexpected content type in chat log: {type}." + }, + "unexpected_stream_object": { + "message": "Expected a stream of messages." + }, + "unknown_issue_id": { + "message": "Unknown issue ID." + }, + "user_message_not_found": { + "message": "Last message must be a user message to add attachments." + }, + "wrong_file_path": { + "message": "`{file_path}` does not exist." + }, + "wrong_file_type": { + "message": "Only images and PDF are supported by the Anthropic API, `{file_path}` ({mime_type}) is not an image file or PDF." + } + }, "issues": { "model_deprecated": { "fix_flow": { diff --git a/tests/components/anthropic/test_conversation.py b/tests/components/anthropic/test_conversation.py index 3ff1ea5fb2d..c753adb5d36 100644 --- a/tests/components/anthropic/test_conversation.py +++ b/tests/components/anthropic/test_conversation.py @@ -645,7 +645,7 @@ async def test_double_system_messages( assert result.response.error_code == "unknown" assert ( result.response.speech["plain"]["speech"] - == "Unexpected content type in chat log" + == "Unexpected content type in chat log: SystemContent" ) diff --git a/tests/components/anthropic/test_init.py b/tests/components/anthropic/test_init.py index 26dcc6d130c..a004e0e1a57 100644 --- a/tests/components/anthropic/test_init.py +++ b/tests/components/anthropic/test_init.py @@ -43,7 +43,7 @@ from tests.common import MockConfigEntry ), body={"type": "error", "error": {"type": "invalid_request_error"}}, ), - "anthropic integration not ready yet: Your credit balance is too low to access the Claude API", + "Your credit balance is too low to access the Claude API", ), ], )