Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ai): Langchain integration #2911

Merged
merged 42 commits into from Apr 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
985bd27
Add the ability to put measurements on a specific span
colin-sentry Apr 10, 2024
068f0e3
Fix linting issues
colin-sentry Apr 10, 2024
53b89e2
Use typed dict for performance
colin-sentry Apr 10, 2024
16dcf05
Move typed dict into _types
colin-sentry Apr 10, 2024
cb3a237
Add type to measurements in event
colin-sentry Apr 10, 2024
8550914
Fix test
colin-sentry Apr 10, 2024
eb30752
Initial work for langchain integration
colin-sentry Mar 26, 2024
26a9b9a
Add more langchain tox targets
colin-sentry Mar 26, 2024
a491d3f
Add an LRU cache of spans and send things upstream
colin-sentry Mar 26, 2024
442942e
Start writing the tests, PII gates
colin-sentry Mar 27, 2024
f6f27d7
Finish test for langchain
colin-sentry Mar 27, 2024
40f2119
Remove variadic **Any
colin-sentry Mar 27, 2024
a8b6ff0
Fix some type issues
colin-sentry Mar 27, 2024
5d58122
Fix kwargs types?
colin-sentry Mar 27, 2024
fb15ba8
Fix API key in tests
colin-sentry Mar 27, 2024
e5527a0
Require OpenAI for tests
colin-sentry Mar 27, 2024
b393622
Remove langchain 0.0
colin-sentry Mar 28, 2024
959d810
Add exception test
colin-sentry Mar 28, 2024
6ca6bcb
Remove langchain 0.0
colin-sentry Mar 28, 2024
02e0918
Add tiktoken to langchain
colin-sentry Mar 28, 2024
0638f43
Add much more metadata to LLM calls
colin-sentry Mar 28, 2024
2005565
Instrument agents too
colin-sentry Mar 28, 2024
518a44f
Send tags as well
colin-sentry Mar 28, 2024
67e4d95
Send model ids for langchain inference
colin-sentry Mar 28, 2024
2b1db20
import gate
colin-sentry Mar 28, 2024
1752973
fix the bug
colin-sentry Mar 28, 2024
c29600a
Remove empty dimensions from AI fields
colin-sentry Mar 28, 2024
1a8623c
Fix tests for removed dimensions
colin-sentry Mar 28, 2024
51ea2c2
Fix openai tests
colin-sentry Apr 1, 2024
e0f270a
Record metrics for AI tokens used
colin-sentry Apr 3, 2024
f270c87
Fix tests and linting
colin-sentry Apr 3, 2024
58588cf
Fix another test
colin-sentry Apr 3, 2024
64962e0
Add a new opcode for top level langchain runs
colin-sentry Apr 10, 2024
f65bd46
Switch to a metric for total tokens used
colin-sentry Apr 12, 2024
e187911
Fixed langchain test matrix
antonpirker Apr 18, 2024
27db63b
Avoid double counting tokens with explicit blocklist
colin-sentry Apr 19, 2024
5560648
Add preliminary AI analytics SDK
colin-sentry Apr 22, 2024
3295b04
Merge decorators together into "ai_track"
colin-sentry Apr 23, 2024
879e42a
Add exception handling to AI monitoring
colin-sentry Apr 25, 2024
1643bdb
Move stuff around
colin-sentry Apr 26, 2024
b74e52f
Merge branch 'master' into langchain-2.0
antonpirker Apr 30, 2024
d8a8ca7
trigger ci
antonpirker Apr 30, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/workflows/test-integrations-data-processing.yml
Expand Up @@ -58,6 +58,10 @@ jobs:
run: |
set -x # print commands that are executed
./scripts/runtox.sh "py${{ matrix.python-version }}-huey-latest" --cov=tests --cov=sentry_sdk --cov-report= --cov-branch
- name: Test langchain latest
run: |
set -x # print commands that are executed
./scripts/runtox.sh "py${{ matrix.python-version }}-langchain-latest" --cov=tests --cov=sentry_sdk --cov-report= --cov-branch
- name: Test openai latest
run: |
set -x # print commands that are executed
Expand Down Expand Up @@ -114,6 +118,10 @@ jobs:
run: |
set -x # print commands that are executed
./scripts/runtox.sh --exclude-latest "py${{ matrix.python-version }}-huey" --cov=tests --cov=sentry_sdk --cov-report= --cov-branch
- name: Test langchain pinned
run: |
set -x # print commands that are executed
./scripts/runtox.sh --exclude-latest "py${{ matrix.python-version }}-langchain" --cov=tests --cov=sentry_sdk --cov-report= --cov-branch
- name: Test openai pinned
run: |
set -x # print commands that are executed
Expand Down
2 changes: 2 additions & 0 deletions mypy.ini
Expand Up @@ -48,6 +48,8 @@ ignore_missing_imports = True
ignore_missing_imports = True
[mypy-asgiref.*]
ignore_missing_imports = True
[mypy-langchain_core.*]
ignore_missing_imports = True
[mypy-executing.*]
ignore_missing_imports = True
[mypy-asttokens.*]
Expand Down
1 change: 1 addition & 0 deletions scripts/split-tox-gh-actions/split-tox-gh-actions.py
Expand Up @@ -70,6 +70,7 @@
"beam",
"celery",
"huey",
"langchain",
"openai",
"rq",
],
Expand Down
Empty file added sentry_sdk/ai/__init__.py
Empty file.
77 changes: 77 additions & 0 deletions sentry_sdk/ai/monitoring.py
@@ -0,0 +1,77 @@
from functools import wraps

import sentry_sdk.utils
from sentry_sdk import start_span
from sentry_sdk.tracing import Span
from sentry_sdk.utils import ContextVar
from sentry_sdk._types import TYPE_CHECKING

if TYPE_CHECKING:
from typing import Optional, Callable, Any

_ai_pipeline_name = ContextVar("ai_pipeline_name", default=None)


def set_ai_pipeline_name(name):
# type: (Optional[str]) -> None
_ai_pipeline_name.set(name)


def get_ai_pipeline_name():
# type: () -> Optional[str]
return _ai_pipeline_name.get()


def ai_track(description, **span_kwargs):
# type: (str, Any) -> Callable[..., Any]
def decorator(f):
# type: (Callable[..., Any]) -> Callable[..., Any]
@wraps(f)
def wrapped(*args, **kwargs):
# type: (Any, Any) -> Any
curr_pipeline = _ai_pipeline_name.get()
op = span_kwargs.get("op", "ai.run" if curr_pipeline else "ai.pipeline")
with start_span(description=description, op=op, **span_kwargs) as span:
if curr_pipeline:
span.set_data("ai.pipeline.name", curr_pipeline)
return f(*args, **kwargs)
else:
_ai_pipeline_name.set(description)
try:
res = f(*args, **kwargs)
except Exception as e:
event, hint = sentry_sdk.utils.event_from_exception(
e,
client_options=sentry_sdk.get_client().options,
mechanism={"type": "ai_monitoring", "handled": False},
)
sentry_sdk.capture_event(event, hint=hint)
raise e from None
finally:
_ai_pipeline_name.set(None)
return res

return wrapped

return decorator


def record_token_usage(
span, prompt_tokens=None, completion_tokens=None, total_tokens=None
):
# type: (Span, Optional[int], Optional[int], Optional[int]) -> None
ai_pipeline_name = get_ai_pipeline_name()
if ai_pipeline_name:
span.set_data("ai.pipeline.name", ai_pipeline_name)
if prompt_tokens is not None:
span.set_measurement("ai_prompt_tokens_used", value=prompt_tokens)
if completion_tokens is not None:
span.set_measurement("ai_completion_tokens_used", value=completion_tokens)
if (
total_tokens is None
and prompt_tokens is not None
and completion_tokens is not None
):
total_tokens = prompt_tokens + completion_tokens
if total_tokens is not None:
span.set_measurement("ai_total_tokens_used", total_tokens)
32 changes: 32 additions & 0 deletions sentry_sdk/ai/utils.py
@@ -0,0 +1,32 @@
from sentry_sdk._types import TYPE_CHECKING

if TYPE_CHECKING:
from typing import Any

from sentry_sdk.tracing import Span
from sentry_sdk.utils import logger


def _normalize_data(data):
# type: (Any) -> Any

# convert pydantic data (e.g. OpenAI v1+) to json compatible format
if hasattr(data, "model_dump"):
try:
return data.model_dump()
except Exception as e:
logger.warning("Could not convert pydantic data to JSON: %s", e)
return data
if isinstance(data, list):
if len(data) == 1:
return _normalize_data(data[0]) # remove empty dimensions
return list(_normalize_data(x) for x in data)
if isinstance(data, dict):
return {k: _normalize_data(v) for (k, v) in data.items()}
return data


def set_data_normalized(span, key, value):
# type: (Span, str, Any) -> None
normalized = _normalize_data(value)
span.set_data(key, normalized)
84 changes: 84 additions & 0 deletions sentry_sdk/consts.py
Expand Up @@ -91,6 +91,85 @@ class SPANDATA:
See: https://develop.sentry.dev/sdk/performance/span-data-conventions/
"""

AI_INPUT_MESSAGES = "ai.input_messages"
"""
The input messages to an LLM call.
Example: [{"role": "user", "message": "hello"}]
"""

AI_MODEL_ID = "ai.model_id"
"""
The unique descriptor of the model being execugted
Example: gpt-4
"""

AI_METADATA = "ai.metadata"
"""
Extra metadata passed to an AI pipeline step.
Example: {"executed_function": "add_integers"}
"""

AI_TAGS = "ai.tags"
"""
Tags that describe an AI pipeline step.
Example: {"executed_function": "add_integers"}
"""

AI_STREAMING = "ai.streaming"
"""
Whether or not the AI model call's repsonse was streamed back asynchronously
Example: true
"""

AI_TEMPERATURE = "ai.temperature"
"""
For an AI model call, the temperature parameter. Temperature essentially means how random the output will be.
Example: 0.5
"""

AI_TOP_P = "ai.top_p"
"""
For an AI model call, the top_p parameter. Top_p essentially controls how random the output will be.
Example: 0.5
"""

AI_TOP_K = "ai.top_k"
"""
For an AI model call, the top_k parameter. Top_k essentially controls how random the output will be.
Example: 35
"""

AI_FUNCTION_CALL = "ai.function_call"
"""
For an AI model call, the function that was called. This is deprecated for OpenAI, and replaced by tool_calls
"""

AI_TOOL_CALLS = "ai.tool_calls"
"""
For an AI model call, the function that was called. This is deprecated for OpenAI, and replaced by tool_calls
"""

AI_TOOLS = "ai.tools"
"""
For an AI model call, the functions that are available
"""

AI_RESPONSE_FORMAT = "ai.response_format"
"""
For an AI model call, the format of the response
"""

AI_LOGIT_BIAS = "ai.response_format"
"""
For an AI model call, the logit bias
"""

AI_RESPONSES = "ai.responses"
"""
The responses to an AI model call. Always as a list.
Example: ["hello", "world"]
"""

DB_NAME = "db.name"
"""
The name of the database being accessed. For commands that switch the database, this should be set to the target database (even if the command fails).
Expand Down Expand Up @@ -245,6 +324,11 @@ class OP:
MIDDLEWARE_STARLITE_SEND = "middleware.starlite.send"
OPENAI_CHAT_COMPLETIONS_CREATE = "ai.chat_completions.create.openai"
OPENAI_EMBEDDINGS_CREATE = "ai.embeddings.create.openai"
LANGCHAIN_PIPELINE = "ai.pipeline.langchain"
LANGCHAIN_RUN = "ai.run.langchain"
LANGCHAIN_TOOL = "ai.tool.langchain"
LANGCHAIN_AGENT = "ai.agent.langchain"
LANGCHAIN_CHAT_COMPLETIONS_CREATE = "ai.chat_completions.create.langchain"
QUEUE_SUBMIT_ARQ = "queue.submit.arq"
QUEUE_TASK_ARQ = "queue.task.arq"
QUEUE_SUBMIT_CELERY = "queue.submit.celery"
Expand Down
1 change: 1 addition & 0 deletions sentry_sdk/integrations/__init__.py
Expand Up @@ -85,6 +85,7 @@ def iter_default_integrations(with_auto_enabling_integrations):
"sentry_sdk.integrations.graphene.GrapheneIntegration",
"sentry_sdk.integrations.httpx.HttpxIntegration",
"sentry_sdk.integrations.huey.HueyIntegration",
"sentry_sdk.integrations.langchain.LangchainIntegration",
"sentry_sdk.integrations.loguru.LoguruIntegration",
"sentry_sdk.integrations.openai.OpenAIIntegration",
"sentry_sdk.integrations.pymongo.PyMongoIntegration",
Expand Down