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 }