# /// script # requires-python = ">=3.10" # dependencies = [ # "trl>=0.13", # "unsloth", # "openenv-core[core]>=0.2.2", # "matplotlib", # "datasets", # "openai", # "huggingface_hub", # "python-dotenv", # "websockets", # ] # /// """ Post-training inference job — runs the trained LoRA adapter against every task in TASKS, captures per-task scores, and uploads ``trained_scores.json`` to the same HF Hub repo as the adapter. Run via HF Jobs after the GRPO main run: hf jobs uv run \ --flavor t4-small \ -s HF_TOKEN \ -e ADAPTER_REPO=pushpam14/api-contract-validator-grpo-7b \ -e ENV_URL=https://pushpam14-api-contract-validator.hf.space \ api_contract_validator/training/run_trained_inference.py Cost: ~$0.30 on t4-small (~30 min for 9 tasks, ~150 total LLM calls). """ from __future__ import annotations import asyncio import json import os import subprocess import sys from pathlib import Path GIT_REPO_URL = os.getenv( "GIT_REPO_URL", "https://github.com/kumarpushpam17-personal/Hackathon.git", ) GIT_REF = os.getenv("GIT_REF", "main") REPO_DIR = Path(os.getenv("REPO_DIR", "/tmp/eg-repo")) ADAPTER = os.environ.get( "ADAPTER_REPO", "pushpam14/api-contract-validator-grpo-7b" ) ENV_URL = os.environ.get( "ENV_URL", "https://pushpam14-api-contract-validator.hf.space" ) def _clone_repo() -> Path: if (REPO_DIR / "api_contract_validator").exists(): print(f"[launcher] repo already at {REPO_DIR} — skipping clone") return REPO_DIR print(f"[launcher] cloning {GIT_REPO_URL} @ {GIT_REF} -> {REPO_DIR}") REPO_DIR.parent.mkdir(parents=True, exist_ok=True) subprocess.run( ["git", "clone", "--depth", "1", "--branch", GIT_REF, GIT_REPO_URL, str(REPO_DIR)], check=True, ) return REPO_DIR def main() -> None: repo = _clone_repo() pkg = repo / "api_contract_validator" sys.path.insert(0, str(pkg)) print(f"[INFO] adapter: {ADAPTER}") print(f"[INFO] env_url: {ENV_URL}") from unsloth import FastLanguageModel # type: ignore import torch # type: ignore print(f"[INFO] loading base + adapter: {ADAPTER}") model, tokenizer = FastLanguageModel.from_pretrained( model_name=ADAPTER, max_seq_length=2048, load_in_4bit=True, dtype=torch.float16, ) FastLanguageModel.for_inference(model) print("[INFO] model ready for inference") from inference import ( # type: ignore # noqa: WPS433 BENCHMARK, MAX_STEPS_PER_TASK, TASKS, _build_action, build_user_prompt, _system_prompt_for_phase, parse_llm_response, log_start, log_step, log_end, ) from client import ValidatorEnv # type: ignore # noqa: WPS433 def query_local(observation: dict, step: int, history: list) -> dict: """Run the trained model on the current observation.""" phase = observation.get("phase", "detection") task_name = observation.get("task_name", "") user = build_user_prompt(observation, step, history) system = _system_prompt_for_phase(phase, task_name) messages = [ {"role": "system", "content": system}, {"role": "user", "content": user}, ] input_ids = tokenizer.apply_chat_template( messages, tokenize=True, add_generation_prompt=True, return_tensors="pt", ).to(model.device) # Temperature is env-configurable so we can tune sampling diversity. # 0.2 was too deterministic — the trained model kept reporting the # same violation across steps. 0.7 introduces enough variance for # the agent to find new violations after the first few. temperature = float(os.environ.get("TEMPERATURE", "0.7")) with torch.no_grad(): output_ids = model.generate( input_ids, max_new_tokens=384, temperature=temperature, do_sample=True, top_p=0.9, pad_token_id=tokenizer.pad_token_id or tokenizer.eos_token_id, ) text = tokenizer.decode( output_ids[0][input_ids.shape[1]:], skip_special_tokens=True ) return parse_llm_response(text) async def run_task(env: ValidatorEnv, task_name: str) -> dict: max_steps = MAX_STEPS_PER_TASK.get(task_name, 15) rewards: list = [] history: list = [] score = 0.01 success = False steps_taken = 0 log_start(task=task_name, env=BENCHMARK, model=ADAPTER) try: result = await env.reset(task_name=task_name) obs = ( result.observation.model_dump() if hasattr(result.observation, "model_dump") else result.observation.__dict__ ) for step in range(1, max_steps + 1): if result.done: break action_data = query_local(obs, step, history) action = _build_action(action_data) result = await env.step(action) obs = ( result.observation.model_dump() if hasattr(result.observation, "model_dump") else result.observation.__dict__ ) reward = float(result.reward or 0.0) rewards.append(reward) steps_taken = step action_str = ( f"{action_data.get('action_type','?')}:" f"{action_data.get('field_path', action_data.get('fix_strategy','?'))}" ) log_step( step=step, action=action_str, reward=reward, done=result.done, error=None, ) history.append(f"Step {step}: {action_str} -> reward {reward:+.2f}") if result.done: break try: state = await env.state() score = float(getattr(state, "score", 0.01)) or 0.01 except Exception: # noqa: BLE001 if rewards: correct = sum(1 for r in rewards if r >= 1.0) total = obs.get("violations_remaining", 0) + len( obs.get("violations_found", []) ) score = correct / total if total > 0 else 0.5 score = min(max(score, 0.01), 0.99) success = score >= 0.3 finally: log_end( success=success, steps=steps_taken, score=score, rewards=rewards, ) return { "task": task_name, "score": round(score, 4), "steps": steps_taken, "success": success, "rewards": [round(r, 4) for r in rewards], } async def main_async() -> None: env = ValidatorEnv(base_url=ENV_URL) results = [] try: for task in TASKS: results.append(await run_task(env, task)) finally: try: await env.close() except Exception: # noqa: BLE001 pass out = { "model": ADAPTER, "benchmark": BENCHMARK, "scores": {r["task"]: r["score"] for r in results}, "details": results, } out_path = Path("/tmp/trained_scores.json") out_path.write_text(json.dumps(out, indent=2)) print(f"[INFO] wrote {out_path}") # Upload to HF Hub adapter repo from huggingface_hub import HfApi api = HfApi(token=os.environ["HF_TOKEN"]) api.upload_file( path_or_fileobj=str(out_path), path_in_repo="trained_scores.json", repo_id=ADAPTER, repo_type="model", commit_message="Add post-training trained_scores.json", ) print(f"[INFO] uploaded trained_scores.json -> {ADAPTER}/trained_scores.json") asyncio.run(main_async()) print("[INFO] done.") if __name__ == "__main__": main()