Skip to content

Commit 67cce6f

Browse files
authored
Merge pull request #1748 from newrelic/fix-langchain-chain-models
LangChain Chain Model Reporting
2 parents c972036 + cd81a1a commit 67cce6f

2 files changed

Lines changed: 170 additions & 6 deletions

File tree

newrelic/hooks/mlmodel_langchain.py

Lines changed: 139 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -768,6 +768,7 @@ async def wrap_chain_async_run(wrapped, instance, args, kwargs):
768768
run_args["timestamp"] = int(1000.0 * time.time())
769769
completion_id = str(uuid.uuid4())
770770
add_nr_completion_id(run_args, completion_id)
771+
nr_callback_handler = _attach_nr_callback_handler(run_args)
771772
# Check to see if launched from agent or directly from chain.
772773
# The trace group will reflect from where it has started.
773774
# The AgentExecutor class has an attribute "agent" that does
@@ -788,6 +789,7 @@ async def wrap_chain_async_run(wrapped, instance, args, kwargs):
788789
completion_id=completion_id,
789790
linking_metadata=linking_metadata,
790791
duration=ft.duration * 1000,
792+
callback_handler=nr_callback_handler,
791793
)
792794
raise
793795
ft.__exit__(None, None, None)
@@ -803,6 +805,7 @@ async def wrap_chain_async_run(wrapped, instance, args, kwargs):
803805
response=response,
804806
linking_metadata=linking_metadata,
805807
duration=ft.duration * 1000,
808+
callback_handler=nr_callback_handler,
806809
)
807810
return response
808811

@@ -824,6 +827,7 @@ def wrap_chain_sync_run(wrapped, instance, args, kwargs):
824827
run_args["timestamp"] = int(1000.0 * time.time())
825828
completion_id = str(uuid.uuid4())
826829
add_nr_completion_id(run_args, completion_id)
830+
nr_callback_handler = _attach_nr_callback_handler(run_args)
827831
# Check to see if launched from agent or directly from chain.
828832
# The trace group will reflect from where it has started.
829833
# The AgentExecutor class has an attribute "agent" that does
@@ -844,6 +848,7 @@ def wrap_chain_sync_run(wrapped, instance, args, kwargs):
844848
completion_id=completion_id,
845849
linking_metadata=linking_metadata,
846850
duration=ft.duration * 1000,
851+
callback_handler=nr_callback_handler,
847852
)
848853
raise
849854
ft.__exit__(None, None, None)
@@ -859,6 +864,7 @@ def wrap_chain_sync_run(wrapped, instance, args, kwargs):
859864
response=response,
860865
linking_metadata=linking_metadata,
861866
duration=ft.duration * 1000,
867+
callback_handler=nr_callback_handler,
862868
)
863869
return response
864870

@@ -880,6 +886,7 @@ def wrap_RunnableSequence_stream(wrapped, instance, args, kwargs):
880886
run_args["timestamp"] = int(1000.0 * time.time())
881887
completion_id = str(uuid.uuid4())
882888
add_nr_completion_id(run_args, completion_id)
889+
nr_callback_handler = _attach_nr_callback_handler(run_args)
883890
# Check to see if launched from agent or directly from chain.
884891
# The trace group will reflect from where it has started.
885892
# The AgentExecutor class has an attribute "agent" that does
@@ -899,18 +906,25 @@ def wrap_RunnableSequence_stream(wrapped, instance, args, kwargs):
899906
completion_id=completion_id,
900907
response=[],
901908
linking_metadata=linking_metadata,
909+
callback_handler=nr_callback_handler,
902910
),
903911
on_error=_on_chain_error(
904912
ft=ft,
905913
instance=instance,
906914
run_args=run_args,
907915
completion_id=completion_id,
908916
linking_metadata=linking_metadata,
917+
callback_handler=nr_callback_handler,
909918
),
910919
)
911920
except Exception:
912921
_on_chain_error(
913-
ft=ft, instance=instance, run_args=run_args, completion_id=completion_id, linking_metadata=linking_metadata
922+
ft=ft,
923+
instance=instance,
924+
run_args=run_args,
925+
completion_id=completion_id,
926+
linking_metadata=linking_metadata,
927+
callback_handler=nr_callback_handler,
914928
)(transaction)
915929
raise
916930

@@ -934,6 +948,7 @@ def wrap_RunnableSequence_astream(wrapped, instance, args, kwargs):
934948
run_args["timestamp"] = int(1000.0 * time.time())
935949
completion_id = str(uuid.uuid4())
936950
add_nr_completion_id(run_args, completion_id)
951+
nr_callback_handler = _attach_nr_callback_handler(run_args)
937952
# Check to see if launched from agent or directly from chain.
938953
# The trace group will reflect from where it has started.
939954
# The AgentExecutor class has an attribute "agent" that does
@@ -953,25 +968,34 @@ def wrap_RunnableSequence_astream(wrapped, instance, args, kwargs):
953968
completion_id=completion_id,
954969
response=[],
955970
linking_metadata=linking_metadata,
971+
callback_handler=nr_callback_handler,
956972
),
957973
on_error=_on_chain_error(
958974
ft=ft,
959975
instance=instance,
960976
run_args=run_args,
961977
completion_id=completion_id,
962978
linking_metadata=linking_metadata,
979+
callback_handler=nr_callback_handler,
963980
),
964981
)
965982
except Exception:
966983
_on_chain_error(
967-
ft=ft, instance=instance, run_args=run_args, completion_id=completion_id, linking_metadata=linking_metadata
984+
ft=ft,
985+
instance=instance,
986+
run_args=run_args,
987+
completion_id=completion_id,
988+
linking_metadata=linking_metadata,
989+
callback_handler=nr_callback_handler,
968990
)(transaction)
969991
raise
970992

971993
return return_val
972994

973995

974-
def _on_chain_stop_iteration(*, ft, instance, run_args, completion_id, response, linking_metadata):
996+
def _on_chain_stop_iteration(
997+
*, ft, instance, run_args, completion_id, response, linking_metadata, callback_handler=None
998+
):
975999
def _on_stop_iteration(proxy, transaction):
9761000
ft.__exit__(None, None, None)
9771001
_create_successful_chain_run_events(
@@ -982,12 +1006,13 @@ def _on_stop_iteration(proxy, transaction):
9821006
response=response,
9831007
linking_metadata=linking_metadata,
9841008
duration=ft.duration * 1000,
1009+
callback_handler=callback_handler,
9851010
)
9861011

9871012
return _on_stop_iteration
9881013

9891014

990-
def _on_chain_error(*, ft, instance, run_args, completion_id, linking_metadata):
1015+
def _on_chain_error(*, ft, instance, run_args, completion_id, linking_metadata, callback_handler=None):
9911016
def _on_error(proxy, transaction):
9921017
ft.notice_error(attributes={"completion_id": completion_id})
9931018
ft.__exit__(*sys.exc_info())
@@ -998,6 +1023,7 @@ def _on_error(proxy, transaction):
9981023
completion_id=completion_id,
9991024
linking_metadata=linking_metadata,
10001025
duration=ft.duration * 1000,
1026+
callback_handler=callback_handler,
10011027
)
10021028

10031029
return _on_error
@@ -1015,13 +1041,109 @@ def add_nr_completion_id(run_args, completion_id):
10151041
run_args["config"]["metadata"] = metadata
10161042

10171043

1018-
def _create_error_chain_run_events(*, transaction, instance, run_args, completion_id, linking_metadata, duration):
1044+
_NR_CALLBACK_HANDLER_CLS = None
1045+
1046+
1047+
def _get_nr_callback_handler_cls():
1048+
global _NR_CALLBACK_HANDLER_CLS
1049+
if _NR_CALLBACK_HANDLER_CLS is not None:
1050+
return _NR_CALLBACK_HANDLER_CLS
1051+
try:
1052+
from langchain_core.callbacks import BaseCallbackHandler
1053+
except ImportError:
1054+
_NR_CALLBACK_HANDLER_CLS = False
1055+
return False
1056+
1057+
class NewRelicCallbackHandler(BaseCallbackHandler):
1058+
raise_error = False
1059+
1060+
def __init__(self):
1061+
super().__init__()
1062+
self.response_model = None
1063+
1064+
def on_llm_end(self, response, **_kwargs):
1065+
if self.response_model is not None:
1066+
return
1067+
try:
1068+
self.response_model = _extract_chain_response_model(response)
1069+
except Exception:
1070+
pass
1071+
1072+
_NR_CALLBACK_HANDLER_CLS = NewRelicCallbackHandler
1073+
return NewRelicCallbackHandler
1074+
1075+
1076+
def _attach_nr_callback_handler(run_args):
1077+
cls = _get_nr_callback_handler_cls()
1078+
if not cls:
1079+
return None
1080+
handler = cls()
1081+
cfg = dict(run_args.get("config") or {})
1082+
callbacks = cfg.get("callbacks")
1083+
if callbacks is None:
1084+
cfg["callbacks"] = [handler]
1085+
elif isinstance(callbacks, list):
1086+
cfg["callbacks"] = [*callbacks, handler]
1087+
else:
1088+
try:
1089+
callbacks.add_handler(handler, inherit=True)
1090+
except Exception:
1091+
return None
1092+
run_args["config"] = cfg
1093+
return handler
1094+
1095+
1096+
def _extract_chain_response_model(response):
1097+
llm_output = getattr(response, "llm_output", None) or {}
1098+
for key in ("model_name", "model", "model_id"):
1099+
val = llm_output.get(key)
1100+
if isinstance(val, str) and val:
1101+
return val
1102+
generations = getattr(response, "generations", None) or []
1103+
for gen_list in generations:
1104+
for gen in gen_list or []:
1105+
info = getattr(gen, "generation_info", None) or {}
1106+
for key in ("model_name", "model", "model_id"):
1107+
val = info.get(key)
1108+
if isinstance(val, str) and val:
1109+
return val
1110+
return None
1111+
1112+
1113+
def _get_chain_request_model(instance):
1114+
# A best effort attempt to pull the request model from the chain or any of
1115+
# its steps for better observability. This is not guaranteed to work in all
1116+
# cases as it depends on how the chain and steps are implemented, and it can
1117+
# only pull the first model it finds. The request model is not a guaranteed
1118+
# attribute on chains or steps, but some implementations may have it.
1119+
try:
1120+
llm = getattr(instance, "llm", None)
1121+
if llm is not None:
1122+
name = getattr(llm, "model_name", None) or getattr(llm, "model", None)
1123+
if isinstance(name, str) and name:
1124+
return name
1125+
steps = getattr(instance, "steps", None)
1126+
if steps:
1127+
for step in steps:
1128+
name = getattr(step, "model_name", None) or getattr(step, "model", None)
1129+
if isinstance(name, str) and name:
1130+
return name
1131+
except Exception:
1132+
pass
1133+
return None
1134+
1135+
1136+
def _create_error_chain_run_events(
1137+
*, transaction, instance, run_args, completion_id, linking_metadata, duration, callback_handler=None
1138+
):
10191139
_input = run_args.get("input")
10201140
llm_metadata_dict = _get_llm_metadata(transaction)
10211141
run_id, metadata, tags = _get_run_manager_info(transaction, run_args, instance, completion_id)
10221142
span_id = linking_metadata.get("span.id")
10231143
trace_id = linking_metadata.get("trace.id")
10241144
input_message_list = [_input]
1145+
request_model = _get_chain_request_model(instance)
1146+
response_model = getattr(callback_handler, "response_model", None) if callback_handler else None
10251147

10261148
# Make sure the builtin attributes take precedence over metadata attributes.
10271149
full_chat_completion_summary_dict = {f"metadata.{key}": value for key, value in metadata.items()}
@@ -1035,6 +1157,8 @@ def _create_error_chain_run_events(*, transaction, instance, run_args, completio
10351157
"virtual_llm": True,
10361158
"request_id": run_id,
10371159
"duration": duration,
1160+
"request.model": request_model,
1161+
"response.model": response_model,
10381162
"response.number_of_messages": len(input_message_list),
10391163
"tags": tags,
10401164
"error": True,
@@ -1053,6 +1177,7 @@ def _create_error_chain_run_events(*, transaction, instance, run_args, completio
10531177
llm_metadata_dict=llm_metadata_dict,
10541178
output_message_list=[],
10551179
request_timestamp=run_args["timestamp"] or None,
1180+
response_model=response_model,
10561181
)
10571182

10581183

@@ -1069,7 +1194,7 @@ def _get_run_manager_info(transaction, run_args, instance, completion_id):
10691194

10701195

10711196
def _create_successful_chain_run_events(
1072-
*, transaction, instance, run_args, completion_id, response, linking_metadata, duration
1197+
*, transaction, instance, run_args, completion_id, response, linking_metadata, duration, callback_handler=None
10731198
):
10741199
_input = run_args.get("input")
10751200
llm_metadata_dict = _get_llm_metadata(transaction)
@@ -1078,6 +1203,8 @@ def _create_successful_chain_run_events(
10781203
trace_id = linking_metadata.get("trace.id")
10791204
input_message_list = [_input]
10801205
output_message_list = []
1206+
request_model = _get_chain_request_model(instance)
1207+
response_model = getattr(callback_handler, "response_model", None) if callback_handler else None
10811208
if isinstance(response, str):
10821209
output_message_list = [response]
10831210
else:
@@ -1104,6 +1231,8 @@ def _create_successful_chain_run_events(
11041231
"virtual_llm": True,
11051232
"request_id": run_id,
11061233
"duration": duration,
1234+
"request.model": request_model,
1235+
"response.model": response_model,
11071236
"response.number_of_messages": len(input_message_list) + len(output_message_list),
11081237
"tags": tags,
11091238
"timestamp": run_args.get("timestamp") or None,
@@ -1125,6 +1254,7 @@ def _create_successful_chain_run_events(
11251254
llm_metadata_dict=llm_metadata_dict,
11261255
output_message_list=output_message_list,
11271256
request_timestamp=run_args["timestamp"] or None,
1257+
response_model=response_model,
11281258
)
11291259

11301260

@@ -1139,6 +1269,7 @@ def create_chat_completion_message_event(
11391269
llm_metadata_dict,
11401270
output_message_list,
11411271
request_timestamp=None,
1272+
response_model=None,
11421273
):
11431274
settings = transaction.settings if transaction.settings is not None else global_settings()
11441275

@@ -1151,6 +1282,7 @@ def create_chat_completion_message_event(
11511282
"trace_id": trace_id,
11521283
"completion_id": chat_completion_id,
11531284
"sequence": index,
1285+
"response.model": response_model,
11541286
"vendor": "langchain",
11551287
"ingest_source": "Python",
11561288
"virtual_llm": True,
@@ -1176,6 +1308,7 @@ def create_chat_completion_message_event(
11761308
"trace_id": trace_id,
11771309
"completion_id": chat_completion_id,
11781310
"sequence": index,
1311+
"response.model": response_model,
11791312
"vendor": "langchain",
11801313
"ingest_source": "Python",
11811314
"is_response": True,

0 commit comments

Comments
 (0)