Subchapter 20.3
references/before-after-examples.mdMarkdown11 KBView on GitHub
Paired code snippets for the Stage 2 call-site swap. Each example shows the same call before extraction and after wrapping with the LaunchDarkly AI SDK. Tools and tracking are deliberately not shown here — they are layered on in Stages 3 and 4. See sdk-ai-tracker-patterns.md for the tracking overlay and agent-mode-frameworks.md for the tool-loading pattern.
A typical one-shot chat app. Hardcoded model, temperature, max tokens, and system prompt.
from openai import OpenAI
openai_client = OpenAI()
def answer(user_question: str) -> str:
response = openai_client.chat.completions.create(
model="gpt-4o",
temperature=0.7,
max_tokens=2000,
messages=[
{"role": "system", "content": "You are a helpful assistant. Answer concisely."},
{"role": "user", "content": user_question},
],
)
return response.choices[0].message.contentimport ldclient
from ldclient import Context
from ldclient.config import Config
from ldai.client import LDAIClient, AICompletionConfigDefault, ModelConfig, ProviderConfig, LDMessage
from openai import OpenAI
openai_client = OpenAI()
ldclient.set_config(Config(os.environ["LD_SDK_KEY"]))
ai_client = LDAIClient(ldclient.get())
# Fallback mirrors the hardcoded values that were removed.
FALLBACK = AICompletionConfigDefault(
enabled=True,
model=ModelConfig(
name="gpt-4o",
parameters={"temperature": 0.7, "max_tokens": 2000},
),
provider=ProviderConfig(name="openai"),
messages=[LDMessage(role="system", content="You are a helpful assistant. Answer concisely.")],
)
def answer(user_id: str, user_question: str) -> str:
context = Context.builder(user_id).kind("user").build()
config = ai_client.completion_config("chat-assistant", context, FALLBACK)
if not config.enabled:
return "" # handle disabled path
params = config.model.parameters or {}
response = openai_client.chat.completions.create(
model=config.model.name,
temperature=params.get("temperature"),
max_tokens=params.get("max_tokens"),
messages=[m.to_dict() for m in (config.messages or [])] + [
{"role": "user", "content": user_question},
],
)
return response.choices[0].message.contentconfig.model.name, config.model.parameters, and config.messagesLDAIClient is initialized once at import timeContext is built per request from user_id (targeting happens here)AICompletionConfigDefault that mirrors the removed hardcoded valuesconfig.enabled is checked before calling the provideropenai_client.chat.completions.create, same return shapeAnthropic separates the system message from the messages array, so this example shows the convertToAnthropicFormat helper used in the relaunch guide.
import Anthropic from '@anthropic-ai/sdk';
const anthropic = new Anthropic();
export async function answer(userQuestion: string): Promise<string> {
const response = await anthropic.messages.create({
model: 'claude-sonnet-4-5',
max_tokens: 1024,
system: 'You are a helpful assistant. Answer concisely.',
messages: [{ role: 'user', content: userQuestion }],
});
const text = response.content.find((b) => b.type === 'text');
return text?.type === 'text' ? text.text : '';
}import Anthropic from '@anthropic-ai/sdk';
import { init, LDContext } from '@launchdarkly/node-server-sdk';
import { initAi, LDAICompletionConfigDefault } from '@launchdarkly/server-sdk-ai';
const anthropic = new Anthropic();
const ldClient = init(process.env.LD_SDK_KEY!);
await ldClient.waitForInitialization({ timeout: 10 });
const aiClient = initAi(ldClient);
const FALLBACK: LDAICompletionConfigDefault = {
enabled: true,
model: {
name: 'claude-sonnet-4-5',
parameters: { max_tokens: 1024 },
},
provider: { name: 'anthropic' },
messages: [
{ role: 'system', content: 'You are a helpful assistant. Answer concisely.' },
],
};
function convertToAnthropicFormat(ldMessages?: Array<{ role: string; content: string }>) {
let systemMessage: string | undefined;
const messages: Array<{ role: 'user' | 'assistant'; content: string }> = [];
for (const msg of ldMessages ?? []) {
if (msg.role === 'system') {
systemMessage = msg.content;
} else {
messages.push({ role: msg.role as 'user' | 'assistant', content: msg.content });
}
}
return { systemMessage, messages };
}
export async function answer(userId: string, userQuestion: string): Promise<string> {
const context: LDContext = { kind: 'user', key: userId };
const aiConfig = await aiClient.completionConfig('chat-assistant', context, FALLBACK);
if (!aiConfig.enabled) return '';
const { systemMessage, messages } = convertToAnthropicFormat(aiConfig.messages);
messages.push({ role: 'user', content: userQuestion });
const response = await anthropic.messages.create({
model: aiConfig.model?.name ?? 'claude-sonnet-4-5',
max_tokens: (aiConfig.model?.parameters?.max_tokens as number) ?? 1024,
system: systemMessage,
messages,
});
const text = response.content.find((b) => b.type === 'text');
return text?.type === 'text' ? text.text : '';
}max_tokens are goneinitAi(ldClient) wraps the base client once at importconvertToAnthropicFormat hoists the system message out of the LDMessage array (since Anthropic takes system as a top-level param, not a role in messages)aiConfig.enabled is checked; the disabled path returns an empty stringcreate_agent (in langchain.agents) takes a model, tools, and system_prompt — a natural fit for agent mode. The instructions string replaces the hardcoded system_prompt argument. Tools remain hardcoded for now (Stage 3 will move them into the config too).
API note. Use
from langchain.agents import create_agent. The earlierfrom langgraph.prebuilt import create_react_agentis deprecated in LangGraph 1.0 and removed in 2.0. Same return shape; the only rename you’ll feel at the call site isprompt=→system_prompt=. Node.js still usescreateReactAgentfrom@langchain/langgraph/prebuilt— no JS deprecation.
from langchain_openai import ChatOpenAI
from langchain.agents import create_agent
from my_tools import search_kb, calculator
llm = ChatOpenAI(model="gpt-4o", temperature=0.3)
agent = create_agent(
llm,
[search_kb, calculator],
system_prompt=(
"You are a technical support assistant. Use the search_kb tool to look up "
"documentation, and the calculator tool for math. Always cite sources."
),
)
def run_support(user_question: str) -> str:
result = agent.invoke({"messages": [{"role": "user", "content": user_question}]})
return result["messages"][-1].contentimport ldclient
from ldclient import Context
from ldclient.config import Config
from ldai.client import LDAIClient, AIAgentConfigDefault, ModelConfig, ProviderConfig
from ldai_langchain import create_langchain_model
from langchain.agents import create_agent
from my_tools import search_kb, calculator
ldclient.set_config(Config(os.environ["LD_SDK_KEY"]))
ai_client = LDAIClient(ldclient.get())
FALLBACK = AIAgentConfigDefault(
enabled=True,
model=ModelConfig(name="gpt-4o", parameters={"temperature": 0.3}),
provider=ProviderConfig(name="openai"),
instructions=(
"You are a technical support assistant. Use the search_kb tool to look up "
"documentation, and the calculator tool for math. Always cite sources."
),
)
def run_support(user_id: str, user_question: str) -> str:
context = Context.builder(user_id).kind("user").build()
config = ai_client.agent_config("support-agent", context, FALLBACK)
if not config.enabled:
return ""
# create_langchain_model forwards every variation parameter. Do NOT hand-roll
# ChatOpenAI(model=...) — it drops unnamed parameters silently.
llm = create_langchain_model(config)
agent = create_agent(
llm,
[search_kb, calculator], # Stage 3 will replace this with config.tools loader
system_prompt=config.instructions,
)
result = agent.invoke({"messages": [{"role": "user", "content": user_question}]})
return result["messages"][-1].contentagent_config() is called instead of completion_config() because the framework expects an instructions stringFALLBACK is an AIAgentConfigDefault (note the different type — same fields as completion except instructions instead of messages)create_langchain_model(config) from the ldai_langchain helper package — forwards every variation parameter. The alternative of hand-rolling ChatOpenAI(model=config.model.name, temperature=...) would silently drop every parameter not explicitly named.create_agent(..., system_prompt=...) reads from config.instructionssetup_run entry node, consume in call_model and finalize) — see agent-mode-frameworks.md § Custom StateGraph for the full architectureContext per request. The context carries targeting inputs — user ID, plan tier, region, whatever the rollout is keyed on. Reuse the same context the app already uses for feature flag evaluation if one exists.config.enabled. Even a successful completion_config call can return a disabled config (if the variation is turned off in LaunchDarkly). The disabled path should not call the provider.completion_config / agent_config inside the request handler so LaunchDarkly can re-evaluate targeting per call. One LDAIClient instance, many completion_config calls.