-
Notifications
You must be signed in to change notification settings - Fork 38.5k
feat: Add Ask AI to HTTP Request Node #8917
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
Merged
Merged
Changes from all commits
Commits
Show all changes
37 commits
Select commit
Hold shift + click to select a range
c7bf3aa
feat: add HTTP Request node Ai integration
alexgrozav 1fd6345
feat: update provider structure and add openapi-directory integration
alexgrozav 633c2ba
chore: remove console.log
alexgrozav 405fdc4
feat: cleanup and translations update
alexgrozav f83eacb
chore: merge master
alexgrozav 195a504
fix: update deps
alexgrozav 9704a63
fix: remove unnecessary gitignore
alexgrozav 72552f5
fix: update deps
alexgrozav 8efdb26
feat: update curl generation integration
alexgrozav 639ed81
chore: remove openapi scripts
alexgrozav 979501c
fix: update dependencies
alexgrozav 0488f1b
feat: update curl import and knowledgebase
alexgrozav 14df23d
fix: update search algorithm to take spaces into account
alexgrozav 579dd48
feat: adjust prompt for better results
alexgrozav c462218
test: update tests
alexgrozav 1a300bc
chore: merge master
alexgrozav 2d6e1aa
fix: remove unused gitignore
alexgrozav 293a399
test: add generateCurl test
alexgrozav 884d3b8
fix: fix linting issue
alexgrozav eaaf307
feat: various ux improvements
alexgrozav dfed1d2
chore: merge master
alexgrozav d393138
fix: update tests
alexgrozav 25fa4cb
feat: update knowledgebase
alexgrozav f23c2be
feat: add telemetry
alexgrozav a6b2c70
feat: update prompt to ensure validity
alexgrozav 555277b
fix: fix generated command rogue brackets using regex
alexgrozav 76cb717
refactor: add checkRequirements method
alexgrozav 131a215
chore: merge master
alexgrozav 82ed5ce
fix: update telemetry
alexgrozav 6a24efb
chore: merge master
alexgrozav 1889090
fix: remove duplicate telemetry events
alexgrozav 86c0248
fix: extract docs url and update incorrect curl command error message
alexgrozav 25187d7
feat: further improvements
alexgrozav 0d1cded
fix: update css to use css variable
alexgrozav 3b8cdf3
feat: add ai resources README
alexgrozav 6afec0c
chore: merge master
alexgrozav a5155fc
chore: merge master
alexgrozav File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,40 +1,212 @@ | ||
import { Service } from 'typedi'; | ||
import config from '@/config'; | ||
import type { INodeType, N8nAIProviderType, NodeError } from 'n8n-workflow'; | ||
import { createDebugErrorPrompt } from '@/services/ai/prompts/debugError'; | ||
import { ApplicationError, jsonParse } from 'n8n-workflow'; | ||
import { debugErrorPromptTemplate } from '@/services/ai/prompts/debugError'; | ||
import type { BaseMessageLike } from '@langchain/core/messages'; | ||
import { AIProviderOpenAI } from '@/services/ai/providers/openai'; | ||
import { AIProviderUnknown } from '@/services/ai/providers/unknown'; | ||
import type { BaseChatModelCallOptions } from '@langchain/core/language_models/chat_models'; | ||
import { summarizeNodeTypeProperties } from '@/services/ai/utils/summarizeNodeTypeProperties'; | ||
import { Pinecone } from '@pinecone-database/pinecone'; | ||
import type { z } from 'zod'; | ||
import apiKnowledgebase from '@/services/ai/resources/api-knowledgebase.json'; | ||
import { JsonOutputFunctionsParser } from 'langchain/output_parsers'; | ||
import { | ||
generateCurlCommandFallbackPromptTemplate, | ||
generateCurlCommandPromptTemplate, | ||
} from '@/services/ai/prompts/generateCurl'; | ||
import { generateCurlSchema } from '@/services/ai/schemas/generateCurl'; | ||
import { PineconeStore } from '@langchain/pinecone'; | ||
import Fuse from 'fuse.js'; | ||
import { N8N_DOCS_URL } from '@/constants'; | ||
|
||
interface APIKnowledgebaseService { | ||
id: string; | ||
title: string; | ||
description?: string; | ||
} | ||
|
||
function isN8nAIProviderType(value: string): value is N8nAIProviderType { | ||
return ['openai'].includes(value); | ||
} | ||
|
||
@Service() | ||
export class AIService { | ||
private provider: N8nAIProviderType = 'unknown'; | ||
private providerType: N8nAIProviderType = 'unknown'; | ||
|
||
public provider: AIProviderOpenAI; | ||
|
||
public model: AIProviderOpenAI | AIProviderUnknown = new AIProviderUnknown(); | ||
public pinecone: Pinecone; | ||
|
||
private jsonOutputParser = new JsonOutputFunctionsParser(); | ||
|
||
constructor() { | ||
const providerName = config.getEnv('ai.provider'); | ||
|
||
if (isN8nAIProviderType(providerName)) { | ||
this.provider = providerName; | ||
this.providerType = providerName; | ||
} | ||
|
||
if (this.provider === 'openai') { | ||
const apiKey = config.getEnv('ai.openAIApiKey'); | ||
if (apiKey) { | ||
this.model = new AIProviderOpenAI({ apiKey }); | ||
if (this.providerType === 'openai') { | ||
const openAIApiKey = config.getEnv('ai.openAI.apiKey'); | ||
const openAIModelName = config.getEnv('ai.openAI.model'); | ||
|
||
if (openAIApiKey) { | ||
this.provider = new AIProviderOpenAI({ openAIApiKey, modelName: openAIModelName }); | ||
} | ||
} | ||
|
||
const pineconeApiKey = config.getEnv('ai.pinecone.apiKey'); | ||
if (pineconeApiKey) { | ||
this.pinecone = new Pinecone({ | ||
apiKey: pineconeApiKey, | ||
}); | ||
} | ||
} | ||
|
||
async prompt(messages: BaseMessageLike[]) { | ||
return await this.model.prompt(messages); | ||
async prompt(messages: BaseMessageLike[], options?: BaseChatModelCallOptions) { | ||
if (!this.provider) { | ||
throw new ApplicationError('No AI provider has been configured.'); | ||
} | ||
|
||
return await this.provider.invoke(messages, options); | ||
} | ||
|
||
async debugError(error: NodeError, nodeType?: INodeType) { | ||
return await this.prompt(createDebugErrorPrompt(error, nodeType)); | ||
this.checkRequirements(); | ||
|
||
const chain = debugErrorPromptTemplate.pipe(this.provider.model); | ||
const result = await chain.invoke({ | ||
nodeType: nodeType?.description.displayName ?? 'n8n Node', | ||
error: JSON.stringify(error), | ||
properties: JSON.stringify( | ||
summarizeNodeTypeProperties(nodeType?.description.properties ?? []), | ||
), | ||
documentationUrl: nodeType?.description.documentationUrl ?? N8N_DOCS_URL, | ||
}); | ||
|
||
return this.provider.mapResponse(result); | ||
} | ||
|
||
validateCurl(result: { curl: string }) { | ||
if (!result.curl.startsWith('curl')) { | ||
throw new ApplicationError( | ||
'The generated HTTP Request Node parameters format is incorrect. Please adjust your request and try again.', | ||
); | ||
} | ||
|
||
result.curl = result.curl | ||
/* | ||
* Replaces placeholders like `{VALUE}` or `{{VALUE}}` with quoted placeholders `"{VALUE}"` or `"{{VALUE}}"`, | ||
* ensuring that the placeholders are properly formatted within the curl command. | ||
* - ": a colon followed by a double quote and a space | ||
* - ( starts a capturing group | ||
* - \{\{ two opening curly braces | ||
* - [A-Za-z0-9_]+ one or more alphanumeric characters or underscores | ||
* - }} two closing curly braces | ||
* - | OR | ||
* - \{ an opening curly brace | ||
* - [A-Za-z0-9_]+ one or more alphanumeric characters or underscores | ||
* - } a closing curly brace | ||
* - ) ends the capturing group | ||
* - /g performs a global search and replace | ||
* | ||
*/ | ||
.replace(/": (\{\{[A-Za-z0-9_]+}}|\{[A-Za-z0-9_]+})/g, '": "$1"') // Fix for placeholders `curl -d '{ "key": {VALUE} }'` | ||
/* | ||
* Removes the rogue curly bracket at the end of the curl command if it is present. | ||
* It ensures that the curl command is properly formatted and doesn't have an extra closing curly bracket. | ||
* - ( starts a capturing group | ||
* - -d flag in the curl command | ||
* - ' a single quote | ||
* - [^']+ one or more characters that are not a single quote | ||
* - ' a single quote | ||
* - ) ends the capturing group | ||
* - } a closing curly bracket | ||
*/ | ||
.replace(/(-d '[^']+')}/, '$1'); // Fix for rogue curly bracket `curl -d '{ "key": "value" }'}` | ||
|
||
return result; | ||
} | ||
|
||
async generateCurl(serviceName: string, serviceRequest: string) { | ||
this.checkRequirements(); | ||
|
||
if (!this.pinecone) { | ||
return await this.generateCurlGeneric(serviceName, serviceRequest); | ||
} | ||
|
||
const fuse = new Fuse(apiKnowledgebase as unknown as APIKnowledgebaseService[], { | ||
threshold: 0.25, | ||
useExtendedSearch: true, | ||
keys: ['id', 'title'], | ||
}); | ||
|
||
const matchedServices = fuse | ||
.search(serviceName.replace(/ +/g, '|')) | ||
.map((result) => result.item); | ||
|
||
if (matchedServices.length === 0) { | ||
return await this.generateCurlGeneric(serviceName, serviceRequest); | ||
} | ||
|
||
const pcIndex = this.pinecone.Index('api-knowledgebase'); | ||
const vectorStore = await PineconeStore.fromExistingIndex(this.provider.embeddings, { | ||
namespace: 'endpoints', | ||
pineconeIndex: pcIndex, | ||
}); | ||
|
||
const matchedDocuments = await vectorStore.similaritySearch( | ||
`${serviceName} ${serviceRequest}`, | ||
4, | ||
{ | ||
id: { | ||
$in: matchedServices.map((service) => service.id), | ||
}, | ||
}, | ||
); | ||
|
||
if (matchedDocuments.length === 0) { | ||
return await this.generateCurlGeneric(serviceName, serviceRequest); | ||
} | ||
|
||
const aggregatedDocuments = matchedDocuments.reduce<unknown[]>((acc, document) => { | ||
const pageData = jsonParse(document.pageContent); | ||
|
||
acc.push(pageData); | ||
|
||
return acc; | ||
}, []); | ||
|
||
const generateCurlChain = generateCurlCommandPromptTemplate | ||
.pipe(this.provider.modelWithOutputParser(generateCurlSchema)) | ||
.pipe(this.jsonOutputParser); | ||
const result = (await generateCurlChain.invoke({ | ||
endpoints: JSON.stringify(aggregatedDocuments), | ||
serviceName, | ||
serviceRequest, | ||
})) as z.infer<typeof generateCurlSchema>; | ||
|
||
return this.validateCurl(result); | ||
} | ||
|
||
async generateCurlGeneric(serviceName: string, serviceRequest: string) { | ||
this.checkRequirements(); | ||
|
||
const generateCurlFallbackChain = generateCurlCommandFallbackPromptTemplate | ||
.pipe(this.provider.modelWithOutputParser(generateCurlSchema)) | ||
.pipe(this.jsonOutputParser); | ||
const result = (await generateCurlFallbackChain.invoke({ | ||
serviceName, | ||
serviceRequest, | ||
})) as z.infer<typeof generateCurlSchema>; | ||
|
||
return this.validateCurl(result); | ||
} | ||
|
||
checkRequirements() { | ||
if (!this.provider) { | ||
throw new ApplicationError('No AI provider has been configured.'); | ||
} | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.