Skip to content
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
73 commits
Select commit Hold shift + click to select a range
b248d24
Fix: Improve serialization of completions/responses in Agents SDK ins…
devin-ai-integration[bot] Mar 14, 2025
30eb11e
Fix: Improve serialization of completions/responses in Agents SDK ins…
devin-ai-integration[bot] Mar 14, 2025
d6c2f8a
Tests for completions.
tcdent Mar 15, 2025
9283b83
Separate OpenAI tests into `completion` and `responses`
tcdent Mar 15, 2025
770b37a
Refactor completions and responses unit tests.
tcdent Mar 15, 2025
29a115f
agents SDK test using semantic conventions.
tcdent Mar 15, 2025
a67deb7
semantic conventions in openai completions and responses tests
tcdent Mar 15, 2025
6f1e77a
Exporter refactor and generalization. standardization and simplificat…
tcdent Mar 15, 2025
5b4e940
Continued refactor of Agents instrumentor. Usurp third-party implemen…
tcdent Mar 15, 2025
0169502
Semantic conventions for messages.
tcdent Mar 15, 2025
960a01f
Tools for generating real test data from OpenAI Agents.
tcdent Mar 15, 2025
124a469
support tool calls and set of responses. missing import
tcdent Mar 15, 2025
ce5b122
reasoning tokens, semantic conventions, and implementation in OpenAI …
tcdent Mar 15, 2025
039978b
populate agents SDK tests with fixture data. Simplify fixture data ge…
tcdent Mar 15, 2025
1fa5fb6
Add chat completion support to openai_agents. Cleanup OpenAI agents i…
tcdent Mar 15, 2025
72ab339
Agents instrumentor cleanup.
tcdent Mar 15, 2025
d206b67
Cleanup.
tcdent Mar 15, 2025
4661fa5
Cleanup init.
tcdent Mar 15, 2025
e44a509
absolute import.
tcdent Mar 15, 2025
cf73879
Merge branch 'main' into serialization-fix-test
dot-agi Mar 15, 2025
913d18b
fix breaking error.
tcdent Mar 15, 2025
d5ac88d
Correct naming
tcdent Mar 15, 2025
734b15d
rename
tcdent Mar 15, 2025
9e8c845
Refactor completions to always use semantic conventions.
tcdent Mar 15, 2025
c6e9bff
More robust output
tcdent Mar 15, 2025
4c17725
use openai_agents tracing api to gather span data.
tcdent Mar 16, 2025
1e140cf
Agents associates spans with a parent span and exports.
tcdent Mar 16, 2025
6d268ec
OpenAi responses instrumentor.
tcdent Mar 16, 2025
cccfef8
Merge branch 'main' into serialization-fix-test
dot-agi Mar 16, 2025
91fea4f
Delete examples/agents-examples/basic/hello_world.py
tcdent Mar 16, 2025
8c9ec5c
pass strings to serialize and return them early.
tcdent Mar 16, 2025
11cc97d
deduplication and better hierarchy. simplification of tests. separati…
tcdent Mar 16, 2025
f01d6dd
Notes and working documents that should not make it into main.
tcdent Mar 16, 2025
1ac8077
Merge main into serialization-fix-test-drafts
tcdent Mar 18, 2025
59a4fc7
more descriptive debug messaging in OpenAI Agents instrumentor
tcdent Mar 18, 2025
1ad9fd7
pertinent testing information in claude.md.
tcdent Mar 18, 2025
c4cb26e
better version determination for the library.
tcdent Mar 18, 2025
c60e29a
Test for generation tokens as well.
tcdent Mar 18, 2025
1d2e4f7
Cleanup attribute formatting to use modular function format with spec…
tcdent Mar 19, 2025
32d7e88
Remove duplicated model export from processor.
tcdent Mar 20, 2025
4256384
nest all spans under the parent_trace root span and open and close th…
tcdent Mar 20, 2025
016172a
clean up common attributes parsing helpers.
tcdent Mar 20, 2025
be9448a
Simplify processor.
tcdent Mar 20, 2025
60392a0
Cleanup exporter.
tcdent Mar 20, 2025
99cd3c5
Cleanup instrumentor
tcdent Mar 20, 2025
62f3bf5
Cleanup attributes
tcdent Mar 20, 2025
8f0f44d
Update README and SPANS definition. Add example with tool usage.
tcdent Mar 20, 2025
cd9954d
Fix tool usage example.
tcdent Mar 21, 2025
d4fe0e8
Get completion data on outputs.
tcdent Mar 21, 2025
8bee74e
Delete notes
tcdent Mar 21, 2025
830f504
Fix tests for attributes. Rewmove debug statements.
tcdent Mar 21, 2025
9bfda9f
Implement tests for OpenAi agents.
tcdent Mar 21, 2025
bd30017
Merge branch 'main' into serialization-fix-test
dot-agi Mar 21, 2025
14c9837
Better naming for spans.
tcdent Mar 21, 2025
1465821
Openai Response type parsing improvements.
tcdent Mar 21, 2025
89d9683
Cleanup exporter imports and naming.
tcdent Mar 21, 2025
9f13810
Handoff agent example.
tcdent Mar 21, 2025
c98bbbf
Cleanup imports on common.
tcdent Mar 21, 2025
6afe3fc
Disable openai completions/responses tests. TODO probably delete these.
tcdent Mar 21, 2025
f325529
Disable openai responses intrumentor; it is handled inside openai_age…
tcdent Mar 21, 2025
7fb5725
Add note about enabling chat.completions api instead of responses.
tcdent Mar 21, 2025
80e30e8
Move exporter convention notes to README
tcdent Mar 21, 2025
3f1a793
Update tests.
tcdent Mar 21, 2025
314cb88
Disable openai responses instrumentation test.
tcdent Mar 21, 2025
528e5b3
Skip `parse` serialization tests.
tcdent Mar 21, 2025
bb71461
Cleanup openai responses instrumention and tests; will be included in…
tcdent Mar 24, 2025
9e3208f
Resolve type checking errors.
tcdent Mar 24, 2025
c91d78c
get correct library version
dot-agi Mar 24, 2025
7c29e96
remove debug statements and import LIBRARY_VERSION
dot-agi Mar 24, 2025
ef7dc3e
Merge branch 'main' into serialization-fix-test
tcdent Mar 24, 2025
cc14de9
Log deeplink to trace on AgentOps dashboard. (#879)
tcdent Mar 24, 2025
be94e41
Merge branch 'main' into serialization-fix-test
tcdent Mar 24, 2025
c8a6531
Merge branch 'main' into serialization-fix-test
dot-agi Mar 24, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 50 additions & 1 deletion agentops/helpers/serialization.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,57 @@ def serialize_uuid(obj: UUID) -> str:
return str(obj)


def model_to_dict(obj: Any) -> dict:
"""Convert a model object to a dictionary safely.

Handles various model types including:
- Pydantic models (model_dump/dict methods)
- Dictionary-like objects
- API response objects with parse method
- Objects with __dict__ attribute

Args:
obj: The model object to convert to dictionary

Returns:
Dictionary representation of the object, or empty dict if conversion fails
"""
if obj is None:
return {}
if isinstance(obj, dict):
return obj
if hasattr(obj, "model_dump"): # Pydantic v2
return obj.model_dump()
elif hasattr(obj, "dict"): # Pydantic v1
return obj.dict()
elif hasattr(obj, "parse"): # Raw API response
return model_to_dict(obj.parse())
else:
# Try to use __dict__ as fallback
try:
return obj.__dict__
except:
return {}


def safe_serialize(obj: Any) -> Any:
"""Safely serialize an object to JSON-compatible format"""
"""Safely serialize an object to JSON-compatible format

This function handles complex objects by:
1. Converting models to dictionaries
2. Using custom JSON encoder to handle special types
3. Falling back to string representation only when necessary

Args:
obj: The object to serialize

Returns:
JSON string representation of the object
"""
# First convert any model objects to dictionaries
if hasattr(obj, "model_dump") or hasattr(obj, "dict") or hasattr(obj, "parse"):
obj = model_to_dict(obj)

try:
return json.dumps(obj, cls=AgentOpsJSONEncoder)
except (TypeError, ValueError) as e:
Expand Down
2 changes: 1 addition & 1 deletion agentops/instrumentation/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ def get_instance(self) -> BaseInstrumentor:
provider_import_name="crewai",
),
InstrumentorLoader(
module_name="opentelemetry.instrumentation.agents",
module_name="agentops.instrumentation.openai_agents",
class_name="AgentsInstrumentor",
provider_import_name="agents",
),
Expand Down
116 changes: 116 additions & 0 deletions agentops/instrumentation/openai/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
"""
AgentOps instrumentation utilities for OpenAI

This module provides shared utilities for instrumenting various OpenAI products and APIs.
It centralizes common functions and behaviors to ensure consistent instrumentation
across all OpenAI-related components.

IMPORTANT DISTINCTION BETWEEN OPENAI API FORMATS:
1. OpenAI Completions API - The traditional API format using prompt_tokens/completion_tokens
2. OpenAI Response API - The newer format used by the Agents SDK using input_tokens/output_tokens
3. Agents SDK - The framework that uses Response API format

This module implements utilities that handle both formats consistently.
"""

import logging
from typing import Any, Dict, List, Optional, Union

# Import span attributes from semconv
from agentops.semconv import SpanAttributes

# Logger
logger = logging.getLogger(__name__)

def get_value(data: Dict[str, Any], keys: Union[str, List[str]]) -> Optional[Any]:
"""
Get a value from a dictionary using a key or prioritized list of keys.

Args:
data: Source dictionary
keys: A single key or list of keys in priority order

Returns:
The value if found, or None if not found
"""
if isinstance(keys, str):
return data.get(keys)

for key in keys:
if key in data:
return data[key]

return None

def process_token_usage(usage: Dict[str, Any], attributes: Dict[str, Any]) -> None:
"""
Process token usage metrics from any OpenAI API response and add them to span attributes.

This function maps token usage fields from various API formats to standardized
attribute names according to OpenTelemetry semantic conventions:

- OpenAI ChatCompletion API uses: prompt_tokens, completion_tokens, total_tokens
- OpenAI Response API uses: input_tokens, output_tokens, total_tokens

Both formats are mapped to the standardized OTel attributes.

Args:
usage: Dictionary containing token usage metrics from an OpenAI API
attributes: The span attributes dictionary where the metrics will be added
"""
if not usage or not isinstance(usage, dict):
return

# Define mapping for standard usage metrics (target → source)
token_mapping = {
# Standard tokens mapping (target attribute → source field)
SpanAttributes.LLM_USAGE_TOTAL_TOKENS: "total_tokens",
SpanAttributes.LLM_USAGE_PROMPT_TOKENS: ["prompt_tokens", "input_tokens"],
SpanAttributes.LLM_USAGE_COMPLETION_TOKENS: ["completion_tokens", "output_tokens"],
}

# Apply the mapping for all token usage fields
for target_attr, source_keys in token_mapping.items():
value = get_value(usage, source_keys)
if value is not None:
attributes[target_attr] = value

# Process output_tokens_details if present
if "output_tokens_details" in usage and isinstance(usage["output_tokens_details"], dict):
process_token_details(usage["output_tokens_details"], attributes)


def process_token_details(details: Dict[str, Any], attributes: Dict[str, Any]) -> None:
"""
Process detailed token metrics from OpenAI API responses and add them to span attributes.

This function maps token detail fields (like reasoning_tokens) to standardized attribute names
according to semantic conventions, ensuring consistent telemetry across the system.

Args:
details: Dictionary containing token detail metrics from an OpenAI API
attributes: The span attributes dictionary where the metrics will be added
"""
if not details or not isinstance(details, dict):
return

# Token details attribute mapping for standardized token metrics
# Maps standardized attribute names to API-specific token detail keys (target → source)
token_details_mapping = {
f"{SpanAttributes.LLM_USAGE_TOTAL_TOKENS}.reasoning": "reasoning_tokens",
# Add more mappings here as OpenAI introduces new token detail types
}

# Process all token detail fields
for detail_key, detail_value in details.items():
# First check if there's a mapping for this key
mapped = False
for target_attr, source_key in token_details_mapping.items():
if source_key == detail_key:
attributes[target_attr] = detail_value
mapped = True
break

# For unknown token details, use generic naming format
if not mapped:
attributes[f"{SpanAttributes.LLM_USAGE_TOTAL_TOKENS}.{detail_key}"] = detail_value
126 changes: 126 additions & 0 deletions agentops/instrumentation/openai_agents/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
# OpenAI Agents SDK Instrumentation

This module provides automatic instrumentation for the OpenAI Agents SDK, adding telemetry that follows OpenTelemetry semantic conventions for Generative AI systems.

## Architecture Overview

The OpenAI Agents SDK instrumentor works by:

1. Intercepting the Agents SDK's trace processor interface to capture Agent, Function, Generation, and other span types
2. Monkey-patching the Agents SDK `Runner` class to capture the full execution lifecycle, including streaming operations
3. Converting all captured data to OpenTelemetry spans and metrics following semantic conventions

## Span Types

The instrumentor captures the following span types:

- **Trace**: The root span representing an entire agent workflow execution
- Implementation: `_export_trace()` method in `exporter.py`
- Creates a span with the trace name, ID, and workflow metadata

- **Agent**: Represents an agent's execution lifecycle
- Implementation: `_process_agent_span()` method in `exporter.py`
- Uses `SpanKind.CONSUMER` to indicate an agent receiving a request
- Captures agent name, input, output, tools, and other metadata

- **Function**: Represents a tool/function call
- Implementation: `_process_function_span()` method in `exporter.py`
- Uses `SpanKind.CLIENT` to indicate an outbound call to a function
- Captures function name, input arguments, output results, and error information

- **Generation**: Captures details of model generation
- Implementation: `_process_generation_span()` method in `exporter.py`
- Uses `SpanKind.CLIENT` to indicate an outbound call to an LLM
- Captures model name, configuration, usage statistics, and response content

- **Response**: Lightweight span for tracking model response IDs
- Implementation: Handled within `_process_response_api()` and `_process_completions()` methods
- Extracts response IDs and metadata from both Chat Completion API and Response API formats

- **Handoff**: Represents control transfer between agents
- Implementation: Captured through the `AgentAttributes.HANDOFFS` attribute
- Maps from the Agents SDK's "handoffs" field to standardized attribute name

## Metrics

The instrumentor collects the following metrics:

- **Agent Runs**: Number of agent runs
- Implementation: `_agent_run_counter` in `instrumentor.py`
- Incremented at the start of each agent run with metadata about the agent and run configuration

- **Agent Turns**: Number of agent turns
- Implementation: Inferred from raw responses processing
- Each raw response represents a turn in the conversation

- **Agent Execution Time**: Time taken for agent execution
- Implementation: `_agent_execution_time_histogram` in `instrumentor.py`
- Measured from the start of an agent run to its completion

- **Token Usage**: Number of input and output tokens used
- Implementation: `_agent_token_usage_histogram` in `instrumentor.py`
- Records both prompt and completion tokens separately with appropriate labels

## Key Design Patterns

### Target → Source Mapping Pattern

We use a consistent pattern for attribute mapping where dictionary keys represent the target attribute names (what we want in the final span), and values represent the source field names (where the data comes from):

```python
_CONFIG_MAPPING = {
# Target semantic convention → source field
<SemanticConvention>: Union[str, list[str]],
# ...
}
```

This pattern makes it easy to maintain mappings and apply them consistently.

### Multi-API Format Support

The instrumentor handles both OpenAI API formats:

1. **Chat Completion API**: Traditional format with "choices" array and prompt_tokens/completion_tokens
2. **Response API**: Newer format with "output" array and input_tokens/output_tokens

The implementation intelligently detects which format is being used and processes accordingly.


### Streaming Operation Tracking

When instrumenting streaming operations, we:

1. Track active streaming operations using unique IDs
2. Handle proper flushing of spans to ensure metrics are recorded
3. Create separate spans for token usage metrics to avoid premature span closure

### Response API Content Extraction

The Response API has a nested structure for content:

```
output → message → content → [items] → text
```

Extracting the actual text requires special handling:

```python
# From _process_response_api in exporter.py
if isinstance(content_items, list):
# Combine text from all text items
texts = []
for content_item in content_items:
if content_item.get("type") == "output_text" and "text" in content_item:
texts.append(content_item["text"])

# Join texts (even if empty)
attributes[f"{prefix}.content"] = " ".join(texts)
```


## TODO
- Add support for additional semantic conventions
- `gen_ai` doesn't have conventions for response data beyond `role` and `content`
- We're shoehorning `responses` into `completions` since the spec doesn't
have a convention in place for this yet.
39 changes: 39 additions & 0 deletions agentops/instrumentation/openai_agents/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
"""
AgentOps Instrumentor for OpenAI Agents SDK

This module provides automatic instrumentation for the OpenAI Agents SDK when AgentOps is imported.
It implements a clean, maintainable implementation that follows semantic conventions.

IMPORTANT DISTINCTION BETWEEN OPENAI API FORMATS:
1. OpenAI Completions API - The traditional API format using prompt_tokens/completion_tokens
2. OpenAI Response API - The newer format used by the Agents SDK using input_tokens/output_tokens
3. Agents SDK - The framework that uses Response API format

The Agents SDK uses the Response API format, which we handle using shared utilities from
agentops.instrumentation.openai.
"""
from typing import Optional
import importlib.metadata
from agentops.logging import logger

def get_version():
"""Get the version of the agents SDK, or 'unknown' if not found"""
try:
installed_version = importlib.metadata.version("agents")
return installed_version
except importlib.metadata.PackageNotFoundError:
logger.debug("`agents` package not found; unable to determine installed version.")
return None

LIBRARY_NAME = "agents-sdk"
LIBRARY_VERSION: Optional[str] = get_version() # Actual OpenAI Agents SDK version

# Import after defining constants to avoid circular imports
from .instrumentor import AgentsInstrumentor

__all__ = [
"LIBRARY_NAME",
"LIBRARY_VERSION",
"SDK_VERSION",
"AgentsInstrumentor",
]
Loading
Loading