mirror of
https://github.com/open-jarvis/OpenJarvis.git
synced 2026-08-14 08:52:06 +00:00
Previously `core/config.py` defined `DEFAULT_CONFIG_DIR = Path.home() /
".openjarvis"` as a by-value module constant imported into ~45 modules, and 34
modules hardcoded `Path.home() / ".openjarvis"` directly. The installer honored
`OPENJARVIS_HOME` but the Python runtime ignored it, producing a split-brain
layout (some modules honored the override, the core config dir did not). Eval
dataset caches also scattered into `~/.cache/<benchmark>`.
This introduces a single env-aware resolver in `openjarvis/core/paths.py` and
routes every state/config/cache path through it. OpenJarvis now keeps ALL of
its state under ONE root, resolved in priority order:
1. $OPENJARVIS_HOME
2. $XDG_DATA_HOME/openjarvis (single nested dir, when XDG_DATA_HOME is set)
3. ~/.openjarvis (default — unchanged, so existing installs are
untouched and no data migration is required)
Implementation:
- New `core/paths.py`: get_config_dir / get_config_path / get_data_dir /
get_cache_dir, with a source-tree rejection guard (fails loudly per
REVIEW.md if the root resolves inside the repo).
- `core/config.py`: DEFAULT_CONFIG_DIR / DEFAULT_CONFIG_PATH are now resolved
via the env-aware resolver at import (real attributes, so existing
monkeypatch.setattr-based tests keep working). All dataclass field defaults
that pointed at ~/.openjarvis converted to default_factory so they honor the
override at instantiation.
- Routed all 34 hardcoders plus several string-literal escapees the original
audit missed: prompt_loader / description_loader (were OPENJARVIS_HOME-only,
no XDG), swebench_harness cache, tools/{memory,skill,user_profile}_manage
defaults, server trace.db fallbacks, doctor_cmd hints.
- spec_search storage/paths now delegates to the unified resolver (gains XDG);
its ConfigurationError is aliased to the core one.
- Eval dataset caches moved from ~/.cache/<name> to <root>/cache/<name>
(~/.cache/huggingface left alone — it is HF's own cache).
- Docs + installer comment + `jarvis config path` to show resolved dirs.
Read-only macOS connectors and OS service files (LaunchAgents/systemd) are
intentionally left untouched.
Fixes #462
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
268 lines
8.9 KiB
Python
268 lines
8.9 KiB
Python
"""WebChoreArena: Realistic tedious web browsing tasks.
|
|
|
|
Evaluates web agents on 532 tasks across Shopping, Shopping Admin,
|
|
Reddit, GitLab, and Cross-site environments. Tests massive memory,
|
|
calculation, and long-term memory capabilities.
|
|
|
|
Requires a running WebArena standalone environment (Shopping/Magento,
|
|
Reddit/Postmill, GitLab, Shopping Admin). Tasks are per-site JSON configs
|
|
cloned from the original GitHub repository.
|
|
|
|
Source: https://github.com/WebChoreArena/WebChoreArena
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import logging
|
|
import os
|
|
import random
|
|
import subprocess
|
|
from pathlib import Path
|
|
from typing import Any, Dict, Iterable, List, Optional
|
|
|
|
from openjarvis.core.paths import get_cache_dir
|
|
from openjarvis.evals.core.dataset import DatasetProvider
|
|
from openjarvis.evals.core.types import EvalRecord
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
_GITHUB_REPO = "https://github.com/WebChoreArena/WebChoreArena.git"
|
|
|
|
_SITE_DIRS = {
|
|
"shopping": "test_shopping.json",
|
|
"shopping_admin": "test_shopping_admin.json",
|
|
"reddit": "test_reddit.json",
|
|
"gitlab": "test_gitlab.json",
|
|
"cross": "test_cross.raw.json",
|
|
}
|
|
|
|
|
|
class WebChoreArenaDataset(DatasetProvider):
|
|
"""WebChoreArena benchmark — interactive browser-based web tasks.
|
|
|
|
Tasks are enumerated from the original GitHub repository's
|
|
``config_files/`` JSON files. Each task requires a live WebArena
|
|
standalone environment and Playwright for evaluation.
|
|
"""
|
|
|
|
dataset_id = "webchorearena"
|
|
dataset_name = "WebChoreArena"
|
|
|
|
def __init__(
|
|
self,
|
|
subset: str = "all",
|
|
cache_dir: Optional[str] = None,
|
|
headless: bool = True,
|
|
) -> None:
|
|
self._subset = subset # "all", "small", or a site name
|
|
self._cache_dir = (
|
|
Path(cache_dir) if cache_dir else get_cache_dir() / "webchorearena"
|
|
)
|
|
self._headless = headless
|
|
self._records: List[EvalRecord] = []
|
|
|
|
def load(
|
|
self,
|
|
*,
|
|
max_samples: Optional[int] = None,
|
|
split: Optional[str] = None,
|
|
seed: Optional[int] = None,
|
|
) -> None:
|
|
repo_dir = self._cache_dir / "repo"
|
|
config_dir = repo_dir / "AgentOccam" / "config_files"
|
|
|
|
if not config_dir.exists():
|
|
self._clone_repo(repo_dir)
|
|
|
|
tasks = self._load_tasks(config_dir)
|
|
|
|
if seed is not None:
|
|
random.Random(seed).shuffle(tasks)
|
|
if max_samples is not None:
|
|
tasks = tasks[:max_samples]
|
|
|
|
self._records = [self._task_to_record(t, i) for i, t in enumerate(tasks)]
|
|
logger.info(
|
|
"WebChoreArena[%s]: loaded %d tasks",
|
|
self._subset,
|
|
len(self._records),
|
|
)
|
|
|
|
def iter_records(self) -> Iterable[EvalRecord]:
|
|
return iter(self._records)
|
|
|
|
def size(self) -> int:
|
|
return len(self._records)
|
|
|
|
def verify_requirements(self) -> List[str]:
|
|
import urllib.request
|
|
|
|
issues: List[str] = []
|
|
try:
|
|
import playwright # noqa: F401
|
|
except ImportError:
|
|
issues.append(
|
|
"playwright required for WebChoreArena. "
|
|
"Install with: pip install playwright && playwright install"
|
|
)
|
|
|
|
# Core services — eval fails without these
|
|
_REQUIRED = {
|
|
"SHOPPING": "http://localhost:7770",
|
|
"SHOPPING_ADMIN": "http://localhost:7780",
|
|
"REDDIT": "http://localhost:9999",
|
|
"GITLAB": "http://localhost:8023",
|
|
"WIKIPEDIA": "http://localhost:8888",
|
|
}
|
|
# Optional — only needed for map tasks; requires a full OpenStreetMap
|
|
# Docker Compose setup (AWS AMI or manual)
|
|
_OPTIONAL = {
|
|
"MAP": "http://localhost:3000",
|
|
}
|
|
|
|
unreachable = []
|
|
for env_var, default_url in _REQUIRED.items():
|
|
url = os.environ.get(env_var, default_url)
|
|
try:
|
|
urllib.request.urlopen(url, timeout=5)
|
|
except Exception:
|
|
unreachable.append(f" {env_var}={url}")
|
|
|
|
if unreachable:
|
|
issues.append(
|
|
"The following WebArena backend services are not reachable.\n"
|
|
"Run scripts/setup_webchorearena.sh to start them:\n"
|
|
+ "\n".join(unreachable)
|
|
)
|
|
|
|
for env_var, default_url in _OPTIONAL.items():
|
|
url = os.environ.get(env_var, default_url)
|
|
try:
|
|
urllib.request.urlopen(url, timeout=5)
|
|
except Exception:
|
|
logger.warning(
|
|
"Optional WebArena service %s (%s) is not reachable — "
|
|
"map-related tasks will fail. "
|
|
"See scripts/setup_webchorearena.sh for setup instructions.",
|
|
env_var,
|
|
url,
|
|
)
|
|
|
|
return issues
|
|
|
|
def create_task_env(self, record: EvalRecord):
|
|
"""Return a WebChoreArenaTaskEnv for the given record."""
|
|
try:
|
|
from openjarvis.evals.execution.webchorearena_env import (
|
|
WebChoreArenaTaskEnv,
|
|
)
|
|
|
|
return WebChoreArenaTaskEnv(record.metadata, headless=self._headless)
|
|
except ImportError:
|
|
return None
|
|
|
|
# ------------------------------------------------------------------
|
|
|
|
def _clone_repo(self, repo_dir: Path) -> None:
|
|
"""Clone the WebChoreArena repository from GitHub."""
|
|
repo_dir.parent.mkdir(parents=True, exist_ok=True)
|
|
logger.info("Cloning WebChoreArena from %s ...", _GITHUB_REPO)
|
|
subprocess.run(
|
|
["git", "clone", "--depth", "1", _GITHUB_REPO, str(repo_dir)],
|
|
check=True,
|
|
capture_output=True,
|
|
)
|
|
logger.info("WebChoreArena cloned to %s", repo_dir)
|
|
|
|
def _load_tasks(self, config_dir: Path) -> List[Dict[str, Any]]:
|
|
"""Load task definitions from the original config_files/ directory."""
|
|
tasks: List[Dict[str, Any]] = []
|
|
|
|
if self._subset == "small":
|
|
small_ids = self._load_small_set_ids(config_dir)
|
|
|
|
for site_key, filename in _SITE_DIRS.items():
|
|
if self._subset not in ("all", "small") and self._subset != site_key:
|
|
continue
|
|
|
|
filepath = config_dir / filename
|
|
if not filepath.exists():
|
|
logger.warning("Config file not found: %s", filepath)
|
|
continue
|
|
|
|
try:
|
|
with open(filepath) as f:
|
|
data = json.load(f)
|
|
except (json.JSONDecodeError, OSError) as exc:
|
|
logger.warning("Failed to load %s: %s", filepath, exc)
|
|
continue
|
|
|
|
items = data if isinstance(data, list) else [data]
|
|
for item in items:
|
|
if not isinstance(item, dict):
|
|
continue
|
|
if not item.get("intent") and not item.get("intent_template"):
|
|
continue
|
|
|
|
if self._subset == "small":
|
|
task_id = str(item.get("task_id", ""))
|
|
if task_id not in small_ids:
|
|
continue
|
|
|
|
tasks.append(item)
|
|
|
|
return tasks
|
|
|
|
def _load_small_set_ids(self, config_dir: Path) -> set:
|
|
"""Load small subset IDs from small_set_ids.txt."""
|
|
ids: set = set()
|
|
small_file = config_dir / "small_set_ids.txt"
|
|
if small_file.exists():
|
|
with open(small_file) as f:
|
|
for line in f:
|
|
line = line.strip()
|
|
if line:
|
|
ids.add(line)
|
|
return ids
|
|
|
|
def _task_to_record(
|
|
self,
|
|
task: Dict[str, Any],
|
|
idx: int,
|
|
) -> EvalRecord:
|
|
"""Convert an original WebChoreArena task config into an EvalRecord."""
|
|
task_id = str(task.get("task_id", idx))
|
|
intent = str(task.get("intent", task.get("intent_template", "")))
|
|
|
|
sites = task.get("sites", [])
|
|
site = sites[0].lower().replace(" ", "_") if sites else "unknown"
|
|
|
|
return EvalRecord(
|
|
record_id=f"webchorearena-{task_id}",
|
|
problem=intent,
|
|
reference="",
|
|
category="agentic",
|
|
subject=site,
|
|
metadata={
|
|
"task_id": task_id,
|
|
"task_config": task,
|
|
"site": site,
|
|
"sites": sites,
|
|
"start_url": task.get("start_url", ""),
|
|
"start_url_lite": task.get("start_url_lite", ""),
|
|
"storage_state": task.get(
|
|
"storage_state", task.get("strage_state", "")
|
|
),
|
|
"required_obs": task.get("required_obs", "text"),
|
|
"type_main": task.get("type_main", ""),
|
|
"type_sub": task.get("type_sub", ""),
|
|
"affect_environment": task.get("affect_environment", False),
|
|
"eval": task.get("eval", {}),
|
|
"headless": self._headless,
|
|
},
|
|
)
|
|
|
|
|
|
__all__ = ["WebChoreArenaDataset"]
|