Talk to your data¶
A type-safe analytics agent that answers questions about a dataset and checks the arithmetic. It threads three ideas into one workflow: signatures, a code-writing agent, and prompt optimization with a grounded metric.
The model's intent is a domain value¶
The agent never lets the model emit a free-form answer. It first asks the model
to produce a QueryPlan: a validated Scala value describing the computation.
The same type constrains the model's output, decodes its reply, drives the
optimizer's metric, and renders the report.
final case class QueryPlan(
agg : Agg,
column : Option[String],
groupBy : List[String],
filters : List[Filter],
timeRange : Option[TimeRange],
sort : Option[Sort],
limit : Option[Int],
answerKind: AnswerKind
) derives Schema
Fields like agg are enums, so the model can only pick a defined operation. The
prompt cannot drift between "avg", "mean", and "average":
Plan, then compute in code¶
The dataset is thousands of CSV rows, too large to put in a prompt. So the agent
runs in two stages. Plan asks the model for a QueryPlan. Act
hands the plan and the data to an RLM, which writes and runs Python in a
sandbox to compute the answer:
def plan(question: String, instructions: String)(using RuntimeContext): Either[DspyError, QueryPlan] =
planner(instructions)(Question(question, Dataset.schemaDescription)).map(_.output)
def act(plan: QueryPlan, question: String, feedback: String)(using
RuntimeContext
): Either[DspyError, AnalysisResult] =
executor(ActInput(Dataset.csv, describePlan(plan), question, feedback)).map(_.output)
At the iteration limit, RLM asks the model for the outputs with no more code,
so the model can then write the values itself.
A third stage, verify, re-computes the same plan independently on the JVM with a plain Scala query engine and requires the two answers to agree, also for such values. A mismatch triggers a refine attempt.
Optimization with a grounded metric¶
The planner's instruction starts vague. GEPA evolves it against a metric that
is the deterministic Scala engine compared to an answer key. The answer key
comes from hand-written plans, which are labels, and from the same engine. The
offline check compares some answers with a direct computation. The metric thus
gives the optimizer a score from code, with no LLM judge.
Running it¶
OPENAI_API_KEY=sk-... sbt "examples/runMain dspy4s.examples.tutorials.talk_to_your_data.talkToYourDataMain"
GEPA optimization needs only a language model. The full plan-act-verify agent
also needs Deno on the path for the sandbox. An offline
foundation check (tytdSelfCheckMain) runs with neither.
Notes¶
This is a multi-file example: schemas, the dataset and query engine, the agent, and the optimization setup each live in their own file. Full source: talk_to_your_data.