From 2e13cdd86c6013bdb0b763d3e3fbc696031344b5 Mon Sep 17 00:00:00 2001 From: Hermes Coder Date: Wed, 12 Aug 2026 05:51:29 +0000 Subject: [PATCH] fix(lint): resolve all ruff violations (E501, RUF001, SIM103, etc.) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI run #20 failed because 'ruff check src/ tests/' reported 97 violations. This commit fixes all of them: - E501 (52): wrapped long lines to <=120 chars — string concatenation, parenthesized expressions, multi-line dict/call formatting - RUF001 (17): replaced cosmetic en-dashes in histogram bucket labels with ASCII hyphens; added noqa comments for intentional Unicode (mojibake test data, CJK regex ranges, sentence-ender sets) - F401 (1): PatternTimeout was a re-export, not unused — added to __all__ - SIM103 (2): collapsed if/return True/return False into direct return - SIM102 (1): collapsed nested if into a single if-with-and - RUF012 (2): mutable list class defaults to tuples in test mock classes - RUF015 (2): list-comprehension-first-element to next() generator - RUF059 (2): renamed unused unpacked vars to underscore-prefixed - E741 (2): ambiguous single-letter 'l' to 'line' in test comprehensions - I001/E401/F541/W292/RUF022: auto-fixed by 'ruff check --fix' All 1694 tests pass. Ruff 0.15.22 (locked version) reports clean. --- src/datasetforge/analysis/templates.py | 16 ++++---- src/datasetforge/api/jobs.py | 7 +++- src/datasetforge/api/server.py | 44 +++++++++++++++++----- src/datasetforge/cli/app.py | 5 ++- src/datasetforge/core/readiness.py | 10 ++--- src/datasetforge/core/store.py | 6 ++- src/datasetforge/core/unsloth_readiness.py | 6 ++- src/datasetforge/export/exporters.py | 22 +++++------ src/datasetforge/sources/__init__.py | 2 +- src/datasetforge/sources/github.py | 7 +++- src/datasetforge/transforms/__init__.py | 1 + src/datasetforge/transforms/columns.py | 25 +++++++++--- src/datasetforge/transforms/curation.py | 11 ++++-- src/datasetforge/ui/jobs.py | 8 +++- tests/unit/test_analysis.py | 15 ++++++-- tests/unit/test_content_faults.py | 7 +++- tests/unit/test_curation.py | 5 ++- tests/unit/test_curation_api.py | 20 ++++++++-- tests/unit/test_eval_split_export.py | 8 ++-- tests/unit/test_export_dialects.py | 4 +- tests/unit/test_final_sweep_round_a.py | 8 ++-- tests/unit/test_final_sweep_round_b.py | 5 +-- tests/unit/test_final_sweep_round_c.py | 3 +- tests/unit/test_github.py | 7 +++- tests/unit/test_make_ready.py | 18 ++++++--- tests/unit/test_mapping_archetypes.py | 20 +++++++--- tests/unit/test_sharegpt_roles.py | 3 +- tests/unit/test_transforms.py | 24 +++++++----- tests/unit/test_unsloth_readiness.py | 6 ++- 29 files changed, 219 insertions(+), 104 deletions(-) diff --git a/src/datasetforge/analysis/templates.py b/src/datasetforge/analysis/templates.py index c3cf1ef..c2f961b 100644 --- a/src/datasetforge/analysis/templates.py +++ b/src/datasetforge/analysis/templates.py @@ -26,14 +26,14 @@ # Buckets chosen around the context sizes people actually train at, so the # histogram answers "will this fit in 2k/4k/8k/32k" at a glance. _BUCKETS: list[tuple[int, int | None, str]] = [ - (0, 128, "0–128"), - (128, 512, "128–512"), - (512, 1024, "512–1k"), - (1024, 2048, "1k–2k"), - (2048, 4096, "2k–4k"), - (4096, 8192, "4k–8k"), - (8192, 16384, "8k–16k"), - (16384, 32768, "16k–32k"), + (0, 128, "0-128"), + (128, 512, "128-512"), + (512, 1024, "512-1k"), + (1024, 2048, "1k-2k"), + (2048, 4096, "2k-4k"), + (4096, 8192, "4k-8k"), + (8192, 16384, "8k-16k"), + (16384, 32768, "16k-32k"), (32768, None, "32k+"), ] diff --git a/src/datasetforge/api/jobs.py b/src/datasetforge/api/jobs.py index f86c2c5..4e1dff3 100644 --- a/src/datasetforge/api/jobs.py +++ b/src/datasetforge/api/jobs.py @@ -765,7 +765,12 @@ def _row_transforms() -> dict[str, Any]: "drop_unclosed_think": (lambda ds, o: drop_unclosed_think_tags(ds), "removed"), "drop_blank_turns": (lambda ds, o: drop_blank_turns(ds), "rows_changed"), "fix_shape": (lambda ds, o: fix_conversation_shape(ds), "changed"), - "convert": (lambda ds, o: convert_format(ds, o["target"], system_prompt=o.get("system_prompt") or None), "changed"), + "convert": ( + lambda ds, o: convert_format( + ds, o["target"], system_prompt=o.get("system_prompt") or None + ), + "changed", + ), }) return _ROW_TRANSFORMS diff --git a/src/datasetforge/api/server.py b/src/datasetforge/api/server.py index b28460c..1ac27a5 100644 --- a/src/datasetforge/api/server.py +++ b/src/datasetforge/api/server.py @@ -431,7 +431,11 @@ def _progress(t: str, i: int) -> None: elif source == "github": profile, pm = profile_from_settings("github", settings) if not settings.get("github_token") and progress_cb: - progress_cb("No GitHub token — unauthenticated limit is 60 req/hr. Add a token in Settings for 5000/hr.", None) + progress_cb( + "No GitHub token — unauthenticated limit is 60 req/hr. " + "Add a token in Settings for 5000/hr.", + None, + ) for i, t in enumerate(targets): if i < targets_done: continue @@ -768,11 +772,13 @@ async def update_dataset_card(dataset_id: str, payload: dict = Body(...)) -> dic async def generate_card_with_llm(dataset_id: str) -> dict[str, Any]: """Use the LLM to generate card fields from sample rows. Returns suggestions for the user to review — nothing is saved without a PUT.""" - import json as _json, re as _re, contextlib as _ctx + import contextlib as _ctx + import json as _json + import re as _re from datasetforge.analysis.ai import _sample_rows from datasetforge.synthetic.config import llm_config_from_settings - from datasetforge.synthetic.generator import _create_client, LLMBackendError + from datasetforge.synthetic.generator import LLMBackendError, _create_client # Two independent sync SQL reads (meta + a 10-row sample) dispatched # to the threadpool as one closure, the same shape the GET /card @@ -817,7 +823,9 @@ def _load() -> tuple[DatasetMeta, list]: ' "pretty_name": "A clean, human-readable name",\n' ' "description": "2-3 paragraphs describing the content, purpose, and characteristics",\n' ' "tags": ["5-10 relevant tags beyond the generic fine-tuning/instruction-tuning/dataset"],\n' - ' "task_categories": ["from: text-generation, text-classification, question-answering, summarization, translation, fill-mask, multiple-choice, reinforcement-learning, conversational"]\n' + ' "task_categories": ["from: text-generation, text-classification, ' + 'question-answering, summarization, translation, fill-mask, ' + 'multiple-choice, reinforcement-learning, conversational"]\n' '}\n\n' f"Dataset:\n{_json.dumps(prompt_data, ensure_ascii=False, indent=2)}" ) @@ -831,9 +839,17 @@ def _load() -> tuple[DatasetMeta, list]: except Exception as e: msg = str(e) if "not reachable" in msg or "Connection" in msg: - raise HTTPException(502, "LLM backend not reachable. Start it with `ollama serve` or check Settings.") from e + raise HTTPException( + 502, + "LLM backend not reachable. Start it with `ollama serve` " + "or check Settings.", + ) from e if "404" in msg or "model not found" in msg.lower(): - raise HTTPException(404, f"Model '{llm_config.model}' not found. Run `ollama pull {llm_config.model}` or change it in Settings.") from e + raise HTTPException( + 404, + f"Model '{llm_config.model}' not found. " + f"Run `ollama pull {llm_config.model}` or change it in Settings.", + ) from e raise HTTPException(502, f"LLM request failed: {msg}") from e parsed: dict[str, Any] = {} @@ -1581,7 +1597,11 @@ async def t_fix_shape(payload: dict = Body(...)) -> dict[str, Any]: job_kind="fix_shape", ) if not report.get("queued"): - _discard_if_empty(meta, "That would remove every row — nothing in this dataset is trainable as a conversation") + _discard_if_empty( + meta, + "That would remove every row — nothing in this dataset " + "is trainable as a conversation", + ) return _transform_result(meta, report) @app.post("/api/transform/strip-markers") @@ -2083,7 +2103,7 @@ async def run_ai_analyze(payload: dict = Body(...)) -> dict[str, Any]: )) from e if "timeout" in msg.lower() or "timed out" in msg.lower(): raise HTTPException(504, ( - f"The LLM took too long to respond. Try a smaller model, " + "The LLM took too long to respond. Try a smaller model, " "or check that the model is loaded in Settings → LLM backend." )) from e raise HTTPException(502, f"AI analysis failed: {msg}") from e @@ -2214,7 +2234,7 @@ def _persist_sides() -> None: @app.post("/api/export") async def export_download(payload: dict = Body(...)): - from datasetforge.export import export_jsonl_stream, check_think_tag_placement + from datasetforge.export import check_think_tag_placement, export_jsonl_stream from datasetforge.transforms.formats import EXPORT_DIALECTS dataset_id = payload["dataset_id"] @@ -2276,7 +2296,11 @@ async def export_hub(payload: dict = Body(...)) -> dict[str, Any]: raise HTTPException(400, "repo_id is required") token = payload.get("token") or ui_settings.get("hf_token") or None if not token: - raise HTTPException(401, "No Hugging Face token configured. Add one in Settings → Credentials → Hugging Face token.") + raise HTTPException( + 401, + "No Hugging Face token configured. Add one in Settings " + "→ Credentials → Hugging Face token.", + ) # Use the stored dataset card if the user has edited it; otherwise # push_to_hub falls back to auto-generation (build_dataset_card). stored_card = ds.meta.metadata.get("card") diff --git a/src/datasetforge/cli/app.py b/src/datasetforge/cli/app.py index e8cd4c0..9acf146 100644 --- a/src/datasetforge/cli/app.py +++ b/src/datasetforge/cli/app.py @@ -409,7 +409,10 @@ def scrape_github( if proxy: pm = ProxyManager(proxies=[p.strip() for p in proxy.split(",") if p.strip()]) if not gh_token: - console.print("[yellow]⚠[/yellow] No GitHub token — unauthenticated limit is 60 req/hr. Add a token with --token or GITHUB_TOKEN env var.") + console.print( + "[yellow]⚠[/yellow] No GitHub token — unauthenticated limit is 60 req/hr. " + "Add a token with --token or GITHUB_TOKEN env var." + ) console.print(f"[dim]Proxy: {pm.describe()} · UA rotation: {profile.rotate_ua}[/dim]") try: pairs = asyncio.run( diff --git a/src/datasetforge/core/readiness.py b/src/datasetforge/core/readiness.py index d7ff4ac..23db5d0 100644 --- a/src/datasetforge/core/readiness.py +++ b/src/datasetforge/core/readiness.py @@ -88,7 +88,7 @@ # The remaining errors need meaning to resolve — "There's a Robert Wayne born # in 1974" is complete, "…orbiting the" is cut off, and nothing short of a # language model separates them. -_SENTENCE_ENDERS = ".!?\"'”’):]}…" +_SENTENCE_ENDERS = ".!?\"'”’):]}…" # noqa: RUF001 _URL_TAIL_RE = re.compile(r"(https?://|www\.)\S*$", re.IGNORECASE) _LIST_LINE_RE = re.compile(r"^\s*(?:[-*•·>]|\d+[.)]|[A-Za-z][.)])\s+|^\s*[-*•]") @@ -132,9 +132,7 @@ def _text_looks_truncated(text: str) -> bool: return False # A markdown table row is complete — the table IS the answer, and a row # ending with a bare number is a complete final answer (math/data sets). - if _TABLE_ROW_RE.match(last_line) or _BARE_NUMBER_RE.match(last_line): - return False - return True + return not (_TABLE_ROW_RE.match(last_line) or _BARE_NUMBER_RE.match(last_line)) _MCQ_LETTERS = frozenset("abcdefghij") @@ -189,9 +187,7 @@ def has_truncated_final(row: Row) -> bool: last = seq[-1] if last.role == Role.ASSISTANT and _text_looks_truncated(last.content): return True - if row.pair is not None and _text_looks_truncated(row.pair[1]): - return True - return False + return row.pair is not None and _text_looks_truncated(row.pair[1]) # Think-tag markers used by reasoning datasets. Each pair is (open, close). diff --git a/src/datasetforge/core/store.py b/src/datasetforge/core/store.py index d4d963d..19483cb 100644 --- a/src/datasetforge/core/store.py +++ b/src/datasetforge/core/store.py @@ -714,7 +714,11 @@ def load(self, dataset_id: str) -> Dataset | None: # (e.g. a save_meta that wrote the flag without recomputing from # rows). This self-heals on the next load so the Transform checklist # and the Datasets page card can never disagree. - stats_ready, _stats_issues = readiness_from_stats(stored_stats, meta.row_count) if isinstance(stored_stats, dict) else (False, ["Dataset has no rows."]) + stats_ready, _stats_issues = ( + readiness_from_stats(stored_stats, meta.row_count) + if isinstance(stored_stats, dict) + else (False, ["Dataset has no rows."]) + ) needs_recompute = ( meta.training_ready is None or stale_stats diff --git a/src/datasetforge/core/unsloth_readiness.py b/src/datasetforge/core/unsloth_readiness.py index 61e77b2..3743cf6 100644 --- a/src/datasetforge/core/unsloth_readiness.py +++ b/src/datasetforge/core/unsloth_readiness.py @@ -401,7 +401,8 @@ def unsloth_readiness_from_stats( auto_fix=False, fix_options=[ {"value": "skip", "label": "Keep the preference pair — train with DPO/ORPO/KTO in code"}, - {"value": "flatten_chosen", "label": "Flatten to ChatML (chosen side) — enables the Studio no-code path"}, + {"value": "flatten_chosen", "label": "Flatten to ChatML (chosen side) " + "— enables the Studio no-code path"}, {"value": "flatten_rejected", "label": "Flatten to ChatML (rejected side)"}, ], default="skip", @@ -663,7 +664,8 @@ def _recommended_export(kind: str, tool_call_rows: int) -> dict[str, str]: return { "dialect": "text", "container": "jsonl", - "reason": "Continued pretraining corpus. In Unsloth, select the format manually (the picker may not auto-detect a bare text column).", + "reason": "Continued pretraining corpus. In Unsloth, select the format " + "manually (the picker may not auto-detect a bare text column).", } if kind == "preference": # Recommend the dialect that keeps the data whole. Flattening to diff --git a/src/datasetforge/export/exporters.py b/src/datasetforge/export/exporters.py index d2eeff8..3ec060a 100644 --- a/src/datasetforge/export/exporters.py +++ b/src/datasetforge/export/exporters.py @@ -12,6 +12,8 @@ from datasetforge.core.models import DataFormat, Dataset, DatasetMeta, Row, SplitResult from datasetforge.transforms.formats import row_to_format_dict +_REPO_URL = "https://github.com/SourceBox-LLC/DatasetForge" + def _get_export_format(dataset: Dataset, fmt: DataFormat | str | None) -> str: return fmt or dataset.meta.format @@ -59,9 +61,8 @@ def check_think_tag_placement(dataset: Dataset) -> str | None: if has_tags and m.role == Role.ASSISTANT: assistant_has_tags = True # Also check pair rows — the output should carry the tags. - if row.pair: - if any(marker in row.pair[1] for marker in _THINK_MARKERS): - assistant_has_tags = True + if row.pair and any(marker in row.pair[1] for marker in _THINK_MARKERS): + assistant_has_tags = True if system_has_tags and not assistant_has_tags: return ( @@ -539,9 +540,7 @@ def auto_fill_card( source = meta.source or "manual" if source in ("reddit", "stackexchange", "fourchan", "github"): ann_creators = ["crowdsourced"] - elif source == "synthetic": - ann_creators = ["machine-generated"] - elif source == "hf": + elif source == "synthetic" or source == "hf": ann_creators = ["machine-generated"] else: ann_creators = ["expert-created"] @@ -650,7 +649,10 @@ def card_to_readme( ex_lines: list[str] = [] for idx, ex in enumerate(examples, 1): - ex_lines.append("\n".join([f"### Example {idx}", "", "```json", json.dumps(ex, ensure_ascii=False, indent=2), "```"])) + ex_lines.append("\n".join([ + f"### Example {idx}", "", "```json", + json.dumps(ex, ensure_ascii=False, indent=2), "```", + ])) examples_md = "\n\n".join(ex_lines) if ex_lines else "No preview examples available." description = card.get("description") or "" @@ -695,8 +697,7 @@ def card_to_readme( - Generated on {__import__("datetime").datetime.now().strftime("%Y-%m-%d")}. --- - -*Made with [DatasetForge](https://github.com/SourceBox-LLC/DatasetForge) — create, format, and export datasets for LLM fine-tuning.* +*Made with [DatasetForge]({_REPO_URL}) — create, format, and export datasets for LLM fine-tuning.* """ return yaml_header + textwrap.dedent(body).lstrip() @@ -856,8 +857,7 @@ def build_dataset_card( - Generated on {__import__("datetime").datetime.now().strftime("%Y-%m-%d")}. --- - -*Made with [DatasetForge](https://github.com/SourceBox-LLC/DatasetForge) — create, format, and export datasets for LLM fine-tuning.* +*Made with [DatasetForge]({_REPO_URL}) — create, format, and export datasets for LLM fine-tuning.* """ return yaml_header + textwrap.dedent(body).lstrip() diff --git a/src/datasetforge/sources/__init__.py b/src/datasetforge/sources/__init__.py index e0456af..27a2de7 100644 --- a/src/datasetforge/sources/__init__.py +++ b/src/datasetforge/sources/__init__.py @@ -18,8 +18,8 @@ "ProxyManager", "ScrapeProfile", "fourchan", - "github", "get_profile", + "github", "looks_blocked", "pick_user_agent", "profile_from_settings", diff --git a/src/datasetforge/sources/github.py b/src/datasetforge/sources/github.py index f49f1f4..61f82cb 100644 --- a/src/datasetforge/sources/github.py +++ b/src/datasetforge/sources/github.py @@ -155,7 +155,12 @@ def _to_record_pair(issue: dict[str, Any], comments: list[dict[str, Any]], *, mi return {"input": q_text, "output": a_text} -def _to_record_conversation(issue: dict[str, Any], comments: list[dict[str, Any]], *, min_len: int) -> dict[str, Any] | None: +def _to_record_conversation( + issue: dict[str, Any], + comments: list[dict[str, Any]], + *, + min_len: int, +) -> dict[str, Any] | None: """Build a {messages: [...]} multi-turn conversation from the issue thread. The issue author is the first ``user`` turn; each subsequent non-bot diff --git a/src/datasetforge/transforms/__init__.py b/src/datasetforge/transforms/__init__.py index b02558b..a150f96 100644 --- a/src/datasetforge/transforms/__init__.py +++ b/src/datasetforge/transforms/__init__.py @@ -49,6 +49,7 @@ "AugmentResult", "EnhanceResult", "ExpandResult", + "PatternTimeout", "assemble_message_tree", "augment_dataset", "ban_pattern_filter", diff --git a/src/datasetforge/transforms/columns.py b/src/datasetforge/transforms/columns.py index b8e0ebe..d2b386b 100644 --- a/src/datasetforge/transforms/columns.py +++ b/src/datasetforge/transforms/columns.py @@ -413,7 +413,10 @@ def suggest_mapping(columns: list[str], sample_records: list[dict[str, Any]]) -> id_col=low.get("message_id") or low.get("id"), parent_col=low.get("parent_id"), role_col=low.get("role") or low.get("from"), - reason="message_id + parent_id columns detected → text rows carrying the tree linkage (assemble with Transform → Curate later)", + reason=( + "message_id + parent_id columns detected → text rows carrying " + "the tree linkage (assemble with Transform → Curate later)" + ), ) # -- preference: chosen + rejected in any encoding ---------------------- @@ -441,13 +444,19 @@ def suggest_mapping(columns: list[str], sample_records: list[dict[str, Any]]) -> markers = sniff_dialogue_markers([t for t in texts if isinstance(t, str)]) if markers: mapping.human_marker, mapping.assistant_marker = markers - mapping.reason = "chosen + rejected columns with Human:/Assistant: markers detected → preference rows (both sides editable, exports to TRL DPO)" + mapping.reason = ( + "chosen + rejected columns with Human:/Assistant: markers " + "detected → preference rows (both sides editable, exports to TRL DPO)" + ) return mapping # transcript encoding # bare-string completions: need a prompt column mapping.prompt_col = _find_col(columns, ("prompt", "question", "input", "instruction")) mapping.system_col = _find_col(columns, _SYSTEM_COL_NAMES) if mapping.prompt_col: - mapping.reason = "chosen + rejected + prompt columns detected → preference rows (both sides editable, exports to TRL DPO)" + mapping.reason = ( + "chosen + rejected + prompt columns detected → preference " + "rows (both sides editable, exports to TRL DPO)" + ) return mapping # -- kto: prompt + completion (+ label) --------------------------------- @@ -517,7 +526,10 @@ def suggest_mapping(columns: list[str], sample_records: list[dict[str, Any]]) -> system_col=_find_col(columns, _SYSTEM_COL_NAMES), human_marker=in_marker, assistant_marker=out_marker, - reason=f"{in_marker}: / {out_marker}: markers detected in {inn}/{outn} values → Alpaca pairs (markers preserved)", + reason=( + f"{in_marker}: / {out_marker}: markers detected in {inn}/{outn} values " + f"→ Alpaca pairs (markers preserved)" + ), ) context = _find_col(columns, _CONTEXT_COL_NAMES) reasoning = _sniff_reasoning(columns, sample_records) @@ -549,7 +561,10 @@ def suggest_mapping(columns: list[str], sample_records: list[dict[str, Any]]) -> system_col=_find_col(columns, _SYSTEM_COL_NAMES), human_marker=human, assistant_marker=assistant, - reason=f'{col} column contains a dialogue transcript (e.g. "{human}: … {assistant}: …") → conversations', + reason=( + f"{col} column contains a dialogue transcript " + f'(e.g. "{human}: … {assistant}: …") → conversations' + ), ) # -- single-text-column corpus ------------------------------------------ diff --git a/src/datasetforge/transforms/curation.py b/src/datasetforge/transforms/curation.py index d37a596..d947579 100644 --- a/src/datasetforge/transforms/curation.py +++ b/src/datasetforge/transforms/curation.py @@ -252,7 +252,7 @@ def fix_msgs(msgs: list[Message]) -> list[Message]: # rows", which is what this is for. It reports "unknown" rather than # guessing when nothing scores. _SCRIPTS: list[tuple[str, re.Pattern[str]]] = [ - ("ja", re.compile(r"[぀-ゟ゠-ヿ]")), + ("ja", re.compile(r"[぀-ゟ゠-ヿ]")), # noqa: RUF001 ("ko", re.compile(r"[가-힯ᄀ-ᇿ]")), ("zh", re.compile(r"[一-鿿]")), ("ru", re.compile(r"[Ѐ-ӿ]")), @@ -297,9 +297,12 @@ def fix_msgs(msgs: list[Message]) -> list[Message]: # and one of "do"/"to"/"na" scored 2 for Polish — "How do I beat my DUI # case? Let's talk through the process." was detected as pl. "pl": {"nie", "sie", "jest", "na", "do", "to", "co", "jak", "tak", "ale", "czy", "ktore", "przez", "przy"}, - "tr": {"bir", "ve", "bu", "için", "ile", "daha", "olarak", "olan", "var", "değil", "çok", "ama", "ne", "gibi", "kadar"}, - "id": {"yang", "dan", "di", "untuk", "dengan", "tidak", "ini", "dari", "itu", "pada", "adalah", "akan", "saya", "ada", "bisa"}, - "vi": {"của", "và", "là", "có", "không", "được", "trong", "người", "một", "cho", "những", "này", "để", "với", "các"}, + "tr": {"bir", "ve", "bu", "için", "ile", "daha", "olarak", "olan", + "var", "değil", "çok", "ama", "ne", "gibi", "kadar"}, + "id": {"yang", "dan", "di", "untuk", "dengan", "tidak", "ini", + "dari", "itu", "pada", "adalah", "akan", "saya", "ada", "bisa"}, + "vi": {"của", "và", "là", "có", "không", "được", "trong", + "người", "một", "cho", "những", "này", "để", "với", "các"}, } LANGUAGE_CODES = sorted({*_STOPWORDS, *(code for code, _ in _SCRIPTS), "unknown"}) diff --git a/src/datasetforge/ui/jobs.py b/src/datasetforge/ui/jobs.py index 82539bb..fc4cf38 100644 --- a/src/datasetforge/ui/jobs.py +++ b/src/datasetforge/ui/jobs.py @@ -358,8 +358,12 @@ def resume_on_startup(self) -> list[str]: # batches_done flat like augment/enhance/scrape do, so # check both shapes rather than just the top level. if intermediate: - chunks_done = intermediate.get("chunks_done", 0) or (intermediate.get("generate") or {}).get("chunks_done", 0) - batches_done = intermediate.get("batches_done", 0) or (intermediate.get("curate") or {}).get("batches_done", 0) + chunks_done = intermediate.get("chunks_done", 0) or ( + (intermediate.get("generate") or {}).get("chunks_done", 0) + ) + batches_done = intermediate.get("batches_done", 0) or ( + (intermediate.get("curate") or {}).get("batches_done", 0) + ) targets_done = intermediate.get("targets_done", 0) if chunks_done: name += f" (resuming, {chunks_done} chunks done)" diff --git a/tests/unit/test_analysis.py b/tests/unit/test_analysis.py index f6f9eaf..b282112 100644 --- a/tests/unit/test_analysis.py +++ b/tests/unit/test_analysis.py @@ -293,12 +293,18 @@ def test_preference_side_analysis_splits_chosen_and_rejected(): rows = [ Row( prompt=[Message(role=Role.USER, content="Explain gravity.")], - chosen=[Message(role=Role.ASSISTANT, content="Gravity pulls things down toward the Earth's center. It keeps us on the ground.")], + chosen=[Message(role=Role.ASSISTANT, content=( + "Gravity pulls things down toward the Earth's center. " + "It keeps us on the ground." + ))], rejected=[Message(role=Role.ASSISTANT, content="idk")], ), Row( prompt=[Message(role=Role.USER, content="What is 2+2?")], - chosen=[Message(role=Role.ASSISTANT, content="The answer is 4, because adding two units to two units gives four.")], + chosen=[Message(role=Role.ASSISTANT, content=( + "The answer is 4, because adding two units to two units " + "gives four." + ))], rejected=[Message(role=Role.ASSISTANT, content="5")], ), ] @@ -318,7 +324,10 @@ def test_preference_side_analysis_splits_chosen_and_rejected(): # Both sides both = analyze_preference_side(ds, side="both") assert "chosen" in both and "rejected" in both - assert both["chosen"].metrics["basic_stats"]["avg_output_len"] > both["rejected"].metrics["basic_stats"]["avg_output_len"] + assert ( + both["chosen"].metrics["basic_stats"]["avg_output_len"] + > both["rejected"].metrics["basic_stats"]["avg_output_len"] + ) def test_preference_side_dataset_carries_prompt_in_both_sides(): diff --git a/tests/unit/test_content_faults.py b/tests/unit/test_content_faults.py index 8e1e214..3788437 100644 --- a/tests/unit/test_content_faults.py +++ b/tests/unit/test_content_faults.py @@ -83,7 +83,10 @@ def test_a_quoted_transcript_further_in_is_not_flagged(self): whole class of false positive.""" quoting = Row(conversation=[ Message(role=Role.USER, content="what does my log say?"), - Message(role=Role.ASSISTANT, content="Looking at the log:\nUser: clicked\nSystem: ok\nThe click registered."), + Message(role=Role.ASSISTANT, content=( + "Looking at the log:\nUser: clicked\n" + "System: ok\nThe click registered." + )), ]) assert not has_leaked_role_markers(quoting) @@ -146,7 +149,7 @@ def test_counters_are_reversible(self): class TestStripTemplateMarkers: def test_headings_are_removed_and_body_kept(self): - ds, report = strip_template_markers(_ds([ + ds, _report = strip_template_markers(_ds([ Row(pair=("### Instruction:\nCalculate the enthalpy", "### Response:\nUse Hess' Law")), ])) assert ds.rows[0].pair == ("Calculate the enthalpy", "Use Hess' Law") diff --git a/tests/unit/test_curation.py b/tests/unit/test_curation.py index 6806626..ef0bc4e 100644 --- a/tests/unit/test_curation.py +++ b/tests/unit/test_curation.py @@ -100,7 +100,10 @@ def test_near_dedupe_scopes_target_different_redundancy(): def test_overlap_pairs_finds_cross_dataset_contamination(): - train = ["What is the capital of France? Paris is the capital city of France.", "Unrelated filler row about gardening tools."] + train = [ + "What is the capital of France? Paris is the capital city of France.", + "Unrelated filler row about gardening tools.", + ] bench = ["What is the capital of France? Paris is the capital city of France!"] hits = overlap_pairs(train, bench, NearDupConfig(threshold=0.6)) assert hits and hits[0][0] == 0 diff --git a/tests/unit/test_curation_api.py b/tests/unit/test_curation_api.py index d46bb7b..9db0b46 100644 --- a/tests/unit/test_curation_api.py +++ b/tests/unit/test_curation_api.py @@ -112,8 +112,14 @@ def test_assemble_tree_endpoint(): def test_contamination_endpoint(await_job): with TestClient(create_app()) as client: - shared = "What is the capital of France? Paris has been the capital city since the tenth century." - train = _seed([Row(pair=(shared, "Paris")), Row(pair=("Unrelated gardening question", "Use mulch"))], name="train") + shared = ( + "What is the capital of France? Paris has been the capital " + "city since the tenth century." + ) + train = _seed( + [Row(pair=(shared, "Paris")), Row(pair=("Unrelated gardening question", "Use mulch"))], + name="train", + ) bench = _seed([Row(pair=(shared + "!", "Paris"))], name="bench") r = client.post( @@ -171,12 +177,18 @@ def test_preference_side_analysis_endpoint(): rows = [ Row( prompt=[Message(role=Role.USER, content="Explain gravity.")], - chosen=[Message(role=Role.ASSISTANT, content="Gravity pulls things down toward the Earth's center. It keeps us on the ground.")], + chosen=[Message(role=Role.ASSISTANT, content=( + "Gravity pulls things down toward the Earth's center. " + "It keeps us on the ground." + ))], rejected=[Message(role=Role.ASSISTANT, content="idk")], ), Row( prompt=[Message(role=Role.USER, content="What is 2+2?")], - chosen=[Message(role=Role.ASSISTANT, content="The answer is 4, because adding two units to two units gives four.")], + chosen=[Message(role=Role.ASSISTANT, content=( + "The answer is 4, because adding two units to two units " + "gives four." + ))], rejected=[Message(role=Role.ASSISTANT, content="5")], ), ] diff --git a/tests/unit/test_eval_split_export.py b/tests/unit/test_eval_split_export.py index 7b86c68..4c7bec3 100644 --- a/tests/unit/test_eval_split_export.py +++ b/tests/unit/test_eval_split_export.py @@ -47,10 +47,10 @@ def test_eval_split_export_returns_a_zip_with_two_jsonl_files(): assert any("_eval.jsonl" in n for n in names) # Identify files by name, not sorted index (eval sorts before train). - train_name = [n for n in names if "_train" in n][0] - eval_name = [n for n in names if "_eval" in n][0] - train_lines = [l for l in zf.read(train_name).decode().strip().split("\n") if l] - eval_lines = [l for l in zf.read(eval_name).decode().strip().split("\n") if l] + train_name = next(n for n in names if "_train" in n) + eval_name = next(n for n in names if "_eval" in n) + train_lines = [line for line in zf.read(train_name).decode().strip().split("\n") if line] + eval_lines = [line for line in zf.read(eval_name).decode().strip().split("\n") if line] assert len(train_lines) == 16, f"expected 16 train rows, got {len(train_lines)}" assert len(eval_lines) == 4, f"expected 4 eval rows, got {len(eval_lines)}" diff --git a/tests/unit/test_export_dialects.py b/tests/unit/test_export_dialects.py index 14e2e0d..8503a1e 100644 --- a/tests/unit/test_export_dialects.py +++ b/tests/unit/test_export_dialects.py @@ -177,7 +177,7 @@ def test_sharegpt_export_warns_when_tool_calls_cannot_be_represented(): # re-export as ChatML. row = Row(conversation=[ Message(role=Role.USER, content="Weather?"), - Message(role=Role.ASSISTANT, content="", tool_calls=[{"id": "c1", "type": "function", "function": {"name": "f"}}]), + Message(role=Role.ASSISTANT, content="", tool_calls=[{"id": "c1", "type": "function", "function": {"name": "f"}}]), # noqa: E501 Message(role=Role.TOOL, content='{"t": 72}', name="f"), ]) warnings: list[str] = [] @@ -209,7 +209,7 @@ def test_export_jsonl_returns_warnings_summary_when_requested(tmp_path: Path): meta=DatasetMeta(name="fc", format="sharegpt"), rows=[Row(conversation=[ Message(role=Role.USER, content="Weather?"), - Message(role=Role.ASSISTANT, content="", tool_calls=[{"id": "c1", "type": "function", "function": {"name": "f"}}]), + Message(role=Role.ASSISTANT, content="", tool_calls=[{"id": "c1", "type": "function", "function": {"name": "f"}}]), # noqa: E501 ])], ) out = tmp_path / "fc.jsonl" diff --git a/tests/unit/test_final_sweep_round_a.py b/tests/unit/test_final_sweep_round_a.py index a957fda..eaa4e90 100644 --- a/tests/unit/test_final_sweep_round_a.py +++ b/tests/unit/test_final_sweep_round_a.py @@ -21,11 +21,9 @@ import pytest from datasetforge.analysis.ai import AIResponseParseError, _parse_response -from datasetforge.core.models import Dataset, DatasetMeta, Row, detect_format from datasetforge.io.hub_pull import pull_from_hub from datasetforge.transforms.columns import ColumnMapping, apply_mapping - # --------------------------------------------------------------------------- # # Fix 1 — hub_pull format detection # --------------------------------------------------------------------------- # @@ -44,7 +42,7 @@ def test_text_corpus_is_text_not_standard(self, monkeypatch): records = [{"text": f"an article about topic {i}"} for i in range(20)] class _DS: - column_names = ["text"] + column_names = ("text",) def __iter__(self): return iter(records) @@ -71,7 +69,7 @@ def test_preference_pull_is_preference_not_standard(self, monkeypatch): ] class _DS: - column_names = ["prompt", "chosen", "rejected"] + column_names = ("prompt", "chosen", "rejected") def __iter__(self): return iter(records) @@ -236,4 +234,4 @@ def test_markdown_wrapped_json_still_parses(self): def test_genuinely_unparseable_still_raises(self): with pytest.raises(AIResponseParseError): - _parse_response("this has no json object at all") \ No newline at end of file + _parse_response("this has no json object at all") diff --git a/tests/unit/test_final_sweep_round_b.py b/tests/unit/test_final_sweep_round_b.py index ac3189f..780b34c 100644 --- a/tests/unit/test_final_sweep_round_b.py +++ b/tests/unit/test_final_sweep_round_b.py @@ -19,7 +19,6 @@ from datasetforge.core.models import Row from datasetforge.transforms.ops import PatternTimeout, ban_pattern_filter - # --------------------------------------------------------------------------- # # Fix 7 — ingest URL scheme validation # --------------------------------------------------------------------------- # @@ -123,7 +122,7 @@ class TestBanPatternTimeout: PatternTimeout, not hang the worker.""" def test_compile_uses_regex_with_a_per_call_timeout(self): - compiled, per_call = _compile_ban_pattern("secret") + _compiled, per_call = _compile_ban_pattern("secret") # `regex` is a transitive dep of datasets; when present the compile # path returns a 2s per-call timeout so .search(timeout=...) can fire. assert per_call is not None and per_call > 0 @@ -215,4 +214,4 @@ def test_filter_endpoint_invalid_regex_is_400(self): "/api/transform/filter", json={"dataset_id": "baninv01", "ban_pattern": "(", "name": "f"}, ) - assert r.status_code == 400, r.text \ No newline at end of file + assert r.status_code == 400, r.text diff --git a/tests/unit/test_final_sweep_round_c.py b/tests/unit/test_final_sweep_round_c.py index 42ea1e0..791d37e 100644 --- a/tests/unit/test_final_sweep_round_c.py +++ b/tests/unit/test_final_sweep_round_c.py @@ -23,7 +23,6 @@ from datasetforge.core.models import Dataset, DatasetMeta, Row from datasetforge.core.store import DatasetStore - # --------------------------------------------------------------------------- # # Fix 10 — in-place transform must not mutate the cached base object # --------------------------------------------------------------------------- # @@ -192,4 +191,4 @@ def first_batch_only(self, dataset_id, rows): # in-memory rows_done (25). The two diverge exactly in the race the fix # closes; without it meta.row_count == 25 while the DB holds 10. assert meta.row_count == committed, f"row_count {meta.row_count} != committed {committed}" - assert meta.row_count != 25 \ No newline at end of file + assert meta.row_count != 25 diff --git a/tests/unit/test_github.py b/tests/unit/test_github.py index b8248f2..4e6967a 100644 --- a/tests/unit/test_github.py +++ b/tests/unit/test_github.py @@ -178,7 +178,9 @@ class TestScrapeNetworkFree: def _mock_issues(self, n=3): return [ { - "number": 100 - i, "title": f"Issue number {i} title", "body": f"Body for issue {i} here with enough text.", + "number": 100 - i, + "title": f"Issue number {i} title", + "body": f"Body for issue {i} here with enough text.", "user": {"login": f"asker{i}"}, "pull_request": None, } for i in range(n) @@ -188,7 +190,8 @@ def _mock_comments(self, issue_num): return [ { "user": {"login": "maintainer"}, "author_association": "owner", - "reactions": {"total_count": 5}, "body": f"Answer for issue {issue_num} with enough text to pass min_len.", + "reactions": {"total_count": 5}, + "body": f"Answer for issue {issue_num} with enough text to pass min_len.", } ] diff --git a/tests/unit/test_make_ready.py b/tests/unit/test_make_ready.py index c0af5cd..ae98f7c 100644 --- a/tests/unit/test_make_ready.py +++ b/tests/unit/test_make_ready.py @@ -55,7 +55,8 @@ def test_preserves_label_scores_metadata(self): ds = _ds([Row( conversation=[ Message(role=Role.USER, content="q"), - Message(role=Role.ASSISTANT, content="", tool_calls=[{"id": "c", "type": "function", "function": {"name": "f"}}]), + Message(role=Role.ASSISTANT, content="", + tool_calls=[{"id": "c", "type": "function", "function": {"name": "f"}}]), ], label=True, scores={"quality": 4.0}, metadata={"src": "test"}, )]) @@ -72,9 +73,15 @@ def test_preserves_label_scores_metadata(self): class TestStripLabels: def test_drops_labels_keeps_conversations(self): ds = _ds([ - Row(conversation=[Message(role=Role.USER, content="q"), Message(role=Role.ASSISTANT, content="a")], label=True), - Row(conversation=[Message(role=Role.USER, content="x"), Message(role=Role.ASSISTANT, content="y")], label=False), - Row(conversation=[Message(role=Role.USER, content="z"), Message(role=Role.ASSISTANT, content="w")]), # no label + Row(conversation=[ + Message(role=Role.USER, content="q"), Message(role=Role.ASSISTANT, content="a"), + ], label=True), + Row(conversation=[ + Message(role=Role.USER, content="x"), Message(role=Role.ASSISTANT, content="y"), + ], label=False), + Row(conversation=[ + Message(role=Role.USER, content="z"), Message(role=Role.ASSISTANT, content="w"), + ]), # no label ]) out, changed = strip_labels(ds) assert changed == 2 @@ -199,7 +206,8 @@ def test_skip_flatten_keeps_preference(self): def test_strip_tool_calls_when_policy_says_so(self): ds = _ds([Row(conversation=[ Message(role=Role.USER, content="q"), - Message(role=Role.ASSISTANT, content="", tool_calls=[{"id": "c", "type": "function", "function": {"name": "f"}}]), + Message(role=Role.ASSISTANT, content="", + tool_calls=[{"id": "c", "type": "function", "function": {"name": "f"}}]), Message(role=Role.ASSISTANT, content="a"), ])]) out, report = make_ready(ds, policies={"tool_calls_ignored": "strip"}) diff --git a/tests/unit/test_mapping_archetypes.py b/tests/unit/test_mapping_archetypes.py index 202a299..d3d1f10 100644 --- a/tests/unit/test_mapping_archetypes.py +++ b/tests/unit/test_mapping_archetypes.py @@ -206,12 +206,19 @@ def test_suggest_mapping_sets_reason_for_confident_detections(): strategy was picked. The generic pair fallback (a guess, not a detection) leaves it empty so no badge misleads the user.""" # preference — transcript encoding (hh-rlhf shape) - m = suggest_mapping(["chosen", "rejected"], [{"chosen": "Human: q\nAssistant: a", "rejected": "Human: q\nAssistant: b"}]) + m = suggest_mapping( + ["chosen", "rejected"], + [{"chosen": "Human: q\nAssistant: a", "rejected": "Human: q\nAssistant: b"}], + ) assert m.strategy == "preference" assert m.reason and "chosen + rejected" in m.reason # preference — message-list encoding (UltraFeedback-binarized shape) - m = suggest_mapping(["chosen", "rejected"], [{"chosen": [{"role": "user", "content": "q"}], "rejected": [{"role": "user", "content": "q"}]}]) + m = suggest_mapping( + ["chosen", "rejected"], + [{"chosen": [{"role": "user", "content": "q"}], + "rejected": [{"role": "user", "content": "q"}]}], + ) assert m.strategy == "preference" assert m.reason and "chosen + rejected" in m.reason @@ -310,9 +317,12 @@ def test_alpaca_markers_in_generic_columns(): markers inside the values (### Instruction: / ### Response:) is detected as Alpaca so the dropdown shows the right strategy and column pickers.""" records = [ - {"input": "### Instruction: Calculate the enthalpy of formation of C2H6.", "output": "### Response: To calculate, we use Hess's Law..."}, - {"input": "### Instruction: What is the VSEPR geometry of CHCl3?", "output": "### Response: The VSEPR geometry is tetrahedral..."}, - {"input": "### Instruction: Identify the oxidation states in Cu + 4HNO3.", "output": "### Response: Cu goes from 0 to +2..."}, + {"input": "### Instruction: Calculate the enthalpy of formation of C2H6.", + "output": "### Response: To calculate, we use Hess's Law..."}, + {"input": "### Instruction: What is the VSEPR geometry of CHCl3?", + "output": "### Response: The VSEPR geometry is tetrahedral..."}, + {"input": "### Instruction: Identify the oxidation states in Cu + 4HNO3.", + "output": "### Response: Cu goes from 0 to +2..."}, ] m = suggest_mapping(list(records[0].keys()), records) assert m.strategy == "alpaca" diff --git a/tests/unit/test_sharegpt_roles.py b/tests/unit/test_sharegpt_roles.py index 4ec225e..95cbcf4 100644 --- a/tests/unit/test_sharegpt_roles.py +++ b/tests/unit/test_sharegpt_roles.py @@ -82,7 +82,8 @@ def test_function_role_maps_to_tool(): records = [{ "conversations": [ {"from": "user", "value": "Call the API"}, - {"from": "assistant", "value": "Sure", "tool_calls": [{"id": "1", "type": "function", "function": {"name": "get", "arguments": "{}"}}]}, + {"from": "assistant", "value": "Sure", + "tool_calls": [{"id": "1", "type": "function", "function": {"name": "get", "arguments": "{}"}}]}, {"from": "function", "value": "result"}, ], }] diff --git a/tests/unit/test_transforms.py b/tests/unit/test_transforms.py index 75ea937..8e81e25 100644 --- a/tests/unit/test_transforms.py +++ b/tests/unit/test_transforms.py @@ -242,7 +242,10 @@ def test_displaced_conversation_is_not_kept_anywhere(self): def test_pair_row_promotion_also_leaves_no_trace(self): ds = Dataset( meta=DatasetMeta(name="pref_pair"), - rows=[Row(pair=("hi", "chosen reply"), metadata={"rejected": "\n\nHuman: hi\n\nAssistant: rejected reply"})], + rows=[Row( + pair=("hi", "chosen reply"), + metadata={"rejected": "\n\nHuman: hi\n\nAssistant: rejected reply"}, + )], ) out = promote_conversation(ds, "rejected", "Human", "Assistant") row = out.rows[0] @@ -334,7 +337,7 @@ def test_fixes_mojibake_in_pair_rows(self): out, changed = fix_text_encoding(ds) # Repaired back to the curly apostrophe the author actually wrote — # not straightened to ASCII, which would be normalizing clean text. - assert out.rows[0].pair == ("q", "Here’s an incomplete list.") + assert out.rows[0].pair == ("q", "Here’s an incomplete list.") # noqa: RUF001 assert changed == 1 def test_fixes_mojibake_in_conversation_turns(self): @@ -347,7 +350,7 @@ def test_fixes_mojibake_in_conversation_turns(self): ) out, changed = fix_text_encoding(ds) assert out.rows[0].conversation[0].content == "clean text" - assert out.rows[0].conversation[1].content == "mama’s boy" + assert out.rows[0].conversation[1].content == "mama’s boy" # noqa: RUF001 assert changed == 1 def test_fixes_mojibake_in_metadata_strings(self): @@ -356,7 +359,7 @@ def test_fixes_mojibake_in_metadata_strings(self): rows=[Row(pair=("q", "a"), metadata={"rejected": "mama’s boy", "score": 5})], ) out, changed = fix_text_encoding(ds) - assert out.rows[0].metadata["rejected"] == "mama’s boy" + assert out.rows[0].metadata["rejected"] == "mama’s boy" # noqa: RUF001 assert out.rows[0].metadata["score"] == 5 # non-strings pass through untouched assert changed == 1 @@ -382,7 +385,10 @@ def test_kept_turn_tool_calls_survive_content_join(self): # tool_call block, then a plain assistant text turn. merge used to # rebuild the joined Message with only role+content, silently # dropping the kept turn's tool_calls. - tc = [{"id": "call_1", "type": "function", "function": {"name": "get_weather", "arguments": '{"city": "Boston"}'}}] + tc = [ + {"id": "call_1", "type": "function", + "function": {"name": "get_weather", "arguments": '{"city": "Boston"}'}}, + ] ds = Dataset( meta=DatasetMeta(name="fc", format="chatml"), rows=[Row(conversation=[ @@ -554,17 +560,17 @@ def _ds(self, *texts): def test_real_mojibake_is_repaired(self): ds, changed = fix_text_encoding(self._ds("Here’s an incomplete list")) assert changed == 1 - assert ds.rows[0].pair[0] == "Here’s an incomplete list" + assert ds.rows[0].pair[0] == "Here’s an incomplete list" # noqa: RUF001 def test_curly_quotes_are_left_alone(self): - original = "Here’s a “quoted” word — and an em dash" + original = "Here’s a “quoted” word — and an em dash" # noqa: RUF001 ds, changed = fix_text_encoding(self._ds(original)) assert changed == 0 assert ds.rows[0].pair[0] == original def test_full_width_and_ligatures_are_left_alone(self): """Folding full-width CJK punctuation to ASCII damages CJK corpora.""" - original = "こんにちは! office" + original = "こんにちは! office" # noqa: RUF001 ds, changed = fix_text_encoding(self._ds(original)) assert changed == 0 assert ds.rows[0].pair[0] == original @@ -582,7 +588,7 @@ def test_hint_count_matches_what_the_transform_would_change(self): rows = [ Row(pair=("Here’s damage", "ok")), # real mojibake - Row(pair=("Here’s a curly quote", "ok")), # clean + Row(pair=("Here’s a curly quote", "ok")), # clean # noqa: RUF001 Row(pair=("plain ascii", "ok")), # clean ] reported = _mojibake_rows(rows) diff --git a/tests/unit/test_unsloth_readiness.py b/tests/unit/test_unsloth_readiness.py index 19a2dc7..7987454 100644 --- a/tests/unit/test_unsloth_readiness.py +++ b/tests/unit/test_unsloth_readiness.py @@ -119,7 +119,8 @@ def test_tool_calls_are_warning_not_blocker(self): r = unsloth_readiness(_ds([ Row(conversation=[ Message(role=Role.USER, content="Weather?"), - Message(role=Role.ASSISTANT, content="", tool_calls=[{"id": "c1", "type": "function", "function": {"name": "f"}}]), + Message(role=Role.ASSISTANT, content="", + tool_calls=[{"id": "c1", "type": "function", "function": {"name": "f"}}]), Message(role=Role.TOOL, content='{"t": 72}', name="f"), Message(role=Role.ASSISTANT, content="72F"), ]), @@ -246,7 +247,8 @@ def test_issues_ordered_blockers_first(self): Row(pair=("", "")), # blank (blocker) Row(conversation=[ # tool_calls (warning) Message(role=Role.USER, content="q"), - Message(role=Role.ASSISTANT, content="", tool_calls=[{"id": "c", "type": "function", "function": {"name": "f"}}]), + Message(role=Role.ASSISTANT, content="", + tool_calls=[{"id": "c", "type": "function", "function": {"name": "f"}}]), Message(role=Role.ASSISTANT, content="a"), ]), ]))