Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file.

The format is based on Keep a Changelog and this project adheres to Semantic Versioning.

## [0.1.74] - 2026-10-14

- Add utility for prompt management


## [0.1.73] - 2026-10-14

- Extended dependency support for opentelemetry and traceloop-sdk
Expand Down Expand Up @@ -195,4 +200,4 @@ The format is based on Keep a Changelog and this project adheres to Semantic Ver

- Added utility to set input and output data for any active span in a trace

[0.1.73]: https://github.com/KeyValueSoftwareSystems/netra-sdk-py/tree/main
[0.1.74]: https://github.com/KeyValueSoftwareSystems/netra-sdk-py/tree/main
31 changes: 28 additions & 3 deletions netra/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@
from netra.dashboard import Dashboard
from netra.evaluation import Evaluation
from netra.instrumentation import init_instrumentations
from netra.instrumentation.instruments import NetraInstruments
from netra.instrumentation.instruments import DEFAULT_INSTRUMENTS_FOR_ROOT, NetraInstruments
from netra.logging_utils import configure_package_logging
from netra.prompts import Prompts
from netra.session_manager import ConversationType, SessionManager
from netra.simulation import Simulation
from netra.span_wrapper import ActionModel, SpanType, SpanWrapper, UsageModel
Expand All @@ -23,6 +24,7 @@
"Netra",
"UsageModel",
"ActionModel",
"Prompts",
]

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -66,6 +68,7 @@ def init(
blocked_spans: Optional[List[str]] = None,
instruments: Optional[Set[NetraInstruments]] = None,
block_instruments: Optional[Set[NetraInstruments]] = None,
root_instruments: Optional[Set[NetraInstruments]] = None,
) -> None:
"""
Thread-safe initialization of Netra.
Expand All @@ -83,6 +86,12 @@ def init(
blocked_spans: List of spans to be blocked
instruments: Set of instruments to be enabled
block_instruments: Set of instruments to be blocked
root_instruments: Set of instruments allowed to produce root-level
spans. When a root span is blocked, its entire subtree is
discarded. Resolution priority:
1. Explicit ``root_instruments`` value if provided.
2. The ``instruments`` value if provided (but ``root_instruments`` is not).
3. ``DEFAULT_INSTRUMENTS_FOR_ROOT`` if neither is provided.

Returns:
None
Expand All @@ -109,8 +118,17 @@ def init(
# Configure logging based on debug mode
configure_package_logging(debug_mode=cfg.debug_mode)

# Resolve root_instruments → set of instrumentation-name strings.
resolved_root: Optional[Set[str]] = None
if root_instruments is not None:
resolved_root = {m.value for m in root_instruments}
elif instruments is not None:
resolved_root = {m.value for m in instruments}
else:
resolved_root = {m.value for m in DEFAULT_INSTRUMENTS_FOR_ROOT}

# Initialize tracer (OTLP exporter, span processor, resource)
Tracer(cfg)
Tracer(cfg, root_instrument_names=resolved_root)

# Initialize evaluation client and expose as class attribute
try:
Expand All @@ -133,6 +151,13 @@ def init(
logger.warning("Failed to initialize dashboard client: %s", e, exc_info=True)
cls.dashboard = None # type:ignore[attr-defined]

# Initialize prompts client and expose as class attribute
try:
cls.prompts = Prompts(cfg) # type:ignore[attr-defined]
except Exception as e:
logger.warning("Failed to initialize prompts client: %s", e, exc_info=True)
cls.prompts = None # type:ignore[attr-defined]

# Initialize simulation client and expose as class attribute
try:
cls.simulation = Simulation(cfg) # type:ignore[attr-defined]
Expand Down Expand Up @@ -313,4 +338,4 @@ def start_span(
return SpanWrapper(name, attributes, module_name, as_type=as_type)


__all__ = ["Netra", "UsageModel", "ActionModel", "SpanType", "EvaluationScore"]
__all__ = ["Netra", "UsageModel", "ActionModel", "SpanType", "EvaluationScore", "Prompts"]
18 changes: 8 additions & 10 deletions netra/instrumentation/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from traceloop.sdk import Instruments, Telemetry
from traceloop.sdk.utils.package_check import is_package_installed

from netra.instrumentation.instruments import CustomInstruments, NetraInstruments
from netra.instrumentation.instruments import DEFAULT_INSTRUMENTS, CustomInstruments, NetraInstruments


def init_instrumentations(
Expand All @@ -18,12 +18,15 @@ def init_instrumentations(
) -> None:
from traceloop.sdk.tracing.tracing import init_instrumentations

# When the user does not pass instruments, fall back to the curated default set.
resolved_instruments = instruments if instruments is not None else DEFAULT_INSTRUMENTS

traceloop_instruments = set()
traceloop_block_instruments = set()
netra_custom_instruments = set()
netra_custom_block_instruments = set()
if instruments:
for instrument in instruments:
if resolved_instruments:
for instrument in resolved_instruments:
if instrument.origin == CustomInstruments: # type: ignore[attr-defined]
netra_custom_instruments.add(getattr(CustomInstruments, instrument.name))
else:
Expand All @@ -36,18 +39,13 @@ def init_instrumentations(
traceloop_block_instruments.add(getattr(Instruments, instrument.name))

# If no instruments in traceloop are provided for instrumentation
if instruments and not traceloop_instruments and not traceloop_block_instruments:
if resolved_instruments and not traceloop_instruments and not traceloop_block_instruments:
traceloop_block_instruments = set(Instruments)

# If no custom instruments in netra are provided for instrumentation
if instruments and not netra_custom_instruments and not netra_custom_block_instruments:
if resolved_instruments and not netra_custom_instruments and not netra_custom_block_instruments:
netra_custom_block_instruments = set(CustomInstruments)

# If no instruments are provided for instrumentation, instrument all instruments
if not instruments and not block_instruments:
traceloop_instruments = set(Instruments)
netra_custom_instruments = set(CustomInstruments)

netra_custom_instruments = netra_custom_instruments - netra_custom_block_instruments
traceloop_instruments = traceloop_instruments - traceloop_block_instruments
if not traceloop_instruments:
Expand Down
145 changes: 138 additions & 7 deletions netra/instrumentation/instruments.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ class CustomInstruments(Enum):
ELEVENLABS = "elevenlabs"


class NetraInstruments(Enum):
class InstrumentSet(Enum):
"""Custom enum that stores the original enum class in an 'origin' attribute."""

def __new__(cls: Any, value: Any, origin: Any = None) -> Any:
Expand All @@ -82,16 +82,147 @@ def __new__(cls: Any, value: Any, origin: Any = None) -> Any:
member.origin = origin
return member

ADK = ("adk", CustomInstruments)
AIOHTTP = ("aiohttp", CustomInstruments)
AIOHTTP_SERVER = ("aiohttp_server", CustomInstruments)
AIO_PIKA = ("aio_pika", CustomInstruments)
AIOKAFKA = ("aiokafka", CustomInstruments)
AIOPG = ("aiopg", CustomInstruments)
ALEPHALPHA = ("alephalpha", Instruments)
ANTHROPIC = ("anthropic", Instruments)
ASGI = ("asgi", CustomInstruments)
ASYNCCLICK = ("asyncclick", CustomInstruments)
ASYNCIO = ("asyncio", CustomInstruments)
ASYNCPG = ("asyncpg", CustomInstruments)
AWS_LAMBDA = ("aws_lambda", CustomInstruments)
BEDROCK = ("bedrock", Instruments)
BOTO = ("boto", CustomInstruments)
BOTO3SQS = ("boto3sqs", CustomInstruments)
BOTOCORE = ("botocore", CustomInstruments)
CARTESIA = ("cartesia", CustomInstruments)
CASSANDRA = ("cassandra", CustomInstruments)
CEREBRAS = ("cerebras", CustomInstruments)
CELERY = ("celery", CustomInstruments)
CHROMA = ("chroma", Instruments)
CLICK = ("click", CustomInstruments)
COHEREAI = ("cohere_ai", CustomInstruments)
CONFLUENT_KAFKA = ("confluent_kafka", CustomInstruments)
CREWAI = ("crewai", Instruments)
DEEPGRAM = ("deepgram", CustomInstruments)
DBAPI = ("dbapi", CustomInstruments)
DJANGO = ("django", CustomInstruments)
DSPY = ("dspy", CustomInstruments)
ELASTICSEARCH = ("elasticsearch", CustomInstruments)
ELEVENLABS = ("elevenlabs", CustomInstruments)
FALCON = ("falcon", CustomInstruments)
FASTAPI = ("fastapi", CustomInstruments)
FLASK = ("flask", CustomInstruments)
GOOGLE_GENERATIVEAI = ("google_genai", CustomInstruments)
GROQ = ("groq", CustomInstruments)
GRPC = ("grpc", CustomInstruments)
HAYSTACK = ("haystack", Instruments)
HTTPX = ("httpx", CustomInstruments)
JINJA2 = ("jinja2", CustomInstruments)
KAFKA_PYTHON = ("kafka_python", CustomInstruments)
LANCEDB = ("lancedb", Instruments)
LANGCHAIN = ("langchain", Instruments)
LITELLM = ("litellm", CustomInstruments)
LLAMA_INDEX = ("llama_index", Instruments)
LOGGING = ("logging", CustomInstruments)
MARQO = ("marqo", Instruments)
MCP = ("mcp", Instruments)
MILVUS = ("milvus", Instruments)
MISTRALAI = ("mistral_ai", CustomInstruments)
MYSQL = ("mysql", CustomInstruments)
MYSQLCLIENT = ("mysqlclient", CustomInstruments)
OLLAMA = ("ollama", Instruments)
OPENAI = ("openai", CustomInstruments)
OPENAI_AGENTS = ("openai_agents", Instruments)
PIKA = ("pika", CustomInstruments)
PINECONE = ("pinecone", Instruments)
PSYCOPG = ("psycopg", CustomInstruments)
PSYCOPG2 = ("psycopg2", CustomInstruments)
PYDANTIC_AI = ("pydantic_ai", CustomInstruments)
PYMEMCACHE = ("pymemcache", CustomInstruments)
PYMONGO = ("pymongo", CustomInstruments)
PYMSSQL = ("pymssql", CustomInstruments)
PYMYSQL = ("pymysql", CustomInstruments)
PYRAMID = ("pyramid", CustomInstruments)
QDRANTDB = ("qdrant_db", CustomInstruments)
REDIS = ("redis", CustomInstruments)
REMOULADE = ("remoulade", CustomInstruments)
REPLICATE = ("replicate", Instruments)
REQUESTS = ("requests", CustomInstruments)
SAGEMAKER = ("sagemaker", Instruments)
SQLALCHEMY = ("sqlalchemy", CustomInstruments)
SQLITE3 = ("sqlite3", CustomInstruments)
STARLETTE = ("starlette", CustomInstruments)
SYSTEM_METRICS = ("system_metrics", CustomInstruments)
THREADING = ("threading", CustomInstruments)
TOGETHER = ("together", Instruments)
TORNADO = ("tornado", CustomInstruments)
TORTOISEORM = ("tortoiseorm", CustomInstruments)
TRANSFORMERS = ("transformers", Instruments)
URLLIB = ("urllib", CustomInstruments)
URLLIB3 = ("urllib3", CustomInstruments)
VERTEXAI = ("vertexai", Instruments)
WATSONX = ("watsonx", Instruments)
WEAVIATEDB = ("weaviate_db", CustomInstruments)
WRITER = ("writer", Instruments)
WSGI = ("wsgi", CustomInstruments)


merged_members = {}
NetraInstruments = InstrumentSet

for member in Instruments:
merged_members[member.name] = (member.value, Instruments)

for member in CustomInstruments:
merged_members[member.name] = (member.value, CustomInstruments)
# Curated default instrument set used for root_instruments when the user does
# not pass an explicit value. Covers core LLM/AI providers and frameworks.
DEFAULT_INSTRUMENTS_FOR_ROOT = {
InstrumentSet.ANTHROPIC,
InstrumentSet.CARTESIA,
InstrumentSet.COHEREAI,
InstrumentSet.CREWAI,
InstrumentSet.DEEPGRAM,
InstrumentSet.ELEVENLABS,
InstrumentSet.GOOGLE_GENERATIVEAI,
InstrumentSet.ADK,
InstrumentSet.GROQ,
InstrumentSet.LANGCHAIN,
InstrumentSet.LITELLM,
InstrumentSet.CEREBRAS,
InstrumentSet.MISTRALAI,
InstrumentSet.OPENAI,
InstrumentSet.OLLAMA,
InstrumentSet.VERTEXAI,
InstrumentSet.LLAMA_INDEX,
InstrumentSet.PYDANTIC_AI,
InstrumentSet.DSPY,
InstrumentSet.HAYSTACK,
InstrumentSet.BEDROCK,
InstrumentSet.TOGETHER,
InstrumentSet.REPLICATE,
InstrumentSet.ALEPHALPHA,
InstrumentSet.WATSONX,
}

InstrumentSet = NetraInstruments("InstrumentSet", merged_members)
# Broader default instrument set used for the ``instruments`` parameter when
# the user does not pass an explicit value. Includes the root defaults plus
# common vector DBs, HTTP client/server, and database ORM/client libraries.
DEFAULT_INSTRUMENTS = DEFAULT_INSTRUMENTS_FOR_ROOT.union(
{
InstrumentSet.PINECONE,
InstrumentSet.CHROMA,
InstrumentSet.WEAVIATEDB,
InstrumentSet.QDRANTDB,
InstrumentSet.MILVUS,
InstrumentSet.LANCEDB,
InstrumentSet.MARQO,
InstrumentSet.PYMYSQL,
InstrumentSet.REQUESTS,
InstrumentSet.SQLALCHEMY,
InstrumentSet.HTTPX,
}
)


#####################################################################################
Expand Down
26 changes: 26 additions & 0 deletions netra/instrumentation/litellm/wrappers.py
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,19 @@ def _ensure_choice(self, index: int) -> None:
else:
self._complete_response["choices"].append({"text": ""})

def __enter__(self) -> "StreamingWrapper":
if hasattr(self.__wrapped__, "__enter__"):
self.__wrapped__.__enter__()
return self

def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
if hasattr(self.__wrapped__, "__exit__"):
self.__wrapped__.__exit__(exc_type, exc_val, exc_tb)
if exc_type is not None:
self._span.set_status(Status(StatusCode.ERROR, str(exc_val)))
self._span.record_exception(exc_val)
self._span.end()

def __iter__(self) -> Iterator[Any]:
return self

Expand Down Expand Up @@ -451,6 +464,19 @@ def _ensure_choice(self, index: int) -> None:
else:
self._complete_response["choices"].append({"text": ""})

async def __aenter__(self) -> "AsyncStreamingWrapper":
if hasattr(self.__wrapped__, "__aenter__"):
await self.__wrapped__.__aenter__()
return self

async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
if hasattr(self.__wrapped__, "__aexit__"):
await self.__wrapped__.__aexit__(exc_type, exc_val, exc_tb)
if exc_type is not None:
self._span.set_status(Status(StatusCode.ERROR, str(exc_val)))
self._span.record_exception(exc_val)
self._span.end()

def __aiter__(self) -> AsyncIterator[Any]:
return self

Expand Down
26 changes: 26 additions & 0 deletions netra/instrumentation/openai/wrappers.py
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,19 @@ def _ensure_choice(self, index: int) -> None:
else:
self._complete_response["choices"].append({"text": ""})

def __enter__(self) -> "StreamingWrapper":
if hasattr(self.__wrapped__, "__enter__"):
self.__wrapped__.__enter__()
return self

def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
if hasattr(self.__wrapped__, "__exit__"):
self.__wrapped__.__exit__(exc_type, exc_val, exc_tb)
if exc_type is not None:
self._span.set_status(Status(StatusCode.ERROR, str(exc_val)))
self._span.record_exception(exc_val)
self._span.end()

def __iter__(self) -> Iterator[Any]:
return self

Expand Down Expand Up @@ -409,6 +422,19 @@ def _ensure_choice(self, index: int) -> None:
else:
self._complete_response["choices"].append({"text": ""})

async def __aenter__(self) -> "AsyncStreamingWrapper":
if hasattr(self.__wrapped__, "__aenter__"):
await self.__wrapped__.__aenter__()
return self

async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
if hasattr(self.__wrapped__, "__aexit__"):
await self.__wrapped__.__aexit__(exc_type, exc_val, exc_tb)
if exc_type is not None:
self._span.set_status(Status(StatusCode.ERROR, str(exc_val)))
self._span.record_exception(exc_val)
self._span.end()

def __aiter__(self) -> AsyncIterator[Any]:
return self

Expand Down
Loading