ui and tiltfile tweaks, update docs

This commit is contained in:
2026-06-03 14:57:25 -03:00
parent be09fcde2c
commit 05fe7000c5
6 changed files with 531 additions and 387 deletions

View File

@@ -63,21 +63,22 @@ k8s_resource('postgres')
k8s_resource('api', resource_deps=['postgres']) k8s_resource('api', resource_deps=['postgres'])
k8s_resource('ui', resource_deps=['api']) k8s_resource('ui', resource_deps=['api'])
k8s_resource('docs') k8s_resource('docs')
# In-cluster Caddy reverse proxy: owns the single NodePort (30060) and routes by
# Host header to ui / api / docs. The host's dnsmasq + system Caddy forward
# *.local.ar to this NodePort — gateway is this cluster's ingress, not the
# multi-cluster host routing.
# NOTE: editing k8s/base/Caddyfile won't roll the pod on its own
# (disableNameSuffixHash keeps the configMap name stable). Apply Caddyfile edits
# with: kubectl --context kind-nvi -n nvi rollout restart deployment/gateway
k8s_resource('gateway', resource_deps=['ui', 'api', 'docs']) k8s_resource('gateway', resource_deps=['ui', 'api', 'docs'])
# Hot-reload gateway Caddy on Caddyfile edit. configMapGenerator uses # Group infra objects (namespace, configmaps, the .env-backed secret).
# disableNameSuffixHash so the Deployment template doesn't change → kustomize
# won't roll the pod on its own. This local_resource closes the loop.
local_resource(
'gateway-reload',
cmd='kubectl --context kind-nvi -n nvi rollout restart deployment/gateway',
deps=['k8s/base/Caddyfile'],
resource_deps=['gateway'],
auto_init=False,
)
# Group infra objects
k8s_resource( k8s_resource(
objects=['nvi:namespace', 'nvi-config:configmap', 'gateway-config:configmap'], objects=[
'nvi:namespace',
'nvi-config:configmap',
'gateway-config:configmap',
'nvi-secrets:secret',
],
new_name='infra', new_name='infra',
) )

View File

@@ -13,8 +13,9 @@ resources:
- gateway.yaml - gateway.yaml
# Hash suffix disabled so the name stays static — lets Tilt group it under the # Hash suffix disabled so the name stays static — lets Tilt group it under the
# 'infra' resource. Pod doesn't auto-restart on Caddyfile edit; the Tiltfile's # 'infra' resource. Trade-off: editing the Caddyfile won't roll the gateway pod
# 'gateway-reload' local_resource closes that loop. # automatically; apply it with
# kubectl --context kind-nvi -n nvi rollout restart deployment/gateway
configMapGenerator: configMapGenerator:
- name: gateway-config - name: gateway-config
files: files:

View File

@@ -21,7 +21,7 @@ digraph nvi {
color="#30363d"; color="#30363d";
fontcolor="#58a6ff"; fontcolor="#58a6ff";
fontsize=11; fontsize=11;
plan [label="planner\n(1 LLM call)"]; plan [label="planner\n(1 LLM call → ordered Analyses)"];
} }
subgraph cluster_l2 { subgraph cluster_l2 {
@@ -29,37 +29,53 @@ digraph nvi {
color="#30363d"; color="#30363d";
fontcolor="#58a6ff"; fontcolor="#58a6ff";
fontsize=11; fontsize=11;
compare [label="compare_periods\n(CoT)"]; direct [label="direct_answer\n(CoT, one shot)"];
drill [label="drill_down\n(ReAct)"]; compare [label="compare_periods\n(CoT, 2 periods)"];
outl [label="find_outliers\n(CoT)"]; drill [label="drill_down\n(ReAct loop)"];
corr [label="correlate\n(plan-and-execute)"]; planned [label="find_outliers · correlate\n(planned)",
style="rounded,filled,dashed", fontcolor="#6e7681"];
} }
subgraph cluster_l1 { subgraph cluster_l1 {
label="L1 — Tools (deterministic)"; label="L1 — Atomic actions (deterministic, no LLM)";
color="#30363d"; color="#30363d";
fontcolor="#58a6ff"; fontcolor="#58a6ff";
fontsize=11; fontsize=11;
t2s [label="text_to_sql"]; compose [label="compose(pick, recon)\n→ SQL"];
exec [label="execute_sql"]; exec [label="execute_sql\n→ rows"];
schema [label="retrieve_schema"]; }
py [label="python_sandbox"];
subgraph cluster_l0 {
label="L0 — Recon + dataset setup (foundation)";
color="#30363d";
fontcolor="#58a6ff";
fontsize=11;
recon [label="recon\nknowledge graph"];
setup [label="schema_docs.yaml · metrics.yaml\nencodings · DDL extract", fontcolor="#8b949e"];
} }
warehouse [label="Postgres\nBIRD financial", fillcolor="#388bfd33"]; warehouse [label="Postgres\nBIRD financial", fillcolor="#388bfd33"];
langfuse [label="Langfuse (lng, WG)", fillcolor="#bf4b8a33", fontcolor="#ffffff"]; langfuse [label="Langfuse (lng, WG)", fillcolor="#bf4b8a33", fontcolor="#ffffff"];
synth [label="synthesize\nfindings → answer"]; synth [label="synthesize\nfindings → answer"];
// General → specific flow.
user -> plan; user -> plan;
plan -> compare; plan -> drill; plan -> outl; plan -> corr; plan -> direct; plan -> compare; plan -> drill;
compare -> t2s; drill -> t2s; outl -> t2s; corr -> t2s; direct -> compose [label="typed Pick"];
drill -> schema [style=dashed]; compare -> compose;
corr -> py [style=dashed]; drill -> compose [label="typed Pick"];
t2s -> exec; drill -> drill [label="iterate on results", color="#bf4b8a", fontcolor="#bf4b8a"];
compose -> exec;
exec -> warehouse; exec -> warehouse;
compare -> synth; drill -> synth; outl -> synth; corr -> synth; direct -> synth; compare -> synth; drill -> synth;
// L0 is consulted by every layer above (not a one-way cascade).
setup -> recon [label="make recon"];
recon -> plan [style=dashed, label="brief names", constraint=false];
recon -> compose [style=dashed, label="columns · joins · metrics", constraint=false];
// Observability.
plan -> langfuse [style=dotted, label="spans"]; plan -> langfuse [style=dotted, label="spans"];
compare -> langfuse [style=dotted]; compose -> langfuse [style=dotted];
exec -> langfuse [style=dotted]; exec -> langfuse [style=dotted];
} }

View File

@@ -4,231 +4,240 @@
<!-- Generated by graphviz version 14.1.2 (0) <!-- Generated by graphviz version 14.1.2 (0)
--> -->
<!-- Title: nvi Pages: 1 --> <!-- Title: nvi Pages: 1 -->
<svg width="562pt" height="487pt" <svg width="745pt" height="562pt"
viewBox="0.00 0.00 562.00 487.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> viewBox="0.00 0.00 745.00 562.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)"> <g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 557.5)">
<title>nvi</title> <title>nvi</title>
<polygon fill="#161b22" stroke="none" points="-4,4 -4,-482.5 558.25,-482.5 558.25,4 -4,4"/> <polygon fill="#161b22" stroke="none" points="-4,4 -4,-557.5 740.65,-557.5 740.65,4 -4,4"/>
<g id="clust1" class="cluster"> <g id="clust1" class="cluster">
<title>cluster_l3</title> <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"/> <polygon fill="#161b22" stroke="#30363d" points="192,-354.75 192,-434 384,-434 384,-354.75 192,-354.75"/>
<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> <text xml:space="preserve" text-anchor="middle" x="288" y="-419.55" font-family="JetBrains Mono" font-size="11.00" fill="#58a6ff">L3 — Plan</text>
</g> </g>
<g id="clust2" class="cluster"> <g id="clust2" class="cluster">
<title>cluster_l2</title> <title>cluster_l2</title>
<polygon fill="#161b22" stroke="#30363d" points="52,-238 52,-317.25 490,-317.25 490,-238 52,-238"/> <polygon fill="#161b22" stroke="#30363d" points="8,-256.5 8,-335.75 584,-335.75 584,-256.5 8,-256.5"/>
<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> <text xml:space="preserve" text-anchor="middle" x="296" y="-321.3" font-family="JetBrains Mono" font-size="11.00" fill="#58a6ff">L2 — Analyses (mini&#45;agents)</text>
</g> </g>
<g id="clust3" class="cluster"> <g id="clust3" class="cluster">
<title>cluster_l1</title> <title>cluster_l1</title>
<polygon fill="#161b22" stroke="#30363d" points="8,-68.5 8,-219 334,-219 334,-68.5 8,-68.5"/> <polygon fill="#161b22" stroke="#30363d" points="62,-68.5 62,-224.25 304,-224.25 304,-68.5 62,-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> <text xml:space="preserve" text-anchor="middle" x="183" y="-209.8" font-family="JetBrains Mono" font-size="11.00" fill="#58a6ff">L1 — Atomic actions (deterministic, no LLM)</text>
</g>
<g id="clust4" class="cluster">
<title>cluster_l0</title>
<polygon fill="#161b22" stroke="#30363d" points="413,-354.75 413,-545.5 637,-545.5 637,-354.75 413,-354.75"/>
<text xml:space="preserve" text-anchor="middle" x="525" y="-531.05" font-family="JetBrains Mono" font-size="11.00" fill="#58a6ff">L0 — Recon + dataset setup (foundation)</text>
</g> </g>
<!-- user --> <!-- user -->
<g id="node1" class="node"> <g id="node1" class="node">
<title>user</title> <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"/> <path fill="#1f6feb" stroke="#30363d" d="M319.25,-512C319.25,-512 256.75,-512 256.75,-512 250.75,-512 244.75,-506 244.75,-500 244.75,-500 244.75,-488 244.75,-488 244.75,-482 250.75,-476 256.75,-476 256.75,-476 319.25,-476 319.25,-476 325.25,-476 331.25,-482 331.25,-488 331.25,-488 331.25,-500 331.25,-500 331.25,-506 325.25,-512 319.25,-512"/>
<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> <text xml:space="preserve" text-anchor="middle" x="288" y="-491.43" font-family="JetBrains Mono" font-size="11.00" fill="#ffffff">user question</text>
</g> </g>
<!-- plan --> <!-- plan -->
<g id="node2" class="node"> <g id="node2" class="node">
<title>plan</title> <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"/> <path fill="#21262d" stroke="#30363d" d="M364.25,-402.25C364.25,-402.25 211.75,-402.25 211.75,-402.25 205.75,-402.25 199.75,-396.25 199.75,-390.25 199.75,-390.25 199.75,-374.75 199.75,-374.75 199.75,-368.75 205.75,-362.75 211.75,-362.75 211.75,-362.75 364.25,-362.75 364.25,-362.75 370.25,-362.75 376.25,-368.75 376.25,-374.75 376.25,-374.75 376.25,-390.25 376.25,-390.25 376.25,-396.25 370.25,-402.25 364.25,-402.25"/>
<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="288" y="-387.8" 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> <text xml:space="preserve" text-anchor="middle" x="288" y="-372.05" font-family="JetBrains Mono" font-size="11.00" fill="#c9d1d9">(1 LLM call → ordered Analyses)</text>
</g> </g>
<!-- user&#45;&gt;plan --> <!-- user&#45;&gt;plan -->
<g id="edge1" class="edge"> <g id="edge1" class="edge">
<title>user&#45;&gt;plan</title> <title>user&#45;&gt;plan</title>
<path fill="none" stroke="#8b949e" d="M303,-442.17C303,-429.09 303,-410.82 303,-395.34"/> <path fill="none" stroke="#8b949e" d="M288,-475.89C288,-459.38 288,-434 288,-414.02"/>
<polygon fill="#8b949e" stroke="#8b949e" points="306.5,-395.69 303,-385.69 299.5,-395.69 306.5,-395.69"/> <polygon fill="#8b949e" stroke="#8b949e" points="291.5,-414.21 288,-404.21 284.5,-414.21 291.5,-414.21"/>
</g>
<!-- direct -->
<g id="node3" class="node">
<title>direct</title>
<path fill="#21262d" stroke="#30363d" d="M285.88,-304C285.88,-304 218.12,-304 218.12,-304 212.12,-304 206.12,-298 206.12,-292 206.12,-292 206.12,-276.5 206.12,-276.5 206.12,-270.5 212.12,-264.5 218.12,-264.5 218.12,-264.5 285.88,-264.5 285.88,-264.5 291.88,-264.5 297.88,-270.5 297.88,-276.5 297.88,-276.5 297.88,-292 297.88,-292 297.88,-298 291.88,-304 285.88,-304"/>
<text xml:space="preserve" text-anchor="middle" x="252" y="-289.55" font-family="JetBrains Mono" font-size="11.00" fill="#c9d1d9">direct_answer</text>
<text xml:space="preserve" text-anchor="middle" x="252" y="-273.8" font-family="JetBrains Mono" font-size="11.00" fill="#c9d1d9">(CoT, one shot)</text>
</g>
<!-- plan&#45;&gt;direct -->
<g id="edge2" class="edge">
<title>plan&#45;&gt;direct</title>
<path fill="none" stroke="#8b949e" d="M280.89,-362.48C275.81,-348.91 268.88,-330.38 263.08,-314.87"/>
<polygon fill="#8b949e" stroke="#8b949e" points="266.41,-313.78 259.63,-305.64 259.85,-316.24 266.41,-313.78"/>
</g> </g>
<!-- compare --> <!-- compare -->
<g id="node3" class="node"> <g id="node4" class="node">
<title>compare</title> <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"/> <path fill="#21262d" stroke="#30363d" d="M410,-304C410,-304 328,-304 328,-304 322,-304 316,-298 316,-292 316,-292 316,-276.5 316,-276.5 316,-270.5 322,-264.5 328,-264.5 328,-264.5 410,-264.5 410,-264.5 416,-264.5 422,-270.5 422,-276.5 422,-276.5 422,-292 422,-292 422,-298 416,-304 410,-304"/>
<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="369" y="-289.55" 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> <text xml:space="preserve" text-anchor="middle" x="369" y="-273.8" font-family="JetBrains Mono" font-size="11.00" fill="#c9d1d9">(CoT, 2 periods)</text>
</g> </g>
<!-- plan&#45;&gt;compare --> <!-- plan&#45;&gt;compare -->
<g id="edge2" class="edge"> <g id="edge3" class="edge">
<title>plan&#45;&gt;compare</title> <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"/> <path fill="none" stroke="#8b949e" d="M304,-362.48C315.98,-348.25 332.55,-328.56 345.96,-312.63"/>
<polygon fill="#8b949e" stroke="#8b949e" points="399.64,-295.82 405.14,-286.77 395.19,-290.41 399.64,-295.82"/> <polygon fill="#8b949e" stroke="#8b949e" points="348.29,-315.29 352.06,-305.38 342.94,-310.78 348.29,-315.29"/>
</g> </g>
<!-- drill --> <!-- drill -->
<g id="node4" class="node"> <g id="node5" class="node">
<title>drill</title> <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"/> <path fill="#21262d" stroke="#30363d" d="M80.38,-304C80.38,-304 27.62,-304 27.62,-304 21.62,-304 15.62,-298 15.62,-292 15.62,-292 15.62,-276.5 15.62,-276.5 15.62,-270.5 21.62,-264.5 27.62,-264.5 27.62,-264.5 80.38,-264.5 80.38,-264.5 86.38,-264.5 92.38,-270.5 92.38,-276.5 92.38,-276.5 92.38,-292 92.38,-292 92.38,-298 86.38,-304 80.38,-304"/>
<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="54" y="-289.55" 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> <text xml:space="preserve" text-anchor="middle" x="54" y="-273.8" font-family="JetBrains Mono" font-size="11.00" fill="#c9d1d9">(ReAct loop)</text>
</g> </g>
<!-- plan&#45;&gt;drill --> <!-- 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"> <g id="edge4" class="edge">
<title>plan&#45;&gt;outl</title> <title>plan&#45;&gt;drill</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"/> <path fill="none" stroke="#8b949e" d="M241.21,-362.25C201.17,-345.79 143.73,-322.16 103.06,-305.43"/>
<polygon fill="#8b949e" stroke="#8b949e" points="216.86,-291.6 207.39,-286.84 211.82,-296.46 216.86,-291.6"/> <polygon fill="#8b949e" stroke="#8b949e" points="104.56,-302.26 93.98,-301.69 101.9,-308.74 104.56,-302.26"/>
</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> </g>
<!-- langfuse --> <!-- langfuse -->
<g id="node12" class="node"> <g id="node12" class="node">
<title>langfuse</title> <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"/> <path fill="#bf4b8a" fill-opacity="0.200000" stroke="#30363d" d="M383.38,-37.75C383.38,-37.75 294.62,-37.75 294.62,-37.75 288.62,-37.75 282.62,-31.75 282.62,-25.75 282.62,-25.75 282.62,-13.75 282.62,-13.75 282.62,-7.75 288.62,-1.75 294.62,-1.75 294.62,-1.75 383.38,-1.75 383.38,-1.75 389.38,-1.75 395.38,-7.75 395.38,-13.75 395.38,-13.75 395.38,-25.75 395.38,-25.75 395.38,-31.75 389.38,-37.75 383.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> <text xml:space="preserve" text-anchor="middle" x="339" y="-17.18" font-family="JetBrains Mono" font-size="11.00" fill="#ffffff">Langfuse (lng, WG)</text>
</g> </g>
<!-- plan&#45;&gt;langfuse --> <!-- plan&#45;&gt;langfuse -->
<g id="edge18" class="edge"> <g id="edge17" class="edge">
<title>plan&#45;&gt;langfuse</title> <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"/> <path fill="none" stroke="#8b949e" stroke-dasharray="1,5" d="M369.12,-362.26C382.4,-359.49 396.05,-356.86 409,-354.75 448.48,-348.31 557.02,-361.05 588,-335.75 606.24,-320.86 604,-308.79 604,-285.25 604,-285.25 604,-285.25 604,-95.25 604,-54.37 485.64,-34.86 407.13,-26.41"/>
<polygon fill="#8b949e" stroke="#8b949e" points="510.9,-44.39 501.7,-39.13 505.61,-48.97 510.9,-44.39"/> <polygon fill="#8b949e" stroke="#8b949e" points="407.66,-22.95 397.35,-25.4 406.94,-29.91 407.66,-22.95"/>
<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> <text xml:space="preserve" text-anchor="middle" x="617.12" y="-170.38" font-family="JetBrains Mono" font-size="10.00" fill="#8b949e">spans</text>
</g> </g>
<!-- t2s --> <!-- compose -->
<g id="node7" class="node"> <g id="node7" class="node">
<title>t2s</title> <title>compose</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"/> <path fill="#21262d" stroke="#30363d" d="M282.75,-192.5C282.75,-192.5 181.25,-192.5 181.25,-192.5 175.25,-192.5 169.25,-186.5 169.25,-180.5 169.25,-180.5 169.25,-165 169.25,-165 169.25,-159 175.25,-153 181.25,-153 181.25,-153 282.75,-153 282.75,-153 288.75,-153 294.75,-159 294.75,-165 294.75,-165 294.75,-180.5 294.75,-180.5 294.75,-186.5 288.75,-192.5 282.75,-192.5"/>
<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> <text xml:space="preserve" text-anchor="middle" x="232" y="-178.05" font-family="JetBrains Mono" font-size="11.00" fill="#c9d1d9">compose(pick, recon)</text>
<text xml:space="preserve" text-anchor="middle" x="232" y="-162.3" font-family="JetBrains Mono" font-size="11.00" fill="#c9d1d9">→ SQL</text>
</g> </g>
<!-- compare&#45;&gt;t2s --> <!-- direct&#45;&gt;compose -->
<g id="edge6" class="edge"> <g id="edge5" class="edge">
<title>compare&#45;&gt;t2s</title> <title>direct&#45;&gt;compose</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"/> <path fill="none" stroke="#8b949e" d="M248.52,-264.18C245.49,-247.63 241.07,-223.4 237.56,-204.18"/>
<polygon fill="#8b949e" stroke="#8b949e" points="191.1,-194.48 182.15,-188.81 185.61,-198.82 191.1,-194.48"/> <polygon fill="#8b949e" stroke="#8b949e" points="241.02,-203.67 235.78,-194.46 234.14,-204.93 241.02,-203.67"/>
</g> <text xml:space="preserve" text-anchor="middle" x="268.59" y="-237" font-family="JetBrains Mono" font-size="10.00" fill="#8b949e">typed Pick</text>
<!-- 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> </g>
<!-- synth --> <!-- synth -->
<g id="node13" class="node"> <g id="node13" class="node">
<title>synth</title> <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"/> <path fill="#21262d" stroke="#30363d" d="M411.25,-192.5C411.25,-192.5 324.75,-192.5 324.75,-192.5 318.75,-192.5 312.75,-186.5 312.75,-180.5 312.75,-180.5 312.75,-165 312.75,-165 312.75,-159 318.75,-153 324.75,-153 324.75,-153 411.25,-153 411.25,-153 417.25,-153 423.25,-159 423.25,-165 423.25,-165 423.25,-180.5 423.25,-180.5 423.25,-186.5 417.25,-192.5 411.25,-192.5"/>
<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="368" y="-178.05" 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> <text xml:space="preserve" text-anchor="middle" x="368" y="-162.3" font-family="JetBrains Mono" font-size="11.00" fill="#c9d1d9">findings → answer</text>
</g>
<!-- direct&#45;&gt;synth -->
<g id="edge11" class="edge">
<title>direct&#45;&gt;synth</title>
<path fill="none" stroke="#8b949e" d="M282.43,-264.09C290.08,-258.76 298.09,-252.71 305,-246.5 320.09,-232.95 335.04,-215.89 346.58,-201.69"/>
<polygon fill="#8b949e" stroke="#8b949e" points="349.2,-204.03 352.7,-194.03 343.73,-199.66 349.2,-204.03"/>
</g>
<!-- compare&#45;&gt;compose -->
<g id="edge6" class="edge">
<title>compare&#45;&gt;compose</title>
<path fill="none" stroke="#8b949e" d="M345.14,-264.18C322.88,-246.39 289.48,-219.69 264.74,-199.92"/>
<polygon fill="#8b949e" stroke="#8b949e" points="267.23,-197.43 257.23,-193.92 262.86,-202.89 267.23,-197.43"/>
</g> </g>
<!-- compare&#45;&gt;synth --> <!-- compare&#45;&gt;synth -->
<g id="edge14" class="edge"> <g id="edge12" class="edge">
<title>compare&#45;&gt;synth</title> <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"/> <path fill="none" stroke="#8b949e" d="M368.83,-264.18C368.68,-247.71 368.46,-223.62 368.28,-204.45"/>
<polygon fill="#8b949e" stroke="#8b949e" points="411.88,-199.26 405.52,-190.79 405.21,-201.38 411.88,-199.26"/> <polygon fill="#8b949e" stroke="#8b949e" points="371.78,-204.45 368.19,-194.49 364.78,-204.52 371.78,-204.45"/>
</g> </g>
<!-- drill&#45;&gt;t2s --> <!-- drill&#45;&gt;drill -->
<g id="edge8" class="edge">
<title>drill&#45;&gt;drill</title>
<path fill="none" stroke="#bf4b8a" d="M92.73,-292.58C102.79,-292.07 110.38,-289.29 110.38,-284.25 110.38,-281.34 107.84,-279.18 103.76,-277.78"/>
<polygon fill="#bf4b8a" stroke="#bf4b8a" points="104.67,-274.38 94.23,-276.17 103.51,-281.29 104.67,-274.38"/>
<text xml:space="preserve" text-anchor="middle" x="149.38" y="-281.88" font-family="JetBrains Mono" font-size="10.00" fill="#bf4b8a">iterate on results</text>
</g>
<!-- drill&#45;&gt;compose -->
<g id="edge7" class="edge"> <g id="edge7" class="edge">
<title>drill&#45;&gt;t2s</title> <title>drill&#45;&gt;compose</title>
<path fill="none" stroke="#8b949e" d="M110.18,-245.63C121.48,-231.38 137.05,-211.77 149.47,-196.12"/> <path fill="none" stroke="#8b949e" d="M85.01,-264.18C114.43,-246.07 158.84,-218.75 191.15,-198.88"/>
<polygon fill="#8b949e" stroke="#8b949e" points="151.94,-198.63 155.42,-188.62 146.46,-194.28 151.94,-198.63"/> <polygon fill="#8b949e" stroke="#8b949e" points="192.78,-201.99 199.46,-193.77 189.11,-196.02 192.78,-201.99"/>
</g> <text xml:space="preserve" text-anchor="middle" x="155.97" y="-237" font-family="JetBrains Mono" font-size="10.00" fill="#8b949e">typed Pick</text>
<!-- 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> </g>
<!-- drill&#45;&gt;synth --> <!-- drill&#45;&gt;synth -->
<g id="edge15" class="edge"> <g id="edge13" class="edge">
<title>drill&#45;&gt;synth</title> <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"/> <path fill="none" stroke="#8b949e" d="M92.73,-275.3C121.43,-269.58 161.55,-261.95 197,-256.5 233.72,-250.86 245.72,-260.82 280,-246.5 304.3,-236.35 327.08,-217.14 343.37,-201.01"/>
<polygon fill="#8b949e" stroke="#8b949e" points="374.58,-199.78 379.7,-190.5 369.91,-194.56 374.58,-199.78"/> <polygon fill="#8b949e" stroke="#8b949e" points="345.73,-203.61 350.21,-194.01 340.72,-198.71 345.73,-203.61"/>
</g> </g>
<!-- outl&#45;&gt;t2s --> <!-- planned -->
<g id="edge8" class="edge"> <g id="node6" class="node">
<title>outl&#45;&gt;t2s</title> <title>planned</title>
<path fill="none" stroke="#8b949e" d="M185.16,-245.63C182.42,-232.05 178.71,-213.59 175.65,-198.34"/> <path fill="#21262d" stroke="#30363d" stroke-dasharray="5,2" d="M563.62,-304C563.62,-304 452.38,-304 452.38,-304 446.38,-304 440.38,-298 440.38,-292 440.38,-292 440.38,-276.5 440.38,-276.5 440.38,-270.5 446.38,-264.5 452.38,-264.5 452.38,-264.5 563.62,-264.5 563.62,-264.5 569.62,-264.5 575.62,-270.5 575.62,-276.5 575.62,-276.5 575.62,-292 575.62,-292 575.62,-298 569.62,-304 563.62,-304"/>
<polygon fill="#8b949e" stroke="#8b949e" points="179.16,-198.04 173.75,-188.92 172.29,-199.42 179.16,-198.04"/> <text xml:space="preserve" text-anchor="middle" x="508" y="-289.55" font-family="JetBrains Mono" font-size="11.00" fill="#6e7681">find_outliers · correlate</text>
</g> <text xml:space="preserve" text-anchor="middle" x="508" y="-273.8" font-family="JetBrains Mono" font-size="11.00" fill="#6e7681">(planned)</text>
<!-- 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> </g>
<!-- exec --> <!-- exec -->
<g id="node8" class="node"> <g id="node8" class="node">
<title>exec</title> <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"/> <path fill="#21262d" stroke="#30363d" d="M257.62,-116C257.62,-116 206.38,-116 206.38,-116 200.38,-116 194.38,-110 194.38,-104 194.38,-104 194.38,-88.5 194.38,-88.5 194.38,-82.5 200.38,-76.5 206.38,-76.5 206.38,-76.5 257.62,-76.5 257.62,-76.5 263.62,-76.5 269.62,-82.5 269.62,-88.5 269.62,-88.5 269.62,-104 269.62,-104 269.62,-110 263.62,-116 257.62,-116"/>
<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> <text xml:space="preserve" text-anchor="middle" x="232" y="-101.55" font-family="JetBrains Mono" font-size="11.00" fill="#c9d1d9">execute_sql</text>
<text xml:space="preserve" text-anchor="middle" x="232" y="-85.8" font-family="JetBrains Mono" font-size="11.00" fill="#c9d1d9">→ rows</text>
</g> </g>
<!-- t2s&#45;&gt;exec --> <!-- compose&#45;&gt;exec -->
<g id="edge12" class="edge"> <g id="edge9" class="edge">
<title>t2s&#45;&gt;exec</title> <title>compose&#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"/> <path fill="none" stroke="#8b949e" d="M232,-152.56C232,-145 232,-136.13 232,-127.74"/>
<polygon fill="#8b949e" stroke="#8b949e" points="210.38,-124.02 213.96,-114.04 204.94,-119.61 210.38,-124.02"/> <polygon fill="#8b949e" stroke="#8b949e" points="235.5,-127.88 232,-117.88 228.5,-127.88 235.5,-127.88"/>
</g>
<!-- compose&#45;&gt;langfuse -->
<g id="edge18" class="edge">
<title>compose&#45;&gt;langfuse</title>
<path fill="none" stroke="#8b949e" stroke-dasharray="1,5" d="M249.21,-152.68C258.42,-142.15 269.74,-128.67 279,-116 295.23,-93.8 311.75,-67.23 323.33,-47.8"/>
<polygon fill="#8b949e" stroke="#8b949e" points="326.23,-49.77 328.3,-39.38 320.2,-46.21 326.23,-49.77"/>
</g> </g>
<!-- warehouse --> <!-- warehouse -->
<g id="node11" class="node"> <g id="node11" class="node">
<title>warehouse</title> <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"/> <path fill="#388bfd" fill-opacity="0.200000" stroke="#30363d" d="M252.38,-39.5C252.38,-39.5 187.62,-39.5 187.62,-39.5 181.62,-39.5 175.62,-33.5 175.62,-27.5 175.62,-27.5 175.62,-12 175.62,-12 175.62,-6 181.62,0 187.62,0 187.62,0 252.38,0 252.38,0 258.38,0 264.38,-6 264.38,-12 264.38,-12 264.38,-27.5 264.38,-27.5 264.38,-33.5 258.38,-39.5 252.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="220" 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> <text xml:space="preserve" text-anchor="middle" x="220" y="-9.3" font-family="JetBrains Mono" font-size="11.00" fill="#c9d1d9">BIRD financial</text>
</g> </g>
<!-- exec&#45;&gt;warehouse --> <!-- exec&#45;&gt;warehouse -->
<g id="edge13" class="edge"> <g id="edge10" class="edge">
<title>exec&#45;&gt;warehouse</title> <title>exec&#45;&gt;warehouse</title>
<path fill="none" stroke="#8b949e" d="M229,-76.26C229,-68.75 229,-59.72 229,-51.14"/> <path fill="none" stroke="#8b949e" d="M228.91,-76.06C227.68,-68.42 226.23,-59.43 224.86,-50.95"/>
<polygon fill="#8b949e" stroke="#8b949e" points="232.5,-51.43 229,-41.43 225.5,-51.43 232.5,-51.43"/> <polygon fill="#8b949e" stroke="#8b949e" points="228.37,-50.68 223.32,-41.36 221.46,-51.79 228.37,-50.68"/>
</g> </g>
<!-- exec&#45;&gt;langfuse --> <!-- exec&#45;&gt;langfuse -->
<g id="edge20" class="edge"> <g id="edge19" class="edge">
<title>exec&#45;&gt;langfuse</title> <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"/> <path fill="none" stroke="#8b949e" stroke-dasharray="1,5" d="M259.56,-76.06C273.37,-66.44 290.23,-54.7 304.86,-44.52"/>
<polygon fill="#8b949e" stroke="#8b949e" points="415.46,-43.52 424.11,-37.4 413.53,-36.8 415.46,-43.52"/> <polygon fill="#8b949e" stroke="#8b949e" points="306.65,-47.54 312.86,-38.95 302.65,-41.79 306.65,-47.54"/>
</g>
<!-- recon -->
<g id="node9" class="node">
<title>recon</title>
<path fill="#21262d" stroke="#30363d" d="M566.38,-402.25C566.38,-402.25 483.62,-402.25 483.62,-402.25 477.62,-402.25 471.62,-396.25 471.62,-390.25 471.62,-390.25 471.62,-374.75 471.62,-374.75 471.62,-368.75 477.62,-362.75 483.62,-362.75 483.62,-362.75 566.38,-362.75 566.38,-362.75 572.38,-362.75 578.38,-368.75 578.38,-374.75 578.38,-374.75 578.38,-390.25 578.38,-390.25 578.38,-396.25 572.38,-402.25 566.38,-402.25"/>
<text xml:space="preserve" text-anchor="middle" x="525" y="-387.8" font-family="JetBrains Mono" font-size="11.00" fill="#c9d1d9">recon</text>
<text xml:space="preserve" text-anchor="middle" x="525" y="-372.05" font-family="JetBrains Mono" font-size="11.00" fill="#c9d1d9">knowledge graph</text>
</g>
<!-- recon&#45;&gt;plan -->
<g id="edge15" class="edge">
<title>recon&#45;&gt;plan</title>
<path fill="none" stroke="#8b949e" stroke-dasharray="5,2" d="M471.26,-382.5C446.58,-382.5 416.49,-382.5 387.88,-382.5"/>
<polygon fill="#8b949e" stroke="#8b949e" points="388.1,-379 378.1,-382.5 388.1,-386 388.1,-379"/>
<text xml:space="preserve" text-anchor="middle" x="423.94" y="-390.25" font-family="JetBrains Mono" font-size="10.00" fill="#8b949e">brief names</text>
</g>
<!-- recon&#45;&gt;compose -->
<g id="edge16" class="edge">
<title>recon&#45;&gt;compose</title>
<path fill="none" stroke="#8b949e" stroke-dasharray="5,2" d="M578.57,-372.19C593.85,-366.78 608.82,-358.24 618,-344.75 641.3,-310.49 620.31,-282.43 588,-256.5 538.47,-216.74 363.39,-246.77 304,-224.25 288.98,-218.56 274.15,-209.02 261.89,-199.75"/>
<polygon fill="#8b949e" stroke="#8b949e" points="264.26,-197.16 254.24,-193.72 259.93,-202.66 264.26,-197.16"/>
<text xml:space="preserve" text-anchor="middle" x="682.27" y="-281.88" font-family="JetBrains Mono" font-size="10.00" fill="#8b949e">columns · joins · metrics</text>
</g>
<!-- setup -->
<g id="node10" class="node">
<title>setup</title>
<path fill="#21262d" stroke="#30363d" d="M606.88,-513.75C606.88,-513.75 443.12,-513.75 443.12,-513.75 437.12,-513.75 431.12,-507.75 431.12,-501.75 431.12,-501.75 431.12,-486.25 431.12,-486.25 431.12,-480.25 437.12,-474.25 443.12,-474.25 443.12,-474.25 606.88,-474.25 606.88,-474.25 612.88,-474.25 618.88,-480.25 618.88,-486.25 618.88,-486.25 618.88,-501.75 618.88,-501.75 618.88,-507.75 612.88,-513.75 606.88,-513.75"/>
<text xml:space="preserve" text-anchor="middle" x="525" y="-499.3" font-family="JetBrains Mono" font-size="11.00" fill="#8b949e">schema_docs.yaml · metrics.yaml</text>
<text xml:space="preserve" text-anchor="middle" x="525" y="-483.55" font-family="JetBrains Mono" font-size="11.00" fill="#8b949e">encodings · DDL extract</text>
</g>
<!-- setup&#45;&gt;recon -->
<g id="edge14" class="edge">
<title>setup&#45;&gt;recon</title>
<path fill="none" stroke="#8b949e" d="M525,-473.93C525,-457.46 525,-433.37 525,-414.2"/>
<polygon fill="#8b949e" stroke="#8b949e" points="528.5,-414.24 525,-404.24 521.5,-414.24 528.5,-414.24"/>
<text xml:space="preserve" text-anchor="middle" x="552" y="-446.75" font-family="JetBrains Mono" font-size="10.00" fill="#8b949e">make recon</text>
</g> </g>
</g> </g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -256,7 +256,8 @@
<a onclick="show('architecture')">Architecture</a> <a onclick="show('architecture')">Architecture</a>
<a onclick="show('plan')">Plan</a> <a onclick="show('plan')">Plan</a>
<a onclick="show('analyses')">Analyses</a> <a onclick="show('analyses')">Analyses</a>
<a onclick="show('tools')">Tools</a> <a onclick="show('tools')">Atomic actions</a>
<a onclick="show('recon')">Recon</a>
<a onclick="show('runtime')">Runtime</a> <a onclick="show('runtime')">Runtime</a>
<a onclick="show('run')">Run it</a> <a onclick="show('run')">Run it</a>
<a onclick="show('decisions')">Decisions</a> <a onclick="show('decisions')">Decisions</a>
@@ -278,27 +279,53 @@
</p> </p>
<p> <p>
The shape mirrors the JD's three building blocks The shape mirrors the JD's three building blocks
(<i>text-to-SQL, reasoning agents, planners</i>) as three distinct (<i>text-to-SQL, reasoning agents, planners</i>) as distinct layers
layers rather than one omnibus prompt. That gives every part a name rather than one omnibus prompt<b>Plan</b> (L3), <b>Analyses</b> (L2),
and a tight contract, and lets each Analysis pick the simplest and deterministic <b>atomic actions</b> (L1) — resting on a <b>recon</b>
reasoning pattern (CoT, ReAct, plan-and-execute) for what it does foundation (L0): the dataset's typed knowledge graph. That gives every
instead of forcing one over everything. 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>
<p>
The load-bearing decision: <b>the LLM never authors SQL.</b> It picks a
typed shape from finite, recon-derived sets; the composer walks recon and
emits the query deterministically. Columntable binding, joins, and
quoting are graph traversals, not model guesses — so a question the system
can't express fails cleanly instead of producing plausible-but-wrong SQL.
</p> </p>
</div> </div>
</section> </section>
<section id="architecture" class="section"> <section id="architecture" class="section">
<h2>Architecture</h2> <h2>Architecture</h2>
<p class="lede">Three layers — Plan composes Analyses; Analyses are mini-agents; Tools are deterministic.</p> <p class="lede">Four layers — Plan composes Analyses; Analyses are mini-agents; atomic actions are deterministic; recon is the foundation they all consult.</p>
<object data="graphs/architecture.svg" type="image/svg+xml" class="diagram"></object> <object data="graphs/architecture.svg" type="image/svg+xml" class="diagram"></object>
<dl> <dl>
<dt>Plan (L3)</dt> <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> <dd>One LLM call that turns a question into an ordered set of Analysis invocations — each with args, an intended interpretation, and an optional fallback. It sees only brief table/metric names and the Analysis catalog, never raw DDL. Stateless: it emits a plan and never observes execution.</dd>
<dt>Analyses (L2)</dt> <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> <dd>Self-contained mini-agents. Each picks the simplest reasoning pattern it needs — CoT for a one-shot move, ReAct for ones that iterate over results, plan-and-execute for compound ones — and owns its loop behind a uniform contract, <code>run(args, question) → Finding</code>. The LLM work lives here: choosing a typed <code>Pick</code> and interpreting rows, never writing SQL.</dd>
<dt>Tools (L1)</dt> <dt>Atomic actions (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> <dd>Deterministic, no LLM. <code>compose(pick, recon) → SQL</code> authors the query by walking recon's join graph; <code>execute_sql</code> runs it read-only with a row cap and timeout. Invalid SQL isn't expressible — the composer binds columns and joins from recon, not from model output.</dd>
<dt>Recon (L0)</dt>
<dd>The dataset's typed knowledge graph — tables, columns, metrics, relationships, storage encodings — merged from sparse human YAML and extracted DDL. The foundation every layer above consults. (Sometimes called L4: the setup that exists before any run, not a step inside one.)</dd>
</dl> </dl>
<div class="prose">
<h3>How the layers interconnect</h3>
<p>
The layers read <b>general → specific</b> (Plan → Analyses → atomic
actions), but execution is a graph, not a one-way cascade. Later steps
depend on earlier <i>intermediate results</i>: <code>drill_down</code>
loops, choosing its next dimension from the previous slice's rows; a
step's <b>fallback</b> Analysis fires based on whether the primary's
Finding came back empty or errored. And <b>L0 recon is consulted at every
level</b> — the planner reads its brief names, the <code>Pick</code>
validates against it, the composer walks it. So the layers are
interconnected through both the data they share (recon) and the results
they pass forward.
</p>
</div>
</section> </section>
<section id="plan" class="section"> <section id="plan" class="section">
@@ -306,13 +333,23 @@
<p class="lede">L3 — one LLM call that decides which Analyses to run.</p> <p class="lede">L3 — one LLM call that decides which Analyses to run.</p>
<div class="prose"> <div class="prose">
<p> <p>
The planner gets the user question, the metric catalog, a schema The planner (<code>api/plan/planner.py</code>) gets the user question
overview, and the catalog of Analyses. It returns a structured plan: plus three context blocks rendered from recon: brief table names, brief
an ordered list of Analysis invocations with arguments and an intended metric names, and the catalog of Analyses with their <code>args_schema</code>.
interpretation. It does not call tools or query data itself — it's a Deliberately no DDL — it decides <i>which Analyses to run</i>, not how to
composition step. query. One LLM call returns a structured <code>Plan</code>.
</p>
<p>Each step in the plan carries:</p>
<ul>
<li><code>analysis</code> — which Analysis to invoke (must be in the registry).</li>
<li><code>args</code> — the arguments dict, shaped by that Analysis's <code>args_schema</code>.</li>
<li><code>why</code> — a one-line rationale, surfaced in the live trace.</li>
<li><code>fallback</code> — an optional alternate Analysis the runtime runs if the primary returns empty or errors.</li>
</ul>
<p>
The planner never calls a tool or touches data — it's a pure composition
step. The runtime (below) walks the steps in order.
</p> </p>
<p><i>To be expanded as the planner is built out.</i></p>
</div> </div>
</section> </section>
@@ -321,32 +358,86 @@
<p class="lede">L2 — each Analysis is its own mini-agent.</p> <p class="lede">L2 — each Analysis is its own mini-agent.</p>
<div class="prose"> <div class="prose">
<p> <p>
Analyses are the agent layer. Each one wraps a small piece of analytical Analyses are the agent layer. Each wraps a piece of analytical intent and
intent (<i>compare two periods</i>, <i>drill down by dimension</i>, decides how to satisfy it — sometimes one LLM call is enough, sometimes a
<i>find outliers</i>) and decides how to satisfy it — sometimes one ReAct loop over query results is the right fit. The base class
LLM call is enough, sometimes a ReAct loop over Tool results is the (<code>api/analyses/base.py</code>) enforces only a uniform external
right fit. The Analysis base class only enforces a uniform external contract, <code>run(args, question) → Finding</code>; the internal
contract (<code>run(state) → Finding</code>); the internal pattern is pattern is a per-Analysis decision and no framework types leak out.
a per-Analysis decision.
</p> </p>
<p><i>Per-Analysis pattern table coming as each Analysis is implemented.</i></p> <p>
Within an Analysis, a free-form question becomes a typed <code>Pick</code>
via one LLM call (<code>pick_for_question</code>) over recon-narrowed
candidate sets — that is the only place the model makes a structural
choice, and it chooses from finite known sets, not free text. The
<code>Pick</code> then goes to the deterministic composer (L1).
</p>
<h3>Implemented Analyses</h3>
<dl>
<dt>direct_answer</dt>
<dd>CoT, one shot. <code>pick → compose → execute → interpret</code>. For single-query lookups and aggregations.</dd>
<dt>compare_periods</dt>
<dd>CoT over two queries. One Pick, cloned with two period filters → <code>compose ×2 → execute ×2 → interpret</code> the pair.</dd>
<dt>drill_down</dt>
<dd>ReAct loop. <code>decide_next</code> (LLM) picks a dimension from the candidate list → build Pick → <code>compose → execute</code> → loop on the slice's results. Zero LLM in the slice path itself — only the dimension choice is a model decision, and it's guarded against the candidate list.</dd>
<dt>find_outliers · correlate</dt>
<dd><i>Planned.</i> Additive — new Analyses register without touching the runtime or the layers below.</dd>
</dl>
</div> </div>
</section> </section>
<section id="tools" class="section"> <section id="tools" class="section">
<h2>Tools layer</h2> <h2>Atomic actions</h2>
<p class="lede">L1 — deterministic primitives.</p> <p class="lede">L1 — deterministic, no LLM inside. The composer is the SQL author.</p>
<div class="prose"> <div class="prose">
<p> <p>
Tools have no LLM reasoning inside them. <code>text_to_sql</code> is There is no LLM in this layer — and notably no <code>text_to_sql</code>
the one exception that uses an LLM, but tightly: schema-RAG context, tool. An earlier design had the model author SQL with schema-RAG context,
sqlglot validation, retry on parse/runtime errors. <code>execute_sql</code> sqlglot validation, and retries; it was removed once the composer landed
runs against Postgres read-only with a row cap and timeout. (see <b>Decisions</b>). The two atomic actions are:
<code>retrieve_schema</code> and <code>retrieve_metric_definition</code> </p>
power the semantic layer. <code>python_sandbox</code> handles stats <dl>
work that's awkward in SQL. <dt>compose</dt>
<dd><code>compose(pick, recon) → SQL</code> (<code>api/composer/</code>). Builds one sqlglot expression tree from a typed <code>Pick</code> by walking recon: resolve the metric's table, bind each group-by/filter column to its owning table, compute join paths, render filters and dates, emit quoted SQL. Split by concern — <code>aliases</code>, <code>joins</code>, <code>filters</code>, <code>metrics</code>, <code>dates</code>, <code>order</code>, <code>encodings</code>. If a <code>Pick</code> can't be expressed against recon it raises <code>PickValidationError</code>; it never fabricates SQL.</dd>
<dt>execute_sql</dt>
<dd><code>execute_sql(sql) → rows</code> (<code>api/tools/execute_sql.py</code>). Runs read-only against Postgres (<code>SELECT</code>/<code>WITH</code> only) with a row cap and a statement timeout, returning columns + rows + truncation metadata.</dd>
</dl>
<p>
Because columntable binding and joins come from recon rather than model
output, the composer <i>cannot</i> emit SQL that references a column that
doesn't exist. The validator that used to catch the model's mistakes after
the fact is demoted to a paranoid self-check on the composer.
</p>
</div>
</section>
<section id="recon" class="section">
<h2>Recon</h2>
<p class="lede">L0 — the dataset's typed knowledge graph, and the only dataset-specific surface.</p>
<div class="prose">
<p>
Recon (<code>api/recon/</code>) is the foundation every other layer
consults. It's built in two stages and cached as <code>recon.json</code>:
</p>
<ul>
<li><b>DDL extraction</b> — SQLAlchemy introspects Postgres for tables, columns, types, nullability → <code>extracted_schema.json</code> (auto, gitignored).</li>
<li><b>Augmentation merge</b> — sparse human YAML in <code>api/datasets/&lt;name&gt;/</code> (<code>schema_docs.yaml</code> descriptions + relationships, <code>metrics.yaml</code> SQL fragments, optional <code>encodings</code>) merges onto the extracted schema.</li>
</ul>
<p>
The merged <code>Recon</code> exposes the queries downstream code needs
without going back to the model: <code>resolve_column</code> (bare or
<code>table.col</code> → owning table), <code>join_path</code> (shortest
table sequence between two tables), the metric catalog, and storage
<code>encodings</code> (e.g. how a YYMMDD-int column coerces to a date).
Rebuild with <code>make recon</code> after changing the warehouse or the
YAML.
</p>
<p>
Everything dataset-specific lives here. The Plan, Analyses, and atomic
actions reference no dataset name — point them at a different
<code>api/datasets/&lt;name&gt;/</code> and the same machinery answers
questions over a new warehouse.
</p> </p>
<p><i>Per-Tool documentation coming as each Tool is implemented.</i></p>
</div> </div>
</section> </section>
@@ -368,10 +459,26 @@
the nodes <i>are</i>, and that's where the interesting reading is. the nodes <i>are</i>, and that's where the interesting reading is.
</p> </p>
<p> <p>
Concretely: <code>api/runtime/</code> contains the langgraph builder Concretely the graph is small and linear:
and event tap. <code>api/plan/</code>, <code>api/analyses/</code>, and </p>
<code>api/tools/</code> contain the domain code — and those files <pre><code>START → plan → execute → synthesize → END</code></pre>
don't expose langgraph types in their public interfaces. <p>
<code>plan</code> runs the planner; <code>execute</code> walks the plan
steps, dispatching each to its Analysis (and to a fallback Analysis when
the primary returns empty or errors); <code>synthesize</code> folds the
Findings into one answer. The whole run streams SSE events
(<code>run_start</code>, <code>plan_ready</code>,
<code>analysis_start/end</code>, <code>tool_call_start/end</code>,
<code>analysis_fallback</code>) that drive the live trace UI, and every
LLM call opens a named Langfuse generation span.
</p>
<p>
<code>api/runtime/runner.py</code> is the one place langgraph appears —
<code>StateGraph</code> is imported there and nowhere else.
<code>api/plan/</code>, <code>api/analyses/</code>,
<code>api/composer/</code>, and <code>api/tools/</code> contain the
domain code, and none of those files expose langgraph types in their
public interfaces.
</p> </p>
</div> </div>
</section> </section>
@@ -398,7 +505,8 @@ open http://nvi.local.ar</code></pre>
<h2>Architecture decisions</h2> <h2>Architecture decisions</h2>
<div class="prose"> <div class="prose">
<ul> <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>The LLM never authors SQL.</b> It picks a typed shape from finite, recon-derived sets; the composer walks recon and emits SQL deterministically. Schema-in-the-prompt is a <i>suggestion</i> the model can ignore; composition makes it a <i>constraint</i>. The trade is fewer question shapes for correctness by construction. (See <code>def/schema-as-constraint.md</code>.)</li>
<li><b>Four layers, not one omnibus prompt.</b> Plan composes Analyses; each Analysis picks its own reasoning pattern (CoT / ReAct / plan-and-execute); atomic actions are deterministic; recon is the shared foundation. 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>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>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>Langfuse hosted independently</b> by the <code>lng</code> project; reached over WireGuard. Same env var works in dev and any future deploy.</li>
@@ -409,9 +517,17 @@ open http://nvi.local.ar</code></pre>
<section id="evals" class="section"> <section id="evals" class="section">
<h2>Evals</h2> <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> <p class="lede">BIRD <code>financial</code> dev set, run by <code>api/evals/run_evals.py</code> (<code>make evals</code>), traces pushed to Langfuse.</p>
<div class="prose"> <div class="prose">
<p><i>To be populated as the agent stack lands.</i></p> <p>
BIRD ships golden question/SQL pairs, so the eval set is free and
verifiable. Each run replays questions through the full stack
(plan → analyses → compose → execute) and records the trace in Langfuse
for inspection — token usage per layer, which Analysis fired, the composed
SQL. The composer refactor's own check is the unit suite
(<code>uv run pytest</code>): golden SQL strings plus validation
rejections, so structural correctness is pinned independent of the model.
</p>
</div> </div>
</section> </section>

View File

@@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, nextTick, ref, watch } from 'vue' import { nextTick, ref, watch } from 'vue'
import { Panel } from 'soleprint-ui' import { Panel, SplitPane } from 'soleprint-ui'
import { useRunStream, type GraphNode, type ToolCall } from '../composables/useRunStream' import { useRunStream, type GraphNode, type ToolCall } from '../composables/useRunStream'
const question = ref('How many loans are currently in debt (status D)?') const question = ref('How many loans are currently in debt (status D)?')
@@ -27,11 +27,6 @@ function toggleNode(id: string) {
expanded.value = s expanded.value = s
} }
// Trace pane grows to 2/3 when the run gets visually dense.
const traceDense = computed(() =>
graphNodes.value.length > 4 || toolCalls.value.length > 6,
)
watch(() => log.value.length, async () => { watch(() => log.value.length, async () => {
await nextTick() await nextTick()
if (logEl.value) logEl.value.scrollTop = logEl.value.scrollHeight if (logEl.value) logEl.value.scrollTop = logEl.value.scrollHeight
@@ -53,26 +48,30 @@ function statusOf(s: string): 'idle' | 'processing' | 'live' | 'error' {
return 'idle' return 'idle'
} }
function focusNode(id: string | null) { // Soft-link: scroll an element into view and flash it. The DOM access lives in
if (!id) return // script (not the template) — Vue templates don't expose `document` as a global.
if (!expanded.value.has(id)) toggleNode(id) function flashInto(elementId: string) {
nextTick(() => { nextTick(() => {
const el = document.getElementById(`node-${id}`) const el = document.getElementById(elementId)
el?.scrollIntoView({ behavior: 'smooth', block: 'center' }) el?.scrollIntoView({ behavior: 'smooth', block: 'center' })
el?.classList.add('flash') el?.classList.add('flash')
setTimeout(() => el?.classList.remove('flash'), 1200) setTimeout(() => el?.classList.remove('flash'), 1200)
}) })
} }
function focusNode(id: string | null) {
if (!id) return
if (!expanded.value.has(id)) toggleNode(id)
flashInto(`node-${id}`)
}
function focusTool(index: number) {
if (index < 0) return
flashInto(`tc-${index}`)
}
function focusFirstTool(id: string) { function focusFirstTool(id: string) {
const first = toolCalls.value.findIndex(c => c.analysisId === id) focusTool(toolCalls.value.findIndex(c => c.analysisId === id))
if (first < 0) return
nextTick(() => {
const el = document.getElementById(`tc-${first}`)
el?.scrollIntoView({ behavior: 'smooth', block: 'center' })
el?.classList.add('flash')
setTimeout(() => el?.classList.remove('flash'), 1200)
})
} }
function toolIndex(call: ToolCall): number { function toolIndex(call: ToolCall): number {
@@ -108,10 +107,15 @@ function nodeClasses(n: GraphNode) {
</script> </script>
<template> <template>
<div :class="['ask-layout', { dense: traceDense }]"> <div class="ask-layout">
<!-- Left / top: ask + answer (the user's surface) --> <!-- Main split: ask/answer (left) | live trace (right). Trace wider by default. -->
<div class="ask-pane"> <SplitPane direction="horizontal" size-mode="ratio" :initial-size="1.4" anchor="second" :min="0.3" :max="3">
<!-- Left: ask + answer (the user's surface) -->
<template #first>
<SplitPane direction="vertical" size-mode="px" :initial-size="340" :min="120" :max="600">
<template #first>
<Panel title="Ask" :status="statusOf(status)"> <Panel title="Ask" :status="statusOf(status)">
<div class="pane-scroll">
<div class="ask-form"> <div class="ask-form">
<textarea v-model="question" rows="3" :disabled="isRunning" <textarea v-model="question" rows="3" :disabled="isRunning"
placeholder="Ask a question about the financial warehouse…" /> placeholder="Ask a question about the financial warehouse…" />
@@ -127,19 +131,28 @@ function nodeClasses(n: GraphNode) {
</button> </button>
</div> </div>
</div> </div>
</div>
</Panel> </Panel>
</template>
<template #second>
<Panel title="Answer" :status="answer ? 'live' : (error ? 'error' : 'idle')"> <Panel title="Answer" :status="answer ? 'live' : (error ? 'error' : 'idle')">
<div class="pane-scroll">
<div v-if="error" class="error-block">{{ error }}</div> <div v-if="error" class="error-block">{{ error }}</div>
<div v-else-if="answer" class="answer-block">{{ answer }}</div> <div v-else-if="answer" class="answer-block">{{ answer }}</div>
<div v-else-if="isRunning" class="muted">Composing…</div> <div v-else-if="isRunning" class="muted">Composing…</div>
<div v-else class="muted">Ask a question to see the answer here.</div> <div v-else class="muted">Ask a question to see the answer here.</div>
</Panel>
</div> </div>
</Panel>
</template>
</SplitPane>
</template>
<!-- Right / bottom: live trace (the demo's surface) --> <!-- Right: live trace (the demo's surface) -->
<div class="trace-pane"> <template #second>
<SplitPane direction="vertical" size-mode="ratio" :initial-size="0.8" :min="0.25" :max="3">
<template #first>
<Panel title="Plan" :status="statusOf(status)"> <Panel title="Plan" :status="statusOf(status)">
<div class="pane-scroll">
<div v-if="rationale" class="rationale">{{ rationale }}</div> <div v-if="rationale" class="rationale">{{ rationale }}</div>
<div class="graph"> <div class="graph">
@@ -174,7 +187,7 @@ function nodeClasses(n: GraphNode) {
<div class="subgraph-label">tool calls</div> <div class="subgraph-label">tool calls</div>
<div v-for="c in subEvents(n.id)" :key="toolIndex(c)" <div v-for="c in subEvents(n.id)" :key="toolIndex(c)"
:class="['sub-node', c.error ? 'error' : (c.t_ended ? 'done' : 'running')]" :class="['sub-node', c.error ? 'error' : (c.t_ended ? 'done' : 'running')]"
@click.stop="document.getElementById(`tc-${toolIndex(c)}`)?.scrollIntoView({ behavior: 'smooth', block: 'center' })"> @click.stop="focusTool(toolIndex(c))">
<span class="sub-dot" /> <span class="sub-dot" />
<span class="sub-tool">{{ c.tool }}</span> <span class="sub-tool">{{ c.tool }}</span>
<span class="sub-time">{{ c.t_ended ? `${c.t_ended - c.t_started}ms` : '…' }}</span> <span class="sub-time">{{ c.t_ended ? `${c.t_ended - c.t_started}ms` : '…' }}</span>
@@ -184,9 +197,15 @@ function nodeClasses(n: GraphNode) {
</div> </div>
</div> </div>
</div> </div>
</div>
</Panel> </Panel>
</template>
<template #second>
<SplitPane direction="vertical" size-mode="ratio" :initial-size="1" :min="0.25" :max="3">
<template #first>
<Panel title="Tool calls" class="tools-panel"> <Panel title="Tool calls" class="tools-panel">
<div class="pane-scroll">
<div v-if="toolCalls.length === 0" class="muted small">No tool calls yet.</div> <div v-if="toolCalls.length === 0" class="muted small">No tool calls yet.</div>
<div v-for="(t, i) in toolCalls" :key="i" <div v-for="(t, i) in toolCalls" :key="i"
:id="`tc-${i}`" :id="`tc-${i}`"
@@ -204,8 +223,11 @@ function nodeClasses(n: GraphNode) {
<pre v-if="t.output?.preview && t.output.preview.length" class="tool-body rows">{{ JSON.stringify(t.output.preview, null, 2) }}</pre> <pre v-if="t.output?.preview && t.output.preview.length" class="tool-body rows">{{ JSON.stringify(t.output.preview, null, 2) }}</pre>
<div v-if="t.error" class="tool-error">{{ t.error }}</div> <div v-if="t.error" class="tool-error">{{ t.error }}</div>
</div> </div>
</div>
</Panel> </Panel>
</template>
<template #second>
<Panel title="Event log" class="log-panel"> <Panel title="Event log" class="log-panel">
<template #actions> <template #actions>
<label class="wrap-toggle"> <label class="wrap-toggle">
@@ -223,33 +245,24 @@ function nodeClasses(n: GraphNode) {
<div v-if="log.length === 0" class="muted small">No events yet.</div> <div v-if="log.length === 0" class="muted small">No events yet.</div>
</div> </div>
</Panel> </Panel>
</div> </template>
</SplitPane>
</template>
</SplitPane>
</template>
</SplitPane>
</div> </div>
</template> </template>
<style scoped> <style scoped>
.ask-layout { .ask-layout {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
height: calc(100vh - 80px); height: calc(100vh - 80px);
min-height: 0; min-height: 0;
transition: grid-template-columns 0.25s ease;
}
.ask-layout.dense { grid-template-columns: 1fr 2fr; }
.ask-pane, .trace-pane {
display: flex; flex-direction: column; gap: 12px;
overflow: auto; min-height: 0;
} }
.trace-pane { /* Each Panel sits in a fixed-height SplitPane pane whose body is overflow:hidden;
border-left: var(--panel-border); this wrapper gives the panel content its own scroll region. */
padding-left: 16px; .pane-scroll { height: 100%; overflow-y: auto; overflow-x: hidden; }
}
.trace-pane > .tools-panel { flex: 1; min-height: 0; }
.trace-pane > .log-panel { flex: 1; min-height: 200px; }
/* ── Ask form ── */ /* ── Ask form ── */
.ask-form { padding: 12px; display: flex; flex-direction: column; gap: 10px; } .ask-form { padding: 12px; display: flex; flex-direction: column; gap: 10px; }
@@ -431,16 +444,4 @@ textarea {
.log-row.warn .log-level { color: var(--status-processing, #ffc107); } .log-row.warn .log-level { color: var(--status-processing, #ffc107); }
.log-stage { color: #7ab0ff; } .log-stage { color: #7ab0ff; }
.log-msg { color: var(--text-primary); } .log-msg { color: var(--text-primary); }
/* ── Mobile: stack vertically ── */
@media (max-width: 900px) {
.ask-layout, .ask-layout.dense {
grid-template-columns: 1fr;
height: auto;
}
.trace-pane {
border-left: none; border-top: var(--panel-border);
padding-left: 0; padding-top: 16px;
}
}
</style> </style>