Ärendestart via arbetsorderskanning med konfidensgranskning
Primärvägen när ett ärende startas: teknikern fotar arbetsorderns framsida, orkesterns nya dokumenttolkningsuppgift (Sonnet 5, vision) läser dokumentet layoutoberoende och returnerar strukturerade fält (kund, fordon, verkstad, felbeskrivning) med konfidens per värde. - Konfidensvalidering: ≥95 % godkänns automatiskt, 80–95 % markeras för genomläsning, <80 % kräver aktiv bekräftelse — teknikern granskar bara osäkra fält - Visuell granskning: dokumentet bredvid fälten; klick på ett fält markerar ungefärlig position i bilden - "Starta diagnos" skapar hela ärendet: objekt ur fordonsfälten, metodikval ur felbeskrivningen, tolkningen loggad som ny organisationsintern händelse arbetsorder_skannad (filtreras ur kund- och partnerdelningar i server, RPC och klient) - Schema-bunden vision-uppgift i båda orkestertjänsterna; bild som data-URL, validerad på servern - Inloggade användare tillfrågas aldrig om namn — kontot används - Manuell inmatning kvar som andrahandsväg; tydligt märkt demo-tolkning i lokalt läge - 37 vitest-tester; integrationstestet verifierar filtreringen Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012EQg3rJsrQ1ZNTvkzmQAtt
This commit is contained in:
@@ -17,7 +17,9 @@ import Anthropic from "@anthropic-ai/sdk";
|
||||
|
||||
const PORT = Number(process.env.PORT ?? 8080);
|
||||
const MAX_PROMPT_LANGD = 40000;
|
||||
const MAX_KROPP = 256 * 1024;
|
||||
// Rymmer en nedskalad arbetsorderbild som data-URL.
|
||||
const MAX_KROPP = 4 * 1024 * 1024;
|
||||
const MAX_BILD_LANGD = 3_000_000;
|
||||
|
||||
const GRUND_REGLER = `Du arbetar i Guidad Felsökning, en professionell diagnostikplattform för tekniker. Du är inte en AI-mekaniker: du ersätter aldrig teknikerns kompetens eller tillverkarens dokumentation.
|
||||
|
||||
@@ -62,6 +64,58 @@ const METODIK_SCHEMA = {
|
||||
additionalProperties: false,
|
||||
};
|
||||
|
||||
// Fält som kan tolkas ur en skannad arbetsorder (samma katalog som
|
||||
// edge-funktionen och klienten).
|
||||
const ARBETSORDER_FALT_ID = [
|
||||
"kund_namn", "kund_foretag", "kund_telefon", "kund_epost",
|
||||
"fordon_regnr", "fordon_vin", "fordon_marke", "fordon_modell",
|
||||
"fordon_motor", "fordon_arsmodell", "fordon_matarstallning",
|
||||
"fordon_motorkod", "fordon_vaxellada",
|
||||
"ao_nummer", "ao_referens", "ao_serviceradgivare", "ao_bokningsdatum",
|
||||
"felbeskrivning",
|
||||
];
|
||||
|
||||
const ARBETSORDER_SCHEMA = {
|
||||
type: "object",
|
||||
properties: {
|
||||
falt: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: { type: "string", enum: ARBETSORDER_FALT_ID },
|
||||
varde: { type: "string" },
|
||||
konfidens: { type: "number", minimum: 0, maximum: 1 },
|
||||
omrade: {
|
||||
type: "object",
|
||||
properties: {
|
||||
x: { type: "number" },
|
||||
y: { type: "number" },
|
||||
bredd: { type: "number" },
|
||||
hojd: { type: "number" },
|
||||
},
|
||||
required: ["x", "y", "bredd", "hojd"],
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
required: ["id", "varde", "konfidens"],
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
required: ["falt"],
|
||||
additionalProperties: false,
|
||||
};
|
||||
|
||||
const DOKUMENT_REGLER = `Du tolkar ett foto av en arbetsorder från en fordonsverkstad (OCR + layoutförståelse). Layouter varierar mellan verkstäder — identifiera fälten oavsett var de står, i tabeller såväl som fritext.
|
||||
|
||||
Regler:
|
||||
- Returnera ENDAST fält som faktiskt går att läsa i dokumentet. Hitta aldrig på värden.
|
||||
- "konfidens" är din läs-säkerhet 0–1: 1.0 endast vid helt entydig läsning; sänk vid oskarp text, handstil eller tvetydig layout.
|
||||
- "omrade" anger ungefär var värdet står i bilden, normaliserat 0–1 (x, y = övre vänstra hörnet).
|
||||
- Normalisera: registreringsnummer i versaler utan mellanslag, VIN med 17 tecken, datum som ÅÅÅÅ-MM-DD, mätarställning med enhet.
|
||||
- "felbeskrivning" är kundens beskrivna problem/arbetsbegäran om en sådan finns i dokumentet.`;
|
||||
|
||||
// Orkestern: en modell per uppgiftstyp (samma routing som edge-funktionen).
|
||||
const ORKESTER = {
|
||||
handledning: {
|
||||
@@ -95,6 +149,16 @@ const ORKESTER = {
|
||||
Gissa inte: välj "generisk" om beskrivningen inte tydligt hör till en specifik metodik.`,
|
||||
schema: METODIK_SCHEMA,
|
||||
},
|
||||
// Ärendestart: teknikern fotograferar arbetsordern; vi läser dokumentet
|
||||
// och returnerar strukturerade fält med konfidens per värde.
|
||||
dokumenttolkning: {
|
||||
modell: "claude-sonnet-5",
|
||||
effort: "low",
|
||||
maxTokens: 2048,
|
||||
system: DOKUMENT_REGLER,
|
||||
schema: ARBETSORDER_SCHEMA,
|
||||
bild: true,
|
||||
},
|
||||
};
|
||||
|
||||
// Minimal HS256-JWT-verifiering (Supabase legacy JWT secret) utan beroenden.
|
||||
@@ -169,9 +233,9 @@ export function skapaServer() {
|
||||
return svara(res, 401, { error: "Inloggning krävs." });
|
||||
}
|
||||
|
||||
let uppgift, prompt;
|
||||
let uppgift, prompt, bild;
|
||||
try {
|
||||
({ uppgift, prompt } = await lasKropp(req));
|
||||
({ uppgift, prompt, bild } = await lasKropp(req));
|
||||
} catch {
|
||||
return svara(res, 400, { error: "Ogiltig förfrågan." });
|
||||
}
|
||||
@@ -184,6 +248,20 @@ export function skapaServer() {
|
||||
return svara(res, 400, { error: "prompt är för lång." });
|
||||
}
|
||||
|
||||
// Bilduppgifter: data-URL → image-block före textprompten.
|
||||
let innehall = prompt;
|
||||
if (konfig.bild) {
|
||||
const matchning =
|
||||
typeof bild === "string" && bild.length <= MAX_BILD_LANGD
|
||||
? bild.match(/^data:(image\/(?:jpeg|png|webp));base64,([A-Za-z0-9+/=]+)$/)
|
||||
: null;
|
||||
if (!matchning) return svara(res, 400, { error: "bild saknas eller har fel format." });
|
||||
innehall = [
|
||||
{ type: "image", source: { type: "base64", media_type: matchning[1], data: matchning[2] } },
|
||||
{ type: "text", text: prompt },
|
||||
];
|
||||
}
|
||||
|
||||
try {
|
||||
const klient = new Anthropic({ apiKey: apiNyckel });
|
||||
const svar = await klient.beta.messages.create({
|
||||
@@ -196,7 +274,7 @@ export function skapaServer() {
|
||||
betas: ["server-side-fallback-2026-07-01"],
|
||||
fallbacks: "default",
|
||||
system: [{ type: "text", text: konfig.system, cache_control: { type: "ephemeral" } }],
|
||||
messages: [{ role: "user", content: prompt }],
|
||||
messages: [{ role: "user", content: innehall }],
|
||||
});
|
||||
if (svar.stop_reason === "refusal") {
|
||||
return svara(res, 502, { error: "AI-tjänsten avböjde förfrågan." });
|
||||
|
||||
Reference in New Issue
Block a user