🔗 feat: Agent Chain (Mixture-of-Agents) (#6374)

* wip: first pass, dropdown for selecting sequential agents

* refactor: Improve agent selection logic and enhance performance in SequentialAgents component

* wip: seq. agents working ideas

* wip: sequential agents style change

* refactor: move agent form options/submission outside of AgentConfig

* refactor: prevent repeating code

* refactor: simplify current agent display in SequentialAgents component

* feat: persist  form value handling in AgentSelect component for agent_ids

* feat: first pass, sequential agnets agent update

* feat: enhance message display with agent updates and empty text handling

* chore: update Icon component to use EModelEndpoint for agent endpoints

* feat: update content type checks in BaseClient to use constants for better readability

* feat: adjust max context tokens calculation to use 90% of the model's max tokens

* feat: first pass, agent run message pruning

* chore: increase max listeners for abort controller to prevent memory leaks

* feat: enhance runAgent function to include current index count map for improved token tracking

* chore: update @librechat/agents dependency to version 2.2.5

* feat: update icons and style of SequentialAgents component for improved UI consistency

* feat: add AdvancedButton and AdvancedPanel components for enhanced agent settings navigation, update styling for agent form

* chore: adjust minimum height of AdvancedPanel component for better layout consistency

* chore: update @librechat/agents dependency to version 2.2.6

* feat: enhance message formatting by incorporating tool set into agent message processing, in order to allow better mix/matching of agents (as tool calls for tools not found in set will be stringified)

* refactor: reorder components in AgentConfig for improved readability and maintainability

* refactor: enhance layout of AgentUpdate component for improved visual structure

* feat: add DeepSeek provider to Bedrock settings and schemas

* feat: enhance link styling in mobile.css for better visibility and accessibility

* fix: update banner model import in update banner script; export Banner model

* refactor: `duplicateAgentHandler` to include tool_resources only for OCR context files

* feat: add 'qwen-vl' to visionModels for enhanced model support

* fix: change image format from JPEG to PNG in DALLE3 response

* feat: reorganize Advanced components and add localizations

* refactor: simplify JSX structure in AgentChain component to defer container styling to parent

* feat: add FormInput component for reusable input handling

* feat: make agent recursion limit configurable from builder

* feat: add support for agent capabilities chain in AdvancedPanel and update data-provider version

* feat: add maxRecursionLimit configuration for agents and update related documentation

* fix: update CONFIG_VERSION to 1.2.3 in data provider configuration

* feat: replace recursion limit input with MaxAgentSteps component and enhance input handling

* feat: enhance AgentChain component with hover card for additional information and update related labels

* fix: pass request and response objects to `createActionTool` when using assistant actions to prevent auth error

* feat: update AgentChain component layout to include agent count display

* feat: increase default max listeners and implement capability check function for agent chain

* fix: update link styles in mobile.css for better visibility in dark mode

* chore: temp. remove agents package while bumping shared packages

* chore: update @langchain/google-genai package to version 0.1.11

* chore: update @langchain/google-vertexai package to version 0.2.2

* chore: add @librechat/agents package at version 2.2.8

* feat: add deepseek.r1 model with token rate and context values for bedrock
This commit is contained in:
Danny Avila 2025-03-17 16:43:44 -04:00 committed by GitHub
parent bc690cc320
commit d6a17784dc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
57 changed files with 1626 additions and 1459 deletions

View file

@ -139,6 +139,7 @@ const ContentParts = memo(
isSubmitting={isSubmitting}
key={`part-${messageId}-${idx}`}
isCreatedByUser={isCreatedByUser}
isLast={idx === content.length - 1}
showCursor={idx === content.length - 1 && isLast}
/>
</MessageContext.Provider>

View file

@ -166,15 +166,12 @@ export const p: React.ElementType = memo(({ children }: TParagraphProps) => {
return <p className="mb-2 whitespace-pre-wrap">{children}</p>;
});
const cursor = ' ';
type TContentProps = {
content: string;
showCursor?: boolean;
isLatestMessage: boolean;
};
const Markdown = memo(({ content = '', showCursor, isLatestMessage }: TContentProps) => {
const Markdown = memo(({ content = '', isLatestMessage }: TContentProps) => {
const LaTeXParsing = useRecoilValue<boolean>(store.LaTeXParsing);
const isInitializing = content === '';
@ -240,7 +237,7 @@ const Markdown = memo(({ content = '', showCursor, isLatestMessage }: TContentPr
}
}
>
{isLatestMessage && (showCursor ?? false) ? currentContent + cursor : currentContent}
{currentContent}
</ReactMarkdown>
</CodeBlockProvider>
</ArtifactProvider>

View file

@ -83,9 +83,7 @@ const DisplayMessage = ({ text, isCreatedByUser, message, showCursor }: TDisplay
let content: React.ReactElement;
if (!isCreatedByUser) {
content = (
<Markdown content={text} showCursor={showCursorState} isLatestMessage={isLatestMessage} />
);
content = <Markdown content={text} isLatestMessage={isLatestMessage} />;
} else if (enableUserMsgMarkdown) {
content = <MarkdownLite content={text} />;
} else {

View file

@ -8,9 +8,11 @@ import {
import { memo } from 'react';
import type { TMessageContentParts, TAttachment } from 'librechat-data-provider';
import { ErrorMessage } from './MessageContent';
import AgentUpdate from './Parts/AgentUpdate';
import ExecuteCode from './Parts/ExecuteCode';
import RetrievalCall from './RetrievalCall';
import Reasoning from './Parts/Reasoning';
import EmptyText from './Parts/EmptyText';
import CodeAnalyze from './CodeAnalyze';
import Container from './Container';
import ToolCall from './ToolCall';
@ -20,145 +22,159 @@ import Image from './Image';
type PartProps = {
part?: TMessageContentParts;
isLast?: boolean;
isSubmitting: boolean;
showCursor: boolean;
isCreatedByUser: boolean;
attachments?: TAttachment[];
};
const Part = memo(({ part, isSubmitting, attachments, showCursor, isCreatedByUser }: PartProps) => {
if (!part) {
return null;
}
if (part.type === ContentTypes.ERROR) {
return (
<ErrorMessage
text={part[ContentTypes.ERROR] ?? part[ContentTypes.TEXT]?.value}
className="my-2"
/>
);
} else if (part.type === ContentTypes.TEXT) {
const text = typeof part.text === 'string' ? part.text : part.text.value;
if (typeof text !== 'string') {
return null;
}
if (part.tool_call_ids != null && !text) {
return null;
}
return (
<Container>
<Text text={text} isCreatedByUser={isCreatedByUser} showCursor={showCursor} />
</Container>
);
} else if (part.type === ContentTypes.THINK) {
const reasoning = typeof part.think === 'string' ? part.think : part.think.value;
if (typeof reasoning !== 'string') {
return null;
}
return <Reasoning reasoning={reasoning} />;
} else if (part.type === ContentTypes.TOOL_CALL) {
const toolCall = part[ContentTypes.TOOL_CALL];
if (!toolCall) {
const Part = memo(
({ part, isSubmitting, attachments, isLast, showCursor, isCreatedByUser }: PartProps) => {
if (!part) {
return null;
}
const isToolCall =
'args' in toolCall && (!toolCall.type || toolCall.type === ToolCallTypes.TOOL_CALL);
if (isToolCall && toolCall.name === Tools.execute_code) {
if (part.type === ContentTypes.ERROR) {
return (
<ExecuteCode
args={typeof toolCall.args === 'string' ? toolCall.args : ''}
output={toolCall.output ?? ''}
initialProgress={toolCall.progress ?? 0.1}
isSubmitting={isSubmitting}
attachments={attachments}
<ErrorMessage
text={part[ContentTypes.ERROR] ?? part[ContentTypes.TEXT]?.value}
className="my-2"
/>
);
} else if (isToolCall) {
} else if (part.type === ContentTypes.AGENT_UPDATE) {
return (
<ToolCall
args={toolCall.args ?? ''}
name={toolCall.name || ''}
output={toolCall.output ?? ''}
initialProgress={toolCall.progress ?? 0.1}
isSubmitting={isSubmitting}
attachments={attachments}
auth={toolCall.auth}
expires_at={toolCall.expires_at}
/>
);
} else if (toolCall.type === ToolCallTypes.CODE_INTERPRETER) {
const code_interpreter = toolCall[ToolCallTypes.CODE_INTERPRETER];
return (
<CodeAnalyze
initialProgress={toolCall.progress ?? 0.1}
code={code_interpreter.input}
outputs={code_interpreter.outputs ?? []}
isSubmitting={isSubmitting}
/>
);
} else if (
toolCall.type === ToolCallTypes.RETRIEVAL ||
toolCall.type === ToolCallTypes.FILE_SEARCH
) {
return (
<RetrievalCall initialProgress={toolCall.progress ?? 0.1} isSubmitting={isSubmitting} />
);
} else if (
toolCall.type === ToolCallTypes.FUNCTION &&
ToolCallTypes.FUNCTION in toolCall &&
imageGenTools.has(toolCall.function.name)
) {
return (
<ImageGen
initialProgress={toolCall.progress ?? 0.1}
args={toolCall.function.arguments as string}
/>
);
} else if (toolCall.type === ToolCallTypes.FUNCTION && ToolCallTypes.FUNCTION in toolCall) {
if (isImageVisionTool(toolCall)) {
if (isSubmitting && showCursor) {
return (
<>
<AgentUpdate currentAgentId={part[ContentTypes.AGENT_UPDATE]?.agentId} />
{isLast && showCursor && (
<Container>
<Text text={''} isCreatedByUser={isCreatedByUser} showCursor={showCursor} />
<EmptyText />
</Container>
);
}
)}
</>
);
} else if (part.type === ContentTypes.TEXT) {
const text = typeof part.text === 'string' ? part.text : part.text.value;
if (typeof text !== 'string') {
return null;
}
if (part.tool_call_ids != null && !text) {
return null;
}
return (
<Container>
<Text text={text} isCreatedByUser={isCreatedByUser} showCursor={showCursor} />
</Container>
);
} else if (part.type === ContentTypes.THINK) {
const reasoning = typeof part.think === 'string' ? part.think : part.think.value;
if (typeof reasoning !== 'string') {
return null;
}
return <Reasoning reasoning={reasoning} />;
} else if (part.type === ContentTypes.TOOL_CALL) {
const toolCall = part[ContentTypes.TOOL_CALL];
if (!toolCall) {
return null;
}
const isToolCall =
'args' in toolCall && (!toolCall.type || toolCall.type === ToolCallTypes.TOOL_CALL);
if (isToolCall && toolCall.name === Tools.execute_code) {
return (
<ExecuteCode
args={typeof toolCall.args === 'string' ? toolCall.args : ''}
output={toolCall.output ?? ''}
initialProgress={toolCall.progress ?? 0.1}
isSubmitting={isSubmitting}
attachments={attachments}
/>
);
} else if (isToolCall) {
return (
<ToolCall
args={toolCall.args ?? ''}
name={toolCall.name || ''}
output={toolCall.output ?? ''}
initialProgress={toolCall.progress ?? 0.1}
isSubmitting={isSubmitting}
attachments={attachments}
auth={toolCall.auth}
expires_at={toolCall.expires_at}
/>
);
} else if (toolCall.type === ToolCallTypes.CODE_INTERPRETER) {
const code_interpreter = toolCall[ToolCallTypes.CODE_INTERPRETER];
return (
<CodeAnalyze
initialProgress={toolCall.progress ?? 0.1}
code={code_interpreter.input}
outputs={code_interpreter.outputs ?? []}
isSubmitting={isSubmitting}
/>
);
} else if (
toolCall.type === ToolCallTypes.RETRIEVAL ||
toolCall.type === ToolCallTypes.FILE_SEARCH
) {
return (
<RetrievalCall initialProgress={toolCall.progress ?? 0.1} isSubmitting={isSubmitting} />
);
} else if (
toolCall.type === ToolCallTypes.FUNCTION &&
ToolCallTypes.FUNCTION in toolCall &&
imageGenTools.has(toolCall.function.name)
) {
return (
<ImageGen
initialProgress={toolCall.progress ?? 0.1}
args={toolCall.function.arguments as string}
/>
);
} else if (toolCall.type === ToolCallTypes.FUNCTION && ToolCallTypes.FUNCTION in toolCall) {
if (isImageVisionTool(toolCall)) {
if (isSubmitting && showCursor) {
return (
<Container>
<Text text={''} isCreatedByUser={isCreatedByUser} showCursor={showCursor} />
</Container>
);
}
return null;
}
return (
<ToolCall
initialProgress={toolCall.progress ?? 0.1}
isSubmitting={isSubmitting}
args={toolCall.function.arguments as string}
name={toolCall.function.name}
output={toolCall.function.output}
/>
);
}
} else if (part.type === ContentTypes.IMAGE_FILE) {
const imageFile = part[ContentTypes.IMAGE_FILE];
const height = imageFile.height ?? 1920;
const width = imageFile.width ?? 1080;
return (
<ToolCall
initialProgress={toolCall.progress ?? 0.1}
isSubmitting={isSubmitting}
args={toolCall.function.arguments as string}
name={toolCall.function.name}
output={toolCall.function.output}
<Image
imagePath={imageFile.filepath}
height={height}
width={width}
altText={imageFile.filename ?? 'Uploaded Image'}
placeholderDimensions={{
height: height + 'px',
width: width + 'px',
}}
/>
);
}
} else if (part.type === ContentTypes.IMAGE_FILE) {
const imageFile = part[ContentTypes.IMAGE_FILE];
const height = imageFile.height ?? 1920;
const width = imageFile.width ?? 1080;
return (
<Image
imagePath={imageFile.filepath}
height={height}
width={width}
altText={imageFile.filename ?? 'Uploaded Image'}
placeholderDimensions={{
height: height + 'px',
width: width + 'px',
}}
/>
);
}
return null;
});
return null;
},
);
export default Part;

View file

@ -0,0 +1,39 @@
import React, { useMemo } from 'react';
import { EModelEndpoint } from 'librechat-data-provider';
import { useAgentsMapContext } from '~/Providers';
import Icon from '~/components/Endpoints/Icon';
interface AgentUpdateProps {
currentAgentId: string;
}
const AgentUpdate: React.FC<AgentUpdateProps> = ({ currentAgentId }) => {
const agentsMap = useAgentsMapContext() || {};
const currentAgent = useMemo(() => agentsMap[currentAgentId], [agentsMap, currentAgentId]);
if (!currentAgentId) {
return null;
}
return (
<div className="relative">
<div className="absolute -left-6 flex h-full w-4 items-center justify-center">
<div className="relative h-full w-4">
<div className="absolute left-0 top-0 h-1/2 w-px border border-border-medium"></div>
<div className="absolute left-0 top-1/2 h-px w-3 border border-border-medium"></div>
</div>
</div>
<div className="my-4 flex items-center gap-2">
<div className="flex h-6 w-6 items-center justify-center overflow-hidden rounded-full">
<Icon
endpoint={EModelEndpoint.agents}
agentName={currentAgent?.name ?? ''}
iconURL={currentAgent?.avatar?.filepath}
isCreatedByUser={false}
/>
</div>
<div className="font-medium text-text-primary">{currentAgent?.name}</div>
</div>
</div>
);
};
export default AgentUpdate;

View file

@ -0,0 +1,17 @@
import { memo } from 'react';
const EmptyTextPart = memo(() => {
return (
<div className="text-message mb-[0.625rem] flex min-h-[20px] flex-col items-start gap-3 overflow-visible">
<div className="markdown prose dark:prose-invert light w-full break-words dark:text-gray-100">
<div className="absolute">
<p className="submitting relative">
<span className="result-thinking" />
</p>
</div>
</div>
</div>
);
});
export default EmptyTextPart;

View file

@ -29,9 +29,7 @@ const TextPart = memo(({ text, isCreatedByUser, showCursor }: TextPartProps) =>
const content: ContentType = useMemo(() => {
if (!isCreatedByUser) {
return (
<Markdown content={text} showCursor={showCursorState} isLatestMessage={isLatestMessage} />
);
return <Markdown content={text} isLatestMessage={isLatestMessage} />;
} else if (enableUserMsgMarkdown) {
return <MarkdownLite content={text} />;
} else {

View file

@ -142,7 +142,7 @@ const AdminSettings = () => {
<Button
size={'sm'}
variant={'outline'}
className="btn btn-neutral border-token-border-light relative mb-4 h-9 w-full gap-1 rounded-lg font-medium"
className="btn btn-neutral border-token-border-light relative h-9 w-full gap-1 rounded-lg font-medium"
>
<ShieldEllipsis className="cursor-pointer" aria-hidden="true" />
{localize('com_ui_admin_settings')}

View file

@ -0,0 +1,27 @@
import React from 'react';
import { Settings2 } from 'lucide-react';
import { Button } from '~/components/ui';
import { useLocalize } from '~/hooks';
import { Panel } from '~/common';
interface AdvancedButtonProps {
setActivePanel: (panel: Panel) => void;
}
const AdvancedButton: React.FC<AdvancedButtonProps> = ({ setActivePanel }) => {
const localize = useLocalize();
return (
<Button
size={'sm'}
variant={'outline'}
className="btn btn-neutral border-token-border-light relative h-9 w-full gap-1 rounded-lg font-medium"
onClick={() => setActivePanel(Panel.advanced)}
>
<Settings2 className="h-4 w-4 cursor-pointer" aria-hidden="true" />
{localize('com_ui_advanced')}
</Button>
);
};
export default AdvancedButton;

View file

@ -0,0 +1,55 @@
import { useMemo } from 'react';
import { ChevronLeft } from 'lucide-react';
import { AgentCapabilities } from 'librechat-data-provider';
import { useFormContext, Controller } from 'react-hook-form';
import type { AgentForm, AgentPanelProps } from '~/common';
import MaxAgentSteps from './MaxAgentSteps';
import AgentChain from './AgentChain';
import { useLocalize } from '~/hooks';
import { Panel } from '~/common';
export default function AdvancedPanel({
agentsConfig,
setActivePanel,
}: Pick<AgentPanelProps, 'setActivePanel' | 'agentsConfig'>) {
const localize = useLocalize();
const methods = useFormContext<AgentForm>();
const { control, watch } = methods;
const currentAgentId = watch('id');
const chainEnabled = useMemo(
() => agentsConfig?.capabilities.includes(AgentCapabilities.chain) ?? false,
[agentsConfig],
);
return (
<div className="scrollbar-gutter-stable h-full min-h-[40vh] overflow-auto pb-12 text-sm">
<div className="advanced-panel relative flex flex-col items-center px-16 py-4 text-center">
<div className="absolute left-0 top-4">
<button
type="button"
className="btn btn-neutral relative"
onClick={() => {
setActivePanel(Panel.builder);
}}
>
<div className="advanced-panel-content flex w-full items-center justify-center gap-2">
<ChevronLeft />
</div>
</button>
</div>
<div className="mb-2 mt-2 text-xl font-medium">{localize('com_ui_advanced_settings')}</div>
</div>
<div className="flex flex-col gap-4 px-2">
<MaxAgentSteps />
{chainEnabled && (
<Controller
name="agent_ids"
control={control}
defaultValue={[]}
render={({ field }) => <AgentChain field={field} currentAgentId={currentAgentId} />}
/>
)}
</div>
</div>
);
}

View file

@ -0,0 +1,179 @@
import { X, Link2, PlusCircle } from 'lucide-react';
import { EModelEndpoint } from 'librechat-data-provider';
import React, { useState, useMemo, useCallback, useEffect } from 'react';
import type { ControllerRenderProps } from 'react-hook-form';
import type { AgentForm, OptionWithIcon } from '~/common';
import ControlCombobox from '~/components/ui/ControlCombobox';
import { HoverCard, HoverCardPortal, HoverCardContent, HoverCardTrigger } from '~/components/ui';
import { CircleHelpIcon } from '~/components/svg';
import { useAgentsMapContext } from '~/Providers';
import Icon from '~/components/Endpoints/Icon';
import { useLocalize } from '~/hooks';
import { ESide } from '~/common';
interface AgentChainProps {
field: ControllerRenderProps<AgentForm, 'agent_ids'>;
currentAgentId: string;
}
/** TODO: make configurable */
const MAX_AGENTS = 10;
const AgentChain: React.FC<AgentChainProps> = ({ field, currentAgentId }) => {
const localize = useLocalize();
const [newAgentId, setNewAgentId] = useState('');
const agentsMap = useAgentsMapContext() || {};
const agentIds = field.value || [];
const agents = useMemo(() => Object.values(agentsMap), [agentsMap]);
const selectableAgents = useMemo(
() =>
agents
.filter((agent) => agent?.id !== currentAgentId)
.map(
(agent) =>
({
label: agent?.name || '',
value: agent?.id,
icon: (
<Icon
endpoint={EModelEndpoint.agents}
agentName={agent?.name ?? ''}
iconURL={agent?.avatar?.filepath}
isCreatedByUser={false}
/>
),
}) as OptionWithIcon,
),
[agents, currentAgentId],
);
const getAgentDetails = useCallback((id: string) => agentsMap[id], [agentsMap]);
useEffect(() => {
if (newAgentId && agentIds.length < MAX_AGENTS) {
field.onChange([...agentIds, newAgentId]);
setNewAgentId('');
}
}, [newAgentId, agentIds, field]);
const removeAgentAt = (index: number) => {
field.onChange(agentIds.filter((_, i) => i !== index));
};
const updateAgentAt = (index: number, id: string) => {
const updated = [...agentIds];
updated[index] = id;
field.onChange(updated);
};
return (
<HoverCard openDelay={50}>
<div className="flex items-center justify-between gap-2">
<div className="flex items-center gap-2">
<label className="font-semibold text-text-primary">
{localize('com_ui_agent_chain')}
</label>
<HoverCardTrigger>
<CircleHelpIcon className="h-4 w-4 text-text-tertiary" />
</HoverCardTrigger>
</div>
<div className="text-xs text-text-secondary">
{agentIds.length} / {MAX_AGENTS}
</div>
</div>
<div className="space-y-1">
{/* Current fixed agent */}
<div className="flex h-10 items-center justify-between rounded-md border border-border-medium bg-surface-primary-contrast px-3 py-2">
<div className="flex items-center gap-2">
<div className="flex h-6 w-6 items-center justify-center overflow-hidden rounded-full">
<Icon
endpoint={EModelEndpoint.agents}
agentName={getAgentDetails(currentAgentId)?.name ?? ''}
iconURL={getAgentDetails(currentAgentId)?.avatar?.filepath}
isCreatedByUser={false}
/>
</div>
<div className="font-medium text-text-primary">
{getAgentDetails(currentAgentId)?.name}
</div>
</div>
</div>
{<Link2 className="mx-auto text-text-secondary" size={14} />}
{agentIds.map((agentId, idx) => (
<React.Fragment key={agentId}>
<div className="flex h-10 items-center gap-2 rounded-md border border-border-medium bg-surface-tertiary pr-2">
<ControlCombobox
isCollapsed={false}
ariaLabel={localize('com_ui_agent_var', { 0: localize('com_ui_select') })}
selectedValue={agentId}
setValue={(id) => updateAgentAt(idx, id)}
selectPlaceholder={localize('com_ui_agent_var', { 0: localize('com_ui_select') })}
searchPlaceholder={localize('com_ui_agent_var', { 0: localize('com_ui_search') })}
items={selectableAgents}
displayValue={getAgentDetails(agentId)?.name ?? ''}
SelectIcon={
<Icon
endpoint={EModelEndpoint.agents}
isCreatedByUser={false}
agentName={getAgentDetails(agentId)?.name ?? ''}
iconURL={getAgentDetails(agentId)?.avatar?.filepath}
/>
}
className="flex-1 border-border-heavy"
containerClassName="px-0"
/>
{/* Future Settings button? */}
{/* <button className="hover:bg-surface-hover p-1 rounded transition">
<Settings size={16} className="text-text-secondary" />
</button> */}
<button
className="rounded-xl p-1 transition hover:bg-surface-hover"
onClick={() => removeAgentAt(idx)}
>
<X size={18} className="text-text-secondary" />
</button>
</div>
{idx < agentIds.length - 1 && (
<Link2 className="mx-auto text-text-secondary" size={14} />
)}
</React.Fragment>
))}
{agentIds.length < MAX_AGENTS && (
<>
{agentIds.length > 0 && <Link2 className="mx-auto text-text-secondary" size={14} />}
<ControlCombobox
isCollapsed={false}
ariaLabel={localize('com_ui_agent_var', { 0: localize('com_ui_add') })}
selectedValue=""
setValue={setNewAgentId}
selectPlaceholder={localize('com_ui_agent_var', { 0: localize('com_ui_add') })}
searchPlaceholder={localize('com_ui_agent_var', { 0: localize('com_ui_search') })}
items={selectableAgents}
className="h-10 w-full border-dashed border-border-heavy text-center text-text-secondary hover:text-text-primary"
containerClassName="px-0"
SelectIcon={<PlusCircle size={16} className="text-text-secondary" />}
/>
</>
)}
{agentIds.length >= MAX_AGENTS && (
<p className="pt-1 text-center text-xs italic text-text-tertiary">
{localize('com_ui_agent_chain_max', { 0: MAX_AGENTS })}
</p>
)}
</div>
<HoverCardPortal>
<HoverCardContent side={ESide.Top} className="w-80">
<div className="space-y-2">
<p className="text-sm text-text-secondary">{localize('com_ui_agent_chain_info')}</p>
</div>
</HoverCardContent>
</HoverCardPortal>
</HoverCard>
);
};
export default AgentChain;

View file

@ -0,0 +1,52 @@
import { useFormContext, Controller } from 'react-hook-form';
import type { AgentForm } from '~/common';
import {
HoverCard,
FormInput,
HoverCardPortal,
HoverCardContent,
HoverCardTrigger,
} from '~/components/ui';
import { CircleHelpIcon } from '~/components/svg';
import { useLocalize } from '~/hooks';
import { ESide } from '~/common';
export default function AdvancedPanel() {
const localize = useLocalize();
const methods = useFormContext<AgentForm>();
const { control } = methods;
return (
<HoverCard openDelay={50}>
<Controller
name="recursion_limit"
control={control}
render={({ field }) => (
<FormInput
field={field}
containerClass="w-1/2"
inputClass="w-full"
label={localize('com_ui_agent_recursion_limit')}
placeholder={localize('com_nav_theme_system')}
type="number"
labelClass="w-fit"
labelAdjacent={
<HoverCardTrigger>
<CircleHelpIcon className="h-4 w-4 text-text-tertiary" />
</HoverCardTrigger>
}
/>
)}
/>
<HoverCardPortal>
<HoverCardContent side={ESide.Top} className="w-80">
<div className="space-y-2">
<p className="text-sm text-text-secondary">
{localize('com_ui_agent_recursion_limit_info')}
</p>
</div>
</HoverCardContent>
</HoverCardPortal>
</HoverCard>
);
}

View file

@ -1,32 +1,19 @@
import React, { useState, useMemo, useCallback } from 'react';
import { useQueryClient } from '@tanstack/react-query';
import { Controller, useWatch, useFormContext } from 'react-hook-form';
import {
QueryKeys,
SystemRoles,
Permissions,
EModelEndpoint,
PermissionTypes,
AgentCapabilities,
} from 'librechat-data-provider';
import { QueryKeys, EModelEndpoint, AgentCapabilities } from 'librechat-data-provider';
import type { TPlugin } from 'librechat-data-provider';
import type { AgentForm, AgentPanelProps, IconComponentTypes } from '~/common';
import { cn, defaultTextProps, removeFocusOutlines, getEndpointField, getIconKey } from '~/utils';
import { useCreateAgentMutation, useUpdateAgentMutation } from '~/data-provider';
import { useLocalize, useAuthContext, useHasAccess } from '~/hooks';
import { useToastContext, useFileMapContext } from '~/Providers';
import { icons } from '~/components/Chat/Menus/Endpoints/Icons';
import Action from '~/components/SidePanel/Builder/Action';
import { ToolSelectDialog } from '~/components/Tools';
import DuplicateAgent from './DuplicateAgent';
import { processAgentOption } from '~/utils';
import AdminSettings from './AdminSettings';
import DeleteButton from './DeleteButton';
import AgentAvatar from './AgentAvatar';
import FileContext from './FileContext';
import { Spinner } from '~/components';
import { useLocalize } from '~/hooks';
import FileSearch from './FileSearch';
import ShareAgent from './ShareAgent';
import Artifacts from './Artifacts';
import AgentTool from './AgentTool';
import CodeForm from './Code/Form';
@ -43,11 +30,10 @@ export default function AgentConfig({
setAction,
actions = [],
agentsConfig,
endpointsConfig,
createMutation,
setActivePanel,
setCurrentAgentId,
endpointsConfig,
}: AgentPanelProps) {
const { user } = useAuthContext();
const fileMap = useFileMapContext();
const queryClient = useQueryClient();
@ -66,11 +52,6 @@ export default function AgentConfig({
const tools = useWatch({ control, name: 'tools' });
const agent_id = useWatch({ control, name: 'id' });
const hasAccessToShareAgents = useHasAccess({
permissionType: PermissionTypes.AGENTS,
permission: Permissions.SHARED_GLOBAL,
});
const toolsEnabled = useMemo(
() => agentsConfig?.capabilities.includes(AgentCapabilities.tools),
[agentsConfig],
@ -156,46 +137,6 @@ export default function AgentConfig({
return _agent.code_files ?? [];
}, [agent, agent_id, fileMap]);
/* Mutations */
const update = useUpdateAgentMutation({
onSuccess: (data) => {
showToast({
message: `${localize('com_assistants_update_success')} ${
data.name ?? localize('com_ui_agent')
}`,
});
},
onError: (err) => {
const error = err as Error;
showToast({
message: `${localize('com_agents_update_error')}${
error.message ? ` ${localize('com_ui_error')}: ${error.message}` : ''
}`,
status: 'error',
});
},
});
const create = useCreateAgentMutation({
onSuccess: (data) => {
setCurrentAgentId(data.id);
showToast({
message: `${localize('com_assistants_create_success')} ${
data.name ?? localize('com_ui_agent')
}`,
});
},
onError: (err) => {
const error = err as Error;
showToast({
message: `${localize('com_agents_create_error')}${
error.message ? ` ${localize('com_ui_error')}: ${error.message}` : ''
}`,
status: 'error',
});
},
});
const handleAddActions = useCallback(() => {
if (!agent_id) {
showToast({
@ -225,26 +166,14 @@ export default function AgentConfig({
Icon = icons[iconKey];
}
const renderSaveButton = () => {
if (create.isLoading || update.isLoading) {
return <Spinner className="icon-md" aria-hidden="true" />;
}
if (agent_id) {
return localize('com_ui_save');
}
return localize('com_ui_create');
};
return (
<>
<div className="h-auto bg-white px-4 pb-8 pt-3 dark:bg-transparent">
<div className="h-auto bg-white px-4 pt-3 dark:bg-transparent">
{/* Avatar & Name */}
<div className="mb-4">
<AgentAvatar
createMutation={create}
agent_id={agent_id}
createMutation={createMutation}
avatar={agent?.['avatar'] ?? null}
/>
<label className={labelClass} htmlFor="name">
@ -366,12 +295,12 @@ export default function AgentConfig({
</label>
{/* Code Execution */}
{codeEnabled && <CodeForm agent_id={agent_id} files={code_files} />}
{/* File Search */}
{fileSearchEnabled && <FileSearch agent_id={agent_id} files={knowledge_files} />}
{/* Artifacts */}
{artifactsEnabled && <Artifacts />}
{/* File Context (OCR) */}
{ocrEnabled && <FileContext agent_id={agent_id} files={context_files} />}
{/* Artifacts */}
{artifactsEnabled && <Artifacts />}
{/* File Search */}
{fileSearchEnabled && <FileSearch agent_id={agent_id} files={knowledge_files} />}
</div>
)}
{/* Agent Tools & Actions */}
@ -431,34 +360,6 @@ export default function AgentConfig({
</div>
</div>
</div>
{user?.role === SystemRoles.ADMIN && <AdminSettings />}
{/* Context Button */}
<div className="flex items-center justify-end gap-2">
<DeleteButton
agent_id={agent_id}
setCurrentAgentId={setCurrentAgentId}
createMutation={create}
/>
{(agent?.author === user?.id || user?.role === SystemRoles.ADMIN) &&
hasAccessToShareAgents && (
<ShareAgent
agent_id={agent_id}
agentName={agent?.name ?? ''}
projectIds={agent?.projectIds ?? []}
isCollaborative={agent?.isCollaborative}
/>
)}
{agent && agent.author === user?.id && <DuplicateAgent agent_id={agent_id} />}
{/* Submit Button */}
<button
className="btn btn-primary focus:shadow-outline flex h-9 w-full items-center justify-center px-4 py-2 font-semibold text-white hover:bg-green-600 focus:border-green-500"
type="submit"
disabled={create.isLoading || update.isLoading}
aria-busy={create.isLoading || update.isLoading}
>
{renderSaveButton()}
</button>
</div>
</div>
<ToolSelectDialog
isOpen={showToolDialog}

View file

@ -0,0 +1,86 @@
import React from 'react';
import { useWatch, useFormContext } from 'react-hook-form';
import { SystemRoles, Permissions, PermissionTypes } from 'librechat-data-provider';
import type { AgentForm, AgentPanelProps } from '~/common';
import { useLocalize, useAuthContext, useHasAccess } from '~/hooks';
import { useUpdateAgentMutation } from '~/data-provider';
import AdvancedButton from './Advanced/AdvancedButton';
import DuplicateAgent from './DuplicateAgent';
import AdminSettings from './AdminSettings';
import DeleteButton from './DeleteButton';
import { Spinner } from '~/components';
import ShareAgent from './ShareAgent';
import { Panel } from '~/common';
export default function AgentFooter({
activePanel,
createMutation,
updateMutation,
setActivePanel,
setCurrentAgentId,
}: Pick<
AgentPanelProps,
'setCurrentAgentId' | 'createMutation' | 'activePanel' | 'setActivePanel'
> & {
updateMutation: ReturnType<typeof useUpdateAgentMutation>;
}) {
const localize = useLocalize();
const { user } = useAuthContext();
const methods = useFormContext<AgentForm>();
const { control } = methods;
const agent = useWatch({ control, name: 'agent' });
const agent_id = useWatch({ control, name: 'id' });
const hasAccessToShareAgents = useHasAccess({
permissionType: PermissionTypes.AGENTS,
permission: Permissions.SHARED_GLOBAL,
});
const renderSaveButton = () => {
if (createMutation.isLoading || updateMutation.isLoading) {
return <Spinner className="icon-md" aria-hidden="true" />;
}
if (agent_id) {
return localize('com_ui_save');
}
return localize('com_ui_create');
};
return (
<div className="mx-1 mb-1 flex w-full flex-col gap-2">
{activePanel !== Panel.advanced && <AdvancedButton setActivePanel={setActivePanel} />}
{user?.role === SystemRoles.ADMIN && <AdminSettings />}
{/* Context Button */}
<div className="flex items-center justify-end gap-2">
<DeleteButton
agent_id={agent_id}
setCurrentAgentId={setCurrentAgentId}
createMutation={createMutation}
/>
{(agent?.author === user?.id || user?.role === SystemRoles.ADMIN) &&
hasAccessToShareAgents && (
<ShareAgent
agent_id={agent_id}
agentName={agent?.name ?? ''}
projectIds={agent?.projectIds ?? []}
isCollaborative={agent?.isCollaborative}
/>
)}
{agent && agent.author === user?.id && <DuplicateAgent agent_id={agent_id} />}
{/* Submit Button */}
<button
className="btn btn-primary focus:shadow-outline flex h-9 w-full items-center justify-center px-4 py-2 font-semibold text-white hover:bg-green-600 focus:border-green-500"
type="submit"
disabled={createMutation.isLoading || updateMutation.isLoading}
aria-busy={createMutation.isLoading || updateMutation.isLoading}
>
{renderSaveButton()}
</button>
</div>
</div>
);
}

View file

@ -19,8 +19,10 @@ import { useSelectAgent, useLocalize, useAuthContext } from '~/hooks';
import AgentPanelSkeleton from './AgentPanelSkeleton';
import { createProviderOption } from '~/utils';
import { useToastContext } from '~/Providers';
import AdvancedPanel from './Advanced/AdvancedPanel';
import AgentConfig from './AgentConfig';
import AgentSelect from './AgentSelect';
import AgentFooter from './AgentFooter';
import { Button } from '~/components';
import ModelPanel from './ModelPanel';
import { Panel } from '~/common';
@ -130,6 +132,7 @@ export default function AgentPanel({
agent_ids,
end_after_tools,
hide_sequential_outputs,
recursion_limit,
} = data;
const model = _model ?? '';
@ -151,6 +154,7 @@ export default function AgentPanel({
agent_ids,
end_after_tools,
hide_sequential_outputs,
recursion_limit,
},
});
return;
@ -175,6 +179,7 @@ export default function AgentPanel({
agent_ids,
end_after_tools,
hide_sequential_outputs,
recursion_limit,
});
},
[agent_id, create, update, showToast, localize],
@ -276,12 +281,25 @@ export default function AgentPanel({
<AgentConfig
actions={actions}
setAction={setAction}
createMutation={create}
agentsConfig={agentsConfig}
setActivePanel={setActivePanel}
endpointsConfig={endpointsConfig}
setCurrentAgentId={setCurrentAgentId}
/>
)}
{canEditAgent && !agentQuery.isInitialLoading && activePanel === Panel.advanced && (
<AdvancedPanel setActivePanel={setActivePanel} agentsConfig={agentsConfig} />
)}
{canEditAgent && !agentQuery.isInitialLoading && (
<AgentFooter
createMutation={create}
updateMutation={update}
activePanel={activePanel}
setActivePanel={setActivePanel}
setCurrentAgentId={setCurrentAgentId}
/>
)}
</form>
</FormProvider>
);

View file

@ -3,7 +3,7 @@ import { Skeleton } from '~/components/ui';
export default function AgentPanelSkeleton() {
return (
<div className="h-auto bg-white px-4 pb-8 pt-3 dark:bg-transparent">
<div className="h-auto bg-white dark:bg-transparent">
{/* Avatar */}
<div className="mb-4">
<div className="flex w-full items-center justify-center gap-4">

View file

@ -81,10 +81,29 @@ export default function AgentSelect({
return;
}
if (capabilities[name] !== undefined) {
formValues[name] = value;
return;
}
if (
name === 'agent_ids' &&
Array.isArray(value) &&
value.every((item) => typeof item === 'string')
) {
formValues[name] = value;
return;
}
if (!keys.has(name)) {
return;
}
if (name === 'recursion_limit' && typeof value === 'number') {
formValues[name] = value;
return;
}
if (typeof value !== 'number' && typeof value !== 'object') {
formValues[name] = value;
}

View file

@ -77,7 +77,7 @@ export default function Parameters({
};
return (
<div className="scrollbar-gutter-stable h-full min-h-[50vh] overflow-auto pb-12 text-sm">
<div className="mx-1 mb-1 flex h-full min-h-[50vh] w-full flex-col gap-2 text-sm">
<div className="model-panel relative flex flex-col items-center px-16 py-4 text-center">
<div className="absolute left-0 top-4">
<button
@ -224,19 +224,17 @@ export default function Parameters({
);
})}
</div>
{/* Reset Parameters Button */}
<div className="mt-6 flex justify-center">
<button
type="button"
onClick={handleResetParameters}
className="btn btn-neutral flex w-full items-center justify-center gap-2 px-4 py-2 text-sm"
>
<RotateCcw className="h-4 w-4" aria-hidden="true" />
{localize('com_ui_reset_var', { 0: localize('com_ui_model_parameters') })}
</button>
</div>
</div>
)}
{/* Reset Parameters Button */}
<button
type="button"
onClick={handleResetParameters}
className="btn btn-neutral my-1 flex w-full items-center justify-center gap-2 px-4 py-2 text-sm"
>
<RotateCcw className="h-4 w-4" aria-hidden="true" />
{localize('com_ui_reset_var', { 0: localize('com_ui_model_parameters') })}
</button>
</div>
);
}

View file

@ -1,74 +0,0 @@
import { AgentCapabilities } from 'librechat-data-provider';
import { useFormContext, Controller } from 'react-hook-form';
import type { AgentForm } from '~/common';
import {
Checkbox,
HoverCard,
// HoverCardContent,
// HoverCardPortal,
// HoverCardTrigger,
} from '~/components/ui';
// import { CircleHelpIcon } from '~/components/svg';
// import { useLocalize } from '~/hooks';
// import { ESide } from '~/common';
export default function HideSequential() {
// const localize = useLocalize();
const methods = useFormContext<AgentForm>();
const { control, setValue, getValues } = methods;
return (
<>
<HoverCard openDelay={50}>
<div className="my-2 flex items-center">
<Controller
name={AgentCapabilities.hide_sequential_outputs}
control={control}
render={({ field }) => (
<Checkbox
{...field}
checked={field.value}
onCheckedChange={field.onChange}
className="relative float-left mr-2 inline-flex h-4 w-4 cursor-pointer"
value={field.value?.toString()}
/>
)}
/>
<button
type="button"
className="flex items-center space-x-2"
onClick={() =>
setValue(
AgentCapabilities.hide_sequential_outputs,
!getValues(AgentCapabilities.hide_sequential_outputs),
{
shouldDirty: true,
},
)
}
>
<label
className="form-check-label text-token-text-primary w-full cursor-pointer"
htmlFor={AgentCapabilities.hide_sequential_outputs}
>
Hide Sequential Agent Outputs except the last agent&apos;s
</label>
{/* <HoverCardTrigger>
<CircleHelpIcon className="h-5 w-5 text-gray-500" />
</HoverCardTrigger> */}
</button>
{/* <HoverCardPortal>
<HoverCardContent side={ESide.Top} className="w-80">
<div className="space-y-2">
<p className="text-sm text-text-secondary">
{localize('com_agents_ttg_info')}
</p>
</div>
</HoverCardContent>
</HoverCardPortal> */}
</div>
</HoverCard>
</>
);
}

View file

@ -1,153 +0,0 @@
import { Plus, X } from 'lucide-react';
import React, { useRef, useState } from 'react';
import { Transition } from 'react-transition-group';
import { Constants } from 'librechat-data-provider';
import { cn, defaultTextProps, removeFocusOutlines } from '~/utils';
import { TooltipAnchor } from '~/components/ui';
import HideSequential from './HideSequential';
interface SequentialAgentsProps {
field: {
value: string[];
onChange: (value: string[]) => void;
};
}
const labelClass = 'mb-2 text-token-text-primary block font-medium';
const inputClass = cn(
defaultTextProps,
'flex w-full px-3 py-2 dark:border-gray-800 dark:bg-gray-800 rounded-xl mb-2',
removeFocusOutlines,
);
const maxAgents = 5;
const SequentialAgents: React.FC<SequentialAgentsProps> = ({ field }) => {
const inputRefs = useRef<(HTMLInputElement | null)[]>([]);
const nodeRef = useRef(null);
const [newAgentId, setNewAgentId] = useState('');
const handleAddAgentId = () => {
if (newAgentId.trim() && field.value.length < maxAgents) {
const newValues = [...field.value, newAgentId];
field.onChange(newValues);
setNewAgentId('');
}
};
const handleDeleteAgentId = (index: number) => {
const newValues = field.value.filter((_, i) => i !== index);
field.onChange(newValues);
};
const defaultStyle = {
transition: 'opacity 200ms ease-in-out',
opacity: 0,
};
const triggerShake = (element: HTMLElement) => {
element.classList.remove('shake');
void element.offsetWidth;
element.classList.add('shake');
setTimeout(() => {
element.classList.remove('shake');
}, 200);
};
const transitionStyles = {
entering: { opacity: 1 },
entered: { opacity: 1 },
exiting: { opacity: 0 },
exited: { opacity: 0 },
};
const hasReachedMax = field.value.length >= Constants.MAX_CONVO_STARTERS;
return (
<div className="relative">
<label className={labelClass} htmlFor="agent_ids">
Sequential Agents
</label>
<div className="mt-4 space-y-2">
<HideSequential />
{/* Display existing agents first */}
{field.value.map((agentId, index) => (
<div key={index} className="relative">
<input
ref={(el) => (inputRefs.current[index] = el)}
value={agentId}
onChange={(e) => {
const newValue = [...field.value];
newValue[index] = e.target.value;
field.onChange(newValue);
}}
className={`${inputClass} pr-10`}
type="text"
maxLength={64}
/>
<TooltipAnchor
side="top"
description={'Remove agent ID'}
className="absolute right-1 top-1 flex size-7 items-center justify-center rounded-lg transition-colors duration-200 hover:bg-surface-hover"
onClick={() => handleDeleteAgentId(index)}
>
<X className="size-4" />
</TooltipAnchor>
</div>
))}
{/* Input for new agent at the bottom */}
<div className="relative">
<input
ref={(el) => (inputRefs.current[field.value.length] = el)}
value={newAgentId}
maxLength={64}
className={`${inputClass} pr-10`}
type="text"
placeholder={hasReachedMax ? 'Max agents reached' : 'Enter agent ID (e.g. agent_1234)'}
onChange={(e) => setNewAgentId(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
if (hasReachedMax) {
triggerShake(e.currentTarget);
} else {
handleAddAgentId();
}
}
}}
/>
<Transition
nodeRef={nodeRef}
in={field.value.length < Constants.MAX_CONVO_STARTERS}
timeout={200}
unmountOnExit
>
{(state: string) => (
<div
ref={nodeRef}
style={{
...defaultStyle,
...transitionStyles[state as keyof typeof transitionStyles],
transition: state === 'entering' ? 'none' : defaultStyle.transition,
}}
className="absolute right-1 top-1"
>
<TooltipAnchor
side="top"
description={hasReachedMax ? 'Max agents reached' : 'Add agent ID'}
className="flex size-7 items-center justify-center rounded-lg transition-colors duration-200 hover:bg-surface-hover"
onClick={handleAddAgentId}
disabled={hasReachedMax}
>
<Plus className="size-4" />
</TooltipAnchor>
</div>
)}
</Transition>
</div>
</div>
</div>
);
};
export default SequentialAgents;

View file

@ -665,6 +665,7 @@ export const settings: Record<string, SettingsConfiguration | undefined> = {
[`${EModelEndpoint.bedrock}-${BedrockProviders.Meta}`]: bedrockGeneral,
[`${EModelEndpoint.bedrock}-${BedrockProviders.AI21}`]: bedrockGeneral,
[`${EModelEndpoint.bedrock}-${BedrockProviders.Amazon}`]: bedrockGeneral,
[`${EModelEndpoint.bedrock}-${BedrockProviders.DeepSeek}`]: bedrockGeneral,
[EModelEndpoint.google]: googleConfig,
};
@ -708,6 +709,7 @@ export const presetSettings: Record<
[`${EModelEndpoint.bedrock}-${BedrockProviders.Meta}`]: bedrockGeneralColumns,
[`${EModelEndpoint.bedrock}-${BedrockProviders.AI21}`]: bedrockGeneralColumns,
[`${EModelEndpoint.bedrock}-${BedrockProviders.Amazon}`]: bedrockGeneralColumns,
[`${EModelEndpoint.bedrock}-${BedrockProviders.DeepSeek}`]: bedrockGeneralColumns,
[EModelEndpoint.google]: {
col1: googleCol1,
col2: googleCol2,

View file

@ -0,0 +1,62 @@
import React from 'react';
import { Label, Input } from '~/components/ui';
import { cn } from '~/utils';
export default function FormInput({
field,
label,
labelClass,
inputClass,
containerClass,
labelAdjacent,
placeholder = '',
type = 'string',
}: {
field: any;
label: string;
labelClass?: string;
inputClass?: string;
placeholder?: string;
containerClass?: string;
type?: 'string' | 'number';
labelAdjacent?: React.ReactNode;
}) {
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
if (type !== 'number') {
field.onChange(value);
return;
}
if (value === '') {
field.onChange(value);
} else if (!isNaN(Number(value))) {
field.onChange(Number(value));
}
};
return (
<div className={cn('flex w-full flex-col items-center gap-2', containerClass)}>
<div className="flex w-full items-center justify-start gap-2">
<Label
htmlFor={`${field.name}-input`}
className={cn('text-left text-sm font-semibold text-text-primary', labelClass)}
>
{label}
</Label>
{labelAdjacent}
</div>
<Input
id={`${field.name}-input`}
value={field.value ?? ''}
onChange={handleChange}
placeholder={placeholder}
className={cn(
'flex h-10 max-h-10 w-full resize-none border-none bg-surface-secondary px-3 py-2',
inputClass,
)}
/>
</div>
);
}

View file

@ -29,6 +29,7 @@ export * from './InputOTP';
export { default as Combobox } from './Combobox';
export { default as Dropdown } from './Dropdown';
export { default as FileUpload } from './FileUpload';
export { default as FormInput } from './FormInput';
export { default as DropdownPopup } from './DropdownPopup';
export { default as DelayedRender } from './DelayedRender';
export { default as ThemeSelector } from './ThemeSelector';