diff --git a/LICENSE.md b/LICENSE.md index 4a27187d8..0f4afcd11 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,7 +1,23 @@ - Apache License + Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ +Flowise is governed by the Apache License 2.0, with additional terms and conditions outlined below: + +Flowise can be used for commercial purposes for "backend-as-a-service" for your applications or as a development platform for enterprises. However, under specific conditions, you must reach out to the project's administrators to secure a commercial license: + +a. Multi-tenant SaaS service: Unless you have explicit written authorization from Flowise, you may not utilize the Flowise source code to operate a multi-tenant SaaS service that closely resembles the Flowise cloud-based services. +b. Logo and copyright information: While using Flowise in commercial application, you are prohibited from removing or altering the LOGO or copyright information displayed in the Flowise console and UI. + +For inquiries regarding licensing matters, please contact hello@flowiseai.com via email. + +Contributors are required to consent to the following terms related to their contributed code: + +a. The project maintainers have the authority to modify the open-source agreement to be more stringent or lenient. +b. Contributed code can be used for commercial purposes, including Flowise's cloud-based services. + +All other rights and restrictions are in accordance with the Apache License 2.0. + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. diff --git a/packages/components/credentials/ElectricsearchUserPassword.credential.ts b/packages/components/credentials/ElectricsearchUserPassword.credential.ts index 6c47f7b18..c1ac82c1c 100644 --- a/packages/components/credentials/ElectricsearchUserPassword.credential.ts +++ b/packages/components/credentials/ElectricsearchUserPassword.credential.ts @@ -11,8 +11,8 @@ class ElasticSearchUserPassword implements INodeCredential { this.label = 'ElasticSearch User Password' this.name = 'elasticSearchUserPassword' this.version = 1.0 - this.description = - 'Refer to official guide on how to get User Password from ElasticSearch' + this.description = `Use Cloud ID field to enter your Elastic Cloud ID or the URL of the Elastic server instance. + Refer to official guide on how to get User Password from ElasticSearch.` this.inputs = [ { label: 'Cloud ID', diff --git a/packages/components/nodes/agents/OpenAIAssistant/OpenAIAssistant.ts b/packages/components/nodes/agents/OpenAIAssistant/OpenAIAssistant.ts index 56e1b290e..b119599dc 100644 --- a/packages/components/nodes/agents/OpenAIAssistant/OpenAIAssistant.ts +++ b/packages/components/nodes/agents/OpenAIAssistant/OpenAIAssistant.ts @@ -111,7 +111,7 @@ class OpenAIAssistant_Agents implements INode { const openai = new OpenAI({ apiKey: openAIApiKey }) options.logger.info(`Clearing OpenAI Thread ${sessionId}`) - await openai.beta.threads.del(sessionId) + if (sessionId) await openai.beta.threads.del(sessionId) options.logger.info(`Successfully cleared OpenAI Thread ${sessionId}`) } @@ -135,16 +135,25 @@ class OpenAIAssistant_Agents implements INode { const openai = new OpenAI({ apiKey: openAIApiKey }) - // Retrieve assistant try { const assistantDetails = JSON.parse(assistant.details) const openAIAssistantId = assistantDetails.id + + // Retrieve assistant const retrievedAssistant = await openai.beta.assistants.retrieve(openAIAssistantId) if (formattedTools.length) { - let filteredTools = uniqWith([...retrievedAssistant.tools, ...formattedTools], isEqual) + let filteredTools = [] + for (const tool of retrievedAssistant.tools) { + if (tool.type === 'code_interpreter' || tool.type === 'retrieval') filteredTools.push(tool) + } + filteredTools = uniqWith([...filteredTools, ...formattedTools], isEqual) + // filter out tool with empty function filteredTools = filteredTools.filter((tool) => !(tool.type === 'function' && !(tool as any).function)) await openai.beta.assistants.update(openAIAssistantId, { tools: filteredTools }) + } else { + let filteredTools = retrievedAssistant.tools.filter((tool) => tool.type !== 'function') + await openai.beta.assistants.update(openAIAssistantId, { tools: filteredTools }) } const chatmessage = await appDataSource.getRepository(databaseEntities['ChatMessage']).findOneBy({ @@ -152,14 +161,45 @@ class OpenAIAssistant_Agents implements INode { }) let threadId = '' + let isNewThread = false if (!chatmessage) { const thread = await openai.beta.threads.create({}) threadId = thread.id + isNewThread = true } else { const thread = await openai.beta.threads.retrieve(chatmessage.sessionId) threadId = thread.id } + // List all runs + if (!isNewThread) { + const promise = (threadId: string) => { + return new Promise((resolve) => { + const timeout = setInterval(async () => { + const allRuns = await openai.beta.threads.runs.list(threadId) + if (allRuns.data && allRuns.data.length) { + const firstRunId = allRuns.data[0].id + const runStatus = allRuns.data.find((run) => run.id === firstRunId)?.status + if ( + runStatus && + (runStatus === 'cancelled' || + runStatus === 'completed' || + runStatus === 'expired' || + runStatus === 'failed') + ) { + clearInterval(timeout) + resolve() + } + } else { + clearInterval(timeout) + resolve() + } + }, 500) + }) + } + await promise(threadId) + } + // Add message to thread await openai.beta.threads.messages.create(threadId, { role: 'user', @@ -217,27 +257,41 @@ class OpenAIAssistant_Agents implements INode { }) resolve(state) } else { - reject( - new Error( - `Error processing thread: ${state}, Thread ID: ${threadId}, Run ID: ${runId}. submit_tool_outputs.tool_calls are empty` - ) - ) + await openai.beta.threads.runs.cancel(threadId, runId) + resolve('requires_action_retry') } } } else if (state === 'cancelled' || state === 'expired' || state === 'failed') { clearInterval(timeout) - reject(new Error(`Error processing thread: ${state}, Thread ID: ${threadId}, Run ID: ${runId}`)) + reject( + new Error(`Error processing thread: ${state}, Thread ID: ${threadId}, Run ID: ${runId}, Status: ${state}`) + ) } }, 500) }) } // Polling run status + let runThreadId = runThread.id let state = await promise(threadId, runThread.id) while (state === 'requires_action') { state = await promise(threadId, runThread.id) } + let retries = 3 + while (state === 'requires_action_retry') { + if (retries > 0) { + retries -= 1 + const newRunThread = await openai.beta.threads.runs.create(threadId, { + assistant_id: retrievedAssistant.id + }) + runThreadId = newRunThread.id + state = await promise(threadId, newRunThread.id) + } else { + throw new Error(`Error processing thread: ${state}, Thread ID: ${threadId}`) + } + } + // List messages const messages = await openai.beta.threads.messages.list(threadId) const messageData = messages.data ?? [] @@ -245,12 +299,58 @@ class OpenAIAssistant_Agents implements INode { if (!assistantMessages.length) return '' let returnVal = '' + const fileAnnotations = [] for (let i = 0; i < assistantMessages[0].content.length; i += 1) { if (assistantMessages[0].content[i].type === 'text') { const content = assistantMessages[0].content[i] as MessageContentText - returnVal += content.text.value - //TODO: handle annotations + if (content.text.annotations) { + const message_content = content.text + const annotations = message_content.annotations + + const dirPath = path.join(getUserHome(), '.flowise', 'openai-assistant') + + // Iterate over the annotations and add footnotes + for (let index = 0; index < annotations.length; index++) { + const annotation = annotations[index] + let filePath = '' + + // Gather citations based on annotation attributes + const file_citation = (annotation as OpenAI.Beta.Threads.Messages.MessageContentText.Text.FileCitation) + .file_citation + if (file_citation) { + const cited_file = await openai.files.retrieve(file_citation.file_id) + // eslint-disable-next-line no-useless-escape + const fileName = cited_file.filename.split(/[\/\\]/).pop() ?? cited_file.filename + filePath = path.join(getUserHome(), '.flowise', 'openai-assistant', fileName) + await downloadFile(cited_file, filePath, dirPath, openAIApiKey) + fileAnnotations.push({ + filePath, + fileName + }) + } else { + const file_path = (annotation as OpenAI.Beta.Threads.Messages.MessageContentText.Text.FilePath).file_path + if (file_path) { + const cited_file = await openai.files.retrieve(file_path.file_id) + // eslint-disable-next-line no-useless-escape + const fileName = cited_file.filename.split(/[\/\\]/).pop() ?? cited_file.filename + filePath = path.join(getUserHome(), '.flowise', 'openai-assistant', fileName) + await downloadFile(cited_file, filePath, dirPath, openAIApiKey) + fileAnnotations.push({ + filePath, + fileName + }) + } + } + + // Replace the text with a footnote + message_content.value = message_content.value.replace(`${annotation.text}`, `${filePath}`) + } + + returnVal += message_content.value + } else { + returnVal += content.text.value + } } else { const content = assistantMessages[0].content[i] as MessageContentImageFile const fileId = content.image_file.file_id @@ -258,7 +358,7 @@ class OpenAIAssistant_Agents implements INode { const dirPath = path.join(getUserHome(), '.flowise', 'openai-assistant') const filePath = path.join(getUserHome(), '.flowise', 'openai-assistant', `${fileObj.filename}.png`) - await downloadFile(fileObj, filePath, dirPath, openAIApiKey) + await downloadImg(openai, fileId, filePath, dirPath) const bitmap = fsDefault.readFileSync(filePath) const base64String = Buffer.from(bitmap).toString('base64') @@ -271,7 +371,8 @@ class OpenAIAssistant_Agents implements INode { return { text: returnVal, usedTools, - assistant: { assistantId: openAIAssistantId, threadId, runId: runThread.id, messages: messageData } + fileAnnotations, + assistant: { assistantId: openAIAssistantId, threadId, runId: runThreadId, messages: messageData } } } catch (error) { throw new Error(error) @@ -279,6 +380,22 @@ class OpenAIAssistant_Agents implements INode { } } +const downloadImg = async (openai: OpenAI, fileId: string, filePath: string, dirPath: string) => { + const response = await openai.files.content(fileId) + + // Extract the binary data from the Response object + const image_data = await response.arrayBuffer() + + // Convert the binary data to a Buffer + const image_data_buffer = Buffer.from(image_data) + + // Save the image to a specific location + if (!fsDefault.existsSync(dirPath)) { + fsDefault.mkdirSync(path.dirname(filePath), { recursive: true }) + } + fsDefault.writeFileSync(filePath, image_data_buffer) +} + const downloadFile = async (fileObj: any, filePath: string, dirPath: string, openAIApiKey: string) => { try { const response = await fetch(`https://api.openai.com/v1/files/${fileObj.id}/content`, { diff --git a/packages/components/nodes/chains/VectaraChain/VectaraChain.ts b/packages/components/nodes/chains/VectaraChain/VectaraChain.ts new file mode 100644 index 000000000..3799d062f --- /dev/null +++ b/packages/components/nodes/chains/VectaraChain/VectaraChain.ts @@ -0,0 +1,339 @@ +import { INode, INodeData, INodeParams } from '../../../src/Interface' +import { getBaseClasses } from '../../../src/utils' +import { VectorDBQAChain } from 'langchain/chains' +import { Document } from 'langchain/document' +import { VectaraStore } from 'langchain/vectorstores/vectara' +import fetch from 'node-fetch' + +// functionality based on https://github.com/vectara/vectara-answer +const reorderCitations = (unorderedSummary: string) => { + const allCitations = unorderedSummary.match(/\[\d+\]/g) || [] + + const uniqueCitations = [...new Set(allCitations)] + const citationToReplacement: { [key: string]: string } = {} + uniqueCitations.forEach((citation, index) => { + citationToReplacement[citation] = `[${index + 1}]` + }) + + return unorderedSummary.replace(/\[\d+\]/g, (match) => citationToReplacement[match]) +} +const applyCitationOrder = (searchResults: any[], unorderedSummary: string) => { + const orderedSearchResults: any[] = [] + const allCitations = unorderedSummary.match(/\[\d+\]/g) || [] + + const addedIndices = new Set() + for (let i = 0; i < allCitations.length; i++) { + const citation = allCitations[i] + const index = Number(citation.slice(1, citation.length - 1)) - 1 + + if (addedIndices.has(index)) continue + orderedSearchResults.push(searchResults[index]) + addedIndices.add(index) + } + + return orderedSearchResults +} + +class VectaraChain_Chains implements INode { + label: string + name: string + version: number + type: string + icon: string + category: string + baseClasses: string[] + description: string + inputs: INodeParams[] + + constructor() { + this.label = 'Vectara QA Chain' + this.name = 'vectaraQAChain' + this.version = 1.0 + this.type = 'VectaraQAChain' + this.icon = 'vectara.png' + this.category = 'Chains' + this.description = 'QA chain for Vectara' + this.baseClasses = [this.type, ...getBaseClasses(VectorDBQAChain)] + this.inputs = [ + { + label: 'Vectara Store', + name: 'vectaraStore', + type: 'VectorStore' + }, + { + label: 'Summarizer Prompt Name', + name: 'summarizerPromptName', + description: + 'Summarize the results fetched from Vectara. Read more', + type: 'options', + options: [ + { + label: 'vectara-summary-ext-v1.2.0 (gpt-3.5-turbo)', + name: 'vectara-summary-ext-v1.2.0' + }, + { + label: 'vectara-experimental-summary-ext-2023-10-23-small (gpt-3.5-turbo)', + name: 'vectara-experimental-summary-ext-2023-10-23-small', + description: 'In beta, available to both Growth and Scale Vectara users' + }, + { + label: 'vectara-summary-ext-v1.3.0 (gpt-4.0)', + name: 'vectara-summary-ext-v1.3.0', + description: 'Only available to paying Scale Vectara users' + }, + { + label: 'vectara-experimental-summary-ext-2023-10-23-med (gpt-4.0)', + name: 'vectara-experimental-summary-ext-2023-10-23-med', + description: 'In beta, only available to paying Scale Vectara users' + } + ], + default: 'vectara-summary-ext-v1.2.0' + }, + { + label: 'Response Language', + name: 'responseLang', + description: + 'Return the response in specific language. If not selected, Vectara will automatically detects the language. Read more', + type: 'options', + options: [ + { + label: 'English', + name: 'eng' + }, + { + label: 'German', + name: 'deu' + }, + { + label: 'French', + name: 'fra' + }, + { + label: 'Chinese', + name: 'zho' + }, + { + label: 'Korean', + name: 'kor' + }, + { + label: 'Arabic', + name: 'ara' + }, + { + label: 'Russian', + name: 'rus' + }, + { + label: 'Thai', + name: 'tha' + }, + { + label: 'Dutch', + name: 'nld' + }, + { + label: 'Italian', + name: 'ita' + }, + { + label: 'Portuguese', + name: 'por' + }, + { + label: 'Spanish', + name: 'spa' + }, + { + label: 'Japanese', + name: 'jpn' + }, + { + label: 'Polish', + name: 'pol' + }, + { + label: 'Turkish', + name: 'tur' + }, + { + label: 'Vietnamese', + name: 'vie' + }, + { + label: 'Indonesian', + name: 'ind' + }, + { + label: 'Czech', + name: 'ces' + }, + { + label: 'Ukrainian', + name: 'ukr' + }, + { + label: 'Greek', + name: 'ell' + }, + { + label: 'Hebrew', + name: 'heb' + }, + { + label: 'Farsi/Persian', + name: 'fas' + }, + { + label: 'Hindi', + name: 'hin' + }, + { + label: 'Urdu', + name: 'urd' + }, + { + label: 'Swedish', + name: 'swe' + }, + { + label: 'Bengali', + name: 'ben' + }, + { + label: 'Malay', + name: 'msa' + }, + { + label: 'Romanian', + name: 'ron' + } + ], + optional: true, + default: 'eng' + }, + { + label: 'Max Summarized Results', + name: 'maxSummarizedResults', + description: 'Maximum results used to build the summarized response', + type: 'number', + default: 7 + } + ] + } + + async init(): Promise { + return null + } + + async run(nodeData: INodeData, input: string): Promise { + const vectorStore = nodeData.inputs?.vectaraStore as VectaraStore + const responseLang = (nodeData.inputs?.responseLang as string) ?? 'auto' + const summarizerPromptName = nodeData.inputs?.summarizerPromptName as string + const maxSummarizedResultsStr = nodeData.inputs?.maxSummarizedResults as string + const maxSummarizedResults = maxSummarizedResultsStr ? parseInt(maxSummarizedResultsStr, 10) : 7 + + const topK = (vectorStore as any)?.k ?? 10 + + const headers = await vectorStore.getJsonHeader() + const vectaraFilter = (vectorStore as any).vectaraFilter ?? {} + const corpusId: number[] = (vectorStore as any).corpusId ?? [] + const customerId = (vectorStore as any).customerId ?? '' + + const corpusKeys = corpusId.map((corpusId) => ({ + customerId, + corpusId, + metadataFilter: vectaraFilter?.filter ?? '', + lexicalInterpolationConfig: { lambda: vectaraFilter?.lambda ?? 0.025 } + })) + + const data = { + query: [ + { + query: input, + start: 0, + numResults: topK, + contextConfig: { + sentencesAfter: vectaraFilter?.contextConfig?.sentencesAfter ?? 2, + sentencesBefore: vectaraFilter?.contextConfig?.sentencesBefore ?? 2 + }, + corpusKey: corpusKeys, + summary: [ + { + summarizerPromptName, + responseLang, + maxSummarizedResults + } + ] + } + ] + } + + try { + const response = await fetch(`https://api.vectara.io/v1/query`, { + method: 'POST', + headers: headers?.headers, + body: JSON.stringify(data) + }) + + if (response.status !== 200) { + throw new Error(`Vectara API returned status code ${response.status}`) + } + + const result = await response.json() + const responses = result.responseSet[0].response + const documents = result.responseSet[0].document + let rawSummarizedText = '' + + for (let i = 0; i < responses.length; i += 1) { + const responseMetadata = responses[i].metadata + const documentMetadata = documents[responses[i].documentIndex].metadata + const combinedMetadata: Record = {} + + responseMetadata.forEach((item: { name: string; value: unknown }) => { + combinedMetadata[item.name] = item.value + }) + + documentMetadata.forEach((item: { name: string; value: unknown }) => { + combinedMetadata[item.name] = item.value + }) + + responses[i].metadata = combinedMetadata + } + + const summaryStatus = result.responseSet[0].summary[0].status + if (summaryStatus.length > 0 && summaryStatus[0].code === 'BAD_REQUEST') { + throw new Error( + `BAD REQUEST: Too much text for the summarizer to summarize. Please try reducing the number of search results to summarize, or the context of each result by adjusting the 'summary_num_sentences', and 'summary_num_results' parameters respectively.` + ) + } + + if ( + summaryStatus.length > 0 && + summaryStatus[0].code === 'NOT_FOUND' && + summaryStatus[0].statusDetail === 'Failed to retrieve summarizer.' + ) { + throw new Error(`BAD REQUEST: summarizer ${summarizerPromptName} is invalid for this account.`) + } + + rawSummarizedText = result.responseSet[0].summary[0]?.text + + let summarizedText = reorderCitations(rawSummarizedText) + let summaryResponses = applyCitationOrder(responses, rawSummarizedText) + + const sourceDocuments: Document[] = summaryResponses.map( + (response: { text: string; metadata: Record; score: number }) => + new Document({ + pageContent: response.text, + metadata: response.metadata + }) + ) + + return { text: summarizedText, sourceDocuments: sourceDocuments } + } catch (error) { + throw new Error(error) + } + } +} + +module.exports = { nodeClass: VectaraChain_Chains } diff --git a/packages/components/nodes/chains/VectaraChain/vectara.png b/packages/components/nodes/chains/VectaraChain/vectara.png new file mode 100644 index 000000000..a13a34e6b Binary files /dev/null and b/packages/components/nodes/chains/VectaraChain/vectara.png differ diff --git a/packages/components/nodes/chatmodels/ChatAnthropic/ChatAnthropic.ts b/packages/components/nodes/chatmodels/ChatAnthropic/ChatAnthropic.ts index f16968b67..358a15d1e 100644 --- a/packages/components/nodes/chatmodels/ChatAnthropic/ChatAnthropic.ts +++ b/packages/components/nodes/chatmodels/ChatAnthropic/ChatAnthropic.ts @@ -19,7 +19,7 @@ class ChatAnthropic_ChatModels implements INode { constructor() { this.label = 'ChatAnthropic' this.name = 'chatAnthropic' - this.version = 2.0 + this.version = 3.0 this.type = 'ChatAnthropic' this.icon = 'chatAnthropic.png' this.category = 'Chat Models' @@ -48,6 +48,11 @@ class ChatAnthropic_ChatModels implements INode { name: 'claude-2', description: 'Claude 2 latest major version, automatically get updates to the model as they are released' }, + { + label: 'claude-2.1', + name: 'claude-2.1', + description: 'Claude 2 latest full version' + }, { label: 'claude-instant-1', name: 'claude-instant-1', diff --git a/packages/components/nodes/vectorstores/Elasticsearch/ElasticSearchBase.ts b/packages/components/nodes/vectorstores/Elasticsearch/ElasticSearchBase.ts index 59294b7ea..a1233c215 100644 --- a/packages/components/nodes/vectorstores/Elasticsearch/ElasticSearchBase.ts +++ b/packages/components/nodes/vectorstores/Elasticsearch/ElasticSearchBase.ts @@ -144,13 +144,26 @@ export abstract class ElasticSearchBase { } else if (cloudId) { let username = getCredentialParam('username', credentialData, nodeData) let password = getCredentialParam('password', credentialData, nodeData) - elasticSearchClientOptions = { - cloud: { - id: cloudId - }, - auth: { - username: username, - password: password + if (cloudId.startsWith('http')) { + elasticSearchClientOptions = { + node: cloudId, + auth: { + username: username, + password: password + }, + tls: { + rejectUnauthorized: false + } + } + } else { + elasticSearchClientOptions = { + cloud: { + id: cloudId + }, + auth: { + username: username, + password: password + } } } } diff --git a/packages/components/package.json b/packages/components/package.json index 01aefc2c3..dc19b591e 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -39,8 +39,6 @@ "cheerio": "^1.0.0-rc.12", "chromadb": "^1.5.11", "cohere-ai": "^6.2.0", - "crypto-js": "^4.1.1", - "css-what": "^6.1.0", "d3-dsv": "2", "dotenv": "^16.0.0", "express": "^4.17.3", @@ -55,7 +53,6 @@ "langsmith": "^0.0.32", "linkifyjs": "^4.1.1", "llmonitor": "^0.5.5", - "lodash": "^4.17.21", "mammoth": "^1.5.1", "moment": "^2.29.3", "mongodb": "^6.2.0", @@ -73,27 +70,23 @@ "pyodide": ">=0.21.0-alpha.2", "redis": "^4.6.7", "replicate": "^0.12.3", - "socket.io": "^4.6.1", "srt-parser-2": "^1.2.3", - "typeorm": "^0.3.6", "vm2": "^3.9.19", "weaviate-ts-client": "^1.1.0", - "winston": "^3.9.0", "ws": "^8.9.0", - "zod": "^3.22.4", "zod-to-json-schema": "^3.21.4" }, "devDependencies": { - "@types/crypto-js": "^4.1.1", "@types/gulp": "4.0.9", - "@types/lodash": "^4.14.202", "@types/node-fetch": "2.6.2", "@types/object-hash": "^3.0.2", "@types/pg": "^8.10.2", "@types/ws": "^8.5.3", + "eslint-plugin-markdown": "^3.0.1", + "eslint-plugin-react": "^7.33.2", + "eslint-plugin-react-hooks": "^4.6.0", "babel-register": "^6.26.0", "gulp": "^4.0.2", - "ts-node": "^10.7.0", "typescript": "^4.8.4" } } diff --git a/packages/server/marketplaces/chatflows/Claude LLM.json b/packages/server/marketplaces/chatflows/Claude LLM.json index b79898154..0ead3dd82 100644 --- a/packages/server/marketplaces/chatflows/Claude LLM.json +++ b/packages/server/marketplaces/chatflows/Claude LLM.json @@ -1,5 +1,5 @@ { - "description": "Use Anthropic Claude with 100k context window to ingest whole document for QnA", + "description": "Use Anthropic Claude with 200k context window to ingest whole document for QnA", "nodes": [ { "width": 300, @@ -148,7 +148,7 @@ "id": "chatAnthropic_0", "label": "ChatAnthropic", "name": "chatAnthropic", - "version": 2, + "version": 3, "type": "ChatAnthropic", "baseClasses": ["ChatAnthropic", "BaseChatModel", "BaseLanguageModel"], "category": "Chat Models", @@ -171,6 +171,11 @@ "name": "claude-2", "description": "Claude 2 latest major version, automatically get updates to the model as they are released" }, + { + "label": "claude-2.1", + "name": "claude-2.1", + "description": "Claude 2 latest full version" + }, { "label": "claude-instant-1", "name": "claude-instant-1", @@ -268,7 +273,7 @@ } ], "inputs": { - "modelName": "claude-2", + "modelName": "claude-2.1", "temperature": 0.9, "maxTokensToSample": "", "topP": "", diff --git a/packages/server/src/Interface.ts b/packages/server/src/Interface.ts index 8d0965f48..1d2724642 100644 --- a/packages/server/src/Interface.ts +++ b/packages/server/src/Interface.ts @@ -30,6 +30,7 @@ export interface IChatMessage { chatflowid: string sourceDocuments?: string usedTools?: string + fileAnnotations?: string chatType: string chatId: string memoryType?: string diff --git a/packages/server/src/database/entities/ChatFlow.ts b/packages/server/src/database/entities/ChatFlow.ts index 376a100b4..b3131c2ea 100644 --- a/packages/server/src/database/entities/ChatFlow.ts +++ b/packages/server/src/database/entities/ChatFlow.ts @@ -36,4 +36,7 @@ export class ChatFlow implements IChatFlow { @UpdateDateColumn() updatedDate: Date + + @Column({ nullable: true, type: 'text' }) + category?: string } diff --git a/packages/server/src/database/entities/ChatMessage.ts b/packages/server/src/database/entities/ChatMessage.ts index b51aa4340..4054a26dd 100644 --- a/packages/server/src/database/entities/ChatMessage.ts +++ b/packages/server/src/database/entities/ChatMessage.ts @@ -23,6 +23,9 @@ export class ChatMessage implements IChatMessage { @Column({ nullable: true, type: 'text' }) usedTools?: string + @Column({ nullable: true, type: 'text' }) + fileAnnotations?: string + @Column() chatType: string diff --git a/packages/server/src/database/migrations/mysql/1699900910291-AddCategoryToChatFlow.ts b/packages/server/src/database/migrations/mysql/1699900910291-AddCategoryToChatFlow.ts new file mode 100644 index 000000000..424f3b0e0 --- /dev/null +++ b/packages/server/src/database/migrations/mysql/1699900910291-AddCategoryToChatFlow.ts @@ -0,0 +1,12 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' + +export class AddCategoryToChatFlow1699900910291 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + const columnExists = await queryRunner.hasColumn('chat_flow', 'category') + if (!columnExists) queryRunner.query(`ALTER TABLE \`chat_flow\` ADD COLUMN \`category\` TEXT;`) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE \`chat_flow\` DROP COLUMN \`category\`;`) + } +} diff --git a/packages/server/src/database/migrations/mysql/1700271021237-AddFileAnnotationsToChatMessage.ts b/packages/server/src/database/migrations/mysql/1700271021237-AddFileAnnotationsToChatMessage.ts new file mode 100644 index 000000000..a352cde8c --- /dev/null +++ b/packages/server/src/database/migrations/mysql/1700271021237-AddFileAnnotationsToChatMessage.ts @@ -0,0 +1,12 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' + +export class AddFileAnnotationsToChatMessage1700271021237 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + const columnExists = await queryRunner.hasColumn('chat_message', 'fileAnnotations') + if (!columnExists) queryRunner.query(`ALTER TABLE \`chat_message\` ADD COLUMN \`fileAnnotations\` TEXT;`) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE \`chat_message\` DROP COLUMN \`fileAnnotations\`;`) + } +} diff --git a/packages/server/src/database/migrations/mysql/index.ts b/packages/server/src/database/migrations/mysql/index.ts index 4b7b8a954..8f9824a86 100644 --- a/packages/server/src/database/migrations/mysql/index.ts +++ b/packages/server/src/database/migrations/mysql/index.ts @@ -8,6 +8,8 @@ import { AddAnalytic1694432361423 } from './1694432361423-AddAnalytic' import { AddChatHistory1694658767766 } from './1694658767766-AddChatHistory' import { AddAssistantEntity1699325775451 } from './1699325775451-AddAssistantEntity' import { AddUsedToolsToChatMessage1699481607341 } from './1699481607341-AddUsedToolsToChatMessage' +import { AddCategoryToChatFlow1699900910291 } from './1699900910291-AddCategoryToChatFlow' +import { AddFileAnnotationsToChatMessage1700271021237 } from './1700271021237-AddFileAnnotationsToChatMessage' export const mysqlMigrations = [ Init1693840429259, @@ -19,5 +21,7 @@ export const mysqlMigrations = [ AddAnalytic1694432361423, AddChatHistory1694658767766, AddAssistantEntity1699325775451, - AddUsedToolsToChatMessage1699481607341 + AddUsedToolsToChatMessage1699481607341, + AddCategoryToChatFlow1699900910291, + AddFileAnnotationsToChatMessage1700271021237 ] diff --git a/packages/server/src/database/migrations/postgres/1699481607341-AddUsedToolsToChatMessage.ts b/packages/server/src/database/migrations/postgres/1699481607341-AddUsedToolsToChatMessage.ts index f9f893f82..ae34c8132 100644 --- a/packages/server/src/database/migrations/postgres/1699481607341-AddUsedToolsToChatMessage.ts +++ b/packages/server/src/database/migrations/postgres/1699481607341-AddUsedToolsToChatMessage.ts @@ -6,6 +6,6 @@ export class AddUsedToolsToChatMessage1699481607341 implements MigrationInterfac } public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "chat_flow" DROP COLUMN "usedTools";`) + await queryRunner.query(`ALTER TABLE "chat_message" DROP COLUMN "usedTools";`) } } diff --git a/packages/server/src/database/migrations/postgres/1699900910291-AddCategoryToChatFlow.ts b/packages/server/src/database/migrations/postgres/1699900910291-AddCategoryToChatFlow.ts new file mode 100644 index 000000000..f5d964391 --- /dev/null +++ b/packages/server/src/database/migrations/postgres/1699900910291-AddCategoryToChatFlow.ts @@ -0,0 +1,11 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' + +export class AddCategoryToChatFlow1699900910291 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "chat_flow" ADD COLUMN IF NOT EXISTS "category" TEXT;`) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "chat_flow" DROP COLUMN "category";`) + } +} diff --git a/packages/server/src/database/migrations/postgres/1700271021237-AddFileAnnotationsToChatMessage.ts b/packages/server/src/database/migrations/postgres/1700271021237-AddFileAnnotationsToChatMessage.ts new file mode 100644 index 000000000..8824f57d5 --- /dev/null +++ b/packages/server/src/database/migrations/postgres/1700271021237-AddFileAnnotationsToChatMessage.ts @@ -0,0 +1,11 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' + +export class AddFileAnnotationsToChatMessage1700271021237 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "chat_message" ADD COLUMN IF NOT EXISTS "fileAnnotations" TEXT;`) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "chat_message" DROP COLUMN "fileAnnotations";`) + } +} diff --git a/packages/server/src/database/migrations/postgres/index.ts b/packages/server/src/database/migrations/postgres/index.ts index 75562c0b5..d196fbc16 100644 --- a/packages/server/src/database/migrations/postgres/index.ts +++ b/packages/server/src/database/migrations/postgres/index.ts @@ -8,6 +8,8 @@ import { AddAnalytic1694432361423 } from './1694432361423-AddAnalytic' import { AddChatHistory1694658756136 } from './1694658756136-AddChatHistory' import { AddAssistantEntity1699325775451 } from './1699325775451-AddAssistantEntity' import { AddUsedToolsToChatMessage1699481607341 } from './1699481607341-AddUsedToolsToChatMessage' +import { AddCategoryToChatFlow1699900910291 } from './1699900910291-AddCategoryToChatFlow' +import { AddFileAnnotationsToChatMessage1700271021237 } from './1700271021237-AddFileAnnotationsToChatMessage' export const postgresMigrations = [ Init1693891895163, @@ -19,5 +21,7 @@ export const postgresMigrations = [ AddAnalytic1694432361423, AddChatHistory1694658756136, AddAssistantEntity1699325775451, - AddUsedToolsToChatMessage1699481607341 + AddUsedToolsToChatMessage1699481607341, + AddCategoryToChatFlow1699900910291, + AddFileAnnotationsToChatMessage1700271021237 ] diff --git a/packages/server/src/database/migrations/sqlite/1699900910291-AddCategoryToChatFlow.ts b/packages/server/src/database/migrations/sqlite/1699900910291-AddCategoryToChatFlow.ts new file mode 100644 index 000000000..270b29988 --- /dev/null +++ b/packages/server/src/database/migrations/sqlite/1699900910291-AddCategoryToChatFlow.ts @@ -0,0 +1,11 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' + +export class AddCategoryToChatFlow1699900910291 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "chat_flow" ADD COLUMN "category" TEXT;`) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "chat_flow" DROP COLUMN "category";`) + } +} diff --git a/packages/server/src/database/migrations/sqlite/1700271021237-AddFileAnnotationsToChatMessage.ts b/packages/server/src/database/migrations/sqlite/1700271021237-AddFileAnnotationsToChatMessage.ts new file mode 100644 index 000000000..af29fba4b --- /dev/null +++ b/packages/server/src/database/migrations/sqlite/1700271021237-AddFileAnnotationsToChatMessage.ts @@ -0,0 +1,20 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' + +export class AddFileAnnotationsToChatMessage1700271021237 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "temp_chat_message" ("id" varchar PRIMARY KEY NOT NULL, "role" varchar NOT NULL, "chatflowid" varchar NOT NULL, "content" text NOT NULL, "sourceDocuments" text, "usedTools" text, "fileAnnotations" text, "createdDate" datetime NOT NULL DEFAULT (datetime('now')), "chatType" VARCHAR NOT NULL DEFAULT 'INTERNAL', "chatId" VARCHAR NOT NULL, "memoryType" VARCHAR, "sessionId" VARCHAR);` + ) + await queryRunner.query( + `INSERT INTO "temp_chat_message" ("id", "role", "chatflowid", "content", "sourceDocuments", "usedTools", "createdDate", "chatType", "chatId", "memoryType", "sessionId") SELECT "id", "role", "chatflowid", "content", "sourceDocuments", "usedTools", "createdDate", "chatType", "chatId", "memoryType", "sessionId" FROM "chat_message";` + ) + await queryRunner.query(`DROP TABLE "chat_message";`) + await queryRunner.query(`ALTER TABLE "temp_chat_message" RENAME TO "chat_message";`) + await queryRunner.query(`CREATE INDEX "IDX_e574527322272fd838f4f0f3d3" ON "chat_message" ("chatflowid") ;`) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TABLE IF EXISTS "temp_chat_message";`) + await queryRunner.query(`ALTER TABLE "chat_message" DROP COLUMN "fileAnnotations";`) + } +} diff --git a/packages/server/src/database/migrations/sqlite/index.ts b/packages/server/src/database/migrations/sqlite/index.ts index 4a14fc407..fdd83064a 100644 --- a/packages/server/src/database/migrations/sqlite/index.ts +++ b/packages/server/src/database/migrations/sqlite/index.ts @@ -8,6 +8,8 @@ import { AddAnalytic1694432361423 } from './1694432361423-AddAnalytic' import { AddChatHistory1694657778173 } from './1694657778173-AddChatHistory' import { AddAssistantEntity1699325775451 } from './1699325775451-AddAssistantEntity' import { AddUsedToolsToChatMessage1699481607341 } from './1699481607341-AddUsedToolsToChatMessage' +import { AddCategoryToChatFlow1699900910291 } from './1699900910291-AddCategoryToChatFlow' +import { AddFileAnnotationsToChatMessage1700271021237 } from './1700271021237-AddFileAnnotationsToChatMessage' export const sqliteMigrations = [ Init1693835579790, @@ -19,5 +21,7 @@ export const sqliteMigrations = [ AddAnalytic1694432361423, AddChatHistory1694657778173, AddAssistantEntity1699325775451, - AddUsedToolsToChatMessage1699481607341 + AddUsedToolsToChatMessage1699481607341, + AddCategoryToChatFlow1699900910291, + AddFileAnnotationsToChatMessage1700271021237 ] diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index ba6c3ce0e..2f7d31e25 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -138,6 +138,7 @@ export class App { '/api/v1/node-icon/', '/api/v1/components-credentials-icon/', '/api/v1/chatflows-streaming', + '/api/v1/openai-assistants-file', '/api/v1/ip' ] this.app.use((req, res, next) => { @@ -355,8 +356,12 @@ export class App { this.AppDataSource.getRepository(ChatFlow).merge(chatflow, updateChatFlow) const result = await this.AppDataSource.getRepository(ChatFlow).save(chatflow) - // Update chatflowpool inSync to false, to build Langchain again because data has been changed - this.chatflowPool.updateInSync(chatflow.id, false) + // chatFlowPool is initialized only when a flow is opened + // if the user attempts to rename/update category without opening any flow, chatFlowPool will be undefined + if (this.chatflowPool) { + // Update chatflowpool inSync to false, to build Langchain again because data has been changed + this.chatflowPool.updateInSync(chatflow.id, false) + } return res.json(result) }) @@ -782,8 +787,8 @@ export class App { await openai.beta.assistants.update(assistantDetails.id, { name: assistantDetails.name, - description: assistantDetails.description, - instructions: assistantDetails.instructions, + description: assistantDetails.description ?? '', + instructions: assistantDetails.instructions ?? '', model: assistantDetails.model, tools: filteredTools, file_ids: uniqWith( @@ -952,7 +957,7 @@ export class App { const results = await this.AppDataSource.getRepository(Assistant).delete({ id: req.params.id }) - await openai.beta.assistants.del(assistantDetails.id) + if (req.query.isDeleteBoth) await openai.beta.assistants.del(assistantDetails.id) return res.json(results) } catch (error: any) { @@ -961,6 +966,14 @@ export class App { } }) + // Download file from assistant + this.app.post('/api/v1/openai-assistants-file', async (req: Request, res: Response) => { + const filePath = path.join(getUserHome(), '.flowise', 'openai-assistant', req.body.fileName) + res.setHeader('Content-Disposition', 'attachment; filename=' + path.basename(filePath)) + const fileStream = fs.createReadStream(filePath) + fileStream.pipe(res) + }) + // ---------------------------------------- // Configuration // ---------------------------------------- @@ -1135,28 +1148,52 @@ export class App { // API Keys // ---------------------------------------- + const addChatflowsCount = async (keys: any, res: Response) => { + if (keys) { + const updatedKeys: any[] = [] + //iterate through keys and get chatflows + for (const key of keys) { + const chatflows = await this.AppDataSource.getRepository(ChatFlow) + .createQueryBuilder('cf') + .where('cf.apikeyid = :apikeyid', { apikeyid: key.id }) + .getMany() + const linkedChatFlows: any[] = [] + chatflows.map((cf) => { + linkedChatFlows.push({ + flowName: cf.name, + category: cf.category, + updatedDate: cf.updatedDate + }) + }) + key.chatFlows = linkedChatFlows + updatedKeys.push(key) + } + return res.json(updatedKeys) + } + return res.json(keys) + } // Get api keys this.app.get('/api/v1/apikey', async (req: Request, res: Response) => { const keys = await getAPIKeys() - return res.json(keys) + return addChatflowsCount(keys, res) }) // Add new api key this.app.post('/api/v1/apikey', async (req: Request, res: Response) => { const keys = await addAPIKey(req.body.keyName) - return res.json(keys) + return addChatflowsCount(keys, res) }) // Update api key this.app.put('/api/v1/apikey/:id', async (req: Request, res: Response) => { const keys = await updateAPIKey(req.params.id, req.body.keyName) - return res.json(keys) + return addChatflowsCount(keys, res) }) // Delete new api key this.app.delete('/api/v1/apikey/:id', async (req: Request, res: Response) => { const keys = await deleteAPIKey(req.params.id) - return res.json(keys) + return addChatflowsCount(keys, res) }) // Verify api key @@ -1499,6 +1536,7 @@ export class App { } if (result?.sourceDocuments) apiMessage.sourceDocuments = JSON.stringify(result.sourceDocuments) if (result?.usedTools) apiMessage.usedTools = JSON.stringify(result.usedTools) + if (result?.fileAnnotations) apiMessage.fileAnnotations = JSON.stringify(result.fileAnnotations) await this.addChatMessage(apiMessage) logger.debug(`[server]: Finished running ${nodeToExecuteData.label} (${nodeToExecuteData.id})`) diff --git a/packages/server/src/utils/index.ts b/packages/server/src/utils/index.ts index 239773a9a..0c6f23624 100644 --- a/packages/server/src/utils/index.ts +++ b/packages/server/src/utils/index.ts @@ -842,7 +842,7 @@ export const isFlowValidForStream = (reactFlowNodes: IReactFlowNode[], endingNod let isValidChainOrAgent = false if (endingNodeData.category === 'Chains') { // Chains that are not available to stream - const blacklistChains = ['openApiChain'] + const blacklistChains = ['openApiChain', 'vectaraQAChain'] isValidChainOrAgent = !blacklistChains.includes(endingNodeData.name) } else if (endingNodeData.category === 'Agents') { // Agent that are available to stream @@ -985,10 +985,14 @@ export const redactCredentialWithPasswordType = ( * @param {any} instance * @param {string} chatId */ -export const checkMemorySessionId = (instance: any, chatId: string): string => { +export const checkMemorySessionId = (instance: any, chatId: string): string | undefined => { if (instance.memory && instance.memory.isSessionIdUsingChatMessageId && chatId) { instance.memory.sessionId = chatId instance.memory.chatHistory.sessionId = chatId } - return instance.memory ? instance.memory.sessionId ?? instance.memory.chatHistory.sessionId : undefined + + if (instance.memory && instance.memory.sessionId) return instance.memory.sessionId + else if (instance.memory && instance.memory.chatHistory && instance.memory.chatHistory.sessionId) + return instance.memory.chatHistory.sessionId + return undefined } diff --git a/packages/ui/src/api/assistants.js b/packages/ui/src/api/assistants.js index 63dd5e18a..ac941126d 100644 --- a/packages/ui/src/api/assistants.js +++ b/packages/ui/src/api/assistants.js @@ -12,7 +12,8 @@ const createNewAssistant = (body) => client.post(`/assistants`, body) const updateAssistant = (id, body) => client.put(`/assistants/${id}`, body) -const deleteAssistant = (id) => client.delete(`/assistants/${id}`) +const deleteAssistant = (id, isDeleteBoth) => + isDeleteBoth ? client.delete(`/assistants/${id}?isDeleteBoth=true`) : client.delete(`/assistants/${id}`) export default { getAllAssistants, diff --git a/packages/ui/src/ui-component/button/FlowListMenu.js b/packages/ui/src/ui-component/button/FlowListMenu.js new file mode 100644 index 000000000..b242d2cb2 --- /dev/null +++ b/packages/ui/src/ui-component/button/FlowListMenu.js @@ -0,0 +1,291 @@ +import { useState } from 'react' +import { useDispatch } from 'react-redux' +import PropTypes from 'prop-types' + +import { styled, alpha } from '@mui/material/styles' +import Menu from '@mui/material/Menu' +import MenuItem from '@mui/material/MenuItem' +import EditIcon from '@mui/icons-material/Edit' +import Divider from '@mui/material/Divider' +import FileCopyIcon from '@mui/icons-material/FileCopy' +import FileDownloadIcon from '@mui/icons-material/Downloading' +import FileDeleteIcon from '@mui/icons-material/Delete' +import FileCategoryIcon from '@mui/icons-material/Category' +import Button from '@mui/material/Button' +import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown' +import { IconX } from '@tabler/icons' + +import chatflowsApi from 'api/chatflows' + +import useApi from '../../hooks/useApi' +import useConfirm from 'hooks/useConfirm' +import { uiBaseURL } from '../../store/constant' +import { closeSnackbar as closeSnackbarAction, enqueueSnackbar as enqueueSnackbarAction } from '../../store/actions' + +import ConfirmDialog from '../dialog/ConfirmDialog' +import SaveChatflowDialog from '../dialog/SaveChatflowDialog' +import TagDialog from '../dialog/TagDialog' + +import { generateExportFlowData } from '../../utils/genericHelper' +import useNotifier from '../../utils/useNotifier' + +const StyledMenu = styled((props) => ( + +))(({ theme }) => ({ + '& .MuiPaper-root': { + borderRadius: 6, + marginTop: theme.spacing(1), + minWidth: 180, + boxShadow: + 'rgb(255, 255, 255) 0px 0px 0px 0px, rgba(0, 0, 0, 0.05) 0px 0px 0px 1px, rgba(0, 0, 0, 0.1) 0px 10px 15px -3px, rgba(0, 0, 0, 0.05) 0px 4px 6px -2px', + '& .MuiMenu-list': { + padding: '4px 0' + }, + '& .MuiMenuItem-root': { + '& .MuiSvgIcon-root': { + fontSize: 18, + color: theme.palette.text.secondary, + marginRight: theme.spacing(1.5) + }, + '&:active': { + backgroundColor: alpha(theme.palette.primary.main, theme.palette.action.selectedOpacity) + } + } + } +})) + +export default function FlowListMenu({ chatflow, updateFlowsApi }) { + const { confirm } = useConfirm() + const dispatch = useDispatch() + const updateChatflowApi = useApi(chatflowsApi.updateChatflow) + + useNotifier() + const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args)) + const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args)) + + const [flowDialogOpen, setFlowDialogOpen] = useState(false) + const [categoryDialogOpen, setCategoryDialogOpen] = useState(false) + const [categoryDialogProps, setCategoryDialogProps] = useState({}) + const [anchorEl, setAnchorEl] = useState(null) + const open = Boolean(anchorEl) + + const handleClick = (event) => { + setAnchorEl(event.currentTarget) + } + + const handleClose = () => { + setAnchorEl(null) + } + + const handleFlowRename = () => { + setAnchorEl(null) + setFlowDialogOpen(true) + } + + const saveFlowRename = async (chatflowName) => { + const updateBody = { + name: chatflowName, + chatflow + } + try { + await updateChatflowApi.request(chatflow.id, updateBody) + await updateFlowsApi.request() + } catch (error) { + const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}` + enqueueSnackbar({ + message: errorData, + options: { + key: new Date().getTime() + Math.random(), + variant: 'error', + persist: true, + action: (key) => ( + + ) + } + }) + } + } + + const handleFlowCategory = () => { + setAnchorEl(null) + if (chatflow.category) { + setCategoryDialogProps({ + category: chatflow.category.split(';') + }) + } + setCategoryDialogOpen(true) + } + + const saveFlowCategory = async (categories) => { + setCategoryDialogOpen(false) + // save categories as string + const categoryTags = categories.join(';') + const updateBody = { + category: categoryTags, + chatflow + } + try { + await updateChatflowApi.request(chatflow.id, updateBody) + await updateFlowsApi.request() + } catch (error) { + const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}` + enqueueSnackbar({ + message: errorData, + options: { + key: new Date().getTime() + Math.random(), + variant: 'error', + persist: true, + action: (key) => ( + + ) + } + }) + } + } + + const handleDelete = async () => { + setAnchorEl(null) + const confirmPayload = { + title: `Delete`, + description: `Delete chatflow ${chatflow.name}?`, + confirmButtonName: 'Delete', + cancelButtonName: 'Cancel' + } + const isConfirmed = await confirm(confirmPayload) + + if (isConfirmed) { + try { + await chatflowsApi.deleteChatflow(chatflow.id) + await updateFlowsApi.request() + } catch (error) { + const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}` + enqueueSnackbar({ + message: errorData, + options: { + key: new Date().getTime() + Math.random(), + variant: 'error', + persist: true, + action: (key) => ( + + ) + } + }) + } + } + } + + const handleDuplicate = () => { + setAnchorEl(null) + try { + localStorage.setItem('duplicatedFlowData', chatflow.flowData) + window.open(`${uiBaseURL}/canvas`, '_blank') + } catch (e) { + console.error(e) + } + } + + const handleExport = () => { + setAnchorEl(null) + try { + const flowData = JSON.parse(chatflow.flowData) + let dataStr = JSON.stringify(generateExportFlowData(flowData), null, 2) + let dataUri = 'data:application/json;charset=utf-8,' + encodeURIComponent(dataStr) + + let exportFileDefaultName = `${chatflow.name} Chatflow.json` + + let linkElement = document.createElement('a') + linkElement.setAttribute('href', dataUri) + linkElement.setAttribute('download', exportFileDefaultName) + linkElement.click() + } catch (e) { + console.error(e) + } + } + + return ( +
+ + + + + Rename + + + + Duplicate + + + + Export + + + + + Update Category + + + + + Delete + + + + setFlowDialogOpen(false)} + onConfirm={saveFlowRename} + /> + setCategoryDialogOpen(false)} + onSubmit={saveFlowCategory} + /> +
+ ) +} + +FlowListMenu.propTypes = { + chatflow: PropTypes.object, + updateFlowsApi: PropTypes.object +} diff --git a/packages/ui/src/ui-component/button/StyledButton.jsx b/packages/ui/src/ui-component/button/StyledButton.jsx index 6e0c70786..29e17f804 100644 --- a/packages/ui/src/ui-component/button/StyledButton.jsx +++ b/packages/ui/src/ui-component/button/StyledButton.jsx @@ -1,5 +1,6 @@ import { styled } from '@mui/material/styles' import { Button } from '@mui/material' +import MuiToggleButton from '@mui/material/ToggleButton' export const StyledButton = styled(Button)(({ theme, color = 'primary' }) => ({ color: 'white', @@ -9,3 +10,10 @@ export const StyledButton = styled(Button)(({ theme, color = 'primary' }) => ({ backgroundImage: `linear-gradient(rgb(0 0 0/10%) 0 0)` } })) + +export const StyledToggleButton = styled(MuiToggleButton)(({ theme, color = 'primary' }) => ({ + '&.Mui-selected, &.Mui-selected:hover': { + color: 'white', + backgroundColor: theme.palette[color].main + } +})) diff --git a/packages/ui/src/ui-component/dialog/TagDialog.js b/packages/ui/src/ui-component/dialog/TagDialog.js new file mode 100644 index 000000000..82c35dde6 --- /dev/null +++ b/packages/ui/src/ui-component/dialog/TagDialog.js @@ -0,0 +1,110 @@ +import { useState, useEffect } from 'react' +import Dialog from '@mui/material/Dialog' +import Box from '@mui/material/Box' +import Button from '@mui/material/Button' +import TextField from '@mui/material/TextField' +import Chip from '@mui/material/Chip' +import PropTypes from 'prop-types' +import { DialogActions, DialogContent, DialogTitle, Typography } from '@mui/material' + +const TagDialog = ({ isOpen, dialogProps, onClose, onSubmit }) => { + const [inputValue, setInputValue] = useState('') + const [categoryValues, setCategoryValues] = useState([]) + + const handleInputChange = (event) => { + setInputValue(event.target.value) + } + + const handleInputKeyDown = (event) => { + if (event.key === 'Enter' && inputValue.trim()) { + event.preventDefault() + if (!categoryValues.includes(inputValue)) { + setCategoryValues([...categoryValues, inputValue]) + setInputValue('') + } + } + } + + const handleDeleteTag = (categoryToDelete) => { + setCategoryValues(categoryValues.filter((category) => category !== categoryToDelete)) + } + + const handleSubmit = (event) => { + event.preventDefault() + let newCategories = [...categoryValues] + if (inputValue.trim() && !categoryValues.includes(inputValue)) { + newCategories = [...newCategories, inputValue] + setCategoryValues(newCategories) + } + onSubmit(newCategories) + } + + useEffect(() => { + if (dialogProps.category) setCategoryValues(dialogProps.category) + + return () => { + setInputValue('') + setCategoryValues([]) + } + }, [dialogProps]) + + return ( + + + Set Chatflow Category Tags + + + +
+ {categoryValues.length > 0 && ( +
+ {categoryValues.map((category, index) => ( + handleDeleteTag(category)} + style={{ marginRight: 5, marginBottom: 5 }} + /> + ))} +
+ )} + + + Enter a tag and press enter to add it to the list. You can add as many tags as you want. + + +
+
+ + + + +
+ ) +} + +TagDialog.propTypes = { + isOpen: PropTypes.bool, + dialogProps: PropTypes.object, + onClose: PropTypes.func, + onSubmit: PropTypes.func +} + +export default TagDialog diff --git a/packages/ui/src/ui-component/dialog/ViewMessagesDialog.jsx b/packages/ui/src/ui-component/dialog/ViewMessagesDialog.jsx index c5374bdf6..cadd4abd9 100644 --- a/packages/ui/src/ui-component/dialog/ViewMessagesDialog.jsx +++ b/packages/ui/src/ui-component/dialog/ViewMessagesDialog.jsx @@ -7,6 +7,7 @@ import rehypeMathjax from 'rehype-mathjax' import rehypeRaw from 'rehype-raw' import remarkGfm from 'remark-gfm' import remarkMath from 'remark-math' +import axios from 'axios' // material-ui import { @@ -25,33 +26,34 @@ import { import { useTheme } from '@mui/material/styles' import DatePicker from 'react-datepicker' -import robotPNG from '@/assets/images/robot.png' -import userPNG from '@/assets/images/account.png' -import msgEmptySVG from '@/assets/images/message_empty.svg' -import { IconFileExport, IconEraser, IconX } from '@tabler/icons' +import robotPNG from 'assets/images/robot.png' +import userPNG from 'assets/images/account.png' +import msgEmptySVG from 'assets/images/message_empty.svg' +import { IconFileExport, IconEraser, IconX, IconDownload } from '@tabler/icons' // Project import -import { MemoizedReactMarkdown } from '@/ui-component/markdown/MemoizedReactMarkdown' -import { CodeBlock } from '@/ui-component/markdown/CodeBlock' -import SourceDocDialog from '@/ui-component/dialog/SourceDocDialog' -import { MultiDropdown } from '@/ui-component/dropdown/MultiDropdown' -import { StyledButton } from '@/ui-component/button/StyledButton' +import { MemoizedReactMarkdown } from 'ui-component/markdown/MemoizedReactMarkdown' +import { CodeBlock } from 'ui-component/markdown/CodeBlock' +import SourceDocDialog from 'ui-component/dialog/SourceDocDialog' +import { MultiDropdown } from 'ui-component/dropdown/MultiDropdown' +import { StyledButton } from 'ui-component/button/StyledButton' // store -import { HIDE_CANVAS_DIALOG, SHOW_CANVAS_DIALOG } from '@/store/actions' +import { HIDE_CANVAS_DIALOG, SHOW_CANVAS_DIALOG } from 'store/actions' // API -import chatmessageApi from '@/api/chatmessage' -import useApi from '@/hooks/useApi' -import useConfirm from '@/hooks/useConfirm' +import chatmessageApi from 'api/chatmessage' +import useApi from 'hooks/useApi' +import useConfirm from 'hooks/useConfirm' // Utils -import { isValidURL, removeDuplicateURL } from '@/utils/genericHelper' -import useNotifier from '@/utils/useNotifier' +import { isValidURL, removeDuplicateURL } from 'utils/genericHelper' +import useNotifier from 'utils/useNotifier' +import { baseURL } from 'store/constant' -import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction } from '@/store/actions' +import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction } from 'store/actions' -import '@/views/chatmessage/ChatMessage.css' +import 'views/chatmessage/ChatMessage.css' import 'react-datepicker/dist/react-datepicker.css' const DatePickerCustomInput = forwardRef(function DatePickerCustomInput({ value, onClick }, ref) { @@ -130,6 +132,7 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => { } if (chatmsg.sourceDocuments) msg.sourceDocuments = JSON.parse(chatmsg.sourceDocuments) if (chatmsg.usedTools) msg.usedTools = JSON.parse(chatmsg.usedTools) + if (chatmsg.fileAnnotations) msg.fileAnnotations = JSON.parse(chatmsg.fileAnnotations) if (!Object.prototype.hasOwnProperty.call(obj, chatPK)) { obj[chatPK] = { @@ -253,6 +256,7 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => { } if (chatmsg.sourceDocuments) obj.sourceDocuments = JSON.parse(chatmsg.sourceDocuments) if (chatmsg.usedTools) obj.usedTools = JSON.parse(chatmsg.usedTools) + if (chatmsg.fileAnnotations) obj.fileAnnotations = JSON.parse(chatmsg.fileAnnotations) loadedMessages.push(obj) } @@ -318,6 +322,26 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => { window.open(data, '_blank') } + const downloadFile = async (fileAnnotation) => { + try { + const response = await axios.post( + `${baseURL}/api/v1/openai-assistants-file`, + { fileName: fileAnnotation.fileName }, + { responseType: 'blob' } + ) + const blob = new Blob([response.data], { type: response.headers['content-type'] }) + const downloadUrl = window.URL.createObjectURL(blob) + const link = document.createElement('a') + link.href = downloadUrl + link.download = fileAnnotation.fileName + document.body.appendChild(link) + link.click() + link.remove() + } catch (error) { + console.error('Download failed:', error) + } + } + const onSourceDialogClick = (data, title) => { setSourceDialogProps({ data, title }) setSourceDialogOpen(true) @@ -648,10 +672,37 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => { {message.message} + {message.fileAnnotations && ( +
+ {message.fileAnnotations.map((fileAnnotation, index) => { + return ( + + ) + })} +
+ )} {message.sourceDocuments && (
{removeDuplicateURL(message).map((source, index) => { - const URL = isValidURL(source.metadata.source) + const URL = + source.metadata && source.metadata.source + ? isValidURL(source.metadata.source) + : undefined return ( ({ + [`&.${tableCellClasses.head}`]: { + backgroundColor: theme.palette.common.black, + color: theme.palette.common.white + }, + [`&.${tableCellClasses.body}`]: { + fontSize: 14 + } +})) + +const StyledTableRow = styled(TableRow)(({ theme }) => ({ + '&:nth-of-type(odd)': { + backgroundColor: theme.palette.action.hover + }, + // hide last border + '&:last-child td, &:last-child th': { + border: 0 + } +})) + +export const FlowListTable = ({ data, images, filterFunction, updateFlowsApi }) => { + const navigate = useNavigate() + const goToCanvas = (selectedChatflow) => { + navigate(`/canvas/${selectedChatflow.id}`) + } + + return ( + <> + + + + + + Name + + + Category + + + Nodes + + + Last Modified Date + + + Actions + + + + + {data.filter(filterFunction).map((row, index) => ( + + + + + + + +
+   + {row.category && + row.category + .split(';') + .map((tag, index) => ( + + ))} +
+
+ + {images[row.id] && ( +
+ {images[row.id].slice(0, images[row.id].length > 5 ? 5 : images[row.id].length).map((img) => ( +
+ +
+ ))} + {images[row.id].length > 5 && ( + + + {images[row.id].length - 5} More + + )} +
+ )} +
+ {moment(row.updatedDate).format('MMMM Do, YYYY')} + + + + + +
+ ))} +
+
+
+ + ) +} + +FlowListTable.propTypes = { + data: PropTypes.object, + images: PropTypes.array, + filterFunction: PropTypes.func, + updateFlowsApi: PropTypes.object +} diff --git a/packages/ui/src/ui-component/toolbar/Toolbar.js b/packages/ui/src/ui-component/toolbar/Toolbar.js new file mode 100644 index 000000000..f72ba339f --- /dev/null +++ b/packages/ui/src/ui-component/toolbar/Toolbar.js @@ -0,0 +1,24 @@ +import * as React from 'react' +import ViewListIcon from '@mui/icons-material/ViewList' +import ViewModuleIcon from '@mui/icons-material/ViewModule' +import ToggleButtonGroup from '@mui/material/ToggleButtonGroup' +import { StyledToggleButton } from '../button/StyledButton' + +export default function Toolbar() { + const [view, setView] = React.useState('list') + + const handleChange = (event, nextView) => { + setView(nextView) + } + + return ( + + + + + + + + + ) +} diff --git a/packages/ui/src/utils/genericHelper.js b/packages/ui/src/utils/genericHelper.js index 32331b142..da1e5ad83 100644 --- a/packages/ui/src/utils/genericHelper.js +++ b/packages/ui/src/utils/genericHelper.js @@ -423,10 +423,14 @@ export const removeDuplicateURL = (message) => { if (!message.sourceDocuments) return newSourceDocuments message.sourceDocuments.forEach((source) => { - if (isValidURL(source.metadata.source) && !visitedURLs.includes(source.metadata.source)) { - visitedURLs.push(source.metadata.source) - newSourceDocuments.push(source) - } else if (!isValidURL(source.metadata.source)) { + if (source.metadata && source.metadata.source) { + if (isValidURL(source.metadata.source) && !visitedURLs.includes(source.metadata.source)) { + visitedURLs.push(source.metadata.source) + newSourceDocuments.push(source) + } else if (!isValidURL(source.metadata.source)) { + newSourceDocuments.push(source) + } + } else { newSourceDocuments.push(source) } }) diff --git a/packages/ui/src/views/apikey/index.jsx b/packages/ui/src/views/apikey/index.jsx index d2febacd2..68113af5b 100644 --- a/packages/ui/src/views/apikey/index.jsx +++ b/packages/ui/src/views/apikey/index.jsx @@ -1,47 +1,188 @@ import { useEffect, useState } from 'react' import { useDispatch, useSelector } from 'react-redux' -import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction } from '@/store/actions' +import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction } from 'store/actions' // material-ui import { Button, Box, + Chip, Stack, Table, TableBody, - TableCell, TableContainer, TableHead, TableRow, Paper, IconButton, Popover, - Typography + Collapse, + Typography, + Toolbar, + TextField, + InputAdornment, + ButtonGroup } from '@mui/material' -import { useTheme } from '@mui/material/styles' +import TableCell, { tableCellClasses } from '@mui/material/TableCell' +import { useTheme, styled } from '@mui/material/styles' // project imports -import MainCard from '@/ui-component/cards/MainCard' -import { StyledButton } from '@/ui-component/button/StyledButton' +import MainCard from 'ui-component/cards/MainCard' +import { StyledButton } from 'ui-component/button/StyledButton' import APIKeyDialog from './APIKeyDialog' -import ConfirmDialog from '@/ui-component/dialog/ConfirmDialog' +import ConfirmDialog from 'ui-component/dialog/ConfirmDialog' // API -import apiKeyApi from '@/api/apikey' +import apiKeyApi from 'api/apikey' // Hooks -import useApi from '@/hooks/useApi' -import useConfirm from '@/hooks/useConfirm' +import useApi from 'hooks/useApi' +import useConfirm from 'hooks/useConfirm' // utils -import useNotifier from '@/utils/useNotifier' +import useNotifier from 'utils/useNotifier' // Icons -import { IconTrash, IconEdit, IconCopy, IconX, IconPlus, IconEye, IconEyeOff } from '@tabler/icons' -import APIEmptySVG from '@/assets/images/api_empty.svg' +import { + IconTrash, + IconEdit, + IconCopy, + IconChevronsUp, + IconChevronsDown, + IconX, + IconSearch, + IconPlus, + IconEye, + IconEyeOff +} from '@tabler/icons' +import APIEmptySVG from 'assets/images/api_empty.svg' +import * as PropTypes from 'prop-types' +import moment from 'moment/moment' // ==============================|| APIKey ||============================== // +const StyledTableCell = styled(TableCell)(({ theme }) => ({ + [`&.${tableCellClasses.head}`]: { + backgroundColor: theme.palette.action.hover + } +})) +const StyledTableRow = styled(TableRow)(() => ({ + // hide last border + '&:last-child td, &:last-child th': { + border: 0 + } +})) + +function APIKeyRow(props) { + const [open, setOpen] = useState(false) + return ( + <> + + {props.apiKey.keyName} + + {props.showApiKeys.includes(props.apiKey.apiKey) + ? props.apiKey.apiKey + : `${props.apiKey.apiKey.substring(0, 2)}${'•'.repeat(18)}${props.apiKey.apiKey.substring( + props.apiKey.apiKey.length - 5 + )}`} + + + + + {props.showApiKeys.includes(props.apiKey.apiKey) ? : } + + + + Copied! + + + + + {props.apiKey.chatFlows.length}{' '} + {props.apiKey.chatFlows.length > 0 && ( + setOpen(!open)}> + {props.apiKey.chatFlows.length > 0 && open ? : } + + )} + + {props.apiKey.createdAt} + + + + + + + + + + + + {open && ( + + + + + + + + + Chatflow Name + + Modified On + Category + + + + {props.apiKey.chatFlows.map((flow, index) => ( + + {flow.flowName} + {moment(flow.updatedDate).format('DD-MMM-YY')} + +   + {flow.category && + flow.category + .split(';') + .map((tag, index) => ( + + ))} + + + ))} + +
+
+
+
+
+ )} + + ) +} + +APIKeyRow.propTypes = { + apiKey: PropTypes.any, + showApiKeys: PropTypes.arrayOf(PropTypes.any), + onCopyClick: PropTypes.func, + onShowAPIClick: PropTypes.func, + open: PropTypes.bool, + anchorEl: PropTypes.any, + onClose: PropTypes.func, + theme: PropTypes.any, + onEditClick: PropTypes.func, + onDeleteClick: PropTypes.func +} const APIKey = () => { const theme = useTheme() const customization = useSelector((state) => state.customization) @@ -59,6 +200,14 @@ const APIKey = () => { const [showApiKeys, setShowApiKeys] = useState([]) const openPopOver = Boolean(anchorEl) + const [search, setSearch] = useState('') + const onSearchChange = (event) => { + setSearch(event.target.value) + } + function filterKeys(data) { + return data.keyName.toLowerCase().indexOf(search.toLowerCase()) > -1 + } + const { confirm } = useConfirm() const getAllAPIKeysApi = useApi(apiKeyApi.getAllAPIKeys) @@ -106,7 +255,10 @@ const APIKey = () => { const deleteKey = async (key) => { const confirmPayload = { title: `Delete`, - description: `Delete key ${key.keyName}?`, + description: + key.chatFlows.length === 0 + ? `Delete key [${key.keyName}] ? ` + : `Delete key [${key.keyName}] ?\n There are ${key.chatFlows.length} chatflows using this key.`, confirmButtonName: 'Delete', cancelButtonName: 'Cancel' } @@ -171,12 +323,53 @@ const APIKey = () => { <> -

API Keys 

- - - }> - Create Key - + + +

API Keys 

+ + + + ) + }} + /> + + + + } + > + Create Key + + + +
+
{apiKeys.length <= 0 && ( @@ -193,72 +386,33 @@ const APIKey = () => { Key Name API Key + Usage Created - {apiKeys.map((key, index) => ( - - - {key.keyName} - - - {showApiKeys.includes(key.apiKey) - ? key.apiKey - : `${key.apiKey.substring(0, 2)}${'•'.repeat(18)}${key.apiKey.substring( - key.apiKey.length - 5 - )}`} - { - navigator.clipboard.writeText(key.apiKey) - setAnchorEl(event.currentTarget) - setTimeout(() => { - handleClosePopOver() - }, 1500) - }} - > - - - onShowApiKeyClick(key.apiKey)}> - {showApiKeys.includes(key.apiKey) ? : } - - - - Copied! - - - - {key.createdAt} - - edit(key)}> - - - - - deleteKey(key)}> - - - - + {apiKeys.filter(filterKeys).map((key, index) => ( + { + navigator.clipboard.writeText(key.apiKey) + setAnchorEl(event.currentTarget) + setTimeout(() => { + handleClosePopOver() + }, 1500) + }} + onShowAPIClick={() => onShowApiKeyClick(key.apiKey)} + open={openPopOver} + anchorEl={anchorEl} + onClose={handleClosePopOver} + theme={theme} + onEditClick={() => edit(key)} + onDeleteClick={() => deleteKey(key)} + /> ))} diff --git a/packages/ui/src/views/assistants/AssistantDialog.jsx b/packages/ui/src/views/assistants/AssistantDialog.jsx index f741f70cb..30087baed 100644 --- a/packages/ui/src/views/assistants/AssistantDialog.jsx +++ b/packages/ui/src/views/assistants/AssistantDialog.jsx @@ -2,33 +2,32 @@ import { createPortal } from 'react-dom' import PropTypes from 'prop-types' import { useState, useEffect } from 'react' import { useDispatch } from 'react-redux' -import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction } from '@/store/actions' +import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction } from 'store/actions' import { v4 as uuidv4 } from 'uuid' import { Box, Typography, Button, IconButton, Dialog, DialogActions, DialogContent, DialogTitle, Stack, OutlinedInput } from '@mui/material' -import { StyledButton } from '@/ui-component/button/StyledButton' -import { TooltipWithParser } from '@/ui-component/tooltip/TooltipWithParser' -import ConfirmDialog from '@/ui-component/dialog/ConfirmDialog' -import { Dropdown } from '@/ui-component/dropdown/Dropdown' -import { MultiDropdown } from '@/ui-component/dropdown/MultiDropdown' -import CredentialInputHandler from '@/views/canvas/CredentialInputHandler' -import { File } from '@/ui-component/file/File' -import { BackdropLoader } from '@/ui-component/loading/BackdropLoader' +import { StyledButton } from 'ui-component/button/StyledButton' +import { TooltipWithParser } from 'ui-component/tooltip/TooltipWithParser' +import { Dropdown } from 'ui-component/dropdown/Dropdown' +import { MultiDropdown } from 'ui-component/dropdown/MultiDropdown' +import CredentialInputHandler from 'views/canvas/CredentialInputHandler' +import { File } from 'ui-component/file/File' +import { BackdropLoader } from 'ui-component/loading/BackdropLoader' +import DeleteConfirmDialog from './DeleteConfirmDialog' // Icons import { IconX } from '@tabler/icons' // API -import assistantsApi from '@/api/assistants' +import assistantsApi from 'api/assistants' // Hooks -import useConfirm from '@/hooks/useConfirm' -import useApi from '@/hooks/useApi' +import useApi from 'hooks/useApi' // utils -import useNotifier from '@/utils/useNotifier' -import { HIDE_CANVAS_DIALOG, SHOW_CANVAS_DIALOG } from '@/store/actions' +import useNotifier from 'utils/useNotifier' +import { HIDE_CANVAS_DIALOG, SHOW_CANVAS_DIALOG } from 'store/actions' const assistantAvailableModels = [ { @@ -71,14 +70,8 @@ const assistantAvailableModels = [ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm }) => { const portalElement = document.getElementById('portal') - - const dispatch = useDispatch() - - // ==============================|| Snackbar ||============================== // - useNotifier() - const { confirm } = useConfirm() - + const dispatch = useDispatch() const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args)) const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args)) @@ -97,6 +90,8 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm }) => { const [assistantFiles, setAssistantFiles] = useState([]) const [uploadAssistantFiles, setUploadAssistantFiles] = useState('') const [loading, setLoading] = useState(false) + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false) + const [deleteDialogProps, setDeleteDialogProps] = useState({}) useEffect(() => { if (show) dispatch({ type: SHOW_CANVAS_DIALOG }) @@ -123,20 +118,7 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm }) => { useEffect(() => { if (getAssistantObjApi.data) { - setOpenAIAssistantId(getAssistantObjApi.data.id) - setAssistantName(getAssistantObjApi.data.name) - setAssistantDesc(getAssistantObjApi.data.description) - setAssistantModel(getAssistantObjApi.data.model) - setAssistantInstructions(getAssistantObjApi.data.instructions) - setAssistantFiles(getAssistantObjApi.data.files ?? []) - - let tools = [] - if (getAssistantObjApi.data.tools && getAssistantObjApi.data.tools.length) { - for (const tool of getAssistantObjApi.data.tools) { - tools.push(tool.type) - } - } - setAssistantTools(tools) + syncData(getAssistantObjApi.data) } }, [getAssistantObjApi.data]) @@ -199,6 +181,23 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm }) => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [dialogProps]) + const syncData = (data) => { + setOpenAIAssistantId(data.id) + setAssistantName(data.name) + setAssistantDesc(data.description) + setAssistantModel(data.model) + setAssistantInstructions(data.instructions) + setAssistantFiles(data.files ?? []) + + let tools = [] + if (data.tools && data.tools.length) { + for (const tool of data.tools) { + tools.push(tool.type) + } + } + setAssistantTools(tools) + } + const addNewAssistant = async () => { setLoading(true) try { @@ -309,41 +308,17 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm }) => { } } - const deleteAssistant = async () => { - const confirmPayload = { - title: `Delete Assistant`, - description: `Delete Assistant ${assistantName}?`, - confirmButtonName: 'Delete', - cancelButtonName: 'Cancel' - } - const isConfirmed = await confirm(confirmPayload) - - if (isConfirmed) { - try { - const delResp = await assistantsApi.deleteAssistant(assistantId) - if (delResp.data) { - enqueueSnackbar({ - message: 'Assistant deleted', - options: { - key: new Date().getTime() + Math.random(), - variant: 'success', - action: (key) => ( - - ) - } - }) - onConfirm() - } - } catch (error) { - const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}` + const onSyncClick = async () => { + setLoading(true) + try { + const getResp = await assistantsApi.getAssistantObj(openAIAssistantId, assistantCredential) + if (getResp.data) { + syncData(getResp.data) enqueueSnackbar({ - message: `Failed to delete Assistant: ${errorData}`, + message: 'Assistant successfully synced!', options: { key: new Date().getTime() + Math.random(), - variant: 'error', - persist: true, + variant: 'success', action: (key) => ( + ) + } + }) + setLoading(false) + } + } + + const onDeleteClick = () => { + setDeleteDialogProps({ + title: `Delete Assistant`, + description: `Delete Assistant ${assistantName}?`, + cancelButtonName: 'Cancel' + }) + setDeleteDialogOpen(true) + } + + const deleteAssistant = async (isDeleteBoth) => { + setDeleteDialogOpen(false) + try { + const delResp = await assistantsApi.deleteAssistant(assistantId, isDeleteBoth) + if (delResp.data) { + enqueueSnackbar({ + message: 'Assistant deleted', + options: { + key: new Date().getTime() + Math.random(), + variant: 'success', + action: (key) => ( + + ) + } + }) + onConfirm() + } + } catch (error) { + const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}` + enqueueSnackbar({ + message: `Failed to delete Assistant: ${errorData}`, + options: { + key: new Date().getTime() + Math.random(), + variant: 'error', + persist: true, + action: (key) => ( + + ) + } + }) + onCancel() } } @@ -578,7 +616,12 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm }) => { {dialogProps.type === 'EDIT' && ( - deleteAssistant()}> + onSyncClick()}> + Sync + + )} + {dialogProps.type === 'EDIT' && ( + onDeleteClick()}> Delete )} @@ -590,7 +633,13 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm }) => { {dialogProps.confirmButtonName} - + setDeleteDialogOpen(false)} + onDelete={() => deleteAssistant()} + onDeleteBoth={() => deleteAssistant(true)} + /> {loading && } ) : null diff --git a/packages/ui/src/views/assistants/DeleteConfirmDialog.js b/packages/ui/src/views/assistants/DeleteConfirmDialog.js new file mode 100644 index 000000000..f4453631b --- /dev/null +++ b/packages/ui/src/views/assistants/DeleteConfirmDialog.js @@ -0,0 +1,47 @@ +import { createPortal } from 'react-dom' +import PropTypes from 'prop-types' +import { Button, Dialog, DialogContent, DialogTitle } from '@mui/material' +import { StyledButton } from 'ui-component/button/StyledButton' + +const DeleteConfirmDialog = ({ show, dialogProps, onCancel, onDelete, onDeleteBoth }) => { + const portalElement = document.getElementById('portal') + + const component = show ? ( + + + {dialogProps.title} + + + {dialogProps.description} +
+ + Delete only from Flowise + + + Delete from both OpenAI and Flowise + + +
+
+
+ ) : null + + return createPortal(component, portalElement) +} + +DeleteConfirmDialog.propTypes = { + show: PropTypes.bool, + dialogProps: PropTypes.object, + onDeleteBoth: PropTypes.func, + onDelete: PropTypes.func, + onCancel: PropTypes.func +} + +export default DeleteConfirmDialog diff --git a/packages/ui/src/views/chatflows/index.jsx b/packages/ui/src/views/chatflows/index.jsx index b4916a462..3c4b89728 100644 --- a/packages/ui/src/views/chatflows/index.jsx +++ b/packages/ui/src/views/chatflows/index.jsx @@ -3,28 +3,31 @@ import { useNavigate } from 'react-router-dom' import { useSelector } from 'react-redux' // material-ui -import { Grid, Box, Stack } from '@mui/material' +import { Grid, Box, Stack, Toolbar, ToggleButton, ButtonGroup, InputAdornment, TextField } from '@mui/material' import { useTheme } from '@mui/material/styles' // project imports -import MainCard from '@/ui-component/cards/MainCard' -import ItemCard from '@/ui-component/cards/ItemCard' -import { gridSpacing } from '@/store/constant' -import WorkflowEmptySVG from '@/assets/images/workflow_empty.svg' -import { StyledButton } from '@/ui-component/button/StyledButton' -import LoginDialog from '@/ui-component/dialog/LoginDialog' +import MainCard from 'ui-component/cards/MainCard' +import ItemCard from 'ui-component/cards/ItemCard' +import { gridSpacing } from 'store/constant' +import WorkflowEmptySVG from 'assets/images/workflow_empty.svg' +import LoginDialog from 'ui-component/dialog/LoginDialog' // API -import chatflowsApi from '@/api/chatflows' +import chatflowsApi from 'api/chatflows' // Hooks -import useApi from '@/hooks/useApi' +import useApi from 'hooks/useApi' // const -import { baseURL } from '@/store/constant' +import { baseURL } from 'store/constant' // icons -import { IconPlus } from '@tabler/icons' +import { IconPlus, IconSearch, IconLayoutGrid, IconList } from '@tabler/icons' +import * as React from 'react' +import ToggleButtonGroup from '@mui/material/ToggleButtonGroup' +import { FlowListTable } from '../../ui-component/table/FlowListTable' +import { StyledButton } from '../../ui-component/button/StyledButton' // ==============================|| CHATFLOWS ||============================== // @@ -35,10 +38,28 @@ const Chatflows = () => { const [isLoading, setLoading] = useState(true) const [images, setImages] = useState({}) + const [search, setSearch] = useState('') const [loginDialogOpen, setLoginDialogOpen] = useState(false) const [loginDialogProps, setLoginDialogProps] = useState({}) const getAllChatflowsApi = useApi(chatflowsApi.getAllChatflows) + const [view, setView] = React.useState(localStorage.getItem('flowDisplayStyle') || 'card') + + const handleChange = (event, nextView) => { + localStorage.setItem('flowDisplayStyle', nextView) + setView(nextView) + } + + const onSearchChange = (event) => { + setSearch(event.target.value) + } + + function filterFlows(data) { + return ( + data.name.toLowerCase().indexOf(search.toLowerCase()) > -1 || + (data.category && data.category.toLowerCase().indexOf(search.toLowerCase()) > -1) + ) + } const onLoginClick = (username, password) => { localStorage.setItem('username', username) @@ -102,26 +123,86 @@ const Chatflows = () => { return ( - -

Chatflows

- - - - }> - Add New - + + + +

Chatflows

+ + + + ) + }} + /> + + + + + + + + + + + + + + + }> + Add New + + + +
+
+ {!isLoading && (!view || view === 'card') && getAllChatflowsApi.data && ( + + {getAllChatflowsApi.data.filter(filterFlows).map((data, index) => ( + + goToCanvas(data)} data={data} images={images[data.id]} /> + + ))} -
+ )} + {!isLoading && view === 'list' && getAllChatflowsApi.data && ( + + )}
- - {!isLoading && - getAllChatflowsApi.data && - getAllChatflowsApi.data.map((data, index) => ( - - goToCanvas(data)} data={data} images={images[data.id]} /> - - ))} - + {!isLoading && (!getAllChatflowsApi.data || getAllChatflowsApi.data.length === 0) && ( diff --git a/packages/ui/src/views/chatmessage/ChatMessage.jsx b/packages/ui/src/views/chatmessage/ChatMessage.jsx index cb3944680..adf23f7a8 100644 --- a/packages/ui/src/views/chatmessage/ChatMessage.jsx +++ b/packages/ui/src/views/chatmessage/ChatMessage.jsx @@ -7,10 +7,11 @@ import rehypeMathjax from 'rehype-mathjax' import rehypeRaw from 'rehype-raw' import remarkGfm from 'remark-gfm' import remarkMath from 'remark-math' +import axios from 'axios' -import { CircularProgress, OutlinedInput, Divider, InputAdornment, IconButton, Box, Chip } from '@mui/material' +import { CircularProgress, OutlinedInput, Divider, InputAdornment, IconButton, Box, Chip, Button } from '@mui/material' import { useTheme } from '@mui/material/styles' -import { IconSend } from '@tabler/icons' +import { IconSend, IconDownload } from '@tabler/icons' // project import import { CodeBlock } from '@/ui-component/markdown/CodeBlock' @@ -139,7 +140,13 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { setMessages((prevMessages) => [ ...prevMessages, - { message: text, sourceDocuments: data?.sourceDocuments, usedTools: data?.usedTools, type: 'apiMessage' } + { + message: text, + sourceDocuments: data?.sourceDocuments, + usedTools: data?.usedTools, + fileAnnotations: data?.fileAnnotations, + type: 'apiMessage' + } ]) } @@ -170,6 +177,26 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { } } + const downloadFile = async (fileAnnotation) => { + try { + const response = await axios.post( + `${baseURL}/api/v1/openai-assistants-file`, + { fileName: fileAnnotation.fileName }, + { responseType: 'blob' } + ) + const blob = new Blob([response.data], { type: response.headers['content-type'] }) + const downloadUrl = window.URL.createObjectURL(blob) + const link = document.createElement('a') + link.href = downloadUrl + link.download = fileAnnotation.fileName + document.body.appendChild(link) + link.click() + link.remove() + } catch (error) { + console.error('Download failed:', error) + } + } + // Get chatmessages successful useEffect(() => { if (getChatmessageApi.data?.length) { @@ -183,6 +210,7 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { } if (message.sourceDocuments) obj.sourceDocuments = JSON.parse(message.sourceDocuments) if (message.usedTools) obj.usedTools = JSON.parse(message.usedTools) + if (message.fileAnnotations) obj.fileAnnotations = JSON.parse(message.fileAnnotations) return obj }) setMessages((prevMessages) => [...prevMessages, ...loadedMessages]) @@ -331,10 +359,30 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { {message.message}
+ {message.fileAnnotations && ( +
+ {message.fileAnnotations.map((fileAnnotation, index) => { + return ( + + ) + })} +
+ )} {message.sourceDocuments && (
{removeDuplicateURL(message).map((source, index) => { - const URL = isValidURL(source.metadata.source) + const URL = + source.metadata && source.metadata.source + ? isValidURL(source.metadata.source) + : undefined return ( { const getAllCredentialsApi = useApi(credentialsApi.getAllCredentials) const getAllComponentsCredentialsApi = useApi(credentialsApi.getAllComponentsCredentials) + const [search, setSearch] = useState('') + const onSearchChange = (event) => { + setSearch(event.target.value) + } + function filterCredentials(data) { + return data.credentialName.toLowerCase().indexOf(search.toLowerCase()) > -1 + } + const listCredential = () => { const dialogProp = { title: 'Add New Credential', @@ -168,17 +192,53 @@ const Credentials = () => { <> -

Credentials 

- - - } - > - Add Credential - + + +

Credentials 

+ + + + ) + }} + /> + + + + } + > + Add Credential + + + +
+
{credentials.length <= 0 && ( @@ -205,7 +265,7 @@ const Credentials = () => { - {credentials.map((credential, index) => ( + {credentials.filter(filterCredentials).map((credential, index) => (