From d42e61bc357de93087dcb66a07328ad32f14eccc Mon Sep 17 00:00:00 2001 From: Charlie Kolb Date: Wed, 16 Apr 2025 08:48:16 +0200 Subject: [PATCH] feat(HTTP Request Node): Port `optimizeResponse` from httpRequest tool to standalone node (no-changelog) (#14307) --- packages/core/src/constants.ts | 1 + .../node-execution-context.ts | 12 +- packages/frontend/editor-ui/src/constants.ts | 2 + .../nodes/HttpRequest/V3/Description.ts | 9 + .../HttpRequest/V3/HttpRequestV3.node.ts | 25 +- .../shared/optimizeResponse.test.ts | 124 ++++++ .../HttpRequest/shared/optimizeResponse.ts | 418 ++++++++++++++++++ packages/nodes-base/package.json | 2 + packages/workflow/src/Interfaces.ts | 2 +- pnpm-lock.yaml | 30 +- 10 files changed, 602 insertions(+), 23 deletions(-) create mode 100644 packages/nodes-base/nodes/HttpRequest/shared/optimizeResponse.test.ts create mode 100644 packages/nodes-base/nodes/HttpRequest/shared/optimizeResponse.ts diff --git a/packages/core/src/constants.ts b/packages/core/src/constants.ts index d69b3f9170..db453cbb7f 100644 --- a/packages/core/src/constants.ts +++ b/packages/core/src/constants.ts @@ -7,6 +7,7 @@ export const CUSTOM_EXTENSION_ENV = 'N8N_CUSTOM_EXTENSIONS'; export const PLACEHOLDER_EMPTY_EXECUTION_ID = '__UNKNOWN__'; export const PLACEHOLDER_EMPTY_WORKFLOW_ID = '__EMPTY__'; export const HTTP_REQUEST_NODE_TYPE = 'n8n-nodes-base.httpRequest'; +export const HTTP_REQUEST_AS_TOOL_NODE_TYPE = 'n8n-nodes-base.httpRequestTool'; export const HTTP_REQUEST_TOOL_NODE_TYPE = '@n8n/n8n-nodes-langchain.toolHttpRequest'; export const RESTRICT_FILE_ACCESS_TO = 'N8N_RESTRICT_FILE_ACCESS_TO'; diff --git a/packages/core/src/execution-engine/node-execution-context/node-execution-context.ts b/packages/core/src/execution-engine/node-execution-context/node-execution-context.ts index ae431c517e..00e84ee3f3 100644 --- a/packages/core/src/execution-engine/node-execution-context/node-execution-context.ts +++ b/packages/core/src/execution-engine/node-execution-context/node-execution-context.ts @@ -28,7 +28,11 @@ import { NodeOperationError, } from 'n8n-workflow'; -import { HTTP_REQUEST_NODE_TYPE, HTTP_REQUEST_TOOL_NODE_TYPE } from '@/constants'; +import { + HTTP_REQUEST_AS_TOOL_NODE_TYPE, + HTTP_REQUEST_NODE_TYPE, + HTTP_REQUEST_TOOL_NODE_TYPE, +} from '@/constants'; import { Memoized } from '@/decorators'; import { InstanceSettings } from '@/instance-settings'; import { Logger } from '@/logging/logger'; @@ -190,7 +194,11 @@ export abstract class NodeExecutionContext implements Omit ({ + ...prop, + displayOptions: { + ...prop.displayOptions, + show: { ...prop.displayOptions?.show, '@tool': [true] }, + }, + })), { displayName: "You can view the raw requests this node makes in your browser's developer console", diff --git a/packages/nodes-base/nodes/HttpRequest/V3/HttpRequestV3.node.ts b/packages/nodes-base/nodes/HttpRequest/V3/HttpRequestV3.node.ts index 070937a4af..a2ed5897c8 100644 --- a/packages/nodes-base/nodes/HttpRequest/V3/HttpRequestV3.node.ts +++ b/packages/nodes-base/nodes/HttpRequest/V3/HttpRequestV3.node.ts @@ -38,6 +38,7 @@ import { sanitizeUiMessage, setAgentOptions, } from '../GenericFunctions'; +import { configureResponseOptimizer } from '../shared/optimizeResponse'; function toText(data: T) { if (typeof data === 'object' && data !== null) { @@ -832,6 +833,8 @@ export class HttpRequestV3 implements INodeType { } } } + // This is a no-op outside of tool usage + const optimizeResponse = configureResponseOptimizer(this, itemIndex); if (autoDetectResponseFormat && !fullResponse) { delete response.headers; @@ -839,9 +842,10 @@ export class HttpRequestV3 implements INodeType { delete response.statusMessage; } if (!fullResponse) { - response = response.body; + response = optimizeResponse(response.body); + } else { + response.body = optimizeResponse(response.body); } - if (responseFormat === 'file') { const outputPropertyName = this.getNodeParameter( 'options.response.response.outputPropertyName', @@ -911,7 +915,6 @@ export class HttpRequestV3 implements INodeType { returnItem[outputPropertyName] = toText(response[property]); continue; } - returnItem[property] = response[property]; } returnItems.push({ @@ -1001,11 +1004,17 @@ export class HttpRequestV3 implements INodeType { returnItems[0].json.data && Array.isArray(returnItems[0].json.data) ) { - this.addExecutionHints({ - message: - 'To split the contents of ‘data’ into separate items for easier processing, add a ‘Split Out’ node after this one', - location: 'outputPane', - }); + const message = + 'To split the contents of ‘data’ into separate items for easier processing, add a ‘Split Out’ node after this one'; + + if (this.addExecutionHints) { + this.addExecutionHints({ + message, + location: 'outputPane', + }); + } else { + this.logger.info(message); + } } return [returnItems]; diff --git a/packages/nodes-base/nodes/HttpRequest/shared/optimizeResponse.test.ts b/packages/nodes-base/nodes/HttpRequest/shared/optimizeResponse.test.ts new file mode 100644 index 0000000000..efd99fa233 --- /dev/null +++ b/packages/nodes-base/nodes/HttpRequest/shared/optimizeResponse.test.ts @@ -0,0 +1,124 @@ +import { type IExecuteFunctions, NodeOperationError } from 'n8n-workflow'; + +import { configureResponseOptimizer } from './optimizeResponse'; + +describe('configureResponseOptimizer', () => { + const mockCtx = { + getNodeParameter: jest.fn(), + getNode: jest.fn(), + } as unknown as jest.Mocked; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should return the original response when optimizeResponse is false', () => { + mockCtx.getNodeParameter.mockImplementation((param) => { + if (param === 'optimizeResponse') return false; + }); + + const optimizer = configureResponseOptimizer(mockCtx, 0); + const response = { key: 'value' }; + + expect(optimizer(response)).toBe(response); + }); + + describe('htmlOptimizer', () => { + it('should optimize HTML response based on CSS selector', () => { + mockCtx.getNodeParameter.mockImplementation((param) => { + if (param === 'optimizeResponse') return true; + if (param === 'responseType') return 'html'; + if (param === 'cssSelector') return 'div'; + if (param === 'onlyContent') return false; + }); + + const optimizer = configureResponseOptimizer(mockCtx, 0); + const response = '
Hello
World
'; + + expect(optimizer(response)).toEqual('[\n "Hello",\n "World"\n]'); + }); + }); + + describe('textOptimizer', () => { + it('should extract readable text from HTML response', () => { + mockCtx.getNodeParameter.mockImplementation((param) => { + if (param === 'optimizeResponse') return true; + if (param === 'responseType') return 'text'; + }); + + const optimizer = configureResponseOptimizer(mockCtx, 0); + const response = '

Title

Content

'; + + expect(optimizer(response)).toContain('Title'); + expect(optimizer(response)).toContain('Content'); + }); + + it('should truncate text if maxLength is set', () => { + mockCtx.getNodeParameter.mockImplementation((param) => { + if (param === 'optimizeResponse') return true; + if (param === 'responseType') return 'text'; + if (param === 'truncateResponse') return true; + if (param === 'maxLength') return 5; + }); + + const optimizer = configureResponseOptimizer(mockCtx, 0); + const response = '

Content

'; + + expect(optimizer(response)).toEqual('Conte'); + }); + }); + + describe('jsonOptimizer', () => { + it('should parse JSON response and include all fields by default', () => { + mockCtx.getNodeParameter.mockImplementation((param) => { + if (param === 'optimizeResponse') return true; + if (param === 'responseType') return 'json'; + if (param === 'fieldsToInclude') return 'all'; + }); + + const optimizer = configureResponseOptimizer(mockCtx, 0); + const response = '{"key": "value"}'; + + expect(optimizer(response)).toEqual([{ key: 'value' }]); + }); + + it('should include only selected fields', () => { + mockCtx.getNodeParameter.mockImplementation((param) => { + if (param === 'optimizeResponse') return true; + if (param === 'responseType') return 'json'; + if (param === 'fieldsToInclude') return 'selected'; + if (param === 'fields') return 'key'; + }); + + const optimizer = configureResponseOptimizer(mockCtx, 0); + const response = [{ key: 'value', otherKey: 'otherValue' }]; + + expect(optimizer(response)).toEqual([{ key: 'value' }]); + }); + + it('should exclude specified fields', () => { + mockCtx.getNodeParameter.mockImplementation((param) => { + if (param === 'optimizeResponse') return true; + if (param === 'responseType') return 'json'; + if (param === 'fieldsToInclude') return 'except'; + if (param === 'fields') return 'otherKey'; + }); + + const optimizer = configureResponseOptimizer(mockCtx, 0); + const response = [{ key: 'value', otherKey: 'otherValue' }]; + + expect(optimizer(response)).toEqual([{ key: 'value' }]); + }); + + it('should throw an error if response is not a valid JSON object', () => { + mockCtx.getNodeParameter.mockImplementation((param) => { + if (param === 'optimizeResponse') return true; + if (param === 'responseType') return 'json'; + }); + + const optimizer = configureResponseOptimizer(mockCtx, 0); + + expect(() => optimizer('invalid json')).toThrow(NodeOperationError); + }); + }); +}); diff --git a/packages/nodes-base/nodes/HttpRequest/shared/optimizeResponse.ts b/packages/nodes-base/nodes/HttpRequest/shared/optimizeResponse.ts new file mode 100644 index 0000000000..826bb6229a --- /dev/null +++ b/packages/nodes-base/nodes/HttpRequest/shared/optimizeResponse.ts @@ -0,0 +1,418 @@ +import { Readability } from '@mozilla/readability'; +import * as cheerio from 'cheerio'; +import { convert } from 'html-to-text'; +import { JSDOM } from 'jsdom'; +import { get, set, unset } from 'lodash'; +import { + type INodeProperties, + jsonParse, + NodeOperationError, + type IDataObject, + type IExecuteFunctions, +} from 'n8n-workflow'; + +type ResponseOptimizerFn = ( + x: IDataObject | IDataObject[] | string, +) => IDataObject | IDataObject[] | string; + +function htmlOptimizer( + ctx: IExecuteFunctions, + itemIndex: number, + maxLength: number, +): ResponseOptimizerFn { + const cssSelector = ctx.getNodeParameter('cssSelector', itemIndex, '') as string; + const onlyContent = ctx.getNodeParameter('onlyContent', itemIndex, false) as boolean; + let elementsToOmit: string[] = []; + + if (onlyContent) { + const elementsToOmitUi = ctx.getNodeParameter('elementsToOmit', itemIndex, '') as + | string + | string[]; + + if (typeof elementsToOmitUi === 'string') { + elementsToOmit = elementsToOmitUi + .split(',') + .filter((s) => s) + .map((s) => s.trim()); + } + } + + return (response) => { + if (typeof response !== 'string') { + throw new NodeOperationError( + ctx.getNode(), + `The response type must be a string. Received: ${typeof response}`, + { itemIndex }, + ); + } + const returnData: string[] = []; + + const html = cheerio.load(response); + const htmlElements = html(cssSelector); + + htmlElements.each((_, el) => { + let value = html(el).html() || ''; + + if (onlyContent) { + let htmlToTextOptions; + + if (elementsToOmit?.length) { + htmlToTextOptions = { + selectors: elementsToOmit.map((selector) => ({ + selector, + format: 'skip', + })), + }; + } + + value = convert(value, htmlToTextOptions); + } + + value = value + .trim() + .replace(/^\s+|\s+$/g, '') + .replace(/(\r\n|\n|\r)/gm, '') + .replace(/\s+/g, ' '); + + returnData.push(value); + }); + + const text = JSON.stringify(returnData, null, 2); + + if (maxLength > 0 && text.length > maxLength) { + return text.substring(0, maxLength); + } + + return text; + }; +} + +const textOptimizer = ( + ctx: IExecuteFunctions, + itemIndex: number, + maxLength: number, +): ResponseOptimizerFn => { + return (response) => { + if (typeof response === 'object') { + try { + response = JSON.stringify(response, null, 2); + } catch (error) {} + } + + if (typeof response !== 'string') { + throw new NodeOperationError( + ctx.getNode(), + `The response type must be a string. Received: ${typeof response}`, + { itemIndex }, + ); + } + + const dom = new JSDOM(response); + const article = new Readability(dom.window.document, { + keepClasses: true, + }).parse(); + + const text = article?.textContent || ''; + + if (maxLength > 0 && text.length > maxLength) { + return text.substring(0, maxLength); + } + + return text; + }; +}; + +const jsonOptimizer = (ctx: IExecuteFunctions, itemIndex: number): ResponseOptimizerFn => { + return (response) => { + let responseData: IDataObject | IDataObject[] | string | null = response; + + if (typeof response === 'string') { + try { + responseData = jsonParse(response, { errorMessage: 'Invalid JSON response' }); + } catch (error) { + throw new NodeOperationError( + ctx.getNode(), + `Received invalid JSON from response '${response}'`, + { itemIndex }, + ); + } + } + + if (typeof responseData !== 'object' || !responseData) { + throw new NodeOperationError( + ctx.getNode(), + 'The response type must be an object or an array of objects', + { itemIndex }, + ); + } + + const dataField = ctx.getNodeParameter('dataField', itemIndex, '') as string; + let returnData: IDataObject[] = []; + + if (!Array.isArray(responseData)) { + if (dataField) { + const data = responseData[dataField] as IDataObject | IDataObject[]; + if (Array.isArray(data)) { + responseData = data; + } else { + responseData = [data]; + } + } else { + responseData = [responseData]; + } + } else { + if (dataField) { + responseData = responseData.map((data) => data[dataField]) as IDataObject[]; + } + } + + const fieldsToInclude = ctx.getNodeParameter('fieldsToInclude', itemIndex, 'all') as + | 'all' + | 'selected' + | 'except'; + + let fields: string | string[] = []; + + if (fieldsToInclude !== 'all') { + fields = ctx.getNodeParameter('fields', itemIndex, []) as string[] | string; + + if (typeof fields === 'string') { + fields = fields.split(',').map((field) => field.trim()); + } + } else { + returnData = responseData; + } + + if (fieldsToInclude === 'selected') { + for (const item of responseData) { + const newItem: IDataObject = {}; + + for (const field of fields) { + set(newItem, field, get(item, field)); + } + + returnData.push(newItem); + } + } + + if (fieldsToInclude === 'except') { + for (const item of responseData) { + for (const field of fields) { + unset(item, field); + } + + returnData.push(item); + } + } + + return returnData; + }; +}; + +export const configureResponseOptimizer = ( + ctx: IExecuteFunctions, + itemIndex: number, +): ResponseOptimizerFn => { + const optimizeResponse = ctx.getNodeParameter('optimizeResponse', itemIndex, false) as boolean; + + if (optimizeResponse) { + const responseType = ctx.getNodeParameter('responseType', itemIndex) as + | 'json' + | 'text' + | 'html'; + + let maxLength = 0; + const truncateResponse = ctx.getNodeParameter('truncateResponse', itemIndex, false) as boolean; + + if (truncateResponse) { + maxLength = ctx.getNodeParameter('maxLength', itemIndex, 0) as number; + } + + switch (responseType) { + case 'html': + return htmlOptimizer(ctx, itemIndex, maxLength); + case 'text': + return textOptimizer(ctx, itemIndex, maxLength); + case 'json': + return jsonOptimizer(ctx, itemIndex); + } + } + + return (x) => x; +}; + +export const optimizeResponseProperties: INodeProperties[] = [ + { + displayName: 'Optimize Response', + name: 'optimizeResponse', + type: 'boolean', + default: false, + noDataExpression: true, + description: + 'Whether the optimize the tool response to reduce amount of data passed to the LLM that could lead to better result and reduce cost', + }, + { + displayName: 'Expected Response Type', + name: 'responseType', + type: 'options', + displayOptions: { + show: { + optimizeResponse: [true], + }, + }, + options: [ + { + name: 'JSON', + value: 'json', + }, + { + name: 'HTML', + value: 'html', + }, + { + name: 'Text', + value: 'text', + }, + ], + default: 'json', + }, + { + displayName: 'Field Containing Data', + name: 'dataField', + type: 'string', + default: '', + placeholder: 'e.g. records', + description: 'Specify the name of the field in the response containing the data', + hint: 'leave blank to use whole response', + requiresDataPath: 'single', + displayOptions: { + show: { + optimizeResponse: [true], + responseType: ['json'], + }, + }, + }, + { + displayName: 'Include Fields', + name: 'fieldsToInclude', + type: 'options', + description: 'What fields response object should include', + default: 'all', + displayOptions: { + show: { + optimizeResponse: [true], + responseType: ['json'], + }, + }, + options: [ + { + name: 'All', + value: 'all', + description: 'Include all fields', + }, + { + name: 'Selected', + value: 'selected', + description: 'Include only fields specified below', + }, + { + name: 'Except', + value: 'except', + description: 'Exclude fields specified below', + }, + ], + }, + { + displayName: 'Fields', + name: 'fields', + type: 'string', + default: '', + placeholder: 'e.g. field1,field2', + description: + 'Comma-separated list of the field names. Supports dot notation. You can drag the selected fields from the input panel.', + requiresDataPath: 'multiple', + displayOptions: { + show: { + optimizeResponse: [true], + responseType: ['json'], + }, + hide: { + fieldsToInclude: ['all'], + }, + }, + }, + { + displayName: 'Selector (CSS)', + name: 'cssSelector', + type: 'string', + description: + 'Select specific element(e.g. body) or multiple elements(e.g. div) of chosen type in the response HTML.', + placeholder: 'e.g. body', + default: 'body', + displayOptions: { + show: { + optimizeResponse: [true], + responseType: ['html'], + }, + }, + }, + { + displayName: 'Return Only Content', + name: 'onlyContent', + type: 'boolean', + default: false, + description: + 'Whether to return only content of html elements, stripping html tags and attributes', + hint: 'Uses less tokens and may be easier for model to understand', + displayOptions: { + show: { + optimizeResponse: [true], + responseType: ['html'], + }, + }, + }, + { + displayName: 'Elements To Omit', + name: 'elementsToOmit', + type: 'string', + displayOptions: { + show: { + optimizeResponse: [true], + responseType: ['html'], + onlyContent: [true], + }, + }, + default: '', + placeholder: 'e.g. img, .className, #ItemId', + description: 'Comma-separated list of selectors that would be excluded when extracting content', + }, + { + displayName: 'Truncate Response', + name: 'truncateResponse', + type: 'boolean', + default: false, + hint: 'Helps save tokens', + displayOptions: { + show: { + optimizeResponse: [true], + responseType: ['text', 'html'], + }, + }, + }, + { + displayName: 'Max Response Characters', + name: 'maxLength', + type: 'number', + default: 1000, + typeOptions: { + minValue: 1, + }, + displayOptions: { + show: { + optimizeResponse: [true], + responseType: ['text', 'html'], + truncateResponse: [true], + }, + }, + }, +]; diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 0d0e1fdef6..72be53629a 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -873,6 +873,7 @@ "@n8n/di": "workspace:*", "@n8n/imap": "workspace:*", "@n8n/vm2": "3.9.25", + "@mozilla/readability": "0.6.0", "alasql": "4.4.0", "amqplib": "0.10.3", "aws4": "1.11.0", @@ -895,6 +896,7 @@ "isbot": "3.6.13", "iso-639-1": "2.1.15", "js-nacl": "1.4.0", + "jsdom": "23.0.1", "jsonwebtoken": "catalog:", "kafkajs": "2.2.4", "ldapts": "4.2.6", diff --git a/packages/workflow/src/Interfaces.ts b/packages/workflow/src/Interfaces.ts index 493e49baa8..38f168db56 100644 --- a/packages/workflow/src/Interfaces.ts +++ b/packages/workflow/src/Interfaces.ts @@ -1377,7 +1377,7 @@ export interface IDisplayOptions { }; show?: { '@version'?: Array; - '@tool'?: [boolean]; + '@tool'?: boolean[]; [key: string]: Array | undefined; }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 731ab5eef9..87b9137ceb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -558,7 +558,7 @@ importers: version: 3.666.0(@aws-sdk/client-sts@3.666.0) '@getzep/zep-cloud': specifier: 1.0.12 - version: 1.0.12(@langchain/core@0.3.30(openai@4.78.1(encoding@0.1.13)(zod@3.24.1)))(encoding@0.1.13)(langchain@0.3.11(d1e86e144e3517fab3dbb7a92ab7f45a)) + version: 1.0.12(@langchain/core@0.3.30(openai@4.78.1(encoding@0.1.13)(zod@3.24.1)))(encoding@0.1.13)(langchain@0.3.11(fd386e1130022c8548c06dd951c5cbf0)) '@getzep/zep-js': specifier: 0.9.0 version: 0.9.0 @@ -585,7 +585,7 @@ importers: version: 0.3.2(@aws-sdk/client-sso-oidc@3.666.0(@aws-sdk/client-sts@3.666.0))(@langchain/core@0.3.30(openai@4.78.1(encoding@0.1.13)(zod@3.24.1)))(encoding@0.1.13) '@langchain/community': specifier: 0.3.24 - version: 0.3.24(bae0580ee8bea2ce19e4657a460c92d0) + version: 0.3.24(c5fc7e11d6e6167a46cb8d3fd9b490a5) '@langchain/core': specifier: 'catalog:' version: 0.3.30(openai@4.78.1(encoding@0.1.13)(zod@3.24.1)) @@ -684,7 +684,7 @@ importers: version: 23.0.1 langchain: specifier: 0.3.11 - version: 0.3.11(d1e86e144e3517fab3dbb7a92ab7f45a) + version: 0.3.11(fd386e1130022c8548c06dd951c5cbf0) lodash: specifier: 'catalog:' version: 4.17.21 @@ -2016,6 +2016,9 @@ importers: '@kafkajs/confluent-schema-registry': specifier: 3.8.0 version: 3.8.0 + '@mozilla/readability': + specifier: 0.6.0 + version: 0.6.0 '@n8n/config': specifier: workspace:* version: link:../@n8n/config @@ -2094,6 +2097,9 @@ importers: js-nacl: specifier: 1.4.0 version: 1.4.0 + jsdom: + specifier: 23.0.1 + version: 23.0.1 jsonwebtoken: specifier: 'catalog:' version: 9.0.2 @@ -16149,7 +16155,7 @@ snapshots: '@gar/promisify@1.1.3': optional: true - '@getzep/zep-cloud@1.0.12(@langchain/core@0.3.30(openai@4.78.1(encoding@0.1.13)(zod@3.24.1)))(encoding@0.1.13)(langchain@0.3.11(d1e86e144e3517fab3dbb7a92ab7f45a))': + '@getzep/zep-cloud@1.0.12(@langchain/core@0.3.30(openai@4.78.1(encoding@0.1.13)(zod@3.24.1)))(encoding@0.1.13)(langchain@0.3.11(fd386e1130022c8548c06dd951c5cbf0))': dependencies: form-data: 4.0.0 node-fetch: 2.7.0(encoding@0.1.13) @@ -16158,7 +16164,7 @@ snapshots: zod: 3.24.1 optionalDependencies: '@langchain/core': 0.3.30(openai@4.78.1(encoding@0.1.13)(zod@3.24.1)) - langchain: 0.3.11(d1e86e144e3517fab3dbb7a92ab7f45a) + langchain: 0.3.11(fd386e1130022c8548c06dd951c5cbf0) transitivePeerDependencies: - encoding @@ -16673,7 +16679,7 @@ snapshots: - aws-crt - encoding - '@langchain/community@0.3.24(bae0580ee8bea2ce19e4657a460c92d0)': + '@langchain/community@0.3.24(c5fc7e11d6e6167a46cb8d3fd9b490a5)': dependencies: '@browserbasehq/stagehand': 1.9.0(@playwright/test@1.49.1)(deepmerge@4.3.1)(dotenv@16.4.5)(encoding@0.1.13)(openai@4.78.1(encoding@0.1.13)(zod@3.24.1))(zod@3.24.1) '@ibm-cloud/watsonx-ai': 1.1.2 @@ -16684,7 +16690,7 @@ snapshots: flat: 5.0.2 ibm-cloud-sdk-core: 5.1.0 js-yaml: 4.1.0 - langchain: 0.3.11(d1e86e144e3517fab3dbb7a92ab7f45a) + langchain: 0.3.11(fd386e1130022c8548c06dd951c5cbf0) langsmith: 0.2.15(openai@4.78.1(encoding@0.1.13)(zod@3.24.1)) openai: 4.78.1(encoding@0.1.13)(zod@3.24.1) uuid: 10.0.0 @@ -16699,7 +16705,7 @@ snapshots: '@aws-sdk/credential-provider-node': 3.666.0(@aws-sdk/client-sso-oidc@3.666.0(@aws-sdk/client-sts@3.666.0))(@aws-sdk/client-sts@3.666.0) '@azure/storage-blob': 12.18.0(encoding@0.1.13) '@browserbasehq/sdk': 2.0.0(encoding@0.1.13) - '@getzep/zep-cloud': 1.0.12(@langchain/core@0.3.30(openai@4.78.1(encoding@0.1.13)(zod@3.24.1)))(encoding@0.1.13)(langchain@0.3.11(d1e86e144e3517fab3dbb7a92ab7f45a)) + '@getzep/zep-cloud': 1.0.12(@langchain/core@0.3.30(openai@4.78.1(encoding@0.1.13)(zod@3.24.1)))(encoding@0.1.13)(langchain@0.3.11(fd386e1130022c8548c06dd951c5cbf0)) '@getzep/zep-js': 0.9.0 '@google-ai/generativelanguage': 2.6.0(encoding@0.1.13) '@google-cloud/storage': 7.12.1(encoding@0.1.13) @@ -22891,7 +22897,7 @@ snapshots: '@types/debug': 4.1.12 '@types/node': 18.16.16 '@types/tough-cookie': 4.0.2 - axios: 1.8.2 + axios: 1.8.2(debug@4.4.0) camelcase: 6.3.0 debug: 4.4.0(supports-color@8.1.1) dotenv: 16.4.5 @@ -22901,7 +22907,7 @@ snapshots: isstream: 0.1.2 jsonwebtoken: 9.0.2 mime-types: 2.1.35 - retry-axios: 2.6.0(axios@1.8.2(debug@4.4.0)) + retry-axios: 2.6.0(axios@1.8.2) tough-cookie: 4.1.3 transitivePeerDependencies: - supports-color @@ -23895,7 +23901,7 @@ snapshots: kuler@2.0.0: {} - langchain@0.3.11(d1e86e144e3517fab3dbb7a92ab7f45a): + langchain@0.3.11(fd386e1130022c8548c06dd951c5cbf0): dependencies: '@langchain/core': 0.3.30(openai@4.78.1(encoding@0.1.13)(zod@3.24.1)) '@langchain/openai': 0.3.17(@langchain/core@0.3.30(openai@4.78.1(encoding@0.1.13)(zod@3.24.1)))(encoding@0.1.13) @@ -26277,7 +26283,7 @@ snapshots: onetime: 5.1.2 signal-exit: 3.0.7 - retry-axios@2.6.0(axios@1.8.2(debug@4.4.0)): + retry-axios@2.6.0(axios@1.8.2): dependencies: axios: 1.8.2