Skip to content

Commit 7c4dbde

Browse files
committed
[AIC-2664] Impl trackers (first pass)
1 parent ad2ac08 commit 7c4dbde

9 files changed

Lines changed: 2552 additions & 19 deletions

File tree

lib/sdk/server-ai/src/main/java/com/launchdarkly/sdk/server/ai/LDAIClient.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,4 +81,19 @@ AIJudgeConfig judgeConfig(
8181
LDContext context,
8282
AIJudgeConfigDefault defaultValue,
8383
Map<String, Object> variables);
84+
85+
/**
86+
* Reconstructs a tracker from a resumption token, preserving the original run's identity.
87+
* <p>
88+
* Use this when a multi-turn or streaming AI interaction spans multiple requests. The caller
89+
* stores the resumption token from a previous tracker (via
90+
* {@link LDAIConfigTracker#getResumptionToken()}) and passes it back here to continue tracking
91+
* against the same run.
92+
*
93+
* @param resumptionToken the token returned by a previous tracker; must not be {@code null}
94+
* @param context the evaluation context for the new request; must not be {@code null}
95+
* @return a tracker with the decoded run identity, never {@code null}
96+
* @throws IllegalArgumentException if the token is malformed
97+
*/
98+
LDAIConfigTracker createTracker(String resumptionToken, LDContext context);
8499
}

lib/sdk/server-ai/src/main/java/com/launchdarkly/sdk/server/ai/LDAIClientImpl.java

Lines changed: 47 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,21 @@
88
import com.launchdarkly.sdk.LDContext;
99
import com.launchdarkly.sdk.LDValue;
1010
import com.launchdarkly.sdk.LDValueType;
11-
import com.launchdarkly.sdk.server.ai.datamodel.LDAIConfigTypes.Mode;
1211
import com.launchdarkly.sdk.server.ai.datamodel.LDAIConfigTypes.Message;
12+
import com.launchdarkly.sdk.server.ai.datamodel.LDAIConfigTypes.Mode;
1313
import com.launchdarkly.sdk.server.ai.internal.AIConfigFlagValue;
1414
import com.launchdarkly.sdk.server.ai.internal.AIConfigParser;
1515
import com.launchdarkly.sdk.server.ai.internal.AISdkInfo;
1616
import com.launchdarkly.sdk.server.ai.internal.Interpolator;
17-
import com.launchdarkly.sdk.server.ai.internal.NoOpAIConfigTracker;
17+
import com.launchdarkly.sdk.server.ai.internal.LDAIConfigTrackerImpl;
1818
import com.launchdarkly.sdk.server.interfaces.LDClientInterface;
1919

2020
import java.util.ArrayList;
2121
import java.util.LinkedHashMap;
2222
import java.util.List;
2323
import java.util.Map;
2424
import java.util.Objects;
25+
import java.util.UUID;
2526
import java.util.function.Supplier;
2627

2728
/**
@@ -51,8 +52,6 @@ public final class LDAIClientImpl implements LDAIClient {
5152
.anonymous(true)
5253
.build();
5354

54-
// Tracking is implemented in a later step; until then every config hands out the no-op tracker.
55-
private static final Supplier<LDAIConfigTracker> TRACKER_FACTORY = () -> NoOpAIConfigTracker.INSTANCE;
5655

5756
private final LDClientInterface client;
5857
private final LDLogger logger;
@@ -187,6 +186,9 @@ private AIConfig buildConfig(
187186
AIConfigFlagValue parsed,
188187
LDContext context,
189188
Map<String, Object> variables) {
189+
Supplier<LDAIConfigTracker> factory = trackerFactory(
190+
key, parsed.getVariationKey(), parsed.getVersion(),
191+
parsed.getModel(), parsed.getProvider(), context);
190192
switch (mode) {
191193
case AGENT:
192194
return new AIAgentConfig(
@@ -197,7 +199,7 @@ private AIConfig buildConfig(
197199
interpolate(parsed.getInstructions(), variables, context),
198200
parsed.getJudgeConfiguration(),
199201
parsed.getTools(),
200-
TRACKER_FACTORY);
202+
factory);
201203
case JUDGE:
202204
return new AIJudgeConfig(
203205
key,
@@ -206,7 +208,7 @@ private AIConfig buildConfig(
206208
parsed.getProvider(),
207209
interpolateMessages(parsed.getMessages(), variables, context),
208210
parsed.getEvaluationMetricKey(),
209-
TRACKER_FACTORY);
211+
factory);
210212
case COMPLETION:
211213
default:
212214
return new AICompletionConfig(
@@ -217,7 +219,7 @@ private AIConfig buildConfig(
217219
interpolateMessages(parsed.getMessages(), variables, context),
218220
parsed.getJudgeConfiguration(),
219221
parsed.getTools(),
220-
TRACKER_FACTORY);
222+
factory);
221223
}
222224
}
223225

@@ -231,6 +233,9 @@ private AIConfig buildConfigFromDefault(
231233
AIConfigDefault defaultValue,
232234
LDContext context,
233235
Map<String, Object> variables) {
236+
// Default configs still get real trackers — the configKey was requested even if no flag was found.
237+
// variationKey is null because no flag evaluation occurred.
238+
Supplier<LDAIConfigTracker> factory = trackerFactory(key, null, null, null, null, context);
234239
switch (mode) {
235240
case AGENT: {
236241
AIAgentConfigDefault agent = (AIAgentConfigDefault) defaultValue;
@@ -242,7 +247,7 @@ private AIConfig buildConfigFromDefault(
242247
interpolate(agent.getInstructions(), variables, context),
243248
agent.getJudgeConfiguration(),
244249
agent.getTools(),
245-
TRACKER_FACTORY);
250+
factory);
246251
}
247252
case JUDGE: {
248253
AIJudgeConfigDefault judge = (AIJudgeConfigDefault) defaultValue;
@@ -253,7 +258,7 @@ private AIConfig buildConfigFromDefault(
253258
judge.getProvider(),
254259
interpolateMessages(judge.getMessages(), variables, context),
255260
judge.getEvaluationMetricKey(),
256-
TRACKER_FACTORY);
261+
factory);
257262
}
258263
case COMPLETION:
259264
default: {
@@ -266,11 +271,43 @@ private AIConfig buildConfigFromDefault(
266271
interpolateMessages(completion.getMessages(), variables, context),
267272
completion.getJudgeConfiguration(),
268273
completion.getTools(),
269-
TRACKER_FACTORY);
274+
factory);
270275
}
271276
}
272277
}
273278

279+
/**
280+
* Creates a per-evaluation tracker factory. Each call to the returned {@link Supplier} produces
281+
* a fresh {@link LDAIConfigTrackerImpl} with a new {@code runId}.
282+
*/
283+
private Supplier<LDAIConfigTracker> trackerFactory(
284+
String configKey,
285+
String variationKey,
286+
Integer version,
287+
com.launchdarkly.sdk.server.ai.datamodel.LDAIConfigTypes.Model model,
288+
com.launchdarkly.sdk.server.ai.datamodel.LDAIConfigTypes.Provider provider,
289+
LDContext context) {
290+
String modelName = model != null && model.getName() != null ? model.getName() : "";
291+
String providerName = provider != null && provider.getName() != null ? provider.getName() : "";
292+
int ver = version != null ? version : 0;
293+
return () -> new LDAIConfigTrackerImpl(
294+
client,
295+
UUID.randomUUID().toString(),
296+
configKey,
297+
variationKey,
298+
ver,
299+
modelName,
300+
providerName,
301+
context,
302+
null, // graphKey — set by agentGraph() in Plan 3
303+
logger);
304+
}
305+
306+
@Override
307+
public LDAIConfigTracker createTracker(String resumptionToken, LDContext context) {
308+
return LDAIConfigTrackerImpl.fromResumptionToken(resumptionToken, client, context, logger);
309+
}
310+
274311
private List<Message> interpolateMessages(
275312
List<Message> messages, Map<String, Object> variables, LDContext context) {
276313
if (messages == null) {
Lines changed: 156 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,165 @@
11
package com.launchdarkly.sdk.server.ai;
22

3+
import com.launchdarkly.sdk.server.ai.datamodel.LDAITrackingTypes.AIMetrics;
4+
import com.launchdarkly.sdk.server.ai.datamodel.LDAITrackingTypes.FeedbackKind;
5+
import com.launchdarkly.sdk.server.ai.datamodel.LDAITrackingTypes.JudgeResult;
6+
import com.launchdarkly.sdk.server.ai.datamodel.LDAITrackingTypes.MetricSummary;
7+
import com.launchdarkly.sdk.server.ai.datamodel.LDAITrackingTypes.TokenUsage;
8+
import com.launchdarkly.sdk.server.ai.datamodel.LDAITrackingTypes.TrackData;
9+
10+
import java.time.Duration;
11+
import java.util.List;
12+
import java.util.concurrent.Callable;
13+
import java.util.function.Function;
14+
315
/**
416
* Reports events related to a single AI run of an {@link AIConfig}.
517
* <p>
6-
* A tracker is obtained from a retrieved config via {@link AIConfig#createTracker()}. Each tracker
7-
* corresponds to one AI run and is used to record metrics such as model usage, duration, and
8-
* feedback against the AI Config it was created from.
18+
* A tracker is obtained from a retrieved config via {@link AIConfig#createTracker()}, or
19+
* reconstructed from a resumption token via {@link LDAIClient#createTracker(String, com.launchdarkly.sdk.LDContext)}.
20+
* Each tracker corresponds to one AI run and is used to record metrics such as model usage,
21+
* duration, and feedback against the AI Config it was created from.
22+
* <p>
23+
* Most tracking methods are at-most-once: a second call to the same method on the same tracker
24+
* is silently dropped. {@link #trackToolCall(String)} and {@link #trackJudgeResult(JudgeResult)}
25+
* are multi-fire — each call records a distinct event.
926
* <p>
10-
* <strong>This interface is an intentional placeholder.</strong> The metric- and feedback-reporting
11-
* methods (and resumption-token support) are introduced in a later step of the AI SDK build-out; it
12-
* is defined here so that the public config types expose a stable {@code createTracker()} surface.
13-
* The only implementation in this release is an internal no-op.
27+
* Implementations are thread-safe.
1428
*/
1529
public interface LDAIConfigTracker {
30+
31+
/**
32+
* Returns the correlation metadata for this tracker's run.
33+
*
34+
* @return the track data, never {@code null}
35+
*/
36+
TrackData getTrackData();
37+
38+
/**
39+
* Returns the resumption token for this run.
40+
* <p>
41+
* The resumption token encodes the run's identity and can be passed to
42+
* {@link LDAIClient#createTracker(String, com.launchdarkly.sdk.LDContext)} to reconstruct a
43+
* tracker on a subsequent request (for example, in a streaming scenario).
44+
*
45+
* @return the resumption token, or {@code null} if not available
46+
*/
47+
String getResumptionToken();
48+
49+
/**
50+
* Records the duration of the AI generation.
51+
* <p>
52+
* At-most-once: subsequent calls on the same tracker are silently dropped.
53+
*
54+
* @param duration the duration; ignored if {@code null}
55+
*/
56+
void trackDuration(Duration duration);
57+
58+
/**
59+
* Executes the given operation and records its wall-clock duration.
60+
* <p>
61+
* The duration is recorded even if the operation throws. Equivalent to wrapping the operation
62+
* in a try/finally that calls {@link #trackDuration(Duration)}.
63+
*
64+
* @param <T> the return type of the operation
65+
* @param operation the operation to execute and time; must not be {@code null}
66+
* @return the result of the operation
67+
* @throws Exception if the operation throws
68+
*/
69+
<T> T trackDurationOf(Callable<T> operation) throws Exception;
70+
71+
/**
72+
* Records the time from request start to receipt of the first token.
73+
* <p>
74+
* At-most-once: subsequent calls on the same tracker are silently dropped.
75+
*
76+
* @param duration the time to first token; ignored if {@code null}
77+
*/
78+
void trackTimeToFirstToken(Duration duration);
79+
80+
/**
81+
* Records that the AI generation succeeded.
82+
* <p>
83+
* At-most-once and mutually exclusive with {@link #trackError()}: whichever is called first wins.
84+
*/
85+
void trackSuccess();
86+
87+
/**
88+
* Records that the AI generation failed.
89+
* <p>
90+
* At-most-once and mutually exclusive with {@link #trackSuccess()}: whichever is called first wins.
91+
*/
92+
void trackError();
93+
94+
/**
95+
* Records user feedback for this AI generation.
96+
* <p>
97+
* At-most-once: subsequent calls on the same tracker are silently dropped.
98+
*
99+
* @param kind the feedback kind; ignored if {@code null}
100+
*/
101+
void trackFeedback(FeedbackKind kind);
102+
103+
/**
104+
* Records token usage for this AI generation.
105+
* <p>
106+
* At-most-once: subsequent calls on the same tracker are silently dropped. Calls where all
107+
* counts are zero do not consume the at-most-once slot.
108+
*
109+
* @param tokens the token usage; ignored if {@code null}
110+
*/
111+
void trackTokens(TokenUsage tokens);
112+
113+
/**
114+
* Records a single tool call made during this AI generation.
115+
* <p>
116+
* Multi-fire: every call emits an event.
117+
*
118+
* @param toolKey the tool key; ignored if {@code null}
119+
*/
120+
void trackToolCall(String toolKey);
121+
122+
/**
123+
* Records multiple tool calls made during this AI generation.
124+
* <p>
125+
* Equivalent to calling {@link #trackToolCall(String)} for each key.
126+
*
127+
* @param toolKeys the tool keys; ignored if {@code null}
128+
*/
129+
void trackToolCalls(List<String> toolKeys);
130+
131+
/**
132+
* Records the result of a judge evaluation.
133+
* <p>
134+
* Multi-fire per judge metric key. The result is silently skipped if it was not sampled, if
135+
* the evaluation did not succeed, or if the metric key or score is absent.
136+
*
137+
* @param result the judge result; ignored if {@code null}
138+
*/
139+
void trackJudgeResult(JudgeResult result);
140+
141+
/**
142+
* Executes the given operation and tracks its metrics using the extracted {@link AIMetrics}.
143+
* <p>
144+
* Tracks duration (preferring runner-reported duration when present), success or error, tokens,
145+
* and tool calls. If the operation throws, {@link #trackError()} is called and the exception
146+
* is re-thrown.
147+
*
148+
* @param <T> the return type of the operation
149+
* @param metricsExtractor a function that extracts {@link AIMetrics} from the operation result;
150+
* exceptions from the extractor propagate to the caller
151+
* @param operation the AI operation to execute; must not be {@code null}
152+
* @return the result of the operation
153+
* @throws Exception if the operation or the metrics extractor throws
154+
*/
155+
<T> T trackMetricsOf(
156+
Function<? super T, AIMetrics> metricsExtractor,
157+
Callable<T> operation) throws Exception;
158+
159+
/**
160+
* Returns a snapshot of all metrics tracked so far on this tracker.
161+
*
162+
* @return the metric summary, never {@code null}
163+
*/
164+
MetricSummary getSummary();
16165
}

0 commit comments

Comments
 (0)