KI-Chat: reichhaltigerer Kontext (Stellenbeschreibung, Korrespondenz, Profil)
Der Assistent kannte nur Firma/Stelle/Status/Notizen und konnte daher keine firmenspezifischen Fragen beantworten. gatherChatContext lädt jetzt pro Bewerbung: Ort, interne Notizen, Stellenbeschreibung (bzw. verknüpftes Jobangebot), Kontakt/Ansprechpartner und die E-Mail-Korrespondenz (Betreffe). Zusätzlich wird der Lebenslauf des Bewerbers injiziert, damit Antworten auf "was für mich wichtig" zugeschnitten werden können. Relevante Bewerbungen = Termin-Bewerbungen + 12 jüngste (gebunden im Token-Budget). Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
+30
-15
@@ -99,8 +99,9 @@ async function streamChat({ system, messages, onToken, temperature = 0.6, signal
|
|||||||
|
|
||||||
// Build the German system prompt that grounds the assistant in the user's
|
// Build the German system prompt that grounds the assistant in the user's
|
||||||
// application data. `context` is gathered by the server from SQLite:
|
// application data. `context` is gathered by the server from SQLite:
|
||||||
// { settings, applications, recentEmails, upcomingTermine }
|
// { heute, settings, applications, upcomingTermine }
|
||||||
// Each entry is already trimmed to the fields the prompt needs.
|
// Each application entry carries firma/stelle/status, notes, the (trimmed)
|
||||||
|
// job description, contact, and the recent e-mail correspondence subjects.
|
||||||
function buildContextPrompt(ctx) {
|
function buildContextPrompt(ctx) {
|
||||||
const name = (ctx && ctx.settings && ctx.settings.name) || 'der Bewerber';
|
const name = (ctx && ctx.settings && ctx.settings.name) || 'der Bewerber';
|
||||||
const heute = (ctx && ctx.heute) || '';
|
const heute = (ctx && ctx.heute) || '';
|
||||||
@@ -112,7 +113,9 @@ function buildContextPrompt(ctx) {
|
|||||||
'Antworte natürlich, knapp und auf Deutsch im lateinischen Alphabet. ' +
|
'Antworte natürlich, knapp und auf Deutsch im lateinischen Alphabet. ' +
|
||||||
'Erfinde KEINE Fakten (keine erfundenen Termine, Zahlen, Zusagen, Firmen). ' +
|
'Erfinde KEINE Fakten (keine erfundenen Termine, Zahlen, Zusagen, Firmen). ' +
|
||||||
'Wenn eine Angabe nötig ist, die du nicht weißt, setze einen klar erkennbaren ' +
|
'Wenn eine Angabe nötig ist, die du nicht weißt, setze einen klar erkennbaren ' +
|
||||||
'Platzhalter in eckigen Klammern. Verwende nur den einfachen Bindestrich "-".'
|
'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.'
|
||||||
);
|
);
|
||||||
if (heute) {
|
if (heute) {
|
||||||
parts.push('# Heute\n' + 'Heutiges Datum: ' + heute + '. ' +
|
parts.push('# Heute\n' + 'Heutiges Datum: ' + heute + '. ' +
|
||||||
@@ -122,34 +125,46 @@ function buildContextPrompt(ctx) {
|
|||||||
'bzw. UTC+2 während der Sommerzeit).');
|
'bzw. UTC+2 während der Sommerzeit).');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ctx && ctx.profil) {
|
||||||
|
parts.push('# Dein Profil (Lebenslauf)\n' + ctx.profil +
|
||||||
|
'\nNutze dies, um Antworten auf den Bewerber zuzuschneiden: Wenn der Nutzer ' +
|
||||||
|
'nach einer Firma fragt, hebe hervor, welche Anforderungen der Stelle zu ' +
|
||||||
|
'seinem Profil passen und wo Lücken bestehen.');
|
||||||
|
}
|
||||||
|
|
||||||
const apps = (ctx && ctx.applications) || [];
|
const apps = (ctx && ctx.applications) || [];
|
||||||
if (apps.length) {
|
if (apps.length) {
|
||||||
parts.push('# Bewerbungen (jüngste zuerst)');
|
parts.push('# Bewerbungen (jüngste zuerst)');
|
||||||
for (const a of apps) {
|
for (const a of apps) {
|
||||||
const lines = [
|
const lines = [
|
||||||
`• ${a.firma || '?'} – ${a.stelle || '?'} (Status: ${a.status || '—'}, Datum: ${a.datum || '—'})`,
|
`• [Bewerbung ${a.id}] ${a.firma || '?'} – ${a.stelle || '?'} ` +
|
||||||
|
`(Status: ${a.status || '—'}, Datum: ${a.datum || '—'}${a.ort ? `, Ort: ${a.ort}` : ''})`,
|
||||||
];
|
];
|
||||||
|
if (a.kontakt || a.ansprechpartner) {
|
||||||
|
lines.push(` Kontakt: ${[a.ansprechpartner, a.kontakt].filter(Boolean).join(', ')}`);
|
||||||
|
}
|
||||||
|
if (a.quelle_url) lines.push(` Quelle: ${a.quelle_url}`);
|
||||||
if (a.notizen) lines.push(` Notizen: ${a.notizen}`);
|
if (a.notizen) lines.push(` Notizen: ${a.notizen}`);
|
||||||
|
if (a.interne_notizen) lines.push(` Interne Notizen: ${a.interne_notizen}`);
|
||||||
|
if (a.stellenbeschreibung) lines.push(` Stellenbeschreibung: ${a.stellenbeschreibung}`);
|
||||||
|
const kor = a.korrespondenz || [];
|
||||||
|
if (kor.length) {
|
||||||
|
lines.push(' Korrespondenz (neueste zuerst):');
|
||||||
|
for (const e of kor) {
|
||||||
|
const tag = e.direction === 'out' ? 'gesendet' : 'eingegangen';
|
||||||
|
lines.push(` - ${tag}: ${e.subject || '(kein Betreff)'}${e.from ? ` — von ${e.from}` : ''}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
parts.push(lines.join('\n'));
|
parts.push(lines.join('\n'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const emails = (ctx && ctx.recentEmails) || [];
|
|
||||||
if (emails.length) {
|
|
||||||
parts.push('# Letzte eingegangene E-Mails');
|
|
||||||
for (const e of emails) {
|
|
||||||
parts.push(
|
|
||||||
`• ${e.subject || '(kein Betreff)'} — von ${e.from || '?'} (${e.bewerbung || '?'})`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const termine = (ctx && ctx.upcomingTermine) || [];
|
const termine = (ctx && ctx.upcomingTermine) || [];
|
||||||
if (termine.length) {
|
if (termine.length) {
|
||||||
parts.push('# Kommende Termine');
|
parts.push('# Kommende Termine');
|
||||||
for (const t of termine) {
|
for (const t of termine) {
|
||||||
parts.push(
|
parts.push(
|
||||||
`• ${t.titel} — ${t.start}${t.bewerbung ? ` (${t.bewerbung})` : ''}`
|
`• ${t.titel} — ${t.start}${t.bewerbung ? ` (Bewerbung ${t.bewerbung_id || '?'}, ${t.bewerbung})` : ''}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2324,29 +2324,74 @@ initializeDatabase().then(() => {
|
|||||||
// Gated behind OLLAMA_API_KEY. Threads + messages persist in SQLite; the
|
// Gated behind OLLAMA_API_KEY. Threads + messages persist in SQLite; the
|
||||||
// assistant answer is streamed back via Server-Sent Events.
|
// assistant answer is streamed back via Server-Sent Events.
|
||||||
async function gatherChatContext() {
|
async function gatherChatContext() {
|
||||||
const [settings, applications, recentEmails, upcoming] = await Promise.all([
|
const [settings, upcoming] = await Promise.all([
|
||||||
dbGet('SELECT name FROM settings WHERE id = 1'),
|
dbGet('SELECT name FROM settings WHERE id = 1'),
|
||||||
dbAll(`SELECT firma, stelle, status, datum, notizen FROM bewerbungen
|
|
||||||
ORDER BY datum DESC, created_at DESC LIMIT 25`),
|
|
||||||
dbAll(`SELECT e.subject, e.from_addr, b.firma AS bewerbung
|
|
||||||
FROM emails e LEFT JOIN bewerbungen b ON b.id = e.bewerbung_id
|
|
||||||
WHERE e.direction = 'received'
|
|
||||||
ORDER BY e.email_date DESC, e.created_at DESC LIMIT 12`),
|
|
||||||
upcomingTermine(8),
|
upcomingTermine(8),
|
||||||
]);
|
]);
|
||||||
|
// Relevant bewerbungen: those tied to upcoming appointments, plus the most
|
||||||
|
// recent ones. Detailed context (job description, correspondence) is loaded
|
||||||
|
// for these so the assistant can answer firmen-specific questions.
|
||||||
|
const terminIds = upcoming.map((t) => t.bewerbung_id).filter(Boolean);
|
||||||
|
const recent = await dbAll(
|
||||||
|
'SELECT id FROM bewerbungen ORDER BY datum DESC, created_at DESC LIMIT 12'
|
||||||
|
);
|
||||||
|
const idSet = new Set([...terminIds, ...recent.map((r) => r.id)]);
|
||||||
|
const ids = [...idSet];
|
||||||
|
const apps = ids.length
|
||||||
|
? await dbAll(
|
||||||
|
`SELECT b.id, b.firma, b.stelle, b.status, b.datum, b.ort, b.notizen,
|
||||||
|
b.interne_notizen, b.stellenbeschreibung, b.quelle_url,
|
||||||
|
j.kontakt_email AS ja_kontakt, j.ansprechpartner AS ja_ansprech, j.beschreibung AS ja_beschreibung
|
||||||
|
FROM bewerbungen b
|
||||||
|
LEFT JOIN jobangebote j ON j.verknuepfte_bewerbung_id = b.id
|
||||||
|
WHERE b.id IN (${ids.map(() => '?').join(',')})
|
||||||
|
ORDER BY b.datum DESC, b.created_at DESC`,
|
||||||
|
ids
|
||||||
|
)
|
||||||
|
: [];
|
||||||
|
// Per-application correspondence (subjects only), newest first per app.
|
||||||
|
const emailsByApp = new Map();
|
||||||
|
if (ids.length) {
|
||||||
|
const emRows = await dbAll(
|
||||||
|
`SELECT bewerbung_id, direction, subject, from_addr
|
||||||
|
FROM emails
|
||||||
|
WHERE bewerbung_id IN (${ids.map(() => '?').join(',')})
|
||||||
|
ORDER BY bewerbung_id, email_date DESC, created_at DESC`,
|
||||||
|
ids
|
||||||
|
);
|
||||||
|
for (const e of emRows) {
|
||||||
|
if (!emailsByApp.has(e.bewerbung_id)) emailsByApp.set(e.bewerbung_id, []);
|
||||||
|
const arr = emailsByApp.get(e.bewerbung_id);
|
||||||
|
if (arr.length < 5) arr.push({ direction: e.direction, subject: e.subject, from: e.from_addr });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// The user's own profile (Lebenslauf / Kurzprofil) so the assistant can
|
||||||
|
// relate job descriptions back to the applicant's background.
|
||||||
|
const profilRows = await 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`
|
||||||
|
);
|
||||||
|
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' });
|
const heute = new Date().toLocaleDateString('de-DE', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' });
|
||||||
return {
|
return {
|
||||||
heute,
|
heute,
|
||||||
|
profil,
|
||||||
settings: settings || {},
|
settings: settings || {},
|
||||||
applications: applications.map((a) => ({
|
applications: apps.map((a) => ({
|
||||||
firma: a.firma, stelle: a.stelle, status: a.status,
|
id: a.id,
|
||||||
datum: a.datum, notizen: (a.notizen || '').slice(0, 240),
|
firma: a.firma, stelle: a.stelle, status: a.status, datum: a.datum, ort: a.ort,
|
||||||
})),
|
quelle_url: a.quelle_url,
|
||||||
recentEmails: recentEmails.map((e) => ({
|
notizen: (a.notizen || '').trim().slice(0, 240),
|
||||||
subject: e.subject, from: e.from_addr, bewerbung: e.bewerbung,
|
interne_notizen: (a.interne_notizen || '').trim().slice(0, 240),
|
||||||
|
stellenbeschreibung: (a.stellenbeschreibung || a.ja_beschreibung || '').trim().slice(0, 900),
|
||||||
|
kontakt: a.ja_kontakt || null,
|
||||||
|
ansprechpartner: a.ja_ansprech || null,
|
||||||
|
korrespondenz: emailsByApp.get(a.id) || [],
|
||||||
})),
|
})),
|
||||||
upcomingTermine: upcoming.map((t) => ({
|
upcomingTermine: upcoming.map((t) => ({
|
||||||
titel: t.titel, start: t.start, bewerbung: t.bewerbung_firma,
|
titel: t.titel, start: t.start, bewerbung_id: t.bewerbung_id, bewerbung: t.bewerbung_firma,
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user