- README führt jetzt mit der Lehre (der Schnitt, Architektur als prüfbare Regeln, Cache->Mini-Model, ehrliche Grenzen, KI-Sichtbarkeit) + Live-Demo-Link + ehrlicher Datenquellen-Hinweis (Bildungskontext, robots.txt, gekapselter Adapter) - LICENSE (MIT) - .github/workflows/tests.yml: volle Suite (inkl. Playwright-E2E) bei jedem Push -> Badge im README Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|---|---|---|
| .claude/skills | ||
| .github/workflows | ||
| docs | ||
| src/angebote | ||
| tests | ||
| .gitignore | ||
| CLAUDE.md | ||
| LICENSE | ||
| pyproject.toml | ||
| README.md | ||
| requirements.txt | ||
Angebots-Übersicht
Ortskonkrete, händlerübergreifende Übersicht wöchentlicher Supermarkt-Angebote, geordnet nach Produktgruppen. Neutral, nicht als Hochglanzprospekt.
Live-Demo: https://dev.dillenberg.net/angebote/
Worum es hier eigentlich geht
Dieses Repo ist mehr als ein Angebots-Tool. Es ist eine Referenz dafür, wie man KI richtig einsetzt -- nicht, indem man die ganze Aufgabe einem Modell übergibt, sondern indem man genau trennt, was deterministisch gehört und was nur ein LLM kann. Wer die fünf Punkte unten versteht, hat das Wesentliche.
1. Der Schnitt: Vor KI muss man erst KI einsparen
Die Aufgabe sieht nach einer KI-Aufgabe aus. Sie ist es nur zur Hälfte. Sie zerfällt in zwei strikt getrennte Teile:
| Teil | Wesen | Werkzeug |
|---|---|---|
| Daten holen (Preise, Gültigkeiten, Händler an Ort X) | reproduzierbar, exakt | kein LLM -- HTTP + Parser |
| Kategorisieren (ist Toffifee Süßware? gehört eine Blühpflanze in eine Lebensmittelliste?) | echte Ambiguität | LLM -- und nur hier |
Wer alles dem Modell gibt, bekommt erfundene Preise. Wer alles deterministisch löst, scheitert an der Einordnung. Richtig ist die Arbeitsteilung.
Im Code ist dieser Schnitt nicht verhandelbar: Der Fetch-Teil enthält keinen
einzigen LLM-Aufruf -- ein Test (tests/) prüft genau das.
2. Architektur statt Präferenz: Qualität als prüfbare Bedingung
Eine Regel, die nur als höfliche Bitte im Prompt steht („möglichst keine erfundenen Preise"), bricht unter Druck still weg. Eine Regel, die als prüfbare Bedingung im Datenfluss steht, hält. Drei solcher Regeln sind hier festverdrahtet -- und durch Tests abgesichert:
- Kein Auffüllen. Keine belegten Angebote für eine Gruppe → „keine Daten", niemals ein plausibel klingendes erfundenes Beispiel.
- Nur Belegtes. Preis, Gültigkeit, Händler stammen aus dem Datensatz, nicht aus dem Modell. Fehlendes Feld = als fehlend markiert, nicht geraten.
- Abbruch statt stiller Drift. Gibt die Datenlage die Anforderung nicht her (kein Treffer für den Ort), bricht das Programm mit klarer Ursache + Vorschlag ab -- statt ein „irgendwie vollständig aussehendes" Ergebnis zu liefern.
Deshalb „sagt das System, wenn es scheitert" -- nicht aus Einsicht des Modells, sondern weil eine externe Bedingung es erzwingt.
3. Der Cache macht ein Mini-Model möglich
Jedes vom LLM (oder per Hand) eingeordnete Produkt landet in einem schnellen
SQLite-Cache (titel+marke → gruppe, modell-agnostischer Schlüssel). Beim
nächsten Lauf überspringen bekannte Produkte das LLM komplett. In der Praxis
sinkt die LLM-Last über die Wochen drastisch -- bis ein kleines, billiges
Modell (oder gar keins) für die wenigen Neuzugänge reicht. KI sparsam machen,
indem man sie ihre eigene Arbeit zwischenspeichern lässt.
4. Ehrlich über Grenzen
- Lokale Modelle (3-9 B über Ollama) liefern für diese Batch-Tool-Calling-Aufgabe oft kein zuverlässiges Ergebnis. Das System rät dann nicht -- es markiert alles als „Sonstiges/unsicher". Der Mangel ist sichtbar, nicht kaschiert.
- Discounter-Abdeckung wird datengetrieben ausgewiesen (beobachtete Händler), nicht behauptet. Die pauschale Annahme „Aldi/Lidl fehlen bei Aggregatoren" wurde von den echten Daten widerlegt -- also steht sie auch nicht im Code.
5. Mensch im Loop + sichtbare KI-Arbeit
- Unsichere (oder falsch eingeordnete) Angebote sind per Klick korrigierbar; die
Korrektur fließt als stärkstes Cache-Signal (
modell="manuell") zurück -- das Produkt ist danach nie wieder unsicher. - Jede LLM-Aktion zeigt sichtbar Aktivität (animierter Indikator, Fortschritt,
Modellname, Button-Ladezustand). Das ist als Grundregel in
CLAUDE.mdverankert: deterministische Schritte dürfen still laufen, LLM-Schritte nicht.
So sieht es aus
Zweistufige UI -- Rohdaten holen (deterministisch), dann kategorisieren (LLM):
Die nach Produktgruppen gruppierte Ergebnisansicht nach Stufe 2:
Die Datenquelle (ehrlich)
Der erste echte Adapter spricht die öffentliche Angebots-API von marktguru
an. Das ist ein Bildungs-/Recherche-Projekt, kein Produkt: Der Abruf
respektiert robots.txt, drosselt sich und bricht bei fehlender Erlaubnis
sauber ab (Regel 4), statt blind weiterzulaufen. Die Quellen-Schicht ist hinter
einer Adapter-Schnittstelle (quellen/basis.py) gekapselt -- ein anderer
Anbieter mit ausdrücklicher API-Erlaubnis lässt sich ohne Eingriff in den Rest
ergänzen. Wer das Projekt produktiv nutzen will, klärt die Nutzungsbedingungen
der jeweiligen Quelle selbst.
Setup
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
Nutzung (CLI)
# PLZ direkt (immer verlässlich):
python -m angebote 60487
# Ortsname (nur für die in config.py hinterlegte Auswahl großer Städte;
# unbekannter Ort -> ehrlicher Abbruch mit Vorschlag, keine Notlösung):
python -m angebote "Frankfurt"
# Ohne Kategorisierung (kein LLM, flache belegte Liste):
python -m angebote 60487 --no-llm
Der Kategorisier-Schritt braucht einen LLM-Zugang in der Umgebung --
OPENROUTER_API_KEY (empfohlen, viele Modelle) oder ANTHROPIC_API_KEY.
Fehlt beides und es wird kein --no-llm gesetzt, bricht das Programm ehrlich
ab. Modell überschreiben mit --modell, Anbieter erzwingen mit
--anbieter openrouter|anthropic. Modelle auflisten:
python -m angebote --modelle [suchbegriff].
Web-UI
Lokale FastAPI-App -- dünne Schicht über denselben Modulen, der Schnitt bleibt gewahrt. Sie macht den zweistufigen Ablauf sichtbar und erzwingt seine Reihenfolge:
-
Stufe 1 -- Rohdaten holen & speichern (deterministisch, kein LLM, kein Key): Abruf für eine PLZ, Persistenz pro PLZ/Woche unter
data/roh/. -
LLM-Konfiguration -- Panel mit Anbieter-Umschalter:
- OpenRouter (Cloud): Key + Modellauswahl (Liste/Suche/Aktualisieren).
Default
deepseek/deepseek-v4-flash-- günstig (~1-2 Cent/Lauf), verlässlich. - Ollama (lokal): zeigt die lokal installierten Modelle (kein Key, kein Netz) -- mit ehrlichem Hinweis zur Tool-Calling-Grenze kleiner Modelle.
Anbieter + Modell werden gemerkt (
localStorage) und überleben einen Reload; das gewählte Modell ist dauerhaft sichtbar. - OpenRouter (Cloud): Key + Modellauswahl (Liste/Suche/Aktualisieren).
Default
-
Stufe 2 -- Kategorisieren (LLM): läuft nur auf den gespeicherten Rohdaten und ist gesperrt, solange keine vorliegen. Ergebnis ist die nach Produktgruppen gruppierte Übersicht mit Filtern, Unsicherheits-Markierung, Korrektur-Button und belegter Quelle je Angebot.
Starten:
pip install -r requirements.txt # enthält fastapi + uvicorn
cd src
OPENROUTER_API_KEY=… PYTHONPATH=. uvicorn angebote.web:app --port 8077
# Browser: http://127.0.0.1:8077/
Die App ist als Single-User-Werkzeug für localhost gedacht -- nicht mit
--host 0.0.0.0 ungeschützt ins Netz stellen (keine Auth auf den Endpoints).
Die öffentliche Demo läuft hinter einem Reverse-Proxy als Schaufenster.
Tests
python -m pytest -q
Die Suite prüft die Architektur-Regeln, nicht nur den Happy Path: kein Auffüllen, Abbruch bei leerem/unauflösbarem Ort, Daten-Integrität nach der Kategorisierung, geschlossene Kategorienliste, Unsicherheits-Flag, der Schnitt-Test „kein LLM im Fetch-Teil", Cache-Verhalten (bekannte Produkte überspringen das LLM), die Korrektur-zu-Cache-Schleife, Modell-Discovery (OpenRouter + Ollama), Anbieter-/Retry-Logik und die Web-Endpoints. Alles läuft offline (Fakes); ein E2E-Test (Playwright) prüft die Konfig-Persistenz im Browser. CI führt die volle Suite bei jedem Push aus (Badge oben).
Entwicklung mit Claude Code
CLAUDE.md ist der verbindliche Leitfaden. Die beiden SKILL.md in
.claude/skills/ sind die Spezifikationen der zwei Teile -- beim Arbeiten am
jeweiligen Teil zuerst die passende SKILL.md lesen, den Schnitt nie vermischen.
Struktur
CLAUDE.md Leitfaden / Architektur
README.md dieses Dokument
requirements.txt Abhängigkeiten
.github/workflows/tests.yml CI: volle Testsuite bei jedem Push
docs/ Screenshots für dieses Dokument
.claude/skills/angebote-fetch/ Spec: deterministischer Datenabruf
.claude/skills/angebote-kategorisieren/ Spec: LLM-gestützte Einordnung
src/angebote/ Implementierung
modell.py eingefrorenes Angebot-Datenmodell
fehler.py AbbruchFehler (Regel 4)
config.py Quellenliste, Produktgruppen, Orts-Auflösung
quellen/ ein Adapter pro Quelle (rein = Ort, raus = [Angebot])
basis.py Adapter-Schnittstelle + Ort
marktguru.py erster echter Adapter, geprüfter Ortsbezug
fetch.py Orchestrator (Ort rein, belegte Angebote raus)
speicher.py Persistenz der belegten Rohdaten (Stufe 1), kein LLM
kategorisieren.py LLM-Schritt hinter Protokoll (OpenRouter/Anthropic), testbar
produktcache.py schneller SQLite-Cache (titel+marke -> gruppe), kein LLM
modelle.py OpenRouter-Modell-Discovery (Liste/Suche/Top-Free)
modellauswahl.py interaktive Modellauswahl (CLI)
uebersicht.py Gruppierung + Rendering (Markdown + JSON-Struktur)
web.py FastAPI-Web-UI (Stufe-1-/Stufe-2-/Korrektur-Endpoints)
web_static/ Frontend (index.html)
cli.py / __main__.py CLI-Einstieg
tests/ Architektur-/Cache-/Web-/E2E-Tests
data/roh/ generierte Rohdaten (ge-ignored)
data/kategorie_cache.sqlite Produkt->Kategorie-Cache (ge-ignored)
Lizenz
MIT.

