scaffold: kind+Tilt cluster, api/ui/docs stubs, green at nvi.local.ar

This commit is contained in:
2026-06-03 00:33:51 -03:00
commit 131f4d9b86
39 changed files with 3571 additions and 0 deletions

7
docs/graphs/README.md Normal file
View File

@@ -0,0 +1,7 @@
# Graphviz sources
Each `.dot` file in this directory is rendered to `.svg` by the docs build
(`make docs-graphs`). The HTML references the SVGs via `<object>`.
Convention follows eth/unt/mpr: dark background, JetBrains Mono labels,
accent blue for cluster titles, dim grey for edges.

View File

@@ -0,0 +1,65 @@
digraph nvi {
rankdir=TB;
bgcolor="#161b22";
fontname="JetBrains Mono";
fontcolor="#c9d1d9";
node [
shape=box,
style="rounded,filled",
fillcolor="#21262d",
color="#30363d",
fontname="JetBrains Mono",
fontcolor="#c9d1d9",
fontsize=11,
];
edge [color="#8b949e", fontname="JetBrains Mono", fontcolor="#8b949e", fontsize=10];
user [label="user question", fillcolor="#1f6feb", fontcolor="#ffffff"];
subgraph cluster_l3 {
label="L3 — Plan";
color="#30363d";
fontcolor="#58a6ff";
fontsize=11;
plan [label="planner\n(1 LLM call)"];
}
subgraph cluster_l2 {
label="L2 — Analyses (mini-agents)";
color="#30363d";
fontcolor="#58a6ff";
fontsize=11;
compare [label="compare_periods\n(CoT)"];
drill [label="drill_down\n(ReAct)"];
outl [label="find_outliers\n(CoT)"];
corr [label="correlate\n(plan-and-execute)"];
}
subgraph cluster_l1 {
label="L1 — Tools (deterministic)";
color="#30363d";
fontcolor="#58a6ff";
fontsize=11;
t2s [label="text_to_sql"];
exec [label="execute_sql"];
schema [label="retrieve_schema"];
py [label="python_sandbox"];
}
warehouse [label="Postgres\nBIRD financial", fillcolor="#388bfd33"];
langfuse [label="Langfuse (lng, WG)", fillcolor="#bf4b8a33", fontcolor="#ffffff"];
synth [label="synthesize\nfindings → answer"];
user -> plan;
plan -> compare; plan -> drill; plan -> outl; plan -> corr;
compare -> t2s; drill -> t2s; outl -> t2s; corr -> t2s;
drill -> schema [style=dashed];
corr -> py [style=dashed];
t2s -> exec;
exec -> warehouse;
compare -> synth; drill -> synth; outl -> synth; corr -> synth;
plan -> langfuse [style=dotted, label="spans"];
compare -> langfuse [style=dotted];
exec -> langfuse [style=dotted];
}

View File

@@ -0,0 +1,234 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Generated by graphviz version 14.1.2 (0)
-->
<!-- Title: nvi Pages: 1 -->
<svg width="562pt" height="487pt"
viewBox="0.00 0.00 562.00 487.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 482.5)">
<title>nvi</title>
<polygon fill="#161b22" stroke="none" points="-4,4 -4,-482.5 558.25,-482.5 558.25,4 -4,4"/>
<g id="clust1" class="cluster">
<title>cluster_l3</title>
<polygon fill="#161b22" stroke="#30363d" points="258,-336.25 258,-415.5 348,-415.5 348,-336.25 258,-336.25"/>
<text xml:space="preserve" text-anchor="middle" x="303" y="-401.05" font-family="JetBrains Mono" font-size="11.00" fill="#58a6ff">L3 — Plan</text>
</g>
<g id="clust2" class="cluster">
<title>cluster_l2</title>
<polygon fill="#161b22" stroke="#30363d" points="52,-238 52,-317.25 490,-317.25 490,-238 52,-238"/>
<text xml:space="preserve" text-anchor="middle" x="271" y="-302.8" font-family="JetBrains Mono" font-size="11.00" fill="#58a6ff">L2 — Analyses (mini&#45;agents)</text>
</g>
<g id="clust3" class="cluster">
<title>cluster_l1</title>
<polygon fill="#161b22" stroke="#30363d" points="8,-68.5 8,-219 334,-219 334,-68.5 8,-68.5"/>
<text xml:space="preserve" text-anchor="middle" x="171" y="-204.55" font-family="JetBrains Mono" font-size="11.00" fill="#58a6ff">L1 — Tools (deterministic)</text>
</g>
<!-- user -->
<g id="node1" class="node">
<title>user</title>
<path fill="#1f6feb" stroke="#30363d" d="M334.25,-478.5C334.25,-478.5 271.75,-478.5 271.75,-478.5 265.75,-478.5 259.75,-472.5 259.75,-466.5 259.75,-466.5 259.75,-454.5 259.75,-454.5 259.75,-448.5 265.75,-442.5 271.75,-442.5 271.75,-442.5 334.25,-442.5 334.25,-442.5 340.25,-442.5 346.25,-448.5 346.25,-454.5 346.25,-454.5 346.25,-466.5 346.25,-466.5 346.25,-472.5 340.25,-478.5 334.25,-478.5"/>
<text xml:space="preserve" text-anchor="middle" x="303" y="-457.93" font-family="JetBrains Mono" font-size="11.00" fill="#ffffff">user question</text>
</g>
<!-- plan -->
<g id="node2" class="node">
<title>plan</title>
<path fill="#21262d" stroke="#30363d" d="M327.5,-383.75C327.5,-383.75 278.5,-383.75 278.5,-383.75 272.5,-383.75 266.5,-377.75 266.5,-371.75 266.5,-371.75 266.5,-356.25 266.5,-356.25 266.5,-350.25 272.5,-344.25 278.5,-344.25 278.5,-344.25 327.5,-344.25 327.5,-344.25 333.5,-344.25 339.5,-350.25 339.5,-356.25 339.5,-356.25 339.5,-371.75 339.5,-371.75 339.5,-377.75 333.5,-383.75 327.5,-383.75"/>
<text xml:space="preserve" text-anchor="middle" x="303" y="-369.3" font-family="JetBrains Mono" font-size="11.00" fill="#c9d1d9">planner</text>
<text xml:space="preserve" text-anchor="middle" x="303" y="-353.55" font-family="JetBrains Mono" font-size="11.00" fill="#c9d1d9">(1 LLM call)</text>
</g>
<!-- user&#45;&gt;plan -->
<g id="edge1" class="edge">
<title>user&#45;&gt;plan</title>
<path fill="none" stroke="#8b949e" d="M303,-442.17C303,-429.09 303,-410.82 303,-395.34"/>
<polygon fill="#8b949e" stroke="#8b949e" points="306.5,-395.69 303,-385.69 299.5,-395.69 306.5,-395.69"/>
</g>
<!-- compare -->
<g id="node3" class="node">
<title>compare</title>
<path fill="#21262d" stroke="#30363d" d="M470,-285.5C470,-285.5 388,-285.5 388,-285.5 382,-285.5 376,-279.5 376,-273.5 376,-273.5 376,-258 376,-258 376,-252 382,-246 388,-246 388,-246 470,-246 470,-246 476,-246 482,-252 482,-258 482,-258 482,-273.5 482,-273.5 482,-279.5 476,-285.5 470,-285.5"/>
<text xml:space="preserve" text-anchor="middle" x="429" y="-271.05" font-family="JetBrains Mono" font-size="11.00" fill="#c9d1d9">compare_periods</text>
<text xml:space="preserve" text-anchor="middle" x="429" y="-255.3" font-family="JetBrains Mono" font-size="11.00" fill="#c9d1d9">(CoT)</text>
</g>
<!-- plan&#45;&gt;compare -->
<g id="edge2" class="edge">
<title>plan&#45;&gt;compare</title>
<path fill="none" stroke="#8b949e" d="M330.68,-343.78C342.05,-335.74 355.28,-326.2 367,-317.25 377.03,-309.59 387.76,-301 397.43,-293.11"/>
<polygon fill="#8b949e" stroke="#8b949e" points="399.64,-295.82 405.14,-286.77 395.19,-290.41 399.64,-295.82"/>
</g>
<!-- drill -->
<g id="node4" class="node">
<title>drill</title>
<path fill="#21262d" stroke="#30363d" d="M118.38,-285.5C118.38,-285.5 71.62,-285.5 71.62,-285.5 65.62,-285.5 59.62,-279.5 59.62,-273.5 59.62,-273.5 59.62,-258 59.62,-258 59.62,-252 65.62,-246 71.62,-246 71.62,-246 118.38,-246 118.38,-246 124.38,-246 130.38,-252 130.38,-258 130.38,-258 130.38,-273.5 130.38,-273.5 130.38,-279.5 124.38,-285.5 118.38,-285.5"/>
<text xml:space="preserve" text-anchor="middle" x="95" y="-271.05" font-family="JetBrains Mono" font-size="11.00" fill="#c9d1d9">drill_down</text>
<text xml:space="preserve" text-anchor="middle" x="95" y="-255.3" font-family="JetBrains Mono" font-size="11.00" fill="#c9d1d9">(ReAct)</text>
</g>
<!-- plan&#45;&gt;drill -->
<g id="edge3" class="edge">
<title>plan&#45;&gt;drill</title>
<path fill="none" stroke="#8b949e" d="M266.28,-358.73C231.58,-353.36 179.14,-341.69 140,-317.25 130.49,-311.31 121.82,-302.78 114.68,-294.41"/>
<polygon fill="#8b949e" stroke="#8b949e" points="117.49,-292.33 108.51,-286.71 112.03,-296.71 117.49,-292.33"/>
</g>
<!-- outl -->
<g id="node5" class="node">
<title>outl</title>
<path fill="#21262d" stroke="#30363d" d="M217.25,-285.5C217.25,-285.5 160.75,-285.5 160.75,-285.5 154.75,-285.5 148.75,-279.5 148.75,-273.5 148.75,-273.5 148.75,-258 148.75,-258 148.75,-252 154.75,-246 160.75,-246 160.75,-246 217.25,-246 217.25,-246 223.25,-246 229.25,-252 229.25,-258 229.25,-258 229.25,-273.5 229.25,-273.5 229.25,-279.5 223.25,-285.5 217.25,-285.5"/>
<text xml:space="preserve" text-anchor="middle" x="189" y="-271.05" font-family="JetBrains Mono" font-size="11.00" fill="#c9d1d9">find_outliers</text>
<text xml:space="preserve" text-anchor="middle" x="189" y="-255.3" font-family="JetBrains Mono" font-size="11.00" fill="#c9d1d9">(CoT)</text>
</g>
<!-- plan&#45;&gt;outl -->
<g id="edge4" class="edge">
<title>plan&#45;&gt;outl</title>
<path fill="none" stroke="#8b949e" d="M274.18,-343.95C262.89,-336.09 250.02,-326.64 239,-317.25 230.58,-310.08 221.94,-301.75 214.25,-293.94"/>
<polygon fill="#8b949e" stroke="#8b949e" points="216.86,-291.6 207.39,-286.84 211.82,-296.46 216.86,-291.6"/>
</g>
<!-- corr -->
<g id="node6" class="node">
<title>corr</title>
<path fill="#21262d" stroke="#30363d" d="M346.25,-285.5C346.25,-285.5 259.75,-285.5 259.75,-285.5 253.75,-285.5 247.75,-279.5 247.75,-273.5 247.75,-273.5 247.75,-258 247.75,-258 247.75,-252 253.75,-246 259.75,-246 259.75,-246 346.25,-246 346.25,-246 352.25,-246 358.25,-252 358.25,-258 358.25,-258 358.25,-273.5 358.25,-273.5 358.25,-279.5 352.25,-285.5 346.25,-285.5"/>
<text xml:space="preserve" text-anchor="middle" x="303" y="-271.05" font-family="JetBrains Mono" font-size="11.00" fill="#c9d1d9">correlate</text>
<text xml:space="preserve" text-anchor="middle" x="303" y="-255.3" font-family="JetBrains Mono" font-size="11.00" fill="#c9d1d9">(plan&#45;and&#45;execute)</text>
</g>
<!-- plan&#45;&gt;corr -->
<g id="edge5" class="edge">
<title>plan&#45;&gt;corr</title>
<path fill="none" stroke="#8b949e" d="M303,-343.98C303,-330.54 303,-312.23 303,-296.82"/>
<polygon fill="#8b949e" stroke="#8b949e" points="306.5,-297.24 303,-287.24 299.5,-297.24 306.5,-297.24"/>
</g>
<!-- langfuse -->
<g id="node12" class="node">
<title>langfuse</title>
<path fill="#bf4b8a" fill-opacity="0.200000" stroke="#30363d" d="M526.38,-37.75C526.38,-37.75 437.62,-37.75 437.62,-37.75 431.62,-37.75 425.62,-31.75 425.62,-25.75 425.62,-25.75 425.62,-13.75 425.62,-13.75 425.62,-7.75 431.62,-1.75 437.62,-1.75 437.62,-1.75 526.38,-1.75 526.38,-1.75 532.38,-1.75 538.38,-7.75 538.38,-13.75 538.38,-13.75 538.38,-25.75 538.38,-25.75 538.38,-31.75 532.38,-37.75 526.38,-37.75"/>
<text xml:space="preserve" text-anchor="middle" x="482" y="-17.18" font-family="JetBrains Mono" font-size="11.00" fill="#ffffff">Langfuse (lng, WG)</text>
</g>
<!-- plan&#45;&gt;langfuse -->
<g id="edge18" class="edge">
<title>plan&#45;&gt;langfuse</title>
<path fill="none" stroke="#8b949e" stroke-dasharray="1,5" d="M339.9,-363.21C403.73,-361.43 528,-347.58 528,-266.75 528,-266.75 528,-266.75 528,-93.5 528,-76.02 518.39,-59.32 507.96,-46.34"/>
<polygon fill="#8b949e" stroke="#8b949e" points="510.9,-44.39 501.7,-39.13 505.61,-48.97 510.9,-44.39"/>
<text xml:space="preserve" text-anchor="middle" x="541.12" y="-166.88" font-family="JetBrains Mono" font-size="10.00" fill="#8b949e">spans</text>
</g>
<!-- t2s -->
<g id="node7" class="node">
<title>t2s</title>
<path fill="#21262d" stroke="#30363d" d="M193.75,-187.25C193.75,-187.25 146.25,-187.25 146.25,-187.25 140.25,-187.25 134.25,-181.25 134.25,-175.25 134.25,-175.25 134.25,-163.25 134.25,-163.25 134.25,-157.25 140.25,-151.25 146.25,-151.25 146.25,-151.25 193.75,-151.25 193.75,-151.25 199.75,-151.25 205.75,-157.25 205.75,-163.25 205.75,-163.25 205.75,-175.25 205.75,-175.25 205.75,-181.25 199.75,-187.25 193.75,-187.25"/>
<text xml:space="preserve" text-anchor="middle" x="170" y="-166.68" font-family="JetBrains Mono" font-size="11.00" fill="#c9d1d9">text_to_sql</text>
</g>
<!-- compare&#45;&gt;t2s -->
<g id="edge6" class="edge">
<title>compare&#45;&gt;t2s</title>
<path fill="none" stroke="#8b949e" d="M388.9,-245.51C381.73,-242.63 374.24,-239.97 367,-238 334.16,-229.06 245.27,-234.56 215,-219 204.66,-213.68 195.48,-204.98 188.15,-196.39"/>
<polygon fill="#8b949e" stroke="#8b949e" points="191.1,-194.48 182.15,-188.81 185.61,-198.82 191.1,-194.48"/>
</g>
<!-- compare&#45;&gt;langfuse -->
<g id="edge19" class="edge">
<title>compare&#45;&gt;langfuse</title>
<path fill="none" stroke="#8b949e" stroke-dasharray="1,5" d="M446.82,-245.68C453,-237.96 459.3,-228.61 463,-219 484.95,-161.95 485.65,-89.15 483.97,-49.56"/>
<polygon fill="#8b949e" stroke="#8b949e" points="487.47,-49.48 483.46,-39.68 480.48,-49.84 487.47,-49.48"/>
</g>
<!-- synth -->
<g id="node13" class="node">
<title>synth</title>
<path fill="#21262d" stroke="#30363d" d="M442.25,-189C442.25,-189 355.75,-189 355.75,-189 349.75,-189 343.75,-183 343.75,-177 343.75,-177 343.75,-161.5 343.75,-161.5 343.75,-155.5 349.75,-149.5 355.75,-149.5 355.75,-149.5 442.25,-149.5 442.25,-149.5 448.25,-149.5 454.25,-155.5 454.25,-161.5 454.25,-161.5 454.25,-177 454.25,-177 454.25,-183 448.25,-189 442.25,-189"/>
<text xml:space="preserve" text-anchor="middle" x="399" y="-174.55" font-family="JetBrains Mono" font-size="11.00" fill="#c9d1d9">synthesize</text>
<text xml:space="preserve" text-anchor="middle" x="399" y="-158.8" font-family="JetBrains Mono" font-size="11.00" fill="#c9d1d9">findings → answer</text>
</g>
<!-- compare&#45;&gt;synth -->
<g id="edge14" class="edge">
<title>compare&#45;&gt;synth</title>
<path fill="none" stroke="#8b949e" d="M422.93,-245.63C418.78,-232.56 413.2,-214.99 408.47,-200.09"/>
<polygon fill="#8b949e" stroke="#8b949e" points="411.88,-199.26 405.52,-190.79 405.21,-201.38 411.88,-199.26"/>
</g>
<!-- drill&#45;&gt;t2s -->
<g id="edge7" class="edge">
<title>drill&#45;&gt;t2s</title>
<path fill="none" stroke="#8b949e" d="M110.18,-245.63C121.48,-231.38 137.05,-211.77 149.47,-196.12"/>
<polygon fill="#8b949e" stroke="#8b949e" points="151.94,-198.63 155.42,-188.62 146.46,-194.28 151.94,-198.63"/>
</g>
<!-- schema -->
<g id="node9" class="node">
<title>schema</title>
<path fill="#21262d" stroke="#30363d" d="M104,-187.25C104,-187.25 28,-187.25 28,-187.25 22,-187.25 16,-181.25 16,-175.25 16,-175.25 16,-163.25 16,-163.25 16,-157.25 22,-151.25 28,-151.25 28,-151.25 104,-151.25 104,-151.25 110,-151.25 116,-157.25 116,-163.25 116,-163.25 116,-175.25 116,-175.25 116,-181.25 110,-187.25 104,-187.25"/>
<text xml:space="preserve" text-anchor="middle" x="66" y="-166.68" font-family="JetBrains Mono" font-size="11.00" fill="#c9d1d9">retrieve_schema</text>
</g>
<!-- drill&#45;&gt;schema -->
<g id="edge10" class="edge">
<title>drill&#45;&gt;schema</title>
<path fill="none" stroke="#8b949e" stroke-dasharray="5,2" d="M89.13,-245.63C84.96,-232.05 79.3,-213.59 74.62,-198.34"/>
<polygon fill="#8b949e" stroke="#8b949e" points="78,-197.42 71.72,-188.89 71.31,-199.47 78,-197.42"/>
</g>
<!-- drill&#45;&gt;synth -->
<g id="edge15" class="edge">
<title>drill&#45;&gt;synth</title>
<path fill="none" stroke="#8b949e" d="M122.66,-245.77C128.2,-242.71 134.13,-239.92 140,-238 182.01,-224.26 297.03,-235.6 338,-219 350.41,-213.97 362.32,-205.57 372.28,-197.14"/>
<polygon fill="#8b949e" stroke="#8b949e" points="374.58,-199.78 379.7,-190.5 369.91,-194.56 374.58,-199.78"/>
</g>
<!-- outl&#45;&gt;t2s -->
<g id="edge8" class="edge">
<title>outl&#45;&gt;t2s</title>
<path fill="none" stroke="#8b949e" d="M185.16,-245.63C182.42,-232.05 178.71,-213.59 175.65,-198.34"/>
<polygon fill="#8b949e" stroke="#8b949e" points="179.16,-198.04 173.75,-188.92 172.29,-199.42 179.16,-198.04"/>
</g>
<!-- outl&#45;&gt;synth -->
<g id="edge16" class="edge">
<title>outl&#45;&gt;synth</title>
<path fill="none" stroke="#8b949e" d="M221.44,-245.59C227.16,-242.74 233.15,-240.07 239,-238 281.23,-223.04 297.31,-237.76 338,-219 349.97,-213.48 361.65,-205.12 371.53,-196.88"/>
<polygon fill="#8b949e" stroke="#8b949e" points="373.69,-199.63 378.91,-190.42 369.08,-194.37 373.69,-199.63"/>
</g>
<!-- corr&#45;&gt;t2s -->
<g id="edge9" class="edge">
<title>corr&#45;&gt;t2s</title>
<path fill="none" stroke="#8b949e" d="M259.14,-245.69C244.45,-238.35 228.41,-229.24 215,-219 206.36,-212.4 197.96,-204.03 190.82,-196.1"/>
<polygon fill="#8b949e" stroke="#8b949e" points="193.51,-193.86 184.32,-188.59 188.22,-198.44 193.51,-193.86"/>
</g>
<!-- py -->
<g id="node10" class="node">
<title>py</title>
<path fill="#21262d" stroke="#30363d" d="M313.75,-187.25C313.75,-187.25 236.25,-187.25 236.25,-187.25 230.25,-187.25 224.25,-181.25 224.25,-175.25 224.25,-175.25 224.25,-163.25 224.25,-163.25 224.25,-157.25 230.25,-151.25 236.25,-151.25 236.25,-151.25 313.75,-151.25 313.75,-151.25 319.75,-151.25 325.75,-157.25 325.75,-163.25 325.75,-163.25 325.75,-175.25 325.75,-175.25 325.75,-181.25 319.75,-187.25 313.75,-187.25"/>
<text xml:space="preserve" text-anchor="middle" x="275" y="-166.68" font-family="JetBrains Mono" font-size="11.00" fill="#c9d1d9">python_sandbox</text>
</g>
<!-- corr&#45;&gt;py -->
<g id="edge11" class="edge">
<title>corr&#45;&gt;py</title>
<path fill="none" stroke="#8b949e" stroke-dasharray="5,2" d="M297.33,-245.63C293.31,-232.05 287.84,-213.59 283.32,-198.34"/>
<polygon fill="#8b949e" stroke="#8b949e" points="286.72,-197.48 280.52,-188.89 280.01,-199.47 286.72,-197.48"/>
</g>
<!-- corr&#45;&gt;synth -->
<g id="edge17" class="edge">
<title>corr&#45;&gt;synth</title>
<path fill="none" stroke="#8b949e" d="M322.43,-245.63C336.61,-231.66 356.03,-212.55 371.76,-197.07"/>
<polygon fill="#8b949e" stroke="#8b949e" points="373.85,-199.92 378.52,-190.41 368.94,-194.93 373.85,-199.92"/>
</g>
<!-- exec -->
<g id="node8" class="node">
<title>exec</title>
<path fill="#21262d" stroke="#30363d" d="M254.62,-112.5C254.62,-112.5 203.38,-112.5 203.38,-112.5 197.38,-112.5 191.38,-106.5 191.38,-100.5 191.38,-100.5 191.38,-88.5 191.38,-88.5 191.38,-82.5 197.38,-76.5 203.38,-76.5 203.38,-76.5 254.62,-76.5 254.62,-76.5 260.62,-76.5 266.62,-82.5 266.62,-88.5 266.62,-88.5 266.62,-100.5 266.62,-100.5 266.62,-106.5 260.62,-112.5 254.62,-112.5"/>
<text xml:space="preserve" text-anchor="middle" x="229" y="-91.92" font-family="JetBrains Mono" font-size="11.00" fill="#c9d1d9">execute_sql</text>
</g>
<!-- t2s&#45;&gt;exec -->
<g id="edge12" class="edge">
<title>t2s&#45;&gt;exec</title>
<path fill="none" stroke="#8b949e" d="M183.98,-151.01C191.06,-142.28 199.8,-131.51 207.69,-121.78"/>
<polygon fill="#8b949e" stroke="#8b949e" points="210.38,-124.02 213.96,-114.04 204.94,-119.61 210.38,-124.02"/>
</g>
<!-- warehouse -->
<g id="node11" class="node">
<title>warehouse</title>
<path fill="#388bfd" fill-opacity="0.200000" stroke="#30363d" d="M261.38,-39.5C261.38,-39.5 196.62,-39.5 196.62,-39.5 190.62,-39.5 184.62,-33.5 184.62,-27.5 184.62,-27.5 184.62,-12 184.62,-12 184.62,-6 190.62,0 196.62,0 196.62,0 261.38,0 261.38,0 267.38,0 273.38,-6 273.38,-12 273.38,-12 273.38,-27.5 273.38,-27.5 273.38,-33.5 267.38,-39.5 261.38,-39.5"/>
<text xml:space="preserve" text-anchor="middle" x="229" y="-25.05" font-family="JetBrains Mono" font-size="11.00" fill="#c9d1d9">Postgres</text>
<text xml:space="preserve" text-anchor="middle" x="229" y="-9.3" font-family="JetBrains Mono" font-size="11.00" fill="#c9d1d9">BIRD financial</text>
</g>
<!-- exec&#45;&gt;warehouse -->
<g id="edge13" class="edge">
<title>exec&#45;&gt;warehouse</title>
<path fill="none" stroke="#8b949e" d="M229,-76.26C229,-68.75 229,-59.72 229,-51.14"/>
<polygon fill="#8b949e" stroke="#8b949e" points="232.5,-51.43 229,-41.43 225.5,-51.43 232.5,-51.43"/>
</g>
<!-- exec&#45;&gt;langfuse -->
<g id="edge20" class="edge">
<title>exec&#45;&gt;langfuse</title>
<path fill="none" stroke="#8b949e" stroke-dasharray="1,5" d="M266.88,-82.61C305.88,-71.39 367.48,-53.68 414.78,-40.08"/>
<polygon fill="#8b949e" stroke="#8b949e" points="415.46,-43.52 424.11,-37.4 413.53,-36.8 415.46,-43.52"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 17 KiB

435
docs/index.html Normal file
View 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>