Merge branch 'main' of https://github.com/use-the-fork/Flowise into Bug/rework-redis-connection

# Conflicts:
#	packages/components/package.json
#	packages/ui/src/ui-component/dialog/ViewMessagesDialog.jsx
#	packages/ui/src/views/apikey/index.jsx
#	packages/ui/src/views/assistants/AssistantDialog.jsx
#	packages/ui/src/views/chatflows/index.jsx
#	packages/ui/src/views/credentials/index.jsx
This commit is contained in:
Greg L
2023-11-23 08:24:59 -05:00
38 changed files with 1990 additions and 284 deletions
@@ -11,8 +11,8 @@ class ElasticSearchUserPassword implements INodeCredential {
this.label = 'ElasticSearch User Password'
this.name = 'elasticSearchUserPassword'
this.version = 1.0
this.description =
'Refer to <a target="_blank" href="https://www.elastic.co/guide/en/kibana/current/tutorial-secure-access-to-kibana.html">official guide</a> 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 <a target="_blank" href="https://www.elastic.co/guide/en/elasticsearch/reference/current/setting-up-authentication.html">official guide</a> on how to get User Password from ElasticSearch.`
this.inputs = [
{
label: 'Cloud ID',
@@ -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<void>((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`, {
@@ -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<number>()
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 <a target="_blank" href="https://docs.vectara.com/docs/learn/grounded-generation/select-a-summarizer">more</a>',
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 <a target="_blank" href="https://docs.vectara.com/docs/learn/grounded-generation/grounded-generation-response-languages">more</a>',
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<any> {
return null
}
async run(nodeData: INodeData, input: string): Promise<object> {
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<string, unknown> = {}
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<string, unknown>; 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 }
Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

@@ -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',
@@ -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
}
}
}
}
+3 -10
View File
@@ -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"
}
}