From 9dc96ca220d29c6d08ce22e9b9154dc825245b1d Mon Sep 17 00:00:00 2001 From: Sascha Date: Mon, 14 Sep 2026 15:34:13 +0200 Subject: [PATCH] Dokumentation: docs/05-ki.md --- docs/05-ki.md | 135 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 docs/05-ki.md diff --git a/docs/05-ki.md b/docs/05-ki.md new file mode 100644 index 0000000..e7e07b6 --- /dev/null +++ b/docs/05-ki.md @@ -0,0 +1,135 @@ +# 05 KI + +Modell: **`gpt-5.6-luna`** (OpenAI), angesprochen über `chat/completions`. +Schlüssel und Modell stehen ausschließlich in `server/.env` (chmod 600) – der +Browser spricht nie direkt mit OpenAI, jede Anfrage läuft über das Backend +(`server/src/ai.js`). + +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. + +> **Fallstrick:** `gpt-5.6-luna` akzeptiert **kein eigenes `temperature`** und +> weist jeden Wert außer dem Standard mit HTTP 400 ab. `ai.js` schickt deshalb +> keines. `response_format: {type:'json_object'}` funktioniert dagegen. + +Drei Stellen nutzen das Modell: + +```mermaid +flowchart LR + CV["CV-Import
Lebenslauf → Kontaktvorschlag"] --> AI["server/src/ai.js"] + TX["Schreibhilfe
Ausschreibungstext"] --> AI + SU["Erweiterte Suche
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). + +```mermaid +flowchart LR + U["Hochladen"] --> E["Eingang"] + E -->|"Mensch: Ja, auslesen"| S["Screening"] + E -->|"Mensch: Nein"| V["Verworfen"] + S -->|"Formular bestaetigt"| A["Auswahl
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](04-diktat.md)). + +## 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. + +```mermaid +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.