diff --git a/client/src/common/agents-types.ts b/client/src/common/agents-types.ts index a49586b8a0..dd31189484 100644 --- a/client/src/common/agents-types.ts +++ b/client/src/common/agents-types.ts @@ -1,9 +1,10 @@ import { AgentCapabilities, ArtifactModes } from 'librechat-data-provider'; import type { - Agent, - AgentProvider, AgentModelParameters, SupportContact, + AgentProvider, + GraphEdge, + Agent, } from 'librechat-data-provider'; import type { OptionWithIcon, ExtendedFile } from './types'; @@ -34,6 +35,7 @@ export type AgentForm = { tools?: string[]; provider?: AgentProvider | OptionWithIcon; agent_ids?: string[]; + edges?: GraphEdge[]; [AgentCapabilities.artifacts]?: ArtifactModes | string; recursion_limit?: number; support_contact?: SupportContact; diff --git a/client/src/components/SidePanel/Agents/Advanced/AdvancedPanel.tsx b/client/src/components/SidePanel/Agents/Advanced/AdvancedPanel.tsx index a1a1c91ee6..cd88d0792c 100644 --- a/client/src/components/SidePanel/Agents/Advanced/AdvancedPanel.tsx +++ b/client/src/components/SidePanel/Agents/Advanced/AdvancedPanel.tsx @@ -5,6 +5,7 @@ import { useFormContext, Controller } from 'react-hook-form'; import type { AgentForm } from '~/common'; import { useAgentPanelContext } from '~/Providers'; import MaxAgentSteps from './MaxAgentSteps'; +import AgentHandoffs from './AgentHandoffs'; import { useLocalize } from '~/hooks'; import AgentChain from './AgentChain'; import { Panel } from '~/common'; @@ -41,6 +42,12 @@ export default function AdvancedPanel() {
+ } + /> {chainEnabled && ( ; + currentAgentId: string; +} + +/** TODO: make configurable */ +const MAX_HANDOFFS = 10; + +const AgentHandoffs: React.FC = ({ field, currentAgentId }) => { + const localize = useLocalize(); + const [newAgentId, setNewAgentId] = useState(''); + const [expandedIndices, setExpandedIndices] = useState>(new Set()); + const agentsMap = useAgentsMapContext(); + const edgesValue = field.value; + const edges = useMemo(() => edgesValue || [], [edgesValue]); + + const agents = useMemo(() => (agentsMap ? Object.values(agentsMap) : []), [agentsMap]); + + const selectableAgents = useMemo( + () => + agents + .filter((agent) => agent?.id !== currentAgentId) + .map( + (agent) => + ({ + label: agent?.name || '', + value: agent?.id || '', + icon: ( + + ), + }) as OptionWithIcon, + ), + [agents, currentAgentId], + ); + + const getAgentDetails = useCallback((id: string) => agentsMap?.[id], [agentsMap]); + + useEffect(() => { + if (newAgentId && edges.length < MAX_HANDOFFS) { + const newEdge: GraphEdge = { + from: currentAgentId, + to: newAgentId, + edgeType: 'handoff', + }; + field.onChange([...edges, newEdge]); + setNewAgentId(''); + } + }, [newAgentId, edges, field, currentAgentId]); + + const removeHandoffAt = (index: number) => { + field.onChange(edges.filter((_, i) => i !== index)); + // Also remove from expanded set + setExpandedIndices((prev) => { + const newSet = new Set(prev); + newSet.delete(index); + return newSet; + }); + }; + + const updateHandoffAt = (index: number, agentId: string) => { + const updated = [...edges]; + updated[index] = { ...updated[index], to: agentId }; + field.onChange(updated); + }; + + const updateHandoffDetailsAt = (index: number, updates: Partial) => { + const updated = [...edges]; + updated[index] = { ...updated[index], ...updates }; + field.onChange(updated); + }; + + const toggleExpanded = (index: number) => { + setExpandedIndices((prev) => { + const newSet = new Set(prev); + if (newSet.has(index)) { + newSet.delete(index); + } else { + newSet.add(index); + } + return newSet; + }); + }; + + const getTargetAgentId = (to: string | string[]): string => { + return Array.isArray(to) ? to[0] : to; + }; + + return ( + +
+
+ + + + +
+
+ {edges.length} / {MAX_HANDOFFS} +
+
+
+ {/* Current fixed agent */} +
+
+
+ +
+
+ {getAgentDetails(currentAgentId)?.name} +
+
+
+ {edges.length > 0 && } + {edges.map((edge, idx) => { + const targetAgentId = getTargetAgentId(edge.to); + const isExpanded = expandedIndices.has(idx); + + return ( + +
+
+ updateHandoffAt(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(targetAgentId)?.name ?? ''} + SelectIcon={ + + } + className="flex-1 border-border-heavy" + containerClassName="px-0" + /> + + +
+ + {isExpanded && ( +
+
+ + + updateHandoffDetailsAt(idx, { description: e.target.value }) + } + className="mt-1 h-8 text-sm" + /> +
+ +
+ +