← Files LaunchDarklyARCHIVED FILE
skills/migrate/references/before-after-examples.md
10.8 KB · Oct 2, 2026 · 00:24 UTC
# Before/After Examples
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](sdk-ai-tracker-patterns.md) for the tracking overlay and [agent-mode-frameworks.md](agent-mode-frameworks.md) for the tool-loading pattern.
---
## Example 1: Python + OpenAI — completion mode
A typical one-shot chat app. Hardcoded model, temperature, max tokens, and system prompt.
### Before
```python
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.content
```
### After
```python
import 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.content
```
### What changed
- Model/params/prompt no longer appear as string literals — they come from `config.model.name`, `config.model.parameters`, and `config.messages`
- `LDAIClient` is initialized once at import time
- A `Context` is built per request from `user_id` (targeting happens here)
- Fallback is an `AICompletionConfigDefault` that mirrors the removed hardcoded values
- `config.enabled` is checked before calling the provider
- The provider call itself is unchanged — same `openai_client.chat.completions.create`, same return shape
---
## Example 2: Node.js + Anthropic — completion mode
Anthropic separates the system message from the messages array, so this example shows the `convertToAnthropicFormat` helper used in the relaunch guide.
### Before
```typescript
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 : '';
}
```
### After
```typescript
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 : '';
}
```
### What changed
- Hardcoded model + system prompt + `max_tokens` are gone
- `initAi(ldClient)` wraps the base client once at import
- `convertToAnthropicFormat` 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 string
- Provider call is otherwise unchanged
---
## Example 3: Python + LangGraph — agent mode
`create_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 earlier `from langgraph.prebuilt import create_react_agent` is deprecated in LangGraph 1.0 and removed in 2.0. Same return shape; the only rename you'll feel at the call site is `prompt=` → `system_prompt=`. Node.js still uses `createReactAgent` from `@langchain/langgraph/prebuilt` — no JS deprecation.
### Before
```python
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].content
```
### After
```python
import 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].content
```
### What changed
- `agent_config()` is called instead of `completion_config()` because the framework expects an `instructions` string
- `FALLBACK` is an `AIAgentConfigDefault` (note the different type — same fields as completion except `instructions` instead of `messages`)
- Model construction goes through `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.instructions`
- Tool list is still hardcoded — Stage 3 handles that move (see [agent-mode-frameworks.md](agent-mode-frameworks.md) for the tool-factory pattern that closes over per-run config)
- **Stage 4 will add a run-scoped tracker** (mint in a `setup_run` entry node, consume in `call_model` and `finalize`) — see [agent-mode-frameworks.md § Custom `StateGraph`](agent-mode-frameworks.md) for the full architecture
- Provider-side logic (LangGraph, ReAct loop) is unchanged
---
## Rules of thumb across all three examples
1. **Nothing is added to the business logic.** The provider call, the framework call, the return shape — all unchanged. Only the *source* of model/prompt/params moves.
2. **Fallback is built from the values you removed.** Full rules at [fallback-defaults-pattern.md § Critical rules](fallback-defaults-pattern.md).
3. **Build a `Context` 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.
4. **Always check `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.
5. **Do not cache the config object across requests.** Call `completion_config` / `agent_config` inside the request handler so LaunchDarkly can re-evaluate targeting per call. One `LDAIClient` instance, many `completion_config` calls.
SHA-256: d321fe0a501c13a35c36056f64269901bc6521047e628d6e9e83b148aa2df77a