fix(email): support legacy AI gateway models
This commit is contained in:
@@ -75,17 +75,14 @@ export function buildSuggestionMail(message: any) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function suggestEmailEntities(message: any, allCandidates: EntityCandidate[]) {
|
export function buildEntitySuggestionRequest(
|
||||||
const central = Boolean(secrets.FEDEO_CENTRAL_SERVICES_ENABLED && centralServicesClient.configured())
|
mail: ReturnType<typeof buildSuggestionMail>,
|
||||||
if (!central && !secrets.OPENAI_API_KEY) {
|
candidates: EntityCandidate[],
|
||||||
throw Object.assign(new Error("Die KI-Erkennung ist noch nicht konfiguriert."), { statusCode: 503 })
|
model = "gpt-5.6-luna",
|
||||||
}
|
) {
|
||||||
const mail = buildSuggestionMail(message)
|
return {
|
||||||
const candidates = selectCandidates(allCandidates, JSON.stringify(mail))
|
model,
|
||||||
if (!candidates.length) return []
|
...(model.startsWith("gpt-5") ? { reasoning_effort: "none" } : {}),
|
||||||
const request: any = {
|
|
||||||
model: "gpt-5.6-luna",
|
|
||||||
reasoning_effort: "none",
|
|
||||||
store: false,
|
store: false,
|
||||||
max_completion_tokens: 1500,
|
max_completion_tokens: 1500,
|
||||||
response_format: zodResponseFormat(suggestionFormat as any, "email_entity_suggestions"),
|
response_format: zodResponseFormat(suggestionFormat as any, "email_entity_suggestions"),
|
||||||
@@ -94,9 +91,35 @@ export async function suggestEmailEntities(message: any, allCandidates: EntityCa
|
|||||||
{ role: "user", content: JSON.stringify({ mail, candidates }) },
|
{ role: "user", content: JSON.stringify({ mail, candidates }) },
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
const completion = central
|
}
|
||||||
? await centralServicesClient.aiChatCompletions(request)
|
|
||||||
: await new OpenAI({ apiKey: secrets.OPENAI_API_KEY, timeout: 45000, maxRetries: 0 }).chat.completions.create(request)
|
export function shouldRetryWithLegacyAiModel(error: any) {
|
||||||
|
return [400, 404, 422, 500, 502].includes(Number(error?.status))
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function suggestEmailEntities(message: any, allCandidates: EntityCandidate[]) {
|
||||||
|
const central = Boolean(secrets.FEDEO_CENTRAL_SERVICES_ENABLED && centralServicesClient.configured())
|
||||||
|
if (!central && !secrets.OPENAI_API_KEY) {
|
||||||
|
throw Object.assign(new Error("Die KI-Erkennung ist noch nicht konfiguriert."), { statusCode: 503 })
|
||||||
|
}
|
||||||
|
const mail = buildSuggestionMail(message)
|
||||||
|
const candidates = selectCandidates(allCandidates, JSON.stringify(mail))
|
||||||
|
if (!candidates.length) return []
|
||||||
|
const request: any = buildEntitySuggestionRequest(mail, candidates)
|
||||||
|
let completion: any
|
||||||
|
if (central) {
|
||||||
|
try {
|
||||||
|
completion = await centralServicesClient.aiChatCompletions(request)
|
||||||
|
} catch (error: any) {
|
||||||
|
if (!shouldRetryWithLegacyAiModel(error)) throw error
|
||||||
|
completion = await centralServicesClient.aiChatCompletions(
|
||||||
|
buildEntitySuggestionRequest(mail, candidates, "gpt-4o-mini"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
completion = await new OpenAI({ apiKey: secrets.OPENAI_API_KEY, timeout: 45000, maxRetries: 0 })
|
||||||
|
.chat.completions.create(request)
|
||||||
|
}
|
||||||
const choice = completion.choices?.[0]
|
const choice = completion.choices?.[0]
|
||||||
if (choice?.finish_reason !== "stop" || choice?.message?.refusal || !choice?.message?.content) {
|
if (choice?.finish_reason !== "stop" || choice?.message?.refusal || !choice?.message?.content) {
|
||||||
throw new Error("Die KI konnte keine vollständige Analyse liefern. Bitte erneut versuchen.")
|
throw new Error("Die KI konnte keine vollständige Analyse liefern. Bitte erneut versuchen.")
|
||||||
|
|||||||
@@ -2,8 +2,10 @@ import assert from "node:assert/strict"
|
|||||||
import test from "node:test"
|
import test from "node:test"
|
||||||
import {
|
import {
|
||||||
buildSuggestionMail,
|
buildSuggestionMail,
|
||||||
|
buildEntitySuggestionRequest,
|
||||||
dismissEntitySuggestion,
|
dismissEntitySuggestion,
|
||||||
selectCandidates,
|
selectCandidates,
|
||||||
|
shouldRetryWithLegacyAiModel,
|
||||||
validateSuggestions,
|
validateSuggestions,
|
||||||
type EntityCandidate,
|
type EntityCandidate,
|
||||||
} from "../src/modules/email/email.entity-suggestions"
|
} from "../src/modules/email/email.entity-suggestions"
|
||||||
@@ -69,3 +71,22 @@ test("dismisses only the selected entity suggestion", () => {
|
|||||||
)
|
)
|
||||||
assert.deepEqual(dismissEntitySuggestion(null, "customers", 1), [])
|
assert.deepEqual(dismissEntitySuggestion(null, "customers", 1), [])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("uses Luna by default and omits reasoning effort for the legacy fallback", () => {
|
||||||
|
const request = buildEntitySuggestionRequest(buildSuggestionMail({ subject: "Test" }), [customer])
|
||||||
|
assert.equal(request.model, "gpt-5.6-luna")
|
||||||
|
assert.equal(request.reasoning_effort, "none")
|
||||||
|
|
||||||
|
const fallback = buildEntitySuggestionRequest(buildSuggestionMail({ subject: "Test" }), [customer], "gpt-4o-mini")
|
||||||
|
assert.equal(fallback.model, "gpt-4o-mini")
|
||||||
|
assert.equal("reasoning_effort" in fallback, false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("retries model compatibility errors without retrying auth, rate-limit, or configuration errors", () => {
|
||||||
|
for (const status of [400, 404, 422, 500, 502]) {
|
||||||
|
assert.equal(shouldRetryWithLegacyAiModel({ status }), true)
|
||||||
|
}
|
||||||
|
for (const status of [401, 403, 429, 503]) {
|
||||||
|
assert.equal(shouldRetryWithLegacyAiModel({ status }), false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ Die Analyse wird pro Mail gespeichert, auch wenn sie keine Treffer findet. Erneu
|
|||||||
|
|
||||||
## Betrieb
|
## Betrieb
|
||||||
|
|
||||||
Vor dem Einsatz die Backend-Migrationen mit `npm run migrate` im Backend-Verzeichnis ausführen. Migration `0068_email_entity_suggestions` ergänzt den Ergebnisspeicher. Migration `0069_reset_email_entity_suggestions` verwirft bisherige KI-Vorschläge, damit sie beim nächsten Öffnen ohne Empfängerbelege neu berechnet werden. Bestehende Verknüpfungen bleiben erhalten. Die Erkennung nutzt wie die bestehende Rechnungserkennung den konfigurierten zentralen KI-Dienst oder `OPENAI_API_KEY`, mit dem im Erkennungsdienst konfigurierten Modell. Ohne KI-Konfiguration erscheint eine Meldung im Postfach.
|
Vor dem Einsatz die Backend-Migrationen mit `npm run migrate` im Backend-Verzeichnis ausführen. Migration `0068_email_entity_suggestions` ergänzt den Ergebnisspeicher. Migration `0069_reset_email_entity_suggestions` verwirft bisherige KI-Vorschläge, damit sie beim nächsten Öffnen ohne Empfängerbelege neu berechnet werden. Bestehende Verknüpfungen bleiben erhalten. Die Erkennung nutzt wie die bestehende Rechnungserkennung den konfigurierten zentralen KI-Dienst oder `OPENAI_API_KEY`. Sie fragt standardmäßig `gpt-5.6-luna` an. Lehnt ein älterer zentraler Dienst dieses Modell als inkompatibel ab, wird der Aufruf einmalig mit `gpt-4o-mini` wiederholt. Ohne KI-Konfiguration erscheint eine Meldung im Postfach.
|
||||||
|
|
||||||
Empfänger- und CC-Kopfdaten werden weder für die Vorauswahl noch als KI-Eingabe verwendet. Empfängerangaben in zitierten Mailköpfen dürfen laut Analyseanweisung keine Zuordnung begründen. Übermittelt werden Betreff, Absender und bis zu 16.000 Zeichen Mailtext (ersatzweise HTML ohne Tags oder Vorschautext). Anhänge werden nicht analysiert. Stammdaten werden auf den aktuellen Mandanten und nicht archivierte Einträge begrenzt; übertragen werden Name, Typ, ID, Nummer und vorhandene E-Mail-Adressen. Bei mehr als 150 Einträgen priorisiert eine lokale Vorauswahl passende Adressen, Nummern und Namen. Dadurch können bei großen Datenbeständen rein semantische Zusammenhänge außerhalb dieser Vorauswahl unentdeckt bleiben.
|
Empfänger- und CC-Kopfdaten werden weder für die Vorauswahl noch als KI-Eingabe verwendet. Empfängerangaben in zitierten Mailköpfen dürfen laut Analyseanweisung keine Zuordnung begründen. Übermittelt werden Betreff, Absender und bis zu 16.000 Zeichen Mailtext (ersatzweise HTML ohne Tags oder Vorschautext). Anhänge werden nicht analysiert. Stammdaten werden auf den aktuellen Mandanten und nicht archivierte Einträge begrenzt; übertragen werden Name, Typ, ID, Nummer und vorhandene E-Mail-Adressen. Bei mehr als 150 Einträgen priorisiert eine lokale Vorauswahl passende Adressen, Nummern und Namen. Dadurch können bei großen Datenbeständen rein semantische Zusammenhänge außerhalb dieser Vorauswahl unentdeckt bleiben.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user