diff --git a/ui/admin/app/components/TruncatedText.tsx b/ui/admin/app/components/TruncatedText.tsx index af3cf6e31..791d8a7bd 100644 --- a/ui/admin/app/components/TruncatedText.tsx +++ b/ui/admin/app/components/TruncatedText.tsx @@ -1,3 +1,5 @@ +import { cn } from "~/lib/utils"; + import { TypographyP } from "~/components/Typography"; import { Tooltip, @@ -8,16 +10,16 @@ import { export function TruncatedText({ content, - maxWidth, + className, }: { - content: string; - maxWidth: string; + content: React.ReactNode; + className?: string; }) { return ( -
+
{content} diff --git a/ui/admin/app/components/Typography.tsx b/ui/admin/app/components/Typography.tsx index dc94e683f..4070495a4 100644 --- a/ui/admin/app/components/Typography.tsx +++ b/ui/admin/app/components/Typography.tsx @@ -1,3 +1,4 @@ +import { VariantProps, cva } from "class-variance-authority"; import React, { ReactNode } from "react"; import { cn } from "~/lib/utils"; @@ -182,3 +183,53 @@ export function TypographyMuted({

); } + +const typographyVariants = cva(`scroll-m-20`, { + variants: { + variant: { + h1: `text-4xl font-extrabold tracking-tight lg:text-5xl`, + h2: `text-3xl font-semibold tracking-tight first:mt-0`, + h3: `text-2xl font-semibold tracking-tight`, + h4: `text-xl font-semibold tracking-tight`, + p: `leading-7`, + blockquote: `mt-6 border-l-2 pl-6 italic`, + inlineCode: `relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-sm font-semibold`, + lead: `text-xl text-muted-foreground`, + large: `text-lg font-semibold`, + small: `text-sm font-medium leading-none`, + muted: `text-sm text-muted-foreground`, + }, + }, + defaultVariants: { + variant: `p`, + }, +}); + +const componentMap: Record< + Required>["variant"], + TypographyElement +> = { + h1: `h1`, + h2: `h2`, + h3: `h3`, + h4: `h4`, + p: `p`, + blockquote: `blockquote`, + inlineCode: `code`, + lead: `p`, + large: `div`, + small: `small`, + muted: `p`, +}; + +export function Typography({ + variant, + className, + ...props +}: TypographyProps & + VariantProps) { + return React.createElement(variantConfig, { + className: cn(variantConfig, className), + ...props, + }); +} diff --git a/ui/admin/app/components/agent/Agent.tsx b/ui/admin/app/components/agent/Agent.tsx index 6d700a0e3..76295f838 100644 --- a/ui/admin/app/components/agent/Agent.tsx +++ b/ui/admin/app/components/agent/Agent.tsx @@ -68,14 +68,18 @@ function AgentContent({ className, onRefresh }: AgentProps) { Tools + Add tools the allow the agent to perform useful actions such as searching the web, reading files, or interacting with other systems. + + debouncedSetAgentInfo(convertTools(tools)) + } />
@@ -122,3 +126,27 @@ function AgentContent({ className, onRefresh }: AgentProps) {
); } +function convertTools( + tools: { tool: string; variant: "fixed" | "default" | "available" }[] +) { + type ToolObj = Pick< + AgentType, + "tools" | "defaultThreadTools" | "availableThreadTools" + >; + + return tools.reduce( + (acc, { tool, variant }) => { + if (variant === "fixed") acc.tools?.push(tool); + else if (variant === "default") acc.defaultThreadTools?.push(tool); + else if (variant === "available") + acc.availableThreadTools?.push(tool); + + return acc; + }, + { + tools: [], + defaultThreadTools: [], + availableThreadTools: [], + } as ToolObj + ); +} diff --git a/ui/admin/app/components/agent/ToolEntry.tsx b/ui/admin/app/components/agent/ToolEntry.tsx index 22555f01d..ef0a9d2cd 100644 --- a/ui/admin/app/components/agent/ToolEntry.tsx +++ b/ui/admin/app/components/agent/ToolEntry.tsx @@ -1,11 +1,25 @@ +import { zodResolver } from "@hookform/resolvers/zod"; import { TrashIcon } from "lucide-react"; +import { useEffect } from "react"; +import { useForm } from "react-hook-form"; import useSWR from "swr"; +import { z } from "zod"; import { ToolReferenceService } from "~/lib/service/api/toolreferenceService"; +import { noop } from "~/lib/utils"; -import { ToolIcon } from "~/components/tools/ToolIcon"; -import { LoadingSpinner } from "~/components/ui/LoadingSpinner"; -import { Button } from "~/components/ui/button"; +import { TruncatedText } from "../TruncatedText"; +import { ToolIcon } from "../tools/ToolIcon"; +import { LoadingSpinner } from "../ui/LoadingSpinner"; +import { Button } from "../ui/button"; +import { Form, FormField, FormItem, FormMessage } from "../ui/form"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "../ui/select"; export function ToolEntry({ tool, @@ -22,26 +36,89 @@ export function ToolEntry({ return (
-
- {isLoading ? ( - - ) : ( - - )} - {toolReference?.name || tool} +
+
+ {isLoading ? ( + + ) : ( + + )} + + +
+ +
+ + + +
-
); } + +const schema = z.object({ + variant: z.enum(["fixed", "default", "canAdd"]), +}); + +type ToolEntryForm = z.infer; + +function ToolEntryForm({ + onChange, +}: { + onChange: (data: ToolEntryForm) => void; +}) { + const form = useForm({ + resolver: zodResolver(schema), + defaultValues: { + variant: "default", + }, + }); + + useEffect(() => { + return form.watch((values) => { + const { success, data } = schema.safeParse(values); + + if (!success) return; + + onChange(data); + }).unsubscribe; + }, [form, onChange]); + + return ( +
+ ( + + + + + + )} + /> + + ); +} diff --git a/ui/admin/app/components/agent/ToolForm.tsx b/ui/admin/app/components/agent/ToolForm.tsx index 78818e5eb..4ff94009b 100644 --- a/ui/admin/app/components/agent/ToolForm.tsx +++ b/ui/admin/app/components/agent/ToolForm.tsx @@ -1,7 +1,7 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { PlusIcon } from "lucide-react"; -import { useCallback, useEffect } from "react"; -import { useForm } from "react-hook-form"; +import { useEffect, useMemo } from "react"; +import { useFieldArray, useForm } from "react-hook-form"; import { z } from "zod"; import { Agent } from "~/lib/model/agents"; @@ -10,22 +10,42 @@ import { noop } from "~/lib/utils"; import { ToolEntry } from "~/components/agent/ToolEntry"; import { ToolCatalog } from "~/components/tools/ToolCatalog"; import { Button } from "~/components/ui/button"; -import { Dialog, DialogContent, DialogTrigger } from "~/components/ui/dialog"; import { - Form, - FormDescription, - FormField, - FormItem, - FormLabel, - FormMessage, -} from "~/components/ui/form"; + Dialog, + DialogContent, + DialogDescription, + DialogTitle, + DialogTrigger, +} from "~/components/ui/dialog"; +import { Form } from "~/components/ui/form"; +const ToolVariant = { + FIXED: "fixed", + DEFAULT: "default", + AVAILABLE: "available", +} as const; +type ToolVariant = (typeof ToolVariant)[keyof typeof ToolVariant]; const formSchema = z.object({ - tools: z.array(z.string()), + tools: z.array( + z.object({ + tool: z.string(), + variant: z.enum([ + ToolVariant.FIXED, + ToolVariant.DEFAULT, + ToolVariant.AVAILABLE, + ] as const), + }) + ), }); export type ToolFormValues = z.infer; +const getVariant = (tool: string, agent: Agent): ToolVariant => { + if (agent.defaultThreadTools?.includes(tool)) return "default"; + if (agent.availableThreadTools?.includes(tool)) return "available"; + return "fixed"; +}; + export function ToolForm({ agent, onSubmit, @@ -35,89 +55,94 @@ export function ToolForm({ onSubmit?: (values: ToolFormValues) => void; onChange?: (values: ToolFormValues) => void; }) { + const defaultValues = useMemo(() => { + return { + tools: agent.tools?.map((tool) => ({ + tool, + variant: getVariant(tool, agent), + })), + }; + }, [agent]); + const form = useForm({ resolver: zodResolver(formSchema), - defaultValues: { tools: agent.tools || [] }, + defaultValues, }); - const handleSubmit = form.handleSubmit(onSubmit || noop); + const toolFields = useFieldArray({ + control: form.control, + name: "tools", + }); - const toolValues = form.watch("tools"); + const handleSubmit = form.handleSubmit(onSubmit || noop); useEffect(() => { return form.watch((values) => { const { data, success } = formSchema.safeParse(values); + console.log(data); + if (!success) return; onChange?.(data); }).unsubscribe; - }, [toolValues, form.formState, onChange, form]); - - const handleToolsChange = useCallback( - (newTools: string[]) => { - form.setValue("tools", newTools, { - shouldValidate: true, - shouldDirty: true, - }); - onChange?.({ tools: newTools }); - }, - [form, onChange] - ); + }, [form, onChange]); + + const [fixedFields, userFields] = useMemo(() => { + return [ + toolFields.fields?.filter( + (field) => field.variant === ToolVariant.FIXED + ), + toolFields.fields?.filter( + (field) => field.variant !== ToolVariant.FIXED + ), + ]; + }, [toolFields]); + + const removeTool = (tool: string) => + toolFields.remove(toolFields.fields.findIndex((t) => t.tool === tool)); + + const addTool = (tool: string) => + toolFields.append({ tool, variant: ToolVariant.FIXED }); return (
- ( - - - - -
- {field.value?.map((tool, index) => ( - { - const newTools = - field.value?.filter( - (_, i) => i !== index - ); - - field.onChange(newTools); - }} - /> - ))} -
-
- - - - - - - - -
- -
- )} - /> +
+ {fixedFields.map((field) => ( + removeTool(field.tool)} + /> + ))} +
+ +
+ + + + + + + + + + field.tool + )} + onAddTool={addTool} + onRemoveTool={removeTool} + /> + + +
); diff --git a/ui/admin/app/components/tools/ToolCatalog.tsx b/ui/admin/app/components/tools/ToolCatalog.tsx index debae4307..779549e09 100644 --- a/ui/admin/app/components/tools/ToolCatalog.tsx +++ b/ui/admin/app/components/tools/ToolCatalog.tsx @@ -20,7 +20,8 @@ import { ToolCategoryHeader } from "./ToolCategoryHeader"; type ToolCatalogProps = React.HTMLAttributes & { tools: string[]; - onChangeTools: (tools: string[]) => void; + onAddTool: (tools: string) => void; + onRemoveTool: (toolId: string) => void; invert?: boolean; classNames?: { list?: string }; }; @@ -29,10 +30,11 @@ export function ToolCatalog({ className, tools, invert = false, - onChangeTools, + onAddTool, + onRemoveTool, classNames, }: ToolCatalogProps) { - const { data: toolCategories = [], isLoading } = useSWR( + const { data: toolCategories, isLoading } = useSWR( ToolReferenceService.getToolReferencesCategoryMap.key("tool"), () => ToolReferenceService.getToolReferencesCategoryMap("tool"), { fallbackData: {} } @@ -41,26 +43,27 @@ export function ToolCatalog({ const handleSelect = useCallback( (toolId: string) => { if (!tools.includes(toolId)) { - onChangeTools([...tools, toolId]); + onAddTool(toolId); } }, - [tools, onChangeTools] + [tools, onAddTool] ); const handleSelectBundle = useCallback( (bundleToolId: string, categoryTools: ToolReference[]) => { - const categoryToolIds = categoryTools.map((tool) => tool.id); - const newTools = tools.includes(bundleToolId) - ? tools.filter((toolId) => toolId !== bundleToolId) - : [ - ...tools.filter( - (toolId) => !categoryToolIds.includes(toolId) - ), - bundleToolId, - ]; - onChangeTools(newTools); + if (tools.includes(bundleToolId)) { + onRemoveTool(bundleToolId); + return; + } + + onAddTool(bundleToolId); + + // remove all tools in the bundle to remove redundancy + categoryTools.forEach((tool) => { + onRemoveTool(tool.id); + }); }, - [tools, onChangeTools] + [tools, onAddTool, onRemoveTool] ); if (isLoading) return ; diff --git a/ui/admin/app/components/tools/toolGrid/ToolCard.tsx b/ui/admin/app/components/tools/toolGrid/ToolCard.tsx index 9d4e9b2a7..8c3319523 100644 --- a/ui/admin/app/components/tools/toolGrid/ToolCard.tsx +++ b/ui/admin/app/components/tools/toolGrid/ToolCard.tsx @@ -69,7 +69,10 @@ export function ToolCard({ tool, onDelete }: ToolCardProps) { - + {tool.description || "No description available"} diff --git a/ui/admin/app/lib/model/agents.ts b/ui/admin/app/lib/model/agents.ts index 86bd3d43c..b25333bbc 100644 --- a/ui/admin/app/lib/model/agents.ts +++ b/ui/admin/app/lib/model/agents.ts @@ -12,6 +12,8 @@ export type AgentBase = { agents?: string[]; workflows?: string[]; tools?: string[]; + defaultThreadTools?: string[]; + availableThreadTools?: string[]; params?: Record; knowledgeSetStatues: KnowledgeSetStatus[]; };