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

@@ -12,6 +12,9 @@ from typing import Any, AsyncIterator, Callable, Dict, Iterator, Tuple
12
12
 
13
13
  from opentelemetry import context as context_api
14
14
  from opentelemetry.instrumentation.utils import _SUPPRESS_INSTRUMENTATION_KEY
15
+ from opentelemetry.semconv_ai import (
16
+ SpanAttributes,
17
+ )
15
18
  from opentelemetry.trace import Span, SpanKind, Tracer
16
19
  from opentelemetry.trace.status import Status, StatusCode
17
20
  from wrapt import ObjectProxy
@@ -55,34 +58,39 @@ def set_request_attributes(span: Span, kwargs: Dict[str, Any], operation_type: s
55
58
  return
56
59
 
57
60
  # Set operation type
58
- span.set_attribute("llm.request.type", operation_type)
61
+ span.set_attribute(f"{SpanAttributes.LLM_REQUEST_TYPE}", operation_type)
59
62
 
60
63
  # Common attributes
61
64
  if kwargs.get("model"):
62
- span.set_attribute("llm.request.model", kwargs["model"])
65
+ span.set_attribute(f"{SpanAttributes.LLM_REQUEST_MODEL}", kwargs["model"])
63
66
 
64
67
  if kwargs.get("temperature") is not None:
65
- span.set_attribute("llm.request.temperature", kwargs["temperature"])
68
+ span.set_attribute(f"{SpanAttributes.LLM_REQUEST_TEMPERATURE}", kwargs["temperature"])
66
69
 
67
70
  if kwargs.get("max_tokens") is not None:
68
- span.set_attribute("llm.request.max_tokens", kwargs["max_tokens"])
71
+ span.set_attribute(f"{SpanAttributes.LLM_REQUEST_MAX_TOKENS}", kwargs["max_tokens"])
69
72
 
70
73
  if kwargs.get("stream") is not None:
71
- span.set_attribute("llm.stream", kwargs["stream"])
74
+ span.set_attribute("gen_ai.stream", kwargs["stream"])
72
75
 
73
76
  # Chat-specific attributes
74
77
  if operation_type == "chat" and kwargs.get("messages"):
75
78
  messages = kwargs["messages"]
76
79
  if isinstance(messages, list) and len(messages) > 0:
77
- span.set_attribute("llm.prompts.0.role", messages[0].get("role", ""))
78
- span.set_attribute("llm.prompts.0.content", str(messages[0].get("content", "")))
80
+ for index, message in enumerate(messages):
81
+ if hasattr(message, "content"):
82
+ span.set_attribute(f"{SpanAttributes.LLM_PROMPTS}.{index}.role", "user")
83
+ span.set_attribute(f"{SpanAttributes.LLM_PROMPTS}.{index}.content", message.content)
84
+ elif isinstance(message, dict):
85
+ span.set_attribute(f"{SpanAttributes.LLM_PROMPTS}.{index}.role", message.get("role", "user"))
86
+ span.set_attribute(f"{SpanAttributes.LLM_PROMPTS}.{index}.content", str(message.get("content", "")))
79
87
 
80
88
  # Response-specific attributes
81
89
  if operation_type == "response":
82
90
  if kwargs.get("instructions"):
83
- span.set_attribute("llm.instructions", kwargs["instructions"])
91
+ span.set_attribute("gen_ai.instructions", kwargs["instructions"])
84
92
  if kwargs.get("input"):
85
- span.set_attribute("llm.input", kwargs["input"])
93
+ span.set_attribute("gen_ai.input", kwargs["input"])
86
94
 
87
95
 
88
96
  def set_response_attributes(span: Span, response_dict: Dict[str, Any]) -> None:
@@ -91,33 +99,36 @@ def set_response_attributes(span: Span, response_dict: Dict[str, Any]) -> None:
91
99
  return
92
100
 
93
101
  if response_dict.get("model"):
94
- span.set_attribute("llm.response.model", response_dict["model"])
102
+ span.set_attribute(f"{SpanAttributes.LLM_RESPONSE_MODEL}", response_dict["model"])
95
103
 
96
104
  if response_dict.get("id"):
97
- span.set_attribute("llm.response.id", response_dict["id"])
105
+ span.set_attribute("gen_ai.response.id", response_dict["id"])
98
106
 
99
107
  # Usage information
100
108
  usage = response_dict.get("usage", {})
101
109
  if usage:
102
110
  if usage.get("prompt_tokens"):
103
- span.set_attribute("llm.usage.prompt_tokens", usage["prompt_tokens"])
111
+ span.set_attribute(f"{SpanAttributes.LLM_USAGE_PROMPT_TOKENS}", usage["prompt_tokens"])
104
112
  if usage.get("completion_tokens"):
105
- span.set_attribute("llm.usage.completion_tokens", usage["completion_tokens"])
113
+ span.set_attribute(f"{SpanAttributes.LLM_USAGE_COMPLETION_TOKENS}", usage["completion_tokens"])
114
+ if usage.get("cache_read_input_token"):
115
+ span.set_attribute(f"{SpanAttributes.LLM_USAGE_CACHE_READ_INPUT_TOKENS}", usage["cache_read_input_token"])
106
116
  if usage.get("total_tokens"):
107
- span.set_attribute("llm.usage.total_tokens", usage["total_tokens"])
117
+ span.set_attribute(f"{SpanAttributes.LLM_USAGE_TOTAL_TOKENS}", usage["total_tokens"])
108
118
 
109
119
  # Response content
110
120
  choices = response_dict.get("choices", [])
111
- if choices and len(choices) > 0:
112
- first_choice = choices[0]
113
- if first_choice.get("message", {}).get("content"):
114
- span.set_attribute("llm.completions.0.content", first_choice["message"]["content"])
115
- if first_choice.get("finish_reason"):
116
- span.set_attribute("llm.completions.0.finish_reason", first_choice["finish_reason"])
121
+ for index, choice in enumerate(choices):
122
+ if choice.get("message", {}).get("role"):
123
+ span.set_attribute(f"{SpanAttributes.LLM_COMPLETIONS}.{index}.role", choice["message"]["role"])
124
+ if choice.get("message", {}).get("content"):
125
+ span.set_attribute(f"{SpanAttributes.LLM_COMPLETIONS}.{index}.content", choice["message"]["content"])
126
+ if choice.get("finish_reason"):
127
+ span.set_attribute(f"{SpanAttributes.LLM_COMPLETIONS}.{index}.finish_reason", choice["finish_reason"])
117
128
 
118
129
  # For responses.create
119
130
  if response_dict.get("output_text"):
120
- span.set_attribute("llm.response.output_text", response_dict["output_text"])
131
+ span.set_attribute("gen_ai.response.output_text", response_dict["output_text"])
121
132
 
122
133
 
123
134
  def chat_wrapper(tracer: Tracer) -> Callable[..., Any]:
netra/span_wrapper.py CHANGED
@@ -1,5 +1,6 @@
1
1
  import json
2
2
  import logging
3
+ import re
3
4
  import time
4
5
  from datetime import datetime, timezone
5
6
  from typing import Any, Dict, List, Literal, Optional
@@ -8,7 +9,7 @@ from opentelemetry import context as context_api
8
9
  from opentelemetry import trace
9
10
  from opentelemetry.trace import SpanKind, Status, StatusCode
10
11
  from opentelemetry.trace.propagation import set_span_in_context
11
- from pydantic import BaseModel
12
+ from pydantic import BaseModel, field_validator
12
13
 
13
14
  from netra.config import Config
14
15
 
@@ -25,6 +26,21 @@ class ActionModel(BaseModel): # type: ignore[misc]
25
26
  affected_records: Optional[List[Dict[str, str]]] = None
26
27
  metadata: Optional[Dict[str, str]] = None
27
28
 
29
+ @field_validator("start_time") # type: ignore[misc]
30
+ @classmethod
31
+ def validate_time_format(cls, value: str) -> str:
32
+ """Validate that start_time is in ISO 8601 format with microseconds and Z suffix."""
33
+ # Pattern for ISO 8601 with microseconds: YYYY-MM-DDTHH:MM:SS.ffffffZ
34
+ pattern = r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{6}Z$"
35
+
36
+ if not re.match(pattern, value):
37
+ raise ValueError(
38
+ f"start_time must be in ISO 8601 format with microseconds: "
39
+ f"YYYY-MM-DDTHH:MM:SS.ffffffZ (e.g., 2025-07-18T14:30:45.123456Z). "
40
+ f"Got: {value}"
41
+ )
42
+ return value
43
+
28
44
 
29
45
  class UsageModel(BaseModel): # type: ignore[misc]
30
46
  model: str
netra/version.py CHANGED
@@ -1,2 +1,2 @@
1
- __version__ = "0.1.13"
1
+ __version__ = "0.1.15"
2
2
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: netra-sdk
3
- Version: 0.1.13
3
+ Version: 0.1.15
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
5
  License: Apache-2.0
6
6
  Keywords: netra,tracing,observability,sdk,ai,llm,vector,database
@@ -67,7 +67,7 @@ Requires-Dist: opentelemetry-instrumentation-urllib (>=0.55b1,<1.0.0)
67
67
  Requires-Dist: opentelemetry-instrumentation-urllib3 (>=0.55b1,<1.0.0)
68
68
  Requires-Dist: opentelemetry-sdk (>=1.34.0,<2.0.0)
69
69
  Requires-Dist: presidio-analyzer (>=2.2.358,<3.0.0)
70
- Requires-Dist: traceloop-sdk (>=0.40.7,<0.41.0)
70
+ Requires-Dist: traceloop-sdk (>=0.40.7,<0.43.0)
71
71
  Project-URL: Bug Tracker, https://github.com/KeyValueSoftwareSystems/netra-sdk-py/issues
72
72
  Project-URL: Documentation, https://github.com/KeyValueSoftwareSystems/netra-sdk-py/blob/main/README.md
73
73
  Project-URL: Homepage, https://github.com/KeyValueSoftwareSystems/netra-sdk-py
@@ -482,7 +482,7 @@ Action tracking follows this schema:
482
482
  ```python
483
483
  [
484
484
  {
485
- "start_time": str, # Start time of the action
485
+ "start_time": str, # Start time of the action in ISO 8601 format with microseconds and Z suffix (e.g., 2025-07-18T14:30:45.123456Z)
486
486
  "action": str, # Type of action (e.g., "DB", "API", "CACHE")
487
487
  "action_type": str, # Action subtype (e.g., "INSERT", "SELECT", "CALL")
488
488
  "affected_records": [ # Optional: List of records affected
@@ -29,7 +29,7 @@ netra/instrumentation/mistralai/utils.py,sha256=nhdIer5gJFxuGwg8FCT222hggDHeMQDh
29
29
  netra/instrumentation/mistralai/version.py,sha256=d6593s-XBNvVxri9lr2qLUDZQ3Zk3-VXHEwdb4pj8qA,22
30
30
  netra/instrumentation/openai/__init__.py,sha256=HztqLMw8Tf30-Ydqr4N7FcvAwj-5cnGZNqI-S3wIZ_4,5143
31
31
  netra/instrumentation/openai/version.py,sha256=_J-N1qG50GykJDM356BSQf0E8LoLbB8AaC3RKho494A,23
32
- netra/instrumentation/openai/wrappers.py,sha256=hXUgWKAs2_LCKIBnScoIJt_AkHhdQKnZWk7D94UjPGU,20685
32
+ netra/instrumentation/openai/wrappers.py,sha256=4VQwIBLYaGovO9gE5TSMC-Ot84IaDuDhGqHndgR-Am4,21637
33
33
  netra/instrumentation/weaviate/__init__.py,sha256=EOlpWxobOLHYKqo_kMct_7nu26x1hr8qkeG5_h99wtg,4330
34
34
  netra/instrumentation/weaviate/version.py,sha256=PiCZHjonujPbnIn0KmD3Yl68hrjPRG_oKe5vJF3mmG8,24
35
35
  netra/pii.py,sha256=S7GnVzoNJEzKiUWnqN9bOCKPeNLsriztgB2E6Rx-yJU,27023
@@ -37,10 +37,10 @@ netra/processors/__init__.py,sha256=wfnSskRBtMT90hO7LqFJoEW374LgoH_gnTxhynqtByI,
37
37
  netra/processors/session_span_processor.py,sha256=qcsBl-LnILWefsftI8NQhXDGb94OWPc8LvzhVA0JS_c,2432
38
38
  netra/scanner.py,sha256=wqjMZnEbVvrGMiUSI352grUyHpkk94oBfHfMiXPhpGU,3866
39
39
  netra/session_manager.py,sha256=EVcnWcSj4NdkH--HmqHx0mmzivQiM4GCyFLu6lwi33M,6252
40
- netra/span_wrapper.py,sha256=BJvWMJWYX95NRMj9Yjs6ZL3BY6c5v1VGVN9i88VfewQ,7409
40
+ netra/span_wrapper.py,sha256=DA5jjXkHBUJ8_mdlYP06rcZzFoSih4gdP71Wwr3btcQ,8104
41
41
  netra/tracer.py,sha256=In5QPVLz_6BxrolWpav9EuR9_hirD2UUIlyY75QUaKk,3450
42
- netra/version.py,sha256=PBhf_BIMfa1fmMCOO5V1SBLORGuuPOjZ5FtpjeZKVI4,24
43
- netra_sdk-0.1.13.dist-info/LICENCE,sha256=8B_UoZ-BAl0AqiHAHUETCgd3I2B9yYJ1WEQtVb_qFMA,11359
44
- netra_sdk-0.1.13.dist-info/METADATA,sha256=ZTfWpwu5b22-e_OCrRaTWut7CayCEtGNE6O0hKQFbNM,25266
45
- netra_sdk-0.1.13.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
46
- netra_sdk-0.1.13.dist-info/RECORD,,
42
+ netra/version.py,sha256=s5mBMy--qmLbYWG9g95dqUqvmR31MlOMaNfvabrDFlQ,24
43
+ netra_sdk-0.1.15.dist-info/LICENCE,sha256=8B_UoZ-BAl0AqiHAHUETCgd3I2B9yYJ1WEQtVb_qFMA,11359
44
+ netra_sdk-0.1.15.dist-info/METADATA,sha256=QkYln6aEvwGYpfto8eAE2rCV-7Lk-kgqWGWcWGFxfok,25352
45
+ netra_sdk-0.1.15.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
46
+ netra_sdk-0.1.15.dist-info/RECORD,,