Skip to content

Commit 95442cd

Browse files
committed
prevent duplicate work and simplify metric handler
1 parent ba4b5dd commit 95442cd

4 files changed

Lines changed: 101 additions & 130 deletions

File tree

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

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from ldai import log
77
from ldai.agent_graph import AgentGraphDefinition, AgentGraphNode
88
from ldai.providers import AgentGraphRunner, ToolRegistry
9-
from ldai.providers.types import AgentGraphRunnerResult, GraphMetrics, LDAIMetrics
9+
from ldai.providers.types import AgentGraphRunnerResult, GraphMetrics
1010

1111
from ldai_langchain.langchain_helper import (
1212
build_structured_tools,
@@ -304,18 +304,7 @@ async def run(self, input: Any) -> AgentGraphRunnerResult:
304304
output = extract_last_message_content(messages)
305305
total_usage = sum_token_usage_from_messages(messages)
306306

307-
# Build per-node LDAIMetrics from callback handler data
308-
node_metrics: Dict[str, LDAIMetrics] = {}
309-
for node_key in handler.path:
310-
usage = handler.node_tokens.get(node_key)
311-
duration = handler.node_durations_ms.get(node_key)
312-
tool_calls = handler.node_tool_calls.get(node_key) or []
313-
node_metrics[node_key] = LDAIMetrics(
314-
success=True,
315-
usage=usage,
316-
duration_ms=duration,
317-
tool_calls=tool_calls if tool_calls else None,
318-
)
307+
node_metrics = handler.node_metrics
319308

320309
return AgentGraphRunnerResult(
321310
content=output,

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

Lines changed: 26 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,9 @@ class LDMetricsCallbackHandler(BaseCallbackHandler):
2020
LangChain callback handler that collects per-node metrics during a LangGraph run.
2121
2222
Records token usage, tool calls, and duration for each agent node in the graph.
23-
Call ``collect_node_metrics()`` after the run completes to retrieve the accumulated
24-
per-node metrics for use by the managed layer.
23+
Each node's :class:`~ldai.providers.types.LDAIMetrics` is built incrementally
24+
as callbacks fire. Access the ``node_metrics`` property after the run completes
25+
to retrieve the accumulated per-node metrics.
2526
"""
2627

2728
def __init__(self, node_keys: Set[str], fn_name_to_config_key: Dict[str, str]):
@@ -39,14 +40,10 @@ def __init__(self, node_keys: Set[str], fn_name_to_config_key: Dict[str, str]):
3940

4041
# run_id -> node_key for active chain runs
4142
self._run_to_node: Dict[UUID, str] = {}
42-
# accumulated token usage per node
43-
self._node_tokens: Dict[str, TokenUsage] = {}
44-
# tool config keys called per node
45-
self._node_tool_calls: Dict[str, List[str]] = {}
4643
# start time (ns) per active run_id — keyed by run_id to handle re-entrant nodes
4744
self._node_start_ns: Dict[UUID, int] = {}
48-
# accumulated duration (ms) per node
49-
self._node_duration_ms: Dict[str, int] = {}
45+
# per-node metrics, built incrementally as callbacks fire
46+
self._node_metrics: Dict[str, LDAIMetrics] = {}
5047
# execution path in order (deduplicated)
5148
self._path: List[str] = []
5249
self._path_set: Set[str] = set()
@@ -61,19 +58,9 @@ def path(self) -> List[str]:
6158
return list(self._path)
6259

6360
@property
64-
def node_tokens(self) -> Dict[str, TokenUsage]:
65-
"""Accumulated token usage per node key."""
66-
return dict(self._node_tokens)
67-
68-
@property
69-
def node_tool_calls(self) -> Dict[str, List[str]]:
70-
"""Tool config keys called per node key."""
71-
return {k: list(v) for k, v in self._node_tool_calls.items()}
72-
73-
@property
74-
def node_durations_ms(self) -> Dict[str, int]:
75-
"""Accumulated duration in milliseconds per node key."""
76-
return dict(self._node_duration_ms)
61+
def node_metrics(self) -> Dict[str, LDAIMetrics]:
62+
"""Per-node metrics keyed by node key."""
63+
return dict(self._node_metrics)
7764

7865
# ------------------------------------------------------------------
7966
# Callbacks
@@ -101,10 +88,10 @@ def on_chain_start(
10188
if name not in self._path_set:
10289
self._path.append(name)
10390
self._path_set.add(name)
91+
self._node_metrics[name] = LDAIMetrics(success=False)
10492
elif name.endswith('__tools'):
10593
stripped = name[: -len('__tools')]
10694
if stripped in self._node_keys:
107-
# Attribute tool events to the owning agent node
10895
self._run_to_node[run_id] = stripped
10996

11097
def on_chain_end(
@@ -121,9 +108,10 @@ def on_chain_end(
121108
start_ns = self._node_start_ns.pop(run_id, None)
122109
if start_ns is not None:
123110
elapsed_ms = (time.perf_counter_ns() - start_ns) // 1_000_000
124-
self._node_duration_ms[node_key] = (
125-
self._node_duration_ms.get(node_key, 0) + elapsed_ms
126-
)
111+
metrics = self._node_metrics.get(node_key)
112+
if metrics is not None:
113+
metrics.success = True
114+
metrics.duration_ms = (metrics.duration_ms or 0) + elapsed_ms
127115

128116
def on_llm_end(
129117
self,
@@ -151,11 +139,14 @@ def on_llm_end(
151139
if usage is None:
152140
return
153141

154-
existing = self._node_tokens.get(node_key)
142+
metrics = self._node_metrics.get(node_key)
143+
if metrics is None:
144+
return
145+
existing = metrics.usage
155146
if existing is None:
156-
self._node_tokens[node_key] = usage
147+
metrics.usage = usage
157148
else:
158-
self._node_tokens[node_key] = TokenUsage(
149+
metrics.usage = TokenUsage(
159150
total=existing.total + usage.total,
160151
input=existing.input + usage.input,
161152
output=existing.output + usage.output,
@@ -179,32 +170,11 @@ def on_tool_end(
179170

180171
config_key = self._fn_name_to_config_key.get(name)
181172
if config_key is None:
182-
# Tool is not a registered functional tool (e.g. a handoff tool) — skip tracking.
183173
return
184-
if node_key not in self._node_tool_calls:
185-
self._node_tool_calls[node_key] = []
186-
self._node_tool_calls[node_key].append(config_key)
187-
188-
def collect_node_metrics(self) -> Dict[str, LDAIMetrics]:
189-
"""
190-
Build a per-node ``LDAIMetrics`` map from data collected during the run.
191-
192-
Pure data extraction — no LaunchDarkly tracker events are emitted.
193-
:class:`LangGraphAgentGraphRunner` uses this to populate
194-
``GraphMetrics.node_metrics`` so the managed layer can drive per-node
195-
events.
196-
197-
:return: Mapping of node key to its accumulated ``LDAIMetrics``.
198-
"""
199-
node_metrics: Dict[str, LDAIMetrics] = {}
200-
for node_key in self._path:
201-
if node_key in node_metrics:
202-
continue
203-
tool_calls = self._node_tool_calls.get(node_key, [])
204-
node_metrics[node_key] = LDAIMetrics(
205-
success=True,
206-
usage=self._node_tokens.get(node_key),
207-
tool_calls=list(tool_calls) if tool_calls else None,
208-
duration_ms=self._node_duration_ms.get(node_key),
209-
)
210-
return node_metrics
174+
metrics = self._node_metrics.get(node_key)
175+
if metrics is None:
176+
return
177+
if metrics.tool_calls is None:
178+
metrics.tool_calls = [config_key]
179+
else:
180+
metrics.tool_calls.append(config_key)

0 commit comments

Comments
 (0)