mirror of
https://github.com/Mintplex-Labs/anything-llm.git
synced 2025-05-02 09:03:12 +00:00
Implement workspace threading that is backwards compatible (#699)
* Implement workspace thread that is compatible with legacy versions * last touches * comment on chat qty enforcement
This commit is contained in:
parent
b985524901
commit
406732830f
21 changed files with 1087 additions and 83 deletions
|
@ -61,6 +61,10 @@ export default function App() {
|
|||
path="/workspace/:slug"
|
||||
element={<PrivateRoute Component={WorkspaceChat} />}
|
||||
/>
|
||||
<Route
|
||||
path="/workspace/:slug/t/:threadSlug"
|
||||
element={<PrivateRoute Component={WorkspaceChat} />}
|
||||
/>
|
||||
<Route path="/accept-invite/:code" element={<InvitePage />} />
|
||||
|
||||
{/* Admin */}
|
||||
|
|
|
@ -0,0 +1,189 @@
|
|||
import Workspace from "@/models/workspace";
|
||||
import paths from "@/utils/paths";
|
||||
import showToast from "@/utils/toast";
|
||||
import { DotsThree, PencilSimple, Trash } from "@phosphor-icons/react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import truncate from "truncate";
|
||||
|
||||
const THREAD_CALLOUT_DETAIL_WIDTH = 26;
|
||||
export default function ThreadItem({ workspace, thread, onRemove, hasNext }) {
|
||||
const optionsContainer = useRef(null);
|
||||
const { slug, threadSlug = null } = useParams();
|
||||
const [showOptions, setShowOptions] = useState(false);
|
||||
const [name, setName] = useState(thread.name);
|
||||
|
||||
const isActive = threadSlug === thread.slug;
|
||||
const linkTo = !thread.slug
|
||||
? paths.workspace.chat(slug)
|
||||
: paths.workspace.thread(slug, thread.slug);
|
||||
|
||||
return (
|
||||
<div className="w-full relative flex h-[40px] items-center border-none hover:bg-slate-600/20 rounded-lg">
|
||||
{/* Curved line Element and leader if required */}
|
||||
<div
|
||||
style={{ width: THREAD_CALLOUT_DETAIL_WIDTH / 2 }}
|
||||
className="border-l border-b border-slate-300 h-[50%] absolute top-0 left-2 rounded-bl-lg"
|
||||
></div>
|
||||
{hasNext && (
|
||||
<div
|
||||
style={{ width: THREAD_CALLOUT_DETAIL_WIDTH / 2 }}
|
||||
className="border-l border-slate-300 h-[100%] absolute top-0 left-2"
|
||||
></div>
|
||||
)}
|
||||
|
||||
{/* Curved line inline placeholder for spacing */}
|
||||
<div
|
||||
style={{ width: THREAD_CALLOUT_DETAIL_WIDTH }}
|
||||
className="w-[26px] h-full"
|
||||
/>
|
||||
<div className="flex w-full items-center justify-between pr-2 group relative">
|
||||
<a href={isActive ? "#" : linkTo} className="w-full">
|
||||
<p
|
||||
className={`text-left text-sm ${
|
||||
isActive
|
||||
? "font-semibold text-slate-300"
|
||||
: "text-slate-400 italic"
|
||||
}`}
|
||||
>
|
||||
{truncate(name, 25)}
|
||||
</p>
|
||||
</a>
|
||||
{!!thread.slug && (
|
||||
<div ref={optionsContainer}>
|
||||
<div className="flex items-center w-fit group-hover:visible md:invisible gap-x-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowOptions(!showOptions)}
|
||||
>
|
||||
<DotsThree className="text-slate-300" size={25} />
|
||||
</button>
|
||||
</div>
|
||||
{showOptions && (
|
||||
<OptionsMenu
|
||||
containerRef={optionsContainer}
|
||||
workspace={workspace}
|
||||
thread={thread}
|
||||
onRemove={onRemove}
|
||||
onRename={setName}
|
||||
close={() => setShowOptions(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function OptionsMenu({
|
||||
containerRef,
|
||||
workspace,
|
||||
thread,
|
||||
onRename,
|
||||
onRemove,
|
||||
close,
|
||||
}) {
|
||||
const menuRef = useRef(null);
|
||||
|
||||
// Ref menu options
|
||||
const outsideClick = (e) => {
|
||||
if (!menuRef.current) return false;
|
||||
if (
|
||||
!menuRef.current?.contains(e.target) &&
|
||||
!containerRef.current?.contains(e.target)
|
||||
)
|
||||
close();
|
||||
return false;
|
||||
};
|
||||
|
||||
const isEsc = (e) => {
|
||||
if (e.key === "Escape" || e.key === "Esc") close();
|
||||
};
|
||||
|
||||
function cleanupListeners() {
|
||||
window.removeEventListener("click", outsideClick);
|
||||
window.removeEventListener("keyup", isEsc);
|
||||
}
|
||||
// end Ref menu options
|
||||
|
||||
useEffect(() => {
|
||||
function setListeners() {
|
||||
if (!menuRef?.current || !containerRef.current) return false;
|
||||
window.document.addEventListener("click", outsideClick);
|
||||
window.document.addEventListener("keyup", isEsc);
|
||||
}
|
||||
|
||||
setListeners();
|
||||
return cleanupListeners;
|
||||
}, [menuRef.current, containerRef.current]);
|
||||
|
||||
const renameThread = async () => {
|
||||
const name = window
|
||||
.prompt("What would you like to rename this thread to?")
|
||||
?.trim();
|
||||
if (!name || name.length === 0) {
|
||||
close();
|
||||
return;
|
||||
}
|
||||
|
||||
const { message } = await Workspace.threads.update(
|
||||
workspace.slug,
|
||||
thread.slug,
|
||||
{ name }
|
||||
);
|
||||
if (!!message) {
|
||||
showToast(`Thread could not be updated! ${message}`, "error", {
|
||||
clear: true,
|
||||
});
|
||||
close();
|
||||
return;
|
||||
}
|
||||
|
||||
onRename(name);
|
||||
close();
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (
|
||||
!window.confirm(
|
||||
"Are you sure you want to delete this thread? All of its chats will be deleted. You cannot undo this."
|
||||
)
|
||||
)
|
||||
return;
|
||||
const success = await Workspace.threads.delete(workspace.slug, thread.slug);
|
||||
if (!success) {
|
||||
showToast("Thread could not be deleted!", "error", { clear: true });
|
||||
return;
|
||||
}
|
||||
if (success) {
|
||||
showToast("Thread deleted successfully!", "success", { clear: true });
|
||||
onRemove(thread.id);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={menuRef}
|
||||
className="absolute w-fit z-[20] top-[25px] right-[10px] bg-zinc-900 rounded-lg p-1"
|
||||
>
|
||||
<button
|
||||
onClick={renameThread}
|
||||
type="button"
|
||||
className="w-full rounded-md flex items-center p-2 gap-x-2 hover:bg-slate-500/20 text-slate-300"
|
||||
>
|
||||
<PencilSimple size={18} />
|
||||
<p className="text-sm">Rename</p>
|
||||
</button>
|
||||
<button
|
||||
onClick={handleDelete}
|
||||
type="button"
|
||||
className="w-full rounded-md flex items-center p-2 gap-x-2 hover:bg-red-500/20 text-slate-300 hover:text-red-100"
|
||||
>
|
||||
<Trash size={18} />
|
||||
<p className="text-sm">Delete Thread</p>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,90 @@
|
|||
import Workspace from "@/models/workspace";
|
||||
import paths from "@/utils/paths";
|
||||
import showToast from "@/utils/toast";
|
||||
import { Plus, CircleNotch } from "@phosphor-icons/react";
|
||||
import { useEffect, useState } from "react";
|
||||
import ThreadItem from "./ThreadItem";
|
||||
|
||||
export default function ThreadContainer({ workspace }) {
|
||||
const [threads, setThreads] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchThreads() {
|
||||
if (!workspace.slug) return;
|
||||
const { threads } = await Workspace.threads.all(workspace.slug);
|
||||
setLoading(false);
|
||||
setThreads(threads);
|
||||
}
|
||||
fetchThreads();
|
||||
}, [workspace.slug]);
|
||||
|
||||
function removeThread(threadId) {
|
||||
setThreads((prev) => prev.filter((thread) => thread.id !== threadId));
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex flex-col bg-pulse w-full h-10 items-center justify-center">
|
||||
<p className="text-xs text-slate-600 animate-pulse">
|
||||
loading threads....
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<ThreadItem
|
||||
thread={{ slug: null, name: "default" }}
|
||||
hasNext={threads.length > 0}
|
||||
/>
|
||||
{threads.map((thread, i) => (
|
||||
<ThreadItem
|
||||
key={thread.slug}
|
||||
workspace={workspace}
|
||||
onRemove={removeThread}
|
||||
thread={thread}
|
||||
hasNext={i !== threads.length - 1}
|
||||
/>
|
||||
))}
|
||||
<NewThreadButton workspace={workspace} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function NewThreadButton({ workspace }) {
|
||||
const [loading, setLoading] = useState();
|
||||
const onClick = async () => {
|
||||
setLoading(true);
|
||||
const { thread, error } = await Workspace.threads.new(workspace.slug);
|
||||
if (!!error) {
|
||||
showToast(`Could not create thread - ${error}`, "error", { clear: true });
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
window.location.replace(
|
||||
paths.workspace.thread(workspace.slug, thread.slug)
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className="w-full relative flex h-[40px] items-center border-none hover:bg-slate-600/20 rounded-lg"
|
||||
>
|
||||
<div className="flex w-full gap-x-2 items-center pl-4">
|
||||
{loading ? (
|
||||
<CircleNotch className="animate-spin text-slate-300" />
|
||||
) : (
|
||||
<Plus className="text-slate-300" />
|
||||
)}
|
||||
{loading ? (
|
||||
<p className="text-left text-slate-300 text-sm">starting thread...</p>
|
||||
) : (
|
||||
<p className="text-left text-slate-300 text-sm">new thread</p>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
}
|
|
@ -10,6 +10,7 @@ import { useParams } from "react-router-dom";
|
|||
import { GearSix, SquaresFour } from "@phosphor-icons/react";
|
||||
import truncate from "truncate";
|
||||
import useUser from "@/hooks/useUser";
|
||||
import ThreadContainer from "./ThreadContainer";
|
||||
|
||||
export default function ActiveWorkspaces() {
|
||||
const { slug } = useParams();
|
||||
|
@ -68,15 +69,16 @@ export default function ActiveWorkspaces() {
|
|||
const isHovered = hoverStates[workspace.id];
|
||||
const isGearHovered = settingHover[workspace.id];
|
||||
return (
|
||||
<div
|
||||
key={workspace.id}
|
||||
className="flex gap-x-2 items-center justify-between"
|
||||
onMouseEnter={() => handleMouseEnter(workspace.id)}
|
||||
onMouseLeave={() => handleMouseLeave(workspace.id)}
|
||||
>
|
||||
<a
|
||||
href={isActive ? null : paths.workspace.chat(workspace.slug)}
|
||||
className={`
|
||||
<div className="flex flex-col w-full">
|
||||
<div
|
||||
key={workspace.id}
|
||||
className="flex gap-x-2 items-center justify-between"
|
||||
onMouseEnter={() => handleMouseEnter(workspace.id)}
|
||||
onMouseLeave={() => handleMouseLeave(workspace.id)}
|
||||
>
|
||||
<a
|
||||
href={isActive ? null : paths.workspace.chat(workspace.slug)}
|
||||
className={`
|
||||
transition-all duration-[200ms]
|
||||
flex flex-grow w-[75%] gap-x-2 py-[6px] px-[12px] rounded-lg text-slate-200 justify-start items-center border
|
||||
hover:bg-workspace-item-selected-gradient hover:border-slate-100 hover:border-opacity-50
|
||||
|
@ -85,44 +87,48 @@ export default function ActiveWorkspaces() {
|
|||
? "bg-workspace-item-selected-gradient border-slate-100 border-opacity-50"
|
||||
: "bg-workspace-item-gradient bg-opacity-60 border-transparent"
|
||||
}`}
|
||||
>
|
||||
<div className="flex flex-row justify-between w-full">
|
||||
<div className="flex items-center space-x-2">
|
||||
<SquaresFour
|
||||
weight={isActive ? "fill" : "regular"}
|
||||
className="h-5 w-5 flex-shrink-0"
|
||||
/>
|
||||
<p
|
||||
className={`text-white text-sm leading-loose font-medium whitespace-nowrap overflow-hidden ${
|
||||
isActive ? "" : "text-opacity-80"
|
||||
}`}
|
||||
>
|
||||
<div className="flex flex-row justify-between w-full">
|
||||
<div className="flex items-center space-x-2">
|
||||
<SquaresFour
|
||||
weight={isActive ? "fill" : "regular"}
|
||||
className="h-5 w-5 flex-shrink-0"
|
||||
/>
|
||||
<p
|
||||
className={`text-white text-sm leading-loose font-medium whitespace-nowrap overflow-hidden ${
|
||||
isActive ? "" : "text-opacity-80"
|
||||
}`}
|
||||
>
|
||||
{isActive
|
||||
? truncate(workspace.name, 17)
|
||||
: truncate(workspace.name, 20)}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setSelectedWs(workspace);
|
||||
showModal();
|
||||
}}
|
||||
onMouseEnter={() => handleGearMouseEnter(workspace.id)}
|
||||
onMouseLeave={() => handleGearMouseLeave(workspace.id)}
|
||||
className="rounded-md flex items-center justify-center text-white ml-auto"
|
||||
>
|
||||
{isActive
|
||||
? truncate(workspace.name, 17)
|
||||
: truncate(workspace.name, 20)}
|
||||
</p>
|
||||
<GearSix
|
||||
weight={isGearHovered ? "fill" : "regular"}
|
||||
hidden={
|
||||
(!isActive && !isHovered) || user?.role === "default"
|
||||
}
|
||||
className="h-[20px] w-[20px] transition-all duration-300"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setSelectedWs(workspace);
|
||||
showModal();
|
||||
}}
|
||||
onMouseEnter={() => handleGearMouseEnter(workspace.id)}
|
||||
onMouseLeave={() => handleGearMouseLeave(workspace.id)}
|
||||
className="rounded-md flex items-center justify-center text-white ml-auto"
|
||||
>
|
||||
<GearSix
|
||||
weight={isGearHovered ? "fill" : "regular"}
|
||||
hidden={
|
||||
(!isActive && !isHovered) || user?.role === "default"
|
||||
}
|
||||
className="h-[20px] w-[20px] transition-all duration-300"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</a>
|
||||
</a>
|
||||
</div>
|
||||
{isActive && (
|
||||
<ThreadContainer workspace={workspace} isActive={isActive} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
|
|
@ -5,8 +5,10 @@ import Workspace from "@/models/workspace";
|
|||
import handleChat from "@/utils/chat";
|
||||
import { isMobile } from "react-device-detect";
|
||||
import { SidebarMobileHeader } from "../../Sidebar";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
export default function ChatContainer({ workspace, knownHistory = [] }) {
|
||||
const { threadSlug = null } = useParams();
|
||||
const [message, setMessage] = useState("");
|
||||
const [loadingResponse, setLoadingResponse] = useState(false);
|
||||
const [chatHistory, setChatHistory] = useState(knownHistory);
|
||||
|
@ -71,20 +73,39 @@ export default function ChatContainer({ workspace, knownHistory = [] }) {
|
|||
return false;
|
||||
}
|
||||
|
||||
await Workspace.streamChat(
|
||||
workspace,
|
||||
promptMessage.userMessage,
|
||||
window.localStorage.getItem(`workspace_chat_mode_${workspace.slug}`) ??
|
||||
"chat",
|
||||
(chatResult) =>
|
||||
handleChat(
|
||||
chatResult,
|
||||
setLoadingResponse,
|
||||
setChatHistory,
|
||||
remHistory,
|
||||
_chatHistory
|
||||
)
|
||||
);
|
||||
if (!!threadSlug) {
|
||||
await Workspace.threads.streamChat(
|
||||
{ workspaceSlug: workspace.slug, threadSlug },
|
||||
promptMessage.userMessage,
|
||||
window.localStorage.getItem(
|
||||
`workspace_chat_mode_${workspace.slug}`
|
||||
) ?? "chat",
|
||||
(chatResult) =>
|
||||
handleChat(
|
||||
chatResult,
|
||||
setLoadingResponse,
|
||||
setChatHistory,
|
||||
remHistory,
|
||||
_chatHistory
|
||||
)
|
||||
);
|
||||
} else {
|
||||
await Workspace.streamChat(
|
||||
workspace,
|
||||
promptMessage.userMessage,
|
||||
window.localStorage.getItem(
|
||||
`workspace_chat_mode_${workspace.slug}`
|
||||
) ?? "chat",
|
||||
(chatResult) =>
|
||||
handleChat(
|
||||
chatResult,
|
||||
setLoadingResponse,
|
||||
setChatHistory,
|
||||
remHistory,
|
||||
_chatHistory
|
||||
)
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
loadingResponse === true && fetchReply();
|
||||
|
|
|
@ -4,8 +4,10 @@ import LoadingChat from "./LoadingChat";
|
|||
import ChatContainer from "./ChatContainer";
|
||||
import paths from "@/utils/paths";
|
||||
import ModalWrapper from "../ModalWrapper";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
export default function WorkspaceChat({ loading, workspace }) {
|
||||
const { threadSlug = null } = useParams();
|
||||
const [history, setHistory] = useState([]);
|
||||
const [loadingHistory, setLoadingHistory] = useState(true);
|
||||
|
||||
|
@ -17,7 +19,9 @@ export default function WorkspaceChat({ loading, workspace }) {
|
|||
return false;
|
||||
}
|
||||
|
||||
const chatHistory = await Workspace.chatHistory(workspace.slug);
|
||||
const chatHistory = threadSlug
|
||||
? await Workspace.threads.chatHistory(workspace.slug, threadSlug)
|
||||
: await Workspace.chatHistory(workspace.slug);
|
||||
setHistory(chatHistory);
|
||||
setLoadingHistory(false);
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { API_BASE } from "@/utils/constants";
|
||||
import { baseHeaders } from "@/utils/request";
|
||||
import { fetchEventSource } from "@microsoft/fetch-event-source";
|
||||
import WorkspaceThread from "@/models/workspaceThread";
|
||||
import { v4 } from "uuid";
|
||||
|
||||
const Workspace = {
|
||||
|
@ -204,6 +205,7 @@ const Workspace = {
|
|||
return { success: false, error: e.message };
|
||||
});
|
||||
},
|
||||
threads: WorkspaceThread,
|
||||
};
|
||||
|
||||
export default Workspace;
|
||||
|
|
146
frontend/src/models/workspaceThread.js
Normal file
146
frontend/src/models/workspaceThread.js
Normal file
|
@ -0,0 +1,146 @@
|
|||
import { API_BASE } from "@/utils/constants";
|
||||
import { baseHeaders } from "@/utils/request";
|
||||
import { fetchEventSource } from "@microsoft/fetch-event-source";
|
||||
import { v4 } from "uuid";
|
||||
|
||||
const WorkspaceThread = {
|
||||
all: async function (workspaceSlug) {
|
||||
const { threads } = await fetch(
|
||||
`${API_BASE}/workspace/${workspaceSlug}/threads`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: baseHeaders(),
|
||||
}
|
||||
)
|
||||
.then((res) => res.json())
|
||||
.catch((e) => {
|
||||
return { threads: [] };
|
||||
});
|
||||
|
||||
return { threads };
|
||||
},
|
||||
new: async function (workspaceSlug) {
|
||||
const { thread, error } = await fetch(
|
||||
`${API_BASE}/workspace/${workspaceSlug}/thread/new`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: baseHeaders(),
|
||||
}
|
||||
)
|
||||
.then((res) => res.json())
|
||||
.catch((e) => {
|
||||
return { thread: null, error: e.message };
|
||||
});
|
||||
|
||||
return { thread, error };
|
||||
},
|
||||
update: async function (workspaceSlug, threadSlug, data = {}) {
|
||||
const { thread, message } = await fetch(
|
||||
`${API_BASE}/workspace/${workspaceSlug}/thread/${threadSlug}/update`,
|
||||
{
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
headers: baseHeaders(),
|
||||
}
|
||||
)
|
||||
.then((res) => res.json())
|
||||
.catch((e) => {
|
||||
return { thread: null, message: e.message };
|
||||
});
|
||||
|
||||
return { thread, message };
|
||||
},
|
||||
delete: async function (workspaceSlug, threadSlug) {
|
||||
return await fetch(
|
||||
`${API_BASE}/workspace/${workspaceSlug}/thread/${threadSlug}`,
|
||||
{
|
||||
method: "DELETE",
|
||||
headers: baseHeaders(),
|
||||
}
|
||||
)
|
||||
.then((res) => res.ok)
|
||||
.catch(() => false);
|
||||
},
|
||||
chatHistory: async function (workspaceSlug, threadSlug) {
|
||||
const history = await fetch(
|
||||
`${API_BASE}/workspace/${workspaceSlug}/thread/${threadSlug}/chats`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: baseHeaders(),
|
||||
}
|
||||
)
|
||||
.then((res) => res.json())
|
||||
.then((res) => res.history || [])
|
||||
.catch(() => []);
|
||||
return history;
|
||||
},
|
||||
streamChat: async function (
|
||||
{ workspaceSlug, threadSlug },
|
||||
message,
|
||||
mode = "query",
|
||||
handleChat
|
||||
) {
|
||||
const ctrl = new AbortController();
|
||||
await fetchEventSource(
|
||||
`${API_BASE}/workspace/${workspaceSlug}/thread/${threadSlug}/stream-chat`,
|
||||
{
|
||||
method: "POST",
|
||||
body: JSON.stringify({ message, mode }),
|
||||
headers: baseHeaders(),
|
||||
signal: ctrl.signal,
|
||||
openWhenHidden: true,
|
||||
async onopen(response) {
|
||||
if (response.ok) {
|
||||
return; // everything's good
|
||||
} else if (
|
||||
response.status >= 400 &&
|
||||
response.status < 500 &&
|
||||
response.status !== 429
|
||||
) {
|
||||
handleChat({
|
||||
id: v4(),
|
||||
type: "abort",
|
||||
textResponse: null,
|
||||
sources: [],
|
||||
close: true,
|
||||
error: `An error occurred while streaming response. Code ${response.status}`,
|
||||
});
|
||||
ctrl.abort();
|
||||
throw new Error("Invalid Status code response.");
|
||||
} else {
|
||||
handleChat({
|
||||
id: v4(),
|
||||
type: "abort",
|
||||
textResponse: null,
|
||||
sources: [],
|
||||
close: true,
|
||||
error: `An error occurred while streaming response. Unknown Error.`,
|
||||
});
|
||||
ctrl.abort();
|
||||
throw new Error("Unknown error");
|
||||
}
|
||||
},
|
||||
async onmessage(msg) {
|
||||
try {
|
||||
const chatResult = JSON.parse(msg.data);
|
||||
handleChat(chatResult);
|
||||
} catch {}
|
||||
},
|
||||
onerror(err) {
|
||||
handleChat({
|
||||
id: v4(),
|
||||
type: "abort",
|
||||
textResponse: null,
|
||||
sources: [],
|
||||
close: true,
|
||||
error: `An error occurred while streaming response. ${err.message}`,
|
||||
});
|
||||
ctrl.abort();
|
||||
throw new Error();
|
||||
},
|
||||
}
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export default WorkspaceThread;
|
|
@ -19,7 +19,7 @@ export default function WorkspaceChat() {
|
|||
}
|
||||
|
||||
function ShowWorkspaceChat() {
|
||||
const { slug } = useParams();
|
||||
const { slug, threadSlug = null } = useParams();
|
||||
const [workspace, setWorkspace] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
|
@ -27,6 +27,10 @@ function ShowWorkspaceChat() {
|
|||
async function getWorkspace() {
|
||||
if (!slug) return;
|
||||
const _workspace = await Workspace.bySlug(slug);
|
||||
if (!_workspace) {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
const suggestedMessages = await Workspace.getSuggestedMessages(slug);
|
||||
setWorkspace({
|
||||
..._workspace,
|
||||
|
|
|
@ -58,6 +58,9 @@ export default {
|
|||
additionalSettings: (slug) => {
|
||||
return `/workspace/${slug}/settings`;
|
||||
},
|
||||
thread: (wsSlug, threadSlug) => {
|
||||
return `/workspace/${wsSlug}/t/${threadSlug}`;
|
||||
},
|
||||
},
|
||||
apiDocs: () => {
|
||||
return `${API_BASE}/docs`;
|
||||
|
|
|
@ -15,6 +15,9 @@ const {
|
|||
flexUserRoleValid,
|
||||
} = require("../utils/middleware/multiUserProtected");
|
||||
const { EventLogs } = require("../models/eventLogs");
|
||||
const {
|
||||
validWorkspaceAndThreadSlug,
|
||||
} = require("../utils/middleware/validWorkspace");
|
||||
|
||||
function chatEndpoints(app) {
|
||||
if (!app) return;
|
||||
|
@ -123,6 +126,117 @@ function chatEndpoints(app) {
|
|||
}
|
||||
}
|
||||
);
|
||||
|
||||
app.post(
|
||||
"/workspace/:slug/thread/:threadSlug/stream-chat",
|
||||
[
|
||||
validatedRequest,
|
||||
flexUserRoleValid([ROLES.all]),
|
||||
validWorkspaceAndThreadSlug,
|
||||
],
|
||||
async (request, response) => {
|
||||
try {
|
||||
const user = await userFromSession(request, response);
|
||||
const { message, mode = "query" } = reqBody(request);
|
||||
const workspace = response.locals.workspace;
|
||||
const thread = response.locals.thread;
|
||||
|
||||
if (!message?.length || !VALID_CHAT_MODE.includes(mode)) {
|
||||
response.status(400).json({
|
||||
id: uuidv4(),
|
||||
type: "abort",
|
||||
textResponse: null,
|
||||
sources: [],
|
||||
close: true,
|
||||
error: !message?.length
|
||||
? "Message is empty."
|
||||
: `${mode} is not a valid mode.`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
response.setHeader("Cache-Control", "no-cache");
|
||||
response.setHeader("Content-Type", "text/event-stream");
|
||||
response.setHeader("Access-Control-Allow-Origin", "*");
|
||||
response.setHeader("Connection", "keep-alive");
|
||||
response.flushHeaders();
|
||||
|
||||
if (multiUserMode(response) && user.role !== ROLES.admin) {
|
||||
const limitMessagesSetting = await SystemSettings.get({
|
||||
label: "limit_user_messages",
|
||||
});
|
||||
const limitMessages = limitMessagesSetting?.value === "true";
|
||||
|
||||
if (limitMessages) {
|
||||
const messageLimitSetting = await SystemSettings.get({
|
||||
label: "message_limit",
|
||||
});
|
||||
const systemLimit = Number(messageLimitSetting?.value);
|
||||
|
||||
if (!!systemLimit) {
|
||||
// Chat qty includes all threads because any user can freely
|
||||
// create threads and would bypass this rule.
|
||||
const currentChatCount = await WorkspaceChats.count({
|
||||
user_id: user.id,
|
||||
createdAt: {
|
||||
gte: new Date(new Date() - 24 * 60 * 60 * 1000),
|
||||
},
|
||||
});
|
||||
|
||||
if (currentChatCount >= systemLimit) {
|
||||
writeResponseChunk(response, {
|
||||
id: uuidv4(),
|
||||
type: "abort",
|
||||
textResponse: null,
|
||||
sources: [],
|
||||
close: true,
|
||||
error: `You have met your maximum 24 hour chat quota of ${systemLimit} chats set by the instance administrators. Try again later.`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await streamChatWithWorkspace(
|
||||
response,
|
||||
workspace,
|
||||
message,
|
||||
mode,
|
||||
user,
|
||||
thread
|
||||
);
|
||||
await Telemetry.sendTelemetry("sent_chat", {
|
||||
multiUserMode: multiUserMode(response),
|
||||
LLMSelection: process.env.LLM_PROVIDER || "openai",
|
||||
Embedder: process.env.EMBEDDING_ENGINE || "inherit",
|
||||
VectorDbSelection: process.env.VECTOR_DB || "pinecone",
|
||||
});
|
||||
|
||||
await EventLogs.logEvent(
|
||||
"sent_chat",
|
||||
{
|
||||
workspaceName: workspace.name,
|
||||
thread: thread.name,
|
||||
chatModel: workspace?.chatModel || "System Default",
|
||||
},
|
||||
user?.id
|
||||
);
|
||||
response.end();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
writeResponseChunk(response, {
|
||||
id: uuidv4(),
|
||||
type: "abort",
|
||||
textResponse: null,
|
||||
sources: [],
|
||||
close: true,
|
||||
error: e.message,
|
||||
});
|
||||
response.end();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = { chatEndpoints };
|
||||
|
|
150
server/endpoints/workspaceThreads.js
Normal file
150
server/endpoints/workspaceThreads.js
Normal file
|
@ -0,0 +1,150 @@
|
|||
const { multiUserMode, userFromSession, reqBody } = require("../utils/http");
|
||||
const { validatedRequest } = require("../utils/middleware/validatedRequest");
|
||||
const { Telemetry } = require("../models/telemetry");
|
||||
const {
|
||||
flexUserRoleValid,
|
||||
ROLES,
|
||||
} = require("../utils/middleware/multiUserProtected");
|
||||
const { EventLogs } = require("../models/eventLogs");
|
||||
const { WorkspaceThread } = require("../models/workspaceThread");
|
||||
const {
|
||||
validWorkspaceSlug,
|
||||
validWorkspaceAndThreadSlug,
|
||||
} = require("../utils/middleware/validWorkspace");
|
||||
const { WorkspaceChats } = require("../models/workspaceChats");
|
||||
const { convertToChatHistory } = require("../utils/chats");
|
||||
|
||||
function workspaceThreadEndpoints(app) {
|
||||
if (!app) return;
|
||||
|
||||
app.post(
|
||||
"/workspace/:slug/thread/new",
|
||||
[validatedRequest, flexUserRoleValid([ROLES.all]), validWorkspaceSlug],
|
||||
async (request, response) => {
|
||||
try {
|
||||
const user = await userFromSession(request, response);
|
||||
const workspace = response.locals.workspace;
|
||||
const { thread, message } = await WorkspaceThread.new(
|
||||
workspace,
|
||||
user?.id
|
||||
);
|
||||
await Telemetry.sendTelemetry(
|
||||
"workspace_thread_created",
|
||||
{
|
||||
multiUserMode: multiUserMode(response),
|
||||
LLMSelection: process.env.LLM_PROVIDER || "openai",
|
||||
Embedder: process.env.EMBEDDING_ENGINE || "inherit",
|
||||
VectorDbSelection: process.env.VECTOR_DB || "pinecone",
|
||||
},
|
||||
user?.id
|
||||
);
|
||||
|
||||
await EventLogs.logEvent(
|
||||
"workspace_thread_created",
|
||||
{
|
||||
workspaceName: workspace?.name || "Unknown Workspace",
|
||||
},
|
||||
user?.id
|
||||
);
|
||||
response.status(200).json({ thread, message });
|
||||
} catch (e) {
|
||||
console.log(e.message, e);
|
||||
response.sendStatus(500).end();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
app.get(
|
||||
"/workspace/:slug/threads",
|
||||
[validatedRequest, flexUserRoleValid([ROLES.all]), validWorkspaceSlug],
|
||||
async (request, response) => {
|
||||
try {
|
||||
const user = await userFromSession(request, response);
|
||||
const workspace = response.locals.workspace;
|
||||
const threads = await WorkspaceThread.where({
|
||||
workspace_id: workspace.id,
|
||||
user_id: user?.id || null,
|
||||
});
|
||||
response.status(200).json({ threads });
|
||||
} catch (e) {
|
||||
console.log(e.message, e);
|
||||
response.sendStatus(500).end();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
app.delete(
|
||||
"/workspace/:slug/thread/:threadSlug",
|
||||
[
|
||||
validatedRequest,
|
||||
flexUserRoleValid([ROLES.all]),
|
||||
validWorkspaceAndThreadSlug,
|
||||
],
|
||||
async (_, response) => {
|
||||
try {
|
||||
const thread = response.locals.thread;
|
||||
await WorkspaceThread.delete({ id: thread.id });
|
||||
response.sendStatus(200).end();
|
||||
} catch (e) {
|
||||
console.log(e.message, e);
|
||||
response.sendStatus(500).end();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
app.get(
|
||||
"/workspace/:slug/thread/:threadSlug/chats",
|
||||
[
|
||||
validatedRequest,
|
||||
flexUserRoleValid([ROLES.all]),
|
||||
validWorkspaceAndThreadSlug,
|
||||
],
|
||||
async (request, response) => {
|
||||
try {
|
||||
const user = await userFromSession(request, response);
|
||||
const workspace = response.locals.workspace;
|
||||
const thread = response.locals.thread;
|
||||
const history = await WorkspaceChats.where(
|
||||
{
|
||||
workspaceId: workspace.id,
|
||||
user_id: user?.id || null,
|
||||
thread_id: thread.id,
|
||||
include: true,
|
||||
},
|
||||
null,
|
||||
{ id: "asc" }
|
||||
);
|
||||
|
||||
response.status(200).json({ history: convertToChatHistory(history) });
|
||||
} catch (e) {
|
||||
console.log(e.message, e);
|
||||
response.sendStatus(500).end();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
app.post(
|
||||
"/workspace/:slug/thread/:threadSlug/update",
|
||||
[
|
||||
validatedRequest,
|
||||
flexUserRoleValid([ROLES.all]),
|
||||
validWorkspaceAndThreadSlug,
|
||||
],
|
||||
async (request, response) => {
|
||||
try {
|
||||
const data = reqBody(request);
|
||||
const currentThread = response.locals.thread;
|
||||
const { thread, message } = await WorkspaceThread.update(
|
||||
currentThread,
|
||||
data
|
||||
);
|
||||
response.status(200).json({ thread, message });
|
||||
} catch (e) {
|
||||
console.log(e.message, e);
|
||||
response.sendStatus(500).end();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = { workspaceThreadEndpoints };
|
|
@ -19,6 +19,7 @@ const { utilEndpoints } = require("./endpoints/utils");
|
|||
const { developerEndpoints } = require("./endpoints/api");
|
||||
const { extensionEndpoints } = require("./endpoints/extensions");
|
||||
const { bootHTTP, bootSSL } = require("./utils/boot");
|
||||
const { workspaceThreadEndpoints } = require("./endpoints/workspaceThreads");
|
||||
const app = express();
|
||||
const apiRouter = express.Router();
|
||||
const FILE_LIMIT = "3GB";
|
||||
|
@ -37,6 +38,7 @@ app.use("/api", apiRouter);
|
|||
systemEndpoints(apiRouter);
|
||||
extensionEndpoints(apiRouter);
|
||||
workspaceEndpoints(apiRouter);
|
||||
workspaceThreadEndpoints(apiRouter);
|
||||
chatEndpoints(apiRouter);
|
||||
adminEndpoints(apiRouter);
|
||||
inviteEndpoints(apiRouter);
|
||||
|
|
|
@ -1,7 +1,13 @@
|
|||
const prisma = require("../utils/prisma");
|
||||
|
||||
const WorkspaceChats = {
|
||||
new: async function ({ workspaceId, prompt, response = {}, user = null }) {
|
||||
new: async function ({
|
||||
workspaceId,
|
||||
prompt,
|
||||
response = {},
|
||||
user = null,
|
||||
threadId = null,
|
||||
}) {
|
||||
try {
|
||||
const chat = await prisma.workspace_chats.create({
|
||||
data: {
|
||||
|
@ -9,6 +15,7 @@ const WorkspaceChats = {
|
|||
prompt,
|
||||
response: JSON.stringify(response),
|
||||
user_id: user?.id || null,
|
||||
thread_id: threadId,
|
||||
},
|
||||
});
|
||||
return { chat, message: null };
|
||||
|
@ -30,6 +37,7 @@ const WorkspaceChats = {
|
|||
where: {
|
||||
workspaceId,
|
||||
user_id: userId,
|
||||
thread_id: null, // this function is now only used for the default thread on workspaces and users
|
||||
include: true,
|
||||
},
|
||||
...(limit !== null ? { take: limit } : {}),
|
||||
|
@ -52,6 +60,7 @@ const WorkspaceChats = {
|
|||
const chats = await prisma.workspace_chats.findMany({
|
||||
where: {
|
||||
workspaceId,
|
||||
thread_id: null, // this function is now only used for the default thread on workspaces
|
||||
include: true,
|
||||
},
|
||||
...(limit !== null ? { take: limit } : {}),
|
||||
|
@ -82,6 +91,29 @@ const WorkspaceChats = {
|
|||
}
|
||||
},
|
||||
|
||||
markThreadHistoryInvalid: async function (
|
||||
workspaceId = null,
|
||||
user = null,
|
||||
threadId = null
|
||||
) {
|
||||
if (!workspaceId || !threadId) return;
|
||||
try {
|
||||
await prisma.workspace_chats.updateMany({
|
||||
where: {
|
||||
workspaceId,
|
||||
thread_id: threadId,
|
||||
user_id: user?.id,
|
||||
},
|
||||
data: {
|
||||
include: false,
|
||||
},
|
||||
});
|
||||
return;
|
||||
} catch (error) {
|
||||
console.error(error.message);
|
||||
}
|
||||
},
|
||||
|
||||
get: async function (clause = {}, limit = null, orderBy = null) {
|
||||
try {
|
||||
const chat = await prisma.workspace_chats.findFirst({
|
||||
|
|
86
server/models/workspaceThread.js
Normal file
86
server/models/workspaceThread.js
Normal file
|
@ -0,0 +1,86 @@
|
|||
const prisma = require("../utils/prisma");
|
||||
const { v4: uuidv4 } = require("uuid");
|
||||
|
||||
const WorkspaceThread = {
|
||||
writable: ["name"],
|
||||
|
||||
new: async function (workspace, userId = null) {
|
||||
try {
|
||||
const thread = await prisma.workspace_threads.create({
|
||||
data: {
|
||||
name: "New thread",
|
||||
slug: uuidv4(),
|
||||
user_id: userId ? Number(userId) : null,
|
||||
workspace_id: workspace.id,
|
||||
},
|
||||
});
|
||||
|
||||
return { thread, message: null };
|
||||
} catch (error) {
|
||||
console.error(error.message);
|
||||
return { thread: null, message: error.message };
|
||||
}
|
||||
},
|
||||
|
||||
update: async function (prevThread = null, data = {}) {
|
||||
if (!prevThread) throw new Error("No thread id provided for update");
|
||||
|
||||
const validKeys = Object.keys(data).filter((key) =>
|
||||
this.writable.includes(key)
|
||||
);
|
||||
if (validKeys.length === 0)
|
||||
return { thread: prevThread, message: "No valid fields to update!" };
|
||||
|
||||
try {
|
||||
const thread = await prisma.workspace_threads.update({
|
||||
where: { id: prevThread.id },
|
||||
data,
|
||||
});
|
||||
return { thread, message: null };
|
||||
} catch (error) {
|
||||
console.error(error.message);
|
||||
return { thread: null, message: error.message };
|
||||
}
|
||||
},
|
||||
|
||||
get: async function (clause = {}) {
|
||||
try {
|
||||
const thread = await prisma.workspace_threads.findFirst({
|
||||
where: clause,
|
||||
});
|
||||
|
||||
return thread || null;
|
||||
} catch (error) {
|
||||
console.error(error.message);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
delete: async function (clause = {}) {
|
||||
try {
|
||||
await prisma.workspace_threads.delete({
|
||||
where: clause,
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(error.message);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
where: async function (clause = {}, limit = null, orderBy = null) {
|
||||
try {
|
||||
const results = await prisma.workspace_threads.findMany({
|
||||
where: clause,
|
||||
...(limit !== null ? { take: limit } : {}),
|
||||
...(orderBy !== null ? { orderBy } : {}),
|
||||
});
|
||||
return results;
|
||||
} catch (error) {
|
||||
console.error(error.message);
|
||||
return [];
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = { WorkspaceThread };
|
24
server/prisma/migrations/20240208224848_init/migration.sql
Normal file
24
server/prisma/migrations/20240208224848_init/migration.sql
Normal file
|
@ -0,0 +1,24 @@
|
|||
-- AlterTable
|
||||
ALTER TABLE "workspace_chats" ADD COLUMN "thread_id" INTEGER;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "workspace_threads" (
|
||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"name" TEXT NOT NULL,
|
||||
"slug" TEXT NOT NULL,
|
||||
"workspace_id" INTEGER NOT NULL,
|
||||
"user_id" INTEGER,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"lastUpdatedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT "workspace_threads_workspace_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "workspaces" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
CONSTRAINT "workspace_threads_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "workspace_threads_slug_key" ON "workspace_threads"("slug");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "workspace_threads_workspace_id_idx" ON "workspace_threads"("workspace_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "workspace_threads_user_id_idx" ON "workspace_threads"("user_id");
|
|
@ -54,18 +54,19 @@ model system_settings {
|
|||
}
|
||||
|
||||
model users {
|
||||
id Int @id @default(autoincrement())
|
||||
username String? @unique
|
||||
id Int @id @default(autoincrement())
|
||||
username String? @unique
|
||||
password String
|
||||
pfpFilename String?
|
||||
role String @default("default")
|
||||
suspended Int @default(0)
|
||||
createdAt DateTime @default(now())
|
||||
lastUpdatedAt DateTime @default(now())
|
||||
role String @default("default")
|
||||
suspended Int @default(0)
|
||||
createdAt DateTime @default(now())
|
||||
lastUpdatedAt DateTime @default(now())
|
||||
workspace_chats workspace_chats[]
|
||||
workspace_users workspace_users[]
|
||||
embed_configs embed_configs[]
|
||||
embed_chats embed_chats[]
|
||||
threads workspace_threads[]
|
||||
}
|
||||
|
||||
model document_vectors {
|
||||
|
@ -101,6 +102,22 @@ model workspaces {
|
|||
documents workspace_documents[]
|
||||
workspace_suggested_messages workspace_suggested_messages[]
|
||||
embed_configs embed_configs[]
|
||||
threads workspace_threads[]
|
||||
}
|
||||
|
||||
model workspace_threads {
|
||||
id Int @id @default(autoincrement())
|
||||
name String
|
||||
slug String @unique
|
||||
workspace_id Int
|
||||
user_id Int?
|
||||
createdAt DateTime @default(now())
|
||||
lastUpdatedAt DateTime @default(now())
|
||||
workspace workspaces @relation(fields: [workspace_id], references: [id], onDelete: Cascade)
|
||||
user users? @relation(fields: [user_id], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([workspace_id])
|
||||
@@index([user_id])
|
||||
}
|
||||
|
||||
model workspace_suggested_messages {
|
||||
|
@ -122,6 +139,7 @@ model workspace_chats {
|
|||
response String
|
||||
include Boolean @default(true)
|
||||
user_id Int?
|
||||
thread_id Int? // No relation to prevent whole table migration
|
||||
createdAt DateTime @default(now())
|
||||
lastUpdatedAt DateTime @default(now())
|
||||
users users? @relation(fields: [user_id], references: [id], onDelete: Cascade, onUpdate: Cascade)
|
||||
|
|
|
@ -1,7 +1,21 @@
|
|||
const { WorkspaceChats } = require("../../../models/workspaceChats");
|
||||
|
||||
async function resetMemory(workspace, _message, msgUUID, user = null) {
|
||||
await WorkspaceChats.markHistoryInvalid(workspace.id, user);
|
||||
async function resetMemory(
|
||||
workspace,
|
||||
_message,
|
||||
msgUUID,
|
||||
user = null,
|
||||
thread = null
|
||||
) {
|
||||
// If thread is present we are wanting to reset this specific thread. Not the whole workspace.
|
||||
thread
|
||||
? await WorkspaceChats.markThreadHistoryInvalid(
|
||||
workspace.id,
|
||||
user,
|
||||
thread.id
|
||||
)
|
||||
: await WorkspaceChats.markHistoryInvalid(workspace.id, user);
|
||||
|
||||
return {
|
||||
uuid: msgUUID,
|
||||
type: "textResponse",
|
||||
|
|
|
@ -204,6 +204,8 @@ async function chatWithWorkspace(
|
|||
|
||||
// On query we dont return message history. All other chat modes and when chatting
|
||||
// with no embeddings we return history.
|
||||
// TODO: Refactor to just run a .where on WorkspaceChat to simplify what is going on here.
|
||||
// see recentThreadChatHistory
|
||||
async function recentChatHistory(
|
||||
user = null,
|
||||
workspace,
|
||||
|
@ -226,6 +228,30 @@ async function recentChatHistory(
|
|||
return { rawHistory, chatHistory: convertToPromptHistory(rawHistory) };
|
||||
}
|
||||
|
||||
// Extension of recentChatHistory that supports threads
|
||||
async function recentThreadChatHistory(
|
||||
user = null,
|
||||
workspace,
|
||||
thread,
|
||||
messageLimit = 20,
|
||||
chatMode = null
|
||||
) {
|
||||
if (chatMode === "query") return [];
|
||||
const rawHistory = (
|
||||
await WorkspaceChats.where(
|
||||
{
|
||||
workspaceId: workspace.id,
|
||||
user_id: user?.id || null,
|
||||
thread_id: thread?.id || null,
|
||||
include: true,
|
||||
},
|
||||
messageLimit,
|
||||
{ id: "desc" }
|
||||
)
|
||||
).reverse();
|
||||
return { rawHistory, chatHistory: convertToPromptHistory(rawHistory) };
|
||||
}
|
||||
|
||||
async function emptyEmbeddingChat({
|
||||
uuid,
|
||||
user,
|
||||
|
@ -270,6 +296,7 @@ function chatPrompt(workspace) {
|
|||
|
||||
module.exports = {
|
||||
recentChatHistory,
|
||||
recentThreadChatHistory,
|
||||
convertToPromptHistory,
|
||||
convertToChatHistory,
|
||||
chatWithWorkspace,
|
||||
|
|
|
@ -6,6 +6,7 @@ const {
|
|||
recentChatHistory,
|
||||
VALID_COMMANDS,
|
||||
chatPrompt,
|
||||
recentThreadChatHistory,
|
||||
} = require(".");
|
||||
|
||||
const VALID_CHAT_MODE = ["chat", "query"];
|
||||
|
@ -19,13 +20,20 @@ async function streamChatWithWorkspace(
|
|||
workspace,
|
||||
message,
|
||||
chatMode = "chat",
|
||||
user = null
|
||||
user = null,
|
||||
thread = null
|
||||
) {
|
||||
const uuid = uuidv4();
|
||||
const command = grepCommand(message);
|
||||
|
||||
if (!!command && Object.keys(VALID_COMMANDS).includes(command)) {
|
||||
const data = await VALID_COMMANDS[command](workspace, message, uuid, user);
|
||||
const data = await VALID_COMMANDS[command](
|
||||
workspace,
|
||||
message,
|
||||
uuid,
|
||||
user,
|
||||
thread
|
||||
);
|
||||
writeResponseChunk(response, data);
|
||||
return;
|
||||
}
|
||||
|
@ -65,6 +73,8 @@ async function streamChatWithWorkspace(
|
|||
}
|
||||
|
||||
// If there are no embeddings - chat like a normal LLM chat interface.
|
||||
// no need to pass in chat mode - because if we are here we are in
|
||||
// "chat" mode + have embeddings.
|
||||
return await streamEmptyEmbeddingChat({
|
||||
response,
|
||||
uuid,
|
||||
|
@ -73,16 +83,21 @@ async function streamChatWithWorkspace(
|
|||
workspace,
|
||||
messageLimit,
|
||||
LLMConnector,
|
||||
thread,
|
||||
});
|
||||
}
|
||||
|
||||
let completeText;
|
||||
const { rawHistory, chatHistory } = await recentChatHistory(
|
||||
user,
|
||||
workspace,
|
||||
messageLimit,
|
||||
chatMode
|
||||
);
|
||||
const { rawHistory, chatHistory } = thread
|
||||
? await recentThreadChatHistory(
|
||||
user,
|
||||
workspace,
|
||||
thread,
|
||||
messageLimit,
|
||||
chatMode
|
||||
)
|
||||
: await recentChatHistory(user, workspace, messageLimit, chatMode);
|
||||
|
||||
const {
|
||||
contextTexts = [],
|
||||
sources = [],
|
||||
|
@ -167,6 +182,7 @@ async function streamChatWithWorkspace(
|
|||
prompt: message,
|
||||
response: { text: completeText, sources, type: chatMode },
|
||||
user,
|
||||
threadId: thread?.id,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
@ -179,13 +195,12 @@ async function streamEmptyEmbeddingChat({
|
|||
workspace,
|
||||
messageLimit,
|
||||
LLMConnector,
|
||||
thread = null,
|
||||
}) {
|
||||
let completeText;
|
||||
const { rawHistory, chatHistory } = await recentChatHistory(
|
||||
user,
|
||||
workspace,
|
||||
messageLimit
|
||||
);
|
||||
const { rawHistory, chatHistory } = thread
|
||||
? await recentThreadChatHistory(user, workspace, thread, messageLimit)
|
||||
: await recentChatHistory(user, workspace, messageLimit);
|
||||
|
||||
// If streaming is not explicitly enabled for connector
|
||||
// we do regular waiting of a response and send a single chunk.
|
||||
|
@ -225,6 +240,7 @@ async function streamEmptyEmbeddingChat({
|
|||
prompt: message,
|
||||
response: { text: completeText, sources: [], type: "chat" },
|
||||
user,
|
||||
threadId: thread?.id,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
|
52
server/utils/middleware/validWorkspace.js
Normal file
52
server/utils/middleware/validWorkspace.js
Normal file
|
@ -0,0 +1,52 @@
|
|||
const { Workspace } = require("../../models/workspace");
|
||||
const { WorkspaceThread } = require("../../models/workspaceThread");
|
||||
const { userFromSession, multiUserMode } = require("../http");
|
||||
|
||||
// Will pre-validate and set the workspace for a request if the slug is provided in the URL path.
|
||||
async function validWorkspaceSlug(request, response, next) {
|
||||
const { slug } = request.params;
|
||||
const user = await userFromSession(request, response);
|
||||
const workspace = multiUserMode(response)
|
||||
? await Workspace.getWithUser(user, { slug })
|
||||
: await Workspace.get({ slug });
|
||||
|
||||
if (!workspace) {
|
||||
response.status(404).send("Workspace does not exist.");
|
||||
return;
|
||||
}
|
||||
|
||||
response.locals.workspace = workspace;
|
||||
next();
|
||||
}
|
||||
|
||||
// Will pre-validate and set the workspace AND a thread for a request if the slugs are provided in the URL path.
|
||||
async function validWorkspaceAndThreadSlug(request, response, next) {
|
||||
const { slug, threadSlug } = request.params;
|
||||
const user = await userFromSession(request, response);
|
||||
const workspace = multiUserMode(response)
|
||||
? await Workspace.getWithUser(user, { slug })
|
||||
: await Workspace.get({ slug });
|
||||
|
||||
if (!workspace) {
|
||||
response.status(404).send("Workspace does not exist.");
|
||||
return;
|
||||
}
|
||||
|
||||
const thread = await WorkspaceThread.get({
|
||||
slug: threadSlug,
|
||||
user_id: user?.id || null,
|
||||
});
|
||||
if (!thread) {
|
||||
response.status(404).send("Workspace thread does not exist.");
|
||||
return;
|
||||
}
|
||||
|
||||
response.locals.workspace = workspace;
|
||||
response.locals.thread = thread;
|
||||
next();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
validWorkspaceSlug,
|
||||
validWorkspaceAndThreadSlug,
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue