Skip to content

Commit 0e67af5

Browse files
authored
Merge pull request #1112 from newrelic/develop-ai-limited-preview-3
Develop ai limited preview 3
2 parents 43e5e25 + a21115e commit 0e67af5

File tree

75 files changed

+25170
-328
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

75 files changed

+25170
-328
lines changed

newrelic/agent.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,10 @@ def __asgi_application(*args, **kwargs):
153153
from newrelic.api.message_transaction import (
154154
wrap_message_transaction as __wrap_message_transaction,
155155
)
156+
from newrelic.api.ml_model import (
157+
record_llm_feedback_event as __record_llm_feedback_event,
158+
)
159+
from newrelic.api.ml_model import set_llm_token_count_callback as __set_llm_token_count_callback
156160
from newrelic.api.ml_model import wrap_mlmodel as __wrap_mlmodel
157161
from newrelic.api.profile_trace import ProfileTraceWrapper as __ProfileTraceWrapper
158162
from newrelic.api.profile_trace import profile_trace as __profile_trace
@@ -174,10 +178,10 @@ def __asgi_application(*args, **kwargs):
174178
from newrelic.api.web_transaction import web_transaction as __web_transaction
175179
from newrelic.api.web_transaction import wrap_web_transaction as __wrap_web_transaction
176180
from newrelic.common.object_names import callable_name as __callable_name
181+
from newrelic.common.object_wrapper import CallableObjectProxy as __CallableObjectProxy
177182
from newrelic.common.object_wrapper import FunctionWrapper as __FunctionWrapper
178183
from newrelic.common.object_wrapper import InFunctionWrapper as __InFunctionWrapper
179184
from newrelic.common.object_wrapper import ObjectProxy as __ObjectProxy
180-
from newrelic.common.object_wrapper import CallableObjectProxy as __CallableObjectProxy
181185
from newrelic.common.object_wrapper import ObjectWrapper as __ObjectWrapper
182186
from newrelic.common.object_wrapper import OutFunctionWrapper as __OutFunctionWrapper
183187
from newrelic.common.object_wrapper import PostFunctionWrapper as __PostFunctionWrapper
@@ -343,3 +347,5 @@ def __asgi_application(*args, **kwargs):
343347
insert_html_snippet = __wrap_api_call(__insert_html_snippet, "insert_html_snippet")
344348
verify_body_exists = __wrap_api_call(__verify_body_exists, "verify_body_exists")
345349
wrap_mlmodel = __wrap_api_call(__wrap_mlmodel, "wrap_mlmodel")
350+
record_llm_feedback_event = __wrap_api_call(__record_llm_feedback_event, "record_llm_feedback_event")
351+
set_llm_token_count_callback = __wrap_api_call(__set_llm_token_count_callback, "set_llm_token_count_callback")

newrelic/api/ml_model.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,18 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
import logging
1516
import sys
17+
import uuid
18+
import warnings
1619

20+
from newrelic.api.transaction import current_transaction
1721
from newrelic.common.object_names import callable_name
22+
from newrelic.core.config import global_settings
1823
from newrelic.hooks.mlmodel_sklearn import _nr_instrument_model
1924

25+
_logger = logging.getLogger(__name__)
26+
2027

2128
def wrap_mlmodel(model, name=None, version=None, feature_names=None, label_names=None, metadata=None):
2229
model_callable_name = callable_name(model)
@@ -33,3 +40,87 @@ def wrap_mlmodel(model, name=None, version=None, feature_names=None, label_names
3340
model._nr_wrapped_label_names = label_names
3441
if metadata:
3542
model._nr_wrapped_metadata = metadata
43+
44+
45+
def record_llm_feedback_event(trace_id, rating, category=None, message=None, metadata=None):
46+
transaction = current_transaction()
47+
if not transaction:
48+
warnings.warn(
49+
"No message feedback events will be recorded. record_llm_feedback_event must be called within the "
50+
"scope of a transaction."
51+
)
52+
return
53+
54+
feedback_event_id = str(uuid.uuid4())
55+
feedback_event = metadata.copy() if metadata else {}
56+
feedback_event.update(
57+
{
58+
"id": feedback_event_id,
59+
"trace_id": trace_id,
60+
"rating": rating,
61+
"category": category,
62+
"message": message,
63+
"ingest_source": "Python",
64+
}
65+
)
66+
67+
transaction.record_custom_event("LlmFeedbackMessage", feedback_event)
68+
69+
70+
def set_llm_token_count_callback(callback, application=None):
71+
"""
72+
Set the current callback to be used to calculate LLM token counts.
73+
74+
Arguments:
75+
callback -- the user-defined callback that will calculate and return the total token count as an integer or None if it does not know
76+
application -- optional application object to associate call with
77+
"""
78+
if callback and not callable(callback):
79+
_logger.error(
80+
"callback passed to set_llm_token_count_callback must be a Callable type or None to unset the callback."
81+
)
82+
return
83+
84+
from newrelic.api.application import application_instance
85+
86+
# Check for activated application if it exists and was not given.
87+
application = application or application_instance(activate=False)
88+
89+
# Get application settings if it exists, or fallback to global settings object.
90+
settings = application.settings if application else global_settings()
91+
92+
if not settings:
93+
_logger.error(
94+
"Failed to set llm_token_count_callback. Settings not found on application or in global_settings."
95+
)
96+
return
97+
98+
if not callback:
99+
settings.ai_monitoring._llm_token_count_callback = None
100+
return
101+
102+
def _wrap_callback(model, content):
103+
if model is None:
104+
_logger.debug(
105+
"The model argument passed to the user-defined token calculation callback is None. The callback will not be run."
106+
)
107+
return None
108+
109+
if content is None:
110+
_logger.debug(
111+
"The content argument passed to the user-defined token calculation callback is None. The callback will not be run."
112+
)
113+
return None
114+
115+
token_count_val = callback(model, content)
116+
117+
if not isinstance(token_count_val, int) or token_count_val < 0:
118+
_logger.warning(
119+
"llm_token_count_callback returned an invalid value of %s. This value must be a positive integer and will not be recorded for the token_count."
120+
% token_count_val
121+
)
122+
return None
123+
124+
return token_count_val
125+
126+
settings.ai_monitoring._llm_token_count_callback = _wrap_callback

newrelic/api/time_trace.py

Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@
2929
)
3030
from newrelic.core.config import is_expected_error, should_ignore_error
3131
from newrelic.core.trace_cache import trace_cache
32-
3332
from newrelic.packages import six
3433

3534
_logger = logging.getLogger(__name__)
@@ -260,6 +259,11 @@ def _observe_exception(self, exc_info=None, ignore=None, expected=None, status_c
260259
module, name, fullnames, message_raw = parse_exc_info((exc, value, tb))
261260
fullname = fullnames[0]
262261

262+
# In case message is in JSON format for OpenAI models
263+
# this will result in a "cleaner" message format
264+
if getattr(value, "_nr_message", None):
265+
message_raw = value._nr_message
266+
263267
# Check to see if we need to strip the message before recording it.
264268

265269
if settings.strip_exception_messages.enabled and fullname not in settings.strip_exception_messages.allowlist:
@@ -422,23 +426,32 @@ def notice_error(self, error=None, attributes=None, expected=None, ignore=None,
422426
input_attributes = {}
423427
input_attributes.update(transaction._custom_params)
424428
input_attributes.update(attributes)
425-
error_group_name_raw = settings.error_collector.error_group_callback(value, {
426-
"traceback": tb,
427-
"error.class": exc,
428-
"error.message": message_raw,
429-
"error.expected": is_expected,
430-
"custom_params": input_attributes,
431-
"transactionName": getattr(transaction, "name", None),
432-
"response.status": getattr(transaction, "_response_code", None),
433-
"request.method": getattr(transaction, "_request_method", None),
434-
"request.uri": getattr(transaction, "_request_uri", None),
435-
})
429+
error_group_name_raw = settings.error_collector.error_group_callback(
430+
value,
431+
{
432+
"traceback": tb,
433+
"error.class": exc,
434+
"error.message": message_raw,
435+
"error.expected": is_expected,
436+
"custom_params": input_attributes,
437+
"transactionName": getattr(transaction, "name", None),
438+
"response.status": getattr(transaction, "_response_code", None),
439+
"request.method": getattr(transaction, "_request_method", None),
440+
"request.uri": getattr(transaction, "_request_uri", None),
441+
},
442+
)
436443
if error_group_name_raw:
437444
_, error_group_name = process_user_attribute("error.group.name", error_group_name_raw)
438445
if error_group_name is None or not isinstance(error_group_name, six.string_types):
439-
raise ValueError("Invalid attribute value for error.group.name. Expected string, got: %s" % repr(error_group_name_raw))
446+
raise ValueError(
447+
"Invalid attribute value for error.group.name. Expected string, got: %s"
448+
% repr(error_group_name_raw)
449+
)
440450
except Exception:
441-
_logger.error("Encountered error when calling error group callback:\n%s", "".join(traceback.format_exception(*sys.exc_info())))
451+
_logger.error(
452+
"Encountered error when calling error group callback:\n%s",
453+
"".join(traceback.format_exception(*sys.exc_info())),
454+
)
442455
error_group_name = None
443456

444457
transaction._create_error_node(
@@ -595,13 +608,11 @@ def update_async_exclusive_time(self, min_child_start_time, exclusive_duration):
595608
def process_child(self, node, is_async):
596609
self.children.append(node)
597610
if is_async:
598-
599611
# record the lowest start time
600612
self.min_child_start_time = min(self.min_child_start_time, node.start_time)
601613

602614
# if there are no children running, finalize exclusive time
603615
if self.child_count == len(self.children):
604-
605616
exclusive_duration = node.end_time - self.min_child_start_time
606617

607618
self.update_async_exclusive_time(self.min_child_start_time, exclusive_duration)

newrelic/api/transaction.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ def __init__(self, application, enabled=None, source=None):
176176

177177
self.thread_id = None
178178

179-
self._transaction_id = id(self)
179+
self._identity = id(self)
180180
self._transaction_lock = threading.Lock()
181181

182182
self._dead = False
@@ -193,6 +193,7 @@ def __init__(self, application, enabled=None, source=None):
193193
self._frameworks = set()
194194
self._message_brokers = set()
195195
self._dispatchers = set()
196+
self._ml_models = set()
196197

197198
self._frozen_path = None
198199

@@ -274,6 +275,7 @@ def __init__(self, application, enabled=None, source=None):
274275
trace_id = "%032x" % random.getrandbits(128)
275276

276277
# 16-digit random hex. Padded with zeros in the front.
278+
# This is the official transactionId in the UI.
277279
self.guid = trace_id[:16]
278280

279281
# 32-digit random hex. Padded with zeros in the front.
@@ -421,7 +423,7 @@ def __exit__(self, exc, value, tb):
421423
if not self.enabled:
422424
return
423425

424-
if self._transaction_id != id(self):
426+
if self._identity != id(self):
425427
return
426428

427429
if not self._settings:
@@ -568,6 +570,10 @@ def __exit__(self, exc, value, tb):
568570
for dispatcher, version in self._dispatchers:
569571
self.record_custom_metric("Python/Dispatcher/%s/%s" % (dispatcher, version), 1)
570572

573+
if self._ml_models:
574+
for ml_model, version in self._ml_models:
575+
self.record_custom_metric("Supportability/Python/ML/%s/%s" % (ml_model, version), 1)
576+
571577
if self._settings.distributed_tracing.enabled:
572578
# Sampled and priority need to be computed at the end of the
573579
# transaction when distributed tracing or span events are enabled.
@@ -1715,7 +1721,7 @@ def record_custom_event(self, event_type, params):
17151721
if not settings.custom_insights_events.enabled:
17161722
return
17171723

1718-
event = create_custom_event(event_type, params)
1724+
event = create_custom_event(event_type, params, settings=settings)
17191725
if event:
17201726
self._custom_events.add(event, priority=self.priority)
17211727

@@ -1728,7 +1734,7 @@ def record_ml_event(self, event_type, params):
17281734
if not settings.ml_insights_events.enabled:
17291735
return
17301736

1731-
event = create_custom_event(event_type, params)
1737+
event = create_custom_event(event_type, params, settings=settings, is_ml_event=True)
17321738
if event:
17331739
self._ml_events.add(event, priority=self.priority)
17341740

@@ -1835,6 +1841,10 @@ def add_dispatcher_info(self, name, version=None):
18351841
if name:
18361842
self._dispatchers.add((name, version))
18371843

1844+
def add_ml_model_info(self, name, version=None):
1845+
if name:
1846+
self._ml_models.add((name, version))
1847+
18381848
def dump(self, file):
18391849
"""Dumps details about the transaction to the file object."""
18401850

0 commit comments

Comments
 (0)