Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: HTTP request tool #9228

Open
wants to merge 58 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
3744248
:zap: setup
michael-radency Apr 26, 2024
a0b0200
Merge branch 'master' of https://github.com/n8n-io/n8n into ai-162-to…
michael-radency Apr 26, 2024
ef9a6ae
:zap: authentication support
michael-radency Apr 26, 2024
ad957fc
Merge branch 'master' of https://github.com/n8n-io/n8n into ai-162-to…
michael-radency Apr 26, 2024
742dd46
:zap: oauth2 authentication fixes, toll description prompt update
michael-radency Apr 29, 2024
4c84098
Merge branch 'master' of https://github.com/n8n-io/n8n into ai-162-to…
michael-radency Apr 29, 2024
f335ec7
:zap: clean up
michael-radency Apr 29, 2024
2f86da6
:zap: fix for tool description prompt
michael-radency Apr 29, 2024
fa73fc6
Merge branch 'master' of https://github.com/n8n-io/n8n into ai-162-to…
michael-radency Apr 29, 2024
f8ed53b
:zap: ui updates, options
michael-radency Apr 29, 2024
3d7bed7
Merge branch 'master' of https://github.com/n8n-io/n8n into ai-162-to…
michael-radency Apr 29, 2024
e086460
:zap: ui for optimize response option
michael-radency Apr 29, 2024
f9a1801
Merge branch 'master' of https://github.com/n8n-io/n8n into ai-162-to…
michael-radency Apr 29, 2024
f632455
:zap: optimize response util
michael-radency Apr 30, 2024
ea784a3
Merge branch 'master' of https://github.com/n8n-io/n8n into ai-162-to…
michael-radency Apr 30, 2024
6e6b211
:zap: optimize response update
michael-radency Apr 30, 2024
58b1e67
Merge branch 'master' of https://github.com/n8n-io/n8n into ai-162-to…
michael-radency Apr 30, 2024
8147c5b
:zap: ui updates
michael-radency Apr 30, 2024
87f2b39
Merge branch 'master' of https://github.com/n8n-io/n8n into ai-162-to…
michael-radency Apr 30, 2024
c5beba4
:zap: clean up
michael-radency Apr 30, 2024
0996986
Merge branch 'master' of https://github.com/n8n-io/n8n into ai-162-to…
michael-radency Apr 30, 2024
448bd7d
:zap: descriptions update
michael-radency Apr 30, 2024
75967f1
Merge branch 'master' of https://github.com/n8n-io/n8n into ai-162-to…
michael-radency Apr 30, 2024
6dd4524
Merge branch 'master' into ai-162-tool-to-visit-a-website
michael-radency May 16, 2024
3f61121
combined url with path, placeholder notice, fixes
michael-radency May 16, 2024
efa655a
Merge branch 'master' of https://github.com/n8n-io/n8n into ai-162-to…
michael-radency May 17, 2024
e3079f5
:zap: updating UI
michael-radency May 17, 2024
22bf42c
updating implementation, WIP
michael-radency May 18, 2024
a9e1a59
Merge branch 'master' of https://github.com/n8n-io/n8n into ai-162-to…
michael-radency May 18, 2024
c937f14
:zap: dynamic tool func update
michael-radency May 20, 2024
6d3671f
placeholders definitions parameter
michael-radency May 20, 2024
03666d3
prompts updates
michael-radency May 20, 2024
7824434
Merge branch 'master' of https://github.com/n8n-io/n8n into ai-162-to…
michael-radency May 20, 2024
fb817a1
require comma separated values from LLM instead filled request options
michael-radency May 21, 2024
fa6ac15
Merge branch 'master' of https://github.com/n8n-io/n8n into ai-162-to…
michael-radency May 21, 2024
23d5c07
Merge branch 'master' of https://github.com/n8n-io/n8n into ai-162-to…
michael-radency May 23, 2024
ea4a9d3
properties schema and DynamicStructuredTool support
michael-radency May 23, 2024
8236d49
construct shcema properties helper
michael-radency May 24, 2024
0cac68d
Merge branch 'master' of https://github.com/n8n-io/n8n into ai-162-to…
michael-radency May 24, 2024
2362c29
Merge branch 'master' of https://github.com/n8n-io/n8n into ai-162-to…
michael-radency May 24, 2024
05bce18
reverted changes related to dynamic structured tool, updated paramete…
michael-radency May 24, 2024
00a8e2a
Merge branch 'master' of https://github.com/n8n-io/n8n into ai-162-to…
michael-radency May 24, 2024
af07e08
Merge branch 'master' of https://github.com/n8n-io/n8n into ai-162-to…
michael-radency May 26, 2024
d671160
json with placheholders processing
michael-radency May 27, 2024
fbca8e9
Merge branch 'master' of https://github.com/n8n-io/n8n into ai-162-to…
michael-radency May 27, 2024
71dabba
refactoring
michael-radency May 27, 2024
7535658
refactoring
michael-radency May 27, 2024
77f1b7d
Merge branch 'master' of https://github.com/n8n-io/n8n into ai-162-to…
michael-radency May 27, 2024
eadb6de
refactoring
michael-radency May 27, 2024
1dd28d5
spelling fixes
michael-radency May 27, 2024
40dba41
Merge branch 'master' of https://github.com/n8n-io/n8n into ai-162-to…
michael-radency May 28, 2024
557f8e8
include http code if present in error
michael-radency May 28, 2024
10c0216
Merge branch 'master' of https://github.com/n8n-io/n8n into ai-162-to…
michael-radency May 29, 2024
eb93f7d
review updates
michael-radency May 29, 2024
8924ca5
tool telemetry
michael-radency May 29, 2024
3e36a22
Merge branch 'master' of https://github.com/n8n-io/n8n into ai-162-to…
michael-radency May 29, 2024
13a47bf
Merge branch 'master' of https://github.com/n8n-io/n8n into ai-162-to…
michael-radency May 29, 2024
8bcfddd
Merge branch 'master' of https://github.com/n8n-io/n8n into ai-162-to…
michael-radency May 30, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,398 @@
/* eslint-disable n8n-nodes-base/node-dirname-against-convention */
import type {
IExecuteFunctions,
INodeType,
INodeTypeDescription,
SupplyData,
ExecutionError,
IDataObject,
IHttpRequestOptions,
IHttpRequestMethods,
} from 'n8n-workflow';
import { NodeConnectionType, NodeOperationError, jsonParse } from 'n8n-workflow';

import { getConnectionHintNoticeField } from '../../../utils/sharedFields';

import { DynamicTool } from '@langchain/core/tools';

import {
type ToolParameter,
configureHttpRequestFunction,
prettifyToolName,
configureResponseOptimizer,
} from './utils';

import {
authenticationProperties,
optimizeResponseProperties,
parametersCollection,
} from './descriptions';

export class ToolHttpRequest implements INodeType {
description: INodeTypeDescription = {
displayName: 'HTTP Request Tool',
name: 'toolHttpRequest',
icon: 'file:httprequest.svg',
group: ['output'],
version: 1,
description: 'Makes an HTTP request and returns the response data',
subtitle: `={{(${prettifyToolName})($parameter.name)}}`,
defaults: {
name: 'HTTP Request',
},
credentials: [],
codex: {
categories: ['AI'],
subcategories: {
AI: ['Tools'],
},
resources: {
primaryDocumentation: [
{
url: 'https://docs.n8n.io/integrations/builtin/cluster-nodes/sub-nodes/n8n-nodes-langchain.toolhttprequest/',
},
],
},
},
// eslint-disable-next-line n8n-nodes-base/node-class-description-inputs-wrong-regular-node
inputs: [],
// eslint-disable-next-line n8n-nodes-base/node-class-description-outputs-wrong
outputs: [NodeConnectionType.AiTool],
outputNames: ['Tool'],
properties: [
getConnectionHintNoticeField([NodeConnectionType.AiAgent]),
{
displayName: 'Name',
name: 'name',
type: 'string',
default: '',
placeholder: 'e.g. get_current_weather',
validateType: 'string-alphanumeric',
description:
'The name of the function to be called, could contain letters, numbers, and underscores only',
},
{
displayName: 'Description',
name: 'toolDescription',
type: 'string',
description:
'Explain to LLM what this tool does, better description would allow LLM to produce expected result',
placeholder: 'e.g. Get the current weather in the requested city',
default: '',
typeOptions: {
rows: 3,
},
},
{
displayName: 'Method',
name: 'method',
type: 'options',
options: [
{
name: 'DELETE',
value: 'DELETE',
},
{
name: 'GET',
value: 'GET',
},
{
name: 'PATCH',
value: 'PATCH',
},
{
name: 'POST',
value: 'POST',
},
{
name: 'PUT',
value: 'PUT',
},
],
default: 'GET',
},
{
displayName: 'URL',
name: 'url',
type: 'string',
default: '',
placeholder: 'e.g. https://wikipedia.org/api',
validateType: 'url',
},
...authenticationProperties,
{
displayName: 'Define Path',
name: 'sendInPath',
type: 'boolean',
default: false,
noDataExpression: true,
description: 'Whether the LLM should provide path parameters',
},
{
displayName: 'Path',
name: 'path',
type: 'string',
default: '',
required: true,
placeholder: 'e.g. /weather/{latitude}/{longitude}',
hint: "Use {parameter_name} to indicate where the parameter's value should be inserted",
displayOptions: {
show: {
sendInPath: [true],
},
},
},
{
...parametersCollection,
name: 'pathParameters',
displayOptions: {
show: {
sendInPath: [true],
},
},
},
{
displayName: 'Define Query',
name: 'sendInQuery',
type: 'boolean',
default: false,
noDataExpression: true,
description: 'Whether the LLM should provide query parameters',
},
{
...parametersCollection,
name: 'queryParameters',
displayOptions: {
show: {
sendInQuery: [true],
},
},
},
{
displayName: 'Define Body',
name: 'sendInBody',
type: 'boolean',
default: false,
noDataExpression: true,
description: 'Whether the LLM should provide body parameters',
},
{
...parametersCollection,
name: 'bodyParameters',
displayOptions: {
show: {
sendInBody: [true],
},
},
},
...optimizeResponseProperties,
{
displayName: 'Options',
name: 'options',
placeholder: 'Add Option',
description: 'Data to send in the request in addition to data provided by LLM',
type: 'collection',
default: {},
options: [
{
displayName: 'Headers',
name: 'headers',
type: 'json',
typeOptions: {
rows: 2,
},
default: '{\n "Authorization": "Bearer my_token"\n}\n',
validateType: 'object',
},
{
displayName: 'Extra Body Parameters',
name: 'bodyParameters',
type: 'json',
typeOptions: {
rows: 2,
},
default: '{\n "id": "some id"\n}\n',
validateType: 'object',
},
{
displayName: 'Extra Query Parameters',
name: 'queryParameters',
type: 'json',
typeOptions: {
rows: 2,
},
default: '{\n "limit": 20\n}\n',
validateType: 'object',
},
],
},
],
};

async supplyData(this: IExecuteFunctions, itemIndex: number): Promise<SupplyData> {
const name = this.getNodeParameter('name', itemIndex) as string;
const toolDescription = this.getNodeParameter('toolDescription', itemIndex) as string;
const method = this.getNodeParameter('method', itemIndex, 'GET') as IHttpRequestMethods;
const url = this.getNodeParameter('url', itemIndex) as string;
const authentication = this.getNodeParameter('authentication', itemIndex, 'none') as
| 'predefinedCredentialType'
| 'genericCredentialType'
| 'none';
const options = this.getNodeParameter('options', itemIndex, {});

let headers = (options?.headers as IDataObject) ?? {};
let body = (options?.bodyParameters as IDataObject) ?? {};
let qs = (options?.queryParameters as IDataObject) ?? {};

if (typeof headers === 'string') {
headers = jsonParse<IDataObject>(headers, {
errorMessage: 'Invalid JSON in "Extra Request Options -> Headers" field',
});
}

if (typeof body === 'string') {
body = jsonParse<IDataObject>(body, {
errorMessage: 'Invalid JSON in "Extra Request Options -> Body Parameters" field',
});
}

if (typeof qs === 'string') {
qs = jsonParse<IDataObject>(qs, {
errorMessage: 'Invalid JSON in "Extra Request Options -> Query Parameters" field',
});
}

const httpRequest = await configureHttpRequestFunction(this, authentication, itemIndex);
const optimizeResponse = configureResponseOptimizer(this, itemIndex);

let path = this.getNodeParameter('path', itemIndex, '') as string;
if (path && path[0] !== '/') {
path = '/' + path;
}
const pathParameters = this.getNodeParameter(
'pathParameters.values',
itemIndex,
[],
) as ToolParameter[];
const queryParameters = this.getNodeParameter(
'queryParameters.values',
itemIndex,
[],
) as ToolParameter[];
const bodyParameters = this.getNodeParameter(
'bodyParameters.values',
itemIndex,
[],
) as ToolParameter[];

if (pathParameters.length) {
for (const parameter of pathParameters) {
if (path.indexOf(`{${parameter.name}}`) === -1) {
throw new NodeOperationError(
this.getNode(),
`'Path' does not contain parameter '${parameter.name}', remove it from 'Path Parameters' or include in 'Path' as {${parameter.name}}`,
);
}
}
}

const parameters = [...pathParameters, ...queryParameters, ...bodyParameters];

let description = toolDescription;
if (parameters.length) {
description +=
` extract from prompt ${parameters.map((parameter) => `${parameter.name}(description: ${parameter.description}, type: ${parameter.type})`).join(', ')}` +
'send as JSON';
}

return {
response: new DynamicTool({
name,
description,
func: async (query: string): Promise<string> => {
const { index } = this.addInputData(NodeConnectionType.AiTool, [[{ json: { query } }]]);

let response: string = '';
let executionError: Error | undefined = undefined;

try {
let toolParameters: IDataObject = {};
try {
toolParameters = jsonParse<IDataObject>(query);
} catch (error) {
if (parameters.length === 1) {
toolParameters = { [parameters[0].name]: query };
}
}

const requestOptions: IHttpRequestOptions = {
method,
url,
};

if (pathParameters.length) {
for (const parameter of pathParameters) {
const parameterName = parameter.name;
const parameterValue = encodeURIComponent(String(toolParameters[parameterName]));
path = path.replace(`{${parameterName}}`, parameterValue);
}
}

if (queryParameters.length) {
for (const parameter of queryParameters) {
const parameterName = parameter.name;
const parameterValue = toolParameters[parameterName];
qs[parameterName] = parameterValue;
}
}

if (bodyParameters.length) {
for (const parameter of bodyParameters) {
const parameterName = parameter.name;
const parameterValue = toolParameters[parameterName];
body[parameterName] = parameterValue;
}
}

requestOptions.url += path;

if (Object.keys(headers).length) {
requestOptions.headers = headers;
}

if (Object.keys(qs).length) {
requestOptions.qs = qs;
}

if (Object.keys(body)) {
requestOptions.body = body;
}

response = optimizeResponse(await httpRequest(requestOptions));
} catch (error) {
executionError = error;
response = `There was an error: "${error.message}"`;
}

if (typeof response !== 'string') {
executionError = new NodeOperationError(this.getNode(), 'Wrong output type returned', {
description: `The response property should be a string, but it is an ${typeof response}`,
});
response = `There was an error: "${executionError.message}"`;
}

if (executionError) {
void this.addOutputData(
NodeConnectionType.AiTool,
index,
executionError as ExecutionError,
);
} else {
void this.addOutputData(NodeConnectionType.AiTool, index, [[{ json: { response } }]]);
}
return response;
},
}),
};
}
}