Mova0.5 pre-release⌕
☰
home / docs / qagen — LLM-generated training questions and answers for fairy-tale paragraphs

qagen — LLM-generated training questions and answers for fairy-tale paragraphs

qagen makes training questions: for each paragraph of a public-domain tale, a large model writes question–answer pairs of the FairytaleQA types, which Mova's readers learn from. The large model writes the questions; Mova answers them on its own.

Problem statement: Smart annotator — layers, context, memory, section "Five-year-old test", paragraph "Training questions from the LLM". This is training data for the memory, narrative and motives of the SLM (small language model): it learns to answer from the md graph.

What

For each fairy-tale paragraph the LLM (Opus) writes 3–6 question-answer pairs. Each pair has:

The pairs live in the database, in the qa table (migrations v3–v4 in db/schema.sql). They are loaded by db qa; the view with names is qa_pairs, current pairs without replaced ones are qa_current.

How

qagen select $MOVA_DATA/db/mova.duckdb <dir> [--rule 2] [--exclude <other run dir>/paras.jsonl]
                                                # paragraph selection → paras.jsonl (with batches), pick.json
qagen prompt <dir> 0                            # batch prompt, no call
qagen run    <dir> [--batches 0] [--jobs 5] [--max-calls 20]   # LLM → raw/bNNN.txt, calls.jsonl
qagen gate   <dir>                              # gates → qa.tsv, rejects.tsv, params.json
qagen sample <dir> [--n 20] [--weak 0.5]        # sample for eyeballing → sample.md; explicit pairs with weak support → weak.md
db qa $MOVA_DATA/db/mova.duckdb <dir>/qa.tsv --run <name> --params <dir>/params.json --path <dir>

# fixing the v1 run to rule v2 (src/fix.rs)
qagen fix select <pilot dir> <dir> [--batch 40]   # explicit non-span pairs → items.jsonl, paras.jsonl, select.json, doubt.tsv
qagen fix prompt <dir> 0
qagen fix run    <dir> [--jobs 5] [--max-calls 10]
qagen fix gate   <dir> --replaces <pilot run>    # → qa.tsv, rejects.tsv, changes.md, params.json
db qa $MOVA_DATA/db/mova.duckdb <dir>/qa.tsv --run <name> --replaces <pilot run> --params <dir>/params.json --path <dir>

Reused from existing code:

Database query: sql/paras.sql.

Selection (src/select.rs)

Gates (src/gate.rs; negative controls are in the tests)

LLM output is a 6-column TSV: p type ex anchors question answer.

Batch. A format rejection is the only reason for a retry, and there is exactly one retry. The format is rejected when:

Pair. Only this pair is rejected; the first reason goes to the log:

Paragraph. If there are no lines, the paragraph is rejected. If there are fewer than 3 questions, no implicit pair, or all of one type, a note goes to the log and the pairs stay.

Database (db qa). Checks integrity:

Type, explicitness, source and a non-empty answer are enforced by CHECKs on the qa table. Any error, and nothing in the database changes (tools/db/tests/qa.rs).

Rejection log: rejects.tsv; there are no blind retries. A call error stops the run. A rerun takes only unfinished batches. Ceiling: --max-calls calls per run directory (default 20).

Rule v2: an explicit answer is a verbatim span

Design note: an explicit answer is a verbatim span of an anchor sentence, as in FairytaleQA. That way the SLM learns to extract the answer from the text rather than paraphrase. An implicit answer remains free text. The rule is stored in the run's pick.json ("rule": 2). The pilot lacks this field, so qagen gate on its directory reproduces v1.

Prompt (prompt::RULES_V2). An explicit answer is a span copied from one sentence: the same words in the same order and form, nothing changed, added or skipped inside. Words at the edges may be dropped, and the shortest span that answers the question should be taken. The sentence the span comes from must be among the anchors. An implicit answer is in one's own words and is not a paraphrase of a single sentence. The prompt includes an example: a three-sentence paragraph and four pairs for it.

Span gate (gate::span_reason; pair rejection). The answer and the sentence are normalized:

Then the answer must occur in the anchor sentence as a substring on word boundaries: "the king" in "the king's garden" — yes, "the kin" — no. The rejection reason is one of three:

"Doubtful implicit" (lemma::doubt; a log flag with level doubt, the pair stays). If at least 80% of the content words of an implicit answer are covered by lemmas or forms of a single anchor sentence, it is most likely a paraphrase of an explicit answer. The verbatim gate does not catch this: "because he had tasted the king's white snake" vs "…had tasted the white snake". Lemmas and parts of speech come from en. Content words are NOUN, PROPN, VERB, ADJ, ADV, NUM, INTJ, X.

Negative controls are in the tests gate::span_gate_v2, lemma::doubt_flags_restated_implicit_answers (lemma stub), lemma::doubt_with_en_model (real model) and fix::fix_gate_and_negative_controls.

Fixing the pilot (src/fix.rs)

Explicit pairs of the v1 run that fail the span gate go to the LLM in batches of 40 paragraphs. The prompt contains the paragraph, question, type, anchors and the old answer. The LLM returns an item anchors span line: a verbatim span, or implicit if no sentence states it directly. In that case the pair keeps the old answer but becomes implicit. The source of pairs is the pilot's qa.tsv, i.e. exactly what is in the database.

Output gate:

Database, migration v4. Fixed pairs are a separate run (db qa --replaces <pilot run>) with the same document, paragraph and question number. The new row carries replaces — the qid of the old one. The old row is not deleted; in the same transaction it gets replaced_by — the qid of the new one. Current pairs: view qa_current.

Pilot 26.09.2026

Directory qagen-2026-09-26, run in the database: qagen-2026-09-26 (run 34).

paragraphs300 from 137 tales, all with questions; 148 with dialogue
pairs1,626, i.e. 5.4 per paragraph
explicit / implicit1,045 / 581, i.e. 64.3 / 35.7%; every paragraph has an implicit pair
pairs per paragraphsix — 146 paragraphs, five — 135, four — 18, three — 1
answer6.7 words on average, longest 14
question9.1 words on average
anchors per pair1.22
rejections2 pairs of 1,628 (0.12%); batch rejections and paragraph notes — 0

Distribution by book:

bookparagraphs (with dialogue)pairs
Grimm, #2591125 (62)693
Andersen, #159781 (40)418
Aesop, #2146 (23)258
Kipling, #278146 (23)247
Potter, #148382 (0)10

Cost

There were 15 calls: a pilot batch and a wave of 14 batches, 5 in parallel. A batch has 20 paragraphs. No retries were needed.

time~7 min wall clock (pilot 104 s + wave 312 s); 1,491 s total across calls, i.e. 5.0 s per paragraph and 0.9 s per pair; longest call 128 s
tokensoutput 171,807 including thinking (573 per paragraph, 106 per pair); cache: read 126,934, write 349,830; uncached input 30

The batch prompt is ~5k tokens: rules ~0.9k plus 20 paragraphs. The cache write is mostly claude -p's own system prompt.

Type distribution

typepairs%explicitimplicitFairytaleQA, %
action41225.33951731.6
causal relationship33320.516616727.8
feeling22814.071157~10
character22213.7105117~10
outcome resolution21413.218628~10
setting1308.011812~5
prediction875.4483~5

FairytaleQA shares are taken from the dataset description in Xu et al. 2022. There it is 75% explicit and 25% implicit. We have more implicit pairs (35.7%) because the prompt requires at least one implicit pair per paragraph. Feelings and characters are also more frequent: the paragraph is short, and these are what stands out in it.

Rejections

  1. Aesop, "The North Wind and the Sun": "Who won the contest…? — the Sun", marked implicit. Both words of the answer appear verbatim in sentence 1, so by the gate's rule this is an explicit pair.
  2. Grimm, "Clever Gretel", paragraph 2: the seventh question of the paragraph, of type prediction.

Quality by eye

20 random pairs checked (sample.md, seed 7):

Answer support by anchors can be estimated automatically: it is the share of answer words that occur in the anchor sentences.

So a word-based measure without lemmas often raises false alarms.

Example: "Hansel and Gretel", paragraph 1

typeexplicitnessanchorsquestionanswer
characterexplicit1, 2Who lived by the great forest?a poor wood-cutter, his wife, and children Hansel and Gretel
causal relationshipexplicit3Why could the wood-cutter no longer get daily bread?a great dearth fell on the land
actionexplicit6What was the wife's plan for the children?leave them alone where the forest is thickest
causal relationshipimplicit9What made the wood-cutter finally agree to the wife's plan?she kept nagging him, saying they would all starve
characterimplicit6, 7, 9What kind of person was the wood-cutter's wife?cruel and selfish
feelingexplicit10How did the wood-cutter feel about the plan?very sorry for the poor children

More examples of implicit pairs:

Rule v2, 26.09.2026: fixing the pilot and the remaining paragraphs

Directory: qagen-v2-2026-09-26: the main run in the root, the pilot fix in fix/. Calls went through the API, so the price below is real.

Fixing the pilot

Run in the database: qagen-v2-fix-2026-09-26 (run 35), replacing run qagen-2026-09-26 (run 34).

explicit pilot pairs1,045
passed the span gate as is379 (36.3%)
to fix666 from 288 paragraphs: 644 "not a span", 21 "different punctuation", 1 "not an anchor"
became spans657
became implicit9 — the answer is assembled from two or three sentences, e.g. "Who lived by the great forest? — a poor wood-cutter, his wife, and children Hansel and Gretel" (anchors 1, 2)
gate rejections0; retries 0; unfixed 0
calls8 batches of 40 paragraphs (17–105 items), 5 in parallel, ~2.5 min wall clock; 424 s total
tokensoutput 51,316; cache: read 60,462, write 157,468

In the database, 666 new rows carry replaces, and 666 pilot rows got replaced_by. Document, paragraph, number and question match in every pair. Current pairs: 1,626, as before — 960 pilot rows and 666 fixed ones.

Spans are longer than the old answers: 10.4 words on average vs 6.7 for explicit pilot pairs, longest 43. By length, the 666 answers are distributed as follows:

words1–56–1011–1516–2021+
pairs1142781855435

Long spans are mostly lists that the old answer compressed: "What did the girl take when she ran away?" now has a whole 43-word list as its answer.

By eye, 12 changes (every 60th in fix/changes.md):

Implicit pilot pairs covered by the lemmas of one anchor ("doubtful implicit"): 27 of 581 (4.6%), list in fix/doubt.tsv. Example: "Why did the little match girl still have so many matches? — No one had bought any from her all day." These pairs were not changed.

Run on the remaining paragraphs

Run in the database: qagen-v2-2026-09-26 (run 36). Paragraphs: all eligible ones except the pilot's: qagen select … --paras 5000 --exclude <pilot>/paras.jsonl, batches of 20 in text order.

paragraphs1,140 from 336 tales, all with questions; 878 with dialogue
pairs6,217, i.e. 5.5 per paragraph
explicit / implicit4,172 / 2,045, i.e. 67.1 / 32.9%; an implicit pair exists in 1,137 of 1,140 paragraphs
span gate4,172 explicit proposed, 4,172 (100%) passed on the first try
pairs per paragraphsix — 582 paragraphs, five — 493, four — 65
answer6.3 words on average. Explicit — 6.0: longest 16 words, three longer than 15. Implicit — 6.8, longest 13
question9.2 words on average
anchors per pair1.26
rejections15 pairs of 6,232 (0.24%): 13 — seventh question of the paragraph, 2 — implicit answer verbatim in a sentence. "No implicit" notes — 3, batch rejections — 0
"doubtful implicit"78 of 2,045 (3.8%)

For comparison: of the explicit pilot pairs, written under rule v1, only 36.3% passed the span gate as is.

Distribution by book:

bookparagraphs (with dialogue)pairs
Grimm, #2591382 (312)2,150
Andersen, #1597358 (237)1,878
Aesop, #21251 (210)1,398
Kipling, #2781146 (119)774
Potter, #148383 (0)17

Type distribution is almost the same as in the pilot:

typepairs%explicitimplicitpilot, %
action1,50824.31,4634525.3
causal relationship1,33921.580053920.5
character90614.646743913.7
feeling89814.429460414.0
outcome resolution68111.05829913.2
setting5438.7516278.0
prediction3425.5502925.4

"Doubtful implicit" pairs are mostly near-verbatim paraphrases: "Which way did people look when they heard the bell? — toward the forest" (lemmas of sentence 2 cover 100%), "What will the princess probably do next? — work as a kitchen-maid in the king's palace". The list is the doubt lines in rejects.tsv.

Cost

calls57: a trial batch and a wave of 56 batches, 5 in parallel; no retries
time~21.5 min wall clock for the wave, trial 130 s; 6,332 s total across calls, i.e. 5.6 s per paragraph; longest call 146 s
tokensoutput 757,875 including thinking (665 per paragraph, 122 per pair); cache: read 479,999, write 709,579; uncached input 114

Together with the pilot fix: 65 calls of the allowed 70, 809,191 output tokens, ~26 min wall clock.

Quality by eye: 20 pairs

20 random pairs checked (sample.md, seed 7): 14 explicit and 6 implicit.

Good examples from the sample:

Automatic answer support by anchors is now almost always 100%. The exception is 2 pairs with a possessive: "his wife" in "his wife's name" (weak.md, --weak 1.0). The span gate treats the apostrophe as a word boundary, while the word-based measure does not.

Database

Before the changes: a copy mova.duckdb.bak-before-qa-v2-2026-09-26. Then db migrate (v4), and db qa for the fix and for the new run. After the changes db verify reported 73 files byte-for-byte identical, counters 73, size as in the card for 29 of 29.

Current pairs (qa_current): 7,843 across all 1,440 eligible paragraphs: 5,208 explicit (66.4%), 2,635 implicit. Every current explicit answer is a span of an anchor sentence: 379 pilot pairs passed the gate as is, 657 were fixed, 4,172 come from the new run.

Run files

filecontents
paras.jsonlselected paragraphs with sentences (sent_id), context and batch number
pick.jsonselection parameters and rule (rule; absent — v1)
raw/bNNN.prompt.txt, raw/bNNN.txtprompt and raw LLM output; the retry, if any, is raw/bNNN.retry.txt
calls.jsonlaccounting: time, tokens and price per call
qa.tsvaccepted pairs for db qa; anchors are comma-separated sent_ids
rejects.tsvlog of rejections and flags: level batch, para, pair, note or doubt ("doubtful implicit")
params.jsonaccounting and distributions; stored in the database's runs.params
sample.md, weak.mdsamples for eyeballing
run.logexecution log: one line per batch

The fix (fix/) has the same paras.jsonl, raw/, calls.jsonl, qa.tsv, rejects.tsv, params.json and run.log, plus:

filecontents
items.jsonlexplicit pairs to fix, with the span gate's rejection reason and batch number
select.jsonselection summary: how many passed as is, reasons, "doubtful implicit" pairs of the old run
doubt.tsvimplicit pairs of the old run flagged "doubtful implicit"
changes.mdeach change: question, before → after, anchors, explicitness

Next steps

Licence

Elamur Cosmic License (Elamur Cosmic License). Third-party data keeps its own terms, as noted where it is used.

This page in Markdown → https://md.mova.elamur.ai/free/en/docs/qagen/