From d4e98dba38d169a1bdf23e0cfd0c366e0cbd8008 Mon Sep 17 00:00:00 2001 From: Brinlee Kidd Date: Thu, 18 Sep 2025 14:01:35 -0700 Subject: [PATCH] =?UTF-8?q?feat:=20implement=20structured=20exception=20cl?= =?UTF-8?q?asses=20with=20error=20codes=20and=20sug=E2=80=A6=20(#3279)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mem0/client/main.py | 211 ++++++++++++++--- mem0/client/project.py | 121 ++++++++-- mem0/client/utils.py | 96 +++++++- mem0/exceptions.py | 503 +++++++++++++++++++++++++++++++++++++++++ mem0/memory/main.py | 38 +++- 5 files changed, 904 insertions(+), 65 deletions(-) create mode 100644 mem0/exceptions.py diff --git a/mem0/client/main.py b/mem0/client/main.py index 19ff3722c..0e257a045 100644 --- a/mem0/client/main.py +++ b/mem0/client/main.py @@ -9,6 +9,7 @@ import requests from mem0.client.project import AsyncProject, Project from mem0.client.utils import api_error_handler +# Exception classes are referenced in docstrings only from mem0.memory.setup import get_user_id, setup_config from mem0.memory.telemetry import capture_client_event @@ -139,7 +140,12 @@ class MemoryClient: A dictionary containing the API response. Raises: - APIError: If the API request fails. + ValidationError: If the input data is invalid. + AuthenticationError: If authentication fails. + RateLimitError: If rate limits are exceeded. + MemoryQuotaExceededError: If memory quota is exceeded. + NetworkError: If network connectivity issues occur. + MemoryNotFoundError: If the memory doesn't exist (for updates/deletes). """ kwargs = self._prepare_params(kwargs) if kwargs.get("output_format") != "v1.1": @@ -173,7 +179,12 @@ class MemoryClient: A dictionary containing the memory data. Raises: - APIError: If the API request fails. + ValidationError: If the input data is invalid. + AuthenticationError: If authentication fails. + RateLimitError: If rate limits are exceeded. + MemoryQuotaExceededError: If memory quota is exceeded. + NetworkError: If network connectivity issues occur. + MemoryNotFoundError: If the memory doesn't exist (for updates/deletes). """ params = self._prepare_params() response = self.client.get(f"/v1/memories/{memory_id}/", params=params) @@ -194,7 +205,12 @@ class MemoryClient: A list of dictionaries containing memories. Raises: - APIError: If the API request fails. + ValidationError: If the input data is invalid. + AuthenticationError: If authentication fails. + RateLimitError: If rate limits are exceeded. + MemoryQuotaExceededError: If memory quota is exceeded. + NetworkError: If network connectivity issues occur. + MemoryNotFoundError: If the memory doesn't exist (for updates/deletes). """ params = self._prepare_params(kwargs) if version == "v1": @@ -236,7 +252,12 @@ class MemoryClient: A list of dictionaries containing search results. Raises: - APIError: If the API request fails. + ValidationError: If the input data is invalid. + AuthenticationError: If authentication fails. + RateLimitError: If rate limits are exceeded. + MemoryQuotaExceededError: If memory quota is exceeded. + NetworkError: If network connectivity issues occur. + MemoryNotFoundError: If the memory doesn't exist (for updates/deletes). """ payload = {"query": query} params = self._prepare_params(kwargs) @@ -303,7 +324,12 @@ class MemoryClient: A dictionary containing the API response. Raises: - APIError: If the API request fails. + ValidationError: If the input data is invalid. + AuthenticationError: If authentication fails. + RateLimitError: If rate limits are exceeded. + MemoryQuotaExceededError: If memory quota is exceeded. + NetworkError: If network connectivity issues occur. + MemoryNotFoundError: If the memory doesn't exist (for updates/deletes). """ params = self._prepare_params() response = self.client.delete(f"/v1/memories/{memory_id}/", params=params) @@ -323,7 +349,12 @@ class MemoryClient: A dictionary containing the API response. Raises: - APIError: If the API request fails. + ValidationError: If the input data is invalid. + AuthenticationError: If authentication fails. + RateLimitError: If rate limits are exceeded. + MemoryQuotaExceededError: If memory quota is exceeded. + NetworkError: If network connectivity issues occur. + MemoryNotFoundError: If the memory doesn't exist (for updates/deletes). """ params = self._prepare_params(kwargs) response = self.client.delete("/v1/memories/", params=params) @@ -346,7 +377,12 @@ class MemoryClient: A list of dictionaries containing the memory history. Raises: - APIError: If the API request fails. + ValidationError: If the input data is invalid. + AuthenticationError: If authentication fails. + RateLimitError: If rate limits are exceeded. + MemoryQuotaExceededError: If memory quota is exceeded. + NetworkError: If network connectivity issues occur. + MemoryNotFoundError: If the memory doesn't exist (for updates/deletes). """ params = self._prepare_params() response = self.client.get(f"/v1/memories/{memory_id}/history/", params=params) @@ -384,7 +420,10 @@ class MemoryClient: Raises: ValueError: If specified entity not found - APIError: If deletion fails + ValidationError: If the input data is invalid. + AuthenticationError: If authentication fails. + MemoryNotFoundError: If the entity doesn't exist. + NetworkError: If network connectivity issues occur. """ if user_id: @@ -438,10 +477,13 @@ class MemoryClient: Dict[str, str]: Message client reset successful. Raises: - APIError: If the API request fails. + ValidationError: If the input data is invalid. + AuthenticationError: If authentication fails. + RateLimitError: If rate limits are exceeded. + MemoryQuotaExceededError: If memory quota is exceeded. + NetworkError: If network connectivity issues occur. + MemoryNotFoundError: If the memory doesn't exist (for updates/deletes). """ - # Delete all users, agents, and sessions - # This will also delete the memories self.delete_users() capture_client_event("client.reset", self, {"sync_type": "sync"}) @@ -459,6 +501,14 @@ class MemoryClient: Returns: Dict[str, Any]: The response from the server. + + Raises: + ValidationError: If the input data is invalid. + AuthenticationError: If authentication fails. + RateLimitError: If rate limits are exceeded. + MemoryQuotaExceededError: If memory quota is exceeded. + NetworkError: If network connectivity issues occur. + MemoryNotFoundError: If the memory doesn't exist (for updates/deletes). """ response = self.client.put("/v1/batch/", json={"memories": memories}) response.raise_for_status() @@ -479,7 +529,12 @@ class MemoryClient: str: Message indicating the success of the batch deletion. Raises: - APIError: If the API request fails. + ValidationError: If the input data is invalid. + AuthenticationError: If authentication fails. + RateLimitError: If rate limits are exceeded. + MemoryQuotaExceededError: If memory quota is exceeded. + NetworkError: If network connectivity issues occur. + MemoryNotFoundError: If the memory doesn't exist (for updates/deletes). """ response = self.client.request("DELETE", "/v1/batch/", json={"memories": memories}) response.raise_for_status() @@ -560,7 +615,12 @@ class MemoryClient: Dictionary containing the requested fields. Raises: - APIError: If the API request fails. + ValidationError: If the input data is invalid. + AuthenticationError: If authentication fails. + RateLimitError: If rate limits are exceeded. + MemoryQuotaExceededError: If memory quota is exceeded. + NetworkError: If network connectivity issues occur. + MemoryNotFoundError: If the memory doesn't exist (for updates/deletes). ValueError: If org_id or project_id are not set. """ logger.warning( @@ -604,7 +664,12 @@ class MemoryClient: Dictionary containing the API response. Raises: - APIError: If the API request fails. + ValidationError: If the input data is invalid. + AuthenticationError: If authentication fails. + RateLimitError: If rate limits are exceeded. + MemoryQuotaExceededError: If memory quota is exceeded. + NetworkError: If network connectivity issues occur. + MemoryNotFoundError: If the memory doesn't exist (for updates/deletes). ValueError: If org_id or project_id are not set. """ logger.warning( @@ -673,7 +738,12 @@ class MemoryClient: Dictionary containing webhook details. Raises: - APIError: If the API request fails. + ValidationError: If the input data is invalid. + AuthenticationError: If authentication fails. + RateLimitError: If rate limits are exceeded. + MemoryQuotaExceededError: If memory quota is exceeded. + NetworkError: If network connectivity issues occur. + MemoryNotFoundError: If the memory doesn't exist (for updates/deletes). ValueError: If project_id is not set. """ @@ -695,7 +765,12 @@ class MemoryClient: Dictionary containing the created webhook details. Raises: - APIError: If the API request fails. + ValidationError: If the input data is invalid. + AuthenticationError: If authentication fails. + RateLimitError: If rate limits are exceeded. + MemoryQuotaExceededError: If memory quota is exceeded. + NetworkError: If network connectivity issues occur. + MemoryNotFoundError: If the memory doesn't exist (for updates/deletes). ValueError: If project_id is not set. """ @@ -725,7 +800,12 @@ class MemoryClient: Dictionary containing the updated webhook details. Raises: - APIError: If the API request fails. + ValidationError: If the input data is invalid. + AuthenticationError: If authentication fails. + RateLimitError: If rate limits are exceeded. + MemoryQuotaExceededError: If memory quota is exceeded. + NetworkError: If network connectivity issues occur. + MemoryNotFoundError: If the memory doesn't exist (for updates/deletes). """ payload = {k: v for k, v in {"name": name, "url": url, "event_types": event_types}.items() if v is not None} @@ -745,7 +825,12 @@ class MemoryClient: Dictionary containing success message. Raises: - APIError: If the API request fails. + ValidationError: If the input data is invalid. + AuthenticationError: If authentication fails. + RateLimitError: If rate limits are exceeded. + MemoryQuotaExceededError: If memory quota is exceeded. + NetworkError: If network connectivity issues occur. + MemoryNotFoundError: If the memory doesn't exist (for updates/deletes). """ response = self.client.delete(f"api/v1/webhooks/{webhook_id}/") @@ -1098,7 +1183,12 @@ class AsyncMemoryClient: A dictionary containing the API response. Raises: - APIError: If the API request fails. + ValidationError: If the input data is invalid. + AuthenticationError: If authentication fails. + RateLimitError: If rate limits are exceeded. + MemoryQuotaExceededError: If memory quota is exceeded. + NetworkError: If network connectivity issues occur. + MemoryNotFoundError: If the memory doesn't exist (for updates/deletes). """ params = self._prepare_params() response = await self.async_client.delete(f"/v1/memories/{memory_id}/", params=params) @@ -1117,7 +1207,12 @@ class AsyncMemoryClient: A dictionary containing the API response. Raises: - APIError: If the API request fails. + ValidationError: If the input data is invalid. + AuthenticationError: If authentication fails. + RateLimitError: If rate limits are exceeded. + MemoryQuotaExceededError: If memory quota is exceeded. + NetworkError: If network connectivity issues occur. + MemoryNotFoundError: If the memory doesn't exist (for updates/deletes). """ params = self._prepare_params(kwargs) response = await self.async_client.delete("/v1/memories/", params=params) @@ -1136,7 +1231,12 @@ class AsyncMemoryClient: A list of dictionaries containing the memory history. Raises: - APIError: If the API request fails. + ValidationError: If the input data is invalid. + AuthenticationError: If authentication fails. + RateLimitError: If rate limits are exceeded. + MemoryQuotaExceededError: If memory quota is exceeded. + NetworkError: If network connectivity issues occur. + MemoryNotFoundError: If the memory doesn't exist (for updates/deletes). """ params = self._prepare_params() response = await self.async_client.get(f"/v1/memories/{memory_id}/history/", params=params) @@ -1174,7 +1274,10 @@ class AsyncMemoryClient: Raises: ValueError: If specified entity not found - APIError: If deletion fails + ValidationError: If the input data is invalid. + AuthenticationError: If authentication fails. + MemoryNotFoundError: If the entity doesn't exist. + NetworkError: If network connectivity issues occur. """ if user_id: @@ -1228,7 +1331,12 @@ class AsyncMemoryClient: Dict[str, str]: Message client reset successful. Raises: - APIError: If the API request fails. + ValidationError: If the input data is invalid. + AuthenticationError: If authentication fails. + RateLimitError: If rate limits are exceeded. + MemoryQuotaExceededError: If memory quota is exceeded. + NetworkError: If network connectivity issues occur. + MemoryNotFoundError: If the memory doesn't exist (for updates/deletes). """ await self.delete_users() capture_client_event("client.reset", self, {"sync_type": "async"}) @@ -1246,6 +1354,14 @@ class AsyncMemoryClient: Returns: Dict[str, Any]: The response from the server. + + Raises: + ValidationError: If the input data is invalid. + AuthenticationError: If authentication fails. + RateLimitError: If rate limits are exceeded. + MemoryQuotaExceededError: If memory quota is exceeded. + NetworkError: If network connectivity issues occur. + MemoryNotFoundError: If the memory doesn't exist (for updates/deletes). """ response = await self.async_client.put("/v1/batch/", json={"memories": memories}) response.raise_for_status() @@ -1266,7 +1382,12 @@ class AsyncMemoryClient: str: Message indicating the success of the batch deletion. Raises: - APIError: If the API request fails. + ValidationError: If the input data is invalid. + AuthenticationError: If authentication fails. + RateLimitError: If rate limits are exceeded. + MemoryQuotaExceededError: If memory quota is exceeded. + NetworkError: If network connectivity issues occur. + MemoryNotFoundError: If the memory doesn't exist (for updates/deletes). """ response = await self.async_client.request("DELETE", "/v1/batch/", json={"memories": memories}) response.raise_for_status() @@ -1334,7 +1455,12 @@ class AsyncMemoryClient: Dictionary containing the requested fields. Raises: - APIError: If the API request fails. + ValidationError: If the input data is invalid. + AuthenticationError: If authentication fails. + RateLimitError: If rate limits are exceeded. + MemoryQuotaExceededError: If memory quota is exceeded. + NetworkError: If network connectivity issues occur. + MemoryNotFoundError: If the memory doesn't exist (for updates/deletes). ValueError: If org_id or project_id are not set. """ logger.warning( @@ -1374,7 +1500,12 @@ class AsyncMemoryClient: Dictionary containing the API response. Raises: - APIError: If the API request fails. + ValidationError: If the input data is invalid. + AuthenticationError: If authentication fails. + RateLimitError: If rate limits are exceeded. + MemoryQuotaExceededError: If memory quota is exceeded. + NetworkError: If network connectivity issues occur. + MemoryNotFoundError: If the memory doesn't exist (for updates/deletes). ValueError: If org_id or project_id are not set. """ logger.warning( @@ -1441,7 +1572,12 @@ class AsyncMemoryClient: Dictionary containing webhook details. Raises: - APIError: If the API request fails. + ValidationError: If the input data is invalid. + AuthenticationError: If authentication fails. + RateLimitError: If rate limits are exceeded. + MemoryQuotaExceededError: If memory quota is exceeded. + NetworkError: If network connectivity issues occur. + MemoryNotFoundError: If the memory doesn't exist (for updates/deletes). ValueError: If project_id is not set. """ @@ -1463,7 +1599,12 @@ class AsyncMemoryClient: Dictionary containing the created webhook details. Raises: - APIError: If the API request fails. + ValidationError: If the input data is invalid. + AuthenticationError: If authentication fails. + RateLimitError: If rate limits are exceeded. + MemoryQuotaExceededError: If memory quota is exceeded. + NetworkError: If network connectivity issues occur. + MemoryNotFoundError: If the memory doesn't exist (for updates/deletes). ValueError: If project_id is not set. """ @@ -1493,7 +1634,12 @@ class AsyncMemoryClient: Dictionary containing the updated webhook details. Raises: - APIError: If the API request fails. + ValidationError: If the input data is invalid. + AuthenticationError: If authentication fails. + RateLimitError: If rate limits are exceeded. + MemoryQuotaExceededError: If memory quota is exceeded. + NetworkError: If network connectivity issues occur. + MemoryNotFoundError: If the memory doesn't exist (for updates/deletes). """ payload = {k: v for k, v in {"name": name, "url": url, "event_types": event_types}.items() if v is not None} @@ -1513,7 +1659,12 @@ class AsyncMemoryClient: Dictionary containing success message. Raises: - APIError: If the API request fails. + ValidationError: If the input data is invalid. + AuthenticationError: If authentication fails. + RateLimitError: If rate limits are exceeded. + MemoryQuotaExceededError: If memory quota is exceeded. + NetworkError: If network connectivity issues occur. + MemoryNotFoundError: If the memory doesn't exist (for updates/deletes). """ response = await self.async_client.delete(f"api/v1/webhooks/{webhook_id}/") diff --git a/mem0/client/project.py b/mem0/client/project.py index 371c9932d..45c90819f 100644 --- a/mem0/client/project.py +++ b/mem0/client/project.py @@ -7,6 +7,7 @@ from pydantic import BaseModel, ConfigDict, Field from mem0.client.utils import api_error_handler from mem0.memory.telemetry import capture_client_event +# Exception classes are referenced in docstrings only logger = logging.getLogger(__name__) @@ -141,7 +142,10 @@ class BaseProject(ABC): Dictionary containing the requested project fields. Raises: - APIError: If the API request fails. + ValidationError: If the input data is invalid. + AuthenticationError: If authentication fails. + RateLimitError: If rate limits are exceeded. + NetworkError: If network connectivity issues occur. ValueError: If org_id or project_id are not set. """ pass @@ -159,7 +163,10 @@ class BaseProject(ABC): Dictionary containing the created project details. Raises: - APIError: If the API request fails. + ValidationError: If the input data is invalid. + AuthenticationError: If authentication fails. + RateLimitError: If rate limits are exceeded. + NetworkError: If network connectivity issues occur. ValueError: If org_id is not set. """ pass @@ -185,7 +192,10 @@ class BaseProject(ABC): Dictionary containing the API response. Raises: - APIError: If the API request fails. + ValidationError: If the input data is invalid. + AuthenticationError: If authentication fails. + RateLimitError: If rate limits are exceeded. + NetworkError: If network connectivity issues occur. ValueError: If org_id or project_id are not set. """ pass @@ -199,7 +209,10 @@ class BaseProject(ABC): Dictionary containing the API response. Raises: - APIError: If the API request fails. + ValidationError: If the input data is invalid. + AuthenticationError: If authentication fails. + RateLimitError: If rate limits are exceeded. + NetworkError: If network connectivity issues occur. ValueError: If org_id or project_id are not set. """ pass @@ -213,7 +226,10 @@ class BaseProject(ABC): Dictionary containing the list of project members. Raises: - APIError: If the API request fails. + ValidationError: If the input data is invalid. + AuthenticationError: If authentication fails. + RateLimitError: If rate limits are exceeded. + NetworkError: If network connectivity issues occur. ValueError: If org_id or project_id are not set. """ pass @@ -231,7 +247,10 @@ class BaseProject(ABC): Dictionary containing the API response. Raises: - APIError: If the API request fails. + ValidationError: If the input data is invalid. + AuthenticationError: If authentication fails. + RateLimitError: If rate limits are exceeded. + NetworkError: If network connectivity issues occur. ValueError: If org_id or project_id are not set. """ pass @@ -249,7 +268,10 @@ class BaseProject(ABC): Dictionary containing the API response. Raises: - APIError: If the API request fails. + ValidationError: If the input data is invalid. + AuthenticationError: If authentication fails. + RateLimitError: If rate limits are exceeded. + NetworkError: If network connectivity issues occur. ValueError: If org_id or project_id are not set. """ pass @@ -266,7 +288,10 @@ class BaseProject(ABC): Dictionary containing the API response. Raises: - APIError: If the API request fails. + ValidationError: If the input data is invalid. + AuthenticationError: If authentication fails. + RateLimitError: If rate limits are exceeded. + NetworkError: If network connectivity issues occur. ValueError: If org_id or project_id are not set. """ pass @@ -310,7 +335,10 @@ class Project(BaseProject): Dictionary containing the requested project fields. Raises: - APIError: If the API request fails. + ValidationError: If the input data is invalid. + AuthenticationError: If authentication fails. + RateLimitError: If rate limits are exceeded. + NetworkError: If network connectivity issues occur. ValueError: If org_id or project_id are not set. """ params = self._prepare_params({"fields": fields}) @@ -339,7 +367,10 @@ class Project(BaseProject): Dictionary containing the created project details. Raises: - APIError: If the API request fails. + ValidationError: If the input data is invalid. + AuthenticationError: If authentication fails. + RateLimitError: If rate limits are exceeded. + NetworkError: If network connectivity issues occur. ValueError: If org_id is not set. """ if not self.config.org_id: @@ -382,7 +413,10 @@ class Project(BaseProject): Dictionary containing the API response. Raises: - APIError: If the API request fails. + ValidationError: If the input data is invalid. + AuthenticationError: If authentication fails. + RateLimitError: If rate limits are exceeded. + NetworkError: If network connectivity issues occur. ValueError: If org_id or project_id are not set. """ if ( @@ -432,7 +466,10 @@ class Project(BaseProject): Dictionary containing the API response. Raises: - APIError: If the API request fails. + ValidationError: If the input data is invalid. + AuthenticationError: If authentication fails. + RateLimitError: If rate limits are exceeded. + NetworkError: If network connectivity issues occur. ValueError: If org_id or project_id are not set. """ response = self._client.delete( @@ -455,7 +492,10 @@ class Project(BaseProject): Dictionary containing the list of project members. Raises: - APIError: If the API request fails. + ValidationError: If the input data is invalid. + AuthenticationError: If authentication fails. + RateLimitError: If rate limits are exceeded. + NetworkError: If network connectivity issues occur. ValueError: If org_id or project_id are not set. """ response = self._client.get( @@ -482,7 +522,10 @@ class Project(BaseProject): Dictionary containing the API response. Raises: - APIError: If the API request fails. + ValidationError: If the input data is invalid. + AuthenticationError: If authentication fails. + RateLimitError: If rate limits are exceeded. + NetworkError: If network connectivity issues occur. ValueError: If org_id or project_id are not set. """ if role not in ["READER", "OWNER"]: @@ -515,7 +558,10 @@ class Project(BaseProject): Dictionary containing the API response. Raises: - APIError: If the API request fails. + ValidationError: If the input data is invalid. + AuthenticationError: If authentication fails. + RateLimitError: If rate limits are exceeded. + NetworkError: If network connectivity issues occur. ValueError: If org_id or project_id are not set. """ if role not in ["READER", "OWNER"]: @@ -547,7 +593,10 @@ class Project(BaseProject): Dictionary containing the API response. Raises: - APIError: If the API request fails. + ValidationError: If the input data is invalid. + AuthenticationError: If authentication fails. + RateLimitError: If rate limits are exceeded. + NetworkError: If network connectivity issues occur. ValueError: If org_id or project_id are not set. """ params = {"email": email} @@ -603,7 +652,10 @@ class AsyncProject(BaseProject): Dictionary containing the requested project fields. Raises: - APIError: If the API request fails. + ValidationError: If the input data is invalid. + AuthenticationError: If authentication fails. + RateLimitError: If rate limits are exceeded. + NetworkError: If network connectivity issues occur. ValueError: If org_id or project_id are not set. """ params = self._prepare_params({"fields": fields}) @@ -632,7 +684,10 @@ class AsyncProject(BaseProject): Dictionary containing the created project details. Raises: - APIError: If the API request fails. + ValidationError: If the input data is invalid. + AuthenticationError: If authentication fails. + RateLimitError: If rate limits are exceeded. + NetworkError: If network connectivity issues occur. ValueError: If org_id is not set. """ if not self.config.org_id: @@ -675,7 +730,10 @@ class AsyncProject(BaseProject): Dictionary containing the API response. Raises: - APIError: If the API request fails. + ValidationError: If the input data is invalid. + AuthenticationError: If authentication fails. + RateLimitError: If rate limits are exceeded. + NetworkError: If network connectivity issues occur. ValueError: If org_id or project_id are not set. """ if ( @@ -725,7 +783,10 @@ class AsyncProject(BaseProject): Dictionary containing the API response. Raises: - APIError: If the API request fails. + ValidationError: If the input data is invalid. + AuthenticationError: If authentication fails. + RateLimitError: If rate limits are exceeded. + NetworkError: If network connectivity issues occur. ValueError: If org_id or project_id are not set. """ response = await self._client.delete( @@ -748,7 +809,10 @@ class AsyncProject(BaseProject): Dictionary containing the list of project members. Raises: - APIError: If the API request fails. + ValidationError: If the input data is invalid. + AuthenticationError: If authentication fails. + RateLimitError: If rate limits are exceeded. + NetworkError: If network connectivity issues occur. ValueError: If org_id or project_id are not set. """ response = await self._client.get( @@ -775,7 +839,10 @@ class AsyncProject(BaseProject): Dictionary containing the API response. Raises: - APIError: If the API request fails. + ValidationError: If the input data is invalid. + AuthenticationError: If authentication fails. + RateLimitError: If rate limits are exceeded. + NetworkError: If network connectivity issues occur. ValueError: If org_id or project_id are not set. """ if role not in ["READER", "OWNER"]: @@ -808,7 +875,10 @@ class AsyncProject(BaseProject): Dictionary containing the API response. Raises: - APIError: If the API request fails. + ValidationError: If the input data is invalid. + AuthenticationError: If authentication fails. + RateLimitError: If rate limits are exceeded. + NetworkError: If network connectivity issues occur. ValueError: If org_id or project_id are not set. """ if role not in ["READER", "OWNER"]: @@ -840,7 +910,10 @@ class AsyncProject(BaseProject): Dictionary containing the API response. Raises: - APIError: If the API request fails. + ValidationError: If the input data is invalid. + AuthenticationError: If authentication fails. + RateLimitError: If rate limits are exceeded. + NetworkError: If network connectivity issues occur. ValueError: If org_id or project_id are not set. """ params = {"email": email} diff --git a/mem0/client/utils.py b/mem0/client/utils.py index 3eaa092c5..06a1c0ef2 100644 --- a/mem0/client/utils.py +++ b/mem0/client/utils.py @@ -1,18 +1,35 @@ +import json import logging - import httpx +from mem0.exceptions import ( + NetworkError, + create_exception_from_response, +) + logger = logging.getLogger(__name__) class APIError(Exception): - """Exception raised for errors in the API.""" + """Exception raised for errors in the API. + + Deprecated: Use specific exception classes from mem0.exceptions instead. + This class is maintained for backward compatibility. + """ pass def api_error_handler(func): - """Decorator to handle API errors consistently.""" + """Decorator to handle API errors consistently. + + This decorator catches HTTP and request errors and converts them to + appropriate structured exception classes with detailed error information. + + The decorator analyzes HTTP status codes and response content to create + the most specific exception type with helpful error messages, suggestions, + and debug information. + """ from functools import wraps @wraps(func) @@ -21,9 +38,78 @@ def api_error_handler(func): return func(*args, **kwargs) except httpx.HTTPStatusError as e: logger.error(f"HTTP error occurred: {e}") - raise APIError(f"API request failed: {e.response.text}") + + # Extract error details from response + response_text = "" + error_details = {} + debug_info = { + "status_code": e.response.status_code, + "url": str(e.request.url), + "method": e.request.method, + } + + try: + response_text = e.response.text + # Try to parse JSON response for additional error details + if e.response.headers.get("content-type", "").startswith("application/json"): + error_data = json.loads(response_text) + if isinstance(error_data, dict): + error_details = error_data + response_text = error_data.get("detail", response_text) + except (json.JSONDecodeError, AttributeError): + # Fallback to plain text response + pass + + # Add rate limit information if available + if e.response.status_code == 429: + retry_after = e.response.headers.get("Retry-After") + if retry_after: + try: + debug_info["retry_after"] = int(retry_after) + except ValueError: + pass + + # Add rate limit headers if available + for header in ["X-RateLimit-Limit", "X-RateLimit-Remaining", "X-RateLimit-Reset"]: + value = e.response.headers.get(header) + if value: + debug_info[header.lower().replace("-", "_")] = value + + # Create specific exception based on status code + exception = create_exception_from_response( + status_code=e.response.status_code, + response_text=response_text, + details=error_details, + debug_info=debug_info, + ) + + raise exception + except httpx.RequestError as e: logger.error(f"Request error occurred: {e}") - raise APIError(f"Request failed: {str(e)}") + + # Determine the appropriate exception type based on error type + if isinstance(e, httpx.TimeoutException): + raise NetworkError( + message=f"Request timed out: {str(e)}", + error_code="NET_TIMEOUT", + suggestion="Please check your internet connection and try again", + debug_info={"error_type": "timeout", "original_error": str(e)}, + ) + elif isinstance(e, httpx.ConnectError): + raise NetworkError( + message=f"Connection failed: {str(e)}", + error_code="NET_CONNECT", + suggestion="Please check your internet connection and try again", + debug_info={"error_type": "connection", "original_error": str(e)}, + ) + else: + # Generic network error for other request errors + raise NetworkError( + message=f"Network request failed: {str(e)}", + error_code="NET_GENERIC", + suggestion="Please check your internet connection and try again", + debug_info={"error_type": "request", "original_error": str(e)}, + ) return wrapper diff --git a/mem0/exceptions.py b/mem0/exceptions.py new file mode 100644 index 000000000..56c2b54c3 --- /dev/null +++ b/mem0/exceptions.py @@ -0,0 +1,503 @@ +"""Structured exception classes for Mem0 with error codes, suggestions, and debug information. + +This module provides a comprehensive set of exception classes that replace the generic +APIError with specific, actionable exceptions. Each exception includes error codes, +user-friendly suggestions, and debug information to enable better error handling +and recovery in applications using Mem0. + +Example: + Basic usage: + try: + memory.add(content, user_id=user_id) + except RateLimitError as e: + # Implement exponential backoff + time.sleep(e.debug_info.get('retry_after', 60)) + except MemoryQuotaExceededError as e: + # Trigger quota upgrade flow + logger.error(f"Quota exceeded: {e.error_code}") + except ValidationError as e: + # Return user-friendly error + raise HTTPException(400, detail=e.suggestion) + + Advanced usage with error context: + try: + memory.update(memory_id, content=new_content) + except MemoryNotFoundError as e: + logger.warning(f"Memory {memory_id} not found: {e.message}") + if e.suggestion: + logger.info(f"Suggestion: {e.suggestion}") +""" + +from typing import Any, Dict, Optional + + +class MemoryError(Exception): + """Base exception for all memory-related errors. + + This is the base class for all Mem0-specific exceptions. It provides a structured + approach to error handling with error codes, contextual details, suggestions for + resolution, and debug information. + + Attributes: + message (str): Human-readable error message. + error_code (str): Unique error identifier for programmatic handling. + details (dict): Additional context about the error. + suggestion (str): User-friendly suggestion for resolving the error. + debug_info (dict): Technical debugging information. + + Example: + raise MemoryError( + message="Memory operation failed", + error_code="MEM_001", + details={"operation": "add", "user_id": "user123"}, + suggestion="Please check your API key and try again", + debug_info={"request_id": "req_456", "timestamp": "2024-01-01T00:00:00Z"} + ) + """ + + def __init__( + self, + message: str, + error_code: str, + details: Optional[Dict[str, Any]] = None, + suggestion: Optional[str] = None, + debug_info: Optional[Dict[str, Any]] = None, + ): + """Initialize a MemoryError. + + Args: + message: Human-readable error message. + error_code: Unique error identifier. + details: Additional context about the error. + suggestion: User-friendly suggestion for resolving the error. + debug_info: Technical debugging information. + """ + self.message = message + self.error_code = error_code + self.details = details or {} + self.suggestion = suggestion + self.debug_info = debug_info or {} + super().__init__(self.message) + + def __repr__(self) -> str: + return ( + f"{self.__class__.__name__}(" + f"message={self.message!r}, " + f"error_code={self.error_code!r}, " + f"details={self.details!r}, " + f"suggestion={self.suggestion!r}, " + f"debug_info={self.debug_info!r})" + ) + + +class AuthenticationError(MemoryError): + """Raised when authentication fails. + + This exception is raised when API key validation fails, tokens are invalid, + or authentication credentials are missing or expired. + + Common scenarios: + - Invalid API key + - Expired authentication token + - Missing authentication headers + - Insufficient permissions + + Example: + raise AuthenticationError( + message="Invalid API key provided", + error_code="AUTH_001", + suggestion="Please check your API key in the Mem0 dashboard" + ) + """ + pass + + +class RateLimitError(MemoryError): + """Raised when rate limits are exceeded. + + This exception is raised when the API rate limit has been exceeded. + It includes information about retry timing and current rate limit status. + + The debug_info typically contains: + - retry_after: Seconds to wait before retrying + - limit: Current rate limit + - remaining: Remaining requests in current window + - reset_time: When the rate limit window resets + + Example: + raise RateLimitError( + message="Rate limit exceeded", + error_code="RATE_001", + suggestion="Please wait before making more requests", + debug_info={"retry_after": 60, "limit": 100, "remaining": 0} + ) + """ + pass + + +class ValidationError(MemoryError): + """Raised when input validation fails. + + This exception is raised when request parameters, memory content, + or configuration values fail validation checks. + + Common scenarios: + - Invalid user_id format + - Missing required fields + - Content too long or too short + - Invalid metadata format + - Malformed filters + + Example: + raise ValidationError( + message="Invalid user_id format", + error_code="VAL_001", + details={"field": "user_id", "value": "123", "expected": "string"}, + suggestion="User ID must be a non-empty string" + ) + """ + pass + + +class MemoryNotFoundError(MemoryError): + """Raised when a memory is not found. + + This exception is raised when attempting to access, update, or delete + a memory that doesn't exist or is not accessible to the current user. + + Example: + raise MemoryNotFoundError( + message="Memory not found", + error_code="MEM_404", + details={"memory_id": "mem_123", "user_id": "user_456"}, + suggestion="Please check the memory ID and ensure it exists" + ) + """ + pass + + +class NetworkError(MemoryError): + """Raised when network connectivity issues occur. + + This exception is raised for network-related problems such as + connection timeouts, DNS resolution failures, or service unavailability. + + Common scenarios: + - Connection timeout + - DNS resolution failure + - Service temporarily unavailable + - Network connectivity issues + + Example: + raise NetworkError( + message="Connection timeout", + error_code="NET_001", + suggestion="Please check your internet connection and try again", + debug_info={"timeout": 30, "endpoint": "api.mem0.ai"} + ) + """ + pass + + +class ConfigurationError(MemoryError): + """Raised when client configuration is invalid. + + This exception is raised when the client is improperly configured, + such as missing required settings or invalid configuration values. + + Common scenarios: + - Missing API key + - Invalid host URL + - Incompatible configuration options + - Missing required environment variables + + Example: + raise ConfigurationError( + message="API key not configured", + error_code="CFG_001", + suggestion="Set MEM0_API_KEY environment variable or pass api_key parameter" + ) + """ + pass + + +class MemoryQuotaExceededError(MemoryError): + """Raised when user's memory quota is exceeded. + + This exception is raised when the user has reached their memory + storage or usage limits. + + The debug_info typically contains: + - current_usage: Current memory usage + - quota_limit: Maximum allowed usage + - usage_type: Type of quota (storage, requests, etc.) + + Example: + raise MemoryQuotaExceededError( + message="Memory quota exceeded", + error_code="QUOTA_001", + suggestion="Please upgrade your plan or delete unused memories", + debug_info={"current_usage": 1000, "quota_limit": 1000, "usage_type": "memories"} + ) + """ + pass + + +class MemoryCorruptionError(MemoryError): + """Raised when memory data is corrupted. + + This exception is raised when stored memory data is found to be + corrupted, malformed, or otherwise unreadable. + + Example: + raise MemoryCorruptionError( + message="Memory data is corrupted", + error_code="CORRUPT_001", + details={"memory_id": "mem_123"}, + suggestion="Please contact support for data recovery assistance" + ) + """ + pass + + +class VectorSearchError(MemoryError): + """Raised when vector search operations fail. + + This exception is raised when vector database operations fail, + such as search queries, embedding generation, or index operations. + + Common scenarios: + - Embedding model unavailable + - Vector index corruption + - Search query timeout + - Incompatible vector dimensions + + Example: + raise VectorSearchError( + message="Vector search failed", + error_code="VEC_001", + details={"query": "find similar memories", "vector_dim": 1536}, + suggestion="Please try a simpler search query" + ) + """ + pass + + +class CacheError(MemoryError): + """Raised when caching operations fail. + + This exception is raised when cache-related operations fail, + such as cache misses, cache invalidation errors, or cache corruption. + + Example: + raise CacheError( + message="Cache operation failed", + error_code="CACHE_001", + details={"operation": "get", "key": "user_memories_123"}, + suggestion="Cache will be refreshed automatically" + ) + """ + pass + + +# OSS-specific exception classes +class VectorStoreError(MemoryError): + """Raised when vector store operations fail. + + This exception is raised when vector store operations fail, + such as embedding storage, similarity search, or vector operations. + + Example: + raise VectorStoreError( + message="Vector store operation failed", + error_code="VECTOR_001", + details={"operation": "search", "collection": "memories"}, + suggestion="Please check your vector store configuration and connection" + ) + """ + def __init__(self, message: str, error_code: str = "VECTOR_001", details: dict = None, + suggestion: str = "Please check your vector store configuration and connection", + debug_info: dict = None): + super().__init__(message, error_code, details, suggestion, debug_info) + + +class GraphStoreError(MemoryError): + """Raised when graph store operations fail. + + This exception is raised when graph store operations fail, + such as relationship creation, entity management, or graph queries. + + Example: + raise GraphStoreError( + message="Graph store operation failed", + error_code="GRAPH_001", + details={"operation": "create_relationship", "entity": "user_123"}, + suggestion="Please check your graph store configuration and connection" + ) + """ + def __init__(self, message: str, error_code: str = "GRAPH_001", details: dict = None, + suggestion: str = "Please check your graph store configuration and connection", + debug_info: dict = None): + super().__init__(message, error_code, details, suggestion, debug_info) + + +class EmbeddingError(MemoryError): + """Raised when embedding operations fail. + + This exception is raised when embedding operations fail, + such as text embedding generation or embedding model errors. + + Example: + raise EmbeddingError( + message="Embedding generation failed", + error_code="EMBED_001", + details={"text_length": 1000, "model": "openai"}, + suggestion="Please check your embedding model configuration" + ) + """ + def __init__(self, message: str, error_code: str = "EMBED_001", details: dict = None, + suggestion: str = "Please check your embedding model configuration", + debug_info: dict = None): + super().__init__(message, error_code, details, suggestion, debug_info) + + +class LLMError(MemoryError): + """Raised when LLM operations fail. + + This exception is raised when LLM operations fail, + such as text generation, completion, or model inference errors. + + Example: + raise LLMError( + message="LLM operation failed", + error_code="LLM_001", + details={"model": "gpt-4", "prompt_length": 500}, + suggestion="Please check your LLM configuration and API key" + ) + """ + def __init__(self, message: str, error_code: str = "LLM_001", details: dict = None, + suggestion: str = "Please check your LLM configuration and API key", + debug_info: dict = None): + super().__init__(message, error_code, details, suggestion, debug_info) + + +class DatabaseError(MemoryError): + """Raised when database operations fail. + + This exception is raised when database operations fail, + such as SQLite operations, connection issues, or data corruption. + + Example: + raise DatabaseError( + message="Database operation failed", + error_code="DB_001", + details={"operation": "insert", "table": "memories"}, + suggestion="Please check your database configuration and connection" + ) + """ + def __init__(self, message: str, error_code: str = "DB_001", details: dict = None, + suggestion: str = "Please check your database configuration and connection", + debug_info: dict = None): + super().__init__(message, error_code, details, suggestion, debug_info) + + +class DependencyError(MemoryError): + """Raised when required dependencies are missing. + + This exception is raised when required dependencies are missing, + such as optional packages for specific providers or features. + + Example: + raise DependencyError( + message="Required dependency missing", + error_code="DEPS_001", + details={"package": "kuzu", "feature": "graph_store"}, + suggestion="Please install the required dependencies: pip install kuzu" + ) + """ + def __init__(self, message: str, error_code: str = "DEPS_001", details: dict = None, + suggestion: str = "Please install the required dependencies", + debug_info: dict = None): + super().__init__(message, error_code, details, suggestion, debug_info) + + +# Mapping of HTTP status codes to specific exception classes +HTTP_STATUS_TO_EXCEPTION = { + 400: ValidationError, + 401: AuthenticationError, + 403: AuthenticationError, + 404: MemoryNotFoundError, + 408: NetworkError, + 409: ValidationError, + 413: MemoryQuotaExceededError, + 422: ValidationError, + 429: RateLimitError, + 500: MemoryError, + 502: NetworkError, + 503: NetworkError, + 504: NetworkError, +} + + +def create_exception_from_response( + status_code: int, + response_text: str, + error_code: Optional[str] = None, + details: Optional[Dict[str, Any]] = None, + debug_info: Optional[Dict[str, Any]] = None, +) -> MemoryError: + """Create an appropriate exception based on HTTP response. + + This function analyzes the HTTP status code and response to create + the most appropriate exception type with relevant error information. + + Args: + status_code: HTTP status code from the response. + response_text: Response body text. + error_code: Optional specific error code. + details: Additional error context. + debug_info: Debug information. + + Returns: + An instance of the appropriate MemoryError subclass. + + Example: + exception = create_exception_from_response( + status_code=429, + response_text="Rate limit exceeded", + debug_info={"retry_after": 60} + ) + # Returns a RateLimitError instance + """ + exception_class = HTTP_STATUS_TO_EXCEPTION.get(status_code, MemoryError) + + # Generate error code if not provided + if not error_code: + error_code = f"HTTP_{status_code}" + + # Create appropriate suggestion based on status code + suggestions = { + 400: "Please check your request parameters and try again", + 401: "Please check your API key and authentication credentials", + 403: "You don't have permission to perform this operation", + 404: "The requested resource was not found", + 408: "Request timed out. Please try again", + 409: "Resource conflict. Please check your request", + 413: "Request too large. Please reduce the size of your request", + 422: "Invalid request data. Please check your input", + 429: "Rate limit exceeded. Please wait before making more requests", + 500: "Internal server error. Please try again later", + 502: "Service temporarily unavailable. Please try again later", + 503: "Service unavailable. Please try again later", + 504: "Gateway timeout. Please try again later", + } + + suggestion = suggestions.get(status_code, "Please try again later") + + return exception_class( + message=response_text or f"HTTP {status_code} error", + error_code=error_code, + details=details or {}, + suggestion=suggestion, + debug_info=debug_info or {}, + ) \ No newline at end of file diff --git a/mem0/memory/main.py b/mem0/memory/main.py index c997983ff..2419c47df 100644 --- a/mem0/memory/main.py +++ b/mem0/memory/main.py @@ -7,7 +7,6 @@ import logging import os import uuid import warnings - from copy import deepcopy from datetime import datetime from typing import Any, Dict, Optional @@ -21,6 +20,7 @@ from mem0.configs.prompts import ( PROCEDURAL_MEMORY_SYSTEM_PROMPT, get_update_memory_messages, ) +from mem0.exceptions import ValidationError as Mem0ValidationError from mem0.memory.base import MemoryBase from mem0.memory.setup import mem0_dir, setup_config from mem0.memory.storage import SQLiteManager @@ -109,7 +109,12 @@ def _build_filters_and_metadata( session_ids_provided.append("run_id") if not session_ids_provided: - raise ValueError("At least one of 'user_id', 'agent_id', or 'run_id' must be provided.") + raise Mem0ValidationError( + message="At least one of 'user_id', 'agent_id', or 'run_id' must be provided.", + error_code="VALIDATION_001", + details={"provided_ids": {"user_id": user_id, "agent_id": agent_id, "run_id": run_id}}, + suggestion="Please provide at least one identifier to scope the memory operation." + ) # ---------- optional actor filter ---------- resolved_actor_id = actor_id or effective_query_filters.get("actor_id") @@ -227,6 +232,14 @@ class Memory(MemoryBase): including a list of memory items affected (added, updated) under a "results" key, and potentially "relations" if graph store is enabled. Example for v1.1+: `{"results": [{"id": "...", "memory": "...", "event": "ADD"}]}` + + Raises: + Mem0ValidationError: If input validation fails (invalid memory_type, messages format, etc.). + VectorStoreError: If vector store operations fail. + GraphStoreError: If graph store operations fail. + EmbeddingError: If embedding generation fails. + LLMError: If LLM operations fail. + DatabaseError: If database operations fail. """ processed_metadata, effective_filters = _build_filters_and_metadata( @@ -237,8 +250,11 @@ class Memory(MemoryBase): ) if memory_type is not None and memory_type != MemoryType.PROCEDURAL.value: - raise ValueError( - f"Invalid 'memory_type'. Please pass {MemoryType.PROCEDURAL.value} to create procedural memories." + raise Mem0ValidationError( + message=f"Invalid 'memory_type'. Please pass {MemoryType.PROCEDURAL.value} to create procedural memories.", + error_code="VALIDATION_002", + details={"provided_type": memory_type, "valid_type": MemoryType.PROCEDURAL.value}, + suggestion=f"Use '{MemoryType.PROCEDURAL.value}' to create procedural memories." ) if isinstance(messages, str): @@ -248,7 +264,12 @@ class Memory(MemoryBase): messages = [messages] elif not isinstance(messages, list): - raise ValueError("messages must be str, dict, or list[dict]") + raise Mem0ValidationError( + message="messages must be str, dict, or list[dict]", + error_code="VALIDATION_003", + details={"provided_type": type(messages).__name__, "valid_types": ["str", "dict", "list[dict]"]}, + suggestion="Convert your input to a string, dictionary, or list of dictionaries." + ) if agent_id is not None and memory_type == MemoryType.PROCEDURAL.value: results = self._create_procedural_memory(messages, metadata=processed_metadata, prompt=prompt) @@ -1090,7 +1111,12 @@ class AsyncMemory(MemoryBase): messages = [messages] elif not isinstance(messages, list): - raise ValueError("messages must be str, dict, or list[dict]") + raise Mem0ValidationError( + message="messages must be str, dict, or list[dict]", + error_code="VALIDATION_003", + details={"provided_type": type(messages).__name__, "valid_types": ["str", "dict", "list[dict]"]}, + suggestion="Convert your input to a string, dictionary, or list of dictionaries." + ) if agent_id is not None and memory_type == MemoryType.PROCEDURAL.value: results = await self._create_procedural_memory(