Skip to content

Commit b6d188c

Browse files
jsonbaileyclaude
andcommitted
feat!: Move graph_key to AIConfigTracker instantiation
graph_key is now set once at tracker construction time rather than passed as an optional parameter to every tracking method. When agent_graph() builds nodes it uses the internal __evaluate_agent() path so each node tracker is born with graph_key set to the parent graph's key. Public tracking methods no longer accept graph_key. BREAKING CHANGE: The graph_key parameter has been removed from all LDAIConfigTracker tracking methods (track_duration, track_success, track_error, track_tokens, track_tool_call, track_tool_calls, track_feedback, track_eval_scores, track_judge_response, track_time_to_first_token, track_metrics_of, track_metrics_of_async). Pass graph_key to the LDAIConfigTracker constructor instead. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent af4e463 commit b6d188c

4 files changed

Lines changed: 74 additions & 77 deletions

File tree

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

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -606,9 +606,10 @@ 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(agent_key, context, AIAgentConfigDefault(enabled=False), graph_key=graph_key_value)
612+
for agent_key in all_agent_keys
612613
}
613614

614615
if not all(config.enabled for config in agent_configs.values()):
@@ -748,6 +749,7 @@ def __evaluate(
748749
context: Context,
749750
default_dict: Dict[str, Any],
750751
variables: Optional[Dict[str, Any]] = None,
752+
graph_key: Optional[str] = None,
751753
) -> Tuple[
752754
Optional[ModelConfig], Optional[ProviderConfig], Optional[List[LDMessage]],
753755
Optional[str], LDAIConfigTracker, bool, Optional[Any], Dict[str, Any]
@@ -759,6 +761,7 @@ def __evaluate(
759761
:param context: The evaluation context.
760762
:param default_dict: Default configuration as dictionary.
761763
:param variables: Variables for interpolation.
764+
:param graph_key: When set, passed to the tracker so all events include ``graphKey``.
762765
:return: Tuple of (model, provider, messages, instructions, tracker, enabled, judge_configuration, variation).
763766
"""
764767
variation = self._client.variation(key, context, default_dict)
@@ -809,6 +812,7 @@ def __evaluate(
809812
model.name if model else '',
810813
provider_config.name if provider_config else '',
811814
context,
815+
graph_key=graph_key,
812816
)
813817

814818
enabled = variation.get('_ldMeta', {}).get('enabled', False)
@@ -836,6 +840,7 @@ def __evaluate_agent(
836840
context: Context,
837841
default: AIAgentConfigDefault,
838842
variables: Optional[Dict[str, Any]] = None,
843+
graph_key: Optional[str] = None,
839844
) -> AIAgentConfig:
840845
"""
841846
Internal method to evaluate an agent configuration.
@@ -844,10 +849,11 @@ def __evaluate_agent(
844849
:param context: The evaluation context.
845850
:param default: Default agent values.
846851
:param variables: Variables for interpolation.
852+
:param graph_key: When set, passed to the tracker so all events include ``graphKey``.
847853
:return: Configured AIAgentConfig instance.
848854
"""
849855
model, provider, messages, instructions, tracker, enabled, judge_configuration, _ = self.__evaluate(
850-
key, context, default.to_dict(), variables
856+
key, context, default.to_dict(), variables, graph_key=graph_key
851857
)
852858

853859
# For agents, prioritize instructions over messages

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

Lines changed: 40 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ def __init__(
7777
model_name: str,
7878
provider_name: str,
7979
context: Context,
80+
graph_key: Optional[str] = None,
8081
):
8182
"""
8283
Initialize an AI Config tracker.
@@ -88,6 +89,8 @@ def __init__(
8889
:param model_name: Name of the model used.
8990
:param provider_name: Name of the provider used.
9091
:param context: Context for evaluation.
92+
:param graph_key: When set, include ``graphKey`` in all event payloads
93+
(e.g. config-level metrics inside a graph).
9194
"""
9295
self._ld_client = ld_client
9396
self._variation_key = variation_key
@@ -96,13 +99,13 @@ def __init__(
9699
self._model_name = model_name
97100
self._provider_name = provider_name
98101
self._context = context
102+
self._graph_key = graph_key
99103
self._summary = LDAIMetricSummary()
100104

101-
def __get_track_data(self, graph_key: Optional[str] = None) -> dict:
105+
def __get_track_data(self) -> dict:
102106
"""
103107
Get tracking data for events.
104108
105-
:param graph_key: When set, include ``graphKey`` in the payload.
106109
:return: Dictionary containing variation and config keys.
107110
"""
108111
data = {
@@ -112,82 +115,72 @@ def __get_track_data(self, graph_key: Optional[str] = None) -> dict:
112115
"modelName": self._model_name,
113116
"providerName": self._provider_name,
114117
}
115-
if graph_key is not None:
116-
data['graphKey'] = graph_key
118+
if self._graph_key is not None:
119+
data['graphKey'] = self._graph_key
117120
return data
118121

119-
def track_duration(self, duration: int, *, graph_key: Optional[str] = None) -> None:
122+
def track_duration(self, duration: int) -> None:
120123
"""
121124
Manually track the duration of an AI operation.
122125
123126
:param duration: Duration in milliseconds.
124-
:param graph_key: When set, include ``graphKey`` in the event payload
125-
(e.g. config-level metrics inside a graph).
126127
"""
127128
self._summary._duration = duration
128129
self._ld_client.track(
129-
"$ld:ai:duration:total", self._context, self.__get_track_data(graph_key), duration
130+
"$ld:ai:duration:total", self._context, self.__get_track_data(), duration
130131
)
131132

132-
def track_time_to_first_token(
133-
self, time_to_first_token: int, *, graph_key: Optional[str] = None
134-
) -> None:
133+
def track_time_to_first_token(self, time_to_first_token: int) -> None:
135134
"""
136135
Manually track the time to first token of an AI operation.
137136
138137
:param time_to_first_token: Time to first token in milliseconds.
139-
:param graph_key: When set, include ``graphKey`` in the event payload.
140138
"""
141139
self._summary._time_to_first_token = time_to_first_token
142140
self._ld_client.track(
143141
"$ld:ai:tokens:ttf",
144142
self._context,
145-
self.__get_track_data(graph_key),
143+
self.__get_track_data(),
146144
time_to_first_token,
147145
)
148146

149-
def track_duration_of(self, func, *, graph_key: Optional[str] = None):
147+
def track_duration_of(self, func):
150148
"""
151149
Automatically track the duration of an AI operation.
152150
153151
An exception occurring during the execution of the function will still
154152
track the duration. The exception will be re-thrown.
155153
156154
:param func: Function to track (synchronous only).
157-
:param graph_key: When set, passed through to :meth:`track_duration`.
158155
:return: Result of the tracked function.
159156
"""
160157
start_ns = time.perf_counter_ns()
161158
try:
162159
result = func()
163160
finally:
164161
duration = (time.perf_counter_ns() - start_ns) // 1_000_000 # duration in milliseconds
165-
self.track_duration(duration, graph_key=graph_key)
162+
self.track_duration(duration)
166163

167164
return result
168165

169166
def _track_from_metrics_extractor(
170167
self,
171168
result: Any,
172169
metrics_extractor: Callable[[Any], Any],
173-
*,
174-
graph_key: Optional[str] = None,
175170
) -> Any:
176171
metrics = metrics_extractor(result)
177172
if metrics.success:
178-
self.track_success(graph_key=graph_key)
173+
self.track_success()
179174
else:
180-
self.track_error(graph_key=graph_key)
175+
self.track_error()
181176
if metrics.usage:
182-
self.track_tokens(metrics.usage, graph_key=graph_key)
177+
self.track_tokens(metrics.usage)
183178
return result
184179

185180
def track_metrics_of(
186181
self,
187182
func: Callable[[], Any],
188183
metrics_extractor: Callable[[Any], Any],
189-
*,
190-
graph_key: Optional[str] = None,
191184
) -> Any:
192185
"""
193186
Track metrics for a synchronous AI operation.
@@ -203,33 +196,29 @@ def track_metrics_of(
203196
204197
:param func: Synchronous callable that runs the operation
205198
:param metrics_extractor: Function that extracts LDAIMetrics from the operation result
206-
:param graph_key: When set, include ``graphKey`` on emitted config-level events.
207199
:return: The result of the operation
208200
"""
209201
start_ns = time.perf_counter_ns()
210202
try:
211203
result = func()
212204
except Exception as err:
213205
duration = (time.perf_counter_ns() - start_ns) // 1_000_000
214-
self.track_duration(duration, graph_key=graph_key)
215-
self.track_error(graph_key=graph_key)
206+
self.track_duration(duration)
207+
self.track_error()
216208
raise err
217209

218210
duration = (time.perf_counter_ns() - start_ns) // 1_000_000
219-
self.track_duration(duration, graph_key=graph_key)
220-
return self._track_from_metrics_extractor(result, metrics_extractor, graph_key=graph_key)
211+
self.track_duration(duration)
212+
return self._track_from_metrics_extractor(result, metrics_extractor)
221213

222-
async def track_metrics_of_async(
223-
self, func, metrics_extractor, *, graph_key: Optional[str] = None
224-
):
214+
async def track_metrics_of_async(self, func, metrics_extractor):
225215
"""
226216
Track metrics for an async AI operation (``func`` is awaited).
227217
228218
Same event semantics as :meth:`track_metrics_of`.
229219
230220
:param func: Async callable or zero-arg callable that returns an awaitable when called
231221
:param metrics_extractor: Function that extracts LDAIMetrics from the operation result
232-
:param graph_key: When set, include ``graphKey`` on emitted config-level events.
233222
:return: The result of the operation
234223
"""
235224
start_ns = time.perf_counter_ns()
@@ -238,23 +227,22 @@ async def track_metrics_of_async(
238227
result = await func()
239228
except Exception as err:
240229
duration = (time.perf_counter_ns() - start_ns) // 1_000_000
241-
self.track_duration(duration, graph_key=graph_key)
242-
self.track_error(graph_key=graph_key)
230+
self.track_duration(duration)
231+
self.track_error()
243232
raise err
244233

245234
duration = (time.perf_counter_ns() - start_ns) // 1_000_000
246-
self.track_duration(duration, graph_key=graph_key)
247-
return self._track_from_metrics_extractor(result, metrics_extractor, graph_key=graph_key)
235+
self.track_duration(duration)
236+
return self._track_from_metrics_extractor(result, metrics_extractor)
248237

249-
def track_judge_result(self, judge_result: Any, *, graph_key: Optional[str] = None) -> None:
238+
def track_judge_result(self, judge_result: Any) -> None:
250239
"""
251240
Track a judge result, including the evaluation score with judge config key.
252241
253242
:param judge_result: JudgeResult object containing score, metric key, and success status
254-
:param graph_key: When set, include ``graphKey`` in the event payload.
255243
"""
256244
if judge_result.success and judge_result.metric_key:
257-
track_data = self.__get_track_data(graph_key=graph_key)
245+
track_data = self.__get_track_data()
258246
if judge_result.judge_config_key:
259247
track_data = {**track_data, 'judgeConfigKey': judge_result.judge_config_key}
260248
self._ld_client.track(
@@ -264,49 +252,44 @@ def track_judge_result(self, judge_result: Any, *, graph_key: Optional[str] = No
264252
judge_result.score,
265253
)
266254

267-
def track_feedback(self, feedback: Dict[str, FeedbackKind], *, graph_key: Optional[str] = None) -> None:
255+
def track_feedback(self, feedback: Dict[str, FeedbackKind]) -> None:
268256
"""
269257
Track user feedback for an AI operation.
270258
271259
:param feedback: Dictionary containing feedback kind.
272-
:param graph_key: When set, include ``graphKey`` in the event payload.
273260
"""
274261
self._summary._feedback = feedback
275262
if feedback["kind"] == FeedbackKind.Positive:
276263
self._ld_client.track(
277264
"$ld:ai:feedback:user:positive",
278265
self._context,
279-
self.__get_track_data(graph_key=graph_key),
266+
self.__get_track_data(),
280267
1,
281268
)
282269
elif feedback["kind"] == FeedbackKind.Negative:
283270
self._ld_client.track(
284271
"$ld:ai:feedback:user:negative",
285272
self._context,
286-
self.__get_track_data(graph_key=graph_key),
273+
self.__get_track_data(),
287274
1,
288275
)
289276

290-
def track_success(self, *, graph_key: Optional[str] = None) -> None:
277+
def track_success(self) -> None:
291278
"""
292279
Track a successful AI generation.
293-
294-
:param graph_key: When set, include ``graphKey`` in the event payload.
295280
"""
296281
self._summary._success = True
297282
self._ld_client.track(
298-
"$ld:ai:generation:success", self._context, self.__get_track_data(graph_key=graph_key), 1
283+
"$ld:ai:generation:success", self._context, self.__get_track_data(), 1
299284
)
300285

301-
def track_error(self, *, graph_key: Optional[str] = None) -> None:
286+
def track_error(self) -> None:
302287
"""
303288
Track an unsuccessful AI generation attempt.
304-
305-
:param graph_key: When set, include ``graphKey`` in the event payload.
306289
"""
307290
self._summary._success = False
308291
self._ld_client.track(
309-
"$ld:ai:generation:error", self._context, self.__get_track_data(graph_key=graph_key), 1
292+
"$ld:ai:generation:error", self._context, self.__get_track_data(), 1
310293
)
311294

312295
def track_openai_metrics(self, func):
@@ -364,15 +347,14 @@ def track_bedrock_converse_metrics(self, res: dict) -> dict:
364347
self.track_tokens(_bedrock_to_token_usage(res["usage"]))
365348
return res
366349

367-
def track_tokens(self, tokens: TokenUsage, *, graph_key: Optional[str] = None) -> None:
350+
def track_tokens(self, tokens: TokenUsage) -> None:
368351
"""
369352
Track token usage metrics.
370353
371354
:param tokens: Token usage data from either custom, OpenAI, or Bedrock sources.
372-
:param graph_key: When set, include ``graphKey`` in the event payload.
373355
"""
374356
self._summary._usage = tokens
375-
td = self.__get_track_data(graph_key=graph_key)
357+
td = self.__get_track_data()
376358
if tokens.total > 0:
377359
self._ld_client.track(
378360
"$ld:ai:tokens:total",
@@ -395,32 +377,28 @@ def track_tokens(self, tokens: TokenUsage, *, graph_key: Optional[str] = None) -
395377
tokens.output,
396378
)
397379

398-
def track_tool_call(self, tool_key: str, *, graph_key: Optional[str] = None) -> None:
380+
def track_tool_call(self, tool_key: str) -> None:
399381
"""
400382
Track a tool invocation for this configuration (standalone or within a graph).
401383
402384
:param tool_key: Identifier of the tool that was invoked.
403-
:param graph_key: When set, include ``graphKey`` in the event payload.
404385
"""
405-
track_data = {**self.__get_track_data(graph_key=graph_key), "toolKey": tool_key}
386+
track_data = {**self.__get_track_data(), "toolKey": tool_key}
406387
self._ld_client.track(
407388
"$ld:ai:tool_call",
408389
self._context,
409390
track_data,
410391
1,
411392
)
412393

413-
def track_tool_calls(
414-
self, tool_keys: Iterable[str], *, graph_key: Optional[str] = None
415-
) -> None:
394+
def track_tool_calls(self, tool_keys: Iterable[str]) -> None:
416395
"""
417396
Track multiple tool invocations for this configuration.
418397
419398
:param tool_keys: Tool identifiers (e.g. from a model response).
420-
:param graph_key: When set, include ``graphKey`` on each event.
421399
"""
422400
for tool_key in tool_keys:
423-
self.track_tool_call(tool_key, graph_key=graph_key)
401+
self.track_tool_call(tool_key)
424402

425403
def get_summary(self) -> LDAIMetricSummary:
426404
"""

packages/sdk/server-ai/tests/test_agent_graph.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,19 @@ def handle_reverse_traverse(node, context):
387387
]
388388

389389

390+
def test_agent_graph_node_trackers_have_graph_key(ldai_client: LDAIClient):
391+
graph = ldai_client.agent_graph("test-agent-graph", Context.create("user-key"))
392+
393+
assert graph.enabled is True
394+
for node in [graph.get_node("customer-support-agent"),
395+
graph.get_node("personalized-agent"),
396+
graph.get_node("multi-context-agent"),
397+
graph.get_node("minimal-agent")]:
398+
config = node.get_config()
399+
assert config.tracker is not None
400+
assert config.tracker._graph_key == "test-agent-graph"
401+
402+
390403
def test_agent_graph_handoff(ldai_client: LDAIClient):
391404
graph = ldai_client.agent_graph(
392405
"test-agent-graph-depth-3", Context.create("user-key")

0 commit comments

Comments
 (0)