-
Notifications
You must be signed in to change notification settings - Fork 7.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Add Ask AI to HTTP Request Node (#8917)
- Loading branch information
1 parent
7ff24f1
commit cd9bc44
Showing
40 changed files
with
3,943 additions
and
369 deletions.
There are no files selected for viewing
This file contains 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 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 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 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 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 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 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 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.