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:
@@ -29,6 +29,7 @@ const multer = require('multer');
|
||||
|
||||
const { generateApplicationDocuments, generateEmailReply } = require('./lib/documents');
|
||||
const chat = require('./lib/chat');
|
||||
const promptStore = require('./lib/prompts');
|
||||
const mailer = require('./lib/mailer');
|
||||
const { createExternalApi } = require('./lib/api');
|
||||
const { buildOpenApiSpec } = require('./lib/openapi');
|
||||
@@ -425,6 +426,19 @@ async function setState(key, value) {
|
||||
);
|
||||
}
|
||||
|
||||
// The user's KI prompt overrides as { key: text }. Keys without a row keep the
|
||||
// default from lib/prompts.js. Read fresh on every generation so an edit takes
|
||||
// effect immediately, without a restart.
|
||||
async function loadPrompts() {
|
||||
try {
|
||||
const rows = await dbAll('SELECT key, inhalt FROM prompts');
|
||||
return Object.fromEntries(rows.map((r) => [r.key, r.inhalt]));
|
||||
} catch (e) {
|
||||
console.error('Konnte Prompts nicht laden, nutze Standardtexte:', e.message);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
// Match an incoming message to an application: first via In-Reply-To/References
|
||||
// pointing at one of our sent messages, then by sender = a previous recipient.
|
||||
async function matchBewerbung(msg) {
|
||||
@@ -583,6 +597,7 @@ async function runGeneration(bewerbungId, options = {}) {
|
||||
const basisDokumente = await dbAll('SELECT * FROM basis_dokumente ORDER BY id ASC');
|
||||
const basisAnhaenge = await dbAll('SELECT * FROM basis_anhaenge ORDER BY id ASC');
|
||||
const settings = await dbGet('SELECT * FROM settings WHERE id = 1');
|
||||
const prompts = await loadPrompts();
|
||||
|
||||
// Only the explicitly selected extra attachments are enclosed (default: none).
|
||||
const anlagenIds = Array.isArray(options.anlagenIds) ? options.anlagenIds.map(Number) : [];
|
||||
@@ -598,6 +613,7 @@ async function runGeneration(bewerbungId, options = {}) {
|
||||
},
|
||||
basisDokumente,
|
||||
settings,
|
||||
prompts,
|
||||
// Names of the selected attachments so the cover letter (and the LLM) lists
|
||||
// exactly these under "Anlagen".
|
||||
zusatzAnlagen: selectedAnhaenge.map((a) => a.name || a.dateiname),
|
||||
@@ -940,6 +956,17 @@ function initializeDatabase() {
|
||||
`, () => {});
|
||||
db.run('CREATE INDEX IF NOT EXISTS idx_chat_messages_thread ON chat_messages(thread_id, id)', () => {});
|
||||
|
||||
// Overridden KI system prompts. Only prompts the user actually edited
|
||||
// are stored; everything else falls back to the defaults in
|
||||
// lib/prompts.js, so "Zurücksetzen" is a plain DELETE.
|
||||
db.run(`
|
||||
CREATE TABLE IF NOT EXISTS prompts (
|
||||
key TEXT PRIMARY KEY,
|
||||
inhalt TEXT NOT NULL,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`, () => {});
|
||||
|
||||
db.run(`
|
||||
CREATE TABLE IF NOT EXISTS settings (
|
||||
id INTEGER PRIMARY KEY CHECK (id = 1),
|
||||
@@ -1578,6 +1605,7 @@ initializeDatabase().then(() => {
|
||||
incoming: { from: orig.from_addr, subject: orig.subject, text: emailPlainText(orig) },
|
||||
job: { firma: bewerbung.firma, stelle: bewerbung.stelle },
|
||||
settings,
|
||||
prompts: await loadPrompts(),
|
||||
hinweise: String(req.body.hinweise || ''),
|
||||
typ: String(req.body.typ || 'antwort'),
|
||||
});
|
||||
@@ -1883,6 +1911,7 @@ initializeDatabase().then(() => {
|
||||
res.render('vorlagen', {
|
||||
basisDokumente,
|
||||
basisAnhaenge,
|
||||
prompts: promptStore.list(await loadPrompts()),
|
||||
hasSignatur: Boolean(currentSignaturFile()),
|
||||
hasFoto: Boolean(currentFotoFile()),
|
||||
basisTypOptions: BASIS_TYP_OPTIONS,
|
||||
@@ -1895,6 +1924,44 @@ initializeDatabase().then(() => {
|
||||
}
|
||||
});
|
||||
|
||||
// ----- Editable KI prompts -----
|
||||
// Registered before /vorlagen/:id so the generic base-document handlers don't
|
||||
// swallow these paths. The prompt text is stored raw (no HTML escaping): it is
|
||||
// sent to the LLM, never rendered as markup — the views escape it on output.
|
||||
|
||||
// Save an overridden prompt. Empty text = fall back to the default.
|
||||
app.post('/vorlagen/prompts/:key', async (req, res) => {
|
||||
try {
|
||||
const { key } = req.params;
|
||||
if (!promptStore.isKnownKey(key)) return res.status(404).send('Unbekannter Prompt');
|
||||
const inhalt = String(req.body.inhalt || '').trim();
|
||||
if (!inhalt || inhalt === promptStore.defaultText(key).trim()) {
|
||||
await dbRun('DELETE FROM prompts WHERE key = ?', [key]);
|
||||
} else {
|
||||
await dbRun(
|
||||
`INSERT INTO prompts (key, inhalt, updated_at) VALUES (?, ?, CURRENT_TIMESTAMP)
|
||||
ON CONFLICT(key) DO UPDATE SET inhalt = excluded.inhalt, updated_at = CURRENT_TIMESTAMP`,
|
||||
[key, inhalt]
|
||||
);
|
||||
}
|
||||
res.redirect('/vorlagen#prompts');
|
||||
} catch (error) {
|
||||
console.error('Error saving prompt:', error);
|
||||
res.status(500).send('Serverfehler');
|
||||
}
|
||||
});
|
||||
|
||||
// Restore the default text of a prompt by dropping the override.
|
||||
app.post('/vorlagen/prompts/:key/reset', async (req, res) => {
|
||||
try {
|
||||
await dbRun('DELETE FROM prompts WHERE key = ?', [req.params.key]);
|
||||
res.redirect('/vorlagen#prompts');
|
||||
} catch (error) {
|
||||
console.error('Error resetting prompt:', error);
|
||||
res.status(500).send('Serverfehler');
|
||||
}
|
||||
});
|
||||
|
||||
// Add a base document
|
||||
app.post('/vorlagen', async (req, res) => {
|
||||
try {
|
||||
@@ -2431,20 +2498,21 @@ initializeDatabase().then(() => {
|
||||
// Gated behind OLLAMA_API_KEY. Threads + messages persist in SQLite; the
|
||||
// assistant answer is streamed back via Server-Sent Events.
|
||||
async function gatherChatContext() {
|
||||
const [settings, profilRows] = await Promise.all([
|
||||
const [settings, profilRows, prompts] = await Promise.all([
|
||||
dbGet('SELECT name FROM settings WHERE id = 1'),
|
||||
dbAll(
|
||||
`SELECT inhalt FROM basis_dokumente
|
||||
WHERE typ IN ('Lebenslauf', 'Profil/Kurzprofil') AND inhalt IS NOT NULL AND inhalt != ''
|
||||
ORDER BY CASE typ WHEN 'Lebenslauf' THEN 0 ELSE 1 END`
|
||||
),
|
||||
loadPrompts(),
|
||||
]);
|
||||
// Lightweight core context only: name, date and the user's profile (static,
|
||||
// small). All application/appointment data is fetched on demand via tools,
|
||||
// so the system prompt stays tiny regardless of how many bewerbungen exist.
|
||||
const profil = (profilRows.map((r) => (r.inhalt || '').trim()).join('\n\n---\n\n')).slice(0, 1800);
|
||||
const heute = new Date().toLocaleDateString('de-DE', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' });
|
||||
return { heute, profil, settings: settings || {} };
|
||||
return { heute, profil, prompts, settings: settings || {} };
|
||||
}
|
||||
|
||||
// Ollama tool definitions the assistant can call to look up application data.
|
||||
|
||||
Reference in New Issue
Block a user