Skip to content

Commit 4c902f5

Browse files
authored
fix: langchain version vulnerability (lightdash#13440)
1 parent 5b0dab5 commit 4c902f5

File tree

4 files changed

+214
-301
lines changed

4 files changed

+214
-301
lines changed

Diff for: packages/backend/package.json

+3-3
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,8 @@
3838
"@casl/ability": "^5.4.3",
3939
"@cubejs-client/core": "^0.35.23",
4040
"@godaddy/terminus": "^4.12.0",
41-
"@langchain/core": "^0.2.0",
42-
"@langchain/openai": "^0.0.33",
41+
"@langchain/core": "^0.3.36",
42+
"@langchain/openai": "^0.4.2",
4343
"@lightdash/common": "workspace:*",
4444
"@lightdash/warehouses": "workspace:*",
4545
"@octokit/app": "^14.0.2",
@@ -88,7 +88,7 @@
8888
"js-yaml": "^4.1.0",
8989
"jsonwebtoken": "^9.0.2",
9090
"knex": "^2.5.1",
91-
"langchain": "^0.2.0",
91+
"langchain": "^0.3.13",
9292
"lodash": "^4.17.21",
9393
"marked": "^9.0.3",
9494
"moment": "^2.29.4",

Diff for: packages/backend/src/ee/services/AiService/AiService.ts

+5-3
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ import {
3030
} from '@lightdash/common';
3131
import * as Sentry from '@sentry/node';
3232
import { AgentExecutor } from 'langchain/agents';
33-
import { intersection, pick } from 'lodash';
33+
import { /* intersection, */ pick } from 'lodash';
3434
import moment from 'moment';
3535
import slackifyMarkdown from 'slackify-markdown';
3636
import { LightdashAnalytics } from '../../../analytics/LightdashAnalytics';
@@ -398,7 +398,7 @@ export class AiService {
398398
// .filter(
399399
// (explore) =>
400400
// !availableTags ||
401-
// intersection(explore.tags, availableTags).length > 0,
401+
// /* (explore. */tags, availableTags).length > 0,
402402
// )
403403
.map((s) => ({
404404
...pick(s, ['name', 'label', 'description', 'baseTable']),
@@ -429,7 +429,7 @@ export class AiService {
429429
// TODO: enable this to allow filtering by tags
430430
// if (
431431
// availableTags &&
432-
// intersection(explore.tags, availableTags).length === 0
432+
// /* (explore. */tags, availableTags).length === 0
433433
// ) {
434434
// throw new Error('Explore is not available');
435435
// }
@@ -642,6 +642,8 @@ export class AiService {
642642
prompt,
643643
tools,
644644
streamRunnable: false,
645+
// TODO: enable this when we have a way to have openai compliant zod/json schemas
646+
// strict: true,
645647
});
646648

647649
const agentExecutor = new AgentExecutor({

Diff for: packages/backend/src/ee/services/AiService/utils/langchainUtils.ts

+98-19
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
// ℹ️ This file was originally sourced from the LangChain repository.
2+
// It has been modified to allow a different ref strategy
3+
// and to improve logging and error handling.
4+
5+
import {
6+
FunctionDefinition,
7+
ToolDefinition,
8+
} from '@langchain/core/language_models/base';
19
import {
210
AIMessage,
311
BaseMessage,
@@ -14,8 +22,13 @@ import {
1422
RunnableLike,
1523
RunnablePassthrough,
1624
RunnableSequence,
25+
RunnableToolLike,
1726
} from '@langchain/core/runnables';
18-
import type { StructuredToolInterface } from '@langchain/core/tools';
27+
import type {
28+
StructuredToolInterface,
29+
StructuredToolParams,
30+
} from '@langchain/core/tools';
31+
import { isLangChainTool } from '@langchain/core/utils/function_calling';
1932
import { OpenAIClient } from '@langchain/openai';
2033
import { AnyType } from '@lightdash/common';
2134
import {
@@ -76,25 +89,88 @@ export class AgentRunnableSequence<
7689
}
7790
}
7891

79-
function formatToOpenAIAssistantTool(tool: StructuredToolInterface) {
80-
const jsonSchema = zodToJsonSchema(tool.schema, {
81-
$refStrategy: 'none',
82-
});
83-
84-
// console.debug('~~~~~~~~~~~~~~~~~~~~');
85-
// console.debug(jsonSchema);
86-
// console.debug('~~~~~~~~~~~~~~~~~~~~');
92+
/**
93+
* Formats a `StructuredTool` or `RunnableToolLike` instance into a format
94+
* that is compatible with OpenAI function calling. It uses the `zodToJsonSchema`
95+
* function to convert the schema of the `StructuredTool` or `RunnableToolLike`
96+
* into a JSON schema, which is then used as the parameters for the OpenAI function.
97+
*
98+
* @param {StructuredToolInterface | RunnableToolLike} tool The tool to convert to an OpenAI function.
99+
* @returns {FunctionDefinition} The inputted tool in OpenAI function format.
100+
*/
101+
export function convertToOpenAIFunction(
102+
tool: StructuredToolInterface | RunnableToolLike | StructuredToolParams,
103+
fields?:
104+
| {
105+
/**
106+
* If `true`, model output is guaranteed to exactly match the JSON Schema
107+
* provided in the function definition.
108+
*/
109+
strict?: boolean;
110+
}
111+
| number,
112+
): FunctionDefinition {
113+
// @TODO 0.3.0 Remove the `number` typing
114+
const fieldsCopy = typeof fields === 'number' ? undefined : fields;
87115

88116
return {
89-
type: 'function',
90-
function: {
91-
name: tool.name,
92-
description: tool.description,
93-
parameters: jsonSchema,
94-
},
117+
name: tool.name,
118+
description: tool.description,
119+
parameters: zodToJsonSchema(tool.schema, {
120+
// ℹ️ we have to use `none` strategy, otherwise it breaks.
121+
$refStrategy: 'none',
122+
}),
123+
// Do not include the `strict` field if it is `undefined`.
124+
...(fieldsCopy?.strict !== undefined
125+
? { strict: fieldsCopy.strict }
126+
: {}),
95127
};
96128
}
97129

130+
/**
131+
* Formats a `StructuredTool` or `RunnableToolLike` instance into a
132+
* format that is compatible with OpenAI tool calling. It uses the
133+
* `zodToJsonSchema` function to convert the schema of the `StructuredTool`
134+
* or `RunnableToolLike` into a JSON schema, which is then used as the
135+
* parameters for the OpenAI tool.
136+
*
137+
* @param {StructuredToolInterface | Record<string, any> | RunnableToolLike} tool The tool to convert to an OpenAI tool.
138+
* @returns {ToolDefinition} The inputted tool in OpenAI tool format.
139+
*/
140+
export function convertToOpenAITool(
141+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
142+
tool: StructuredToolInterface | Record<string, any> | RunnableToolLike,
143+
fields?:
144+
| {
145+
/**
146+
* If `true`, model output is guaranteed to exactly match the JSON Schema
147+
* provided in the function definition.
148+
*/
149+
strict?: boolean;
150+
}
151+
| number,
152+
): ToolDefinition {
153+
// @TODO 0.3.0 Remove the `number` typing
154+
const fieldsCopy = typeof fields === 'number' ? undefined : fields;
155+
156+
let toolDef: ToolDefinition | undefined;
157+
if (isLangChainTool(tool)) {
158+
toolDef = {
159+
type: 'function',
160+
function: convertToOpenAIFunction(tool),
161+
};
162+
} else {
163+
toolDef = tool as ToolDefinition;
164+
}
165+
166+
if (fieldsCopy?.strict !== undefined) {
167+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
168+
(toolDef.function as any).strict = fieldsCopy.strict;
169+
}
170+
171+
return toolDef;
172+
}
173+
98174
/**
99175
* Convert agent action and observation into a function message.
100176
* @param agentAction - The tool invocation request from the agent
@@ -195,8 +271,8 @@ export class OpenAIToolsAgentOutputParser extends AgentMultiActionOutputParser {
195271
throw new OutputParserException(
196272
`Failed to parse tool arguments from chat model response. Text: "${JSON.stringify(
197273
toolCalls,
198-
)}".
199-
274+
)}".
275+
200276
${error}
201277
202278
Analyze what fields are missing based on the schema.
@@ -231,7 +307,8 @@ export async function createOpenAIToolsAgent({
231307
tools,
232308
prompt,
233309
streamRunnable,
234-
}: CreateOpenAIToolsAgentParams) {
310+
strict,
311+
}: CreateOpenAIToolsAgentParams & { strict?: boolean }) {
235312
if (!prompt.inputVariables.includes('agent_scratchpad')) {
236313
throw new Error(
237314
[
@@ -240,9 +317,11 @@ export async function createOpenAIToolsAgent({
240317
].join('\n'),
241318
);
242319
}
320+
243321
const modelWithTools = llm.bind({
244-
tools: tools.map(formatToOpenAIAssistantTool),
322+
tools: tools.map((tool) => convertToOpenAITool(tool, { strict })),
245323
});
324+
246325
const agent = AgentRunnableSequence.fromRunnables(
247326
[
248327
RunnablePassthrough.assign({

0 commit comments

Comments
 (0)