Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
.FormLayout {
width: 800px;
}

.GoldenQAHelper {
margin-top: 8px;
}
Expand Down Expand Up @@ -47,11 +43,16 @@

.GoldenQaRow {
display: flex;
align-items: flex-start;
align-items: center;
gap: 12px;
}

.UploadGoldenQaButton {
margin-top: 40px;
white-space: nowrap;
border-radius: 20px !important;
flex-shrink: 0;
}

.GoldenQaHelperText {
margin-top: 6px;
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@ import {
createGoldenQaCustomSuccessMock,
createGoldenQaSuccessMock,
getAIEvaluationCreateMocks,
getAssistantConfigVersionsMock,
} from 'mocks/AIEvaluations';
import AIEvaluationCreate, { DUMMY_CREATE, DUMMY_GET_ITEM } from './AIEvaluationCreate';
import AIEvaluationCreate from './AIEvaluationCreate';

const defaultMocks = getAIEvaluationCreateMocks(DUMMY_GET_ITEM, DUMMY_CREATE);
const defaultMocks = getAIEvaluationCreateMocks();

const wrapper = (mocks: any[] = defaultMocks) => (
<MockedProvider mocks={mocks}>
Expand Down Expand Up @@ -150,10 +151,10 @@ describe('AIEvaluationCreate', () => {
render(wrapper());

await waitFor(() => {
expect(screen.getByPlaceholderText('Give a unique name for the evaluation experiment.')).toBeInTheDocument();
expect(screen.getByPlaceholderText('Give a unique name for the evaluation experiment')).toBeInTheDocument();
});

const nameInput = screen.getByPlaceholderText('Give a unique name for the evaluation experiment.');
const nameInput = screen.getByPlaceholderText('Give a unique name for the evaluation experiment');
fireEvent.change(nameInput, { target: { value: 'valid_name' } });
fireEvent.click(screen.getByText('Run Evaluation'));

Expand All @@ -162,33 +163,36 @@ describe('AIEvaluationCreate', () => {
});
});

test('shows validation error for invalid evaluation name pattern', async () => {
test('accepts valid evaluation name with alphanumeric, underscore and hyphen', async () => {
render(wrapper());

await waitFor(() => {
expect(screen.getByPlaceholderText('Give a unique name for the evaluation experiment.')).toBeInTheDocument();
expect(screen.getByPlaceholderText('Give a unique name for the evaluation experiment')).toBeInTheDocument();
});

const nameInput = screen.getByPlaceholderText('Give a unique name for the evaluation experiment.');
fireEvent.change(nameInput, { target: { value: 'invalid name with spaces' } });
fireEvent.click(screen.getByText('Run Evaluation'));
const nameInput = screen.getByPlaceholderText('Give a unique name for the evaluation experiment');
fireEvent.change(nameInput, { target: { value: 'valid_evaluation-name123' } });

await waitFor(() => {
expect(screen.getByText('Invalid evaluation name')).toBeInTheDocument();
});
expect((nameInput as HTMLInputElement).value).toBe('valid_evaluation-name123');
});

test('accepts valid evaluation name with alphanumeric, underscore and hyphen', async () => {
render(wrapper());
test('shows assistant options from query using assistantName and versionNumber', async () => {
render(wrapper([...defaultMocks, getAssistantConfigVersionsMock]));

await waitFor(() => {
expect(screen.getByPlaceholderText('Give a unique name for the evaluation experiment.')).toBeInTheDocument();
expect(screen.getByText('Create AI Evaluation')).toBeInTheDocument();
});

const nameInput = screen.getByPlaceholderText('Give a unique name for the evaluation experiment.');
fireEvent.change(nameInput, { target: { value: 'valid_evaluation-name123' } });
const dropdowns = screen.getAllByTestId('dropdown');
const assistantDropdown = dropdowns[1];
const selectTrigger =
assistantDropdown.querySelector('[role="combobox"]') ?? assistantDropdown.querySelector('button');
fireEvent.mouseDown(selectTrigger!);

expect((nameInput as HTMLInputElement).value).toBe('valid_evaluation-name123');
await waitFor(() => {
expect(screen.getByText('Test Assistant (Version 2)')).toBeInTheDocument();
expect(screen.getByText('Test Assistant (Version 1)')).toBeInTheDocument();
});
});

test('shows assistant helper text', async () => {
Expand Down Expand Up @@ -284,8 +288,8 @@ describe('AIEvaluationCreate', () => {
render(
wrapper([
...defaultMocks,
createGoldenQaCustomSuccessMock('first_qa', 1),
createGoldenQaCustomSuccessMock('second_qa', 1),
createGoldenQaCustomSuccessMock('first_qa', 1, '100'),
createGoldenQaCustomSuccessMock('second_qa', 1, '200'),
])
);

Expand Down
138 changes: 84 additions & 54 deletions src/containers/AIEvals/AIEvaluationCreate/AIEvaluationCreate.tsx
Original file line number Diff line number Diff line change
@@ -1,36 +1,18 @@
import { gql } from '@apollo/client';
import { useMutation, useQuery } from '@apollo/client';
import { Button } from 'components/UI/Form/Button/Button';
import { Dropdown } from 'components/UI/Form/Dropdown/Dropdown';
import { Input } from 'components/UI/Form/Input/Input';
import { FormLayout } from 'containers/Form/FormLayout';
import { CREATE_EVALUATION } from 'graphql/mutations/AIEvaluations';
import { GET_ASSISTANT_CONFIG_VERSIONS } from 'graphql/queries/Assistant';
import React, { useRef, useState } from 'react';
import { useNavigate } from 'react-router';
import * as Yup from 'yup';
import { setNotification } from 'common/notification';

import styles from './AIEvaluationCreate.module.css';
import { UploadGoldenQaDialog } from './UploadGoldenQaDialog';

// Dummy GraphQL documents until backend supports get/update/delete for AI evaluations (exported for tests)
export const DUMMY_GET_ITEM = gql`
query DummyAiEvalGet {
__typename
}
`;
export const DUMMY_UPDATE = gql`
mutation DummyAiEvalUpdate {
__typename
}
`;
export const DUMMY_DELETE = gql`
mutation DummyAiEvalDelete {
__typename
}
`;
export const DUMMY_CREATE = gql`
mutation DummyAiEvalCreate {
__typename
}
`;

const goldenQAHelperContent = (
<div className={styles.GoldenQAHelper}>
<p className={styles.GoldenQAHelperDescription}>
Expand All @@ -49,47 +31,72 @@ const goldenQAHelperContent = (
);

const GoldenQaField = (props: any) => {
const { onUploadGoldenQaClick, form, ...dropdownProps } = props;
const { onUploadGoldenQaClick, form, helperText, ...dropdownProps } = props;
return (
<div className={styles.GoldenQaRow}>
<Dropdown {...dropdownProps} form={form} />
<Button
variant="outlined"
color="primary"
type="button"
className={styles.UploadGoldenQaButton}
onClick={onUploadGoldenQaClick}
>
Upload Golden QA
</Button>
<div>
<div className={styles.GoldenQaRow}>
<Dropdown {...dropdownProps} form={form} />
<Button
variant="outlined"
color="primary"
type="button"
className={styles.UploadGoldenQaButton}
onClick={onUploadGoldenQaClick}
>
Upload Golden QA
</Button>
</div>
{helperText && <div className={styles.GoldenQaHelperText}>{helperText}</div>}
</div>
);
};

const SectionDivider = (_props: any) => null;

export default function AIEvaluationCreate() {
const [goldenQADatasets, setGoldenQADatasets] = useState<string[]>([]);
const navigate = useNavigate();
const [goldenQADatasets, setGoldenQADatasets] = useState<Array<{ datasetId: number; name: string }>>([]);
const [showUploadGoldenQaDialog, setShowUploadGoldenQaDialog] = useState(false);
const [selectedGoldenQaFileName, setSelectedGoldenQaFileName] = useState<string | null>(null);
const [selectedGoldenQaFile, setSelectedGoldenQaFile] = useState<File | null>(null);
const fileInputRef = useRef<HTMLInputElement | null>(null);

const [createEvaluation, { loading: evaluationLoading }] = useMutation(CREATE_EVALUATION, {
onCompleted: () => {
setNotification('Evaluation started successfully!');
navigate('/chat');
},
onError: (error) => {
setNotification(error.message, 'warning');
},
});

const goldenQaOptions =
goldenQADatasets.length === 0
? [{ id: '0', label: 'No Golden QA available, upload one first' }]
: goldenQADatasets.map((name) => ({ id: name, label: name }));
const assistantOptions = [{ id: '', label: 'Pick your assistant & version to evaluate' }];
: goldenQADatasets.map(({ datasetId, name }) => ({ id: datasetId, label: name }));

const { data: versionsData } = useQuery(GET_ASSISTANT_CONFIG_VERSIONS, {
variables: { filter: {} },
});

const assistantOptions =
versionsData?.assistantConfigVersions?.length > 0
? versionsData.assistantConfigVersions.map((v: any) => ({
id: v.id,
label: `${v.assistantName} (Version ${v.versionNumber})`,
}))
: [{ id: '', label: 'No assistants available' }];

const validationSchema = Yup.object().shape({
evaluationName: Yup.string()
.required('Evaluation name is required')
.matches(/^[a-zA-Z0-9_-]+$/, 'Invalid evaluation name'),
evaluationName: Yup.string().required('Evaluation name is required'),
goldenQaId: Yup.string().required('Please select a Golden QA dataset'),
assistantId: Yup.string().required('Please select an AI Assistant'),
});

const [states, setStates] = useState<{ evaluationName: string; goldenQaId: string; assistantId: string }>({
const [states, setStates] = useState<{ evaluationName: string; goldenQaId: number; assistantId: string }>({
evaluationName: '',
goldenQaId: '0',
goldenQaId: 0,
assistantId: '',
});

Expand All @@ -110,10 +117,9 @@ export default function AIEvaluationCreate() {
setShowUploadGoldenQaDialog(true);
};

const handleUploadGoldenQaProceed = (values: { name: string }) => {
// Placeholder for when backend mutation response needs to be used further. New upload at top.
setGoldenQADatasets((prev) => [values.name, ...prev]);
setStates((prev) => ({ ...prev, goldenQaId: values.name }));
const handleUploadGoldenQaProceed = (values: { datasetId: number; name: string }) => {
setGoldenQADatasets((prev) => [{ datasetId: values.datasetId, name: values.name }, ...prev]);
setStates((prev) => ({ ...prev, goldenQaId: values.datasetId }));
setShowUploadGoldenQaDialog(false);
};

Expand All @@ -127,24 +133,46 @@ export default function AIEvaluationCreate() {
helperText: goldenQAHelperContent,
onUploadGoldenQaClick: handleUploadGoldenQaButtonClick,
},
{
component: SectionDivider,
name: '__evaluationDetailsDivider',
label: 'Evaluation Details',
placedolder: '',
},
{
component: Input,
name: 'evaluationName',
type: 'text',
label: 'Evaluation Name*',
placeholder: 'Give a unique name for the evaluation experiment.',
placeholder: 'Give a unique name for the evaluation experiment',
},
{
component: Dropdown,
name: 'assistantId',
label: 'AI Assistant*',
options: assistantOptions,
placeholder: '',
helperText: "This list includes all assistants and versions you've created.",
},
];

const dialogMessage = 'This action cannot be undone.';

const handleSetPayload = (payload: any) => {
const selectedVersion = versionsData?.assistantConfigVersions?.find((v: any) => v.id === payload.assistantId);
createEvaluation({
variables: {
input: {
datasetId: payload.goldenQaId,
experimentName: payload.evaluationName,
configId: selectedVersion?.kaapiUuid ?? payload.assistantId,
configVersion: payload.assistantId,
},
},
});
return payload;
};

return (
<div>
<FormLayout
Expand All @@ -156,10 +184,10 @@ export default function AIEvaluationCreate() {
formFields={formFields}
redirectionLink="ai-evaluations"
listItem="aiEvaluation"
getItemQuery={DUMMY_GET_ITEM}
createItemQuery={DUMMY_CREATE}
updateItemQuery={DUMMY_UPDATE}
deleteItemQuery={DUMMY_DELETE}
getItemQuery={GET_ASSISTANT_CONFIG_VERSIONS}
createItemQuery={CREATE_EVALUATION}
updateItemQuery={CREATE_EVALUATION}
deleteItemQuery={CREATE_EVALUATION}
defaultAttribute={null}
icon={null}
refetchQueries={[]}
Expand All @@ -177,7 +205,9 @@ export default function AIEvaluationCreate() {
noHeading={false}
partialPage={false}
confirmationState={{ show: false, title: '', message: '' }}
customStyles={styles.FormLayout}
setPayload={handleSetPayload}
customHandler={() => {}}
buttonState={{ status: evaluationLoading, text: 'Running...', styles: '', show: true }}
/>
<input
ref={fileInputRef}
Expand All @@ -197,4 +227,4 @@ export default function AIEvaluationCreate() {
)}
</div>
);
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ describe('UploadGoldenQaDialog', () => {
expect(notificationSpy).toHaveBeenCalledWith('Golden QA uploaded successfully', 'success');
});
expect(onProceed).toHaveBeenCalledWith({
datasetId: 123,
name: 'golden_qa',
duplicationFactor: 1,
});
Expand All @@ -215,6 +216,7 @@ describe('UploadGoldenQaDialog', () => {

await waitFor(() => {
expect(onProceed).toHaveBeenCalledWith({
datasetId: 456,
name: 'my_custom_name',
duplicationFactor: 3,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export interface UploadGoldenQaDialogProps {
fileName: string;
file: File | null;
onClose: () => void;
onProceed: (values: { name: string; duplicationFactor: number }) => void;
onProceed: (values: { datasetId: number; name: string; duplicationFactor: number }) => void;
}

export const UploadGoldenQaDialog = ({ open, fileName, file, onClose, onProceed }: UploadGoldenQaDialogProps) => {
Expand Down Expand Up @@ -94,6 +94,7 @@ export const UploadGoldenQaDialog = ({ open, fileName, file, onClose, onProceed

setNotification('Golden QA uploaded successfully', 'success');
onProceed({
datasetId: parseInt(goldenQa.datasetId, 10),
name: goldenQa.name,
duplicationFactor: goldenQa.duplication_factor,
});
Expand Down
9 changes: 9 additions & 0 deletions src/graphql/mutations/AIEvaluations.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
import { gql } from '@apollo/client';

export const CREATE_EVALUATION = gql`
mutation createEvaluation($input: EvaluationInput!) {
createEvaluation(input: $input) {
status
}
}
`;

export const CREATE_GOLDEN_QA = gql`
mutation CreateGoldenQa($input: GoldenQaInput!) {
createGoldenQa(input: $input) {
goldenQa {
datasetId
name
}
errors {
Expand Down
Loading
Loading