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

LLM Custom Attributes Context Manager API #1214

Merged
merged 32 commits into from
Oct 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
c84d105
Add new custom attribute context manager.
umaannamalai Sep 18, 2024
5a7c183
Add new custom attribute context manager.
umaannamalai Sep 18, 2024
df11858
Add test cases.
umaannamalai Sep 20, 2024
37e5b3b
Merge branch 'llm-custom-attrs-api' of github.com:newrelic/newrelic-p…
umaannamalai Sep 20, 2024
977f1b7
Cleanup files.
umaannamalai Sep 20, 2024
4f82456
Merge branch 'main' into llm-custom-attrs-api
umaannamalai Sep 20, 2024
a5c6f1e
Merge branch 'main' into llm-custom-attrs-api
umaannamalai Sep 25, 2024
15605f2
Merge branch 'main' into llm-custom-attrs-api
mergify[bot] Sep 25, 2024
79b5cbe
Merge branch 'main' into llm-custom-attrs-api
mergify[bot] Sep 26, 2024
f51c621
Add new custom attribute context manager.
umaannamalai Sep 18, 2024
f6a8c5d
Add test cases.
umaannamalai Sep 20, 2024
9cd6d6b
Add new custom attribute context manager.
umaannamalai Sep 26, 2024
db2ac31
Cleanup files.
umaannamalai Sep 20, 2024
ba684ed
Add contextvars.
umaannamalai Sep 26, 2024
34ae93e
Cleanup.
umaannamalai Sep 30, 2024
fa903ec
Merge branch 'main' into llm-custom-attrs-api
umaannamalai Oct 1, 2024
7a01097
Merge branch 'main' into llm-custom-attrs-api
mergify[bot] Oct 1, 2024
d926afe
Merge branch 'llm-custom-attrs-api' of github.com:newrelic/newrelic-p…
umaannamalai Oct 2, 2024
7d690f6
Merge branch 'llm-custom-attrs-api' of github.com:newrelic/newrelic-p…
umaannamalai Oct 2, 2024
b38c89b
Update error handling.
umaannamalai Oct 2, 2024
a8beff8
Merge branch 'main' into llm-custom-attrs-api
mergify[bot] Oct 2, 2024
18278db
Undo unnecessary formatting changes
hmstepanek Oct 7, 2024
1694844
Revert to transaction based storage of attrs.
umaannamalai Oct 8, 2024
1c27c2b
Merge branch 'llm-custom-attrs-api' of github.com:newrelic/newrelic-p…
umaannamalai Oct 8, 2024
76e7640
[Mega-Linter] Apply linters fixes
umaannamalai Oct 8, 2024
6edffd1
Merge branch 'main' into llm-custom-attrs-api
mergify[bot] Oct 8, 2024
beeb67f
Delete context attrs instead of nullifying.
umaannamalai Oct 8, 2024
4f7c555
Merge branch 'llm-custom-attrs-api' of github.com:newrelic/newrelic-p…
umaannamalai Oct 8, 2024
46d5521
Update tests.
umaannamalai Oct 9, 2024
7160696
Remove newline.
umaannamalai Oct 9, 2024
ef40ca6
Assert attribute deletion was successful.
umaannamalai Oct 9, 2024
182cc71
Merge branch 'main' into llm-custom-attrs-api
mergify[bot] Oct 9, 2024
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
8 changes: 7 additions & 1 deletion newrelic/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,9 @@ def __asgi_application(*args, **kwargs):
from newrelic.api.html_insertion import verify_body_exists as __verify_body_exists
from newrelic.api.lambda_handler import LambdaHandlerWrapper as __LambdaHandlerWrapper
from newrelic.api.lambda_handler import lambda_handler as __lambda_handler
from newrelic.api.llm_custom_attributes import (
WithLlmCustomAttributes as __WithLlmCustomAttributes,
)
from newrelic.api.message_trace import MessageTrace as __MessageTrace
from newrelic.api.message_trace import MessageTraceWrapper as __MessageTraceWrapper
from newrelic.api.message_trace import message_trace as __message_trace
Expand All @@ -156,7 +159,9 @@ def __asgi_application(*args, **kwargs):
from newrelic.api.ml_model import (
record_llm_feedback_event as __record_llm_feedback_event,
)
from newrelic.api.ml_model import set_llm_token_count_callback as __set_llm_token_count_callback
from newrelic.api.ml_model import (
set_llm_token_count_callback as __set_llm_token_count_callback,
)
from newrelic.api.ml_model import wrap_mlmodel as __wrap_mlmodel
from newrelic.api.profile_trace import ProfileTraceWrapper as __ProfileTraceWrapper
from newrelic.api.profile_trace import profile_trace as __profile_trace
Expand Down Expand Up @@ -251,6 +256,7 @@ def __asgi_application(*args, **kwargs):
record_custom_event = __wrap_api_call(__record_custom_event, "record_custom_event")
record_log_event = __wrap_api_call(__record_log_event, "record_log_event")
record_ml_event = __wrap_api_call(__record_ml_event, "record_ml_event")
WithLlmCustomAttributes = __wrap_api_call(__WithLlmCustomAttributes, "WithLlmCustomAttributes")
accept_distributed_trace_payload = __wrap_api_call(
__accept_distributed_trace_payload, "accept_distributed_trace_payload"
)
Expand Down
47 changes: 47 additions & 0 deletions newrelic/api/llm_custom_attributes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Copyright 2010 New Relic, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import logging

from newrelic.api.transaction import current_transaction

_logger = logging.getLogger(__name__)


class WithLlmCustomAttributes(object):
def __init__(self, custom_attr_dict):
transaction = current_transaction()
if not custom_attr_dict or not isinstance(custom_attr_dict, dict):
raise TypeError(
"custom_attr_dict must be a non-empty dictionary. Received type: %s" % type(custom_attr_dict)
)

# Add "llm." prefix to all keys in attribute dictionary
context_attrs = {k if k.startswith("llm.") else f"llm.{k}": v for k, v in custom_attr_dict.items()}

self.attr_dict = context_attrs
self.transaction = transaction

def __enter__(self):
if not self.transaction:
_logger.warning("WithLlmCustomAttributes must be called within the scope of a transaction.")
return self

self.transaction._llm_context_attrs = self.attr_dict
return self

def __exit__(self, exc, value, tb):
# Clear out context attributes once we leave the current context
if self.transaction:
del self.transaction._llm_context_attrs
4 changes: 4 additions & 0 deletions newrelic/hooks/external_botocore.py
Original file line number Diff line number Diff line change
Expand Up @@ -787,6 +787,10 @@ def handle_chat_completion_event(transaction, bedrock_attrs):
custom_attrs_dict = transaction._custom_params
llm_metadata_dict = {key: value for key, value in custom_attrs_dict.items() if key.startswith("llm.")}

llm_context_attrs = getattr(transaction, "_llm_context_attrs", None)
if llm_context_attrs:
llm_metadata_dict.update(llm_context_attrs)

span_id = bedrock_attrs.get("span_id", None)
trace_id = bedrock_attrs.get("trace_id", None)
request_id = bedrock_attrs.get("request_id", None)
Expand Down
6 changes: 5 additions & 1 deletion newrelic/hooks/mlmodel_langchain.py
Original file line number Diff line number Diff line change
Expand Up @@ -697,7 +697,7 @@ def _get_run_manager_info(transaction, run_args, instance, completion_id):
# metadata and tags are keys in the config parameter.
metadata = {}
metadata.update((run_args.get("config") or {}).get("metadata") or {})
# Do not report intenral nr_completion_id in metadata.
# Do not report internal nr_completion_id in metadata.
umaannamalai marked this conversation as resolved.
Show resolved Hide resolved
metadata = {key: value for key, value in metadata.items() if key != "nr_completion_id"}
tags = []
tags.extend((run_args.get("config") or {}).get("tags") or [])
Expand All @@ -708,6 +708,10 @@ def _get_llm_metadata(transaction):
# Grab LLM-related custom attributes off of the transaction to store as metadata on LLM events
custom_attrs_dict = transaction._custom_params
llm_metadata_dict = {key: value for key, value in custom_attrs_dict.items() if key.startswith("llm.")}
llm_context_attrs = getattr(transaction, "_llm_context_attrs", None)
if llm_context_attrs:
llm_metadata_dict.update(llm_context_attrs)

return llm_metadata_dict


Expand Down
9 changes: 7 additions & 2 deletions newrelic/hooks/mlmodel_openai.py
Original file line number Diff line number Diff line change
Expand Up @@ -927,8 +927,13 @@ def is_stream(wrapped, args, kwargs):
def _get_llm_attributes(transaction):
"""Returns llm.* custom attributes off of the transaction."""
custom_attrs_dict = transaction._custom_params
llm_metadata = {key: value for key, value in custom_attrs_dict.items() if key.startswith("llm.")}
return llm_metadata
llm_metadata_dict = {key: value for key, value in custom_attrs_dict.items() if key.startswith("llm.")}

llm_context_attrs = getattr(transaction, "_llm_context_attrs", None)
if llm_context_attrs:
llm_metadata_dict.update(llm_context_attrs)

return llm_metadata_dict


def instrument_openai_api_resources_embedding(module):
Expand Down
50 changes: 50 additions & 0 deletions tests/agent_features/test_llm_custom_attributes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Copyright 2010 New Relic, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import pytest

from newrelic.api.background_task import background_task
from newrelic.api.llm_custom_attributes import WithLlmCustomAttributes
from newrelic.api.transaction import current_transaction


@background_task()
def test_llm_custom_attributes():
transaction = current_transaction()
with WithLlmCustomAttributes({"test": "attr", "test1": "attr1"}):
assert transaction._llm_context_attrs == {"llm.test": "attr", "llm.test1": "attr1"}

assert not hasattr(transaction, "_llm_context_attrs")


@pytest.mark.parametrize("context_attrs", (None, "not-a-dict"))
@background_task()
def test_llm_custom_attributes_no_attrs(context_attrs):
transaction = current_transaction()

with pytest.raises(TypeError):
with WithLlmCustomAttributes(context_attrs):
pass

assert not hasattr(transaction, "_llm_context_attrs")


@background_task()
def test_llm_custom_attributes_prefixed_attrs():
transaction = current_transaction()
with WithLlmCustomAttributes({"llm.test": "attr", "test1": "attr1"}):
# Validate API does not prefix attributes that already begin with "llm."
assert transaction._llm_context_attrs == {"llm.test": "attr", "llm.test1": "attr1"}
umaannamalai marked this conversation as resolved.
Show resolved Hide resolved

assert not hasattr(transaction, "_llm_context_attrs")
42 changes: 23 additions & 19 deletions tests/external_botocore/test_bedrock_chat_completion.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
disabled_ai_monitoring_streaming_settings,
events_sans_content,
events_sans_llm_metadata,
events_with_context_attrs,
llm_token_count_callback,
set_trace_info,
)
Expand All @@ -58,6 +59,7 @@
)

from newrelic.api.background_task import background_task
from newrelic.api.llm_custom_attributes import WithLlmCustomAttributes
from newrelic.api.transaction import add_custom_attribute
from newrelic.common.object_names import callable_name
from newrelic.hooks.external_botocore import MODEL_EXTRACTORS
Expand Down Expand Up @@ -161,7 +163,7 @@ def expected_invalid_access_key_error_events(model_id):
def test_bedrock_chat_completion_in_txn_with_llm_metadata(
set_trace_info, exercise_model, expected_events, expected_metrics
):
@validate_custom_events(expected_events)
@validate_custom_events(events_with_context_attrs(expected_events))
# One summary event, one user message, and one response message from the assistant
@validate_custom_event_count(count=3)
@validate_transaction_metrics(
Expand All @@ -180,7 +182,8 @@ def _test():
add_custom_attribute("llm.conversation_id", "my-awesome-id")
add_custom_attribute("llm.foo", "bar")
add_custom_attribute("non_llm_attr", "python-agent")
exercise_model(prompt=_test_bedrock_chat_completion_prompt, temperature=0.7, max_tokens=100)
with WithLlmCustomAttributes({"context": "attr"}):
exercise_model(prompt=_test_bedrock_chat_completion_prompt, temperature=0.7, max_tokens=100)

_test()

Expand Down Expand Up @@ -320,7 +323,7 @@ def _test():
def test_bedrock_chat_completion_error_invalid_model(
bedrock_server, set_trace_info, response_streaming, expected_metrics
):
@validate_custom_events(chat_completion_invalid_model_error_events)
@validate_custom_events(events_with_context_attrs(chat_completion_invalid_model_error_events))
@validate_error_trace_attributes(
"botocore.errorfactory:ValidationException",
exact_attrs={
Expand Down Expand Up @@ -350,22 +353,23 @@ def _test():
add_custom_attribute("non_llm_attr", "python-agent")

with pytest.raises(_client_error):
if response_streaming:
stream = bedrock_server.invoke_model_with_response_stream(
body=b"{}",
modelId="does-not-exist",
accept="application/json",
contentType="application/json",
)
for _ in stream:
pass
else:
bedrock_server.invoke_model(
body=b"{}",
modelId="does-not-exist",
accept="application/json",
contentType="application/json",
)
with WithLlmCustomAttributes({"context": "attr"}):
if response_streaming:
stream = bedrock_server.invoke_model_with_response_stream(
body=b"{}",
modelId="does-not-exist",
accept="application/json",
contentType="application/json",
)
for _ in stream:
pass
else:
bedrock_server.invoke_model(
body=b"{}",
modelId="does-not-exist",
accept="application/json",
contentType="application/json",
)

_test()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,18 @@
)
from conftest import BOTOCORE_VERSION # pylint: disable=E0611
from testing_support.fixtures import reset_core_stats_engine, validate_attributes
from testing_support.ml_testing_utils import set_trace_info # noqa: F401
from testing_support.ml_testing_utils import ( # noqa: F401
events_with_context_attrs,
set_trace_info,
)
from testing_support.validators.validate_custom_event import validate_custom_event_count
from testing_support.validators.validate_custom_events import validate_custom_events
from testing_support.validators.validate_transaction_metrics import (
validate_transaction_metrics,
)

from newrelic.api.background_task import background_task
from newrelic.api.llm_custom_attributes import WithLlmCustomAttributes
from newrelic.api.transaction import add_custom_attribute

UNSUPPORTED_LANGCHAIN_MODELS = [
Expand Down Expand Up @@ -105,7 +109,7 @@ def test_bedrock_chat_completion_in_txn_with_llm_metadata(
expected_metrics,
response_streaming,
):
@validate_custom_events(expected_events)
@validate_custom_events(events_with_context_attrs(expected_events))
# One summary event, one user message, and one response message from the assistant
@validate_custom_event_count(count=6)
@validate_transaction_metrics(
Expand All @@ -124,6 +128,7 @@ def _test():
add_custom_attribute("llm.conversation_id", "my-awesome-id")
add_custom_attribute("llm.foo", "bar")
add_custom_attribute("non_llm_attr", "python-agent")
exercise_model(prompt="Hi there!")
with WithLlmCustomAttributes({"context": "attr"}):
exercise_model(prompt="Hi there!")

_test()
Loading
Loading