KI-Einstellungen: Schluessel und Modell in der Oberflaeche

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