Skip to content

Commit bd19208

Browse files
Grafi 50/better exception propagation (#72)
* creates exceptions * update exception propagation * update unit test and lint * address mypy errors, remove record decorator from mypy check * update version * update tracer and unit test
1 parent 3064160 commit bd19208

34 files changed

Lines changed: 1538 additions & 249 deletions

grafi/common/decorators/record_base.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
# mypy: ignore-errors
12
"""Base decorator utilities for recording component invoke events and tracing."""
23

34
import functools

grafi/common/decorators/record_decorators.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
# mypy: ignore-errors
12
"""
23
Unified module for all record decorators.
34
This replaces 8 separate files with a single, maintainable module.
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
"""
2+
Grafi exception hierarchy for comprehensive error handling.
3+
"""
4+
5+
from grafi.common.exceptions.base import (
6+
GrafiError,
7+
ValidationError,
8+
)
9+
from grafi.common.exceptions.duplicate_node_error import DuplicateNodeError
10+
from grafi.common.exceptions.event_exceptions import (
11+
EventPersistenceError,
12+
EventSerializationError,
13+
EventStoreError,
14+
TopicError,
15+
TopicPublicationError,
16+
TopicSubscriptionError,
17+
)
18+
from grafi.common.exceptions.tool_exceptions import (
19+
FunctionCallException,
20+
FunctionToolException,
21+
LLMToolException,
22+
ToolInvocationError,
23+
)
24+
from grafi.common.exceptions.workflow_exceptions import (
25+
NodeExecutionError,
26+
WorkflowError,
27+
)
28+
29+
__all__ = [
30+
# Base errors
31+
"GrafiError",
32+
"ValidationError",
33+
# Tool errors
34+
"ToolInvocationError",
35+
"LLMToolException",
36+
"FunctionCallException",
37+
"FunctionToolException",
38+
# Workflow errors
39+
"WorkflowError",
40+
"NodeExecutionError",
41+
# Event and topic errors
42+
"EventStoreError",
43+
"EventSerializationError",
44+
"EventPersistenceError",
45+
"TopicError",
46+
"TopicSubscriptionError",
47+
"TopicPublicationError",
48+
# Domain-specific errors
49+
"DuplicateNodeError",
50+
]

grafi/common/exceptions/base.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
"""
2+
Base exception classes for the Grafi framework.
3+
"""
4+
5+
import time
6+
from typing import Any
7+
from typing import Dict
8+
from typing import Optional
9+
10+
from grafi.common.models.invoke_context import InvokeContext
11+
12+
13+
class GrafiError(Exception):
14+
"""Base exception for all Grafi framework errors.
15+
16+
Attributes:
17+
message: Human-readable error message
18+
context: Additional context information about the error
19+
cause: The underlying exception that caused this error
20+
timestamp: When the error occurred
21+
severity: Error severity level
22+
"""
23+
24+
def __init__(
25+
self,
26+
message: str,
27+
invoke_context: Optional[InvokeContext] = None,
28+
cause: Optional[Exception] = None,
29+
severity: str = "ERROR",
30+
):
31+
super().__init__(message)
32+
self.message = message
33+
self.cause = cause
34+
self.invoke_context = invoke_context
35+
self.timestamp = time.time()
36+
self.severity = severity
37+
38+
def __str__(self) -> str:
39+
base_msg = f"[{self.severity}] {self.message}"
40+
if self.invoke_context:
41+
base_msg = f"{base_msg} [Invoke Context: {self.invoke_context}]"
42+
if self.cause:
43+
base_msg = f"{base_msg} [Caused by: {type(self.cause).__name__}: {str(self.cause)}]"
44+
return base_msg
45+
46+
def to_dict(self) -> Dict[str, Any]:
47+
"""Convert error to dictionary for structured logging."""
48+
return {
49+
"error_type": type(self).__name__,
50+
"message": self.message,
51+
"timestamp": self.timestamp,
52+
"severity": self.severity,
53+
"cause": str(self.cause) if self.cause else None,
54+
"invoke_context": (
55+
self.invoke_context.model_dump() if self.invoke_context else None
56+
),
57+
}
58+
59+
60+
class ValidationError(GrafiError):
61+
"""Raised when input validation fails."""
62+
63+
pass
Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,20 @@
11
"""Exception for handling duplicate nodes in the graph."""
22

3-
from grafi.nodes.node_base import NodeBase
3+
from typing import TYPE_CHECKING
44

5+
from grafi.common.exceptions.workflow_exceptions import WorkflowError
56

6-
class DuplicateNodeError(Exception):
7+
8+
if TYPE_CHECKING:
9+
from grafi.nodes.node_base import NodeBase
10+
11+
12+
class DuplicateNodeError(WorkflowError):
713
"""Exception raised when a duplicate node is detected in the graph."""
814

9-
def __init__(self, node: NodeBase):
10-
super().__init__(f"Duplicate element detected: {node.name}")
15+
def __init__(self, node: "NodeBase") -> None:
16+
super().__init__(
17+
message=f"Duplicate node detected: {node.name}", severity="ERROR"
18+
)
1119
self.node = node
20+
self.node_name = node.name
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
"""
2+
Event store and topic-related exception classes.
3+
"""
4+
5+
from typing import Any
6+
from typing import Optional
7+
8+
from grafi.common.exceptions.base import GrafiError
9+
from grafi.common.models.invoke_context import InvokeContext
10+
11+
12+
class EventStoreError(GrafiError):
13+
"""Raised when event store operations fail."""
14+
15+
pass
16+
17+
18+
class EventSerializationError(EventStoreError):
19+
"""Raised when event serialization/deserialization fails."""
20+
21+
pass
22+
23+
24+
class EventPersistenceError(EventStoreError):
25+
"""Raised when persisting events to storage fails."""
26+
27+
pass
28+
29+
30+
class TopicError(GrafiError):
31+
"""Raised when topic pub/sub operations fail."""
32+
33+
def __init__(
34+
self,
35+
topic_name: str,
36+
message: str,
37+
invoke_context: Optional[InvokeContext] = None,
38+
cause: Optional[Exception] = None,
39+
**kwargs: Any,
40+
) -> None:
41+
super().__init__(message, invoke_context, cause, **kwargs)
42+
self.topic_name = topic_name
43+
44+
45+
class TopicSubscriptionError(TopicError):
46+
"""Raised when topic subscription fails."""
47+
48+
pass
49+
50+
51+
class TopicPublicationError(TopicError):
52+
"""Raised when publishing to a topic fails."""
53+
54+
pass
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
"""
2+
Tool-related exception classes.
3+
"""
4+
5+
from typing import Any
6+
from typing import Optional
7+
8+
from grafi.common.exceptions.base import GrafiError
9+
from grafi.common.models.invoke_context import InvokeContext
10+
11+
12+
class ToolInvocationError(GrafiError):
13+
"""Base class for tool invocation failures."""
14+
15+
def __init__(
16+
self,
17+
tool_name: str,
18+
message: str,
19+
invoke_context: Optional[InvokeContext] = None,
20+
cause: Optional[Exception] = None,
21+
**kwargs: Any,
22+
) -> None:
23+
super().__init__(message, invoke_context, cause, **kwargs)
24+
self.tool_name = tool_name
25+
26+
27+
class LLMToolException(ToolInvocationError):
28+
"""Raised when LLM tool operations fail."""
29+
30+
def __init__(
31+
self,
32+
tool_name: str,
33+
model: Optional[str] = None,
34+
message: str = "LLM tool operation failed",
35+
invoke_context: Optional[InvokeContext] = None,
36+
cause: Optional[Exception] = None,
37+
**kwargs: Any,
38+
) -> None:
39+
super().__init__(tool_name, message, invoke_context, cause, **kwargs)
40+
self.model = model
41+
self.type = "LLM"
42+
43+
44+
class FunctionCallException(ToolInvocationError):
45+
"""Raised when function call tool operations fail."""
46+
47+
def __init__(
48+
self,
49+
tool_name: str,
50+
function_name: Optional[str] = None,
51+
message: str = "Function call operation failed",
52+
invoke_context: Optional[InvokeContext] = None,
53+
cause: Optional[Exception] = None,
54+
**kwargs: Any,
55+
) -> None:
56+
super().__init__(tool_name, message, invoke_context, cause, **kwargs)
57+
self.function_name = function_name
58+
self.type = "FunctionCall"
59+
60+
61+
class FunctionToolException(ToolInvocationError):
62+
"""Raised when generic function tool operations fail."""
63+
64+
def __init__(
65+
self,
66+
tool_name: str,
67+
operation: Optional[str] = None,
68+
message: str = "Function tool operation failed",
69+
invoke_context: Optional[InvokeContext] = None,
70+
cause: Optional[Exception] = None,
71+
**kwargs: Any,
72+
) -> None:
73+
super().__init__(tool_name, message, invoke_context, cause, **kwargs)
74+
self.operation = operation
75+
self.type = "Function"
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
"""
2+
Workflow and node-related exception classes.
3+
"""
4+
5+
from typing import Any
6+
from typing import Optional
7+
8+
from grafi.common.exceptions.base import GrafiError
9+
from grafi.common.models.invoke_context import InvokeContext
10+
11+
12+
class WorkflowError(GrafiError):
13+
"""Raised when workflow execution encounters an error."""
14+
15+
pass
16+
17+
18+
class NodeExecutionError(WorkflowError):
19+
"""Raised when a node fails during execution."""
20+
21+
def __init__(
22+
self,
23+
node_name: str,
24+
message: str,
25+
invoke_context: Optional[InvokeContext] = None,
26+
cause: Optional[Exception] = None,
27+
**kwargs: Any,
28+
) -> None:
29+
super().__init__(message, invoke_context, cause, **kwargs)
30+
self.node_name = node_name

0 commit comments

Comments
 (0)