Files
OpenJarvis/scripts/orchestrator/build_unified_sft.py
T
Andrew Park f3d9705961 orchestrator: expand tool catalog, fix shell_exec confirmation gate, wire SFT data pipeline
Tools:
- Bridge 4 more real OpenJarvis tools into the orchestrator catalog
  (think, apply_patch, pdf_extract, db_query); catalog is now 6 models
  + 11 basic tools.
- Fix the dispatch path so confirmation-gated tools actually run: the
  ToolExecutor was built with no confirm callback, so shell_exec (and
  any requires_confirmation tool) returned a "requires confirmation"
  error instead of executing -- which silently broke TerminalBench.
  Headless eval/rollouts now auto-approve.
- Drop dead-end candidates: git_* (need the unbuilt openjarvis_rust
  extension and just duplicate shell_exec), browser_* (needs Playwright),
  and knowledge_search/sql/retrieval (personal-data RAG, empty on the
  academic benchmarks).

SFT data pipeline:
- Reasoning-task loaders (GeneralThought-430K + OpenThoughts3) with an
  8K cold-start set and a 30K GRPO prompt pool.
- Domain-dispatched verifier (math/code checkers + Gemini judge fallback,
  since the OpenAI key is dead).
- Rejection-sampling generator + unified <tool_call> serializer matching
  the GRPO rollout format; base self-sampling driver.
- Drop the old paradigm/tier scaffolding (adp_loader, build, paradigms,
  select, serialize, tiers) replaced by the unified path.

Training/eval:
- SFT + GRPO configs for Qwen3.5-9B and gemma-4-12B-it.
- OrchestratorBackend + eval harness over GAIA/TerminalBench/TauBench/
  MMLU-Pro/SuperGPQA.
- Cost-aware GRPO reward.

Ignore generated data/ artifacts.
2026-07-11 12:23:46 -07:00

104 lines
3.6 KiB
Python

#!/usr/bin/env python
"""Build ToolOrchestra SFT data by rejection sampling over ToolScale.
For each ToolScale task, a teacher orchestrator is rolled out N times over the
faithful unified tool catalog (one tool per model); passing trajectories are
serialized into the conversations JSONL the SFT trainer consumes.
Needs API access for the teacher + expert models (set the usual env keys).
Example:
uv run python scripts/orchestrator/build_unified_sft.py \
--out data/orchestrator_unified_sft.jsonl \
--teacher-model gpt-5 --max-tasks 200 --samples-per-task 4 \
--local-model qwen3:8b --local-endpoint http://localhost:8001/v1
"""
from __future__ import annotations
import argparse
import json
import logging
import os
from typing import Optional
from openjarvis.agents.hybrid.expert_registry import default_catalog
from openjarvis.agents.hybrid.toolorchestra.unified import (
make_call_orchestrator,
make_dispatch,
)
from openjarvis.agents.hybrid.toolorchestra.rollout import run_unified_rollout
from openjarvis.learning.intelligence.orchestrator.sft_data.reject_sample import (
generate_sft_dataset,
gold_coverage_verify,
)
from openjarvis.learning.intelligence.orchestrator.sft_data.toolscale import (
load_toolscale,
)
def main(argv: Optional[list[str]] = None) -> int:
p = argparse.ArgumentParser(description=__doc__)
p.add_argument("--out", default="data/orchestrator_unified_sft.jsonl")
p.add_argument("--teacher-model", default="claude-sonnet-4-6")
p.add_argument("--teacher-base-url", default="https://api.anthropic.com/v1/",
help="OpenAI-compatible base URL; pass '' for OpenAI cloud.")
p.add_argument("--max-tasks", type=int, default=200)
p.add_argument("--samples-per-task", type=int, default=4)
p.add_argument("--max-keep-per-task", type=int, default=1)
p.add_argument("--max-turns", type=int, default=50)
p.add_argument("--temperature", type=float, default=1.0)
p.add_argument("--local-model", default=None)
p.add_argument("--local-endpoint", default=None)
args = p.parse_args(argv)
logging.basicConfig(level=logging.INFO, format="%(message)s")
tools = default_catalog(
local_model=args.local_model, local_endpoint=args.local_endpoint,
)
logging.info("Tool catalog (%d): %s", len(tools), [t.name for t in tools])
base_url = args.teacher_base_url or None
api_key = (
os.environ.get("ANTHROPIC_API_KEY")
if base_url and "anthropic" in base_url
else os.environ.get("OPENAI_API_KEY")
)
call_orch = make_call_orchestrator(
args.teacher_model,
base_url=base_url,
api_key=api_key,
temperature=args.temperature,
)
dispatch = make_dispatch({})
def rollout_fn(task):
try:
return run_unified_rollout(
task.instruction, tools,
call_orchestrator=call_orch, dispatch=dispatch,
max_turns=args.max_turns,
)
except Exception as exc: # network/key failures shouldn't kill the run
logging.warning("rollout failed for %s: %s", task.task_id, exc)
return None
tasks = load_toolscale(max_tasks=args.max_tasks)
stats = generate_sft_dataset(
args.out,
tasks=tasks,
tools=tools,
rollout_fn=rollout_fn,
verify_fn=gold_coverage_verify,
samples_per_task=args.samples_per_task,
max_keep_per_task=args.max_keep_per_task,
reward_fn=lambda r: -r.cost_usd, # cheapest-correct gets highest reward
)
print(json.dumps(stats, indent=2))
return 0
if __name__ == "__main__":
raise SystemExit(main())