import base64 import json import re import requests import math from io import BytesIO import os from concurrent.futures import ThreadPoolExecutor, as_completed from tqdm import tqdm from contextlib import redirect_stdout, redirect_stderr import sys from openai import OpenAI from PIL import Image, ImageDraw from transformers import AutoProcessor import argparse from mmrag_r1.llm_agent.qwen_tool_call import Qwen_agent import datetime sys_prompt = """\ You are a Web Information Seeking Master. Your task is to thoroughly seek the internet for information and provide accurate answers to visual questions. As you proceed, adhere to the following principles: 1. Decompose the original visual question into sub-questions and solve them step by step. Summarize the knowledge obtained from the previous round of dialogue, then think about what is next sub-question. 2. Whether you can answer the question or not, you should describe the image in detail. if the image includes multiple sub-image, you should describe each one separately. 3. You should provide the final answer within 10 turns, regardless of whether all valid information has been collected. """ prompt_ins = '''\ You are an intelligent agent engaged in a conversation with a user. The user poses a question and provides a corresponding image for context. As an agent, you approach the problem with care and methodical precision, following a multi-step process to arrive at a solution. You utilize a variety of tools, ensuring that the information gathered from each one is cross-validated before you reach a final answer. Rather than relying on any single tool for accuracy, you employ multiple tools iteratively to prioritize the comprehensiveness and reliability of your responses. { "name": "web_search", "description": "Call this tool to interact with the web_search API. You will receive the top 10 text excerpts from Google's text search engine using text as the search query.", "parameters": { "type": "object", "properties": { "queries": { "type": "array", "items": { "type": "string", "description": "The search query." }, "description": "The list of search queries." } }, "required": [ "queries" ] } }, { "name": "VLSearchImage", "description": "Call this tool to receive the top 10 images and corresponding descriptions from Google's image search engine. You can only search the input image and cannot conduct additional searches on the results obtained from the initial search. You'd better use this tool only once", "parameters": { "type": "object", "properties": { "image_urls": { "type": "array", "items": {"type": "string", "description": "The search image url."}, "description": "The list of search image url." } }, "required": [ "image_urls" ] } }, { "name": "visit", "description": "visit a webpage and return the summary of webpage.", "parameters": { "type": "object", "properties": { "url": { "type": "string", "description": "the url you want to explore." }, "goal": { "type": "string", "description": "the goal of the visit for the webpage." } }, "required": ["url","goal"] } }, { "name": "code_interpreter", "description": "Call this tool to execute Python code for calculation, data analysis, or content extraction tasks.", "parameters": { "type": "object", "properties": { "code": { "type": "string", "description": "The Python code to execute." }, "required": ["code"] } } The assistant starts with one or more cycles of (thinking about which tool to use -> performing tool call -> waiting for tool response), and ends with (thinking about the answer -> answer of the question). The thinking processes, tool calls, tool responses, and answer are enclosed within their tags. There could be multiple thinking processes, tool calls, tool call parameters and tool response parameters. Example response: thinking process here {"name": "tool name here", "arguments": {"parameter name here": parameter value here, "another parameter name here": another parameter value here, ...}} {"name": "tool name here", "content": {"result name here": result value here, "another result name here": another result value here, ...}} thinking process here {"name": "another tool name here", "arguments": {...}} {"name": "another tool name here", "content": {...}} (more thinking processes, tool calls and tool responses here) thinking process here answer here Input Question:{Question} Input image:{Image_url} ''' class OmniSearch: def __init__(self, base_url='http://0.0.0.0:8001/v1', api_key='EMPTY'): self.client = OpenAI(base_url=base_url, api_key=api_key) self.max_pixels = 1024 * 28 * 28 self.min_pixels = 256 * 28 * 28 self.repeated_nums = 1 self.max_steps = 12 self.qwen_agent = Qwen_agent(function_list=['web_search','VLSearchImage','visit','code_interpreter']) self.processor = AutoProcessor.from_pretrained(os.getenv('VLLM_MODEL', '')) def process_image(self, image): if isinstance(image, dict): image = Image.open(BytesIO(image['bytes'])) elif isinstance(image, str): image = Image.open(image) if (image.width * image.height) > self.max_pixels: resize_factor = math.sqrt(self.max_pixels / (image.width * image.height)) width, height = int(image.width * resize_factor), int(image.height * resize_factor) image = image.resize((width, height)) if (image.width * image.height) < self.min_pixels: resize_factor = math.sqrt(self.min_pixels / (image.width * image.height)) width, height = int(image.width * resize_factor), int(image.height * resize_factor) image = image.resize((width, height)) if image.mode != 'RGB': image = image.convert('RGB') byte_stream = BytesIO() image.save(byte_stream, format="JPEG") byte_array = byte_stream.getvalue() base64_encoded_image = base64.b64encode(byte_array) base64_string = base64_encoded_image.decode("utf-8") base64_qwen = f"data:image;base64,{base64_string}" return image, base64_qwen def search(self,query): if isinstance(query,str): query = [query] # search_response = requests.get(self.search_url, params={"queries": query}) search_results = search_response.json() image_path_list = [result['image_file'] for result in search_results[0]] return image_path_list def run_main(self, sample): self.image_raw = [] self.image_input = [] self.image_path = [] test_image_dir = os.getenv("IMAGE_DIR") test_image_name = os.path.basename(sample['file_path']) image_raw = Image.open(os.path.join(test_image_dir, test_image_name)) _, test_img_base64 = self.process_image(image_raw) messages = [dict( role="system", content=[ { "type": "text", "text": sys_prompt, } ] ), dict( role="user", content=[ { "type": "text", "text": prompt_ins.replace("{Image_url}", sample['file_path']).replace("{Question}", sample['prompt']), }, { 'type': 'image_url', 'image_url': { 'url': test_img_base64 } } ] )] max_steps = self.max_steps while True: ## assistant gen_times = 10 while True: if gen_times < 0: return 'time_out', messages, 'No answer' try: response = self.client.chat.completions.create( model=os.getenv('VLLM_MODEL', ''), messages=messages, stream=False, top_p=0.95, temperature=0.6 ) response_content = response.choices[0].message.content # break if response_content: break else: raise Exception('vllm model failed, retrying...') except Exception as e: print(e) gen_times -= 1 messages.append(dict( role="assistant", content=[{ "type": "text", "text": response_content }] )) ## think pattern = r'(.*?)' match = re.search(pattern, response_content, re.DOTALL) # thought = match.group(1) if match: thought = match.group(1) else: pattern = r'(.*?)\n\n' print("[no valid tag]") # print(f"messages:{messages}") print("response_content: ", response_content) ## opration pattern = r'<(tool_call|answer)>(.*?)' match = re.search(pattern, response_content, re.DOTALL) if match: raw_content = match.group(0) content = match.group(2).strip() # Return only the content inside the tags action = match.group(1) else: print(f"[No match tool_call or answer]") content = '' action = None print("action: ", action) ## whether end if action is None: user_content=[{ 'type': 'text', 'text': 'please use valid tool or answer the question.' }] elif action == 'answer': return 'answer', messages, content elif max_steps==0: return 'time_out', messages, 'No answer' elif action == 'tool_call': # request_para = None try: try: request_para = json.loads(content) print(request_para) except Exception as e: # Step 1: 修复 queries 字段中带双引号的字符串 content = re.sub(r'"\s*queries\s*"\s*:\s*\[\s*""(.*?)""\s*\]', r'"queries": ["\1"]', content) # Step 2: 如果还有 "" 替换成 " content = content.replace('""', '"') request_para = json.loads(content) if request_para is None: raise Exception(f"Invalid request parameters. request_para is None, content:{content}") img_save_path = 'scripts_eval/scripts_eval/images/search_image/' + datetime.datetime.now().strftime("%m%d") if not os.path.exists(img_save_path): os.makedirs(img_save_path) if request_para['name'] in ['VLSearchImage', 'vlsearchimage']: user_query = sample.get('prompt', '') search_results = self.qwen_agent._call_tool( request_para['name'], request_para['arguments'] ) else: search_results = self.qwen_agent._call_tool( request_para['name'], request_para['arguments'], img_save_path=img_save_path, byte=True ) print(search_results) except Exception as e: print(e, f"Invalid request parameters. request_para is None, content:{content}") request_para = None if request_para is None: user_content = [{ 'type': 'text', 'text': 'please use valid tool or answer the question.' }] elif request_para['name'] in ['VLSearchImage']: ## prefix user_content = [{ 'type': 'text', 'text': '' }] ## content images_path = re.findall(r"Image: (.*?), Text:", search_results) text_description = re.findall(r"Text: (.*?)\nImage:", search_results) text_description_last = re.findall(r"Text: (.+)$", search_results) text_description_list = text_description + text_description_last # images_path = [] if len(images_path)>0: for image_path in images_path: image_raw = Image.open(image_path) image_input, img_base64 = self.process_image(image_raw) user_content.append({ 'type': 'image_url', 'image_url': { 'url': img_base64 } }) user_content.append({ 'type': 'text', 'text': search_results }) else: user_content.append({ 'type': 'text', 'text': search_results }) ## suffix user_content.append({ 'type': 'text', 'text': '' }) elif request_para['name'] in ['web_search','visit','Visit']: user_content=[{ 'type': 'text', 'text': '' + search_results + '' }] elif request_para['name'] in ['Code_Interpreter', 'code_interpreter', 'PythonInterpreter']: if isinstance(search_results, dict): code_result = json.dumps(search_results, ensure_ascii=False) else: code_result = str(search_results) print("Generated Code: ", code_result) user_content = [{ 'type': 'text', 'text': f'{code_result}' }] else: user_content = [{ 'type': 'text', 'text': 'please use valid tool or answer the question.' }] max_steps -= 1 if max_steps == 0: user_content.append({ 'type': 'text', 'text': 'please answer the question now with answer in ... ' }) messages.append(dict( role='user', content=user_content )) def infer(self, sample): try: status, messages, content = self.run_main(sample) except Exception as e: sample["response"] = e sample["gen"] = 'No Answer' print(e) return sample text = self.processor.apply_chat_template(messages, tokenize=False, add_generation_prompt=True) sample["response"] = text sample["gen"] = content return sample def infer_with_timeout_retry(self, sample, max_retry=2, timeout_seconds=300): for attempt in range(max_retry+1): with ThreadPoolExecutor(max_workers=1) as executor: future = executor.submit(self.infer, sample) try: result = future.result(timeout=timeout_seconds) return result except TimeoutError: print(f"[Timeout] Inference timeout for sample {sample.get('file_path','N/A')}, retry {attempt+1}/{max_retry}") except Exception as e: print(f"[Exception] Inference error for sample {sample.get('file_path', 'N/A')}: {e}, retry {attempt+1}/{max_retry}") print(f"[Fail] Inference failed after {max_retry+1} attempts for sample {sample.get('file_path','N/A')}") sample['response'] = 'Timeout/Error' sample['gen'] = 'Timeout/Error' return sample def eval(self, input_file_list, output_file): data = [] for input_file in input_file_list: with open(input_file,'r') as f: data.extend([json.loads(line) for line in f]) max_workers = 20 results = [None] * len(data) with ThreadPoolExecutor(max_workers=max_workers) as executor: future_to_index = {executor.submit(self.infer_with_timeout_retry, sample): idx for idx, sample in enumerate(data)} for future in tqdm(as_completed(future_to_index), total=len(data), desc="Inference Progress"): idx = future_to_index[future] try: res = future.result() except Exception as e: print(f"[Fatal] Sample {data[idx].get('file_path','')} error: {e}") res = {'response': 'Timeout/Error', 'gen': 'Timeout/Error'} results[idx] = res with open(output_file, 'a') as f: json.dump(res, f, ensure_ascii=False) f.write('\n') if __name__ == '__main__': agent = OmniSearch() parser = argparse.ArgumentParser() parser.add_argument("--output_file", type=str, required=True) parser.add_argument("--eval_data", type=str, required=True) args = parser.parse_args() output_file = args.output_file if args.eval_data == 'hle': agent.eval(['vl_search_r1/eval_data/hle.jsonl'], output_file) elif args.eval_data == 'gaia': agent.eval(['vl_search_r1/eval_data/gaia.jsonl'], output_file) elif args.eval_data == 'livevqa': agent.eval(['vl_search_r1/eval_data/livevqa.jsonl'], output_file) elif args.eval_data == 'mmsearch': agent.eval(['vl_search_r1/eval_data/mmsearch.jsonl'], output_file) elif args.eval_data == 'simplevqa': agent.eval(['vl_search_r1/eval_data/simplevqa.jsonl'], output_file) elif args.eval_data == 'bc_vl_v1': agent.eval(['vl_search_r1/eval_data/bc_vl_v1.jsonl'], output_file) elif args.eval_data == 'bc_vl_v2': agent.eval(['vl_search_r1/eval_data/bc_vl_v2.jsonl'], output_file) else: raise ValueError('Invalid eval_data')