Spaces:
Runtime error
How question generation works
The Oracle's golden rule: the model never decides anything factual. The deterministic engine chooses which attribute to ask about; the model's only job is to phrase that attribute as a friendly question. And even that phrasing is done once at boot and cached, so during a game there are no model calls at all.
Files involved: engine.py (picks the attribute), question_maker.py (phrases &
caches), llm.py (runs the model via llama.cpp), app.py (the turn loop).
The turn loop
flowchart TD
A[answers so far] --> B[engine.filter_candidates]
B --> C{how many left?}
C -->|1| G[guess]
C -->|0| T[discovery / teach]
C -->|many| D[engine.choose_attribute]
D --> E[question_maker.make_question โ instant cache lookup]
E --> F[ask the player]
Each turn, app.py:next_turn():
- Rebuilds the answer facts and calls
engine.filter_candidates()to narrow the candidate list (pure Python, exact). - If 1 candidate remains โ guess; if 0 โ discovery/teach; otherwise:
engine.choose_attribute()picks the unused attribute whose yes/no split is closest to 50/50 (maximum information gain โ it halves the field each time).question_maker.make_question(category, attribute)returns the text for that attribute โ an instant dictionary lookup.
So the "AI question" the player sees is really: engine chooses the attribute โ the cache returns the model's phrasing of it.
Where the model actually runs: boot-time pre-generation
There are only ~42 possible questions (one per attribute, per category). Calling a
model live every turn would be slow on a CPU Space, so instead app.py kicks off a
background thread at startup:
def _boot_warm():
llm.warmup() # download + load the GGUF once
question_maker.prewarm_questions() # generate & cache all questions
prewarm_questions() loops over every category:attribute, asks the model to
phrase it, and writes the result to a cache file (questions_cache.json, stored in
ORACLE_DATA_DIR / the bucket so it survives restarts). It logs timing per
question and a total, e.g.:
[question_maker] animal:long_tail (3.4s): Does this animal have a long tail?
[question_maker] generated 42 questions in 138.0s (3.3s each)
[question_maker] question cache ready: 42 questions (+42 new), style v3
After the first successful boot the cache is full and persisted, so every later restart is instant.
The prompt
question_maker.py asks for simple, clear, kid-friendly yes/no questions. It
passes the attribute's plain-English meaning (from discovery.ATTR_MEANING) rather
than the raw attribute key, forbids using the attribute word itself, and gives a
few tone examples. Temperature is low (0.3) for predictable phrasing.
Turn the fact below into ONE simple yes/no question for a kids' guessing game.
... The {category} either {meaning} โ or not. Ask about exactly that.
Cache versioning
CACHE_VERSION (e.g. "v3") is stored in the cache file. Whenever the prompt or
style changes, bumping this constant makes _cache() discard the old entries so
they regenerate on the next boot โ no manual cleanup needed.
Running the model: llm.py
question_maker calls llm.chat(...), which runs the model through the
llama.cpp runtime in one of two ways:
- In-process (
llama-cpp-python) โ loads the GGUF directly in the Python process. This is the path used on the Space (no server needed). - HTTP (
llama-server) โ a fallback for local dev.
llm.py also caps threads (CPU Spaces over-report cores), wraps each call in a
timeout, warms the model at boot, and logs the active mode
(๐ข MODE = IN-PROCESS llama.cpp โฆ).
Always-works fallback
If the model is disabled (ORACLE_QUESTION_LLM=0), unavailable, or hasn't filled
the cache yet, make_question() returns the built-in phrasing from
engine.ATTR_QUESTIONS (e.g. "Is it a root vegetable?"). The game is therefore
playable with no model at all โ the model only upgrades the wording.
TL;DR
| Concern | Owner |
|---|---|
| Which attribute to ask | engine.choose_attribute (deterministic) |
| Wording of the question | question_maker (model, cached at boot) |
| Running the model | llm.py (llama.cpp, in-process) |
| If the model is missing | built-in phrasing fallback (instant) |