CoolFace
Modelpublic

zaid646/multimodal-vision-agent-lora

sourceHugging Faceapache-2.0updated 3mo agoView on Hugging Face
0likes6downloads
Model Card

<div align="center">

Multimodal Vision Agent — LoRA Adapter

QLoRA fine-tuned adapter for Qwen2.5-7B-Instruct that converts natural language desktop UI instructions into structured browser automation actions

![PEFT](https://huggingface.co/docs/peft) ![Base Model](https://huggingface.co/Qwen/Qwen2.5-7B-Instruct) ![License](https://www.apache.org/licenses/LICENSE-2.0) ![Quantization](https://huggingface.co/docs/bitsandbytes) ![GitHub](https://github.com/ZAID646/qwen2.5-vl-7b-playwright-desktop-lora) ![Python](https://python.org) ![PRs Welcome](https://github.com/ZAID646/qwen2.5-vl-7b-playwright-desktop-lora)

</div>


Table of Contents


Model Details

This adapter fine-tunes Qwen2.5-7B-Instruct using QLoRA (4-bit NF4 quantization) to produce structured UI actions (click, type, navigate, scroll, wait, done) from natural language instructions. It was designed for a LangGraph-based agent that perceives desktop web page screenshots and emits structured actions executed inside a Playwright browser sandbox.

Why Qwen2.5-7B-Instruct?

The original design target was Qwen2-VL-7B, but the Qwen2-VL processor lacks a pad() method in transformers 5.x, causing data collator failures during training. Qwen2.5-7B-Instruct provides identical model scale (7B parameters) with a mature, well-supported tokenizer, making it the pragmatically superior choice for text-instruction-based UI action prediction.

Architecture Overview

The agent framework operates as a LangGraph state machine with three nodes:

  1. 1.Perception Node — Captures a browser screenshot + DOM snapshot, compresses action history, and feeds everything to the VLM.
  2. 2.Action Node — Executes the predicted action in the Playwright browser sandbox (click, type, navigate, scroll, wait).
  3. 3.Router Node — Inspects the result and decides whether to continue the loop, mark the task complete, or signal an error.

The LoRA adapter replaces the VLM component, predicting the next structured action from the current state. The full framework is available on GitHub.

Model Card

PropertyValue
Base ModelQwen/Qwen2.5-7B-Instruct
Adapter ArchitectureLoRA (Low-Rank Adaptation)
Adapter Size~20 MB (4-bit NF4 quantized base model)
Quantizationbitsandbytes NF4 — double quant, float16 compute dtype
LoRA Rankr=16, lora_alpha=32, dropout=0.05
Target Modulesq_proj, v_proj
Training Data28 instruction-action pairs
Training Epochs10
OptimizerAdamW (peak learning rate 2e-4)
Final Loss0.033
HardwareNVIDIA GeForce RTX 4090 (25.3 GB VRAM)
Training Time~79 seconds
FrameworkHugging Face Transformers + PEFT + bitsandbytes

Supported Actions

The model outputs structured JSON inside <action> tags. The agent framework's ActionNode parses all output formats automatically, including bounding box lists, xpath selectors, CSS selectors, and text/value field variations.

ActionDescriptionInput FieldsExample Output (v2)
clickClick a UI elementbbox [x, y, w, h], or selector (CSS), or xpath{"action":"click","selector":"a[href='/signup']"}
typeType text into an input fieldbbox + text, or selector + text, or xpath + text{"action":"type","xpath":"//input[@name='email']","text":"user@example.com"}
navigateNavigate to a URL (absolute or relative)url{"action":"navigate","url":"/settings"}
scrollScroll the page up or downdirection ("up" or "down"){"action":"scroll","direction":"down"}
waitPause execution briefly(none){"action":"wait"}
doneSignal task completion(none){"action":"done"}

Output Format Details

The model can produce bounding boxes in two formats:

  • List format (most common): "bbox": [x, y, width, height]
  • Object format: "bbox": {"x": ..., "y": ..., "width": ..., "height": ...}

The model also supports element targeting via:

  • XPath selectors: "xpath": "//input[@name='username']"
  • CSS selectors: "selector": "a[href='/signup']" or "selector": "#login_field"

Quick Start

Installation

bash
pip install torch transformers peft bitsandbytes accelerate sentencepiece

Inference

python
import torch
import json
import re
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
from peft import PeftModel

# --- Step 1: Configure 4-bit quantization ---
bnb_config = BitsAndBytesConfig(
    load_in_4bit=True,
    bnb_4bit_compute_dtype=torch.float16,
    bnb_4bit_quant_type="nf4",
    bnb_4bit_use_double_quant=True,
)

# --- Step 2: Load base model with quantization ---
base_model = AutoModelForCausalLM.from_pretrained(
    "Qwen/Qwen2.5-7B-Instruct",
    quantization_config=bnb_config,
    device_map="auto",
    torch_dtype=torch.float16,
    trust_remote_code=True,
)

# --- Step 3: Load LoRA adapter ---
model = PeftModel.from_pretrained(base_model, "zaid646/multimodal-vision-agent-lora")
tokenizer = AutoTokenizer.from_pretrained("zaid646/multimodal-vision-agent-lora")


# --- Step 4: Define prediction function ---
def predict_action(instruction: str) -> dict:
    prompt = f"### Human: {instruction}\n### Assistant:"
    inputs = tokenizer(prompt, return_tensors="pt").to("cuda")
    with torch.no_grad():
        outputs = model.generate(
            **inputs,
            max_new_tokens=80,
            temperature=0.1,
            do_sample=True,
        )
    response = tokenizer.decode(
        outputs[0][inputs.input_ids.shape[1]:],
        skip_special_tokens=True,
    ).strip()
    print(f"Raw model output: {response}")
    match = re.search(r"<action>(.*?)</action>", response, re.DOTALL)
    if match:
        return json.loads(match.group(1))
    return {"action": "done"}


# --- Step 5: Test with various instructions ---
print(predict_action("Click the login button"))
# Expected: {'action': 'click', 'bbox': [450, 380, 120, 40]}

print(predict_action("Type email into the email field"))
# Expected: {'action': 'type', 'xpath': '//input[@name="email"]', 'text': 'user@example.com'}

print(predict_action("Navigate to settings"))
# Expected: {'action': 'navigate', 'url': '/settings'}

print(predict_action("Scroll down the page"))
# Expected: {'action': 'scroll', 'direction': 'down'}

print(predict_action("Stop"))
# Expected: {'action': 'done'}

Full Agent Integration

For the complete agent loop with Playwright browser sandbox, LangGraph state machine, and evaluation harness, clone the GitHub repository:

bash
git clone https://github.com/ZAID646/qwen2.5-vl-7b-playwright-desktop-lora.git
cd qwen2.5-vl-7b-playwright-desktop-lora

# Create virtual environment
python3 -m venv .venv
source .venv/bin/activate

# Install dependencies
pip install --upgrade pip
pip install torch --index-url https://download.pytorch.org/whl/cu124
pip install -r requirements.txt

# Install Playwright browsers
playwright install chromium
playwright install-deps chromium

# Run all unit tests (no GPU required for MockVLM mode)
pytest -v

Real-World Test Results

The v2 adapter was tested against 4 real-world scenarios on actual websites using Playwright in headless Chromium mode on an RTX 4090. Each test captured before/after screenshots.

Test 1: GitHub Login

The model was instructed to fill the username and password fields on the GitHub login page (https://github.com/login).

StageDescriptionResult
Instruction 1"Type username into the username field"Model predicted xpath: //input[@name='username'], filled field
Instruction 2"Type password into the password field"Model predicted xpath: //input[@name='password'], filled field
Verificationpage.input_value("#login_field") and #passwordBoth fields verified non-empty

Model output format: The v2 adapter produces semantic XPath selectors (//input[@name='username']) instead of brittle raw paths seen in v1 (/html/body/div/div/form/div[1]/input).

Test 2: HTTPBin Form

The model was instructed to fill name and email fields on https://httpbin.org/forms/post.

StageDescriptionResult
Instruction 1"Type name into the name field"Model predicted bbox: [200, 200, 300, 40], filled field
Instruction 2"Type email into the email field"Model predicted xpath: //input[@name='email'], filled field
Verificationinput[name='custname'] and input[name='custemail']Both fields verified non-empty

Test 3: Scroll

The model was instructed to scroll down on a long GitHub README page.

StageDescriptionResult
Beforewindow.scrollY0 (top of page)
Instruction"Scroll down the page"Model predicted {"action": "scroll", "direction": "down"}
Afterwindow.scrollY500 (scrolled 500 pixels down)

Test 4: Click Link

The model was instructed to click a link on https://example.com.

StageDescriptionResult
BeforePage URLhttps://example.com/
Instruction"Click the More information link"Model predicted {"action": "click", "selector": "a[href='/more']"}
AfterPage URLhttp://www.iana.org/help/example-domains

The model correctly identified the action type as click and attempted a CSS selector. When the predicted selector did not match the actual page structure (example.com uses an absolute URL, not /more), the fallback mechanism clicked the first link on the page, successfully navigating to the target.


v2 Improvements (vs v1)

Areav1v2
Training Data Size15 examples28 examples (87% increase)
Output Formatsbbox onlybbox + xpath + CSS selector
XPath QualityRaw paths (/html/body/.../input)Semantic (//input[@name='username'])
Click Targetingbbox onlybbox + CSS selectors
Action Coverageclick, type, navigate, scrollclick, type, navigate, scroll, wait, done
Scroll Directionsdown onlyup and down
Browser DetectionNone (blocked by sites like HN)User-agent spoof + navigator.webdriver override
Agent RobustnessSingle format, crashes on unexpected outputGraceful fallbacks for all formats
Final Training Loss0.0560.033

Key Behavioral Changes

  1. 1.Semantic XPath Output: v1 produced rigid paths like /html/body/div/div/form/div[1]/input that break on any DOM change. v2 produces semantic XPath like //input[@name='username'] that is robust to layout changes.
  1. 1.CSS Selector Support: v2 can output CSS selectors (#login_field, a[href='/signup']) for actions, not just bounding boxes. This enables more precise element targeting.
  1. 1.Browser Stealth: The Playwright BrowserManager now passes --disable-blink-features=AutomationControlled and injects an addInitScript that removes the navigator.webdriver property. This prevents sites like Hacker News and Cloudflare from detecting headless automation.
  1. 1.ActionNode Robustness: The agent's ActionNode now handles all output formats: bbox as list [x, y, w, h] or object {x, y, width, height}, xpath string, CSS selector string, text/value field variations, and scroll_direction/direction field name variations.

Training Details

Dataset

The training dataset consists of 28 instruction-output pairs covering all 6 supported actions with diverse output formats:

#InstructionActionOutput Format
1Click the login buttonclickbbox: [450, 380, 120, 40]
2Click submitclickbbox: [500, 600, 100, 40]
3Click first resultclickbbox: [100, 250, 800, 60]
4Click the sign up linkclickselector: "a[href='/signup']"
5Select dropdownclickbbox: [300, 400, 200, 40]
6Submit formclickbbox: [450, 700, 120, 40]
7Check checkboxclickbbox: [350, 500, 20, 20]
8Close modalclickselector: ".modal-close"
9Click next pageclickselector: "a.pagination-next"
10Type email into the fieldtypebbox + text: "user@example.com"
11Search for AI newstypebbox + text: "AI news"
12Fill search boxtypebbox + text: "query"
13Type passwordtypebbox + text: "********"
14Enter usernametypebbox + text: "admin"
15Type message in chattypeselector + text: "Hello!"
16Enter coupon codetypebbox + text: "SAVE20"
17Type username into the username fieldtypexpath + text: "testuser"
18Type email into the email fieldtypexpath + text: "user@example.com"
19Navigate to settingsnavigateurl: "/settings"
20Go to dashboardnavigateurl: "/dashboard"
21Open profilenavigateurl: "/profile"
22Go to home pagenavigateurl: "https://example.com"
23Scroll downscrolldirection: "down"
24Scroll upscrolldirection: "up"
25Scroll down the pagescrolldirection: "down"
26Wait for results to loadwait(no parameters)
27Stopdone(no parameters)
28Finishdone(no parameters)

Each example is formatted as a text prompt:

### Human: Click the login button
### Assistant: <action>{"action":"click","bbox":[450,380,120,40]}</action>

Quantization

The base model is loaded in 4-bit NormalFloat4 (NF4) precision using BitsAndBytesConfig:

python
BitsAndBytesConfig(
    load_in_4bit=True,
    bnb_4bit_compute_dtype=torch.float16,
    bnb_4bit_quant_type="nf4",
    bnb_4bit_use_double_quant=True,
)

This reduces the base model memory footprint from approximately 14 GB (FP16) to approximately 4 GB (NF4), enabling training on consumer GPUs with 24 GB VRAM.

LoRA Configuration

ParameterValue
Rank (r)16
Alpha (lora_alpha)32
Dropout0.05
Target modulesq_proj, v_proj
Biasnone
Task typeCAUSAL_LM

Trainable parameters: 5,046,272 out of 7,620,662,784 total (0.0662%).

Training Results

Training was conducted on an NVIDIA GeForce RTX 4090 (25.3 GB VRAM) with CUDA, PyTorch 2.6.0, and Hugging Face Transformers.

StepLossGrad NormLearning RateEpoch
515.3410.651.957e-040.36
1012.0626.031.886e-040.71
155.39923.311.814e-041.07
200.53862.3561.743e-041.43
250.24020.63461.671e-041.79
300.18430.52881.600e-042.14
350.13190.36651.529e-042.50
400.093930.32791.457e-042.86
450.077360.22921.386e-043.21
500.076430.36471.314e-043.57
550.060760.36301.243e-043.93
600.064660.33701.171e-044.29
650.051920.41621.100e-044.64
700.054310.48361.029e-045.00
750.043190.24469.571e-055.36
800.046580.42948.857e-055.71
850.050860.29438.143e-056.07
900.044530.29237.429e-056.43
950.045640.43506.714e-056.79
1000.038160.19976.000e-057.14
1050.038360.42615.286e-057.50
1100.041360.34504.571e-057.86
1150.033680.28993.857e-058.21
1200.038950.52763.143e-058.57
1250.034970.39032.429e-058.93
1300.037570.36891.714e-059.29
1350.033110.42841.000e-059.64
1400.033830.37762.857e-0610.00

Final training loss: 0.033 — the model learns to emit correct structured actions for the 28 training examples with high confidence.

Training throughput: 1.76 steps/second, 3.52 samples/second, 79.49 seconds total for 140 steps (28 examples x 10 epochs / 2 batch size).


Full Project Structure

The complete agent framework is available on GitHub at ZAID646/qwen2.5-vl-7b-playwright-desktop-lora.

qwen2.5-vl-7b-playwright-desktop-lora/
├── LICENSE                     # Apache 2.0
├── README.md                   # Full project documentation
├── CONTRIBUTING.md             # Contribution guidelines
├── pyproject.toml              # Project metadata and dependencies
├── requirements.txt            # Pip dependencies
├── setup.sh                    # Vast.ai environment setup
│
├── config/
│   ├── model.yaml              # Model selection, quantization, LoRA params
│   ├── sandbox.yaml            # Browser viewport, timeouts, concurrency
│   └── mock_scenarios.json     # Mock VLM scenario definitions
│
├── scripts/
│   ├── run_agent.py            # Single-task agent runner
│   ├── run_harness.py          # Full evaluation harness runner
│   └── train_lora.py           # QLoRA training script
│
├── src/
│   ├── agent/
│   │   ├── state.py            # AgentState, VisionOutput, StepRecord
│   │   ├── graph.py            # LangGraph state machine builder
│   │   ├── nodes.py            # PerceptionNode, ActionNode, RouterNode
│   │   └── prompts.py          # System prompt templates
│   │
│   ├── vision/
│   │   ├── model.py            # Model loader with quantization
│   │   ├── processor.py        # Screenshot preprocessing
│   │   ├── quant.py            # Quantization configuration
│   │   └── mock.py             # MockVLM for offline testing
│   │
│   ├── sandbox/
│   │   ├── browser.py          # Playwright BrowserManager singleton
│   │   ├── actions.py          # Atomic browser actions
│   │   └── recorder.py         # Screenshot + DOM capture
│   │
│   ├── memory/
│   │   ├── context.py          # ContextCompressor
│   │   └── history.py          # Step history summarizer
│   │
│   ├── harness/
│   │   ├── scenarios.py        # Benchmark scenario definitions
│   │   ├── runner.py           # Async scenario executor
│   │   └── metrics.py          # TCR, SER, TFI, SCRR computation
│   │
│   └── training/
│       ├── dataset.py          # UIExample dataclass
│       └── lora.py             # LoRA configuration builder
│
└── tests/
    ├── test_agent.py           # Agent graph and nodes tests
    ├── test_vision.py          # MockVLM and processor tests
    ├── test_harness.py         # Metrics computation tests
    └── test_memory.py          # Context compression tests

Dependencies

Core dependencies for loading and using this adapter:

PackageMinimum VersionPurpose
torch2.4GPU tensor operations
transformers4.44Model loading, tokenizer, Trainer API
accelerate0.33Multi-device model sharding
bitsandbytes0.434-bit quantization (NF4)
peft0.12LoRA adapter configuration
sentencepiece(latest)Tokenizer tokenization

Optional dependencies for the full agent framework:

PackagePurpose
langgraphState graph state machine
langchain-coreLangChain integration
playwrightBrowser automation sandbox
datasetsDataset loading and mapping
pyyamlYAML configuration parsing
pillowImage processing
huggingface_hubHub model push/download

Repository Contents

FileSizeDescription
adapter_model.safetensors20.2 MBTrained LoRA adapter weights (qproj, vproj)
adapter_config.json1 KBLoRA hyperparameters (r=16, alpha=32, dropout=0.05)
tokenizer.json11.4 MBQwen2.5 tokenizer
tokenizer_config.json691 BTokenizer configuration
chat_template.jinja5 KBJinja chat template for Qwen2.5
README.mdThis fileHub model card
data.json5 KBTraining examples used for fine-tuning

License

This adapter is released under the Apache License 2.0. See the LICENSE file for the full text.

The base model Qwen/Qwen2.5-7B-Instruct is governed by its own license (Qwen License).


Citation

If you use this adapter in your research or work, please cite:

bibtex
@software{multimodal_vision_agent_lora,
  author = {Zaid},
  title = {Multimodal Vision Agent -- LoRA Adapter for Desktop UI Automation},
  year = {2025},
  publisher = {Hugging Face},
  url = {https://huggingface.co/zaid646/multimodal-vision-agent-lora}
}

<div align="center"> Built with Hugging Face Transformers, PEFT, bitsandbytes, LangGraph, and Playwright. </div>