Files
alibaba-nlp--deepresearch/WebAgent/NestBrowse/infer_async_nestbrowse.py
T
2026-07-13 13:26:09 +08:00

203 lines
8.1 KiB
Python

import re
import os
import json
import copy
import random
import asyncio
import traceback
from tqdm import tqdm
from collections import Counter
from transformers import AutoTokenizer
from prompts import *
from toolkit.tool_search import Search
from toolkit.mcp_client import mcp_client
from toolkit.browser import Visit, Click, Fill
from utils import read_jsonl, count_tokens, call_llm
async def call_tool(sem, tool_name: str, tool_args: dict, client, lock):
global tokenizer
async with sem['tool']:
if tool_name == "search":
return await search.call(tool_args)
elif tool_name == "visit":
return await visit.call(tool_args, client=client, lock=lock, tokenizer=tokenizer, sem=sem)
elif tool_name == "click":
return await click.call(tool_args, client=client, lock=lock, tokenizer=tokenizer, sem=sem)
elif tool_name == "fill":
return await fill.call(tool_args, client=client, lock=lock)
else:
await asyncio.sleep(1)
return f'Tool {tool_name} does not exist.'
async def agentic_loop(sem, data, messages):
global tokenizer
question = data['question']
answer = data['answer']
record = copy.deepcopy(messages)
summary_record = []
termination = 'max_turn_exceeded'
prediction = '[No Prediction]'
async with sem['session']:
async with mcp_client(server_url=BROWSER_SERVER_URL) as (client, lock):
for turn in range(MAX_AGENT_TURN):
if count_tokens(record, tokenizer) > MAX_AGENT_LEN:
termination = 'max_length_exceeded'
break
response = await call_llm(sem, record, int(os.getenv("MAX_SINGLE_GEN_TOKENS")), os.getenv("MODEL_NAME"))
if not response:
return {'question': question, 'answer': answer, 'prediction': prediction, 'messages': record, 'summary_record': summary_record, 'termination': 'llm_response_error'}
record.append({"role": "assistant", "content": response})
if "<tool_call>" in response and "</tool_call>" in response:
cur_summary_record = None
tool_call = response.split('<tool_call>')[-1].split('</tool_call>')[0].strip()
try:
tool_call = json.loads(tool_call)
tool_name = tool_call['name']
tool_args = tool_call['arguments']
if isinstance(tool_args, str):
tool_args = json.loads(tool_args)
print("========================================================")
print(f"Call tool {tool_name}, args: {tool_args}")
result = await call_tool(sem, tool_name, tool_args, client, lock)
if isinstance(result, tuple):
observation, cur_summary_record = result
elif isinstance(result, str):
observation = result
else:
raise Exception(f"Invalid tool result format: {result}")
if cur_summary_record:
summary_record.extend(cur_summary_record)
print("========================================================")
print(f"Call `{tool_name}`: {tool_args}")
print(f"Tool call {tool_name} invocation success with length {len(observation)}")
print(observation)
except Exception as e:
observation = 'Error: Tool call is not a valid JSON. Tool call must contain a valid "name" and "arguments" field.'
print(f"Tool call error {str(e)}")
tool_response = f"<tool_response>\n{observation}\n</tool_response>"
if "server-side error" in observation:
return {'question': question, 'answer': answer, 'prediction': prediction, 'messages': record, 'summary_record': summary_record, 'termination': 'server_side_error'}
record.append({"role": "user", "content": tool_response, "tool_name": tool_name, "tool_args": tool_args, "function_result": observation})
else:
if "<answer>" in response and "</answer>" in response:
prediction = response.split('<answer>')[-1].split('</answer>')[0].strip()
termination = 'answer'
else:
termination = 'llm_response_error'
break
return {'question': question, 'answer': answer, 'prediction': prediction, 'messages': record, 'summary_record': summary_record, 'termination': termination}
async def main(sem, rollout_count, input_path, output_path):
global tokenizer
dataset = read_jsonl(input_path)
visited_counter = Counter()
if os.path.exists(output_path):
existing_rollouts = read_jsonl(output_path)
for visited_data in existing_rollouts:
question = visited_data['question']
visited_counter[question] += 1
# submit task
tasks = []
pending_counter = Counter()
for data in dataset:
question = data.get('question')
total_count = visited_counter[question] + pending_counter[question]
need_to_submit = rollout_count - total_count if rollout_count - total_count > 0 else 0
for _ in range(need_to_submit):
messages = [
{"role": "system", "content": SYSTEM_PROMPT_OURS},
{"role": "user", "content": question}
]
tasks.append(agentic_loop(sem, data, messages))
pending_counter[question] += 1
print(f"Total number of tasks: {len(tasks)}")
# process task
with open(output_path, "a") as f:
for future in tqdm(asyncio.as_completed(tasks), total=len(tasks), desc=f"No Blocking Rollout ..."):
try:
result = await future
f.write(json.dumps(result, ensure_ascii=False) + "\n")
f.flush()
os.fsync(f.fileno())
except Exception as e:
exception_type = type(e).__name__
exception_message = str(e)
traceback_info = ''.join(traceback.format_tb(e.__traceback__))
error_message = f'{exception_type}: {exception_message}\n' \
f'Traceback:\n{traceback_info}'
print(f"[ERROR]: {error_message}")
if __name__ == '__main__':
BROWSER_SERVER_URL = "[YOUR-BROWSER-MCP-SERVER-URL]"
AGENT_LLM_BASE_URL = "http://localhost:8000/v1" # locally hosted nestbrowse model
AGENT_LLM_API_KEY = "EMPTY"
tokenizer = AutoTokenizer.from_pretrained("[TOKENIZER-PATH]")
# ========================================
rollout_count = 1
MAX_AGENT_TURN = 100
MAX_AGENT_LEN = 128 * 1024
MAX_SINGLE_GEN_TOKENS = 32 * 1024
MAX_SUMMARY_SHARD_LEN = 64 * 1024
benchmark_name = "[BENCHMARK-NAME]"
MODEL_NAME = "[CUSTOMIZED-MODEL-NAME]"
MAX_WORKERS = 16
sem = {
'session': asyncio.Semaphore(MAX_WORKERS),
'llm': asyncio.Semaphore(MAX_WORKERS),
'tool': asyncio.Semaphore(MAX_WORKERS),
}
# ========================================
os.environ["AGENT_LLM_BASE_URL"] = AGENT_LLM_BASE_URL
os.environ["AGENT_LLM_API_KEY"] = AGENT_LLM_API_KEY
os.environ["MAX_SINGLE_GEN_TOKENS"] = str(MAX_SINGLE_GEN_TOKENS)
os.environ["MAX_SUMMARY_SHARD_LEN"] = str(MAX_SUMMARY_SHARD_LEN)
os.environ["MODEL_NAME"] = MODEL_NAME
input_path = f"./data/{benchmark_name}.jsonl"
output_path = f"./results/{MODEL_NAME}_results_{benchmark_name}.jsonl"
search = Search()
visit = Visit()
click = Click()
fill = Fill()
TOOLS_SCHEMA = [search.tool_schema, visit.tool_schema, click.tool_schema, fill.tool_schema]
asyncio.run(main(sem, rollout_count, input_path, output_path))