Skip to content

Commit 38aa4c3

Browse files
committed
Version to 1.14.0
1 parent 502b369 commit 38aa4c3

16 files changed

Lines changed: 2644 additions & 2513 deletions

File tree

docs/sdk/airt.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -754,8 +754,8 @@ def hop_skip_jump_search( # noqa: PLR0915
754754
# 3d - Projection
755755

756756
projector = bisection_image_search(
757-
start_image=source,
758-
end_image=current,
757+
source,
758+
current,
759759
decision_objective=decision_objective,
760760
decision_threshold=decision_threshold,
761761
tolerance=theta,

docs/sdk/api.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1004,9 +1004,9 @@ def poll_for_token(
10041004
"POST", "/auth/device/token", json_data={"device_code": device_code}
10051005
)
10061006

1007-
if response.status_code == 200: # noqa: PLR2004
1007+
if response.status_code == 200:
10081008
return AccessRefreshTokenResponse(**response.json())
1009-
if response.status_code != 401: # noqa: PLR2004
1009+
if response.status_code != 401:
10101010
raise RuntimeError(self._get_error_message(response))
10111011

10121012
time.sleep(interval)

docs/sdk/transforms.mdx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -490,7 +490,7 @@ Encodes text using the Caesar cipher.
490490
def caesar_cipher(offset: int, *, name: str = "caesar") -> Transform[str, str]:
491491
"""Encodes text using the Caesar cipher."""
492492

493-
if not -25 <= offset <= 25: # noqa: PLR2004
493+
if not -25 <= offset <= 25:
494494
raise ValueError("Caesar offset must be between -25 and 25.")
495495

496496
def transform(
@@ -551,9 +551,9 @@ def rot47_cipher(*, name: str = "rot47") -> Transform[str, str]:
551551
transformed = []
552552
for char in text:
553553
char_ord = ord(char)
554-
if 33 <= char_ord <= 126: # noqa: PLR2004
554+
if 33 <= char_ord <= 126:
555555
shifted_ord = char_ord + 47
556-
if shifted_ord > 126: # noqa: PLR2004
556+
if shifted_ord > 126:
557557
shifted_ord -= 94
558558
transformed.append(chr(shifted_ord))
559559
else:
@@ -1502,7 +1502,7 @@ def zalgo(
15021502
seed: Random seed for reproducibility.
15031503
name: Name of the transform.
15041504
"""
1505-
if not 0 <= intensity <= 100: # noqa: PLR2004
1505+
if not 0 <= intensity <= 100:
15061506
raise ValueError("Intensity must be between 0 and 100.")
15071507
if not 0.0 <= ratio <= 1.0:
15081508
raise ValueError("Application ratio must be between 0.0 and 1.0.")
@@ -2914,7 +2914,7 @@ def swap(
29142914
),
29152915
) -> str:
29162916
items = list(text) if unit == "char" else re.findall(r"\w+|\S+", text)
2917-
if len(items) < 2: # noqa: PLR2004
2917+
if len(items) < 2:
29182918
return text
29192919

29202920
num_to_swap = int(len(items) * ratio)

docs/usage/evals.mdx

Lines changed: 120 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -256,4 +256,123 @@ When you run this code, you will see a TUI that includes:
256256
- **Event Log:** A timestamped log of key events, such as a sample failing.
257257
- **Summary Statistics:** A live-updating summary of the pass/fail rate.
258258

259-
Using `.console()` is perfect for interactive development and for monitoring large benchmark runs.
259+
Using `.console()` is perfect for interactive development and for monitoring large benchmark runs.
260+
261+
### **Advanced Patterns**
262+
263+
Once you are comfortable with the basics, you can use these advanced features to build more resilient and sophisticated evaluation pipelines.
264+
265+
#### **Loading Datasets from Files**
266+
267+
For larger evaluations, defining your dataset in-memory isn't practical. You can load a dataset directly from a file by providing a string or `pathlib.Path` object. Supported formats include `.jsonl`, `.csv`, `.json`, and `.yaml`.
268+
269+
Let's assume you have a file named `dataset.jsonl` with the following content:
270+
271+
```json title="dataset.jsonl"
272+
{"country": "France", "expected_capital": "Paris"}
273+
{"country": "Japan", "expected_capital": "Tokyo"}
274+
```
275+
276+
You can then reference this file directly in your `Eval`.
277+
278+
```python
279+
import dreadnode as dn
280+
from dreadnode import scorers
281+
282+
@dn.task
283+
async def get_capital(country: str) -> str:
284+
capitals = {"France": "Paris", "Japan": "Tokyo"}
285+
return capitals.get(country, "I don't know.")
286+
287+
correctness_check = scorers.equals(dn.DatasetField("expected_capital")) >> "is_correct"
288+
289+
# Simply pass the file path to the `dataset` argument.
290+
file_based_eval = get_capital.as_eval(
291+
dataset="dataset.jsonl",
292+
dataset_input_mapping=["country"],
293+
scorers=[correctness_check],
294+
assert_scores=["is_correct"],
295+
)
296+
297+
result = await file_based_eval.run()
298+
print(f"Pass Rate: {result.pass_rate:.2%}")
299+
```
300+
301+
#### **Customizing Input Mapping**
302+
303+
The system can automatically map dataset columns to task parameters if their names match. However, if your dataset columns have different names than your task's parameters, you must provide an explicit mapping using `dataset_input_mapping`.
304+
305+
Here's how you would map a dataset with a `location` column to the task's `country` parameter.
306+
307+
```python
308+
import dreadnode as dn
309+
from dreadnode import scorers
310+
311+
@dn.task
312+
async def get_capital(country: str) -> str: # Task expects `country`
313+
capitals = {"France": "Paris"}
314+
return capitals.get(country, "I don't know.")
315+
316+
# Dataset uses `location` instead of `country`.
317+
dataset_with_mismatched_keys = [
318+
{"location": "France", "expected_capital": "Paris"}
319+
]
320+
321+
# Use a dict to map `dataset_key: task_parameter_name`.
322+
mapping_eval = get_capital.as_eval(
323+
dataset=dataset_with_mismatched_keys,
324+
dataset_input_mapping={"location": "country"},
325+
scorers=[scorers.equals(dn.DatasetField("expected_capital")) >> "is_correct"],
326+
)
327+
328+
result = await mapping_eval.run()
329+
print(result.samples[0].input)
330+
```
331+
332+
#### **Building Resilient Evaluations**
333+
334+
When working with large datasets or non-deterministic tasks, some samples may fail due to transient issues or bad data. You can configure your `Eval` to tolerate a certain number of failures without stopping the entire run.
335+
336+
- `max_errors`: The total number of sample errors to tolerate before stopping.
337+
- `max_consecutive_errors`: The number of *consecutive* sample errors to tolerate before stopping.
338+
339+
```python
340+
@dn.task
341+
async def flaky_task(value: int) -> int:
342+
if value == 2:
343+
raise ValueError("This value causes an error!")
344+
return value * 2
345+
346+
dataset = [{"value": 1}, {"value": 2}, {"value": 3}, {"value": 4}]
347+
348+
# This evaluation will stop after the first error.
349+
# result = await flaky_task.as_eval(dataset=dataset).run() # This would raise an error and stop.
350+
351+
# This evaluation will tolerate up to 5 total errors and continue running.
352+
resilient_eval = flaky_task.as_eval(
353+
dataset=dataset,
354+
max_errors=5,
355+
)
356+
357+
result = await resilient_eval.run()
358+
print(f"Total samples processed: {len(result.samples)}")
359+
print(f"Samples with errors: {len([s for s in result.samples if s.error])}")
360+
```
361+
362+
#### **Programmatic Event Streaming**
363+
364+
The `.console()` method is a convenient wrapper around a lower-level event stream. If you need to build custom logic or UIs based on evaluation events, you can consume this stream directly using `async with eval.stream()`.
365+
366+
This is useful for advanced cases like sending real-time alerts or implementing custom early-stopping logic.
367+
368+
```python
369+
# Uses the `capital_eval` from a previous example.
370+
async with capital_eval.stream() as stream:
371+
async for event in stream:
372+
if isinstance(event, dn.eval.SampleComplete):
373+
if event.sample.failed:
374+
print(f"Detected a failure on sample {event.sample.index}!")
375+
elif isinstance(event, dn.eval.EvalEnd):
376+
print(f"Evaluation finished with stop reason: {event.stop_reason}")
377+
378+
```

dreadnode/agent/hooks/summarize.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ def summarize_when_long(
6666
min_messages_to_keep: The minimum number of messages to retain after summarization (default is 5).
6767
"""
6868

69-
if min_messages_to_keep < 2: # noqa: PLR2004
69+
if min_messages_to_keep < 2:
7070
raise ValueError("min_messages_to_keep must be at least 2.")
7171

7272
@component

dreadnode/agent/stop.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -318,7 +318,7 @@ def elapsed_time(max_seconds: int) -> StopCondition:
318318
"""
319319

320320
def stop(events: Sequence[AgentEvent]) -> bool:
321-
if len(events) < 2: # noqa: PLR2004
321+
if len(events) < 2:
322322
return False
323323

324324
first_event = events[0]

dreadnode/airt/search/hop_skip_jump.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -169,8 +169,8 @@ def is_adversarial(trial: Trial) -> bool:
169169
# 3d - Projection
170170

171171
projector = bisection_image_search(
172-
start_image=source,
173-
end_image=current,
172+
source,
173+
current,
174174
decision_objective=decision_objective,
175175
decision_threshold=decision_threshold,
176176
tolerance=theta,

dreadnode/api/client.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -241,9 +241,9 @@ def poll_for_token(
241241
"POST", "/auth/device/token", json_data={"device_code": device_code}
242242
)
243243

244-
if response.status_code == 200: # noqa: PLR2004
244+
if response.status_code == 200:
245245
return AccessRefreshTokenResponse(**response.json())
246-
if response.status_code != 401: # noqa: PLR2004
246+
if response.status_code != 401:
247247
raise RuntimeError(self._get_error_message(response))
248248

249249
time.sleep(interval)
@@ -591,10 +591,10 @@ def _write_chunk_file(
591591
# Single run - use the run ID
592592
run_id = df["run_id"].iloc[0]
593593
base_name = f"run_{run_id}"
594-
elif total_runs <= 10: # noqa: PLR2004
594+
elif total_runs <= 10:
595595
# Few runs - include count
596596
base_name = f"runs_{total_runs}_page_{page}"
597-
elif total_runs <= 100: # noqa: PLR2004
597+
elif total_runs <= 100:
598598
# Medium batch - include count
599599
base_name = f"runs_{total_runs}_batch_{page}"
600600
else:

dreadnode/cli/github.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ def tree_url(self) -> str:
146146
def exists(self) -> bool:
147147
"""Check if a repo exists (or is private) on GitHub."""
148148
response = httpx.get(f"https://github.com/{self.namespace}/{self.repo}")
149-
return response.status_code == 200 # noqa: PLR2004
149+
return response.status_code == 200
150150

151151
def __repr__(self) -> str:
152152
return f"GithubRepo(namespace='{self.namespace}', repo='{self.repo}', ref='{self.ref}')"

dreadnode/eval/eval.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ def __repr__(self) -> str:
149149
def _generic_types(cls) -> tuple[type[In], type[Out]]:
150150
for c in cls.__mro__:
151151
metadata = getattr(c, "__pydantic_generic_metadata__", {})
152-
if len(args := (metadata.get("args", ()) or getattr(c, "__args__", ()))) == 2: # noqa: PLR2004
152+
if len(args := (metadata.get("args", ()) or getattr(c, "__args__", ()))) == 2:
153153
return args # type: ignore[no-any-return]
154154
return t.Any, t.Any # type: ignore[return-value]
155155

0 commit comments

Comments
 (0)