genai-otel-instrument 0.1.24__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.
- genai_otel/__init__.py +132 -0
- genai_otel/__version__.py +34 -0
- genai_otel/auto_instrument.py +602 -0
- genai_otel/cli.py +92 -0
- genai_otel/config.py +333 -0
- genai_otel/cost_calculator.py +467 -0
- genai_otel/cost_enriching_exporter.py +207 -0
- genai_otel/cost_enrichment_processor.py +174 -0
- genai_otel/evaluation/__init__.py +76 -0
- genai_otel/evaluation/bias_detector.py +364 -0
- genai_otel/evaluation/config.py +261 -0
- genai_otel/evaluation/hallucination_detector.py +525 -0
- genai_otel/evaluation/pii_detector.py +356 -0
- genai_otel/evaluation/prompt_injection_detector.py +262 -0
- genai_otel/evaluation/restricted_topics_detector.py +316 -0
- genai_otel/evaluation/span_processor.py +962 -0
- genai_otel/evaluation/toxicity_detector.py +406 -0
- genai_otel/exceptions.py +17 -0
- genai_otel/gpu_metrics.py +516 -0
- genai_otel/instrumentors/__init__.py +71 -0
- genai_otel/instrumentors/anthropic_instrumentor.py +134 -0
- genai_otel/instrumentors/anyscale_instrumentor.py +27 -0
- genai_otel/instrumentors/autogen_instrumentor.py +394 -0
- genai_otel/instrumentors/aws_bedrock_instrumentor.py +94 -0
- genai_otel/instrumentors/azure_openai_instrumentor.py +69 -0
- genai_otel/instrumentors/base.py +919 -0
- genai_otel/instrumentors/bedrock_agents_instrumentor.py +398 -0
- genai_otel/instrumentors/cohere_instrumentor.py +140 -0
- genai_otel/instrumentors/crewai_instrumentor.py +311 -0
- genai_otel/instrumentors/dspy_instrumentor.py +661 -0
- genai_otel/instrumentors/google_ai_instrumentor.py +310 -0
- genai_otel/instrumentors/groq_instrumentor.py +106 -0
- genai_otel/instrumentors/guardrails_ai_instrumentor.py +510 -0
- genai_otel/instrumentors/haystack_instrumentor.py +503 -0
- genai_otel/instrumentors/huggingface_instrumentor.py +399 -0
- genai_otel/instrumentors/hyperbolic_instrumentor.py +236 -0
- genai_otel/instrumentors/instructor_instrumentor.py +425 -0
- genai_otel/instrumentors/langchain_instrumentor.py +340 -0
- genai_otel/instrumentors/langgraph_instrumentor.py +328 -0
- genai_otel/instrumentors/llamaindex_instrumentor.py +36 -0
- genai_otel/instrumentors/mistralai_instrumentor.py +315 -0
- genai_otel/instrumentors/ollama_instrumentor.py +197 -0
- genai_otel/instrumentors/ollama_server_metrics_poller.py +336 -0
- genai_otel/instrumentors/openai_agents_instrumentor.py +291 -0
- genai_otel/instrumentors/openai_instrumentor.py +260 -0
- genai_otel/instrumentors/pydantic_ai_instrumentor.py +362 -0
- genai_otel/instrumentors/replicate_instrumentor.py +87 -0
- genai_otel/instrumentors/sambanova_instrumentor.py +196 -0
- genai_otel/instrumentors/togetherai_instrumentor.py +146 -0
- genai_otel/instrumentors/vertexai_instrumentor.py +106 -0
- genai_otel/llm_pricing.json +1676 -0
- genai_otel/logging_config.py +45 -0
- genai_otel/mcp_instrumentors/__init__.py +14 -0
- genai_otel/mcp_instrumentors/api_instrumentor.py +144 -0
- genai_otel/mcp_instrumentors/base.py +105 -0
- genai_otel/mcp_instrumentors/database_instrumentor.py +336 -0
- genai_otel/mcp_instrumentors/kafka_instrumentor.py +31 -0
- genai_otel/mcp_instrumentors/manager.py +139 -0
- genai_otel/mcp_instrumentors/redis_instrumentor.py +31 -0
- genai_otel/mcp_instrumentors/vector_db_instrumentor.py +265 -0
- genai_otel/metrics.py +148 -0
- genai_otel/py.typed +2 -0
- genai_otel/server_metrics.py +197 -0
- genai_otel_instrument-0.1.24.dist-info/METADATA +1404 -0
- genai_otel_instrument-0.1.24.dist-info/RECORD +69 -0
- genai_otel_instrument-0.1.24.dist-info/WHEEL +5 -0
- genai_otel_instrument-0.1.24.dist-info/entry_points.txt +2 -0
- genai_otel_instrument-0.1.24.dist-info/licenses/LICENSE +680 -0
- genai_otel_instrument-0.1.24.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
"""OpenTelemetry instrumentor for AWS Bedrock Agents.
|
|
2
|
+
|
|
3
|
+
This instrumentor automatically traces agent invocations, action groups,
|
|
4
|
+
knowledge base queries, and agent orchestration using AWS Bedrock Agents.
|
|
5
|
+
|
|
6
|
+
AWS Bedrock Agents is a managed service that helps you build and deploy
|
|
7
|
+
generative AI applications with agents that can reason, take actions, and
|
|
8
|
+
access knowledge bases.
|
|
9
|
+
|
|
10
|
+
Requirements:
|
|
11
|
+
pip install boto3 botocore
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import logging
|
|
16
|
+
from typing import Any, Dict, Optional
|
|
17
|
+
|
|
18
|
+
from ..config import OTelConfig
|
|
19
|
+
from .base import BaseInstrumentor
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class BedrockAgentsInstrumentor(BaseInstrumentor):
|
|
25
|
+
"""Instrumentor for AWS Bedrock Agents"""
|
|
26
|
+
|
|
27
|
+
def __init__(self):
|
|
28
|
+
"""Initialize the instrumentor."""
|
|
29
|
+
super().__init__()
|
|
30
|
+
self._bedrock_agents_available = False
|
|
31
|
+
self._check_availability()
|
|
32
|
+
|
|
33
|
+
def _check_availability(self):
|
|
34
|
+
"""Check if Bedrock Agents runtime is available."""
|
|
35
|
+
try:
|
|
36
|
+
import boto3
|
|
37
|
+
|
|
38
|
+
# Check if bedrock-agent-runtime service is available
|
|
39
|
+
session = boto3.session.Session()
|
|
40
|
+
if "bedrock-agent-runtime" in session.get_available_services():
|
|
41
|
+
self._bedrock_agents_available = True
|
|
42
|
+
logger.debug(
|
|
43
|
+
"AWS Bedrock Agents runtime detected and available for instrumentation"
|
|
44
|
+
)
|
|
45
|
+
else:
|
|
46
|
+
logger.debug("AWS Bedrock Agents runtime service not available")
|
|
47
|
+
self._bedrock_agents_available = False
|
|
48
|
+
except ImportError:
|
|
49
|
+
logger.debug("boto3 not installed, Bedrock Agents instrumentation will be skipped")
|
|
50
|
+
self._bedrock_agents_available = False
|
|
51
|
+
|
|
52
|
+
def instrument(self, config: OTelConfig):
|
|
53
|
+
"""Instrument AWS Bedrock Agents if available.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
config (OTelConfig): The OpenTelemetry configuration object.
|
|
57
|
+
"""
|
|
58
|
+
if not self._bedrock_agents_available:
|
|
59
|
+
logger.debug("Skipping Bedrock Agents instrumentation - library not available")
|
|
60
|
+
return
|
|
61
|
+
|
|
62
|
+
self.config = config
|
|
63
|
+
|
|
64
|
+
try:
|
|
65
|
+
import wrapt
|
|
66
|
+
from botocore.client import BaseClient
|
|
67
|
+
|
|
68
|
+
# Store original make_request method
|
|
69
|
+
original_make_request = BaseClient._make_request
|
|
70
|
+
|
|
71
|
+
# Wrap the _make_request method
|
|
72
|
+
def wrapped_make_request(self, operation_model, request_dict, *args, **kwargs):
|
|
73
|
+
# Only instrument bedrock-agent-runtime operations
|
|
74
|
+
if (
|
|
75
|
+
hasattr(self, "_service_model")
|
|
76
|
+
and self._service_model.service_name == "bedrock-agent-runtime"
|
|
77
|
+
):
|
|
78
|
+
operation_name = operation_model.name
|
|
79
|
+
|
|
80
|
+
# Instrument invoke_agent operation
|
|
81
|
+
if operation_name == "InvokeAgent":
|
|
82
|
+
return self._instrumentor.create_span_wrapper(
|
|
83
|
+
span_name="bedrock.agents.invoke_agent",
|
|
84
|
+
extract_attributes=lambda inst, args, kwargs: self._instrumentor._extract_invoke_agent_attributes(
|
|
85
|
+
request_dict
|
|
86
|
+
),
|
|
87
|
+
)(original_make_request)(
|
|
88
|
+
self, operation_model, request_dict, *args, **kwargs
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
# Instrument retrieve operation
|
|
92
|
+
elif operation_name == "Retrieve":
|
|
93
|
+
return self._instrumentor.create_span_wrapper(
|
|
94
|
+
span_name="bedrock.agents.retrieve",
|
|
95
|
+
extract_attributes=lambda inst, args, kwargs: self._instrumentor._extract_retrieve_attributes(
|
|
96
|
+
request_dict
|
|
97
|
+
),
|
|
98
|
+
)(original_make_request)(
|
|
99
|
+
self, operation_model, request_dict, *args, **kwargs
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
# Instrument retrieve_and_generate operation
|
|
103
|
+
elif operation_name == "RetrieveAndGenerate":
|
|
104
|
+
return self._instrumentor.create_span_wrapper(
|
|
105
|
+
span_name="bedrock.agents.retrieve_and_generate",
|
|
106
|
+
extract_attributes=lambda inst, args, kwargs: self._instrumentor._extract_retrieve_and_generate_attributes(
|
|
107
|
+
request_dict
|
|
108
|
+
),
|
|
109
|
+
)(original_make_request)(
|
|
110
|
+
self, operation_model, request_dict, *args, **kwargs
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
# Call original for non-bedrock-agent-runtime operations
|
|
114
|
+
return original_make_request(self, operation_model, request_dict, *args, **kwargs)
|
|
115
|
+
|
|
116
|
+
# Attach instrumentor reference for access in wrapper
|
|
117
|
+
BaseClient._instrumentor = self
|
|
118
|
+
|
|
119
|
+
# Replace the method
|
|
120
|
+
BaseClient._make_request = wrapped_make_request
|
|
121
|
+
|
|
122
|
+
self._instrumented = True
|
|
123
|
+
logger.info("AWS Bedrock Agents instrumentation enabled")
|
|
124
|
+
|
|
125
|
+
except Exception as e:
|
|
126
|
+
logger.error("Failed to instrument AWS Bedrock Agents: %s", e, exc_info=True)
|
|
127
|
+
if config.fail_on_error:
|
|
128
|
+
raise
|
|
129
|
+
|
|
130
|
+
def _extract_invoke_agent_attributes(self, request_dict: Dict[str, Any]) -> Dict[str, Any]:
|
|
131
|
+
"""Extract attributes from InvokeAgent request.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
request_dict: The request dictionary from boto3.
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
Dict[str, Any]: Dictionary of attributes to set on the span.
|
|
138
|
+
"""
|
|
139
|
+
attrs = {}
|
|
140
|
+
|
|
141
|
+
# Core attributes
|
|
142
|
+
attrs["gen_ai.system"] = "bedrock_agents"
|
|
143
|
+
attrs["gen_ai.operation.name"] = "invoke_agent"
|
|
144
|
+
|
|
145
|
+
try:
|
|
146
|
+
# Extract from body (already serialized JSON)
|
|
147
|
+
body = request_dict.get("body", {})
|
|
148
|
+
if isinstance(body, str):
|
|
149
|
+
body = json.loads(body)
|
|
150
|
+
|
|
151
|
+
# Extract agent identifiers
|
|
152
|
+
if "agentId" in body:
|
|
153
|
+
attrs["bedrock.agent.id"] = body["agentId"]
|
|
154
|
+
if "agentAliasId" in body:
|
|
155
|
+
attrs["bedrock.agent.alias_id"] = body["agentAliasId"]
|
|
156
|
+
|
|
157
|
+
# Extract session information
|
|
158
|
+
if "sessionId" in body:
|
|
159
|
+
attrs["bedrock.agent.session_id"] = body["sessionId"]
|
|
160
|
+
|
|
161
|
+
# Extract input text
|
|
162
|
+
if "inputText" in body:
|
|
163
|
+
attrs["bedrock.agent.input_text"] = str(body["inputText"])[:500]
|
|
164
|
+
|
|
165
|
+
# Extract session state if present
|
|
166
|
+
if "sessionState" in body:
|
|
167
|
+
session_state = body["sessionState"]
|
|
168
|
+
if isinstance(session_state, dict):
|
|
169
|
+
# Extract prompt session attributes
|
|
170
|
+
if "promptSessionAttributes" in session_state:
|
|
171
|
+
attrs["bedrock.agent.prompt_attributes"] = str(
|
|
172
|
+
session_state["promptSessionAttributes"]
|
|
173
|
+
)[:200]
|
|
174
|
+
|
|
175
|
+
# Extract session attributes
|
|
176
|
+
if "sessionAttributes" in session_state:
|
|
177
|
+
attrs["bedrock.agent.session_attributes"] = str(
|
|
178
|
+
session_state["sessionAttributes"]
|
|
179
|
+
)[:200]
|
|
180
|
+
|
|
181
|
+
# Extract enable trace flag
|
|
182
|
+
if "enableTrace" in body:
|
|
183
|
+
attrs["bedrock.agent.enable_trace"] = body["enableTrace"]
|
|
184
|
+
|
|
185
|
+
except Exception as e:
|
|
186
|
+
logger.debug("Failed to extract invoke_agent attributes: %s", e)
|
|
187
|
+
|
|
188
|
+
return attrs
|
|
189
|
+
|
|
190
|
+
def _extract_retrieve_attributes(self, request_dict: Dict[str, Any]) -> Dict[str, Any]:
|
|
191
|
+
"""Extract attributes from Retrieve request.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
request_dict: The request dictionary from boto3.
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
Dict[str, Any]: Dictionary of attributes to set on the span.
|
|
198
|
+
"""
|
|
199
|
+
attrs = {}
|
|
200
|
+
|
|
201
|
+
# Core attributes
|
|
202
|
+
attrs["gen_ai.system"] = "bedrock_agents"
|
|
203
|
+
attrs["gen_ai.operation.name"] = "retrieve"
|
|
204
|
+
|
|
205
|
+
try:
|
|
206
|
+
body = request_dict.get("body", {})
|
|
207
|
+
if isinstance(body, str):
|
|
208
|
+
body = json.loads(body)
|
|
209
|
+
|
|
210
|
+
# Extract knowledge base ID
|
|
211
|
+
if "knowledgeBaseId" in body:
|
|
212
|
+
attrs["bedrock.knowledge_base.id"] = body["knowledgeBaseId"]
|
|
213
|
+
|
|
214
|
+
# Extract retrieval query
|
|
215
|
+
if "retrievalQuery" in body:
|
|
216
|
+
query = body["retrievalQuery"]
|
|
217
|
+
if isinstance(query, dict) and "text" in query:
|
|
218
|
+
attrs["bedrock.retrieval.query"] = str(query["text"])[:500]
|
|
219
|
+
|
|
220
|
+
# Extract retrieval configuration
|
|
221
|
+
if "retrievalConfiguration" in body:
|
|
222
|
+
config = body["retrievalConfiguration"]
|
|
223
|
+
if isinstance(config, dict):
|
|
224
|
+
# Extract vector search config
|
|
225
|
+
if "vectorSearchConfiguration" in config:
|
|
226
|
+
vector_config = config["vectorSearchConfiguration"]
|
|
227
|
+
if "numberOfResults" in vector_config:
|
|
228
|
+
attrs["bedrock.retrieval.number_of_results"] = vector_config[
|
|
229
|
+
"numberOfResults"
|
|
230
|
+
]
|
|
231
|
+
if "overrideSearchType" in vector_config:
|
|
232
|
+
attrs["bedrock.retrieval.search_type"] = vector_config[
|
|
233
|
+
"overrideSearchType"
|
|
234
|
+
]
|
|
235
|
+
|
|
236
|
+
except Exception as e:
|
|
237
|
+
logger.debug("Failed to extract retrieve attributes: %s", e)
|
|
238
|
+
|
|
239
|
+
return attrs
|
|
240
|
+
|
|
241
|
+
def _extract_retrieve_and_generate_attributes(
|
|
242
|
+
self, request_dict: Dict[str, Any]
|
|
243
|
+
) -> Dict[str, Any]:
|
|
244
|
+
"""Extract attributes from RetrieveAndGenerate request.
|
|
245
|
+
|
|
246
|
+
Args:
|
|
247
|
+
request_dict: The request dictionary from boto3.
|
|
248
|
+
|
|
249
|
+
Returns:
|
|
250
|
+
Dict[str, Any]: Dictionary of attributes to set on the span.
|
|
251
|
+
"""
|
|
252
|
+
attrs = {}
|
|
253
|
+
|
|
254
|
+
# Core attributes
|
|
255
|
+
attrs["gen_ai.system"] = "bedrock_agents"
|
|
256
|
+
attrs["gen_ai.operation.name"] = "retrieve_and_generate"
|
|
257
|
+
|
|
258
|
+
try:
|
|
259
|
+
body = request_dict.get("body", {})
|
|
260
|
+
if isinstance(body, str):
|
|
261
|
+
body = json.loads(body)
|
|
262
|
+
|
|
263
|
+
# Extract input
|
|
264
|
+
if "input" in body:
|
|
265
|
+
input_data = body["input"]
|
|
266
|
+
if isinstance(input_data, dict) and "text" in input_data:
|
|
267
|
+
attrs["bedrock.rag.input_text"] = str(input_data["text"])[:500]
|
|
268
|
+
|
|
269
|
+
# Extract session ID
|
|
270
|
+
if "sessionId" in body:
|
|
271
|
+
attrs["bedrock.rag.session_id"] = body["sessionId"]
|
|
272
|
+
|
|
273
|
+
# Extract retrieve and generate configuration
|
|
274
|
+
if "retrieveAndGenerateConfiguration" in body:
|
|
275
|
+
config = body["retrieveAndGenerateConfiguration"]
|
|
276
|
+
if isinstance(config, dict):
|
|
277
|
+
# Extract type
|
|
278
|
+
if "type" in config:
|
|
279
|
+
attrs["bedrock.rag.type"] = config["type"]
|
|
280
|
+
|
|
281
|
+
# Extract knowledge base configuration
|
|
282
|
+
if "knowledgeBaseConfiguration" in config:
|
|
283
|
+
kb_config = config["knowledgeBaseConfiguration"]
|
|
284
|
+
if "knowledgeBaseId" in kb_config:
|
|
285
|
+
attrs["bedrock.knowledge_base.id"] = kb_config["knowledgeBaseId"]
|
|
286
|
+
if "modelArn" in kb_config:
|
|
287
|
+
attrs["gen_ai.request.model"] = kb_config["modelArn"]
|
|
288
|
+
|
|
289
|
+
# Extract generation configuration
|
|
290
|
+
if "generationConfiguration" in kb_config:
|
|
291
|
+
gen_config = kb_config["generationConfiguration"]
|
|
292
|
+
if "inferenceConfig" in gen_config:
|
|
293
|
+
inference = gen_config["inferenceConfig"]
|
|
294
|
+
if "temperature" in inference:
|
|
295
|
+
attrs["gen_ai.request.temperature"] = inference["temperature"]
|
|
296
|
+
if "maxTokens" in inference:
|
|
297
|
+
attrs["gen_ai.request.max_tokens"] = inference["maxTokens"]
|
|
298
|
+
if "topP" in inference:
|
|
299
|
+
attrs["gen_ai.request.top_p"] = inference["topP"]
|
|
300
|
+
|
|
301
|
+
except Exception as e:
|
|
302
|
+
logger.debug("Failed to extract retrieve_and_generate attributes: %s", e)
|
|
303
|
+
|
|
304
|
+
return attrs
|
|
305
|
+
|
|
306
|
+
def _extract_usage(self, result) -> Optional[Dict[str, int]]:
|
|
307
|
+
"""Extract token usage from agent response.
|
|
308
|
+
|
|
309
|
+
Note: Bedrock Agents may not expose token usage directly.
|
|
310
|
+
Token usage is captured by underlying Bedrock model calls.
|
|
311
|
+
|
|
312
|
+
Args:
|
|
313
|
+
result: The agent invocation result.
|
|
314
|
+
|
|
315
|
+
Returns:
|
|
316
|
+
Optional[Dict[str, int]]: Dictionary with token counts or None.
|
|
317
|
+
"""
|
|
318
|
+
# Bedrock Agents responses don't typically include token usage
|
|
319
|
+
# Token usage is tracked by underlying Bedrock model instrumentor
|
|
320
|
+
return None
|
|
321
|
+
|
|
322
|
+
def _extract_response_attributes(self, result) -> Dict[str, Any]:
|
|
323
|
+
"""Extract response attributes from agent result.
|
|
324
|
+
|
|
325
|
+
Args:
|
|
326
|
+
result: The agent invocation result.
|
|
327
|
+
|
|
328
|
+
Returns:
|
|
329
|
+
Dict[str, Any]: Dictionary of response attributes.
|
|
330
|
+
"""
|
|
331
|
+
attrs = {}
|
|
332
|
+
|
|
333
|
+
try:
|
|
334
|
+
# For invoke_agent responses
|
|
335
|
+
if isinstance(result, dict):
|
|
336
|
+
# Extract session ID
|
|
337
|
+
if "sessionId" in result:
|
|
338
|
+
attrs["bedrock.agent.response.session_id"] = result["sessionId"]
|
|
339
|
+
|
|
340
|
+
# Extract content type
|
|
341
|
+
if "contentType" in result:
|
|
342
|
+
attrs["bedrock.agent.response.content_type"] = result["contentType"]
|
|
343
|
+
|
|
344
|
+
# For streaming responses, we get an event stream
|
|
345
|
+
# The actual response parsing would happen in the application code
|
|
346
|
+
# We can log that a response was received
|
|
347
|
+
if "completion" in result:
|
|
348
|
+
attrs["bedrock.agent.response.has_completion"] = True
|
|
349
|
+
|
|
350
|
+
# For retrieve responses
|
|
351
|
+
if "retrievalResults" in result:
|
|
352
|
+
retrieval_results = result["retrievalResults"]
|
|
353
|
+
if isinstance(retrieval_results, list):
|
|
354
|
+
attrs["bedrock.retrieval.results_count"] = len(retrieval_results)
|
|
355
|
+
|
|
356
|
+
# For retrieve_and_generate responses
|
|
357
|
+
if "output" in result:
|
|
358
|
+
output = result["output"]
|
|
359
|
+
if isinstance(output, dict):
|
|
360
|
+
if "text" in output:
|
|
361
|
+
attrs["bedrock.rag.output_text"] = str(output["text"])[:500]
|
|
362
|
+
|
|
363
|
+
# Extract citations if present
|
|
364
|
+
if "citations" in result:
|
|
365
|
+
citations = result["citations"]
|
|
366
|
+
if isinstance(citations, list):
|
|
367
|
+
attrs["bedrock.rag.citations_count"] = len(citations)
|
|
368
|
+
|
|
369
|
+
except Exception as e:
|
|
370
|
+
logger.debug("Failed to extract response attributes: %s", e)
|
|
371
|
+
|
|
372
|
+
return attrs
|
|
373
|
+
|
|
374
|
+
def _extract_finish_reason(self, result) -> Optional[str]:
|
|
375
|
+
"""Extract finish reason from agent result.
|
|
376
|
+
|
|
377
|
+
Args:
|
|
378
|
+
result: The agent invocation result.
|
|
379
|
+
|
|
380
|
+
Returns:
|
|
381
|
+
Optional[str]: The finish reason string or None if not available.
|
|
382
|
+
"""
|
|
383
|
+
try:
|
|
384
|
+
# For streaming responses, finish reason might be in the final event
|
|
385
|
+
# For non-streaming, completion indicates success
|
|
386
|
+
if isinstance(result, dict):
|
|
387
|
+
# Check for stop reason in streaming events
|
|
388
|
+
if "stopReason" in result:
|
|
389
|
+
return result["stopReason"]
|
|
390
|
+
|
|
391
|
+
# If we have output/completion, assume successful completion
|
|
392
|
+
if "output" in result or "completion" in result:
|
|
393
|
+
return "completed"
|
|
394
|
+
|
|
395
|
+
except Exception as e:
|
|
396
|
+
logger.debug("Failed to extract finish reason: %s", e)
|
|
397
|
+
|
|
398
|
+
return None
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""OpenTelemetry instrumentor for the Cohere SDK.
|
|
2
|
+
|
|
3
|
+
This instrumentor automatically traces calls to Cohere models, capturing
|
|
4
|
+
relevant attributes such as the model name and token usage.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
from typing import Any, Dict, Optional
|
|
9
|
+
|
|
10
|
+
from ..config import OTelConfig
|
|
11
|
+
from .base import BaseInstrumentor
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class CohereInstrumentor(BaseInstrumentor):
|
|
17
|
+
"""Instrumentor for Cohere"""
|
|
18
|
+
|
|
19
|
+
def __init__(self):
|
|
20
|
+
"""Initialize the instrumentor."""
|
|
21
|
+
super().__init__()
|
|
22
|
+
self._cohere_available = False
|
|
23
|
+
self._check_availability()
|
|
24
|
+
|
|
25
|
+
def _check_availability(self):
|
|
26
|
+
"""Check if cohere library is available."""
|
|
27
|
+
try:
|
|
28
|
+
import cohere
|
|
29
|
+
|
|
30
|
+
self._cohere_available = True
|
|
31
|
+
logger.debug("cohere library detected and available for instrumentation")
|
|
32
|
+
except ImportError:
|
|
33
|
+
logger.debug("cohere library not installed, instrumentation will be skipped")
|
|
34
|
+
self._cohere_available = False
|
|
35
|
+
|
|
36
|
+
def instrument(self, config: OTelConfig):
|
|
37
|
+
"""Instrument cohere if available."""
|
|
38
|
+
if not self._cohere_available:
|
|
39
|
+
logger.debug("Skipping instrumentation - library not available")
|
|
40
|
+
return
|
|
41
|
+
|
|
42
|
+
self.config = config
|
|
43
|
+
try:
|
|
44
|
+
import cohere
|
|
45
|
+
|
|
46
|
+
original_init = cohere.Client.__init__
|
|
47
|
+
|
|
48
|
+
def wrapped_init(instance, *args, **kwargs):
|
|
49
|
+
original_init(instance, *args, **kwargs)
|
|
50
|
+
self._instrument_client(instance)
|
|
51
|
+
|
|
52
|
+
cohere.Client.__init__ = wrapped_init
|
|
53
|
+
self._instrumented = True
|
|
54
|
+
logger.info("Cohere instrumentation enabled")
|
|
55
|
+
|
|
56
|
+
except Exception as e:
|
|
57
|
+
logger.error("Failed to instrument Cohere: %s", e, exc_info=True)
|
|
58
|
+
if config.fail_on_error:
|
|
59
|
+
raise
|
|
60
|
+
|
|
61
|
+
def _instrument_client(self, client):
|
|
62
|
+
"""Instrument Cohere client methods."""
|
|
63
|
+
original_generate = client.generate
|
|
64
|
+
|
|
65
|
+
# Wrap using create_span_wrapper
|
|
66
|
+
wrapped_generate = self.create_span_wrapper(
|
|
67
|
+
span_name="cohere.generate",
|
|
68
|
+
extract_attributes=self._extract_generate_attributes,
|
|
69
|
+
)(original_generate)
|
|
70
|
+
|
|
71
|
+
client.generate = wrapped_generate
|
|
72
|
+
|
|
73
|
+
def _extract_generate_attributes(self, instance: Any, args: Any, kwargs: Any) -> Dict[str, Any]:
|
|
74
|
+
"""Extract attributes from Cohere generate call.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
instance: The client instance.
|
|
78
|
+
args: Positional arguments.
|
|
79
|
+
kwargs: Keyword arguments.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
Dict[str, Any]: Dictionary of attributes to set on the span.
|
|
83
|
+
"""
|
|
84
|
+
attrs = {}
|
|
85
|
+
model = kwargs.get("model", "command")
|
|
86
|
+
prompt = kwargs.get("prompt", "")
|
|
87
|
+
|
|
88
|
+
attrs["gen_ai.system"] = "cohere"
|
|
89
|
+
attrs["gen_ai.request.model"] = model
|
|
90
|
+
attrs["gen_ai.operation.name"] = "generate"
|
|
91
|
+
attrs["gen_ai.request.message_count"] = 1 if prompt else 0
|
|
92
|
+
|
|
93
|
+
return attrs
|
|
94
|
+
|
|
95
|
+
def _extract_usage(self, result) -> Optional[Dict[str, int]]:
|
|
96
|
+
"""Extract token usage from Cohere response.
|
|
97
|
+
|
|
98
|
+
Cohere responses include meta.tokens with:
|
|
99
|
+
- input_tokens: Input tokens
|
|
100
|
+
- output_tokens: Output tokens
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
result: The API response object.
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
Optional[Dict[str, int]]: Dictionary with token counts or None.
|
|
107
|
+
"""
|
|
108
|
+
try:
|
|
109
|
+
# Handle object response
|
|
110
|
+
if hasattr(result, "meta") and result.meta:
|
|
111
|
+
meta = result.meta
|
|
112
|
+
# Check for tokens object
|
|
113
|
+
if hasattr(meta, "tokens") and meta.tokens:
|
|
114
|
+
tokens = meta.tokens
|
|
115
|
+
input_tokens = getattr(tokens, "input_tokens", 0)
|
|
116
|
+
output_tokens = getattr(tokens, "output_tokens", 0)
|
|
117
|
+
|
|
118
|
+
if input_tokens or output_tokens:
|
|
119
|
+
return {
|
|
120
|
+
"prompt_tokens": int(input_tokens) if input_tokens else 0,
|
|
121
|
+
"completion_tokens": int(output_tokens) if output_tokens else 0,
|
|
122
|
+
"total_tokens": int(input_tokens or 0) + int(output_tokens or 0),
|
|
123
|
+
}
|
|
124
|
+
# Fallback to billed_units
|
|
125
|
+
elif hasattr(meta, "billed_units") and meta.billed_units:
|
|
126
|
+
billed = meta.billed_units
|
|
127
|
+
input_tokens = getattr(billed, "input_tokens", 0)
|
|
128
|
+
output_tokens = getattr(billed, "output_tokens", 0)
|
|
129
|
+
|
|
130
|
+
if input_tokens or output_tokens:
|
|
131
|
+
return {
|
|
132
|
+
"prompt_tokens": int(input_tokens) if input_tokens else 0,
|
|
133
|
+
"completion_tokens": int(output_tokens) if output_tokens else 0,
|
|
134
|
+
"total_tokens": int(input_tokens or 0) + int(output_tokens or 0),
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return None
|
|
138
|
+
except Exception as e:
|
|
139
|
+
logger.debug("Failed to extract usage from Cohere response: %s", e)
|
|
140
|
+
return None
|