initial commit
This commit is contained in:
172
packages/summarizer/scoring.ts
Normal file
172
packages/summarizer/scoring.ts
Normal file
@@ -0,0 +1,172 @@
|
||||
import type { Ampelfarbe, ScoringDimensionDefinition, ScoringFarbe } from "./scoring-model";
|
||||
|
||||
export interface LlmSubcriterionAssessment {
|
||||
id: string;
|
||||
farbe: ScoringFarbe;
|
||||
evidence: string;
|
||||
begruendung: string;
|
||||
confidence: number;
|
||||
missingReason: string;
|
||||
}
|
||||
|
||||
export interface LlmDimensionAssessment {
|
||||
id: string;
|
||||
subcriteria: LlmSubcriterionAssessment[];
|
||||
}
|
||||
|
||||
export interface LlmScoringAssessment {
|
||||
ausschlussgruende: string[];
|
||||
dimensionen: LlmDimensionAssessment[];
|
||||
}
|
||||
|
||||
export interface ScoredSubcriterion extends LlmSubcriterionAssessment {
|
||||
name: string;
|
||||
indikator: string;
|
||||
weight: number;
|
||||
score: number | null;
|
||||
weightedScore: number;
|
||||
}
|
||||
|
||||
export interface ScoringDimension {
|
||||
id: string;
|
||||
name: string;
|
||||
weight: number;
|
||||
farbe: ScoringFarbe;
|
||||
score: number;
|
||||
weightedScore: number;
|
||||
scorableWeight: number;
|
||||
subcriteria: ScoredSubcriterion[];
|
||||
}
|
||||
|
||||
export interface ScoringResult {
|
||||
farbe: Ampelfarbe;
|
||||
gesamtScore: number;
|
||||
unbewertbareKriterien: number;
|
||||
missingDataWarnings: string[];
|
||||
dimensionen: ScoringDimension[];
|
||||
}
|
||||
|
||||
const COLOR_SCORE: Record<ScoringFarbe, number | null> = {
|
||||
gruen: 100,
|
||||
gelb: 50,
|
||||
rot: 0,
|
||||
unbewertbar: null,
|
||||
};
|
||||
|
||||
export function normalizeScoringFarbe(value: unknown): ScoringFarbe {
|
||||
return value === "gruen" || value === "gelb" || value === "rot" || value === "unbewertbar"
|
||||
? value
|
||||
: "unbewertbar";
|
||||
}
|
||||
|
||||
export function scoreToFarbe(score: number): Ampelfarbe {
|
||||
if (score >= 75) return "gruen";
|
||||
return "gelb";
|
||||
}
|
||||
|
||||
export function calculateScoringResult(
|
||||
model: ScoringDimensionDefinition[],
|
||||
assessment: LlmScoringAssessment,
|
||||
): ScoringResult {
|
||||
const assessmentByDimension = new Map(assessment.dimensionen.map((dimension) => [dimension.id, dimension]));
|
||||
let totalWeightedScore = 0;
|
||||
let unbewertbareKriterien = 0;
|
||||
const missingDataWarnings: string[] = [];
|
||||
|
||||
const dimensionen = model.map((dimensionDefinition): ScoringDimension => {
|
||||
const dimensionAssessment = assessmentByDimension.get(dimensionDefinition.id);
|
||||
const assessmentBySubcriterion = new Map(
|
||||
(dimensionAssessment?.subcriteria ?? []).map((subcriterion) => [subcriterion.id, subcriterion]),
|
||||
);
|
||||
|
||||
let achievedScore = 0;
|
||||
let scorableWeight = 0;
|
||||
const subcriteria = dimensionDefinition.subcriteria.map((subcriterionDefinition): ScoredSubcriterion => {
|
||||
const rawAssessment = assessmentBySubcriterion.get(subcriterionDefinition.id);
|
||||
const farbe = normalizeScoringFarbe(rawAssessment?.farbe);
|
||||
const score = COLOR_SCORE[farbe];
|
||||
const confidence = Number.isFinite(rawAssessment?.confidence)
|
||||
? Math.max(0, Math.min(1, Number(rawAssessment?.confidence)))
|
||||
: 0;
|
||||
const missingReason = String(rawAssessment?.missingReason ?? "").trim();
|
||||
const evidence = String(rawAssessment?.evidence ?? "").trim();
|
||||
|
||||
if (score == null) {
|
||||
unbewertbareKriterien += 1;
|
||||
if (missingReason) {
|
||||
missingDataWarnings.push(`${dimensionDefinition.name} / ${subcriterionDefinition.name}: ${missingReason}`);
|
||||
}
|
||||
} else {
|
||||
scorableWeight += subcriterionDefinition.weight;
|
||||
}
|
||||
|
||||
achievedScore += ((score ?? 0) * subcriterionDefinition.weight) / 100;
|
||||
|
||||
return {
|
||||
id: subcriterionDefinition.id,
|
||||
name: subcriterionDefinition.name,
|
||||
indikator: subcriterionDefinition.indikator,
|
||||
farbe,
|
||||
evidence,
|
||||
begruendung: String(rawAssessment?.begruendung ?? "").trim(),
|
||||
confidence,
|
||||
missingReason,
|
||||
weight: subcriterionDefinition.weight,
|
||||
score,
|
||||
weightedScore: 0,
|
||||
};
|
||||
});
|
||||
|
||||
const roundedScorableWeight = Number(scorableWeight.toFixed(2));
|
||||
const roundedDimensionScore = roundedScorableWeight
|
||||
? Number(((achievedScore / roundedScorableWeight) * 100).toFixed(2))
|
||||
: 0;
|
||||
const weightedScore = roundedScorableWeight ? (roundedDimensionScore * dimensionDefinition.weight) / 100 : 0;
|
||||
if (roundedScorableWeight) {
|
||||
totalWeightedScore += weightedScore;
|
||||
}
|
||||
const normalizedSubcriteria = subcriteria.map((subcriterion) => ({
|
||||
...subcriterion,
|
||||
weightedScore:
|
||||
subcriterion.score == null || !roundedScorableWeight
|
||||
? 0
|
||||
: Number(((subcriterion.score * subcriterion.weight) / roundedScorableWeight).toFixed(2)),
|
||||
}));
|
||||
|
||||
return {
|
||||
id: dimensionDefinition.id,
|
||||
name: dimensionDefinition.name,
|
||||
weight: dimensionDefinition.weight,
|
||||
farbe: scoreToFarbe(roundedDimensionScore),
|
||||
score: roundedDimensionScore,
|
||||
weightedScore: Number(weightedScore.toFixed(2)),
|
||||
scorableWeight: roundedScorableWeight,
|
||||
subcriteria: normalizedSubcriteria,
|
||||
};
|
||||
});
|
||||
|
||||
const scorableDimensionWeight = dimensionen
|
||||
.filter((dimension) => dimension.scorableWeight > 0)
|
||||
.reduce((sum, dimension) => sum + dimension.weight, 0);
|
||||
const gesamtScore = scorableDimensionWeight
|
||||
? Number(((totalWeightedScore / scorableDimensionWeight) * 100).toFixed(2))
|
||||
: 0;
|
||||
const hasWeakDimension = dimensionen.some((dimension) => dimension.scorableWeight > 0 && dimension.score < 35);
|
||||
const rawFarbe = scoreToFarbe(gesamtScore);
|
||||
const farbe = rawFarbe === "gruen" && hasWeakDimension ? "gelb" : rawFarbe;
|
||||
|
||||
return {
|
||||
farbe,
|
||||
gesamtScore,
|
||||
unbewertbareKriterien,
|
||||
missingDataWarnings,
|
||||
dimensionen,
|
||||
};
|
||||
}
|
||||
|
||||
export function deriveTrafficLight(
|
||||
scoring: ScoringResult,
|
||||
ausschlussgruende: string[],
|
||||
): Ampelfarbe {
|
||||
return ausschlussgruende.length ? "rot" : scoring.farbe;
|
||||
}
|
||||
Reference in New Issue
Block a user