KI-Prompts unter Vorlagen editierbar statt hardcoded
Rolle, Tonfall und Regeln der KI (Unterlagen, Chat, E-Mail-Antwort, E-Mail-Absage, gemeinsame Stilregeln) lagen fest im Code. Sie liegen jetzt als Defaults in lib/prompts.js und lassen sich auf der Vorlagen-Seite je Prompt anpassen und wieder zuruecksetzen. Nur die System-Prompts sind editierbar. Die User-Prompts tragen das JSON-Skeleton, gegen das die Antwort geparst wird - ein Tippfehler dort wuerde die Generierung lahmlegen, also bleiben sie im Code. Gespeichert wird nur, was abweicht: ein Override ist eine Zeile in der neuen Tabelle `prompts`, "Zuruecksetzen" loescht sie. Damit bleiben die Defaults im Code die Wahrheit und wandern bei Updates automatisch mit. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+5
-11
@@ -10,6 +10,8 @@
|
||||
// Unlike lib/documents.js, no JSON output schema is enforced — the model just
|
||||
// replies as text, token by token, so the UI can render progressively.
|
||||
|
||||
const promptStore = require('./prompts');
|
||||
|
||||
const OLLAMA_HOST = (process.env.OLLAMA_HOST || 'https://ollama.com').replace(/\/+$/, '');
|
||||
const OLLAMA_MODEL = process.env.OLLAMA_MODEL || 'gpt-oss:120b';
|
||||
const OLLAMA_TIMEOUT_MS = Number(process.env.OLLAMA_TIMEOUT_MS || 300000);
|
||||
@@ -184,17 +186,9 @@ function buildContextPrompt(ctx) {
|
||||
const name = (ctx && ctx.settings && ctx.settings.name) || 'der Bewerber';
|
||||
const heute = (ctx && ctx.heute) || '';
|
||||
const parts = [];
|
||||
parts.push(
|
||||
'Du bist ein hilfreicher, deutschsprachiger Bewerbungs-Assistent für ' + name + '. ' +
|
||||
'Du beantwortest Fragen zu laufenden Bewerbungen, hilfst beim Formulieren von ' +
|
||||
'Antworten, beim Vorbereiten auf Gespräche und beim Überblick über den Status. ' +
|
||||
'Antworte natürlich, knapp und auf Deutsch im lateinischen Alphabet. ' +
|
||||
'Erfinde KEINE Fakten (keine erfundenen Termine, Zahlen, Zusagen, Firmen). ' +
|
||||
'Wenn eine Angabe nötig ist, die du nicht weißt, setze einen klar erkennbaren ' +
|
||||
'Platzhalter in eckigen Klammern. Verwende nur den einfachen Bindestrich "-". ' +
|
||||
'Fasse Stellenbeschreibungen verständlich zusammen, statt sie wörtlich ' +
|
||||
'wiederzugeben, und beziehe sie auf das Profil des Bewerbers.'
|
||||
);
|
||||
// The assistant's role/tone is editable (Vorlagen page); the sections below
|
||||
// describe the runtime wiring (tools, date, profile) and stay in code.
|
||||
parts.push(promptStore.get(ctx && ctx.prompts, 'chat', { name }));
|
||||
parts.push(
|
||||
'# Werkzeuge\n' +
|
||||
'Du hast Werkzeuge, um Bewerbungsdaten nachzuschlagen. Nutze sie, sobald der ' +
|
||||
|
||||
+16
-36
@@ -9,6 +9,7 @@
|
||||
const { jsPDF } = require('jspdf');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const promptStore = require('./prompts');
|
||||
|
||||
// Embedded professional typeface for both cover letter and résumé: Lato
|
||||
// (SIL Open Font License, see lib/fonts/OFL.txt). The base64 TTFs are read
|
||||
@@ -127,7 +128,7 @@ const OUTPUT_SCHEMA = {
|
||||
required: ['headline', 'kontakt', 'anschreiben', 'email', 'lebenslauf'],
|
||||
};
|
||||
|
||||
async function generateTailoredTexts({ job, basisDokumente, settings, llmNotizen = '', zusatzAnlagen = [] }) {
|
||||
async function generateTailoredTexts({ job, basisDokumente, settings, llmNotizen = '', zusatzAnlagen = [], prompts = null }) {
|
||||
const apiKey = process.env.OLLAMA_API_KEY;
|
||||
if (!apiKey) {
|
||||
throw new Error(
|
||||
@@ -162,19 +163,9 @@ async function generateTailoredTexts({ job, basisDokumente, settings, llmNotizen
|
||||
job.stellenbeschreibung || '(keine Beschreibung übermittelt)',
|
||||
].filter((l) => l !== null).join('\n');
|
||||
|
||||
const system =
|
||||
'Du bist ein erfahrener Bewerbungscoach und erstellst professionelle, ' +
|
||||
'deutschsprachige Bewerbungstexte. Du passt die BEREITGESTELLTEN Basis-Unterlagen ' +
|
||||
'des Bewerbers auf eine konkrete Stellenausschreibung an. Wichtigste Regel: Du ' +
|
||||
'erfindest KEINE Fakten, Qualifikationen, Abschlüsse, Kontaktdaten oder ' +
|
||||
'Berufserfahrungen. Verwende ausschließlich Informationen aus den Basis-Unterlagen. ' +
|
||||
'Du darfst umformulieren, gewichten, relevante Punkte hervorheben und auf die Stelle ' +
|
||||
'zuschneiden — aber nichts hinzudichten. Schreibe natürlich, konkret und ohne Floskeln, so wie ein ' +
|
||||
'deutscher Muttersprachler schreibt: keine Buzzword-Paare, keine leeren Eigenschaftswörter und keine ' +
|
||||
'Selbstetiketten wie "Als Muttersprachler ..."; jede Aussage aus einer konkreten Tätigkeit heraus. ' +
|
||||
'Verwende AUSSCHLIESSLICH deutsche Sprache und das lateinische Alphabet. Keine englischen, ' +
|
||||
'chinesischen, japanischen, koreanischen oder anderen fremdsprachigen Wörter oder Schriftzeichen - ' +
|
||||
'auch nicht einzelne Zeichen oder Füllwörter.';
|
||||
// Role, tone and rules come from the (editable) prompt store; the JSON skeleton
|
||||
// below stays in code because the response parsing depends on it.
|
||||
const system = promptStore.get(prompts, 'unterlagen');
|
||||
|
||||
const skeleton =
|
||||
`{\n` +
|
||||
@@ -1140,8 +1131,8 @@ function hasLebenslauf(l) {
|
||||
|| l.schulbildung.length || l.weiterbildungen.length || l.kenntnisse.length);
|
||||
}
|
||||
|
||||
async function generateApplicationDocuments({ job, basisDokumente, settings, zusatzAnlagen = [], llmNotizen = '', signatur = null, bewerbungsfoto = null }) {
|
||||
const data = await generateTailoredTexts({ job, basisDokumente, settings, llmNotizen, zusatzAnlagen });
|
||||
async function generateApplicationDocuments({ job, basisDokumente, settings, zusatzAnlagen = [], llmNotizen = '', signatur = null, bewerbungsfoto = null, prompts = null }) {
|
||||
const data = await generateTailoredTexts({ job, basisDokumente, settings, llmNotizen, zusatzAnlagen, prompts });
|
||||
const headline = data.headline || job.stelle || '';
|
||||
const header = buildHeader(settings, data.kontakt, headline);
|
||||
|
||||
@@ -1241,28 +1232,20 @@ const REPLY_SCHEMA = {
|
||||
// `settings` = { name, ... }. `hinweise` = optional free-text steering.
|
||||
// `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' }) {
|
||||
async function generateEmailReply({ incoming, job = {}, settings = {}, hinweise = '', typ = 'antwort', prompts = null }) {
|
||||
const name = (settings && settings.name) || 'der Bewerber';
|
||||
const isAbsage = typ === 'absage';
|
||||
|
||||
const commonRules =
|
||||
' Wichtig: Du erfindest KEINE Fakten (keine erfundenen Termine, Zahlen, Zusagen). ' +
|
||||
'Wenn eine konkrete Angabe nötig ist, die du nicht kennst, setze einen klar erkennbaren ' +
|
||||
'Platzhalter in eckigen Klammern, z. B. "[Terminvorschlag einfügen]". Schreibe natürlich, ' +
|
||||
'knapp und verbindlich, wie ein deutscher Muttersprachler - ohne leere Floskeln, ohne ' +
|
||||
'Buzzword-Paare und ohne Selbstetiketten wie "Als Muttersprachler ...". Verwende ausschließlich ' +
|
||||
'deutsche Sprache und das lateinische Alphabet - keine fremdsprachigen Wörter oder Schriftzeichen.';
|
||||
// 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');
|
||||
// Joined with a space so neither part has to carry padding of its own — the
|
||||
// user edits these as plain paragraphs.
|
||||
const withRules = (rolle) => [rolle, commonRules].filter((s) => s && s.trim()).join(' ');
|
||||
|
||||
let system, user, betreffDefault;
|
||||
if (isAbsage) {
|
||||
system =
|
||||
'Du bist ' + name + ' und schreibst als Bewerber eine höfliche, professionelle, ' +
|
||||
'deutschsprachige Absage-E-Mail an ein Unternehmen im laufenden Bewerbungsprozess. ' +
|
||||
'Du teilst mit, dass du dich entschieden hast, eine andere Stelle anzunehmen, und ' +
|
||||
'bedankst dich für die Zeit, das Gespräch und das Vertrauen. Halte die E-Mail bewusst ' +
|
||||
'kurz: ein bis zwei kurze Absätze genügen - keine langen Erklärungen, keine Rechtfertigung. ' +
|
||||
'Bleibe wertschätzend und bestimmt zugleich; nenne keine Gründe im Detail und erfinde keine ' +
|
||||
'konkreten Namen anderer Unternehmen.' + commonRules;
|
||||
system = withRules(promptStore.get(prompts, 'email_absage', { name }));
|
||||
user =
|
||||
`# Kontext der Bewerbung\n` +
|
||||
`Unternehmen: ${job.firma || '-'}\n` +
|
||||
@@ -1284,10 +1267,7 @@ async function generateEmailReply({ incoming, job = {}, settings = {}, hinweise
|
||||
`Antworte AUSSCHLIESSLICH mit dem JSON-Objekt, ohne Markdown, ohne Code-Fences.`;
|
||||
betreffDefault = 'Re: ' + (incoming.subject || ('Absage zur Bewerbung als ' + (job.stelle || '')));
|
||||
} else {
|
||||
system =
|
||||
'Du bist ' + name + ' und schreibst als Bewerber eine höfliche, professionelle, ' +
|
||||
'deutschsprachige Antwort-E-Mail an ein Unternehmen im laufenden Bewerbungsprozess. ' +
|
||||
'Du antwortest konkret auf die eingegangene Nachricht.' + commonRules;
|
||||
system = withRules(promptStore.get(prompts, 'email_antwort', { name }));
|
||||
user =
|
||||
`# Kontext der Bewerbung\n` +
|
||||
`Unternehmen: ${job.firma || '-'}\n` +
|
||||
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
// Editable KI system prompts.
|
||||
//
|
||||
// Every prompt that shapes the assistant's role, tone and rules lives here as a
|
||||
// default and can be overridden per installation on the Vorlagen page. Only the
|
||||
// *system* prompts are editable: the user prompts carry the JSON skeleton the
|
||||
// answers are parsed against, so they stay in code where a typo can't break
|
||||
// generation. An override is a row in the `prompts` table; deleting the row
|
||||
// restores the default (that's what "Zurücksetzen" does).
|
||||
//
|
||||
// Placeholders are written as {{name}} and filled by render().
|
||||
|
||||
const DEFAULTS = [
|
||||
{
|
||||
key: 'unterlagen',
|
||||
titel: 'Bewerbungsunterlagen (Anschreiben & Lebenslauf)',
|
||||
beschreibung:
|
||||
'Rolle und Regeln für die Generierung der Bewerbungsunterlagen aus deinen Basis-Unterlagen. ' +
|
||||
'Der Aufbau der Dokumente (JSON-Struktur, Felder) ist fest verdrahtet und wird hiervon nicht berührt.',
|
||||
platzhalter: [],
|
||||
inhalt:
|
||||
'Du bist ein erfahrener Bewerbungscoach und erstellst professionelle, ' +
|
||||
'deutschsprachige Bewerbungstexte. Du passt die BEREITGESTELLTEN Basis-Unterlagen ' +
|
||||
'des Bewerbers auf eine konkrete Stellenausschreibung an. Wichtigste Regel: Du ' +
|
||||
'erfindest KEINE Fakten, Qualifikationen, Abschlüsse, Kontaktdaten oder ' +
|
||||
'Berufserfahrungen. Verwende ausschließlich Informationen aus den Basis-Unterlagen. ' +
|
||||
'Du darfst umformulieren, gewichten, relevante Punkte hervorheben und auf die Stelle ' +
|
||||
'zuschneiden — aber nichts hinzudichten. Schreibe natürlich, konkret und ohne Floskeln, so wie ein ' +
|
||||
'deutscher Muttersprachler schreibt: keine Buzzword-Paare, keine leeren Eigenschaftswörter und keine ' +
|
||||
'Selbstetiketten wie "Als Muttersprachler ..."; jede Aussage aus einer konkreten Tätigkeit heraus. ' +
|
||||
'Verwende AUSSCHLIESSLICH deutsche Sprache und das lateinische Alphabet. Keine englischen, ' +
|
||||
'chinesischen, japanischen, koreanischen oder anderen fremdsprachigen Wörter oder Schriftzeichen - ' +
|
||||
'auch nicht einzelne Zeichen oder Füllwörter.',
|
||||
},
|
||||
{
|
||||
key: 'chat',
|
||||
titel: 'KI-Chat (Bewerbungs-Assistent)',
|
||||
beschreibung:
|
||||
'Grundhaltung des Chat-Assistenten. Die Hinweise zu Werkzeugen, Datum und deinem Profil ' +
|
||||
'werden automatisch ergänzt.',
|
||||
platzhalter: ['name'],
|
||||
inhalt:
|
||||
'Du bist ein hilfreicher, deutschsprachiger Bewerbungs-Assistent für {{name}}. ' +
|
||||
'Du beantwortest Fragen zu laufenden Bewerbungen, hilfst beim Formulieren von ' +
|
||||
'Antworten, beim Vorbereiten auf Gespräche und beim Überblick über den Status. ' +
|
||||
'Antworte natürlich, knapp und auf Deutsch im lateinischen Alphabet. ' +
|
||||
'Erfinde KEINE Fakten (keine erfundenen Termine, Zahlen, Zusagen, Firmen). ' +
|
||||
'Wenn eine Angabe nötig ist, die du nicht weißt, setze einen klar erkennbaren ' +
|
||||
'Platzhalter in eckigen Klammern. Verwende nur den einfachen Bindestrich "-". ' +
|
||||
'Fasse Stellenbeschreibungen verständlich zusammen, statt sie wörtlich ' +
|
||||
'wiederzugeben, und beziehe sie auf das Profil des Bewerbers.',
|
||||
},
|
||||
{
|
||||
key: 'email_antwort',
|
||||
titel: 'E-Mail: Antwort auf eine Nachricht',
|
||||
beschreibung: 'Rolle für den KI-Entwurf einer Antwort-E-Mail an ein Unternehmen.',
|
||||
platzhalter: ['name'],
|
||||
inhalt:
|
||||
'Du bist {{name}} und schreibst als Bewerber eine höfliche, professionelle, ' +
|
||||
'deutschsprachige Antwort-E-Mail an ein Unternehmen im laufenden Bewerbungsprozess. ' +
|
||||
'Du antwortest konkret auf die eingegangene Nachricht.',
|
||||
},
|
||||
{
|
||||
key: 'email_absage',
|
||||
titel: 'E-Mail: Absage (andere Stelle angenommen)',
|
||||
beschreibung: 'Rolle für den KI-Entwurf einer Absage an ein Unternehmen.',
|
||||
platzhalter: ['name'],
|
||||
inhalt:
|
||||
'Du bist {{name}} und schreibst als Bewerber eine höfliche, professionelle, ' +
|
||||
'deutschsprachige Absage-E-Mail an ein Unternehmen im laufenden Bewerbungsprozess. ' +
|
||||
'Du teilst mit, dass du dich entschieden hast, eine andere Stelle anzunehmen, und ' +
|
||||
'bedankst dich für die Zeit, das Gespräch und das Vertrauen. Halte die E-Mail bewusst ' +
|
||||
'kurz: ein bis zwei kurze Absätze genügen - keine langen Erklärungen, keine Rechtfertigung. ' +
|
||||
'Bleibe wertschätzend und bestimmt zugleich; nenne keine Gründe im Detail und erfinde keine ' +
|
||||
'konkreten Namen anderer Unternehmen.',
|
||||
},
|
||||
{
|
||||
key: 'email_stil',
|
||||
titel: 'E-Mail: gemeinsame Stilregeln',
|
||||
beschreibung:
|
||||
'Wird an beide E-Mail-Prompts oben angehängt. Hier stehen die Regeln, die für jede ' +
|
||||
'generierte E-Mail gelten sollen.',
|
||||
platzhalter: [],
|
||||
inhalt:
|
||||
'Wichtig: Du erfindest KEINE Fakten (keine erfundenen Termine, Zahlen, Zusagen). ' +
|
||||
'Wenn eine konkrete Angabe nötig ist, die du nicht kennst, setze einen klar erkennbaren ' +
|
||||
'Platzhalter in eckigen Klammern, z. B. "[Terminvorschlag einfügen]". Schreibe natürlich, ' +
|
||||
'knapp und verbindlich, wie ein deutscher Muttersprachler - ohne leere Floskeln, ohne ' +
|
||||
'Buzzword-Paare und ohne Selbstetiketten wie "Als Muttersprachler ...". Verwende ausschließlich ' +
|
||||
'deutsche Sprache und das lateinische Alphabet - keine fremdsprachigen Wörter oder Schriftzeichen.',
|
||||
},
|
||||
];
|
||||
|
||||
const BY_KEY = new Map(DEFAULTS.map((p) => [p.key, p]));
|
||||
|
||||
// Default text of a prompt, or '' for an unknown key.
|
||||
function defaultText(key) {
|
||||
const p = BY_KEY.get(key);
|
||||
return p ? p.inhalt : '';
|
||||
}
|
||||
|
||||
function isKnownKey(key) {
|
||||
return BY_KEY.has(key);
|
||||
}
|
||||
|
||||
// Substitute {{placeholders}}. Unknown placeholders are left untouched so a typo
|
||||
// in the UI shows up in the output instead of silently emptying the sentence.
|
||||
function render(text, vars = {}) {
|
||||
return String(text == null ? '' : text).replace(/\{\{(\w+)\}\}/g, (match, name) =>
|
||||
Object.prototype.hasOwnProperty.call(vars, name) && vars[name] ? String(vars[name]) : match
|
||||
);
|
||||
}
|
||||
|
||||
// Look a prompt up in the overrides map (key -> text), falling back to the
|
||||
// default, and fill its placeholders. `prompts` may be undefined — callers that
|
||||
// don't pass any overrides simply get the defaults.
|
||||
function get(prompts, key, vars = {}) {
|
||||
const override = prompts && typeof prompts[key] === 'string' ? prompts[key].trim() : '';
|
||||
return render(override || defaultText(key), vars);
|
||||
}
|
||||
|
||||
// The list rendered on the Vorlagen page: every prompt with its current text and
|
||||
// whether it deviates from the default.
|
||||
function list(overrides = {}) {
|
||||
return DEFAULTS.map((p) => {
|
||||
const override = typeof overrides[p.key] === 'string' ? overrides[p.key].trim() : '';
|
||||
return {
|
||||
key: p.key,
|
||||
titel: p.titel,
|
||||
beschreibung: p.beschreibung,
|
||||
platzhalter: p.platzhalter,
|
||||
inhalt: override || p.inhalt,
|
||||
standard: p.inhalt,
|
||||
angepasst: Boolean(override && override !== p.inhalt.trim()),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { DEFAULTS, defaultText, isKnownKey, render, get, list };
|
||||
Reference in New Issue
Block a user