From eb9c7fae5ac9c9e8dd5216570fe41a9e40c30bf7 Mon Sep 17 00:00:00 2001 From: Clifford Tawiah Date: Thu, 26 Jun 2025 13:08:40 -0400 Subject: [PATCH 1/3] Adding agent support for AI Configs --- .../__tests__/LDAIClientImpl.test.ts | 120 +++++++++++++++++ packages/sdk/server-ai/src/LDAIClientImpl.ts | 124 ++++++++++++++++-- packages/sdk/server-ai/src/api/LDAIClient.ts | 53 ++++++++ .../sdk/server-ai/src/api/agents/LDAIAgent.ts | 26 ++++ .../sdk/server-ai/src/api/agents/index.ts | 1 + packages/sdk/server-ai/src/api/index.ts | 1 + 6 files changed, 315 insertions(+), 10 deletions(-) create mode 100644 packages/sdk/server-ai/src/api/agents/LDAIAgent.ts create mode 100644 packages/sdk/server-ai/src/api/agents/index.ts diff --git a/packages/sdk/server-ai/__tests__/LDAIClientImpl.test.ts b/packages/sdk/server-ai/__tests__/LDAIClientImpl.test.ts index 41d035564a..1cd478147e 100644 --- a/packages/sdk/server-ai/__tests__/LDAIClientImpl.test.ts +++ b/packages/sdk/server-ai/__tests__/LDAIClientImpl.test.ts @@ -1,5 +1,6 @@ import { LDContext } from '@launchdarkly/js-server-sdk-common'; +import { LDAIAgentDefaults } from '../src/api/agents'; import { LDAIDefaults } from '../src/api/config'; import { LDAIClientImpl } from '../src/LDAIClientImpl'; import { LDClientMin } from '../src/LDClientMin'; @@ -129,3 +130,122 @@ it('passes the default value to the underlying client', async () => { expect(mockLdClient.variation).toHaveBeenCalledWith(key, testContext, defaultValue); }); + +it('returns agent config with interpolated instructions', async () => { + const client = new LDAIClientImpl(mockLdClient); + const key = 'test-flag'; + const defaultValue: LDAIAgentDefaults = { + model: { name: 'test', parameters: { name: 'test-model' } }, + instructions: 'You are a helpful assistant.', + enabled: true, + }; + + const mockVariation = { + model: { + name: 'example-model', + parameters: { name: 'imagination', temperature: 0.7, maxTokens: 4096 }, + }, + provider: { + name: 'example-provider', + }, + instructions: 'You are a helpful assistant. your name is {{name}} and your score is {{score}}', + _ldMeta: { + variationKey: 'v1', + enabled: true, + }, + }; + + mockLdClient.variation.mockResolvedValue(mockVariation); + + const variables = { name: 'John', score: 42 }; + const result = await client.agents([key], testContext, defaultValue, variables); + + expect(result).toEqual({ + 'test-flag': { + model: { + name: 'example-model', + parameters: { name: 'imagination', temperature: 0.7, maxTokens: 4096 }, + }, + provider: { + name: 'example-provider', + }, + instructions: 'You are a helpful assistant. your name is John and your score is 42', + tracker: expect.any(Object), + enabled: true, + }, + }); +}); + +it('includes context in variables for agent instructions interpolation', async () => { + const client = new LDAIClientImpl(mockLdClient); + const key = 'test-flag'; + const defaultValue: LDAIAgentDefaults = { + model: { name: 'test', parameters: { name: 'test-model' } }, + instructions: 'You are a helpful assistant.', + }; + + const mockVariation = { + instructions: 'You are a helpful assistant. your user key is {{ldctx.key}}', + _ldMeta: { variationKey: 'v1', enabled: true, mode: 'agent' }, + }; + + mockLdClient.variation.mockResolvedValue(mockVariation); + + const result = await client.agents([key], testContext, defaultValue); + + expect(result[key].instructions).toBe('You are a helpful assistant. your user key is test-user'); +}); + +it('handles missing metadata in agent variation', async () => { + const client = new LDAIClientImpl(mockLdClient); + const key = 'test-flag'; + const defaultValue: LDAIAgentDefaults = { + model: { name: 'test', parameters: { name: 'test-model' } }, + instructions: 'You are a helpful assistant.', + }; + + const mockVariation = { + model: { name: 'example-provider', parameters: { name: 'imagination' } }, + instructions: 'Hello.', + }; + + mockLdClient.variation.mockResolvedValue(mockVariation); + + const result = await client.agents([key], testContext, defaultValue); + + expect(result).toEqual({ + 'test-flag': { + model: { name: 'example-provider', parameters: { name: 'imagination' } }, + instructions: 'Hello.', + tracker: expect.any(Object), + enabled: false, + }, + }); +}); + +it('passes the default value to the underlying client for agents', async () => { + const client = new LDAIClientImpl(mockLdClient); + const key = 'non-existent-flag'; + const defaultValue: LDAIAgentDefaults = { + model: { name: 'default-model', parameters: { name: 'default' } }, + provider: { name: 'default-provider' }, + instructions: 'Default instructions', + enabled: true, + }; + + mockLdClient.variation.mockResolvedValue(defaultValue); + + const result = await client.agents([key], testContext, defaultValue); + + expect(result).toEqual({ + 'non-existent-flag': { + model: defaultValue.model, + instructions: defaultValue.instructions, + provider: defaultValue.provider, + tracker: expect.any(Object), + enabled: false, + }, + }); + + expect(mockLdClient.variation).toHaveBeenCalledWith(key, testContext, defaultValue); +}); diff --git a/packages/sdk/server-ai/src/LDAIClientImpl.ts b/packages/sdk/server-ai/src/LDAIClientImpl.ts index bca8431cce..3f481b7b09 100644 --- a/packages/sdk/server-ai/src/LDAIClientImpl.ts +++ b/packages/sdk/server-ai/src/LDAIClientImpl.ts @@ -2,11 +2,21 @@ import * as Mustache from 'mustache'; import { LDContext } from '@launchdarkly/js-server-sdk-common'; -import { LDAIConfig, LDAIDefaults, LDMessage, LDModelConfig, LDProviderConfig } from './api/config'; +import { LDAIAgent, LDAIAgentDefaults, LDAIAgents } from './api/agents'; +import { + LDAIConfig, + LDAIConfigTracker, + LDAIDefaults, + LDMessage, + LDModelConfig, + LDProviderConfig, +} from './api/config'; import { LDAIClient } from './api/LDAIClient'; import { LDAIConfigTrackerImpl } from './LDAIConfigTrackerImpl'; import { LDClientMin } from './LDClientMin'; +type Mode = 'completion' | 'agent'; + /** * Metadata assorted with a model configuration variation. */ @@ -14,6 +24,7 @@ interface LDMeta { variationKey: string; enabled: boolean; version?: number; + mode?: Mode; } /** @@ -23,10 +34,24 @@ interface LDMeta { interface VariationContent { model?: LDModelConfig; messages?: LDMessage[]; + instructions?: string; provider?: LDProviderConfig; _ldMeta?: LDMeta; } +/** + * The result of evaluating a configuration. + */ +interface EvaluationResult { + tracker: LDAIConfigTracker; + enabled: boolean; + model?: LDModelConfig; + provider?: LDProviderConfig; + messages?: LDMessage[]; + instructions?: string; + mode?: string; +} + export class LDAIClientImpl implements LDAIClient { constructor(private _ldClient: LDClientMin) {} @@ -34,13 +59,13 @@ export class LDAIClientImpl implements LDAIClient { return Mustache.render(template, variables, undefined, { escape: (item: any) => item }); } - async config( + private async _evaluate( key: string, context: LDContext, defaultValue: LDAIDefaults, - variables?: Record, - ): Promise { + ): Promise { const value: VariationContent = await this._ldClient.variation(key, context, defaultValue); + const tracker = new LDAIConfigTrackerImpl( this._ldClient, key, @@ -50,24 +75,85 @@ export class LDAIClientImpl implements LDAIClient { value._ldMeta?.version ?? 1, context, ); + // eslint-disable-next-line no-underscore-dangle const enabled = !!value._ldMeta?.enabled; + + return { + tracker, + enabled, + model: value.model, + provider: value.provider, + messages: value.messages, + instructions: value.instructions, + // eslint-disable-next-line no-underscore-dangle + mode: value._ldMeta?.mode ?? 'completion', + }; + } + + private async _evaluateAgent( + key: string, + context: LDContext, + defaultValue: LDAIAgentDefaults, + variables?: Record, + ): Promise { + const { tracker, enabled, model, provider, instructions } = await this._evaluate( + key, + context, + defaultValue, + ); + + const agent: LDAIAgent = { + tracker, + enabled, + }; + // We are going to modify the contents before returning them, so we make a copy. + // This isn't a deep copy and the application developer should not modify the returned content. + if (model) { + agent.model = { ...model }; + } + + if (provider) { + agent.provider = { ...provider }; + } + + const allVariables = { ...variables, ldctx: context }; + + if (instructions) { + agent.instructions = this._interpolateTemplate(instructions, allVariables); + } + + return agent; + } + + async config( + key: string, + context: LDContext, + defaultValue: LDAIDefaults, + variables?: Record, + ): Promise { + const { tracker, enabled, model, provider, messages } = await this._evaluate( + key, + context, + defaultValue, + ); + const config: LDAIConfig = { tracker, enabled, }; // We are going to modify the contents before returning them, so we make a copy. // This isn't a deep copy and the application developer should not modify the returned content. - if (value.model) { - config.model = { ...value.model }; + if (model) { + config.model = { ...model }; } - if (value.provider) { - config.provider = { ...value.provider }; + if (provider) { + config.provider = { ...provider }; } const allVariables = { ...variables, ldctx: context }; - if (value.messages) { - config.messages = value.messages.map((entry: any) => ({ + if (messages) { + config.messages = messages.map((entry: any) => ({ ...entry, content: this._interpolateTemplate(entry.content, allVariables), })); @@ -75,4 +161,22 @@ export class LDAIClientImpl implements LDAIClient { return config; } + + async agents( + agentKeys: readonly TKey[], + context: LDContext, + defaultValue: LDAIAgentDefaults, + variables?: Record, + ): Promise> { + const agents = {} as LDAIAgents; + + await Promise.all( + agentKeys.map(async (agentKey) => { + const result = await this._evaluateAgent(agentKey, context, defaultValue, variables); + agents[agentKey] = result; + }), + ); + + return agents; + } } diff --git a/packages/sdk/server-ai/src/api/LDAIClient.ts b/packages/sdk/server-ai/src/api/LDAIClient.ts index 4bf5f617e0..f08c684e50 100644 --- a/packages/sdk/server-ai/src/api/LDAIClient.ts +++ b/packages/sdk/server-ai/src/api/LDAIClient.ts @@ -1,5 +1,6 @@ import { LDContext } from '@launchdarkly/js-server-sdk-common'; +import { LDAIAgentDefaults, LDAIAgents } from './agents'; import { LDAIConfig, LDAIDefaults } from './config/LDAIConfig'; /** @@ -63,4 +64,56 @@ export interface LDAIClient { defaultValue: LDAIDefaults, variables?: Record, ): Promise; + + /** + * Retrieves and processes an AI Config agents based on the provided keys, LaunchDarkly context, + * and variables. This includes the model configuration and the customized instructions. + * + * @param agentKeys The keys of the AI Config Agents. + * @param context The LaunchDarkly context object that contains relevant information about the + * current environment, user, or session. This context may influence how the configuration is + * processed or personalized. + * @param defaultValue A fallback value containing model configuration and messages. This will + * be used if the configuration is not available from LaunchDarkly. + * @param variables A map of key-value pairs representing dynamic variables to be injected into + * the instruction. The keys correspond to placeholders within the template, and the values + * are the corresponding replacements. + * + * @returns Map of AI `config` agent keys to `agent`, customized `instructions`, and a `tracker`. If the configuration cannot be accessed from + * LaunchDarkly, then the return value will include information from the `defaultValue`. The returned `tracker` can + * be used to track AI operation metrics (latency, token usage, etc.). + * + * @example + * ``` + * const agentKeys = ["agent-key-1", "agent-key-2"]; + * const context = {...}; + * const variables = {username: 'john'}; + * const defaultValue = { + * enabled: false, + * }; + * + * const result = agents(agentKeys, context, defaultValue, variables); + * // Output: + * { + * 'agent-key-1': { + * enabled: true, + * config: { + * modelId: "gpt-4o", + * temperature: 0.2, + * maxTokens: 4096, + * userDefinedKey: "myValue", + * }, + * instructions: "You are an amazing GPT.", + * tracker: ... + * }, + * 'agent-key-2': {...}, + * } + * ``` + */ + agents( + agentKeys: readonly TKey[], + context: LDContext, + defaultValue: LDAIAgentDefaults, + variables?: Record, + ): Promise>; } diff --git a/packages/sdk/server-ai/src/api/agents/LDAIAgent.ts b/packages/sdk/server-ai/src/api/agents/LDAIAgent.ts new file mode 100644 index 0000000000..3bc4085752 --- /dev/null +++ b/packages/sdk/server-ai/src/api/agents/LDAIAgent.ts @@ -0,0 +1,26 @@ +import { LDAIConfig } from '../config'; + +/** + * AI Config agent and tracker. + */ +export interface LDAIAgent extends Omit { + /** + * Instructions for the agent. + */ + instructions?: string; +} + +export type LDAIAgents = Record; + +/** + * Default value for a `modelConfig`. This is the same as the LDAIAgent, but it does not include + * a tracker and `enabled` is optional. + */ +export type LDAIAgentDefaults = Omit & { + /** + * Whether the agent configuration is enabled. + * + * defaults to false + */ + enabled?: boolean; +}; diff --git a/packages/sdk/server-ai/src/api/agents/index.ts b/packages/sdk/server-ai/src/api/agents/index.ts new file mode 100644 index 0000000000..f68fcd9a24 --- /dev/null +++ b/packages/sdk/server-ai/src/api/agents/index.ts @@ -0,0 +1 @@ +export * from './LDAIAgent'; diff --git a/packages/sdk/server-ai/src/api/index.ts b/packages/sdk/server-ai/src/api/index.ts index c6c70867bb..cd6333b027 100644 --- a/packages/sdk/server-ai/src/api/index.ts +++ b/packages/sdk/server-ai/src/api/index.ts @@ -1,3 +1,4 @@ export * from './config'; +export * from './agents'; export * from './metrics'; export * from './LDAIClient'; From 1d61859103e85e455a904d5a63eedd479f8e6d43 Mon Sep 17 00:00:00 2001 From: Clifford Tawiah Date: Mon, 14 Jul 2025 13:06:40 -0500 Subject: [PATCH 2/3] Updated code based on updated AI config spec --- .../__tests__/LDAIClientImpl.test.ts | 178 ++++++++++++++---- packages/sdk/server-ai/src/LDAIClientImpl.ts | 43 ++++- packages/sdk/server-ai/src/api/LDAIClient.ts | 89 ++++++--- .../sdk/server-ai/src/api/agents/LDAIAgent.ts | 24 ++- 4 files changed, 256 insertions(+), 78 deletions(-) diff --git a/packages/sdk/server-ai/__tests__/LDAIClientImpl.test.ts b/packages/sdk/server-ai/__tests__/LDAIClientImpl.test.ts index 1cd478147e..acde14cbf7 100644 --- a/packages/sdk/server-ai/__tests__/LDAIClientImpl.test.ts +++ b/packages/sdk/server-ai/__tests__/LDAIClientImpl.test.ts @@ -12,6 +12,10 @@ const mockLdClient: jest.Mocked = { const testContext: LDContext = { kind: 'user', key: 'test-user' }; +beforeEach(() => { + jest.clearAllMocks(); +}); + it('returns config with interpolated messagess', async () => { const client = new LDAIClientImpl(mockLdClient); const key = 'test-flag'; @@ -131,9 +135,9 @@ it('passes the default value to the underlying client', async () => { expect(mockLdClient.variation).toHaveBeenCalledWith(key, testContext, defaultValue); }); -it('returns agent config with interpolated instructions', async () => { +it('returns single agent config with interpolated instructions', async () => { const client = new LDAIClientImpl(mockLdClient); - const key = 'test-flag'; + const key = 'test-agent'; const defaultValue: LDAIAgentDefaults = { model: { name: 'test', parameters: { name: 'test-model' } }, instructions: 'You are a helpful assistant.', @@ -148,57 +152,64 @@ it('returns agent config with interpolated instructions', async () => { provider: { name: 'example-provider', }, - instructions: 'You are a helpful assistant. your name is {{name}} and your score is {{score}}', + instructions: 'You are a helpful assistant. Your name is {{name}} and your score is {{score}}', _ldMeta: { variationKey: 'v1', enabled: true, + mode: 'agent', }, }; mockLdClient.variation.mockResolvedValue(mockVariation); const variables = { name: 'John', score: 42 }; - const result = await client.agents([key], testContext, defaultValue, variables); + const result = await client.agent(key, testContext, defaultValue, variables); expect(result).toEqual({ - 'test-flag': { - model: { - name: 'example-model', - parameters: { name: 'imagination', temperature: 0.7, maxTokens: 4096 }, - }, - provider: { - name: 'example-provider', - }, - instructions: 'You are a helpful assistant. your name is John and your score is 42', - tracker: expect.any(Object), - enabled: true, + model: { + name: 'example-model', + parameters: { name: 'imagination', temperature: 0.7, maxTokens: 4096 }, + }, + provider: { + name: 'example-provider', }, + instructions: 'You are a helpful assistant. Your name is John and your score is 42', + tracker: expect.any(Object), + enabled: true, }); + + // Verify tracking was called + expect(mockLdClient.track).toHaveBeenCalledWith( + '$ld:ai:agent:function:single', + testContext, + key, + 1, + ); }); it('includes context in variables for agent instructions interpolation', async () => { const client = new LDAIClientImpl(mockLdClient); - const key = 'test-flag'; + const key = 'test-agent'; const defaultValue: LDAIAgentDefaults = { model: { name: 'test', parameters: { name: 'test-model' } }, instructions: 'You are a helpful assistant.', }; const mockVariation = { - instructions: 'You are a helpful assistant. your user key is {{ldctx.key}}', + instructions: 'You are a helpful assistant. Your user key is {{ldctx.key}}', _ldMeta: { variationKey: 'v1', enabled: true, mode: 'agent' }, }; mockLdClient.variation.mockResolvedValue(mockVariation); - const result = await client.agents([key], testContext, defaultValue); + const result = await client.agent(key, testContext, defaultValue); - expect(result[key].instructions).toBe('You are a helpful assistant. your user key is test-user'); + expect(result.instructions).toBe('You are a helpful assistant. Your user key is test-user'); }); it('handles missing metadata in agent variation', async () => { const client = new LDAIClientImpl(mockLdClient); - const key = 'test-flag'; + const key = 'test-agent'; const defaultValue: LDAIAgentDefaults = { model: { name: 'test', parameters: { name: 'test-model' } }, instructions: 'You are a helpful assistant.', @@ -211,21 +222,19 @@ it('handles missing metadata in agent variation', async () => { mockLdClient.variation.mockResolvedValue(mockVariation); - const result = await client.agents([key], testContext, defaultValue); + const result = await client.agent(key, testContext, defaultValue); expect(result).toEqual({ - 'test-flag': { - model: { name: 'example-provider', parameters: { name: 'imagination' } }, - instructions: 'Hello.', - tracker: expect.any(Object), - enabled: false, - }, + model: { name: 'example-provider', parameters: { name: 'imagination' } }, + instructions: 'Hello.', + tracker: expect.any(Object), + enabled: false, }); }); -it('passes the default value to the underlying client for agents', async () => { +it('passes the default value to the underlying client for single agent', async () => { const client = new LDAIClientImpl(mockLdClient); - const key = 'non-existent-flag'; + const key = 'non-existent-agent'; const defaultValue: LDAIAgentDefaults = { model: { name: 'default-model', parameters: { name: 'default' } }, provider: { name: 'default-provider' }, @@ -235,17 +244,114 @@ it('passes the default value to the underlying client for agents', async () => { mockLdClient.variation.mockResolvedValue(defaultValue); - const result = await client.agents([key], testContext, defaultValue); + const result = await client.agent(key, testContext, defaultValue); + + expect(result).toEqual({ + model: defaultValue.model, + instructions: defaultValue.instructions, + provider: defaultValue.provider, + tracker: expect.any(Object), + enabled: false, + }); + + expect(mockLdClient.variation).toHaveBeenCalledWith(key, testContext, defaultValue); +}); + +it('returns multiple agents config with interpolated instructions', async () => { + const client = new LDAIClientImpl(mockLdClient); + + const agentConfigs = [ + { + agentKey: 'research-agent', + defaultConfig: { + model: { name: 'test', parameters: { name: 'test-model' } }, + instructions: 'You are a research assistant.', + enabled: true, + }, + variables: { topic: 'climate change' }, + }, + { + agentKey: 'writing-agent', + defaultConfig: { + model: { name: 'test', parameters: { name: 'test-model' } }, + instructions: 'You are a writing assistant.', + enabled: true, + }, + variables: { style: 'academic' }, + }, + ] as const; + + const mockVariations = { + 'research-agent': { + model: { + name: 'research-model', + parameters: { temperature: 0.3, maxTokens: 2048 }, + }, + provider: { name: 'openai' }, + instructions: 'You are a research assistant specializing in {{topic}}.', + _ldMeta: { variationKey: 'v1', enabled: true, mode: 'agent' }, + }, + 'writing-agent': { + model: { + name: 'writing-model', + parameters: { temperature: 0.7, maxTokens: 1024 }, + }, + provider: { name: 'anthropic' }, + instructions: 'You are a writing assistant with {{style}} style.', + _ldMeta: { variationKey: 'v2', enabled: true, mode: 'agent' }, + }, + }; + + mockLdClient.variation.mockImplementation((key) => + Promise.resolve(mockVariations[key as keyof typeof mockVariations]), + ); + + const result = await client.agents(agentConfigs, testContext); expect(result).toEqual({ - 'non-existent-flag': { - model: defaultValue.model, - instructions: defaultValue.instructions, - provider: defaultValue.provider, + 'research-agent': { + model: { + name: 'research-model', + parameters: { temperature: 0.3, maxTokens: 2048 }, + }, + provider: { name: 'openai' }, + instructions: 'You are a research assistant specializing in climate change.', tracker: expect.any(Object), - enabled: false, + enabled: true, + }, + 'writing-agent': { + model: { + name: 'writing-model', + parameters: { temperature: 0.7, maxTokens: 1024 }, + }, + provider: { name: 'anthropic' }, + instructions: 'You are a writing assistant with academic style.', + tracker: expect.any(Object), + enabled: true, }, }); - expect(mockLdClient.variation).toHaveBeenCalledWith(key, testContext, defaultValue); + // Verify tracking was called + expect(mockLdClient.track).toHaveBeenCalledWith( + '$ld:ai:agent:function:multiple', + testContext, + agentConfigs.length, + agentConfigs.length, + ); +}); + +it('handles empty agent configs array', async () => { + const client = new LDAIClientImpl(mockLdClient); + + const result = await client.agents([], testContext); + + expect(result).toEqual({}); + + // Verify tracking was called with 0 agents + expect(mockLdClient.track).toHaveBeenCalledWith( + '$ld:ai:agent:function:multiple', + testContext, + 0, + 0, + ); }); diff --git a/packages/sdk/server-ai/src/LDAIClientImpl.ts b/packages/sdk/server-ai/src/LDAIClientImpl.ts index 3f481b7b09..d6bdea91ec 100644 --- a/packages/sdk/server-ai/src/LDAIClientImpl.ts +++ b/packages/sdk/server-ai/src/LDAIClientImpl.ts @@ -2,7 +2,7 @@ import * as Mustache from 'mustache'; import { LDContext } from '@launchdarkly/js-server-sdk-common'; -import { LDAIAgent, LDAIAgentDefaults, LDAIAgents } from './api/agents'; +import { LDAIAgent, LDAIAgentConfig, LDAIAgentDefaults } from './api/agents'; import { LDAIConfig, LDAIConfigTracker, @@ -18,7 +18,7 @@ import { LDClientMin } from './LDClientMin'; type Mode = 'completion' | 'agent'; /** - * Metadata assorted with a model configuration variation. + * Metadata associated with a model configuration variation. */ interface LDMeta { variationKey: string; @@ -107,6 +107,7 @@ export class LDAIClientImpl implements LDAIClient { tracker, enabled, }; + // We are going to modify the contents before returning them, so we make a copy. // This isn't a deep copy and the application developer should not modify the returned content. if (model) { @@ -142,6 +143,7 @@ export class LDAIClientImpl implements LDAIClient { tracker, enabled, }; + // We are going to modify the contents before returning them, so we make a copy. // This isn't a deep copy and the application developer should not modify the returned content. if (model) { @@ -162,18 +164,41 @@ export class LDAIClientImpl implements LDAIClient { return config; } - async agents( - agentKeys: readonly TKey[], + async agent( + key: string, context: LDContext, defaultValue: LDAIAgentDefaults, variables?: Record, - ): Promise> { - const agents = {} as LDAIAgents; + ): Promise { + // Track agent usage + this._ldClient.track('$ld:ai:agent:function:single', context, key, 1); + + return this._evaluateAgent(key, context, defaultValue, variables); + } + + async agents( + agentConfigs: TConfigs, + context: LDContext, + ): Promise> { + // Track multiple agents usage + this._ldClient.track( + '$ld:ai:agent:function:multiple', + context, + agentConfigs.length, + agentConfigs.length, + ); + + const agents = {} as Record; await Promise.all( - agentKeys.map(async (agentKey) => { - const result = await this._evaluateAgent(agentKey, context, defaultValue, variables); - agents[agentKey] = result; + agentConfigs.map(async (config) => { + const agent = await this._evaluateAgent( + config.agentKey, + context, + config.defaultConfig, + config.variables, + ); + agents[config.agentKey as TConfigs[number]['agentKey']] = agent; }), ); diff --git a/packages/sdk/server-ai/src/api/LDAIClient.ts b/packages/sdk/server-ai/src/api/LDAIClient.ts index f08c684e50..520c34e53b 100644 --- a/packages/sdk/server-ai/src/api/LDAIClient.ts +++ b/packages/sdk/server-ai/src/api/LDAIClient.ts @@ -1,6 +1,6 @@ import { LDContext } from '@launchdarkly/js-server-sdk-common'; -import { LDAIAgentDefaults, LDAIAgents } from './agents'; +import { LDAIAgent, LDAIAgentConfig, LDAIAgentDefaults } from './agents'; import { LDAIConfig, LDAIDefaults } from './config/LDAIConfig'; /** @@ -66,54 +66,83 @@ export interface LDAIClient { ): Promise; /** - * Retrieves and processes an AI Config agents based on the provided keys, LaunchDarkly context, + * Retrieves and processes a single AI Config agent based on the provided key, LaunchDarkly context, * and variables. This includes the model configuration and the customized instructions. * - * @param agentKeys The keys of the AI Config Agents. + * @param key The key of the AI Config agent. * @param context The LaunchDarkly context object that contains relevant information about the * current environment, user, or session. This context may influence how the configuration is * processed or personalized. - * @param defaultValue A fallback value containing model configuration and messages. This will + * @param defaultValue A fallback value containing model configuration and instructions. This will * be used if the configuration is not available from LaunchDarkly. * @param variables A map of key-value pairs representing dynamic variables to be injected into - * the instruction. The keys correspond to placeholders within the template, and the values + * the instructions. The keys correspond to placeholders within the template, and the values * are the corresponding replacements. * - * @returns Map of AI `config` agent keys to `agent`, customized `instructions`, and a `tracker`. If the configuration cannot be accessed from - * LaunchDarkly, then the return value will include information from the `defaultValue`. The returned `tracker` can - * be used to track AI operation metrics (latency, token usage, etc.). + * @returns An AI agent with customized `instructions` and a `tracker`. If the configuration + * cannot be accessed from LaunchDarkly, then the return value will include information from the + * `defaultValue`. The returned `tracker` can be used to track AI operation metrics (latency, token usage, etc.). * * @example * ``` - * const agentKeys = ["agent-key-1", "agent-key-2"]; + * const key = "research_agent"; * const context = {...}; - * const variables = {username: 'john'}; + * const variables = { topic: 'climate change' }; * const defaultValue = { - * enabled: false, + * enabled: true, + * instructions: 'You are a research assistant.', * }; * - * const result = agents(agentKeys, context, defaultValue, variables); - * // Output: - * { - * 'agent-key-1': { - * enabled: true, - * config: { - * modelId: "gpt-4o", - * temperature: 0.2, - * maxTokens: 4096, - * userDefinedKey: "myValue", - * }, - * instructions: "You are an amazing GPT.", - * tracker: ... - * }, - * 'agent-key-2': {...}, - * } + * const agent = await client.agent(key, context, defaultValue, variables); + * const researchResult = agent.instructions; // Interpolated instructions + * agent.tracker.trackSuccess(); * ``` */ - agents( - agentKeys: readonly TKey[], + agent( + key: string, context: LDContext, defaultValue: LDAIAgentDefaults, variables?: Record, - ): Promise>; + ): Promise; + + /** + * Retrieves and processes multiple AI Config agents based on the provided agent configurations + * and LaunchDarkly context. This includes the model configuration and the customized instructions. + * + * @param agentConfigs An array of agent configurations, each containing the agent key, default configuration, + * and variables for instructions interpolation. + * @param context The LaunchDarkly context object that contains relevant information about the + * current environment, user, or session. This context may influence how the configuration is + * processed or personalized. + * + * @returns A map of agent keys to their respective AI agents with customized `instructions` and `tracker`. + * If a configuration cannot be accessed from LaunchDarkly, then the return value will include information + * from the respective `defaultConfig`. The returned `tracker` can be used to track AI operation metrics + * (latency, token usage, etc.). + * + * @example + * ``` + * const agentConfigs: LDAIAgentConfig[] = [ + * { + * agentKey: 'research_agent', + * defaultConfig: { enabled: true, instructions: 'You are a research assistant.' }, + * variables: { topic: 'climate change' } + * }, + * { + * agentKey: 'writing_agent', + * defaultConfig: { enabled: true, instructions: 'You are a writing assistant.' }, + * variables: { style: 'academic' } + * } + * ] as const; + * const context = {...}; + * + * const agents = await client.agents(agentConfigs, context); + * const researchResult = agents["research_agent"].instructions; // Interpolated instructions + * agents["research_agent"].tracker.trackSuccess(); + * ``` + */ + agents( + agentConfigs: TConfigs, + context: LDContext, + ): Promise>; } diff --git a/packages/sdk/server-ai/src/api/agents/LDAIAgent.ts b/packages/sdk/server-ai/src/api/agents/LDAIAgent.ts index 3bc4085752..71d9cd45a0 100644 --- a/packages/sdk/server-ai/src/api/agents/LDAIAgent.ts +++ b/packages/sdk/server-ai/src/api/agents/LDAIAgent.ts @@ -10,17 +10,35 @@ export interface LDAIAgent extends Omit { instructions?: string; } -export type LDAIAgents = Record; +/** + * Configuration for a single agent request. + */ +export interface LDAIAgentConfig { + /** + * The agent key to retrieve. + */ + agentKey: string; + + /** + * Default configuration for the agent. + */ + defaultConfig: LDAIAgentDefaults; + + /** + * Variables for instructions interpolation. + */ + variables?: Record; +} /** - * Default value for a `modelConfig`. This is the same as the LDAIAgent, but it does not include + * Default value for an agent configuration. This is the same as the LDAIAgent, but it does not include * a tracker and `enabled` is optional. */ export type LDAIAgentDefaults = Omit & { /** * Whether the agent configuration is enabled. * - * defaults to false + * @default false */ enabled?: boolean; }; From a7c1d1d1065567714bbb4676f4c535afc8b54e32 Mon Sep 17 00:00:00 2001 From: Clifford Tawiah Date: Tue, 15 Jul 2025 08:55:44 -0500 Subject: [PATCH 3/3] Added default values and updated prop names --- .../__tests__/LDAIClientImpl.test.ts | 121 +++++++++++++++++- packages/sdk/server-ai/src/LDAIClientImpl.ts | 24 ++-- packages/sdk/server-ai/src/api/LDAIClient.ts | 39 ++++-- .../sdk/server-ai/src/api/agents/LDAIAgent.ts | 6 +- 4 files changed, 160 insertions(+), 30 deletions(-) diff --git a/packages/sdk/server-ai/__tests__/LDAIClientImpl.test.ts b/packages/sdk/server-ai/__tests__/LDAIClientImpl.test.ts index acde14cbf7..5cbbdfce21 100644 --- a/packages/sdk/server-ai/__tests__/LDAIClientImpl.test.ts +++ b/packages/sdk/server-ai/__tests__/LDAIClientImpl.test.ts @@ -257,13 +257,71 @@ it('passes the default value to the underlying client for single agent', async ( expect(mockLdClient.variation).toHaveBeenCalledWith(key, testContext, defaultValue); }); +it('handles single agent with optional defaultValue', async () => { + const client = new LDAIClientImpl(mockLdClient); + const key = 'test-agent'; + + const mockVariation = { + instructions: 'You are a helpful assistant named {{name}}.', + _ldMeta: { variationKey: 'v1', enabled: true, mode: 'agent' }, + }; + + mockLdClient.variation.mockResolvedValue(mockVariation); + + const variables = { name: 'Helper' }; + + // Test without providing defaultValue + const result = await client.agent(key, testContext, undefined, variables); + + expect(result).toEqual({ + instructions: 'You are a helpful assistant named Helper.', + tracker: expect.any(Object), + enabled: true, + }); + + // Verify tracking was called + expect(mockLdClient.track).toHaveBeenCalledWith( + '$ld:ai:agent:function:single', + testContext, + key, + 1, + ); + + // Verify the agent was called with { enabled: false } as default + expect(mockLdClient.variation).toHaveBeenCalledWith(key, testContext, { enabled: false }); +}); + +it('handles single agent without any optional parameters', async () => { + const client = new LDAIClientImpl(mockLdClient); + const key = 'simple-agent'; + + const mockVariation = { + instructions: 'Simple instructions.', + _ldMeta: { variationKey: 'v1', enabled: false, mode: 'agent' }, + }; + + mockLdClient.variation.mockResolvedValue(mockVariation); + + // Test with only required parameters + const result = await client.agent(key, testContext); + + expect(result).toEqual({ + instructions: 'Simple instructions.', + tracker: expect.any(Object), + enabled: false, + }); + + // Verify the agent was called with { enabled: false } as default and no variables + expect(mockLdClient.variation).toHaveBeenCalledWith(key, testContext, { enabled: false }); +}); + it('returns multiple agents config with interpolated instructions', async () => { const client = new LDAIClientImpl(mockLdClient); const agentConfigs = [ { - agentKey: 'research-agent', - defaultConfig: { + key: 'research-agent', + defaultValue: { model: { name: 'test', parameters: { name: 'test-model' } }, instructions: 'You are a research assistant.', enabled: true, @@ -271,8 +329,8 @@ it('returns multiple agents config with interpolated instructions', async () => variables: { topic: 'climate change' }, }, { - agentKey: 'writing-agent', - defaultConfig: { + key: 'writing-agent', + defaultValue: { model: { name: 'test', parameters: { name: 'test-model' } }, instructions: 'You are a writing assistant.', enabled: true, @@ -355,3 +413,58 @@ it('handles empty agent configs array', async () => { 0, ); }); + +it('handles agents with optional defaultValue', async () => { + const client = new LDAIClientImpl(mockLdClient); + + const agentConfigs = [ + { + key: 'agent-with-default', + defaultValue: { + instructions: 'You are a helpful assistant.', + enabled: true, + }, + variables: { name: 'Assistant' }, + }, + { + key: 'agent-without-default', + variables: { name: 'Helper' }, + // No defaultValue provided - should default to { enabled: false } + }, + ] as const; + + const mockVariations = { + 'agent-with-default': { + instructions: 'Hello {{name}}!', + _ldMeta: { variationKey: 'v1', enabled: true, mode: 'agent' }, + }, + 'agent-without-default': { + instructions: 'Hi {{name}}!', + _ldMeta: { variationKey: 'v2', enabled: false, mode: 'agent' }, + }, + }; + + mockLdClient.variation.mockImplementation((key) => + Promise.resolve(mockVariations[key as keyof typeof mockVariations]), + ); + + const result = await client.agents(agentConfigs, testContext); + + expect(result).toEqual({ + 'agent-with-default': { + instructions: 'Hello Assistant!', + tracker: expect.any(Object), + enabled: true, + }, + 'agent-without-default': { + instructions: 'Hi Helper!', + tracker: expect.any(Object), + enabled: false, + }, + }); + + // Verify the agent without defaultValue was called with { enabled: false } + expect(mockLdClient.variation).toHaveBeenCalledWith('agent-without-default', testContext, { + enabled: false, + }); +}); diff --git a/packages/sdk/server-ai/src/LDAIClientImpl.ts b/packages/sdk/server-ai/src/LDAIClientImpl.ts index d6bdea91ec..57755a2a55 100644 --- a/packages/sdk/server-ai/src/LDAIClientImpl.ts +++ b/packages/sdk/server-ai/src/LDAIClientImpl.ts @@ -167,19 +167,22 @@ export class LDAIClientImpl implements LDAIClient { async agent( key: string, context: LDContext, - defaultValue: LDAIAgentDefaults, + defaultValue?: LDAIAgentDefaults, variables?: Record, ): Promise { // Track agent usage this._ldClient.track('$ld:ai:agent:function:single', context, key, 1); - return this._evaluateAgent(key, context, defaultValue, variables); + // Use provided defaultValue or fallback to { enabled: false } + const resolvedDefaultValue = defaultValue ?? { enabled: false }; + + return this._evaluateAgent(key, context, resolvedDefaultValue, variables); } - async agents( - agentConfigs: TConfigs, + async agents( + agentConfigs: T, context: LDContext, - ): Promise> { + ): Promise> { // Track multiple agents usage this._ldClient.track( '$ld:ai:agent:function:multiple', @@ -188,17 +191,20 @@ export class LDAIClientImpl implements LDAIClient { agentConfigs.length, ); - const agents = {} as Record; + const agents = {} as Record; await Promise.all( agentConfigs.map(async (config) => { + // Use provided defaultValue or fallback to { enabled: false } + const defaultValue = config.defaultValue ?? { enabled: false }; + const agent = await this._evaluateAgent( - config.agentKey, + config.key, context, - config.defaultConfig, + defaultValue, config.variables, ); - agents[config.agentKey as TConfigs[number]['agentKey']] = agent; + agents[config.key as T[number]['key']] = agent; }), ); diff --git a/packages/sdk/server-ai/src/api/LDAIClient.ts b/packages/sdk/server-ai/src/api/LDAIClient.ts index 520c34e53b..bbf71c359f 100644 --- a/packages/sdk/server-ai/src/api/LDAIClient.ts +++ b/packages/sdk/server-ai/src/api/LDAIClient.ts @@ -73,8 +73,8 @@ export interface LDAIClient { * @param context The LaunchDarkly context object that contains relevant information about the * current environment, user, or session. This context may influence how the configuration is * processed or personalized. - * @param defaultValue A fallback value containing model configuration and instructions. This will - * be used if the configuration is not available from LaunchDarkly. + * @param defaultValue A fallback value containing model configuration and instructions. If not + * provided, defaults to { enabled: false }. * @param variables A map of key-value pairs representing dynamic variables to be injected into * the instructions. The keys correspond to placeholders within the template, and the values * are the corresponding replacements. @@ -88,12 +88,18 @@ export interface LDAIClient { * const key = "research_agent"; * const context = {...}; * const variables = { topic: 'climate change' }; - * const defaultValue = { + * + * // With explicit defaultValue + * const agent = await client.agent(key, context, { * enabled: true, * instructions: 'You are a research assistant.', - * }; + * }, variables); + * + * // Without defaultValue (defaults to { enabled: false }) + * const simpleAgent = await client.agent(key, context, undefined, variables); + * // or even simpler: + * const simpleAgent2 = await client.agent(key, context); * - * const agent = await client.agent(key, context, defaultValue, variables); * const researchResult = agent.instructions; // Interpolated instructions * agent.tracker.trackSuccess(); * ``` @@ -101,7 +107,7 @@ export interface LDAIClient { agent( key: string, context: LDContext, - defaultValue: LDAIAgentDefaults, + defaultValue?: LDAIAgentDefaults, variables?: Record, ): Promise; @@ -117,21 +123,26 @@ export interface LDAIClient { * * @returns A map of agent keys to their respective AI agents with customized `instructions` and `tracker`. * If a configuration cannot be accessed from LaunchDarkly, then the return value will include information - * from the respective `defaultConfig`. The returned `tracker` can be used to track AI operation metrics + * from the respective `defaultValue`. The returned `tracker` can be used to track AI operation metrics * (latency, token usage, etc.). * * @example * ``` * const agentConfigs: LDAIAgentConfig[] = [ * { - * agentKey: 'research_agent', - * defaultConfig: { enabled: true, instructions: 'You are a research assistant.' }, + * key: 'research_agent', + * defaultValue: { enabled: true, instructions: 'You are a research assistant.' }, * variables: { topic: 'climate change' } * }, * { - * agentKey: 'writing_agent', - * defaultConfig: { enabled: true, instructions: 'You are a writing assistant.' }, + * key: 'writing_agent', + * defaultValue: { enabled: true, instructions: 'You are a writing assistant.' }, * variables: { style: 'academic' } + * }, + * { + * key: 'simple_agent', + * variables: { name: 'Helper' } + * // defaultValue is optional, will default to { enabled: false } * } * ] as const; * const context = {...}; @@ -141,8 +152,8 @@ export interface LDAIClient { * agents["research_agent"].tracker.trackSuccess(); * ``` */ - agents( - agentConfigs: TConfigs, + agents( + agentConfigs: T, context: LDContext, - ): Promise>; + ): Promise>; } diff --git a/packages/sdk/server-ai/src/api/agents/LDAIAgent.ts b/packages/sdk/server-ai/src/api/agents/LDAIAgent.ts index 71d9cd45a0..ddfcfc6d64 100644 --- a/packages/sdk/server-ai/src/api/agents/LDAIAgent.ts +++ b/packages/sdk/server-ai/src/api/agents/LDAIAgent.ts @@ -17,12 +17,12 @@ export interface LDAIAgentConfig { /** * The agent key to retrieve. */ - agentKey: string; + key: string; /** - * Default configuration for the agent. + * Default configuration for the agent. If not provided, defaults to { enabled: false }. */ - defaultConfig: LDAIAgentDefaults; + defaultValue?: LDAIAgentDefaults; /** * Variables for instructions interpolation.