[FEAT] Slash templates ()

* WIP slash presets

* WIP slash command customization CRUD + validations complete

* backend slash command support

* fix permission setting on new slash commands
rework form submit and pattern on frontend

* Add field updates for hooks,
required=true to field
add user<>command constraint to keep them unique
enforce uniquness via teritary uid field on table for multi and non-multi user

* reset migration

---------

Co-authored-by: timothycarambat <rambat1010@gmail.com>
This commit is contained in:
Sean Hatfield 2024-05-10 12:35:33 -07:00 committed by GitHub
parent 6eefd0d280
commit d36c3ff8b2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 722 additions and 11 deletions
frontend/src
components/WorkspaceChat/ChatContainer/PromptInput/SlashCommands
models
server
endpoints
models
prisma
migrations/20240510032311_init
schema.prisma
utils/chats

View file

@ -0,0 +1,111 @@
import { useState } from "react";
import { X } from "@phosphor-icons/react";
import ModalWrapper from "@/components/ModalWrapper";
import { CMD_REGEX } from ".";
export default function AddPresetModal({ isOpen, onClose, onSave }) {
const [command, setCommand] = useState("");
const handleSubmit = async (e) => {
e.preventDefault();
const form = new FormData(e.target);
const sanitizedCommand = command.replace(CMD_REGEX, "");
const saved = await onSave({
command: `/${sanitizedCommand}`,
prompt: form.get("prompt"),
description: form.get("description"),
});
if (saved) setCommand("");
};
const handleCommandChange = (e) => {
const value = e.target.value.replace(CMD_REGEX, "");
setCommand(value);
};
return (
<ModalWrapper isOpen={isOpen}>
<form
onSubmit={handleSubmit}
className="relative w-full max-w-2xl max-h-full"
>
<div className="relative bg-main-gradient rounded-lg shadow">
<div className="flex items-start justify-between p-4 border-b rounded-t border-gray-500/50">
<h3 className="text-xl font-semibold text-white">Add New Preset</h3>
<button
onClick={onClose}
type="button"
className="transition-all duration-300 text-gray-400 bg-transparent hover:border-white/60 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border"
>
<X className="text-gray-300 text-lg" />
</button>
</div>
<div className="p-6 space-y-6 flex h-full w-full">
<div className="w-full flex flex-col gap-y-4">
<div>
<label className="block mb-2 text-sm font-medium text-white">
Command
</label>
<div className="flex items-center">
<span className="text-white text-sm mr-2 font-bold">/</span>
<input
name="command"
type="text"
placeholder="your-command"
value={command}
onChange={handleCommandChange}
maxLength={25}
autoComplete="off"
required={true}
className="border-none bg-zinc-900 placeholder:text-white/20 border-gray-500 text-white text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
/>
</div>
</div>
<div>
<label className="block mb-2 text-sm font-medium text-white">
Prompt
</label>
<textarea
name="prompt"
autoComplete="off"
placeholder="This is the content that will be injected in front of your prompt."
required={true}
className="border-none bg-zinc-900 placeholder:text-white/20 border-gray-500 text-white text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
></textarea>
</div>
<div>
<label className="border-none block mb-2 text-sm font-medium text-white">
Description
</label>
<input
type="text"
name="description"
placeholder="Responds with a poem about LLMs."
maxLength={80}
autoComplete="off"
required={true}
className="border-none bg-zinc-900 placeholder:text-white/20 border-gray-500 text-white text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
/>
</div>
</div>
</div>
<div className="flex w-full justify-between items-center p-6 space-x-2 border-t rounded-b border-gray-500/50">
<button
onClick={onClose}
type="button"
className="px-4 py-2 rounded-lg text-white hover:bg-stone-900 transition-all duration-300"
>
Cancel
</button>
<button
type="submit"
className="transition-all duration-300 border border-slate-200 px-4 py-2 rounded-lg text-white text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 focus:ring-gray-800"
>
Save
</button>
</div>
</div>
</form>
</ModalWrapper>
);
}

View file

@ -0,0 +1,148 @@
import { useState } from "react";
import { X } from "@phosphor-icons/react";
import ModalWrapper from "@/components/ModalWrapper";
import { CMD_REGEX } from ".";
export default function EditPresetModal({
isOpen,
onClose,
onSave,
onDelete,
preset,
}) {
const [command, setCommand] = useState(preset?.command?.slice(1) || "");
const [deleting, setDeleting] = useState(false);
const handleSubmit = (e) => {
e.preventDefault();
const form = new FormData(e.target);
const sanitizedCommand = command.replace(CMD_REGEX, "");
onSave({
id: preset.id,
command: `/${sanitizedCommand}`,
prompt: form.get("prompt"),
description: form.get("description"),
});
};
const handleCommandChange = (e) => {
const value = e.target.value.replace(CMD_REGEX, "");
setCommand(value);
};
const handleDelete = async () => {
const confirmDelete = window.confirm(
"Are you sure you want to delete this preset?"
);
if (!confirmDelete) return;
setDeleting(true);
await onDelete(preset.id);
setDeleting(false);
onClose();
};
return (
<ModalWrapper isOpen={isOpen}>
<form
onSubmit={handleSubmit}
className="relative w-full max-w-2xl max-h-full"
>
<div className="relative bg-main-gradient rounded-lg shadow">
<div className="flex items-start justify-between p-4 border-b rounded-t border-gray-500/50">
<h3 className="text-xl font-semibold text-white">Edit Preset</h3>
<button
onClick={onClose}
type="button"
className="transition-all duration-300 text-gray-400 bg-transparent hover:border-white/60 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border"
>
<X className="text-gray-300 text-lg" />
</button>
</div>
<div className="p-6 space-y-6 flex h-full w-full">
<div className="w-full flex flex-col gap-y-4">
<div>
<label
htmlFor="command"
className="block mb-2 text-sm font-medium text-white"
>
Command
</label>
<div className="flex items-center">
<span className="text-white text-sm mr-2 font-bold">/</span>
<input
type="text"
name="command"
placeholder="your-command"
value={command}
onChange={handleCommandChange}
required={true}
className="border-none bg-zinc-900 placeholder:text-white/20 border-gray-500 text-white text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
/>
</div>
</div>
<div>
<label
htmlFor="prompt"
className="block mb-2 text-sm font-medium text-white"
>
Prompt
</label>
<textarea
name="prompt"
placeholder="This is a test prompt. Please respond with a poem about LLMs."
defaultValue={preset.prompt}
required={true}
className="border-none bg-zinc-900 placeholder:text-white/20 border-gray-500 text-white text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
></textarea>
</div>
<div>
<label
htmlFor="description"
className="block mb-2 text-sm font-medium text-white"
>
Description
</label>
<input
type="text"
name="description"
defaultValue={preset.description}
placeholder="Responds with a poem about LLMs."
required={true}
className="border-none bg-zinc-900 placeholder:text-white/20 border-gray-500 text-white text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
/>
</div>
</div>
</div>
<div className="flex w-full justify-between items-center p-6 space-x-2 border-t rounded-b border-gray-500/50">
<div className="flex flex-col space-y-2">
<button
disabled={deleting}
onClick={handleDelete}
type="button"
className="px-4 py-2 rounded-lg text-red-500 hover:bg-red-500/25 transition-all duration-300 disabled:opacity-50"
>
{deleting ? "Deleting..." : "Delete Preset"}
</button>
</div>
<div className="flex space-x-2">
<button
onClick={onClose}
type="button"
className="px-4 py-2 rounded-lg text-white hover:bg-stone-900 transition-all duration-300"
>
Cancel
</button>
<button
type="submit"
className="transition-all duration-300 border border-slate-200 px-4 py-2 rounded-lg text-white text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 focus:ring-gray-800"
>
Save
</button>
</div>
</div>
</div>
</form>
</ModalWrapper>
);
}

View file

@ -0,0 +1,127 @@
import { useEffect, useState } from "react";
import { useIsAgentSessionActive } from "@/utils/chat/agent";
import AddPresetModal from "./AddPresetModal";
import EditPresetModal from "./EditPresetModal";
import { useModal } from "@/hooks/useModal";
import System from "@/models/system";
import { DotsThree, Plus } from "@phosphor-icons/react";
import showToast from "@/utils/toast";
export const CMD_REGEX = new RegExp(/[^a-zA-Z0-9_-]/g);
export default function SlashPresets({ setShowing, sendCommand }) {
const isActiveAgentSession = useIsAgentSessionActive();
const {
isOpen: isAddModalOpen,
openModal: openAddModal,
closeModal: closeAddModal,
} = useModal();
const {
isOpen: isEditModalOpen,
openModal: openEditModal,
closeModal: closeEditModal,
} = useModal();
const [presets, setPresets] = useState([]);
const [selectedPreset, setSelectedPreset] = useState(null);
useEffect(() => {
fetchPresets();
}, []);
if (isActiveAgentSession) return null;
const fetchPresets = async () => {
const presets = await System.getSlashCommandPresets();
setPresets(presets);
};
const handleSavePreset = async (preset) => {
const { error } = await System.createSlashCommandPreset(preset);
if (!!error) {
showToast(error, "error");
return false;
}
fetchPresets();
closeAddModal();
return true;
};
const handleEditPreset = (preset) => {
setSelectedPreset(preset);
openEditModal();
};
const handleUpdatePreset = async (updatedPreset) => {
const { error } = await System.updateSlashCommandPreset(
updatedPreset.id,
updatedPreset
);
if (!!error) {
showToast(error, "error");
return;
}
fetchPresets();
closeEditModal();
};
const handleDeletePreset = async (presetId) => {
await System.deleteSlashCommandPreset(presetId);
fetchPresets();
closeEditModal();
};
return (
<>
{presets.map((preset) => (
<button
key={preset.id}
onClick={() => {
setShowing(false);
sendCommand(`${preset.command} `, false);
}}
className="w-full hover:cursor-pointer hover:bg-zinc-700 px-2 py-2 rounded-xl flex flex-row justify-start"
>
<div className="w-full flex-col text-left flex pointer-events-none">
<div className="text-white text-sm font-bold">{preset.command}</div>
<div className="text-white text-opacity-60 text-sm">
{preset.description}
</div>
</div>
<button
onClick={(e) => {
e.stopPropagation();
handleEditPreset(preset);
}}
className="text-white text-sm p-1 hover:cursor-pointer hover:bg-zinc-900 rounded-full mt-1"
>
<DotsThree size={24} weight="bold" />
</button>
</button>
))}
<button
onClick={openAddModal}
className="w-full hover:cursor-pointer hover:bg-zinc-700 px-2 py-1 rounded-xl flex flex-col justify-start"
>
<div className="w-full flex-row flex pointer-events-none items-center gap-2">
<Plus size={24} weight="fill" fill="white" />
<div className="text-white text-sm font-medium">Add New Preset </div>
</div>
</button>
<AddPresetModal
isOpen={isAddModalOpen}
onClose={closeAddModal}
onSave={handleSavePreset}
/>
{selectedPreset && (
<EditPresetModal
isOpen={isEditModalOpen}
onClose={closeEditModal}
onSave={handleUpdatePreset}
onDelete={handleDeletePreset}
preset={selectedPreset}
/>
)}
</>
);
}

View file

@ -3,6 +3,7 @@ import SlashCommandIcon from "./icons/slash-commands-icon.svg";
import { Tooltip } from "react-tooltip";
import ResetCommand from "./reset";
import EndAgentSession from "./endAgentSession";
import SlashPresets from "./SlashPresets";
export default function SlashCommandsButton({ showing, setShowSlashCommand }) {
return (
@ -52,10 +53,11 @@ export function SlashCommands({ showing, setShowing, sendCommand }) {
<div className="w-full flex justify-center absolute bottom-[130px] md:bottom-[150px] left-0 z-10 px-4">
<div
ref={cmdRef}
className="w-[600px] p-2 bg-zinc-800 rounded-2xl shadow flex-col justify-center items-start gap-2.5 inline-flex"
className="w-[600px] overflow-auto p-2 bg-zinc-800 rounded-2xl shadow flex-col justify-center items-start gap-2.5 inline-flex"
>
<ResetCommand sendCommand={sendCommand} setShowing={setShowing} />
<EndAgentSession sendCommand={sendCommand} setShowing={setShowing} />
<SlashPresets sendCommand={sendCommand} setShowing={setShowing} />
</div>
</div>
</div>

View file

@ -567,6 +567,74 @@ const System = {
});
},
dataConnectors: DataConnector,
getSlashCommandPresets: async function () {
return await fetch(`${API_BASE}/system/slash-command-presets`, {
method: "GET",
headers: baseHeaders(),
})
.then((res) => {
if (!res.ok) throw new Error("Could not fetch slash command presets.");
return res.json();
})
.then((res) => res.presets)
.catch((e) => {
console.error(e);
return [];
});
},
createSlashCommandPreset: async function (presetData) {
return await fetch(`${API_BASE}/system/slash-command-presets`, {
method: "POST",
headers: baseHeaders(),
body: JSON.stringify(presetData),
})
.then((res) => {
if (!res.ok) throw new Error("Could not create slash command preset.");
return res.json();
})
.then((res) => {
return { preset: res.preset, error: null };
})
.catch((e) => {
console.error(e);
return { preset: null, error: e.message };
});
},
updateSlashCommandPreset: async function (presetId, presetData) {
return await fetch(`${API_BASE}/system/slash-command-presets/${presetId}`, {
method: "POST",
headers: baseHeaders(),
body: JSON.stringify(presetData),
})
.then((res) => {
if (!res.ok) throw new Error("Could not update slash command preset.");
return res.json();
})
.then((res) => {
return { preset: res.preset, error: null };
})
.catch((e) => {
return { preset: null, error: "Failed to update this command." };
});
},
deleteSlashCommandPreset: async function (presetId) {
return await fetch(`${API_BASE}/system/slash-command-presets/${presetId}`, {
method: "DELETE",
headers: baseHeaders(),
})
.then((res) => {
if (!res.ok) throw new Error("Could not delete slash command preset.");
return true;
})
.catch((e) => {
console.error(e);
return false;
});
},
};
export default System;

View file

@ -50,6 +50,7 @@ const {
resetPassword,
generateRecoveryCodes,
} = require("../utils/PasswordRecovery");
const { SlashCommandPresets } = require("../models/slashCommandsPresets");
function systemEndpoints(app) {
if (!app) return;
@ -1044,6 +1045,111 @@ function systemEndpoints(app) {
response.sendStatus(500).end();
}
});
app.get(
"/system/slash-command-presets",
[validatedRequest, flexUserRoleValid([ROLES.all])],
async (request, response) => {
try {
const user = await userFromSession(request, response);
const userPresets = await SlashCommandPresets.getUserPresets(user?.id);
response.status(200).json({ presets: userPresets });
} catch (error) {
console.error("Error fetching slash command presets:", error);
response.status(500).json({ message: "Internal server error" });
}
}
);
app.post(
"/system/slash-command-presets",
[validatedRequest, flexUserRoleValid([ROLES.all])],
async (request, response) => {
try {
const user = await userFromSession(request, response);
const { command, prompt, description } = reqBody(request);
const presetData = {
command: SlashCommandPresets.formatCommand(String(command)),
prompt: String(prompt),
description: String(description),
};
const preset = await SlashCommandPresets.create(user?.id, presetData);
if (!preset) {
return response
.status(500)
.json({ message: "Failed to create preset" });
}
response.status(201).json({ preset });
} catch (error) {
console.error("Error creating slash command preset:", error);
response.status(500).json({ message: "Internal server error" });
}
}
);
app.post(
"/system/slash-command-presets/:slashCommandId",
[validatedRequest, flexUserRoleValid([ROLES.all])],
async (request, response) => {
try {
const user = await userFromSession(request, response);
const { slashCommandId } = request.params;
const { command, prompt, description } = reqBody(request);
// Valid user running owns the preset if user session is valid.
const ownsPreset = await SlashCommandPresets.get({
userId: user?.id ?? null,
id: Number(slashCommandId),
});
if (!ownsPreset)
return response.status(404).json({ message: "Preset not found" });
const updates = {
command: SlashCommandPresets.formatCommand(String(command)),
prompt: String(prompt),
description: String(description),
};
const preset = await SlashCommandPresets.update(
Number(slashCommandId),
updates
);
if (!preset) return response.sendStatus(422);
response.status(200).json({ preset: { ...ownsPreset, ...updates } });
} catch (error) {
console.error("Error updating slash command preset:", error);
response.status(500).json({ message: "Internal server error" });
}
}
);
app.delete(
"/system/slash-command-presets/:slashCommandId",
[validatedRequest, flexUserRoleValid([ROLES.all])],
async (request, response) => {
try {
const { slashCommandId } = request.params;
const user = await userFromSession(request, response);
// Valid user running owns the preset if user session is valid.
const ownsPreset = await SlashCommandPresets.get({
userId: user?.id ?? null,
id: Number(slashCommandId),
});
if (!ownsPreset)
return response
.status(403)
.json({ message: "Failed to delete preset" });
await SlashCommandPresets.delete(Number(slashCommandId));
response.sendStatus(204);
} catch (error) {
console.error("Error deleting slash command preset:", error);
response.status(500).json({ message: "Internal server error" });
}
}
);
}
module.exports = { systemEndpoints };

View file

@ -0,0 +1,105 @@
const { v4 } = require("uuid");
const prisma = require("../utils/prisma");
const CMD_REGEX = new RegExp(/[^a-zA-Z0-9_-]/g);
const SlashCommandPresets = {
formatCommand: function (command = "") {
if (!command || command.length < 2) return `/${v4().split("-")[0]}`;
let adjustedCmd = command.toLowerCase(); // force lowercase
if (!adjustedCmd.startsWith("/")) adjustedCmd = `/${adjustedCmd}`; // Fix if no preceding / is found.
return `/${adjustedCmd.slice(1).toLowerCase().replace(CMD_REGEX, "-")}`; // replace any invalid chars with '-'
},
get: async function (clause = {}) {
try {
const preset = await prisma.slash_command_presets.findFirst({
where: clause,
});
return preset || null;
} catch (error) {
console.error(error.message);
return null;
}
},
where: async function (clause = {}, limit) {
try {
const presets = await prisma.slash_command_presets.findMany({
where: clause,
take: limit || undefined,
});
return presets;
} catch (error) {
console.error(error.message);
return [];
}
},
// Command + userId must be unique combination.
create: async function (userId = null, presetData = {}) {
try {
const preset = await prisma.slash_command_presets.create({
data: {
...presetData,
// This field (uid) is either the user_id or 0 (for non-multi-user mode).
// the UID field enforces the @@unique(userId, command) constraint since
// the real relational field (userId) cannot be non-null so this 'dummy' field gives us something
// to constrain against within the context of prisma and sqlite that works.
uid: userId ? Number(userId) : 0,
userId: userId ? Number(userId) : null,
},
});
return preset;
} catch (error) {
console.error("Failed to create preset", error.message);
return null;
}
},
getUserPresets: async function (userId = null) {
try {
return (
await prisma.slash_command_presets.findMany({
where: { userId: !!userId ? Number(userId) : null },
orderBy: { createdAt: "asc" },
})
)?.map((preset) => ({
id: preset.id,
command: preset.command,
prompt: preset.prompt,
description: preset.description,
}));
} catch (error) {
console.error("Failed to get user presets", error.message);
return [];
}
},
update: async function (presetId = null, presetData = {}) {
try {
const preset = await prisma.slash_command_presets.update({
where: { id: Number(presetId) },
data: presetData,
});
return preset;
} catch (error) {
console.error("Failed to update preset", error.message);
return null;
}
},
delete: async function (presetId = null) {
try {
await prisma.slash_command_presets.delete({
where: { id: Number(presetId) },
});
return true;
} catch (error) {
console.error("Failed to delete preset", error.message);
return false;
}
},
};
module.exports.SlashCommandPresets = SlashCommandPresets;

View file

@ -0,0 +1,15 @@
-- CreateTable
CREATE TABLE "slash_command_presets" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"command" TEXT NOT NULL,
"prompt" TEXT NOT NULL,
"description" TEXT NOT NULL,
"uid" INTEGER NOT NULL DEFAULT 0,
"userId" INTEGER,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"lastUpdatedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "slash_command_presets_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
-- CreateIndex
CREATE UNIQUE INDEX "slash_command_presets_uid_command_key" ON "slash_command_presets"("uid", "command");

View file

@ -73,6 +73,7 @@ model users {
recovery_codes recovery_codes[]
password_reset_tokens password_reset_tokens[]
workspace_agent_invocations workspace_agent_invocations[]
slash_command_presets slash_command_presets[]
}
model recovery_codes {
@ -260,3 +261,17 @@ model event_logs {
@@index([event])
}
model slash_command_presets {
id Int @id @default(autoincrement())
command String
prompt String
description String
uid Int @default(0) // 0 is null user
userId Int?
createdAt DateTime @default(now())
lastUpdatedAt DateTime @default(now())
user users? @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([uid, command])
}

View file

@ -4,14 +4,28 @@ const { resetMemory } = require("./commands/reset");
const { getVectorDbClass, getLLMProvider } = require("../helpers");
const { convertToPromptHistory } = require("../helpers/chat/responses");
const { DocumentManager } = require("../DocumentManager");
const { SlashCommandPresets } = require("../../models/slashCommandsPresets");
const VALID_COMMANDS = {
"/reset": resetMemory,
};
function grepCommand(message) {
async function grepCommand(message, user = null) {
const userPresets = await SlashCommandPresets.getUserPresets(user?.id);
const availableCommands = Object.keys(VALID_COMMANDS);
// Check if the message starts with any preset command
const foundPreset = userPresets.find((p) => message.startsWith(p.command));
if (!!foundPreset) {
// Replace the preset command with the corresponding prompt
const updatedMessage = message.replace(
foundPreset.command,
foundPreset.prompt
);
return updatedMessage;
}
// Check if the message starts with any built-in command
for (let i = 0; i < availableCommands.length; i++) {
const cmd = availableCommands[i];
const re = new RegExp(`^(${cmd})`, "i");
@ -20,7 +34,7 @@ function grepCommand(message) {
}
}
return null;
return message;
}
async function chatWithWorkspace(
@ -31,10 +45,10 @@ async function chatWithWorkspace(
thread = null
) {
const uuid = uuidv4();
const command = grepCommand(message);
const updatedMessage = await grepCommand(message, user);
if (!!command && Object.keys(VALID_COMMANDS).includes(command)) {
return await VALID_COMMANDS[command](workspace, message, uuid, user);
if (Object.keys(VALID_COMMANDS).includes(updatedMessage)) {
return await VALID_COMMANDS[updatedMessage](workspace, message, uuid, user);
}
const LLMConnector = getLLMProvider({
@ -164,7 +178,7 @@ async function chatWithWorkspace(
const messages = await LLMConnector.compressMessages(
{
systemPrompt: chatPrompt(workspace),
userPrompt: message,
userPrompt: updatedMessage,
contextTexts,
chatHistory,
},

View file

@ -23,10 +23,10 @@ async function streamChatWithWorkspace(
thread = null
) {
const uuid = uuidv4();
const command = grepCommand(message);
const updatedMessage = await grepCommand(message, user);
if (!!command && Object.keys(VALID_COMMANDS).includes(command)) {
const data = await VALID_COMMANDS[command](
if (Object.keys(VALID_COMMANDS).includes(updatedMessage)) {
const data = await VALID_COMMANDS[updatedMessage](
workspace,
message,
uuid,
@ -185,7 +185,7 @@ async function streamChatWithWorkspace(
const messages = await LLMConnector.compressMessages(
{
systemPrompt: chatPrompt(workspace),
userPrompt: message,
userPrompt: updatedMessage,
contextTexts,
chatHistory,
},