README als Lern-Referenz, MIT-Lizenz, CI-Workflow
- 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>
This commit is contained in:
parent
20012a7d46
commit
4ba5f65f39
3 changed files with 185 additions and 75 deletions
30
.github/workflows/tests.yml
vendored
Normal file
30
.github/workflows/tests.yml
vendored
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
name: tests
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Python 3.12 einrichten
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
cache: pip
|
||||
|
||||
- name: Abhängigkeiten installieren
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r requirements.txt
|
||||
|
||||
- name: Playwright-Browser (für den E2E-Test)
|
||||
run: python -m playwright install --with-deps chromium
|
||||
|
||||
- name: Testsuite (Architektur-Regeln, Cache, Web, E2E)
|
||||
env:
|
||||
PYTHONPATH: src
|
||||
run: python -m pytest -q
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2026 Jeuners
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
209
README.md
209
README.md
|
|
@ -1,32 +1,108 @@
|
|||
# Angebots-Übersicht
|
||||
|
||||
[](https://github.com/Jeuners/timopro/actions/workflows/tests.yml)
|
||||
|
||||
Ortskonkrete, händlerübergreifende Übersicht wöchentlicher Supermarkt-Angebote,
|
||||
geordnet nach Produktgruppen. Neutral, nicht als Hochglanzprospekt.
|
||||
|
||||
## Der Gedanke
|
||||
**Live-Demo:** <https://dev.dillenberg.net/angebote/>
|
||||
|
||||
Die Aufgabe sieht nach einer KI-Aufgabe aus, ist es aber nur zur Hälfte. Sie
|
||||
---
|
||||
|
||||
## 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:
|
||||
|
||||
1. **Daten holen** -- deterministisch, ohne LLM. (Skill `angebote-fetch`)
|
||||
2. **Kategorisieren** -- die echte Ambiguität, hier gehört das LLM hin.
|
||||
(Skill `angebote-kategorisieren`)
|
||||
| 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. Die Architektur erzwingt den Schnitt.
|
||||
> Wer alles dem Modell gibt, bekommt erfundene Preise. Wer alles deterministisch
|
||||
> löst, scheitert an der Einordnung. Richtig ist die Arbeitsteilung.
|
||||
|
||||
Der zweite Punkt, der dieses Projekt trägt: Qualitätsregeln stehen nicht als
|
||||
gut gemeinte Bitten im Code, sondern als prüfbare Bedingungen -- kein
|
||||
Auffüllen, nur Belegtes, Abbruch statt stiller Drift. Genau dadurch "sagt das
|
||||
System, wenn es scheitert": nicht aus Einsicht des Modells, sondern weil eine
|
||||
externe Bedingung es erzwingt.
|
||||
Im Code ist dieser Schnitt nicht verhandelbar: Der Fetch-Teil enthält **keinen
|
||||
einzigen LLM-Aufruf** -- ein Test (`tests/`) prüft genau das.
|
||||
|
||||
## Entwicklung mit Claude Code
|
||||
### 2. Architektur statt Präferenz: Qualität als prüfbare Bedingung
|
||||
|
||||
`CLAUDE.md` ist der verbindliche Leitfaden. Die beiden `SKILL.md` in
|
||||
`.claude/skills/` sind die Spezifikationen der zwei Teile. Claude Code liest
|
||||
sie automatisch -- beim Arbeiten am jeweiligen Teil zuerst die passende
|
||||
SKILL.md lesen.
|
||||
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.md`
|
||||
verankert: 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
|
||||
|
||||
|
|
@ -35,7 +111,7 @@ python3 -m venv .venv && source .venv/bin/activate
|
|||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
## Nutzung
|
||||
## Nutzung (CLI)
|
||||
|
||||
```bash
|
||||
# PLZ direkt (immer verlässlich):
|
||||
|
|
@ -52,8 +128,8 @@ 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, statt ungeordnet weiterzulaufen. Modell überschreiben mit `--modell`,
|
||||
Anbieter erzwingen mit `--anbieter openrouter|anthropic`. Modelle auflisten:
|
||||
ab. Modell überschreiben mit `--modell`, Anbieter erzwingen mit
|
||||
`--anbieter openrouter|anthropic`. Modelle auflisten:
|
||||
`python -m angebote --modelle [suchbegriff]`.
|
||||
|
||||
## Web-UI
|
||||
|
|
@ -63,30 +139,19 @@ gewahrt. Sie macht den **zweistufigen Ablauf** sichtbar und erzwingt seine
|
|||
Reihenfolge:
|
||||
|
||||
1. **Stufe 1 -- Rohdaten holen & speichern** (deterministisch, kein LLM, kein
|
||||
Key): Abruf für eine PLZ, Persistenz pro PLZ/Woche unter `data/roh/`. Die
|
||||
belegte Rohliste ist für sich ansehbar.
|
||||
2. **LLM-Konfiguration** -- separates Panel mit **Anbieter-Umschalter**:
|
||||
Key): Abruf für eine PLZ, Persistenz pro PLZ/Woche unter `data/roh/`.
|
||||
2. **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). Ehrlicher Hinweis: kleine lokale Modelle (3-9 B) liefern für diese
|
||||
Batch-Aufgabe oft kein zuverlässiges Tool-Calling -- dann markiert das
|
||||
System alles als „Sonstiges/unsicher" statt zu raten. Brauchbare lokale
|
||||
Kategorisierung braucht ein starkes tool-fähiges Modell.
|
||||
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 (Chip im Kopf + im Ergebnis:
|
||||
„kategorisiert mit … (anbieter)").
|
||||
Anbieter + Modell werden gemerkt (`localStorage`) und überleben einen Reload;
|
||||
das gewählte Modell ist dauerhaft sichtbar.
|
||||
3. **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
|
||||
und belegter Quelle je Angebot.
|
||||
|
||||

|
||||
|
||||
Die gruppierte Ergebnisansicht nach Stufe 2:
|
||||
|
||||

|
||||
Produktgruppen gruppierte Übersicht mit Filtern, Unsicherheits-Markierung,
|
||||
Korrektur-Button und belegter Quelle je Angebot.
|
||||
|
||||
Starten:
|
||||
|
||||
|
|
@ -97,43 +162,30 @@ OPENROUTER_API_KEY=… PYTHONPATH=. uvicorn angebote.web:app --port 8077
|
|||
# Browser: http://127.0.0.1:8077/
|
||||
```
|
||||
|
||||
Der Key kann auch im UI-Konfigpanel eingegeben werden (bleibt lokal). Die App
|
||||
ist als **Single-User-Werkzeug für localhost** gedacht -- nicht mit
|
||||
`--host 0.0.0.0` ins Netz stellen (keine Auth auf den Endpoints).
|
||||
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.
|
||||
|
||||
## Stand der Implementierung (ehrlich)
|
||||
## Tests
|
||||
|
||||
- `requirements.txt` -- **vorhanden**.
|
||||
- `src/angebote/` -- **vorhanden**: Datenmodell, Adapter-Schnittstelle,
|
||||
Fetch-Orchestrator, Kategorisier-Schritt, Übersicht-Renderer, CLI.
|
||||
- `tests/` -- **57 Tests**: Architektur-Regeln (kein Auffüllen, Abbruch bei
|
||||
leerem/unauflösbarem Ort, Daten-Integrität nach Kategorisierung,
|
||||
geschlossene Kategorienliste, Unsicherheits-Flag, Schnitt-Test "kein LLM im
|
||||
Fetch-Teil"), Modell-Discovery/-Auswahl (OpenRouter + Ollama), Anbieter-/
|
||||
Retry-Logik, content-JSON-Fallback, Rohdaten-Persistenz und die Web-Endpoints.
|
||||
Laufen offline; ein E2E-Test (Playwright) prüft die Konfig-Persistenz im
|
||||
Browser.
|
||||
- **Web-UI + zweistufiger Flow** -- vorhanden und live verifiziert: Stufe 1
|
||||
(Fetch + Speichern) und Stufe 2 (LLM-Kategorisierung auf den gespeicherten
|
||||
Rohdaten, gesperrt bis Daten da sind) end-to-end gegen PLZ 60487 getestet.
|
||||
Kategorisierung modellstabil (gpt-oss-120b und gemini-3.1-flash-lite liefern
|
||||
praktisch dieselbe Gruppenverteilung).
|
||||
- **Live bestätigt:** der marktguru-Adapter wurde gegen die echte API getestet
|
||||
(PLZ 60487) und liefert reale, belegte Angebote (u. a. ALDI SÜD, PENNY, Lidl,
|
||||
REWE, Kaufland, nahkauf). Erkenntnisse aus dem echten Lauf, die direkt in den
|
||||
Code geflossen sind:
|
||||
- `offers/search` ist query-orientiert -- leeres `q` liefert 0 Treffer. Der
|
||||
Adapter aggregiert daher über Kategorie-Seedbegriffe (config) und weist
|
||||
diese **Teilabdeckung** ehrlich aus, statt Vollständigkeit zu behaupten.
|
||||
- Die pauschale Annahme "Aldi/Lidl fehlen bei Aggregatoren" wurde von den
|
||||
Daten **widerlegt** (beide sind enthalten). Die Abdeckung wird deshalb
|
||||
**datengetrieben** ausgewiesen (beobachtete Händler), nicht hartkodiert.
|
||||
- marktgurus Marken-Sentinel `thisisnobrand123` wird als "keine Marke"
|
||||
behandelt, nicht als Beleg durchgereicht.
|
||||
- **Voraussetzungen für den Live-Lauf:** installiertes `requests` (certifi für
|
||||
TLS), erreichbares Netz, von der Seite lesbare API-Schlüssel. Fehlt eines
|
||||
davon, ist das der vorgesehene Abbruchfall (Regel 4) mit *zutreffend*
|
||||
benannter Ursache -- keine Krücke.
|
||||
```bash
|
||||
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
|
||||
|
||||
|
|
@ -141,6 +193,7 @@ ist als **Single-User-Werkzeug für localhost** gedacht -- nicht mit
|
|||
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
|
||||
|
|
@ -154,12 +207,18 @@ src/angebote/ Implementierung
|
|||
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-Endpoints)
|
||||
web.py FastAPI-Web-UI (Stufe-1-/Stufe-2-/Korrektur-Endpoints)
|
||||
web_static/ Frontend (index.html)
|
||||
cli.py / __main__.py CLI-Einstieg
|
||||
tests/ 57 Architektur-/Web-/E2E-Tests
|
||||
tests/ Architektur-/Cache-/Web-/E2E-Tests
|
||||
data/roh/ generierte Rohdaten (ge-ignored)
|
||||
data/kategorie_cache.sqlite Produkt->Kategorie-Cache (ge-ignored)
|
||||
```
|
||||
|
||||
## Lizenz
|
||||
|
||||
[MIT](LICENSE).
|
||||
|
|
|
|||
Loading…
Reference in a new issue