Prompt variables (#3359)
* wip prompt variables * refactor backend + add popup suggestions menu to frontend * use processString to replace all variables in system prompts * update translations * fix translations * wip highlight variables * revert accidental name change * rename everything, remove translations * Update prompt var UI and backend logic * Update form handler linting * linting * normalize all translation files for prompt variables * prompt vars dev image --------- Co-authored-by: Timothy Carambat <rambat1010@gmail.com>
This commit is contained in:
parent
9cc1b26af2
commit
f3ea21bcd1
2
.github/workflows/dev-build.yaml
vendored
2
.github/workflows/dev-build.yaml
vendored
@ -6,7 +6,7 @@ concurrency:
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ['chore-bump-lancedb'] # put your current branch to create a build. Core team only.
|
||||
branches: ['3157-feat-prompt-variables'] # put your current branch to create a build. Core team only.
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
- 'cloud-deployments/*'
|
||||
|
||||
@ -33,6 +33,7 @@
|
||||
"react-device-detect": "^2.2.2",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-dropzone": "^14.2.3",
|
||||
"react-highlight-words": "^0.21.0",
|
||||
"react-i18next": "^14.1.1",
|
||||
"react-loading-skeleton": "^3.1.0",
|
||||
"react-router-dom": "^6.3.0",
|
||||
@ -54,6 +55,7 @@
|
||||
"@vitejs/plugin-react": "^4.0.0-beta.0",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"buffer": "^6.0.3",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^8.50.0",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
"eslint-plugin-ft-flow": "^3.0.0",
|
||||
@ -69,7 +71,6 @@
|
||||
"prettier": "^3.0.3",
|
||||
"rollup-plugin-visualizer": "^5.9.0",
|
||||
"tailwindcss": "^3.3.1",
|
||||
"vite": "^4.3.0",
|
||||
"cross-env": "^7.0.3"
|
||||
"vite": "^4.3.0"
|
||||
}
|
||||
}
|
||||
|
||||
@ -68,7 +68,6 @@ const LiveDocumentSyncManage = lazy(
|
||||
() => import("@/pages/Admin/ExperimentalFeatures/Features/LiveSync/manage")
|
||||
);
|
||||
const AgentBuilder = lazy(() => import("@/pages/Admin/AgentBuilder"));
|
||||
|
||||
const CommunityHubTrending = lazy(
|
||||
() => import("@/pages/GeneralSettings/CommunityHub/Trending")
|
||||
);
|
||||
@ -78,6 +77,9 @@ const CommunityHubAuthentication = lazy(
|
||||
const CommunityHubImportItem = lazy(
|
||||
() => import("@/pages/GeneralSettings/CommunityHub/ImportItem")
|
||||
);
|
||||
const SystemPromptVariables = lazy(
|
||||
() => import("@/pages/Admin/SystemPromptVariables")
|
||||
);
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
@ -195,6 +197,10 @@ export default function App() {
|
||||
path="/settings/api-keys"
|
||||
element={<AdminRoute Component={GeneralApiKeys} />}
|
||||
/>
|
||||
<Route
|
||||
path="/settings/system-prompt-variables"
|
||||
element={<AdminRoute Component={SystemPromptVariables} />}
|
||||
/>
|
||||
<Route
|
||||
path="/settings/browser-extension"
|
||||
element={
|
||||
|
||||
@ -354,6 +354,12 @@ const SidebarOptions = ({ user = null, t }) => (
|
||||
flex: true,
|
||||
roles: ["admin"],
|
||||
},
|
||||
{
|
||||
btnText: t("settings.system-prompt-variables"),
|
||||
href: paths.settings.systemPromptVariables(),
|
||||
flex: true,
|
||||
roles: ["admin"],
|
||||
},
|
||||
{
|
||||
btnText: t("settings.browser-extension"),
|
||||
href: paths.settings.browserExtension(),
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -97,6 +97,7 @@ const TRANSLATIONS = {
|
||||
"agent-skills": "Agent Skills",
|
||||
admin: "Admin",
|
||||
tools: "Tools",
|
||||
"system-prompt-variables": "System Prompt Variables",
|
||||
"experimental-features": "Experimental Features",
|
||||
contact: "Contact Support",
|
||||
"browser-extension": "Browser Extension",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -3,6 +3,7 @@ import { baseHeaders, safeJsonParse } from "@/utils/request";
|
||||
import DataConnector from "./dataConnector";
|
||||
import LiveDocumentSync from "./experimental/liveSync";
|
||||
import AgentPlugins from "./experimental/agentPlugins";
|
||||
import SystemPromptVariable from "./systemPromptVariable";
|
||||
|
||||
const System = {
|
||||
cacheKeys: {
|
||||
@ -745,6 +746,7 @@ const System = {
|
||||
liveSync: LiveDocumentSync,
|
||||
agentPlugins: AgentPlugins,
|
||||
},
|
||||
promptVariables: SystemPromptVariable,
|
||||
};
|
||||
|
||||
export default System;
|
||||
|
||||
106
frontend/src/models/systemPromptVariable.js
Normal file
106
frontend/src/models/systemPromptVariable.js
Normal file
@ -0,0 +1,106 @@
|
||||
import { API_BASE } from "@/utils/constants";
|
||||
import { baseHeaders } from "@/utils/request";
|
||||
|
||||
/**
|
||||
* @typedef {Object} SystemPromptVariable
|
||||
* @property {number|null} id - The ID of the system prompt variable
|
||||
* @property {string} key - The key of the system prompt variable
|
||||
* @property {string} value - The value of the system prompt variable
|
||||
* @property {string} description - The description of the system prompt variable
|
||||
* @property {string} type - The type of the system prompt variable
|
||||
*/
|
||||
|
||||
const SystemPromptVariable = {
|
||||
/**
|
||||
* Get all system prompt variables
|
||||
* @returns {Promise<{variables: SystemPromptVariable[]}>} - An array of system prompt variables
|
||||
*/
|
||||
getAll: async function () {
|
||||
try {
|
||||
return await fetch(`${API_BASE}/system/prompt-variables`, {
|
||||
method: "GET",
|
||||
headers: baseHeaders(),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.catch((error) => {
|
||||
console.error("Error fetching system prompt variables:", error);
|
||||
return { variables: [] };
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error fetching system prompt variables:", error);
|
||||
return { variables: [] };
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a new system prompt variable
|
||||
* @param {SystemPromptVariable} variable - The system prompt variable to create
|
||||
* @returns {Promise<{success: boolean, error: string}>} - A promise that resolves to an object containing a success flag and an error message
|
||||
*/
|
||||
create: async function (variable = {}) {
|
||||
try {
|
||||
return await fetch(`${API_BASE}/system/prompt-variables`, {
|
||||
method: "POST",
|
||||
headers: baseHeaders(),
|
||||
body: JSON.stringify(variable),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.catch((error) => {
|
||||
console.error("Error creating system prompt variable:", error);
|
||||
return { success: false, error };
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error creating system prompt variable:", error);
|
||||
return { success: false, error };
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Update a system prompt variable
|
||||
* @param {string} id - The ID of the system prompt variable to update
|
||||
* @param {SystemPromptVariable} variable - The system prompt variable to update
|
||||
* @returns {Promise<{success: boolean, error: string}>} - A promise that resolves to an object containing a success flag and an error message
|
||||
*/
|
||||
update: async function (id, variable = {}) {
|
||||
try {
|
||||
return await fetch(`${API_BASE}/system/prompt-variables/${id}`, {
|
||||
method: "PUT",
|
||||
headers: baseHeaders(),
|
||||
body: JSON.stringify(variable),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.catch((error) => {
|
||||
console.error("Error updating system prompt variable:", error);
|
||||
return { success: false, error };
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error updating system prompt variable:", error);
|
||||
return { success: false, error };
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete a system prompt variable
|
||||
* @param {string} id - The ID of the system prompt variable to delete
|
||||
* @returns {Promise<{success: boolean, error: string}>} - A promise that resolves to an object containing a success flag and an error message
|
||||
*/
|
||||
delete: async function (id = null) {
|
||||
try {
|
||||
if (id === null) return { success: false, error: "ID is required" };
|
||||
return await fetch(`${API_BASE}/system/prompt-variables/${id}`, {
|
||||
method: "DELETE",
|
||||
headers: baseHeaders(),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.catch((error) => {
|
||||
console.error("Error deleting system prompt variable:", error);
|
||||
return { success: false, error };
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error deleting system prompt variable:", error);
|
||||
return { success: false, error };
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export default SystemPromptVariable;
|
||||
@ -0,0 +1,129 @@
|
||||
import React, { useState } from "react";
|
||||
import { X } from "@phosphor-icons/react";
|
||||
import System from "@/models/system";
|
||||
import showToast from "@/utils/toast";
|
||||
|
||||
export default function AddVariableModal({ closeModal, onRefresh }) {
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
const handleCreate = async (e) => {
|
||||
e.preventDefault();
|
||||
setError(null);
|
||||
const formData = new FormData(e.target);
|
||||
const newVariable = {};
|
||||
for (const [key, value] of formData.entries())
|
||||
newVariable[key] = value.trim();
|
||||
|
||||
if (!newVariable.key || !newVariable.value) {
|
||||
setError("Key and value are required");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await System.promptVariables.create(newVariable);
|
||||
showToast("Variable created successfully", "success", { clear: true });
|
||||
if (onRefresh) onRefresh();
|
||||
closeModal();
|
||||
} catch (error) {
|
||||
console.error("Error creating variable:", error);
|
||||
setError("Failed to create variable");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 overflow-auto bg-black bg-opacity-50 flex items-center justify-center">
|
||||
<div className="relative w-full max-w-2xl bg-theme-bg-secondary rounded-lg shadow border-2 border-theme-modal-border">
|
||||
<div className="relative p-6 border-b rounded-t border-theme-modal-border">
|
||||
<div className="w-full flex gap-x-2 items-center">
|
||||
<h3 className="text-xl font-semibold text-white overflow-hidden overflow-ellipsis whitespace-nowrap">
|
||||
Add New Variable
|
||||
</h3>
|
||||
</div>
|
||||
<button
|
||||
onClick={closeModal}
|
||||
type="button"
|
||||
className="absolute top-4 right-4 transition-all duration-300 bg-transparent rounded-lg text-sm p-1 inline-flex items-center hover:bg-theme-modal-border hover:border-theme-modal-border hover:border-opacity-50 border-transparent border"
|
||||
>
|
||||
<X size={24} weight="bold" className="text-white" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="p-6">
|
||||
<form onSubmit={handleCreate}>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label
|
||||
htmlFor="key"
|
||||
className="block mb-2 text-sm font-medium text-white"
|
||||
>
|
||||
Key
|
||||
</label>
|
||||
<input
|
||||
name="key"
|
||||
type="text"
|
||||
minLength={3}
|
||||
maxLength={255}
|
||||
className="border-none bg-theme-settings-input-bg w-full text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5"
|
||||
placeholder="e.g., company_name"
|
||||
required={true}
|
||||
autoComplete="off"
|
||||
pattern="^[a-zA-Z0-9_]+$"
|
||||
/>
|
||||
<p className="mt-2 text-xs text-white/60">
|
||||
Key must be unique and will be used in prompts as {"{key}"}.
|
||||
Only letters, numbers and underscores are allowed.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
htmlFor="value"
|
||||
className="block mb-2 text-sm font-medium text-white"
|
||||
>
|
||||
Value
|
||||
</label>
|
||||
<input
|
||||
name="value"
|
||||
type="text"
|
||||
className="border-none bg-theme-settings-input-bg w-full text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5"
|
||||
placeholder="e.g., Acme Corp"
|
||||
required={true}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
htmlFor="description"
|
||||
className="block mb-2 text-sm font-medium text-white"
|
||||
>
|
||||
Description
|
||||
</label>
|
||||
<input
|
||||
name="description"
|
||||
type="text"
|
||||
className="border-none bg-theme-settings-input-bg w-full text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5"
|
||||
placeholder="Optional description"
|
||||
autoComplete="off"
|
||||
/>
|
||||
</div>
|
||||
{error && <p className="text-red-400 text-sm">Error: {error}</p>}
|
||||
</div>
|
||||
<div className="flex justify-between items-center mt-6 pt-6 border-t border-theme-modal-border">
|
||||
<button
|
||||
onClick={closeModal}
|
||||
type="button"
|
||||
className="transition-all duration-300 text-white hover:bg-zinc-700 px-4 py-2 rounded-lg text-sm"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="transition-all duration-300 bg-white text-black hover:opacity-60 px-4 py-2 rounded-lg text-sm"
|
||||
>
|
||||
Create variable
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,133 @@
|
||||
import React, { useState } from "react";
|
||||
import { X } from "@phosphor-icons/react";
|
||||
import System from "@/models/system";
|
||||
import showToast from "@/utils/toast";
|
||||
|
||||
export default function EditVariableModal({ variable, closeModal, onRefresh }) {
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
const handleUpdate = async (e) => {
|
||||
if (!variable.id) return;
|
||||
e.preventDefault();
|
||||
setError(null);
|
||||
const formData = new FormData(e.target);
|
||||
const updatedVariable = {};
|
||||
for (const [key, value] of formData.entries())
|
||||
updatedVariable[key] = value.trim();
|
||||
|
||||
if (!updatedVariable.key || !updatedVariable.value) {
|
||||
setError("Key and value are required");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await System.promptVariables.update(variable.id, updatedVariable);
|
||||
showToast("Variable updated successfully", "success", { clear: true });
|
||||
if (onRefresh) onRefresh();
|
||||
closeModal();
|
||||
} catch (error) {
|
||||
console.error("Error updating variable:", error);
|
||||
setError("Failed to update variable");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 overflow-auto bg-black bg-opacity-50 flex items-center justify-center">
|
||||
<div className="relative w-full max-w-2xl bg-theme-bg-secondary rounded-lg shadow border-2 border-theme-modal-border">
|
||||
<div className="relative p-6 border-b rounded-t border-theme-modal-border">
|
||||
<div className="w-full flex gap-x-2 items-center">
|
||||
<h3 className="text-xl font-semibold text-white overflow-hidden overflow-ellipsis whitespace-nowrap">
|
||||
Edit {variable.key}
|
||||
</h3>
|
||||
</div>
|
||||
<button
|
||||
onClick={closeModal}
|
||||
type="button"
|
||||
className="absolute top-4 right-4 transition-all duration-300 bg-transparent rounded-lg text-sm p-1 inline-flex items-center hover:bg-theme-modal-border hover:border-theme-modal-border hover:border-opacity-50 border-transparent border"
|
||||
>
|
||||
<X size={24} weight="bold" className="text-white" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="p-6">
|
||||
<form onSubmit={handleUpdate}>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label
|
||||
htmlFor="key"
|
||||
className="block mb-2 text-sm font-medium text-white"
|
||||
>
|
||||
Key
|
||||
</label>
|
||||
<input
|
||||
name="key"
|
||||
minLength={3}
|
||||
maxLength={255}
|
||||
type="text"
|
||||
className="border-none bg-theme-settings-input-bg w-full text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5"
|
||||
placeholder="e.g., company_name"
|
||||
defaultValue={variable.key}
|
||||
required={true}
|
||||
autoComplete="off"
|
||||
pattern="^[a-zA-Z0-9_]+$"
|
||||
/>
|
||||
<p className="mt-2 text-xs text-white/60">
|
||||
Key must be unique and will be used in prompts as {"{key}"}.
|
||||
Only letters, numbers and underscores are allowed.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
htmlFor="value"
|
||||
className="block mb-2 text-sm font-medium text-white"
|
||||
>
|
||||
Value
|
||||
</label>
|
||||
<input
|
||||
name="value"
|
||||
type="text"
|
||||
className="border-none bg-theme-settings-input-bg w-full text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5"
|
||||
placeholder="e.g., Acme Corp"
|
||||
defaultValue={variable.value}
|
||||
required={true}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
htmlFor="description"
|
||||
className="block mb-2 text-sm font-medium text-white"
|
||||
>
|
||||
Description
|
||||
</label>
|
||||
<input
|
||||
name="description"
|
||||
type="text"
|
||||
className="border-none bg-theme-settings-input-bg w-full text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5"
|
||||
placeholder="Optional description"
|
||||
defaultValue={variable.description}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</div>
|
||||
{error && <p className="text-red-400 text-sm">Error: {error}</p>}
|
||||
</div>
|
||||
<div className="flex justify-between items-center mt-6 pt-6 border-t border-theme-modal-border">
|
||||
<button
|
||||
onClick={closeModal}
|
||||
type="button"
|
||||
className="transition-all duration-300 text-white hover:bg-zinc-700 px-4 py-2 rounded-lg text-sm"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="transition-all duration-300 bg-white text-black hover:opacity-60 px-4 py-2 rounded-lg text-sm"
|
||||
>
|
||||
Update variable
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,114 @@
|
||||
import { useRef } from "react";
|
||||
import System from "@/models/system";
|
||||
import showToast from "@/utils/toast";
|
||||
import { useModal } from "@/hooks/useModal";
|
||||
import ModalWrapper from "@/components/ModalWrapper";
|
||||
import EditVariableModal from "./EditVariableModal";
|
||||
import { titleCase } from "text-case";
|
||||
import truncate from "truncate";
|
||||
|
||||
/**
|
||||
* A row component for displaying a system prompt variable
|
||||
* @param {{id: number|null, key: string, value: string, description: string, type: string}} variable - The system prompt variable to display
|
||||
* @param {Function} onRefresh - A function to call when the variable is refreshed
|
||||
* @returns {JSX.Element} A JSX element for displaying the variable
|
||||
*/
|
||||
export default function VariableRow({ variable, onRefresh }) {
|
||||
const rowRef = useRef(null);
|
||||
const { isOpen, openModal, closeModal } = useModal();
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (!variable.id) return;
|
||||
if (
|
||||
!window.confirm(
|
||||
`Are you sure you want to delete the variable "${variable.key}"?\nThis action is irreversible.`
|
||||
)
|
||||
)
|
||||
return false;
|
||||
|
||||
try {
|
||||
await System.promptVariables.delete(variable.id);
|
||||
rowRef?.current?.remove();
|
||||
showToast("Variable deleted successfully", "success", { clear: true });
|
||||
if (onRefresh) onRefresh();
|
||||
} catch (error) {
|
||||
console.error("Error deleting variable:", error);
|
||||
showToast("Failed to delete variable", "error", { clear: true });
|
||||
}
|
||||
};
|
||||
|
||||
const getTypeColorTheme = (type) => {
|
||||
switch (type) {
|
||||
case "system":
|
||||
return {
|
||||
bg: "bg-blue-600/20",
|
||||
text: "text-blue-400 light:text-blue-800",
|
||||
};
|
||||
case "dynamic":
|
||||
return {
|
||||
bg: "bg-green-600/20",
|
||||
text: "text-green-400 light:text-green-800",
|
||||
};
|
||||
default:
|
||||
return {
|
||||
bg: "bg-yellow-600/20",
|
||||
text: "text-yellow-400 light:text-yellow-800",
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const colorTheme = getTypeColorTheme(variable.type);
|
||||
|
||||
return (
|
||||
<>
|
||||
<tr
|
||||
ref={rowRef}
|
||||
className="bg-transparent text-white text-opacity-80 text-sm font-medium"
|
||||
>
|
||||
<th scope="row" className="px-6 py-4 whitespace-nowrap">
|
||||
{variable.key}
|
||||
</th>
|
||||
<td className="px-6 py-4">
|
||||
{typeof variable.value === "function"
|
||||
? variable.value()
|
||||
: truncate(variable.value, 50)}
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
{truncate(variable.description || "-", 50)}
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<span
|
||||
className={`rounded-full ${colorTheme.bg} px-2 py-0.5 text-xs leading-5 font-semibold ${colorTheme.text} shadow-sm`}
|
||||
>
|
||||
{titleCase(variable.type)}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 flex items-center justify-end gap-x-6">
|
||||
{variable.type === "static" && (
|
||||
<>
|
||||
<button
|
||||
onClick={openModal}
|
||||
className="text-sm font-medium text-white/80 light:text-black/80 rounded-lg hover:text-white hover:light:text-gray-500 px-2 py-1 hover:bg-white hover:bg-opacity-10"
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
onClick={handleDelete}
|
||||
className="text-sm font-medium text-white/80 light:text-black/80 hover:light:text-red-500 hover:text-red-300 rounded-lg px-2 py-1 hover:bg-white hover:light:bg-red-50 hover:bg-opacity-10"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
<ModalWrapper isOpen={isOpen}>
|
||||
<EditVariableModal
|
||||
variable={variable}
|
||||
closeModal={closeModal}
|
||||
onRefresh={onRefresh}
|
||||
/>
|
||||
</ModalWrapper>
|
||||
</>
|
||||
);
|
||||
}
|
||||
120
frontend/src/pages/Admin/SystemPromptVariables/index.jsx
Normal file
120
frontend/src/pages/Admin/SystemPromptVariables/index.jsx
Normal file
@ -0,0 +1,120 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import System from "@/models/system";
|
||||
import showToast from "@/utils/toast";
|
||||
import { Plus } from "@phosphor-icons/react";
|
||||
import Sidebar from "@/components/SettingsSidebar";
|
||||
import { isMobile } from "react-device-detect";
|
||||
import CTAButton from "@/components/lib/CTAButton";
|
||||
import VariableRow from "./VariableRow";
|
||||
import ModalWrapper from "@/components/ModalWrapper";
|
||||
import AddVariableModal from "./AddVariableModal";
|
||||
import { useModal } from "@/hooks/useModal";
|
||||
import * as Skeleton from "react-loading-skeleton";
|
||||
import "react-loading-skeleton/dist/skeleton.css";
|
||||
|
||||
export default function SystemPromptVariables() {
|
||||
const [variables, setVariables] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const { isOpen, openModal, closeModal } = useModal();
|
||||
|
||||
useEffect(() => {
|
||||
fetchVariables();
|
||||
}, []);
|
||||
|
||||
const fetchVariables = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const { variables } = await System.promptVariables.getAll();
|
||||
setVariables(variables || []);
|
||||
} catch (error) {
|
||||
console.error("Error fetching variables:", error);
|
||||
showToast("No variables found", "error");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-screen h-screen overflow-hidden bg-theme-bg-container flex">
|
||||
<Sidebar />
|
||||
<div
|
||||
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
|
||||
className="relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-theme-bg-secondary w-full h-full overflow-y-scroll p-4 md:p-0"
|
||||
>
|
||||
<div className="flex flex-col w-full px-1 md:pl-6 md:pr-[50px] md:py-6 py-16">
|
||||
<div className="w-full flex flex-col gap-y-1 pb-6 border-white/10 border-b-2">
|
||||
<div className="items-center flex gap-x-4">
|
||||
<p className="text-lg leading-6 font-bold text-theme-text-primary">
|
||||
System Prompt Variables
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-xs leading-[18px] font-base text-theme-text-secondary">
|
||||
System prompt variables are used to store configuration values
|
||||
that can be referenced in your system prompt to enable dynamic
|
||||
content in your prompts.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="w-full justify-end flex">
|
||||
<CTAButton
|
||||
onClick={openModal}
|
||||
className="mt-3 mr-0 mb-4 md:-mb-6 z-10"
|
||||
>
|
||||
<Plus className="h-4 w-4" weight="bold" /> Add Variable
|
||||
</CTAButton>
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto">
|
||||
{loading ? (
|
||||
<Skeleton.default
|
||||
height="80vh"
|
||||
width="100%"
|
||||
highlightColor="var(--theme-bg-primary)"
|
||||
baseColor="var(--theme-bg-secondary)"
|
||||
count={1}
|
||||
className="w-full p-4 rounded-b-2xl rounded-tr-2xl rounded-tl-sm mt-8"
|
||||
containerClassName="flex w-full"
|
||||
/>
|
||||
) : variables.length === 0 ? (
|
||||
<div className="text-center py-4 text-theme-text-secondary">
|
||||
No variables found
|
||||
</div>
|
||||
) : (
|
||||
<table className="w-full text-sm text-left rounded-lg min-w-[640px] border-spacing-0">
|
||||
<thead className="text-theme-text-secondary text-xs leading-[18px] font-bold uppercase border-white/10 border-b">
|
||||
<tr>
|
||||
<th scope="col" className="px-6 py-3 rounded-tl-lg">
|
||||
Key
|
||||
</th>
|
||||
<th scope="col" className="px-6 py-3">
|
||||
Value
|
||||
</th>
|
||||
<th scope="col" className="px-6 py-3">
|
||||
Description
|
||||
</th>
|
||||
<th scope="col" className="px-6 py-3">
|
||||
Type
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{variables.map((variable) => (
|
||||
<VariableRow
|
||||
key={variable.id}
|
||||
variable={variable}
|
||||
onRefresh={fetchVariables}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ModalWrapper isOpen={isOpen}>
|
||||
<AddVariableModal closeModal={closeModal} onRefresh={fetchVariables} />
|
||||
</ModalWrapper>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,7 +1,25 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { chatPrompt } from "@/utils/chat";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import SystemPromptVariable from "@/models/systemPromptVariable";
|
||||
import Highlighter from "react-highlight-words";
|
||||
import { Link } from "react-router-dom";
|
||||
import paths from "@/utils/paths";
|
||||
|
||||
export default function ChatPromptSettings({ workspace, setHasChanges }) {
|
||||
const { t } = useTranslation();
|
||||
const [availableVariables, setAvailableVariables] = useState([]);
|
||||
const [prompt, setPrompt] = useState(chatPrompt(workspace));
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
async function setupVariableHighlighting() {
|
||||
const { variables } = await SystemPromptVariable.getAll();
|
||||
setAvailableVariables(variables);
|
||||
}
|
||||
setupVariableHighlighting();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex flex-col">
|
||||
@ -11,18 +29,97 @@ export default function ChatPromptSettings({ workspace, setHasChanges }) {
|
||||
<p className="text-white text-opacity-60 text-xs font-medium py-1.5">
|
||||
{t("chat.prompt.description")}
|
||||
</p>
|
||||
<p className="text-white text-opacity-60 text-xs font-medium mb-2">
|
||||
You can insert{" "}
|
||||
<Link
|
||||
to={paths.settings.systemPromptVariables()}
|
||||
className="text-primary-button"
|
||||
>
|
||||
prompt variables
|
||||
</Link>{" "}
|
||||
like:{" "}
|
||||
{availableVariables.slice(0, 3).map((v, i) => (
|
||||
<>
|
||||
<span
|
||||
key={v.key}
|
||||
className="bg-theme-settings-input-bg px-1 py-0.5 rounded"
|
||||
>
|
||||
{`{${v.key}}`}
|
||||
</span>
|
||||
{i < availableVariables.length - 1 && ", "}
|
||||
</>
|
||||
))}
|
||||
{availableVariables.length > 3 && (
|
||||
<Link
|
||||
to={paths.settings.systemPromptVariables()}
|
||||
className="text-primary-button"
|
||||
>
|
||||
+{availableVariables.length - 3} more...
|
||||
</Link>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<input type="hidden" name="openAiPrompt" defaultValue={prompt} />
|
||||
|
||||
<div className="relative">
|
||||
<span
|
||||
className={`${!!prompt ? "hidden" : "block"} text-sm pointer-events-none absolute top-0 left-0 p-2.5 w-full h-full !text-theme-settings-input-placeholder opacity-60`}
|
||||
>
|
||||
Given the following conversation, relevant context, and a follow up
|
||||
question, reply with an answer to the current question the user is
|
||||
asking. Return only your response to the question given the above
|
||||
information following the users instructions as needed.
|
||||
</span>
|
||||
{isEditing ? (
|
||||
<textarea
|
||||
autoFocus={true}
|
||||
rows={5}
|
||||
onFocus={(e) => {
|
||||
const length = e.target.value.length;
|
||||
e.target.setSelectionRange(length, length);
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
setIsEditing(false);
|
||||
setPrompt(e.target.value);
|
||||
}}
|
||||
onChange={(e) => {
|
||||
setPrompt(e.target.value);
|
||||
setHasChanges(true);
|
||||
}}
|
||||
onPaste={(e) => {
|
||||
setPrompt(e.target.value);
|
||||
setHasChanges(true);
|
||||
}}
|
||||
style={{
|
||||
resize: "vertical",
|
||||
overflowY: "scroll",
|
||||
minHeight: "150px",
|
||||
}}
|
||||
defaultValue={prompt}
|
||||
className="border-none bg-theme-settings-input-bg text-white text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5 mt-2"
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
onClick={() => setIsEditing(true)}
|
||||
style={{
|
||||
resize: "vertical",
|
||||
overflowY: "scroll",
|
||||
minHeight: "150px",
|
||||
}}
|
||||
className="border-none bg-theme-settings-input-bg text-white text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5 mt-2"
|
||||
>
|
||||
<Highlighter
|
||||
className="whitespace-pre-wrap"
|
||||
highlightClassName="bg-cta-button p-0.5 rounded-md"
|
||||
searchWords={availableVariables.map((v) => `{${v.key}}`)}
|
||||
autoEscape={true}
|
||||
caseSensitive={true}
|
||||
textToHighlight={prompt}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<textarea
|
||||
name="openAiPrompt"
|
||||
rows={5}
|
||||
defaultValue={chatPrompt(workspace)}
|
||||
className="border-none bg-theme-settings-input-bg placeholder:text-theme-settings-input-placeholder text-white text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5 mt-2"
|
||||
placeholder="Given the following conversation, relevant context, and a follow up question, reply with an answer to the current question the user is asking. Return only your response to the question given the above information following the users instructions as needed."
|
||||
required={true}
|
||||
wrap="soft"
|
||||
autoComplete="off"
|
||||
onChange={() => setHasChanges(true)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -120,6 +120,7 @@ export default {
|
||||
apiKeys: () => {
|
||||
return "/settings/api-keys";
|
||||
},
|
||||
systemPromptVariables: () => "/settings/system-prompt-variables",
|
||||
logs: () => {
|
||||
return "/settings/event-logs";
|
||||
},
|
||||
|
||||
@ -2076,6 +2076,11 @@ hermes-parser@0.22.0:
|
||||
dependencies:
|
||||
hermes-estree "0.22.0"
|
||||
|
||||
highlight-words-core@^1.2.0:
|
||||
version "1.2.3"
|
||||
resolved "https://registry.yarnpkg.com/highlight-words-core/-/highlight-words-core-1.2.3.tgz#781f37b2a220bf998114e4ef8c8cb6c7a4802ea8"
|
||||
integrity sha512-m1O9HW3/GNHxzSIXWw1wCNXXsgLlxrP0OI6+ycGUhiUHkikqW3OrwVHz+lxeNBe5yqLESdIcj8PowHQ2zLvUvQ==
|
||||
|
||||
highlight.js@^11.9.0:
|
||||
version "11.10.0"
|
||||
resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-11.10.0.tgz#6e3600dc4b33d6dc23d5bd94fbf72405f5892b92"
|
||||
@ -2559,6 +2564,11 @@ mdurl@^1.0.1:
|
||||
resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e"
|
||||
integrity sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==
|
||||
|
||||
memoize-one@^4.0.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-4.1.0.tgz#a2387c58c03fff27ca390c31b764a79addf3f906"
|
||||
integrity sha512-2GApq0yI/b22J2j9rhbrAlsHb0Qcz+7yWxeLG8h+95sl1XPUgeLimQSOdur4Vw7cUhrBHwaUZxWFZueojqNRzA==
|
||||
|
||||
memoize-one@^5.1.1:
|
||||
version "5.2.1"
|
||||
resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e"
|
||||
@ -2998,6 +3008,14 @@ react-dropzone@^14.2.3:
|
||||
file-selector "^0.6.0"
|
||||
prop-types "^15.8.1"
|
||||
|
||||
react-highlight-words@^0.21.0:
|
||||
version "0.21.0"
|
||||
resolved "https://registry.yarnpkg.com/react-highlight-words/-/react-highlight-words-0.21.0.tgz#a109acdf7dc6fac3ed7db82e9cba94e8d65c281c"
|
||||
integrity sha512-SdWEeU9fIINArEPO1rO5OxPyuhdEKZQhHzZZP1ie6UeXQf+CjycT1kWaB+9bwGcVbR0NowuHK3RqgqNg6bgBDQ==
|
||||
dependencies:
|
||||
highlight-words-core "^1.2.0"
|
||||
memoize-one "^4.0.0"
|
||||
|
||||
react-i18next@^14.1.1:
|
||||
version "14.1.3"
|
||||
resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-14.1.3.tgz#85525c4294ef870ddd3f5d184e793cae362f47cb"
|
||||
|
||||
@ -56,6 +56,7 @@ const {
|
||||
} = require("../utils/middleware/chatHistoryViewable");
|
||||
const { simpleSSOEnabled } = require("../utils/middleware/simpleSSOEnabled");
|
||||
const { TemporaryAuthToken } = require("../models/temporaryAuthToken");
|
||||
const { SystemPromptVariables } = require("../models/systemPromptVariables");
|
||||
const { VALID_COMMANDS } = require("../utils/chats");
|
||||
|
||||
function systemEndpoints(app) {
|
||||
@ -1244,6 +1245,130 @@ function systemEndpoints(app) {
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
app.get(
|
||||
"/system/prompt-variables",
|
||||
[validatedRequest, flexUserRoleValid([ROLES.all])],
|
||||
async (request, response) => {
|
||||
try {
|
||||
const user = await userFromSession(request, response);
|
||||
const variables = await SystemPromptVariables.getAll(user?.id);
|
||||
response.status(200).json({ variables });
|
||||
} catch (error) {
|
||||
console.error("Error fetching system prompt variables:", error);
|
||||
response.status(500).json({
|
||||
success: false,
|
||||
error: `Failed to fetch system prompt variables: ${error.message}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
app.post(
|
||||
"/system/prompt-variables",
|
||||
[validatedRequest, flexUserRoleValid([ROLES.admin])],
|
||||
async (request, response) => {
|
||||
try {
|
||||
const user = await userFromSession(request, response);
|
||||
const { key, value, description = null } = reqBody(request);
|
||||
|
||||
if (!key || !value) {
|
||||
return response.status(400).json({
|
||||
success: false,
|
||||
error: "Key and value are required",
|
||||
});
|
||||
}
|
||||
|
||||
const variable = await SystemPromptVariables.create({
|
||||
key,
|
||||
value,
|
||||
description,
|
||||
userId: user?.id || null,
|
||||
});
|
||||
|
||||
response.status(200).json({
|
||||
success: true,
|
||||
variable,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error creating system prompt variable:", error);
|
||||
response.status(500).json({
|
||||
success: false,
|
||||
error: `Failed to create system prompt variable: ${error.message}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
app.put(
|
||||
"/system/prompt-variables/:id",
|
||||
[validatedRequest, flexUserRoleValid([ROLES.admin])],
|
||||
async (request, response) => {
|
||||
try {
|
||||
const { id } = request.params;
|
||||
const { key, value, description = null } = reqBody(request);
|
||||
|
||||
if (!key || !value) {
|
||||
return response.status(400).json({
|
||||
success: false,
|
||||
error: "Key and value are required",
|
||||
});
|
||||
}
|
||||
|
||||
const variable = await SystemPromptVariables.update(Number(id), {
|
||||
key,
|
||||
value,
|
||||
description,
|
||||
});
|
||||
|
||||
if (!variable) {
|
||||
return response.status(404).json({
|
||||
success: false,
|
||||
error: "Variable not found",
|
||||
});
|
||||
}
|
||||
|
||||
response.status(200).json({
|
||||
success: true,
|
||||
variable,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error updating system prompt variable:", error);
|
||||
response.status(500).json({
|
||||
success: false,
|
||||
error: `Failed to update system prompt variable: ${error.message}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
app.delete(
|
||||
"/system/prompt-variables/:id",
|
||||
[validatedRequest, flexUserRoleValid([ROLES.admin])],
|
||||
async (request, response) => {
|
||||
try {
|
||||
const { id } = request.params;
|
||||
const success = await SystemPromptVariables.delete(Number(id));
|
||||
|
||||
if (!success) {
|
||||
return response.status(404).json({
|
||||
success: false,
|
||||
error: "System prompt variable not found or could not be deleted",
|
||||
});
|
||||
}
|
||||
|
||||
response.status(200).json({
|
||||
success: true,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error deleting system prompt variable:", error);
|
||||
response.status(500).json({
|
||||
success: false,
|
||||
error: `Failed to delete system prompt variable: ${error.message}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = { systemEndpoints };
|
||||
|
||||
286
server/models/systemPromptVariables.js
Normal file
286
server/models/systemPromptVariables.js
Normal file
@ -0,0 +1,286 @@
|
||||
const prisma = require("../utils/prisma");
|
||||
const moment = require("moment");
|
||||
|
||||
/**
|
||||
* @typedef {Object} SystemPromptVariable
|
||||
* @property {number} id
|
||||
* @property {string} key
|
||||
* @property {string|function} value
|
||||
* @property {string} description
|
||||
* @property {'system'|'user'|'static'} type
|
||||
* @property {number} userId
|
||||
* @property {boolean} multiUserRequired
|
||||
*/
|
||||
|
||||
const SystemPromptVariables = {
|
||||
VALID_TYPES: ["user", "system", "static"],
|
||||
DEFAULT_VARIABLES: [
|
||||
{
|
||||
key: "time",
|
||||
value: () => moment().format("LTS"),
|
||||
description: "Current time",
|
||||
type: "system",
|
||||
multiUserRequired: false,
|
||||
},
|
||||
{
|
||||
key: "date",
|
||||
value: () => moment().format("LL"),
|
||||
description: "Current date",
|
||||
type: "system",
|
||||
multiUserRequired: false,
|
||||
},
|
||||
{
|
||||
key: "datetime",
|
||||
value: () => moment().format("LLLL"),
|
||||
description: "Current date and time",
|
||||
type: "system",
|
||||
multiUserRequired: false,
|
||||
},
|
||||
{
|
||||
key: "user.name",
|
||||
value: async (userId = null) => {
|
||||
if (!userId) return "[User name]";
|
||||
try {
|
||||
const user = await prisma.users.findUnique({
|
||||
where: { id: Number(userId) },
|
||||
select: { username: true },
|
||||
});
|
||||
return user?.username || "[User name is empty or unknown]";
|
||||
} catch (error) {
|
||||
console.error("Error fetching user name:", error);
|
||||
return "[User name is empty or unknown]";
|
||||
}
|
||||
},
|
||||
description: "Current user's username",
|
||||
type: "user",
|
||||
multiUserRequired: true,
|
||||
},
|
||||
{
|
||||
key: "user.bio",
|
||||
value: async (userId = null) => {
|
||||
if (!userId) return "[User bio]";
|
||||
try {
|
||||
const user = await prisma.users.findUnique({
|
||||
where: { id: Number(userId) },
|
||||
select: { bio: true },
|
||||
});
|
||||
return user?.bio || "[User bio is empty]";
|
||||
} catch (error) {
|
||||
console.error("Error fetching user bio:", error);
|
||||
return "[User bio is empty]";
|
||||
}
|
||||
},
|
||||
description: "Current user's bio field from their profile",
|
||||
type: "user",
|
||||
multiUserRequired: true,
|
||||
},
|
||||
],
|
||||
|
||||
/**
|
||||
* Gets a system prompt variable by its key
|
||||
* @param {string} key
|
||||
* @returns {Promise<SystemPromptVariable>}
|
||||
*/
|
||||
get: async function (key = null) {
|
||||
if (!key) return null;
|
||||
const variable = await prisma.system_prompt_variables.findUnique({
|
||||
where: { key: String(key) },
|
||||
});
|
||||
return variable;
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieves all system prompt variables with dynamic variables as well
|
||||
* as user defined variables
|
||||
* @param {number|null} userId - the user ID to filter variables by
|
||||
* @returns {Promise<SystemPromptVariable[]>}
|
||||
*/
|
||||
getAll: async function (userId = null) {
|
||||
const dbVariables = await prisma.system_prompt_variables.findMany({
|
||||
where: userId ? { userId: Number(userId) } : {},
|
||||
});
|
||||
|
||||
const formattedDbVars = dbVariables.map((v) => ({
|
||||
id: v.id,
|
||||
key: v.key,
|
||||
value: v.value,
|
||||
description: v.description,
|
||||
type: v.type,
|
||||
userId: v.userId,
|
||||
}));
|
||||
|
||||
// If userId is not provided, filter the default variables to only include non-multiUserRequired ones
|
||||
const filteredSystemVars = !userId
|
||||
? this.DEFAULT_VARIABLES.filter((v) => !v.multiUserRequired)
|
||||
: this.DEFAULT_VARIABLES;
|
||||
|
||||
return [...filteredSystemVars, ...formattedDbVars];
|
||||
},
|
||||
|
||||
/**
|
||||
* Creates a new system prompt variable
|
||||
* @param {{ key: string, value: string, description: string, type: string, userId: number }} data
|
||||
* @returns {Promise<SystemPromptVariable>}
|
||||
*/
|
||||
create: async function ({
|
||||
key,
|
||||
value,
|
||||
description = null,
|
||||
type = "static",
|
||||
userId = null,
|
||||
}) {
|
||||
await this._checkVariableKey(key, true);
|
||||
return await prisma.system_prompt_variables.create({
|
||||
data: {
|
||||
key: String(key),
|
||||
value: String(value),
|
||||
description: description ? String(description) : null,
|
||||
type: type ? String(type) : "static",
|
||||
userId: userId ? Number(userId) : null,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Updates a system prompt variable by its unique database ID
|
||||
* @param {number} id
|
||||
* @param {{ key: string, value: string, description: string }} data
|
||||
* @returns {Promise<SystemPromptVariable>}
|
||||
*/
|
||||
update: async function (id, { key, value, description = null }) {
|
||||
if (!id || !key || !value) return null;
|
||||
const existingRecord = await prisma.system_prompt_variables.findFirst({
|
||||
where: { id: Number(id) },
|
||||
});
|
||||
if (!existingRecord) throw new Error("System prompt variable not found");
|
||||
await this._checkVariableKey(key, false);
|
||||
|
||||
return await prisma.system_prompt_variables.update({
|
||||
where: { id: existingRecord.id },
|
||||
data: {
|
||||
key: String(key),
|
||||
value: String(value),
|
||||
description: description ? String(description) : null,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Deletes a system prompt variable by its unique database ID
|
||||
* @param {number} id
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
delete: async function (id = null) {
|
||||
try {
|
||||
await prisma.system_prompt_variables.delete({
|
||||
where: { id: Number(id) },
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("Error deleting variable:", error);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Injects variables into a string based on the user ID (if provided) and the variables available
|
||||
* @param {string} str - the input string to expand variables into
|
||||
* @param {number|null} userId - the user ID to use for dynamic variables
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
expandSystemPromptVariables: async function (str, userId = null) {
|
||||
if (!str) return str;
|
||||
|
||||
try {
|
||||
const allVariables = await this.getAll(userId);
|
||||
let result = str;
|
||||
|
||||
// Find all variable patterns in the string
|
||||
const matches = str.match(/\{([^}]+)\}/g) || [];
|
||||
|
||||
// Process each match
|
||||
for (const match of matches) {
|
||||
const key = match.substring(1, match.length - 1); // Remove { and }
|
||||
|
||||
// Handle `user.X` variables with current user's data
|
||||
if (key.startsWith("user.")) {
|
||||
const userProp = key.split(".")[1];
|
||||
const variable = allVariables.find((v) => v.key === key);
|
||||
|
||||
if (variable && typeof variable.value === "function") {
|
||||
if (variable.value.constructor.name === "AsyncFunction") {
|
||||
try {
|
||||
const value = await variable.value(userId);
|
||||
result = result.replace(match, value);
|
||||
} catch (error) {
|
||||
console.error(`Error processing user variable ${key}:`, error);
|
||||
result = result.replace(match, `[User ${userProp}]`);
|
||||
}
|
||||
} else {
|
||||
const value = variable.value();
|
||||
result = result.replace(match, value);
|
||||
}
|
||||
} else {
|
||||
result = result.replace(match, `[User ${userProp}]`);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle regular variables (static types)
|
||||
const variable = allVariables.find((v) => v.key === key);
|
||||
if (!variable) continue;
|
||||
|
||||
// For dynamic and system variables, call the function to get the current value
|
||||
if (
|
||||
["system"].includes(variable.type) &&
|
||||
typeof variable.value === "function"
|
||||
) {
|
||||
try {
|
||||
if (variable.value.constructor.name === "AsyncFunction") {
|
||||
const value = await variable.value(userId);
|
||||
result = result.replace(match, value);
|
||||
} else {
|
||||
const value = variable.value();
|
||||
result = result.replace(match, value);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error processing dynamic variable ${key}:`, error);
|
||||
result = result.replace(match, match);
|
||||
}
|
||||
} else {
|
||||
result = result.replace(match, variable.value || match);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error("Error in expandSystemPromptVariables:", error);
|
||||
return str;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Internal function to check if a variable key is valid
|
||||
* @param {string} key
|
||||
* @param {boolean} checkExisting
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
_checkVariableKey: async function (key = null, checkExisting = true) {
|
||||
if (!key) throw new Error("Key is required");
|
||||
if (typeof key !== "string") throw new Error("Key must be a string");
|
||||
if (!/^[a-zA-Z0-9_]+$/.test(key))
|
||||
throw new Error("Key must contain only letters, numbers and underscores");
|
||||
if (key.length > 255)
|
||||
throw new Error("Key must be less than 255 characters");
|
||||
if (key.length < 3) throw new Error("Key must be at least 3 characters");
|
||||
if (key.startsWith("user."))
|
||||
throw new Error("Key cannot start with 'user.'");
|
||||
if (key.startsWith("system."))
|
||||
throw new Error("Key cannot start with 'system.'");
|
||||
if (checkExisting && (await this.get(key)) !== null)
|
||||
throw new Error("System prompt variable with this key already exists");
|
||||
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = { SystemPromptVariables };
|
||||
18
server/prisma/migrations/20250318154720_init/migration.sql
Normal file
18
server/prisma/migrations/20250318154720_init/migration.sql
Normal file
@ -0,0 +1,18 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "system_prompt_variables" (
|
||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"key" TEXT NOT NULL,
|
||||
"value" TEXT,
|
||||
"description" TEXT,
|
||||
"type" TEXT NOT NULL DEFAULT 'system',
|
||||
"userId" INTEGER,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" DATETIME NOT NULL,
|
||||
CONSTRAINT "system_prompt_variables_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "system_prompt_variables_key_key" ON "system_prompt_variables"("key");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "system_prompt_variables_userId_idx" ON "system_prompt_variables"("userId");
|
||||
@ -80,6 +80,7 @@ model users {
|
||||
slash_command_presets slash_command_presets[]
|
||||
browser_extension_api_keys browser_extension_api_keys[]
|
||||
temporary_auth_tokens temporary_auth_tokens[]
|
||||
system_prompt_variables system_prompt_variables[]
|
||||
}
|
||||
|
||||
model recovery_codes {
|
||||
@ -326,3 +327,17 @@ model temporary_auth_tokens {
|
||||
@@index([token])
|
||||
@@index([userId])
|
||||
}
|
||||
|
||||
model system_prompt_variables {
|
||||
id Int @id @default(autoincrement())
|
||||
key String @unique
|
||||
value String?
|
||||
description String?
|
||||
type String @default("system") // system, user, dynamic
|
||||
userId Int?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
user users? @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([userId])
|
||||
}
|
||||
|
||||
@ -291,7 +291,7 @@ async function chatSync({
|
||||
// and build system messages based on inputs and history.
|
||||
const messages = await LLMConnector.compressMessages(
|
||||
{
|
||||
systemPrompt: chatPrompt(workspace),
|
||||
systemPrompt: await chatPrompt(workspace, user),
|
||||
userPrompt: message,
|
||||
contextTexts,
|
||||
chatHistory,
|
||||
@ -626,7 +626,7 @@ async function streamChat({
|
||||
// and build system messages based on inputs and history.
|
||||
const messages = await LLMConnector.compressMessages(
|
||||
{
|
||||
systemPrompt: chatPrompt(workspace),
|
||||
systemPrompt: await chatPrompt(workspace, user),
|
||||
userPrompt: message,
|
||||
contextTexts,
|
||||
chatHistory,
|
||||
|
||||
@ -152,7 +152,7 @@ async function streamChatWithForEmbed(
|
||||
// and build system messages based on inputs and history.
|
||||
const messages = await LLMConnector.compressMessages(
|
||||
{
|
||||
systemPrompt: chatPrompt(embed.workspace),
|
||||
systemPrompt: await chatPrompt(embed.workspace, username),
|
||||
userPrompt: message,
|
||||
contextTexts,
|
||||
chatHistory,
|
||||
|
||||
@ -3,6 +3,7 @@ const { WorkspaceChats } = require("../../models/workspaceChats");
|
||||
const { resetMemory } = require("./commands/reset");
|
||||
const { convertToPromptHistory } = require("../helpers/chat/responses");
|
||||
const { SlashCommandPresets } = require("../../models/slashCommandsPresets");
|
||||
const { SystemPromptVariables } = require("../../models/systemPromptVariables");
|
||||
|
||||
const VALID_COMMANDS = {
|
||||
"/reset": resetMemory,
|
||||
@ -80,10 +81,20 @@ async function recentChatHistory({
|
||||
return { rawHistory, chatHistory: convertToPromptHistory(rawHistory) };
|
||||
}
|
||||
|
||||
function chatPrompt(workspace) {
|
||||
return (
|
||||
/**
|
||||
* Returns the base prompt for the chat. This method will also do variable
|
||||
* substitution on the prompt if there are any defined variables in the prompt.
|
||||
* @param {Object|null} workspace - the workspace object
|
||||
* @param {Object|null} user - the user object
|
||||
* @returns {Promise<string>} - the base prompt
|
||||
*/
|
||||
async function chatPrompt(workspace, user = null) {
|
||||
const basePrompt =
|
||||
workspace?.openAiPrompt ??
|
||||
"Given the following conversation, relevant context, and a follow up question, reply with an answer to the current question the user is asking. Return only your response to the question given the above information following the users instructions as needed."
|
||||
"Given the following conversation, relevant context, and a follow up question, reply with an answer to the current question the user is asking. Return only your response to the question given the above information following the users instructions as needed.";
|
||||
return await SystemPromptVariables.expandSystemPromptVariables(
|
||||
basePrompt,
|
||||
user?.id
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -150,7 +150,7 @@ async function chatSync({
|
||||
// Compress & Assemble message to ensure prompt passes token limit with room for response
|
||||
// and build system messages based on inputs and history.
|
||||
const messages = await LLMConnector.compressMessages({
|
||||
systemPrompt: systemPrompt ?? chatPrompt(workspace),
|
||||
systemPrompt: systemPrompt ?? (await chatPrompt(workspace)),
|
||||
userPrompt: prompt,
|
||||
contextTexts,
|
||||
chatHistory: history,
|
||||
@ -374,7 +374,7 @@ async function streamChat({
|
||||
// Compress & Assemble message to ensure prompt passes token limit with room for response
|
||||
// and build system messages based on inputs and history.
|
||||
const messages = await LLMConnector.compressMessages({
|
||||
systemPrompt: systemPrompt ?? chatPrompt(workspace),
|
||||
systemPrompt: systemPrompt ?? (await chatPrompt(workspace)),
|
||||
userPrompt: prompt,
|
||||
contextTexts,
|
||||
chatHistory: history,
|
||||
|
||||
@ -213,7 +213,7 @@ async function streamChatWithWorkspace(
|
||||
// and build system messages based on inputs and history.
|
||||
const messages = await LLMConnector.compressMessages(
|
||||
{
|
||||
systemPrompt: chatPrompt(workspace),
|
||||
systemPrompt: await chatPrompt(workspace, user),
|
||||
userPrompt: updatedMessage,
|
||||
contextTexts,
|
||||
chatHistory,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user