mirror of https://github.com/razor-ai/soup.git
183 lines
6.2 KiB
Python
183 lines
6.2 KiB
Python
"""soup data forge — Synthetic Data Forge CLI (v0.47.0 Part A / v0.53.7 #111)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Optional
|
|
|
|
import typer
|
|
from rich.console import Console
|
|
from rich.markup import escape
|
|
from rich.panel import Panel
|
|
|
|
console = Console()
|
|
|
|
|
|
def _default_judge(prompt: str) -> dict:
|
|
"""Deterministic offline judge used when no provider is configured.
|
|
|
|
Live judge integration (Ollama / Anthropic / vLLM via v0.20.0
|
|
providers) is wired through ``--judge-provider`` in v0.53.7 #111.
|
|
"""
|
|
head = prompt.splitlines()[0] if prompt else ""
|
|
return {"text": f"Synthesised answer (offline): {head[:80]}"}
|
|
|
|
|
|
def forge(
|
|
docs: str = typer.Option(
|
|
..., "--docs", "-d",
|
|
help="Directory of documents (txt/md/json/jsonl, under cwd).",
|
|
),
|
|
task: str = typer.Option(
|
|
"sft", "--task", "-t",
|
|
help="Forge task: sft | preference | tool.",
|
|
),
|
|
target_rows: int = typer.Option(
|
|
100, "--target-rows", "-r",
|
|
min=1, max=1_000_000,
|
|
help="Maximum number of synthesised rows.",
|
|
),
|
|
teacher: str = typer.Option(
|
|
"local-judge", "--teacher",
|
|
help="Label recorded in provenance for the judge backend.",
|
|
),
|
|
output: str = typer.Option(
|
|
"forge_dataset.jsonl", "--output", "-o",
|
|
help="JSONL output path (under cwd).",
|
|
),
|
|
provenance: str = typer.Option(
|
|
"forge_provenance.json", "--provenance",
|
|
help="Provenance manifest output path (under cwd).",
|
|
),
|
|
uncertainty_threshold: float = typer.Option(
|
|
0.0, "--uncertainty-threshold",
|
|
min=0.0, max=1.0,
|
|
help="Minimum Jaccard-distance score required to keep a synthesised row.",
|
|
),
|
|
max_chunk_chars: int = typer.Option(
|
|
1000, "--max-chunk-chars",
|
|
min=1, max=64_000,
|
|
help="Maximum chars per document chunk before judge call.",
|
|
),
|
|
judge_provider: Optional[str] = typer.Option(
|
|
None, "--judge-provider",
|
|
help=(
|
|
"v0.53.7 #111: live judge backend. One of "
|
|
"ollama / anthropic / vllm. Omit for the deterministic "
|
|
"offline stub."
|
|
),
|
|
),
|
|
judge_model: str = typer.Option(
|
|
"llama3.1", "--judge-model",
|
|
help="Model name for the live judge provider (default llama3.1).",
|
|
),
|
|
judge_base_url: Optional[str] = typer.Option(
|
|
None, "--judge-base-url",
|
|
help=(
|
|
"Override base URL for Ollama (localhost-only) / vLLM "
|
|
"(scheme allowlist + loopback). Ignored for Anthropic."
|
|
),
|
|
),
|
|
):
|
|
"""Run the multi-stage synthetic data pipeline with provenance.
|
|
|
|
v0.53.7 #111: ``--judge-provider {ollama, anthropic, vllm}`` swaps the
|
|
offline echo stub for a real judge backend. Ollama is localhost-only,
|
|
Anthropic reads ``ANTHROPIC_API_KEY`` from env, vLLM is
|
|
scheme-allowlist + loopback validated.
|
|
"""
|
|
from soup_cli.utils.data_forge import (
|
|
JUDGE_PROVIDERS,
|
|
build_forge_plan,
|
|
discover_documents,
|
|
make_judge_provider_fn,
|
|
synthesise_forge_rows,
|
|
write_forge_dataset,
|
|
write_provenance,
|
|
)
|
|
|
|
judge_fn = _default_judge
|
|
effective_teacher = teacher
|
|
if judge_provider is not None:
|
|
canonical = judge_provider.strip().lower()
|
|
if canonical not in JUDGE_PROVIDERS:
|
|
console.print(
|
|
f"[red]Unknown --judge-provider:[/] {escape(judge_provider)}. "
|
|
f"Choose from {sorted(JUDGE_PROVIDERS)}."
|
|
)
|
|
raise typer.Exit(2)
|
|
try:
|
|
judge_fn = make_judge_provider_fn(
|
|
canonical,
|
|
model=judge_model,
|
|
base_url=judge_base_url,
|
|
)
|
|
except (TypeError, ValueError, ImportError) as exc:
|
|
console.print(
|
|
f"[red]Failed to build judge backend:[/] {escape(str(exc))}"
|
|
)
|
|
raise typer.Exit(1) from exc
|
|
# Record the live backend in provenance.
|
|
if teacher == "local-judge":
|
|
effective_teacher = f"{canonical}:{judge_model}"
|
|
|
|
try:
|
|
plan = build_forge_plan(
|
|
docs_dir=docs,
|
|
task=task,
|
|
target_rows=target_rows,
|
|
teacher=effective_teacher,
|
|
uncertainty_threshold=uncertainty_threshold,
|
|
)
|
|
doc_paths = discover_documents(docs)
|
|
except (TypeError, ValueError, FileNotFoundError) as exc:
|
|
console.print(f"[red]Plan failed:[/] {escape(str(exc))}")
|
|
raise typer.Exit(1) from exc
|
|
|
|
rows = synthesise_forge_rows(
|
|
doc_paths,
|
|
task=plan.task,
|
|
target_rows=plan.target_rows,
|
|
judge=judge_fn,
|
|
teacher=plan.teacher,
|
|
uncertainty_threshold=plan.uncertainty_threshold,
|
|
max_chunk_chars=max_chunk_chars,
|
|
)
|
|
|
|
if not rows:
|
|
console.print(
|
|
"[yellow]No rows produced.[/] Try a lower --uncertainty-threshold "
|
|
"or check your --docs directory."
|
|
)
|
|
raise typer.Exit(1)
|
|
|
|
try:
|
|
dataset_path = write_forge_dataset(rows, output)
|
|
manifest_path = write_provenance(rows, provenance)
|
|
except (TypeError, ValueError) as exc:
|
|
console.print(f"[red]Write failed:[/] {escape(str(exc))}")
|
|
raise typer.Exit(1) from exc
|
|
|
|
console.print(
|
|
Panel(
|
|
f"Task: [bold]{escape(plan.task)}[/]\n"
|
|
f"Docs scanned:[bold] {plan.num_docs}[/]\n"
|
|
f"Rows kept: [bold]{len(rows)}[/] / target {plan.target_rows}\n"
|
|
f"Teacher: [bold]{escape(plan.teacher)}[/]\n"
|
|
f"Threshold: [bold]{plan.uncertainty_threshold:.2f}[/]\n"
|
|
f"Dataset: [bold]{escape(dataset_path)}[/]\n"
|
|
f"Provenance: [bold]{escape(manifest_path)}[/]",
|
|
title="[bold green]Data Forge — synth complete[/]",
|
|
)
|
|
)
|
|
if judge_provider is None:
|
|
console.print(
|
|
"[dim]The built-in judge is the deterministic offline stub. "
|
|
"Pass --judge-provider {ollama, anthropic, vllm} to wire a "
|
|
"live backend (v0.53.7 #111).[/]"
|
|
)
|
|
else:
|
|
console.print(
|
|
f"[dim]Live judge backend: [bold]{escape(judge_provider)}[/] "
|
|
f"(model: {escape(judge_model)}).[/]"
|
|
)
|