| <!DOCTYPE html> |
| <html lang="ru"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>NL→SQL Assistant — схема движения данных</title> |
| <link rel="preconnect" href="https://fonts.googleapis.com"> |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> |
| <link href="https://fonts.googleapis.com/css2?family=Source+Serif+4:opsz,wght@8..60,500;8..60,600&family=IBM+Plex+Sans:wght@300;400;500;600&family=IBM+Plex+Mono:wght@400;500&display=swap&subset=cyrillic" rel="stylesheet"> |
| <style> |
| :root{ |
| --bg:#FAFAF8; |
| --text:#1C1C1A; |
| --accent:#9C5B38; |
| --accent-dark:#7C4426; |
| --muted:rgba(28,28,26,0.60); |
| --faint:rgba(28,28,26,0.38); |
| --line:rgba(28,28,26,0.40); |
| --divider:rgba(28,28,26,0.08); |
| --card:#FFFFFF; |
| } |
| *{box-sizing:border-box} |
| body{ |
| margin:0;background:var(--bg);color:var(--text); |
| font-family:'IBM Plex Sans',sans-serif;font-size:16px;line-height:1.6; |
| font-variant-numeric:tabular-nums lining-nums; |
| font-feature-settings:"tnum" 1,"lnum" 1; |
| text-wrap:pretty; |
| } |
| .mono{font-family:'IBM Plex Mono',monospace} |
| |
| |
| header{padding:48px 48px 24px;max-width:1880px;margin:0 auto} |
| h1{ |
| font-family:'Source Serif 4',Georgia,serif;font-size:40px;font-weight:600; |
| line-height:1.1;margin:0 0 8px;letter-spacing:-0.01em; |
| } |
| .subtitle{font-size:15px;color:var(--muted);margin:0;max-width:1020px} |
| .head-row{display:flex;justify-content:space-between;align-items:flex-end;gap:48px;margin-top:32px;flex-wrap:wrap} |
| .kpis{display:flex;gap:48px;flex-wrap:wrap} |
| .kpi .v{font-size:32px;font-weight:300;line-height:1.1;letter-spacing:-0.01em} |
| .kpi .v sup{font-size:14px;font-weight:400;color:var(--muted)} |
| .kpi .l{font-size:11px;text-transform:uppercase;letter-spacing:0.08em;color:var(--muted);margin-top:6px} |
| .legend{display:grid;grid-template-columns:auto auto;gap:6px 24px;font-size:12px;color:var(--muted);padding-bottom:4px} |
| .legend .li{display:flex;align-items:center;gap:10px;white-space:nowrap} |
| .legend svg{flex:0 0 auto} |
| .legend .chip{width:18px;height:18px;border-radius:50%;background:var(--accent);color:#FDFBF9; |
| font-size:10px;font-weight:600;display:inline-flex;align-items:center;justify-content:center;flex:0 0 auto} |
| |
| |
| #viewport{width:100%;overflow:hidden;padding:0 0 48px} |
| #scaler{transform-origin:top left;margin:0 auto} |
| #canvas{position:relative;width:1880px;height:2236px;font-size:13px} |
| |
| |
| .zone{position:absolute;border-radius:10px} |
| .zone-tint{background:rgba(28,28,26,0.024)} |
| .zone-hot{background:rgba(156,91,56,0.045)} |
| .zone-h{position:absolute;font-size:12px;font-weight:600;text-transform:uppercase;letter-spacing:0.1em;color:var(--text)} |
| .zone-h .zn{color:var(--accent);margin-right:8px} |
| .zone-h small{display:block;font-weight:400;text-transform:none;letter-spacing:0;font-size:12px;color:var(--muted);margin-top:2px} |
| |
| |
| .node{ |
| position:absolute;background:var(--card); |
| border:1px solid rgba(28,28,26,0.12);border-radius:8px; |
| padding:12px 14px;line-height:1.45; |
| box-shadow:0 1px 3px rgba(30,25,20,0.05); |
| transition:box-shadow 160ms ease; |
| } |
| .node:hover{box-shadow:0 4px 10px -2px rgba(30,25,20,0.13),0 2px 4px -2px rgba(30,25,20,0.08)} |
| .node.hot{border-top:3px solid var(--accent);padding-top:10px} |
| .node.dashed{border-style:dashed;background:rgba(255,255,255,0.7)} |
| .node h3{font-size:14px;font-weight:600;margin:0 0 1px;line-height:1.25;font-family:'IBM Plex Sans',sans-serif} |
| .node .path{font-family:'IBM Plex Mono',monospace;font-size:11px;color:var(--muted);margin:0 0 6px} |
| .node p{margin:0;font-size:13px;color:rgba(28,28,26,0.80)} |
| .node p b{font-weight:600;color:var(--text)} |
| .node ul{margin:2px 0 0;padding:0;list-style:none} |
| .node li{font-size:13px;color:rgba(28,28,26,0.80);padding-left:12px;position:relative} |
| .node li::before{content:"";position:absolute;left:0;top:0.62em;width:5px;height:1.5px;background:var(--accent)} |
| .badges{margin-top:8px;display:flex;flex-wrap:wrap;gap:4px} |
| .bdg{ |
| font-family:'IBM Plex Mono',monospace;font-size:11px;line-height:1; |
| background:rgba(28,28,26,0.055);border-radius:4px;padding:4px 7px;color:rgba(28,28,26,0.74); |
| white-space:nowrap; |
| } |
| .bdg.a{background:rgba(156,91,56,0.12);color:var(--accent-dark)} |
| .tag{ |
| display:inline-block;font-size:9.5px;font-weight:600;letter-spacing:0.08em;text-transform:uppercase; |
| border-radius:4px;padding:3px 6px;line-height:1;vertical-align:2px;margin-left:6px; |
| } |
| .tag.opt{background:rgba(28,28,26,0.07);color:var(--muted)} |
| .tag.pri{background:var(--accent);color:#FDFBF9} |
| .tag.nollm{background:rgba(28,28,26,0.85);color:#FAFAF8} |
| |
| |
| .step{ |
| position:absolute;top:-11px;left:-11px;width:22px;height:22px;border-radius:50%; |
| background:var(--accent);color:#FDFBF9;font-size:11.5px;font-weight:600; |
| display:flex;align-items:center;justify-content:center; |
| box-shadow:0 1px 3px rgba(30,25,20,0.28); |
| } |
| .step.alt{background:var(--card);color:var(--accent-dark);border:1.5px dashed var(--accent);box-shadow:none} |
| |
| |
| .group{ |
| position:absolute;border:1px dashed rgba(28,28,26,0.22);border-radius:10px; |
| } |
| .group .gt{ |
| position:absolute;top:14px;left:16px;font-size:11px;font-weight:600; |
| text-transform:uppercase;letter-spacing:0.09em;color:var(--muted); |
| } |
| .pcard{padding:10px 12px} |
| .pcard h3{font-size:13px} |
| .pcard p{font-size:12px;line-height:1.45} |
| .note{ |
| position:absolute;font-size:12px;color:var(--muted);line-height:1.5; |
| padding:10px 12px;border:1px dashed rgba(156,91,56,0.4);border-radius:8px; |
| background:rgba(156,91,56,0.04); |
| } |
| |
| |
| .ribbon{ |
| position:absolute;background:var(--card);border:1px solid rgba(28,28,26,0.12); |
| border-radius:8px;display:flex;align-items:center;gap:8px;padding:14px 20px;flex-wrap:wrap; |
| } |
| .ribbon .rt{font-size:12px;font-weight:600;text-transform:uppercase;letter-spacing:0.1em;margin-right:12px} |
| .ribbon .bdg{font-size:11px;padding:5px 9px} |
| |
| |
| #wires{position:absolute;inset:0;pointer-events:none} |
| .edge path.l{fill:none} |
| .edge .hot{stroke:var(--accent);stroke-width:2.2} |
| .edge .rep{stroke:var(--accent);stroke-width:1.7;stroke-dasharray:7 5} |
| .edge .bus{stroke:var(--accent);stroke-width:1.7;opacity:0.85} |
| .edge .idx{stroke:var(--line);stroke-width:1.6;stroke-dasharray:8 6} |
| .edge .opt{stroke:var(--line);stroke-width:1.5;stroke-dasharray:3 5} |
| .edge .eval{stroke:rgba(28,28,26,0.5);stroke-width:1.6;stroke-dasharray:2 6;stroke-linecap:round} |
| .edge text{font-family:'IBM Plex Sans',sans-serif;font-size:12px} |
| .edge .lbg{fill:#FAFAF8;opacity:0.94} |
| .edge .lt-hot{fill:var(--accent-dark)} |
| .edge .lt-mut{fill:rgba(28,28,26,0.70)} |
| #canvas.focus .edge{opacity:0.16;transition:opacity 120ms} |
| #canvas.focus .edge.on{opacity:1} |
| #canvas.focus .node{opacity:0.45;transition:opacity 120ms} |
| #canvas.focus .node.on{opacity:1} |
| #canvas.focus .group,#canvas.focus .zone-h{opacity:0.55} |
| |
| footer{max-width:1880px;margin:0 auto;padding:0 48px 56px;font-size:12px;color:var(--muted)} |
| footer .mono{font-size:11px} |
| |
| |
| #mobile-stub{display:none;position:fixed;inset:0;z-index:9999;background:var(--bg);color:var(--text); |
| align-items:center;justify-content:center;text-align:center;padding:40px} |
| |
| |
| #zoombar{ |
| position:fixed;right:20px;bottom:20px;z-index:500; |
| display:flex;align-items:center;gap:4px; |
| background:var(--card);border:1px solid rgba(28,28,26,0.14);border-radius:8px; |
| padding:6px 8px;box-shadow:0 2px 6px -1px rgba(30,25,20,0.12),0 1px 3px rgba(30,25,20,0.07); |
| } |
| #zoombar button{ |
| width:30px;height:28px;border:1px solid rgba(28,28,26,0.14);border-radius:6px; |
| background:var(--bg);color:var(--text);font:500 14px/1 'IBM Plex Mono',monospace; |
| cursor:pointer;padding:0; |
| } |
| #zoombar button:hover{background:rgba(156,91,56,0.08);border-color:rgba(156,91,56,0.4)} |
| #zoombar button:focus-visible{outline:2px solid var(--accent);outline-offset:1px} |
| #zoombar button.fit{width:auto;padding:0 10px;font-size:11px} |
| #zoombar #zval{font:400 11px 'IBM Plex Mono',monospace;color:var(--muted);min-width:38px;text-align:right} |
| |
| @media print{ |
| #zoombar,#mobile-stub{display:none!important} |
| .node,.ribbon,#zoombar{box-shadow:none!important} |
| body{-webkit-print-color-adjust:exact;print-color-adjust:exact} |
| } |
| </style> |
| </head> |
| <body> |
|
|
| <div id="mobile-stub"><p style="font-size:18px;line-height:1.6">Откройте страницу на десктопе<br>для корректного отображения схемы.</p></div> |
|
|
| <div id="zoombar"> |
| <button data-z="out" title="Уменьшить">−</button> |
| <button data-z="fit" class="fit" title="Вписать в окно">fit</button> |
| <button data-z="in" title="Увеличить">+</button> |
| <span id="zval">100%</span> |
| </div> |
|
|
| <header> |
| <h1>NL→SQL Assistant · схема движения данных</h1> |
| <p class="subtitle">Полный data flow портфолио-проекта: offline-индексация схем (01) → online query pipeline на LangGraph (02) → слой LLM-провайдеров с кэшем (03) → eval-контур с residue-циклом (04). Подписи на стрелках — что именно течёт между узлами; цифры в кружках — порядок шагов happy path.</p> |
| <div class="head-row"> |
| <div class="kpis"> |
| <div class="kpi"><div class="v">94.0<sup>%</sup></div><div class="l">EA · BIRD Mini-Dev n=200 · v31</div></div> |
| <div class="kpi"><div class="v">+1.04<sup>pp</sup></div><div class="l">над human-expert 92.96%</div></div> |
| <div class="kpi"><div class="v">$0</div><div class="l">external cost · free tiers + кэш</div></div> |
| <div class="kpi"><div class="v">370</div><div class="l">tests · coverage 91% · mypy strict</div></div> |
| <div class="kpi"><div class="v">7+2</div><div class="l">узлов LangGraph · база + opt-in</div></div> |
| </div> |
| <div class="legend"> |
| <div class="li"><svg width="36" height="8"><line x1="0" y1="4" x2="36" y2="4" stroke="#9C5B38" stroke-width="2.2"/></svg>горячий путь запроса</div> |
| <div class="li"><svg width="36" height="8"><line x1="0" y1="4" x2="36" y2="4" stroke="rgba(28,28,26,.4)" stroke-width="1.6" stroke-dasharray="8 6"/></svg>offline-индексация</div> |
| <div class="li"><svg width="36" height="8"><line x1="0" y1="4" x2="36" y2="4" stroke="#9C5B38" stroke-width="1.7" stroke-dasharray="7 5"/></svg>repair / retry</div> |
| <div class="li"><svg width="36" height="8"><line x1="0" y1="4" x2="36" y2="4" stroke="rgba(28,28,26,.5)" stroke-width="1.6" stroke-dasharray="2 6" stroke-linecap="round"/></svg>eval-контур</div> |
| <div class="li"><svg width="36" height="8"><line x1="0" y1="4" x2="36" y2="4" stroke="rgba(28,28,26,.4)" stroke-width="1.5" stroke-dasharray="3 5"/></svg>opt-in узлы</div> |
| <div class="li"><span class="chip">1</span>шаги happy path</div> |
| </div> |
| </div> |
| </header> |
|
|
| <div id="viewport"><div id="scaler"><div id="canvas"> |
|
|
| |
| <div class="zone zone-tint" style="left:24px;top:36px;width:1832px;height:360px"></div> |
| <div class="zone zone-hot" style="left:24px;top:420px;width:1832px;height:776px"></div> |
| <div class="zone zone-tint" style="left:24px;top:1240px;width:1832px;height:368px"></div> |
| <div class="zone zone-tint" style="left:24px;top:1648px;width:1832px;height:404px"></div> |
|
|
| <div class="zone-h" style="left:48px;top:52px"><span class="zn">01</span>Offline indexing<small>scripts/build_index.py · build_fewshot_index.py — однократно при setup / смене схемы</small></div> |
| <div class="zone-h" style="left:48px;top:436px"><span class="zn">02</span>Online query pipeline<small>agent/graph.py · LangGraph StateGraph · PipelineState</small></div> |
| <div class="zone-h" style="left:48px;top:1258px"><span class="zn">03</span>LLM provider layer<small>единый Protocol · кэш как основа $0-бюджета</small></div> |
| <div class="zone-h" style="left:48px;top:1666px"><span class="zn">04</span>Eval harness & audit<small>ablation → voting → merge → re-score → audit · residue-цикл ×31 (47% → 94.0%)</small></div> |
|
|
| |
| <div class="node" id="src" style="left:48px;top:88px;width:248px;height:216px"> |
| <h3>Data sources</h3> |
| <p class="path">scripts/download_data.py → data/</p> |
| <ul> |
| <li><b>BIRD Mini-Dev</b> — 11 SQLite БД · 500 Q→SQL (dev)</li> |
| <li><b>BIRD train</b> — 9 428 Q→SQL пар (HF parquet)</li> |
| <li><b>Chinook.sqlite</b> — smoke</li> |
| <li><b>PostgreSQL 16</b> — docker-compose, opt-in</li> |
| </ul> |
| <div class="badges"><span class="bdg">SQLite</span><span class="bdg">PostgreSQL 16</span></div> |
| </div> |
|
|
| <div class="node" id="intro" style="left:372px;top:104px;width:250px;height:160px"> |
| <h3>Introspector</h3> |
| <p class="path">schema_index/introspector.py</p> |
| <p><b>SQLAlchemy reflection</b> (read-only): таблицы, колонки, PK/FK, top-K sample values, NULL count, distinct count.</p> |
| <div class="badges"><span class="bdg">SQLAlchemy</span></div> |
| </div> |
|
|
| <div class="node" id="chunk" style="left:700px;top:104px;width:250px;height:148px"> |
| <h3>Chunker</h3> |
| <p class="path">schema_index/chunker.py</p> |
| <p><b>1 таблица = 1 card</b>: имя + колонки + типы + samples + FK от/к + business-hints; fk_targets → metadata.</p> |
| </div> |
|
|
| <div class="node" id="idx" style="left:1028px;top:104px;width:240px;height:148px"> |
| <h3>Indexer</h3> |
| <p class="path">schema_index/indexer.py</p> |
| <p>Upsert со <b>stable chunk_id</b> — без дублей при переиндексации; API чтения — SchemaIndex.</p> |
| </div> |
|
|
| <div class="node hot" id="chroma" style="left:1352px;top:96px;width:256px;height:204px"> |
| <h3>Vector store</h3> |
| <p class="path">ChromaDB · chroma_data/ (persistent)</p> |
| <ul> |
| <li><b>schema_chunks</b> — 1 запись = (db, table)</li> |
| <li><b>fewshot_qsql</b> — эмбеддится только вопрос; SQL + db_id + intent в metadata</li> |
| </ul> |
| <div class="badges"><span class="bdg a">chromadb</span></div> |
| </div> |
|
|
| <div class="node" id="fewshot" style="left:700px;top:304px;width:250px;height:88px"> |
| <h3 style="font-size:13px">Few-shot builder</h3> |
| <p class="path">scripts/build_fewshot_index.py</p> |
| <p style="font-size:12px">Q→SQL <b>только из train</b> · hard guard от dev-leakage.</p> |
| </div> |
|
|
| <div class="node" id="embed" style="left:1028px;top:304px;width:240px;height:88px"> |
| <h3 style="font-size:13px">Embeddings</h3> |
| <p class="path">mistral-embed · 1024-dim</p> |
| <p style="font-size:12px">CachingEmbeddingProvider → <b>diskcache</b>: реиндексация = 0 API.</p> |
| </div> |
|
|
| <div class="node" id="fkg" style="left:1672px;top:104px;width:168px;height:148px"> |
| <h3>FK graph</h3> |
| <p class="path">in-memory dict</p> |
| <p style="font-size:12px">SchemaIndex.fk_graph из fk_targets. <b>Не в Chroma</b>: FK-рёбра не несут семантики.</p> |
| </div> |
|
|
| |
| <div class="node hot" id="user" style="left:48px;top:510px;width:250px;height:64px"> |
| <span class="step">1</span> |
| <h3>Пользователь</h3> |
| <p>вопрос на RU / EN</p> |
| </div> |
|
|
| <div class="node hot" id="st" style="left:48px;top:610px;width:250px;height:184px"> |
| <h3>Streamlit UI</h3> |
| <p class="path">app/streamlit_app.py · 8 модулей</p> |
| <p>Chat + sample-вопросы · schema explorer · show-working trace · EN↔RU · режимы Accurate / Fast / Debug.</p> |
| <div class="badges"><span class="bdg">Streamlit</span><span class="bdg">Plotly</span><span class="bdg">@st.cache_resource</span></div> |
| </div> |
|
|
| <div class="node hot" id="api" style="left:48px;top:830px;width:250px;height:184px"> |
| <h3>FastAPI</h3> |
| <p class="path">src/nl_sql/api/main.py</p> |
| <p>POST /ask · GET /databases · /healthz · /readyz · /eval/latest. X-API-Key + token bucket <span class="mono">60 req/min</span> · Singletons DI.</p> |
| <div class="badges"><span class="bdg">FastAPI</span><span class="bdg">Pydantic v2</span><span class="bdg">uvicorn</span></div> |
| </div> |
|
|
| <div class="node" id="hf" style="left:48px;top:1050px;width:250px;height:106px"> |
| <h3 style="font-size:13px">Hugging Face Spaces</h3> |
| <p class="path">liovina-nl-sql.hf.space</p> |
| <p style="font-size:12px">Docker free tier · UI + API в одном контейнере · deploy ≈ 90 с.</p> |
| </div> |
|
|
| |
| <div class="node hot" id="ctx" style="left:400px;top:510px;width:270px;height:184px"> |
| <span class="step">2</span> |
| <h3>context_builder</h3> |
| <p class="path">schema_index/retriever.py</p> |
| <p>Dense top-k=5 schema-чанков (filter db_id) → <b>FK BFS ≤1 hop</b>, бюджет 12 таблиц → few-shot k=3 (cross-db на BIRD) → extended samples 3→5 → dialect hints. Выход: <b>ContextBundle</b>.</p> |
| </div> |
|
|
| <div class="node dashed" id="plan" style="left:742px;top:424px;width:270px;height:66px"> |
| <h3 style="font-size:13px">plan_query<span class="tag opt">opt-in</span></h3> |
| <p style="font-size:12px">JSON-скелет: tables · joins · filters · group_by · sort · limit</p> |
| </div> |
|
|
| <div class="node hot" id="gen" style="left:742px;top:510px;width:270px;height:184px"> |
| <span class="step">3</span> |
| <h3>generate_sql</h3> |
| <p class="path">codestral-latest · T=0</p> |
| <p>Structured JSON: <span class="mono" style="font-size:11px">{sql, rationale, tables_used, confidence}</span>. Шаблоны: cards | M-Schema (XiYan) | DAC (CHASE-SQL) + <b>P3.F hints</b> — 11 правил, gated db_id+phrase.</p> |
| </div> |
|
|
| <div class="node hot" id="val" style="left:1084px;top:510px;width:250px;height:184px"> |
| <span class="step">4</span> |
| <h3>validate · AST guard</h3> |
| <p class="path">execution/guards.py · sqlglot</p> |
| <p>SELECT-only · no DML/DDL в дереве · 1 statement · function denylist (pg_sleep, load_extension…) · denied tables · ATTACH / PRAGMA block.</p> |
| <div class="badges"><span class="bdg a">sqlglot</span></div> |
| </div> |
|
|
| <div class="node hot" id="exe" style="left:1406px;top:510px;width:270px;height:196px"> |
| <span class="step">5</span> |
| <h3>execute · read-only</h3> |
| <p class="path">execution/runner.py → db/connection.py</p> |
| <p>SQLite: URI <span class="mono" style="font-size:11px">mode=ro</span> + PRAGMA query_only + progress-deadline; Postgres: read-only транзакции + statement_timeout 30 s; <b>row cap 10 000</b>.</p> |
| <div class="badges"><span class="bdg a">3-layer safety</span><span class="bdg">SQLAlchemy</span></div> |
| </div> |
|
|
| <div class="node" id="rep" style="left:1042px;top:752px;width:250px;height:110px"> |
| <span class="step alt">×1</span> |
| <h3>repair_once</h3> |
| <p class="path">agent/nodes/repair_once.py</p> |
| <p style="font-size:12px"><b>Ровно 1 повтор</b> с error-context (guard repair_attempted): validate-fail / runtime-fail / empty (G) / critique-fail.</p> |
| </div> |
|
|
| |
| <div class="node dashed" id="crit" style="left:1406px;top:936px;width:270px;height:84px"> |
| <h3 style="font-size:13px">grounded_critique<span class="tag opt">opt-in</span></h3> |
| <p style="font-size:12px">Row-shape проверка результата → не более 1 retry.</p> |
| </div> |
|
|
| <div class="node hot" id="fmt" style="left:1080px;top:936px;width:260px;height:176px"> |
| <span class="step">6</span> |
| <h3>deterministic_format<span class="tag nollm">no LLM</span></h3> |
| <p class="path">render/picker.py · formats.py</p> |
| <p>Чистый Python, эвристики по shape результата: <b>Scalar · Sentence · Table · Line · Bar · Pie · Scatter</b>.</p> |
| <div class="badges"><span class="bdg">Plotly</span></div> |
| </div> |
|
|
| <div class="node hot" id="exp" style="left:748px;top:936px;width:260px;height:176px"> |
| <span class="step">7</span> |
| <h3>explain_trace</h3> |
| <p class="path">mistral-large-latest</p> |
| <p>NL-caption ≤ 2 предложений; финализация trace (model, tokens, latency, confidence по каждому узлу).</p> |
| </div> |
|
|
| <div class="node hot" id="ans" style="left:400px;top:936px;width:270px;height:176px"> |
| <span class="step">8</span> |
| <h3>Ответ · AskResponse</h3> |
| <p class="path">PipelineRunResult</p> |
| <p>answer + SQL (подсветка) + rationale + confidence + caption + полный trace. Error taxonomy: <span class="mono" style="font-size:10.5px">invalid_sql · execution_timeout · execution_failed · empty_result · low_confidence · repair_failed</span></p> |
| </div> |
|
|
| |
| <div class="node" id="proto" style="left:400px;top:1300px;width:290px;height:158px"> |
| <h3>LLMProvider Protocol</h3> |
| <p class="path">llm/providers/base.py · factory.py</p> |
| <p>PEP 544 runtime_checkable · <span class="mono" style="font-size:11px">build_provider(name)</span> — смена модели = env var · ProviderError taxonomy. Embed-протокол отдельно.</p> |
| </div> |
|
|
| <div class="node hot" id="cache" style="left:760px;top:1300px;width:290px;height:158px"> |
| <h3>Caching layer</h3> |
| <p class="path">llm/cache.py · diskcache</p> |
| <p>Ключ <span class="mono" style="font-size:10.5px">sha256(provider · model · system · prompt · T · max_tok)</span>: hit = 0 quota, 0 latency — основа <b>$0-бюджета</b>. В тестах — fake-провайдеры, CI без live API.</p> |
| <div class="badges"><span class="bdg a">diskcache</span></div> |
| </div> |
|
|
| <div class="group" id="provgroup" style="left:1120px;top:1268px;width:724px;height:330px"> |
| <span class="gt">7 provider-модулей + GraceKelly browser-мост · voting / residue-слои</span> |
| </div> |
| <div class="node pcard" id="p-mistral" style="left:1136px;top:1316px;width:160px;height:124px"> |
| <h3>Mistral<span class="tag pri">primary</span></h3> |
| <p>codestral-latest (SQL) · mistral-large (NL) · mistral-embed. La Plateforme free.</p> |
| </div> |
| <div class="node pcard" id="p-groq" style="left:1312px;top:1316px;width:160px;height:124px"> |
| <h3>Groq</h3> |
| <p>llama-3.3-70b · qwen3-32b · gpt-oss. TPM/TPD-bounded.</p> |
| </div> |
| <div class="node pcard" id="p-gh" style="left:1488px;top:1316px;width:160px;height:124px"> |
| <h3>GitHub Models</h3> |
| <p>gpt-4o-mini · auth по PAT · OpenAI-compatible SDK.</p> |
| </div> |
| <div class="node pcard" id="p-or" style="left:1664px;top:1316px;width:160px;height:124px"> |
| <h3>OpenRouter</h3> |
| <p>deepseek-v4-flash:free + 24 беспл. reasoning/code-моделей.</p> |
| </div> |
| <div class="node pcard" id="p-ollama" style="left:1136px;top:1456px;width:160px;height:124px"> |
| <h3>Ollama</h3> |
| <p>local · qwen2.5-coder:7b — offline-слот bakeoff.</p> |
| </div> |
| <div class="node pcard" id="p-pplx" style="left:1312px;top:1456px;width:160px;height:124px"> |
| <h3>Perplexity / helallao</h3> |
| <p>GPT-5.2 · Grok-4.1 · Claude-4.5 · Kimi-K2 (reasoning / Pro).</p> |
| </div> |
| <div class="node pcard" id="p-gk" style="left:1488px;top:1456px;width:160px;height:124px"> |
| <h3>GraceKelly</h3> |
| <p>browser-orchestrator → Sonnet 4.6 (eval-only мост).</p> |
| </div> |
| <div class="note" style="left:1664px;top:1456px;width:160px;height:124px"> |
| <b>$0 hard constraint</b> — free tiers + кэш + user-подписки; ротация аккаунтов запрещена. |
| </div> |
|
|
| |
| <div class="node" id="ds" style="left:48px;top:1704px;width:250px;height:158px"> |
| <h3>Dataset</h3> |
| <p class="path">eval/dataset.py</p> |
| <p>BIRD Mini-Dev loader · <span class="mono" style="font-size:11px">dev_split(seed=0, n)</span> — stable-prefix: n=50 ⊂ n=200 → кэш промптов переиспользуется.</p> |
| </div> |
|
|
| <div class="node" id="runner" style="left:372px;top:1704px;width:260px;height:158px"> |
| <h3>Ablation runner</h3> |
| <p class="path">eval/runner.py</p> |
| <p>Конфиги A · C · D · E · F (T-sweep 0.2–0.8) · G (B=BM25, N/I): A–D без repair, G + verify_retry_on_empty → <span class="mono" style="font-size:11px">eval/reports/*.json</span>.</p> |
| </div> |
|
|
| <div class="node" id="voting" style="left:706px;top:1704px;width:290px;height:194px"> |
| <h3>Voting / rescue scripts</h3> |
| <p class="path">scripts/run_*.py</p> |
| <p>groq_voting · sonnet (GraceKelly) · helallao · openrouter · critique_retry · selfcon (T-sweep) · wide_schema · ensemble_vote. Работают по <b>residue</b> (misses) поверх v_N.</p> |
| </div> |
|
|
| <div class="node" id="merge" style="left:1070px;top:1704px;width:250px;height:158px"> |
| <h3>Merge</h3> |
| <p class="path">scripts/merge_voting_rescues.py</p> |
| <p><span class="mono" style="font-size:11px">--reverify</span> — re-exec через safe_compare_pred · archive_sweep / rescore → merged baseline v<sub>N+1</sub>.</p> |
| </div> |
|
|
| <div class="node" id="metrics" style="left:1394px;top:1704px;width:200px;height:158px"> |
| <h3>Metrics</h3> |
| <p class="path">eval/metrics/</p> |
| <p style="font-size:12px">execution_accuracy — <b>BIRD-official set-equality</b> · safe_compare_pred (pred-fail → False) · schema_recall@k.</p> |
| </div> |
|
|
| <div class="node" id="audit" style="left:1640px;top:1704px;width:204px;height:194px"> |
| <h3>Audit gates</h3> |
| <p class="path">scripts/</p> |
| <p style="font-size:12px">audit_rescore — row-by-row re-execution · p3f_acceptance — 11 gates (req/forbidden columns по AST) · error_taxonomy buckets · refresh_baseline_summary.</p> |
| </div> |
|
|
| <div class="node hot" id="result" style="left:1070px;top:1944px;width:524px;height:92px"> |
| <h3>v31 baseline · <span class="numeric">94.0% EA (188/200) · 0 mismatches</span></h3> |
| <p style="font-size:12px">→ README headline · GET /eval/latest · HF redeploy. Lift trace: 47% (config A) → 94.0% (v31), 31 версия, каждая с negative/saturation evidence.</p> |
| </div> |
|
|
| |
| <div class="ribbon" style="left:48px;top:2104px;width:1784px;height:92px"> |
| <span class="rt">05 · Quality gates & CI/CD</span> |
| <span class="bdg">pytest · 370 green</span> |
| <span class="bdg">coverage 91%</span> |
| <span class="bdg">mypy --strict · 0 issues / 59 files</span> |
| <span class="bdg">ruff check + format</span> |
| <span class="bdg">GitHub Actions · Ubuntu · py3.13 · uv</span> |
| <span class="bdg">uv.lock pinned + requirements-guard</span> |
| <span class="bdg">fake-провайдеры — CI без live API</span> |
| <span class="bdg">.deploy_hf.py + Playwright E2E grep-gate</span> |
| <span class="bdg">Makefile</span> |
| <span class="bdg">docker-compose · postgres / langfuse profiles</span> |
| </div> |
|
|
| <svg id="wires" width="1880" height="2236"> |
| <defs> |
| <marker id="m-hot" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="7.5" markerHeight="7.5" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" fill="#9C5B38"/></marker> |
| <marker id="m-rep" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" fill="#9C5B38"/></marker> |
| <marker id="m-bus" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" fill="#9C5B38"/></marker> |
| <marker id="m-idx" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" fill="rgba(28,28,26,0.45)"/></marker> |
| <marker id="m-opt" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6.5" markerHeight="6.5" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" fill="rgba(28,28,26,0.45)"/></marker> |
| <marker id="m-eval" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" fill="rgba(28,28,26,0.55)"/></marker> |
| </defs> |
| </svg> |
|
|
| </div></div></div> |
|
|
| <footer> |
| Источник: <span class="mono">src/nl_sql/* · app/* · eval/* · scripts/*</span> @ <span class="mono">8e6c61d</span> · baseline v31 (2026-05-26) · схема сгенерирована 2026-06-06 · подробности: <span class="mono">docs/02_architecture_v2.md</span>, <span class="mono">docs/03_eval_methodology.md</span>, <span class="mono">docs/SESSION_HANDOFF.md</span> · печатная версия: <a href="data_flow.pdf" style="color:var(--accent-dark)">data_flow.pdf</a> · зум: кнопки справа внизу или клавиши <span class="mono">+ − 0</span> |
| </footer> |
|
|
| <script> |
| |
| |
| |
| const EDGES = [ |
| |
| {f:'src',fs:'r',t:'intro',ts:'l',k:'idx',lb:'engine (ro)'}, |
| {f:'intro',fs:'r',t:'chunk',ts:'l',k:'idx',lb:'TableInfo'}, |
| {f:'chunk',fs:'r',t:'idx',ts:'l',k:'idx',lb:'SchemaChunk'}, |
| {f:'idx',fs:'r',t:'chroma',ts:'l',k:'idx',lb:'upsert'}, |
| {f:'src',fs:'r',ff:.9,t:'fewshot',ts:'l',k:'idx',lb:'train split',la:.5,c:.2}, |
| {f:'fewshot',fs:'t',ff:.6,t:'idx',ts:'b',tf:.3,k:'idx',lb:'Q→SQL · 9 428 train',la:.5,dy:-4}, |
| {f:'idx',fs:'b',ff:.62,t:'embed',ts:'t',k:'idx',lb:'texts',la:.4}, |
| {f:'embed',fs:'r',t:'chroma',ts:'b',tf:.35,k:'idx',lb:'vectors 1024-d',la:.5}, |
| {f:'chroma',fs:'r',t:'fkg',ts:'l',k:'idx',lb:'fk_targets'}, |
| |
| {f:'chroma',fs:'b',ff:.55,t:'ctx',ts:'t',tf:.25,k:'hot',lb:'top-k=5 schema + k=3 few-shot (db_id)',la:.45,via:[[1493,412],[467,412]]}, |
| {f:'fkg',fs:'b',ff:.5,t:'ctx',ts:'t',tf:.72,k:'hot',lb:'FK BFS ≤1 hop · budget 12',la:.42,via:[[1756,400],[594,400]]}, |
| |
| {f:'user',fs:'b',t:'st',ts:'t',k:'hot'}, |
| {f:'st',fs:'r',ff:.35,t:'ctx',ts:'l',tf:.35,k:'hot',lb:'question · db_id · dialect',la:.45}, |
| {f:'api',fs:'r',ff:.3,t:'ctx',ts:'l',tf:.78,k:'hot',lb:'POST /ask',la:.4}, |
| {f:'ctx',fs:'r',ff:.82,t:'gen',ts:'l',tf:.82,k:'hot',lb:'ContextBundle → prompt',dy:30}, |
| {f:'gen',fs:'r',ff:.82,t:'val',ts:'l',tf:.82,k:'hot',lb:'sql · rationale · confidence',dy:36}, |
| {f:'val',fs:'r',ff:.82,t:'exe',ts:'l',tf:.82,k:'hot',lb:'AST-clean SQL',dy:30}, |
| {f:'exe',fs:'b',ff:.32,t:'fmt',ts:'t',tf:.6,k:'hot',lb:'QueryResult · rows ≤ 10 000',la:.6,via:[[1492,880],[1236,880]]}, |
| {f:'fmt',fs:'b',ff:.2,t:'exp',ts:'b',tf:.8,k:'hot',lb:'OutputFormat (1 из 7)',la:.5,c:.22}, |
| {f:'exp',fs:'l',t:'ans',ts:'r',k:'hot',lb:'caption'}, |
| {f:'ans',fs:'l',ff:.3,t:'st',ts:'r',tf:.72,k:'hot',lb:'answer + SQL + trace',la:.5}, |
| {f:'ans',fs:'l',ff:.72,t:'api',ts:'r',tf:.65,k:'hot',lb:'AskResponse',la:.45}, |
| |
| {f:'val',fs:'b',ff:.55,t:'rep',ts:'t',tf:.62,k:'rep',lb:'fail + error ctx',la:.5}, |
| {f:'rep',fs:'t',ff:.12,t:'val',ts:'b',tf:.12,k:'rep',lb:'SQL v2 · ×1',la:.5}, |
| {f:'exe',fs:'b',ff:.1,t:'rep',ts:'r',tf:.2,k:'rep',lb:'runtime / empty (G)',la:.45,c:.3}, |
| |
| {f:'ctx',fs:'t',ff:.6,t:'plan',ts:'l',k:'opt',la:.5,c:.4}, |
| {f:'plan',fs:'b',ff:.5,t:'gen',ts:'t',tf:.5,k:'opt',lb:'JSON skeleton',la:.5}, |
| {f:'exe',fs:'b',ff:.78,t:'crit',ts:'t',tf:.5,k:'opt',lb:'opt-in',la:.5}, |
| {f:'crit',fs:'l',t:'fmt',ts:'r',tf:.25,k:'opt',lb:'ok',la:.5}, |
| {f:'crit',fs:'t',ff:.3,t:'rep',ts:'b',tf:.7,k:'opt',lb:'fail ×1',la:.55,via:[[1487,906],[1247,906]]}, |
| |
| {f:'gen',fs:'b',ff:.5,t:'proto',ts:'l',tf:.5,k:'bus',lb:'LLM-вызовы: plan · generate · repair · explain',la:.22,via:[[877,716],[349,716],[349,1379]]}, |
| {f:'exp',fs:'b',ff:.5,t:'proto',ts:'l',tf:.5,k:'bus',via:[[878,1146],[349,1146],[349,1379]]}, |
| {f:'proto',fs:'r',t:'cache',ts:'l',k:'bus',lb:'wrap'}, |
| {f:'cache',fs:'b',ff:.9,t:'provgroup',ts:'l',tf:.8,k:'bus',lb:'HTTP только при cache-miss',la:.55,dy:12,c:.3}, |
| |
| {f:'ds',fs:'r',t:'runner',ts:'l',k:'eval',lb:'n=200 · seed=0',dy:-8}, |
| {f:'runner',fs:'r',t:'voting',ts:'l',k:'eval',lb:'residue v_N'}, |
| {f:'voting',fs:'r',t:'merge',ts:'l',k:'eval',lb:'verified rescues'}, |
| {f:'merge',fs:'r',t:'metrics',ts:'l',k:'eval',lb:'re-score'}, |
| {f:'metrics',fs:'r',t:'audit',ts:'l',k:'eval'}, |
| {f:'audit',fs:'b',ff:.5,t:'result',ts:'r',tf:.5,k:'eval',lb:'0 mismatches',la:.5,c:.4}, |
| {f:'result',fs:'l',ff:.5,t:'voting',ts:'b',tf:.6,k:'eval',lb:'residue-loop ×31',la:.5,c:.4}, |
| {f:'runner',fs:'t',ff:.72,t:'ans',ts:'b',tf:.82,k:'eval',lb:'run_pipeline() на каждый qid',la:.35,via:[[559,1676],[725,1676],[725,1150]]}, |
| {f:'voting',fs:'t',ff:.5,t:'cache',ts:'b',tf:.5,k:'eval',lb:'те же providers',la:.5}, |
| |
| {f:'hf',fs:'t',ff:.5,t:'api',ts:'b',tf:.5,k:'opt',lb:'Docker',la:.5}, |
| ]; |
| |
| const canvas=document.getElementById('canvas'); |
| const svg=document.getElementById('wires'); |
| const NS='http://www.w3.org/2000/svg'; |
| |
| function anchor(id,side,frac){ |
| const el=document.getElementById(id); |
| const x=el.offsetLeft,y=el.offsetTop,w=el.offsetWidth,h=el.offsetHeight; |
| const f=(frac==null?0.5:frac); |
| switch(side){ |
| case 'l':return{x:x, y:y+h*f, nx:-1,ny:0}; |
| case 'r':return{x:x+w, y:y+h*f, nx:1, ny:0}; |
| case 't':return{x:x+w*f, y:y, nx:0, ny:-1}; |
| case 'b':return{x:x+w*f, y:y+h, nx:0, ny:1}; |
| } |
| } |
| |
| function buildPath(e){ |
| const a=anchor(e.f,e.fs,e.ff), b=anchor(e.t,e.ts,e.tf); |
| if(e.via){ |
| let d=`M ${a.x},${a.y}`; |
| e.via.forEach(p=>{d+=` L ${p[0]},${p[1]}`;}); |
| d+=` L ${b.x},${b.y}`; |
| return d; |
| } |
| const dist=Math.hypot(b.x-a.x,b.y-a.y); |
| const k=dist*(e.c!=null?e.c:0.38); |
| const c1x=a.x+a.nx*k, c1y=a.y+a.ny*k; |
| const c2x=b.x+b.nx*k, c2y=b.y+b.ny*k; |
| return `M ${a.x},${a.y} C ${c1x},${c1y} ${c2x},${c2y} ${b.x},${b.y}`; |
| } |
| |
| EDGES.forEach(e=>{ |
| const g=document.createElementNS(NS,'g'); |
| g.setAttribute('class','edge'); |
| g.dataset.f=e.f; g.dataset.t=e.t; |
| const p=document.createElementNS(NS,'path'); |
| p.setAttribute('d',buildPath(e)); |
| p.setAttribute('class','l '+e.k); |
| p.setAttribute('marker-end',`url(#m-${e.k})`); |
| if(e.via){p.setAttribute('stroke-linejoin','round');} |
| g.appendChild(p); |
| if(e.lb){ |
| const len=p.getTotalLength(); |
| const pt=p.getPointAtLength(len*(e.la!=null?e.la:0.5)); |
| const tx=document.createElementNS(NS,'text'); |
| tx.setAttribute('x',pt.x+(e.dx||0)); tx.setAttribute('y',pt.y+4+(e.dy||0)); |
| tx.setAttribute('text-anchor','middle'); |
| tx.setAttribute('class',(e.k==='hot'||e.k==='rep'||e.k==='bus')?'lt-hot':'lt-mut'); |
| tx.textContent=e.lb; |
| g.appendChild(tx); |
| svg.appendChild(g); |
| const bb=tx.getBBox(); |
| const r=document.createElementNS(NS,'rect'); |
| r.setAttribute('x',bb.x-5);r.setAttribute('y',bb.y-2); |
| r.setAttribute('width',bb.width+10);r.setAttribute('height',bb.height+4); |
| r.setAttribute('rx',3);r.setAttribute('class','lbg'); |
| g.insertBefore(r,tx); |
| } else { |
| svg.appendChild(g); |
| } |
| }); |
| |
| |
| document.querySelectorAll('.node,.group').forEach(n=>{ |
| n.addEventListener('mouseenter',()=>{ |
| canvas.classList.add('focus'); |
| n.classList.add('on'); |
| document.querySelectorAll('.edge').forEach(ed=>{ |
| if(ed.dataset.f===n.id||ed.dataset.t===n.id){ |
| ed.classList.add('on'); |
| const other=document.getElementById(ed.dataset.f===n.id?ed.dataset.t:ed.dataset.f); |
| if(other)other.classList.add('on'); |
| } |
| }); |
| }); |
| n.addEventListener('mouseleave',()=>{ |
| canvas.classList.remove('focus'); |
| document.querySelectorAll('.on').forEach(x=>x.classList.remove('on')); |
| }); |
| }); |
| |
| |
| let userZoom=null; |
| function fitScale(){ |
| const vw=document.getElementById('viewport').clientWidth; |
| return Math.min(1,(vw-16)/1880); |
| } |
| function rescale(){ |
| const vp=document.getElementById('viewport'); |
| const s=userZoom!=null?userZoom:fitScale(); |
| const sc=document.getElementById('scaler'); |
| sc.style.transform=`scale(${s})`; |
| sc.style.width='1880px'; |
| sc.style.height=(2236*s)+'px'; |
| vp.style.overflowX=(1880*s>vp.clientWidth)?'auto':'hidden'; |
| document.getElementById('zval').textContent=Math.round(s*100)+'%'; |
| } |
| document.querySelectorAll('#zoombar button').forEach(btn=>{ |
| btn.addEventListener('click',()=>{ |
| const cur=userZoom!=null?userZoom:fitScale(); |
| if(btn.dataset.z==='in') userZoom=Math.min(1.6,Math.round((cur+0.15)*100)/100); |
| if(btn.dataset.z==='out') userZoom=Math.max(0.3,Math.round((cur-0.15)*100)/100); |
| if(btn.dataset.z==='fit') userZoom=null; |
| rescale(); |
| }); |
| }); |
| window.addEventListener('keydown',e=>{ |
| if(e.target.tagName==='INPUT'||e.ctrlKey||e.metaKey)return; |
| if(e.key==='+'||e.key==='='){userZoom=Math.min(1.6,(userZoom!=null?userZoom:fitScale())+0.15);rescale();} |
| if(e.key==='-'){userZoom=Math.max(0.3,(userZoom!=null?userZoom:fitScale())-0.15);rescale();} |
| if(e.key==='0'){userZoom=null;rescale();} |
| }); |
| window.addEventListener('resize',rescale); |
| rescale(); |
| |
| |
| if(window.innerWidth<900){ |
| document.getElementById('mobile-stub').style.display='flex'; |
| document.body.style.overflow='hidden'; |
| } |
| </script> |
| </body> |
| </html> |
|
|