Skip to content

Commit bce8be6

Browse files
ericapisaniclaude
andcommitted
feat(langchain): Change LLM span operation to generate_text and capture agent name
Update the LangChain integration to track LLM operations as gen_ai.generate_text instead of gen_ai.pipeline. This provides more specific operation semantics for LLM invocations. Additionally, capture the agent name when an LLM is invoked within an agent context using _push_agent/_pop_agent tracking. - Change span operation from OP.GEN_AI_PIPELINE to OP.GEN_AI_GENERATE_TEXT - Update span name to include model information - Add GEN_AI_OPERATION_NAME span data - Add GEN_AI_AGENT_NAME span data when agent context is active - Add GEN_AI_PIPELINE_NAME span data when pipeline name is provided - Update tests to check for new operation type and span data fields Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
1 parent dc65e13 commit bce8be6

File tree

2 files changed

+127
-6
lines changed

2 files changed

+127
-6
lines changed

sentry_sdk/integrations/langchain.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -366,15 +366,25 @@ def on_llm_start(
366366
or ""
367367
)
368368

369+
pipeline_name = kwargs.get("name")
369370
watched_span = self._create_span(
370371
run_id,
371372
parent_run_id,
372-
op=OP.GEN_AI_PIPELINE,
373-
name=kwargs.get("name") or "Langchain LLM call",
373+
op=OP.GEN_AI_GENERATE_TEXT,
374+
name=f"generate_text {model}".strip(),
374375
origin=LangchainIntegration.origin,
375376
)
376377
span = watched_span.span
377378

379+
span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "generate_text")
380+
381+
agent_name = _get_current_agent()
382+
if agent_name:
383+
span.set_data(SPANDATA.GEN_AI_AGENT_NAME, agent_name)
384+
385+
if pipeline_name:
386+
span.set_data(SPANDATA.GEN_AI_PIPELINE_NAME, pipeline_name)
387+
378388
if model:
379389
span.set_data(
380390
SPANDATA.GEN_AI_REQUEST_MODEL,

tests/integrations/langchain/test_langchain.py

Lines changed: 115 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727
SentryLangchainCallback,
2828
_transform_langchain_content_block,
2929
_transform_langchain_message_content,
30+
_push_agent,
31+
_pop_agent,
3032
)
3133

3234
try:
@@ -851,12 +853,15 @@ def test_langchain_integration_with_langchain_core_only(sentry_init, capture_eve
851853
assert tx["type"] == "transaction"
852854

853855
llm_spans = [
854-
span for span in tx.get("spans", []) if span.get("op") == "gen_ai.pipeline"
856+
span
857+
for span in tx.get("spans", [])
858+
if span.get("op") == "gen_ai.generate_text"
855859
]
856860
assert len(llm_spans) > 0
857861

858862
llm_span = llm_spans[0]
859-
assert llm_span["description"] == "Langchain LLM call"
863+
assert llm_span["description"] == "generate_text gpt-3.5-turbo"
864+
assert llm_span["data"]["gen_ai.operation.name"] == "generate_text"
860865
assert llm_span["data"]["gen_ai.request.model"] == "gpt-3.5-turbo"
861866
assert (
862867
llm_span["data"]["gen_ai.response.text"]
@@ -867,6 +872,110 @@ def test_langchain_integration_with_langchain_core_only(sentry_init, capture_eve
867872
assert llm_span["data"]["gen_ai.usage.output_tokens"] == 15
868873

869874

875+
def test_langchain_llm_span_includes_agent_name(sentry_init, capture_events):
876+
from langchain_core.outputs import LLMResult, Generation
877+
878+
with patch("sentry_sdk.integrations.langchain.AgentExecutor", None):
879+
from sentry_sdk.integrations.langchain import (
880+
LangchainIntegration,
881+
SentryLangchainCallback,
882+
)
883+
884+
sentry_init(
885+
integrations=[LangchainIntegration(include_prompts=True)],
886+
traces_sample_rate=1.0,
887+
send_default_pii=True,
888+
)
889+
events = capture_events()
890+
891+
callback = SentryLangchainCallback(max_span_map_size=100, include_prompts=True)
892+
893+
run_id = "12345678-1234-1234-1234-123456789abc"
894+
serialized = {"_type": "openai", "model_name": "gpt-3.5-turbo"}
895+
prompts = ["Hello"]
896+
897+
with start_transaction():
898+
_push_agent("test-agent")
899+
try:
900+
callback.on_llm_start(
901+
serialized=serialized,
902+
prompts=prompts,
903+
run_id=run_id,
904+
invocation_params={"model": "gpt-3.5-turbo"},
905+
)
906+
907+
response = LLMResult(
908+
generations=[[Generation(text="Hi")]],
909+
llm_output={},
910+
)
911+
callback.on_llm_end(response=response, run_id=run_id)
912+
finally:
913+
_pop_agent()
914+
915+
assert len(events) > 0
916+
tx = events[0]
917+
918+
llm_spans = [
919+
span
920+
for span in tx.get("spans", [])
921+
if span.get("op") == "gen_ai.generate_text"
922+
]
923+
assert len(llm_spans) == 1
924+
925+
llm_span = llm_spans[0]
926+
assert llm_span["data"][SPANDATA.GEN_AI_AGENT_NAME] == "test-agent"
927+
928+
929+
def test_langchain_llm_span_no_agent_name_when_no_agent(sentry_init, capture_events):
930+
from langchain_core.outputs import LLMResult, Generation
931+
932+
with patch("sentry_sdk.integrations.langchain.AgentExecutor", None):
933+
from sentry_sdk.integrations.langchain import (
934+
LangchainIntegration,
935+
SentryLangchainCallback,
936+
)
937+
938+
sentry_init(
939+
integrations=[LangchainIntegration(include_prompts=True)],
940+
traces_sample_rate=1.0,
941+
send_default_pii=True,
942+
)
943+
events = capture_events()
944+
945+
callback = SentryLangchainCallback(max_span_map_size=100, include_prompts=True)
946+
947+
run_id = "12345678-1234-1234-1234-123456789def"
948+
serialized = {"_type": "openai", "model_name": "gpt-3.5-turbo"}
949+
prompts = ["Hello"]
950+
951+
with start_transaction():
952+
callback.on_llm_start(
953+
serialized=serialized,
954+
prompts=prompts,
955+
run_id=run_id,
956+
invocation_params={"model": "gpt-3.5-turbo"},
957+
)
958+
959+
response = LLMResult(
960+
generations=[[Generation(text="Hi")]],
961+
llm_output={},
962+
)
963+
callback.on_llm_end(response=response, run_id=run_id)
964+
965+
assert len(events) > 0
966+
tx = events[0]
967+
968+
llm_spans = [
969+
span
970+
for span in tx.get("spans", [])
971+
if span.get("op") == "gen_ai.generate_text"
972+
]
973+
assert len(llm_spans) == 1
974+
975+
llm_span = llm_spans[0]
976+
assert SPANDATA.GEN_AI_AGENT_NAME not in llm_span["data"]
977+
978+
870979
def test_langchain_message_role_mapping(sentry_init, capture_events):
871980
"""Test that message roles are properly normalized in langchain integration."""
872981
global llm_type
@@ -1062,11 +1171,12 @@ def test_langchain_message_truncation(sentry_init, capture_events):
10621171
assert tx["type"] == "transaction"
10631172

10641173
llm_spans = [
1065-
span for span in tx.get("spans", []) if span.get("op") == "gen_ai.pipeline"
1174+
span for span in tx.get("spans", []) if span.get("op") == "gen_ai.generate_text"
10661175
]
10671176
assert len(llm_spans) > 0
10681177

10691178
llm_span = llm_spans[0]
1179+
assert llm_span["data"]["gen_ai.operation.name"] == "generate_text"
10701180
assert SPANDATA.GEN_AI_REQUEST_MESSAGES in llm_span["data"]
10711181

10721182
messages_data = llm_span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES]
@@ -1776,11 +1886,12 @@ def test_langchain_response_model_extraction(
17761886
assert tx["type"] == "transaction"
17771887

17781888
llm_spans = [
1779-
span for span in tx.get("spans", []) if span.get("op") == "gen_ai.pipeline"
1889+
span for span in tx.get("spans", []) if span.get("op") == "gen_ai.generate_text"
17801890
]
17811891
assert len(llm_spans) > 0
17821892

17831893
llm_span = llm_spans[0]
1894+
assert llm_span["data"]["gen_ai.operation.name"] == "generate_text"
17841895

17851896
if expected_model is not None:
17861897
assert SPANDATA.GEN_AI_RESPONSE_MODEL in llm_span["data"]

0 commit comments

Comments
 (0)