laya-browser / code /finetune /dagger.py
cklxx's picture
laya-browser v10 / v10s: laya fine-tuned as a browser-agent decision head + code + results
adf912b verified
Raw History Blame Contribute Delete
8.18 kB
"""DAgger-style harvesting: run real tasks with the current laya agent, and at every step ask the local Qwen teacher which
action is right given the page's element table. Disagreements (and agreements) become training cases with the *agent's*
on-policy states, so the next model learns exactly where this one goes wrong.
python finetune/dagger.py out/dagger_cases.jsonl [tasks.jsonl] (services: chromium 9222, laya 8791, sglang 30000)
tasks.jsonl lines: {"url": ..., "goal": ...}; default = apps/browser_suite.TASKS plus extra goals below.
"""
import json, os, sys, time
import httpx
sys.path.insert(0, "/home/ckl/projects/S/jev-ultrafast"); sys.path.insert(0, "/home/ckl/projects/S/laya/apps")
os.environ.update(BU_CDP_URL="http://127.0.0.1:9222", TYPESAFE_BASE_URL="http://127.0.0.1:8791", TYPESAFE_API_KEY="local",
TEXT_MODEL_API_KEY="local", TEXT_MODEL_BASE_URL="http://127.0.0.1:30000/v1", TEXT_MODEL="Qwen/Qwen3-8B-AWQ",
TEXT_MODEL_EXTRA_JSON='{"chat_template_kwargs": {"enable_thinking": false}}')
from jev_ultrafast import Agent
from jev_ultrafast.model import action_space
from browser_suite import TASKS
EXTRA = [
("https://en.wikipedia.org/wiki/Main_Page", "Open the article about Albert Einstein."),
("https://news.ycombinator.com/", "Open the 'past' page."),
("https://news.ycombinator.com/", "Open the comments of the first story on the front page."),
("https://github.com/tile-ai/tilelang", "Open the Pull requests tab."),
("https://github.com/tile-ai/tilelang", "Open the README's 'examples' folder."),
("https://www.python.org/", "Open the documentation page."),
("https://docs.python.org/3/", "Open the tutorial."),
("https://books.toscrape.com/", "Open the 'Mystery' category and then open the first book in it."),
("https://books.toscrape.com/", "Go to page 2 of the catalogue."),
("https://the-internet.herokuapp.com/", "Open the 'Dropdown' example and select 'Option 2'."),
("https://the-internet.herokuapp.com/", "Open the 'Checkboxes' example and tick the first checkbox."),
("https://quotes.toscrape.com/", "Open the quotes tagged 'love'."),
("https://quotes.toscrape.com/", "Go to the next page of quotes."),
("https://arxiv.org/", "Open the listing of new submissions in cs.CL."),
("https://pypi.org/", "Search PyPI for 'tilelang' and open the project page."),
("https://duckduckgo.com/", "Search for 'ModernBERT paper'."),
("https://www.saucedemo.com/", "Log in with username 'standard_user' and password 'secret_sauce', then add the 'Sauce Labs Backpack' to the cart."),
("https://demo.opencart.com/", "Open the 'Desktops' category from the top menu."),
("https://www.demoblaze.com/", "Open the 'Laptops' category."),
("https://huggingface.co/models", "Search models for 'laya'."),
]
TEACHER = os.environ["TEXT_MODEL_BASE_URL"] + "/chat/completions"
client = httpx.Client(timeout=180)
SYS = """You are the teacher for a browser agent. You see the user's goal, the actions taken so far, the current page (title, url, text excerpt) and a
numbered table of the controls on it. Decide the single best NEXT step:
- {"operation": "CLICK", "index": n} click control n
- {"operation": "TYPE_TEXT", "index": n} type into text control n (a separate helper supplies the value)
- {"operation": "SELECT", "index": n, "value": "..."} choose that option of select control n
- {"operation": "DONE"} every requirement of the goal is already visibly satisfied on this page
- {"operation": "WAIT"} / {"operation": "SCROLL_DOWN"} / {"operation": "SCROLL_UP"}
Do not re-do satisfied steps; a field that already shows the requested value is done. Return JSON only."""
def teach(goal, history, page, elements):
table = [{"index": e["index"], "label": e["label"][:70], "role": e.get("role"), "ops": e["operations"], **({"value": e["value"]} if e.get("value") else {})} for e in elements[:120]]
user = {"goal": goal, "actions_so_far": [{k: h.get(k) for k in ("action", "kind", "text")} for h in history[-8:]],
"page": {"title": page["title"], "url": page["url"], "text": page["text"][:2500]}, "controls": table}
body = {"model": os.environ["TEXT_MODEL"], "max_tokens": 120, "temperature": 0.0, "response_format": {"type": "json_object"},
"chat_template_kwargs": {"enable_thinking": False},
"messages": [{"role": "system", "content": SYS}, {"role": "user", "content": json.dumps(user, ensure_ascii=False)}]}
r = client.post(TEACHER, json=body).json()
return json.loads(r["choices"][0]["message"]["content"])
def to_case(goal, history, page, verdict):
elements, targets, controls = action_space(page["actions"])
op = str(verdict.get("operation", "")).upper()
if op in ("DONE",):
return {"page": -1, "url": page["url"], "title": page["title"], "goal": goal, "gold_op": "DONE", "gold_id": "DONE", "kind": "done", "label": "",
"history": history, "source": "dagger", "page_obj": {k: page[k] for k in ("url", "title", "text", "actions")}}
if op in ("CLICK", "TYPE_TEXT", "SELECT"):
idx = str(verdict.get("index"))
cands = targets.get(op, {})
if op == "SELECT":
hit = [k for k, a in cands.items() if k.split(":")[0] == idx and (a["value"] == verdict.get("value") or a["label"].endswith(str(verdict.get("value"))))]
key = hit[0] if hit else None
else:
key = idx if idx in cands else None
if key is None: return None
a = cands[key]
return {"page": -1, "url": page["url"], "title": page["title"], "goal": goal, "gold_op": op, "gold_id": a["id"], "kind": a["kind"], "label": a["label"],
"history": history, "source": "dagger", "page_obj": {k: page[k] for k in ("url", "title", "text", "actions")}}
return None
def main():
out = sys.argv[1]
tasks = [(u, g) for _, u, g, _ in TASKS] + EXTRA
if len(sys.argv) > 2:
tasks += [(json.loads(l)["url"], json.loads(l)["goal"]) for l in open(sys.argv[2])]
n_cases = n_dis = 0
with open(out, "a") as f:
for url, goal in tasks:
t0 = time.time()
try:
with Agent(url, goal) as agent:
steps = 0
while agent.state["status"] not in ("done", "blocked") and steps < 12:
page = agent.state["page"]; history = list(agent.state["history"])
elements = action_space(page["actions"])[0]
try:
verdict = teach(goal, history, page, elements)
except Exception as e:
print(" teacher fail", str(e)[:60]); break
case = to_case(goal, [{k: h.get(k) for k in ("action", "kind", "text", "page_changed")} for h in history], page, verdict)
if case:
f.write(json.dumps(case, ensure_ascii=False) + "\n"); f.flush(); n_cases += 1
# step the agent with its own policy
try:
st = agent.command("tick")
except Exception as e:
print(" tick fail", type(e).__name__, str(e)[:50]); break
d = st["decisions"][-1] if st["decisions"] else None
agent_choice = (d["operation"], d.get("target")) if d else None
teacher_choice = (str(verdict.get("operation", "")).upper(), str(verdict.get("index")) if verdict.get("index") is not None else None)
if agent_choice and agent_choice[0] != teacher_choice[0] or (agent_choice and agent_choice[1] != teacher_choice[1] and teacher_choice[0] in ("CLICK", "TYPE_TEXT")):
n_dis += 1
steps += 1
except Exception as e:
print(f" task fail {type(e).__name__}: {str(e)[:60]}")
print(f"{goal[:60]:60s} cases={n_cases} disagreements={n_dis} {time.time()-t0:.0f}s", flush=True)
print("wrote", n_cases, "cases ->", out)
if __name__ == "__main__":
main()