recon-driven sql composer; pick → compose → execute; llm out of structural sql
This commit is contained in:
@@ -13,12 +13,12 @@ from typing import Any
|
||||
|
||||
from api import langfuse_client as lf
|
||||
from api.analyses.drill_down.types import Slice
|
||||
from api.composer import Pick, compose
|
||||
from api.llm import chat
|
||||
from api.prompts import load, render
|
||||
from api.recon import load_recon
|
||||
from api.runtime import events
|
||||
from api.tools.execute_sql import execute_sql
|
||||
from api.tools.text_to_sql import text_to_sql
|
||||
|
||||
logger = logging.getLogger("nvi.analyses.drill_down.helpers")
|
||||
|
||||
@@ -69,54 +69,25 @@ async def decide_next(question: str, metric: str, dimensions: list[str],
|
||||
|
||||
# ── Slice execution ──
|
||||
|
||||
def build_slice_question(question: str, metric: str, dim: str) -> str:
|
||||
"""Construct a slice question with explicit table/join hints from the
|
||||
recon graph. Stops the LLM from inventing FROM clauses that omit the
|
||||
table that owns the dimension column.
|
||||
"""
|
||||
recon = load_recon()
|
||||
dim_owners = recon.owning_tables(dim)
|
||||
metric_def = recon.metrics.get(metric)
|
||||
metric_table = metric_def.from_table if metric_def else None
|
||||
|
||||
hints: list[str] = []
|
||||
if dim_owners:
|
||||
hints.append(f"- The dimension column `{dim}` lives in table: {', '.join(dim_owners)}.")
|
||||
if metric_table:
|
||||
hints.append(f"- The metric `{metric}` is defined over table `{metric_table}`.")
|
||||
if metric_def and metric_def.filter:
|
||||
hints.append(f" Apply this filter for the metric: {metric_def.filter}.")
|
||||
if metric_def:
|
||||
hints.append(f" Compute the metric as: {metric_def.sql} (use this expression verbatim).")
|
||||
if dim_owners and metric_table and dim_owners[0] != metric_table:
|
||||
path = recon.join_path(metric_table, dim_owners[0])
|
||||
if path:
|
||||
hints.append(f"- Required JOIN path: {' → '.join(path)}.")
|
||||
|
||||
hint_block = ("\n".join(hints) + "\n\n") if hints else ""
|
||||
return (
|
||||
f"{question}\n\n"
|
||||
f"Slice the metric `{metric}` by `{dim}` and return the top rows by metric value.\n\n"
|
||||
f"{hint_block}"
|
||||
f"GROUP BY the dimension. ORDER BY the metric DESC. Limit to top 10."
|
||||
)
|
||||
|
||||
|
||||
async def execute_slice(question: str, metric: str, dim: str, reason: str) -> Slice:
|
||||
"""Generate SQL for one slice, execute it, emit tool-call events
|
||||
around both, and return the Slice."""
|
||||
slice_q = build_slice_question(question, metric, dim)
|
||||
"""Compose SQL for one slice deterministically (metric + dim are already
|
||||
chosen by `decide_next`), execute it, emit tool-call events around both,
|
||||
and return the Slice. Zero LLM calls — recon authors the SQL."""
|
||||
recon = load_recon()
|
||||
pick = Pick(kind="aggregate", metric=metric, group_by=[dim], limit=10)
|
||||
|
||||
await events.publish_current(events.tool_call_start("text_to_sql", input={"question": slice_q}))
|
||||
t2s = text_to_sql(slice_q)
|
||||
await events.publish_current(events.tool_call_start(
|
||||
"compose_sql", input={"pick": pick.to_dict()},
|
||||
))
|
||||
composed = compose(pick, recon)
|
||||
await events.publish_current(events.tool_call_end(
|
||||
"text_to_sql", output={"sql": t2s.sql, "tables": t2s.used_tables},
|
||||
"compose_sql", output={"sql": composed.sql, "tables": composed.used_tables},
|
||||
))
|
||||
|
||||
await events.publish_current(events.tool_call_start(
|
||||
"execute_sql", input={"sql": t2s.sql, "dimension": dim},
|
||||
"execute_sql", input={"sql": composed.sql, "dimension": dim},
|
||||
))
|
||||
result = execute_sql(t2s.sql)
|
||||
result = execute_sql(composed.sql)
|
||||
await events.publish_current(events.tool_call_end(
|
||||
"execute_sql",
|
||||
output={"dimension": dim, "row_count": result.row_count,
|
||||
@@ -125,7 +96,7 @@ async def execute_slice(question: str, metric: str, dim: str, reason: str) -> Sl
|
||||
|
||||
return Slice(
|
||||
dimension=dim,
|
||||
sql=t2s.sql,
|
||||
sql=composed.sql,
|
||||
rows=result.as_dicts()[:20],
|
||||
reason=reason,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user