Zum Inhalt springen

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.py korrigiert 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 in extraktionshinweise; 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 fragt generate() einmal nach. Abgelehnte Antworten stehen auf 100.000 Zeichen gekürzt unter rejected im Kostenprotokoll.
  • Teilung: Liegt die Zählung, die generate ohnehin vornimmt, über 500.000 Tokens, endet der Aufruf vor dem bezahlten Modellaufruf mit InputTooLargeError. Schritt 1 halbiert die Dokumentliste dann nach Bytes (erste Hälfte im selben Schritt, zweite über extend, 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/PfadVertrag
POST /{project_id?: UUID, entry_ids: UUID[], idempotency_key: string} → 202 Job
GET /latest, GET /jobs/latestoptional ?project_id=UUID; ohne Parameter privater Scope
GET /jobs/{id}dauerhafter Status, Phase und Dokumentzähler; keine Tokens/Prompts
POST /jobs/{id}/retry, /cancelexplizite Fortsetzung bzw. Abbruch; beides auch aus awaiting_decision
POST /jobs/{id}/continue202; 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}, /criteriaFachobjekte bzw. gespeicherte Dokumentergebnisse
GET/PUT /runs/{id}/reviewOriginal, 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; eignung und bepunktete_auswahl → eignung; zuschlag → zuschlag; angebots_mindestanforderung und ausfuehrungsbedingung → leistung. formale_einreichungsanforderung und 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.documents auf die procur_docs-Dokument-ID abgebildet, damit der Fit-Check den Dokumenttitel zeigt.
  • Regel: annual_turnover wird min_turnover, wenn genau ein Euro-Mindestbetrag und eindeutige Geschäftsjahre genannt sind; Durchschnitts- oder Gesamtumsätze, unklare Beträge oder Jahre bleiben manual_review mit offener Frage. sector_turnover bleibt immer manual_review, weil die Regel nur den Gesamtumsatz prüft. reference_count wird min_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. certification mit "27001" wird certificate: iso27001, außer die Unterlagen lassen einen anderen oder gleichwertigen Nachweis zu. Die vier im Verfahren nachzuweisenden Codes register_entry, professional_authorization, professional_membership und professional_liability_insurance sowie alle Angebots-Mindestanforderungen und Ausführungsbedingungen werden procedural. Alles andere bleibt manual_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.

    Kriterienextraktion: VergabeFit Doku