Files
dify/knowledge-fs/apps/admin/lib/evaluation-dashboard.ts
Jyong 4ee43b8afc chore: migrate knowledge-fs source tree
Import the committed KnowledgeFS snapshot dc4072ee302317145612087ce7440851dc329fd0 under knowledge-fs/ without its Git history, local IDE settings, or build artifacts.
2026-07-20 04:54:20 -04:00

187 lines
5.6 KiB
TypeScript

export interface EvaluationDashboardRun {
readonly citationAccuracy: number;
readonly costUsd: number;
readonly failedQuestions: number;
readonly faithfulnessScore: number;
readonly id: string;
readonly latencyMs: number;
readonly recallAtK: number;
readonly runAt: string;
readonly totalQuestions: number;
}
export interface EvaluationDashboardTrendPoint {
readonly label: string;
readonly runId: string;
readonly value: number;
readonly valueLabel: string;
}
export interface EvaluationDashboardSummary {
readonly citationTrend: readonly EvaluationDashboardTrendPoint[];
readonly costLatency: {
readonly averageCostLabel: string;
readonly averageLatencyLabel: string;
readonly latestCostLabel: string;
readonly latestLatencyLabel: string;
};
readonly latest: {
readonly citationAccuracyLabel: string;
readonly faithfulnessLabel: string;
readonly passRateLabel: string;
readonly recallLabel: string;
readonly runId: string;
};
readonly recallTrend: readonly EvaluationDashboardTrendPoint[];
}
export interface EvaluationDashboardSummaryInput {
readonly maxRuns?: number | undefined;
readonly maxTrendPoints?: number | undefined;
readonly runs: readonly EvaluationDashboardRun[];
}
const defaultMaxRuns = 20;
const defaultMaxTrendPoints = 12;
export function createEvaluationDashboardSummary({
maxRuns = defaultMaxRuns,
maxTrendPoints = defaultMaxTrendPoints,
runs,
}: EvaluationDashboardSummaryInput): EvaluationDashboardSummary {
validatePositiveInteger(maxRuns, "Evaluation dashboard maxRuns");
validatePositiveInteger(maxTrendPoints, "Evaluation dashboard maxTrendPoints");
if (runs.length > maxRuns) {
throw new Error(`Evaluation dashboard runs exceeds maxRuns=${maxRuns}`);
}
const sortedRuns = [...runs].sort((left, right) => left.runAt.localeCompare(right.runAt));
for (const run of sortedRuns) {
validateRun(run);
}
const latest = sortedRuns.at(-1) ?? emptyRun();
const trendRuns = sortedRuns.slice(-maxTrendPoints);
const averageCost =
sortedRuns.length === 0
? 0
: sortedRuns.reduce((total, run) => total + run.costUsd, 0) / sortedRuns.length;
const averageLatency =
sortedRuns.length === 0
? 0
: sortedRuns.reduce((total, run) => total + run.latencyMs, 0) / sortedRuns.length;
return {
citationTrend: trendRuns.map((run) => trendPoint(run, run.citationAccuracy)),
costLatency: {
averageCostLabel: formatUsd(averageCost),
averageLatencyLabel: formatMs(averageLatency),
latestCostLabel: formatUsd(latest.costUsd),
latestLatencyLabel: formatMs(latest.latencyMs),
},
latest: {
citationAccuracyLabel: formatPercent(latest.citationAccuracy),
faithfulnessLabel: formatPercent(latest.faithfulnessScore),
passRateLabel: formatPercent(passRate(latest)),
recallLabel: formatPercent(latest.recallAtK),
runId: latest.id,
},
recallTrend: trendRuns.map((run) => trendPoint(run, run.recallAtK)),
};
}
function validateRun(run: EvaluationDashboardRun): void {
if (!run.id.trim()) {
throw new Error("Evaluation dashboard run id is required");
}
if (Number.isNaN(Date.parse(run.runAt))) {
throw new Error("Evaluation dashboard runAt must be a valid timestamp");
}
validateZeroToOne(run.recallAtK, "Evaluation dashboard recallAtK");
validateZeroToOne(run.citationAccuracy, "Evaluation dashboard citationAccuracy");
validateZeroToOne(run.faithfulnessScore, "Evaluation dashboard faithfulnessScore");
if (!Number.isInteger(run.totalQuestions) || run.totalQuestions < 1) {
throw new Error("Evaluation dashboard totalQuestions must be at least 1");
}
if (!Number.isInteger(run.failedQuestions) || run.failedQuestions < 0) {
throw new Error("Evaluation dashboard failedQuestions must be non-negative");
}
if (run.failedQuestions > run.totalQuestions) {
throw new Error("Evaluation dashboard failedQuestions cannot exceed totalQuestions");
}
if (!Number.isFinite(run.costUsd) || run.costUsd < 0) {
throw new Error("Evaluation dashboard costUsd must be non-negative");
}
if (!Number.isFinite(run.latencyMs) || run.latencyMs < 0) {
throw new Error("Evaluation dashboard latencyMs must be non-negative");
}
}
function validatePositiveInteger(value: number, label: string): void {
if (!Number.isInteger(value) || value < 1) {
throw new Error(`${label} must be at least 1`);
}
}
function validateZeroToOne(value: number, label: string): void {
if (!Number.isFinite(value) || value < 0 || value > 1) {
throw new Error(`${label} must be between 0 and 1`);
}
}
function trendPoint(run: EvaluationDashboardRun, value: number): EvaluationDashboardTrendPoint {
return {
label: formatHourMinute(run.runAt),
runId: run.id,
value,
valueLabel: formatPercent(value),
};
}
function passRate(run: EvaluationDashboardRun): number {
return (run.totalQuestions - run.failedQuestions) / run.totalQuestions;
}
function formatPercent(value: number): string {
return `${Math.round(value * 100)}%`;
}
function formatUsd(value: number): string {
return `$${value.toFixed(2)}`;
}
function formatMs(value: number): string {
return `${Math.round(value).toLocaleString("en-US")} ms`;
}
function formatHourMinute(timestamp: string): string {
const date = new Date(timestamp);
return `${String(date.getUTCHours()).padStart(2, "0")}:${String(date.getUTCMinutes()).padStart(
2,
"0",
)}`;
}
function emptyRun(): EvaluationDashboardRun {
return {
citationAccuracy: 0,
costUsd: 0,
failedQuestions: 0,
faithfulnessScore: 0,
id: "",
latencyMs: 0,
recallAtK: 0,
runAt: "1970-01-01T00:00:00.000Z",
totalQuestions: 1,
};
}