@lobehub/chat
Version:
Lobe Chat - an open-source, high-performance chatbot framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.
299 lines (254 loc) • 9.24 kB
text/typescript
/* eslint-disable sort-keys-fix/sort-keys-fix */
import { TRPCError } from '@trpc/server';
import dayjs from 'dayjs';
import JSONL from 'jsonl-parse-stringify';
import pMap from 'p-map';
import { z } from 'zod';
import { DEFAULT_EMBEDDING_MODEL, DEFAULT_MODEL } from '@/const/settings';
import { FileModel } from '@/database/models/file';
import {
EvalDatasetModel,
EvalDatasetRecordModel,
EvalEvaluationModel,
EvaluationRecordModel,
} from '@/database/server/models/ragEval';
import { authedProcedure, router } from '@/libs/trpc/lambda';
import { keyVaults, serverDatabase } from '@/libs/trpc/lambda/middleware';
import { createAsyncServerClient } from '@/server/routers/async';
import { FileService } from '@/server/services/file';
import {
EvalDatasetRecord,
EvalEvaluationStatus,
InsertEvalDatasetRecord,
RAGEvalDataSetItem,
insertEvalDatasetRecordSchema,
insertEvalDatasetsSchema,
insertEvalEvaluationSchema,
} from '@/types/eval';
const ragEvalProcedure = authedProcedure
.use(serverDatabase)
.use(keyVaults)
.use(async (opts) => {
const { ctx } = opts;
return opts.next({
ctx: {
datasetModel: new EvalDatasetModel(ctx.userId),
fileModel: new FileModel(ctx.serverDB, ctx.userId),
datasetRecordModel: new EvalDatasetRecordModel(ctx.userId),
evaluationModel: new EvalEvaluationModel(ctx.userId),
evaluationRecordModel: new EvaluationRecordModel(ctx.userId),
fileService: new FileService(ctx.serverDB, ctx.userId),
},
});
});
export const ragEvalRouter = router({
createDataset: ragEvalProcedure
.input(
z.object({
description: z.string().optional(),
knowledgeBaseId: z.string(),
name: z.string(),
}),
)
.mutation(async ({ input, ctx }) => {
const data = await ctx.datasetModel.create({
description: input.description,
knowledgeBaseId: input.knowledgeBaseId,
name: input.name,
});
return data?.id;
}),
getDatasets: ragEvalProcedure
.input(z.object({ knowledgeBaseId: z.string() }))
.query(async ({ ctx, input }): Promise<RAGEvalDataSetItem[]> => {
return ctx.datasetModel.query(input.knowledgeBaseId);
}),
removeDataset: ragEvalProcedure
.input(z.object({ id: z.number() }))
.mutation(async ({ input, ctx }) => {
return ctx.datasetModel.delete(input.id);
}),
updateDataset: ragEvalProcedure
.input(
z.object({
id: z.number(),
value: insertEvalDatasetsSchema.partial(),
}),
)
.mutation(async ({ input, ctx }) => {
return ctx.datasetModel.update(input.id, input.value);
}),
// Dataset Item operations
createDatasetRecords: ragEvalProcedure
.input(
z.object({
datasetId: z.number(),
question: z.string(),
ideal: z.string().optional(),
referenceFiles: z.array(z.string()).optional(),
metadata: z.record(z.unknown()).optional(),
}),
)
.mutation(async ({ input, ctx }) => {
const data = await ctx.datasetRecordModel.create(input);
return data?.id;
}),
getDatasetRecords: ragEvalProcedure
.input(z.object({ datasetId: z.number() }))
.query(async ({ ctx, input }): Promise<EvalDatasetRecord[]> => {
return ctx.datasetRecordModel.query(input.datasetId);
}),
removeDatasetRecords: ragEvalProcedure
.input(z.object({ id: z.number() }))
.mutation(async ({ input, ctx }) => {
return ctx.datasetRecordModel.delete(input.id);
}),
updateDatasetRecords: ragEvalProcedure
.input(
z.object({
id: z.number(),
value: z
.object({
question: z.string(),
ideal: z.string(),
referenceFiles: z.array(z.string()),
metadata: z.record(z.unknown()),
})
.partial(),
}),
)
.mutation(async ({ input, ctx }) => {
return ctx.datasetRecordModel.update(input.id, input.value);
}),
importDatasetRecords: ragEvalProcedure
.input(
z.object({
datasetId: z.number(),
pathname: z.string(),
}),
)
.mutation(async ({ input, ctx }) => {
const dataStr = await ctx.fileService.getFileContent(input.pathname);
const items = JSONL.parse<InsertEvalDatasetRecord>(dataStr);
insertEvalDatasetRecordSchema.array().parse(items);
const data = await Promise.all(
items.map(async ({ referenceFiles, question, ideal }) => {
const files = typeof referenceFiles === 'string' ? [referenceFiles] : referenceFiles;
let fileIds: string[] | undefined = undefined;
if (files) {
const items = await ctx.fileModel.findByNames(files);
fileIds = items.map((item) => item.id);
}
return {
question,
ideal,
referenceFiles: fileIds,
datasetId: input.datasetId,
};
}),
);
return ctx.datasetRecordModel.batchCreate(data);
}),
// Evaluation operations
startEvaluationTask: ragEvalProcedure
.input(z.object({ id: z.number() }))
.mutation(async ({ input, ctx }) => {
// Start evaluation task
const evaluation = await ctx.evaluationModel.findById(input.id);
if (!evaluation) {
throw new TRPCError({ code: 'BAD_REQUEST', message: 'Evaluation not found' });
}
// create evaluation records by dataset records
const datasetRecords = await ctx.datasetRecordModel.findByDatasetId(evaluation.datasetId);
if (datasetRecords.length === 0) {
throw new TRPCError({ code: 'BAD_REQUEST', message: 'Dataset record is empty' });
}
const evalRecords = await ctx.evaluationRecordModel.batchCreate(
datasetRecords.map((record) => ({
evaluationId: input.id,
datasetRecordId: record.id,
question: record.question!,
ideal: record.ideal,
status: EvalEvaluationStatus.Pending,
embeddingModel: DEFAULT_EMBEDDING_MODEL,
languageModel: DEFAULT_MODEL,
})),
);
const asyncCaller = await createAsyncServerClient(ctx.userId, ctx.jwtPayload);
await ctx.evaluationModel.update(input.id, { status: EvalEvaluationStatus.Processing });
try {
await pMap(
evalRecords,
async (record) => {
asyncCaller.ragEval.runRecordEvaluation
.mutate({ evalRecordId: record.id })
.catch(async (e) => {
await ctx.evaluationModel.update(input.id, { status: EvalEvaluationStatus.Error });
throw new TRPCError({
code: 'BAD_GATEWAY',
message: `[ASYNC_TASK] Failed to start evaluation task: ${e.message}`,
});
});
},
{
concurrency: 30,
},
);
return { success: true };
} catch (e) {
console.error('[startEvaluationTask]:', e);
await ctx.evaluationModel.update(input.id, { status: EvalEvaluationStatus.Error });
return { success: false };
}
}),
checkEvaluationStatus: ragEvalProcedure
.input(z.object({ id: z.number() }))
.query(async ({ input, ctx }) => {
const evaluation = await ctx.evaluationModel.findById(input.id);
if (!evaluation) {
throw new TRPCError({ code: 'BAD_REQUEST', message: 'Evaluation not found' });
}
const records = await ctx.evaluationRecordModel.findByEvaluationId(input.id);
const isSuccess = records.every((record) => record.status === EvalEvaluationStatus.Success);
if (isSuccess) {
// 将结果上传到 S3
const evalRecords = records.map((record) => ({
question: record.question,
context: record.context,
answer: record.answer,
ground_truth: record.ideal,
}));
const date = dayjs().format('YYYY-MM-DD-HH-mm');
const filename = `${date}-eval_${evaluation.id}-${evaluation.name}.jsonl`;
const path = `rag_eval_records/${filename}`;
await ctx.fileService.uploadContent(path, JSONL.stringify(evalRecords));
// 保存数据
await ctx.evaluationModel.update(input.id, {
status: EvalEvaluationStatus.Success,
evalRecordsUrl: await ctx.fileService.getFullFileUrl(path),
});
}
return { success: isSuccess };
}),
createEvaluation: ragEvalProcedure
.input(insertEvalEvaluationSchema)
.mutation(async ({ input, ctx }) => {
const data = await ctx.evaluationModel.create({
description: input.description,
knowledgeBaseId: input.knowledgeBaseId,
datasetId: input.datasetId,
name: input.name,
});
return data?.id;
}),
removeEvaluation: ragEvalProcedure
.input(z.object({ id: z.number() }))
.mutation(async ({ input, ctx }) => {
return ctx.evaluationModel.delete(input.id);
}),
getEvaluationList: ragEvalProcedure
.input(z.object({ knowledgeBaseId: z.string() }))
.query(async ({ ctx, input }) => {
return ctx.evaluationModel.queryByKnowledgeBaseId(input.knowledgeBaseId);
}),
});