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:
@@ -1,5 +1,6 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { AI_MODELL, SYSTEM_PROMPT, byggAnvandarPrompt, tolkaAiSvar } from "../ai";
|
||||
import { AI_MODELL, byggAnvandarPrompt, tolkaAiSvar } from "../ai";
|
||||
import { VIBRATION_METODIK } from "../metodik";
|
||||
import { brief } from "../projektioner";
|
||||
import { byggDemoArende } from "../demo";
|
||||
@@ -9,11 +10,16 @@ describe("AI-motorn", () => {
|
||||
expect(AI_MODELL).toBe("claude-opus-5");
|
||||
});
|
||||
|
||||
it("systemprompten kodar AI-reglerna", () => {
|
||||
expect(SYSTEM_PROMPT).toContain("Hitta aldrig på fakta");
|
||||
expect(SYSTEM_PROMPT).toContain("aldrig en hypotes som ett konstaterat fel");
|
||||
expect(SYSTEM_PROMPT).toContain("KRÄVER verifiering");
|
||||
expect(SYSTEM_PROMPT).toContain("kortfattat");
|
||||
it("plattformens AI-endpoint äger systemprompten och kodar AI-reglerna", () => {
|
||||
// AI:n drivs av plattformen: systemprompt, modell och schema ligger i
|
||||
// backend (edge-funktionen), inte i klienten.
|
||||
const endpoint = readFileSync("supabase/functions/felsokning-ai/index.ts", "utf8");
|
||||
expect(endpoint).toContain('const AI_MODELL = "claude-opus-5"');
|
||||
expect(endpoint).toContain("Hitta aldrig på fakta");
|
||||
expect(endpoint).toContain("aldrig en hypotes som ett konstaterat fel");
|
||||
expect(endpoint).toContain("KRÄVER verifiering");
|
||||
expect(endpoint).toContain("ANTHROPIC_API_KEY");
|
||||
expect(endpoint).toContain('"json_schema"');
|
||||
});
|
||||
|
||||
it("användarprompten byggs ur ärendebriefen och den nya inmatningen", () => {
|
||||
|
||||
+20
-75
@@ -1,17 +1,15 @@
|
||||
// AI-motor: Claude som diagnostikhandledare.
|
||||
// AI-handledning: Claude, driven av plattformen.
|
||||
//
|
||||
// Kommunikationsmodellen är text in, text ut: teknikerns (bekräftade)
|
||||
// inmatning skickas tillsammans med ärendebriefen, och Claude svarar
|
||||
// kortfattat och klassificerat enligt AI-reglerna i Master Prompt —
|
||||
// observation / verifierat / hypotes / rekommendation. Svaret är
|
||||
// schema-bundet (structured outputs), så det kan aldrig komma tillbaka
|
||||
// som fritext som råkar läsas som ett konstaterat fel.
|
||||
// AI:n hostas av tjänsten — inte av kunden. Klienten skickar ärendekontexten
|
||||
// till plattformens backend (edge-funktionen felsokning-ai), som äger
|
||||
// Claude API-nyckeln, systemprompten, modellvalet och svarsschemat.
|
||||
// Inga AI-nycklar hanteras eller lagras någonsin i klienten.
|
||||
//
|
||||
// MVP: tenantens egen API-nyckel används direkt från klienten
|
||||
// (dangerouslyAllowBrowser). I produktionsversionen flyttas anropet
|
||||
// bakom plattformens backend så att nycklar aldrig lämnar servern.
|
||||
// Kommunikationsmodellen är text in, text ut: teknikerns bekräftade
|
||||
// inmatning skickas med ärendebriefen, och svaret kommer tillbaka strikt
|
||||
// klassificerat enligt AI-reglerna — observation / verifierat / hypotes /
|
||||
// rekommendation — plus ETT konkret nästa steg.
|
||||
|
||||
import Anthropic from "@anthropic-ai/sdk";
|
||||
import type { Brief } from "./projektioner";
|
||||
|
||||
export const AI_MODELL = "claude-opus-5";
|
||||
@@ -35,43 +33,6 @@ export interface AiSvar {
|
||||
nastaSteg: string;
|
||||
}
|
||||
|
||||
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,
|
||||
} as const;
|
||||
|
||||
// Fryst systemprompt (cachas mellan anrop) — all ärendespecifik kontext
|
||||
// ligger i användarmeddelandet.
|
||||
export 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.`;
|
||||
|
||||
export function byggAnvandarPrompt(brief: Brief, metodikNamn: string, inmatning: string): string {
|
||||
const rader: string[] = [];
|
||||
rader.push(`Metodik: ${metodikNamn}`);
|
||||
@@ -105,36 +66,20 @@ export function tolkaAiSvar(data: unknown): AiSvar {
|
||||
return { rader, nastaSteg: svar.nastaSteg };
|
||||
}
|
||||
|
||||
// Returnerar null i lokalt läge (ej inloggad) — AI:n är en del av tjänsten
|
||||
// och nås via plattformens autentiserade backend.
|
||||
export async function fragaAi(
|
||||
apiNyckel: string,
|
||||
brief: Brief,
|
||||
metodikNamn: string,
|
||||
inmatning: string,
|
||||
): Promise<AiSvar> {
|
||||
const klient = new Anthropic({ apiKey: apiNyckel, dangerouslyAllowBrowser: true });
|
||||
const svar = await klient.beta.messages.create({
|
||||
model: AI_MODELL,
|
||||
max_tokens: 1024,
|
||||
// Verkstadsgolvet är latenskänsligt och uppgiften är väl avgränsad
|
||||
// av briefen — 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: byggAnvandarPrompt(brief, metodikNamn, inmatning) }],
|
||||
} as Parameters<typeof klient.beta.messages.create>[0]);
|
||||
): Promise<AiSvar | null> {
|
||||
const { supabase } = await import("@/integrations/supabase/client");
|
||||
const { data } = await supabase.auth.getSession();
|
||||
if (!data.session) return null;
|
||||
|
||||
if (svar.stop_reason === "refusal") {
|
||||
throw new Error("AI-tjänsten avböjde förfrågan.");
|
||||
}
|
||||
const textBlock = svar.content.find((block) => block.type === "text");
|
||||
if (!textBlock || textBlock.type !== "text") {
|
||||
throw new Error("AI-svaret saknade innehåll.");
|
||||
}
|
||||
return tolkaAiSvar(JSON.parse(textBlock.text));
|
||||
const { data: svar, error } = await supabase.functions.invoke("felsokning-ai", {
|
||||
body: { prompt: byggAnvandarPrompt(brief, metodikNamn, inmatning) },
|
||||
});
|
||||
if (error) throw error;
|
||||
return tolkaAiSvar(svar);
|
||||
}
|
||||
|
||||
@@ -12,13 +12,9 @@ import { GENERISK_METODIK, valjMetodik, type Metodik } from "./metodik";
|
||||
|
||||
interface FelsokningState {
|
||||
anvandare: string;
|
||||
// Tenantens Claude API-nyckel (MVP: lagras endast på enheten; i
|
||||
// produktionsversionen hanteras nycklar av backend, aldrig i klienten).
|
||||
aiNyckel: string;
|
||||
arenden: Record<string, Arende>;
|
||||
nastaNummer: number;
|
||||
sattAnvandare: (namn: string) => void;
|
||||
sattAiNyckel: (nyckel: string) => void;
|
||||
skapaArende: (objekt: Objekt, felbeskrivningText: string) => string;
|
||||
laggTill: (arendeId: string, handelse: Handelse) => void;
|
||||
// Används endast av synken: ersätter listan med den ihopflätade versionen.
|
||||
@@ -38,12 +34,10 @@ export const useFelsokning = create<FelsokningState>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
anvandare: "",
|
||||
aiNyckel: "",
|
||||
arenden: {},
|
||||
nastaNummer: 1,
|
||||
|
||||
sattAnvandare: (namn) => set({ anvandare: namn.trim() }),
|
||||
sattAiNyckel: (nyckel) => set({ aiNyckel: nyckel.trim() }),
|
||||
|
||||
skapaArende: (objekt, felbeskrivningText) => {
|
||||
const { anvandare, nastaNummer } = get();
|
||||
|
||||
Reference in New Issue
Block a user