Basis: Certvia dev@a48c5fb als Fundament für Craftvia
Unveränderter Stand von certvia/dev (a48c5fb) plus Craftvia-Spezifikation und Brandbook unter docs/craftvia/. ISMS-Module werden im Folgecommit entfernt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
import Anthropic from "@anthropic-ai/sdk";
|
||||
|
||||
/**
|
||||
* Zentrale, gekapselte LLM-Anbindung (Anthropic). Erste und derzeit einzige
|
||||
* Nutzung: KI-Entwurf der Umsetzungsbeschreibungen (VDA-ISA-ABGABE) im
|
||||
* Audit-Wizard. Bewusst als Singleton mit lazy-Init, damit ein fehlender
|
||||
* API-Key den restlichen Serverstart nicht beeinträchtigt.
|
||||
*
|
||||
* Konfiguration ausschließlich über Umgebungsvariablen:
|
||||
* - ANTHROPIC_API_KEY Pflicht für den KI-Betrieb. Fehlt er, ist die
|
||||
* KI-Anbindung deaktiviert (graceful degradation):
|
||||
* `getAnthropic()` liefert `null`, Aufrufer fallen auf
|
||||
* „manuell zu erfassen" zurück.
|
||||
* - ANTHROPIC_MODEL Optionale Modell-ID; Default `claude-opus-5`.
|
||||
*
|
||||
* Das offizielle SDK liest `ANTHROPIC_API_KEY` selbst aus der Umgebung; wir
|
||||
* prüfen die Existenz dennoch explizit, um den Client nur bei vorhandenem Key
|
||||
* zu instanziieren und die Deaktivierung eindeutig zu machen.
|
||||
*/
|
||||
|
||||
const DEFAULT_MODEL = "claude-opus-5";
|
||||
|
||||
/** Modell-ID für KI-Aufrufe (ENV-Override, sonst Default). */
|
||||
export const AI_MODEL = process.env.ANTHROPIC_MODEL?.trim() || DEFAULT_MODEL;
|
||||
|
||||
let client: Anthropic | null | undefined;
|
||||
|
||||
/** True, wenn ein API-Key gesetzt ist und die KI-Anbindung genutzt werden kann. */
|
||||
export function isAiConfigured(): boolean {
|
||||
return Boolean(process.env.ANTHROPIC_API_KEY?.trim());
|
||||
}
|
||||
|
||||
/**
|
||||
* Liefert den (gecachten) Anthropic-Client oder `null`, wenn kein API-Key
|
||||
* konfiguriert ist. Aufrufer müssen den `null`-Fall behandeln (kein Entwurf).
|
||||
*/
|
||||
export function getAnthropic(): Anthropic | null {
|
||||
if (client !== undefined) return client;
|
||||
client = isAiConfigured() ? new Anthropic() : null;
|
||||
return client;
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
import { getAnthropic, AI_MODEL } from "./client";
|
||||
|
||||
/**
|
||||
* KI-Entwurf einer „Beschreibung der Umsetzung" je ISA-Teilanforderung im
|
||||
* VDA-ISA-ABGABE-Stil. Aus dem Anforderungstext (MUSS/SOLL) sowie den
|
||||
* verknüpften Dokumenten/Nachweisen wird EIN konkreter „wie-umgesetzt"-Satz mit
|
||||
* Dokumentverweis formuliert. Die Konfidenz wird deterministisch aus der
|
||||
* Quellenlage abgeleitet (nicht vom Modell), damit sie nachvollziehbar bleibt.
|
||||
*
|
||||
* Graceful degradation: Ohne konfigurierten API-Key (oder bei einem Fehler des
|
||||
* Aufrufs) liefert `draftControlDescription` `null` — der Aufrufer belässt den
|
||||
* Status dann bei „manuell zu erfassen".
|
||||
*/
|
||||
|
||||
export interface DraftDocument {
|
||||
code: string;
|
||||
title: string;
|
||||
version: string;
|
||||
/** PolicyStatus (FREIGEGEBEN = validiert). */
|
||||
status: string;
|
||||
}
|
||||
|
||||
export interface DraftInput {
|
||||
control: string;
|
||||
reqId: string;
|
||||
/** MUSS | SOLL */
|
||||
obligation: string;
|
||||
/** Anforderungstext (PolicyRequirement.requirement). */
|
||||
requirement: string;
|
||||
/** Umsetzungshinweis (PolicyRequirement.implementation). */
|
||||
implementation: string;
|
||||
/** Verknüpfte Richtlinien/Verfahren. */
|
||||
documents: DraftDocument[];
|
||||
/** Titel vorhandener Nachweise (Evidence) zum Control. */
|
||||
evidence: string[];
|
||||
}
|
||||
|
||||
export interface DraftResult {
|
||||
/** Umsetzungssatz inkl. Dokumentverweis (ABGABE-Stil). */
|
||||
draftText: string;
|
||||
/** Kompakter Quellenverweis (Dokumentliste), semikolon-getrennt. */
|
||||
sourceRef: string | null;
|
||||
confidence: "high" | "medium" | "low";
|
||||
}
|
||||
|
||||
/**
|
||||
* Konfidenz aus der Quellenlage: ein freigegebenes (validiertes) Dokument UND
|
||||
* ein Nachweis ⇒ high; irgendein verknüpftes Dokument ⇒ medium; sonst low.
|
||||
*/
|
||||
function deriveConfidence(input: DraftInput): DraftResult["confidence"] {
|
||||
const hasApprovedDoc = input.documents.some((d) => d.status === "FREIGEGEBEN");
|
||||
const hasEvidence = input.evidence.length > 0;
|
||||
if (hasApprovedDoc && hasEvidence) return "high";
|
||||
if (input.documents.length > 0) return "medium";
|
||||
return "low";
|
||||
}
|
||||
|
||||
/** Semikolon-getrennter Dokumentverweis („R08 – Titel, Version 1.0"). */
|
||||
function buildSourceRef(docs: DraftDocument[]): string | null {
|
||||
if (docs.length === 0) return null;
|
||||
return docs.map((d) => `${d.code} – ${d.title}, Version ${d.version}`).join("; ");
|
||||
}
|
||||
|
||||
const SYSTEM_PROMPT = [
|
||||
"Du unterstützt bei der Erstellung einer VDA-ISA-Prüfungsdokumentation (TISAX).",
|
||||
"Formuliere für eine einzelne Anforderung EINEN präzisen deutschen Satz im ABGABE-Stil,",
|
||||
"der beschreibt, WIE die Anforderung umgesetzt ist. Der Satz nennt konkrete Spezifika",
|
||||
"aus dem Umsetzungshinweis und schließt mit einem Dokumentverweis in Klammern ab,",
|
||||
"Form: (<Dokument-Code> – <Titel>, Version <X>, Abschnitt <passend>).",
|
||||
"Erfinde keine Fakten, keine Zahlen und keine Abschnitte, die sich nicht aus den",
|
||||
"Eingaben ableiten lassen. Wenn kein Dokument verknüpft ist, lasse den Klammerzusatz weg.",
|
||||
"Antworte ausschließlich mit dem Satz — ohne Aufzählungszeichen, ohne Anführungszeichen,",
|
||||
"ohne Vor- oder Nachbemerkung und ohne interne XML-Tags.",
|
||||
].join(" ");
|
||||
|
||||
function buildUserPrompt(input: DraftInput): string {
|
||||
const docs =
|
||||
input.documents.length > 0
|
||||
? input.documents
|
||||
.map((d) => `- ${d.code}: ${d.title} (Version ${d.version}, Status ${d.status})`)
|
||||
.join("\n")
|
||||
: "- (keine verknüpften Dokumente)";
|
||||
const evidence =
|
||||
input.evidence.length > 0 ? input.evidence.map((e) => `- ${e}`).join("\n") : "- (keine Nachweise vorhanden)";
|
||||
return [
|
||||
`Control: ${input.control} (${input.reqId}), Verbindlichkeit: ${input.obligation}`,
|
||||
`Anforderung: ${input.requirement}`,
|
||||
`Umsetzungshinweis: ${input.implementation || "(keiner)"}`,
|
||||
`Verknüpfte Dokumente:\n${docs}`,
|
||||
`Vorhandene Nachweise:\n${evidence}`,
|
||||
].join("\n\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* Erzeugt den KI-Entwurf. Liefert `null`, wenn keine KI konfiguriert ist oder
|
||||
* der Aufruf fehlschlägt (kein Fehler-Spam; der Aufrufer degradiert sauber).
|
||||
*/
|
||||
export async function draftControlDescription(input: DraftInput): Promise<DraftResult | null> {
|
||||
const client = getAnthropic();
|
||||
if (!client) return null;
|
||||
|
||||
try {
|
||||
const response = await client.messages.create({
|
||||
model: AI_MODEL,
|
||||
max_tokens: 1024,
|
||||
system: SYSTEM_PROMPT,
|
||||
messages: [{ role: "user", content: buildUserPrompt(input) }],
|
||||
});
|
||||
|
||||
if (response.stop_reason === "refusal") return null;
|
||||
const text = response.content
|
||||
.filter((b): b is Extract<typeof b, { type: "text" }> => b.type === "text")
|
||||
.map((b) => b.text)
|
||||
.join("")
|
||||
.trim();
|
||||
if (!text) return null;
|
||||
|
||||
return {
|
||||
draftText: text,
|
||||
sourceRef: buildSourceRef(input.documents),
|
||||
confidence: deriveConfidence(input),
|
||||
};
|
||||
} catch {
|
||||
// Netzwerk-/API-Fehler dürfen die UI nicht blockieren.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user