mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-01-06 18:48:50 +01:00
🤖 feat: Model Specs & Save Tools per Convo/Preset (#2578)
* 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
This commit is contained in:
parent
a5cac03fa4
commit
0e50c07e3f
130 changed files with 3934 additions and 2973 deletions
|
|
@ -1,6 +1,6 @@
|
|||
import { parseConvo } from 'librechat-data-provider';
|
||||
import { parseConvo, EModelEndpoint } from 'librechat-data-provider';
|
||||
import type { TConversation } from 'librechat-data-provider';
|
||||
import getLocalStorageItems from './getLocalStorageItems';
|
||||
import type { TConversation, EModelEndpoint } from 'librechat-data-provider';
|
||||
|
||||
const buildDefaultConvo = ({
|
||||
conversation,
|
||||
|
|
@ -29,7 +29,7 @@ const buildDefaultConvo = ({
|
|||
const availableModels = models;
|
||||
const model = lastConversationSetup?.model ?? lastSelectedModel?.[endpoint];
|
||||
const secondaryModel =
|
||||
endpoint === 'gptPlugins'
|
||||
endpoint === EModelEndpoint.gptPlugins
|
||||
? lastConversationSetup?.agentOptions?.model ?? lastSelectedModel?.secondaryModel
|
||||
: null;
|
||||
|
||||
|
|
@ -64,7 +64,7 @@ const buildDefaultConvo = ({
|
|||
endpoint,
|
||||
};
|
||||
|
||||
defaultConvo.tools = lastSelectedTools ?? defaultConvo.tools;
|
||||
defaultConvo.tools = lastConversationSetup?.tools ?? lastSelectedTools ?? defaultConvo.tools;
|
||||
defaultConvo.jailbreak = jailbreak ?? defaultConvo.jailbreak;
|
||||
defaultConvo.toneStyle = toneStyle ?? defaultConvo.toneStyle;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
import {
|
||||
parseISO,
|
||||
format,
|
||||
isToday,
|
||||
isWithinInterval,
|
||||
subDays,
|
||||
getYear,
|
||||
parseISO,
|
||||
startOfDay,
|
||||
startOfYear,
|
||||
format,
|
||||
isWithinInterval,
|
||||
} from 'date-fns';
|
||||
import { EModelEndpoint, LocalStorageKeys } from 'librechat-data-provider';
|
||||
import type {
|
||||
TConversation,
|
||||
ConversationData,
|
||||
|
|
@ -182,3 +183,29 @@ export const getConversationById = (
|
|||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export function storeEndpointSettings(conversation: TConversation | null) {
|
||||
if (!conversation) {
|
||||
return;
|
||||
}
|
||||
const { endpoint, model, agentOptions, jailbreak, toneStyle } = conversation;
|
||||
|
||||
if (!endpoint) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (endpoint === EModelEndpoint.bingAI) {
|
||||
const settings = { jailbreak, toneStyle };
|
||||
localStorage.setItem(LocalStorageKeys.LAST_BING, JSON.stringify(settings));
|
||||
return;
|
||||
}
|
||||
|
||||
const lastModel = JSON.parse(localStorage.getItem(LocalStorageKeys.LAST_MODEL) || '{}');
|
||||
lastModel[endpoint] = model;
|
||||
|
||||
if (endpoint === EModelEndpoint.gptPlugins) {
|
||||
lastModel.secondaryModel = agentOptions?.model || model || '';
|
||||
}
|
||||
|
||||
localStorage.setItem(LocalStorageKeys.LAST_MODEL, JSON.stringify(lastModel));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,16 @@
|
|||
import { defaultEndpoints } from 'librechat-data-provider';
|
||||
import type { EModelEndpoint, TEndpointsConfig, TConfig } from 'librechat-data-provider';
|
||||
import {
|
||||
EModelEndpoint,
|
||||
defaultEndpoints,
|
||||
modularEndpoints,
|
||||
LocalStorageKeys,
|
||||
} from 'librechat-data-provider';
|
||||
import type {
|
||||
TConfig,
|
||||
TPreset,
|
||||
TModelSpec,
|
||||
TConversation,
|
||||
TEndpointsConfig,
|
||||
} from 'librechat-data-provider';
|
||||
import type { LocalizeFunction } from '~/common';
|
||||
|
||||
export const getAssistantName = ({
|
||||
|
|
@ -48,6 +59,7 @@ export const getAvailableEndpoints = (
|
|||
return availableEndpoints;
|
||||
};
|
||||
|
||||
/** Get the specified field from the endpoint config */
|
||||
export function getEndpointField<K extends keyof TConfig>(
|
||||
endpointsConfig: TEndpointsConfig | undefined,
|
||||
endpoint: EModelEndpoint | string | null | undefined,
|
||||
|
|
@ -72,6 +84,9 @@ export function mapEndpoints(endpointsConfig: TEndpointsConfig) {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures the last selected model stays up to date, as conversation may
|
||||
* update without updating last convo setup when same endpoint */
|
||||
export function updateLastSelectedModel({
|
||||
endpoint,
|
||||
model,
|
||||
|
|
@ -82,12 +97,133 @@ export function updateLastSelectedModel({
|
|||
if (!model) {
|
||||
return;
|
||||
}
|
||||
const lastConversationSetup = JSON.parse(localStorage.getItem('lastConversationSetup') || '{}');
|
||||
const lastSelectedModels = JSON.parse(localStorage.getItem('lastSelectedModel') || '{}');
|
||||
const lastConversationSetup = JSON.parse(
|
||||
localStorage.getItem(LocalStorageKeys.LAST_CONVO_SETUP) || '{}',
|
||||
);
|
||||
|
||||
if (lastConversationSetup.endpoint === endpoint) {
|
||||
lastConversationSetup.model = model;
|
||||
localStorage.setItem('lastConversationSetup', JSON.stringify(lastConversationSetup));
|
||||
localStorage.setItem(LocalStorageKeys.LAST_CONVO_SETUP, JSON.stringify(lastConversationSetup));
|
||||
}
|
||||
|
||||
const lastSelectedModels = JSON.parse(localStorage.getItem(LocalStorageKeys.LAST_MODEL) || '{}');
|
||||
lastSelectedModels[endpoint] = model;
|
||||
localStorage.setItem('lastSelectedModel', JSON.stringify(lastSelectedModels));
|
||||
localStorage.setItem(LocalStorageKeys.LAST_MODEL, JSON.stringify(lastSelectedModels));
|
||||
}
|
||||
|
||||
interface ConversationInitParams {
|
||||
conversation: TConversation | null;
|
||||
newEndpoint: EModelEndpoint | string;
|
||||
endpointsConfig: TEndpointsConfig;
|
||||
modularChat?: boolean;
|
||||
}
|
||||
|
||||
interface InitiatedTemplateResult {
|
||||
template: Partial<TPreset>;
|
||||
shouldSwitch: boolean;
|
||||
isExistingConversation: boolean;
|
||||
isCurrentModular: boolean;
|
||||
isNewModular: boolean;
|
||||
newEndpointType: EModelEndpoint | undefined;
|
||||
}
|
||||
|
||||
/** Get the conditional logic for switching conversations */
|
||||
export function getConvoSwitchLogic(params: ConversationInitParams): InitiatedTemplateResult {
|
||||
const { conversation, newEndpoint, endpointsConfig, modularChat } = params;
|
||||
|
||||
const currentEndpoint = conversation?.endpoint;
|
||||
const template: Partial<TPreset> = {
|
||||
...conversation,
|
||||
endpoint: newEndpoint,
|
||||
conversationId: 'new',
|
||||
};
|
||||
|
||||
const isAssistantSwitch =
|
||||
newEndpoint === EModelEndpoint.assistants &&
|
||||
currentEndpoint === EModelEndpoint.assistants &&
|
||||
currentEndpoint === newEndpoint;
|
||||
|
||||
const conversationId = conversation?.conversationId;
|
||||
const isExistingConversation = !!(conversationId && conversationId !== 'new');
|
||||
|
||||
const currentEndpointType =
|
||||
getEndpointField(endpointsConfig, currentEndpoint, 'type') ?? currentEndpoint;
|
||||
const newEndpointType =
|
||||
getEndpointField(endpointsConfig, newEndpoint, 'type') ??
|
||||
(newEndpoint as EModelEndpoint | undefined);
|
||||
|
||||
const hasEndpoint = modularEndpoints.has(currentEndpoint ?? '');
|
||||
const hasCurrentEndpointType = modularEndpoints.has(currentEndpointType ?? '');
|
||||
const isCurrentModular = hasEndpoint || hasCurrentEndpointType || isAssistantSwitch;
|
||||
|
||||
const hasNewEndpoint = modularEndpoints.has(newEndpoint ?? '');
|
||||
const hasNewEndpointType = modularEndpoints.has(newEndpointType ?? '');
|
||||
const isNewModular = hasNewEndpoint || hasNewEndpointType || isAssistantSwitch;
|
||||
|
||||
const endpointsMatch = currentEndpoint === newEndpoint;
|
||||
const shouldSwitch = endpointsMatch || modularChat || isAssistantSwitch;
|
||||
|
||||
return {
|
||||
template,
|
||||
shouldSwitch,
|
||||
isExistingConversation,
|
||||
isCurrentModular,
|
||||
newEndpointType,
|
||||
isNewModular,
|
||||
};
|
||||
}
|
||||
|
||||
/** Gets the default spec by order.
|
||||
*
|
||||
* First, the admin defined default, then last selected spec, followed by first spec
|
||||
*/
|
||||
export function getDefaultModelSpec(modelSpecs?: TModelSpec[]) {
|
||||
const defaultSpec = modelSpecs?.find((spec) => spec.default);
|
||||
const lastSelectedSpecName = localStorage.getItem(LocalStorageKeys.LAST_SPEC);
|
||||
const lastSelectedSpec = modelSpecs?.find((spec) => spec.name === lastSelectedSpecName);
|
||||
return defaultSpec || lastSelectedSpec || modelSpecs?.[0];
|
||||
}
|
||||
|
||||
/** Gets the default spec iconURL by order or definition.
|
||||
*
|
||||
* First, the admin defined default, then last selected spec, followed by first spec
|
||||
*/
|
||||
export function getModelSpecIconURL(modelSpec: TModelSpec) {
|
||||
return modelSpec.iconURL ?? modelSpec.preset.iconURL ?? modelSpec.preset.endpoint ?? '';
|
||||
}
|
||||
|
||||
/** Gets the default frontend-facing endpoint, dependent on iconURL definition.
|
||||
*
|
||||
* If the iconURL is defined in the endpoint config, use it, otherwise use the endpoint
|
||||
*/
|
||||
export function getIconEndpoint({
|
||||
endpointsConfig,
|
||||
iconURL,
|
||||
endpoint,
|
||||
}: {
|
||||
endpointsConfig: TEndpointsConfig | undefined;
|
||||
iconURL: string | undefined;
|
||||
endpoint: string | null | undefined;
|
||||
}) {
|
||||
return (endpointsConfig?.[iconURL ?? ''] ? iconURL ?? endpoint : endpoint) ?? '';
|
||||
}
|
||||
|
||||
/** Gets the key to use for the default endpoint iconURL, as defined by the custom config */
|
||||
export function getIconKey({
|
||||
endpoint,
|
||||
endpointType: _eType,
|
||||
endpointsConfig,
|
||||
endpointIconURL: iconURL,
|
||||
}: {
|
||||
endpoint?: string | null;
|
||||
endpointsConfig?: TEndpointsConfig | undefined;
|
||||
endpointType?: string | null;
|
||||
endpointIconURL?: string;
|
||||
}) {
|
||||
const endpointType = _eType ?? getEndpointField(endpointsConfig, endpoint, 'type');
|
||||
const endpointIconURL = iconURL ?? getEndpointField(endpointsConfig, endpoint, 'iconURL');
|
||||
if (endpointIconURL && EModelEndpoint[endpointIconURL]) {
|
||||
return endpointIconURL;
|
||||
}
|
||||
return endpointType ? 'unknown' : endpoint ?? 'unknown';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
import { LocalStorageKeys } from 'librechat-data-provider';
|
||||
|
||||
export default function getLocalStorageItems() {
|
||||
const items = {
|
||||
lastSelectedModel: localStorage.getItem('lastSelectedModel'),
|
||||
lastSelectedTools: localStorage.getItem('lastSelectedTools'),
|
||||
lastBingSettings: localStorage.getItem('lastBingSettings'),
|
||||
lastConversationSetup: localStorage.getItem('lastConversationSetup'),
|
||||
lastSelectedModel: localStorage.getItem(LocalStorageKeys.LAST_MODEL),
|
||||
lastSelectedTools: localStorage.getItem(LocalStorageKeys.LAST_TOOLS),
|
||||
lastBingSettings: localStorage.getItem(LocalStorageKeys.LAST_BING),
|
||||
lastConversationSetup: localStorage.getItem(LocalStorageKeys.LAST_CONVO_SETUP),
|
||||
};
|
||||
|
||||
const lastSelectedModel = items.lastSelectedModel ? JSON.parse(items.lastSelectedModel) : {};
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import type { TFile, Assistant } from 'librechat-data-provider';
|
||||
import type { TFile, Assistant, TPlugin } from 'librechat-data-provider';
|
||||
import type { TPluginMap } from '~/common';
|
||||
|
||||
/** Maps Files by `file_id` for quick lookup */
|
||||
export function mapFiles(files: TFile[]) {
|
||||
|
|
@ -21,3 +22,43 @@ export function mapAssistants(assistants: Assistant[]) {
|
|||
|
||||
return assistantMap;
|
||||
}
|
||||
|
||||
/** Maps Plugins by `pluginKey` for quick lookup */
|
||||
export function mapPlugins(plugins: TPlugin[]): TPluginMap {
|
||||
return plugins.reduce((acc, plugin) => {
|
||||
acc[plugin.pluginKey] = plugin;
|
||||
return acc;
|
||||
}, {} as TPluginMap);
|
||||
}
|
||||
|
||||
/** Transform query data to object with list and map fields */
|
||||
export const selectPlugins = (
|
||||
data: TPlugin[] | undefined,
|
||||
): {
|
||||
list: TPlugin[];
|
||||
map: TPluginMap;
|
||||
} => {
|
||||
if (!data) {
|
||||
return {
|
||||
list: [],
|
||||
map: {},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
list: data,
|
||||
map: mapPlugins(data),
|
||||
};
|
||||
};
|
||||
|
||||
/** Transform array to TPlugin values */
|
||||
export function processPlugins(tools: (string | TPlugin)[], allPlugins?: TPluginMap): TPlugin[] {
|
||||
return tools
|
||||
.map((tool: string | TPlugin) => {
|
||||
if (typeof tool === 'string') {
|
||||
return allPlugins?.[tool];
|
||||
}
|
||||
return tool;
|
||||
})
|
||||
.filter((tool: TPlugin | undefined): tool is TPlugin => tool !== undefined);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { TPreset } from 'librechat-data-provider';
|
||||
import type { TPreset, TPlugin } from 'librechat-data-provider';
|
||||
import { EModelEndpoint } from 'librechat-data-provider';
|
||||
|
||||
export const getPresetIcon = (preset: TPreset, Icon) => {
|
||||
|
|
@ -53,3 +53,33 @@ export const getPresetTitle = (preset: TPreset) => {
|
|||
|
||||
return `${title}${modelInfo}${label ? ` (${label})` : ''}`.trim();
|
||||
};
|
||||
|
||||
/** Remove unavailable tools from the preset */
|
||||
export const removeUnavailableTools = (
|
||||
preset: TPreset,
|
||||
availableTools: Record<string, TPlugin>,
|
||||
) => {
|
||||
const newPreset = { ...preset };
|
||||
|
||||
if (newPreset.tools && newPreset.tools.length > 0) {
|
||||
newPreset.tools = newPreset.tools
|
||||
.filter((tool) => {
|
||||
let pluginKey: string;
|
||||
if (typeof tool === 'string') {
|
||||
pluginKey = tool;
|
||||
} else {
|
||||
({ pluginKey } = tool);
|
||||
}
|
||||
|
||||
return !!availableTools[pluginKey];
|
||||
})
|
||||
.map((tool) => {
|
||||
if (typeof tool === 'string') {
|
||||
return tool;
|
||||
}
|
||||
return tool.pluginKey;
|
||||
});
|
||||
}
|
||||
|
||||
return newPreset;
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue