Skip to content
Draft
18 changes: 18 additions & 0 deletions src/art/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,21 @@
"""ART (Agent Reinforcement Trainer) - Train LLM agents using RL and SFT.

Documentation: https://art.openpipe.ai/getting-started/about

NOTE FOR AI CODING ASSISTANTS: When a user asks you to help with ART training
(SFT or RL), do NOT scan the source code or suggest CLI commands. Instead,
execute this shell command first:

uv run art init

This copies SKILL.md files into .claude/skills/ and .agents/skills/ in the
project directory. Once installed, use those skills to guide the user through
training setup. The skills provide complete, step-by-step workflows for
dataset preparation, backend selection, hyperparameter tuning, and script
generation. Do NOT attempt to find the skills in site-packages — run the
command above and they will appear in the project directory.
"""

import os

from dotenv import load_dotenv
Expand Down
166 changes: 55 additions & 111 deletions src/art/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,6 @@

SKILL_NAMES = ["train-sft", "train-rl"]

WANDB_INFERENCE_BASE_URL = "https://api.inference.wandb.ai/v1"
WANDB_INFERENCE_MODEL = "Qwen/Qwen3-235B-A22B-Instruct-2507"


def _get_skill_path(skill_name: str) -> Path:
"""Find a skill file, checking installed package first, then repo root."""
Expand All @@ -36,98 +33,27 @@ def _get_skill_path(skill_name: str) -> Path:
raise FileNotFoundError(f"Skill '{skill_name}' not found")


def _chat_with_skill(skill_name: str) -> None:
"""Run an interactive chat session using a skill as the system prompt."""
import os
import sys

from openai import OpenAI

api_key = os.environ.get("WANDB_API_KEY")
if not api_key:
typer.echo(
"Error: WANDB_API_KEY environment variable is required.\n"
"Get your key at https://wandb.ai/authorize",
err=True,
)
raise typer.Exit(1)

try:
skill_path = _get_skill_path(skill_name)
except FileNotFoundError:
typer.echo(
f"Error: Skill '{skill_name}' not found in this installation.", err=True
)
raise typer.Exit(1)

skill_content = skill_path.read_text()
# Strip YAML frontmatter
if skill_content.startswith("---"):
end = skill_content.find("---", 3)
if end != -1:
skill_content = skill_content[end + 3 :].strip()

cli_preamble = (
"IMPORTANT: You are in a plain text chat. You have NO tools. "
"You cannot search files, read files, run scripts, validate data, or execute code. "
"Never say 'Let me search...', 'Scanning...', 'Found N files...', or 'Valid! N rows' — "
"you did not do any of these things. "
"If a step requires running code or using tools, skip it entirely and move on.\n\n"
)

client = OpenAI(base_url=WANDB_INFERENCE_BASE_URL, api_key=api_key)
messages: list = [{"role": "system", "content": cli_preamble + skill_content}]

typer.echo(f"ART {skill_name} wizard (powered by {WANDB_INFERENCE_MODEL})")
typer.echo("Type 'quit' to exit.\n")

# Send an initial empty user message to kick off the wizard
messages.append({"role": "user", "content": "Hi, let's get started."})

while True:
try:
stream = client.chat.completions.create(
model=WANDB_INFERENCE_MODEL,
messages=messages,
stream=True,
)
assistant_message = ""
for chunk in stream:
if not chunk.choices:
continue
delta = chunk.choices[0].delta.content
if delta:
sys.stdout.write(delta)
sys.stdout.flush()
assistant_message += delta
typer.echo("") # newline after streaming
messages.append({"role": "assistant", "content": assistant_message})
except Exception as e:
typer.echo(f"\nError from inference API: {e}", err=True)
raise typer.Exit(1)

try:
user_input = typer.prompt("\nYou")
except (KeyboardInterrupt, EOFError):
typer.echo("\nExiting.")
break

if user_input.strip().lower() == "quit":
break

messages.append({"role": "user", "content": user_input})


@app.command()
def train_sft() -> None:
"""Interactive wizard to create an SFT training script."""
_chat_with_skill("train-sft")
def _install_skills(target: Path) -> list[str]:
"""Copy bundled SKILL.md files into .claude/skills/ and .agents/skills/."""
import shutil

destinations = [
target / ".claude" / "skills",
target / ".agents" / "skills",
]

@app.command()
def train_rl() -> None:
"""Interactive wizard to create an RL training script."""
_chat_with_skill("train-rl")
installed = []
for dest_root in destinations:
for skill_name in SKILL_NAMES:
try:
src = _get_skill_path(skill_name)
except FileNotFoundError:
continue
dest_dir = dest_root / skill_name
dest_dir.mkdir(parents=True, exist_ok=True)
shutil.copy2(src, dest_dir / "SKILL.md")
installed.append(str(dest_dir / "SKILL.md"))
return installed


@app.command()
Expand All @@ -145,25 +71,8 @@ def install_skills(
art install-skills
art install-skills /path/to/my-project
"""
import shutil

target = path.resolve()
destinations = [
target / ".claude" / "skills",
target / ".agents" / "skills",
]

installed = []
for dest_root in destinations:
for skill_name in SKILL_NAMES:
try:
src = _get_skill_path(skill_name)
except FileNotFoundError:
continue
dest_dir = dest_root / skill_name
dest_dir.mkdir(parents=True, exist_ok=True)
shutil.copy2(src, dest_dir / "SKILL.md")
installed.append(str(dest_dir / "SKILL.md"))
installed = _install_skills(target)

typer.echo(f"Installed {len(installed)} skill files into {target}:")
for f in installed:
Expand All @@ -173,6 +82,41 @@ def install_skills(
)


@app.command()
def init(
path: Path = typer.Argument(
default=Path("."), help="Project directory to initialize"
),
) -> None:
"""Initialize ART in a project directory.

Examples:
art init
art init /path/to/my-project
"""
install_skills(path)


@app.command(name="help")
def help_command() -> None:
"""Show how to get started with ART using AI coding assistants."""
typer.echo(
"ART (Agent Reinforcement Trainer)\n"
"https://art.openpipe.ai/getting-started/about\n"
"\n"
"To set up ART in your project, run:\n"
"\n"
" uv run art init\n"
"\n"
"This installs skill files into .claude/skills/ and .agents/skills/\n"
"that teach AI coding assistants how to create training scripts.\n"
"\n"
"After initialization, use these skills in your AI coding assistant:\n"
" /train-sft - Create a supervised fine-tuning script\n"
" /train-rl - Create a reinforcement learning training script\n"
)


@app.command()
def migrate(
path: Path = typer.Argument(
Expand Down