netra-sdk 0.1.42__py3-none-any.whl → 0.1.44__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.

Potentially problematic release.


This version of netra-sdk might be problematic. Click here for more details.

netra/config.py CHANGED
@@ -28,7 +28,9 @@ class Config:
28
28
  LIBRARY_NAME = "netra"
29
29
  LIBRARY_VERSION = __version__
30
30
  # Maximum length for any attribute value (strings and bytes). Processors should honor this.
31
- ATTRIBUTE_MAX_LEN = 1000
31
+ ATTRIBUTE_MAX_LEN = 2000
32
+ # Maximum length specifically for conversation entry content (strings or JSON when serialized)
33
+ CONVERSATION_CONTENT_MAX_LEN = 1000
32
34
 
33
35
  def __init__(
34
36
  self,
netra/session_manager.py CHANGED
@@ -13,6 +13,7 @@ from opentelemetry import context as otel_context
13
13
  from opentelemetry import trace
14
14
 
15
15
  from netra.config import Config
16
+ from netra.utils import process_content_for_max_len
16
17
 
17
18
  logger = logging.getLogger(__name__)
18
19
 
@@ -279,6 +280,10 @@ class SessionManager:
279
280
 
280
281
  if not isinstance(role, str):
281
282
  raise TypeError(f"role must be a string, got {type(role)}")
283
+
284
+ if not isinstance(content, (str, dict)):
285
+ raise TypeError(f"content must be a string or dict, got {type(content)}")
286
+
282
287
  if not role:
283
288
  raise ValueError("role must be a non-empty string")
284
289
 
@@ -286,11 +291,14 @@ class SessionManager:
286
291
  raise ValueError("content must not be empty")
287
292
 
288
293
  try:
294
+
295
+ # Get active recording span
289
296
  span = trace.get_current_span()
290
297
  if not (span and getattr(span, "is_recording", lambda: False)()):
291
298
  logger.warning("No active span to add conversation attribute.")
292
299
  return
293
300
 
301
+ # Load existing conversation (JSON string -> list)
294
302
  existing: List[Dict[str, Any]] = []
295
303
  raw_data = None
296
304
 
@@ -300,10 +308,12 @@ class SessionManager:
300
308
  raw_data = attrs.get("conversation")
301
309
  except Exception:
302
310
  logger.exception("Failed to retrieve conversation attribute")
311
+
303
312
  if raw_data:
304
313
  try:
305
314
  import json
306
315
 
316
+ parsed: Any = None
307
317
  if isinstance(raw_data, str):
308
318
  parsed = json.loads(raw_data)
309
319
  if isinstance(parsed, list):
@@ -311,16 +321,30 @@ class SessionManager:
311
321
  except Exception:
312
322
  existing = []
313
323
 
314
- # Append new entry
315
- entry: Dict[str, Any] = {"type": normalized_type, "role": role, "content": content}
316
- # Add value_type and media_type based on value type for backend parsing
317
- if isinstance(content, str):
324
+ # Enforce per-entry content length limit without breaking the entire conversation structure
325
+ max_len = Config.CONVERSATION_CONTENT_MAX_LEN
326
+ processed_content = process_content_for_max_len(content, max_len)
327
+
328
+ # Create a conversation entry
329
+ entry: Dict[str, Any] = {"type": normalized_type, "role": role, "content": processed_content}
330
+
331
+ # Add format based on processed value type for backend parsing
332
+ if isinstance(processed_content, str):
318
333
  entry["format"] = "text"
319
- elif isinstance(content, dict):
334
+ elif isinstance(processed_content, dict):
320
335
  entry["format"] = "json"
321
336
  existing.append(entry)
322
337
 
323
- SessionManager.set_attribute_on_active_span("conversation", existing)
338
+ # Bypass global attribute value truncation by writing directly to the span's
339
+ # private attribute store. We intentionally avoid span.set_attribute here.
340
+ try:
341
+ import json
342
+
343
+ payload = json.dumps(existing, default=str)
344
+ attrs = getattr(span, "_attributes", None)
345
+ attrs["conversation"] = payload # type: ignore[index]
346
+ except Exception:
347
+ logger.exception("Failed to set conversation attribute directly on span")
324
348
  except Exception as e:
325
349
  logger.exception("Failed to add conversation attribute: %s", e)
326
350
 
netra/tracer.py CHANGED
@@ -57,50 +57,74 @@ class FilteringSpanExporter(SpanExporter): # type: ignore[misc]
57
57
 
58
58
  def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult:
59
59
  filtered: List[ReadableSpan] = []
60
- for s in spans:
61
- name = getattr(s, "name", None)
62
- if name is None:
63
- filtered.append(s)
60
+ blocked_parent_map: Dict[Any, Any] = {}
61
+ for span in spans:
62
+ name = getattr(span, "name", None)
63
+ if name is None or not self._is_blocked(name):
64
+ filtered.append(span)
64
65
  continue
65
- # Only apply blocked span patterns to root-level spans (no valid parent)
66
- parent = getattr(s, "parent", None)
67
- # Determine if the span has a valid parent. SpanContext.is_valid may be a property or method.
68
- has_valid_parent = False
69
- if parent is not None:
70
- is_valid_attr = getattr(parent, "is_valid", None)
71
- if callable(is_valid_attr):
72
- try:
73
- has_valid_parent = bool(is_valid_attr())
74
- except Exception:
75
- has_valid_parent = False
76
- else:
77
- has_valid_parent = bool(is_valid_attr)
78
-
79
- is_root_span = parent is None or not has_valid_parent
80
-
81
- if is_root_span:
82
- # Apply name-based blocking only for root spans
83
- if name in self._exact:
84
- continue
85
- blocked = False
86
- for pref in self._prefixes:
87
- if name.startswith(pref):
88
- blocked = True
89
- break
90
- if not blocked and self._suffixes:
91
- for suf in self._suffixes:
92
- if name.endswith(suf):
93
- blocked = True
94
- break
95
- if not blocked:
96
- filtered.append(s)
97
- else:
98
- # Do not block child spans based on name
99
- filtered.append(s)
66
+
67
+ span_context = getattr(span, "context", None)
68
+ span_id = getattr(span_context, "span_id", None) if span_context else None
69
+ if span_id is not None:
70
+ blocked_parent_map[span_id] = getattr(span, "parent", None)
71
+ if blocked_parent_map:
72
+ self._reparent_blocked_children(filtered, blocked_parent_map)
100
73
  if not filtered:
101
74
  return SpanExportResult.SUCCESS
102
75
  return self._exporter.export(filtered)
103
76
 
77
+ def _is_blocked(self, name: str) -> bool:
78
+ if name in self._exact:
79
+ return True
80
+ for pref in self._prefixes:
81
+ if name.startswith(pref):
82
+ return True
83
+ for suf in self._suffixes:
84
+ if name.endswith(suf):
85
+ return True
86
+ return False
87
+
88
+ def _reparent_blocked_children(
89
+ self,
90
+ spans: Sequence[ReadableSpan],
91
+ blocked_parent_map: Dict[Any, Any],
92
+ ) -> None:
93
+ if not blocked_parent_map:
94
+ return
95
+
96
+ for span in spans:
97
+ parent_context = getattr(span, "parent", None)
98
+ if parent_context is None:
99
+ continue
100
+
101
+ updated_parent = parent_context
102
+ visited: set[Any] = set()
103
+ changed = False
104
+
105
+ while updated_parent is not None:
106
+ parent_span_id = getattr(updated_parent, "span_id", None)
107
+ if parent_span_id not in blocked_parent_map or parent_span_id in visited:
108
+ break
109
+ visited.add(parent_span_id)
110
+ updated_parent = blocked_parent_map[parent_span_id]
111
+ changed = True
112
+
113
+ if changed:
114
+ self._set_span_parent(span, updated_parent)
115
+
116
+ def _set_span_parent(self, span: ReadableSpan, parent: Any) -> None:
117
+ if hasattr(span, "_parent"):
118
+ try:
119
+ span._parent = parent
120
+ return
121
+ except Exception:
122
+ pass
123
+ try:
124
+ setattr(span, "parent", parent)
125
+ except Exception:
126
+ logger.debug("Failed to reparent span %s", getattr(span, "name", "<unknown>"), exc_info=True)
127
+
104
128
  def shutdown(self) -> None:
105
129
  try:
106
130
  self._exporter.shutdown()
netra/utils.py ADDED
@@ -0,0 +1,81 @@
1
+ """General utility helpers for Netra SDK.
2
+
3
+ This module centralizes common helpers that can be reused across the codebase.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from typing import Any
9
+
10
+
11
+ def truncate_string(value: str, max_len: int) -> str:
12
+ """Truncate a string to max_len characters.
13
+
14
+ If value is not a string, it is returned unchanged.
15
+ """
16
+ try:
17
+ if not isinstance(value, str):
18
+ return value
19
+ return value if len(value) <= max_len else value[:max_len]
20
+ except Exception:
21
+ return value
22
+
23
+
24
+ def truncate_and_repair_json(content: Any, max_len: int) -> Any:
25
+ """Truncate a dict/list by JSON-serializing and hard-cutting, then attempt repair.
26
+
27
+ The function will:
28
+ - json.dumps(content, default=str)
29
+ - hard-cut the string to max_len
30
+ - try to repair using `json-repair` (optional dependency)
31
+ - parse back with json.loads
32
+
33
+ On failure, returns a minimal safe container with a preview of the truncated text.
34
+ """
35
+ try:
36
+ import json
37
+
38
+ json_str = json.dumps(content, default=str)
39
+ if len(json_str) <= max_len:
40
+ return content
41
+
42
+ truncated = json_str[:max_len]
43
+
44
+ # Try json_repair if available
45
+ repaired_obj: Any = None
46
+ try:
47
+ try:
48
+ from json_repair import repair_json as _repair_json
49
+ except Exception: # pragma: no cover - optional dependency not installed
50
+ _repair_json = None
51
+
52
+ if _repair_json is not None:
53
+ repaired_str = _repair_json(truncated)
54
+ repaired_obj = json.loads(repaired_str)
55
+ except Exception:
56
+ repaired_obj = None
57
+
58
+ if repaired_obj is not None:
59
+ return repaired_obj
60
+
61
+ # Fallback: safe container preserving a preview
62
+ return {"__truncated__": True, "preview": truncated}
63
+ except Exception:
64
+ # If anything goes wrong, return original content as-is
65
+ return content
66
+
67
+
68
+ def process_content_for_max_len(content: Any, max_len: int) -> Any:
69
+ """Ensure the content fits within max_len when serialized.
70
+
71
+ - If content is a string: truncate to max_len.
72
+ - If content is a dict or list: attempt truncate+repair to keep it valid JSON.
73
+ """
74
+ try:
75
+ if isinstance(content, str):
76
+ return truncate_string(content, max_len)
77
+ if isinstance(content, (dict, list)):
78
+ return truncate_and_repair_json(content, max_len)
79
+ return content
80
+ except Exception:
81
+ return content
netra/version.py CHANGED
@@ -1 +1 @@
1
- __version__ = "0.1.42"
1
+ __version__ = "0.1.44"
@@ -1,8 +1,9 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: netra-sdk
3
- Version: 0.1.42
3
+ Version: 0.1.44
4
4
  Summary: A Python SDK for AI application observability that provides OpenTelemetry-based monitoring, tracing, and PII protection for LLM and vector database applications. Enables easy instrumentation, session tracking, and privacy-focused data collection for AI systems in production environments.
5
- License: Apache-2.0
5
+ License-Expression: Apache-2.0
6
+ License-File: LICENCE
6
7
  Keywords: netra,tracing,observability,sdk,ai,llm,vector,database
7
8
  Author: Sooraj Thomas
8
9
  Author-email: sooraj@keyvalue.systems
@@ -19,6 +20,7 @@ Classifier: Programming Language :: Python :: 3.13
19
20
  Classifier: Typing :: Typed
20
21
  Provides-Extra: llm-guard
21
22
  Provides-Extra: presidio
23
+ Requires-Dist: json-repair (==0.44.1)
22
24
  Requires-Dist: llm-guard (==0.3.16) ; extra == "llm-guard"
23
25
  Requires-Dist: opentelemetry-api (>=1.34.0,<2.0.0)
24
26
  Requires-Dist: opentelemetry-instrumentation-aio-pika (>=0.55b1,<1.0.0)
@@ -3,7 +3,7 @@ netra/anonymizer/__init__.py,sha256=KeGPPZqKVZbtkbirEKYTYhj6aZHlakjdQhD7QHqBRio,
3
3
  netra/anonymizer/anonymizer.py,sha256=IcrYkdwWrFauGWUeAW-0RwrSUM8VSZCFNtoywZhvIqU,3778
4
4
  netra/anonymizer/base.py,sha256=ytPxHCUD2OXlEY6fNTuMmwImNdIjgj294I41FIgoXpU,5946
5
5
  netra/anonymizer/fp_anonymizer.py,sha256=_6svIYmE0eejdIMkhKBUWCNjGtGimtrGtbLvPSOp8W4,6493
6
- netra/config.py,sha256=51m8R0NoOrw58gMV7arOniEuFdJ7EIu3PNdFtIQ5xfg,6893
6
+ netra/config.py,sha256=MMSAKrX_HCZ7QECjTocs_jsNFgcOnbP8aAVfOdq9YEs,7032
7
7
  netra/decorators.py,sha256=qZFHrwdj10FsTFqggo3XjdGB12aMxsrrDMMmslDqZ-0,17424
8
8
  netra/exceptions/__init__.py,sha256=uDgcBxmC4WhdS7HRYQk_TtJyxH1s1o6wZmcsnSHLAcM,174
9
9
  netra/exceptions/injection.py,sha256=ke4eUXRYUFJkMZgdSyPPkPt5PdxToTI6xLEBI0hTWUQ,1332
@@ -45,11 +45,12 @@ netra/processors/instrumentation_span_processor.py,sha256=VzurzwtGleFltxzKD_gjVk
45
45
  netra/processors/scrubbing_span_processor.py,sha256=dJ86Ncmjvmrhm_uAdGTwcGvRpZbVVWqD9AOFwEMWHZY,6701
46
46
  netra/processors/session_span_processor.py,sha256=qcsBl-LnILWefsftI8NQhXDGb94OWPc8LvzhVA0JS_c,2432
47
47
  netra/scanner.py,sha256=kyDpeZiscCPb6pjuhS-sfsVj-dviBFRepdUWh0sLoEY,11554
48
- netra/session_manager.py,sha256=iU5UG_Y-fPMoyRxjfEqYCBxzzmcSVJi411Ni6gL7g9w,12683
48
+ netra/session_manager.py,sha256=VzmSAiP63ODCuOWv-irsxyU2LvHoqjOBUuXtyxboBU0,13740
49
49
  netra/span_wrapper.py,sha256=IygQX78xQRlL_Z1MfKfUbv0okihx92qNClnRlYFtRNc,8004
50
- netra/tracer.py,sha256=FJO8Cine-WL9K_4wn6RVjQOgX6c1JCp_8QowUbRSVHk,7718
51
- netra/version.py,sha256=a0ON039K5sX117g1thh7kP35cYMBjBhhhU9A-PERuT0,23
52
- netra_sdk-0.1.42.dist-info/LICENCE,sha256=8B_UoZ-BAl0AqiHAHUETCgd3I2B9yYJ1WEQtVb_qFMA,11359
53
- netra_sdk-0.1.42.dist-info/METADATA,sha256=ogWH-5_FBACadeZamwnUqoiAiIkRclRg5561U_a0Lqc,28137
54
- netra_sdk-0.1.42.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
55
- netra_sdk-0.1.42.dist-info/RECORD,,
50
+ netra/tracer.py,sha256=3s_ZAHgpfeegcnA43KVEinEPVyo3kGi_0bvSrQcKljk,8348
51
+ netra/utils.py,sha256=FblSzI8qMTfEbusakGBKE9CNELW0GEBHl09mPPxgI-w,2521
52
+ netra/version.py,sha256=AoJtnEXXv6E20uj57ChQUsGoLfKG8mvSQpdz97tcyis,23
53
+ netra_sdk-0.1.44.dist-info/METADATA,sha256=f7QrKDQQtQyaaAzIzGFF44KOpHpNW00wrZ3KIPt0zvA,28208
54
+ netra_sdk-0.1.44.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
55
+ netra_sdk-0.1.44.dist-info/licenses/LICENCE,sha256=8B_UoZ-BAl0AqiHAHUETCgd3I2B9yYJ1WEQtVb_qFMA,11359
56
+ netra_sdk-0.1.44.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 2.1.3
2
+ Generator: poetry-core 2.2.1
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any