Skip to content

Commit e57a147

Browse files
jsonbaileyclaude
andcommitted
feat: improve graph runner tracking with node metrics, path, and token rollup
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 99b4024 commit e57a147

4 files changed

Lines changed: 185 additions & 66 deletions

File tree

packages/ai-providers/server-ai-langchain/src/ldai_langchain/langgraph_agent_graph_runner.py

Lines changed: 44 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,22 @@
44
import time
55
from typing import Annotated, Any, List
66

7+
from ldai import log
78
from ldai.agent_graph import AgentGraphDefinition, AgentGraphNode
89
from ldai.providers.types import LDAIMetrics
910
from ldai.runners.agent_graph_runner import AgentGraphRunner
1011
from ldai.runners.types import AgentGraphResult, ToolRegistry
1112

13+
from ldai_langchain.langchain_helper import LangChainHelper
14+
1215

1316
class LangGraphAgentGraphRunner(AgentGraphRunner):
1417
"""
1518
AgentGraphRunner implementation for LangGraph.
1619
17-
Builds a LangGraph StateGraph from an AgentGraphDefinition and
18-
ToolRegistry via traverse(), compiles it, and executes it with
19-
ainvoke(). Auto-tracks latency and invocation success/failure via
20-
the graph's AIGraphTracker.
20+
Compiles and runs the agent graph with LangGraph and automatically records
21+
graph- and node-level AI metric data to the LaunchDarkly trackers on the
22+
graph definition and each node.
2123
2224
Requires ``langgraph`` to be installed.
2325
"""
@@ -43,18 +45,12 @@ async def run(self, input: Any) -> AgentGraphResult:
4345
:return: AgentGraphResult with the final output and metrics
4446
"""
4547
tracker = self._graph.get_tracker()
46-
start_time = time.time()
48+
start_ns = time.perf_counter_ns()
4749
try:
48-
try:
49-
from langchain.chat_models import init_chat_model
50-
from langchain_core.messages import AnyMessage, HumanMessage
51-
from langgraph.graph import END, START, StateGraph
52-
from typing_extensions import TypedDict
53-
except ImportError as exc:
54-
raise ImportError(
55-
"langgraph is required for LangGraphAgentGraphRunner. "
56-
"Install it with: pip install langgraph"
57-
) from exc
50+
from langchain.chat_models import init_chat_model
51+
from langchain_core.messages import AnyMessage, HumanMessage
52+
from langgraph.graph import END, START, StateGraph
53+
from typing_extensions import TypedDict
5854

5955
class WorkflowState(TypedDict):
6056
messages: Annotated[List[AnyMessage], operator.add]
@@ -63,10 +59,12 @@ class WorkflowState(TypedDict):
6359
root_node = self._graph.root()
6460
root_key = root_node.get_key() if root_node else None
6561
tools_ref = self._tools
62+
exec_path: List[str] = []
6663

6764
def handle_traversal(node: AgentGraphNode, ctx: dict) -> None:
6865
node_config = node.get_config()
6966
node_key = node.get_key()
67+
node_tracker = node_config.tracker
7068

7169
model = None
7270
if node_config.model:
@@ -82,10 +80,24 @@ def handle_traversal(node: AgentGraphNode, ctx: dict) -> None:
8280
model = lc_model
8381

8482
def invoke(state: WorkflowState) -> WorkflowState:
85-
if model:
83+
exec_path.append(node_key)
84+
if not model:
85+
return state
86+
gk = tracker.graph_key if tracker is not None else None
87+
if node_tracker:
88+
response = node_tracker.track_metrics_of(
89+
lambda: model.invoke(state['messages']),
90+
LangChainHelper.get_ai_metrics_from_response,
91+
graph_key=gk,
92+
)
93+
node_tracker.track_tool_calls(
94+
LangChainHelper.get_tool_calls_from_response(response),
95+
graph_key=tracker.graph_key if tracker is not None else None,
96+
)
97+
else:
8698
response = model.invoke(state['messages'])
87-
return {'messages': [response]}
88-
return state
99+
100+
return {'messages': [response]}
89101

90102
invoke.__name__ = node_key
91103

@@ -108,7 +120,7 @@ def invoke(state: WorkflowState) -> WorkflowState:
108120
result = await compiled.ainvoke(
109121
{'messages': [HumanMessage(content=str(input))]}
110122
)
111-
duration = int((time.time() - start_time) * 1000)
123+
duration = (time.perf_counter_ns() - start_ns) // 1_000_000
112124

113125
output = ''
114126
messages = result.get('messages', [])
@@ -118,16 +130,27 @@ def invoke(state: WorkflowState) -> WorkflowState:
118130
output = str(last.content)
119131

120132
if tracker:
133+
tracker.track_path(exec_path)
121134
tracker.track_latency(duration)
122135
tracker.track_invocation_success()
136+
tracker.track_total_tokens(
137+
LangChainHelper.sum_token_usage_from_messages(messages)
138+
)
123139

124140
return AgentGraphResult(
125141
output=output,
126142
raw=result,
127143
metrics=LDAIMetrics(success=True),
128144
)
129-
except Exception:
130-
duration = int((time.time() - start_time) * 1000)
145+
except Exception as exc:
146+
if isinstance(exc, ImportError):
147+
log.warning(
148+
"langgraph is required for LangGraphAgentGraphRunner. "
149+
"Install it with: pip install langgraph"
150+
)
151+
else:
152+
log.warning(f'LangGraphAgentGraphRunner run failed: {exc}')
153+
duration = (time.perf_counter_ns() - start_ns) // 1_000_000
131154
if tracker:
132155
tracker.track_latency(duration)
133156
tracker.track_invocation_failure()

packages/ai-providers/server-ai-langchain/tests/test_langgraph_agent_graph_runner.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,8 @@ async def test_langgraph_runner_run_success():
9696

9797
mock_message = MagicMock()
9898
mock_message.content = "langgraph answer"
99+
mock_message.usage_metadata = None
100+
mock_message.response_metadata = None
99101

100102
mock_compiled = MagicMock()
101103
mock_compiled.ainvoke = AsyncMock(return_value={'messages': [mock_message]})
@@ -115,8 +117,17 @@ async def test_langgraph_runner_run_success():
115117
mock_lc_core_messages.HumanMessage = MagicMock(return_value=mock_human_message)
116118
mock_lc_core_messages.AnyMessage = MagicMock()
117119

120+
mock_model_response = MagicMock()
121+
mock_model_response.content = 'langgraph answer'
122+
mock_model_response.usage_metadata = None
123+
mock_model_response.response_metadata = None
124+
mock_model_response.tool_calls = None
125+
126+
mock_llm = MagicMock()
127+
mock_llm.invoke = MagicMock(return_value=mock_model_response)
128+
118129
mock_init_model = MagicMock()
119-
mock_init_model.return_value = MagicMock()
130+
mock_init_model.return_value = mock_llm
120131
mock_langchain_chat = MagicMock()
121132
mock_langchain_chat.init_chat_model = mock_init_model
122133

@@ -135,5 +146,6 @@ async def test_langgraph_runner_run_success():
135146
assert isinstance(result, AgentGraphResult)
136147
assert result.output == "langgraph answer"
137148
assert result.metrics.success is True
149+
tracker.track_path.assert_called_once_with([])
138150
tracker.track_invocation_success.assert_called_once()
139151
tracker.track_latency.assert_called_once()

0 commit comments

Comments
 (0)