zach commited on
Commit
7854f13
·
1 Parent(s): bf6610d

raise friendlier error message to UI from integration code

Browse files
src/integrations/anthropic_api.py CHANGED
@@ -14,7 +14,7 @@ Key Features:
14
  # Standard Library Imports
15
  import logging
16
  from dataclasses import dataclass, field
17
- from typing import Any, Dict, List, Optional, Union, cast
18
 
19
  # Third-Party Library Imports
20
  from anthropic import APIError
@@ -214,22 +214,47 @@ async def generate_text_with_claude(character_description: str, config: Config)
214
  logger.warning(f"Unexpected response type: {type(blocks)}")
215
  return str(blocks or "No content generated.")
216
 
 
 
 
 
 
 
 
 
 
 
 
 
 
217
  except Exception as e:
218
- # If the error is an APIError, check if it's unretryable.
219
- if isinstance(e, APIError):
220
- status_code: Optional[int] = getattr(e, "status_code", None)
221
- if status_code is not None and CLIENT_ERROR_CODE <= status_code < SERVER_ERROR_CODE:
222
- error_body: Any = e.body
223
- error_message: str = "Unknown error"
224
- if isinstance(error_body, dict):
225
- error_message = cast(Dict[str, Any], error_body).get("error", {}).get("message", "Unknown error")
226
- raise UnretryableAnthropicError(
227
- message=f'"{error_message}"',
228
- original_exception=e,
229
- ) from e
230
-
231
- # For all other errors, wrap them in an AnthropicError.
232
- raise AnthropicError(
233
- message=str(e),
234
- original_exception=e,
235
- ) from e
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  # Standard Library Imports
15
  import logging
16
  from dataclasses import dataclass, field
17
+ from typing import List, Optional, Union
18
 
19
  # Third-Party Library Imports
20
  from anthropic import APIError
 
214
  logger.warning(f"Unexpected response type: {type(blocks)}")
215
  return str(blocks or "No content generated.")
216
 
217
+ except APIError as e:
218
+ logger.error(f"Anthropic API request failed: {e!s}")
219
+ clean_message = _extract_anthropic_error_message(e)
220
+
221
+ if (
222
+ hasattr(e, 'status_code')
223
+ and e.status_code is not None
224
+ and CLIENT_ERROR_CODE <= e.status_code < SERVER_ERROR_CODE
225
+ ):
226
+ raise UnretryableAnthropicError(message=clean_message, original_exception=e) from e
227
+
228
+ raise AnthropicError(message=clean_message, original_exception=e) from e
229
+
230
  except Exception as e:
231
+ error_type = type(e).__name__
232
+ error_message = str(e) if str(e) else f"An error of type {error_type} occurred"
233
+ logger.error(f"Error during Anthropic API call: {error_type} - {error_message}")
234
+ clean_message = "An unexpected error occurred while processing your request. Please try again later."
235
+
236
+ raise AnthropicError(message=clean_message, original_exception=e) from e
237
+
238
+
239
+ def _extract_anthropic_error_message(e: APIError) -> str:
240
+ """
241
+ Extracts a clean, user-friendly error message from an Anthropic API error response.
242
+
243
+ Args:
244
+ e (APIError): The Anthropic API error exception containing response information.
245
+
246
+ Returns:
247
+ str: A clean, user-friendly error message suitable for display to end users.
248
+ """
249
+ clean_message = "An unknown error has occurred. Please try again later."
250
+
251
+ if hasattr(e, 'body') and isinstance(e.body, dict):
252
+ error_body = e.body
253
+ if (
254
+ 'error' in error_body
255
+ and isinstance(error_body['error'], dict)
256
+ and 'message' in error_body['error']
257
+ ):
258
+ clean_message = error_body['error']['message']
259
+
260
+ return clean_message
src/integrations/elevenlabs_api.py CHANGED
@@ -128,18 +128,44 @@ async def text_to_speech_with_elevenlabs(
128
 
129
  return None, audio_file_path
130
 
 
 
 
 
 
 
 
 
 
131
  except Exception as e:
132
- if (
133
- isinstance(e, ApiError)
134
- and e.status_code is not None
135
- and CLIENT_ERROR_CODE <= e.status_code < SERVER_ERROR_CODE
136
- ):
137
- raise UnretryableElevenLabsError(
138
- message=f"{e.body['detail']['message']}",
139
- original_exception=e,
140
- ) from e
141
-
142
- raise ElevenLabsError(
143
- message=f"{e}",
144
- original_exception=e,
145
- ) from e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
128
 
129
  return None, audio_file_path
130
 
131
+ except ApiError as e:
132
+ logger.error(f"ElevenLabs API request failed: {e!s}")
133
+ clean_message = _extract_elevenlabs_error_message(e)
134
+
135
+ if e.status_code is not None and CLIENT_ERROR_CODE <= e.status_code < SERVER_ERROR_CODE:
136
+ raise UnretryableElevenLabsError(message=clean_message, original_exception=e) from e
137
+
138
+ raise ElevenLabsError(message=clean_message, original_exception=e) from e
139
+
140
  except Exception as e:
141
+ error_type = type(e).__name__
142
+ error_message = str(e) if str(e) else f"An error of type {error_type} occurred"
143
+ logger.error(f"Error during ElevenLabs API call: {error_type} - {error_message}")
144
+ clean_message = "An unexpected error occurred while processing your speech request. Please try again later."
145
+
146
+ raise ElevenLabsError(message=error_message, original_exception=e) from e
147
+
148
+
149
+ def _extract_elevenlabs_error_message(e: ApiError) -> str:
150
+ """
151
+ Extracts a clean, user-friendly error message from an ElevenLabs API error response.
152
+
153
+ Args:
154
+ e (ApiError): The ElevenLabs API error exception containing response information.
155
+
156
+ Returns:
157
+ str: A clean, user-friendly error message suitable for display to end users.
158
+ """
159
+ clean_message = "An unknown error has occurred. Please try again later."
160
+
161
+ if (
162
+ hasattr(e, 'body') and e.body
163
+ and isinstance(e.body, dict)
164
+ and 'detail' in e.body
165
+ and isinstance(e.body['detail'], dict)
166
+ ):
167
+ detail = e.body['detail']
168
+ if 'message' in detail:
169
+ clean_message = detail['message']
170
+
171
+ return clean_message
src/integrations/hume_api.py CHANGED
@@ -77,30 +77,26 @@ class UnretryableHumeError(HumeError):
77
  async def text_to_speech_with_hume(
78
  character_description: str,
79
  text: str,
80
- num_generations: int,
81
  config: Config,
82
- ) -> Union[Tuple[str, str], Tuple[str, str, str, str]]:
83
  """
84
  Asynchronously synthesizes speech using the Hume TTS API, processes audio data, and writes audio to a file.
85
 
86
  This function uses the Hume Python SDK to send a request to the Hume TTS API with a character description
87
- and text to be converted to speech. Depending on the specified number of generations (1 or 2), the API
88
- returns one or two generations. For each generation, the function extracts the base64-encoded audio
89
- and generation ID, saves the audio as an MP3 file, and returns the relevant details.
90
 
91
  Args:
92
  character_description (str): Description used for voice synthesis.
93
  text (str): Text to be converted to speech.
94
- num_generations (int): Number of audio generations to request (1 or 2).
95
  config (Config): Application configuration containing Hume API settings.
96
 
97
  Returns:
98
- Union[Tuple[str, str], Tuple[str, str, str, str]]:
99
- - If num_generations == 1: (generation_a_id, audio_a_path).
100
- - If num_generations == 2: (generation_a_id, audio_a_path, generation_b_id, audio_b_path).
101
 
102
  Raises:
103
- ValueError: If num_generations is not 1 or 2.
104
  HumeError: For errors communicating with the Hume API.
105
  UnretryableHumeError: For client-side HTTP errors (status code 4xx).
106
  """
@@ -110,30 +106,23 @@ async def text_to_speech_with_hume(
110
  f"Text length: {len(text)}."
111
  )
112
 
113
- if num_generations < 1 or num_generations > 2:
114
- raise ValueError("Invalid number of generations specified. Must be 1 or 2.")
115
-
116
  hume_config = config.hume_config
117
 
118
  start_time = time.time()
119
  try:
120
- # Initialize the client for this request
121
  hume_client = AsyncHumeClient(
122
  api_key=hume_config.api_key,
123
  timeout=hume_config.request_timeout
124
  )
125
 
126
- # Create the utterance with the character description and text
127
  utterance = PostedUtterance(
128
  text=text,
129
  description=character_description or None
130
  )
131
 
132
- # Call the TTS API through the SDK
133
  response: ReturnTts = await hume_client.tts.synthesize_json(
134
  utterances=[utterance],
135
  format=hume_config.file_format,
136
- num_generations=num_generations
137
  )
138
 
139
  elapsed_time = time.time() - start_time
@@ -148,31 +137,22 @@ async def text_to_speech_with_hume(
148
  generation_a = generations[0]
149
  generation_a_id, audio_a_path = _parse_hume_tts_generation(generation_a, config)
150
 
151
- if num_generations == 1:
152
- return (generation_a_id, audio_a_path)
153
-
154
- generation_b = generations[1]
155
- generation_b_id, audio_b_path = _parse_hume_tts_generation(generation_b, config)
156
- return (generation_a_id, audio_a_path, generation_b_id, audio_b_path)
157
 
158
  except ApiError as e:
159
  elapsed_time = time.time() - start_time
160
  logger.error(f"Hume API request failed after {elapsed_time:.2f} seconds: {e!s}")
 
 
161
 
162
- if hasattr(e, 'status_code') and e.status_code is not None:
163
- if CLIENT_ERROR_CODE <= e.status_code < SERVER_ERROR_CODE:
164
- error_message = f"HTTP Error {e.status_code}: {e!s}"
165
- logger.error(error_message)
166
- raise UnretryableHumeError(message=error_message, original_exception=e) from e
167
- error_message = f"HTTP Error {e.status_code}: {e!s}"
168
- else:
169
- error_message = str(e)
170
-
171
- logger.error(error_message)
172
- raise HumeError(
173
- message=error_message,
174
- original_exception=e,
175
- ) from e
176
 
177
  except Exception as e:
178
  error_type = type(e).__name__
@@ -205,3 +185,21 @@ def _parse_hume_tts_generation(generation: ReturnGeneration, config: Config) ->
205
  filename = f"{generation.generation_id}.mp3"
206
  audio_file_path = save_base64_audio_to_file(generation.audio, filename, config)
207
  return generation.generation_id, audio_file_path
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77
  async def text_to_speech_with_hume(
78
  character_description: str,
79
  text: str,
 
80
  config: Config,
81
+ ) -> Tuple[str, str]:
82
  """
83
  Asynchronously synthesizes speech using the Hume TTS API, processes audio data, and writes audio to a file.
84
 
85
  This function uses the Hume Python SDK to send a request to the Hume TTS API with a character description
86
+ and text to be converted to speech. It extracts the base64-encoded audio and generation ID from the response,
87
+ saves the audio as an MP3 file, and returns the relevant details.
 
88
 
89
  Args:
90
  character_description (str): Description used for voice synthesis.
91
  text (str): Text to be converted to speech.
 
92
  config (Config): Application configuration containing Hume API settings.
93
 
94
  Returns:
95
+ Tuple[str, str]: A tuple containing:
96
+ - generation_id (str): Unique identifier for the generated audio.
97
+ - audio_file_path (str): Path to the saved audio file.
98
 
99
  Raises:
 
100
  HumeError: For errors communicating with the Hume API.
101
  UnretryableHumeError: For client-side HTTP errors (status code 4xx).
102
  """
 
106
  f"Text length: {len(text)}."
107
  )
108
 
 
 
 
109
  hume_config = config.hume_config
110
 
111
  start_time = time.time()
112
  try:
 
113
  hume_client = AsyncHumeClient(
114
  api_key=hume_config.api_key,
115
  timeout=hume_config.request_timeout
116
  )
117
 
 
118
  utterance = PostedUtterance(
119
  text=text,
120
  description=character_description or None
121
  )
122
 
 
123
  response: ReturnTts = await hume_client.tts.synthesize_json(
124
  utterances=[utterance],
125
  format=hume_config.file_format,
 
126
  )
127
 
128
  elapsed_time = time.time() - start_time
 
137
  generation_a = generations[0]
138
  generation_a_id, audio_a_path = _parse_hume_tts_generation(generation_a, config)
139
 
140
+ return (generation_a_id, audio_a_path)
 
 
 
 
 
141
 
142
  except ApiError as e:
143
  elapsed_time = time.time() - start_time
144
  logger.error(f"Hume API request failed after {elapsed_time:.2f} seconds: {e!s}")
145
+ clean_message = _extract_hume_api_error_message(e)
146
+ logger.error(f"Full Hume API error: {e!s}")
147
 
148
+ if (
149
+ hasattr(e, 'status_code')
150
+ and e.status_code is not None
151
+ and CLIENT_ERROR_CODE <= e.status_code < SERVER_ERROR_CODE
152
+ ):
153
+ raise UnretryableHumeError(message=clean_message, original_exception=e) from e
154
+
155
+ raise HumeError(message=clean_message, original_exception=e) from e
 
 
 
 
 
 
156
 
157
  except Exception as e:
158
  error_type = type(e).__name__
 
185
  filename = f"{generation.generation_id}.mp3"
186
  audio_file_path = save_base64_audio_to_file(generation.audio, filename, config)
187
  return generation.generation_id, audio_file_path
188
+
189
+
190
+ def _extract_hume_api_error_message(e: ApiError) -> str:
191
+ """
192
+ Extracts a clean, user-friendly error message from a Hume API error response.
193
+
194
+ Args:
195
+ e (ApiError): The Hume API error exception containing response information.
196
+
197
+ Returns:
198
+ str: A clean, user-friendly error message suitable for display to end users.
199
+ """
200
+ clean_message = "An unknown error has occurred. Please try again later."
201
+
202
+ if hasattr(e, 'body') and isinstance(e.body, dict) and 'message' in e.body:
203
+ clean_message = e.body['message']
204
+
205
+ return clean_message