Skip to content

Commit 355bc10

Browse files
committed
Feature: add a qna evaluation for post training checkpoint evals
- Add endpoints for a checkpoint listing and the execution of a qna eval. - The backend go api will call a container that loads the requested checkpoint and qna.yaml into vLLM and queries the checkpoint with the seed questions from the QnA to get a sense of the fidelity of the checkpoint from the orignal seed examples. Signed-off-by: Brent Salisbury <[email protected]>
1 parent 0d8574e commit 355bc10

File tree

3 files changed

+282
-4
lines changed

3 files changed

+282
-4
lines changed
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// app/api/native/eval/checkpoints/route.ts
2+
'use server';
3+
4+
import type { NextRequest } from 'next/server';
5+
import { NextResponse } from 'next/server';
6+
7+
export async function GET(request: NextRequest) {
8+
console.log('Received GET request for /api/native/eval/checkpoints');
9+
10+
try {
11+
const apiUrl = 'http://localhost:8080/checkpoints';
12+
console.log(`Fetching checkpoints from external API: ${apiUrl}`);
13+
14+
const res = await fetch(apiUrl, {
15+
method: 'GET',
16+
headers: {
17+
'Content-Type': 'application/json'
18+
}
19+
});
20+
21+
console.log(`External API response status: ${res.status}`);
22+
23+
if (!res.ok) {
24+
const errorData = await res.json();
25+
console.error('Error from external API:', errorData);
26+
return NextResponse.json({ error: errorData.error || 'Failed to fetch checkpoints' }, { status: res.status });
27+
}
28+
29+
const data = await res.json();
30+
console.log('Checkpoints data fetched successfully:', data);
31+
32+
// Validate that data is an array
33+
if (!Array.isArray(data)) {
34+
console.warn('Unexpected data format from external API:', data);
35+
return NextResponse.json({ error: 'Invalid data format received from checkpoints API.' }, { status: 500 });
36+
}
37+
38+
return NextResponse.json(data);
39+
} catch (error) {
40+
console.error('Error fetching checkpoints:', error);
41+
return NextResponse.json({ error: 'Unable to reach the checkpoints endpoint' }, { status: 500 });
42+
}
43+
}

src/app/api/native/eval/qna/route.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// src/app/api/native/eval/qna/route.ts
2+
import { NextResponse, NextRequest } from 'next/server';
3+
4+
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8080';
5+
const HARD_CODED_QNA_PATH = '/var/home/cloud-user/.local/share/instructlab/taxonomy/knowledge/history/amazon/qna.yaml';
6+
7+
export async function POST(request: NextRequest) {
8+
try {
9+
const body = await request.json();
10+
console.log('[SERVER] Body received:', body);
11+
12+
const { selectedModelDir } = body;
13+
if (!selectedModelDir) {
14+
console.error('[SERVER] Missing selectedModelDir in request body!');
15+
return NextResponse.json({ error: 'Missing required field: selectedModelDir' }, { status: 400 });
16+
}
17+
18+
console.log('[SERVER] selectedModelDir:', selectedModelDir);
19+
20+
// Forward to Go backend
21+
const response = await fetch(`${BACKEND_URL}/qna-eval`, {
22+
method: 'POST',
23+
headers: { 'Content-Type': 'application/json' },
24+
body: JSON.stringify({
25+
model_path: selectedModelDir,
26+
yaml_file: HARD_CODED_QNA_PATH
27+
})
28+
});
29+
30+
const data = await response.json();
31+
console.log('[SERVER] Response from Go backend:', data);
32+
33+
if (!response.ok) {
34+
return NextResponse.json({ error: data.error || 'Failed to initiate QnA evaluation' }, { status: response.status });
35+
}
36+
37+
return NextResponse.json(data, { status: 200 });
38+
} catch (error) {
39+
console.error('Error in /api/native/eval/qna route:', error);
40+
return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 });
41+
}
42+
}

src/components/Dashboard/Native/dashboard.tsx

Lines changed: 197 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,12 @@ import {
3333
ModalVariant,
3434
ModalBody,
3535
ModalFooter,
36-
ModalHeader
36+
ModalHeader,
37+
Dropdown,
38+
DropdownItem,
39+
DropdownList,
40+
MenuToggle,
41+
MenuToggleElement
3742
} from '@patternfly/react-core';
3843
import {
3944
ExternalLinkAltIcon,
@@ -42,7 +47,8 @@ import {
4247
CatalogIcon,
4348
PencilAltIcon,
4449
UploadIcon,
45-
TrashIcon
50+
TrashIcon,
51+
BalanceScaleIcon
4652
} from '@patternfly/react-icons';
4753
import { ExpandableSection } from '@patternfly/react-core/dist/esm/components/ExpandableSection/ExpandableSection';
4854
import { v4 as uuidv4 } from 'uuid';
@@ -77,6 +83,18 @@ const DashboardNative: React.FunctionComponent = () => {
7783
const [isEditModalOpen, setIsEditModalOpen] = React.useState(false);
7884
const [expandedFiles, setExpandedFiles] = React.useState<Record<string, boolean>>({});
7985

86+
// State Variables for Evaluate Checkpoint
87+
const [isEvalModalOpen, setIsEvalModalOpen] = React.useState<boolean>(false);
88+
const [checkpoints, setCheckpoints] = React.useState<string[]>([]);
89+
const [isCheckpointsLoading, setIsCheckpointsLoading] = React.useState<boolean>(false);
90+
const [checkpointsError, setCheckpointsError] = React.useState<string | null>(null);
91+
const [isDropdownOpen, setIsDropdownOpen] = React.useState<boolean>(false);
92+
const [selectedCheckpoint, setSelectedCheckpoint] = React.useState<string | null>(null);
93+
94+
// QnA eval result
95+
const [qnaEvalResult, setQnaEvalResult] = React.useState<string>('');
96+
const [isQnaLoading, setIsQnaLoading] = React.useState<boolean>(false);
97+
8098
const router = useRouter();
8199

82100
// Fetch branches from the API route
@@ -285,6 +303,100 @@ const DashboardNative: React.FunctionComponent = () => {
285303
}));
286304
};
287305

306+
const handleOpenEvalModal = () => {
307+
setIsEvalModalOpen(true);
308+
fetchCheckpoints();
309+
};
310+
311+
const handleCloseEvalModal = () => {
312+
setIsEvalModalOpen(false);
313+
setCheckpoints([]);
314+
setCheckpointsError(null);
315+
setSelectedCheckpoint(null);
316+
setQnaEvalResult('');
317+
setIsQnaLoading(false);
318+
};
319+
320+
// **New Function to Fetch Checkpoints from API Route**
321+
const fetchCheckpoints = async () => {
322+
setIsCheckpointsLoading(true);
323+
setCheckpointsError(null);
324+
try {
325+
const response = await fetch('/api/native/eval/checkpoints');
326+
console.log('Response status:', response.status);
327+
const data = await response.json();
328+
console.log('Checkpoints data:', data);
329+
330+
if (response.ok) {
331+
// Assuming the API returns an array of checkpoints
332+
if (Array.isArray(data) && data.length > 0) {
333+
setCheckpoints(data);
334+
console.log('Checkpoints set successfully:', data);
335+
} else {
336+
setCheckpoints([]);
337+
console.log('No checkpoints returned from API.');
338+
}
339+
} else {
340+
setCheckpointsError(data.error || 'Failed to fetch checkpoints.');
341+
console.error('Error fetching checkpoints:', data.error || 'Failed to fetch checkpoints.');
342+
}
343+
} catch (error) {
344+
console.error('Error fetching checkpoints:', error);
345+
setCheckpointsError('Unable to reach the checkpoints endpoint.');
346+
} finally {
347+
setIsCheckpointsLoading(false);
348+
}
349+
};
350+
351+
// Checkpoint select dropdown
352+
const onDropdownToggle = (isOpen: boolean) => setIsDropdownOpen(isOpen);
353+
const onSelectCheckpoint = (event: React.MouseEvent<Element, MouseEvent>, selection: string) => {
354+
setSelectedCheckpoint(selection);
355+
setIsDropdownOpen(false);
356+
};
357+
358+
const handleEvaluateQnA = async () => {
359+
if (!selectedCheckpoint) {
360+
addDangerAlert('Please select a checkpoint to evaluate.');
361+
return;
362+
}
363+
364+
setIsQnaLoading(true);
365+
setQnaEvalResult('');
366+
367+
// TODO: dynamically prepend the checkpoint path
368+
const selectedModelDir = '/var/home/cloud-user/.local/share/instructlab/checkpoints/hf_format/' + selectedCheckpoint;
369+
370+
console.log('[CLIENT] Sending to /api/native/eval/qna:', selectedModelDir);
371+
372+
try {
373+
const res = await fetch('/api/native/eval/qna', {
374+
method: 'POST',
375+
headers: { 'Content-Type': 'application/json' },
376+
body: JSON.stringify({ selectedModelDir })
377+
});
378+
379+
const data = await res.json();
380+
console.log('[CLIENT] Response from /api/native/eval/qna:', data);
381+
382+
if (!res.ok) {
383+
addDangerAlert(data.error || 'Failed to evaluate QnA.');
384+
} else {
385+
if (data.result) {
386+
setQnaEvalResult(data.result);
387+
addSuccessAlert('QnA Evaluation succeeded!');
388+
} else {
389+
setQnaEvalResult('Evaluation completed (no result field).');
390+
}
391+
}
392+
} catch (error) {
393+
console.error('Error evaluating QnA:', error);
394+
addDangerAlert('Error evaluating QnA.');
395+
} finally {
396+
setIsQnaLoading(false);
397+
}
398+
};
399+
288400
return (
289401
<div>
290402
<PageBreadcrumb hasBodyWrapper={false}>
@@ -397,6 +509,9 @@ const DashboardNative: React.FunctionComponent = () => {
397509
<Tooltip aria="none" aria-live="polite" content={<div>Delete</div>}>
398510
<Button icon={<TrashIcon />} variant="plain" aria-label="delete" onClick={() => handleDeleteContribution(branch.name)} />
399511
</Tooltip>
512+
<Tooltip aria="none" aria-live="polite" content={<div>Evaluate QnA Checkpoint</div>}>
513+
<Button icon={<BalanceScaleIcon />} variant="plain" aria-label="evaluate" onClick={handleOpenEvalModal} />
514+
</Tooltip>
400515
</FlexItem>
401516
</Flex>
402517
</CardBody>
@@ -412,6 +527,84 @@ const DashboardNative: React.FunctionComponent = () => {
412527
</PageSection>
413528
)}
414529

530+
{/* Evaluate Checkpoint Modal */}
531+
<Modal
532+
variant={ModalVariant.medium}
533+
title="Evaluate Checkpoint"
534+
isOpen={isEvalModalOpen}
535+
onClose={handleCloseEvalModal}
536+
aria-labelledby="evaluate-checkpoint-modal-title"
537+
aria-describedby="evaluate-checkpoint-modal-body"
538+
>
539+
<ModalHeader title="Evaluate Checkpoint" />
540+
<ModalBody id="evaluate-checkpoint-modal-body">
541+
{isCheckpointsLoading ? (
542+
<Spinner size="lg" aria-label="Loading checkpoints" />
543+
) : checkpointsError ? (
544+
<Alert variant="danger" title={checkpointsError} isInline />
545+
) : (
546+
<>
547+
<div style={{ marginBottom: '1rem' }}>
548+
<label style={{ display: 'block', marginBottom: '0.4rem' }}>Select a Checkpoint:</label>
549+
<Dropdown
550+
isOpen={isDropdownOpen}
551+
onSelect={onSelectCheckpoint}
552+
onOpenChange={onDropdownToggle}
553+
toggle={(toggleRef: React.Ref<MenuToggleElement>) => (
554+
<MenuToggle ref={toggleRef} onClick={() => onDropdownToggle(!isDropdownOpen)} isExpanded={isDropdownOpen}>
555+
{selectedCheckpoint || 'Select a Checkpoint'}
556+
</MenuToggle>
557+
)}
558+
ouiaId="EvaluateCheckpointDropdown"
559+
shouldFocusToggleOnSelect
560+
>
561+
<DropdownList>
562+
{checkpoints.length > 0 ? (
563+
checkpoints.map((checkpoint) => (
564+
<DropdownItem key={checkpoint} value={checkpoint}>
565+
{checkpoint}
566+
</DropdownItem>
567+
))
568+
) : (
569+
<DropdownItem key="no-checkpoints" isDisabled>
570+
No checkpoints available
571+
</DropdownItem>
572+
)}
573+
</DropdownList>
574+
</Dropdown>
575+
</div>
576+
577+
{/* Display the evaluation result */}
578+
{qnaEvalResult && (
579+
<div style={{ marginTop: '1rem' }}>
580+
<b>Evaluation Output:</b>
581+
<pre
582+
style={{
583+
marginTop: '0.5rem',
584+
backgroundColor: '#f5f5f5',
585+
padding: '1rem',
586+
borderRadius: '4px',
587+
maxHeight: '300px',
588+
overflowY: 'auto'
589+
}}
590+
>
591+
{qnaEvalResult}
592+
</pre>
593+
</div>
594+
)}
595+
</>
596+
)}
597+
</ModalBody>
598+
<ModalFooter>
599+
<Button key="evaluateQnA" variant="primary" onClick={handleEvaluateQnA} isDisabled={!selectedCheckpoint || isQnaLoading}>
600+
{isQnaLoading ? 'Evaluating...' : 'Evaluate'}
601+
</Button>
602+
<Button key="cancel" variant="secondary" onClick={handleCloseEvalModal}>
603+
Cancel
604+
</Button>
605+
</ModalFooter>
606+
</Modal>
607+
415608
<Modal
416609
variant={ModalVariant.medium}
417610
title={`Files Contained in Branch: ${diffData?.branch}`}
@@ -486,7 +679,7 @@ const DashboardNative: React.FunctionComponent = () => {
486679
>
487680
<ModalHeader title="Deleting Contribution" labelId="delete-contribution-modal-title" titleIconVariant="warning" />
488681
<ModalBody id="delete-contribution-body-variant">
489-
<p>are you sure you want to delete this contribution?</p>
682+
<p>Are you sure you want to delete this contribution?</p>
490683
</ModalBody>
491684
<ModalFooter>
492685
<Button key="confirm" variant="primary" onClick={() => handleDeleteContributionConfirm()}>
@@ -509,7 +702,7 @@ const DashboardNative: React.FunctionComponent = () => {
509702
>
510703
<ModalHeader title="Publishing Contribution" labelId="publish-contribution-modal-title" titleIconVariant="warning" />
511704
<ModalBody id="publish-contribution-body-variant">
512-
<p>are you sure you want to publish contribution to remote taxonomy repository present at : {taxonomyRepoDir}?</p>
705+
<p>Are you sure you want to publish contribution to remote taxonomy repository present at : {taxonomyRepoDir}?</p>
513706
</ModalBody>
514707
<ModalFooter>
515708
<Button key="confirm" variant="primary" onClick={() => handlePublishContributionConfirm()}>

0 commit comments

Comments
 (0)