From d57e7aec73a53fb178d363d826b33e6474aab49b Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Thu, 14 Aug 2025 19:13:48 -0400 Subject: [PATCH] =?UTF-8?q?=F0=9F=A5=B7=20fix:=20Correct=20Agents=20Handli?= =?UTF-8?q?ng=20for=20Marketplace=20Users=20(#9065)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: Introduce ModelSelectorChatContext and integrate with ModelSelector * fix: agents handling in ModelSelector to show expected agents if user has marketplace access --- .../Chat/Menus/Endpoints/ModelSelector.tsx | 13 +++-- .../Endpoints/ModelSelectorChatContext.tsx | 54 +++++++++++++++++++ .../Menus/Endpoints/ModelSelectorContext.tsx | 45 ++++++++++++---- .../components/Chat/Menus/Endpoints/utils.ts | 5 ++ .../SidePanel/Agents/AgentSelect.tsx | 7 +-- client/src/hooks/Agents/useAgentsMap.ts | 10 ++-- client/src/hooks/Endpoint/useEndpoints.ts | 45 ++++++---------- client/src/utils/forms.tsx | 2 - 8 files changed, 124 insertions(+), 57 deletions(-) create mode 100644 client/src/components/Chat/Menus/Endpoints/ModelSelectorChatContext.tsx diff --git a/client/src/components/Chat/Menus/Endpoints/ModelSelector.tsx b/client/src/components/Chat/Menus/Endpoints/ModelSelector.tsx index 1a1463a41..01d96432a 100644 --- a/client/src/components/Chat/Menus/Endpoints/ModelSelector.tsx +++ b/client/src/components/Chat/Menus/Endpoints/ModelSelector.tsx @@ -1,6 +1,7 @@ import React, { useMemo } from 'react'; import type { ModelSelectorProps } from '~/common'; import { ModelSelectorProvider, useModelSelectorContext } from './ModelSelectorContext'; +import { ModelSelectorChatProvider } from './ModelSelectorChatContext'; import { renderModelSpecs, renderEndpoints, renderSearchResults } from './components'; import { getSelectedIcon, getDisplayValue } from './utils'; import { CustomMenu as Menu } from './CustomMenu'; @@ -12,6 +13,7 @@ function ModelSelectorContent() { const { // LibreChat + agentsMap, modelSpecs, mappedEndpoints, endpointsConfig, @@ -43,11 +45,12 @@ function ModelSelectorContent() { () => getDisplayValue({ localize, + agentsMap, modelSpecs, selectedValues, mappedEndpoints, }), - [localize, modelSpecs, selectedValues, mappedEndpoints], + [localize, agentsMap, modelSpecs, selectedValues, mappedEndpoints], ); const trigger = ( @@ -100,8 +103,10 @@ function ModelSelectorContent() { export default function ModelSelector({ startupConfig }: ModelSelectorProps) { return ( - - - + + + + + ); } diff --git a/client/src/components/Chat/Menus/Endpoints/ModelSelectorChatContext.tsx b/client/src/components/Chat/Menus/Endpoints/ModelSelectorChatContext.tsx new file mode 100644 index 000000000..bd639523d --- /dev/null +++ b/client/src/components/Chat/Menus/Endpoints/ModelSelectorChatContext.tsx @@ -0,0 +1,54 @@ +import React, { createContext, useContext, useMemo } from 'react'; +import type { EModelEndpoint } from 'librechat-data-provider'; +import { useChatContext } from '~/Providers/ChatContext'; + +interface ModelSelectorChatContextValue { + endpoint?: EModelEndpoint | null; + model?: string | null; + spec?: string | null; + agent_id?: string | null; + assistant_id?: string | null; + newConversation: ReturnType['newConversation']; +} + +const ModelSelectorChatContext = createContext( + undefined, +); + +export function ModelSelectorChatProvider({ children }: { children: React.ReactNode }) { + const { conversation, newConversation } = useChatContext(); + + /** Context value only created when relevant conversation properties change */ + const contextValue = useMemo( + () => ({ + endpoint: conversation?.endpoint, + model: conversation?.model, + spec: conversation?.spec, + agent_id: conversation?.agent_id, + assistant_id: conversation?.assistant_id, + newConversation, + }), + [ + conversation?.endpoint, + conversation?.model, + conversation?.spec, + conversation?.agent_id, + conversation?.assistant_id, + newConversation, + ], + ); + + return ( + + {children} + + ); +} + +export function useModelSelectorChatContext() { + const context = useContext(ModelSelectorChatContext); + if (!context) { + throw new Error('useModelSelectorChatContext must be used within ModelSelectorChatProvider'); + } + return context; +} diff --git a/client/src/components/Chat/Menus/Endpoints/ModelSelectorContext.tsx b/client/src/components/Chat/Menus/Endpoints/ModelSelectorContext.tsx index 01fc0e5a7..fa27a3244 100644 --- a/client/src/components/Chat/Menus/Endpoints/ModelSelectorContext.tsx +++ b/client/src/components/Chat/Menus/Endpoints/ModelSelectorContext.tsx @@ -3,10 +3,16 @@ import React, { createContext, useContext, useState, useMemo } from 'react'; import { isAgentsEndpoint, isAssistantsEndpoint } from 'librechat-data-provider'; import type * as t from 'librechat-data-provider'; import type { Endpoint, SelectedValues } from '~/common'; -import { useAgentsMapContext, useAssistantsMapContext, useChatContext } from '~/Providers'; -import { useEndpoints, useSelectorEffects, useKeyDialog } from '~/hooks'; +import { + useAgentDefaultPermissionLevel, + useSelectorEffects, + useKeyDialog, + useEndpoints, +} from '~/hooks'; +import { useAgentsMapContext, useAssistantsMapContext } from '~/Providers'; +import { useGetEndpointsQuery, useListAgentsQuery } from '~/data-provider'; +import { useModelSelectorChatContext } from './ModelSelectorChatContext'; import useSelectMention from '~/hooks/Input/useSelectMention'; -import { useGetEndpointsQuery } from '~/data-provider'; import { filterItems } from './utils'; type ModelSelectorContextType = { @@ -51,14 +57,24 @@ export function ModelSelectorProvider({ children, startupConfig }: ModelSelector const agentsMap = useAgentsMapContext(); const assistantsMap = useAssistantsMapContext(); const { data: endpointsConfig } = useGetEndpointsQuery(); - const { conversation, newConversation } = useChatContext(); + const { endpoint, model, spec, agent_id, assistant_id, newConversation } = + useModelSelectorChatContext(); const modelSpecs = useMemo(() => startupConfig?.modelSpecs?.list ?? [], [startupConfig]); + const permissionLevel = useAgentDefaultPermissionLevel(); + const { data: agents = null } = useListAgentsQuery( + { requiredPermission: permissionLevel }, + { + select: (data) => data?.data, + }, + ); + const { mappedEndpoints, endpointRequiresUserKey } = useEndpoints({ - agentsMap, + agents, assistantsMap, startupConfig, endpointsConfig, }); + const { onSelectEndpoint, onSelectSpec } = useSelectMention({ // presets, modelSpecs, @@ -70,13 +86,21 @@ export function ModelSelectorProvider({ children, startupConfig }: ModelSelector // State const [selectedValues, setSelectedValues] = useState({ - endpoint: conversation?.endpoint || '', - model: conversation?.model || '', - modelSpec: conversation?.spec || '', + endpoint: endpoint || '', + model: model || '', + modelSpec: spec || '', }); useSelectorEffects({ agentsMap, - conversation, + conversation: endpoint + ? ({ + endpoint: endpoint ?? null, + model: model ?? null, + spec: spec ?? null, + agent_id: agent_id ?? null, + assistant_id: assistant_id ?? null, + } as any) + : null, assistantsMap, setSelectedValues, }); @@ -86,7 +110,7 @@ export function ModelSelectorProvider({ children, startupConfig }: ModelSelector const keyProps = useKeyDialog(); - // Memoized search results + /** Memoized search results */ const searchResults = useMemo(() => { if (!searchValue) { return null; @@ -95,7 +119,6 @@ export function ModelSelectorProvider({ children, startupConfig }: ModelSelector return filterItems(allItems, searchValue, agentsMap, assistantsMap || {}); }, [searchValue, modelSpecs, mappedEndpoints, agentsMap, assistantsMap]); - // Functions const setDebouncedSearchValue = useMemo( () => debounce((value: string) => { diff --git a/client/src/components/Chat/Menus/Endpoints/utils.ts b/client/src/components/Chat/Menus/Endpoints/utils.ts index 5ed155c6a..1681ed7f1 100644 --- a/client/src/components/Chat/Menus/Endpoints/utils.ts +++ b/client/src/components/Chat/Menus/Endpoints/utils.ts @@ -167,11 +167,13 @@ export const getDisplayValue = ({ mappedEndpoints, selectedValues, modelSpecs, + agentsMap, }: { localize: ReturnType; selectedValues: SelectedValues; mappedEndpoints: Endpoint[]; modelSpecs: TModelSpec[]; + agentsMap?: TAgentsMap; }) => { if (selectedValues.modelSpec) { const spec = modelSpecs.find((s) => s.name === selectedValues.modelSpec); @@ -190,6 +192,9 @@ export const getDisplayValue = ({ endpoint.agentNames[selectedValues.model] ) { return endpoint.agentNames[selectedValues.model]; + } else if (isAgentsEndpoint(endpoint.value) && agentsMap) { + const agent = agentsMap[selectedValues.model]; + return agent?.name || selectedValues.model; } if ( diff --git a/client/src/components/SidePanel/Agents/AgentSelect.tsx b/client/src/components/SidePanel/Agents/AgentSelect.tsx index 411f1c45f..6379a64ee 100644 --- a/client/src/components/SidePanel/Agents/AgentSelect.tsx +++ b/client/src/components/SidePanel/Agents/AgentSelect.tsx @@ -7,8 +7,8 @@ import type { UseMutationResult, QueryObserverResult } from '@tanstack/react-que import type { Agent, AgentCreateParams } from 'librechat-data-provider'; import type { TAgentCapabilities, AgentForm } from '~/common'; import { cn, createProviderOption, processAgentOption, getDefaultAgentFormValues } from '~/utils'; -import { useGetStartupConfig, useListAgentsQuery } from '~/data-provider'; import { useLocalize, useAgentDefaultPermissionLevel } from '~/hooks'; +import { useListAgentsQuery } from '~/data-provider'; const keys = new Set(Object.keys(defaultAgentFormValues)); @@ -26,8 +26,6 @@ export default function AgentSelect({ const localize = useLocalize(); const lastSelectedAgent = useRef(null); const { control, reset } = useFormContext(); - - const { data: startupConfig } = useGetStartupConfig(); const permissionLevel = useAgentDefaultPermissionLevel(); const { data: agents = null } = useListAgentsQuery( @@ -40,7 +38,6 @@ export default function AgentSelect({ ...agent, name: agent.name || agent.id, }, - instanceProjectId: startupConfig?.instanceProjectId, }), ), }, @@ -122,7 +119,7 @@ export default function AgentSelect({ reset(formValues); }, - [reset, startupConfig], + [reset], ); const onSelect = useCallback( diff --git a/client/src/hooks/Agents/useAgentsMap.ts b/client/src/hooks/Agents/useAgentsMap.ts index 19a0b1d99..020990a5c 100644 --- a/client/src/hooks/Agents/useAgentsMap.ts +++ b/client/src/hooks/Agents/useAgentsMap.ts @@ -9,7 +9,7 @@ export default function useAgentsMap({ }: { isAuthenticated: boolean; }): TAgentsMap | undefined { - const { data: agentsList = null } = useListAgentsQuery( + const { data: mappedAgents = null } = useListAgentsQuery( { requiredPermission: PermissionBits.VIEW }, { select: (res) => mapAgents(res.data), @@ -17,9 +17,9 @@ export default function useAgentsMap({ }, ); - const agents = useMemo(() => { - return agentsList !== null ? agentsList : undefined; - }, [agentsList]); + const agentsMap = useMemo(() => { + return mappedAgents !== null ? mappedAgents : undefined; + }, [mappedAgents]); - return agents; + return agentsMap; } diff --git a/client/src/hooks/Endpoint/useEndpoints.ts b/client/src/hooks/Endpoint/useEndpoints.ts index 5a074a0fc..f43f8b535 100644 --- a/client/src/hooks/Endpoint/useEndpoints.ts +++ b/client/src/hooks/Endpoint/useEndpoints.ts @@ -1,71 +1,56 @@ import React, { useMemo, useCallback } from 'react'; import { useGetModelsQuery } from 'librechat-data-provider/react-query'; import { - EModelEndpoint, - PermissionTypes, Permissions, alternateName, + EModelEndpoint, + PermissionTypes, } from 'librechat-data-provider'; import type { - Agent, - Assistant, TEndpointsConfig, - TAgentsMap, TAssistantsMap, TStartupConfig, + Assistant, + Agent, } from 'librechat-data-provider'; import type { Endpoint } from '~/common'; import { mapEndpoints, getIconKey, getEndpointField } from '~/utils'; import { useGetEndpointsQuery } from '~/data-provider'; -import { useChatContext } from '~/Providers'; import { useHasAccess } from '~/hooks'; import { icons } from './Icons'; export const useEndpoints = ({ - agentsMap, + agents, assistantsMap, endpointsConfig, startupConfig, }: { - agentsMap?: TAgentsMap; + agents?: Agent[] | null; assistantsMap?: TAssistantsMap; endpointsConfig: TEndpointsConfig; startupConfig: TStartupConfig | undefined; }) => { const modelsQuery = useGetModelsQuery(); - const { conversation } = useChatContext(); const { data: endpoints = [] } = useGetEndpointsQuery({ select: mapEndpoints }); - const { instanceProjectId } = startupConfig ?? {}; const interfaceConfig = startupConfig?.interface ?? {}; const includedEndpoints = useMemo( () => new Set(startupConfig?.modelSpecs?.addedEndpoints ?? []), [startupConfig?.modelSpecs?.addedEndpoints], ); - const { endpoint } = conversation ?? {}; - const hasAgentAccess = useHasAccess({ permissionType: PermissionTypes.AGENTS, permission: Permissions.USE, }); - const agents = useMemo( - () => - Object.values(agentsMap ?? {}).filter( - (agent): agent is Agent & { name: string } => - agent !== undefined && 'id' in agent && 'name' in agent && agent.name !== null, - ), - [agentsMap], - ); - const assistants: Assistant[] = useMemo( () => Object.values(assistantsMap?.[EModelEndpoint.assistants] ?? {}), - [endpoint, assistantsMap], + [assistantsMap], ); const azureAssistants: Assistant[] = useMemo( () => Object.values(assistantsMap?.[EModelEndpoint.azureAssistants] ?? {}), - [endpoint, assistantsMap], + [assistantsMap], ); const filteredEndpoints = useMemo(() => { @@ -84,7 +69,7 @@ export const useEndpoints = ({ } return result; - }, [endpoints, hasAgentAccess, includedEndpoints]); + }, [endpoints, hasAgentAccess, includedEndpoints, interfaceConfig.modelSelect]); const endpointRequiresUserKey = useCallback( (ep: string) => { @@ -100,7 +85,7 @@ export const useEndpoints = ({ const Icon = icons[iconKey]; const endpointIconURL = getEndpointField(endpointsConfig, ep, 'iconURL'); const hasModels = - (ep === EModelEndpoint.agents && agents?.length > 0) || + (ep === EModelEndpoint.agents && (agents?.length ?? 0) > 0) || (ep === EModelEndpoint.assistants && assistants?.length > 0) || (ep !== EModelEndpoint.assistants && ep !== EModelEndpoint.agents && @@ -122,16 +107,16 @@ export const useEndpoints = ({ }; // Handle agents case - if (ep === EModelEndpoint.agents && agents.length > 0) { - result.models = agents.map((agent) => ({ + if (ep === EModelEndpoint.agents && (agents?.length ?? 0) > 0) { + result.models = agents?.map((agent) => ({ name: agent.id, isGlobal: agent.isPublic ?? false, })); - result.agentNames = agents.reduce((acc, agent) => { + result.agentNames = agents?.reduce((acc, agent) => { acc[agent.id] = agent.name || ''; return acc; }, {}); - result.modelIcons = agents.reduce((acc, agent) => { + result.modelIcons = agents?.reduce((acc, agent) => { acc[agent.id] = agent?.avatar?.filepath; return acc; }, {}); @@ -192,7 +177,7 @@ export const useEndpoints = ({ return result; }); - }, [filteredEndpoints, endpointsConfig, modelsQuery.data, agents, assistants]); + }, [filteredEndpoints, endpointsConfig, modelsQuery.data, agents, assistants, azureAssistants]); return { mappedEndpoints, diff --git a/client/src/utils/forms.tsx b/client/src/utils/forms.tsx index d8fa4cc8a..ae6ed69a6 100644 --- a/client/src/utils/forms.tsx +++ b/client/src/utils/forms.tsx @@ -57,11 +57,9 @@ export const getDefaultAgentFormValues = () => ({ export const processAgentOption = ({ agent: _agent, fileMap, - instanceProjectId, }: { agent?: Agent; fileMap?: Record; - instanceProjectId?: string; }): TAgentOption => { const isGlobal = _agent?.isPublic ?? false; const agent: TAgentOption = {