mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-17 00:40:14 +01:00
📛 feat: Chat Badges via Model Specs (#10272)
* refactor: remove `useChatContext` from `useSelectMention`, explicitly pass `conversation` object * feat: ephemeral agents via model specs * refactor: Sync Jotai state with ephemeral agent state, also when Ephemeral Agent has no MCP servers selected * refactor: move `useUpdateEphemeralAgent` to store and clean up imports * refactor: reorder imports and invalidate queries for mcpConnectionStatus in event handler * refactor: replace useApplyModelSpecEffects with useApplyModelSpecAgents and update event handlers to use new agent template logic * ci: update useMCPSelect test to verify mcpValues sync with empty ephemeralAgent.mcp
This commit is contained in:
parent
64df54528d
commit
33d6b337bc
17 changed files with 254 additions and 41 deletions
|
|
@ -62,25 +62,37 @@ const getAgents = async (searchParameter) => await Agent.find(searchParameter).l
|
||||||
*
|
*
|
||||||
* @param {Object} params
|
* @param {Object} params
|
||||||
* @param {ServerRequest} params.req
|
* @param {ServerRequest} params.req
|
||||||
|
* @param {string} params.spec
|
||||||
* @param {string} params.agent_id
|
* @param {string} params.agent_id
|
||||||
* @param {string} params.endpoint
|
* @param {string} params.endpoint
|
||||||
* @param {import('@librechat/agents').ClientOptions} [params.model_parameters]
|
* @param {import('@librechat/agents').ClientOptions} [params.model_parameters]
|
||||||
* @returns {Promise<Agent|null>} The agent document as a plain object, or null if not found.
|
* @returns {Promise<Agent|null>} The agent document as a plain object, or null if not found.
|
||||||
*/
|
*/
|
||||||
const loadEphemeralAgent = async ({ req, agent_id, endpoint, model_parameters: _m }) => {
|
const loadEphemeralAgent = async ({ req, spec, agent_id, endpoint, model_parameters: _m }) => {
|
||||||
const { model, ...model_parameters } = _m;
|
const { model, ...model_parameters } = _m;
|
||||||
|
const modelSpecs = req.config?.modelSpecs?.list;
|
||||||
|
/** @type {TModelSpec | null} */
|
||||||
|
let modelSpec = null;
|
||||||
|
if (spec != null && spec !== '') {
|
||||||
|
modelSpec = modelSpecs?.find((s) => s.name === spec) || null;
|
||||||
|
}
|
||||||
/** @type {TEphemeralAgent | null} */
|
/** @type {TEphemeralAgent | null} */
|
||||||
const ephemeralAgent = req.body.ephemeralAgent;
|
const ephemeralAgent = req.body.ephemeralAgent;
|
||||||
const mcpServers = new Set(ephemeralAgent?.mcp);
|
const mcpServers = new Set(ephemeralAgent?.mcp);
|
||||||
|
if (modelSpec?.mcpServers) {
|
||||||
|
for (const mcpServer of modelSpec.mcpServers) {
|
||||||
|
mcpServers.add(mcpServer);
|
||||||
|
}
|
||||||
|
}
|
||||||
/** @type {string[]} */
|
/** @type {string[]} */
|
||||||
const tools = [];
|
const tools = [];
|
||||||
if (ephemeralAgent?.execute_code === true) {
|
if (ephemeralAgent?.execute_code === true || modelSpec?.executeCode === true) {
|
||||||
tools.push(Tools.execute_code);
|
tools.push(Tools.execute_code);
|
||||||
}
|
}
|
||||||
if (ephemeralAgent?.file_search === true) {
|
if (ephemeralAgent?.file_search === true || modelSpec?.fileSearch === true) {
|
||||||
tools.push(Tools.file_search);
|
tools.push(Tools.file_search);
|
||||||
}
|
}
|
||||||
if (ephemeralAgent?.web_search === true) {
|
if (ephemeralAgent?.web_search === true || modelSpec?.webSearch === true) {
|
||||||
tools.push(Tools.web_search);
|
tools.push(Tools.web_search);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -122,17 +134,18 @@ const loadEphemeralAgent = async ({ req, agent_id, endpoint, model_parameters: _
|
||||||
*
|
*
|
||||||
* @param {Object} params
|
* @param {Object} params
|
||||||
* @param {ServerRequest} params.req
|
* @param {ServerRequest} params.req
|
||||||
|
* @param {string} params.spec
|
||||||
* @param {string} params.agent_id
|
* @param {string} params.agent_id
|
||||||
* @param {string} params.endpoint
|
* @param {string} params.endpoint
|
||||||
* @param {import('@librechat/agents').ClientOptions} [params.model_parameters]
|
* @param {import('@librechat/agents').ClientOptions} [params.model_parameters]
|
||||||
* @returns {Promise<Agent|null>} The agent document as a plain object, or null if not found.
|
* @returns {Promise<Agent|null>} The agent document as a plain object, or null if not found.
|
||||||
*/
|
*/
|
||||||
const loadAgent = async ({ req, agent_id, endpoint, model_parameters }) => {
|
const loadAgent = async ({ req, spec, agent_id, endpoint, model_parameters }) => {
|
||||||
if (!agent_id) {
|
if (!agent_id) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (agent_id === EPHEMERAL_AGENT_ID) {
|
if (agent_id === EPHEMERAL_AGENT_ID) {
|
||||||
return await loadEphemeralAgent({ req, agent_id, endpoint, model_parameters });
|
return await loadEphemeralAgent({ req, spec, agent_id, endpoint, model_parameters });
|
||||||
}
|
}
|
||||||
const agent = await getAgent({
|
const agent = await getAgent({
|
||||||
id: agent_id,
|
id: agent_id,
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,10 @@ const { isAgentsEndpoint, removeNullishValues, Constants } = require('librechat-
|
||||||
const { loadAgent } = require('~/models/Agent');
|
const { loadAgent } = require('~/models/Agent');
|
||||||
|
|
||||||
const buildOptions = (req, endpoint, parsedBody, endpointType) => {
|
const buildOptions = (req, endpoint, parsedBody, endpointType) => {
|
||||||
const { spec, iconURL, agent_id, instructions, ...model_parameters } = parsedBody;
|
const { spec, iconURL, agent_id, ...model_parameters } = parsedBody;
|
||||||
const agentPromise = loadAgent({
|
const agentPromise = loadAgent({
|
||||||
req,
|
req,
|
||||||
|
spec,
|
||||||
agent_id: isAgentsEndpoint(endpoint) ? agent_id : Constants.EPHEMERAL_AGENT_ID,
|
agent_id: isAgentsEndpoint(endpoint) ? agent_id : Constants.EPHEMERAL_AGENT_ID,
|
||||||
endpoint,
|
endpoint,
|
||||||
model_parameters,
|
model_parameters,
|
||||||
|
|
@ -20,7 +21,6 @@ const buildOptions = (req, endpoint, parsedBody, endpointType) => {
|
||||||
endpoint,
|
endpoint,
|
||||||
agent_id,
|
agent_id,
|
||||||
endpointType,
|
endpointType,
|
||||||
instructions,
|
|
||||||
model_parameters,
|
model_parameters,
|
||||||
agent: agentPromise,
|
agent: agentPromise,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -220,6 +220,7 @@ const ChatForm = memo(({ index = 0 }: { index?: number }) => {
|
||||||
<div className={cn('flex w-full items-center', isRTL && 'flex-row-reverse')}>
|
<div className={cn('flex w-full items-center', isRTL && 'flex-row-reverse')}>
|
||||||
{showPlusPopover && !isAssistantsEndpoint(endpoint) && (
|
{showPlusPopover && !isAssistantsEndpoint(endpoint) && (
|
||||||
<Mention
|
<Mention
|
||||||
|
conversation={conversation}
|
||||||
setShowMentionPopover={setShowPlusPopover}
|
setShowMentionPopover={setShowPlusPopover}
|
||||||
newConversation={generateConversation}
|
newConversation={generateConversation}
|
||||||
textAreaRef={textAreaRef}
|
textAreaRef={textAreaRef}
|
||||||
|
|
@ -230,6 +231,7 @@ const ChatForm = memo(({ index = 0 }: { index?: number }) => {
|
||||||
)}
|
)}
|
||||||
{showMentionPopover && (
|
{showMentionPopover && (
|
||||||
<Mention
|
<Mention
|
||||||
|
conversation={conversation}
|
||||||
setShowMentionPopover={setShowMentionPopover}
|
setShowMentionPopover={setShowMentionPopover}
|
||||||
newConversation={newConversation}
|
newConversation={newConversation}
|
||||||
textAreaRef={textAreaRef}
|
textAreaRef={textAreaRef}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { useState, useRef, useEffect } from 'react';
|
||||||
import { useCombobox } from '@librechat/client';
|
import { useCombobox } from '@librechat/client';
|
||||||
import { AutoSizer, List } from 'react-virtualized';
|
import { AutoSizer, List } from 'react-virtualized';
|
||||||
import { EModelEndpoint } from 'librechat-data-provider';
|
import { EModelEndpoint } from 'librechat-data-provider';
|
||||||
|
import type { TConversation } from 'librechat-data-provider';
|
||||||
import type { MentionOption, ConvoGenerator } from '~/common';
|
import type { MentionOption, ConvoGenerator } from '~/common';
|
||||||
import type { SetterOrUpdater } from 'recoil';
|
import type { SetterOrUpdater } from 'recoil';
|
||||||
import useSelectMention from '~/hooks/Input/useSelectMention';
|
import useSelectMention from '~/hooks/Input/useSelectMention';
|
||||||
|
|
@ -14,6 +15,7 @@ import MentionItem from './MentionItem';
|
||||||
const ROW_HEIGHT = 40;
|
const ROW_HEIGHT = 40;
|
||||||
|
|
||||||
export default function Mention({
|
export default function Mention({
|
||||||
|
conversation,
|
||||||
setShowMentionPopover,
|
setShowMentionPopover,
|
||||||
newConversation,
|
newConversation,
|
||||||
textAreaRef,
|
textAreaRef,
|
||||||
|
|
@ -21,6 +23,7 @@ export default function Mention({
|
||||||
placeholder = 'com_ui_mention',
|
placeholder = 'com_ui_mention',
|
||||||
includeAssistants = true,
|
includeAssistants = true,
|
||||||
}: {
|
}: {
|
||||||
|
conversation: TConversation | null;
|
||||||
setShowMentionPopover: SetterOrUpdater<boolean>;
|
setShowMentionPopover: SetterOrUpdater<boolean>;
|
||||||
newConversation: ConvoGenerator;
|
newConversation: ConvoGenerator;
|
||||||
textAreaRef: React.MutableRefObject<HTMLTextAreaElement | null>;
|
textAreaRef: React.MutableRefObject<HTMLTextAreaElement | null>;
|
||||||
|
|
@ -42,6 +45,7 @@ export default function Mention({
|
||||||
const { onSelectMention } = useSelectMention({
|
const { onSelectMention } = useSelectMention({
|
||||||
presets,
|
presets,
|
||||||
modelSpecs,
|
modelSpecs,
|
||||||
|
conversation,
|
||||||
assistantsMap,
|
assistantsMap,
|
||||||
endpointsConfig,
|
endpointsConfig,
|
||||||
newConversation,
|
newConversation,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import React, { createContext, useContext, useMemo } from 'react';
|
import React, { createContext, useContext, useMemo } from 'react';
|
||||||
import type { EModelEndpoint } from 'librechat-data-provider';
|
import type { EModelEndpoint, TConversation } from 'librechat-data-provider';
|
||||||
import { useChatContext } from '~/Providers/ChatContext';
|
import { useChatContext } from '~/Providers/ChatContext';
|
||||||
|
|
||||||
interface ModelSelectorChatContextValue {
|
interface ModelSelectorChatContextValue {
|
||||||
|
|
@ -8,6 +8,7 @@ interface ModelSelectorChatContextValue {
|
||||||
spec?: string | null;
|
spec?: string | null;
|
||||||
agent_id?: string | null;
|
agent_id?: string | null;
|
||||||
assistant_id?: string | null;
|
assistant_id?: string | null;
|
||||||
|
conversation: TConversation | null;
|
||||||
newConversation: ReturnType<typeof useChatContext>['newConversation'];
|
newConversation: ReturnType<typeof useChatContext>['newConversation'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -26,16 +27,10 @@ export function ModelSelectorChatProvider({ children }: { children: React.ReactN
|
||||||
spec: conversation?.spec,
|
spec: conversation?.spec,
|
||||||
agent_id: conversation?.agent_id,
|
agent_id: conversation?.agent_id,
|
||||||
assistant_id: conversation?.assistant_id,
|
assistant_id: conversation?.assistant_id,
|
||||||
|
conversation,
|
||||||
newConversation,
|
newConversation,
|
||||||
}),
|
}),
|
||||||
[
|
[conversation, newConversation],
|
||||||
conversation?.endpoint,
|
|
||||||
conversation?.model,
|
|
||||||
conversation?.spec,
|
|
||||||
conversation?.agent_id,
|
|
||||||
conversation?.assistant_id,
|
|
||||||
newConversation,
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,7 @@ export function ModelSelectorProvider({ children, startupConfig }: ModelSelector
|
||||||
const agentsMap = useAgentsMapContext();
|
const agentsMap = useAgentsMapContext();
|
||||||
const assistantsMap = useAssistantsMapContext();
|
const assistantsMap = useAssistantsMapContext();
|
||||||
const { data: endpointsConfig } = useGetEndpointsQuery();
|
const { data: endpointsConfig } = useGetEndpointsQuery();
|
||||||
const { endpoint, model, spec, agent_id, assistant_id, newConversation } =
|
const { endpoint, model, spec, agent_id, assistant_id, conversation, newConversation } =
|
||||||
useModelSelectorChatContext();
|
useModelSelectorChatContext();
|
||||||
const modelSpecs = useMemo(() => {
|
const modelSpecs = useMemo(() => {
|
||||||
const specs = startupConfig?.modelSpecs?.list ?? [];
|
const specs = startupConfig?.modelSpecs?.list ?? [];
|
||||||
|
|
@ -96,6 +96,7 @@ export function ModelSelectorProvider({ children, startupConfig }: ModelSelector
|
||||||
const { onSelectEndpoint, onSelectSpec } = useSelectMention({
|
const { onSelectEndpoint, onSelectSpec } = useSelectMention({
|
||||||
// presets,
|
// presets,
|
||||||
modelSpecs,
|
modelSpecs,
|
||||||
|
conversation,
|
||||||
assistantsMap,
|
assistantsMap,
|
||||||
endpointsConfig,
|
endpointsConfig,
|
||||||
newConversation,
|
newConversation,
|
||||||
|
|
|
||||||
|
|
@ -6,3 +6,4 @@ export { default as useAgentCapabilities } from './useAgentCapabilities';
|
||||||
export { default as useGetAgentsConfig } from './useGetAgentsConfig';
|
export { default as useGetAgentsConfig } from './useGetAgentsConfig';
|
||||||
export { default as useAgentDefaultPermissionLevel } from './useAgentDefaultPermissionLevel';
|
export { default as useAgentDefaultPermissionLevel } from './useAgentDefaultPermissionLevel';
|
||||||
export { default as useAgentToolPermissions } from './useAgentToolPermissions';
|
export { default as useAgentToolPermissions } from './useAgentToolPermissions';
|
||||||
|
export * from './useApplyModelSpecAgents';
|
||||||
|
|
|
||||||
95
client/src/hooks/Agents/useApplyModelSpecAgents.ts
Normal file
95
client/src/hooks/Agents/useApplyModelSpecAgents.ts
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
import type { TStartupConfig, TSubmission } from 'librechat-data-provider';
|
||||||
|
import { useUpdateEphemeralAgent, useApplyNewAgentTemplate } from '~/store/agents';
|
||||||
|
import { getModelSpec, applyModelSpecEphemeralAgent } from '~/utils';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook that applies a model spec from a preset to an ephemeral agent.
|
||||||
|
* This is used when initializing a new conversation with a preset that has a spec.
|
||||||
|
*/
|
||||||
|
export function useApplyModelSpecEffects() {
|
||||||
|
const updateEphemeralAgent = useUpdateEphemeralAgent();
|
||||||
|
const applyPresetModelSpec = useCallback(
|
||||||
|
({
|
||||||
|
convoId,
|
||||||
|
specName,
|
||||||
|
startupConfig,
|
||||||
|
}: {
|
||||||
|
convoId: string | null;
|
||||||
|
specName?: string | null;
|
||||||
|
startupConfig?: TStartupConfig;
|
||||||
|
}) => {
|
||||||
|
if (specName == null || !specName) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const modelSpec = getModelSpec({
|
||||||
|
specName,
|
||||||
|
startupConfig,
|
||||||
|
});
|
||||||
|
|
||||||
|
applyModelSpecEphemeralAgent({
|
||||||
|
convoId,
|
||||||
|
modelSpec,
|
||||||
|
updateEphemeralAgent,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[updateEphemeralAgent],
|
||||||
|
);
|
||||||
|
|
||||||
|
return applyPresetModelSpec;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useApplyAgentTemplate() {
|
||||||
|
const applyAgentTemplate = useApplyNewAgentTemplate();
|
||||||
|
/**
|
||||||
|
* Helper function to apply agent template with model spec merged into ephemeral agent
|
||||||
|
*/
|
||||||
|
const applyAgentTemplateWithSpec = useCallback(
|
||||||
|
({
|
||||||
|
targetId,
|
||||||
|
sourceId,
|
||||||
|
ephemeralAgent,
|
||||||
|
specName,
|
||||||
|
startupConfig,
|
||||||
|
}: {
|
||||||
|
targetId: string;
|
||||||
|
sourceId?: TSubmission['conversation']['conversationId'] | null;
|
||||||
|
ephemeralAgent: TSubmission['ephemeralAgent'];
|
||||||
|
specName?: string | null;
|
||||||
|
startupConfig?: TStartupConfig;
|
||||||
|
}) => {
|
||||||
|
if (!specName) {
|
||||||
|
applyAgentTemplate(targetId, sourceId, ephemeralAgent);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const modelSpec = getModelSpec({
|
||||||
|
specName,
|
||||||
|
startupConfig,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!modelSpec) {
|
||||||
|
applyAgentTemplate(targetId, sourceId, ephemeralAgent);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge model spec fields into ephemeral agent
|
||||||
|
const mergedAgent = {
|
||||||
|
...ephemeralAgent,
|
||||||
|
mcp: [...(ephemeralAgent?.mcp ?? []), ...(modelSpec.mcpServers ?? [])],
|
||||||
|
web_search: ephemeralAgent?.web_search ?? modelSpec.webSearch ?? false,
|
||||||
|
file_search: ephemeralAgent?.file_search ?? modelSpec.fileSearch ?? false,
|
||||||
|
execute_code: ephemeralAgent?.execute_code ?? modelSpec.executeCode ?? false,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Deduplicate MCP servers
|
||||||
|
mergedAgent.mcp = [...new Set(mergedAgent.mcp)];
|
||||||
|
|
||||||
|
applyAgentTemplate(targetId, sourceId, mergedAgent);
|
||||||
|
},
|
||||||
|
[applyAgentTemplate],
|
||||||
|
);
|
||||||
|
|
||||||
|
return applyAgentTemplateWithSpec;
|
||||||
|
}
|
||||||
|
|
@ -1,8 +1,14 @@
|
||||||
|
import { useCallback } from 'react';
|
||||||
import { useSetRecoilState } from 'recoil';
|
import { useSetRecoilState } from 'recoil';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
import { QueryKeys, Constants, dataService } from 'librechat-data-provider';
|
import { QueryKeys, Constants, dataService } from 'librechat-data-provider';
|
||||||
import type { TConversation, TEndpointsConfig, TModelsConfig } from 'librechat-data-provider';
|
import type {
|
||||||
|
TEndpointsConfig,
|
||||||
|
TStartupConfig,
|
||||||
|
TModelsConfig,
|
||||||
|
TConversation,
|
||||||
|
} from 'librechat-data-provider';
|
||||||
import {
|
import {
|
||||||
getDefaultEndpoint,
|
getDefaultEndpoint,
|
||||||
clearMessagesCache,
|
clearMessagesCache,
|
||||||
|
|
@ -10,15 +16,34 @@ import {
|
||||||
getEndpointField,
|
getEndpointField,
|
||||||
logger,
|
logger,
|
||||||
} from '~/utils';
|
} from '~/utils';
|
||||||
|
import { useApplyModelSpecEffects } from '~/hooks/Agents';
|
||||||
import store from '~/store';
|
import store from '~/store';
|
||||||
|
|
||||||
const useNavigateToConvo = (index = 0) => {
|
const useNavigateToConvo = (index = 0) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const clearAllConversations = store.useClearConvoState();
|
const clearAllConversations = store.useClearConvoState();
|
||||||
|
const applyModelSpecEffects = useApplyModelSpecEffects();
|
||||||
const setSubmission = useSetRecoilState(store.submissionByIndex(index));
|
const setSubmission = useSetRecoilState(store.submissionByIndex(index));
|
||||||
const clearAllLatestMessages = store.useClearLatestMessages(`useNavigateToConvo ${index}`);
|
const clearAllLatestMessages = store.useClearLatestMessages(`useNavigateToConvo ${index}`);
|
||||||
const { hasSetConversation, setConversation } = store.useCreateConversationAtom(index);
|
const { hasSetConversation, setConversation: setConvo } = store.useCreateConversationAtom(index);
|
||||||
|
|
||||||
|
const setConversation = useCallback(
|
||||||
|
(conversation: TConversation) => {
|
||||||
|
setConvo(conversation);
|
||||||
|
if (!conversation.spec) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const startupConfig = queryClient.getQueryData<TStartupConfig>([QueryKeys.startupConfig]);
|
||||||
|
applyModelSpecEffects({
|
||||||
|
startupConfig,
|
||||||
|
specName: conversation?.spec,
|
||||||
|
convoId: conversation.conversationId,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[setConvo, queryClient, applyModelSpecEffects],
|
||||||
|
);
|
||||||
|
|
||||||
const fetchFreshData = async (conversation?: Partial<TConversation>) => {
|
const fetchFreshData = async (conversation?: Partial<TConversation>) => {
|
||||||
const conversationId = conversation?.conversationId;
|
const conversationId = conversation?.conversationId;
|
||||||
|
|
|
||||||
|
|
@ -10,18 +10,19 @@ import type {
|
||||||
} from 'librechat-data-provider';
|
} from 'librechat-data-provider';
|
||||||
import type { MentionOption, ConvoGenerator } from '~/common';
|
import type { MentionOption, ConvoGenerator } from '~/common';
|
||||||
import { getConvoSwitchLogic, getModelSpecIconURL, removeUnavailableTools, logger } from '~/utils';
|
import { getConvoSwitchLogic, getModelSpecIconURL, removeUnavailableTools, logger } from '~/utils';
|
||||||
import { useChatContext } from '~/Providers';
|
|
||||||
import { useDefaultConvo } from '~/hooks';
|
import { useDefaultConvo } from '~/hooks';
|
||||||
import store from '~/store';
|
import store from '~/store';
|
||||||
|
|
||||||
export default function useSelectMention({
|
export default function useSelectMention({
|
||||||
presets,
|
presets,
|
||||||
modelSpecs,
|
modelSpecs,
|
||||||
|
conversation,
|
||||||
assistantsMap,
|
assistantsMap,
|
||||||
|
returnHandlers,
|
||||||
endpointsConfig,
|
endpointsConfig,
|
||||||
newConversation,
|
newConversation,
|
||||||
returnHandlers,
|
|
||||||
}: {
|
}: {
|
||||||
|
conversation: TConversation | null;
|
||||||
presets?: TPreset[];
|
presets?: TPreset[];
|
||||||
modelSpecs: TModelSpec[];
|
modelSpecs: TModelSpec[];
|
||||||
assistantsMap?: TAssistantsMap;
|
assistantsMap?: TAssistantsMap;
|
||||||
|
|
@ -29,7 +30,6 @@ export default function useSelectMention({
|
||||||
endpointsConfig: TEndpointsConfig;
|
endpointsConfig: TEndpointsConfig;
|
||||||
returnHandlers?: boolean;
|
returnHandlers?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const { conversation } = useChatContext();
|
|
||||||
const getDefaultConversation = useDefaultConvo();
|
const getDefaultConversation = useDefaultConvo();
|
||||||
const modularChat = useRecoilValue(store.modularChat);
|
const modularChat = useRecoilValue(store.modularChat);
|
||||||
const availableTools = useRecoilValue(store.availableTools);
|
const availableTools = useRecoilValue(store.availableTools);
|
||||||
|
|
|
||||||
|
|
@ -431,9 +431,10 @@ describe('useMCPSelect', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Values should remain unchanged since empty mcp array doesn't trigger update
|
// Values should sync to empty array when ephemeralAgent.mcp is set to []
|
||||||
// (due to the condition: ephemeralAgent?.mcp && ephemeralAgent.mcp.length > 0)
|
await waitFor(() => {
|
||||||
expect(result.current.mcpHook.mcpValues).toEqual(['initial-value']);
|
expect(result.current.mcpHook.mcpValues).toEqual([]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should properly sync non-empty arrays from ephemeralAgent', async () => {
|
it('should properly sync non-empty arrays from ephemeralAgent', async () => {
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,8 @@ export function useMCPSelect({ conversationId }: { conversationId?: string | nul
|
||||||
// Strip out servers that are not available in the startup config
|
// Strip out servers that are not available in the startup config
|
||||||
const activeMcps = mcps.filter((mcp) => configuredServers.has(mcp));
|
const activeMcps = mcps.filter((mcp) => configuredServers.has(mcp));
|
||||||
setMCPValuesRaw(activeMcps);
|
setMCPValuesRaw(activeMcps);
|
||||||
|
} else {
|
||||||
|
setMCPValuesRaw([]);
|
||||||
}
|
}
|
||||||
}, [ephemeralAgent?.mcp, setMCPValuesRaw, configuredServers]);
|
}, [ephemeralAgent?.mcp, setMCPValuesRaw, configuredServers]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { v4 } from 'uuid';
|
|
||||||
import { useCallback, useRef } from 'react';
|
import { useCallback, useRef } from 'react';
|
||||||
|
import { v4 } from 'uuid';
|
||||||
import { useSetRecoilState } from 'recoil';
|
import { useSetRecoilState } from 'recoil';
|
||||||
import { useParams, useNavigate, useLocation } from 'react-router-dom';
|
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { useParams, useNavigate, useLocation } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
QueryKeys,
|
QueryKeys,
|
||||||
Constants,
|
Constants,
|
||||||
|
|
@ -13,7 +13,12 @@ import {
|
||||||
tConvoUpdateSchema,
|
tConvoUpdateSchema,
|
||||||
isAssistantsEndpoint,
|
isAssistantsEndpoint,
|
||||||
} from 'librechat-data-provider';
|
} from 'librechat-data-provider';
|
||||||
import type { TMessage, TConversation, EventSubmission } from 'librechat-data-provider';
|
import type {
|
||||||
|
TMessage,
|
||||||
|
TConversation,
|
||||||
|
EventSubmission,
|
||||||
|
TStartupConfig,
|
||||||
|
} from 'librechat-data-provider';
|
||||||
import type { TResData, TFinalResData, ConvoGenerator } from '~/common';
|
import type { TResData, TFinalResData, ConvoGenerator } from '~/common';
|
||||||
import type { InfiniteData } from '@tanstack/react-query';
|
import type { InfiniteData } from '@tanstack/react-query';
|
||||||
import type { TGenTitleMutation } from '~/data-provider';
|
import type { TGenTitleMutation } from '~/data-provider';
|
||||||
|
|
@ -31,11 +36,12 @@ import {
|
||||||
} from '~/utils';
|
} from '~/utils';
|
||||||
import useAttachmentHandler from '~/hooks/SSE/useAttachmentHandler';
|
import useAttachmentHandler from '~/hooks/SSE/useAttachmentHandler';
|
||||||
import useContentHandler from '~/hooks/SSE/useContentHandler';
|
import useContentHandler from '~/hooks/SSE/useContentHandler';
|
||||||
import store, { useApplyNewAgentTemplate } from '~/store';
|
|
||||||
import useStepHandler from '~/hooks/SSE/useStepHandler';
|
import useStepHandler from '~/hooks/SSE/useStepHandler';
|
||||||
|
import { useApplyAgentTemplate } from '~/hooks/Agents';
|
||||||
import { useAuthContext } from '~/hooks/AuthContext';
|
import { useAuthContext } from '~/hooks/AuthContext';
|
||||||
import { MESSAGE_UPDATE_INTERVAL } from '~/common';
|
import { MESSAGE_UPDATE_INTERVAL } from '~/common';
|
||||||
import { useLiveAnnouncer } from '~/Providers';
|
import { useLiveAnnouncer } from '~/Providers';
|
||||||
|
import store from '~/store';
|
||||||
|
|
||||||
type TSyncData = {
|
type TSyncData = {
|
||||||
sync: boolean;
|
sync: boolean;
|
||||||
|
|
@ -172,7 +178,7 @@ export default function useEventHandlers({
|
||||||
}: EventHandlerParams) {
|
}: EventHandlerParams) {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const { announcePolite } = useLiveAnnouncer();
|
const { announcePolite } = useLiveAnnouncer();
|
||||||
const applyAgentTemplate = useApplyNewAgentTemplate();
|
const applyAgentTemplate = useApplyAgentTemplate();
|
||||||
const setAbortScroll = useSetRecoilState(store.abortScroll);
|
const setAbortScroll = useSetRecoilState(store.abortScroll);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
@ -356,6 +362,7 @@ export default function useEventHandlers({
|
||||||
|
|
||||||
const createdHandler = useCallback(
|
const createdHandler = useCallback(
|
||||||
(data: TResData, submission: EventSubmission) => {
|
(data: TResData, submission: EventSubmission) => {
|
||||||
|
queryClient.invalidateQueries([QueryKeys.mcpConnectionStatus]);
|
||||||
const { messages, userMessage, isRegenerate = false, isTemporary = false } = submission;
|
const { messages, userMessage, isRegenerate = false, isTemporary = false } = submission;
|
||||||
const initialResponse = {
|
const initialResponse = {
|
||||||
...submission.initialResponse,
|
...submission.initialResponse,
|
||||||
|
|
@ -411,11 +418,13 @@ export default function useEventHandlers({
|
||||||
}
|
}
|
||||||
|
|
||||||
if (conversationId) {
|
if (conversationId) {
|
||||||
applyAgentTemplate(
|
applyAgentTemplate({
|
||||||
conversationId,
|
targetId: conversationId,
|
||||||
submission.conversation.conversationId,
|
sourceId: submission.conversation?.conversationId,
|
||||||
submission.ephemeralAgent,
|
ephemeralAgent: submission.ephemeralAgent,
|
||||||
);
|
specName: submission.conversation?.spec,
|
||||||
|
startupConfig: queryClient.getQueryData<TStartupConfig>([QueryKeys.startupConfig]),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (resetLatestMessage) {
|
if (resetLatestMessage) {
|
||||||
|
|
@ -566,11 +575,13 @@ export default function useEventHandlers({
|
||||||
});
|
});
|
||||||
|
|
||||||
if (conversation.conversationId && submission.ephemeralAgent) {
|
if (conversation.conversationId && submission.ephemeralAgent) {
|
||||||
applyAgentTemplate(
|
applyAgentTemplate({
|
||||||
conversation.conversationId,
|
targetId: conversation.conversationId,
|
||||||
submissionConvo.conversationId,
|
sourceId: submissionConvo.conversationId,
|
||||||
submission.ephemeralAgent,
|
ephemeralAgent: submission.ephemeralAgent,
|
||||||
);
|
specName: submission.conversation?.spec,
|
||||||
|
startupConfig: queryClient.getQueryData<TStartupConfig>([QueryKeys.startupConfig]),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (location.pathname === `/c/${Constants.NEW_CONVO}`) {
|
if (location.pathname === `/c/${Constants.NEW_CONVO}`) {
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ import {
|
||||||
import { useDeleteFilesMutation, useGetEndpointsQuery, useGetStartupConfig } from '~/data-provider';
|
import { useDeleteFilesMutation, useGetEndpointsQuery, useGetStartupConfig } from '~/data-provider';
|
||||||
import useAssistantListMap from './Assistants/useAssistantListMap';
|
import useAssistantListMap from './Assistants/useAssistantListMap';
|
||||||
import { useResetChatBadges } from './useChatBadges';
|
import { useResetChatBadges } from './useChatBadges';
|
||||||
|
import { useApplyModelSpecEffects } from './Agents';
|
||||||
import { usePauseGlobalAudio } from './Audio';
|
import { usePauseGlobalAudio } from './Audio';
|
||||||
import { logger } from '~/utils';
|
import { logger } from '~/utils';
|
||||||
import store from '~/store';
|
import store from '~/store';
|
||||||
|
|
@ -37,6 +38,7 @@ const useNewConvo = (index = 0) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const { data: startupConfig } = useGetStartupConfig();
|
const { data: startupConfig } = useGetStartupConfig();
|
||||||
|
const applyModelSpecEffects = useApplyModelSpecEffects();
|
||||||
const clearAllConversations = store.useClearConvoState();
|
const clearAllConversations = store.useClearConvoState();
|
||||||
const defaultPreset = useRecoilValue(store.defaultPreset);
|
const defaultPreset = useRecoilValue(store.defaultPreset);
|
||||||
const { setConversation } = store.useCreateConversationAtom(index);
|
const { setConversation } = store.useCreateConversationAtom(index);
|
||||||
|
|
@ -265,6 +267,12 @@ const useNewConvo = (index = 0) => {
|
||||||
preset = getModelSpecPreset(defaultModelSpec);
|
preset = getModelSpecPreset(defaultModelSpec);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
applyModelSpecEffects({
|
||||||
|
startupConfig,
|
||||||
|
specName: preset?.spec,
|
||||||
|
convoId: conversation.conversationId,
|
||||||
|
});
|
||||||
|
|
||||||
if (conversation.conversationId === Constants.NEW_CONVO && !modelsData) {
|
if (conversation.conversationId === Constants.NEW_CONVO && !modelsData) {
|
||||||
const filesToDelete = Array.from(files.values())
|
const filesToDelete = Array.from(files.values())
|
||||||
.filter(
|
.filter(
|
||||||
|
|
@ -311,6 +319,7 @@ const useNewConvo = (index = 0) => {
|
||||||
saveBadgesState,
|
saveBadgesState,
|
||||||
pauseGlobalAudio,
|
pauseGlobalAudio,
|
||||||
switchToConversation,
|
switchToConversation,
|
||||||
|
applyModelSpecEffects,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,18 @@ export const ephemeralAgentByConvoId = atomFamily<TEphemeralAgent | null, string
|
||||||
] as const,
|
] as const,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export function useUpdateEphemeralAgent() {
|
||||||
|
const updateEphemeralAgent = useRecoilCallback(
|
||||||
|
({ set }) =>
|
||||||
|
(convoId: string, agent: TEphemeralAgent | null) => {
|
||||||
|
set(ephemeralAgentByConvoId(convoId), agent);
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
return updateEphemeralAgent;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a callback function to apply the ephemeral agent state
|
* Creates a callback function to apply the ephemeral agent state
|
||||||
* from the "new" conversation template to a specified conversation ID.
|
* from the "new" conversation template to a specified conversation ID.
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import {
|
import {
|
||||||
|
Constants,
|
||||||
EModelEndpoint,
|
EModelEndpoint,
|
||||||
defaultEndpoints,
|
defaultEndpoints,
|
||||||
modularEndpoints,
|
modularEndpoints,
|
||||||
|
|
@ -176,6 +177,39 @@ export function getConvoSwitchLogic(params: ConversationInitParams): InitiatedTe
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getModelSpec({
|
||||||
|
specName,
|
||||||
|
startupConfig,
|
||||||
|
}: {
|
||||||
|
specName?: string | null;
|
||||||
|
startupConfig?: t.TStartupConfig;
|
||||||
|
}): t.TModelSpec | undefined {
|
||||||
|
if (!startupConfig || !specName) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return startupConfig.modelSpecs?.list?.find((spec) => spec.name === specName);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyModelSpecEphemeralAgent({
|
||||||
|
convoId,
|
||||||
|
modelSpec,
|
||||||
|
updateEphemeralAgent,
|
||||||
|
}: {
|
||||||
|
convoId?: string | null;
|
||||||
|
modelSpec?: t.TModelSpec;
|
||||||
|
updateEphemeralAgent: ((convoId: string, agent: t.TEphemeralAgent | null) => void) | undefined;
|
||||||
|
}) {
|
||||||
|
if (!modelSpec || !updateEphemeralAgent) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
updateEphemeralAgent((convoId ?? Constants.NEW_CONVO) || Constants.NEW_CONVO, {
|
||||||
|
mcp: modelSpec.mcpServers ?? [],
|
||||||
|
web_search: modelSpec.webSearch ?? false,
|
||||||
|
file_search: modelSpec.fileSearch ?? false,
|
||||||
|
execute_code: modelSpec.executeCode ?? false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets default model spec from config and user preferences.
|
* Gets default model spec from config and user preferences.
|
||||||
* Priority: admin default → last selected → first spec (when prioritize=true or modelSelect disabled).
|
* Priority: admin default → last selected → first spec (when prioritize=true or modelSelect disabled).
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,10 @@ export type TModelSpec = {
|
||||||
showIconInHeader?: boolean;
|
showIconInHeader?: boolean;
|
||||||
iconURL?: string | EModelEndpoint; // Allow using project-included icons
|
iconURL?: string | EModelEndpoint; // Allow using project-included icons
|
||||||
authType?: AuthType;
|
authType?: AuthType;
|
||||||
|
webSearch?: boolean;
|
||||||
|
fileSearch?: boolean;
|
||||||
|
executeCode?: boolean;
|
||||||
|
mcpServers?: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const tModelSpecSchema = z.object({
|
export const tModelSpecSchema = z.object({
|
||||||
|
|
@ -40,6 +44,10 @@ export const tModelSpecSchema = z.object({
|
||||||
showIconInHeader: z.boolean().optional(),
|
showIconInHeader: z.boolean().optional(),
|
||||||
iconURL: z.union([z.string(), eModelEndpointSchema]).optional(),
|
iconURL: z.union([z.string(), eModelEndpointSchema]).optional(),
|
||||||
authType: authTypeSchema.optional(),
|
authType: authTypeSchema.optional(),
|
||||||
|
webSearch: z.boolean().optional(),
|
||||||
|
fileSearch: z.boolean().optional(),
|
||||||
|
executeCode: z.boolean().optional(),
|
||||||
|
mcpServers: z.array(z.string()).optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const specsConfigSchema = z.object({
|
export const specsConfigSchema = z.object({
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue