agentreplay 0.1.2__py3-none-any.whl → 0.4.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 +152 -10
- agentreplay/context.py +133 -0
- agentreplay/decorators.py +541 -0
- agentreplay/install_pth.py +6 -2
- agentreplay/privacy.py +452 -0
- agentreplay/sdk.py +578 -0
- agentreplay/wrappers.py +522 -0
- agentreplay-0.4.2.dist-info/METADATA +1083 -0
- {agentreplay-0.1.2.dist-info → agentreplay-0.4.2.dist-info}/RECORD +13 -9
- agentreplay-0.1.2.dist-info/METADATA +0 -285
- {agentreplay-0.1.2.dist-info → agentreplay-0.4.2.dist-info}/WHEEL +0 -0
- {agentreplay-0.1.2.dist-info → agentreplay-0.4.2.dist-info}/entry_points.txt +0 -0
- {agentreplay-0.1.2.dist-info → agentreplay-0.4.2.dist-info}/licenses/LICENSE +0 -0
- {agentreplay-0.1.2.dist-info → agentreplay-0.4.2.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,541 @@
|
|
|
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
|
+
"""
|
|
16
|
+
Decorator-based tracing for Agentreplay.
|
|
17
|
+
|
|
18
|
+
Provides @traceable and @observe decorators for easy function instrumentation.
|
|
19
|
+
|
|
20
|
+
Example:
|
|
21
|
+
>>> from agentreplay import init, traceable
|
|
22
|
+
>>>
|
|
23
|
+
>>> init()
|
|
24
|
+
>>>
|
|
25
|
+
>>> @traceable
|
|
26
|
+
>>> def my_function(query: str) -> str:
|
|
27
|
+
... return f"Result for {query}"
|
|
28
|
+
>>>
|
|
29
|
+
>>> result = my_function("hello") # Automatically traced!
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
import functools
|
|
33
|
+
import inspect
|
|
34
|
+
import time
|
|
35
|
+
import logging
|
|
36
|
+
from typing import (
|
|
37
|
+
Optional, Callable, TypeVar, Any, Dict, Union,
|
|
38
|
+
overload, ParamSpec, Awaitable
|
|
39
|
+
)
|
|
40
|
+
from contextvars import ContextVar
|
|
41
|
+
|
|
42
|
+
logger = logging.getLogger(__name__)
|
|
43
|
+
|
|
44
|
+
# Type variables for generic decorators
|
|
45
|
+
P = ParamSpec("P")
|
|
46
|
+
R = TypeVar("R")
|
|
47
|
+
|
|
48
|
+
# Context variable for current span
|
|
49
|
+
_current_span: ContextVar[Optional[Any]] = ContextVar("current_span", default=None)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# =============================================================================
|
|
53
|
+
# Span Kind
|
|
54
|
+
# =============================================================================
|
|
55
|
+
|
|
56
|
+
class SpanKind:
|
|
57
|
+
"""Span kind constants for categorizing operations."""
|
|
58
|
+
CHAIN = "chain"
|
|
59
|
+
LLM = "llm"
|
|
60
|
+
TOOL = "tool"
|
|
61
|
+
RETRIEVER = "retriever"
|
|
62
|
+
EMBEDDING = "embedding"
|
|
63
|
+
GUARDRAIL = "guardrail"
|
|
64
|
+
CACHE = "cache"
|
|
65
|
+
HTTP = "http"
|
|
66
|
+
DB = "db"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# =============================================================================
|
|
70
|
+
# Active Span
|
|
71
|
+
# =============================================================================
|
|
72
|
+
|
|
73
|
+
class ActiveSpan:
|
|
74
|
+
"""Active span with methods to add data.
|
|
75
|
+
|
|
76
|
+
This is yielded by the trace() context manager and passed to
|
|
77
|
+
decorated functions.
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
def __init__(
|
|
81
|
+
self,
|
|
82
|
+
name: str,
|
|
83
|
+
kind: str = SpanKind.CHAIN,
|
|
84
|
+
span_id: Optional[str] = None,
|
|
85
|
+
parent_id: Optional[str] = None,
|
|
86
|
+
trace_id: Optional[str] = None,
|
|
87
|
+
):
|
|
88
|
+
self.name = name
|
|
89
|
+
self.kind = kind
|
|
90
|
+
self.span_id = span_id or self._generate_id()
|
|
91
|
+
self.parent_id = parent_id
|
|
92
|
+
self.trace_id = trace_id or self._generate_id()
|
|
93
|
+
self.start_time = time.time()
|
|
94
|
+
self.end_time: Optional[float] = None
|
|
95
|
+
self.attributes: Dict[str, Any] = {}
|
|
96
|
+
self.events: list = []
|
|
97
|
+
self.input_data: Optional[Any] = None
|
|
98
|
+
self.output_data: Optional[Any] = None
|
|
99
|
+
self.error: Optional[Exception] = None
|
|
100
|
+
self.token_usage: Dict[str, int] = {}
|
|
101
|
+
self._ended = False
|
|
102
|
+
|
|
103
|
+
@staticmethod
|
|
104
|
+
def _generate_id() -> str:
|
|
105
|
+
"""Generate unique span ID."""
|
|
106
|
+
import uuid
|
|
107
|
+
return uuid.uuid4().hex[:16]
|
|
108
|
+
|
|
109
|
+
def set_input(self, data: Any) -> "ActiveSpan":
|
|
110
|
+
"""Set input data."""
|
|
111
|
+
self.input_data = data
|
|
112
|
+
return self
|
|
113
|
+
|
|
114
|
+
def set_output(self, data: Any) -> "ActiveSpan":
|
|
115
|
+
"""Set output data."""
|
|
116
|
+
self.output_data = data
|
|
117
|
+
return self
|
|
118
|
+
|
|
119
|
+
def set_attribute(self, key: str, value: Any) -> "ActiveSpan":
|
|
120
|
+
"""Set a span attribute."""
|
|
121
|
+
self.attributes[key] = value
|
|
122
|
+
return self
|
|
123
|
+
|
|
124
|
+
def set_attributes(self, attributes: Dict[str, Any]) -> "ActiveSpan":
|
|
125
|
+
"""Set multiple attributes."""
|
|
126
|
+
self.attributes.update(attributes)
|
|
127
|
+
return self
|
|
128
|
+
|
|
129
|
+
def add_event(self, name: str, attributes: Optional[Dict[str, Any]] = None) -> "ActiveSpan":
|
|
130
|
+
"""Add an event to the span."""
|
|
131
|
+
self.events.append({
|
|
132
|
+
"name": name,
|
|
133
|
+
"timestamp": time.time(),
|
|
134
|
+
"attributes": attributes or {},
|
|
135
|
+
})
|
|
136
|
+
return self
|
|
137
|
+
|
|
138
|
+
def set_error(self, error: Exception) -> "ActiveSpan":
|
|
139
|
+
"""Set error on span."""
|
|
140
|
+
self.error = error
|
|
141
|
+
self.attributes["error.type"] = type(error).__name__
|
|
142
|
+
self.attributes["error.message"] = str(error)
|
|
143
|
+
import traceback
|
|
144
|
+
self.attributes["error.stack"] = traceback.format_exc()
|
|
145
|
+
return self
|
|
146
|
+
|
|
147
|
+
def set_token_usage(
|
|
148
|
+
self,
|
|
149
|
+
prompt_tokens: Optional[int] = None,
|
|
150
|
+
completion_tokens: Optional[int] = None,
|
|
151
|
+
total_tokens: Optional[int] = None,
|
|
152
|
+
) -> "ActiveSpan":
|
|
153
|
+
"""Set token usage for LLM calls."""
|
|
154
|
+
if prompt_tokens is not None:
|
|
155
|
+
self.token_usage["prompt"] = prompt_tokens
|
|
156
|
+
self.attributes["gen_ai.usage.prompt_tokens"] = prompt_tokens
|
|
157
|
+
if completion_tokens is not None:
|
|
158
|
+
self.token_usage["completion"] = completion_tokens
|
|
159
|
+
self.attributes["gen_ai.usage.completion_tokens"] = completion_tokens
|
|
160
|
+
if total_tokens is not None:
|
|
161
|
+
self.token_usage["total"] = total_tokens
|
|
162
|
+
self.attributes["gen_ai.usage.total_tokens"] = total_tokens
|
|
163
|
+
return self
|
|
164
|
+
|
|
165
|
+
def set_model(self, model: str, provider: Optional[str] = None) -> "ActiveSpan":
|
|
166
|
+
"""Set model information."""
|
|
167
|
+
self.attributes["gen_ai.request.model"] = model
|
|
168
|
+
if provider:
|
|
169
|
+
self.attributes["gen_ai.system"] = provider
|
|
170
|
+
return self
|
|
171
|
+
|
|
172
|
+
def end(self) -> None:
|
|
173
|
+
"""End the span and send to backend."""
|
|
174
|
+
if self._ended:
|
|
175
|
+
return
|
|
176
|
+
|
|
177
|
+
self._ended = True
|
|
178
|
+
self.end_time = time.time()
|
|
179
|
+
|
|
180
|
+
# Send to backend
|
|
181
|
+
self._send()
|
|
182
|
+
|
|
183
|
+
def _send(self) -> None:
|
|
184
|
+
"""Send span to Agentreplay backend."""
|
|
185
|
+
try:
|
|
186
|
+
from agentreplay.sdk import get_batching_client, is_initialized, get_config
|
|
187
|
+
|
|
188
|
+
if not is_initialized():
|
|
189
|
+
return
|
|
190
|
+
|
|
191
|
+
config = get_config()
|
|
192
|
+
if not config.enabled:
|
|
193
|
+
return
|
|
194
|
+
|
|
195
|
+
# Build edge
|
|
196
|
+
from agentreplay.models import AgentFlowEdge, SpanType
|
|
197
|
+
|
|
198
|
+
# Map kind to SpanType
|
|
199
|
+
span_type_map = {
|
|
200
|
+
SpanKind.CHAIN: SpanType.ROOT,
|
|
201
|
+
SpanKind.LLM: SpanType.TOOL_CALL,
|
|
202
|
+
SpanKind.TOOL: SpanType.TOOL_CALL,
|
|
203
|
+
SpanKind.RETRIEVER: SpanType.TOOL_CALL,
|
|
204
|
+
SpanKind.EMBEDDING: SpanType.TOOL_CALL,
|
|
205
|
+
}
|
|
206
|
+
span_type = span_type_map.get(self.kind, SpanType.ROOT)
|
|
207
|
+
|
|
208
|
+
# Calculate duration
|
|
209
|
+
duration_us = int((self.end_time - self.start_time) * 1_000_000) if self.end_time else 0
|
|
210
|
+
|
|
211
|
+
# Build payload
|
|
212
|
+
payload = {}
|
|
213
|
+
if self.input_data is not None and config.capture_input:
|
|
214
|
+
payload["input"] = self._safe_serialize(self.input_data)
|
|
215
|
+
if self.output_data is not None and config.capture_output:
|
|
216
|
+
payload["output"] = self._safe_serialize(self.output_data)
|
|
217
|
+
if self.attributes:
|
|
218
|
+
payload["attributes"] = self.attributes
|
|
219
|
+
if self.events:
|
|
220
|
+
payload["events"] = self.events
|
|
221
|
+
if self.error:
|
|
222
|
+
payload["error"] = {
|
|
223
|
+
"type": type(self.error).__name__,
|
|
224
|
+
"message": str(self.error),
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
edge = AgentFlowEdge(
|
|
228
|
+
tenant_id=config.tenant_id,
|
|
229
|
+
project_id=config.project_id,
|
|
230
|
+
agent_id=config.agent_id,
|
|
231
|
+
session_id=int(self.trace_id[:8], 16) if self.trace_id else 0,
|
|
232
|
+
span_type=span_type,
|
|
233
|
+
timestamp_us=int(self.start_time * 1_000_000),
|
|
234
|
+
duration_us=duration_us,
|
|
235
|
+
token_count=self.token_usage.get("total", 0),
|
|
236
|
+
payload=payload,
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
# Send via batching client
|
|
240
|
+
client = get_batching_client()
|
|
241
|
+
client.insert(edge)
|
|
242
|
+
|
|
243
|
+
except Exception as e:
|
|
244
|
+
logger.debug(f"Failed to send span: {e}")
|
|
245
|
+
|
|
246
|
+
def _safe_serialize(self, data: Any, max_size: int = 10000) -> Any:
|
|
247
|
+
"""Safely serialize data with size limits."""
|
|
248
|
+
import json
|
|
249
|
+
|
|
250
|
+
try:
|
|
251
|
+
serialized = json.dumps(data, default=str)
|
|
252
|
+
if len(serialized) > max_size:
|
|
253
|
+
return {"__truncated": True, "__preview": serialized[:1000]}
|
|
254
|
+
return data
|
|
255
|
+
except Exception:
|
|
256
|
+
return str(data)[:max_size]
|
|
257
|
+
|
|
258
|
+
def __enter__(self) -> "ActiveSpan":
|
|
259
|
+
"""Context manager entry."""
|
|
260
|
+
# Set as current span
|
|
261
|
+
self._token = _current_span.set(self)
|
|
262
|
+
return self
|
|
263
|
+
|
|
264
|
+
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
265
|
+
"""Context manager exit."""
|
|
266
|
+
# Capture error if any
|
|
267
|
+
if exc_val is not None:
|
|
268
|
+
self.set_error(exc_val)
|
|
269
|
+
|
|
270
|
+
# End span
|
|
271
|
+
self.end()
|
|
272
|
+
|
|
273
|
+
# Reset current span
|
|
274
|
+
_current_span.reset(self._token)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
# =============================================================================
|
|
278
|
+
# Get Current Span
|
|
279
|
+
# =============================================================================
|
|
280
|
+
|
|
281
|
+
def get_current_span() -> Optional[ActiveSpan]:
|
|
282
|
+
"""Get the currently active span.
|
|
283
|
+
|
|
284
|
+
Returns:
|
|
285
|
+
ActiveSpan if inside a traced context, None otherwise
|
|
286
|
+
"""
|
|
287
|
+
return _current_span.get()
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
# =============================================================================
|
|
291
|
+
# Traceable Decorator
|
|
292
|
+
# =============================================================================
|
|
293
|
+
|
|
294
|
+
@overload
|
|
295
|
+
def traceable(func: Callable[P, R]) -> Callable[P, R]: ...
|
|
296
|
+
|
|
297
|
+
@overload
|
|
298
|
+
def traceable(
|
|
299
|
+
*,
|
|
300
|
+
name: Optional[str] = None,
|
|
301
|
+
kind: str = SpanKind.CHAIN,
|
|
302
|
+
capture_input: bool = True,
|
|
303
|
+
capture_output: bool = True,
|
|
304
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
305
|
+
) -> Callable[[Callable[P, R]], Callable[P, R]]: ...
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def traceable(
|
|
309
|
+
func: Optional[Callable[P, R]] = None,
|
|
310
|
+
*,
|
|
311
|
+
name: Optional[str] = None,
|
|
312
|
+
kind: str = SpanKind.CHAIN,
|
|
313
|
+
capture_input: bool = True,
|
|
314
|
+
capture_output: bool = True,
|
|
315
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
316
|
+
) -> Union[Callable[P, R], Callable[[Callable[P, R]], Callable[P, R]]]:
|
|
317
|
+
"""Decorator to trace a function.
|
|
318
|
+
|
|
319
|
+
Works with both sync and async functions. Automatically captures
|
|
320
|
+
inputs, outputs, errors, and timing.
|
|
321
|
+
|
|
322
|
+
Args:
|
|
323
|
+
func: Function to decorate (when used without parentheses)
|
|
324
|
+
name: Span name (default: function name)
|
|
325
|
+
kind: Span kind (chain, llm, tool, retriever, etc.)
|
|
326
|
+
capture_input: Whether to capture function inputs
|
|
327
|
+
capture_output: Whether to capture function output
|
|
328
|
+
metadata: Additional metadata to attach
|
|
329
|
+
|
|
330
|
+
Returns:
|
|
331
|
+
Decorated function
|
|
332
|
+
|
|
333
|
+
Example:
|
|
334
|
+
>>> @traceable
|
|
335
|
+
>>> def simple_function():
|
|
336
|
+
... return "hello"
|
|
337
|
+
|
|
338
|
+
>>> @traceable(name="my_operation", kind="tool")
|
|
339
|
+
>>> def tool_function(query: str):
|
|
340
|
+
... return search(query)
|
|
341
|
+
|
|
342
|
+
>>> @traceable(capture_input=False) # Don't capture sensitive inputs
|
|
343
|
+
>>> def sensitive_function(password: str):
|
|
344
|
+
... return authenticate(password)
|
|
345
|
+
"""
|
|
346
|
+
def decorator(fn: Callable[P, R]) -> Callable[P, R]:
|
|
347
|
+
span_name = name or fn.__name__
|
|
348
|
+
|
|
349
|
+
# Check if async
|
|
350
|
+
if inspect.iscoroutinefunction(fn):
|
|
351
|
+
@functools.wraps(fn)
|
|
352
|
+
async def async_wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
|
|
353
|
+
# Get parent span
|
|
354
|
+
parent = get_current_span()
|
|
355
|
+
|
|
356
|
+
# Create span
|
|
357
|
+
span = ActiveSpan(
|
|
358
|
+
name=span_name,
|
|
359
|
+
kind=kind,
|
|
360
|
+
parent_id=parent.span_id if parent else None,
|
|
361
|
+
trace_id=parent.trace_id if parent else None,
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
# Add metadata
|
|
365
|
+
if metadata:
|
|
366
|
+
span.set_attributes(metadata)
|
|
367
|
+
|
|
368
|
+
# Capture input
|
|
369
|
+
if capture_input:
|
|
370
|
+
try:
|
|
371
|
+
input_data = _capture_args(fn, args, kwargs)
|
|
372
|
+
span.set_input(input_data)
|
|
373
|
+
except Exception:
|
|
374
|
+
pass
|
|
375
|
+
|
|
376
|
+
# Execute with span context
|
|
377
|
+
with span:
|
|
378
|
+
try:
|
|
379
|
+
result = await fn(*args, **kwargs)
|
|
380
|
+
|
|
381
|
+
# Capture output
|
|
382
|
+
if capture_output:
|
|
383
|
+
span.set_output(result)
|
|
384
|
+
|
|
385
|
+
return result
|
|
386
|
+
except Exception as e:
|
|
387
|
+
span.set_error(e)
|
|
388
|
+
raise
|
|
389
|
+
|
|
390
|
+
return async_wrapper
|
|
391
|
+
else:
|
|
392
|
+
@functools.wraps(fn)
|
|
393
|
+
def sync_wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
|
|
394
|
+
# Get parent span
|
|
395
|
+
parent = get_current_span()
|
|
396
|
+
|
|
397
|
+
# Create span
|
|
398
|
+
span = ActiveSpan(
|
|
399
|
+
name=span_name,
|
|
400
|
+
kind=kind,
|
|
401
|
+
parent_id=parent.span_id if parent else None,
|
|
402
|
+
trace_id=parent.trace_id if parent else None,
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
# Add metadata
|
|
406
|
+
if metadata:
|
|
407
|
+
span.set_attributes(metadata)
|
|
408
|
+
|
|
409
|
+
# Capture input
|
|
410
|
+
if capture_input:
|
|
411
|
+
try:
|
|
412
|
+
input_data = _capture_args(fn, args, kwargs)
|
|
413
|
+
span.set_input(input_data)
|
|
414
|
+
except Exception:
|
|
415
|
+
pass
|
|
416
|
+
|
|
417
|
+
# Execute with span context
|
|
418
|
+
with span:
|
|
419
|
+
try:
|
|
420
|
+
result = fn(*args, **kwargs)
|
|
421
|
+
|
|
422
|
+
# Capture output
|
|
423
|
+
if capture_output:
|
|
424
|
+
span.set_output(result)
|
|
425
|
+
|
|
426
|
+
return result
|
|
427
|
+
except Exception as e:
|
|
428
|
+
span.set_error(e)
|
|
429
|
+
raise
|
|
430
|
+
|
|
431
|
+
return sync_wrapper
|
|
432
|
+
|
|
433
|
+
# Handle @traceable vs @traceable()
|
|
434
|
+
if func is not None:
|
|
435
|
+
return decorator(func)
|
|
436
|
+
return decorator
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
# Alias for Langfuse-style API
|
|
440
|
+
observe = traceable
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
def _capture_args(fn: Callable, args: tuple, kwargs: dict) -> Dict[str, Any]:
|
|
444
|
+
"""Capture function arguments as a dict."""
|
|
445
|
+
sig = inspect.signature(fn)
|
|
446
|
+
params = list(sig.parameters.keys())
|
|
447
|
+
|
|
448
|
+
result = {}
|
|
449
|
+
for i, arg in enumerate(args):
|
|
450
|
+
if i < len(params):
|
|
451
|
+
result[params[i]] = arg
|
|
452
|
+
else:
|
|
453
|
+
result[f"arg_{i}"] = arg
|
|
454
|
+
|
|
455
|
+
result.update(kwargs)
|
|
456
|
+
return result
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
# =============================================================================
|
|
460
|
+
# Trace Context Manager
|
|
461
|
+
# =============================================================================
|
|
462
|
+
|
|
463
|
+
def trace(
|
|
464
|
+
name: str,
|
|
465
|
+
*,
|
|
466
|
+
kind: str = SpanKind.CHAIN,
|
|
467
|
+
input: Optional[Any] = None,
|
|
468
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
469
|
+
) -> ActiveSpan:
|
|
470
|
+
"""Create a trace span as a context manager.
|
|
471
|
+
|
|
472
|
+
Args:
|
|
473
|
+
name: Span name
|
|
474
|
+
kind: Span kind (chain, llm, tool, retriever, etc.)
|
|
475
|
+
input: Input data to record
|
|
476
|
+
metadata: Additional metadata
|
|
477
|
+
|
|
478
|
+
Returns:
|
|
479
|
+
ActiveSpan context manager
|
|
480
|
+
|
|
481
|
+
Example:
|
|
482
|
+
>>> with trace("retrieve_documents", kind="retriever") as span:
|
|
483
|
+
... docs = vector_db.search(query)
|
|
484
|
+
... span.set_output({"count": len(docs)})
|
|
485
|
+
... return docs
|
|
486
|
+
"""
|
|
487
|
+
# Get parent span
|
|
488
|
+
parent = get_current_span()
|
|
489
|
+
|
|
490
|
+
# Create span
|
|
491
|
+
span = ActiveSpan(
|
|
492
|
+
name=name,
|
|
493
|
+
kind=kind,
|
|
494
|
+
parent_id=parent.span_id if parent else None,
|
|
495
|
+
trace_id=parent.trace_id if parent else None,
|
|
496
|
+
)
|
|
497
|
+
|
|
498
|
+
# Set input
|
|
499
|
+
if input is not None:
|
|
500
|
+
span.set_input(input)
|
|
501
|
+
|
|
502
|
+
# Set metadata
|
|
503
|
+
if metadata:
|
|
504
|
+
span.set_attributes(metadata)
|
|
505
|
+
|
|
506
|
+
return span
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
def start_span(
|
|
510
|
+
name: str,
|
|
511
|
+
*,
|
|
512
|
+
kind: str = SpanKind.CHAIN,
|
|
513
|
+
input: Optional[Any] = None,
|
|
514
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
515
|
+
) -> ActiveSpan:
|
|
516
|
+
"""Start a manual span (must call span.end()).
|
|
517
|
+
|
|
518
|
+
Use trace() context manager when possible. This is for cases
|
|
519
|
+
where you need manual control over span lifetime.
|
|
520
|
+
|
|
521
|
+
Args:
|
|
522
|
+
name: Span name
|
|
523
|
+
kind: Span kind
|
|
524
|
+
input: Input data
|
|
525
|
+
metadata: Additional metadata
|
|
526
|
+
|
|
527
|
+
Returns:
|
|
528
|
+
ActiveSpan (call .end() when done)
|
|
529
|
+
|
|
530
|
+
Example:
|
|
531
|
+
>>> span = start_span("long_operation", kind="tool")
|
|
532
|
+
>>> try:
|
|
533
|
+
... result = do_something()
|
|
534
|
+
... span.set_output(result)
|
|
535
|
+
>>> except Exception as e:
|
|
536
|
+
... span.set_error(e)
|
|
537
|
+
... raise
|
|
538
|
+
>>> finally:
|
|
539
|
+
... span.end()
|
|
540
|
+
"""
|
|
541
|
+
return trace(name, kind=kind, input=input, metadata=metadata)
|
agentreplay/install_pth.py
CHANGED
|
@@ -50,8 +50,12 @@ def get_site_packages():
|
|
|
50
50
|
import agentreplay
|
|
51
51
|
agentreplay_dir = os.path.dirname(agentreplay.__file__)
|
|
52
52
|
site_packages = os.path.dirname(agentreplay_dir)
|
|
53
|
-
|
|
54
|
-
|
|
53
|
+
|
|
54
|
+
# Prioritize the directory where agentreplay is actually installed
|
|
55
|
+
if site_packages in paths:
|
|
56
|
+
paths.remove(site_packages)
|
|
57
|
+
paths.insert(0, site_packages)
|
|
58
|
+
|
|
55
59
|
except ImportError:
|
|
56
60
|
pass
|
|
57
61
|
|