Ä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:
Claude
2026-08-03 08:54:53 +00:00
parent c3e831b478
commit a197ee10ca
15 changed files with 605 additions and 33 deletions
+82 -4
View File
@@ -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 01: 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 01 (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." });