Dokumentation: docs/05-ki.md
This commit is contained in:
+135
@@ -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<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).
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
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](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.
|
||||||
Reference in New Issue
Block a user