Skip to content

Commit 20fff24

Browse files
authored
feat!: Move graph_key to AIConfigTracker instantiation (#134)
1 parent af4e463 commit 20fff24

10 files changed

Lines changed: 147 additions & 114 deletions

File tree

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -300,7 +300,7 @@ async def run(self, input: Any) -> AgentGraphResult:
300300
output = extract_last_message_content(messages)
301301

302302
# Flush per-node metrics to LD trackers
303-
handler.flush(self._graph, tracker)
303+
handler.flush(self._graph)
304304

305305
# Graph-level metrics
306306
if tracker:

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

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -188,16 +188,14 @@ def on_tool_end(
188188
# Flush
189189
# ------------------------------------------------------------------
190190

191-
def flush(self, graph: AgentGraphDefinition, graph_tracker: Any) -> None:
191+
def flush(self, graph: AgentGraphDefinition) -> None:
192192
"""
193193
Emit all collected per-node metrics to the LaunchDarkly trackers.
194194
195195
Call this once after the graph run completes.
196196
197197
:param graph: The AgentGraphDefinition whose nodes hold the LD config trackers.
198-
:param graph_tracker: The AIGraphTracker for the overall graph (may be None).
199198
"""
200-
gk = graph_tracker.graph_key if graph_tracker is not None else None
201199
for node_key in self._path:
202200
node = graph.get_node(node_key)
203201
if not node:
@@ -208,13 +206,13 @@ def flush(self, graph: AgentGraphDefinition, graph_tracker: Any) -> None:
208206

209207
usage = self._node_tokens.get(node_key)
210208
if usage:
211-
config_tracker.track_tokens(usage, graph_key=gk)
209+
config_tracker.track_tokens(usage)
212210

213211
duration = self._node_duration_ms.get(node_key)
214212
if duration is not None:
215-
config_tracker.track_duration(duration, graph_key=gk)
213+
config_tracker.track_duration(duration)
216214

217-
config_tracker.track_success(graph_key=gk)
215+
config_tracker.track_success()
218216

219217
for tool_key in self._node_tool_calls.get(node_key, []):
220-
config_tracker.track_tool_call(tool_key, graph_key=gk)
218+
config_tracker.track_tool_call(tool_key)

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

Lines changed: 42 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ def _make_graph(mock_ld_client: MagicMock, node_key: str = 'root-agent', graph_k
3535
model_name='gpt-4',
3636
provider_name='openai',
3737
context=context,
38+
graph_key=graph_key,
3839
)
3940
graph_tracker = AIGraphTracker(
4041
ld_client=mock_ld_client,
@@ -325,7 +326,7 @@ def test_flush_emits_token_events_to_ld_tracker():
325326
node_run_id = uuid4()
326327
handler.on_chain_start({}, {}, run_id=node_run_id, name='root-agent')
327328
handler.on_llm_end(_llm_result(15, 10, 5), run_id=uuid4(), parent_run_id=node_run_id)
328-
handler.flush(graph, tracker)
329+
handler.flush(graph)
329330

330331
ev = _events(mock_ld_client)
331332
assert ev['$ld:ai:tokens:total'][0][1] == 15
@@ -344,7 +345,7 @@ def test_flush_emits_duration():
344345
run_id = uuid4()
345346
handler.on_chain_start({}, {}, run_id=run_id, name='root-agent')
346347
handler.on_chain_end({}, run_id=run_id)
347-
handler.flush(graph, tracker)
348+
handler.flush(graph)
348349

349350
ev = _events(mock_ld_client)
350351
assert '$ld:ai:duration:total' in ev
@@ -364,7 +365,7 @@ def test_flush_emits_tool_calls():
364365
tools_run_id = uuid4()
365366
handler.on_chain_start({}, {}, run_id=tools_run_id, name='root-agent__tools')
366367
handler.on_tool_end('r', run_id=uuid4(), parent_run_id=tools_run_id, name='fn_search')
367-
handler.flush(graph, tracker)
368+
handler.flush(graph)
368369

369370
ev = _events(mock_ld_client)
370371
tool_events = ev.get('$ld:ai:tool_call', [])
@@ -382,23 +383,54 @@ def test_flush_includes_graph_key_in_node_events():
382383
node_run_id = uuid4()
383384
handler.on_chain_start({}, {}, run_id=node_run_id, name='root-agent')
384385
handler.on_llm_end(_llm_result(5, 3, 2), run_id=uuid4(), parent_run_id=node_run_id)
385-
handler.flush(graph, tracker)
386+
handler.flush(graph)
386387

387388
ev = _events(mock_ld_client)
388389
token_data = ev['$ld:ai:tokens:total'][0][0]
389390
assert token_data.get('graphKey') == 'my-graph'
390391

391392

392-
def test_flush_with_none_tracker_uses_no_graph_key():
393-
"""flush() with graph_tracker=None does not fail and omits graphKey."""
393+
def test_flush_with_no_graph_key_on_node_tracker():
394+
"""When node tracker has no graph_key, events omit graphKey."""
394395
mock_ld_client = MagicMock()
395-
graph = _make_graph(mock_ld_client)
396+
context = MagicMock()
397+
node_tracker = LDAIConfigTracker(
398+
ld_client=mock_ld_client,
399+
variation_key='v1',
400+
config_key='root-agent',
401+
version=1,
402+
model_name='gpt-4',
403+
provider_name='openai',
404+
context=context,
405+
)
406+
node_config = AIAgentConfig(
407+
key='root-agent',
408+
enabled=True,
409+
model=ModelConfig(name='gpt-4', parameters={}),
410+
provider=ProviderConfig(name='openai'),
411+
instructions='Be helpful.',
412+
tracker=node_tracker,
413+
)
414+
graph_config = AIAgentGraphConfig(
415+
key='test-graph',
416+
root_config_key='root-agent',
417+
edges=[],
418+
enabled=True,
419+
)
420+
nodes = AgentGraphDefinition.build_nodes(graph_config, {'root-agent': node_config})
421+
graph = AgentGraphDefinition(
422+
agent_graph=graph_config,
423+
nodes=nodes,
424+
context=context,
425+
enabled=True,
426+
tracker=None,
427+
)
396428

397429
handler = LDMetricsCallbackHandler({'root-agent'}, {})
398430
node_run_id = uuid4()
399431
handler.on_chain_start({}, {}, run_id=node_run_id, name='root-agent')
400432
handler.on_llm_end(_llm_result(5, 3, 2), run_id=uuid4(), parent_run_id=node_run_id)
401-
handler.flush(graph, None) # graph_tracker=None
433+
handler.flush(graph)
402434

403435
ev = _events(mock_ld_client)
404436
token_data = ev['$ld:ai:tokens:total'][0][0]
@@ -413,7 +445,7 @@ def test_flush_skips_nodes_not_in_path():
413445

414446
# Handler with 'root-agent' in node_keys but never started
415447
handler = LDMetricsCallbackHandler({'root-agent'}, {})
416-
handler.flush(graph, tracker)
448+
handler.flush(graph)
417449

418450
ev = _events(mock_ld_client)
419451
assert '$ld:ai:tokens:total' not in ev
@@ -449,7 +481,7 @@ def test_flush_skips_node_without_tracker():
449481
node_run_id = uuid4()
450482
handler.on_chain_start({}, {}, run_id=node_run_id, name='no-track')
451483
handler.on_llm_end(_llm_result(5, 3, 2), run_id=uuid4(), parent_run_id=node_run_id)
452-
handler.flush(graph, None) # should not raise
484+
handler.flush(graph) # should not raise
453485

454486
mock_ld_client.track.assert_not_called()
455487

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

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ def _make_graph(
4545
model_name='gpt-4',
4646
provider_name='openai',
4747
context=context,
48+
graph_key=graph_key,
4849
)
4950

5051
graph_tracker = AIGraphTracker(
@@ -141,6 +142,7 @@ def _make_two_node_graph(mock_ld_client: MagicMock) -> 'AgentGraphDefinition':
141142
model_name='gpt-4',
142143
provider_name='openai',
143144
context=context,
145+
graph_key='two-node-graph',
144146
)
145147
child_tracker = LDAIConfigTracker(
146148
ld_client=mock_ld_client,
@@ -150,6 +152,7 @@ def _make_two_node_graph(mock_ld_client: MagicMock) -> 'AgentGraphDefinition':
150152
model_name='gpt-4',
151153
provider_name='openai',
152154
context=context,
155+
graph_key='two-node-graph',
153156
)
154157
graph_tracker = AIGraphTracker(
155158
ld_client=mock_ld_client,
@@ -240,7 +243,7 @@ async def test_tracks_node_and_graph_tokens_on_success():
240243
)
241244
handler.on_llm_end(llm_result, run_id=uuid4(), parent_run_id=node_run_id)
242245
handler.on_chain_end({}, run_id=node_run_id)
243-
handler.flush(graph2, tracker2)
246+
handler.flush(graph2)
244247

245248
ev2 = _events(mock_ld_client2)
246249
assert ev2['$ld:ai:tokens:total'][0][1] == 15
@@ -314,7 +317,7 @@ def get_weather(location: str = 'NYC') -> str:
314317
tools_run_id = uuid4()
315318
handler.on_chain_start({}, {}, run_id=tools_run_id, name='root-agent__tools')
316319
handler.on_tool_end('sunny', run_id=uuid4(), parent_run_id=tools_run_id, name='get_weather')
317-
handler.flush(graph2, tracker2)
320+
handler.flush(graph2)
318321

319322
ev2 = _events(mock_ld_client2)
320323
tool_events = ev2.get('$ld:ai:tool_call', [])
@@ -368,7 +371,7 @@ def summarize(text: str = '') -> str:
368371
handler.on_chain_start({}, {}, run_id=tools_run_id, name='root-agent__tools')
369372
handler.on_tool_end('result', run_id=uuid4(), parent_run_id=tools_run_id, name='search')
370373
handler.on_tool_end('summary', run_id=uuid4(), parent_run_id=tools_run_id, name='summarize')
371-
handler.flush(graph2, tracker2)
374+
handler.flush(graph2)
372375

373376
ev2 = _events(mock_ld_client2)
374377
tool_keys = [data['toolKey'] for data, _ in ev2.get('$ld:ai:tool_call', [])]
@@ -399,7 +402,7 @@ async def test_tracks_graph_key_on_node_events():
399402
llm_output={},
400403
)
401404
handler.on_llm_end(llm_result, run_id=uuid4(), parent_run_id=node_run_id)
402-
handler.flush(graph, tracker)
405+
handler.flush(graph)
403406

404407
ev = _events(mock_ld_client)
405408
token_data = ev['$ld:ai:tokens:total'][0][0]
@@ -484,7 +487,7 @@ def model_factory(node_config, **kwargs):
484487
)
485488
handler.on_llm_end(child_llm_result, run_id=uuid4(), parent_run_id=child_run_id)
486489

487-
handler.flush(graph2, tracker2)
490+
handler.flush(graph2)
488491

489492
ev2 = _events(mock_ld_client2)
490493

@@ -517,6 +520,7 @@ def _node_tracker(key: str) -> LDAIConfigTracker:
517520
model_name='gpt-4',
518521
provider_name='openai',
519522
context=context,
523+
graph_key='multi-child-graph',
520524
)
521525

522526
graph_tracker = AIGraphTracker(
@@ -627,6 +631,7 @@ def _node_tracker(key: str) -> LDAIConfigTracker:
627631
model_name='gpt-4',
628632
provider_name='openai',
629633
context=context,
634+
graph_key='multi-child-tools-graph',
630635
)
631636

632637
graph_tracker = AIGraphTracker(

packages/ai-providers/server-ai-openai/src/ldai_openai/openai_agent_graph_runner.py

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -82,8 +82,8 @@ async def run(self, input: Any) -> AgentGraphResult:
8282
from agents import Runner
8383
root_agent = self._build_agents(path, state)
8484
result = await Runner.run(root_agent, str(input))
85-
self._flush_final_segment(state, tracker, result)
86-
self._track_tool_calls(result, tracker)
85+
self._flush_final_segment(state, result)
86+
self._track_tool_calls(result)
8787

8888
duration = (time.perf_counter_ns() - start_ns) // 1_000_000
8989

@@ -248,18 +248,16 @@ def _handle_handoff(
248248
except Exception:
249249
pass
250250

251-
gk = tracker.graph_key if tracker is not None else None
252251
if config_tracker is not None:
253252
if usage is not None:
254-
config_tracker.track_tokens(usage, graph_key=gk)
253+
config_tracker.track_tokens(usage)
255254
if duration_ms is not None:
256-
config_tracker.track_duration(int(duration_ms), graph_key=gk)
257-
config_tracker.track_success(graph_key=gk)
255+
config_tracker.track_duration(int(duration_ms))
256+
config_tracker.track_success()
258257

259258
def _flush_final_segment(
260259
self,
261260
state: _RunState,
262-
tracker: Any,
263261
result: Any,
264262
) -> None:
265263
"""Record duration/tokens for the last active agent (no handoff after it)."""
@@ -283,15 +281,13 @@ def _flush_final_segment(
283281
except Exception:
284282
pass
285283

286-
gk = tracker.graph_key if tracker is not None else None
287284
if usage is not None:
288-
config_tracker.track_tokens(usage, graph_key=gk)
289-
config_tracker.track_duration(int(duration_ms), graph_key=gk)
290-
config_tracker.track_success(graph_key=gk)
285+
config_tracker.track_tokens(usage)
286+
config_tracker.track_duration(int(duration_ms))
287+
config_tracker.track_success()
291288

292-
def _track_tool_calls(self, result: Any, tracker: Any) -> None:
289+
def _track_tool_calls(self, result: Any) -> None:
293290
"""Track all tool calls from the run result, attributed to the node that called them."""
294-
gk = tracker.graph_key if tracker is not None else None
295291
for agent_name, tool_fn_name in get_tool_calls_from_run_items(result.new_items):
296292
agent_key = self._agent_name_map.get(agent_name, agent_name)
297293
tool_name = self._tool_name_map.get(tool_fn_name)
@@ -302,4 +298,4 @@ def _track_tool_calls(self, result: Any, tracker: Any) -> None:
302298
continue
303299
config_tracker = node.get_config().tracker
304300
if config_tracker is not None:
305-
config_tracker.track_tool_call(tool_name, graph_key=gk)
301+
config_tracker.track_tool_call(tool_name)

packages/ai-providers/server-ai-openai/tests/test_tracking_openai_agents.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ def _make_graph(
4040
model_name='gpt-4',
4141
provider_name='openai',
4242
context=context,
43+
graph_key=graph_key,
4344
)
4445

4546
graph_tracker = AIGraphTracker(
@@ -178,6 +179,7 @@ def _make_two_node_graph(mock_ld_client: MagicMock) -> AgentGraphDefinition:
178179
model_name='gpt-4',
179180
provider_name='openai',
180181
context=context,
182+
graph_key='two-node-graph',
181183
)
182184
child_tracker = LDAIConfigTracker(
183185
ld_client=mock_ld_client,
@@ -187,6 +189,7 @@ def _make_two_node_graph(mock_ld_client: MagicMock) -> AgentGraphDefinition:
187189
model_name='gpt-4',
188190
provider_name='openai',
189191
context=context,
192+
graph_key='two-node-graph',
190193
)
191194
graph_tracker = AIGraphTracker(
192195
ld_client=mock_ld_client,

packages/sdk/server-ai/src/ldai/client.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -606,9 +606,12 @@ def agent_graph(
606606
for single_edge in variation.get("edges", {}).get(edge_key, []):
607607
all_agent_keys.add(single_edge.get("key", ""))
608608

609+
graph_key_value = key
609610
agent_configs = {
610-
key: self.agent_config(key, context, AIAgentConfigDefault(enabled=False))
611-
for key in all_agent_keys
611+
agent_key: self.__evaluate_agent(
612+
agent_key, context, AIAgentConfigDefault(enabled=False), graph_key=graph_key_value
613+
)
614+
for agent_key in all_agent_keys
612615
}
613616

614617
if not all(config.enabled for config in agent_configs.values()):
@@ -748,6 +751,7 @@ def __evaluate(
748751
context: Context,
749752
default_dict: Dict[str, Any],
750753
variables: Optional[Dict[str, Any]] = None,
754+
graph_key: Optional[str] = None,
751755
) -> Tuple[
752756
Optional[ModelConfig], Optional[ProviderConfig], Optional[List[LDMessage]],
753757
Optional[str], LDAIConfigTracker, bool, Optional[Any], Dict[str, Any]
@@ -759,6 +763,7 @@ def __evaluate(
759763
:param context: The evaluation context.
760764
:param default_dict: Default configuration as dictionary.
761765
:param variables: Variables for interpolation.
766+
:param graph_key: When set, passed to the tracker so all events include ``graphKey``.
762767
:return: Tuple of (model, provider, messages, instructions, tracker, enabled, judge_configuration, variation).
763768
"""
764769
variation = self._client.variation(key, context, default_dict)
@@ -809,6 +814,7 @@ def __evaluate(
809814
model.name if model else '',
810815
provider_config.name if provider_config else '',
811816
context,
817+
graph_key=graph_key,
812818
)
813819

814820
enabled = variation.get('_ldMeta', {}).get('enabled', False)
@@ -836,6 +842,7 @@ def __evaluate_agent(
836842
context: Context,
837843
default: AIAgentConfigDefault,
838844
variables: Optional[Dict[str, Any]] = None,
845+
graph_key: Optional[str] = None,
839846
) -> AIAgentConfig:
840847
"""
841848
Internal method to evaluate an agent configuration.
@@ -844,10 +851,11 @@ def __evaluate_agent(
844851
:param context: The evaluation context.
845852
:param default: Default agent values.
846853
:param variables: Variables for interpolation.
854+
:param graph_key: When set, passed to the tracker so all events include ``graphKey``.
847855
:return: Configured AIAgentConfig instance.
848856
"""
849857
model, provider, messages, instructions, tracker, enabled, judge_configuration, _ = self.__evaluate(
850-
key, context, default.to_dict(), variables
858+
key, context, default.to_dict(), variables, graph_key=graph_key
851859
)
852860

853861
# For agents, prioritize instructions over messages

0 commit comments

Comments
 (0)