agnt5 0.2.8a9__cp310-abi3-manylinux_2_34_aarch64.whl → 0.3.7a4__cp310-abi3-manylinux_2_34_aarch64.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.

Potentially problematic release.


This version of agnt5 might be problematic. Click here for more details.

agnt5/_sentry.py ADDED
@@ -0,0 +1,515 @@
1
+ """Sentry integration for AGNT5 SDK error tracking and monitoring.
2
+
3
+ This module provides automatic SDK error tracking to help improve AGNT5.
4
+
5
+ **Telemetry Behavior:**
6
+ - Alpha/Beta releases (e.g., 0.2.8a12, 1.0.0b3): Telemetry ENABLED by default
7
+ - Stable releases (e.g., 1.0.0, 2.1.3): Telemetry DISABLED by default
8
+
9
+ **What's Collected:**
10
+ - SDK initialization failures and crashes
11
+ - Rust FFI import errors
12
+ - Component registration failures
13
+ - Anonymized service metadata (no user code/data)
14
+
15
+ **Privacy:**
16
+ - Only SDK errors are captured (not your application errors)
17
+ - All data is anonymized (no secrets, IP addresses, or personal data)
18
+ - Full transparency in what's sent
19
+
20
+ Environment Variables:
21
+ AGNT5_DISABLE_SDK_TELEMETRY: Set to "true" to disable (for alpha/beta)
22
+ AGNT5_ENABLE_SDK_TELEMETRY: Set to "true" to enable (for stable)
23
+ AGNT5_SENTRY_ENVIRONMENT: Environment tag (default: "production")
24
+ AGNT5_SENTRY_TRACES_SAMPLE_RATE: APM trace sampling rate (default: 0.1)
25
+
26
+ Example:
27
+ # Disable telemetry in alpha/beta
28
+ export AGNT5_DISABLE_SDK_TELEMETRY="true"
29
+
30
+ # Enable telemetry in stable (to help AGNT5 team)
31
+ export AGNT5_ENABLE_SDK_TELEMETRY="true"
32
+ """
33
+
34
+ import logging
35
+ import os
36
+ import re
37
+ from typing import Any, Dict, Optional
38
+
39
+ logger = logging.getLogger(__name__)
40
+
41
+ # AGNT5-owned Sentry project for SDK error collection
42
+ # This DSN is hardcoded and sends SDK errors to the AGNT5 team
43
+ # Users can override for testing with AGNT5_SDK_SENTRY_DSN env var
44
+ AGNT5_SDK_SENTRY_DSN = os.getenv(
45
+ "AGNT5_SDK_SENTRY_DSN",
46
+ "https://a25fea6eeec2e8b393a77f1e2cc7fe2c@o4509047159521280.ingest.us.sentry.io/4509047294656512"
47
+ )
48
+
49
+ _sentry_initialized = False
50
+ _sentry_available = False
51
+
52
+ try:
53
+ import sentry_sdk
54
+ from sentry_sdk.integrations.logging import LoggingIntegration
55
+
56
+ _sentry_available = True
57
+ except ImportError:
58
+ logger.debug("sentry-sdk not installed, Sentry integration disabled")
59
+ _sentry_available = False
60
+
61
+
62
+ def is_sentry_enabled() -> bool:
63
+ """Check if Sentry integration is enabled and initialized.
64
+
65
+ Returns:
66
+ True if Sentry is available and initialized, False otherwise
67
+ """
68
+ return _sentry_initialized and _sentry_available
69
+
70
+
71
+ def _is_prerelease_version(version: str) -> bool:
72
+ """Check if SDK version is alpha or beta (pre-release).
73
+
74
+ Args:
75
+ version: Version string (e.g., "0.2.8a12", "1.0.0b3", "1.2.3")
76
+
77
+ Returns:
78
+ True if version contains 'a' (alpha) or 'b' (beta), False otherwise
79
+
80
+ Examples:
81
+ >>> _is_prerelease_version("0.2.8a12")
82
+ True
83
+ >>> _is_prerelease_version("1.0.0b3")
84
+ True
85
+ >>> _is_prerelease_version("1.2.3")
86
+ False
87
+ >>> _is_prerelease_version("1.2.3rc1")
88
+ False
89
+ """
90
+ # Match alpha (a) or beta (b) followed by digits after version number
91
+ # Pattern: <major>.<minor>.<patch>(a|b)<number>
92
+ # More robust: anchored to end and requires digits after a/b
93
+ return bool(re.search(r'\d+\.\d+\.\d+(a|b)\d+', version))
94
+
95
+
96
+ def _should_enable_telemetry(sdk_version: str) -> bool:
97
+ """Determine if SDK telemetry should be enabled based on version and env vars.
98
+
99
+ Default behavior:
100
+ - Alpha/Beta releases: ENABLED by default (users can opt-out)
101
+ - Stable releases: DISABLED by default (users can opt-in)
102
+
103
+ Environment variable overrides:
104
+ - AGNT5_DISABLE_SDK_TELEMETRY="true" → Force disable
105
+ - AGNT5_ENABLE_SDK_TELEMETRY="true" → Force enable
106
+
107
+ Args:
108
+ sdk_version: SDK version string
109
+
110
+ Returns:
111
+ True if telemetry should be enabled, False otherwise
112
+ """
113
+ # Check explicit disable flag (takes precedence)
114
+ disable_flag = os.getenv("AGNT5_DISABLE_SDK_TELEMETRY", "").lower()
115
+ if disable_flag in ("true", "1", "yes"):
116
+ logger.debug("SDK telemetry explicitly disabled via AGNT5_DISABLE_SDK_TELEMETRY")
117
+ return False
118
+
119
+ # Check explicit enable flag
120
+ enable_flag = os.getenv("AGNT5_ENABLE_SDK_TELEMETRY", "").lower()
121
+ if enable_flag in ("true", "1", "yes"):
122
+ logger.debug("SDK telemetry explicitly enabled via AGNT5_ENABLE_SDK_TELEMETRY")
123
+ return True
124
+
125
+ # Default behavior based on version
126
+ is_prerelease = _is_prerelease_version(sdk_version)
127
+
128
+ if is_prerelease:
129
+ logger.debug(f"SDK version {sdk_version} is pre-release → telemetry enabled by default")
130
+ return True
131
+ else:
132
+ logger.debug(f"SDK version {sdk_version} is stable → telemetry disabled by default")
133
+ return False
134
+
135
+
136
+ def _anonymize_event(event, hint):
137
+ """Remove potentially sensitive data before sending to Sentry.
138
+
139
+ This ensures no user secrets, environment variables, or personal data
140
+ is sent to Sentry. This includes:
141
+ - IP addresses
142
+ - Environment variables
143
+ - Stack trace local variables (may contain API keys, passwords, etc.)
144
+ - Request data and headers
145
+ - Sensitive breadcrumb data
146
+
147
+ Args:
148
+ event: Sentry event dict
149
+ hint: Event hint with exception info
150
+
151
+ Returns:
152
+ Sanitized event or None to drop the event
153
+ """
154
+ # Remove user IP address
155
+ if 'user' in event:
156
+ event['user'].pop('ip_address', None)
157
+
158
+ # Remove environment variables (might contain secrets)
159
+ if 'contexts' in event:
160
+ if 'os' in event['contexts']:
161
+ event['contexts']['os'].pop('env', None)
162
+
163
+ # Remove sensitive runtime context
164
+ if 'runtime' in event['contexts']:
165
+ event['contexts']['runtime'].pop('env', None)
166
+
167
+ # CRITICAL: Remove stack trace local variables (may contain secrets)
168
+ # Example: api_key = "sk-abc123..." in local scope
169
+ if 'exception' in event:
170
+ for exc in event['exception'].get('values', []):
171
+ if 'stacktrace' in exc:
172
+ for frame in exc['stacktrace'].get('frames', []):
173
+ # Remove all local variables from stack frames
174
+ if 'vars' in frame:
175
+ frame.pop('vars')
176
+
177
+ # Remove request data if present (may contain secrets in POST data)
178
+ if 'request' in event:
179
+ event['request'].pop('data', None)
180
+ event['request'].pop('env', None)
181
+ event['request'].pop('headers', None)
182
+ # Keep only safe request metadata
183
+ safe_request_keys = {'url', 'method', 'query_string'}
184
+ event['request'] = {k: v for k, v in event['request'].items() if k in safe_request_keys}
185
+
186
+ # Sanitize breadcrumbs (remove any data fields that might be sensitive)
187
+ if 'breadcrumbs' in event:
188
+ for crumb in event['breadcrumbs'].get('values', []):
189
+ if 'data' in crumb:
190
+ # Keep only safe metadata
191
+ safe_keys = {'category', 'level', 'message', 'timestamp', 'type'}
192
+ crumb['data'] = {k: v for k, v in crumb['data'].items() if k in safe_keys}
193
+
194
+ return event
195
+
196
+
197
+ def initialize_sentry(
198
+ service_name: str,
199
+ service_version: str,
200
+ sdk_version: str,
201
+ environment: Optional[str] = None,
202
+ traces_sample_rate: Optional[float] = None,
203
+ ) -> bool:
204
+ """Initialize Sentry SDK for automatic SDK error tracking.
205
+
206
+ This function is idempotent - calling it multiple times will not reinitialize Sentry.
207
+
208
+ **Telemetry Behavior:**
209
+ - Alpha/Beta releases: ENABLED by default (opt-out with AGNT5_DISABLE_SDK_TELEMETRY=true)
210
+ - Stable releases: DISABLED by default (opt-in with AGNT5_ENABLE_SDK_TELEMETRY=true)
211
+
212
+ **What's Collected:**
213
+ - SDK initialization failures and crashes
214
+ - Component registration errors
215
+ - Anonymized metadata (no user code, secrets, or personal data)
216
+
217
+ Args:
218
+ service_name: Name of the service (used in event context)
219
+ service_version: Version of the service (used in event context)
220
+ sdk_version: AGNT5 SDK version (determines default telemetry behavior)
221
+ environment: Environment tag (if None, reads from AGNT5_SENTRY_ENVIRONMENT, defaults to "production")
222
+ traces_sample_rate: APM sampling rate 0.0-1.0 (if None, reads from AGNT5_SENTRY_TRACES_SAMPLE_RATE, defaults to 0.1)
223
+
224
+ Returns:
225
+ True if Sentry was initialized, False if disabled or unavailable
226
+
227
+ Example:
228
+ >>> initialize_sentry("my-service", "1.0.0", "0.2.8a12")
229
+ True # Telemetry enabled (alpha version)
230
+
231
+ >>> initialize_sentry("my-service", "1.0.0", "1.0.0")
232
+ False # Telemetry disabled (stable version)
233
+ """
234
+ global _sentry_initialized
235
+
236
+ # Check if already initialized
237
+ if _sentry_initialized:
238
+ logger.debug("Sentry already initialized, skipping")
239
+ return True
240
+
241
+ # Check if Sentry SDK is available
242
+ if not _sentry_available:
243
+ logger.debug("Sentry SDK not available, skipping initialization")
244
+ return False
245
+
246
+ # Check if AGNT5 team has configured the DSN
247
+ if not AGNT5_SDK_SENTRY_DSN:
248
+ logger.debug("AGNT5_SDK_SENTRY_DSN not configured, telemetry disabled")
249
+ return False
250
+
251
+ # Determine if telemetry should be enabled based on version and env vars
252
+ if not _should_enable_telemetry(sdk_version):
253
+ is_prerelease = _is_prerelease_version(sdk_version)
254
+ if is_prerelease:
255
+ logger.info(
256
+ f"SDK telemetry disabled for pre-release version {sdk_version} "
257
+ f"(set AGNT5_ENABLE_SDK_TELEMETRY=true to enable)"
258
+ )
259
+ else:
260
+ logger.debug(
261
+ f"SDK telemetry disabled by default for stable version {sdk_version} "
262
+ f"(set AGNT5_ENABLE_SDK_TELEMETRY=true to help AGNT5 team)"
263
+ )
264
+ return False
265
+
266
+ # Get environment and sampling rate
267
+ sentry_env = environment or os.getenv("AGNT5_SENTRY_ENVIRONMENT", "production")
268
+ sample_rate_str = os.getenv("AGNT5_SENTRY_TRACES_SAMPLE_RATE", "0.1")
269
+ if traces_sample_rate is None:
270
+ try:
271
+ traces_sample_rate = float(sample_rate_str)
272
+ except ValueError:
273
+ logger.warning(
274
+ f"Invalid AGNT5_SENTRY_TRACES_SAMPLE_RATE: {sample_rate_str}, using default 0.1"
275
+ )
276
+ traces_sample_rate = 0.1
277
+
278
+ # Configure logging integration
279
+ # Capture ERROR and above automatically
280
+ logging_integration = LoggingIntegration(
281
+ level=logging.INFO, # Capture info and above as breadcrumbs
282
+ event_level=logging.ERROR, # Send errors and above as events
283
+ )
284
+
285
+ try:
286
+ # Initialize Sentry SDK with AGNT5's hardcoded DSN
287
+ sentry_sdk.init(
288
+ dsn=AGNT5_SDK_SENTRY_DSN, # Hardcoded AGNT5 Sentry project
289
+ environment=sentry_env,
290
+ release=f"agnt5-python-sdk@{sdk_version}", # SDK version, not service version
291
+ traces_sample_rate=traces_sample_rate,
292
+ integrations=[logging_integration],
293
+ # Anonymize all events before sending
294
+ before_send=_anonymize_event,
295
+ # Add default tags
296
+ default_integrations=True,
297
+ # Enable performance monitoring
298
+ enable_tracing=True,
299
+ # Attach stack traces to messages
300
+ attach_stacktrace=True,
301
+ # Max breadcrumbs to keep
302
+ max_breadcrumbs=50,
303
+ )
304
+
305
+ # Set global tags for filtering
306
+ sentry_sdk.set_tag("sdk_version", sdk_version)
307
+ sentry_sdk.set_tag("sdk_component", "python")
308
+ sentry_sdk.set_tag("is_prerelease", str(_is_prerelease_version(sdk_version)))
309
+
310
+ _sentry_initialized = True
311
+
312
+ # Log different messages based on version
313
+ is_prerelease = _is_prerelease_version(sdk_version)
314
+ if is_prerelease:
315
+ logger.info(
316
+ f"SDK telemetry enabled for alpha/beta version {sdk_version} "
317
+ f"(helps AGNT5 team find bugs). To disable: export AGNT5_DISABLE_SDK_TELEMETRY=true"
318
+ )
319
+ else:
320
+ logger.info(
321
+ f"SDK telemetry enabled for version {sdk_version} (thank you for helping improve AGNT5!)"
322
+ )
323
+
324
+ return True
325
+
326
+ except Exception as e:
327
+ logger.error(f"Failed to initialize SDK telemetry: {e}", exc_info=True)
328
+ return False
329
+
330
+
331
+ def capture_exception(
332
+ exception: Exception,
333
+ context: Optional[Dict[str, Any]] = None,
334
+ tags: Optional[Dict[str, str]] = None,
335
+ level: str = "error",
336
+ ) -> Optional[str]:
337
+ """Capture an exception and send it to Sentry.
338
+
339
+ Args:
340
+ exception: The exception to capture
341
+ context: Additional context data to attach
342
+ tags: Tags to add to this event
343
+ level: Severity level (error, warning, info)
344
+
345
+ Returns:
346
+ Event ID if captured, None if Sentry not initialized
347
+
348
+ Example:
349
+ >>> try:
350
+ ... risky_operation()
351
+ ... except Exception as e:
352
+ ... capture_exception(e, context={"run_id": "123"}, tags={"component": "workflow"})
353
+ """
354
+ if not is_sentry_enabled():
355
+ return None
356
+
357
+ with sentry_sdk.push_scope() as scope:
358
+ # Add tags
359
+ if tags:
360
+ for key, value in tags.items():
361
+ scope.set_tag(key, value)
362
+
363
+ # Add context
364
+ if context:
365
+ scope.set_context("additional_context", context)
366
+
367
+ # Set level
368
+ scope.level = level
369
+
370
+ # Capture exception
371
+ event_id = sentry_sdk.capture_exception(exception)
372
+ return event_id
373
+
374
+
375
+ def capture_message(
376
+ message: str,
377
+ level: str = "info",
378
+ context: Optional[Dict[str, Any]] = None,
379
+ tags: Optional[Dict[str, str]] = None,
380
+ ) -> Optional[str]:
381
+ """Capture a message and send it to Sentry.
382
+
383
+ Args:
384
+ message: The message to capture
385
+ level: Severity level (error, warning, info, debug)
386
+ context: Additional context data to attach
387
+ tags: Tags to add to this event
388
+
389
+ Returns:
390
+ Event ID if captured, None if Sentry not initialized
391
+
392
+ Example:
393
+ >>> capture_message("Unusual behavior detected", level="warning", tags={"component": "agent"})
394
+ """
395
+ if not is_sentry_enabled():
396
+ return None
397
+
398
+ with sentry_sdk.push_scope() as scope:
399
+ # Add tags
400
+ if tags:
401
+ for key, value in tags.items():
402
+ scope.set_tag(key, value)
403
+
404
+ # Add context
405
+ if context:
406
+ scope.set_context("additional_context", context)
407
+
408
+ # Set level
409
+ scope.level = level
410
+
411
+ # Capture message
412
+ event_id = sentry_sdk.capture_message(message, level=level)
413
+ return event_id
414
+
415
+
416
+ def add_breadcrumb(
417
+ message: str,
418
+ category: str = "default",
419
+ level: str = "info",
420
+ data: Optional[Dict[str, Any]] = None,
421
+ ) -> None:
422
+ """Add a breadcrumb to the current scope.
423
+
424
+ Breadcrumbs are a trail of events that led up to an error.
425
+
426
+ Args:
427
+ message: Breadcrumb message
428
+ category: Breadcrumb category (e.g., "execution", "state", "api")
429
+ level: Severity level
430
+ data: Additional data
431
+
432
+ Example:
433
+ >>> add_breadcrumb("Starting workflow execution", category="workflow", data={"workflow_id": "123"})
434
+ """
435
+ if not is_sentry_enabled():
436
+ return
437
+
438
+ sentry_sdk.add_breadcrumb(
439
+ message=message,
440
+ category=category,
441
+ level=level,
442
+ data=data or {},
443
+ )
444
+
445
+
446
+ def set_user(user_id: Optional[str] = None, **kwargs: Any) -> None:
447
+ """Set user information for the current scope.
448
+
449
+ Args:
450
+ user_id: User ID
451
+ **kwargs: Additional user attributes (email, username, etc.)
452
+
453
+ Example:
454
+ >>> set_user(user_id="user123", email="user@example.com")
455
+ """
456
+ if not is_sentry_enabled():
457
+ return
458
+
459
+ user_data = {}
460
+ if user_id:
461
+ user_data["id"] = user_id
462
+ user_data.update(kwargs)
463
+
464
+ sentry_sdk.set_user(user_data)
465
+
466
+
467
+ def set_context(name: str, context: Dict[str, Any]) -> None:
468
+ """Set context information for the current scope.
469
+
470
+ Args:
471
+ name: Context name (e.g., "runtime", "execution")
472
+ context: Context data
473
+
474
+ Example:
475
+ >>> set_context("runtime", {"run_id": "123", "tenant_id": "tenant456"})
476
+ """
477
+ if not is_sentry_enabled():
478
+ return
479
+
480
+ sentry_sdk.set_context(name, context)
481
+
482
+
483
+ def set_tag(key: str, value: str) -> None:
484
+ """Set a tag for the current scope.
485
+
486
+ Tags are searchable key-value pairs.
487
+
488
+ Args:
489
+ key: Tag key
490
+ value: Tag value
491
+
492
+ Example:
493
+ >>> set_tag("component_type", "workflow")
494
+ """
495
+ if not is_sentry_enabled():
496
+ return
497
+
498
+ sentry_sdk.set_tag(key, value)
499
+
500
+
501
+ def flush(timeout: float = 2.0) -> None:
502
+ """Flush pending Sentry events.
503
+
504
+ This should be called before shutdown to ensure all events are sent.
505
+
506
+ Args:
507
+ timeout: Maximum time to wait in seconds
508
+
509
+ Example:
510
+ >>> flush(timeout=5.0)
511
+ """
512
+ if not is_sentry_enabled():
513
+ return
514
+
515
+ sentry_sdk.flush(timeout=timeout)
@@ -0,0 +1,103 @@
1
+ """Centralized serialization utilities for AGNT5 SDK.
2
+
3
+ This module provides consistent JSON serialization across the SDK,
4
+ handling Pydantic models, dataclasses, and other complex types.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import dataclasses
10
+ from typing import Any
11
+
12
+ import orjson
13
+
14
+
15
+ def serialize(obj: Any) -> bytes:
16
+ """Serialize any Python object to JSON bytes.
17
+
18
+ Handles:
19
+ - Pydantic models (v1 and v2)
20
+ - Dataclasses
21
+ - Bytes (decoded as UTF-8)
22
+ - Sets (converted to lists)
23
+ - All standard JSON types
24
+
25
+ Args:
26
+ obj: Any Python object to serialize
27
+
28
+ Returns:
29
+ JSON-encoded bytes
30
+ """
31
+ return orjson.dumps(obj, default=_default_serializer)
32
+
33
+
34
+ def serialize_to_str(obj: Any) -> str:
35
+ """Serialize any Python object to JSON string.
36
+
37
+ Args:
38
+ obj: Any Python object to serialize
39
+
40
+ Returns:
41
+ JSON string
42
+ """
43
+ return serialize(obj).decode("utf-8")
44
+
45
+
46
+ def deserialize(data: bytes | str) -> Any:
47
+ """Deserialize JSON bytes or string to Python object.
48
+
49
+ Args:
50
+ data: JSON bytes or string
51
+
52
+ Returns:
53
+ Deserialized Python object
54
+ """
55
+ if isinstance(data, str):
56
+ data = data.encode("utf-8")
57
+ return orjson.loads(data)
58
+
59
+
60
+ def _default_serializer(obj: Any) -> Any:
61
+ """Default serializer for orjson to handle complex types."""
62
+ # Pydantic v2
63
+ if hasattr(obj, "model_dump"):
64
+ return obj.model_dump()
65
+ # Pydantic v1
66
+ if hasattr(obj, "dict") and hasattr(obj, "__fields__"):
67
+ return obj.dict()
68
+ # Dataclasses
69
+ if dataclasses.is_dataclass(obj) and not isinstance(obj, type):
70
+ return dataclasses.asdict(obj)
71
+ # Bytes
72
+ if isinstance(obj, bytes):
73
+ return obj.decode("utf-8", errors="replace")
74
+ # Sets
75
+ if isinstance(obj, set):
76
+ return list(obj)
77
+ # Let orjson raise for unknown types
78
+ raise TypeError(f"Object of type {type(obj).__name__} is not JSON serializable")
79
+
80
+
81
+ def normalize_metadata(metadata: dict[str, Any]) -> dict[str, str]:
82
+ """Convert metadata dict to string values for Rust FFI compatibility.
83
+
84
+ PyO3 requires HashMap<String, String>, but Python code may include
85
+ booleans, integers, or other types.
86
+
87
+ Args:
88
+ metadata: Dictionary with potentially mixed types
89
+
90
+ Returns:
91
+ Dictionary with all string values
92
+ """
93
+ result: dict[str, str] = {}
94
+ for key, value in metadata.items():
95
+ if isinstance(value, str):
96
+ result[key] = value
97
+ elif isinstance(value, bool):
98
+ result[key] = str(value).lower()
99
+ elif value is None:
100
+ result[key] = ""
101
+ else:
102
+ result[key] = str(value)
103
+ return result