scale-gp-beta 0.1.0a16__py3-none-any.whl → 0.1.0a18__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,181 @@
1
+ import time
2
+ import queue
3
+ import atexit
4
+ import logging
5
+ import threading
6
+ from typing import TYPE_CHECKING, Optional
7
+
8
+ from scale_gp_beta import SGPClient, SGPClientError
9
+
10
+ from .util import configure, is_disabled
11
+ from .trace_exporter import TraceExporter
12
+
13
+ if TYPE_CHECKING:
14
+ from .span import Span
15
+ from .trace import Trace
16
+
17
+ # configurable by env vars?
18
+ DEFAULT_MAX_QUEUE_SIZE = 4_000
19
+ DEFAULT_TRIGGER_QUEUE_SIZE = 200
20
+ DEFAULT_TRIGGER_CADENCE = 4.0
21
+ DEFAULT_MAX_BATCH_SIZE = 50
22
+ DEFAULT_RETRIES = 4
23
+
24
+ WORKER_SLEEP_SECONDS = 0.1
25
+
26
+ log: logging.Logger = logging.getLogger(__name__)
27
+
28
+
29
+ class TraceQueueManager:
30
+ """Manage trace and spans queue
31
+ Store spans in-memory until the threshold has been reached then flush to server.
32
+
33
+ Optionally provide a client, if unprovided we will attempt to create a default client.
34
+ """
35
+
36
+ def __init__(
37
+ self,
38
+ client: Optional[SGPClient] = None,
39
+ max_queue_size: int = DEFAULT_MAX_QUEUE_SIZE,
40
+ max_batch_size: int = DEFAULT_MAX_BATCH_SIZE,
41
+ trigger_queue_size: int = DEFAULT_TRIGGER_QUEUE_SIZE,
42
+ trigger_cadence: float = DEFAULT_TRIGGER_CADENCE,
43
+ retries: int = DEFAULT_RETRIES,
44
+ ):
45
+ self._client = client
46
+ self._attempted_local_client_creation = False
47
+ self._trigger_queue_size = trigger_queue_size
48
+ self._trigger_cadence = trigger_cadence
49
+
50
+ self._reset_trigger_time()
51
+
52
+ self._exporter = TraceExporter(max_batch_size, retries)
53
+
54
+ self._shutdown_event = threading.Event()
55
+ self._queue: queue.Queue[Span] = queue.Queue(maxsize=max_queue_size)
56
+
57
+ if not is_disabled():
58
+ self._worker = threading.Thread(daemon=True, target=self._run)
59
+ self._worker.start()
60
+
61
+ # ensure the thread joins on exit
62
+ atexit.register(self.shutdown)
63
+
64
+ def register_client(self, client: SGPClient) -> None:
65
+ log.info("Registering client")
66
+ self._client = client
67
+
68
+ def shutdown(self, timeout: Optional[float] = None) -> None:
69
+ if is_disabled():
70
+ log.debug("No worker to shutdown")
71
+ return
72
+ log.info(f"Shutting down trace queue manager, joining worker thread with timeout {timeout}")
73
+ self._shutdown_event.set()
74
+ self._worker.join(timeout=timeout)
75
+ log.info("Shutdown complete")
76
+
77
+ def report_span_start(self, span: "Span") -> None:
78
+ # TODO: support making this optional. Current backend requires us to send span starts
79
+ try:
80
+ self._queue.put_nowait(span)
81
+ except queue.Full:
82
+ log.warning(f"Queue full, ignoring span {span.span_id}")
83
+
84
+ def report_span_end(self, span: "Span") -> None:
85
+ try:
86
+ self._queue.put_nowait(span)
87
+ except queue.Full:
88
+ log.warning(f"Queue full, ignoring span {span.span_id}")
89
+
90
+ def report_trace_start(self, trace: "Trace") -> None:
91
+ pass
92
+
93
+ def report_trace_end(self, trace: "Trace") -> None:
94
+ pass
95
+
96
+ def flush_queue(self) -> None:
97
+ self._export()
98
+
99
+ @property
100
+ def client(self) -> Optional[SGPClient]:
101
+ """
102
+ Use client provided client on init if available, otherwise attempt once to create a default one.
103
+ :return: SGPClient
104
+ """
105
+ if self._client is not None:
106
+ return self._client
107
+ if self._attempted_local_client_creation:
108
+ # Already tried and failed to create a client
109
+ return None
110
+
111
+ log.info("Tracing queue manager not initialized, attempting to create a default one")
112
+ try:
113
+ self.register_client(SGPClient())
114
+ except SGPClientError:
115
+ log.warning(
116
+ f"Failed to create SGPClient for tracing queue manager {self}, ignoring traces. Please initialize with a working client."
117
+ )
118
+ finally:
119
+ self._attempted_local_client_creation = True
120
+ return self._client
121
+
122
+ def _run(self) -> None:
123
+ # daemon worker loop
124
+ while not self._shutdown_event.is_set():
125
+ current_time = time.time()
126
+ queue_size = self._queue.qsize()
127
+
128
+ if queue_size >= self._trigger_queue_size or current_time >= self._next_trigger_time:
129
+ self._export()
130
+ self._reset_trigger_time()
131
+ continue
132
+
133
+ time.sleep(WORKER_SLEEP_SECONDS)
134
+
135
+ # flush all on shutdown
136
+ self._export()
137
+
138
+ def _export(self) -> None:
139
+ if self.client:
140
+ self._exporter.export(self.client, self._queue)
141
+
142
+ def _reset_trigger_time(self) -> None:
143
+ self._next_trigger_time = time.time() + self._trigger_cadence
144
+
145
+
146
+ _global_tracing_queue_manager: Optional[TraceQueueManager] = None
147
+ _queue_manager_lock = threading.Lock()
148
+
149
+
150
+ def init(client: Optional[SGPClient] = None, disabled: Optional[bool] = None) -> None:
151
+ """Initialize the tracing backend
152
+
153
+ Good practice to always include this method call with a valid client at your program entrypoint.
154
+
155
+ Tracing will attempt to generate a default client if unprovided.
156
+
157
+ :param client: SGPClient
158
+ :param disabled: Set to True to disable tracing. Overrides environment variable ``DISABLE_SCALE_TRACING``
159
+ """
160
+ if disabled is not None:
161
+ configure(disabled=disabled)
162
+
163
+ global _global_tracing_queue_manager
164
+ if _global_tracing_queue_manager is not None:
165
+ return
166
+
167
+ with _queue_manager_lock:
168
+ if _global_tracing_queue_manager is None:
169
+ _global_tracing_queue_manager = TraceQueueManager(client)
170
+
171
+
172
+ def tracing_queue_manager() -> TraceQueueManager:
173
+ global _global_tracing_queue_manager
174
+ if _global_tracing_queue_manager is None:
175
+ init(None)
176
+
177
+ if _global_tracing_queue_manager is None:
178
+ # should never happen... useful for linting
179
+ raise RuntimeError("Tracing queue manager failed to initialize.")
180
+
181
+ return _global_tracing_queue_manager
@@ -0,0 +1,180 @@
1
+ import logging
2
+ from typing import Any, Dict, Union, Optional
3
+
4
+ from .span import Span, BaseSpan, NoOpSpan
5
+ from .util import is_disabled
6
+ from .scope import Scope
7
+ from .trace import Trace, BaseTrace, NoOpTrace
8
+ from .trace_queue_manager import tracing_queue_manager
9
+
10
+ log: logging.Logger = logging.getLogger(__name__)
11
+
12
+
13
+ def current_span() -> Optional[BaseSpan]:
14
+ """Retrieves the currently active span from the execution context.
15
+
16
+ This function relies on `contextvars` to manage the active span in
17
+ a context-local manner, making it safe for concurrent execution
18
+ environments (e.g., threads, asyncio tasks).
19
+
20
+ Returns:
21
+ Optional[BaseSpan]: The current BaseSpan instance if one is active,
22
+ otherwise None. This could be a 'Span' or 'NoOpSpan'.
23
+ """
24
+ return Scope.get_current_span()
25
+
26
+
27
+ def current_trace() -> Optional[BaseTrace]:
28
+ """Retrieves the currently active trace from the execution context.
29
+
30
+ Similarly, to `current_span()`, this uses `contextvars` for context-local
31
+ trace management.
32
+
33
+ Returns:
34
+ Optional[BaseTrace]: The current BaseTrace instance if one is active,
35
+ otherwise None. This could be a 'Trace' or 'NoOpTrace'.
36
+ """
37
+ return Scope.get_current_trace()
38
+
39
+
40
+ def flush_queue() -> None:
41
+ """
42
+ Blocking flush of all requests in the queue.
43
+
44
+ Useful for distributed applications to ensure spans have been committed before continuing.
45
+ :return:
46
+ """
47
+ queue_manager = tracing_queue_manager()
48
+ queue_manager.flush_queue()
49
+
50
+
51
+ def create_trace(
52
+ name: str,
53
+ trace_id: Optional[str] = None,
54
+ root_span_id: Optional[str] = None,
55
+ metadata: Optional[Dict[str, Optional[str]]] = None,
56
+ ) -> BaseTrace:
57
+ """Creates a new trace and root span instance.
58
+
59
+ A trace represents a single, logical operation or workflow. It groups multiple
60
+ spans together. If tracing is disabled (via the 'DISABLE_SCALE_TRACING'
61
+ environment variable), a `NoOpTrace` instance is returned which performs no
62
+ actual tracing operations.
63
+
64
+ When a trace is started (e.g., by using it as a context manager or calling its
65
+ `start()` method), it becomes the `current_trace()` in the active scope.
66
+ Similarly, the root span instance becomes the `current_span()` in the active
67
+ scope.
68
+
69
+ Args:
70
+ name: The name of the trace.
71
+ trace_id (Optional[str]): An optional, user-defined ID for the trace.
72
+ If None, a unique trace ID will be generated.
73
+ root_span_id (Optional[str]): An optional, user-defined ID for the root span.
74
+ metadata (Optional[Dict[str, Optional[str]]]): An optional, user-defined metadata.
75
+
76
+ Returns:
77
+ BaseTrace: A `Trace` instance if tracing is enabled, or a `NoOpTrace`
78
+ instance if tracing is disabled.
79
+ """
80
+ if is_disabled():
81
+ log.debug(f"Tracing is disabled. Not creating a new trace.")
82
+ return NoOpTrace(name=name, trace_id=trace_id, root_span_id=root_span_id, metadata=metadata)
83
+
84
+ active_trace = current_trace()
85
+ if active_trace is not None:
86
+ log.warning(f"Trace with id {active_trace.trace_id} is already active. Creating a new trace anyways.")
87
+
88
+ trace = Trace(name=name, trace_id=trace_id, queue_manager=tracing_queue_manager(), metadata=metadata)
89
+ log.debug(f"Created new trace: {trace.trace_id}")
90
+
91
+ return trace
92
+
93
+
94
+ def create_span(
95
+ name: str,
96
+ span_id: Optional[str] = None,
97
+ input: Optional[Dict[str, Any]] = None,
98
+ output: Optional[Dict[str, Any]] = None,
99
+ metadata: Optional[Dict[str, Optional[str]]] = None,
100
+ trace: Optional[Union[BaseTrace, str]] = None,
101
+ parent_span: Optional[BaseSpan] = None,
102
+ ) -> BaseSpan:
103
+ """Creates a new span instance.
104
+
105
+ A span represents a single unit of work or operation within a trace. Spans
106
+ can be nested to form a hierarchy.
107
+
108
+ If tracing is disabled (via 'DISABLE_SCALE_TRACING' environment variable),
109
+ a `NoOpSpan` is returned. Additionally, if no explicit `parent` (Trace or Span)
110
+ is provided and there is no `current_trace()` active in the scope, a `NoOpSpan`
111
+ will also be returned to prevent orphaned spans.
112
+
113
+ When a span is started (e.g., via context manager or `start()`), it becomes
114
+ the `current_span()` in the active scope.
115
+
116
+ Args:
117
+ name (str): A descriptive name for the span (e.g., "database_query",
118
+ "http_request").
119
+ span_id (Optional[str]): An optional, user-defined ID for the span.
120
+ input (Optional[dict[str, Any]], optional): A dictionary containing
121
+ input data or parameters relevant to this span's operation. Defaults to None.
122
+ output (Optional[dict[str, Any]], optional): A dictionary containing
123
+ output data or results from this span's operation. Defaults to None.
124
+ metadata (Optional[dict[str, Union[str, int, float, bool, None]]], optional):
125
+ A dictionary for arbitrary key-value pairs providing additional
126
+ context or annotations for the span. Values should be simple types.
127
+ Defaults to None.
128
+ trace (Optional[Union[BaseTrace, str]], optional): A `Trace` instance
129
+ or a trace ID. Used for explicit control. Default to trace fetched
130
+ from the active scope.
131
+ parent_span (Optional[BaseSpan], optional): A `Span` instance. Like
132
+ trace, used for explicit control. Defaults to span fetched from the
133
+ active scope.
134
+
135
+ Returns:
136
+ BaseSpan: A `Span` instance if tracing is enabled and a valid trace context
137
+ exists, or a `NoOpSpan` otherwise.
138
+ """
139
+ queue_manager = tracing_queue_manager()
140
+ parent_span_id: Optional[str] = None
141
+
142
+ if parent_span is not None:
143
+ trace_id = parent_span.trace_id
144
+ parent_span_id = parent_span.span_id
145
+ elif trace is not None:
146
+ trace_id = trace if isinstance(trace, str) else trace.trace_id
147
+
148
+ parent_span = Scope.get_current_span()
149
+ parent_span_id = parent_span.span_id if parent_span else None
150
+ else:
151
+ trace = Scope.get_current_trace()
152
+ parent_span = Scope.get_current_span()
153
+
154
+ parent_span_id = parent_span.span_id if parent_span else None
155
+
156
+ if trace is None:
157
+ # need to think about default behavior here... do we create a trace, some other options?
158
+ # I am leaning towards setting it as an option as sometimes people might want to be succinct or when we
159
+ # build decorators we might want this functionality
160
+ log.debug(f"attempting to create a span with no trace")
161
+ return NoOpSpan(name=name, span_id=span_id, parent_span_id=parent_span_id)
162
+
163
+ trace_id = trace.trace_id
164
+
165
+ if is_disabled():
166
+ return NoOpSpan(name=name, span_id=span_id, parent_span_id=parent_span_id, trace_id=trace_id)
167
+
168
+ span = Span(
169
+ name=name,
170
+ span_id=span_id,
171
+ parent_span_id=parent_span_id,
172
+ trace_id=trace_id,
173
+ input=input or {},
174
+ output=output or {},
175
+ metadata=metadata or {},
176
+ queue_manager=queue_manager,
177
+ )
178
+ log.debug(f"Created new span: {span.span_id}")
179
+
180
+ return span
@@ -0,0 +1,40 @@
1
+ """
2
+ This is necessary, unfortunately. Stainless does not provide these as enums, only as type annotations.
3
+
4
+ For strict linting, we need to reference these enums.
5
+
6
+ NOTE: These will have to be manually updated to support updated span_types and status.
7
+ """
8
+
9
+ from typing_extensions import Literal
10
+
11
+ SpanStatusLiterals = Literal["SUCCESS", "ERROR"]
12
+
13
+ SpanTypeLiterals = Literal[
14
+ "TEXT_INPUT",
15
+ "TEXT_OUTPUT",
16
+ "COMPLETION_INPUT",
17
+ "COMPLETION",
18
+ "KB_RETRIEVAL",
19
+ "KB_INPUT",
20
+ "RERANKING",
21
+ "EXTERNAL_ENDPOINT",
22
+ "PROMPT_ENGINEERING",
23
+ "DOCUMENT_INPUT",
24
+ "MAP_REDUCE",
25
+ "DOCUMENT_SEARCH",
26
+ "DOCUMENT_PROMPT",
27
+ "CUSTOM",
28
+ "INPUT_GUARDRAIL",
29
+ "OUTPUT_GUARDRAIL",
30
+ "CODE_EXECUTION",
31
+ "DATA_MANIPULATION",
32
+ "EVALUATION",
33
+ "FILE_RETRIEVAL",
34
+ "KB_ADD_CHUNK",
35
+ "KB_MANAGEMENT",
36
+ "TRACER",
37
+ "AGENT_TRACER",
38
+ "AGENT_WORKFLOW",
39
+ "STANDALONE",
40
+ ]
@@ -0,0 +1,68 @@
1
+ import os
2
+ import uuid
3
+ from typing import Optional
4
+ from datetime import datetime, timezone
5
+ from functools import cache
6
+
7
+ _tracing_disabled: Optional[bool] = None
8
+
9
+
10
+ def generate_trace_id() -> str:
11
+ """
12
+ Generate a unique trace ID.
13
+
14
+ Returns:
15
+ str: A unique trace ID.
16
+ """
17
+ return str(uuid.uuid4())
18
+
19
+
20
+ def generate_span_id() -> str:
21
+ """
22
+ Generate a unique span ID.
23
+
24
+ Returns:
25
+ str: A unique span ID.
26
+ """
27
+ return str(uuid.uuid4())
28
+
29
+
30
+ def iso_timestamp() -> str:
31
+ """
32
+ Generate an ISO 8601 timestamp.
33
+
34
+ Returns:
35
+ str: The current time in ISO 8601 format.
36
+ """
37
+ return datetime.now(timezone.utc).isoformat()
38
+
39
+
40
+ def configure(disabled: bool) -> None:
41
+ """
42
+ Programmatically enable or disable tracing.
43
+
44
+ This setting takes precedence over the `DISABLE_SCALE_TRACING` environment
45
+ variable.
46
+
47
+ Args:
48
+ disabled (bool): Set to True to disable tracing, False to enable.
49
+ """
50
+ global _tracing_disabled
51
+ _tracing_disabled = disabled
52
+ is_disabled.cache_clear()
53
+
54
+
55
+ @cache
56
+ def is_disabled() -> bool:
57
+ """
58
+ Check if tracing is disabled, with programmatic control taking precedence.
59
+
60
+ Tracing is considered disabled if `configure(disabled=True)` has been called,
61
+ or if the `DISABLE_SCALE_TRACING` environment variable is set.
62
+
63
+ Returns:
64
+ bool: True if tracing is disabled, otherwise False.
65
+ """
66
+ if _tracing_disabled is not None:
67
+ return _tracing_disabled
68
+ return os.getenv("DISABLE_SCALE_TRACING") is not None