Skip to content

Commit

Permalink
🗨️ feat: Prompt Slash Commands (#3219)
Browse files Browse the repository at this point in the history
* chore: Update prompt description placeholder text

* fix: promptsPathPattern to not include new

* feat: command input and styling change for prompt views

* fix: intended validation

* feat: prompts slash command

* chore: localizations and fix add command during creation

* refactor(PromptsCommand): better label

* feat: update `allPrompGroups` cache on all promptGroups mutations

* refactor: ensure assistants builder is first within sidepanel

* refactor: allow defining emailVerified via create-user script
  • Loading branch information
danny-avila authored Jun 27, 2024
1 parent b8f2bee commit 83619de
Show file tree
Hide file tree
Showing 33 changed files with 764 additions and 80 deletions.
89 changes: 89 additions & 0 deletions api/models/Prompt.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,94 @@ const createGroupPipeline = (query, skip, limit) => {
];
};

/**
* Create a pipeline for the aggregation to get all prompt groups
* @param {Object} query
* @param {Partial<MongoPromptGroup>} $project
* @returns {[Object]} - The pipeline for the aggregation
*/
const createAllGroupsPipeline = (
query,
$project = {
name: 1,
oneliner: 1,
category: 1,
author: 1,
authorName: 1,
createdAt: 1,
updatedAt: 1,
command: 1,
'productionPrompt.prompt': 1,
},
) => {
return [
{ $match: query },
{ $sort: { createdAt: -1 } },
{
$lookup: {
from: 'prompts',
localField: 'productionId',
foreignField: '_id',
as: 'productionPrompt',
},
},
{ $unwind: { path: '$productionPrompt', preserveNullAndEmptyArrays: true } },
{
$project,
},
];
};

/**
* Get all prompt groups with filters
* @param {Object} req
* @param {TPromptGroupsWithFilterRequest} filter
* @returns {Promise<PromptGroupListResponse>}
*/
const getAllPromptGroups = async (req, filter) => {
try {
const { name, ...query } = filter;

if (!query.author) {
throw new Error('Author is required');
}

let searchShared = true;
let searchSharedOnly = false;
if (name) {
query.name = new RegExp(name, 'i');
}
if (!query.category) {
delete query.category;
} else if (query.category === SystemCategories.MY_PROMPTS) {
searchShared = false;
delete query.category;
} else if (query.category === SystemCategories.NO_CATEGORY) {
query.category = '';
} else if (query.category === SystemCategories.SHARED_PROMPTS) {
searchSharedOnly = true;
delete query.category;
}

let combinedQuery = query;

if (searchShared) {
const project = await getProjectByName('instance', 'promptGroupIds');
if (project && project.promptGroupIds.length > 0) {
const projectQuery = { _id: { $in: project.promptGroupIds }, ...query };
delete projectQuery.author;
combinedQuery = searchSharedOnly ? projectQuery : { $or: [projectQuery, query] };
}
}

const promptGroupsPipeline = createAllGroupsPipeline(combinedQuery);
return await PromptGroup.aggregate(promptGroupsPipeline).exec();
} catch (error) {
console.error('Error getting all prompt groups', error);
return { message: 'Error getting all prompt groups' };
}
};

/**
* Get prompt groups with filters
* @param {Object} req
Expand Down Expand Up @@ -126,6 +214,7 @@ const getPromptGroups = async (req, filter) => {

module.exports = {
getPromptGroups,
getAllPromptGroups,
/**
* Create a prompt and its respective group
* @param {TCreatePromptRecord} saveData
Expand Down
17 changes: 17 additions & 0 deletions api/models/schema/promptSchema.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const mongoose = require('mongoose');
const { Constants } = require('librechat-data-provider');
const Schema = mongoose.Schema;

/**
Expand All @@ -12,6 +13,7 @@ const Schema = mongoose.Schema;
* @property {number} [numberOfGenerations=0] - Number of generations the prompt group has
* @property {string} [oneliner=''] - Oneliner description of the prompt group
* @property {string} [category=''] - Category of the prompt group
* @property {string} [command] - Command for the prompt group
* @property {Date} [createdAt] - Date when the prompt group was created (added by timestamps)
* @property {Date} [updatedAt] - Date when the prompt group was last updated (added by timestamps)
*/
Expand Down Expand Up @@ -57,6 +59,21 @@ const promptGroupSchema = new Schema(
type: String,
required: true,
},
command: {
type: String,
index: true,
validate: {
validator: function (v) {
return v === undefined || v === null || v === '' || /^[a-z0-9-]+$/.test(v);
},
message: (props) =>
`${props.value} is not a valid command. Only lowercase alphanumeric characters and highfins (') are allowed.`,
},
maxlength: [
Constants.COMMANDS_MAX_LENGTH,
`Command cannot be longer than ${Constants.COMMANDS_MAX_LENGTH} characters`,
],
},
},
{
timestamps: true,
Expand Down
17 changes: 17 additions & 0 deletions api/server/routes/prompts.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const {
updatePromptGroup,
deletePromptGroup,
createPromptGroup,
getAllPromptGroups,
// updatePromptLabels,
makePromptProduction,
} = require('~/models/Prompt');
Expand Down Expand Up @@ -65,6 +66,22 @@ router.get('/groups/:groupId', async (req, res) => {
}
});

/**
* Route to fetch all prompt groups
* GET /groups
*/
router.get('/all', async (req, res) => {
try {
const groups = await getAllPromptGroups(req, {
author: req.user._id,
});
res.status(200).send(groups);
} catch (error) {
logger.error(error);
res.status(500).send({ error: 'Error getting prompt groups' });
}
});

/**
* Route to fetch paginated prompt groups with filters
* GET /groups
Expand Down
17 changes: 12 additions & 5 deletions api/server/services/AuthService.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,9 @@ const sendVerificationEmail = async (user) => {
let verifyToken = crypto.randomBytes(32).toString('hex');
const hash = bcrypt.hashSync(verifyToken, 10);

const verificationLink = `${domains.client}/verify?token=${verifyToken}&email=${encodeURIComponent(user.email)}`;
const verificationLink = `${
domains.client
}/verify?token=${verifyToken}&email=${encodeURIComponent(user.email)}`;
await sendEmail({
email: user.email,
subject: 'Verify your email',
Expand Down Expand Up @@ -119,9 +121,10 @@ const verifyEmail = async (req) => {
/**
* Register a new user.
* @param {MongoUser} user <email, password, name, username>
* @param {Partial<MongoUser>} [additionalData={}]
* @returns {Promise<{status: number, message: string, user?: MongoUser}>}
*/
const registerUser = async (user) => {
const registerUser = async (user, additionalData = {}) => {
const { error } = registerSchema.safeParse(user);
if (error) {
const errorMessage = errorsToString(error.errors);
Expand Down Expand Up @@ -171,11 +174,13 @@ const registerUser = async (user) => {
avatar: null,
role: isFirstRegisteredUser ? SystemRoles.ADMIN : SystemRoles.USER,
password: bcrypt.hashSync(password, salt),
...additionalData,
};

const emailEnabled = checkEmailConfig();
newUserId = await createUser(newUserData, false);
if (emailEnabled) {
const newUser = await createUser(newUserData, false, true);
newUserId = newUser._id;
if (emailEnabled && !newUser.emailVerified) {
await sendVerificationEmail({
_id: newUserId,
email,
Expand Down Expand Up @@ -363,7 +368,9 @@ const resendVerificationEmail = async (req) => {
let verifyToken = crypto.randomBytes(32).toString('hex');
const hash = bcrypt.hashSync(verifyToken, 10);

const verificationLink = `${domains.client}/verify?token=${verifyToken}&email=${encodeURIComponent(user.email)}`;
const verificationLink = `${
domains.client
}/verify?token=${verifyToken}&email=${encodeURIComponent(user.email)}`;

await sendEmail({
email: user.email,
Expand Down
3 changes: 3 additions & 0 deletions client/src/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,9 @@ export type MentionOption = OptionWithIcon & {
value: string;
description?: string;
};
export type PromptOption = MentionOption & {
id: string;
};

export type TOptionSettings = {
showExamples?: boolean;
Expand Down
11 changes: 9 additions & 2 deletions client/src/components/Chat/Input/ChatForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { TextareaAutosize } from '~/components/ui';
import { useGetFileConfig } from '~/data-provider';
import { cn, removeFocusRings } from '~/utils';
import TextareaHeader from './TextareaHeader';
import PromptsCommand from './PromptsCommand';
import AttachFile from './Files/AttachFile';
import AudioRecorder from './AudioRecorder';
import { mainTextareaId } from '~/common';
Expand All @@ -48,7 +49,12 @@ const ChatForm = ({ index = 0 }) => {
);

const { requiresKey } = useRequiresKey();
const handleKeyUp = useHandleKeyUp({ textAreaRef, setShowPlusPopover, setShowMentionPopover });
const handleKeyUp = useHandleKeyUp({
index,
textAreaRef,
setShowPlusPopover,
setShowMentionPopover,
});
const { handlePaste, handleKeyDown, handleCompositionStart, handleCompositionEnd } = useTextarea({
textAreaRef,
submitButtonRef,
Expand Down Expand Up @@ -83,7 +89,7 @@ const ChatForm = ({ index = 0 }) => {
});

const assistantMap = useAssistantsMapContext();
const { submitMessage } = useSubmitMessage({ clearDraft });
const { submitMessage, submitPrompt } = useSubmitMessage({ clearDraft });

const { endpoint: _endpoint, endpointType } = conversation ?? { endpoint: null };
const endpoint = endpointType ?? _endpoint;
Expand Down Expand Up @@ -136,6 +142,7 @@ const ChatForm = ({ index = 0 }) => {
textAreaRef={textAreaRef}
/>
)}
<PromptsCommand index={index} textAreaRef={textAreaRef} submitPrompt={submitPrompt} />
<div className="bg-token-main-surface-primary relative flex w-full flex-grow flex-col overflow-hidden rounded-2xl border dark:border-gray-600 dark:text-white [&:has(textarea:focus)]:border-gray-300 [&:has(textarea:focus)]:shadow-[0_2px_6px_rgba(0,0,0,.05)] dark:[&:has(textarea:focus)]:border-gray-500">
<TextareaHeader addedConvo={addedConvo} setAddedConvo={setAddedConvo} />
<FileRow
Expand Down
Loading

0 comments on commit 83619de

Please sign in to comment.