Changelog
Všechny podstatné změny projektu GMS Asistent (chatbot pro vyhledávání zboží
na gms.cz). Formát dle Keep a Changelog.
Konvence: každý release má verzi + datum a čas (Europe/Prague). Časy F1–F3
odpovídají git commitům.
[Nevydáno]
Přidáno
scripts/sync_widget_settings.py— přenos vybraných klíčů nastavení widgetu
mezi prostředími (DEV → PROD) s diffem a ověřením po zápisu; bez --apply
jen náhled. Nastavení je v DB každého prostředí, deploy ho nepřenáší.
Opraveno
- Hledání s omezeními trvalo až 130 s (dotaz „dn 12, do 250 bar hydraulickou
hadici" — navrhovaná otázka na prezentaci): plánovač PostgreSQL u EXISTS
s OR v podmínce volil nested loop se 118 M porovnáními. Filtr parametrů
je teď dva korelované poddotazy (produkt / varianta) přes index owner_id
a sémantické hledání s omezeními nejdřív filtruje (MATERIALIZED CTE) a
až pak řadí podle vektoru — jinak by HNSW index po filtru vrátil 0 řádků.
Ověřeno na DEV DB: 1,9 s, stejné výsledky. Nový regresní scénář hlídá
navrhované otázky do 20 s. Cizí review docs/reviews/2026-09-18-search-perf.md.
- Regrese chatu: kontrola citace strany z katalogu přijímá i „na straně 36",
„stranách", „str." (stejné tvary jako pojistka v pipeline) — 18. 9. test
padal na správné odpovědi.
Přidáno
- Zadání zadavatele 16. 9. 2026 (IMS #5045, prezentace v sobotu 20. 9.) —
detail ARCHITECTURE §19:
- hledání podle kódu produktu / objednacího kódu (
12011,ES14A,
SMC-AS2201F-02-08A): přesný lookup v DB před LLM, „jen kód" bez
extrakce a hledání, u složeného dotazu produkt na prvním místě, shoda na
variantu vrátí jen tu variantu; regex ověřen na 528 reálných dotazech
(rozměry, normy, jednotky se nechytají); 2 nové regresní scénáře;
- režim prezentace (
app/faq.py, admin → 🎤 Prezentace): připravené
otázky a odpovědi (Excel „VIP DEN", 19 unikátních párů), odpověď
doslova bez LLM při shodě (embedding ≥ 0,90 nebo ≥ 0,85 + 2
rozlišující slova; 27/27 na parafrázích), výchozí vypnuto, štítek v
administraci, GET /api/admin/faq/preview;
- přepínač 🔊 v hlavičce widgetu — čte každou odpověď i na psaný
dotaz, pamatuje si stav, výchozí z administrace (widget_tts_always);
voice-journey.cjs má nový krok;
- písmo widgetu až 32 px (28 = 2× na prezentaci);
- navrhované otázky (4 z e-mailu) jsou jen nastavení v administraci DEV.
- regresní scénář režim prezentace (parafráze → doslovná odpověď bez AI,
jiná formulace, běžný dotaz nesepne; přeskočí se, když je režim vypnutý)
a v voice-journey důkaz skutečného přehrávání (stav ⏹ + velikost
streamovaného audia přes Performance API, ~580 kB PCM).
- migrace
0003_code_lookup_indexes.sql(výrazové indexy pro normalizovaný
kód, seq scan 104 ms → index); cizí review docs/reviews/2026-09-16-zadani-16-9.md
(7 nálezů, 5 opraveno, 1 vyvrácen, 1 předchozí chování).
Opraveno
- Widget na úzké obrazovce s velkým písmem (28 px, mobil 390 px): hlavička se
škáluje jen do 1,35×, název se zkracuje výpustkou, tlačítka se nezalamují —
✕ dřív vypadlo z obrazovky (ruční mobilní proklik 16. 9.).
deploy.sh,restore_test.sh:docker compose exec/runuvnitřbash -s
četl zbytek skriptu ze stdin — vzdálený krok tiše skončil po prvním docker
příkazu (chytila zkouška obnovy). Tělo vzdálených bloků je ve funkci main
volané na konci. restore_test.sh navíc bere POSTGRES_DB s fallbackem a
hlásí neúspěch vzdáleného kroku.
- Pojistka citace strany z katalogu (
pipeline.ensure_catalog_citation):
když odpověď čerpá z úseku katalogu výrobce (obsahuje značku nebo číslo
z něj) a model stranu neuvedl, připojí se "Zdroj: katalog X, strana N v PDF"
z úseků, které model skutečně dostal. Regrese 16. 9. chytila, že
flash-lite instrukci k citaci občas ignoruje (zásada "spolehlivost
deterministicky"). Cizí review docs/reviews/2026-09-16-catalog-citation.md.
- Velikost písma widgetu v administraci (Widget → Písmo, 12–22 px,
výchozí 14 = dosavadní vzhled). Škáluje všechny texty widgetu naráz
(CSS proměnná --k = px/14, 21 velikostí), ne jen zprávy. Přání
zadavatele při předvádění; PROD se nemění, dokud to admin nenastaví.
Cizí review přes AIHub (docs/reviews/2026-09-16-widget-font-size.md):
výšky vstupu a tlačítek se škálují taky.
Vizuální předloha widget-iframe-UI-standalone-.png (DEV) znovu založena —
důvod: DEV má nastaveno 16 px (odchylka 3,45 %), ne změna designu.
Opraveno
tests/gui/eshop-journey.cjs: čekání na obsah panelu 25 → 60 s — v regresní
sadě běží jako poslední a iframe z DEV se načítal déle (flake 16. 9. 2026,
samostatně 16/16 OK). Jen test, chování aplikace beze změny.
[0.28.0] – 2026-09-16 – hlasová odpověď (IMS #5045, 15. 9.)
Přidáno
- Odpověď přečtená nahlas mužským hlasem. Po dotazu položeném hlasem se
odpověď asistenta rovnou přehraje; u psaných dotazů je u odpovědi tlačítko
▶ Přehrát / ⏹ Zastavit (i u historie). U odpovědí s produkty se čte jen
shrnutí („Našel jsem 3 produkty. Nejlépe odpovídá …") — karty zůstávají
na obrazovce; markdown a odkazy se nečtou; delší text se zkrátí po větě
(600 znaků). Rozhodnutí zadavatele 16. 9.: cloud, Google Gemini
(gemini-3.1-flash-tts-preview, hlas Charon; čeština ověřena poslechem),
záloha OpenAI gpt-4o-mini-tts (onyx). Ne hlas prohlížeče: mužský český
hlas má jen část Windows, Android/iOS česky jen ženský.
POST /api/widget/ttsčte uloženou odpověď podle id zprávy v konverzaci
volajícího (endpoint nejde použít jako TTS libovolného textu); stejné
ověření, limity a Origin kontrola jako hlasový vstup; audio se streamuje
(PCM 16 bit mono 24 kHz, první chunk ~1 s — změřeno na DEV) a nikde
neukládá. Widget přehrává po chuncích přes Web Audio API (mp3 zálohy přes
<audio>). /api/widget/chat a historie nově vrací message_id.
- Administrace → Widget: 🔊 Hlasová odpověď zap/vyp, automaticky po hlasovém
dotazu, výběr hlasu (Charon, Orus, Iapetus, Algenib, Sadaltager).
- Env:
TTS_PROVIDER,TTS_MODEL,TTS_OPENAI_MODEL,TTS_OPENAI_VOICE.
Cena ≈ 0,1 Kč za přečtenou odpověď (≈450 výstupních tokenů, $10/1M).
- Cizí review (Codex + Gemini přes AIHub,
docs/reviews/) zapracováno:
vlastní kvóta TTS (nesdílí limit chatu; TTS_RATE_*), LRU cache hotového
audia — opakované přehrání téže odpovědi nestojí nic, AudioContext se
odemyká synchronně v gestu (iOS Safari), PCM explicitně little-endian,
klient bez Web Audio dostane WAV (format: "wav"), spotřeba a cena TTS
do logu i při přerušení, message_id i po idempotentním retry,
klientovi jen stabilní tts_failed, revoke blob URL, ořez po slově,
TTS_FALLBACK_PROVIDER odděleně od chatu.
- Testy:
tests/test_tts.py(+34: mluvená verze, fallback, práva, limity,
cache, wav, streaming). Neověřeno na iPhonu/Safari (přehrání ve vloženém okně).
scripts/check-secrets.sh: hodnota čtená zesettings.NAZEV(konfigurace,
jen identifikátor) už není falešný poplach.
[0.27.0] – 2026-09-16 13:20 – soulad s template: migrace, DB role, brána podle běžícího DEV
Přidáno
tests/gui/eshop-journey.cjs— widget se testuje na skutečné stránce
e-shopu (DEV kopie i ostrý www.gms.cz jen načtením), ne jen na podvržené;
regression.sh ho pouští, na PROD jen se SHOP_USER/SHOP_PASS
(commit 6b04a92, doplněno zpětně).
- Template check 16. 9. 2026 (
docs/reviews/template-check-2026-09-16.md):
Claude + Codex GPT-5.5 + Gemini 3.1 Pro přes AIHub, 20 nálezů, všechny
schváleny k opravě (jen DEV). Tento release je řeší:
Změna prošla cizím review přes AIHub (docs/reviews/2026-09-16-template-soulad.md,
Codex + Gemini, 12 připomínek: 8 potvrzeno a opraveno — mj. CI grep -E -P,
hesla DB rolí s apostrofy, sourcování .env v deployi, savepointy místo
transakcí v migrate — 3 vyvráceny, 1 nepřijata s důvodem).
- Konfigurace regresní sady žije v projektu (§1.6, §3.4):
tests/regression/dev.json + prod.json (widget key a cesta projektu se
doplní z prostředí při běhu), vizuální předlohy
tests/regression/baselines/, výstupy a snímky v gitignorovaném
tmp/regression/ místo /tmp/gms-*. Skill regrese je jen spouštěč.
Test skenu tajemství tests/test_secret_scan.sh je nově zaregistrovaný
v sadě (dosud ho nikdo nepouštěl).
GET /api/healthvracíversion— smoke test po deployi ověří, že
běží právě nasazený release (EXPECTED_VERSION).
tests/test_docs_consistency.py: každá proměnná čtená kódem je
v .env.example (chybělo 14), verze v kódu = poslední v CHANGELOGu,
[Nevydáno] existuje.
- CI (GitHub Actions): pyflakes, pytest, sken tajemství (gitleaks nad
historií + check-secrets.sh), docker compose config, build image,
blokace .env*/klíčů/velkých binárek (§9.1).
- Verzované migrace schématu (§4.8):
db/migrations/0001_baseline.sql
(celé schéma, idempotentní) + 0002_hnsw_indexes.sql; app/migrate.py +
python scripts/migrate.py [--status|--dry-run] (tabulka schema_migrations,
checksum, pg_advisory_lock). Aplikace při startu žádné DDL nespouští —
inline CREATE TABLE IF NOT EXISTS / ALTER … ADD COLUMN z chatlog, widget,
adminsettings, catalog, sync, stock, heureka, extract_docs, embed_products
jsou pryč, migrate.check_schema() jen hlásí, co čeká. deploy.sh migrace
aplikuje před přepnutím releasu a před nimi dělá dump
(backups/pre-migrate-<release>.sql.gz). ingest_partner.py --no-migrate
místo --schema. Vyzkoušeno na kopii DEV DB.
- Oddělené DB účty (§8.3):
db/roles.sqlzakládágms_app(jen DML +
TRUNCATE), gms_migrate (vlastník, DDL), gms_backup (jen SELECT), s
default privileges pro nové tabulky. Compose bere DATABASE_URL a
MIGRATE_DATABASE_URL z .env. DEV přepnuto 16. 9. 2026; PROD až s jeho
deployem (role + .env nejdřív).
- RPO 24 h / RTO 1 h zapsáno (
docs/INCIDENTS.md§3) a **obnova ze zálohy
ověřena** (scripts/restore_test.sh: dump 112 MB za 19 s, pg_restore 15 s,
počty řádků shodné, migrace na kopii zelené).
scripts/check-secrets.sh: migracedb/migrations/NNNN_*.sqladb/roles.sql
jsou kód, ne dump (dump s COPY/INSERT se dál odmítá); ${VAR:?hláška}
není hodnota; heslo v URL se neplete s ${USER:-gms}. Testy 23 → 29.
Změněno
- Marker regrese (§4.3):
regression-history/last-dev.jsons klíči
commit, time, errors, layers_ok, base, release; **commit je
commit releasu, který na DEV skutečně běží** (readlink current), ne
lokální HEAD. deploy.sh prod navíc ověří, že DEV právě běží HEAD.
Přepínače sady anglicky: --quick, --modules=, --theme=; obejití
brány FORCE_NO_REGRESSION=1; GMS_ENV=prod místo PROSTREDI (§5.4).
- Rollback podle
releases/.history(§4.7): pořadí nasazení drží
soubor, ne názvy adresářů; rollback jde po historii dozadu a historii za
cílem urizne (druhý rollback nevrátí verzi, ze které se právě uteklo).
Přepnutí current je i při rollbacku atomické (ln -sT + mv -Tf),
drží flock, a retence releasů běží až po zeleném smoke testu.
Deploy, rollback i auto-rollback po smoke jdou jednou cestou
(switch_release).
- Smoke test na PROD je jen čtení (§4.6): žádný
POST /api/chat
(zapisoval do chat_logs); místo toho GET /, /changelog (verze
v HTML), /admin/settings, autorizace /api/logs → 401 a ai_ok
z health. E2E chat zůstává na DEV.
- Serverový compose přejmenován
vps/gms/→vps/stack/(§11.1). .env.examplebez placeholder hodnot (§11.1), doplněných 14 proměnných,
sekce pro testovací ENV; lokální compose bez tichého hesla DB
(POSTGRES_PASSWORD:?) a s výchozím refreshem 6:30 jako README (§6.2).
- Regrese chatu: kontrola „odpověď čerpá z katalogu" se váže na data
(citovaná strana), ne na slovo „katalog" v textu (commit a06b86c,
doplněno zpětně).
Opraveno
smoke_test.sh: kontrola verze v/changelogmá 3 pokusy a delší timeout —
první odpověď po startu trvá déle a falešný FAIL poslal zdravý deploy do
auto-rollbacku (ověřeno: auto-rollback fungoval, .history uříznuta, DEV
zůstal na předchozím releasu). deploy.sh po návratu čeká 8 s před smoke.
regression.sh:VYSTUP/PREDLOHYse exportovaly uvnitř$(…), takže se
do sady nedostaly — první běh psal snímky do /tmp a předlohy zakládal znovu.
Předlohy DEV znovu založeny z 0.27.0 (stejný vzhled, důvod: cesta, ne design).
deploy.sh: vzdálené kroky přesssh -n … bash -s <<heredocneběžely —
-n zahodí stdin i s heredocem a krok tiše skončí s exit 0 (první pokus
o nasazení 0.27.0 na DEV: migrace ani přepnutí releasu neproběhly, smoke to
chytil na verzi v health). -n zůstává jen u ssh s přímým příkazem.
Odstraněno
ims/*.cjs— jediný nástroj na IMS je skillelevantemimo repo
(commit 6d43ceb, doplněno zpětně).
[0.26.2] – 2026-09-11 – hlas končí sám, testuje se doopravdy
Přidáno
- Nahrávání hlasu skončí samo po tichu. Prohlížeč bez Web Speech API
(Firefox) nahrává přes MediaRecorder, který sám nepozná konec řeči —
zákazník musel klikat dvakrát. Hlasitost se nově měří přes Web Audio
(AnalyserNode, RMS) a nahrávání se ukončí po 1,5 s ticha; když nikdo
nezačne mluvit, skončí po 6 s. Ruční stop i 60s strop zůstávají.
V Chrome se nemění nic — tam konec řeči pozná SpeechRecognition.
tests/gui/voice-journey.cjs— hlas se konečně testuje automaticky:
Chromiu se podstrčí skutečná nahrávka jako mikrofon a vypne se Web Speech
API, takže projde celá serverová cesta (MediaRecorder → /api/widget/voice
→ přepis → zpráva → odpověď). Změřeno: auto-stop zkrátil nahrávání
z 48 s na 7 s.
[0.26.1] – 2026-09-10 – nálezy regrese opraveny, sada hlídá i katalogy, hlas a náklady
Opraveno
- Mobil: inline šířka bočního panelu přebíjela mobilní CSS — widget měl
293 px místo celé obrazovky. Chytila regresní sada hned první běh.
- Kontrast v administraci:
--muted(#64748b) mělo na světlém podkladu
4,34:1, pod limitem WCAG 4,5:1 → #5b6878 (5,19:1).
- Formuláře na mobilu: pole v administraci i v debug chatu mají 44px
dotykový cíl a 16px písmo (pod 16px iOS při kliknutí přiblíží celou
stránku); textarea chatu dostala aria-label, přihlašovací pole label for.
- Citace strany katalogu: model bral číslo vytištěné uvnitř katalogu
(jiné než strana v PDF) — cituje se „strana N v PDF" a prompt to výslovně
žádá.
- Modul kontrastu regresní sady četl jen
background-color, takže tmavou
hlavičku s gradientem viděl jako bílý text na bílé: 21 z 23 nálezů bylo
falešných a skutečné dva se v tom šumu ztratily. Modul teď rozumí
gradientům (bere nejhorší zarážku) a přeskakuje neaktivní prvky, které
WCAG 1.4.3 vyjímá. Modul formulářů kliká jen na viditelný popisek.
Přidáno (rozšíření testů)
- Regrese chatu hlídá i katalogy (odborný dotaz musí dostat kontext
a citovat stranu, produkt musí dostat katalogovou stránku), hlas
(přiložená nahrávka → /api/widget/voice → přepis do 15 s pod $0,002)
a rozpočet: průměrná i nejdražší cena dotazu a p95 latence proti
stropu. Katalogový kontext zdvojnásobil cenu odborných dotazů
($0,011 → $0,023) — bez rozpočtu by se to poznalo až na faktuře.
- Průchod administrací nahraje skutečné PDF cizí značky a ověří, že ho
pojistka odmítne a že jde smazat (fixture tests/gui/data/).
- Unit testy celé indexace nad reálným PDF: odmítnutý katalog nesmí
zapsat jedinou stránku, přijatý uloží stránky i úseky, PDF bez textové
vrstvy se odmítne s vysvětlením.
[0.26.0] – 2026-09-10 – hlasový vstup, boční panel, katalogy výrobců (IMS #5045)
Tři body ze zadání zákazníka (úkol IMS #5045). Vše proti DEV, PROD nedotčen.
Přidáno
- Hlasový vstup do widgetu. Mikrofon u textového pole; přepis se odesílá
rovnou, aby se při předvádění neklikalo. Rozpoznává primárně prohlížeč
(Web Speech API, cs-CZ, průběžný přepis) — zdarma a okamžitě; loader
proto deleguje na iframe allow="microphone", bez toho prohlížeč mikrofon
v cizím rámu nepustí. Kde API není (Firefox) nebo selže, nahraje se zvuk
a přepíše ho náš server (POST /api/widget/voice, Gemini, záloha
OpenAI). Audio se nikde neukládá; platí allowlist typů, limit
velikosti, rate limit per identita i IP a přepínač v administraci.
Vlastní whisper na VPS jsme změřili a zamítli: medium int8 přepisoval
10 s audia 12,8 s a bral 2,2 GB RAM ze sdílených 7,8 GB, small byl česky
nepoužitelný. Cloud stojí ~$0,0005 za dotaz a VPS nezatíží.
- Boční panel jako výchozí zobrazení — plovoucí okno bylo při předvádění
malé. Panel jde přes 75 % šířky (nastavitelné 40–95 %) a celou výšku;
zákazník přepne ikonou v hlavičce a volba se mu pamatuje. Dosavadní
„ukotvení" tím bylo nahrazeno jedním zobrazením s nastavitelnou šířkou,
karty smí mít v panelu 3 sloupce. Na mobilu se nic nemění (fullscreen).
- Katalogy výrobců (PDF) jako podklad pro asistenta. Nahrávají se
v administraci (nová záložka Katalogy), značku **zadává admin —
z PDF se nehádá**. Indexace běží na pozadí: text stránek → objednací kódy
→ párování s feedem → pojistka → stránky u produktů + úseky s embeddingy.
V odpovědi pak asistent zná parametry, které ve feedu nejsou, a u otázek
dostane odborný kontext s citací („podle katalogu Lüdecke, str. 31").
Katalog může být anglicky, odpověď zůstává česky.
- Pojistka proti katalogu cizí značky: přijme se jen katalog, kde je
zboží z našeho feedu na aspoň 30 % stran. Odmítnutý katalog jde
přeindexovat, až se produkty do feedu dostanou.
/api/metricshlásí i přehled katalogů (aktivní, úseky, pokryté produkty).
Opraveno
- Párování kódů přes prefix výrobce — feed předsazuje kódům jméno
značky (SMC-AF20-F01B-D), katalog je tiskne bez něj (AF20-F01B-D).
Bez odříznutí přesného prefixu vycházel katalog SMC na 0 % shod, přestože
gms.cz 562 SMC produktů prodává; po opravě 56 % stran / 1 899 produktů.
- Měří se stránky, ne kódy — první návrh počítal podíl nalezených
objednacích kódů a na reálném katalogu dával 2 % (kandidátů z n-gramů je
řádově víc než skutečných kódů). Podíl stran se zbožím z feedu je stabilní:
Lüdecke 55 %, SMC 56 %, katalog mimo sortiment 0 %.
[0.25.0] – 2026-09-10 – testování klikáním a regresní sada (podle led-loga)
Do teď měl projekt jen unit testy s mocky. Ty nechytí, že se bublina na
e-shopu nevykreslí, že CSP nepustí iframe, že se po reloadu ztratí historie,
nebo že chatbot 15 dní jede na záložním provideru (CHANGELOG 0.23.0). Nově
čtyři vrstvy, popsané v docs/TESTS.md; všechny běží
výhradně proti DEV (proti ai-gms.elevante.cz se samy odmítnou) a nic
nenasazují — PROD zůstává jen na výslovné „yes".
Přidáno
- Průchod cestou zákazníka klikáním (
tests/gui/customer-journey.cjs,
Playwright): produktová stránka www.gms.cz (Playwright ji podvrhne — jen náš
<script src=…/widget.js>, vše ostatní jde na skutečný DEV, takže CSP
frame-ancestors i kontrola Origin běží naostro) → bublina → „Díváte se
na: PRODUKT" + návrhy → dotaz na tento produkt → hledání s kartami →
reload = historie → Nový chat → mimo obor → mobil → přihlášení přes HMAC
(převzetí anonymní historie, jiné zařízení, špatný podpis odmítnut).
Nic se nevkládá SQL. 24 kontrol, snímky do /tmp/gms-customer-journey.
- Průchod administrací klikáním (
tests/gui/admin-journey.cjs): špatné
a správné heslo → změna titulku → skutečný widget ho ukáže hned →
Historie verzí → Obnovit → titulek zpět → preset uložit/smazat → logy
konverzací (sdílené přihlášení, detail) → Odhlásit. Po pádu uklidí přes
API. 20 kontrol.
- Regrese chatu přes skutečné
/api/chat(tests/e2e/chat_regression.py):
vícekrokové konverzace proti živému DEV, ověřují se jen deterministické
příznaky v debug a tvrdé filtry (pozdrav bez LLM, off-topic, provozní
dotaz, injection, 2" → DN 47,8–53,8 jen KAMLOK, jen spony, „ta první",
dědění rozměru, srovnání není negace, skladem, chemikálie) + **zdraví AI
vrstvy** u každé odpovědi (žádný fallback, rerank běží, cena > 0, odkazy
jen z dat). ≈ 13 volání LLM, ~$0,15 za běh.
- Regresní sada (skill
regrese, configgms-dev.json, wrapper
scripts/regression.sh [--rychle|chat|gui|unit]): dostupnost stránek,
autorizace (anonym → /api/logs, /api/admin/*, /api/metrics,
/widget-demo, /widget bez klíče = odmítnuto), formuláře, axe/WCAG,
kontrast, překryvy, výkon proti rozpočtu, vizuální porovnání s předlohou,
historie běhů v regression-history/ (negitováno). Skill dostal podporu
přihlášení bez uživatelského jména (login: null, selektor_uspech).
CLAUDE.mds pravidlem DEV/PROD, zásadami, workflow a bránou před
nasazením; docs/TESTS.md; deploy.sh dev připomene regresi.
Opraveno (nálezy z prvního běhu sady, po odsouhlasení)
- **Nové hledání na produktové stránce se už nepřipne k prohlíženému
produktu** — „hadice na benzín, vnitřní průměr 19 mm" na stránce
PETROTEC 25 vracelo jen tutéž hadici bez karet. Zpráva, která pojmenuje
typ zboží a přidá rozměr, je nové hledání (pipeline.is_new_search,
debug.page_ctx_new_search); otázka na vlastnost bez typu („vydrží
10 bar?") se dál váže k prohlíženému produktu.
- Bezpečnostní hlavičky posílá aplikace (
X-Content-Type-Options,
Referrer-Policy, HSTS, X-Frame-Options: DENY mimo /widget, kde
framing řídí CSP frame-ancestors). Traefik žádné nepřidával.
- Odkazy v renderovaných dokumentech (
/api-docs,/architecture,
/changelog): relativní ARCHITECTURE.md/API.md/CHANGELOG.md vedou
na jejich stránky, placeholdery a soubory bez stránky zůstanou textem
místo odkazu na 404.
Nálezy z prvního běhu (neopraveno, k rozhodnutí)
- Formuláře na mobilu: pole admin přihlášení 32 px a písmo < 16 px (iOS
přibližuje), textarea debug chatu bez popisku, dotykové cíle 27 px.
- Kontrast: „Počet dotazů" v adminu 4,34:1, tlačítko Uložit v neaktivním
stavu 1,92:1. (Ostatní hlášení kontrastu jsou artefakty měření —
průhledné pozadí standalone widgetu a JSON stránky.)
[0.24.0] – 2026-09-05 – widget na produkci, zbývající nálezy z review
Přidáno
- Widget běží na produkci (
ai-gms.elevante.cz) se stejným site key
i WIDGET_ID_SECRET jako DEV, takže podpisy identity z backendu gms.cz
zůstávají platné. WIDGET_ALLOWED_ORIGINS omezené na gms.cz (CSP
frame-ancestors se vynucuje), /widget-demo vypnuté. Historie konverzací
(59 konverzací / 150 zpráv) přenesena z DEV, aby o ni zákazníci nepřišli.
Totéž omezení origins nasazeno i na DEV — do teď tam bylo *, tedy bez CSP
a bez kontroly Origin.
- Dotaz na skladem filtruje sklad — „Co máte skladem na benzín?" vracelo
i zboží na objednávku. Nově se ukáže jen skladové; když skladem není nic,
nabídka zůstane (radši něco než prázdno).
- Chemické dotazy si řeknou o koncentraci a teplotu — bez nich nelze
vhodnost potvrdit. Detekce je deterministická (kyseliny, louhy, peroxidy,
rozpouštědla…); prompt pak vhodnost potvrdit nesmí a produkty nabídne jen
jako předvýběr.
- Ceník zná modely Gemini 3.x —
cost_usdu nich tiše vycházel 0, takže
nebylo vidět, co provoz stojí. Reálná cena za dotaz vyšla $0,0115
(90 % připadá na krok odpovědi).
/api/healthhlásíai_ok— false znamená, že odpovídá záložní
provider nebo neběží rerank. Veřejně a bez METRICS_KEY, protože
konfigurace Nagiosu je ve veřejném repu a klíč do ní nepatří. Doplněny
dva Nagios checky (PROD i DEV) do vps/nagios/chatbot-nagios.cfg.
Opraveno
- Nehořlavý ≠ těžkohořlavý — když se zákazník ptá na nehořlavost a data
mají jen těžkohořlavost či teplotní odolnost, asistent to musí říct.
- Aplikační oblast — trubka na stlačený vzduch se přestala nabízet jako
chránička na vedení elektřiny. Elektroinstalace, potraviny, pitná voda
a ATEX mají vlastní normy; bez opory v datech se náhrada podle tvaru
nebo materiálu nenabízí.
[0.23.1] – 2026-09-04 – tvrdé filtry přestaly lhát
Nálezy z regresních kol zopakovaných s funkčním rerankem + nezávislý review
(GPT-5.5), všechny ověřené proti produkčním datům.
Opraveno
- Prázdný průnik omezení už neprojde jako shoda — u dotazu „hydraulická
hadice DN 12 a minimálně 300 bar" se vypsala varianta DN 5 za 153 Kč,
jako by požadavek splňovala. Ten produkt v DN 12 na 300 bar nemá žádnou
variantu; filtr variant vrátil prázdno a soft fallback pak ukázal všech 12
variant bez jakéhokoli označení. Nově se rozlišuje „filtrováno" a
„rozvolněno": rozvolněný produkt jde na konec výpisu, přijde bez rozměrů
varianty a s příznakem, že zadané rozměry nesplňuje.
- „Ta první / ta druhá" míří na to, co zákazník čte — pořadí se ukládalo
z interního seznamu, ne z vykreslené odpovědi. Odpověď začínala PETROTEC 10,
ale „ta druhá" sáhla po úplně jiném produktu.
- Odkazy jen z dat — model si složil
gms.cz/nbr-nbr-65-…místo
gms.cz/nbr-65-…. Odkaz mimo předaná data se opraví podle názvu produktu,
nerozpoznatelný se rozbalí na prostý text (links_fixed v debugu).
- Přesný název parametru má přednost před aliasem — „těsnění do příruby
DN 100" propouštělo těsnění s DN 80, které má náhodou vnitřní průměr 100 mm.
Aliasy (průměr ↔ DN ↔ JS) jsou pojistka proti různému pojmenování téhož
rozměru, ne povolení zaměnit rozměry; použijí se, jen když produkt parametr
přesného názvu vůbec nemá.
- Rozsah spony se musí pokrýt, ne překrýt — na „hadicová spona 20-32 mm"
se nabízela spona 32-50, která se s požadavkem dotkne v jediném bodě.
[0.23.0] – 2026-09-04 – Gemini 2.5 retirováno: konec tichého fallbacku
Při přípravě testu thinkingu se ukázalo, že **od 20. 8. 22:49 vracela všechna
volání Gemini 404** („model no longer available to new users"). Pipeline tiše
přešla na FALLBACK_PROVIDER=openai (gpt-4o) a **rerank se 15 dní vůbec
nespouštěl** (359 ze 423 chatů rerank_error: 404). Embeddingy jedou dál,
hledání zasažené nebylo. Cena za dotaz byla $0,019 místo dokumentovaných
~$0,003.
Opraveno
- Log říká pravdu o modelech —
debug.modelshlásil KONFIGUROVANÝ model,
ne ten, který odpověděl; proto výpadek 15 dní nikdo neviděl. Nově obsahuje
skutečně použité modely (rerank: null = rerank neproběhl) a vedle nich
configured.
- Účtují se thinking tokeny —
thoughts_token_countGoogle účtuje, ale
nebyl v candidates_token_count; s uvažováním podhodnocovala naše cena
spotřebu zhruba 9×.
- Extrakce mimo očekávaný tvar neshodí chat — když model vrátí JSON pole
místo objektu, padal první extracted.get(...) na AttributeError a zákazník
dostal 500. Pole se rozbalí, nesmysl = prázdná extrakce.
Přidáno
/api/metrics→ai_health_24h—fallback_used,rerank_ok,
rerank_failed a souhrnný ok. Přesně tohle chybělo, aby se výpadek
poskytovatele ohlásil sám. Patří na Nagios.
[0.22.3] – 2026-09-04 – navazující dotazy drží rozměr, zámek rodiny spojek
Nálezy z nezávislého review (GPT-5.5) nad výstupem regresních kol.
Opraveno
- Navazující dotaz už neztrácí rozměr — „která je nejlevnější?" nebo
„ta druhá — kolik stojí?" po dotazu na 19mm hadici odpovídalo cenou 6mm
varianty (92 Kč místo 257 Kč). Extrakce u follow-upu rozměry nezopakuje, tak
se přebírají z minulého kola, když jde zjevně o totéž téma (shodná hledaná
fráze, odkaz řadovou číslovkou nebo meta-dotaz na cenu/sklad). Změna tématu
omezení nedědí.
- Připnutý produkt respektuje omezení — u odkazu „ta první/druhá" a u
krátkého meta-dotazu se varianty dotahovaly BEZ rozměrového filtru, takže se
ukázala cena a parametry jiné varianty.
- Katalogový zápis rodiny spojek — extrakce vrací i anglické „Camlock",
katalog je píše „KAMLOK" a sémantika je nespojí (s „kamlok" ve frázi se najdou
všechny, s „Camlock" ani jeden). Katalogový tvar se do hledané fráze doplní
deterministicky, stejně jako u synonym médií („kejda" → fekálie).
- Systém spojek je zámek — na „dvoucoulová kamloková spojka" asistent
odpověděl „Kamlockový typ spojky v nabídce nemáme" a nabídl GUILLEMIN,
přestože katalog KAMLOK v DN 50 má. Rodina spojek (kamlok/camlock, GEKA,
Guillemin, Storz, Perrot, Bauer) je nezaměnitelná a filtruje se tvrdě; když
ji opravdu nemáme, seznam se nechá projít, ať může asistent poctivě nabídnout
alternativu.
[0.22.2] – 2026-09-04 – nálezy ze širokého regresního kola
Nová sada 35 dotazů napříč sortimentem (vícekrokové konverzace, palce, negace,
odkazy na dřívější nabídku, kontext stránky, poradní režim).
Opraveno
- Typ zboží se pozná jen na začátku slova — „aspoň 50 metrů" se
vyhodnotilo jako požadavek na sponu a „vnitřní" u produktu jako koncovka.
Zároveň teď zabírá česká flexe („gumová" = guma).
- Srovnání materiálů už není negace — „co je lepší na horkou vodu, guma
nebo PVC?" extrakce vyhodnotila jako exclude=[PVC], filtr vyhodil přesně to,
co měl zákazník porovnat, a odpověď nabídla hadičku na huštění pneumatik.
- U srovnání se neztrácí médium — z „co je lepší na horkou vodu, guma nebo
PVC?" zbylo v hledání jen „gumová hadice" a asistent nabídl hadičku na huštění
pneumatik a asfaltérskou hadici. U srovnávacích dotazů se do hledání přibalí
celá zpráva BEZ srovnávané alternativy — s „PVC" ve frázi vrátila sémantika
zase samé PVC hadice. Nově odpoví „pro horkou vodu je lepší guma, konkrétně
EPDM" a nabídne CALORTEC.
- „Máte ji i v jiném průměru?" — model viděl jen první variantu a odpovídal
„další průměry nemáme", i když produkt další rozměry má. Do dat odpovědi jde
nově pole dostupne_rozmery = distinct hodnoty rozměrových parametrů napříč
všemi variantami (z dat, nic se nedopočítává).
[0.22.1] – 2026-09-04 – typ zboží jako filtr, deterministická kategorie
Přidáno
- Typ zboží je tvrdý filtr (
app/producttype.py) — kdo chce trysku,
spojku, naviják, manometr, tyč nebo trubku, nedostane hadice. Typ se z dotazu
vytěží regexem (ne LLM, takže je stabilní) a porovná s názvem a kategorií
produktu. Pojistky proti přefiltrování: bez pojmenovaného typu se nefiltruje
vůbec a produkt s nerozpoznatelným typem se nevyhazuje. Ověřeno proti
produkčním datům na 20 dotazech napříč sortimentem — u žádného nezmizely
správné výsledky (např. „olejiodolná pryžová deska" a „hydraulická hadice"
20→20), zato „trubka odolná na UV" 20→5 a zbyly hliníkové trubky místo
potravinářského silikonu a stahovacích pásek.
Opraveno
- Kategorie na konci odpovědi se vybírá deterministicky — model ji volil
jako „nejčastější mezi produkty", takže se mezi běhy měnila a u smíšených
výsledků byla nesmyslná (koncovky → „Čistící spreje, maziva"). Nově ji
spočítá pipeline; když výsledky pokrývají kategorie roztříštěně (nejčastější
nemá ani polovinu), řádek s kategorií se vynechá úplně.
[0.22.0] – 2026-09-04 – opravy z vyhodnocení reálných konverzací
Analýza 133 reálných dotazů z DEV (kde běží widget z gms.cz) + nezávislý review
Gemini 3.1 Pro a GPT-5.5. Všechny opravy jsou deterministické, v pipeline.
Opraveno
- Tyče a desky: „průměr" je v katalogu „Tloušťka (mm)" — dotaz
„polyamidová tyč průměr 50 mm" vracel PTFE a EPDM a asistent odpovídal
„nemáme", přestože PA TYČ PA6 SILON Ø50 existuje. U plochého sortimentu
(tyče, desky, pryže, profily, plasty) se průměrové omezení filtruje i přes
Tloušťka (mm); u hadic se nemění nic (tam by šlo o tloušťku stěny).
- Parametr, podle kterého se filtrovalo, je v odpovědi vždy vidět — u pryží
a desek se nevypisoval (chyběl ve whitelistu klíčových parametrů), takže
asistent tvrdil „5 mm nemáme", i když filtr 5 mm prošel. Whitelist doplněn
o tloušťku, šířku a průměr; filtrovaný parametr jde první a limit ho neuřízne.
- Cena se bere z vyfiltrované varianty, ne z produktu — u dotazu „hadice
25 mm" se ukazovalo „od 48 Kč", což byla cena varianty Ø3 mm. Když varianty
prošly rozměrovým filtrem, cena i rozpětí se počítají jen z nich.
- Režim produktových karet už neumlčí
/api/chat— admin nastavení
widget_result_format=cards instruovalo model, ať produkty nevypisuje. To
je správně pro widget (karty vykreslí sám), ale platilo to globálně, takže
přes API i v debug UI chodily odpovědi úplně bez produktů. Nově si režim
karet vyžádá volající (widget), API vypisuje produkty vždy.
- Kontext produktové stránky nepřebíjí off-topic dotaz — „co bude
k večeři?" na stránce hadice dostávalo tip na večeři, protože pojistka
„na produktové stránce nikdy mimo obor" se uplatňovala bezpodmínečně. Nově
jen u dotazu, který na produkt opravdu míří (zájmeno nebo dotaz na vlastnost).
- Dotaz na jiný typ zboží na produktové stránce — „nabídni trysku a spojku"
hledalo pořád tutéž hadici, protože extrakce vrátila název prohlíženého
produktu. Nově se hledá podle zprávy zákazníka.
- Sklad bez vymyšlené jednotky — feed měrnou jednotku nedává, model si ji
domýšlel („skladem 40,1 ks" u metrážní hadice). Množství se předává jako text
bez jednotky a prompt zakazuje ji doplnit i vypisovat názvy datových polí.
- Navazující dotaz nespadne do „mimo nabídku" — „kolik stojí ta nejlevnější
a kolik metrů máte skladem?" po nabídce produktů extrakce občas označila za
mimo obor. V rozjeté diskuzi s nabídnutými produkty se meta-dotaz a otázka
drží v oboru deterministicky.
- Množství skladem se posílá jen na vyžádání — model si k číslu bez měrné
jednotky doplňoval „kusů“ i u metrážního zboží; do dat odpovědi jde teď jen
u dotazu na sklad/množství.
Přidáno
- Pozdrav je vlastní intent — na „ahoj" už nechodí „tohle není z naší
nabídky", ale přivítání. Řeší se deterministicky, bez volání LLM (a bez ceny).
- Rerank vidí kategorii produktu — bez ní pouštěl hadici na chladicí vodu
a glykol mezi výsledky na benzín. Prompt reranku navíc explicitně zakazuje
míchat typy (spojka × hadice) a média.
- Terminologická pojistka v promptu — „těžkohořlavý" (DIN 4102 B1) se nesmí
přepsat na „nehořlavý".
docs/FEED-REQUIREMENTS.md: 5 nových požadavků na GMS — měrná jednotka skladu,
pojmenování rozměru u tyčí, nečíselné hodnoty Průměr (mm) (965 z 1 425),
překlep v kategorii „bezníny" a koncovky zařazené mezi čisticí spreje,
a export tabulky chemických odolností.
[0.21.4] – 2026-08-22 – lišta „Dotaz k produktu" nad rozjetým chatem
Přidáno
- Když má návštěvník rozjetou konverzaci a přejde na stránku produktu,
objeví se nad zprávami lišta „💬 Dotaz k produktu: NÁZEV" — jedním klikem
otevře nový chat s kontextem daného produktu (rovnou i s návrhy otázek k němu).
V prázdné konverzaci se lišta neukazuje (tam kontext ukáže uvítací obrazovka).
- SPA překlik na jiný produkt lištu i kontext aktualizuje bez reloadu.
[0.21.3] – 2026-08-22 – dotaz na prohlížený produkt nenabízí cizí zboží
Opraveno
- Na produktové stránce se u dotazu na daný produkt („umíte ji nařezat na
míru?", „jakou má tvrdost?", „je skladem?") už nevypisují cizí produkty —
odpoví se jen k prohlíženému zboží, bez karet (zákazník ho má před sebou).
Alternativy se ukážou, jen když si o ně řekne („máte levnější alternativu?",
„máte ji v jiném průměru?", „jaké tloušťky nabízíte?") — deterministicky.
[0.21.2] – 2026-08-22 – návrhy otázek podle typu produktu
Změněno
- Na produktové stránce se návrhy otázek odvozují z typu produktu
(deterministicky z kategorie/názvu, bez LLM volání navíc): hadice → média,
provozní tlak, poloměr ohybu, jiný průměr; pryže → tvrdost a pevnost,
odolnost olejům, tloušťky, řezání na míru; spojky → na jakou hadici, materiál,
tlak; plasty/tyče → rozměry, teplota, obrábění; podlahoviny → protiskluznost,
údržba. Ke každému typu se přidá „Je skladem a kolik stojí?" a „Máte levnější
alternativu?".
- Vlastní návrhy z administrace mají přednost a podporují placeholder
{nazev} (doplní název prohlíženého produktu); prázdné pole = automatika.
[0.21.1] – 2026-08-22 – asistent odpovídá NA produkt ze stránky
Opraveno / přidáno
- „Jaké má parametry?" na produktové stránce už není „mimo nabídku" (reálný
případ z gms.cz): extrakce nově dostává název prohlíženého produktu a navíc
je deterministická pojistka — s kontextem stránky se krátký dotaz vždy váže
na daný produkt (on_topic + hledaný text = jeho název) a produkt se připne
mezi výsledky.
- Parametry ze stránky produktu do odpovědí: nově se do promptu dotahují
i parametry na ÚROVNI PRODUKTU (materiál, tvrdost, pracovní teplota, pevnost,
tažnost, měrná hmotnost, barva…) — dosud se posílaly jen rozměrové parametry
variant, takže asistent na dotaz „z čeho je?" neuměl odpovědět.
- U prohlíženého produktu dostane LLM **3× delší popis a 4× více textu
z katalogových listů (PDF/OCR)** + pokyn odpovídat vyčerpávajícím způsobem
z jeho dat (režim B0 v promptu).
[0.21.0] – 2026-08-22 – asistent zná stránku, na které zákazník je
Přidáno
- Kontext prohlíženého produktu: návštěvník se na produktové stránce může
zeptat „*je tato hadice vhodná na naftu?*" bez uvedení názvu. Loader posílá
referenci (data-product / window.gmsWidgetProduct / URL stránky), server ji
použije VÝHRADNĚ jako klíč do naší DB (search.get_by_ref) — nikdy nejde do
promptu; nenalezeno či duplicitní slug = tichý fallback bez kontextu.
- Kontext se posílá s každou zprávou (snapshot, ne globální stav) a při
ukazovacím zájmenu („tato/tento/je to vhodné") se produkt **připne mezi
výsledky** — deterministicky, ne přes prompt.
- SPA překliky: loader hookuje History API (pushState/replaceState/popstate)
s debounce — asistent ví o změně produktu i bez reloadu stránky.
- Uvítací obrazovka ukazuje „Díváte se na: NÁZEV" a **vlastní návrhy otázek pro
produktové stránky** (admin). Celé vypnutelné v adminu (widget_page_context).
- Demo:
/widget-demo?produkt=KODsimuluje produktovou stránku e-shopu. - Návrh review GPT-5.5 + Gemini 3.1 Pro; 8 nových testů (celkem 331).
[0.20.0] – 2026-08-21 – styl „velký asistent" + kompaktní karty
Přidáno
- Styl okna „velký asistent" (inspirace Rohlík/Maia, dle screenshotu z jejich
webu): bublina zůstává, ale vyskočí velký panel s uvítací obrazovkou (pozdrav +
podtitulek na jemném gradientu), klikacími návrhy otázek (editovatelné
v adminu, 1/řádek, max 6) a plovoucím zaobleným vstupem. Volí se v adminu
(widget_style), klasický chat zůstává výchozí.
- Formát výsledků (požadavek „zbytečně dlouhé"): Text / Text + karty /
Kompaktní karty — v kompaktním režimu LLM odpoví jen 1–3 větami
(+ odkaz na kategorii) a produkty se zobrazí POUZE jako bohaté karty:
obrázek, název, krátký popis, klíčové parametry, cena a dostupnost.
- 4 nové testy (celkem 324).
[0.19.1] – 2026-08-21 – refaktor administrace (přehledné záložky)
Změněno
- /admin/settings přestavěno na záložkové rozhraní (levá navigace: Odpovědi,
Prompt, Widget, Limity, Historie verzí, Zabezpečení) místo jedné dlouhé stránky;
IA odsouhlasena GPT-5.5 + Gemini. Presety a rychlé vzhledy nahoře v příslušné
záložce, potvrzovací dialogy před použitím/smazáním presetu a obnovou verze.
- Sticky lišta „Uložit" se zobrazuje jen na konfiguračních záložkách a jen
při neuložené změně (dirty-state, ochrana před odchodem se změnami).
- Jednotná hlavička obou admin stránek (Logy | Nastavení + Odhlásit); pole
Admin heslo v logu skryté — přihlášení je sdílené, box se ukáže jen když
heslo chybí nebo je špatné.
[0.19.0] – 2026-08-21 – pojmenované presety + verzování konfigurace
Přidáno
- Pojmenované presety — zvlášť prompt (instrukce + override) a zvlášť vzhled
(všechna widget_* nastavení): „Uložit jako…", Použít, Smazat; unikátní název
per scope, klíče striktně whitelistované dle scope.
- Verzování konfigurace (review GPT-5.5 + Gemini): každé uložení = nová
immutable verze (tabulka settings_versions, jedna DB transakce, retence 100);
historie v adminu se změněnými klíči a poznámkou; **„Obnovit" zkopíruje starou
verzi jako novou** — edituje se vždy jen nejnovější. Optimistic lock
(expected_version → 409 při souběhu dvou panelů); stará verze se před obnovou
validuje aktuálním modelem (evoluce schématu).
- Jednotné přihlášení admin sekce: heslo se sdílí mezi nastavením a logy
(localStorage), takže se zadává jen jednou.
[0.18.1] – 2026-08-21 – vzhledy widgetu, dark mode, sloupce karet
Přidáno
- Předdefinované vzhledy v /admin/settings (Světlý modrý, Tmavý, Světlý
zelený, Tmavý jantar) + widget_theme light/dark — widget má kompletní
tmavou paletu (pozadí, bubliny, vstup, karty).
- Rozložení produktových karet: počet sloupců (1/2) nastavitelný ZVLÁŠŤ
pro počítač a mobil (loader posílá do iframe detekci zařízení).
- Override promptu se předvyplňuje aktuálním výchozím promptem (Reset ho
vrací); uložení nezměněného defaultu se normalizuje na „výchozí", aby budoucí
vylepšení promptu nezůstala zmrazená.
- /admin (logy) má odkaz na ⚙️ nastavení a běží pod stejným admin heslem.
[0.18.0] – 2026-08-21 – admin nastavení: vše konfigurovatelné za běhu
Přidáno
- Admin sekce
/admin/settings(nové ADMIN_PASSWORD, scrypt hash v DB,
změna hesla z UI; oddělené od DEBUG_PASSWORD, které gate-uje veřejný chat) —
konfigurace se ukládá do tabulky app_settings a platí OKAMŽITĚ bez restartu.
Návrh zreviewován GPT-5.5 + Gemini 3.1 Pro (Pydantic validace všech hodnot
proti CSS/XSS injection, scrypt místo holého sha256, config přes API endpoint).
- Konfigurovatelné: počty výsledků (default/max), počet parametrů u produktu,
toggle cena / dostupnost / kategorie / výrobce / PDF listy; prompt (doplňkové
instrukce i úplný override s resetem); widget — barva, pozice L/R, velikost
bubliny, rozměry okna, font, titulek, uvítání, placeholder, patička, auto-open;
produktové karty pod odpovědí (volitelně s obrázky z feedu); rate limity.
GET /api/widget/config— veřejná validovaná podmnožina pro loader i iframe
(widget.js i widget.html se stylují podle admin nastavení).
- 20 nových testů (celkem 307); testy nově tvrdě blokují reálnou DB (conftest).
[0.17.1] – 2026-08-21 – čerstvost dat v /api/health (sync_fresh)
Přidáno
- Denní sync zapisuje výsledek do tabulky
sync_status(forai i legacy flow,
úspěch i selhání pojistkou); /api/health nově vrací last_sync
(čas, ok, zdroj, stáří v hodinách) a sync_fresh = poslední sync OK
a mladší než SYNC_FRESH_HOURS (default 25 h) — pro vzdálený monitoring
(Icinga content-check "sync_fresh":true).
[0.17.0] – 2026-08-21 – jednotný feed forai.xml (ceny 100 % variant)
Změněno
- Nový datový zdroj: jednotný feed „for AI" (
app/forai.py) nahrazuje
5 původních feedů. Přináší ceny u 100 % variant (dřív ~26 % přes Heureka
párování), přesné URL produktů i variant, kategorie cestou, FEATURE =
product-level parametry, sklad u všech variant. Návrh migrace zreviewován
GPT-5.5 + Gemini 3.1 Pro.
- Denní sync (scheduler, flow zapíná
FORAI_FEED_URLv env; prázdné = legacy
rollback): soft-delete + upsert produktů a SKU, hromadná náhrada parametrů
(jedna transakce, COPY), FTS rebuild, **re-embedding jen produktů se změněným
textem** (id se nemění → embeddingy a doc_text z PDF OCR se zachovávají).
Pojistky: min. produktů, max. % deaktivací, min. počet parametrů.
sklady.xmlse stahuje dál JEN kvůlionstockfrom(termín naskladnění) —
ve forai chybí (požadavek na GMS v docs/FEED-REQUIREMENTS.md).
- Scheduler:
REFRESH_MINUTE(deploy: 6:30 místo 5:00). - Heureka párování, GUID strom skupin a starý stock modul zůstávají v kódu
jen pro legacy rollback (ze scheduleru se s forai flow nevolají).
scripts/apply_forai.py— ruční sync / prvotní migrace.
[0.16.0] – 2026-08-20 18:40 – embedovatelný chat widget pro e-shop (bublina)
Přidáno
- Chat widget: e-shop vloží jeden
<script src=…/widget.js data-key=…>tag
a získá bublinu vpravo dole s kompletním chatem (static/widget.js loader +
static/widget.html UI v iframe, postMessage handshake s kontrolou originu,
mobilní fullscreen 100dvh + safe-area, lazy-load iframe, přístupnost).
- Persistentní konverzace (
app/widget.py, tabulkywg_conversations+
wg_messages): přihlášený uživatel pokračuje v konverzaci i po návratu či na
jiném zařízení, anonym přes id v localStorage; po přihlášení se anonymní
konverzace převedou k účtu. In-memory paměť pipeline se seeduje z DB
(přežije restart). Debug chat_logs běží dál nezávisle.
- Bezpečnostní model (návrh zreviewován GPT-5.5 + Gemini 3.1 Pro, nálezy
zapracovány): veřejný site key (WIDGET_KEY) + CSP frame-ancestors
allowlist + kontrola Origin i na API + identity verification à la Intercom
(WIDGET_ID_SECRET, HMAC-SHA256 podpis user_id s kanonickým prefixem —
bez podpisu nelze číst cizí historii).
- Ochrana nákladů na LLM: rate limit per identita + per IP (poslední XFF)
+ globální denní strop, zámek konverzace proti paralelním zprávám,
idempotence msg_id (retry klienta neplatí LLM dvakrát), limit délky zprávy.
- GDPR retence: konverzace bez aktivity se mažou po
WIDGET_RETENTION_DAYS
(default 90) při startu aplikace.
GET /widget-demo(jen sWIDGET_DEMO=true): simulace e-shopu s widgetem
v anonymním i přihlášeném režimu. Integrace popsána v docs/API.md §9.
- 37 nových testů (auth/HMAC, origin, limity, zámek, idempotence, seed, HTTP).
[0.15.1] – 2026-08-20 17:45 – deploy a smoke-test skripty pro CHATBOT VPS
Přidáno
scripts/deploy.sh dev|prod— deploy na CHATBOT VPS (AI-ELEVANTE) s vynucenými
kontrolami: pyflakes + celá pytest suite PŘED deployem (fail = deploy se nekoná),
nasazuje jen commitnutý kód (git archive HEAD), .env na serveru nikdy nepřepisuje,
po deployi automaticky smoke test. **Prod (ai-gms.elevante.cz) vyžaduje interaktivní
potvrzení „yes"** — v souladu s pravidlem, že do produkce se nasazuje jen na výslovný
souhlas (dev ai-gms.elevante.work je default).
scripts/smoke_test.sh [dev|prod|both]— po-deploy kontrola nasazení:/api/health
(status + database), HTTP→HTTPS 301, expirace certifikátu (≥14 dní) a end-to-end
/api/chat (≥1 výsledek). Exit kód pro použití v automatizaci.
- Infrastruktura VPS dokumentována ve složce
vps/(README, INSTALL, SECURITY,
CHANGELOG serveru, compose stacků, zálohy, monitoring); vps/CREDENTIALS.md
je v .gitignore.
[0.15.0] – 2026-07-11 – 5 iteračních regresních kol (40+ konverzací)
Přidáno / opraveno napříč koly 4–8
- PDF katalogové listy: na žádost o katalogový list odpověď dává přímý
markdown odkaz na PDF z dat (katalogovy_list_pdf).
- Otázky ≠ hledání: na „jaký nejmenší průměr…?", „z čeho je vyrobená…?"
se odpovídá přímo (superlativy z dat), bez refine boilerplate (_QUESTION_RE).
- Oborová synonyma médií deterministicky (
expand_media_synonyms):
kejda/močůvka/splašky → fekálie, hobliny → piliny – embedding zákaznický
termín nespojí, LLM instrukce nestačila.
- „Skladem X metrů" nikdy nefiltruje Délku (deterministická pojistka)
a rerank množství/cenu/dostupnost při hodnocení ignoruje.
- Zmínka konkrétního produktu („levnější než CALORTEC 165") jde do
hledaného textu beze změny → pool z příbuzné řady; reference s cenou,
produkt sám se mezi „levnějšími" neuvádí.
- Provozní dotazy: reklamovat/vrátit/dodat/stihnete → shop-meta hláška;
náhrada za produkt = on_topic (bez vymyšlených důvodů náhrady);
prompt-injection → off-topic.
- cm → mm deterministicky přes prompt (2,5 cm = 25 mm, ne palce);
„3 nejlepší/možnosti/varianty" mění počet výsledků; multi-item dotaz
(hadice + spony) vedlejší položku neignoruje mlčky.
Vývoj skóre kol (10 konverzací/kolo, stále těžší scénáře)
kolo 3: 4,45 → kolo 4: 4,4 → kolo 5: 4,5 → kolo 6: 4,5 → kolo 7: 4,4 → kolo 8: ~4,6
[0.14.0] – 2026-07-11 – Konverzační inteligence (2 regresní kola × 10 konverzací)
Přidáno / opraveno (kolo 1)
- Navazující zprávy neztrácí kontext média pro rerank a odpověď (ctx_query).
- České slovní palce: půlcoulka, dvoucoulová, třičtvrtěcoulová, coulová, 2 couly.
- Paměť nabídnutých produktů per session + deterministické rozuzlení
„ta první/druhá/poslední" (resolve_ordinal).
- Refine (výzva k upřesnění) jen u první zprávy session.
- „Nejlevnější?" → řazení podle ceny z dat;
_num()odmítá bool;
„deska A na B metrů" = plocha, ne tloušťka.
Přidáno / opraveno (kolo 2)
- Odkazovaný produkt se přibalí do odpovědi (
search.get_by_name+ pin):
„ta druhá, jakou má cenu?" odpoví cenou konkrétního produktu (dřív „nemáme").
- Krátké meta-dotazy (cena?/sklad?) připnou dřívější nabídku – „bude
skladem aspoň 20 metrů?" odpovídá reálnými čísly skladu dřív nabídnutých
produktů (skladem_mnozstvi v datech odpovědi).
- Negace: pole
excludev extrakci („ne PVC") → filtr kandidátů dle názvu
+ instrukce reranku/odpovědi.
- Provozní dotazy e-shopu (doprava/platba/reklamace) → slušný odkaz na
gms.cz místo „není z naší nabídky".
- Extrakce: „X metrů skladem" není rozměrové omezení; u extrémních chemikálií
odpověď doporučí ověření s technikem GMS.
[0.13.0] – 2026-07-10 – Reálná skladová dostupnost (sklady.xml)
Přidáno
- Objeven a nasazen skladový export
…/api/partnerfeed/get/sklady.xml
(30 558 záznamů: onstock/reserved/onstockfrom, klíč = skus.code, 100%
shoda). app/stock.py: stock_qty = max(onstock − reserved, 0),
restock_date. 15 084 SKU reálně skladem (dřív konstantní
DELIVERY_DATE=0 z Heureky u všeho).
- Dostupnost v odpovědi: „skladem" / „dostupné od 15. 7. 2026" /
„na objednávku" — z reálného skladu; Heureka delivery_days jen fallback
pro SKU bez záznamu; bez dat se vynechá. Pojistka MIN_STOCK_ITEMS.
- Denní refresh: feedy (5) → sync → kategorie → sklad → ceny z Heureky.
Ručně: scripts/apply_stock.py.
- FEED-REQUIREMENTS.md zjednodušeno: sklad i kategorie vyřešeny z XML; zbývá
hlavně ceny per ordercode, URL skupin a integrita exportů (EAN
odstraněn na žádost zadavatele).
[0.12.0] – 2026-07-10 – Kategorie u 99,7 % sortimentu (číselník skupin z XML)
Přidáno
- Objeven a nasazen číselník skupin
…/api/partnerfeed/get/skupiny.xml
(676 skupin: guid → název, parent) — nikde nedokumentovaný, ale existuje.
app/groups.py: kategorie produktu = nejhlubší z jeho <groups> GUIDů,
cesta průchodem stromu, URL = slug cesty (ověřený vzor). **Pokrytí kategorií
vzrostlo z 2 653 na 5 654 z 5 673 produktů (99,7 %)** — hydraulika, PU
spirály atd. teď mají odkaz na kategorii. Vše z XML (žádný web).
- Skupiny jsou autoritativní zdroj kategorií; Heureka enrich nastavuje už jen
ceny/dostupnost/URL. Denní refresh: feedy → sync → kategorie → enrich.
Pojistka MIN_GROUPS (torzo číselníku nic nepřepíše). Ruční běh:
scripts/apply_categories.py.
- FEED-REQUIREMENTS.md: P3 (číselník) vyřešeno, zbývá jen nice-to-have
<url>
ve skupinách; P1 sklad, P2 ceny, P4 integrita ověřeny jako skutečně chybějící.
[0.11.0] – 2026-07-10 – Relevance: palce, aliasy průměrů, rozvolnění, oprava slabých dotazů
Z vyhodnocení 20 testovacích dotazů (16× výborné, 2× slabé) – opravy slabých míst:
Přidáno / opraveno
- Palcové průměry deterministicky (
pipeline.apply_inch_diameters): 1/2", 2"
se z dotazu vytěží regexem a převedou na mm jako range ±6 % (2" → 47,8–53,8);
závity (G 1/2") se ignorují. Nespolehlivý převod LLM se přepíše.
→ „spojka kamlok 2\"" teď vrací skutečné KAMLOKy DN 50 (dřív 1 nesouvisející trn),
„zahradní hadice 1/2\"" vrací AQUATEC ⌀ 12,5–13 (dřív se rozměr zahodil).
- Aliasy průměrových parametrů ve filtru (Vnitřní průměr ↔ DN ↔ JS ↔ Určeno
na hadici ↔ Průměr ↔ Trubka) – tvrdý filtr už nevyřadí správný produkt jen
kvůli jinému názvu téhož rozměru.
- Rozvolnění při málo shodách: <
SEARCH_RELAX_MIN(3) přesných výsledků →
pool se doplní rozvolněnými kandidáty; přesné shody nesou označení, které
rerank preferuje (dřív se rozvolňovalo jen při 0 výsledcích).
- Rerank i pro jediného kandidáta – nevhodný smí vyřadit; prázdné pořadí
u ≤ 2 kandidátů se respektuje (konec rozporu „nemám nic vhodného" + 1 výsledek).
- Dedup dle názvu ve výsledcích (2 produkty stejného jména matou).
- Přiznání alternativy: když produkty neodpovídají žádanému materiálu/typu
(„polyamidová tyč" → jen PTFE), asistent to řekne na rovinu.
- Zásada „vše z XML": veškerá vstupní data výhradně z feedů, web se
nescrapuje. Chybějící údaje (kategorie mimo Heureku, sklad, ceny části
sortimentu) řeší požadavky na vývojáře GMS — docs/FEED-REQUIREMENTS.md
(sklad a ceny per ordercode, číselník skupin s URL, kontrola integrity
exportu; vše aditivní).
[0.10.0] – 2026-07-10 – Sync katalogu (soft-delete) + pojistky proti vadnému exportu
Přidáno
- Denní synchronizace katalogu (
app/sync.py, součást scheduleru; ručně
scripts/sync_catalog.py): položky zrušené na e-shopu se označí
products.active=false (soft-delete) a hledání je vyřadí — data vč.
embeddingů zůstávají (návrat položky = jen reaktivace). Nové položky se
vloží vč. SKU, parametrů, fulltextu a embeddingu; SKU existujících produktů
se přidají/odeberou dle feedu. Zrušené zboží mizí z výsledků do 24 h.
- Pojistky proti vadnému/poloprázdnému exportu (DB se nikdy nevyprázdní):
sync odmítne feed s < INGEST_MIN_PRODUCTS (1000) produkty nebo deaktivaci
SYNC_MAX_DEACTIVATE_PCT(20 %) aktivních; plný import (ingest_partner.py)
odmítne feed menší než INGEST_MIN_KEEP_PCT (90 %) stavu DB — před TRUNCATE,
vynucení --force; enrich přeskočí Heureka feed s < HEUREKA_MIN_PRODUCTS
(500) produkty; fetch_feeds.sh stahuje do .tmp + kontrola velikosti a XML
hlavičky (vadný download nepřepíše funkční feed).
- Ověřeno na DEV s čerstvými feedy: +8 nových produktů (rovnou s embeddingem),
2 zrušené deaktivovány (zmizely z hledání), SKU +132/−16; pojistky odmítly
3-produktový feed (sync i ingest), DB nedotčena.
Změněno
embed_products.py→ helperapp.embed.product_text(sdílený se syncem).- Scheduler: denní běh = feedy → sync katalogu → enrich (dřív jen enrich).
[0.9.0] – 2026-07-10 – Automatický denní refresh dat + monitoring
Přidáno
- Denní automatický refresh dat (compose služba
scheduler,
scripts/scheduler.py): každý den v 5:00 (REFRESH_HOUR, TZ=Europe/Prague)
stáhne feedy z gms.cz (atomicky, sanity-check velikosti a XML hlavičky – při
chybě zůstane starý soubor) a obohatí DB z Heureky (ceny/sklad/kategorie).
Plný reingest katalogu zůstává ruční (chrání embeddingy).
- Monitoring
GET /api/metricschráněný tajným klíčemMETRICS_KEY
(?key= nebo hlavička X-Metrics-Key; bez klíče v configu endpoint
neexistuje – 404). Vrací:
process– perf countery od startu (per endpoint: počet, chyby, avg/max ms),chats– statistiky z chat_logs (celkem/24 h/7 dní, chyby, sessions,
off-topic, avg+p95 latence, tokeny a cena celkem i za 24 h),
data– pokrytí dat (produkty/SKU, s cenou, s kategorií, s embeddingem),app– verze, provider, stav DB.
[0.8.0] – 2026-07-10 – Ceny, dostupnost, kategorie + navádění na upřesnění
Přidáno
- Obohacení z Heureka feedu (
app/heureka.py,scripts/enrich_heureka.py):
ceny („od X Kč" / přesná u spárované varianty), dostupnost („skladem" z
DELIVERY_DATE), kategorie s odkazem (URL = slug cesty, ověřený vzor
gms.cz) a přesné URL produktů/variant. Napojení: produkt přes slug názvu
(93,4 % Heureka produktů), varianta→SKU přes hmotnost / ≥2 číselné
parametry, jen jednoznačné shody (79,0 % variant). Dřívější napojení cen
přes přesnou shodu názvu bylo 0% (tiše rozbité) — nahrazeno.
- Odpověď: u každého produktu cena a dostupnost (jen z dat), na konci
Celá kategorie: název. Nové sloupce v products/skus (viz schema).
- Navádění na upřesnění (režim D): u obecného dotazu („hadice na benzín")
pipeline z dat zjistí, které parametry se u nalezených produktů liší
(refine_hints — průměr, tlak…), asistent vyzve k upřesnění, ale produkty
+ kategorii vypíše vždy. Deterministická detekce (žádná číselná omezení
v dotazu), žádné hádání.
enrich_heureka.pyjde pouštět samostatně (jen UPDATE — refresh cen/skladu
bez ztráty embeddingů, vhodné pro cron) i jako součást ingest_partner.py.
- Testy: heureka parsování/párování, ceny/dostupnost/kategorie v promptu,
refine_hints + integrace.
Známá omezení dat (k dohodě s GMS)
- Heureka pokrývá ~2,8 k z 5,7 k produktů — zbytek bez ceny/kategorie (vynechá se).
DELIVERY_DATE=0 u všech položek — reálný sklad by chtěl rozšířit partnerský
feed (množství per ordercode) + číselník skupin (guid→název).
[0.7.1] – 2026-07-10 – Refactoring do modulů + testovací suite
Změněno (chování beze změny)
- Modularizace pro paralelní vývoj (více vývojářů/agentů může pracovat bez
konfliktů): app/server.py je JEN HTTP vrstva; orchestrace chatu přesunuta do
app/pipeline.py (run_chat, HistoryStore, ChatError, náklady,
normalizace omezení); monolitický app/llm.py rozdělen na balíček
app/llm/ (providers.py, prompts.py, rerank.py, retry.py +
fasáda __init__.py se stabilním API); noise-filtr vyčleněn do app/noise.py.
- Klíčové parametry produktu pro odpověď extrahuje
prompts._key_params
(dřív inline v _answer_prompt).
Přidáno
- Testovací suite (pytest, bez DB a bez sítě – mocky): pipeline helpery
(normalize_constraints, desired_count, cost_usd, HistoryStore),
celé flow run_chat (happy path, off-topic, fallback, rerank chyby, paměť
session), search (slugify, blend, SQL fragmenty omezení), noise-filtr,
prompty, rerank (dedup/rozsahy/fallback), auth (HMAC podpisy, debug heslo),
HTTP vrstva (TestClient). Kořenový conftest.py pro import path.
/api/chatvrací 400empty_requestna prázdnou zprávu (dřív šla do LLM).
Odstraněno
- Větev
feature/rozpoznavani-obrazku(rozpoznání produktu z obrázku, MVP
0.8.0-dev) smazána vč. originu — funkce se nezachovává.
[0.7.0] – 2026-06-20 15:36 – Migrace na Gemini, logování tokenů/ceny, oprava relevance
AI vrstva → Google Gemini (jedna firma)
- Chat: extrakce
gemini-2.5-flash-lite, odpověďgemini-2.5-flash, rerank
gemini-2.5-flash-lite. Vypnuté „thinking" (rychlost+cena). ~4–5× levnější než gpt-4o.
- Embeddingy → Gemini (
gemini-embedding-001, 1536 dim). A/B proti OpenAI
ukázal výrazně lepší relevanci na těžkých dotazech (pára→CALORTEC, kyselina→CHEMITEC,
odsávání pilin→FLEXADUR). Práh relevance rekalibrován na 0.62 (Gemini sims jsou vyšší).
- Role-specific modely + provideři přes
.env(levný extract, kvalitní answer).
Sledování nákladů
- Logování tokenů + modelů každého volání do
chat_logs. - Cena za dotaz se zobrazuje v chatu (💰 $cena · tokeny · ms).
Relevance
- Hybridní blend sémantika ∪ fulltext (oprava „hadice na páru").
Integrace
- API dokumentace pro vývojáře
docs/API.md+ CORS (chat widget do e-shopu). - Architektura
docs/ARCHITECTURE.md. Responzivní UI pro mobil. Přepínač modelu z UI pryč.
[0.6.8] – 2026-06-20 14:21 – Klíčové parametry u každého produktu (default)
Změněno
- Parametry se zobrazují DEFAULTNĚ u každého produktu (dřív jen na vyžádání):
kompaktní klíčová sada – Vnitřní/Vnější průměr, Provozní tlak, Poloměr ohybu,
příp. DN/Délka (max 5). Balast (balení, hmotnost, série, barva, normy) se
nevypisuje. Funguje i v poradním/srovnávacím režimu.
[0.6.7] – 2026-06-20 14:15 – Poradní režim (srovnávání a doporučování)
Přidáno
- Poradní/diskuzní režim: asistent umí odborně PORADIT a SROVNAT (co je lepší
/ vhodnější, rozdíl mezi materiály/typy) na základě oborových znalostí –
nejdřív krátká věcná odpověď, pak doporučené produkty z katalogu.
- Hranice zůstává: konkrétní ČÍSLA o produktu (rozměry, tlaky, teploty, normy,
ceny) si NIKDY nevymýšlí – jen z dat, jinak odkáže na stránku produktu;
produkty doporučuje jen reálné z katalogu. (Princip „nehádat" se týká čísel,
ne obecné rady.)
[0.6.6] – 2026-06-20 14:09 – 5 výsledků, odpovědi na dotazy z DB
Změněno / přidáno
- Standardně 5 produktů (dřív max 3–4); na vyžádání („ukaž víc", „10 hadic")
až 10. Asistent vypisuje VŠECHNY dohledané produkty.
- Odpovědi na dotazy z databáze: když se zákazník na něco zeptá (materiál,
teplota, tlak, norma, atest, vhodnost), asistent odpoví STRUČNĚ a JEN z dat
(detail / dokumentace / parametry). Když údaj v datech není → odkáže na stránku
produktu, nic si nevymýšlí.
- Do dat odpovědi přidána
dokumentace(doc_text: normy/atesty/materiály). - Oprava formátu parametrů: jednotka se nepřidává dvakrát (už ne „300 mm mm").
[0.6.5] – 2026-06-20 13:57 – Parametry na vyžádání + oprava zdůvodnění
Opraveno / přidáno
- Parametry na vyžádání: když zákazník výslovně chce parametry/rozměry, asistent
je teď VYPÍŠE u každého produktu (tvar „Název: hodnota", jen užitečné – bez
Balení/Hmotnost/Série/Barva). Defaultně se parametry stále neukazují.
(Dřív na žádost o parametry nic neukázal.)
- Zdůvodnění odpovídá dotazu: úvodní věta se formuluje podle média/použití
z dotazu zákazníka, nezužuje význam (dřív „hadice na vodu" → „potravinářské").
[0.6.4] – 2026-06-20 13:42 – Reálná diskuze: meta-dotazy a žádost o parametry
Opraveno (na základě reálné diskuze na TESTu)
- Meta-instrukce neředí vyhledávání: do sémantiky jde jen čistá fráze od LLM
(text), ne surová historie+zpráva. Dřív „Ukaž mne parametry daných produktů"
naředilo dotaz a mezi palivové hadice se připletla potravinářská.
- Žádost o parametry/ceny → asistent rovnou vypíše produkty s krátkým popisem
a odkazem (konkrétní čísla neuvádí – přesné parametry/ceny jsou na stránce produktu).
[0.6.3] – 2026-06-20 13:24 – Code review: opravy chyb
Opraveno
- Reranker mohl zahodit vše → pokud LLM-reranker nevybral žádný index, vrátí
se top-k dle podobnosti (dřív „nic nenalezeno" i u relevantních produktů).
- Plýtvání DB dotazy: SKU/parametry se dotahují až pro FINÁLNÍ výsledky
(po reranku, 4), ne pro celý pool 20 kandidátů. Méně round-tripů na chat.
- SKU u rozvolněných omezení: když omezení vynulují varianty (soft/FTS
fallback), _matching_skus ukáže všechny SKU produktu (dřív 0 variant).
- Strop paměti diskuzí (
_HISTORY): max 2000 sessions (proti neomez. růstu).
Ověřeno (review)
- Noise filtr NEodebírá reálné hadice – 144 zachycených „hadic" jsou příslušenství
(trny, spojky, vsuvky, redukce). Funguje správně.
- Žádná SQL injekce (parametrizováno), XSS ošetřené, HMAC
compare_digest.
[0.6.2] – 2026-06-20 11:10 – Chat UI: jen čistá odpověď
Změněno
- Debug chat UI (
static/index.html): pod odpovědí už NEvypisuje velký blok
„Nalezené produkty" s tabulkami SKU/rozměrů/tlaků. Tyto detaily jsou teď uvnitř
sbaleného 🔧 Debug panelu (na rozkliknutí pro ladění). Hlavní chat ukazuje
jen čistou odpověď (produkt + popis + odkaz na e-shop).
[0.6.1] – 2026-06-20 10:57 – Pevný formát odpovědi (jen produkt + popis + odkaz)
Změněno
- Odpověď asistenta má pevný stručný formát: max 3 produkty, každý jako
- název — krátký popis (≤8 slov) + na konci
🔎 [Zobrazit vše na e-shopu]. **Žádné výpisy rozměrů/tlaků/teplot/norem/
materiálů/cen** – ty si zákazník zobrazí na e-shopu.
Pozn.
- Mezi 0.6.0 a 0.6.1 proběhla „kampaň2" zpřesnění relevance (commity v gitu, mimo
changelog): noise filtr balastu (ne-hadice) v hledání + LLM-reranker
(gpt-4o-mini: pool 20 → top 4, vyhazuje příslušenství/koncovky) + on_topic gate
uznává popis použití.
[0.6.0] – 2026-06-19 19:40 – Extrakce dat z dokumentů (PDF+OCR) a zpřesnění
Added — maximální extrakce dat
scripts/extract_docs.py– stáhne a vytěží text ze VŠECH dokumentů produktů:
PDF (text vrstva, PyMuPDF) + interní OCR (tesseract, ces+eng+deu) na scany
i obrázky. Resilientní (cache, resume, limit paměti proti OOM, strip NUL).
→ 9 324 dokumentů, 4 393 produktů má doc_text (~18,7M znaků).
scripts/reindex_fts.py– fulltext zahrnuje idoc_text(normy, atesty, materiály).doc_textv odpovědi asistenta → odpovědi cituji normy/odolnosti/teploty/
atesty z katalogových listů (i cizojazyčných, shrnuté česky). Zpřesnění výstupů.
Zlepšeno — relevance a robustnost (iterativně přes scripts/eval3.py, 20 odborných otázek)
- Soft-constraints: když přesná rozměrová/parametrová omezení vrátí 0, rozvolní se
→ topicky relevantní výsledky (nic se nehádá, odpověď ukáže reálné parametry).
Přesné rozměry zůstávají, když existují (benzín Ø50 → jen Ø50).
- Validace omezení: zahazují se neaplikovatelná (textová/materiálová) omezení
s lo/hi=None (dřív nulovala výsledky, např. „Camlock z nerezu" → 0).
- Deterministická extrakce (OpenAI
temperature=0) → stabilní on_topic/omezení
(konec kolísání stejného dotazu mezi v=0 a v=5).
- Embeddingy ZÁMĚRNĚ bez
doc_text(cizojazyčný OCR text ředil relevanci –
ověřeno: zhoršoval skóre); doc_text se využívá jen ve fulltextu a v odpovědi.
Výsledky (eval3, 20 odborných dotazů, LLM-soudce)
| před | po | |
|---|---|---|
| spokojenost | 4.10/5 | 4.20–4.35/5 (stabilní) |
| false-negativy (v=0) | několik (Camlock, EN 12115…) | 0 |
- Zbývají 2 niche aplikační dotazy (silikon na páru, vrapovaná na piliny) – produkty
existují, ale embedding nadřazuje příbuzné položky; hraniční ranking, ne chyba.
[0.5.2] – 2026-06-19 17:28 – Changelog na webu, UI drobnosti
Added
- Changelog na webu: stránka
/changelog(renderujeCHANGELOG.md,
app/mdrender.py – minimální Markdown→HTML bez závislostí). Odkazy
„Changelog" a „Admin" v hlavičce chatu.
Změněno
- Indikátor psaní v chatu: „typuji…" → „Přemýšlím…".
Pozn. (infrastruktura)
- Cron pro obnovu certu zúžen na
--cert-name gms.x86.cz(neplete se do NPM
certů); dry-run obnovy gms.x86.cz = success.
[0.5.1] – 2026-06-19 17:23 – Recall a HTTPS
Zlepšeno
- Recall vágních dotazů: pro sémantiku se embeduje kombinace čisté fráze
od LLM (dobrá diakritika) + celého dotazu (zachová kontext jako teplota/médium).
„najdi hadici na benzin" → opět 5 výsledků (dřív 1); „pryž nad 150 °C" → 5.
Ověřeno cílenými testy (benzín/polyamid/pryž 150°C/voda = 5, banán = 0).
Nasazení
- HTTPS na gms.x86.cz hotové ✅ – Let's Encrypt cert (certbot webroot v NPM),
443 SSL blok + redirect 80→443 v nginx-proxy-manager:/data/nginx/custom/http.conf.
Ověřeno z internetu: https://gms.x86.cz → 200, http → 301. Cert expiruje
2026-09-17, automatická obnova přes cron na droidu (certbot renew + reload).
[0.5.0] – 2026-06-19 17:06 – F4: Relevance přes evaluaci 55 otázek, robustnost, TEST/rproxy
Zlepšeno (iterativně přes scripts/eval2.py – 55 reálných zákaznických otázek po kategoriích)
- on_topic doména rozšířena podle skutečných kategorií GMS (pryže, gumové/
těsnící desky, podlahoviny, profily, tyče, technické plasty) – přestaly se
chybně zahazovat dotazy typu „polyamidová tyč", „pryž nad 150 °C".
- Sémantika embeduje PŘIROZENÝ dotaz (historie + zpráva), ne LLM-keywordy →
spolehlivější relevance (false-negativy zmizely).
- Poradenské dotazy (rozdíl materiálů, jak vybrat, na míru): když není přesný
produkt, LLM poradí obecně a doptá se (bez výmyslu produktů). Mimo-obor →
stručná hláška bez volání LLM.
- Max 5 produktů (tvrdý strop), klidně méně (práh relevance ořeže slabé).
Robustnost
- Ošetření 429 (rate limit TPM): OpenAI i Anthropic klient
max_retries=6
(automatický exponenciální backoff, respektuje retry-after); zmenšen prompt
odpovědi (méně tokenů). Eval harness má pacing.
Výsledky evaluace (55 otázek, LLM-soudce spokojenosti)
| baseline | po iteracích | |
|---|---|---|
| spokojenost | 3.73/5 | 4.20/5 (+12,6 %) |
| slabé odpovědi (≤2/5) | 12 | 1 |
| nejslabší kategorie | Plasty 2.0, Pryže 3.3 | vše ≥ 4.0 |
Nasazení / infrastruktura
- rproxy hotová: http://gms.x86.cz/ jede (NPM custom include
/data/nginx/custom/http.conf → 192.168.254.199:45080). HTTPS = TODO
(Let's Encrypt přes NPM GUI).
- Changelog: konvence verze + datum a čas u každého releasu (časy z gitu).
[0.4.0] – 2026-06-19 15:16 – F3: Logování, admin, multi-turn, evaluace relevance
Added
- Logování diskuzí (
app/chatlog.py, tabulkachat_logs– idempotentní,
přežije re-ingestaci): každý chat se ukládá (zpráva, extrakce, výsledky, odpověď,
časy, ok/error, session_id).
- Admin rozhraní
static/admin.htmlna/admin+ endpointy/api/logs,
/api/logs/{id} (chráněné DEBUG_PASSWORD): procházení diskuzí, detail,
filtr podle session, „Kopírovat odkaz na log", deep-linky.
- Paměť diskuze (multi-turn): server kumuluje kontext podle
session_id,
LLM extrakce dostává historii → zákazník může postupně upřesňovat (5× pokračovat).
- on_topic gate: LLM rozhodne, zda je dotaz z oboru; mimo-oborové (notebook,
banán, auto) → vrací NIC (radši nic než nerelevantní).
- Laditelný práh relevance
SEARCH_MIN_SIM(env, default 0.40 – kalibrováno:
relevantní ~0.43+, nesmysly ~0.30–0.39).
scripts/eval.py– evaluační harness: 21 simulovaných diskuzí (laická i přesná
zadání, multi-turn), LLM-soudce spokojenosti.
- Odpověď obohacena o technický detail (materiál/teplota/použití z
body_text),
výpis až 5 produktů s odkazy; default limit 5.
Opraveno (přes iterace eval harness)
- Fulltext fallback se NEspouští, když sémantika nic relevantního nenašla
(dřív vracel keyword balast).
- Sémantika dostává PŘIROZENOU frázi (např. „hadice na naftu"), ne holá klíčová
slova (ta embedovala špatně) → opravena regrese vágních dotazů.
- OpenAI/Claude/Gemini extrakce: pole
on_topic, multi-turn historie.
Výsledky evaluace (21 diskuzí, LLM-soudce)
| baseline | po iteracích | |
|---|---|---|
| správné chování | 18/21 (86 %) | 21/21 (100 %) |
| spokojenost (relevantní) | 3.56/5 | 3.72/5 |
| spokojenost (celkem) | 3.48/5 | 3.71/5 |
- Strop spokojenosti drží chybějící ceny (soudce opakovaně „cena chybí")
– přijdou z plné DB.
Analýza PDF (závěr)
- Datasheety lze extrahovat (pypdf), ale jsou často v IT/EN, dokumenty jsou
smíšené (i .jpg, právní prohlášení). body_text už pokrývá podstatu.
- Plošná ingestace PDF se nedoporučuje (okrajový přínos, velká práce/OCR).
Selektivně jen pro produkty s prázdným popisem. Větší přínos = ceny + kategorie.
[0.3.0] – 2026-06-19 14:31 – F2: Sémantika, relevance, ověřeno v Dockeru
Added
app/embed.py+scripts/embed_products.py– sémantické embeddingy
(OpenAI text-embedding-3-small, 1536 dim) do products.embedding (pgvector),
HNSW index. Pro všech 5 665 produktů spočítáno.
app/search.pypřepsán na sémantické vyhledávání s prahem relevance
(MIN_SIM=0.30) – přirozený dotaz řadí embeddingy, rozměry zůstávají tvrdým
filtrem; fulltext (FTS) je fallback. → vrací jen relevantní výsledky.
- Odkaz na produkt v každém výsledku (
url) na eshopové vyhledávání
https://www.gms.cz/?query=<název>; asistent ho uvádí jako markdown odkaz.
scripts/ask.py– ruční test relevance přes API.
Ověřeno v běžícím Dockeru (PostgreSQL+pgvector)
- Ingestace partnerských feedů do Postgresu: **5 665 produktů, 45 593 SKU,
343 878 hodnot parametrů**.
- Hybrid hledání s přesným filtrem rozměrů (benzín+Ø50, silikon+Ø8, tlak≥40 bar) ✓.
- Cíl „najdi hadici na benzin" → jen relevantní palivové hadice (PETROTEC TX/
NBR/FUB/CLC…, FKM ponorná), žádné navijáky/pistole/požární hadice. ✓
- Chat end-to-end přes OpenAI i Gemini (extrakce → hledání → česká odpověď
s odkazy). Klíče v .env (negitováno).
Opraveno
- Pořadí vazby SQL parametrů v hledání (
%spoziční). - OpenAI extrakce přepnuta na striktní
json_schema(constraints jako pole). - Gemini model
gemini-2.0-flash(404) →gemini-2.5-flash. .dockerignore(image bez.venv/dat); feedy mimo image, kopírují se k ingestaci.
Pozn.
- Přímé URL produktu (slug) nejsou v partnerském feedu – dočasně odkazujeme na
eshopové hledání podle názvu; přesné URL doplníme z plné DB.
- Ceny zatím NULL (Heureka join podle názvu nesedí) – doladí se z plné DB.
[0.2.0] – 2026-06-19 14:00 – F1: Microservice (FastAPI + PostgreSQL + AI)
Rozhodnutí (odsouhlaseno zadavatelem)
- Databáze: PostgreSQL + pgvector (docker-compose, 2 kontejnery).
- AI: přepínatelní poskytovatelé Claude / OpenAI / Gemini (default Claude,
model claude-opus-4-8).
- Port služby 8040; doména gms.x86.cz přes RPROXY na PikoDroidu.
Added
db/schema.sql– PostgreSQL schéma (products/skus/params/param_dict,
pgvector embedding, tsvector fulltext s unaccent). ID jako TEXT
(katalogové kódy vč. SERVICE*, NV260347 – o žádnou položku nepřijdeme).
app/feed.py– čisté parsování partnerských feedů (testovatelné bez DB).scripts/ingest_partner.py– ingestace zbozi.xml + parametry.xml do Postgresu
přes COPY, stavba fulltextu, doplnění cen z Heureka feedu.
app/db.py– psycopg3 connection pool (lazy, app jde importovat i bez DB).app/search.py– hybrid retrieval: fulltext + PŘESNÝ strukturovaný filtr
rozměrů nad parsovanými čísly (na úrovni produktu i SKU).
app/llm.py– přepínatelná AI vrstva (Claude/OpenAI/Gemini), structured
extrakce omezení + generace odpovědi nad dohledanými daty (lazy import SDK).
app/auth.py– zabezpečení API pro e-shop: API klíč + HMAC podpis (+ replay
ochrana), volitelné heslo pro debug UI.
app/server.py– FastAPI:/api/health,/api/params,/api/search
(chráněné), /api/chat (LLM + debug info), debug UI na /.
static/index.html– debug chat rozhraní (česky) s debug panelem
(extrahované filtry, časy, nalezené produkty/SKU/rozměry).
Dockerfile,docker-compose.yml(app + pgvector),.env.example,
scripts/fetch_feeds.sh.
- Rozšířena normalizace o teploty (
-30°C až +100°C→ −30..100 °C). - Testy:
tests/test_feed.py(3) +tests/test_normalize.py(9 vč. teplot) =
12 prochází.
Ověřeno
- Všechny moduly se kompilují; aplikace nastartuje a degraduje elegantně bez DB/
klíčů (smoke test TestClient: /api/health 200, UI na /, čisté 503/502).
- Parsování partnerských feedů ověřeno na reálných datech (5 665 produktů,
45 592 SKU, 343 878 hodnot parametrů, 0 sirotků).
Čeká na zadavatele (blokuje finální ověření)
- Spustit Docker (lokálně chybí daemon) → integrační test Postgres+ingestace+
hybrid search + build image.
- GitHub auth (
gh/token chybí) → push do privátního repapikodrak/gms-asistent. - AI klíče do
.env→ ověření/api/chatend-to-end.
[0.1.0] – 2026-06-19 13:40 – F0: Průzkum a datový základ
Added
- Inicializace projektu, profesionální struktura (
app/,scripts/,data/,
docs/, static/, tests/).
- Stažen pracovní produktový feed
data/feed-raw.xml(Heureka XML, 18,8 MB). app/normalize.py– normalizace rozměrů/parametrů. Klíčový princip
„nesmí odhadovat“: uchovává raw hodnotu, numeriku parsuje jen když je
jednoznačná (rozsahy, české čárky, převod MPa/kPa→bar), jinak ponechá text.
scripts/ingest.py– ingestace feedu do SQLite (products/variants/params
+ FTS5 fulltext bez diakritiky).
tests/test_normalize.py– 9 testů normalizace (vše prochází).docs/DESIGN.md– návrh architektury (hybrid retrieval, AI vrstva,
zabezpečené API, debug UI, nasazení).
Zjištění z dat (ověřeno)
- 11 894 variant / 2 829 produktů / 75 550 parametrů / 74 typů parametrů.
- Varianty se liší hlavně rozměry → nutné přesné strukturované filtrování.
- 82,3 % parametrů bezpečně parsováno na čísla; zbytek (závity, placeholdery)
ponechán jako přesný text.
- Ověřeno: hybrid dotaz „benzín + vnitřní Ø 50 mm“ → 1 přesná shoda.
Partnerské feedy (přidáno, bohatší zdroj)
- Staženy
data/zbozi.xml(22 MB) adata/parametry.xml(27 MB). - zbozi.xml: 5 665 produktů, 45 594 objednacích kódů (SKU), dlouhé technické
popisy, 9 324 PDF datasheetů, 12 481 obrázků, vazby skupin a souvisejících.
- parametry.xml: slovník 1 638 parametrů + hodnoty pro 51 259 položek
(produkty i SKU), ø 6,7 parametru/položku.
- Ověřena vazba:
parametryid ↔ produkt/SKU zzbozi= 100 % shoda, 0 sirotků. - Závěr: partnerské feedy budou primární zdroj (F1), Heureka feed záloha + ceny.
Infrastruktura (poznámky)
- Cílová doména: gms.x86.cz na PikoDroidu (DNS hotovo). Nutno přidat záznam
na RPROXY → HTTPS do Docker kontejneru.
- Deploy a privátní GitHub repo dle vzoru
pikodrak-game-marshal-spy.