Fix exception handling across cognee - lost exception stacktraces and exception metadata (#1518)

<!-- .github/pull_request_template.md -->

## Description
<!--
Please provide a clear, human-generated description of the changes in
this PR.
DO NOT use AI-generated descriptions. We want to understand your thought
process and reasoning.
-->

In several places, our exception handling discards valuable debugging
context - either by losing the original stack trace or omitting metadata
from the underlying error. This makes diagnosing root causes
significantly harder.

This PR updates exception throwing to use [`raise ...
from`](https://docs.python.org/3/reference/simple_stmts.html#raise),
which:
1. The original stack trace is always preserved, even when wrapping
exceptions
2. Custom exceptions explicitly reference their underlying cause, making
error chains clearer



## Type of Change
<!-- Please check the relevant option -->
- [ ] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update
- [ ] Code refactoring
- [ ] Performance improvement
- [ ] Other (please specify):

## Screenshots/Videos (if applicable)
<!-- Add screenshots or videos to help explain your changes -->

## Pre-submission Checklist
<!-- Please check all boxes that apply before submitting your PR -->
- [ ] **I have tested my changes thoroughly before submitting this PR**
- [ ] **This PR contains minimal changes necessary to address the
issue/feature**
- [ ] My code follows the project's coding standards and style
guidelines
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] I have added necessary documentation (if applicable)
- [ ] All new and existing tests pass
- [ ] I have searched existing PRs to ensure this change hasn't been
submitted already
- [ ] I have linked any relevant issues in the description
- [ ] My commits have clear and descriptive messages

## DCO Affirmation
I affirm that all code in every commit of this pull request conforms to
the terms of the Topoteretes Developer Certificate of Origin.
This commit is contained in:
Vasilije 2025-10-12 10:29:26 +02:00 committed by GitHub
commit fb8e6ae47c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 34 additions and 28 deletions

View file

@ -70,11 +70,11 @@ After adding data, use `cognee cognify` to process it into knowledge graphs.
await cognee.add(data=data_to_add, dataset_name=args.dataset_name) await cognee.add(data=data_to_add, dataset_name=args.dataset_name)
fmt.success(f"Successfully added data to dataset '{args.dataset_name}'") fmt.success(f"Successfully added data to dataset '{args.dataset_name}'")
except Exception as e: except Exception as e:
raise CliCommandInnerException(f"Failed to add data: {str(e)}") raise CliCommandInnerException(f"Failed to add data: {str(e)}") from e
asyncio.run(run_add()) asyncio.run(run_add())
except Exception as e: except Exception as e:
if isinstance(e, CliCommandInnerException): if isinstance(e, CliCommandInnerException):
raise CliCommandException(str(e), error_code=1) raise CliCommandException(str(e), error_code=1) from e
raise CliCommandException(f"Error adding data: {str(e)}", error_code=1) raise CliCommandException(f"Error adding data: {str(e)}", error_code=1) from e

View file

@ -107,7 +107,7 @@ After successful cognify processing, use `cognee search` to query the knowledge
) )
return result return result
except Exception as e: except Exception as e:
raise CliCommandInnerException(f"Failed to cognify: {str(e)}") raise CliCommandInnerException(f"Failed to cognify: {str(e)}") from e
result = asyncio.run(run_cognify()) result = asyncio.run(run_cognify())
@ -124,5 +124,5 @@ After successful cognify processing, use `cognee search` to query the knowledge
except Exception as e: except Exception as e:
if isinstance(e, CliCommandInnerException): if isinstance(e, CliCommandInnerException):
raise CliCommandException(str(e), error_code=1) raise CliCommandException(str(e), error_code=1) from e
raise CliCommandException(f"Error during cognification: {str(e)}", error_code=1) raise CliCommandException(f"Error during cognification: {str(e)}", error_code=1) from e

View file

@ -79,8 +79,10 @@ Configuration changes will affect how cognee processes and stores data.
except Exception as e: except Exception as e:
if isinstance(e, CliCommandInnerException): if isinstance(e, CliCommandInnerException):
raise CliCommandException(str(e), error_code=1) raise CliCommandException(str(e), error_code=1) from e
raise CliCommandException(f"Error managing configuration: {str(e)}", error_code=1) raise CliCommandException(
f"Error managing configuration: {str(e)}", error_code=1
) from e
def _handle_get(self, args: argparse.Namespace) -> None: def _handle_get(self, args: argparse.Namespace) -> None:
try: try:
@ -122,7 +124,7 @@ Configuration changes will affect how cognee processes and stores data.
fmt.note("Configuration viewing not fully implemented yet") fmt.note("Configuration viewing not fully implemented yet")
except Exception as e: except Exception as e:
raise CliCommandInnerException(f"Failed to get configuration: {str(e)}") raise CliCommandInnerException(f"Failed to get configuration: {str(e)}") from e
def _handle_set(self, args: argparse.Namespace) -> None: def _handle_set(self, args: argparse.Namespace) -> None:
try: try:
@ -141,7 +143,7 @@ Configuration changes will affect how cognee processes and stores data.
fmt.error(f"Failed to set configuration key '{args.key}'") fmt.error(f"Failed to set configuration key '{args.key}'")
except Exception as e: except Exception as e:
raise CliCommandInnerException(f"Failed to set configuration: {str(e)}") raise CliCommandInnerException(f"Failed to set configuration: {str(e)}") from e
def _handle_unset(self, args: argparse.Namespace) -> None: def _handle_unset(self, args: argparse.Namespace) -> None:
try: try:
@ -189,7 +191,7 @@ Configuration changes will affect how cognee processes and stores data.
fmt.note("Use 'cognee config list' to see all available configuration options") fmt.note("Use 'cognee config list' to see all available configuration options")
except Exception as e: except Exception as e:
raise CliCommandInnerException(f"Failed to unset configuration: {str(e)}") raise CliCommandInnerException(f"Failed to unset configuration: {str(e)}") from e
def _handle_list(self, args: argparse.Namespace) -> None: def _handle_list(self, args: argparse.Namespace) -> None:
try: try:
@ -209,7 +211,7 @@ Configuration changes will affect how cognee processes and stores data.
fmt.echo(" cognee config reset - Reset all to defaults") fmt.echo(" cognee config reset - Reset all to defaults")
except Exception as e: except Exception as e:
raise CliCommandInnerException(f"Failed to list configuration: {str(e)}") raise CliCommandInnerException(f"Failed to list configuration: {str(e)}") from e
def _handle_reset(self, args: argparse.Namespace) -> None: def _handle_reset(self, args: argparse.Namespace) -> None:
try: try:
@ -222,4 +224,4 @@ Configuration changes will affect how cognee processes and stores data.
fmt.echo("This would reset all settings to their default values") fmt.echo("This would reset all settings to their default values")
except Exception as e: except Exception as e:
raise CliCommandInnerException(f"Failed to reset configuration: {str(e)}") raise CliCommandInnerException(f"Failed to reset configuration: {str(e)}") from e

View file

@ -100,7 +100,7 @@ Be careful with deletion operations as they are irreversible.
else: else:
await cognee.delete(dataset_name=args.dataset_name, user_id=args.user_id) await cognee.delete(dataset_name=args.dataset_name, user_id=args.user_id)
except Exception as e: except Exception as e:
raise CliCommandInnerException(f"Failed to delete: {str(e)}") raise CliCommandInnerException(f"Failed to delete: {str(e)}") from e
asyncio.run(run_delete()) asyncio.run(run_delete())
# This success message may be inaccurate due to the underlying bug, but we leave it for now. # This success message may be inaccurate due to the underlying bug, but we leave it for now.
@ -108,5 +108,5 @@ Be careful with deletion operations as they are irreversible.
except Exception as e: except Exception as e:
if isinstance(e, CliCommandInnerException): if isinstance(e, CliCommandInnerException):
raise CliCommandException(str(e), error_code=1) raise CliCommandException(str(e), error_code=1) from e
raise CliCommandException(f"Error deleting data: {str(e)}", error_code=1) raise CliCommandException(f"Error deleting data: {str(e)}", error_code=1) from e

View file

@ -104,7 +104,7 @@ Search Types & Use Cases:
) )
return results return results
except Exception as e: except Exception as e:
raise CliCommandInnerException(f"Failed to search: {str(e)}") raise CliCommandInnerException(f"Failed to search: {str(e)}") from e
results = asyncio.run(run_search()) results = asyncio.run(run_search())
@ -141,5 +141,5 @@ Search Types & Use Cases:
except Exception as e: except Exception as e:
if isinstance(e, CliCommandInnerException): if isinstance(e, CliCommandInnerException):
raise CliCommandException(str(e), error_code=1) raise CliCommandException(str(e), error_code=1) from e
raise CliCommandException(f"Error searching: {str(e)}", error_code=1) raise CliCommandException(f"Error searching: {str(e)}", error_code=1) from e

View file

@ -53,7 +53,7 @@ def parse_neptune_url(url: str) -> Tuple[str, str]:
return graph_id, region return graph_id, region
except Exception as e: except Exception as e:
raise ValueError(f"Failed to parse Neptune Analytics URL '{url}': {str(e)}") raise ValueError(f"Failed to parse Neptune Analytics URL '{url}': {str(e)}") from e
def validate_graph_id(graph_id: str) -> bool: def validate_graph_id(graph_id: str) -> bool:

View file

@ -283,7 +283,7 @@ class SQLAlchemyAdapter:
try: try:
data_entity = (await session.scalars(select(Data).where(Data.id == data_id))).one() data_entity = (await session.scalars(select(Data).where(Data.id == data_id))).one()
except (ValueError, NoResultFound) as e: except (ValueError, NoResultFound) as e:
raise EntityNotFoundError(message=f"Entity not found: {str(e)}") raise EntityNotFoundError(message=f"Entity not found: {str(e)}") from e
# Check if other data objects point to the same raw data location # Check if other data objects point to the same raw data location
raw_data_location_entities = ( raw_data_location_entities = (

View file

@ -90,7 +90,9 @@ class FastembedEmbeddingEngine(EmbeddingEngine):
except Exception as error: except Exception as error:
logger.error(f"Embedding error in FastembedEmbeddingEngine: {str(error)}") logger.error(f"Embedding error in FastembedEmbeddingEngine: {str(error)}")
raise EmbeddingException(f"Failed to index data points using model {self.model}") raise EmbeddingException(
f"Failed to index data points using model {self.model}"
) from error
def get_vector_size(self) -> int: def get_vector_size(self) -> int:
""" """

View file

@ -150,7 +150,7 @@ class LiteLLMEmbeddingEngine(EmbeddingEngine):
litellm.exceptions.NotFoundError, litellm.exceptions.NotFoundError,
) as e: ) as e:
logger.error(f"Embedding error with model {self.model}: {str(e)}") logger.error(f"Embedding error with model {self.model}: {str(e)}")
raise EmbeddingException(f"Failed to index data points using model {self.model}") raise EmbeddingException(f"Failed to index data points using model {self.model}") from e
except Exception as error: except Exception as error:
logger.error("Error embedding text: %s", str(error)) logger.error("Error embedding text: %s", str(error))

View file

@ -37,6 +37,8 @@ async def get_authenticated_user(
except Exception as e: except Exception as e:
# Convert any get_default_user failure into a proper HTTP 500 error # Convert any get_default_user failure into a proper HTTP 500 error
logger.error(f"Failed to create default user: {str(e)}") logger.error(f"Failed to create default user: {str(e)}")
raise HTTPException(status_code=500, detail=f"Failed to create default user: {str(e)}") raise HTTPException(
status_code=500, detail=f"Failed to create default user: {str(e)}"
) from e
return user return user

View file

@ -40,8 +40,8 @@ async def create_role(
# Add association directly to the association table # Add association directly to the association table
role = Role(name=role_name, tenant_id=tenant.id) role = Role(name=role_name, tenant_id=tenant.id)
session.add(role) session.add(role)
except IntegrityError: except IntegrityError as e:
raise EntityAlreadyExistsError(message="Role already exists for tenant.") raise EntityAlreadyExistsError(message="Role already exists for tenant.") from e
await session.commit() await session.commit()
await session.refresh(role) await session.refresh(role)

View file

@ -35,5 +35,5 @@ async def create_tenant(tenant_name: str, user_id: UUID) -> UUID:
await session.merge(user) await session.merge(user)
await session.commit() await session.commit()
return tenant.id return tenant.id
except IntegrityError: except IntegrityError as e:
raise EntityAlreadyExistsError(message="Tenant already exists.") raise EntityAlreadyExistsError(message="Tenant already exists.") from e