agentreplay 0.1.2__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.
- agentreplay/__init__.py +81 -0
- agentreplay/auto_instrument/__init__.py +237 -0
- agentreplay/auto_instrument/openai.py +431 -0
- agentreplay/batching.py +270 -0
- agentreplay/bootstrap.py +202 -0
- agentreplay/circuit_breaker.py +300 -0
- agentreplay/client.py +1560 -0
- agentreplay/config.py +215 -0
- agentreplay/context.py +168 -0
- agentreplay/env_config.py +327 -0
- agentreplay/env_init.py +128 -0
- agentreplay/exceptions.py +92 -0
- agentreplay/genai.py +510 -0
- agentreplay/genai_conventions.py +502 -0
- agentreplay/install_pth.py +159 -0
- agentreplay/langchain_tracer.py +385 -0
- agentreplay/models.py +120 -0
- agentreplay/otel_bridge.py +281 -0
- agentreplay/patch.py +308 -0
- agentreplay/propagation.py +328 -0
- agentreplay/py.typed +3 -0
- agentreplay/retry.py +151 -0
- agentreplay/sampling.py +298 -0
- agentreplay/session.py +164 -0
- agentreplay/sitecustomize.py +73 -0
- agentreplay/span.py +270 -0
- agentreplay/unified.py +465 -0
- agentreplay-0.1.2.dist-info/METADATA +285 -0
- agentreplay-0.1.2.dist-info/RECORD +33 -0
- agentreplay-0.1.2.dist-info/WHEEL +5 -0
- agentreplay-0.1.2.dist-info/entry_points.txt +2 -0
- agentreplay-0.1.2.dist-info/licenses/LICENSE +190 -0
- agentreplay-0.1.2.dist-info/top_level.txt +1 -0
agentreplay/span.py
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
# Copyright 2025 Sushanth (https://github.com/sushanthpy)
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""Span context manager for convenient trace logging."""
|
|
16
|
+
|
|
17
|
+
from typing import Optional, TYPE_CHECKING
|
|
18
|
+
import time
|
|
19
|
+
import threading
|
|
20
|
+
from contextlib import contextmanager
|
|
21
|
+
from agentreplay.models import AgentFlowEdge, SpanType
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from agentreplay.client import AgentreplayClient
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Span:
|
|
28
|
+
"""Context manager for tracking agent execution spans.
|
|
29
|
+
|
|
30
|
+
Automatically creates parent-child relationships and logs
|
|
31
|
+
edges to Agentreplay on context exit.
|
|
32
|
+
|
|
33
|
+
Example:
|
|
34
|
+
>>> with client.trace() as root:
|
|
35
|
+
... root.set_token_count(100)
|
|
36
|
+
...
|
|
37
|
+
... with root.child(SpanType.PLANNING) as planning:
|
|
38
|
+
... planning.set_confidence(0.95)
|
|
39
|
+
...
|
|
40
|
+
... with planning.child(SpanType.TOOL_CALL) as tool:
|
|
41
|
+
... tool.set_duration_ms(150)
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def __init__(
|
|
45
|
+
self,
|
|
46
|
+
client: "AgentreplayClient",
|
|
47
|
+
span_type: SpanType,
|
|
48
|
+
tenant_id: int,
|
|
49
|
+
project_id: int,
|
|
50
|
+
agent_id: int,
|
|
51
|
+
session_id: int,
|
|
52
|
+
parent_id: int = 0,
|
|
53
|
+
):
|
|
54
|
+
"""Initialize span."""
|
|
55
|
+
self.client = client
|
|
56
|
+
self.span_type = span_type
|
|
57
|
+
self.tenant_id = tenant_id
|
|
58
|
+
self.project_id = project_id
|
|
59
|
+
self.agent_id = agent_id
|
|
60
|
+
self.session_id = session_id
|
|
61
|
+
self.parent_id = parent_id
|
|
62
|
+
self._lock = threading.RLock() # Thread-safe operations
|
|
63
|
+
|
|
64
|
+
# Edge fields
|
|
65
|
+
self.edge_id: Optional[int] = None
|
|
66
|
+
self.start_time_us = int(time.time() * 1_000_000)
|
|
67
|
+
|
|
68
|
+
# Validate timestamp (must be >= 2020-01-01 in microseconds)
|
|
69
|
+
MIN_VALID_TIMESTAMP = 1_577_836_800_000_000 # 2020-01-01 00:00:00 UTC
|
|
70
|
+
if self.start_time_us < MIN_VALID_TIMESTAMP:
|
|
71
|
+
import warnings
|
|
72
|
+
warnings.warn(
|
|
73
|
+
f"WARNING: Timestamp {self.start_time_us} is too old/small! "
|
|
74
|
+
f"Check microsecond conversion. Expected >= {MIN_VALID_TIMESTAMP}."
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
self.confidence = 1.0
|
|
78
|
+
self.token_count = 0
|
|
79
|
+
self.duration_us = 0
|
|
80
|
+
self.sampling_rate = 1.0
|
|
81
|
+
self.sensitivity_flags = 0
|
|
82
|
+
self.flags = 0
|
|
83
|
+
|
|
84
|
+
def __enter__(self) -> "Span":
|
|
85
|
+
"""Enter span context - create initial edge."""
|
|
86
|
+
with self._lock:
|
|
87
|
+
# Create initial edge on entry so children can reference parent_id
|
|
88
|
+
edge = AgentFlowEdge(
|
|
89
|
+
edge_id=0, # Will be assigned by client
|
|
90
|
+
causal_parent=self.parent_id,
|
|
91
|
+
timestamp_us=self.start_time_us,
|
|
92
|
+
logical_clock=0,
|
|
93
|
+
tenant_id=self.tenant_id,
|
|
94
|
+
project_id=self.project_id,
|
|
95
|
+
schema_version=2,
|
|
96
|
+
sensitivity_flags=self.sensitivity_flags,
|
|
97
|
+
agent_id=self.agent_id,
|
|
98
|
+
session_id=self.session_id,
|
|
99
|
+
span_type=self.span_type,
|
|
100
|
+
parent_count=1 if self.parent_id != 0 else 0,
|
|
101
|
+
confidence=self.confidence,
|
|
102
|
+
token_count=0, # Will be updated on exit
|
|
103
|
+
duration_us=0, # Will be calculated on exit
|
|
104
|
+
sampling_rate=self.sampling_rate,
|
|
105
|
+
compression_type=0,
|
|
106
|
+
has_payload=False,
|
|
107
|
+
flags=self.flags,
|
|
108
|
+
checksum=0,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
# Insert initial edge to get assigned ID
|
|
112
|
+
inserted = self.client.insert(edge)
|
|
113
|
+
self.edge_id = inserted.edge_id
|
|
114
|
+
|
|
115
|
+
return self
|
|
116
|
+
|
|
117
|
+
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
118
|
+
"""Exit span context - update edge with final metrics."""
|
|
119
|
+
with self._lock:
|
|
120
|
+
# Calculate duration
|
|
121
|
+
end_time_us = int(time.time() * 1_000_000)
|
|
122
|
+
self.duration_us = end_time_us - self.start_time_us
|
|
123
|
+
|
|
124
|
+
# Handle errors
|
|
125
|
+
if exc_type is not None:
|
|
126
|
+
self.span_type = SpanType.ERROR
|
|
127
|
+
self.confidence = 0.0
|
|
128
|
+
|
|
129
|
+
# Create completion edge with final metrics
|
|
130
|
+
edge = AgentFlowEdge(
|
|
131
|
+
edge_id=self.edge_id, # Use same ID for completion
|
|
132
|
+
causal_parent=self.parent_id,
|
|
133
|
+
timestamp_us=self.start_time_us,
|
|
134
|
+
logical_clock=0,
|
|
135
|
+
tenant_id=self.tenant_id,
|
|
136
|
+
project_id=self.project_id,
|
|
137
|
+
schema_version=2,
|
|
138
|
+
sensitivity_flags=self.sensitivity_flags,
|
|
139
|
+
agent_id=self.agent_id,
|
|
140
|
+
session_id=self.session_id,
|
|
141
|
+
span_type=self.span_type,
|
|
142
|
+
parent_count=1 if self.parent_id != 0 else 0,
|
|
143
|
+
confidence=self.confidence,
|
|
144
|
+
token_count=self.token_count, # Final token count
|
|
145
|
+
duration_us=self.duration_us, # Final duration
|
|
146
|
+
sampling_rate=self.sampling_rate,
|
|
147
|
+
compression_type=0,
|
|
148
|
+
has_payload=False,
|
|
149
|
+
flags=self.flags,
|
|
150
|
+
checksum=0,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
# Update edge with completion metrics
|
|
154
|
+
self.client.insert(edge)
|
|
155
|
+
|
|
156
|
+
def child(self, span_type: SpanType) -> "Span":
|
|
157
|
+
"""Create a child span.
|
|
158
|
+
|
|
159
|
+
Thread-safe child span creation.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
span_type: Type of child span
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
Child span context manager
|
|
166
|
+
|
|
167
|
+
Raises:
|
|
168
|
+
RuntimeError: If called before parent span has been entered
|
|
169
|
+
"""
|
|
170
|
+
with self._lock:
|
|
171
|
+
if self.edge_id is None:
|
|
172
|
+
raise RuntimeError("Parent span must be entered before creating children")
|
|
173
|
+
|
|
174
|
+
return Span(
|
|
175
|
+
client=self.client,
|
|
176
|
+
span_type=span_type,
|
|
177
|
+
tenant_id=self.tenant_id,
|
|
178
|
+
project_id=self.project_id,
|
|
179
|
+
agent_id=self.agent_id,
|
|
180
|
+
session_id=self.session_id,
|
|
181
|
+
parent_id=self.edge_id,
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
# Fluent API for setting properties
|
|
185
|
+
|
|
186
|
+
def set_token_count(self, count: int) -> "Span":
|
|
187
|
+
"""Set token count (thread-safe)."""
|
|
188
|
+
with self._lock:
|
|
189
|
+
self.token_count = count
|
|
190
|
+
return self
|
|
191
|
+
|
|
192
|
+
def set_confidence(self, confidence: float) -> "Span":
|
|
193
|
+
"""Set confidence score (0.0 to 1.0, thread-safe)."""
|
|
194
|
+
if not 0.0 <= confidence <= 1.0:
|
|
195
|
+
raise ValueError("Confidence must be between 0.0 and 1.0")
|
|
196
|
+
with self._lock:
|
|
197
|
+
self.confidence = confidence
|
|
198
|
+
return self
|
|
199
|
+
|
|
200
|
+
def set_duration_ms(self, duration_ms: int) -> "Span":
|
|
201
|
+
"""Set duration in milliseconds (thread-safe).
|
|
202
|
+
|
|
203
|
+
IMPORTANT: This method accepts milliseconds and converts to microseconds.
|
|
204
|
+
If you accidentally pass microseconds, the duration will be 1000x too large!
|
|
205
|
+
|
|
206
|
+
Examples:
|
|
207
|
+
span.set_duration_ms(150) # 150 milliseconds = 150,000 microseconds ✓
|
|
208
|
+
span.set_duration_ms(150_000) # ERROR! This is 150 seconds, not milliseconds! ✗
|
|
209
|
+
|
|
210
|
+
Use set_duration_us() if you have microseconds already.
|
|
211
|
+
"""
|
|
212
|
+
with self._lock:
|
|
213
|
+
# DESKTOP APP FIX: Validate input to catch common mistakes
|
|
214
|
+
if duration_ms > 600_000: # > 10 minutes in ms
|
|
215
|
+
import warnings
|
|
216
|
+
warnings.warn(
|
|
217
|
+
f"WARNING: duration_ms={duration_ms} seems suspiciously large (>{duration_ms/1000}s). "
|
|
218
|
+
f"Did you mean to use set_duration_us({duration_ms}) instead? "
|
|
219
|
+
f"If this duration is correct, ignore this warning.",
|
|
220
|
+
UserWarning,
|
|
221
|
+
stacklevel=2
|
|
222
|
+
)
|
|
223
|
+
self.duration_us = duration_ms * 1_000
|
|
224
|
+
return self
|
|
225
|
+
|
|
226
|
+
def set_duration_us(self, duration_us: int) -> "Span":
|
|
227
|
+
"""Set duration in microseconds (thread-safe).
|
|
228
|
+
|
|
229
|
+
Use this when you already have duration in microseconds.
|
|
230
|
+
For milliseconds, use set_duration_ms() instead.
|
|
231
|
+
|
|
232
|
+
Examples:
|
|
233
|
+
span.set_duration_us(150_000) # 150 milliseconds = 150,000 microseconds ✓
|
|
234
|
+
span.set_duration_us(150) # 150 microseconds = 0.15 milliseconds ✓
|
|
235
|
+
"""
|
|
236
|
+
with self._lock:
|
|
237
|
+
self.duration_us = duration_us
|
|
238
|
+
return self
|
|
239
|
+
|
|
240
|
+
def set_sampling_rate(self, rate: float) -> "Span":
|
|
241
|
+
"""Set sampling rate (0.0 to 1.0, thread-safe)."""
|
|
242
|
+
if not 0.0 <= rate <= 1.0:
|
|
243
|
+
raise ValueError("Sampling rate must be between 0.0 and 1.0")
|
|
244
|
+
with self._lock:
|
|
245
|
+
self.sampling_rate = rate
|
|
246
|
+
return self
|
|
247
|
+
|
|
248
|
+
def mark_pii(self) -> "Span":
|
|
249
|
+
"""Mark this span as containing PII (thread-safe)."""
|
|
250
|
+
from agentreplay.models import SensitivityFlags
|
|
251
|
+
|
|
252
|
+
with self._lock:
|
|
253
|
+
self.sensitivity_flags |= SensitivityFlags.PII
|
|
254
|
+
return self
|
|
255
|
+
|
|
256
|
+
def mark_secret(self) -> "Span":
|
|
257
|
+
"""Mark this span as containing secrets (thread-safe)."""
|
|
258
|
+
from agentreplay.models import SensitivityFlags
|
|
259
|
+
|
|
260
|
+
with self._lock:
|
|
261
|
+
self.sensitivity_flags |= SensitivityFlags.SECRET
|
|
262
|
+
return self
|
|
263
|
+
|
|
264
|
+
def mark_no_embed(self) -> "Span":
|
|
265
|
+
"""Mark this span to never be embedded in vector index (thread-safe)."""
|
|
266
|
+
from agentreplay.models import SensitivityFlags
|
|
267
|
+
|
|
268
|
+
with self._lock:
|
|
269
|
+
self.sensitivity_flags |= SensitivityFlags.NO_EMBED
|
|
270
|
+
return self
|