/ config_portal / frontend / app / components / AddAgentFlow.tsx
AddAgentFlow.tsx
  1  import { useState, useEffect, useCallback } from "react";
  2  import StepIndicator from "./StepIndicator";
  3  import AgentBasicInfoStep from "./steps/agent/AgentBasicInfoStep";
  4  import AgentServicesStep from "./steps/agent/AgentServicesStep";
  5  import AgentToolsStep, { Tool } from "./steps/agent/AgentToolsStep";
  6  import AgentFeaturesStep from "./steps/agent/AgentFeaturesStep";
  7  import AgentCardStep from "./steps/agent/AgentCardStep";
  8  import SuccessScreen from "./steps/InitSuccessScreen/SuccessScreen";
  9  
 10  export interface Skill {
 11    id: string;
 12    name: string;
 13    description: string;
 14  }
 15  
 16  export interface AgentFormData {
 17    agent_name?: string;
 18    namespace?: string;
 19    supports_streaming?: boolean;
 20    model_provider?: string;
 21    instruction?: string;
 22  
 23    session_service_type?: string;
 24    session_service_behavior?: string;
 25    database_url?: string;
 26  
 27    artifact_service_type?: string;
 28    artifact_service_base_path?: string;
 29    artifact_service_scope?: string;
 30  
 31    artifact_handling_mode?: string;
 32    enable_embed_resolution?: boolean;
 33    enable_artifact_content_instruction?: boolean;
 34  
 35    tools?: Tool[];
 36  
 37    agent_card_description?: string;
 38    agent_card_default_input_modes?: string[];
 39    agent_card_default_output_modes?: string[];
 40    agent_card_skills_str?: string;
 41    agent_card_skills?: Skill[];
 42  
 43    agent_card_publishing_interval?: number;
 44    agent_discovery_enabled?: boolean;
 45    inter_agent_communication_allow_list?: string[];
 46    inter_agent_communication_deny_list?: string[];
 47    inter_agent_communication_timeout?: number;
 48  
 49    showSuccessScreen_agent?: boolean;
 50    [key: string]: unknown;
 51  }
 52  
 53  export interface StepProps {
 54    data: AgentFormData;
 55    updateData: (data: Partial<AgentFormData>) => void;
 56    onNext: () => void;
 57    onPrevious: () => void;
 58    serverUrl?: string;
 59    availableTools?: Tool[];
 60  }
 61  
 62  export type Step = {
 63    id: string;
 64    title: string;
 65    description: string;
 66    component: React.ComponentType<StepProps>;
 67  };
 68  
 69  // eslint-disable-next-line react/prop-types
 70  const AgentReviewSubmitStep: React.FC<StepProps> = ({
 71    data,
 72    updateData,
 73    onPrevious,
 74    serverUrl = "",
 75  }) => {
 76    const handleSubmit = async () => {
 77      console.log("Submitting agent configuration:", data);
 78      try {
 79        const processedConfig: Partial<AgentFormData> = JSON.parse(
 80          JSON.stringify(data)
 81        );
 82  
 83        if (
 84          !Array.isArray(processedConfig.agent_card_skills) &&
 85          processedConfig.agent_card_skills_str
 86        ) {
 87          try {
 88            const parsedSkills = JSON.parse(
 89              processedConfig.agent_card_skills_str
 90            );
 91            if (Array.isArray(parsedSkills)) {
 92              processedConfig.agent_card_skills = parsedSkills as Skill[];
 93            } else {
 94              console.warn(
 95                "Parsed agent_card_skills_str was not an array, ensuring agent_card_skills is empty array."
 96              );
 97              processedConfig.agent_card_skills = [];
 98            }
 99          } catch (e) {
100            console.warn(
101              "Could not parse agent_card_skills_str as JSON, ensuring agent_card_skills is empty array.",
102              e
103            );
104            processedConfig.agent_card_skills = [];
105          }
106        } else if (!Array.isArray(processedConfig.agent_card_skills)) {
107          processedConfig.agent_card_skills = [];
108        }
109        delete processedConfig.agent_card_skills_str;
110  
111        if (Array.isArray(processedConfig.tools)) {
112          processedConfig.tools = processedConfig.tools
113            .map((toolInstance) => {
114              if (!toolInstance.tool_type) {
115                console.error("Tool is missing tool_type:", toolInstance);
116                return null;
117              }
118  
119              // Build tool with explicit field ordering - tool_type MUST be first
120              const cleanTool: Record<string, any> = {
121                tool_type: toolInstance.tool_type,
122              };
123  
124              // Add other fields in a specific order after tool_type
125              if (toolInstance.tool_name)
126                cleanTool.tool_name = toolInstance.tool_name;
127              if (toolInstance.tool_description)
128                cleanTool.tool_description = toolInstance.tool_description;
129              if (toolInstance.group_name)
130                cleanTool.group_name = toolInstance.group_name;
131              if (toolInstance.component_module)
132                cleanTool.component_module = toolInstance.component_module;
133              if (toolInstance.function_name)
134                cleanTool.function_name = toolInstance.function_name;
135              if (toolInstance.component_base_path)
136                cleanTool.component_base_path = toolInstance.component_base_path;
137              if (toolInstance.connection_params)
138                cleanTool.connection_params = toolInstance.connection_params;
139              if (toolInstance.auth)
140                cleanTool.auth = toolInstance.auth;
141              if (toolInstance.environment_variables)
142                cleanTool.environment_variables =
143                  toolInstance.environment_variables;
144              if (
145                toolInstance.required_scopes &&
146                toolInstance.required_scopes.length > 0
147              )
148                cleanTool.required_scopes = toolInstance.required_scopes;
149              if (toolInstance.tool_config)
150                cleanTool.tool_config = toolInstance.tool_config;
151  
152              return cleanTool as Tool;
153            })
154            .filter((tool) => tool !== null) as Tool[];
155        }
156  
157        const apiPayload = {
158          agent_name_input: data.agent_name,
159          config: processedConfig,
160        };
161  
162        const response = await fetch(`${serverUrl}/api/save_agent_config`, {
163          method: "POST",
164          headers: { "Content-Type": "application/json" },
165          body: JSON.stringify(apiPayload),
166        });
167        const result = await response.json();
168  
169        if (response.ok && result.status === "success") {
170          setTimeout(async () => {
171            try {
172              await fetch(`${serverUrl}/api/shutdown`, { method: "POST" });
173            } catch (error) {
174              // error expected, shutting the server down
175            }
176          }, 200);
177          if (updateData) updateData({ showSuccessScreen_agent: true });
178        } else {
179          alert(`Error saving agent: ${result.message || "Unknown error"}`);
180        }
181      } catch (err) {
182        alert(
183          `Failed to save agent: ${
184            err instanceof Error ? err.message : String(err)
185          }`
186        );
187      }
188    };
189  
190    return (
191      <div>
192        <h3 className="text-lg font-semibold mb-2">
193          Review and Submit Agent Configuration
194        </h3>
195        <pre className="bg-gray-100 p-3 rounded overflow-x-auto text-xs max-h-96">
196          {JSON.stringify(data, null, 2)}
197        </pre>
198        <div className="flex justify-end space-x-3 mt-6">
199          <button
200            onClick={onPrevious}
201            className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-solace-blue-dark"
202          >
203            Previous
204          </button>
205          <button
206            onClick={handleSubmit}
207            className="px-4 py-2 text-sm font-medium text-white bg-green-600 rounded-md shadow-sm hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500"
208          >
209            Save Agent & Finish
210          </button>
211        </div>
212      </div>
213    );
214  };
215  
216  export const addAgentSteps: Step[] = [
217    {
218      id: "agent-basic",
219      title: "Basic Info",
220      description: "Name, model, instruction",
221      component: AgentBasicInfoStep,
222    },
223    {
224      id: "agent-services",
225      title: "Services",
226      description: "Session & artifact services",
227      component: AgentServicesStep,
228    },
229    {
230      id: "agent-features",
231      title: "Features",
232      description: "Enable built-in features",
233      component: AgentFeaturesStep,
234    },
235    {
236      id: "agent-tools",
237      title: "Custom Tools",
238      description: "Define custom tools for the agent",
239      component: AgentToolsStep,
240    },
241    {
242      id: "agent-card",
243      title: "Agent Card & Comms",
244      description: "Discovery and communication settings",
245      component: AgentCardStep,
246    },
247    {
248      id: "agent-review",
249      title: "Review & Submit",
250      description: "Review and save configuration",
251      component: AgentReviewSubmitStep,
252    },
253  ];
254  
255  export default function AddAgentFlow() {
256    const [currentStepIndex, setCurrentStepIndex] = useState(0);
257    const [formData, setFormData] = useState<AgentFormData>({});
258    const [isLoading, setIsLoading] = useState(true);
259    const [error, setError] = useState<string | null>(null);
260    const [availableTools, setAvailableTools] = useState<Tool[]>([]);
261  
262    const serverUrl = "";
263  
264    useEffect(() => {
265      Promise.all([
266        fetch(`/api/form_schema?type=agent`),
267        fetch(`/api/available_tools`),
268      ])
269        .then(async ([schemaRes, toolsRes]) => {
270          if (!schemaRes.ok)
271            throw new Error(
272              `Failed to fetch agent form schema (status: ${schemaRes.status})`
273            );
274          if (!toolsRes.ok)
275            throw new Error(
276              `Failed to fetch available tools (status: ${toolsRes.status})`
277            );
278  
279          const schemaData = await schemaRes.json();
280          const toolsData = await toolsRes.json();
281  
282          if (schemaData?.status !== "success" || !schemaData?.defaults) {
283            throw new Error(
284              schemaData?.message || "Invalid response for agent form schema"
285            );
286          }
287          if (toolsData?.status !== "success") {
288            throw new Error(
289              toolsData?.message || "Invalid response for available tools"
290            );
291          }
292  
293          setAvailableTools(toolsData);
294  
295          const defaults = schemaData.defaults;
296          const sanitizedDefaults: AgentFormData = {
297            ...defaults,
298            agent_card_default_input_modes: (
299              defaults.agent_card_default_input_modes_str || "text"
300            )
301              .split(",")
302              .map((s: string) => s.trim())
303              .filter(Boolean),
304            agent_card_default_output_modes: (
305              defaults.agent_card_default_output_modes_str || "text,file"
306            )
307              .split(",")
308              .map((s: string) => s.trim())
309              .filter(Boolean),
310            inter_agent_communication_allow_list: (
311              defaults.inter_agent_communication_allow_list_str || "*"
312            )
313              .split(",")
314              .map((s: string) => s.trim())
315              .filter(Boolean),
316            inter_agent_communication_deny_list: (
317              defaults.inter_agent_communication_deny_list_str || ""
318            )
319              .split(",")
320              .map((s: string) => s.trim())
321              .filter(Boolean),
322            agent_card_skills_str: defaults.agent_card_skills_str || "[]",
323            agent_card_skills: [],
324            tools: Array.isArray(defaults.tools) ? defaults.tools : [],
325            showSuccessScreen_agent: false,
326          };
327          delete sanitizedDefaults.agent_card_default_input_modes_str;
328          delete sanitizedDefaults.agent_card_default_output_modes_str;
329          delete sanitizedDefaults.inter_agent_communication_allow_list_str;
330          delete sanitizedDefaults.inter_agent_communication_deny_list_str;
331  
332          try {
333            const parsedSkills = JSON.parse(
334              sanitizedDefaults.agent_card_skills_str || "[]"
335            );
336            if (Array.isArray(parsedSkills)) {
337              sanitizedDefaults.agent_card_skills = parsedSkills.filter(
338                (skill: unknown): skill is Skill =>
339                  typeof skill === "object" &&
340                  skill !== null &&
341                  "id" in skill &&
342                  typeof (skill as Skill).id === "string" &&
343                  "name" in skill &&
344                  typeof (skill as Skill).name === "string" &&
345                  "description" in skill &&
346                  typeof (skill as Skill).description === "string"
347              ) as Skill[];
348            }
349          } catch (e) {
350            console.warn(
351              "Could not parse agent_card_skills_str from defaults:",
352              e
353            );
354            sanitizedDefaults.agent_card_skills = [];
355          }
356  
357          setFormData(sanitizedDefaults);
358        })
359        .catch((err) => {
360          console.error("Error fetching initial data:", err);
361          setError(
362            `Failed to load agent configuration form: ${
363              err instanceof Error ? err.message : String(err)
364            }`
365          );
366        })
367        .finally(() => setIsLoading(false));
368    }, []);
369  
370    const updateFormDataCb = useCallback((newData: Partial<AgentFormData>) => {
371      setFormData((prevData) => ({ ...prevData, ...newData }));
372    }, []);
373  
374    useEffect(() => {
375      if (isLoading) return;
376  
377      const updatedTools: Tool[] = [];
378      updatedTools.push({
379        id: "default-artifact-management",
380        tool_type: "builtin-group",
381        group_name: "artifact_management",
382      });
383      updateFormDataCb({ tools: updatedTools });
384    }, [
385      updateFormDataCb,
386      isLoading,
387    ]);
388  
389    const handleNextCb = useCallback(() => {
390      if (currentStepIndex < addAgentSteps.length - 1) {
391        setCurrentStepIndex((prev) => prev + 1);
392      }
393    }, [currentStepIndex]);
394  
395    const handlePreviousCb = useCallback(() => {
396      if (currentStepIndex > 0) {
397        setCurrentStepIndex((prev) => prev - 1);
398      }
399    }, [currentStepIndex]);
400  
401    if (isLoading) {
402      return (
403        <div className="text-center p-10">
404          Loading agent configuration form...
405        </div>
406      );
407    }
408  
409    if (error) {
410      return (
411        <div className="text-center p-10 text-red-600">
412          <p>Error: {error}</p>
413        </div>
414      );
415    }
416  
417    if (formData.showSuccessScreen_agent) {
418      let title = "Agent Configured Successfully!";
419      if (formData.agent_name) {
420        title = `Agent "${formData.agent_name}" configured successfully!`;
421      }
422      return (
423        <div className="max-w-2xl mx-auto p-6">
424          <SuccessScreen
425            title={title}
426            message="Your new agent configuration has been saved."
427            initTab="tutorials"
428          />
429        </div>
430      );
431    }
432  
433    const CurrentStepComponent = addAgentSteps[currentStepIndex]?.component;
434  
435    if (!CurrentStepComponent) {
436      return (
437        <div className="text-center p-10 text-red-600">
438          Error: Configuration step not found for index {currentStepIndex}.
439        </div>
440      );
441    }
442  
443    return (
444      <div className="max-w-2xl mx-auto p-4">
445        <h2 className="text-2xl font-bold mb-6 text-center text-solace-purple">
446          Add New Agent
447        </h2>
448        {addAgentSteps.length > 1 && (
449          <div className="mb-8">
450            <StepIndicator
451              steps={addAgentSteps}
452              currentStepIndex={currentStepIndex}
453            />
454          </div>
455        )}
456        <div className="bg-white rounded-lg shadow-xl p-6 min-h-[500px]">
457          <CurrentStepComponent
458            data={formData}
459            updateData={updateFormDataCb}
460            onNext={handleNextCb}
461            onPrevious={handlePreviousCb}
462            serverUrl={serverUrl}
463            availableTools={availableTools}
464          />
465        </div>
466      </div>
467    );
468  }