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
|
@@ -0,0 +1,502 @@
|
|
|
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
|
+
"""GenAI Semantic Conventions validator and normalizer.
|
|
16
|
+
|
|
17
|
+
This module enforces OpenTelemetry GenAI semantic conventions and normalizes
|
|
18
|
+
framework-specific attributes to standard conventions.
|
|
19
|
+
|
|
20
|
+
Reference: https://opentelemetry.io/docs/specs/semconv/gen-ai/
|
|
21
|
+
|
|
22
|
+
Example:
|
|
23
|
+
>>> from agentreplay.genai_conventions import normalize_attributes, validate_genai_span
|
|
24
|
+
>>>
|
|
25
|
+
>>> # Normalize LangChain attributes to OTEL GenAI conventions
|
|
26
|
+
>>> langchain_attrs = {
|
|
27
|
+
... "langchain.model": "gpt-4o",
|
|
28
|
+
... "langchain.token_usage": 150
|
|
29
|
+
... }
|
|
30
|
+
>>> normalized = normalize_attributes(langchain_attrs, framework="langchain")
|
|
31
|
+
>>> # Result: {"gen_ai.request.model": "gpt-4o", "gen_ai.usage.total_tokens": 150}
|
|
32
|
+
>>>
|
|
33
|
+
>>> # Validate span has required GenAI attributes
|
|
34
|
+
>>> warnings = validate_genai_span(normalized)
|
|
35
|
+
>>> for warning in warnings:
|
|
36
|
+
... print(warning)
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
import logging
|
|
40
|
+
from typing import Dict, List, Optional, Any
|
|
41
|
+
from dataclasses import dataclass
|
|
42
|
+
|
|
43
|
+
logger = logging.getLogger(__name__)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class GenAIConventions:
|
|
48
|
+
"""OpenTelemetry GenAI semantic conventions constants.
|
|
49
|
+
|
|
50
|
+
Reference: https://opentelemetry.io/docs/specs/semconv/gen-ai/
|
|
51
|
+
Updated for OTEL GenAI semantic conventions v1.36+
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
# =========================================================================
|
|
55
|
+
# PROVIDER IDENTIFICATION (REQUIRED)
|
|
56
|
+
# =========================================================================
|
|
57
|
+
SYSTEM = "gen_ai.system" # Legacy, use PROVIDER_NAME
|
|
58
|
+
PROVIDER_NAME = "gen_ai.provider.name" # "openai", "anthropic", "aws.bedrock", etc.
|
|
59
|
+
OPERATION_NAME = "gen_ai.operation.name" # "chat", "embeddings", "text_completion"
|
|
60
|
+
|
|
61
|
+
# Well-known provider names
|
|
62
|
+
PROVIDER_OPENAI = "openai"
|
|
63
|
+
PROVIDER_ANTHROPIC = "anthropic"
|
|
64
|
+
PROVIDER_AWS_BEDROCK = "aws.bedrock"
|
|
65
|
+
PROVIDER_AZURE_OPENAI = "azure.ai.openai"
|
|
66
|
+
PROVIDER_GCP_GEMINI = "gcp.gemini"
|
|
67
|
+
PROVIDER_GCP_VERTEX_AI = "gcp.vertex_ai"
|
|
68
|
+
PROVIDER_COHERE = "cohere"
|
|
69
|
+
PROVIDER_DEEPSEEK = "deepseek"
|
|
70
|
+
PROVIDER_GROQ = "groq"
|
|
71
|
+
PROVIDER_MISTRAL_AI = "mistral_ai"
|
|
72
|
+
PROVIDER_PERPLEXITY = "perplexity"
|
|
73
|
+
PROVIDER_X_AI = "x_ai"
|
|
74
|
+
PROVIDER_IBM_WATSONX = "ibm.watsonx.ai"
|
|
75
|
+
|
|
76
|
+
# =========================================================================
|
|
77
|
+
# MODEL INFORMATION (REQUIRED)
|
|
78
|
+
# =========================================================================
|
|
79
|
+
REQUEST_MODEL = "gen_ai.request.model"
|
|
80
|
+
RESPONSE_MODEL = "gen_ai.response.model"
|
|
81
|
+
RESPONSE_ID = "gen_ai.response.id"
|
|
82
|
+
|
|
83
|
+
# =========================================================================
|
|
84
|
+
# TOKEN USAGE (CRITICAL for cost calculation)
|
|
85
|
+
# =========================================================================
|
|
86
|
+
INPUT_TOKENS = "gen_ai.usage.input_tokens"
|
|
87
|
+
OUTPUT_TOKENS = "gen_ai.usage.output_tokens"
|
|
88
|
+
TOTAL_TOKENS = "gen_ai.usage.total_tokens"
|
|
89
|
+
REASONING_TOKENS = "gen_ai.usage.reasoning_tokens" # OpenAI o1 models
|
|
90
|
+
CACHE_READ_TOKENS = "gen_ai.usage.cache_read_tokens" # Anthropic cache
|
|
91
|
+
CACHE_CREATION_TOKENS = "gen_ai.usage.cache_creation_tokens" # Anthropic cache
|
|
92
|
+
|
|
93
|
+
# =========================================================================
|
|
94
|
+
# FINISH REASONS
|
|
95
|
+
# =========================================================================
|
|
96
|
+
FINISH_REASONS = "gen_ai.response.finish_reasons"
|
|
97
|
+
|
|
98
|
+
# =========================================================================
|
|
99
|
+
# REQUEST PARAMETERS / HYPERPARAMETERS (RECOMMENDED)
|
|
100
|
+
# =========================================================================
|
|
101
|
+
TEMPERATURE = "gen_ai.request.temperature"
|
|
102
|
+
TOP_P = "gen_ai.request.top_p"
|
|
103
|
+
TOP_K = "gen_ai.request.top_k" # Anthropic/Google
|
|
104
|
+
MAX_TOKENS = "gen_ai.request.max_tokens"
|
|
105
|
+
FREQUENCY_PENALTY = "gen_ai.request.frequency_penalty"
|
|
106
|
+
PRESENCE_PENALTY = "gen_ai.request.presence_penalty"
|
|
107
|
+
STOP_SEQUENCES = "gen_ai.request.stop_sequences"
|
|
108
|
+
SEED = "gen_ai.request.seed" # Reproducibility
|
|
109
|
+
CHOICE_COUNT = "gen_ai.request.choice.count" # n parameter
|
|
110
|
+
|
|
111
|
+
# =========================================================================
|
|
112
|
+
# SERVER INFORMATION (REQUIRED for distributed tracing)
|
|
113
|
+
# =========================================================================
|
|
114
|
+
SERVER_ADDRESS = "server.address"
|
|
115
|
+
SERVER_PORT = "server.port"
|
|
116
|
+
|
|
117
|
+
# =========================================================================
|
|
118
|
+
# ERROR TRACKING (REQUIRED when errors occur)
|
|
119
|
+
# =========================================================================
|
|
120
|
+
ERROR_TYPE = "error.type"
|
|
121
|
+
|
|
122
|
+
# =========================================================================
|
|
123
|
+
# AGENT ATTRIBUTES (for agentic systems)
|
|
124
|
+
# =========================================================================
|
|
125
|
+
AGENT_ID = "gen_ai.agent.id"
|
|
126
|
+
AGENT_NAME = "gen_ai.agent.name"
|
|
127
|
+
AGENT_DESCRIPTION = "gen_ai.agent.description"
|
|
128
|
+
CONVERSATION_ID = "gen_ai.conversation.id"
|
|
129
|
+
|
|
130
|
+
# =========================================================================
|
|
131
|
+
# TOOL CALL ATTRIBUTES
|
|
132
|
+
# =========================================================================
|
|
133
|
+
TOOL_NAME = "gen_ai.tool.name"
|
|
134
|
+
TOOL_TYPE = "gen_ai.tool.type" # "function", "extension", "datastore"
|
|
135
|
+
TOOL_DESCRIPTION = "gen_ai.tool.description"
|
|
136
|
+
TOOL_CALL_ID = "gen_ai.tool.call.id"
|
|
137
|
+
TOOL_CALL_ARGUMENTS = "gen_ai.tool.call.arguments"
|
|
138
|
+
TOOL_CALL_RESULT = "gen_ai.tool.call.result"
|
|
139
|
+
TOOL_DEFINITIONS = "gen_ai.tool.definitions" # Array of tool schemas
|
|
140
|
+
|
|
141
|
+
# =========================================================================
|
|
142
|
+
# CONTENT ATTRIBUTES
|
|
143
|
+
# =========================================================================
|
|
144
|
+
SYSTEM_INSTRUCTIONS = "gen_ai.system_instructions"
|
|
145
|
+
INPUT_MESSAGES = "gen_ai.input.messages"
|
|
146
|
+
OUTPUT_MESSAGES = "gen_ai.output.messages"
|
|
147
|
+
|
|
148
|
+
# =========================================================================
|
|
149
|
+
# STRUCTURED PROMPTS/RESPONSES (indexed format)
|
|
150
|
+
# =========================================================================
|
|
151
|
+
PROMPT_PREFIX = "gen_ai.prompt"
|
|
152
|
+
COMPLETION_PREFIX = "gen_ai.completion"
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
# Framework-specific attribute mappings
|
|
156
|
+
FRAMEWORK_MAPPINGS = {
|
|
157
|
+
"langchain": {
|
|
158
|
+
"langchain.model": GenAIConventions.REQUEST_MODEL,
|
|
159
|
+
"langchain.model_name": GenAIConventions.REQUEST_MODEL,
|
|
160
|
+
"langchain.llm.model_name": GenAIConventions.REQUEST_MODEL,
|
|
161
|
+
"langchain.token_usage": GenAIConventions.TOTAL_TOKENS,
|
|
162
|
+
"langchain.tokens": GenAIConventions.TOTAL_TOKENS,
|
|
163
|
+
"langchain.prompt_tokens": GenAIConventions.INPUT_TOKENS,
|
|
164
|
+
"langchain.completion_tokens": GenAIConventions.OUTPUT_TOKENS,
|
|
165
|
+
"langchain.temperature": GenAIConventions.TEMPERATURE,
|
|
166
|
+
"langchain.max_tokens": GenAIConventions.MAX_TOKENS,
|
|
167
|
+
},
|
|
168
|
+
"llamaindex": {
|
|
169
|
+
"llama_index.model": GenAIConventions.REQUEST_MODEL,
|
|
170
|
+
"llama_index.model_name": GenAIConventions.REQUEST_MODEL,
|
|
171
|
+
"llama_index.token_count": GenAIConventions.TOTAL_TOKENS,
|
|
172
|
+
"llama_index.prompt_tokens": GenAIConventions.INPUT_TOKENS,
|
|
173
|
+
"llama_index.completion_tokens": GenAIConventions.OUTPUT_TOKENS,
|
|
174
|
+
"llama_index.temperature": GenAIConventions.TEMPERATURE,
|
|
175
|
+
},
|
|
176
|
+
"autogen": {
|
|
177
|
+
"autogen.model": GenAIConventions.REQUEST_MODEL,
|
|
178
|
+
"autogen.token_usage": GenAIConventions.TOTAL_TOKENS,
|
|
179
|
+
},
|
|
180
|
+
"crewai": {
|
|
181
|
+
"crewai.model": GenAIConventions.REQUEST_MODEL,
|
|
182
|
+
"crewai.llm_model": GenAIConventions.REQUEST_MODEL,
|
|
183
|
+
},
|
|
184
|
+
"openai": {
|
|
185
|
+
"openai.model": GenAIConventions.REQUEST_MODEL,
|
|
186
|
+
"openai.response.model": GenAIConventions.RESPONSE_MODEL,
|
|
187
|
+
"openai.response.id": GenAIConventions.RESPONSE_ID,
|
|
188
|
+
"openai.usage.prompt_tokens": GenAIConventions.INPUT_TOKENS,
|
|
189
|
+
"openai.usage.completion_tokens": GenAIConventions.OUTPUT_TOKENS,
|
|
190
|
+
"openai.usage.total_tokens": GenAIConventions.TOTAL_TOKENS,
|
|
191
|
+
"openai.usage.completion_tokens_details.reasoning_tokens": GenAIConventions.REASONING_TOKENS,
|
|
192
|
+
},
|
|
193
|
+
"anthropic": {
|
|
194
|
+
"anthropic.model": GenAIConventions.REQUEST_MODEL,
|
|
195
|
+
"anthropic.response.model": GenAIConventions.RESPONSE_MODEL,
|
|
196
|
+
"anthropic.response.id": GenAIConventions.RESPONSE_ID,
|
|
197
|
+
"anthropic.usage.input_tokens": GenAIConventions.INPUT_TOKENS,
|
|
198
|
+
"anthropic.usage.output_tokens": GenAIConventions.OUTPUT_TOKENS,
|
|
199
|
+
"anthropic.usage.cache_read_input_tokens": GenAIConventions.CACHE_READ_TOKENS,
|
|
200
|
+
},
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def normalize_attributes(
|
|
205
|
+
attributes: Dict[str, Any],
|
|
206
|
+
framework: Optional[str] = None,
|
|
207
|
+
) -> Dict[str, Any]:
|
|
208
|
+
"""Normalize framework-specific attributes to GenAI conventions.
|
|
209
|
+
|
|
210
|
+
Takes attributes from various AI frameworks and maps them to standard
|
|
211
|
+
OpenTelemetry GenAI semantic conventions.
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
attributes: Original attributes dict
|
|
215
|
+
framework: Framework name (langchain, llamaindex, etc.)
|
|
216
|
+
If None, attempts auto-detection
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
Normalized attributes dict with GenAI conventions
|
|
220
|
+
|
|
221
|
+
Example:
|
|
222
|
+
>>> attrs = {"langchain.model": "gpt-4o", "langchain.tokens": 150}
|
|
223
|
+
>>> normalized = normalize_attributes(attrs, framework="langchain")
|
|
224
|
+
>>> print(normalized["gen_ai.request.model"])
|
|
225
|
+
'gpt-4o'
|
|
226
|
+
"""
|
|
227
|
+
# Auto-detect framework if not specified
|
|
228
|
+
if framework is None:
|
|
229
|
+
framework = _detect_framework(attributes)
|
|
230
|
+
|
|
231
|
+
# Start with original attributes
|
|
232
|
+
normalized = dict(attributes)
|
|
233
|
+
|
|
234
|
+
# Apply framework-specific mappings
|
|
235
|
+
if framework and framework in FRAMEWORK_MAPPINGS:
|
|
236
|
+
mapping = FRAMEWORK_MAPPINGS[framework]
|
|
237
|
+
|
|
238
|
+
for old_key, new_key in mapping.items():
|
|
239
|
+
if old_key in attributes:
|
|
240
|
+
value = attributes[old_key]
|
|
241
|
+
normalized[new_key] = value
|
|
242
|
+
logger.debug(f"Mapped {old_key} -> {new_key}: {value}")
|
|
243
|
+
|
|
244
|
+
# Ensure system is set
|
|
245
|
+
if GenAIConventions.SYSTEM not in normalized:
|
|
246
|
+
# Try to infer from model name
|
|
247
|
+
if GenAIConventions.REQUEST_MODEL in normalized:
|
|
248
|
+
model = str(normalized[GenAIConventions.REQUEST_MODEL]).lower()
|
|
249
|
+
if "gpt" in model or "davinci" in model:
|
|
250
|
+
normalized[GenAIConventions.SYSTEM] = "openai"
|
|
251
|
+
elif "claude" in model:
|
|
252
|
+
normalized[GenAIConventions.SYSTEM] = "anthropic"
|
|
253
|
+
elif "gemini" in model or "palm" in model:
|
|
254
|
+
normalized[GenAIConventions.SYSTEM] = "google"
|
|
255
|
+
elif "llama" in model:
|
|
256
|
+
normalized[GenAIConventions.SYSTEM] = "meta"
|
|
257
|
+
|
|
258
|
+
# Calculate total_tokens if not present
|
|
259
|
+
if GenAIConventions.TOTAL_TOKENS not in normalized:
|
|
260
|
+
input_tokens = normalized.get(GenAIConventions.INPUT_TOKENS)
|
|
261
|
+
output_tokens = normalized.get(GenAIConventions.OUTPUT_TOKENS)
|
|
262
|
+
|
|
263
|
+
if input_tokens is not None and output_tokens is not None:
|
|
264
|
+
try:
|
|
265
|
+
total = int(input_tokens) + int(output_tokens)
|
|
266
|
+
normalized[GenAIConventions.TOTAL_TOKENS] = total
|
|
267
|
+
logger.debug(f"Calculated total_tokens: {total}")
|
|
268
|
+
except (ValueError, TypeError):
|
|
269
|
+
pass
|
|
270
|
+
|
|
271
|
+
# Ensure all numeric values are properly typed
|
|
272
|
+
_normalize_numeric_types(normalized)
|
|
273
|
+
|
|
274
|
+
return normalized
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def validate_genai_span(attributes: Dict[str, Any]) -> List[str]:
|
|
278
|
+
"""Validate that span has required GenAI attributes.
|
|
279
|
+
|
|
280
|
+
Checks for required fields according to OpenTelemetry GenAI semantic conventions
|
|
281
|
+
and returns a list of warnings for missing or invalid attributes.
|
|
282
|
+
|
|
283
|
+
Args:
|
|
284
|
+
attributes: Span attributes dict
|
|
285
|
+
|
|
286
|
+
Returns:
|
|
287
|
+
List of warning messages (empty if valid)
|
|
288
|
+
|
|
289
|
+
Example:
|
|
290
|
+
>>> attrs = {"gen_ai.system": "openai"}
|
|
291
|
+
>>> warnings = validate_genai_span(attrs)
|
|
292
|
+
>>> for warning in warnings:
|
|
293
|
+
... print(f"WARNING: {warning}")
|
|
294
|
+
"""
|
|
295
|
+
warnings = []
|
|
296
|
+
|
|
297
|
+
# Check required fields
|
|
298
|
+
if GenAIConventions.SYSTEM not in attributes:
|
|
299
|
+
warnings.append("Missing required field: gen_ai.system (e.g., 'openai', 'anthropic')")
|
|
300
|
+
|
|
301
|
+
if GenAIConventions.REQUEST_MODEL not in attributes:
|
|
302
|
+
warnings.append("Missing required field: gen_ai.request.model (e.g., 'gpt-4o')")
|
|
303
|
+
|
|
304
|
+
# Check token usage (required for cost calculation)
|
|
305
|
+
has_input = GenAIConventions.INPUT_TOKENS in attributes
|
|
306
|
+
has_output = GenAIConventions.OUTPUT_TOKENS in attributes
|
|
307
|
+
has_total = GenAIConventions.TOTAL_TOKENS in attributes
|
|
308
|
+
|
|
309
|
+
if not (has_input and has_output) and not has_total:
|
|
310
|
+
warnings.append(
|
|
311
|
+
"Missing token usage: should have gen_ai.usage.input_tokens and "
|
|
312
|
+
"gen_ai.usage.output_tokens (or gen_ai.usage.total_tokens)"
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
# Validate token counts are consistent
|
|
316
|
+
if has_input and has_output and has_total:
|
|
317
|
+
try:
|
|
318
|
+
input_val = int(attributes[GenAIConventions.INPUT_TOKENS])
|
|
319
|
+
output_val = int(attributes[GenAIConventions.OUTPUT_TOKENS])
|
|
320
|
+
total_val = int(attributes[GenAIConventions.TOTAL_TOKENS])
|
|
321
|
+
|
|
322
|
+
expected_total = input_val + output_val
|
|
323
|
+
if total_val != expected_total:
|
|
324
|
+
warnings.append(
|
|
325
|
+
f"Token count mismatch: total_tokens={total_val} but "
|
|
326
|
+
f"input_tokens + output_tokens = {expected_total}"
|
|
327
|
+
)
|
|
328
|
+
except (ValueError, TypeError):
|
|
329
|
+
warnings.append("Token counts must be numeric values")
|
|
330
|
+
|
|
331
|
+
# Validate model name format
|
|
332
|
+
if GenAIConventions.REQUEST_MODEL in attributes:
|
|
333
|
+
model = str(attributes[GenAIConventions.REQUEST_MODEL])
|
|
334
|
+
if not model or model.lower() == "unknown":
|
|
335
|
+
warnings.append("Model name should not be empty or 'unknown'")
|
|
336
|
+
|
|
337
|
+
# Check recommended fields
|
|
338
|
+
if GenAIConventions.OPERATION_NAME not in attributes:
|
|
339
|
+
warnings.append(
|
|
340
|
+
"Recommended field missing: gen_ai.operation.name "
|
|
341
|
+
"(e.g., 'chat', 'completion', 'embedding')"
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
return warnings
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def get_missing_attributes(attributes: Dict[str, Any]) -> List[str]:
|
|
348
|
+
"""Get list of recommended GenAI attributes that are missing.
|
|
349
|
+
|
|
350
|
+
Args:
|
|
351
|
+
attributes: Span attributes dict
|
|
352
|
+
|
|
353
|
+
Returns:
|
|
354
|
+
List of missing attribute names
|
|
355
|
+
"""
|
|
356
|
+
recommended = [
|
|
357
|
+
GenAIConventions.SYSTEM,
|
|
358
|
+
GenAIConventions.REQUEST_MODEL,
|
|
359
|
+
GenAIConventions.OPERATION_NAME,
|
|
360
|
+
GenAIConventions.INPUT_TOKENS,
|
|
361
|
+
GenAIConventions.OUTPUT_TOKENS,
|
|
362
|
+
]
|
|
363
|
+
|
|
364
|
+
return [attr for attr in recommended if attr not in attributes]
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def _detect_framework(attributes: Dict[str, Any]) -> Optional[str]:
|
|
368
|
+
"""Auto-detect framework from attribute keys.
|
|
369
|
+
|
|
370
|
+
Args:
|
|
371
|
+
attributes: Attributes dict
|
|
372
|
+
|
|
373
|
+
Returns:
|
|
374
|
+
Framework name or None
|
|
375
|
+
"""
|
|
376
|
+
keys = set(attributes.keys())
|
|
377
|
+
|
|
378
|
+
# Check for framework-specific prefixes
|
|
379
|
+
if any(k.startswith("langchain.") for k in keys):
|
|
380
|
+
return "langchain"
|
|
381
|
+
elif any(k.startswith("llama_index.") for k in keys):
|
|
382
|
+
return "llamaindex"
|
|
383
|
+
elif any(k.startswith("autogen.") for k in keys):
|
|
384
|
+
return "autogen"
|
|
385
|
+
elif any(k.startswith("crewai.") for k in keys):
|
|
386
|
+
return "crewai"
|
|
387
|
+
elif any(k.startswith("openai.") for k in keys):
|
|
388
|
+
return "openai"
|
|
389
|
+
elif any(k.startswith("anthropic.") for k in keys):
|
|
390
|
+
return "anthropic"
|
|
391
|
+
|
|
392
|
+
return None
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def _normalize_numeric_types(attributes: Dict[str, Any]) -> None:
|
|
396
|
+
"""Ensure numeric attributes have correct types (modifies in-place).
|
|
397
|
+
|
|
398
|
+
Args:
|
|
399
|
+
attributes: Attributes dict to normalize
|
|
400
|
+
"""
|
|
401
|
+
# Token counts should be integers
|
|
402
|
+
token_fields = [
|
|
403
|
+
GenAIConventions.INPUT_TOKENS,
|
|
404
|
+
GenAIConventions.OUTPUT_TOKENS,
|
|
405
|
+
GenAIConventions.TOTAL_TOKENS,
|
|
406
|
+
GenAIConventions.REASONING_TOKENS,
|
|
407
|
+
GenAIConventions.CACHE_READ_TOKENS,
|
|
408
|
+
GenAIConventions.MAX_TOKENS,
|
|
409
|
+
]
|
|
410
|
+
|
|
411
|
+
for field in token_fields:
|
|
412
|
+
if field in attributes:
|
|
413
|
+
try:
|
|
414
|
+
attributes[field] = int(attributes[field])
|
|
415
|
+
except (ValueError, TypeError):
|
|
416
|
+
logger.warning(f"Could not convert {field} to int: {attributes[field]}")
|
|
417
|
+
|
|
418
|
+
# Hyperparameters should be floats
|
|
419
|
+
float_fields = [
|
|
420
|
+
GenAIConventions.TEMPERATURE,
|
|
421
|
+
GenAIConventions.TOP_P,
|
|
422
|
+
GenAIConventions.FREQUENCY_PENALTY,
|
|
423
|
+
GenAIConventions.PRESENCE_PENALTY,
|
|
424
|
+
]
|
|
425
|
+
|
|
426
|
+
for field in float_fields:
|
|
427
|
+
if field in attributes:
|
|
428
|
+
try:
|
|
429
|
+
attributes[field] = float(attributes[field])
|
|
430
|
+
except (ValueError, TypeError):
|
|
431
|
+
logger.warning(f"Could not convert {field} to float: {attributes[field]}")
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def create_genai_attributes_dict(
|
|
435
|
+
system: str,
|
|
436
|
+
model: str,
|
|
437
|
+
input_tokens: Optional[int] = None,
|
|
438
|
+
output_tokens: Optional[int] = None,
|
|
439
|
+
total_tokens: Optional[int] = None,
|
|
440
|
+
operation_name: Optional[str] = "chat",
|
|
441
|
+
**kwargs
|
|
442
|
+
) -> Dict[str, Any]:
|
|
443
|
+
"""Create a GenAI-compliant attributes dictionary.
|
|
444
|
+
|
|
445
|
+
Helper function to create attributes following semantic conventions.
|
|
446
|
+
|
|
447
|
+
Args:
|
|
448
|
+
system: Provider name (openai, anthropic, google, etc.)
|
|
449
|
+
model: Model name (gpt-4o, claude-3-5-sonnet, etc.)
|
|
450
|
+
input_tokens: Number of input tokens
|
|
451
|
+
output_tokens: Number of output tokens
|
|
452
|
+
total_tokens: Total tokens (calculated if not provided)
|
|
453
|
+
operation_name: Operation type (chat, completion, embedding)
|
|
454
|
+
**kwargs: Additional GenAI attributes
|
|
455
|
+
|
|
456
|
+
Returns:
|
|
457
|
+
Dict with GenAI semantic conventions
|
|
458
|
+
|
|
459
|
+
Example:
|
|
460
|
+
>>> attrs = create_genai_attributes_dict(
|
|
461
|
+
... system="openai",
|
|
462
|
+
... model="gpt-4o",
|
|
463
|
+
... input_tokens=100,
|
|
464
|
+
... output_tokens=50,
|
|
465
|
+
... temperature=0.7
|
|
466
|
+
... )
|
|
467
|
+
"""
|
|
468
|
+
attributes = {
|
|
469
|
+
GenAIConventions.SYSTEM: system,
|
|
470
|
+
GenAIConventions.REQUEST_MODEL: model,
|
|
471
|
+
GenAIConventions.OPERATION_NAME: operation_name,
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
if input_tokens is not None:
|
|
475
|
+
attributes[GenAIConventions.INPUT_TOKENS] = input_tokens
|
|
476
|
+
|
|
477
|
+
if output_tokens is not None:
|
|
478
|
+
attributes[GenAIConventions.OUTPUT_TOKENS] = output_tokens
|
|
479
|
+
|
|
480
|
+
if total_tokens is not None:
|
|
481
|
+
attributes[GenAIConventions.TOTAL_TOKENS] = total_tokens
|
|
482
|
+
elif input_tokens is not None and output_tokens is not None:
|
|
483
|
+
attributes[GenAIConventions.TOTAL_TOKENS] = input_tokens + output_tokens
|
|
484
|
+
|
|
485
|
+
# Add any additional attributes
|
|
486
|
+
for key, value in kwargs.items():
|
|
487
|
+
if key.startswith("gen_ai."):
|
|
488
|
+
attributes[key] = value
|
|
489
|
+
else:
|
|
490
|
+
# Prefix with gen_ai. if not already prefixed
|
|
491
|
+
attributes[f"gen_ai.{key}"] = value
|
|
492
|
+
|
|
493
|
+
return attributes
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
__all__ = [
|
|
497
|
+
"GenAIConventions",
|
|
498
|
+
"normalize_attributes",
|
|
499
|
+
"validate_genai_span",
|
|
500
|
+
"get_missing_attributes",
|
|
501
|
+
"create_genai_attributes_dict",
|
|
502
|
+
]
|
|
@@ -0,0 +1,159 @@
|
|
|
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
|
+
"""Install the agentreplay-init.pth file to site-packages for auto-initialization.
|
|
16
|
+
|
|
17
|
+
This script copies the .pth file that enables zero-code auto-instrumentation.
|
|
18
|
+
Run after pip install: python -m agentreplay.install_pth
|
|
19
|
+
|
|
20
|
+
Or use the CLI command: agentreplay-install
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import os
|
|
24
|
+
import site
|
|
25
|
+
import sys
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
PTH_CONTENT = """import agentreplay.bootstrap; agentreplay.bootstrap._auto_init()
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
PTH_FILENAME = "agentreplay-init.pth"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def get_site_packages():
|
|
35
|
+
"""Get the user's site-packages directory."""
|
|
36
|
+
# Try user site first, then system site
|
|
37
|
+
paths = []
|
|
38
|
+
|
|
39
|
+
# User site-packages
|
|
40
|
+
user_site = site.getusersitepackages()
|
|
41
|
+
if user_site:
|
|
42
|
+
paths.append(user_site)
|
|
43
|
+
|
|
44
|
+
# System site-packages
|
|
45
|
+
for path in site.getsitepackages():
|
|
46
|
+
paths.append(path)
|
|
47
|
+
|
|
48
|
+
# Also check where agentreplay itself is installed
|
|
49
|
+
try:
|
|
50
|
+
import agentreplay
|
|
51
|
+
agentreplay_dir = os.path.dirname(agentreplay.__file__)
|
|
52
|
+
site_packages = os.path.dirname(agentreplay_dir)
|
|
53
|
+
if site_packages not in paths:
|
|
54
|
+
paths.insert(0, site_packages)
|
|
55
|
+
except ImportError:
|
|
56
|
+
pass
|
|
57
|
+
|
|
58
|
+
return paths
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def install():
|
|
62
|
+
"""Install the .pth file to site-packages."""
|
|
63
|
+
paths = get_site_packages()
|
|
64
|
+
|
|
65
|
+
installed = False
|
|
66
|
+
for site_packages in paths:
|
|
67
|
+
if not os.path.isdir(site_packages):
|
|
68
|
+
continue
|
|
69
|
+
|
|
70
|
+
pth_path = os.path.join(site_packages, PTH_FILENAME)
|
|
71
|
+
|
|
72
|
+
try:
|
|
73
|
+
with open(pth_path, 'w') as f:
|
|
74
|
+
f.write(PTH_CONTENT)
|
|
75
|
+
print(f"✅ Installed {pth_path}")
|
|
76
|
+
installed = True
|
|
77
|
+
break
|
|
78
|
+
except PermissionError:
|
|
79
|
+
print(f"⚠️ Permission denied: {pth_path}")
|
|
80
|
+
continue
|
|
81
|
+
except Exception as e:
|
|
82
|
+
print(f"⚠️ Failed to write {pth_path}: {e}")
|
|
83
|
+
continue
|
|
84
|
+
|
|
85
|
+
if not installed:
|
|
86
|
+
print("❌ Could not install .pth file to any site-packages directory.")
|
|
87
|
+
print(" Try running with sudo or in a virtual environment.")
|
|
88
|
+
return False
|
|
89
|
+
|
|
90
|
+
print("\n🎉 Agentreplay auto-instrumentation is now enabled!")
|
|
91
|
+
print(" Set AGENTREPLAY_ENABLED=true to activate tracing.")
|
|
92
|
+
print(" Set AGENTREPLAY_PROJECT_ID=<id> to specify your project.")
|
|
93
|
+
return True
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def uninstall():
|
|
97
|
+
"""Remove the .pth file from site-packages."""
|
|
98
|
+
paths = get_site_packages()
|
|
99
|
+
|
|
100
|
+
removed = False
|
|
101
|
+
for site_packages in paths:
|
|
102
|
+
pth_path = os.path.join(site_packages, PTH_FILENAME)
|
|
103
|
+
if os.path.exists(pth_path):
|
|
104
|
+
try:
|
|
105
|
+
os.remove(pth_path)
|
|
106
|
+
print(f"✅ Removed {pth_path}")
|
|
107
|
+
removed = True
|
|
108
|
+
except Exception as e:
|
|
109
|
+
print(f"⚠️ Failed to remove {pth_path}: {e}")
|
|
110
|
+
|
|
111
|
+
if not removed:
|
|
112
|
+
print("ℹ️ No .pth file found to remove.")
|
|
113
|
+
|
|
114
|
+
return removed
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def main():
|
|
118
|
+
"""CLI entry point."""
|
|
119
|
+
import argparse
|
|
120
|
+
|
|
121
|
+
parser = argparse.ArgumentParser(
|
|
122
|
+
description="Install Agentreplay auto-instrumentation .pth file"
|
|
123
|
+
)
|
|
124
|
+
parser.add_argument(
|
|
125
|
+
"--uninstall", "-u",
|
|
126
|
+
action="store_true",
|
|
127
|
+
help="Uninstall the .pth file"
|
|
128
|
+
)
|
|
129
|
+
parser.add_argument(
|
|
130
|
+
"--check", "-c",
|
|
131
|
+
action="store_true",
|
|
132
|
+
help="Check if .pth file is installed"
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
args = parser.parse_args()
|
|
136
|
+
|
|
137
|
+
if args.check:
|
|
138
|
+
paths = get_site_packages()
|
|
139
|
+
found = False
|
|
140
|
+
for site_packages in paths:
|
|
141
|
+
pth_path = os.path.join(site_packages, PTH_FILENAME)
|
|
142
|
+
if os.path.exists(pth_path):
|
|
143
|
+
print(f"✅ Found: {pth_path}")
|
|
144
|
+
found = True
|
|
145
|
+
if not found:
|
|
146
|
+
print("❌ .pth file not installed")
|
|
147
|
+
print(" Run: agentreplay-install")
|
|
148
|
+
return 0 if found else 1
|
|
149
|
+
|
|
150
|
+
if args.uninstall:
|
|
151
|
+
success = uninstall()
|
|
152
|
+
return 0 if success else 1
|
|
153
|
+
|
|
154
|
+
success = install()
|
|
155
|
+
return 0 if success else 1
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
if __name__ == "__main__":
|
|
159
|
+
sys.exit(main())
|