mirror of https://github.com/razor-ai/soup.git
feat(envs): bundled calculator/retrieval-qa/guess-number rollout envs (v0.71.30)
This commit is contained in:
parent
28e306731f
commit
56db9f79ab
|
|
@ -0,0 +1,18 @@
|
|||
"""Bundled toy rollout environments — v0.71.30.
|
||||
|
||||
Each module exposes a ``rollout(prompts) -> list[{"prompt", "answer"}]`` entry
|
||||
point usable as ``soup_cli.envs.<name>:rollout`` in ``training.rollout_func``
|
||||
(with ``training.rollout_backend='openenv'``). They let the live openenv GRPO
|
||||
rollout path (#125) run out-of-the-box against a deterministic curriculum.
|
||||
|
||||
Honesty: these are deterministic single-shot **seeders**, NOT interactive
|
||||
multi-turn model-in-the-loop episodes — the live openenv contract calls
|
||||
``fn(seed_prompts)`` and does not pass the model/tokenizer. Each env produces a
|
||||
fixed, seeded set of ``{prompt, answer}`` rows scored by the existing
|
||||
``accuracy`` / ``math`` reward. The seed prompts are accepted (contract shape)
|
||||
but their content does not change the generated curriculum.
|
||||
"""
|
||||
|
||||
from soup_cli.envs import calculator, guess_number, retrieval_qa
|
||||
|
||||
__all__ = ["calculator", "guess_number", "retrieval_qa"]
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
"""Calculator tool-use rollout env — v0.71.30.
|
||||
|
||||
Generates a deterministic set of single-operation arithmetic problems as GRPO
|
||||
prompt+answer rows. Score with ``reward_fn='math'`` (or ``'accuracy'``).
|
||||
|
||||
Usage: ``training.rollout_backend='openenv'`` +
|
||||
``training.rollout_func='soup_cli.envs.calculator:rollout'``.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import random
|
||||
from typing import Any
|
||||
|
||||
_SEED = 20730
|
||||
_DEFAULT_ROWS = 64
|
||||
_OPS = ("+", "-", "*")
|
||||
|
||||
|
||||
def rollout(prompts: Any = None) -> list[dict]:
|
||||
"""Return a deterministic list of ``{"prompt", "answer"}`` arithmetic rows.
|
||||
|
||||
``prompts`` (the seed prompts from the GRPO dataset) is accepted for the
|
||||
openenv contract but does not change the generated curriculum — the env is
|
||||
a self-contained deterministic seeder.
|
||||
"""
|
||||
rng = random.Random(_SEED)
|
||||
rows: list[dict] = []
|
||||
for _ in range(_DEFAULT_ROWS):
|
||||
op = _OPS[rng.randrange(len(_OPS))]
|
||||
if op == "*":
|
||||
a = rng.randint(2, 12)
|
||||
b = rng.randint(2, 12)
|
||||
else:
|
||||
a = rng.randint(0, 99)
|
||||
b = rng.randint(0, 99)
|
||||
result = {"+": a + b, "-": a - b, "*": a * b}[op]
|
||||
prompt = (
|
||||
f"What is {a} {op} {b}? Reply with just the number."
|
||||
)
|
||||
rows.append({"prompt": prompt, "answer": str(result)})
|
||||
return rows
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
"""Number-deduction rollout env — v0.71.30.
|
||||
|
||||
Generates deterministic single-shot number-deduction puzzles (constraints that
|
||||
pin a unique integer) as GRPO prompt+answer rows. Score with
|
||||
``reward_fn='math'`` (or ``'accuracy'``).
|
||||
|
||||
Honesty: this is single-shot *deduction*, not interactive guess-and-feedback —
|
||||
the live openenv contract does not pass the model, so a true guessing loop is
|
||||
out of scope (a follow-up once openenv gains a model-in-the-loop hook).
|
||||
|
||||
Usage: ``training.rollout_backend='openenv'`` +
|
||||
``training.rollout_func='soup_cli.envs.guess_number:rollout'``.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import random
|
||||
from typing import Any
|
||||
|
||||
_SEED = 20731
|
||||
_DEFAULT_ROWS = 64
|
||||
|
||||
|
||||
def rollout(prompts: Any = None) -> list[dict]:
|
||||
"""Return a deterministic list of ``{"prompt", "answer"}`` deduction rows.
|
||||
|
||||
Each puzzle states two factors whose product is the answer, plus a range,
|
||||
so the answer is uniquely deducible. ``prompts`` is accepted for the
|
||||
openenv contract but does not change the curriculum.
|
||||
"""
|
||||
rng = random.Random(_SEED)
|
||||
rows: list[dict] = []
|
||||
for _ in range(_DEFAULT_ROWS):
|
||||
a = rng.randint(2, 9)
|
||||
b = rng.randint(2, 9)
|
||||
answer = a * b
|
||||
prompt = (
|
||||
f"I'm thinking of a number between 1 and 100. "
|
||||
f"It equals {a} times {b}. What is the number? "
|
||||
"Reply with just the number."
|
||||
)
|
||||
rows.append({"prompt": prompt, "answer": str(answer)})
|
||||
return rows
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
"""Retrieval-QA rollout env — v0.71.30.
|
||||
|
||||
Generates deterministic short-document + question rows whose answer is a span
|
||||
present in the document, as GRPO prompt+answer rows. Score with
|
||||
``reward_fn='accuracy'`` (the answer span appears in the model's completion).
|
||||
|
||||
Usage: ``training.rollout_backend='openenv'`` +
|
||||
``training.rollout_func='soup_cli.envs.retrieval_qa:rollout'``.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import random
|
||||
from typing import Any
|
||||
|
||||
_SEED = 20732
|
||||
_DEFAULT_ROWS = 64
|
||||
|
||||
# (entity, attribute, value) fact templates — the answer is always ``value``,
|
||||
# which is embedded verbatim in the document so it is a retrievable span.
|
||||
_FACTS = (
|
||||
("The Zephyr rover", "landed in the year", "2031"),
|
||||
("The city of Aldermere", "has a population of", "48000"),
|
||||
("The Blue Comet", "orbits every", "76 years"),
|
||||
("The Marlow Bridge", "spans", "1200 metres"),
|
||||
("The Quill library", "holds", "90000 books"),
|
||||
("Mount Calder", "rises to", "3400 metres"),
|
||||
("The Aster festival", "lasts", "9 days"),
|
||||
("The Nadir mine", "reaches a depth of", "800 metres"),
|
||||
)
|
||||
|
||||
|
||||
def rollout(prompts: Any = None) -> list[dict]:
|
||||
"""Return a deterministic list of ``{"prompt", "answer"}`` retrieval rows.
|
||||
|
||||
Each row embeds a short document containing several facts and asks about
|
||||
one of them; the answer is a span present in the document. ``prompts`` is
|
||||
accepted for the openenv contract but does not change the curriculum.
|
||||
"""
|
||||
rng = random.Random(_SEED)
|
||||
rows: list[dict] = []
|
||||
for _ in range(_DEFAULT_ROWS):
|
||||
# Pick 3 distinct facts as the document; ask about one of them.
|
||||
facts = rng.sample(_FACTS, 3)
|
||||
target = facts[rng.randrange(len(facts))]
|
||||
entity, attribute, value = target
|
||||
doc = " ".join(f"{e} {a} {v}." for (e, a, v) in facts)
|
||||
prompt = (
|
||||
f"Document: {doc}\n"
|
||||
f"Question: What does {entity} {attribute.rstrip()}? "
|
||||
"Reply with just the value."
|
||||
)
|
||||
rows.append({"prompt": prompt, "answer": value})
|
||||
return rows
|
||||
|
|
@ -349,6 +349,69 @@ class TestBuildPrmRewardFn:
|
|||
mod.build_prm_reward_fn(_T(), device="cpu", trust_remote_code=False)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Task 5 — bundled rollout envs
|
||||
# ---------------------------------------------------------------------------
|
||||
_ENV_MODULES = ["calculator", "retrieval_qa", "guess_number"]
|
||||
|
||||
|
||||
class TestEnvs:
|
||||
@pytest.mark.parametrize("modname", _ENV_MODULES)
|
||||
def test_rows_normalise(self, modname):
|
||||
import importlib
|
||||
|
||||
from soup_cli.utils.agent_rollout import _normalise_rollout_rows
|
||||
|
||||
mod = importlib.import_module(f"soup_cli.envs.{modname}")
|
||||
rows = mod.rollout([])
|
||||
assert rows, "env must produce a non-empty row set"
|
||||
norm = _normalise_rollout_rows(rows, "openenv")
|
||||
assert len(norm) == len(rows)
|
||||
for row in rows:
|
||||
assert isinstance(row["prompt"], str) and row["prompt"]
|
||||
assert isinstance(row["answer"], str) and row["answer"]
|
||||
|
||||
@pytest.mark.parametrize("modname", _ENV_MODULES)
|
||||
def test_deterministic(self, modname):
|
||||
import importlib
|
||||
|
||||
mod = importlib.import_module(f"soup_cli.envs.{modname}")
|
||||
assert mod.rollout([]) == mod.rollout([])
|
||||
|
||||
@pytest.mark.parametrize("modname", _ENV_MODULES)
|
||||
def test_rollout_signature_ignores_prompt_content(self, modname):
|
||||
import importlib
|
||||
|
||||
mod = importlib.import_module(f"soup_cli.envs.{modname}")
|
||||
# Passing seed prompts must not crash and stays deterministic.
|
||||
assert mod.rollout(["seed a", "seed b"]) == mod.rollout(["x", "y"])
|
||||
|
||||
def test_calculator_answers_correct(self):
|
||||
import re
|
||||
|
||||
from soup_cli.envs.calculator import rollout
|
||||
|
||||
for row in rollout([]):
|
||||
m = re.search(r"(-?\d+)\s*([+\-*])\s*(-?\d+)", row["prompt"])
|
||||
assert m is not None, row["prompt"]
|
||||
a, op, b = int(m.group(1)), m.group(2), int(m.group(3))
|
||||
expected = {"+": a + b, "-": a - b, "*": a * b}[op]
|
||||
assert row["answer"] == str(expected)
|
||||
|
||||
def test_guess_number_answer_is_int(self):
|
||||
from soup_cli.envs.guess_number import rollout
|
||||
|
||||
for row in rollout([]):
|
||||
assert row["answer"].lstrip("-").isdigit()
|
||||
|
||||
def test_retrieval_qa_answer_in_prompt(self):
|
||||
from soup_cli.envs.retrieval_qa import rollout
|
||||
|
||||
for row in rollout([]):
|
||||
# The answer span must appear in the document/prompt.
|
||||
assert row["answer"] in row["prompt"]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Task 4 — GRPO wiring
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
|
|||
Loading…
Reference in New Issue