Kriterienextraktion
Aktueller Vertrag ab 21.10.2026. Dies ist die einzige Kriterienextraktion; frühere Ansätze sind samt Endpunkten und Datenbankobjekten entfernt.
Ablauf und Module
document_intake übernimmt authentisierte Dateivorkommen, ZIP-Grenzen, Pfade,
Fingerprints, Text-Encoding und Tabellen. criteria_extraction/documents.py
erstellt den multimodalen Input einschließlich Office-Bildern und Warnungen.
Die fachlichen Stufen sind service.py (Auftrag/Lose), criteria.py (je Dokument),
merging.py (Vergleich), review_models.py/review.py (Prüfung) und
supplement.py (neue Unterlagen). worker.py führt dieselben Stufen für beide Scopes aus.
jobs.py kapselt RPCs und öffentliche Statusantworten. results.py liest
veröffentlichte Ergebnisse ohne Schreibzugriff oder Prozessstatus. Nur ein exakter,
vollständiger Dokumentbestand samt erfolgreichem Vergleich gilt als abgeschlossen.
provider_resources.py verwaltet Dateinamen zur berechtigten Provider-Bereinigung.
Neue Ergebnisse verwenden den v2-Vertrag project-lots-v2.1 (Stand 21.10.2026):
sieben Kategorien mit 116 Bezeichnungen aus criteria_catalog.json, Nachweise mit
nie leerer frist (mit_angebot, mit_teilnahmeantrag, sonst auf_verlangen) und
herkunft. Bleibt eine Modellantwort auch nach der Reparatur ungültig, gilt das
Dokument als fehlgeschlagen und der Job hält mit Entscheidung. Ergebnisse mit criteria-1 (22.10. bis 01.11.2026) bleiben lesbar,
sind aber nicht mehr bearbeitbar oder ergänzbar. Gemeinsame Review-Typen sind frei
von Datenbank-/Provider-Abhängigkeiten. Vertrag aktualisieren:
services/agent/.venv/bin/python scripts/generate_criteria_contract.py
services/agent/.venv/bin/python scripts/generate_criteria_contract.py --check
Unter Windows lautet der Interpreter services/agent/.venv/Scripts/python.exe.
Korrektur- und Rückfallregeln
- Reparatur:
repair.pykorrigiert jede Antwort zentral vor der Schema-Prüfung: Fremde Felder entfallen, fehlende oder ungültige Werte erhalten Ersatzwerte, und nicht korrigierbare Kriterien, Informationen, Nachweise oder Quellen werden einzeln verworfen. Korrekturen stehen inextraktionshinweise; die Oberfläche zeigt sie im Kasten "Extraktionshinweise" am Ende des Ergebnisses, getrennt von den Prüfhinweisen. - Vergleich: Ab 200 Kriterien läuft er ohne formale Einreichungsanforderungen. Darunter folgt auf eine zu lange Antwort (
length) ein zweiter Aufruf ohne sie; scheitert auch dieser oder der Aufruf ab 200, wird ohne Zusammenführung gespeichert. Formale Kriterien bleiben immer erhalten. - Muss-Regel: Wäre das Ergebnis einer Zusammenführung nur ein Soll-Kriterium, bleiben enthaltene Muss-Kriterien einzeln bestehen.
- Wiederholung: Zeitüberschreitungen, Transportfehler und Inhaltsfilter (
content_filter) werden einmal automatisch wiederholt, eine zu lange Antwort (length) nicht; ungültige Antworten fragtgenerate()einmal nach. Abgelehnte Antworten stehen auf 100.000 Zeichen gekürzt unterrejectedim Kostenprotokoll. - Teilung: Liegt die Zählung, die
generateohnehin vornimmt, über 500.000 Tokens, endet der Aufruf vor dem bezahlten Modellaufruf mitInputTooLargeError. Schritt 1 halbiert die Dokumentliste dann nach Bytes (erste Hälfte im selben Schritt, zweite überextend, ein Checkpoint je Paket); ein einzelnes Dokument läuft ohne Grenzwert. Schritt 2 halbiert PDFs nach Seiten und führt die Teile zu einem Dokumentergebnis zusammen; andere Formate und einseitige PDFs laufen ungeteilt. Beide Schritte hinterlassen einen Extraktionshinweis.
API
Alle Pfade beginnen mit /api/vergabefit/criteria-extractions; erforderlich ist
eine verifizierte Supabase-Identität. OWNER/EDITOR dürfen schreiben, VIEWER lesen.
Private Ergebnisse sind ausschließlich für ihren Eigentümer zugänglich.
| Methode/Pfad | Vertrag |
|---|---|
POST / | {project_id?: UUID, entry_ids: UUID[], idempotency_key: string} → 202 Job |
GET /latest, GET /jobs/latest | optional ?project_id=UUID; ohne Parameter privater Scope |
GET /jobs/{id} | dauerhafter Status, Phase und Dokumentzähler; keine Tokens/Prompts |
POST /jobs/{id}/retry, /cancel | explizite Fortsetzung bzw. Abbruch; beides auch aus awaiting_decision |
POST /jobs/{id}/continue | 202; setzt einen pausierten Job ohne die fehlgeschlagenen Dokumente fort |
POST /jobs/{id}/documents | {remove_entry_ids, add_entry_ids} (mindestens eine nichtleer); ändert die Dokumentliste eines pausierten Jobs, startet ihn aber nicht |
GET /runs/{id}, /criteria | Fachobjekte bzw. gespeicherte Dokumentergebnisse |
GET/PUT /runs/{id}/review | Original, aktueller Stand, Katalog; Speichern mit expected_revision; andere Schema-Versionen 409 |
POST /runs/{id}/resume | {idempotency_key}; ausschließlich unvollständige historische V2-Läufe |
POST /runs/{id}/supplements | {entry_ids, idempotency_key}; erweitert den letzten vollständigen Stand |
POST /runs/{id}/fit-requirements | übernimmt das gespeicherte Review in den Fit-Check → {requirement_set_id, revision, count} |
Ein wiederholter identischer Auftrag liefert denselben Job. Derselbe Schlüssel
für eine andere Auswahl ergibt 409. Ein fehlgeschlagener Dispatch lässt den
gespeicherten Job queued; er kann ausdrücklich gestartet werden. GET startet
keine Verarbeitung. Die Statuswerte sind queued, running, awaiting_decision,
interrupted, failed, cancelled, succeeded; Phasen: project, extracting, merging, completed.
Entscheidungspause: Scheitern einzelne Dokumente, speichert der Worker jeden
Fehler als Schritt und beendet den Job mit awaiting_decision statt failed.
Erfolgreiche Schritte bleiben erhalten. Der Job hält den aktiven Scope weiter
(criteria_one_active_scope), bis entschieden wird: fehlgeschlagene Dokumente
über /documents entfernen oder ersetzen (Ersetzen = Entfernen plus Hinzufügen
in einem Aufruf) und mit /retry verarbeiten, mit /continue ohne sie
fortfahren oder mit /cancel abbrechen. Erfolgreiche Dokumente lassen sich nicht
entfernen. Fortsetzungsjobs (initial mit source_run_id) erlauben weder
/documents noch /continue (409); dort bleiben Wiederholen und Abbrechen.
Die Oberfläche zeigt die Entscheidung im Statusbereich der Dokumentenprüfung.
Private Läufe haben context_id und owner_id, aber NULL-Projekt/Organisation.
Die Kontext-ID bleibt in Auftrag und Losinformationen unverändert. Bereits
vollständige Ergebnisse werden nicht erneut über /resume verglichen.
Eine neue private Prüfung erzeugt einen neuen Vergabekontext. Das Ergänzen eines
vorhandenen privaten Ergebnisses ist eine gesonderte, ausdrücklich gewählte Aktion;
neue Uploads werden nicht automatisch der zuletzt geprüften Vergabe zugeschlagen.
Übernahme in den Fit-Check
Angebotsprojekte (kind = bid) übernehmen die geprüften Kriterien in den Fit-Check;
Ausschreibungsprojekte veröffentlichen sie (Abschnitt "Veröffentlichung").
Der Knopf "In Fit-Check übernehmen" erscheint im Schritt "Kriterien und Nachweise",
wenn der Nutzer schreiben darf, ein Review gespeichert ist und kein Job läuft. Er ruft
POST /runs/{id}/fit-requirements auf; fit_requirements.py übersetzt den
gespeicherten Review-Stand in das Criterion-Format von
services/agent/src/vergabefit/models.py, und shared/projects.save_revision
legt daraus eine neue vorgeschlagene Revision in procur_fit.requirement_sets an.
Übernommen werden alle Kriterien des gespeicherten Reviews, auch noch nicht
bestätigte; die Revision bleibt ein Vorschlag, bis sie im Fit-Check bestätigt wird.
Ablehnungen: VIEWER 403; andere Schema-Versionen als project-lots-v2.1 (etwa
criteria-1) 409; private Läufe und Ausschreibungsprojekte 409; ohne gespeichertes
Review 409; ein Review ohne übernehmbare Kriterien 422, ohne dass ein leeres
Requirement Set entsteht; eine gleichzeitig gespeicherte Revision 409.
Übersetzungsregeln:
- Kategorie:
ausschlussgrund→ausschluss;eignungundbepunktete_auswahl→eignung;zuschlag→zuschlag;angebots_mindestanforderungundausfuehrungsbedingung→leistung.formale_einreichungsanforderungund unbekannte Kategorien werden nicht übernommen. - Verbindlichkeit:
ausschluss = "Ja"ergibt immer "muss"; Zuschlags- und bepunktete Auswahlkriterien "soll"; sonst "soll" nur bei nicht verneintem "soll", "sollen", "wünschenswert" oder "vorteilhaft" in der Beschreibung, im Übrigen "muss". - Titel: deutsche Bezeichnung des Katalogcodes, sonst der Kategoriename, bei
Losbezug mit Losnummer ergänzt um " (Los N)". Anforderung ist die Beschreibung, Fundstelle
die erste Quelle; ihre Importeintrag-ID wird über
run.documentsauf dieprocur_docs-Dokument-ID abgebildet, damit der Fit-Check den Dokumenttitel zeigt. - Regel:
annual_turnoverwirdmin_turnover, wenn genau ein Euro-Mindestbetrag und eindeutige Geschäftsjahre genannt sind; Durchschnitts- oder Gesamtumsätze, unklare Beträge oder Jahre bleibenmanual_reviewmit offener Frage.sector_turnoverbleibt immermanual_review, weil die Regel nur den Gesamtumsatz prüft.reference_countwirdmin_references, wenn Anzahl und Zeitraum eindeutig sind. Ein Zeitraum zählt nur mit Vergangenheitsbezug ("letzten drei Jahre", "seit 5 Jahren"), ein Bezugsjahr nur als genanntes, nicht künftiges Jahr; eine Vertragslaufzeit oder "Formblatt 2024" ergibt keine Regel. Relative Jahre zählen ab dem Bewertungsdatum des Projekts.certificationmit "27001" wirdcertificate: iso27001, außer die Unterlagen lassen einen anderen oder gleichwertigen Nachweis zu. Die vier im Verfahren nachzuweisenden Codesregister_entry,professional_authorization,professional_membershipundprofessional_liability_insurancesowie alle Angebots-Mindestanforderungen und Ausführungsbedingungen werdenprocedural. Alles andere bleibtmanual_review.
Die Regeln werden aus Freitext abgeleitet und sind Heuristiken; der Fit-Check zeigt sie als Vorschlag zur Bestätigung.
Persistenz, Upgrade und Betrieb
Referenz: database/documents/criteria_extraction_jobs.sql; additive Migration:
202610212330_unified_criteria_extraction.sql. Die bisherigen Ergebnistabellen
criteria_extraction_v2_{runs,documents,merges,reviews} bleiben erhalten. Neue
Jobs, Checkpoints und Provider-Dateiregister sind nur für service_role zugänglich.
RLS der Ergebnistabellen erbt den autorisierten privaten oder Projekt-Scope.
Vor einem manuellen Staging-Sync Job-Worker stoppen; siehe database/README.md.
Migration und Referenzen werden über den bestehenden Release-Runner angewendet.
Der neue Schema-Vertrag verhindert, dass eine alte Release-Freigabe genügt.
Die abschließende Schema-Prüfung verlangt Tabellen, Kontext-/Lease-Spalten und
backend-exklusive RPC-Rechte auch nach Anwendung der zentralen Datenbankkonfiguration.
Die Entscheidungspause ergänzt Migration 202611011200_criteria_awaiting_decision.sql
(Statusprüfung und Index criteria_one_active_scope um awaiting_decision);
die Schema-Prüfung verlangt beides. Vor dem Einspielen dürfen keine Jobs
queued oder running sein. Diese Codeänderung führt keinen Remote-Sync und keine Datenkonvertierung aus.
CRITERIA_WORKER_ENABLED (Standard true) aktiviert ausschließlich gezielte Starts
und Lease-/Ressourcenpflege. CRITERIA_LEASE_SECONDS ist standardmäßig 90,
CRITERIA_RUN_TIMEOUT 14400. Vorhandene CRITERIA_V2_* Parallelitäts-
und Budgetvariablen bleiben aus Konfigurationskompatibilität gültig.
CRITERIA_V2_CRITERIA_THINKING_LEVEL (Standard medium) setzt die Denktiefe der
Dokumentschritte und gehört zur Versionsprüfung eines Jobs; ein Wechsel macht
unterbrochene oder pausierte Jobs nicht mehr fortsetzbar. CRITERIA_USD_EUR_RATE
(Standard 0,86) rechnet die Kosten des Kostenprotokolls in Euro um.
Die Upload-Grenzen heißen DOCUMENT_INTAKE_*.
Modellwahl: Alle Schritte rufen das Modell über service.generate() und Pydantic AI
auf, für Gemini, Claude und OpenAI auf demselben Weg. Das Modell kommt aus
procur_regx.platform_settings.criteria_model_id, ohne Auswahl aus LLM_MODEL.
Vor dem Anlegen prüft die API, ob sich das gewählte
Modell bauen lässt; fehlt der Schlüssel seines Anbieters, antwortet sie mit 503.
Der Job speichert das Modell als anbieter:modell in model und behält es bis zum
Ende, auch wenn er nach einer Pause oder Unterbrechung fortgesetzt wird und die
Auswahl inzwischen geändert wurde. Das Modell gehört deshalb nicht zur
Versionsprüfung; ältere Einträge ohne Präfix gelten als Google. Gemini erhält
Dateien ab 8 MB Gesamtgröße über die Files API. Claude erhält die Dokumente direkt
in der Anfrage und nimmt höchstens 600 PDF-Seiten und 128 MB pro Anfrage an; ein
größerer Bestand scheitert vor dem Aufruf mit einer Meldung, gekürzt wird nichts.
Der Worker wiederholt bei Modellaufrufen Ratenbegrenzungen (429), Serverfehler
(5xx, bei Claude auch 529) und verlorene Verbindungen aller Anbieter mit festen
Wartezeiten; die SDKs wiederholen diese Aufrufe nicht selbst. Gemini-Uploads wiederholt
nur das SDK. Claude-Anfragen laufen gestreamt, weil Anthropic einen offenen Stream alle
30 s anpingt: Bleibt er 120 s still, gilt die Verbindung als tot und der Aufruf scheitert
früh. Eine stumme Anfrage ließe sich von laufender Arbeit nicht unterscheiden.
Ausgabeschema und Prompt-Cache: Bei Gemini und OpenAI geht das Ausgabeschema als
native strukturierte Ausgabe an den Anbieter und steht nicht als Text im Prompt; nur
Gemini erzwingt es, OpenAI läuft mangels Strict-Tauglichkeit der Schemas ohne Zwang.
Claude erhält es als Text in den gecachten Anweisungen, weil die Schemas die
Grammatikgrenze von Anthropic überschreiten; Reparatur und Validierung der Antwort
gelten für alle Anbieter gleich. Scheitern sie oder die Abschlussprüfung des Schritts
(etwa Quellen oder Losbezug), fragt generate() einmal kostenpflichtig mit der
Fehlerliste nach, in einem eigenen Zeitfenster; erst eine zweite Ablehnung lässt den
Schritt scheitern. Eine unvollständige oder
blockierte Antwort wird nicht nachgefragt. Die Budgetprüfung
zählt es weiterhin mit, sofern der Anbieter eine Tokenzählung anbietet (nicht
bei OpenAI). Der Kriterien-Prompt (Regeln, Katalog, Auftrags- und Loskontext) ist
für alle Dokumente eines Jobs identisch. Bei Gemini wird er als expliziter Cache
angelegt, sobald in einem Segment mindestens zwei Dokumente ausstehen; bei
Fehlern laufen die Aufrufe ohne Cache. Die Ressourcenpflege
entfernt den Cache kurz nach dem Ende des Segments, auch bei awaiting_decision;
eine Wiederaufnahme legt einen neuen an. Bei Claude markiert jede Anfrage den
System-Prompt für das Prompt-Caching des Anbieters; einen eigenen Cache-Schritt
gibt es dort nicht.
Kostenprotokoll: Jedes Ausführungssegment eines Projektlaufs (Erfolg, Fehler,
Unterbrechung oder Halt) schreibt über procur_docs.log_criteria_job_usage (nur
service_role) eine Zeile in procur_docs.criteria_extraction_usage: Tokens und
ca. Kosten aller beantworteten Modellaufrufe des Segments, Dauer und fehlgeschlagene
Dokumente. Kosten berechnet das Protokoll nur für Modelle mit hinterlegtem Preis
(derzeit Gemini 3.8 Flash); für andere Modelle, etwa Claude, stehen dort nur die Tokens. Antworten, die unvollständig oder ungültig waren und verworfen wurden,
sind mitgezählt und unter discarded getrennt ausgewiesen. Die Tabelle ist nicht für Browser freigegeben, bleibt beim Löschen von
Ergebnissen erhalten und wird mit dem Projekt gelöscht; das Projekt selbst ändert
sich beim Protokollieren nicht. Private Läufe werden nicht
protokolliert; ein Protokollfehler beeinflusst den Job nicht.
Neustart/Lease-Verlust setzt unterbrochene Arbeit nicht automatisch fort. Ein Review- oder Ergänzungskonflikt überschreibt keine menschlichen Änderungen. Fehlende Berechtigung, entfernte Quellen, Versionswechsel und Budgetgrenzen beenden den Lauf sichtbar. Eine Anbieterantwort vor noch ungesichertem Checkpoint kann bei manueller Wiederholung weitere Kosten verursachen.
Veröffentlichung
Ein Ausschreibungsprojekt (kind = tender) kann seinen geprüften Stand veröffentlichen
(GET/PUT /runs/{id}/publication, POST /runs/{id}/publication/refresh;
Schalter "Auftrag veröffentlichen" im Schritt "Veröffentlichen"). publication.py schreibt
eine eigenständige Kopie in procur_fit.published_tenders (Notice, content, fit_criteria);
nur Kriterien mit review_status = 'geprüft' gelangen hinein, ein Review ohne solche Kriterien
lässt sich nicht veröffentlichen. Pro Projekt gibt es höchstens eine öffentliche Zeile,
published_at der Erstveröffentlichung bleibt. Zurücknehmen setzt is_public zurück und löscht
nichts; nach einem neuen Review bleibt der alte Stand sichtbar, bis er per "refresh" aktualisiert
wird. Löschen von Projekt, Lauf oder Organisation lässt die Zeile bestehen (on delete set null,
der Organisationsname steht zusätzlich als Text). Mitglieder können ein Projekt erst nach dem
Zurücknehmen löschen (projects_keep_published), denn ohne Projekt lässt sich eine öffentliche Zeile
nicht mehr zurücknehmen und ihre Dokumente sind nicht mehr abrufbar.
Rechte (Backend, nicht nur Oberfläche): Projekt kind = tender, Organisation mit org_kind
authority oder both und Rolle OWNER oder EDITOR; platform_admin unabhängig von der Rolle.
Unternehmen und Bewerbungen nie. Zurücknehmen darf auch, wer nicht mehr veröffentlichen darf.
Freigegebene Konten lesen öffentliche Zeilen, Mitglieder zusätzlich ihre eigenen; schreiben darf
nur service_role.
Quelle der Ausschreibungsliste: NEXT_PUBLIC_NOTICE_SEARCH unset oder published (Standard)
liest published_tenders, mock die Demo-Tabelle demo_notices bzw. die eingebauten Beispiele,
backend den Suchindex.
Startet ein Unternehmen eine Bewerbung auf eine veröffentlichte Ausschreibung (Quelle VF,
"Bewerbung starten"), legt startBidProject aus fit_criteria ein Requirement Set mit
source = ai_extracted an und bestätigt es, sodass Fit&Gap sofort startet. Das gilt auch beim
erneuten Öffnen einer bestehenden Bewerbung: Fehlt das Set oder blieb eine gleich große,
unbestätigte Kopie liegen, wird es ergänzt; jedes andere Set bleibt unberührt. Scheitert das
Übernehmen oder ist die Zeile nicht lesbar (etwa zurückgenommen), entsteht die Bewerbung
trotzdem, und das nächste Öffnen versucht es erneut. TED-, NAS- und Demo-Notices bleiben unverändert.
Das Requirement Set trägt zusätzlich requirement_sets.content: die vollständige, eigene Kopie der
geprüften Ausschreibung (Projekt- und Losinformationen, geprüfte Kriterien mit Nachweisen und Quellen,
dokumente). Die Bewerbung liest nur diese Kopie; spätere Aktualisierungen der Vergabestelle ändern
sie nicht. Sets ohne content (Altbestand, TED, Demo, manuelle Sets) zeigen die bisherige Ansicht.
Dokumentfreigabe: published_tenders.documents_public (Standard aus) schaltet die Vergabestelle mit
PUT /runs/{id}/publication/documents; ein Wechsel auf einen neuen Lauf übernimmt die Freigabe nicht.
Download nur, wenn die Freigabe an ist und die Ausschreibung öffentlich ist oder das Unternehmen eine
eigene Bewerbung darauf hat; Mitglieder der veröffentlichenden Organisation dürfen immer. Nach einer
Rücknahme bleiben content und Downloads für bestehende Bewerber erhalten, solange die Freigabe an
ist; neue Unternehmen haben keinen Zugriff.
Reiter (visibleSteps in lib/fit/types.ts): "Kriterien und Nachweise" erscheint erst mit gespeichertem
Ergebnis eines abgeschlossenen Dokumentenchecks, nicht während der Extraktion (die Fortschrittsanzeige
bleibt im Dokumentencheck). Bewerbung auf eine von einer Vergabestelle veröffentlichte Ausschreibung
(noticeSnapshot.source === "VF"): kein Dokumentencheck, es sei denn, das Projekt hat einen eigenen Lauf.
Bewerbung auf eine andere Bekanntmachung (TED, NAS, Demo): Dokumentencheck und "Angebotsanforderungen"
(interner Schlüssel requirements) immer. Blankprojekt: Dokumentencheck immer, "Angebotsanforderungen" erst
mit Requirement Set. Eigene Ausschreibung: Dokumentencheck immer. Solange das Ergebnis lädt, gelten die vom
Lauf abhängigen Reiter als vorhanden, damit ein Link oder Klick sein Ziel behält.
"Angebotsanforderungen" zeigt die Kopie nur lesend: Projekt- und Losinformationen, Kriterien je Kategorie
mit Nachweisen, Quellen und Dokumentbereich mit Download. Fit & Gap gruppiert die Muss-Kriterien je Los,
zeigt je Karte Beschreibung, Status, Begründung und Nachweise und eine Sektion "Passung nach
Projektdetails"; Zeilen ohne Eintrag in content erscheinen weiterhin. Nachweise mit
herkunft = 'unternehmensprofil' stehen rot mit "Abgleich mit Unternehmensprofil folgt",
angebotsspezifische dezent mit "Nur zur Information". Die Prüflogik (rules.ts) bleibt unverändert.
Der Abgleich der Nachweise sowie von CPV-Codes, Erfüllungsorten und Leistungsgegenständen mit dem
Unternehmensprofil folgt später.
Verifikation
Backendtests prüfen Modell-/Feldtreue, Adapter, Scope, Idempotenz, Wiederaufnahme, Abbruch, Fehler und Vollständigkeit. Frontendtests prüfen dieselben privaten und projektbezogenen Abläufe, Entwürfe, Schemawechsel und verlorene Startantworten. PGlite prüft RLS/RPCs und Migrations-Replay; echte PostgreSQL-Verbindungen prüfen konkurrierende Starts und Lease-Schreibzugriffe. Die CI entdeckt neue Datenbanktests automatisch. Deterministische Tests ersetzen keine Messung der fachlichen Qualität anhand echter Modellantworten.