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 all 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,391 @@
/* eslint-disable n8n-nodes-base/node-dirname-against-convention */
import type {
IExecuteFunctions,
INodeType,
INodeTypeDescription,
SupplyData,
IHttpRequestMethods,
IHttpRequestOptions,
} from 'n8n-workflow';
import { NodeConnectionType, NodeOperationError } from 'n8n-workflow';

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

import {
configureHttpRequestFunction,
configureResponseOptimizer,
extractParametersFromText,
prepareToolDescription,
configureToolFunction,
updateParametersAndOptions,
} from './utils';

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

import type { PlaceholderDefinition, ToolParameter } from './interfaces';

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

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: '={{ $parameter.toolDescription }}',
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: '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:
'Tip: You can use a {placeholder} for any part of the request to be filled by the model. Provide more context about them in the placeholders section',
name: 'placeholderNotice',
type: 'notice',
default: '',
},
{
displayName: 'URL',
name: 'url',
type: 'string',
default: '',
required: true,
placeholder: 'e.g. http://www.example.com/{path}',
},
...authenticationProperties,
//----------------------------------------------------------------
{
displayName: 'Send Query Parameters',
name: 'sendQuery',
type: 'boolean',
default: false,
noDataExpression: true,
description: 'Whether the request has query params or not',
},
{
...specifyBySelector,
displayName: 'Specify Query Parameters',
name: 'specifyQuery',
displayOptions: {
show: {
sendQuery: [true],
},
},
},
{
...parametersCollection,
displayName: 'Query Parameters',
name: 'parametersQuery',
displayOptions: {
show: {
sendQuery: [true],
specifyQuery: ['keypair'],
},
},
},
{
...jsonInput,
name: 'jsonQuery',
displayOptions: {
show: {
sendQuery: [true],
specifyQuery: ['json'],
},
},
},
//----------------------------------------------------------------
{
displayName: 'Send Headers',
name: 'sendHeaders',
type: 'boolean',
default: false,
noDataExpression: true,
description: 'Whether the request has headers or not',
},
{
...specifyBySelector,
displayName: 'Specify Headers',
name: 'specifyHeaders',
displayOptions: {
show: {
sendHeaders: [true],
},
},
},
{
...parametersCollection,
displayName: 'Header Parameters',
name: 'parametersHeaders',
displayOptions: {
show: {
sendHeaders: [true],
specifyHeaders: ['keypair'],
},
},
},
{
...jsonInput,
name: 'jsonHeaders',
displayOptions: {
show: {
sendHeaders: [true],
specifyHeaders: ['json'],
},
},
},
//----------------------------------------------------------------
{
displayName: 'Send Body',
name: 'sendBody',
type: 'boolean',
default: false,
noDataExpression: true,
description: 'Whether the request has body or not',
},
{
...specifyBySelector,
displayName: 'Specify Body',
name: 'specifyBody',
displayOptions: {
show: {
sendBody: [true],
},
},
},
{
...parametersCollection,
displayName: 'Body Parameters',
name: 'parametersBody',
displayOptions: {
show: {
sendBody: [true],
specifyBody: ['keypair'],
},
},
},
{
...jsonInput,
name: 'jsonBody',
displayOptions: {
show: {
sendBody: [true],
specifyBody: ['json'],
},
},
},
//----------------------------------------------------------------
placeholderDefinitionsCollection,
...optimizeResponseProperties,
],
};

async supplyData(this: IExecuteFunctions, itemIndex: number): Promise<SupplyData> {
const name = this.getNode().name.replace(/ /g, '_');
const toolDescription = this.getNodeParameter('toolDescription', itemIndex) as string;
const sendQuery = this.getNodeParameter('sendQuery', itemIndex, false) as boolean;
const sendHeaders = this.getNodeParameter('sendHeaders', itemIndex, false) as boolean;
const sendBody = this.getNodeParameter('sendBody', itemIndex, false) as boolean;

const requestOptions: IHttpRequestOptions = {
method: this.getNodeParameter('method', itemIndex, 'GET') as IHttpRequestMethods,
url: this.getNodeParameter('url', itemIndex) as string,
qs: {},
headers: {},
body: {},
};

const authentication = this.getNodeParameter('authentication', itemIndex, 'none') as
| 'predefinedCredentialType'
| 'genericCredentialType'
| 'none';

if (authentication !== 'none') {
const domain = new URL(requestOptions.url).hostname;
if (domain.includes('{') && domain.includes('}')) {
throw new NodeOperationError(
this.getNode(),
"Can't use a placeholder for the domain when using authentication",
{
itemIndex,
description:
'This is for security reasons, to prevent the model accidentally sending your credentials to an unauthorized domain',
},
);
}
}

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

const rawRequestOptions: { [key: string]: string } = {
qs: '',
headers: '',
body: '',
};

const placeholdersDefinitions = (
this.getNodeParameter(
'placeholderDefinitions.values',
itemIndex,
[],
) as PlaceholderDefinition[]
).map((p) => {
if (p.name.startsWith('{') && p.name.endsWith('}')) {
p.name = p.name.slice(1, -1);
}
return p;
});

const toolParameters: ToolParameter[] = [];

toolParameters.push(
...extractParametersFromText(placeholdersDefinitions, requestOptions.url, 'path'),
);

if (sendQuery) {
updateParametersAndOptions(
this,
itemIndex,
toolParameters,
placeholdersDefinitions,
requestOptions,
rawRequestOptions,
'qs',
'specifyQuery',
'jsonQuery',
'parametersQuery.values',
);
}

if (sendHeaders) {
updateParametersAndOptions(
this,
itemIndex,
toolParameters,
placeholdersDefinitions,
requestOptions,
rawRequestOptions,
'headers',
'specifyHeaders',
'jsonHeaders',
'parametersHeaders.values',
);
}

if (sendBody) {
updateParametersAndOptions(
this,
itemIndex,
toolParameters,
placeholdersDefinitions,
requestOptions,
rawRequestOptions,
'body',
'specifyBody',
'jsonBody',
'parametersBody.values',
);
}

for (const placeholder of placeholdersDefinitions) {
if (!toolParameters.find((parameter) => parameter.name === placeholder.name)) {
throw new NodeOperationError(
this.getNode(),
`Misconfigured placeholder '${placeholder.name}'`,
{
itemIndex,
description:
"This placeholder is defined in the 'Placeholder Definitions' but isn't used anywhere. Either remove the definition, or add the placeholder to a part of the request.",
},
);
}
}

const func = configureToolFunction(
this,
itemIndex,
toolParameters,
requestOptions,
rawRequestOptions,
httpRequest,
optimizeResponse,
);

const description = prepareToolDescription(toolDescription, toolParameters);

const tool = new DynamicTool({ name, description, func });

return {
response: tool,
};
}
}