Skip to content

Commit

Permalink
Add teacher/judge section to ilab form (#3777)
Browse files Browse the repository at this point in the history
* Add teacher/judge section to ilab form

* Resolve conflicts, address feedback

* update zod validation for URL

* Address comments

* address feedback

* change run name
  • Loading branch information
DaoDaoNoCode authored Feb 27, 2025
1 parent 5fd48b6 commit e3b27a0
Show file tree
Hide file tree
Showing 15 changed files with 719 additions and 120 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,26 @@ class ModelCustomizationFormGlobal {
}
}

class TeacherModelSection {
findEndpointInput() {
return cy.findByTestId('teacher-endpoint-input');
}

findModelNameInput() {
return cy.findByTestId('teacher-model-name-input');
}
}

class JudgeModelSection {
findEndpointInput() {
return cy.findByTestId('judge-endpoint-input');
}

findModelNameInput() {
return cy.findByTestId('judge-model-name-input');
}
}

export const modelCustomizationFormGlobal = new ModelCustomizationFormGlobal();
export const teacherModelSection = new TeacherModelSection();
export const judgeModelSection = new JudgeModelSection();
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
/* eslint-disable camelcase */
import { modelCustomizationFormGlobal } from '~/__tests__/cypress/cypress/pages/pipelines/modelCustomizationForm';
import {
judgeModelSection,
modelCustomizationFormGlobal,
teacherModelSection,
} from '~/__tests__/cypress/cypress/pages/pipelines/modelCustomizationForm';
import {
buildMockPipeline,
buildMockPipelines,
Expand Down Expand Up @@ -46,6 +50,10 @@ describe('Model Customization Form', () => {
modelCustomizationFormGlobal.visit(projectName);
cy.wait('@getAllPipelines');
cy.wait('@getAllPipelineVersions');
teacherModelSection.findEndpointInput().type('http://test.com');
teacherModelSection.findModelNameInput().type('test');
judgeModelSection.findEndpointInput().type('http://test.com');
judgeModelSection.findModelNameInput().type('test');
modelCustomizationFormGlobal.findSubmitButton().should('not.be.disabled');
});
it('Should not submit', () => {
Expand Down
18 changes: 18 additions & 0 deletions frontend/src/api/k8s/secrets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,24 @@ export const assembleSecret = (
};
};

export const assembleSecretTeacher = (
projectName: string,
data: Record<string, string>,
secretName?: string,
): SecretKind => {
const k8sName = secretName || `teacher-secret-${genRandomChars()}`;
return assembleSecret(projectName, data, 'generic', k8sName);
};

export const assembleSecretJudge = (
projectName: string,
data: Record<string, string>,
secretName?: string,
): SecretKind => {
const k8sName = secretName || `judge-secret-${genRandomChars()}`;
return assembleSecret(projectName, data, 'generic', k8sName);
};

export const assembleISSecretBody = (
assignableData: Record<string, string>,
): [Record<string, string>, string] => {
Expand Down
11 changes: 7 additions & 4 deletions frontend/src/concepts/pipelines/content/createRun/submitUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { convertPeriodicTimeToSeconds, convertToDate } from '~/utilities/time';
const createRun = async (
formData: SafeRunFormData,
createPipelineRun: PipelineAPIs['createPipelineRun'],
dryRun?: boolean,
): Promise<PipelineRunKF> => {
/* eslint-disable camelcase */
const data: CreatePipelineRunKFData = {
Expand All @@ -44,7 +45,7 @@ const createRun = async (
};

/* eslint-enable camelcase */
return createPipelineRun({}, data);
return createPipelineRun({ dryRun }, data);
};

export const convertDateDataToKFDateTime = (dateData?: RunDateTime): DateTimeKF | null => {
Expand All @@ -58,6 +59,7 @@ export const convertDateDataToKFDateTime = (dateData?: RunDateTime): DateTimeKF
const createRecurringRun = async (
formData: SafeRunFormData,
createPipelineRecurringRun: PipelineAPIs['createPipelineRecurringRun'],
dryRun?: boolean,
): Promise<PipelineRecurringRunKF> => {
if (formData.runType.type !== RunTypeOption.SCHEDULED) {
return Promise.reject(new Error('Cannot create a schedule with incomplete data.'));
Expand Down Expand Up @@ -109,23 +111,24 @@ const createRecurringRun = async (
};
/* eslint-enable camelcase */

return createPipelineRecurringRun({}, data);
return createPipelineRecurringRun({ dryRun }, data);
};

/** Returns the relative path to navigate to from the namespace qualified route */
export const handleSubmit = (
formData: RunFormData,
api: PipelineAPIs,
dryRun?: boolean,
): Promise<PipelineRunKF | PipelineRecurringRunKF> => {
if (!isFilledRunFormData(formData)) {
throw new Error('Form data was incomplete.');
}

switch (formData.runType.type) {
case RunTypeOption.ONE_TRIGGER:
return createRun(formData, api.createPipelineRun);
return createRun(formData, api.createPipelineRun, dryRun);
case RunTypeOption.SCHEDULED:
return createRecurringRun(formData, api.createPipelineRecurringRun);
return createRecurringRun(formData, api.createPipelineRecurringRun, dryRun);
default:
// eslint-disable-next-line no-console
console.error('Unknown run type', formData.runType);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { ModelCustomizationEndpointType } from '~/concepts/pipelines/content/modelCustomizationForm/modelCustomizationFormSchema/types';
import {
TeacherJudgeFormData,
teacherJudgeModel,
} from '~/concepts/pipelines/content/modelCustomizationForm/modelCustomizationFormSchema/validationUtils';

describe('TeacherJudgeSchema', () => {
it('should validate when it is public without token', () => {
const field: TeacherJudgeFormData = {
endpointType: ModelCustomizationEndpointType.PUBLIC,
apiToken: '',
modelName: 'test',
endpoint: 'http://test.com',
};
const result = teacherJudgeModel.safeParse(field);
expect(result.success).toBe(true);
});
it('should error when it is private without token', () => {
const field: TeacherJudgeFormData = {
endpointType: ModelCustomizationEndpointType.PRIVATE,
apiToken: '',
modelName: 'test',
endpoint: 'http://test.com',
};
const result = teacherJudgeModel.safeParse(field);
expect(result.success).toBe(false);
});
it('should validate when it is private with token', () => {
const field: TeacherJudgeFormData = {
endpointType: ModelCustomizationEndpointType.PRIVATE,
apiToken: 'test',
modelName: 'test',
endpoint: 'http://test.com',
};
const result = teacherJudgeModel.safeParse(field);
expect(result.success).toBe(true);
});
it('should error when the endpoint is not a uri', () => {
const field: TeacherJudgeFormData = {
endpointType: ModelCustomizationEndpointType.PRIVATE,
apiToken: 'test',
modelName: 'test',
endpoint: 'not a uri',
};
const result = teacherJudgeModel.safeParse(field);
expect(result.success).toBe(false);
});
});
Original file line number Diff line number Diff line change
@@ -1,36 +1,47 @@
import { z } from 'zod';
import { ModelCustomizationEndpointType, ModelCustomizationRunType } from './types';

export const uriFieldSchema = z.string().refine(
(value) => {
if (!value) {
return true;
}
try {
return !!new URL(value);
} catch (e) {
return false;
}
},
{ message: 'Invalid URI' },
);
export const uriFieldSchemaBase = (
isOptional: boolean,
): z.ZodEffects<z.ZodString, string, string> =>
z.string().refine(
(value) => {
if (!value) {
return !!isOptional;
}
try {
return !!new URL(value);
} catch (e) {
return false;
}
},
{ message: 'Invalid URI' },
);

export const baseModelSchema = z.object({
registryName: z.string(),
name: z.string(),
version: z.string(),
inputStorageLocationUri: uriFieldSchema,
inputStorageLocationUri: uriFieldSchemaBase(true),
});

export const teacherJudgeModel = z.object({
endpointType: z.enum([
ModelCustomizationEndpointType.PUBLIC,
ModelCustomizationEndpointType.PRIVATE,
]),
endpoint: uriFieldSchema,
username: z.string().min(1, 'Username is required'),
password: z.string().min(1, 'Password is required'),
const teacherJudgeBaseSchema = z.object({
endpoint: uriFieldSchemaBase(false),
modelName: z.string().trim().min(1, 'Model name is required'),
});
const teacherJudgePublicSchema = teacherJudgeBaseSchema.extend({
endpointType: z.literal(ModelCustomizationEndpointType.PUBLIC),
apiToken: z.string(),
});
const teacherJudgePrivateSchema = teacherJudgeBaseSchema.extend({
endpointType: z.literal(ModelCustomizationEndpointType.PRIVATE),
apiToken: z.string().trim().min(1, 'Token is required'),
});

export const teacherJudgeModel = z.discriminatedUnion('endpointType', [
teacherJudgePrivateSchema,
teacherJudgePublicSchema,
]);

export const numericFieldSchema = z
.object({
Expand Down Expand Up @@ -113,8 +124,11 @@ export const fineTunedModelDetailsSchema = z.object({
export const modelCustomizationFormSchema = z.object({
projectName: z.object({ value: z.string().min(1, { message: 'Project is required' }) }),
baseModel: baseModelSchema,
teacher: teacherJudgeModel,
judge: teacherJudgeModel,
});

export type ModelCustomizationFormData = z.infer<typeof modelCustomizationFormSchema>;

export type BaseModelFormData = z.infer<typeof baseModelSchema>;
export type TeacherJudgeFormData = z.infer<typeof teacherJudgeModel>;
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import React from 'react';
import { ILAB_PIPELINE_NAME } from '~/pages/pipelines/global/modelCustomization/const';
import { FetchState } from '~/utilities/useFetchState';
import { useLatestPipelineVersion } from '~/concepts/pipelines/apiHooks/useLatestPipelineVersion';
import { usePipelineByName } from '~/concepts/pipelines/apiHooks/usePipelineByName';
import { PipelineVersionKF } from '~/concepts/pipelines/kfTypes';
import { PipelineKF, PipelineVersionKF } from '~/concepts/pipelines/kfTypes';

export const useIlabPipeline = (): FetchState<PipelineVersionKF | null> => {
export const useIlabPipeline = (): {
ilabPipeline: PipelineKF | null;
ilabPipelineVersion: PipelineVersionKF | null;
loaded: boolean;
loadError: Error | undefined;
refresh: () => Promise<PipelineVersionKF | null | undefined>;
} => {
const [ilabPipeline, ilabPipelineLoaded, ilabPipelineLoadError, refreshIlabPipeline] =
usePipelineByName(ILAB_PIPELINE_NAME);
const [
Expand All @@ -21,5 +26,5 @@ export const useIlabPipeline = (): FetchState<PipelineVersionKF | null> => {
return refreshIlabPipelineVersion();
}, [refreshIlabPipeline, refreshIlabPipelineVersion]);

return [ilabPipelineVersion, loaded, loadError, refresh];
return { ilabPipeline, ilabPipelineVersion, loaded, loadError, refresh };
};
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,27 @@ import { ModelCustomizationFormData } from '~/concepts/pipelines/content/modelCu
import { UpdateObjectAtPropAndValue } from '~/pages/projects/types';
import FineTunePageFooter from '~/pages/pipelines/global/modelCustomization/FineTunePageFooter';
import BaseModelSection from '~/pages/pipelines/global/modelCustomization/baseModelSection/BaseModelSection';
import TeacherModelSection from '~/pages/pipelines/global/modelCustomization/teacherJudgeSection/TeacherModelSection';
import JudgeModelSection from '~/pages/pipelines/global/modelCustomization/teacherJudgeSection/JudgeModelSection';
import { PipelineKF, PipelineVersionKF } from '~/concepts/pipelines/kfTypes';

type FineTunePageProps = {
isInvalid: boolean;
onSuccess: () => void;
data: ModelCustomizationFormData;
setData: UpdateObjectAtPropAndValue<ModelCustomizationFormData>;
ilabPipeline: PipelineKF | null;
ilabPipelineVersion: PipelineVersionKF | null;
};

const FineTunePage: React.FC<FineTunePageProps> = ({ isInvalid, onSuccess, data, setData }) => {
const FineTunePage: React.FC<FineTunePageProps> = ({
isInvalid,
onSuccess,
data,
setData,
ilabPipeline,
ilabPipelineVersion,
}) => {
const projectDetailsDescription = 'This project is used for running your pipeline';
const { project } = usePipelinesAPI();

Expand All @@ -41,8 +53,19 @@ const FineTunePage: React.FC<FineTunePageProps> = ({ isInvalid, onSuccess, data,
data={data.baseModel}
setData={(baseModelData) => setData('baseModel', baseModelData)}
/>
<TeacherModelSection
data={data.teacher}
setData={(teacherData) => setData('teacher', teacherData)}
/>
<JudgeModelSection data={data.judge} setData={(judgeData) => setData('judge', judgeData)} />
<FormSection>
<FineTunePageFooter isInvalid={isInvalid} onSuccess={onSuccess} data={data} />
<FineTunePageFooter
isInvalid={isInvalid}
onSuccess={onSuccess}
data={data}
ilabPipeline={ilabPipeline}
ilabPipelineVersion={ilabPipelineVersion}
/>
</FormSection>
</Form>
);
Expand Down
Loading

0 comments on commit e3b27a0

Please sign in to comment.