Skip to content

Commit 80f4a3c

Browse files
authored
Merge pull request #1630 from newrelic/feat-langgraph
LangChain / LangGraph Agent Instrumentation
2 parents 002fc85 + 5b67731 commit 80f4a3c

23 files changed

+1830
-762
lines changed

newrelic/common/llm_utils.py

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

15+
import itertools
16+
import logging
17+
18+
from newrelic.api.transaction import current_transaction
19+
from newrelic.common.object_wrapper import ObjectProxy
20+
21+
_logger = logging.getLogger(__name__)
22+
1523

1624
def _get_llm_metadata(transaction):
17-
# Grab LLM-related custom attributes off of the transaction to store as metadata on LLM events
18-
custom_attrs_dict = transaction._custom_params
19-
llm_metadata_dict = {key: value for key, value in custom_attrs_dict.items() if key.startswith("llm.")}
20-
llm_context_attrs = getattr(transaction, "_llm_context_attrs", None)
21-
if llm_context_attrs:
22-
llm_metadata_dict.update(llm_context_attrs)
25+
if not transaction:
26+
return {}
27+
try:
28+
# Grab LLM-related custom attributes off of the transaction to store as metadata on LLM events
29+
custom_attrs_dict = getattr(transaction, "_custom_params", {})
30+
llm_metadata_dict = {key: value for key, value in custom_attrs_dict.items() if key.startswith("llm.")}
31+
llm_context_attrs = getattr(transaction, "_llm_context_attrs", None)
32+
if llm_context_attrs:
33+
llm_metadata_dict.update(llm_context_attrs)
34+
except Exception:
35+
_logger.warning("Unable to capture custom metadata attributes to record on LLM events.")
36+
return {}
2337

2438
return llm_metadata_dict
39+
40+
41+
class GeneratorProxy(ObjectProxy):
42+
def __init__(self, wrapped, on_stop_iteration, on_error):
43+
super().__init__(wrapped)
44+
self._nr_on_stop_iteration = on_stop_iteration
45+
self._nr_on_error = on_error
46+
47+
def __iter__(self):
48+
self._nr_wrapped_iter = self.__wrapped__.__iter__()
49+
return self
50+
51+
def __next__(self):
52+
transaction = current_transaction()
53+
if not transaction:
54+
return self._nr_wrapped_iter.__next__()
55+
56+
return_val = None
57+
try:
58+
return_val = self._nr_wrapped_iter.__next__()
59+
except StopIteration:
60+
self._nr_on_stop_iteration(self, transaction)
61+
raise
62+
except Exception:
63+
self._nr_on_error(self, transaction)
64+
raise
65+
return return_val
66+
67+
def close(self):
68+
return self.__wrapped__.close()
69+
70+
def __copy__(self):
71+
# Required to properly interface with itertool.tee, which can be called by LangChain on generators
72+
self.__wrapped__, copy = itertools.tee(self.__wrapped__, 2)
73+
return GeneratorProxy(copy, self._nr_on_stop_iteration, self._nr_on_error)
74+
75+
76+
class AsyncGeneratorProxy(ObjectProxy):
77+
def __init__(self, wrapped, on_stop_iteration, on_error):
78+
super().__init__(wrapped)
79+
self._nr_on_stop_iteration = on_stop_iteration
80+
self._nr_on_error = on_error
81+
82+
def __aiter__(self):
83+
self._nr_wrapped_iter = self.__wrapped__.__aiter__()
84+
return self
85+
86+
async def __anext__(self):
87+
transaction = current_transaction()
88+
if not transaction:
89+
return await self._nr_wrapped_iter.__anext__()
90+
91+
return_val = None
92+
try:
93+
return_val = await self._nr_wrapped_iter.__anext__()
94+
except StopAsyncIteration:
95+
self._nr_on_stop_iteration(self, transaction)
96+
raise
97+
except Exception:
98+
self._nr_on_error(self, transaction)
99+
raise
100+
return return_val
101+
102+
async def aclose(self):
103+
return await self.__wrapped__.aclose()
104+
105+
def __copy__(self):
106+
# Required to properly interface with itertool.tee, which can be called by LangChain on generators
107+
self.__wrapped__, copy = itertools.tee(self.__wrapped__, n=2)
108+
return AsyncGeneratorProxy(copy, self._nr_on_stop_iteration, self._nr_on_error)

newrelic/config.py

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2088,6 +2088,10 @@ def _process_module_builtin_defaults():
20882088

20892089
_process_module_definition("asyncio.runners", "newrelic.hooks.coroutines_asyncio", "instrument_asyncio_runners")
20902090

2091+
_process_module_definition(
2092+
"langgraph.prebuilt.tool_node", "newrelic.hooks.mlmodel_langgraph", "instrument_langgraph_prebuilt_tool_node"
2093+
)
2094+
20912095
_process_module_definition(
20922096
"langchain_core.runnables.base",
20932097
"newrelic.hooks.mlmodel_langchain",
@@ -2099,13 +2103,19 @@ def _process_module_builtin_defaults():
20992103
"instrument_langchain_core_runnables_config",
21002104
)
21012105
_process_module_definition(
2102-
"langchain.chains.base", "newrelic.hooks.mlmodel_langchain", "instrument_langchain_chains_base"
2106+
"langchain_core.tools.structured",
2107+
"newrelic.hooks.mlmodel_langchain",
2108+
"instrument_langchain_core_tools_structured",
21032109
)
2110+
21042111
_process_module_definition(
2105-
"langchain_classic.chains.base", "newrelic.hooks.mlmodel_langchain", "instrument_langchain_chains_base"
2112+
"langchain.agents.factory", "newrelic.hooks.mlmodel_langchain", "instrument_langchain_agents_factory"
21062113
)
21072114
_process_module_definition(
2108-
"langchain_core.callbacks.manager", "newrelic.hooks.mlmodel_langchain", "instrument_langchain_callbacks_manager"
2115+
"langchain.chains.base", "newrelic.hooks.mlmodel_langchain", "instrument_langchain_chains_base"
2116+
)
2117+
_process_module_definition(
2118+
"langchain_classic.chains.base", "newrelic.hooks.mlmodel_langchain", "instrument_langchain_chains_base"
21092119
)
21102120

21112121
# VectorStores with similarity_search method
@@ -2671,10 +2681,6 @@ def _process_module_builtin_defaults():
26712681
"langchain_core.tools", "newrelic.hooks.mlmodel_langchain", "instrument_langchain_core_tools"
26722682
)
26732683

2674-
_process_module_definition(
2675-
"langchain_core.callbacks.manager", "newrelic.hooks.mlmodel_langchain", "instrument_langchain_callbacks_manager"
2676-
)
2677-
26782684
_process_module_definition("asgiref.sync", "newrelic.hooks.adapter_asgiref", "instrument_asgiref_sync")
26792685

26802686
_process_module_definition(

newrelic/hooks/external_botocore.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1070,7 +1070,7 @@ def __next__(self):
10701070
return return_val
10711071

10721072
def close(self):
1073-
return super().close()
1073+
return self.__wrapped__.close()
10741074

10751075

10761076
class AsyncEventStreamWrapper(ObjectProxy):
@@ -1108,7 +1108,7 @@ async def __anext__(self):
11081108
return return_val
11091109

11101110
async def aclose(self):
1111-
return await super().aclose()
1111+
return await self.__wrapped__.aclose()
11121112

11131113

11141114
def handle_embedding_event(transaction, bedrock_attrs):

0 commit comments

Comments
 (0)