android-skill-router / modal_apps /evaluate_pocket_benchmark_modal.py
kriyanshi's picture
Ship v2 intent extraction with API, demo UI, eval, and benchmark suite.
40a90bb
Raw
History Blame Contribute Delete
6.97 kB
"""
Evaluate the Pocket Automator real-world benchmark on Modal.
Prerequisites:
pip install modal
modal setup
python scripts/generate_pocket_benchmark.py
modal run modal_apps/train_modal.py
Run evaluation:
modal run modal_apps/evaluate_pocket_benchmark_modal.py
"""
from __future__ import annotations
import json
import pathlib
import modal
app = modal.App("pocket-automator-benchmark")
MODEL_NAME = "Qwen/Qwen2.5-3B-Instruct"
PROJECT_ROOT = pathlib.Path(__file__).resolve().parent.parent
LOCAL_BENCHMARK_PROMPTS = PROJECT_ROOT / "data" / "pocket_benchmark_prompts.json"
REMOTE_BENCHMARK_PROMPTS = "/data/pocket_benchmark_prompts.json"
REMOTE_RESULTS = "/data/pocket_benchmark_results.json"
REMOTE_REPORT = "/data/pocket_benchmark_report.txt"
MODEL_DIR = pathlib.Path("/model")
ADAPTER_DIR = MODEL_DIR / "adapter"
MAX_SEQ_LENGTH = 2048
MAX_NEW_TOKENS = 128
INTENT_SYSTEM_PROMPT = (
"You extract structured Android automation intents from natural language. "
'Reply with JSON only: {"skill": "<skill_name>", "parameters": {<extracted_fields>}}. '
"Pick exactly one skill. Extract all relevant parameters mentioned in the request "
"(contact names, messages, times, destinations, channel names, search queries, etc.). "
"Use an empty object for parameters when the skill needs none. "
"Use the app or action named in the request (contacts, Gmail, Slack, YouTube, etc.) "
"to pick the correct skill."
)
GPU_TYPE = "A10G"
TIMEOUT_SECONDS = 45 * 60
dataset_volume = modal.Volume.from_name(
"android-dataset-data",
create_if_missing=True,
)
model_volume = modal.Volume.from_name(
"android-dataset-model",
create_if_missing=True,
)
model_cache_volume = modal.Volume.from_name(
"android-dataset-hf-cache",
create_if_missing=True,
)
eval_image = (
modal.Image.debian_slim(python_version="3.11")
.pip_install_from_requirements(
str(pathlib.Path(__file__).parent / "requirements-modal.txt")
)
.env(
{
"HF_HOME": "/model_cache",
"HF_HUB_ENABLE_HF_TRANSFER": "1",
"PYTHONPATH": "/root/src",
}
)
.add_local_dir(str(PROJECT_ROOT / "src"), remote_path="/root/src", copy=True)
)
def build_intent_messages(user_content: str) -> list[dict[str, str]]:
return [
{"role": "system", "content": INTENT_SYSTEM_PROMPT},
{"role": "user", "content": user_content},
]
with eval_image.imports():
import unsloth # noqa: F401
import torch
from peft import PeftModel
from unsloth import FastLanguageModel
from unsloth.chat_templates import get_chat_template
from pocket_benchmark import record_result, save_benchmark_outputs
from skill_utils import extract_intent
@app.function(
image=eval_image,
gpu=GPU_TYPE,
timeout=TIMEOUT_SECONDS,
volumes={
"/data": dataset_volume,
"/model": model_volume,
"/model_cache": model_cache_volume,
},
)
def evaluate() -> None:
dataset_volume.reload()
model_volume.reload()
benchmark_path = pathlib.Path(REMOTE_BENCHMARK_PROMPTS)
if not benchmark_path.exists():
raise FileNotFoundError(
f"Benchmark prompts not found at {benchmark_path}. "
"Run `modal run modal_apps/evaluate_pocket_benchmark_modal.py` locally first."
)
if not (ADAPTER_DIR / "adapter_config.json").exists():
raise FileNotFoundError(
f"LoRA adapter not found at {ADAPTER_DIR}. "
"Run `modal run modal_apps/train_modal.py` first."
)
with benchmark_path.open(encoding="utf-8") as handle:
cases = json.load(handle)
print(f"Loading base model: {MODEL_NAME}")
model, tokenizer = FastLanguageModel.from_pretrained(
model_name=MODEL_NAME,
max_seq_length=MAX_SEQ_LENGTH,
dtype=None,
load_in_4bit=True,
)
print(f"Loading LoRA adapter from {ADAPTER_DIR}")
model = PeftModel.from_pretrained(model, str(ADAPTER_DIR))
tokenizer = get_chat_template(
tokenizer,
chat_template="qwen-2.5",
)
FastLanguageModel.for_inference(model)
results = []
total = len(cases)
print(f"Running Pocket Automator benchmark on {total} prompts...\n")
for index, case in enumerate(cases, start=1):
prompt = case["prompt"]
messages = build_intent_messages(prompt)
inputs = tokenizer.apply_chat_template(
messages,
tokenize=True,
add_generation_prompt=True,
return_tensors="pt",
).to("cuda")
with torch.inference_mode():
outputs = model.generate(
input_ids=inputs,
max_new_tokens=MAX_NEW_TOKENS,
use_cache=True,
do_sample=False,
)
generated = outputs[0][inputs.shape[1] :]
raw_output = tokenizer.decode(generated, skip_special_tokens=True).strip()
predicted = extract_intent(raw_output)
result = record_result(case, raw_output, predicted)
results.append(result)
print(f"--- [{index}/{total}] {case.get('id', 'n/a')} ---")
print(f"Prompt: {prompt}")
print(f"Expected: {json.dumps(case['expected'], separators=(',', ':'))}")
print(
f"Predicted: {json.dumps(predicted, separators=(',', ':')) if predicted else raw_output}"
)
print(
f"Skill: {'PASS' if result['skill_correct'] else 'FAIL'} | "
f"Params: {'PASS' if result['parameter_correct'] else 'FAIL'} | "
f"Exact JSON: {'PASS' if result['exact_json_match'] else 'FAIL'}"
)
print()
metrics, report = save_benchmark_outputs(
results,
results_path=pathlib.Path(REMOTE_RESULTS),
report_path=pathlib.Path(REMOTE_REPORT),
)
dataset_volume.commit()
print("--- Pocket Automator Benchmark Summary ---")
print(report)
print(f"Saved results to {REMOTE_RESULTS}")
print(f"Saved report to {REMOTE_REPORT}")
@app.local_entrypoint()
def main() -> None:
benchmark_path = pathlib.Path(LOCAL_BENCHMARK_PROMPTS)
if not benchmark_path.exists():
raise FileNotFoundError(
f"Local benchmark prompts not found: {benchmark_path.resolve()}. "
"Run `python scripts/generate_pocket_benchmark.py` first."
)
remote_name = "pocket_benchmark_prompts.json"
try:
dataset_volume.remove_file(remote_name)
except Exception:
pass
print(f"Uploading {benchmark_path} to dataset volume...")
with dataset_volume.batch_upload() as batch:
batch.put_file(str(benchmark_path), remote_name)
print("Launching Pocket Automator benchmark on Modal GPU...")
evaluate.remote()
print(
"Benchmark complete. Results saved to Modal volume 'android-dataset-data':\n"
" - pocket_benchmark_results.json\n"
" - pocket_benchmark_report.txt"
)