scaffold: kind+Tilt cluster, api/ui/docs stubs, green at nvi.local.ar
This commit is contained in:
435
docs/index.html
Normal file
435
docs/index.html
Normal file
@@ -0,0 +1,435 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>nvi — analytics agent</title>
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap');
|
||||
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
background: #0a0e17;
|
||||
color: #e8eaf0;
|
||||
font-family: 'Inter', sans-serif;
|
||||
line-height: 1.6;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
header {
|
||||
padding: 16px 24px;
|
||||
border-bottom: 1px solid #1e2a4a;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 3px;
|
||||
color: #0066ff;
|
||||
}
|
||||
|
||||
header .subtitle {
|
||||
font-size: 13px;
|
||||
color: #4a5568;
|
||||
letter-spacing: 1px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.layout {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
nav {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 200px;
|
||||
flex-shrink: 0;
|
||||
background: #121829;
|
||||
border-right: 1px solid #1e2a4a;
|
||||
padding: 8px 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
nav a {
|
||||
padding: 10px 20px;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 12px;
|
||||
color: #8892a8;
|
||||
text-decoration: none;
|
||||
border-left: 2px solid transparent;
|
||||
transition: all 0.15s;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
nav a:hover { color: #e8eaf0; background: #1a2340; }
|
||||
nav a.active { color: #0066ff; border-left-color: #0066ff; background: #0d1a33; }
|
||||
|
||||
main {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
padding: 32px 48px;
|
||||
}
|
||||
|
||||
.section {
|
||||
display: none;
|
||||
animation: fadeIn 0.2s ease;
|
||||
}
|
||||
|
||||
.section.active { display: block; }
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
.section h2 {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
color: #8892a8;
|
||||
margin-bottom: 8px;
|
||||
letter-spacing: 1px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.section > p.lede {
|
||||
font-size: 13px;
|
||||
color: #4a5568;
|
||||
margin-bottom: 24px;
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
.prose { max-width: 820px; }
|
||||
|
||||
.prose p {
|
||||
font-size: 14px;
|
||||
color: #b4bccf;
|
||||
margin-bottom: 14px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.prose p b { color: #e8eaf0; font-weight: 600; }
|
||||
|
||||
.prose ul {
|
||||
margin: 8px 0 16px 20px;
|
||||
font-size: 14px;
|
||||
color: #b4bccf;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.prose ul li { margin-bottom: 8px; }
|
||||
|
||||
.prose h3 {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: #e8eaf0;
|
||||
letter-spacing: 1px;
|
||||
margin: 32px 0 10px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.prose code, pre code {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 12px;
|
||||
color: #7ab0ff;
|
||||
background: #121829;
|
||||
padding: 1px 5px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
pre {
|
||||
background: #121829;
|
||||
border: 1px solid #1e2a4a;
|
||||
padding: 16px;
|
||||
overflow: auto;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
pre code {
|
||||
background: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.diagram {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
margin: 16px 0;
|
||||
background: #121829;
|
||||
border: 1px solid #1e2a4a;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
dl {
|
||||
display: grid;
|
||||
grid-template-columns: max-content 1fr;
|
||||
gap: 10px 24px;
|
||||
margin: 16px 0;
|
||||
max-width: 820px;
|
||||
}
|
||||
|
||||
dt {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
color: #0066ff;
|
||||
font-size: 13px;
|
||||
padding-top: 2px;
|
||||
}
|
||||
|
||||
dd {
|
||||
font-size: 14px;
|
||||
color: #b4bccf;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.menu-toggle {
|
||||
display: none;
|
||||
background: transparent;
|
||||
border: 1px solid #1e2a4a;
|
||||
color: #8892a8;
|
||||
padding: 6px 10px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
line-height: 1;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.menu-toggle:hover { background: #1a2340; }
|
||||
|
||||
.nav-backdrop {
|
||||
display: none;
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.layout.nav-open .nav-backdrop { display: block; }
|
||||
|
||||
@media (max-width: 720px) {
|
||||
header { padding: 10px 12px; gap: 8px; }
|
||||
header h1 { font-size: 16px; letter-spacing: 1px; }
|
||||
header .subtitle { display: none; }
|
||||
.menu-toggle { display: inline-block; }
|
||||
|
||||
.layout { position: relative; }
|
||||
nav {
|
||||
position: absolute;
|
||||
left: 0; top: 0; bottom: 0;
|
||||
width: 200px;
|
||||
z-index: 20;
|
||||
transform: translateX(-100%);
|
||||
transition: transform 0.2s ease;
|
||||
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
.layout.nav-open nav { transform: translateX(0); }
|
||||
|
||||
main { padding: 16px; }
|
||||
.section h2 { font-size: 13px; }
|
||||
.prose p, .prose ul { font-size: 13px; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<header>
|
||||
<h1>NVI</h1>
|
||||
<span class="subtitle">analytics agent — BIRD financial</span>
|
||||
<button class="menu-toggle" onclick="toggleNav()" aria-label="Toggle navigation">☰</button>
|
||||
</header>
|
||||
|
||||
<div class="layout">
|
||||
<div class="nav-backdrop" onclick="toggleNav()"></div>
|
||||
|
||||
<nav>
|
||||
<a class="active" onclick="show('overview')">Overview</a>
|
||||
<a onclick="show('architecture')">Architecture</a>
|
||||
<a onclick="show('plan')">Plan</a>
|
||||
<a onclick="show('analyses')">Analyses</a>
|
||||
<a onclick="show('tools')">Tools</a>
|
||||
<a onclick="show('runtime')">Runtime</a>
|
||||
<a onclick="show('run')">Run it</a>
|
||||
<a onclick="show('decisions')">Decisions</a>
|
||||
<a onclick="show('evals')">Evals</a>
|
||||
</nav>
|
||||
|
||||
<main>
|
||||
|
||||
<section id="overview" class="section active">
|
||||
<h2>Overview</h2>
|
||||
<p class="lede">A text-to-SQL analytics agent over a real warehouse — structured so the agent layer is legible, not buried in a framework.</p>
|
||||
<div class="prose">
|
||||
<p>
|
||||
nvi answers business questions over the BIRD <code>financial</code>
|
||||
warehouse (PKDD'99 Czech bank: accounts, transactions, loans, cards,
|
||||
clients, districts, orders, dispositions). It does this by composing
|
||||
a small set of <b>Analyses</b> — each its own mini-agent — orchestrated
|
||||
through a langgraph wiring layer with full Langfuse observability.
|
||||
</p>
|
||||
<p>
|
||||
The shape mirrors the JD's three building blocks
|
||||
(<i>text-to-SQL, reasoning agents, planners</i>) as three distinct
|
||||
layers rather than one omnibus prompt. That gives every part a name
|
||||
and a tight contract, and lets each Analysis pick the simplest
|
||||
reasoning pattern (CoT, ReAct, plan-and-execute) for what it does
|
||||
instead of forcing one over everything.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="architecture" class="section">
|
||||
<h2>Architecture</h2>
|
||||
<p class="lede">Three layers — Plan composes Analyses; Analyses are mini-agents; Tools are deterministic.</p>
|
||||
<object data="graphs/architecture.svg" type="image/svg+xml" class="diagram"></object>
|
||||
<dl>
|
||||
<dt>Plan (L3)</dt>
|
||||
<dd>One LLM call that turns a question into an ordered set of Analyses with intended interpretation. Not agentic by itself — produces a structured artifact the runtime executes.</dd>
|
||||
<dt>Analyses (L2)</dt>
|
||||
<dd>Each Analysis is a self-contained mini-agent. It picks the simplest reasoning pattern it needs — CoT for one-shot interpretive moves, ReAct for ones that iterate over Tool results, plan-and-execute for compound ones. The Analysis owns its loop.</dd>
|
||||
<dt>Tools (L1)</dt>
|
||||
<dd>Deterministic primitives — <code>text_to_sql</code>, <code>execute_sql</code>, <code>retrieve_schema</code>, <code>retrieve_metric_definition</code>, <code>python_sandbox</code>. No LLM reasoning inside. Called from inside Analyses.</dd>
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
<section id="plan" class="section">
|
||||
<h2>Plan layer</h2>
|
||||
<p class="lede">L3 — one LLM call that decides which Analyses to run.</p>
|
||||
<div class="prose">
|
||||
<p>
|
||||
The planner gets the user question, the metric catalog, a schema
|
||||
overview, and the catalog of Analyses. It returns a structured plan:
|
||||
an ordered list of Analysis invocations with arguments and an intended
|
||||
interpretation. It does not call tools or query data itself — it's a
|
||||
composition step.
|
||||
</p>
|
||||
<p><i>To be expanded as the planner is built out.</i></p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="analyses" class="section">
|
||||
<h2>Analyses layer</h2>
|
||||
<p class="lede">L2 — each Analysis is its own mini-agent.</p>
|
||||
<div class="prose">
|
||||
<p>
|
||||
Analyses are the agent layer. Each one wraps a small piece of analytical
|
||||
intent (<i>compare two periods</i>, <i>drill down by dimension</i>,
|
||||
<i>find outliers</i>) and decides how to satisfy it — sometimes one
|
||||
LLM call is enough, sometimes a ReAct loop over Tool results is the
|
||||
right fit. The Analysis base class only enforces a uniform external
|
||||
contract (<code>run(state) → Finding</code>); the internal pattern is
|
||||
a per-Analysis decision.
|
||||
</p>
|
||||
<p><i>Per-Analysis pattern table coming as each Analysis is implemented.</i></p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="tools" class="section">
|
||||
<h2>Tools layer</h2>
|
||||
<p class="lede">L1 — deterministic primitives.</p>
|
||||
<div class="prose">
|
||||
<p>
|
||||
Tools have no LLM reasoning inside them. <code>text_to_sql</code> is
|
||||
the one exception that uses an LLM, but tightly: schema-RAG context,
|
||||
sqlglot validation, retry on parse/runtime errors. <code>execute_sql</code>
|
||||
runs against Postgres read-only with a row cap and timeout.
|
||||
<code>retrieve_schema</code> and <code>retrieve_metric_definition</code>
|
||||
power the semantic layer. <code>python_sandbox</code> handles stats
|
||||
work that's awkward in SQL.
|
||||
</p>
|
||||
<p><i>Per-Tool documentation coming as each Tool is implemented.</i></p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="runtime" class="section">
|
||||
<h2>Runtime</h2>
|
||||
<p class="lede">langgraph wires the graph; nvi's domain code stays legible on its own.</p>
|
||||
<div class="prose">
|
||||
<p>
|
||||
The orchestration is built on <b>langgraph</b> — it's the right tool
|
||||
for the graph wiring, parallel execution, and streaming state we need,
|
||||
and reaching for a custom DAG runner here would be reinventing the
|
||||
wheel.
|
||||
</p>
|
||||
<p>
|
||||
The intent is the opposite, though: nvi's domain code (Plan, Analyses,
|
||||
Tools) presents a clean surface that's readable on its own terms.
|
||||
Understanding what nvi does should not require first learning
|
||||
langgraph's API. The framework wires nodes; nvi's code defines what
|
||||
the nodes <i>are</i>, and that's where the interesting reading is.
|
||||
</p>
|
||||
<p>
|
||||
Concretely: <code>api/runtime/</code> contains the langgraph builder
|
||||
and event tap. <code>api/plan/</code>, <code>api/analyses/</code>, and
|
||||
<code>api/tools/</code> contain the domain code — and those files
|
||||
don't expose langgraph types in their public interfaces.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="run" class="section">
|
||||
<h2>Run it locally</h2>
|
||||
<p class="lede">Prerequisites: lng Langfuse cluster reachable, soleprint-ui framework propagated by spr, <code>.env</code> with Anthropic + Langfuse keys.</p>
|
||||
<pre><code>make install # uv sync
|
||||
make kind # create kind-nvi cluster
|
||||
make tilt-up # bring up postgres, api, ui, docs, gateway
|
||||
make seed # load BIRD financial into postgres
|
||||
|
||||
open http://nvi.local.ar</code></pre>
|
||||
<div class="prose">
|
||||
<p>
|
||||
<code>*.local.ar</code> resolves via dnsmasq; the system Caddy at
|
||||
<code>~/wdir/ppl/local/Caddyfile</code> proxies <code>nvi.local.ar</code>
|
||||
to the kind NodePort. Reload after first install: <code>sudo systemctl reload caddy</code>.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="decisions" class="section">
|
||||
<h2>Architecture decisions</h2>
|
||||
<div class="prose">
|
||||
<ul>
|
||||
<li><b>Three layers, not one omnibus prompt.</b> Plan composes Analyses; each Analysis picks its own reasoning pattern (CoT / ReAct / plan-and-execute); Tools are deterministic. Borrowing elements from agent frameworks without committing the whole system to one.</li>
|
||||
<li><b>langgraph for orchestration, nvi for domain.</b> Framework owns graph wiring and streaming; nvi's <code>plan/</code>, <code>analyses/</code>, <code>tools/</code> own the interesting code and are readable without reading langgraph.</li>
|
||||
<li><b>BIRD <code>financial</code> as the warehouse.</b> Fits Nivii's stated Finance vertical; ships with golden SQL pairs so the eval set is free and verifiable.</li>
|
||||
<li><b>Langfuse hosted independently</b> by the <code>lng</code> project; reached over WireGuard. Same env var works in dev and any future deploy.</li>
|
||||
<li><b>Postgres dedicated to nvi.</b> Persisted via <code>.data/postgres</code> hostPath mount so the BIRD load survives cluster restarts.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="evals" class="section">
|
||||
<h2>Evals</h2>
|
||||
<p class="lede">BIRD <code>financial</code> dev set, run by <code>evals/run_evals.py</code>, results pushed to Langfuse tagged <code>eval</code>.</p>
|
||||
<div class="prose">
|
||||
<p><i>To be populated as the agent stack lands.</i></p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function show(id) {
|
||||
document.querySelectorAll('nav a').forEach(a => a.classList.remove('active'));
|
||||
document.querySelectorAll('.section').forEach(s => s.classList.remove('active'));
|
||||
event.target.classList.add('active');
|
||||
document.getElementById(id).classList.add('active');
|
||||
if (window.innerWidth <= 720) document.querySelector('.layout').classList.remove('nav-open');
|
||||
}
|
||||
function toggleNav() {
|
||||
document.querySelector('.layout').classList.toggle('nav-open');
|
||||
}
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user