mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-19 09:50:15 +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
|
|
@ -6,3 +6,4 @@ export { default as useAgentCapabilities } from './useAgentCapabilities';
|
|||
export { default as useGetAgentsConfig } from './useGetAgentsConfig';
|
||||
export { default as useAgentDefaultPermissionLevel } from './useAgentDefaultPermissionLevel';
|
||||
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 { useNavigate } from 'react-router-dom';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
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 {
|
||||
getDefaultEndpoint,
|
||||
clearMessagesCache,
|
||||
|
|
@ -10,15 +16,34 @@ import {
|
|||
getEndpointField,
|
||||
logger,
|
||||
} from '~/utils';
|
||||
import { useApplyModelSpecEffects } from '~/hooks/Agents';
|
||||
import store from '~/store';
|
||||
|
||||
const useNavigateToConvo = (index = 0) => {
|
||||
const navigate = useNavigate();
|
||||
const queryClient = useQueryClient();
|
||||
const clearAllConversations = store.useClearConvoState();
|
||||
const applyModelSpecEffects = useApplyModelSpecEffects();
|
||||
const setSubmission = useSetRecoilState(store.submissionByIndex(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 conversationId = conversation?.conversationId;
|
||||
|
|
|
|||
|
|
@ -10,18 +10,19 @@ import type {
|
|||
} from 'librechat-data-provider';
|
||||
import type { MentionOption, ConvoGenerator } from '~/common';
|
||||
import { getConvoSwitchLogic, getModelSpecIconURL, removeUnavailableTools, logger } from '~/utils';
|
||||
import { useChatContext } from '~/Providers';
|
||||
import { useDefaultConvo } from '~/hooks';
|
||||
import store from '~/store';
|
||||
|
||||
export default function useSelectMention({
|
||||
presets,
|
||||
modelSpecs,
|
||||
conversation,
|
||||
assistantsMap,
|
||||
returnHandlers,
|
||||
endpointsConfig,
|
||||
newConversation,
|
||||
returnHandlers,
|
||||
}: {
|
||||
conversation: TConversation | null;
|
||||
presets?: TPreset[];
|
||||
modelSpecs: TModelSpec[];
|
||||
assistantsMap?: TAssistantsMap;
|
||||
|
|
@ -29,7 +30,6 @@ export default function useSelectMention({
|
|||
endpointsConfig: TEndpointsConfig;
|
||||
returnHandlers?: boolean;
|
||||
}) {
|
||||
const { conversation } = useChatContext();
|
||||
const getDefaultConversation = useDefaultConvo();
|
||||
const modularChat = useRecoilValue(store.modularChat);
|
||||
const availableTools = useRecoilValue(store.availableTools);
|
||||
|
|
|
|||
|
|
@ -431,9 +431,10 @@ describe('useMCPSelect', () => {
|
|||
});
|
||||
});
|
||||
|
||||
// Values should remain unchanged since empty mcp array doesn't trigger update
|
||||
// (due to the condition: ephemeralAgent?.mcp && ephemeralAgent.mcp.length > 0)
|
||||
expect(result.current.mcpHook.mcpValues).toEqual(['initial-value']);
|
||||
// Values should sync to empty array when ephemeralAgent.mcp is set to []
|
||||
await waitFor(() => {
|
||||
expect(result.current.mcpHook.mcpValues).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
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
|
||||
const activeMcps = mcps.filter((mcp) => configuredServers.has(mcp));
|
||||
setMCPValuesRaw(activeMcps);
|
||||
} else {
|
||||
setMCPValuesRaw([]);
|
||||
}
|
||||
}, [ephemeralAgent?.mcp, setMCPValuesRaw, configuredServers]);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { v4 } from 'uuid';
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { v4 } from 'uuid';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
import { useParams, useNavigate, useLocation } from 'react-router-dom';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { useParams, useNavigate, useLocation } from 'react-router-dom';
|
||||
import {
|
||||
QueryKeys,
|
||||
Constants,
|
||||
|
|
@ -13,7 +13,12 @@ import {
|
|||
tConvoUpdateSchema,
|
||||
isAssistantsEndpoint,
|
||||
} 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 { InfiniteData } from '@tanstack/react-query';
|
||||
import type { TGenTitleMutation } from '~/data-provider';
|
||||
|
|
@ -31,11 +36,12 @@ import {
|
|||
} from '~/utils';
|
||||
import useAttachmentHandler from '~/hooks/SSE/useAttachmentHandler';
|
||||
import useContentHandler from '~/hooks/SSE/useContentHandler';
|
||||
import store, { useApplyNewAgentTemplate } from '~/store';
|
||||
import useStepHandler from '~/hooks/SSE/useStepHandler';
|
||||
import { useApplyAgentTemplate } from '~/hooks/Agents';
|
||||
import { useAuthContext } from '~/hooks/AuthContext';
|
||||
import { MESSAGE_UPDATE_INTERVAL } from '~/common';
|
||||
import { useLiveAnnouncer } from '~/Providers';
|
||||
import store from '~/store';
|
||||
|
||||
type TSyncData = {
|
||||
sync: boolean;
|
||||
|
|
@ -172,7 +178,7 @@ export default function useEventHandlers({
|
|||
}: EventHandlerParams) {
|
||||
const queryClient = useQueryClient();
|
||||
const { announcePolite } = useLiveAnnouncer();
|
||||
const applyAgentTemplate = useApplyNewAgentTemplate();
|
||||
const applyAgentTemplate = useApplyAgentTemplate();
|
||||
const setAbortScroll = useSetRecoilState(store.abortScroll);
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
|
@ -356,6 +362,7 @@ export default function useEventHandlers({
|
|||
|
||||
const createdHandler = useCallback(
|
||||
(data: TResData, submission: EventSubmission) => {
|
||||
queryClient.invalidateQueries([QueryKeys.mcpConnectionStatus]);
|
||||
const { messages, userMessage, isRegenerate = false, isTemporary = false } = submission;
|
||||
const initialResponse = {
|
||||
...submission.initialResponse,
|
||||
|
|
@ -411,11 +418,13 @@ export default function useEventHandlers({
|
|||
}
|
||||
|
||||
if (conversationId) {
|
||||
applyAgentTemplate(
|
||||
conversationId,
|
||||
submission.conversation.conversationId,
|
||||
submission.ephemeralAgent,
|
||||
);
|
||||
applyAgentTemplate({
|
||||
targetId: conversationId,
|
||||
sourceId: submission.conversation?.conversationId,
|
||||
ephemeralAgent: submission.ephemeralAgent,
|
||||
specName: submission.conversation?.spec,
|
||||
startupConfig: queryClient.getQueryData<TStartupConfig>([QueryKeys.startupConfig]),
|
||||
});
|
||||
}
|
||||
|
||||
if (resetLatestMessage) {
|
||||
|
|
@ -566,11 +575,13 @@ export default function useEventHandlers({
|
|||
});
|
||||
|
||||
if (conversation.conversationId && submission.ephemeralAgent) {
|
||||
applyAgentTemplate(
|
||||
conversation.conversationId,
|
||||
submissionConvo.conversationId,
|
||||
submission.ephemeralAgent,
|
||||
);
|
||||
applyAgentTemplate({
|
||||
targetId: conversation.conversationId,
|
||||
sourceId: submissionConvo.conversationId,
|
||||
ephemeralAgent: submission.ephemeralAgent,
|
||||
specName: submission.conversation?.spec,
|
||||
startupConfig: queryClient.getQueryData<TStartupConfig>([QueryKeys.startupConfig]),
|
||||
});
|
||||
}
|
||||
|
||||
if (location.pathname === `/c/${Constants.NEW_CONVO}`) {
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ import {
|
|||
import { useDeleteFilesMutation, useGetEndpointsQuery, useGetStartupConfig } from '~/data-provider';
|
||||
import useAssistantListMap from './Assistants/useAssistantListMap';
|
||||
import { useResetChatBadges } from './useChatBadges';
|
||||
import { useApplyModelSpecEffects } from './Agents';
|
||||
import { usePauseGlobalAudio } from './Audio';
|
||||
import { logger } from '~/utils';
|
||||
import store from '~/store';
|
||||
|
|
@ -37,6 +38,7 @@ const useNewConvo = (index = 0) => {
|
|||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
const { data: startupConfig } = useGetStartupConfig();
|
||||
const applyModelSpecEffects = useApplyModelSpecEffects();
|
||||
const clearAllConversations = store.useClearConvoState();
|
||||
const defaultPreset = useRecoilValue(store.defaultPreset);
|
||||
const { setConversation } = store.useCreateConversationAtom(index);
|
||||
|
|
@ -265,6 +267,12 @@ const useNewConvo = (index = 0) => {
|
|||
preset = getModelSpecPreset(defaultModelSpec);
|
||||
}
|
||||
|
||||
applyModelSpecEffects({
|
||||
startupConfig,
|
||||
specName: preset?.spec,
|
||||
convoId: conversation.conversationId,
|
||||
});
|
||||
|
||||
if (conversation.conversationId === Constants.NEW_CONVO && !modelsData) {
|
||||
const filesToDelete = Array.from(files.values())
|
||||
.filter(
|
||||
|
|
@ -311,6 +319,7 @@ const useNewConvo = (index = 0) => {
|
|||
saveBadgesState,
|
||||
pauseGlobalAudio,
|
||||
switchToConversation,
|
||||
applyModelSpecEffects,
|
||||
],
|
||||
);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue