Skip to content

Commit 0427908

Browse files
authored
feat!: Support invoke with structured output in LangChain provider (#970)
1 parent 515dbdf commit 0427908

File tree

2 files changed

+155
-31
lines changed

2 files changed

+155
-31
lines changed

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

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,66 @@ describe('LangChainProvider', () => {
155155
expect(result.message.content).toBe('');
156156
expect(mockLogger.warn).toHaveBeenCalledTimes(1);
157157
});
158+
159+
it('returns success=false when model invocation throws an error', async () => {
160+
const error = new Error('Model invocation failed');
161+
mockLLM.invoke.mockRejectedValue(error);
162+
163+
const messages = [{ role: 'user' as const, content: 'Hello' }];
164+
const result = await provider.invokeModel(messages);
165+
166+
expect(result.metrics.success).toBe(false);
167+
expect(result.message.content).toBe('');
168+
expect(result.message.role).toBe('assistant');
169+
expect(mockLogger.warn).toHaveBeenCalledWith('LangChain model invocation failed:', error);
170+
});
171+
});
172+
173+
describe('invokeStructuredModel', () => {
174+
let mockLLM: any;
175+
let provider: LangChainProvider;
176+
177+
beforeEach(() => {
178+
mockLLM = {
179+
withStructuredOutput: jest.fn(),
180+
};
181+
provider = new LangChainProvider(mockLLM, mockLogger);
182+
jest.clearAllMocks();
183+
});
184+
185+
it('returns success=true for successful invocation', async () => {
186+
const mockResponse = { result: 'structured data' };
187+
const mockInvoke = jest.fn().mockResolvedValue(mockResponse);
188+
mockLLM.withStructuredOutput.mockReturnValue({ invoke: mockInvoke });
189+
190+
const messages = [{ role: 'user' as const, content: 'Hello' }];
191+
const responseStructure = { type: 'object', properties: {} };
192+
const result = await provider.invokeStructuredModel(messages, responseStructure);
193+
194+
expect(result.metrics.success).toBe(true);
195+
expect(result.data).toEqual(mockResponse);
196+
expect(result.rawResponse).toBe(JSON.stringify(mockResponse));
197+
expect(mockLogger.warn).not.toHaveBeenCalled();
198+
});
199+
200+
it('returns success=false when structured model invocation throws an error', async () => {
201+
const error = new Error('Structured invocation failed');
202+
const mockInvoke = jest.fn().mockRejectedValue(error);
203+
mockLLM.withStructuredOutput.mockReturnValue({ invoke: mockInvoke });
204+
205+
const messages = [{ role: 'user' as const, content: 'Hello' }];
206+
const responseStructure = { type: 'object', properties: {} };
207+
const result = await provider.invokeStructuredModel(messages, responseStructure);
208+
209+
expect(result.metrics.success).toBe(false);
210+
expect(result.data).toEqual({});
211+
expect(result.rawResponse).toBe('');
212+
expect(result.metrics.usage).toEqual({ total: 0, input: 0, output: 0 });
213+
expect(mockLogger.warn).toHaveBeenCalledWith(
214+
'LangChain structured model invocation failed:',
215+
error,
216+
);
217+
});
158218
});
159219

160220
describe('mapProvider', () => {

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

Lines changed: 95 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import type {
1010
LDLogger,
1111
LDMessage,
1212
LDTokenUsage,
13+
StructuredResponse,
1314
} from '@launchdarkly/server-sdk-ai';
1415

1516
/**
@@ -44,39 +45,102 @@ export class LangChainProvider extends AIProvider {
4445
* Invoke the LangChain model with an array of messages.
4546
*/
4647
async invokeModel(messages: LDMessage[]): Promise<ChatResponse> {
47-
// Convert LDMessage[] to LangChain messages
48-
const langchainMessages = LangChainProvider.convertMessagesToLangChain(messages);
49-
50-
// Get the LangChain response
51-
const response: AIMessage = await this._llm.invoke(langchainMessages);
52-
53-
// Generate metrics early (assumes success by default)
54-
const metrics = LangChainProvider.getAIMetricsFromResponse(response);
55-
56-
// Extract text content from the response
57-
let content: string = '';
58-
if (typeof response.content === 'string') {
59-
content = response.content;
60-
} else {
61-
// Log warning for non-string content (likely multimodal)
62-
this.logger?.warn(
63-
`Multimodal response not supported, expecting a string. Content type: ${typeof response.content}, Content:`,
64-
JSON.stringify(response.content, null, 2),
65-
);
66-
// Update metrics to reflect content loss
67-
metrics.success = false;
48+
try {
49+
// Convert LDMessage[] to LangChain messages
50+
const langchainMessages = LangChainProvider.convertMessagesToLangChain(messages);
51+
52+
// Get the LangChain response
53+
const response: AIMessage = await this._llm.invoke(langchainMessages);
54+
55+
// Generate metrics early (assumes success by default)
56+
const metrics = LangChainProvider.getAIMetricsFromResponse(response);
57+
58+
// Extract text content from the response
59+
let content: string = '';
60+
if (typeof response.content === 'string') {
61+
content = response.content;
62+
} else {
63+
// Log warning for non-string content (likely multimodal)
64+
this.logger?.warn(
65+
`Multimodal response not supported, expecting a string. Content type: ${typeof response.content}, Content:`,
66+
JSON.stringify(response.content, null, 2),
67+
);
68+
// Update metrics to reflect content loss
69+
metrics.success = false;
70+
}
71+
72+
// Create the assistant message
73+
const assistantMessage: LDMessage = {
74+
role: 'assistant',
75+
content,
76+
};
77+
78+
return {
79+
message: assistantMessage,
80+
metrics,
81+
};
82+
} catch (error) {
83+
this.logger?.warn('LangChain model invocation failed:', error);
84+
85+
return {
86+
message: {
87+
role: 'assistant',
88+
content: '',
89+
},
90+
metrics: {
91+
success: false,
92+
},
93+
};
6894
}
95+
}
6996

70-
// Create the assistant message
71-
const assistantMessage: LDMessage = {
72-
role: 'assistant',
73-
content,
74-
};
97+
/**
98+
* Invoke the LangChain model with structured output support.
99+
*/
100+
async invokeStructuredModel(
101+
messages: LDMessage[],
102+
responseStructure: Record<string, unknown>,
103+
): Promise<StructuredResponse> {
104+
try {
105+
// Convert LDMessage[] to LangChain messages
106+
const langchainMessages = LangChainProvider.convertMessagesToLangChain(messages);
75107

76-
return {
77-
message: assistantMessage,
78-
metrics,
79-
};
108+
// Get the LangChain response
109+
const response = await this._llm
110+
.withStructuredOutput(responseStructure)
111+
.invoke(langchainMessages);
112+
113+
// Using structured output doesn't support metrics
114+
const metrics = {
115+
success: true,
116+
usage: {
117+
total: 0,
118+
input: 0,
119+
output: 0,
120+
},
121+
};
122+
123+
return {
124+
data: response,
125+
rawResponse: JSON.stringify(response),
126+
metrics,
127+
};
128+
} catch (error) {
129+
this.logger?.warn('LangChain structured model invocation failed:', error);
130+
131+
return {
132+
data: {},
133+
rawResponse: '',
134+
metrics: {
135+
success: false,
136+
usage: {
137+
total: 0,
138+
input: 0,
139+
output: 0,
140+
},
141+
},
142+
};
143+
}
80144
}
81145

82146
/**
@@ -191,8 +255,8 @@ export class LangChainProvider extends AIProvider {
191255

192256
// Use LangChain's universal initChatModel to support multiple providers
193257
return initChatModel(modelName, {
194-
modelProvider: LangChainProvider.mapProvider(provider),
195258
...parameters,
259+
modelProvider: LangChainProvider.mapProvider(provider),
196260
});
197261
}
198262
}

0 commit comments

Comments
 (0)