Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
953305e
Update attribute extraction to support dict as well as object.
tcdent Mar 25, 2025
ab8dad1
Adjust tests to match serialization format of `list[str]`. Patch JSON…
tcdent Mar 25, 2025
fd4b4f8
Instrumentor and wrappers for OpenAI responses.
tcdent Mar 25, 2025
4c6b12f
Collect base usage attributes, too.
tcdent Mar 25, 2025
0ebd74d
Merge branch 'main' into fix-openai-agents-counts
tcdent Mar 25, 2025
5589ec6
Move Response attribute parsing to openai module. Move common attribu…
tcdent Mar 25, 2025
f2dca86
Include tags in parent span. Helpers for accessing global config and …
tcdent Mar 25, 2025
e1e3506
Add tags to an example.
tcdent Mar 25, 2025
d7a9f6f
Remove duplicate library attributes.
tcdent Mar 25, 2025
f30f43a
Pass OpenAI responses objects through our new instrumentor.
tcdent Mar 25, 2025
d98a5b0
Merge branch 'fix-openai-agents-counts' into openai-responses
tcdent Mar 25, 2025
e942f94
Incorporate common attributes, too.
tcdent Mar 26, 2025
9b2c5f7
Add indexed PROMPT semconv to MessageAttributes. Provide reusable wra…
tcdent Mar 26, 2025
d58af58
Type checking.
tcdent Mar 26, 2025
9d2d493
Test coverage for instrumentation.common
tcdent Mar 26, 2025
12b559b
Type in method def should be string in case of missing import.
tcdent Mar 26, 2025
9d77845
Wrap third party module imports from openai in try except block
tcdent Mar 26, 2025
d5cdb57
OpenAI instrumentation tests. (Relocated to openai_core to avoid impo…
tcdent Mar 26, 2025
0df12c2
Merge branch 'main' into openai-responses
tcdent Mar 26, 2025
c430ad9
Merge branch 'openai-responses' into agents-voice
tcdent Mar 27, 2025
fe8932c
OpenAI Agents voice support
tcdent Mar 28, 2025
ef91208
Additional voice-specific fields.
tcdent Mar 31, 2025
2830999
update pyproject.toml and uv.lock to use correct dependency
dot-agi Apr 1, 2025
bebc809
Remove `upload_object` helper.
tcdent Apr 1, 2025
b6cb5dd
Remove tag helpers.
tcdent Apr 1, 2025
869a8af
Move agents example scripts.
tcdent Apr 1, 2025
6427682
Merge branch 'main' into agents-voice
dot-agi Apr 2, 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
11 changes: 11 additions & 0 deletions agentops/client/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from agentops.client.api.base import BaseApiClient
from agentops.client.api.types import AuthTokenResponse
from agentops.client.api.versions.v3 import V3Client
from agentops.client.api.versions.v4 import V4Client

# Define a type variable for client classes
T = TypeVar("T", bound=BaseApiClient)
Expand Down Expand Up @@ -44,6 +45,16 @@ def v3(self) -> V3Client:
"""
return self._get_client("v3", V3Client)

@property
def v4(self) -> V4Client:
"""
Get the V4 API client.

Returns:
The V4 API client
"""
return self._get_client("v4", V4Client)

def _get_client(self, version: str, client_class: Type[T]) -> T:
"""
Get or create a version-specific client.
Expand Down
8 changes: 8 additions & 0 deletions agentops/client/api/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,18 @@
"""

from typing import TypedDict
from pydantic import BaseModel


class AuthTokenResponse(TypedDict):
"""Response from the auth/token endpoint"""

token: str
project_id: str


class UploadedObjectResponse(BaseModel):
"""Response from the v4/objects/upload endpoint"""
url: str
size: int

3 changes: 2 additions & 1 deletion agentops/client/api/versions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@
"""

from agentops.client.api.versions.v3 import V3Client
from agentops.client.api.versions.v4 import V4Client

__all__ = ["V3Client"]
__all__ = ["V3Client", "V4Client"]
71 changes: 71 additions & 0 deletions agentops/client/api/versions/v4.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
"""
V4 API client for the AgentOps API.
This module provides the client for the V4 version of the AgentOps API.
"""
from typing import Optional, Union, Dict

from agentops.client.api.base import BaseApiClient
from agentops.exceptions import ApiServerException
from agentops.client.api.types import UploadedObjectResponse


class V4Client(BaseApiClient):
"""Client for the AgentOps V4 API"""
auth_token: str

def set_auth_token(self, token: str):
"""
Set the authentication token for API requests.
Args:
token: The authentication token to set
"""
self.auth_token = token

def prepare_headers(self, custom_headers: Optional[Dict[str, str]] = None) -> Dict[str, str]:
"""
Prepare headers for API requests.
Args:
custom_headers: Additional headers to include
Returns:
Headers dictionary with standard headers and any custom headers
"""
headers = {

Check warning on line 35 in agentops/client/api/versions/v4.py

View check run for this annotation

Codecov / codecov/patch

agentops/client/api/versions/v4.py#L35

Added line #L35 was not covered by tests
"Authorization": f"Bearer {self.auth_token}",
}
if custom_headers:
headers.update(custom_headers)
return headers

Check warning on line 40 in agentops/client/api/versions/v4.py

View check run for this annotation

Codecov / codecov/patch

agentops/client/api/versions/v4.py#L38-L40

Added lines #L38 - L40 were not covered by tests

def upload_object(self, body: Union[str, bytes]) -> UploadedObjectResponse:
"""
Upload an object to the API and return the response.
Args:
body: The object to upload, either as a string or bytes.
Returns:
UploadedObjectResponse: The response from the API after upload.
"""
if isinstance(body, bytes):
body = body.decode("utf-8")

Check warning on line 52 in agentops/client/api/versions/v4.py

View check run for this annotation

Codecov / codecov/patch

agentops/client/api/versions/v4.py#L51-L52

Added lines #L51 - L52 were not covered by tests

response = self.post("/v4/objects/upload/", body, self.prepare_headers())

Check warning on line 54 in agentops/client/api/versions/v4.py

View check run for this annotation

Codecov / codecov/patch

agentops/client/api/versions/v4.py#L54

Added line #L54 was not covered by tests

if response.status_code != 200:
error_msg = f"Upload failed: {response.status_code}"
try:
error_data = response.json()
if "error" in error_data:
error_msg = error_data["error"]
except Exception:
pass
raise ApiServerException(error_msg)

Check warning on line 64 in agentops/client/api/versions/v4.py

View check run for this annotation

Codecov / codecov/patch

agentops/client/api/versions/v4.py#L56-L64

Added lines #L56 - L64 were not covered by tests

try:
response_data = response.json()
return UploadedObjectResponse(**response_data)
except Exception as e:
raise ApiServerException(f"Failed to process upload response: {str(e)}")

Check warning on line 70 in agentops/client/api/versions/v4.py

View check run for this annotation

Codecov / codecov/patch

agentops/client/api/versions/v4.py#L66-L70

Added lines #L66 - L70 were not covered by tests

3 changes: 3 additions & 0 deletions agentops/client/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ def init(self, **kwargs):
# Prefetch JWT token if enabled
# TODO: Move this validation somewhere else (and integrate with self.config.prefetch_jwt_token once we have a solution to that)
response = self.api.v3.fetch_auth_token(self.config.api_key)

# Save the bearer for use with the v4 API
self.api.v4.set_auth_token(response["token"])

# Initialize TracingCore with the current configuration and project_id
tracing_config = self.config.dict()
Expand Down
1 change: 1 addition & 0 deletions agentops/helpers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,5 @@
"get_env_bool",
"get_env_int",
"get_env_list",
"get_tags_from_config",
]
4 changes: 2 additions & 2 deletions agentops/instrumentation/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ class InstrumentorLoader:
We use the `provider_import_name` to determine if the library is installed i
n the environment.

`modue_name` is the name of the module to import from.
`module_name` is the name of the module to import from.
`class_name` is the name of the class to instantiate from the module.
`provider_import_name` is the name of the package to check for availability.
"""
Expand Down Expand Up @@ -53,7 +53,7 @@ def get_instance(self) -> BaseInstrumentor:

available_instrumentors: list[InstrumentorLoader] = [
InstrumentorLoader(
module_name="opentelemetry.instrumentation.openai",
module_name="agentops.instrumentation.openai",
class_name="OpenAIInstrumentor",
provider_import_name="openai",
),
Expand Down
65 changes: 65 additions & 0 deletions agentops/instrumentation/common/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# AgentOps Instrumentation Common Module

The `agentops.instrumentation.common` module provides shared utilities for OpenTelemetry instrumentation across different LLM service providers.

## Core Components

### Attribute Handler Example

Attribute handlers extract data from method inputs and outputs:

```python
from typing import Optional, Any, Tuple, Dict
from agentops.instrumentation.common.attributes import AttributeMap
from agentops.semconv import SpanAttributes

def my_attribute_handler(args: Optional[Tuple] = None, kwargs: Optional[Dict] = None, return_value: Optional[Any] = None) -> AttributeMap:
attributes = {}

# Extract attributes from kwargs (method inputs)
if kwargs:
if "model" in kwargs:
attributes[SpanAttributes.MODEL_NAME] = kwargs["model"]
# ...

# Extract attributes from return value (method outputs)
if return_value:
if hasattr(return_value, "model"):
attributes[SpanAttributes.LLM_RESPONSE_MODEL] = return_value.model
# ...

return attributes
```

### `WrapConfig` Class

Config object defining how a method should be wrapped:

```python
from agentops.instrumentation.common.wrappers import WrapConfig
from opentelemetry.trace import SpanKind

config = WrapConfig(
trace_name="llm.completion", # Name that will appear in trace spans
package="openai.resources", # Path to the module containing the class
class_name="Completions", # Name of the class containing the method
method_name="create", # Name of the method to wrap
handler=my_attribute_handler, # Function that extracts attributes
span_kind=SpanKind.CLIENT # Type of span to create
)
```

### Wrapping/Unwrapping Methods

```python
from opentelemetry.trace import get_tracer
from agentops.instrumentation.common.wrappers import wrap, unwrap

# Create a tracer and wrap a method
tracer = get_tracer("openai", "0.0.0")
wrap(config, tracer)

# Later, unwrap the method
unwrap(config)
```

6 changes: 6 additions & 0 deletions agentops/instrumentation/common/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from .attributes import AttributeMap, _extract_attributes_from_mapping

__all__ = [
"AttributeMap",
"_extract_attributes_from_mapping",
]
137 changes: 137 additions & 0 deletions agentops/instrumentation/common/attributes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
"""Common attribute processing utilities shared across all instrumentors.

This module provides core utilities for extracting and formatting
OpenTelemetry-compatible attributes from span data. These functions
are provider-agnostic and used by all instrumentors in the AgentOps
package.

The module includes:

1. Helper functions for attribute extraction and mapping
2. Common attribute getters used across all providers
3. Base trace and span attribute functions

All functions follow a consistent pattern:
- Accept span/trace data as input
- Process according to semantic conventions
- Return a dictionary of formatted attributes

These utilities ensure consistent attribute handling across different
LLM service instrumentors while maintaining separation of concerns.
"""
from typing import Dict, Any, Optional, List
from agentops.logging import logger
from agentops.helpers import safe_serialize, get_agentops_version
from agentops.semconv import (
CoreAttributes,
InstrumentationAttributes,
WorkflowAttributes,
)

# target_attribute_key: source_attribute
AttributeMap = Dict[str, Any]


def _extract_attributes_from_mapping(span_data: Any, attribute_mapping: AttributeMap) -> AttributeMap:
"""Helper function to extract attributes based on a mapping.

Args:
span_data: The span data object or dict to extract attributes from
attribute_mapping: Dictionary mapping target attributes to source attributes

Returns:
Dictionary of extracted attributes
"""
attributes = {}
for target_attr, source_attr in attribute_mapping.items():
if hasattr(span_data, source_attr):
# Use getattr to handle properties
value = getattr(span_data, source_attr)
elif isinstance(span_data, dict) and source_attr in span_data:
# Use direct key access for dicts
value = span_data[source_attr]
else:
continue

# Skip if value is None or empty
if value is None or (isinstance(value, (list, dict, str)) and not value):
continue

# Serialize complex objects
elif isinstance(value, (dict, list, object)) and not isinstance(value, (str, int, float, bool)):
value = safe_serialize(value)

attributes[target_attr] = value

return attributes


def get_common_attributes() -> AttributeMap:
"""Get common instrumentation attributes used across traces and spans.

Returns:
Dictionary of common instrumentation attributes
"""
return {
InstrumentationAttributes.NAME: "agentops",
InstrumentationAttributes.VERSION: get_agentops_version(),
}


def get_base_trace_attributes(trace: Any) -> AttributeMap:
"""Create the base attributes dictionary for an OpenTelemetry trace.

Args:
trace: The trace object to extract attributes from

Returns:
Dictionary containing base trace attributes
"""
if not hasattr(trace, 'trace_id'):
logger.warning("Cannot create trace attributes: missing trace_id")
return {}

attributes = {
WorkflowAttributes.WORKFLOW_NAME: trace.name,
CoreAttributes.TRACE_ID: trace.trace_id,
WorkflowAttributes.WORKFLOW_STEP_TYPE: "trace",
**get_common_attributes(),
}

# Add tags from the config to the trace attributes (these should only be added to the trace)
from agentops import get_client

config = get_client().config
tags = []
if config.default_tags:
# `default_tags` can either be a `set` or a `list`
tags = list(config.default_tags)

Check warning on line 108 in agentops/instrumentation/common/attributes.py

View check run for this annotation

Codecov / codecov/patch

agentops/instrumentation/common/attributes.py#L108

Added line #L108 was not covered by tests

attributes[CoreAttributes.TAGS] = tags

return attributes


def get_base_span_attributes(span: Any) -> AttributeMap:
"""Create the base attributes dictionary for an OpenTelemetry span.

Args:
span: The span object to extract attributes from

Returns:
Dictionary containing base span attributes
"""
span_id = getattr(span, 'span_id', 'unknown')
trace_id = getattr(span, 'trace_id', 'unknown')
parent_id = getattr(span, 'parent_id', None)

attributes = {
CoreAttributes.TRACE_ID: trace_id,
CoreAttributes.SPAN_ID: span_id,
**get_common_attributes(),
}

if parent_id:
attributes[CoreAttributes.PARENT_ID] = parent_id

return attributes
Loading
Loading