diff --git a/lib/documents.js b/lib/documents.js index a82fc1f..a07af68 100644 --- a/lib/documents.js +++ b/lib/documents.js @@ -220,7 +220,11 @@ async function generateTailoredTexts({ job, basisDokumente, settings, llmNotizen `(nur falls aus Stellenanzeige oder Notizen bekannt, sonst leer); ort = "PLZ Ort"; ` + `ansprechpartner = konkrete Kontaktperson (nur falls bekannt, sonst leer). Nutze eine in den ` + `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 ," ` + + `bzw. "Sehr geehrter Herr ," 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: 3–4 kurze, überzeugende Absätze (kein Adressblock, kein Datum, ` + `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, ` + @@ -400,6 +404,29 @@ function fixParagraphCasing(anrede, absaetze) { return out; } +// Defensive: the salutation must be "Sehr geehrte Frau " / +// "Sehr geehrter Herr " 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 // ignores the instruction. 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'])), }, 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 e = a.empfaenger || a.recipient || a.empfänger || {}; return {