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/unified.py
ADDED
|
@@ -0,0 +1,465 @@
|
|
|
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
|
+
"""Unified observability interface for vendor-agnostic tracing.
|
|
16
|
+
|
|
17
|
+
This module provides a single API that works with multiple observability
|
|
18
|
+
backends (Agentreplay, LangSmith, Langfuse, Opik) based on environment variables.
|
|
19
|
+
|
|
20
|
+
This allows users to:
|
|
21
|
+
- Switch vendors without code changes
|
|
22
|
+
- A/B test different observability tools
|
|
23
|
+
- Send traces to multiple backends simultaneously
|
|
24
|
+
|
|
25
|
+
Example:
|
|
26
|
+
# Set environment variable to choose backend
|
|
27
|
+
export OBSERVABILITY_BACKEND="agentreplay" # or "langsmith", "langfuse", "opik"
|
|
28
|
+
|
|
29
|
+
# Python code - works with any backend!
|
|
30
|
+
from agentreplay.unified import UnifiedObservability
|
|
31
|
+
|
|
32
|
+
obs = UnifiedObservability()
|
|
33
|
+
|
|
34
|
+
with obs.trace("operation") as span:
|
|
35
|
+
span.set_attribute("key", "value")
|
|
36
|
+
result = do_work()
|
|
37
|
+
span.set_output(result)
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
import os
|
|
41
|
+
import logging
|
|
42
|
+
from typing import Optional, Dict, Any, ContextManager
|
|
43
|
+
from contextlib import contextmanager
|
|
44
|
+
from abc import ABC, abstractmethod
|
|
45
|
+
|
|
46
|
+
logger = logging.getLogger(__name__)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class ObservabilityBackend(ABC):
|
|
50
|
+
"""Abstract interface for observability backends."""
|
|
51
|
+
|
|
52
|
+
@abstractmethod
|
|
53
|
+
def trace(self, name: str, **kwargs) -> ContextManager:
|
|
54
|
+
"""Create a trace span.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
name: Span name
|
|
58
|
+
**kwargs: Backend-specific parameters
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
Context manager for span
|
|
62
|
+
"""
|
|
63
|
+
pass
|
|
64
|
+
|
|
65
|
+
@abstractmethod
|
|
66
|
+
def set_attribute(self, span: Any, key: str, value: Any) -> None:
|
|
67
|
+
"""Set attribute on span.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
span: Span object
|
|
71
|
+
key: Attribute key
|
|
72
|
+
value: Attribute value
|
|
73
|
+
"""
|
|
74
|
+
pass
|
|
75
|
+
|
|
76
|
+
@abstractmethod
|
|
77
|
+
def close(self) -> None:
|
|
78
|
+
"""Close backend and flush any buffered data."""
|
|
79
|
+
pass
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class AgentreplayBackend(ObservabilityBackend):
|
|
83
|
+
"""Agentreplay observability backend."""
|
|
84
|
+
|
|
85
|
+
def __init__(self, **config):
|
|
86
|
+
"""Initialize Agentreplay backend.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
**config: Configuration passed to AgentreplayClient
|
|
90
|
+
"""
|
|
91
|
+
from agentreplay import AgentreplayClient
|
|
92
|
+
|
|
93
|
+
url = config.get("url", os.getenv("AGENTREPLAY_URL", "http://localhost:8080"))
|
|
94
|
+
tenant_id = config.get("tenant_id", int(os.getenv("AGENTREPLAY_TENANT_ID", "1")))
|
|
95
|
+
project_id = config.get("project_id", int(os.getenv("AGENTREPLAY_PROJECT_ID", "0")))
|
|
96
|
+
|
|
97
|
+
self.client = AgentreplayClient(
|
|
98
|
+
url=url,
|
|
99
|
+
tenant_id=tenant_id,
|
|
100
|
+
project_id=project_id,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
logger.info("AgentreplayBackend initialized")
|
|
104
|
+
|
|
105
|
+
@contextmanager
|
|
106
|
+
def trace(self, name: str, **kwargs):
|
|
107
|
+
"""Create Agentreplay span."""
|
|
108
|
+
span = self.client.trace(**kwargs)
|
|
109
|
+
span_ctx = span.__enter__()
|
|
110
|
+
try:
|
|
111
|
+
yield span_ctx
|
|
112
|
+
finally:
|
|
113
|
+
span.__exit__(None, None, None)
|
|
114
|
+
|
|
115
|
+
def set_attribute(self, span: Any, key: str, value: Any) -> None:
|
|
116
|
+
"""Set attribute on Agentreplay span."""
|
|
117
|
+
# Agentreplay uses specific methods for common attributes
|
|
118
|
+
if key == "token_count" and hasattr(span, "set_token_count"):
|
|
119
|
+
span.set_token_count(int(value))
|
|
120
|
+
elif key == "confidence" and hasattr(span, "set_confidence"):
|
|
121
|
+
span.set_confidence(float(value))
|
|
122
|
+
# Other attributes stored in payload
|
|
123
|
+
|
|
124
|
+
def close(self) -> None:
|
|
125
|
+
"""Close Agentreplay client."""
|
|
126
|
+
self.client.close()
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class LangSmithBackend(ObservabilityBackend):
|
|
130
|
+
"""LangSmith observability backend."""
|
|
131
|
+
|
|
132
|
+
def __init__(self, **config):
|
|
133
|
+
"""Initialize LangSmith backend.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
**config: Configuration for LangSmith
|
|
137
|
+
"""
|
|
138
|
+
try:
|
|
139
|
+
from langsmith import Client
|
|
140
|
+
|
|
141
|
+
api_key = config.get("api_key", os.getenv("LANGCHAIN_API_KEY"))
|
|
142
|
+
if not api_key:
|
|
143
|
+
raise ValueError("LANGCHAIN_API_KEY environment variable required for LangSmith")
|
|
144
|
+
|
|
145
|
+
self.client = Client(api_key=api_key)
|
|
146
|
+
logger.info("LangSmithBackend initialized")
|
|
147
|
+
|
|
148
|
+
except ImportError:
|
|
149
|
+
raise ImportError(
|
|
150
|
+
"LangSmith not installed. Install with: pip install langsmith"
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
@contextmanager
|
|
154
|
+
def trace(self, name: str, **kwargs):
|
|
155
|
+
"""Create LangSmith span."""
|
|
156
|
+
from langsmith import traceable
|
|
157
|
+
|
|
158
|
+
# Use LangSmith's traceable decorator as context manager
|
|
159
|
+
with traceable(name=name, **kwargs) as span:
|
|
160
|
+
yield span
|
|
161
|
+
|
|
162
|
+
def set_attribute(self, span: Any, key: str, value: Any) -> None:
|
|
163
|
+
"""Set attribute on LangSmith span."""
|
|
164
|
+
if hasattr(span, "metadata"):
|
|
165
|
+
span.metadata[key] = value
|
|
166
|
+
|
|
167
|
+
def close(self) -> None:
|
|
168
|
+
"""Close LangSmith client."""
|
|
169
|
+
pass # LangSmith handles cleanup automatically
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class LangfuseBackend(ObservabilityBackend):
|
|
173
|
+
"""Langfuse observability backend."""
|
|
174
|
+
|
|
175
|
+
def __init__(self, **config):
|
|
176
|
+
"""Initialize Langfuse backend.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
**config: Configuration for Langfuse
|
|
180
|
+
"""
|
|
181
|
+
try:
|
|
182
|
+
from langfuse import Langfuse
|
|
183
|
+
|
|
184
|
+
public_key = config.get("public_key", os.getenv("LANGFUSE_PUBLIC_KEY"))
|
|
185
|
+
secret_key = config.get("secret_key", os.getenv("LANGFUSE_SECRET_KEY"))
|
|
186
|
+
host = config.get("host", os.getenv("LANGFUSE_HOST", "https://cloud.langfuse.com"))
|
|
187
|
+
|
|
188
|
+
if not public_key or not secret_key:
|
|
189
|
+
raise ValueError(
|
|
190
|
+
"LANGFUSE_PUBLIC_KEY and LANGFUSE_SECRET_KEY required for Langfuse"
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
self.client = Langfuse(
|
|
194
|
+
public_key=public_key,
|
|
195
|
+
secret_key=secret_key,
|
|
196
|
+
host=host,
|
|
197
|
+
)
|
|
198
|
+
logger.info("LangfuseBackend initialized")
|
|
199
|
+
|
|
200
|
+
except ImportError:
|
|
201
|
+
raise ImportError(
|
|
202
|
+
"Langfuse not installed. Install with: pip install langfuse"
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
@contextmanager
|
|
206
|
+
def trace(self, name: str, **kwargs):
|
|
207
|
+
"""Create Langfuse span."""
|
|
208
|
+
trace = self.client.trace(name=name, **kwargs)
|
|
209
|
+
try:
|
|
210
|
+
yield trace
|
|
211
|
+
finally:
|
|
212
|
+
self.client.flush()
|
|
213
|
+
|
|
214
|
+
def set_attribute(self, span: Any, key: str, value: Any) -> None:
|
|
215
|
+
"""Set attribute on Langfuse span."""
|
|
216
|
+
if hasattr(span, "update"):
|
|
217
|
+
span.update(metadata={key: value})
|
|
218
|
+
|
|
219
|
+
def close(self) -> None:
|
|
220
|
+
"""Close Langfuse client."""
|
|
221
|
+
self.client.flush()
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
class OpikBackend(ObservabilityBackend):
|
|
225
|
+
"""Opik observability backend."""
|
|
226
|
+
|
|
227
|
+
def __init__(self, **config):
|
|
228
|
+
"""Initialize Opik backend.
|
|
229
|
+
|
|
230
|
+
Args:
|
|
231
|
+
**config: Configuration for Opik
|
|
232
|
+
"""
|
|
233
|
+
try:
|
|
234
|
+
import opik
|
|
235
|
+
|
|
236
|
+
api_key = config.get("api_key", os.getenv("OPIK_API_KEY"))
|
|
237
|
+
workspace = config.get("workspace", os.getenv("OPIK_WORKSPACE"))
|
|
238
|
+
|
|
239
|
+
if api_key:
|
|
240
|
+
opik.configure(api_key=api_key, workspace=workspace)
|
|
241
|
+
|
|
242
|
+
self.client = opik.Opik()
|
|
243
|
+
logger.info("OpikBackend initialized")
|
|
244
|
+
|
|
245
|
+
except ImportError:
|
|
246
|
+
raise ImportError(
|
|
247
|
+
"Opik not installed. Install with: pip install opik"
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
@contextmanager
|
|
251
|
+
def trace(self, name: str, **kwargs):
|
|
252
|
+
"""Create Opik span."""
|
|
253
|
+
trace = self.client.trace(name=name, **kwargs)
|
|
254
|
+
try:
|
|
255
|
+
yield trace
|
|
256
|
+
finally:
|
|
257
|
+
trace.end()
|
|
258
|
+
|
|
259
|
+
def set_attribute(self, span: Any, key: str, value: Any) -> None:
|
|
260
|
+
"""Set attribute on Opik span."""
|
|
261
|
+
if hasattr(span, "log_metadata"):
|
|
262
|
+
span.log_metadata(key, value)
|
|
263
|
+
|
|
264
|
+
def close(self) -> None:
|
|
265
|
+
"""Close Opik client."""
|
|
266
|
+
self.client.flush()
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
class UnifiedObservability:
|
|
270
|
+
"""Unified interface for multiple observability backends.
|
|
271
|
+
|
|
272
|
+
Automatically selects backend based on environment variables:
|
|
273
|
+
- OBSERVABILITY_BACKEND: Backend name (agentreplay, langsmith, langfuse, opik)
|
|
274
|
+
- Or checks for API keys: LANGCHAIN_API_KEY, LANGFUSE_PUBLIC_KEY, OPIK_API_KEY
|
|
275
|
+
|
|
276
|
+
Example:
|
|
277
|
+
>>> from agentreplay.unified import UnifiedObservability
|
|
278
|
+
>>>
|
|
279
|
+
>>> # Auto-detects backend from environment
|
|
280
|
+
>>> obs = UnifiedObservability()
|
|
281
|
+
>>>
|
|
282
|
+
>>> with obs.trace("my_operation") as span:
|
|
283
|
+
... span.set_attribute("key", "value")
|
|
284
|
+
... result = do_work()
|
|
285
|
+
"""
|
|
286
|
+
|
|
287
|
+
def __init__(self, backend_name: Optional[str] = None, **config):
|
|
288
|
+
"""Initialize unified observability.
|
|
289
|
+
|
|
290
|
+
Args:
|
|
291
|
+
backend_name: Override backend selection (agentreplay, langsmith, langfuse, opik)
|
|
292
|
+
**config: Backend-specific configuration
|
|
293
|
+
"""
|
|
294
|
+
# Determine backend
|
|
295
|
+
if backend_name:
|
|
296
|
+
backend = backend_name.lower()
|
|
297
|
+
else:
|
|
298
|
+
backend = self._auto_detect_backend()
|
|
299
|
+
|
|
300
|
+
# Create backend
|
|
301
|
+
self.backend_name = backend
|
|
302
|
+
self.backend = self._create_backend(backend, **config)
|
|
303
|
+
|
|
304
|
+
logger.info(f"UnifiedObservability initialized with backend: {backend}")
|
|
305
|
+
|
|
306
|
+
def _auto_detect_backend(self) -> str:
|
|
307
|
+
"""Auto-detect backend from environment variables.
|
|
308
|
+
|
|
309
|
+
Returns:
|
|
310
|
+
Backend name
|
|
311
|
+
"""
|
|
312
|
+
# Check explicit setting
|
|
313
|
+
backend = os.getenv("OBSERVABILITY_BACKEND", "").lower()
|
|
314
|
+
if backend in ("agentreplay", "langsmith", "langfuse", "opik"):
|
|
315
|
+
return backend
|
|
316
|
+
|
|
317
|
+
# Auto-detect from API keys
|
|
318
|
+
if os.getenv("LANGCHAIN_API_KEY"):
|
|
319
|
+
return "langsmith"
|
|
320
|
+
elif os.getenv("LANGFUSE_PUBLIC_KEY"):
|
|
321
|
+
return "langfuse"
|
|
322
|
+
elif os.getenv("OPIK_API_KEY"):
|
|
323
|
+
return "opik"
|
|
324
|
+
else:
|
|
325
|
+
# Default to Agentreplay
|
|
326
|
+
return "agentreplay"
|
|
327
|
+
|
|
328
|
+
def _create_backend(self, backend_name: str, **config) -> ObservabilityBackend:
|
|
329
|
+
"""Create backend instance.
|
|
330
|
+
|
|
331
|
+
Args:
|
|
332
|
+
backend_name: Backend name
|
|
333
|
+
**config: Backend configuration
|
|
334
|
+
|
|
335
|
+
Returns:
|
|
336
|
+
ObservabilityBackend instance
|
|
337
|
+
"""
|
|
338
|
+
if backend_name == "agentreplay":
|
|
339
|
+
return AgentreplayBackend(**config)
|
|
340
|
+
elif backend_name == "langsmith":
|
|
341
|
+
return LangSmithBackend(**config)
|
|
342
|
+
elif backend_name == "langfuse":
|
|
343
|
+
return LangfuseBackend(**config)
|
|
344
|
+
elif backend_name == "opik":
|
|
345
|
+
return OpikBackend(**config)
|
|
346
|
+
else:
|
|
347
|
+
raise ValueError(
|
|
348
|
+
f"Unknown backend: {backend_name}. "
|
|
349
|
+
f"Supported: agentreplay, langsmith, langfuse, opik"
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
def trace(self, name: str, **kwargs) -> ContextManager:
|
|
353
|
+
"""Create a trace span.
|
|
354
|
+
|
|
355
|
+
Args:
|
|
356
|
+
name: Span name
|
|
357
|
+
**kwargs: Backend-specific parameters
|
|
358
|
+
|
|
359
|
+
Returns:
|
|
360
|
+
Context manager for span
|
|
361
|
+
"""
|
|
362
|
+
return self.backend.trace(name, **kwargs)
|
|
363
|
+
|
|
364
|
+
def set_attribute(self, span: Any, key: str, value: Any) -> None:
|
|
365
|
+
"""Set attribute on span.
|
|
366
|
+
|
|
367
|
+
Args:
|
|
368
|
+
span: Span object
|
|
369
|
+
key: Attribute key
|
|
370
|
+
value: Attribute value
|
|
371
|
+
"""
|
|
372
|
+
self.backend.set_attribute(span, key, value)
|
|
373
|
+
|
|
374
|
+
def close(self) -> None:
|
|
375
|
+
"""Close backend and flush buffered data."""
|
|
376
|
+
self.backend.close()
|
|
377
|
+
|
|
378
|
+
def __enter__(self):
|
|
379
|
+
"""Context manager entry."""
|
|
380
|
+
return self
|
|
381
|
+
|
|
382
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
383
|
+
"""Context manager exit."""
|
|
384
|
+
self.close()
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
class MultiBackend:
|
|
388
|
+
"""Send traces to multiple backends simultaneously.
|
|
389
|
+
|
|
390
|
+
Example:
|
|
391
|
+
>>> from agentreplay.unified import MultiBackend
|
|
392
|
+
>>>
|
|
393
|
+
>>> # Send to both Agentreplay and LangSmith
|
|
394
|
+
>>> obs = MultiBackend(["agentreplay", "langsmith"])
|
|
395
|
+
>>>
|
|
396
|
+
>>> with obs.trace("operation") as spans:
|
|
397
|
+
... for span in spans:
|
|
398
|
+
... span.set_attribute("key", "value")
|
|
399
|
+
"""
|
|
400
|
+
|
|
401
|
+
def __init__(self, backend_names: list, **config):
|
|
402
|
+
"""Initialize multi-backend.
|
|
403
|
+
|
|
404
|
+
Args:
|
|
405
|
+
backend_names: List of backend names
|
|
406
|
+
**config: Configuration for backends
|
|
407
|
+
"""
|
|
408
|
+
self.backends = []
|
|
409
|
+
|
|
410
|
+
for name in backend_names:
|
|
411
|
+
try:
|
|
412
|
+
obs = UnifiedObservability(backend_name=name, **config)
|
|
413
|
+
self.backends.append(obs)
|
|
414
|
+
logger.info(f"Added backend: {name}")
|
|
415
|
+
except Exception as e:
|
|
416
|
+
logger.warning(f"Failed to initialize {name}: {e}")
|
|
417
|
+
|
|
418
|
+
@contextmanager
|
|
419
|
+
def trace(self, name: str, **kwargs):
|
|
420
|
+
"""Create spans in all backends.
|
|
421
|
+
|
|
422
|
+
Args:
|
|
423
|
+
name: Span name
|
|
424
|
+
**kwargs: Parameters passed to all backends
|
|
425
|
+
|
|
426
|
+
Yields:
|
|
427
|
+
List of span objects (one per backend)
|
|
428
|
+
"""
|
|
429
|
+
contexts = []
|
|
430
|
+
spans = []
|
|
431
|
+
|
|
432
|
+
# Enter all contexts
|
|
433
|
+
for backend_obs in self.backends:
|
|
434
|
+
try:
|
|
435
|
+
ctx = backend_obs.trace(name, **kwargs)
|
|
436
|
+
span = ctx.__enter__()
|
|
437
|
+
contexts.append(ctx)
|
|
438
|
+
spans.append(span)
|
|
439
|
+
except Exception as e:
|
|
440
|
+
logger.error(f"Failed to create span in {backend_obs.backend_name}: {e}")
|
|
441
|
+
|
|
442
|
+
try:
|
|
443
|
+
yield spans
|
|
444
|
+
finally:
|
|
445
|
+
# Exit all contexts
|
|
446
|
+
for ctx in contexts:
|
|
447
|
+
try:
|
|
448
|
+
ctx.__exit__(None, None, None)
|
|
449
|
+
except Exception as e:
|
|
450
|
+
logger.error(f"Error exiting span context: {e}")
|
|
451
|
+
|
|
452
|
+
def close(self) -> None:
|
|
453
|
+
"""Close all backends."""
|
|
454
|
+
for backend_obs in self.backends:
|
|
455
|
+
try:
|
|
456
|
+
backend_obs.close()
|
|
457
|
+
except Exception as e:
|
|
458
|
+
logger.error(f"Error closing backend: {e}")
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
__all__ = [
|
|
462
|
+
"ObservabilityBackend",
|
|
463
|
+
"UnifiedObservability",
|
|
464
|
+
"MultiBackend",
|
|
465
|
+
]
|