Skip to content

Commit f3d06fc

Browse files
author
Nagkumar Arkalgud
committed
Updates according to latest spec
1 parent 523fb66 commit f3d06fc

File tree

6 files changed

+875
-542
lines changed

6 files changed

+875
-542
lines changed

instrumentation-genai/opentelemetry-instrumentation-openai-agents/src/opentelemetry/instrumentation/openai_agents/__init__.py

Lines changed: 40 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@
1313
# limitations under the License.
1414

1515
"""
16-
OpenAI Agents instrumentation supporting `openai` in agent frameworks, it can be enabled by
17-
using ``OpenAIAgentsInstrumentor``.
16+
OpenAI Agents instrumentation supporting `openai` in agent frameworks.
17+
Enable with ``OpenAIAgentsInstrumentor``.
1818
1919
.. _openai: https://pypi.org/project/openai/
2020
@@ -24,7 +24,9 @@
2424
.. code:: python
2525
2626
from openai import OpenAI
27-
from opentelemetry.instrumentation.openai_agents import OpenAIAgentsInstrumentor
27+
from opentelemetry.instrumentation.openai_agents import (
28+
OpenAIAgentsInstrumentor,
29+
)
2830
2931
OpenAIAgentsInstrumentor().instrument()
3032
@@ -34,7 +36,8 @@
3436
API
3537
---
3638
37-
The OpenAI Agents instrumentation captures spans for OpenAI API calls made within agent frameworks.
39+
This instrumentation captures spans for OpenAI API calls made within
40+
agent frameworks.
3841
It provides detailed tracing information including:
3942
4043
- Request and response content (if configured)
@@ -45,21 +48,29 @@
4548
Configuration
4649
-------------
4750
48-
The following environment variables can be used to configure the instrumentation:
51+
Environment variables:
4952
50-
- ``OTEL_INSTRUMENTATION_OPENAI_AGENTS_CAPTURE_CONTENT``: Set to ``true`` to capture the content of the requests and responses. Default is ``false``.
51-
- ``OTEL_INSTRUMENTATION_OPENAI_AGENTS_CAPTURE_METRICS``: Set to ``true`` to capture metrics. Default is ``true``.
53+
- ``OTEL_INSTRUMENTATION_OPENAI_AGENTS_CAPTURE_CONTENT``: ``true`` to
54+
capture request/response content (default ``false``).
55+
- ``OTEL_INSTRUMENTATION_OPENAI_AGENTS_CAPTURE_METRICS``: ``true`` to
56+
capture metrics (default ``true``).
5257
"""
5358

5459
import logging
55-
from os import environ
5660
from typing import Collection
5761

5862
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
5963
from opentelemetry.instrumentation.openai_agents.package import _instruments
60-
from opentelemetry.instrumentation.openai_agents.version import __version__
61-
from opentelemetry.instrumentation.openai_agents.utils import is_content_enabled
62-
from opentelemetry.instrumentation.openai_agents.genai_semantic_processor import GenAISemanticProcessor
64+
from opentelemetry.instrumentation.openai_agents.genai_semantic_processor import (
65+
GenAISemanticProcessor,
66+
)
67+
from opentelemetry.instrumentation.openai_agents.constants import (
68+
GenAIProvider,
69+
GenAIOperationName,
70+
GenAIToolType,
71+
GenAIOutputType,
72+
GenAIEvaluationAttributes,
73+
)
6374
from opentelemetry.semconv.schemas import Schemas
6475
from opentelemetry.trace import get_tracer
6576
from opentelemetry._events import get_event_logger
@@ -92,14 +103,28 @@ def _instrument(self, **kwargs):
92103
event_logger_provider=event_logger_provider,
93104
)
94105

95-
add_trace_processor(GenAISemanticProcessor(tracer=tracer, event_logger=event_logger))
96-
97-
106+
add_trace_processor(
107+
GenAISemanticProcessor(
108+
tracer=tracer,
109+
event_logger=event_logger,
110+
)
111+
)
98112

113+
# Expose constants via module attributes for convenience
114+
globals().update(
115+
{
116+
"GenAIProvider": GenAIProvider,
117+
"GenAIOperationName": GenAIOperationName,
118+
"GenAIToolType": GenAIToolType,
119+
"GenAIOutputType": GenAIOutputType,
120+
"GenAIEvaluationAttributes": GenAIEvaluationAttributes,
121+
}
122+
)
123+
99124
def _uninstrument(self, **kwargs):
100125
"""Uninstruments the OpenAI library for agent frameworks."""
101126
pass
102127

103128
def instrumentation_dependencies(self) -> Collection[str]:
104-
"""Return a list of python packages with versions that the will be instrumented."""
129+
"""Return list of python packages with versions instrumented."""
105130
return _instruments
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
"""Centralized semantic convention constants for GenAI instrumentation.
2+
3+
Consolidates provider names, operation names, tool types, output types,
4+
evaluation attributes, and helper maps so other modules can import from
5+
one place. Keeping strings in one module reduces drift as the spec evolves.
6+
"""
7+
from __future__ import annotations
8+
9+
# Provider names (superset for forward compatibility)
10+
class GenAIProvider:
11+
OPENAI = "openai"
12+
GCP_GEN_AI = "gcp.gen_ai"
13+
GCP_VERTEX_AI = "gcp.vertex_ai"
14+
GCP_GEMINI = "gcp.gemini"
15+
ANTHROPIC = "anthropic"
16+
COHERE = "cohere"
17+
AZURE_AI_INFERENCE = "azure.ai.inference"
18+
AZURE_AI_OPENAI = "azure.ai.openai"
19+
IBM_WATSONX_AI = "ibm.watsonx.ai"
20+
AWS_BEDROCK = "aws.bedrock"
21+
PERPLEXITY = "perplexity"
22+
X_AI = "x_ai"
23+
DEEPSEEK = "deepseek"
24+
GROQ = "groq"
25+
MISTRAL_AI = "mistral_ai"
26+
27+
ALL = {
28+
OPENAI,
29+
GCP_GEN_AI,
30+
GCP_VERTEX_AI,
31+
GCP_GEMINI,
32+
ANTHROPIC,
33+
COHERE,
34+
AZURE_AI_INFERENCE,
35+
AZURE_AI_OPENAI,
36+
IBM_WATSONX_AI,
37+
AWS_BEDROCK,
38+
PERPLEXITY,
39+
X_AI,
40+
DEEPSEEK,
41+
GROQ,
42+
MISTRAL_AI,
43+
}
44+
45+
46+
class GenAIOperationName:
47+
CHAT = "chat"
48+
GENERATE_CONTENT = "generate_content"
49+
TEXT_COMPLETION = "text_completion"
50+
EMBEDDINGS = "embeddings"
51+
CREATE_AGENT = "create_agent"
52+
INVOKE_AGENT = "invoke_agent"
53+
EXECUTE_TOOL = "execute_tool"
54+
TRANSCRIPTION = "transcription"
55+
SPEECH = "speech_generation"
56+
GUARDRAIL = "guardrail_check"
57+
HANDOFF = "agent_handoff"
58+
RESPONSE = "response" # internal aggregator in current processor
59+
60+
# Mapping of span data class (lower) to default op (heuristic)
61+
CLASS_FALLBACK = {
62+
"generationspan": CHAT,
63+
"responsespan": RESPONSE,
64+
"functionspan": EXECUTE_TOOL,
65+
"agentspan": INVOKE_AGENT,
66+
}
67+
68+
69+
class GenAIOutputType:
70+
TEXT = "text"
71+
JSON = "json"
72+
IMAGE = "image"
73+
SPEECH = "speech"
74+
# existing custom inference types retained for backward compatibility
75+
76+
77+
class GenAIToolType:
78+
FUNCTION = "function"
79+
EXTENSION = "extension"
80+
DATASTORE = "datastore"
81+
82+
ALL = {FUNCTION, EXTENSION, DATASTORE}
83+
84+
85+
class GenAIEvaluationAttributes:
86+
NAME = "gen_ai.evaluation.name"
87+
SCORE_VALUE = "gen_ai.evaluation.score.value"
88+
SCORE_LABEL = "gen_ai.evaluation.score.label"
89+
EXPLANATION = "gen_ai.evaluation.explanation"
90+
91+
92+
# Complete list of GenAI semantic convention attribute keys
93+
GEN_AI_PROVIDER_NAME = "gen_ai.provider.name"
94+
GEN_AI_OPERATION_NAME = "gen_ai.operation.name"
95+
GEN_AI_REQUEST_MODEL = "gen_ai.request.model"
96+
GEN_AI_REQUEST_MAX_TOKENS = "gen_ai.request.max_tokens"
97+
GEN_AI_REQUEST_TEMPERATURE = "gen_ai.request.temperature"
98+
GEN_AI_REQUEST_TOP_P = "gen_ai.request.top_p"
99+
GEN_AI_REQUEST_TOP_K = "gen_ai.request.top_k"
100+
GEN_AI_REQUEST_FREQUENCY_PENALTY = "gen_ai.request.frequency_penalty"
101+
GEN_AI_REQUEST_PRESENCE_PENALTY = "gen_ai.request.presence_penalty"
102+
GEN_AI_REQUEST_CHOICE_COUNT = "gen_ai.request.choice.count"
103+
GEN_AI_REQUEST_STOP_SEQUENCES = "gen_ai.request.stop_sequences"
104+
GEN_AI_REQUEST_ENCODING_FORMATS = "gen_ai.request.encoding_formats"
105+
GEN_AI_REQUEST_SEED = "gen_ai.request.seed"
106+
GEN_AI_RESPONSE_ID = "gen_ai.response.id"
107+
GEN_AI_RESPONSE_MODEL = "gen_ai.response.model"
108+
GEN_AI_RESPONSE_FINISH_REASONS = "gen_ai.response.finish_reasons"
109+
GEN_AI_USAGE_INPUT_TOKENS = "gen_ai.usage.input_tokens"
110+
GEN_AI_USAGE_OUTPUT_TOKENS = "gen_ai.usage.output_tokens"
111+
GEN_AI_USAGE_TOTAL_TOKENS = "gen_ai.usage.total_tokens"
112+
GEN_AI_CONVERSATION_ID = "gen_ai.conversation.id"
113+
GEN_AI_AGENT_ID = "gen_ai.agent.id"
114+
GEN_AI_AGENT_NAME = "gen_ai.agent.name"
115+
GEN_AI_AGENT_DESCRIPTION = "gen_ai.agent.description"
116+
GEN_AI_TOOL_NAME = "gen_ai.tool.name"
117+
GEN_AI_TOOL_TYPE = "gen_ai.tool.type"
118+
GEN_AI_TOOL_CALL_ID = "gen_ai.tool.call.id"
119+
GEN_AI_TOOL_DESCRIPTION = "gen_ai.tool.description"
120+
GEN_AI_TOOL_CALL_ARGUMENTS = "gen_ai.tool.call.arguments"
121+
GEN_AI_TOOL_CALL_RESULT = "gen_ai.tool.call.result"
122+
GEN_AI_TOOL_DEFINITIONS = "gen_ai.tool.definitions"
123+
GEN_AI_ORCHESTRATOR_AGENT_DEFINITIONS = "gen_ai.orchestrator.agent.definitions"
124+
GEN_AI_OUTPUT_TYPE = "gen_ai.output.type"
125+
GEN_AI_SYSTEM_INSTRUCTIONS = "gen_ai.system_instructions"
126+
GEN_AI_INPUT_MESSAGES = "gen_ai.input.messages"
127+
GEN_AI_OUTPUT_MESSAGES = "gen_ai.output.messages"
128+
GEN_AI_GUARDRAIL_NAME = "gen_ai.guardrail.name"
129+
GEN_AI_GUARDRAIL_TRIGGERED = "gen_ai.guardrail.triggered"
130+
GEN_AI_HANDOFF_FROM_AGENT = "gen_ai.handoff.from_agent"
131+
GEN_AI_HANDOFF_TO_AGENT = "gen_ai.handoff.to_agent"
132+
GEN_AI_EMBEDDINGS_DIMENSION_COUNT = "gen_ai.embeddings.dimension.count"
133+
GEN_AI_DATA_SOURCE_ID = "gen_ai.data_source.id"
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
"""Event emission helpers for GenAI instrumentation.
2+
3+
Provides thin wrappers around the OpenTelemetry event logger for
4+
spec-defined GenAI events:
5+
6+
- gen_ai.client.inference.operation.details
7+
- gen_ai.evaluation.result (helper only; actual evaluation may happen
8+
externally – e.g. offline evaluator script)
9+
10+
Environment toggles:
11+
- OTEL_GENAI_EMIT_OPERATION_DETAILS (default: true)
12+
- OTEL_GENAI_EMIT_EVALUATION_RESULTS (default: true)
13+
"""
14+
from __future__ import annotations
15+
16+
from typing import Any, Dict, Optional
17+
import os
18+
from opentelemetry._events import Event
19+
20+
OP_DETAILS_EVENT = "gen_ai.client.inference.operation.details"
21+
EVAL_RESULT_EVENT = "gen_ai.evaluation.result"
22+
23+
_OP_DETAILS_ENABLED_ENV = "OTEL_GENAI_EMIT_OPERATION_DETAILS"
24+
_EVAL_RESULTS_ENABLED_ENV = "OTEL_GENAI_EMIT_EVALUATION_RESULTS"
25+
26+
# Attribute allowlist subset to avoid dumping huge blobs blindly.
27+
# (Large content already controlled by capture-content env var.)
28+
_OPERATION_DETAILS_CORE_KEYS = {
29+
"gen_ai.provider.name",
30+
"gen_ai.operation.name",
31+
"gen_ai.request.model",
32+
"gen_ai.response.id",
33+
"gen_ai.response.model",
34+
"gen_ai.usage.input_tokens",
35+
"gen_ai.usage.output_tokens",
36+
"gen_ai.usage.total_tokens",
37+
"gen_ai.output.type",
38+
"gen_ai.system_instructions",
39+
"gen_ai.input.messages",
40+
"gen_ai.output.messages",
41+
}
42+
43+
_MAX_VALUE_LEN = 4000 # defensive truncation
44+
45+
46+
def _enabled(env_name: str, default_true: bool = True) -> bool:
47+
raw = os.getenv(env_name)
48+
if raw is None:
49+
return default_true
50+
return raw.lower() not in {"0", "false", "no"}
51+
52+
53+
def emit_operation_details_event(
54+
event_logger,
55+
span_attributes: Dict[str, Any],
56+
otel_span: Optional[Any] = None
57+
) -> None:
58+
"""Emit operation details event with span attributes.
59+
60+
Args:
61+
event_logger: The event logger instance
62+
span_attributes: Dictionary of span attributes to include
63+
otel_span: Optional OpenTelemetry span for context
64+
"""
65+
if not event_logger:
66+
return
67+
if not _enabled(_OP_DETAILS_ENABLED_ENV, True):
68+
return
69+
70+
attrs: Dict[str, Any] = {}
71+
for k in _OPERATION_DETAILS_CORE_KEYS:
72+
if k in span_attributes:
73+
v = span_attributes[k]
74+
if isinstance(v, str) and len(v) > _MAX_VALUE_LEN:
75+
v = v[: _MAX_VALUE_LEN - 3] + "..."
76+
attrs[k] = v
77+
78+
if not attrs:
79+
return
80+
81+
try:
82+
event_logger.emit(
83+
Event(
84+
name=OP_DETAILS_EVENT,
85+
attributes=attrs,
86+
)
87+
)
88+
except Exception: # pragma: no cover - defensive
89+
pass
90+
91+
92+
def emit_evaluation_result_event(
93+
event_logger,
94+
*,
95+
name: str,
96+
score_value: Optional[float] = None,
97+
score_label: Optional[str] = None,
98+
explanation: Optional[str] = None,
99+
response_id: Optional[str] = None,
100+
error_type: Optional[str] = None,
101+
) -> None:
102+
"""Emit evaluation result event.
103+
104+
Args:
105+
event_logger: The event logger instance
106+
name: Name of the evaluation metric
107+
score_value: Numeric score value
108+
score_label: Label for the score (e.g., "good", "bad")
109+
explanation: Explanation of the evaluation
110+
response_id: ID of the response being evaluated
111+
error_type: Type of error if evaluation failed
112+
"""
113+
if not event_logger:
114+
return
115+
if not _enabled(_EVAL_RESULTS_ENABLED_ENV, True):
116+
return
117+
118+
attrs: Dict[str, Any] = {"gen_ai.evaluation.name": name}
119+
if score_value is not None:
120+
attrs["gen_ai.evaluation.score.value"] = float(score_value)
121+
if score_label is not None:
122+
attrs["gen_ai.evaluation.score.label"] = score_label
123+
if explanation:
124+
attrs["gen_ai.evaluation.explanation"] = (
125+
explanation[:4000] if len(explanation) > 4000 else explanation
126+
)
127+
if response_id:
128+
attrs["gen_ai.response.id"] = response_id
129+
if error_type:
130+
attrs["error.type"] = error_type
131+
132+
try:
133+
event_logger.emit(
134+
Event(
135+
name=EVAL_RESULT_EVENT,
136+
attributes=attrs,
137+
)
138+
)
139+
except Exception: # pragma: no cover - defensive
140+
pass

0 commit comments

Comments
 (0)