Skip to content

Commit

Permalink
🤖 feat: Model Specs & Save Tools per Convo/Preset (#2578)
Browse files Browse the repository at this point in the history
* WIP: first pass ModelSpecs

* refactor(onSelectEndpoint): use `getConvoSwitchLogic`

* feat: introduce iconURL, greeting, frontend fields for conversations/presets/messages

* feat: conversation.iconURL & greeting in Landing

* feat: conversation.iconURL & greeting in New Chat button

* feat: message.iconURL

* refactor: ConversationIcon -> ConvoIconURL

* WIP: add spec as a conversation field

* refactor: useAppStartup, set spec on initial load for new chat, allow undefined spec, add localStorage keys enum, additional type fields for spec

* feat: handle `showIconInMenu`, `showIconInHeader`, undefined `iconURL` and no specs on initial load

* chore: handle undefined or empty modelSpecs

* WIP: first pass, modelSpec schema for custom config

* refactor: move default filtered tools definition to ToolService

* feat: pass modelSpecs from backend via startupConfig

* refactor: modelSpecs config, return and define list

* fix: react error and include iconURL in responseMessage

* refactor: add iconURL to responseMessage only

* refactor: getIconEndpoint

* refactor: pass TSpecsConfig

* fix(assistants): differentiate compactAssistantSchema, correctly resets shared conversation state with other endpoints

* refactor: assistant id prefix localStorage key

* refactor: add more LocalStorageKeys and replace hardcoded values

* feat: prioritize spec on new chat behavior: last selected modelSpec behavior (localStorage)

* feat: first pass, interface config

* chore: WIP, todo: add warnings based on config.modelSpecs settings.

* feat: enforce modelSpecs if configured

* feat: show config file yaml errors

* chore: delete unused legacy Plugins component

* refactor: set tools to localStorage from recoil store

* chore: add stable recoil setter to useEffect deps

* refactor: save tools to conversation documents

* style(MultiSelectPop): dynamic height, remove unused import

* refactor(react-query): use localstorage keys and pass config to useAvailablePluginsQuery

* feat(utils): add mapPlugins

* refactor(Convo): use conversation.tools if defined, lastSelectedTools if not

* refactor: remove unused legacy code using `useSetOptions`, remove conditional flag `isMultiChat` for using legacy settings

* refactor(PluginStoreDialog): add exhaustive-deps which are stable react state setters

* fix(HeaderOptions): pass `popover` as true

* refactor(useSetStorage): use project enums

* refactor: use LocalStorageKeys enum

* fix: prevent setConversation from setting falsy values in lastSelectedTools

* refactor: use map for availableTools state and available Plugins query

* refactor(updateLastSelectedModel): organize logic better and add note on purpose

* fix(setAgentOption): prevent reseting last model to secondary model for gptPlugins

* refactor(buildDefaultConvo): use enum

* refactor: remove `useSetStorage` and consolidate areas where conversation state is saved to localStorage

* fix: conversations retain tools on refresh

* fix(gptPlugins): prevent nullish tools from being saved

* chore: delete useServerStream

* refactor: move initial plugins logic to useAppStartup

* refactor(MultiSelectDropDown): add more pass-in className props

* feat: use tools in presets

* chore: delete unused usePresetOptions

* refactor: new agentOptions default handling

* chore: note

* feat: add label and custom instructions to agents

* chore: remove 'disabled with tools' message

* style: move plugins to 2nd column in parameters

* fix: TPreset type for agentOptions

* fix: interface controls

* refactor: add interfaceConfig, use Separator within Switcher

* refactor: hide Assistants panel if interface.parameters are disabled

* fix(Header): only modelSpecs if list is greater than 0

* refactor: separate MessageIcon logic from useMessageHelpers for better react rule-following

* fix(AppService): don't use reserved keyword 'interface'

* feat: set existing Icon for custom endpoints through iconURL

* fix(ci): tests passing for App Service

* docs: refactor custom_config.md for readability and better organization, also include missing values

* docs: interface section and re-organize docs

* docs: update modelSpecs info

* chore: remove unused files

* chore: remove unused files

* chore: move useSetIndexOptions

* chore: remove unused file

* chore: move useConversation(s)

* chore: move useDefaultConvo

* chore: move useNavigateToConvo

* refactor: use plugin install hook so it can be used elsewhere

* chore: import order

* update docs

* refactor(OpenAI/Plugins): allow modelLabel as an initial value for chatGptLabel

* chore: remove unused EndpointOptionsPopover and hide 'Save as Preset' button if preset UI visibility disabled

* feat(loadDefaultInterface): issue warnings based on values

* feat: changelog for custom config file

* docs: add additional changelog note

* fix: prevent unavailable tool selection from preset and update availableTools on Plugin installations

* feat: add `filteredTools` option in custom config

* chore: changelog

* fix(MessageIcon): always overwrite conversation.iconURL in messageSettings

* fix(ModelSpecsMenu): icon edge cases

* fix(NewChat): dynamic icon

* fix(PluginsClient): always include endpoint in responseMessage

* fix: always include endpoint and iconURL in responseMessage across different response methods

* feat: interchangeable keys for modelSpec enforcing
  • Loading branch information
danny-avila committed May 1, 2024
1 parent a5cac03 commit 0e50c07
Show file tree
Hide file tree
Showing 130 changed files with 3,910 additions and 2,949 deletions.
3 changes: 3 additions & 0 deletions api/app/clients/AnthropicClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -655,6 +655,9 @@ class AnthropicClient extends BaseClient {
promptPrefix: this.options.promptPrefix,
modelLabel: this.options.modelLabel,
resendFiles: this.options.resendFiles,
iconURL: this.options.iconURL,
greeting: this.options.greeting,
spec: this.options.spec,
...this.modelOptions,
};
}
Expand Down
15 changes: 14 additions & 1 deletion api/app/clients/BaseClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,8 @@ class BaseClient {
sender: this.sender,
text: addSpaceIfNeeded(generation) + completion,
promptTokens,
iconURL: this.options.iconURL,
endpoint: this.options.endpoint,
...(this.metadata ?? {}),
};

Expand Down Expand Up @@ -525,8 +527,19 @@ class BaseClient {
return _messages;
}

/**
* Save a message to the database.
* @param {TMessage} message
* @param {Partial<TConversation>} endpointOptions
* @param {string | null} user
*/
async saveMessageToDatabase(message, endpointOptions, user = null) {
await saveMessage({ ...message, endpoint: this.options.endpoint, user, unfinished: false });
await saveMessage({
...message,
endpoint: this.options.endpoint,
unfinished: false,
user,
});
await saveConvo(user, {
conversationId: message.conversationId,
endpoint: this.options.endpoint,
Expand Down
3 changes: 3 additions & 0 deletions api/app/clients/GoogleClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -708,6 +708,9 @@ class GoogleClient extends BaseClient {
return {
promptPrefix: this.options.promptPrefix,
modelLabel: this.options.modelLabel,
iconURL: this.options.iconURL,
greeting: this.options.greeting,
spec: this.options.spec,
...this.modelOptions,
};
}
Expand Down
3 changes: 3 additions & 0 deletions api/app/clients/OpenAIClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,9 @@ class OpenAIClient extends BaseClient {
promptPrefix: this.options.promptPrefix,
resendFiles: this.options.resendFiles,
imageDetail: this.options.imageDetail,
iconURL: this.options.iconURL,
greeting: this.options.greeting,
spec: this.options.spec,
...this.modelOptions,
};
}
Expand Down
10 changes: 9 additions & 1 deletion api/app/clients/PluginsClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,12 @@ class PluginsClient extends OpenAIClient {
return {
chatGptLabel: this.options.chatGptLabel,
promptPrefix: this.options.promptPrefix,
tools: this.options.tools,
...this.modelOptions,
agentOptions: this.agentOptions,
iconURL: this.options.iconURL,
greeting: this.options.greeting,
spec: this.options.spec,
};
}

Expand Down Expand Up @@ -144,9 +148,11 @@ class PluginsClient extends OpenAIClient {
signal,
pastMessages,
tools: this.tools,
currentDateString: this.currentDateString,
verbose: this.options.debug,
returnIntermediateSteps: true,
customName: this.options.chatGptLabel,
currentDateString: this.currentDateString,
customInstructions: this.options.promptPrefix,
callbackManager: CallbackManager.fromHandlers({
async handleAgentAction(action, runId) {
handleAction(action, runId, onAgentAction);
Expand Down Expand Up @@ -304,6 +310,8 @@ class PluginsClient extends OpenAIClient {
}

const responseMessage = {
endpoint: EModelEndpoint.gptPlugins,
iconURL: this.options.iconURL,
messageId: responseMessageId,
conversationId,
parentMessageId: userMessage.messageId,
Expand Down
8 changes: 8 additions & 0 deletions api/app/clients/agents/CustomAgent/initializeCustomAgent.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,18 @@ const initializeCustomAgent = async ({
tools,
model,
pastMessages,
customName,
customInstructions,
currentDateString,
...rest
}) => {
let prompt = CustomAgent.createPrompt(tools, { currentDateString, model: model.modelName });
if (customName) {
prompt = `You are "${customName}".\n${prompt}`;
}
if (customInstructions) {
prompt = `${prompt}\n${customInstructions}`;
}

const chatPrompt = ChatPromptTemplate.fromMessages([
new SystemMessagePromptTemplate(prompt),
Expand Down
10 changes: 9 additions & 1 deletion api/app/clients/agents/Functions/initializeFunctionsAgent.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ const initializeFunctionsAgent = async ({
tools,
model,
pastMessages,
customName,
customInstructions,
currentDateString,
...rest
}) => {
Expand All @@ -24,7 +26,13 @@ const initializeFunctionsAgent = async ({
returnMessages: true,
});

const prefix = addToolDescriptions(`Current Date: ${currentDateString}\n${PREFIX}`, tools);
let prefix = addToolDescriptions(`Current Date: ${currentDateString}\n${PREFIX}`, tools);
if (customName) {
prefix = `You are "${customName}".\n${prefix}`;
}
if (customInstructions) {
prefix = `${prefix}\n${customInstructions}`;
}

return await initializeAgentExecutorWithOptions(tools, model, {
agentType: 'openai-functions',
Expand Down
2 changes: 2 additions & 0 deletions api/models/Message.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ module.exports = {
async saveMessage({
user,
endpoint,
iconURL,
messageId,
newMessageId,
conversationId,
Expand All @@ -35,6 +36,7 @@ module.exports = {

const update = {
user,
iconURL,
endpoint,
messageId: newMessageId || messageId,
conversationId,
Expand Down
6 changes: 6 additions & 0 deletions api/models/Preset.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ module.exports = {
try {
const setter = { $set: {} };
const update = { presetId, ...preset };
if (preset.tools && Array.isArray(preset.tools)) {
update.tools =
preset.tools
.map((tool) => tool?.pluginKey ?? tool)
.filter((toolName) => typeof toolName === 'string') ?? [];
}
if (newPresetId) {
update.presetId = newPresetId;
}
Expand Down
11 changes: 11 additions & 0 deletions api/models/schema/defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,17 @@ const conversationPreset = {
type: String,
},
stop: { type: [{ type: String }], default: undefined },
/* UI Components */
iconURL: {
type: String,
},
greeting: {
type: String,
},
spec: {
type: String,
},
tools: { type: [{ type: String }], default: undefined },
};

const agentOptions = {
Expand Down
4 changes: 4 additions & 0 deletions api/models/schema/messageSchema.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,10 @@ const messageSchema = mongoose.Schema(
thread_id: {
type: String,
},
/* frontend components */
iconURL: {
type: String,
},
},
{ timestamps: true },
);
Expand Down
8 changes: 7 additions & 1 deletion api/server/controllers/PluginController.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ const getAvailablePluginsController = async (req, res) => {
return;
}

/** @type {{ filteredTools: string[] }} */
const { filteredTools = [] } = req.app.locals;

const pluginManifest = await fs.readFile(req.app.locals.paths.pluginManifest, 'utf8');

const jsonData = JSON.parse(pluginManifest);
Expand All @@ -67,7 +70,10 @@ const getAvailablePluginsController = async (req, res) => {
return plugin;
}
});
const plugins = await addOpenAPISpecs(authenticatedPlugins);

let plugins = await addOpenAPISpecs(authenticatedPlugins);
plugins = plugins.filter((plugin) => !filteredTools.includes(plugin.pluginKey));

await cache.set(CacheKeys.PLUGINS, plugins);
res.status(200).json(plugins);
} catch (error) {
Expand Down
2 changes: 2 additions & 0 deletions api/server/middleware/abortMiddleware.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ const createAbortController = (req, res, getAbortData) => {
...responseData,
conversationId,
finish_reason: 'incomplete',
endpoint: endpointOption.endpoint,
iconURL: endpointOption.iconURL,
model: endpointOption.modelOptions.model,
unfinished: false,
error: false,
Expand Down
27 changes: 27 additions & 0 deletions api/server/middleware/buildEndpointOption.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ const anthropic = require('~/server/services/Endpoints/anthropic');
const openAI = require('~/server/services/Endpoints/openAI');
const custom = require('~/server/services/Endpoints/custom');
const google = require('~/server/services/Endpoints/google');
const enforceModelSpec = require('./enforceModelSpec');
const { handleError } = require('~/server/utils');

const buildFunction = {
[EModelEndpoint.openAI]: openAI.buildOptions,
Expand All @@ -21,6 +23,31 @@ const buildFunction = {
async function buildEndpointOption(req, res, next) {
const { endpoint, endpointType } = req.body;
const parsedBody = parseConvo({ endpoint, endpointType, conversation: req.body });

if (req.app.locals.modelSpecs?.list && req.app.locals.modelSpecs?.enforce) {
/** @type {{ list: TModelSpec[] }}*/
const { list } = req.app.locals.modelSpecs;
const { spec } = parsedBody;

if (!spec) {
return handleError(res, { text: 'No model spec selected' });
}

const currentModelSpec = list.find((s) => s.name === spec);
if (!currentModelSpec) {
return handleError(res, { text: 'Invalid model spec' });
}

if (endpoint !== currentModelSpec.preset.endpoint) {
return handleError(res, { text: 'Model spec mismatch' });
}

const isValidModelSpec = enforceModelSpec(currentModelSpec, parsedBody);
if (!isValidModelSpec) {
return handleError(res, { text: 'Model spec mismatch' });
}
}

req.body.endpointOption = buildFunction[endpointType ?? endpoint](
endpoint,
parsedBody,
Expand Down
47 changes: 47 additions & 0 deletions api/server/middleware/enforceModelSpec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
const interchangeableKeys = new Map([
['chatGptLabel', ['modelLabel']],
['modelLabel', ['chatGptLabel']],
]);

/**
* Middleware to enforce the model spec for a conversation
* @param {TModelSpec} modelSpec - The model spec to enforce
* @param {TConversation} parsedBody - The parsed body of the conversation
* @returns {boolean} - Whether the model spec is enforced
*/
const enforceModelSpec = (modelSpec, parsedBody) => {
for (const [key, value] of Object.entries(modelSpec.preset)) {
if (key === 'endpoint') {
continue;
}

if (!checkMatch(key, value, parsedBody)) {
return false;
}
}
return true;
};

/**
* Checks if there is a match for the given key and value in the parsed body
* or any of its interchangeable keys.
* @param {string} key
* @param {any} value
* @param {TConversation} parsedBody
* @returns {boolean}
*/
const checkMatch = (key, value, parsedBody) => {
if (parsedBody[key] === value) {
return true;
}

if (interchangeableKeys.has(key)) {
return interchangeableKeys
.get(key)
.some((interchangeableKey) => parsedBody[interchangeableKey] === value);
}

return false;
};

module.exports = enforceModelSpec;
4 changes: 3 additions & 1 deletion api/server/routes/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ router.get('/', async function (req, res) {
};

try {
/** @type {TStartupConfig} */
const payload = {
appTitle: process.env.APP_TITLE || 'LibreChat',
socialLogins: req.app.locals.socialLogins ?? defaultSocialLogins,
Expand Down Expand Up @@ -44,7 +45,8 @@ router.get('/', async function (req, res) {
isEnabled(process.env.SHOW_BIRTHDAY_ICON) ||
process.env.SHOW_BIRTHDAY_ICON === '',
helpAndFaqURL: process.env.HELP_AND_FAQ_URL || 'https://librechat.ai',
interface: req.app.locals.interface,
interface: req.app.locals.interfaceConfig,
modelSpecs: req.app.locals.modelSpecs,
};

if (typeof process.env.CUSTOM_FOOTER === 'string') {
Expand Down
Loading

0 comments on commit 0e50c07

Please sign in to comment.