/* ┌──────────────────────────────────────────────────────────────────────────────┐ │ @author: Davidson Gomes │ │ @file: /app/agents/dialogs/CustomToolDialog.tsx │ │ Developed by: Davidson Gomes │ │ Creation date: May 13, 2025 │ │ Contact: contato@evolution-api.com │ ├──────────────────────────────────────────────────────────────────────────────┤ │ @copyright © Evolution API 2025. All rights reserved. │ │ Licensed under the Apache License, Version 2.0 │ │ │ │ You may not use this file except in compliance with the License. │ │ You may obtain a copy of the License at │ │ │ │ http://www.apache.org/licenses/LICENSE-2.0 │ │ │ │ Unless required by applicable law or agreed to in writing, software │ │ distributed under the License is distributed on an "AS IS" BASIS, │ │ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │ │ See the License for the specific language governing permissions and │ │ limitations under the License. │ ├──────────────────────────────────────────────────────────────────────────────┤ │ @important │ │ For any future changes to the code in this file, it is recommended to │ │ include, together with the modification, the information of the developer │ │ who changed it and the date of modification. │ └──────────────────────────────────────────────────────────────────────────────┘ */ "use client"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { X, Plus, Info, Trash, Globe, FileJson, LayoutList, Settings, Database, Code, Server, Wand } from "lucide-react"; import { useState, useEffect } from "react"; import { HTTPTool, HTTPToolParameter } from "@/types/agent"; import { sanitizeAgentName } from "@/lib/utils"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip"; import { cn } from "@/lib/utils"; import { Badge } from "@/components/ui/badge"; interface CustomToolDialogProps { open: boolean; onOpenChange: (open: boolean) => void; onSave: (tool: HTTPTool) => void; initialTool?: HTTPTool | null; } export function CustomToolDialog({ open, onOpenChange, onSave, initialTool = null, }: CustomToolDialogProps) { const [tool, setTool] = useState>({ name: "", method: "GET", endpoint: "", description: "", headers: {}, values: {}, parameters: {}, error_handling: { timeout: 30, retry_count: 0, fallback_response: {}, }, }); const [headersList, setHeadersList] = useState<{ id: string; key: string; value: string }[]>([]); const [bodyParams, setBodyParams] = useState<{ id: string; key: string; param: HTTPToolParameter }[]>([]); const [pathParams, setPathParams] = useState<{ id: string; key: string; desc: string }[]>([]); const [queryParams, setQueryParams] = useState<{ id: string; key: string; value: string }[]>([]); const [valuesList, setValuesList] = useState<{ id: string; key: string; value: string }[]>([]); const [timeout, setTimeout] = useState(30); const [fallbackError, setFallbackError] = useState(""); const [fallbackMessage, setFallbackMessage] = useState(""); const [activeTab, setActiveTab] = useState("basics"); useEffect(() => { if (open) { if (initialTool) { setTool(initialTool); setHeadersList( Object.entries(initialTool.headers || {}).map(([key, value], idx) => ({ id: `header-${idx}`, key, value, })) ); setBodyParams( Object.entries(initialTool.parameters?.body_params || {}).map(([key, param], idx) => ({ id: `body-${idx}`, key, param, })) ); setPathParams( Object.entries(initialTool.parameters?.path_params || {}).map(([key, desc], idx) => ({ id: `path-${idx}`, key, desc: desc as string, })) ); setQueryParams( Object.entries(initialTool.parameters?.query_params || {}).map(([key, value], idx) => ({ id: `query-${idx}`, key, value: value as string, })) ); setValuesList( Object.entries(initialTool.values || {}).map(([key, value], idx) => ({ id: `val-${idx}`, key, value: value as string, })) ); setTimeout(initialTool.error_handling?.timeout || 30); setFallbackError(initialTool.error_handling?.fallback_response?.error || ""); setFallbackMessage(initialTool.error_handling?.fallback_response?.message || ""); } else { setTool({ name: "", method: "GET", endpoint: "", description: "", headers: {}, values: {}, parameters: {}, error_handling: { timeout: 30, retry_count: 0, fallback_response: {}, }, }); setHeadersList([]); setBodyParams([]); setPathParams([]); setQueryParams([]); setValuesList([]); setTimeout(30); setFallbackError(""); setFallbackMessage(""); } setActiveTab("basics"); } }, [open, initialTool]); const handleAddHeader = () => { setHeadersList([...headersList, { id: `header-${Date.now()}`, key: "", value: "" }]); }; const handleRemoveHeader = (id: string) => { setHeadersList(headersList.filter((h) => h.id !== id)); }; const handleHeaderChange = (id: string, field: "key" | "value", value: string) => { setHeadersList(headersList.map((h) => (h.id === id ? { ...h, [field]: value } : h))); }; const handleAddBodyParam = () => { setBodyParams([ ...bodyParams, { id: `body-${Date.now()}`, key: "", param: { type: "string", required: false, description: "" }, }, ]); }; const handleRemoveBodyParam = (id: string) => { setBodyParams(bodyParams.filter((p) => p.id !== id)); }; const handleBodyParamChange = (id: string, field: "key" | keyof HTTPToolParameter, value: string | boolean) => { setBodyParams( bodyParams.map((p) => p.id === id ? field === "key" ? { ...p, key: value as string } : { ...p, param: { ...p.param, [field]: value } } : p ) ); }; // Path Params const handleAddPathParam = () => { setPathParams([...pathParams, { id: `path-${Date.now()}`, key: "", desc: "" }]); }; const handleRemovePathParam = (id: string) => { setPathParams(pathParams.filter((p) => p.id !== id)); }; const handlePathParamChange = (id: string, field: "key" | "desc", value: string) => { setPathParams(pathParams.map((p) => (p.id === id ? { ...p, [field]: value } : p))); }; // Query Params const handleAddQueryParam = () => { setQueryParams([...queryParams, { id: `query-${Date.now()}`, key: "", value: "" }]); }; const handleRemoveQueryParam = (id: string) => { setQueryParams(queryParams.filter((q) => q.id !== id)); }; const handleQueryParamChange = (id: string, field: "key" | "value", value: string) => { setQueryParams(queryParams.map((q) => (q.id === id ? { ...q, [field]: value } : q))); }; // Values const handleAddValue = () => { setValuesList([...valuesList, { id: `val-${Date.now()}`, key: "", value: "" }]); }; const handleRemoveValue = (id: string) => { setValuesList(valuesList.filter((v) => v.id !== id)); }; const handleValueChange = (id: string, field: "key" | "value", value: string) => { setValuesList(valuesList.map((v) => (v.id === id ? { ...v, [field]: value } : v))); }; const handleSave = () => { if (!tool.name || !tool.endpoint) return; const headersObject: Record = {}; headersList.forEach((h) => { if (h.key.trim()) headersObject[h.key] = h.value; }); const bodyParamsObject: Record = {}; bodyParams.forEach((p) => { if (p.key.trim()) bodyParamsObject[p.key] = p.param; }); const pathParamsObject: Record = {}; pathParams.forEach((p) => { if (p.key.trim()) pathParamsObject[p.key] = p.desc; }); const queryParamsObject: Record = {}; queryParams.forEach((q) => { if (q.key.trim()) queryParamsObject[q.key] = q.value; }); const valuesObject: Record = {}; valuesList.forEach((v) => { if (v.key.trim()) valuesObject[v.key] = v.value; }); // Sanitize the tool name const sanitizedName = sanitizeAgentName(tool.name); onSave({ ...(tool as HTTPTool), name: sanitizedName, headers: headersObject, values: valuesObject, parameters: { ...tool.parameters, body_params: bodyParamsObject, path_params: pathParamsObject, query_params: queryParamsObject, }, error_handling: { timeout, retry_count: tool.error_handling?.retry_count ?? 0, fallback_response: { error: fallbackError, message: fallbackMessage, }, }, } as HTTPTool); onOpenChange(false); }; const ParamField = ({ children, label, tooltip }: { children: React.ReactNode, label: string, tooltip?: string }) => (
{tooltip && (

{tooltip}

)}
{children}
); const FieldList = >({ items, onAdd, onRemove, onChange, fields, addText, emptyText, icon }: { items: T[], onAdd: () => void, onRemove: (id: string) => void, onChange: (id: string, field: string, value: any) => void, fields: { name: string, field: string, placeholder: string, width: number, type?: string }[], addText: string, emptyText: string, icon: React.ReactNode }) => (
{items.length > 0 ? (
{items.map((item) => (
{fields.map(field => { // Calculate percentage width based on the field's width value const widthPercent = (field.width / 12) * 100; return (
{field.type === 'select' ? ( ) : field.type === 'checkbox' ? (
) : ( onChange( item.id, field.field, e.target.value )} className="h-9 w-full bg-neutral-900 border-neutral-700 text-white placeholder:text-neutral-500 text-sm" placeholder={field.placeholder} /> )}
); })}
))}
) : (
{icon}

{emptyText}

)}
); return (
{/* Header Area */}

{initialTool ? "Edit Custom Tool" : "Create Custom Tool"}

Configure an HTTP tool for your agent to interact with external APIs

{tool.method || "GET"}
{/* Main Content Area */}
{/* Left Side - Navigation */}
{/* Right Side - Content */}
{activeTab === 'basics' && (
setTool({ ...tool, name: e.target.value })} onBlur={(e) => { const sanitizedName = sanitizeAgentName(e.target.value); if (sanitizedName !== e.target.value) { setTool({ ...tool, name: sanitizedName }); } }} className="bg-neutral-800 border-neutral-700 text-white" placeholder="e.g. weatherApi, searchTool" /> setTool({ ...tool, description: e.target.value })} className="bg-neutral-800 border-neutral-700 text-white" placeholder="Provides weather information for a given location" />
)} {activeTab === 'endpoint' && (
{['GET', 'POST', 'PUT', 'PATCH', 'DELETE'].map(method => ( ))}
setTool({ ...tool, endpoint: e.target.value })} className="bg-neutral-800 border-neutral-700 text-white font-mono" placeholder="https://api.example.com/v1/resource/{id}" /> {tool.endpoint && tool.endpoint.includes('{') && (

URL contains path variables. Don't forget to define them in the Path Parameters section.

)}

How to use variables in your endpoint

You can use dynamic variables in your endpoint using the {"{VARIABLE_NAME}"} syntax.

Example:

https://api.example.com/users/{"{userId}"}/profile

Define userId as a Path Parameter

)} {activeTab === 'headers' && (
{ handleHeaderChange( id, field as "key" | "value", value as string ); }} fields={[ { name: "Header Name", field: "key", placeholder: "e.g. Authorization", width: 6 }, { name: "Header Value", field: "value", placeholder: "e.g. Bearer token123", width: 6 } ]} addText="Add Header" emptyText="No headers configured. Add headers to customize your HTTP requests." icon={} />
)} {activeTab === 'body' && (

About Body Parameters

Parameters that will be sent in the request body. Only applicable for POST, PUT, and PATCH methods. You can use variables like {"{variableName}"} that will be replaced at runtime.

{ if (field === "key") { handleBodyParamChange(id, "key", value as string); } else if (field.startsWith("param.")) { const paramField = field.split('.')[1] as keyof HTTPToolParameter; handleBodyParamChange(id, paramField, paramField === "required" ? value as boolean : value as string ); } }} fields={[ { name: "Parameter Name", field: "key", placeholder: "e.g. userId", width: 4 }, { name: "Type", field: "param.type", placeholder: "", width: 2, type: "select" }, { name: "Description", field: "param.description", placeholder: "What this parameter does", width: 4 }, { name: "Required", field: "param.required", placeholder: "", width: 2, type: "checkbox" } ]} addText="Add Body Parameter" emptyText="No body parameters configured. Add parameters for POST/PUT/PATCH requests." icon={} />
)} {activeTab === 'path' && (

About Path Parameters

Path parameters are placeholders in your URL path that will be replaced with actual values. For example, in https://api.example.com/users/{"{userId}"}, userId is a path parameter.

{ handlePathParamChange( id, field as "key" | "desc", value as string ); }} fields={[ { name: "Parameter Name", field: "key", placeholder: "e.g. userId", width: 4 }, { name: "Description", field: "desc", placeholder: "What this parameter represents", width: 6 }, { name: "Required", field: "required", placeholder: "", width: 2, type: "checkbox" } ]} addText="Add Path Parameter" emptyText="No path parameters configured. Add parameters if your endpoint URL contains {placeholders}." icon={} />
)} {activeTab === 'query' && (

About Query Parameters

Query parameters are added to the URL after a question mark (?) to filter or customize the request. For example: https://api.example.com/search?query=term&limit=10

{ handleQueryParamChange( id, field as "key" | "value", value as string ); }} fields={[ { name: "Parameter Name", field: "key", placeholder: "e.g. search", width: 4 }, { name: "Description", field: "value", placeholder: "What this parameter does", width: 6 }, { name: "Required", field: "required", placeholder: "", width: 2, type: "checkbox" } ]} addText="Add Query Parameter" emptyText="No query parameters configured. Add parameters to customize your URL query string." icon={} />
)} {activeTab === 'defaults' && (

About Default Values

Set default values for parameters you've defined in the Body, Path, or Query sections. These values will be used when the parameter isn't explicitly provided during a tool call.

{ handleValueChange( id, field as "key" | "value", value as string ); }} fields={[ { name: "Parameter Name", field: "key", placeholder: "Name of an existing parameter", width: 5 }, { name: "Default Value", field: "value", placeholder: "Value to use if not provided", width: 7 } ]} addText="Add Default Value" emptyText="No default values configured. Add default values for your previously defined parameters." icon={} />
)} {activeTab === 'error' && (

About Error Handling

Configure how the tool should handle errors and timeouts when making HTTP requests. Proper error handling ensures reliable operation of your agent.

setTimeout(Number(e.target.value))} className="w-32 bg-neutral-800 border-neutral-700 text-white" /> setFallbackError(e.target.value)} className="bg-neutral-800 border-neutral-700 text-white" placeholder="e.g. API_ERROR" /> setFallbackMessage(e.target.value)} className="bg-neutral-800 border-neutral-700 text-white" placeholder="e.g. Failed to retrieve data from the API." />
)}
{/* Footer Area */}
); }