Skip to content

Commit 612d518

Browse files
authored
chore: Clarify runId purpose and per-method tracker semantics (#183)
1 parent 20df94b commit 612d518

3 files changed

Lines changed: 136 additions & 35 deletions

File tree

packages/sdk/server-ai/README.md

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ ai_config = ai_client.completion_config(
9292
if ai_config.enabled:
9393
messages = ai_config.messages
9494
model = ai_config.model
95-
tracker = ai_config.tracker
95+
tracker = ai_config.create_tracker()
9696
# Use with your AI provider
9797
```
9898

@@ -156,8 +156,9 @@ async def main():
156156
# Create LangChain model from configuration
157157
llm = await LangChainProvider.create_langchain_model(ai_config)
158158

159-
# Use with tracking (sync invoke)
160-
response = ai_config.tracker.track_metrics_of(
159+
# Use with tracking (sync invoke). Mint a tracker once per AI run.
160+
tracker = ai_config.create_tracker()
161+
response = tracker.track_metrics_of(
161162
lambda: llm.invoke(messages),
162163
lambda result: LangChainProvider.get_ai_metrics_from_response(result)
163164
)
@@ -196,7 +197,9 @@ async def main():
196197
temperature=ai_config.model.get_parameter('temperature') if ai_config.model else 0.5,
197198
)
198199

199-
result = await ai_config.tracker.track_metrics_of_async(
200+
# Mint a tracker once per AI run.
201+
tracker = ai_config.create_tracker()
202+
result = await tracker.track_metrics_of_async(
200203
call_custom_provider,
201204
map_custom_provider_metrics
202205
)

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

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,10 +203,20 @@ class AIConfig:
203203
204204
Instances are always created by the SDK client, which injects a real
205205
``create_tracker`` factory. User code should never need to construct
206-
this directly — use the ``*Default`` variants for default values.
206+
this directly -- use the ``*Default`` variants for default values.
207+
208+
``create_tracker`` is a zero-argument callable: each invocation creates a
209+
new tracker for a fresh AI run. Each call mints a new ``runId`` (a UUIDv4)
210+
that LaunchDarkly uses to correlate the run's events in metrics views.
211+
Call it once per AI run; metrics from different ``runId``s cannot be
212+
combined.
207213
"""
208214
key: str
209215
enabled: bool
216+
#: Factory that creates a new tracker for a fresh AI run. Each call mints a
217+
#: new ``runId`` (a UUIDv4) so LaunchDarkly can correlate the run's events
218+
#: in metrics views. Call this once per AI run; metrics from different
219+
#: ``runId``s cannot be combined.
210220
create_tracker: Callable[[], Any]
211221
model: Optional[ModelConfig] = None
212222
provider: Optional[ProviderConfig] = None

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

Lines changed: 118 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -84,14 +84,20 @@ def resumption_token(self) -> Optional[str]:
8484
"""
8585
URL-safe Base64-encoded resumption token captured at tracker
8686
instantiation. Useful for deferred feedback flows where a downstream
87-
process needs to associate events with the original execution.
87+
process needs to associate events with the original AI run.
8888
"""
8989
return self._resumption_token
9090

9191

9292
class LDAIConfigTracker:
9393
"""
94-
Tracks configuration and usage metrics for LaunchDarkly AI operations.
94+
Records metrics for a single AI run.
95+
96+
All events a tracker emits share a runId (a UUIDv4) so LaunchDarkly can correlate
97+
them in metrics views. See individual track methods for their specific semantics.
98+
Call ``create_tracker`` on the AI Config to start a new run. A resumption token
99+
preserves the runId, so events emitted by a tracker reconstructed in another
100+
process correlate with the original run.
95101
"""
96102

97103
def __init__(
@@ -110,7 +116,7 @@ def __init__(
110116
Initialize an AI Config tracker.
111117
112118
:param ld_client: LaunchDarkly client instance.
113-
:param run_id: Unique identifier for this execution.
119+
:param run_id: Unique identifier for this AI run.
114120
:param config_key: Configuration key for tracking.
115121
:param variation_key: Variation key for tracking.
116122
:param version: Version of the variation.
@@ -162,7 +168,7 @@ def from_resumption_token(cls, token: str, ld_client: LDClient, context: Context
162168
163169
This is used for cross-process scenarios such as deferred feedback,
164170
where a different service needs to associate tracking events with the
165-
original execution's ``runId``.
171+
original tracker's ``runId``.
166172
167173
:param token: A URL-safe Base64-encoded resumption token obtained from
168174
:attr:`resumption_token`.
@@ -219,12 +225,18 @@ def __get_track_data(self) -> dict:
219225

220226
def track_duration(self, duration: int) -> None:
221227
"""
222-
Manually track the duration of an AI operation.
228+
Manually track the duration of an AI run.
229+
230+
Records at most once per Tracker; further calls are ignored.
223231
224232
:param duration: Duration in milliseconds.
225233
"""
226234
if self._summary.duration_ms is not None:
227-
log.warning("Duration has already been tracked for this execution. %s", self.__get_track_data())
235+
log.warning(
236+
"Skipping track_duration: duration already recorded on this tracker. "
237+
"Call create_tracker on the AI Config for a new run. %s",
238+
self.__get_track_data(),
239+
)
228240
return
229241
self._summary._duration_ms = duration
230242
self._ld_client.track(
@@ -233,13 +245,16 @@ def track_duration(self, duration: int) -> None:
233245

234246
def track_time_to_first_token(self, time_to_first_token: int) -> None:
235247
"""
236-
Manually track the time to first token of an AI operation.
248+
Manually track the time to first token of an AI run.
249+
250+
Records at most once per Tracker; further calls are ignored.
237251
238252
:param time_to_first_token: Time to first token in milliseconds.
239253
"""
240254
if self._summary.time_to_first_token is not None:
241255
log.warning(
242-
"Time to first token has already been tracked for this execution. %s",
256+
"Skipping track_time_to_first_token: time-to-first-token already recorded on this tracker. "
257+
"Call create_tracker on the AI Config for a new run. %s",
243258
self.__get_track_data(),
244259
)
245260
return
@@ -253,10 +268,10 @@ def track_time_to_first_token(self, time_to_first_token: int) -> None:
253268

254269
def track_duration_of(self, func):
255270
"""
256-
Automatically track the duration of an AI operation.
271+
Automatically track the duration of an AI run.
257272
258-
An exception occurring during the execution of the function will still
259-
track the duration. The exception will be re-thrown.
273+
An exception raised while the function runs will still record the
274+
duration. The exception will be re-thrown.
260275
261276
:param func: Function to track (synchronous only).
262277
:return: Result of the tracked function.
@@ -317,6 +332,10 @@ def track_metrics_of(
317332
non-``None`` ``duration_ms`` field, that value is used as the measured duration
318333
instead of the wall-clock elapsed time.
319334
335+
Because each inner metric is at-most-once per Tracker, calling this twice
336+
on the same Tracker will run the inner block again but produce no
337+
additional metric events.
338+
320339
:param metrics_extractor: Function that extracts LDAIMetrics from the operation result
321340
:param func: Synchronous callable that runs the operation
322341
:return: The result of the operation
@@ -348,6 +367,10 @@ async def track_metrics_of_async(
348367
non-``None`` ``duration_ms`` field, that value is used as the measured duration
349368
instead of the wall-clock elapsed time.
350369
370+
Because each inner metric is at-most-once per Tracker, calling this twice
371+
on the same Tracker will run the inner block again but produce no
372+
additional metric events.
373+
351374
:param metrics_extractor: Function that extracts LDAIMetrics from the operation result
352375
:param func: Async callable or zero-arg callable that returns an awaitable when called
353376
:return: The result of the operation
@@ -370,6 +393,9 @@ def track_judge_result(self, judge_result: Any) -> None:
370393
"""
371394
Track a judge result, including the evaluation score with judge config key.
372395
396+
May be called multiple times per Tracker; each call records the
397+
provided judge result.
398+
373399
:param judge_result: JudgeResult object containing score, metric key, and success status
374400
"""
375401
if not judge_result.sampled:
@@ -388,12 +414,18 @@ def track_judge_result(self, judge_result: Any) -> None:
388414

389415
def track_feedback(self, feedback: Dict[str, FeedbackKind]) -> None:
390416
"""
391-
Track user feedback for an AI operation.
417+
Track user feedback for an AI run.
418+
419+
Records at most once per Tracker; further calls are ignored.
392420
393421
:param feedback: Dictionary containing feedback kind.
394422
"""
395423
if self._summary.feedback is not None:
396-
log.warning("Feedback has already been tracked for this execution. %s", self.__get_track_data())
424+
log.warning(
425+
"Skipping track_feedback: feedback already recorded on this tracker. "
426+
"Call create_tracker on the AI Config for a new run. %s",
427+
self.__get_track_data(),
428+
)
397429
return
398430
self._summary._feedback = feedback
399431
if feedback["kind"] == FeedbackKind.Positive:
@@ -413,11 +445,14 @@ def track_feedback(self, feedback: Dict[str, FeedbackKind]) -> None:
413445

414446
def track_tool_calls(self, tool_calls: Iterable[str]) -> None:
415447
"""
416-
Track the tool calls made during an AI operation.
448+
Track the tool calls made during an AI run.
417449
418450
Appends to the summary's tool call list and fires a
419451
``$ld:ai:tool_call`` event for each tool.
420452
453+
May be called multiple times per Tracker; each call records an event
454+
for every tool identifier provided.
455+
421456
:param tool_calls: Tool identifiers (e.g. from a model response).
422457
"""
423458
tool_calls_list = list(tool_calls)
@@ -428,9 +463,17 @@ def track_tool_calls(self, tool_calls: Iterable[str]) -> None:
428463
def track_success(self) -> None:
429464
"""
430465
Track a successful AI generation.
466+
467+
Records at most once per Tracker. track_success and track_error share
468+
state; only one of the two can record per Tracker, and subsequent calls
469+
are ignored.
431470
"""
432471
if self._summary.success is not None:
433-
log.warning("Success has already been tracked for this execution. %s", self.__get_track_data())
472+
log.warning(
473+
"Skipping track_success: success/error already recorded on this tracker. "
474+
"Call create_tracker on the AI Config for a new run. %s",
475+
self.__get_track_data(),
476+
)
434477
return
435478
self._summary._success = True
436479
self._ld_client.track(
@@ -440,9 +483,17 @@ def track_success(self) -> None:
440483
def track_error(self) -> None:
441484
"""
442485
Track an unsuccessful AI generation attempt.
486+
487+
Records at most once per Tracker. track_success and track_error share
488+
state; only one of the two can record per Tracker, and subsequent calls
489+
are ignored.
443490
"""
444491
if self._summary.success is not None:
445-
log.warning("Success has already been tracked for this execution. %s", self.__get_track_data())
492+
log.warning(
493+
"Skipping track_error: success/error already recorded on this tracker. "
494+
"Call create_tracker on the AI Config for a new run. %s",
495+
self.__get_track_data(),
496+
)
446497
return
447498
self._summary._success = False
448499
self._ld_client.track(
@@ -475,10 +526,16 @@ def track_tokens(self, tokens: TokenUsage) -> None:
475526
"""
476527
Track token usage metrics.
477528
529+
Records at most once per Tracker; further calls are ignored.
530+
478531
:param tokens: Token usage data from either custom, OpenAI, or Bedrock sources.
479532
"""
480533
if self._summary.tokens is not None:
481-
log.warning("Tokens have already been tracked for this execution. %s", self.__get_track_data())
534+
log.warning(
535+
"Skipping track_tokens: token usage already recorded on this tracker. "
536+
"Call create_tracker on the AI Config for a new run. %s",
537+
self.__get_track_data(),
538+
)
482539
return
483540
self._summary._tokens = tokens
484541
td = self.__get_track_data()
@@ -506,7 +563,10 @@ def track_tokens(self, tokens: TokenUsage) -> None:
506563

507564
def track_tool_call(self, tool_key: str) -> None:
508565
"""
509-
Track a tool invocation for this configuration (standalone or within a graph).
566+
Track a tool call for this configuration (standalone or within a graph).
567+
568+
May be called multiple times per Tracker; each call records a tool
569+
call event for the provided tool key.
510570
511571
:param tool_key: Identifier of the tool that was invoked.
512572
"""
@@ -604,12 +664,18 @@ def __get_track_data(self):
604664

605665
def track_invocation_success(self) -> None:
606666
"""
607-
Track a successful graph invocation.
667+
Track a successful graph run.
668+
669+
Records at most once per graph tracker. track_invocation_success and
670+
track_invocation_failure share state; only one of the two can record
671+
per graph tracker, and subsequent calls are ignored.
608672
"""
609673
if self._summary.success is not None:
610674
log.warning(
611-
"Invocation status has already been tracked for this graph execution. %s",
612-
self.__get_track_data())
675+
"Skipping track_invocation_success: invocation result already recorded on this graph tracker. "
676+
"Call create_tracker on the agent graph for a new run. %s",
677+
self.__get_track_data(),
678+
)
613679
return
614680
self._summary.success = True
615681
self._ld_client.track(
@@ -621,12 +687,18 @@ def track_invocation_success(self) -> None:
621687

622688
def track_invocation_failure(self) -> None:
623689
"""
624-
Track an unsuccessful graph invocation.
690+
Track an unsuccessful graph run.
691+
692+
Records at most once per graph tracker. track_invocation_success and
693+
track_invocation_failure share state; only one of the two can record
694+
per graph tracker, and subsequent calls are ignored.
625695
"""
626696
if self._summary.success is not None:
627697
log.warning(
628-
"Invocation status has already been tracked for this graph execution. %s",
629-
self.__get_track_data())
698+
"Skipping track_invocation_failure: invocation result already recorded on this graph tracker. "
699+
"Call create_tracker on the agent graph for a new run. %s",
700+
self.__get_track_data(),
701+
)
630702
return
631703
self._summary.success = False
632704
self._ld_client.track(
@@ -638,12 +710,18 @@ def track_invocation_failure(self) -> None:
638710

639711
def track_duration(self, duration: int) -> None:
640712
"""
641-
Track the total duration of graph execution.
713+
Track the total duration of a graph run.
714+
715+
Records at most once per graph tracker; further calls are ignored.
642716
643717
:param duration: Duration in milliseconds.
644718
"""
645719
if self._summary.duration_ms is not None:
646-
log.warning("Duration has already been tracked for this graph execution. %s", self.__get_track_data())
720+
log.warning(
721+
"Skipping track_duration: duration already recorded on this graph tracker. "
722+
"Call create_tracker on the agent graph for a new run. %s",
723+
self.__get_track_data(),
724+
)
647725
return
648726
self._summary.duration_ms = duration
649727
self._ld_client.track(
@@ -655,14 +733,20 @@ def track_duration(self, duration: int) -> None:
655733

656734
def track_total_tokens(self, tokens: Optional[TokenUsage] = None) -> None:
657735
"""
658-
Track aggregated token usage across the entire graph invocation.
736+
Track aggregated token usage across the entire graph run.
737+
738+
Records at most once per graph tracker; further calls are ignored.
659739
660740
:param tokens: Token usage data, or ``None`` when usage is unknown.
661741
"""
662742
if tokens is None or tokens.total <= 0:
663743
return
664744
if self._summary.tokens is not None:
665-
log.warning("Token usage has already been tracked for this graph execution. %s", self.__get_track_data())
745+
log.warning(
746+
"Skipping track_total_tokens: tokens already recorded on this graph tracker. "
747+
"Call create_tracker on the agent graph for a new run. %s",
748+
self.__get_track_data(),
749+
)
666750
return
667751
self._summary.tokens = tokens
668752
self._ld_client.track(
@@ -674,10 +758,14 @@ def track_total_tokens(self, tokens: Optional[TokenUsage] = None) -> None:
674758

675759
def track_path(self, path: List[str]) -> None:
676760
"""
677-
Track the execution path through the graph.
761+
Track the path traversed through the graph during a graph run.
678762
679763
Appends to the summary's path list and fires a ``$ld:ai:graph:path``
680-
event. Can be called multiple times to build the path incrementally.
764+
event.
765+
766+
May be called multiple times per Tracker; each call records the
767+
provided path segment and appends it to the summary so the full
768+
path can be built incrementally.
681769
682770
:param path: An array of configuration keys representing the sequence of nodes executed during graph traversal.
683771
"""

0 commit comments

Comments
 (0)