Skip to content

Commit

Permalink
feat: Anthropic instrumentation (#71)
Browse files Browse the repository at this point in the history
  • Loading branch information
nirga authored Sep 26, 2023
1 parent e927618 commit 029e375
Show file tree
Hide file tree
Showing 9 changed files with 506 additions and 30 deletions.
1 change: 1 addition & 0 deletions .github/workflows/ci-traceloop-sdk.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ jobs:
- name: Test with pytest
env:
OPENAI_API_KEY: ${{ secrets.OPEN_API_KEY }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
PINECONE_API_KEY: ${{ secrets.PINECONE_API_KEY }}
PINECONE_ENVIRONMENT: ${{ secrets.PINECONE_ENVIRONMENT }}
TRACELOOP_PROMPT_REGISTRY_ENABLED: false
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ OpenLLMetry can instrument everything that [OpenTelemetry already instruments](h
### LLM Providers

- [x] OpenAI / Azure OpenAI
- [ ] Anthropic
- [x] Anthropic
- [ ] Cohere
- [ ] Replicate
- [ ] HuggingFace
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1,150 @@
"""OpenTelemetry Anthropic instrumentation"""
import logging
from typing import Collection
from wrapt import wrap_function_wrapper

from opentelemetry import context as context_api
from opentelemetry.trace import get_tracer, SpanKind
from opentelemetry.trace.status import Status, StatusCode

from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
from opentelemetry.instrumentation.utils import (
_SUPPRESS_INSTRUMENTATION_KEY,
unwrap,
)

from opentelemetry.semconv.ai import SpanAttributes, LLMRequestTypeValues

logger = logging.getLogger(__name__)

_instruments = ("anthropic >= 0.3.11",)
__version__ = "0.1.0"

WRAPPED_METHODS = [
{
"object": "Completions",
"method": "create",
"span_name": "anthropic.completion",
},
]


def _set_span_attribute(span, name, value):
if value is not None:
if value != "":
span.set_attribute(name, value)
return


def _set_input_attributes(span, kwargs):
_set_span_attribute(span, SpanAttributes.LLM_REQUEST_MODEL, kwargs.get("model"))
_set_span_attribute(
span, SpanAttributes.LLM_REQUEST_MAX_TOKENS, kwargs.get("max_tokens_to_sample")
)
_set_span_attribute(span, SpanAttributes.LLM_TEMPERATURE, kwargs.get("temperature"))
_set_span_attribute(span, SpanAttributes.LLM_TOP_P, kwargs.get("top_p"))
_set_span_attribute(
span, SpanAttributes.LLM_FREQUENCY_PENALTY, kwargs.get("frequency_penalty")
)
_set_span_attribute(
span, SpanAttributes.LLM_PRESENCE_PENALTY, kwargs.get("presence_penalty")
)

_set_span_attribute(
span, f"{SpanAttributes.LLM_PROMPTS}.0.user", kwargs.get("prompt")
)

return


def _set_span_completions(span, llm_request_type, completion):
index = 0
prefix = f"{SpanAttributes.LLM_COMPLETIONS}.{index}"
_set_span_attribute(span, f"{prefix}.finish_reason", completion.get("stop_reason"))
_set_span_attribute(span, f"{prefix}.content", completion.get("completion"))


def _set_response_attributes(span, llm_request_type, response):
_set_span_attribute(span, SpanAttributes.LLM_RESPONSE_MODEL, response.get("model"))
_set_span_completions(span, llm_request_type, response)


def _with_tracer_wrapper(func):
"""Helper for providing tracer for wrapper functions."""

def _with_tracer(tracer, to_wrap):
def wrapper(wrapped, instance, args, kwargs):
return func(tracer, to_wrap, wrapped, instance, args, kwargs)

return wrapper

return _with_tracer


@_with_tracer_wrapper
def _wrap(tracer, to_wrap, wrapped, instance, args, kwargs):
"""Instruments and calls every function defined in TO_WRAP."""
if context_api.get_value(_SUPPRESS_INSTRUMENTATION_KEY):
return wrapped(*args, **kwargs)

name = to_wrap.get("span_name")
with tracer.start_as_current_span(
name,
kind=SpanKind.CLIENT,
attributes={
SpanAttributes.LLM_VENDOR: "Anthropic",
SpanAttributes.LLM_REQUEST_TYPE: LLMRequestTypeValues.COMPLETION,
},
) as span:
try:
if span.is_recording():
_set_input_attributes(span, kwargs)

except Exception as ex: # pylint: disable=broad-except
logger.warning(
"Failed to set input attributes for anthropic span, error: %s", str(ex)
)

response = wrapped(*args, **kwargs)

if response:
try:
if span.is_recording():
_set_response_attributes(span, response)

except Exception as ex: # pylint: disable=broad-except
logger.warning(
"Failed to set response attributes for openai span, error: %s",
str(ex),
)
if span.is_recording():
span.set_status(Status(StatusCode.OK))

return response


class AnthropicInstrumentor(BaseInstrumentor):
"""An instrumentor for Anthropic's client library."""

def instrumentation_dependencies(self) -> Collection[str]:
return _instruments

def _instrument(self, **kwargs):
tracer_provider = kwargs.get("tracer_provider")
tracer = get_tracer(__name__, __version__, tracer_provider)
for wrapped_method in WRAPPED_METHODS:
wrap_object = wrapped_method.get("object")
wrap_method = wrapped_method.get("method")
wrap_function_wrapper(
"anthropic.resources.completions",
f"{wrap_object}.{wrap_method}",
_wrap(tracer, wrapped_method),
)

def _uninstrument(self, **kwargs):
for wrapped_method in WRAPPED_METHODS:
wrap_object = wrapped_method.get("object")
unwrap(
f"anthropic.resources.completions.{wrap_object}",
wrapped_method.get("method"),
)
28 changes: 19 additions & 9 deletions packages/opentelemetry-instrumentation-anthropic/poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ addopts = "--cov --cov-report html:'../../coverage/packages/opentelemetry-instru

[tool.poetry]
name = "opentelemetry-instrumentation-anthropic"
version = "0.0.1"
version = "0.0.4"
description = "OpenTelemetry Anthropic instrumentation"
authors = [
"Gal Kleinman <gal@traceloop.com>",
Expand All @@ -26,9 +26,9 @@ include = "opentelemetry/instrumentation/anthropic"

[tool.poetry.dependencies]
python = ">=3.8.1,<3.12"
opentelemetry-api = "^1.20.0"
opentelemetry-api = "^1.19.0"
opentelemetry-instrumentation = "^0.40b0"
opentelemetry-semantic-conventions-llm = "^0.0.1"
opentelemetry-semantic-conventions-ai = "^0.0.3"

[tool.poetry.group.dev.dependencies]
autopep8 = "2.0.4"
Expand Down
Loading

0 comments on commit 029e375

Please sign in to comment.