Files
simular-ai--agent-s/osworld_setup/s3/bbon/utils.py
T
wehub-resource-sync c8c954c85d
lint / build (3.10) (push) Failing after 1s
lint / build (3.11) (push) Failing after 1s
chore: import upstream snapshot with attribution
2026-07-13 12:23:35 +08:00

302 lines
10 KiB
Python

import logging
import os
import re
import json
from PIL import Image
from typing import Optional, List
import base64
def image_to_openai_message_format(
image_path: str, caption: str = None
) -> Optional[dict]:
"""Convert an image file to OpenAI message format."""
if not os.path.exists(image_path):
print(f"Image file not found: {image_path}")
return None
try:
with open(image_path, "rb") as f:
image_bytes = f.read()
if not image_bytes:
print(f"Empty image file: {image_path}")
return None
base64_image = base64.b64encode(image_bytes).decode("utf-8")
if not base64_image:
print(f"Failed to encode image to base64: {image_path}")
return None
content = []
if caption:
content.append({"type": "text", "text": caption})
content.append(
{
"type": "image_url",
"image_url": {"url": f"data:image/png;base64,{base64_image}"},
}
)
return {"role": "user", "content": content}
except Exception as e:
print(f"Error processing image {image_path}: {e}")
return None
def load_facts(task_dir: str) -> List[str]:
"""Load existing facts from facts.jsonl file."""
fact_captions_file = os.path.join(task_dir, "fact_captions.jsonl")
if not os.path.exists(fact_captions_file):
print(f"fact_captions.jsonl not found at {fact_captions_file}")
return []
fact_captions = []
with open(fact_captions_file, "r") as f:
for line in f:
if line.strip():
data = json.loads(line)
if "fact_answer" in data:
fact_captions.append(data["fact_answer"])
return fact_captions
def load_task_instruction(task: str, examples_path: str) -> Optional[str]:
"""
Load task instruction from examples path.
Args:
task: Task ID in format "domain/example_id"
examples_path: Path to the examples directory (e.g., "/home/ubuntu/Simular/OSWorld/evaluation_examples/examples")
Returns:
Task instruction string or None if not found
"""
domain, example_id = task.split("/", 1)
# Construct path to the JSON file
json_file_path = os.path.join(examples_path, domain, f"{example_id}.json")
if not os.path.exists(json_file_path):
logging.warning(f"Example file not found: {json_file_path}")
return None
try:
with open(json_file_path, "r", encoding="utf-8") as f:
data = json.load(f)
# Extract instruction from the JSON
if "instruction" in data:
instruction = data["instruction"]
if instruction and instruction.strip():
return instruction.strip()
logging.warning(f"No 'instruction' key found in {json_file_path}")
return None
except Exception as e:
logging.warning(f"Error reading example file {json_file_path}: {e}")
return None
def get_final_screenshot_file(result_dir: str) -> str:
"""
Finds the screenshot file with the largest valid step index in the given directory.
Works with filenames like step_0.png, step_1_20250.png, step-2.png, etc.
Only considers .png files (case-insensitive).
If the highest index file is invalid/corrupted, it tries the next lower index.
Returns None if no valid matching files are found.
"""
# First, collect all valid step files with their indices
step_files = {}
pattern = re.compile(r"step[_\-]?(\d+)", re.IGNORECASE)
for fname in os.listdir(result_dir):
if not fname.lower().endswith(".png"):
continue
match = pattern.match(fname)
if match:
idx = int(match.group(1))
step_files[idx] = fname
if not step_files:
return None
# Sort indices in descending order (highest first)
sorted_indices = sorted(step_files.keys(), reverse=True)
# Try each file from highest to lowest index
for idx in sorted_indices:
fname = step_files[idx]
file_path = os.path.join(result_dir, fname)
# Check if file exists and is valid
if os.path.exists(file_path) and is_valid_image(file_path):
return fname
else:
print(
f"Invalid or corrupted image at step {idx}: {fname}, trying previous step..."
)
return None
def is_valid_image(file_path: str) -> bool:
"""
Check if an image file is valid by trying to open it with PIL.
Also checks if file is not empty.
"""
try:
# Check file size first (quick check)
if os.path.getsize(file_path) == 0:
return False
# Try to open and verify the image
with Image.open(file_path) as img:
img.verify() # This will raise an exception if image is corrupted
return True
except Exception as e:
print(f"Image validation failed for {file_path}: {e}")
return False
def get_new_tasks_classification(results_dirs: [str]):
# Step 1: collect domain/task_ids for each trajectory
tasks_per_dir = []
for results_dir in results_dirs:
domain_tasks = set()
for domain in os.listdir(results_dir):
domain_dir = os.path.join(results_dir, domain)
if not os.path.isdir(domain_dir):
continue
for task_id in os.listdir(domain_dir):
task_dir = os.path.join(domain_dir, task_id)
if os.path.isdir(task_dir):
domain_tasks.add(f"{domain}/{task_id}")
tasks_per_dir.append(domain_tasks)
# Step 2: find tasks common to all trajectories
common_tasks = set.intersection(*tasks_per_dir)
constant_tasks = []
variance_tasks = []
constant_tasks_scores = []
optimal_sum = 0.0
expected_value = 0.0
# Step 3: evaluate each common task
for domain_task in sorted(common_tasks):
domain, task_id = domain_task.split("/", 1)
results = []
for results_dir in results_dirs:
task_dir = os.path.join(results_dir, domain, task_id)
result_file = os.path.join(task_dir, "result.txt")
if os.path.isfile(result_file):
with open(result_file, "r") as f:
try:
val = float(f.read().strip())
results.append(val)
except ValueError:
continue
if not results: # skip if no valid results
logging.warning(f"No valid results for {domain_task}")
continue
# classification
if all(r == results[0] for r in results):
constant_tasks.append(domain_task)
constant_tasks_scores.append(results[0])
else:
variance_tasks.append(domain_task)
# accumulate min/optimal
# minimum_sum += min(results) #We incorrectly also counted the minimum sum of variance tasks, we should not do this
optimal_sum += max(results)
expected_value += sum(results) / len(results)
return {
"constant": constant_tasks, # We dont evaluate constant tasks
"variance": variance_tasks, # We evaluate variance tasks
"minimum": sum(
constant_tasks_scores
), # sum of constant tasks scores (easy + hard)
"optimal": optimal_sum, # If we get the best score, we get the optimal score
"expected_value": expected_value, # If we get the average score across all tasks for all trajectories, we get the expected value
}
def check_selected_trajectory(results_dirs: [str], selected_trajectory: str, task: str):
"""
results_dirs: list of directories in format results_dir/<domain>/<task_id>
selected_trajectory: the path of the selected trajectory
task: string in format "<domain>/<task_id>"
Returns (selected_val, optimal_val)
"""
domain, task_id = task.split("/")
all_results = []
if not any(
os.path.commonpath([os.path.abspath(selected_trajectory), os.path.abspath(rd)])
== os.path.abspath(rd)
for rd in results_dirs
):
return None, None
for rd in results_dirs:
result_file = os.path.join(rd, domain, task_id, "result.txt")
if os.path.isfile(result_file):
try:
all_results.append(float(open(result_file).read().strip()))
except ValueError:
pass
selected_file = os.path.join(selected_trajectory, domain, task_id, "result.txt")
if not os.path.isfile(selected_file):
return None, max(all_results) if all_results else None
try:
selected_val = float(open(selected_file).read().strip())
except ValueError:
return None, max(all_results) if all_results else None
optimal_val = max(all_results) if all_results else selected_val
return selected_val, optimal_val
def evaluate_comparative_results(results_dirs: [str], json_path: str = None):
"""
Opens comparative_judge_results.json (default) or a given path,
evaluates each task, and returns results.
Args:
results_dirs: list of result directories
json_path: optional path to comparative_judge_results.json
Returns:
dict mapping task -> {"selected_val": float or None, "optimal_val": float or None}
"""
judge_score = 0
optimal_score = 0
if json_path is None:
json_path = "comparative_judge_results.json"
with open(json_path, "r") as f:
data = json.load(f)
results = {}
for task, info in data.items():
selected_trajectory = info.get("selected_trajectory")
if selected_trajectory:
selected_val, optimal_val = check_selected_trajectory(
results_dirs, selected_trajectory, task
)
if selected_val is not None and optimal_val is not None:
print(
f"task: {task}, selected_val: {selected_val}, optimal_val: {optimal_val}"
)
judge_score += selected_val
optimal_score += optimal_val
return judge_score, optimal_score