Windows-GPU-UNLOCK
RTX3090 UEFI 포트, 한국어 무리셋 윈도우 부팅 가이드, 검증된 소스와 벤치마크 제공
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
Chatbot für industrielle Elektrodokumentation: Stromlaufpläne, Stücklisten, Klemmenpläne, Siemens STEP 7 AWL-Programme und Handbücher. Der Agent verfolgt Betriebsmittel (-K12), Klemmen (-X1:5) und SPS-Adressen (E0.0) über alle Dokumente hinweg. Beim ersten Upload lädt das Backend das Embedding-Modell (BAAI/bge-m3, ca. 2 GB) und die Docling-Layoutmodelle von Hugging Face.README 전문 보기 ↗
A chat assistant for maintenance staff that answers questions about a machine's electrical documentation (circuit diagrams, parts lists, terminal plans, Siemens STEP 7 AWL programs, manuals) and backs every statement with a citation to file and location.
When a machine stops, maintenance searches the same component across five documents under four notations (-K1, -X3:1, E0.0, /3.8), and a chatbot that only searches text tends to invent terminals and pages. Stromlauf AI builds a tag index and a signal graph across all documents at upload time, answers the deterministic questions (signal path, fact card, gap list) without a model call, and lets the language model answer only through tools, with every claim cited as [[file|location]] and checked by a citation resolver. The OT background sets the input contract: it reads what a CAE tool exports as PDF, parts and terminal lists as tables, and PLC code as AWL/SCL sources, not the proprietary project files.
Python 3.11+ · FastAPI · LangGraph · Docling · RapidOCR · Postgres with pgvector · Alembic · Next.js 16 · React 19 · TypeScript · Docker Compose · GitHub Actions
git clone https://github.com/siinanXD/stromlauf-ai && cd stromlauf-ai
cp .env.example .env # ANTHROPIC_API_KEY is only needed for chat answers
docker compose --profile demo up --build # UI http://localhost:3100, API http://localhost:8010/api/health
The demo loads the example machine FB-01 (examples/foerderband, invented documentation); the first start downloads the embedding model BAAI/bge-m3 (about 2 GB). Evaluation results per model are in eval/results/vergleich_2026-10-02.md and in the table below.
MIT, see LICENSE. The rest of this README documents the system in German.
Chatbot für industrielle Elektrodokumentation: Stromlaufpläne, Stücklisten, Klemmenpläne,
Siemens STEP 7 AWL-Programme und Handbücher. Der Agent verfolgt Betriebsmittel (-K12),
Klemmen (-X1:5) und SPS-Adressen (E0.0) über alle Dokumente hinweg.
Problem. Steht eine Maschine, sucht die Instandhaltung in einem Stromlaufplan, einer Stückliste, einem Klemmenplan,
einem SPS-Programm und einem Handbuch nach demselben Bauteil, unter vier Schreibweisen (-K1, -X3:1, E0.0,
/3.8). Ein Chatbot, der nur Text sucht, rät dabei leicht: Er erfindet Spalten, Klemmen und Seiten.
Ansatz. Die Maschine ist die zentrale Einheit. Beim Upload entsteht ein Kennzeichen-Index über alle Dokumente, dazu ein
Signalgraph aus Klemmenplan, Stückliste, Symboltabelle, AWL und der Vektorebene des Plans. Alles, was sich deterministisch
lesen lässt (Signalweg, Befundkarte, Lückenliste, Maschinenmodell), kommt ohne Modellaufruf. Das Sprachmodell antwortet nur
mit Werkzeugen darauf und muss jede Aussage mit [[Datei|Ort]] belegen; ein Zitat-Resolver prüft die Belege gegen die
Fundstellen. Dokumenttext gilt im Prompt als Daten, nicht als Anweisung. Architektur: Diagramm oben.
Ergebnis. Im Beispiel FB-01 führt der Signalweg vom Taster bis zum Motor und nennt je Kante die Herkunft:
Die Abbildung ist nach dem Beispiel gezeichnet, kein Bildschirmfoto der Oberfläche. Ein echter Screenshot oder ein GIF der Maschinenseite fehlt noch (siehe Hinweis am Ende des Abschnitts).
Agentenlauf mit 20 Fragen je Modell aus eval/questions.jsonl (FB-01 7, UR-01 5, PM1-AR 4, AWL 2, Festo 1, Injection 1),
gleiche Dokumente, gleicher Agent und Prompt v3. Quelle: eval/results/vergleich_2026-10-02.md,
Lauf vom 2026-10-02, Kosten nach backend/app/pricing.py mit Cache-Tokens. Bewertung ohne LLM-Richter (eval/README.md).
| Modell | Fakten | Quellen | Zitate gültig | voll bestanden (von 20) | USD je Antwort | s je Antwort |
|---|---|---|---|---|---|---|
| claude-sonnet-5-5 | 0,988 | 0,95 | 0,991 | 18 | 0,046 | 12,2 |
| openai:gpt-6.1-sol | 0,872 | 0,90 | 0,947 | 10 | 0,012 | 12,6 |
| openai:gpt-5.4-mini@none | 0,820 | 0,95 | 0,721 | 11 | 0,004 | 5,5 |
| ollama:qwen35-4b-32k (lokal) | 0,524 | 0,60 | 0,350 | 5 | 0 | 6,4 |
Die Tabelle zeigt vier der neun gemessenen Modelle; alle stehen im Vergleich. claude-opus-5-5 lief nicht
(Guthaben erschöpft), claude-sonnet-5 nur mit 6 Fragen.
Lesegenauigkeit der Ingestion gegen die Ground Truth des Plangenerators, ohne Datenbank und Modell
(python eval/run_ingest.py --gold eval/ingest_gold/fb01.json --min 0.95, Quelle eval/README.md, Abschnitt
„Lesegenauigkeit“):
| Plan | Stand | Betriebsmittel, Klemmen, SPS-Adressen (Recall und Precision) | Querverweise |
|---|---|---|---|
| FB-01, Text-PDF, 7 Seiten | 2026-09-29 | je 1,00 | 0,95 |
| UR-01 (16 Seiten) und PM1-AR (13 Seiten), Text-PDF | 2026-09-30 | je 1,00 | 1,00 |
| FB-01, Voll-Scan mit lokaler OCR | 2026-09-30 | Betriebsmittel 0,99, Klemmen 0,81, SPS-Adressen 1,00, Precision 1,00 | – |
Die Zahlen stammen aus selbst erzeugten Beispielplänen, nicht aus Kundendokumenten. Wie gut das Lesen auf einem eigenen
Plan klappt, zeigt „Fremd- und Firmendaten“. Die Gates laufen bei jedem PR (.github/workflows/eval.yml).
Offen: Screenshot der Maschinenseite als
docs/assets/maschinenseite.png. Den nimmtfrontend/e2e/screenshot.spec.tsim Compose-Job auf (Vorlage.github/workflows/ci.yml (Jobcompose-demo), siehe „Demo in einem Befehl“); die Datei fehlt, bis der Job einmal gelaufen ist und das Bild eingecheckt wurde.
cp .env.example .env # ANTHROPIC_API_KEY eintragen (nur für Chat-Antworten nötig)
docker compose --profile demo up --build
Das startet Postgres mit pgvector, das Backend im Demo-Modus und das Frontend. Das Backend legt beim ersten Start die
Beispiel-Maschine FB-01 an (examples/foerderband, read-only eingebunden) und liest die Dokumente ein; der erste Start
lädt dafür BAAI/bge-m3 (ca. 2 GB) und dauert entsprechend. Danach: Oberfläche http://localhost:3100, API
http://localhost:8010/api/health. Ohne --profile demo startet wie bisher nur die Datenbank. Aufräumen:
docker compose --profile demo down -v.
cp .env.example .env # ANTHROPIC_API_KEY eintragen
docker compose up -d db # Postgres + pgvector auf Port 5433
cd backend
python -m venv .venv
.venv/Scripts/pip install torch --index-url https://download.pytorch.org/whl/cu126 # NVIDIA-GPU; sonst weglassen
.venv/Scripts/pip install -e ".[dev]"
.venv/Scripts/uvicorn app.main:app --reload --port 8010
cd frontend
npm install
npm run dev # http://localhost:3100
Beim ersten Upload lädt das Backend das Embedding-Modell (BAAI/bge-m3, ca. 2 GB) und die
Docling-Layoutmodelle von Hugging Face.
Ohne Docker gegen Supabase. DATABASE_URL kommt aus Infisical (/stromlauf-ai) und zeigt auf das Supabase-Projekt; das Schema wählt der Zusatz ?options=-c%20search_path%3Dstromlauf%2Cextensions. Leere Werte in Infisical brechen infisical run, weil pydantic leere Zahlen und Bools ablehnt: leere Einträge löschen statt leer lassen.
Die Seite /ablauf nimmt Stromlaufplan, Stückliste, AWL oder Handbuch entgegen und führt acht Schritte aus (Hochladen, Lesen, Maschine, Steckbrief, Modell, Zusammenfassung, Lernen, Fertig), die sich live färben. Das Modell für die Zusammenfassung steht in ABLAUF_MODEL (Standard ollama:qwen3.5:9b). Sind die Langfuse-Schlüssel gesetzt, zeigt jeder Schritt einen Link „In Langfuse öffnen“ auf seinen Trace, ohne Schlüssel zeigt das Panel „Tracing aus“.
Unten auf der Seite steht „Zeiten“: je Schritt Median, letzter und langsamster Wert über die letzten 50 Läufe, dazu feste Zielwerte (Lesen ≤ 60 s je Dokument, Zusammenfassung ≤ 30 s je Frage, gesamt ≤ 180 s), die rot werden, sobald der letzte Lauf darüber liegt. Die Zahlen kommen aus GET /api/runs/zeiten; Tokens und Modell-Latenz bleiben in Langfuse.
Der Schritt „Lernen“ zieht aus den drei Antworten und dem Steckbrief höchstens fünf belegte Sätze je Maschine; nach jeder Chat-Antwort mit Quellen kommen im Hintergrund bis zu zwei dazu, nur Sätze mit Beleg aus der Dokumentation werden gespeichert, und der Agent liest die jüngsten 30 bei jeder Antwort mit. Der Tab „Wissen“ auf der Maschinenseite (unter „Mehr“) zeigt die Sätze mit Beleg und Quelle (API: GET/POST/PATCH/DELETE /api/machines/{id}/notes); Sperren oder Korrigieren markiert einen Satz als vom Menschen bestätigt, und solche Sätze gewinnen gegen neue. Jede Änderung schreibt DATA_DIR/wissen/<Maschine> (<ID-Anfang>).md neu; der Ordner lässt sich als Obsidian-Vault öffnen.
Ein ganzer Ordner geht auch: Unterordner werden zu Anlagen, Dateien ohne Ordner nach der Kennung im Namen (FB-01, FB01, =FB-01, PM1-AR; FB01 und FB-01 zählen als dieselbe Anlage) gebündelt, der Rest steht als „unzugeordnet“ und wird per Auswahl zugewiesen. Die Seite zeigt den Vorschlag („3 Anlagen erkannt“), Namen und Zuordnung lassen sich ändern, „Alle starten“ startet je Anlage einen Lauf. Grenzen: 200 Dateien oder 500 MB je Ablage, 100 Dateien je Anlage.
Das Schema wird beim Start über Alembic auf den neuesten Stand gebracht (init_db() ruft
alembic upgrade head). Von Hand aus backend/: alembic upgrade head, alembic current,
alembic downgrade base. Die Baseline 0001_stromlauf_baseline legt auf einer leeren Datenbank alle
Tabellen an und bringt eine Datenbank aus der früheren create_all-Zeit über die additiven
Statements in app/migrations.py auf denselben Stand. Neue Schemaänderungen: Modell ändern, dann
alembic revision --autogenerate -m "kurz-was" (braucht eine laufende Datenbank), Datei prüfen,
committen. backend/tests/test_alembic_baseline.py hält Baseline und Modell deckungsgleich; CI
prüft die Migration von null gegen pgvector/pgvector:pg17.
Jede fachliche Zeile (Quelle, Dokument, Chunk, Kennzeichen, Chat, Halle, Maschine, Fehler, Schaltschrank) gehört zu einem Workspace. Der Workspace des Requests kommt aus dem Login:
JWT_SECRET gesetzt: Anmeldung per Magic-Link (/login → POST /api/auth/magic-link → Mail mit
Link → POST /api/auth/exchange → JWT, 12 h). Erste Anmeldung legt Nutzer und einen eigenen Workspace an.
Das Frontend schickt das JWT als Authorization: Bearer, Bild-URLs bekommen ?token=.API_KEY gesetzt: Dienstzugriff für Skripte und den MCP-Server im Workspace default (admin); ein JWT
geht dort ebenfalls als X-API-Key/STROMLAUF_API_KEY.default (nur lokal).Demo-Modus (DEMO_MODE=true, backend/app/demo.py) für eine öffentliche Instanz: Upload und alle
Schreibzugriffe unter /api liefern 403 (Chat, Beleg-Prüfung und Login bleiben), je IP gelten
DEMO_RATE_LIMIT Anfragen und DEMO_CHAT_RATE_LIMIT Chat-Fragen pro Minute (darüber 429 mit Retry-After),
und beim Start legt das Backend die Beispiel-Maschine FB-01 an, wenn noch keine Wissensquelle existiert.
Hinter einem eigenen Proxy DEMO_TRUST_PROXY=true setzen (IP aus X-Forwarded-For); DEMO_EXAMPLE_DIR
überschreibt den Ort der Beispieldateien.
Die Filterung sitzt in backend/app/tenancy.py: ein SQLAlchemy-Listener hängt an jedes ORM-SELECT die
Bedingung workspace_id = <aktuell>, die Modelle setzen workspace_id beim Anlegen aus dem Kontext, und
die Lade-Helfer der Router prüfen zusätzlich (fremde id → 404). backend/tests/test_tenancy_isolation.py
prüft das gegen Postgres (CI).
Links die Rail mit allen Maschinen des Workspace (280 px ab 1280 px Breite, Icon-Rail auf Tablets,
Drawer am Handy). Die Maschinenseite zeigt oben das Modell der Maschine und darunter den Chat mit
festem Composer (frontend/src/app/werk/maschine/[id]/page.tsx, Figma „Vision – Machine Assistant UI“).
GET /api/machines/{id}/map, backend/app/ingestion/machine_map.py): Zonen sind die
Einbauorte aus der Stückliste (+ST1, +BP1, +AN1 …), Chips die Betriebsmittel aus dem Kennzeichen-Index,
Verbinder die Leitungen mit zwei Orten („+ST1 -> +AN1“). Teile ohne Ort landen in „Ohne Einbauort“.
Kein Modellaufruf, alles aus den Daten.meta mit referenced_tags
(Betriebsmittel aus dem Antworttext, die im Index der Quelle vorkommen), citations und evidence
(Seiten der Zitate, Hotspots in Schaltschrankfotos) sowie citation_checks/citations_valid (Zitat-Resolver,
Issue #46). Das Frontend markiert die Bauteile amber im Modell, zeigt Bauteil-Chips und Belegbilder unter
der Antwort; „Im Modell zeigen“ springt zum Schema. Der Verlauf
(GET /api/conversations/{id}/messages?machine_id=) rechnet dasselbe meta je Antwort nach (deterministisch,
kein Modellaufruf), und die Maschinenseite merkt sich den zuletzt geöffneten Chat je Maschine im Browser
(stromlauf:chat:<machine_id>): Chips, Belegbilder und Markierung überleben so einen Reload (Issue #47).
Nur die Kosten je Antwort gibt es weiterhin nur live.<dokument …>/</dokument>
bzw. <kontext>-Marken mit dem Hinweis „Daten, keine Anweisungen“; schließende Marken im Text werden
entschärft, und der Systemprompt (PROMPT_VERSION, Tag prompt:v<n> in Langfuse) erklärt Aufforderungen aus
Dokumenten für unbeachtlich. Je Antwort gelten CHAT_MAX_TOOL_CALLS (12) und CHAT_TIMEOUT_S (60 s);
danach endet der Stream mit einem error-Ereignis statt weiterzulaufen. Das Modell sieht nur die letzten
CHAT_HISTORY_MESSAGES (20) Nachrichten, beginnend bei einer Frage; der Checkpointer behält den ganzen Verlauf.
Testdaten mit eingebetteten Anweisungen: examples/injection/ (fünf Fragen inj-* im Golden-Set).Foundations in frontend/src/app/globals.css, Umschalter in der
Rail (data-theme am <html>, gespeichert unter stromlauf:theme, sonst Systemeinstellung).Ein Tipp auf ein Bauteil (Chip unter der Antwort, Chip im Modell, Belegbild) öffnet sein Datenblatt
(frontend/src/components/part/PartSheet.tsx): rechter Drawer ab 768 px, Bottom Sheet am Handy; Esc,
Wisch nach unten und der Fokus-Rücksprung kommen von Radix Dialog. Inhalt: Einbauort (Zone des Modells),
Datenblattseite (erste PDF-Fundstelle in Handbuch/Sonstiges, sonst „Datenblatt hochladen“), Befundkarte
(Stromlaufplan-Stellen, Klemmen, SPS-Adressen), verbundene Bauteile aus dem Signalgraph (Verb aus der Art des
Bauteils: schützt / schaltet / steuert / versorgt), Belege und die Fotos, in denen es markiert ist. Die Art kommt
aus dem Kennbuchstaben in der Lesart der Quelle: Ältere Pläne nach DIN 40719 nennen das Schütz -K1, Pläne nach
IEC 81346-2:2019 nennen es -QA1 und meinen mit -K Relais und SPS. Lässt sich die Lesart nicht bestimmen, bleibt
die Art bei widersprüchlichen Buchstaben leer, statt geraten zu werden.
„Im Foto zeigen“ öffnet die Lightbox (CabinetLightbox.tsx): referenzierte Rahmen amber und gefüllt,
andere als Umriss, Seitenliste mit Konfidenz. „Markierung korrigieren“ macht den gewählten Rahmen zieh-
und skalierbar (Ecken, Pfeiltasten, Shift+Pfeile = Größe), Enter speichert per PATCH /api/hotspots/{id};
ein korrigierter Rahmen gilt als origin=manual, confirmed=true und landet so im Beleg der nächsten
Antwort. frontend/e2e/part-sheet.spec.ts prüft Öffnen/Schließen/Fokus und den Box-Roundtrip.
Jeder KI-Aufruf (Chat-Antwort, Seitenanalyse, Schaltschrank-Erkennung)
landet als Zeile in ai_call_ledger mit Workspace, Maschine, Zweck, Modell, Tokens und Kosten
(backend/app/ledger.py, Preise aus backend/app/pricing.py, Listenpreise 1:1 als Euro-Cent).
Gebucht wird in derselben Transaktion wie das Ergebnis; die Seitenanalyse bucht je Seite sofort.
GET /api/machines/{id}/costs: laufender Monat und gesamt, je Zweck. Im Kopf der Maschinenansicht
steht der Chip „KI diesen Monat …“, jede Chat-Antwort zeigt ihre Kosten im Footer.GET /api/machines/estimate?pages=&photos=&vision=: Schaetzung vor der Ingestion aus gemessenen
Medianen (ab 5 Aufrufen je Zweck), sonst aus den Annahmen in docs/product/cost-model.md. Der
Upload-Dialog zeigt sie unter den erkannten Dateien („Modell erstellen · ≈ x €“).PATCH /api/workspace/budget {"cap_cents": 5000} (Admin), null = kein
Limit. Ist das Limit erreicht, lehnt die API jeden weiteren KI-Aufruf mit 402 ab, bevor der
Provider gerufen wird; die Ingestion laeuft ohne Vision-Seiten weiter. Die Oberflaeche zeigt ein
Banner mit „Limit erhoehen“.Zwei Provider: Anthropic (Standard) und OpenAI. Ein Modellname gilt mit Präfix (openai:gpt-5-mini,
anthropic:claude-sonnet-5) oder ohne (claude-* = Anthropic, gpt-*/o3* = OpenAI). Der Schlüssel des
Providers muss in der .env stehen (ANTHROPIC_API_KEY, OPENAI_API_KEY), sonst antwortet /api/chat mit 400.
CHAT_MODEL und VISION_MODEL sind die Standards für Chat bzw. Seitenanalyse und Schaltschrank
(app/llm.py, make_chat_model).POST /api/chat nimmt model entgegen. python eval/run_eval.py --model openai:gpt-5-mini --only "Foerderband FB-01" --max-cost 1.00 fährt denselben Fragensatz mit einem anderen Modell; Kosten je
Antwort kommen aus app/pricing.py (beide Provider).EMBEDDING_PROVIDER=openai nutzt text-embedding-3-small mit dimensions = EMBEDDING_DIM. Ein
Wechsel des Embedders heißt: alle Dokumente neu verarbeiten, sonst passen die Vektoren nicht zusammen.
EMBEDDING_CACHE_DIR ist nur für CI und Eval gedacht: Die Vektoren der Abschnitte liegen dann je exaktem Text auf
der Platte, und nur neue Abschnitte werden eingebettet. Leer ist der Cache aus, so wie im Betrieb.eval.yml) per Hand starten mit chat_model und vision_model als Eingabe; die Schlüssel liegen
als Repository-Secrets (OPEN_API_KEY wird als OPENAI_API_KEY durchgereicht).Backend als Container (backend/Dockerfile, Build-Kontext backend/), Datenbank Postgres mit pgvector,
Frontend auf Vercel (Projekt stromlauf-ai, Root Directory frontend). Railway-Service:
backend (dann greift backend/railway.toml:
Dockerfile-Build, Healthcheck /api/health)./data (Uploads, Bilder, Plan-Cache, HF-Modellcache). Eine Instanz; fuer mehrere
Instanzen waere ein Bucket noetig (siehe docs/product/architecture.md).pgvector/pgvector:pg17 oder Railway-Postgres mit
CREATE EXTENSION vector), Variable DATABASE_URL=postgresql+psycopg://....ANTHROPIC_API_KEY, API_KEY (Zugriffsschutz), CORS_ORIGINS=https://<vercel-domain>,
CHECKPOINTER=postgres, EMBEDDING_PROVIDER=voyage + VOYAGE_API_KEY (oder local, dann
mindestens 3 GB RAM fuer bge-m3), optional LANGFUSE_*, OCR_MODE (Standard auto).NEXT_PUBLIC_API_URL=https://<railway-domain>, NEXT_PUBLIC_API_KEY=<API_KEY>.docs/product/ACCEPTANCE.md (Abschnitt „Teil 2“).Beim Start laeuft alembic upgrade head; mit CHECKPOINTER=postgres legt der Agent seine
Verlaufstabellen selbst an. EMBEDDING_PROVIDER wechseln heisst: alle Dokumente neu verarbeiten
(„Neu verarbeiten“ im Quellen-Panel oder POST /api/documents/{id}/reingest), sonst passen die
Vektoren nicht zusammen. CI baut das Image bei jedem PR (docker build backend).
Ohne API_KEY in .env läuft das Backend offen (nur lokal sinnvoll). Mit API_KEY verlangt jede
Route unter /api/ den Schlüssel, nur /api/health bleibt frei:
python -c "import secrets; print(secrets.token_urlsafe(32))" # Schlüssel erzeugen
# .env: API_KEY=<Schlüssel> NEXT_PUBLIC_API_KEY=<Schlüssel> STROMLAUF_API_KEY=<Schlüssel>
Frontend, Skripte (scripts/, eval/) und MCP-Server schicken ihn als Header X-API-Key; auch
Authorization: Bearer gilt. Bilder lädt der Browser ohne Header, dafür hängt das Frontend
?api_key= an Bild-URLs. Es ist ein gemeinsamer Schlüssel je Installation, keine Benutzerverwaltung.
Im Ordner examples/foerderband/ liegt eine komplette, frei erfundene
Anlagendokumentation: Stromlaufplan (7 Blaetter), Stueckliste, Klemmenplan, STEP 7 AWL-Programm,
Symboltabelle und Betriebsanleitung, alle mit denselben Kennzeichen. Backend laeuft, dann:
python scripts/load_example.py # legt die Quelle an, laedt 6 Dateien, wartet auf die Ingestion
Danach im Frontend fragen, zum Beispiel: „Was haengt an E0.3 und wo ist das im Plan?“ oder
„Warum leuchtet -H2 nach 20 Sekunden?“ Weitere Fragen mit Loesungsweg in
examples/foerderband/README.md.
Zwei größere Maschinen des Testwerks haben eine vollständige Elektrodokumentation, frei erfunden und
in sich stimmig: Stromlaufplan (16 bzw. 13 Blätter), Stückliste, Klemmenplan, AWL (FB mit 13 bzw.
16 Netzwerken, Schrittkette), Symboltabelle und Betriebsanleitung mit Fehlertabelle. Sie liegen in
examples/umroller/ und examples/aufrollung/ und
entstehen aus einem datengetriebenen Generator (scripts/testdoku/, Maschinenmodell → alle sechs
Dokumente, Blatt/Spalten-Verweise werden beim Zeichnen erfasst):
pip install -e "backend[examples]" # reportlab, openpyxl
python scripts/testdoku/build.py all # erzeugt beide Sätze neu
python scripts/load_testwerk.py --docs # lädt sie hoch und verknüpft L1-UR und PM1-S6
Der Lader übernimmt die Fehlertabellen in die Fehlerlisten der Maschinen; Signalweg und Befundkarte
funktionieren damit an beiden Maschinen. Tests (backend/tests/test_testdoku.py)
prüfen jeden Verweis gegen den Plan und lassen alle Parser über die Dateien laufen.
Reiter Maschinen (/werk/maschinen) ist der Einstieg: alle Maschinen des Werks in einer Tabelle
mit Typ, Linie, Halle, Stand der Dokumentation (keine / n von m fertig / fertig), Zahl der
Fehlereinträge und erster Kennzahl; Filter über Name, Linie, Halle, Typ und Wissensquelle.
Daten: GET /api/machines.
Testwerk Tissue laden (4 Hallen, 30 Maschinen, Kennzahlen mit Quellen, kein KI-Aufruf):
python scripts/load_testwerk.py # --refresh ersetzt ein vorhandenes Testwerk
Papiermaschine PM1 in 6 Sektoren, 6 Verarbeitungslinien (je Hauptmaschine, Verpackung, Palettierer),
Lager & Versand (8 Verladetore als Kennzahl) und Buero.
Daten: examples/testwerk/testwerk.json, Recherche: .ai/research/solution-comparisons/.
Halle: Gruppe von Maschinen mit Name und Beschreibung (/api/halls); die Maschinenuebersicht ordnet
danach, Linie oder Sektor steht am Feld „Linie/Sektor“ der Maschine. Eine neue Maschine entsteht in der
Maschinenuebersicht aus ihrer Dokumentation („Aus Dokumentation anlegen“: Halle waehlen oder neu anlegen,
Name, Typ und Fehlerliste als Vorschlag ohne KI-Aufruf). Jede Maschine hat eine
Maschinenseite mit Foto, zugeordneter Wissensquelle, Fehlerliste (Code, Symptom, Ursache,
Behebung, beteiligte BMK) und Schaltschrankbildern. Im Schaltschrankbild werden Bauteile als
Rechtecke markiert, von Hand oder per Bauteile erkennen lassen (Claude Vision schlaegt
Bauteilart und BMK vor, kostet API-Tokens je Bild). Klick auf ein Bauteil zeigt seine Fundstellen
in Stromlaufplan, Stueckliste, Klemmenplan und AWL. scripts/load_example.py legt dazu eine
Beispielhalle mit Aufbauplan und 14 fertigen Markierungen an.
Die Maschinenseite hat die Tabs Modell · Schaltschrank · Signalweg · Dokumente und hinter „Mehr“ Fehlerliste · Kennzahlen. Kennzahlen sind Wert, Einheit und Quelle (URL oder „Richtwert“).
Strg+K sucht BMK, Klemmen und SPS-Adressen ueber alle Maschinen und springt zur Fundstelle.
Beim Hochladen mit „Automatisch erkennen“ liest das Backend eine Textprobe (erste drei PDF-Seiten,
erste Zeilen einer Tabelle oder Textdatei) und schlägt den Typ mit Begründung vor, etwa
„Kopfzeile Klemmleiste;Klemme;Ziel“ oder „Schriftfeld Blatt n / m; Spaltenkopf 1 … 8“. Der Dialog
zeigt den Vorschlag je Datei; du bestätigst oder änderst ihn, dann wird hochgeladen. Reihenfolge:
Endung (.awl, .scl, .sdf) vor Inhalt vor Dateiname. Regeln in backend/app/ingestion/doctype.py, Vorschau
POST /api/documents/detect. Alle 18 Beispieldateien werden allein aus dem Inhalt richtig erkannt. Ein Schweizer
„Elektroschema“ mit Spaltenkopf 0 … 9 gilt als Stromlaufplan.
Nach dem Upload zeigt /quelle/{id} (Link im Quellen-Panel, im Tab „Dokumente“ der Maschine und in
der Maschinenübersicht), was die Dokumente hergeben: welche der sechs Dokumenttypen da sind, eine
Abdeckungsmatrix (jedes Betriebsmittel, jede Klemme, jede SPS-Adresse mit Fundstellen je Dokumenttyp)
und eine Lückenliste aus Regeln zwischen zwei Dokumenttypen, etwa „Betriebsmittel im Plan, aber nicht in
der Stückliste“, „Klemme im Plan, aber nicht im Klemmenplan“, „SPS-Adresse im Programm ohne Symbol“ oder
„Blattverweis auf ein Blatt, das der Plan nicht hat“. Eine Regel greift nur, wenn beide Dokumenttypen
vorhanden sind. Rechenkern backend/app/ingestion/profile.py, Daten GET /api/sources/{id}/profile.
Die Beispielanlage FB-01 hat genau eine Lücke: Symbol M10.1 ohne Verwendung im AWL.
Diese beiden Funktionen arbeiten nur mit den hochgeladenen Dokumenten, ohne Claude-Aufruf:
-S1 → -S1:13 → -X3:1 → E0.0 → FB 10 NW 1 → Freigabe → NW 2 → A4.0 → -X3:9 → -K1:A1 → -K1 → -K1:2 → -X4:U → -M1. Schaltgeräte
laufen über ihre Anschlüsse nach IEC 60947-1: erst die Spule A1/A2, dann das Gerät, dann der
Schließer, Öffner oder Hauptkontakt, der weiterschaltet. Klick oeffnet das Blatt
mit markierter Spalte bzw. den AWL-Code, Doppelklick verfolgt ab dort. GET /api/signal-path.Symptom | Ursache | Abhilfe. Schaltschrank-Markierungen
bleiben optional (Vision kostet API-Tokens).Stromlauf stellt seine Funktionen als MCP-Server bereit: Maschinen, Kennzeichen, Dokumentensuche und Signalweg. Nur lesen, auf Stromlauf-Seite kein KI-Aufruf; das Sprachmodell ist der MCP-Client (Claude Desktop / Claude Code mit dem eigenen Abo, kein API-Guthaben). Voraussetzung: Backend läuft (Port 8010).
| Werkzeug | Zweck |
|---|---|
machine_details | Maschine mit Halle, Kennzahlen, Fehlerliste, Dokumentation |
search_tags, find_references | Wo kommt -K1 / -X3:1 / E0.0 vor, alle Fundstellen |
search_documents | Semantische oder wörtliche Suche in der Doku |
signal_path | Quellen und Folgen eines Kennzeichens (Klemmenplan, AWL) |
Claude Code (Pfade absolut, dann egal aus welchem Ordner gestartet):
claude mcp add stromlauf -- C:/dev/Repositories/stromlauf-ai/backend/.venv/Scripts/python.exe C:/dev/Repositories/stromlauf-ai/scripts/mcp_server.py
Claude Desktop (%APPDATA%\Claude\claude_desktop_config.json, Pfade anpassen; Schrägstriche / gehen unter Windows):
{
"mcpServers": {
"stromlauf": {
"command": "C:/dev/Repositories/stromlauf-ai/backend/.venv/Scripts/python.exe",
"args": ["C:/dev/Repositories/stromlauf-ai/scripts/mcp_server.py"],
"env": { "STROMLAUF_API": "http://127.0.0.1:8010" }
}
}
}
Als HTTP-Server (z. B. für den MCP Inspector): backend/.venv/Scripts/python scripts/mcp_server.py --http →
http://127.0.0.1:8765/mcp. Code: backend/stromlauf_mcp/, Suche über GET /api/search.
Tab Fehler, Knopf Zeigen an einem Eintrag: ein roter Balken über allen Tabs nennt den Fehler und seine
Kennzeichen. Gleichzeitig werden die betroffenen Bauteile im Schaltschrankfoto rot markiert. Der Balken zählt die
Treffer je Ansicht, springt per Klick dorthin und nennt nicht platzierte Kennzeichen. Rein aus Daten, kein
Modellaufruf. Logik in frontend/src/lib/faults.ts.
Tab Chat auf der Maschinenseite: der Scope ist fest die Wissensquelle der Maschine. Das Backend erzwingt
das über machine_id im Chat-Aufruf, die Auswahl anderer Quellen ist dort nicht möglich; der Agent bekommt
den Maschinenkontext (Name, Halle) in den Systemprompt. Ausnahme, bewusst: die handgepflegten
Fehlerlisten aller Maschinen bleiben werksweit durchsuchbar (Werkzeug search_faults), Treffer an anderen
Maschinen kennzeichnet der Agent als Erfahrung, nicht als Beleg. Die Chats einer Maschine sind die
Konversationen, deren Scope genau ihre Quelle ist (GET /api/conversations?source_id=…, keine neue Spalte).
Der Reiter Chat in der Navigation bleibt der werksweite Chat mit freier Quellenwahl.
Antworten sind fest gegliedert: Kurzantwort (max. 2 Saetze), Pruefen (max. 5 Schritte,
je ein Beleg), Sicherheit (nur wenn relevant), Details (eingeklappt). Belege schreibt das
Modell als [[Dateiname|Ort]]; die Oberflaeche macht daraus Chips und listet nur zitierte Stellen.
Ein Klick auf einen Stromlaufplan-Verweis wie /3.8 oeffnet rechts Blatt 3 und markiert Spalte 8.
Blatt und Spalten liest das Backend aus der PDF-Textebene (GET /api/documents/{id}/locate),
ohne Vision. Nennt die Frage ein Betriebsmittel (-K1), zeigt eine Befundkarte Einbauort,
Stromlaufplan-Verweise, Klemmen und SPS-Adressen aus dem Kennzeichen-Index (GET /api/facts), nicht
vom Modell. Der Einbauort kommt aus der Einbauort-Spalte der Stückliste (+ST1, +BP1, +AN1 …), den
Klartext („Schaltschrank +ST1") liefert die Kopfzeile derselben Datei; Leitungen tragen beide Orte.
Das Blatt eines Verweises steht im Schriftfeld unten auf der Seite. Erkannt werden „Blatt 3 / 7“, „Blatt 3 von 7“,
„Bl. 3“, „Sheet 3 of 7“, „Seite 5“ (EPLAN), „Folio : 3“ und „Page: 3“ (QElectroTech), getrennte Felder („Blatt“
klein, die Nummer darunter) und der EPLAN-Seitenname =ANL+ORT/3, auch hinter Deckblatt und Inhaltsverzeichnis und
auf hochkant gescannten Blättern. Querverweise und Inhaltsverzeichnisse im Plan zählen nicht als Blatt. Findet die
Blatt-Map keine Nummer, nimmt sie das Blatt an und sagt das: Am Dokument steht „Blatt-Map unsicher: Seite = Blatt
angenommen“ (oder welche Seiten keine eindeutige Nummer tragen), und die Zitatprüfung meldet Belege auf diese Blätter
als „nicht geprüft“ statt „gültig“.
Kommt die Dokumentation als eine PDF (EPLAN-, QElectroTech-Export mit Deckblatt, Inhaltsverzeichnis, Plan,
Klemmenplan und Stückliste), gibt es keine Stücklisten-Datei. Dann entsteht das Modell aus dem Kennzeichen-Index:
Teile sind die Betriebsmittel der Planseiten, Zonen die Blätter, auf denen sie zuerst vorkommen („Blatt 4 · Mains
Power Supply“). Die Blattnummer kommt aus dem Schriftfeld, auch hinter einem Deckblatt; ohne lesbare Nummer heißt die
Zone nach der Seite („Seite 4“). Die Blatttitel liest die Ingestion aus Inhaltsverzeichnis und Schriftfeld. Fehlt das
Inhaltsverzeichnis, wie im Schweizer Elektroschema, gilt das Feld im Schriftfeld als Titel, das sich von Blatt zu Blatt
ändert; Dokumentart, Anlage und Zeichner stehen überall gleich, das Datum zählt nicht. Bleiben zwei Felder übrig, bekommt
das Blatt keinen Titel. Seiten mit Titel „Stückliste“, „Nomenclature“ oder „Parts list“ liefern die Bezeichnungen. Kennzeichen ohne Minus im Blatt-Stil
(4Q1, 9K1) werden erkannt, wenn ein Dokument diesen Stil durchgängig nutzt, ebenso Klemmen wie im Schweizer
Elektroschema (X420 3 wird zu -X420:3, auch in der Suche) und Adressbereiche einer SPS-Karte wie E8.0..E9.7
mit beiden Enden. Bestehende Quellen brauchen dafür „Neu verarbeiten“.
Ohne Kopfzeile steht nur das Kennzeichen da — geraten wird nichts.
frontend/ Next.js + TypeScript: Wissensquellen, Upload, Chat (SSE-Streaming), Seiten-Viewer,
Werk (Maschinenuebersicht, Maschinenseite, Schaltschrank-Editor), shadcn/ui im
Blaupause-Design, Strg+K-Suche
backend/ FastAPI
app/ingestion/ Docling (PDF/Office -> Markdown je Seite), AWL-Parser, Kennzeichen-Index,
optionale Vision-Analyse der Schaltplanseiten (Claude)
app/agent/ LangGraph-Agent (Claude) mit Werkzeugen: search_knowledge, find_tag,
keyword_search, get_page, view_page, get_plc_block, list_documents
app/retrieval.py Hybrid-Suche: Vektor (pgvector, HNSW) + Volltext (tsvector 'german', GIN),
Fusion per Reciprocal Rank Fusion; search_knowledge und /api/search?mode=semantic
app/api/ REST + SSE; plant.py: Hallen, Maschinen, Kennzahlen, Fehlerliste,
Schaltschrank-Hotspots, Tag-Suche
app/werk/ Werk-Logik ohne DB und ohne Modell (Kennzahlen, Fehlerlisten-Treffer)
Postgres + pgvector Dokumente, Chunks mit Embeddings (HNSW), Kennzeichen-Index, Chats
SQLite LangGraph-Checkpointer (Gesprächsverlauf), backend/data/checkpoints.sqlite
Langflow (optional) docker compose --profile langflow up -d -> http://localhost:7860
Wie Zusammenhänge entstehen:
tag_occurrences): Jede Fundstelle von BMK, Klemme, SPS-Adresse und
Seitenverweis wird normalisiert gespeichert (E 0.0, %I0.0, I0.0 -> E0.0).
find_tag liefert damit Plan, Stückliste, Klemmenplan und AWL-Netzwerk in einer Abfrage.| Typ | Endungen |
|---|---|
| Stromlaufplan, Klemmenplan, Stückliste, Handbuch | .pdf, .xlsx, .csv, .docx, .pptx, .md, .html, .txt, Bilder |
| SPS-Programm | .awl (STEP 7 AWL-Quelle), .scl (TIA-Portal-Quelle: ein Chunk je Baustein mit Deklaration und Rumpf, keine Netzwerke) |
| Symboltabelle | .sdf |
Gescannte PDFs und Bilder liest die Ingestion selbst: Seiten ohne Textebene laufen durch die lokale Texterkennung
(backend/app/ingestion/ocr.py, RapidOCR auf der CPU, ohne Download und ohne API-Kosten, etwa 5 s je Seite). Sie legt
eine unsichtbare Textebene ins PDF, damit Suche, Blattverweise wie /3.4 und Zitatprüfung auf dem Scan genauso
funktionieren wie auf einem Text-PDF; das Original bleibt neben der Upload-Datei liegen. Am Dokument steht danach z. B.
„7 Seiten per OCR, Ø Konfidenz 0,98“. OCR_MODE=auto (Standard) erkennt nur Seiten ohne Textebene, always jede
Seite, off keine. Grenze: Klemmenbeschriftungen direkt neben dem Klemmensymbol liest die OCR oft ohne Minus
(„X1:2“ statt „-X1:2“); im Beispiel-Scan werden 81 % der Klemmen gefunden, Geräte und SPS-Adressen zu 99 bis 100 %.
Laufen die Signalwege eines Stromlaufplans als Spalten, etwa Taster, Klemme und SPS-Eingang untereinander, ordnet die Ingestion die Beschriftungen je Spalte. So bekommt der Chat zusammen, was im Plan zusammengehört, ohne die Seite als Bild anzusehen. Seiten, deren Kanäle als Zeilen laufen, bleiben in Lesereihenfolge.
Der PDF-Export ist der Vertrag: Stromlauf AI liest, was ein CAE-Werkzeug als PDF ausgibt (dazu Stücklisten und
Klemmenpläne als Tabelle, SPS-Quellen als .awl/.scl), nicht die Projektdatei des Werkzeugs. Nicht unterstützt:
.elk) und TIA-Portal-Projekte (.ap*). Dafür PDF-Export, Stückliste als .xlsx oder
.csv und die SPS-Bausteine als Quelle exportieren.Wie gut das Lesen auf einem eigenen Plan klappt, zeigt eine Gold-Vorlage (siehe „Fremd- und Firmendaten“).
Mit LANGFUSE_PUBLIC_KEY/LANGFUSE_SECRET_KEY in der .env und dem Extra
pip install -e "backend[trace]" schicken zwei Stellen Traces:
| Was | Session in Langfuse | Tags | Woher |
|---|---|---|---|
| Chat: Agent, Werkzeugaufrufe, Tokens, Kosten | Konversations-ID | stromlauf-ai, model:…, dazu ChatRequest.trace_tags | app/tracing.py als LangChain-Callback in graph.astream |
| Vision: Seitenanalyse beim Upload, Schaltschrank | Dokument- bzw. Bild-ID | ingestion plus seitenanalyse oder schaltschrank | derselbe Callback über app/tracing.py: vision_trace |
Ohne Schlüssel ist alles ein No-op: trace_config liefert ein leeres Dict. Die Schlüssel- und Paketprüfung
steht nur in app/tracing.py.
Nicht getrackt, weil ohne Modell und ohne Kosten: hybride Suche (app/retrieval.py), Embeddings
(bge-m3 lokal) und die deterministischen Parser (Signalweg, Steckbrief).
Im Chat-Strom kommt je Modellaufruf ein SSE-Ereignis usage mit Input-/Output-Tokens und Modell —
unabhängig von Langfuse, daraus rechnet der Eval-Lauf seine Kosten (app/pricing.py).
Live gegen Langfuse noch ungeprüft (hier ohne Schlüssel gelaufen); abgedeckt sind Konfiguration und
Weitergabe durch backend/tests/test_tracing.py.
eval/questions.jsonl enthaelt 69 Fragen mit Erwartungen (Pflichtangaben, verbotene Angaben, zu
zitierende Quellen) zu FB-01, UR-01, PM1-AR, Festo, AWL, Scans und Injection-Fallen, darunter fuenf Fallen
ohne Antwort im Material. Drei Schichten, bewertet ohne LLM-Richter:
python eval/run_retrieval.py # kostenlos: liefern die Werkzeuge die Belege? (Sekunden)
python eval/rescore.py eval/results/referenz_2026-09-26.json # kostenlos: gespeicherten Lauf neu bewerten
python eval/run_eval.py # Agentenlauf, kostet API-Tokens je Frage
Details in eval/README.md.
Testdaten mit fremder Lizenz (QElectroTech-Beispielprojekte, AWL-Quellen aus awlsim, Festo MPS) liegen nie im Repo.
python scripts/fetch_testdata.py lädt sie nach testdata/ (per .gitignore ausgeschlossen) und prüft jede Datei
gegen die SHA-256 in scripts/testdata_manifest.json, das je Datei Quelle, Lizenz und Zielpfad nennt. Ein zweiter
Lauf lädt nichts neu. Weicht eine Prüfsumme ab, überschreibt das Skript nichts und endet mit Exit 1. Für die
Festo-Unterlagen fehlt noch die Download-Quelle: Sie werden von Hand abgelegt und nur geprüft.
Eigene Firmendokumente gehören nach testdata/private/ (ebenfalls ausgeschlossen). So entsteht ein eigenes Gold:
python scripts/make_gold_template.py --doc testdata/private/anlage.pdf --out testdata/private/anlage.gold.json
python eval/run_ingest.py --gold testdata/private/anlage.gold.json --min 0.95
Die Vorlage enthält, was die Lesekette heute findet, und misst sich unverändert mit 1,0. Aussagekräftig wird sie, wenn jede Seite von Hand gegen das Dokument geprüft ist: fehlende Kennzeichen ergänzen, falsch gelesene löschen. An eine Stelle, die git committen würde, schreibt das Skript nicht.
Alle Prüfungen auf einmal, wie sie eine CI ausführen würde (ohne Cloud-Kosten, etwa 1 Minute):
backend/.venv/Scripts/python scripts/check.py # ruff, pytest, eslint, tsc, vitest
backend/.venv/Scripts/python scripts/check.py --install-hook # dasselbe automatisch vor jedem Push
Der GitHub-Workflow .github/workflows/ci.yml läuft bei jedem Push auf master und jedem Pull Request
(öffentliches Repo, Actions kostenlos): Backend, Frontend, E2E, Migration von null, Container-Build und ein
Secret-Scan mit gitleaks über die gesamte Historie bis zum geprüften Stand, mit fester Version und Prüfsumme;
Ausnahmen gibt es nur für die festen Testwerte der Backend-Tests (.gitleaks.toml) und einen begründeten
Fehlalarm in .env.example (.gitleaksignore). eval.yml fährt das
kostenlose Retrieval-Gate je PR und den bezahlten Agentenlauf wöchentlich (siehe „Evaluation“).
cd backend && .venv/Scripts/python -m pytest -q
cd frontend && npm run lint && npx tsc --noEmit && npm test
MIT, siehe LICENSE. Die Beispielanlagen unter examples/ (Förderband FB-01, Umroller UR-01, Aufrollung PM1-AR,
Testwerk, Injection-Test) sind frei erfunden und stehen unter derselben Lizenz. Testdaten mit fremder Lizenz
(Festo Didactic, awlsim GPLv2, QElectroTech GPL) liegen nur lokal unter testdata/ und sind per .gitignore
ausgeschlossen; scripts/fetch_testdata.py holt sie mit geprüften Prüfsummen (siehe „Fremd- und Firmendaten“).
Ein Secret-Scan mit gitleaks läuft in der CI über die gesamte Historie.
Jeder PR wird automatisch geprüft (CI, pr-title, merge-gate). PRs mit risk:low oder risk:medium mergen von selbst, sobald alle Checks grün sind. risk:high wartet auf das Label freigegeben. Details: AGENTS.md, Abschnitt „Pull Requests und Merge“.
cd frontend && npx playwright test prüft die Ansicht bei 390/768/1440 px gegen eine gemockte API
(frontend/e2e/), inklusive axe (WCAG 2 A/AA) und „kein horizontaler Body-Scroll“.