TAI BUI
Back to curriculum
CapstonePython

Hypothesis Generator

A research agent that asks the same question twice is wasting tokens. Force each draft to land somewhere new.

TIME: ~90 minutesPREREQUISITES: Phase 19 Track A lessons 20-29

Learning Objectives

  • Drive a sampler from a seed prompt and turn its outputs into typed hypothesis records.
  • Ramp the sampler temperature on each pass so the next draft drifts further from the last.
  • Filter near duplicates with a small embedding model and a cosine distance threshold.
  • Rank the survivors with a scoring function that blends novelty, specificity, and testability.
  • Hold every step deterministic so the same seed always produces the same queue.

Why generate, then filter

A generate-then-filter pipeline samples several drafts, drops any that sit too close to one already accepted, and ranks the survivors into a queue. A planner that asks one model one time gets one hypothesis. That is fine for a worked example, but a research loop wants a queue with depth, so when the first hypothesis fails the runner has the next one ready without paying for another full sampling pass.

Two ideas combine to produce that queue. The first is temperature ramping: each pass through the sampler raises the temperature a notch, so later drafts are encouraged to wander. The second is novelty filtering: after each draft, the generator measures the embedding distance from every prior survivor and rejects anything inside the cluster.

The lesson ships a mock language model that returns scripted token sequences for fixed prompts. It is enough to exercise the full path: seed prompt in, temperature ramp applied, candidates parsed, novelty filter run, ranked queue out.

Check: Why generate, then filter0/2
1. What is a generate-then-filter pipeline?Define
2. Why does the generator produce a ranked list of survivors instead of a single hypothesis?Explain

The Hypothesis shape

A Hypothesis is a typed record for one candidate claim and the fields a runner needs to test it.

text
Hypothesis
  id             : int           (monotonic within a run)
  text           : str           (the claim)
  variables      : list[str]     (what changes between conditions)
  metric         : str           (what the runner will measure)
  baseline_ref   : str | None    (which paper or run the comparison cites)
  draft_pass     : int           (which sampler pass produced this)
  temperature    : float         (the sampler setting at draft time)
  novelty_score  : float         (distance from prior survivors, 0..1)
  rank_score     : float         (weighted sum used for ordering)

variables and metric are not free text. The parser pulls them from a tagged response, and the runner in lesson fifty-two reads these fields directly when it builds the experiment config.

baseline_ref is optional but recommended. The evaluator in lesson fifty-three needs a baseline to compare against, and if the hypothesis omits one, the evaluator falls back to the previous run on the same metric.

Check: The Hypothesis shape0/3
1. What is a Hypothesis record in this generator?Define
2. Why are variables and metric parsed out of tags instead of left as free text?Explain
3. A hypothesis is generated without a baseline_ref. What happens downstream?Explain

Architecture

Mermaid diagram

The loop is straightforward. The interesting part is that each box has a hard contract.

Temperature ramp

The temperature ramp is a schedule of n_passes evenly spaced temperatures from t_min to t_max, with step (t_max - t_min) / (n_passes - 1). GeneratorConfig.schedule() produces the values, and each pass calls the sampler at the current one. The mock model honors temperature by switching between a small set of scripted responses keyed on (prompt, temp_bucket). The buckets are half-open ranges: t < 0.35 is bucket 0, t < 0.65 is bucket 1, t < 0.95 is bucket 2, and anything higher is bucket 3. A small change in temperature can therefore land in a different bucket and produce a different draft. In production the sampler would be a real model with temperature=t passed through.

The default schedule is six passes from 0.2 to 1.2. Six is enough to fill the queue without paying for samples that the novelty filter will reject anyway. With real models, a sampler below about 0.2 typically parrots the seed back, and above about 1.2 the responses tend to drift off topic and fail the parser. The mock has no such behavior; those edges are properties of real samplers, not of this code.

Check: Temperature ramp0/5
1. What is the temperature ramp?Define
2. Why is the mock language model keyed on a temperature bucket rather than the raw float?Explain
3. Why does the ramp start at about 0.2 when the sampler is a real model?Explain
4. With the default config (six passes from 0.2 to 1.2), how many scheduled temperatures fall into bucket 1, the range from 0.35 up to 0.65?Apply
5. On a real model, someone raises t_max far above 1.2, and the logs for the last passes now show parse reasons. What is the most likely cause?Analyze

Novelty filter

The novelty filter drops any draft that lands too close to a hypothesis the generator already accepted. After each draft is parsed, the generator embeds the text and compares it against every accepted hypothesis. The embedding is a small hashed bag of word tokens, normalised to unit length, and cosine distance between two unit vectors is 1 - dot(a, b). A draft passes if its minimum distance to any prior survivor is at least novelty_threshold, which defaults to 0.25. It is rejected as a duplicate when that distance is below the threshold.

The hashed embedding is plain: deterministic, zero dependencies, and enough to catch the obvious case of two drafts that share most of their nouns. A production deployment would swap in a small sentence model, and the interface stays the same.

Check: Novelty filter0/6
1. What is the hashed embedding that the novelty filter uses?Define
2. When does the novelty filter reject a draft?Define
3. Why does the filter take the minimum distance to the accepted drafts instead of the average?Explain
4. The novelty threshold is 0.25. Three drafts have minimum distances to prior survivors of 0.30, 0.25 and 0.24. Which are rejected as duplicates?Apply
5. Two drafts use the same words in a different order and make opposite claims. What does the filter do with the second one?Analyze
6. In production, paraphrases that share few nouns keep passing the filter. What should you change first?Design

Rank score

The rank score is a weighted sum of three sub scores.

text
rank_score = w_novelty * novelty_score
           + w_specificity * specificity_score
           + w_testability * testability_score

novelty_score is the minimum embedding distance from prior survivors. specificity_score is the count of concrete variables in the hypothesis divided by a target count. testability_score is one if the hypothesis specifies both a metric and a baseline, half if it has only a metric, and zero otherwise.

Default weights are 0.4, 0.3, 0.3. They live in the generator config, so a downstream lesson can shift them without forking the code.

Check: Rank score0/7
1. What does the specificity_score measure?Define
2. What does the testability_score measure?Define
3. Which three components combine in the rank score?Define
4. Why do the weights 0.4, 0.3 and 0.3 live in the generator config instead of inside the scoring code?Explain
5. A draft has two variables, a metric but no baseline, and a novelty score of 0.5. The target variable count is 3 and the weights are the defaults. What is its rank score?Apply
6. A draft has one variable, a metric, and a baseline. The target variable count is 3, its novelty is 1.0, and the weights are the defaults. What is its rank score?Apply
7. Reviewers say the queue is full of exciting but hard-to-verify hypotheses. What is the least invasive fix?Design

Mock language model

The mock language model is a scripted stand-in for a real sampler, with the signature below.

python
class MockLLM:
    def sample(self, prompt: str, temperature: float, seed: int) -> str:
        ...

It is deterministic given a (prompt, temperature, seed) triple. The mock keeps a scripted response table keyed on (prompt_signature, temperature_bucket). If the table has no entry for a key, the sampler returns a fallback that fails the parser, and one of the tests exercises that path.

The seed selects a draft from the scripted bank for that key (bank[seed % len(bank)]), so a bank with several entries gives different drafts for different seeds. In the demo every bank holds exactly one entry, so the seed has no effect on which draft comes back. The generator passes base_seed + pass_index as the seed, and tests pin base_seed to keep results reproducible.

Check: Mock language model0/6
1. What is the MockLLM for?Define
2. Why does the author pin the seed while tuning a setting such as a weight?Explain
3. What happens if every draft from the mock model fails the parser?Apply
4. You run the demo with n_passes=4, then change base_seed from 0 to 7. Every scripted bank holds one entry. What changes in the output?Apply
5. base_seed is 10 and a scripted bank holds three drafts. Which draft does the pass with index 2 return?Apply
6. A teammate adds a trailing question mark to the seed prompt. Every pass now logs a parse reason and the survivors list is empty. What explains it?Analyze

Output queue

run(seed_prompt) returns a tuple (survivors, logs). There is no separate queue object: survivors is a list of Hypothesis records sorted by (-rank_score, id), so higher rank comes first and ties go to the earlier id, and logs is a list of GenerationLog entries, one per pass, recording the temperature, seed, accepted id or reject reason. The runner in lesson fifty-two treats that survivors list as its queue and pops the head and runs the experiment, then the evaluator in lesson fifty-three writes a verdict back. If the verdict says the hypothesis was wrong, the runner pops the next one.

The queue is finite. When it is empty, the orchestrator can either widen the seed prompt and run the generator again or stop and report the budget exhausted.

Check: Output queue0/4
1. What is the queue that the runner consumes in this lesson?Define
2. You call run(seed_prompt) and want the highest-ranked hypothesis plus the reason each rejected draft was dropped. What do you use?Apply
3. After a run, survivors[0] has id 2 and the draft with id 1 sits third. A teammate calls it a sorting bug. What is the right response?Analyze
4. The runner pops the last hypothesis, the verdict says it was wrong, and budget remains. Which step is sound?Design
Author's Note

I'd keep the seed pinned while I tune anything here. The sampler is deterministic given a (prompt, temperature, seed) triple, so two runs can be compared directly. My bet is that the novelty filter is where a real deployment gets adjusted. The hashed embedding catches drafts that share most of their nouns, and I'd swap in a sentence model behind the same interface before I touched the 0.25 threshold.

I'd also leave the weights at 0.4, 0.3, 0.3 until a downstream lesson gives a reason to move them. They sit in the generator config, so changing them later costs no fork of the code.

How to read the code

code/main.py defines Hypothesis, MockLLM, HypothesisGenerator, and a deterministic demo. The generator exposes a single run(seed_prompt) method that returns a tuple (survivors, logs), with survivors sorted by rank; the pass count is read from GeneratorConfig.n_passes rather than passed as an argument. The embedding is a hashed bag of tokens. Novelty and rank are methods on the generator (_novelty and _score), not standalone functions. Nothing depends on numpy; the embedding math is pure stdlib so the lesson stays portable.

code/tests/test_generator.py covers the linear path, the duplicate rejection path, the parser failure path, the temperature ramp boundaries, and the rank ordering.

Check: How to read the code0/1
1. You call GeneratorConfig(n_passes=1, t_min=0.5, t_max=1.2).schedule(). What does it return?Apply

Where this slots in

Lesson fifty produces the queue. Lesson fifty-one takes the head of the queue and runs a literature search to confirm or refute it. Lesson fifty-two takes the same head and runs an actual experiment. Lesson fifty-three reads both outputs and writes a verdict. Together the four lessons make a research loop with no human in it, and a human can step in at any boundary.

Warm-up0/3
1. You sample a language model several times on the same prompt. What does raising the sampling temperature do?Warm-up
2. Which measure is commonly used to compare two text embeddings for similarity?Warm-up
3. An agent harness expects a model reply in a fixed tagged format, and one reply comes back without the tags. What is the sound way to handle it?Warm-up

Code