Upload folder using huggingface_hub
Browse files- README.md +28 -6
- app.py +86 -0
- requirements.txt +6 -0
README.md
CHANGED
|
@@ -1,13 +1,35 @@
|
|
| 1 |
---
|
| 2 |
title: Tiny Browser Planner
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: gradio
|
| 7 |
-
sdk_version:
|
| 8 |
-
python_version:
|
| 9 |
app_file: app.py
|
| 10 |
pinned: false
|
| 11 |
---
|
| 12 |
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
title: Tiny Browser Planner
|
| 3 |
+
emoji: 🤖
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: purple
|
| 6 |
sdk: gradio
|
| 7 |
+
sdk_version: 4.36.1
|
| 8 |
+
python_version: 3.12.0
|
| 9 |
app_file: app.py
|
| 10 |
pinned: false
|
| 11 |
---
|
| 12 |
|
| 13 |
+
# Tiny Browser Planner
|
| 14 |
+
|
| 15 |
+
A 1B model that plans browser actions — fine-tuned MiniCPM5-1B with LoRA.
|
| 16 |
+
|
| 17 |
+
**The core finding:** A 1B model can explain the correct browser action before it can reliably choose it.
|
| 18 |
+
|
| 19 |
+
- [Model on HF](https://huggingface.co/Georgefifth/tiny-browser-planner-reason)
|
| 20 |
+
- [Dataset on HF](https://huggingface.co/datasets/Georgefifth/tiny-browser-planner-reason-dataset)
|
| 21 |
+
|
| 22 |
+
## Experiments
|
| 23 |
+
|
| 24 |
+
### v1 → v4
|
| 25 |
+
Data scaling didn't help. 200 targeted hard examples outperformed 2000 generic ones.
|
| 26 |
+
|
| 27 |
+
### Ablation: Action Space Paradox
|
| 28 |
+
Adding `back` to the action space created a powerful tool — but the model over-generalized it, using `back` for everything.
|
| 29 |
+
|
| 30 |
+
### Reason-First
|
| 31 |
+
With just 40 reasoning examples (7.8s training), the model went from 4/12 → 10/12. The reasoning head eliminates heuristic shortcutting.
|
| 32 |
+
|
| 33 |
+
## Usage
|
| 34 |
+
|
| 35 |
+
Enter a task and browsing history. The model shows its reasoning, then picks the next action.
|
app.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import re, time, json, os, shutil, torch, gradio as gr
|
| 2 |
+
import tempfile
|
| 3 |
+
from transformers import AutoModelForCausalLM, AutoTokenizer
|
| 4 |
+
from peft import PeftModel
|
| 5 |
+
from huggingface_hub import snapshot_download
|
| 6 |
+
|
| 7 |
+
BASE_ID = "openbmb/MiniCPM5-1B"
|
| 8 |
+
ADAPTER_ID = "Georgefifth/tiny-browser-planner-reason"
|
| 9 |
+
|
| 10 |
+
print("Loading model (this may take a minute)...")
|
| 11 |
+
start = time.time()
|
| 12 |
+
|
| 13 |
+
# Download adapter and create clean config
|
| 14 |
+
adapter_dir = os.path.join(tempfile.gettempdir(), "adapter")
|
| 15 |
+
from huggingface_hub import snapshot_download
|
| 16 |
+
snapshot_download(repo_id="Georgefifth/tiny-browser-planner-reason", local_dir=adapter_dir)
|
| 17 |
+
with open(os.path.join(adapter_dir, "adapter_config.json")) as f:
|
| 18 |
+
raw_cfg = json.load(f)
|
| 19 |
+
|
| 20 |
+
KEEP = {"r","lora_alpha","lora_dropout","target_modules","bias","task_type","peft_type","inference_mode"}
|
| 21 |
+
clean_cfg = {k: v for k, v in json.load(open(os.path.join(adapter_dir, "adapter_config.json"))).items() if k in {"r","lora_alpha","lora_dropout","target_modules","bias","task_type","peft_type","inference_mode"}}
|
| 22 |
+
|
| 23 |
+
clean_dir = os.path.join(tempfile.gettempdir(), "clean_adapter")
|
| 24 |
+
os.makedirs(clean_dir, exist_ok=True)
|
| 25 |
+
import shutil
|
| 26 |
+
for fname in os.listdir(adapter_dir):
|
| 27 |
+
src = os.path.join(adapter_dir, fname)
|
| 28 |
+
dst = os.path.join(clean_dir, fname)
|
| 29 |
+
if os.path.isfile(src):
|
| 30 |
+
if fname == "adapter_config.json":
|
| 31 |
+
with open(dst, "w") as f:
|
| 32 |
+
json.dump({"r":16,"lora_alpha":16,"lora_dropout":0,"target_modules":["q_proj","k_proj","v_proj","o_proj","gate_proj","up_proj","down_proj"],"bias":"none","task_type":"CAUSAL_LM","peft_type":"LORA","inference_mode":True}, f)
|
| 33 |
+
else:
|
| 34 |
+
shutil.copy2(src, dst)
|
| 35 |
+
|
| 36 |
+
model = AutoModelForCausalLM.from_pretrained("openbmb/MiniCPM5-1B", torch_dtype=torch.float16, trust_remote_code=True)
|
| 37 |
+
model = PeftModel.from_pretrained(model, clean_dir)
|
| 38 |
+
tokenizer = AutoTokenizer.from_pretrained("openbmb/MiniCPM5-1B", trust_remote_code=True)
|
| 39 |
+
if tokenizer.pad_token is None:
|
| 40 |
+
tokenizer.pad_token = tokenizer.eos_token
|
| 41 |
+
print(f"Ready! ({time.time()-start:.0f}s)")
|
| 42 |
+
|
| 43 |
+
ACTIONS = ["search", "open_page", "extract", "refine_search", "back", "finish"]
|
| 44 |
+
|
| 45 |
+
def predict(task, history_text):
|
| 46 |
+
if not task or not task.strip():
|
| 47 |
+
return "Error: task is empty", ""
|
| 48 |
+
history = [l.strip() for l in history_text.strip().split("\n") if l.strip()]
|
| 49 |
+
hist_str = "\n".join(history)
|
| 50 |
+
msgs = [
|
| 51 |
+
{"role": "system", "content": "You are a browser planner. First reason about the situation, then output the next action."},
|
| 52 |
+
{"role": "user", "content": f"Task: {task}\n\nHistory:\n{hist_str}\n\nWhat is the next action?"},
|
| 53 |
+
]
|
| 54 |
+
prompt = tokenizer.apply_chat_template(msgs, tokenize=False, add_generation_prompt=True)
|
| 55 |
+
inputs = tokenizer(prompt, return_tensors="pt")
|
| 56 |
+
input_len = inputs["input_ids"].shape[1]
|
| 57 |
+
inputs.pop("token_type_ids", None)
|
| 58 |
+
outs = model.generate(**inputs, max_new_tokens=64, temperature=0.01, do_sample=False,
|
| 59 |
+
pad_token_id=tokenizer.eos_token_id)
|
| 60 |
+
output = tokenizer.decode(outs[0][input_len:], skip_special_tokens=True).strip()
|
| 61 |
+
reason_m = re.search(r"Reason:\s*(.+?)(?:\n|$)", output)
|
| 62 |
+
action_m = re.search(r"Action:\s*(\S+)", output)
|
| 63 |
+
reason = reason_m.group(1).strip() if reason_m else "?"
|
| 64 |
+
action = action_m.group(1).strip().lower() if action_m else "?"
|
| 65 |
+
if action not in ["search", "open_page", "extract", "refine_search", "back", "finish"]:
|
| 66 |
+
action = f"{action} (unknown)"
|
| 67 |
+
return reason, action
|
| 68 |
+
|
| 69 |
+
with gr.Blocks(title="Tiny Browser Planner", theme=gr.themes.Soft()) as demo:
|
| 70 |
+
gr.Markdown("""
|
| 71 |
+
# Tiny Browser Planner — Reason-First
|
| 72 |
+
MiniCPM5-1B + LoRA | Actions: `search`, `open_page`, `extract`, `refine_search`, `back`, `finish`
|
| 73 |
+
""")
|
| 74 |
+
with gr.Row():
|
| 75 |
+
with gr.Column(scale=2):
|
| 76 |
+
task = gr.Textbox(label="Task", placeholder="e.g. Find Apple stock price")
|
| 77 |
+
history = gr.Textbox(label="History (one action per line)", lines=5,
|
| 78 |
+
placeholder="[search] Search completed.\n[open_page] Page content here...")
|
| 79 |
+
btn = gr.Button("Predict", variant="primary")
|
| 80 |
+
with gr.Column(scale=1):
|
| 81 |
+
reason = gr.Textbox(label="Reason", lines=3, interactive=False)
|
| 82 |
+
action = gr.Textbox(label="Next Action", lines=1, interactive=False)
|
| 83 |
+
btn.click(fn=predict, inputs=[task, history], outputs=[reason, action])
|
| 84 |
+
|
| 85 |
+
if __name__ == "__main__":
|
| 86 |
+
demo.launch()
|
requirements.txt
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
transformers>=4.30.0
|
| 2 |
+
torch>=2.0.0
|
| 3 |
+
peft>=0.6.0
|
| 4 |
+
accelerate>=0.24.0
|
| 5 |
+
safetensors>=0.4.0
|
| 6 |
+
huggingface_hub>=0.20.0,<0.25.0
|