ładowanie…
Jedno realne ogłoszenie z BZP. Jedna firma demo. Pełen przebieg pipeline'u — od surowego HTML-a ogłoszenia, przez ekstrakcję 315-blokowej SIWZ, po draft siedmiosekcyjny i raport weryfikatora.
Zero wywołań LLM na wizytę. API Anthropic kosztuje pieniądze, a my nie palimy ich na boty. Produkt istnieje i działa. Demo to time-slice — jeden cached run z 19 maja 2026 roku.
Wejście, które nie znajdzie się tu nigdy: pole z numerem BZP, w które wpisujesz swoje ogłoszenie. To mamy w produkcji, nie w demo.
2026/BZP 00236579 · SPZOZ Łapy
Małe miasto na Podlasiu, samodzielny publiczny ZOZ, krajowy próg, tryb podstawowy (art. 275 pkt 1 ustawy Pzp). Wdrożenie nowej domeny katalogowej i migracja 200 kont użytkowników. CPV 72263000-6 (Usługi wdrażania oprogramowania) plus cztery dodatkowe IT.
BZP API serwuje ogłoszenie jako JSON + 18 234 bajty HTML-a sekcji I–IX. Wszystko bez OAuth, bez paperworku, bez dev-portal-registration. Tylko publiczny endpoint /mo-board/api/v1/notice.
Identyfikatory
Dane wyciągnięte z BZP API + sparsowanego HTML-a ogłoszenia (parse.py · deterministyczne, zero LLM).
↓ Wykonawca · profil demo
firm_demo.json
PrzykładIT
Sp. z o.o.
⚠ Firma fikcyjna
PrzykładIT to czysta fikcja na potrzeby demo. NIP/REGON/KRS są w zakresie poprawnych formatów, ale nie wskazują żadnego realnego podmiotu w rejestrach.
W produkcji wczytujesz własny profil z osobnego JSON-a (legal name, NIP, REGON, KRS, adres, reprezentant, dane kontaktowe). Profil żyje lokalnie, nigdy nie trafia do żadnej naszej bazy danych ani do rejestrów państwowych.
Profil demo osadzony w branży IT-procurement — typowym terenie ogłoszeń BZP.
parse.py · deterministyczne · ~150 ms
Sekcje SEKCJA I–IX z BZP API mają stabilne prefiksy numeryczne. BeautifulSoup4 + 20+ pól pydantic, trzy layouty parsera (span-w-h3, p-sibling, raw-text-tail). Zero LLM na tym etapie — wszystko regex/CSS.
Edge case, który łapie parser: pole organizationNationalId z API to NIP, ale pole HTML 1.4 to REGON. Bez rozróżnienia model w pierwszym runie zmyślił label „REGON” na wartości NIP — fix wymagał trzech miejsc (models.py, parse.py, prompt).
parsed.json · wycinek
TenderAnnouncement
{
"bzp_number": "2026/BZP 00236579",
"organization_name": "SPZOZ ŁAPY",
"organization_nip": "9661319909",
"order_object": "Wdrożenie …",
"cpv_main": {
"code": "72263000-6",
"label": "Usługi wdrażania …"
},
"cpv_additional": [ … 4 kody ],
"submitting_offers_date":
"2026-05-19T07:00:00Z",
"realization_period": "60 dni",
"criteria": [
{ "name": "Cena", "weight_pct": 100.0 }
],
"tender_amount_below_eu": true
}
siwz_extract.py · claude-haiku-4-5 · tool-use forced JSON
Ogłoszenie BZP to czterostronicowy nagłówek. Prawdziwe wymagania siedzą w SIWZ (Specyfikacja Warunków Zamówienia) — tu plik SPECYFIKACJA WARUNKÓW ZAMÓWIENIA (SWZ).docx, 315 paragraph-bloków, ≈57k znaków, pobrany z platformy e-Zamówienia mp-client przez nieudokumentowany endpoint /mp-readmodels/api/Search/GetTender.
Wyciąg do SiwzRequirements (pydantic): warunki udziału, kryteria oceny z formułą, wymagane dokumenty, terminy, wadium, kary umowne, kontakt. Haiku 4.5 z wymuszonym JSON-em (tool-use). Zero halucynacji: jeśli SIWZ czegoś nie definiuje, pole wraca jako null albo [].
W Łapach: warunki_udzialu=[] (poza brakiem wykluczenia), kary_umowne=[] (regulowane w projekcie umowy, załącznik nr 2), wadium=null. Każde z tych brak jest weryfikowalne w źródle — i jest powodem, dla którego drafter w sekcji D pisze „wadium nie jest wymagane” zamiast wymyślać kwotę.
SiwzRequirements · wycinek
Ekstrakcja: Haiku 4.5
draft.py · claude-haiku-4-5 · ~19s · prompt-cached
Drafter dostaje obie struktury: TenderAnnouncement + SiwzRequirements + FirmProfile. Wyjście to siedem oznaczonych sekcji w jednym pliku Markdown, gotowe do skopiowania do platformy e-Zamówienia po edycji przez specjalistę.
Wszędzie, gdzie brakuje danych (np. cena ofertowa, data podpisu), drafter wstawia [DO UZUPEŁNIENIA: ...] z konkretnym opisem co specjalista musi wpisać — zamiast halucynować kwoty albo daty.
Sekcje · draft.md
Wariant Sonnet 4.6: ~3× wall (review pass)
↓ pełny draft · renderowany z draft.md
draft.md · 2026/BZP 00236579 · ~14 KB
attempt 1/3 · verifier ✓ PASSED
ładowanie…
Źródło: artifacts/draft.md · cached z runa 19.05.2026
verify.py · deterministic + LLM cross-check
Drafter dostaje retry — do dwóch razy regenerujemy draft, jeśli weryfikator flaguje błędy severity=error. Keep best: jeśli żadna próba nie PASSuje, zostawiamy tę z najmniejszą liczbą findingów (errors first, attempt index tie-break).
Tier 1 — deterministyczny. Pure regex/string, zero LLM. Łapie literówki w nazwie firmy (klasa PrykładIT → PrzykładIT: brakująca litera + warianty diakrytyczne), brakujące NIP/REGON/KRS firmy lub zamawiającego, brakujący numer BZP, oraz 10-cyfrowe tokeny które nie pasują do żadnego znanego identyfikatora.
Tier 2 — LLM cross-check. Haiku 4.5 z forced-JSON tool-use. Każdy cytat warunku z sekcji G musi prześledzić się z powrotem do SiwzRequirements; każda liczba/data w sekcji A/B/C musi zgadzać się z TenderAnnouncement. Flaguje tylko rzeczywiste rozbieżności faktyczne, nie preferencje stylistyczne.
Wynik (Łapy)
Phase 0.9 test matrix: 3/3 samples PASS first-attempt (Łapy IT, Miastko hospital maint., Pucki GIS).
↓ findings · verify.json (renderowane)
verify.json
2 findings · 0 error · 1 warn · 1 info
Produkt · bez bota publicznego
Tego konkretnego flow nie uruchomisz tutaj — bo każde uruchomienie pipeline'u to realny koszt LLM, a my nie wystawiamy boto-vendingu publicznie zanim mamy chociaż jednego płacącego klienta na ten produkt.
Chcesz uruchomić na własnym BZP — z własnym profilem firmy, własnym CPV-watchlistem, własnym monitorem dziennego digestu? Napisz mailem. W odpowiedzi: profil firmy do wgrania (JSON) i termin pierwszego draftu w 24 h.
patryk@