ed483774fb
- 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
115 lines
4.6 KiB
TypeScript
115 lines
4.6 KiB
TypeScript
// 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" },
|
|
});
|
|
}
|