|
|
const { v4: uuidv4 } = require("uuid"); |
|
|
const { DocumentManager } = require("../DocumentManager"); |
|
|
const { WorkspaceChats } = require("../../models/workspaceChats"); |
|
|
const { getVectorDbClass, getLLMProvider } = require("../helpers"); |
|
|
const { writeResponseChunk } = require("../helpers/chat/responses"); |
|
|
const { chatPrompt, sourceIdentifier } = require("./index"); |
|
|
|
|
|
const { PassThrough } = require("stream"); |
|
|
|
|
|
async function chatSync({ |
|
|
workspace, |
|
|
systemPrompt = null, |
|
|
history = [], |
|
|
prompt = null, |
|
|
attachments = [], |
|
|
temperature = null, |
|
|
}) { |
|
|
const uuid = uuidv4(); |
|
|
const chatMode = workspace?.chatMode ?? "chat"; |
|
|
const LLMConnector = getLLMProvider({ |
|
|
provider: workspace?.chatProvider, |
|
|
model: workspace?.chatModel, |
|
|
}); |
|
|
const VectorDb = getVectorDbClass(); |
|
|
const hasVectorizedSpace = await VectorDb.hasNamespace(workspace.slug); |
|
|
const embeddingsCount = await VectorDb.namespaceCount(workspace.slug); |
|
|
|
|
|
|
|
|
|
|
|
if ((!hasVectorizedSpace || embeddingsCount === 0) && chatMode === "query") { |
|
|
const textResponse = |
|
|
workspace?.queryRefusalResponse ?? |
|
|
"There is no relevant information in this workspace to answer your query."; |
|
|
|
|
|
await WorkspaceChats.new({ |
|
|
workspaceId: workspace.id, |
|
|
prompt: String(prompt), |
|
|
response: { |
|
|
text: textResponse, |
|
|
sources: [], |
|
|
type: chatMode, |
|
|
attachments, |
|
|
}, |
|
|
include: false, |
|
|
}); |
|
|
|
|
|
return formatJSON( |
|
|
{ |
|
|
id: uuid, |
|
|
type: "textResponse", |
|
|
sources: [], |
|
|
close: true, |
|
|
error: null, |
|
|
textResponse, |
|
|
}, |
|
|
{ model: workspace.slug, finish_reason: "abort" } |
|
|
); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let contextTexts = []; |
|
|
let sources = []; |
|
|
let pinnedDocIdentifiers = []; |
|
|
await new DocumentManager({ |
|
|
workspace, |
|
|
maxTokens: LLMConnector.promptWindowLimit(), |
|
|
}) |
|
|
.pinnedDocs() |
|
|
.then((pinnedDocs) => { |
|
|
pinnedDocs.forEach((doc) => { |
|
|
const { pageContent, ...metadata } = doc; |
|
|
pinnedDocIdentifiers.push(sourceIdentifier(doc)); |
|
|
contextTexts.push(doc.pageContent); |
|
|
sources.push({ |
|
|
text: |
|
|
pageContent.slice(0, 1_000) + |
|
|
"...continued on in source document...", |
|
|
...metadata, |
|
|
}); |
|
|
}); |
|
|
}); |
|
|
|
|
|
const vectorSearchResults = |
|
|
embeddingsCount !== 0 |
|
|
? await VectorDb.performSimilaritySearch({ |
|
|
namespace: workspace.slug, |
|
|
input: String(prompt), |
|
|
LLMConnector, |
|
|
similarityThreshold: workspace?.similarityThreshold, |
|
|
topN: workspace?.topN, |
|
|
filterIdentifiers: pinnedDocIdentifiers, |
|
|
rerank: workspace?.vectorSearchMode === "rerank", |
|
|
}) |
|
|
: { |
|
|
contextTexts: [], |
|
|
sources: [], |
|
|
message: null, |
|
|
}; |
|
|
|
|
|
|
|
|
if (!!vectorSearchResults.message) { |
|
|
return formatJSON( |
|
|
{ |
|
|
id: uuid, |
|
|
type: "abort", |
|
|
textResponse: null, |
|
|
sources: [], |
|
|
close: true, |
|
|
error: vectorSearchResults.message, |
|
|
}, |
|
|
{ model: workspace.slug, finish_reason: "abort" } |
|
|
); |
|
|
} |
|
|
|
|
|
|
|
|
contextTexts = [...contextTexts, ...vectorSearchResults.contextTexts]; |
|
|
sources = [...sources, ...vectorSearchResults.sources]; |
|
|
|
|
|
|
|
|
|
|
|
if (chatMode === "query" && contextTexts.length === 0) { |
|
|
const textResponse = |
|
|
workspace?.queryRefusalResponse ?? |
|
|
"There is no relevant information in this workspace to answer your query."; |
|
|
|
|
|
await WorkspaceChats.new({ |
|
|
workspaceId: workspace.id, |
|
|
prompt: String(prompt), |
|
|
response: { |
|
|
text: textResponse, |
|
|
sources: [], |
|
|
type: chatMode, |
|
|
attachments, |
|
|
}, |
|
|
include: false, |
|
|
}); |
|
|
|
|
|
return formatJSON( |
|
|
{ |
|
|
id: uuid, |
|
|
type: "textResponse", |
|
|
sources: [], |
|
|
close: true, |
|
|
error: null, |
|
|
textResponse, |
|
|
}, |
|
|
{ model: workspace.slug, finish_reason: "no_content" } |
|
|
); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const messages = await LLMConnector.compressMessages({ |
|
|
systemPrompt: systemPrompt ?? (await chatPrompt(workspace)), |
|
|
userPrompt: String(prompt), |
|
|
contextTexts, |
|
|
chatHistory: history, |
|
|
attachments, |
|
|
}); |
|
|
|
|
|
|
|
|
const { textResponse, metrics } = await LLMConnector.getChatCompletion( |
|
|
messages, |
|
|
{ |
|
|
temperature: |
|
|
temperature ?? workspace?.openAiTemp ?? LLMConnector.defaultTemp, |
|
|
} |
|
|
); |
|
|
|
|
|
if (!textResponse) { |
|
|
return formatJSON( |
|
|
{ |
|
|
id: uuid, |
|
|
type: "textResponse", |
|
|
sources: [], |
|
|
close: true, |
|
|
error: "No text completion could be completed with this input.", |
|
|
textResponse: null, |
|
|
}, |
|
|
{ model: workspace.slug, finish_reason: "no_content", usage: metrics } |
|
|
); |
|
|
} |
|
|
|
|
|
const { chat } = await WorkspaceChats.new({ |
|
|
workspaceId: workspace.id, |
|
|
prompt: String(prompt), |
|
|
response: { |
|
|
text: textResponse, |
|
|
sources, |
|
|
type: chatMode, |
|
|
metrics, |
|
|
attachments, |
|
|
}, |
|
|
}); |
|
|
|
|
|
return formatJSON( |
|
|
{ |
|
|
id: uuid, |
|
|
type: "textResponse", |
|
|
close: true, |
|
|
error: null, |
|
|
chatId: chat.id, |
|
|
textResponse, |
|
|
sources, |
|
|
}, |
|
|
{ model: workspace.slug, finish_reason: "stop", usage: metrics } |
|
|
); |
|
|
} |
|
|
|
|
|
async function streamChat({ |
|
|
workspace, |
|
|
response, |
|
|
systemPrompt = null, |
|
|
history = [], |
|
|
prompt = null, |
|
|
attachments = [], |
|
|
temperature = null, |
|
|
}) { |
|
|
const uuid = uuidv4(); |
|
|
const chatMode = workspace?.chatMode ?? "chat"; |
|
|
const LLMConnector = getLLMProvider({ |
|
|
provider: workspace?.chatProvider, |
|
|
model: workspace?.chatModel, |
|
|
}); |
|
|
const VectorDb = getVectorDbClass(); |
|
|
const hasVectorizedSpace = await VectorDb.hasNamespace(workspace.slug); |
|
|
const embeddingsCount = await VectorDb.namespaceCount(workspace.slug); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const responseInterceptor = new PassThrough({}); |
|
|
responseInterceptor.on("data", (chunk) => { |
|
|
try { |
|
|
const originalData = JSON.parse(chunk.toString().split("data: ")[1]); |
|
|
const modified = formatJSON(originalData, { |
|
|
chunked: true, |
|
|
model: workspace.slug, |
|
|
}); |
|
|
response.write(`data: ${JSON.stringify(modified)}\n\n`); |
|
|
} catch (e) { |
|
|
console.error(e); |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
if ((!hasVectorizedSpace || embeddingsCount === 0) && chatMode === "query") { |
|
|
const textResponse = |
|
|
workspace?.queryRefusalResponse ?? |
|
|
"There is no relevant information in this workspace to answer your query."; |
|
|
|
|
|
await WorkspaceChats.new({ |
|
|
workspaceId: workspace.id, |
|
|
prompt: String(prompt), |
|
|
response: { |
|
|
text: textResponse, |
|
|
sources: [], |
|
|
type: chatMode, |
|
|
attachments, |
|
|
}, |
|
|
include: false, |
|
|
}); |
|
|
|
|
|
writeResponseChunk( |
|
|
response, |
|
|
formatJSON( |
|
|
{ |
|
|
id: uuid, |
|
|
type: "textResponse", |
|
|
sources: [], |
|
|
close: true, |
|
|
error: null, |
|
|
textResponse, |
|
|
}, |
|
|
{ chunked: true, model: workspace.slug, finish_reason: "abort" } |
|
|
) |
|
|
); |
|
|
return; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let contextTexts = []; |
|
|
let sources = []; |
|
|
let pinnedDocIdentifiers = []; |
|
|
await new DocumentManager({ |
|
|
workspace, |
|
|
maxTokens: LLMConnector.promptWindowLimit(), |
|
|
}) |
|
|
.pinnedDocs() |
|
|
.then((pinnedDocs) => { |
|
|
pinnedDocs.forEach((doc) => { |
|
|
const { pageContent, ...metadata } = doc; |
|
|
pinnedDocIdentifiers.push(sourceIdentifier(doc)); |
|
|
contextTexts.push(doc.pageContent); |
|
|
sources.push({ |
|
|
text: |
|
|
pageContent.slice(0, 1_000) + |
|
|
"...continued on in source document...", |
|
|
...metadata, |
|
|
}); |
|
|
}); |
|
|
}); |
|
|
|
|
|
const vectorSearchResults = |
|
|
embeddingsCount !== 0 |
|
|
? await VectorDb.performSimilaritySearch({ |
|
|
namespace: workspace.slug, |
|
|
input: String(prompt), |
|
|
LLMConnector, |
|
|
similarityThreshold: workspace?.similarityThreshold, |
|
|
topN: workspace?.topN, |
|
|
filterIdentifiers: pinnedDocIdentifiers, |
|
|
rerank: workspace?.vectorSearchMode === "rerank", |
|
|
}) |
|
|
: { |
|
|
contextTexts: [], |
|
|
sources: [], |
|
|
message: null, |
|
|
}; |
|
|
|
|
|
|
|
|
if (!!vectorSearchResults.message) { |
|
|
writeResponseChunk( |
|
|
response, |
|
|
formatJSON( |
|
|
{ |
|
|
id: uuid, |
|
|
type: "abort", |
|
|
textResponse: null, |
|
|
sources: [], |
|
|
close: true, |
|
|
error: vectorSearchResults.message, |
|
|
}, |
|
|
{ chunked: true, model: workspace.slug, finish_reason: "abort" } |
|
|
) |
|
|
); |
|
|
return; |
|
|
} |
|
|
|
|
|
|
|
|
contextTexts = [...contextTexts, ...vectorSearchResults.contextTexts]; |
|
|
sources = [...sources, ...vectorSearchResults.sources]; |
|
|
|
|
|
|
|
|
|
|
|
if (chatMode === "query" && contextTexts.length === 0) { |
|
|
const textResponse = |
|
|
workspace?.queryRefusalResponse ?? |
|
|
"There is no relevant information in this workspace to answer your query."; |
|
|
|
|
|
await WorkspaceChats.new({ |
|
|
workspaceId: workspace.id, |
|
|
prompt: String(prompt), |
|
|
response: { |
|
|
text: textResponse, |
|
|
sources: [], |
|
|
type: chatMode, |
|
|
attachments, |
|
|
}, |
|
|
include: false, |
|
|
}); |
|
|
|
|
|
writeResponseChunk( |
|
|
response, |
|
|
formatJSON( |
|
|
{ |
|
|
id: uuid, |
|
|
type: "textResponse", |
|
|
sources: [], |
|
|
close: true, |
|
|
error: null, |
|
|
textResponse, |
|
|
}, |
|
|
{ chunked: true, model: workspace.slug, finish_reason: "no_content" } |
|
|
) |
|
|
); |
|
|
return; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const messages = await LLMConnector.compressMessages({ |
|
|
systemPrompt: systemPrompt ?? (await chatPrompt(workspace)), |
|
|
userPrompt: String(prompt), |
|
|
contextTexts, |
|
|
chatHistory: history, |
|
|
attachments, |
|
|
}); |
|
|
|
|
|
if (!LLMConnector.streamingEnabled()) { |
|
|
writeResponseChunk( |
|
|
response, |
|
|
formatJSON( |
|
|
{ |
|
|
id: uuid, |
|
|
type: "textResponse", |
|
|
sources: [], |
|
|
close: true, |
|
|
error: "Streaming is not available for the connected LLM Provider", |
|
|
textResponse: null, |
|
|
}, |
|
|
{ |
|
|
chunked: true, |
|
|
model: workspace.slug, |
|
|
finish_reason: "streaming_disabled", |
|
|
} |
|
|
) |
|
|
); |
|
|
return; |
|
|
} |
|
|
|
|
|
const stream = await LLMConnector.streamGetChatCompletion(messages, { |
|
|
temperature: |
|
|
temperature ?? workspace?.openAiTemp ?? LLMConnector.defaultTemp, |
|
|
}); |
|
|
const completeText = await LLMConnector.handleStream( |
|
|
responseInterceptor, |
|
|
stream, |
|
|
{ |
|
|
uuid, |
|
|
sources, |
|
|
} |
|
|
); |
|
|
|
|
|
if (completeText?.length > 0) { |
|
|
const { chat } = await WorkspaceChats.new({ |
|
|
workspaceId: workspace.id, |
|
|
prompt: String(prompt), |
|
|
response: { |
|
|
text: completeText, |
|
|
sources, |
|
|
type: chatMode, |
|
|
metrics: stream.metrics, |
|
|
attachments, |
|
|
}, |
|
|
}); |
|
|
|
|
|
writeResponseChunk( |
|
|
response, |
|
|
formatJSON( |
|
|
{ |
|
|
uuid, |
|
|
type: "finalizeResponseStream", |
|
|
close: true, |
|
|
error: false, |
|
|
chatId: chat.id, |
|
|
textResponse: "", |
|
|
}, |
|
|
{ |
|
|
chunked: true, |
|
|
model: workspace.slug, |
|
|
finish_reason: "stop", |
|
|
usage: stream.metrics, |
|
|
} |
|
|
) |
|
|
); |
|
|
return; |
|
|
} |
|
|
|
|
|
writeResponseChunk( |
|
|
response, |
|
|
formatJSON( |
|
|
{ |
|
|
uuid, |
|
|
type: "finalizeResponseStream", |
|
|
close: true, |
|
|
error: false, |
|
|
textResponse: "", |
|
|
}, |
|
|
{ |
|
|
chunked: true, |
|
|
model: workspace.slug, |
|
|
finish_reason: "stop", |
|
|
usage: stream.metrics, |
|
|
} |
|
|
) |
|
|
); |
|
|
return; |
|
|
} |
|
|
|
|
|
function formatJSON( |
|
|
chat, |
|
|
{ chunked = false, model, finish_reason = null, usage = {} } |
|
|
) { |
|
|
const data = { |
|
|
id: chat.uuid ?? chat.id, |
|
|
object: "chat.completion", |
|
|
created: Math.floor(Number(new Date()) / 1000), |
|
|
model: model, |
|
|
choices: [ |
|
|
{ |
|
|
index: 0, |
|
|
[chunked ? "delta" : "message"]: { |
|
|
role: "assistant", |
|
|
content: chat.textResponse, |
|
|
}, |
|
|
logprobs: null, |
|
|
finish_reason: finish_reason, |
|
|
}, |
|
|
], |
|
|
usage, |
|
|
}; |
|
|
|
|
|
return data; |
|
|
} |
|
|
|
|
|
module.exports.OpenAICompatibleChat = { |
|
|
chatSync, |
|
|
streamChat, |
|
|
}; |
|
|
|