Datasets & augmentation¶
The dataset file¶
Class-agent datasets are JSONL, one AgentExample per line. Every
template's datasets/all.jsonl uses this format. A row carries the whole
case: the question, the documents the answer must come from, any other agent
input, labels, tags and a rubric. All of it reaches the optimization, not
just the question and the answer.
{"id": "policy_05",
"split": "train",
"input": {"current_query": "How long are audit logs kept?",
"context": {"documents": [{"source": "audit.md",
"content": "Audit logs are kept for 400 days on Enterprise and 90 days on Team."}]},
"customer_plan": "team"},
"expected_output": {"response": "On the Team plan, audit logs are kept for 90 days (400 days on Enterprise)."},
"metadata": {"topic": "security", "difficulty": "medium", "must_include": ["90 days"]},
"tags": ["security", "compliance", "plan-specific"],
"rubric": {"groundedness": "Every claim comes from the row's context documents.",
"plan": "Applies the policy to the customer's plan when one is given."}}
| Field | Required | Meaning |
|---|---|---|
id |
recommended | Stable id; generated (example_0003) when missing. Reports join before/after results on it. |
split |
recommended | train, validation, holdout or test (default train). See data roles. |
input |
yes | What the agent receives, unchanged: the same dict input_to_state() gets. The question is read from question, query, current_query or request. |
input.context |
no | Documents the answer must come from (RAG). The value can be {"documents": [...]}, a list, or one string. A document is a string or a dict with content (or text / page_content) and an optional source. |
other input.* keys |
no | Any other agent input: messages (earlier turns), a plan, a locale, a user profile, and so on. |
expected_output |
for scoring | The reference answer ({"response": "..."}, or your structured output). Judges receive it as the reference. |
metadata |
no | Labels metrics need: required facts (must_include), topic, difficulty. |
tags |
no | Grouping labels. Filter with data.filter_by_tags(...), and break results down by tag. |
rubric |
no | Per-dimension criteria, added to the judge's reference. |
from agentomatic.optimize import load_data
data = load_data("agents/support/datasets/all.jsonl") # AgentDataset
data.train, data.validation, data.holdout, data.test # lists of AgentExample
billing = data.filter_by_tags("billing") # any-match
PromptFitter works on the simpler agentomatic.optimize.Dataset of
DataPoint(query, expected_answer, context, metadata, tags). Convert, or
build one directly:
from agentomatic.optimize import Dataset
valset = Dataset(points=[e.to_datapoint() for e in data.validation]) # keeps everything
Dataset.from_list([{"query": "Refund window?", "expected_answer": "30 days",
"context": ["Refunds: 30 days."], "tags": ["billing"],
"metadata": {"invoke": {"customer_plan": "team"}}}])
Dataset.from_jsonl("eval.jsonl") # {"query", "expected_answer", "context", "metadata", "tags"}
trainset, valset = dataset.split(ratio=0.8)
to_datapoint() maps a row as follows:
| Row field | DataPoint field |
|---|---|
input (except the question) |
metadata.invoke, sent to the agent exactly as written |
input.context |
context, as document texts such as "Audit logs are kept … (source: audit.md)" |
metadata |
metadata |
tags |
tags |
rubric |
the expected reference, ahead of the answer, for the judge |
What each stage sees¶
| Stage | Receives |
|---|---|
| Agent | input exactly as written: the question, context, customer_plan and the rest. Evaluation hands it a copy, so an agent that edits its input never changes the dataset. |
| Metrics | The whole row (example.input, .metadata, .tags, .rubric), the same inside fit(), in evaluate() and nested in a composite. |
| LLM judges | Question, answer, expected answer plus rubric, and the context documents. If the row has no documents, they get what the agent retrieved instead. Inside fit() / evaluate() they also get the other inputs, the metadata and the tags. |
| Rewrite model | Every failure, success and dataset sample in its briefing shows Context documents, Other inputs, Example metadata, Tags and what the agent retrieved. It is told to write instructions that use such context and never to copy one row's facts into the prompt. |
| Augmenter | The seed's documents and other inputs (a "Seed context" section). Variations inherit a copy of them, plus the seed's tags (and augmented) and rubric. |
| Report | A context column in the per-example table: tags, documents, inputs and metadata. |
RAG datasets¶
There are two ways for a RAG agent to get its documents. Both work with every stage above.
- The row brings its documents (
input.context.documents). The agent answers from exactly those. The judge checks groundedness against them, and the rewrite model reads them next to each answer. Use this to optimize the answering prompt independently of retrieval quality, or when the gold passages are known. Theragtemplate reads them ininput_to_state(context.documents, flattened todocumentsover REST) and falls back to its own retrieval when there are none. - The agent retrieves for itself. List what it used in the output under
retrieval_context,citations,sourcesordocuments. These can be strings or dicts withcontentandsource. Judges then score groundedness against those, and the rewrite model sees them asRetrieved by the agent.
def input_to_state(self, data):
ctx = data.get("context") or {}
docs = ctx.get("documents") if isinstance(ctx, dict) else ctx
return MyState(question=data.get("current_query", ""), documents=list(docs or []))
def state_to_output(self, state):
return {"response": state.answer, "citations": state.citations} # what it used
Metrics can read the documents too. For example, a groundedness check that
counts how many of the answer's words come from them:
grounded_score.
examples/prompt_optimization/rag_context.py runs the whole loop on rows
that bring their own documents, plan, metadata, tags and rubric. It prints
what each stage sees and reports test scores per tag.
Writing good examples¶
- One fact per expectation you care about. An expected answer is what the judge compares against; state the facts a correct answer must contain, not a vague description.
- Put checkable facts in
metadata(must_include, allowed values) so a deterministic metric can score them. - Cover the question space in every split. Validation and holdout should ask about the same topics as train, in different words.
- Size. Validation decides; it needs enough examples for a real improvement to be significant — 20+ if you can. Seed rows in scaffolded templates are placeholders: replace them before trusting a score.
Augmentation¶
prepare_dataset(augment=True) asks an LLM for variations of your train
rows and adds the ones that are safe to learn from.
from agentomatic.optimize import prepare_dataset
data, written = prepare_dataset(
data,
augment=True,
n_examples=60, # target TOTAL size
strategies=["paraphrase", "formality_shift", "expansion"],
model="omlx/Qwen3.5-9B-MLX-4bit",
llm_base_url="http://127.0.0.1:8000/v1",
llm_api_key="local",
per_call=4,
persist=True,
persist_path="agents/support/datasets/all.augmented.jsonl",
)
print(data.metadata["augment_stats"])
Options¶
| Option | Default | Meaning |
|---|---|---|
augment |
False |
Run augmentation (False: only persist / pass through). |
n_examples |
3 × seed size | Target total size; only train grows. |
strategies |
["paraphrase"] |
See the table below. Strategies rotate call by call, so a short run still mixes them. |
model |
"openai/gpt-4o-mini" |
Augmentation LLM spec (omlx/…, openai/…, ollama/…, …). |
llm_base_url, llm_api_key |
process default | Endpoint for these calls only — no process-wide side effect. |
per_call |
4 |
Variations requested per call. Small batches finish within small local models' output limits; a truncated reply is salvaged, not dropped. |
max_tokens |
4096 |
Reply budget per call. |
strict |
False |
Raise RuntimeError when fewer rows than requested were added (otherwise a warning). |
persist, persist_path, seed_path |
Write the result as JSONL for review. Default path <seed>.augmented.jsonl (or <seed>.prepared.jsonl without augmentation); the seed file is never overwritten. |
Strategies¶
| Strategy | Kind | Produces |
|---|---|---|
paraphrase |
label-preserving | Same question, different words |
perturbation |
label-preserving | Small wording changes, typos, word order |
add_noise |
label-preserving | Irrelevant detail around the question |
simplify |
label-preserving | Shorter, plainer phrasing |
formality_shift |
label-preserving | More or less formal register |
expansion |
new question | Related questions on the same topic |
complicate |
new question | Multi-part or harder versions |
adversarial |
new question | Tricky or misleading phrasings |
edge_case |
new question | Boundary conditions |
Label-preserving rows keep the seed's expected_output, metadata and
input keys, so every metric scores them exactly like the seed. New
question rows get an LLM-written answer in the seed's schema — review them
before trusting scores computed on them.
What keeps augmented data safe¶
- Only train grows; validation, holdout and test are untouched, and without a train split nothing is generated.
- Generated questions that duplicate an existing one, or are ≥ 90 % similar to any validation / holdout / test question, are dropped (a paraphrase of a test question in train would inflate every score measured on it).
- The generator sees each seed's documents and other inputs (a "Seed
context" section) and is told to ask only what those documents answer.
Every variation inherits a copy of its seed's inputs, rubric and tags (plus
augmented), so a RAG row stays answerable. - Each new row records
metadata.source = "augment", itsstrategyand itsparent_id. data.metadata["augment_stats"]recordsrequested,calls,parsed,duplicates,near_duplicates,empty_calls,schema_mismatch,addedandby_strategy; the report shows them.
Runnable: data_augmentation.py, or
python examples/prompt_optimization/train.py --augment.
Generating a seed dataset from scratch¶
DataSynthesizer can draft a first dataset from a description. Always review
it and keep a human-written validation set.
from agentomatic.optimize import DataSynthesizer
synth = DataSynthesizer(model="omlx/Qwen3.5-9B-MLX-4bit", base_url="http://127.0.0.1:8000/v1")
dataset = await synth.generate(
description="A support assistant that explains refunds and account settings.",
n_samples=40,
categories=["refunds", "security"],
)
dataset.to_jsonl("seed.jsonl")