diff --git a/docs/05-ki.md b/docs/05-ki.md index e7e07b6..7dfe0ef 100644 --- a/docs/05-ki.md +++ b/docs/05-ki.md @@ -1,135 +1,155 @@ -# 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. +# 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-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.