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:
2026-07-07 14:36:45 +00:00
co-authored by Claude
parent cc397e8f71
commit cf6cf9ee33
2 changed files with 89 additions and 29 deletions
+59 -14
View File
@@ -2324,29 +2324,74 @@ 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, applications, recentEmails, upcoming] = await Promise.all([
const [settings, upcoming] = await Promise.all([
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),
]);
// 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' });
return {
heute,
profil,
settings: settings || {},
applications: applications.map((a) => ({
firma: a.firma, stelle: a.stelle, status: a.status,
datum: a.datum, notizen: (a.notizen || '').slice(0, 240),
})),
recentEmails: recentEmails.map((e) => ({
subject: e.subject, from: e.from_addr, bewerbung: e.bewerbung,
applications: apps.map((a) => ({
id: a.id,
firma: a.firma, stelle: a.stelle, status: a.status, datum: a.datum, ort: a.ort,
quelle_url: a.quelle_url,
notizen: (a.notizen || '').trim().slice(0, 240),
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) => ({
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,
})),
};
}