### What problem does this PR solve? Feat: Display sub-agents in agent form #3221 ### Type of change - [x] New Feature (non-breaking change which adds functionality)tags/v0.20.0
| @@ -1,7 +1,13 @@ | |||
| import { BlockButton } from '@/components/ui/button'; | |||
| import { cn } from '@/lib/utils'; | |||
| import { Position } from '@xyflow/react'; | |||
| import { PencilLine, X } from 'lucide-react'; | |||
| import { PropsWithChildren } from 'react'; | |||
| import { PropsWithChildren, useCallback, useContext, useMemo } from 'react'; | |||
| import { Operator } from '../../constant'; | |||
| import { AgentInstanceContext } from '../../context'; | |||
| import { INextOperatorForm } from '../../interface'; | |||
| import useGraphStore from '../../store'; | |||
| import { filterDownstreamAgentNodeIds } from '../../utils/filter-downstream-nodes'; | |||
| import { ToolPopover } from './tool-popover'; | |||
| import { useDeleteAgentNodeTools } from './tool-popover/use-update-tools'; | |||
| import { useGetAgentToolNames } from './use-get-tools'; | |||
| @@ -24,6 +30,32 @@ export function ToolCard({ | |||
| ); | |||
| } | |||
| type ActionButtonProps<T> = { | |||
| record: T; | |||
| deleteRecord(record: T): void; | |||
| edit(record: T): void; | |||
| }; | |||
| function ActionButton<T>({ edit, deleteRecord, record }: ActionButtonProps<T>) { | |||
| const handleDelete = useCallback(() => { | |||
| deleteRecord(record); | |||
| }, [deleteRecord, record]); | |||
| const handleEdit = useCallback(() => { | |||
| edit(record); | |||
| }, [edit, record]); | |||
| return ( | |||
| <div className="flex items-center gap-2 text-text-sub-title"> | |||
| <PencilLine | |||
| className="size-4 cursor-pointer" | |||
| data-tool={record} | |||
| onClick={handleEdit} | |||
| /> | |||
| <X className="size-4 cursor-pointer" onClick={handleDelete} /> | |||
| </div> | |||
| ); | |||
| } | |||
| export function AgentTools() { | |||
| const { toolNames } = useGetAgentToolNames(); | |||
| const { deleteNodeTool } = useDeleteAgentNodeTools(); | |||
| @@ -35,13 +67,11 @@ export function AgentTools() { | |||
| {toolNames.map((x) => ( | |||
| <ToolCard key={x}> | |||
| {x} | |||
| <div className="flex items-center gap-2 text-text-sub-title"> | |||
| <PencilLine className="size-4 cursor-pointer" data-tool={x} /> | |||
| <X | |||
| className="size-4 cursor-pointer" | |||
| onClick={deleteNodeTool(x)} | |||
| /> | |||
| </div> | |||
| <ActionButton | |||
| record={x} | |||
| edit={() => {}} | |||
| deleteRecord={deleteNodeTool(x)} | |||
| ></ActionButton> | |||
| </ToolCard> | |||
| ))} | |||
| </ul> | |||
| @@ -51,3 +81,44 @@ export function AgentTools() { | |||
| </section> | |||
| ); | |||
| } | |||
| export function Agents({ node }: INextOperatorForm) { | |||
| const { addCanvasNode } = useContext(AgentInstanceContext); | |||
| const { deleteAgentDownstreamNodesById, edges, getNode } = useGraphStore( | |||
| (state) => state, | |||
| ); | |||
| const subBottomAgentNodeIds = useMemo(() => { | |||
| return filterDownstreamAgentNodeIds(edges, node?.id); | |||
| }, [edges, node?.id]); | |||
| return ( | |||
| <section className="space-y-2.5"> | |||
| <span className="text-text-sub-title">Agents</span> | |||
| <ul className="space-y-2"> | |||
| {subBottomAgentNodeIds.map((id) => { | |||
| const currentNode = getNode(id); | |||
| return ( | |||
| <ToolCard key={id}> | |||
| {currentNode?.data.name} | |||
| <ActionButton | |||
| record={id} | |||
| edit={() => {}} | |||
| deleteRecord={deleteAgentDownstreamNodesById} | |||
| ></ActionButton> | |||
| </ToolCard> | |||
| ); | |||
| })} | |||
| </ul> | |||
| <BlockButton | |||
| onClick={addCanvasNode(Operator.Agent, { | |||
| nodeId: node?.id, | |||
| position: Position.Bottom, | |||
| })} | |||
| > | |||
| Add Agent | |||
| </BlockButton> | |||
| </section> | |||
| ); | |||
| } | |||
| @@ -2,7 +2,6 @@ import { FormContainer } from '@/components/form-container'; | |||
| import { LargeModelFormField } from '@/components/large-model-form-field'; | |||
| import { LlmSettingSchema } from '@/components/llm-setting-items/next'; | |||
| import { MessageHistoryWindowSizeFormField } from '@/components/message-history-window-size-item'; | |||
| import { BlockButton } from '@/components/ui/button'; | |||
| import { | |||
| Form, | |||
| FormControl, | |||
| @@ -11,20 +10,18 @@ import { | |||
| FormLabel, | |||
| } from '@/components/ui/form'; | |||
| import { zodResolver } from '@hookform/resolvers/zod'; | |||
| import { Position } from '@xyflow/react'; | |||
| import { useContext, useMemo } from 'react'; | |||
| import { useMemo } from 'react'; | |||
| import { useForm } from 'react-hook-form'; | |||
| import { useTranslation } from 'react-i18next'; | |||
| import { z } from 'zod'; | |||
| import { Operator, initialAgentValues } from '../../constant'; | |||
| import { AgentInstanceContext } from '../../context'; | |||
| import { initialAgentValues } from '../../constant'; | |||
| import { INextOperatorForm } from '../../interface'; | |||
| import useGraphStore from '../../store'; | |||
| import { isBottomSubAgent } from '../../utils'; | |||
| import { DescriptionField } from '../components/description-field'; | |||
| import { Output } from '../components/output'; | |||
| import { PromptEditor } from '../components/prompt-editor'; | |||
| import { AgentTools } from './agent-tools'; | |||
| import { AgentTools, Agents } from './agent-tools'; | |||
| import { useValues } from './use-values'; | |||
| import { useWatchFormChange } from './use-watch-change'; | |||
| @@ -74,8 +71,6 @@ const AgentForm = ({ node }: INextOperatorForm) => { | |||
| useWatchFormChange(node?.id, form); | |||
| const { addCanvasNode } = useContext(AgentInstanceContext); | |||
| return ( | |||
| <Form {...form}> | |||
| <form | |||
| @@ -129,14 +124,7 @@ const AgentForm = ({ node }: INextOperatorForm) => { | |||
| )} | |||
| <FormContainer> | |||
| <AgentTools></AgentTools> | |||
| <BlockButton | |||
| onClick={addCanvasNode(Operator.Agent, { | |||
| nodeId: node?.id, | |||
| position: Position.Bottom, | |||
| })} | |||
| > | |||
| Add Agent | |||
| </BlockButton> | |||
| <Agents node={node}></Agents> | |||
| </FormContainer> | |||
| <Output list={outputList}></Output> | |||
| </form> | |||
| @@ -1,23 +1,21 @@ | |||
| import { Edge } from '@xyflow/react'; | |||
| import { NodeHandleId } from '../constant'; | |||
| // Get all downstream agent operators of the current agent operator | |||
| export function filterAllDownstreamAgentAndToolNodeIds( | |||
| // Get all downstream node ids | |||
| export function filterAllDownstreamNodeIds( | |||
| edges: Edge[], | |||
| nodeIds: string[], | |||
| predicate: (edge: Edge) => boolean, | |||
| ) { | |||
| return nodeIds.reduce<string[]>((pre, nodeId) => { | |||
| const currentEdges = edges.filter( | |||
| (x) => | |||
| x.source === nodeId && | |||
| (x.sourceHandle === NodeHandleId.AgentBottom || | |||
| x.sourceHandle === NodeHandleId.Tool), | |||
| (x) => x.source === nodeId && predicate(x), | |||
| ); | |||
| const downstreamNodeIds: string[] = currentEdges.map((x) => x.target); | |||
| const ids = downstreamNodeIds.concat( | |||
| filterAllDownstreamAgentAndToolNodeIds(edges, downstreamNodeIds), | |||
| filterAllDownstreamNodeIds(edges, downstreamNodeIds, predicate), | |||
| ); | |||
| ids.forEach((x) => { | |||
| @@ -29,3 +27,37 @@ export function filterAllDownstreamAgentAndToolNodeIds( | |||
| return pre; | |||
| }, []); | |||
| } | |||
| // Get all downstream agent and tool operators of the current agent operator | |||
| export function filterAllDownstreamAgentAndToolNodeIds( | |||
| edges: Edge[], | |||
| nodeIds: string[], | |||
| ) { | |||
| return filterAllDownstreamNodeIds( | |||
| edges, | |||
| nodeIds, | |||
| (edge: Edge) => | |||
| edge.sourceHandle === NodeHandleId.AgentBottom || | |||
| edge.sourceHandle === NodeHandleId.Tool, | |||
| ); | |||
| } | |||
| // Get all downstream agent operators of the current agent operator | |||
| export function filterAllDownstreamAgentNodeIds( | |||
| edges: Edge[], | |||
| nodeIds: string[], | |||
| ) { | |||
| return filterAllDownstreamNodeIds( | |||
| edges, | |||
| nodeIds, | |||
| (edge: Edge) => edge.sourceHandle === NodeHandleId.AgentBottom, | |||
| ); | |||
| } | |||
| // The direct child agent node of the current node | |||
| export function filterDownstreamAgentNodeIds(edges: Edge[], nodeId?: string) { | |||
| return edges | |||
| .filter( | |||
| (x) => x.source === nodeId && x.sourceHandle === NodeHandleId.AgentBottom, | |||
| ) | |||
| .map((x) => x.target); | |||
| } | |||