Skip to content

Commit 6090da7

Browse files
committed
feat: Add LangChain Provider for AI SDK
1 parent f0b9b5d commit 6090da7

File tree

10 files changed

+387
-13
lines changed

10 files changed

+387
-13
lines changed

.release-please-manifest.json

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,25 @@
11
{
2-
"packages/shared/common": "2.19.0",
3-
"packages/shared/sdk-server": "2.16.2",
4-
"packages/sdk/server-node": "9.10.2",
2+
"packages/ai-providers/server-ai-langchain": "0.1.0-alpha.0",
3+
"packages/sdk/akamai-base": "3.0.10",
4+
"packages/sdk/akamai-edgekv": "1.4.12",
5+
"packages/sdk/browser": "0.8.1",
56
"packages/sdk/cloudflare": "2.7.10",
7+
"packages/sdk/combined-browser": "0.0.0",
68
"packages/sdk/fastly": "0.2.1",
7-
"packages/shared/sdk-server-edge": "2.6.9",
9+
"packages/sdk/react-native": "10.11.0",
10+
"packages/sdk/react-universal": "0.0.1",
11+
"packages/sdk/server-ai": "0.11.4",
12+
"packages/sdk/server-node": "9.10.2",
13+
"packages/sdk/svelte": "0.1.0",
814
"packages/sdk/vercel": "1.3.34",
9-
"packages/sdk/akamai-base": "3.0.10",
10-
"packages/sdk/akamai-edgekv": "1.4.12",
1115
"packages/shared/akamai-edgeworker-sdk": "2.0.10",
16+
"packages/shared/common": "2.19.0",
17+
"packages/shared/sdk-client": "1.15.1",
18+
"packages/shared/sdk-server": "2.16.2",
19+
"packages/shared/sdk-server-edge": "2.6.9",
1220
"packages/store/node-server-sdk-dynamodb": "6.2.14",
1321
"packages/store/node-server-sdk-redis": "4.2.14",
14-
"packages/shared/sdk-client": "1.15.1",
15-
"packages/sdk/react-native": "10.11.0",
16-
"packages/telemetry/node-server-sdk-otel": "1.3.2",
17-
"packages/sdk/browser": "0.8.1",
18-
"packages/sdk/server-ai": "0.11.4",
1922
"packages/telemetry/browser-telemetry": "1.0.11",
20-
"packages/tooling/jest": "0.1.11",
21-
"packages/sdk/combined-browser": "0.0.0"
23+
"packages/telemetry/node-server-sdk-otel": "1.3.2",
24+
"packages/tooling/jest": "0.1.11"
2225
}

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{
22
"name": "@launchdarkly/js-core",
33
"workspaces": [
4+
"packages/ai-providers/server-ai-langchain",
45
"packages/shared/common",
56
"packages/shared/sdk-client",
67
"packages/shared/sdk-server",
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { AIMessage, HumanMessage, SystemMessage } from 'langchain/schema';
2+
3+
import { LangChainProvider } from '../src/LangChainProvider';
4+
5+
describe('LangChainProvider', () => {
6+
describe('convertMessagesToLangChain', () => {
7+
it('converts system messages to SystemMessage', () => {
8+
const messages = [{ role: 'system' as const, content: 'You are a helpful assistant.' }];
9+
const result = LangChainProvider.convertMessagesToLangChain(messages);
10+
11+
expect(result).toHaveLength(1);
12+
expect(result[0]).toBeInstanceOf(SystemMessage);
13+
expect(result[0].content).toBe('You are a helpful assistant.');
14+
});
15+
16+
it('converts user messages to HumanMessage', () => {
17+
const messages = [{ role: 'user' as const, content: 'Hello, how are you?' }];
18+
const result = LangChainProvider.convertMessagesToLangChain(messages);
19+
20+
expect(result).toHaveLength(1);
21+
expect(result[0]).toBeInstanceOf(HumanMessage);
22+
expect(result[0].content).toBe('Hello, how are you?');
23+
});
24+
25+
it('converts assistant messages to AIMessage', () => {
26+
const messages = [{ role: 'assistant' as const, content: 'I am doing well, thank you!' }];
27+
const result = LangChainProvider.convertMessagesToLangChain(messages);
28+
29+
expect(result).toHaveLength(1);
30+
expect(result[0]).toBeInstanceOf(AIMessage);
31+
expect(result[0].content).toBe('I am doing well, thank you!');
32+
});
33+
34+
it('converts multiple messages in order', () => {
35+
const messages = [
36+
{ role: 'system' as const, content: 'You are a helpful assistant.' },
37+
{ role: 'user' as const, content: 'What is the weather like?' },
38+
{ role: 'assistant' as const, content: 'I cannot check the weather.' },
39+
];
40+
const result = LangChainProvider.convertMessagesToLangChain(messages);
41+
42+
expect(result).toHaveLength(3);
43+
expect(result[0]).toBeInstanceOf(SystemMessage);
44+
expect(result[1]).toBeInstanceOf(HumanMessage);
45+
expect(result[2]).toBeInstanceOf(AIMessage);
46+
});
47+
48+
it('throws error for unsupported message role', () => {
49+
const messages = [{ role: 'unknown' as any, content: 'Test message' }];
50+
51+
expect(() => LangChainProvider.convertMessagesToLangChain(messages)).toThrow(
52+
'Unsupported message role: unknown'
53+
);
54+
});
55+
56+
it('handles empty message array', () => {
57+
const result = LangChainProvider.convertMessagesToLangChain([]);
58+
59+
expect(result).toHaveLength(0);
60+
});
61+
});
62+
});
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
module.exports = {
2+
preset: 'ts-jest',
3+
testEnvironment: 'node',
4+
roots: ['<rootDir>/src'],
5+
testMatch: ['**/__tests__/**/*.test.ts'],
6+
collectCoverageFrom: ['src/**/*.ts', '!src/**/*.d.ts'],
7+
coverageDirectory: 'coverage',
8+
coverageReporters: ['text', 'lcov', 'html'],
9+
};
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
{
2+
"name": "@launchdarkly/server-sdk-ai-langchain",
3+
"version": "0.1.0-alpha.0",
4+
"description": "LaunchDarkly AI SDK LangChain Provider for Server-Side JavaScript",
5+
"homepage": "https://github.com/launchdarkly/js-core/tree/main/packages/ai-providers/server-ai-langchain",
6+
"repository": {
7+
"type": "git",
8+
"url": "https://github.com/launchdarkly/js-core.git"
9+
},
10+
"main": "dist/index.js",
11+
"types": "dist/index.d.ts",
12+
"type": "commonjs",
13+
"scripts": {
14+
"build": "npx tsc",
15+
"lint": "npx eslint . --ext .ts",
16+
"prettier": "prettier --write '**/*.@(js|ts|tsx|json|css)' --ignore-path ../../../.prettierignore",
17+
"lint:fix": "yarn run lint --fix",
18+
"check": "yarn prettier && yarn lint && yarn build && yarn test",
19+
"test": "jest"
20+
},
21+
"keywords": [
22+
"launchdarkly",
23+
"ai",
24+
"llm",
25+
"langchain"
26+
],
27+
"author": "LaunchDarkly",
28+
"license": "Apache-2.0",
29+
"dependencies": {
30+
"@langchain/core": ">=0.2.21 <0.3.0",
31+
"@launchdarkly/server-sdk-ai": "0.11.4",
32+
"langchain": "^0.2.11"
33+
},
34+
"devDependencies": {
35+
"@launchdarkly/js-server-sdk-common": "2.16.2",
36+
"@trivago/prettier-plugin-sort-imports": "^4.1.1",
37+
"@types/jest": "^29.5.3",
38+
"@typescript-eslint/eslint-plugin": "^6.20.0",
39+
"@typescript-eslint/parser": "^6.20.0",
40+
"eslint": "^8.45.0",
41+
"eslint-config-airbnb-base": "^15.0.0",
42+
"eslint-config-airbnb-typescript": "^17.1.0",
43+
"eslint-config-prettier": "^8.8.0",
44+
"eslint-plugin-import": "^2.27.5",
45+
"eslint-plugin-jest": "^27.6.3",
46+
"eslint-plugin-prettier": "^5.0.0",
47+
"jest": "^29.6.1",
48+
"prettier": "^3.0.0",
49+
"ts-jest": "^29.1.1",
50+
"typescript": "5.1.6"
51+
},
52+
"peerDependencies": {
53+
"@launchdarkly/js-server-sdk-common": "2.x"
54+
}
55+
}
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import { BaseChatModel } from '@langchain/core/language_models/chat_models';
2+
import { AIMessage, HumanMessage, SystemMessage } from '@langchain/core/messages';
3+
import { initChatModel } from 'langchain/chat_models/universal';
4+
5+
import {
6+
LDAIConfig,
7+
LDAIConfigTracker,
8+
LDMessage,
9+
LDTokenUsage,
10+
} from '@launchdarkly/server-sdk-ai';
11+
12+
/**
13+
* LangChain provider utilities and helper functions.
14+
*/
15+
export class LangChainProvider {
16+
/**
17+
* Map LaunchDarkly provider names to LangChain provider names.
18+
* This method enables seamless integration between LaunchDarkly's standardized
19+
* provider naming and LangChain's naming conventions.
20+
*/
21+
static mapProvider(ldProviderName: string): string {
22+
const lowercasedName = ldProviderName.toLowerCase();
23+
24+
const mapping: Record<string, string> = {
25+
gemini: 'google-genai',
26+
};
27+
28+
return mapping[lowercasedName] || lowercasedName;
29+
}
30+
31+
/**
32+
* Create token usage information from a LangChain provider response.
33+
* This method extracts token usage information from LangChain responses
34+
* and returns a LaunchDarkly TokenUsage object.
35+
*/
36+
static createTokenUsage(langChainResponse: AIMessage): LDTokenUsage | undefined {
37+
if (!langChainResponse?.response_metadata?.tokenUsage) {
38+
return undefined;
39+
}
40+
41+
const { tokenUsage } = langChainResponse.response_metadata;
42+
43+
return {
44+
total: tokenUsage.totalTokens || 0,
45+
input: tokenUsage.promptTokens || 0,
46+
output: tokenUsage.completionTokens || 0,
47+
};
48+
}
49+
50+
/**
51+
* Convert LaunchDarkly messages to LangChain messages.
52+
* This helper method enables developers to work directly with LangChain message types
53+
* while maintaining compatibility with LaunchDarkly's standardized message format.
54+
*/
55+
static convertMessagesToLangChain(
56+
messages: LDMessage[],
57+
): (HumanMessage | SystemMessage | AIMessage)[] {
58+
return messages.map((msg) => {
59+
switch (msg.role) {
60+
case 'system':
61+
return new SystemMessage(msg.content);
62+
case 'user':
63+
return new HumanMessage(msg.content);
64+
case 'assistant':
65+
return new AIMessage(msg.content);
66+
default:
67+
throw new Error(`Unsupported message role: ${msg.role}`);
68+
}
69+
});
70+
}
71+
72+
/**
73+
* Track metrics for a LangChain callable execution.
74+
* This helper method enables developers to work directly with LangChain callables
75+
* while ensuring consistent tracking behavior.
76+
*/
77+
static async trackMetricsOf(
78+
tracker: LDAIConfigTracker,
79+
callable: () => Promise<AIMessage>,
80+
): Promise<AIMessage> {
81+
return tracker.trackDurationOf(async () => {
82+
try {
83+
const result = await callable();
84+
85+
// Extract and track token usage if available
86+
const tokenUsage = this.createTokenUsage(result);
87+
if (tokenUsage) {
88+
tracker.trackTokens({
89+
total: tokenUsage.total,
90+
input: tokenUsage.input,
91+
output: tokenUsage.output,
92+
});
93+
}
94+
95+
tracker.trackSuccess();
96+
return result;
97+
} catch (error) {
98+
tracker.trackError();
99+
throw error;
100+
}
101+
});
102+
}
103+
104+
/**
105+
* Create a LangChain model from an AI configuration.
106+
* This public helper method enables developers to initialize their own LangChain models
107+
* using LaunchDarkly AI configurations.
108+
*
109+
* @param aiConfig The LaunchDarkly AI configuration
110+
* @returns A Promise that resolves to a configured LangChain BaseChatModel
111+
*/
112+
static async createLangChainModel(aiConfig: LDAIConfig): Promise<BaseChatModel> {
113+
const modelName = aiConfig.model?.name || '';
114+
const provider = aiConfig.provider?.name || '';
115+
const parameters = aiConfig.model?.parameters || {};
116+
117+
// Use LangChain's universal initChatModel to support multiple providers
118+
return initChatModel(modelName, {
119+
modelProvider: this.mapProvider(provider),
120+
...parameters,
121+
});
122+
}
123+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { BaseChatModel } from '@langchain/core/language_models/chat_models';
2+
import { AIMessage, HumanMessage, SystemMessage } from '@langchain/core/messages';
3+
4+
import {
5+
BaseTrackedChat,
6+
ChatResponse,
7+
LDAIConfig,
8+
LDAIConfigTracker,
9+
LDMessage,
10+
} from '@launchdarkly/server-sdk-ai';
11+
12+
import { LangChainProvider } from './LangChainProvider';
13+
14+
/**
15+
* LangChain-specific implementation of TrackedChat.
16+
* This implementation integrates LangChain models with LaunchDarkly's tracking capabilities.
17+
*/
18+
export class LangChainTrackedChat extends BaseTrackedChat {
19+
private _llm: BaseChatModel;
20+
21+
constructor(aiConfig: LDAIConfig, tracker: LDAIConfigTracker, llm: BaseChatModel) {
22+
super(aiConfig, tracker);
23+
this._llm = llm;
24+
}
25+
26+
/**
27+
* Provider-specific implementation that converts LDMessage[] to LangChain format,
28+
* invokes the model, and returns a ChatResponse.
29+
*/
30+
protected async invokeModel(messages: LDMessage[]): Promise<ChatResponse> {
31+
// Convert LDMessage[] to LangChain messages
32+
const langchainMessages = LangChainProvider.convertMessagesToLangChain(messages);
33+
34+
// Get the LangChain response
35+
const response = await this._llm.invoke(langchainMessages);
36+
37+
// Extract token usage if available using the helper method
38+
const usage = LangChainProvider.createTokenUsage(response);
39+
40+
// Handle different content types from LangChain
41+
let content: string;
42+
if (typeof response.content === 'string') {
43+
content = response.content;
44+
} else if (Array.isArray(response.content)) {
45+
// Handle complex content (e.g., with images)
46+
content = response.content
47+
.map((item: any) => {
48+
if (typeof item === 'string') return item;
49+
if (item.type === 'text') return item.text;
50+
return '';
51+
})
52+
.join('');
53+
} else {
54+
content = String(response.content);
55+
}
56+
57+
// Create the assistant message
58+
const assistantMessage: LDMessage = {
59+
role: 'assistant',
60+
content,
61+
};
62+
63+
return {
64+
message: assistantMessage,
65+
usage,
66+
};
67+
}
68+
69+
/**
70+
* LangChain-specific invoke method that accepts LangChain-native message types.
71+
* This is the main implementation that does all the tracking and LangChain logic.
72+
*/
73+
async trackLangChainInvoke(
74+
messages: (HumanMessage | SystemMessage | AIMessage)[],
75+
): Promise<AIMessage> {
76+
// Use the trackMetricsOf helper to handle all tracking automatically
77+
return LangChainProvider.trackMetricsOf(this.tracker, () => this._llm.invoke(messages));
78+
}
79+
80+
/**
81+
* Get the underlying LangChain model instance.
82+
*/
83+
async getChatModel(): Promise<BaseChatModel> {
84+
return this._llm;
85+
}
86+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/**
2+
* This is the API reference for the LaunchDarkly AI SDK LangChain Provider for Server-Side JavaScript.
3+
*
4+
* This package provides LangChain integration for the LaunchDarkly AI SDK, allowing you to use
5+
* LangChain models and chains with LaunchDarkly's tracking and configuration capabilities.
6+
*
7+
* @packageDocumentation
8+
*/
9+
10+
export * from './LangChainTrackedChat';
11+
export * from './LangChainProvider';

0 commit comments

Comments
 (0)