Files
..

Wissensdatenbank ETL-Pipeline

Worum geht es? (in einfach)

Dies ist die zentrale Wissenssammlung fuer den 1st-Level-Support und Chatbots rund um DB InfraGO. Wir sammeln hier das Wissen aus vielen Quellen (Confluence, Webseiten, PDFs), pruefen es und legen es versioniert im Git-Repo ab. Damit ist dieses Repo die Single Source of Truth: Wenn etwas hier steht und freigegeben ist, gilt es - und genau das lesen die Chatbots.

Was ist „ETL"? Drei einfache Schritte, die bei jeder Aufnahme passieren:

  1. Extract = Sammeln. Wissen aus den Quellen holen (Confluence-Seiten, Webseiten, PDFs).
  2. Transform = Aufbereiten. In einheitliches Markdown umwandeln, mit Schlagworten/Scope versehen und vertrauliche Inhalte herausfiltern.
  3. Load = Ablegen. Das geprueefte Wissen versioniert nach output/processed/ schreiben - die zentrale Wahrheit, aus der Chatbots/Vektor-DB lesen.

Kurz: Quellen rein → aufbereiten/pruefen → als Single Source of Truth ablegen.

Ziel

Klar definieren, welches Wissen in Chatbots darf und welches nicht. Wissen wird bei der Aufnahme speziell verarbeitet und bereitgestellt, sodass Chatbots es nutzen koennen. Klassifikation nach Scope:

  • allgemein darf intern und extern genutzt werden; nicht toolspezifisch (z.B. Regulierung/INB, Kundeninformationen).
  • intern nur DB InfraGO intern.
  • extern public im Internet bzw. fuer EVUs/EIUs.

Technisch eine ETL-Pipeline fuer LLM/RAG: holt Wissen aus Confluence, Webseiten und PDFs, klassifiziert es nach Scope, filtert vertrauliche Inhalte und gibt nur geprueftes Wissen frei (output/processed/<scope>/... = Feed fuer die Vektor-DB).

Architektur & vollstaendiger Wissensfluss inkl. Diagramme: .kiro/steering/architecture.md · Manuelle Einrichtungsschritte: docs/SETUP.md

Denkmodell

Domaene -> Scope (intern|extern|allgemein) -> Tool -> Dokument(e)
  • Domaene: fachlicher Bereich (jedes Tool ist eigene Domaene; regulierung, kundeninfo fuer allgemeines Wissen).
  • Scope: intern | extern | allgemein (allgemein gilt fuer intern UND extern).
  • Tool: eine Fachanwendung aus dem 1st-Level-Support (Katalog automatisch aus Seite 428909879).
  • Strategie pro Quelle siehe Tabelle unten.

Strategien (Verarbeitung pro Quelle)

Strategie Was sie tut Wichtige Options
confluence_page genau diese eine Seite scope (auch intern,extern), incremental, attachments
confluence_tree Seite inkl. Unterseiten max_depth (-1=alle, 0=nur Seite), incremental, attachments
confluence_faq FAQs aus Tabellen incremental
crawler Webseite: Index → Detailseiten detail_pattern, selector, max_pages
sitemap Detailseiten aus (gz-)Sitemap sitemap_url, url_pattern, limit, incremental
gitlab_md Markdown-Dateien aus einem GitLab-Repo ref, path, max_depth (Token: GITLAB_TOKEN)
file Dateien (md/pdf) aus dem Repo-Ordner files/ max_depth
pdf PDFs einer Seite → Markdown (1→n) split: headings|pages, heading_pattern, keep_raw, redact

Confluence-Anhaenge (eingebundene PDFs)

Bei confluence_page/confluence_tree werden eingebundene PDF-Anhaenge (view-file/viewpdf-Makros) automatisch geparst und als eigene Dokumente im selben Feed abgelegt (Frontmatter kind: attachment, parent_url zeigt auf die Seite). Scope/Domain/Owner werden von der Seite geerbt. Inkrementell ueber die Attachment-Version (unveraenderter Anhang -> kein Re-Parse). Default an, pro Quelle ueber options: { attachments: false } abschaltbar.

Bilder werden nicht binaer uebernommen; der alt-Text/Dateiname bleibt als [Bild: ...] im Markdown - gibt dem RAG Kontext ohne Volumen. Datei-Embeds ohne Parsing-Pfad (z.B. Office-Docs) tauchen als [Anhang: ...]-Marker im Seitentext auf.

Mehrere Quellen pro Tool

Ein Tool kann beliebig viele, gemischte Quellen haben einfach weitere Eintraege unter sources: anhaengen (z.B. mehrere Confluence-Quellen mit max_depth, eine interne und eine externe Seite, oder zusaetzlich eine FAQ-Seite). Siehe die Tools pathos (mehrere Quellen inkl. confluence_faq und intern,extern) und nur.

intern vs. extern wie wird klassifiziert?

Es gibt keine Inhaltstrennung innerhalb einer Seite mehr. Klassifiziert wird pro Tool und pro Quelle:

Tool-scope (in config/tools.yaml):

Tool-scope Bedeutung
intern alles intern (Default, restriktiv z.B. Salesforce, NuR)
extern alles extern
allgemein gilt fuer intern UND extern
mixed Tool hat Quellen mit unterschiedlichem Scope (z.B. eine interne und eine externe Seite)

Source-scope (pro Quelle, gilt fuer die ganze Seite):

  • intern | extern | allgemein
  • "intern,extern" → die Seite wird fuer beide Scopes genutzt (dupliziert).

Quellen ohne eigenen scope erben den Tool-Scope (bei mixed => intern). Wichtig: Hat ein Tool gemischtes Wissen, braucht es im Zweifel zwei Seiten (eine interne, eine externe) als zwei Quellen statt einer gemischten Seite.

Allgemeines, tool-uebergreifendes Wissen steht separat in config/general.yaml.

Der Tool-Katalog wird initial mit scripts/bootstrap_tools.py erzeugt und danach manuell in config/tools.yaml gepflegt. Uebersicht aller Quellen: GitLab-Pages-Seite „Wissensquellen" (config.html, geparst aus tools.yaml/general.yaml/approvals.yaml).

Neues Wissen hinzufuegen (Workflow)

Jedes Wissen hat owners (intern Verantwortliche, Accountability) und einen contact (herausgebbare Kontaktadresse, Default einfachbahn@deutschebahn.com). Beide stehen im Frontmatter jedes Dokuments und auf der Pages-Seite, damit klar ist, wer verantwortlich ist und an wen man sich wenden darf.

  1. Issue anlegen mit der Vorlage „Neues Wissen" (.gitlab/issue_templates/): Tool/Domaene, Quellen je Zeile (URL | Strategie | Scope), Verantwortliche (owners) und optional Kontakt (Default einfachbahn@deutschebahn.com).
  2. Reviewer uebernimmt die Angaben nach config/tools.yaml bzw. config/general.yaml (neuer Tool-/Quellen-Eintrag inkl. owners:) per GitLab Web-IDE (Variante A) oder manuell (Variante B).
  3. Merge Request öffnen. Die MR-Freigabe ist das Quality Gate (4-Augen, via CODEOWNERS + Protected Branch).
  4. Vorschau im MR: den preview-Job manuell starten und PREVIEW_ONLY auf Tool-Id/Domaene/URL der neuen Quelle setzen. Er verarbeitet nur diese Quelle und stellt die erzeugten Markdowns als Job-Artefakt bereit. Lokal alternativ python -m src.main --only "<tool|domaene|url>" --data preview.
  5. Merge → der ETL-Schedule verarbeitet die Quelle (stuendlich MoFr, inkrementell) und committet das Wissen; die Vektor-DB liest direkt aus output/processed/<scope>/....

Freigabe-Prozess

MR mergen (Mensch) -> ETL: Extract -> Transform -> Auto-Filter -> output/processed/ -> Vektor-DB
  • Freigabe = Merge Request (Mensch, das eigentliche Gate): Wer eine Quelle (Link + Strategie + Scope) in config/tools.yaml/general.yaml eintraegt, stellt einen MR. Beim Merge (4-Augen via CODEOWNERS + Protected Branch) wird entschieden, welche Quelle mit welchem Scope aufgenommen wird. Danach laeuft der ETL automatisch.
  • Auto-Filter im ETL (Sicherheitsnetz, kein Freigabe-Knopf): setzt pro Dokument review_status standardmaessig approved:
    • approved -> output/processed/... (direkt live, sobald gemergt + ETL gelaufen)
    • pending -> staging/pending/ (Blacklist-Treffer, Scope-Warnung, oder Inhalt < 50 Zeichen Grund im Frontmatter review_notes)
    • trusted: true (z.B. INB) bleibt trotz Treffer approved; redact: false schaltet die Redaction pro Quelle ab.
  • config/approvals.yaml (nachtraegliche Korrektur): hebt einzelne pending-Dokumente per URL oder hash:<content_hash> doch auf approved. Kein Schritt, den jedes Dokument durchlaeuft. Den Hash zeigt die Pages-Seite (Detail-Fenster).
  • Vektor-DB liest direkt aus output/processed/<scope>/<domaene>/ (implementiert): Nur freigegebenes Wissen liegt dort, der Scope steckt im Ordnerpfad und im Frontmatter (inkl. owners/contact). Kein separater Export-/Ingestion-Schritt processed/ IST der Feed. Der externe Index nimmt nur extern/ + allgemein/.
  • output/_meta.json: globale Statusdatei fuer nachgelagerte Systeme mit last_run (letzter ETL-Lauf), last_change (wann sich der Bestand zuletzt inhaltlich/metadatenseitig geaendert hat), documents, by_scope und einer content_signature. Ein Index-/RAG-Consumer kann daran erkennen, ob ein erneutes Einlesen ueberhaupt noetig ist.
  • output/_index.json: dokument-genauer Katalog des gesamten Bestands. Pro Voll-Dokument: domain/tool/scope, url, path, content_hash, last_updated und - falls vorhanden - die zugehoerigen chunks (Anzahl + Pfad) und attachments (Anhang-Dokumente: Name + Pfad). Anhang-Dokumente haben zudem kind: "attachment" und parent_url. Aggregate: documents_total, chunks_total, attachments_total, by_domain (mit documents/chunks/attachments), by_scope. So sieht ein Anschliesser auf einen Blick, was pro Domaene/Tool wo liegt und welche Dokumente zusaetzlich als Chunks oder Anhaenge vorliegen (z.B. INB = Voll-Dokument und Chunks; pathOS-Seite = Voll-Dokument und Anhang-PDF). Deterministisch (kein Zeitstempel) -> aendert sich nur bei echten Bestandsaenderungen.
  • output/run_log.jsonl: append-only Lauf-Historie (1 JSON-Zeile je ETL-Lauf): Zeitstempel, verarbeitete Dokumente, Status-Counts und Fehler je Quelle (z.B. fehlgeschlagene Confluence-Abrufe). Auf die letzten 500 Laeufe gekappt. So sieht man - auch historisch - ob ein Lauf sauber durchlief, ohne die fluechtigen CI-Job-Logs zu durchsuchen. Die Uebersichtsseite zeigt zusaetzlich eine kompakte Health-Zeile („N Dok verarbeitet, M Quellen mit Fehler") in der Fusszeile.

Wo liegt das final freigegebene Wissen?

Ausschliesslich unter output/processed/<scope>/<domaene>/[<tool>/] im Repo (versioniert, = Single Source of Truth). pending-Dokumente liegen in staging/pending/ und werden zur Transparenz ebenfalls committet (auf der Pages-Seite einsehbar). Der Audit-Trail steht in staging/review_report.json und in der Git-Historie.

Lokal testen

python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt

ruff check src tests          # Lint
python -m pytest -q           # Offline-Tests
python -m src.main --config config/tools.yaml --data output --staging staging   # Live-Lauf
python -m src.chunk --data output                           # Chunks erzeugen (offline, optional)

Confluence aktivieren

export CONFLUENCE_URL="https://arija-confluence.jaas.service.deutschebahn.com"
export CONFLUENCE_TOKEN="<PAT>"     # Bearer-PAT (Server/DC)
pip install atlassian-python-api

Ohne Credentials werden Confluence-Quellen sauber uebersprungen.

Betrieb (GitLab CI)

  • quality-Stage: ruff (Lint) + gitleaks (Secret-Scan) bei jedem Push/MR.
  • etl-Stage: stuendlich MoFr 817 Uhr (Cron 0 8-17 * * 1-5), baut Wissen, committet data/ nach main (per GIT_PUSH_TOKEN); der Commit loest automatisch einen pages-Refresh aus.
  • pages-Stage: baut public/ (Uebersicht, Hilfe, Chatbot-Anschluss, Wissensquellen) fuer GitLab Pages auf main.
  • preview-Job (MR, manuell): verarbeitet eine neue/geaenderte Quelle via --only und stellt die erzeugten Markdowns als Job-Artefakt bereit (zum Pruefen vor dem Merge). PREVIEW_ONLY beim Start auf Tool-Id/Domaene/URL setzen.
  • Kundeninfos inkrementell: incremental: true crawlt nur NEUE Detailseiten und ueberschreibt vorhandene nicht Re-Runs bleiben schnell.
  • Confluence inkrementell: options: { incremental: true } prueft pro Seite die Confluence-Versionsnummer (source_version im Frontmatter). Unveraenderte Seiten werden uebersprungen, nur geaenderte/neue Seiten neu verarbeitet spart API-Last.
  • Re-Tagging bei Metadaten-Aenderung: Aenderst du in tools.yaml/general.yaml nur tags/owners/contact einer Quelle, weicht deren meta_fingerprint (im Frontmatter) vom gespeicherten ab. Der naechste inkrementelle Lauf passt dann die bestehenden Dateien dieser Quelle in-place an (nur Frontmatter, kein erneuter Download/API-Call).
  • Scope-Wechsel: Aenderst du den scope einer Quelle, wird die Seite neu verarbeitet und an output/processed/<neuer-scope>/... geschrieben; die alte Ablage wird nach erfolgreichem Schreiben automatisch geloescht (domaenen-intern, damit eine URL, die legitim unter mehreren Domaenen liegt z.B. eine pathOS-Seite auch unter web nicht versehentlich entfernt wird).
  • Domain-Umbenennung: Die neue Ablage entsteht automatisch, die alte Domain-Ablage bleibt aber stehen (domaenenuebergreifendes Loeschen waere wegen geteilter URLs unsicher). Den alten output/processed/<scope>/<alte-domain>/-Ordner daher bei einer Domain-Umbenennung manuell loeschen.
  • CI/CD-Variablen: GIT_PUSH_TOKEN, CONFLUENCE_URL, CONFLUENCE_TOKEN (Masked).
  • scm-info.yaml: Pflichtdatei fuer DB-GitLab-Compliance.

Konfiguration

  • config/tools.yaml Tools, Domaenen, Quellen, Strategien, Scopes, Tags
  • config/filter_rules.json Blacklist-Keywords, Regex-Redaction
  • config/chunking.yaml gemeinsame Chunk-Defaults (pro Quelle ueber options.chunk ueberschreibbar)

Chunking (optional, Default aus)

Grosse, stark gegliederte Dokumente (v.a. INB) werden zusaetzlich zur Voll-Datei in Chunks entlang der Ueberschriften zerlegt ein abgeleitetes, jederzeit neu erzeugbares Artefakt unter output/chunks/<scope>/<domaene>/<docslug>/. Das Voll-Dokument in output/processed/ bleibt unangetastet (Parent-Document-Muster): ein Anschliesser waehlt Voll-Dokument, Chunks oder beides. Es werden bewusst nur deterministische, embedding-freie Strategien angeboten (headings | faq | recursive); semantisches Chunking gehoert in die Vektor-DB des Anschliessers. Jeder Chunk bekommt eine kurze Contextual-Retrieval-Zeile (> Kontext: <Dokument> > <Abschnitt>). Aktivierung pro Quelle via options: { chunk: headings }.

Inkrementell (kein Voll-Rebuild): Chunking laeuft am Ende jedes ETL-Laufs automatisch mit (src.main) und ist Teil von data/. Pro Dokument wird nur dann neu gechunkt, wenn sich der Inhalt (parent_hash) ODER die wirksamen Optionen/Strategie (chunk_fingerprint) geaendert haben - sonst wird das Dokument uebersprungen (keine Datei-Aenderung, kein Git-Churn). Aenderst du also bei einer Quelle z.B. chunk_level oder von headings auf recursive, werden nur die Dokumente dieser Ablage neu erzeugt. Wird chunk wieder auf off gestellt oder ein Voll-Dokument geloescht, werden die zugehoerigen Chunks automatisch entfernt.

Feinsteuerung (Default aus, pro Quelle ueberschreibbar):

  • chunk_min_doc_chars: Dokumente unter dieser Zeichenzahl bleiben ganz (kleine FAQ-/How-to-Seiten muessen nicht zwingend gechunkt werden).
  • chunk_kind: chunkt nur Dokumente mit passendem kind ("" = alle; attachment = nur Anhang-PDFs; document = nur Voll-Seiten).

Beispiel pathOS (an der Tool-Tree-Quelle in config/tools.yaml): grosse Anhang-PDFs (z.B. EVU-Schnittstellen-Doku, 2,8 MB) werden gechunkt, Seiten und FAQs bleiben ganz.

options:
  incremental: true
  chunk: headings
  chunk_kind: attachment        # nur Anhang-PDFs chunken
  chunk_min_doc_chars: 3000     # < 3k Zeichen bleiben ganz (~750 Token)

Version & Changelog

Aktuelle Version steht in VERSION (SemVer), die Historie in CHANGELOG.md (Format „Keep a Changelog"). Beides wird auf der GitLab-Pages-Seite „Changelog" angezeigt (Version auch in der Navigationsleiste). Pflege-Regeln: .kiro/steering/changelog-versioning.md.