nl-sql / docs /data_flow.html
liovina's picture
Deploy NL_SQL HEAD to HF Space
b907ba4 verified
Raw
History Blame Contribute Delete
37.8 kB
<!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 ---------- */
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}
/* ---------- canvas scaling ---------- */
#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}
/* ---------- zones ---------- */
.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}
/* ---------- nodes ---------- */
.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 chips on the hot path */
.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}
/* provider group */
.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);
}
/* CI ribbon */
.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}
/* ---------- edges ---------- */
#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 */
#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}
/* zoom bar */
#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">
<!-- ================= zones ================= -->
<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 &amp; audit<small>ablation → voting → merge → re-score → audit · residue-цикл ×31 (47% → 94.0%)</small></div>
<!-- ================= zone A: offline indexing ================= -->
<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>
<!-- ================= zone B: clients column ================= -->
<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>
<!-- ================= zone B: LangGraph row 1 ================= -->
<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>
<!-- ================= zone B: LangGraph row 2 ================= -->
<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>
<!-- ================= zone C: providers ================= -->
<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>
<!-- ================= zone D: eval ================= -->
<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>
<!-- ================= zone E: CI ribbon ================= -->
<div class="ribbon" style="left:48px;top:2104px;width:1784px;height:92px">
<span class="rt">05 · Quality gates &amp; 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>
/* ---------- edge definitions ---------- */
/* f/t: node id; fs/ts: side l|r|t|b; ff/tf: fraction along side (0..1, default .5);
k: kind; lb: label; la: label position 0..1; via: waypoints; c: curvature override */
const EDGES = [
/* zone A: offline indexing */
{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'},
/* query-time retrieval */
{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]]},
/* hot path */
{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},
/* repair loops */
{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},
/* opt-in */
{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]]},
/* LLM bus */
{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},
/* eval loop */
{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},
/* hosting */
{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);
}
});
/* hover focus: highlight a node's edges + neighbours */
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'));
});
});
/* ---------- scaling + zoom ---------- */
let userZoom=null; /* null = fit to viewport */
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();
/* mobile stub */
if(window.innerWidth<900){
document.getElementById('mobile-stub').style.display='flex';
document.body.style.overflow='hidden';
}
</script>
</body>
</html>