Mova0.5 pre-release⌕
☰
home / docs / train / Reading tales: world read, reader-, and qagen

Reading tales: world read*, reader-*, and qagen

Gist

Mova reads public-domain tales and answers questions about them without a neural network. Every step is a deterministic Rust rule or a small linear model whose weights you can inspect:

LLM steps are optional. They call an external LLM command-line client and are marked [LLM] below. Everything else runs offline.

When to use

Setup

The crates form one Cargo workspace at the repo root. Run each one with -p <crate> or through --manifest-path:

cargo run --release --manifest-path world/Cargo.toml -- <command> ...
cargo run --release --manifest-path qagen/Cargo.toml -- <command> ...

The examples below shorten this to world <command> and qagen <command>. world depends on prag, en, math and global by path. qagen depends on prag and en.

en model (required for every parse). The readers parse sentences with the en annotator. Train one on UD English treebanks (we use UD English EWT + ESLSpok train, CC BY-SA 4.0):

cargo run --release --manifest-path en/Cargo.toml -- \
  train <UD_English-EWT/en_ewt-ud-train.conllu> <UD_English-ESLSpok/...-train.conllu> -- models/en.bin
# optional: the shipped model en/models/en.bin is used by default
export WORLD_EN_MODEL=models/en.bin     # world
export QAGEN_EN_MODEL=models/en.bin     # qagen gate (rule 2) and qagen fix

If you skip these variables, the code falls back to a default path that probably does not exist on your machine. Always set them.

Environment variables

variableused bymeaning
WORLD_EN_MODELworldpath to the en model
QAGEN_EN_MODELqagenpath to the en model, for lemmas
FTQAworld read-qa, read-graph, read-events, read-feel, reader-*, ftqa-*FairytaleQA directory (test split) with stories/ and questions/
PRAG_MODEL, PRAG_EFFORTevery [LLM] stepmodel name and effort passed to the LLM client (default effort medium)
DUCKDBqagen selectpath to the duckdb CLI binary
READ_SPANS=1read-trainextractor candidates become all 1–3-word spans instead of syntactic groups
READ_NB (0.3), READ_NEXTread-qa and friendsweight of neighbouring sentences; READ_NEXT=0 disables the "next sentence" rule

Metric names are the usual ones: EM (exact match), F1, ROUGE-L. On the result lines, "snake" is the model, "sentence" (or "whole sentence") is the whole-sentence baseline, "rules" is the rule-based extractor, "learned choice" is the learned extractor, and "learned search" is the learned retriever.

Data

None of the data below ships in the repo. Every path is a placeholder for your own working directory.

Data formats (exact, from the readers in code)

Gutenberg book .txt (world::read::split_book):

<book-dir>/summaries.jsonl: one JSON object per paragraph, written by world read:

{"book":"grimm","story":0,"title":"THE GOLDEN BIRD","par":0,"text":"<paragraph>","summary":"<skeleton; skeleton>"}

book is the file stem, and story and par count from 0. Downstream commands group paragraphs by title, so title is the story key.

<qa-dir>/qa.jsonl: short questions. read-qgen writes this file; read-short and read-train read it:

{"story":"THE GOLDEN BIRD","para":3,"multi":false,"question":"What did the king own?","answer":"a garden"}

<qa-dir>/sets.json and queue.jsonl: written by read-short and updated by read-classify.

check.jsonl (read-check): {"title","par","faithful":0-2,"coverage":0-2,"missing"}.

calls.jsonl: one line per LLM call, written by every [LLM] step. It records tokens, seconds and estimated cost.

FairytaleQA CSV (world::ftqa::read_rows):

$FTQA/stories/<name>.tsv: written by ftqa-import, read by world::ftqa::read_story:

# sec	sent	text
1	1	a hungry fox saw some grapes on a vine .
1	2	he could not reach them .
2	3	so he walked away and said they were sour .

$FTQA/questions/<name>.tsv (world::ftqa::read_qa): exactly 9 tab-separated columns:

# n	attr	ex	ex2	local	secs	question	answer1	answer2
1	action	explicit		local	1	what did the fox see ?	some grapes

secs is a comma-separated list of section numbers.

Directory layout for the FTQA commands. Each command reads stories/*.tsv in sorted order and the matching file in questions/. A story without a questions file is skipped.

Steps

1. Read a book and summarise paragraphs

world read work/books/grimm.txt work/grimm [--limit <stories>]
# → work/grimm/summaries.jsonl
world read-check work/grimm [--limit <paragraphs>]        # [LLM] rates faithfulness and coverage 0–2, 15 per call
# → work/grimm/check.jsonl, calls.jsonl

2. Answer FairytaleQA from raw text (no LLM)

world ftqa-import work/ftqa-test.csv work/ftqa          # CSV → stories/, questions/; prints a summary table
world ftqa-import work/ftqa-valid.csv work/ftqa-valid
FTQA=work/ftqa world read-qa --show 5                   # sentence retrieval + role by question type; ROUGE-L vs whole sentence
FTQA=work/ftqa world read-graph                         # tunes context-graph weights on valid, measures on test
FTQA=work/ftqa world read-events                        # learns a hop policy per question type on valid, tests on test
FTQA=work/ftqa world read-feel                          # explicit or inferred feeling words, valid and test

read-graph, read-events and read-feel take no arguments:

3. Short questions, retriever and extractor

world read-qgen work/grimm work/grimm-qa [--stories 20]   # [LLM] 8 paragraphs per call; resumable
world read-short work/grimm work/grimm-qa [--show 10]     # EM and F1 with no LLM; writes sets.json, queue.jsonl
world read-classify work/grimm-qa [--limit 200]           # [LLM] optional: judge unknown answers, 40 per call
world read-train work/grimm work/grimm-qa work/aesop work/aesop-qa [...]   # pairs: <book-dir> <qa-dir>

read-qgen:

read-train:

4. Question reader v2 over a story world (optional, needs world files)

world reader-parse work/dev-questions.tsv              # lines: type<TAB>explicit|implicit<TAB>question → UD question frame
world reader-span-dev work/ftqa-valid [--show <type>]  # sentence spans on valid, without a world
world reader-ask <name> work/worlds <n> [--facts]      # debug one question
world reader-eval work/worlds work/out [--dev q.tsv] [--skip q.tsv] [--control shuffle|noindex] \
                  [--sample k --seed s --exclude seen.tsv ...]

work/worlds/ holds:

Questions come from $FTQA/questions/<name>.tsv. Each run writes these files into work/out/:

The question files for --dev, --skip and --exclude are TSV files of story<TAB>n.

5. qagen: LLM training questions with gates

qagen select <db.duckdb> work/qg [--paras 300] [--batch 20] [--seed 26] [--rule 2] [--exclude other/paras.jsonl]
qagen prompt work/qg 0                     # show the prompt for batch 0, no call
qagen run    work/qg [--batches 0,1] [--jobs 5] [--max-calls 20]    # [LLM] → raw/bNNN.txt, calls.jsonl
qagen gate   work/qg                       # → qa.tsv, rejects.tsv, params.json (JSON summary on stdout)
qagen sample work/qg [--n 20] [--seed 7] [--weak 0.5]   # → sample.md, or weak.md with --weak

Database. select needs a DuckDB database with tables docs(doc, key, title, author, license) and sentences(doc, para, ord, sent_id, text, comments). The exact query is qagen/sql/paras.sql.

Other entry point. You can also skip select and write paras.jsonl and pick.json yourself:

{"doc":"gutenberg:21:the-fox","book":"gutenberg:21","title":"The Fox","author":"Aesop","para":1,"block":"p",
 "sents":[{"sent_id":"gutenberg:21:the-fox:1","text":"A hungry fox saw some grapes on a vine."},
          {"sent_id":"gutenberg:21:the-fox:2","text":"He could not reach them, so he walked away."}],
 "dialogue":false,"words":19,"batch":0}

Each paras.jsonl record must be on one line; the example above is wrapped for reading.

{"paras":1,"batch":20,"seed":26,"min_words":40,"max_words":250,"min_sents":2,"max_sents":16,"rule":2}

LLM output format. The LLM's raw output (raw/bNNN.txt, or bNNN.retry.txt after the single retry on a format failure) is TSV with 6 columns:

p	type	ex	anchors	question	answer

Rule 2. An explicit answer must be a verbatim span of an anchor sentence. An implicit answer that mostly reuses one anchor's lemmas is flagged doubt.

Output files.

Run behaviour.

qagen fix. These subcommands repair answers in an older rule-1 run. See qagen/src/main.rs.

qagen writes qa.tsv, which is a different format from the qa.jsonl that read-train reads. There is no converter between them at the time of writing.

Example

A tiny offline round trip. All files are hand-made, and no LLM is involved.

mkdir -p work/qa
cat > work/book.txt <<'EOF'
*** START OF THE PROJECT GUTENBERG EBOOK TINY TALES ***

THE DOG AND THE BONE

A dog was carrying a bone in his mouth across a bridge. He looked down and saw his reflection in the water of the river.

He thought it was another dog with a bigger bone. He opened his mouth to bark, and his own bone fell into the river.

*** END OF THE PROJECT GUTENBERG EBOOK TINY TALES ***
EOF
world read work/book.txt work/book
cat > work/qa/qa.jsonl <<'EOF'
{"story":"THE DOG AND THE BONE","para":0,"multi":false,"question":"What was the dog carrying?","answer":"a bone"}
{"story":"THE DOG AND THE BONE","para":1,"multi":false,"question":"Where did the bone fall?","answer":"into the river"}
EOF
world read-short work/book work/qa --show 2

On a similar tiny input, the model answered "a bone" (F1 1.00) and "the river" (F1 0.67 against "into the river"). It printed EM and F1 per question type, and queued "the river" in queue.jsonl as an unknown alternative answer. For a fox fable, a summaries.jsonl summary reads like hungry fox saw fine bunches; vine trained along high trellis.

Gate demo. Write a qagen raw line whose explicit answer is not in the anchor, for example a hungry wolf for "Who was hungry?". qagen gate keeps that line out of qa.tsv and logs it in rejects.tsv at level pair, with the reason "not a span of an anchor sentence". It also adds a note when a paragraph has fewer than 3 accepted questions. That is the gate showing red.

Evaluating honestly

Results at the time of writing, from committed project notes. Treat them as orientation, not as benchmarks.

commandsetupresult
read-qaFairytaleQA, 1007 questionsROUGE-L 0.181, below the whole-sentence baseline at 0.192
read-eventsFairytaleQA test0.199 vs 0.192; adding read-feel feelings gives 0.212
read-train4 books, 750 test questionsrules EM 16.0% / F1 0.216; retriever + extractor EM 24.1% / F1 0.296
reader v2305 held-out questionsROUGE-L 0.294 vs baseline 0.229

Reader v2 controls: with the shuffled world, the state-only answers scored about 0.003; with no index, the score equalled the baseline.

Pitfalls

This page in Markdown → https://md.mova.elamur.ai/free/en/docs/train/reading-tales/