Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/style.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@ jobs:
- name: Lint
uses: astral-sh/ruff-action@v4.1.0
with:
version: "0.15.21"
version: "0.16.0"
- name: Format
uses: astral-sh/ruff-action@v4.1.0
with:
version: "0.15.21"
version: "0.16.0"
args: "format --check"
ty:
name: Ty
Expand Down
6 changes: 3 additions & 3 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,19 @@ repos:
hooks:
- id: ruff-check
name: ruff check
entry: uv run ruff check --fix
entry: uv run --locked ruff check --fix
language: system
types_or: [python, pyi]
exclude: ^environments/.*/tasks/
- id: ruff-format
name: ruff format
entry: uv run ruff format
entry: uv run --locked ruff format
language: system
types_or: [python, pyi]
exclude: ^environments/.*/tasks/
- id: ty
name: ty (ci parity)
entry: uv run --python 3.13 ty check verifiers
entry: uv run --locked --python 3.13 ty check verifiers
language: system
pass_filenames: false
stages: [pre-push]
14 changes: 5 additions & 9 deletions docs/v0/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -263,26 +263,22 @@ prime eval run my-environment -m openai/gpt-4.1-mini -n 5
# my_environment.py
import verifiers as vf


def load_environment(**kwargs):
"""Load the environment."""
dataset = vf.load_example_dataset("dataset_name")
parser = vf.XMLParser(fields=["reasoning", "answer"])

def reward_func(parser, completion, answer, **kwargs):
return 1.0 if parser.parse_answer(completion) == answer else 0.0

rubric = vf.Rubric(
funcs=[reward_func, parser.get_format_reward_func()],
weights=[1.0, 0.2],
parser=parser
)

return vf.SingleTurnEnv(
dataset=dataset,
parser=parser,
rubric=rubric,
**kwargs
)

return vf.SingleTurnEnv(dataset=dataset, parser=parser, rubric=rubric, **kwargs)
```

## Quick Reference
Expand Down
75 changes: 53 additions & 22 deletions docs/v0/environments.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,15 @@ The simplest single-turn environments need only a dataset of tasks and a reward
import verifiers as vf
from datasets import Dataset


def load_environment():
# Your task data
dataset = Dataset.from_list([
{"prompt": [{"role": "user", "content": "What is 2+2?"}], "answer": "4"},
{"prompt": [{"role": "user", "content": "What is 3*5?"}], "answer": "15"},
])
dataset = Dataset.from_list(
[
{"prompt": [{"role": "user", "content": "What is 2+2?"}], "answer": "4"},
{"prompt": [{"role": "user", "content": "What is 3*5?"}], "answer": "15"},
]
)

# Your reward function
async def correct_answer(completion, answer) -> float:
Expand Down Expand Up @@ -84,10 +87,12 @@ Depending on what your environment needs, you can include `answer`, `info`, both
When using `info`, prefer using JSON strings if rows may have different schemas, e.g. different fields or nested structures:

```python
dataset = Dataset.from_list([
{"prompt": [...], "info": '{"type": "math", "difficulty": 3}'},
{"prompt": [...], "info": '{"type": "code", "language": "python"}'},
])
dataset = Dataset.from_list(
[
{"prompt": [...], "info": '{"type": "math", "difficulty": 3}'},
{"prompt": [...], "info": '{"type": "code", "language": "python"}'},
]
)
```

These are parsed into a `dict` by the environment when running rollouts.
Expand All @@ -97,9 +102,11 @@ These are parsed into a `dict` by the environment when running rollouts.
The examples above use `prompt` directly, providing a list of messages ready to send to the model. Alternatively, you can provide a `question` column containing a string, and the environment will wrap it in a user message:

```python
dataset = Dataset.from_list([
{"question": "What is 2+2?", "answer": "4"},
])
dataset = Dataset.from_list(
[
{"question": "What is 2+2?", "answer": "4"},
]
)
```

You can also pass a `system_prompt` to the environment, which prepends a system message:
Expand All @@ -117,7 +124,7 @@ Together, these construct the full prompt:
```python
[
{"role": "system", "content": "You are a helpful math tutor."},
{"role": "user", "content": "What is 2+2?"}
{"role": "user", "content": "What is 2+2?"},
]
```

Expand All @@ -144,19 +151,22 @@ For large datasets or when running multiple environment replicas, you can defer
```python
def get_dataset_builder(split: str = "train", seed: int = 42) -> vf.DatasetBuilder:
"""Returns a builder that lazily loads the dataset."""

def build() -> Dataset:
ds = load_dataset("my-dataset", split=split)
ds = ds.shuffle(seed=seed)
return ds

return build


def load_environment():
dataset_builder = get_dataset_builder(split="train")
eval_builder = get_dataset_builder(split="test")

return vf.SingleTurnEnv(
dataset=dataset_builder, # built on first access
eval_dataset=eval_builder, # built on first access
dataset=dataset_builder, # built on first access
eval_dataset=eval_builder, # built on first access
rubric=rubric,
)
```
Expand Down Expand Up @@ -204,14 +214,13 @@ async def check_keywords(completion, info) -> float:
found = sum(1 for kw in keywords if kw.lower() in response.lower())
return found / len(keywords)


async def length_reward(completion) -> float:
response = completion[-1]["content"]
return 1.0 if len(response) < 500 else 0.5

rubric = vf.Rubric(
funcs=[check_keywords, length_reward],
weights=[1.0, 0.1]
)

rubric = vf.Rubric(funcs=[check_keywords, length_reward], weights=[1.0, 0.1])
```

The final rollout reward is computed as the weighted sum of all reward function scores.
Expand All @@ -229,6 +238,8 @@ Beyond the final score, reward functions can be used to track metrics for observ
```python
async def response_length(completion) -> float:
return float(len(completion[-1]["content"]))


rubric.add_metric(response_length) # shorthand for weight=0
```

Expand All @@ -245,12 +256,14 @@ async def similarity_score(completion, answer, state) -> float:
state["similarity"] = score
return score


async def similarity_threshold(state) -> float:
return 1.0 if state["similarity"] > 0.8 else 0.0


rubric = vf.Rubric(
funcs=[similarity_score, similarity_threshold],
weights=[0.0, 1.0] # log similarity, but only reward threshold
weights=[0.0, 1.0], # log similarity, but only reward threshold
)
```

Expand All @@ -270,6 +283,7 @@ async def diversity_bonus(completions) -> list[float]:
# Higher reward if this response is unique
return [0.2 if responses.count(r) == 1 else 0.0 for r in responses]


rubric = vf.Rubric(funcs=[correct_answer, diversity_bonus])
```

Expand All @@ -281,6 +295,7 @@ In rubric environments, reward functions can request static helper objects that
rubric = vf.Rubric(funcs=[my_reward_func])
rubric.add_class_object("my_helper", some_helper_object)


async def my_reward_func(completion, my_helper) -> float:
# my_helper is now available by name
return await my_helper.score(completion)
Expand All @@ -295,10 +310,12 @@ judge_rubric = vf.JudgeRubric(
judge_model="gpt-4.1-mini",
)


async def judge_correctness(prompt, completion, answer, judge) -> float:
verdict = await judge(prompt, completion, answer)
return 1.0 if "yes" in verdict.lower() else 0.0


judge_rubric.add_reward_func(judge_correctness)
```

Expand All @@ -311,10 +328,13 @@ judge_rubric = vf.JudgeRubric(
judge_model="gpt-4.1-mini",
judge_prompt="""Rate the writing quality of this response from 0-10.
Response: {response}
Score:"""
Score:""",
)

async def quality_score(completion, judge_client, judge_model, judge_prompt, parser) -> float:

async def quality_score(
completion, judge_client, judge_model, judge_prompt, parser
) -> float:
response = parser.parse_answer(completion)
filled_prompt = judge_prompt.format(response=response)
result = await judge_client.chat.completions.create(
Expand Down Expand Up @@ -374,6 +394,7 @@ class MyMonitorRubric(vf.Rubric):
async def custom_metric(self, state: vf.State) -> float:
return len(state["trajectory"])


env = vf.ToolEnv(dataset=dataset, tools=tools, rubric=rubric)
env.add_rubric(MyMonitorRubric())
```
Expand Down Expand Up @@ -402,6 +423,7 @@ async def calculate(expression: str) -> str:
except Exception as e:
return f"Error: {e}"


async def lookup(term: str) -> str:
"""Look up a term in the knowledge base.

Expand Down Expand Up @@ -583,6 +605,7 @@ async def answer_submitted(self, state: vf.State) -> bool:
return False
return "FINAL ANSWER:" in completion[-1].get("content", "")


@vf.stop(priority=-10) # expensive validation runs last
async def answer_detected(self, state: vf.State) -> bool:
# only runs if cheap checks didn't already stop
Expand Down Expand Up @@ -648,7 +671,12 @@ To end a rollout from within `env_response` (e.g., when the game ends), set `sta
```python
async def env_response(self, messages: vf.Messages, state: vf.State) -> vf.Messages:
if check_game_over(state):
final_message = [{"role": "user", "content": "Game over! Final score: " + str(state["score"])}]
final_message = [
{
"role": "user",
"content": "Game over! Final score: " + str(state["score"]),
}
]
state["final_env_response"] = final_message
return final_message
# ... normal response logic
Expand Down Expand Up @@ -732,6 +760,7 @@ Environments that require external API keys (e.g., for judge models or external
```python
import verifiers as vf


def load_environment(api_key_var: str = "OPENAI_API_KEY") -> vf.Environment:
vf.ensure_keys([api_key_var])
# now safe to use os.environ[api_key_var]
Expand Down Expand Up @@ -816,6 +845,7 @@ with open(file, "w") as f:
f.write(data)
# ✅ use the built-in helper
from verifiers.utils.path_utils import write_temp_file

tmp_path = await asyncio.to_thread(write_temp_file, data, ".txt")
```

Expand All @@ -826,6 +856,7 @@ from concurrent.futures import ProcessPoolExecutor

executor = ProcessPoolExecutor(max_workers=4)


async def heavy_reward(data):
loop = asyncio.get_event_loop()
return await loop.run_in_executor(executor, cpu_bound_fn, data)
Expand Down
9 changes: 6 additions & 3 deletions docs/v0/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,11 +84,14 @@ Environment modules should expose a `load_environment` function which returns an
# my_env.py
import verifiers as vf

def load_environment(dataset_name: str = 'gsm8k') -> vf.Environment:
dataset = vf.load_example_dataset(dataset_name) # 'question'

def load_environment(dataset_name: str = "gsm8k") -> vf.Environment:
dataset = vf.load_example_dataset(dataset_name) # 'question'

async def correct_answer(completion, answer) -> float:
completion_ans = completion[-1]['content']
completion_ans = completion[-1]["content"]
return 1.0 if completion_ans == answer else 0.0

rubric = vf.Rubric(funcs=[correct_answer])
env = vf.SingleTurnEnv(dataset=dataset, rubric=rubric)
return env
Expand Down
Loading
Loading