6.9 KiB
05 KI
Modell: gpt-5.6-luna (OpenAI), angesprochen über chat/completions. Der
Browser spricht nie direkt mit OpenAI, jede Anfrage läuft über das Backend
(server/src/ai.js). Schlüssel und Modell werden unter Einstellungen → KI
gepflegt und liegen in icon_settings; server/.env (chmod 600) bleibt
Startwert und Rückfall.
Fehlt der Schlüssel, läuft das CRM normal weiter und blendet die KI-Knöpfe aus.
GET /api/ai/status sagt der Oberfläche, ob etwas eingerichtet ist.
Schlüssel pflegen
Einstellungen → KI (nur Administratoren). Der Schlüssel steht in
icon_settings (openai_api_key), die .env bleibt Startwert und Rückfall –
die Datenbank gewinnt. Grund: ein Schlüssel läuft ab oder wird gewechselt,
und dann soll niemand dafür eine Datei auf dem Server bearbeiten und einen
Dienst neu starten müssen.
| Was | Verhalten |
|---|---|
| Anzeige | nur verkürzt (sk-proj…a1b2) – vollständig verlässt der Schlüssel den Server nie |
| Eingabe | Passwortfeld, schreibt nur; ein bestehender Wert wird ersetzt |
| Entfernen | löscht den Wert in der Datenbank; steht in der .env einer, gilt wieder dieser |
| Modell | frei setzbar, Vorgabe gpt-5.6-luna |
| Test | ein Miniaufruf gegen das Modell – sagt, ob Schlüssel, Modellname und Kontingent zusammenpassen |
In der Historie landet, dass jemand den Schlüssel gewechselt hat, nie der Wert. Ein stiller Wechsel wäre später nicht mehr nachvollziehbar.
Fallstrick:
gpt-5.6-lunaakzeptiert kein eigenestemperatureund weist jeden Wert außer dem Standard mit HTTP 400 ab.ai.jsschickt deshalb keines.response_format: {type:'json_object'}funktioniert dagegen.
Drei Stellen nutzen das Modell:
flowchart LR
CV["CV-Import<br/>Lebenslauf → Kontaktvorschlag"] --> AI["server/src/ai.js"]
TX["Schreibhilfe<br/>Ausschreibungstext"] --> AI
SU["Erweiterte Suche<br/>Frage in Worten"] --> AI
AI -->|"HTTPS, Schluessel bleibt am Server"| OAI["api.openai.com"]
CV-Import
Kontakte → CV-Import. Lebensläufe als PDF, DOCX oder Text, per Dateiauswahl oder Ziehen, auch mehrere auf einmal (bis 20, je 15 MB).
flowchart LR
U["Hochladen"] --> E["Eingang"]
E -->|"Mensch: Ja, auslesen"| S["Screening"]
E -->|"Mensch: Nein"| V["Verworfen"]
S -->|"Formular bestaetigt"| A["Auswahl<br/>Kontakt angelegt"]
S -->|"abgelehnt"| V
| Ordner | Bedeutung |
|---|---|
| Eingang | hochgeladen; ein Mensch entscheidet je Datei „Ja, auslesen" oder „Nein" |
| Screening | ausgelesen; der Vorschlag steht im Formular und wird bestätigt |
| Auswahl | übernommen – Kontakt angelegt, Lebenslauf hängt daran |
Es wird bewusst nichts automatisch angelegt: ein falsch gelesener Name oder Stundensatz wäre in der Datenbank schwerer zu finden als im Formular. Was dort korrigiert wird, gewinnt gegen die KI.
Übernommen werden Name, Position, Kontaktdaten, LinkedIn, aktueller Arbeitgeber, Schlagworte, eine Zusammenfassung und – nur wenn sie ausdrücklich im Text stehen – die Stundensätze. Steht im Lebenslauf ein Tagessatz, wird er nicht umgerechnet, sondern weggelassen.
Frühere Stationen werden als beendete Anstellungen angelegt; dadurch erscheint der Kontakt beim jeweiligen Unternehmen sofort im Zweig „Verlassen". Unbekannte Unternehmen legt der Import mit an.
Der Text wird vor der Anfrage lokal aus der Datei geholt
(server/src/cvText.js) – PDF und DOCX ohne Fremddienst. Die Datei bleibt am
Kontakt (icon_contacts.cv_file) und lässt sich aus dessen Karte herunterladen;
der Download läuft über fetch mit Bearer-Kopf, ein normaler Link schickt ihn
nicht mit.
Schreibhilfe für Ausschreibungen
Das Feld Ausschreibungstext (icon_tenders.notes) ist ein WYSIWYG-Editor mit
fünf Knöpfen: Text erzeugen, Verbessern, Kürzen, Stichpunkte,
Anforderungsprofil. Darunter ein Feld für die Vorgabe („Interim PL SAP
S/4HANA, Bank, 12 Monate, 80 % remote").
Der vorherige Stand lässt sich mit Zurück zurückholen – die KI überschreibt nichts unwiderruflich. Gespeichert wird 800 ms nach dem letzten Tastendruck, es gibt keinen Speichern-Knopf.
Der Diktat-Knopf steht in derselben Leiste und ist unabhängig von der KI: er rechnet im Browser und braucht keinen Schlüssel (siehe 04 Diktat).
Erweiterte Suche
Ein Feld, zwei Modi – umgeschaltet über die Symbole rechts im Feld:
Lupe – die normale Suche (Strg+K). Über Kontakte, Unternehmen,
Ausschreibungen, Projekte, Leads und die Historie; läuft beim Tippen mit, ein
Treffer öffnet direkt die Karte. Bewusst LIKE statt Volltextindex: die
Datenmengen sind klein, und ein FULLTEXT-Index hülfe bei Teilwortsuche
(„informat" soll „Finanz Informatik" finden) nicht.
Funke – die erweiterte Suche. Eine Frage in Worten, beantwortet vom Modell. Die genannten Datensätze kommen als anklickbare Chips zurück.
sequenceDiagram
actor B as Benutzer
participant E as Express
participant M as MariaDB
participant O as OpenAI
B->>E: POST /api/search/ai { question }
E->>M: Bestand lesen (Kontakte, Unternehmen, Ausschreibungen, Projekte, Leads)
E->>E: zusammenfassen, ab 400 Zeilen je Liste kuerzen
E->>O: Systemprompt + Bestand + Frage
O-->>E: { answer, refs }
E-->>B: Antwort + anklickbare Chips
Sie läuft nicht beim Tippen mit, sondern erst auf Enter – jede Anfrage kostet. Und sie erzeugt kein SQL: der Bestand wird mitgegeben. Ein Modell, das Abfragen auf die Datenbank schreiben darf, wäre eine Angriffsfläche; der Bestand ist klein genug, um ihn vollständig mitzuliefern. Wird gekürzt, sagt die Antwort das dazu.
Was im Bestand steht
Je Kontakt: Name, Position, Art, Status, E-Mail, Telefon, Quelle, Schlagworte,
freiberuflich, Kontaktstand, rate_onsite und rate_remote, LinkedIn,
Unternehmen und dessen Branche.
Zwei Regeln stehen ausdrücklich im Systemprompt, weil die Antworten sonst falsch werden:
- Ein fehlender Satz ist nicht null Euro. Verglichen wird nur, wo ein Wert steht; die Antwort sagt dazu, bei wie vielen keiner hinterlegt ist.
- Es sind Stundensätze, keine Tagessätze. Fragt jemand nach dem Tagessatz, antwortet die Suche mit dem Stundensatz und sagt das dazu.
Beispiel:
Frage: Wer hat den höchsten Tagessatz? Antwort: Oliver Schaible hat mit 165,00 € pro Stunde den höchsten hinterlegten Satz vor Ort. Im CRM sind Stundensätze, keine Tagessätze, gepflegt. Bei 8 Kontakten ist kein Satz hinterlegt.
Die Kontakte des perinco CRM stehen dem Bestand ebenfalls zur Verfügung, solange
die Schnittstelle eingerichtet ist – gelesen über denselben Zwischenspeicher wie
die Ansicht (fetchPerincoContacts()), und ausdrücklich als fremd gekennzeichnet.