Commit 2eeea151 by light5980 Committed by GitHub

debug: training count error (#7169)

parent b46748e1
...@@ -14,14 +14,12 @@ import type { GetCollectionTrainingDetailResponseType } from '@fastgpt/global/op ...@@ -14,14 +14,12 @@ import type { GetCollectionTrainingDetailResponseType } from '@fastgpt/global/op
import type { Permission } from '@fastgpt/global/support/permission/controller'; import type { Permission } from '@fastgpt/global/support/permission/controller';
import React from 'react'; import React from 'react';
import TrainingErrorList from './TrainingErrorList'; import TrainingErrorList from './TrainingErrorList';
import {
enum TrainingStatus { getTrainingStepStatus,
NotStart = 'NotStart', isTrainingDetailReady,
Queued = 'Queued', // wait count>0 isTrainingStepHighlighted,
Running = 'Running', // wait count=0; training count>0. TrainingStatus
Ready = 'Ready', } from './trainingStatesUtils';
Error = 'Error'
}
const ProgressView = ({ const ProgressView = ({
trainingDetail trainingDetail
...@@ -36,28 +34,23 @@ const ProgressView = ({ ...@@ -36,28 +34,23 @@ const ProgressView = ({
const isImageIndex = trainingDetail.advancedTraining.imageIndex; const isImageIndex = trainingDetail.advancedTraining.imageIndex;
const isAutoIndexes = trainingDetail.advancedTraining.autoIndexes; const isAutoIndexes = trainingDetail.advancedTraining.autoIndexes;
/*
状态计算
1. 暂时没有内容解析的状态
2. 完全没有训练数据时候,已就绪
3. 有训练数据,中间过程全部是进行中
*/
const statesArray = useMemo(() => { const statesArray = useMemo(() => {
const isReady = const isReady = isTrainingDetailReady(trainingDetail);
Object.values(trainingDetail.queuedCounts).every((count) => count === 0) && const modeOrder = [
Object.values(trainingDetail.trainingCounts).every((count) => count === 0) && TrainingModeEnum.parse,
Object.values(trainingDetail.errorCounts).every((count) => count === 0); ...(isImageParse ? [TrainingModeEnum.imageParse] : []),
...(isQA ? [TrainingModeEnum.qa] : []),
const isContentParsing = trainingDetail.trainingCounts.parse > 0; ...(isImageIndex ? [TrainingModeEnum.image] : []),
...(isAutoIndexes ? [TrainingModeEnum.auto] : []),
TrainingModeEnum.chunk
];
const getTrainingStatus = ({ errorCount }: { errorCount: number }) => { const getTrainingStatus = (mode: TrainingModeEnum) =>
if (isContentParsing) return TrainingStatus.NotStart; getTrainingStepStatus({
if (isReady) return TrainingStatus.Ready; trainingDetail,
if (errorCount > 0) { mode,
return TrainingStatus.Error; modeOrder
} });
return TrainingStatus.Running;
};
// 只显示排队和处理中的数量 // 只显示排队和处理中的数量
const getStatusText = (mode: TrainingModeEnum) => { const getStatusText = (mode: TrainingModeEnum) => {
...@@ -85,9 +78,7 @@ const ProgressView = ({ ...@@ -85,9 +78,7 @@ const ProgressView = ({
{ {
label: t(TrainingProcess.parsing.label), label: t(TrainingProcess.parsing.label),
statusText: getStatusText(TrainingModeEnum.parse), statusText: getStatusText(TrainingModeEnum.parse),
status: getTrainingStatus({ status: getTrainingStatus(TrainingModeEnum.parse),
errorCount: trainingDetail.errorCounts.parse
}),
errorCount: trainingDetail.errorCounts.parse errorCount: trainingDetail.errorCounts.parse
}, },
...(isImageParse ...(isImageParse
...@@ -96,9 +87,7 @@ const ProgressView = ({ ...@@ -96,9 +87,7 @@ const ProgressView = ({
errorCount: trainingDetail.errorCounts.imageParse, errorCount: trainingDetail.errorCounts.imageParse,
label: t(TrainingProcess.parseImage.label), label: t(TrainingProcess.parseImage.label),
statusText: getStatusText(TrainingModeEnum.imageParse), statusText: getStatusText(TrainingModeEnum.imageParse),
status: getTrainingStatus({ status: getTrainingStatus(TrainingModeEnum.imageParse)
errorCount: trainingDetail.errorCounts.imageParse
})
} }
] ]
: []), : []),
...@@ -107,9 +96,7 @@ const ProgressView = ({ ...@@ -107,9 +96,7 @@ const ProgressView = ({
{ {
label: t(TrainingProcess.getQA.label), label: t(TrainingProcess.getQA.label),
statusText: getStatusText(TrainingModeEnum.qa), statusText: getStatusText(TrainingModeEnum.qa),
status: getTrainingStatus({ status: getTrainingStatus(TrainingModeEnum.qa),
errorCount: trainingDetail.errorCounts.qa
}),
errorCount: trainingDetail.errorCounts.qa errorCount: trainingDetail.errorCounts.qa
} }
] ]
...@@ -120,9 +107,7 @@ const ProgressView = ({ ...@@ -120,9 +107,7 @@ const ProgressView = ({
errorCount: trainingDetail.errorCounts.image, errorCount: trainingDetail.errorCounts.image,
label: t(TrainingProcess.imageIndex.label), label: t(TrainingProcess.imageIndex.label),
statusText: getStatusText(TrainingModeEnum.image), statusText: getStatusText(TrainingModeEnum.image),
status: getTrainingStatus({ status: getTrainingStatus(TrainingModeEnum.image)
errorCount: trainingDetail.errorCounts.image
})
} }
] ]
: []), : []),
...@@ -132,9 +117,7 @@ const ProgressView = ({ ...@@ -132,9 +117,7 @@ const ProgressView = ({
errorCount: trainingDetail.errorCounts.auto, errorCount: trainingDetail.errorCounts.auto,
label: t(TrainingProcess.autoIndex.label), label: t(TrainingProcess.autoIndex.label),
statusText: getStatusText(TrainingModeEnum.auto), statusText: getStatusText(TrainingModeEnum.auto),
status: getTrainingStatus({ status: getTrainingStatus(TrainingModeEnum.auto)
errorCount: trainingDetail.errorCounts.auto
})
} }
] ]
: []), : []),
...@@ -142,9 +125,7 @@ const ProgressView = ({ ...@@ -142,9 +125,7 @@ const ProgressView = ({
errorCount: trainingDetail.errorCounts.chunk, errorCount: trainingDetail.errorCounts.chunk,
label: t(TrainingProcess.vectorizing.label), label: t(TrainingProcess.vectorizing.label),
statusText: getStatusText(TrainingModeEnum.chunk), statusText: getStatusText(TrainingModeEnum.chunk),
status: getTrainingStatus({ status: getTrainingStatus(TrainingModeEnum.chunk)
errorCount: trainingDetail.errorCounts.chunk
})
}, },
{ {
errorCount: 0, errorCount: 0,
...@@ -159,21 +140,16 @@ const ProgressView = ({ ...@@ -159,21 +140,16 @@ const ProgressView = ({
]; ];
return states; return states;
}, [ }, [trainingDetail, isImageIndex, isAutoIndexes, t, isImageParse, isQA]);
trainingDetail.queuedCounts,
trainingDetail.trainingCounts,
trainingDetail.errorCounts,
isImageIndex,
isAutoIndexes,
trainingDetail.trainedCount,
t,
isImageParse,
isQA
]);
return ( return (
<Flex flexDirection={'column'} gap={6}> <Flex flexDirection={'column'} gap={6}>
{statesArray.map((item, index) => ( {statesArray.map((item, index) => {
const isHighlighted = isTrainingStepHighlighted(item.status);
const isActive =
item.status === TrainingStatus.Queued || item.status === TrainingStatus.Running;
return (
<Flex alignItems={'center'} pl={4} key={index}> <Flex alignItems={'center'} pl={4} key={index}>
{/* Status round */} {/* Status round */}
<Box <Box
...@@ -185,16 +161,13 @@ const ProgressView = ({ ...@@ -185,16 +161,13 @@ const ProgressView = ({
display={'flex'} display={'flex'}
alignItems={'center'} alignItems={'center'}
justifyContent={'center'} justifyContent={'center'}
{...((item.status === TrainingStatus.Running || {...(isHighlighted && {
item.status === TrainingStatus.Error) && {
bg: 'primary.600',
borderColor: 'primary.600',
boxShadow: '0 0 0 4px var(--Royal-Blue-100, #E1EAFF)'
})}
{...(item.status === TrainingStatus.Ready && {
bg: 'primary.600', bg: 'primary.600',
borderColor: 'primary.600' borderColor: 'primary.600'
})} })}
{...(isActive && {
boxShadow: '0 0 0 4px var(--Royal-Blue-100, #E1EAFF)'
})}
// Line // Line
{...(index !== statesArray.length - 1 && { {...(index !== statesArray.length - 1 && {
_after: { _after: {
...@@ -217,10 +190,10 @@ const ProgressView = ({ ...@@ -217,10 +190,10 @@ const ProgressView = ({
alignItems={'center'} alignItems={'center'}
w={'full'} w={'full'}
bg={ bg={
item.status === TrainingStatus.Running item.status === TrainingStatus.Error
? 'primary.50'
: item.status === TrainingStatus.Error
? 'red.50' ? 'red.50'
: isHighlighted
? 'primary.50'
: 'myGray.50' : 'myGray.50'
} }
py={2.5} py={2.5}
...@@ -259,7 +232,8 @@ const ProgressView = ({ ...@@ -259,7 +232,8 @@ const ProgressView = ({
)} )}
</Flex> </Flex>
</Flex> </Flex>
))} );
})}
</Flex> </Flex>
); );
}; };
......
import type { TrainingModeEnum } from '@fastgpt/global/core/dataset/constants';
import type { GetCollectionTrainingDetailResponseType } from '@fastgpt/global/openapi/core/dataset/collection/api';
export enum TrainingStatus {
NotStart = 'NotStart',
Queued = 'Queued',
Running = 'Running',
Ready = 'Ready',
Error = 'Error'
}
/**
* 训练进度弹窗只有未开始阶段置灰;已完成、排队中、处理中和异常阶段都需要保持视觉激活。
*/
export const isTrainingStepHighlighted = (status: TrainingStatus) =>
status !== TrainingStatus.NotStart;
/**
* 判断当前集合是否已经没有任何剩余训练或最终异常。
*/
export const isTrainingDetailReady = (trainingDetail: GetCollectionTrainingDetailResponseType) =>
Object.values(trainingDetail.queuedCounts).every((count) => count === 0) &&
Object.values(trainingDetail.trainingCounts).every((count) => count === 0) &&
Object.values(trainingDetail.errorCounts).every((count) => count === 0);
/**
* 根据当前集合各训练阶段的计数计算单个阶段的展示状态。
* 已进入后续阶段时,前序阶段展示为完成;仍被前序阶段阻塞时,后续阶段保持未开始。
*/
export const getTrainingStepStatus = ({
trainingDetail,
mode,
modeOrder
}: {
trainingDetail: GetCollectionTrainingDetailResponseType;
mode: TrainingModeEnum;
modeOrder: TrainingModeEnum[];
}) => {
if (isTrainingDetailReady(trainingDetail)) return TrainingStatus.Ready;
if (trainingDetail.errorCounts[mode] > 0) return TrainingStatus.Error;
if (trainingDetail.trainingCounts[mode] > 0) return TrainingStatus.Running;
if (trainingDetail.queuedCounts[mode] > 0) return TrainingStatus.Queued;
const modeIndex = modeOrder.indexOf(mode);
if (modeIndex === -1) return TrainingStatus.NotStart;
const hasLaterProgress = modeOrder.slice(modeIndex + 1).some((nextMode) => {
return (
trainingDetail.queuedCounts[nextMode] > 0 ||
trainingDetail.trainingCounts[nextMode] > 0 ||
trainingDetail.errorCounts[nextMode] > 0
);
});
return hasLaterProgress ? TrainingStatus.Ready : TrainingStatus.NotStart;
};
...@@ -14,9 +14,9 @@ import { ...@@ -14,9 +14,9 @@ import {
} from '@fastgpt/global/openapi/core/dataset/collection/api'; } from '@fastgpt/global/openapi/core/dataset/collection/api';
import { import {
BLOCKED_LOCK_TIME, BLOCKED_LOCK_TIME,
activeTrainingMatch,
finalErrorTrainingMatch finalErrorTrainingMatch
} from '@fastgpt/service/core/dataset/training/query'; } from '@fastgpt/service/core/dataset/training/query';
import { subMinutes } from 'date-fns';
const defaultCounts: Record<TrainingModeEnum, number> = { const defaultCounts: Record<TrainingModeEnum, number> = {
parse: 0, parse: 0,
...@@ -27,6 +27,15 @@ const defaultCounts: Record<TrainingModeEnum, number> = { ...@@ -27,6 +27,15 @@ const defaultCounts: Record<TrainingModeEnum, number> = {
imageParse: 0 imageParse: 0
}; };
const MODE_LOCK_TIMEOUT_MINUTES: Record<TrainingModeEnum, number> = {
parse: 10,
qa: 10,
chunk: 3,
image: 10,
auto: 10,
imageParse: 10
};
async function handler(req: ApiRequestProps): Promise<GetCollectionTrainingDetailResponseType> { async function handler(req: ApiRequestProps): Promise<GetCollectionTrainingDetailResponseType> {
const { collectionId } = parseApiInput({ const { collectionId } = parseApiInput({
req, req,
...@@ -47,19 +56,23 @@ async function handler(req: ApiRequestProps): Promise<GetCollectionTrainingDetai ...@@ -47,19 +56,23 @@ async function handler(req: ApiRequestProps): Promise<GetCollectionTrainingDetai
collectionId: new Types.ObjectId(collection._id) collectionId: new Types.ObjectId(collection._id)
}; };
// Computed global queue const now = new Date();
const minId = ( const activeTrainingExpr = Object.entries(MODE_LOCK_TIMEOUT_MINUTES).map(
await MongoDatasetTraining.findOne(match, { sort: { _id: 1 }, select: '_id' }).lean() ([mode, timeoutMinutes]) => ({
)?._id; mode,
lockTime: { $gt: subMinutes(now, timeoutMinutes), $lt: BLOCKED_LOCK_TIME }
})
);
const [ququedCountData, trainingCountData, errorCountData, trainedCount] = (await Promise.all([ const [ququedCountData, trainingCountData, errorCountData, trainedCount] = (await Promise.all([
minId MongoDatasetTraining.aggregate([
? MongoDatasetTraining.aggregate([
{ {
$match: { $match: {
_id: { $lt: new Types.ObjectId(minId) }, ...match,
retryCount: { $gt: 0 }, retryCount: { $gt: 0 },
lockTime: { $lt: BLOCKED_LOCK_TIME } lockTime: { $lt: BLOCKED_LOCK_TIME },
// 只统计当前集合里未被 worker 领取或锁超时后可重试的任务,避免跨知识库队列污染状态展示。
$nor: activeTrainingExpr
} }
}, },
{ {
...@@ -68,13 +81,13 @@ async function handler(req: ApiRequestProps): Promise<GetCollectionTrainingDetai ...@@ -68,13 +81,13 @@ async function handler(req: ApiRequestProps): Promise<GetCollectionTrainingDetai
count: { $sum: 1 } count: { $sum: 1 }
} }
} }
]) ]),
: Promise.resolve([]),
MongoDatasetTraining.aggregate([ MongoDatasetTraining.aggregate([
{ {
$match: { $match: {
...match, ...match,
...activeTrainingMatch retryCount: { $gt: 0 },
$or: activeTrainingExpr
} }
}, },
{ {
......
...@@ -94,7 +94,7 @@ describe('collection training status api', () => { ...@@ -94,7 +94,7 @@ describe('collection training status api', () => {
}); });
}); });
it('should use active counts for progress and final errors for error tab', async () => { it('should split current collection queued/running counts and final errors', async () => {
const root = await getRootUser(); const root = await getRootUser();
const dataset = await MongoDataset.create({ const dataset = await MongoDataset.create({
name: 'test', name: 'test',
...@@ -120,6 +120,7 @@ describe('collection training status api', () => { ...@@ -120,6 +120,7 @@ describe('collection training status api', () => {
billId: 'test', billId: 'test',
mode: TrainingModeEnum.qa, mode: TrainingModeEnum.qa,
retryCount: 3, retryCount: 3,
lockTime: new Date(),
errorMsg: 'temporary failed' errorMsg: 'temporary failed'
}, },
{ {
...@@ -128,6 +129,16 @@ describe('collection training status api', () => { ...@@ -128,6 +129,16 @@ describe('collection training status api', () => {
datasetId: dataset._id, datasetId: dataset._id,
collectionId: collection._id, collectionId: collection._id,
billId: 'test', billId: 'test',
mode: TrainingModeEnum.parse,
retryCount: 3,
lockTime: new Date('2000')
},
{
teamId: root.teamId,
tmbId: root.tmbId,
datasetId: dataset._id,
collectionId: collection._id,
billId: 'test',
mode: TrainingModeEnum.chunk, mode: TrainingModeEnum.chunk,
retryCount: 0, retryCount: 0,
errorMsg: 'final failed' errorMsg: 'final failed'
...@@ -142,11 +153,84 @@ describe('collection training status api', () => { ...@@ -142,11 +153,84 @@ describe('collection training status api', () => {
}); });
expect(res.code).toBe(200); expect(res.code).toBe(200);
expect(res.data.queuedCounts.parse).toBe(1);
expect(res.data.trainingCounts.parse).toBe(0);
expect(res.data.queuedCounts.qa).toBe(0);
expect(res.data.trainingCounts.qa).toBe(1); expect(res.data.trainingCounts.qa).toBe(1);
expect(res.data.errorCounts.qa).toBe(0); expect(res.data.errorCounts.qa).toBe(0);
expect(res.data.errorCounts.chunk).toBe(1); expect(res.data.errorCounts.chunk).toBe(1);
}); });
it('should not include other dataset training records in collection queued counts', async () => {
const root = await getRootUser();
const [dataset, otherDataset] = await MongoDataset.create([
{
name: 'current',
teamId: root.teamId,
tmbId: root.tmbId,
vectorModel: 'test',
agentModel: 'test'
},
{
name: 'other',
teamId: root.teamId,
tmbId: root.tmbId,
vectorModel: 'test',
agentModel: 'test'
}
]);
const [collection, otherCollection] = await MongoDatasetCollection.create([
{
name: 'current',
type: DatasetCollectionTypeEnum.file,
teamId: root.teamId,
tmbId: root.tmbId,
datasetId: dataset._id
},
{
name: 'other',
type: DatasetCollectionTypeEnum.file,
teamId: root.teamId,
tmbId: root.tmbId,
datasetId: otherDataset._id
}
]);
await MongoDatasetTraining.create([
...Array.from({ length: 6 }).map(() => ({
teamId: root.teamId,
tmbId: root.tmbId,
datasetId: otherDataset._id,
collectionId: otherCollection._id,
billId: 'test',
mode: TrainingModeEnum.chunk,
retryCount: 3,
lockTime: new Date('2000')
})),
{
teamId: root.teamId,
tmbId: root.tmbId,
datasetId: dataset._id,
collectionId: collection._id,
billId: 'test',
mode: TrainingModeEnum.parse,
retryCount: 3,
lockTime: new Date('2000')
}
]);
const res = await Call(trainingDetailHandler, {
auth: root,
query: {
collectionId: collection._id
}
});
expect(res.code).toBe(200);
expect(res.data.queuedCounts.parse).toBe(1);
expect(res.data.queuedCounts.chunk).toBe(0);
});
it('should keep deprecated scrollList compatible with the collection list item schema', async () => { it('should keep deprecated scrollList compatible with the collection list item schema', async () => {
const root = await getRootUser(); const root = await getRootUser();
const dataset = await MongoDataset.create({ const dataset = await MongoDataset.create({
......
import { describe, expect, it } from 'vitest';
import {
DatasetCollectionDataProcessModeEnum,
TrainingModeEnum
} from '@fastgpt/global/core/dataset/constants';
import type { GetCollectionTrainingDetailResponseType } from '@fastgpt/global/openapi/core/dataset/collection/api';
import {
getTrainingStepStatus,
isTrainingStepHighlighted,
TrainingStatus
} from '@/pageComponents/dataset/detail/CollectionCard/trainingStatesUtils';
const createTrainingDetail = (
overrides: Partial<GetCollectionTrainingDetailResponseType> = {}
): GetCollectionTrainingDetailResponseType => {
const counts = {
parse: 0,
qa: 0,
chunk: 0,
image: 0,
auto: 0,
imageParse: 0
};
return {
trainingType: DatasetCollectionDataProcessModeEnum.chunk,
advancedTraining: {
customPdfParse: false,
imageIndex: false,
autoIndexes: false
},
queuedCounts: { ...counts },
trainingCounts: { ...counts },
errorCounts: { ...counts },
trainedCount: 0,
...overrides
};
};
describe('trainingStatesUtils', () => {
it('should mark parsing step as running while content parsing is active', () => {
const trainingDetail = createTrainingDetail({
trainingCounts: {
parse: 1,
qa: 0,
chunk: 0,
image: 0,
auto: 0,
imageParse: 0
}
});
const modeOrder = [TrainingModeEnum.parse, TrainingModeEnum.chunk];
expect(
getTrainingStepStatus({
trainingDetail,
mode: TrainingModeEnum.parse,
modeOrder
})
).toBe(TrainingStatus.Running);
expect(
getTrainingStepStatus({
trainingDetail,
mode: TrainingModeEnum.chunk,
modeOrder
})
).toBe(TrainingStatus.NotStart);
});
it('should mark parsing step as queued while waiting to be picked by worker', () => {
const trainingDetail = createTrainingDetail({
queuedCounts: {
parse: 1,
qa: 0,
chunk: 0,
image: 0,
auto: 0,
imageParse: 0
}
});
const modeOrder = [TrainingModeEnum.parse, TrainingModeEnum.chunk];
expect(
getTrainingStepStatus({
trainingDetail,
mode: TrainingModeEnum.parse,
modeOrder
})
).toBe(TrainingStatus.Queued);
expect(
getTrainingStepStatus({
trainingDetail,
mode: TrainingModeEnum.chunk,
modeOrder
})
).toBe(TrainingStatus.NotStart);
});
it('should mark earlier steps ready after later steps start', () => {
const trainingDetail = createTrainingDetail({
trainingCounts: {
parse: 0,
qa: 0,
chunk: 1,
image: 0,
auto: 0,
imageParse: 0
},
trainedCount: 1
});
const modeOrder = [TrainingModeEnum.parse, TrainingModeEnum.chunk];
expect(
getTrainingStepStatus({
trainingDetail,
mode: TrainingModeEnum.parse,
modeOrder
})
).toBe(TrainingStatus.Ready);
expect(
getTrainingStepStatus({
trainingDetail,
mode: TrainingModeEnum.chunk,
modeOrder
})
).toBe(TrainingStatus.Running);
});
it('should keep multiple in-progress stages highlighted at the same time', () => {
const trainingDetail = createTrainingDetail({
trainingCounts: {
parse: 1,
qa: 0,
chunk: 2,
image: 0,
auto: 0,
imageParse: 0
}
});
const modeOrder = [TrainingModeEnum.parse, TrainingModeEnum.chunk];
const parseStatus = getTrainingStepStatus({
trainingDetail,
mode: TrainingModeEnum.parse,
modeOrder
});
const chunkStatus = getTrainingStepStatus({
trainingDetail,
mode: TrainingModeEnum.chunk,
modeOrder
});
expect(parseStatus).toBe(TrainingStatus.Running);
expect(chunkStatus).toBe(TrainingStatus.Running);
expect(isTrainingStepHighlighted(parseStatus)).toBe(true);
expect(isTrainingStepHighlighted(chunkStatus)).toBe(true);
});
it('should highlight completed steps and gray out only not-started steps', () => {
expect(isTrainingStepHighlighted(TrainingStatus.Ready)).toBe(true);
expect(isTrainingStepHighlighted(TrainingStatus.Queued)).toBe(true);
expect(isTrainingStepHighlighted(TrainingStatus.Running)).toBe(true);
expect(isTrainingStepHighlighted(TrainingStatus.Error)).toBe(true);
expect(isTrainingStepHighlighted(TrainingStatus.NotStart)).toBe(false);
});
});
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment