Skip to content

Commit 369f8dd

Browse files
committed
Add preliminary AI analytics SDK
1 parent 5d7c4a7 commit 369f8dd

File tree

4 files changed

+92
-26
lines changed

4 files changed

+92
-26
lines changed

sentry_sdk/ai_analytics.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
from functools import wraps
2+
3+
from sentry_sdk import start_span
4+
from sentry_sdk.tracing import Span
5+
from sentry_sdk.utils import ContextVar
6+
from sentry_sdk._types import TYPE_CHECKING
7+
8+
if TYPE_CHECKING:
9+
from typing import Optional, Callable, Any
10+
11+
_ai_pipeline_name = ContextVar("ai_pipeline_name", default=None)
12+
13+
14+
def set_ai_pipeline_name(name):
15+
# type: (Optional[str]) -> None
16+
_ai_pipeline_name.set(name)
17+
18+
19+
def get_ai_pipeline_name():
20+
# type: () -> Optional[str]
21+
return _ai_pipeline_name.get()
22+
23+
24+
def ai_pipeline(description, op="ai.pipeline", **span_kwargs):
25+
# type: (str, str, Any) -> Callable[..., Any]
26+
def decorator(f):
27+
# type: (Callable[..., Any]) -> Callable[..., Any]
28+
@wraps(f)
29+
def wrapped(*args, **kwargs):
30+
# type: (Any, Any) -> Any
31+
with start_span(description=description, op=op, **span_kwargs):
32+
_ai_pipeline_name.set(description)
33+
res = f(*args, **kwargs)
34+
_ai_pipeline_name.set(None)
35+
return res
36+
37+
return wrapped
38+
39+
return decorator
40+
41+
42+
def ai_run(description, op="ai.run", **span_kwargs):
43+
# type: (str, str, Any) -> Callable[..., Any]
44+
def decorator(f):
45+
# type: (Callable[..., Any]) -> Callable[..., Any]
46+
@wraps(f)
47+
def wrapped(*args, **kwargs):
48+
# type: (Any, Any) -> Any
49+
with start_span(description=description, op=op, **span_kwargs) as span:
50+
curr_pipeline = _ai_pipeline_name.get()
51+
if curr_pipeline:
52+
span.set_data("ai.pipeline.name", curr_pipeline)
53+
return f(*args, **kwargs)
54+
55+
return wrapped
56+
57+
return decorator
58+
59+
60+
def record_token_usage(
61+
span, prompt_tokens=None, completion_tokens=None, total_tokens=None
62+
):
63+
# type: (Span, Optional[int], Optional[int], Optional[int]) -> None
64+
ai_pipeline_name = get_ai_pipeline_name()
65+
if ai_pipeline_name:
66+
span.set_data("ai.pipeline.name", ai_pipeline_name)
67+
if prompt_tokens is not None:
68+
span.set_measurement("ai_prompt_tokens_used", value=prompt_tokens)
69+
if completion_tokens is not None:
70+
span.set_measurement("ai_completion_tokens_used", value=completion_tokens)
71+
if (
72+
total_tokens is None
73+
and prompt_tokens is not None
74+
and completion_tokens is not None
75+
):
76+
total_tokens = prompt_tokens + completion_tokens
77+
if total_tokens is not None:
78+
span.set_measurement("ai_total_tokens_used", total_tokens)

sentry_sdk/integrations/_ai_common.py

Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from sentry_sdk._types import TYPE_CHECKING
22

33
if TYPE_CHECKING:
4-
from typing import Any, Optional
4+
from typing import Any
55

66
from sentry_sdk.tracing import Span
77
from sentry_sdk.utils import logger
@@ -30,21 +30,3 @@ def set_data_normalized(span, key, value):
3030
# type: (Span, str, Any) -> None
3131
normalized = _normalize_data(value)
3232
span.set_data(key, normalized)
33-
34-
35-
def record_token_usage(
36-
span, prompt_tokens=None, completion_tokens=None, total_tokens=None
37-
):
38-
# type: (Span, Optional[int], Optional[int], Optional[int]) -> None
39-
if prompt_tokens is not None:
40-
span.set_measurement("ai_prompt_tokens_used", value=prompt_tokens)
41-
if completion_tokens is not None:
42-
span.set_measurement("ai_completion_tokens_used", value=completion_tokens)
43-
if (
44-
total_tokens is None
45-
and prompt_tokens is not None
46-
and completion_tokens is not None
47-
):
48-
total_tokens = prompt_tokens + completion_tokens
49-
if total_tokens is not None:
50-
span.set_measurement("ai_total_tokens_used", total_tokens)

sentry_sdk/integrations/langchain.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@
33

44
import sentry_sdk
55
from sentry_sdk._types import TYPE_CHECKING
6+
from sentry_sdk.ai_analytics import set_ai_pipeline_name, record_token_usage
67
from sentry_sdk.consts import OP, SPANDATA
7-
from sentry_sdk.integrations._ai_common import set_data_normalized, record_token_usage
8+
from sentry_sdk.integrations._ai_common import set_data_normalized
89
from sentry_sdk.scope import should_send_default_pii
910
from sentry_sdk.tracing import Span
1011

@@ -88,6 +89,7 @@ class WatchedSpan:
8889
num_prompt_tokens = 0 # type: int
8990
no_collect_tokens = False # type: bool
9091
children = [] # type: List[WatchedSpan]
92+
is_pipeline = False # type: bool
9193

9294
def __init__(self, span):
9395
# type: (Span) -> None
@@ -134,9 +136,6 @@ def _normalize_langchain_message(self, message):
134136
def _create_span(self, run_id, parent_id, **kwargs):
135137
# type: (SentryLangchainCallback, UUID, Optional[Any], Any) -> WatchedSpan
136138

137-
if "origin" not in kwargs:
138-
kwargs["origin"] = "auto.ai.langchain"
139-
140139
watched_span = None # type: Optional[WatchedSpan]
141140
if parent_id:
142141
parent_span = self.span_map[parent_id] # type: Optional[WatchedSpan]
@@ -146,6 +145,11 @@ def _create_span(self, run_id, parent_id, **kwargs):
146145
if watched_span is None:
147146
watched_span = WatchedSpan(sentry_sdk.start_span(**kwargs))
148147

148+
if kwargs.get("op", "").startswith("ai.pipeline."):
149+
if kwargs.get("description"):
150+
set_ai_pipeline_name(kwargs.get("description"))
151+
watched_span.is_pipeline = True
152+
149153
watched_span.span.__enter__()
150154
self.span_map[run_id] = watched_span
151155
self.gc_span_map()
@@ -154,6 +158,9 @@ def _create_span(self, run_id, parent_id, **kwargs):
154158
def _exit_span(self, span_data, run_id):
155159
# type: (SentryLangchainCallback, WatchedSpan, UUID) -> None
156160

161+
if span_data.is_pipeline:
162+
set_ai_pipeline_name(None)
163+
157164
span_data.span.__exit__(None, None, None)
158165
del self.span_map[run_id]
159166

sentry_sdk/integrations/openai.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22

33
from sentry_sdk import consts
44
from sentry_sdk._types import TYPE_CHECKING
5+
from sentry_sdk.ai_analytics import record_token_usage
56
from sentry_sdk.consts import SPANDATA
6-
from sentry_sdk.integrations._ai_common import set_data_normalized, record_token_usage
7+
from sentry_sdk.integrations._ai_common import set_data_normalized
78

89
if TYPE_CHECKING:
910
from typing import Any, Iterable, List, Optional, Callable, Iterator
@@ -141,7 +142,6 @@ def new_chat_completion(*args, **kwargs):
141142

142143
span = sentry_sdk.start_span(
143144
op=consts.OP.OPENAI_CHAT_COMPLETIONS_CREATE,
144-
origin="auto.ai.openai",
145145
description="Chat Completion",
146146
)
147147
span.__enter__()
@@ -225,7 +225,6 @@ def new_embeddings_create(*args, **kwargs):
225225
# type: (*Any, **Any) -> Any
226226
with sentry_sdk.start_span(
227227
op=consts.OP.OPENAI_EMBEDDINGS_CREATE,
228-
origin="auto.ai.openai",
229228
description="OpenAI Embedding Creation",
230229
) as span:
231230
integration = sentry_sdk.get_client().get_integration(OpenAIIntegration)

0 commit comments

Comments
 (0)