From ed483774fb150c147b0c0fcbc129f2cd88255964 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 3 Aug 2026 01:08:19 +0000 Subject: [PATCH] =?UTF-8?q?AI:n=20drivs=20av=20plattformen=20=E2=80=94=20f?= =?UTF-8?q?lytta=20Claude-anropet=20till=20backend?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 Claude-Session: https://claude.ai/code/session_012EQg3rJsrQ1ZNTvkzmQAtt --- docs/MASTER-PROMPT.md | 9 +- docs/MVP.md | 2 +- package-lock.json | 63 ------------ package.json | 1 - src/felsokning/__tests__/ai.test.ts | 18 ++-- src/felsokning/ai.ts | 95 ++++-------------- src/felsokning/store.ts | 6 -- src/pages/felsokning/ArendeSida.tsx | 17 ++-- src/pages/felsokning/Arendelista.tsx | 41 ++------ supabase/config.toml | 3 + supabase/functions/felsokning-ai/index.ts | 114 ++++++++++++++++++++++ 11 files changed, 170 insertions(+), 199 deletions(-) create mode 100644 supabase/functions/felsokning-ai/index.ts diff --git a/docs/MASTER-PROMPT.md b/docs/MASTER-PROMPT.md index 377c476..cc035d6 100644 --- a/docs/MASTER-PROMPT.md +++ b/docs/MASTER-PROMPT.md @@ -10,7 +10,9 @@ Det här är ett produktdirektiv, inte en teknisk specifikation. Visionen och re Bygg en produktionsredo SaaS-plattform med namnet **Guidad Felsökning**. -Plattformen är en AI-wrapper ovanpå Claude API (Anthropic) och fungerar som ett professionellt arbetsverktyg för mekaniker och servicetekniker. Standardmodell: Claude Opus 5 (`claude-opus-5`), med Anthropics rekommenderade reservmodell som automatisk fallback när en förfrågan avböjs av säkerhetsklassificerarna. Den ska inte ersätta teknisk kompetens eller tillverkarens dokumentation, utan vägleda användaren genom en strukturerad felsökningsprocess, dokumentera allt arbete och skapa full spårbarhet. +Plattformen är en AI-wrapper ovanpå Claude API (Anthropic) och fungerar som ett professionellt arbetsverktyg för mekaniker och servicetekniker. Standardmodell: Claude Opus 5 (`claude-opus-5`), med Anthropics rekommenderade reservmodell som automatisk fallback när en förfrågan avböjs av säkerhetsklassificerarna. + +**AI:n drivs av plattformen, inte av kunden.** Claude API-nycklarna är plattformshemligheter i backend och exponeras aldrig för kunder eller klienter — AI-handledningen ingår i tjänsten. Backend äger systemprompt, modellval och svarsschema, så AI-reglerna kan inte kringgås från klientsidan. Den ska inte ersätta teknisk kompetens eller tillverkarens dokumentation, utan vägleda användaren genom en strukturerad felsökningsprocess, dokumentera allt arbete och skapa full spårbarhet. Målet är att lansera en stabil, enkel och köpvärdig beta-version. @@ -53,10 +55,9 @@ Första gången en kund loggar in: 1. Skapa företag 2. Lägg till logotyp 3. Lägg till användare -4. Lägg till Claude API-nyckel -5. Lägg till eventuella integrationer +4. Lägg till eventuella integrationer -Klart. Hela onboarding ska ta mindre än fem minuter. +Klart. Hela onboarding ska ta mindre än fem minuter. AI-handledningen ingår i tjänsten — kunden hanterar inga AI-nycklar. --- diff --git a/docs/MVP.md b/docs/MVP.md index 3666ebb..35dd2a7 100644 --- a/docs/MVP.md +++ b/docs/MVP.md @@ -17,7 +17,7 @@ Demomanus för visning: [DEMO.md](DEMO.md). Knappen **Skapa demoärende** på st | Direktivets kärna | Status i MVP | | --- | --- | | Objektidentifiering först | ✅ Manuell inmatning (reg.nr/VIN/serienummer/maskinnummer) med bekräftelsesteg. QR/streckkod/OCR är markerade som kommande. | -| AI-guidad felsökning | ✅ Deterministisk metodikmotor (en fråga i taget, tre metodiker) **plus Claude-handledning**: med organisationens Claude API-nyckel (läggs in på startsidan) svarar Claude Opus 5 skriftligt på varje bekräftad dokumentation — klassificerat som observation/verifierat/hypotes/rekommendation enligt AI-reglerna, schema-bundet så svaret aldrig kan läsas som fritext, med automatisk fallback till Anthropics rekommenderade reservmodell vid avböjd förfrågan. Svaren loggas som händelser (interna — delas aldrig i kundvyer). Utan nyckel guidar metodiken ensam. MVP: nyckeln lagras på enheten och anropet görs från webbläsaren; i produktion går det via backend. | +| AI-guidad felsökning | ✅ Deterministisk metodikmotor (en fråga i taget, tre metodiker) **plus Claude-handledning driven av plattformen**: edge-funktionen `felsokning-ai` äger Claude API-nyckeln (serverhemligheten `ANTHROPIC_API_KEY`), systemprompten, modellen (Claude Opus 5) och svarsschemat — kunden hanterar inga AI-nycklar. Claude svarar på varje bekräftad dokumentation, klassificerat som observation/verifierat/hypotes/rekommendation enligt AI-reglerna, schema-bundet, med automatisk fallback till Anthropics rekommenderade reservmodell vid avböjd förfrågan. Kräver inloggad användare; svaren loggas som händelser (interna — delas aldrig i kundvyer). I lokalt läge guidar metodiken ensam. | | Arbetslogg | ✅ Append-only händelselogg med tidsstämpel och användare på varje post. Ingenting skrivs över. | | Tidredovisning | ✅ Kategorier (aktiv felsökning, väntetid, provkörning …) via kategoribyten i loggen; paus räknas inte i total tid. Inaktivitetsfråga efter 20 min utan händelser. | | Dokumentation | ✅ Observationer, mätvärden, foton (nedskalade), kommentarer och hypoteser. Hypoteser märks alltid 🔴 och kan aldrig loggas som konstaterade fel. | diff --git a/package-lock.json b/package-lock.json index 7183894..04c0e94 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,6 @@ "name": "vite_react_shadcn_ts", "version": "0.0.0", "dependencies": { - "@anthropic-ai/sdk": "^0.115.0", "@hookform/resolvers": "^3.10.0", "@radix-ui/react-accordion": "^1.2.11", "@radix-ui/react-alert-dialog": "^1.1.14", @@ -106,27 +105,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@anthropic-ai/sdk": { - "version": "0.115.0", - "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.115.0.tgz", - "integrity": "sha512-BJrFIVyjNuU8lfDyIJTvlRYzgQg+zEl78BxE7fq8esULsGz9IRQvGtW5spq3tydmtjQb/GFdooKGdGsetpx+lQ==", - "license": "MIT", - "dependencies": { - "json-schema-to-ts": "^3.1.1", - "standardwebhooks": "^1.0.0" - }, - "bin": { - "anthropic-ai-sdk": "bin/cli" - }, - "peerDependencies": { - "zod": "^3.25.0 || ^4.0.0" - }, - "peerDependenciesMeta": { - "zod": { - "optional": true - } - } - }, "node_modules/@babel/code-frame": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", @@ -2559,12 +2537,6 @@ "win32" ] }, - "node_modules/@stablelib/base64": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz", - "integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==", - "license": "MIT" - }, "node_modules/@stripe/react-stripe-js": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/@stripe/react-stripe-js/-/react-stripe-js-6.2.0.tgz", @@ -4943,12 +4915,6 @@ "dev": true, "license": "MIT" }, - "node_modules/fast-sha256": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz", - "integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==", - "license": "Unlicense" - }, "node_modules/fastq": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", @@ -5602,19 +5568,6 @@ "dev": true, "license": "MIT" }, - "node_modules/json-schema-to-ts": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz", - "integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.18.3", - "ts-algebra": "^2.0.0" - }, - "engines": { - "node": ">=16" - } - }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -7365,16 +7318,6 @@ "dev": true, "license": "MIT" }, - "node_modules/standardwebhooks": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/standardwebhooks/-/standardwebhooks-1.0.0.tgz", - "integrity": "sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==", - "license": "MIT", - "dependencies": { - "@stablelib/base64": "^1.0.0", - "fast-sha256": "^1.3.0" - } - }, "node_modules/std-env": { "version": "3.10.0", "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", @@ -7763,12 +7706,6 @@ "node": ">=12" } }, - "node_modules/ts-algebra": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz", - "integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==", - "license": "MIT" - }, "node_modules/ts-api-utils": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", diff --git a/package.json b/package.json index a560c4c..e27141f 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,6 @@ "test:watch": "vitest" }, "dependencies": { - "@anthropic-ai/sdk": "^0.115.0", "@hookform/resolvers": "^3.10.0", "@radix-ui/react-accordion": "^1.2.11", "@radix-ui/react-alert-dialog": "^1.1.14", diff --git a/src/felsokning/__tests__/ai.test.ts b/src/felsokning/__tests__/ai.test.ts index f0fe551..c89dd08 100644 --- a/src/felsokning/__tests__/ai.test.ts +++ b/src/felsokning/__tests__/ai.test.ts @@ -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", () => { diff --git a/src/felsokning/ai.ts b/src/felsokning/ai.ts index c3de370..15cc995 100644 --- a/src/felsokning/ai.ts +++ b/src/felsokning/ai.ts @@ -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 { - 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[0]); +): Promise { + 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); } diff --git a/src/felsokning/store.ts b/src/felsokning/store.ts index 885e752..200f76c 100644 --- a/src/felsokning/store.ts +++ b/src/felsokning/store.ts @@ -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; 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()( 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(); diff --git a/src/pages/felsokning/ArendeSida.tsx b/src/pages/felsokning/ArendeSida.tsx index 09a3295..6ed76fa 100644 --- a/src/pages/felsokning/ArendeSida.tsx +++ b/src/pages/felsokning/ArendeSida.tsx @@ -185,18 +185,19 @@ function GuideFlik({ }) { const steg = useMemo(() => nastaSteg(arende, metodik), [arende, metodik]); const [visaOverlamning, setVisaOverlamning] = useState(false); - const aiNyckel = useFelsokning((s) => s.aiNyckel); const [aiStatus, setAiStatus] = useState<"vilar" | "arbetar" | "fel">("vilar"); - // AI:n svarar skriftligt på varje bekräftad dokumentation: klassificerat - // (observation/verifierat/hypotes/rekommendation) och loggat som händelse. - // Utan nyckel guidar den deterministiska metodiken ensam. + // AI:n drivs av plattformen och svarar skriftligt på varje bekräftad + // dokumentation: klassificerat (observation/verifierat/hypotes/ + // rekommendation) och loggat som händelse. I lokalt läge (ej inloggad) + // guidar den deterministiska metodiken ensam. const fragaAiOmDokumentation = async (inmatning: string) => { - if (!aiNyckel) return; setAiStatus("arbetar"); try { - const svar = await fragaAi(aiNyckel, brief(arende, metodik, nu), metodik.namn, inmatning); - skicka({ typ: "ai_svar", rader: svar.rader, nastaSteg: svar.nastaSteg, modell: AI_MODELL }); + const svar = await fragaAi(brief(arende, metodik, nu), metodik.namn, inmatning); + if (svar) { + skicka({ typ: "ai_svar", rader: svar.rader, nastaSteg: svar.nastaSteg, modell: AI_MODELL }); + } setAiStatus("vilar"); } catch { setAiStatus("fel"); @@ -245,7 +246,7 @@ function GuideFlik({ > {aiStatus === "arbetar" &&

AI analyserar …

} {aiStatus === "fel" && ( -

AI-svaret kunde inte hämtas — kontrollera nyckel och nät. Metodiken fortsätter som vanligt.

+

AI-svaret kunde inte hämtas — försök igen. Metodiken fortsätter som vanligt.

)} {senasteAiSvar && senasteAiSvar.handelse.typ === "ai_svar" && ( <> diff --git a/src/pages/felsokning/Arendelista.tsx b/src/pages/felsokning/Arendelista.tsx index ec8c607..bf0cb18 100644 --- a/src/pages/felsokning/Arendelista.tsx +++ b/src/pages/felsokning/Arendelista.tsx @@ -12,11 +12,9 @@ type Filter = "alla" | "pagaende" | "klara"; // Dashboard enligt direktivet: endast det viktigaste — mina ärenden, // pågående, klara och starta nytt ärende. export default function Arendelista() { - const { anvandare, arenden, sattAnvandare, laggInArende, nastaNummer, aiNyckel, sattAiNyckel } = useFelsokning(); + const { anvandare, arenden, sattAnvandare, laggInArende, nastaNummer } = useFelsokning(); const [namn, setNamn] = useState(""); const [filter, setFilter] = useState("alla"); - const [visaAi, setVisaAi] = useState(false); - const [nyckel, setNyckel] = useState(""); if (!anvandare) { return ( @@ -94,39 +92,12 @@ export default function Arendelista() { )} -

- {aiNyckel - ? `Aktiv — Claude (${AI_MODELL}) svarar klassificerat på dokumenterade observationer och mätvärden.` - : "Inte aktiverad — den deterministiska metodiken guidar. Lägg till organisationens Claude API-nyckel för AI-handledning."} +

+ Ingår i tjänsten och drivs av plattformen — Claude ({AI_MODELL}) svarar klassificerat på + dokumenterade observationer och mätvärden. Anropen går via plattformens backend; inga AI-nycklar + hanteras av verkstaden. Aktiveras automatiskt för inloggade användare; i lokalt läge guidar den + deterministiska metodiken ensam.

- {!visaAi ? ( - setVisaAi(true)}> - {aiNyckel ? "Ändra API-nyckel" : "Lägg till Claude API-nyckel"} - - ) : ( - <> - -

- MVP: nyckeln sparas endast på den här enheten och anropen görs direkt från webbläsaren. I - produktionsversionen hanteras nycklar av plattformens backend. -

-
- { setVisaAi(false); setNyckel(""); }}> - Avbryt - - { - sattAiNyckel(nyckel); - setVisaAi(false); - setNyckel(""); - }} - > - {nyckel.trim() ? "Spara nyckel" : "Ta bort nyckel"} - -
- - )}
{lista.map((arende) => { diff --git a/supabase/config.toml b/supabase/config.toml index c6cfd2c..c8fe74d 100644 --- a/supabase/config.toml +++ b/supabase/config.toml @@ -26,3 +26,6 @@ verify_jwt = false [functions.update-subscription-quantity] verify_jwt = false + +[functions.felsokning-ai] +verify_jwt = true diff --git a/supabase/functions/felsokning-ai/index.ts b/supabase/functions/felsokning-ai/index.ts new file mode 100644 index 0000000..65cc045 --- /dev/null +++ b/supabase/functions/felsokning-ai/index.ts @@ -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 => { + 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" }, + }); +}