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/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