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": }} Additionally, if you think the task is infeasible (e.g., the task is not related to the image), return \n{{\"name\": \"computer_use\", \"arguments\": {{\"action\": \"wait\", \"time\": 10}}}}\n """ 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, } def parse_coordinates(response): try: action = json.loads( response.split("\n")[1].split("\n")[0] ) action_name = action["name"] action_type = action["arguments"]["action"] print(f"action_name: {action_name}, action_type: {action_type}") if action_type == "wait": return [-1, -1, -1, -1] action_args = action["arguments"]["coordinate"] return [action_args[0], action_args[1], action_args[0], action_args[1]] except Exception as e: print(f"Error parsing coordinates: {response}") return [0, 0, 0, 0] 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), } ], } ) 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 via mouse click or wait: {instruction}", }, ], } ) payload["messages"].append( { "role": "assistant", "content": [ { "type": "text", "text": '\n{"name": "computer_use", "arguments": {"action": ', } ], } ) 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": ' + 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, annotation_path, model_name, model_path, image_dir, use_cache=False, classification_path=None, ): 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) self.classification_path = classification_path def load_annotations(self): with open(self.annotation_path, "r") as f: data = json.load(f) flatten_data_items = [] for i, item in enumerate(data): image_path = os.path.join(self.image_dir, item["image_path"]) image = Image.open(image_path) flatten_data_items.append( { "id": image_path[:-4], "annotation_id": str(i), "data_id": item["id"], "image": image, "image_path": item["image_path"], "instruction": item["instruction"], "image_size": [image.width, image.height], "box_type": item["box_type"], "box_coordinates": item["box_coordinates"], } ) 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_group = {} classification_result = {} with open( self.classification_path, "r", ) as f: classification_result = json.load(f) for item in items: instance_group_list = [] instance_id = f"{item['id']}_{item['annotation_id']}" for cls_type, classification_items in classification_result[ "classified" ].items(): for classification_item in classification_items: if classification_item["id"] == item["data_id"]: instance_group_list.append(cls_type) break item["instance_group_list"] = instance_group_list if len(instance_group_list) == 0: instance_group_list.append("unclassified") if "unclassified" not in accuracy_dict_group: accuracy_dict_group["unclassified"] = { "total": 0, "correct": 0, "accuracy": 0, } accuracy_dict_group["unclassified"]["total"] += 1 else: for instance_group in instance_group_list: 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 if instance_id in predictions_cache: cached_results.append(predictions_cache[instance_id]) 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) ): 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"] elif "polygon" == item["box_type"]: boxes_type = "polygon" boxes_coordinate = item["box_coordinates"] boxes_size = item["image_size"] image_size = item["image_size"] elif "refusal" == item["box_type"]: boxes_type = "refusal" boxes_coordinate = item["box_coordinates"] boxes_size = item["image_size"] image_size = item["image_size"] else: raise ValueError(f"Unknown box type: {item['box_type']}") # 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 for instance_group in item["instance_group_list"]: accuracy_dict_group[instance_group]["correct"] += 1 accuracy = correct / 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_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., annotations_v5.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: 'images').", ) parser.add_argument( "--use_cache", type=bool, default=False, help="Use cache (default: False)." ) parser.add_argument( "--classification_path", type=str, required=True, help="Path to the classification result file (default: 'classification_result.json').", ) # 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( annotation_path=args.annotation_path, model_name=args.model_name, model_path=args.model_path, image_dir=args.image_dir, use_cache=args.use_cache, classification_path=args.classification_path, ) 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']})" ) # Terminate VLLM service terminate_vllm_service(process)