Merge branch 'main' into feature/OAuth2-Tools

# Conflicts:
#	packages/components/src/utils.ts
This commit is contained in:
Henry 2025-06-08 20:44:22 +01:00
commit a8ba0956fa
28 changed files with 706 additions and 120 deletions

View File

@ -20,6 +20,9 @@
"start-worker": "run-script-os", "start-worker": "run-script-os",
"start-worker:windows": "cd packages/server/bin && run worker", "start-worker:windows": "cd packages/server/bin && run worker",
"start-worker:default": "cd packages/server/bin && ./run worker", "start-worker:default": "cd packages/server/bin && ./run worker",
"user": "run-script-os",
"user:windows": "cd packages/server/bin && run user",
"user:default": "cd packages/server/bin && ./run user",
"test": "turbo run test", "test": "turbo run test",
"clean": "pnpm --filter \"./packages/**\" clean", "clean": "pnpm --filter \"./packages/**\" clean",
"nuke": "pnpm --filter \"./packages/**\" nuke && rimraf node_modules .turbo", "nuke": "pnpm --filter \"./packages/**\" nuke && rimraf node_modules .turbo",

View File

@ -1401,10 +1401,19 @@ class Agent_Agentflow implements INode {
return { response, usedTools, sourceDocuments, artifacts, totalTokens, isWaitingForHumanInput: true } return { response, usedTools, sourceDocuments, artifacts, totalTokens, isWaitingForHumanInput: true }
} }
let toolIds: ICommonObject | undefined
if (options.analyticHandlers) {
toolIds = await options.analyticHandlers.onToolStart(toolCall.name, toolCall.args, options.parentTraceIds)
}
try { try {
//@ts-ignore //@ts-ignore
let toolOutput = await selectedTool.call(toolCall.args, { signal: abortController?.signal }, undefined, flowConfig) let toolOutput = await selectedTool.call(toolCall.args, { signal: abortController?.signal }, undefined, flowConfig)
if (options.analyticHandlers && toolIds) {
await options.analyticHandlers.onToolEnd(toolIds, toolOutput)
}
// Extract source documents if present // Extract source documents if present
if (typeof toolOutput === 'string' && toolOutput.includes(SOURCE_DOCUMENTS_PREFIX)) { if (typeof toolOutput === 'string' && toolOutput.includes(SOURCE_DOCUMENTS_PREFIX)) {
const [output, docs] = toolOutput.split(SOURCE_DOCUMENTS_PREFIX) const [output, docs] = toolOutput.split(SOURCE_DOCUMENTS_PREFIX)
@ -1459,6 +1468,10 @@ class Agent_Agentflow implements INode {
toolOutput toolOutput
}) })
} catch (e) { } catch (e) {
if (options.analyticHandlers && toolIds) {
await options.analyticHandlers.onToolEnd(toolIds, e)
}
console.error('Error invoking tool:', e) console.error('Error invoking tool:', e)
usedTools.push({ usedTools.push({
tool: selectedTool.name, tool: selectedTool.name,
@ -1650,10 +1663,19 @@ class Agent_Agentflow implements INode {
toolsInstance = toolsInstance.filter((tool) => tool.name !== toolCall.name) toolsInstance = toolsInstance.filter((tool) => tool.name !== toolCall.name)
} }
if (humanInput.type === 'proceed') { if (humanInput.type === 'proceed') {
let toolIds: ICommonObject | undefined
if (options.analyticHandlers) {
toolIds = await options.analyticHandlers.onToolStart(toolCall.name, toolCall.args, options.parentTraceIds)
}
try { try {
//@ts-ignore //@ts-ignore
let toolOutput = await selectedTool.call(toolCall.args, { signal: abortController?.signal }, undefined, flowConfig) let toolOutput = await selectedTool.call(toolCall.args, { signal: abortController?.signal }, undefined, flowConfig)
if (options.analyticHandlers && toolIds) {
await options.analyticHandlers.onToolEnd(toolIds, toolOutput)
}
// Extract source documents if present // Extract source documents if present
if (typeof toolOutput === 'string' && toolOutput.includes(SOURCE_DOCUMENTS_PREFIX)) { if (typeof toolOutput === 'string' && toolOutput.includes(SOURCE_DOCUMENTS_PREFIX)) {
const [output, docs] = toolOutput.split(SOURCE_DOCUMENTS_PREFIX) const [output, docs] = toolOutput.split(SOURCE_DOCUMENTS_PREFIX)
@ -1708,6 +1730,10 @@ class Agent_Agentflow implements INode {
toolOutput toolOutput
}) })
} catch (e) { } catch (e) {
if (options.analyticHandlers && toolIds) {
await options.analyticHandlers.onToolEnd(toolIds, e)
}
console.error('Error invoking tool:', e) console.error('Error invoking tool:', e)
usedTools.push({ usedTools.push({
tool: selectedTool.name, tool: selectedTool.name,

View File

@ -1,8 +1,10 @@
import axios, { AxiosRequestConfig } from 'axios'
import { omit } from 'lodash'
import { Document } from '@langchain/core/documents' import { Document } from '@langchain/core/documents'
import { TextSplitter } from 'langchain/text_splitter' import axios, { AxiosRequestConfig } from 'axios'
import * as https from 'https'
import { BaseDocumentLoader } from 'langchain/document_loaders/base' import { BaseDocumentLoader } from 'langchain/document_loaders/base'
import { TextSplitter } from 'langchain/text_splitter'
import { omit } from 'lodash'
import { getFileFromStorage } from '../../../src'
import { ICommonObject, IDocument, INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface' import { ICommonObject, IDocument, INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface'
import { handleEscapeCharacters } from '../../../src/utils' import { handleEscapeCharacters } from '../../../src/utils'
@ -21,7 +23,7 @@ class API_DocumentLoaders implements INode {
constructor() { constructor() {
this.label = 'API Loader' this.label = 'API Loader'
this.name = 'apiLoader' this.name = 'apiLoader'
this.version = 2.0 this.version = 2.1
this.type = 'Document' this.type = 'Document'
this.icon = 'api.svg' this.icon = 'api.svg'
this.category = 'Document Loaders' this.category = 'Document Loaders'
@ -61,6 +63,15 @@ class API_DocumentLoaders implements INode {
additionalParams: true, additionalParams: true,
optional: true optional: true
}, },
{
label: 'SSL Certificate',
description: 'Please upload a SSL certificate file in either .pem or .crt',
name: 'caFile',
type: 'file',
fileType: '.pem, .crt',
additionalParams: true,
optional: true
},
{ {
label: 'Body', label: 'Body',
name: 'body', name: 'body',
@ -105,8 +116,10 @@ class API_DocumentLoaders implements INode {
} }
] ]
} }
async init(nodeData: INodeData): Promise<any> {
async init(nodeData: INodeData, _: string, options: ICommonObject): Promise<any> {
const headers = nodeData.inputs?.headers as string const headers = nodeData.inputs?.headers as string
const caFileBase64 = nodeData.inputs?.caFile as string
const url = nodeData.inputs?.url as string const url = nodeData.inputs?.url as string
const body = nodeData.inputs?.body as string const body = nodeData.inputs?.body as string
const method = nodeData.inputs?.method as string const method = nodeData.inputs?.method as string
@ -120,22 +133,37 @@ class API_DocumentLoaders implements INode {
omitMetadataKeys = _omitMetadataKeys.split(',').map((key) => key.trim()) omitMetadataKeys = _omitMetadataKeys.split(',').map((key) => key.trim())
} }
const options: ApiLoaderParams = { const apiLoaderParam: ApiLoaderParams = {
url, url,
method method
} }
if (headers) { if (headers) {
const parsedHeaders = typeof headers === 'object' ? headers : JSON.parse(headers) const parsedHeaders = typeof headers === 'object' ? headers : JSON.parse(headers)
options.headers = parsedHeaders apiLoaderParam.headers = parsedHeaders
}
if (caFileBase64.startsWith('FILE-STORAGE::')) {
let file = caFileBase64.replace('FILE-STORAGE::', '')
file = file.replace('[', '')
file = file.replace(']', '')
const orgId = options.orgId
const chatflowid = options.chatflowid
const fileData = await getFileFromStorage(file, orgId, chatflowid)
apiLoaderParam.ca = fileData.toString()
} else {
const splitDataURI = caFileBase64.split(',')
splitDataURI.pop()
const bf = Buffer.from(splitDataURI.pop() || '', 'base64')
apiLoaderParam.ca = bf.toString('utf-8')
} }
if (body) { if (body) {
const parsedBody = typeof body === 'object' ? body : JSON.parse(body) const parsedBody = typeof body === 'object' ? body : JSON.parse(body)
options.body = parsedBody apiLoaderParam.body = parsedBody
} }
const loader = new ApiLoader(options) const loader = new ApiLoader(apiLoaderParam)
let docs: IDocument[] = [] let docs: IDocument[] = []
@ -195,6 +223,7 @@ interface ApiLoaderParams {
method: string method: string
headers?: ICommonObject headers?: ICommonObject
body?: ICommonObject body?: ICommonObject
ca?: string
} }
class ApiLoader extends BaseDocumentLoader { class ApiLoader extends BaseDocumentLoader {
@ -206,28 +235,36 @@ class ApiLoader extends BaseDocumentLoader {
public readonly method: string public readonly method: string
constructor({ url, headers, body, method }: ApiLoaderParams) { public readonly ca?: string
constructor({ url, headers, body, method, ca }: ApiLoaderParams) {
super() super()
this.url = url this.url = url
this.headers = headers this.headers = headers
this.body = body this.body = body
this.method = method this.method = method
this.ca = ca
} }
public async load(): Promise<IDocument[]> { public async load(): Promise<IDocument[]> {
if (this.method === 'POST') { if (this.method === 'POST') {
return this.executePostRequest(this.url, this.headers, this.body) return this.executePostRequest(this.url, this.headers, this.body, this.ca)
} else { } else {
return this.executeGetRequest(this.url, this.headers) return this.executeGetRequest(this.url, this.headers, this.ca)
} }
} }
protected async executeGetRequest(url: string, headers?: ICommonObject): Promise<IDocument[]> { protected async executeGetRequest(url: string, headers?: ICommonObject, ca?: string): Promise<IDocument[]> {
try { try {
const config: AxiosRequestConfig = {} const config: AxiosRequestConfig = {}
if (headers) { if (headers) {
config.headers = headers config.headers = headers
} }
if (ca) {
config.httpsAgent = new https.Agent({
ca: ca
})
}
const response = await axios.get(url, config) const response = await axios.get(url, config)
const responseJsonString = JSON.stringify(response.data, null, 2) const responseJsonString = JSON.stringify(response.data, null, 2)
const doc = new Document({ const doc = new Document({
@ -242,12 +279,17 @@ class ApiLoader extends BaseDocumentLoader {
} }
} }
protected async executePostRequest(url: string, headers?: ICommonObject, body?: ICommonObject): Promise<IDocument[]> { protected async executePostRequest(url: string, headers?: ICommonObject, body?: ICommonObject, ca?: string): Promise<IDocument[]> {
try { try {
const config: AxiosRequestConfig = {} const config: AxiosRequestConfig = {}
if (headers) { if (headers) {
config.headers = headers config.headers = headers
} }
if (ca) {
config.httpsAgent = new https.Agent({
ca: ca
})
}
const response = await axios.post(url, body ?? {}, config) const response = await axios.post(url, body ?? {}, config)
const responseJsonString = JSON.stringify(response.data, null, 2) const responseJsonString = JSON.stringify(response.data, null, 2)
const doc = new Document({ const doc = new Document({

View File

@ -67,6 +67,29 @@ interface ExtractResponse {
data?: Record<string, any> data?: Record<string, any>
} }
interface SearchResult {
url: string
title: string
description: string
}
interface SearchResponse {
success: boolean
data?: SearchResult[]
warning?: string
}
interface SearchRequest {
query: string
limit?: number
tbs?: string
lang?: string
country?: string
location?: string
timeout?: number
ignoreInvalidURLs?: boolean
}
interface Params { interface Params {
[key: string]: any [key: string]: any
extractorOptions?: { extractorOptions?: {
@ -161,7 +184,11 @@ class FirecrawlApp {
} }
try { try {
const response: AxiosResponse = await this.postRequest(this.apiUrl + '/v1/scrape', validParams, headers) const parameters = {
...validParams,
integration: 'flowise'
}
const response: AxiosResponse = await this.postRequest(this.apiUrl + '/v1/scrape', parameters, headers)
if (response.status === 200) { if (response.status === 200) {
const responseData = response.data const responseData = response.data
if (responseData.success) { if (responseData.success) {
@ -259,7 +286,11 @@ class FirecrawlApp {
} }
try { try {
const response: AxiosResponse = await this.postRequest(this.apiUrl + '/v1/crawl', validParams, headers) const parameters = {
...validParams,
integration: 'flowise'
}
const response: AxiosResponse = await this.postRequest(this.apiUrl + '/v1/crawl', parameters, headers)
if (response.status === 200) { if (response.status === 200) {
const crawlResponse = response.data as CrawlResponse const crawlResponse = response.data as CrawlResponse
if (!crawlResponse.success) { if (!crawlResponse.success) {
@ -367,7 +398,11 @@ class FirecrawlApp {
} }
try { try {
const response: AxiosResponse = await this.postRequest(this.apiUrl + '/v1/extract', validParams, headers) const parameters = {
...validParams,
integration: 'flowise'
}
const response: AxiosResponse = await this.postRequest(this.apiUrl + '/v1/extract', parameters, headers)
if (response.status === 200) { if (response.status === 200) {
const extractResponse = response.data as ExtractResponse const extractResponse = response.data as ExtractResponse
if (waitUntilDone) { if (waitUntilDone) {
@ -384,18 +419,55 @@ class FirecrawlApp {
return { success: false, id: '', url: '' } return { success: false, id: '', url: '' }
} }
async search(request: SearchRequest): Promise<SearchResponse> {
const headers = this.prepareHeaders()
// Create a clean payload with only valid parameters
const validParams: any = {
query: request.query
}
// Add optional parameters if they exist and are not empty
const validSearchParams = ['limit', 'tbs', 'lang', 'country', 'location', 'timeout', 'ignoreInvalidURLs'] as const
validSearchParams.forEach((param) => {
if (request[param] !== undefined && request[param] !== null) {
validParams[param] = request[param]
}
})
try {
const parameters = {
...validParams,
integration: 'flowise'
}
const response: AxiosResponse = await this.postRequest(this.apiUrl + '/v1/search', parameters, headers)
if (response.status === 200) {
const searchResponse = response.data as SearchResponse
if (!searchResponse.success) {
throw new Error(`Search request failed: ${searchResponse.warning || 'Unknown error'}`)
}
return searchResponse
} else {
this.handleError(response, 'perform search')
}
} catch (error: any) {
throw new Error(error.message)
}
return { success: false }
}
private prepareHeaders(idempotencyKey?: string): AxiosRequestHeaders { private prepareHeaders(idempotencyKey?: string): AxiosRequestHeaders {
return { return {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
Authorization: `Bearer ${this.apiKey}`, Authorization: `Bearer ${this.apiKey}`,
'X-Origin': 'flowise',
'X-Origin-Type': 'integration',
...(idempotencyKey ? { 'x-idempotency-key': idempotencyKey } : {}) ...(idempotencyKey ? { 'x-idempotency-key': idempotencyKey } : {})
} as AxiosRequestHeaders & { 'X-Origin': string; 'X-Origin-Type': string; 'x-idempotency-key'?: string } } as AxiosRequestHeaders & { 'x-idempotency-key'?: string }
} }
private postRequest(url: string, data: Params, headers: AxiosRequestHeaders): Promise<AxiosResponse> { private async postRequest(url: string, data: Params, headers: AxiosRequestHeaders): Promise<AxiosResponse> {
return axios.post(url, data, { headers }) const result = await axios.post(url, data, { headers })
return result
} }
private getRequest(url: string, headers: AxiosRequestHeaders): Promise<AxiosResponse> { private getRequest(url: string, headers: AxiosRequestHeaders): Promise<AxiosResponse> {
@ -468,29 +540,32 @@ class FirecrawlApp {
// FireCrawl Loader // FireCrawl Loader
interface FirecrawlLoaderParameters { interface FirecrawlLoaderParameters {
url: string url?: string
query?: string
apiKey?: string apiKey?: string
apiUrl?: string apiUrl?: string
mode?: 'crawl' | 'scrape' | 'extract' mode?: 'crawl' | 'scrape' | 'extract' | 'search'
params?: Record<string, unknown> params?: Record<string, unknown>
} }
export class FireCrawlLoader extends BaseDocumentLoader { export class FireCrawlLoader extends BaseDocumentLoader {
private apiKey: string private apiKey: string
private apiUrl: string private apiUrl: string
private url: string private url?: string
private mode: 'crawl' | 'scrape' | 'extract' private query?: string
private mode: 'crawl' | 'scrape' | 'extract' | 'search'
private params?: Record<string, unknown> private params?: Record<string, unknown>
constructor(loaderParams: FirecrawlLoaderParameters) { constructor(loaderParams: FirecrawlLoaderParameters) {
super() super()
const { apiKey, apiUrl, url, mode = 'crawl', params } = loaderParams const { apiKey, apiUrl, url, query, mode = 'crawl', params } = loaderParams
if (!apiKey) { if (!apiKey) {
throw new Error('Firecrawl API key not set. You can set it as FIRECRAWL_API_KEY in your .env file, or pass it to Firecrawl.') throw new Error('Firecrawl API key not set. You can set it as FIRECRAWL_API_KEY in your .env file, or pass it to Firecrawl.')
} }
this.apiKey = apiKey this.apiKey = apiKey
this.url = url this.url = url
this.query = query
this.mode = mode this.mode = mode
this.params = params this.params = params
this.apiUrl = apiUrl || 'https://api.firecrawl.dev' this.apiUrl = apiUrl || 'https://api.firecrawl.dev'
@ -500,13 +575,37 @@ export class FireCrawlLoader extends BaseDocumentLoader {
const app = new FirecrawlApp({ apiKey: this.apiKey, apiUrl: this.apiUrl }) const app = new FirecrawlApp({ apiKey: this.apiKey, apiUrl: this.apiUrl })
let firecrawlDocs: FirecrawlDocument[] let firecrawlDocs: FirecrawlDocument[]
if (this.mode === 'scrape') { if (this.mode === 'search') {
if (!this.query) {
throw new Error('Firecrawl: Query is required for search mode')
}
const response = await app.search({ query: this.query, ...this.params })
if (!response.success) {
throw new Error(`Firecrawl: Failed to search. Warning: ${response.warning}`)
}
// Convert search results to FirecrawlDocument format
firecrawlDocs = (response.data || []).map((result) => ({
markdown: result.description,
metadata: {
title: result.title,
sourceURL: result.url,
description: result.description
}
}))
} else if (this.mode === 'scrape') {
if (!this.url) {
throw new Error('Firecrawl: URL is required for scrape mode')
}
const response = await app.scrapeUrl(this.url, this.params) const response = await app.scrapeUrl(this.url, this.params)
if (!response.success) { if (!response.success) {
throw new Error(`Firecrawl: Failed to scrape URL. Error: ${response.error}`) throw new Error(`Firecrawl: Failed to scrape URL. Error: ${response.error}`)
} }
firecrawlDocs = [response.data as FirecrawlDocument] firecrawlDocs = [response.data as FirecrawlDocument]
} else if (this.mode === 'crawl') { } else if (this.mode === 'crawl') {
if (!this.url) {
throw new Error('Firecrawl: URL is required for crawl mode')
}
const response = await app.crawlUrl(this.url, this.params) const response = await app.crawlUrl(this.url, this.params)
if ('status' in response) { if ('status' in response) {
if (response.status === 'failed') { if (response.status === 'failed') {
@ -520,6 +619,9 @@ export class FireCrawlLoader extends BaseDocumentLoader {
firecrawlDocs = [response.data as FirecrawlDocument] firecrawlDocs = [response.data as FirecrawlDocument]
} }
} else if (this.mode === 'extract') { } else if (this.mode === 'extract') {
if (!this.url) {
throw new Error('Firecrawl: URL is required for extract mode')
}
this.params!.urls = [this.url] this.params!.urls = [this.url]
const response = await app.extract(this.params as any as ExtractRequest) const response = await app.extract(this.params as any as ExtractRequest)
if (!response.success) { if (!response.success) {
@ -557,7 +659,7 @@ export class FireCrawlLoader extends BaseDocumentLoader {
} }
return [] return []
} else { } else {
throw new Error(`Unrecognized mode '${this.mode}'. Expected one of 'crawl', 'scrape', 'extract'.`) throw new Error(`Unrecognized mode '${this.mode}'. Expected one of 'crawl', 'scrape', 'extract', 'search'.`)
} }
// Convert Firecrawl documents to LangChain documents // Convert Firecrawl documents to LangChain documents
@ -602,7 +704,7 @@ class FireCrawl_DocumentLoaders implements INode {
this.name = 'fireCrawl' this.name = 'fireCrawl'
this.type = 'Document' this.type = 'Document'
this.icon = 'firecrawl.png' this.icon = 'firecrawl.png'
this.version = 3.0 this.version = 4.0
this.category = 'Document Loaders' this.category = 'Document Loaders'
this.description = 'Load data from URL using FireCrawl' this.description = 'Load data from URL using FireCrawl'
this.baseClasses = [this.type] this.baseClasses = [this.type]
@ -620,14 +722,7 @@ class FireCrawl_DocumentLoaders implements INode {
optional: true optional: true
}, },
{ {
label: 'URLs', label: 'Type',
name: 'url',
type: 'string',
description: 'URL to be crawled/scraped/extracted',
placeholder: 'https://docs.flowiseai.com'
},
{
label: 'Crawler type',
type: 'options', type: 'options',
name: 'crawlerType', name: 'crawlerType',
options: [ options: [
@ -645,89 +740,179 @@ class FireCrawl_DocumentLoaders implements INode {
label: 'Extract', label: 'Extract',
name: 'extract', name: 'extract',
description: 'Extract data from a URL' description: 'Extract data from a URL'
},
{
label: 'Search',
name: 'search',
description: 'Search the web using FireCrawl'
} }
], ],
default: 'crawl' default: 'crawl'
}, },
{
label: 'URLs',
name: 'url',
type: 'string',
description: 'URL to be crawled/scraped/extracted',
placeholder: 'https://docs.flowiseai.com',
optional: true,
show: {
crawlerType: ['crawl', 'scrape', 'extract']
}
},
{ {
// includeTags // includeTags
label: '[Scrape] Include Tags', label: 'Include Tags',
name: 'includeTags', name: 'includeTags',
type: 'string', type: 'string',
description: 'Tags to include in the output. Use comma to separate multiple tags.', description: 'Tags to include in the output. Use comma to separate multiple tags.',
optional: true, optional: true,
additionalParams: true additionalParams: true,
show: {
crawlerType: ['scrape']
}
}, },
{ {
// excludeTags // excludeTags
label: '[Scrape] Exclude Tags', label: 'Exclude Tags',
name: 'excludeTags', name: 'excludeTags',
type: 'string', type: 'string',
description: 'Tags to exclude from the output. Use comma to separate multiple tags.', description: 'Tags to exclude from the output. Use comma to separate multiple tags.',
optional: true, optional: true,
additionalParams: true additionalParams: true,
show: {
crawlerType: ['scrape']
}
}, },
{ {
// onlyMainContent // onlyMainContent
label: '[Scrape] Only Main Content', label: 'Only Main Content',
name: 'onlyMainContent', name: 'onlyMainContent',
type: 'boolean', type: 'boolean',
description: 'Extract only the main content of the page', description: 'Extract only the main content of the page',
optional: true, optional: true,
additionalParams: true additionalParams: true,
show: {
crawlerType: ['scrape']
}
}, },
{ {
// limit // limit
label: '[Crawl] Limit', label: 'Limit',
name: 'limit', name: 'limit',
type: 'string', type: 'string',
description: 'Maximum number of pages to crawl', description: 'Maximum number of pages to crawl',
optional: true, optional: true,
additionalParams: true, additionalParams: true,
default: '10000' default: '10000',
show: {
crawlerType: ['crawl']
}
}, },
{ {
label: '[Crawl] Include Paths', label: 'Include Paths',
name: 'includePaths', name: 'includePaths',
type: 'string', type: 'string',
description: description:
'URL pathname regex patterns that include matching URLs in the crawl. Only the paths that match the specified patterns will be included in the response.', 'URL pathname regex patterns that include matching URLs in the crawl. Only the paths that match the specified patterns will be included in the response.',
placeholder: `blog/.*, news/.*`, placeholder: `blog/.*, news/.*`,
optional: true, optional: true,
additionalParams: true additionalParams: true,
show: {
crawlerType: ['crawl']
}
}, },
{ {
label: '[Crawl] Exclude Paths', label: 'Exclude Paths',
name: 'excludePaths', name: 'excludePaths',
type: 'string', type: 'string',
description: 'URL pathname regex patterns that exclude matching URLs from the crawl.', description: 'URL pathname regex patterns that exclude matching URLs from the crawl.',
placeholder: `blog/.*, news/.*`, placeholder: `blog/.*, news/.*`,
optional: true, optional: true,
additionalParams: true additionalParams: true,
show: {
crawlerType: ['crawl']
}
}, },
{ {
label: '[Extract] Schema', label: 'Schema',
name: 'extractSchema', name: 'extractSchema',
type: 'json', type: 'json',
description: 'JSON schema for data extraction', description: 'JSON schema for data extraction',
optional: true, optional: true,
additionalParams: true additionalParams: true,
show: {
crawlerType: ['extract']
}
}, },
{ {
label: '[Extract] Prompt', label: 'Prompt',
name: 'extractPrompt', name: 'extractPrompt',
type: 'string', type: 'string',
description: 'Prompt for data extraction', description: 'Prompt for data extraction',
optional: true, optional: true,
additionalParams: true additionalParams: true,
show: {
crawlerType: ['extract']
}
}, },
{ {
label: '[Extract] Job ID', label: 'Query',
name: 'extractJobId', name: 'searchQuery',
type: 'string', type: 'string',
description: 'ID of the extract job', description: 'Search query to find relevant content',
optional: true, optional: true,
additionalParams: true show: {
crawlerType: ['search']
}
},
{
label: 'Limit',
name: 'searchLimit',
type: 'string',
description: 'Maximum number of results to return',
optional: true,
additionalParams: true,
default: '5',
show: {
crawlerType: ['search']
}
},
{
label: 'Language',
name: 'searchLang',
type: 'string',
description: 'Language code for search results (e.g., en, es, fr)',
optional: true,
additionalParams: true,
default: 'en',
show: {
crawlerType: ['search']
}
},
{
label: 'Country',
name: 'searchCountry',
type: 'string',
description: 'Country code for search results (e.g., us, uk, ca)',
optional: true,
additionalParams: true,
default: 'us',
show: {
crawlerType: ['search']
}
},
{
label: 'Timeout',
name: 'searchTimeout',
type: 'number',
description: 'Timeout in milliseconds for search operation',
optional: true,
additionalParams: true,
default: 60000,
show: {
crawlerType: ['search']
}
} }
] ]
this.outputs = [ this.outputs = [
@ -758,6 +943,11 @@ class FireCrawl_DocumentLoaders implements INode {
const firecrawlApiUrl = getCredentialParam('firecrawlApiUrl', credentialData, nodeData, 'https://api.firecrawl.dev') const firecrawlApiUrl = getCredentialParam('firecrawlApiUrl', credentialData, nodeData, 'https://api.firecrawl.dev')
const output = nodeData.outputs?.output as string const output = nodeData.outputs?.output as string
// Validate URL only for non-search methods
if (crawlerType !== 'search' && !url) {
throw new Error('Firecrawl: URL is required for ' + crawlerType + ' mode')
}
const includePaths = nodeData.inputs?.includePaths ? (nodeData.inputs.includePaths.split(',') as string[]) : undefined const includePaths = nodeData.inputs?.includePaths ? (nodeData.inputs.includePaths.split(',') as string[]) : undefined
const excludePaths = nodeData.inputs?.excludePaths ? (nodeData.inputs.excludePaths.split(',') as string[]) : undefined const excludePaths = nodeData.inputs?.excludePaths ? (nodeData.inputs.excludePaths.split(',') as string[]) : undefined
@ -767,9 +957,16 @@ class FireCrawl_DocumentLoaders implements INode {
const extractSchema = nodeData.inputs?.extractSchema const extractSchema = nodeData.inputs?.extractSchema
const extractPrompt = nodeData.inputs?.extractPrompt as string const extractPrompt = nodeData.inputs?.extractPrompt as string
const searchQuery = nodeData.inputs?.searchQuery as string
const searchLimit = nodeData.inputs?.searchLimit as string
const searchLang = nodeData.inputs?.searchLang as string
const searchCountry = nodeData.inputs?.searchCountry as string
const searchTimeout = nodeData.inputs?.searchTimeout as number
const input: FirecrawlLoaderParameters = { const input: FirecrawlLoaderParameters = {
url, url,
mode: crawlerType as 'crawl' | 'scrape' | 'extract', query: searchQuery,
mode: crawlerType as 'crawl' | 'scrape' | 'extract' | 'search',
apiKey: firecrawlApiToken, apiKey: firecrawlApiToken,
apiUrl: firecrawlApiUrl, apiUrl: firecrawlApiUrl,
params: { params: {
@ -785,6 +982,19 @@ class FireCrawl_DocumentLoaders implements INode {
} }
} }
// Add search-specific parameters only when in search mode
if (crawlerType === 'search') {
if (!searchQuery) {
throw new Error('Firecrawl: Search query is required for search mode')
}
input.params = {
limit: searchLimit ? parseInt(searchLimit, 10) : 5,
lang: searchLang,
country: searchCountry,
timeout: searchTimeout
}
}
if (onlyMainContent === true) { if (onlyMainContent === true) {
const scrapeOptions = input.params?.scrapeOptions as any const scrapeOptions = input.params?.scrapeOptions as any
input.params!.scrapeOptions = { input.params!.scrapeOptions = {

View File

@ -27,6 +27,16 @@ For example, you have a variable called "var1":
} }
} }
\`\`\` \`\`\`
For example, when using SSE, you can use the variable "var1" in the headers:
\`\`\`json
{
"url": "https://api.example.com/endpoint/sse",
"headers": {
"Authorization": "Bearer {{$vars.var1}}"
}
}
\`\`\`
` `
class Custom_MCP implements INode { class Custom_MCP implements INode {

View File

@ -53,10 +53,29 @@ export class MCPToolkit extends BaseToolkit {
const baseUrl = new URL(this.serverParams.url) const baseUrl = new URL(this.serverParams.url)
try { try {
transport = new StreamableHTTPClientTransport(baseUrl) if (this.serverParams.headers) {
transport = new StreamableHTTPClientTransport(baseUrl, {
requestInit: {
headers: this.serverParams.headers
}
})
} else {
transport = new StreamableHTTPClientTransport(baseUrl)
}
await client.connect(transport) await client.connect(transport)
} catch (error) { } catch (error) {
transport = new SSEClientTransport(baseUrl) if (this.serverParams.headers) {
transport = new SSEClientTransport(baseUrl, {
requestInit: {
headers: this.serverParams.headers
},
eventSourceInit: {
fetch: (url, init) => fetch(url, { ...init, headers: this.serverParams.headers })
}
})
} else {
transport = new SSEClientTransport(baseUrl)
}
await client.connect(transport) await client.connect(transport)
} }
} }

View File

@ -4,7 +4,7 @@ import { WeaviateLibArgs, WeaviateStore } from '@langchain/weaviate'
import { Document } from '@langchain/core/documents' import { Document } from '@langchain/core/documents'
import { Embeddings } from '@langchain/core/embeddings' import { Embeddings } from '@langchain/core/embeddings'
import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams, IndexingResult } from '../../../src/Interface' import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams, IndexingResult } from '../../../src/Interface'
import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils' import { getBaseClasses, getCredentialData, getCredentialParam, normalizeKeysRecursively } from '../../../src/utils'
import { addMMRInputParams, resolveVectorStoreOrRetriever } from '../VectorStoreUtils' import { addMMRInputParams, resolveVectorStoreOrRetriever } from '../VectorStoreUtils'
import { index } from '../../../src/indexing' import { index } from '../../../src/indexing'
import { VectorStore } from '@langchain/core/vectorstores' import { VectorStore } from '@langchain/core/vectorstores'
@ -175,7 +175,11 @@ class Weaviate_VectorStores implements INode {
const finalDocs = [] const finalDocs = []
for (let i = 0; i < flattenDocs.length; i += 1) { for (let i = 0; i < flattenDocs.length; i += 1) {
if (flattenDocs[i] && flattenDocs[i].pageContent) { if (flattenDocs[i] && flattenDocs[i].pageContent) {
finalDocs.push(new Document(flattenDocs[i])) const doc = { ...flattenDocs[i] }
if (doc.metadata) {
doc.metadata = normalizeKeysRecursively(doc.metadata)
}
finalDocs.push(new Document(doc))
} }
} }

View File

@ -1216,6 +1216,35 @@ export const handleDocumentLoaderDocuments = async (loader: DocumentLoader, text
return docs return docs
} }
/**
* Normalize special characters in key to be used in vector store
* @param str - Key to normalize
* @returns Normalized key
*/
export const normalizeSpecialChars = (str: string) => {
return str.replace(/[^a-zA-Z0-9_]/g, '_')
}
/**
* recursively normalize object keys
* @param data - Object to normalize
* @returns Normalized object
*/
export const normalizeKeysRecursively = (data: any): any => {
if (Array.isArray(data)) {
return data.map(normalizeKeysRecursively)
}
if (data !== null && typeof data === 'object') {
return Object.entries(data).reduce((acc, [key, value]) => {
const newKey = normalizeSpecialChars(key)
acc[newKey] = normalizeKeysRecursively(value)
return acc
}, {} as Record<string, any>)
}
return data
}
/** /**
* Check if OAuth2 token is expired and refresh if needed * Check if OAuth2 token is expired and refresh if needed
* @param {string} credentialId * @param {string} credentialId

View File

@ -1,6 +1,6 @@
import { Command, Flags } from '@oclif/core' import { Command, Flags } from '@oclif/core'
import path from 'path'
import dotenv from 'dotenv' import dotenv from 'dotenv'
import path from 'path'
import logger from '../utils/logger' import logger from '../utils/logger'
dotenv.config({ path: path.join(__dirname, '..', '..', '.env'), override: true }) dotenv.config({ path: path.join(__dirname, '..', '..', '.env'), override: true })
@ -120,7 +120,7 @@ export abstract class BaseCommand extends Command {
logger.error('unhandledRejection: ', err) logger.error('unhandledRejection: ', err)
}) })
const { flags } = await this.parse(BaseCommand) const { flags } = await this.parse(this.constructor as any)
if (flags.PORT) process.env.PORT = flags.PORT if (flags.PORT) process.env.PORT = flags.PORT
if (flags.CORS_ORIGINS) process.env.CORS_ORIGINS = flags.CORS_ORIGINS if (flags.CORS_ORIGINS) process.env.CORS_ORIGINS = flags.CORS_ORIGINS
if (flags.IFRAME_ORIGINS) process.env.IFRAME_ORIGINS = flags.IFRAME_ORIGINS if (flags.IFRAME_ORIGINS) process.env.IFRAME_ORIGINS = flags.IFRAME_ORIGINS

View File

@ -0,0 +1,80 @@
import { Args } from '@oclif/core'
import { QueryRunner } from 'typeorm'
import * as DataSource from '../DataSource'
import { User } from '../enterprise/database/entities/user.entity'
import { getHash } from '../enterprise/utils/encryption.util'
import { isInvalidPassword } from '../enterprise/utils/validation.util'
import logger from '../utils/logger'
import { BaseCommand } from './base'
export default class user extends BaseCommand {
static args = {
email: Args.string({
description: 'Email address to search for in the user database'
}),
password: Args.string({
description: 'New password for that user'
})
}
async run(): Promise<void> {
const { args } = await this.parse(user)
let queryRunner: QueryRunner | undefined
try {
logger.info('Initializing DataSource')
const dataSource = await DataSource.getDataSource()
await dataSource.initialize()
queryRunner = dataSource.createQueryRunner()
await queryRunner.connect()
if (args.email && args.password) {
logger.info('Running resetPassword')
await this.resetPassword(queryRunner, args.email, args.password)
} else {
logger.info('Running listUserEmails')
await this.listUserEmails(queryRunner)
}
} catch (error) {
logger.error(error)
} finally {
if (queryRunner && !queryRunner.isReleased) await queryRunner.release()
await this.gracefullyExit()
}
}
async listUserEmails(queryRunner: QueryRunner) {
logger.info('Listing all user emails')
const users = await queryRunner.manager.find(User, {
select: ['email']
})
const emails = users.map((user) => user.email)
logger.info(`Email addresses: ${emails.join(', ')}`)
logger.info(`Email count: ${emails.length}`)
logger.info('To reset user password, run the following command: pnpm user --email "myEmail" --password "myPassword"')
}
async resetPassword(queryRunner: QueryRunner, email: string, password: string) {
logger.info(`Finding user by email: ${email}`)
const user = await queryRunner.manager.findOne(User, {
where: { email }
})
if (!user) throw new Error(`User not found with email: ${email}`)
if (isInvalidPassword(password)) {
const errors = []
if (!/(?=.*[a-z])/.test(password)) errors.push('at least one lowercase letter')
if (!/(?=.*[A-Z])/.test(password)) errors.push('at least one uppercase letter')
if (!/(?=.*\d)/.test(password)) errors.push('at least one number')
if (!/(?=.*[^a-zA-Z0-9])/.test(password)) errors.push('at least one special character')
if (password.length < 8) errors.push('minimum length of 8 characters')
throw new Error(`Invalid password: Must contain ${errors.join(', ')}`)
}
user.credential = getHash(password)
await queryRunner.manager.save(user)
logger.info(`Password reset for user: ${email}`)
}
}

View File

@ -7,6 +7,6 @@ export class AddSeqNoToDatasetRow1733752119696 implements MigrationInterface {
} }
public async down(queryRunner: QueryRunner): Promise<void> { public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "dataset_row" DROP COLUMN "sequence_no";`) await queryRunner.query(`ALTER TABLE \`dataset_row\` DROP COLUMN \`sequence_no\``)
} }
} }

View File

@ -7,6 +7,6 @@ export class AddErrorToEvaluationRun1744964560174 implements MigrationInterface
} }
public async down(queryRunner: QueryRunner): Promise<void> { public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "evaluation_run" DROP COLUMN "errors";`) await queryRunner.query(`ALTER TABLE \`evaluation_run\` DROP COLUMN \`errors\`;`)
} }
} }

View File

@ -9,8 +9,8 @@ export class AddSSOColumns1730519457880 implements MigrationInterface {
} }
public async down(queryRunner: QueryRunner): Promise<void> { public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "organization" DROP COLUMN "sso_config";`) await queryRunner.query(`ALTER TABLE \`organization\` DROP COLUMN \`sso_config\`;`)
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "user_type";`) await queryRunner.query(`ALTER TABLE \`user\` DROP COLUMN \`user_type\`;`)
await queryRunner.query(`ALTER TABLE "login_activity" DROP COLUMN "login_mode";`) await queryRunner.query(`ALTER TABLE \`login_activity\` DROP COLUMN \`login_mode\`;`)
} }
} }

View File

@ -1503,7 +1503,14 @@ export const executeAgentFlow = async ({
try { try {
if (chatflow.analytic) { if (chatflow.analytic) {
analyticHandlers = AnalyticHandler.getInstance({ inputs: {} } as any, { // Override config analytics
let analyticInputs: ICommonObject = {}
if (overrideConfig?.analytics && Object.keys(overrideConfig.analytics).length > 0) {
analyticInputs = {
...overrideConfig.analytics
}
}
analyticHandlers = AnalyticHandler.getInstance({ inputs: { analytics: analyticInputs } } as any, {
orgId, orgId,
workspaceId, workspaceId,
appDataSource, appDataSource,

View File

@ -68,6 +68,7 @@
"rehype-raw": "^7.0.0", "rehype-raw": "^7.0.0",
"remark-gfm": "^3.0.1", "remark-gfm": "^3.0.1",
"remark-math": "^5.1.1", "remark-math": "^5.1.1",
"showdown": "^2.1.0",
"tippy.js": "^6.3.7", "tippy.js": "^6.3.7",
"uuid": "^9.0.1", "uuid": "^9.0.1",
"yup": "^0.32.9" "yup": "^0.32.9"

View File

@ -15,7 +15,7 @@ import { Dropdown } from '@/ui-component/dropdown/Dropdown'
import { useTheme } from '@mui/material/styles' import { useTheme } from '@mui/material/styles'
import assistantsApi from '@/api/assistants' import assistantsApi from '@/api/assistants'
import { baseURL } from '@/store/constant' import { baseURL } from '@/store/constant'
import { initNode } from '@/utils/genericHelper' import { initNode, showHideInputParams } from '@/utils/genericHelper'
import DocStoreInputHandler from '@/views/docstore/DocStoreInputHandler' import DocStoreInputHandler from '@/views/docstore/DocStoreInputHandler'
import useApi from '@/hooks/useApi' import useApi from '@/hooks/useApi'
@ -55,6 +55,15 @@ const AgentflowGeneratorDialog = ({ show, dialogProps, onCancel, onConfirm }) =>
const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args)) const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args))
const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args)) const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args))
const handleChatModelDataChange = ({ inputParam, newValue }) => {
setSelectedChatModel((prevData) => {
const updatedData = { ...prevData }
updatedData.inputs[inputParam.name] = newValue
updatedData.inputParams = showHideInputParams(updatedData)
return updatedData
})
}
useEffect(() => { useEffect(() => {
if (getChatModelsApi.data) { if (getChatModelsApi.data) {
setChatModelsComponents(getChatModelsApi.data) setChatModelsComponents(getChatModelsApi.data)
@ -303,10 +312,15 @@ const AgentflowGeneratorDialog = ({ show, dialogProps, onCancel, onConfirm }) =>
borderRadius: 2 borderRadius: 2
}} }}
> >
{(selectedChatModel.inputParams ?? []) {showHideInputParams(selectedChatModel)
.filter((inputParam) => !inputParam.hidden) .filter((inputParam) => !inputParam.hidden && inputParam.display !== false)
.map((inputParam, index) => ( .map((inputParam, index) => (
<DocStoreInputHandler key={index} inputParam={inputParam} data={selectedChatModel} /> <DocStoreInputHandler
key={index}
inputParam={inputParam}
data={selectedChatModel}
onNodeDataChange={handleChatModelDataChange}
/>
))} ))}
</Box> </Box>
)} )}

View File

@ -81,7 +81,11 @@ const NodeInfoDialog = ({ show, dialogProps, onCancel }) => {
height: 50, height: 50,
marginRight: 10, marginRight: 10,
borderRadius: '50%', borderRadius: '50%',
backgroundColor: 'white' backgroundColor: 'white',
flexShrink: 0,
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}} }}
> >
<img <img

View File

@ -379,7 +379,11 @@ const SpeechToText = ({ dialogProps }) => {
width: 50, width: 50,
height: 50, height: 50,
borderRadius: '50%', borderRadius: '50%',
backgroundColor: 'white' backgroundColor: 'white',
flexShrink: 0,
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}} }}
> >
<img <img

View File

@ -53,7 +53,7 @@ import { baseURL } from '@/store/constant'
import { SET_CHATFLOW, closeSnackbar as closeSnackbarAction, enqueueSnackbar as enqueueSnackbarAction } from '@/store/actions' import { SET_CHATFLOW, closeSnackbar as closeSnackbarAction, enqueueSnackbar as enqueueSnackbarAction } from '@/store/actions'
// Utils // Utils
import { initNode } from '@/utils/genericHelper' import { initNode, showHideInputParams } from '@/utils/genericHelper'
import useNotifier from '@/utils/useNotifier' import useNotifier from '@/utils/useNotifier'
import { toolAgentFlow } from './toolAgentFlow' import { toolAgentFlow } from './toolAgentFlow'
@ -127,6 +127,28 @@ const CustomAssistantConfigurePreview = () => {
const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args)) const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args))
const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args)) const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args))
const handleChatModelDataChange = ({ inputParam, newValue }) => {
setSelectedChatModel((prevData) => {
const updatedData = { ...prevData }
updatedData.inputs[inputParam.name] = newValue
updatedData.inputParams = showHideInputParams(updatedData)
return updatedData
})
}
const handleToolDataChange =
(toolIndex) =>
({ inputParam, newValue }) => {
setSelectedTools((prevTools) => {
const updatedTools = [...prevTools]
const updatedTool = { ...updatedTools[toolIndex] }
updatedTool.inputs[inputParam.name] = newValue
updatedTool.inputParams = showHideInputParams(updatedTool)
updatedTools[toolIndex] = updatedTool
return updatedTools
})
}
const displayWarning = () => { const displayWarning = () => {
enqueueSnackbar({ enqueueSnackbar({
message: 'Please fill in all mandatory fields.', message: 'Please fill in all mandatory fields.',
@ -1126,13 +1148,14 @@ const CustomAssistantConfigurePreview = () => {
borderRadius: 2 borderRadius: 2
}} }}
> >
{(selectedChatModel.inputParams ?? []) {showHideInputParams(selectedChatModel)
.filter((inputParam) => !inputParam.hidden) .filter((inputParam) => !inputParam.hidden && inputParam.display !== false)
.map((inputParam, index) => ( .map((inputParam, index) => (
<DocStoreInputHandler <DocStoreInputHandler
key={index} key={index}
inputParam={inputParam} inputParam={inputParam}
data={selectedChatModel} data={selectedChatModel}
onNodeDataChange={handleChatModelDataChange}
/> />
))} ))}
</Box> </Box>
@ -1217,13 +1240,16 @@ const CustomAssistantConfigurePreview = () => {
mb: 1 mb: 1
}} }}
> >
{(tool.inputParams ?? []) {showHideInputParams(tool)
.filter((inputParam) => !inputParam.hidden) .filter(
.map((inputParam, index) => ( (inputParam) => !inputParam.hidden && inputParam.display !== false
)
.map((inputParam, inputIndex) => (
<DocStoreInputHandler <DocStoreInputHandler
key={index} key={inputIndex}
inputParam={inputParam} inputParam={inputParam}
data={tool} data={tool}
onNodeDataChange={handleToolDataChange(index)}
/> />
))} ))}
</Box> </Box>

View File

@ -64,7 +64,9 @@ const CustomAssistantLayout = () => {
const getImages = (details) => { const getImages = (details) => {
const images = [] const images = []
if (details && details.chatModel && details.chatModel.name) { if (details && details.chatModel && details.chatModel.name) {
images.push(`${baseURL}/api/v1/node-icon/${details.chatModel.name}`) images.push({
imageSrc: `${baseURL}/api/v1/node-icon/${details.chatModel.name}`
})
} }
return images return images
} }

View File

@ -3,6 +3,7 @@ import { Handle, Position, useUpdateNodeInternals } from 'reactflow'
import { useEffect, useRef, useState, useContext } from 'react' import { useEffect, useRef, useState, useContext } from 'react'
import { useSelector, useDispatch } from 'react-redux' import { useSelector, useDispatch } from 'react-redux'
import { cloneDeep } from 'lodash' import { cloneDeep } from 'lodash'
import showdown from 'showdown'
// material-ui // material-ui
import { useTheme, styled } from '@mui/material/styles' import { useTheme, styled } from '@mui/material/styles'
@ -98,6 +99,13 @@ const StyledPopper = styled(Popper)({
} }
}) })
const markdownConverter = new showdown.Converter({
simplifiedAutoLink: true,
strikethrough: true,
tables: true,
tasklists: true
})
// ===========================|| NodeInputHandler ||=========================== // // ===========================|| NodeInputHandler ||=========================== //
const NodeInputHandler = ({ const NodeInputHandler = ({
@ -1389,7 +1397,12 @@ const NodeInputHandler = ({
onCancel={() => setPromptGeneratorDialogOpen(false)} onCancel={() => setPromptGeneratorDialogOpen(false)}
onConfirm={(generatedInstruction) => { onConfirm={(generatedInstruction) => {
try { try {
data.inputs[inputParam.name] = generatedInstruction if (inputParam?.acceptVariable && window.location.href.includes('v2/agentcanvas')) {
const htmlContent = markdownConverter.makeHtml(generatedInstruction)
data.inputs[inputParam.name] = htmlContent
} else {
data.inputs[inputParam.name] = generatedInstruction
}
setPromptGeneratorDialogOpen(false) setPromptGeneratorDialogOpen(false)
} catch (error) { } catch (error) {
enqueueSnackbar({ enqueueSnackbar({

View File

@ -140,7 +140,11 @@ const CredentialListDialog = ({ show, dialogProps, onCancel, onCredentialSelecte
width: 50, width: 50,
height: 50, height: 50,
borderRadius: '50%', borderRadius: '50%',
backgroundColor: 'white' backgroundColor: 'white',
flexShrink: 0,
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}} }}
> >
<img <img

View File

@ -153,7 +153,11 @@ const ComponentsListDialog = ({ show, dialogProps, onCancel, apiCall, onSelected
width: 50, width: 50,
height: 50, height: 50,
borderRadius: '50%', borderRadius: '50%',
backgroundColor: 'white' backgroundColor: 'white',
flexShrink: 0,
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}} }}
> >
<img <img

View File

@ -1,5 +1,5 @@
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { useState } from 'react' import { useState, useContext } from 'react'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
// material-ui // material-ui
@ -20,14 +20,17 @@ import { CodeEditor } from '@/ui-component/editor/CodeEditor'
import ExpandTextDialog from '@/ui-component/dialog/ExpandTextDialog' import ExpandTextDialog from '@/ui-component/dialog/ExpandTextDialog'
import ManageScrapedLinksDialog from '@/ui-component/dialog/ManageScrapedLinksDialog' import ManageScrapedLinksDialog from '@/ui-component/dialog/ManageScrapedLinksDialog'
import CredentialInputHandler from '@/views/canvas/CredentialInputHandler' import CredentialInputHandler from '@/views/canvas/CredentialInputHandler'
import { flowContext } from '@/store/context/ReactFlowContext'
// const // const
import { FLOWISE_CREDENTIAL_ID } from '@/store/constant' import { FLOWISE_CREDENTIAL_ID } from '@/store/constant'
// ===========================|| DocStoreInputHandler ||=========================== // // ===========================|| DocStoreInputHandler ||=========================== //
const DocStoreInputHandler = ({ inputParam, data, disabled = false }) => { const DocStoreInputHandler = ({ inputParam, data, disabled = false, onNodeDataChange }) => {
const customization = useSelector((state) => state.customization) const customization = useSelector((state) => state.customization)
const flowContextValue = useContext(flowContext)
const nodeDataChangeHandler = onNodeDataChange || flowContextValue?.onNodeDataChange
const [showExpandDialog, setShowExpandDialog] = useState(false) const [showExpandDialog, setShowExpandDialog] = useState(false)
const [expandDialogProps, setExpandDialogProps] = useState({}) const [expandDialogProps, setExpandDialogProps] = useState({})
@ -35,6 +38,14 @@ const DocStoreInputHandler = ({ inputParam, data, disabled = false }) => {
const [manageScrapedLinksDialogProps, setManageScrapedLinksDialogProps] = useState({}) const [manageScrapedLinksDialogProps, setManageScrapedLinksDialogProps] = useState({})
const [reloadTimestamp, setReloadTimestamp] = useState(Date.now().toString()) const [reloadTimestamp, setReloadTimestamp] = useState(Date.now().toString())
const handleDataChange = ({ inputParam, newValue }) => {
data.inputs[inputParam.name] = newValue
const allowedShowHideInputTypes = ['boolean', 'asyncOptions', 'asyncMultiOptions', 'options', 'multiOptions']
if (allowedShowHideInputTypes.includes(inputParam.type) && nodeDataChangeHandler) {
nodeDataChangeHandler({ nodeId: data.id, inputParam, newValue })
}
}
const onExpandDialogClicked = (value, inputParam) => { const onExpandDialogClicked = (value, inputParam) => {
const dialogProps = { const dialogProps = {
value, value,
@ -149,7 +160,7 @@ const DocStoreInputHandler = ({ inputParam, data, disabled = false }) => {
{inputParam.type === 'boolean' && ( {inputParam.type === 'boolean' && (
<SwitchInput <SwitchInput
disabled={disabled} disabled={disabled}
onChange={(newValue) => (data.inputs[inputParam.name] = newValue)} onChange={(newValue) => handleDataChange({ inputParam, newValue })}
value={data.inputs[inputParam.name] ?? inputParam.default ?? false} value={data.inputs[inputParam.name] ?? inputParam.default ?? false}
/> />
)} )}
@ -203,7 +214,7 @@ const DocStoreInputHandler = ({ inputParam, data, disabled = false }) => {
disabled={disabled} disabled={disabled}
name={inputParam.name} name={inputParam.name}
options={inputParam.options} options={inputParam.options}
onSelect={(newValue) => (data.inputs[inputParam.name] = newValue)} onSelect={(newValue) => handleDataChange({ inputParam, newValue })}
value={data.inputs[inputParam.name] ?? inputParam.default ?? 'choose an option'} value={data.inputs[inputParam.name] ?? inputParam.default ?? 'choose an option'}
/> />
)} )}
@ -213,7 +224,7 @@ const DocStoreInputHandler = ({ inputParam, data, disabled = false }) => {
disabled={disabled} disabled={disabled}
name={inputParam.name} name={inputParam.name}
options={inputParam.options} options={inputParam.options}
onSelect={(newValue) => (data.inputs[inputParam.name] = newValue)} onSelect={(newValue) => handleDataChange({ inputParam, newValue })}
value={data.inputs[inputParam.name] ?? inputParam.default ?? 'choose an option'} value={data.inputs[inputParam.name] ?? inputParam.default ?? 'choose an option'}
/> />
)} )}
@ -230,7 +241,7 @@ const DocStoreInputHandler = ({ inputParam, data, disabled = false }) => {
freeSolo={inputParam.freeSolo} freeSolo={inputParam.freeSolo}
multiple={inputParam.type === 'asyncMultiOptions'} multiple={inputParam.type === 'asyncMultiOptions'}
value={data.inputs[inputParam.name] ?? inputParam.default ?? 'choose an option'} value={data.inputs[inputParam.name] ?? inputParam.default ?? 'choose an option'}
onSelect={(newValue) => (data.inputs[inputParam.name] = newValue)} onSelect={(newValue) => handleDataChange({ inputParam, newValue })}
onCreateNew={() => addAsyncOption(inputParam.name)} onCreateNew={() => addAsyncOption(inputParam.name)}
/> />
</div> </div>
@ -296,7 +307,8 @@ const DocStoreInputHandler = ({ inputParam, data, disabled = false }) => {
DocStoreInputHandler.propTypes = { DocStoreInputHandler.propTypes = {
inputParam: PropTypes.object, inputParam: PropTypes.object,
data: PropTypes.object, data: PropTypes.object,
disabled: PropTypes.bool disabled: PropTypes.bool,
onNodeDataChange: PropTypes.func
} }
export default DocStoreInputHandler export default DocStoreInputHandler

View File

@ -153,7 +153,11 @@ const DocumentLoaderListDialog = ({ show, dialogProps, onCancel, onDocLoaderSele
width: 50, width: 50,
height: 50, height: 50,
borderRadius: '50%', borderRadius: '50%',
backgroundColor: 'white' backgroundColor: 'white',
flexShrink: 0,
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}} }}
> >
<img <img

View File

@ -35,7 +35,7 @@ import { closeSnackbar as closeSnackbarAction, enqueueSnackbar as enqueueSnackba
import { useError } from '@/store/context/ErrorContext' import { useError } from '@/store/context/ErrorContext'
// Utils // Utils
import { initNode } from '@/utils/genericHelper' import { initNode, showHideInputParams } from '@/utils/genericHelper'
import useNotifier from '@/utils/useNotifier' import useNotifier from '@/utils/useNotifier'
const CardWrapper = styled(MainCard)(({ theme }) => ({ const CardWrapper = styled(MainCard)(({ theme }) => ({
@ -98,6 +98,24 @@ const LoaderConfigPreviewChunks = () => {
const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args)) const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args))
const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args)) const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args))
const handleDocumentLoaderDataChange = ({ inputParam, newValue }) => {
setSelectedDocumentLoader((prevData) => {
const updatedData = { ...prevData }
updatedData.inputs[inputParam.name] = newValue
updatedData.inputParams = showHideInputParams(updatedData)
return updatedData
})
}
const handleTextSplitterDataChange = ({ inputParam, newValue }) => {
setSelectedTextSplitter((prevData) => {
const updatedData = { ...prevData }
updatedData.inputs[inputParam.name] = newValue
updatedData.inputParams = showHideInputParams(updatedData)
return updatedData
})
}
const onSplitterChange = (name) => { const onSplitterChange = (name) => {
const textSplitter = (textSplitterNodes ?? []).find((splitter) => splitter.name === name) const textSplitter = (textSplitterNodes ?? []).find((splitter) => splitter.name === name)
if (textSplitter) { if (textSplitter) {
@ -452,13 +470,14 @@ const LoaderConfigPreviewChunks = () => {
</Box> </Box>
{selectedDocumentLoader && {selectedDocumentLoader &&
Object.keys(selectedDocumentLoader).length > 0 && Object.keys(selectedDocumentLoader).length > 0 &&
(selectedDocumentLoader.inputParams ?? []) showHideInputParams(selectedDocumentLoader)
.filter((inputParam) => !inputParam.hidden) .filter((inputParam) => !inputParam.hidden && inputParam.display !== false)
.map((inputParam, index) => ( .map((inputParam, index) => (
<DocStoreInputHandler <DocStoreInputHandler
key={index} key={index}
inputParam={inputParam} inputParam={inputParam}
data={selectedDocumentLoader} data={selectedDocumentLoader}
onNodeDataChange={handleDocumentLoaderDataChange}
/> />
))} ))}
{textSplitterNodes && textSplitterNodes.length > 0 && ( {textSplitterNodes && textSplitterNodes.length > 0 && (
@ -511,10 +530,15 @@ const LoaderConfigPreviewChunks = () => {
</> </>
)} )}
{Object.keys(selectedTextSplitter).length > 0 && {Object.keys(selectedTextSplitter).length > 0 &&
(selectedTextSplitter.inputParams ?? []) showHideInputParams(selectedTextSplitter)
.filter((inputParam) => !inputParam.hidden) .filter((inputParam) => !inputParam.hidden && inputParam.display !== false)
.map((inputParam, index) => ( .map((inputParam, index) => (
<DocStoreInputHandler key={index} data={selectedTextSplitter} inputParam={inputParam} /> <DocStoreInputHandler
key={index}
data={selectedTextSplitter}
inputParam={inputParam}
onNodeDataChange={handleTextSplitterDataChange}
/>
))} ))}
</div> </div>
</Grid> </Grid>

View File

@ -40,7 +40,7 @@ import Storage from '@mui/icons-material/Storage'
import DynamicFeed from '@mui/icons-material/Filter1' import DynamicFeed from '@mui/icons-material/Filter1'
// utils // utils
import { initNode } from '@/utils/genericHelper' import { initNode, showHideInputParams } from '@/utils/genericHelper'
import useNotifier from '@/utils/useNotifier' import useNotifier from '@/utils/useNotifier'
// const // const
@ -89,6 +89,33 @@ const VectorStoreConfigure = () => {
const [showUpsertHistoryDetailsDialog, setShowUpsertHistoryDetailsDialog] = useState(false) const [showUpsertHistoryDetailsDialog, setShowUpsertHistoryDetailsDialog] = useState(false)
const [upsertDetailsDialogProps, setUpsertDetailsDialogProps] = useState({}) const [upsertDetailsDialogProps, setUpsertDetailsDialogProps] = useState({})
const handleEmbeddingsProviderDataChange = ({ inputParam, newValue }) => {
setSelectedEmbeddingsProvider((prevData) => {
const updatedData = { ...prevData }
updatedData.inputs[inputParam.name] = newValue
updatedData.inputParams = showHideInputParams(updatedData)
return updatedData
})
}
const handleVectorStoreProviderDataChange = ({ inputParam, newValue }) => {
setSelectedVectorStoreProvider((prevData) => {
const updatedData = { ...prevData }
updatedData.inputs[inputParam.name] = newValue
updatedData.inputParams = showHideInputParams(updatedData)
return updatedData
})
}
const handleRecordManagerProviderDataChange = ({ inputParam, newValue }) => {
setSelectedRecordManagerProvider((prevData) => {
const updatedData = { ...prevData }
updatedData.inputs[inputParam.name] = newValue
updatedData.inputParams = showHideInputParams(updatedData)
return updatedData
})
}
const onEmbeddingsSelected = (component) => { const onEmbeddingsSelected = (component) => {
const nodeData = cloneDeep(initNode(component, uuidv4())) const nodeData = cloneDeep(initNode(component, uuidv4()))
if (!showEmbeddingsListDialog && documentStore.embeddingConfig) { if (!showEmbeddingsListDialog && documentStore.embeddingConfig) {
@ -599,14 +626,17 @@ const VectorStoreConfigure = () => {
</Box> </Box>
{selectedEmbeddingsProvider && {selectedEmbeddingsProvider &&
Object.keys(selectedEmbeddingsProvider).length > 0 && Object.keys(selectedEmbeddingsProvider).length > 0 &&
(selectedEmbeddingsProvider.inputParams ?? []) showHideInputParams(selectedEmbeddingsProvider)
.filter((inputParam) => !inputParam.hidden) .filter(
(inputParam) => !inputParam.hidden && inputParam.display !== false
)
.map((inputParam, index) => ( .map((inputParam, index) => (
<DocStoreInputHandler <DocStoreInputHandler
key={index} key={index}
data={selectedEmbeddingsProvider} data={selectedEmbeddingsProvider}
inputParam={inputParam} inputParam={inputParam}
isAdditionalParams={inputParam.additionalParams} isAdditionalParams={inputParam.additionalParams}
onNodeDataChange={handleEmbeddingsProviderDataChange}
/> />
))} ))}
</div> </div>
@ -714,14 +744,17 @@ const VectorStoreConfigure = () => {
</Box> </Box>
{selectedVectorStoreProvider && {selectedVectorStoreProvider &&
Object.keys(selectedVectorStoreProvider).length > 0 && Object.keys(selectedVectorStoreProvider).length > 0 &&
(selectedVectorStoreProvider.inputParams ?? []) showHideInputParams(selectedVectorStoreProvider)
.filter((inputParam) => !inputParam.hidden) .filter(
(inputParam) => !inputParam.hidden && inputParam.display !== false
)
.map((inputParam, index) => ( .map((inputParam, index) => (
<DocStoreInputHandler <DocStoreInputHandler
key={index} key={index}
data={selectedVectorStoreProvider} data={selectedVectorStoreProvider}
inputParam={inputParam} inputParam={inputParam}
isAdditionalParams={inputParam.additionalParams} isAdditionalParams={inputParam.additionalParams}
onNodeDataChange={handleVectorStoreProviderDataChange}
/> />
))} ))}
</div> </div>
@ -837,17 +870,18 @@ const VectorStoreConfigure = () => {
</Box> </Box>
{selectedRecordManagerProvider && {selectedRecordManagerProvider &&
Object.keys(selectedRecordManagerProvider).length > 0 && Object.keys(selectedRecordManagerProvider).length > 0 &&
(selectedRecordManagerProvider.inputParams ?? []) showHideInputParams(selectedRecordManagerProvider)
.filter((inputParam) => !inputParam.hidden) .filter(
(inputParam) => !inputParam.hidden && inputParam.display !== false
)
.map((inputParam, index) => ( .map((inputParam, index) => (
<> <DocStoreInputHandler
<DocStoreInputHandler key={index}
key={index} data={selectedRecordManagerProvider}
data={selectedRecordManagerProvider} inputParam={inputParam}
inputParam={inputParam} isAdditionalParams={inputParam.additionalParams}
isAdditionalParams={inputParam.additionalParams} onNodeDataChange={handleRecordManagerProviderDataChange}
/> />
</>
))} ))}
</div> </div>
</Grid> </Grid>

View File

@ -31,7 +31,7 @@ import useApi from '@/hooks/useApi'
import { useAuth } from '@/hooks/useAuth' import { useAuth } from '@/hooks/useAuth'
import useNotifier from '@/utils/useNotifier' import useNotifier from '@/utils/useNotifier'
import { baseURL } from '@/store/constant' import { baseURL } from '@/store/constant'
import { initNode } from '@/utils/genericHelper' import { initNode, showHideInputParams } from '@/utils/genericHelper'
import { closeSnackbar as closeSnackbarAction, enqueueSnackbar as enqueueSnackbarAction } from '@/store/actions' import { closeSnackbar as closeSnackbarAction, enqueueSnackbar as enqueueSnackbarAction } from '@/store/actions'
const CardWrapper = styled(MainCard)(({ theme }) => ({ const CardWrapper = styled(MainCard)(({ theme }) => ({
@ -84,6 +84,15 @@ const VectorStoreQuery = () => {
const getVectorStoreNodeDetailsApi = useApi(nodesApi.getSpecificNode) const getVectorStoreNodeDetailsApi = useApi(nodesApi.getSpecificNode)
const [selectedVectorStoreProvider, setSelectedVectorStoreProvider] = useState({}) const [selectedVectorStoreProvider, setSelectedVectorStoreProvider] = useState({})
const handleVectorStoreProviderDataChange = ({ inputParam, newValue }) => {
setSelectedVectorStoreProvider((prevData) => {
const updatedData = { ...prevData }
updatedData.inputs[inputParam.name] = newValue
updatedData.inputParams = showHideInputParams(updatedData)
return updatedData
})
}
const chunkSelected = (chunkId, selectedChunkNumber) => { const chunkSelected = (chunkId, selectedChunkNumber) => {
const selectedChunk = documentChunks.find((chunk) => chunk.id === chunkId) const selectedChunk = documentChunks.find((chunk) => chunk.id === chunkId)
const dialogProps = { const dialogProps = {
@ -354,14 +363,15 @@ const VectorStoreQuery = () => {
</Box> </Box>
{selectedVectorStoreProvider && {selectedVectorStoreProvider &&
Object.keys(selectedVectorStoreProvider).length > 0 && Object.keys(selectedVectorStoreProvider).length > 0 &&
(selectedVectorStoreProvider.inputParams ?? []) showHideInputParams(selectedVectorStoreProvider)
.filter((inputParam) => !inputParam.hidden) .filter((inputParam) => !inputParam.hidden && inputParam.display !== false)
.map((inputParam, index) => ( .map((inputParam, index) => (
<DocStoreInputHandler <DocStoreInputHandler
key={index} key={index}
data={selectedVectorStoreProvider} data={selectedVectorStoreProvider}
inputParam={inputParam} inputParam={inputParam}
isAdditionalParams={inputParam.additionalParams} isAdditionalParams={inputParam.additionalParams}
onNodeDataChange={handleVectorStoreProviderDataChange}
/> />
))} ))}
</div> </div>