Skip to content

Commit 2143219

Browse files
authored
feat: Convert LangChain implementation to new AIProvider interface (#942)
1 parent a08bfa0 commit 2143219

File tree

6 files changed

+170
-143
lines changed

6 files changed

+170
-143
lines changed

packages/ai-providers/server-ai-langchain/__tests__/LangChainProvider.test.ts

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
1-
import { AIMessage, HumanMessage, SystemMessage } from 'langchain/schema';
1+
import { AIMessage, HumanMessage, SystemMessage } from '@langchain/core/messages';
22

33
import { LangChainProvider } from '../src/LangChainProvider';
44

5+
// Mock LangChain dependencies
6+
jest.mock('langchain/chat_models/universal', () => ({
7+
initChatModel: jest.fn(),
8+
}));
9+
510
describe('LangChainProvider', () => {
611
describe('convertMessagesToLangChain', () => {
712
it('converts system messages to SystemMessage', () => {
@@ -49,7 +54,7 @@ describe('LangChainProvider', () => {
4954
const messages = [{ role: 'unknown' as any, content: 'Test message' }];
5055

5156
expect(() => LangChainProvider.convertMessagesToLangChain(messages)).toThrow(
52-
'Unsupported message role: unknown'
57+
'Unsupported message role: unknown',
5358
);
5459
});
5560

@@ -59,4 +64,53 @@ describe('LangChainProvider', () => {
5964
expect(result).toHaveLength(0);
6065
});
6166
});
67+
68+
describe('createAIMetrics', () => {
69+
it('creates metrics with success=true and token usage', () => {
70+
const mockResponse = new AIMessage('Test response');
71+
mockResponse.response_metadata = {
72+
tokenUsage: {
73+
totalTokens: 100,
74+
promptTokens: 50,
75+
completionTokens: 50,
76+
},
77+
};
78+
79+
const result = LangChainProvider.createAIMetrics(mockResponse);
80+
81+
expect(result).toEqual({
82+
success: true,
83+
usage: {
84+
total: 100,
85+
input: 50,
86+
output: 50,
87+
},
88+
});
89+
});
90+
91+
it('creates metrics with success=true and no usage when metadata is missing', () => {
92+
const mockResponse = new AIMessage('Test response');
93+
94+
const result = LangChainProvider.createAIMetrics(mockResponse);
95+
96+
expect(result).toEqual({
97+
success: true,
98+
usage: undefined,
99+
});
100+
});
101+
});
102+
103+
describe('mapProvider', () => {
104+
it('maps gemini to google-genai', () => {
105+
expect(LangChainProvider.mapProvider('gemini')).toBe('google-genai');
106+
expect(LangChainProvider.mapProvider('Gemini')).toBe('google-genai');
107+
expect(LangChainProvider.mapProvider('GEMINI')).toBe('google-genai');
108+
});
109+
110+
it('returns provider name unchanged for unmapped providers', () => {
111+
expect(LangChainProvider.mapProvider('openai')).toBe('openai');
112+
expect(LangChainProvider.mapProvider('anthropic')).toBe('anthropic');
113+
expect(LangChainProvider.mapProvider('unknown')).toBe('unknown');
114+
});
115+
});
62116
});
Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
module.exports = {
2-
preset: 'ts-jest',
2+
transform: { '^.+\\.ts?$': 'ts-jest' },
3+
testMatch: ['**/__tests__/**/*test.ts?(x)'],
34
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'],
5+
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
6+
collectCoverageFrom: ['src/**/*.ts'],
97
};

packages/ai-providers/server-ai-langchain/src/LangChainProvider.ts

Lines changed: 108 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,95 @@ import { AIMessage, HumanMessage, SystemMessage } from '@langchain/core/messages
33
import { initChatModel } from 'langchain/chat_models/universal';
44

55
import {
6+
AIProvider,
7+
ChatResponse,
68
LDAIConfig,
7-
LDAIConfigTracker,
9+
LDAIMetrics,
810
LDMessage,
911
LDTokenUsage,
1012
} from '@launchdarkly/server-sdk-ai';
1113

1214
/**
13-
* LangChain provider utilities and helper functions.
15+
* LangChain implementation of AIProvider.
16+
* This provider integrates LangChain models with LaunchDarkly's tracking capabilities.
1417
*/
15-
export class LangChainProvider {
18+
export class LangChainProvider extends AIProvider {
19+
private _llm: BaseChatModel;
20+
21+
constructor(llm: BaseChatModel) {
22+
super();
23+
this._llm = llm;
24+
}
25+
26+
// =============================================================================
27+
// MAIN FACTORY METHOD
28+
// =============================================================================
29+
30+
/**
31+
* Static factory method to create a LangChain AIProvider from an AI configuration.
32+
*/
33+
static async create(aiConfig: LDAIConfig): Promise<LangChainProvider> {
34+
const llm = await LangChainProvider.createLangChainModel(aiConfig);
35+
return new LangChainProvider(llm);
36+
}
37+
38+
// =============================================================================
39+
// INSTANCE METHODS (AIProvider Implementation)
40+
// =============================================================================
41+
42+
/**
43+
* Invoke the LangChain model with an array of messages.
44+
*/
45+
async invokeModel(messages: LDMessage[]): Promise<ChatResponse> {
46+
// Convert LDMessage[] to LangChain messages
47+
const langchainMessages = LangChainProvider.convertMessagesToLangChain(messages);
48+
49+
// Get the LangChain response
50+
const response: AIMessage = await this._llm.invoke(langchainMessages);
51+
52+
// Handle different content types from LangChain
53+
let content: string;
54+
if (typeof response.content === 'string') {
55+
content = response.content;
56+
} else if (Array.isArray(response.content)) {
57+
// Handle complex content (e.g., with images)
58+
content = response.content
59+
.map((item: any) => {
60+
if (typeof item === 'string') return item;
61+
if (item.type === 'text') return item.text;
62+
return '';
63+
})
64+
.join('');
65+
} else {
66+
content = String(response.content);
67+
}
68+
69+
// Create the assistant message
70+
const assistantMessage: LDMessage = {
71+
role: 'assistant',
72+
content,
73+
};
74+
75+
// Extract metrics including token usage and success status
76+
const metrics = LangChainProvider.createAIMetrics(response);
77+
78+
return {
79+
message: assistantMessage,
80+
metrics,
81+
};
82+
}
83+
84+
/**
85+
* Get the underlying LangChain model instance.
86+
*/
87+
getChatModel(): BaseChatModel {
88+
return this._llm;
89+
}
90+
91+
// =============================================================================
92+
// STATIC UTILITY METHODS
93+
// =============================================================================
94+
1695
/**
1796
* Map LaunchDarkly provider names to LangChain provider names.
1897
* This method enables seamless integration between LaunchDarkly's standardized
@@ -29,21 +108,35 @@ export class LangChainProvider {
29108
}
30109

31110
/**
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.
111+
* Create AI metrics information from a LangChain provider response.
112+
* This method extracts token usage information and success status from LangChain responses
113+
* and returns a LaunchDarkly AIMetrics object.
114+
*
115+
* @example
116+
* ```typescript
117+
* // Use with tracker.trackMetricsOf for automatic tracking
118+
* const response = await tracker.trackMetricsOf(
119+
* (result: AIMessage) => LangChainProvider.createAIMetrics(result),
120+
* () => llm.invoke(messages)
121+
* );
122+
* ```
35123
*/
36-
static createTokenUsage(langChainResponse: AIMessage): LDTokenUsage | undefined {
37-
if (!langChainResponse?.response_metadata?.tokenUsage) {
38-
return undefined;
124+
static createAIMetrics(langChainResponse: AIMessage): LDAIMetrics {
125+
// Extract token usage if available
126+
let usage: LDTokenUsage | undefined;
127+
if (langChainResponse?.response_metadata?.tokenUsage) {
128+
const { tokenUsage } = langChainResponse.response_metadata;
129+
usage = {
130+
total: tokenUsage.totalTokens || 0,
131+
input: tokenUsage.promptTokens || 0,
132+
output: tokenUsage.completionTokens || 0,
133+
};
39134
}
40135

41-
const { tokenUsage } = langChainResponse.response_metadata;
42-
136+
// LangChain responses that complete successfully are considered successful
43137
return {
44-
total: tokenUsage.totalTokens || 0,
45-
input: tokenUsage.promptTokens || 0,
46-
output: tokenUsage.completionTokens || 0,
138+
success: true,
139+
usage,
47140
};
48141
}
49142

@@ -69,38 +162,6 @@ export class LangChainProvider {
69162
});
70163
}
71164

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-
104165
/**
105166
* Create a LangChain model from an AI configuration.
106167
* This public helper method enables developers to initialize their own LangChain models
@@ -116,7 +177,7 @@ export class LangChainProvider {
116177

117178
// Use LangChain's universal initChatModel to support multiple providers
118179
return initChatModel(modelName, {
119-
modelProvider: this.mapProvider(provider),
180+
modelProvider: LangChainProvider.mapProvider(provider),
120181
...parameters,
121182
});
122183
}

packages/ai-providers/server-ai-langchain/src/LangChainTrackedChat.ts

Lines changed: 0 additions & 86 deletions
This file was deleted.

packages/ai-providers/server-ai-langchain/src/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,4 @@
77
* @packageDocumentation
88
*/
99

10-
export * from './LangChainTrackedChat';
1110
export * from './LangChainProvider';
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
{
22
"extends": "./tsconfig.json",
3-
"include": ["src/**/*", "**/*.test.ts", "**/*.spec.ts"]
3+
"include": ["/**/*.ts"],
4+
"exclude": ["node_modules"]
45
}

0 commit comments

Comments
 (0)