AI:n drivs av plattformen — flytta Claude-anropet till backend

- Ny edge-funktion supabase/functions/felsokning-ai: äger Claude
  API-nyckeln (serverhemligheten ANTHROPIC_API_KEY), systemprompten,
  modellvalet (claude-opus-5) och svarsschemat. Kräver inloggad
  användare (verify_jwt = true), validerar promptlängd och hanterar
  refusal/fallback på serversidan.
- Klienten anropar plattformens endpoint via functions.invoke —
  @anthropic-ai/sdk borttagen ur klientberoendena, ingen nyckel
  hanteras eller lagras någonsin i webbläsaren.
- Nyckelpanelen på startsidan ersatt med statuspanel: AI-handledning
  ingår i tjänsten; i lokalt läge (ej inloggad) guidar den
  deterministiska metodiken ensam, utan felbrus.
- Master Prompt: onboarding utan API-nyckelsteg; nytt stycke om att
  AI:n drivs av plattformen och att AI-reglerna inte kan kringgås
  från klientsidan.
- Test låser att endpointen äger systemprompt, modell och schema.

Verifierat: 25 vitest-tester gröna, produktionsbygge ok, båda
Playwright-sviterna gröna.

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 01:08:19 +00:00
parent 82b469b7e6
commit ed483774fb
11 changed files with 170 additions and 199 deletions
+114
View File
@@ -0,0 +1,114 @@
// Plattformens AI-endpoint för Guidad Felsökning.
//
// AI:n drivs av plattformen, inte av kunden: Claude API-nyckeln är en
// serverhemlighet (ANTHROPIC_API_KEY) och lämnar aldrig backend. Klienten
// skickar ärendekontexten som text; servern äger systemprompt, modellval
// och svarsschema, så AI-reglerna kan inte kringgås från klientsidan.
//
// Kräver inloggad användare (verify_jwt = true i config.toml).
import { serve } from "https://deno.land/std@0.190.0/http/server.ts";
import Anthropic from "npm:@anthropic-ai/sdk";
const corsHeaders = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers":
"authorization, x-client-info, apikey, content-type",
};
const AI_MODELL = "claude-opus-5";
const MAX_PROMPT_LANGD = 20000;
const SYSTEM_PROMPT = `Du är en digital felsökningshandledare för professionella tekniker i plattformen Guidad Felsökning. Du är inte en AI-mekaniker: du ersätter aldrig teknikerns kompetens eller tillverkarens dokumentation — du säkerställer att arbetet sker metodiskt och dokumenteras korrekt.
Absoluta regler:
- Hitta aldrig på fakta. Låtsas aldrig veta. Gissa aldrig.
- Presentera aldrig en hypotes som ett konstaterat fel.
- Skilj strikt mellan raderna du returnerar:
- "verifierat": endast det som är belagt av mätvärden eller dokumenterade kontroller i underlaget.
- "observation": det som konstaterats utan slutsats.
- "hypotes": möjlig felorsak som KRÄVER verifiering — formulera alltid vad som skulle verifiera den.
- "rekommendation": nästa verifierbara kontroll eller mätning.
- Om underlaget är otillräckligt: säg det uttryckligen i en observation och rekommendera vad som behöver dokumenteras.
Arbetssätt: en kontroll i taget, inte långa utläggningar. Svara på svenska, konsekvent, kortfattat och metodiskt — max fyra rader plus nästa steg. "nastaSteg" är EN konkret, verifierbar åtgärd.`;
const SVARS_SCHEMA = {
type: "object",
properties: {
rader: {
type: "array",
items: {
type: "object",
properties: {
typ: { type: "string", enum: ["observation", "verifierat", "hypotes", "rekommendation"] },
text: { type: "string" },
},
required: ["typ", "text"],
additionalProperties: false,
},
},
nastaSteg: { type: "string" },
},
required: ["rader", "nastaSteg"],
additionalProperties: false,
};
serve(async (req: Request): Promise<Response> => {
if (req.method === "OPTIONS") {
return new Response(null, { headers: corsHeaders });
}
try {
const apiNyckel = Deno.env.get("ANTHROPIC_API_KEY");
if (!apiNyckel) {
return svara(503, { error: "AI-tjänsten är inte konfigurerad." });
}
const { prompt } = await req.json();
if (typeof prompt !== "string" || prompt.trim().length === 0) {
return svara(400, { error: "prompt saknas." });
}
if (prompt.length > MAX_PROMPT_LANGD) {
return svara(400, { error: "prompt är för lång." });
}
const klient = new Anthropic({ apiKey: apiNyckel });
const svar = await klient.beta.messages.create({
model: AI_MODELL,
max_tokens: 1024,
// Verkstadsgolvet är latenskänsligt och uppgiften väl avgränsad
// av ärendebriefen — medium balanserar svarstid mot kvalitet.
output_config: {
effort: "medium",
format: { type: "json_schema", schema: SVARS_SCHEMA },
},
// Avböjer säkerhetsklassificerarna faller anropet automatiskt
// tillbaka till Anthropics rekommenderade reservmodell.
betas: ["server-side-fallback-2026-07-01"],
fallbacks: "default",
system: [{ type: "text", text: SYSTEM_PROMPT, cache_control: { type: "ephemeral" } }],
messages: [{ role: "user", content: prompt }],
// deno-lint-ignore no-explicit-any -- fallbacks/output_config ligger före SDK-typerna
} as any);
if (svar.stop_reason === "refusal") {
return svara(502, { error: "AI-tjänsten avböjde förfrågan." });
}
const textBlock = svar.content.find((block: { type: string }) => block.type === "text");
if (!textBlock) {
return svara(502, { error: "AI-svaret saknade innehåll." });
}
return new Response((textBlock as { text: string }).text, {
headers: { ...corsHeaders, "Content-Type": "application/json" },
});
} catch (fel) {
console.error("felsokning-ai:", fel);
return svara(500, { error: "AI-anropet misslyckades." });
}
});
function svara(status: number, kropp: object): Response {
return new Response(JSON.stringify(kropp), {
status,
headers: { ...corsHeaders, "Content-Type": "application/json" },
});
}