diff --git a/backend/data/default_agent_types.json b/backend/data/default_agent_types.json index bfc72bc..5eeddfc 100644 --- a/backend/data/default_agent_types.json +++ b/backend/data/default_agent_types.json @@ -9,7 +9,8 @@ "fallback_models": ["claude-haiku-3-5-20241022"], "model_params": { "temperature": 0.7, - "max_tokens": 4096 + "max_tokens": 4096, + "top_p": 0.95 }, "mcp_servers": ["gitea", "knowledge-base"], "tool_permissions": { @@ -29,7 +30,8 @@ "fallback_models": ["claude-haiku-3-5-20241022"], "model_params": { "temperature": 0.5, - "max_tokens": 8192 + "max_tokens": 8192, + "top_p": 0.95 }, "mcp_servers": ["gitea", "knowledge-base"], "tool_permissions": { @@ -49,7 +51,8 @@ "fallback_models": ["claude-haiku-3-5-20241022"], "model_params": { "temperature": 0.6, - "max_tokens": 8192 + "max_tokens": 8192, + "top_p": 0.95 }, "mcp_servers": ["gitea", "knowledge-base", "filesystem"], "tool_permissions": { @@ -69,7 +72,8 @@ "fallback_models": ["claude-haiku-3-5-20241022"], "model_params": { "temperature": 0.3, - "max_tokens": 16384 + "max_tokens": 16384, + "top_p": 0.95 }, "mcp_servers": ["gitea", "knowledge-base", "filesystem"], "tool_permissions": { @@ -89,7 +93,8 @@ "fallback_models": ["claude-haiku-3-5-20241022"], "model_params": { "temperature": 0.4, - "max_tokens": 8192 + "max_tokens": 8192, + "top_p": 0.95 }, "mcp_servers": ["gitea", "knowledge-base", "filesystem"], "tool_permissions": { @@ -109,7 +114,8 @@ "fallback_models": ["claude-haiku-3-5-20241022"], "model_params": { "temperature": 0.4, - "max_tokens": 8192 + "max_tokens": 8192, + "top_p": 0.95 }, "mcp_servers": ["gitea", "knowledge-base", "filesystem"], "tool_permissions": { diff --git a/frontend/src/components/agents/AgentTypeForm.tsx b/frontend/src/components/agents/AgentTypeForm.tsx index 052d861..cc6ce0b 100644 --- a/frontend/src/components/agents/AgentTypeForm.tsx +++ b/frontend/src/components/agents/AgentTypeForm.tsx @@ -57,28 +57,37 @@ export function AgentTypeForm({ const [activeTab, setActiveTab] = useState('basic'); const [expertiseInput, setExpertiseInput] = useState(''); + // Build initial values with proper defaults for missing fields + const getInitialValues = useCallback((): AgentTypeCreateFormValues => { + if (!agentType) return defaultAgentTypeValues; + + // Ensure model_params has all required fields with proper defaults + const modelParams = agentType.model_params ?? {}; + const safeModelParams = { + temperature: typeof modelParams.temperature === 'number' ? modelParams.temperature : 0.7, + max_tokens: typeof modelParams.max_tokens === 'number' ? modelParams.max_tokens : 8192, + top_p: typeof modelParams.top_p === 'number' ? modelParams.top_p : 0.95, + }; + + return { + name: agentType.name, + slug: agentType.slug, + description: agentType.description, + expertise: agentType.expertise ?? [], + personality_prompt: agentType.personality_prompt ?? '', + primary_model: agentType.primary_model ?? 'claude-opus-4-5-20251101', + fallback_models: agentType.fallback_models ?? [], + model_params: safeModelParams, + mcp_servers: agentType.mcp_servers ?? [], + tool_permissions: agentType.tool_permissions ?? {}, + is_active: agentType.is_active ?? false, + }; + }, [agentType]); + // Always use create schema for validation - editing requires all fields too const form = useForm({ resolver: zodResolver(agentTypeCreateSchema), - defaultValues: agentType - ? { - name: agentType.name, - slug: agentType.slug, - description: agentType.description, - expertise: agentType.expertise, - personality_prompt: agentType.personality_prompt, - primary_model: agentType.primary_model, - fallback_models: agentType.fallback_models, - model_params: (agentType.model_params ?? { - temperature: 0.7, - max_tokens: 8192, - top_p: 0.95, - }) as AgentTypeCreateFormValues['model_params'], - mcp_servers: agentType.mcp_servers, - tool_permissions: agentType.tool_permissions, - is_active: agentType.is_active, - } - : defaultAgentTypeValues, + defaultValues: getInitialValues(), }); const { @@ -90,29 +99,58 @@ export function AgentTypeForm({ formState: { errors }, } = form; + // Helper to extract first error message from nested errors + const getFirstErrorMessage = useCallback( + (formErrors: FieldErrors): { field: string; message: string } => { + for (const key of Object.keys(formErrors)) { + const error = formErrors[key as keyof typeof formErrors]; + if (!error) continue; + + // Handle nested errors (like model_params.temperature) + if (typeof error === 'object' && !('message' in error)) { + for (const nestedKey of Object.keys(error)) { + const nestedError = (error as Record)[nestedKey]; + if (nestedError?.message) { + return { field: `${key}.${nestedKey}`, message: nestedError.message }; + } + } + } + + // Handle direct error + if ('message' in error && error.message) { + return { field: key, message: error.message as string }; + } + } + return { field: 'unknown', message: 'Validation error' }; + }, + [] + ); + // Handle form validation errors - show toast with first error - const handleFormError = useCallback((formErrors: FieldErrors) => { - // Find the first error and show it - const firstErrorKey = Object.keys(formErrors)[0] as keyof AgentTypeCreateFormValues; - if (firstErrorKey) { - const error = formErrors[firstErrorKey]; - const message = error && 'message' in error ? error.message : 'Validation error'; + const handleFormError = useCallback( + (formErrors: FieldErrors) => { + // Log for debugging + console.error('[AgentTypeForm] Validation errors:', formErrors); + + const { field, message } = getFirstErrorMessage(formErrors); toast.error('Please fix form errors', { - description: `${String(firstErrorKey)}: ${message}`, + description: `${field}: ${message}`, }); // Navigate to the tab containing the error - if (['name', 'slug', 'description', 'expertise', 'is_active'].includes(firstErrorKey)) { + const topLevelField = field.split('.')[0]; + if (['name', 'slug', 'description', 'expertise', 'is_active'].includes(topLevelField)) { setActiveTab('basic'); - } else if (['primary_model', 'fallback_models', 'model_params'].includes(firstErrorKey)) { + } else if (['primary_model', 'fallback_models', 'model_params'].includes(topLevelField)) { setActiveTab('model'); - } else if (['mcp_servers', 'tool_permissions'].includes(firstErrorKey)) { + } else if (['mcp_servers', 'tool_permissions'].includes(topLevelField)) { setActiveTab('permissions'); - } else if (firstErrorKey === 'personality_prompt') { + } else if (topLevelField === 'personality_prompt') { setActiveTab('personality'); } - } - }, []); + }, + [getFirstErrorMessage] + ); const watchName = watch('name'); /* istanbul ignore next -- defensive fallback, expertise always has default */ @@ -120,6 +158,14 @@ export function AgentTypeForm({ /* istanbul ignore next -- defensive fallback, mcp_servers always has default */ const watchMcpServers = watch('mcp_servers') || []; + // Reset form when agentType changes (e.g., switching to edit mode) + useEffect(() => { + if (agentType) { + const values = getInitialValues(); + form.reset(values); + } + }, [agentType?.id, form, getInitialValues]); + // Auto-generate slug from name for new agent types useEffect(() => { if (!isEditing && watchName) {