import base64 import json import time from io import BytesIO import os from typing import List, Tuple from loguru import logger as eval_logger from tqdm import tqdm from openai import OpenAI from concurrent.futures import ThreadPoolExecutor, as_completed from PIL import Image from lmms_eval.api.instance import Instance from lmms_eval.api.model import lmms from eval import GroundingEval import torch import subprocess import socket from transformers import Qwen2_5_VLProcessor from transformers.models.qwen2_vl.image_processing_qwen2_vl_fast import smart_resize import sys sys.path.append("../OSWorld-G") from agent_function_call import ComputerUse MAX_ATTEMPTS = 5 FN_CALL_TEMPLATE = """You are a helpful assistant. # Tools You may call one or more functions to assist with the user query. You are provided with function signatures within XML tags: {tool_descs} For each function call, return a json object with function name and arguments within XML tags: {{"name": , "arguments": }} """ NUM_SECONDS_TO_SLEEP = 5 client = OpenAI( base_url="http://localhost:8908/v1", api_key="token-abc123" ) # TODO: change to your own base_url and API key for hosting vllm service gen_kwargs = { "max_new_tokens": 1024, "temperature": 0.01, } def parse_coordinates(response): action = json.loads(response.split("\n")[1].split("\n")[0]) action_name = action["name"] action_type = action["arguments"]["action"] action_args = action["arguments"]["coordinate"] if ( action_name != "computer_use" or action_type not in ("mouse_move", "left_click", "right_click", "double_click") or action_args is None ): print(f"Error parsing coordinates: {response}") return None return [action_args[0], action_args[1], action_args[0], action_args[1]] class Qwen25VL_OpenAI(lmms): def __init__(self, model_name, model_path, **kwargs): super().__init__() self.model_name = model_name self.model_path = model_path def encode_image(self, image: Image): output_buffer = BytesIO() image.save(output_buffer, format="PNG") byte_data = output_buffer.getvalue() base64_str = base64.b64encode(byte_data).decode("utf-8") return base64_str def flatten(self, input): new_list = [] for i in input: for j in i: new_list.append(j) return new_list def generate_until(self, requests) -> List[str]: res = [] pbar = tqdm( total=len(requests), disable=(self.rank != 0), desc="Model Responding" ) def process_request(index_request): index, request = index_request instruction, tools, input_image, resized_args = ( request["instruction"], request["tools"], request["image"], request["resized_args"], ) payload = {"messages": []} tool_descs = [{"type": "function", "function": f} for f in tools] tool_descs = "\n".join( [json.dumps(f, ensure_ascii=False) for f in tool_descs] ) payload["messages"].append( { "role": "system", "content": [ { "type": "text", "text": FN_CALL_TEMPLATE.format(tool_descs=tool_descs), } ], } ) # resized_width, resized_height = resized_args print( f"original_width: {input_image.width}, original_height: {input_image.height}" ) # input_image = input_image.resize((resized_width, resized_height)) # print( # f"resized_width: {input_image.width}, resized_height: {input_image.height}" # ) payload["messages"].append( { "role": "user", "content": [ { "type": "image_url", "image_url": { "url": f"data:image/png;base64,{self.encode_image(input_image)}" }, }, { "type": "text", "text": f"Please complete the following tasks by clicking using `left_click` function: {instruction}", }, ], } ) payload["messages"].append( { "role": "assistant", "content": [ { "type": "text", "text": '\n{"name": "computer_use", "arguments": {"action": "left_click", "coordinate":', } ], } ) payload["max_tokens"] = gen_kwargs["max_new_tokens"] payload["temperature"] = gen_kwargs["temperature"] for attempt in range(MAX_ATTEMPTS): try: completion = client.chat.completions.create( model=self.model_name, messages=payload["messages"], temperature=payload["temperature"], max_tokens=payload["max_tokens"], ) response_text = completion.choices[0].message.content print("response_text: ", response_text) response_text = ( '\n{"name": "computer_use", "arguments": {"action": "left_click", "coordinate":' + response_text ) predicted_coords = parse_coordinates(response_text) if predicted_coords is None: eval_logger.info( f"Attempt {attempt + 1} failed to parse coordinates." ) if attempt < MAX_ATTEMPTS - 1: time.sleep(NUM_SECONDS_TO_SLEEP) else: break except Exception as e: eval_logger.error(f"Error during API call: {str(e)}") response_text = "" if attempt < MAX_ATTEMPTS - 1: time.sleep(NUM_SECONDS_TO_SLEEP) else: eval_logger.error( f"All {MAX_ATTEMPTS} attempts failed due to errors.\n" ) eval_logger.info(f"text_outputs: {response_text}") pbar.update(1) return index, response_text with ThreadPoolExecutor() as executor: res.extend([None] * len(requests)) futures = { executor.submit(process_request, (i, request)): i for i, request in enumerate(requests) } for future in as_completed(futures): index, response_text = future.result() res[index] = response_text pbar.close() return res def loglikelihood(self, requests: List[Instance]) -> List[Tuple[float, bool]]: raise NotImplementedError("Loglikelihood is not implemented for this model.") class BenchmarkRunner: def __init__( self, model_name, model_path, image_dir, annotation_path, use_cache=False, ): self.annotation_path = annotation_path self.model_name = model_name self.model_path = model_path self.image_dir = image_dir self.use_cache = use_cache self.model = Qwen25VL_OpenAI(model_name, model_path) self.processor = Qwen2_5_VLProcessor.from_pretrained(model_path) def load_annotations(self): data = [] file_list = [ "screenspot_desktop_v2.json", "screenspot_mobile_v2.json", "screenspot_web_v2.json", ] for file in file_list: with open(file, "r") as f: sub_data = json.load(f) data.extend(sub_data) flatten_data_items = [] for i, item in enumerate(data): image_path = os.path.join(self.image_dir, item["img_filename"]) image = Image.open(image_path) # Get instruction and coordinates flatten_data_items.append( { "id": image_path[:-4], "annotation_id": str(i), "image": image, "image_path": item["img_filename"], "instruction": item["instruction"], "image_size": [image.width, image.height], "box_type": "bbox", "box_coordinates": item["bbox"], "data_type": item["data_type"], } ) print(f"flatten_data_items: {len(flatten_data_items)}") return flatten_data_items def evaluate(self): items = self.load_annotations() evaluator = GroundingEval(None) predictions_cache = {} if self.use_cache: cache_file = ( "_".join(self.model_path.split("/")[-3:]) + self.annotation_path.replace("/", "_").replace(".json", ".cache") + "_prediction_cache_paper_prompt.json" ) if os.path.exists(cache_file): print("Loading cache file: ", cache_file) with open(cache_file, "r") as f: predictions_cache = json.load(f) else: predictions_cache = {} instances = [] idx = 0 cached_results = [] accuracy_dict_2x2 = {} accuracy_dict_ui_type = {} accuracy_dict_group = {} for item in items: instance_group = item["image_path"].split("_")[0] instance_ui_type = item["data_type"] # accuracy_dict_2x2 if instance_group not in accuracy_dict_2x2: accuracy_dict_2x2[instance_group] = {} if instance_ui_type not in accuracy_dict_2x2[instance_group]: accuracy_dict_2x2[instance_group][instance_ui_type] = { "total": 0, "correct": 0, "accuracy": 0, } accuracy_dict_2x2[instance_group][instance_ui_type]["total"] += 1 # accuracy_dict_ui_type if instance_ui_type not in accuracy_dict_ui_type: accuracy_dict_ui_type[instance_ui_type] = { "total": 0, "correct": 0, "accuracy": 0, } accuracy_dict_ui_type[instance_ui_type]["total"] += 1 # accuracy_dict_group if instance_group not in accuracy_dict_group: accuracy_dict_group[instance_group] = { "total": 0, "correct": 0, "accuracy": 0, } accuracy_dict_group[instance_group]["total"] += 1 instance_id = f"{item['id']}_{item['annotation_id']}" if instance_id in predictions_cache: cached_results.append(predictions_cache[instance_id]["response"]) continue input_image, image_path, user_query = ( item["image"], item["image_path"], item["instruction"], ) resized_height, resized_width = smart_resize( input_image.height, input_image.width, factor=self.processor.image_processor.patch_size * self.processor.image_processor.merge_size, min_pixels=self.processor.image_processor.min_pixels, max_pixels=self.processor.image_processor.max_pixels, ) computer_use = ComputerUse( cfg={ "display_width_px": resized_width, "display_height_px": resized_height, } ) tools = [computer_use.function] instance = { "instance_id": instance_id, "instruction": user_query, "tools": tools, "image": input_image, "resized_args": (resized_width, resized_height), } instances.append(instance) idx += 1 responses = self.model.generate_until(instances) if self.use_cache: for instance, response in zip(instances, responses): predictions_cache[instance["instance_id"]] = response.strip() with open(cache_file, "w") as f: json.dump(predictions_cache, f) total = len(items) correct = 0 for i, (response, item, instance) in enumerate( zip(responses, items, instances) ): instance_group = item["image_path"].split("_")[0] instance_ui_type = item["data_type"] try: predicted_coords = parse_coordinates(response) except Exception as e: eval_logger.info( f"Error parsing coordinates: {e}. The error response is: {response}" ) predicted_coords = None continue resized_width, resized_height = instance["resized_args"] if "bbox" == item["box_type"]: boxes_type = "bbox" boxes_coordinate = item["box_coordinates"][:2] boxes_size = item["box_coordinates"][2:] image_size = item["image_size"] else: boxes_type = "polygon" boxes_coordinate = item["box_coordinates"] boxes_size = item["image_size"] image_size = item["image_size"] # normalize predicted_coords -- must have, otherwise no ability to this resolution predicted_coords[0] = predicted_coords[0] * image_size[0] / resized_width predicted_coords[1] = predicted_coords[1] * image_size[1] / resized_height predicted_coords[2] = predicted_coords[2] * image_size[0] / resized_width predicted_coords[3] = predicted_coords[3] * image_size[1] / resized_height is_correct = evaluator._eval( predicted_coords, boxes_type, boxes_size, boxes_coordinate, image_size ) if is_correct: correct += 1 accuracy_dict_2x2[instance_group][instance_ui_type]["correct"] += 1 accuracy_dict_ui_type[instance_ui_type]["correct"] += 1 accuracy_dict_group[instance_group]["correct"] += 1 accuracy = correct / total for group in accuracy_dict_2x2: for ui_type in accuracy_dict_2x2[group]: accuracy_dict_2x2[group][ui_type]["accuracy"] = ( accuracy_dict_2x2[group][ui_type]["correct"] / accuracy_dict_2x2[group][ui_type]["total"] ) for ui_type in accuracy_dict_ui_type: accuracy_dict_ui_type[ui_type]["accuracy"] = ( accuracy_dict_ui_type[ui_type]["correct"] / accuracy_dict_ui_type[ui_type]["total"] ) for group in accuracy_dict_group: accuracy_dict_group[group]["accuracy"] = ( accuracy_dict_group[group]["correct"] / accuracy_dict_group[group]["total"] ) return { "total": total, "correct": correct, "accuracy": accuracy, "cached_predictions": len(predictions_cache), "accuracy_dict_2x2": accuracy_dict_2x2, "accuracy_dict_ui_type": accuracy_dict_ui_type, "accuracy_dict_group": accuracy_dict_group, } def start_vllm_service(ckpt_path, port, model_name): command = [ "vllm", "serve", ckpt_path, "--served-model-name", model_name, "--host", "0.0.0.0", "--port", str(port), "--tensor-parallel-size", str(torch.cuda.device_count()), "--enforce-eager", # "--dtype", "bfloat16", "--max-model-len", str(16384), # "--enable-auto-tool-choice", "--chat-template", "qwen25vl_tool_use_grounding.jinja", ] return subprocess.Popen(command) def wait_for_service(port, timeout=600): start_time = time.time() while time.time() - start_time < timeout: with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: result = sock.connect_ex(("localhost", port)) if result == 0: return True time.sleep(1) return False def terminate_vllm_service(process): try: process.terminate() time.sleep(5) if process.poll() is None: process.kill() time.sleep(1) except Exception as e: print(f"Failed to terminate VLLM service: {e}") import argparse if __name__ == "__main__": # Set up argument parsing parser = argparse.ArgumentParser( description="Run benchmark evaluation with custom annotation, model, and image paths." ) # Add arguments for annotation_path, model_path, and image_dir parser.add_argument( "--annotation_path", type=str, required=True, help="Path to the annotation file (e.g., screenspot_desktop_v2.json).", ) parser.add_argument( "--port", type=int, default=8908, help="Port number for the VLLM service (default: 8908).", ) parser.add_argument( "--model_name", type=str, default="qwen25vl", help="Name of the model (default: 'qwen25vl').", ) parser.add_argument( "--model_path", type=str, required=True, help="Path to the model checkpoint." ) parser.add_argument( "--image_dir", type=str, required=True, help="Directory containing images (default: 'screenspotv2_image').", ) parser.add_argument( "--use_cache", type=bool, default=False, help="Use cache (default: False)." ) # Parse the arguments args = parser.parse_args() # Start VLLM service process = start_vllm_service(args.model_path, args.port, args.model_name) # Wait for the service to start if not wait_for_service(args.port): print(f"Failed to start VLLM service on port {args.port}") terminate_vllm_service(process) else: # Initialize BenchmarkRunner and evaluate runner = BenchmarkRunner( model_name=args.model_name, model_path=args.model_path, image_dir=args.image_dir, annotation_path=args.annotation_path, use_cache=args.use_cache, ) results = runner.evaluate() print(f"Evaluation Results:") print(f"Total samples: {results['total']}") print(f"Correct predictions: {results['correct']}") print(f"Accuracy: {results['accuracy']*100:.2f}%") print(f"Accuracy by Group:") for group, stats in results["accuracy_dict_group"].items(): print( f" {group}: {stats['accuracy']*100:.2f}% ({stats['correct']}/{stats['total']})" ) print(f"Accuracy by UI Type:") for ui_type, stats in results["accuracy_dict_ui_type"].items(): print( f" {ui_type}: {stats['accuracy']*100:.2f}% ({stats['correct']}/{stats['total']})" ) print(f"Detailed Accuracy by Group and UI Type:") for group, ui_types in results["accuracy_dict_2x2"].items(): print(f" {group}:") for ui_type, stats in ui_types.items(): print( f" {ui_type}: {stats['accuracy']*100:.2f}% ({stats['correct']}/{stats['total']})" ) # Terminate VLLM service terminate_vllm_service(process)