Phase 8: Lokale KI

Wiki mit semantischer Suche ueber pgvector, Diktat ueber faster-whisper,
Berichtsassistent und Belegerfassung — alles gegen selbst betriebene
Modelle. Dazu zwei Protokollansichten: was die Maschine getan hat und was
Menschen getan haben.

Nichts, was ein Modell erzeugt, gelangt ungeprueft in einen Nachweis. Jede
Ausgabe ist zuerst ein eigener Datensatz, gekennzeichnet, mit zwei gleich
grossen Knoepfen davor. Die Datenbank haelt gegen: Eingabe, Ausgabe und
Modell sind unveraenderlich, geloescht wird nicht, entschieden wird einmal.

Kein Rueckfall auf einen Anbieter: fehlen die Container, fehlt die Funktion.
Dafuer bleibt alles andere ohne Modell vollstaendig benutzbar — geprueft von
vierzehn E2E-Tests, die ohne laufendes Modell durchlaufen.

Postgres bringt jetzt PostGIS und pgvector mit (eigenes Image), Ollama und
faster-whisper stehen im Compose-Profil "ki".

73 neue Unit-Tests, 14 neue E2E-Tests, Version 0.9.0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Florian Wilde
2026-09-04 11:06:03 +02:00
co-authored by Claude Opus 5
parent 12243f177d
commit f7ccd9a908
134 changed files with 8624 additions and 72 deletions
@@ -0,0 +1,242 @@
-- =============================================================================
-- Phase 8 — Lokale KI
--
-- HINWEIS zu den entfernten DropIndex-Blöcken:
-- Prisma erzeugt bei jeder Migration erneut den Vorschlag, die von Hand
-- angelegten GiST- und Trigram-Indizes aus Phase 1 abzuräumen — es kennt sie
-- nicht, weil sie nicht aus dem Schema stammen, sondern aus einer eigenen
-- Migration. Die Blöcke sind deshalb hier entfernt worden, wie in jeder
-- Migration seit Phase 1. Ohne sie wäre die Volltext- und Umkreissuche nach
-- diesem Schritt ein vollständiger Tabellendurchlauf.
--
-- Neu ist die Erweiterung "vector": sie kommt aus dem eigenen Postgres-Image
-- (docker/postgres/Dockerfile). Bei einer bestehenden Installation muss es
-- vorher gebaut werden — `docker compose build postgres`, dann den Dienst neu
-- starten. Ohne die Erweiterung bricht diese Migration mit einer klaren
-- Meldung ab, und das ist besser als eine halb angelegte Tabelle.
-- =============================================================================
-- CreateExtension
CREATE EXTENSION IF NOT EXISTS "vector";
-- CreateEnum
CREATE TYPE "KiArt" AS ENUM ('DIKTAT', 'BERICHTSENTWURF', 'WIKI_ANTWORT', 'BELEGERFASSUNG', 'EINBETTUNG');
-- CreateEnum
CREATE TYPE "KiStand" AS ENUM ('ENTWURF', 'UEBERNOMMEN', 'VERWORFEN', 'FEHLGESCHLAGEN');
-- CreateEnum
CREATE TYPE "WikiKategorie" AS ENUM ('DIENSTVORSCHRIFT', 'AUSBILDUNG', 'GERAETE', 'EINSATZ', 'ORGANISATION', 'SONSTIGES');
-- CreateTable
CREATE TABLE "ki_vorgang" (
"id" UUID NOT NULL,
"organisationId" UUID NOT NULL,
"art" "KiArt" NOT NULL,
"stand" "KiStand" NOT NULL DEFAULT 'ENTWURF',
"modell" VARCHAR(120) NOT NULL,
"entitaet" VARCHAR(80),
"entitaetId" UUID,
"eingabe" VARCHAR(8000) NOT NULL,
"ausgabe" VARCHAR(40000),
"fehler" VARCHAR(1000),
"dauerMs" INTEGER,
"tokenEingabe" INTEGER,
"tokenAusgabe" INTEGER,
"erzeugtVonId" UUID,
"entschiedenVonId" UUID,
"entschiedenAm" TIMESTAMPTZ(3),
"erstelltAm" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "ki_vorgang_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "wikiseite" (
"id" UUID NOT NULL,
"organisationId" UUID NOT NULL,
"slug" VARCHAR(160) NOT NULL,
"titel" VARCHAR(200) NOT NULL,
"art" "WikiKategorie" NOT NULL DEFAULT 'SONSTIGES',
"inhalt" TEXT NOT NULL,
"quelle" VARCHAR(300),
"schlagworte" TEXT[],
"veroeffentlicht" BOOLEAN NOT NULL DEFAULT true,
"autorId" UUID,
"eingebettetAm" TIMESTAMPTZ(3),
"erstelltAm" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"geaendertAm" TIMESTAMPTZ(3) NOT NULL,
"geloeschtAm" TIMESTAMPTZ(3),
CONSTRAINT "wikiseite_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "wikiabschnitt" (
"id" UUID NOT NULL,
"organisationId" UUID NOT NULL,
"seiteId" UUID NOT NULL,
"reihenfolge" INTEGER NOT NULL,
"ueberschrift" VARCHAR(300),
"text" TEXT NOT NULL,
"einbettung" vector(768),
"modell" VARCHAR(120),
"erstelltAm" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "wikiabschnitt_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "wikiversion" (
"id" UUID NOT NULL,
"organisationId" UUID NOT NULL,
"seiteId" UUID NOT NULL,
"nummer" INTEGER NOT NULL,
"titel" VARCHAR(200) NOT NULL,
"inhalt" TEXT NOT NULL,
"bemerkung" VARCHAR(500),
"autorId" UUID,
"erstelltAm" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "wikiversion_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "diktat" (
"id" UUID NOT NULL,
"organisationId" UUID NOT NULL,
"audioDateiId" UUID NOT NULL,
"dauerSekunden" INTEGER,
"sprache" VARCHAR(10) NOT NULL DEFAULT 'de',
"transkript" TEXT,
"stand" "KiStand" NOT NULL DEFAULT 'ENTWURF',
"entitaet" VARCHAR(80),
"entitaetId" UUID,
"kiVorgangId" UUID,
"erzeugtVonId" UUID,
"erstelltAm" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"geaendertAm" TIMESTAMPTZ(3) NOT NULL,
"geloeschtAm" TIMESTAMPTZ(3),
CONSTRAINT "diktat_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "belegentwurf" (
"id" UUID NOT NULL,
"organisationId" UUID NOT NULL,
"fotoDateiId" UUID NOT NULL,
"betrag" DECIMAL(12,2),
"waehrung" VARCHAR(3) NOT NULL DEFAULT 'EUR',
"belegdatum" DATE,
"haendler" VARCHAR(200),
"kategorie" VARCHAR(80),
"steuersatz" DECIMAL(5,2),
"rohantwort" JSONB,
"stand" "KiStand" NOT NULL DEFAULT 'ENTWURF',
"kiVorgangId" UUID,
"erfasstVonId" UUID,
"bestaetigtVonId" UUID,
"bestaetigtAm" TIMESTAMPTZ(3),
"erstelltAm" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"geaendertAm" TIMESTAMPTZ(3) NOT NULL,
"geloeschtAm" TIMESTAMPTZ(3),
CONSTRAINT "belegentwurf_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "ki_vorgang_organisationId_art_erstelltAm_idx" ON "ki_vorgang"("organisationId", "art", "erstelltAm" DESC);
-- CreateIndex
CREATE INDEX "ki_vorgang_organisationId_entitaet_entitaetId_idx" ON "ki_vorgang"("organisationId", "entitaet", "entitaetId");
-- CreateIndex
CREATE INDEX "ki_vorgang_organisationId_stand_idx" ON "ki_vorgang"("organisationId", "stand");
-- CreateIndex
CREATE INDEX "wikiseite_organisationId_art_idx" ON "wikiseite"("organisationId", "art");
-- CreateIndex
CREATE UNIQUE INDEX "wikiseite_organisationId_slug_key" ON "wikiseite"("organisationId", "slug");
-- CreateIndex
CREATE INDEX "wikiabschnitt_organisationId_idx" ON "wikiabschnitt"("organisationId");
-- CreateIndex
CREATE UNIQUE INDEX "wikiabschnitt_seiteId_reihenfolge_key" ON "wikiabschnitt"("seiteId", "reihenfolge");
-- CreateIndex
CREATE INDEX "wikiversion_organisationId_seiteId_idx" ON "wikiversion"("organisationId", "seiteId");
-- CreateIndex
CREATE UNIQUE INDEX "wikiversion_seiteId_nummer_key" ON "wikiversion"("seiteId", "nummer");
-- CreateIndex
CREATE INDEX "diktat_organisationId_stand_idx" ON "diktat"("organisationId", "stand");
-- CreateIndex
CREATE INDEX "diktat_organisationId_entitaet_entitaetId_idx" ON "diktat"("organisationId", "entitaet", "entitaetId");
-- CreateIndex
CREATE INDEX "belegentwurf_organisationId_stand_idx" ON "belegentwurf"("organisationId", "stand");
-- CreateIndex
CREATE INDEX "belegentwurf_organisationId_belegdatum_idx" ON "belegentwurf"("organisationId", "belegdatum");
-- AddForeignKey
ALTER TABLE "ki_vorgang" ADD CONSTRAINT "ki_vorgang_organisationId_fkey" FOREIGN KEY ("organisationId") REFERENCES "organisation"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "ki_vorgang" ADD CONSTRAINT "ki_vorgang_erzeugtVonId_fkey" FOREIGN KEY ("erzeugtVonId") REFERENCES "person"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "ki_vorgang" ADD CONSTRAINT "ki_vorgang_entschiedenVonId_fkey" FOREIGN KEY ("entschiedenVonId") REFERENCES "person"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "wikiseite" ADD CONSTRAINT "wikiseite_organisationId_fkey" FOREIGN KEY ("organisationId") REFERENCES "organisation"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "wikiseite" ADD CONSTRAINT "wikiseite_autorId_fkey" FOREIGN KEY ("autorId") REFERENCES "person"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "wikiabschnitt" ADD CONSTRAINT "wikiabschnitt_organisationId_fkey" FOREIGN KEY ("organisationId") REFERENCES "organisation"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "wikiabschnitt" ADD CONSTRAINT "wikiabschnitt_seiteId_fkey" FOREIGN KEY ("seiteId") REFERENCES "wikiseite"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "wikiversion" ADD CONSTRAINT "wikiversion_organisationId_fkey" FOREIGN KEY ("organisationId") REFERENCES "organisation"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "wikiversion" ADD CONSTRAINT "wikiversion_seiteId_fkey" FOREIGN KEY ("seiteId") REFERENCES "wikiseite"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "wikiversion" ADD CONSTRAINT "wikiversion_autorId_fkey" FOREIGN KEY ("autorId") REFERENCES "person"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "diktat" ADD CONSTRAINT "diktat_organisationId_fkey" FOREIGN KEY ("organisationId") REFERENCES "organisation"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "diktat" ADD CONSTRAINT "diktat_audioDateiId_fkey" FOREIGN KEY ("audioDateiId") REFERENCES "datei"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "diktat" ADD CONSTRAINT "diktat_kiVorgangId_fkey" FOREIGN KEY ("kiVorgangId") REFERENCES "ki_vorgang"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "diktat" ADD CONSTRAINT "diktat_erzeugtVonId_fkey" FOREIGN KEY ("erzeugtVonId") REFERENCES "person"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "belegentwurf" ADD CONSTRAINT "belegentwurf_organisationId_fkey" FOREIGN KEY ("organisationId") REFERENCES "organisation"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "belegentwurf" ADD CONSTRAINT "belegentwurf_fotoDateiId_fkey" FOREIGN KEY ("fotoDateiId") REFERENCES "datei"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "belegentwurf" ADD CONSTRAINT "belegentwurf_kiVorgangId_fkey" FOREIGN KEY ("kiVorgangId") REFERENCES "ki_vorgang"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "belegentwurf" ADD CONSTRAINT "belegentwurf_erfasstVonId_fkey" FOREIGN KEY ("erfasstVonId") REFERENCES "person"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "belegentwurf" ADD CONSTRAINT "belegentwurf_bestaetigtVonId_fkey" FOREIGN KEY ("bestaetigtVonId") REFERENCES "person"("id") ON DELETE SET NULL ON UPDATE CASCADE;