KI-Feinschliff, Volltextsuche über E-Mails, Prompt für die Antwort-Mail
Drei Erweiterungen, die sich server.js und lib/documents.js teilen und deshalb zusammen liegen: Jobsuche: Der Feinschliff (Zusatzbegriffe/Ausschluesse) laesst sich per KI aus den eigenen Basis-Unterlagen vorschlagen. Der Rollen-Prompt ist wie die uebrigen unter Vorlagen editierbar; gespeichert wird erst auf Klick. Suche: Startseite und KI-Chat durchsuchen jetzt per SQLite-FTS5 auch die E-Mail-Korrespondenz (Absender, Betreff, Text), Notizen und Stellenbeschreibung - ein Name aus einer Mail findet die Bewerbung. Ranking per bm25, Fundstellen mit Snippet, Feldfilter (firma:, von:), Phrasen, Ausschluesse, Umlaut-Varianten (mueller/muller/mueller). Der Index wird von SQL-Triggern gepflegt, weil Bewerbungen und Mails aus UI, REST-API, IMAP und KI-Import geschrieben werden. E-Mail: Vor dem Generieren laesst sich vorgeben, was in der Antwort stehen soll; die Vorgabe geht als verbindliche Anweisung in den Prompt. Steht schon ein Entwurf im Feld, ueberarbeitet die KI ihn, statt neu anzufangen. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+123
-4
@@ -1990,13 +1990,48 @@ const REPLY_SCHEMA = {
|
||||
|
||||
// Draft a reply to a recruiter/company e-mail in the context of an application.
|
||||
// `incoming` = { from, subject, text }; `job` = { firma, stelle };
|
||||
// `settings` = { name, ... }. `hinweise` = optional free-text steering.
|
||||
// `settings` = { name, ... }.
|
||||
// `typ` = 'antwort' (default) for a normal reply, or 'absage' for a polite, short
|
||||
// rejection because the applicant accepted another position.
|
||||
async function generateEmailReply({ incoming, job = {}, settings = {}, hinweise = '', typ = 'antwort', prompts = null }) {
|
||||
//
|
||||
// `hinweise` is what the user typed into the prompt field above the reply form
|
||||
// ("Termin am Dienstag zusagen, nach dem Gehaltsrahmen fragen"). It is the point
|
||||
// of the whole call: without it the model can only produce a generic reply, so it
|
||||
// is passed as a binding instruction, not as background colour.
|
||||
//
|
||||
// `entwurf` is the draft currently in the reply box. When present, the model
|
||||
// *revises* that text along the new instructions instead of starting over — that
|
||||
// is how the field is actually used ("gleiche Antwort, aber kürzer und förmlicher"),
|
||||
// and re-generating from scratch would throw the user's own edits away.
|
||||
async function generateEmailReply({
|
||||
incoming, job = {}, settings = {}, hinweise = '', entwurf = '', typ = 'antwort', prompts = null,
|
||||
}) {
|
||||
const name = (settings && settings.name) || 'der Bewerber';
|
||||
const isAbsage = typ === 'absage';
|
||||
|
||||
const anweisung = String(hinweise || '').trim().slice(0, 2000);
|
||||
const bisher = String(entwurf || '').trim().slice(0, 6000);
|
||||
|
||||
// Beide Blöcke stehen NACH der eingegangenen Mail und VOR der Aufgabe: das
|
||||
// Zuletztgelesene wiegt beim Modell am schwersten, und genau das soll die
|
||||
// Anweisung des Nutzers sein.
|
||||
const anweisungsBlock = anweisung
|
||||
? `# Anweisungen von ${name} für diese Antwort (verbindlich)\n` +
|
||||
`${anweisung}\n\n` +
|
||||
`Setze diese Anweisungen vollständig um - sie bestimmen den Inhalt der E-Mail und haben ` +
|
||||
`Vorrang vor allgemeinen Höflichkeitsformeln. Was dort nicht steht und sich nicht aus der ` +
|
||||
`eingegangenen Nachricht ergibt, gehört nicht in die Antwort: keine zusätzlichen Zusagen, ` +
|
||||
`Termine oder Angaben dazuerfinden. Ist eine Anweisung unklar oder fehlt eine konkrete ` +
|
||||
`Angabe, setze einen Platzhalter in eckigen Klammern, statt etwas anzunehmen.\n\n`
|
||||
: '';
|
||||
|
||||
const entwurfsBlock = bisher
|
||||
? `# Bisheriger Entwurf\n${bisher}\n\n` +
|
||||
`Überarbeite DIESEN Entwurf gemäß den Anweisungen oben. Behalte bei, was passt - schreibe ` +
|
||||
`nicht ohne Not alles neu, und verwirf keine inhaltlichen Punkte des Entwurfs, sofern die ` +
|
||||
`Anweisungen das nicht verlangen.\n\n`
|
||||
: '';
|
||||
|
||||
// Role prompt (per type) plus the shared style rules — both editable; the task
|
||||
// description below stays in code because it defines the expected JSON.
|
||||
const commonRules = promptStore.get(prompts, 'email_stil');
|
||||
@@ -2016,8 +2051,10 @@ async function generateEmailReply({ incoming, job = {}, settings = {}, hinweise
|
||||
`Von: ${incoming.from || '-'}\n` +
|
||||
`Betreff: ${incoming.subject || '-'}\n\n` +
|
||||
`${incoming.text || ''}\n\n` +
|
||||
(hinweise && hinweise.trim() ? `# Hinweise für die Absage (aktiv berücksichtigen)\n${hinweise.trim()}\n\n` : '') +
|
||||
anweisungsBlock +
|
||||
entwurfsBlock +
|
||||
`# Aufgabe\n` +
|
||||
(bisher ? 'Gib den überarbeiteten Entwurf als vollständige E-Mail zurück. ' : '') +
|
||||
`Formuliere eine kurze, höfliche Absage-E-Mail: Du hast eine andere Stelle angenommen ` +
|
||||
`und ziehst dich aus diesem Prozess zurück. Struktur des Feldes "text": Anrede (an den ` +
|
||||
`konkreten Absender, falls Name erkennbar, sonst "Sehr geehrte Damen und Herren,"), ein ` +
|
||||
@@ -2038,8 +2075,10 @@ async function generateEmailReply({ incoming, job = {}, settings = {}, hinweise
|
||||
`Von: ${incoming.from || '-'}\n` +
|
||||
`Betreff: ${incoming.subject || '-'}\n\n` +
|
||||
`${incoming.text || ''}\n\n` +
|
||||
(hinweise && hinweise.trim() ? `# Hinweise für die Antwort (aktiv berücksichtigen)\n${hinweise.trim()}\n\n` : '') +
|
||||
anweisungsBlock +
|
||||
entwurfsBlock +
|
||||
`# Aufgabe\n` +
|
||||
(bisher ? 'Gib den überarbeiteten Entwurf als vollständige E-Mail zurück. ' : '') +
|
||||
`Formuliere eine passende Antwort-E-Mail. Struktur des Feldes "text": Anrede (an den ` +
|
||||
`konkreten Absender, falls Name erkennbar, sonst "Sehr geehrte Damen und Herren,"), ` +
|
||||
`2-4 kurze Absätze, Grußformel "Mit freundlichen Grüßen" und in der letzten Zeile der ` +
|
||||
@@ -2057,6 +2096,85 @@ async function generateEmailReply({ incoming, job = {}, settings = {}, hinweise
|
||||
};
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// AI job-search guard rails ("Feinschliff")
|
||||
// ===========================================================================
|
||||
|
||||
const FEINSCHLIFF_SCHEMA = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
zusatz_begriffe: { type: 'array', items: { type: 'string' } },
|
||||
ausschluesse: { type: 'array', items: { type: 'string' } },
|
||||
},
|
||||
required: ['zusatz_begriffe', 'ausschluesse'],
|
||||
};
|
||||
|
||||
// How many terms per field we keep. The two lists end up verbatim in the search
|
||||
// agent's prompt, so a long one dilutes the search instead of sharpening it.
|
||||
const FEINSCHLIFF_MAX = 8;
|
||||
|
||||
// Suggest the two guard-rail lists of a Suchprofil ("Zusätzlich berücksichtigen"
|
||||
// / "Ausschließen") from the applicant's own base documents. `unterlagen` is the
|
||||
// concatenated text of their Vorlagen (Lebenslauf, Kurzprofil, ...), `profil` the
|
||||
// current Suchprofil (modus + cities give the model the search's frame).
|
||||
// Returns { zusatz_begriffe, ausschluesse } as comma-separated strings — exactly
|
||||
// the shape the two textareas and the DB hold.
|
||||
async function generateFeinschliff({ unterlagen, profil = {}, prompts = null }) {
|
||||
const text = String(unterlagen || '').trim();
|
||||
if (!text) {
|
||||
throw new Error('Keine Basis-Unterlagen hinterlegt - ohne sie kann die KI nichts ableiten.');
|
||||
}
|
||||
|
||||
const modusText = {
|
||||
regional: 'regional (nur Stellen mit Arbeitsort in den genannten Städten)',
|
||||
remote: '100 % Remote (deutschlandweit, ortsunabhängig)',
|
||||
beides: 'regional in den genannten Städten und zusätzlich 100 % Remote deutschlandweit',
|
||||
}[profil.modus] || 'regional';
|
||||
const staedte = (profil.staedte || []).join(', ');
|
||||
|
||||
const system = promptStore.get(prompts, 'jobsuche_feinschliff');
|
||||
const user =
|
||||
`# Basis-Unterlagen des Bewerbers\n${text.slice(0, 6000)}\n\n` +
|
||||
`# Rahmen der Suche\n` +
|
||||
`Suchmodus: ${modusText}\n` +
|
||||
(staedte ? `Städte: ${staedte}\n` : '') +
|
||||
`\n# Aufgabe\n` +
|
||||
`Schlage die beiden Leitplanken der Stellensuche vor:\n` +
|
||||
`- "zusatz_begriffe": Begriffe, die die Suche zusätzlich berücksichtigen soll - Technologien, ` +
|
||||
`Branchen, Arbeitsumfelder oder Arbeitszeitmodelle, die aus den Unterlagen belegbar sind ` +
|
||||
`(z. B. "Rechenzentrum", "Systemhaus"). KEINE Rollen-/Stellenbezeichnungen.\n` +
|
||||
`- "ausschluesse": Kriterien, bei denen ein Treffer verworfen werden soll (z. B. Anstellungsformen ` +
|
||||
`oder Rollen, die erkennbar nicht zum Profil passen).\n` +
|
||||
`Höchstens ${FEINSCHLIFF_MAX} Einträge je Liste, nach Wichtigkeit sortiert. Jeder Eintrag ist ein ` +
|
||||
`kurzer Begriff (1-4 Wörter), kein Satz. Verwende ausschließlich den einfachen Bindestrich "-". ` +
|
||||
`Antworte AUSSCHLIESSLICH mit dem JSON-Objekt, ohne Markdown, ohne Code-Fences.`;
|
||||
|
||||
const parsed = await ollamaChatJSON({ system, user, schema: FEINSCHLIFF_SCHEMA, temperature: 0.4 });
|
||||
|
||||
// The model honours the schema keys only loosely; accept the usual synonyms and
|
||||
// flatten each list into the comma-separated free text the profile stores. A
|
||||
// model that answers with one comma-separated string instead of an array is
|
||||
// split back apart rather than dropped.
|
||||
const liste = (keys) => {
|
||||
const roheListe = pickArray(parsed, keys);
|
||||
const werte = roheListe.length ? roheListe : String(pick(parsed, keys) || '').split(/[,;\n]/);
|
||||
const out = [];
|
||||
for (const roh of werte) {
|
||||
const v = str(roh).replace(/^[-•*\s]+/, '').replace(/[,;]+$/, '').trim();
|
||||
if (!v) continue;
|
||||
if (out.some((x) => x.toLowerCase() === v.toLowerCase())) continue;
|
||||
out.push(v);
|
||||
if (out.length >= FEINSCHLIFF_MAX) break;
|
||||
}
|
||||
return out.join(', ');
|
||||
};
|
||||
|
||||
return {
|
||||
zusatz_begriffe: liste(['zusatz_begriffe', 'zusatzbegriffe', 'zusatz', 'begriffe', 'einschluesse']),
|
||||
ausschluesse: liste(['ausschluesse', 'ausschlüsse', 'ausschlusskriterien', 'exclude', 'ausschluss']),
|
||||
};
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// Design preview
|
||||
// ===========================================================================
|
||||
@@ -2112,6 +2230,7 @@ function renderDesignVorschau({ settings = {}, signatur = null, bewerbungsfoto =
|
||||
module.exports = {
|
||||
generateApplicationDocuments,
|
||||
generateEmailReply,
|
||||
generateFeinschliff,
|
||||
generateTailoredTexts,
|
||||
renderLebenslaufPdf,
|
||||
renderAnschreibenPdf,
|
||||
|
||||
Reference in New Issue
Block a user