|
1 | 1 | from email.utils import parsedate_to_datetime
|
2 |
| -from json import JSONDecodeError |
3 | 2 | from time import time
|
4 | 3 | from typing import Any, Literal, Optional, Union
|
5 | 4 |
|
6 | 5 | from httpx import Response
|
7 |
| -from pydantic import BaseModel |
| 6 | +from pydantic import BaseModel, ValidationError |
| 7 | +from typing_extensions import override |
8 | 8 |
|
9 | 9 | from workflowai.core.domain import tool_call
|
10 | 10 |
|
|
79 | 79 |
|
80 | 80 | class BaseError(BaseModel):
|
81 | 81 | details: Optional[dict[str, Any]] = None
|
82 |
| - message: str |
| 82 | + message: str = "Unknown error" |
83 | 83 | status_code: Optional[int] = None
|
84 | 84 | code: Optional[ErrorCode] = None
|
85 | 85 |
|
@@ -127,41 +127,29 @@ def __str__(self):
|
127 | 127 | return f"WorkflowAIError : [{self.error.code}] ({self.error.status_code}): [{self.error.message}]"
|
128 | 128 |
|
129 | 129 | @classmethod
|
130 |
| - def error_cls(cls, code: str): |
| 130 | + def error_cls(cls, status_code: int, code: Optional[str] = None): |
| 131 | + if status_code == 401: |
| 132 | + return InvalidAPIKeyError |
131 | 133 | if code == "invalid_generation" or code == "failed_generation" or code == "agent_run_failed":
|
132 | 134 | return InvalidGenerationError
|
133 | 135 | return cls
|
134 | 136 |
|
135 | 137 | @classmethod
|
136 |
| - def from_response(cls, response: Response): |
| 138 | + def from_response(cls, response: Response, data: Union[bytes, str, None] = None): |
137 | 139 | try:
|
138 |
| - response_json = response.json() |
139 |
| - r_error = response_json.get("error", {}) |
140 |
| - error_message = response_json.get("detail", {}) or r_error.get("message", "Unknown Error") |
141 |
| - details = r_error.get("details", {}) |
142 |
| - error_code = r_error.get("code", "unknown_error") |
143 |
| - status_code = response.status_code |
144 |
| - run_id = response_json.get("id", None) |
145 |
| - partial_output = response_json.get("task_output", None) |
146 |
| - except JSONDecodeError: |
147 |
| - error_message = "Unknown error" |
148 |
| - details = {"raw": response.content.decode()} |
149 |
| - error_code = "unknown_error" |
150 |
| - status_code = response.status_code |
151 |
| - run_id = None |
152 |
| - partial_output = None |
153 |
| - |
154 |
| - return cls.error_cls(error_code)( |
155 |
| - response=response, |
156 |
| - error=BaseError( |
157 |
| - message=error_message, |
158 |
| - details=details, |
159 |
| - status_code=status_code, |
160 |
| - code=error_code, |
161 |
| - ), |
162 |
| - run_id=run_id, |
163 |
| - partial_output=partial_output, |
164 |
| - ) |
| 140 | + res = ErrorResponse.model_validate_json(data or response.content) |
| 141 | + error_cls = cls.error_cls(response.status_code, res.error.code) |
| 142 | + return error_cls(error=res.error, run_id=res.id, response=response, partial_output=res.task_output) |
| 143 | + except ValidationError: |
| 144 | + return cls.error_cls(response.status_code)( |
| 145 | + error=BaseError( |
| 146 | + message="Unknown error", |
| 147 | + details={ |
| 148 | + "raw": str(data), |
| 149 | + }, |
| 150 | + ), |
| 151 | + response=response, |
| 152 | + ) |
165 | 153 |
|
166 | 154 | @property
|
167 | 155 | def retry_after_delay_seconds(self) -> Optional[float]:
|
@@ -194,3 +182,17 @@ class InvalidGenerationError(WorkflowAIError): ...
|
194 | 182 |
|
195 | 183 |
|
196 | 184 | class MaxTurnsReachedError(WorkflowAIError): ...
|
| 185 | + |
| 186 | + |
| 187 | +class InvalidAPIKeyError(WorkflowAIError): |
| 188 | + @property |
| 189 | + @override |
| 190 | + def message(self) -> str: |
| 191 | + return ( |
| 192 | + "❌ Your API key is invalid. Please double-check your API key, " |
| 193 | + "or create a new one at https://workflowai.com/organization/settings/api-keys " |
| 194 | + "or from your self-hosted WorkflowAI instance." |
| 195 | + ) |
| 196 | + |
| 197 | + def __str__(self) -> str: |
| 198 | + return self.message |
0 commit comments