Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
3 changes: 3 additions & 0 deletions packages/global/core/evaluation/api.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ export type DeleteEvaluationResponse = MessageResponse;
// List Evaluations
export type ListEvaluationsRequest = PaginationProps<{
searchKey?: string;
appName?: string;
appId?: string;
versionId?: string;
}>;
export type ListEvaluationsResponse = PaginationResponse<EvaluationDisplayType>;

Expand Down
5 changes: 2 additions & 3 deletions packages/service/core/evaluation/metric/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,7 @@ const EvalMetricSchema = new Schema({
},
name: {
type: String,
required: true,
unique: true
required: true
},
description: {
type: String,
Expand Down Expand Up @@ -84,7 +83,7 @@ const EvalMetricSchema = new Schema({
}
});

EvalMetricSchema.index({ teamId: 1, name: 1 });
EvalMetricSchema.index({ teamId: 1, name: 1 }, { unique: true });
EvalMetricSchema.index({ createTime: -1 });

EvalMetricSchema.pre('save', function (next) {
Expand Down
115 changes: 74 additions & 41 deletions packages/service/core/evaluation/task/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,10 @@ export class EvaluationTaskService {
searchKey?: string,
accessibleIds?: string[],
tmbId?: string,
isOwner: boolean = false
isOwner: boolean = false,
appName?: string,
appId?: string,
versionId?: string
): Promise<{ list: EvaluationDisplayType[]; total: number }> {
// Build basic filter and pagination
const filter: any = { teamId: new Types.ObjectId(teamId) };
Expand All @@ -195,43 +198,74 @@ export class EvaluationTaskService {
};
}

// Build aggregation pipeline with target filtering
const aggregationPipeline = [
{ $match: finalFilter },
{
$lookup: {
from: 'eval_dataset_collections',
localField: 'datasetId',
foreignField: '_id',
as: 'dataset'
}
},
{
$addFields: {
'target.config.appObjectId': { $toObjectId: '$target.config.appId' }
}
},
{
$lookup: {
from: 'apps',
localField: 'target.config.appObjectId',
foreignField: '_id',
as: 'app'
}
},
{
$addFields: {
'target.config.versionObjectId': { $toObjectId: '$target.config.versionId' }
}
},
{
$lookup: {
from: 'app_versions',
localField: 'target.config.versionObjectId',
foreignField: '_id',
as: 'appVersion'
}
},
{
$addFields: {
'target.config.appName': { $arrayElemAt: ['$app.name', 0] },
'target.config.avatar': { $arrayElemAt: ['$app.avatar', 0] },
'target.config.versionName': { $arrayElemAt: ['$appVersion.versionName', 0] }
}
}
];

// Add target filtering stage if any target filters are provided
if (appName || appId || versionId) {
const targetFilter: any = {};

if (appName) {
targetFilter['target.config.appName'] = { $regex: appName, $options: 'i' };
}

if (appId) {
targetFilter['target.config.appId'] = appId;
}

if (versionId) {
targetFilter['target.config.versionId'] = versionId;
}

aggregationPipeline.push({ $match: targetFilter });
}

const [evaluations, total] = await Promise.all([
MongoEvaluation.aggregate([
{ $match: finalFilter },
{
$lookup: {
from: 'eval_dataset_collections',
localField: 'datasetId',
foreignField: '_id',
as: 'dataset'
}
},
{
$addFields: {
'target.config.appObjectId': { $toObjectId: '$target.config.appId' }
}
},
{
$lookup: {
from: 'apps',
localField: 'target.config.appObjectId',
foreignField: '_id',
as: 'app'
}
},
{
$addFields: {
'target.config.versionObjectId': { $toObjectId: '$target.config.versionId' }
}
},
{
$lookup: {
from: 'app_versions',
localField: 'target.config.versionObjectId',
foreignField: '_id',
as: 'appVersion'
}
},
...aggregationPipeline,
// Add real-time statistics lookup
{
$lookup: {
Expand All @@ -258,10 +292,6 @@ export class EvaluationTaskService {
{
$addFields: {
datasetName: { $arrayElemAt: ['$dataset.name', 0] },
// Add app name and avatar to target.config
'target.config.appName': { $arrayElemAt: ['$app.name', 0] },
'target.config.avatar': { $arrayElemAt: ['$app.avatar', 0] },
'target.config.versionName': { $arrayElemAt: ['$appVersion.versionName', 0] },
metricNames: {
$map: {
input: '$evaluators',
Expand Down Expand Up @@ -316,7 +346,10 @@ export class EvaluationTaskService {
{ $skip: skip },
{ $limit: limit }
]),
MongoEvaluation.countDocuments(finalFilter)
// Get total count using the same aggregation pipeline (without pagination)
MongoEvaluation.aggregate([...aggregationPipeline, { $count: 'total' }]).then(
(result) => result[0]?.total || 0
)
]);

// Return raw data - permissions will be handled in API layer
Expand Down
3 changes: 1 addition & 2 deletions packages/service/core/evaluation/task/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,6 @@ export const EvaluationTaskSchema = new Schema({
name: {
type: String,
required: true,
unique: true,
trim: true,
maxlength: 100
},
Expand Down Expand Up @@ -170,7 +169,7 @@ export const EvaluationTaskSchema = new Schema({
// Optimized indexes for EvaluationTaskSchema
EvaluationTaskSchema.index({ teamId: 1, createTime: -1 }); // Main query: team filtering + time sorting
EvaluationTaskSchema.index({ teamId: 1, status: 1, createTime: -1 }); // Status filtering + time sorting
EvaluationTaskSchema.index({ teamId: 1, name: 1 }); // Name uniqueness check
EvaluationTaskSchema.index({ teamId: 1, name: 1 }, { unique: true }); // Name uniqueness check
EvaluationTaskSchema.index({ tmbId: 1, createTime: -1 }); // Query by creator
EvaluationTaskSchema.index({ status: 1 }); // Status-based queue processing

Expand Down
88 changes: 73 additions & 15 deletions packages/service/core/evaluation/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,59 @@ import type { CreateEvaluationParams } from '@fastgpt/global/core/evaluation/typ
import type { ValidationResult } from '@fastgpt/global/core/evaluation/validate';
import { EvaluationErrEnum } from '@fastgpt/global/common/error/code/evaluation';
import { MAX_NAME_LENGTH, MAX_DESCRIPTION_LENGTH } from '@fastgpt/global/core/evaluation/constants';
import { MongoEvalDatasetCollection } from '../dataset/evalDatasetCollectionSchema';
import { Types } from 'mongoose';

export type EvaluationValidationParams = Partial<CreateEvaluationParams>;

export interface EvaluationValidationOptions {
mode?: 'create' | 'update'; // validation mode
teamId?: string; // required for dataset existence validation
}

/**
* Validate if a dataset exists and is accessible by the team
*/
async function validateDatasetExists(
datasetId: string,
teamId?: string
): Promise<ValidationResult> {
// Validate datasetId format
if (!Types.ObjectId.isValid(datasetId)) {
return {
isValid: false,
errors: [
{
code: EvaluationErrEnum.evalDatasetIdRequired,
message: 'Invalid dataset ID format',
field: 'datasetId'
}
]
};
}

// Check if dataset exists
const filter: any = { _id: new Types.ObjectId(datasetId) };
if (teamId) {
filter.teamId = new Types.ObjectId(teamId);
}

const dataset = await MongoEvalDatasetCollection.findOne(filter).lean();

if (!dataset) {
return {
isValid: false,
errors: [
{
code: EvaluationErrEnum.datasetCollectionNotFound,
message: 'Dataset not found or access denied',
field: 'datasetId'
}
]
};
}

return { isValid: true, errors: [] };
}

export async function validateEvaluationParams(
Expand Down Expand Up @@ -118,17 +166,25 @@ export async function validateEvaluationParams(
};
}

if (datasetId !== undefined && !datasetId) {
return {
isValid: false,
errors: [
{
code: EvaluationErrEnum.evalDatasetIdRequired,
message: 'Dataset ID is required',
field: 'datasetId'
}
]
};
if (datasetId !== undefined) {
if (!datasetId) {
return {
isValid: false,
errors: [
{
code: EvaluationErrEnum.evalDatasetIdRequired,
message: 'Dataset ID is required',
field: 'datasetId'
}
]
};
}

// Validate dataset exists and is accessible
const datasetValidation = await validateDatasetExists(datasetId, options?.teamId);
if (!datasetValidation.isValid) {
return datasetValidation;
}
}

if (target !== undefined) {
Expand Down Expand Up @@ -192,13 +248,15 @@ export async function validateEvaluationParams(
}

export async function validateEvaluationParamsForCreate(
params: EvaluationValidationParams
params: EvaluationValidationParams,
teamId?: string
): Promise<ValidationResult> {
return validateEvaluationParams(params, { mode: 'create' });
return validateEvaluationParams(params, { mode: 'create', teamId });
}

export async function validateEvaluationParamsForUpdate(
params: EvaluationValidationParams
params: EvaluationValidationParams,
teamId?: string
): Promise<ValidationResult> {
return validateEvaluationParams(params, { mode: 'update' });
return validateEvaluationParams(params, { mode: 'update', teamId });
}
28 changes: 16 additions & 12 deletions projects/app/src/pages/api/core/evaluation/task/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,24 +21,28 @@ async function handler(
): Promise<CreateEvaluationResponse> {
const { name, description, datasetId, target, evaluators, autoStart } = req.body;

// Validate all evaluation parameters (includes target validation)
const paramValidation = await validateEvaluationParamsForCreate({
name,
description,
datasetId,
target,
evaluators
});
if (!paramValidation.isValid) {
throw ValidationResultUtils.toError(paramValidation);
}

// First perform auth to get teamId
const { teamId, tmbId } = await authEvaluationTaskCreate(target as EvalTarget, {
req,
authApiKey: true,
authToken: true
});

// Now validate all evaluation parameters with teamId (includes target and dataset validation)
const paramValidation = await validateEvaluationParamsForCreate(
{
name,
description,
datasetId,
target,
evaluators
},
teamId
);
if (!paramValidation.isValid) {
throw ValidationResultUtils.toError(paramValidation);
}

// Check evaluation task limit
await checkTeamEvaluationTaskLimit(teamId);

Expand Down
7 changes: 5 additions & 2 deletions projects/app/src/pages/api/core/evaluation/task/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ async function handler(
req: ApiRequestProps<ListEvaluationsRequest>
): Promise<ListEvaluationsResponse> {
const { offset, pageSize } = parsePaginationRequest(req);
const { searchKey } = req.body;
const { searchKey, appName, appId, versionId } = req.body;

const { teamId, tmbId, isOwner, roleList, myGroupMap, myOrgSet } =
await getEvaluationPermissionAggregation({
Expand All @@ -49,7 +49,10 @@ async function handler(
searchKey?.trim(),
accessibleIds,
tmbId,
isOwner
isOwner,
appName?.trim(),
appId?.trim(),
versionId?.trim()
);

const formatEvaluations = result.list
Expand Down
Loading
Loading