Anrede: last name only, strip first name (keep titles/particles)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-07-03 14:18:02 +02:00
co-authored by Claude
parent 86f94ce642
commit e914dee855
+29 -2
View File
@@ -220,7 +220,11 @@ async function generateTailoredTexts({ job, basisDokumente, settings, llmNotizen
`(nur falls aus Stellenanzeige oder Notizen bekannt, sonst leer); ort = "PLZ Ort"; ` + `(nur falls aus Stellenanzeige oder Notizen bekannt, sonst leer); ort = "PLZ Ort"; ` +
`ansprechpartner = konkrete Kontaktperson (nur falls bekannt, sonst leer). Nutze eine in den ` + `ansprechpartner = konkrete Kontaktperson (nur falls bekannt, sonst leer). Nutze eine in den ` +
`Notizen angegebene vollständige Firmenanschrift und einen genannten Ansprechpartner. ` + `Notizen angegebene vollständige Firmenanschrift und einen genannten Ansprechpartner. ` +
`Ist ein Ansprechpartner bekannt, passe auch die Anrede an (z. B. "Sehr geehrte Frau …,").\n` + `Ist ein Ansprechpartner bekannt, passe die Anrede an - aber NUR als "Sehr geehrte Frau <Nachname>," ` +
`bzw. "Sehr geehrter Herr <Nachname>," mit ausschließlichem NACHNAMEN, niemals mit Vorname oder ` +
`vollem Namen (also "Sehr geehrte Frau Müller," nicht "Sehr geehrte Frau Anna Müller,"). Steht in ` +
`den Unterlagen ein voller Name (z. B. "Anna Schmidt"), verwende in der Anrede nur den Nachnamen ` +
`("Frau Schmidt"). Der ansprechpartner-Wert im JSON darf der volle Name sein (für "z. Hd. …").\n` +
`- anschreiben.absaetze: 34 kurze, überzeugende Absätze (kein Adressblock, kein Datum, ` + `- anschreiben.absaetze: 34 kurze, überzeugende Absätze (kein Adressblock, kein Datum, ` +
`keine Grußformel hier). Sachlich und positiv, in Aktiv-Sätzen, ohne Floskeln und ` + `keine Grußformel hier). Sachlich und positiv, in Aktiv-Sätzen, ohne Floskeln und ` +
`Weichspüler ("Ich würde gerne …"). KEINE Gehaltsvorstellungen, KEINE Kündigungsgründe, ` + `Weichspüler ("Ich würde gerne …"). KEINE Gehaltsvorstellungen, KEINE Kündigungsgründe, ` +
@@ -400,6 +404,29 @@ function fixParagraphCasing(anrede, absaetze) {
return out; return out;
} }
// Defensive: the salutation must be "Sehr geehrte Frau <Nachname>" /
// "Sehr geehrter Herr <Nachname>" with ONLY the last name — never a first
// name or full name. Strip any leading given names the model may have added,
// but keep academic titles (Dr., Prof., …) and name particles (van, von, …).
const SAL_TITLES = new Set(['dr', 'prof', 'dipl', 'ing', 'rer', 'nat', 'mag', 'phd', 'habil', 'dr.habil']);
const SAL_PARTICLES = new Set(['van', 'de', 'von', 'zu', 'der', 'den', 'ten', 'da', 'di', 'del', 'della', 'la', 'le', 'vom', 'zur', 'mc', 'mac']);
function cleanAnrede(anrede) {
return String(anrede).replace(
/^(Sehr geehrte\s+Frau|Sehr geehrter\s+Herr)\s+(.+?)(,?\s*)$/i,
(m, sal, name, tail) => {
const words = String(name).trim().split(/\s+/);
if (words.length <= 1) return m; // already a single name
const kept = words.filter((w, i) => {
if (i === words.length - 1) return true; // last word = surname
const lw = w.toLowerCase().replace(/[.­]/g, '');
return SAL_TITLES.has(lw) || SAL_PARTICLES.has(lw); // keep titles / particles
});
if (kept.length === words.length) return m; // nothing to strip
return `${sal} ${kept.join(' ')}${tail}`;
},
);
}
// Defensive: never let an HR red-flag phrase reach the PDF, even if the model // Defensive: never let an HR red-flag phrase reach the PDF, even if the model
// ignores the instruction. // ignores the instruction.
const RED_FLAG = /(nicht\s*beendet|abgebrochen|abbruch|ohne\s*abschluss|arbeits(los|suchend)|gek(ü|ue)ndigt|entlassen)/i; const RED_FLAG = /(nicht\s*beendet|abgebrochen|abbruch|ohne\s*abschluss|arbeits(los|suchend)|gek(ü|ue)ndigt|entlassen)/i;
@@ -441,7 +468,7 @@ function normalizeResult(parsed) {
fuehrerschein: str(pick(k, ['fuehrerschein', 'führerschein', 'driving_license', 'license', 'licence', 'fuehrerscheinklasse'])), fuehrerschein: str(pick(k, ['fuehrerschein', 'führerschein', 'driving_license', 'license', 'licence', 'fuehrerscheinklasse'])),
}, },
anschreiben: (() => { anschreiben: (() => {
const anrede = str(pick(a, ['anrede', 'salutation'])); const anrede = cleanAnrede(str(pick(a, ['anrede', 'salutation'])));
const absaetze = pickArray(a, ['absaetze', 'absätze', 'paragraphs', 'text', 'inhalt']).map(str).filter(Boolean); const absaetze = pickArray(a, ['absaetze', 'absätze', 'paragraphs', 'text', 'inhalt']).map(str).filter(Boolean);
const e = a.empfaenger || a.recipient || a.empfänger || {}; const e = a.empfaenger || a.recipient || a.empfänger || {};
return { return {