kailash 0.4.0__py3-none-any.whl → 0.4.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,148 @@
1
+ """Vision utilities for AI providers - lazy loaded to avoid overhead."""
2
+
3
+ from pathlib import Path
4
+ from typing import Optional, Tuple
5
+
6
+
7
+ def encode_image(image_path: str) -> str:
8
+ """
9
+ Encode image file to base64 string.
10
+
11
+ Args:
12
+ image_path: Path to the image file
13
+
14
+ Returns:
15
+ Base64 encoded string of the image
16
+
17
+ Raises:
18
+ FileNotFoundError: If image file doesn't exist
19
+ IOError: If unable to read the image file
20
+ """
21
+ # Lazy import to avoid overhead when not using vision
22
+ import base64
23
+
24
+ image_path = Path(image_path).resolve()
25
+ if not image_path.exists():
26
+ raise FileNotFoundError(f"Image file not found: {image_path}")
27
+
28
+ try:
29
+ with open(image_path, "rb") as image_file:
30
+ return base64.b64encode(image_file.read()).decode("utf-8")
31
+ except Exception as e:
32
+ raise IOError(f"Failed to read image file: {e}")
33
+
34
+
35
+ def get_media_type(image_path: str) -> str:
36
+ """
37
+ Get media type from file extension.
38
+
39
+ Args:
40
+ image_path: Path to the image file
41
+
42
+ Returns:
43
+ Media type string (e.g., "image/jpeg")
44
+ """
45
+ ext = Path(image_path).suffix.lower()
46
+ media_types = {
47
+ ".jpg": "image/jpeg",
48
+ ".jpeg": "image/jpeg",
49
+ ".png": "image/png",
50
+ ".gif": "image/gif",
51
+ ".webp": "image/webp",
52
+ ".bmp": "image/bmp",
53
+ ".tiff": "image/tiff",
54
+ ".tif": "image/tiff",
55
+ }
56
+ return media_types.get(ext, "image/jpeg")
57
+
58
+
59
+ def validate_image_size(
60
+ image_path: str, max_size_mb: float = 20.0
61
+ ) -> Tuple[bool, Optional[str]]:
62
+ """
63
+ Validate image file size.
64
+
65
+ Args:
66
+ image_path: Path to the image file
67
+ max_size_mb: Maximum allowed size in megabytes
68
+
69
+ Returns:
70
+ Tuple of (is_valid, error_message)
71
+ """
72
+ import os
73
+
74
+ try:
75
+ size_bytes = os.path.getsize(image_path)
76
+ size_mb = size_bytes / (1024 * 1024)
77
+
78
+ if size_mb > max_size_mb:
79
+ return False, f"Image size {size_mb:.1f}MB exceeds maximum {max_size_mb}MB"
80
+
81
+ return True, None
82
+ except Exception as e:
83
+ return False, f"Failed to check image size: {e}"
84
+
85
+
86
+ def resize_image_if_needed(
87
+ image_path: str, max_size_mb: float = 20.0, max_dimension: int = 4096
88
+ ) -> Optional[str]:
89
+ """
90
+ Resize image if it exceeds size or dimension limits.
91
+
92
+ Args:
93
+ image_path: Path to the image file
94
+ max_size_mb: Maximum file size in MB
95
+ max_dimension: Maximum width or height in pixels
96
+
97
+ Returns:
98
+ Base64 encoded resized image, or None if no resize needed
99
+ """
100
+ try:
101
+ # Lazy import to avoid PIL dependency when not using vision
102
+ import base64
103
+ import io
104
+
105
+ from PIL import Image
106
+
107
+ # Check if resize is needed
108
+ is_valid, _ = validate_image_size(image_path, max_size_mb)
109
+
110
+ with Image.open(image_path) as img:
111
+ # Check dimensions
112
+ needs_resize = (
113
+ not is_valid or img.width > max_dimension or img.height > max_dimension
114
+ )
115
+
116
+ if not needs_resize:
117
+ return None
118
+
119
+ # Calculate new size maintaining aspect ratio
120
+ ratio = min(max_dimension / img.width, max_dimension / img.height, 1.0)
121
+ new_size = (int(img.width * ratio), int(img.height * ratio))
122
+
123
+ # Resize image
124
+ img = img.resize(new_size, Image.Resampling.LANCZOS)
125
+
126
+ # Convert to RGB if necessary (for JPEG)
127
+ if img.mode not in ("RGB", "L"):
128
+ img = img.convert("RGB")
129
+
130
+ # Save to bytes
131
+ output = io.BytesIO()
132
+ img_format = (
133
+ "JPEG"
134
+ if Path(image_path).suffix.lower() in [".jpg", ".jpeg"]
135
+ else "PNG"
136
+ )
137
+ img.save(output, format=img_format, optimize=True, quality=85)
138
+
139
+ # Encode to base64
140
+ output.seek(0)
141
+ return base64.b64encode(output.read()).decode("utf-8")
142
+
143
+ except ImportError:
144
+ # PIL not available, skip resizing
145
+ return None
146
+ except Exception:
147
+ # Any error in resizing, return None to use original
148
+ return None
@@ -0,0 +1,26 @@
1
+ """Alert and notification nodes for the Kailash SDK.
2
+
3
+ This module provides specialized nodes for sending alerts and notifications
4
+ through various channels. Each alert node follows a consistent interface while
5
+ providing channel-specific features and optimizations.
6
+
7
+ The module includes:
8
+ - Base alert node infrastructure
9
+ - Discord webhook integration
10
+ - (Future) Slack, email, webhook, and other integrations
11
+
12
+ Design Philosophy:
13
+ - Provide purpose-built nodes for common alert patterns
14
+ - Abstract channel-specific complexity
15
+ - Support both simple and advanced use cases
16
+ - Enable consistent alert formatting across channels
17
+ """
18
+
19
+ from .base import AlertNode, AlertSeverity
20
+ from .discord import DiscordAlertNode
21
+
22
+ __all__ = [
23
+ "AlertNode",
24
+ "AlertSeverity",
25
+ "DiscordAlertNode",
26
+ ]
@@ -0,0 +1,234 @@
1
+ """Base alert node class for the Kailash SDK.
2
+
3
+ This module provides the foundation for all alert and notification nodes in the system.
4
+ It defines common parameters, severity levels, and formatting utilities that are shared
5
+ across different alert implementations.
6
+
7
+ The alert system is designed to provide:
8
+ - Consistent interface across different notification channels
9
+ - Severity-based formatting and colors
10
+ - Structured context data support
11
+ - Easy extensibility for new alert types
12
+ """
13
+
14
+ from abc import abstractmethod
15
+ from enum import Enum
16
+ from typing import Any
17
+
18
+ from kailash.nodes.base import Node, NodeParameter
19
+
20
+
21
+ class AlertSeverity(str, Enum):
22
+ """Standard alert severity levels with associated colors."""
23
+
24
+ SUCCESS = "success"
25
+ WARNING = "warning"
26
+ ERROR = "error"
27
+ CRITICAL = "critical"
28
+ INFO = "info"
29
+
30
+ def get_color(self) -> int:
31
+ """Get the color code for this severity level (Discord/Slack compatible)."""
32
+ colors = {
33
+ AlertSeverity.SUCCESS: 0x28A745, # Green
34
+ AlertSeverity.WARNING: 0xFFC107, # Yellow/Amber
35
+ AlertSeverity.ERROR: 0xDC3545, # Red
36
+ AlertSeverity.CRITICAL: 0x8B0000, # Dark Red
37
+ AlertSeverity.INFO: 0x007BFF, # Blue
38
+ }
39
+ return colors.get(self, 0x808080) # Default to gray
40
+
41
+
42
+ class AlertNode(Node):
43
+ """
44
+ Base class for all alert and notification nodes in the Kailash SDK.
45
+
46
+ This abstract base class provides common functionality for sending alerts
47
+ through various channels (Discord, Slack, email, webhooks, etc.). It defines
48
+ standard parameters that all alert nodes should support and provides utilities
49
+ for formatting messages consistently.
50
+
51
+ Design Philosophy:
52
+ Alert nodes should provide a simple, consistent interface for sending
53
+ notifications while allowing channel-specific features when needed.
54
+ The base class handles common concerns like severity levels, titles,
55
+ and context data, while subclasses implement channel-specific logic.
56
+
57
+ Common Parameters:
58
+ - alert_type: Severity level (success, warning, error, critical, info)
59
+ - title: Alert title/subject
60
+ - message: Main alert message body
61
+ - context: Additional structured data
62
+
63
+ Subclasses must implement:
64
+ - get_channel_parameters(): Define channel-specific parameters
65
+ - send_alert(): Implement the actual alert sending logic
66
+ """
67
+
68
+ category = "alerts"
69
+
70
+ def get_parameters(self) -> dict[str, NodeParameter]:
71
+ """Define common parameters for all alert nodes.
72
+
73
+ Returns:
74
+ Dictionary of common alert parameters merged with channel-specific ones
75
+ """
76
+ common_params = {
77
+ "alert_type": NodeParameter(
78
+ name="alert_type",
79
+ type=str,
80
+ required=False,
81
+ default="info",
82
+ description="Alert severity level: success, warning, error, critical, info",
83
+ ),
84
+ "title": NodeParameter(
85
+ name="title",
86
+ type=str,
87
+ required=True,
88
+ description="Alert title or subject",
89
+ ),
90
+ "message": NodeParameter(
91
+ name="message",
92
+ type=str,
93
+ required=False,
94
+ default="",
95
+ description="Main alert message body",
96
+ ),
97
+ "context": NodeParameter(
98
+ name="context",
99
+ type=dict,
100
+ required=False,
101
+ default={},
102
+ description="Additional context data to include in the alert",
103
+ ),
104
+ }
105
+
106
+ # Merge with channel-specific parameters
107
+ channel_params = self.get_channel_parameters()
108
+ return {**common_params, **channel_params}
109
+
110
+ @abstractmethod
111
+ def get_channel_parameters(self) -> dict[str, NodeParameter]:
112
+ """Define channel-specific parameters.
113
+
114
+ Subclasses must implement this to add their specific parameters
115
+ like webhook URLs, authentication tokens, formatting options, etc.
116
+
117
+ Returns:
118
+ Dictionary of channel-specific parameters
119
+ """
120
+ pass
121
+
122
+ def validate_alert_type(self, alert_type: str) -> AlertSeverity:
123
+ """Validate and normalize the alert type.
124
+
125
+ Args:
126
+ alert_type: String representation of alert severity
127
+
128
+ Returns:
129
+ Normalized AlertSeverity enum value
130
+
131
+ Raises:
132
+ ValueError: If alert_type is not valid
133
+ """
134
+ try:
135
+ return AlertSeverity(alert_type.lower())
136
+ except ValueError:
137
+ valid_types = [s.value for s in AlertSeverity]
138
+ raise ValueError(
139
+ f"Invalid alert_type '{alert_type}'. Must be one of: {', '.join(valid_types)}"
140
+ )
141
+
142
+ def format_context(self, context: dict[str, Any]) -> str:
143
+ """Format context dictionary for display.
144
+
145
+ Args:
146
+ context: Dictionary of context data
147
+
148
+ Returns:
149
+ Formatted string representation of context
150
+ """
151
+ if not context:
152
+ return ""
153
+
154
+ lines = []
155
+ for key, value in context.items():
156
+ # Handle nested dictionaries and lists
157
+ if isinstance(value, (dict, list)):
158
+ import json
159
+
160
+ value_str = json.dumps(value) # No indent for single-line format
161
+ else:
162
+ value_str = str(value)
163
+
164
+ lines.append(f"**{key}**: {value_str}")
165
+
166
+ return "\n".join(lines)
167
+
168
+ def run(self, **kwargs) -> dict[str, Any]:
169
+ """Execute the alert node.
170
+
171
+ This method validates common parameters, normalizes the alert type,
172
+ and delegates to the subclass's send_alert method.
173
+
174
+ Args:
175
+ **kwargs: Alert parameters including common and channel-specific ones
176
+
177
+ Returns:
178
+ Dictionary with alert execution results
179
+ """
180
+ # Validate and normalize alert type
181
+ alert_type_str = kwargs.get("alert_type", "info")
182
+ alert_severity = self.validate_alert_type(alert_type_str)
183
+
184
+ # Extract common parameters
185
+ title = kwargs["title"]
186
+ message = kwargs.get("message", "")
187
+ context = kwargs.get("context", {})
188
+
189
+ # Extract channel-specific parameters only
190
+ channel_params = {
191
+ k: v
192
+ for k, v in kwargs.items()
193
+ if k not in ["alert_type", "title", "message", "context"]
194
+ }
195
+
196
+ # Call subclass implementation
197
+ result = self.send_alert(
198
+ severity=alert_severity,
199
+ title=title,
200
+ message=message,
201
+ context=context,
202
+ **channel_params, # Pass only channel-specific params
203
+ )
204
+
205
+ # Add standard metadata to result
206
+ result["alert_type"] = alert_severity.value
207
+ result["title"] = title
208
+
209
+ return result
210
+
211
+ @abstractmethod
212
+ def send_alert(
213
+ self,
214
+ severity: AlertSeverity,
215
+ title: str,
216
+ message: str,
217
+ context: dict[str, Any],
218
+ **kwargs,
219
+ ) -> dict[str, Any]:
220
+ """Send the alert through the specific channel.
221
+
222
+ Subclasses must implement this method to handle the actual alert delivery.
223
+
224
+ Args:
225
+ severity: Normalized alert severity
226
+ title: Alert title
227
+ message: Alert message body
228
+ context: Additional context data
229
+ **kwargs: Channel-specific parameters
230
+
231
+ Returns:
232
+ Dictionary with channel-specific response data
233
+ """
234
+ pass